Library script to jump party

While working on SKS, I’m running into a few situations where jumping the party around for cutscene-like events is resulting in characters being on top of each other. I started looking at a library script to better position the party.

Ideally, I’d like all henchmen, follower, companions and other PCs to be lined up in rows of three behind the first PC. This lead me down the path of some facing and vector math that I’m finding to be a bit of a stretch.

The goal is for the function to get the facing of the waypoint we are jumping to and base everything on that. I’m wondering if there’s a better way to do this. I’m also wondering if someone who is better at vectors and facing math has any suggestions. :slight_smile:

Also, I’m pretty sure my modulus is off on the first run and needs adjusting.

void SKS_JumpPartyToLocation(object oPC, object oTarget)
{
    SKS_Debug("---starting SKS_JumpPartyToLocation---");
    float fXIncrement = 1.0;
    float fYIncrement = 1.0;
    // Get the vector of our target
    object oArea = GetArea(oTarget);
    vector vStart = GetPosition(oTarget);
    float fTargetFacing = GetFacing(oTarget);
    SKS_Debug(VectorToString(vStart));
    // Create variables to track our offsets
    float fXOffset = 0.0;
    float fYOffset = 0.0;
    int nJumpCount = 0;
    float fYAdjusted = 0.0;
    float fXAdjusted = 0.0;
    // Loop through all party members
    object oPartyMember = GetFirstFactionMember(oPC, FALSE);
    while(GetIsObjectValid(oPartyMember)){
        // PC only, fades and camera
        if (GetIsPC(oPartyMember))
        {
            // Fade to black
            DelayCommand(0.2, FadeToBlack(oPartyMember, FADE_SPEED_FAST));
            // Adjust the camera
            DelayCommand(1.2, AssignCommand(oPartyMember, SetCameraFacing(fTargetFacing)));
            // Fade out of black
            DelayCommand(3.5, FadeFromBlack(oPartyMember, FADE_SPEED_MEDIUM));
        }
        // Clear actions
        DelayCommand(0.1, AssignCommand(oPartyMember, ClearAllActions()));
        // Jump to the new location
        vector vOffset = Vector(fXOffset, fYOffset);
        vector vEndvector = vStart+vOffset;
        SKS_Debug("jumping: " + VectorToString(vEndvector));
        location lFinalTarget = Location(oArea, vEndvector, 0.0);
        DelayCommand(1.0, AssignCommand(oPartyMember, ActionJumpToLocation(lFinalTarget)));
        DelayCommand(1.1, AssignCommand(oPartyMember, SetFacing(fTargetFacing)));
        // Get next party member
        oPartyMember = GetNextFactionMember(oPC, FALSE);
        // Adjustt values for the next run
        if (nJumpCount == 0 || nJumpCount % 3 == 0)
        {
            fXAdjusted = fXAdjusted + fXIncrement;
            fYAdjusted = fXIncrement * -1;
            SKS_Debug("modulus: x "+ FloatToString(fXAdjusted) +" y "+ FloatToString(fYAdjusted) +".");
        }
        else {
            fYAdjusted = fYAdjusted + fXIncrement;
            SKS_Debug("else: x "+ FloatToString(fXAdjusted) +" y "+ FloatToString(fYAdjusted) +".");
        }
        nJumpCount++;
        // Adjust the location for the next loop
        // See https://math.stackexchange.com/questions/143932/calculate-point-given-x-y-angle-and-distance
        fXOffset = fXAdjusted * cos(fTargetFacing);
        fYOffset = fYAdjusted * sin(fTargetFacing);
        SKS_Debug("final offest values are: x "+ FloatToString(fXOffset) +" y "+ FloatToString(fYOffset) +".");
    }
}

Thanks in advance for any suggestions!

Have a look in Unearthed Gold, in the Endless List of Functions section, in Endless 1.txt on line 114 onwards. It’s a function called MoveLocation() by Jassper. You should be able to adjust it for what you need.

FWIW, I barely understand the maths myself but it definitely works for a single character as I’ve used it myself and I think it can be adapted.

TR

I haven’t tested your code, but I suggest you also try the opposite. First make the formation, then align it with that waypoint. Basic idea:

  1. Calculate vector where the creature would be if PC is at origin and its facing is 0 (PC’s facing vector is <1, 0, 0>).
  2. Rotate this vector in 2D (around Z axis) by degrees equal to destination’s facing (this is universal - works with any kind of formation).
  3. This rotated vector is now the shift from PC’s actual destination position.
  4. Jump, set same facing as PC; repeat for all creatures.

This should save some headaches, mostly by decoupling the formation generation subroutine from final placement in the game world.

When arranging creatures in an array, both the rank and file spacing needs to be at least one “perspace” unit, as defined in appearance.2da. For dismounted humans, this is 1.5m by default.

