/* -*- LPC -*- */
/*
* $Locker: $
* $Id: skills.c,v 1.24 2003/05/07 22:49:32 ceres Exp $
*
*/
/**
* This skills modules for living creates. This deals will all the
* skill interactions needed for the living objects.
*
* @author Pinkfish
*/
/*
* sigh, this will be interesting wont it?
* quick summary of routines
* skill_bonus(string,int) - gives the bonus...
* skill_lvl(string) - gives the raw level, with out stat bonus etc
* modify_skill(string,int,int)- modifies the skills level by int.
* calc_bonus(int,string,int) - given the skill lvl, stat bonus str cals bonus
* calc_lvl(string *) - calculate number of lvls in the path
* add_skill_lvl(...) - horror recursive skill adder. arghh
* teach_skill(objct *,string) - Used to teach skills to other people.
* skill_commands() - all the skill add_actioned commands.
*/
#include <skills.h>
#include <tasks.h>
#include <tune.h>
// #define BAD_TM "BAD_TM"
#undef LOGGING
varargs int calc_bonus( int lvl, string skill, int use_base_stats );
varargs int stat_modify( int bonus, string skill, int use_base_stats );
mixed recursive_skill_add(mixed skil, string *path, int avr, int lvl, int exp,
mixed standard);
private void convert_skills(mixed *skills, string path);
int query_skill(string skill);
mapping new_skills;
nosave mapping _bonus_cache,
_stat_cache,
_teach_offer;
mapping _last_info;
mapping query_skills() { return copy( new_skills ); }
void set_skills( mapping map ) { new_skills = map; }
int calc_level(string *path);
void create() {
_bonus_cache = ([ ]);
_teach_offer = ([ ]);
_stat_cache = ([ ]);
new_skills = ([ ]);
if(!_last_info)
_last_info = ([ "time" : time() ]);
} /* create() */
/**
* This method checks to see if the skill exists in the skill array or
* not.
* @param skill the skill to check for non-existance
* @return 0 if it does not exist, 1 if it does
*/
int not_there( string skill ) {
//return member_array( skill, keys( new_skills ) ) == -1;
return undefinedp(new_skills[skill]);
} /* not_there() */
/**
* This method returns the current bonus cache for the living thing.
* The bonus cache is where the calculated bonuses for the skills are
* kept.
* @return the bonus cache mapping
*/
mapping query_bonus_cache() { return copy(_bonus_cache); }
/**
* This method returns the cached values for the stats.
* @return the caches stat values
*/
mapping query_stat_cache() { return copy(_stat_cache); }
/**
* This method zaps the stat cache when a certain stat changes.
* It calls the function stats_to_zap() on the living object to
* figure out which stats have changed.
* @see /std/living/stats->stats_to_zap()
*/
void zap_stat_cache() {
int i;
string word, *list, stat;
stat = this_object()->stats_to_zap();
if ( !stat ) {
return;
}
if ( find_call_out( "reset_all2" ) == -1 ) {
call_out( "reset_all2", 1 );
}
foreach( i in stat ) {
list = _stat_cache[ i ];
if ( !list )
continue;
foreach( word in list ) {
map_delete( _stat_cache, word );
}
}
word = (string)this_object()->query_race_ob();
if ( word ) {
word->set_unarmed_attacks( this_object() );
}
} /* zap_stat_cache() */
/**
* This method zaps the bonus cache.
*/
void totaly_zap_bonus_cache() {
_bonus_cache = ([ ]);
} /* zap_bonus_cache() */
/**
* This method zaps the stat cache.
*/
protected void totaly_zap_stat_cache() {
_stat_cache = ([ ]);
} /* zap_stat_cache() */
/**
* This method returns the skill bonus for the specified skill.
* It returns the skill + all its bonsues for stats/whatever.
* It first checks to see if the skill is in it's cache. THe
* real stat values are ones not modified by bonuses or temporary
* values.
* @param skill the skill to get the bonus for
* @param use_base_stats tells the system not to use the real stat values
* @return the skill bonus
*/
varargs int query_skill_bonus( string skill, int use_base_stats ) {
#ifdef 0
int tmp, lvl;
string *path;
object guild, race;
#endif
if (!stringp(skill) || !strlen(skill)) {
return 0;
}
if (!new_skills) {
new_skills = ([ ]);
}
if (skill[0] == '.') {
skill = skill[1..];
}
TASKER->set_control( ({ this_object(), skill }) );
if ( _bonus_cache[ skill ] )
return stat_modify( _bonus_cache[ skill ], skill, use_base_stats );
return calc_bonus( query_skill(skill), skill, use_base_stats );
#ifdef 0
// The following just isn't used anymore so can be removed.
lvl = query_skill(skill);
guild = (object)this_object()->query_guild_ob();
race = (object)this_object()->query_race_ob();
if (race) {
tmp = (int)race->query_skill_bonus(lvl, skill);
}
if (guild) {
tmp += (int)guild->query_skill_bonus(lvl, skill);
}
return calc_bonus( lvl + tmp, skill, use_base_stats );
#endif
} /* query_skill_bonus() */
/**
* This returns jus the skill level. Used a lot to determine if you
* can use/teach/whatever a skill.
* This also uses a cache.
* @param skill the skill to return the level of
* @return the skill level
*/
int query_skill(string skill) {
string *path;
if (!new_skills) {
new_skills = ([ ]);
}
if (!stringp(skill)) {
return 0;
}
if (skill[0] == '.') {
skill = skill[1..];
}
TASKER->set_control( ({ this_object(), skill }) );
if ( not_there( skill ) ) {
int i;
path = (string *)SKILL_OB->query_skill_tree(skill);
if (path) {
for (i=0;i<sizeof(path);i++) {
if ( !not_there( path[ i ] ) ) {
return new_skills[path[i]];
}
}
}
} else {
return new_skills[skill];
}
return 0;
} /* query_skill() */
/**
* This method fills out a complete skill branch, complete with ALL child skills.
* It saves using many call_others to check skills.
* @arg string The branch you want to query [ie: faith, magic, etc.]
*/
mapping query_complete_skill_branch( string branch ) {
string *skills = SKILL_OB->query_all_children( branch );
if ( !arrayp( skills ) || !sizeof( skills ) )
return ([ ]);
return allocate_mapping( skills, (: query_skill( $1 ) :) );
}
/**
* This is used to convert a previously not only_leaf tree into an only_leaf
* tree.
*/
protected void flatten_it(string skill) {
int value;
int i;
string *same;
reset_eval_cost();
value = new_skills[skill];
same = (mixed *)SKILL_OB->query_immediate_children(skill);
for (i=0;i<sizeof(same);i++) {
if ( not_there( same[ i ] ) ) {
new_skills[same[i]] = value;
}
flatten_it(same[i]);
}
if (sizeof(same)) {
map_delete(new_skills, skill);
}
} /* flatten_it() */
int tm_check_ok(string skill, object exp) {
string *history, *bits, *abits;
int i, j, last, delay;
if ( !_last_info ) {
_last_info = ([ "time" : time() ]);
}
#ifdef LOGGING
if(base_name(previous_object()) != "/obj/handlers/taskmaster" &&
base_name(previous_object()) != "/std/effects/fighting/combat" &&
base_name(previous_object()) != "/std/shadows/misc/team" &&
base_name(previous_object()) != "/std/shadows/other/group" &&
base_name(previous_object()) != "/global/player" &&
base_name(previous_object())[0..2] != "/w/") {
log_file("ATTEMPT_TASK", "Object %s gave skill increase without using "
"the taskmaster.\n", base_name(previous_object()));
}
#endif
history = this_object()->get_history();
if(sizeof(history)) {
// This code looks for people repeating commands over and over in an
// attempt to gain TMs.
for(i=0; i<sizeof(history) && history[i]; i++)
;
last = i - 1;
// deal with aliases and command queuing
if(!this_object()->is_alias(history[last]))
last -= this_object()->query_queued_commands();
if(last > 0 && sizeof(history[last]) > 1 && skill[<7..] != ".points") {
for(i=0; i<sizeof(history) && history[i]; i++) {
if(history[last] == history[i]) {
j++;
}
}
if(j > 5 || j * 100 / i > 30) {
#ifdef BAD_TM
log_file(BAD_TM, "%s %s in %s by %O too many attempts at %s [%s]\n",
ctime(time())[4..18], this_object()->query_name(), skill,
exp, history[last], history[i-1]);
#endif
if(!_last_info["skill"] || _last_info["skill"][0] != skill)
_last_info["skill"] = ({ skill, 2 });
else
_last_info["skill"][1] += 1;
_last_info["time"] = time();
if(!_last_info["object"] || _last_info["object"][0] != base_name(exp))
_last_info["object"] = ({ base_name(exp), 2 });
else
_last_info["object"][1] += 1;
if(!_last_info["env"] || _last_info["env"][0] != environment())
_last_info["env"] = ({ environment(), 2 });
else
_last_info["env"][1] += 1;
return 0;
}
}
}
// Checks for repeated TMs with the same or similar skill trees.
// starting value (minimum delay) is proportional to their guild level
// and the level of this skill.
delay = 30 + random(this_object()->query_level()) +
random(this_object()->query_skill(skill));
// if this is the same object as last award then double the delay required
if(_last_info["object"] && base_name(exp) == _last_info["object"][0])
delay *= _last_info["object"][1];
// if this skill is more than twice their guild level then increase the
// delay.
if(this_object()->query_level() * 2 < this_object()->query_skill(skill))
delay *= 2;
if(_last_info["env"] && environment(this_object()) == _last_info["env"][0]) {
delay *= _last_info["env"][1];
}
// see how many parts of the tree matches. The closer the skill
// matches the longer between repeats.
// Note that if there is no match at all their delay will be 60 seconds.
bits = explode(skill, ".");
if(_last_info["skill"])
abits = explode(_last_info["skill"][0], ".");
else
abits = ({ });
for(i=0; i<sizeof(bits) && i<sizeof(abits) && bits[i] == abits[i]; i++)
;
if(i && _last_info["skill"])
delay *= (i * _last_info["skill"][1]);
else
delay = 60;
if(_last_info["time"] > (time() - delay)) {
#ifdef BAD_TM
log_file(BAD_TM, "%s %s in %s by %O last %d secs ago, delay %d.\n",
ctime(time())[4..18], this_object()->query_name(), skill,
exp, (time() - _last_info["time"]), delay);
#endif
if(!_last_info["skill"] || _last_info["skill"][0] != skill)
_last_info["skill"] = ({ skill, 2 });
else
_last_info["skill"][1] += 1;
_last_info["time"] = time();
if(!_last_info["object"] || _last_info["object"][0] != base_name(exp))
_last_info["object"] = ({ base_name(exp), 2 });
else
_last_info["object"][1] += 1;
if(!_last_info["env"] || _last_info["env"][0] != environment())
_last_info["env"] = ({ environment(), 2 });
else
_last_info["env"][1] += 1;
return 0;
}
if(!_last_info["skill"] || _last_info["skill"][0] != skill)
_last_info["skill"] = ({ skill, 2 });
else
_last_info["skill"][1] += 1;
_last_info["time"] = time();
if(!_last_info["object"] || _last_info["object"][0] != base_name(exp))
_last_info["object"] = ({ base_name(exp), 2 });
else
_last_info["object"][1] += 1;
if(!_last_info["env"] || _last_info["env"][0] != environment())
_last_info["env"] = ({ environment(), 2 });
else
_last_info["env"][1] += 1;
return 1;
}
/**
* This method adds a skill level to the specified skill to the
* system.
* @param skill the skill to add a level to
* @param lvl the number of levels to add
* @param exp the amount of exp spent on the skill
* @return 1 if the skill level was changed
* @see query_skill()
* @see query_skill_bonus()
*/
varargs int add_skill_level( string skill, int lvl, mixed exp ) {
string guild, *recursive_skills, *same_level, *bits, *tree;
int i;
reset_eval_cost();
if (!stringp(skill) || !intp(lvl) || lvl > 1000) {
return 0;
}
if (!new_skills || (!mapp(new_skills))) {
new_skills = ([ ]);
}
if (skill[0] == '.') {
skill = skill[1..];
}
recursive_skills = (string *)SKILL_OB->query_related_skills(skill);
if (!recursive_skills) {
return 0;
}
bits = explode(skill, ".");
/*
* Make sure the path leading up to this skill exists so that we can
* get the right value for the skill when we add it in.
* This should only be done if they are not only leaf skills.
*/
if ( not_there( skill ) && !SKILL_OB->query_only_leaf(skill)) {
int tmp_lvl, j;
if (sizeof(bits) > 1) {
tmp_lvl = 0;
for (i=sizeof(bits)-1;!tmp_lvl && i>=0;i--) {
if ( !not_there( implode( bits[ 0 .. i ], "." ) ) ) {
tmp_lvl = new_skills[implode(bits[0..i], ".")];
break;
}
}
if (i>=0) {
for (;i<sizeof(bits);i++) {
same_level = (string *)
SKILL_OB->query_immediate_children(implode(bits[0..i], "."));
for ( j = 0; j < sizeof( same_level ); j++ ) {
new_skills[ same_level[ j ] ] = tmp_lvl;
map_delete( _bonus_cache, same_level[ j ] );
}
}
} else {
tmp_lvl = 0;
}
}
}
/* Includes the current skill */
for (i=0;i<sizeof(recursive_skills);i++) {
if ( !not_there( recursive_skills[ i ] ) ) {
new_skills[recursive_skills[i]] += lvl;
if (new_skills[recursive_skills[i]] < 0) {
new_skills[recursive_skills[i]] = 0;
}
}
map_delete(_bonus_cache, recursive_skills[i]);
}
if ( not_there( skill ) ) {
new_skills[skill] = lvl;
}
/*
* If it is not a only_leaf heirarchy, then fix up all the lower level
* average skill levels.
* The first element is the current skill.
*/
tree = (string *)SKILL_OB->query_skill_tree(skill);
for (i=1;i<sizeof(tree);i++) {
int total, j;
same_level = (string *)SKILL_OB->query_immediate_children(tree[i]);
if (sizeof(same_level)) {
total = 0;
for (j=0;j<sizeof(same_level);j++) {
/* If it does not exist. Set it from the top value down. */
if ( not_there( same_level[ j ] ) ) {
new_skills[ same_level[ j ] ] = new_skills[ tree[ i ] ];
map_delete( _bonus_cache, same_level[ j ] );
}
total += new_skills[same_level[j]];
}
new_skills[tree[i]] = total/sizeof(same_level);
map_delete( _bonus_cache, tree[ i ] );
}
}
/* Update the high level players table */
if ( interactive( this_object() ) &&
( guild = (string)this_object()->query_guild_ob() ) ) {
if ( stringp( guild ) ) {
// TOP_TEN_HANDLER->player_skill_advance( explode( guild, "/" )[ 2 ],
//this_object() );
guild->skills_advanced( this_object(), skill, new_skills[ skill ] );
}
}
if((lvl == 1) && userp(this_object()) && (!exp || objectp(exp))) {
if(!exp)
exp = previous_object();
if(!tm_check_ok(skill, exp)) {
new_skills[skill] -= 1; // take the award away again.
return 0;
} else {
TASKER->award_made( (string)this_object()->query_name(),
base_name( exp ), skill, new_skills[ skill ] );
}
}
/* Make sure that there is at most one call_out running */
if ( find_call_out( "reset_all" ) == -1 ) {
call_out( "reset_all", 1 );
}
if(interactive(this_object()) && !this_object()->query_auto_loading())
this_object()->save();
return 1;
} /* add_skill_level() */
/**
* This method returns the skill as it should be modified by the
* stats associated with it.
* @param lvl the level to modify
* @param skill the skill the modify the bonus of
* @param use_base_stats use the real unmodified stat values
* @see query_skill_bonus()
* @return the stat modification
*/
varargs int stat_modify( int lvl, string skill, int use_base_stats ) {
int i, stat;
string stat_bonus;
float bonus;
bonus = 0.0;
if ( !_stat_cache[ skill ] || use_base_stats ) {
stat_bonus = (string)SKILL_OB->query_skill_stat(skill);
foreach ( i in stat_bonus ) {
switch( i ) {
case 'C' :
if ( use_base_stats )
stat = (int)this_object()->query_real_con();
else
stat = (int)this_object()->query_con();
break;
case 'D' :
if ( use_base_stats )
stat = (int)this_object()->query_real_dex();
else
stat = (int)this_object()->query_dex();
break;
case 'I' :
if ( use_base_stats )
stat = (int)this_object()->query_real_int();
else
stat = (int)this_object()->query_int();
break;
case 'S' :
if ( use_base_stats )
stat = (int)this_object()->query_real_str();
else
stat = (int)this_object()->query_str();
break;
case 'W' :
default :
if ( use_base_stats )
stat = (int)this_object()->query_real_wis();
else
stat = (int)this_object()->query_wis();
}
//bonus += ( stat - 13 ) * 3;
if (stat > 0) {
bonus += (log(stat) / 9.8) - 0.25;
} else if (stat < 0) {
bonus -= (log(-stat) / 9.8) + 0.25;
} else {
bonus -= 0.25;
}
if ( !_stat_cache[ i ] ) {
_stat_cache[ i ] = ({ skill });
} else {
_stat_cache[ i ] |= ({ skill });
}
}
if ( !use_base_stats ) {
_stat_cache[ skill ] = ({ bonus, stat_bonus });
}
} else {
bonus = _stat_cache[ skill ][ 0 ];
stat_bonus = _stat_cache[ skill ][ 1 ];
}
i = strlen( stat_bonus );
if ( i ) {
//return lvl + ( lvl * bonus ) / ( i * 60 );
stat = to_int(lvl + ( lvl * bonus ));
if (stat < 0) {
return 0;
}
return stat;
}
return lvl;
} /* stat_modify() */
/*
* Handy fact: stat_modify( 100, skill ) - 35 is the stat total
* for that skill.
*/
/**
* This method calculates the bonus for the skill. It takes the raw
* level and turns that into a bonus and then adds on the stats
* modifications.
* @param lvl the level to turn into bonus
* @param skill the skill to modify the bonus of
* @param use_base_stats use the real unmodified stats
* @return the bonus associated with the skill
*/
varargs int calc_bonus( int lvl, string skill, int use_base_stats ) {
// int bonus, stat, i;
if (lvl > 60) {
lvl = 170 + ((lvl-60) >> 1);
} else if (lvl > 40) {
lvl = 150 + (lvl-40);
} else if (lvl > 20) {
lvl = 100 + ( ((lvl-20)*5) >> 1);
} else {
lvl = lvl * 5;
}
if ( !use_base_stats ) {
_bonus_cache[ skill ] = lvl;
}
return stat_modify( lvl, skill, use_base_stats );
} /* calc_bonus() */
/**
* This method does a skill successful check. Does this check:<br>
* (bonus + mos) >= random(200)
* @param str the skill to check
* @param mod the modification value
* @return 1 if the skill check is successful
*/
int query_skill_successful(string str, int mod) {
return (query_skill_bonus(str, 0) + mod >= random(200));
} /* query_skill_successful */
/**
* This method adds a teaching offer to the living object.
* @param ob the object teaching us
* @param skill the skill they are teaching
* @param num the number of levels they are teaching
* @param lvl the level they are teaching us from
* @param xp the cost of the level increase in xp
*/
void add_teach_offer(object ob, string skill, int num, int lvl, int xp) {
_teach_offer[ob] = ({ skill, num, lvl, xp });
} /* add_teach_offer() */
/**
* This method returns the current list of teach offerings on the
* living object.
* @return the mapping containing the teach offerings
*/
mapping query_teach_offer() { return copy(_teach_offer); }
/**
* The method to call when we stop teaching skills. THis will stop the
* stuff being taught if the stop is successful, and only teach partial
* amounts if we are not finished yet.
* @param left the amount of time left
* @param bing the data associated with the command
*/
void stop_teaching_skills(int left, mixed bing) {
object ob;
if (left > 0) {
/* Someone did a stop! Naughty frogs! */
if (bing[O_OTHER_PER] == this_object()) {
say(this_object()->short() + " stops teaching themselves some "
"skills.\n");
} else if (previous_object() == this_object()) {
ob = bing[O_OTHER_PER];
tell_object(ob, this_object()->short() + " interupts your "
"training.\n");
} else {
ob = this_object();
tell_object(ob, bing[O_OTHER_PER]->short() + " interupts your "
"training.\n");
}
say(bing[O_OTHER_PER]->short() + " stops teaching some skills to " +
this_object()->short() + ".\n",
({ this_object(), bing[O_OTHER_PER] }));
this_object()->adjust_time_left(-((int)this_object()->query_time_left()));
this_object()->set_interupt_command(0);
return ;
}
if (previous_object() != this_object()) {
/* First make sure we dont get the level twice... */
return ;
}
// additional test added by ceres coz people are getting put into negative
// xp by getting taught twice somehow.
if(this_object()->query_xp() < bing[O_XP]) {
write("Something has gone wrong. :(\n");
return;
}
/* Ok... We did it! Finished! */
if (this_object() != bing[O_OTHER_PER]) {
bing[O_OTHER_PER]->adjust_xp(bing[O_XP]/10);
}
this_object()->adjust_xp(-bing[O_XP]);
add_skill_level(bing[O_SKILL], bing[O_NUM], bing[O_XP]);
if (this_object() != bing[O_OTHER_PER]) {
tell_object(this_object(), "You finish learning " + bing[O_NUM] +
" levels of "
+ bing[O_SKILL] + " from " + bing[O_OTHER_PER]->short() +
".\n");
tell_object(bing[O_OTHER_PER], this_object()->short() + " finishes " +
"learning " + bing[O_NUM] + " levels of "
+bing[O_SKILL] + " from you.\n");
say(this_object()->short() + " finishes learning some skills "+
"from "+bing[O_OTHER_PER]->short()+".\n",
({ this_object(), bing[O_OTHER_PER] }));
} else {
tell_object(this_object(), "You finish teaching yourself " + bing[O_NUM] +
" levels of " + bing[O_SKILL] + ".\n");
say(this_object()->short() + " finishes learning some skills "
"from " + this_object()->query_objective() + "self.\n",
({ this_object(), bing[O_OTHER_PER] }));
}
} /* stop_teaching_skills() */