Transcendence Wiki

Documenting the Universe.

User Tools

Site Tools


game:george_thoughts

Collection of unsorted information posted by George Moromisato on the inner workings of Transcendence

Records from the Ministry

George will sometimes post some highly detailed stuff onMinistry. See the ministry_notes page

On the next version

See the next_version page

On future versions

There is a chain of gates leading from Sol to St. Katharine's, but it is not included in v1 (it probably will be in future version).Forum Link

The Kuiper stargate leads to the Centauri system (which is not included in v1). Forum Link

On ingame scale

Planets and stars use an inverse exponential scale:

ObjectSize (Km)Image (pixels)Example
Ringed planet300,000512Saturn
Large gas giant150,000430Jupiter
Medium gas giant50,000330Neptune
Large rocky planet13,000240Earth
Medium rocky planet7,000200Mars
Small rocky planet5,000180Mercury
Large dwarf planet3,500170Earth’s Moon
Medium dwarf planet2,250150Pluto
Small dwarf planet1,000128Ceres
Giant asteroid500105Vesta
Large asteroid25090Hyperion
Medium asteroid10064Prometheus
Small asteroid5050Eros
Tiny asteroid1032Deimos

The formula is (roughly)

pixel size = ( (km size) ^ (0.25) ) * 22.25

Weapon of mass destruction

When hitting a large station, only WMD counts. WMD is some percentage of normal weapon damage, using this table:

* WMD0 = 0%
* WMD1 = 4%
* WMD2 = 10%
* WMD3 = 20%
* WMD4 = 34%
* WMD5 = 52%
* WMD6 = 74%
* WMD7 = 100%

For example, if 10 points of WMD3 damage hit a station, then the station only takes 20% of the damage or 2 points. Regardless of the above, any hit on a station causes at least 1 point of damage, so even weapons with no WMD do some damage (this may or may not change in the future).

Note that only stations with multiHull=“true” count as “large stations”

Ships generally take full damage (regardless of WMD) but for ships that have “non critical” armor segments, WMD comes into play. The chance that a hit to a non critical segment will destroy the ship increases for WMD weapons.

When hitting wrecks, etc. only WMD counts.

Time in Transcendence

Time in Transcendence is quantized. Bullet-time doubly so. Remember that the universe updates every 1/30th of a second (real-time) which means that time cannot be subdivided below that quantum.

Since motion is just (velocity X time), motion in Transcendence is also quantized–that is, objects will appear to “jump” from one point in space to another.

Bullets in Transcendence obey these quantum laws. Sometimes, bullets appear to “jump” across a solid barrier. This is not an illusion, but rather a property of the quantum laws of Transcendence.

Ships never seem to pass through walls because they are large and (relatively) slow macroscopic objects; but bullets are small and fast and subject to these quantum effects.

Why don't I just fix this (quantum) bug?

Yeah, I don't have a good answer for that :)

More on Time in Transcendence

From the forums

The current timescale is 1 tick = 2 seconds game time.

In a future version I will keep a separate game clock which may advance (and retreat?) independently of the tick count.

For example, when traveling through a gate, more time might elapse than indicated on the tick count.

I will add functions to convert from ticks to game time and vice versa.

If you write some functions today that do that conversion and don't make assumptions about the relationship between ticks and game time, then you should be able to migrate naturally once the built-in functions show up.

A few notes about the canonical universe:

1. In Human Space, dates are still measured by Earth standards: days, months, years.

2. At the start of the game in the Domina & Oracus campaign it is the year 2419.

3. Galactic dates (i.e., the calendar used by the Ancient Races) is obviously different. They measure “dates” using four values:

An 'orbis' is roughly one galactic revolution or ~211.65 million years. An 'orbis' is divided into 30 'trisem' A 'trisem' is divided into 200 'arc' An 'arc' is divided into 780 'generi'

One 'generi' is roughly 45 years.

Any date in galactic history can be expressed with those four number (to a 45 year resolution):

The current Galactic date is:

21.2.198.473

I haven't quite worked out time-scales below 45 year granularity.

Rendering spaceObjects for Transcendence

To position the camera and the lights, imagine that the object is at the origin: The camera is 6 units away and 12 units above the object. The light source is 3 units to the left (as seen from the camera), 5 units behind [Note: THE LIGHT COMES FROM THE TOP LEFT OF THE IMAGE, “BEHIND” translates to “behind the ship from the camera's perspective” NOT “behind the ship from the ship's perspective”] and 5 units above the object (Note: treat the light source as being at infinity).

Acceptable facings for ships: 1 2 3 4 5 6 8 9 10 12 15 18 20 24 30 36 40 45 60 72 90 120 180 360

Another Way Of Understainding 3d Scene Steup

This section not directly from George, this is an attempt to further clarify george's setup.

Assuming a right-handed coordinate system:

* Ships's center of rotation is at origin. When ship is facing straight 'up' in Trans, it will be pointing down the +Y axis, the left and right side are -X and +X respectivly, +Z is the top of the ship (what you mostly see in Trans).

* Camera is located along the line -1y:2z, and points toward the origin.

* Main light (an infinite/sun lamp) is located along the line -1x:2y:4z. (E.G -1,2,4; -2,4,8, etc), and points toward the origin. Probably want shadowcasting on this one.

For a left handed coordinate system, swap X and Y (+X is 'foreward', +Y is 'right')

DebugVideo Parameters

When running the game with /debugvideo, parameters are found printed in the debug.log

Paint = The time (in milliseconds) that it takes to paint all of the objects (ships, stations, effects, etc.) to an offscreen buffer. If this is high, then it means that there are a ton of objects on the screen OR that some of the effects (explosions, etc.) are taking a long time to paint.

Blt = The time (in milliseconds) that it takes to blt the offscreen buffer to the video card. If this is high, then it means that the videocard can't keep up. This might improve with /dx. This number is constant no matter how many objects are on screen or in the system (but might vary if other apps are accessing the videocard).

