Page 1 of 1

[script] Alternative fighting method with mouse gestures

Posted: Thu Feb 19, 2015 11:10 pm
by JKos
This script adds an alternative fighting method alongside of the native fighting, which means
that you can use "mouse gestures" or native method, both will work in parallel. The goal of this script was to reduce the time spent for staring of the right bottom corner of the screen, and I think that I succeeded.
I wanted to keep this simple as possible, so actually there isn't many gestures used, only action where gestures is used is selecting the active champion,
so don't look any fancy gesture detection algorithms from this script :)

INSTALLATION
Just copy paste this script to some script entity, for example named mouse_gesture_fighting.
And you must have party.onDrawGui-hook defined, it can be empty or not, it's just needed for the onDrawGui-connector which doesn't get called if onDrawGui-hook is not defined.

Code: Select all

--You can modify these properties

--Show instructions on Gui
showInstructions = true
-- if false you must call enable() to enable mouse gesture fighting
-- and you can call disable() to disable it  
enabledByDefault = true 


--Don't modify below this unless you know what you are doing
--This script is pretty messy, sorry about that. But it works so I don't mind :)
rmDown=false --RMB down
lmDown = false --LMB down
rmStartPos = {} --RMB press position 
rmStartTime = 0 --RMB press start time
activeChampion = false --Number of the active champion (0-4)
powerAttack = false --Power attack buildup started
powerAttackReady = false --Power attack fully charged
powerAttackBuildupTime = false --Buildup time
powerAttackThreshold = 0.7 --How long RMB must be pressed before the power attack starts charging
powerAttackAlpha = 0
alphaModifier = 10
cancelled = false --Action cancelled
secondHandActivated = false
runeWidth =  50
castingSpell = false
castStartX = 0
castStartY = 0
selectedRunes = ''
prevRune = 0
prevRune2 = 0


function enable()
	party.party:addConnector('onDrawGui',self.go.id,'main')
end

function disable()
	party.party:removeConnector('onDrawGui',self.go.id,'main')
end
if enabledByDefault then
	enable()
end


function main(party,g)
	if showInstructions then 
		instructions(g)
	end
	if cancelled then
		--cancelled and mouse released
		if not g.mouseDown(0) then
			reset()
		end
		return
	end

	if castingSpell then
		checkCancel(g,400)
		castSpell(g)
		return
	end

	if g.mouseDown(0) then
		onLeftMouseDown(party,g)
	else 
		if rmDown then
			if activeChampion then -- mouse button released
				local champ = party:getChampion(activeChampion)
				championAttack(g,champ)
			else
				reset()
			end
		end
	end 

end

function onLeftMouseDown(party,g)
	checkCancel(g,400)
	if not rmDown then -- mouse button pressed 
		rmStartPos = {g.mouseX,g.mouseY}
		rmStartTime = Time.currentTime()
		rmDown=true
		GameMode.setGameFlag('DisableMouseLook',true)
	else -- mouse button hold
		checkChangeHand(g)
		if not activeChampion then
			activeChampion = getChampionNumber(g)
		end
		if activeChampion then
			onChampionActive(g)
		end
	end
end

function onChampionActive(g)
	local champ = party.party:getChampion(activeChampion)
	local readyHand = championGetReadyHand(champ)

	if readyHand then
		local item = champ:getItem(readyHand)
		if not powerAttack and item and Time.currentTime() - rmStartTime > powerAttackThreshold and championCanDoPowerAttack(champ,readyHand) then			
			powerAttackBuildupTime = itemGetPowerAttack(item):getBuildup()
			playSound('power_attack_charging')
			powerAttack = true
		end
		
		if item then 
			if powerAttack then
				drawPowerAttack(g,item)
			end
			g.color(255,255,255,255)

			drawItem(g,item,g.mouseX,g.mouseY)
		else
			if champ:hasTrait('hand_caster') then
				g.drawGuiItem('UnarmedAttackMageLeft',g.mouseX,g.mouseY)
			else
				g.drawGuiItem('UnarmedAttackLeft',g.mouseX,g.mouseY)
			end
		end
	end
end




function checkCancel(g,distanceFromRightEdge)
		if g.mouseX > g.width - distanceFromRightEdge or getMouseItem() then
			cancelled = true
		end
