-- meseor 0.2.0 by paramat.
-- License WTFPL, see license.txt.

-- Parameters.

local ONGEN = true -- (true / false) Enable / disable impacts on generated chunk.
local ONGCHA = 90 -- 9 -- Ongen 1/x chance of impact per generated chunk.
local ABM = false -- Enable / disable impacts by abm on surface material.
local ABMINT = 18100 -- 181 -- Abm interval.
local ABMCHA = 100000 -- 100000 -- Abm 1/x chance per node.
local DAMAGE = true -- Enable / disable player damage loop.
local DAMMAX = 20 -- 20 -- Maximum damage. 20 = direct hit is fatal.

local RADMIN = 3 -- 3 -- Minimum crater radius.
local RADMAX = 31 -- 31 -- Maximum crater radius, on-gen craters are limited to 16.
local STOCHA = 5 -- 5 -- 1/x chance of stone boulders instead of gravel.

local XMIN = -16000 -- Impact area dimensions. Impacts only inside this area.
local XMAX = 16000
local ZMIN = -16000
local ZMAX = 16000

local SAXMIN = 0 -- Safe area dimensions. No impacts inside this area.
local SAXMAX = 0 -- When overlapping impact area, the safe area overrides.
local SAZMIN = 0
local SAZMAX = 0

local DEBUG = false

-- Stuff.

meseor = {}

function meseor.crater(pos, conrad)
	local x = pos.x
	local y = pos.y
	local z = pos.z
	local rimrad = math.ceil(conrad * 2.4)
	-- If in safe zone or not in impact zone then abort.
	if (x > SAXMIN - rimrad and x < SAXMAX + rimrad and z > SAZMIN - rimrad and z < SAZMAX + rimrad)
	or not (x > XMIN and x < XMAX and z > ZMIN and z < ZMAX) then
		return
	end
	-- Check enough depth.
	for j = -conrad - 1, -1 do
		local nodename = minetest.get_node({x=x,y=y+j,z=z}).name
		if nodename == "air" or nodename == "ignore" then
			return
		end
	end
	-- Check pos open to sky.
	for j = 1, 160 do
		local nodename = minetest.get_node({x=x,y=y+j,z=z}).name
		if nodename ~= "air" and nodename ~= "ignore"
		and nodename ~= "default:leaves" and nodename ~= "default:jungleleaves"
		and nodename ~= "snow:ice" and nodename ~= "snow:needles" then
			return
		end
	end
	-- Excavate path.
	for j = 1, 160 do
		local nodename = minetest.get_node({x=x,y=y+j,z=z}).name
		if nodename ~= "air" and nodename ~= "ignore" then
			minetest.remove_node({x=x,y=y+j,z=z})
		end
	end
	-- Add meseorite.
	minetest.set_node({x=x,y=y-conrad-1,z=z},{name="default:mese"})
	-- Excavate cone and count excavated nodes.
	local exsto = 0
	local exdsto = 0
	local exdirt = 0
	local exdsan = 0
	local exsan = 0
	local extree = 0
	local exjtree = 0
	for j = 0, conrad * 2 do
		for i = -j, j do
		for k = -j, j do
			if i ^ 2 + k ^ 2 <= j ^ 2 then
				local exsno = false
				local nodename = minetest.get_node({x=x+i,y=y-conrad+j,z=z+k}).name
				if nodename == "default:stone" then
					exsto = exsto + 1
				elseif nodename == "default:desert_stone" then
					exdsto = exdsto + 1
				elseif nodename == "default:dirt" or nodename == "default:dirt_with_grass" then
					exdirt = exdirt + 1
				elseif nodename == "default:desert_sand" then
					exdsan = exdsan + 1
				elseif nodename == "default:sand" then
					exsan = exsan + 1
				elseif nodename == "default:tree" then
					extree = extree + 1
				elseif nodename == "default:jungletree" then
					exjtree = exjtree + 1
				elseif nodename == "snow:snow" then
					exsno = true
				end
				if nodename ~= "air" then
					minetest.remove_node({x=x+i,y=y-conrad+j,z=z+k})
				end
				if exsno then
					minetest.remove_node({x=x+i,y=y-conrad+j,z=z+k})
				end
			end
		end
		end
	end
	-- Calculate proportions of ejecta.
	local extot = exsto + exdsto + exdirt + exdsan + exsan + extree + exjtree
	local pexsto = exsto / extot
	local pexdsto = exdsto / extot
	local pexdirt = exdirt / extot
	local pexdsan = exdsan / extot
	local pexsan = exsan / extot
	local pextree = extree / extot
	local pexjtree = exjtree / extot
	-- Print to terminal.
	if DEBUG then
		print ("[meseor] Cone radius "..conrad.." node ("..x.." "..y.." "..z..")")
		print ("[meseor] exsto "..exsto.." exdsto "..exdsto.." exdirt "..exdirt.." exdsan "..exdsan)
		print ("[meseor] exsan "..exsan.." extree "..extree.." exjtree "..exjtree)
		print ("[meseor] extot "..extot)
	end
	-- Add ejecta.
	local addtot = 0
	for rep = 1, 128 do
		for i = -rimrad, rimrad do
		for k = -rimrad, rimrad do
			local rad = (i ^ 2 + k ^ 2) ^ 0.5
			if rad <= rimrad and math.random() > math.abs(rad - conrad * 1.2) / (conrad * 1.2)
			and addtot < extot and math.random(3) == 2 then
				-- Find ground.
				local groundy = false
				for j = conrad - 1, -160, -1 do
					local nodename = minetest.get_node({x=x+i,y=y+j,z=z+k}).name
					if nodename == "default:leaves" or nodename == "default:jungleleaves"
					or nodename == "default:papyrus" or nodename == "default:dry_shrub"
					or nodename == "default:grass_1" or nodename == "default:grass_2"
					or nodename == "default:grass_3" or nodename == "default:grass_4"
					or nodename == "default:grass_5" or nodename == "default:apple"
					or nodename == "default:junglegrass" or nodename == "snow:needles" then
						minetest.remove_node({x=x+i,y=y+j,z=z+k})
					elseif nodename ~= "air" and nodename ~= "ignore" and nodename ~= "snow:snow"
					and nodename ~= "default:water_source" and nodename ~= "default:water_flowing" then
						groundy = y+j
						break
					end
				end
				if groundy then
					local x = x + i
					local y = groundy + 1
					local z = z + k
					if math.random() < pexjtree then
						minetest.set_node({x=x,y=y,z=z},{name="default:jungletree"})
					elseif math.random() < pextree then
						minetest.set_node({x=x,y=y,z=z},{name="default:tree"})
					elseif math.random() < pexsan then
						minetest.set_node({x=x,y=y,z=z},{name="default:sand"})
					elseif math.random() < pexdsan then
						minetest.set_node({x=x,y=y,z=z},{name="default:desert_sand"})
					elseif math.random() < pexdirt then
						minetest.set_node({x=x,y=y,z=z},{name="default:dirt"})
					elseif math.random() < pexdsto then
						minetest.set_node({x=x,y=y,z=z},{name="default:desert_stone"})
					elseif math.random() < pexsto then
						if math.random(STOCHA) == 2 then
							minetest.set_node({x=x,y=y,z=z},{name="default:stone"})
						else
							minetest.set_node({x=x,y=y,z=z},{name="default:gravel"})
						end
					end
					addtot = addtot + 1
				end
			end
		end
		end
		if addtot == extot then break end
	end
	-- Play sound.
	minetest.sound_play("meseor", {gain = 1})
	-- Damage player if inside rimrad.
	if DAMAGE then
		for _,player in ipairs(minetest.get_connected_players()) do
			local plapos = player:getpos()
			local pladis = ((plapos.x - x) ^ 2 + (plapos.y - y) ^ 2 + (plapos.z - z) ^ 2) ^ 0.5
			local pladam = math.ceil((1 - pladis / rimrad) * DAMMAX)
			if pladam > 0 then
				player:set_hp(player:get_hp() - pladam)
			end
		end
	end
	minetest.after(0.1, function(pos1, pos2)
		local vm = minetest.get_voxel_manip(pos1, pos2)
		vm:update_liquids()
		vm:write_to_map()
	end, {x = pos.x-rimrad, y = pos.y-rimrad, z= pos.z-rimrad }, {x= pos.x+rimrad, y = pos.y+160, z= pos.z+rimrad})
