Subversion Repositories Games.Descent

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 pmbaty 1
/*
2
 * Portions of this file are copyright Rebirth contributors and licensed as
3
 * described in COPYING.txt.
4
 * Portions of this file are copyright Parallax Software and licensed
5
 * according to the Parallax license below.
6
 * See COPYING.txt for license details.
7
 
8
THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
9
SOFTWARE CORPORATION ("PARALLAX").  PARALLAX, IN DISTRIBUTING THE CODE TO
10
END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
11
ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
12
IN USING, DISPLAYING,  AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
13
SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
14
FREE PURPOSES.  IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
15
CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES.  THE END-USER UNDERSTANDS
16
AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
17
COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION.  ALL RIGHTS RESERVED.
18
*/
19
 
20
/*
21
 *
22
 * Escort robot behavior.
23
 *
24
 */
25
 
26
#include <stdio.h>              // for printf()
27
#include <stdlib.h>             // for rand() and qsort()
28
#include <string.h>             // for memset()
29
 
30
#include "window.h"
31
#include "console.h"
32
#include "vecmat.h"
33
#include "gr.h"
34
#include "gameseg.h"
35
#include "3d.h"
36
#include "palette.h"
37
#include "timer.h"
38
#include "u_mem.h"
39
 
40
#include "object.h"
41
#include "dxxerror.h"
42
#include "ai.h"
43
#include "robot.h"
44
#include "fvi.h"
45
#include "physics.h"
46
#include "wall.h"
47
#include "player.h"
48
#include "fireball.h"
49
#include "game.h"
50
#include "powerup.h"
51
#include "hudmsg.h"
52
#include "cntrlcen.h"
53
#include "gauges.h"
54
#include "event.h"
55
#include "key.h"
56
#include "fuelcen.h"
57
#include "sounds.h"
58
#include "screens.h"
59
#include "text.h"
60
#include "gamefont.h"
61
#include "newmenu.h"
62
#include "playsave.h"
63
#include "gameseq.h"
64
#include "automap.h"
65
#include "laser.h"
66
#include "escort.h"
67
 
68
#include "segiter.h"
69
#include "compiler-range_for.h"
70
#include "d_range.h"
71
#include "partial_range.h"
72
#include <utility>
73
 
74
#if DXX_USE_EDITOR
75
#include "editor/editor.h"
76
#endif
77
 