end

function reset()
		rmDown=false
		rmStartPos = {}
		powerAttackAlpha = 0
		powerAttackReady = false
		alphaModifier = 10
		powerAttack = false
		rmStartTime = 0
		activeChampion = false
		powerAttackBuildupTime = 0
		castingSpell = false
		cancelled = false
		secondHandActivated = false
		lmDown = false
		GameMode.setGameFlag('DisableMouseLook',false)
end

function drawPowerAttack(g,item)
	if not powerAttackReady and rmStartTime + powerAttackThreshold + powerAttackBuildupTime < Time.currentTime() then
		--print(rmStartTime,powerAttackBuildupTime,Time.currentTime())
		powerAttackReady = true
		playSound('power_attack_ready')
	end						
	
	powerAttackAlpha = powerAttackAlpha + alphaModifier
	
	if powerAttackAlpha >= 255 then 
		powerAttackAlpha = 255
		alphaModifier = -1 
	end
	
	if alphaModifier < 0 and powerAttackAlpha < 150 then 
		alphaModifier = 1
	end
	g.color(255,255,255,powerAttackAlpha)
	drawItem(g,item,g.mouseX,g.mouseY,powerAttack)
end

function itemGetPowerAttack(item)
	local pattack = item:getSecondaryAction()
	if pattack then
		return item.go:getComponent(pattack)
	end
end

function championCanDoPowerAttack(champ,hand)
	local item = champ:getItem(hand)
	if not item then return false end
	local powerAttack = itemGetPowerAttack(item)
	if not powerAttack then return false end
	if champ:getEnergy() < powerAttack:getEnergyCost() then
		return false
	end
	
	local requirements = powerAttack:getRequirements()
	if not requirements then return true end
	for i=1,#requirements,2 do
		if champ:getSkillLevel(requirements[i]) < requirements[i+1] then
			return false
		end
	end
	--print( powerAttack:getUiName())
	return true
end

function drawItem(g,item,x,y,pAttack)
	local index = item:getGfxIndex()
	if pAttack then
		index = item:getGfxIndexPowerAttack()
	end

	local atlasNumber = math.floor(index/200)
	if index >= 200 then 
		index = index - 200 * (atlasNumber)
	end
	local atlasIndex = index%169
	local row = math.floor(atlasIndex/13)
	local col = atlasIndex%13	
	local atlas = 'assets/textures/gui/items.tga'
	if atlasNumber > 0 then
		atlas = 'assets/textures/gui/items_'..(atlasNumber+1)..'.tga'
	end
	g.drawImage2(atlas, x, y, col*75, row*75, 75,75, 75, 75)

end

function checkChangeHand(g)
	if g.mouseDown(2) then
		if not lmDown then
			lmDown = true
			secondHandActivated = not secondHandActivated
			powerAttack = false
			powerAttackReady = false
			powerAttackBuildupTime = false
			powerAttackAlpha = 0
			alphaModifier = 10
			rmStartTime = Time.currentTime()
		end
	else
		lmDown = false
	end

end

function championPowerAttack(champ,hand)
	local item = champ:getItem(hand)
	if not item then return false end
	local powerAttack = itemGetPowerAttack(item)
	champ:attack(hand,true)
	champ:setEnergy(champ:getEnergy() - powerAttack:getEnergyCost())
end

function championGetReadyHand(champ)

	if secondHandActivated then
		local item = champ:getItem(2)
		if item then 
			if (itemIsWeapon(item) or itemIsWand(item)) and champ:isReadyToAttack(2) then
				return 2
			end
		elseif champ:isReadyToAttack(2) then 
			return 2
		end
	end


	if champ:isReadyToAttack(1) then
		local item = champ:getItem(1)
		if item then
			if itemIsWeapon(item) or itemIsWand(item) then
				return 1
			end
		elseif champ:hasTrait('hand_caster') then
			return 1
		end
	end
	if champ:isReadyToAttack(2) then
		local item = champ:getItem(2)
		if item and (itemIsWeapon(item) or itemIsWand(item)) then --dual wielding
			return 2
		end
		if not item then
			return 2
		end
	end
end

function itemIsWand(item)
	return item.go:getComponent('runepanel') ~= nil

