/* This file contains the site's basic functionality: both the formulas and
 * the functions to be used on different pages
 */

/* Basic string functions */

  /* When writing possibly large integers, place commas at the right places
   * to make them easier to read.
   */
  function FancyInteger(number) {
    if (! ((number > 0)||(number < 0)) ) return "0"; 
    var s = new String(Math.floor(Math.abs(number)));
    var l = new Number(s.length);
    var m = new Number((l-1) % 3 + 1);
    var n = new Number((l-m) / 3);
    var t = new String(s.substring(0, m));
    for (var i = 0; i < n; i++) t += ","+s.substring(3*i+m, 3*(i+1)+m);
    if (number < 0) t = "-"+t;
    return t;
  }

  /* Erase leading spaces from a string */
  function TrimLeft(text) {
    text = new String(text);
    for (var i = 0; i < text.length; i++) {
      if (text.charAt(i) != ' ') return text.substring(i);
    }
    return "";
  }

  /* Returns the short name of the stat identified by its index in the stat
   * array.
   */
  function StatName(index) {
    if ( (index >= 0) && (index <= 4) )
      return ["con","dex","int","str","wis"][index];
    return "unknown"
  }



/* Functions to help with input from and output to forms */

  /* Reads a set of stats from a given location and returns it as an array.
   */
  function ReadStatsFrom(location) {
    return [ location.con.value * 1,
             location.dex.value * 1,
             location.inc.value * 1,
             location.str.value * 1,
             location.wis.value * 1 ]
  }

  /* Computes the stat total and writes it to the right place */
  function WriteStatTotal(location, stats) {
    var total = stats[0]+stats[1]+stats[2]+stats[3]+stats[4];
    location.total.value = total;
    return total;
  }

  /* Reads the index of the selected guild from a given location */
  function ReadGuildFrom(location) {
    var guild = location.guild.selectedIndex;
    return guild;
  }

  /* Reads the information on the selected guild from a given location */
  function ReadGuildInfoFrom(location) {
    var info = location.guild.value;
    return info;
  }



/* Functions to extract information from skill and guild data */

  /* Check if a string represents a proper skill, i.e. one that gives the
   * stat weighting for a skill.
   */
  function IsProperSkill(skill) {
    if ( !(skill.charAt(8)=="_") ) return 0;
    return 1;
  }

  /* Generate the skill's weighing from the skill info string */
  function ReadWeighting(skillinfo) {
    if ( !IsProperSkill(skillinfo) ) return [1,1,1,1,1];

    var weighting = new Array();
    for (var j = 0; j < 5; j++)
      weighting[j] = new Number(skillinfo.charAt(j+9));
    return weighting;
  }

  /* Read the skill short name from the skill info string */
  function ReadSkillShort(skillinfo) {
    return skillinfo.substring(0, 8);
  }

  /* Read the relevant points skill from the given guild information */
  function PrimaryGuildPointsSkill(guildinfo) {
    var guild = guildinfo.substring(15*9,15*9+2);
    var points = "ad";
    if (guild == "th" || guild == "as") points = "co";
    if (guild == "pr"                 ) points = "fa";
    if (guild == "wt" || guild == "wz") points = "ma";
    if (guild == "wa"                 ) points = "fi";
    return points;
  }

  /* Read the primary skills from the given guild information */
  function ReadPrimarySkills(guildinfo) {
    var primaries = new Array();
    for (var i = 0; i < 15; i++) primaries[i] = guildinfo.substring(i*9, i*9+8);
    return primaries;
  }

  /* Checks whether a given skill is in the list of primary skills */
  function IsPrimarySkill(skillshort, primaries) {
    var primary = false;
    for (var i = 0; i < 15; i++) primary |= (skillshort == primaries[i]);
    return primary;
  }



