local battleCamera = require("scripts/battleCamera")

local onlinePlay = require("scripts/onlinePlay")
local onlinePlayNPC = require("scripts/onlinePlay_npc")

local weirdGoop = require("maskCollection/scripts/weirdGoop")
local tilemap = require("maskCollection/scripts/tilemap")
local enemies = require("maskCollection/scripts/enemies")

local easing = require("ext/easing")

local npcManager = require("npcManager")
local npcutils = require("npcs/npcutils")

local lanky = {}
local npcID = NPC_ID

local lankySettings = {
	id = npcID,

	gfxwidth = 120,
	gfxheight = 128,

	width = 108,
	height = 128,

	gfxoffsetx = 0,
	gfxoffsety = 0,

	frames = 2,
	framestyle = 0,
	framespeed = 8,

	luahandlesspeed = false,
	nowaterphysics = false,
	cliffturn = false,
	staticdirection = true,

	npcblock = false,
	npcblocktop = false,
	playerblock = false,
	playerblocktop = false,

	nohurt = false,
	nogravity = true,
	noblockcollision = true,
	notcointransformable = false,
	nofireball = false,
	noiceball = true,
	noyoshi = false,

	jumphurt = false,
	spinjumpsafe = false,
	harmlessgrab = false,
	harmlessthrown = false,
	nowalldeath = false,

	terminalvelocity = -1,

	health = 9,
	angryHealth = 4,
	jumpDamage = 2,
	normalDamage = 3,
	fireballDamage = 1,
	hammerDamage = 2,

	invincibilityTime = 48,


	deathEffectID = 751,

	footNPCID = npcID + 1,
	footDistance = 160,

	legRootOffsetX = 54,
	legRootOffsetY = 0,

	headOffset = 256,

	headBounceDuration = 32,
	headBouceOffset = 24,

	jointDistances = {96,96,192},


	walkCycleCount = 6,

	stepWalkDistance = 96,
	stepDuration = 80,
	stepRiseTime = 0.75,
	stepMinRiseDistance = 48,
	stepMaxRiseDistance = 160,

	turnWaitTime = 32,
	turnLowerTime = 80,
	turnLowerStopTime = 96,
	turnLowerDistance = 64,
	turnLowerCount = 2,
	
	angrySpeedModifier = 1.4,


	fallSpeed = 16,
	fallTime = 160,


	legThickness = 16,


	jointImage = Graphics.loadImageResolved("npc-".. npcID.. "-joint.png"),
	legImage = Graphics.loadImageResolved("npc-".. npcID.. "-leg.png"),


	fallSound = Misc.resolveSoundFile("maskCollection/resources/sound/lanky/fall"),
	landSound = Misc.resolveSoundFile("maskCollection/resources/sound/lanky/land"),
	moveSound = Misc.resolveSoundFile("maskCollection/resources/sound/lanky/move"),
	stepSound = Misc.resolveSoundFile("maskCollection/resources/sound/lanky/step"),
	turnSound = Misc.resolveSoundFile("maskCollection/resources/sound/lanky/turn"),
}

npcManager.setNpcSettings(lankySettings)
npcManager.registerHarmTypes(npcID,
	{
		HARM_TYPE_JUMP,
		--HARM_TYPE_FROMBELOW,
		HARM_TYPE_NPC,
		--HARM_TYPE_PROJECTILE_USED,
		--HARM_TYPE_LAVA,
		--HARM_TYPE_HELD,
		HARM_TYPE_TAIL,
		--HARM_TYPE_SPINJUMP,
		--HARM_TYPE_OFFSCREEN,
		HARM_TYPE_SWORD
	}, 
	{
		--[HARM_TYPE_JUMP]=10,
		--[HARM_TYPE_FROMBELOW]=10,
		--[HARM_TYPE_NPC]=10,
		--[HARM_TYPE_PROJECTILE_USED]=10,
		--[HARM_TYPE_LAVA]={id=13, xoffset=0.5, xoffsetBack = 0, yoffset=1, yoffsetBack = 1.5},
		--[HARM_TYPE_HELD]=10,
		--[HARM_TYPE_TAIL]=10,
		--[HARM_TYPE_SPINJUMP]=10,
		--[HARM_TYPE_OFFSCREEN]=10,
		--[HARM_TYPE_SWORD]=10,
	}
)


