Greasy Fork is available in English.

Extra Statistics

Generate additional statistical data in the dungeon and duel report pages

//-----------------------------------------------------------------------------
// [WoD] Extra Statistics
// Version 1.20, 2010-04-15
// Copyright (c) Fenghou, Tomy
// This script can generate additional statistical data in the dungeon and duel report pages.
// When you entered the details or statistics page of reports, a new button will appear beside
//   the details button. At the details page, the new button is "Extra Stat", which will show
//   the statistics of the current level when you click it. At the statistics page, the new
//   button is "Entire Extra Stat", which will show the statistics of entire dungeon.
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// If you want to add a new Stat table, you should create a new sub class of CInfoList,
//   and use CStat::RegInfoList() to register your new info list.
// A detailed example is CILItemDamage.
//-----------------------------------------------------------------------------

// ==UserScript==
// @name		Extra Statistics
// @namespace		fenghou
// @version             1.20
// @description		Generate additional statistical data in the dungeon and duel report pages
// @include		http*://*.world-of-dungeons.*/wod/spiel/*dungeon/report.php*
// @include		http*://*.world-of-dungeons.*/wod/spiel/tournament/*duell.php*
// @include     file:///*f1.htm
// ==/UserScript==


// COMMON FUNCTIONS ///////////////////////////////////////////////////////////

function $(id) {return document.getElementById(id);}


// Choose contents of the corresponding language
// Contents: {Name1 : [lang1, lang2, ...], Name2 : [lang1, lang2, ...], ...}
// return: Local contents, or null
// It will edit the input contents directly, so the returned object is not necessary
function GetLocalContents(Contents)
	{
	function GetLanguageId()
		{
		var langText = null;
		var allMetas = document.getElementsByTagName("meta");
		for (var i = 0; i < allMetas.length; ++i)
			{
			if (allMetas[i].httpEquiv == "Content-Language")
				{
				langText = allMetas[i].content;
				break;
				}
			}
		if (langText == null)
			return false;

		switch (langText)
			{
			case "en":
				return 0;
			case "cn":
				return 1;
			default:
				return null;
			}
		}

	var nLangId = GetLanguageId();
	if (nLangId == null)
		return null;

	if (Contents instanceof Object)
		{
		for (var name in Contents)
			Contents[name] = Contents[name][nLangId];
		return Contents;
		}
	else
		return null;
	}


function CompareString(a, b)
	{
	a = a || "";
	b = b || "";
	return a.toLowerCase().localeCompare( b.toLowerCase() );
	}


function CreateElementHTML(Name, Content /* , [AttrName1, AttrValue1], [AttrName2, AttrValue2], ... */)
	{
	var HTML = '<' + Name;

	for (var i = 2; i < arguments.length; ++i)
		HTML += ' ' + arguments[i][0] + '="' + arguments[i][1] + '"';

	HTML += (Content != null && Content != "") ? ('>' + Content + '</' + Name + '>') : (' />');

	return HTML;
	}


function DbgMsg(Text) {if (DEBUG) alert(Text);}


// EXTERN FUNCTIONS ///////////////////////////////////////////////////////////

/**
 * A utility function for defining JavaScript classes.
 *
 * This function expects a single object as its only argument.  It defines
 * a new JavaScript class based on the data in that object and returns the
 * constructor function of the new class.  This function handles the repetitive
 * tasks of defining classes: setting up the prototype object for correct
 * inheritance, copying methods from other types, and so on.
 *
 * The object passed as an argument should have some or all of the
 * following properties:
 *
 *      name: The name of the class being defined.
 *            If specified, this value will be stored in the classname
 *            property of the prototype object.
 *
 *    extend: The constructor of the class to be extended. If omitted,
 *            the Object( ) constructor will be used. This value will
 *            be stored in the superclass property of the prototype object.
 *
 * construct: The constructor function for the class. If omitted, a new
 *            empty function will be used. This value becomes the return
 *            value of the function, and is also stored in the constructor
 *            property of the prototype object.
 *
 *   methods: An object that specifies the instance methods (and other shared
 *            properties) for the class. The properties of this object are
 *            copied into the prototype object of the class. If omitted,
 *            an empty object is used instead. Properties named
 *            "classname", "superclass", and "constructor" are reserved
 *            and should not be used in this object.
 *
 *   statics: An object that specifies the static methods (and other static
 *            properties) for the class. The properties of this object become
 *            properties of the constructor function. If omitted, an empty
 *            object is used instead.
 *
 *   borrows: A constructor function or array of constructor functions.
 *            The instance methods of each of the specified classes are copied
 *            into the prototype object of this new class so that the
 *            new class borrows the methods of each specified class.
 *            Constructors are processed in the order they are specified,
 *            so the methods of a class listed at the end of the array may
 *            overwrite the methods of those specified earlier. Note that
 *            borrowed methods are stored in the prototype object before
 *            the properties of the methods object above. Therefore,
 *            methods specified in the methods object can overwrite borrowed
 *            methods. If this property is not specified, no methods are
 *            borrowed.
 *
 *  provides: A constructor function or array of constructor functions.
 *            After the prototype object is fully initialized, this function
 *            verifies that the prototype includes methods whose names and
 *            number of arguments match the instance methods defined by each
 *            of these classes. No methods are copied; this is simply an
 *            assertion that this class "provides" the functionality of the
 *            specified classes. If the assertion fails, this method will
 *            throw an exception. If no exception is thrown, any
 *            instance of the new class can also be considered (using "duck
 *            typing") to be an instance of these other types.  If this
 *            property is not specified, no such verification is performed.
 **/
function DefineClass(data) {
    // Extract the fields we'll use from the argument object.
    // Set up default values.
    var classname = data.name;
    var superclass = data.extend || Object;
    var constructor = data.construct || function(){};
    var methods = data.methods || {};
    var statics = data.statics || {};
    var borrows;
    var provides;

    // Borrows may be a single constructor or an array of them.
    if (!data.borrows) borrows = [];
    else if (data.borrows instanceof Array) borrows = data.borrows;
    else borrows = [ data.borrows ];

    // Ditto for the provides property.
    if (!data.provides) provides = [];
    else if (data.provides instanceof Array) provides = data.provides;
    else provides = [ data.provides ];

    // Create the object that will become the prototype for our class.
    var proto = new superclass( );

    // Delete any noninherited properties of this new prototype object.
    for(var p in proto)
        if (proto.hasOwnProperty(p)) delete proto[p];

    // Borrow methods from "mixin" classes by copying to our prototype.
    for(var i = 0; i < borrows.length; i++) {
        var c = data.borrows[i];
        borrows[i] = c;
        // Copy method properties from prototype of c to our prototype
        for(var p in c.prototype) {
            if (typeof c.prototype[p] != "function") continue;
            proto[p] = c.prototype[p];
        }
    }
    // Copy instance methods to the prototype object
    // This may overwrite methods of the mixin classes
    for(var p in methods) proto[p] = methods[p];

    // Set up the reserved "constructor", "superclass", and "classname"
    // properties of the prototype.
    proto.constructor = constructor;
    proto.superclass = superclass;
    // classname is set only if a name was actually specified.
    if (classname) proto.classname = classname;

    // Verify that our prototype provides all of the methods it is supposed to.
    for(var i = 0; i < provides.length; i++) {  // for each class
        var c = provides[i];
        for(var p in c.prototype) {   // for each property
            if (typeof c.prototype[p] != "function") continue;  // methods only
            if (p == "constructor" || p == "superclass") continue;
            // Check that we have a method with the same name and that
            // it has the same number of declared arguments.  If so, move on
            if (p in proto &&
                typeof proto[p] == "function" &&
                proto[p].length == c.prototype[p].length) continue;
            // Otherwise, throw an exception
            throw new Error("Class " + classname + " does not provide method "+
                            c.classname + "." + p);
        }
    }

    // Associate the prototype object with the constructor function
    constructor.prototype = proto;

    // Copy static properties to the constructor
    for(var p in statics) constructor[p] = statics[p];

    // Finally, return the constructor function
    return constructor;
}


/**
 * Throughout, whitespace is defined as one of the characters
 *  "\t" TAB \u0009
 *  "\n" LF  \u000A
 *  "\r" CR  \u000D
 *  " "  SPC \u0020
 *
 * This does not use Javascript's "\s" because that includes non-breaking
 * spaces (and also some other characters).
 */


/**
 * Determine whether a node's text content is entirely whitespace.
 *
 * @param nod  A node implementing the |CharacterData| interface (i.e.,
 *             a |Text|, |Comment|, or |CDATASection| node
 * @return     True if all of the text content of |nod| is whitespace,
 *             otherwise false.
 */
function is_all_ws( nod )
{
  // Use ECMA-262 Edition 3 String and RegExp features
  return !(/[^\t\n\r ]/.test(nod.data));
}


/**
 * Determine if a node should be ignored by the iterator functions.
 *
 * @param nod  An object implementing the DOM1 |Node| interface.
 * @return     true if the node is:
 *                1) A |Text| node that is all whitespace
 *                2) A |Comment| node
 *             and otherwise false.
 */

function is_ignorable( nod )
{
  return ( nod.nodeType == 8) || // A comment node
         ( (nod.nodeType == 3) && is_all_ws(nod) ); // a text node, all ws
}

/**
 * Version of |previousSibling| that skips nodes that are entirely
 * whitespace or comments.  (Normally |previousSibling| is a property
 * of all DOM nodes that gives the sibling node, the node that is
 * a child of the same parent, that occurs immediately before the
 * reference node.)
 *
 * @param sib  The reference node.
 * @return     Either:
 *               1) The closest previous sibling to |sib| that is not
 *                  ignorable according to |is_ignorable|, or
 *               2) null if no such node exists.
 */