There are battle line functions in my Enigma Island and Dark Energy modules that do the maths centred on any waypoint for both mounted and dismounted humans.

This works fine on older tilesets, which are either flat or have crude elevations with ramps. Newer tilesets with more sophisticated elevations sometimes have tiny walkmesh variations which make it difficult to determine the minimum spacing.

See Lexicon for more information and workarounds.

Thanks for the responses. I looked into each of these and spent some time digging into the solution the @NWShacker suggested of jumping, then rotating.

At this point, I think my degrees to radian conversion is wrong. I’m getting the same vector for both the jump and the rotation. I’m probably getting to a point where I might back away from this. Here’s my current code for anyone interested. We’ll see if I pick it back up, or move towards something a bit more simple. At some point, I was finding that the way I was doing things helped resolve the main issue of characters on top of the PC. I might actually go back to that.

void SKS_JumpPartyToLocation(object oPC, object oTarget)
{
    SKS_Debug("---starting SKS_JumpPartyToLocation---");
    // If the camera is east, then we want to go backwards on the x axis
    float fXIncrement = 2.0;
    float fYIncrement = 2.0;
    // Get the vector of our target
    object oArea = GetArea(oTarget);
    vector vStart = GetPosition(oTarget);
    float fTargetFacing = GetFacing(oTarget);
    SKS_Debug("Target vector: "+ VectorToString(vStart) +". Facing: "+ FloatToString(fTargetFacing));
    // Create variables to track our offsets
    float fXOffset = 0.0;
    float fYOffset = 0.0;
    int nJumpCount = 0;
    float fYAdjusted = 0.0;
    float fXAdjusted = 0.0;
    // Loop through all party members
    object oPartyMember = GetFirstFactionMember(oPC, FALSE);
    while(GetIsObjectValid(oPartyMember)){
        float fDelay = IntToFloat(nJumpCount);
        // PC only, fades and camera
        if (GetIsPC(oPartyMember))
        {
            // Fade to black
            //DelayCommand(fDelay+0.2, FadeToBlack(oPartyMember, FADE_SPEED_FAST));
            // Adjust the camera
            DelayCommand(fDelay+1.2, AssignCommand(oPartyMember, SetCameraFacing(fTargetFacing)));
            // Fade out of black
            //DelayCommand(fDelay+3.5, FadeFromBlack(oPartyMember, FADE_SPEED_MEDIUM));
        }
        else {
            // NPC followers need an extra bit of time to catch up
            fDelay = fDelay + 1.0;
        }
        // Clear actions
        DelayCommand(fDelay+0.1, AssignCommand(oPartyMember, ClearAllActions()));

        // Jump to the new location
        vector vOffset = Vector(fXOffset, fYOffset, 0.0);
        vector vEndvector = vStart+vOffset;
        SKS_Debug("jumping " + GetName(oPartyMember) +": " + VectorToString(vEndvector));
        location lTarget = Location(oArea, vEndvector, 0.0);
        DelayCommand(fDelay+1.0, AssignCommand(oPartyMember, ActionJumpToLocation(lTarget)));

        // Rotate based on facing
        // http://danceswithcode.net/engineeringnotes/rotations_in_2d/rotations_in_2d.html
        float fRadians = fTargetFacing * 3.14/180;
        float fCos = cos(fRadians);
        float fSin = sin(fRadians);
        float fRotatedX = ( (vEndvector.x - vStart.x) * fCos - (vEndvector.y - vStart.y) * fSin ) + vStart.x;
        float fRotatedY = ( (vEndvector.x - vStart.x) * fSin + (vEndvector.y - vStart.y) * fCos ) + vStart.y;
        vector vRotatedPos = Vector(fRotatedX, fRotatedY, vEndvector.z);
        SKS_Debug("rotating " + GetName(oPartyMember) +": " + VectorToString(vRotatedPos));
        location lFinalTarget = Location(oArea, vRotatedPos, 0.0);
        DelayCommand(fDelay+2.0, AssignCommand(oPartyMember, ActionJumpToLocation(lFinalTarget)));

        // Set facing
        DelayCommand(fDelay+2.1, AssignCommand(oPartyMember, SetFacing(fTargetFacing)));
        
        // Get next party member
        oPartyMember = GetNextFactionMember(oPC, FALSE);

        // Adjust values for the next run
        if (nJumpCount % 3 == 0)
        {
            fXAdjusted = fXAdjusted - fXIncrement;
            fYAdjusted = fXIncrement;
            SKS_Debug("modulus: x "+ FloatToString(fXAdjusted) +" y "+ FloatToString(fYAdjusted) +".");
        }
        else {
            fYAdjusted = fYAdjusted - fXIncrement;
            SKS_Debug("else: x "+ FloatToString(fXAdjusted) +" y "+ FloatToString(fYAdjusted) +".");
        }
        nJumpCount++;
        fXOffset = fXAdjusted;
        fYOffset = fYAdjusted;
        SKS_Debug("final offest values are: x "+ FloatToString(fXOffset) +" y "+ FloatToString(fYOffset) +".");
    }
}

