Vehicles/VehicleScripts/SnowGroomer.lua

SnowGroomer is a vehicle script that is responsible for the control of the groomer.

The lua script is based on the C# class SnowGroomerIK, which was specifically designed to solve 4-dimensional inverse kinematics problems. This leads to the best possible visual results at almost no performance impact. However, the script's configuration requires some parameters that have to be provided in the dataTable.

  28  SnowGroomer                         = SnowGroomer or {};
  29  SnowGroomer.directionSmoothRadius   = 0.8;

SnowGroomer:load(dataTable)

First, we set up the groomer-specific functions. Afterwards, we read in the configuration data from the dataTable.

Some remarks:

  • axis1 to axis4 are the indexes of the objects that are rotated for each axis. We need 4 degrees of freedom, because the groomer shall be allowed to rotate freely within some limits (i.e. three degrees of freedom in rotation), but it should also stay parallel to the ground (i.e. it needs another rotation axis to allow for a translation movement).
    • axis 1 rotates around Y (left/right, required for trail)
    • axis 2 rotates around X (up/down)
    • axis 3 again rotates around X (required for the 4th degree of freedom)
    • axis 4 rotates around Z (tilt)
  • v.lifted.axis1 to v.lifted.axis4 specify the rotation targets if the groomer is lifted.
  • If the groomer is lowered, the individual axis limits (axis1Limit, axis2MinLimit, axis2MaxLimit etc.) are activated and the groomer can move freely between the limits.
  • The script supports flaps and also a rotating milling shaft
  • As soon as the groomer is lowered, the milling shaft starts rotating, and the snowGroomerPrepAreas are activated. Only then, the slope can actually be groomed.
  45  function SnowGroomer:load(dataTable)
  46      self.updateSnowGroomerLimits    = VehicleManager:newFunction("updateSnowGroomerLimits");
  47      self.setIsSnowGroomerIsLowered  = VehicleManager:newFunction("setIsSnowGroomerIsLowered");
  48      self.setIsSnowGroomerIsLocked   = VehicleManager:newFunction("setIsSnowGroomerIsLocked");
  49      self.setSnowGroomerFlaps        = VehicleManager:newFunction("setSnowGroomerFlaps");
  50      self.setSnowGroomerRotation     = VehicleManager:newFunction("setSnowGroomerRotation");
  51      
  52      
  53      if dataTable.snowGroomer ~= nil then
  54          local v                     = dataTable.snowGroomer;
  55          local axis1                 = getChild(self.id, v.axis1 or "");
  56          local axis2                 = getChild(self.id, v.axis2 or "");
  57          local axis3                 = getChild(self.id, v.axis3 or "");
  58          local axis4                 = getChild(self.id, v.axis4 or "");
  59  
  60          self.useRectangularPrepAreas = v.useRectangularPrepAreas or false;
  61          if not self.useRectangularPrepAreas then
  62              print("Warning: Your vehicle '" .. tostring(self.vehicleType) .. "' is still not using the rectangularPrepAreas. Please add 'useRectangularPrepAreas' to your groomer and implement the new areas accordingly to avoid low-FPS issues.");
  63          end;
  64  
  65          local raycastOrigin         = getChild(self.id, v.raycastOrigin or v.groomerCenter or "");
  66          if raycastOrigin == 0 then
  67              print("Error: You are missing the 'raycastOrigin' in your groomer in vehicle type '" .. tostring(self.vehicleType) .. "'. Please add a raycastOrigin in the dataTable.");
  68          end;
  69  
  70          local raycastOrigin_left    = ifelse(v.raycastOrigin_left ~= nil, getChild(self.id, v.raycastOrigin_left), 0);
  71          local raycastOrigin_right   = ifelse(v.raycastOrigin_right ~= nil, getChild(self.id, v.raycastOrigin_right), 0);
  72          
  73          -- if raycastOrigin_left == 0 or raycastOrigin_right == 0 then
  74          --  print("Note: Vehicle type '" .. tostring(self.vehicleType) .. "' does not have a 'raycastOrigin_left' and 'raycastOrigin_right' defined.")
  75          -- end;
  76  
  77          self.snowGroomerIK          = SnowGroomerIK.new(self.id, axis1, axis2, axis3, axis4, raycastOrigin, raycastOrigin_left, raycastOrigin_right);
  78          SnowGroomerIK.setupGeometry(self.snowGroomerIK,
  79              v.axis23Length      or 1,
  80              v.groundToAxis4Y    or 0.5,
  81              v.groundToAxis4Z    or 0.5,
  82              v.axis4To3Y         or 0,
  83              v.axis4To3Z         or 0,
  84              v.groomerMoveLimit  or 0
  85          );
  86  
  87          self.snowGroomerLifted      = {
  88              axis1                   = v.lifted.axis1    or   0,
  89              axis2                   = v.lifted.axis2    or  45,
  90              axis3                   = v.lifted.axis3    or -45,
  91              axis4                   = v.lifted.axis4    or   0,
  92          };
  93          self.snowGroomerLowered     = {
  94              axis1Limit              = v.lowered.axis1Limit      or 20,
  95              axis2MinLimit           = v.lowered.axis2MinLimit   or -45,
  96              axis2MaxLimit           = v.lowered.axis2MaxLimit   or  45,
  97              axis3MinLimit           = v.lowered.axis3MinLimit   or -45,
  98              axis3MaxLimit           = v.lowered.axis3MaxLimit   or  45,
  99              axis4MinLimit           = v.lowered.axis4MinLimit   or -10,
 100              axis4MaxLimit           = v.lowered.axis4MaxLimit   or  10,
 101          };
 102          self.snowGroomerTimeToLower = math.max(v.timeToLower or 2, 0.1);
 103          self.snowGroomerTimeToLift  = math.max(v.timeToLift  or 2, 0.1);
 104          self.snowGroomerTimeToLock  = math.max(v.timeToLock  or 2, 0.1);
 105  
 106          if v.leftFlap ~= nil then
 107              self.snowGroomerLeftFlap    = getChild(self.id, v.leftFlap or "");
 108              self.leftFlapPosition       = 0;
 109              self.leftFlapTarget         = 1;
 110              self.leftFlapDuration       = Animation.getLength(self.snowGroomerLeftFlap);
 111              Animation.stop(self.snowGroomerLeftFlap);
 112              Animation.sampleTime(self.snowGroomerLeftFlap, 0);
 113          end;
 114          if v.rightFlap ~= nil then
 115              self.snowGroomerRightFlap   = getChild(self.id, v.rightFlap or "");
 116              self.rightFlapPosition      = 0;
 117              self.rightFlapTarget        = 1;
 118              self.rightFlapDuration      = Animation.getLength(self.snowGroomerRightFlap);
 119              Animation.stop(self.snowGroomerRightFlap);
 120              Animation.sampleTime(self.snowGroomerRightFlap, 0);
 121          end;
 122  
 123          self.enableTurnGroomer          = getNoNil(v.isTurnable, false);
 124  
 125          if v.turningPointSnowcat ~= nil then
 126              self.turningPointSnowcat    = getChild(self.id, v.turningPointSnowcat);
 127          end;
 128          if v.turningPointGroomer ~= nil then
 129              self.turningPointGroomer    = getChild(self.id, v.turningPointGroomer);
 130          end;
 131  
 132          self.groomerRotation            = 0;
 133          self.maxTurnAngle               = v.maxTurnAngle or 15;
 134  
 135          if v.rotatingParts ~= nil then
 136              self.groomerRotatingParts   = {};
 137  
 138              for k2, v2 in pairs(v.rotatingParts) do
 139                  table.insert(self.groomerRotatingParts, {
 140                      id                  = getChild(self.id, v2.index),
 141                      axis                = v2.axis or 1,
 142                      speed               = (v2.speed or 300)/60 * 360, -- rpm (here converted to degrees per second)
 143                  });
 144              end;
 145          end;
 146  
 147          -- preparation areas
 148          self.snowGroomerPrepAreas       = {};
 149          if self.useRectangularPrepAreas then
 150              self.snowGroomerPrepAreasLastPositions = {};
 151              
 152              if v.preparationAreas ~= nil then
 153                  for k, n in pairs(v.preparationAreas) do
 154                      if n.backLeft == nil then
 155                          print("Error: You set 'useRectangularPrepAreas' in your groomer in vehicle type '" .. tostring(self.vehicleType) .. "' to true but it seems you are still using the triangle preparation areas.");
 156                      end;
 157                      local area              = {
 158                          backLeft            = getChild(self.id, n.backLeft or ""),
 159                          backRight           = getChild(self.id, n.backRight or ""),
 160                          frontLeft           = getChild(self.id, n.frontLeft or ""),
 161                          frontRight          = getChild(self.id, n.frontRight or ""),
 162                          smoothRadius        = n[5] or SnowGroomer.directionSmoothRadius,
 163                          quality             = 100,      -- 100% quality if driving straight forward
 164                          --[[slipQualityLossCoeff= 45,
 165                          slipTolerance       = 1,
 
 166  
 167                          lastPositionCenter  = Vector3.zero:clone(),
 168                      };
 169                      local lastPositions     = {
 170                          lastBackLeft        = VectorUtils.getWorldPosition(area.backLeft),
 171                          lastBackRight       = VectorUtils.getWorldPosition(area.backRight),
 172                          lastFrontLeft       = VectorUtils.getWorldPosition(area.frontLeft),
 173                          lastFrontRight      = VectorUtils.getWorldPosition(area.frontRight),
 174                      }
 175                      area.slipReferenceId    = area.backLeft;
 176  
 177                      table.insert(self.snowGroomerPrepAreas, area);
 178                      table.insert(self.snowGroomerPrepAreasLastPositions, lastPositions);
 179                  end;
 180              end;
 181          elseif v.preparationAreas ~= nil then
 182              for k, n in pairs(v.preparationAreas) do
 183                  if  n.backLeft ~= nil then
 184                      print("Error: You set 'useRectangularPrepAreas' in your groomer in vehicle type '" .. tostring(self.vehicleType) .. "' to false but it seems you are using the rectangular preparation areas.");
 185                  end;
 186                  local area              = {
 187                      id1                 = getChild(self.id, n[1] or ""),
 188                      id2                 = getChild(self.id, n[2] or ""),
 189                      id3                 = getChild(self.id, n[3] or ""),
 190                      smoothRadius        = n[4] or SnowGroomer.directionSmoothRadius,
 191                      quality             = 100,      -- 100% quality if driving straight forward
 192                      --[[slipQualityLossCoeff= 45,
 193                      slipTolerance       = 1,
 
 194  
 195                      lastPositionCenter  = Vector3.zero:clone(),
 196                  };
 197                  area.slipReferenceId    = area.id1;
 198  
 199                  table.insert(self.snowGroomerPrepAreas, area);
 200              end;
 201          end;
 202  
 203          -- don't lower by default
 204          self.snowGroomerIsLowered   = false;
 205          self.snowGroomerIsLocked    = false;
 206          self.snowGroomerPos         = 0; -- position to interpolate between limits and lifted
 207          self.snowGroomerLockedPos   = 0;
 208          self.shouldStayLocked       = false;
 209          
 210          self:updateSnowGroomerLimits();
 211      end;
 212  end;

SnowGroomer:saveToTable(tbl)

As always, we need to save some variables in our savegame.

 216  function SnowGroomer:saveToTable(tbl)
 217      if tbl == nil then return end;
 218      if self.snowGroomerIK == nil then return end;
 219  
 220      tbl.snowGroomerIsLowered        = self.snowGroomerIsLowered;
 221      tbl.snowGroomerIsLocked         = self.snowGroomerIsLocked;
 222      tbl.snowGroomerPos              = self.snowGroomerPos;
 223      tbl.snowGroomerLockedPos        = self.snowGroomerLockedPos;
 224  
 225      tbl.leftFlapTarget              = self.leftFlapTarget;
 226      tbl.leftFlapPosition            = self.leftFlapPosition;
 227      tbl.rightFlapTarget             = self.rightFlapTarget;
 228      tbl.rightFlapPosition           = self.rightFlapPosition;
 229  end;

SnowGroomer:loadFromTable(tbl)

Restore the variables from the savegame.

 233  function SnowGroomer:loadFromTable(tbl)
 234      if tbl == nil then return end;
 235      if self.snowGroomerIK == nil then return end;
 236  
 237      self.snowGroomerIsLowered       = getNoNil(tbl.snowGroomerIsLowered,    self.snowGroomerIsLowered);
 238      self.snowGroomerIsLocked        = getNoNil(tbl.snowGroomerIsLocked,     self.snowGroomerIsLocked);
 239      self.snowGroomerPos             = getNoNil(tbl.snowGroomerPos,          self.snowGroomerPos);
 240      self.snowGroomerLockedPos       = getNoNil(tbl.snowGroomerLockedPos,    self.snowGroomerLockedPos);
 241  
 242      self.leftFlapTarget             = getNoNil(tbl.leftFlapTarget,          self.leftFlapTarget);
 243      self.leftFlapPosition           = getNoNil(tbl.leftFlapPosition,        self.leftFlapPosition);
 244      self.rightFlapTarget            = getNoNil(tbl.rightFlapTarget,         self.rightFlapTarget);
 245      self.rightFlapPosition          = getNoNil(tbl.rightFlapPosition,       self.rightFlapPosition);
 246  
 247      -- always update real flap position in update
 248      if self.leftFlapTarget ~= nil then
 249          self.leftFlapPosition       = 0.5;
 250      end;
 251      if self.rightFlapTarget ~= nil then
 252          self.rightFlapPosition      = 0.5;
 253      end;
 254  
 255      SnowGroomer.updateGroomerActive(self, 0, true);
 256  end;

SnowGroomer:update(dt)

Update the controls each frame (i.e. check whether the player wants to lower/lift the groomer, lock the trail or move some flaps). The actual groomer update is done in SnowGroomer:updateGroomerActive().

 261  function SnowGroomer:update(dt)
 262      if not self.isActive then return end;
 263      if self.snowGroomerIK == nil then return end;
 264      
 265      if self:getIsInputActive() and not self.hydraulicLiftGroomer then
 266          if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_LowerGroomer) then
 267              self:setIsSnowGroomerIsLowered(not self.snowGroomerIsLowered);
 268          end;
 269          g_GUI:addKeyHint(InputMapper.VehicleSnowcat_LowerGroomer,       l10n.get(self.snowGroomerIsLowered  and "Input_VehicleSnowcat_LowerGroomer1"    or "Input_VehicleSnowcat_LowerGroomer0"));
 270  
 271          if self.snowGroomerIsLowered then
 272              -- only allow to lock/unlock if snow groomer is lowered
 273              if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_LockTrail) then
 274                  self:setIsSnowGroomerIsLocked(not self.snowGroomerIsLocked);
 275              end;
 276              if not self.snowGroomerIsLocked then
 277                  if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_TurnGroomerLeft) or InputMapper:getKeyDown(InputMapper.VehicleSnowcat_TurnGroomerRight) then
 278                      self:setIsSnowGroomerIsLocked(not self.snowGroomerIsLocked);
 279                  end;
 280              end;
 281  
 282              -- left/right flap controls
 283              if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_LeftFlap) then
 284                  self:setSnowGroomerFlaps(self.leftFlapTarget == 0, nil);
 285              end;
 286              if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_RightFlap) then
 287                  self:setSnowGroomerFlaps(nil, self.rightFlapTarget == 0);
 288              end;
 289  
 290              g_GUI:addKeyHint(InputMapper.VehicleSnowcat_LockTrail,              l10n.get(self.snowGroomerIsLocked       and "Input_VehicleSnowcat_LockTrail1"       or "Input_VehicleSnowcat_LockTrail0"));
 291              
 292              if self.enableTurnGroomer then
 293                  g_GUI:addKeyHint(InputMapper.VehicleSnowcat_TurnGroomerLeft,        l10n.get("Input_VehicleSnowcat_TurnGroomerLeft1"));
 294                  g_GUI:addKeyHint(InputMapper.VehicleSnowcat_TurnGroomerRight,       l10n.get("Input_VehicleSnowcat_TurnGroomerRight1"));
 295              end;
 296              
 297              if self.snowGroomerIsLocked then 
 298                  if InputMapper:getKey(InputMapper.VehicleSnowcat_TurnGroomerLeft) then
 299                      if self.groomerRotation > -self.maxTurnAngle then
 300                          self.groomerRotation = self.groomerRotation - 0.5;
 301                          self:setSnowGroomerRotation(self.groomerRotation);
 302                      end;
 303                  end;
 304                  --if InputMapper:getKeyUp(InputMapper.VehicleSnowcat_TurnGroomerLeft) then
 305                  --  if self.groomerRotation > -self.maxTurnAngle-2 then
 306                  --      self.groomerRotation = self.groomerRotation - 1;
 307                  --      self:setSnowGroomerRotation(self.groomerRotation);
 308                  --  end;
 309                  --end;
 310                  
 311                  if InputMapper:getKey(InputMapper.VehicleSnowcat_TurnGroomerRight) then
 312                      if self.groomerRotation < self.maxTurnAngle then
 313                          self.groomerRotation = self.groomerRotation + 0.5;
 314                          self:setSnowGroomerRotation(self.groomerRotation);
 315                      end;
 316                  end;
 317                  --if InputMapper:getKeyUp(InputMapper.VehicleSnowcat_TurnGroomerRight) then
 318                  --  if self.groomerRotation < self.maxTurnAngle+2 then
 319                  --      self.groomerRotation = self.groomerRotation + 1;
 320                  --      self:setSnowGroomerRotation(self.groomerRotation);
 321                  --  end;
 322                  --end;
 323  
 324              end;
 325          end;
 326      end;
 327  
 328      SnowGroomer.updateGroomerActive(self, dt);
 329  end;