end

function itemIsWeapon(item)
	if not item then return false end
	return item.go.meleeattack or item.go.rangedattack or item.go.throwattack or item.go.firearmattack
end

function championAttack(g,champ)
	local hand = championGetReadyHand(champ)
	if not hand then 
		reset()
		return 
	end
	if powerAttackReady and championCanDoPowerAttack(champ,hand) then
		championPowerAttack(champ,hand)
		reset()
	else
		local item = champ:getItem(hand)
		if item then
			if itemIsWeapon(item) then
				champ:attack(hand,false)
				reset()
			elseif itemIsWand(item) then
				castSpell(g)
			end
		else
			if champ:hasTrait('hand_caster') and champ:isReadyToAttack(1) then
				castSpell(g)
			else
				champ:attack(hand,false)
				reset()
			end
		end
	end
end

function getDirection(g)
	local distX = rmStartPos[1] - g.mouseX
	local distY = rmStartPos[2] - g.mouseY
	if math.abs(distX)  < 50 and math.abs(distY)  < 50 then
		return nil
	end
	local dirX = iff(distX < 0,1,-1)
	local dirY = iff(distY < 0,1,-1)
	return dirX,dirY
end

function getChampionNumber(g)
	local dirX,dirY = getDirection(g)
	if dirX == nil then return false end
	if dirX == -1 and dirY == -1 then return 1 end
	if dirX == 1 and dirY == -1 then return 2 end
	if dirX == -1 and dirY == 1 then return 3 end
	return 4
end




function castSpell(g)
	if not castingSpell then
		castingSpell = true
		selectingRunes = false
		castStartX,castStartY = g.mouseX,g.mouseY
	else
		if not selectingRunes and g.mouseDown(0) then 
			selectingRunes = true
		end
		if selectingRunes and not g.mouseDown(0) then
			castingSpell = false
			selectingRunes = false
			--hudPrint('selected runes '..selectedRunes)
			local champ = party.party:getChampion(activeChampion)
			if selectedRunes ~= '' then
				champ:castSpell(tonumber(selectedRunes))
				
			end
			selectedRunes = ''
			prevRune = 0
			reset()
		end
		drawRunes(g)
	end

end

function drawRune(g,number,x,y,w,h)
	g.drawGuiItem2('SpellRunes', x, y, number*68-68, 0, 68, 60, w, h)
	
end

function drawRunes(g)

	for i=1,9 do
		local x,y = (i-1)%3*runeWidth,math.floor((i-1)/3)*runeWidth
		if string.find(selectedRunes,tostring(i)) then
			g.color(255, 255, 255, 255)
		else
			g.color(150, 150, 150, 255)
		end
		drawRune(g,i,castStartX+x,castStartY+y,runeWidth,runeWidth)
		if g.mouseDown(0) and prevRune ~= i and prevRune2 ~= i and guiMouseInRect(g,castStartX+x,castStartY+y,runeWidth,runeWidth) then
			selectedRunes = selectedRunes..i
			prevRune2 = prevRune
			prevRune = i
		end
	end
end

function guiMouseInRect(g, bx, by, bw, bh)
	return (g.mouseX >= bx) and (g.mouseX <= bx + bw) 
		and (g.mouseY >= by) and (g.mouseY <= by + bh)
end


function instructions(g)

	local w,h,x,y,gap = 300,0,200,30,20
	w,h = g.drawParagraph("ATTACKING\n\nHold the right mouse button, drag it towards some corner of the screen and release. \nLeft up = champion 1, \nRight up = champion 2 etc...",x,y,w)
	y = y + h + gap
	w,h = g.drawParagraph("POWER ATTACK\n\nHold the mouse until the power attack is charged and then release the button.",x,y,w)
	y = y + h + gap	
	w,h = g.drawParagraph("CHANGING WEAPONS\n\nRight click while holding left mouse button.",x,y,w)
	y = y + h + gap
	w,h = g.drawParagraph("CASTING A SPELL\n\nTry Attacking with wizard, you will figure it out.",x,y,w)
	y = y + h + gap
	w,h = g.drawParagraph("DUAL WIELD ATTACK\n\nYou can attack 2 times in a row quickly while dual wielding(no need to change the weapon).",x,y,w)
	y = y + h + gap
	w,h = g.drawParagraph("CANCEL ACTION\n\nMove mouse to the right edge of the screen while holding the LMB.",x,y,w)
	y = y + h + gap
	g.drawGuiItem('ButtonClose',x,y)
	if g.button('close',x,y,100,30) then
		showInstructions = false
	end
	g.color(255,255,255,20)
	g.drawRect(g.width-400,0,400,g.height)
	g.color(255,255,255,255)
	w,h = g.drawParagraph("CANCEL ATTACK AREA",g.width-350,300,400)