function node_before( sib )
{
  while ((sib = sib.previousSibling)) {
    if (!is_ignorable(sib)) return sib;
  }
  return null;
}

/**
 * Version of |nextSibling| that skips nodes that are entirely
 * whitespace or comments.
 *
 * @param sib  The reference node.
 * @return     Either:
 *               1) The closest next sibling to |sib| that is not
 *                  ignorable according to |is_ignorable|, or
 *               2) null if no such node exists.
 */
function node_after( sib )
{
  while ((sib = sib.nextSibling)) {
    if (!is_ignorable(sib)) return sib;
  }
  return null;
}

/**
 * Version of |lastChild| that skips nodes that are entirely
 * whitespace or comments.  (Normally |lastChild| is a property
 * of all DOM nodes that gives the last of the nodes contained
 * directly in the reference node.)
 *
 * @param par  The reference node.
 * @return     Either:
 *               1) The last child of |sib| that is not
 *                  ignorable according to |is_ignorable|, or
 *               2) null if no such node exists.
 */
function last_child( par )
{
  var res=par.lastChild;
  while (res) {
    if (!is_ignorable(res)) return res;
    res = res.previousSibling;
  }
  return null;
}

/**
 * Version of |firstChild| that skips nodes that are entirely
 * whitespace and comments.
 *
 * @param par  The reference node.
 * @return     Either:
 *               1) The first child of |sib| that is not
 *                  ignorable according to |is_ignorable|, or
 *               2) null if no such node exists.
 */
function first_child( par )
{
  var res=par.firstChild;
  while (res) {
    if (!is_ignorable(res)) return res;
    res = res.nextSibling;
  }
  return null;
}

/**
 * Version of |data| that doesn't include whitespace at the beginning
 * and end and normalizes all whitespace to a single space.  (Normally
 * |data| is a property of text nodes that gives the text of the node.)
 *
 * @param txt  The text node whose data should be returned
 * @return     A string giving the contents of the text node with
 *             whitespace collapsed.
 */
function data_of( txt )
{
  var data = txt.data;
  // Use ECMA-262 Edition 3 String and RegExp features
  data = data.replace(/[\t\n\r ]+/g, " ");
  if (data.charAt(0) == " ")
    data = data.substring(1, data.length);
  if (data.charAt(data.length - 1) == " ")
    data = data.substring(0, data.length - 1);
  return data;
}


// CLASSES ////////////////////////////////////////////////////////////////////

// NextNode: the node next to the statistics node when it is created
function CStat(NextNode)
	{
	var NewSection = document.createElement("div");
	NewSection.id = "stat_all";
	this._Node = NextNode.parentNode.insertBefore(NewSection, NextNode);

	this._HTML = '';

	this._gInfoList = [];

	this.nTotalPages = 0;
	this.nReadPages = 0;
	}

CStat.prototype._Write = function(Text) {this._HTML += Text;};

CStat.prototype._Flush = function() {this._Node.innerHTML = this._HTML;};

CStat.prototype.RegInfoList = function(InfoList)
	{
	if (InfoList instanceof CInfoList)
		{
		this._gInfoList.push(InfoList);
		return true;
		}
	return false;
	};

CStat.prototype.SaveInfo = function(Info)
	{
	for (var i = 0; i < this._gInfoList.length; ++i)
		this._gInfoList[i].SaveInfo(Info);
	};

CStat.prototype.Show = function()
	{
	this._Write("<hr />");
	for (var i = 0; i < this._gInfoList.length; ++i)
		this._Write( this._gInfoList[i].Show() );
	this._Write(this._OptionsHTML());
	this._Write("<hr />");
	this._Flush();

	for (var i = 0; i < this._gInfoList.length; ++i)
		this._gInfoList[i].AddEvents();
	this._AddEvents();
	};

CStat.prototype.ShowProgress = function()
	{
	this._Node.innerHTML = '<hr /><h1>' + Local.Text_Loading + ' (' +
		this.nReadPages + '/' + this.nTotalPages + ') ...</h1><hr />';
	};

CStat.prototype._OptionsHTML = function()
	{
	var Str = '<div id="stat_options">' +
		'<div class="stat_header"><span class="stat_title">' + Local.Text_Options + '</span>';
	Str += CreateElementHTML("input", null, ["type", "button"], ["class", "button"],
		["id", "stat_options_default"], ["value", Local.Text_Button_Default]);
	Str += '</div></div>';
	return Str;
	};

CStat.prototype._AddEvents = function()
	{
	function OnDelGMValues()
		{
		try	{
			var ValueList = GM_listValues();
			for (var name in ValueList) {GM_deleteValue(ValueList[name]);}
			alert(Local.Text_DefaultMsg);
			}
		catch (e) {alert("OnDelGMValues(): " + e);}
		}
	$("stat_options_default").addEventListener("click", OnDelGMValues, false);
	};


///////////////////////////////////////////////////////////////////////////////
function CTable(Title, Id, nColumns)
	{
	this._Title = Title;
	this._Id = Id;
	this._nColumns = nColumns;
	this._HeadCellContents = new Array(nColumns);
	this._BodyCellContentTypes = new Array(nColumns);
	this._BodyCellContents = [];
	this._HTML = '';

	this._bShow = GM_getValue(Id, true);
	}

CTable._ContentAttrs = {
		string	: '',
		number	: 'align="right"',
		button	: 'align="center"'};

CTable.prototype.SetHeadCellContents = function(/* Content1, Content2, ... */)
	{
	for (var i = 0; i < this._nColumns; ++i)
		this._HeadCellContents[i] = arguments[i] != null ? arguments[i] : "";
	};

// Type: a string that is the property name of CTable::ContentAttrs
CTable.prototype.SetBodyCellContentTypes = function(/* Type1, Type2, ... */)
	{
	for (var i = 0; i < this._nColumns; ++i)
		this._BodyCellContentTypes[i] =
			arguments[i] != null ? CTable._ContentAttrs[arguments[i]] : "";
	};

CTable.prototype.SetBodyCellContents = function(/* Content1, Content2, ... */)
	{
	var Contents = new Array(this._nColumns);
	for (var i = 0; i < this._nColumns; ++i)
		Contents[i] = arguments[i] != null ? arguments[i] : "";
	this._BodyCellContents.push(Contents);
	};

CTable.prototype.CreateHTML = function()
	{
	this._HTML = '<div id="' + this._Id + '">' +
		'<div class="stat_header"><span class="stat_title clickable">' + this._Title + '</span></div>' +
		'<table class="content_table" ' + (this._bShow ? '' : 'hide="hide"') + '>' +
		'<tr class="content_table_header">';

	for (var i = 0; i < this._nColumns; ++i)
		this._HTML += '<th class="content_table">' + this._HeadCellContents[i] + '</th>';
	this._HTML += '</tr>';
	
	for (var i = 0; i < this._BodyCellContents.length; ++i)
		{
		this._HTML += '<tr class="content_table_row_' + i % 2 + '">';
		for (var j = 0; j < this._nColumns; ++j)
			{
			this._HTML += '<td class="content_table" ' +
				this._BodyCellContentTypes[j] + '>' +
				this._BodyCellContents[i][j] + '</td>';
			}
		this._HTML += '</tr>';
		}
	this._HTML += '</table></div>';

	return this._HTML;
	};

CTable.prototype.GetHTML = function()
	{return this._HTML;};

CTable.prototype.AddEvents = function()
	{
	var Title = $(this._Id).getElementsByTagName("span")[0];
	function Factory(Id) {return function(){CTable.OnClickTitle(Id);};}
	Title.addEventListener("click", Factory(this._Id), false);
	};

CTable.OnClickTitle = function(Id)
	{
	try	{
		var Table = $(Id).getElementsByTagName("table")[0];
		if (Table.hasAttribute("hide"))
			{
			Table.removeAttribute("hide");
			GM_setValue(Id, true);
			}
		else
			{
			Table.setAttribute("hide", "hide");
			GM_setValue(Id, false);
			}
		}
	catch (e) {alert("CTable.OnClickTitle(): " + e);}
	};


///////////////////////////////////////////////////////////////////////////////
function CActiveInfo()
	{
	this.nIniRoll;
	this.nCurrAction;
	this.nTotalActions;
	this.Char		= new CChar();
	this.nCharId;
	this.ActionType		= new CActionType();
	this.Skill		= new CSkill();
	this.nAttackRoll;
	this.nSkillMP;
	this.gItem		= new CKeyList();
	}


function CPassiveInfo()
	{
	this.Char		= new CChar();
	this.nCharId;
	this.Skill		= new CSkill();
	this.nDefenceRoll;
	this.nSkillMP;
	this.gItem		= new CKeyList();
	this.HitType		= new CHitType();
	this.bStruckDown;
	this.gDamage		= [];
	this.DamagedItem	= new CItem();
	this.nItemDamage;
	this.nHealedHP;
	this.nHealedMP;
	}


function CNavi(nLevel, nRoom, nRound, nRow)
	{
	this.nLevel		= nLevel;
	this.nRoom		= nRoom;
	this.nRound		= nRound;
	this.nRow		= nRow;
	}


function CActionInfo(Navi)
	{
	this.Navi		= Navi;
	this.Active		= new CActiveInfo();
	this.gPassive		= [];
	}


///////////////////////////////////////////////////////////////////////////////
// Class: Key
// Every key should have two function properties: compareTo() and toString(),
//   and can work without initialization parameters

var CKey = DefineClass({
	methods:
		{
		compareTo: function(that) {return this - that;},
		toString: function() {return "";}
		}
	});