SnowGroomer:updateGroomerActive(dt, forceUpdate)

This is called in every frame if the groomer is active. It determines current animation positions and grooms the preparation areas.

 333  function SnowGroomer:updateGroomerActive(dt, forceUpdate)
 334      -- update groomer position
 335      local target                = self.snowGroomerIsLowered and not self.hydraulicLiftGroomer   and 1 or 0;
 336      local lockTarget            = self.snowGroomerIsLocked                                      and 1 or 0;
 337      local leftTarget            = (target < 1 or self.snowGroomerPos < 0.5) and 0 or self.leftFlapTarget;
 338      local rightTarget           = (target < 1 or self.snowGroomerPos < 0.5) and 0 or self.rightFlapTarget;
 339  
 340      local update                = false;
 341      if math.abs(self.snowGroomerPos - target) > 1e-3 or forceUpdate then
 342          local duration          = self.snowGroomerIsLowered and self.snowGroomerTimeToLower or self.snowGroomerTimeToLift;
 343          self.snowGroomerPos     = Utils.moveTowards(self.snowGroomerPos, target, dt/duration);
 344          update                  = true;
 345      end;
 346      
 347      if math.abs(self.snowGroomerLockedPos - lockTarget) > 1e-3 or forceUpdate then
 348          self.snowGroomerLockedPos = Utils.moveTowards(self.snowGroomerLockedPos, lockTarget, dt/self.snowGroomerTimeToLock);
 349          update                  = true;
 350      end;
 351  
 352      if self.snowGroomerIsLocked and self.groomerRotation ~= nil and self.turningPointGroomer ~= nil and self.turningPointSnowcat ~= nil then        
 353          setRotationY(self.turningPointGroomer, self.groomerRotation);
 354          setRotationY(self.turningPointSnowcat, -self.groomerRotation);
 355  
 356      elseif not self.snowGroomerIsLocked and self.groomerRotation ~= nil and self.turningPointGroomer ~= nil and self.turningPointSnowcat ~= nil then
 357          if math.abs(self.groomerRotation) > 2 then
 358              if self.groomerRotation > 0 then
 359                  self.groomerRotation = self.groomerRotation - 0.5;
 360              else
 361                  self.groomerRotation = self.groomerRotation + 0.5;
 362              end;
 363          end;
 364          
 365          setRotationY(self.turningPointGroomer, self.groomerRotation);
 366          setRotationY(self.turningPointSnowcat, -self.groomerRotation);
 367      end;
 368  
 369      if not self.snowGroomerIsLowered then
 370          if self.groomerRotation ~= nil and self.turningPointGroomer ~= nil and self.turningPointSnowcat ~= nil then
 371              if math.abs(self.groomerRotation) > 2 then
 372                  if self.groomerRotation > 0 then
 373                      self.groomerRotation = self.groomerRotation - 0.5;
 374                  else
 375                      self.groomerRotation = self.groomerRotation + 0.5;
 376                  end;
 377              end;
 378              setRotationY(self.turningPointGroomer, self.groomerRotation);
 379              setRotationY(self.turningPointSnowcat, -self.groomerRotation);
 380          end;
 381      end;
 382  
 383      if self.snowGroomerLeftFlap ~= nil and (math.abs(self.leftFlapPosition - leftTarget) > 1e-3 or forceUpdate) then
 384          self.leftFlapPosition   = Utils.moveTowards(self.leftFlapPosition, leftTarget, dt/self.leftFlapDuration);
 385          Animation.sampleTime(self.snowGroomerLeftFlap, self.leftFlapPosition * self.leftFlapDuration);
 386      end;
 387      if self.snowGroomerRightFlap ~= nil and (math.abs(self.rightFlapPosition - rightTarget) > 1e-3 or forceUpdate) then
 388          self.rightFlapPosition  = Utils.moveTowards(self.rightFlapPosition, rightTarget, dt/self.rightFlapDuration);
 389          Animation.sampleTime(self.snowGroomerRightFlap, self.rightFlapPosition * self.rightFlapDuration);
 390      end;
 391  
 392      if update then
 393          self:updateSnowGroomerLimits();
 394      end;
 395  
 396      -- prepare
 397      local maxBridgingDistanceSquare = 9;
 398      if g_isMaster and self.snowGroomerPos > 0.5 then
 399          local totalPosCenter;
 400          if self.useRectangularPrepAreas then
 401              for k, v in pairs(self.snowGroomerPrepAreas) do
 402                  local quality           = v.quality;
 403  
 404                  local bl                = VectorUtils.getWorldPosition(v.backLeft);
 405                  local br                = VectorUtils.getWorldPosition(v.backRight);
 406                  local fl                = VectorUtils.getWorldPosition(v.frontLeft);
 407                  local fr                = VectorUtils.getWorldPosition(v.frontRight);
 408  
 409                  local lastBl            = self.snowGroomerPrepAreasLastPositions[k].lastBackLeft;
 410                  local lastBr            = self.snowGroomerPrepAreasLastPositions[k].lastBackRight;
 411                  
 412                  if Vector3.sqrMagnitude(lastBl - bl) < maxBridgingDistanceSquare then
 413                      local posCenter     = (lastBl + lastBr + fl):multiply(0.33333);
 414                      if totalPosCenter == nil then
 415                          totalPosCenter  = posCenter;
 416                      end;
 417  
 418                      -- groom every 7 cm
 419                      local delta         = v.lastPositionCenter - posCenter;
 420                      local maxSqrMag     = 0.07*0.07;
 421  
 422                      if delta:sqrMagnitude() > maxSqrMag then
 423  
 424                          local forward;
 425  
 426                          if not Vector3.isEqual(v.lastPositionCenter, Vector3.zero) then
 427                              -- angle depends on direction of last movement
 428                              forward     = delta;
 429                          else
 430                              forward     = VectorUtils.transformDirection(v.backLeft, Vector3.forward);
 431                          end;
 432  
 433                          v.lastPositionCenter = posCenter;
 434  
 435                          local angle     = (math.deg(math.atan2(-forward.z, forward.x)) - 90) % 360;
 436                          SnowSystem.groomTriangleByPosition(lastBl,  lastBr, fl,     angle,  v.smoothRadius, quality);
 437                          SnowSystem.groomTriangleByPosition(fl,      fr,     lastBr, angle,  v.smoothRadius, quality);
 438  
 439                          -- send an event in MP
 440                          if g_isServer then
 441                              EventGroomTriangle:send(quality,    v.smoothRadius, angle,  lastBl, lastBr, fl);
 442                              EventGroomTriangle:send(quality,    v.smoothRadius, angle,  fl,     fr,     lastBr);
 443                          end;
 444  
 445                      end;
 446                  end;
 447  
 448                  self.snowGroomerPrepAreasLastPositions[k].lastBackLeft      = bl;
 449                  self.snowGroomerPrepAreasLastPositions[k].lastBackRight     = br;
 450                  self.snowGroomerPrepAreasLastPositions[k].lastFrontLeft     = fl;
 451                  self.snowGroomerPrepAreasLastPositions[k].lastFrontRight        = fr;
 452              end;
 453          else
 454              -- legacy snow groomer areas from Season 1 (with low-FPS issues)
 455              for k, v in pairs(self.snowGroomerPrepAreas) do
 456                  local quality           = v.quality;
 457  
 458                  -- slip is currently unused due to irritating rendering results
 459                  --[[if v.slipReferenceId ~= nil then
 460                      -- coordinates of last x/y/z position relative to current position
 461                      local lx,ly,lz      = Rigidbody.getVelocityAtPoint(self.mainId, getWorldPosition(v.slipReferenceId));
 462                      lx, ly, lz          = Utils.inverseTransformDirection(self.groomerRaycastOrigin or v.slipReferenceId, lx,ly,lz);
 463                      local slip          = math.abs(lx) - math.abs(lz);
 464                      --printf("slip %.2f %.2f %.2f => %.2f", lx,ly,lz, slip);
 465  
 466                      if slip > 0.7 then
 467                          quality         = clamp(quality - 12 * slip, 0, 100);
 468                          printf("slip %.2f quality %.2f", slip, quality);
 469                      end;
 470  
 471                      --v.lastPosition        = VectorUtils.getWorldPosition(v.slipReferenceId);
 472                  end;
 
 473  
 474                  local pos1              = VectorUtils.getWorldPosition(v.id1);
 475                  local pos2              = VectorUtils.getWorldPosition(v.id2);
 476                  local pos3              = VectorUtils.getWorldPosition(v.id3);
 477                  local posCenter         = (pos1 + pos2 + pos3):multiply(0.3333);
 478                  if totalPosCenter == nil then
 479                      totalPosCenter      = posCenter;
 480                  end;
 481  
 482                  -- groom every 7 cm
 483                  local delta             = v.lastPositionCenter - posCenter;
 484                  local maxSqrMag         = 0.07*0.07;
 485  
 486                  if delta:sqrMagnitude() > maxSqrMag then
 487                      local forward;
 488  
 489                      if not Vector3.isEqual(v.lastPositionCenter, Vector3.zero) then
 490                          -- angle depends on direction of last movement
 491                          forward         = delta;
 492                      else
 493                          forward         = VectorUtils.transformDirection(v.id1, Vector3.forward);
 494                      end;
 495  
 496                      v.lastPositionCenter = posCenter;
 497  
 498                      local angle         = (math.deg(math.atan2(-forward.z, forward.x)) - 90) % 360;
 499                      SnowSystem.groomTriangleByPosition(pos1, pos2, pos3, angle, v.smoothRadius, quality);
 500  
 501                      -- send an event in MP
 502                      if g_isServer then
 503                          EventGroomTriangle:send(quality, v.smoothRadius, angle, pos1, pos2, pos3);
 504                      end;
 505                  end;
 506              end;
 507          end;
 508  
 509          -- check whether the slope has been bought (only on servers)
 510          local slopeNotOwned         = false;
 511          local slope;
 512          if g_scenario ~= nil and g_scenario.skiResort ~= nil then
 513              local snowfall, hardness, unused2, layer    = SnowSystem.getInfoAtPosition(VectorUtils.getWorldPosition(self.mainId));
 514  
 515              slopeNotOwned, slope    = g_scenario.skiResort:getIsSlopeOwnedByLayer(layer);
 516          end;
 517  
 518          if self:getIsInputActive() and slopeNotOwned == false and BottomWarningHUD.showSlopeNotBoughtWarning() then
 519              if slope ~= nil and self.bottomWarningHUD == nil then
 520                  self.bottomWarningHUD   = g_GUI:showHUD(BottomWarningHUD, l10n.format("msg_slopeNotBought", slope.slope));
 521              end;
 522  
 523          elseif self.bottomWarningHUD ~= nil then
 524              local hud               = self.bottomWarningHUD;
 525              self.bottomWarningHUD   = nil;
 526              hud:close();
 527          end;
 528      end;
 529  
 530      -- animate rotating parts
 531      if self.groomerRotatingParts ~= nil and self.snowGroomerPos > 0.9 and self.isMotorOn then
 532          for k, rp in pairs(self.groomerRotatingParts) do
 533              if rp.axis == 1 then
 534                  rotate(rp.id, rp.speed * dt, 0,0);
 535  
 536              elseif rp.axis == 2 then
 537                  rotate(rp.id, 0, rp.speed * dt, 0);
 538  
 539              else
 540                  rotate(rp.id, 0,0, rp.speed * dt);
 541              end;
 542          end;
 543      end;
 544  end;

SnowGroomer:setIsSnowGroomerIsLowered(isLowered, noEvent)

Lower/lift the groomer, depending on isLowered (bool). noEvent (bool) specifies whether the multiplayer event shall be suppressed.

 547  function SnowGroomer:setIsSnowGroomerIsLowered(isLowered, noEvent)
 548      self.snowGroomerIsLowered           = isLowered or false;
 549  
 550      if not noEvent then
 551          EventSetSnowGroomerPosition:send(self, self.snowGroomerIsLowered);
 552      end;
 553  end;

SnowGroomer:setIsSnowGroomerIsLocked(isLocked, noEvent)

Lock/release the groomer, depending on isLocked (bool). noEvent (bool) specifies whether the multiplayer event shall be suppressed.

 556  function SnowGroomer:setIsSnowGroomerIsLocked(isLocked, noEvent)
 557      self.snowGroomerIsLocked            = isLocked or false;
 558  
 559      if not noEvent then
 560          EventSetSnowGroomerLocked:send(self, self.snowGroomerIsLocked);
 561      end;
 562  end;

SnowGroomer:setSnowGroomerFlaps(left, right, noEvent)

Fold out the left and the right flap (both bool). true means the flap shall be moved out. noEvent (bool) specifies whether the multiplayer event shall be suppressed.

 565  function SnowGroomer:setSnowGroomerFlaps(left, right, noEvent)
 566      if left ~= nil then     self.leftFlapTarget         = left and 1 or 0;      end;
 567      if right ~= nil then    self.rightFlapTarget        = right and 1 or 0;     end;
 568  
 569      if not noEvent then
 570          EventSetSnowGroomerFlaps:send(self, self.leftFlapTarget == 1, self.rightFlapTarget == 1);
 571      end;
 572  end;

SnowGroomer:setSnowGroomerRotation(rotation, noEvent)

Adjusts the groomer rotation (left/right) to the specified target value rotation (number). noEvent (bool) specifies whether the multiplayer event shall be suppressed.

 575  function SnowGroomer:setSnowGroomerRotation(rotation, noEvent)
 576  
 577      if rotation ~= nil then
 578          self.groomerRotation        = rotation;
 579      end;
 580  
 581      if not noEvent then
 582          EventSetSnowGroomerRotation:send(self, rotation);
 583      end;
 584  end;

SnowGroomer:updateSnowGroomerLimits()

Passes on the axis target values and limits to the C# class SnowGroomerIK, which will then solve the 4D inverse kinematics.

 588  function SnowGroomer:updateSnowGroomerLimits()
 589      if self.snowGroomerIK == nil then return end;
 590  
 591      local weight                    = self.snowGroomerPos;
 592      local axis1Weight               = clamp01(weight - self.snowGroomerLockedPos);
 593  
 594      SnowGroomerIK.setAxis1Limit(self.snowGroomerIK, lerp(self.snowGroomerLifted.axis1, self.snowGroomerLowered.axis1Limit, axis1Weight));
 595      SnowGroomerIK.setAxis2Limit(self.snowGroomerIK,
 596          lerp(self.snowGroomerLifted.axis2, self.snowGroomerLowered.axis2MinLimit, weight),
 597          lerp(self.snowGroomerLifted.axis2, self.snowGroomerLowered.axis2MaxLimit, weight)
 598      );
 599      SnowGroomerIK.setAxis3Limit(self.snowGroomerIK,
 600          lerp(self.snowGroomerLifted.axis3, self.snowGroomerLowered.axis3MinLimit, weight),
 601          lerp(self.snowGroomerLifted.axis3, self.snowGroomerLowered.axis3MaxLimit, weight)
 602      );
 603      SnowGroomerIK.setAxis4Limit(self.snowGroomerIK,
 604          lerp(self.snowGroomerLifted.axis4, self.snowGroomerLowered.axis4MinLimit, weight),
 605          lerp(self.snowGroomerLifted.axis4, self.snowGroomerLowered.axis4MaxLimit, weight)
 606      );
 607  end;

SnowGroomer:onLeave(player, isLocalPlayer)

Disables the warning if a slope has not yet been bought whenever the player exits the vehicle.

 611  function SnowGroomer:onLeave(player, isLocalPlayer)
 612      if isLocalPlayer and self.bottomWarningHUD ~= nil then
 613          local hud               = self.bottomWarningHUD;
 614          self.bottomWarningHUD   = nil;
 615          hud:close();
 616      end;
 617  end;

SnowGroomer:writeResync()

Resynchronizes all variables when a new player joins the game. The data sent by writeResync will be received by readResync.

 621  function SnowGroomer:writeResync()
 622      streamWriteBool(self.snowGroomerIsLowered);
 623      streamWriteBool(self.snowGroomerIsLocked);
 624      if self.leftFlapTarget ~= nil or self.rightFlapTarget ~= nil then
 625          streamWriteBool(self.leftFlapTarget == 0);
 626          streamWriteBool(self.rightFlapTarget == 0);
 627      end;
 628  end;

SnowGroomer:readResync()

Resynchronizes all variables when a new player joins the game. The data sent by writeResync will be received by readResync.

 632  function SnowGroomer:readResync()
 633      local isLowered         = streamReadBool();
 634      self:setIsSnowGroomerIsLowered(isLowered, true);
 635  
 636      local isLocked          = streamReadBool()
 637      self:setIsSnowGroomerIsLocked(isLocked, true);
 638  
 639      local leftFlap          = streamReadBool();
 640      local rightFlap         = streamReadBool();
 641      self:setSnowGroomerFlaps(leftFlap, rightFlap, true);
 642  end;

All contents of this page may be used for modding use for Winter Resort Simulator - Season 2 only. Any use exceeding this regulation is not permitted.

Copyright (C) HR Innoways, 2021. All Rights Reserved.