Spell – rune order

Ask for help about creating mods and scripts for Grimrock 2 or share your tips, scripts, tools and assets with other modders here. Warning: forum contains spoilers!
User avatar
.rhavin
Posts: 25
Joined: Fri Oct 13, 2017 1:10 pm
Location: Berlin
Contact:

Spell – rune order

Post by .rhavin »

hi there

Is it possible to get notified if a rune is selected?
Is it possible to change the rune-tile?

In other words: is a DM-like behaviour doable where the runes you'd able to add change with every rune selected? :?:
User avatar
Isaac
Posts: 3185
Joined: Fri Mar 02, 2012 10:02 pm

Re: Spell – rune order

Post by Isaac »

I believe that it is ~possible, but that it is not built in behavior, and that you would have to script the entire system for this; including re-implementing the drag-path behavior on the runes, if you wanted it, and all of the other graphics involved. It might not be possible to make it perfect.
User avatar
.rhavin
Posts: 25
Joined: Fri Oct 13, 2017 1:10 pm
Location: Berlin
Contact:

Re: Spell – rune order

Post by .rhavin »

Lets say I want this tiles: how would you do it? Can someone post a simple example? Creating other graphipcs on request is no problem.
Image, Image
Image, Image
User avatar
Isaac
Posts: 3185
Joined: Fri Mar 02, 2012 10:02 pm

Re: Spell – rune order

Post by Isaac »

.rhavin wrote:Can someone post a simple example?
No... :? (That's the point... it won't be simple at all...)

The runes are nice. What you are asking, I don't think many (if any) have tried. What it entails is scripting a feature that reads the mouse input over the spell caster's attack panel, and uses the graphic drawing functions to write your bitmaps to the screen; while tracking the state of your (more complicated) rune panel; and casting the spells that come of the interaction with it. It is possible to indicate and cast spells with rune combinations that are not adjacent or consecutive, like the ones in the game. If your script can discern the mouse position and clicks that occur in each attack panel, then you can animate the menu and call these spells to be cast.

I do not have any ready-made examples, but you will need to have a decent command of the LUA scripting language, and to use the gui.context drawing functions. Those can be seen here: https://github.com/JKos/log2doc/wiki/Ob ... icscontext

The button drawing function creates a region anywhere on the screen, and detects clicks within the region. You would use that to detect clicks on your tiles, and trigger changes to the rune panel layout; to reveal new runes, and remove unrelated ones, etc...

The idea is to write a function that uses these functions to create the UI panel. Your function gets called every frame, and has to decide what to draw—each frame —each time it is called. Of particular note is the party.onDrawAttackPanel() hook, seen here:
https://github.com/JKos/log2doc/wiki/Co ... -component

This hook can be set to call your function, and provide gui context, and the screen region of the (specific) attack panel being drawn. The game draws four attack panels, and provides enough information to detect which champion is being affected by the current call.

I have an example of generating a menu; it does not use the attack panel, but the premise and function calls are more or less the same, and illustrate the process. What this does, is composite a custom menu using a three state button texture, and remembers what button was pressed.
https://www.dropbox.com/s/h359z7ayvv0r1 ... o.zip?dl=0
Image

**The video recorded an incoming Skype notification... so expect it when played.
User avatar
.rhavin
Posts: 25
Joined: Fri Oct 13, 2017 1:10 pm
Location: Berlin
Contact:

Re: Spell – rune order

Post by .rhavin »

Thanx for your code, i already have a working panel now with mouse hover highlighting and automatic change of tiles that can combine my runes and craft spells.
Now - how do I hook into the spell-panel? Basically, I want my panel to pop up instead of the games spell-panel or at least above (before) it.

Or - even better: how do i completely disable the games spell-panel so that a right-click on the hands always become either a weapon or a unarmed attack? I'd prefer to have my spell-panel above the character icons. Of course, a third way to do it would be a third plane for the character hands. Is that possible?
User avatar
Isaac
Posts: 3185
Joined: Fri Mar 02, 2012 10:02 pm

Re: Spell – rune order

