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;
  27  SnowCannon.CONTROL_RANGE            = 5*5;

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.

  33  function SnowCannon:load(dataTable)
  34      self.setIsCannonTurnedOn        = VehicleManager:newFunction("setIsCannonTurnedOn");
  35      self.setSnowCannonGroupId       = VehicleManager:newFunction("setSnowCannonGroupId");
  36      self.setCannonRotationX         = VehicleManager:newFunction("setCannonRotationX");
  37      self.setCannonRotationY         = VehicleManager:newFunction("setCannonRotationY");
  38      self.snowCannonRemoteCallback   = VehicleManager:newFunction("snowCannonRemoteCallback")
  39      self.snowCannonRemoteMove       = VehicleManager:newFunction("snowCannonRemoteMove")
  40      self.getSnowCannonRemoteButton  = SnowCannon.getSnowCannonRemoteButton;
  41      self.getMayTurnCannonOn         = SnowCannon.getMayTurnCannonOn;
  42  
  43      self.snowCannons                = {};
  44      self.snowCannonGroupId          = 0; -- to be overridden by Escape Menu GUI
  45  
  46      for n, v in pairs(dataTable.snowCannons) do
  47          -- note: rotation is controlled via separate scripts (as this function is shared e.g. with the blade)
  48          -- we only handle starting/stopping, particle system and snow spawning
  49          local spawnIndex            = v.snowSpawnIndex or "";
  50          local propellerIndex        = v.propellerIndex or "";
  51  
  52          if cannonIndex == "" then break else
  53              local cannon            = {};
  54              cannon.snowSpawnId      = getChild(self.id, spawnIndex);
  55              cannon.controlId        = getChild(self.id, v.controlIndex or "")
  56              cannon.cameraId         = getChild(self.id, v.cameraIndex or "");
  57  
  58              if propellerIndex ~= "" then
  59                  cannon.propellerId          = getChild(self.id, propellerIndex);
  60                  cannon.propellerMaxSpeed    = (v.propellerSpeed or 300)/60 * 360; -- rpm (here converted to degrees per second)
  61                  cannon.propellerSpeed       = 0;
  62              end;
  63  
  64              if v.rotX ~= nil then
  65                  cannon.rotXId       = getChild(self.id, v.rotX.index);
  66                  cannon.rotXmin      = v.rotX.min or 0;
  67                  cannon.rotXmax      = v.rotX.max or 45;
  68                  cannon.rotXSpeed    = v.rotX.speed or 15;
  69                  cannon.rotXAttach   = v.rotX.attachValue;
  70                  
  71                  local x,y,z         = getRotation(cannon.rotXId);
  72                  cannon.rotX         = x;
  73              end;
  74              if v.rotY ~= nil then
  75                  cannon.rotYId       = getChild(self.id, v.rotY.index);
  76                  cannon.rotYmin      = v.rotY.min;
  77                  cannon.rotYmax      = v.rotY.max;
  78                  cannon.rotYSpeed    = v.rotY.speed or 30;
  79                  cannon.rotYAttach   = v.rotY.attachValue;
  80                  
  81                  local x,y,z         = getRotation(cannon.rotYId);
  82                  cannon.rotY         = y;
  83              end;
  84  
  85              cannon.particleSystems  = {};
  86              if v.particleSystems ~= nil then
  87                  for _, str in pairs(v.particleSystems) do
  88                      local particleId    = getChild(self.id, str);
  89                      ParticleSystem.stop(particleId);
  90  
  91                      table.insert(cannon.particleSystems, particleId);
  92                  end;
  93              end;
  94              cannon.particleSystemsPlaying   = false;
  95  
  96              if v.idleSound ~= nil then
  97                  cannon.idleSoundId          = Utils.loadBundleGameObject(self.bundleId, v.idleSound);
  98                  setParent(cannon.idleSoundId, cannon.snowSpawnId);
  99                  setPosition(cannon.idleSoundId, 0,0,0);
 100                  AudioSource.stop(cannon.idleSoundId);
 101              end;
 102  
 103              cannon.isTurnedOn           = false;
 104              cannon.inputKey             = v.inputKey or "SnowCannon_TurnOnOff";
 105              cannon.startupDuration      = v.startupDuration or 5; -- wait some time before snow is spawned
 106              cannon.startupTimer         = 0;
 107              cannon.rayLength            = v.rayLength or 15;
 108              cannon.spreadFactor         = v.spreadFactor or 1.4;
 109              cannon.minSpreadRadius      = v.minSpreadRadius or 7;
 110              cannon.volumePerSecond      = (v.volumePerHour or 5000)/3600; -- in m³ per hour
 111              cannon.cumulatedVolume      = 0; -- cumulated volume of snow spawned already
 112              cannon.pricePerCubic        = v.pricePerCubic or 1; -- should be a realistic value
 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          local px,py,pz                  = getWorldPosition(cannon.controlId);
 140          local dx,dy,dz                  = px - player.x, py - player.y, pz - player.z;
 141  
 142          if player.isActive and self.attacherMasterVehicle == nil and (dx*dx + dy*dy + dz*dz) < SnowCannon.CONTROL_RANGE then
 143              -- check if the player wants to toggle on/off the cannon
 144              local inputKey              = InputMapper[cannon.inputKey];
 145              if InputMapper:getKeyDown(inputKey) then
 146                  self:setIsCannonTurnedOn(k, not cannon.isTurnedOn);
 147              end;
 148  
 149              -- key hint
 150              g_GUI:addKeyHint(inputKey, l10n.get(cannon.isTurnedOn and "Input_SnowCannon_TurnOff" or "Input_SnowCannon_TurnOn"));
 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 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                  end;
 193                  
 194                  -- we're spawning some snow!
 195                  -- get the world position and direction of our cannon
 196                  local x,y,z         = getWorldPosition(cannon.snowSpawnId);
 197                  local dx,dy,dz      = Utils.transformDirection(cannon.snowSpawnId, 0,0,1); -- yeet snow in +Z direction
 198  
 199                  -- snow focuses on a fixed point that is located some metres (rayLength) in front of our snowSpawnId
 200                  local rayLength     = cannon.rayLength;
 201                  x,y,z               = x + rayLength*dx, y + rayLength*dy, z + rayLength*dz;
 202  
 203                  local terrainDelta  = y - Utils.sampleTerrainHeight(x, z);
 204  
 205                  -- let's assume snow to spread in linear way:
 206                  -- if emitted e.g. 5 metres above terrain, it will spread in a radius of e.g. 5 metres (=> diametre 10 m!)
 207                  -- this can be customised using the cannon's spread factor
 208                  -- this is approximated using a slightly modified cone volume formula (V = 1/3 * r^2 * pi * h)
 209                  cannon.cumulatedVolume  = cannon.cumulatedVolume + cannon.volumePerSecond * dt;
 210  
 211                  local spreadRadius  = math.max(terrainDelta * cannon.spreadFactor, cannon.minSpreadRadius);
 212                  local deltaHeight   = 0.7 * cannon.cumulatedVolume / (spreadRadius^1.6);
 213  
 214                  -- spawn if delta height is larger than 2x step size
 215                  if deltaHeight >= 10*SnowSystem.SNOW_HEIGHTSTEP then
 216                      local price     = cannon.cumulatedVolume * cannon.pricePerCubic;
 217                      
 218                      if g_scenario:canAffordExpense(price) then
 219                          -- pass this on to snow system
 220                          SnowSystem.addSnowCircular(x, deltaHeight, z, spreadRadius);
 221                          g_scenario:addSnowmakingExpense(price);
 222                      else
 223                          -- shut cannon off
 224                          self:setIsCannonTurnedOn(k, false);
 225                      end;
 226                      cannon.cumulatedVolume  = 0;
 227  
 228                  end;
 229              end;
 230          end;
 231      end;
 232  end;

