1 // SONIC ROBO BLAST 2
2 //-----------------------------------------------------------------------------
3 // Copyright (C) 2007-2016 by John "JTE" Muniz.
4 // Copyright (C) 2011-2020 by Sonic Team Junior.
5 //
6 // This program is free software distributed under the
7 // terms of the GNU General Public License, version 2.
8 // See the 'LICENSE' file for more details.
9 //-----------------------------------------------------------------------------
10 /// \file  b_bot.c
11 /// \brief Basic bot handling
12 
13 #include "doomdef.h"
14 #include "d_player.h"
15 #include "g_game.h"
16 #include "r_main.h"
17 #include "p_local.h"
18 #include "b_bot.h"
19 #include "lua_hook.h"
20 
21 // If you want multiple bots, variables like this will
22 // have to be stuffed in something accessible through player_t.
23 static boolean lastForward = false;
24 static boolean lastBlocked = false;
25 static boolean blocked = false;
26 
27 static boolean jump_last = false;
28 static boolean spin_last = false;
29 static UINT8 anxiety = 0;
30 static boolean panic = false;
31 static UINT8 flymode = 0;
32 static boolean spinmode = false;
33 static boolean thinkfly = false;
34 
B_ResetAI(void)35 static inline void B_ResetAI(void)
36 {
37 	jump_last = false;
38 	spin_last = false;
39 	anxiety = 0;
40 	panic = false;
41 	flymode = 0;
42 	spinmode = false;
43 	thinkfly = false;
44 }
45 
B_BuildTailsTiccmd(mobj_t * sonic,mobj_t * tails,ticcmd_t * cmd)46 static void B_BuildTailsTiccmd(mobj_t *sonic, mobj_t *tails, ticcmd_t *cmd)
47 {
48 	boolean forward=false, backward=false, left=false, right=false, jump=false, spin=false;
49 
50 	player_t *player = sonic->player, *bot = tails->player;
51 	ticcmd_t *pcmd = &player->cmd;
52 	boolean water = tails->eflags & MFE_UNDERWATER;
53 	SINT8 flip = P_MobjFlip(tails);
54 	boolean _2d = (tails->flags2 & MF2_TWOD) || twodlevel;
55 	fixed_t scale = tails->scale;
56 
57 	fixed_t dist = P_AproxDistance(sonic->x - tails->x, sonic->y - tails->y);
58 	fixed_t zdist = flip * (sonic->z - tails->z);
59 	angle_t ang = sonic->angle;
60 	fixed_t pmom = P_AproxDistance(sonic->momx, sonic->momy);
61 	fixed_t bmom = P_AproxDistance(tails->momx, tails->momy);
62 	fixed_t followmax = 128 * 8 * scale; // Max follow distance before AI begins to enter "panic" state
63 	fixed_t followthres = 92 * scale; // Distance that AI will try to reach
64 	fixed_t followmin = 32 * scale;
65 	fixed_t comfortheight = 96 * scale;
66 	fixed_t touchdist = 24 * scale;
67 	boolean stalled = (bmom < scale >> 1) && dist > followthres; // Helps to see if the AI is having trouble catching up
68 	boolean samepos = (sonic->x == tails->x && sonic->y == tails->y);
69 
70 	if (!samepos)
71 		ang = R_PointToAngle2(tails->x, tails->y, sonic->x, sonic->y);
72 
73 	// We can't follow Sonic if he's not around!
74 	if (!sonic || sonic->health <= 0)
75 		return;
76 
77 	// Lua can handle it!
78 	if (LUAh_BotAI(sonic, tails, cmd))
79 		return;
80 
81 	if (tails->player->powers[pw_carry] == CR_MACESPIN || tails->player->powers[pw_carry] == CR_GENERIC)
82 	{
83 		boolean isrelevant = (sonic->player->powers[pw_carry] == CR_MACESPIN || sonic->player->powers[pw_carry] == CR_GENERIC);
84 		dist = P_AproxDistance(tails->x-sonic->x, tails->y-sonic->y);
85 		if (sonic->player->cmd.buttons & BT_JUMP && (sonic->player->pflags & PF_JUMPED) && isrelevant)
86 			cmd->buttons |= BT_JUMP;
87 		if (isrelevant)
88 		{
89 			cmd->forwardmove = sonic->player->cmd.forwardmove;
90 			cmd->angleturn = abs((signed)(tails->angle - sonic->angle))>>16;
91 			if (sonic->angle < tails->angle)
92 				cmd->angleturn = -cmd->angleturn;
93 		} else if (dist > FixedMul(512*FRACUNIT, tails->scale))
94 			cmd->buttons |= BT_JUMP;
95 		return;
96 	}
97 
98 	// Adapted from CobaltBW's tails_AI.wad
99 
100 	// Check water
101 	if (water)
102 	{
103 		followmin = 0;
104 		followthres = 16*scale;
105 		followmax >>= 1;
106 		thinkfly = false;
107 	}
108 
109 	// Check anxiety
110 	if (spinmode)
111 	{
112 		anxiety = 0;
113 		panic = false;
114 	}
115 	else if (dist > followmax || zdist > comfortheight || stalled)
116 	{
117 		anxiety = min(anxiety + 2, 70);
118 		if (anxiety >= 70)
119 			panic = true;
120 	}
121 	else
122 	{
123 		anxiety = max(anxiety - 1, 0);
124 		panic = false;
125 	}
126 
127 	// Orientation
128 	if (bot->pflags & (PF_SPINNING|PF_STARTDASH))
129 	{
130 		cmd->angleturn = (sonic->angle - tails->angle) >> 16; // NOT FRACBITS DAMNIT
131 	}
132 	else if (flymode == 2)
133 	{
134 		cmd->angleturn = sonic->player->cmd.angleturn - (tails->angle >> 16);
135 	}
136 	else
137 	{
138 		cmd->angleturn = (ang - tails->angle) >> 16; // NOT FRACBITS DAMNIT
139 	}
140 
141 	// ********
142 	// FLY MODE
143 	// spinmode check
144 	if (spinmode || player->exiting)
145 		thinkfly = false;
146 	else
147 	{
148 		// Activate co-op flight
149 		if (thinkfly && player->pflags & PF_JUMPED)
150 		{
151 			if (!jump_last)
152 			{
153 				jump = true;
154 				flymode = 1;
155 				thinkfly = false;
156 				bot->pflags |= PF_CANCARRY;
157 			}
158 		}
159 
160 		// Check positioning
161 		// Thinker for co-op flight
162 		if (!(water || pmom || bmom)
163 			&& (dist < touchdist && !samepos)
164 			&& !(pcmd->forwardmove || pcmd->sidemove || player->dashspeed)
165 			&& P_IsObjectOnGround(sonic) && P_IsObjectOnGround(tails)
166 			&& !(player->pflags & PF_STASIS)
167 			&& bot->charability == CA_FLY)
168 				thinkfly = true;
169 		else
170 			thinkfly = false;
171 
172 		// Set carried state
173 		if (player->powers[pw_carry] == CR_PLAYER && sonic->tracer == tails)
174 		{
175 			flymode = 2;
176 		}
177 
178 		// Ready for takeoff
179 		if (flymode == 1)
180 		{
181 			thinkfly = false;
182 			if (zdist < -64*scale || (flip * tails->momz) > scale) // Make sure we're not too high up
183 				spin = true;
184 			else if (!jump_last)
185 				jump = true;
186 
187 			// Abort if the player moves away or spins
188 			if (dist > followthres || player->dashspeed)
189 				flymode = 0;
190 		}
191 		// Read player inputs while carrying
192 		else if (flymode == 2)
193 		{
194 			cmd->forwardmove = pcmd->forwardmove;
195 			cmd->sidemove = pcmd->sidemove;
196 			if (pcmd->buttons & BT_SPIN)
197 			{
198 				spin = true;
199 				jump = false;
200 			}
201 			else if (!jump_last)
202 				jump = true;
203 			// End flymode
204 			if (player->powers[pw_carry] != CR_PLAYER)
205 			{
206 				flymode = 0;
207 			}
208 		}
209 	}
210 
211 	if (flymode && P_IsObjectOnGround(tails) && !(pcmd->buttons & BT_JUMP))
212 		flymode = 0;
213 
214 	// ********
215 	// SPINNING
216 	if (panic || flymode || !(player->pflags & PF_SPINNING) || (player->pflags & PF_JUMPED))
217 		spinmode = false;
218 	else
219 	{
220 		if (!_2d)
221 		{
222 			// Spindash
223 			if (player->dashspeed)
224 			{
225 				if (dist < followthres && dist > touchdist) // Do positioning
226 				{
227 					cmd->angleturn = (ang - tails->angle) >> 16; // NOT FRACBITS DAMNIT
228 					cmd->forwardmove = 50;
229 					spinmode = true;
230 				}
231 				else if (dist < touchdist)
232 				{
233 					if (!bmom && (!(bot->pflags & PF_SPINNING) || (bot->dashspeed && bot->pflags & PF_SPINNING)))
234 					{
235 						cmd->angleturn = (sonic->angle - tails->angle) >> 16; // NOT FRACBITS DAMNIT
236 						spin = true;
237 					}
238 					spinmode = true;
239 				}
240 				else
241 					spinmode = false;
242 			}
243 			// Spin
244 			else if (player->dashspeed == bot->dashspeed && player->pflags & PF_SPINNING)
245 			{
246 				if (bot->pflags & PF_SPINNING || !spin_last)
247 				{
248 					spin = true;
249 					cmd->angleturn = (sonic->angle - tails->angle) >> 16; // NOT FRACBITS DAMNIT
250 					cmd->forwardmove = MAXPLMOVE;
251 					spinmode = true;
252 				}
253 				else
254 					spinmode = false;
255 			}
256 		}
257 		// 2D mode
258 		else
259 		{
260 			if (((player->dashspeed && !bmom) || (player->dashspeed == bot->dashspeed && (player->pflags & PF_SPINNING)))
261 				&& ((bot->pflags & PF_SPINNING) || !spin_last))
262 			{
263 				spin = true;
264 				spinmode = true;
265 			}
266 		}
267 	}
268 
269 	// ********
270 	// FOLLOW
271 	if (!(flymode || spinmode))
272 	{
273 		// Too far
274 		if (panic || dist > followthres)
275 		{
276 			if (!_2d)
277 				cmd->forwardmove = MAXPLMOVE;
278 			else if (sonic->x > tails->x)
279 				cmd->sidemove = MAXPLMOVE;
280 			else
281 				cmd->sidemove = -MAXPLMOVE;
282 		}
283 		// Within threshold
284 		else if (!panic && dist > followmin && abs(zdist) < 192*scale)
285 		{
286 			if (!_2d)
287 				cmd->forwardmove = FixedHypot(pcmd->forwardmove, pcmd->sidemove);
288 			else
289 				cmd->sidemove = pcmd->sidemove;
290 		}
291 		// Below min
292 		else if (dist < followmin)
293 		{
294 			// Copy inputs
295 			cmd->angleturn = (sonic->angle - tails->angle) >> 16; // NOT FRACBITS DAMNIT
296 			bot->drawangle = ang;
297 			cmd->forwardmove = 8 * pcmd->forwardmove / 10;
298 			cmd->sidemove = 8 * pcmd->sidemove / 10;
299 		}
300 	}
301 
302 	// ********
303 	// JUMP
304 	if (!(flymode || spinmode))
305 	{
306 		// Flying catch-up
307 		if (bot->pflags & PF_THOKKED)
308 		{
309 			cmd->forwardmove = min(MAXPLMOVE, (dist/scale)>>3);
310 			if (zdist < -64*scale)
311 				spin = true;
312 			else if (zdist > 0 && !jump_last)
313 				jump = true;
314 		}
315 
316 		// Just landed
317 		if (tails->eflags & MFE_JUSTHITFLOOR)
318 			jump = false;
319 		// Start jump
320 		else if (!jump_last && !(bot->pflags & PF_JUMPED) //&& !(player->pflags & PF_SPINNING)
321 			&& ((zdist > 32*scale && player->pflags & PF_JUMPED) // Following
322 				|| (zdist > 64*scale && panic) // Vertical catch-up
323 				|| (stalled && anxiety > 20 && bot->powers[pw_carry] == CR_NONE)
324 				//|| (bmom < scale>>3 && dist > followthres && !(bot->powers[pw_carry])) // Stopped & not in carry state
325 				|| (bot->pflags & PF_SPINNING && !(bot->pflags & PF_JUMPED)))) // Spinning
326 					jump = true;
327 		// Hold jump
328 		else if (bot->pflags & PF_JUMPED && jump_last && tails->momz*flip > 0 && (zdist > 0 || panic))
329 			jump = true;
330 		// Start flying
331 		else if (bot->pflags & PF_JUMPED && panic && !jump_last && bot->charability == CA_FLY)
332 			jump = true;
333 	}
334 
335 	// ********
336 	// HISTORY
337 	jump_last = jump;
338 	spin_last = spin;
339 
340 	// Turn the virtual keypresses into ticcmd_t.
341 	B_KeysToTiccmd(tails, cmd, forward, backward, left, right, false, false, jump, spin);
342 
343 	// Update our status
344 	lastForward = forward;
345 	lastBlocked = blocked;
346 	blocked = false;
347 }
348 
B_BuildTiccmd(player_t * player,ticcmd_t * cmd)349 void B_BuildTiccmd(player_t *player, ticcmd_t *cmd)
350 {
351 	// Can't build a ticcmd if we aren't spawned...
352 	if (!player->mo)
353 		return;
354 
355 	if (player->playerstate == PST_DEAD)
356 	{
357 		if (B_CheckRespawn(player))
358 			cmd->buttons |= BT_JUMP;
359 		return;
360 	}
361 
362 	// Bot AI isn't programmed in analog.
363 	CV_SetValue(&cv_analog[1], false);
364 
365 	// Let Lua scripts build ticcmds
366 	if (LUAh_BotTiccmd(player, cmd))
367 		return;
368 
369 	// We don't have any main character AI, sorry. D:
370 	if (player-players == consoleplayer)
371 		return;
372 
373 	// Basic Tails AI
374 	B_BuildTailsTiccmd(players[consoleplayer].mo, player->mo, cmd);
375 }
376 
B_KeysToTiccmd(mobj_t * mo,ticcmd_t * cmd,boolean forward,boolean backward,boolean left,boolean right,boolean strafeleft,boolean straferight,boolean jump,boolean spin)377 void B_KeysToTiccmd(mobj_t *mo, ticcmd_t *cmd, boolean forward, boolean backward, boolean left, boolean right, boolean strafeleft, boolean straferight, boolean jump, boolean spin)
378 {
379 	// don't try to do stuff if your sonic is in a minecart or something
380 	if (players[consoleplayer].powers[pw_carry] && players[consoleplayer].powers[pw_carry] != CR_PLAYER)
381 		return;
382 	// Turn the virtual keypresses into ticcmd_t.
383 	if (twodlevel || mo->flags2 & MF2_TWOD) {
384 		if (players[consoleplayer].climbing
385 		|| mo->player->pflags & PF_GLIDING) {
386 			// Don't mess with bot inputs during these unhandled movement conditions.
387 			// The normal AI doesn't use abilities, so custom AI should be sending us exactly what it wants anyway.
388 			if (forward)
389 				cmd->forwardmove += MAXPLMOVE<<FRACBITS>>16;
390 			if (backward)
391 				cmd->forwardmove -= MAXPLMOVE<<FRACBITS>>16;
392 			if (left || strafeleft)
393 				cmd->sidemove -= MAXPLMOVE<<FRACBITS>>16;
394 			if (right || straferight)
395 				cmd->sidemove += MAXPLMOVE<<FRACBITS>>16;
396 		} else {
397 			// In standard 2D mode, interpret "forward" as "the way you're facing" and everything else as "the way you're not facing"
398 			if (left || right)
399 				backward = true;
400 			left = right = false;
401 			if (forward) {
402 				if (mo->angle < ANGLE_90 || mo->angle > ANGLE_270)
403 					right = true;
404 				else
405 					left = true;
406 			} else if (backward) {
407 				if (mo->angle < ANGLE_90 || mo->angle > ANGLE_270)
408 					left = true;
409 				else
410 					right = true;
411 			}
412 			if (left || strafeleft)
413 				cmd->sidemove -= MAXPLMOVE<<FRACBITS>>16;
414 			if (right || straferight)
415 				cmd->sidemove += MAXPLMOVE<<FRACBITS>>16;
416 		}
417 	} else {
418 		angle_t angle;
419 		if (forward)
420 			cmd->forwardmove += MAXPLMOVE<<FRACBITS>>16;
421 		if (backward)
422 			cmd->forwardmove -= MAXPLMOVE<<FRACBITS>>16;
423 		if (left)
424 			cmd->angleturn += 1280;
425 		if (right)
426 			cmd->angleturn -= 1280;
427 		if (strafeleft)
428 			cmd->sidemove -= MAXPLMOVE<<FRACBITS>>16;
429 		if (straferight)
430 			cmd->sidemove += MAXPLMOVE<<FRACBITS>>16;
431 
432 		// cap inputs so the bot can't accelerate faster diagonally
433 		angle = R_PointToAngle2(0, 0, cmd->sidemove << FRACBITS, cmd->forwardmove << FRACBITS);
434 		{
435 			INT32 maxforward = abs(P_ReturnThrustY(NULL, angle, MAXPLMOVE));
436 			INT32 maxside = abs(P_ReturnThrustX(NULL, angle, MAXPLMOVE));
437 			cmd->forwardmove = max(min(cmd->forwardmove, maxforward), -maxforward);
438 			cmd->sidemove = max(min(cmd->sidemove, maxside), -maxside);
439 		}
440 	}
441 	if (jump)
442 		cmd->buttons |= BT_JUMP;
443 	if (spin)
444 		cmd->buttons |= BT_SPIN;
445 }
446 
B_MoveBlocked(player_t * player)447 void B_MoveBlocked(player_t *player)
448 {
449 	(void)player;
450 	blocked = true;
451 }
452 
B_CheckRespawn(player_t * player)453 boolean B_CheckRespawn(player_t *player)
454 {
455 	mobj_t *sonic = players[consoleplayer].mo;
456 	mobj_t *tails = player->mo;
457 
458 	// We can't follow Sonic if he's not around!
459 	if (!sonic || sonic->health <= 0)
460 		return false;
461 
462 	// B_RespawnBot doesn't do anything if the condition above this isn't met
463 	{
464 		UINT8 shouldForce = LUAh_BotRespawn(sonic, tails);
465 
466 		if (P_MobjWasRemoved(sonic) || P_MobjWasRemoved(tails))
467 			return (shouldForce == 1); // mobj was removed
468 
469 		if (shouldForce == 1)
470 			return true;
471 		else if (shouldForce == 2)
472 			return false;
473 	}
474 
475 	// Check if Sonic is busy first.
476 	// If he's doing any of these things, he probably doesn't want to see us.
477 	if (sonic->player->pflags & (PF_GLIDING|PF_SLIDING|PF_BOUNCING)
478 	|| (sonic->player->panim != PA_IDLE && sonic->player->panim != PA_WALK)
479 	|| (sonic->player->powers[pw_carry] && sonic->player->powers[pw_carry] != CR_PLAYER))
480 		return false;
481 
482 	// Low ceiling, do not want!
483 	if (sonic->eflags & MFE_VERTICALFLIP)
484 	{
485 		if (sonic->z - sonic->floorz < (sonic->player->exiting ? 5 : 2)*sonic->height)
486 			return false;
487 	}
488 	else if (sonic->ceilingz - sonic->z < (sonic->player->exiting ? 6 : 3)*sonic->height)
489 		return false;
490 
491 	// If you're dead, wait a few seconds to respawn.
492 	if (player->playerstate == PST_DEAD) {
493 		if (player->deadtimer > 4*TICRATE)
494 			return true;
495 		return false;
496 	}
497 
498 	// If you can't see Sonic, I guess we should?
499 	if (!P_CheckSight(sonic, tails) && P_AproxDistance(P_AproxDistance(tails->x-sonic->x, tails->y-sonic->y), tails->z-sonic->z) > FixedMul(1024*FRACUNIT, tails->scale))
500 		return true;
501 	return false;
502 }
503 
B_RespawnBot(INT32 playernum)504 void B_RespawnBot(INT32 playernum)
505 {
506 	player_t *player = &players[playernum];
507 	fixed_t x,y,z;
508 	mobj_t *sonic = players[consoleplayer].mo;
509 	mobj_t *tails;
510 
511 	if (!sonic || sonic->health <= 0)
512 		return;
513 
514 	B_ResetAI();
515 
516 	player->bot = 1;
517 	P_SpawnPlayer(playernum);
518 	tails = player->mo;
519 
520 	x = sonic->x;
521 	y = sonic->y;
522 	if (sonic->eflags & MFE_VERTICALFLIP) {
523 		tails->eflags |= MFE_VERTICALFLIP;
524 		z = sonic->z - (512*sonic->scale);
525 		if (z < sonic->floorz)
526 			z = sonic->floorz;
527 	} else {
528 		z = sonic->z + sonic->height + (512*sonic->scale);
529 		if (z > sonic->ceilingz - sonic->height)
530 			z = sonic->ceilingz - sonic->height;
531 	}
532 
533 	if (sonic->flags2 & MF2_OBJECTFLIP)
534 		tails->flags2 |= MF2_OBJECTFLIP;
535 	if (sonic->flags2 & MF2_TWOD)
536 		tails->flags2 |= MF2_TWOD;
537 	if (sonic->eflags & MFE_UNDERWATER)
538 		tails->eflags |= MFE_UNDERWATER;
539 	player->powers[pw_underwater] = sonic->player->powers[pw_underwater];
540 	player->powers[pw_spacetime] = sonic->player->powers[pw_spacetime];
541 	player->powers[pw_gravityboots] = sonic->player->powers[pw_gravityboots];
542 	player->powers[pw_nocontrol] = sonic->player->powers[pw_nocontrol];
543 	player->acceleration = sonic->player->acceleration;
544 	player->accelstart = sonic->player->accelstart;
545 	player->thrustfactor = sonic->player->thrustfactor;
546 	player->normalspeed = sonic->player->normalspeed;
547 	player->pflags |= PF_AUTOBRAKE|(sonic->player->pflags & PF_DIRECTIONCHAR);
548 
549 	P_TeleportMove(tails, x, y, z);
550 	if (player->charability == CA_FLY)
551 	{
552 		P_SetPlayerMobjState(tails, S_PLAY_FLY);
553 		tails->player->powers[pw_tailsfly] = (UINT16)-1;
554 	}
555 	else
556 		P_SetPlayerMobjState(tails, S_PLAY_FALL);
557 	P_SetScale(tails, sonic->scale);
558 	tails->destscale = sonic->destscale;
559 }
560 
B_HandleFlightIndicator(player_t * player)561 void B_HandleFlightIndicator(player_t *player)
562 {
563 	mobj_t *tails = player->mo;
564 
565 	if (!tails)
566 		return;
567 
568 	if (thinkfly && player->bot == 1 && tails->health)
569 	{
570 		if (!tails->hnext)
571 		{
572 			P_SetTarget(&tails->hnext, P_SpawnMobjFromMobj(tails, 0, 0, 0, MT_OVERLAY));
573 			if (tails->hnext)
574 			{
575 				P_SetTarget(&tails->hnext->target, tails);
576 				P_SetTarget(&tails->hnext->hprev, tails);
577 				P_SetMobjState(tails->hnext, S_FLIGHTINDICATOR);
578 			}
579 		}
580 	}
581 	else if (tails->hnext && tails->hnext->type == MT_OVERLAY && tails->hnext->state == states+S_FLIGHTINDICATOR)
582 	{
583 		P_RemoveMobj(tails->hnext);
584 		P_SetTarget(&tails->hnext, NULL);
585 	}
586 }
587