function lanky.onInitAPI()
	npcManager.registerEvent(npcID, lanky, "onTickNPC")
	npcManager.registerEvent(npcID, lanky, "onDrawNPC")

	registerEvent(lanky, "onNPCHarm", "onNPCHarm", false)
	registerEvent(lanky, "onPostNPCKill")
	registerEvent(lanky, "onCameraDraw")
end


local shakeBuffer = Graphics.CaptureBuffer()

local wrapShader = Shader.fromFile(nil,"maskCollection/resources/wrapTexture.frag")
local solidColorShader = Shader.fromFile(nil,"resources/solidColor.frag")


local STATE = {
	WALK = 0,
	TURN_WAIT = 1,
	TURN_LOWER = 2,
	DEATH = 3,
	INTRO_FALL = 4,
	INTRO_WAIT = 5,
}

local cameraShakeData = {}


onlinePlayNPC.onlineHandlingConfig[npcID] = {
	getExtraData = function(v)
		local data = v.data

		if not data.initialised then
			lanky.initialise(v)
		end

		local sendData = {
			state = data.state,
			stateTimer = data.stateTimer,

			hitShakeTimer = data.hitShakeTimer,
			health = data.health,

			startY = data.startY,

			stepLegCount = data.stepLegCount,
			stepCycleCount = data.stepCycleCount,

			turnLowerCount = data.turnLowerCount,
			bounceTimer = data.bounceTimer,

			speedModifier = data.speedModifier,

			footUIDs = data.footUIDs,

			legs = {},
		}

		for legIndex,leg in ipairs(data.legs) do
			sendData.legs[legIndex] = {
				footX = leg.footX,
				footY = leg.footY,
				fallFootY = leg.fallFootY,
				stepStartX = leg.stepStartX,
				stepStartY = leg.stepStartY,
				stepGoalX = leg.stepGoalX,
				stepGoalY = leg.stepGoalY,
				stepDuration = leg.stepDuration,
				stepTimer = leg.stepTimer,
			}
		end

		return sendData
	end,
	setExtraData = function(v,receivedData)
		local data = v.data

		data.footUIDs = receivedData.footUIDs
		data.startY = receivedData.startY

		if not data.initialised then
			lanky.initialise(v)
		end

		data.state = receivedData.state
		data.stateTimer = receivedData.stateTimer

		data.hitShakeTimer = receivedData.hitShakeTimer
		data.health = receivedData.health

		data.stepLegCount = receivedData.stepLegCount
		data.stepCycleCount = receivedData.stepCycleCount

		data.turnLowerCount = receivedData.turnLowerCount
		data.bounceTimer = receivedData.bounceTimer

		data.speedModifier = receivedData.speedModifier

		for legIndex,leg in ipairs(data.legs) do
			local legData = receivedData.legs[legIndex]

			leg.footX = legData.footX
			leg.footY = legData.footY
			leg.fallFootY = legData.fallFootY
			leg.stepStartX = legData.stepStartX
			leg.stepStartY = legData.stepStartY
			leg.stepGoalX = legData.stepGoalX
			leg.stepGoalY = legData.stepGoalY
			leg.stepDuration = legData.stepDuration
			leg.stepTimer = legData.stepTimer
		end
	end,
	shouldStealFunc = function(v)
		return false
	end,
}


local function getAverageFootPosition(v)
	local data = v.data

	local averageFootX = 0
	local averageFootY = 0

	for _,leg in ipairs(data.legs) do
		averageFootX = averageFootX + leg.footX/#data.legs
		averageFootY = averageFootY + leg.footY/#data.legs
	end

	return averageFootX,averageFootY
end

local function beginStep(v,leg,stepDuration)
	local config = NPC.config[v.id]

	local data = v.data

	leg.stepStartX = leg.footX
	leg.stepStartY = leg.footY

	leg.stepDuration = stepDuration or config.stepDuration
	leg.stepTimer = leg.stepDuration
end


