]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/bot/default/havocbot/havocbot.qc
Move some weapon definitions out of defs.qh, fix compilation units test
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / bot / default / havocbot / havocbot.qc
1 #include "havocbot.qh"
2
3 #include "roles.qh"
4
5 #include <server/defs.qh>
6 #include <server/miscfunctions.qh>
7 #include <server/weapons/selection.qh>
8 #include <server/weapons/weaponsystem.qh>
9 #include "../cvars.qh"
10
11 #include "../aim.qh"
12 #include "../bot.qh"
13 #include "../navigation.qh"
14 #include "../scripting.qh"
15 #include "../waypoints.qh"
16
17 #include <common/constants.qh>
18 #include <common/impulses/all.qh>
19 #include <common/net_linked.qh>
20 #include <common/physics/player.qh>
21 #include <common/state.qh>
22 #include <common/items/_mod.qh>
23 #include <common/wepent.qh>
24
25 #include <common/mapobjects/func/ladder.qh>
26 #include <common/mapobjects/teleporters.qh>
27 #include <common/mapobjects/trigger/jumppads.qh>
28
29 #include <lib/warpzone/common.qh>
30
31 void havocbot_ai(entity this)
32 {
33         if(this.draggedby)
34                 return;
35
36         this.bot_aimdir_executed = false;
37         // lock aim if teleported or passing through a warpzone
38         if (this.lastteleporttime && !this.jumppadcount)
39                 this.bot_aimdir_executed = true;
40
41         if(bot_execute_commands(this))
42                 return;
43
44         if (bot_strategytoken == this && !bot_strategytoken_taken)
45         {
46                 if(this.havocbot_blockhead)
47                 {
48                         this.havocbot_blockhead = false;
49                 }
50                 else
51                 {
52                         if (!this.jumppadcount && !STAT(FROZEN, this)
53                                 && !(this.goalcurrent_prev && (this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP) && !IS_ONGROUND(this)))
54                         {
55                                 // find a new goal
56                                 this.havocbot_role(this); // little too far down the rabbit hole
57                         }
58                 }
59
60                 // if we don't have a goal and we're under water look for a waypoint near the "shore" and push it
61                 if(!(IS_DEAD(this) || STAT(FROZEN, this)))
62                 if(!this.goalcurrent)
63                 if(this.waterlevel == WATERLEVEL_SWIMMING || (this.aistatus & AI_STATUS_OUT_WATER))
64                 {
65                         // Look for the closest waypoint out of water
66                         entity newgoal = NULL;
67                         IL_EACH(g_waypoints, vdist(it.origin - this.origin, <=, 10000),
68                         {
69                                 if(it.origin.z < this.origin.z)
70                                         continue;
71
72                                 if(it.origin.z - this.origin.z - this.view_ofs.z > 100)
73                                         continue;
74
75                                 if (pointcontents(it.origin + it.maxs + '0 0 1') != CONTENT_EMPTY)
76                                         continue;
77
78                                 traceline(this.origin + this.view_ofs, ((it.absmin + it.absmax) * 0.5), true, this);
79
80                                 if(trace_fraction < 1)
81                                         continue;
82
83                                 if(!newgoal || vlen2(it.origin - this.origin) < vlen2(newgoal.origin - this.origin))
84                                         newgoal = it;
85                         });
86
87                         if(newgoal)
88                         {
89                         //      te_wizspike(newgoal.origin);
90                                 navigation_pushroute(this, newgoal);
91                         }
92                 }
93
94                 // token has been used this frame
95                 bot_strategytoken_taken = true;
96         }
97
98         if (this.goalcurrent && wasfreed(this.goalcurrent))
99         {
100                 navigation_clearroute(this);
101                 navigation_goalrating_timeout_force(this);
102                 return;
103         }
104
105         if(IS_DEAD(this) || STAT(FROZEN, this))
106         {
107                 if (this.goalcurrent)
108                         navigation_clearroute(this);
109                 this.enemy = NULL;
110                 this.bot_aimtarg = NULL;
111                 return;
112         }
113
114         havocbot_chooseenemy(this);
115
116         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
117         {
118                 .entity weaponentity = weaponentities[slot];
119                 if(this.(weaponentity).m_weapon != WEP_Null || slot == 0)
120                 if(this.(weaponentity).bot_chooseweapontime < time)
121                 {
122                         this.(weaponentity).bot_chooseweapontime = time + autocvar_bot_ai_chooseweaponinterval;
123                         havocbot_chooseweapon(this, weaponentity);
124                 }
125         }
126         havocbot_aim(this);
127         lag_update(this);
128
129         if (this.bot_aimtarg)
130         {
131                 this.aistatus |= AI_STATUS_ATTACKING;
132                 this.aistatus &= ~AI_STATUS_ROAMING;
133
134                 if(STAT(WEAPONS, this))
135                 {
136                         if (autocvar_bot_nofire || IS_INDEPENDENT_PLAYER(this))
137                         {
138                                 PHYS_INPUT_BUTTON_ATCK(this) = false;
139                                 PHYS_INPUT_BUTTON_ATCK2(this) = false;
140                         }
141                         else
142                         {
143                                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
144                                 {
145                                         .entity weaponentity = weaponentities[slot];
146                                         Weapon w = this.(weaponentity).m_weapon;
147                                         if(w == WEP_Null && slot != 0)
148                                                 continue;
149                                         w.wr_aim(w, this, weaponentity);
150                                         if(PHYS_INPUT_BUTTON_ATCK(this) || PHYS_INPUT_BUTTON_ATCK2(this)) // TODO: what if we didn't fire this weapon, but the previous?
151                                                 this.(weaponentity).lastfiredweapon = this.(weaponentity).m_weapon.m_id;
152                                 }
153                         }
154                 }
155                 else
156                 {
157                         if(IS_PLAYER(this.bot_aimtarg))
158                                 bot_aimdir(this, this.bot_aimtarg.origin + this.bot_aimtarg.view_ofs - this.origin - this.view_ofs, 0);
159                 }
160         }
161         else if (this.goalcurrent)
162         {
163                 this.aistatus |= AI_STATUS_ROAMING;
164                 this.aistatus &= ~AI_STATUS_ATTACKING;
165         }
166
167         havocbot_movetogoal(this);
168         if (!this.bot_aimdir_executed && this.goalcurrent)
169         {
170                 // Heading
171                 vector dir = get_closer_dest(this.goalcurrent, this.origin);
172                 dir -= this.origin + this.view_ofs;
173                 dir.z = 0;
174                 bot_aimdir(this, dir, 0);
175         }
176
177         // if the bot is not attacking, consider reloading weapons
178         if (!(this.aistatus & AI_STATUS_ATTACKING))
179         {
180                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
181                 {
182                         .entity weaponentity = weaponentities[slot];
183
184                         if(this.(weaponentity).m_weapon == WEP_Null && slot != 0)
185                                 continue;
186
187                         // we are currently holding a weapon that's not fully loaded, reload it
188                         if(skill >= 2) // bots can only reload the held weapon on purpose past this skill
189                         if(this.(weaponentity).clip_load < this.(weaponentity).clip_size)
190                                 CS(this).impulse = IMP_weapon_reload.impulse; // not sure if this is done right
191
192                         // if we're not reloading a weapon, switch to any weapon in our invnetory that's not fully loaded to reload it next
193                         // the code above executes next frame, starting the reloading then
194                         if(skill >= 5) // bots can only look for unloaded weapons past this skill
195                         if(this.(weaponentity).clip_load >= 0) // only if we're not reloading a weapon already
196                         {
197                                 FOREACH(Weapons, it != WEP_Null, {
198                                         if((STAT(WEAPONS, this) & (it.m_wepset)) && (it.spawnflags & WEP_FLAG_RELOADABLE) && (this.(weaponentity).weapon_load[it.m_id] < it.reloading_ammo))
199                                         {
200                                                 this.(weaponentity).m_switchweapon = it;
201                                                 break;
202                                         }
203                                 });
204                         }
205                 }
206         }
207 }
208
209 void havocbot_bunnyhop(entity this, vector dir)
210 {
211         bool can_run = false;
212         if (!(this.aistatus & AI_STATUS_ATTACKING) && this.goalcurrent && !IS_PLAYER(this.goalcurrent)
213                 && vdist(vec2(this.velocity), >=, autocvar_sv_maxspeed) && !(this.aistatus & AI_STATUS_DANGER_AHEAD)
214                 && this.waterlevel <= WATERLEVEL_WETFEET && !IS_DUCKED(this)
215                 && IS_ONGROUND(this) && !(this.goalcurrent_prev && (this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP)))
216         {
217                 vector vel_angles = vectoangles(this.velocity);
218                 vector deviation = vel_angles - vectoangles(dir);
219                 while (deviation.y < -180) deviation.y = deviation.y + 360;
220                 while (deviation.y > 180) deviation.y = deviation.y - 360;
221                 if (fabs(deviation.y) < autocvar_bot_ai_bunnyhop_dir_deviation_max)
222                 {
223                         vector gco = get_closer_dest(this.goalcurrent, this.origin);
224                         float vel = vlen(vec2(this.velocity));
225
226                         // with the current physics, jump distance grows linearly with the speed
227                         float jump_distance = 52.661 + 0.606 * vel;
228                         jump_distance += this.origin.z - gco.z; // roughly take into account vertical distance too
229                         if (vdist(vec2(gco - this.origin), >, max(0, jump_distance)))
230                                 can_run = true;
231                         else if (!(this.goalcurrent.wpflags & WAYPOINTFLAG_JUMP)
232                                 && !(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
233                                 && this.goalstack01 && !wasfreed(this.goalstack01) && !(this.goalstack01.wpflags & WAYPOINTFLAG_JUMP)
234                                 && vdist(vec2(gco - this.goalstack01.origin), >, 70))
235                         {
236                                 vector gno = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5;
237                                 vector ang = vectoangles(gco - this.origin);
238                                 deviation = vectoangles(gno - gco) - vel_angles;
239                                 while (deviation.y < -180) deviation.y = deviation.y + 360;
240                                 while (deviation.y > 180) deviation.y = deviation.y - 360;
241
242                                 float max_turn_angle = autocvar_bot_ai_bunnyhop_turn_angle_max;
243                                 max_turn_angle -= autocvar_bot_ai_bunnyhop_turn_angle_reduction * ((vel - autocvar_sv_maxspeed) / autocvar_sv_maxspeed);
244                                 if ((ang.x < 90 || ang.x > 360 - autocvar_bot_ai_bunnyhop_downward_pitch_max)
245                                         && fabs(deviation.y) < max(autocvar_bot_ai_bunnyhop_turn_angle_min, max_turn_angle))
246                                 {
247                                         can_run = true;
248                                 }
249                         }
250                 }
251         }
252
253         if (can_run)
254         {
255                 PHYS_INPUT_BUTTON_JUMP(this) = true;
256                 this.bot_jump_time = time;
257                 this.aistatus |= AI_STATUS_RUNNING;
258         }
259         else
260         {
261                 if (IS_ONGROUND(this) || this.waterlevel > WATERLEVEL_WETFEET)
262                         this.aistatus &= ~AI_STATUS_RUNNING;
263         }
264 }
265
266 void havocbot_keyboard_movement(entity this, vector destorg)
267 {
268         if(time <= this.havocbot_keyboardtime)
269                 return;
270
271         float sk = skill + this.bot_moveskill;
272         this.havocbot_keyboardtime =
273                 max(
274                         this.havocbot_keyboardtime
275                                 + 0.05 / max(1, sk + this.havocbot_keyboardskill)
276                                 + random() * 0.025 / max(0.00025, skill + this.havocbot_keyboardskill)
277                 , time);
278         vector keyboard = CS(this).movement / autocvar_sv_maxspeed;
279
280         float trigger = autocvar_bot_ai_keyboard_threshold;
281
282         // categorize forward movement
283         // at skill < 1.5 only forward
284         // at skill < 2.5 only individual directions
285         // at skill < 4.5 only individual directions, and forward diagonals
286         // at skill >= 4.5, all cases allowed
287         if (keyboard.x > trigger)
288         {
289                 keyboard.x = 1;
290                 if (sk < 2.5)
291                         keyboard.y = 0;
292         }
293         else if (keyboard.x < -trigger && sk > 1.5)
294         {
295                 keyboard.x = -1;
296                 if (sk < 4.5)
297                         keyboard.y = 0;
298         }
299         else
300         {
301                 keyboard.x = 0;
302                 if (sk < 1.5)
303                         keyboard.y = 0;
304         }
305         if (sk < 4.5)
306                 keyboard.z = 0;
307
308         if (keyboard.y > trigger)
309                 keyboard.y = 1;
310         else if (keyboard.y < -trigger)
311                 keyboard.y = -1;
312         else
313                 keyboard.y = 0;
314
315         if (keyboard.z > trigger)
316                 keyboard.z = 1;
317         else if (keyboard.z < -trigger)
318                 keyboard.z = -1;
319         else
320                 keyboard.z = 0;
321
322         // make sure bots don't get stuck if havocbot_keyboardtime is very high
323         if (keyboard == '0 0 0')
324                 this.havocbot_keyboardtime = min(this.havocbot_keyboardtime, time + 0.2);
325
326         this.havocbot_keyboard = keyboard * autocvar_sv_maxspeed;
327         if (this.havocbot_ducktime > time)
328                 PHYS_INPUT_BUTTON_CROUCH(this) = true;
329
330         keyboard = this.havocbot_keyboard;
331         float blend = bound(0, vlen(destorg - this.origin) / autocvar_bot_ai_keyboard_distance, 1); // When getting close move with 360 degree
332         //dprint("movement ", vtos(CS(this).movement), " keyboard ", vtos(keyboard), " blend ", ftos(blend), "\n");
333         CS(this).movement = CS(this).movement + (keyboard - CS(this).movement) * blend;
334 }
335
336 // return true when bot isn't getting closer to the current goal
337 bool havocbot_checkgoaldistance(entity this, vector gco)
338 {
339         if (this.bot_stop_moving_timeout > time)
340                 return false;
341         float curr_dist_z = max(20, fabs(this.origin.z - gco.z));
342         float curr_dist_2d = max(20, vlen(vec2(this.origin - gco)));
343         float distance_time = this.goalcurrent_distance_time;
344         if(distance_time < 0)
345                 distance_time = -distance_time;
346         if(curr_dist_z >= this.goalcurrent_distance_z && curr_dist_2d >= this.goalcurrent_distance_2d)
347         {
348                 if(!distance_time)
349                         this.goalcurrent_distance_time = time;
350                 else if (time - distance_time > 0.5)
351                         return true;
352         }
353         else
354         {
355                 // reduce it a little bit so it works even with very small approaches to the goal
356                 this.goalcurrent_distance_z = max(20, curr_dist_z - 10);
357                 this.goalcurrent_distance_2d = max(20, curr_dist_2d - 10);
358                 this.goalcurrent_distance_time = 0;
359         }
360         return false;
361 }
362
363 entity havocbot_select_an_item_of_group(entity this, int gr)
364 {
365         entity selected = NULL;
366         float selected_dist2 = 0;
367         // select farthest item of this group from bot's position
368         IL_EACH(g_items, it.item_group == gr && it.solid,
369         {
370                 float dist2 = vlen2(this.origin - it.origin);
371                 if (dist2 < 600 ** 2 && dist2 > selected_dist2)
372                 {
373                         selected = it;
374                         selected_dist2 = vlen2(this.origin - selected.origin);
375                 }
376         });
377
378         if (!selected)
379                 return NULL;
380
381         set_tracewalk_dest(selected, this.origin, false);
382         if (!tracewalk(this, this.origin, STAT(PL_MIN, this), STAT(PL_MAX, this),
383                 tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
384         {
385                 return NULL;
386         }
387
388         return selected;
389 }
390
391 void havocbot_movetogoal(entity this)
392 {
393         vector diff;
394         vector dir;
395         vector flatdir;
396         float dodge_enemy_factor = 1;
397         float maxspeed;
398         //float dist;
399         vector dodge;
400         //if (this.goalentity)
401         //      te_lightning2(this, this.origin, (this.goalentity.absmin + this.goalentity.absmax) * 0.5);
402         CS(this).movement = '0 0 0';
403         maxspeed = autocvar_sv_maxspeed;
404
405         PHYS_INPUT_BUTTON_CROUCH(this) = boolean(this.goalcurrent.wpflags & WAYPOINTFLAG_CROUCH);
406
407         PHYS_INPUT_BUTTON_JETPACK(this) = false;
408         // Jetpack navigation
409         if(this.navigation_jetpack_goal)
410         if(this.goalcurrent==this.navigation_jetpack_goal)
411         if(GetResource(this, RES_FUEL))
412         {
413                 if(autocvar_bot_debug_goalstack)
414                 {
415                         debuggoalstack(this);
416                         te_wizspike(this.navigation_jetpack_point);
417                 }
418
419                 // Take off
420                 if (!(this.aistatus & AI_STATUS_JETPACK_FLYING))
421                 {
422                         // Brake almost completely so it can get a good direction
423                         if(vdist(this.velocity, >, 10))
424                                 return;
425                         this.aistatus |= AI_STATUS_JETPACK_FLYING;
426                 }
427
428                 makevectors(this.v_angle.y * '0 1 0');
429                 dir = normalize(this.navigation_jetpack_point - this.origin);
430
431                 // Landing
432                 if(this.aistatus & AI_STATUS_JETPACK_LANDING)
433                 {
434                         // Calculate brake distance in xy
435                         float d = vlen(vec2(this.origin - (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5));
436                         float vel2 = vlen2(vec2(this.velocity));
437                         float db = (vel2 / (autocvar_g_jetpack_acceleration_side * 2)) + 100;
438                         //LOG_INFOF("distance %d, velocity %d, brake at %d ", ceil(d), ceil(v), ceil(db));
439                         if(d < db || d < 500)
440                         {
441                                 // Brake
442                                 if (vel2 > (maxspeed * 0.3) ** 2)
443                                 {
444                                         CS(this).movement_x = dir * v_forward * -maxspeed;
445                                         return;
446                                 }
447                                 // Switch to normal mode
448                                 this.navigation_jetpack_goal = NULL;
449                                 this.aistatus &= ~AI_STATUS_JETPACK_LANDING;
450                                 this.aistatus &= ~AI_STATUS_JETPACK_FLYING;
451                                 return;
452                         }
453                 }
454                 else if(checkpvs(this.origin,this.goalcurrent))
455                 {
456                         // If I can see the goal switch to landing code
457                         this.aistatus &= ~AI_STATUS_JETPACK_FLYING;
458                         this.aistatus |= AI_STATUS_JETPACK_LANDING;
459                         return;
460                 }
461
462                 // Flying
463                 PHYS_INPUT_BUTTON_JETPACK(this) = true;
464                 if(this.navigation_jetpack_point.z - STAT(PL_MAX, this).z + STAT(PL_MIN, this).z < this.origin.z)
465                 {
466                         CS(this).movement_x = dir * v_forward * maxspeed;
467                         CS(this).movement_y = dir * v_right * maxspeed;
468                 }
469                 return;
470         }
471
472         // Handling of jump pads
473         if(this.jumppadcount)
474         {
475                 if(this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
476                 {
477                         this.aistatus |= AI_STATUS_OUT_JUMPPAD;
478                         if(navigation_poptouchedgoals(this))
479                                 return;
480                 }
481                 else if(this.aistatus & AI_STATUS_OUT_JUMPPAD)
482                 {
483                         // If got stuck on the jump pad try to reach the farthest visible waypoint
484                         // but with some randomness so it can try out different paths
485                         if(!this.goalcurrent)
486                         {
487                                 entity newgoal = NULL;
488                                 IL_EACH(g_waypoints, vdist(it.origin - this.origin, <=, 1000),
489                                 {
490                                         if(it.wpflags & WAYPOINTFLAG_TELEPORT)
491                                         if(it.origin.z < this.origin.z - 100 && vdist(vec2(it.origin - this.origin), <, 100))
492                                                 continue;
493
494                                         traceline(this.origin + this.view_ofs, ((it.absmin + it.absmax) * 0.5), true, this);
495
496                                         if(trace_fraction < 1)
497                                                 continue;
498
499                                         if(!newgoal || ((random() < 0.8) && vlen2(it.origin - this.origin) > vlen2(newgoal.origin - this.origin)))
500                                                 newgoal = it;
501                                 });
502
503                                 if(newgoal)
504                                 {
505                                         this.ignoregoal = this.goalcurrent;
506                                         this.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout;
507                                         navigation_clearroute(this);
508                                         navigation_routetogoal(this, newgoal, this.origin);
509                                         if(autocvar_bot_debug_goalstack)
510                                                 debuggoalstack(this);
511                                         this.aistatus &= ~AI_STATUS_OUT_JUMPPAD;
512                                 }
513                         }
514                         else //if (this.goalcurrent)
515                         {
516                                 if (this.goalcurrent.bot_pickup)
517                                 {
518                                         entity jumppad_wp = this.goalcurrent_prev;
519                                         navigation_poptouchedgoals(this);
520                                         if(!this.goalcurrent && jumppad_wp.wp00)
521                                         {
522                                                 // head to the jumppad destination once bot reaches the goal item
523                                                 navigation_pushroute(this, jumppad_wp.wp00);
524                                         }
525                                 }
526                                 vector gco = (this.goalcurrent.absmin + this.goalcurrent.absmax) * 0.5;
527                                 if (this.origin.z > gco.z && vdist(vec2(this.velocity), <, autocvar_sv_maxspeed))
528                                 {
529                                         if (this.velocity.z < 0)
530                                                 this.aistatus &= ~AI_STATUS_OUT_JUMPPAD;
531                                 }
532                                 else if(havocbot_checkgoaldistance(this, gco))
533                                 {
534                                         navigation_clearroute(this);
535                                         navigation_goalrating_timeout_force(this);
536                                 }
537                                 else
538                                         return;
539                         }
540                 }
541                 else //if (!(this.aistatus & AI_STATUS_OUT_JUMPPAD))
542                 {
543                         if(this.velocity.z > 0 && this.origin.z - this.lastteleport_origin.z > (this.maxs.z - this.mins.z) * 0.5)
544                         {
545                                 vector velxy = this.velocity; velxy_z = 0;
546                                 if(vdist(velxy, <, autocvar_sv_maxspeed * 0.2))
547                                 {
548                                         LOG_TRACE("Warning: ", this.netname, " got stuck on a jumppad (velocity in xy is ", vtos(velxy), "), trying to get out of it now");
549                                         this.aistatus |= AI_STATUS_OUT_JUMPPAD;
550                                 }
551                                 return;
552                         }
553
554                         // Don't chase players while using a jump pad
555                         if(IS_PLAYER(this.goalcurrent) || IS_PLAYER(this.goalstack01))
556                                 return;
557                 }
558         }
559         else if(this.aistatus & AI_STATUS_OUT_JUMPPAD)
560                 this.aistatus &= ~AI_STATUS_OUT_JUMPPAD;
561
562         // If there is a trigger_hurt right below try to use the jetpack or make a rocketjump
563         if (skill > 6 && !(IS_ONGROUND(this)))
564         {
565                 #define ROCKETJUMP_DAMAGE() WEP_CVAR(devastator, damage) * 0.8 \
566                         * ((STAT(STRENGTH_FINISHED, this) > time) ? autocvar_g_balance_powerup_strength_selfdamage : 1) \
567                         * ((STAT(INVINCIBLE_FINISHED, this) > time) ? autocvar_g_balance_powerup_invincible_takedamage : 1)
568
569                 // save some CPU cycles by checking trigger_hurt after checking
570                 // that something can be done to evade it (cheaper checks)
571                 int action_for_trigger_hurt = 0;
572                 if (this.items & IT_JETPACK)
573                         action_for_trigger_hurt = 1;
574                 else if (!this.jumppadcount && !waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
575                         && !(this.goalcurrent_prev && this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP)
576                         && GetResource(this, RES_HEALTH) + GetResource(this, RES_ARMOR) > ROCKETJUMP_DAMAGE())
577                 {
578                         action_for_trigger_hurt = 2;
579                 }
580                 else if (!this.goalcurrent)
581                         action_for_trigger_hurt = 3;
582
583                 if (action_for_trigger_hurt)
584                 {
585                         tracebox(this.origin, this.mins, this.maxs, this.origin + '0 0 -65536', MOVE_NOMONSTERS, this);
586                         if(!tracebox_hits_trigger_hurt(this.origin, this.mins, this.maxs, trace_endpos))
587                                 action_for_trigger_hurt = 0;
588                 }
589
590                 if(action_for_trigger_hurt == 1) // jetpack
591                 {
592                         tracebox(this.origin, this.mins, this.maxs, this.origin + '0 0 65536', MOVE_NOMONSTERS, this);
593                         if(tracebox_hits_trigger_hurt(this.origin, this.mins, this.maxs, trace_endpos + '0 0 1' ))
594                         {
595                                 if(this.velocity.z<0)
596                                         PHYS_INPUT_BUTTON_JETPACK(this) = true;
597                         }
598                         else
599                                 PHYS_INPUT_BUTTON_JETPACK(this) = true;
600
601                         // If there is no goal try to move forward
602
603                         if(this.goalcurrent==NULL)
604                                 dir = v_forward;
605                         else
606                                 dir = normalize(( ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5 ) - this.origin);
607
608                         vector xyvelocity = this.velocity; xyvelocity_z = 0;
609                         float xyspeed = xyvelocity * dir;
610
611                         if(xyspeed < (maxspeed / 2))
612                         {
613                                 makevectors(this.v_angle.y * '0 1 0');
614                                 tracebox(this.origin, this.mins, this.maxs, this.origin + (dir * maxspeed * 3), MOVE_NOMONSTERS, this);
615                                 if(trace_fraction==1)
616                                 {
617                                         CS(this).movement_x = dir * v_forward * maxspeed;
618                                         CS(this).movement_y = dir * v_right * maxspeed;
619                                         if (skill < 10)
620                                                 havocbot_keyboard_movement(this, this.origin + dir * 100);
621                                 }
622                         }
623
624                         this.havocbot_blockhead = true;
625
626                         return;
627                 }
628                 else if(action_for_trigger_hurt == 2) // rocketjump
629                 {
630                         if(this.velocity.z < 0)
631                         {
632                                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
633                                 {
634                                         .entity weaponentity = weaponentities[slot];
635
636                                         if(this.(weaponentity).m_weapon == WEP_Null && slot != 0)
637                                                 continue;
638
639                                         if(client_hasweapon(this, WEP_DEVASTATOR, weaponentity, true, false))
640                                         {
641                                                 CS(this).movement_x = maxspeed;
642
643                                                 if(this.rocketjumptime)
644                                                 {
645                                                         if(time > this.rocketjumptime)
646                                                         {
647                                                                 PHYS_INPUT_BUTTON_ATCK2(this) = true;
648                                                                 this.rocketjumptime = 0;
649                                                         }
650                                                         return;
651                                                 }
652
653                                                 this.(weaponentity).m_switchweapon = WEP_DEVASTATOR;
654                                                 this.v_angle_x = 90;
655                                                 PHYS_INPUT_BUTTON_ATCK(this) = true;
656                                                 this.rocketjumptime = time + WEP_CVAR(devastator, detonatedelay);
657                                                 return;
658                                         }
659                                 }
660                         }
661                 }
662                 else if(action_for_trigger_hurt == 3) // no goal
663                 {
664                         // If there is no goal try to move forward
665                         CS(this).movement_x = maxspeed;
666                 }
667         }
668
669         // If we are under water with no goals, swim up
670         if(this.waterlevel && !this.goalcurrent)
671         {
672                 dir = '0 0 0';
673                 if(this.waterlevel>WATERLEVEL_SWIMMING)
674                         dir.z = 1;
675                 else if(this.velocity.z >= 0 && !(this.waterlevel == WATERLEVEL_WETFEET && this.watertype == CONTENT_WATER))
676                         PHYS_INPUT_BUTTON_JUMP(this) = true;
677                 makevectors(this.v_angle.y * '0 1 0');
678                 vector v = dir * maxspeed;
679                 CS(this).movement.x = v * v_forward;
680                 CS(this).movement.y = v * v_right;
681                 CS(this).movement.z = v * v_up;
682         }
683
684         // if there is nowhere to go, exit
685         if (this.goalcurrent == NULL)
686                 return;
687
688
689         bool locked_goal = false;
690         if((this.goalentity && wasfreed(this.goalentity))
691                 || (this.goalcurrent == this.goalentity && this.goalentity.tag_entity))
692         {
693                 navigation_clearroute(this);
694                 navigation_goalrating_timeout_force(this);
695                 return;
696         }
697         else if(this.goalentity.tag_entity)
698         {
699                 navigation_goalrating_timeout_expire(this, 2);
700         }
701         else if(this.goalentity.bot_pickup)
702         {
703                 if(this.goalentity.bot_pickup_respawning)
704                 {
705                         if(this.goalentity.solid) // item respawned
706                                 this.goalentity.bot_pickup_respawning = false;
707                         else if(time < this.goalentity.scheduledrespawntime - 10) // item already taken (by someone else)
708                         {
709                                 if(checkpvs(this.origin, this.goalentity))
710                                 {
711                                         this.goalentity.bot_pickup_respawning = false;
712                                         navigation_goalrating_timeout_expire(this, random());
713                                 }
714                                 locked_goal = true; // wait for item to respawn
715                         }
716                         else if(this.goalentity == this.goalcurrent)
717                                 locked_goal = true; // wait for item to respawn
718                 }
719                 else if(!this.goalentity.solid && !boxesoverlap(this.goalentity.absmin, this.goalentity.absmax, this.absmin, this.absmax))
720                 {
721                         if(checkpvs(this.origin, this.goalentity))
722                         {
723                                 navigation_goalrating_timeout_expire(this, random());
724                         }
725                 }
726         }
727         if (this.goalcurrent == this.goalentity && this.goalentity_lock_timeout > time)
728                 locked_goal = true;
729
730         if (navigation_shortenpath(this))
731         {
732                 if (vdist(this.origin - this.goalcurrent_prev.origin, <, 50)
733                         && navigation_goalrating_timeout_can_be_anticipated(this))
734                 {
735                         navigation_goalrating_timeout_force(this);
736                 }
737         }
738
739         bool goalcurrent_can_be_removed = false;
740         if (IS_PLAYER(this.goalcurrent) || IS_MONSTER(this.goalcurrent))
741         {
742                 bool freeze_state_changed = (boolean(STAT(FROZEN, this.goalentity)) != this.goalentity_shouldbefrozen);
743                 if (IS_DEAD(this.goalcurrent) || (this.goalentity == this.goalcurrent && freeze_state_changed))
744                 {
745                         goalcurrent_can_be_removed = true;
746                         // don't remove if not visible
747                         if (checkpvs(this.origin + this.view_ofs, this.goalcurrent))
748                         {
749                                 if (IS_DEAD(this.goalcurrent))
750                                 {
751                                         IL_EACH(g_items, it.enemy == this.goalcurrent && Item_IsLoot(it),
752                                         {
753                                                 if (vdist(it.origin - this.goalcurrent.death_origin, <, 50))
754                                                 {
755                                                         navigation_clearroute(this);
756                                                         navigation_pushroute(this, it);
757                                                         // loot can't be immediately rated since it isn't on ground yet
758                                                         // it will be rated after a second when on ground, meanwhile head to it
759                                                         navigation_goalrating_timeout_expire(this, 1);
760                                                         return;
761                                                 }
762                                         });
763                                 }
764                                 if (!Item_IsLoot(this.goalcurrent))
765                                 {
766                                         navigation_goalrating_timeout_force(this);
767                                         return;
768                                 }
769                         }
770                 }
771                 else if (!(STAT(FROZEN, this.goalentity)) && this.bot_tracewalk_time < time)
772                 {
773                         set_tracewalk_dest(this.goalcurrent, this.origin, true);
774                         if (!(trace_ent == this || tracewalk(this, this.origin, this.mins, this.maxs,
775                                 tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode)))
776                         {
777                                 navigation_goalrating_timeout_force(this);
778                                 return;
779                         }
780                         this.bot_tracewalk_time = max(time, this.bot_tracewalk_time) + 0.25;
781                 }
782         }
783
784         if(!locked_goal)
785         {
786                 // optimize path finding by anticipating goalrating when bot is near a waypoint;
787                 // in this case path finding can start directly from a waypoint instead of
788                 // looking for all the reachable waypoints up to a certain distance
789                 if (navigation_poptouchedgoals(this))
790                 {
791                         if (this.goalcurrent)
792                         {
793                                 if (goalcurrent_can_be_removed)
794                                 {
795                                         // remove even if not visible
796                                         navigation_goalrating_timeout_force(this);
797                                         return;
798                                 }
799                                 else if (navigation_goalrating_timeout_can_be_anticipated(this))
800                                         navigation_goalrating_timeout_force(this);
801                         }
802                         else
803                         {
804                                 entity old_goal = this.goalcurrent_prev;
805                                 if (old_goal.item_group && this.item_group != old_goal.item_group)
806                                 {
807                                         // Avoid multiple costly calls of path finding code that selects one of the closest
808                                         // item of the group by telling the bot to head directly to the farthest item.
809                                         // Next time we let the bot select a goal as usual which can be another item
810                                         // of this group (the closest one) and so on
811                                         this.item_group = old_goal.item_group;
812                                         entity new_goal = havocbot_select_an_item_of_group(this, old_goal.item_group);
813                                         if (new_goal)
814                                                 navigation_pushroute(this, new_goal);
815                                 }
816                         }
817                 }
818         }
819
820         // if ran out of goals try to use an alternative goal or get a new strategy asap
821         if(this.goalcurrent == NULL)
822         {
823                 navigation_goalrating_timeout_force(this);
824                 return;
825         }
826
827
828         if(autocvar_bot_debug_goalstack)
829                 debuggoalstack(this);
830
831         bool bunnyhop_forbidden = false;
832         vector destorg = get_closer_dest(this.goalcurrent, this.origin);
833         if (this.jumppadcount && this.goalcurrent.wpflags & WAYPOINTFLAG_TELEPORT)
834         {
835                 // if bot used the jumppad, push towards jumppad origin until jumppad waypoint gets removed
836                 destorg = this.goalcurrent.origin;
837         }
838         else if (this.goalcurrent.wpisbox)
839         {
840                 // if bot is inside the teleport waypoint, head to teleport origin until teleport gets used
841                 // do it even if bot is on a ledge above a teleport/jumppad so it doesn't get stuck
842                 if (boxesoverlap(this.goalcurrent.absmin, this.goalcurrent.absmax, this.origin + eZ * this.mins.z, this.origin + eZ * this.maxs.z)
843                         || (this.absmin.z > destorg.z && destorg.x == this.origin.x && destorg.y == this.origin.y))
844                 {
845                         bunnyhop_forbidden = true;
846                         destorg = this.goalcurrent.origin;
847                         if(destorg.z > this.origin.z)
848                                 PHYS_INPUT_BUTTON_JUMP(this) = true;
849                 }
850         }
851
852         diff = destorg - this.origin;
853
854         if (time < this.bot_stop_moving_timeout
855                 || (this.goalcurrent == this.goalentity && time < this.goalentity_lock_timeout && vdist(diff, <, 10)))
856         {
857                 // stop if the locked goal has been reached
858                 destorg = this.origin;
859                 diff = dir = '0 0 0';
860         }
861         else if (IS_PLAYER(this.goalcurrent) || IS_MONSTER(this.goalcurrent))
862         {
863                 if (vdist(diff, <, 80))
864                 {
865                         // stop if too close to target player (even if frozen)
866                         destorg = this.origin;
867                         diff = dir = '0 0 0';
868                 }
869                 else
870                 {
871                         // move destorg out of target players, otherwise bot will consider them
872                         // an obstacle that needs to be jumped (especially if frozen)
873                         dir = normalize(diff);
874                         destorg -= dir * PL_MAX_CONST.x * M_SQRT2;
875                         diff = destorg - this.origin;
876                 }
877         }
878         else
879                 dir = normalize(diff);
880         flatdir = (diff.z == 0) ? dir : normalize(vec2(diff));
881
882         bool danger_detected = false;
883         vector do_break = '0 0 0';
884
885         //if (this.bot_dodgevector_time < time)
886         {
887                 //this.bot_dodgevector_time = time + cvar("bot_ai_dodgeupdateinterval");
888                 //this.bot_dodgevector_jumpbutton = 1;
889
890                 this.aistatus &= ~AI_STATUS_DANGER_AHEAD;
891                 makevectors(this.v_angle.y * '0 1 0');
892                 if (this.waterlevel > WATERLEVEL_WETFEET)
893                 {
894                         if (this.waterlevel > WATERLEVEL_SWIMMING)
895                         {
896                                 if(!this.goalcurrent)
897                                         this.aistatus |= AI_STATUS_OUT_WATER;
898                                 else if(destorg.z > this.origin.z)
899                                         PHYS_INPUT_BUTTON_JUMP(this) = true;
900                         }
901                         else
902                         {
903                                 if(this.velocity.z >= 0 && !(this.watertype == CONTENT_WATER && destorg.z < this.origin.z) &&
904                                         (this.aistatus & AI_STATUS_OUT_WATER))
905                                 {
906                                         PHYS_INPUT_BUTTON_JUMP(this) = true;
907                                         dir = flatdir;
908                                 }
909                                 else
910                                 {
911                                         if (destorg.z > this.origin.z)
912                                                 dir = flatdir;
913                                 }
914                         }
915                 }
916                 else
917                 {
918                         float s;
919                         vector offset;
920                         if(this.aistatus & AI_STATUS_OUT_WATER)
921                                 this.aistatus &= ~AI_STATUS_OUT_WATER;
922
923                         // jump if going toward an obstacle that doesn't look like stairs we
924                         // can walk up directly
925                         vector deviation = '0 0 0';
926                         float current_speed = vlen(vec2(this.velocity));
927                         if (current_speed < maxspeed * 0.2)
928                                 current_speed = maxspeed * 0.2;
929                         else
930                         {
931                                 deviation = vectoangles(diff) - vectoangles(this.velocity);
932                                 while (deviation.y < -180) deviation.y += 360;
933                                 while (deviation.y > 180) deviation.y -= 360;
934                         }
935                         float turning = false;
936                         vector flat_diff = vec2(diff);
937                         offset = max(32, current_speed * cos(deviation.y * DEG2RAD) * 0.3) * flatdir;
938                         vector actual_destorg = this.origin + offset;
939                         if (this.goalcurrent_prev && (this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP))
940                         {
941                                 if (time > this.bot_stop_moving_timeout
942                                         && fabs(deviation.y) > 20 && current_speed > maxspeed * 0.4
943                                         && vdist(vec2(this.origin - this.goalcurrent_prev.origin), <, 50))
944                                 {
945                                         this.bot_stop_moving_timeout = time + 0.1;
946                                 }
947                                 if (current_speed > autocvar_sv_maxspeed * 0.9
948                                         && vlen2(flat_diff) < vlen2(vec2(this.goalcurrent_prev.origin - destorg))
949                                         && vdist(vec2(this.origin - this.goalcurrent_prev.origin), >, 50)
950                                         && vdist(vec2(this.origin - this.goalcurrent_prev.origin), <, 150)
951                                 )
952                                 {
953                                         PHYS_INPUT_BUTTON_JUMP(this) = true;
954                                         this.bot_jump_time = time;
955                                 }
956                         }
957                         else if (!this.goalstack01 || (this.goalcurrent.wpflags & (WAYPOINTFLAG_TELEPORT | WAYPOINTFLAG_LADDER)))
958                         {
959                                 if (vlen2(flat_diff) < vlen2(offset))
960                                 {
961                                         if ((this.goalcurrent.wpflags & WAYPOINTFLAG_JUMP) && this.goalstack01)
962                                         {
963                                                 // oblique warpzones need a jump otherwise bots gets stuck
964                                                 PHYS_INPUT_BUTTON_JUMP(this) = true;
965                                         }
966                                         else
967                                         {
968                                                 actual_destorg.x = destorg.x;
969                                                 actual_destorg.y = destorg.y;
970                                         }
971                                 }
972                         }
973                         else if (vdist(flat_diff, <, 32) && diff.z < -16) // destination is under the bot
974                         {
975                                 actual_destorg.x = destorg.x;
976                                 actual_destorg.y = destorg.y;
977                         }
978                         else if (vlen2(flat_diff) < vlen2(offset))
979                         {
980                                 vector next_goal_org = (this.goalstack01.absmin + this.goalstack01.absmax) * 0.5;
981                                 vector next_dir = normalize(vec2(next_goal_org - destorg));
982                                 float dist = vlen(vec2(this.origin + offset - destorg));
983                                 // if current and next goal are close to each other make sure
984                                 // actual_destorg isn't set beyond next_goal_org
985                                 if (dist ** 2 > vlen2(vec2(next_goal_org - destorg)))
986                                         actual_destorg = next_goal_org;
987                                 else
988                                         actual_destorg = vec2(destorg) + dist * next_dir;
989                                 actual_destorg.z = this.origin.z;
990                                 turning = true;
991                         }
992
993                         LABEL(jumpobstacle_check);
994                         dir = flatdir = normalize(actual_destorg - this.origin);
995
996                         bool jump_forbidden = false;
997                         if (!turning && fabs(deviation.y) > 50)
998                                 jump_forbidden = true;
999                         else if (IS_DUCKED(this))
1000                         {
1001                                 tracebox(this.origin, PL_MIN_CONST, PL_MAX_CONST, this.origin, false, this);
1002                                 if (trace_startsolid)
1003                                         jump_forbidden = true;
1004                         }
1005
1006                         if (!jump_forbidden)
1007                         {
1008                                 tracebox(this.origin, this.mins, this.maxs, actual_destorg, false, this);
1009                                 if (trace_fraction < 1 && trace_plane_normal.z < 0.7)
1010                                 {
1011                                         s = trace_fraction;
1012                                         tracebox(this.origin + stepheightvec, this.mins, this.maxs, actual_destorg + stepheightvec, false, this);
1013                                         if (trace_fraction < s + 0.01 && trace_plane_normal.z < 0.7)
1014                                         {
1015                                                 // found an obstacle
1016                                                 if (turning && fabs(deviation.y) > 5)
1017                                                 {
1018                                                         // check if the obstacle is still there without turning
1019                                                         actual_destorg = destorg;
1020                                                         turning = false;
1021                                                         this.bot_tracewalk_time = time + 0.25;
1022                                                         goto jumpobstacle_check;
1023                                                 }
1024                                                 s = trace_fraction;
1025                                                 // don't artificially reduce max jump height in real-time
1026                                                 // (jumpstepheightvec is reduced a bit to make the jumps easy in tracewalk)
1027                                                 vector jump_height = (IS_ONGROUND(this)) ? stepheightvec + jumpheight_vec : jumpstepheightvec;
1028                                                 tracebox(this.origin + jump_height, this.mins, this.maxs, actual_destorg + jump_height, false, this);
1029                                                 if (trace_fraction > s)
1030                                                 {
1031                                                         PHYS_INPUT_BUTTON_JUMP(this) = true;
1032                                                         this.bot_jump_time = time;
1033                                                 }
1034                                                 else
1035                                                 {
1036                                                         jump_height = stepheightvec + jumpheight_vec / 2;
1037                                                         tracebox(this.origin + jump_height, this.mins, this.maxs, actual_destorg + jump_height, false, this);
1038                                                         if (trace_fraction > s)
1039                                                         {
1040                                                                 PHYS_INPUT_BUTTON_JUMP(this) = true;
1041                                                                 this.bot_jump_time = time;
1042                                                         }
1043                                                 }
1044                                         }
1045                                 }
1046                         }
1047
1048                         // if bot for some reason doesn't get close to the current goal find another one
1049                         if(!this.jumppadcount && !IS_PLAYER(this.goalcurrent))
1050                         if(!(locked_goal && this.goalcurrent_distance_z < 50 && this.goalcurrent_distance_2d < 50))
1051                         if(havocbot_checkgoaldistance(this, destorg))
1052                         {
1053                                 if(this.goalcurrent_distance_time < 0) // can't get close for the second time
1054                                 {
1055                                         navigation_clearroute(this);
1056                                         navigation_goalrating_timeout_force(this);
1057                                         return;
1058                                 }
1059
1060                                 set_tracewalk_dest(this.goalcurrent, this.origin, false);
1061                                 if (!tracewalk(this, this.origin, this.mins, this.maxs,
1062                                         tracewalk_dest, tracewalk_dest_height, bot_navigation_movemode))
1063                                 {
1064                                         navigation_clearroute(this);
1065                                         navigation_goalrating_timeout_force(this);
1066                                         return;
1067                                 }
1068
1069                                 // give bot only another chance to prevent bot getting stuck
1070                                 // in case it thinks it can walk but actually can't
1071                                 this.goalcurrent_distance_z = FLOAT_MAX;
1072                                 this.goalcurrent_distance_2d = FLOAT_MAX;
1073                                 this.goalcurrent_distance_time = -time; // mark second try
1074                         }
1075
1076                         if (skill + this.bot_moveskill <= 3 && time > this.bot_stop_moving_timeout
1077                                 && current_speed > maxspeed * 0.9 && fabs(deviation.y) > 70)
1078                         {
1079                                 this.bot_stop_moving_timeout = time + 0.4 + random() * 0.2;
1080                         }
1081
1082                         // Check for water/slime/lava and dangerous edges
1083                         // (only when the bot is on the ground or jumping intentionally)
1084
1085                         offset = (vdist(this.velocity, >, 32) ? this.velocity * 0.2 : flatdir * 32);
1086                         vector dst_ahead = this.origin + this.view_ofs + offset;
1087                         vector dst_down = dst_ahead - '0 0 3000';
1088                         traceline(this.origin + this.view_ofs, dst_ahead, true, NULL);
1089
1090                         bool unreachable = false;
1091                         s = CONTENT_SOLID;
1092                         if (trace_fraction == 1 && !this.jumppadcount
1093                                 && !waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
1094                                 && !(this.goalcurrent_prev && (this.goalcurrent_prev.wpflags & WAYPOINTFLAG_JUMP)))
1095                         if((IS_ONGROUND(this)) || (this.aistatus & AI_STATUS_RUNNING) || (this.aistatus & AI_STATUS_ROAMING) || PHYS_INPUT_BUTTON_JUMP(this))
1096                         {
1097                                 // Look downwards
1098                                 traceline(dst_ahead , dst_down, true, NULL);
1099                                 //te_lightning2(NULL, this.origin + this.view_ofs, dst_ahead); // Draw "ahead" look
1100                                 //te_lightning2(NULL, dst_ahead, trace_endpos); // Draw "downwards" look
1101                                 if(trace_endpos.z < this.origin.z + this.mins.z)
1102                                 {
1103                                         if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)
1104                                                 danger_detected = true;
1105                                         else if (trace_endpos.z < min(this.origin.z + this.mins.z, this.goalcurrent.origin.z) - 100)
1106                                                 danger_detected = true;
1107                                         else
1108                                         {
1109                                                 s = pointcontents(trace_endpos + '0 0 1');
1110                                                 if (s != CONTENT_SOLID)
1111                                                 {
1112                                                         if (s == CONTENT_LAVA || s == CONTENT_SLIME)
1113                                                                 danger_detected = true;
1114                                                         else if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
1115                                                         {
1116                                                                 // the traceline check isn't enough but is good as optimization,
1117                                                                 // when not true (most of the time) this tracebox call is avoided
1118                                                                 tracebox(dst_ahead, this.mins, this.maxs, dst_down, true, this);
1119                                                                 if (tracebox_hits_trigger_hurt(dst_ahead, this.mins, this.maxs, trace_endpos))
1120                                                                 {
1121                                                                         if (destorg.z > this.origin.z + jumpstepheightvec.z)
1122                                                                         {
1123                                                                                 // the goal is probably on an upper platform, assume bot can't get there
1124                                                                                 unreachable = true;
1125                                                                         }
1126                                                                         else
1127                                                                                 danger_detected = true;
1128                                                                 }
1129                                                         }
1130                                                 }
1131                                         }
1132                                 }
1133                         }
1134
1135                         dir = flatdir;
1136                         makevectors(this.v_angle.y * '0 1 0');
1137
1138                         if (danger_detected || (s == CONTENT_WATER))
1139                         {
1140                                 this.aistatus |= AI_STATUS_DANGER_AHEAD;
1141                                 if(IS_PLAYER(this.goalcurrent))
1142                                         unreachable = true;
1143                         }
1144
1145                         // slow down if bot is in the air and goal is under it
1146                         if (!waypoint_is_hardwiredlink(this.goalcurrent_prev, this.goalcurrent)
1147                                 && vdist(flat_diff, <, 250) && this.origin.z - destorg.z > 120
1148                                 && (!IS_ONGROUND(this) || vdist(vec2(this.velocity), >, maxspeed * 0.3)))
1149                         {
1150                                 // tracebox wouldn't work when bot is still on the ledge
1151                                 traceline(this.origin, this.origin - '0 0 200', true, this);
1152                                 if (this.origin.z - trace_endpos.z > 120)
1153                                         do_break = normalize(this.velocity) * -1;
1154                         }
1155
1156                         if(unreachable)
1157                         {
1158                                 navigation_clearroute(this);
1159                                 navigation_goalrating_timeout_force(this);
1160                                 this.ignoregoal = this.goalcurrent;
1161                                 this.ignoregoaltime = time + autocvar_bot_ai_ignoregoal_timeout;
1162                         }
1163                 }
1164
1165                 dodge = havocbot_dodge(this);
1166                 if (dodge)
1167                         dodge *= bound(0, 0.5 + (skill + this.bot_dodgeskill) * 0.1, 1);
1168                 if (this.enemy)
1169                 {
1170                         traceline(this.origin, (this.enemy.absmin + this.enemy.absmax) * 0.5, true, NULL);
1171                         if (IS_PLAYER(trace_ent))
1172                                 dodge_enemy_factor = bound(0, (skill + this.bot_dodgeskill) / 7, 1);
1173                 }
1174         //      this.bot_dodgevector = dir;
1175         //      this.bot_dodgevector_jumpbutton = PHYS_INPUT_BUTTON_JUMP(this);
1176         }
1177
1178         float ladder_zdir = 0;
1179         if(this.ladder_entity)
1180         {
1181                 if(this.goalcurrent.origin.z + this.goalcurrent.mins.z > this.origin.z + this.mins.z)
1182                 {
1183                         if(this.origin.z + this.mins.z  < this.ladder_entity.origin.z + this.ladder_entity.maxs.z)
1184                                 ladder_zdir = 1;
1185                 }
1186                 else
1187                 {
1188                         if(this.origin.z + this.mins.z  > this.ladder_entity.origin.z + this.ladder_entity.mins.z)
1189                                 ladder_zdir = -1;
1190                 }
1191                 if (ladder_zdir)
1192                 {
1193                         if (vdist(vec2(diff), <, 40))
1194                                 dir.z = ladder_zdir * 4;
1195                         else
1196                                 dir.z = ladder_zdir * 2;
1197                         dir = normalize(dir);
1198                 }
1199         }
1200
1201         if (this.goalcurrent.wpisbox
1202                 && boxesoverlap(this.goalcurrent.absmin, this.goalcurrent.absmax, this.origin, this.origin))
1203         {
1204                 // bot is inside teleport waypoint but hasn't touched the real teleport yet
1205                 // head to teleport origin
1206                 dir = (this.goalcurrent.origin - this.origin);
1207                 dir.z = 0;
1208                 dir = normalize(dir);
1209         }
1210
1211         // already executed when bot targets an enemy
1212         if (!this.bot_aimdir_executed)
1213         {
1214                 if (time < this.bot_stop_moving_timeout)
1215                         bot_aimdir(this, normalize(this.goalcurrent.origin - this.origin), 0);
1216                 else
1217                         bot_aimdir(this, dir, 0);
1218         }
1219
1220         vector evadedanger = '0 0 0';
1221         if (!ladder_zdir)
1222         {
1223                 dir *= dodge_enemy_factor;
1224                 if (danger_detected && vdist(this.velocity, >, maxspeed * 0.8) && this.goalcurrent_prev
1225                         && this.goalcurrent.classname == "waypoint")
1226                 {
1227                         vector p = this.origin + this.velocity * 0.2;
1228                         vector evadedanger = point_line_vec(p, vec2(this.goalcurrent_prev.origin) + eZ * p.z,
1229                                 vec2(destorg - this.goalcurrent_prev.origin));
1230                         if (vdist(evadedanger, >, 20))
1231                         {
1232                                 if (vdist(evadedanger, >, 40))
1233                                         do_break = normalize(this.velocity) * -1;
1234                                 evadedanger = normalize(evadedanger);
1235                                 evadedanger *= bound(1, 3 - (skill + this.bot_dodgeskill), 3); // Noobs fear dangers a lot and take more distance from them
1236                         }
1237                         else
1238                                 evadedanger = '0 0 0';
1239                 }
1240                 dir = normalize(dir + dodge + do_break + evadedanger);
1241         }
1242
1243         makevectors(this.v_angle);
1244         //dir = this.bot_dodgevector;
1245         //if (this.bot_dodgevector_jumpbutton)
1246         //      PHYS_INPUT_BUTTON_JUMP(this) = true;
1247         CS(this).movement_x = dir * v_forward * maxspeed;
1248         CS(this).movement_y = dir * v_right * maxspeed;
1249         CS(this).movement_z = dir * v_up * maxspeed;
1250
1251         // Emulate keyboard interface
1252         if (skill < 10)
1253                 havocbot_keyboard_movement(this, destorg);
1254
1255         // Bunnyhop!
1256         if (!bunnyhop_forbidden && !evadedanger && !do_break && skill + this.bot_moveskill >= autocvar_bot_ai_bunnyhop_skilloffset)
1257                 havocbot_bunnyhop(this, dir);
1258
1259         if (dir * v_up >= autocvar_sv_jumpvelocity * 0.5 && IS_ONGROUND(this))
1260                 PHYS_INPUT_BUTTON_JUMP(this) = true;
1261         if (dodge)
1262         {
1263                 if (dodge * v_up > 0 && random() * frametime >= 0.2 * bound(0, (10 - skill - this.bot_dodgeskill) * 0.1, 1))
1264                         PHYS_INPUT_BUTTON_JUMP(this) = true;
1265                 if (dodge * v_up < 0 && random() * frametime >= 0.5 * bound(0, (10 - skill - this.bot_dodgeskill) * 0.1, 1))
1266                         this.havocbot_ducktime = time + 0.3 / bound(0.1, skill + this.bot_dodgeskill, 10);
1267         }
1268 }
1269
1270 entity havocbot_gettarget(entity this, bool secondary)
1271 {
1272         entity best = NULL;
1273         vector eye = CENTER_OR_VIEWOFS(this);
1274         IL_EACH(g_bot_targets, boolean((secondary) ? it.classname == "misc_breakablemodel" : it.classname != "misc_breakablemodel"),
1275         {
1276                 vector v = CENTER_OR_VIEWOFS(it);
1277                 if(vdist(v - eye, <, autocvar_bot_ai_enemydetectionradius))
1278                 if(!best || vlen2(CENTER_OR_VIEWOFS(best) - eye) > vlen2(v - eye))
1279                 if(bot_shouldattack(this, it))
1280                 {
1281                         traceline(eye, v, true, this);
1282                         if (trace_ent == it || trace_fraction >= 1)
1283                                 best = it;
1284                 }
1285         });
1286
1287         return best;
1288 }
1289
1290 void havocbot_chooseenemy(entity this)
1291 {
1292         if (autocvar_bot_nofire || IS_INDEPENDENT_PLAYER(this))
1293         {
1294                 this.enemy = NULL;
1295                 return;
1296         }
1297
1298         if (this.enemy)
1299         {
1300                 if (!bot_shouldattack(this, this.enemy))
1301                 {
1302                         // enemy died or something, find a new target
1303                         this.enemy = NULL;
1304                         this.havocbot_chooseenemy_finished = time;
1305                 }
1306                 else if (this.havocbot_stickenemy_time && time < this.havocbot_stickenemy_time)
1307                 {
1308                         // tracking last chosen enemy
1309                         vector targ_pos = (this.enemy.absmin + this.enemy.absmax) * 0.5;
1310                         traceline(this.origin + this.view_ofs, targ_pos, false, NULL);
1311                         if (trace_ent == this.enemy || trace_fraction == 1)
1312                         if (vdist(targ_pos - this.origin, <, 1000))
1313                         {
1314                                 // remain tracking him for a shot while (case he went after a small corner or pilar
1315                                 this.havocbot_chooseenemy_finished = time + 0.5;
1316                                 return;
1317                         }
1318
1319                         // stop preferring this enemy
1320                         this.havocbot_stickenemy_time = 0;
1321                 }
1322         }
1323         if (time < this.havocbot_chooseenemy_finished)
1324                 return;
1325         this.havocbot_chooseenemy_finished = time + autocvar_bot_ai_enemydetectioninterval;
1326         vector eye = this.origin + this.view_ofs;
1327         entity best = NULL;
1328         float bestrating = autocvar_bot_ai_enemydetectionradius ** 2;
1329
1330         // Backup hit flags
1331         int hf = this.dphitcontentsmask;
1332
1333         // Search for enemies, if no enemy can be seen directly try to look through transparent objects
1334
1335         this.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_CORPSE;
1336
1337         bool scan_transparent = false;
1338         bool scan_secondary_targets = false;
1339         bool have_secondary_targets = false;
1340         while(true)
1341         {
1342                 scan_secondary_targets = false;
1343 LABEL(scan_targets)
1344                 IL_EACH(g_bot_targets, it.bot_attack,
1345                 {
1346                         if(!scan_secondary_targets)
1347                         {
1348                                 if(it.classname == "misc_breakablemodel")
1349                                 {
1350                                         have_secondary_targets = true;
1351                                         continue;
1352                                 }
1353                         }
1354                         else if(it.classname != "misc_breakablemodel")
1355                                 continue;
1356
1357                         vector v = (it.absmin + it.absmax) * 0.5;
1358                         float rating = vlen2(v - eye);
1359                         if (rating < bestrating && bot_shouldattack(this, it))
1360                         {
1361                                 traceline(eye, v, true, this);
1362                                 if (trace_ent == it || trace_fraction >= 1)
1363                                 {
1364                                         best = it;
1365                                         bestrating = rating;
1366                                 }
1367                         }
1368                 });
1369
1370                 if(!best && have_secondary_targets && !scan_secondary_targets)
1371                 {
1372                         scan_secondary_targets = true;
1373                         // restart the loop
1374                         bestrating = autocvar_bot_ai_enemydetectionradius ** 2;
1375                         goto scan_targets;
1376                 }
1377
1378                 // I want to do a second scan if no enemy was found or I don't have weapons
1379                 // TODO: Perform the scan when using the rifle (requires changes on the rifle code)
1380                 if(best || STAT(WEAPONS, this)) // || this.weapon == WEP_RIFLE.m_id
1381                         break;
1382                 if(scan_transparent)
1383                         break;
1384
1385                 // Set flags to see through transparent objects
1386                 this.dphitcontentsmask |= DPCONTENTS_OPAQUE;
1387
1388                 scan_transparent = true;
1389         }
1390
1391         // Restore hit flags
1392         this.dphitcontentsmask = hf;
1393
1394         this.enemy = best;
1395         this.havocbot_stickenemy_time = time + autocvar_bot_ai_enemydetectioninterval_stickingtoenemy;
1396         if(best && best.classname == "misc_breakablemodel")
1397                 this.havocbot_stickenemy_time = 0;
1398 }
1399
1400 float havocbot_chooseweapon_checkreload(entity this, .entity weaponentity, int new_weapon)
1401 {
1402         // bots under this skill cannot find unloaded weapons to reload idly when not in combat,
1403         // so skip this for them, or they'll never get to reload their weapons at all.
1404         // this also allows bots under this skill to be more stupid, and reload more often during combat :)
1405         if(skill < 5)
1406                 return false;
1407
1408         // if this weapon is scheduled for reloading, don't switch to it during combat
1409         if (this.(weaponentity).weapon_load[new_weapon] < 0)
1410         {
1411                 FOREACH(Weapons, it != WEP_Null, {
1412                         if(it.wr_checkammo1(it, this, weaponentity) + it.wr_checkammo2(it, this, weaponentity))
1413                                 return true; // other weapon available
1414                 });
1415         }
1416
1417         return false;
1418 }
1419
1420 void havocbot_chooseweapon(entity this, .entity weaponentity)
1421 {
1422         int i;
1423
1424         // ;)
1425         if(g_weaponarena_weapons == WEPSET(TUBA))
1426         {
1427                 this.(weaponentity).m_switchweapon = WEP_TUBA;
1428                 return;
1429         }
1430
1431         // TODO: clean this up by moving it to weapon code
1432         if(this.enemy==NULL)
1433         {
1434                 // If no weapon was chosen get the first available weapon
1435                 if(this.(weaponentity).m_weapon==WEP_Null)
1436                 FOREACH(Weapons, it != WEP_Null, {
1437                         if(client_hasweapon(this, it, weaponentity, true, false))
1438                         {
1439                                 this.(weaponentity).m_switchweapon = it;
1440                                 return;
1441                         }
1442                 });
1443                 return;
1444         }
1445
1446         // Do not change weapon during the next second after a combo
1447         float f = time - this.lastcombotime;
1448         if(f < 1)
1449                 return;
1450
1451         float w;
1452         float distance; distance=bound(10,vlen(this.origin-this.enemy.origin)-200,10000);
1453
1454         // Should it do a weapon combo?
1455         float af, ct, combo_time, combo;
1456
1457         af = ATTACK_FINISHED(this, weaponentity);
1458         ct = autocvar_bot_ai_weapon_combo_threshold;
1459
1460         // Bots with no skill will be 4 times more slower than "godlike" bots when doing weapon combos
1461         // Ideally this 4 should be calculated as longest_weapon_refire / bot_ai_weapon_combo_threshold
1462         combo_time = time + ct + (ct * ((-0.3*(skill+this.bot_weaponskill))+3));
1463
1464         combo = false;
1465
1466         if(autocvar_bot_ai_weapon_combo)
1467         if(this.(weaponentity).m_weapon.m_id == this.(weaponentity).lastfiredweapon)
1468         if(af > combo_time)
1469         {
1470                 combo = true;
1471                 this.lastcombotime = time;
1472         }
1473
1474         distance *= (2 ** this.bot_rangepreference);
1475
1476         // Custom weapon list based on distance to the enemy
1477         if(bot_custom_weapon){
1478
1479                 // Choose weapons for far distance
1480                 if ( distance > bot_distance_far ) {
1481                         for(i=0; i < REGISTRY_COUNT(Weapons) && bot_weapons_far[i] != -1 ; ++i){
1482                                 w = bot_weapons_far[i];
1483                                 if ( client_hasweapon(this, REGISTRY_GET(Weapons, w), weaponentity, true, false) )
1484                                 {
1485                                         if ((this.(weaponentity).m_weapon.m_id == w && combo) || havocbot_chooseweapon_checkreload(this, weaponentity, w))
1486                                                 continue;
1487                                         this.(weaponentity).m_switchweapon = REGISTRY_GET(Weapons, w);
1488                                         return;
1489                                 }
1490                         }
1491                 }
1492
1493                 // Choose weapons for mid distance
1494                 if ( distance > bot_distance_close) {
1495                         for(i=0; i < REGISTRY_COUNT(Weapons) && bot_weapons_mid[i] != -1 ; ++i){
1496                                 w = bot_weapons_mid[i];
1497                                 if ( client_hasweapon(this, REGISTRY_GET(Weapons, w), weaponentity, true, false) )
1498                                 {
1499                                         if ((this.(weaponentity).m_weapon.m_id == w && combo) || havocbot_chooseweapon_checkreload(this, weaponentity, w))
1500                                                 continue;
1501                                         this.(weaponentity).m_switchweapon = REGISTRY_GET(Weapons, w);
1502                                         return;
1503                                 }
1504                         }
1505                 }
1506
1507                 // Choose weapons for close distance
1508                 for(i=0; i < REGISTRY_COUNT(Weapons) && bot_weapons_close[i] != -1 ; ++i){
1509                         w = bot_weapons_close[i];
1510                         if ( client_hasweapon(this, REGISTRY_GET(Weapons, w), weaponentity, true, false) )
1511                         {
1512                                 if ((this.(weaponentity).m_weapon.m_id == w && combo) || havocbot_chooseweapon_checkreload(this, weaponentity, w))
1513                                         continue;
1514                                 this.(weaponentity).m_switchweapon = REGISTRY_GET(Weapons, w);
1515                                 return;
1516                         }
1517                 }
1518         }
1519 }
1520
1521 void havocbot_aim(entity this)
1522 {
1523         if (time < this.nextaim)
1524                 return;
1525         this.nextaim = time + 0.1;
1526         vector myvel = this.velocity;
1527         if (!this.waterlevel)
1528                 myvel.z = 0;
1529         if(MUTATOR_CALLHOOK(HavocBot_Aim, this)) { /* do nothing */ }
1530         else if (this.enemy)
1531         {
1532                 vector enemyvel = this.enemy.velocity;
1533                 if (!this.enemy.waterlevel)
1534                         enemyvel.z = 0;
1535                 lag_additem(this, time + CS(this).ping, 0, 0, this.enemy, this.origin, myvel, (this.enemy.absmin + this.enemy.absmax) * 0.5, enemyvel);
1536         }
1537         else
1538                 lag_additem(this, time + CS(this).ping, 0, 0, NULL, this.origin, myvel, ( this.goalcurrent.absmin + this.goalcurrent.absmax ) * 0.5, '0 0 0');
1539 }
1540
1541 bool havocbot_moveto_refresh_route(entity this)
1542 {
1543         // Refresh path to goal if necessary
1544         entity wp;
1545         wp = this.havocbot_personal_waypoint;
1546         navigation_goalrating_start(this);
1547         navigation_routerating(this, wp, 10000, 10000);
1548         navigation_goalrating_end(this);
1549         return (this.goalentity != NULL);
1550 }
1551
1552 float havocbot_moveto(entity this, vector pos)
1553 {
1554         entity wp;
1555
1556         if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_GOING)
1557         {
1558                 // Step 4: Move to waypoint
1559                 if(this.havocbot_personal_waypoint==NULL)
1560                 {
1561                         LOG_TRACE("Error: ", this.netname, " trying to walk to a non existent personal waypoint");
1562                         this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_GOING;
1563                         return CMD_STATUS_ERROR;
1564                 }
1565
1566                 if (!bot_strategytoken_taken)
1567                 if(this.havocbot_personal_waypoint_searchtime<time)
1568                 {
1569                         bot_strategytoken_taken = true;
1570                         if(havocbot_moveto_refresh_route(this))
1571                         {
1572                                 LOG_TRACE(this.netname, " walking to its personal waypoint (after ", ftos(this.havocbot_personal_waypoint_failcounter), " failed attempts)");
1573                                 this.havocbot_personal_waypoint_searchtime = time + 10;
1574                                 this.havocbot_personal_waypoint_failcounter = 0;
1575                         }
1576                         else
1577                         {
1578                                 this.havocbot_personal_waypoint_failcounter += 1;
1579                                 this.havocbot_personal_waypoint_searchtime = time + 2;
1580                                 if(this.havocbot_personal_waypoint_failcounter >= 30)
1581                                 {
1582                                         LOG_TRACE("Warning: can't walk to the personal waypoint located at ", vtos(this.havocbot_personal_waypoint.origin));
1583                                         this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING;
1584                                         delete(this.havocbot_personal_waypoint);
1585                                         return CMD_STATUS_ERROR;
1586                                 }
1587                                 else
1588                                         LOG_TRACE(this.netname, " can't walk to its personal waypoint (after ", ftos(this.havocbot_personal_waypoint_failcounter), " failed attempts), trying later");
1589                         }
1590                 }
1591
1592                 if(autocvar_bot_debug_goalstack)
1593                         debuggoalstack(this);
1594
1595
1596                 // Go!
1597                 havocbot_movetogoal(this);
1598
1599                 if (!this.bot_aimdir_executed && this.goalcurrent)
1600                 {
1601                         // Heading
1602                         vector dir = get_closer_dest(this.goalcurrent, this.origin);
1603                         dir -= this.origin + this.view_ofs;
1604                         dir.z = 0;
1605                         bot_aimdir(this, dir, 0);
1606                 }
1607
1608                 if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_REACHED)
1609                 {
1610                         // Step 5: Waypoint reached
1611                         LOG_TRACE(this.netname, "'s personal waypoint reached");
1612                         waypoint_remove(this.havocbot_personal_waypoint);
1613                         this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_REACHED;
1614                         return CMD_STATUS_FINISHED;
1615                 }
1616
1617                 return CMD_STATUS_EXECUTING;
1618         }
1619
1620         // Step 2: Linking waypoint
1621         if(this.aistatus & AI_STATUS_WAYPOINT_PERSONAL_LINKING)
1622         {
1623                 // Wait until it is linked
1624                 if(!this.havocbot_personal_waypoint.wplinked)
1625                 {
1626                         LOG_TRACE(this.netname, " waiting for personal waypoint to be linked");
1627                         return CMD_STATUS_EXECUTING;
1628                 }
1629
1630                 this.havocbot_personal_waypoint_searchtime = time; // so we set the route next frame
1631                 this.aistatus &= ~AI_STATUS_WAYPOINT_PERSONAL_LINKING;
1632                 this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_GOING;
1633
1634                 // Step 3: Route to waypoint
1635                 LOG_TRACE(this.netname, " walking to its personal waypoint");
1636
1637                 return CMD_STATUS_EXECUTING;
1638         }
1639
1640         // Step 1: Spawning waypoint
1641         wp = waypoint_spawnpersonal(this, pos);
1642         if(wp==NULL)
1643         {
1644                 LOG_TRACE("Error: Can't spawn personal waypoint at ",vtos(pos));
1645                 return CMD_STATUS_ERROR;
1646         }
1647
1648         this.havocbot_personal_waypoint = wp;
1649         this.havocbot_personal_waypoint_failcounter = 0;
1650         this.aistatus |= AI_STATUS_WAYPOINT_PERSONAL_LINKING;
1651
1652         // if pos is inside a teleport, then let's mark it as teleport waypoint
1653         IL_EACH(g_teleporters, WarpZoneLib_BoxTouchesBrush(pos, pos, it, NULL),
1654         {
1655                 wp.wpflags |= WAYPOINTFLAG_TELEPORT;
1656                 this.lastteleporttime = 0;
1657         });
1658
1659 /*
1660         if(wp.wpflags & WAYPOINTFLAG_TELEPORT)
1661                 print("routing to a teleporter\n");
1662         else
1663                 print("routing to a non-teleporter\n");
1664 */
1665
1666         return CMD_STATUS_EXECUTING;
1667 }
1668
1669 float havocbot_resetgoal(entity this)
1670 {
1671         navigation_clearroute(this);
1672         return CMD_STATUS_FINISHED;
1673 }
1674
1675 void havocbot_setupbot(entity this)
1676 {
1677         this.bot_ai = havocbot_ai;
1678         this.cmd_moveto = havocbot_moveto;
1679         this.cmd_resetgoal = havocbot_resetgoal;
1680
1681         // NOTE: bot is not player yet
1682         havocbot_chooserole(this);
1683 }
1684
1685 vector havocbot_dodge(entity this)
1686 {
1687         // LordHavoc: disabled because this is too expensive
1688         return '0 0 0';
1689 #if 0
1690         entity head;
1691         vector dodge, v, n;
1692         float danger, bestdanger, vl, d;
1693         dodge = '0 0 0';
1694         bestdanger = -20;
1695         // check for dangerous objects near bot or approaching bot
1696         head = findchainfloat(bot_dodge, true);
1697         while(head)
1698         {
1699                 if (head.owner != this)
1700                 {
1701                         vl = vlen(head.velocity);
1702                         if (vl > autocvar_sv_maxspeed * 0.3)
1703                         {
1704                                 n = normalize(head.velocity);
1705                                 v = this.origin - head.origin;
1706                                 d = v * n;
1707                                 if (d > (0 - head.bot_dodgerating))
1708                                 if (d < (vl * 0.2 + head.bot_dodgerating))
1709                                 {
1710                                         // calculate direction and distance from the flight path, by removing the forward axis
1711                                         v = v - (n * (v * n));
1712                                         danger = head.bot_dodgerating - vlen(v);
1713                                         if (bestdanger < danger)
1714                                         {
1715                                                 bestdanger = danger;
1716                                                 // dodge to the side of the object
1717                                                 dodge = normalize(v);
1718                                         }
1719                                 }
1720                         }
1721                         else
1722                         {
1723                                 danger = head.bot_dodgerating - vlen(head.origin - this.origin);
1724                                 if (bestdanger < danger)
1725                                 {
1726                                         bestdanger = danger;
1727                                         dodge = normalize(this.origin - head.origin);
1728                                 }
1729                         }
1730                 }
1731                 head = head.chain;
1732         }
1733         return dodge;
1734 #endif
1735 }