server-1.12/server/player.c

4264 lines
142 KiB
C

/*
* static char *rcsid_player_c =
* "$Id: player.c 11578 2009-02-23 22:02:27Z lalo $";
*/
/*
CrossFire, A Multiplayer game for X-windows
Copyright (C) 2002,2006-2007 Mark Wedel & Crossfire Development Team
Copyright (C) 1992 Frank Tore Johansen
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
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., 675 Mass Ave, Cambridge, MA 02139, USA.
The author can be reached via e-mail to crossfire-devel@real-time.com
*/
/**
* @file
* Player-related functions, including those used during the login and creation phases.
* @todo describe login/creation functions/cycles.
*/
#include <global.h>
#include <assert.h>
#ifndef WIN32 /* ---win32 remove headers */
#include <pwd.h>
#endif
#ifndef __CEXTRACT__
#include <sproto.h>
#endif
#include <sounds.h>
#include <living.h>
#include <object.h>
#include <spells.h>
#include <skills.h>
#include <newclient.h>
static archetype *get_player_archetype(archetype *at);
static int action_makes_visible(object *op);
/**
* Find a player by her full name.
*
* @param plname
* name to find.
* @return
* matching player, or NULL if no match.
*/
player *find_player(const char *plname) {
player *pl;
char name[MAX_BUF];
for (pl = first_player; pl != NULL; pl = pl->next) {
if (pl->ob != NULL) {
query_name(pl->ob, name, MAX_BUF);
if (!strcmp(name, plname))
return pl;
}
}
return NULL;
}
/**
* Find a player by a partial name.
*
* @param plname
* name to match.
* @return
* matching player if only one matching, or one perfectly matching, NULL if no match or more than one.
*/
player *find_player_partial_name(const char *plname) {
player *pl;
player *found = NULL;
size_t namelen = strlen(plname);
for (pl = first_player; pl != NULL; pl = pl->next) {
if (strlen(pl->ob->name) < namelen)
continue;
if (!strcmp(pl->ob->name, plname))
return pl;
if (!strncasecmp(pl->ob->name, plname, namelen)) {
if (found)
return NULL;
found = pl;
}
}
return found;
}
/**
* Sends the message of the day to the player.
*
* @param op
* player to send to.
*/
void display_motd(const object *op) {
char buf[MAX_BUF];
char motd[HUGE_BUF];
FILE *fp;
int comp;
int size;
snprintf(buf, sizeof(buf), "%s/%s", settings.confdir, settings.motd);
if ((fp = open_and_uncompress(buf, 0, &comp)) == NULL) {
return;
}
motd[0] = '\0';
size = 0;
while (fgets(buf, MAX_BUF, fp) != NULL) {
if (*buf == '#')
continue;
strncat(motd+size, buf, HUGE_BUF-size);
size += strlen(buf);
}
draw_ext_info(NDI_UNIQUE|NDI_GREEN, 0, op, MSG_TYPE_MOTD, MSG_SUBTYPE_NONE,
motd, NULL);
close_and_delete(fp, comp);
}
/**
* Send the rules to a player.
*
* @param op
* player to send rules to.
*/
void send_rules(const object *op) {
char buf[MAX_BUF];
char rules[HUGE_BUF];
FILE *fp;
int comp;
int size;
snprintf(buf, sizeof(buf), "%s/%s", settings.confdir, settings.rules);
if ((fp = open_and_uncompress(buf, 0, &comp)) == NULL) {
return;
}
rules[0] = '\0';
size = 0;
while (fgets(buf, MAX_BUF, fp) != NULL) {
if (*buf == '#')
continue;
if (size+strlen(buf) >= HUGE_BUF) {
LOG(llevDebug, "Warning, rules size is > %d bytes.\n", HUGE_BUF);
break;
}
strncat(rules+size, buf, HUGE_BUF-size);
size += strlen(buf);
}
draw_ext_info(NDI_UNIQUE|NDI_GREEN, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_RULES,
rules, NULL);
close_and_delete(fp, comp);
}
/**
* Send the news to a player.
*
* @param op
* player to send to.
*/
void send_news(const object *op) {
char buf[MAX_BUF];
char news[HUGE_BUF];
char subject[MAX_BUF];
FILE *fp;
int comp;
int size;
snprintf(buf, sizeof(buf), "%s/%s", settings.confdir, settings.news);
if ((fp = open_and_uncompress(buf, 0, &comp)) == NULL)
return;
news[0] = '\0';
subject[0] = '\0';
size = 0;
while (fgets(buf, MAX_BUF, fp) != NULL) {
if (*buf == '#')
continue;
if (*buf == '%') { /* send one news */
if (size > 0)
draw_ext_info_format(NDI_UNIQUE|NDI_GREEN, 0, op,
MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_NEWS,
"%s:\n%s",
"%s:\n%s",
subject, news); /*send previously read news*/
strcpy(subject, buf+1);
strip_endline(subject);
size = 0;
news[0] = '\0';
} else {
if (size+strlen(buf) >= HUGE_BUF) {
LOG(llevDebug, "Warning, one news item has size > %d bytes.\n", HUGE_BUF);
break;
}
strncat(news+size, buf, HUGE_BUF-size);
size += strlen(buf);
}
}
draw_ext_info_format(NDI_UNIQUE|NDI_GREEN, 0, op,
MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_NEWS,
"%s:\n%s",
"%s:\n%s",
subject, news);
close_and_delete(fp, comp);
}
/**
* Is the player name valid.
*
* @param cp
* name to test.
* @return
* 0 if invalid, 1 if valid.
*/
int playername_ok(const char *cp) {
/* Don't allow - or _ as first character in the name */
if (*cp == '-' || *cp == '_')
return 0;
for (; *cp != '\0'; cp++)
if (!((*cp >= 'a' && *cp <= 'z') || (*cp >= 'A' && *cp <= 'Z'))
&& *cp != '-'
&& *cp != '_')
return 0;
return 1;
}
/**
* Create a player's object, initialize a player's structure.
*
* This no longer sets the player map. Also, it now updates
* all the pointers so the caller doesn't need to do that.
* Caller is responsible for setting the correct map.
*
* Redo this to do both get_player_ob and get_player.
* Hopefully this will be less bugfree and simpler.
*
* @param p
* if NULL, a new player structure is created, else p is recycled.
* @return
* initialized player structure.
*/
static player *get_player(player *p) {
object *op = arch_to_object(get_player_archetype(NULL));
int i;
if (!p) {
player *tmp;
p = (player *)malloc(sizeof(player));
if (p == NULL)
fatal(OUT_OF_MEMORY);
/* This adds the player in the linked list. There is extra
* complexity here because we want to add the new player at the
* end of the list - there is in fact no compelling reason that
* that needs to be done except for things like output of
* 'who'.
*/
tmp = first_player;
while (tmp != NULL && tmp->next != NULL)
tmp = tmp->next;
if (tmp != NULL)
tmp->next = p;
else
first_player = p;
p->next = NULL;
} else {
/* Only needed when reusing existing player. */
clear_player(p);
}
/* Clears basically the entire player structure except
* for next and socket.
*/
memset((void *)((char *)p+offsetof(player, maplevel)), 0, sizeof(player)-offsetof(player, maplevel));
/* There are some elements we want initialized to non zero value -
* we deal with that below this point.
*/
p->party = NULL;
p->rejoin_party = party_rejoin_if_exists;
p->outputs_sync = 16; /* Every 2 seconds */
p->outputs_count = 1; /* Keeps present behaviour */
p->unapply = unapply_nochoice;
p->Swap_First = -1;
#ifdef AUTOSAVE
p->last_save_tick = 9999999;
#endif
strcpy(p->savebed_map, first_map_path); /* Init. respawn position */
op->contr = p; /* this aren't yet in archetype */
p->ob = op;
op->speed_left = 0.5;
op->speed = 1.0;
op->direction = 5; /* So player faces south */
op->stats.wc = 2;
op->run_away = 25; /* Then we panick... */
p->socket.monitor_spells = 0; /* this needs to be set before roll_stats() as it calls fix_object() that sends the spells. */
roll_stats(op);
p->state = ST_ROLL_STAT;
clear_los(op);
p->gen_sp_armour = 10;
p->last_speed = -1;
p->shoottype = range_none;
p->bowtype = bow_normal;
p->petmode = pet_normal;
p->listening = 10;
p->last_weapon_sp = -1;
p->peaceful = 1; /* default peaceful */
p->do_los = 1;
p->explore = 0;
p->no_shout = 0; /* default can shout */
p->language = 0;
strncpy(p->title, op->arch->clone.name, sizeof(p->title)-1);
p->title[sizeof(p->title)-1] = '\0';
LOG(llevError, "Trying to load race from: %s \n", op->arch->clone.name);
op->race = add_string(op->arch->clone.race);
CLEAR_FLAG(op, FLAG_READY_SKILL);
/* we need to clear these to -1 and not zero - otherwise,
* if a player quits and starts a new character, we wont
* send new values to the client, as things like exp start
* at zero.
*/
for (i = 0; i < NUM_SKILLS; i++) {
p->last_skill_exp[i] = -1;
p->last_skill_ob[i] = NULL;
}
for (i = 0; i < NROFATTACKS; i++) {
p->last_resist[i] = -1;
}
p->last_stats.exp = -1;
p->last_weight = (uint32)-1;
p->socket.update_look = 0;
p->socket.look_position = 0;
return p;
}
/**
* This loads the first map an puts the player on it.
*
* @param op
* player to put on map.
*/
static void set_first_map(object *op) {
strcpy(op->contr->maplevel, first_map_path);
op->x = -1;
op->y = -1;
enter_exit(op, NULL);
}
/**
* Tries to add player on the connection passwd in ns.
*
* Player object is created and put on the first map, rules/news/motd are sent.
*
* @param ns
* connection.
*/
void add_player(socket_struct *ns) {
player *p;
p = get_player(NULL);
memcpy(&p->socket, ns, sizeof(socket_struct));
/* The memcpy above copies the reference to faces sent. So we need to clear
* that pointer in ns, otherwise we get a double free.
*/
ns->faces_sent = NULL;
if (p->socket.faces_sent == NULL)
fatal(OUT_OF_MEMORY);
/* Needed because the socket we just copied over needs to be cleared.
* Note that this can result in a client reset if there is partial data
* on the uncoming socket.
*/
SockList_ResetRead(&p->socket.inbuf);
set_first_map(p->ob);
CLEAR_FLAG(p->ob, FLAG_FRIENDLY);
add_friendly_object(p->ob);
send_rules(p->ob);
send_news(p->ob);
display_motd(p->ob);
get_name(p->ob);
}
/**
* Get next player archetype from archetype list.
* Not very efficient routine, but used only creating new players.
* @note there MUST be at least one player archetype! Will exit() if none.
*
* @param at
* archetype to search from.
* @return
* next player archetype available.
*/
static archetype *get_player_archetype(archetype *at) {
archetype *start = at;
for (;;) {
if (at == NULL || at->next == NULL)
at = first_archetype;
else
at = at->next;
if (at->clone.type == PLAYER)
return at;
if (at == start) {
LOG(llevError, "No Player archetypes\n");
exit(-1);
}
}
}
/**
* Finds the nearest visible player for some object.
*
* @param mon
* what object is searching a player.
* @return
* player, or NULL if nothing suitable.
*/
object *get_nearest_player(object *mon) {
object *op = NULL;
player *pl = NULL;
objectlink *ol;
unsigned lastdist;
rv_vector rv;
for (ol = first_friendly_object, lastdist = 1000; ol != NULL; ol = ol->next) {
/* We should not find free objects on this friendly list, but it
* does periodically happen. Given that, lets deal with it.
* While unlikely, it is possible the next object on the friendly
* list is also free, so encapsulate this in a while loop.
*/
while (QUERY_FLAG(ol->ob, FLAG_FREED) || !QUERY_FLAG(ol->ob, FLAG_FRIENDLY)) {
object *tmp = ol->ob;
/* Can't do much more other than log the fact, because the object
* itself will have been cleared.
*/
LOG(llevDebug, "get_nearest_player: Found free/non friendly object on friendly list\n");
ol = ol->next;
remove_friendly_object(tmp);
if (!ol)
return op;
}
/* Remove special check for player from this. First, it looks to cause
* some crashes (ol->ob->contr not set properly?), but secondly, a more
* complicated method of state checking would be needed in any case -
* as it was, a clever player could type quit, and the function would
* skip them over while waiting for confirmation. Remove
* on_same_map check, as can_detect_enemy also does this
*/
if (!can_detect_enemy(mon, ol->ob, &rv))
continue;
if (lastdist > rv.distance) {
op = ol->ob;
lastdist = rv.distance;
}
}
for (pl = first_player; pl != NULL; pl = pl->next) {
if (can_detect_enemy(mon, pl->ob, &rv)) {
if (lastdist > rv.distance) {
op = pl->ob;
lastdist = rv.distance;
}
}
}
return op;
}
/**
* This value basically determines how large a detour a monster will take from
* the direction path when looking for a path to the player.
*
* The values are in the amount of direction the deviation is.
*
* I believe this can safely go to 2, 3 is questionable, 4 will likely
* result in a monster paths backtracking.
*/
#define DETOUR_AMOUNT 2
/**
* This is used to prevent infinite loops. Consider a case where the
* player is in a chamber (with gate closed), and monsters are outside.
* with DETOUR_AMOUNT==2, the function will turn each corner, trying to
* find a path into the chamber. This is a good thing, but since there
* is no real path, it will just keep circling the chamber for
* ever (this could be a nice effect for monsters, but not for the function
* to get stuck in. I think for the monsters, if max is reached and
* we return the first direction the creature could move would result in the
* circling behaviour. Unfortunately, this function is also used to determined
* if the creature should cast a spell, so returning a direction in that case
* is probably not a good thing.
*/
#define MAX_SPACES 50
/**
* Returns the direction to the player, if valid. Returns 0 otherwise.
*
* Modified to verify there is a path to the player. Does this by stepping towards
* player and if path is blocked then see if blockage is close enough to player that
* direction to player is changed (ie zig or zag). Continue zig zag until either
* reach player or path is blocked. Thus, will only return true if there is a free
* path to player. Though path may not be a straight line. Note that it will find
* player hiding along a corridor at right angles to the corridor with the monster.
*
* Modified by MSW 2001-08-06 to handle tiled maps. Various notes:
* - With DETOUR_AMOUNT being 2, it should still go and find players hiding
* down corriders.
* - I think the old code was broken if the first direction the monster
* should move was blocked - the code would store the first direction without
* verifying that the player can actually move in that direction. The new
* code does not store anything in firstdir until we have verified that the
* monster can in fact move one space in that direction.
* - I'm not sure how good this code will be for moving multipart monsters,
* since only simple checks to blocked are being called, which could mean the monster
* is blocking itself.
*
* @param mon
* source object.
* @param pl
* target.
* @param mindiff
* minimal distance mon and pl should have.
* @return
* direction from mon to pl, 0 if can't get there.
*/
int path_to_player(object *mon, object *pl, unsigned mindiff) {
rv_vector rv;
sint16 x, y;
int lastx, lasty, dir, i, diff, firstdir = 0, lastdir, max = MAX_SPACES, mflags, blocked;
mapstruct *m, *lastmap;
get_rangevector(mon, pl, &rv, 0);
if (rv.distance < mindiff)
return 0;
x = mon->x;
y = mon->y;
m = mon->map;
dir = rv.direction;
lastdir = firstdir = rv.direction; /* perhaps we stand next to pl, init firstdir too */
diff = MAX(FABS(rv.distance_x), FABS(rv.distance_y));
/* If we can't solve it within the search distance, return now. */
if (diff > max)
return 0;
while (diff > 1 && max > 0) {
lastx = x;
lasty = y;
lastmap = m;
x = lastx+freearr_x[dir];
y = lasty+freearr_y[dir];
mflags = get_map_flags(m, &m, x, y, &x, &y);
blocked = (mflags&P_OUT_OF_MAP) ? MOVE_ALL : GET_MAP_MOVE_BLOCK(m, x, y);
/* Space is blocked - try changing direction a little */
if ((mflags&P_OUT_OF_MAP)
|| ((OB_TYPE_MOVE_BLOCK(mon, blocked) || (mflags&P_IS_ALIVE))
&& (m == mon->map && blocked_link(mon, m, x, y)))) {
/* recalculate direction from last good location. Possible
* we were not traversing ideal location before.
*/
get_rangevector_from_mapcoord(lastmap, lastx, lasty, pl, &rv, 0);
if (rv.direction != dir) {
/* OK - says direction should be different - lets reset the
* the values so it will try again.
*/
x = lastx;
y = lasty;
m = lastmap;
dir = firstdir = rv.direction;
} else {
/* direct path is blocked - try taking a side step to
* either the left or right.
* Note increase the values in the loop below to be
* more than -1/1 respectively will mean the monster takes
* bigger detour. Have to be careful about these values getting
* too big (3 or maybe 4 or higher) as the monster may just try
* stepping back and forth
*/
for (i = -DETOUR_AMOUNT; i <= DETOUR_AMOUNT; i++) {
if (i == 0)
continue; /* already did this, so skip it */
/* Use lastdir here - otherwise,
* since the direction that the creature should move in
* may change, you could get infinite loops.
* ie, player is northwest, but monster can only
* move west, so it does that. It goes some distance,
* gets blocked, finds that it should move north,
* can't do that, but now finds it can move east, and
* gets back to its original point. lastdir contains
* the last direction the creature has successfully
* moved.
*/
x = lastx+freearr_x[absdir(lastdir+i)];
y = lasty+freearr_y[absdir(lastdir+i)];
m = lastmap;
mflags = get_map_flags(m, &m, x, y, &x, &y);
if (mflags&P_OUT_OF_MAP)
continue;
blocked = GET_MAP_MOVE_BLOCK(m, x, y);
if (OB_TYPE_MOVE_BLOCK(mon, blocked))
continue;
if (mflags&P_IS_ALIVE)
continue;
if (m == mon->map && blocked_link(mon, m, x, y))
break;
}
/* go through entire loop without finding a valid
* sidestep to take - thus, no valid path.
*/
if (i == (DETOUR_AMOUNT+1))
return 0;
diff--;
lastdir = dir;
max--;
if (!firstdir)
firstdir = dir+i;
} /* else check alternate directions */
} /* if blocked */
else {
/* we moved towards creature, so diff is less */
diff--;
max--;
lastdir = dir;
if (!firstdir)
firstdir = dir;
}
if (diff <= 1) {
/* Recalculate diff (distance) because we may not have actually
* headed toward player for entire distance.
*/
get_rangevector_from_mapcoord(m, x, y, pl, &rv, 0);
diff = MAX(FABS(rv.distance_x), FABS(rv.distance_y));
}
if (diff > max)
return 0;
}
/* If we reached the max, didn't find a direction in time */
if (!max)
return 0;
return firstdir;
}
/**
* Gives a new player her initial items.
*
* They will be god-given, and suitable for the player's race/restrictions.
*
* @param pl
* player.
* @param items
* treasure list containing the items.
*/
void give_initial_items(object *pl, treasurelist *items) {
object *op, *next = NULL;
if (pl->randomitems != NULL)
create_treasure(items, pl, GT_STARTEQUIP|GT_ONLY_GOOD, 1, 0);
for (op = pl->inv; op; op = next) {
next = op->below;
/* Forces get applied per default, unless they have the
* flag "neutral" set. Sorry but I can't think of a better way
*/
if (op->type == FORCE && !QUERY_FLAG(op, FLAG_NEUTRAL))
SET_FLAG(op, FLAG_APPLIED);
/* we never give weapons/armour if these cannot be used
* by this player due to race restrictions
*/
if (pl->type == PLAYER) {
if ((!QUERY_FLAG(pl, FLAG_USE_ARMOUR) && IS_ARMOR(op))
|| (!QUERY_FLAG(pl, FLAG_USE_WEAPON) && IS_WEAPON(op))
|| (!QUERY_FLAG(pl, FLAG_USE_SHIELD) && IS_SHIELD(op))) {
remove_ob(op);
free_object(op);
continue;
}
}
/* This really needs to be better - we should really give
* a substitute spellbook. The problem is that we don't really
* have a good idea what to replace it with (need something like
* a first level treasurelist for each skill.)
* remove duplicate skills also
*/
if (op->type == SPELLBOOK || op->type == SKILL) {
object *tmp;
for (tmp = op->below; tmp; tmp = tmp->below)
if (tmp->type == op->type && tmp->name == op->name)
break;
if (tmp) {
remove_ob(op);
free_object(op);
LOG(llevError, "give_initial_items: Removing duplicate object %s\n", tmp->name);
continue;
}
if (op->nrof > 1)
op->nrof = 1;
}
if (op->type == SPELLBOOK && op->inv) {
CLEAR_FLAG(op->inv, FLAG_STARTEQUIP);
}
/* Give starting characters identified, uncursed, and undamned
* items. Just don't identify gold or silver, or it won't be
* merged properly.
*/
if (need_identify(op)) {
SET_FLAG(op, FLAG_IDENTIFIED);
CLEAR_FLAG(op, FLAG_CURSED);
CLEAR_FLAG(op, FLAG_DAMNED);
}
if (op->type == SPELL) {
remove_ob(op);
free_object(op);
continue;
} else if (op->type == SKILL) {
SET_FLAG(op, FLAG_CAN_USE_SKILL);
op->stats.exp = 0;
op->level = 1;
} else if (op->type == POTION && op->subtype == POT_THROW) {
/* don't lock, etc. */
}
/* lock all 'normal items by default */
else
SET_FLAG(op, FLAG_INV_LOCKED);
} /* for loop of objects in player inv */
/* Need to set up the skill pointers */
link_player_skills(pl);
/**
* Now we do a second loop, to apply weapons/armors/...
* This is because weapons require the skill, which can be given after the first loop.
*/
for (op = pl->inv; op; op = next) {
next = op->below;
if ((IS_ARMOR(op) || IS_WEAPON(op) || IS_SHIELD(op)) && !QUERY_FLAG(op, FLAG_APPLIED))
manual_apply(pl, op, AP_NOPRINT);
}
}
/**
* Waiting for the player's name.
*
* @param op
* player.
*/
void get_name(object *op) {
op->contr->write_buf[0] = '\0';
op->contr->state = ST_GET_NAME;
send_query(&op->contr->socket, 0, "What is your name?\n:");
}
/**
* Waiting for the player's password.
*
* @param op
* player.
*/
void get_password(object *op) {
op->contr->write_buf[0] = '\0';
op->contr->state = ST_GET_PASSWORD;
send_query(&op->contr->socket, CS_QUERY_HIDEINPUT, "What is your password?\n:");
}
/**
* Ask the player whether to play again or disconnect.
*
* @param op
* player.
*/
void play_again(object *op) {
op->contr->state = ST_PLAY_AGAIN;
op->chosen_skill = NULL;
send_query(&op->contr->socket, CS_QUERY_SINGLECHAR, "Do you want to play again (a/q)?");
/* a bit of a hack, but there are various places early in th
* player creation process that a user can quit (eg, roll
* stats) that isn't removing the player. Taking a quick
* look, there are many places that call play_again without
* removing the player - it probably makes more sense
* to leave it to play_again to remove the object in all
* cases.
*/
if (!QUERY_FLAG(op, FLAG_REMOVED))
remove_ob(op);
/* Need to set this to null - otherwise, it could point to garbage,
* and draw() doesn't check to see if the player is removed, only if
* the map is null or not swapped out.
*/
op->map = NULL;
}
/**
* Player replied to play again / disconnect.
*
* @param op
* player.
* @param key
* received choice.
*/
void receive_play_again(object *op, char key) {
if (key == 'q' || key == 'Q') {
remove_friendly_object(op);
leave(op->contr, 0); /* ericserver will draw the message */
return;
} else if (key == 'a' || key == 'A') {
player *pl = op->contr;
const char *name = op->name;
add_refcount(name);
remove_friendly_object(op);
free_object(op);
pl = get_player(pl);
op = pl->ob;
add_friendly_object(op);
op->contr->password[0] = '~';
FREE_AND_CLEAR_STR(op->name);
FREE_AND_CLEAR_STR(op->name_pl);
/* Lets put a space in here */
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_LOGIN,
"\n", "\n");
get_name(op);
op->name = name; /* Already added a refcount above */
op->name_pl = add_string(name);
set_first_map(op);
} else {
/* user pressed something else so just ask again... */
play_again(op);
}
}
/**
* Ask the player to confirm her password during creation.
*
* @param op
* player.
*/
void confirm_password(object *op) {
op->contr->write_buf[0] = '\0';
op->contr->state = ST_CONFIRM_PASSWORD;
send_query(&op->contr->socket, CS_QUERY_HIDEINPUT, "Please type your password again.\n:");
}
/**
* Ask the player for the password of the party she wants to join.
*
* @param op
* player.
* @param party
* party op wishes to join.
*/
void get_party_password(object *op, partylist *party) {
if (party == NULL) {
LOG(llevError, "get_party_password(): tried to make player %s join a NULL party\n", op->name);
return;
}
op->contr->write_buf[0] = '\0';
op->contr->state = ST_GET_PARTY_PASSWORD;
op->contr->party_to_join = party;
send_query(&op->contr->socket, CS_QUERY_HIDEINPUT, "What is the password?\n:");
}
/**
* This rolls four 1-6 rolls and sums the best 3 of the 4.
*
* @return
* sum of rolls.
*/
int roll_stat(void) {
int a[4], i, j, k;
for (i = 0; i < 4; i++)
a[i] = (int)RANDOM()%6+1;
for (i = 0, j = 0, k = 7; i < 4; i++)
if (a[i] < k)
k = a[i],
j = i;
for (i = 0, k = 0; i < 4; i++) {
if (i != j)
k += a[i];
}
return k;
}
/**
* Roll the initial player's statistics.
*
* @param op
* player to roll for.
*/
void roll_stats(object *op) {
int sum = 0;
int i = 0, j = 0;
int statsort[7];
do {
op->stats.Str = roll_stat();
op->stats.Dex = roll_stat();
op->stats.Int = roll_stat();
op->stats.Con = roll_stat();
op->stats.Wis = roll_stat();
op->stats.Pow = roll_stat();
op->stats.Cha = roll_stat();
sum = op->stats.Str+op->stats.Dex+op->stats.Int+op->stats.Con+op->stats.Wis+op->stats.Pow+op->stats.Cha;
} while (sum != 105); /* 116 used to be best possible character */
/* Sort the stats so that rerolling is easier... */
statsort[0] = op->stats.Str;
statsort[1] = op->stats.Dex;
statsort[2] = op->stats.Int;
statsort[3] = op->stats.Con;
statsort[4] = op->stats.Wis;
statsort[5] = op->stats.Pow;
statsort[6] = op->stats.Cha;
/* a quick and dirty bubblesort? */
do {
if (statsort[i] < statsort[i+1]) {
j = statsort[i];
statsort[i] = statsort[i+1];
statsort[i+1] = j;
i = 0;
} else {
i++;
}
} while (i < 6);
op->stats.Str = statsort[0];
op->stats.Dex = statsort[1];
op->stats.Con = statsort[2];
op->stats.Int = statsort[3];
op->stats.Wis = statsort[4];
op->stats.Pow = statsort[5];
op->stats.Cha = statsort[6];
op->contr->orig_stats.Str = op->stats.Str;
op->contr->orig_stats.Dex = op->stats.Dex;
op->contr->orig_stats.Int = op->stats.Int;
op->contr->orig_stats.Con = op->stats.Con;
op->contr->orig_stats.Wis = op->stats.Wis;
op->contr->orig_stats.Pow = op->stats.Pow;
op->contr->orig_stats.Cha = op->stats.Cha;
op->level = 1;
op->stats.exp = 0;
op->stats.ac = 0;
op->contr->levhp[1] = 9;
op->contr->levsp[1] = 6;
op->contr->levgrace[1] = 3;
fix_object(op);
op->stats.hp = op->stats.maxhp;
op->stats.sp = op->stats.maxsp;
op->stats.grace = op->stats.maxgrace;
op->contr->orig_stats = op->stats;
}
/**
* Ask the player what to do with the statistics.
*
* @param op
* player.
*/
void roll_again(object *op) {
esrv_new_player(op->contr, 0);
send_query(&op->contr->socket, CS_QUERY_SINGLECHAR, "[y] to roll new stats [n] to use stats\n[1-7] [1-7] to swap stats.\nRoll again (y/n/1-7)? ");
}
/**
* Player finishes selecting what stats to swap.
*
* @param op
* player.
* @param Swap_Second
* second statistic to swap.
* @todo why the reinit of exp/ac/...?
*/
static void swap_stat(object *op, int Swap_Second) {
signed char tmp;
if (op->contr->Swap_First == -1) {
LOG(llevError, "player.c:swap_stat() - Swap_First is -1\n");
return;
}
tmp = get_attr_value(&op->contr->orig_stats, op->contr->Swap_First);
set_attr_value(&op->contr->orig_stats, op->contr->Swap_First, get_attr_value(&op->contr->orig_stats, Swap_Second));
set_attr_value(&op->contr->orig_stats, Swap_Second, tmp);
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_NEWPLAYER,
"%s done\n",
"%s done\n",
short_stat_name[Swap_Second]);
op->stats.Str = op->contr->orig_stats.Str;
op->stats.Dex = op->contr->orig_stats.Dex;
op->stats.Con = op->contr->orig_stats.Con;
op->stats.Int = op->contr->orig_stats.Int;
op->stats.Wis = op->contr->orig_stats.Wis;
op->stats.Pow = op->contr->orig_stats.Pow;
op->stats.Cha = op->contr->orig_stats.Cha;
op->stats.ac = 0;
op->level = 1;
op->stats.exp = 0;
op->stats.ac = 0;
op->contr->levhp[1] = 9;
op->contr->levsp[1] = 6;
op->contr->levgrace[1] = 3;
fix_object(op);
op->stats.hp = op->stats.maxhp;
op->stats.sp = op->stats.maxsp;
op->stats.grace = op->stats.maxgrace;
op->contr->orig_stats = op->stats;
op->contr->Swap_First = -1;
}
/**
* Player is currently swapping stats.
*
* This code has been greatly reduced, because with set_attr_value
* and get_attr_value, the stats can be accessed just numeric
* ids. stat_trans is a table that translate the number entered
* into the actual stat. It is needed because the order the stats
* are displayed in the stat window is not the same as how
* the number's access that stat. The table does that translation.
*
* @param op
* player.
* @param key
* received key.
*/
void key_roll_stat(object *op, char key) {
int keynum = key-'0';
static const sint8 stat_trans[] = {
-1,
STR,
DEX,
CON,
INT,
WIS,
POW,
CHA
};
if (keynum > 0 && keynum <= 7) {
if (op->contr->Swap_First == -1) {
op->contr->Swap_First = stat_trans[keynum];
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_NEWPLAYER,
"%s ->",
"%s ->",
short_stat_name[stat_trans[keynum]]);
} else
swap_stat(op, stat_trans[keynum]);
send_query(&op->contr->socket, CS_QUERY_SINGLECHAR, "");
return;
}
switch (key) {
case 'n':
case 'N': {
SET_FLAG(op, FLAG_WIZ);
if (op->map == NULL) {
LOG(llevError, "Map == NULL in state 2\n");
break;
}
SET_ANIMATION(op, 2); /* So player faces south */
/* Enter exit adds a player otherwise */
add_statbonus(op);
send_query(&op->contr->socket, CS_QUERY_SINGLECHAR, "Now choose a character.\nPress any key to change outlook.\nPress `d' when you're pleased.\n");
op->contr->state = ST_CHANGE_CLASS;
if (op->msg)
draw_ext_info(NDI_BLUE, 0, op,
MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_NEWPLAYER,
op->msg, op->msg);
return;
}
case 'y':
case 'Y':
roll_stats(op);
send_query(&op->contr->socket, CS_QUERY_SINGLECHAR, "");
return;
case 'q':
case 'Q':
play_again(op);
return;
default:
send_query(&op->contr->socket, CS_QUERY_SINGLECHAR, "Yes, No, Quit or 1-6. Roll again?");
return;
}
return;
}
/**
* This function takes the key that is passed, and does the
* appropriate action with it (change race, or other things).
* The function name is for historical reasons - now we have
* separate race and class; this actually changes the RACE,
* not the class.
*
* @param op
* player.
* @param key
* key to handle.
*/
void key_change_class(object *op, char key) {
int tmp_loop;
if (key == 'q' || key == 'Q') {
remove_ob(op);
play_again(op);
return;
}
if (key == 'd' || key == 'D') {
char buf[MAX_BUF];
/* this must before then initial items are given */
esrv_new_player(op->contr, op->weight+op->carrying);
create_treasure(find_treasurelist("starting_wealth"), op, 0, 0, 0);
/* Lauwenmark : Here we handle the BORN global event */
execute_global_event(EVENT_BORN, op);
/* Lauwenmark : We then generate a LOGIN event */
execute_global_event(EVENT_LOGIN, op->contr, op->contr->socket.host);
op->contr->state = ST_PLAYING;
if (op->msg) {
free_string(op->msg);
op->msg = NULL;
}
/* We create this now because some of the unique maps will need it
* to save here.
*/
snprintf(buf, sizeof(buf), "%s/%s/%s", settings.localdir, settings.playerdir, op->name);
make_path_to_file(buf);
#ifdef AUTOSAVE
op->contr->last_save_tick = pticks;
#endif
start_info(op);
CLEAR_FLAG(op, FLAG_WIZ);
give_initial_items(op, op->randomitems);
link_player_skills(op);
esrv_send_inventory(op, op);
fix_object(op);
/* This moves the player to a different start map, if there
* is one for this race
*/
if (*first_map_ext_path) {
object *tmp;
char mapname[MAX_BUF];
mapstruct *oldmap;
oldmap = op->map;
snprintf(mapname, MAX_BUF-1, "%s/%s", first_map_ext_path, op->arch->name);
/*printf("%s\n", mapname);*/
tmp = get_object();
EXIT_PATH(tmp) = add_string(mapname);
EXIT_X(tmp) = op->x;
EXIT_Y(tmp) = op->y;
enter_exit(op, tmp);
if (oldmap != op->map) {
/* map exists, update bed of reality location, in case player dies */
op->contr->bed_x = op->x;
op->contr->bed_y = op->y;
snprintf(op->contr->savebed_map, sizeof(op->contr->savebed_map), "%s", mapname);
}
free_object(tmp);
} else {
LOG(llevDebug, "first_map_ext_path not set\n");
}
return;
}
/* Following actually changes the race - this is the default command
* if we don't match with one of the options above.
*/
tmp_loop = 0;
while (!tmp_loop) {
const char *name = add_string(op->name);
int x = op->x, y = op->y;
remove_statbonus(op);
remove_ob(op);
op->arch = get_player_archetype(op->arch);
copy_object(&op->arch->clone, op);
op->stats = op->contr->orig_stats;
free_string(op->name);
op->name = name;
free_string(op->name_pl);
op->name_pl = add_string(name);
op->x = x;
op->y = y;
SET_ANIMATION(op, 2); /* So player faces south */
insert_ob_in_map(op, op->map, op, 0);
strncpy(op->contr->title, op->arch->clone.name, sizeof(op->contr->title)-1);
op->contr->title[sizeof(op->contr->title)-1] = '\0';
add_statbonus(op);
tmp_loop = allowed_class(op);
}
update_object(op, UP_OBJ_FACE);
esrv_update_item(UPD_FACE, op, op);
fix_object(op);
op->stats.hp = op->stats.maxhp;
op->stats.sp = op->stats.maxsp;
op->stats.grace = 0;
if (op->msg)
draw_ext_info(NDI_BLUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_NEWPLAYER,
op->msg, op->msg);
send_query(&op->contr->socket, CS_QUERY_SINGLECHAR, "Press any key for the next race.\nPress `d' to play this race.\n");
}
/**
* We receive the reply to the 'quit confirmation' message.
*
* @param op
* player.
* @param key
* reply.
*/
void key_confirm_quit(object *op, char key) {
char buf[MAX_BUF];
if (key != 'y' && key != 'Y' && key != 'q' && key != 'Q') {
op->contr->state = ST_PLAYING;
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_LOGIN,
"OK, continuing to play.", NULL);
return;
}
/* Lauwenmark : Here we handle the REMOVE global event */
execute_global_event(EVENT_REMOVE, op);
terminate_all_pets(op);
remove_ob(op);
op->direction = 0;
draw_ext_info_format(NDI_UNIQUE|NDI_ALL|NDI_DK_ORANGE, 5, NULL, MSG_TYPE_ADMIN, MSG_TYPE_ADMIN_PLAYER,
"%s quits the game.",
"%s quits the game.",
op->name);
strcpy(op->contr->killer, "quit");
check_score(op, 0);
op->contr->party = NULL;
if (settings.set_title == TRUE)
op->contr->own_title[0] = '\0';
if (!QUERY_FLAG(op, FLAG_WAS_WIZ)) {
mapstruct *mp, *next;
/* We need to hunt for any per player unique maps in memory and
* get rid of them. The trailing slash in the path is intentional,
* so that players named 'Ab' won't match against players 'Abe' pathname
*/
snprintf(buf, sizeof(buf), "%s/%s/%s/", settings.localdir, settings.playerdir, op->name);
for (mp = first_map; mp != NULL; mp = next) {
next = mp->next;
if (!strncmp(mp->path, buf, strlen(buf)))
delete_map(mp);
}
delete_character(op->name);
}
play_again(op);
}
/**
* The player is scared, and should flee. If she can't, then she isn't scared anymore.
*
* @param op
* player.
*/
static void flee_player(object *op) {
int dir, diff;
rv_vector rv;
if (op->stats.hp < 0) {
LOG(llevDebug, "Fleeing player is dead.\n");
CLEAR_FLAG(op, FLAG_SCARED);
return;
}
if (op->enemy == NULL) {
LOG(llevDebug, "Fleeing player had no enemy.\n");
CLEAR_FLAG(op, FLAG_SCARED);
return;
}
/* Seen some crashes here. Since we don't store an
* op->enemy_count, it is possible that something destroys the
* actual enemy, and the object is recycled.
*/
if (op->enemy->map == NULL) {
CLEAR_FLAG(op, FLAG_SCARED);
op->enemy = NULL;
return;
}
if (!(random_roll(0, 4, op, PREFER_LOW)) && did_make_save(op, op->level, 0)) {
op->enemy = NULL;
CLEAR_FLAG(op, FLAG_SCARED);
return;
}
get_rangevector(op, op->enemy, &rv, 0);
dir = absdir(4+rv.direction);
for (diff = 0; diff < 3; diff++) {
int m = 1-(RANDOM()&2);
if (move_ob(op, absdir(dir+diff*m), op)
|| (diff == 0 && move_ob(op, absdir(dir-diff*m), op))) {
return;
}
}
/* Cornered, get rid of scared */
CLEAR_FLAG(op, FLAG_SCARED);
op->enemy = NULL;
}
/**
* Sees if there is stuff to be picked up/picks up stuff, for players only.
* @param op
* player to check for.
* @retval 1
* player should keep on moving.
* @retval 0
* player should stop.
*/
int check_pick(object *op) {
object *tmp, *next;
tag_t next_tag = 0, op_tag;
int stop = 0;
int j, k, wvratio;
char putstring[128], tmpstr[16];
/* if you're flying, you can't pick up anything */
if (op->move_type&MOVE_FLYING)
return 1;
op_tag = op->count;
next = op->below;
if (next)
next_tag = next->count;
/* loop while there are items on the floor that are not marked as
* destroyed */
while (next && !was_destroyed(next, next_tag)) {
tmp = next;
next = tmp->below;
if (next)
next_tag = next->count;
if (was_destroyed(op, op_tag))
return 0;
if (!can_pick(op, tmp))
continue;
if (op->contr->search_str[0] != '\0' && settings.search_items == TRUE) {
if (item_matched_string(op, tmp, op->contr->search_str))
pick_up(op, tmp);
continue;
}
/* high not bit set? We're using the old autopickup model */
if (!(op->contr->mode&PU_NEWMODE)) {
switch (op->contr->mode) {
case 0:
return 1; /* don't pick up */
case 1:
pick_up(op, tmp);
return 1;
case 2:
pick_up(op, tmp);
return 0;
case 3:
return 0; /* stop before pickup */
case 4:
pick_up(op, tmp);
break;
case 5:
pick_up(op, tmp);
stop = 1;
break;
case 6:
if (QUERY_FLAG(tmp, FLAG_KNOWN_MAGICAL)
&& !QUERY_FLAG(tmp, FLAG_KNOWN_CURSED))
pick_up(op, tmp);
break;
case 7:
if (tmp->type == MONEY || tmp->type == GEM)
pick_up(op, tmp);
break;
default:
/* use value density */
if (!QUERY_FLAG(tmp, FLAG_UNPAID)
&& (query_cost(tmp, op, F_TRUE)*100/(tmp->weight*MAX(tmp->nrof, 1))) >= op->contr->mode)
pick_up(op, tmp);
}
} else { /* old model */
/* NEW pickup handling */
if (op->contr->mode&PU_DEBUG) {
/* some debugging code to figure out item information */
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_DEBUG,
"item name: %s item type: %d weight/value: %d",
"item name: %s item type: %d weight/value: %d",
tmp->name ? tmp->name : tmp->arch->name, tmp->type,
(int)(query_cost(tmp, op, F_TRUE)*100/(tmp->weight*MAX(tmp->nrof, 1))));
snprintf(putstring, sizeof(putstring), "...flags: ");
for (k = 0; k < 4; k++) {
for (j = 0; j < 32; j++) {
if ((tmp->flags[k]>>j)&0x01) {
snprintf(tmpstr, sizeof(tmpstr), "%d ", k*32+j);
strcat(putstring, tmpstr);
}
}
}
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_SUCCESS,
putstring, putstring);
}
/* philosophy:
* It's easy to grab an item type from a pile, as long as it's
* generic. This takes no game-time. For more detailed pickups
* and selections, select-items should be used. This is a
* grab-as-you-run type mode that's really useful for arrows for
* example.
* The drawback: right now it has no frontend, so you need to
* stick the bits you want into a calculator in hex mode and then
* convert to decimal and then 'pickup <#>
*/
/* the first two modes are exclusive: if NOTHING we return, if
* STOP then we stop. All the rest are applied sequentially,
* meaning if any test passes, the item gets picked up. */
/* if mode is set to pick nothing up, return */
if (op->contr->mode&PU_NOTHING)
return 1;
/* if mode is set to stop when encountering objects, return.
* Take STOP before INHIBIT since it doesn't actually pick
* anything up */
if (op->contr->mode&PU_STOP)
return 0;
/* useful for going into stores and not losing your settings... */
/* and for battles wher you don't want to get loaded down while
* fighting */
if (op->contr->mode&PU_INHIBIT)
return 1;
/* prevent us from turning into auto-thieves :) */
if (QUERY_FLAG(tmp, FLAG_UNPAID))
continue;
/* ignore known cursed objects */
if (QUERY_FLAG(tmp, FLAG_KNOWN_CURSED) && op->contr->mode&PU_NOT_CURSED)
continue;
/* all food and drink if desired */
/* question: don't pick up known-poisonous stuff? */
if (op->contr->mode&PU_FOOD)
if (tmp->type == FOOD) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "FOOD\n");
continue;
}
if (op->contr->mode&PU_DRINK)
if (tmp->type == DRINK || (tmp->type == POISON && !QUERY_FLAG(tmp, FLAG_KNOWN_CURSED))) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "DRINK\n");
continue;
}
/* we don't forget dragon food */
if (op->contr->mode&PU_FLESH)
if (tmp->type == FLESH) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "FLESH\n");
continue;
}
if (op->contr->mode&PU_POTION)
if (tmp->type == POTION) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "POTION\n");
continue;
}
/* spellbooks, skillscrolls and normal books/scrolls */
if (op->contr->mode&PU_SPELLBOOK)
if (tmp->type == SPELLBOOK) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "SPELLBOOK\n");
continue;
}
if (op->contr->mode&PU_SKILLSCROLL)
if (tmp->type == SKILLSCROLL) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "SKILLSCROLL\n");
continue;
}
if (op->contr->mode&PU_READABLES)
if (tmp->type == BOOK || tmp->type == SCROLL) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "READABLES\n");
continue;
}
/* wands/staves/rods/horns */
if (op->contr->mode&PU_MAGIC_DEVICE)
if (tmp->type == WAND || tmp->type == ROD || tmp->type == HORN) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "MAGIC_DEVICE\n");
continue;
}
/* pick up all magical items */
if (op->contr->mode&PU_MAGICAL)
if (QUERY_FLAG(tmp, FLAG_KNOWN_MAGICAL) && !QUERY_FLAG(tmp, FLAG_KNOWN_CURSED)) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "MAGICAL\n");
continue;
}
if (op->contr->mode&PU_VALUABLES) {
if (tmp->type == MONEY || tmp->type == GEM) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "MONEY/GEM\n");
continue;
}
}
/* rings & amulets - talismans seems to be typed AMULET */
if (op->contr->mode&PU_JEWELS)
if (tmp->type == RING || tmp->type == EARRING || tmp->type == AMULET) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "JEWELS\n");
continue;
}
/* bows and arrows. Bows are good for selling! */
if (op->contr->mode&PU_BOW)
if (tmp->type == BOW) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "BOW\n");
continue;
}
if (op->contr->mode&PU_ARROW)
if (tmp->type == ARROW) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "ARROW\n");
continue;
}
/* all kinds of armor etc. */
if (op->contr->mode&PU_ARMOUR)
if (tmp->type == ARMOUR) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "ARMOUR\n");
continue;
}
if (op->contr->mode&PU_HELMET)
if (tmp->type == HELMET) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "HELMET\n");
continue;
}
if (op->contr->mode&PU_SHIELD)
if (tmp->type == SHIELD) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "SHIELD\n");
continue;
}
if (op->contr->mode&PU_BOOTS)
if (tmp->type == BOOTS) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "BOOTS\n");
continue;
}
if (op->contr->mode&PU_GLOVES)
if (tmp->type == GLOVES) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "GLOVES\n");
continue;
}
if (op->contr->mode&PU_CLOAK)
if (tmp->type == CLOAK) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "GLOVES\n");
continue;
}
/* hoping to catch throwing daggers here */
if (op->contr->mode&PU_MISSILEWEAPON)
if (tmp->type == WEAPON && QUERY_FLAG(tmp, FLAG_IS_THROWN)) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "MISSILEWEAPON\n");
continue;
}
/* careful: chairs and tables are weapons! */
if (op->contr->mode&PU_ALLWEAPON) {
if (tmp->type == WEAPON && tmp->name != NULL) {
if (strstr(tmp->name, "table") == NULL
&& strstr(tmp->arch->name, "table") == NULL
&& strstr(tmp->name, "chair")
&& strstr(tmp->arch->name, "chair") == NULL) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "WEAPON\n");
continue;
}
}
if (tmp->type == WEAPON && tmp->name == NULL) {
if (strstr(tmp->arch->name, "table") == NULL
&& strstr(tmp->arch->name, "chair") == NULL) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "WEAPON\n");
continue;
}
}
}
/* misc stuff that's useful */
if (op->contr->mode&PU_KEY)
if (tmp->type == KEY || tmp->type == SPECIAL_KEY) {
pick_up(op, tmp);
if (0)
fprintf(stderr, "KEY\n");
continue;
}
/* any of the last 4 bits set means we use the ratio for value
* pickups */
if (op->contr->mode&PU_RATIO) {
/* use value density to decide what else to grab.
* >=7 was >= op->contr->mode
* >=7 is the old standard setting. Now we take the last 4 bits
* and multiply them by 5, giving 0..15*5== 5..75 */
wvratio = (op->contr->mode&PU_RATIO)*5;
if ((query_cost(tmp, op, F_TRUE)*100/(tmp->weight*MAX(tmp->nrof, 1))) >= wvratio) {
pick_up(op, tmp);
continue;
}
}
} /* the new pickup model */
}
return !stop;
}
/**
* Find an arrow in the inventory and after that
* in the right type container (quiver). Pointer to the
* found object is returned.
*
* @param op
* object to find arrow for.
* @param type
* what arrow race to search for.
* @return
* suitable arrow, NULL if none found.
*/
static object *find_arrow(object *op, const char *type) {
object *tmp = NULL;
object *marked = NULL;
// @@ allow players to mark arrows or containers to use
marked = find_marked_object(op);
if (marked != NULL && marked->race == type) {
if (marked->type == ARROW) {
return marked;
} else if (marked->type == CONTAINER && QUERY_FLAG(marked, FLAG_APPLIED)) {
marked = find_arrow(marked, type);
if (marked != NULL) {
return marked;
}
}
}
for (op = op->inv; op; op = op->below)
if (!tmp
&& op->type == CONTAINER
&& op->race == type
&& QUERY_FLAG(op, FLAG_APPLIED)) {
tmp = find_arrow(op, type);
/* @@ if attacktype is 1, then it's an infinite container */
if (tmp != NULL && op->attacktype == 1) {
tmp = object_create_clone(tmp);
tmp->stats.food = 100; // always break
tmp->nrof = 1; // getting destroyed so weight doesn't matter (I think?)
}
} else if (op->type == ARROW && op->race == type) {
return op;
}
return tmp;
}
/**
* Similar to find_arrow(), but looks for (roughly) the best arrow to use
* against the target. A full test is not performed, simply a basic test
* of resistances. The archer is making a quick guess at what he sees down
* the hall. Failing that it does it's best to pick the highest plus arrow.
*
* @param op
* who to search arrows for.
* @param target
* what op is aiming at.
* @param type
* arrow race to search for.
* @param[out] better
* will contain the arrow's value if not NULL.
* @return
* suitable arrow, NULL if none found.
*/
static object *find_better_arrow(object *op, object *target, const char *type, int *better) {
object *tmp = NULL, *arrow, *ntmp;
int attacknum, attacktype, betterby = 0, i;
if (!type)
return NULL;
for (arrow = op->inv; arrow; arrow = arrow->below) {
if (arrow->type == CONTAINER
&& arrow->race == type
&& QUERY_FLAG(arrow, FLAG_APPLIED)) {
i = 0;
ntmp = find_better_arrow(arrow, target, type, &i);
if (i > betterby) {
tmp = ntmp;
betterby = i;
}
} else if (arrow->type == ARROW && arrow->race == type) {
/* allways prefer assasination/slaying */
if (target->race != NULL
&& arrow->slaying != NULL
&& strstr(arrow->slaying, target->race)) {
if (arrow->attacktype&AT_DEATH) {
if (better)
*better = 100;
return arrow;
} else {
tmp = arrow;
betterby = (arrow->magic+arrow->stats.dam)*2;
}
} else {
for (attacknum = 0; attacknum < NROFATTACKS; attacknum++) {
attacktype = 1<<attacknum;
if ((arrow->attacktype&attacktype) && (target->arch->clone.resist[attacknum]) < 0)
if (((arrow->magic+arrow->stats.dam)*(100-target->arch->clone.resist[attacknum])/100) > betterby) {
tmp = arrow;
betterby = (arrow->magic+arrow->stats.dam)*(100-target->arch->clone.resist[attacknum])/100;
}
}
if ((2+arrow->magic+arrow->stats.dam) > betterby) {
tmp = arrow;
betterby = 2+arrow->magic+arrow->stats.dam;
}
if (arrow->title && (1+arrow->magic+arrow->stats.dam) > betterby) {
tmp = arrow;
betterby = 1+arrow->magic+arrow->stats.dam;
}
}
}
}
if (tmp == NULL && arrow == NULL)
return find_arrow(op, type);
if (better)
*better = betterby;
return tmp;
}
/**
* Looks in a given direction, finds the first valid target, and calls
* find_better_arrow() to find a decent arrow to use.
* @param op
* shooter.
* @param type
* arrow's race to search for (the bow's usually).
* @param dir
* fire direction.
* @return
* suitable arrow, or NULL if none found.
*/
static object *pick_arrow_target(object *op, const char *type, int dir) {
object *tmp = NULL;
mapstruct *m;
int i, mflags, found, number;
sint16 x, y;
if (op->map == NULL)
return find_arrow(op, type);
/* do a dex check */
number = (die_roll(2, 40, op, PREFER_LOW)-2)/2;
if (number > (op->stats.Dex+(op->chosen_skill ? op->chosen_skill->level : op->level)))
return find_arrow(op, type);
m = op->map;
x = op->x;
y = op->y;
/* find the first target */
for (i = 0, found = 0; i < 20; i++) {
x += freearr_x[dir];
y += freearr_y[dir];
mflags = get_map_flags(m, &m, x, y, &x, &y);
if (mflags&P_OUT_OF_MAP || mflags&P_BLOCKSVIEW) {
tmp = NULL;
break;
} else if (GET_MAP_MOVE_BLOCK(m, x, y) == MOVE_FLY_LOW) {
/* This block presumes arrows and the like are MOVE_FLY_SLOW -
* perhaps a bad assumption.
*/
tmp = NULL;
break;
}
if (mflags&P_IS_ALIVE) {
for (tmp = GET_MAP_OB(m, x, y); tmp; tmp = tmp->above)
if (QUERY_FLAG(tmp, FLAG_ALIVE)) {
found++;
break;
}
if (found)
break;
}
}
if (tmp == NULL)
return find_arrow(op, type);
if (tmp->head)
tmp = tmp->head;
return find_better_arrow(op, tmp, type, NULL);
}
/**
* Creature (monster or player) fires a bow.
*
* @param op
* object firing the bow.
* @param arrow
* object to fire.
* @param dir
* direction of fire.
* @param wc_mod
* any special modifier to give (used in special player fire modes)
* @param sx
* @param sy
* coordinates to fire arrow from - also used in some of the special player fire modes.
* @return
* 1 if bow was actually fired, 0 otherwise.
* @todo describe player firing modes.
*/
int fire_bow(object *op, object *arrow, int dir, int wc_mod, sint16 sx, sint16 sy) {
object *left, *bow;
tag_t left_tag, tag;
int bowspeed, mflags;
mapstruct *m;
if (!dir) {
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
"You can't shoot yourself!", NULL);
return 0;
}
if (op->type == PLAYER)
bow = op->contr->ranges[range_bow];
else {
for (bow = op->inv; bow; bow = bow->below)
/* Don't check for applied - monsters don't apply bows - in that way, they
* don't need to switch back and forth between bows and weapons.
*/
if (bow->type == BOW)
break;
if (!bow) {
LOG(llevError, "Range: bow without activated bow (%s).\n", op->name);
return 0;
}
}
if (!bow->race || !bow->skill) {
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
"Your %s is broken.",
"Your %s is broken.",
bow->name);
return 0;
}
bowspeed = bow->stats.sp+dex_bonus[op->stats.Dex];
/* penalize ROF for bestarrow */
if (op->type == PLAYER && op->contr->bowtype == bow_bestarrow)
bowspeed -= dex_bonus[op->stats.Dex]+5;
if (bowspeed < 1)
bowspeed = 1;
if (arrow == NULL) {
if ((arrow = find_arrow(op, bow->race)) == NULL) {
if (op->type == PLAYER)
draw_ext_info_format(NDI_UNIQUE, 0, op,
MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
"You have no %s left.",
"You have no %s left.",
bow->race);
/* FLAG_READY_BOW will get reset if the monsters picks up some arrows */
else
CLEAR_FLAG(op, FLAG_READY_BOW);
return 0;
}
}
mflags = get_map_flags(op->map, &m, sx, sy, &sx, &sy);
if (mflags&P_OUT_OF_MAP) {
return 0;
}
if (GET_MAP_MOVE_BLOCK(m, sx, sy)&MOVE_FLY_LOW) {
return 0;
}
/* this should not happen, but sometimes does */
if (arrow->nrof == 0) {
remove_ob(arrow);
free_object(arrow);
return 0;
}
left = arrow; /* these are arrows left to the player */
/* BUG? The value in left_tag doesn't seem to be used. */
left_tag = left->count;
arrow = get_split_ob(arrow, 1, NULL, 0);
if (arrow == NULL) {
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
"You have no %s left.",
"You have no %s left.",
bow->race);
return 0;
}
set_owner(arrow, op);
if (arrow->skill)
free_string(arrow->skill);
arrow->skill = add_refcount(bow->skill);
arrow->direction = dir;
arrow->x = sx;
arrow->y = sy;
if (op->type == PLAYER) {
if (op->contr->bowtype == bow_precise) {
op->speed_left -= (float)FABS(op->speed * 6.0f)*100/bowspeed;
} else {
op->speed_left = 0.01-(float)FABS(op->speed)*100/bowspeed;
}
fix_object(op);
}
SET_ANIMATION(arrow, arrow->direction);
arrow->stats.sp = arrow->stats.wc; /* save original wc and dam */
arrow->stats.hp = arrow->stats.dam;
arrow->stats.grace = arrow->attacktype;
if (arrow->slaying != NULL)
arrow->spellarg = strdup_local(arrow->slaying);
/* Note that this was different for monsters - they got their level
* added to the damage. I think the strength bonus is more proper.
*/
arrow->stats.dam += (QUERY_FLAG(bow, FLAG_NO_STRENGTH) ? 0 : dam_bonus[op->stats.Str])
+bow->stats.dam
+bow->magic
+arrow->magic;
/* update the speed */
arrow->speed = (float)((QUERY_FLAG(bow, FLAG_NO_STRENGTH) ? 0 : dam_bonus[op->stats.Str])+bow->magic+arrow->magic)/5.0
+(float)bow->stats.dam/7.0;
if (arrow->speed < 1.0)
arrow->speed = 1.0;
update_ob_speed(arrow);
arrow->speed_left = 0;
if (op->type == PLAYER) {
/* we don't want overflows of wc (sint), so cap the value - mod and pl should be substracted */
int mod = bow->magic
+arrow->magic
+dex_bonus[op->stats.Dex]
+thaco_bonus[op->stats.Str]
+arrow->stats.wc
-wc_mod;
/* adjust for precise shot */
if (op->contr->bowtype == bow_precise) {
arrow->stats.dam *= 3.0;
mod += (dex_bonus[op->stats.Dex] + thaco_bonus[op->stats.Str] + bow->stats.wc) * 0.25;
}
int plmod = (op->chosen_skill ? op->chosen_skill->level : op->level);
if (plmod+mod > 140)
plmod = 140-mod;
else if (plmod+mod < -100)
plmod = -100-mod;
arrow->stats.wc = 20-(sint8)plmod-(sint8)mod;
arrow->level = op->chosen_skill ? op->chosen_skill->level : op->level;
} else {
arrow->stats.wc = op->stats.wc
-bow->magic
-arrow->magic
-arrow->stats.wc
+wc_mod;
arrow->level = op->level;
}
if (arrow->attacktype == AT_PHYSICAL)
arrow->attacktype |= bow->attacktype;
if (bow->slaying != NULL)
arrow->slaying = add_string(bow->slaying);
arrow->map = m;
/* If move_type is ever changed, monster.c:monster_use_bow() needs to be changed too. */
arrow->move_type = MOVE_FLY_LOW;
arrow->move_on = MOVE_FLY_LOW|MOVE_WALK;
tag = arrow->count;
insert_ob_in_map(arrow, m, op, 0);
if (!was_destroyed(arrow, tag)) {
play_sound_map(SOUND_TYPE_ITEM, arrow, arrow->direction, "fire");
ob_process(arrow);
}
return 1;
}
/**
* Special fire code for players - this takes into
* account the special fire modes players can have
* but monsters can't. Putting that code here
* makes the fire_bow() code much cleaner.
*
* This function should only be called if 'op' is a player,
* hence the function name.
*
* @param op
* player.
* @param dir
* firing direction.
* @return
* 1 if arrow was fired, 0 else.
*/
static int player_fire_bow(object *op, int dir) {
int ret = 0, wcmod = 0;
if (op->contr->bowtype == bow_bestarrow) {
ret = fire_bow(op, pick_arrow_target(op, op->contr->ranges[range_bow]->race, dir), dir, 0, op->x, op->y);
} else if (op->contr->bowtype >= bow_n && op->contr->bowtype <= bow_nw) {
if (!similar_direction(dir, op->contr->bowtype-bow_n+1))
wcmod = -1;
ret = fire_bow(op, NULL, op->contr->bowtype-bow_n+1, wcmod, op->x, op->y);
} else if (op->contr->bowtype == bow_threewide) {
ret = fire_bow(op, NULL, dir, 0, op->x, op->y);
ret |= fire_bow(op, NULL, dir, -5, op->x+freearr_x[absdir(dir+2)], op->y+freearr_y[absdir(dir+2)]);
ret |= fire_bow(op, NULL, dir, -5, op->x+freearr_x[absdir(dir-2)], op->y+freearr_y[absdir(dir-2)]);
} else if (op->contr->bowtype == bow_spreadshot) {
ret |= fire_bow(op, NULL, dir, 0, op->x, op->y);
ret |= fire_bow(op, NULL, absdir(dir-1), -5, op->x, op->y);
ret |= fire_bow(op, NULL, absdir(dir+1), -5, op->x, op->y);
} else {
/* Simple case */
ret = fire_bow(op, NULL, dir, 0, op->x, op->y);
}
return ret;
}
/**
* Fires a misc (wand/rod/horn) object in 'dir'.
* Broken apart from 'fire' to keep it more readable.
*
* @param op
* player firing.
* @param dir
* firing direction.
*
* @warning
* op must be a player (contr != NULL).
*/
static void fire_misc_object(object *op, int dir) {
object *item;
char name[MAX_BUF];
if (!op->contr->ranges[range_misc]) {
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
"You have no range item readied.", NULL);
return;
}
item = op->contr->ranges[range_misc];
if (!item->inv) {
LOG(llevError, "Object %s lacks a spell\n", item->name);
return;
}
if (item->type == WAND) {
if (item->stats.food <= 0) {
play_sound_player_only(op->contr, SOUND_TYPE_ITEM, item, 0, "poof");
query_base_name(item, 0, name, MAX_BUF);
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_FAILURE,
"The %s goes poof.",
"The %s goes poof.",
name);
return;
}
} else if (item->type == ROD || item->type == HORN) {
if (item->stats.hp < SP_level_spellpoint_cost(item, item->inv, SPELL_HIGHEST) && item->stats.maxhp >= SP_level_spellpoint_cost(item, item->inv, SPELL_HIGHEST)) {
play_sound_player_only(op->contr, SOUND_TYPE_ITEM, item, 0, "poof");
query_base_name(item, 0, name, MAX_BUF);
if (item->type == ROD)
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_FAILURE,
"The %s whines for a while, but nothing happens.",
"The %s whines for a while, but nothing happens.",
name);
else
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_FAILURE,
"The %s needs more time to charge.",
"The %s needs more time to charge.",
name);
return;
}
}
if (cast_spell(op, item, dir, item->inv, NULL)) {
SET_FLAG(op, FLAG_BEEN_APPLIED); /* You now know something about it */
if (item->type == WAND) {
drain_wand_charge(item);
} else if (item->type == ROD || item->type == HORN) {
drain_rod_charge(item);
}
}
}
/**
* Received a fire command for the player - go and do it.
*
* @param op
* player.
* @param dir
* direction to fire into.
*/
void fire(object *op, int dir) {
int spellcost = 0;
/* check for loss of invisiblity/hide */
if (action_makes_visible(op))
make_visible(op);
switch (op->contr->shoottype) {
case range_none:
return;
case range_bow:
player_fire_bow(op, dir);
return;
case range_magic: /* Casting spells */
/* BUG? The value in spellcost is never used again it seems. */
spellcost = (cast_spell(op, op, dir, op->contr->ranges[range_magic], op->contr->spellparam[0] ? op->contr->spellparam : NULL));
return;
case range_misc:
fire_misc_object(op, dir);
return;
case range_golem: /* Control summoned monsters from scrolls */
if (op->contr->ranges[range_golem] == NULL
|| op->contr->golem_count != op->contr->ranges[range_golem]->count) {
op->contr->ranges[range_golem] = NULL;
op->contr->shoottype = range_none;
op->contr->golem_count = 0;
} else
control_golem(op->contr->ranges[range_golem], dir);
return;
case range_skill:
if (!op->chosen_skill) {
if (op->type == PLAYER)
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
"You have no applicable skill to use.", NULL);
return;
}
(void)do_skill(op, op, op->chosen_skill, dir, NULL);
return;
case range_builder:
apply_map_builder(op, dir);
return;
default:
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_COMMAND, MSG_TYPE_COMMAND_ERROR,
"Illegal shoot type.", NULL);
return;
}
}
/**
* We try to find a key for the door as passed. If we find a key
* and player can use it (based on the usekeys settings), we return the key, otherwise NULL.
*
* This function merges both normal and locked door, since the logic
* for both is the same - just the specific key is different.
*
* This function can be called recursively to search containers.
*
* @param pl
* player.
* @param container
* inventory to searched for keys.
* @param door
* door we are trying to match against.
* @return
* key to use, NULL if none found or usekeys mode doesn't let reach the key.
* @todo document use key modes.
*/
object *find_key(object *pl, object *container, object *door) {
object *tmp, *key;
/* Should not happen, but sanity checking is never bad */
if (container->inv == NULL)
return NULL;
/* First, lets try to find a key in the top level inventory */
for (tmp = container->inv; tmp != NULL; tmp = tmp->below) {
if (door->type == DOOR && tmp->type == KEY)
break;
/* For sanity, we should really check door type, but other stuff
* (like containers) can be locked with special keys
*/
if (tmp->slaying
&& tmp->type == SPECIAL_KEY
&& tmp->slaying == door->slaying)
break;
}
/* No key found - lets search inventories now */
/* If we find and use a key in an inventory, return at that time.
* otherwise, if we search all the inventories and still don't find
* a key, return
*/
if (!tmp) {
for (tmp = container->inv; tmp != NULL; tmp = tmp->below) {
/* No reason to search empty containers */
if (tmp->type == CONTAINER && tmp->inv) {
if ((key = find_key(pl, tmp, door)) != NULL)
return key;
}
}
if (!tmp)
return NULL;
}
/* We get down here if we have found a key. Now if its in a container,
* see if we actually want to use it
*/
if (pl != container) {
/* Only let players use keys in containers */
if (!pl->contr)
return NULL;
/* cases where this fails:
* If we only search the player inventory, return now since we
* are not in the players inventory.
* If the container is not active, return now since only active
* containers can be used.
* If we only search keyrings and the container does not have
* a race/isn't a keyring.
* No checking for all containers - to fall through past here,
* inv must have been an container and must have been active.
*
* Change the color so that the message doesn't disappear with
* all the others.
*/
if (pl->contr->usekeys == key_inventory
|| !QUERY_FLAG(container, FLAG_APPLIED)
|| (pl->contr->usekeys == keyrings && (!container->race || strcmp(container->race, "keys")))) {
char name_tmp[MAX_BUF], name_cont[MAX_BUF];
query_name(tmp, name_tmp, MAX_BUF);
query_name(container, name_cont, MAX_BUF);
draw_ext_info_format(NDI_UNIQUE|NDI_BROWN, 0, pl,
MSG_TYPE_ITEM, MSG_TYPE_ITEM_INFO,
"The %s in your %s vibrates as you approach the door",
"The %s in your %s vibrates as you approach the door",
name_tmp, name_cont);
return NULL;
}
}
return tmp;
}
/**
* Player is "attacking" a door. Will try to open it with a key, or warn if can't open it.
*
* Moved out of move_player_attack().
*
* @retval 1
* player has opened the door with a key such that the caller should not do anything more.
* @retval 0
* nothing happened.
*/
static int player_attack_door(object *op, object *door) {
/* If its a door, try to find a use a key. If we do destroy the door,
* might as well return immediately as there is nothing more to do -
* otherwise, we fall through to the rest of the code.
*/
object *key = find_key(op, op, door);
/* IF we found a key, do some extra work */
if (key) {
object *container = key->env;
play_sound_map(SOUND_TYPE_GROUND, door, 0, "open");
if (action_makes_visible(op))
make_visible(op);
if (door->inv && (door->inv->type == RUNE || door->inv->type == TRAP))
spring_trap(door->inv, op);
if (door->type == DOOR) {
hit_player(door, 9998, op, AT_PHYSICAL, 1); /* Break through the door */
} else if (door->type == LOCKED_DOOR) {
char name[HUGE_BUF];
query_short_name(key, name, HUGE_BUF);
draw_ext_info_format(NDI_UNIQUE, NDI_BROWN, op,
MSG_TYPE_ITEM, MSG_TYPE_ITEM_REMOVE,
"You open the door with the %s",
"You open the door with the %s",
name);
remove_locked_door(door); /* remove door without violence ;-) */
}
/* Do this after we print the message */
decrease_ob(key); /* Use up one of the keys */
/* Need to update the weight the container the key was in */
if (container != op)
esrv_update_item(UPD_WEIGHT, op, container);
return 1; /* Nothing more to do below */
} else if (door->type == LOCKED_DOOR) {
/* Might as well return now - no other way to open this */
draw_ext_info(NDI_UNIQUE|NDI_NAVY, 0, op, MSG_TYPE_ATTACK, MSG_TYPE_ATTACK_NOKEY,
door->msg, door->msg);
return 1;
}
return 0;
}
/**
* The player is also actually going to try and move (not fire weapons).
*
* This function is just part of a breakup from move_player().
* It should keep the code cleaner.
* When this is called, the players direction has been updated
* (taking into account confusion).
*
* @param op
* player moving.
* @param dir
* moving direction.
*/
void move_player_attack(object *op, int dir) {
object *tmp, *mon, *tpl, *mon_owner;
sint16 nx, ny;
int on_battleground;
mapstruct *m;
if (op->contr->transport)
tpl = op->contr->transport;
else
tpl = op;
nx = freearr_x[dir]+tpl->x;
ny = freearr_y[dir]+tpl->y;
on_battleground = op_on_battleground(tpl, NULL, NULL, NULL);
/* If braced, or can't move to the square, and it is not out of the
* map, attack it. Note order of if statement is important - don't
* want to be calling move_ob if braced, because move_ob will move the
* player. This is a pretty nasty hack, because if we could
* move to some space, it then means that if we are braced, we should
* do nothing at all. As it is, if we are braced, we go through
* quite a bit of processing. However, it probably is less than what
* move_ob uses.
*/
if ((op->contr->braced || !move_ob(tpl, dir, tpl)) && !out_of_map(tpl->map, nx, ny)) {
if (OUT_OF_REAL_MAP(tpl->map, nx, ny)) {
m = get_map_from_coord(tpl->map, &nx, &ny);
if (!m)
return; /* Don't think this should happen */
} else
m = tpl->map;
if ((tmp = GET_MAP_OB(m, nx, ny)) == NULL) {
/* LOG(llevError, "player_move_attack: GET_MAP_OB returns NULL, but player can not more there.\n");*/
return;
}
mon = NULL;
/* Go through all the objects, and find ones of interest. Only stop if
* we find a monster - that is something we know we want to attack.
* if its a door or barrel (can roll) see if there may be monsters
* on the space
*/
while (tmp != NULL) {
if (tmp == op) {
tmp = tmp->above;
continue;
}
if (QUERY_FLAG(tmp, FLAG_ALIVE)) {
mon = tmp;
/* Gros: Objects like (pass-through) doors are alive, but haven't
* their monster flag set - so this is a good way attack real
* monsters in priority.
*/
if (QUERY_FLAG(tmp, FLAG_MONSTER))
break;
}
if (tmp->type == LOCKED_DOOR || QUERY_FLAG(tmp, FLAG_CAN_ROLL))
mon = tmp;
tmp = tmp->above;
}
if (mon == NULL) /* This happens anytime the player tries to move */
return; /* into a wall */
if (mon->head != NULL)
mon = mon->head;
if ((mon->type == DOOR && mon->stats.hp >= 0) || (mon->type == LOCKED_DOOR))
if (player_attack_door(op, mon))
return;
/* The following deals with possibly attacking peaceful
* or frienddly creatures. Basically, all players are considered
* unaggressive. If the moving player has peaceful set, then the
* object should be pushed instead of attacked. It is assumed that
* if you are braced, you will not attack friends accidently,
* and thus will not push them.
*/
/* If the creature is a pet, push it even if the player is not
* peaceful. Our assumption is the creature is a pet if the
* player owns it and it is either friendly or unagressive.
*/
mon_owner = get_owner(mon);
if ((op->type == PLAYER)
&& (mon_owner == op || (mon_owner != NULL && mon_owner->type == PLAYER && mon_owner->contr->party != NULL && mon_owner->contr->party == op->contr->party))
&& (QUERY_FLAG(mon, FLAG_UNAGGRESSIVE) || QUERY_FLAG(mon, FLAG_FRIENDLY))) {
/* If we're braced, we don't want to switch places with it */
if (op->contr->braced)
return;
play_sound_map(SOUND_TYPE_LIVING, mon, dir, "push");
(void)push_ob(mon, dir, op);
if (op->contr->tmp_invis || op->hide)
make_visible(op);
return;
}
/* in certain circumstances, you shouldn't attack friendly
* creatures. Note that if you are braced, you can't push
* someone, but put it inside this loop so that you won't
* attack them either.
*/
if ((mon->type == PLAYER || mon->enemy != op)
&& (mon->type == PLAYER || QUERY_FLAG(mon, FLAG_UNAGGRESSIVE) || QUERY_FLAG(mon, FLAG_FRIENDLY))
&& (op->contr->peaceful && !on_battleground)) {
if (!op->contr->braced) {
play_sound_map(SOUND_TYPE_LIVING, mon, dir, "push");
(void)push_ob(mon, dir, op);
} else {
draw_ext_info(0, 0, op, MSG_TYPE_ATTACK, MSG_TYPE_ATTACK_NOATTACK,
"You withhold your attack", NULL);
}
if (op->contr->tmp_invis || op->hide)
make_visible(op);
}
/* If the object is a boulder or other rollable object, then
* roll it if not braced. You can't roll it if you are braced.
*/
else if (QUERY_FLAG(mon, FLAG_CAN_ROLL) && (!op->contr->braced)) {
recursive_roll(mon, dir, op);
if (action_makes_visible(op))
make_visible(op);
/* Any generic living creature. Including things like doors.
* Way it works is like this: First, it must have some hit points
* and be living. Then, it must be one of the following:
* 1) Not a player, 2) A player, but of a different party. Note
* that party_number -1 is no party, so attacks can still happen.
*/
} else if ((mon->stats.hp >= 0)
&& QUERY_FLAG(mon, FLAG_ALIVE)
&& ((mon->type != PLAYER || op->contr->party == NULL || op->contr->party != mon->contr->party))) {
/* If the player hasn't hit something this tick, and does
* so, give them speed boost based on weapon speed. Doing
* it here is better than process_players2, which basically
* incurred a 1 tick offset.
*/
if (!op->contr->has_hit) {
op->speed_left += op->speed / op->contr->weapon_sp;
op->contr->has_hit = 1; /* The last action was to hit, so use weapon_sp */
}
skill_attack(mon, op, 0, NULL, NULL);
/* If attacking another player, that player gets automatic
* hitback, and doesn't loose luck either.
* Disable hitback on the battleground or if the target is
* the wiz.
*/
if (mon->type == PLAYER
&& mon->stats.hp >= 0
&& !mon->contr->has_hit
&& !on_battleground
&& !QUERY_FLAG(mon, FLAG_WIZ)) {
short luck = mon->stats.luck;
mon->contr->has_hit = 1;
skill_attack(op, mon, 0, NULL, NULL);
mon->stats.luck = luck;
}
if (action_makes_visible(op))
make_visible(op);
}
} /* if player should attack something */
}
/**
* Update the move_type of a transport based on the direction. The transport MUST be square.
* Depending on the direction, the right column of tiles or the bottom line of tiles will have a move_type of 0.
* @param transport what to update.
* @param dir direction to update flags for.
*/
static void update_transport_block(object *transport, int dir) {
object *part;
int sx, sy, x, y;
get_multi_size(transport, &sx, &sy, NULL, NULL);
assert(sx == sy);
if (dir == 1 || dir == 5) {
part = transport;
for (y = 0; y <= sy; y++) {
for (x = 0; x < sx; x++) {
part->move_type = transport->move_type;
part = part->more;
}
part->move_type = 0;
part = part->more;
}
} else if (dir == 3 || dir == 7) {
part = transport;
for (y = 0; y < sy; y++) {
for (x = 0; x <= sx; x++) {
part->move_type = transport->move_type;
part = part->more;
}
}
while (part) {
part->move_type = 0;
part = part->more;
}
} else {
for (part = transport; part; part = part->more) {
part->move_type = transport->move_type;
}
}
}
/**
* Turn a transport to an adjacent direction (+1 or -1), updating the move_type flags in the same process.
* @param transport what to turn. Must be of type TRANSPORT.
* @param captain who wants to turn the boat.
* @param dir direction to turn to.
* @return
* - 1 if the transport turned (so can't move anymore this tick)
* - 2 if the transport couldn't turn
*/
static int turn_one_transport(object *transport, object *captain, int dir) {
int x, y, scroll_dir = 0;
assert(transport->type == TRANSPORT);
x = transport->x;
y = transport->y;
if (transport->direction == 1 && dir == 8) {
x--;
} else if (transport->direction == 2 && dir == 3) {
y++;
} else if (transport->direction == 3 && dir == 2) {
y--;
} else if (transport->direction == 5 && dir == 6) {
x--;
} else if (transport->direction == 6 && dir == 5) {
x++;
} else if (transport->direction == 7 && dir == 8) {
y--;
} else if (transport->direction == 8 && dir == 7) {
y++;
} else if (transport->direction == 8 && dir == 1) {
x++;
}
update_transport_block(transport, dir);
remove_ob(transport);
if (ob_blocked(transport, transport->map, x, y)) {
update_transport_block(transport, transport->direction);
insert_ob_in_map(transport, transport->map, NULL, 0);
return 2;
}
if (x != transport->x || y != transport->y) {
object *pl;
/* assert(scroll_dir != 0);*/
for (pl = transport->inv; pl; pl = pl->below) {
if (pl->type == PLAYER) {
pl->contr->do_los = 1;
pl->map = transport->map;
pl->x = x;
pl->y = y;
esrv_map_scroll(&pl->contr->socket, freearr_x[scroll_dir], freearr_y[scroll_dir]);
pl->contr->socket.update_look = 1;
pl->contr->socket.look_position = 0;
}
}
}
insert_ob_in_map_at(transport, transport->map, NULL, 0, x, y);
transport->direction = dir;
transport->facing = dir;
animate_object(transport, dir);
captain->direction = dir;
return 1;
}
/**
* Try to turn a transport in the desired direction.
* This takes into account transports that turn and don't occupy the same space depending on the direction it is facing.
* The transport MUST be a square for it to turn correctly when adjusting tile occupation.
* @param transport what to turn. Must be of type TRANSPORT.
* @param captain who wants to turn the boat.
* @param dir direction to turn to.
* @return
* - 0 if transport is in the right direction
* - 1 if the transport turned (so can't move anymore this tick)
* - 2 if the transport couldn't turn
*/
static int turn_transport(object *transport, object *captain, int dir) {
assert(transport->type == TRANSPORT);
if (get_ob_key_value(transport, "turnable_transport") == NULL) {
transport->direction = dir;
transport->facing = dir;
animate_object(transport, dir);
captain->direction = dir;
return 0;
}
if (transport->direction == dir)
return 0;
if (absdir(transport->direction-dir) > 2)
return turn_one_transport(transport, captain, absdir(transport->direction+1));
else
return turn_one_transport(transport, captain, absdir(transport->direction-1));
}
/**
* Player gave us a direction, check whether to move or fire.
*
* @param op
* player.
* @param dir
* direction to move/fire.
* @return
* 0.
*/
int move_player(object *op, int dir) {
int pick;
object *transport = op->contr->transport;
if (!transport && (op->map == NULL || op->map->in_memory != MAP_IN_MEMORY))
return 0;
/* Sanity check: make sure dir is valid */
if ((dir < 0) || (dir >= 9)) {
LOG(llevError, "move_player: invalid direction %d\n", dir);
return 0;
}
/* peterm: added following line */
if (QUERY_FLAG(op, FLAG_CONFUSED) && dir)
dir = absdir(dir+RANDOM()%3+RANDOM()%3-2);
op->facing = dir;
if (!transport && op->hide)
do_hidden_move(op);
if (transport) {
int turn;
/* transport->contr is set up for the person in charge of the boat.
* if that isn't this person, he can't steer it, etc
*/
if (transport->contr != op->contr)
return 0;
/* Transport can't move. But update dir so it at least
* will point in the same direction if player is running.
*/
if (transport->speed_left < 0.0) {
return 0;
}
/* Remove transport speed. Give player just a little speed -
* enough so that they will get an action again quickly.
*/
transport->speed_left -= 1.0;
if (op->speed_left < 0.0)
op->speed_left = -0.01;
turn = turn_transport(transport, op, dir);
if (turn != 0)
return 0;
} else {
/* it is important to change the animation now, as fire or move_player_attack can start a compound animation,
* and leave us with state = 0, which we don't want to change again. */
op->state++; /* player moved, so change animation. */
animate_object(op, op->facing);
}
if (op->contr->fire_on) {
fire(op, dir);
} else
move_player_attack(op, dir);
pick = check_pick(op);
/* Add special check for newcs players and fire on - this way, the
* server can handle repeat firing.
*/
if (op->contr->fire_on || (op->contr->run_on && pick != 0)) {
op->direction = dir;
} else {
op->direction = 0;
}
return 0;
}
/**
* Handles commands the player can send us, and various checks on
* invisibility, golem and such.
*
* This is sort of special, in that the new client/server actually uses
* the new speed values for commands.
*
* @param op
* player to handle.
* @return
* true if there are more actions we can do.
*/
int handle_newcs_player(object *op) {
if (op->contr->hidden) {
op->invisible = 1000;
/* the socket code flasehs the player visible/invisible
* depending on the value if invisible, so we need to
* alternate it here for it to work correctly.
*/
if (pticks&2)
op->invisible--;
} else if (op->invisible && !(QUERY_FLAG(op, FLAG_MAKE_INVIS))) {
op->invisible--;
if (!op->invisible) {
make_visible(op);
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_SPELL, MSG_TYPE_SPELL_END,
"Your invisibility spell runs out.", NULL);
}
}
if (QUERY_FLAG(op, FLAG_SCARED)) {
flee_player(op);
/* If player is still scared, that is his action for this tick */
if (QUERY_FLAG(op, FLAG_SCARED)) {
op->speed_left--;
return 0;
}
}
/* I've been seeing crashes where the golem has been destroyed, but
* the player object still points to the defunct golem. The code that
* destroys the golem looks correct, and it doesn't always happen, so
* put this in a a workaround to clean up the golem pointer.
*/
if (op->contr->ranges[range_golem]
&& ((op->contr->golem_count != op->contr->ranges[range_golem]->count) || QUERY_FLAG(op->contr->ranges[range_golem], FLAG_REMOVED))) {
op->contr->ranges[range_golem] = NULL;
op->contr->golem_count = 0;
}
/* call this here - we also will call this in do_ericserver, but
* the players time has been increased when doericserver has been
* called, so we recheck it here.
*/
handle_client(&op->contr->socket, op->contr);
if (op->speed_left < 0)
return 0;
if (op->direction && (op->contr->run_on || op->contr->fire_on)) {
/* All move commands take 1 tick, at least for now */
op->speed_left--;
/* Instead of all the stuff below, let move_player take care
* of it. Also, some of the skill stuff is only put in
* there, as well as the confusion stuff.
*/
move_player(op, op->direction);
if (op->speed_left > 0)
return 1;
else
return 0;
}
return 0;
}
/**
* Can the player be saved by an item?
*
* @param op
* player to try to save.
* @retval 1
* player had his life saved by an item, first item saving life is removed.
* @retval 0
* player had no life-saving item.
*/
static int save_life(object *op) {
object *tmp;
if (!QUERY_FLAG(op, FLAG_LIFESAVE))
return 0;
for (tmp = op->inv; tmp != NULL; tmp = tmp->below)
if (QUERY_FLAG(tmp, FLAG_APPLIED) && QUERY_FLAG(tmp, FLAG_LIFESAVE)) {
char name[MAX_BUF];
query_name(tmp, name, MAX_BUF);
play_sound_map(SOUND_TYPE_ITEM, tmp, 0, "evaporate");
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_ITEM, MSG_TYPE_ITEM_REMOVE,
"Your %s vibrates violently, then evaporates.",
"Your %s vibrates violently, then evaporates.",
name);
remove_ob(tmp);
free_object(tmp);
CLEAR_FLAG(op, FLAG_LIFESAVE);
if (op->stats.hp < 0)
op->stats.hp = op->stats.maxhp;
if (op->stats.food < 0)
op->stats.food = 999;
fix_object(op);
return 1;
}
LOG(llevError, "Error: LIFESAVE set without applied object.\n");
CLEAR_FLAG(op, FLAG_LIFESAVE);
enter_player_savebed(op); /* bring him home. */
return 0;
}
/**
* This goes throws the inventory and removes unpaid objects, and puts them
* back in the map (location and map determined by values of env) or frees them. This
* function will descend into containers.
*
* @param op
* object to start the search from.
* @param env
* top-level container, should be in a map if free_items is 0, unused if free_items is 1.
* @param free_items
* if set, unpaid items are freed, else they are inserted in the same map as env.
*/
void remove_unpaid_objects(object *op, object *env, int free_items) {
object *next;
while (op) {
next = op->below; /* Make sure we have a good value, in case
* we remove object 'op'
*/
if (QUERY_FLAG(op, FLAG_UNPAID)) {
remove_ob(op);
if (free_items)
free_object(op);
else {
op->x = env->x;
op->y = env->y;
insert_ob_in_map(op, env->map, NULL, 0);
}
} else if (op->inv)
remove_unpaid_objects(op->inv, env, free_items);
op = next;
}
}
/**
* Create a text for a player's gravestobe.
*
* Moved from apply.c to player.c - player.c is what
* actually uses this function. player.c may not be quite the
* best, a misc file for object actions is probably better,
* but there isn't one in the server directory.
*
* @param op
* player.
* @param buf2
* buffer to write the text to. Mustn't be NULL.
* @param len
* length of buf2.
* @return
* buf2, containing gravestone text.
*/
static const char *gravestone_text(object *op, char *buf2, int len) {
char buf[MAX_BUF];
time_t now = time(NULL);
strncpy(buf2, " R.I.P.\n\n", len);
if (op->type == PLAYER)
snprintf(buf, sizeof(buf), "%s the %s\n", op->name, op->contr->title);
else
snprintf(buf, sizeof(buf), "%s\n", op->name);
strncat(buf2, " ", 20-strlen(buf)/2);
strncat(buf2, buf, len-strlen(buf2)-1);
if (op->type == PLAYER)
snprintf(buf, sizeof(buf), "who was in level %d when killed\n", op->level);
else
snprintf(buf, sizeof(buf), "who was in level %d when died.\n\n", op->level);
strncat(buf2, " ", 20-strlen(buf)/2);
strncat(buf2, buf, len-strlen(buf2)-1);
if (op->type == PLAYER) {
snprintf(buf, sizeof(buf), "by %s.\n\n", op->contr->killer);
strncat(buf2, " ", 21-strlen(buf)/2);
strncat(buf2, buf, len-strlen(buf2)-1);
}
strftime(buf, MAX_BUF, "%b %d %Y\n", localtime(&now));
strncat(buf2, " ", 20-strlen(buf)/2);
strncat(buf2, buf, len-strlen(buf2)-1);
return buf2;
}
/**
* Regenerate hp/sp/gr, decreases food. This only works for players.
* Will grab food if needed, or kill player.
*
* @param op
* player to regenerate for.
*/
void do_some_living(object *op) {
int last_food = op->stats.food;
int gen_hp, gen_sp, gen_grace;
int over_hp, over_sp, over_grace;
int i;
int rate_hp = 1200;
int rate_sp = 2500;
int rate_grace = 2000;
const int max_hp = 1;
const int max_sp = 1;
const int max_grace = 1;
if (op->contr->outputs_sync) {
for (i = 0; i < NUM_OUTPUT_BUFS; i++)
if (op->contr->outputs[i].buf != NULL
&& (op->contr->outputs[i].first_update+op->contr->outputs_sync) < pticks)
flush_output_element(op, &op->contr->outputs[i]);
}
if (op->contr->state == ST_PLAYING) {
/* these next three if clauses make it possible to SLOW DOWN
hp/grace/spellpoint regeneration. */
if (op->contr->gen_hp >= 0)
gen_hp = (op->contr->gen_hp+1)*op->stats.maxhp;
else {
gen_hp = op->stats.maxhp;
rate_hp -= rate_hp/2*op->contr->gen_hp;
}
if (op->contr->gen_sp >= 0)
gen_sp = (op->contr->gen_sp+1)*op->stats.maxsp;
else {
gen_sp = op->stats.maxsp;
rate_sp -= rate_sp/2*op->contr->gen_sp;
}
if (op->contr->gen_grace >= 0)
gen_grace = (op->contr->gen_grace+1)*op->stats.maxgrace;
else {
gen_grace = op->stats.maxgrace;
rate_grace -= rate_grace/2*op->contr->gen_grace;
}
/* Regenerate Spell Points */
if (op->contr->ranges[range_golem] == NULL && --op->last_sp < 0) {
gen_sp = gen_sp*10/MAX(op->contr->gen_sp_armour, 10);
if (op->stats.sp < op->stats.maxsp) {
op->stats.sp++;
/* dms do not consume food */
if (!QUERY_FLAG(op, FLAG_WIZ)) {
op->stats.food--;
if (op->contr->digestion < 0)
op->stats.food += op->contr->digestion;
else if (op->contr->digestion > 0
&& random_roll(0, op->contr->digestion, op, PREFER_HIGH))
op->stats.food = last_food;
}
}
if (max_sp > 1) {
over_sp = (gen_sp+10)/rate_sp;
if (over_sp > 0) {
if (op->stats.sp < op->stats.maxsp) {
op->stats.sp += MIN(over_sp, max_sp);
if (random_roll(0, rate_sp-1, op, PREFER_LOW) > ((gen_sp+10)%rate_sp))
op->stats.sp--;
if (op->stats.sp > op->stats.maxsp)
op->stats.sp = op->stats.maxsp;
}
op->last_sp = 0;
} else {
op->last_sp = rate_sp/(MAX(gen_sp, 20)+10);
}
} else {
op->last_sp = rate_sp/(MAX(gen_sp, 20)+10);
}
}
/* Regenerate Grace */
/* I altered this a little - maximum grace is ony achieved through prayer -b.t.*/
if (--op->last_grace < 0) {
if (op->stats.grace < op->stats.maxgrace/2)
op->stats.grace++; /* no penalty in food for regaining grace */
if (max_grace > 1) {
over_grace = (MAX(gen_grace, 20)+10)/rate_grace;
if (over_grace > 0) {
op->stats.sp += over_grace+(random_roll(0, rate_grace-1, op, PREFER_HIGH) > ((MAX(gen_grace, 20)+10)%rate_grace)) ? -1 : 0;
op->last_grace = 0;
} else {
op->last_grace = rate_grace/(MAX(gen_grace, 20)+10);
}
} else {
op->last_grace = rate_grace/(MAX(gen_grace, 20)+10);
}
/* wearing stuff doesn't detract from grace generation. */
}
/* Regenerate Hit Points (unless you are a wraith player) */
if (--op->last_heal < 0 && !is_wraith_pl(op)) {
if (op->stats.hp < op->stats.maxhp) {
op->stats.hp++;
/* dms do not consume food */
if (!QUERY_FLAG(op, FLAG_WIZ)) {
op->stats.food--;
if (op->contr->digestion < 0)
op->stats.food += op->contr->digestion;
else if (op->contr->digestion > 0
&& random_roll(0, op->contr->digestion, op, PREFER_HIGH))
op->stats.food = last_food;
}
}
if (max_hp > 1 && !is_wraith_pl(op)) {
over_hp = (MAX(gen_hp, 20)+10)/rate_hp;
if (over_hp > 0) {
op->stats.sp += over_hp+(RANDOM()%rate_hp > ((MAX(gen_hp, 20)+10)%rate_hp)) ? -1 : 0;
op->last_heal = 0;
} else {
op->last_heal = rate_hp/(MAX(gen_hp, 20)+10);
}
} else {
op->last_heal = rate_hp/(MAX(gen_hp, 20)+10);
}
}
/* Digestion */
if (--op->last_eat < 0) {
int bonus = MAX(op->contr->digestion, 0);
int penalty = MAX(-op->contr->digestion, 0);
if (op->contr->gen_hp > 0)
op->last_eat = 25*(1+bonus)/(op->contr->gen_hp+penalty+1);
else
op->last_eat = 25*(1+bonus)/(penalty+1);
/* dms do not consume food */
if (!QUERY_FLAG(op, FLAG_WIZ))
op->stats.food--;
}
}
if (op->contr->state == ST_PLAYING && op->stats.food < 0 && op->stats.hp >= 0) {
if (is_wraith_pl(op))
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ITEM, MSG_TYPE_ITEM_REMOVE, "You feel a hunger for living flesh.", NULL);
else {
object *tmp, *flesh = NULL;
for (tmp = op->inv; tmp != NULL; tmp = tmp->below) {
if (!QUERY_FLAG(tmp, FLAG_UNPAID)) {
if (tmp->type == FOOD || tmp->type == DRINK || tmp->type == POISON) {
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ITEM, MSG_TYPE_ITEM_REMOVE,
"You blindly grab for a bite of food.", NULL);
manual_apply(op, tmp, 0);
if (op->stats.food >= 0 || op->stats.hp < 0)
break;
} else if (tmp->type == FLESH)
flesh = tmp;
} /* End if paid for object */
} /* end of for loop */
/* If player is still starving, it means they don't have any food, so
* eat flesh instead.
*/
if (op->stats.food < 0 && op->stats.hp >= 0 && flesh) {
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ITEM, MSG_TYPE_ITEM_REMOVE,
"You blindly grab for a bite of food.", NULL);
manual_apply(op, flesh, 0);
}
} /* end not wraith */
} /* end if player is starving */
while (op->stats.food < 0 && op->stats.hp > 0)
op->stats.food++,
op->stats.hp--;
if (!op->contr->state && !QUERY_FLAG(op, FLAG_WIZ) && (op->stats.hp < 0 || op->stats.food < 0))
kill_player(op);
}
/**
* Grab and destroy some treasure.
*
* @param op
* object to loot.
*/
static void loot_object(object *op) {
object *tmp, *tmp2, *next;
if (op->container) { /* close open sack first */
apply_container(op, op->container);
}
for (tmp = op->inv; tmp != NULL; tmp = next) {
next = tmp->below;
if (tmp->type == EXPERIENCE || tmp->invisible)
continue;
remove_ob(tmp);
tmp->x = op->x,
tmp->y = op->y;
if (tmp->type == CONTAINER) { /* empty container to ground */
loot_object(tmp);
}
if (!QUERY_FLAG(tmp, FLAG_UNIQUE)
&& (QUERY_FLAG(tmp, FLAG_STARTEQUIP) || QUERY_FLAG(tmp, FLAG_NO_DROP) || !(RANDOM()%3))) {
if (tmp->nrof > 1) {
tmp2 = get_split_ob(tmp, 1+RANDOM()%(tmp->nrof-1), NULL, 0);
free_object(tmp2);
insert_ob_in_map(tmp, op->map, NULL, 0);
} else
free_object(tmp);
} else
insert_ob_in_map(tmp, op->map, NULL, 0);
}
}
/**
* When a player should die (lack of hp, food, etc), we call this.
*
* If the player can not be saved (permadeath, no lifesave), this will take care of removing the player file.
*
* Will remove diseases, apply death penalties, and so on.
*
* Takes battleground into account.
*
* @param op
* player in jeopardy.
* @todo describe battleground.
*/
void kill_player(object *op) {
char buf[MAX_BUF];
int x, y, i;
mapstruct *map; /* this is for resurrection */
int z;
int num_stats_lose;
int lost_a_stat;
int lose_this_stat;
int this_stat;
int will_kill_again;
archetype *at;
object *tmp;
archetype *trophy;
if (save_life(op))
return;
/* If player dies on BATTLEGROUND, no stat/exp loss! For Combat-Arenas
* in cities ONLY!!! It is very important that this doesn't get abused.
* Look at op_on_battleground() for more info --AndreasV
*/
if (op_on_battleground(op, &x, &y, &trophy)) {
draw_ext_info(NDI_UNIQUE|NDI_NAVY, 0, op, MSG_TYPE_VICTIM, MSG_TYPE_VICTIM_DIED,
"You have been defeated in combat!\n"
"Local medics have saved your life...",
NULL);
/* restore player */
at = find_archetype("poisoning");
tmp = present_arch_in_ob(at, op);
if (tmp) {
remove_ob(tmp);
free_object(tmp);
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_BAD_EFFECT_END,
"Your body feels cleansed", NULL);
}
at = find_archetype("confusion");
tmp = present_arch_in_ob(at, op);
if (tmp) {
remove_ob(tmp);
free_object(tmp);
draw_ext_info(NDI_UNIQUE, 0, tmp, MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_BAD_EFFECT_END,
"Your mind feels clearer", NULL);
}
cure_disease(op, NULL); /* remove any disease */
op->stats.hp = op->stats.maxhp;
if (op->stats.food <= 0)
op->stats.food = 999;
/* create a bodypart-trophy to make the winner happy */
tmp = arch_to_object(trophy);
if (tmp != NULL) {
snprintf(buf, sizeof(buf), "%s's %s", op->name, tmp->name);
tmp->name = add_string(buf);
if (tmp->type == FLESH)
snprintf(buf, sizeof(buf), " This %s has been cut off %s\n"
" the %s, when he was defeated at\n"
" level %d by %s.\n",
tmp->name, op->name, op->contr->title,
(int)(op->level), op->contr->killer);
else
snprintf(buf, sizeof(buf), " This %s has been taken from %s\n"
" the %s, when he was defeated at\n"
" level %d by %s.\n",
tmp->name, op->name, op->contr->title,
(int)(op->level), op->contr->killer);
tmp->msg = add_string(buf);
tmp->type = 0;
tmp->value = 0;
tmp->material = 0;
tmp->materialname = NULL;
tmp->x = op->x,
tmp->y = op->y;
insert_ob_in_map(tmp, op->map, op, 0);
}
/* teleport defeated player to new destination*/
transfer_ob(op, x, y, 0, NULL);
op->contr->braced = 0;
return;
}
/* Lauwenmark: Handle for plugin death event */
if (execute_event(op, EVENT_DEATH, NULL, NULL, NULL, SCRIPT_FIX_ALL) != 0)
return;
/* Lauwenmark: Handle for the global death event */
execute_global_event(EVENT_PLAYER_DEATH, op);
if (op->stats.food < 0) {
if (op->contr->explore) {
draw_ext_info(NDI_UNIQUE, 0, op,
MSG_TYPE_VICTIM, MSG_TYPE_VICTIM_DIED,
"You would have starved, but you are "
"in explore mode, so...", NULL);
op->stats.food = 999;
return;
}
snprintf(buf, sizeof(buf), "%s starved to death.", op->name);
strcpy(op->contr->killer, "starvation");
} else {
if (op->contr->explore) {
draw_ext_info(NDI_UNIQUE, 0, op,
MSG_TYPE_VICTIM, MSG_TYPE_VICTIM_DIED,
"You would have died, but you are "
"in explore mode, so...", NULL);
op->stats.hp = op->stats.maxhp;
return;
}
snprintf(buf, sizeof(buf), "%s died.", op->name);
}
play_sound_player_only(op->contr, SOUND_TYPE_LIVING, op, 0, "death");
/* save the map location for corpse, gravestone*/
x = op->x;
y = op->y;
map = op->map;
if (settings.not_permadeth == TRUE) {
/* NOT_PERMADEATH code. This basically brings the character back to
* life if they are dead - it takes some exp and a random stat.
* See the config.h file for a little more in depth detail about this.
*/
/* Basically two ways to go - remove a stat permanently, or just
* make it depletion. This bunch of code deals with that aspect
* of death.
*/
if (settings.balanced_stat_loss) {
/* If stat loss is permanent, lose one stat only. */
/* Lower level chars don't lose as many stats because they suffer
more if they do. */
/* Higher level characters can afford things such as potions of
restoration, or better, stat potions. So we slug them that
little bit harder. */
/* GD */
if (settings.stat_loss_on_death)
num_stats_lose = 1;
else
num_stats_lose = 1+op->level/BALSL_NUMBER_LOSSES_RATIO;
} else {
num_stats_lose = 1;
}
lost_a_stat = 0;
for (z = 0; z < num_stats_lose; z++) {
if (settings.stat_loss_on_death) {
/* Pick a random stat and take a point off it. Tell the player
* what he lost.
*/
i = RANDOM()%7;
change_attr_value(&(op->stats), i, -1);
check_stat_bounds(&(op->stats));
change_attr_value(&(op->contr->orig_stats), i, -1);
check_stat_bounds(&(op->contr->orig_stats));
draw_ext_info(NDI_UNIQUE, 0, op,
MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_STAT_LOSS,
lose_msg[i], lose_msg[i]);
lost_a_stat = 1;
} else {
/* deplete a stat */
archetype *deparch = find_archetype("depletion");
object *dep;
i = RANDOM()%7;
dep = present_arch_in_ob(deparch, op);
if (!dep) {
dep = arch_to_object(deparch);
insert_ob_in_ob(dep, op);
}
lose_this_stat = 1;
if (settings.balanced_stat_loss) {
/* GD */
/* Get the stat that we're about to deplete. */
this_stat = get_attr_value(&(dep->stats), i);
if (this_stat < 0) {
int loss_chance = 1+op->level/BALSL_LOSS_CHANCE_RATIO;
int keep_chance = this_stat*this_stat;
/* Yes, I am paranoid. Sue me. */
if (keep_chance < 1)
keep_chance = 1;
/* There is a maximum depletion total per level. */
if (this_stat < -1-op->level/BALSL_MAX_LOSS_RATIO) {
lose_this_stat = 0;
/* Take loss chance vs keep chance to see if we
retain the stat. */
} else {
if (random_roll(0, loss_chance+keep_chance-1, op, PREFER_LOW) < keep_chance)
lose_this_stat = 0;
/* LOG(llevDebug, "Determining stat loss. Stat: %d Keep: %d Lose: %d Result: %s.\n", this_stat, keep_chance, loss_chance, lose_this_stat ? "LOSE" : "KEEP"); */
}
}
}
if (lose_this_stat) {
this_stat = get_attr_value(&(dep->stats), i);
/* We could try to do something clever like find another
* stat to reduce if this fails. But chances are, if
* stats have been depleted to -50, all are pretty low
* and should be roughly the same, so it shouldn't make a
* difference.
*/
if (this_stat >= -50) {
change_attr_value(&(dep->stats), i, -1);
SET_FLAG(dep, FLAG_APPLIED);
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_STAT_LOSS,
lose_msg[i], lose_msg[i]);
fix_object(op);
lost_a_stat = 1;
}
}
}
}
/* If no stat lost, tell the player. */
if (!lost_a_stat) {
/* determine_god() seems to not work sometimes... why is this? Should I be using something else? GD */
const char *god = determine_god(op);
if (god && (strcmp(god, "none")))
draw_ext_info_format(NDI_UNIQUE, 0, op,
MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_GOD,
"For a brief moment you feel the holy presence of %s protecting you",
"For a brief moment you feel the holy presence of %s protecting you",
god);
else
draw_ext_info(NDI_UNIQUE, 0, op,
MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_GOD,
"For a brief moment you feel a holy presence protecting you.",
NULL);
}
/* Put a gravestone up where the character 'almost' died. List the
* exp loss on the stone.
*/
tmp = arch_to_object(find_archetype("gravestone"));
snprintf(buf, sizeof(buf), "%s's gravestone", op->name);
FREE_AND_COPY(tmp->name, buf);
snprintf(buf, sizeof(buf), "%s's gravestones", op->name);
FREE_AND_COPY(tmp->name_pl, buf);
snprintf(buf, sizeof(buf), "RIP\nHere rests the hero %s the %s,\n"
"who was killed\n"
"by %s.\n",
op->name, op->contr->title,
op->contr->killer);
tmp->msg = add_string(buf);
tmp->x = op->x,
tmp->y = op->y;
insert_ob_in_map(tmp, op->map, NULL, 0);
/* restore player: remove any poisoning, disease and confusion the
* character may be suffering.*/
at = find_archetype("poisoning");
tmp = present_arch_in_ob(at, op);
if (tmp) {
remove_ob(tmp);
free_object(tmp);
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_BAD_EFFECT_END,
"Your body feels cleansed", NULL);
}
at = find_archetype("confusion");
tmp = present_arch_in_ob(at, op);
if (tmp) {
remove_ob(tmp);
free_object(tmp);
draw_ext_info(NDI_UNIQUE, 0, tmp, MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_BAD_EFFECT_END,
"Your mind feels clearer", NULL);
}
cure_disease(op, NULL); /* remove any disease */
/* Subtract the experience points, if we died cause of food, give
* us food, and reset HP's...
*/
apply_death_exp_penalty(op);
if (op->stats.food < 100)
op->stats.food = 900;
op->stats.hp = op->stats.maxhp;
op->stats.sp = MAX(op->stats.sp, op->stats.maxsp);
op->stats.grace = MAX(op->stats.grace, op->stats.maxgrace);
/* Check to see if the player is in a shop. IF so, then check to see if
* the player has any unpaid items. If so, remove them and put them back
* in the map.
*
* If they are not in a shop, just free the unpaid items instead of
* putting them back on map.
*/
if (is_in_shop(op))
remove_unpaid_objects(op->inv, op, 0);
else
remove_unpaid_objects(op->inv, op, 1);
/* Move player to his current respawn-position (usually last savebed) */
enter_player_savebed(op);
/* Save the player before inserting the force to reduce chance of abuse. */
op->contr->braced = 0;
save_player(op, 1);
/* it is possible that the player has blown something up
* at his savebed location, and that can have long lasting
* spell effects. So first see if there is a spell effect
* on the space that might harm the player.
*/
will_kill_again = 0;
for (tmp = GET_MAP_OB(op->map, op->x, op->y); tmp; tmp = tmp->above) {
if (tmp->type == SPELL_EFFECT)
will_kill_again |= tmp->attacktype;
}
if (will_kill_again) {
object *force;
int at;
force = create_archetype(FORCE_NAME);
/* 50 ticks should be enough time for the spell to abate */
force->speed = 0.1;
force->speed_left = -5.0;
SET_FLAG(force, FLAG_APPLIED);
for (at = 0; at < NROFATTACKS; at++) {
if (will_kill_again&(1<<at))
force->resist[at] = 100;
}
insert_ob_in_ob(force, op);
fix_object(op);
}
/* Tell the player they have died */
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_VICTIM, MSG_TYPE_VICTIM_DIED,
"YOU HAVE DIED.", NULL);
return;
} /* NOT_PERMADETH */
else {
/* If NOT_PERMADETH is set, then the rest of this is not reachable. This
* should probably be embedded in an else statement.
*/
op->contr->party = NULL;
if (settings.set_title == TRUE)
op->contr->own_title[0] = '\0';
/* buf should be the kill message */
draw_ext_info(NDI_UNIQUE|NDI_ALL, 0, NULL, MSG_TYPE_VICTIM, MSG_TYPE_VICTIM_DIED,
buf, buf);
check_score(op, 0);
if (op->contr->ranges[range_golem] != NULL) {
remove_friendly_object(op->contr->ranges[range_golem]);
remove_ob(op->contr->ranges[range_golem]);
free_object(op->contr->ranges[range_golem]);
op->contr->ranges[range_golem] = NULL;
op->contr->golem_count = 0;
}
loot_object(op); /* Remove some of the items for good */
remove_ob(op);
op->direction = 0;
if (!QUERY_FLAG(op, FLAG_WAS_WIZ) && op->stats.exp) {
if (settings.resurrection == TRUE) {
/* save playerfile sans equipment when player dies
* -then save it as player.pl.dead so that future resurrection
* -type spells will work on them nicely
*/
op->stats.hp = op->stats.maxhp;
op->stats.food = 999;
/* set the location of where the person will reappear when */
/* maybe resurrection code should fix map also */
strcpy(op->contr->maplevel, settings.emergency_mapname);
if (op->map != NULL)
op->map = NULL;
op->x = settings.emergency_x;
op->y = settings.emergency_y;
save_player(op, 0);
op->map = map;
/* please see resurrection.c: peterm */
dead_player(op);
} else {
delete_character(op->name);
}
}
play_again(op);
/* peterm: added to create a corpse at deathsite. */
tmp = arch_to_object(find_archetype("corpse_pl"));
snprintf(buf, sizeof(buf), "%s", op->name);
FREE_AND_COPY(tmp->name, buf);
FREE_AND_COPY(tmp->name_pl, buf);
tmp->level = op->level;
tmp->x = x;
tmp->y = y;
if (tmp->msg)
free_string(tmp->msg);
tmp->msg = add_string(gravestone_text(op, buf, sizeof(buf)));
SET_FLAG(tmp, FLAG_UNIQUE);
insert_ob_in_map(tmp, map, NULL, 0);
}
}
/**
* Check recursively the weight of all players, and fix
* what needs to be fixed. Refresh windows and fix speed if anything
* was changed.
*
* @todo is this still useful?
*/
void fix_weight(void) {
player *pl;
for (pl = first_player; pl != NULL; pl = pl->next) {
int old = pl->ob->carrying, sum = sum_weight(pl->ob);
if (old == sum)
continue;
fix_object(pl->ob);
LOG(llevDebug, "Fixed inventory in %s (%d -> %d)\n", pl->ob->name, old, sum);
}
}
/**
* Fixes luck of players, slowly move it towards 0.
*/
void fix_luck(void) {
player *pl;
for (pl = first_player; pl != NULL; pl = pl->next)
if (!pl->ob->contr->state)
change_luck(pl->ob, 0);
}
/**
* Handles op throwing objects of type 'DUST'.
* This is much simpler in the new spell code - we basically
* just treat this as any other spell casting object.
*
* @param op
* object throwing.
* @param throw_ob
* what to throw.
* @param dir
* direction to throw into.
*/
void cast_dust(object *op, object *throw_ob, int dir) {
object *skop, *spob;
skop = find_skill_by_name(op, throw_ob->skill);
/* casting POTION 'dusts' is really a use_magic_item skill */
if (op->type == PLAYER && throw_ob->type == POTION && !skop) {
LOG(llevError, "Player %s lacks critical skill use_magic_item!\n", op->name);
return;
}
spob = throw_ob->inv;
if (op->type == PLAYER && spob)
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_APPLY, MSG_TYPE_APPLY_SUCCESS,
"You cast %s.",
"You cast %s.",
spob->name);
cast_spell(op, throw_ob, dir, spob, NULL);
if (!QUERY_FLAG(throw_ob, FLAG_REMOVED))
remove_ob(throw_ob);
free_object(throw_ob);
}
/**
* Makes an object visible again.
*
* @param op
* what to make visible.
*/
void make_visible(object *op) {
op->hide = 0;
op->invisible = 0;
if (op->type == PLAYER) {
op->contr->tmp_invis = 0;
if (op->contr->invis_race)
FREE_AND_CLEAR_STR(op->contr->invis_race);
}
update_object(op, UP_OBJ_FACE);
}
/**
* Is the object a true undead?
*
* @param op
* object to test.
* @return
* 1 if undead, 0 else.
*/
int is_true_undead(object *op) {
object *tmp = NULL;
if (QUERY_FLAG(&op->arch->clone, FLAG_UNDEAD))
return 1;
if (op->type == PLAYER)
for (tmp = op->inv; tmp; tmp = tmp->below)
if (tmp->type == EXPERIENCE && tmp->stats.Wis)
if (QUERY_FLAG(tmp, FLAG_UNDEAD))
return 1;
return 0;
}
/**
* Look at the surrounding terrain to determine
* the hideability of this object. Positive levels
* indicate greater hideability.
*
* @param ob
* object that may want to hide.
* @return
* the higher the value, the easier to hide here.
*/
int hideability(object *ob) {
int i, level = 0, mflag;
sint16 x, y;
if (!ob || !ob->map)
return 0;
/* so, on normal lighted maps, its hard to hide */
level = ob->map->darkness-2;
/* this also picks up whether the object is glowing.
* If you carry a light on a non-dark map, its not
* as bad as carrying a light on a pitch dark map
*/
if (has_carried_lights(ob))
level = -(10+(2*ob->map->darkness));
/* scan through all nearby squares for terrain to hide in */
for (i = 0, x = ob->x, y = ob->y; i < 9; i++, x = ob->x+freearr_x[i], y = ob->y+freearr_y[i]) {
mflag = get_map_flags(ob->map, NULL, x, y, NULL, NULL);
if (mflag&P_OUT_OF_MAP) {
continue;
}
if (mflag&P_BLOCKSVIEW) /* something to hide near! */
level += 2;
else /* open terrain! */
level -= 1;
}
return level;
}
/**
* For hidden creatures - a chance of becoming 'unhidden'
* every time they move - as we subtract off 'invisibility'
* AND, for players, if they move into a ridiculously unhideable
* spot (surrounded by clear terrain in broad daylight). -b.t.
*
* @param op
* object moving.
*/
void do_hidden_move(object *op) {
int hide = 0, num = random_roll(0, 19, op, PREFER_LOW);
object *skop;
if (!op || !op->map)
return;
skop = find_obj_by_type_subtype(op, SKILL, SK_HIDING);
/* its *extremely *hard to run and sneak/hide at the same time! */
if (op->type == PLAYER && op->contr->run_on) {
if (!skop || num >= skop->level) {
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_SKILL, MSG_TYPE_SKILL_FAILURE,
"You ran too much! You are no longer hidden!", NULL);
make_visible(op);
return;
} else
num += 20;
}
num += op->map->difficulty;
hide = hideability(op); /* modify by terrain hidden level */
num -= hide;
if ((op->type == PLAYER && hide < -10)
|| ((op->invisible -= num) <= 0)) {
make_visible(op);
if (op->type == PLAYER)
draw_ext_info(NDI_UNIQUE, 0, op, MSG_TYPE_SKILL, MSG_TYPE_SKILL_FAILURE,
"You moved out of hiding! You are visible!", NULL);
} else if (op->type == PLAYER && skop) {
change_exp(op, calc_skill_exp(op, NULL, skop), skop->skill, 0);
}
}
/**
* Determine if who is standing near a hostile creature.
*
* @param who
* object to check.
* @return
* 1 if near a monster, 0 else.
*/
int stand_near_hostile(object *who) {
object *tmp = NULL;
int i, friendly = 0, player = 0, mflags;
mapstruct *m;
sint16 x, y;
if (!who)
return 0;
if (who->type == PLAYER)
player = 1;
else
friendly = QUERY_FLAG(who, FLAG_FRIENDLY);
/* search adjacent squares */
for (i = 1; i < 9; i++) {
x = who->x+freearr_x[i];
y = who->y+freearr_y[i];
m = who->map;
mflags = get_map_flags(m, &m, x, y, &x, &y);
/* space must be blocked if there is a monster. If not
* blocked, don't need to check this space.
*/
if (mflags&P_OUT_OF_MAP)
continue;
if (OB_TYPE_MOVE_BLOCK(who, GET_MAP_MOVE_BLOCK(m, x, y)))
continue;
for (tmp = GET_MAP_OB(m, x, y); tmp; tmp = tmp->above) {
if ((player || friendly)
&& QUERY_FLAG(tmp, FLAG_MONSTER)
&& !QUERY_FLAG(tmp, FLAG_UNAGGRESSIVE))
return 1;
else if (tmp->type == PLAYER) {
/*don't let a hidden DM prevent you from hiding*/
if (!QUERY_FLAG(tmp, FLAG_WIZ) || tmp->contr->hidden == 0)
return 1;
}
}
}
return 0;
}
/**
* Check the player los field for viewability of the
* object op. This function works fine for monsters,
* but we dont worry if the object isnt the top one in
* a pile (say a coin under a table would return "viewable"
* by this routine). Another question, should we be
* concerned with the direction the player is looking
* in? Realistically, most of use cant see stuff behind
* our backs...on the other hand, does the "facing" direction
* imply the way your head, or body is facing? Its possible
* for them to differ. Sigh, this fctn could get a bit more complex.
* -b.t.
*
* This function is now map tiling safe.
*
* @param pl
* player that may see op.
* @param op
* what may be seen by pl.
* @retval -1
* pl isn't a player
* @retval 0
* pl can't see op.
* @retval 1
* pl can see op.
*/
int player_can_view(object *pl, object *op) {
rv_vector rv;
int dx, dy;
if (pl->type != PLAYER) {
LOG(llevError, "player_can_view() called for non-player object\n");
return -1;
}
if (!pl || !op)
return 0;
if (op->head) {
op = op->head;
}
get_rangevector(pl, op, &rv, 0x1);
/* starting with the 'head' part, lets loop
* through the object and find if it has any
* part that is in the los array but isnt on
* a blocked los square.
* we use the archetype to figure out offsets.
*/
while (op) {
dx = rv.distance_x+op->arch->clone.x;
dy = rv.distance_y+op->arch->clone.y;
/* only the viewable area the player sees is updated by LOS
* code, so we need to restrict ourselves to that range of values
* for any meaningful values.
*/
if (FABS(dx) <= (pl->contr->socket.mapx/2)
&& FABS(dy) <= (pl->contr->socket.mapy/2)
&& !pl->contr->blocked_los[dx+(pl->contr->socket.mapx/2)][dy+(pl->contr->socket.mapy/2)])
return 1;
op = op->more;
}
return 0;
}
/**
* We call this when there is a possibility for our action disturbing our hiding
* place or invisiblity spell. Artefact invisiblity is not
* effected by this. If we arent invisible to begin with, we
* return 0.
*
* This routine works for both players and monsters.
*
* @param op
* object to check.
* @return
* 1 if op isn't invisible anymore, 0 else.
*/
static int action_makes_visible(object *op) {
if (op->invisible && QUERY_FLAG(op, FLAG_ALIVE)) {
if (QUERY_FLAG(op, FLAG_MAKE_INVIS))
return 0;
if (op->contr && op->contr->tmp_invis == 0)
return 0;
/* If monsters, they should become visible */
if (op->hide || !op->contr || (op->contr && op->contr->tmp_invis)) {
draw_ext_info_format(NDI_UNIQUE, 0, op, MSG_TYPE_MISC, MSG_SUBTYPE_NONE,
"You become %s!",
"You become %s!",
op->hide ? "unhidden" : "visible");
return 1;
}
}
return 0;
}
/**
* Checks if the given object op (usually a player) is standing on a valid battleground-tile.
*
* Function returns TRUE/FALSE. If true x, y returns the battleground
* -exit-coord. (and if x, y not NULL)
*
* 19 March 2005 - josh@woosworld.net modifed to check if the battleground also has slaying, maxhp, and maxsp set
* and if those are all set and the player has a marker that matches the slaying send them to a different x, y
* Default is to do the same as before, so only people wanting to have different points need worry about this
*
* 28 July 2008 - Modified to allow other archetypes than fingers as trophies.
* If other_arch is specified in the battleground floor, then that archetype
* will be used instead of the default ("finger"). -R.Q.
*
* @param op
* object to check.
* @param[out] x
* @param[out] y
* if not null and if on battleground (return 1), will contain the exit coordinates for the battleground.
* @param[out] trophy
* if not null and if on battleground (return 1), will contain a pointer to the archetype that can be collected by the winner
* @return
* 1 if op is on battleground, 0 else.
*/
int op_on_battleground(object *op, int *x, int *y, archetype **trophy) {
object *tmp;
/* A battleground-tile needs the following attributes to be valid:
* is_floor 1 (has to be the FIRST floor beneath the player's feet),
* name="battleground", no_pick 1, type=58 (type BATTLEGROUND)
* and the exit-coordinates sp/hp must both be > 0.
* => The intention here is to prevent abuse of the battleground-
* feature (like pickable or hidden battleground tiles). */
for (tmp = op->below; tmp != NULL; tmp = tmp->below) {
if (QUERY_FLAG(tmp, FLAG_IS_FLOOR)) {
if (QUERY_FLAG(tmp, FLAG_NO_PICK)
&& strcmp(tmp->name, "battleground") == 0
&& tmp->type == BATTLEGROUND
&& EXIT_X(tmp)
&& EXIT_Y(tmp)) {
/*before we assign the exit, check if this is a teambattle*/
if (EXIT_ALT_X(tmp) && EXIT_ALT_Y(tmp) && EXIT_PATH(tmp)) {
object *invtmp;
for (invtmp = op->inv; invtmp != NULL; invtmp = invtmp->below) {
if (invtmp->type == FORCE
&& invtmp->slaying
&& !strcmp(EXIT_PATH(tmp), invtmp->slaying)) {
if (x != NULL && y != NULL)
*x = EXIT_ALT_X(tmp),
*y = EXIT_ALT_Y(tmp);
return 1;
}
}
}
if (x != NULL && y != NULL)
*x = EXIT_X(tmp),
*y = EXIT_Y(tmp);
if (trophy != NULL) {
if (tmp->other_arch)
*trophy = tmp->other_arch;
else
*trophy = find_archetype("finger");
}
return 1;
}
}
}
/* If we got here, did not find a battleground */
return 0;
}
/**
* When a dragon-player gains a new stage of evolution, he gets some treasure.
*
* @param who
* the dragon player.
* @param atnr
* the attack-number of the ability focus.
* @param level
* ability level.
*/
void dragon_ability_gain(object *who, int atnr, int level) {
treasurelist *trlist = NULL; /* treasurelist */
treasure *tr; /* treasure */
object *tmp, *skop; /* tmp. object */
object *item; /* treasure object */
char buf[MAX_BUF]; /* tmp. string buffer */
int i = 0, j = 0;
/* get the appropriate treasurelist */
if (atnr == ATNR_FIRE)
trlist = find_treasurelist("dragon_ability_fire");
else if (atnr == ATNR_COLD)
trlist = find_treasurelist("dragon_ability_cold");
else if (atnr == ATNR_ELECTRICITY)
trlist = find_treasurelist("dragon_ability_elec");
else if (atnr == ATNR_POISON)
trlist = find_treasurelist("dragon_ability_poison");
if (trlist == NULL || who->type != PLAYER)
return;
for (i = 0, tr = trlist->items; tr != NULL && i < level-1; tr = tr->next, i++)
;
if (tr == NULL || tr->item == NULL) {
/* LOG(llevDebug, "-> no more treasure for %s\n", change_resist_msg[atnr]); */
return;
}
/* everything seems okay - now bring on the gift: */
item = &(tr->item->clone);
if (item->type == SPELL) {
if (check_spell_known(who, item->name))
return;
draw_ext_info_format(NDI_UNIQUE|NDI_BLUE, 0, who,
MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_RACE,
"You gained the ability of %s",
"You gained the ability of %s",
item->name);
do_learn_spell(who, item, 0);
return;
}
/* grant direct spell */
if (item->type == SPELLBOOK) {
if (!item->inv) {
LOG(llevDebug, "dragon_ability_gain: Broken spellbook %s\n", item->name);
return;
}
if (check_spell_known(who, item->inv->name))
return;
if (item->invisible) {
draw_ext_info_format(NDI_UNIQUE|NDI_BLUE, 0, who,
MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_RACE,
"You gained the ability of %s",
"You gained the ability of %s",
item->inv->name);
do_learn_spell(who, item->inv, 0);
return;
}
} else if (item->type == SKILL_TOOL && item->invisible) {
if (item->subtype == SK_CLAWING && (skop = find_skill_by_name(who, item->skill)) != NULL) {
/* should this perhaps be (skop->attackyp&item->attacktype) != item->attacktype ...
* in this way, if the player is missing any of the attacktypes, he gets
* them. As it is now, if the player has any that match the granted skill,
* but not all of them, he gets nothing.
*/
if (!(skop->attacktype&item->attacktype)) {
/* Give new attacktype */
skop->attacktype |= item->attacktype;
/* always add physical if there's none */
skop->attacktype |= AT_PHYSICAL;
if (item->msg != NULL)
draw_ext_info(NDI_UNIQUE|NDI_BLUE, 0, who,
MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_RACE,
item->msg, item->msg);
/* Give player new face */
if (item->animation_id) {
who->face = skop->face;
who->animation_id = item->animation_id;
who->anim_speed = item->anim_speed;
who->last_anim = 0;
who->state = 0;
animate_object(who, who->direction);
}
}
}
} else if (item->type == FORCE) {
/* forces in the treasurelist can alter the player's stats */
object *skin;
/* first get the dragon skin force */
for (skin = who->inv; skin != NULL && strcmp(skin->arch->name, "dragon_skin_force") != 0; skin = skin->below)
;
if (skin == NULL)
return;
/* adding new spellpath attunements */
if (item->path_attuned > 0 && !(skin->path_attuned&item->path_attuned)) {
skin->path_attuned |= item->path_attuned; /* add attunement to skin */
/* print message */
snprintf(buf, sizeof(buf), "You feel attuned to ");
for (i = 0, j = 0; i < NRSPELLPATHS; i++) {
if (item->path_attuned&(1<<i)) {
if (j)
strcat(buf, " and ");
else
j = 1;
strcat(buf, spellpathnames[i]);
}
}
strcat(buf, ".");
draw_ext_info(NDI_UNIQUE|NDI_BLUE, 0, who,
MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_RACE,
buf, buf);
}
/* evtl. adding flags: */
if (QUERY_FLAG(item, FLAG_XRAYS))
SET_FLAG(skin, FLAG_XRAYS);
if (QUERY_FLAG(item, FLAG_STEALTH))
SET_FLAG(skin, FLAG_STEALTH);
if (QUERY_FLAG(item, FLAG_SEE_IN_DARK))
SET_FLAG(skin, FLAG_SEE_IN_DARK);
/* print message if there is one */
if (item->msg != NULL)
draw_ext_info(NDI_UNIQUE|NDI_BLUE, 0, who,
MSG_TYPE_ATTRIBUTE, MSG_TYPE_ATTRIBUTE_RACE,
item->msg, item->msg);
} else {
/* generate misc. treasure */
char name[HUGE_BUF];
tmp = arch_to_object(tr->item);
query_short_name(tmp, name, HUGE_BUF);
draw_ext_info_format(NDI_UNIQUE|NDI_BLUE, 0, who,
MSG_TYPE_ITEM, MSG_TYPE_ITEM_ADD,
"You gained %s",
"You gained %s",
name);
tmp = insert_ob_in_ob(tmp, who);
if (who->type == PLAYER)
esrv_send_item(who, tmp);
}
}
/**
* Unready an object for a player. This function does nothing if the object was
* not readied.
*
* @param pl
* player.
* @param ob
* object to unready.
*/
void player_unready_range_ob(player *pl, object *ob) {
rangetype i;
for (i = 0; i < range_size; i++) {
if (pl->ranges[i] == ob) {
pl->ranges[i] = NULL;
if (pl->shoottype == i) {
pl->shoottype = range_none;
}
}
}
}