var CKeyList = DefineClass({
	extend: CKey,
	construct: function() {this._gKey = [];},
	methods:
		{
		push: function(Key) {return this._gKey.push(Key);},
		compareTo: function(that)
			{
			var result = this._gKey.length - that._gKey.length;
			if (result !== 0)
				return result;

			var i = 0;
			while (i < this._gKey.length && this._gKey[i].compareTo(that._gKey[i]) === 0)
				++i;
			if (i === this._gKey.length)
				return 0;
			else
				return this._gKey[i].compareTo(that._gKey[i]);
			},
		toString: function() {return this._gKey.join(", ");}
		}
	});


var CChar = DefineClass({
	extend: CKey,
	construct: function(HTMLElement)
		{
		this._Name;
		this._Href;
		this._OnClick;
		this._Class;
		this._nType;

		if (HTMLElement != null)
			{
			this._Name = HTMLElement.firstChild.data;
			this._Href = HTMLElement.getAttribute("href");
			this._OnClick = HTMLElement.getAttribute("onclick");
			this._Class = HTMLElement.className;
			this._nType = CChar._GetCharType(this._Class);
			if (this._nType === null)
				DbgMsg("CChar(): Unknown type: " + this._Class);
			}
		},
	methods:
		{
		GetType: function() {return this._nType;},
		compareTo: function(that)
			{
			var result = this._nType - that._nType;
			if (result !== 0)
				return result;
			return CompareString(this._Name, that._Name);
			},
		toString: function()
			{
			if (this._Name != null)
				return CreateElementHTML("a", this._Name, ["href", this._Href],
					["onclick", this._OnClick], ["class", this._Class]);
			else
				return "";
			}
		},
	statics:
		{
		_GetCharType: function(Class)
			{
			switch (Class)
				{
				case "rep_hero":
				case "rep_myhero":
					return 0;
				case "rep_monster":
				case "rep_myhero_defender":
					return 1;
				default:
					return null;
				}
			}
		}
	});


// In-round actions
var CActionType = DefineClass({
	extend: CKey,
	construct: function(ActionText)
		{
		this._nType;
		this._nKind;

		if (ActionText != null)
			{
			var ret = CActionType._GetActionTypeAndKind(ActionText);
			this._nType = ret[0];
			this._nKind = ret[1];
			}
		},
	methods:
		{
		GetType: function() {return this._nType;},
		GetKind: function() {return this._nKind;},
		compareTo: function(that) {return this._nType - that._nType;},
		toString: function()
			{
			switch (this._nKind)
				{
				case 0:		return Local.TextList_AttackType[this._nType];
				case 1:		return "heal";
				case 2:		return "buff";
				case 3:		return "wait";
				default:	return "unknown";
				}
			}
		},
	statics:
		{
		// return: an array, [0]: _nType, [1]: _nKind
		_GetActionTypeAndKind: function(ActionText)
			{
			switch (ActionText)
				{
				case Local.OrigTextList_ActionType[0]:	// melee
					return [0, 0];
				case Local.OrigTextList_ActionType[1]:	// ranged
					return [1, 0];
				case Local.OrigTextList_ActionType[2]:	// magic
					return [2, 0];
				case Local.OrigTextList_ActionType[3]:	// social
					return [3, 0];
				case Local.OrigTextList_ActionType[4]:	// ambush
					return [4, 0];
				case Local.OrigTextList_ActionType[5]:	// trap
					return [5, 0];
				case Local.OrigTextList_ActionType[6]:	// nature
					return [6, 0];
				case Local.OrigTextList_ActionType[7]:	// disease
					return [7, 0];
				case Local.OrigTextList_ActionType[8]:	// detonate
					return [8, 0];
				case Local.OrigTextList_ActionType[9]:	// disarm trap
					return [9, 0];
				case Local.OrigTextList_ActionType[10]:	// magic projectile
					return [10, 0];
				case Local.OrigTextList_ActionType[11]:	// curse
					return [11, 0];
				case Local.OrigTextList_ActionType[12]:	// scare
					return [12, 0];
				case Local.OrigTextList_ActionType[13]:	// heal
					return [13, 1];
				case Local.OrigTextList_ActionType[14]:	// buff
					return [14, 2];
				case Local.OrigTextList_ActionType[15]:	// summon
					return [15, 2];
				case Local.OrigTextList_ActionType[16]:	// do nothing
					return [16, 3];
				case Local.OrigTextList_ActionType[17]:	// wait
					return [17, 3];
				default:
					return [null, null];
				}
			}
		}
	});


var CSkill = DefineClass({
	extend: CKey,
	construct: function(HTMLElement)
		{
		this._Name;
		this._Href;
		this._OnClick;

		if (HTMLElement != null)
			{
			this._Name = HTMLElement.firstChild.data;
			this._Href = HTMLElement.getAttribute("href");
			this._OnClick = HTMLElement.getAttribute("onclick");
			}
		},
	methods:
		{
		compareTo: function(that) {return CompareString(this._Name, that._Name);},
		toString: function()
			{
			if (this._Name != null)
				return CreateElementHTML("a", this._Name, ["href", this._Href],
					["onclick", this._OnClick]);
			else
				return "";
			}
		}
	});


var CItem = DefineClass({
	extend: CKey,
	construct: function(HTMLElement)
		{
		this._Name;
		this._Href;
		this._OnClick;
		this._Class;

		if (HTMLElement != null)
			{
			this._Name = HTMLElement.firstChild.data;
			this._Href = HTMLElement.getAttribute("href");
			this._OnClick = HTMLElement.getAttribute("onclick");
			this._Class = HTMLElement.className;
			}
		},
	methods:
		{
		compareTo: function(that) {return CompareString(this._Name, that._Name);},
		toString: function()
			{
			if (this._Name != null)
				return CreateElementHTML("a", this._Name, ["href", this._Href],
					["onclick", this._OnClick], ["class", this._Class]);
			else
				return "";
			}
		}
	});


var CHitType = DefineClass({
	extend: CKey,
	construct: function(HitClassText)
		{
		this._nType;

		if (HitClassText != null)
			{
			this._nType = CHitType._GetHitType(HitClassText);
			if (this._nType === null)
				DbgMsg("CHitType(): Unknown type: " + HitClassText);
			}
		},
	methods:
		{
		GetType: function() {return this._nType;},
		compareTo: function(that) {return this._nType - that._nType;},
		toString: function()
			{
			if (this._nType != null)
				return Local.TextList_HitType[this._nType];
			else
				return "";
			}
		},
	statics:
		{
		_GetHitType: function(Class)
			{
			switch (Class)
				{
				case "rep_miss":
					return 0;
				case "rep_hit":
					return 1;
				case "rep_hit_good":
					return 2;
				case "rep_hit_crit":
					return 3;
				default:
					return null;
				}
			}
		}
	});


var CDamage = DefineClass({
	extend: CKey,
	construct: function(HTMLElement)
		{
		this._nBasicDmg;
		this._nActualDmg;
		this._nArmor;
		this._nType;

		if (HTMLElement != null)
			{
			var Str;
			if (HTMLElement.nodeType != 3)
				{
				Str = HTMLElement.getAttribute("onmouseover");
				// \1	basic damage
				var Patt_BasicDamage = Local.Pattern_BasicDamage;
				var result = Patt_BasicDamage.exec(Str);
				if (result == null)
					throw "CDamage() :" + Str;
				this._nBasicDmg = Number(result[1]);
				Str = HTMLElement.firstChild.data;
				}
			else
				Str = HTMLElement.data;

			// \1	actual damage
			// \2	armor
			// \3	damage type
			var Patt_Damage = Local.Pattern_Damage;
			var result = Patt_Damage.exec(Str);
			if (result == null)
				throw "CDamage() :" + Str;
			this._nActualDmg = Number(result[1]);
			this._nArmor = result[2] != null ? Number(result[2]) : 0;
			this._nType = CDamage._GetDamageType(result[3]);

			if (this._nType === null)
				DbgMsg("CDamage(): Unknown type: " + result[3]);
			if (this._nBasicDmg == null)
				this._nBasicDmg = this._nActualDmg + this._nArmor;
			}
		},
	methods:
		{
		GetType:	function() {return this._nType;},
		GetBasicDmg:	function() {return this._nBasicDmg;},
		GetArmor:	function() {return this._nArmor;},
		GetActualDmg:	function() {return this._nActualDmg;},
		IsHPDamage:	function() {return this._nType !== 11;},
		compareTo: function(that) {return this._nBasicDmg - that._nBasicDmg;},
		toString: function()
			{
			if (this._nType != null)
				{
				var Str = String(this._nBasicDmg);
				if (this._nArmor > 0)
					Str += " - " + this._nArmor + " -> " + this._nActualDmg;
				else if (this._nBasicDmg !== this._nActualDmg)
					Str += " -> " + this._nActualDmg;
				Str += " " + Local.OrigTextList_DamageType[this._nType] + " damage";
				return Str;
				}
			else
				return "";
			}
		},
	statics:
		{
		_GetDamageType: function(Text)
			{
			switch (Text)
				{
				case Local.OrigTextList_DamageType[0]:	// crushing
					return 0;
				case Local.OrigTextList_DamageType[1]:	// cutting
					return 1;
				case Local.OrigTextList_DamageType[2]:	// piercing
					return 2;
				case Local.OrigTextList_DamageType[3]:	// fire
					return 3;
				case Local.OrigTextList_DamageType[4]:	// ice
					return 4;
				case Local.OrigTextList_DamageType[5]:	// lightning
					return 5;
				case Local.OrigTextList_DamageType[6]:	// poison
					return 6;
				case Local.OrigTextList_DamageType[7]:	// acid
					return 7;
				case Local.OrigTextList_DamageType[8]:	// psychological
					return 8;
				case Local.OrigTextList_DamageType[9]:	// holy
					return 9;
				case Local.OrigTextList_DamageType[10]:	// disarm trap
					return 10;
				case Local.OrigTextList_DamageType[11]:	// mana
					return 11;
				default:
					return null;
				}
			}
		}
	});


