Looking for spells, movement ideas, general modding help.

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!
Post Reply
Corpsey
Posts: 5
Joined: Sun Sep 08, 2024 2:08 am

Looking for spells, movement ideas, general modding help.

Post by Corpsey »

I actually got some of the logic stuff working finally for traps to the point that I'm comfortable with it, but now I'm into spells and movement stuff and hitting roadblocks... I'm a bit surprised these two things are so complicated to mod around because it's like 60% of the game.


Ok the movement one would take some mad wizardry but I could see a script that:

On key press or item (screen following object) press (would be kinda cool to have a button to press for it, or ctrl + space)
If NOT moving (PartyComponent:isMoving(false))
Check if the forward 9 squares have a collider (not sure how to GameObject:getPosition() with good data..)
If they don't, move up to 9 squares but stopped by anything that has a collider (Maybe this could just be a short lived spell that gets shot extremely fast? if it hits anything your position is the nearest tile to impact/dead of the projectile)
Script is adaptable from 9 so you can make it jump less squares so it can be tailored to strength.


For spells I'm at a loss, I tried following: https://www.grimrock.net/forum/viewtopic.php?t=9416
I have the spell defined, I have the fireball redefined, but I have no idea where or how to add the onCastSpell. I even think I found the 'data component' that was mentionned but no idea what to do with it. I didn't actually implement a lot because the fail cases I simply abandoned, but there was point I was getting really deep into reading someone's spell mod from nexus and I'm hours into this with nothing to show. I'm a bit surprised there isn't just a "this is how to make a spell mod" video. I started getting into all sorts of not being able to define an object errors and got really confused. It's stripped down to bare essentials again, but I honestly have no idea where to put that onCastSpell, and calling the data bit in the init didn't seem to help or do much of anything. My last attempt was a party.lua which gives me "attempt to index a function value".


Then lastly I was poking around model files and wondering how to get normal maps to map properly in blender. I know they don't have to, it's just crazy how many steps were taken to seemingly make modding an absolute nightmare... I think the closest I got was:
Green + Blue Value Mix 100%, then that GB has the alpha added as Red (Although mixing it as value is also a possibility, both seem to have weird lighting) Then that output stuck in a normal map and applied.
Anyone have the correct approach?


What else... I got a lot of my last posts stuff working which is awesome. I made my own particle fire light, made one that flickers (which I'll probably scrap but it was a good exercise), and managed to make some great logic stuff work with switches (to solo out components and then reset it all) that makes me excited to make more.

Image
Corpsey
Posts: 5
Joined: Sun Sep 08, 2024 2:08 am

Re: Looking for spells, movement ideas, general modding help.

Post by Corpsey »

After messing around a bunch, I think the movement would make the most sense if a monster with no model gets spawned in the most possible "flee from party" brain that it can muster, with animation speed high and cooldown speed low:

function spawnUnturningFleeingMonster()
spawn("air_elemental", party.level, party.x, party.y, party.facing, party.elevation, "fleeingAirElemental")
fleeingAirElemental:getComponent("turn"):disable()
fleeingAirElemental:getComponent("basicAttack"):disable()
fleeingAirElemental:getComponent("sound"):disable()
fleeingAirElemental:getComponent("move"):setSound("nulltiny")
fleeingAirElemental:getComponent("move"):setCooldown(0.001)
fleeingAirElemental:getComponent("move"):setAnimationSpeed(400)
fleeingAirElemental:getComponent("brain"):setSight(0)
fleeingAirElemental:getComponent("brain"):startFleeing(600)
fleeingAirElemental:getComponent("brain"):setMorale(0)
-- WIP delayedCall(fleeingAirElemental, 10, destroy())
end

nulltiny is a custom definition I made to inject a null sound, you can remove this line if you test it.

something like this, then from there it can get the world position before being destroyed, subtract that world position from the party's world position and move to the new position over a series of setWorldPosition pings. This ensures that the movement is possible because the monster collides (even at this insane speed). Then, all we need to do is find which way the player is facing and apply the setWorldPosition in that direction, either adding x OR adding y OR subtracting x OR subtracting y which is really simple.

My current problem(s):
1) I don't know how to just make a spell or device or screen button call the function. Right now I have it hooked up to a lever because I wanted to test if it was possible to make a monster flee in a straight path like this at all.
2) Speaking of which, this air elemental brain does funny things sometimes. I noticed if I spawned an x+1 with slower animspeed and cooldown it would sometimes hitch, likely from it's brain attempting turns or some other actions even though I'm trying to get it to flee as much as possible. I'm not sure what the actual best approach here is, but giving it these high speeds and spawning it at my location without the ability to turn DOES appear to make it beeline away from me pretty quick which is nice - like before it can even think it's already further than I would want the "jump" to be.

