Vehicles/VehicleScripts/SnowCannon.lua

SnowCannon handles all vehicle-related features of a snow cannon. A single vehicle may contain more than just one snow cannon (e.g. a twin snow cannon, or maybe even some separately controllable nozzles).

  25  SnowCannon                          = SnowCannon or {};
  26  SnowSystem.SNOW_HEIGHTSTEP          = 2 / 255;

SnowCannon:load(dataTable)

First, we set up all functions required by this vehicle script. Functions with return values are not supported by VehicleManager:newFunction(), therefore these are assigned directly.

Afterwards, we read some configuration for all the snow cannons that are available on this vehicle, and initialize them.

  32  function SnowCannon:load(dataTable)
  33      self.setIsCannonTurnedOn        = VehicleManager:newFunction("setIsCannonTurnedOn");
  34      self.setSnowCannonGroupId       = VehicleManager:newFunction("setSnowCannonGroupId");
  35      self.setCannonRotationX         = VehicleManager:newFunction("setCannonRotationX");
  36      self.setCannonRotationY         = VehicleManager:newFunction("setCannonRotationY");
  37      self.snowCannonRemoteCallback   = VehicleManager:newFunction("snowCannonRemoteCallback")
  38      self.snowCannonRemoteMove       = VehicleManager:newFunction("snowCannonRemoteMove")
  39      self.getSnowCannonRemoteButton  = SnowCannon.getSnowCannonRemoteButton;
  40      self.getMayTurnCannonOn         = SnowCannon.getMayTurnCannonOn;
  41  
  42      self.snowCannons                = {};
  43      self.snowCannonGroupId          = 0; -- to be overridden by Escape Menu GUI
  44  
  45      for n, v in pairs(dataTable.snowCannons) do
  46          -- note: rotation is controlled via separate scripts (as this function is shared e.g. with the blade)
  47          -- we only handle starting/stopping, particle system and snow spawning
  48          local spawnIndex            = v.snowSpawnIndex or "";
  49          local propellerIndex        = v.propellerIndex or "";
  50  
  51          if cannonIndex == "" then break else
  52              local cannon            = {};
  53              cannon.snowSpawnId      = getChild(self.id, spawnIndex);
  54              cannon.controlId        = getChild(self.id, v.controlIndex or "")
  55              cannon.cameraId         = getChild(self.id, v.cameraIndex or "");
  56  
  57              if propellerIndex ~= "" then
  58                  cannon.propellerId          = getChild(self.id, propellerIndex);
  59                  cannon.propellerMaxSpeed    = (v.propellerSpeed or 300)/60 * 360; -- rpm (here converted to degrees per second)
  60                  cannon.propellerSpeed       = 0;
  61              end;
  62  
  63              if v.rotX ~= nil then
  64                  cannon.rotXId       = getChild(self.id, v.rotX.index);
  65                  cannon.rotXmin      = v.rotX.min or 0;
  66                  cannon.rotXmax      = v.rotX.max or 45;
  67                  cannon.rotXSpeed    = v.rotX.speed or 15;
  68                  cannon.rotXAttach   = v.rotX.attachValue;
  69                  
  70                  local x,y,z         = getRotation(cannon.rotXId);
  71                  cannon.rotX         = x;
  72              end;
  73              if v.rotY ~= nil then
  74                  cannon.rotYId       = getChild(self.id, v.rotY.index);
  75                  cannon.rotYmin      = v.rotY.min;
  76                  cannon.rotYmax      = v.rotY.max;
  77                  cannon.rotYSpeed    = v.rotY.speed or 30;
  78                  cannon.rotYAttach   = v.rotY.attachValue;
  79                  
  80                  local x,y,z         = getRotation(cannon.rotYId);
  81                  cannon.rotY         = y;
  82              end;
  83  
  84              cannon.particleSystems  = {};
  85              if v.particleSystems ~= nil then
  86                  for _, str in pairs(v.particleSystems) do
  87                      local particleId    = getChild(self.id, str);
  88                      ParticleSystem.stop(particleId);
  89  
  90                      table.insert(cannon.particleSystems, particleId);
  91                  end;
  92              end;
  93              cannon.particleSystemsPlaying   = false;
  94  
  95              if v.idleSound ~= nil then
  96                  cannon.idleSoundId          = Utils.loadBundleGameObject(self.bundleId, v.idleSound);
  97                  setParent(cannon.idleSoundId, cannon.snowSpawnId);
  98                  setPosition(cannon.idleSoundId, 0,0,0);
  99                  AudioSource.stop(cannon.idleSoundId);
 100              end;
 101  
 102              cannon.isTurnedOn           = false;
 103              cannon.inputKey             = v.inputKey or "SnowCannon_TurnOnOff";
 104              cannon.rayLength            = v.throwDistance or v.rayLength or 15;
 105              cannon.startupTimer         = 0;
 106              cannon.startupDuration      = getNoNil(v.startupDuration,       5); -- wait some time before snow is spawned
 107              cannon.spreadFactor         = getNoNil(v.spreadFactor,          1.5);
 108              cannon.minSpreadRadius      = getNoNil(v.minSpreadRadius,       8);
 109              cannon.maxSpreadRadius      = getNoNil(v.maxSpreadRadius,       13);
 110              cannon.volumePerSecond      = getNoNil(v.volumePerHour,         500)/3600; -- in m³ per hour
 111              cannon.pricePerCubic        = getNoNil(v.pricePerCubic,         0.3); -- should be a realistic value
 112              cannon.cumulatedVolume      = 0; -- cumulated volume of snow spawned already
 113  
 114              self.snowCannons[n]         = cannon;
 115          end;
 116      end;
 117  
 118      SnowmakingManager:registerVehicle(self);
 119  end;