Post by Isaac »

Minmay seems to have the most experience with the internal behavior of the engine—short of Petri himself. He's really the one to ask.

Probably the easiest way to add a custom spell menu is through an equipable item, but if you want the right-click panel, then from what I've seen, just remove the 'hand_caster' trait from the definitions for the character classes that have it, and none should be able to activate it, except through items. There might be a much better way.

The hand_caster trait is special, but if you define your own trait named 'hand_caster', then you can add it or remove it from PCs during the game; provided their class didn't have it in the definition. Not having the hand_caster trait means they attack with empty hands, instead of open a rune panel. This can be tested for. Presumably one could test for (and cancel) unarmed attacks by any PC with an alternative (user defined) caster trait present (to mark them as spell casters), and then open a custom spell panel on right-clicks to that PC's attack panel.

Testing for unarmed attacks is not as straightforward as one would think, because for some reason they don't trigger the onAttack party hook. The only way I know of to test for unarmed attacks is by using one of the Compute hooks found in the definitions for skills, traits, and conditions.

**It does come to mind that a more complex spell panel could come from having greater skill, and/or some new magic tome. As such the normal spell panel could work for novice casters, and the advanced panel for an arch-mage. This might be possible by using a tome item, or by removing the hand_caster trait from all classes, then restoring it (at-load) to any class with spell-skills; and later changing it to a 'master_caster' trait once their skill level is high enough. (The specific caster trait determining what spell menu is used.)
User avatar
Isaac
Posts: 3185
Joined: Fri Mar 02, 2012 10:02 pm

Re: Spell – rune order

Post by Isaac »

Update: I have been tinkering around with this, and had some neat progress, but every idea seems to come with bad side effects. I had forgotten that (though detectable)—afaik, unarmed attacks cannot be canceled.

I'm beginning to think that (barring new information), that the menu idea will almost have to come in the form of a magical inventory item, that opens the menu on use.

If there is ever another Glögg session (and there most likely won't be :| ), I will ask for a dedicated means to disable the PC without decorations or UI side effects; to more easily detect (and/or cancel) unarmed attacks, and also a way to limit or occlude UI interaction when desired... because it accepts attacks and draws spells underneath custom menu graphics.
User avatar
.rhavin
Posts: 25
Joined: Fri Oct 13, 2017 1:10 pm
Location: Berlin
Contact:

Re: Spell – rune order

Post by .rhavin »

First concept Demo, just in the upper left corner without scaling, call skript function showPanel() to activate. This is my first time coding in Lua, so any hints appreciated ;)

you'll need those files:

http://doc.rhavin.de/dmf/runes.dds
http://doc.rhavin.de/dmf/runes_h.dds
http://doc.rhavin.de/dmf/runes_s.dds

Code: Select all


function showPanel()
	party.party:addConnector('onDrawGui', self.go.id, "renderRunePanel")
end

function hidePanel()
	party.party:removeConnector('onDrawGui', self.go.id, "renderRunePanel")
end

-- definition of spells
QDMF_Spell = {}
QDMF_Spell.__index = QDMF_Spell

-- definition of runes
QDMF_Rune = {}
QDMF_Rune.__index = QDMF_Rune
QDMF_Rune.tiers = 3
QDMF_Rune.ways  = 9

