/**
* @main
* This is the hospital inheritable for Counterweight Continent, it handles the
* creation of NPC's (and other nicities.)
* <p>
* The CWC hospital object works differently to many hospitals used in
* other domains, because it allows certain methods to be redefined and
* the hospital "customized". This allows different NPC zones and
* configurations, and even different logging options all based on the
* same code.
* <p>
* @author Taffyd
* @started December 3, 1998.
* @index cwc_hospital
*/
#include <armoury.h>
#include <hospital.h>
#include "path.h"
#include HOSPITAL_INC
/* Comment this out if you want to disable hospital generation
of NPC's through the new data compiler system. */
#define CREATE_NPCS 1
#define REGEN_TIME 1 * 60 * 60
#define DEFAULT_LOG_DIRECTORY "/d/cwc/log"
#define DEFAULT_POPULATION 3
#define MINIMUM_DEATH_REGEN 3
inherit "/std/room";
class regenerated_npc {
string type;
string load_position;
}
protected void load_file();
protected void save_file();
protected void set_save_file(string new_save);
protected void restore_default_npcs();
protected void set_max_living(int new_living);
protected void set_hospital_type(string new_type);
protected object *make_monster(string type);
public object *get_monster(string type);
public void hospital_log_file(string file, string format, mixed *args ...);
public int get_item(object destination, string *items);
public string query_save_file();
public int query_max_living();
public int *query_npcs();
public mapping query_hospital_npcs();
public void add_npc_type(string type, string *data);
public void regen_after_death( object dead_npc );
private nosave string _save_file, _hospital_type;
private nosave int _max_living;
private nosave mixed *_regenerated_npcs;
private nosave int _hospital_call_id;
private nosave mapping _log_file_info;
private nosave string _hospital_log_directory;
public mapping _unique_npcs;
public mapping _hospital_npcs;
public int _last_npc_check, _zone_npcs;
private nosave mapping _npc_groups;
private nosave mapping _npc_info;
/** @ignore yes */
void create() {
seteuid(master()->creator_file(file_name(this_object())));
do_setup++;
::create();
do_setup--;
if (_save_file) {
load_file();
if (!_hospital_npcs) {
restore_default_npcs();
}
if (!_unique_npcs) {
_unique_npcs = ([ ]);
}
save_file();
}
if (!_npc_groups)
_npc_groups = ([ ]);
if (!_npc_groups)
_npc_info = ([ ]);
_regenerated_npcs = ({ });
_log_file_info = ([ ]);
if (!do_setup) {
set_short("Counterweight Continent Hospital");
set_long("All around you NPC's wait in neat little lines, "
"ready to be sent on a massive journey that will ultimately "
"lead to their deaths. Some of them stand by a small window "
"in the far wall, looking out at freedom.\n");
add_property("determinate", "the ");
set_light(60);
}
} /* create() */
/**
* This function provides an interface to the armoury, selecting
* an item at random from the 'items' array and attempting to
* clone it.
* <p>
* Any items of jewellery, clothing, weapons, armour or scabbards
* that are available in the armoury can be accessed using this
* function.
* <p>
* @param destination where to place the item that is fetched
* from the armoury.
* @param items an array of items, of which one will be selected,
* cloned, and then moved to destination.
* <p>
* @see /obj/handlers/armoury
* @return 1 if the item was successfully created, 0 if it was not.
*/
public int get_item(object destination, string *items) {
int i;
object item;
if (!objectp(destination)) {
return 0;
}
while (!item) {
i = random(sizeof(items));
item = ARMOURY->request_item(items[i], 80 + random(21));
if (!objectp(item)) {
items = items[0..i-1] + items[i+1..];
continue;
}
if (!sizeof(items)) {
hospital_log_file("BROKEN_ITEMS", "Unable to select any items "
"for %s in the CWC hospital.\n", previous_object()->short());
break;
}
}
if (objectp(item)) {
item->move(destination);
return 1;
}
return 0;
} /* get_item() */
/** @ignore yes */
protected void load_file() {
if (file_size(_save_file + ".o") > -1) {
unguarded((: restore_object, _save_file :));
}
} /* load_file() */
/** @ignore yes */
protected void save_file() {
unguarded((: save_object, _save_file :));
} /* save_file() */
/**
* This method is used to set the save file for the hospital. This
* file is where all of the hospitals NPC data is stored while it is
* not active and in between reboots.
* <p>
* If this file does not exist when the hospital is loaded, then the
* variables will be initialised to their defaults and
* <b>restore_default_npcs()</b> called.
* <p>
* @param new_save the file name to save the hospital data in
*/
protected void set_save_file(string new_save) {
_save_file = new_save;
} /* set_save_file() */
/**
* This returns the location where hospital data will be stored.
*/
public string query_save_file() {
return _save_file;
} /* query_save_file() */
/**
* This method returns whether or not it is time to regenerate a
* unique NPC. The regeneration time is controlled by the REGEN_TIME
* #define.
* <p>
* @param who this is the name or object reference of the NPC which is trying
* to be cloned. Now that I think about it, having an object reference
* is pretty useless but I'm sure there was a very good reason for it.
* <p>
* @return 1 if it is time to make the unique NPC, or 0 if it is not.
*/
public int make_unique(mixed who) {
if (!_unique_npcs)
return 0;
if (objectp(who))
who = who->query_name();
if ( _unique_npcs[who] > time() )
return 0;
_unique_npcs[who] = time() + REGEN_TIME;
save_file();
} /* make_unique() */
/**
* This method is used to query how long until it is time for the
* NPC to regenerate.
* <p>
* @param who the name or object reference of the NPC to test.
* @return the time at which this NPC will regenerate.
*/
public int query_regen_time(mixed who) {
if (objectp(who))
who = who->query_name();
if (!_unique_npcs)
return 0;
return _unique_npcs[who];
} /* query_regen_time() */
/**
* This sets the maximum number of NPC's to be cloned by the
* hospital. If this is not set then no NPC's will ever be
* made by the hospital, and if it is too high then the streets
* will be overflowing with NPC's.
* <p>
* @param new_living the new maximum number of NPC's to clone
* @see ok_to_clone()
*/
protected void set_max_living(int new_living) {
_max_living = new_living;
} /* set_max_living() */
/**
* @return the maximum number of NPC's that can be cloned by
* the hospital.
* @see set_max_living()
*/
public int query_max_living() {
return _max_living;
} /* query_max_living() */
/**
* This method is used to restore the hospital NPC's mapping if the
* save file cannot be restored. This should be masked in localised
* hospitals so that their unique region settings can be automatically
* setup.
* <p>
* If this function is overloaded, then ::restore_default_npcs() should
* not be called, otherwise they will be cleared.
*/
protected void restore_default_npcs() {
_hospital_npcs = ([ ]);
} /* restore_default_npcs() */
/**
* This method is used to determine whether or not it is ok to create
* a new NPC. It calculates the number of NPC's that exist in the
* area controlled by the hospital, and checks this against the maximum
* number of living objects allowed in this area.
* <p>
* @see set_max_living()
* @see /d/cwc/city_inherit
*/
public int ok_to_clone(string where) {
if (time() > _last_npc_check + 300) {
_last_npc_check = time();
_zone_npcs = sizeof(filter(named_livings(), (: environment($1) &&
base_name(environment($1))[0..(sizeof(CWC) + sizeof($(where)) - 1)] ==
(CWC + $(where)) :)));
}
return _zone_npcs < _max_living;
} /* ok_to_clone() */
/**
* This method is used to return statistical information about the hospital.
* @return an array containing the number of NPC's in the zone, and the
* time at which this number was last calculated.
*/
public int *query_npcs() {
return ({ _zone_npcs, _last_npc_check });
} /* query_npcs() */
/**
* This method can be used to add a new type of NPC to the hospital, or
* to extend the NPC's in an existing zone.
* <p>
* @param type the zone in which the NPC's will be created
* @param data an array of NPC's to create in the zone.
*/
public void add_npc_type(string type, string *data) {
if (undefinedp(_hospital_npcs[type])) {
_hospital_npcs[type] = copy(data);
}
else {
if (arrayp(_hospital_npcs[type])) {
_hospital_npcs[type] += data;
}
}
save_file();
} /* add_npc_type() */
/**
* This method returns all of the hospital zones and NPC data. It
* is fairly useless.
* <p>
* @return a mapping containing hospital zone and NPC data information.
*/
public mapping query_hospital_npcs() {
return copy(_hospital_npcs);
} /* query_hospital_npcs() */
/**
* This method is used to create an NPC of a specific name or of a
* hospital type.
* <p>
* @param type the type of NPC to create. This can either be a specific
* name (such as 'sailor') or a zone (such as 'docks'). Valid zones
* must be defined in the hospital for this to work correctly.
* @return the NPC's made by the hospital
*/
public object *get_monster(string type) {
object *monsters, monster;
if (_hospital_npcs[type]) {
tell_creator("taffyd", "Attempting to create a %s NPC.\n", type);
monsters = make_monster(_hospital_npcs[type][random(sizeof(_hospital_npcs[type]))]);
tell_creator("taffyd", "Monsters are now %O.\n", monsters);
}
else {
monsters = make_monster(type);
}
// Now do the stuff that is common for all npcs
foreach (monster in monsters) {
monster->add_property("monster_type", type);
monster->add_effect("/std/effects/npc/i_died",
(: regen_after_death :) );
}
return monsters;
} /* get_monster() */
/**
* This method is used to clone a monster. It is called by get_monster() and
* should be redefined in your localised hospital zone to add support for
* new monsters.
* <p>
* Note that if you to redefine this function, you do not need to call the
* precursor. Otherwise your hospital will start producing 'hospital
* accidents'. (The default NPC for an unconfigured NPC).
* <p>
* @param type the type of NPC to generate.
* @return the object reference of the NPC or NPC's made by the hospital.
*/
protected object *make_monster(string type) {
object monster;
monster = clone_object("/obj/monster");
monster->set_name("failure");
monster->set_short("hospital accident");
monster->set_race("human");
monster->set_guild("warrior");
monster->set_level(1);
return ({ monster });
} /* make_monster() */
/**
* This method is called by the npc.death effect after an NPC that is handled
* by the hospital has died. It tells the room where it was generated to
* make a new NPC and to all sorts of funky things.
* <p>
* This is not fully implemented yet.
*/
public void regen_after_death( object dead_npc ) {
object *monsters, destination;
class regenerated_npc new_npc;
int i, max;
if ( !dead_npc )
return;
_regenerated_npcs += ({ new( class regenerated_npc,
type : dead_npc->query_property("monster_type"),
load_position : previous_object()->query_property("start location") ) });
if ( !ok_to_clone(_hospital_type) || sizeof(_regenerated_npcs) < MINIMUM_DEATH_REGEN )
return;
new_npc = _regenerated_npcs[0];
if ( !classp( new_npc ) )
return;
_regenerated_npcs = _regenerated_npcs[1..];
if (!new_npc->load_position || !new_npc->type );
return;
max = random(5);
for ( i = 0; i < max; i++ ){
destination = find_object( new_npc->load_position );
if ( !objectp( destination ) )
return;
monsters = get_monster(new_npc->type);
destination->replace_monster(previous_object(), monsters);
call_out((: $1->announce_entry($2) :), 8 + random(6), destination,
monsters);
}
} /* regen_after_death() */
/**
* This method is used to set the hospital type. This is actually used to
* set the geographical region that the hospital controls.
* <p>
* @example
* // Only NPC's in /d/cwc/Bes_Pelargic will be managed by this hospital.
* set_hospital_type("Bes_Pelargic");
* @example
* // Only NPC's in the Aarrgh forest will be managed by this hospital.
* set_hospital_type("Forests/Aarrgh");
* @param new_type the type and geographical region managed by this hospital
*/
protected void set_hospital_type(string new_type) {
_hospital_type = new_type;
} /* set_hospital_type() */
/**
* This method provides log_file() functionality but for /d/cwc/log
* instead. You should use this method to record errors and whatever
* that are domain specific instead of using log_file().
* @example
* hospital_log_file("BAD_HOSPITAL_NPC", "%s: Bad NPC hospital data, %O.\n",
* ctime(time()), _regenerated_npcs);
* // This would log to /d/cwc/log/BAD_HOSPITAL_NPC.
* @param file the file name to log to. This will have /d/cwc/log/ inserted
* before it.
* @param format the text to log, can contain sprintf() format specifiers.
* @param args the arguments for any sprintf format thingies
*/
public void hospital_log_file(string file, string format, mixed *args ...) {
string filename;
filename = _hospital_log_directory;
/* We are restricted to logging in our domain, let's be neat about it. */
if (file[0] == '/') {
file = file[ strsrch( file, "/", -1 ) + 1 .. ];
}
/* Link to sefun */
log_file( _hospital_log_directory + "/" + file, format, args ...);
} /* hospital_log_file() */
/**
* This method sets the directory in to which hospital_log_file will
* log by default. If the directory does not exist, then it is set to
* DEFAULT_LOG_DIRECTORY.
* @param new_dir the directory to log to.
* @example
* set_log_directory( "/d/cwc/Bes_Pelargic/log" );
* // log directory is now /d/cwc/Bes_Pelargic/log
* @example
* // (cre) MacChirton: ithinkmykeyboardisbroken
* set_log_directory( "/d/cWc/BeS_pelarGic/lrg" );
* // Displays: "/d/cwc/hospital/bes_pelargic: Invalid log
* // directory, defaulting to /d/cwc/log"
*/
protected void set_log_directory(string new_dir) {
if (file_size(new_dir) != -2) {
tell_creator( previous_object( 1 ), "%s: Invalid "
"log directory -- defaulting to %s.\n",
file_name( this_object() ), DEFAULT_LOG_DIRECTORY );
new_dir = DEFAULT_LOG_DIRECTORY;
}
_hospital_log_directory = new_dir;
} /* set_log_directory() */
protected void set_npc_groups(mapping x) {
_npc_groups = x;
} /* set_npc_groups() */
protected void set_npc_info(mapping x) {
_npc_info = x;
} /* set_npc_info() */
public object data_get_npc(string npc_name) {
class npc_info info;
object npc;
function func;
if ( undefinedp(_npc_info[npc_name]) )
return 0;
info = _npc_info[npc_name];
if (stringp(info->base))
npc = clone_object(info->base);
else
return 0;
if (arrayp(info->functions)) {
foreach (func in info->functions) {
evaluate(func, npc, previous_object());
}
}
return npc;
} /* data_get_npc() */
object *data_get_group(string group_name) {
class npc_group group;
function func;
int population, quantity, i;
object *npcs, npc;
if ( undefinedp(_npc_groups[group_name]) )
return 0;
group = _npc_groups[group_name];
/* Determine the population from the class, if this is undefined
* then we make set it to DEFAULT_POPULATION.
*/
if (functionp(group->population))
population = evaluate(group->population);
else {
population = DEFAULT_POPULATION;
}
if (!group->storage) {
group->storage = ({ });
}
else {
group->storage = filter(group->storage, (: objectp($1) :));
/*
* If we have more NPC's than the population, then stop right now.
*/
if (sizeof(group->storage) >= population)
return 0;
}
if (functionp(group->quantity)) {
quantity = evaluate(group->quantity);
}
else {
quantity = 1;
}
if (stringp(group->npc)) {
npcs = allocate(quantity);
for (i = 0; i < quantity; i++) {
npcs[i] = data_get_npc(group->npc);
}
npcs = filter(npcs, (: objectp($1) :));
group->storage += npcs;
}
foreach (npc in npcs) {
if (arrayp(group->functions)) {
foreach (func in group->functions) {
evaluate(func, npc, previous_object());
}
}
}
return npcs;
} /* data_get_group() */
/*
* I stole this function straight out of Jeremy's sur hospital.
* It does weird and wonderful things.
* <p>
* This is a utility function for the rooms. It's easier on the
* programmer to make the weights non-cumulative, but it's inefficient
* for the code. The room can call this in setup() (or in a callout
* if time is critical) to make the npc array cumulative. Note that
* the original array isn't modified. This allows the original array to be
* dynamically changed and recalculated.
* <p>
* @param a an array of NPC's to generate, in the format
* ({ ({ "name", chance }), ... })
*/
mixed accumulate_npc_array( mixed a ) {
int cum, i;
mixed choices;
choices = copy(a);
cum = 0;
for (i = 0; i < sizeof(choices); i++) {
cum += choices[i][0];
choices[i][0] = cum;
}
return choices;
} /* accumulate_npc_array() */
/**
* I stole this one as well.
* <p>
* This method randomly selects an NPC or a group of NPC's from
* the list of defined NPC's for the room/region.
* @param choices if this is passed, then the array is used to
* select NPC's from. This array must be a cumulative array. (see example)
* @return an
* Returns a group of NPCs randomly selected. "choices" is an array
* of 2-element arrays giving the cumulative chance of the NPC group
* and the group name, e.g.:
* ({ ({ 5, "wolves" }), ({ 10, "trolls" }), ({ 20, "bandits" }) })
* This would clone wolves 25% of the time, trolls 25% of the time, and
* bandits 50% of the time. If this is too confusing, see
* accumulate_npc_array().
*/
varargs mixed get_random_npcs(mixed *choices) {
int i;
int chance;
if (!choices)
choices = previous_object()->query_npcs();
if (!arrayp(choices) || !sizeof(choices))
return 0;
chance = random(choices[<1][0]);
for (i = 0; i < sizeof( choices ); i++) {
if ( choices[ i ][ 0 ] > chance ) {
return data_get_group( choices[ i ][ 1 ]);
}
}
return 0;
} /* get_random_npcs() */