/* -*- LPC -*- */
/*
* $Id: vessel.c,v 1.66 2003/07/23 14:32:19 terano Exp $
*/
/**
* This is a container specifically for holding liquids. The hope is
* to reduce the memory requirements by taking all of the liquid stuff
* out of /obj/container, since most of the containers are sacks and
* chests. Also, the reaction handler will replace the potion space.
* <p>
* Some additional notes:
* <ol>
* <li>As is (hopefully) documented elsewhere, the base units of
* volume for most continuous media are drops and pinches, both of
* which are roughly 1/4 of a cc. This means that water has
* 200 drops per weight unit (1g/cc). Non-continuous objects
* are assumed to be the same density as water.
* </ol>
* @@author Jeremy
*/
#define REACTION_HANDLER ("/obj/handlers/reaction")
#define TASTE_AMT 200
#include <tasks.h>
#include <move_failures.h>
#include <volumes.h>
inherit "/obj/baggage";
private int leak_rate;
private int hb_count;
private int sub_query_contents;
private int volume;
private int max_volume;
private int cont_volume;
private int is_liquid;
private nosave int* _fraction;
private nosave object* _liquids;
int drink_amount(int drinking, object player);
private int query_fighting(object player);
/**
* This method sets the leak rate of the container. The leak rate is how
* fast stuff leaks out of the container.
* @param i the new leak rate of the container
* @see query_leak_rate() */
void set_leak_rate(int i) { leak_rate = i; }
/**
* This method returns the leak rate of the container
* @see set_leak_rate()
* @return the current leak rate */
int query_leak_rate() { return leak_rate; }
/**
* This method sets the maximum volume of the container.
* @param v the new maximum volume
* @see add_volume()
* @see query_max_volume() */
void set_max_volume(int v) { max_volume = v; }
/**
* This method returns the current maxium volume associated with this
* container.
* @return the current maximum volume */
int query_max_volume() { return max_volume; }
string *leak_verb = ({ " drips slowly",
" drips",
" dribbles",
" trickles slowly",
" trickles",
" trickles rapidly",
" pours sluggishly",
" pours",
" streams",
" gushes",
" fountains"
});
string *drink_pat = ({ "[from] <direct:object>",
"<fraction> {of|from} <direct:object>"
});
string *splash_pat = ({ "[from] <direct:object> {on|to|onto} <indirect:living>",
"<fraction> {of|from} <direct:object> {on|to|onto} "
"<indirect:living>"
});
string *apply_pat = ({ "[from] <direct:object> {on|to} <indirect:living>",
"<fraction> {of|from} <direct:object> {on|to} "
"<indirect:living>"
});
string *pour_pat = ({ "<direct:object> {from|into} <indirect:object>",
"<fraction> of <direct:object> {from|into} <indirect:object>"
});
string *fill_pat = ({ "<indirect:object> <fraction> full {from|into} <direct:object>",
"<indirect:object> <fraction> up {from|into} <direct:object>",
"<indirect:object> {from|into} <direct:object>"
});
void create() {
do_setup++;
::create();
do_setup--;
if ( !do_setup ) {
this_object()->setup();
this_object()->reset();
}
add_help_file("vessel");
} /* create() */
/** @ignore yes */
void init() {
::init();
add_command("drink", drink_pat);
add_command("quaff", drink_pat);
add_command("splash", splash_pat);
add_command("rub", apply_pat);
add_command("apply", apply_pat);
add_command("pour", pour_pat);
add_command("taste", "<direct:object>");
add_command("sip", "<direct:object>");
//add_command("smell",
add_command("fill", fill_pat);
add_command("empty", "<direct:object>");
} /* init() */
/**
* This returns an adjective for how full the current object is with
* liquid. This is used in the parse command handling code.
* @return the fullness adjective
* @see query_max_volume() */
string *fullness_adjectives() {
if (!max_volume) return ({ "totally", "empty" });
switch (100 * volume / max_volume) {
case 0..4: return ({ "totally", "empty" });
case 5..13: return ({ "empty" });
case 65..94: return ({ "full" });
case 95..100: return ({ "totally", "full" });
default: return ({ });
}
} /* fullness_adjectives() */
/** @ingore yes */
mixed stats() {
return ::stats() + ({
({ "leak_rate", leak_rate }),
({ "volume", volume }),
({ "cont_volume", cont_volume }),
({ "liquid", is_liquid }),
({ "max_volume", max_volume })
});
} /* stats() */
int cmp_amount_r(object a, object b) {
return ((int)b->query_amount() - (int)a->query_amount());
} /* cmp_amount() */
int cmp_weight_r(object a, object b) {
if (a->query_weight() || b->query_weight())
return ((int)b->query_weight() - (int)a->query_weight());
return cmp_amount_r(a, b);
} /* cmp_weight() */
private void figure_out_liquids() {
_liquids = filter(all_inventory(this_object()), (: $1->query_liquid() :));
_liquids = sort_array(_liquids, "cmp_amount_r", this_object());
} /* figure_out_liquids() */
/**
* This method returns the description of the liquid inside the vessel.
* @return the current liquids description */
string query_liquid_desc() {
object *contents, *solids = ({});
int i;
string desc, *shorts;
mixed *others;
contents = all_inventory(this_object());
if( !sizeof( contents ) ) {
return (string) 0;
}
figure_out_liquids();
solids = contents - _liquids;
if (sizeof(_liquids)) {
_liquids = sort_array(_liquids, "cmp_amount_r", this_object());
others = unique_array(_liquids->a_short(),
(: "/global/events"->convert_message($1) :));
shorts = ({ });
for (i = 0; i < sizeof(others); i++) {
shorts += ({ others[i][0] });
}
desc = shorts[0];
if (sizeof(shorts) > 1) {
desc += ", mixed with ";
if (sizeof(shorts) > 4) {
desc += "other liquids";
} else {
desc += query_multiple_short(shorts[1..]);
}
}
} else {
desc = "";
}
if (!sizeof(solids)) {
return desc;
}
solids = sort_array(solids, "cmp_weight_r", this_object());
others = unique_array(solids,
(: $1->query_continuous()?
this_player()->convert_message($1->a_short()) :
$1 :) );
for (i = 0; i < sizeof(others); i++) {
if( arrayp( others ) ) {
others[i] = others[i][0];
} else {
// Er... OK, so wtf is it?
tell_creator( "gruper", "others is %O.\n", others );
}
}
if (sizeof(_liquids)) {
desc += ", with ";
}
if (sizeof(others) > 10) {
desc += "various undissolved substances";
} else {
desc += query_multiple_short(others);
}
if (sizeof(_liquids)) {
desc += " floating in it";
}
return desc;
} /* query_liquid_desc() */
/**
* This method returns the fullness description of the vessel.
* @return the fullness description of the vessel */
string query_fullness_desc() {
int full;
/* Ok, here comes a terrible kludge, but it's all I can think of
* that will fix a buglet without introducing more serious Bugs.
* It's a hack around continous objects and their volumes. - Tilly */
full = (100 * volume) / max_volume;
if( full < 1 && query_liquid_desc() ) {
full = 1;
}
switch (full) {
case 0: return "";
case 1..12: return "It is almost empty.";
case 13..37: return "It is about one-quarter full.";
case 38..62: return "It is about half full.";
case 63..87: return "It is about three-quarters full.";
case 88..95: return "It is almost full.";
case 96..100: return "It is completely full.";
default: return "Its capacity defies the laws of physics. "
"Please submit a bug report.";
}
}
/* This is a bit of a kludge, but I need a way to inhibit query_contents()
* in long() of /obj/baggage. It may be that I could permanently
* replace query_contents() with query_liquid_desc(), but I'm not sure... */
/** @ignore yes */
varargs string query_contents(string str, object *obs ) {
string s, c;
/* This next bit used to check for volume, but to deal with the buglet
* mentioned in the previous function, I changed it to check for the
* presence of some kind of contents instead. - Tilly */
if ( query_liquid_desc() ) {
c = "\n"+ query_fullness_desc() +"\n";
} else {
c = "";
}
s = query_liquid_desc();
if (sub_query_contents) {
if (s) {
return str + capitalize(s) + "." + c;
}
return ::query_contents(str, obs) + c;
}
return ::query_contents(str, obs);
} /* query_contents */
/** @ignore yes */
string short(int dark) {
object *inv;
if(query_opaque())
return ::short(dark);
inv = all_inventory(this_object());
if(!sizeof(inv))
return ::short(dark);
return ::short(dark) + " of " +
"/global/events"->convert_message(query_multiple_short(map(inv,
(: $1->query_short() :))));
}
/** @ignore yes */
string long(string str, int dark) {
string ret;
sub_query_contents = 1;
ret = ::long(str, dark);
sub_query_contents = 0;
return ret;
} /* long() */
int query_cont_volume() { return cont_volume; }
/**
* This method returns if this vessel is currenly a liquid. This means
* it has a liquid inside it.
* @see calc_liquid()
* @return 1 if it is a liquid, 0 if not */
int query_liquid() { return is_liquid; }
/* This method determines if we have any liquids inside us at all. */
void calc_liquid() {
if (sizeof(filter(all_inventory(), (: $1->query_liquid() :)))) {
is_liquid = 1;
} else {
is_liquid = 0;
}
}
/**
* This method returns the current amount of liquid in the container.
* @return the current amount of liquid in the container */
int query_volume() { return volume; }
/**
* This method returns the amount of volume left for liquids to be
* added into.
* @return the amount of volume left
* @see add_volume()
* @see transfer_liquid_to() */
int query_volume_left() {
if (!query_max_weight()) {
return max_volume - volume;
}
return max_volume - volume - (max_volume*query_loc_weight())/query_max_weight();
} /* query_volume_left() */
/**
* This method returns the amount of volume left for liquids to be
* added into.
* @param vol the amount of volume added
* @return 1 if the addition was successful, 0 if not
* @see add_volume() */
int add_volume(int vol) {
if ((vol <= 0) || !max_volume || (vol + volume <= max_volume)) {
volume += vol;
if (previous_object()->query_continuous()) {
cont_volume += vol;
}
return 1;
}
// Should spillage be handled here, or in the caller?
return 0;
} /* add_volume() */
/** @ignore yes */
int add_weight(int n) {
int v;
// Debugging. Can be removed if I forget -- Jeremy
if (this_player() && (this_player()->query_name() == "pinkfish")) {
tell_creator("pinkfish", "vessel::add_weight(%O)\n", n);
}
if (previous_object()->query_continuous()) {
return (::add_weight(n));
}
//if (!(v = previous_object()->query_amount()))
v = n*200;
if (max_volume && (v + volume > max_volume)) {
return 0;
}
if (::add_weight(n)) {
//printf("Increasing volume by %d (add_weight)\n", v);
volume += v;
return 1;
}
return 0;
} /* add_weight() */
/**
* This method removes some volume of liquid from the container.
* @param vol_lost the amount of volume removed
* @see add_volume()
* @see query_volume() */
int remove_volume(int vol_lost)
{
// Removes equal proportions of all continuous matter.
int amt_lost, i, orig_cv;
object *contents;
if (!cont_volume) {
return 0;
}
orig_cv = cont_volume;
contents = all_inventory(this_object());
for (i = 0; i < sizeof(contents); i++) {
if (contents[i]->query_continuous()) {
amt_lost = -to_int((int)contents[i]->query_amount()
* (to_float(vol_lost) / orig_cv));
if (!amt_lost) {
// Always take at least one unit
amt_lost++;
}
contents[i]->adjust_amount(amt_lost);
}
}
return vol_lost;
} /* remove_volume() */
/**
* This method transfers a given amount of a liquid to a new container.
* @param dest the destination of the liquid
* @param vol_xferred the amount of volume transfered */
int xfer_volume(int vol_xferred, object dest) {
// Transfers equal portions of all continuous matter to dest.
// If successful, returns 0; if it failed for some reason, it returns
// the volume not transferred (note that full checks should be done
// by the caller).
int vol_to_go;
int i;
int amt_xferred;
int tmp;
int orig_cv;
object *contents;
object copy;
string file_path;
mapping map;
vol_to_go = vol_xferred;
if (!cont_volume) {
return 0;
}
orig_cv = cont_volume;
contents = filter(all_inventory(this_object()),
(: $1->query_continuous() :));
for (i = 0; i < sizeof(contents) && vol_to_go > 0; i++) {
// This should prevent roundoff errors.
if (i == sizeof(contents) - 1) {
amt_xferred = vol_to_go;
} else {
amt_xferred = to_int((int)contents[i]->query_amount()
* (to_float(vol_xferred) / orig_cv));
}
if (!amt_xferred) {
// Always take at least one unit
amt_xferred++;
}
file_path = explode( file_name(contents[i]), "#" )[ 0 ];
copy = clone_object(file_path);
map = (mapping)contents[i]->query_dynamic_auto_load();
copy->init_dynamic_arg( map );
map = (mapping)contents[i]->query_static_auto_load();
if (map) {
copy->init_static_arg( map );
}
copy->set_amount(amt_xferred);
tmp = copy->move(dest);
if (tmp == MOVE_OK) {
// There should probably be enough checks before here to
// make sure the move succeeds, or an explanation why it
// didn't.
vol_to_go -= amt_xferred;
contents[i]->adjust_amount(-amt_xferred);
} else {
copy->dest_me();
}
}
return vol_to_go;
} /* xfer_volume() */
/** @ignore yes */
void heart_beat() {
// Note that having a leak rate can be expensive, so it should only
// be done if it's important to the application (such as using it
// to impose a time restriction).
int lost, off;
if (leak_rate == 0 || !is_liquid) {
set_heart_beat(0);
return;
}
if (hb_count--) return ;
hb_count = 10;
lost = leak_rate;
if (lost > cont_volume)
lost = cont_volume;
off = lost/100;
if (off > 10)
off = 10;
// tell_room(environment(),
// capitalize(query_liquid_desc())+leak_verb[off]+" out of the "+
// short(1)+".\n");
/* This is hacked because as far as I can tell there is no way to get a 'generic short'
* for a continuous liquid, ie: some water, instead of two pints of water. */
if ( interactive( environment() ) )
tell_object( environment(), "$C$Some " + query_multiple_short( map( all_inventory(),
(: $1->query_short() :) ) ) + leak_verb[off] + " out of the "+ short(1) + ".\n" );
else
tell_room( environment(), "$C$Some " + query_multiple_short( map( all_inventory(),
(: $1->query_short() :) ) ) + leak_verb[off] + " out of the "+ short(1) + ".\n" );
(void)remove_volume(lost);
if (!cont_volume) {
set_heart_beat(0);
}
} /*heart_beat() */
/** @ignore yes */
int do_pour(object *to, mixed *args_b, mixed *args_a, mixed *args) {
int m, n, volume_needed, their_volume, their_max, ovf, xfer_result;
if (query_fighting(this_player())) {
add_failed_mess("You cannot attempt to do this while in combat.\n");
return 0;
}
if (environment(this_object()) != this_player()) {
add_failed_mess("You aren't carrying $D.\n");
return 0;
}
if (sizeof(args) == 5) {
//m = args[1];
//n = args[2];
sscanf(args[0] + " " + args[1], "%d %d", m, n);
if ((m > n) || (m < 0) || (n <= 0)) {
add_failed_mess("Interesting fraction you have there!\n");
return 0;
}
} else {
m = 1;
n = 1;
}
if (query_locked()) {
add_failed_mess("$C$$D $V$0=is,are$V$ locked!\n");
return 0;
}
if (query_closed()) {
if (do_open()) {
write("You open the " + short(0) + ".\n");
} else {
add_failed_mess("You cannot open $D.\n");
return 0;
}
}
if (cont_volume <= 0) {
add_failed_mess("$C$$D has nothing to pour!\n");
return 0;
}
if (sizeof(to) > 1) {
add_failed_mess("You can only pour into one object at a time.\n");
return 0;
}
their_volume = (int)to[0]->query_volume();
their_max = (int)to[0]->query_max_volume();
if (their_max <= 0) {
add_failed_mess("$C$" + to[0]->the_short(0) +
" doesn't look like it can be filled!\n");
return 0;
}
if (their_volume >= their_max) {
add_failed_mess("The " + to[0]->short(0)
+ " $V$0=is,are$V$ full to the brim already.\n");
their_volume = their_max;
}
if ((m == 1) && (n == 1)) {
volume_needed = volume;
} else {
volume_needed = max_volume * m / n;
}
if (volume < volume_needed) {
add_failed_mess("$C$$D $V$0=is,are$V$ less than " + m + "/" + n +
" full.\n");
return 0;
}
if (volume_needed > 120) {
// +/- 1 ounce (could make this skill-dependent...)
volume_needed += random(240) - 120;
}
if (volume_needed > (their_max - their_volume)) {
volume_needed = their_max - their_volume;
ovf = 1;
}
if (volume_needed > cont_volume) {
add_failed_mess("You drain the " + short(0) + " into the "
+ to[0]->short(0) + " but it $V$0=is,are$V$ not enough.\n");
volume_needed = cont_volume;
this_player()->add_succeeded(to[0]);
} else {
this_player()->add_succeeded(to[0]);
}
xfer_result = xfer_volume( volume_needed, to[0] );
//If the result is less then needed, then it worked.
//If it is exactly the same, then 0 was transferred.
if (xfer_result < volume_needed) {
this_player()->add_succeeded_mess(this_object(), "$N $V $D into $I.\n", ({to[0]}));
}
else {
add_failed_mess( "You were unable to $V $D into $I.\n", ({to[0]}));
return 0;
}
if (ovf) {
this_player()->add_succeeded_mess(this_object(), "$N $V $D into $I, " +
"spilling some in the process.\n", ({to[0]}));
}
return 1;
} /* do_pour() */
/** @ignore yes */
int do_fill(object *to, mixed *args_b, mixed *args_a, mixed *args) {
int m;
int n;
int i;
int run_out;
int volume_needed;
int their_volume;
int their_max;
int amount_not_poured;
int ok;
if (query_fighting(this_player())) {
add_failed_mess("You cannot attempt to do this while in combat.\n");
return 0;
}
if (sizeof(args) == 4) {
//m = args[1];
//n = args[2];
sscanf(args[1] + " " + args[2], "%d %d", m, n);
if ((m > n) || (m < 0) || (n <= 0)) {
add_failed_mess("Interesting fraction you have there!\n");
return 0;
}
} else {
m = 1;
n = 1;
}
if (query_closed() &&
query_locked()) {
add_failed_mess("$C$$D $V$0=is,are$V$ locked!\n");
return 0;
}
if (query_closed()) {
if (do_open()) {
write("You open the " + short(0) + ".\n");
} else {
add_failed_mess("You cannot open $D.\n");
return 0;
}
}
if (cont_volume <= 0) {
add_failed_mess("$C$$D has nothing to pour!\n");
return 0;
}
run_out = 0;
for (i = 0; i < sizeof(to) && !run_out; i++) {
if ((environment(this_object()) != this_player()) &&
(environment(to[i]) != this_player())) {
write("You're not carrying " + the_short() + " or " +
to[i]->the_short() + ".\n");
continue;
}
if (to[i]->query_closed()) {
add_failed_mess("$I $V$0=is,are$V$ closed.\n", to[i..i]);
continue;
}
their_volume = (int)to[i]->query_volume();
their_max = (int)to[i]->query_max_volume();
if (their_max <= 0) {
add_failed_mess("$I doesn't look like it can be filled!\n",
to[i..i]);
continue;
}
if (their_volume >= their_max) {
add_failed_mess("$I $V$0=is,are$V$ full to the brim already.\n", to[i..i]);
continue;
}
volume_needed = their_max * m / n;
if (their_volume >= volume_needed) {
add_failed_mess("$I $V$0=is,are$V$ more than " + m + "/" + n +
" full already.\n", to[i..i]);
continue;
}
if (volume_needed > 120) {
// +/- 1 ounce (could make this skill-dependent...)
volume_needed += random(240) - 120;
}
if (volume_needed > their_max) {
volume_needed = their_max;
}
ok++;
volume_needed -= their_volume;
if (volume_needed > cont_volume) {
add_failed_mess("You drain " + the_short() + " into "
+ to[i]->the_short() + " but it $V$0=is,are$V$ not enough.\n");
volume_needed = cont_volume;
run_out = 1;
this_player()->add_succeeded(to[i]);
} else {
this_player()->add_succeeded(to[i]);
}
amount_not_poured = xfer_volume(volume_needed, to[i]);
if (amount_not_poured) {
ok--;
}
}
if (!ok) {
add_failed_mess("You cannot seem to do anything useful with this "
"container, it seems unwilling to accept what you "
"offer.\n");
}
return ok;
} /* do_fill() */
/**
* This method checks to see if they are fighting anyone and if anyone
* (that can see them) is fighting them.
* @param player the player to check */
private int query_fighting(object player) {
object ob;
if (!player || !environment(player)) {
return 0;
}
if (player->query_fighting()) {
return 1;
}
foreach (ob in all_inventory(environment(player))) {
if (living(ob)) {
if (ob->query_attacker_list() &&
member_array(this_player(), ob->query_attacker_list()) != -1) {
return 1;
}
}
}
if (environment(ob)->query_mirror_room()) {
foreach (ob in all_inventory(environment(player)->query_mirror_room())) {
if (living(ob)) {
if (member_array(this_player(), ob->query_attacker_list()) != -1) {
return 1;
}
}
}
}
} /* query_fighting() */
/**
* This method checks to see if the person doing the drinking can hold onto
* their bottle without loosing it while in combat. Warning! This code
* may be used in other objects to deal with handling drinking while in
* combat.
* @return 1 if the bottle is stopped, 0 if it is not
* @param player the player doing the drinking
* @param me the object being drunk */
int is_fighting_bottle_smashed(object player,
object me) {
object* fighting;
object ob;
object weapon;
string skill;
string my_skill;
int bonus;
int stopped;
stopped = 0;
// See if we are in combat and give a chance to drop the item if we
// are. Chance is higher if we have no free hands. */
if (query_fighting(player)) {
// Ok, we are in combat. Check out free hands. The more people
// we are fighting the harder it is.
fighting = filter(player->query_attacker_list(),
(: environment($1) == $2 :), environment(player));
if (query_holder()) {
bonus = 0;
bonus += -10 * player->query_free_limbs();
} else {
bonus = 20;
}
if (sizeof(fighting)) {
bonus += (sizeof(fighting) - 1) * 20;
}
if (player->query_free_limbs() < 2 &&
!query_holder()) {
bonus += 50 - player->query_free_limbs() * 25;
}
// Check against the other persons fighting skill.
foreach (ob in fighting) {
// If they are using a weapon use the weapon skill, otherwise unarmed.
if (sizeof(ob->query_holding() - ({ 0 }))) {
weapon = (ob->query_holding() - ({ 0 }))[0];
skill = weapon->query_weapon_type();
if (skill == "mixed" ||
(skill != "sharp" && skill != "blunt" && skill != "pierce")) {
skill = ({ "sharp", "blunt", "pierce" })[random(3)];
}
skill = "fighting.combat.melee." + skill;
} else {
skill = "fighting.combat.melee.u