Skill Daemon

// ----------------------------------------------------------------------------

/*
  Skill_d controls weapon skill learning.
  It is based off skill_pts which are used to calculate the actual skill.

  Note: Adventurer is a 'special' class. When changing classes, points are
        not recalculated on change out of the class (to adventurer). This
        would cause something like Longsword from Fighter -> Adv -> Paladin
        to drop a lot, whereas ideally it should remain fairly high.

        This means adventurers when they skill up will be ignored if they have
        too high a skill (ie fighter -> adv) but will be clipped if they were
        too low. Otherwise they would gain 'free' skills.
*/

#include <wizards.h>
// This needs to be changed before installing to just the default skills.h
#ifdef REALMUD
#include <skills.h>
#else
#include "skills.h"
#endif
#include <combat.h>
#include <terminal.h>

#define WIS_CHECK 20
#define INT_CHECK 20
#define MAX_LEARN 20

// ----------------------------------------------------------------------------

private static mapping learning_rate;
private static mapping partial_learning;
private static mapping max_learning;
private static mapping taught_skills;
private static string *skill_types;

int calc_skill(object person, string type, int amount);
int calc_points(object person, string type, int amount);
// ----------------------------------------------------------------------------

void create() {
  if ( is_clone() ) {
    destruct( this_object() );
    raise_error( "*** Do not clone this object.\n" );
  }

  learning_rate = SKILL_RATES;

  max_learning = MAX_LEARNING;

  taught_skills = ([]);

  skill_types = SKILL_TYPES;
}

// find_k() calculates the actual number of points that are needed for
// a particular skill rate per skill point.
int find_k( int rate ) {
  int k;
  k = 6 + (11 - rate) * (11 - rate) / 4;
  return k;
}

// Log any errors found in players skills.
void log_error(object person, string type, string msg) {
  mapping spts;
  string who;
  int sk, calc_sk;
  int skp, calc_skp;

  if (!person) {
     // This shouldnt happen, but just incase give it something.
     who = "Unknown";
     sk = -1;
     skp = -1;
     calc_sk = -1;
     calc_skp = -1;

  } else {
     who = person->query_real_name();
     // use the mapping as a direct query uses tmp value.
     sk = person->query_weapon_skills()[type];
     spts = person->query_skill_pts(); // can be a 0 if player has no skills
     if (spts) skp = spts[type];
     calc_sk = calc_skill(person, type, skp);
     calc_skp = calc_points(person, type, sk);
  }

  log_file("SKILL_ERROR","["+ctime()+"] "+who+" "+msg+" in "+type+" "+
           "Skill "+sk+" -> "+calc_sk+", Pts "+skp+" -> "+calc_skp+"\n"); 
}

// Attempt to learn a skill.
// Note: This function uses max variables.
mapping do_learn( mapping pts, mixed weapon, object attacker, int weap_sk,
                 int Int, int Wis, string Name, string class, int pl_level ) {
  object person;
  string type, colour,reset;
  int cur_skill, rate, level_diff, max_skill;
  int calculated_skill;
  int learn_bonus;

  if ( !pts )
    pts = ([]);

  if (!attacker) return pts;
  if (attacker->query_is_player()) return pts;

  person = previous_object();
  if (!living(person)) person = this_player();

  if ( weapon )
    if (objectp(weapon))
      type = (string)weapon->query_weapon_type();
    else
      type = weapon;
  else
    type = "Unarmed";

  // Capping p_level to 20
  if (pl_level > MAX_LEARN) pl_level = MAX_LEARN;

  // To learn from your actual skill
  weap_sk -= (int)person->query_tmp_skill( type );

  if ( member(learning_rate, class) ) {
    rate = learning_rate[class][type];
    max_skill = max_learning[class][type];
  } else {
    rate = 0; /* why doesn't this class exist? */
    if ( member(max_learning, class) )
      max_skill = max_learning[class][type];
    else
      max_skill = MAX_UNDEFINED_SKILL;
  }
  
  learn_bonus = (int)person->hook("skill_rate_bonus", 
                                  ({type, max_skill, rate, weapon }));

  if(max_skill>100)
    max_skill=100;
  
  level_diff = ( (pl_level<MAX_LEARN?
               pl_level:MAX_LEARN) + 1 + (weap_sk*MAX_LEARN)/(weap_sk<max_skill?
               (max_skill+1):(weap_sk+1)))/2 - (int)attacker->query_level();

  if ( taught_skills[Name] == type ) {
    level_diff -= 10;
//  tell_object( person, "You feel the training helps you a little.\n" );
  }

  if ( level_diff > 5 )
    return pts;

  calculated_skill = calc_skill(person, type, pts[type]);

  // Cap skill at the max hidden skill.
  if (calculated_skill >= MAX_HIDDEN_SKILL)
    return pts;

  // Want to go by say int and wis also a random
  // Don't go above max skill and get 1/20th per level of max skill
  // so if max is 100 then get 5 skill per level +5

  if ( (random( max_skill + 1 ) >= weap_sk &&
       weap_sk < max_skill * (pl_level + 1) / MAX_LEARN ) ||
       (weap_sk >= max_skill && pl_level > MAX_LEARN - 2 &&
        random( 101 ) >= weap_sk && max_skill) ) {

    // To allow anyone up, just making it harder
    if ( weap_sk >= max_skill )
      rate -= 5;
 
    if ( random( Int + rate + learn_bonus ) >
           random( INT_CHECK ) + level_diff &&
         random( Wis + rate + learn_bonus ) >
           random( WIS_CHECK ) + level_diff ) {
      weap_sk=(person->query_weapon_skills())[type];
      // Only recalc if skill < max display as points can go higher
      // to allow death of high levels to be less painful

      // If too low a skill, bring points down to visible level.
      if (weap_sk < MAX_DISPLAY_SKILL &&
          weap_sk < calculated_skill) {
        log_error(person, type, "too low a skill");
        pts[type] = calc_points(person,type,weap_sk);
      }

      // Too high a skill, bring points up to visible level.
      if (weap_sk < MAX_DISPLAY_SKILL &&
          weap_sk > calculated_skill) {
        if (class) {
          log_error(person, type, "too high a skill");
          pts[type] = calc_points(person, type, weap_sk);
        } else {
          log_error(person, type, "adventurer skill high, not changed");
        }
      }
      // Now increase points and raise skill if necessary.
      pts[type]++;
      person->set_skill_pts(type,pts[type]);
      if ((person->query_weapon_skills())[type]>weap_sk) {
        if (person->query_ansi()) {
           colour=ANSI_YELLOW;
           reset=ANSI_RESET;
        }
        else {
           colour="";
           reset="";
        }
        tell_object(person,colour+"Your skill in "+type+" has gone up! "+
                    "You now have "+(person->query_weapon_skills())[type]+".\n"+
          reset);
      }
    }
  }
  return pts;
}

