Fix wrong jumppad push when entity that is to be pushed has a gravity factor differen...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / triggers / trigger / jumppads.qc
1 #include "jumppads.qh"
2 // TODO: split target_push and put it in the target folder
3 #ifdef SVQC
4 #include "jumppads.qh"
5 #include <common/physics/movetypes/movetypes.qh>
6
7 void trigger_push_use(entity this, entity actor, entity trigger)
8 {
9         if(teamplay)
10         {
11                 this.team = actor.team;
12                 this.SendFlags |= 2;
13         }
14 }
15 #endif
16
17 REGISTER_NET_LINKED(ENT_CLIENT_TRIGGER_PUSH)
18 REGISTER_NET_LINKED(ENT_CLIENT_TARGET_PUSH)
19
20 /*
21         trigger_push_calculatevelocity
22
23         Arguments:
24           org - origin of the object which is to be pushed
25           tgt - target entity (can be either a point or a model entity; if it is
26                 the latter, its midpoint is used)
27           ht  - jump height, measured from the higher one of org and tgt's midpoint
28           pushed_entity - object that is to be pushed
29
30         Returns: velocity for the jump
31         the global trigger_push_calculatevelocity_flighttime is set to the total
32         jump time
33  */
34
35 vector trigger_push_calculatevelocity(vector org, entity tgt, float ht, entity pushed_entity)
36 {
37         float grav, sdist, zdist, vs, vz, jumpheight;
38         vector sdir, torg;
39
40         torg = tgt.origin + (tgt.mins + tgt.maxs) * 0.5;
41
42         grav = PHYS_GRAVITY(NULL);
43         if(pushed_entity && PHYS_ENTGRAVITY(pushed_entity))
44                 grav *= PHYS_ENTGRAVITY(pushed_entity);
45
46         zdist = torg.z - org.z;
47         sdist = vlen(torg - org - zdist * '0 0 1');
48         sdir = normalize(torg - org - zdist * '0 0 1');
49
50         // how high do we need to push the player?
51         jumpheight = fabs(ht);
52         if(zdist > 0)
53                 jumpheight = jumpheight + zdist;
54
55         /*
56                 STOP.
57
58                 You will not understand the following equations anyway...
59                 But here is what I did to get them.
60
61                 I used the functions
62
63                   s(t) = t * vs
64                   z(t) = t * vz - 1/2 grav t^2
65
66                 and solved for:
67
68                   s(ti) = sdist
69                   z(ti) = zdist
70                   max(z, ti) = jumpheight
71
72                 From these three equations, you will find the three parameters vs, vz
73                 and ti.
74          */
75
76         // push him so high...
77         vz = sqrt(fabs(2 * grav * jumpheight)); // NOTE: sqrt(positive)!
78
79         // we start with downwards velocity only if it's a downjump and the jump apex should be outside the jump!
80         if(ht < 0)
81                 if(zdist < 0)
82                         vz = -vz;
83
84         vector solution;
85         solution = solve_quadratic(0.5 * grav, -vz, zdist); // equation "z(ti) = zdist"
86         // ALWAYS solvable because jumpheight >= zdist
87         if(!solution.z)
88                 solution_y = solution.x; // just in case it is not solvable due to roundoff errors, assume two equal solutions at their center (this is mainly for the usual case with ht == 0)
89         if(zdist == 0)
90                 solution_x = solution.y; // solution_x is 0 in this case, so don't use it, but rather use solution_y (which will be sqrt(0.5 * jumpheight / grav), actually)
91
92         if(zdist < 0)
93         {
94                 // down-jump
95                 if(ht < 0)
96                 {
97                         // almost straight line type
98                         // jump apex is before the jump
99                         // we must take the larger one
100                         trigger_push_calculatevelocity_flighttime = solution.y;
101                 }
102                 else
103                 {
104                         // regular jump
105                         // jump apex is during the jump
106                         // we must take the larger one too
107                         trigger_push_calculatevelocity_flighttime = solution.y;
108                 }
109         }
110         else
111         {
112                 // up-jump
113                 if(ht < 0)
114                 {
115                         // almost straight line type
116                         // jump apex is after the jump
117                         // we must take the smaller one
118                         trigger_push_calculatevelocity_flighttime = solution.x;
119                 }
120                 else
121                 {
122                         // regular jump
123                         // jump apex is during the jump
124                         // we must take the larger one
125                         trigger_push_calculatevelocity_flighttime = solution.y;
126                 }
127         }
128         vs = sdist / trigger_push_calculatevelocity_flighttime;
129
130         // finally calculate the velocity
131         return sdir * vs + '0 0 1' * vz;
132 }
133
134 bool jumppad_push(entity this, entity targ)
135 {
136         if (!isPushable(targ))
137                 return false;
138
139         if(this.enemy)
140         {
141                 targ.velocity = trigger_push_calculatevelocity(targ.origin, this.enemy, this.height, targ);
142         }
143         else if(this.target && this.target != "")
144         {
145                 entity e;
146                 RandomSelection_Init();
147                 for(e = NULL; (e = find(e, targetname, this.target)); )
148                 {
149                         if(e.cnt)
150                                 RandomSelection_AddEnt(e, e.cnt, 1);
151                         else
152                                 RandomSelection_AddEnt(e, 1, 1);
153                 }
154                 targ.velocity = trigger_push_calculatevelocity(targ.origin, RandomSelection_chosen_ent, this.height, targ);
155         }
156         else
157         {
158                 targ.velocity = this.movedir;
159         }
160
161         UNSET_ONGROUND(targ);
162
163 #ifdef CSQC
164         if (targ.flags & FL_PROJECTILE)
165         {
166                 targ.angles = vectoangles (targ.velocity);
167                 switch(targ.move_movetype)
168                 {
169                         case MOVETYPE_FLY:
170                                 set_movetype(targ, MOVETYPE_TOSS);
171                                 targ.gravity = 1;
172                                 break;
173                         case MOVETYPE_BOUNCEMISSILE:
174                                 set_movetype(targ, MOVETYPE_BOUNCE);
175                                 targ.gravity = 1;
176                                 break;
177                 }
178         }
179 #endif
180
181 #ifdef SVQC
182         if (IS_PLAYER(targ))
183         {
184                 // reset tracking of oldvelocity for impact damage (sudden velocity changes)
185                 targ.oldvelocity = targ.velocity;
186
187                 if(this.pushltime < time)  // prevent "snorring" sound when a player hits the jumppad more than once
188                 {
189                         // flash when activated
190                         Send_Effect(EFFECT_JUMPPAD, targ.origin, targ.velocity, 1);
191                         _sound (targ, CH_TRIGGER, this.noise, VOL_BASE, ATTEN_NORM);
192                         this.pushltime = time + 0.2;
193                 }
194                 if(IS_REAL_CLIENT(targ) || IS_BOT_CLIENT(targ))
195                 {
196                         bool found = false;
197                         for(int i = 0; i < targ.jumppadcount && i < NUM_JUMPPADSUSED; ++i)
198                                 if(targ.(jumppadsused[i]) == this)
199                                         found = true;
200                         if(!found)
201                         {
202                                 targ.(jumppadsused[targ.jumppadcount % NUM_JUMPPADSUSED]) = this;
203                                 targ.jumppadcount = targ.jumppadcount + 1;
204                         }
205
206                         if(IS_REAL_CLIENT(targ))
207                         {
208                                 if(this.message)
209                                         centerprint(targ, this.message);
210                         }
211                         else
212                                 targ.lastteleporttime = time;
213
214                         if (!IS_DEAD(targ))
215                                 animdecide_setaction(targ, ANIMACTION_JUMP, true);
216                 }
217                 else
218                         targ.jumppadcount = true;
219
220                 // reset tracking of who pushed you into a hazard (for kill credit)
221                 targ.pushltime = 0;
222                 targ.istypefrag = 0;
223         }
224
225         if(this.enemy.target)
226                 SUB_UseTargets(this.enemy, targ, this);
227
228         if (targ.flags & FL_PROJECTILE)
229         {
230                 targ.angles = vectoangles (targ.velocity);
231                 targ.com_phys_gravity_factor = 1;
232                 switch(targ.move_movetype)
233                 {
234                         case MOVETYPE_FLY:
235                                 set_movetype(targ, MOVETYPE_TOSS);
236                                 targ.gravity = 1;
237                                 break;
238                         case MOVETYPE_BOUNCEMISSILE:
239                                 set_movetype(targ, MOVETYPE_BOUNCE);
240                                 targ.gravity = 1;
241                                 break;
242                 }
243                 UpdateCSQCProjectile(targ);
244         }
245 #endif
246
247         return true;
248 }
249
250 void trigger_push_touch(entity this, entity toucher)
251 {
252         if (this.active == ACTIVE_NOT)
253                 return;
254
255         if(this.team)
256                 if(((this.spawnflags & 4) == 0) == (DIFF_TEAM(this, toucher)))
257                         return;
258
259         EXACTTRIGGER_TOUCH(this, toucher);
260
261         noref bool success = jumppad_push(this, toucher);
262
263 #ifdef SVQC
264         if (success && (this.spawnflags & PUSH_ONCE))
265         {
266                 settouch(this, func_null);
267                 setthink(this, SUB_Remove);
268                 this.nextthink = time;
269         }
270 #endif
271 }
272
273 #ifdef SVQC
274 void trigger_push_link(entity this);
275 void trigger_push_updatelink(entity this);
276 #endif
277 void trigger_push_findtarget(entity this)
278 {
279         // first calculate a typical start point for the jump
280         vector org = (this.absmin + this.absmax) * 0.5;
281         org.z = this.absmax.z - PL_MIN_CONST.z;
282
283         if (this.target)
284         {
285                 int n = 0;
286                 for(entity t = NULL; (t = find(t, targetname, this.target)); )
287                 {
288                         ++n;
289 #ifdef SVQC
290                         entity e = spawn();
291                         setorigin(e, org);
292                         setsize(e, PL_MIN_CONST, PL_MAX_CONST);
293                         e.velocity = trigger_push_calculatevelocity(org, t, this.height, e);
294                         tracetoss(e, e);
295                         if(e.move_movetype == MOVETYPE_NONE)
296                                 waypoint_spawnforteleporter(this, trace_endpos, vlen(trace_endpos - org) / vlen(e.velocity));
297                         delete(e);
298 #endif
299                 }
300
301                 if(!n)
302                 {
303                         // no dest!
304 #ifdef SVQC
305                         objerror (this, "Jumppad with nonexistant target");
306 #endif
307                         return;
308                 }
309                 else if(n == 1)
310                 {
311                         // exactly one dest - bots love that
312                         this.enemy = find(NULL, targetname, this.target);
313                 }
314                 else
315                 {
316                         // have to use random selection every single time
317                         this.enemy = NULL;
318                 }
319         }
320 #ifdef SVQC
321         else
322         {
323                 entity e = spawn();
324                 setorigin(e, org);
325                 setsize(e, PL_MIN_CONST, PL_MAX_CONST);
326                 e.velocity = this.movedir;
327                 tracetoss(e, e);
328                 waypoint_spawnforteleporter(this, trace_endpos, vlen(trace_endpos - org) / vlen(e.velocity));
329                 delete(e);
330         }
331
332         defer(this, 0.1, trigger_push_updatelink);
333 #endif
334 }
335
336 #ifdef SVQC
337 float trigger_push_send(entity this, entity to, float sf)
338 {
339         WriteHeader(MSG_ENTITY, ENT_CLIENT_TRIGGER_PUSH);
340
341         WriteByte(MSG_ENTITY, this.team);
342         WriteInt24_t(MSG_ENTITY, this.spawnflags);
343         WriteByte(MSG_ENTITY, this.active);
344         WriteCoord(MSG_ENTITY, this.height);
345
346         WriteCoord(MSG_ENTITY, this.movedir_x);
347         WriteCoord(MSG_ENTITY, this.movedir_y);
348         WriteCoord(MSG_ENTITY, this.movedir_z);
349
350         trigger_common_write(this, true);
351
352         return true;
353 }
354
355 void trigger_push_updatelink(entity this)
356 {
357         this.SendFlags |= 1;
358 }
359
360 void trigger_push_link(entity this)
361 {
362         trigger_link(this, trigger_push_send);
363 }
364
365 /*
366  * ENTITY PARAMETERS:
367  *
368  *   target:  target of jump
369  *   height:  the absolute value is the height of the highest point of the jump
370  *            trajectory above the higher one of the player and the target.
371  *            the sign indicates whether the highest point is INSIDE (positive)
372  *            or OUTSIDE (negative) of the jump trajectory. General rule: use
373  *            positive values for targets mounted on the floor, and use negative
374  *            values to target a point on the ceiling.
375  *   movedir: if target is not set, this * speed * 10 is the velocity to be reached.
376  */
377 spawnfunc(trigger_push)
378 {
379         SetMovedir(this);
380
381         trigger_init(this);
382
383         this.active = ACTIVE_ACTIVE;
384         this.use = trigger_push_use;
385         settouch(this, trigger_push_touch);
386
387         // normal push setup
388         if (!this.speed)
389                 this.speed = 1000;
390         this.movedir = this.movedir * this.speed * 10;
391
392         if (!this.noise)
393                 this.noise = "misc/jumppad.wav";
394         precache_sound (this.noise);
395
396         trigger_push_link(this); // link it now
397
398         // this must be called to spawn the teleport waypoints for bots
399         InitializeEntity(this, trigger_push_findtarget, INITPRIO_FINDTARGET);
400 }
401
402
403 bool target_push_send(entity this, entity to, float sf)
404 {
405         WriteHeader(MSG_ENTITY, ENT_CLIENT_TARGET_PUSH);
406
407         WriteByte(MSG_ENTITY, this.cnt);
408         WriteString(MSG_ENTITY, this.targetname);
409         WriteCoord(MSG_ENTITY, this.origin_x);
410         WriteCoord(MSG_ENTITY, this.origin_y);
411         WriteCoord(MSG_ENTITY, this.origin_z);
412
413         WriteAngle(MSG_ENTITY, this.angles_x);
414         WriteAngle(MSG_ENTITY, this.angles_y);
415         WriteAngle(MSG_ENTITY, this.angles_z);
416
417         return true;
418 }
419
420 void target_push_use(entity this, entity actor, entity trigger)
421 {
422         if(trigger.classname == "trigger_push" || trigger == this)
423                 return; // WTF, why is this a thing
424
425         jumppad_push(this, actor);
426 }
427
428 void target_push_link(entity this)
429 {
430         BITSET_ASSIGN(this.effects, EF_NODEPTHTEST);
431         Net_LinkEntity(this, false, 0, target_push_send);
432         //this.SendFlags |= 1; // update
433 }
434
435 void target_push_init(entity this)
436 {
437         this.mangle = this.angles;
438         setorigin(this, this.origin);
439         target_push_link(this);
440 }
441
442 void target_push_init2(entity this)
443 {
444         if(this.target && this.target != "") // we have an old style pusher!
445         {
446                 InitializeEntity(this, trigger_push_findtarget, INITPRIO_FINDTARGET);
447                 this.use = target_push_use;
448         }
449
450         target_push_init(this); // normal push target behaviour can be combined with a legacy pusher?
451 }
452
453 spawnfunc(target_push) { target_push_init2(this); }
454 spawnfunc(info_notnull) { target_push_init(this); }
455 spawnfunc(target_position) { target_push_init(this); }
456
457 #elif defined(CSQC)
458
459 NET_HANDLE(ENT_CLIENT_TRIGGER_PUSH, bool isnew)
460 {
461         this.classname = "jumppad";
462         int mytm = ReadByte(); if(mytm) { this.team = mytm - 1; }
463         this.spawnflags = ReadInt24_t();
464         this.active = ReadByte();
465         this.height = ReadCoord();
466
467         this.movedir_x = ReadCoord();
468         this.movedir_y = ReadCoord();
469         this.movedir_z = ReadCoord();
470
471         trigger_common_read(this, true);
472
473         this.entremove = trigger_remove_generic;
474         this.solid = SOLID_TRIGGER;
475         settouch(this, trigger_push_touch);
476         this.move_time = time;
477         defer(this, 0.25, trigger_push_findtarget);
478
479         return true;
480 }
481
482 void target_push_remove(entity this)
483 {
484         //if(this.classname)
485                 //strunzone(this.classname);
486         //this.classname = string_null;
487
488         if(this.targetname)
489                 strunzone(this.targetname);
490         this.targetname = string_null;
491 }
492
493 NET_HANDLE(ENT_CLIENT_TARGET_PUSH, bool isnew)
494 {
495         this.classname = "push_target";
496         this.cnt = ReadByte();
497         this.targetname = strzone(ReadString());
498         this.origin_x = ReadCoord();
499         this.origin_y = ReadCoord();
500         this.origin_z = ReadCoord();
501
502         this.angles_x = ReadAngle();
503         this.angles_y = ReadAngle();
504         this.angles_z = ReadAngle();
505
506         return = true;
507
508         setorigin(this, this.origin);
509
510         this.drawmask = MASK_NORMAL;
511         this.entremove = target_push_remove;
512 }
513 #endif