78
namespace dsx {
79
 
80
static void show_escort_menu(const std::array<char, 300> &);
81
static void say_escort_goal(escort_goal_t goal_num);
82
 
83
constexpr std::array<char[12], ESCORT_GOAL_MARKER9> Escort_goal_text = {{
84
        "BLUE KEY",
85
        "YELLOW KEY",
86
        "RED KEY",
87
        "REACTOR",
88
        "EXIT",
89
        "ENERGY",
90
        "ENERGYCEN",
91
        "SHIELD",
92
        "POWERUP",
93
        "ROBOT",
94
        "HOSTAGES",
95
        "SPEW",
96
        "SCRAM",
97
        "EXIT",
98
        "BOSS",
99
        "MARKER 1",
100
        "MARKER 2",
101
        "MARKER 3",
102
        "MARKER 4",
103
        "MARKER 5",
104
        "MARKER 6",
105
        "MARKER 7",
106
        "MARKER 8",
107
        "MARKER 9",
108
// -- too much work --  "KAMIKAZE  "
109
}};
110
 
111
constexpr std::integral_constant<unsigned, 200> Max_escort_length{};
112
 
113
void init_buddy_for_level(void)
114
{
115
        auto &Objects = LevelUniqueObjectState.Objects;
116
        auto &vmobjptridx = Objects.vmptridx;
117
        auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
118
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
119
        BuddyState = {};
120
        BuddyState.Buddy_gave_hint_count = 5;
121
        BuddyState.Looking_for_marker = game_marker_index::None;
122
        BuddyState.Escort_goal_object = ESCORT_GOAL_UNSPECIFIED;
123
        BuddyState.Escort_special_goal = ESCORT_GOAL_UNSPECIFIED;
124
        BuddyState.Last_buddy_key = -1;
125
        BuddyState.Buddy_sorry_time = -F1_0;
126
        BuddyState.Buddy_last_seen_player = 0;
127
        BuddyState.Buddy_last_missile_time = 0;
128
        BuddyState.Last_time_buddy_gave_hint = 0;
129
        BuddyState.Last_come_back_message_time = 0;
130
        BuddyState.Escort_last_path_created = 0;
131
        BuddyState.Buddy_last_player_path_created = 0;
132
        BuddyState.Last_buddy_message_time = 0;
133
        BuddyState.Buddy_objnum = find_escort(vmobjptridx, Robot_info);
134
}
135
 
136
//      -----------------------------------------------------------------------------
137
//      See if segment from curseg through sidenum is reachable.
138
//      Return true if it is reachable, else return false.
139
static int segment_is_reachable(const vmobjptr_t robot, const shared_segment &segp, int sidenum, const player_flags powerup_flags)
140
{
141
        const auto wall_num = segp.sides[sidenum].wall_num;
142
 
143
        //      If no wall, then it is reachable
144
        if (wall_num == wall_none)
145
                return 1;
146
 
147
        return ai_door_is_openable(robot, powerup_flags, segp, sidenum);
148
 
149
// -- MK, 10/17/95 -- 
150
// -- MK, 10/17/95 --   //      Hmm, a closed wall.  I think this mean not reachable.
151
// -- MK, 10/17/95 --   if (Walls[wall_num].type == WALL_CLOSED)
152
// -- MK, 10/17/95 --           return 0;
153
// -- MK, 10/17/95 -- 
154
// -- MK, 10/17/95 --   if (Walls[wall_num].type == WALL_DOOR) {
155
// -- MK, 10/17/95 --           if (Walls[wall_num].keys == KEY_NONE) {
156
// -- MK, 10/17/95 --                   return 1;               //      @MK, 10/17/95: Be consistent with ai_door_is_openable
157
// -- MK, 10/17/95 -- // --                     if (Walls[wall_num].flags & WALL_DOOR_LOCKED)
158
// -- MK, 10/17/95 -- // --                             return 0;
159
// -- MK, 10/17/95 -- // --                     else
160
// -- MK, 10/17/95 -- // --                             return 1;
161
// -- MK, 10/17/95 --           } else if (Walls[wall_num].keys == KEY_BLUE)
162
// -- MK, 10/17/95 --                   return (Players[Player_num].flags & PLAYER_FLAGS_BLUE_KEY);
163
// -- MK, 10/17/95 --           else if (Walls[wall_num].keys == KEY_GOLD)
164
// -- MK, 10/17/95 --                   return (Players[Player_num].flags & PLAYER_FLAGS_GOLD_KEY);
165
// -- MK, 10/17/95 --           else if (Walls[wall_num].keys == KEY_RED)
166
// -- MK, 10/17/95 --                   return (Players[Player_num].flags & PLAYER_FLAGS_RED_KEY);
167
// -- MK, 10/17/95 --           else
168
// -- MK, 10/17/95 --                   Int3(); //      Impossible!  Doesn't have no key, but doesn't have any key!
169
// -- MK, 10/17/95 --   } else
170
// -- MK, 10/17/95 --           return 1;
171
// -- MK, 10/17/95 -- 
172
// -- MK, 10/17/95 --   Int3(); //      Hmm, thought 'if' above had to return!
173
// -- MK, 10/17/95 --   return 0;
174
 
175
}
176
 
177
 
178
//      -----------------------------------------------------------------------------
179
//      Create a breadth-first list of segments reachable from current segment.
180
//      max_segs is maximum number of segments to search.  Use MAX_SEGMENTS to search all.
181
//      On exit, *length <= max_segs.
182
//      Input:
183
//              start_seg
184
//      Output:
185
//              bfs_list:       array of shorts, each reachable segment.  Includes start segment.
186
//              length:         number of elements in bfs_list
187
std::size_t create_bfs_list(const vmobjptr_t robot, const vcsegidx_t start_seg, const player_flags powerup_flags, segnum_t *const bfs_list, std::size_t max_segs)
188
{
189
        std::size_t head = 0, tail = 0;
190
        visited_segment_bitarray_t visited;
191
        bfs_list[head++] = start_seg;
192
        visited[start_seg] = true;
193
 
194
        while ((head != tail) && (head < max_segs)) {
195
                auto curseg = bfs_list[tail++];
196
                const auto &&cursegp = vcsegptr(curseg);
197
                for (int i=0; i<MAX_SIDES_PER_SEGMENT; i++) {
198
                        auto connected_seg = cursegp->children[i];
199
 
200
                        if (IS_CHILD(connected_seg) && (!visited[connected_seg])) {
201
                                if (segment_is_reachable(robot, cursegp, i, powerup_flags)) {
202
                                        bfs_list[head++] = connected_seg;
203
                                        if (head >= max_segs)
204
                                                break;
205
                                        visited[connected_seg] = true;
206
                                        Assert(head < MAX_SEGMENTS);
207
                                }
208
                        }
209
                }
210
        }
211
        return head;
212
}
213
 
214
//      -----------------------------------------------------------------------------
215
//      Return true if ok for buddy to talk, else return false.
216
//      Buddy is allowed to talk if the segment he is in does not contain a blastable wall that has not been blasted
217
//      AND he has never yet, since being initialized for level, been allowed to talk.
218
static uint8_t ok_for_buddy_to_talk(void)
219
{
220
        auto &Objects = LevelUniqueObjectState.Objects;
221
        auto &vmobjptridx = Objects.vmptridx;
222
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
223
        const auto Buddy_objnum = BuddyState.Buddy_objnum;
224
        if (Buddy_objnum == object_none)
225
                return 0;
226
 
227
        const vmobjptridx_t buddy = vmobjptridx(Buddy_objnum);
228
        const auto buddy_type = buddy->type;
229
        if (buddy_type != OBJ_ROBOT) {
230
                BuddyState.Buddy_allowed_to_talk = 0;
231
                BuddyState.Buddy_objnum = object_none;
232
                con_printf(CON_URGENT, "BUG: buddy is object %u, but that object is type %u.", Buddy_objnum.get_unchecked_index(), buddy_type);
233
                return 0;
234
        }
235
 
236
        if (const auto Buddy_allowed_to_talk = BuddyState.Buddy_allowed_to_talk)
237
                return Buddy_allowed_to_talk;
238
 
239
        const shared_segment &segp = vcsegptr(buddy->segnum);
240
 
241
        auto &Walls = LevelUniqueWallSubsystemState.Walls;
242
        auto &vcwallptr = Walls.vcptr;
243
        for (int i=0; i<MAX_SIDES_PER_SEGMENT; i++) {
244
                const auto wall_num = segp.sides[i].wall_num;
245
 
246
                if (wall_num != wall_none) {
247
                        auto &w = *vcwallptr(wall_num);
248
                        if (w.type == WALL_BLASTABLE && !(w.flags & WALL_BLASTED))
249
                                return 0;
250
                }
251
 
252
                //      Check one level deeper.
253
                const auto child = segp.children[i];
254
                if (IS_CHILD(child))
255
                {
256
                        const shared_segment &cseg = *vcsegptr(child);
257
                        range_for (const auto &j, cseg.sides)
258
                        {
259
                                auto wall2 = j.wall_num;
260
                                if (wall2 != wall_none) {
261
                                        auto &w = *vcwallptr(wall2);
262
                                        if (w.type == WALL_BLASTABLE && !(w.flags & WALL_BLASTED))
263
                                                return 0;
264
                                }
265
                        }
266
                }
267
        }
268
 
269
        BuddyState.Buddy_allowed_to_talk = 1;
270
        return 1;
271
}
272
 
273
static void record_escort_goal_accomplished()
274
{
275
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
276
        if (ok_for_buddy_to_talk()) {
277
                digi_play_sample_once(SOUND_BUDDY_MET_GOAL, F1_0);
278
                BuddyState.Escort_goal_objidx = object_none;
279
                BuddyState.Escort_goal_reachable = d_unique_buddy_state::Escort_goal_reachability::unreachable;
280
                BuddyState.Looking_for_marker = game_marker_index::None;
281
                BuddyState.Escort_goal_object = ESCORT_GOAL_UNSPECIFIED;
282
                BuddyState.Escort_special_goal = ESCORT_GOAL_UNSPECIFIED;
283
        }
284
}
285
 
286
//      --------------------------------------------------------------------------------------------
287
void detect_escort_goal_fuelcen_accomplished()
288
{
289
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
290
        if (!BuddyState.Buddy_allowed_to_talk)
291
                return;
292
        if (BuddyState.Escort_special_goal == ESCORT_GOAL_ENERGYCEN)
293
                record_escort_goal_accomplished();
294
}
295
 
296
void detect_escort_goal_accomplished(const vmobjptridx_t index)
297
{
298
        auto &Objects = LevelUniqueObjectState.Objects;
299
        auto &vcobjptr = Objects.vcptr;
300
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
301
        if (!BuddyState.Buddy_allowed_to_talk)
302
                return;
303
 
304
        // If goal is not an object (FUELCEN, EXIT, SCRAM), bail. FUELCEN is handled in detect_escort_goal_fuelcen_accomplished(), EXIT and SCRAM are never accomplished.
305
        if (BuddyState.Escort_goal_reachable == d_unique_buddy_state::Escort_goal_reachability::unreachable)
306
                return;
307
        const auto Escort_goal_objidx = BuddyState.Escort_goal_objidx;
308
        if (Escort_goal_objidx == object_none)
309
        {
310
                con_printf(CON_URGENT, "BUG: buddy goal is reachable, but goal object is object_none");
311
                return;
312
        }
313
 
314
        // See if goal found was a key.  Need to handle default goals differently.
315
        // Note, no buddy_met_goal sound when blow up reactor or exit.  Not great, but ok
316
        // since for reactor, noisy, for exit, buddy is disappearing.
317
        if (BuddyState.Escort_special_goal == ESCORT_GOAL_UNSPECIFIED && Escort_goal_objidx == index)
318
        {
319
                record_escort_goal_accomplished();
320
                return;
321
        }
322
 
323
        if (index->type == OBJ_POWERUP)  {
324
                const auto index_id = get_powerup_id(index);
325
                escort_goal_t goal_key;
326
                if ((index_id == POW_KEY_BLUE && (goal_key = ESCORT_GOAL_BLUE_KEY, true)) ||
327
                        (index_id == POW_KEY_GOLD && (goal_key = ESCORT_GOAL_GOLD_KEY, true)) ||
328
                        (index_id == POW_KEY_RED && (goal_key = ESCORT_GOAL_RED_KEY, true))
329
                )
330
                {
331
                        if (BuddyState.Escort_goal_object == goal_key)
332
                        {
333
                                record_escort_goal_accomplished();
334
                                return;
335
                        }
336
                }
337
        }
338
        if (BuddyState.Escort_special_goal != ESCORT_GOAL_UNSPECIFIED)
339
        {
340
                if (index->type == OBJ_POWERUP && BuddyState.Escort_special_goal == ESCORT_GOAL_POWERUP)
341
                        record_escort_goal_accomplished();      //      Any type of powerup picked up will do.
342
                else
343
                {
344
                        auto &egi_obj = *vcobjptr(Escort_goal_objidx);
345
                        if (index->type == egi_obj.type && index->id == egi_obj.id)
346
                                // Note: This will help a little bit in making the buddy believe a goal is satisfied.  Won't work for a general goal like "find any powerup"
347
                                // because of the insistence of both type and id matching.
348
                                record_escort_goal_accomplished();
349
                }
350
        }
351
}
352
 
353
#define DXX_GUIDEBOT_RENAME_MENU(VERB)  \
354
        DXX_MENUITEM(VERB, INPUT, text, opt_name)       \
355
 
356
void change_guidebot_name()
357
{
358
        auto text = PlayerCfg.GuidebotName;
359
        std::array<newmenu_item, DXX_GUIDEBOT_RENAME_MENU(COUNT)> m;
360
        enum
361
        {
362
                DXX_GUIDEBOT_RENAME_MENU(ENUM)
363
        };
364
        DXX_GUIDEBOT_RENAME_MENU(ADD);
365
        const auto item = newmenu_do(nullptr, "Enter Guide-bot name:", m, unused_newmenu_subfunction, unused_newmenu_userdata);
366
 
367
        if (item != -1) {
368
                PlayerCfg.GuidebotName = PlayerCfg.GuidebotNameReal = text;
369
                write_player_file();
370
        }
371
}
372
 
373
#undef DXX_GUIDEBOT_RENAME_MENU
374
 
375
//      -----------------------------------------------------------------------------
376
static uint8_t show_buddy_message()
377
{
378
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
379
        if (BuddyState.Buddy_messages_suppressed)
380
                return 0;
381
 
382
        if (Game_mode & GM_MULTI)
383
        {
384
                if (!Netgame.AllowGuidebot)
385
                        return 0;
386
        }
387
 
388
        if (BuddyState.Last_buddy_message_time + F1_0 < GameTime64) {
389
                if (const auto r = ok_for_buddy_to_talk())
390
                        return r;
391
        }
392
        return 0;
393
}
394
 
395
__attribute_nonnull()
396
static void buddy_message_force_str(const char *str)
397
{
398
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
399
        BuddyState.Last_buddy_message_time = GameTime64;
400
        HUD_init_message(HM_DEFAULT, "%c%c%s:%c%c %s", CC_COLOR, BM_XRGB(28, 0, 0), static_cast<const char *>(PlayerCfg.GuidebotName), CC_COLOR, BM_XRGB(0, 31, 0), str);
401
}
402
 
403
__attribute_format_printf(1, 0)
404
static void buddy_message_force_va(const char *const fmt, va_list vl)
405
{
406
        char buf[128];
407
        vsnprintf(buf, sizeof(buf), fmt, vl);
408
        buddy_message_force_str(buf);
409
}
410
 
411
__attribute_format_printf(1, 2)
412
static void buddy_message_ignore_time(const char *const fmt, ...)
413
{
414
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
415
        if (BuddyState.Buddy_messages_suppressed)
416
                return;
417
        if (!ok_for_buddy_to_talk())
418
                return;
419
        va_list args;
420
        va_start(args, fmt);
421
        buddy_message_force_va(fmt, args);
422
        va_end(args);
423
}
424
 
425
void (buddy_message)(const char * format, ... )
426
{
427
        if (!show_buddy_message())
428
                return;
429
 
430
                        va_list args;
431
 
432
                        va_start(args, format );
433
        buddy_message_force_va(format, args);
434
                        va_end(args);
435
}
436
 
437
void buddy_message_str(const char *str)
438
{
439
        if (!show_buddy_message())
440
                return;
441
        buddy_message_force_str(str);
442
}
443
 
444
//      -----------------------------------------------------------------------------
445
static void thief_message_str(const char * str) __attribute_nonnull();
446
static void thief_message_str(const char * str)
447
{
448
        HUD_init_message(HM_DEFAULT, "%c%cTHIEF:%c%c %s", 1, BM_XRGB(28, 0, 0), 1, BM_XRGB(0, 31, 0), str);
449
}
450
 
451
static void thief_message(const char * format, ... ) __attribute_format_printf(1, 2);
452
static void thief_message(const char * format, ... )
453
#define thief_message(F,...)    dxx_call_printf_checked(thief_message,thief_message_str,(),(F),##__VA_ARGS__)
454
{
455
 
456
        char    new_format[128];
457
        va_list args;
458
 
459
        va_start(args, format );
460
        vsnprintf(new_format, sizeof(new_format), format, args);
461
        va_end(args);
462
        thief_message_str(new_format);
463
}
464
 
465
//      -----------------------------------------------------------------------------
466
//      Return true if marker #id has been placed.
467
static int marker_exists_in_mine(const game_marker_index id)
468
{
469
        auto &Objects = LevelUniqueObjectState.Objects;
470
        auto &vcobjptr = Objects.vcptr;
471
        range_for (const auto &&objp, vcobjptr)
472
        {
473
                if (objp->type == OBJ_MARKER)
474
                        if (get_marker_id(objp) == id)
475
                                return 1;
476
        }
477
        return 0;
478
}
479
 
480
//      -----------------------------------------------------------------------------
481
void set_escort_special_goal(d_unique_buddy_state &BuddyState, const int raw_special_key)
482
{
483
        auto &Objects = LevelUniqueObjectState.Objects;
484
        auto &vmobjptridx = Objects.vmptridx;
485
        int marker_key;
486
 
487
        BuddyState.Buddy_messages_suppressed = 0;
488
 
489
        if (!BuddyState.Buddy_allowed_to_talk) {
490
                if (!ok_for_buddy_to_talk()) {
491
                        auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
492
                        const auto &&o = find_escort(vmobjptridx, Robot_info);
493
                        if (o == object_none)
494
                                HUD_init_message_literal(HM_DEFAULT, "No Guide-Bot in mine.");
495
                        else
496
                                HUD_init_message(HM_DEFAULT, "%s has not been released.", static_cast<const char *>(PlayerCfg.GuidebotName));
497
                        return;
498
                }
499
        }
500
 
501
        const int special_key = raw_special_key & (~KEY_SHIFTED);
502
 
503
        marker_key = special_key;
504
 
505
        if (BuddyState.Last_buddy_key == special_key)
506
        {
507
                auto &Looking_for_marker = BuddyState.Looking_for_marker;
508
                if (Looking_for_marker == game_marker_index::None && special_key != KEY_0)
509
                {
510
                        const unsigned zero_based_marker_id = marker_key - KEY_1;
511
                        const auto gmi = static_cast<game_marker_index>(zero_based_marker_id);
512
                        if (marker_exists_in_mine(gmi))
513
                                Looking_for_marker = gmi;
514
                        else {
515
                                buddy_message_ignore_time("Marker %i not placed.", zero_based_marker_id + 1);
516
                                Looking_for_marker = game_marker_index::None;
517
                        }
518
                } else {
519
                        Looking_for_marker = game_marker_index::None;
520
                }
521
        }
522
 
523
        BuddyState.Last_buddy_key = special_key;
524
 
525
        if (special_key == KEY_0)
526
                BuddyState.Looking_for_marker = game_marker_index::None;
527
        else if (BuddyState.Looking_for_marker != game_marker_index::None)
528
        {
529
                BuddyState.Escort_special_goal = static_cast<escort_goal_t>(ESCORT_GOAL_MARKER1 + marker_key - KEY_1);
530
        } else {
531
                auto &Escort_special_goal = BuddyState.Escort_special_goal;
532
                switch (special_key) {
533
                        case KEY_1:     Escort_special_goal = ESCORT_GOAL_ENERGY;                       break;
534
                        case KEY_2:     Escort_special_goal = ESCORT_GOAL_ENERGYCEN;            break;
535
                        case KEY_3:     Escort_special_goal = ESCORT_GOAL_SHIELD;                       break;
536
                        case KEY_4:     Escort_special_goal = ESCORT_GOAL_POWERUP;              break;
537
                        case KEY_5:     Escort_special_goal = ESCORT_GOAL_ROBOT;                        break;
538
                        case KEY_6:     Escort_special_goal = ESCORT_GOAL_HOSTAGE;              break;
539
                        case KEY_7:     Escort_special_goal = ESCORT_GOAL_SCRAM;                        break;
540
                        case KEY_8:     Escort_special_goal = ESCORT_GOAL_PLAYER_SPEW;  break;
541
                        case KEY_9:     Escort_special_goal = ESCORT_GOAL_EXIT;                 break;
542
                        case KEY_0:     Escort_special_goal = ESCORT_GOAL_UNSPECIFIED;                                                          break;
543
                        default:
544
                                Int3();         //      Oops, called with illegal key value.
545
                }
546
        }
547
 
548
        BuddyState.Last_buddy_message_time = GameTime64 - 2*F1_0;       //      Allow next message to come through.
549
 
550
        say_escort_goal(BuddyState.Escort_special_goal);
551
        BuddyState.Escort_goal_object = ESCORT_GOAL_UNSPECIFIED;
552
        multi_send_escort_goal(BuddyState);
553
}
554
 
555
//      -----------------------------------------------------------------------------
556
//      Return id of boss.
557
static int get_boss_id(void)
558
{
559
        auto &Objects = LevelUniqueObjectState.Objects;
560
        auto &vcobjptr = Objects.vcptr;
561
        auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
562
        range_for (const auto &&objp, vcobjptr)
563
        {
564
                if (objp->type == OBJ_ROBOT)
565
                {
566
                        const auto objp_id = get_robot_id(objp);
567
                        if (Robot_info[objp_id].boss_flag)
568
                                return objp_id;
569
                }
570
        }
571
        return -1;
572
}
573
 
574
//      -----------------------------------------------------------------------------
575
//      Return object index if object of objtype, objid exists in mine, else return -1
576
//      "special" is used to find objects spewed by player which is hacked into flags field of powerup.
577
static icobjidx_t exists_in_mine_2(const unique_segment &segp, const int objtype, const int objid, const int special)
578
{
579
        auto &Objects = LevelUniqueObjectState.Objects;
580
        auto &vcobjptridx = Objects.vcptridx;
581
        auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
582
        range_for (const auto curobjp, objects_in(segp, vcobjptridx, vcsegptr))
583
        {
584
                        if (special == ESCORT_GOAL_PLAYER_SPEW && curobjp->type == OBJ_POWERUP)
585
                        {
586
                                if (curobjp->flags & OF_PLAYER_DROPPED)
587
                                        return curobjp;
588
                        }
589
 
590
                        if (curobjp->type == objtype) {
591
                                //      Don't find escort robots if looking for robot!
592
                                if ((curobjp->type == OBJ_ROBOT) && (Robot_info[get_robot_id(curobjp)].companion))
593
                                        ;
594
                                else if (objid == -1) {
595
                                                return curobjp;
596
                                } else if (curobjp->id == objid)
597
                                        return curobjp;
598
                        }
599
 
600
                        if (objtype == OBJ_POWERUP && curobjp->type == OBJ_ROBOT)
601
                                if (curobjp->contains_count)
602
                                        if (curobjp->contains_type == OBJ_POWERUP)
603
                                                if (curobjp->contains_id == objid)
604
                                                        return curobjp;
605
        }
606
        return object_none;
607
}
608
 
609
//      -----------------------------------------------------------------------------
610
static std::pair<icsegidx_t, d_unique_buddy_state::Escort_goal_reachability> exists_fuelcen_in_mine(const vcsegidx_t start_seg, const player_flags powerup_flags)
611
{
612
        auto &Objects = LevelUniqueObjectState.Objects;
613
        auto &vmobjptr = Objects.vmptr;
614
        std::array<segnum_t, MAX_SEGMENTS> bfs_list;
615
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
616
        const auto Buddy_objnum = BuddyState.Buddy_objnum;
617
        const auto length = create_bfs_list(vmobjptr(Buddy_objnum), start_seg, powerup_flags, bfs_list);
618
        {
619
                const auto &&predicate = [](const segnum_t &s) {
620
                        return vcsegptr(s)->special == SEGMENT_IS_FUELCEN;
621
                };
622
                const auto &&rb = partial_const_range(bfs_list, length);
623
                const auto i = std::find_if(rb.begin(), rb.end(), predicate);
624
                if (i != rb.end())
625
                        return {*i, d_unique_buddy_state::Escort_goal_reachability::reachable};
626
        }
627
        {
628
                const auto &rh = vcsegptridx;
629
                const auto &&predicate = [](const shared_segment &s) {
630
                        return s.special == SEGMENT_IS_FUELCEN;
631
                };
632
                const auto i = std::find_if(rh.begin(), rh.end(), predicate);
633
                if (i != rh.end())
634
                        return {*i, d_unique_buddy_state::Escort_goal_reachability::unreachable};
635
        }
636
        return {segment_none, d_unique_buddy_state::Escort_goal_reachability::unreachable};
637
}
638
 
639
//      Return nearest object of interest.
640
//      If special == ESCORT_GOAL_PLAYER_SPEW, then looking for any object spewed by player.
641
//      -1 means object does not exist in mine.
642
//      -2 means object does exist in mine, but buddy-bot can't reach it (eg, behind triggered wall)
643
static std::pair<icobjidx_t, d_unique_buddy_state::Escort_goal_reachability> exists_in_mine(const vcsegidx_t start_seg, const int objtype, const int objid, const int special, const player_flags powerup_flags)
644
{
645
        auto &Objects = LevelUniqueObjectState.Objects;
646
        auto &vmobjptr = Objects.vmptr;
647
        std::array<segnum_t, MAX_SEGMENTS> bfs_list;
648
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
649
        const auto Buddy_objnum = BuddyState.Buddy_objnum;
650
        const auto length = create_bfs_list(vmobjptr(Buddy_objnum), start_seg, powerup_flags, bfs_list);
651
 
652
        range_for (const auto segnum, partial_const_range(bfs_list, length))
653
        {
654
                const auto &&objnum = exists_in_mine_2(vcsegptr(segnum), objtype, objid, special);
655
                        if (objnum != object_none)
656
                                return {objnum, d_unique_buddy_state::Escort_goal_reachability::reachable};
657
        }
658
 
659
        //      Couldn't find what we're looking for by looking at connectivity.
660
        //      See if it's in the mine.  It could be hidden behind a trigger or switch
661
        //      which the buddybot doesn't understand.
662
        range_for (const auto &&segnum, vcsegptr)
663
                {
664
                const auto &&objnum = exists_in_mine_2(segnum, objtype, objid, special);
665
                        if (objnum != object_none)
666
                                return {objnum, d_unique_buddy_state::Escort_goal_reachability::unreachable};
667
                }
668
        return {object_none, d_unique_buddy_state::Escort_goal_reachability::unreachable};
669
}
670
 
671
//      -----------------------------------------------------------------------------
672
//      Return true if it happened, else return false.
673
static imsegidx_t find_exit_segment()
674
{
675
        //      ---------- Find exit doors ----------
676
        range_for (const auto &&segp, vcsegptridx)
677
        {
678
                range_for (const auto j, segp->children)
679
                        if (j == segment_exit)
680
                                return segp;
681
        }
682
        return segment_none;
683
}
684
 
685
//      -----------------------------------------------------------------------------
686
static void say_escort_goal(const escort_goal_t goal_num)
687
{
688
        if (Player_dead_state != player_dead_state::no)
689
                return;
690
 
691
        const char *str;
692
        switch (goal_num) {
693
                default:
694
                case ESCORT_GOAL_UNSPECIFIED:
695
                        return;
696
                case ESCORT_GOAL_BLUE_KEY:
697
                        str = "Finding BLUE KEY";
698
                        break;
699
                case ESCORT_GOAL_GOLD_KEY:
700
                        str = "Finding YELLOW KEY";
701
                        break;
702
                case ESCORT_GOAL_RED_KEY:
703
                        str = "Finding RED KEY";
704
                        break;
705
                case ESCORT_GOAL_CONTROLCEN:
706
                        str = "Finding REACTOR";
707
                        break;
708
                case ESCORT_GOAL_EXIT:
709
                        str = "Finding EXIT";
710
                        break;
711
                case ESCORT_GOAL_ENERGY:
712
                        str = "Finding ENERGY";
713
                        break;
714
                case ESCORT_GOAL_ENERGYCEN:
715
                        str = "Finding ENERGY CENTER";
716
                        break;
717
                case ESCORT_GOAL_SHIELD:
718
                        str = "Finding a SHIELD";
719
                        break;
720
                case ESCORT_GOAL_POWERUP:
721
                        str = "Finding a POWERUP";
722
                        break;
723
                case ESCORT_GOAL_ROBOT:
724
                        str = "Finding a ROBOT";
725
                        break;
726
                case ESCORT_GOAL_HOSTAGE:
727
                        str = "Finding a HOSTAGE";
728
                        break;
729
                case ESCORT_GOAL_SCRAM:
730
                        str = "Staying away...";
731
                        break;
732
                case ESCORT_GOAL_BOSS:
733
                        str = "Finding BOSS robot";
734
                        break;
735
                case ESCORT_GOAL_PLAYER_SPEW:
736
                        str = "Finding your powerups";
737
                        break;
738
                case ESCORT_GOAL_MARKER1:
739
                case ESCORT_GOAL_MARKER2:
740
                case ESCORT_GOAL_MARKER3:
741
                case ESCORT_GOAL_MARKER4:
742
                case ESCORT_GOAL_MARKER5:
743
                case ESCORT_GOAL_MARKER6:
744
                case ESCORT_GOAL_MARKER7:
745
                case ESCORT_GOAL_MARKER8:
746
                case ESCORT_GOAL_MARKER9:
747
                        {
748
                                const uint8_t zero_based_goal_num = goal_num - ESCORT_GOAL_MARKER1;
749
                                buddy_message("Finding marker %i: '%.24s'", zero_based_goal_num + 1, &MarkerState.message[game_marker_index{zero_based_goal_num}][0]);
750
                        }
751
                        return;
752
        }
753
        buddy_message_str(str);
754
}
755
 
756
static void clear_escort_goals()
757
{
758
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
759
        BuddyState.Looking_for_marker = game_marker_index::None;
760
        BuddyState.Escort_goal_object = ESCORT_GOAL_UNSPECIFIED;
761
        BuddyState.Escort_special_goal = ESCORT_GOAL_UNSPECIFIED;
762
}
763
 
764
static void escort_goal_does_not_exist(const escort_goal_t goal)
765
{
766
        buddy_message_ignore_time("No %s in mine.", Escort_goal_text[goal - 1]);
767
        clear_escort_goals();
768
}
769
 
770
static void escort_goal_unreachable(const escort_goal_t goal)
771
{
772
        buddy_message_ignore_time("Can't reach %s.", Escort_goal_text[goal - 1]);
773
        clear_escort_goals();
774
}
775
 
776
static void escort_go_to_goal(const vmobjptridx_t objp, ai_static *const aip, const segnum_t goal_seg)
777
{
778
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
779
        auto &Objects = LevelUniqueObjectState.Objects;
780
        create_path_to_segment(objp, goal_seg, Max_escort_length, create_path_safety_flag::safe);       //      MK!: Last parm (safety_flag) used to be 1!!
781
        if (aip->path_length > 3)
782
                aip->path_length = polish_path(objp, &Point_segs[aip->hide_index], aip->path_length);
783
        if ((aip->path_length > 0) && (Point_segs[aip->hide_index + aip->path_length - 1].segnum != goal_seg)) {
784
                const unsigned goal_text_index = std::exchange(BuddyState.Escort_goal_object, ESCORT_GOAL_SCRAM) - 1;
785
                BuddyState.Looking_for_marker = game_marker_index::None;
786
                auto &plr = get_player_controlling_guidebot(BuddyState, Players);
787
                if (plr.objnum == object_none)
788
                        return;
789
                auto &plrobj = *Objects.vcptr(plr.objnum);
790
                if (plrobj.type != OBJ_PLAYER)
791
                        return;
792
                buddy_message_ignore_time("Cannot reach %s.", goal_text_index < Escort_goal_text.size() ? Escort_goal_text[goal_text_index] : "<unknown>");
793
                const auto goal_segment = plrobj.segnum;
794
                const fix dist_to_player = find_connected_distance(objp->pos, vmsegptridx(objp->segnum), plrobj.pos, vmsegptridx(goal_segment), 100, WID_FLY_FLAG);
795
                if (dist_to_player > MIN_ESCORT_DISTANCE)
796
                        create_path_to_segment(objp, Max_escort_length, create_path_safety_flag::safe, goal_segment);
797
                else {
798
                        create_n_segment_path(objp, 8 + d_rand() * 8, segment_none);
799
                        aip->path_length = polish_path(objp, &Point_segs[aip->hide_index], aip->path_length);
800
                }
801
        }
802
}
803
 
804
//      -----------------------------------------------------------------------------
805
static imsegidx_t escort_get_goal_segment(const object_base &buddy_obj, const int objtype, const int objid, const player_flags powerup_flags)
806
{
807
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
808
        auto &Objects = LevelUniqueObjectState.Objects;
809
        auto &vcobjptr = Objects.vcptr;
810
        const auto &&eim = exists_in_mine(buddy_obj.segnum, objtype, objid, -1, powerup_flags);
811
        BuddyState.Escort_goal_objidx = eim.first;
812
        BuddyState.Escort_goal_reachable = eim.second;
813
        if (eim.second != d_unique_buddy_state::Escort_goal_reachability::unreachable)
814
                return vcobjptr(eim.first)->segnum;
815
        return segment_none;
816
}
817
 
818
static void set_escort_goal_non_object(d_unique_buddy_state &BuddyState)
819
{
820
        BuddyState.Escort_goal_objidx = object_none;
821
        BuddyState.Escort_goal_reachable = d_unique_buddy_state::Escort_goal_reachability::unreachable;
822
}
823
 
824
static void escort_create_path_to_goal(const vmobjptridx_t objp, const player_info &player_info)
825
{
826
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
827
        auto &Objects = LevelUniqueObjectState.Objects;
828
        auto &vcobjptr = Objects.vcptr;
829
        segnum_t        goal_seg = segment_none;
830
        ai_static       *aip = &objp->ctype.ai_info;
831
        ai_local                *ailp = &objp->ctype.ai_info.ail;
832
 
833
        if (BuddyState.Escort_special_goal != ESCORT_GOAL_UNSPECIFIED)
834
                BuddyState.Escort_goal_object = BuddyState.Escort_special_goal;
835
 
836
        const auto powerup_flags = player_info.powerup_flags;
837
        const auto Escort_goal_object = BuddyState.Escort_goal_object;
838
        if (BuddyState.Looking_for_marker != game_marker_index::None)
839
        {
840
                goal_seg = escort_get_goal_segment(objp, OBJ_MARKER, Escort_goal_object - ESCORT_GOAL_MARKER1, powerup_flags);
841
        } else {
842
                switch (Escort_goal_object) {
843
                        case ESCORT_GOAL_BLUE_KEY:
844
                                goal_seg = escort_get_goal_segment(objp, OBJ_POWERUP, POW_KEY_BLUE, powerup_flags);
845
                                break;
846
                        case ESCORT_GOAL_GOLD_KEY:
847
                                goal_seg = escort_get_goal_segment(objp, OBJ_POWERUP, POW_KEY_GOLD, powerup_flags);
848
                                break;
849
                        case ESCORT_GOAL_RED_KEY:
850
                                goal_seg = escort_get_goal_segment(objp, OBJ_POWERUP, POW_KEY_RED, powerup_flags);
851
                                break;
852
                        case ESCORT_GOAL_CONTROLCEN:
853
                                goal_seg = escort_get_goal_segment(objp, OBJ_CNTRLCEN, -1, powerup_flags);
854
                                break;
855
                        case ESCORT_GOAL_EXIT:
856
                                goal_seg = find_exit_segment();
857
                                set_escort_goal_non_object(BuddyState);
858
                                if (goal_seg == segment_none)
859
                                        escort_goal_does_not_exist(ESCORT_GOAL_EXIT);
860
                                else if (goal_seg == segment_exit)
861
                                        escort_goal_unreachable(ESCORT_GOAL_EXIT);
862
                                else
863
                                        escort_go_to_goal(objp, aip, goal_seg);
864
                                return;
865
                        case ESCORT_GOAL_ENERGY:
866
                                goal_seg = escort_get_goal_segment(objp, OBJ_POWERUP, POW_ENERGY, powerup_flags);
867
                                break;
868
                        case ESCORT_GOAL_ENERGYCEN:
869
                                {
870
                                        const auto &&ef = exists_fuelcen_in_mine(objp->segnum, powerup_flags);
871
                                        set_escort_goal_non_object(BuddyState);
872
                                if (ef.second != d_unique_buddy_state::Escort_goal_reachability::unreachable)
873
                                        escort_go_to_goal(objp, aip, ef.first);
874
                                else if (ef.first == segment_none)
875
                                        escort_goal_does_not_exist(ESCORT_GOAL_ENERGYCEN);
876
                                else
877
                                        escort_goal_unreachable(ESCORT_GOAL_ENERGYCEN);
878
                                return;
879
                                }
880
                        case ESCORT_GOAL_SHIELD:
881
                                goal_seg = escort_get_goal_segment(objp, OBJ_POWERUP, POW_SHIELD_BOOST, powerup_flags);
882
                                break;
883
                        case ESCORT_GOAL_POWERUP:
884
                                goal_seg = escort_get_goal_segment(objp, OBJ_POWERUP, -1, powerup_flags);
885
                                break;
886
                        case ESCORT_GOAL_ROBOT:
887
                                goal_seg = escort_get_goal_segment(objp, OBJ_ROBOT, -1, powerup_flags);
888
                                break;
889
                        case ESCORT_GOAL_HOSTAGE:
890
                                goal_seg = escort_get_goal_segment(objp, OBJ_HOSTAGE, -1, powerup_flags);
891
                                break;
892
                        case ESCORT_GOAL_PLAYER_SPEW:
893
                                {
894
                                        const auto &&egi = exists_in_mine(objp->segnum, -1, -1, ESCORT_GOAL_PLAYER_SPEW, powerup_flags);
895
                                        BuddyState.Escort_goal_objidx = egi.first;
896
                                        BuddyState.Escort_goal_reachable = egi.second;
897
                                        if (egi.second != d_unique_buddy_state::Escort_goal_reachability::unreachable)
898
                                        {
899
                                                auto &o = *vcobjptr(egi.first);
900
                                                goal_seg = o.segnum;
901
                                        }
902
                                }
903
                                break;
904
                        case ESCORT_GOAL_BOSS: {
905
                                int     boss_id;
906
 
907
                                boss_id = get_boss_id();
908
                                Assert(boss_id != -1);
909
                                goal_seg = escort_get_goal_segment(objp, OBJ_ROBOT, boss_id, powerup_flags);
910
                                break;
911
                        }
912
                        default:
913
                                Int3(); //      Oops, Illegal value in Escort_goal_object.
914
                                con_printf(CON_URGENT, "BUG: buddy goal is %.8x, resetting to SCRAM", Escort_goal_object);
915
                                BuddyState.Escort_goal_object = ESCORT_GOAL_SCRAM;
916
                                DXX_BOOST_FALLTHROUGH;
917
                        case ESCORT_GOAL_SCRAM:
918
                                set_escort_goal_non_object(BuddyState);
919
                                create_n_segment_path(objp, 16 + d_rand() * 16, segment_none);
920
                                aip->path_length = polish_path(objp, &Point_segs[aip->hide_index], aip->path_length);
921
                                ailp->mode = ai_mode::AIM_GOTO_OBJECT;
922
                                say_escort_goal(Escort_goal_object);
923
                                return;
924
                }
925
        }
926
        const auto Escort_goal_objidx = BuddyState.Escort_goal_objidx;
927
        if (BuddyState.Escort_goal_reachable == d_unique_buddy_state::Escort_goal_reachability::unreachable)
928
        {
929
                if (Escort_goal_objidx == object_none) {
930
                        escort_goal_does_not_exist(Escort_goal_object);
931
                } else {
932
                        escort_goal_unreachable(Escort_goal_object);
933
                }
934
        } else {
935
                escort_go_to_goal(objp, aip, goal_seg);
936
                ailp->mode = ai_mode::AIM_GOTO_OBJECT;
937
                say_escort_goal(Escort_goal_object);
938
        }
939
}
940
 
941
//      -----------------------------------------------------------------------------
942
//      Escort robot chooses goal object based on player's keys, location.
943
//      Returns goal object.
944
static escort_goal_t escort_set_goal_object(const player_flags pl_flags)
945
{
946
        auto &Boss_teleport_segs = LevelSharedBossState.Teleport_segs;
947
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
948
        auto &LevelUniqueControlCenterState = LevelUniqueObjectState.ControlCenterState;
949
        auto &Objects = LevelUniqueObjectState.Objects;
950
        if (BuddyState.Escort_special_goal != ESCORT_GOAL_UNSPECIFIED)
951
                return ESCORT_GOAL_UNSPECIFIED;
952
 
953
        auto &plr = get_player_controlling_guidebot(BuddyState, Players);
954
        if (plr.objnum == object_none)
955
                /* should never happen */
956
                return ESCORT_GOAL_UNSPECIFIED;
957
        auto &plrobj = *Objects.vcptr(plr.objnum);
958
        if (plrobj.type != OBJ_PLAYER)
959
                return ESCORT_GOAL_UNSPECIFIED;
960
 
961
        const auto need_key_and_key_exists = [pl_flags, start_search_seg = plrobj.segnum](const PLAYER_FLAG flag_key, const powerup_type_t powerup_key) {
962
                if (pl_flags & flag_key)
963
                        /* Player already has this key, so no need to get it again.
964
                         */
965
                        return false;
966
                const auto &&e = exists_in_mine(start_search_seg, OBJ_POWERUP, powerup_key, -1, pl_flags);
967
                /* For compatibility with classic Descent 2, test only whether
968
                 * the key exists, but ignore whether it can be reached by the
969
                 * guide bot.
970
                 */
971
                return e.first != object_none;
972
        };
973
        if (need_key_and_key_exists(PLAYER_FLAGS_BLUE_KEY, POW_KEY_BLUE))
974
                return ESCORT_GOAL_BLUE_KEY;
975
        else if (need_key_and_key_exists(PLAYER_FLAGS_GOLD_KEY, POW_KEY_GOLD))
976
                return ESCORT_GOAL_GOLD_KEY;
977
        else if (need_key_and_key_exists(PLAYER_FLAGS_RED_KEY, POW_KEY_RED))
978
                return ESCORT_GOAL_RED_KEY;
979
        else if (LevelUniqueControlCenterState.Control_center_destroyed == 0)
980
        {
981
                if (!Boss_teleport_segs.empty())
982
                        return ESCORT_GOAL_BOSS;
983
                else
984
                        return ESCORT_GOAL_CONTROLCEN;
985
        } else
986
                return ESCORT_GOAL_EXIT;
987
}
988
 
989
#define MAX_ESCORT_TIME_AWAY            (F1_0*4)
990
 
991
//      -----------------------------------------------------------------------------
992
static const player *time_to_visit_player(const d_level_unique_object_state &LevelUniqueObjectState, const object &buddy_object)
993
{
994
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
995
        auto &plr = get_player_controlling_guidebot(BuddyState, Players);
996
        if (plr.objnum == object_none)
997
                /* should never happen */
998
                return nullptr;
999
        auto &Objects = LevelUniqueObjectState.Objects;
1000
        auto &plrobj = *Objects.vcptr(plr.objnum);
1001
        if (plrobj.type != OBJ_PLAYER)
1002
                return nullptr;
1003
        //      Note: This one has highest priority because, even if already going towards player,
1004
        //      might be necessary to create a new path, as player can move.
1005
        if (GameTime64 - BuddyState.Buddy_last_seen_player > MAX_ESCORT_TIME_AWAY)
1006
                if (GameTime64 - BuddyState.Buddy_last_player_path_created > F1_0)
1007
                        return &plr;
1008
 
1009
        auto &ais = buddy_object.ctype.ai_info;
1010
        if (ais.ail.mode == ai_mode::AIM_GOTO_PLAYER)
1011
                return nullptr;
1012
 
1013
        if (ais.cur_path_index < ais.path_length / 2)
1014
                return nullptr;
1015
 
1016
        if (buddy_object.segnum == plrobj.segnum)
1017
                return nullptr;
1018
        return &plr;
1019
}
1020
 
1021
//      -----------------------------------------------------------------------------
1022
static void bash_buddy_weapon_info(d_unique_buddy_state &BuddyState, fvmobjptridx &vmobjptridx, object &weapon_obj)
1023
{
1024
        auto &plr = get_player_controlling_guidebot(BuddyState, Players);
1025
        if (plr.objnum == object_none)
1026
                /* should never happen */
1027
                return;
1028
        /* Buddy can still fire while player is dead, so skip check for
1029
         * plrobj.type */
1030
        auto &&plrobj = vmobjptridx(plr.objnum);
1031
        auto &laser_info = weapon_obj.ctype.laser_info;
1032
        laser_info.parent_num = plrobj;
1033
        laser_info.parent_type = OBJ_PLAYER;
1034
        laser_info.parent_signature = plrobj->signature;
1035
}
1036
 
1037
//      -----------------------------------------------------------------------------
1038
static int maybe_buddy_fire_mega(const vmobjptridx_t objp)
1039
{
1040
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
1041
        auto &Objects = LevelUniqueObjectState.Objects;
1042
        const auto Buddy_objnum = BuddyState.Buddy_objnum;
1043
        const auto &&buddy_objp = objp.absolute_sibling(Buddy_objnum);
1044
        fix             dist, dot;
1045
        auto vec_to_robot = vm_vec_sub(buddy_objp->pos, objp->pos);
1046
        dist = vm_vec_normalize_quick(vec_to_robot);
1047
 
1048
        if (dist > F1_0*100)
1049
                return 0;
1050
 
1051
        dot = vm_vec_dot(vec_to_robot, buddy_objp->orient.fvec);
1052
 
1053
        if (dot < F1_0/2)
1054
                return 0;
1055
 
1056
        if (!object_to_object_visibility(buddy_objp, objp, FQ_TRANSWALL))
1057
                return 0;
1058
 
1059
        if (Weapon_info[weapon_id_type::MEGA_ID].render_type == 0) {
1060
                con_puts(CON_VERBOSE, "Buddy can't fire mega (shareware)");
1061
                buddy_message("CLICK!");
1062
                return 0;
1063
        }
1064
 
1065
        buddy_message("GAHOOGA!");
1066
 
1067
        const imobjptridx_t weapon_objnum = Laser_create_new_easy( buddy_objp->orient.fvec, buddy_objp->pos, objp, weapon_id_type::MEGA_ID, 1);
1068
 
1069
        if (weapon_objnum != object_none)
1070
                bash_buddy_weapon_info(BuddyState, Objects.vmptridx, weapon_objnum);
1071
 
1072
        return 1;
1073
}
1074
 
1075
//-----------------------------------------------------------------------------
1076
static int maybe_buddy_fire_smart(const vmobjptridx_t objp)
1077
{
1078
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
1079
        auto &Objects = LevelUniqueObjectState.Objects;
1080
        const auto Buddy_objnum = BuddyState.Buddy_objnum;
1081
        const auto &&buddy_objp = objp.absolute_sibling(Buddy_objnum);
1082
        fix             dist;
1083
 
1084
        dist = vm_vec_dist_quick(buddy_objp->pos, objp->pos);
1085
 
1086
        if (dist > F1_0*80)
1087
                return 0;
1088
 
1089
        if (!object_to_object_visibility(buddy_objp, objp, FQ_TRANSWALL))
1090
                return 0;
1091
 
1092
        buddy_message("WHAMMO!");
1093
 
1094
        const imobjptridx_t weapon_objnum = Laser_create_new_easy( buddy_objp->orient.fvec, buddy_objp->pos, objp, weapon_id_type::SMART_ID, 1);
1095
 
1096
        if (weapon_objnum != object_none)
1097
                bash_buddy_weapon_info(BuddyState, Objects.vmptridx, weapon_objnum);
1098
 
1099
        return 1;
1100
}
1101
 
1102
//      -----------------------------------------------------------------------------
1103
static void do_buddy_dude_stuff(void)
1104
{
1105
        auto &Objects = LevelUniqueObjectState.Objects;
1106
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
1107
        auto &vmobjptridx = Objects.vmptridx;
1108
        if (!ok_for_buddy_to_talk())
1109
                return;
1110
 
1111
        if (BuddyState.Buddy_last_missile_time + F1_0*2 < GameTime64) {
1112
                //      See if a robot potentially in view cone
1113
                auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1114
                auto &&rh = make_range(vmobjptridx);
1115
                range_for (const auto &&objp, rh)
1116
                {
1117
                        if ((objp->type == OBJ_ROBOT) && !Robot_info[get_robot_id(objp)].companion)
1118
                                if (maybe_buddy_fire_mega(objp)) {
1119
                                        BuddyState.Buddy_last_missile_time = GameTime64;
1120
                                        return;
1121
                                }
1122
                }
1123
 
1124
                //      See if a robot near enough that buddy should fire smart missile
1125
                range_for (const auto &&objp, rh)
1126
                {
1127
                        if ((objp->type == OBJ_ROBOT) && !Robot_info[get_robot_id(objp)].companion)
1128
                                if (maybe_buddy_fire_smart(objp)) {
1129
                                        BuddyState.Buddy_last_missile_time = GameTime64;
1130
                                        return;
1131
                                }
1132
                }
1133
        }
1134
}
1135
 
1136
static void escort_set_goal_toward_controlling_player(d_unique_buddy_state &BuddyState, fvcobjptr &vcobjptr, const vmobjptridx_t buddy_obj)
1137
{
1138
        auto &aip = buddy_obj->ctype.ai_info;
1139
        aip.path_length = polish_path(buddy_obj, &Point_segs[aip.hide_index], aip.path_length);
1140
        if (aip.path_length < 3) {
1141
                auto &plr = get_player_controlling_guidebot(BuddyState, Players);
1142
                if (plr.objnum != object_none)
1143
                {
1144
                        auto &plrobj = *vcobjptr(plr.objnum);
1145
                        if (plrobj.type == OBJ_PLAYER)
1146
                                create_n_segment_path(buddy_obj, 5, plrobj.segnum);
1147
                }
1148
        }
1149
        aip.ail.mode = ai_mode::AIM_GOTO_OBJECT;
1150
}
1151
 
1152
//      -----------------------------------------------------------------------------
1153
//      Called every frame (or something).
1154
void do_escort_frame(const vmobjptridx_t objp, const object &plrobj, const player_visibility_state player_visibility)
1155
{
1156
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
1157
        auto &Objects = LevelUniqueObjectState.Objects;
1158
        auto &vcobjptr = Objects.vcptr;
1159
        const auto dist_to_player = vm_vec_dist_quick(plrobj.pos, objp->pos);
1160
        ai_static       *aip = &objp->ctype.ai_info;
1161
        ai_local                *ailp = &objp->ctype.ai_info.ail;
1162
 
1163
        auto &player_info = plrobj.ctype.player_info;
1164
        if (player_is_visible(player_visibility))
1165
        {
1166
                BuddyState.Buddy_last_seen_player = GameTime64;
1167
                if (player_info.powerup_flags & PLAYER_FLAGS_HEADLIGHT_ON)      //      DAMN! MK, stupid bug, fixed 12/08/95, changed PLAYER_FLAGS_HEADLIGHT to PLAYER_FLAGS_HEADLIGHT_ON
1168
                {
1169
                        const auto energy = player_info.energy;
1170
                        const auto ienergy = f2i(energy);
1171
                        if (ienergy < 40)
1172
                                if (ienergy & 4)
1173
                                                buddy_message("Hey, your headlight's on!");
1174
                }
1175
        }
1176
 
1177
        if (cheats.buddyangry)
1178
                do_buddy_dude_stuff();
1179
 
1180
        {
1181
                const auto buddy_sorry_time = BuddyState.Buddy_sorry_time;
1182
                if (buddy_sorry_time + F1_0 > GameTime64)
1183
                {
1184
                        BuddyState.Buddy_sorry_time = -F1_0*2;
1185
                        if (buddy_sorry_time < GameTime64 + F1_0*2)
1186
                        {
1187
                                buddy_message_ignore_time("Oops, sorry 'bout that...");
1188
                        }
1189
                }
1190
        }
1191
 
1192
        //      If buddy not allowed to talk, then he is locked in his room.  Make him mostly do nothing unless you're nearby.
1193
        if (!BuddyState.Buddy_allowed_to_talk)
1194
                if (dist_to_player > F1_0*100)
1195
                        aip->SKIP_AI_COUNT = (F1_0/4)/FrameTime;
1196
 
1197
        //      ai_mode::AIM_WANDER has been co-opted for buddy behavior (didn't want to modify aistruct.h)
1198
        //      It means the object has been told to get lost and has come to the end of its path.
1199
        //      If the player is now visible, then create a path.
1200
        if (ailp->mode == ai_mode::AIM_WANDER)
1201
                if (player_is_visible(player_visibility))
1202
                {
1203
                        create_n_segment_path(objp, 16 + d_rand() * 16, segment_none);
1204
                        aip->path_length = polish_path(objp, &Point_segs[aip->hide_index], aip->path_length);
1205
                }
1206
 
1207
        if (BuddyState.Escort_special_goal == ESCORT_GOAL_SCRAM) {
1208
                auto &plr = get_player_controlling_guidebot(BuddyState, Players);
1209
                if (plr.objnum == object_none)
1210
                        return;
1211
                auto &plrobj = *Objects.vcptr(plr.objnum);
1212
                if (plrobj.type != OBJ_PLAYER)
1213
                        return;
1214
                if (player_is_visible(player_visibility))
1215
                        if (BuddyState.Escort_last_path_created + F1_0*3 < GameTime64) {
1216
                                BuddyState.Escort_last_path_created = GameTime64;
1217
                                create_n_segment_path(objp, 10 + d_rand() * 16, plrobj.segnum);
1218
                        }
1219
 
1220
                return;
1221
        }
1222
 
1223
        //      Force checking for new goal every 5 seconds, and create new path, if necessary.
1224
        if (BuddyState.Escort_last_path_created + (BuddyState.Escort_special_goal != ESCORT_GOAL_SCRAM ? (F1_0 * 5) : (F1_0 * 15)) < GameTime64)
1225
        {
1226
                BuddyState.Escort_goal_object = ESCORT_GOAL_UNSPECIFIED;
1227
                BuddyState.Escort_last_path_created = GameTime64;
1228
        }
1229
 
1230
        const player *guidebot_controller_player;
1231
        if (BuddyState.Escort_special_goal != ESCORT_GOAL_SCRAM && (guidebot_controller_player = time_to_visit_player(LevelUniqueObjectState, objp)))
1232
        {
1233
                unsigned max_len;
1234
                BuddyState.Buddy_last_player_path_created = GameTime64;
1235
                ailp->mode = ai_mode::AIM_GOTO_PLAYER;
1236
                if (!player_is_visible(player_visibility))
1237
                {
1238
                        if (BuddyState.Last_come_back_message_time + F1_0 < GameTime64)
1239
                        {
1240
                                BuddyState.Last_come_back_message_time = GameTime64;
1241
                                auto &local_player = *Players.vcptr(Player_num);
1242
                                if (guidebot_controller_player == &local_player)
1243
                                        buddy_message_str("Coming back to get you.");
1244
                                else
1245
                                        buddy_message("Going back to get %s.", guidebot_controller_player->callsign.operator const char *());
1246
                        }
1247
                }
1248
                //      No point in Buddy creating very long path if he's not allowed to talk.  Really kills framerate.
1249
                max_len = Max_escort_length;
1250
                if (!BuddyState.Buddy_allowed_to_talk)
1251
                        max_len = 3;
1252
                create_path_to_segment(objp, max_len, create_path_safety_flag::safe, Believed_player_seg);      //      MK!: Last parm used to be 1!
1253
                aip->path_length = polish_path(objp, &Point_segs[aip->hide_index], aip->path_length);
1254
                ailp->mode = ai_mode::AIM_GOTO_PLAYER;
1255
        }
1256
        else if (GameTime64 - BuddyState.Buddy_last_seen_player > MAX_ESCORT_TIME_AWAY)
1257
        {
1258
                //      This is to prevent buddy from looking for a goal, which he will do because we only allow path creation once/second.
1259
                return;
1260
        } else if ((ailp->mode == ai_mode::AIM_GOTO_PLAYER) && (dist_to_player < MIN_ESCORT_DISTANCE)) {
1261
                BuddyState.Escort_goal_object = escort_set_goal_object(player_info.powerup_flags);
1262
                ailp->mode = ai_mode::AIM_GOTO_OBJECT;          //      May look stupid to be before path creation, but ai_door_is_openable uses mode to determine what doors can be got through
1263
                escort_create_path_to_goal(objp, player_info);
1264
                escort_set_goal_toward_controlling_player(BuddyState, vcobjptr, objp);
1265
        }
1266
        else if (BuddyState.Escort_goal_object == ESCORT_GOAL_UNSPECIFIED)
1267
        {
1268
                if ((ailp->mode != ai_mode::AIM_GOTO_PLAYER) || (dist_to_player < MIN_ESCORT_DISTANCE)) {
1269
                        BuddyState.Escort_goal_object = escort_set_goal_object(player_info.powerup_flags);
1270
                        ailp->mode = ai_mode::AIM_GOTO_OBJECT;          //      May look stupid to be before path creation, but ai_door_is_openable uses mode to determine what doors can be got through
1271
                        escort_create_path_to_goal(objp, player_info);
1272
                        escort_set_goal_toward_controlling_player(BuddyState, vcobjptr, objp);
1273
                }
1274
        }
1275
}
1276
 
1277
void invalidate_escort_goal(d_unique_buddy_state &BuddyState)
1278
{
1279
        BuddyState.Escort_goal_object = ESCORT_GOAL_UNSPECIFIED;
1280
}
1281
 
1282
//      -------------------------------------------------------------------------------------------------
1283
void do_snipe_frame(const vmobjptridx_t objp, const fix dist_to_player, const player_visibility_state player_visibility, const vms_vector &vec_to_player)
1284
{
1285
        ai_local                *ailp = &objp->ctype.ai_info.ail;
1286
        fix                     connected_distance;
1287
 
1288
        if (dist_to_player > F1_0*500)
1289
                return;
1290
 
1291
        switch (ailp->mode) {
1292
                case ai_mode::AIM_SNIPE_WAIT:
1293
                        if ((dist_to_player > F1_0*50) && (ailp->next_action_time > 0))
1294
                                return;
1295
 
1296
                        ailp->next_action_time = SNIPE_WAIT_TIME;
1297
 
1298
                        connected_distance = find_connected_distance(objp->pos, vmsegptridx(objp->segnum), Believed_player_pos, vmsegptridx(Believed_player_seg), 30, WID_FLY_FLAG);
1299
                        if (connected_distance < F1_0*500) {
1300
                                create_path_to_believed_player_segment(objp, 30, create_path_safety_flag::safe);
1301
                                ailp->mode = ai_mode::AIM_SNIPE_ATTACK;
1302
                                ailp->next_action_time = SNIPE_ATTACK_TIME;     //      have up to 10 seconds to find player.
1303
                        }
1304
                        break;
1305
 
1306
                case ai_mode::AIM_SNIPE_RETREAT:
1307
                case ai_mode::AIM_SNIPE_RETREAT_BACKWARDS:
1308
                        if (ailp->next_action_time < 0) {
1309
                                ailp->mode = ai_mode::AIM_SNIPE_WAIT;
1310
                                ailp->next_action_time = SNIPE_WAIT_TIME;
1311
                        }
1312
                        else if (player_visibility == player_visibility_state::no_line_of_sight || ailp->next_action_time > SNIPE_ABORT_RETREAT_TIME)
1313
                        {
1314
                                ai_follow_path(objp, player_visibility, &vec_to_player);
1315
                                ailp->mode = ai_mode::AIM_SNIPE_RETREAT_BACKWARDS;
1316
                        } else {
1317
                                ailp->mode = ai_mode::AIM_SNIPE_FIRE;
1318
                                ailp->next_action_time = SNIPE_FIRE_TIME/2;
1319
                        }
1320
                        break;
1321
 
1322
                case ai_mode::AIM_SNIPE_ATTACK:
1323
                        if (ailp->next_action_time < 0) {
1324
                                ailp->mode = ai_mode::AIM_SNIPE_RETREAT;
1325
                                ailp->next_action_time = SNIPE_WAIT_TIME;
1326
                        } else {
1327
                                ai_follow_path(objp, player_visibility, &vec_to_player);
1328
                                if (player_is_visible(player_visibility))
1329
                                {
1330
                                        ailp->mode = ai_mode::AIM_SNIPE_FIRE;
1331
                                        ailp->next_action_time = SNIPE_FIRE_TIME;
1332
                                } else
1333
                                        ailp->mode = ai_mode::AIM_SNIPE_ATTACK;
1334
                        }
1335
                        break;
1336
 
1337
                case ai_mode::AIM_SNIPE_FIRE:
1338
                        if (ailp->next_action_time < 0) {
1339
                                ai_static       *aip = &objp->ctype.ai_info;
1340
                                create_n_segment_path(objp, 10 + d_rand()/2048, ConsoleObject->segnum);
1341
                                aip->path_length = polish_path(objp, &Point_segs[aip->hide_index], aip->path_length);
1342
                                if (d_rand() < 8192)
1343
                                        ailp->mode = ai_mode::AIM_SNIPE_RETREAT_BACKWARDS;
1344
                                else
1345
                                        ailp->mode = ai_mode::AIM_SNIPE_RETREAT;
1346
                                ailp->next_action_time = SNIPE_RETREAT_TIME;
1347
                        } else {
1348
                        }
1349
                        break;
1350
 
1351
                default:
1352
                        Int3(); //      Oops, illegal mode for snipe behavior.
1353
                        ailp->mode = ai_mode::AIM_SNIPE_ATTACK;
1354
                        ailp->next_action_time = F1_0;
1355
                        break;
1356
        }
1357
 
1358
}
1359
 
1360
#define THIEF_DEPTH     20
1361
 
1362
//      ------------------------------------------------------------------------------------------------------
1363
//      Choose segment to recreate thief in.
1364
static vmsegidx_t choose_thief_recreation_segment(const vcsegidx_t plrseg)
1365
{
1366
        segnum_t        segnum = segment_none;
1367
        int     cur_drop_depth;
1368
 
1369
        cur_drop_depth = THIEF_DEPTH;
1370
 
1371
        while ((segnum == segment_none) && (cur_drop_depth > THIEF_DEPTH/2)) {
1372
                segnum = pick_connected_segment(plrseg, cur_drop_depth);
1373
                if (segnum != segment_none && vcsegptr(segnum)->special == SEGMENT_IS_CONTROLCEN)
1374
                        segnum = segment_none;
1375
                cur_drop_depth--;
1376
        }
1377
 
1378
        if (segnum == segment_none) {
1379
                return (d_rand() * Highest_segment_index) >> 15;
1380
        } else
1381
                return segnum;
1382
 
1383
}
1384
 
1385
static fix64    Re_init_thief_time = 0x3f000000;
1386
 
1387
//      ----------------------------------------------------------------------
1388
void recreate_thief(const uint8_t thief_id)
1389
{
1390
        auto &LevelSharedVertexState = LevelSharedSegmentState.get_vertex_state();
1391
        auto &Vertices = LevelSharedVertexState.get_vertices();
1392
        const auto segnum = choose_thief_recreation_segment(ConsoleObject->segnum);
1393
        const auto &&segp = vmsegptridx(segnum);
1394
        auto &vcvertptr = Vertices.vcptr;
1395
        const auto &&center_point = compute_segment_center(vcvertptr, segp);
1396
 
1397
        const auto &&new_obj = create_morph_robot(segp, center_point, thief_id);
1398
        if (new_obj == object_none)
1399
                return;
1400
        Re_init_thief_time = GameTime64 + F1_0*10;              //      In 10 seconds, re-initialize thief.
1401
}
1402
 
1403
//      ----------------------------------------------------------------------------
1404
#define THIEF_ATTACK_TIME               (F1_0*10)
1405
 
1406
constexpr std::array<fix, NDL> Thief_wait_times = {{
1407
        F1_0*30, F1_0*25, F1_0*20, F1_0*15, F1_0*10
1408
}};
1409
 
1410
//      -------------------------------------------------------------------------------------------------
1411
void do_thief_frame(const vmobjptridx_t objp, const fix dist_to_player, const player_visibility_state player_visibility, const vms_vector &vec_to_player)
1412
{
1413
        const auto Difficulty_level = GameUniqueState.Difficulty_level;
1414
        auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1415
        ai_local                *ailp = &objp->ctype.ai_info.ail;
1416
        fix                     connected_distance;
1417
 
1418
        if ((Current_level_num < 0) && (Re_init_thief_time < GameTime64)) {
1419
                if (Re_init_thief_time > GameTime64 - F1_0*2)
1420
                        init_thief_for_level();
1421
                Re_init_thief_time = 0x3f000000;
1422
        }
1423
 
1424
        if ((dist_to_player > F1_0*500) && (ailp->next_action_time > 0))
1425
                return;
1426
 
1427
        if (Player_dead_state != player_dead_state::no)
1428
                ailp->mode = ai_mode::AIM_THIEF_RETREAT;
1429
 
1430
        switch (ailp->mode) {
1431
                case ai_mode::AIM_THIEF_WAIT:
1432
                        if (ailp->player_awareness_type >= player_awareness_type_t::PA_PLAYER_COLLISION) {
1433
                                ailp->player_awareness_type = player_awareness_type_t::PA_NONE;
1434
                                create_path_to_believed_player_segment(objp, 30, create_path_safety_flag::safe);
1435
                                ailp->mode = ai_mode::AIM_THIEF_ATTACK;
1436
                                ailp->next_action_time = THIEF_ATTACK_TIME/2;
1437
                                return;
1438
                        }
1439
                        else if (player_is_visible(player_visibility))
1440
                        {
1441
                                create_n_segment_path(objp, 15, ConsoleObject->segnum);
1442
                                ailp->mode = ai_mode::AIM_THIEF_RETREAT;
1443
                                return;
1444
                        }
1445
 
1446
                        if ((dist_to_player > F1_0*50) && (ailp->next_action_time > 0))
1447
                                return;
1448
 
1449
                        ailp->next_action_time = Thief_wait_times[Difficulty_level]/2;
1450
 
1451
                        connected_distance = find_connected_distance(objp->pos, vmsegptridx(objp->segnum), Believed_player_pos, vmsegptridx(Believed_player_seg), 30, WID_FLY_FLAG);
1452
                        if (connected_distance < F1_0*500) {
1453
                                create_path_to_believed_player_segment(objp, 30, create_path_safety_flag::safe);
1454
                                ailp->mode = ai_mode::AIM_THIEF_ATTACK;
1455
                                ailp->next_action_time = THIEF_ATTACK_TIME;     //      have up to 10 seconds to find player.
1456
                        }
1457
 
1458
                        break;
1459
 
1460
                case ai_mode::AIM_THIEF_RETREAT:
1461
                        if (ailp->next_action_time < 0) {
1462
                                ailp->mode = ai_mode::AIM_THIEF_WAIT;
1463
                                ailp->next_action_time = Thief_wait_times[Difficulty_level];
1464
                        }
1465
                        else if (dist_to_player < F1_0 * 100 || player_is_visible(player_visibility) || ailp->player_awareness_type >= player_awareness_type_t::PA_PLAYER_COLLISION)
1466
                        {
1467
                                ai_follow_path(objp, player_visibility, &vec_to_player);
1468
                                if ((dist_to_player < F1_0*100) || (ailp->player_awareness_type >= player_awareness_type_t::PA_PLAYER_COLLISION)) {
1469
                                        ai_static       *aip = &objp->ctype.ai_info;
1470
                                        if (((aip->cur_path_index <=1) && (aip->PATH_DIR == -1)) || ((aip->cur_path_index >= aip->path_length-1) && (aip->PATH_DIR == 1))) {
1471
                                                ailp->player_awareness_type = player_awareness_type_t::PA_NONE;
1472
                                                create_n_segment_path(objp, 10, ConsoleObject->segnum);
1473
 
1474
                                                //      If path is real short, try again, allowing to go through player's segment
1475
                                                if (aip->path_length < 4) {
1476
                                                        create_n_segment_path(objp, 10, segment_none);
1477
                                                } else if (objp->shields* 4 < Robot_info[get_robot_id(objp)].strength) {
1478
                                                        //      If robot really low on hits, will run through player with even longer path
1479
                                                        if (aip->path_length < 8) {
1480
                                                                create_n_segment_path(objp, 10, segment_none);
1481
                                                        }
1482
                                                }
1483
 
1484
                                                ailp->mode = ai_mode::AIM_THIEF_RETREAT;
1485
                                        }
1486
                                } else
1487
                                        ailp->mode = ai_mode::AIM_THIEF_RETREAT;
1488
 
1489
                        }
1490
 
1491
                        break;
1492
 
1493
                //      This means the thief goes from wherever he is to the player.
1494
                //      Note: When thief successfully steals something, his action time is forced negative and his mode is changed
1495
                //                      to retreat to get him out of attack mode.
1496
                case ai_mode::AIM_THIEF_ATTACK:
1497
                        if (ailp->player_awareness_type >= player_awareness_type_t::PA_PLAYER_COLLISION) {
1498
                                ailp->player_awareness_type = player_awareness_type_t::PA_NONE;
1499
                                if (d_rand() > 8192) {
1500
                                        create_n_segment_path(objp, 10, ConsoleObject->segnum);
1501
                                        ailp->next_action_time = Thief_wait_times[Difficulty_level]/2;
1502
                                        ailp->mode = ai_mode::AIM_THIEF_RETREAT;
1503
                                }
1504
                        } else if (ailp->next_action_time < 0) {
1505
                                //      This forces him to create a new path every second.
1506
                                ailp->next_action_time = F1_0;
1507
                                create_path_to_believed_player_segment(objp, 100, create_path_safety_flag::unsafe);
1508
                                ailp->mode = ai_mode::AIM_THIEF_ATTACK;
1509
                        } else {
1510
                                if (player_is_visible(player_visibility) && dist_to_player < F1_0*100)
1511
                                {
1512
                                        //      If the player is close to looking at the thief, thief shall run away.
1513
                                        //      No more stupid thief trying to sneak up on you when you're looking right at him!
1514
                                        if (dist_to_player > F1_0*60) {
1515
                                                fix     dot = vm_vec_dot(vec_to_player, ConsoleObject->orient.fvec);
1516
                                                if (dot < -F1_0/2) {    //      Looking at least towards thief, so thief will run!
1517
                                                        create_n_segment_path(objp, 10, ConsoleObject->segnum);
1518
                                                        ailp->next_action_time = Thief_wait_times[Difficulty_level]/2;
1519
                                                        ailp->mode = ai_mode::AIM_THIEF_RETREAT;
1520
                                                }
1521
                                        }
1522
                                        ai_turn_towards_vector(vec_to_player, objp, F1_0/4);
1523
                                        move_towards_player(objp, vec_to_player);
1524
                                } else {
1525
                                        ai_static       *aip = &objp->ctype.ai_info;
1526
                                        //      If path length == 0, then he will keep trying to create path, but he is probably stuck in his closet.
1527
                                        if ((aip->path_length > 1) || ((d_tick_count & 0x0f) == 0)) {
1528
                                                ai_follow_path(objp, player_visibility, &vec_to_player);
1529
                                                ailp->mode = ai_mode::AIM_THIEF_ATTACK;
1530
                                        }
1531
                                }
1532
                        }
1533
                        break;
1534
 
1535
                default:
1536
                        ailp->mode = ai_mode::AIM_THIEF_ATTACK;
1537
                        ailp->next_action_time = F1_0;
1538
                        break;
1539
        }
1540
 
1541
}
1542
 
1543
//      ----------------------------------------------------------------------------
1544
//      Return true if this item (whose presence is indicated by Players[player_num].flags) gets stolen.
1545
static int maybe_steal_flag_item(const vmobjptr_t playerobjp, const PLAYER_FLAG flagval)
1546
{
1547
        auto &ThiefUniqueState = LevelUniqueObjectState.ThiefState;
1548
        auto &plr_flags = playerobjp->ctype.player_info.powerup_flags;
1549
        if (plr_flags & flagval)
1550
        {
1551
                if (d_rand() < THIEF_PROBABILITY) {
1552
                        int     powerup_index;
1553
                        const char *msg;
1554
                        plr_flags &= (~flagval);
1555
                        switch (flagval) {
1556
                                case PLAYER_FLAGS_INVULNERABLE:
1557
                                        powerup_index = POW_INVULNERABILITY;
1558
                                        msg = "Invulnerability stolen!";
1559
                                        break;
1560
                                case PLAYER_FLAGS_CLOAKED:
1561
                                        powerup_index = POW_CLOAK;
1562
                                        msg = "Cloak stolen!";
1563
                                        break;
1564
                                case PLAYER_FLAGS_MAP_ALL:
1565
                                        powerup_index = POW_FULL_MAP;
1566
                                        msg = "Full map stolen!";
1567
                                        break;
1568
                                case PLAYER_FLAGS_QUAD_LASERS:
1569
                                        update_laser_weapon_info();
1570
                                        powerup_index = POW_QUAD_FIRE;
1571
                                        msg = "Quad lasers stolen!";
1572
                                        break;
1573
                                case PLAYER_FLAGS_AFTERBURNER:
1574
                                        powerup_index = POW_AFTERBURNER;
1575
                                        msg = "Afterburner stolen!";
1576
                                        break;
1577
                                case PLAYER_FLAGS_CONVERTER:
1578
                                        powerup_index = POW_CONVERTER;
1579
                                        msg = "Converter stolen!";
1580
                                        break;
1581
                                case PLAYER_FLAG::HEADLIGHT_PRESENT_AND_ON:
1582
                                        powerup_index = POW_HEADLIGHT;
1583
                                        msg = "Headlight stolen!";
1584
                                        break;
1585
                                default:
1586
                                        assert(false);
1587
                                        return 0;
1588
                        }
1589
                        ThiefUniqueState.Stolen_items[ThiefUniqueState.Stolen_item_index] = powerup_index;
1590
                        thief_message_str(msg);
1591
                        return 1;
1592
                }
1593
        }
1594
 
1595
        return 0;
1596
}
1597
 
1598
//      ----------------------------------------------------------------------------
1599
static int maybe_steal_secondary_weapon(const vmobjptr_t playerobjp, int weapon_num)
1600
{
1601
        auto &ThiefUniqueState = LevelUniqueObjectState.ThiefState;
1602
        auto &player_info = playerobjp->ctype.player_info;
1603
        if (auto &secondary_ammo = player_info.secondary_ammo[weapon_num])
1604
                if (d_rand() < THIEF_PROBABILITY) {
1605
                        if (weapon_index_is_player_bomb(weapon_num))
1606
                        {
1607
                                if (d_rand() > 8192)            //      Come in groups of 4, only add 1/4 of time.
1608
                                        return 0;
1609
                        }
1610
                        //      Smart mines and proxbombs don't get dropped because they only come in 4 packs.
1611
                        else
1612
                                ThiefUniqueState.Stolen_items[ThiefUniqueState.Stolen_item_index] = Secondary_weapon_to_powerup[weapon_num];
1613
                        thief_message("%s stolen!", SECONDARY_WEAPON_NAMES(weapon_num));                //      Danger! Danger! Use of literal!  Danger!
1614
                        if (-- secondary_ammo == 0)
1615
                                auto_select_secondary_weapon(player_info);
1616
 
1617
                        // -- compress_stolen_items();
1618
                        return 1;
1619
                }
1620
 
1621
        return 0;
1622
}
1623
 
1624
//      ----------------------------------------------------------------------------
1625
static int maybe_steal_primary_weapon(const vmobjptr_t playerobjp, int weapon_num)
1626
{
1627
        auto &ThiefUniqueState = LevelUniqueObjectState.ThiefState;
1628
        auto &player_info = playerobjp->ctype.player_info;
1629
        bool is_energy_weapon = true;
1630
        switch (static_cast<primary_weapon_index_t>(weapon_num))
1631
        {
1632
                case primary_weapon_index_t::LASER_INDEX:
1633
                        if (!player_info.laser_level)
1634
                                return 0;
1635
                        break;
1636
                case primary_weapon_index_t::VULCAN_INDEX:
1637
                case primary_weapon_index_t::GAUSS_INDEX:
1638
                        if (!player_info.vulcan_ammo)
1639
                                return 0;
1640
                        is_energy_weapon = false;
1641
                        DXX_BOOST_FALLTHROUGH;
1642
                default:
1643
                        if (!(player_info.primary_weapon_flags & HAS_PRIMARY_FLAG(weapon_num)))
1644
                                return 0;
1645
                        break;
1646
        }
1647
        if (is_energy_weapon)
1648
        {
1649
                if (((Game_mode & GM_MULTI)
1650
                        ? Netgame.ThiefModifierFlags
1651
                        : PlayerCfg.ThiefModifierFlags) & ThiefModifier::NoEnergyWeapons)
1652
                        return 0;
1653
        }
1654
        {
1655
                if (d_rand() < THIEF_PROBABILITY) {
1656
                        powerup_type_t primary_weapon_powerup;
1657
                        if (weapon_num == primary_weapon_index_t::LASER_INDEX)
1658
                        {
1659
                                auto &laser_level = player_info.laser_level;
1660
                                primary_weapon_powerup = (laser_level > MAX_LASER_LEVEL)
1661
                                        ? POW_SUPER_LASER
1662
                                        : Primary_weapon_to_powerup[weapon_num];
1663
                                        /* Laser levels are zero-based, so print the old
1664
                                         * level, then decrement it.  Decrementing first
1665
                                         * would produce confusing output, particularly when
1666
                                         * the user loses the final laser powerup and drops
1667
                                         * to level 0 lasers.
1668
                                         */
1669
                                        const laser_level_t l = laser_level;
1670
                                        -- laser_level;
1671
                                        thief_message("%s level decreased to %u!", PRIMARY_WEAPON_NAMES(weapon_num), l);
1672
                        }
1673
                        else
1674
                        {
1675
                                player_info.primary_weapon_flags &= ~HAS_PRIMARY_FLAG(weapon_num);
1676
                                primary_weapon_powerup = Primary_weapon_to_powerup[weapon_num];
1677
 
1678
                                thief_message("%s stolen!", PRIMARY_WEAPON_NAMES(weapon_num));          //      Danger! Danger! Use of literal!  Danger!
1679
                                auto_select_primary_weapon(player_info);
1680
                        }
1681
                        ThiefUniqueState.Stolen_items[ThiefUniqueState.Stolen_item_index] = primary_weapon_powerup;
1682
                        return 1;
1683
                }
1684
        }
1685
 
1686
        return 0;
1687
}
1688
 
1689
 
1690
 
1691
//      ----------------------------------------------------------------------------
1692
//      Called for a thief-type robot.
1693
//      If a item successfully stolen, returns true, else returns false.
1694
//      If a wapon successfully stolen, do everything, removing it from player,
1695
//      updating Stolen_items information, deselecting, etc.
1696
static int attempt_to_steal_item_3(const vmobjptr_t objp, const vmobjptr_t player_num)
1697
{
1698
        ai_local                *ailp = &objp->ctype.ai_info.ail;
1699
        if (ailp->mode != ai_mode::AIM_THIEF_ATTACK)
1700
                return 0;
1701
 
1702
        //      First, try to steal equipped items.
1703
 
1704
        if (auto r = maybe_steal_flag_item(player_num, PLAYER_FLAGS_INVULNERABLE))
1705
                return r;
1706
 
1707
        //      If primary weapon = laser, first try to rip away those nasty quad lasers!
1708
        const auto Primary_weapon = player_num->ctype.player_info.Primary_weapon;
1709
        if (Primary_weapon == primary_weapon_index_t::LASER_INDEX)
1710
                if (auto r = maybe_steal_flag_item(player_num, PLAYER_FLAGS_QUAD_LASERS))
1711
                        return r;
1712
 
1713
        //      Makes it more likely to steal primary than secondary.
1714
        range_for (const int i, xrange(2u))
1715
        {
1716
                (void)i;
1717
                if (auto r = maybe_steal_primary_weapon(player_num, Primary_weapon))
1718
                        return r;
1719
        }
1720
 
1721
        if (auto r = maybe_steal_secondary_weapon(player_num, player_num->ctype.player_info.Secondary_weapon))
1722
                return r;
1723
 
1724
        //      See what the player has and try to snag something.
1725
        //      Try best things first.
1726
        if (auto r = maybe_steal_flag_item(player_num, PLAYER_FLAGS_INVULNERABLE))
1727
                return r;
1728
        if (auto r = maybe_steal_flag_item(player_num, PLAYER_FLAGS_CLOAKED))
1729
                return r;
1730
        if (auto r = maybe_steal_flag_item(player_num, PLAYER_FLAGS_QUAD_LASERS))
1731
                return r;
1732
        if (auto r = maybe_steal_flag_item(player_num, PLAYER_FLAGS_AFTERBURNER))
1733
                return r;
1734
        if (auto r = maybe_steal_flag_item(player_num, PLAYER_FLAGS_CONVERTER))
1735
                return r;
1736
// --   if (maybe_steal_flag_item(player_num, PLAYER_FLAGS_AMMO_RACK))  //      Can't steal because what if have too many items, say 15 homing missiles?
1737
// --           return 1;
1738
        if (auto r = maybe_steal_flag_item(player_num, PLAYER_FLAG::HEADLIGHT_PRESENT_AND_ON))
1739
                return r;
1740
        if (auto r = maybe_steal_flag_item(player_num, PLAYER_FLAGS_MAP_ALL))
1741
                return r;
1742
 
1743
        for (int i=MAX_SECONDARY_WEAPONS-1; i>=0; i--) {
1744
                if (auto r = maybe_steal_primary_weapon(player_num, i))
1745
                        return r;
1746
                if (auto r = maybe_steal_secondary_weapon(player_num, i))
1747
                        return r;
1748
        }
1749
 
1750
        return 0;
1751
}
1752
 
1753
//      ----------------------------------------------------------------------------
1754
static int attempt_to_steal_item_2(const vmobjptr_t objp, const vmobjptr_t player_num)
1755
{
1756
        auto &ThiefUniqueState = LevelUniqueObjectState.ThiefState;
1757
        const auto rval = attempt_to_steal_item_3(objp, player_num);
1758
        if (rval) {
1759
                digi_play_sample_once(SOUND_WEAPON_STOLEN, F1_0);
1760
                auto i = ThiefUniqueState.Stolen_item_index;
1761
                if (d_rand() > 20000)   //      Occasionally, boost the value again
1762
                        ++i;
1763
                constexpr auto size = std::tuple_size<decltype(ThiefUniqueState.Stolen_items)>::value;
1764
                if (++ i >= size)
1765
                        i -= size;
1766
                ThiefUniqueState.Stolen_item_index = i;
1767
        }
1768
        return rval;
1769
}
1770
 
1771
//      ----------------------------------------------------------------------------
1772
//      Called for a thief-type robot.
1773
//      If a item successfully stolen, returns true, else returns false.
1774
//      If a wapon successfully stolen, do everything, removing it from player,
1775
//      updating Stolen_items information, deselecting, etc.
1776
int attempt_to_steal_item(const vmobjptridx_t objp, const vmobjptr_t player_num)
1777
{
1778
        int     rval = 0;
1779
 
1780
        if (objp->ctype.ai_info.dying_start_time)
1781
                return 0;
1782
 
1783
        rval += attempt_to_steal_item_2(objp, player_num);
1784
 
1785
        range_for (const int i, xrange(3u)) {
1786
                (void)i;
1787
                if (!rval || (d_rand() < 11000)) {      //      about 1/3 of time, steal another item
1788
                        rval += attempt_to_steal_item_2(objp, player_num);
1789
                } else
1790
                        break;
1791
        }
1792
        create_n_segment_path(objp, 10, ConsoleObject->segnum);
1793
        ai_local                *ailp = &objp->ctype.ai_info.ail;
1794
        ailp->next_action_time = Thief_wait_times[GameUniqueState.Difficulty_level] / 2;
1795
        ailp->mode = ai_mode::AIM_THIEF_RETREAT;
1796
        if (rval) {
1797
                PALETTE_FLASH_ADD(30, 15, -20);
1798
                if (Game_mode & GM_NETWORK)
1799
                 multi_send_stolen_items();
1800
        }
1801
        return rval;
1802
}
1803
 
1804
// --------------------------------------------------------------------------------------------------------------
1805
//      Indicate no items have been stolen.
1806
void init_thief_for_level(void)
1807
{
1808
        auto &ThiefUniqueState = LevelUniqueObjectState.ThiefState;
1809
        ThiefUniqueState.Stolen_item_index = 0;
1810
        auto &Stolen_items = ThiefUniqueState.Stolen_items;
1811
        Stolen_items.fill(255);
1812
 
1813
        constexpr unsigned iterations = 3;
1814
        static_assert (std::tuple_size<decltype(ThiefUniqueState.Stolen_items)>::value >= iterations * 2, "Stolen_items too small");    //      Oops!  Loop below will overwrite memory!
1815
   if (!(Game_mode & GM_MULTI))    
1816
                for (unsigned i = 0; i < iterations; i++)
1817
                {
1818
                        Stolen_items[2*i] = POW_SHIELD_BOOST;
1819
                        Stolen_items[2*i+1] = POW_ENERGY;
1820
                }
1821
}
1822
 
1823
// --------------------------------------------------------------------------------------------------------------
1824
void drop_stolen_items(const vcobjptr_t objp)
1825
{
1826
        auto &ThiefUniqueState = LevelUniqueObjectState.ThiefState;
1827
        const auto &&segp = vmsegptridx(objp->segnum);
1828
        range_for (auto &i, ThiefUniqueState.Stolen_items)
1829
        {
1830
                if (i != 255)
1831
                {
1832
                        drop_powerup(Vclip, std::exchange(i, 255), 1, objp->mtype.phys_info.velocity, objp->pos, segp, true);
1833
                }
1834
        }
1835
 
1836
}
1837
 
1838
// --------------------------------------------------------------------------------------------------------------
1839
namespace {
1840
 
1841
struct escort_menu : ignore_window_pointer_t
1842
{
1843
        std::array<char, 300> msg;
1844
        static window_event_result event_handler(window *wind, const d_event &event, escort_menu *menu);
1845
        static window_event_result event_key_command(const d_event &event);
1846
};
1847
 
1848
}
1849
 
1850
window_event_result escort_menu::event_key_command(const d_event &event)
1851
{
1852
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
1853
        switch (const auto key = event_key_get(event))
1854
        {
1855
                case KEY_0:
1856
                case KEY_1:
1857
                case KEY_2:
1858
                case KEY_3:
1859
                case KEY_4:
1860
                case KEY_5:
1861
                case KEY_6:
1862
                case KEY_7:
1863
                case KEY_8:
1864
                case KEY_9:
1865
                        BuddyState.Looking_for_marker = game_marker_index::None;
1866
                        BuddyState.Last_buddy_key = -1;
1867
                        set_escort_special_goal(BuddyState, key);
1868
                        BuddyState.Last_buddy_key = -1;
1869
                        return window_event_result::close;
1870
                case KEY_ESC:
1871
                case KEY_ENTER:
1872
                        return window_event_result::close;
1873
                case KEY_T: {
1874
                        const auto temp = std::exchange(BuddyState.Buddy_messages_suppressed, 0);
1875
                        buddy_message("Messages %s.", temp ? "enabled" : "suppressed");
1876
                        BuddyState.Buddy_messages_suppressed = ~temp;
1877
                        return window_event_result::close;
1878
                }
1879
 
1880
                default:
1881
                        break;
1882
        }
1883
        return window_event_result::ignored;
1884
}
1885
 
1886
window_event_result escort_menu::event_handler(window *, const d_event &event, escort_menu *menu)
1887
{
1888
        switch (event.type)
1889
        {
1890
                case EVENT_WINDOW_ACTIVATED:
1891
                        game_flush_inputs();
1892
                        break;
1893
 
1894
                case EVENT_KEY_COMMAND:
1895
                        return event_key_command(event);
1896
                case EVENT_IDLE:
1897
                        timer_delay2(50);
1898
                        break;
1899
 
1900
                case EVENT_WINDOW_DRAW:
1901
                        show_escort_menu(menu->msg);            //TXT_PAUSE);
1902
                        break;
1903
 
1904
                case EVENT_WINDOW_CLOSE:
1905
                        d_free(menu);
1906
                        return window_event_result::ignored;    // continue closing
1907
                default:
1908
                        return window_event_result::ignored;
1909
        }
1910
        return window_event_result::handled;
1911
}
1912
 
1913
unsigned check_warn_local_player_can_control_guidebot(fvcobjptr &vcobjptr, const d_unique_buddy_state &BuddyState, const netgame_info &Netgame)
1914
{
1915
        if (!Netgame.AllowGuidebot || !(Game_mode & GM_MULTI_COOP))
1916
        {
1917
                HUD_init_message_literal(HM_DEFAULT, "Guide-Bot is not enabled!");
1918
                return 0;
1919
        }
1920
        auto &plr = get_player_controlling_guidebot(BuddyState, Players);
1921
        if (plr.objnum == object_none)
1922
                return 0;
1923
        auto &plrobj = *vcobjptr(plr.objnum);
1924
        if (ConsoleObject != &plrobj)
1925
        {
1926
                HUD_init_message(HM_DEFAULT, "Guide-Bot is controlled by %s!", plr.callsign.operator const char *());
1927
                return 0;
1928
        }
1929
        return 1;
1930
}
1931
 
1932
void do_escort_menu(void)
1933
{
1934
        auto &BuddyState = LevelUniqueObjectState.BuddyState;
1935
        auto &Objects = LevelUniqueObjectState.Objects;
1936
        auto &vcobjptr = Objects.vcptr;
1937
        auto &vmobjptr = Objects.vmptr;
1938
        auto &vmobjptridx = Objects.vmptridx;
1939
        int     next_goal;
1940
        char    goal_str[12];
1941
        const char *goal_txt;
1942
        const char *tstr;
1943
        escort_menu *menu;
1944
 
1945
        if (Game_mode & GM_MULTI) {
1946
                if (!check_warn_local_player_can_control_guidebot(vcobjptr, BuddyState, Netgame))
1947
                        return;
1948
        }
1949
 
1950
        auto &Robot_info = LevelSharedRobotInfoState.Robot_info;
1951
        const auto &&buddy = find_escort(vmobjptridx, Robot_info);
1952
        if (buddy == object_none)
1953
        {
1954
                HUD_init_message_literal(HM_DEFAULT, "No Guide-Bot present in mine!");
1955
                return;
1956
        }
1957
        //      Needed here or we might not know buddy can talk when he can.
1958
        if (!ok_for_buddy_to_talk()) {
1959
                HUD_init_message(HM_DEFAULT, "%s has not been released.", static_cast<const char *>(PlayerCfg.GuidebotName));
1960
                return;
1961
        }
1962
 
1963
        MALLOC(menu, escort_menu, 1);
1964
        if (!menu)
1965
                return;
1966
 
1967
        // Just make it the full screen size and let show_escort_menu figure it out
1968
        const auto wind = window_create(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT, &escort_menu::event_handler, menu);
1969
        if (!wind)
1970
        {
1971
                d_free(menu);
1972
                return;
1973
        }
1974
 
1975
        auto &plrobj = get_local_plrobj();
1976
        //      This prevents the buddy from coming back if you've told him to scram.
1977
        //      If we don't set next_goal, we get garbage there.
1978
        if (BuddyState.Escort_special_goal == ESCORT_GOAL_SCRAM) {
1979
                BuddyState.Escort_special_goal = ESCORT_GOAL_UNSPECIFIED;       //      Else setting next goal might fail.
1980
                next_goal = escort_set_goal_object(plrobj.ctype.player_info.powerup_flags);
1981
                BuddyState.Escort_special_goal = ESCORT_GOAL_SCRAM;
1982
        } else {
1983
                BuddyState.Escort_special_goal = ESCORT_GOAL_UNSPECIFIED;       //      Else setting next goal might fail.
1984
                next_goal = escort_set_goal_object(plrobj.ctype.player_info.powerup_flags);
1985
        }
1986
 
1987
        switch (next_goal) {
1988
                default:
1989
                case ESCORT_GOAL_UNSPECIFIED:
1990
                        Int3();
1991
                        goal_txt = "ERROR";
1992
                        break;
1993
                case ESCORT_GOAL_BLUE_KEY:
1994
                        goal_txt = "blue key";
1995
                        break;
1996
                case ESCORT_GOAL_GOLD_KEY:
1997
                        goal_txt = "yellow key";
1998
                        break;
1999
                case ESCORT_GOAL_RED_KEY:
2000
                        goal_txt = "red key";
2001
                        break;
2002
                case ESCORT_GOAL_CONTROLCEN:
2003
                        goal_txt = "reactor";
2004
                        break;
2005
                case ESCORT_GOAL_BOSS:
2006
                        goal_txt = "boss";
2007
                        break;
2008
                case ESCORT_GOAL_EXIT:
2009
                        goal_txt = "exit";
2010
                        break;
2011
                case ESCORT_GOAL_MARKER1:
2012
                case ESCORT_GOAL_MARKER2:
2013
                case ESCORT_GOAL_MARKER3:
2014
                case ESCORT_GOAL_MARKER4:
2015
                case ESCORT_GOAL_MARKER5:
2016
                case ESCORT_GOAL_MARKER6:
2017
                case ESCORT_GOAL_MARKER7:
2018
                case ESCORT_GOAL_MARKER8:
2019
                case ESCORT_GOAL_MARKER9:
2020
                        goal_txt = goal_str;
2021
                        snprintf(goal_str, sizeof(goal_str), "marker %i", next_goal-ESCORT_GOAL_MARKER1+1);
2022
                        break;
2023
 
2024
        }
2025
 
2026
        if (!BuddyState.Buddy_messages_suppressed)
2027
                tstr =  "Suppress";
2028
        else
2029
                tstr =  "Enable";
2030
 
2031
        snprintf(menu->msg.data(), menu->msg.size(), "Select Guide-Bot Command:\n\n\n"
2032
                                                "0.  Next Goal: %s" CC_LSPACING_S "3\n\n"
2033
                                                "\x84.  Find Energy Powerup" CC_LSPACING_S "3\n\n"
2034
                                                "2.  Find Energy Center" CC_LSPACING_S "3\n\n"
2035
                                                "3.  Find Shield Powerup" CC_LSPACING_S "3\n\n"
2036
                                                "4.  Find Any Powerup" CC_LSPACING_S "3\n\n"
2037
                                                "5.  Find a Robot" CC_LSPACING_S "3\n\n"
2038
                                                "6.  Find a Hostage" CC_LSPACING_S "3\n\n"
2039
                                                "7.  Stay Away From Me" CC_LSPACING_S "3\n\n"
2040
                                                "8.  Find My Powerups" CC_LSPACING_S "3\n\n"
2041
                                                "9.  Find the exit\n\n"
2042
                                                "T.  %s Messages"
2043
                                                // -- "9.       Find the exit" CC_LSPACING_S "3\n"
2044
                                , goal_txt, tstr);
2045
}
2046
 
2047
//      -------------------------------------------------------------------------------
2048
//      Show the Buddy menu!
2049
void show_escort_menu(const std::array<char, 300> &amsg)
2050
{      
2051
        const auto msg = amsg.data();
2052
        int     w,h;
2053
        int     x,y;
2054
 
2055
 
2056
        gr_set_default_canvas();
2057
 
2058
        auto &canvas = *grd_curcanv;
2059
        const auto &game_font = *GAME_FONT;
2060
        gr_get_string_size(game_font, msg, &w, &h, nullptr);
2061
 
2062
        x = (SWIDTH-w)/2;
2063
        y = (SHEIGHT-h)/2;
2064
 
2065
        gr_set_fontcolor(canvas, BM_XRGB(0, 28, 0), -1);
2066
 
2067
        nm_draw_background(canvas, x - BORDERX, y - BORDERY, x + w + BORDERX, y + h + BORDERY);
2068
 
2069
        gr_ustring(canvas, game_font, x, y, msg);
2070
 
2071
        reset_cockpit();
2072
}
2073
 
2074
}