-- the spellphrase-pseudoclass-ctor
function QDMF_PhraseCreate(runestring)
	local phrase = {}

	--[[
	# clear this phrase ]]--
	function phrase.clear(self)
		self.name    = ""
		self.align   = ""
		self.runes   = {}
		self.power   = 0
		self.cost    = 0
		self.minimum = 0
		self._length = 0
		self._valid  = false
		self._tested = false
		self.string  = ""
	end

	--[[
	# add a rune to the phrase, invalidating it ]]--
	function phrase.add(self, rune, pos)
		local word
		if (rune == nil) then
			return false end
		if (type(rune) ~= "table") then
			rune = QDMF_Rune[rune]
			if (rune == nil) then  return false end
		end
		if self:hasRuneIndex(rune.index) then
			return false end
			
		if (rune.tier == 0) then
			self.power = rune.way + 1 -- need 0 for "no powerrune selected"
			return true end
			
		if (pos ~= nil and pos > 0) then
			self.runes[pos - 1] = rune
		else
			self.runes[self._length] = rune
			self._length = self._length + 1
		end

		self._tested = false
		self._valid  = false
	end

	--[[
	# get effective length including optional power rune ]]--
	function phrase.getLength(self)
		if (self.power == 0) then
			return self._length
		else
			return self._length + 1
		end
	end

	--[[
	# get the phrases rune-tier by index ]]--
	function phrase.getTierAt(self, index)
		local rune = self:getRuneAt(index)
		if (rune == nil) then
			return -1
		end
		return rune.tier
	end
	
	--[[
	# get the phrases rune by index ]]--
	function phrase.getRuneAt(self, index)
		if (self.power > 0) then
			index = index - 1 end
		
		if (index < -1) then
			return nil end
		
		if (index == -1) then
			return QDMF_Rune[self.power - 1] end
		
		if (index < self._length) then
			return self.runes[index] end
		
		return nil
	end

	--[[
	# check whether a certain rune is part of the spell ]]--
	function phrase.hasRuneIndex(self, idx)
		if (self == nil or idx == nil) then return nil end
		-- a power rune?
		if idx < QDMF_Rune.ways then
			if self.power == idx + 1 then
				return true
			else
				return false
			end
		end
		for k,v in pairs(self.runes) do
			if v.index == idx then return true end
		end
		return false
	end

	--[[
	# check if the phrase is a valid spell and if: update name and alignment ]]--
	function phrase.isValid(self)
		if (self._tested) then return self._valid end
		
		self.string = ""
		self.cost = 0
		if (self.power) > 0 then
			self.string = QDMF_Rune[self.power - 1]
		end

		for k,v in pairs(self.runes) do
			if (self.string ~="") then
				self.string = self.string .. " "
			end
			self.string = self.string .. v.name
			self.cost = self.cost + v.cost
		end
		
		local spell = QDMF_Spell[self.string]
		self._tested = true
		if spell == nil then
			self.name   = ""
			self.align  = ""
			self._valid = false
			return false
		end
		self.name   = spell.name
		self.aligh  = spell.align
		self._valid = true
		return true
	end

	--[[
	# convert the phrase to a full string with power rune ]]--
	function phrase.toString(self)
		local str = "", rune
		if self.power > 0 and self.power <= QDMF_Rune.ways then
			rune = QDMF_Rune[self.power - 1]
			if rune ~=nil then
				str = QDMF_Rune[self.power - 1].name .. " "
			end
		end
		return str .. self.string
	end
	
	function phrase.getNextTier(self)
		if (self.power == 0) then return 0 	end
		if (self._length > 5) then return -1 end
		return (self._length % 3) + 1
	end
	
	phrase:clear()
	if runestring == nil then
		return phrase end
	for i in string.gmatch(runestring, "%S+") do
		phrase:add(QDMF_Rune[i])
	end
	return phrase
end

function QDMF_Rune:new(name,tier,way,cost,effect)
	local rune = {}
	rune.name  = name
	rune.tier  = tier
	rune.way   = way
	rune.index = tier * QDMF_Rune.ways + way
	rune.cost  = cost

	-- make accessible by name and position, in lua, this
	-- is automatically a ref to the object, not a copy 
	QDMF_Rune[name] = rune
	QDMF_Rune[rune.index] = rune
	return rune
end

--[[
# Definition of a spell.
# minimum : the least possible power-rune to cast this spell
# ]]--
function QDMF_Spell:new(name, align, runestring, minimum)
	local spell = QDMF_PhraseCreate(runestring)
	spell.name  = name
	spell.align = align
	QDMF_Spell[name] = spell
	QDMF_Spell[spell.string] = spell
	return spell
end


QDMF_Rune:new("Sha",   0,0,3, "Neophyte")
QDMF_Rune:new("Lo",    0,1,3, "Novice")
QDMF_Rune:new("Beth",  0,2,3, "Apprentice")
QDMF_Rune:new("Um",    0,3,3, "Journeyman")
QDMF_Rune:new("On",    0,4,3, "Craftsman")
QDMF_Rune:new("Ee",    0,5,3, "Artisan")
QDMF_Rune:new("Pal",   0,6,3, "Adept")
QDMF_Rune:new("Mon",   0,7,3, "Expert")
QDMF_Rune:new("Arch",  0,8,3, "Master")

QDMF_Rune:new("Fri",   1,0,3, "Cold")
QDMF_Rune:new("Ya",    1,1,3, "Earth")
QDMF_Rune:new("Vi",    1,2,3, "Water")
QDMF_Rune:new("Oh",    1,3,3, "Air")
QDMF_Rune:new("Ful",   1,4,3, "Fire")
QDMF_Rune:new("Eck",   1,5,3, "Energy")
QDMF_Rune:new("Man",   1,6,3, "Mind")
QDMF_Rune:new("Des",   1,7,3, "Void")
QDMF_Rune:new("Zo",    1,8,3, "Negation")

QDMF_Rune:new("Sta",   2,0,3, "Unity")
QDMF_Rune:new("Ven",   2,1,3, "Time")
QDMF_Rune:new("Des",   2,2,3, "Life")
QDMF_Rune:new("Kath",  2,3,3, "Expand")
QDMF_Rune:new("Ir",    2,4,3, "Flight")
QDMF_Rune:new("Het",   2,5,3, "Twist")
QDMF_Rune:new("Bro",   2,6,3, "Peace")
QDMF_Rune:new("Trans", 2,7,3, "Transit")
QDMF_Rune:new("Gor",   2,8,3, "War")

QDMF_Rune:new("Lyr",   3,0,3, "Artistry")
QDMF_Rune:new("Ku",    3,1,3, "Strenght")
QDMF_Rune:new("Ros",   3,2,3, "Dexterity")
QDMF_Rune:new("Dain",  3,3,3, "Magic")
QDMF_Rune:new("Neta",  3,4,3, "Belief")
QDMF_Rune:new("Ing",   3,5,3, "Insight")
QDMF_Rune:new("Ra",    3,6,3, "Order")
QDMF_Rune:new("Sar",   3,7,3, "Chaos")
QDMF_Rune:new("Woo",   3,8,3, "Balace")

QDMF_Spell:new("Fireball", "Mage", "Ful Ir", 0)

file_runes = "mod_assets/textures/runes.dds"
file_runes_h = "mod_assets/textures/runes_h.dds"
file_runes_s = "mod_assets/textures/runes_s.dds"


function renderRunePanel(self,context)
	runePanel:render(context)
end

function runePanelCreate(runestring)
	local panel = {}
	panel.phrase = QDMF_PhraseCreate(runestring)
	panel.runesize = 30
	panel.phrase_h = 20
	panel.X = 0
	panel.Y = 0
	panel.clickcycle = false
	panel.click  = -1
	panel.select = -1
	panel.hover  = -1
	panel.tier = panel.phrase:getNextTier()
	panel.base = panel.tier * QDMF_Rune.ways
	
	function panel.checkHover(self,context)
		for r = 0, self.phrase:getLength() do
			if (context.button("", self.X + r*self.phrase_h, self.Y, self.phrase_h, self.phrase_h) ~= nil) then
				self.hover = r
				return
			end
		end
		local yy = self.Y + self.phrase_h
		for r = 0, 8 do
			y = math.floor(r / 3)
			x = self.X + (r - (y * 3)) * self.runesize
			y = yy + y * self.runesize
			if (context.button("", x, y, self.runesize, self.runesize) ~= nil) then
				self.hover = r + 50 + self.base
				return
			end
		end
		self.hover = -1
	end

	function panel.drawPhrase(self, context, x, y)
		local len = self.phrase:getLength()
		
		for r = 0, len do
			rune = self.phrase:getRuneAt(r)
			if (rune ~= nil) then
				if (self.click == r or self.select == r) then
					file = file_runes_s
				elseif (self.hover == r) then
					file = file_runes_h
				else
					file = file_runes
				end
				context.drawImage2(file, x + r * self.phrase_h, y, rune.way * 100 + 10,
					rune.tier * 100 + 10, 80, 80, self.phrase_h, self.phrase_h)
			end
		end
	end

	function panel.drawTiles(self, context, x, y)
		if (self.tier == -1) then
			return
		end
		local hover = self.hover - 50
		local click = self.click - 50
		local rx = 0
		local ry = 0
		local rr
		for r = 0, 8 do
			ry = math.floor(r / 3)
			rx = x + (r - (ry * 3)) * self.runesize
			ry = y + ry * self.runesize
			rr = r + self.base

			if (hover == rr) then
				if (click == rr) then
					file = file_runes_s
				else
					file = file_runes_h
				end
			elseif (self.phrase:hasRuneIndex(rr)) then
				file = file_runes_s
			else
				file = file_runes
			end
			context.drawImage2(file, rx, ry, r * 100, self.tier * 100, 100, 100, self.runesize, self.runesize)
		end
	end

	function panel.render(self, context)
		self:checkHover(context)
		
		if (context.mouseDown(0)) then
			-- mousebutton pressed, save click position if valid
			if (not self.clickcycle and self.hover > -1) then
				self.clickcycle = true
				self.click = self.hover
			end
		else
			-- mousebutton released, act if position is still the same
			if (self.clickcycle) then
				self.clickcycle = false
				if (self.hover == self.click) then
					if (self.click > -1 and self.click < 50) then
						-- click into spell-phrase
						self.select = self.click
						self.tier = self.phrase:getTierAt(self.click)
						self.base = self.tier * QDMF_Rune.ways
						
					elseif (self.click > 49 and self.click < 100) then
						-- click into tile-zone
						if (self.select > -1 and self.select < 50) then
							self.phrase:add(self.click - 50, self.select)
						else
							self.phrase:add(self.click - 50)
						end
						self.tier = self.phrase:getNextTier()
						self.base = self.tier * QDMF_Rune.ways
						self.select = -1
					end
				end
				self.click = -1
			end
		end
		self:drawPhrase(context, self.X, self.Y)
		self:drawTiles(context, self.X, self.Y + self.phrase_h)
	end

	return panel
end


runePanel = runePanelCreate()
Last edited by .rhavin on Mon Oct 16, 2017 5:15 pm, edited 1 time in total.
User avatar
Isaac
Posts: 3185
Joined: Fri Mar 02, 2012 10:02 pm

Re: Spell – rune order

Post by Isaac »

The script itself looks great. I pasted it *as copied*, and connected a floor_trigger to it; when triggered, nothing happened. The same (nothing) happened when I called it direct from the console. I double checked my copy from your post, and called showPanel() both ways. No menu :?

So my first question is (the obvious one)... Does it also not work for anyone else but me?
Then... Is there any chance it was miss-copied from the editor when posted?
Also what screen resolution do you use? Is there anything else needed besides just calling showPanel(), and having the three textures @ "mod_assets/textures/" ?

Can you post a screenshot of it?

**As a side note to regulars here: I would have sworn that DDS textures had to be referenced in scripts as TGA files, but the runes show (via GameMode), and are included in the exported dat. When did they change (patch) that historical quirk?

EDIT: This is definitely something on my end that is not working.

Now that I've seen it in action, it reminds me a little bit of Arx Fatalis, at least in principle (not with mouse gestures). It's very impressive; doubly so, being a first use of Lua. (So what do you normally use? :D )
User avatar
Zo Kath Ra
Posts: 937
Joined: Sat Apr 21, 2012 9:57 am
Location: Germany

Re: Spell – rune order

Post by Zo Kath Ra »

Another side note:
Clicking "Select all" above the code sample should select the entire code, but it takes me to the beginning of the thread instead.
Tested on Firefox and Chromium.
Post Reply