/* Cookie functionality functions */

  function ReadCookie() {
    var cookies = document.cookie.split(";");
    for (var i = 0; i < cookies.length; i++) {
      cookie = TrimLeft(cookies[i]);
      var parts = cookie.split("=");
      if (parts[0] == "data") return parts[1];
    }
    return "";
  }

  function SplitCookie(cookie) {
    var result = new Array();
    result[0] = (cookie.charAt(0)=="a");
    result[1] = cookie.charAt(1);
    if ("sdmwy".indexOf(result[1]) <= 0) result[1] = "s";
    result[2] = Math.floor(cookie.substring(3, 5));
    if ((result[2] < 0)||(result[2] > 99)) result[2] = 0;
    result[3] = new Array();
    for (var i = 0; i < 5; i++) {
      var t = cookie.substring(3*i+6, 3*i+8);
      if (t == "")
        result[3][i] = -1;
      else
        result[3][i] = Math.floor(t);
      if ((result[3][i] < 0)||(result[3][i] > 99)) result[3][i] = 13;
    }
    result[4] = cookie.substring(21, cookie.length);
    return result;
  }

  function CookieTime(expiry) {
    if (expiry == "y") return 1000*60*60*24*365;
    if (expiry == "m") return 1000*60*60*24*30;
    if (expiry == "w") return 1000*60*60*24*7;
    if (expiry == "d") return 1000*60*60*24;
    return 0;
  }

  function CookieDigits(n) {
    n = Math.floor(n);
    if ((n >=  0)&&(n <  10)) return "x0"+n;
    if ((n >= 10)&&(n < 100)) return "x"+n;
    return "x00";
  }

  function BuildCookie(auto, expiry, guild, stats, levels) {
    if (auto) cookie = "a"; else cookie = "m";
    cookie += expiry + CookieDigits(guild);
    for (var i = 0; i < 5; i++) cookie += CookieDigits(stats[i])
    if (levels != "") cookie += "y" + levels;
    return cookie;
  }

  function WriteCookie(cookie, expiry) {
    var temp = "data="+cookie;
    if (expiry != "s") {
      var exdate = new Date();
      exdate = new Date(exdate.getTime()+CookieTime(expiry));
      temp += "; expires="+exdate.toGMTString();
    }
    temp += "; path=/";
    document.cookie = temp;
  }

  function ModifyCookie(auto, expiry, guild, stats, levels, force) {
    var cookie = ReadCookie()
    var data = SplitCookie(cookie);
    if (auto != -1) data[0] = auto;
    if (force||data[0]) {
      if (expiry != -1) data[1] = expiry;
      if (guild != -1) data[2] = guild;
      if (stats != -1) 
        for (var i = 0; i < 5; i++)
          if (stats[i] != -1) data[3][i] = stats[i];
      if (levels != -1) data[4] = levels;
      cookie = BuildCookie(data[0], data[1], data[2], data[3], data[4]);
      WriteCookie(cookie, data[1]);
    }
    return cookie;
  }



/* Functions to calculate all kinds of data on skills.
 *
 * Arguments used in the different functions are:
 *   level:        the character's level in the relevant skill
 *   stats:        the character's stats in form of an array, for example
 *                   [12;11;14;10;18] for a character with the stats
 *                   con 12, dex 11, int 14, str 10, wis 18
 *   weighting:    the weighting of the different stats in the skill, for
 *                   example [1;2;2;0;0] for the skill covert.points
 */

  /* Calculates the raw bonus that is used in the calculation of skill
   * bonuses. This raw bonus depends only on the skill level.
   */
  function CalcRawBonus(level) {
         if (level <=  0) return                                 0;
    else if (level <= 20) return              5 *  level;
    else if (level <= 40) return Math.floor(2.5 * (level-20) + 100);
    else if (level <= 60) return                  (level-40) + 150;
    else                  return Math.floor(0.5 * (level-60) + 170);
  }

  /* Calculates the product of the stats used by a skill, with the special
   * condition that any value lower than 1 is treated as 1.
   */
  function CalcStatProduct(stats, weighting) {
    var prod = 1;
    for (var i = 0; i < 5; i++)
      if (stats[i] >= 1)
        for (var j = 0; j < weighting[i]; j++)
          prod *= stats[i];
    return prod;
  }

  /* Calculates stat multiplicator from the product of the stats used by a
   * skill, with the special condition that the multiplicator cannot be
   * negative.
   */
  function CalcMultiplicatorFromProduct(product) {
    var multiplicator = 1 / 9.8 * Math.log(product) - 0.25;
    if (multiplicator < 0) return 0; else return multiplicator;
  }

  /* Calculates the stat multiplicator that is used in the calculation of
   * skill bonuses. This multiplicator depends only on the stats that the
   * relevant skill depends on.
   */
  function CalcStatMultiplicator(stats, weighting) {
    var product       = CalcStatProduct(stats, weighting);
    var multiplicator = CalcMultiplicatorFromProduct(product);
    return multiplicator;
  }

  /* Calculates the skill bonus from the given level and stat
   * multiplicator.
   */
  function CalcBonusAtLevel(level, multiplicator) {
    var rawbonus      = CalcRawBonus(level);
    return Math.floor(multiplicator * rawbonus);
  }

  /* Calculates the skill bonus from the given level, stats and stat
   * weighting.
   */
  function CalcBonus(level, stats, weighting) {
    var multiplicator = CalcStatMultiplicator(stats, weighting);
    return CalcBonusAtLevel(level, multiplicator);
  }

  /* Calculates the level you need to gain the given bonus, for a skill
   * with the given statweighting and a player with the given stats.
   * This function is basically the inverse of CalcBonus.
   */
  function findLevel(bonus, stats, weighting) {
    var multiplicator, rawbonus, level;
  
    // calculate statmultiplicator
    multiplicator = CalcStatMultiplicator(stats, weighting);
  
    // find raw bonus
    rawbonus = Math.floor(bonus / multiplicator);
    while (Math.floor(multiplicator * rawbonus) < bonus) rawbonus++;
  
    // find the level that could have caused this bonus
    if (rawbonus <= 100) level = Math.ceil(rawbonus / 5);
    else if (rawbonus <= 150) level = Math.ceil((rawbonus-100)/2.5+20);
    else if (rawbonus <= 170) level = Math.ceil((rawbonus-150)/1+40);
    else level = Math.ceil((rawbonus-170)*2+60);

    return level;
  }



