diff --git a/README.md b/README.md index 947d5df..69041d7 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,13 @@ Lower values can possible increase performance. - ``Dynamically modify nodes`` (default true): If set to true, weather packs are allowed to register node update handlers. These can be used to dynamically place snow layers, melt ice, or hydrate soil. +- ``Include wind speed in damage checks`` (default true): +If set to true, Climate API will factor in wind speed and obstacles to determine damage sources. +If set to false, a simple check will be used whether the player is outside. -### Visuals +### Weather Effects +- ``Cause player damage`` (default true): +If set to true, dangerous weather presets will damage affected players over time. - ``Show particle effects`` (default true): If set to true, weather effects (like rain) are allowed to render particles. Deactivating this feature will prevent some presets from being visible. diff --git a/ca_effects/damage.lua b/ca_effects/damage.lua new file mode 100644 index 0000000..65245be --- /dev/null +++ b/ca_effects/damage.lua @@ -0,0 +1,84 @@ +--[[ +# Player Damage Effect +Use this effect to damage a player during dangerous weather events. +Expects a table as the parameter containing the following values: +- value [1]: The amount of damage to be applied per successful roll. +- chance [1]: Defines a 1/x roll per cycle for the player to get damaged. Higher values result in less frequent damage. +- check [nil]: Use an additional outdoors check before applying damage. Consists of the following values: + - type <"light"|"raycast"> ["light"] (Whether the light level should be used a raycast should be performed) + - height [0] (Height offset of weather origin from the player. Only used for raycasts) + - velocity [1] (Velocity of damaging particles. Only used for raycasts) + - use_wind [true] (Whether the wind should be factored in. Only used for raycasts) +]] + +if not minetest.is_yes(minetest.settings:get_bool("enable_damage")) +or not climate_mod.settings.damage then return end + +local EFFECT_NAME = "climate_api:damage" + +local rng = PcgRandom(7819792) + +local function check_hit(player, ray) + local ppos = vector.add(player:get_pos(), {x=0, y=1, z=0}) + if ray.type ~= nil and ray.type ~= "light" and ray.type ~= "raycast" then + minetest.log("warning", "[Climate API] Invalid damage check configuration") + return false + end + + -- use light level if specified or in performance mode + if ray.type == nil + or ray.type == "light" + or not climate_mod.settings.raycast then + return minetest.get_node_light(ppos, 0.5) == 15 + end + + -- use raycating to factor in wind speed + local origin = vector.add(ppos, {x = 0, y = ray.height or 0, z = 0 }) + if ray.use_wind ~= false then + local wind = climate_api.environment.get_wind() + local velocity = ray.velocity or 1 + local windpos = vector.multiply( + vector.normalize(vector.add({ x = 0, y = -velocity, z = 0 }, wind)), + -vector.length(wind) + ) + origin = vector.add(origin, windpos) + end + local ray = minetest.raycast(origin, ppos) + local obj = ray:next() + -- found nothing + if obj == nil then return false end + -- found node + if obj.type ~= "object" then return false end + -- found different entity + if not obj.ref:is_player() then return false end + -- found another player + if obj.ref:get_player_name() ~= player:get_player_name() then return false end + return true +end + +local function calc_damage(player, dmg) + if dmg.value == nil then dmg.value = 1 end + if dmg.chance == nil then dmg.chance = 1 end + -- check if damage should be applied + if rng:next(1, dmg.chance) ~= 1 then return 0 end + if dmg.check ~= nil then + -- check for obstacles in the way + if not check_hit(player, dmg.check) then return 0 end + end + return dmg.value +end + +local function handle_effect(player_data) + for playername, data in pairs(player_data) do + local player = minetest.get_player_by_name(playername) + local hp = player:get_hp() + for weather, dmg in pairs(data) do + hp = hp - calc_damage(player, dmg) + end + -- deal damage to player + player:set_hp(hp, "weather damage") + end +end + +climate_api.register_effect(EFFECT_NAME, handle_effect, "tick") +climate_api.set_effect_cycle(EFFECT_NAME, climate_api.MEDIUM_CYCLE) \ No newline at end of file diff --git a/ca_effects/particles.lua b/ca_effects/particles.lua index 2f592d9..ec50a1d 100644 --- a/ca_effects/particles.lua +++ b/ca_effects/particles.lua @@ -1,96 +1,198 @@ --[[ # Particle Effect -Use this effect to render downfall using particles. -Expects a table as the parameter containing the following values: -- amount : The quantity of spawned particles per cycle -- EITHER texture : The image file name -- OR textures
: A list of possible texture variants -- falling_speed : The downwards speed -- min_pos : Bottom-left corner of spawn position (automatically adjusted by wind) -- max_pos : Top-right corner of spawn position (automatically adjusted by wind) -- acceleration (optional): Particle acceleration in any direction -- exptime : Time of life of particles -- time (optional): The time of life of particle spawners (defaults to 0.5) -- EITHER size : Size of the particles -- OR min_size and max_size : Minimum and maximum size -- vertical (optional): Whether particles should rotate in 2D space only (default depends on falling vector) +Use this effect to render downfall or similar visuals using particles. +Expects a table as the parameter containing information for the spawner. +All values for ParticleSpawner definitions are valid. +See https://minetest.gitlab.io/minetest/definition-tables/#particlespawner-definition + +Furthermore, the following default values have been changed: +- time [0.5] (reduced time results in smoother position updates, but more lag) +- collisiondetection [true] +- collision_removal [true] +- playername [current player] (Set to empty string to show for everyone) + +The following optional values have been introduced or expanded for convenience: +- size [nil] (Overrides both minsize and maxsize if set) +- boxsize [nil] (Overrides minpos and maxpos based on specified sizes per direction with the player in the center) +- boxsize [nil] (If set to a number, the resulting vector will have the specified size in all directions) +- v_offset [0] (Use in conjunctin with boxsize. Adds specified height to minpos and maxpos y-coordinates) +- minvel [nil] (Overrides minvel with a downward facing vector of specified length) +- maxvel [nil] (Overrides maxvel with a downward facing vector of specified length) +- velocity [nil] (Overrides both minvel and maxvel if set) +- minacc [nil] (Overrides minacc with a downward facing vector of specified length) +- maxacc [nil] (Overrides maxacc with a downward facing vector of specified length) +- acceleration [nil] (Overrides both minacc and maxacc if set) +- attach_to_player [false] (Overrides attached object with current player) + +The following new behaviours have been introduced: +- use_wind [true] (Adjusts velocity and position for current windspeed) +- detached [false] (Unless enabled, considers positions as relative to current player like being attached) +- vertical [nil] (Unless explicitly set, will be automatically set based on direction of velocity) ]] if not climate_mod.settings.particles then return end local EFFECT_NAME = "climate_api:particles" -local function get_particle_texture(particles) - if type(particles.textures) == "nil" or next(particles.textures) == nil then - return particles.texture - end - return particles.textures[math.random(#particles.textures)] -end - -local function spawn_particles(player, particles) - local ppos = player:get_pos() - local wind = climate_api.environment.get_wind() - - local amount = particles.amount * climate_mod.settings.particle_count - local texture = get_particle_texture(particles) - - local vel = vector.new({ - x = wind.x, - y = -particles.falling_speed, - z = wind.z - }) - - if particles.acceleration == nil then - particles.acceleration = vector.new({x=0, y=0, z=0}) - end - - local wind_pos = vector.multiply( - vector.normalize(vel), - -vector.length(wind) - ) - wind_pos.y = 0 - local minp = vector.add(vector.add(ppos, particles.min_pos), wind_pos) - local maxp = vector.add(vector.add(ppos, particles.max_pos), wind_pos) - - if particles.time == nil then - particles.time = 0.5 - end - - if particles.vertical == nil then - particles.vertical = math.abs(vector.normalize(vel).y) >= 0.6 - end - - if particles.size ~= nil then - particles.min_size = particles.size - particles.max_size = particles.size - end - - minetest.add_particlespawner({ - amount = amount, - time = particles.time, - minpos = minp, - maxpos = maxp, - minvel = vel, - maxvel = vel, - minacc = particles.acceleration, - maxacc = particles.acceleration, - minexptime = particles.exptime, - maxexptime = particles.exptime, - minsize = particles.min_size, - maxsize = particles.max_size, +-- parse config by injecting default values and adding additional parameters +local function parse_config(player, particles) + -- override default values with more useful ones + local defaults = { + time = 0.5, collisiondetection = true, collision_removal = true, - vertical = particles.vertical, - texture = texture, - player = player:get_player_name() - }) + playername = player:get_player_name(), + use_wind = true, + attach_to_player = false, + detached = false + } + + -- inject missing default values into specified config + local config = climate_api.utility.merge_tables(defaults, particles) + + -- scale particle amount based on mod config + if particles.amount ~= nil then + config.amount = particles.amount * climate_mod.settings.particle_count + end + + -- restore default visibility if specified + if particles.playername == "" then + config.playername = nil + end + + -- provide easier param for exptime + if particles.expirationtime ~= nil then + config.minexptime = particles.expirationtime + config.maxexptime = particles.expirationtime + config.expirationtime = nil + end + + -- provide easier param for size + if particles.size ~= nil then + config.minsize = particles.size + config.maxsize = particles.size + config.size = nil + end + + -- randomly select a texture when given a table + if type(particles.texture) == "table" then + config.texture = particles.texture[math.random(#particles.texture)] + end + + if particles.pos ~= nil then + config.minpos = particles.pos + config.maxpos = particles.pos + config.pos = nil + end + + -- provide easier size based param for position + if type(particles.boxsize) == "number" then + particles.boxsize = { + x = particles.boxsize, + y = particles.boxsize, + z = particles.boxsize + } + end + + if particles.boxsize ~= nil then + local size_x = particles.boxsize.x or 0 + local size_y = particles.boxsize.y or 0 + local size_z = particles.boxsize.z or 0 + local v_offset = particles.v_offset or 0 + v_offset = v_offset + (size_y / 2) + config.minpos = { + x = -size_x / 2, + y = v_offset - (size_y / 2), + z = -size_z / 2 + } + config.maxpos = { + x = size_x / 2, + y = v_offset + (size_y / 2), + z = size_z / 2 + } + config.size_x = nil + config.size_y = nil + config.size_z = nil + config.v_offset = nil + end + + -- provide easy param to define unanimous falling speed + if particles.velocity ~= nil then + particles.minvel = particles.velocity + particles.maxvel = particles.velocity + config.velocity = nil + end + + if type(particles.minvel) == "number" then + config.minvel = { x = 0, y = -particles.minvel, z = 0 } + end + + if type(particles.maxvel) ~= nil then + config.maxvel = { x = 0, y = -particles.maxvel, z = 0 } + end + + -- provide easy param to define unanimous falling acceleration + if particles.acceleration ~= nil then + particles.minacc = particles.acceleration + particles.maxacc = particles.acceleration + config.acceleration = nil + end + + if type(particles.minacc) == "number" then + config.minacc = { x = 0, y = -particles.minacc, z = 0 } + end + + if type(particles.maxacc) == "number" then + config.maxacc = { x = 0, y = -particles.maxacc, z = 0 } + end + + -- attach particles to current player if specified + if config.attach_to_player then + config.attached = player + end + config.attach_to_player = nil + + -- attach coordinates to player unless specified or already attached + if (not config.detached) and config.attached == nil then + local ppos = player:get_pos() + config.minpos = vector.add(config.minpos, ppos) + config.maxpos = vector.add(config.maxpos, ppos) + end + config.detached = nil + + -- move particles in wind direction + if config.use_wind then + local wind = climate_api.environment.get_wind() + -- adjust velocity to include wind + config.minvel = vector.add(config.minvel, wind) + config.maxvel = vector.add(config.maxvel, wind) + + -- adjust spawn position for better visibility + local vel = vector.multiply(vector.add(config.minvel, config.maxvel), 0.5) + local windpos = vector.multiply( + vector.normalize(vel), + -vector.length(wind) + ) + config.minpos = vector.add(config.minpos, windpos) + config.maxpos = vector.add(config.maxpos, windpos) + end + config.use_wind = nil + + -- if unspecified, use 2D or 3D rotation based on movement direction + if particles.vertical == nil then + local vel = vector.multiply(vector.add(config.minvel, config.maxvel), 0.5) + config.vertical = math.abs(vector.normalize(vel).y) >= 0.6 + end + + return config end local function handle_effect(player_data) for playername, data in pairs(player_data) do local player = minetest.get_player_by_name(playername) for weather, value in pairs(data) do - spawn_particles(player, value) + local config = parse_config(player, value) + minetest.add_particlespawner(config) end end end diff --git a/init.lua b/init.lua index 756c59d..6d590b8 100644 --- a/init.lua +++ b/init.lua @@ -25,6 +25,8 @@ end -- load settings from config file climate_mod.settings = { + damage = get_setting_bool("damage", true), + raycast = get_setting_bool("raycast", true), particles = get_setting_bool("particles", true), skybox = get_setting_bool("skybox", true), sound = get_setting_bool("sound", true), @@ -72,6 +74,7 @@ dofile(modpath.."/lib/commands.lua") dofile(modpath .. "/lib/influences.lua") -- import predefined environment effects +dofile(modpath .. "/ca_effects/damage.lua") dofile(modpath .. "/ca_effects/hud_overlay.lua") dofile(modpath .. "/ca_effects/particles.lua") dofile(modpath .. "/ca_effects/skybox.lua") diff --git a/lib/influences.lua b/lib/influences.lua index f1e9a5f..ec5ae58 100644 --- a/lib/influences.lua +++ b/lib/influences.lua @@ -14,6 +14,19 @@ climate_api.register_influence("base_humidity", function(pos) return minetest.get_humidity(pos) end) +-- see https://en.wikipedia.org/wiki/Dew_point#Simple_approximation +climate_api.register_influence("dewpoint", function(pos) + local heat = climate_api.environment.get_heat(pos) + local humidity = climate_api.environment.get_humidity(pos) + return heat - (9/25 * (100 - humidity)) +end) + +climate_api.register_influence("base_dewpoint", function(pos) + local heat = minetest.get_heat(pos) + local humidity = minetest.get_humidity(pos) + return heat - (9/25 * (100 - humidity)) +end) + climate_api.register_influence("biome", function(pos) local data = minetest.get_biome_data(pos) local biome = minetest.get_biome_name(data.biome) diff --git a/settingtypes.txt b/settingtypes.txt index 456f115..1257c13 100644 --- a/settingtypes.txt +++ b/settingtypes.txt @@ -14,8 +14,17 @@ climate_api_particle_count (Multiplicator for used particles) float 1 0.1 2 # These can be used to dynamically place snow layers, melt ice, or hydrate soil. climate_api_block_updates (Dynamically modify nodes) bool true +# If set to true, Climate API will factor in wind speed and obstacles to determine damage sources. +# If set to false, a simple check will be used whether the player is outside. +# Only applied if climate_api_damage is also set to true. +climate_api_raycast (Include wind speed in damage checks) bool true + + +[Weather Effects] + +# If set to true, dangerous weather presets will damage affected players over time. +climate_api_damage (Cause player damage) bool true -[Visuals] # If set to true, weather effects (like rain) are allowed to render particles. # Deactivating this feature will prevent some presets from being visible. # For performance considerations it is recommended to decrease the amount of particles instead.