Thanks!

I actually suggested rotating first, then jumping :slight_smile:

NWN trigonometric functions already accept and return degrees (although their descriptions don’t mention that) - you don’t need to convert them to radians.

AML has a 2D rotation function (any many other) - you may find them useful.

Ah, thanks @NWShacker. Removing my attempt to convert to radians actually got it mostly working. I think I’m still struggling with some timing issues, however. I’ll probably take another look when I have more time this weekend.

I’m still debating if this is even a good idea. All I want is to jump the party to a location and not have them all standing on top of each other.

And now I just found how mounts work, so that feels like a whole thing I should be looking at instead. I might have to look at @Proleric’s horse demo module at some point. :slight_smile:

Let me know if you have any questions about that.

The horse demo covers the Bioware basics. I made some improvements, which you can see most easily at the beginning of Enigma Island Part 3, where, having gathered your party, you can obtain horses from the stables, one-click party mount, ride to Koberg faster than walking, and hitch the horses on arrival.

The battle line script isn’t in the demo, either, so here it is:

// Battle lines

const int NO_TAIL      = 0;
const int FRIENDLY     = FALSE;
const int HOSTILE      = TRUE;
const int MOUNTED      = TRUE;
const int FOOT         = FALSE;
const int CENTRE       = 0;
const int CENTRE_LEFT  = 1;
const int CENTRE_RIGHT = -1;

// Set up a battle line and return location of last soldier.
// sNPC           - Soldier template
// lLocation      - Line location
// sHorseTemplate - If specified, soldiers will be given a mounted appearance.
// bLocal         - If TRUE, faction is reset to Local.
// fFileSpacing   - The distance between files
// fRankOffset    - The distance between ranks
// nFile          - Number of files to form
// nRank          - Number of ranks to form
location bhBattleLine(string sNPC, location lLocation, int nHorseTail = 0, int bLocal = FALSE,
                      int bMounted = FALSE, int nFile = 1, int nRank = 1);

// Set up a battle line

location bhBattleLine(string sNPC, location lLocation, int nHorseTail = 0, int bLocal = FALSE,
                      int bMounted = FALSE, int nFile = 1, int nRank = 1)
{
  int      i           = 0;
  object   oOldGuard   = GetObjectByTag(sNPC, i);
  object   oGuard;
  object   oArea       = GetAreaFromLocation(lLocation);
  float    fFacing     = GetFacingFromLocation(lLocation);
  vector   vNPC        = GetPositionFromLocation(lLocation);
  float    fFirstRank  = vNPC.y;
  location lNew;
  float    fFileSpacing = 1.5;
  float    fRankSpacing = 1.5;
  float    fFileOffset;
  float    fRankOffset;
  int      nMyRank     = 99;
  int      nMyFile     = 0;
  int      n           = 0;
  int      m           = 0;

  if (bMounted) {fFileSpacing = 2.0; fRankSpacing = 3.0;}

  fFileOffset = fFileSpacing * IntToFloat(nFile - 1) / 2.0; // Centre first file on location

  vNPC.x -= fFileOffset * sin(fFacing);
  vNPC.y += fFileOffset * cos(fFacing);

  while (++n <= nFile)
  {
    m = 0;
    while (++m <= nRank)
      {
        lNew = Location(oArea, vNPC, fFacing);

        oGuard = CreateObject(OBJECT_TYPE_CREATURE, sNPC, lNew);

        SetLocalLocation(oGuard, "Location", lNew);

        if (nHorseTail)
         {
            SetPhenoType(HORSE_PHENOTYPE_JOUSTING_N, oGuard);
            SetCreatureTailType(nHorseTail, oGuard);
            SetFootstepType(FOOTSTEP_TYPE_HORSE, oGuard);
         }

//      AssignCommand(oGuard, bhArm());

        vNPC.x -= fRankSpacing * cos(fFacing);
        vNPC.y -= fRankSpacing * sin(fFacing);
      } // End rank

      vNPC.x += IntToFloat(nRank) * fRankSpacing * cos(fFacing); // Front of rank
      vNPC.y += IntToFloat(nRank) * fRankSpacing * sin(fFacing);

      vNPC.x += fFileSpacing * sin(fFacing); // Next file
      vNPC.y -= fFileSpacing * cos(fFacing);
  } // End file
  return lNew;
}

The script returns the location of the last soldier, so that further units and NPCs can be added to the army in an orderly fashion.

