Zum Inhalt wechseln

Foto

Script for dumping pet stats to the log file (guru input requested)


  • Bitte melde dich an um zu Antworten
4 Antworten in diesem Thema

#1
DarthGizka

DarthGizka
  • Members
  • 867 Beiträge
Mike's post in Ranger pet stats gave me the idea to query the actual stats of actual pets and dump them to the log file, which is way better than the unverified guesswork in the wiki.
 
The script creates a succubus who summons each type of pet in turn and dumps the stats, for each level from 1 to GetMaxLevel(). Then she does the same again, but with the Master Ranger ability added. Calling the script again will abort the test if it is still running; it will also unsummon the summoner and any remaining pets. Moving more than 5 metres away from the summoner will abort the script as well.
 
A full test series runs a while, about a quarter hour for level cap 35. If the succubus is facing your toon when you get back to your computer then the test did complete normally.
 
The script dumps several stats that are returned by GetCreatureProperty(). Which stats to dump is easily configured via properties_to_log_(), a function that returns a list of property ids.
 
Log lines are prefixed with a unique string, so that tools can pick them out of the log file:
...
Script log_pet_stats_nss__20140424 >>> 3 59 epi300ar_post_coronation
Script GetClassDataFloat:BaseManaStamina nClass 24-> 50.000000
...
Script SetPlot [tut_control_followers] [TUT_CONTROL_FOLLOWERS_1] -> [1]
Script log_pet_stats_nss__20140424 succubus.utc,01,R,wolf.utc,15:1,7:130,28:0,8:52.5,30:1,9:59.65,53:1.35,39:1,10:61.3,11:3.25,16:0,32:1.5,33:6.15,52:0,42:10,43:10,44:10,46:0,45:0
...
Script SetPlot [tut_control_followers] [TUT_CONTROL_FOLLOWERS_1] -> [1]
Script log_pet_stats_nss__20140424 succubus.utc,25,M,spider_poisonous.utc,15:22,7:394.5,28:2.5,8:231.25,30:1,9:106.3,53:9.27,39:22,10:113.6,11:9.6,16:0,32:15,33:69.8,52:0,42:-10,43:0,44:0,46:0,45:75
Script log_pet_stats_nss__20140424 <<< 3 729 epi300ar_post_coronation
...
The format makes it easy to pick apart the data and process it using your favourite tools (mine happens to be Visual FoxPro).
 
It can be processed in its denormalised form (like in w_block_1 in the picture below) or it can be put into structured form (tables w_test, w_line and w_prop) for easy processing. This makes it easy to run all kinds of tests on the data and to generate any desired output.

DAO_Fox.png

Now to the part where only our resident gurus can help. The data collected by the script is fairly useful, and it is reliable because it queries the engine instead of calling included scripts (which would always give the toolset's answer and not the game's).

However, what's missing is an indicator of how much damage the pet will do. That is probably the most important point for many people and the reason why Mike started his topic in the first place.

I know which function computes the damage guesstimate that you can see in game (inventory screen etc.) but that is done by an include file. Besides not being very accurate, it would also introduce the problem I alluded to above: the answers would be computed by the toolset code, not by the actual game code.

On the other hand, some kind of DPS indicator is definitely needed...

#2
DarthGizka

DarthGizka
  • Members
  • 867 Beiträge
// log_pet_stats.nss
// 2014-04-24 r002 DarthGizka
//
// call the script to start the test, call it again to abort (or move away from the summoner)
//
// bin_ship/ECLog.ini must exist, with "Script = 1" in section [LogTypes]

#include "events_h"           // EVENT_TYPE_xxx
#include "global_objects_h"   // RESOURCE_SCRIPT_CREATURE_CORE

void handle_event_ (event e);
void start_test_series_ ();
int  unsummon_summoner_ ();

void main ()
{
   event e = GetCurrentEvent();

   if (IsEventValid(e))
   {
      handle_event_(e);

      return;
   }

   // remove summoner if present, otherwise start a new test series

   if (!unsummon_summoner_())
   {
      start_test_series_();
   }
}

///////////////////////////////////////////////////////////////////////////////////////////////////

