/* * 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 #include #ifndef WIN32 /* ---win32 remove headers */ #include #endif #ifndef __CEXTRACT__ #include #endif #include #include #include #include #include #include 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<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<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<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; } } } }