end
Feel free to use or modify it in any way you like. And any feedback (positive or negative) is appreciated.


The original opening post:
SpoilerShow
I have seen people complaining that they would like to be able to watch the monsters while in battle instead of the bottom right corner of the screen. It don't see it as a big issue, but it would be nice to be able to watch those beautiful graphics more. So I just started to develop an alternative attack system with mouse gestures and I was able to put together something promising (In my opinion). But I would like to get some feedback from other people before I sink more time in to it, so do you guys think that this is something that is worth developing further or is it just a bad idea? Would you like to use something like this in your mods?

Here is a really simple dungeon for testing purposes:
https://drive.google.com/file/d/0B7cR7s ... sp=sharing
ATTENTION: this is very experimental and unpolished feature, so expect some problems, but I hope you get the idea.

How it works:

Attacking with weapons
(only left hand weapons supported atm., and no dual wielding )

Just hold left mouse button down, drag and release.
Left up: Champion 1
Right up: Champion 2
Left down: Champion 3
Right down: Champion 4

Special attack
(This is really simple and unfinished feature, and I'm not even sure if it can be implemented properly)
hold mouse button over 0.5 seconds


Casting spells

Just like attacking with weapons, but when you release the button the runes are drawn at the location of the mouse cursor and you can select runes by holding the mouse button down again.
Current runes are ugly, and if I'm going to continue developing this further I will definitely replace them with better looking images.

Re: Attacking by mouse gestures (experimental)

Posted: Thu Feb 19, 2015 11:23 pm
by minmay
Definitely an interesting idea. My approach to this was to try to implement unified keyboard controls but it's awkward since you can't change the default party movement behaviour (which is incompatible with sane control schemes).

Re: Attacking by mouse gestures (experimental)

Posted: Thu Feb 19, 2015 11:25 pm
by Azel
Grimrock is a good setting for this due to Arx Fatalis being one of its inspirations; which uses gestures as a game mechanic.

Reading your post, other games do come to mind that add intriguing complexity: Black & White (gestures for magic) and Blade of Darkness (intricate hand-to-hand combat mechanics).

If done correctly it could work wonders for the world of Grimrock. One immediate benefit would be the limitation of relying solely on "square dancing" for combat, since it's harder to square dance while making gestures for all forms of effective combat. More importantly... if blocking/parrying could be accomplished via a gesture system, then FINALLY the world of Grimrock can support true "face tanking."

I'd be down for this 100%.

Re: Attacking by mouse gestures (experimental)

Posted: Sat Feb 21, 2015 9:30 pm
by Drakkan
I just wanted to say I am impressed by the scripitng and that you have managed to do it. Speaking for myself I will not use this as I like more standard log system. I think default "clicking" system it fits more to the typical dungeon crawlers. I am sure some mods could use this "new" mechanics.
nice work

Re: Attacking using mouse gestures (experimental)

Posted: Sun Mar 08, 2015 12:08 am
by JKos
Thank for the comments guys, I decided to develop this further and I'm pretty pleased to the results so far, IMO it makes fighting more fun and visual experience.
Demo dungeon is published in steam workshop:
http://steamcommunity.com/sharedfiles/f ... =404277589

I will release the script in near future, it needs some polishing and there is probably a few bugs still. I'm going to make it optional so that you can turn it off if you don't want to use it. This way it can be added to any dungeon as an optional feature. I really would like to test this with some more complete dungeon but I haven't had the time to build such by myself.

Re: [script] Alternative fighting method with mouse gestures

Posted: Sun Mar 08, 2015 9:04 pm
by JKos
Script released, opening post updated.