I spent some more time on this today, and I’m feeling pretty good about the result. The timing issues were resolved by giving NPC followers a delay. That seems to ensure these commands were not getting tripped up by the standard commands that happen when a follower jumps with a PC master. Here’s the final code for others who are interested.

I might loop back and try to adjust for mounted creatures.

Thanks again to @Proleric, @NWShacker and @Tarot_Redhand for the help on this! The examples and suggestions were very helpful.

// Jump the party to an object (in the current or another area).
// Spreads out party members in a 3-column formation behind the first PC so
// that they are not on top of each other. Also sets the facing and camera.
// Adds a fast fadedout that lasts for 4.5 seconds, then a medium fade in.
// Camera changes and other effects should happen between 3.1s and 4.4s or after 6.0s.
void SKS_JumpPartyToLocation(object oPC, object oTarget);

// Implementation
void SKS_JumpPartyToLocation(object oPC, object oTarget)
{
    // Set the increments based on the stanard spacing for non-mounted creatures
    // TODO: Determine if mounted and we need to adjust the size.
    float fXIncrement = 1.5;
    float fYIncrement = 1.5;

    // Get information about our target object
    object oArea = GetArea(oTarget);
    vector vStart = GetPosition(oTarget);
    float fTargetFacing = GetFacing(oTarget);

    // Create a variable to track the x,y offsets
    float fXOffset = 0.0;
    float fYOffset = 0.0;

    // Create a variable to track the number of jumps
    // This is used to determine the positioning for members of the party
    int nJumpCount = 0;

    // Loop through all party members
    object oPartyMember = GetFirstFactionMember(oPC, FALSE);
    while(GetIsObjectValid(oPartyMember)){

        // Clear actions as an immeidate command
        AssignCommand(oPartyMember, ClearAllActions());

        // Set up a delay we will use to adjust timing for NPC followers
        float fDelay = 0.0;

        // Determine the adjusted positioning based on this location when facing is 0.0 (east).
        vector vOffset = Vector(fXOffset, fYOffset, 0.0);
        vector vEndvector = vStart + vOffset;

        // Rotate x,y coordinates around our target location based on facing
        // http://danceswithcode.net/engineeringnotes/rotations_in_2d/rotations_in_2d.html
        float fCos = cos(fTargetFacing);
        float fSin = sin(fTargetFacing);
        float fRotatedX = ( (vEndvector.x - vStart.x) * fCos - (vEndvector.y - vStart.y) * fSin ) + vStart.x;
        float fRotatedY = ( (vEndvector.x - vStart.x) * fSin + (vEndvector.y - vStart.y) * fCos ) + vStart.y;
        vector vRotatedPos = Vector(fRotatedX, fRotatedY, vEndvector.z);
        location lFinalTarget = Location(oArea, vRotatedPos, fTargetFacing);

        // Actions for PCs
        if (GetIsPC(oPartyMember))
        {
            // Clear actions
            DelayCommand(fDelay+0.1, AssignCommand(oPartyMember, ClearAllActions()));
            // Fade to black
            DelayCommand(fDelay+0.2, FadeToBlack(oPartyMember, FADE_SPEED_FAST));
            // Jump into position
            DelayCommand(fDelay+1.0, AssignCommand(oPartyMember, JumpToLocation(lFinalTarget)));
            // Adjust the camera
            DelayCommand(fDelay+3.0, AssignCommand(oPartyMember, SetCameraFacing(fTargetFacing)));
            // Fade out of black
            DelayCommand(fDelay+4.5, FadeFromBlack(oPartyMember, FADE_SPEED_MEDIUM));
        }
        // Actions for NPC followers
        else
        {
            // NPC followers need an extra bit of time to catch up after an area transition
            fDelay = 2.0;
            // Clear actions after the jump
            DelayCommand(fDelay+0.1, AssignCommand(oPartyMember, ClearAllActions()));
            // Jump to the rotated position
            DelayCommand(fDelay+0.2, AssignCommand(oPartyMember, JumpToLocation(lFinalTarget)));
            // Henchmen need another facing after jumping
            DelayCommand(fDelay+0.4, AssignCommand(oPartyMember, SetFacing(fTargetFacing)));
        }

        // Get next party member
        oPartyMember = GetNextFactionMember(oPC, FALSE);

        // Adjust values for the next run
        // Every third party member (including after the first pc)
        if (nJumpCount % 3 == 0)
        {
            // Move back to the next row.
            fXOffset = fXOffset - fXIncrement;
            // Reset to the right column.
            fYOffset = fXIncrement;
        }
        // All other jumps
        else
        {
            // Move to the next column in the current row
            fYOffset = fYOffset - fYIncrement;
        }
        // Update the jump count
        nJumpCount++;
    }
}