SnowCannon:saveToTable(tbl)

Saves all relevant variables.

 236  function SnowCannon:saveToTable(tbl)
 237      if tbl == nil then return end;
 238  
 239      tbl.snowCannonGroupId       = self.snowCannonGroupId;
 240      tbl.snowCannons             = {};
 241  
 242      for n, cannon in pairs(self.snowCannons) do
 243          tbl.snowCannons[n]      = {
 244              isTurnedOn          = cannon.isTurnedOn,
 245              cumulatedVolume     = cannon.cumulatedVolume, -- save this to avoid all cannons "eating" money at once
 246              propellerSpeed      = cannon.propellerSpeed,
 247              startupTimer        = cannon.startupTimer,
 248              rotX                = cannon.rotX,
 249              rotY                = cannon.rotY,
 250          };
 251      end;
 252  end;

SnowCannon:loadFromTable(tbl)

Restores all values from the savegame.

 256  function SnowCannon:loadFromTable(tbl)
 257      if tbl == nil then return end;
 258  
 259      self.snowCannonGroupId      = getNoNil(tbl.snowCannonGroupId, self.snowCannonGroupId);
 260  
 261      if tbl.snowCannons == nil then return end;
 262  
 263      for n, cannon in pairs(self.snowCannons) do
 264          local savedCannon       = tbl.snowCannons[n];
 265  
 266          if savedCannon ~= nil then
 267              cannon.isTurnedOn       = getNoNil(savedCannon.isTurnedOn,          false);
 268              cannon.cumulatedVolume  = getNoNil(savedCannon.cumulatedVolume,     cannon.cumulatedVolume);
 269              cannon.propellerSpeed   = getNoNil(savedCannon.propellerSpeed,      cannon.propellerSpeed);
 270              cannon.startupTimer     = getNoNil(savedCannon.startupTimer,        cannon.startupTimer);
 271  
 272              if savedCannon.rotX ~= nil and cannon.rotXId ~= nil then
 273                  self:setCannonRotationX(n, savedCannon.rotX);
 274              end;
 275              if savedCannon.rotY ~= nil and cannon.rotYId ~= nil then
 276                  self:setCannonRotationY(n, savedCannon.rotY);
 277              end;
 278          end;
 279  
 280          -- apply that (to make sure everything is alright)
 281          self:setIsCannonTurnedOn(n, cannon.isTurnedOn, true);
 282      end;
 283  end;