///////////////////////////////////////////////////////////////////////////////
// Class: Value list
// Value list is a special key, it can contains any type of values, including keys

var CValueList = DefineClass({
	extend: CKey,
	construct: function()
		{
		this._gValue = [];
		this._nAvgValue;	// unsure type 
		},
	methods:
		{
		GetLength: function() {return this._gValue.length;},
		Calculate: function() {},
		push: function(Value) {return this._gValue.push(Value);},
		compareTo: function(that) {return this._nAvgValue - that._nAvgValue;},
		AvgValueStr: function() {return String(this._nAvgValue);},
		toString: function() {return this._gValue.join(", ");}
		}
	});


var CVLNumber = DefineClass({
	extend: CValueList,
	construct: function() {this.superclass();},
	methods:
		{
		Calculate: function()
			{
			var nTotalValue = 0;
			for (var i = 0; i < this._gValue.length; ++i)
				nTotalValue += this._gValue[i];
			this._nAvgValue = Number((nTotalValue / this._gValue.length).toFixed(2));
			}
		}
	});


// value: [Number1, Number2]
var CVLPairNumber = DefineClass({
	extend: CValueList,
	construct: function() {this.superclass();},
	methods:
		{
		Calculate: function()
			{
			var nTotalValue = [0, 0];
			for (var i = 0; i < this._gValue.length; ++i)
				{
				nTotalValue[0] += this._gValue[i][0];
				nTotalValue[1] += this._gValue[i][1];
				}
			this._nAvgValue = new Array(2);
			this._nAvgValue[0] = Number((nTotalValue[0] / this._gValue.length).toFixed(2));
			this._nAvgValue[1] = Number((nTotalValue[1] / this._gValue.length).toFixed(2));
			},
		compareTo: function(that)
			{
			if (this._nAvgValue[0] !== 0 || that._nAvgValue[0] !== 0)
				return this._nAvgValue[0] - that._nAvgValue[0];
			else
				return this._nAvgValue[1] - that._nAvgValue[1];
			},
		AvgValueStr: function()
			{
			return '<table class="pair_value"><tr><td>' +
				((this._nAvgValue[0] !== 0) ? String(this._nAvgValue[0]) : '') +
				'</td><td>' +
				((this._nAvgValue[1] !== 0) ? String(this._nAvgValue[1]) : '') +
				'</td></tr></table>';
			},
		toString: function()
			{
			var Str = "";
			for (var i = 0; i < this._gValue.length; ++i)
				{
				Str += (this._gValue[i][0] != null) ? this._gValue[i][0] : 0;
				Str += "/";
				Str += (this._gValue[i][1] != null) ? this._gValue[i][1] : 0;
				if (i < this._gValue.length -1)
					Str += ", ";
				}
			return Str;
			}
		}
	});


// value: An Array of CDamage
var CVLDamage = DefineClass({
	extend: CValueList,
	construct: function() {this.superclass();},
	methods:
		{
		Calculate: function()
			{
			var nTotalValue = [0, 0];
			for (var i = 0; i < this._gValue.length; ++i)
				{
				for (var j = 0; j < this._gValue[i].length; ++j)
					{
					if (this._gValue[i][j].IsHPDamage())
						{
						nTotalValue[0] += this._gValue[i][j].GetBasicDmg();
						nTotalValue[1] += this._gValue[i][j].GetActualDmg();
						}
					}
				}
			this._nAvgValue = new Array(2);
			this._nAvgValue[0] = Number((nTotalValue[0] / this._gValue.length).toFixed(2));
			this._nAvgValue[1] = Number((nTotalValue[1] / this._gValue.length).toFixed(2));
			},
		compareTo: function(that) {return this._nAvgValue[1] - that._nAvgValue[1];},
		AvgValueStr: function()
			{
			return '<table class="pair_value"><tr><td>' +
				this._nAvgValue[1] + '</td><td>' +
				this._nAvgValue[0] + '</td></tr></table>';
			},
		toString: function()
			{
			var Str = "";
			for (var i = 0; i < this._gValue.length; ++i)
				{
				var nTotalValue = [0, 0];
				for (var j = 0; j < this._gValue[i].length; ++j)
					{
					if (this._gValue[i][j].IsHPDamage())
						{
						nTotalValue[0] += this._gValue[i][j].GetBasicDmg();
						nTotalValue[1] += this._gValue[i][j].GetActualDmg();
						}
					}
				Str += nTotalValue[1] + "/" + nTotalValue[0];
				if (i < this._gValue.length -1)
					Str += ", ";
				}
			return Str;
			}
		}
	});


///////////////////////////////////////////////////////////////////////////////
// Class: Info list

var CInfoList = DefineClass({
	construct: function(nKeys, CValueList)
		{
		this._gInfo		= [];
		this._nKeys		= nKeys;
		this._CValueList	= CValueList;
		this._Table		= null;
		},
	methods:
		{
		_CompareKeys: function(gKeyA, gKeyB)
			{
			for (var i = 0; i < this._nKeys; ++i)
				{
				var result = gKeyA[i].compareTo( gKeyB[i] );
				if (result !== 0)
					return result;
				}
			return 0;
			},
		_SetTableBodyCellContents: function()
			{
			for (var i = 0; i < this._gInfo.length; ++i)
				{
				var gBodyCellContent = [];
				for (var j = 0; j < this._gInfo[i].gKey.length; ++j)
					gBodyCellContent.push( this._gInfo[i].gKey[j] );

				gBodyCellContent.push( this._gInfo[i].ValueList.AvgValueStr(),
					String(this._gInfo[i].ValueList.GetLength()),
					CreateElementHTML("input", null, ["type", "button"],
						["class", "button"], ["value", Local.Text_Button_Show],
						["onclick", 'alert(&quot;' + this._gInfo[i].ValueList.toString() + '&quot;);']) );

				this._Table.SetBodyCellContents.apply(this._Table, gBodyCellContent);
				}
			},
		SaveInfo: function(Info) {},
		Show: function() {},
		// Call this function when read all data, and before sort and export data
		CalculateValue: function()
			{
			for (var i = 0; i < this._gInfo.length; ++i)
				this._gInfo[i].ValueList.Calculate();
			},
		CreateTable: function(Title, Id, gKeyName)
			{
			// Key1, Key2, ..., AverageValue, Times, ValueList
			this._Table = new CTable(Title, Id, this._nKeys + 3);

			var gHeadCellContent = new Array(this._nKeys + 3);
			for (var i = 0; i < this._nKeys; ++i)
				gHeadCellContent[i] = gKeyName[i];
			gHeadCellContent[this._nKeys] = Local.Text_Table_AvgRoll;
			gHeadCellContent[this._nKeys + 1] = Local.Text_Table_Times;
			gHeadCellContent[this._nKeys + 2] = Local.Text_Table_RollList;
			this._Table.SetHeadCellContents.apply(this._Table, gHeadCellContent);

			var gBodyCellContentType = new Array(this._nKeys + 3);
			for (var i = 0; i < this._nKeys; ++i)
				gBodyCellContentType[i] = "string";
			gBodyCellContentType[this._nKeys] = "number";
			gBodyCellContentType[this._nKeys + 1] = "number";
			gBodyCellContentType[this._nKeys + 2] = "button";
			this._Table.SetBodyCellContentTypes.apply(this._Table, gBodyCellContentType);

			this._SetTableBodyCellContents();

			return this._Table.CreateHTML();
			},
		// Call this function when edited the info list (for example, re-sorted it)
		ReCreateTableHTML: function()
			{
			this._SetTableBodyCellContents();
			return this._Table.CreateHTML();
			},
		GetTableHTML: function() {return this._Table.GetHTML();},
		AddEvents: function() {if (this._Table != null) this._Table.AddEvents();},
		push: function(gKey, Value)
			{
			for (var i = 0; i < this._gInfo.length; ++i)
				{
				if (this._CompareKeys(this._gInfo[i].gKey, gKey) === 0)
					{
					this._gInfo[i].ValueList.push(Value);
					return this._gInfo.length;
					}
				}

			var ValueList = new this._CValueList();
			ValueList.push(Value);
			return this._gInfo.push(new CInfoList._CInfo(gKey, ValueList));
			},
		sort: function(gSortKeyId)
			{
			function Factory(gId) {return function(A, B){return CInfoList._CompareInfo(A, B, gId);};}
			return this._gInfo.sort(Factory(gSortKeyId));
			}
		},
	statics:
		{
		_CInfo: function(gKey, ValueList)
			{
			this.gKey = gKey;
			this.ValueList = ValueList;
			},
		// SortKeyId: Id of keys, or null
		// The list will be sorted in this way: sort them by the first key, if there are
		//   elements are still equal, then sort them by the second key, and so on.
		// If SortKeyId is null, then sort the list by value
		// If gSortKeyId is null, then sort the list by default order of keys
		_CompareInfo: function(InfoA, InfoB, gSortKeyId)
			{
			if (gSortKeyId == null)
				{
				for (var i = 0; i < InfoA.gKey.length; ++i)
					{
					var result = InfoA.gKey[i].compareTo(InfoB.gKey[i]);
					if (result !== 0) return result;
					}
				return 0;
				}
			else
				{
				for (var i = 0; i < gSortKeyId.length; ++i)
					{
					var KeyId = gSortKeyId[i];
					var result = (KeyId != null) ?
						InfoA.gKey[KeyId].compareTo(InfoB.gKey[KeyId]) :
						InfoA.ValueList.compareTo(InfoB.ValueList);
					if (result !== 0) return result;
					}
				return 0;
				}
			}
		}
	});


