1 /* vi: set ts=2 shiftwidth=2 expandtab:
2 *
3 * Copyright (C) 2003-2008 Simon Baldwin and Mark J. Tilford
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License
7 * as published by the Free Software Foundation.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
17 * USA
18 */
19
20 /*
21 * Module notes:
22 *
23 * o ...
24 */
25
26 #include <assert.h>
27 #include <stdlib.h>
28 #include <stddef.h>
29
30 #include "scare.h"
31 #include "scprotos.h"
32 #include "scgamest.h"
33
34
35 /* Trace flag, set before running. */
36 static sc_bool npc_trace = FALSE;
37
38
39 /*
40 * npc_in_room()
41 *
42 * Return TRUE if a given NPC is currently in a given room.
43 */
44 sc_bool
npc_in_room(sc_gameref_t game,sc_int npc,sc_int room)45 npc_in_room (sc_gameref_t game, sc_int npc, sc_int room)
46 {
47 if (npc_trace)
48 {
49 sc_trace ("NPC: checking NPC %ld in room %ld (NPC is in %ld)\n",
50 npc, room, gs_npc_location (game, npc));
51 }
52
53 return gs_npc_location (game, npc) - 1 == room;
54 }
55
56
57 /*
58 * npc_count_in_room()
59 *
60 * Return the count of characters in the room, including the player.
61 */
62 sc_int
npc_count_in_room(sc_gameref_t game,sc_int room)63 npc_count_in_room (sc_gameref_t game, sc_int room)
64 {
65 sc_int count, npc;
66
67 /* Start with the player. */
68 count = gs_player_in_room (game, room) ? 1 : 0;
69
70 /* Total up other NPCs inhabiting the room. */
71 for (npc = 0; npc < gs_npc_count (game); npc++)
72 {
73 if (gs_npc_location (game, npc) - 1 == room)
74 count++;
75 }
76 return count;
77 }
78
79
80 /*
81 * npc_start_npc_walk()
82 *
83 * Start the given walk for the given NPC.
84 */
85 void
npc_start_npc_walk(sc_gameref_t game,sc_int npc,sc_int walk)86 npc_start_npc_walk (sc_gameref_t game, sc_int npc, sc_int walk)
87 {
88 const sc_prop_setref_t bundle = gs_get_bundle (game);
89 sc_vartype_t vt_key[6];
90 sc_int movetime;
91
92 /* Retrieve movetime 0 for the NPC walk. */
93 vt_key[0].string = "NPCs";
94 vt_key[1].integer = npc;
95 vt_key[2].string = "Walks";
96 vt_key[3].integer = walk;
97 vt_key[4].string = "MoveTimes";
98 vt_key[5].integer = 0;
99 movetime = prop_get_integer (bundle, "I<-sisisi", vt_key) + 1;
100
101 /* Set up walkstep. */
102 gs_set_npc_walkstep (game, npc, walk, movetime);
103 }
104
105
106 /*
107 * npc_turn_update()
108 * npc_setup_initial()
109 *
110 * Set initial values for NPC states, and update on turns.
111 */
112 void
npc_turn_update(sc_gameref_t game)113 npc_turn_update (sc_gameref_t game)
114 {
115 sc_int index_;
116
117 /* Set current values for NPC seen states. */
118 for (index_ = 0; index_ < gs_npc_count (game); index_++)
119 {
120 if (!gs_npc_seen (game, index_)
121 && npc_in_room (game, index_, gs_playerroom (game)))
122 gs_set_npc_seen (game, index_, TRUE);
123 }
124 }
125
126 void
npc_setup_initial(sc_gameref_t game)127 npc_setup_initial (sc_gameref_t game)
128 {
129 const sc_prop_setref_t bundle = gs_get_bundle (game);
130 sc_vartype_t vt_key[5];
131 sc_int index_;
132
133 /* Start any walks that do not depend on a StartTask */
134 vt_key[0].string = "NPCs";
135 for (index_ = 0; index_ < gs_npc_count (game); index_++)
136 {
137 sc_int walk;
138
139 /* Set up invariant parts of the properties key. */
140 vt_key[1].integer = index_;
141 vt_key[2].string = "Walks";
142
143 /* Process each walk, starting at the last and working backwards. */
144 for (walk = gs_npc_walkstep_count (game, index_) - 1; walk >= 0; walk--)
145 {
146 sc_int starttask;
147
148 /* If StartTask is zero, start walk at game start. */
149 vt_key[3].integer = walk;
150 vt_key[4].string = "StartTask";
151 starttask = prop_get_integer (bundle, "I<-sisis", vt_key);
152 if (starttask == 0)
153 npc_start_npc_walk (game, index_, walk);
154 }
155 }
156
157 /* Update seen flags for initial states. */
158 npc_turn_update (game);
159 }
160
161
162 /*
163 * npc_room_in_roomgroup()
164 *
165 * Return TRUE if a given room is in a given group.
166 */
167 static sc_bool
npc_room_in_roomgroup(sc_gameref_t game,sc_int room,sc_int group)168 npc_room_in_roomgroup (sc_gameref_t game, sc_int room, sc_int group)
169 {
170 const sc_prop_setref_t bundle = gs_get_bundle (game);
171 sc_vartype_t vt_key[4];
172 sc_int member;
173
174 /* Check roomgroup membership. */
175 vt_key[0].string = "RoomGroups";
176 vt_key[1].integer = group;
177 vt_key[2].string = "List";
178 vt_key[3].integer = room;
179 member = prop_get_integer (bundle, "I<-sisi", vt_key);
180 return member != 0;
181 }
182
183
184 /* List of direction names, for printing entry/exit messages. */
185 static const sc_char *const DIRNAMES_4[] = {
186 "the north", "the east", "the south", "the west", "above", "below",
187 "inside", "outside",
188 NULL
189 };
190 static const sc_char *const DIRNAMES_8[] = {
191 "the north", "the east", "the south", "the west", "above", "below",
192 "inside", "outside",
193 "the north-east", "the south-east", "the south-west", "the north-west",
194 NULL
195 };
196
197 /*
198 * npc_random_adjacent_roomgroup_member()
199 *
200 * Return a random member of group adjacent to given room.
201 */
202 static sc_int
npc_random_adjacent_roomgroup_member(sc_gameref_t game,sc_int room,sc_int group)203 npc_random_adjacent_roomgroup_member (sc_gameref_t game,
204 sc_int room, sc_int group)
205 {
206 const sc_prop_setref_t bundle = gs_get_bundle (game);
207 sc_vartype_t vt_key[5];
208 sc_bool eightpointcompass;
209 sc_int roomlist[12], count, length, index_;
210
211 /* If given room is "hidden", return nothing. */
212 if (room == -1)
213 return -1;
214
215 /* How many exits to consider? */
216 vt_key[0].string = "Globals";
217 vt_key[1].string = "EightPointCompass";
218 eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key);
219 if (eightpointcompass)
220 length = sizeof (DIRNAMES_8) / sizeof (DIRNAMES_8[0]) - 1;
221 else
222 length = sizeof (DIRNAMES_4) / sizeof (DIRNAMES_4[0]) - 1;
223
224 /* Poll adjacent rooms. */
225 vt_key[0].string = "Rooms";
226 vt_key[1].integer = room;
227 vt_key[2].string = "Exits";
228 count = 0;
229 for (index_ = 0; index_ < length; index_++)
230 {
231 sc_int adjacent;
232
233 vt_key[3].integer = index_;
234 vt_key[4].string = "Dest";
235 adjacent = prop_get_child_count (bundle, "I<-sisis", vt_key);
236
237 if (adjacent > 0 && npc_room_in_roomgroup (game, adjacent - 1, group))
238 {
239 roomlist[count] = adjacent - 1;
240 count++;
241 }
242 }
243
244 /* Return a random adjacent room, or -1 if nothing is adjacent. */
245 return (count > 0) ? roomlist[sc_randomint (0, count - 1)] : -1;
246 }
247
248
249 /*
250 * npc_announce()
251 *
252 * Helper for npc_tick_npc().
253 */
254 static void
npc_announce(sc_gameref_t game,sc_int npc,sc_int room,sc_bool is_exit,sc_int npc_room)255 npc_announce (sc_gameref_t game, sc_int npc,
256 sc_int room, sc_bool is_exit, sc_int npc_room)
257 {
258 const sc_filterref_t filter = gs_get_filter (game);
259 const sc_prop_setref_t bundle = gs_get_bundle (game);
260 sc_vartype_t vt_key[5], vt_rvalue;
261 const sc_char *text, *name, *const *dirnames;
262 sc_int dir, dir_match;
263 sc_bool eightpointcompass, showenterexit, found;
264
265 /* If no announcement required, return immediately. */
266 vt_key[0].string = "NPCs";
267 vt_key[1].integer = npc;
268 vt_key[2].string = "ShowEnterExit";
269 showenterexit = prop_get_boolean (bundle, "B<-sis", vt_key);
270 if (!showenterexit)
271 return;
272
273 /* Get exit or entry text, and NPC name. */
274 vt_key[2].string = is_exit ? "ExitText" : "EnterText";
275 text = prop_get_string (bundle, "S<-sis", vt_key);
276 vt_key[2].string = "Name";
277 name = prop_get_string (bundle, "S<-sis", vt_key);
278
279 /* Decide on four or eight point compass names list. */
280 vt_key[0].string = "Globals";
281 vt_key[1].string = "EightPointCompass";
282 eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key);
283 dirnames = eightpointcompass ? DIRNAMES_8 : DIRNAMES_4;
284
285 /* Set invariant key for room exit search. */
286 vt_key[0].string = "Rooms";
287 vt_key[1].integer = room;
288 vt_key[2].string = "Exits";
289
290 /* Find the room exit that matches the NPC room. */
291 found = FALSE;
292 dir_match = 0;
293 for (dir = 0; dirnames[dir]; dir++)
294 {
295 vt_key[3].integer = dir;
296 if (prop_get (bundle, "I<-sisi", &vt_rvalue, vt_key))
297 {
298 sc_int dest;
299
300 /* Get room's direction destination, and compare. */
301 vt_key[4].string = "Dest";
302 dest = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
303 if (dest == npc_room)
304 {
305 dir_match = dir;
306 found = TRUE;
307 break;
308 }
309 }
310 }
311
312 /* Print NPC exit/entry details. */
313 pf_buffer_character (filter, '\n');
314 pf_new_sentence (filter);
315 pf_buffer_string (filter, name);
316 pf_buffer_character (filter, ' ');
317 pf_buffer_string (filter, text);
318 if (found)
319 {
320 pf_buffer_string (filter, is_exit ? " to " : " from ");
321 pf_buffer_string (filter, dirnames[dir_match]);
322 }
323 pf_buffer_string (filter, ".\n");
324
325 /* Handle any associated resource. */
326 vt_key[0].string = "NPCs";
327 vt_key[1].integer = npc;
328 vt_key[2].string = "Res";
329 vt_key[3].integer = is_exit ? 3 : 2;
330 res_handle_resource (game, "sisi", vt_key);
331 }
332
333
334 /*
335 * npc_tick_npc_walk()
336 *
337 * Helper for npc_tick_npc().
338 */
339 static void
npc_tick_npc_walk(sc_gameref_t game,sc_int npc,sc_int walk)340 npc_tick_npc_walk (sc_gameref_t game, sc_int npc, sc_int walk)
341 {
342 const sc_prop_setref_t bundle = gs_get_bundle (game);
343 sc_vartype_t vt_key[6];
344 sc_int roomgroups, movetimes, walkstep, start, dest, destnum;
345 sc_int chartask, objecttask;
346
347 if (npc_trace)
348 {
349 sc_trace ("NPC: ticking NPC %ld, walk %ld: step %ld\n",
350 npc, walk, gs_npc_walkstep (game, npc, walk));
351 }
352
353 /* Count roomgroups for later use. */
354 vt_key[0].string = "RoomGroups";
355 roomgroups = prop_get_child_count (bundle, "I<-s", vt_key);
356
357 /* Get move times array length. */
358 vt_key[0].string = "NPCs";
359 vt_key[1].integer = npc;
360 vt_key[2].string = "Walks";
361 vt_key[3].integer = walk;
362 vt_key[4].string = "MoveTimes";
363 movetimes = prop_get_child_count (bundle, "I<-sisis", vt_key);
364
365 /* Find a step to match the movetime. */
366 for (walkstep = 0; walkstep < movetimes - 1; walkstep++)
367 {
368 sc_int movetime;
369
370 vt_key[5].integer = walkstep + 1;
371 movetime = prop_get_integer (bundle, "I<-sisisi", vt_key);
372 if (gs_npc_walkstep (game, npc, walk) > movetime)
373 break;
374 }
375
376 /* Sort out a destination. */
377 dest = start = gs_npc_location (game, npc) - 1;
378
379 vt_key[4].string = "Rooms";
380 vt_key[5].integer = walkstep;
381 destnum = prop_get_integer (bundle, "I<-sisisi", vt_key);
382
383 if (destnum == 0) /* Hidden. */
384 dest = -1;
385 else if (destnum == 1) /* Follow player. */
386 dest = gs_playerroom (game);
387 else if (destnum < gs_room_count (game) + 2)
388 dest = destnum - 2; /* To room. */
389 else if (destnum < gs_room_count (game) + 2 + roomgroups)
390 {
391 sc_int initial;
392
393 /* For roomgroup walks, move only if walksteps has just refreshed. */
394 vt_key[4].string = "MoveTimes";
395 vt_key[5].integer = 0;
396 initial = prop_get_integer (bundle, "I<-sisisi", vt_key);
397 if (gs_npc_walkstep (game, npc, walk) == initial)
398 {
399 sc_int group;
400
401 group = destnum - 2 - gs_room_count (game);
402 dest = npc_random_adjacent_roomgroup_member (game, start, group);
403 if (dest == -1)
404 dest = lib_random_roomgroup_member (game, group);
405 }
406 }
407
408 /* See if the NPC actually moved. */
409 if (start != dest)
410 {
411 if (npc_trace)
412 sc_trace ("NPC: walking NPC %ld moved to %ld\n", npc, dest);
413
414 /* Move NPC to destination. */
415 gs_set_npc_location (game, npc, dest + 1);
416
417 /* Announce NPC movements, and handle meeting characters and objects. */
418 if (gs_player_in_room (game, start))
419 npc_announce (game, npc, start, TRUE, dest);
420 else if (gs_player_in_room (game, dest))
421 npc_announce (game, npc, dest, FALSE, start);
422 }
423
424 /* Handle meeting characters and objects. */
425 vt_key[4].string = "CharTask";
426 chartask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
427 if (chartask >= 0)
428 {
429 sc_int meetchar;
430
431 /* Run meetchar task if appropriate. */
432 vt_key[4].string = "MeetChar";
433 meetchar = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
434 if ((meetchar == -1 && gs_player_in_room (game, dest))
435 || (meetchar >= 0 && dest == gs_npc_location (game, meetchar) - 1))
436 {
437 if (task_can_run_task (game, chartask))
438 task_run_task (game, chartask, TRUE);
439 }
440 }
441
442 vt_key[4].string = "ObjectTask";
443 objecttask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
444 if (objecttask >= 0)
445 {
446 sc_int meetobject;
447
448 /* Run meetobject task if appropriate. */
449 vt_key[4].string = "MeetObject";
450 meetobject = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
451 if (meetobject >= 0 && obj_directly_in_room (game, meetobject, dest))
452 {
453 if (task_can_run_task (game, objecttask))
454 task_run_task (game, objecttask, TRUE);
455 }
456 }
457 }
458
459
460 /*
461 * npc_tick_npc()
462 *
463 * Move an NPC one step along current walk.
464 */
465 static void
npc_tick_npc(sc_gameref_t game,sc_int npc)466 npc_tick_npc (sc_gameref_t game, sc_int npc)
467 {
468 const sc_prop_setref_t bundle = gs_get_bundle (game);
469 sc_vartype_t vt_key[6];
470 sc_int walk;
471 sc_bool has_moved = FALSE;
472
473 if (npc_trace)
474 sc_trace ("NPC: ticking NPC %ld\n", npc);
475
476 /* Set up invariant key parts. */
477 vt_key[0].string = "NPCs";
478 vt_key[1].integer = npc;
479 vt_key[2].string = "Walks";
480
481 /* Find active walk, and if any found, make a step along it. */
482 for (walk = gs_npc_walkstep_count (game, npc) - 1; walk >= 0; walk--)
483 {
484 sc_int starttask, stoppingtask;
485
486 /* Ignore finished walks. */
487 if (gs_npc_walkstep (game, npc, walk) <= 0)
488 continue;
489
490 /* Get start task. */
491 vt_key[3].integer = walk;
492 vt_key[4].string = "StartTask";
493 starttask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
494
495 /*
496 * Check that the starter is still complete, and if not, stop walk.
497 * Then keep on looking for an active walk.
498 */
499 if (starttask >= 0 && !gs_task_done (game, starttask))
500 {
501 if (npc_trace)
502 sc_trace ("NPC: stopping NPC %ld walk, start task undone\n", npc);
503
504 gs_set_npc_walkstep (game, npc, walk, -1);
505 continue;
506 }
507
508 /* Get stopping task. */
509 vt_key[4].string = "StoppingTask";
510 stoppingtask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
511
512 /*
513 * If any stopping task has completed, ignore this walk but don't
514 * actually finish it; more like an event pauser, then.
515 *
516 * TODO Is this right?
517 */
518 if (stoppingtask >= 0 && gs_task_done (game, stoppingtask))
519 {
520 if (npc_trace)
521 sc_trace ("NPC: ignoring NPC %ld walk, stop task done\n", npc);
522
523 continue;
524 }
525
526 /* Decrement steps. */
527 gs_decrement_npc_walkstep (game, npc, walk);
528
529 /* If we just hit a walk end, loop if called for. */
530 if (gs_npc_walkstep (game, npc, walk) == 0)
531 {
532 sc_bool is_loop;
533
534 /* If walk is a loop, restart it. */
535 vt_key[4].string = "Loop";
536 is_loop = prop_get_boolean (bundle, "B<-sisis", vt_key);
537 if (is_loop)
538 {
539 vt_key[4].string = "MoveTimes";
540 vt_key[5].integer = 0;
541 gs_set_npc_walkstep (game, npc, walk,
542 prop_get_integer (bundle,
543 "I<-sisisi", vt_key));
544 }
545 else
546 gs_set_npc_walkstep (game, npc, walk, -1);
547 }
548
549 /*
550 * If not yet made a move on this walk, make one, and once made, make
551 * no other
552 */
553 if (!has_moved)
554 {
555 npc_tick_npc_walk (game, npc, walk);
556 has_moved = TRUE;
557 }
558 }
559 }
560
561
562 /*
563 * npc_tick_npcs()
564 *
565 * Move each NPC one step along current walk.
566 */
567 void
npc_tick_npcs(sc_gameref_t game)568 npc_tick_npcs (sc_gameref_t game)
569 {
570 const sc_prop_setref_t bundle = gs_get_bundle (game);
571 const sc_gameref_t undo = game->undo;
572 sc_int npc;
573
574 /*
575 * Compare the player location to last turn, to see if the player has moved
576 * this turn. If moved, look for meetings with NPCs.
577 *
578 * TODO Is this the right place to do this. After ticking each NPC, rather
579 * than before, seems more appropriate. But the messages come out in the
580 * right order by putting it here.
581 *
582 * Also, note that we take the shortcut of using the undo gamestate here,
583 * rather than properly recording the prior location of the player, and
584 * perhaps also NPCs, in the live gamestate.
585 */
586 if (undo && !gs_player_in_room (undo, gs_playerroom (game)))
587 {
588 for (npc = 0; npc < gs_npc_count (game); npc++)
589 {
590 sc_int walk;
591
592 /* Iterate each NPC's walks. */
593 for (walk = gs_npc_walkstep_count (game, npc) - 1; walk >= 0; walk--)
594 {
595 sc_vartype_t vt_key[5];
596 sc_int chartask;
597
598 /* Ignore finished walks. */
599 if (gs_npc_walkstep (game, npc, walk) <= 0)
600 continue;
601
602 /* Retrieve any character meeting task for the NPC. */
603 vt_key[0].string = "NPCs";
604 vt_key[1].integer = npc;
605 vt_key[2].string = "Walks";
606 vt_key[3].integer = walk;
607 vt_key[4].string = "CharTask";
608 chartask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
609 if (chartask >= 0)
610 {
611 sc_int meetchar;
612
613 /* Run meetchar task if appropriate. */
614 vt_key[4].string = "MeetChar";
615 meetchar = prop_get_integer (bundle, "I<-sisis", vt_key) - 1;
616 if (meetchar == -1 &&
617 gs_player_in_room (game, gs_npc_location (game, npc) - 1))
618 {
619 if (task_can_run_task (game, chartask))
620 task_run_task (game, chartask, TRUE);
621 }
622 }
623 }
624 }
625 }
626
627 /* Iterate and tick each individual NPC. */
628 for (npc = 0; npc < gs_npc_count (game); npc++)
629 npc_tick_npc (game, npc);
630 }
631
632
633 /*
634 * npc_debug_trace()
635 *
636 * Set NPC tracing on/off.
637 */
638 void
npc_debug_trace(sc_bool flag)639 npc_debug_trace (sc_bool flag)
640 {
641 npc_trace = flag;
642 }
643