// Remove all visible skills and set all skill_pts to 0.
int clear_skills( object ob ) {
  mapping skill_map;
  int i, j;

  skill_map = (mapping)ob->query_weapon_skills();
  if ( skill_map ) {
    string *ind;
    ind = m_indices( skill_map );

    for ( i = sizeof( ind ) ; i-- ; ) {
      j += ob->remove_skill( ind[i] );
      ob->set_skill_pts( ind[i], 0);
    }
  }

  return j;
}

// Reduce skills on death (lose 1/50 (2%) from all skill_pts
int reduce_skill( object ob ) {
  mapping pl_skills;
  string *ind;
  int max_skill, i, points, rate, p_level, new_points, bonus;
  string class;

  class = (string)ob->query_class();
  p_level = (int)ob->query_level();
  pl_skills = (mapping)ob->query_skill_pts();
  if (!pl_skills) return 0;

  ind = m_indices(pl_skills);
  for ( i = sizeof( ind ) ; i-- ; ) {
    if ( member(learning_rate, class) )
      rate = learning_rate[class][ind[i]];
    else
      rate = 0;
    if ( max_learning[class] )
      max_skill = max_learning[class][ind[i]];
    else
      max_skill = MAX_UNDEFINED_SKILL;

    points = pl_skills[ind[i]];
   
    bonus = (int)ob->hook("skill_loss_bonus", ({ind[i], rate, points, max_skill}));
    points -= points / 50;

    points += bonus;
    if (points < 0) points = 0; // Shouldnt happen, but the hook could anything
    ob->set_skill_pts( ind[i], points );
  }

  return 1;
}

// Original function used to set 'initial' skills when skills were
// implemented. This should no longer be necessary for anything.

