/* vi: set ts=2 shiftwidth=2 expandtab: * * Copyright (C) 2003-2007 Simon Baldwin and Mark J. Tilford * * This program is free software; you can redistribute it and/or modify * it under the terms of version 2 of the GNU General Public License * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ /* * Module notes: * * o ... */ #include #include #include "scare.h" #include "scprotos.h" #include "scgamest.h" /* Trace flag, set before running. */ static sc_bool npc_trace = FALSE; /* * npc_in_room() * * Return TRUE if a given NPC is currently in a given room. */ sc_bool npc_in_room (sc_gameref_t game, sc_int npc, sc_int room) { if (npc_trace) { sc_trace ("NPC: checking NPC %ld in room %ld (NPC is in %ld)\n", npc, room, gs_npc_location (game, npc)); } return gs_npc_location (game, npc) - 1 == room; } /* * npc_count_in_room() * * Return the count of characters in the room, including the player. */ sc_int npc_count_in_room (sc_gameref_t game, sc_int room) { sc_int count, npc; /* Start with the player. */ count = (gs_playerroom (game) == room) ? 1 : 0; /* Total up other NPCs inhabiting the room. */ for (npc = 0; npc < gs_npc_count (game); npc++) { if (gs_npc_location (game, npc) - 1 == room) count++; } return count; } /* * npc_meet_npcs_common() * npc_meet_any_npcs() * npc_meet_specific_npc() * npc_meet_any_objects() * * Handle meeting characters and objects while walking. For characters, we * can restrict reports to just meetings for that character. */ static void npc_meet_npcs_common (sc_gameref_t game, sc_int npc, sc_int walk, sc_int other_npc) { const sc_prop_setref_t bundle = gs_get_bundle (game); sc_vartype_t vt_key[5]; sc_int room, chartask; /* Get the current NPC room, and set invariant key parts. */ room = gs_npc_location (game, npc) - 1; vt_key[0].string = "NPCs"; vt_key[1].integer = npc; vt_key[2].string = "Walks"; vt_key[3].integer = walk; /* Handle meeting characters. */ vt_key[4].string = "CharTask"; chartask = prop_get_integer (bundle, "I<-sisis", vt_key); if (chartask != 0) { sc_int meetchar; /* * Run meetchar task if appropriate. If other_npc is passed in, we * restrict task running to only that NPC or to the player. */ vt_key[4].string = "MeetChar"; meetchar = prop_get_integer (bundle, "I<-sisis", vt_key); if ((meetchar == 0 && room == gs_playerroom (game)) || (meetchar > 0 && (other_npc == -1 || other_npc == meetchar - 1) && room == gs_npc_location (game, meetchar - 1) - 1)) { if (task_can_run_task (game, chartask - 1)) task_run_task (game, chartask - 1, TRUE); } } } static void npc_meet_any_npcs (sc_gameref_t game, sc_int npc, sc_int walk) { npc_meet_npcs_common (game, npc, walk, -1); } static void npc_meet_specific_npc (sc_gameref_t game, sc_int npc, sc_int walk, sc_int other_npc) { npc_meet_npcs_common (game, npc, walk, other_npc); } static void npc_meet_any_objects (sc_gameref_t game, sc_int npc, sc_int walk) { const sc_prop_setref_t bundle = gs_get_bundle (game); sc_vartype_t vt_key[5]; sc_int room, objecttask; /* Get the current NPC room, and set invariant key parts. */ room = gs_npc_location (game, npc) - 1; vt_key[0].string = "NPCs"; vt_key[1].integer = npc; vt_key[2].string = "Walks"; vt_key[3].integer = walk; /* Handle meeting objects. */ vt_key[4].string = "ObjectTask"; objecttask = prop_get_integer (bundle, "I<-sisis", vt_key); if (objecttask != 0) { sc_int meetobject; /* Run meetobject task if appropriate. */ vt_key[4].string = "MeetObject"; meetobject = prop_get_integer (bundle, "I<-sisis", vt_key); if (meetobject > 0 && obj_directly_in_room (game, meetobject - 1, room)) { if (task_can_run_task (game, objecttask - 1)) task_run_task (game, objecttask - 1, TRUE); } } } /* * npc_start_npc_walk() * * Start the given walk for the given NPC. */ void npc_start_npc_walk (sc_gameref_t game, sc_int npc, sc_int walk) { const sc_prop_setref_t bundle = gs_get_bundle (game); sc_vartype_t vt_key[6]; sc_int movetime; /* Retrieve movetime 0 for the NPC walk. */ vt_key[0].string = "NPCs"; vt_key[1].integer = npc; vt_key[2].string = "Walks"; vt_key[3].integer = walk; vt_key[4].string = "MoveTimes"; vt_key[5].integer = 0; movetime = prop_get_integer (bundle, "I<-sisisi", vt_key) + 1; /* Set up walkstep, and handle possibly meeting characters and objects. */ gs_set_npc_walkstep (game, npc, walk, movetime); npc_meet_any_npcs (game, npc, walk); npc_meet_any_objects (game, npc, walk); } /* * npc_turn_update() * npc_setup_initial() * * Set initial values for NPC states, and update on turns. */ void npc_turn_update (sc_gameref_t game) { sc_int index_; /* Set current values for NPC seen states. */ for (index_ = 0; index_ < gs_npc_count (game); index_++) { if (npc_in_room (game, index_, gs_playerroom (game))) gs_set_npc_seen (game, index_, TRUE); } } void npc_setup_initial (sc_gameref_t game) { const sc_prop_setref_t bundle = gs_get_bundle (game); sc_vartype_t vt_key[5]; sc_int index_; /* Start any walks that do not depend on a StartTask */ vt_key[0].string = "NPCs"; for (index_ = 0; index_ < gs_npc_count (game); index_++) { sc_int walk; /* Set up invariant parts of the properties key. */ vt_key[1].integer = index_; vt_key[2].string = "Walks"; /* Process each walk, starting at the last and working backwards. */ for (walk = gs_npc_walkstep_count (game, index_) - 1; walk >= 0; walk--) { sc_int starttask; /* If StartTask is zero, start walk at game start. */ vt_key[3].integer = walk; vt_key[4].string = "StartTask"; starttask = prop_get_integer (bundle, "I<-sisis", vt_key); if (starttask == 0) npc_start_npc_walk (game, index_, walk); } } /* Update seen flags for initial states. */ npc_turn_update (game); } /* * npc_room_in_roomgroup() * * Return TRUE if a given room is in a given group. */ static sc_bool npc_room_in_roomgroup (sc_gameref_t game, sc_int room, sc_int group) { const sc_prop_setref_t bundle = gs_get_bundle (game); sc_vartype_t vt_key[4]; sc_int member; /* Check roomgroup membership. */ vt_key[0].string = "RoomGroups"; vt_key[1].integer = group; vt_key[2].string = "List"; vt_key[3].integer = room; member = prop_get_integer (bundle, "I<-sisi", vt_key); return member != 0; } /* List of direction names, for printing entry/exit messages. */ static const sc_char *const DIRNAMES_4[] = { "the north", "the east", "the south", "the west", "above", "below", "inside", "outside", NULL }; static const sc_char *const DIRNAMES_8[] = { "the north", "the east", "the south", "the west", "above", "below", "inside", "outside", "the north-east", "the south-east", "the south-west", "the north-west", NULL }; /* * npc_random_adjacent_roomgroup_member() * * Return a random member of group adjacent to given room. */ static sc_int npc_random_adjacent_roomgroup_member (sc_gameref_t game, sc_int room, sc_int group) { const sc_prop_setref_t bundle = gs_get_bundle (game); sc_vartype_t vt_key[5]; sc_bool eightpointcompass; sc_int roomlist[12], count, length, index_; /* If given room is "hidden", return nothing. */ if (room == -1) return -1; /* How many exits to consider? */ vt_key[0].string = "Globals"; vt_key[1].string = "EightPointCompass"; eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key); if (eightpointcompass) length = sizeof (DIRNAMES_8) / sizeof (DIRNAMES_8[0]) - 1; else length = sizeof (DIRNAMES_4) / sizeof (DIRNAMES_4[0]) - 1; /* Poll adjacent rooms. */ vt_key[0].string = "Rooms"; vt_key[1].integer = room; vt_key[2].string = "Exits"; count = 0; for (index_ = 0; index_ < length; index_++) { sc_int adjacent; vt_key[3].integer = index_; vt_key[4].string = "Dest"; adjacent = prop_get_child_count (bundle, "I<-sisis", vt_key); if (adjacent > 0 && npc_room_in_roomgroup (game, adjacent - 1, group)) { roomlist[count] = adjacent - 1; count++; } } /* Return a random adjacent room, or -1 if nothing is adjacent. */ return (count > 0) ? roomlist[sc_randomint (0, count - 1)] : -1; } /* * npc_announce() * * Helper for npc_tick_npc(). */ static void npc_announce (sc_gameref_t game, sc_int npc, sc_int room, sc_bool is_exit, sc_int npc_room) { const sc_filterref_t filter = gs_get_filter (game); const sc_prop_setref_t bundle = gs_get_bundle (game); sc_vartype_t vt_key[5], vt_rvalue; const sc_char *text, *name, *const *dirnames; sc_int dir, dir_match; sc_bool eightpointcompass, showenterexit, found; /* If no announcement required, return immediately. */ vt_key[0].string = "NPCs"; vt_key[1].integer = npc; vt_key[2].string = "ShowEnterExit"; showenterexit = prop_get_boolean (bundle, "B<-sis", vt_key); if (!showenterexit) return; /* Get exit or entry text, and NPC name. */ vt_key[2].string = is_exit ? "ExitText" : "EnterText"; text = prop_get_string (bundle, "S<-sis", vt_key); vt_key[2].string = "Name"; name = prop_get_string (bundle, "S<-sis", vt_key); /* Decide on four or eight point compass names list. */ vt_key[0].string = "Globals"; vt_key[1].string = "EightPointCompass"; eightpointcompass = prop_get_boolean (bundle, "B<-ss", vt_key); dirnames = eightpointcompass ? DIRNAMES_8 : DIRNAMES_4; /* Set invariant key for room exit search. */ vt_key[0].string = "Rooms"; vt_key[1].integer = room; vt_key[2].string = "Exits"; /* Find the room exit that matches the NPC room. */ found = FALSE; dir_match = 0; for (dir = 0; dirnames[dir]; dir++) { vt_key[3].integer = dir; if (prop_get (bundle, "I<-sisi", &vt_rvalue, vt_key)) { sc_int dest; /* Get room's direction destination, and compare. */ vt_key[4].string = "Dest"; dest = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; if (dest == npc_room) { dir_match = dir; found = TRUE; break; } } } /* Print NPC exit/entry details. */ pf_buffer_character (filter, '\n'); pf_new_sentence (filter); pf_buffer_string (filter, name); pf_buffer_character (filter, ' '); pf_buffer_string (filter, text); if (found) { pf_buffer_string (filter, is_exit ? " to " : " from "); pf_buffer_string (filter, dirnames[dir_match]); } pf_buffer_string (filter, ".\n"); /* Handle any associated resource. */ vt_key[0].string = "NPCs"; vt_key[1].integer = npc; vt_key[2].string = "Res"; vt_key[3].integer = is_exit ? 3 : 2; res_handle_resource (game, "sisi", vt_key); } /* * npc_tick_npc_walk() * * Helper for npc_tick_npc(). Returns TRUE if the NPC moved. */ static sc_bool npc_tick_npc_walk (sc_gameref_t game, sc_int npc, sc_int walk) { const sc_prop_setref_t bundle = gs_get_bundle (game); sc_vartype_t vt_key[6]; sc_int roomgroups, movetimes, walkstep, start, dest, destnum; if (npc_trace) { sc_trace ("NPC:__ ticking NPC %ld, walk %ld: step %ld\n", npc, walk, gs_npc_walkstep (game, npc, walk)); } /* Count roomgroups for later use. */ vt_key[0].string = "RoomGroups"; roomgroups = prop_get_child_count (bundle, "I<-s", vt_key); /* Get move times array length. */ vt_key[0].string = "NPCs"; vt_key[1].integer = npc; vt_key[2].string = "Walks"; vt_key[3].integer = walk; vt_key[4].string = "MoveTimes"; movetimes = prop_get_child_count (bundle, "I<-sisis", vt_key); /* Find a step to match the movetime. */ for (walkstep = 0; walkstep < movetimes - 1; walkstep++) { sc_int movetime; vt_key[5].integer = walkstep + 1; movetime = prop_get_integer (bundle, "I<-sisisi", vt_key); if (gs_npc_walkstep (game, npc, walk) > movetime) break; } /* Sort out a destination. */ dest = start = gs_npc_location (game, npc) - 1; vt_key[4].string = "Rooms"; vt_key[5].integer = walkstep; destnum = prop_get_integer (bundle, "I<-sisisi", vt_key); if (destnum == 0) /* Hidden. */ dest = -1; else if (destnum == 1) /* Follow player. */ dest = gs_playerroom (game); else if (destnum < gs_room_count (game) + 2) dest = destnum - 2; /* To room. */ else if (destnum < gs_room_count (game) + 2 + roomgroups) { sc_int initial; /* For roomgroup walks, move only if walksteps has just refreshed. */ vt_key[4].string = "MoveTimes"; vt_key[5].integer = 0; initial = prop_get_integer (bundle, "I<-sisisi", vt_key); if (gs_npc_walkstep (game, npc, walk) == initial) { sc_int group; group = destnum - 2 - gs_room_count (game); dest = npc_random_adjacent_roomgroup_member (game, start, group); if (dest == -1) dest = lib_random_roomgroup_member (game, group); } } /* See if the NPC actually moved. */ if (start != dest) { if (npc_trace) sc_trace ("NPC:__ walking NPC %ld moved to %ld\n", npc, dest); /* Move NPC to destination. */ gs_set_npc_location (game, npc, dest + 1); /* Announce NPC movements, and handle meeting characters and objects. */ if (start == gs_playerroom (game)) npc_announce (game, npc, start, TRUE, dest); else if (dest == gs_playerroom (game)) npc_announce (game, npc, dest, FALSE, start); /* Handle meeting characters and objects, done every step. */ npc_meet_any_npcs (game, npc, walk); npc_meet_any_objects (game, npc, walk); } if (npc_trace) { sc_trace ("NPC:__ after ticking NPC %ld, walk %ld: step %ld\n", npc, walk, gs_npc_walkstep (game, npc, walk)); } /* Return TRUE if the NPC moved. */ return start != dest; } /* * npc_find_active_walk() * npc_find_any_walk() * * Locate and return the current active walk, or the last finished walk, * for the NPC, or -1 if none. */ static sc_int npc_find_active_walk (sc_gameref_t game, sc_int npc) { const sc_prop_setref_t bundle = gs_get_bundle (game); sc_vartype_t vt_key[6]; sc_int walk; /* Set up invariant parts of the properties key. */ vt_key[0].string = "NPCs"; vt_key[1].integer = npc; vt_key[2].string = "Walks"; /* Process each walk, starting at the last and working backwards. */ for (walk = gs_npc_walkstep_count (game, npc) - 1; walk >= 0; walk--) { sc_int starttask, stoppingtask; /* Ignore finished walks. */ if (gs_npc_walkstep (game, npc, walk) <= 0) continue; /* Get start task. */ vt_key[3].integer = walk; vt_key[4].string = "StartTask"; starttask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; /* * Check that the starter is still complete, and if not, stop walk. * Then keep on looking for an active walk. */ if (starttask >= 0 && !gs_task_done (game, starttask)) { if (npc_trace) sc_trace ("NPC:_ stopping NPC %ld walk, start task undone\n", npc); gs_set_npc_walkstep (game, npc, walk, -1); continue; } /* Get stopping task. */ vt_key[4].string = "StoppingTask"; stoppingtask = prop_get_integer (bundle, "I<-sisis", vt_key) - 1; /* * If any stopping task has completed, ignore this walk but don't * actually finish it; more like an event pauser, then. * * TODO Is this right? */ if (stoppingtask >= 0 && gs_task_done (game, stoppingtask)) { if (npc_trace) sc_trace ("NPC:_ ignoring NPC %ld walk, stop task done\n", npc); continue; } /* This walk is active and not stopped. */ break; } /* Return the walk, or -1 if none (loop found nothing). */ return walk; } static sc_int npc_find_any_walk (sc_gameref_t game, sc_int npc) { sc_int walk; /* Process each walk, starting at the last and working backwards. */ for (walk = gs_npc_walkstep_count (game, npc) - 1; walk >= 0; walk--) { /* * A walk with a positive step is active, -1 finished, and we'll return * either of these. Zero means not yet started, and we ignore these. */ if (gs_npc_walkstep (game, npc, walk) > 0 || gs_npc_walkstep (game, npc, walk) == -1) break; } /* Return the walk, or -1 if none (loop found nothing). */ return walk; } /* * npc_tick_npc() * * Move an NPC one step along current walk. Returns TRUE if the NPC moved, * FALSE otherwise. */ static sc_bool npc_tick_npc (sc_gameref_t game, sc_int npc) { const sc_prop_setref_t bundle = gs_get_bundle (game); sc_int walk; sc_bool has_moved; if (npc_trace) sc_trace ("NPC: ticking NPC %ld\n", npc); /* Find active walk, and if any found, make a step along it. */ walk = npc_find_active_walk (game, npc); if (walk != -1) { /* Decrement steps; if this a walk end, loop if called for. */ gs_decrement_npc_walkstep (game, npc, walk); if (gs_npc_walkstep (game, npc, walk) == 0) { sc_vartype_t vt_key[6]; sc_bool is_loop; /* If walk is a loop, restart it, otherwise complete it. */ vt_key[0].string = "NPCs"; vt_key[1].integer = npc; vt_key[2].string = "Walks"; vt_key[3].integer = walk; vt_key[4].string = "Loop"; is_loop = prop_get_boolean (bundle, "B<-sisis", vt_key); if (is_loop) { vt_key[4].string = "MoveTimes"; vt_key[5].integer = 0; gs_set_npc_walkstep (game, npc, walk, prop_get_integer (bundle, "I<-sisisi", vt_key)); } else gs_set_npc_walkstep (game, npc, walk, -1); } /* Make a move along this walk. */ has_moved = npc_tick_npc_walk (game, npc, walk); } else has_moved = FALSE; if (npc_trace) sc_trace ("NPC: after ticking NPC %ld\n", npc); return has_moved; } /* * npc_tick_npcs() * * Move each NPC one step along current walk. */ void npc_tick_npcs (sc_gameref_t game) { sc_int npc; /* * Tick each NPC, and note those that moved. For performance and conven- * ience, or out of laziness, use game references to note NPCs that move. */ for (npc = 0; npc < gs_npc_count (game); npc++) game->npc_references[npc] = npc_tick_npc (game, npc); /* * Now look for any NPCs that didn't move, but have walks and are in the * same room as ones that did. For these cases, we need an extra NPC * meeting message attempt. */ for (npc = 0; npc < gs_npc_count (game); npc++) { sc_int other_npc; /* Ignore this NPC if it moved, or if it didn't and has no walk. */ if (game->npc_references[npc] || npc_find_any_walk (game, npc) == -1) continue; /* Now find other NPCs that _did_ move this turn. */ for (other_npc = 0; other_npc < gs_npc_count (game); other_npc++) { if (game->npc_references[other_npc]) { /* * If that NPC has an active walk, try a meeting between the * unmoved NPC and this NPC as if the unmoved NPC had moved to * the same room as the moved one. */ if (npc_find_active_walk (game, other_npc) != -1) { sc_int walk; walk = npc_find_any_walk (game, npc); npc_meet_specific_npc (game, npc, walk, other_npc); } } } } } /* * npc_debug_trace() * * Set NPC tracing on/off. */ void npc_debug_trace (sc_bool flag) { npc_trace = flag; }