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 &¢er_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 | } |