int initialise_skills( object player, string choice) {
  string *categories;
  string class, prim_abi;
  float temp, k;
  int xp, i, orig_k, rate, val, Int, Wis, num_pts, max_skill;
  int pl_xp, pl_level, pl_int, pl_wis;

  if ( !player ) return 0;

  prim_abi = choice;
  if ( !prim_abi ) return 0; /* Hasn't been chosen */

  clear_skills( player );

  pl_xp = (int)player->query_exp();
  pl_level = (int)player->query_level();
  class = (string)player->query_class();
  pl_int = (int)player->query_int();
  pl_wis = (int)player->query_wis();

  categories = m_indices( learning_rate[class] );
  xp = pl_xp / sizeof( categories );
  xp = xp / 20; /* ie. number of hits to make that xp. I hope */
  xp = xp / 2; /* Half for all skills, and Half for a chosen skill */

  for ( i = 0 ; i < sizeof( categories ) ; ++i ) {
    if ( class )
      rate = learning_rate[class][categories[i]];   
    else
      rate = 6; /* basic adv. ones */

    Int = pl_int + rate; /* Assume half time less stats and half more */
    if ( Int < 1 ) Int = 1;
    Wis = pl_wis + rate; /* for the passing of the int/wis checks */
    if ( Wis < 1 ) Wis = 1;

    orig_k = find_k( rate );
    k = (float)(orig_k * INT_CHECK * WIS_CHECK / (Int * Wis));
    if ( !k || (k == 0.0) ) k = 1.0;
    temp = sqrt( (float)(xp / k) );
    val = (int)temp; 

    max_skill = max_learning[class][categories[i]] * (pl_level+1)/MAX_LEARN;
  if ( val > max_skill*95/100 ) val = max_skill - max_skill * 5 / 100;
    if ( val )
      player->set_skill( categories[i], val );
    player->set_skill_pts( categories[i], orig_k * val );
  }

  xp = pl_xp / 80; /* 1/4 xp used for skill */

  if ( class )
    rate = learning_rate[class][prim_abi];   
  else
    rate = 6; /* basic adv. ones */

  Int = pl_int + rate; /* Assume half time less stats and half more */
  if ( Int < 1 ) Int = 1;
  Wis = pl_wis + rate; /* for the passing of the int/wis checks */
  if ( Wis < 1 ) Wis = 1;

  orig_k = find_k( rate );
  k = (float)(orig_k * INT_CHECK * WIS_CHECK / (Int * Wis));
  temp = sqrt( (float)(xp / k) );
  val = (int)temp;

  // partial skill from level.
  // Fighter requires 30 points for max skill so choose half.
  num_pts = pl_level * 15;
  num_pts = val * orig_k + num_pts;
  val = num_pts / orig_k;

  max_skill = max_learning[class][prim_abi] * (pl_level + 1) / MAX_LEARN;
  if ( val > max_skill*95/100 ) val = max_skill - max_skill * 5 / 100;
  if ( val )
    player->set_skill( prim_abi, val );
  player->set_skill_pts( prim_abi, orig_k * val );

  return 1; 
}

// Changing class code.
// This should be called after the player has changed classes.
// 
void change_class( object player ) {
  mapping pts;
  string *ind;
  string class;
  int calculated_skill;
  int calculated_points;
  int i;
  int skill;

  class = (string)player->query_class();

  /* If going to Adv dont lose skills till rejoin */
  if (!class) {
    return;
  }

  if (!learning_rate[class]) return;

  ind = m_indices( learning_rate[class] );
  pts = player->query_skill_pts();
  if (!pts) pts = ([]); // Can be a 0 if no skills at all
  for ( i = sizeof( ind ) ; i-- ; ) {
    skill = player->query_skill( ind[i] );
    calculated_skill = calc_skill(player, ind[i], pts[ind[i]]);

    // If calculated points == skill then do nothing, as same skill rate.
    // Just requires the 'death' change at the end.

    // Changed class to a harder skill. Drop skill drop based on points.
    if (skill > calculated_skill) {
      player->set_skill( ind[i], calculated_skill);
    }

    // Changed class to an easier skill. Drop points down and leave skill.
    if (skill < calculated_skill) {
      calculated_points = calc_points(player, ind[i], skill);
      player->set_skill_pts(ind[i], calculated_points);
    }
  }
  reduce_skill( player );
}

// Calculate a skill value based on the number of points passed in.
int calc_skill(object person, string type, int amount) {
  int max_skill,rate,k1,k2,weap_sk;
  string class;

  class = person->query_class();
  if ( member(learning_rate, class) ) {
    rate = learning_rate[class][type];
    max_skill = max_learning[class][type];
  } else {
    rate = 0;
    if (member(max_learning,class))
      max_skill = max_learning[class][type];
    else
     max_skill = MAX_UNDEFINED_SKILL;
  }
  k1 = find_k(rate);
  if (amount > (max_skill-1)*k1) {
    k2 = find_k(rate-5);
    weap_sk = (amount - (max_skill-1)*k1)/k2 + max_skill;
  } else {
    weap_sk = amount/k1 + 1;
  }
  return weap_sk;
}

// For a given skill, calculate the points to achieve the skill.
int calc_points(object person, string type, int amount) {
  int max_skill,rate,k1,k2,points;
  string class;

  if (amount == 1) return 1;
  class = person->query_class();
  if ( member(learning_rate, class) ) {
    rate = learning_rate[class][type];
    max_skill = max_learning[class][type];
  } else {
    rate = 0;
    if ( member(max_learning, class) )
      max_skill = max_learning[class][type];
    else
      max_skill = MAX_UNDEFINED_SKILL;
  }
  k1 = find_k(rate);
  if (amount >= max_skill) {
    k2 = find_k(rate-5);
    points = (amount - max_skill)*k2 + (max_skill-1)*k1; 
  } else {
    points = (amount - 1)*k1;
  }
  if (points < 0) points = 0;
  return points;
}

// Zhou teaching.
void teach_skill( string who, string what ) {
  if (member(taught_skills, who))
    taught_skills[who] = what;
  else
    taught_skills += ([ who : what ]);
  call_out( "forget_skill", 600, who );
}

void forget_skill( string who ) {
  if ( taught_skills[who] )
    m_delete( taught_skills, who );
}

mixed query_taught_skills( string who ) {
  return who ? taught_skills[who] : copy_mapping(taught_skills);
}

mapping query_learning_rate() {
  return copy_mapping(learning_rate);
}

string *query_skill_types() {
  return skill_types + ({});
}

// ------------------------------------------------------------------------ EOF