/** Cloud Generic cloud, features: rain (water, acid) and thunder. The clouds have periods of condensing, idle and raining. Different types of rain (water, acid) are hardcoded. @authors Ringwaul, Maikel */ local Plane = -300; // Cloud modes: idle, raining, condensing. static const CLOUD_ModeIdle = 0; static const CLOUD_ModeRaining = 1; static const CLOUD_ModeCondensing = 2; static const CLOUD_RainFallOffDistance = 1000; local mode; // The time a cloud is in this mode. local mode_time; local lightning_chance; // chance of lightning strikes 0-100. local evap_x; // x coordinate for evaporation local rain; // Number of liquid pixels the cloud holds. local rain_mat; // Precipitation type from scenario or other. Material name or nil for no rain. local rain_inserts_mat; // Should the rain insert actual material pixels on impact? local rain_amount; // Precipitation amount from scenario or other. local rain_max; // Max rain the cloud can hold. local rain_mat_freeze_temp; // Freezing temperature of current rain material. local rain_mat_frozen; // Material currently frozen to. local rain_visual_strength; // Amount of rain particles local rain_sound_dummy; // Helper object that emits the sound local cloud_shade; // Cloud shade. local cloud_alpha; // Cloud alpha. local cloud_color; // Cloud color proplist. // This is an environment object (e.g., shouldn't be a target for the lift tower) public func IsEnvironment() { return true; } protected func Initialize() { // Clouds start idle. mode = CLOUD_ModeIdle; mode_time = 360 + RandomX(-60, 60); // Default values for rain. rain = 0; rain_max = 960; rain_visual_strength = 10; rain_inserts_mat = true; rain_sound_dummy = CreateObject(Dummy, 0, 0, NO_OWNER); rain_sound_dummy.Visibility = VIS_All; // Cloud defaults lightning_chance = 0; cloud_shade = 0; cloud_alpha = 255; cloud_color = {r = 255, g = 255, b = 255}; evap_x = 0; DoCon(Random(75)); SetAction("Fly"); SetComDir(COMD_None); SetPhase(RandomX(1,16)); // Push low flying clouds up to proper height var xoff = 0, yoff = 0; while (GetY() + yoff > 0 && !MaterialDepthCheck(xoff, yoff, "Sky", 150)) { yoff--; } // Failsafe for stupid grounded clouds if (GetMaterial(xoff, yoff+30) != Material("Sky")) yoff -= 180; SetPosition(GetX()+xoff, GetY()+yoff); // Add effect to process all cloud features. AddEffect("ProcessCloud", this, 100, 5, this); return; } protected func Destruction() { if (rain_sound_dummy) { rain_sound_dummy->RemoveObject(); } _inherited(...); } /*-- Definition call interface --*/ // Id call: Creates the indicated number of clouds. // May fail and will indicate the number of placed clouds. public func Place(int count) { if (this != Cloud) return; var created = 0; for (var max_tries = count * 500; created < count && max_tries > 0; max_tries--) { var pos = FindPosInMat("Sky", 0, 0, LandscapeWidth(), LandscapeHeight()); if (pos && MaterialDepthCheck(pos[0], pos[1], "Sky", 200)) { CreateObjectAbove(Cloud, pos[0], pos[1], NO_OWNER); created++; } } return created; } // Changes the precipitation type of this cloud. // Also an id call: Changes all clouds to this settings. public func SetPrecipitation(string mat, int amount) { // Called to proplist: change all clouds. if (this == Cloud) { for (var cloud in FindObjects(Find_ID(Cloud))) cloud->SetPrecipitation(mat, amount); } else // Otherwise change the clouds precipitation. { rain_mat = mat; rain_amount = amount; // Also change rain content. rain = BoundBy(amount * rain_max / 100, 0, 960); // Store snow/water conversion rain_mat_freeze_temp = GetMaterialVal("BelowTempConvert", "Material", Material(rain_mat)); // Hack: material val does not return nil for materials that do not freeze, so fix those explicitly. if (mat == "Snow" || mat == "Ice") rain_mat_freeze_temp = nil; rain_mat_frozen = GetMaterialVal("BelowTempConvertTo", "Material", Material(rain_mat)); if (rain_mat_frozen == "Ice") rain_mat_frozen = "Snow"; } return; } // Changes the lightning frequency type of this cloud. // Also an id call: Changes all clouds to this settings. public func SetLightning(int freq) { // Called to proplist: change all clouds. if (this == Cloud) { for (var cloud in FindObjects(Find_ID(Cloud))) cloud->SetLightning(freq); } else // Otherwise change this clouds lightning. { lightning_chance = freq; } return; } public func SetRain(int to_rain) { rain = BoundBy(to_rain, 0, rain_max); return; } // Sets the strength of the visual particle rain. // Also an id call: Changes all clouds to this settings. public func SetVisualRainStrength(int to) { // Called to proplist: change all clouds. if (this == Cloud) { for (var cloud in FindObjects(Find_ID(Cloud))) cloud->SetVisualRainStrength(to); } else { rain_visual_strength = to; } } // Changes the color of this cloud. // Also an id call: Changes all clouds to this settings. public func SetCloudRGB(r, g, b) { // Called to proplist: change all clouds. if (this == Cloud) { for (var cloud in FindObjects(Find_ID(Cloud))) cloud->SetCloudRGB(r, g, b); } else // Otherwise change the clouds color. { cloud_color.r = r ?? 255; cloud_color.g = g ?? 255; cloud_color.b = b ?? 255; } return; } // Changes the behavior of this cloud: // if the parameter is true, the cloud will create particles effects and insert material on impact. // if the parameter is false, the could will create particle effects, but not insert material // Also an id call: Changes all clouds to this settings. public func SetInsertMaterial(bool should_insert) { // Called to proplist: change all clouds. if (this == Cloud) { for (var cloud in FindObjects(Find_ID(Cloud))) cloud->SetInsertMaterial(should_insert); } else // Otherwise change the clouds precipitation. { rain_inserts_mat = should_insert; } return; } /*-- Cloud processing --*/ protected func FxProcessCloudStart(object target, proplist effect, int temporary) { // Make sure the effect interval is 5. if (!temporary) effect.Interval = 5; return 1; } protected func FxProcessCloudTimer() { // Move clouds. MoveCloud(); // Update mode time. mode_time--; if (mode_time <= 0) { // Change mode, reset timer. mode = (mode + 1) % 3; mode_time = 480 + RandomX(-90, 90); // Start or stop the rain sound. The object where the sound appears updates position in DropHit() if (mode == CLOUD_ModeRaining) { var mat = RainMat(); if (rain_sound_dummy && (mat == "Water" || mat == "Acid")) rain_sound_dummy->Sound("Liquids::StereoRain", false, 80, nil, +1, CLOUD_RainFallOffDistance); } else { if (rain_sound_dummy) rain_sound_dummy->Sound("Liquids::StereoRain",,,, -1); } } // Process modes. /*if (mode == CLOUD_ModeIdle) { // Empty } else*/ if (mode == CLOUD_ModeRaining) { Precipitation(); ThunderStrike(); } else if (mode == CLOUD_ModeCondensing) { Evaporation(); } // Update cloud appearance. ShadeCloud(); return 1; } private func MoveCloud() { // Get wind speed from various locations of the cloud. var con = GetCon(); var wdt = GetDefWidth() * con / 100; var hgt = GetDefHeight() * con / 100; var xoff = wdt * 10 / 25; var yoff = hgt * 10 / 35; var wind = (GetWind() + GetWind(xoff, yoff) + GetWind(xoff, -yoff) + GetWind(-xoff, -yoff) + GetWind(-xoff, yoff) + GetWind(nil, nil, true)) / 6; // Move according to wind. if (Abs(wind) < 7) SetXDir(0); else SetXDir(wind * 10, 1000); var x = GetX(), y = GetY(); // Loop clouds around the map. if (x >= LandscapeWidth() + wdt/2 - 10) x = 12 - wdt/2; else if (x <= 10 - wdt/2) x = LandscapeWidth() + wdt/2 - 12; // Some other safety. if (y <= 5) y = 6; if (GetYDir() != 0) SetYDir(0); if (x != GetX() || y != GetY()) SetPosition(x, y); // We're moving the cloud to y = 6 above, so don't bother moving it higher // than that. while (Stuck() && GetY() > 10) SetPosition(GetX(), GetY() - 5); if (rain_sound_dummy) { rain_sound_dummy->SetPosition(GetX(), GetY()); } return; } private func Precipitation() { if (!rain_mat) return; // Precipitaion: water or snow. if (rain > 0) { if (RainDrop()) rain--; } // If out of liquids, skip mode. if (rain == 0) mode_time = 0; return; } // Check if liquid is maybe in frozen form. private func RainMat() { if (rain_mat_freeze_temp != nil && GetTemperature() < rain_mat_freeze_temp) return rain_mat_frozen; else return rain_mat; } local last_raindrop_color = nil; local particle_cache; // Raindrop somewhere from the cloud. private func RainDrop() { var con = GetCon(); var wdt = GetDefWidth() * con / 500; var hgt = GetDefHeight() * con / 700; var mat = RainMat(); var particle_name = "Raindrop"; var color = GetMaterialColor(mat); if (color != last_raindrop_color) { particle_cache = {}; last_raindrop_color = color; } if (mat == "Lava" || mat == "DuroLava") particle_name = "RaindropLava"; if (mat == "Snow") particle_cache.snow = particle_cache.snow ?? Particles_Snow(color); else particle_cache.rain = particle_cache.rain ?? Particles_Rain(color); var count = Max(rain_visual_strength, 1); for (var i = 0; i < count; i++) { var x = RandomX(-wdt, wdt); var y = RandomX(-hgt, hgt); var xdir = RandomX(GetWind(0, 0, true) - 5, GetWind(0, 0, true) + 5) / 5; var ydir = 30; if (!GBackSky(x, y)) continue; if (mat == "Ice") { // Ice (-> hail) falls faster. xdir *= 4; ydir *= 4; } // Snow is special. if (mat == "Snow") { CreateParticle("RaindropSnow", x, y, xdir, 10, PV_Random(2000, 3000), particle_cache.snow, 0); if (!i && rain_inserts_mat) CastPXS(mat, 1, 0, x, y); continue; } var particle = new particle_cache.rain {}; if (Random(2)) particle.Attach = ATTACH_Back; CreateParticle(particle_name, x, y, xdir, ydir, PV_Random(200, 300), particle, 0); // Splash. if (!i) { var hit = SimFlight(x, y, xdir, ydir, C4M_Liquid); var x_final = hit[0], y_final = hit[1], time_passed = hit[4]; if (time_passed > 0) { ScheduleCall(this, this.DropHit, time_passed, 0, mat, color, x_final, y_final); } } } return true; } // Checks whether the given material might smoke when in contact with water. private func SmokeyMaterial(string material_name) { var mat = Material(material_name); return GetMaterialVal("Corrosive", "Material", mat) || GetMaterialVal("Incendiary", "Material", mat); } private func DropHit(string material_name, int color, int x_orig, int y_orig) { // Adjust position so that it's in the air. var x = AbsX(x_orig), y = AbsY(y_orig); while (y > 0 && GBackSemiSolid(x, y - 1)) y--; // Add material at impact if (rain_inserts_mat) { InsertMaterial(Material(material_name), x, y - 1); } // Sound? if (rain_sound_dummy && Distance(x_orig, y_orig, rain_sound_dummy->GetX(), rain_sound_dummy->GetY()) > CLOUD_RainFallOffDistance) { rain_sound_dummy->SetPosition(x_orig, y_orig); } // Some materials cast smoke when hitting water. if (GetMaterial(x,y) == Material("Water") && SmokeyMaterial(material_name)) { Smoke(x, y, 3, RGB(150,160,150)); } // Liquid? liquid splash! else if(GBackLiquid(x,y)) { particle_cache.splash_water = particle_cache.splash_water ?? Particles_SplashWater(color); CreateParticle("RaindropSplashLiquid", x, y - 3, 0, 0, 20, particle_cache.splash_water); } // Solid? normal splash! else { if( (material_name == "Acid" && GetMaterial(x,y) == Material("Earth")) || material_name == "Lava" || material_name == "DuroLava") Smoke(x, y, 3, RGB(150,160,150)); CreateParticle("RaindropSplash", x, y, 0, 0, 5, Particles_Splash(color), 0); if(material_name == "Ice") { particle_cache.hail = particle_cache.hail ?? Particles_Hail(color); CreateParticle("Hail", x, y - 1, RandomX(-2,2), -Random(10), PV_Random(300, 300), particle_cache.hail, 0); } else { particle_cache.small_rain = particle_cache.small_rain ?? Particles_RainSmall(color); CreateParticle("SphereSpark", x, y - 1, PV_Random(-10, 10), PV_Random(-30, -10), PV_Random(200, 300), particle_cache.small_rain, 5); } } } private func GetMaterialColor(string name) { // A Material's color is actually defined by its texture. var texture = GetMaterialVal("TextureOverlay", "Material", Material(name)); return GetAverageTextureColor(texture); } // Launches possibly one thunder strike from the cloud. private func ThunderStrike() { // Determine whether to launch a strike. if (rain < 100) return; if (Random(100) >= lightning_chance || Random(80)) return; // Find random position in the cloud. var con = GetCon(); var wdt = GetDefWidth() * con / 250; var hgt = GetDefHeight() * con / 350; var x = GetX() + RandomX(-wdt, wdt); var y = GetY() + RandomX(-hgt, hgt); var pix = 0; // Check if there is sky for at least 60 pixels. while (GBackSky(x - GetX(), y - GetY() + pix) && pix <= 60) pix++; if (pix < 60) return; var str = 2 * con / 3 + RandomX(-15, 15); // Launch lightning. return LaunchLightning(x, y, str, 0, str / 5, str / 10, str / 10); } // Tries to evaporate liquids from the surface. protected func Evaporation() { var prec = 5; // Found enough water/acid, skip condensing phase. if (rain >= 960) { mode_time = 0; return; } // determine new x coordinate from the cloud to test for liquids. var wdt = GetDefWidth() * GetCon() * 3 / 4; evap_x += prec; if (evap_x > wdt) evap_x = - wdt; // Test the line downwards from this coordinate. var y = 0; while (!GBackSemiSolid(evap_x, y) && y < LandscapeHeight()) y += prec; // Try to extract the specified material. if (GetMaterial(evap_x, y) == Material(rain_mat)) { // No idea why the value was originally increased by +3 regardless // of the return value of ExtractMaterialAmount; just left it that way if (rain_inserts_mat) { ExtractMaterialAmount(evap_x, y, Material("Water"), 3); } rain += 3; } // Also add some rain by scenario value. if (Random(100) < rain_amount) rain += 1 + Random(3); return; } //Shades the clouds based on iSize: the water density value of the cloud. private func ShadeCloud() { var alpha = (cloud_alpha + ((rain + 40) * 255) / 960) / 2; var alpha = Min(alpha, 255); var shade = BoundBy(cloud_shade, 0, 255); var factor = 255 - shade; SetClrModulation(RGBa(factor * cloud_color.r / 255, factor * cloud_color.g / 255, factor * cloud_color.b / 255, alpha)); } // Utilized by time to make clouds invisible at night public func SetLightingShade(int darkness) { cloud_shade = darkness; } // Utilized by time to make clouds invisible at night public func SetCloudAlpha(int alpha) { cloud_alpha = BoundBy(alpha, 0, 255); } /* Scenartio saving */ func SaveScenarioObject(props) { if (!inherited(props, ...)) return false; if (GetComDir() == COMD_None) props->Remove("ComDir"); props->Remove("Con"); props->Remove("ClrModulation"); if (rain_mat != nil) props->AddCall("Precipitation", this, "SetPrecipitation", Format("%v", rain_mat), rain_amount); if (lightning_chance) props->AddCall("Lightning", this, "SetLightning", lightning_chance); if (rain) props->AddCall("Rain", this, "SetRain", rain); return true; } /* Properties */ local ActMap = { Fly = { Prototype = Action, Name = "Fly", Procedure = DFA_FLOAT, Speed = 20, Accel = 16, Decel = 16, X = 0, Y = 0, Wdt = 512, Hgt = 350, Length = 16, Delay = 0, NextAction = "Fly", TurnAction = "Turn", }, }; local Name = "Cloud";