end


-- On generated.

if ONGEN then
	minetest.register_on_generated(function(minp, maxp, seed)
		if math.random(ONGCHA) == 2 then
			local maxrad = RADMAX
			if maxrad > 16 then maxrad = 16 end
			local conrad = math.random(RADMIN, maxrad)
			local rimrad = math.ceil(conrad * 2.4)
			local x = math.random(minp.x + rimrad, maxp.x - rimrad)
			local z = math.random(minp.z + rimrad, maxp.z - rimrad)
			-- Find surface with air above, abort if water found, ignore trees and ice.
			local surfy = false
			local aa = false
			for y = maxp.y, minp.y, -1 do
				local nodename = minetest.get_node({x=x,y=y,z=z}).name
				if nodename == "default:water_source" or nodename == "default:water_flowing" then
					return
				elseif nodename == "air" then
					aa = true
				elseif aa and nodename ~= "air" and nodename ~= "ignore" and nodename ~= "snow:ice"
				and nodename ~= "default:leaves" and nodename ~= "default:jungleleaves"
				and nodename ~= "default:tree" and nodename ~= "default:jungletree" then
					surfy = y
					break
				end
			end
			if not surfy then
				return
			end
			DAMAGE = false
			meseor.crater({x=x,y=surfy,z=z},conrad)
		end
	end)
end

-- Abm.

if ABM then
	minetest.register_abm({
		nodenames = {
			"default:dirt",
			"default:dirt_with_grass",
			"default:desert_sand",
			"default:sand",
		},    
		interval = ABMINT,	
		chance = ABMCHA,
		action = function(pos, node, _, _)
			local x = pos.x
			local y = pos.y
			local z = pos.z
			-- Find close surface above, abort if underwater or no close surface found.
			local surfy = false
			for j = 1, 4 do
				local nodename = minetest.get_node({x=x,y=y+j,z=z}).name
				if nodename == "default:water_source" or nodename == "default:water_flowing" then
					return
				elseif nodename == "air" then
					surfy = y+j-1
					break
				end
			end
			if not surfy then
				return
			end
			local conrad = math.random(RADMIN, RADMAX)
			meseor.crater({x=x,y=surfy,z=z},conrad)
		end,
	})
end