local function getPlayerProximity(p,x,y)
	local distX = p.x + p.width*0.5 - x
	local distY = p.y + p.height*0.5 - y
	local distance = math.sqrt(distX*distX + distY*distY)

	local proximity = math.max(0,1 - math.max(0,distance - 512)/512)

	return proximity*proximity
end

local function getCameraProximity(camIdx,x,y)
	local proximity = 0

	for _,p in ipairs(battleCamera.getCamerasPlayers(camIdx)) do
		proximity = math.max(proximity,getPlayerProximity(p,x,y))
	end

	return proximity
end

local function getProximity(x,y)
	if camera2.isSplit then
		return math.min(getCameraProximity(1,x,y),getCameraProximity(2,x,y))
	else
		return getCameraProximity(1,x,y)
	end
end


local function doCameraShake(camIdx,magnitude,duration,x,y)
	local cam = Camera(camIdx)

	local proximity = getCameraProximity(camIdx,x,y)

	if proximity <= 0 then
		return
	end

	if cameraShakeData[camIdx] ~= nil and cameraShakeData[camIdx].magnitude*cameraShakeData[camIdx].timer > magnitude*proximity then
		return
	end

	cameraShakeData[camIdx] = {
		magnitude = magnitude*proximity,
		duration = duration,
		timer = 1,
	}
end

local function doShake(magnitude,duration,x,y)
	doCameraShake(1,magnitude,duration,x,y)

	if camera2.isSplit then
		doCameraShake(2,magnitude,duration,x,y)
	end
end


local function feetHaveReachedFallY(v)
	local data = v.data

	for _,leg in ipairs(data.legs) do
		if leg.footY < leg.fallFootY then
			return false
		end
	end

	return true
end

local function getMaxFootFallY(v)
	local data = v.data
	local maxY = -math.huge

	for _,leg in ipairs(data.legs) do
		maxY = math.max(maxY,leg.fallFootY)
	end

	return maxY
end


local function getLegRootPosition(v,leg)
	local config = NPC.config[v.id]
	local data = v.data

	return vector(v.x + v.width*0.5 + leg.direction*config.legRootOffsetX + data.hitShakeX,v.y + v.height*0.5 + config.legRootOffsetY + data.hitShakeY)
end

local function solveJointPositions(v,leg)
	local config = NPC.config[v.id]

	local targetPosition = vector(leg.footX,leg.footY - NPC.config[config.footNPCID].height)
	local rootPosition = getLegRootPosition(v,leg)

	local setPosition = rootPosition
	local jointCount = #leg.joints

	for _,joint in ipairs(leg.joints) do
		setPosition = setPosition + vector(leg.direction,-1)*joint.distance
		joint.position = setPosition
	end

	for iteration = 1,10 do
		-- Forward
		local lastJoint = leg.joints[jointCount]

		lastJoint.position = vector.v2(targetPosition)

		for jointIndex = jointCount,2,-1 do
			local previousJoint = leg.joints[jointIndex]
			local joint = leg.joints[jointIndex - 1]

			joint.position = previousJoint.position - (previousJoint.position - joint.position):normalise()*previousJoint.distance
		end

		-- Backward
		local firstJoint = leg.joints[1]

		firstJoint.position = rootPosition - (rootPosition - firstJoint.position):normalise()*firstJoint.distance

		for jointIndex = 2,(jointCount - 1) do
			local previousJoint = leg.joints[jointIndex - 1]
			local joint = leg.joints[jointIndex]

			joint.position = previousJoint.position - (previousJoint.position - joint.position):normalise()*joint.distance
		end
	end
end

local function createLeg(v,direction)
	local config = NPC.config[v.id]
	local footConfig = NPC.config[config.footNPCID]

	local data = v.data

	local leg = {}

	leg.direction = direction

	leg.offsetX = config.footDistance*direction

	leg.footX = v.x + v.width*0.5 + leg.offsetX
	leg.footY = tilemap.boxCastDown(leg.footX - footConfig.width*0.5 + 1,footConfig.width - 2, data.startY + 96,v.sectionObj.boundary.bottom, true) or (v.y + v.height*0.5 + config.headOffset)

	leg.stepTimer = 0

	leg.joints = {}

	for _,distance in ipairs(config.jointDistances) do
		table.insert(leg.joints,{
			position = vector.zero2,distance = distance,
			collider = Colliders.Rect(0,0,0,0,0),
		})
	end

	if data.state == STATE.INTRO_FALL then
		leg.fallFootY = leg.footY
		leg.footY = v.y + v.height*0.5 + config.headOffset
	end

	solveJointPositions(v,leg)

	return leg