///////////////////////////////////////////////////////////////////////////////
// Sub classes of CInfoList
//
// var CIL_ = DefineClass({
//	extend: CInfoList,
//	construct: function(_nKeys, CValueList) {this.superclass(_nKeys, CValueList);},
//	methods:
//		{
//		_SetTableBodyCellContents: function() {},
//		SaveInfo: function(Info) {},
//		Show: function() {},
//		CreateTable: function(Title, Id, gKeyName) {}
//		}
//	});

var CILIni = DefineClass({
	extend: CInfoList,
	construct: function(nKeys, CValueList) {this.superclass(nKeys, CValueList);},
	methods:
		{
		SaveInfo: function(Info)
			{
			if (Info.Active.nCurrAction === 1)
				this.push([Info.Active.Char], Info.Active.nIniRoll);
			},
		Show: function()
			{
			if (this._gInfo.length > 0)
				{
				this.CalculateValue();
				this.sort();
				return this.CreateTable(Local.Text_Table_Ini, "stat_ini", [Local.Text_Table_Char]);
				}
			return "";
			}
		}
	});


var CILAttackRoll = DefineClass({
	extend: CInfoList,
	construct: function(nKeys, CValueList) {this.superclass(nKeys, CValueList);},
	methods:
		{
		SaveInfo: function(Info)
			{
			if (Info.Active.ActionType.GetKind() === 0 && Info.Active.nAttackRoll != null)
				this.push([Info.Active.Char, Info.Active.ActionType, Info.Active.Skill, Info.Active.gItem],
					Info.Active.nAttackRoll);
			},
		Show: function()
			{
			if (this._gInfo.length > 0)
				{
				this.CalculateValue();
				this.sort();
				return this.CreateTable( Local.Text_Table_Attack, "stat_attack",
					[Local.Text_Table_Char, Local.Text_Table_AttackType, Local.Text_Table_Skill, Local.Text_Table_Item]);
				}
			return "";
			}
		}
	});


var CILDefenceRoll = DefineClass({
	extend: CInfoList,
	construct: function(nKeys, CValueList) {this.superclass(nKeys, CValueList);},
	methods:
		{
		SaveInfo: function(Info)
			{
			if (Info.Active.ActionType.GetKind() === 0)
				{
				for (var i = 0; i < Info.gPassive.length; ++i)
					{
					if (Info.gPassive[i].nDefenceRoll != null)
						this.push([Info.gPassive[i].Char, Info.Active.ActionType,
							Info.gPassive[i].Skill, Info.gPassive[i].gItem], Info.gPassive[i].nDefenceRoll);
					}
				}
			},
		Show: function()
			{
			if (this._gInfo.length > 0)
				{
				this.CalculateValue();
				this.sort();
				return this.CreateTable(Local.Text_Table_Defence, "stat_defence",
					[Local.Text_Table_Char, Local.Text_Table_DefenceType, Local.Text_Table_Skill, Local.Text_Table_Item]);
				}
			return "";
			}
		}
	});


var CILDamage = DefineClass({
	extend: CInfoList,
	construct: function(nKeys, CValueList) {this.superclass(nKeys, CValueList);},
	methods:
		{
		SaveInfo: function(Info)
			{
			if (Info.Active.ActionType.GetKind() === 0)
				{
				for (var i = 0; i < Info.gPassive.length; ++i)
					{
					if (Info.gPassive[i].gDamage.length > 0)
						this.push([Info.Active.Char, Info.Active.ActionType, Info.Active.Skill, Info.Active.gItem],
							Info.gPassive[i].gDamage);
					}
				}
			},
		Show: function()
			{
			if (this._gInfo.length > 0)
				{
				this.CalculateValue();
				this.sort();
				return this.CreateTable(Local.Text_Table_Damage, "stat_damage",
					[Local.Text_Table_Char, Local.Text_Table_AttackType, Local.Text_Table_Skill, Local.Text_Table_Item]);
				}
			return "";
			}
		}
	});


var CILHeal = DefineClass({
	extend: CInfoList,
	construct: function(nKeys, CValueList) {this.superclass(nKeys, CValueList);},
	methods:
		{
		SaveInfo: function(Info)
			{
			if (Info.Active.ActionType.GetKind() === 1)
				{
				for (var i = 0; i < Info.gPassive.length; ++i)
					{
					if (Info.gPassive[i].nHealedHP != null || Info.gPassive[i].nHealedMP != null)
						this.push([Info.Active.Char, Info.Active.Skill, Info.Active.gItem],
							[Info.gPassive[i].nHealedHP, Info.gPassive[i].nHealedMP]);
					}
				}
			},
		Show: function()
			{
			if (this._gInfo.length > 0)
				{
				this.CalculateValue();
				this.sort();
				return this.CreateTable(Local.Text_Table_Heal, "stat_heal",
					[Local.Text_Table_Char, Local.Text_Table_Skill, Local.Text_Table_Item]);
				}
			return "";
			}
		}
	});


var CILHealed = DefineClass({
	extend: CInfoList,
	construct: function(nKeys, CValueList) {this.superclass(nKeys, CValueList);},
	methods:
		{
		SaveInfo: function(Info)
			{
			if (Info.Active.ActionType.GetKind() === 1)
				{
				for (var i = 0; i < Info.gPassive.length; ++i)
					{
					if (Info.gPassive[i].nHealedHP != null || Info.gPassive[i].nHealedMP != null)
						this.push([Info.gPassive[i].Char],
							[Info.gPassive[i].nHealedHP, Info.gPassive[i].nHealedMP]);
					}
				}
			},
		Show: function()
			{
			if (this._gInfo.length > 0)
				{
				this.CalculateValue();
				this.sort();
				return this.CreateTable(Local.Text_Table_Healed, "stat_healed", [Local.Text_Table_Char]);
				}
			return "";
			}
		}
	});


var CILItemDamage = DefineClass({
	extend: CInfoList,
	construct: function(nKeys, CValueList) {this.superclass(nKeys, CValueList);},
	methods:
		{
		_SetTableBodyCellContents: function()
			{
			for (var i = 0; i < this._gInfo.length; ++i)
				{
				var gBodyCellContent = [];
				for (var j = 0; j < this._gInfo[i].gKey.length; ++j)
					gBodyCellContent.push( this._gInfo[i].gKey[j] );

				gBodyCellContent.push( this._gInfo[i].ValueList.AvgValueStr());

				this._Table.SetBodyCellContents.apply(this._Table, gBodyCellContent);
				}
			},
		SaveInfo: function(Info)
			{
			if (Info.Active.ActionType.GetKind() === 0)
				{
				for (var i = 0; i < Info.gPassive.length; ++i)
					{
					if (Info.gPassive[i].nItemDamage != null)
						this.push([Info.gPassive[i].Char, Info.gPassive[i].DamagedItem],
							Info.gPassive[i].nItemDamage);
					}
				}
			},
		Show: function()
			{
			if (this._gInfo.length > 0)
				{
				this.CalculateValue();
				this.sort();
				return this.CreateTable(Local.Text_Table_DamagedItems, "stat_item_damage",
					[Local.Text_Table_Char, Local.Text_Table_Item]);
				}
			return "";
			},
		CreateTable: function(Title, Id, gKeyName)
			{
			// Key1, Key2, ..., nDamage
			this._Table = new CTable(Title, Id, this._nKeys + 1);

			var gHeadCellContent = new Array(this._nKeys + 1);
			for (var i = 0; i < this._nKeys; ++i)
				gHeadCellContent[i] = gKeyName[i];
			gHeadCellContent[this._nKeys] = Local.Text_Table_ItemDamagePoints;
			this._Table.SetHeadCellContents.apply(this._Table, gHeadCellContent);

			var gBodyCellContentType = new Array(this._nKeys + 1);
			for (var i = 0; i < this._nKeys; ++i)
				gBodyCellContentType[i] = "string";
			gBodyCellContentType[this._nKeys] = "number";
			this._Table.SetBodyCellContentTypes.apply(this._Table, gBodyCellContentType);

			this._SetTableBodyCellContents();

			return this._Table.CreateHTML();
			},
		push: function(gKey, Value)
			{
			var ValueList = new this._CValueList();
			ValueList.push(Value);
			return this._gInfo.push(new CInfoList._CInfo(gKey, ValueList));
			}
		}
	});


// FUNCTIONS //////////////////////////////////////////////////////////////////

function CountStat(Document, bLastSubPage)
	{
	// Read the last round only when reading the last sub page
	if (!bLastSubPage) RemoveLastRound(Document);

	var Navi = new CNavi(0, 0, 0, 0);

	var allRows = Document.getElementsByTagName("tr");
	for (var i = 0; i < allRows.length; ++i)
		{
		var Info = new CActionInfo(Navi);

		var IniColumn = first_child(allRows[i]);
		GetIniInfo(IniColumn, Info);
		if (Info.Active.nIniRoll == null)	// not a initiative column
			continue;
		++Info.Navi.nRow;

		var ActiveColumn = node_after(IniColumn);
		GetActiveInfo(ActiveColumn, Info);

		switch (Info.Active.ActionType.GetKind())
			{
			case 0:		// Attack
				{
				var PassiveColumn = node_after(ActiveColumn);
				GetAttackedInfo(PassiveColumn, Info);
				break;
				}
			case 1:		// Heal
			case 2:		// Buff
				{
				var PassiveColumn = node_after(ActiveColumn);
				GetHealedBuffedInfo(PassiveColumn, Info);
				break;
				}
			case 3:		// Wait
			default:	// Unknown
				;
			}
		Stat.SaveInfo(Info);
		}
	}