const int MIN_LEVEL_ =  1;   // clamping levels can be useful during development because a full
const int MAX_LEVEL_ = 99;   // test series takes quite a while otherwise

const string LOG_PREFIX_     = "log_pet_stats_nss__20140424";
const string SUMMONER_TAG_   = "log_pet_stats_nss__summoner";

const float ABORT_DISTANCE_  = 5.0f;    // moving the PC away from the summoner stops the script

const int SAFE_STAMINA_RESERVE_ = 250;  // maximum needed is 120 for the spider

const int CYCLE_STEP_NONE_   = 0;
const int CYCLE_STEP_WOLF_   = 1;
const int CYCLE_STEP_BEAR_   = 2;
const int CYCLE_STEP_SPIDER_ = 3;
const int CYCLE_STEP_LEVEL_  = 4;
const int CYCLE_STEP_DONE_   = 5;

// 'imports' from tpv_utility_h
void floaty_ (string s, int colour = 0xFFFFFF, object above_whom = OBJECT_INVALID, float duration = 3.0f);
vector horizontal_vector_ (float angle, float magnitude);
string trim_ (string s);

///////////////////////////////////////////////////////////////////////////////////////////////////

int is_pet_summoning_command_ (event command_complete_event);
int find_and_log_current_pet_ (object summoner);
int initiate_next_cycle_step_ (object summoner, int next_step);
int level_summoner_           (object summoner, int level = 0);