SnowCannon:update(dt)

There are multiple things that have to be updated every frame:

  • If the player is active (g_scenario.player.isActive), we need to check whether the player wants to switch the cannon on or off, or maybe wants to rotate the cannon.
  • If the cannon is turned on, it should start emitting snow after some time, i.e. we have to update some particle effects.
  • The actual process of creating snow also has to be updated.
 126  function SnowCannon:update(dt)
 127      -- alright, first animate all cannons
 128      local player                    = g_scenario.player;
 129  
 130      for k, cannon in pairs(self.snowCannons) do
 131          -- animate propeller in x axis
 132          if cannon.propellerId ~= nil then
 133              cannon.propellerSpeed   = Utils.moveTowards(cannon.propellerSpeed, cannon.isTurnedOn and cannon.propellerMaxSpeed or 0, cannon.propellerMaxSpeed * dt);
 134              if cannon.propellerSpeed ~= nil then
 135                  rotate(cannon.propellerId, 0,0, cannon.propellerSpeed * dt);
 136              end;
 137          end;
 138  
 139          if SnowmakingManager:getCannonActive(self) then
 140          
 141              -- check if the player wants to toggle on/off the cannon
 142              local inputKey              = InputMapper[cannon.inputKey];
 143              if InputMapper:getKeyDown(inputKey) then
 144                  self:setIsCannonTurnedOn(k, not cannon.isTurnedOn);
 145              end;
 146  
 147              -- key hint
 148              if self:getMayTurnCannonOn(k) then
 149                  g_GUI:addKeyHint(inputKey, l10n.get(cannon.isTurnedOn and "Input_SnowCannon_TurnOff" or "Input_SnowCannon_TurnOn"));
 150              end;
 151  
 152              -- let the player turn the cannon
 153              if cannon.rotYId ~= nil then
 154                  if InputMapper:getKey(InputMapper.SnowCannon_RotateLeft) then
 155                      self:setCannonRotationY(k, cannon.rotY - cannon.rotYSpeed * dt);
 156  
 157                  elseif InputMapper:getKey(InputMapper.SnowCannon_RotateRight) then
 158                      self:setCannonRotationY(k, cannon.rotY + cannon.rotYSpeed * dt);
 159                  end;
 160                  g_GUI:addDoubleKeyHint(InputMapper.SnowCannon_RotateLeft, InputMapper.SnowCannon_RotateRight, l10n.get("SnowCannon_RotateLeftRight"));
 161              end;
 162              
 163              if cannon.rotXId ~= nil then
 164                  if InputMapper:getKey(InputMapper.SnowCannon_RotateUp) then
 165                      self:setCannonRotationX(k, cannon.rotX + cannon.rotXSpeed * dt);
 166  
 167                  elseif InputMapper:getKey(InputMapper.SnowCannon_RotateDown) then
 168                      self:setCannonRotationX(k, cannon.rotX - cannon.rotXSpeed * dt);
 169                  end;
 170                  g_GUI:addDoubleKeyHint(InputMapper.SnowCannon_RotateUp, InputMapper.SnowCannon_RotateDown, l10n.get("SnowCannon_RotateUpDown"));
 171              end;
 172          end;
 173  
 174          if g_isMaster and cannon.isTurnedOn and not self:getMayTurnCannonOn(k) then
 175              self:setIsCannonTurnedOn(k, false);
 176          end;
 177  
 178          if cannon.isTurnedOn then
 179              cannon.startupTimer     = cannon.startupTimer + dt;
 180  
 181              if cannon.startupTimer >= cannon.startupDuration then
 182  
 183                  -- start playing the particle systems
 184                  if not cannon.particleSystemsPlaying then
 185                      for _, id in pairs(cannon.particleSystems) do
 186                          ParticleSystem.play(id);
 187                      end;
 188  
 189                      if cannon.idleSoundId ~= nil then
 190                          AudioSource.play(cannon.idleSoundId);
 191                      end;
 192  
 193                      -- don't call this again
 194                      cannon.particleSystemsPlaying   = true;
 195                  end;
 196                  
 197                  if g_isMaster then
 198                      -- we're spawning some snow!
 199                      -- get the world position and direction of our cannon
 200                      local x,y,z         = getWorldPosition(cannon.snowSpawnId);
 201                      local dx,dy,dz      = Utils.transformDirection(cannon.snowSpawnId, 0,0,1); -- yeet snow in +Z direction
 202  
 203                      -- snow focuses on a fixed point that is located some metres (rayLength) in front of our snowSpawnId
 204                      local rayLength     = cannon.rayLength;
 205                      x,y,z               = x + rayLength*dx, y + rayLength*dy, z + rayLength*dz;
 206  
 207                      local terrainDelta  = y - Utils.sampleTerrainHeight(x, z);
 208  
 209                      -- let's assume snow to spread in linear way:
 210                      -- if emitted e.g. 5 metres above terrain, it will spread in a radius of e.g. 5 metres (=> diametre 10 m!)
 211                      -- this can be customised using the cannon's spread factor
 212                      -- this is approximated using a slightly modified cone volume formula (V = 1/3 * r^2 * pi * h)
 213                      cannon.cumulatedVolume  = cannon.cumulatedVolume + cannon.volumePerSecond * GameplaySettings.snowmakingSpeedCoeff * dt;
 214  
 215                      local spreadRadius  = clamp(terrainDelta * cannon.spreadFactor, cannon.minSpreadRadius, cannon.maxSpreadRadius);
 216                      
 217                      -- snow system distributes with distribution y(x) = (1-x^2) with x=0..1 over the radius
 218                      -- volume of such a rotation body is V = h * pi * r^2 / 2 (determined using Guldin's formulas)
 219                      -- => following line determines the maximum height of the pile, and this must be at least 10 times the minimum snow step
 220                      local maxDeltaHeight    = 2 * cannon.cumulatedVolume / math.pi / spreadRadius^2;
 221  
 222                      -- spawn if delta height is larger than 2x step size
 223                      if maxDeltaHeight >= 10*SnowSystem.SNOW_HEIGHTSTEP then
 224                          local price     = cannon.cumulatedVolume * cannon.pricePerCubic;
 225                          
 226                          if g_scenario:canAffordExpense(price) then
 227                              -- pass this on to snow system
 228                              SnowSystem.addSnowCircular(x, maxDeltaHeight, z, spreadRadius);
 229                              g_scenario:addSnowmakingExpense(price);
 230  
 231                              if g_isServer then
 232                                  EventAddSnowCircular:send(x, maxDeltaHeight, z, spreadRadius);
 233                              end;
 234                          else
 235                              -- shut cannon off
 236                              self:setIsCannonTurnedOn(k, false);
 237                          end;
 238                          cannon.cumulatedVolume  = 0;
 239  
 240                      end;
 241                  end;
 242              end;
 243          end;
 244      end;
 245  end;

SnowCannon:saveToTable(tbl)

Saves all relevant variables.

 249  function SnowCannon:saveToTable(tbl)
 250      if tbl == nil then return end;
 251  
 252      tbl.snowCannonGroupId       = self.snowCannonGroupId;
 253      tbl.snowCannons             = {};
 254  
 255      for n, cannon in pairs(self.snowCannons) do
 256          tbl.snowCannons[n]      = {
 257              isTurnedOn          = cannon.isTurnedOn,
 258              cumulatedVolume     = cannon.cumulatedVolume, -- save this to avoid all cannons "eating" money at once
 259              propellerSpeed      = cannon.propellerSpeed,
 260              startupTimer        = cannon.startupTimer,
 261              rotX                = cannon.rotX,
 262              rotY                = cannon.rotY,
 263          };
 264      end;
 265  end;

SnowCannon:loadFromTable(tbl)

Restores all values from the savegame.

 269  function SnowCannon:loadFromTable(tbl)
 270      if tbl == nil then return end;
 271  
 272      self.snowCannonGroupId      = getNoNil(tbl.snowCannonGroupId, self.snowCannonGroupId);
 273  
 274      if tbl.snowCannons == nil then return end;
 275  
 276      for n, cannon in pairs(self.snowCannons) do
 277          local savedCannon       = tbl.snowCannons[n];
 278  
 279          if savedCannon ~= nil then
 280              cannon.isTurnedOn       = getNoNil(savedCannon.isTurnedOn,          false);
 281              cannon.cumulatedVolume  = getNoNil(savedCannon.cumulatedVolume,     cannon.cumulatedVolume);
 282              cannon.propellerSpeed   = getNoNil(savedCannon.propellerSpeed,      cannon.propellerSpeed);
 283              cannon.startupTimer     = getNoNil(savedCannon.startupTimer,        cannon.startupTimer);
 284  
 285              if savedCannon.rotX ~= nil and cannon.rotXId ~= nil then
 286                  self:setCannonRotationX(n, savedCannon.rotX);
 287              end;
 288              if savedCannon.rotY ~= nil and cannon.rotYId ~= nil then
 289                  self:setCannonRotationY(n, savedCannon.rotY);
 290              end;
 291          end;
 292  
 293          -- apply that (to make sure everything is alright)
 294          self:setIsCannonTurnedOn(n, cannon.isTurnedOn, true);
 295      end;
 296  end;

SnowCannon:setIsCannonTurnedOn(cannonIndex, state, noEvent)

Call this to turn the snow cannon number cannonIndex (int, starting with 1) on or off, depending on state (bool). noEvent (bool) specifies whether the multiplayer event shall be suppressed.

The corresponding network event is EventSetCannonTurnedOn (to be found in EventSetAnimatedPart).

 302  function SnowCannon:setIsCannonTurnedOn(cannonIndex, state, noEvent)
 303      -- don't allow the player to turn on the cannon if he doesn't have enough cash
 304      if state and not g_scenario:canAffordExpense(0) then
 305          state                       = false;
 306      end;
 307      
 308      if cannonIndex == nil then
 309          -- apply this to all cannons
 310          for k, _ in pairs(self.snowCannons) do
 311              -- k cannot be nil since a table index is never nil => no danger of an infinite loop
 312              self:setIsCannonTurnedOn(k, state, noEvent);
 313          end;
 314          return;
 315      end;
 316      
 317      local cannon                    = self.snowCannons[cannonIndex];
 318      if cannon == nil then return end;
 319  
 320      cannon.isTurnedOn               = state and (g_isClient or self:getMayTurnCannonOn(cannonIndex));
 321  
 322      if not state then
 323          -- stop emitting particles
 324          for _, id in pairs(cannon.particleSystems) do
 325              ParticleSystem.stop(id);
 326          end;
 327          cannon.particleSystemsPlaying   = false;
 328  
 329          cannon.startupTimer         = 0;
 330          cannon.cumulatedVolume      = 0;
 331          
 332          if cannon.idleSoundId ~= nil then
 333              AudioSource.stop(cannon.idleSoundId);
 334          end;
 335      end;
 336  
 337      if not noEvent then
 338          EventSetCannonTurnedOn:send(self, cannonIndex, state);
 339      end;
 340  end;

SnowCannon:getMayTurnCannonOn(cannonIndex)

Returns whether the snow cannon may be turned on. This is always allowed unless the snow cannon is attached to another vehicle.

 345  function SnowCannon:getMayTurnCannonOn(cannonIndex)
 346      return self.attacherMasterVehicle == nil;
 347  end;

SnowCannon:onAttach()

Rotate the snow cannon to a specific position if rotXAttach or rotYAttach are enabled in the cannon's configuration table. These will not be required for most cannons.

 353  function SnowCannon:onAttach()
 354      for k, v in pairs(self.snowCannons) do
 355          self:setIsCannonTurnedOn(k, false);
 356  
 357          if v.rotXAttach ~= nil then
 358              self:setCannonRotationX(k, v.rotXAttach);
 359          end;
 360  
 361          if v.rotYAttach ~= nil then
 362              self:setCannonRotationY(k, v.rotYAttach);
 363          end;
 364      end;
 365  end;

SnowCannon:setSnowCannonGroupId(groupId, noEvent)

Makes the snow cannon join group number groupId (int). 0 means the snow cannon does not belong to any group.

 369  function SnowCannon:setSnowCannonGroupId(groupId, noEvent)
 370      self.snowCannonGroupId          = math.max(groupId or 0, 0);
 371  
 372      if not noEvent then
 373          EventSetCannonGroupId:send(self, groupId);
 374      end;
 375  end;

SnowCannon:getSnowCannonRemoteButton()

Returns the button description that is displayed in the overview menu's snowmaking section (if this snow cannon is selected).

 379  function SnowCannon:getSnowCannonRemoteButton()
 380      if self.snowCannons == nil or self.snowCannons[1] == nil then
 381          return "Invalid";
 382      end;
 383  
 384      return l10n.get(self.snowCannons[1].isTurnedOn and "ui_esc_snowCannonRemote1" or "ui_esc_snowCannonRemote0");
 385  end;

SnowCannon:snowCannonRemoteCallback()

Is called every time the player clicks the start/stop button in the overview menu's snowmaking section. The button's label is specified by the return value of SnowCannon:getSnowCannonRemoteButton().

 389  function SnowCannon:snowCannonRemoteCallback()
 390      if self.snowCannons == nil or self.snowCannons[1] == nil then return end;
 391      
 392      self:setIsCannonTurnedOn(1, not self.snowCannons[1].isTurnedOn);
 393  end;

SnowCannon:snowCannonRemoteMove(deltaX, deltaY)

Allows to rotate the snow cannon via the overview menu's snowmaking section.

 397  function SnowCannon:snowCannonRemoteMove(deltaX, deltaY)
 398      local cannon                    = self.snowCannons[1];
 399      if cannon == nil then return end;
 400  
 401      local dt                        = 1/30;
 402  
 403      if deltaY ~= nil and deltaY ~= 0 and cannon.rotYId ~= nil then
 404          self:setCannonRotationX(1, cannon.rotX + cannon.rotXSpeed * dt * deltaY);
 405      end;
 406      if deltaX ~= nil and deltaX ~= 0 and cannon.rotXId ~= nil then
 407          self:setCannonRotationY(1, cannon.rotY + cannon.rotYSpeed * dt * deltaX);
 408      end;
 409  end;

SnowCannon:setCannonRotationX(cannonIndex, rotX, noEvent)

Rotates the snow cannon along its X rotation axis.

 413  function SnowCannon:setCannonRotationX(cannonIndex, rotX, noEvent)
 414      local cannon                    = self.snowCannons[cannonIndex];
 415      if cannon == nil then return end;
 416  
 417      if cannon.rotXId ~= nil then
 418          cannon.rotX                 = clamp(rotX, cannon.rotXmin, cannon.rotXmax);
 419          setRotationX(cannon.rotXId, cannon.rotX);
 420      end;
 421  
 422      if not noEvent then
 423          EventSetCannonRotation:send(self, cannonIndex, 0, rotX);
 424      end;
 425  end;

SnowCannon:setCannonRotationY(cannonIndex, rotY, noEvent)

Rotates the snow cannon along its Y rotation axis.

 429  function SnowCannon:setCannonRotationY(cannonIndex, rotY, noEvent)
 430      local cannon                    = self.snowCannons[cannonIndex];
 431      if cannon == nil then return end;
 432  
 433      if cannon.rotYId ~= nil then
 434          cannon.rotY                 = clamp(rotY, cannon.rotYmin, cannon.rotYmax);
 435          setRotationY(cannon.rotYId, cannon.rotY);
 436      end;
 437  
 438      if not noEvent then
 439          EventSetCannonRotation:send(self, cannonIndex, 1, rotY);
 440      end;
 441  end;

SnowCannon:destroy()

 443  function SnowCannon:destroy()
 444      SnowmakingManager:unregisterVehicle(self);
 445  end;

SnowCannon:writeResync()

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

 449  function SnowCannon:writeResync()
 450      for k, cannon in ipairs(self.snowCannons) do
 451          streamWriteBool(cannon.isTurnedOn);
 452          
 453          if cannon.isTurnedOn then
 454              streamWriteFloat(cannon.startupTimer);
 455          end;
 456          if cannon.rotXId ~= nil then
 457              streamWriteFloat(cannon.rotX);
 458          end;
 459          if cannon.rotYId ~= nil then
 460              streamWriteFloat(cannon.rotY);
 461          end;
 462      end;
 463  end;

SnowCannon:readResync()

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

 467  function SnowCannon:readResync()
 468      for k, cannon in ipairs(self.snowCannons) do
 469          local isOn                  = streamReadBool(cannon.isTurnedOn);
 470          self:setIsCannonTurnedOn(k, isOn, true);
 471          
 472          if isOn then
 473              cannon.startupTimer     = streamReadFloat();
 474          end;
 475  
 476          if cannon.rotXId ~= nil then
 477              self:setCannonRotationX(k, streamReadFloat(), true);
 478          end;
 479          if cannon.rotYId ~= nil then
 480              self:setCannonRotationY(k, streamReadFloat(), true);
 481          end;
 482      end;
 483  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.