/* Calculates the weight a person with the given stats would approximately
 * have. Since there is also a random factor involved in determining
 * someone's weight, this is really just an approximation. It shouldn't be
 * out by more than 1, maybe 1.5 lbs though.
 */
function GuessWeight(stats) {
  var inner = Math.floor(((stats[0]-8)+((stats[3]-8)*3))/4);
  return Math.floor(668 + 26.5 * inner) / 10;
}

/* Calculates the hitpoints you will have with the given health level,
 * weight and stats.
 */
function findHitpoints(health_level, weight, stats) {
  var weighting = [4,0,0,1,0];
  var con = stats[0];
  var str = stats[3];
  var withnil, bonusmultiplier;
  
  var bonus = CalcBonus(health_level, stats, weighting);

  var hpsminuswf = ( 48.71 - (1.685*str) + ( con * ( 16.24 + ( 0.1366 * str ))));

  // calculate withnil
  if (weight >=90) withnil = Math.floor(hpsminuswf + 170 + (1*weight));
  else if (weight >=80) withnil = Math.floor(hpsminuswf + 80 + (2*weight));
  else if (weight >=0) withnil = Math.floor(hpsminuswf + (3*weight));

  // calculate bonusmultiplier
  if (weight>=90) bonusmultiplier = ((weight*0.0125)+ 6.325);
  else if (weight>=80) bonusmultiplier = ((weight*0.025) + 5.2);
  else if (weight>=0) bonusmultiplier = ((weight*0.05) + 3.2);

  // find hitpoints
  return Math.floor(bonusmultiplier*bonus + withnil);
}

/* Calculates the health level you need for the given number of hitpoints,
 * if you have the given weight and stats. This function basically inverses
 * findHitpoints.
 */
function findNeededHealthLevel(hitpoints, realweight, stats) {
  var weighting = [4,0,0,1,0];
  var con = stats[0] * 1;
  var str = stats[3] * 1;

  var hpsminuswf = ( 48.71 - (1.685*str) + ( con * ( 16.24 + ( 0.1366 * str ))));

  // calculate bonusmultiplier and withnil
  if (realweight>=90) {
    bonusmultiplier = ((realweight*0.0125)+ 6.325);
    withnil = Math.floor(hpsminuswf + 170 + (1*realweight));
  }
  else if (realweight>=80) {
    bonusmultiplier = ((realweight*0.025) + 5.2);
    withnil = Math.floor(hpsminuswf + 80 + (2*realweight));
  }
  else if (realweight>=0) {
    bonusmultiplier = ((realweight*0.05) + 3.2);
    withnil = Math.floor(hpsminuswf + (3*realweight));
  }

  // find needed adventuring.health bonus and level
  var bonus = (hitpoints - withnil) / bonusmultiplier;
  if (bonus < 0) bonus = 0;
  return findLevel(bonus, stats, weighting);
}