void handle_event_ (event e)
{
   switch (GetEventType(e))
   {
      case EVENT_TYPE_SPAWN:
      {
         int original_level = GetLevel(OBJECT_SELF);

         HandleEvent(e, RESOURCE_SCRIPT_CREATURE_CORE);

         if (GetLevel(OBJECT_SELF) != original_level)
         {
            // creature_core smashed the character level
            level_summoner_(OBJECT_SELF, original_level);
         }

         return;
      }

      case EVENT_TYPE_CUSTOM_COMMAND_COMPLETE:
      {
         if (GetDistanceBetween(OBJECT_SELF, GetHero()) >= ABORT_DISTANCE_)
         {
            floaty_("*ABORTED*", 0xFF0000);

            return;
         }
/** /
         floaty_("0:" + IntToString(GetEventInteger(e, 0))
              + " 1:" + IntToString(GetEventInteger(e, 1))
              + " 2:" + IntToString(GetEventInteger(e, 2))
              + " 3:" + IntToString(GetEventInteger(e, 3))  );
/**/
         if (is_pet_summoning_command_(e))
         {
            int current_step = find_and_log_current_pet_(OBJECT_SELF);

            if (current_step > CYCLE_STEP_NONE_)
            {
               initiate_next_cycle_step_(OBJECT_SELF, current_step + 1);
            }

            return;
         }
      }
   }

   HandleEvent(e, RESOURCE_SCRIPT_CREATURE_CORE);
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// *todo* add stats needed for damage calculation (e.g. STR)

int[] properties_to_log_ ()
{
   int i = 0;
   int[] p;

   p[i++] = PROPERTY_SIMPLE_LEVEL;

   p[i++] = PROPERTY_DEPLETABLE_HEALTH;
   p[i++] = PROPERTY_ATTRIBUTE_REGENERATION_HEALTH_COMBAT;
   p[i++] = PROPERTY_DEPLETABLE_MANA_STAMINA;
   p[i++] = PROPERTY_ATTRIBUTE_REGENERATION_STAMINA_COMBAT;

   p[i++] = PROPERTY_ATTRIBUTE_ATTACK;
   p[i++] = PROPERTY_ATTRIBUTE_AP;
   p[i++] = PROPERTY_ATTRIBUTE_DAMAGE_BONUS;

   p[i++] = PROPERTY_ATTRIBUTE_DEFENSE;
   p[i++] = PROPERTY_ATTRIBUTE_ARMOR;
   p[i++] = PROPERTY_ATTRIBUTE_DISPLACEMENT;

   p[i++] = PROPERTY_ATTRIBUTE_RESISTANCE_MENTAL;
   p[i++] = PROPERTY_ATTRIBUTE_RESISTANCE_PHYSICAL;
   p[i++] = 52;  // PROPERTY_ATTRIBUTE_SPELLRESISTANCE_

   p[i++] = PROPERTY_ATTRIBUTE_DAMAGE_RESISTANCE_FIRE;
   p[i++] = PROPERTY_ATTRIBUTE_DAMAGE_RESISTANCE_COLD;
   p[i++] = PROPERTY_ATTRIBUTE_DAMAGE_RESISTANCE_ELEC;
   p[i++] = PROPERTY_ATTRIBUTE_DAMAGE_RESISTANCE_SPIRIT;
   p[i++] = PROPERTY_ATTRIBUTE_DAMAGE_RESISTANCE_NATURE;

   return p;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// http://social.bioware.com/wiki/datoolset/index.php/EVENT_TYPE_COMMAND_COMPLETE

int is_pet_summoning_ability_ (int ability_id);

int is_pet_summoning_command_ (event command_complete_event)
{
   if (GetEventInteger(command_complete_event, 0) == COMMAND_TYPE_USE_ABILITY)
   {
      if (GetEventInteger(command_complete_event, 1) != 1)
      {
         floaty_("evt[1] == " + IntToString(GetEventInteger(command_complete_event, 1)), 0xFF0000);

         return FALSE;
      }

      return is_pet_summoning_ability_(GetEventInteger(command_complete_event, 2));
   }

   return FALSE;
}

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

int unsummon_pets_ (object summoner)
{
   object[] party = GetPartyList(summoner);
   int i, n = GetArraySize(party), num_destroyed = 0;

   for (i = 0; i < n; ++i)
   {
      if (IsSummoned(party[i]))
      {
         DestroyObject(party[i]);
         ++num_destroyed;
      }
   }

   return num_destroyed;
}

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

int unsummon_summoner_ ()
{
   object[] o = GetObjectsInArea(GetArea(GetMainControlled()), SUMMONER_TAG_);
   int i, n = GetArraySize(o);

   for (i = 0; i < n; ++i)
   {
      unsummon_pets_(o[i]);
      DestroyObject(o[i]);
   }

   return n;
}

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

int is_master_ranger_ (object summoner)
{
   return HasAbility(summoner, ABILITY_TALENT_MASTER_RANGER);
}

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

int level_summoner_ (object summoner, int level = 0)
{
   if (level <= 0)
   {
      level = Max(0, GetLevel(summoner)) + 1;
   }

   level = Max(MIN_LEVEL_, level);

   if (level > Min(MAX_LEVEL_, GetMaxLevel()))
   {
      return FALSE;
   }

   SetCreatureProperty(summoner, PROPERTY_SIMPLE_LEVEL, IntToFloat(level), PROPERTY_VALUE_BASE);

   if (GetLevel(summoner) != level)
   {
      floaty_("level_summoner_(" + IntToString(level) + ") -> " + IntToString(GetLevel(summoner)), 0xFF0000);

      return FALSE;
   }

   floaty_((is_master_ranger_(summoner) ? "Master " : "") + "Ranger " + IntToString(level));

   return TRUE;
}

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

int make_master_ (object summoner, int level = 0)
{
   if (is_master_ranger_(summoner))
   {
      return FALSE;
   }

   AddAbility(summoner, ABILITY_TALENT_MASTER_RANGER);

   return level <= 0 || level_summoner_(summoner, level);
}

//-------------------------------------------------------------------------------------------------
// no setting of tags etc., in case we want to prep the real Leliana

void prep_summoner_ (object summoner, int level = 1)
{
   AddAbility(summoner, ABILITY_TALENT_HIDDEN_RANGER);
   AddAbility(summoner, ABILITY_TALENT_NATURE_I_COURAGE_OF_THE_PACK);
   AddAbility(summoner, ABILITY_TALENT_NATURE_II_HARDINESS_OF_THE_BEAR);
   AddAbility(summoner, ABILITY_TALENT_SUMMON_SPIDER);

   SetEventScript(summoner, GetCurrentScriptResource());

   // strange syntax for EnablevEvent(summoner, TRUE, EVENT_TYPE_CUSTOM_COMMAND_COMPLETE):
   SetLocalInt(summoner, "AI_CUSTOM_AI_ACTIVE", 1);

   level_summoner_(summoner, level);
}

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

int ability_for_step_ (int step)
{
   switch (step)
   {
      case CYCLE_STEP_WOLF_   :  return ABILITY_TALENT_NATURE_I_COURAGE_OF_THE_PACK;
      case CYCLE_STEP_BEAR_   :  return ABILITY_TALENT_NATURE_II_HARDINESS_OF_THE_BEAR;
      case CYCLE_STEP_SPIDER_ :  return ABILITY_TALENT_SUMMON_SPIDER;
   }

   return 0;
}

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

int is_pet_summoning_ability_ (int ability_id)
{
   switch (ability_id)
   {
      case ABILITY_TALENT_NATURE_I_COURAGE_OF_THE_PACK:
      case ABILITY_TALENT_NATURE_II_HARDINESS_OF_THE_BEAR:
      case ABILITY_TALENT_SUMMON_SPIDER:
      {
         return TRUE;
      }
   }

   return FALSE;
}

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

int do_summon_ (object summoner, int step)
{
   int ability_id = ability_for_step_(step);

   if (!IsModalAbilityActive(summoner, ability_id))
   {
      SetCreatureStamina(summoner, Max(SAFE_STAMINA_RESERVE_, GetCreatureStamina(summoner)));
      SetCooldown(summoner, ability_id, 0.0f);
   }

   ClearAllCommands(summoner, TRUE);

   return AddCommand(summoner, CommandUseAbility(ability_id, summoner), TRUE, TRUE);
}

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

void log_ (string text, int flush = FALSE)
{
   PrintToLog(LOG_PREFIX_ + " " + text);

   if (flush)  PrintToLogAndFlush("");
}

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

void log_begin_end_ (string tag)
{
   string s = tag
      + " " + IntToString(GetGameDifficulty())
      + " " + IntToString(GetTime())
      + " " + GetResRef(GetArea(GetHero()));

   log_(s, TRUE);
}

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

void summoning_series_finished_ (object summoner)
{
   object controlled = GetMainControlled();

   if (controlled == summoner)
   {
      controlled = GetHero();
   }

   if (controlled != summoner)
   {
      vector direction = GetPosition(controlled) - GetPosition(summoner);

      AddCommand(summoner, CommandTurn(180.0f - VectorToAngle(direction)), TRUE, TRUE);
   }

   floaty_("*DONE*", 0xFFFF00);
   log_begin_end_("<<<");
}

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

int initiate_next_cycle_step_ (object summoner, int next_step)
{
   switch (next_step)
   {
      case CYCLE_STEP_WOLF_:
      case CYCLE_STEP_BEAR_:
      case CYCLE_STEP_SPIDER_:
      {
         break;
      }

      case CYCLE_STEP_LEVEL_:
      {
         if (level_summoner_(summoner) || make_master_(summoner, MIN_LEVEL_))
         {
            next_step = CYCLE_STEP_WOLF_;

            break;
         }
      }
      /*FALL THRU*/

      default:
      {
         if (next_step != CYCLE_STEP_LEVEL_)
         {
            floaty_("next(" + IntToString(next_step) + ")", 0xFF0000);
         }
      }
      /*FALL THRU*/

      case CYCLE_STEP_DONE_:
      {
         summoning_series_finished_(summoner);

         return FALSE;
      }
   }

   return do_summon_(summoner, next_step);
}

///////////////////////////////////////////////////////////////////////////////////////////////////

object create_and_prep_summoner_ ();

void start_test_series_ ()
{
   log_begin_end_(">>>");

   object summoner = create_and_prep_summoner_();

   if (IsObjectValid(summoner))
   {
      ClearAllCommands(summoner, TRUE);
      do_summon_(summoner, CYCLE_STEP_WOLF_);
   }
}

//-------------------------------------------------------------------------------------------------
// can't return a resource array from a function?

object create_first_available_summoner_candidate_ (location where)
{
   resource[] candidates;
   int i = 0;

/** /
   candidates[i++] = R"den300cr_shianni.utc";  // doesn't summon
   candidates[i++] = R"house_cat.utc";         // doesn't summon
/**/
   candidates[i++] = R"succubus.utc";

   for (i = 0; i < GetArraySize(candidates); ++i)
   {
      object summoner = CreateObject(OBJECT_TYPE_CREATURE, candidates[i], where);

      if (IsObjectValid(summoner))
      {
         return summoner;
      }
   }

   return OBJECT_INVALID;
}

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

object create_and_prep_summoner_ ()
{
   object hero = GetMainControlled();
   float angle = 180.0f - GetFacing(hero);
   vector shift = horizontal_vector_(angle + 60.0f, 2.0f);
   location loc = GetSafeLocation(Location(GetArea(hero), GetPosition(hero) + shift, 240.0f - angle));

   object summoner = create_first_available_summoner_candidate_(loc);

   if (IsObjectValid(summoner))
   {
      SetGroupId(summoner, GROUP_NEUTRAL);
      SetTag(summoner, SUMMONER_TAG_);
      prep_summoner_(summoner, MIN_LEVEL_);
   }

   return summoner;
}

///////////////////////////////////////////////////////////////////////////////////////////////////

void log_pet_for_summoner_ (object pet, object summoner);
int classify_pet_res_ref_ (string res_ref);

int find_and_log_current_pet_ (object summoner)
{
   object[] party = GetPartyList(summoner);
   int i, step = CYCLE_STEP_NONE_;

   for (i = 0; i < GetArraySize(party); ++i)
   {
      object critter = party[i];

      if (IsSummoned(critter))
      {
         step = classify_pet_res_ref_(GetResRef(critter));

         if (step != CYCLE_STEP_NONE_)
         {
            log_pet_for_summoner_(critter, summoner);

            break;
         }
      }
   }

   return step;
}

//-------------------------------------------------------------------------------------------------
// returns the step index corresponding to the pet, STEP_NONE else

int classify_pet_res_ref_ (string res_ref)
{
   if (StringRight(res_ref, 4) == ".utc")
   {
      res_ref = StringLeft(res_ref, GetStringLength(res_ref) - 4);
   }

   if (res_ref == "wolf" || res_ref == "wolf_blight")
   {
      return CYCLE_STEP_WOLF_;
   }

   if (res_ref == "bear_black" || res_ref == "bear_great")
   {
      return CYCLE_STEP_BEAR_;
   }

   if (res_ref == "spider_giant" || res_ref == "spider_poisonous")
   {
      return CYCLE_STEP_SPIDER_;
   }

   return CYCLE_STEP_NONE_;
}

///////////////////////////////////////////////////////////////////////////////////////////////////

string f2s_ (float value, int decimals = 2)
{
   string s = trim_(FloatToString(value, 18, decimals));

   if (StringLeft(s, 1) == ".")
   {
      s = "0" + s;
   }

   while (StringRight(s, 1) == "0")
   {
      s = StringLeft(s, GetStringLength(s) - 1);
   }

   if (StringRight(s, 1) == ".")
   {
      s = StringLeft(s, GetStringLength(s) - 1);
   }

   return s == "" ? "0" : s;
}

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

string stats_item_ (object critter, int prop_id, int prop_type = PROPERTY_VALUE_TOTAL)
{
   return "," + IntToString(prop_id) + ":" + f2s_(GetCreatureProperty(critter, prop_id, prop_type));
}

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

string level_text_ (int level)
{
   return (level < 10 ? "0" : "") + IntToString(level);
}

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

void log_pet_for_summoner_ (object pet, object summoner)
{
   int[] properties = properties_to_log_();
   int i;

   string s = GetResRef(summoner)
      + "," + level_text_(GetLevel(summoner))
      + "," + (is_master_ranger_(summoner) ? "M" : "R")
      + "," + GetResRef(pet);

   for (i = 0; i < GetArraySize(properties); ++i)
   {
      s = s + stats_item_(pet, properties[i]);
   }

   log_(s);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// tpv_utilty_h

void floaty_ (string s, int colour = 0xFFFFFF, object above_whom = OBJECT_INVALID, float duration = 3.0f)
{
   if (above_whom == OBJECT_INVALID)
   {
      above_whom = GetMainControlled();
   }

   DisplayFloatyMessage(above_whom, s, FLOATY_MESSAGE, colour, duration);
}


vector vscale_ (vector v, float l)
{
   float m = GetVectorMagnitude(v);

   if (m > 0.0f)
   {
      return v * l / m;
   }

   return v;
}


vector horizontal_vector_ (float angle, float magnitude)
{
   return vscale_(AngleToVector(angle), magnitude);
}


string trim_ (string s)
{
   int l = GetStringLength(s);

   while (StringLeft(s, 1) == " ")
   {
      s = StringRight(s, --l);
   }

   while (StringRight(s, 1) == " ")
   {
      s = StringLeft(s, --l);
   }

   return s;
}
Note: I wrote the code 'blind', on a computer without the toolset. That is why I had to structure a bit more aggressively than usual, in order to facilitate debugging and reformulating parts of the logic using existing 'words'. As it turned out that wasn't needed, but it should make it easier to reuse bits for other experiments.

#3
Mike3207

Mike3207
  • Members
  • 1.709 Beiträge

Interesting thread so far. One thing I can add is that I have seen Great Bear do 80-90 points of damage against foes in Awakening at around level 31, likely under a Rage. I saw it do that against the Possessed Ogre Commander and the Spirits in the Deep Crypt in Awakening.



#4
Sunjammer

Sunjammer
  • Members
  • 925 Beiträge

Just thinking out loud here ...

 

When you spawn a pet, spawn a victim for it to fight,  make them hostile to each other (but not to the player or the pet summoner (toolset_groups.xls)). Use the victim's events to log damage (I assume GetLowResTimer will allow you to calculate DPS) and to spawn the next pet and/or victim when it dies. Depending on your need for accuracy repeat each pair of pet and victim multiple times.


  • DarthGizka gefällt das

#5
DarthGizka

DarthGizka
  • Members
  • 867 Beiträge
I like the way you're thinking. In fact, I've planned to do something like that in one of my projects, codename 'DPS'. The idea is exactly as you described: take a toon (like the player character), give it something to beat on, log damage events and compute effective DPS. The punching bag will get healed after each hit, so it can go on pretty much forever. This also works in duel mode - two combatants beating on each other. I've planned to make the DPS thingy as some kind of spell (or item, like the Litany of Adralla) so that it can be used from the quickbar. It is a subproject of a thing I'm working on, called The Wherewithal of Arcane Studies.

Some parts are finished already, like the equipment stuff that I posted about a while ago. Basically, you can summon an armour stand, a weapon stand and a chest, with all the goodies you might need to equip and test a build. Another click and they're gone again. You can also level, train and equip a character using predefined, named configurations (e.g. "Blazomancer#1") for easy testing. The idea is to test-drive complete builds with equipment, abilities and stats without having to play a hundred hours before the proof of the pudding becomes possible. But I digress...

The main point behind DPS and the Wherewithal is basically the same as that for the script under discussion: replace guesswork and theoretical discussion with actual results from within the game. The structure of the script can also be used to gather hard data for all fixed-damage abilities (i.e., without a damage roll), like spells.

I've already tinkered around with spells, because there it is easy to see whether the floaty is the expected number or not. I used that to work out the exact spell damage formulas for Origins. Manually. I roasted untold numbers of golems, brontos and darkspawn to accomplish that, and I have no intention of repeating that work for Awakenings just because the formulas don't work there.

With the idea behind this script it becomes much easier. Define some arrays for which spells, resistances, and damage amplifications to test, iterate over spellpower and the four levels of hexing. So you only have to spend a few minutes to invent the tests, let the computer run over night and then slurp the logs into database tables for analysis.

Abilities with damage rolls are trickier, because of the variation. That makes it necessary to let things run for quite a bit longer than the three seconds per test in this script. Like a minute at least, or several minutes. A complete series would be impossible, it would run for many hours or even days. What's possible is to run three tests (lower limit, middle, upper limit) for each pet, and use them to verify that the math works as intended.

However, I won't be able to do much for a couple of weeks after this weekend. In order to get Mike's tables ready it would be useful to review and refine the choice of data to be collected, and perhaps get an idea how the actual damage for the pets relates to the values that various functions spit out. I.e., we have to put something reasonable into the tables now, something that people can work with.

I'll refine my FoxPro scripts so that wiki tables can be generated directly at the press of a button.