[Solved] Casting custom spells like built in spells

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!
MrChoke
Posts: 324
Joined: Sat Oct 25, 2014 7:20 pm

[Solved] Casting custom spells like built in spells

Post by MrChoke »

So I am creating some new spells. I have a new fireball for example. It is not a built-in spell so I define its onCast() as a function having parms (champion, x, y, dir, elev, skillLevel)
Since we do not have a working shootProjectile() function to call, I spawn the new fireball object using the x and y specified.

Realization number 1: You cannot use the same X and Y specified or the fireball explodes in your tile!

That is a bit annoying but I can use getForward() with the direction given and have it spawn in the tile in front of the party.

Realization 2: Now the fireball casts and looks good, except it clearly has spawned in the next tile and does not look at all like it was cast by a champion. You look at the built-in spells and they spawn directly in front of the champion who cast it. This takes into account what position in the party he is in as well.

Now I am getting annoyed. I can do setWorldPosition() calls on the newly spawned fireball to make it appear closer to the party but how do I put it in front of the champion who cast it?

Realization 3: The Champion component does not return the position he is in in the party!?!?!? I can get the ordinal but that is different if the champions moved around in the party. The object given to the hook is the champion and we cannot get his position to use an offset to place the newly cast spell in front of him. OK, do I need to reference the party now and match the name of the champion to get his position???

Am I missing the boat on how to cast a custom spell or is it very difficult? I am going to have to write many lines of custom LUA code to do something that to me should be a single command.
Thoughts? Comments?
Last edited by MrChoke on Wed Feb 04, 2015 1:31 am, edited 1 time in total.
User avatar
AndakRainor
Posts: 674
Joined: Thu Nov 20, 2014 5:18 pm

Re: How do I cast a custom spell like built in spells are ca

Post by AndakRainor »

In the scripting reference http://www.grimrock.net/modding/scripti ... eComponent you should use the function ProjectileComponent:setIgnoreEntity(ent)

You spawn your entity, for example a fire ball (any size) then use a syntax similar to myFireballEntity.projectile:setIgnoreEntity(party)
I created spells with projectiles you can see the code in this thread : viewtopic.php?f=22&t=8265

In this old version of my code I made a mistake when I thought those projectiles should not ignore the party as in game it is possible to hit the party with its own spells, but in reality all those built-in spells do in fact ignore the party. It's only the damageTile components they create when they hit another obstacle, like a wall or monster that can damage the party.

So this way you can just launch your projectile spells from the exact party's position and orientation and obtain the same visual as the original spells.

Also those spells do not use the caster's position in the party. If you really want to do it I think you will have to compare the results of the function that gets the champion by its position to the local caster champion parameter.

And yes, the current asset pack doesn't include enough information on various subjects such as spells definition so for now we have to guess a lot !!! For example, be very careful with customs spells not granting XP and always check the castByChampion variable in your damageTile components !
MrChoke
Posts: 324
Joined: Sat Oct 25, 2014 7:20 pm

Re: How do I cast a custom spell like built in spells are ca

Post by MrChoke »

AndakRainor wrote:In the scripting reference http://www.grimrock.net/modding/scripti ... eComponent you should use the function ProjectileComponent:setIgnoreEntity(ent)

You spawn your entity, for example a fire ball (any size) then use a syntax similar to myFireballEntity.projectile:setIgnoreEntity(party)
I created spells with projectiles you can see the code in this thread : viewtopic.php?f=22&t=8265

In this old version of my code I made a mistake when I thought those projectiles should not ignore the party as in game it is possible to hit the party with its own spells, but in reality all those built-in spells do in fact ignore the party. It's only the damageTile components they create when they hit another obstacle, like a wall or monster that can damage the party.

So this way you can just launch your projectile spells from the exact party's position and orientation and obtain the same visual as the original spells.

Also those spells do not use the caster's position in the party. If you really want to do it I think you will have to compare the results of the function that gets the champion by its position to the local caster champion parameter.

And yes, the current asset pack doesn't include enough information on various subjects such as spells definition so for now we have to guess a lot !!! For example, be very careful with customs spells not granting XP and always check the castByChampion variable in your damageTile components !
Yup, setIgnoreEntity(party) allows me to fire the spell from the same tile. It looks much better. But it launches center to the party, not cast from a champion. Your suggestion on how to address this is what I was thinking as well. It is a pain to have to do all that work though. Also, I think it is a miss in my opinion that the Champion component has no way to return what position in the party he is in. Oh well. Thanks.
MrChoke
Posts: 324
Joined: Sat Oct 25, 2014 7:20 pm

