meta data for this page
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;
Copyright
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.