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