Skip to content
Allofich edited this page Mar 12, 2024 · 27 revisions

General notes

The sprite size formula:

# mul2 == mul3 == 160
# scale2 == 200
# var3 == ???

baseH <- ((sprite.H * obj.Scale)/256*scale2)/256
baseW <- (sprite.W * obj.Scale)/256

pX,pY <- project(obj.X-camera.X,obj.Y-camera.Y)
if pY<=20 then return
pX <- pX + var3

WW <- (baseW*mul2)/pY
HH <- (baseH*mul3)/pY

XX <- (pX*mul2)/pY + centerX - WW/2
YY <- (((sprite.Z+camera.Z)-baseH)*mul3)/pY + centerY

Monsters

The monster races are 1-based. The lists below are indexed by race - 1. There are 24 different monster types.

  • Names: szlist @3A8EE
  • Level: byte array @42096 (zero-based, as with the player)
  • Hit points: 48-entry WORD array @420DE (min, max format)
  • Base experience: DWORD array @4213E.
  • Experience multiplier: byte array @4219E
  • Sound: byte array @4201E. An index into the VOC szlist @437CD
  • Damage: 48-entry byte array @421B6 (min, max format)
  • Magical effects: WORD array @420AE (these go into the NPC's ActiveEffects)
  • Scale: WORD array @42066 (in 1/256s, 0 = 100%)
  • Y offset: signed byte array @42036
  • Has no corpse: byte array @4204E
  • Blood: byte array @4762F (index into animation list @42EFC)
  • Disease chance: signed byte array @42212. Negative values have special meaning.

Spell assignment

For the final boss, npcData->KnownSpellCount is set to 6, and a byte-array @421F9 is used for assigning to npcData->KnownSpellIDs. These IDs (0-based) match the order of the spells as seen in the "SPELLS" files, such as SPELLS.LST.

@421F9
6h       Wizard's Fire
Ch       Ice Bolt
14h      Wyvern's Sting
1Ch      Lightning
20h      Far Silence
3Fh      Spell Drain

For other enemy-types, the following is done. "creatureID" is a 0-based byte for the enemy index. "npcData" is a pointer to an NPCDATA structure.

  if (creatureID != 0x23) { // If not the final boss
    if (npcData->Race == 0x16 || npcData->Race == 0x0E || npcData->Race == 0x17) // If Vampire, Troll or Lich
    {
      AddRegeneration(); // TODO: Parameters passed to this function differ for these three enemy types
    }
    npcData->KnownSpellCount = 0;
    if (creatureID == 4) { // Snow Wolf
      npcData->KnownSpellCount = 1;
      npcData->KnownSpellIDs[0] = 0xc; // Ice Bolt
      *(unsigned char *)(npcData + 8) = 0xff; // Unknown (number of spell casts? 0xFF for infinite?)
      *(unsigned char *)(npcData + 5) = 6; // Unknown (caster level?)
    }
    if (creatureID == 0xB) { // Ghost
      npcData->KnownSpellCount = 1;
      npcData->KnownSpellIDs[0] = 0x3f; // Spell Drain
      *(unsigned char *)(npcData + 8) = 0xff; // Unknown
      *(unsigned char *)(npcData + 5) = 5; // Unknown
    }
    if (creatureID == 0xA) { // Hell Hound
      npcData->KnownSpellCount = 1;
      npcData->KnownSpellIDs[0] = 0x10; // Fireball
      *(unsigned char *)(npcData + 8) = 0xff; // Unknown
      *(unsigned char *)(npcData + 5) = 6; // Unknown
    }
    if (creatureID >= 0xE) { // Wraith or higher
      npcData->KnownSpellCount = CreatureKnownSpellCounts[creatureID]; // Byte-array @421E6
      npcData->KnownSpellIDs[0] = CreatureKnownSpellIDs[creatureID]; // Byte-array @421F0
     *(unsigned char *)(npcData + 8) = CreatureSpellUnknowns[creatureID]; // Byte-array @42209
     *(unsigned char *)(npcData + 5) = CreatureSpellUnknowns2[creatureID]; // Byte-array @421FF
    }
  }
CreatureKnownSpellCounts
1h Wraith
1h Homonculus
0h Ice Golem
1h Stone Golem
0h Iron Golem
1h Fire Daemon
1h Medusa
1h Vampire
1h Lich
6h Final boss (unused?)

CreatureKnownSpellIDs
10h Wraith: Fireball
1Ch Homonculus: Lightning
FFh Ice Golem: None
1Ch Stone Golem: Lightning
FFh Iron Golem: None
23h Fire Daemon: Fire Storm
2Ah Medusa: Medusa's Gaze
10h Vampire: Fireball
1Ch Lich: Lightning
(From the next byte is the 6-element array for the final boss's spell IDs)

CreatureSpellUnknowns (numbers of spell casts?)
3h Wraith
5h Homonculus
0h Ice Golem
3h Stone Golem
0h Iron Golem
5h Fire Daemon
5h Medusa
5h Vampire
5h Lich
5h Final boss (unused?)

CreatureSpellUnknowns2 (caster levels?)
Bh Wraith
Ch Homonculus
0h Ice Golem
Eh Stone Golem
0h Iron Golem
10h Fire Daemon
11h Medusa
12h Vampire
13h Lich
13h Final boss (unused?)

TODO: spells for enemy humanoids, regeneration/ability details

Creature experience is calculated as baseExperience + maxHP * experienceMultiplier.

  • Animation: @4222B. Monsters have a special 3-frame death animation at the index 5 (...6).
00..05  walk
06..07  look around (a special sequence of 6, 0, 7, 0)
08..11  attack (damage is done  in the frame 10)

Humanoids

Humanoids have an animation combined of walk and attack part. The attack part is also overlaid with a weapon animation, when applicable.

  • If an NPC has a cuirass, or at least 3 other items, of a certain armor type, that type is used for the sprite (indices 0..2)
  • Otherwise, it is 3
  • For spellcasters, it is 4, for Monks 5, and for Barbarians 6.

The sprite type array is located @45D20.

  • The weapon index is 0 to 2 for swords, axes, and maces correspondingly.
  • If the sprite index was 0, weapon index increases by 3, if 6 by 6.

A female variant is not selected for the plate armor sprite (index 0).

The weapon sprite array is located @45D97

  • For monks and mages, the special attack animations are used, MNKKIC, MAGSWD, and MAGSTF.

Humanoids do not have the death animation, they are replaced with a 'corpse' sprite (ITEM 2).

Max Health

Humanoids roll for max health in the same manner as the player, using the same per-class health dice. However, instead of having 25 added to the total as the player does in character generation, they roll their level + 1 times. (TODO: Needs more research)

Experience

Humanoid experience values are calculated as follows. Note that this function appears to not work as intended because the class value does not have ID_MASK applied to it, resulting in every class getting the "warrior type" multiplier.:

void SetHumanoidEXP(NPCSTATS* npcStats)
{
  unsigned char class = npcStats->Class;
  int index = 0;
  if (class > 5) // Not a mage-type class
  {
    index = 1;
    if (class > 12) // Not a thief-type class
    {
      index = 2;
    }
  }
  unsigned char level = npcStats->Level;
  npcStats->Experience = (level * level * HumanoidExpModifiers[index]);
  return;
}

HumanoidExpModifiers `@43591`
0Fh // Mage type
14h // Thief type
19h // Warrior type

Townsfolk

Townsfolk do not have NPC properties. They are dying with a single hit, yielding 1..4 gold and corresponding message.

Male sprites (@4559A) are chosen by the tileset. Special female sprites are chosen for the desert tileset (FMGEND) and snow/snow overcast weather (FMGENW).

Color transformation

For clothing transformation, the random value Data & 0x7fff is used. colorBase is a byte array @47096.

val <- seed
for i <- 0, 16
   flag <- val & 0x8000
   val <- rol16(val,1)
   if flag then
      block <- val & 0xF
      dest <- colorBase[i]
      if dest == 128 and block == 11 then continue  # no green hair
      src <- colorBase[block]
      for j <- 0, 10
         new[src+j] <- old[dest+j]

For skin transformation, the following values are used:

  • Bretons, Nords, Wood Elves, Khajiits - no transformation
  • Dark Elves - 52
  • High Elves - 192
  • Argonians - 116
  • Everyone else - 148

skinColor is a byte array @470A6.

for i <- 0, 10
   new[skinColor[i]] <- old[VAL+i]

Movement logic (whether to stop and idle near the player or wander around the area) is as follows:

  int xDiff = PlayerX - CitizenX;
  if (xDiff < 0) {
    xDiff = -xDiff;
  }
  int zDiff = PlayerZ - CitizenZ;
  if (zDiff < 0) {
    zDiff = -zDiff;
  }
  int shorterDist = xDiff;
  int longerDist = zDiff;
  if (zDiff <= xDiff) {
    shorterDist = zDiff;
    longerDist = xDiff;
  }
  int calculatedDistance = (shorterDist >> 2) + longerDist;
  if ((calculatedDistance < 200) &&
     ((MouseCursorIsXIcon || PlayerTargetMoveSpeed == 0) || !LeftMouseButtonPressed) &&
      (!PlayerWeaponDrawn && !PlayerInvisible)) {
    npcIdlingFlag = true;
    if (npcSprite->Frame < 6) {
      npcSprite->Frame = 6;
      return;
    }
  }
  else {
    npcIdlingFlag = false;
    int coordinateOfMovement;
    if ((npcSprite->Angle & 0x80) == 0) { // Moving along the Z-axis, so center on the X-axis of the voxel
      npcSprite->X = npcSprite->X & 0xff80;
      npcSprite->X += 0x40;
      coordinateOfMovement = npcSprite->Z;
    }
    else { // Moving along the X-axis, so center on the Z-axis of the voxel
      npcSprite->Z = npcSprite->Z & 0xff80;
      npcSprite->Z += 0x40;
      coordinateOfMovement = npcSprite)->X;
    }
    if ((49 < (coordinateOfMovement & 0x7f)) && ((coordinateOfMovement & 0x7f) < 79)) { // Collision check every once in a while
      bool collision = TownspersonCollisionCheck();
      if (collision) {
        int angleChange = 128;
        ushort rand = GetRandomNumber();
        if (rand + rand > 65535) { // If adding the 16-bit number from GetRandomNumber() to itself overflows, in other words a 1 out of 2 random chance
          angleChange = -angleChange;
        }
        npcSprite->Angle += angleChange; // Ex. Change from angle 0 to angle -128.
        npcSprite->Angle = npcSprite->Angle & 0x1ff; // Limit to 511 or lower
        return;
      }
    }
    int index = (npcSprite->Angle >> 7) * 4; // Angle >> 7 will be a value from 0 to 3
    int xMovement = CitizenMovementsX[index]; // 0, 16 or -16
    int zMovement = CitizenMovementsZ[index]; // 0, 16 or -16
    npcSprite->X += xMovement;
    npcSprite->Z += zMovement;
  }
  return;
  }