Re: How do I cast a custom spell like built in spells are ca

Post by MrChoke »

A few more issues with dealing with casting custom spells. The Projectile component has setCastByChampion() but its a boolean, not a number for his ordinal. And then look at TileDamanger. Its got getCastByChampion() and setCastByChampion() and they use number for the ordinal. Its not consistent. And what's worse yet, is when you spawn a spell object, you get the projectile component and can set true for setCastByChampion(), but you don't get the TileDamager. That doesn't even exist until the spell hits something. And NO, setting true on setCastByChampion() on the Projectile does nothing to what you can get out of the TileDamager. I can add hooks for onHitObstacle() or onHitMonster() and there is no way to tell who/what created the spell. There seems to be some serious holes in this. Yet the built-in spells work great.
User avatar
AndakRainor
Posts: 674
Joined: Thu Nov 20, 2014 5:18 pm

Re: How do I cast a custom spell like built in spells are ca

Post by AndakRainor »

Exact, it's really not consistent !

Here are the methods I used to deal with this problem;

- The lazy one : you add "castByChampion = 1," in all the TileDamager components you use with your custom spells. To do it you will have to define all your custom projectiles and linked tile damagers and never use the original game objects. You can just copy/paste those original definitions, change their names (I added "_custom" to theirs names) and add the line for the caster's ordinal. This is an ugly solution, the worst part being the fact that the champion's ordinal will be incorrect most of the time. BUT in the native grimrock 2 XP system, the XP gains are equally shared among champions (before xp modifiers similar to the necklace bonus) so if you don't want to change this system, the solution will work with any valid ordinal number (1 should be always valid).

- The less ugly one : when you spawn a magic projectile, store it's caster ordinal with a syntax similar to "myFireball.projectile.champion = X" (X being the ordinal). Define all your custom projectiles and never use the original game objects. With this method you won't have to define custom tileDamagers. However, you will need to redirect all your custom projectiles onProjectileHit function to a generic function that spawns its hitEffect (this contains the name of the tileDamager to create). From this point in the code you will be able to fully customize your tileDamager, setting its caster's ordinal, and why not change it's attackPower. Unless I missed something important, you can add dynamically any new variable to a component, you just then need the code to use it.
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: How do I cast a custom spell like built in spells are ca

Post by minmay »

AndakRainor wrote:Unless I missed something important, you can add dynamically any new variable to a component, you just then need the code to use it.
I'm pretty sure this doesn't serialize properly. I strongly recommend not doing it. What's wrong with using ProjectileComponent:setCastByChampion() and checking ProjectileComponent:getCastByChampion() when it's time to create the TileDamager?
Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
User avatar
AndakRainor
Posts: 674
Joined: Thu Nov 20, 2014 5:18 pm

Re: How do I cast a custom spell like built in spells are ca

Post by AndakRainor »

minmay wrote:
AndakRainor wrote:Unless I missed something important, you can add dynamically any new variable to a component, you just then need the code to use it.
I'm pretty sure this doesn't serialize properly. I strongly recommend not doing it. What's wrong with using ProjectileComponent:setCastByChampion() and checking ProjectileComponent:getCastByChampion() when it's time to create the TileDamager?
Well in this case, these functions store and return a boolean, when we need an ordinal number.
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: How do I cast a custom spell like built in spells are ca

Post by minmay »

Why would you care about the specific champion ordinal used for the TileDamager? Surely any champion-dependent changes you want to make should apply at the time the projectile is created, not when it hits. Unless you want to remove equal experience gain for some completely insane reason, the ordinal doesn't matter once the projectile is created.
Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
MrChoke
Posts: 324
Joined: Sat Oct 25, 2014 7:20 pm

Re: How do I cast a custom spell like built in spells are ca

Post by MrChoke »

@Andakrainor, I agree with minmay with setting custom variables to game objects. You are asking for big trouble when you save, in the form of game crashes or warnings. I like your solutions but they still do not address that the spell looks like its cast from the tile and not the champion who cast it. Below I will detail a solution I figured out that does that and is enough for me to call this solved.

But first, @minmay, regarding why worry about the champion who cast the spell in TIleDamager. The game does for "poison_bolt". See this code from poison_bolt.lua:

Code: Select all