/* Functions to calculate all kinds of data (usually experience costs) on
 * teaching. Arguments used in the different functions are:
 *   currentlevel: the student's current level in the skill to learn
 *   currentbonus: the student's current bonus in the skill to learn
 *   teachbonus:   the teacher's effective teaching bonus in the skill to
 *                   teach; when self-teaching this is equal to the 
 *                   student's skills bonus when teaching starts
 *   teachfactor:  usually the quotient currentbonus/teachbonus, unless
 *                   teachbonus is 0; in that case, teachfactor equals
 *                   currentbonus
 *   stats:        the student's (unmodified) stats in form of an array
 *   weighting:    the weighting of the different stats in the skill to
 *                   learn in form of an array
 */

  /* A function to calculate the experience cost of advancing a primary
   * skill
   */
  function CalcAdvanceCost(startlevel, targetlevel) {
    var totalcost = new Number(0);
    for (var i = startlevel; i < targetlevel; i++)
      totalcost += Math.floor(75*Math.floor(i/3+1) * Math.exp(i/150));
    return totalcost;
  }

  /* Functions to calculate the experience cost for a single level */
  function CalcBasicLevelCost(currentlevel, teachfactor) {
    if ( !((currentlevel >= 0)&&(teachfactor >= 0)) ) return -1;

    return 500 + Math.floor((250+125*teachfactor)*currentlevel*Math.exp(currentlevel/500));
  }
  function CalcSingleLevelCost(currentlevel, currentbonus, teachbonus) {
    if ( !((currentbonus >= 0)&&(teachbonus >= 0)) ) return -1;

    if ( teachbonus == 0 ) return CalcBasicLevelCost(currentlevel, currentbonus);
    return CalcBasicLevelCost(currentlevel, currentbonus/teachbonus);
  }

  /* Functions to calculate the experience cost for multiple levels */
  function CalcSelfCost(startlevel, targetlevel) {
    var totalcost = new Number(0);

    for (var currentlevel = startlevel; currentlevel < targetlevel; currentlevel++)
      totalcost += CalcBasicLevelCost(currentlevel, 1);

    return totalcost;
  }
  function CalcStudentCost(startlevel, targetlevel, teachbonus, stats, weighting) {
    if ( !(teachbonus >= 0) ) return -1;

    var totalcost = new Number(0);
    for (var currentlevel = startlevel; currentlevel < targetlevel; j++)
      totalcost += CalcSingleLevelCost(currentlevel, CalcBonus(currentlevel, stats, weighting), teachbonus);

    return totalcost;
  }

  /* A function to calculate the experience the teacher gains */
  function CalcTeacherGain(studentcost) {
    if ( !(studentcost > 0) ) return 0;
    return Math.floor(Math.exp(0.8*Math.log(studentcost)));
  }

  /* A function to calculate the experience cost of advancing if and as
   * long as possible and self-teaching single levels otherwise
   */
  function CalcVariableCost(startlevel, targetlevel, primary) {
    if ( (primary)&&(startlevel < 300) ) {
      if (targetlevel > 300)
        return CalcAdvanceCost(startlevel, 300) + CalcSelfCost(300, targetlevel)
      else
        return CalcAdvanceCost(startlevel, targetlevel)
    }
    else return CalcSelfCost(startlevel, targetlevel);
  }
  function CalcFlexibleCost(startlevel, targetlevel, primary, multiplicator, teacherbonus) {
    var currentcost = new Number(0);
    var currentlevel = startlevel;
    var currentbonus = new Number(0);

    if ( primary && (startlevel < 300) ) {
      if (targetlevel <= 300) {
        currentlevel = targetlevel;
        currentcost = CalcAdvanceCost(startlevel, targetlevel);
      }
      else {
        currentlevel = 300;
        currentcost = CalcAdvanceCost(startlevel, 300);
      }
    }
    while ( (currentlevel < targetlevel) && ( (currentbonus = CalcBonusAtLevel(currentlevel, multiplicator)) < teacherbonus) ) {
      currentcost += CalcSingleLevelCost(currentlevel, currentbonus, teacherbonus);
      currentlevel++;
    }
    if ( currentlevel < targetlevel ) {
      currentcost += CalcSelfCost(currentlevel, targetlevel);
      currentlevel = targetlevel;
    }
    return currentcost;
  }

  /* A function to calculate all kinds of data on teaching:
   *   selfsinglecost:    the experience cost of teaching oneself one level
   *                        at a time
   *   selfallcost:       the experience cost of teaching oneself all
   *                        levels in one go
   *   studentcost:       the experience cost of learning from a player
   *                        with the given bonus
   *   teachersinglegain: the teacher's experience again when teaching one
   *                        level at a time
   *   teacherallgain:    the teacher's experience again when teaching all
   *                        levels in one go
   *   canteach:          states whether the teacher is able to teach the
   *                        student as wanted
   */
  function CalcTeachingData(startlevel, targetlevel, teachbonus, stats, weighting) {
    if ( !(teachbonus >= 0) ) return -1;

    var startbonus = new Number(CalcBonus(startlevel, stats, weighting));

    var selfsinglecost = new Number(0);
    var selfallcost = new Number(0);
    var studentcost = new Number(0);
    var teachersinglegain = new Number(0);
    var teacherallgain = new Number(0);
    var canteach = new Number(1);

    var currentbonus = new Number(0);
    var currentcost  = new Number(0);

    for (var currentlevel = startlevel; currentlevel < targetlevel; currentlevel++) {
      currentbonus = CalcBonus(currentlevel, stats, weighting);
      if (currentbonus > teachbonus) canteach = 0;
      selfsinglecost += CalcSingleLevelCost(currentlevel, currentbonus, currentbonus);
      selfallcost    += CalcSingleLevelCost(currentlevel, currentbonus, startbonus  );
      currentcost     = CalcSingleLevelCost(currentlevel, currentbonus, teachbonus  );
      studentcost    += currentcost;
      teachersinglegain += CalcTeacherGain(currentcost);
    }
    teacherallgain = CalcTeacherGain(studentcost);

    return [selfsinglecost, selfallcost, studentcost, teachersinglegain, teacherallgain, canteach];
  }