function RemoveLastRound(Document)
	{
	var allRows = Document.getElementsByTagName("tr");
	for (var i = 0; i < allRows.length; ++i)
		{
		if (allRows[i].className != null &&
			allRows[i].className.indexOf("content_table_row_") === 0)
			{
			var allH1 = allRows[i].getElementsByTagName("h1");
			if (allH1[0] != null &&
				allH1[0].firstChild != null &&
				allH1[0].firstChild.nodeType == 3 &&
				allH1[0].firstChild.data == Local.OrigText_LastRound)
				{
				allRows[i].parentNode.removeChild(allRows[i]);
				break;
				}
			}
		}
	}


// return: true: it's not a initiative column, or it's a initiative column and the format is right
//         false: it's a initiative column but the format is wrong
function GetIniInfo(Node, Info)
	{
	if (Node == null || Node.className != "rep_initiative")
		return true;

	if (Node.innerHTML == "&nbsp;")
		return true;

	// \1	ini
	// \2	current action
	// \3	total actions
	var Patt_Ini = Local.Pattern_Ini;
	var result = Patt_Ini.exec(Node.innerHTML);
	if (result == null)
		{
		DbgMsgAction(Info, "IniInfo: " + Node.innerHTML);
		return false;
		}

	Info.Active.nIniRoll = Number(result[1]);
	Info.Active.nCurrAction = Number(result[2]);
	Info.Active.nTotalActions = Number(result[3]);
	return true;
	}


// return: whether the format is right
function GetActiveInfo(Node, Info)
	{
	if (Node == null)
		{
		DbgMsgAction(Info, "ActiveInfo: null");
		return false;
		}
	var nStartNode = 0;
	var Str = Node.innerHTML;

	// \1	span node
	// \2	npc Id
	var Patt_Char = Local.Pattern_Active_Char;
	var result = Patt_Char.exec(Str);
	if (result == null)
		{
		DbgMsgAction(Info, "ActiveInfo (Char): " + Node.innerHTML);
		return true;
		}
	var CharNode = result[1] != null ? Node.childNodes[nStartNode].childNodes[0] :
		Node.childNodes[nStartNode];
	Info.Active.Char = new CChar(CharNode);
	Info.Active.nCharId = result[2] != null ? Number(result[2]) : null;
	nStartNode += result[1] != null ? 1 : (result[2] != null ? 2 : 1);
	Str = Str.substring(result[0].length);

	// \1	attack
	// \2	heal or buff
	// \3	left parenthesis
	var Patt_Action1 = Local.Pattern_Active_Action1;
	result = Patt_Action1.exec(Str);
	if (result == null)
		{
		// \1	other action
		var Patt_Action2 = Local.Pattern_Active_Action2;
		result = Patt_Action2.exec(Str);
		if (result == null)
			{
			DbgMsgAction(Info, "ActiveInfo (Action2): " + Node.innerHTML);
			return false;
			}
		Info.Active.ActionType = new CActionType(result[1]);
		return true;
		}
	if (result[1] != null)
		{
		Info.Active.ActionType = new CActionType(result[1]);
		if (Info.Active.ActionType.GetKind() !== 0)
			{
			DbgMsgAction(Info, "ActiveInfo (Attack Type): " + result[1]);
			return false;
			}
		nStartNode += 1;
		Str = Str.substring(result[0].length);
		}
	else
		{
		Info.Active.ActionType = new CActionType(result[2]);
		if (Info.Active.ActionType.GetKind() !== 1 && Info.Active.ActionType.GetKind() !== 2)
			{
			DbgMsgAction(Info, "ActiveInfo (Heal/Buff Type): " + result[2]);
			return false;
			}
		Info.Active.Skill = new CSkill(Node.childNodes[nStartNode + 1]);
		if (result[3] == null)
			return true;
		nStartNode += 3;
		Str = Str.substring(result[0].length);
		}

	switch (Info.Active.ActionType.GetKind())
		{
		case 0:	// attack
			{
			// \1	single roll
			// \2	position n
			// \3	multiple roll n
			// \4	MP
			// \5	item list
			var Patt_ActtackDetails = Local.Pattern_Active_AttackDetails;
			result = Patt_ActtackDetails.exec(Str);
			if (result == null)
				{
				DbgMsgAction(Info, "ActiveInfo (ActtackDetails): " + Node.innerHTML);
				return false;
				}
			Info.Active.Skill = new CSkill(Node.childNodes[nStartNode]);
			Info.Active.nAttackRoll = Number(result[1] != null ? result[1] : result[3]);
			Info.Active.nSkillMP = result[4] != null ? Number(result[4]) : null;
			if (result[5] != null)
				{
				Info.Active.gItem = new CKeyList();
				nStartNode += result[4] != null ? 4 : 2;
				var ItemNode;
				while ((ItemNode = Node.childNodes[nStartNode]) != null)
					{
					Info.Active.gItem.push(new CItem(ItemNode));
					nStartNode += 2;
					}
				}
			return true;
			}
		case 1:	// heal
		case 2:	// buff
			{
			// \1	MP
			// \2	normal item list
			// \3	magical potion
			var Patt_HealBuffDetails = Local.Pattern_Active_HealBuffDetails;
			result = Patt_HealBuffDetails.exec(Str);
			if (result == null)
				{
				DbgMsgAction(Info, "ActiveInfo (HealBuffDetails): " + Node.innerHTML);
				return false;
				}
			Info.Active.nSkillMP = result[1] != null ? Number(result[1]) : null;
			if (result[2] != null)
				{
				Info.Active.gItem = new CKeyList();
				nStartNode += result[1] != null ? 2 : 0;
				var ItemNode;
				while ((ItemNode = Node.childNodes[nStartNode]) != null)
					{
					Info.Active.gItem.push(new CItem(ItemNode));
					nStartNode += 2;
					}
				}
			else if (result[3] != null)
				{
				Info.Active.gItem = new CKeyList();
				nStartNode += result[1] != null ? 2 : 0;
				Info.Active.gItem.push(new CItem(Node.childNodes[nStartNode]));
				// nStartNode: determine by the number of reagents
				}
			return true;
			}
		default:// impossible, the value can only be 0, 1, or 2
			return false;
		}
	}


// return: whether the format is right
function GetAttackedInfo(Node, Info)
	{
	if (Node == null)
		{
		DbgMsgAction(Info, "AttackedInfo: null");
		return false;
		}
	var nStartNode = 0;
	var Str = Node.innerHTML;

	// \1	char span node
	// \2	char Id
	// \3	skill
	// \4	defence roll
	// \5	MP
	// \6	item list
	// \7	hit type
	// \8	struck down
	// \9	damage list
	// \10	item damage
	// \11	next flag
	var Patt_Attacked = Local.Pattern_Passive_Attacked;
	var bEnd = false;
	while (!bEnd)
		{
		var PassiveInfo = new CPassiveInfo();
		var result = Patt_Attacked.exec(Str);
		if (result == null)
			{
			DbgMsgAction(Info, "AttackedInfo: " + Node.innerHTML);
			return true;
			}
		var CharNode = result[1] != null ? Node.childNodes[nStartNode].childNodes[0] :
			Node.childNodes[nStartNode];
		PassiveInfo.Char = new CChar(CharNode);
		PassiveInfo.nCharId = result[2] != null ? Number(result[2]) : null;
		nStartNode += result[1] != null ? 1 : (result[2] != null ? 2 : 1);
		if (result[3] != null)
			{
			PassiveInfo.Skill = new CSkill(Node.childNodes[nStartNode +1]);
			nStartNode += 2;
			}
		PassiveInfo.nDefenceRoll = Number(result[4]);
		if (result[5] != null)
			{
			PassiveInfo.nSkillMP = Number(result[5]);
			nStartNode += 2;
			}
		if (result[6] != null)
			{
			PassiveInfo.gItem = new CKeyList();
			nStartNode += 1;
			var ItemNode = Node.childNodes[nStartNode];
			while (ItemNode != null && ItemNode.nodeName == "A")
				{
				PassiveInfo.gItem.push(new CItem(ItemNode));
				nStartNode += 2;
				ItemNode = Node.childNodes[nStartNode];
				}
			}
		else
			nStartNode += 1;
		PassiveInfo.HitType = new CHitType(result[7]);
		PassiveInfo.bStruckDown = (result[8] != null);
		nStartNode += result[8] != null ? 2 : 1;
		if (result[9] != null)
			{
			PassiveInfo.gDamage = [];
			nStartNode += 1;
			var DamageNode = Node.childNodes[nStartNode];
			while (DamageNode != null && (DamageNode.nodeType == 3 ||
				(DamageNode.nodeName == "SPAN" &&
				DamageNode.firstChild != null && DamageNode.firstChild.nodeType == 3)))
				{
				PassiveInfo.gDamage.push(new CDamage(DamageNode));
				nStartNode += 2;
				DamageNode = Node.childNodes[nStartNode];
				}
			nStartNode -= 1;
			}
		if (result[10] != null)
			{
			PassiveInfo.DamagedItem = new CItem(Node.childNodes[nStartNode +1]);
			PassiveInfo.nItemDamage = Number(result[10]);
			nStartNode += 3;
			}
		if (result[11] != null)
			nStartNode += 1;
		else
			bEnd = true;

		Info.gPassive.push(PassiveInfo);
		Str = Str.substring(result[0].length);
		}
	return true;
	}