end

function lanky.initialise(v)
	local config = NPC.config[v.id]
	local data = v.data

	data.state = STATE.INTRO_FALL
	data.stateTimer = 0

	data.hitShakeTimer = 0
	data.hitShakeX = 0
	data.hitShakeY = 0

	data.health = config.health

	data.startY = data.startY or (v.y + v.height*0.5)

	data.legs = {
		createLeg(v,DIR_LEFT),
		createLeg(v,DIR_RIGHT),
	}

	data.stepLegCount = #data.legs
	data.stepCycleCount = config.walkCycleCount

	data.turnLowerCount = 0

	data.bounceTimer = 0
	data.headOffset = 0

	data.speedModifier = 1

	if data.state == STATE.INTRO_FALL then
		v.y = getMaxFootFallY(v) - config.headOffset - v.height*0.5 - 256 - config.fallSpeed*config.fallTime
		v.friendly = true

		for _,leg in ipairs(data.legs) do
			if leg.footNPC ~= nil and leg.footNPC.isValid then
				leg.footNPC.friendly = true
			end

			leg.footY = v.y + v.height*0.5 + config.headOffset
		end

		SFX.play(config.fallSound,0.5)
	end

	-- Create feet NPCs, being careful to allow them to sync properly online
	data.footUIDs = data.footUIDs or {}

	for legIndex,leg in ipairs(data.legs) do
		if onlinePlay.currentMode ~= onlinePlay.MODE_CLIENT then
			leg.footNPC = NPC.spawn(config.footNPCID,leg.footX,leg.footY - NPC.config[config.footNPCID].height*0.5,v.section,false,true)
		end

		if onlinePlay.currentMode == onlinePlay.MODE_HOST then
			onlinePlayNPC.claimNPCSpawn(leg.footNPC)
			data.footUIDs[legIndex] = onlinePlayNPC.getUIDFromNPC(leg.footNPC) or -1
		elseif onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
			leg.footNPC = onlinePlayNPC.getNPCFromUID(data.footUIDs[legIndex])
		end
	end

	data.initialised = true
end