/* Calculates the money (in dollars) you need to advance, given the xp you
 * need for this advancement.
 */
function findAdvanceMoney(advancexp) {
  return Math.floor(advancexp / 80) / 100;
}

/* Calculate the gp regen a player with the given stats will have, if the
 * primary .points skill is as given by points (this should be "co" for a
 * thief or assassin, "ot" for an adventurer etcetera):
 *
 * CalcGPProduct returns the product of the stats involved, same as used in
 *   the calculation of the *.points skill bonus.
 * CalcGPRegen returns the gp regeneration rate.
 */
function CalcGPProduct(stats, points) {
  if (points == "ad") return CalcStatProduct(stats, [1,1,1,1,1]);  // adventuring: 1 con, 1 dex, 1 int, 1 str, 1 wis
  if (points == "co") return CalcStatProduct(stats, [1,2,2,0,0]);  // covert:      1 con, 2 dex, 2 int
  if (points == "fa") return CalcStatProduct(stats, [1,0,2,0,2]);  // faith:       1 con, 2 int, 2 wis
  if (points == "fi") return CalcStatProduct(stats, [2,1,0,2,0]);  // fighting:    2 con, 1 dex, 2 str
  if (points == "ma") return CalcStatProduct(stats, [0,0,2,1,2]);  // magic:       2 int, 1 str, 2 wis
  return -1;
}
function CalcGPRegen(stats, points) {
  var product       = CalcGPProduct(stats, points);
  var multiplicator = CalcMultiplicatorFromProduct(product);
  return Math.floor(Math.sqrt(175*multiplicator)-10);
}

/* Calculate the hp regen a player with the given stats will have */
function CalcHPRegen(stats) {
  var product       = CalcStatProduct(stats, [4,0,0,1,0]);
  var multiplicator = CalcMultiplicatorFromProduct(product);
  return Math.floor(Math.sqrt(200*multiplicator)-10);
}


/* Gives an estimation of how burdened a player with the given strength and
 * weight would be if he were carrying inv pounds.
 *
 * This is still in a very rudimentary stage and does require some work. :)
 */
function findBurden(strength, weight, inv) {
  var burdenfactor;
  if (strength <= 8)  burdenfactor = 10*strength+9;
  if (strength == 9)  burdenfactor = 100;
  if (strength == 10) burdenfactor = 112;
  if (strength == 11) burdenfactor = 122;
  if (strength == 12) burdenfactor = 134;
  if (strength == 13) burdenfactor = 146;
  if (strength == 14) burdenfactor = 163;
  if (strength == 15) burdenfactor = 177;
  if (strength == 16) burdenfactor = 192;
  if (strength == 17) burdenfactor = 210;
  if (strength == 18) burdenfactor = 227;
  if (strength == 19) burdenfactor = 245;
  if (strength == 20) burdenfactor = 267;
  if (strength == 21) burdenfactor = 287;
  if (strength == 22) burdenfactor = 308;
  if (strength >= 23) burdenfactor = 20*strength-131;

  return Math.floor(inv/(burdenfactor*weight)*10000);
}
