/*
*
*$Id: locksmith.c,v 1.8 2002/10/11 10:32:25 wirble Exp $
*
*/
/**
* -- Inheritable for playerhousing locksmiths --
* An NPC using this inherit is able to unlock the doors to playerhouses and
* commercial properties when prompted to do so by the rightful owner.
*
* -- How such an NPC works from a player's point of view --
* - A player can "hire" the npc if he wants a door of his house opened.
* - He then has to "pay" (*) the amount the npc demands (if that player killed
* the npc before, the base price is doubled). He has 120 seconds to do so.
* - After that happens, the npc will follow the player until the player
* "asks"s him to opens an exit door.
* - Then player is prompted to enter [y]es [n]o". If the player confirms and
* the handler doesn't return this as well, the npc complains and walks off
* (keeping the money, of course). Otherwise it unlocks the door and then
* leaves.
*
* (*) Giving the money to the npc does not work, it'll return it.
*
* The same procedure applies if the player wants all doors opened.
*
* All attempts to hire a locksmith with the result of that action are logged
* in the file defined in LOG
*
* In case of problems you can always try to:
* 1) look at the log to see if the player in question hired and payed the npc
* 2) call query_client() on the npc
* 3) call free_for_hire() on the npc
* 4) dest and re-load the npc
*
*
* @example
* inherit "/std/npc/types/locksmith";
*
* void setup() {
* // basic npc-setup
* set_savefile(SAVE + "glodson_killers");
* set_price(20000); // 50 $AM
* set_currency_region("Ankh-Morpork");
* set_domain("/d/am/");
* }
*
* @author Wirble
* @started Nov. 2001
*/
#include <armoury.h>
#include <money.h>
#define LOG "/log/LOCKSMITH"
#define LOGGING 1
inherit "/obj/monster";
//private functions
private void save_killers();
private void load_killers();
private void remove_killer(string name);
private void clear_false_client();
private void payment_timeout();
private int check_owner();
private void unlock(object door);
private string get_price();
private int calc_price();
private void generic_responses();
private void go_back();
private void unlock_all( string path );
private void delay_opening( string env );
void finish_opening( string path, string env );
// public functions
public void confirmation(string response, object player);
public void free_for_hire();
public string query_client();
public void move_recording();
public string* query_killers();
public string* query_responses();
public string query_domain();
public string query_last_room();
//setup functions
protected int set_savefile(string file);
protected int set_price(int price);
protected int set_currency_region(string region);
protected int set_responses(string *responses);
protected int set_domain(string domain);
// command functions
public int do_hire( int all );
public int do_pay();
public int do_ask(string exit);
// Variables
private string _client;
private string* _killers;
private int _price;
private int _paid;
private int _all_doors;
private object _door;
private string _savefile;
private string _region;
private string* _responses;
private string _domain;
private string _last_room;
// -------------------- basic functions --------------------
/** @ignore */
void create() {
generic_responses();
do_setup++;
::create();
do_setup--;
if (!do_setup)
this_object()->setup();
add_enter_commands("#move_recording");
}
/** @ignore */
void init() {
::init();
if(interactive(this_player())) {
this_player()->add_command("hire", this_object(), "<direct>",
(: do_hire( 0 ) :) );
this_player()->add_command("hire", this_object(),
"<direct> to unlock an entire house",
(: do_hire( 1 ) :) );
this_player()->add_command("pay", this_object(), "<direct>");
this_player()->add_command("ask", this_object(),
"<direct> to {open|unlock} [the] "
"<word'exit'> door",
(: do_ask($4[2]) :));
this_player()->add_command("confirm", this_object(), "");
}
}
/** @ignore */
void reset() {
clear_false_client();
}
// -------------------- public functions --------------------
/**
* Use this function if something went wrong and the NPC does not react to the
* client/new clients anymore.
*/
public void free_for_hire() {
_client = 0;
_paid = 0;
_door = 0;
_all_doors = 0;
set_move_after(60, 60);
}
/**
* This queries the client's name.
* @return the name of the client or 0 for no client
*/
public string query_client() {
if(!_client)
return 0;
return _client;
}
/**
* This function gets called after every move of the NPC and records it for a
* potential return. It should not be called manually.
*/
public void move_recording() {
object room = environment(this_object());
if(!room) {
if(query_name() != "object")
call_out("dest_me", 5);
return;
}
if(_client) {
// check if the NPC is still in the designated domain
if(_domain && base_name(room)[0..5] == _domain) {
// record the room if it's not inside housing
if(!room->query_owner())
_last_room = base_name(room);
return;
}
do_command("sayto " + _client + " " + _responses[11]);
go_back();
}
}
/**
* This queries the killers of the NPC.
* @return an array listing the killers' names
*/
public string* query_killers() {
return _killers;
}
/**
* This queries the NPC's domain.
* @return the first 6 chars of the domain.
*/
public string query_domain() {
return _domain;
}
/**
* This queries the last valid room the NPC was in.
* @return the basename of the last room
*/
public string query_last_room() {
return _last_room;
}
/**
* This function is used to evaluate the client's response to the "is this the
* door"-question.
* @param response the response the player entered
* @param player the player doing the input
*/
public void confirmation(string response, object player) {
if(!response || !player)
return;
switch(lower_case(response)) {
case "yes":
case "y":
break;
case "no":
case "n":
do_command("nod");
do_command("follow " + player->query_name());
_door = 0;
return;
default:
do_command("sayto " + player->query_name() + " " + _responses[0]);
return;
}
if(!_client || player->query_name() != _client) {
do_command("sayto " + player->query_name() + " " + _responses[1]);
return;
}
// If the destination doesn't belong to the client, abort and move away
if(!check_owner()) {
do_command("sayto " + player->query_name() + " " + _responses[2]);
#ifdef LOGGING
log_file(LOG, "%s: %s tried to get %s to unlock door in %s leading "
"to %s.\n", ctime(time()), _client, query_short(),
base_name(environment(this_object())),_door->query_dest());
#endif
} else if( _all_doors == 0 ) {
queue_command("emote eyes the lock on the door.", 1);
queue_command("emote fiddles around on the lock with some wires.", 5);
queue_command("emote seems satisfied.", 5);
unlock(_door);
queue_command("sayto " + player->query_name() + " " + _responses[3], 2);
#ifdef LOGGING
log_file(LOG,
"%s: %s successfully got %s to unlock door in %s leading %s.\n",
ctime(time()), _client, query_short(),
base_name(environment(this_object())), _door->query_dest());
#endif
} else {
queue_command("emote eyes the lock on the door.", 1);
queue_command("emote fiddles around on the lock with some wires.", 5);
queue_command("'Stand back and wait for me.", 5 );
queue_command("emote opens the door and disappears into the house, locking "
"the door after " + query_objective() + ".", 2 );
delay_opening( base_name( environment( this_object() ) ) );
return;
}
free_for_hire();
go_back();
}
/**
* This queries the responses of the NPC.
* @return an array of response-strings
*/
public string* query_responses() {
return _responses;
}
// -------------------- setup functions --------------------
/**
* Use this function to set the file where the killers of the NPC are stored,
* if you want to charge the killers the double price for opening their house.
* @param file the name of the file to save to
* @return 1 on success, 0 on failure
*/
protected int set_savefile(string file) {
if(!file)
return 0;
_savefile = file;
load_killers();
return 1;
}
/**
* This function sets the base price for unlocking a door. If the savefile is
* set, a killer will be charged the double price.
* @param price the base-price in money-units
* @return 1 on success, 0 on failure
*/
protected int set_price(int price) {
if(!price)
return 0;
_price = price;
return 1;
}
/**
* This function sets the NPCs region for determining the currency.
* @param region the region to use
* @return 1 on success, 0 on failure
*/
protected int set_currency_region(string region) {
if(!region)
return 0;
_region = region;
return 1;
}
/**
* This sets the domain the NPC is restricted to.
* @param domain the first 6 letters of the domain-path, ie "/d/am/"
* @return 1 on success, 0 on failure
*/
protected int set_domain(string domain) {
if(sizeof(domain) != 6)
return 0;
_domain = domain;
}
/**
* This function sets the NPC's responses for the various commands. If not
* used generic answers will be used. Mind that you cannot enter only a few
* responses. It's all or nothing. The array should be like this:
* 1) response to failed confirmation
* 2) response for not hired by that player
* 3) response for trying to enter another player's house
* 4) response for successful opening of the door
* 5) response for already hired by other player
* 6) response for not paid yet
* 7) response for attempt to hire
* 8) response for already paid
* 9) response for not enough money to pay
* 10) response for cannot find specified exit
* 11) response for successful payment (please include 'ask' hint)
* 12) response for not leaving the domain
* 13) response for already hired by that player
* @param responses an array with the responses, as explained above
* @return 1 on success, 0 on failure
*/
protected int set_responses(string* responses) {
if(sizeof(responses) != 13)
return 0;
_responses = responses;
}
// -------------------- command functions --------------------
/** @ignore */
public int do_hire( int all ) {
if(!this_player())
return 0;
// If the prospective client is invis to the NPC, abort
if(!this_player()->query_visible(this_object())) {
this_player()->add_failed_mess(this_object(),
"$C$" + this_object()->the_short() +
" cannot even see you.\n", ({ }));
return 0;
}
// If the NPC is already hired by someone else, abort
if(_client && this_player()->query_name() != _client) {
do_command("sayto " + this_player()->query_name() + " " + _responses[4]);
this_player()->add_failed_mess(this_object(), "", ({ }));
return 0;
}
// If he is already hired by the player asking, abort
if(_client && this_player()->query_name() == _client) {
if(_paid) {
if(member_array(this_object(), this_player()->query_followers())
== -1) {
do_command("nod " + this_player()->query_name());
do_command("follow " + this_player()->query_name());
this_player()->add_succeeded_mess(this_object(), "", ({ }));
return 1;
}
do_command("sayto " + this_player()->query_name() + " " +
_responses[12]);
this_player()->add_failed_mess(this_object(), "", ({ }));
return 0;
}
do_command("sayto " + this_player()->query_name() + " " +
_responses[5]);
this_player()->add_failed_mess(this_object(), "", ({ }));
return 0;
}
// Otherwise accept
_client = this_player()->query_name();
_all_doors = all;
queue_command("sayto " + this_player()->query_name() + " " +
_responses[6], 1);
if(member_array(_client, _killers) != -1)
queue_command("sayto " + this_player()->query_name() +
" I remember you... for you it'll be " + get_price() +
". Up front, of course.", 2);
else
queue_command("sayto " + this_player()->query_name() + " That'll be "+
get_price() + " up front.", 2);
this_player()->add_succeeded_mess(this_object(), "", ({ }));
call_out((: payment_timeout() :), 60);
return 1;
}
/** @ignore */
public int do_pay() {
if(!this_player())
return 0;
// If the player is invis to the NPC, abort
if(!this_player()->query_visible(this_object())) {
this_player()->add_failed_mess(this_object(),
"$C$" + this_object()->the_short() +
" cannot even see you.\n", ({ }));
return 0;
}
// Only accept payment from the client
if(!_client || this_player()->query_name() != _client) {
do_command("whisper " + this_player()->query_name() + " I appreciate "
"that you want to give away your cash, but don't you think "
"should 'hire' me first?");
this_player()->add_failed_mess(this_object(), "", ({ }));
return 0;
}
// If he has already been paid, abort
if(_paid) {
do_command("sayto " + this_player()->query_name() + " " + _responses[7]);
this_player()->add_failed_mess(this_object(), "", ({ }));
return 0;
}
// In case the client can't pay, abort
if(this_player()->query_value_in(_region) < _price) {
do_command("sayto " + this_player()->query_name() + " " +
_responses[8]);
free_for_hire();
this_player()->add_failed_mess(this_object(), "", ({ }));
return 0;
}
this_player()->pay_money(MONEY_HAND->create_money_array(calc_price(),
_region), _region);
_paid = 1;
set_move_after(1200, 0);
do_command("emote puts the money into a pocket.");
do_command("whisper " + this_player()->query_name() + " " + _responses[10]);
init_command("follow " + this_player()->query_name(), 1);
this_player()->add_succeeded_mess(this_object(), "$N pay$s $D.\n", ({ }));
#ifdef LOGGING
log_file(LOG, "%s: %s hired %s for %s" +
( (_all_doors == 1)?" (all doors)":"" ) + "\n",
ctime(time()), _client, query_short(), get_price() );
#endif
remove_killer(_client);
return 1;
}
/** @ignore */
public int do_ask(string exit) {
object room, door;
string dest;
room = environment(this_object());
if(!this_player() || !exit || !room)
return 0;
// If the player is invis to the NPC, abort
if(!this_player()->query_visible(this_object())) {
this_player()->add_failed_mess(this_object(),
"$C$" + this_object()->the_short() +
" cannot even see you.\n", ({ }));
return 0;
}
// If the player asking is not the client, abort
if(!_client || this_player()->query_name() != _client) {
do_command("sayto " + this_player()->query_name() + " " + _responses[1]);
this_player()->add_failed_mess(this_object(),
"You are not " + the_short() +
"'s client.\n", ({ }));
return 0;
}
do_command("unfollow " + this_player()->query_name());
dest = this_player()->find_abs(exit);
if(room->query_destination(dest) == "/room/void") {
do_command("sayto " + this_player()->query_name() + " " +
_responses[9]);
do_command("follow " + this_player()->query_name());
this_player()->add_failed_mess(this_object(),
"This exit is not valid.\n", ({ }));
return 0;
}
foreach(door in room->query_hidden_objects())
if(door->query_dest() == room->query_destination(dest)) {
_door = door;
break;
}
if( _all_doors == 0 )
do_command("whisper " + this_player()->query_name() + " Can you confirm "
"that this is your place? And that you want the " + exit +
" door opened?");
else
do_command("whisper " + this_player()->query_name() + " So that's your "
"place on the other side? Can you confirm that you want the " +
exit + " door and all the others in your house opened?" );
this_player()->add_succeeded_mess(this_object(), "", ({ }));
tell_object(this_player(), "[y]es/[n]o: ");
input_to("confirmation", 0, this_player());
return 1;
}
// -------------------- internals --------------------
/** @ignore */
void event_enter(object ob, string message, object from) {
if(from && environment(ob) == this_object()) {
if(ob->query_name() == "coin") {
do_command("sayto " + file_name(from) + " You have to pay me.");
do_command("give coins to " + file_name(from));
return;
}
do_command("sayto " + file_name(from) + " I don't take donations.");
do_command("give " + file_name(ob) + " to " + file_name(from));
}
}
/** @ignore */
private void save_killers() {
string killerstring, killer;
if(!_savefile)
return;
if(!sizeof(_killers)) {
unguarded((: write_file($(_savefile), "--- no killers ---", 1) :));
return;
}
killerstring = "";
foreach(killer in _killers)
killerstring += killer + " ";
unguarded((: write_file($(_savefile), $(killerstring), 1) :));
}
/** @ignore */
private void load_killers() {
string killerstring;
if(!_savefile)
return;
if(file_size(_savefile) == -1) {
_killers = ({ });
return;
}
killerstring = unguarded((: read_file($(_savefile)) :));
if( !killerstring || killerstring == "--- no killers ---" ||
!_killers = explode(killerstring, " "))
_killers = ({ });
}
/** @ignore */
varargs object do_death(object killer, object weapon, string attack) {
if(killer && interactive(killer)) {
if(member_array(killer->query_name(), _killers) == -1) {
if (!_killers) {
_killers = ({ });
}
_killers += ({ killer->query_name() });
}
}
save_killers();
if(weapon && attack)
return ::do_death(killer, weapon, attack);
if(weapon)
return ::do_death(killer, weapon);
if(killer)
return ::do_death(killer);
return ::do_death();
}
/** @ignore */
private void remove_killer(string name) {
int index;
index = member_array(name, _killers);
if(index == -1)
return;
if(sizeof(_killers) == 1)
_killers = ({ });
if(index == 0)
_killers = _killers[ 1.. ];
if(index == sizeof(_killers))
_killers = _killers[ 0..(sizeof(_killers) -1) ];
_killers = _killers[ 0..(index - 1) ] + _killers[ (index + 1).. ];
}
/** @ignore */
private void clear_false_client() {
if(_client && find_living(_client) &&
environment(find_living(_client)) == environment(this_object()))
return;
free_for_hire();
}
/** @ignore */
private void payment_timeout() {
if(_client && !_paid)
free_for_hire();
}
/** @ignore */
private int check_owner() {
string dest = _door->query_dest();
if(load_object(dest)->query_owner() == _client)
return 1;
return 0;
}
/** @ignore */
private void unlock(object door) {
door->set_unlocked();
event( load_object( door->query_dest() ), "save");
event( environment(), "save");
}
/** @ignore */
private string get_price() {
int price;
if(member_array(_client, _killers) != -1)
price = 2 * _price;
else
price = _price;
if( _all_doors == 1 )
price*= 5;
return MONEY_HAND->money_string(MONEY_HAND->create_money_array(price,
_region));
}
/** @ignore */
private int calc_price() {
int price;
if(member_array(_client, _killers) != -1)
price = 2 * _price;
price = _price;
if( _all_doors == 1 )
price *= 5;
return price;
}
/** @ignore */
private void go_back() {
object room, door;
int i;
if(!room = environment(this_object()))
return;
if(!_last_room || base_name(room) == _last_room) {
do_command(room->query_exits()[random(sizeof(room->query_exits()))]);
return;
}
i = member_array(_last_room, room->query_dest_dir());
if(i == -1)
move(_last_room, "$N walk$s in.", "$N walk$s away.");
else {
foreach(door in room->query_hidden_objects())
if(door->query_dest() == _last_room &&
door->query_locked()) {
move(_last_room, "$N walk$s in, locking the door.",
"$N unlock$s the door and leave$s, locking it "
"again.");
return;
}
do_command(room->query_dest_dir()[i-1]);
}
}
/** @ignore */
private void generic_responses() {
_responses = ({0,0,0,0,0,0,0,0,0,0,0,0,0});
_responses[0] = "I do not understand.";
_responses[1] = "You have to hire me.";
_responses[2] = "This door does not lead to your house. I cannot help you.";
_responses[3] = "It has been a pleasure doing business with you.";
_responses[4] = "I am already hired by someone else.";
_responses[5] = "You have to 'pay' me.";
_responses[6] = "You do require my services?";
_responses[7] = "You already paid me.";
_responses[8] = "You do not have enough money.";
_responses[9] = "I cannot find that door.";
_responses[10] = "Very well. Please lead me to your house and 'ask' me to "
"open the door that troubles you.";
_responses[11] = "I won't leave my beloved home, sorry. I'll wait there "
"for a while, if you change your mind and come back.";
_responses[12] = "You already hired me.";
}
/** @ignore */
private void unlock_all( string path ) {
string* files = get_dir( path + "*.c" );
string file;
object room, ob;
if( !sizeof( files ) )
return;
foreach( file in files ) {
room = load_object( path + file );
if( room ) {
foreach( ob in room->query_hidden_objects() )
if( ob->query_dest() )
ob->set_unlocked();
event( room, "save" );
}
}
}
/** @ignore */
private void delay_opening( string env ) {
string path;
string* path_bits;
path_bits = explode( env, "/" );
path_bits[ sizeof( path_bits ) -1 ] = "";
path = "/" + implode( path_bits, "/" );
move( "/room/void" );
call_out( "finish_opening", 30, path, env );
}
/** @ignore */
void finish_opening( string path, string env ) {
unlock_all( path );
#ifdef LOGGING
log_file(LOG,
"%s: %s successfully got %s to unlock all doors in %s.\n",
ctime(time()), _client, query_short(), path );
#endif
move( env, "$N leaves the house, rubbing " + query_objective() +
" hands.\n", "" );
free_for_hire();
go_back();
}