// return: whether the format is right
function GetHealedBuffedInfo(Node, Info)
	{
	if (Node == null)
		{
		DbgMsgAction(Info, "HealedBuffedInfo: null");
		return false;
		}
	var nStartNode = 0;
	var Str = Node.innerHTML;

	// \1	span node
	// \2	char Id
	// \3	self
	// \4	HP
	// \5	MP
	// \6	next flag
	var Patt_HealedBuffed = Local.Pattern_Passive_Healed_Buffed;
	var bEnd = false;
	while (!bEnd)
		{
		var PassiveInfo = new CPassiveInfo();
		var result = Patt_HealedBuffed.exec(Str);
		if (result == null)
			{
			DbgMsgAction(Info, "HealedBuffedInfo: " + Node.innerHTML);
			return true;
			}
		if (result[3] != null)
			{
			PassiveInfo.Char = Info.Active.Char;
			PassiveInfo.nCharId = Info.Active.nCharId;
			}
		else
			{
			var CharNode = result[1] != null ? Node.childNodes[nStartNode].childNodes[0] :
				Node.childNodes[nStartNode];
			PassiveInfo.Char = new CChar(CharNode);
			PassiveInfo.nCharId = result[2] != null ? Number(result[2]) : null;
			nStartNode += result[1] != null ? 1 : (result[2] != null ? 2 : 1);
			}
		PassiveInfo.nHealedHP = result[4] != null ? Number(result[4]) : null;
		PassiveInfo.nHealedMP = result[5] != null ? Number(result[5]) : null;
		nStartNode += 1;
		if (result[6] != null)
			nStartNode += 1;
		else
			bEnd = true;

		Info.gPassive.push(PassiveInfo);
		Str = Str.substring(result[0].length);
		}
	return true;
	}


function DbgMsgAction(Info, Text)
	{
	if (DEBUG)
		alert("[" + Info.Navi.nLevel + "." + Info.Navi.nRoom + "." +
			Info.Navi.nRound + "." + Info.Navi.nRow + "] " + Text);
	}


// GLOBAL VARIABLES ///////////////////////////////////////////////////////////

var DEBUG = false;

var Contents = {
	OrigText_Button_DungeonDetails	: ["details",
					   "详细资料"],
	OrigText_Button_DuelDetails	: ["Details",
					   "详细"],
	OrigText_Button_DungeonStat	: ["statistics",
					   "统计表"],
	OrigText_Level			: ["Level",
					   "层数"],
	OrigText_LastRound		: ["Last round:",
					   "最后回合:"],
	OrigTextList_ActionType		: [["attacks", "ranged attacks", "attacks with magic", "socially attacks", "cunningly attacks", "activates on", "works as a force of nature upon", "infected", "casts an explosion at", "deactivated", "magic projectile", "curse", "scare", "heals with", "uses", "summons with", "is unable to do anything.", "looks around in boredom and waits."],
					   ["近战攻击", "远程攻击", "魔法攻击", "心理攻击", "偷袭", "触发", "作为自然灾害", "散布", "制造爆炸", "解除", "魔法投射", "诅咒", "恐吓", "治疗", "使用", "召唤", "不能执行任何动作.", "无聊的打量四周,等待着."]],
	OrigTextList_DamageType		: [["crushing damage", "cutting damage", "piercing damage", "fire damage", "ice damage", "lightning damage", "poison damage", "acid damage", "psychological damage", "holy damage", "disarm trap", "mana damage"],
					   ["粉碎伤害", "切割伤害", "穿刺伤害", "火焰伤害", "寒冰伤害", "闪电伤害", "毒素伤害", "酸性伤害", "心灵伤害", "神圣伤害", "陷阱伤害", "法力伤害"]],
	Pattern_Ini			: [/^Initiative ([\d]+)<br><span .*?>Action ([\d]+) of ([\d]+)<\/span>$/,
					   /^先攻([\d]+)<br><span .*?>第([\d]+)步行动 \/ 共([\d]+)步<\/span>$/],
	Pattern_Active_Char		: [/^(<span .*?>)?<a .*?>.*?<\/a>(?:<span .*?>([\d]+)<\/span>)?(?:<img .*?><\/span>)?/,
					   /^(<span .*?>)?<a .*?>.*?<\/a>(?:<span .*?>([\d]+)<\/span>)?(?:<img .*?><\/span>)?/],
	Pattern_Active_Action1		: [/^\s*(?:([A-Za-z][A-Za-z ]+[A-Za-z]) +\(|([A-Za-z][A-Za-z ]+[A-Za-z]) +<a .*?>.*?<\/a>(?:( \()|$| on $))/,
					   /^\s*(?:([^\u0000-\u007F]+) +\(|([^\u0000-\u007F]+)<a .*?>.*?<\/a>(?:( \()|$|给$))/],
	Pattern_Active_Action2		: [/^\s*([\S].*[\S])\s*$/,
					   /^\s*([\S].*[\S])\s*$/],
	Pattern_Active_AttackDetails	: [/^<a .*?>.*?<\/a>(?:\/([\d]+)|(?:\/([A-Za-z ]+): ([\d]+))+)(?:\/<span .*?>([\d]+) MP<\/span>)?(\/(?:<a .*?>.*?<\/a>,)*<a .*?>.*?<\/a>)?\)$/,
					   /^<a .*?>.*?<\/a>(?:\/([\d]+)|(?:\/([^\u0000-\u007F]+): ([\d]+))+)(?:\/<span .*?>([\d]+) 法力<\/span>)?(\/(?:<a .*?>.*?<\/a>,)*<a .*?>.*?<\/a>)?\)$/],
	Pattern_Active_HealBuffDetails	: [/^(?:<span .*?>([\d]+) MP<\/span>)?(?:\/)?(?:((<a .*?>.*?<\/a>,)*<a .*?>.*?<\/a>)|(<a .*?>.*?<\/a>\s+(?:<img .*?>)+))?\)(?: on )?$/,
					   /^(?:<span .*?>([\d]+) 法力<\/span>)?(?:\/)?(?:((<a .*?>.*?<\/a>,)*<a .*?>.*?<\/a>)|(<a .*?>.*?<\/a>\s+(?:<img .*?>)+))?\)(?:给)?$/],
	Pattern_Passive_Attacked	: [/^(<span .*?>)?<a .*?>.*?<\/a>(?:<span .*?>([\d]+)<\/span>)?(?:<img .*?><\/span>)?\s*\((<a .*?>.*?<\/a>\/)?([\d]+)(?:\/<span .*?>([\d]+) MP<\/span>)?(\/(?:<a .*?>.*?<\/a>,)*<a .*?>.*?<\/a>)?\): <span class="([A-Za-z_]+)">[A-Za-z ]+<\/span>( - [A-Za-z ]+)?(<br>(?:<span .*?>)?(?:-)?[\d]+ (?:\[(?:\+|-)[\d]+\] )?[A-Za-z ]+(?:<img .*?><\/span>)?)*(?:<br><a .*?>.*?<\/a> -([\d]+) HP)?(?:(<br>)|$)/,
					   /^(<span .*?>)?<a .*?>.*?<\/a>(?:<span .*?>([\d]+)<\/span>)?(?:<img .*?><\/span>)?\s*\((<a .*?>.*?<\/a>\/)?([\d]+)(?:\/<span .*?>([\d]+) 法力<\/span>)?(\/(?:<a .*?>.*?<\/a>,)*<a .*?>.*?<\/a>)?\): <span class="([A-Za-z_]+)">[^\u0000-\u007F]+<\/span>( - [^\u0000-\u007F]+ *)?(<br>(?:<span .*?>)?(?:-)?[\d]+ (?:\[(?:\+|-)[\d]+\] )?[^\u0000-\u007F]+(?:<img .*?><\/span>)?)*(?:<br><a .*?>.*?<\/a> -([\d]+) HP)?(?:(<br>)|$)/],
	Pattern_BasicDamage		: [/causes: <b>([\d]+)<\/b>/,
					   /造成: <b>([\d]+)<\/b>/],
	Pattern_Damage			: [/^((?:-)?[\d]+) (?:\[((?:\+|-)[\d]+)\] )?([A-Za-z][A-Za-z ]+[A-Za-z])$/,
					   /^((?:-)?[\d]+) (?:\[((?:\+|-)[\d]+)\] )?([^\u0000-\u007F]+)$/],
	Pattern_Passive_Healed_Buffed	: [/^(?:(<span .*?>)?<a .*?>.*?<\/a>(?:<span .*?>([\d]+)<\/span>)?(?:<img .*?><\/span>)?\s+|(themselves))(?: \+([\d]+) HP)?(?: \+([\d]+) MP)?(?:(<br>)|$)/,
					   /^(?:(<span .*?>)?<a .*?>.*?<\/a>(?:<span .*?>([\d]+)<\/span>)?(?:<img .*?><\/span>)?\s+|(自己))(?: \+([\d]+) HP)?(?: \+([\d]+) 法力)?(?:(<br>)|$)/],
	Text_Button_ExtraStat		: ["Extra Stat",
					   "额外统计"],
	Text_Button_EntireStat		: ["Entire Extra Stat",
					   "全城额外统计"],
	Text_Button_Show		: ["Show",
					   "显示"],
	Text_Button_Default		: ["Default",
					   "默认"],
	TextList_AttackType		: [["melee", "ranged", "spell", "social", "ambush", "trap", "nature", "disease", "detonate","disarm trap", "magic projectile", "curse", "scare"],
					   ["近战", "远程", "魔法", "心理", "偷袭", "陷阱", "自然", "疾病", "爆破", "解除陷阱", "魔法投射", "诅咒", "恐吓"]],
	TextList_HitType		: [["failed", "success", "good success", "critical success"],
					   ["闪避", "普通", "重击", "致命一击"]],
	Text_Loading			: ["Loading",
					   "载入中"],
	Text_Options			: ["Options:",
					   "选项:"],
	Text_DefaultMsg			: ["All the data this script stored in your machine has been cleared.",
					   "此脚本储存在你的机器上的所有数据已被清除。"],
	Text_Table_Ini			: ["Initiative",
					   "先攻权"],
	Text_Table_Attack		: ["Attack",
					   "攻击骰"],
	Text_Table_Defence		: ["Defence",
					   "防御骰"],
	Text_Table_Damage		: ["Damage",
					   "伤害"],
	Text_Table_Heal			: ["Healing By The Hero",
					   "给予治疗"],
	Text_Table_Healed		: ["Healing On The Hero",
					   "接受治疗"],
	Text_Table_DamagedItems		: ["Damaged Items",
					   "物品损坏"],
	Text_Table_Char			: ["Character",
					   "人物"],
	Text_Table_AttackType		: ["Attack type",
					   "攻击类型"],
	Text_Table_DefenceType		: ["Defence type",
					   "防御类型"],
	Text_Table_Skill		: ["Skill",
					   "技能"],
	Text_Table_Item			: ["Item",
					   "物品"],
	Text_Table_AvgRoll		: ["Average roll",
					   "平均值"],
	Text_Table_Times		: ["Times",
					   "次数"],
	Text_Table_RollList		: ["Roll list",
					   "数值列表"],
	Text_Table_ItemDamagePoints	: ["Damage Points",
					   "损坏点数"]
	};