// CitizenMovementsX are at offset 0x45586, and CitizenMovementsZ are at 0x45588, in the unpacked 1.06 executable.
// You could also treat them like a 4-element array of XZ pairs starting from 0x45586.

00 00 // X0 0x45586
10 00 // Z0 0x45588
10 00 // X1
00 00 // Z1
00 00 // X2
F0 FF // Z2
F0 FF // X3
00 00 // Z3

The logic for animation is as follows:

// (the "stop if close to player" code sets CitizenIdling flag to 1 and npcSprite->Frame to 6 (the start of the idle frames) if it was at a lower value // (one of the walking frames))

    if ((npcSprite->Flags & CitizenIdling) == 0) { // if not idling
      npcSprite->Frame += 1; // animate walking animation by advancing 1 frame
      if (npcSprite->Frame > 5) {
        npcSprite->Frame = 0; // cycle back to first frame
        return;
      }
    }
    else { // Is idling
      if (npcSprite->Frame < 7) { // is frame 6
        unsigned short random = GetRandomNumber();
        if ((random & 7) == 0) {
          npcSprite->Frame += 1; // advance idle animation
          return;
        }
      }
      else { // frame is 7 or above
        if ((UpdateCount & 7) == 0) { // UpdateCount is a counter incremented by 1 every main game loop
           npcSprite->Frame += 1;
           if (npcSprite->Frame > 8) { // if past the final frame
               npcSprite->Frame = 6; // reset to start of idle animation
           }
        }
      }
    }

// The UpdateCount value is a 16-bit value that is incremented by 1 in the main game update loop.
// It gets reset to 0 when transitioning to a new area. The above function is for each citizen every game loop, so
// UpdateCount will be 1 higher than the last time the function was called for each citizen.
Clone this wiki locally