function lanky.onTickNPC(v)
	if Defines.levelFreeze then return end

	local data = v.data

	if v.despawnTimer <= 0 then
		data.initialised = false
		return
	end

	local config = NPC.config[v.id]
	local footConfig = NPC.config[config.footNPCID]

	if not data.initialised then
		lanky.initialise(v)
	end

	if data.state == STATE.WALK then
		local legIndex = (v.direction == DIR_RIGHT and (#data.legs - data.stepLegCount + 1)) or data.stepLegCount
		local leg = data.legs[legIndex]

		if leg.stepTimer == 0 then
			if data.stepCycleCount > 0 then
				data.stepLegCount = data.stepLegCount - 1

				if data.stepLegCount == 0 then
					data.stepLegCount = #data.legs
					data.stepCycleCount = data.stepCycleCount - 1
				end

				legIndex = (v.direction == DIR_RIGHT and (#data.legs - data.stepLegCount + 1)) or data.stepLegCount
				leg = data.legs[legIndex]

				leg.stepGoalX = leg.footX + config.stepWalkDistance*v.direction
				leg.stepGoalY = tilemap.boxCastDown(leg.stepGoalX - footConfig.width*0.5 + 1,footConfig.width - 2, leg.footY - 96,leg.footY + 128, true)

				--Colliders.Box(leg.stepGoalX - footConfig.width*0.5 + 1,leg.stepGoalY - config.headOffset,footConfig.width - 2,config.headOffset - 1):debug(true)

				if leg.stepGoalY ~= nil and tilemap.areaIsEmpty(leg.stepGoalX - 40,leg.stepGoalY - config.headOffset,leg.stepGoalX + 40,leg.stepGoalY - 24,false) then
					beginStep(v,leg)

					SFX.play(config.moveSound,math.lerp(0.25,1,getProximity(v.x + v.width*0.5,v.y + v.height*0.5)))
				elseif data.stepLegCount > 1 then
					leg.stepGoalX = leg.footX
					leg.stepGoalY = leg.footY
					data.stepCycleCount = 0

					beginStep(v,leg)

					SFX.play(config.moveSound,math.lerp(0.25,1,getProximity(v.x + v.width*0.5,v.y + v.height*0.5)))
				else
					data.state = STATE.TURN_WAIT
					data.stateTimer = 0
				end
			else
				data.state = STATE.TURN_WAIT
				data.stateTimer = 0
			end
		end
	elseif data.state == STATE.TURN_WAIT then
		data.stateTimer = data.stateTimer + data.speedModifier

		if data.stateTimer >= config.turnWaitTime then
			data.state = STATE.TURN_LOWER
			data.stateTimer = 0

			data.turnLowerCount = 0

			SFX.play(config.turnSound,math.lerp(0.25,1,getProximity(v.x + v.width*0.5,v.y + v.height*0.5)))
		end
	elseif data.state == STATE.TURN_LOWER then
		data.stateTimer = data.stateTimer + data.speedModifier
		
		local lowerTimer = math.min(1,data.stateTimer/config.turnLowerTime)

		data.headOffset = easing.inOutQuad(math.min(lowerTimer,1 - lowerTimer),0,config.turnLowerDistance,0.5)

		if data.stateTimer >= config.turnLowerStopTime then
			data.turnLowerCount = data.turnLowerCount + 1

			data.stateTimer = 0

			if data.turnLowerCount >= config.turnLowerCount then
				data.state = STATE.WALK
				v.direction = -v.direction

				data.stepLegCount = #data.legs
				data.stepCycleCount = config.walkCycleCount
			end
		end
	elseif data.state == STATE.DEATH then
		data.stateTimer = data.stateTimer + 1

		data.headOffset = easing.outBounce(math.clamp((data.stateTimer - 64)/64),0,80,1)

		if data.stateTimer >= 160 then
			v:kill(HARM_TYPE_NPC)
		end
	elseif data.state == STATE.INTRO_FALL then
		for _,leg in ipairs(data.legs) do
			leg.footY = math.min(leg.fallFootY,leg.footY + config.fallSpeed)
		end

		if feetHaveReachedFallY(v) then
			data.state = STATE.INTRO_WAIT
			data.stateTimer = 0
			data.bounceTimer = 2

			SFX.play(config.landSound,math.lerp(0.25,0.75,getProximity(v.x + v.width*0.5,getMaxFootFallY(v))))
			doShake(12,48,v.x + v.width*0.5,getMaxFootFallY(v))

			v.friendly = false

			for _,leg in ipairs(data.legs) do
				if leg.footNPC ~= nil and leg.footNPC.isValid then
					leg.footNPC.friendly = false
				end
			end
		end
	elseif data.state == STATE.INTRO_WAIT then
		data.stateTimer = data.stateTimer + 1

		if data.stateTimer >= 64 then
			data.state = STATE.WALK
			data.stateTimer = 0
		end
	end

	if data.bounceTimer > 0 then
		data.bounceTimer = math.max(0,data.bounceTimer - data.speedModifier/config.headBounceDuration)
		data.headOffset = math.sin(data.bounceTimer*math.pi*3)*data.bounceTimer*config.headBouceOffset
	end

	for _,leg in ipairs(data.legs) do
		-- Handle stepping
		if leg.stepTimer > 0 then
			leg.stepTimer = math.max(0,leg.stepTimer - data.speedModifier)

			local riseTimer = math.clamp(math.invlerp(0,config.stepRiseTime,1 - leg.stepTimer/leg.stepDuration))
			local lowerTimer = math.clamp(math.invlerp(config.stepRiseTime,1,1 - leg.stepTimer/leg.stepDuration))

			local riseY = math.min(math.max(leg.stepStartY,leg.stepGoalY) - config.stepMaxRiseDistance,math.min(leg.stepGoalY,leg.stepStartY) - config.stepMinRiseDistance)

			leg.footX = easing.linear(riseTimer + lowerTimer,leg.stepStartX,leg.stepGoalX - leg.stepStartX,2)

			if riseTimer < 1 then
				leg.footY = easing.outQuad(riseTimer,leg.stepStartY,riseY - leg.stepStartY,1)
			else
				leg.footY = easing.inQuad(lowerTimer,riseY,leg.stepGoalY - riseY,1)
			end

			if leg.stepTimer == 0 then
				leg.footX = leg.stepGoalX
				leg.footY = leg.stepGoalY

				data.bounceTimer = 1

				SFX.play(config.stepSound,math.lerp(0.25,1,getProximity(leg.footX,leg.footY)))
				doShake(6*data.speedModifier,16,leg.footX,leg.footY)
			end
		end

		-- Move the feet NPCs into place
		if leg.footNPC ~= nil and leg.footNPC.isValid then
			leg.footNPC.speedX = leg.footX - (leg.footNPC.x + leg.footNPC.width*0.5)
			leg.footNPC.speedY = leg.footY - (leg.footNPC.y + leg.footNPC.height)

			leg.footNPC.despawnTimer = math.max(10,leg.footNPC.despawnTimer)
		end
	end

	-- Shaking
	if data.hitShakeTimer > 0 then
		data.hitShakeTimer = math.max(0,data.hitShakeTimer - 1/12)

		local hitShake = vector.randomInCircle(data.hitShakeTimer*10)
		
		data.hitShakeX = hitShake.x
		data.hitShakeY = hitShake.y
	end

	-- Move the main body relative to the feet
	local averageFootX,averageFootY = getAverageFootPosition(v)

	v.speedX = averageFootX - (v.x + v.width*0.5)
	v.speedY = (averageFootY - config.headOffset + data.headOffset) - (v.y + v.height*0.5)

	-- Move the legs relative to the body and the feet
	local legMinX = math.huge
	local legMinY = math.huge
	local legMaxX = -math.huge
	local legMaxY = -math.huge

	for _,leg in ipairs(data.legs) do
		solveJointPositions(v,leg)

		for jointIndex,joint in ipairs(leg.joints) do
			-- Update AABB
			legMinX = math.min(legMinX,joint.position.x)
			legMinY = math.min(legMinY,joint.position.y)
			legMaxX = math.max(legMaxX,joint.position.x)
			legMaxY = math.max(legMaxY,joint.position.y)

			-- Update collider
			local previousPoint = (jointIndex > 1 and leg.joints[jointIndex - 1].position) or getLegRootPosition(v,leg)
			local point = joint.position

			joint.collider.rotation = math.deg(math.atan2(point.y - previousPoint.y,point.x - previousPoint.x))

			joint.collider.width = (previousPoint - point).length
			joint.collider.height = config.legThickness

			joint.collider.x = (previousPoint.x + point.x)*0.5
			joint.collider.y = (previousPoint.y + point.y)*0.5

			--joint.collider:draw()
		end
	end

	-- Leg collision
	--Colliders.Box(legMinX,legMinY,legMaxX - legMinX,legMaxY - legMinY):draw()

	for _,p in ipairs(Player.getIntersecting(legMinX - 64,legMinY - 64,legMaxX + 64,legMaxY + 64)) do
		if p.forcedState == FORCEDSTATE_NONE then
			local playerCollider = Colliders.getHitbox(p)

			for _,leg in ipairs(data.legs) do
				for _,joint in ipairs(leg.joints) do
					if joint.collider:collide(playerCollider) then
						p:harm()
					end
				end

				-- Prevent the player from falling off the foot if it's moving too fast
				if leg.footNPC ~= nil and leg.footNPC.isValid and p.standingNPC == leg.footNPC then
					p.y = p.y + math.max(0,leg.footNPC.speedY - Defines.gravity)
				end
			end
		end
	end


	v.despawnTimer = math.max(10,v.despawnTimer)
end


local function drawLegPart(image,priority,start,stop)
	local length = (start - stop).length

	Graphics.drawBox{
		texture = image,priority = priority,
		centred = true,sceneCoords = true,

		x = (start.x + stop.x)*0.5,
		y = (start.y + stop.y)*0.5,

		sourceWidth = length,
		sourceX = -length*0.5,

		rotation = math.deg(math.atan2(stop.y - start.y,stop.x - start.x)),

		shader = wrapShader,
	}
end

function lanky.onDrawNPC(v)
	if v.despawnTimer <= 0 then
		return
	end

	local config = NPC.config[v.id]
	local data = v.data

	if not data.initialised then
		lanky.initialise(v)
	end

	local priority = -16

	v.animationFrame = npcutils.getFrameByFramestyle(v,{frame = (data.health > 0 and data.health <= config.angryHealth and 1) or 0})

	-- Legs
	for _,leg in ipairs(data.legs) do
		drawLegPart(config.legImage,priority,getLegRootPosition(v,leg),leg.joints[1].position)

		for jointIndex = 1,(#leg.joints - 1) do
			local joint = leg.joints[jointIndex]
			local nextJoint = leg.joints[jointIndex + 1]

			drawLegPart(config.legImage,priority,joint.position,nextJoint.position)
		end
	end

	-- Joints
	for _,leg in ipairs(data.legs) do
		for _,joint in ipairs(leg.joints) do
			Graphics.drawBox{
				texture = config.jointImage,priority = priority,
				centred = true,sceneCoords = true,
				x = joint.position.x,
				y = joint.position.y,
			}
		end
	end

	Graphics.drawBox{
		texture = Graphics.sprites.npc[v.id].img,priority = priority,
		centred = true,sceneCoords = true,

		x = v.x + v.width*0.5 + config.gfxoffsetx + data.hitShakeX,
		y = v.y + v.height - config.gfxheight*0.5 + config.gfxoffsety + data.hitShakeY,
		sourceWidth = config.gfxwidth,
		sourceHeight = config.gfxheight,
		sourceY = v.animationFrame*config.gfxheight,

		shader = solidColorShader,uniforms = {
			color = Color.white,
			extremity = data.hitShakeTimer*0.75,
		},
	}

	npcutils.hideNPC(v)

	--[[Text.print(data.stepCycleCount,32,32)
	Text.print(data.stepLegCount,32,64)
	Text.print(math.abs(data.legs[1].footX - data.legs[2].footX),32,96)]]

	--[[for _,leg in ipairs(data.legs) do
		Graphics.drawLine{
			color = Color.green,
			sceneCoords = true,
			start = getLegRootPosition(v,leg),
			stop = leg.joints[1].position,
		}

		for jointIndex = 1,(#leg.joints - 1) do
			local joint = leg.joints[jointIndex]
			local nextJoint = leg.joints[jointIndex + 1]

			Graphics.drawLine{
				color = Color.green,
				sceneCoords = true,
				start = joint.position,
				stop = nextJoint.position,
			}
		end
	end]]
end


function lanky.onNPCHarm(eventObj,v,reason,culprit)
	if v.id ~= npcID or eventObj.cancelled then
		return
	end

	local config = NPC.config[v.id]
	local data = v.data

	if not data.initialised then
		return
	end

	if reason == HARM_TYPE_JUMP or reason == HARM_TYPE_SPINJUMP then
		if type(culprit) == "Player" then
			culprit.speedX = math.sign((culprit.x + culprit.width*0.5) - (v.x + v.width*0.5))*5
		end
	end

	if data.health <= 0 or data.hitShakeTimer > 0 or v:mem(0x156,FIELD_WORD) > 0 or data.state == STATE.DEATH then
		if reason == HARM_TYPE_JUMP then
			SFX.play(2)
		end

		eventObj.cancelled = true
		return
	end

	local damage = config.normalDamage

	if reason == HARM_TYPE_JUMP or reason == HARM_TYPE_SPINJUMP then
		damage = config.jumpDamage
	elseif reason == HARM_TYPE_NPC then
		if type(culprit) == "NPC" then
			if culprit.id == 13 then
				damage = config.fireballDamage
			elseif culprit.id == 171 then
				damage = config.hammerDamage
			end

			culprit:harm(HARM_TYPE_NPC)
		end
	end

	data.health = data.health - damage

	if data.health > 0 then
		data.speedModifier = (data.health <= config.angryHealth and config.angrySpeedModifier) or 1

		data.hitShakeTimer = 1
	else
		data.state = STATE.DEATH
		data.stateTimer = 0

		data.hitShakeTimer = 2

		data.bounceTimer = 1
		data.headOffset = 0

		for _,leg in ipairs(data.legs) do
			if leg.stepTimer > 0 then
				leg.footX = leg.stepStartX
				leg.footY = leg.stepStartY
				leg.stepTimer = 0

				if leg.footNPC ~= nil and leg.footNPC.isValid then
					leg.footNPC.x = leg.footX - leg.footNPC.width*0.5
					leg.footNPC.y = leg.footY - leg.footNPC.height
				end
			end
		end
	end

	v:mem(0x156,FIELD_WORD,config.invincibilityTime)

	SFX.play(39)

	eventObj.cancelled = true
end

function lanky.onPostNPCKill(v,reason)
	if v.id ~= npcID then
		return
	end

	local config = NPC.config[v.id]
	local footConfig = NPC.config[config.footNPCID]

	local data = v.data

	if not data.initialised then
		return
	end

	-- Remove foot NPCs
	for _,leg in ipairs(data.legs) do
		if leg.footNPC ~= nil and leg.footNPC.isValid then
			leg.footNPC.animationFrame = -999
			leg.footNPC:kill(HARM_TYPE_VANISH)
		end
	end

	-- Goop effects
	if reason ~= HARM_TYPE_VANISH then
		local images = {}
		local clumps = {}

		-- Legs
		for _,leg in ipairs(data.legs) do
			for jointIndex,joint in ipairs(leg.joints) do
				local previousPoint = (jointIndex > 1 and leg.joints[jointIndex - 1].position) or getLegRootPosition(v,leg)
				local point = joint.position

				local rotation = math.deg(math.atan2(point.y - previousPoint.y,point.x - previousPoint.x))
				local length = (previousPoint - point).length

				local x = (previousPoint.x + point.x)*0.5
				local y = (previousPoint.y + point.y)*0.5

				table.insert(images,{
					texture = config.legImage,
					x = x,
					y = y,
					width = length,
					sourceX = -length*0.5,
					rotation = rotation,
				})

				table.insert(clumps,{
					x = x,
					y = y,
					radius = length*0.25,
					eccentricity = 0.8,
					rotation = rotation,
				})
			end
		end

		-- Joints
		for _,leg in ipairs(data.legs) do
			for _,joint in ipairs(leg.joints) do
				table.insert(images,{
					texture = config.jointImage,
					x = joint.position.x,
					y = joint.position.y,
				})
			end
		end

		-- Foot
		for _,leg in ipairs(data.legs) do
			table.insert(images,{
				texture = Graphics.sprites.npc[config.footNPCID].img,
				x = leg.footX,
				y = leg.footY - footConfig.gfxheight*0.5,
			})

			table.insert(clumps,{
				x = leg.footX,
				y = leg.footY - footConfig.gfxheight*0.5,
				radius = (footConfig.gfxwidth + footConfig.gfxheight)*0.25,
				eccentricity = 0.75,
			})
		end

		weirdGoop.createDestructionEffect{
			images = images,
			clumps = clumps,

			particleWaitTime = 64,
			disintegrateTime = 96,

			particleSpeed = 0.5,

			countFactor = 0.03,
		}

		-- Mask
		Effect.spawn(config.deathEffectID,v.x + v.width*0.5,v.y + v.height*0.5)
	end
end


function lanky.onCameraDraw(camIdx)
	if cameraShakeData[camIdx] == nil then
		return
	end

	local cam = Camera(camIdx)

	local shakeData = cameraShakeData[camIdx]
	local priority = 0.1

	local shakeOffset = math.floor(math.sin((1 - shakeData.timer)*shakeData.duration*math.pi/3)*shakeData.timer*shakeData.magnitude + 0.5)

	shakeBuffer:captureAt(priority)
	
	Graphics.drawBox{
		texture = shakeBuffer,priority = priority,
		sourceWidth = cam.width,
		sourceHeight = cam.height,
		sourceX = 0,
		sourceY = shakeOffset,
		x = 0,
		y = 0,
	}

	shakeData.timer = math.max(0,shakeData.timer - 1/shakeData.duration)
end


enemies.config[npcID] = {
	droppedMaskCount = 5,
}


return lanky