onHitMonster = function(self, monster)
				monster:setCondition("poisoned", 25)

				-- mark condition so that exp is awarded if monster is killed by the condition
				local poisonedCondition = monster.go.poisoned
				local ord = self:getCastByChampion()
				if poisonedCondition and ord then
					poisonedCondition:setCausedByChampion(ord)
				end
			end,
The above code will not work like that unless it is cast as a built-in spell. I experimented quite a bit on it.

So, here is my solution to this. In order to take advantage of proper setting of the Champion who cast the spell, both on the Projectile and on the TileDamager, and also to see proper casting from that Champion, I had to re-use a built-in spell. But I re-defined it. I created a "Green Fireball". It is the same as the default fireball but I changed all of the color vectors to a shade of green. NOTE, that the built-in "fireball" spell creates game object, "fireball_large".

First my spell (ignore all that test_skill stuff, that was another test)

Code: Select all

defineSpell{
    name = "green_fireball",
    uiName = "Green Fireball",
    gesture = 125,
    manaCost = 43,
    onCast="fireball",
    skill = "test_skill",
    requirements = { "test_skill", 1},
    icon = 61,
    spellIcon = 7,
    description = "A flaming ball of fire shoots from your hands.",
}
Then re-define of what the game calls for "FireBall":

Code: Select all

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 == "green_fireball" then
                        self:setParticleSystem("green_fireball")
                    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 == "green_fireball" then
                        self:setColor(vec(0.4, 1, 0.25))
                    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 == "green_fireball" then
                        self:setHitEffect("green_fireball_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",
        },
    },
}
The key to this is overriding the default particleSystem, light and projectile behavior by using the onInit() hook. Also, you will see my solution to storing custom data on game objects. I am using a very cool "data" component that someone on the forum came up with. I'd give credit here if I could remember who. I use it everywhere and it works great. Its a "ScriptComponent" definition with built in getter and setters for any data you want to put on an object. Based on the value I get from this data, it determines how to customize the spell. Note that if I don't get anything, it implies the normal game fireball was cast and the defaults are used.

Here is the code from my party's onCastSpell() method where I set the "data" for "castSpell":

Code: Select all

onCastSpell =
                function(self, champion, spell)
                    print("in party onCastSpell", champion:getOrdinal(), spell)
                    self.go.data:set("castSpell", spell)
                end
And lastly, here is the definition of the custom fireball_blast, set by "hitEffect" in the projectile onInit():

Code: Select all

defineObject{
    name = "green_fireball_blast",
    baseObject = "base_spell",
    components = {
        {
            class = "Particle",
            particleSystem = "green_fireball_blast",
            destroyObject = true,
        },
        {
            class = "Light",
            --color = vec(1, 0.5, 0.25),
            color = vec(0.1, 1.0, 0.3),
            brightness = 40,
            range = 10,
            fadeOut = 0.5,
            disableSelf = true,
        },
        {
            class = "TileDamager",
            attackPower = 70,
            damageType = "fire",
            sound = "fireball_hit",
            screenEffect = "green_fireball_screen",
            woundChance = 40,
            onHitObstacle = function(self, obstacle)               
                local ord = self:getCastByChampion()
                print("in green_fireball onHitObstacle", obstacle.go.id, ord)               
            end,
            onHitMonster = function(self, monster)               
                local ord = self:getCastByChampion()
                print("in green_fireball onHitMonster", monster.go.id, ord, self:getDestroyObject())               
            end,
        },
    },
}
In this code you will see that the print statement with show the ordinal of who cast the spell. Because it started as a built-in spell, it works.

I didn't include the particle defines here. Just copy the ones for fireball and modify as you need.
Last edited by MrChoke on Wed Feb 04, 2015 1:34 am, edited 1 time in total.
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: How do I cast a custom spell like built in spells are ca

Post by minmay »

MrChoke wrote:

Code: Select all

onHitMonster = function(self, monster)
				monster:setCondition("poisoned", 25)

				-- mark condition so that exp is awarded if monster is killed by the condition
				local poisonedCondition = monster.go.poisoned
				local ord = self:getCastByChampion()
				if poisonedCondition and ord then
					poisonedCondition:setCausedByChampion(ord)
				end
			end,
The above code will not work like that unless it is cast as a built-in spell. I experimented quite a bit on it.
But you don't need this. You can just change

Code: Select all

poisonedCondition:setCausedByChampion(ord)
to

Code: Select all

poisonedCondition:setCausedByChampion(1)
and get the same effect.
Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
Post Reply