/**
* This is the taskmaster handler.
* It includes functions that deal with all sorts of skill checks
* and give out skill increases.
*
* @changed Changed to use general.language instead of other.language.
* - Shaydz, 2002.
* @changed Tried to improve the level of information returned by the
* perform task function.
* - Shiannar, 2001.
* @changed Added a new function to handle TMing points skills.
* - Sandoz, 2002.
* @changed Changed compare_skills() to be more skills dependent.
* - Sandoz, June 2003.
* @changed Changed success/fail degree to be slightly skill based.
* - Sandoz, July 2003.
*/
#include <tasks.h>
#undef DEBUG "shaydz"
#ifdef DEBUG
#define WATCH "molark"
#endif
#define LOG_STATS 2
/*
* These are the defines for tweaking the TM rates at
* different skill levels.
*/
#define BASE 30.0
#define DECAY 350.0
#define MODIFIER 5
#define SAVE_FILES "/save/tasks/"
#define TIME_PER_USER 1800
#define CONT_OB control[0]
#define CONT_SK control[1]
#define CONTROL ( pointerp(control) && CONT_OB && CONT_SK )
mapping stats;
nosave int last_save;
nosave int last;
nosave string skill;
nosave mixed control;
/** @ignore yes */
void create() { seteuid("Root"); }
varargs mixed attempt_task( int difficulty, int bonus, int upper, mixed extra,
int use_class );
varargs mixed attempt_task_e( int difficulty, int bonus, int upper, int half,
int use_class );
/** @ignore yes */
int is_valid_tm( object person, string skill ) {
skill = explode( skill, "." )[0];
// if( interactive(person) && !person->query_property("tm_"+skill) ) {
// person->add_property("tm_"+skill, 1, 180 );
// return 1;
// }
//
// This is temporary until I decide how I want the tm system to handle things.
if( interactive(person) && !person->query_property("last_tm") ) {
person->add_property("last_tm", 1, 300 );
return 1;
}
return 0;
} /* is_valid_tm() */
/** @ignore yes */
mapping query_stats( string s_name ) {
if( skill != s_name ) {
skill = s_name;
if( file_size( SAVE_FILES + skill +".o" ) > 0 )
unguarded( (: restore_object, SAVE_FILES + skill :) );
else
stats = 0;
}
if( !stats )
stats = ([ ]);
return copy( stats );
} /* query_stats() */
/** @ignore yes */
int query_last() { return last; }
/** @ignore yes */
mixed *query_control() { return control; }
/**
* This function should only be used in the very rare situations
* where the last skill checked with query_skill_bonus() wasn't the
* one relevant for the task attempt.
*
* @param args an array consisting of ({ object player, string skill })
*/
void set_control( mixed *args ) { control = args; }
/** @ignore yes */
void reset_control() { control = 0; }
/** @ignore yes */
void award_made( string p_name, string o_name, string s_name, int level ) {
user_event("inform", "TM:("+ctime(time())+") "+p_name+" gains a level in "+s_name+" from "+
o_name+" at level "+level, "skill");
log_file("TMS", "TM:(%s) %s gains a level in %s from %s at level %d\n",
ctime(time()),p_name,s_name,o_name,level);
#ifdef LOG_STATS
#if LOG_STATS == 1
query_stats( s_name );
if( !stats[ level ] )
stats[ level ] = ([ ]);
stats[ level ][ explode( o_name, "#" )[ 0 ] ]++;
#else
query_stats("summary");
// These two just skew the stats so we don't record them.
if( s_name[<7..] == ".points" || s_name[0..13] == "general.language")
return;
s_name = explode( s_name, ".")[0];
if( !stats[s_name])
stats[s_name] = ({ ({ 0 , 0 }), ({ 0, 0 }), ({ 0, 0 }) , ({0,0})});
switch( level ) {
case 0..149:
stats[s_name][0] = ({ stats[s_name][0][0], stats[s_name][0][1]+1 });
break;
case 150..299:
stats[s_name][1] = ({ stats[s_name][1][0], stats[s_name][1][1]+1 });
break;
case 300..599:
stats[s_name][2] = ({ stats[s_name][2][0], stats[s_name][2][1]+1 });
break;
default:
stats[s_name][3] = ({ stats[s_name][3][0], stats[s_name][3][1]+1 });
break;
}
if( last_save < time() - 300 ) {
unguarded( (: save_object, SAVE_FILES + "summary" :) );
last_save = time();
}
#endif
#endif
} /* award_made() */
#if LOG_STATS == 2
/** @ignore yes */
void skill_checked( string s_name, int level ) {
query_stats("summary");
if( s_name[<7..] == ".points" || s_name[0..13] == "general.language")
return;
s_name = explode(s_name, ".")[0];
if( !stats[s_name] )
stats[s_name] = ({ ({ 0 , 0 }), ({ 0, 0 }), ({ 0, 0 }), ({0,0}) });
switch(level) {
case 0..149:
stats[s_name][0] = ({ stats[s_name][0][0]+1, stats[s_name][0][1] });
break;
case 150..299:
stats[s_name][1] = ({ stats[s_name][1][0]+1, stats[s_name][1][1] });
break;
case 300..599:
stats[s_name][2] = ({ stats[s_name][2][0]+1, stats[s_name][2][1] });
break;
default:
stats[s_name][3] = ({ stats[s_name][3][0]+1, stats[s_name][3][1] });
break;
}
if( last_save < time() - 300 ) {
unguarded( (: save_object, SAVE_FILES + "summary" :) );
last_save = time();
}
} /* skill_checked() */
#endif
/** @ignore yes */
string *query_skill_files() {
return unguarded( (: get_dir, SAVE_FILES +"*.o" :) );
} /* query_skill_files() */
/** @ignore yes */
void clear_skill_files() {
string word;
foreach ( word in unguarded( (: get_dir, SAVE_FILES +"*.o" :) ) )
unguarded( (: rm, SAVE_FILES + word :) );
skill = 0;
} /* clear_skill_files() */
/**
* This function will attempt a task. It handles all the stuff about
* looking up the skill, and giving the tm advance, but doesn't give
* any messages to the player, you'll have to do that.
*
* @param person the one attempting the task (could be any living thing)
* @param skill the skill tested against
* @param difficulty the lowest bonus where the attempt can succeed
* @param tm_type This should use one of the standard definitions in
* /include/tasks.h
* They are:
* TM_FIXED - for use where the difficulty is a fixed value
* TM_FREE - for use when the tm attempt doesn't cost anything.
* TM_CONTINUOUS - for use in continuous actions eg. combat or sneak
* TM_COMMAND - for use with guild commands
* TM_RITUAL - when the action is a ritual
* TM_SPELL - when the action is a spell
* @param use_class This is 0 when you don't want a class returned,
* and 1 when you do.
*
* @return BARF if something screwed up, AWARD if the task succeeded, and
* should give an advance, SUCCEED if it succeeded, FAIL if it failed.
*
* @example
*
* switch( TASKER->perform_task( person, "covert.manipulation.stealing", TM_COMMAND ) ) {
* case AWARD :
* tell_object( person, "%^YELLOW%^You manage to grasp the principles "
* "of stealing more firmly.%^RESET%^\n");
* // Note, no break;
* case SUCCEED :
* // Whatever happens when it succeeds
* break;
* default :
* // Whatever happens when it fails
* }
*/
varargs mixed perform_task( object person, string skill, int difficulty,
int tm_type, int use_class ) {
int bonus, extra;
mixed result;
bonus = person->query_skill_bonus( skill );
if( difficulty )
extra = 6 * sqrt(difficulty);
switch( tm_type ) {
case TM_FIXED:
result = attempt_task( difficulty, bonus, 100, extra, use_class );
break;
case TM_FREE:
result = attempt_task( difficulty, bonus, 25, extra, use_class );
break;
case TM_CONTINUOUS:
result = attempt_task( difficulty, bonus, 40, extra, use_class );
break;
case TM_COMMAND:
if( explode( skill, ".")[0] == "covert")
result = attempt_task_e( difficulty, bonus, 50, 30, use_class );
else
result = attempt_task( difficulty, bonus, 50, extra, use_class );
break;
case TM_RITUAL:
result = attempt_task_e( difficulty, bonus, 30, 25, use_class );
break;
case TM_SPELL:
result = attempt_task_e( difficulty, bonus, 30, 40, use_class );
break;
case TM_NONE:
result = attempt_task_e( difficulty, bonus, 1, extra, use_class );
if( result == AWARD )
result = SUCCEED;
break;
default:
error("Invalid TM type in perform_task().\n");
}
if( classp(result) ) {
if( result->result == AWARD ) {
if( !is_valid_tm( person, skill ) ||
!person->add_skill_level( skill, 1, PO ) ) {
result->result = SUCCEED; // No advance.
}
}
} else {
if( result == AWARD ) {
if( !is_valid_tm( person, skill ) ||
!person->add_skill_level( skill, 1, PO ) ) {
result = SUCCEED; // No advance.
}
}
}
return result;
} /* perform_task() */
/**
* This function will compare the skills of two objects and return which one
* won and if the winner got a TM.
* With equal skills the chances are 50/50 as to who will win. As the balance
* shifts so do the chances. Additionally a modifier can be applied to
* account for conditions favouring one or the other. This should be
* considered a percentage eg. -50 will add 50% to the defenders chances of
* winning.
*
* @param offob the attacking object
* @param offskill the name of the skill the attacker is using
* @param deffob the defending object
* @param deffskill the name of the skill the defender is using
* @param modifier the percentage modifier
* @param off_tm_type this should be one of the standard definitions in
* /include/tasks.h and is for the attacker
* @param def_tm_type this should be one of the standard definitions in
* /include/tasks.h and is for the defender
* @param use_class whether or not to return a class like perform_task()
*
* @example
*
* switch( TASKER->compare_skills( attacker, "fighting.combat.melee.sharp",
* defender, "fighting.combat.parry.unarmed",
* 25, TM_COMMAND, TM_FREE ) {
* case OFFAWARD:
* tell_object( attacker, "%^YELLOW%^You manage to grasp one of the "
* "principles of slicing people up more firmly.%^RESET%^\n");
* case OFFWIN:
* tell_object( defender, "You lose an arm.\n");
* tell_room( ENV(attacker), defender->one_short()+" loses an arm.\n",
* defender );
* break;
* case DEFAWARD:
* tell_object( defender, "%^YELLOW%^You feel more able to keep your "
* "arms attached when parrying unarmed.%^RESET%^\n");
* case DEFWIN:
* tell_object( defender, "You keep your arm attached.\n");
* tell_room( ENV(defender), defender->one_short()+" keeps "+
* defender->HIS+" arm attached.\n", defender );
* break;
* }
*
* @see perform_task()
*
*/
varargs mixed compare_skills( object offob, string offskill, object defob,
string defskill, int modifier, int off_tm_type, int def_tm_type,
int use_class ) {
int offbonus, defbonus, percent;
mixed result;
offbonus = offob->query_skill_bonus(offskill);
defbonus = defob->query_skill_bonus(defskill);
if( !offbonus && !defbonus ) {
percent = 50;
} else {
percent = ( offbonus * 100 ) / ( offbonus + defbonus );
if( offbonus > defbonus )
percent += 100 * offbonus / ( offbonus + defbonus ) - 50;
else if( defbonus > offbonus )
percent -= 100 * defbonus / ( offbonus + defbonus ) - 50;
}
// The difficulty may be weighted.
percent += modifier;
if( random(100) < percent ) {
// The winner is the offender, now do a TM check.
result = perform_task( offob, offskill, defbonus + modifier,
off_tm_type, use_class );
if( classp(result) ) {
result->result = ( result->result == AWARD ? OFFAWARD : OFFWIN );
return result;
} else {
return ( result == AWARD ? OFFAWARD : OFFWIN );
}
} else {
// The winner is the defender.
result = perform_task( defob, defskill, offbonus + modifier,
def_tm_type, use_class );
if( classp(result) ) {
result->result = ( result->result == AWARD ? DEFAWARD : DEFWIN );
return result;
} else {
return ( result == AWARD ? DEFAWARD : DEFWIN );
}
}
} /* compare_skills() */
/**
* This function will attempt to figure out if a tm should be given
* in the points skill used. It handles all the stuff about looking
* up the skill, giving the tm advance, and reducing the guild points.
* It will also give a generic tm message to the player.
*
* @param person the one attempting the points task (could be any living thing)
* @param type the type (covert, magic etc.) of points skill to check against
* @param amount the amount of guild points to check against
* @return 1 if player has enough available guild points of the type, 0 if not
*
* @example
*
* if( !TASKER->point_tasker( person, "covert", 80 ) ) {
* add_failed_mess("You can't concentrate enough to hide.");
* return 0;
* }
*/
int point_tasker( object person, string type, int amount ) {
string skill;
int m, level, bonus;
float b;
if( ( amount < 1 ) || ( person->query_gp() < amount ) )
return 0;
person->adjust_gp( -amount );
skill = "general.points";
level = (int)person->query_skill( skill );
bonus = (int)person->query_skill_bonus( skill );
b = to_float( ( bonus < 50 ? 50 : bonus ) + ( level < 50 ? 50 : level ) );
m = (int)person->stat_modify( 10, skill, 1 );
if( random(1000) < to_int( 25 * sqrt( to_float( 20 + amount ) ) * ( m / b ) ) &&
is_valid_tm( person, type ) && person->add_skill_level( skill, 1, PO ) )
tell_object( person, "%^YELLOW%^You feel more able to concentrate "
"on this task than you thought.%^RESET%^\n");
return 1;
} /* point_tasker() */
/** @ignore yes */
float calc_upper( mixed upper, int bonus ) {
float tmp;
#ifdef DEBUG
tell_creator( DEBUG, "TM: %s - %s [lvl: %d, bonus: %d] OU: %O",
CONT_OB->query_name(), CONT_SK, CONT_OB->query_skill(CONT_SK),
bonus, upper );
#endif
// Reduce the upper dependant on their stats.
upper = CONT_OB->stat_modify( upper, CONT_SK );
#ifdef DEBUG
tell_creator( DEBUG, "Statted Upper: %O", upper );
#endif
// Reduce the upper dependant on their level.
tmp = exp( ( CONT_OB->query_skill(CONT_SK) - BASE ) / DECAY );
upper = upper / tmp - MODIFIER;
#ifdef DEBUG
tell_creator( DEBUG, "Levelled Upper: %O", upper );
#endif
// Prevent upper from going negative.
if( upper < 0 )
upper = 0;
#ifdef DEBUG
tell_creator( DEBUG, "Final Upper: %O", upper );
#endif
return upper;
} /* calc_upper() */
/**
* DO NOT USE THIS FUNCTION. Use perform_task() instead.
* This function will attempt a task and return whether it succeeded.<br>
* <br>
* Chance of /|\<br>
* success 100% __| ______..<br>
* | /<br>
* | /<br>
* | /<br>
* 0% __| .._____/<br>
* |_____________________\ Player's bonus<br>
* | | /<br>
* difficulty difficulty + margin<br>
* <br>
* Chance of /|\<br>
* gaining upper __| .<br>
* | |\<br>
* | | \<br>
* | | \<br>
* 0% __| .._____| \_____..<br>
* |_____________________\ Player's bonus<br>
* | | / <br>
* difficulty difficulty + margin<br>
* <br>
* @param difficulty the lowest bonus where the attempt can succeed
* @param bonus the bonus the player has in the relevant skill
* @param upper the maximum chance of getting an advance
* @param extra the margin control. If it is an int, the margin is set
* to extra. It it's 0 the margin will be calculated automatically from
* the difficulty as 3*sqrt(difficulty), if it's an array, it'll be
* calculated as extra[0]+extra[1]*sqrt(difficulty)
* @return BARF if something screwed up, AWARD if the task succeeded, and
* should give an advance, SUCCEED if it succeeded, FAIL if it failed.
* @see perform_task
*/
mixed attempt_task( int difficulty, int bonus, int upper, mixed extra,
int use_class ) {
class task_class_result return_class;
int margin, result;
#if LOG_STATS == 2
if( CONTROL )
skill_checked( CONT_SK, CONT_OB->query_skill( CONT_SK ) );
#endif
// If the bonus is below the difficulty, they fail.
if( bonus < difficulty ) {
if( use_class ) {
return_class = new( class task_class_result );
return_class->result = FAIL;
// Compatible with degree of failure, and not always critical.
if( difficulty < 1 )
difficulty = 1;
return_class->degree = 100 * bonus / difficulty;
return_class->degree -= 100 + random(50);
if( return_class->degree < -100 )
return_class->degree = -100;
return return_class;
} else {
return FAIL;
}
}
// Work out the margin between total failure and total success.
if( !extra )
if( !difficulty )
margin = 6;
else
margin = 6 * sqrt( difficulty );
else {
if( intp( extra ) )
margin = extra;
if( pointerp( extra ) )
margin = extra[ 0 ] + extra[ 1 ] * sqrt( difficulty );
}
if( !margin )
margin = 1;
// If the bonus is above the margin, they succeed.
if( bonus > difficulty + margin ) {
if( use_class ) {
return_class = new( class task_class_result );
return_class->result = SUCCEED;
// Compatible with degree of failure, and not always critical.
if( bonus < 1 )
bonus = 1;
return_class->degree = 100 + random(50);
return_class->degree -= 100 * ( difficulty + margin ) / bonus;
if( return_class->degree > 100 )
return_class->degree = 100;
return return_class;
} else {
return SUCCEED;
}
}
// They might fail in the margin.
result = 50 * ( bonus - difficulty ) / margin - random(50);
// This is so that the degree would be from -50 to -1 or 1 to 50.
if( result < 1 )
result--;
if( use_class ) {
return_class = new( class task_class_result );
return_class->degree = result;
}
if( result < 0 ) {
#ifdef DEBUG
if( CONTROL && CONT_OB->query_name() == WATCH )
tell_creator( DEBUG, "TM: %s Skill: %s [%d] [%d] - FAILED",
CONT_OB->query_name(), CONT_SK, bonus, difficulty );
#endif
if( use_class ) {
return_class->result = FAIL;
return return_class;
} else {
return FAIL;
}
}
// If information is available, adjust the chance to award based on stats.
if( CONTROL )
upper = calc_upper( upper, bonus );
// If they succeed, they might be awarded a level.
#ifdef DEBUG
tell_creator( DEBUG, "TM variable %d.",
( upper * ( difficulty + margin - bonus + 5 ) ) / margin );
#endif
if( random(200) < ( upper * ( difficulty + margin - bonus + 5 ) ) / margin ) {
if( use_class) {
return_class->result = AWARD;
return return_class;
} else {
return AWARD;
}
}
if( use_class ) {
return_class->result = SUCCEED;
return return_class;
} else {
return SUCCEED;
}
} /* attempt_task() */
/**
* DO NOT USE THIS FUNCTION. Use perform_task() instead.
* This method acts like attempt_task() except that the failure chance
* starts at 100% at the difficulty and is halved every (half) bonus levels.
*
* @param difficulty the lowest bonus where the attempt can succeed
* @param bonus the bonus the player has in the relevant skill
* @param upper the maximum chance of getting an advance
* @param half every time the bonus rise by half, the failure
* chance is halved
*
* @return BARF is something screwed up, AWARD if the task succeeded, and
* should give an advance, SUCCEED if it succeeded, FAIL if it failed.
*
* @see perform_task
*/
varargs mixed attempt_task_e( int difficulty, int bonus, mixed upper,
int half, int use_class ) {
class task_class_result return_class;
float fail_chance, degree, tmp;
#ifdef DEBUG
tell_creator( DEBUG, "Difficulty[%d], Bonus[%d], Upper[%d], Half[%d]",
difficulty, bonus, upper, half );
#endif
#if LOG_STATS == 2
if( CONTROL )
skill_checked( CONT_SK, CONT_OB->query_skill(CONT_SK) );
#endif
// If the bonus is below the difficulty, they fail.
if( bonus < difficulty ) {
if( use_class ) {
return_class = new( class task_class_result );
return_class->result = FAIL;
// Compatible with degree of failure, and not always critical.
if( difficulty < 1 )
difficulty = 1;
return_class->degree = 100 * bonus / difficulty;
return_class->degree -= 100 + random(50);
if( return_class->degree < -100 )
return_class->degree = -100;
return return_class;
} else {
return FAIL;
}
}
if( !half )
half = 2 * sqrt( difficulty );
fail_chance = exp( ( -0.693 * ( bonus - difficulty ) ) / half );
degree = ( random(100) + ( 100 * ( 1 - fail_chance ) ) - 99 );
// The degree will be 50 units wide.
switch( to_int(degree) ) {
case -1 :
break;
case 0 :
degree++;
break;
case 1 :
break;
default :
degree /= 2;
}
// This means we can no longer fail the task,
// so we make our degree rise above the 0-49 range.
#ifdef DEBUG
tell_creator( DEBUG, "Fail chance (first): %O", fail_chance );
#endif
if( fail_chance * 100 < 1 ) {
tmp = 50 + ( 50 * ( ( 100 * ( 1 - fail_chance ) ) - 100 ) );
if( tmp < 1 )
degree++;
else
degree += tmp;
}
if( degree > 99 )
degree = 99;
if( use_class ) {
return_class = new( class task_class_result );
return_class->degree = degree;
}
if( degree < 0 ) {
#ifdef DEBUG
tell_creator( DEBUG, "Fail chance: %O", fail_chance );
#endif
if( use_class ) {
return_class->result = FAIL;
return return_class;
} else {
return FAIL;
}
}
// If information is available, adjust the chance to award based on stats.
if( CONTROL )
upper = calc_upper( upper, bonus );
// If they succeed, they might be awarded a level.
#ifdef DEBUG
tell_creator( DEBUG,
"TM chance: fail chance: %O\n"
"TM chance: part 1: random(1000) < %O\n"
"TM chance: part 2: %O < %O",
fail_chance, ( upper * fail_chance * 5 ),
bonus, ( difficulty + ( half * 5 ) ) );
#endif
if( random(1000) < ( upper * fail_chance * 5 ) &&
bonus < ( difficulty + ( half * 5 ) ) ) {
#ifdef DEBUG
tell_creator( DEBUG, "TM given out, difficulty was %O, "
"upper was %O, bonus was %O.\n---", difficulty, upper, bonus );
#endif
if( use_class ) {
return_class->result = AWARD;
return return_class;
} else {
return AWARD;
}
}
#ifdef DEBUG
tell_creator( DEBUG, "---");
#endif
if( use_class ) {
return_class->result = SUCCEED;
return return_class;
} else {
return SUCCEED;
}
} /* attempt_task_e() */