Update = The time (in milliseconds) that it takes to compute the game state. If this is high then it means that there are a lot of objects doing a lot of things. For example, this time increases with lots of Dwargs. This also increases if you are in autopilot (since we update multiple times per frame).

If you do the math, each frame has to process in a little more than 33 milliseconds to achieve 30 frames per second. If the sum of the values above exceeds 33 milliseconds, then you won't get smooth game-play.

On a Dell M1330, my times in Eridani look like this:

12/22/2008 11:31:36 Frames: 30 Paint: 2 Blt: 7 Update: 0 12/22/2008 11:31:46 Frames: 30 Paint: 3 Blt: 10 Update: 0 12/22/2008 11:31:56 Frames: 30 Paint: 3 Blt: 9 Update: 0

Deeper in the game, Paint and Update start to climb.

DockScreen Actions capitalization scheme

1. If it's a command, use sentence capitalization:

Buy items Replace armor Talk to station boss

2. If it's the name of a place, then use title capitalization:

Dock Services Ringer Lab Hall of Mirrors

3. If it's something the player says, put it in quotes and punctuate:

“Who is Domina?” “OK, you have a deal.” “He says you betrayed the Fleet.”

George's Missile Internal Calculations SourceCode (probably old)

I have no idea if this will help (or confuse) but here is the code that I use to calculate the intercept time for a missile of a given speed to hit a target moving at a certain (constant) speed.

Once you've calculated the intercept time, you can use simple trigonometry to calculate where to aim (since you will know where the ship will be when the missile hits).

Good luck!

  Metric CSystem::CalcInterceptTime
  (
  const Vector &vTarget,
  const Vector &vTargetVel,
  Metric rMissileSpeed,
  Metric *retrRange
  )
 
  // CalcInterceptTime
  //
  // Returns the time that it would take to intercept a target
  // at vTarget, moving with velocity vTargetVel, with
  // a missile of speed rMissileSpeed. Returns < 0.0 if the missile cannot
  // intercept the target.
  //
  // The formula for interception is:
  //
  //        A +- B sqrt(C)
  // t  =    --------------
  //                 D
  //
  //               Where:
  //                A = B * rVi
  //                B = 2 * rRange
  //                C = rMissileSpeed^2 - rVj^2
  //                D = 2 * (C - rVi^2)
  //
  //                rVi = the target speed along the position vector
  //                rVj = the target speed tangential to the position vector
  //                rRange = the (initial) distance to the target
  //                rMissileSpeed = the speed of the missile
 
  {
  Metric rRange = vTarget.Length();
  Vector vPosNormal = vTarget / rRange;
 
  if (retrRange)
   *retrRange = rRange;
 
  // Compute the orthogonals of the velocity along the position vector
 
  Metric rVi, rVj;
  vTargetVel.GenerateOrthogonals(vPosNormal, &rVi, &rVj);
 
  // Figure out the inside of the square root. If this value is negative
  // then we don't have an interception course.
 
  Metric C = rMissileSpeed * rMissileSpeed - rVj * rVj;
  if (C < 0.0)
   return -1.0;
 
  // Figure out the denominator. If this value is 0 then we don't
  // have an interception course.
 
  Metric D = 2 * (C - rVi * rVi);
  if (D ==== 0.0)
   return -1.0;
 
  // Compute A and B
 
  Metric B = 2 * rRange;
  Metric A = B * rVi;
 
  // Compute both roots
 
  Metric Z = B * sqrt(C);
  Metric R1 = (A + Z) / D;
  Metric R2 = (A - Z) / D;
 
  // If the first root is positive then return it
 
  if (R1 > 0.0)
   return R1;
 
  // Otherwise we return the second root, which may or may not
  // be positive
 
  return R2;
  }

A bit of history on (objCommunicate)

The truth is that (objCommunicate) is probably not the right way to implement autons.

A bit of history:

As you know, all ship AIs (regardless of controller) honor a sequential list of orders. Example:

Code:

… (shpOrder theShip 'attack theTarget) (shpOrder theShip 'gate) …

The snippet of code above orders a ship to first attack theTarget and after the target is destroyed, to gate out. Ideally, what I should have done, is to use that mechanism for autons. For example, in the <Communications> code, when you order an auton to attack, the code should just call something like:

Code:

(shpOrder theAuton 'attack theTarget)

Unfortunately, one of the orders that a ship obeys is the escort order:

Code:

(shpOrder theAuton 'escort gPlayerShip)

So when an auton is following you around, it is in the middle of the escort order. If you give it another order, it will not obey the order until the escort order has completed (which it never will).

One possible solution is to cancel the escort order before giving the attack order. Unfortunately, the problem there is that the escort order is the only way that we know that the auton is following the player. If we cancel the escort order, we won't know that the auton should follow the player (for example, if the player enters a stargate).

Anyway, to overcome these problems, I created a special controller to implement the auton. The auton is always in running the escort order. But it can change its behavior based on messages from the player. Thus I added (objCommunicate) to tell the auton that it should attack.

I think this was a mistake. Instead, I should have added a separate variable on a ship that indicates that it is escorting another ship. Then we could use the standard order machinery to command autons and wingmen.

FTL Travel in Transcendence

In this time-period (2400's) humans do NOT have the technology for FTL anything. The stargates (and alien jumpdrive) are all they've got (and they don't know how those work). All interstellar communications is via messenger ships passing through stargates.

The alien races of the galaxy (e.g., Iocrym) DO have FTL technology, but even that is not instantaneous (e.g., it takes non-zero time to pass through a stargate–time proportional to distance). Comms is the same way–FLT but not instantaneous.

Domina and Oracus do have instantaneous communication, but only when they communicate with a sentient being.

In-game score calculation

int CShipClass::ComputeScore (const CDeviceDescList &Devices, 
                       int iArmorLevel, 
                       int iPrimaryWeapon, 
                       int iSpeed, 
                       int iThrust, 
                       int iManeuver, 
                       bool bPrimaryIsLauncher) 
 
//   ComputeScore 
// 
//   Compute the score of the class based on equipment 
 
   { 
   int i; 
   int iSpecial = 0; 
   int iExceptional = 0; 
   int iDrawback = 0; 
   int iStdLevel = iArmorLevel; 
   int iWeaponLevel = (iPrimaryWeapon ==== -1 ? 0 : ComputeDeviceLevel(Devices.GetDeviceDesc(iPrimaryWeapon))); 
 
   //   If our weapon is better than our armor then adjust the level 
   //   depending on the difference. 
 
   if (iWeaponLevel > iArmorLevel) 
      { 
      switch (iWeaponLevel - iArmorLevel) 
         { 
         case 1: 
            iStdLevel = iWeaponLevel; 
            iDrawback++; 
            break; 
 
         case 3: 
            iStdLevel = iWeaponLevel - 2; 
            iSpecial += 2; 
            break; 
 
         default: 
            iStdLevel = (iWeaponLevel + iArmorLevel) / 2; 
         } 
      } 
 
   //   If our best weapon is 2 or more levels below our standard 
   //   level then take drawbacks exponentially. 
 
   if (iStdLevel > iWeaponLevel + 1) 
      iDrawback += min(16, (1 << (iStdLevel - (iWeaponLevel + 2)))); 
   else if (iStdLevel > iWeaponLevel) 
      iDrawback++; 
 
   //   If all movement stats are high then this counts as an 
   //   exceptional ability 
 
   if (iSpeed ==== enumHigh && iThrust ==== enumHigh && iManeuver ==== enumHigh) 
      iExceptional++; 
 
   //   Otherwise, treat them as special abilities or drawbacks 
 
   else 
      { 
      if (iSpeed ==== enumLow) 
         iDrawback++; 
      else if (iSpeed ==== enumHigh) 
         iSpecial++; 
 
      if (iThrust ==== enumLow) 
         iDrawback++; 
      else if (iThrust ==== enumHigh) 
         iSpecial++; 
 
      if (iManeuver ==== enumLow) 
         iDrawback++; 
      else if (iManeuver ==== enumHigh) 
         iSpecial++; 
      } 
 
   //   1 armor segment is a drawback 
 
   int iArmorSections = GetHullSectionCount(); 
   if (iArmorSections <= 1) 
      iDrawback++; 
 
   //   2-3 armor segments is normal 
 
   else if (iArmorSections < 4) 
      ; 
 
   //   4 or more armor segments is special 
 
   else if (iArmorSections < 8 ) 
      iSpecial++; 
   else if (iArmorSections < 16) 
      iSpecial += 2; 
   else if (iArmorSections < 32) 
      iSpecial += 3; 
   else if (iArmorSections < 64) 
      iSpecial += 4; 
   else 
      iSpecial += 5; 
 
   //   Checkout all the devices 
 
   bool bDirectionalBonus = false; 
   bool bGoodSecondary = false; 
   int iDirectionalBonus = 0; 
   for (i = 0; i < Devices.GetCount(); i++) 
      { 
      const SDeviceDesc &Dev = Devices.GetDeviceDesc(i); 
      CDeviceClass *pDevice = Dev.Item.GetType()->GetDeviceClass(); 
      int iDeviceLevel = ComputeDeviceLevel(Dev); 
 
      //   Specific devices 
 
      switch (pDevice->GetCategory()) 
         { 
         case itemcatWeapon: 
         case itemcatLauncher: 
            { 
            int iWeaponAdj = (iDeviceLevel - iStdLevel); 
 
            //   If this is a secondary weapon, then add it to the score 
 
            if (i != iPrimaryWeapon) 
               { 
               //   Calculate any potential bonus based on the weapon level 
               //   compared to the base level 
 
               iSpecial += max(iWeaponAdj + 3, 0); 
               } 
 
            //   Compute fire arc 
 
            int iFireArc = (Dev.bOmnidirectional ? 360 : AngleRange(Dev.iMinFireArc, Dev.iMaxFireArc)); 
 
            //   Adjust for turret-mount 
 
            iDirectionalBonus += (max(iWeaponAdj + 3, 0) * iFireArc); 
            break; 
            } 
 
         case itemcatReactor: 
            //   Reactors don't count as improvements 
            break; 
 
         default: 
            { 
            //   Other devices are special abilities depending on level 
 
            if (iDeviceLevel > iStdLevel+1) 
               iExceptional++; 
            else if (iDeviceLevel > iStdLevel) 
               iSpecial += 4; 
            else if (iDeviceLevel >= iStdLevel-1) 
               iSpecial += 2; 
            else 
               iSpecial++; 
            } 
         } 
      } 
 
   //   If we have no weapons then we have some drawbacks 
 
   if (iPrimaryWeapon ==== -1) 
      iDrawback += 3; 
 
   //   Add bonus if weapon is omnidirectional 
 
   iSpecial += (int)((iDirectionalBonus / 270.0) + 0.5); 
 
   //   Checkout AI settings 
 
   const SAISettings &AI = GetAISettings(); 
   int iFireAccuracyScore, iFireRateScore; 
 
   if (AI.iFireAccuracy > 97) 
      iFireAccuracyScore = 5; 
   else if (AI.iFireAccuracy >= 93) 
      iFireAccuracyScore = 4; 
   else if (AI.iFireAccuracy >= 90) 
      iFireAccuracyScore = 3; 
   else if (AI.iFireAccuracy < 75) 
      iFireAccuracyScore = 1; 
   else 
      iFireAccuracyScore = 2; 
 
   if (AI.iFireRateAdj <= 10) 
      iFireRateScore = 5; 
   else if (AI.iFireRateAdj <= 20) 
      iFireRateScore = 4; 
   else if (AI.iFireRateAdj <= 30) 
      iFireRateScore = 3; 
   else if (AI.iFireRateAdj >= 60) 
      iFireRateScore = 1; 
   else 
      iFireRateScore = 2; 
 
   int iFireControlScore = iFireRateScore * iFireAccuracyScore; 
   if (iFireControlScore >= 20) 
      iExceptional++; 
   else if (iFireControlScore > 6) 
      iSpecial += ((iFireControlScore - 5) / 2); 
   else if (iFireControlScore < 2) 
      iDrawback += 4; 
   else if (iFireControlScore < 4) 
      iDrawback += 2; 
 
   //   Compute final score 
 
   ScoreDesc *pBase = &g_XP[iStdLevel-1]; 
   int iScore = pBase->iBaseXP 
         + iSpecial * pBase->iSpecialXP 
         + iExceptional * pBase->iExceptionalXP 
         + iDrawback * pBase->iDrawbackXP; 
 
   return iScore; 
   } 

Score table

static ScoreDesc g_XP[] = 

   { 
      //   Base Score 
      //         Special Ability 
      //               Exceptional Ability 
      //                     Drawback 
      //                           Level Score 

      {   20,      5,      20,      0,      50 },         //   I 
      {   50,      10,      50,      -5,      100 },         //   II 
      {   115,   15,      100,   -10,   200 },         //   III 
      {   200,   20,      170,   -20,   350 },         //   IV 
      {   340,   30,      260,   -35,   600 },         //   V 
      {   500,   45,      370,   -50,   900 },         //   VI 
      {   750,   60,      500,   -65,   1400 },         //   VII 
      {   1050,   80,      650,   -85,   1900 },         //   VIII 
      {   1450,   100,   820,   -105,   2600 },         //   IX 
      {   1900,   125,   1010,   -130,   3250 },         //   X 
      {   2400,   150,   1220,   -155,   4200 },         //   XI 
      {   3000,   180,   1450,   -185,   5500 },         //   XII 
      {   3600,   210,   1700,   -215,   6750 },         //   XIII 
      {   4250,   245,   1970,   -250,   8250 },         //   XIV 
      {   5000,   280,   2260,   -285,   10000 },      //   XV 
      {   6000,   320,   2570,   -325,   11500 },      //   XVI 
      {   7000,   360,   2900,   -365,   13250 },      //   XVII 
      {   8000,   405,   3250,   -410,   15000 },      //   XVIII 
      {   9000,   450,   3620,   -455,   16750 },      //   XIX 
      {   10000,   500,   4010,   -505,   18500 },      //   XX 
      {   11000,   550,   4420,   -555,   20500 },      //   XXI 
      {   12000,   605,   4850,   -610,   22500 },      //   XXII 
      {   13000,   660,   5300,   -665,   25000 },      //   XXIII 
      {   14000,   720,   5770,   -725,   26500 },      //   XXIV 
      {   15000,   780,   6260,   -785,   30000 },      //   XXV 
   }; 

Keybindings for Settings.xml

If you look at Settings.xml you can see that there is a mapping table between keys and commands.

You can map any single key to any command (you can map multiple keys to the same command, but not a single key to multiple commands).

Currently, the full list of supported commands is the in the Settings.xml file.

To specify a key, you can always use the hexadecimal representation for the virtual key code passed in wParam of WM_KEYDOWN (http://msdn.microsoft.com/en-us/library/ms646280(VS.85).aspx)

Or you can use one of the following key names:

"Backspace" 
"Tab" 
"Clear" 
"Return" 
"Shift" 
"Control" 
"Menu" 
"Pause" 
"Capital" 
"Kana" 
"Junja" 
"Final" 
"Kanji" 
"Escape" 
"Convert" 
"NonConvert" 
"Accept" 
"ModeChange" 
"Space" 
"PageUp" 
"PageDown" 
"End" 
"Home" 
"Left" 
"Up" 
"Right" 
"Down" 
"Select" 
"Print" 
"Execute" 
"Snapshot" 
"Insert" 
"Delete" 
"Help" 
"0" 
"1" 
"2" 
"3" 
"4" 
"5" 
"6" 
"7" 
"8" 
"9" 
"A" 
"B" 
"C" 
"D" 
"E" 
"F" 
"G" 
"H" 
"I" 
"J" 
"K" 
"L" 
"M" 
"N" 
"O" 
"P" 
"Q" 
"R" 
"S" 
"T" 
"U" 
"V" 
"W" 
"X" 
"Y" 
"Z" 
"LWindows" 
"RWindows" 
"Apps" 
"Sleep" 
"Numpad0" 
"Numpad1" 
"Numpad2" 
"Numpad3" 
"Numpad4" 
"Numpad5" 
"Numpad6" 
"Numpad7" 
"Numpad8" 
"Numpad9" 
"NumpadStar" 
"NumpadPlus" 
"NumpadSeparator" 
"NumpadMinus" 
"NumpadPeriod" 
"NumpadSlash" 
"F1" 
"F2" 
"F3" 
"F4" 
"F5" 
"F6" 
"F7" 
"F8" 
"F9" 
"F10" 
"F11" 
"F12" 
"F13" 
"F14" 
"F15" 
"F16" 
"F17" 
"F18" 
"F19" 
"F20" 
"F21" 
"F22" 
"F23" 
"F24" 
"NumLock" 
"ScrollLock" 
"FJ_Jisho" 
"FJ_Masshou" 
"FJ_Touroku" 
"FJ_Loya" 
"FJ_Roya" 
"BrowserBack" 
"BrowserForward" 
"BrowserRefresh" 
"BrowserStop" 
"BrowserSearch" 
"BrowserFavorites" 
"BrowserHome" 
"VolumeMute" 
"VolumeDown" 
"VolumeUp" 
"MediaNext" 
"MediaPrev" 
"MediaStop" 
"MediaPlay" 
"LaunchMail" 
"LaunchMediaSelect" 
"LaunchApp1" 
"LaunchApp2" 
"SemiColon" 
"Equal" 
"Comma" 
"Minus" 
"Period" 
"Slash" 

Note: Many of the above key names refer to non-English keyboards, so they may not work for you. You will have to do some experimenting.

Topology

Here's a quick update on my work on system topologies:

As you know, I've been working on Part II, which is going to require a more sophisticated way of generating a system topology. I want to create something more random, instead of the linear topology of Part I.

But in order to understand the changes that are coming, it will help to know exactly how system topologies work today. Here is a primer:

At the beginning of the game, we generate the entire system topology as follows:

1. Gather together all <SystemTopology> elements. Transcendence.xml has one (the main topology). Huari.xml has another (for Huaramarca). And any extension can add their own. [There is currently no way to override a <SystemTopology> element from an extension.]

2. Each <SystemTopology> element has one or more “root nodes” (a node marked with the rootNode=“true” attribute.

3. We start with the first root node that we find and create it. [Note: at this stage in the game all we are doing is creating topology nodes. We do not create the actual star system until the player actually enters the system.]

4. Next we look at the list of stargates leading out of the node. [The stargate list can be a random table, in which case we pick random stargates.]

5. For each stargate we look at what node it leads to. If it leads to a node that we've already created, then we connect the gate and continue. But if it leads to a new node, then we recurse: we create the new node and then look at its stargate table (and so on).

6. Note that the stargate element sometimes points to a node called “[Prev]”. This just means that the stargate connects back to whatever node we were processing to reach this node. [This is needed for stargates to be two-way–and in fact, in future versions this will not be necessary: stargates will be two-way by default.]

7. After all root nodes are processed, we call <OnGlobalTopologyCreated> for all types. At this point (and only at this point) code can manipulate the topology (generally by adding new stargates, as in the case of Huaramarca).

From the above algorithm you can see a few implications:

i. Only root nodes are guaranteed to be created. If a node doesn't (ultimately) connect to some root node, it will not be created. [For example, you notice that there are no nodes that lead to Huaramarca–we randomly connect to Huaramarca later in code (using sysAddStargateTopology). But if Huaramarca were not a root node, it would never have been created in the first place, and sysAddStargateTopology would fail.]

ii. The above also means that nodes that are randomly skipped are never created. You cannot reach them in the game, even through code. For example, node C4A is sometimes (50%) skipped.

iii. The topology is more or less set after <OnGlobalTopologyCreated>. There is no way to change the topology after that (nor am I planning any way–we can always enable/disable stargates later).

Given all that, it is now a little easier to talk about the changes and enhancements that are coming:

Galactic Map While no UI is implemented yet, I'm preparing for a Galactic Map that shows the position of every star system on a map. Thus every node will now have an X,Y coordinate. As you are designing your topologies, make sure that you take that into account.

Reusable Fragments Suppose you want your topology to look like this:

A1--B1--B2 
|    |   | 
|   B3--B4 
| 
A2--B5--B6 
     |   | 
    B7--B8 

Notice that the B1-B4 cluster is the same as the B5-B8 cluster. It would be a shame if you had to specify the same topology twice.

The <Fragment> element lets you define a named sub-topology that is reusable. When a stargate leads to a fragment, we generate all the nodes for in the fragment (even if the fragment has been used before).

Normally, nodes specify an absolute x,y position for where they show up on the map. But for fragments, the x,y position is relative to whatever node leads to them.

With fragments you can create a modular topology. For example, suppose you had two fragments that looked different:

--B1--B2 
   |   | 
  B3--B4 

and 

--C1--C2--C3--C4 

Now you can have the nodes A1 and A2 randomly lead to either the B1 fragment or the C1 fragment. Of course, as you increase the number of kinds of fragments (or have fragments lead to fragments) you can increase the randomness of the network.

Two-Pass System Assignment Today, each of the nodes specifies the system type that it will have. Each node can have a random table of system types, which is how sometimes a system is a nebula system while other times it is an asteroid system.

But the system selection happens at the time that the node is created (before the whole topology is created). This makes it difficult to place a special system at a specific part of the topology.

Future versions will add a second pass which allows you to alter the system types in specific nodes.

A topology can have one or more <TopologyProcessor> elements, which will let you select a set of nodes (by some kind of criteria) and assign systems types to them.

For example, you could use a <TopologyProcessor> to pick a random node that has at least three stargates and make it a special system type. Or you could pick 20 random nodes in the topology and make them nebula systems (or binary systems, or whatever).

You can also use the <TopologyProcessor> elements to apply attributes to specific nodes (attributes that you can later query when the player enters a system).

Part II Galaxy My plans for Part II are to use the above features to create semi-random topology that spreads across the whole galaxy. [Of which the current Human Space topology will be a small part.]

Using the two phases above, creating the galaxy will involve:

1. Generating a stargate topology. The topology will generally follow around two major spiral arms, but each arm will have various random fragments sticking out. [This is all TBD–until I playtest it I won't know how well it will work.]

2. Assigning system types randomly to each node. With the <TopologyProcessor> elements I can carve out the galaxy into random regions. Each region will have a particular environment (nebula, metal-rich, water-poor, etc.) Different environments will have different system types, and the encounters, sovereigns, etc. can then be adjusted based on the particular environment.

Hope this primer helps to see where we're going, both in terms of Part II and the modding features that are coming.

Reusable fragments and the two-pass system assignment should be done in time for 1.04.

System Generation

From this forum post: http://neurohack.com/transcendence/forums/viewtopic.php?p=36513#p36513

There are two kinds of tags (XML elements) in a system definition. There are “creation” tags and there are “location” tags. A creation tag is something that creates an object in the system. For example:

   <Station type="..."/>

This creates a station of the given type. But where does it create it? It creates it wherever the parent “location” tag says. A location tag is an element that specifies a particular location in the system.

The outermost tag is a location tag:

<SystemGroup>
   <!-- anything inside the outermost tag is placed at the exact
      center of the system: coordinates 0,0 -->
 
   <Station type="..."/>
</SystemGroup>

In the code above, since the Station tag is inside the SystemGroup tag, the station gets created at the SystemGroup's location, which is 0,0.

In general, the purpose of a location tag is to offset from its parent's location to some new location.

The Orbitals tag is a location tag that offsets from some center to a point on an orbit:

<SystemGroup>
   <Orbitals distance="10" angle="90">
      <Station type="..."/>
   </Orbitals>
</SystemGroup>

The “distance” attribute is the radius of the orbit (unless otherwise specified, all distances are expressed in light-seconds). The “angle” is (no surprise) the angle around the orbit (as in standard trig, angle=0 is along the positive X axis). In this example, an angle of 90 is straight up.

Remember that the Orbitals tag is relative to its parent. It just so happens that its parent is the SystemGroup, so the parent location is 0,0. But if the Orbitals were inside a different location, it would be relative to that.

For example:

<SystemGroup>
   <Orbitals distance="10" angle="90">
      <Orbitals distance="5" angle="0">
         <Station type="..."/>
      </Orbitals>
   </Orbitals>
</SystemGroup>

Notice that there are two orbital definitions. The outermost, offsets from its parent (0,0) to a point 10 light-seconds in the 90 degrees direction (straight up). Then the inner Orbitals offset from its parent 5 light-seconds in the 0 degrees direction (to the right). The station will therefore be placed 10 light-seconds up and 5 light-seconds to the right from 0,0.

Orbitals has many additional attributes to define its behavior, but in the end, it's all about positioning things on an orbit.

The “count” attribute creates multiple of whatever orbitals element contains. If you've specified an absolute location (as above) it will just create multiple stations in the same spot. But you can specify a random location. By adding the “random” keyword as an angle, you create a station at a random angle on the orbit. When multiple stations are created (with the count attribute) each station will be placed at a different (random) angle.

There are other kinds of tags that offset the location:

Siblings: This tag moves along the orbit of its parent. You can use the arcInc and radiusInc attributes to move along the orbit of the parent. Or you can pick a random angle on the parent's orbit.

Trojan and AntiTrojan and just special cases of the Siblings tag. They always move 60 degrees forward or 60 degrees back on their parent's orbit. In practice they are identical to Siblings with angleInc=“60” and angleInc=“-60” [BTW: arcInc moves along the orbit in light-seconds. angleInc moves in degrees.]

Offset: This tag offsets in cartessian (x,y) instead of orbital coordinates.

There are two other special tags that require explanation. First is Lookup.

The Lookup tag is just a way to share portions of the system definition. It is like a macro.

<SystemPartTable unid="...">
   <MyCoolStuff>
      <Orbitals distance="10" angle="90">
         <Station type="..."/>
      </Orbitals>
   </MyCoolStuff>
</SystemPartTable>
 
<SystemType unid="...">
   <SystemGroup>
      <Lookup table="MyCoolStuff"/>
   </SystemGroup>
</SystemType>

When the above is processed, the <Lookup…> element will be replaced with the contents defined in the SystemPartTable; the result is:

<SystemType unid="...">
   <SystemGroup>
      <Orbitals distance="10" angle="90">
         <Station type="..."/>
      </Orbitals>
   </SystemGroup>
</SystemType>

The other interesting tag is Label. A label is like a placeholder location that later gets filled in with something.

When the system places planets, it also positions labels around the planets. For example:

<Group>
   <Station type="&stIcePlanet;" showOrbit="true"/>
 
   <Orbitals distance="2d6+10" angle="random">
      <Label attributes="planet,planetary,frost"/>
   </Orbitals>
</Group>

The above creates an ice planet with a label in orbit around it at 2d6+10 light-seconds. What's in the label? Nothing yet. Later, there are other tags that “fill” labels with stations:

<RandomLocation locationCriteria="+planet">
   <Station type="..."/>
</RandomLocation>

The RandomLocation tag loops over all free labels and picks one randomly. The locationCriteria attribute is a way of preferring particular labels (+planet means prefer labels with the “planet” attribute).

Once we pick a label, we use that as the location for anything inside the RandomLocation tag. In this case, we create a station at that location.

But what if you want to create a random station? Then you can use:

<RandomLocation locationCriteria="+planet">
   <RandomStation stationCriteria="*friendly"/>
</RandomLocation>

The RandomStation tag chooses a random station that matches the stationCriteria and the attributes of the location.

The full picture looks like this:

1. When planets are generated a bunch of labels are created. Each label has a set of attributes describing something about the location of the label (e.g., a label will have the “planet” attribute if it is around a planet.)

2. The RandomLocation tag picks a random label. It uses the locationCriteria specified in the RandomLocation tag to pick a label. In the example above, we prefer labels that have the “planet” attribute.

3. Let's say that we pick a random label with the following attributes: “planet, planetary, frost”. These attributes now get associated (temporarily) with the label's location.

4. Now that we've picked a label and location, we process the RandomStation tag. We loop over every single StationType and assign each station a probability of being at this location. We compute the probability as follows:

a. We compare the stationCriteria against the attributes of the station. If the station does not match, we exclude it. Otherwise, we adjust the probability accordingly. In the example above, we make sure that the station has the “friendly” attribute.

b. Next we check the levelFrequency attribute on the StationType and compare against the level of the system. (Any station that does not have a levelFrequency is considered not random and is excluded).

c. Finally we check the locationCriteria attribute on the StationType and compare it against the attributes at the label's location (most of the attributes come from the label itself, but we also take any attributes in the system into account).

5. Once we've computed the probability of all stations, we roll a random number and choose a station.

There are other tags that work on similar principles. The FillLocations tag is an iterated version of the above. It loops over all empty labels and proceeds to pick random stations to fill them.

On Version Strings

This is a quick note to clarify how I'm doing version numbers for extensions.

There are really only two valid extension version number:

“1.0”: This version number indicates that your extension works with 1.0 or 1.01.

“1.1”: This version number indicates that your extension requires some feature that will be first released after 1.01 and up to 1.1.

My basic philosophy is that all releases that are not on the main site are betas, meaning that I can't guarantee that the APIs will remain stable for the final release.

The next version that will show up on the main site is 1.1. By then, all the new features that have appeared in 1.02-1.05+ will be official and I will support backwards compatibility on those in future version (1.2 and beyond).

Order Codes

A list of order codes for those pesky vague debuglog statements.

1 = “guard”
2 = “dock”
3 = “attack”
4 = “wait”
5 = “gate”
6 = “gateOnThreat”
7 = “gateOnStationDestroyed”
8 = “patrol”
9 = “escort”
10 = “scavenge”
11 = “followPlayerThroughGate”
12 = “attackNearestEnemy”
13 = “tradeRoute”
14 = “wander”
15 = “loot”
16 = “hold”
17 = “mine”
18 = “waitForPlayer”
19 = “attackPlayerOnReturn”
20 = “follow”
21 = “navPath”
22 = “goto”
23 = “waitForTarget”
24 = “waitForEnemy”
25 = “bombard”
26 = “approach”
27 = “aim”
28 = “orbit”
29 = “holdCourse”
30 = “turnTo”
31 = “attackHold”

On Reactors

This information was taken from this post.
1. Each fuel item has a certain number of “fuel units”. For example, a Helium fuel rod has 2500 fuel units. [Look at the data= attribute on an item type to determine the number of fuel units.]

2. A reactor with 100% efficiency consumes 1 fuel unit to generate 1.5 MW of power for 1 tick. Example: A weapon is firing and drawing 15 MW. Every tick it will consume 10 fuel units (15 / 1.5 = 10). Enhanced reactors have greater than 100% efficiency and can generate more power on 1 fuel unit. Damaged reactors are the opposite.

3. A weapon rated at 10 MW draws 10 MW only while firing. If the weapon is not firing, it draws 1/10 that amount (unless otherwise specified in idlePowerUse).

Hope that helps!

As an aside, this whole design has helped me learn about power a little more. Now I know the difference between a space heater that draws 1 kilowatt vs. a bill in the mail that shows I've used 10 kilowatt-hours. A kilowatt is a measure of the rate at which power flows; a kilowatt-hour is a measure of the quantity of power that I've consumed.

In the same way, power draw of devices is measured in watts (megawatts) while consumption, which is expressed in fuel units, can be converted to megawatt-hours with a simple formula:

megawatt-hours = (1.5 * fuel-units) / (ticks-per-hour)

Since there are 1800 ticks per hour (in game time), and since a single Helium fuel rod has 2500 fuel units, we can convert as follows:

megawatt-hours = 1.5 * 2500 / 1800 = ~2.08

That is, a Helium fuel rod has 2.08 megawatt-hours of power.

And at the risk of going even further astray, note that these numbers wildly undervalue to power of nuclear energy. For example, a barrel of oil masses 139 kg and has about 1.6 megawatt-hours of power–about the same energy density as a Helium fuel rod. In contrast, 100 kg of uranium used in a fission reactor yields about 2.2 million megawatt-hours of power.

Guess I should have done these calculations when I first came up with the values.

To be fair, a Helium fuel rod contains all of the shielding and structure designed to contain the fuel–the fuel itself is probably much less than 100 kgs. To reach the same energy density as uranium, the actual fuel in a Helium fuel rod should mass no more than 0.1 grams.

p.s.: Let me know if I screwed-up the arithmetic.

Laser Color

This information was taken from this post.
Yes, it is intentional, but with some limitations.

In general I wanted each damage type to have a unique look:

Lasers: red, green, or purple (in ascending order of strength)
Kinetic: gray
Particle: green, with particulate texture
Blast: flame
Ion: cyan
Thermo: flame/orange
Positron: yellow, with particulate texture
Plasma: yellow
Antimatter: yellow
Nano: various (gray/green/etc) but with complex texture
Graviton: purple/violet
Singularity: black/white/violet
Dark Acid: teal
Dark Steel: gray-green
Dark Lightning: blue
Dark Fire: red

For lasers, we should probably avoid yellow and blue (to avoid confusion with positron/antimatter and ion). If you want to create new laser styles I recommend mixing in more white (to make the beam whiter). For example, I imagine a gamma-ray laser could be white with fringes of violet/purple.

Planets for API version 14

Taken from here
Spawning uses system macros and labels. For example, if you look at StarSystems.xml you'll see a definition for <AsteroidBeltSystem>. This is a macro used to create an asteroid belt system. The macro defines (randomly) the position of several planets. Under <Orbitals …> you can see a reference to a “DesertPrimary” macro.

If you look at the definition of the DesertPrimary macro (in Worlds.xml) you'll see that it places two things: a planet (stDesertTerrestrialSizeH) and a label.

A label is a point in space that may hold a station (friendly or enemy). This particular label has certain attributes, including the “planet” attribute, which means that the label is around a planet. When the system is created we may randomly put a station here, preferring stations that want to be around a planet.

In previous versions, the planet object itself (stDesertTerrestrial…) would ALSO have a “planet” attribute. This was useful in case you later wanted to enumerate all the planets in a system. But it turned out that some objects were being used both as planets and moons, so I couldn't put the attribute on the object itself.

Instead, I use a special attribute +isPlanet:true; which matches only if the object is a true planet (orbiting a star).

If you're interested in enumerating objects in the system, then here are a few tips:

1. You can use the special attribute +scale:world; to enumerate all planets, asteroids, and moons in the star system. +scale:star; matches all stars.

2. All worlds (defined in core) have an explicit size (in kilometers). They are split up into size classes:

                        Asteroid         < 1,000 km
                  sizeA      Tiny         ~ 10 km
                  sizeB      Small         ~ 50 km
                  sizeC      Medium         ~ 100 km
                  sizeD      Large         ~ 500 km

                        Planetoid         ~ 2,500 km
                  sizeE      Small         ~ 1,000 km
                  sizeF      Medium         ~ 2,000   km
                  sizeG      Large         ~ 4,000 km
      
                        Terrestrial         ~ 10,000 km
                  sizeH      Small         ~ 5,000 km
                  sizeI      Medium         ~ 10,000 km
                  sizeJ      Large         ~ 20,000 km

                        Gas Giant         ~ 100,000 km
                  sizeK      Small         ~ 50,000 km
                  sizeL      Medium         ~ 100,000 km
                  sizeM      Large         ~ 200,000 km

3. You can match a specific size class with +sizeClass:H; (for example) to match all sizeH worlds.

4. You can use normal attributes to match sets of classes:

+asteroid; Matches all size A, B, C, D worlds +planetoid; Matches all size E, F, G worlds +terrestrial; Matches all size H, I, J worlds +gasGiant; Matches all size K, L, M worlds.

Hope that helps!

On Items

Taken from here
The most important thing to remember about items (armor, devices, etc.) is that you can only manipulate them by value not by reference. What does that means? Here is a quick tutorial:

When you call a function like:

(objAddItem gPlayerShip (itmcreate 0xD518FF28 4) 4)

you add a single item record to the player ship. The player ship's inventory will look like this:

Player Ship Inventory: [UNID: 0xD518FF28 count:4 flags:none]

First, notice that even though you've created 4 armor segments there is only a single record. The record itself has a count of 4, which is how we keep track. This is done for efficiency. Otherwise, if you pick up 1000 rounds of ammo we would have 1000 records, which is too expensive.

Next, notice that there is no ID field! The fact that there is no ID field means that you can't refer to the record by ID. So how do you refer to the armor segments? How do you, for example, install them?

You need to install them by describing exactly the items that you want to install. You have to refer to them by value. Effectively, you need to do something like this:

(shpInstallArmor gPlayerShip “the armor that looks like: [UNID: 0xD518FF28 count:4 flags:none]”)

Of course, the above won't work, but the function itmCreate essentially is a way of creating records of that form. So the following works:

(shpInstallArmor gPlayerShip (itmCreate 0xD518FF28 1) 0)

Indeed, the output of itmCreate is a record structure. Under the covers:

(itmCreate 0xD518FF28 1) → [UNID: 0xD518FF28 count:1 flags:none]

After you do this, you will end up with inventory that looks like this:

Player Ship Inventory: [UNID: 0xD518FF28 count:3 flags:none] [UNID: 0xD518FF28 count:1 flags:installed installedAt:0]

Notice that the act of installing armor changes the record! Because the record has changed, you can't refer to it by the original name. For example, suppose I wanted to enhance the armor I just installed. I might naively try this:

(shpEnhanceItem gPlayerShip (itmCreate 0xD518FF28 1))

Which armor segment would get enhanced? Because the installed armor has changed (because it is installed), I can't use the same code to refer to it anymore. I need to do something like:

(shpEnhanceItem gPlayerShip “the armor that looks like: [UNID: 0xD518FF28 count:1 flags:installed installedAt:0]”)

Unfortunately, itmCreate can't output that kind of record (it doesn't know how to set the installed flag), so you can't use it. Instead, you need to find the item on the player ship and use that. objGetItems returns a list of item records that match a specific criteria.

(objGetItems gPlayerShip “unid:0xD518FF28;”) → [UNID: 0xD518FF28 count:3 flags:none] [UNID: 0xD518FF28 count:1 flags:installed installedAt:0]

Since both records match the UNID, objGetItems returns both the installed and uninstalled. If I only want the installed item, I need the 'I' flag:

(objGetItems gPlayerShip “unid:0xD518FF28; I”) → [UNID: 0xD518FF28 count:1 flags:installed installedAt:0]

Of course, the result is still a list, so I would need to use the '@' syntax to get the first element of the list:

(shpEnhanceItem gPlayerShip (@ (objGetItems gPlayerShip “unid:0xD518FF28; I”) 0))

objGetItems is a way to find the item record that you want to refer to. So with that (long) tutorial out of the way, we can talk about your code.

In your first example, there is a small bug in the criteria for objGetItems. Your code should be something like:

(objadditem gplayership (itmcreate 0xD518FF28 4) 4)
(shpinstallarmor gplayership (@ (objgetitems gplayership "unid:0xD518FF28; U") 0) 0)
(shpinstallarmor gplayership (@ (objgetitems gplayership "unid:0xD518FF28; U") 0) 1)
(shpinstallarmor gplayership (@ (objgetitems gplayership "unid:0xD518FF28; U") 0) 2)
(shpinstallarmor gplayership (@ (objgetitems gplayership "unid:0xD518FF28; U") 0) 3)

Notice that the “unid:…” term ends in a semi-colon. And the “U” term doesn't need a “^”.

But your second example works just as well because you've described the item perfectly. The second method would get into problems if the item were enhanced, damaged, installed, or had data associated with it. In those cases, you need to use the objGetItems syntax.

Also, many functions return the item record after modification. For example, objSetItemData returns the newly modified item record and you can use the result to refer to the item in subsequent calls.

Shield and Level Adjustment curves

game/george_thoughts.txt · Last modified: 2016/01/06 20:43 by xephyr