SnowCannon:setIsCannonTurnedOn(cannonIndex, state)

Call this to turn the snow cannon number cannonIndex (int, starting with 1) on or off, depending on state (bool).

 287  function SnowCannon:setIsCannonTurnedOn(cannonIndex, state)
 288      -- don't allow the player to turn on the cannon if he doesn't have enough cash
 289      if state and not g_scenario:canAffordExpense(0) then
 290          state                       = false;
 291      end;
 292      
 293      if cannonIndex == nil then
 294          -- apply this to all cannons
 295          for k, _ in pairs(self.snowCannons) do
 296              -- make sure k is not nil, as this would result in an infinite loop
 297              if k ~= nil then
 298                  self:setIsCannonTurnedOn(k, state);
 299              end;
 300          end;
 301      end;
 302      
 303      local cannon                    = self.snowCannons[cannonIndex];
 304      if cannon == nil then return end;
 305  
 306      cannon.isTurnedOn               = state and self:getMayTurnCannonOn(cannonIndex);
 307  
 308      if not state then
 309          -- stop emitting particles
 310          for _, id in pairs(cannon.particleSystems) do
 311              ParticleSystem.stop(id);
 312          end;
 313          cannon.particleSystemsPlaying   = false;
 314  
 315          cannon.startupTimer         = 0;
 316          cannon.cumulatedVolume      = 0;
 317          
 318          if cannon.idleSoundId ~= nil then
 319              AudioSource.stop(cannon.idleSoundId);
 320          end;
 321      end;
 322  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.

 327  function SnowCannon:getMayTurnCannonOn(cannonIndex)
 328      return self.attacherMasterVehicle == nil;
 329  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.

 335  function SnowCannon:onAttach()
 336      for k, v in pairs(self.snowCannons) do
 337          self:setIsCannonTurnedOn(k, false);
 338  
 339          if v.rotXAttach ~= nil then
 340              self:setCannonRotationX(k, v.rotXAttach);
 341          end;
 342  
 343          if v.rotYAttach ~= nil then
 344              self:setCannonRotationY(k, v.rotYAttach);
 345          end;
 346      end;
 347  end;

SnowCannon:setSnowCannonGroupId(groupId)

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

 351  function SnowCannon:setSnowCannonGroupId(groupId)
 352      self.snowCannonGroupId          = math.max(groupId or 0, 0);
 353  end;

SnowCannon:getSnowCannonRemoteButton()

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

 357  function SnowCannon:getSnowCannonRemoteButton()
 358      if self.snowCannons == nil or self.snowCannons[1] == nil then
 359          return "Invalid";
 360      end;
 361  
 362      return l10n.get(self.snowCannons[1].isTurnedOn and "ui_esc_snowCannonRemote1" or "ui_esc_snowCannonRemote0");
 363  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().

 367  function SnowCannon:snowCannonRemoteCallback()
 368      if self.snowCannons == nil or self.snowCannons[1] == nil then return end;
 369      
 370      self:setIsCannonTurnedOn(1, not self.snowCannons[1].isTurnedOn);
 371  end;

SnowCannon:snowCannonRemoteMove(deltaX, deltaY)

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

 375  function SnowCannon:snowCannonRemoteMove(deltaX, deltaY)
 376      local cannon                    = self.snowCannons[1];
 377      if cannon == nil then return end;
 378  
 379      local dt                        = 1/30;
 380  
 381      if deltaY ~= nil and deltaY ~= 0 then
 382          self:setCannonRotationX(1, cannon.rotX + cannon.rotXSpeed * dt * deltaY);
 383      end;
 384      if deltaX ~= nil and deltaX ~= 0 then
 385          self:setCannonRotationY(1, cannon.rotY + cannon.rotYSpeed * dt * deltaX);
 386      end;
 387  end;

SnowCannon:setCannonRotationX(cannonIndex, rotX)

Rotates the snow cannon along its X rotation axis.

 391  function SnowCannon:setCannonRotationX(cannonIndex, rotX)
 392      local cannon                    = self.snowCannons[cannonIndex];
 393      if cannon == nil then return end;
 394  
 395      if cannon.rotXId ~= nil then
 396          cannon.rotX                 = clamp(rotX, cannon.rotXmin, cannon.rotXmax);
 397          setRotationX(cannon.rotXId, cannon.rotX);
 398      end;
 399  end;

SnowCannon:setCannonRotationY(cannonIndex, rotY)

Rotates the snow cannon along its Y rotation axis.

 403  function SnowCannon:setCannonRotationY(cannonIndex, rotY)
 404      local cannon                    = self.snowCannons[cannonIndex];
 405      if cannon == nil then return end;
 406  
 407      if cannon.rotYId ~= nil then
 408          cannon.rotY                 = clamp(rotY, cannon.rotYmin, cannon.rotYmax);
 409          setRotationY(cannon.rotYId, cannon.rotY);
 410      end;
 411  end;

SnowCannon:destroy()

 413  function SnowCannon:destroy()
 414      SnowmakingManager:unregisterVehicle(self);
 415  end;

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

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