I also got a setWorldPosition movement script working already, it basically defines a 'frames' variable, divides the distance of your tiles jump by this frames amount and then moves that amount. setPosition wont work for a smooth animation like this because we can't feed it decimal values, so the actual jump animation if it's applied to the party must be done via setWorldPosition, which doesn't break anything if you never go out of bounds which shouldn't happen using a fleeing monster to get a final position thanks to monsters colliding with the bounds anyways (though it is possible to break if we just move based on facing, which also has problems with phasing through walls and stuff - not that this method doesn't have that problem like for example if a monster moves in your path AFTER the check if things are there is done, but the idea is that it should be fast firing anyways so this is whatever).

The actual getPosition grid is like:
0,0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 31,0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
0,31 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 31,31

where the world position grid is:

1.5,(z),94.5 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 94.5,(z),94.5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1.5,(z),1.5 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 94.5,(z),1.5

I've basically abandoned the setPosition command for moving things though, because as I said we can't input decimals so if you want the party to appear moving in a smooth way using worldPosition is honestly best.
Something to note here is that there's a 1.5 offset, and a 3 unit multiple difference if you were to ever convert worldPosition to position or vice versa.
So for example for my test on an object I can define 2 squares of movement (runAmount), and then the actual runDist is runAmount*3 (basically always, we know it's 3 so is there a point to define a new grid conversion variable?). Then the runLength (amount of distance to move each time the function is pinged) is your runDist/frames (in my case 6/60) and then I have a timer activate and ping the function until it hits 60 where it resets 1) a counter made just to let it know it's active so it can't be retriggered, 2) the counter that is counting to 60, 3) the timer gets stopped and deactivated.

Using that I was able to move a teleporter a distance of 2 in-game units using worldPosition !!! :D which was pretty exciting not gonna lie. Though, I'm not a great programmer so maybe someone else can do some more epic stuff. At the very least it gives me a way to make a linear animation for walls without adding an animation, though an animation is likely a better solution and I was mostly making this to figure out the jump stuff. Not sure what I'm expecting here I just thought I'd update where I'm at with this and maybe see if some script wizard can run with it a bit more.

So the code for the moving teleporter is
Setup:
runTeleporter - teleporter placed bottom right of the map, facing doesn't matter really. You could technically make any object I guess
theRunCounter - a counter that just makes sure the script cant be reinitiated while running. 0.
counterForSpacing - this ticks up to the 'frames' value by incrementing each time the script is called. 0.
timerPulses - a timer that is set to 0.016667. This should really be set to 1/frames in the script but I had set it to a specific value just in case, because as I said I'm bad at programming so I was removing as much complication as possible. This timer starts with the timer disabled, everything unchecked, connected (onActivate) to the script to call the main function (teleporterRun()).

Then a lever that activates incrementToBreak() and startTheTimer(). Increment to break simply makes it so if you're flipping the lever after nothing can happen until it resets, and start the timer activates the timer if that counter is 1 (as in, if its infinity-0 or 2-inifinity it does nothing, so doing anything until the reset doesn't retrigger the timer under any circumstances).

function incrementToBreak()
local counter3ID = findEntity("theRunCounter")
counter3ID:getComponent("counter"):increment()
end

function startTheTimer()
local counter3ID = findEntity("theRunCounter")
local timer5ID = findEntity("timerPulses")
if counter3ID:getComponent("counter"):getValue() == 1 then
timer5ID:getComponent("timer"):enable()
timer5ID:getComponent("timer"):start()
end
end

Code: Select all

function teleporterRun()

	local counter3ID = findEntity("theRunCounter")
	local counter4ID = findEntity("counterForSpacing")
	local timer5ID = findEntity("timerPulses")

	local runCeiling = 94.5
	local runOffset = 1.5
	-- this variable could be used if ever using setPosition(), however the idea is to not use setPos due to non decimal.
	local addToNonworldPos = 1
	-- the tile length you want to move in a 'non world amount' sense
	local runAmount = 2
	-- 3 is the distance of a unit on the grid, use runAmount to change movement distance
	local runDist = runAmount*3
	local frames = 60
	local runLength = runDist/frames
	local floorHeight1 = 94.5
	local levelNumberFindWorld = 1

		if counter3ID:getComponent("counter"):getValue() >= 1 then
		
			if counter4ID:getComponent("counter"):getValue() <= 60 then

			counter4ID:getComponent("counter"):increment()
			counter3ID:getComponent("counter"):setValue(1)
			print("Started. CounterForSpacing=",counter4ID:getComponent("counter"):getValue(),"/60. TheRunCounter=1")

			

				if counter4ID:getComponent("counter"):getValue() == 60 then
					print ("Frame 60 hit")
					local counter3IDinLoop = findEntity("theRunCounter")
					local counter4IDinLoop = findEntity("counterForSpacing")
					counter3IDinLoop:getComponent("counter"):setValue(0)
					timer5ID:getComponent("timer"):stop()
					timer5ID:getComponent("timer"):disable()
					counter4IDinLoop:getComponent("counter"):reset()
					print ("Frame 60 hit. theRunCounter = 0, timer stopped. counterForSpacing reset.")
				end

			print ("In the main push")
			local objTele = findEntity("runTeleporter")
				
			local originalworldataX, originalworldataZ, originalworldataY, originalworldataElev, originalworldataLevel = unpack(objTele:getWorldPosition())
--			ORGANIZED FOR setPosition INPUT, XZY(0)(1)
			local outdataX = 0+originalworldataX
			local outdataZ = 0+originalworldataZ
			local outdataY = 0+originalworldataY+runLength
			local outdataElev = 0+originalworldataElev

			local posChangeRapid = vec(outdataX, outdataZ, outdataY, outdataElev, levelNumberFindWorld)

			objTele:setWorldPosition(posChangeRapid)
		end
	end
end

and then yeah if you wanna mess with the monster one:

Code: Select all

function spawnUnturningFleeingMonster()
	spawn("air_elemental", party.level, party.x, party.y, party.facing, party.elevation, "fleeingAirElemental")
	fleeingAirElemental:getComponent("turn"):disable()
	fleeingAirElemental:getComponent("basicAttack"):disable()
	fleeingAirElemental:getComponent("sound"):disable()
--	fleeingAirElemental:getComponent("move"):setSound("nulltiny")
	fleeingAirElemental:getComponent("move"):setCooldown(0.001)
	fleeingAirElemental:getComponent("move"):setAnimationSpeed(400)
	fleeingAirElemental:getComponent("brain"):setSight(0)
	fleeingAirElemental:getComponent("brain"):startFleeing(600)
	fleeingAirElemental:getComponent("brain"):setMorale(0)
end
It can be hooked to any button really, I just used a lever because my dumb self is trigger happy so this prevents me from double firing which crashes it because I don't know how to work with IDs properly yet.
Corpsey
Posts: 5
Joined: Sun Sep 08, 2024 2:08 am

Re: Looking for spells, movement ideas, general modding help.

Post by Corpsey »

I started realizing teleporters and pressure plates could be a problem, and other ways to manage the movement.

I found some tidbits that could help like:

Code: Select all

for i in allEntities(level) do
	if i.getTeleportTarget ~= nil then
		if type(i.getTeleportTarget) == "function" then
			return true
		end
	end
	return false
end
This should get all items that have the function getTeleportTarget (of which, only teleports and stairs do)

This could potentially help me write a script that simply finds all the teleporter and stairs and temporarily adds an invisible wall object to all of them, but that sounds like it could go really bad, idk maybe its just me but if theres many teleporters in one level it could be either laggy or straight up crashy.

The pressure plates if I use a monster actually arent a problem (tested) because the monster I used was flying I think, which makes sense I guess? The monster simply removes itself from being a possible triggering mechanic, so that's fine, but teleporters are crazy. I could maybe have it save data of the level of the monster, and if the level changes EVER, then it could - instead of starting a worldpos movement (basically 'bypass all normal function') - summon a teleporter on the player; the output destination being the destination as normal. It would also have to fetch the monster facing to apply to party as well because teleporters have specific facing conditions*. Then the teleporter deletes itself. This fulfils the logic but in rare cases could potentially skip some sort of door timing - but maybe that would be the point of the ability anyways, I guess? Or a second monster could be spawned as a double check for new collisions before the teleporter is created.

The scope of this got a little too big mostly because of the teleporters. The idea I keep going for is "force jump" or something along those lines.

*Not only that but the destination could have had some sort of logic that was just triggered by the monster that shouldn't have been able to get to the area, where our teleport in presence also applies another instance of whatever if this logic exists, which could cause problems that I can't comprehend right this second >.>
Corpsey
Posts: 5
Joined: Sun Sep 08, 2024 2:08 am

Re: Looking for spells, movement ideas, general modding help.

Post by Corpsey »

Seriously what am I doing wrong for spells though....

init.lua

Code: Select all

-- This file has been generated by Dungeon Editor 2.2.4

-- new data component for hooks (????????????????????????????????????????????????????????????)
userData = {
	class = "Script",
	name = "data",
	source = [[
data = {}
function get(self,name)
	return self.data[name]
end
function set(self,name,value)
	self.data[name] = value
end
]],		
}

-- import standard assets
import "assets/scripts/standard_assets.lua"

-- import custom assets
import "mod_assets/scripts/coobjects.lua"
import "mod_assets/scripts/cosounds.lua"
import "mod_assets/scripts/coparticles.lua"
import "mod_assets/scripts/cospells.lua"
import "mod_assets/scripts/coparty.lua"
import "mod_assets/scripts/items.lua"
import "mod_assets/scripts/monsters.lua"
import "mod_assets/scripts/objects.lua"
import "mod_assets/scripts/tiles.lua"
import "mod_assets/scripts/recipes.lua"
import "mod_assets/scripts/spells.lua"
import "mod_assets/scripts/materials.lua"
import "mod_assets/scripts/sounds.lua"
cospells.lua

Code: Select all

defineSpell{
	name = "fingeroffensive",
	uiName = "Spirit Shot",
	gesture = 7852,
	manaCost = 20,
	onCast = "fireball",
	skill = "concentration",
	requirements = { "concentration", 1},
	icon = 61,
	spellIcon = 7,
	description = "A fallen spirit aids you in battle.",
}
coparticles.lua

Code: Select all

defineParticleSystem{
	name = "castle_magic_light",
	emitters = {
		-- glow
		{
			spawnBurst = true,
			emissionRate = 1,
			emissionTime = 0,
			maxParticles = 1,
			boxMin = {0, 0,-0.1},
			boxMax = {0, 0,-0.1},
			sprayAngle = {0,30},
			velocity = {0,0},
			texture = "assets/textures/particles/glow.tga",
			lifetime = {1000000, 1000000},
			color0 = {0, 0.11, 0.3},
			opacity = 0.6,
			fadeIn = 0.1,
			fadeOut = 0.1,
			size = {3, 3},
			gravity = {0,0,0},
			airResistance = 1,
			rotationSpeed = 0,
			depthBias = 0.5,
			blendMode = "Additive",
		},

		-- flames
		{
			emissionRate = 30,
			emissionTime = 0,
			maxParticles = 100,
			boxMin = {-0.03, -0.07, 0.03},
			boxMax = { 0.03, -0.07,  -0.03},
			sprayAngle = {-4,10},
			velocity = {0.1, 0.4},
			texture = "assets/textures/particles/goromorg_lantern.tga",
			frameRate = 45,
			frameSize = 64,
			frameCount = 16,
			lifetime = {0.25, 0.85},
			colorAnimation = false,
			color0 = {1.5, 1.5, 1.5},
			opacity = 1,
			fadeIn = 0.15,
			fadeOut = 0.3,
			size = {0.2, 0.1},
			gravity = {0,1.8,0},
			airResistance = 1.0,
			rotationSpeed = 1,
			blendMode = "Additive",
			depthBias = 0,
			objectSpace = true,
		},

		-- outer glow
		{
			spawnBurst = true,
			emissionRate = 1,
			emissionTime = 0,
			maxParticles = 1,
			boxMin = {0,0,0},
			boxMax = {0,0,0},
			sprayAngle = {0,30},
			velocity = {0,0},
			texture = "assets/textures/particles/glow.tga",
			lifetime = {1000000, 1000000},
			colorAnimation = false,
			color0 = {0.9, 0.9, 1.2},
			opacity = 0.6,
			fadeIn = 0.1,
			fadeOut = 0.8,
			size = {1, 1},
			gravity = {0,0,0},
			airResistance = 1,
			rotationSpeed = 0,
			blendMode = "Additive",
			depthBias = 0.1,
			objectSpace = true,
		}
	}
}
coobjects.lua

Code: Select all

-- #################################### SPELL CONTAINERS ##########################################
defineObject{
    name = "fireball_large",
    baseObject = "base_spell",
    components = {        
        {
            class = "Particle",
            particleSystem = "fireball_large",
            onInit =
                function(self)
                    --print("in fireball override particle init")
                    local spell = party.data:get("castSpell")
                    if spell == "fingeroffensive" then
                        self:setParticleSystem("castle_magic_light")
                    end
                end
        },        
        {
            class = "Light",
            color = vec(1, 0.5, 0.25),
            brightness = 15,
            range = 7,
            castShadow = true,
             onInit =
                function(self)
                    --print("in fireball override light init")
                    local spell = party.data:get("castSpell")
                    if spell == "fingeroffensive" then
                        self:setColor(vec(0.9, 0.9, 1.2))
                    end
                end
        },
        {
            class = "Projectile",
            spawnOffsetY = 1.35,
            velocity = 10,
            radius = 0.1,
            hitEffect = "fireball_blast_large",
            onInit =
                function(self)
                    --print("in fireball override projectile init")
                    local spell = party.data:get("castSpell")
                    if spell == "fingeroffensive" then
                        self:setHitEffect("dispel_blast")
                    end          
                end,
            onProjectileHit = 
                function(self, what, entity)                
                    local spell = party.data:get("castSpell")
                    if what == "entity" then
                        print("fireball_override onProjectileHit", spell, what, entity.name, self:getHitEffect(), self:getCastByChampion())
                    else
                        print("fireball_override onProjectileHit", spell, what, entity)
                    end
                end 
        },
        {
            class = "Sound",
            sound = "fireball",
        },
        {
            class = "Sound",
            name = "launchSound",
            sound = "fireball_launch",
        },
    },
}
coparty.lua

Code: Select all

defineObject{
	name = "party",
	components = {
		{
			class = "Party",
			userData
			onCastSpell =
                function(self, champion, spell)
                    print("in party onCastSpell", champion:getOrdinal(), spell)
                    self.go.data:set("castSpell", spell)
                end
		},
		{
			class = "Light",
			name = "torch",
			range = 12,
		},
	},
	editorIcon = 32,
	placement = "floor",
}
These are all the relevant blocks, in the init I've been using co to show myself what has been modded instead of using the base naming convention on purpose, in case I need to rip pieces back out. Anyways so in the init I added the userData define from the thread below, or at least I think I added it....

I was guessing userData needed to be it's own component but the example given doesn't have anything like that so I have no idea what this means
https://www.grimrock.net/forum/viewtopi ... =22&t=8147
The other script on there looks useful too to just make a script in-game for pressure plates.
Post Reply