var Style = "div.stat_header {margin:1em auto 0.5em auto;} " +
	"span.stat_title {margin: auto 1em auto 0em; font-size:20px; font-weight:bold; color:#FFF;} span.clickable {cursor:pointer;} " +
	"table[hide] {display:none;} " +
	"table.pair_value {width:100%;} table.pair_value td {width:50%; min-width:3em; text-align:right; color:#F8A400;} table.pair_value td + td {color:#00CC00;} ";

var Local;
var Stat;

try {Main();} catch(e) {alert("Main(): " + e);}


// FUNCTIONS //////////////////////////////////////////////////////////////////
if( typeof(GM_addStyle)=='undefined' ){function GM_addStyle(styles){         
        var S = document.createElement('style'); 
        S.type = 'text/css'; 
        var T = ''+styles+''; 
        T = document.createTextNode(T) 
        S.appendChild(T); 
        document.body.appendChild(S); 
        return; 
}}

if (!this.GM_getValue || this.GM_getValue.toString().indexOf("not supported")>-1) {
     this.GM_getValue=function (key,def) {
         return localStorage[key] || def;
     };
     this.GM_setValue=function (key,value) {
         return localStorage[key]=value;
    };
 }
		
function Main()
	{
	// Language selection
	Local = GetLocalContents(Contents);
	if (Local === null) return;
    //GM_log(Local);
	// Add CSS
	GM_addStyle(Style);

	// Add buttons
	var KeyButton = AddButtonBesideDisabledButton(
		[Local.OrigText_Button_DungeonDetails, Local.Text_Button_ExtraStat, OnCountStat],
		[Local.OrigText_Button_DungeonStat, Local.Text_Button_EntireStat, OnCountEntireStat],
		[Local.OrigText_Button_DuelDetails, Local.Text_Button_ExtraStat, OnCountStat]);
	if (KeyButton === null) return;

	// Stat initialization
	Stat = new CStat( node_after(KeyButton.parentNode) );
	Stat.RegInfoList(new CILIni(		1, CVLNumber));
	Stat.RegInfoList(new CILAttackRoll(	4, CVLNumber));
	Stat.RegInfoList(new CILDefenceRoll(	4, CVLNumber));
	Stat.RegInfoList(new CILDamage(		4, CVLDamage));
	Stat.RegInfoList(new CILHeal(		3, CVLPairNumber));
	Stat.RegInfoList(new CILHealed(		1, CVLPairNumber));
	Stat.RegInfoList(new CILItemDamage(	2, CVLNumber));
	}


// It will only add the first eligible button
// return: the node of the first eligible disabled button, or null if didn't find anyone
function AddButtonBesideDisabledButton(/* [DisabledButtonText, ButtonText, ClickEvent], [...], ... */)
	{
	var allInputs = document.getElementsByTagName("input");
	for (var i = 0; i < allInputs.length; ++i)
		{
		if (allInputs[i].className == "button_disabled")
			{
			for (var j = 0; j < arguments.length; ++j)
				{
				if (allInputs[i].getAttribute("value") == arguments[j][0])
					{
					AddButton(allInputs[i], arguments[j][1], arguments[j][2]);
					return allInputs[i];
					}
				}
			}
		}
	return null;
	}


// Add a button to the end of the given node's parent node
function AddButton(SiblingNode, Value, OnClick)
	{
	var newButton = document.createElement("input");
	newButton.setAttribute("type", "button");
	newButton.setAttribute("class", "button");
	newButton.setAttribute("value", Value);
	newButton.addEventListener("click", OnClick, false);
	var newBlank = document.createTextNode("            ");
	SiblingNode.parentNode.appendChild(newBlank);
	SiblingNode.parentNode.appendChild(newButton);
	}


function OnCountStat()
	{
	try	{
		if (this.className == "button_disabled")
			return;
		else
			this.className = "button_disabled";

		Stat.nTotalPages = 1;
		ReadPage(document, true);
		}
	catch (e) {alert("OnCountStat(): " + e);}
	}


function OnCountEntireStat()
	{
	try	{
		if (this.className == "button_disabled")
			return;
		else
			this.className = "button_disabled";

		CountEntireStat();
		}
	catch (e) {alert("OnCountEntireStat(): " + e);}
	}


function CountEntireStat()
	{
	var nCurrRepId = GetHiddenInfo(document, "report_id[0]", "");
	var nMaxLevel = Stat.nTotalPages = GetStatPageMaxLevel(document, 1);

	for (var CurrLevel = 1; CurrLevel <= nMaxLevel; ++CurrLevel)
		GetPage(nCurrRepId, CurrLevel, 1, true);

	Stat.ShowProgress();
	}


function GetPage(nRepId, nLevel, nRepPage, bFirstRead)
	{
	var XmlHttp = new XMLHttpRequest();

	XmlHttp.onreadystatechange = function ()
		{
		try	{
			if (XmlHttp.readyState == 4 && XmlHttp.status == 200)
				{
				var Page = document.createElement("div");
				Page.innerHTML = XmlHttp.responseText;
				ReadPage(Page, bFirstRead);
				}
			}
		catch (e) {alert("XMLHttpRequest.onreadystatechange(): " + e);}
		};

	var URL = location.protocol + "//" + location.host + "/wod/spiel/dungeon/report.php" +
		"?cur_rep_id=" + nRepId +
		"&gruppe_id=&current_level=" + nLevel +
		"&REPORT_PAGE=" + nRepPage +
		"&IS_POPUP=1";

	XmlHttp.open("GET", URL, true);
	XmlHttp.send(null);
	}


function ReadPage(Document, bFirstRead)
	{
	var ret = GetRepPageInfo(Document, [1, 1]);
	var nCurrRepPage = ret[0];
	var nMaxRepPage = ret[1];

	if (bFirstRead && nMaxRepPage > 1)
		{
		var nRepId = GetHiddenInfo(Document, "report_id[0]", "");
		var nLevel = GetHiddenInfo(Document, "current_level", 1);

		Stat.nTotalPages += nMaxRepPage -1;
		for (var i = 1; i <= nMaxRepPage; ++i)
			{
			if (i !== nCurrRepPage)
				GetPage(nRepId, nLevel, i, false);
			}
		}

	CountStat(Document, (nCurrRepPage === nMaxRepPage));
	if (++Stat.nReadPages >= Stat.nTotalPages)
		Stat.Show();
	else
		Stat.ShowProgress();
	}


function GetHiddenInfo(Document, InfoName, DefaultValue)
	{
	var allInputs = Document.getElementsByTagName("input");
	for (var i = 0; i < allInputs.length; ++i)
		{
		if (allInputs[i].getAttribute("type") == "hidden" &&
			allInputs[i].name == InfoName)
			return allInputs[i].value;
		}
	return DefaultValue;
	}


function GetStatPageMaxLevel(Document, DefaultValue)
	{
	var allTds = Document.getElementsByTagName("td");
	for (var i = 0; i < allTds.length; ++i)
		{
		if (first_child(allTds[i].parentNode) != allTds[i])
			continue;
		var LevelNode = first_child(allTds[i]);
		if (LevelNode != null && LevelNode.nodeType == 3 && LevelNode.data == Local.OrigText_Level)
			{
			var Patt_Level = /^(?:<span .*?>)?(?:[\d]+)\/([\d]+)(?:<\/span>)?$/;
			var result = Patt_Level.exec(node_after(allTds[i]).innerHTML);
			if (result == null) return DefaultValue;
			return Number(result[1]);
			}
		}
	return DefaultValue;
	}


// return: an array, [0]: nCurrRepPage, [1]: nMaxRepPage
function GetRepPageInfo(Document, DefaultValue)
	{
	var ret = [DefaultValue[0], DefaultValue[1]];

	var SubPageIndexNode;
	var allInputs = Document.getElementsByTagName("input");
	for (var i = 0; i < allInputs.length; ++i)
		{
		if (allInputs[i].value.indexOf("=1=") !== -1)
			{
			SubPageIndexNode = allInputs[i];
			break;
			}
		}
	if (SubPageIndexNode == null)
		return ret;

	var bIndexEnd = false;
	while (!bIndexEnd)
		{
		var IndexPatt = /=([\d]+)=/;
		var target = (SubPageIndexNode.value == null)? SubPageIndexNode.firstChild.textContent:SubPageIndexNode.value;		
		var Result = IndexPatt.exec(target);
		var nCurrIndex = Number(Result[1]);

		if (SubPageIndexNode.className == "paginator_selected clickable")
			ret[0] = nCurrIndex;

		SubPageIndexNode = node_after(node_after(SubPageIndexNode));
		if (SubPageIndexNode == null || SubPageIndexNode.nodeName != "A")
			{
			ret[1] = nCurrIndex;
			bIndexEnd = true;
			}
		}

	return ret;
	}