some scripting tricks I've come across

Talk about creating Grimrock 1 levels and mods here. Warning: forum contains spoilers!
User avatar
Grimwold
Posts: 511
Joined: Thu Sep 13, 2012 11:45 pm
Location: A Dungeon somewhere in the UK

some scripting tricks I've come across

Post by Grimwold »

As I've been working with Grimrock editor.. and in particular writing scripts, I've come across some neat things that may not be well documented, or are hidden within scripts that people have written... so I thought I'd start a thread to share a few I've come across... if people are interested and this takes off, please post your own tricks.

These are taken from working scripts that I have used, but are not complete scripts by themselves.

Number of items in a list.
if you have a list in a script entity then you can quickly find how many by using #

Code: Select all

monsters = {"snail","herder","crowern","ogre"}
random_monster = math.random(1,#monsters)
chooses a random monster from the list... but now you can easily add more to the list without ever having to change the math.random range.


Compare text using a Substring
if you want to compare the name or id of entities and use a partial match all that begin with a particular word or phrase you can you sub to create a substring

Code: Select all

for i in allEntities(party.level) do
  if i.id:sub(1,13) == "sequence_pit_" then
    i:open()
  end
end
in this example we create a substring of the id that starts at character 1 and is 13 characters long. It then checks every pit on the same level as the party and opens it if the id substring begins with "sequence_pit_" Therefore sequence_pit_1; sequence_pit_2; sequence_pit_3; sequence_pit_4 etc. will all be opened because the first 13 characters all match "sequence_pit_"

EDIT - clarification on how to use sub
s:sub(i,j)
OR
string.sub(s,i,j)
s is the string we want to look at.
i is the start character
j (optional) is the end character. if j is omitted, then the result will run to the end of the original string s.

if i is negative, then the substring will start that many characters in from the right.

Check which plate/button/switch triggered the function
When you create a connection in the editor between an entity and a script, the entity is passed to the script as a variable..

Code: Select all

function checkTrigger(trigger)
if trigger.id == "plate_N" then
     Tfacing = 3
  elseif trigger.id == "plate_E" then
    Tfacing = 0
  elseif trigger.id == "plate_S" then
    Tfacing = 1
  elseif trigger.id == "plate_W" then
    Tfacing = 2
  else
    Tfacing = 0
  end
spawn("teleporter",party.level,party.x,party.y,Tfacing)
Here we change the facing of a teleporter we spawn at the party's location, based on which of 4 pressure plates was activated.


Spawn items "up in the air"
When spawning items to automatically give to the party, or insert into containers, It was suggested by Petri to spawn them "up in the air", because if spawned with co-ordinates, they will remain in that space when (a copy is) placed in a champion's inventory.
A simple way to spawn something "in the air" is

Code: Select all

spawn("item_name") 
If you want to give an item a particular id to refer to later, you need a more detailed spawn command

Code: Select all

eye_socket_left:addItem(spawn("blue_gem",nil,nil,nil,nil,"fake_blue_gem"))
here we set the level, x, y and facing to nil when we spawn the gem as we add it to the eye socket.


Check if something has a nil value
As I've been working with more complex scripts I've often found the game crashes because I have attempted to use something that does not exist, or has a nil value... so it can often be beneficial to check if things are nil before acting upon them.

Code: Select all

if party:getChampion(1):getItem(7) == nil then
  party:getChampion(1):InsertItem(7,spawn("throwing_axe"))
else
  spawn("throwing_axe",party.level,party.x,party.y,party.facing)
end
This is a basic example where we check if the 1st champion's left hand is empty, and spawn a throwing axe there, otherwise we spawn it on the ground at the party's location. (this was part of my returning axe script)



Check if something IS NOT equal
Possibly a very basic one, but most people will be familar with checking if a == b, but also useful is to use ~= to check if something is not equal.

Code: Select all

if party:getChampion(1):getItem(7) ~= nil then
  party:getChampion(1):removeItem(7)
end
Here we check if the 1st champion has an item in his left hand, if an item exists, we remove it.


Repeat something until something else happens
Repeat until is a very powerful way to create a loop... though care should be taken using it, as it has the potential to cause an endless loop if the until condition can never be met.

Code: Select all

a = math.random(1,4)
repeat b = math.random(1,4)
until a ~= b
This is a simple way to pick two random numbers a and b between 1 and 4 without a and b ever being the same. This could be used to pick two different champions in the party.


Do something for every value in a range (loop)
the for command can be very useful for creating looping scripts in Grimrock editor, allowing us to automate tasks a certain number of times.

Code: Select all

for i = 1,4 do
  party:getChampion(i):insertItem(11,spawn("pitroot_bread"))
end
This loops over the values 1,2,3 and 4 and for each it adds a pitroot bread to the first inventory slot of that numbered champion.



Do something for every entry in a list (loop)
we can also use a for loop to do something for every entry in a list.

Code: Select all

local list_of_gates = {"dungeon_portcullis_4","dungeon_portcullis_2","dungeon_portcullis_3"}
  for _,i in ipairs(list_of_gates) do
    door = findEntity(i)
    door:open()
  end
In this case we go through a list of portcullis entity ids and open each door with that id.



Concatenate (i.e. join) strings
Sometimes it is useful to join separate strings together as a single string, especially when working with a variable string and a fixed string. To do this we can use the concatenate operator ..

Code: Select all

hudPrint(party:getChampion(1):getName() .. " is feeling hungry")
in this example we print to the screen that our first champion is hungry... if you are using the default party it will actually say "Contar Stoneskull is feeling hungry". It's worth noting that If you're joining a variable to a fixed string you will probably need to put a space in at the beginning of your string (as in the example above).


Add multiple tests to the same hook
With so many spells and effects using party, monster or other hooks, it can happen that you need to run two different scripts as part of the same hook. This can be done by evaluating each script as a variable and then returning both.

Code: Select all

    onTurn = function(monster,turn_dir)
      local hold_test = grimwold_spell_script.checkHeld(monster)
      local turn_test = grimwold_spell_script.checkTurn(monster,turn_dir)
      return hold_test and turn_test
    end,
here we first evaluate the checkHeld() function which will run a script and output either true or false. then we evaluate the checkTurn() function which will run another script and again output either true of false. Finally we return the AND of the outputs.. so if either (or both) output is false the overall output will be false and the monster will not execute its turn. If both are true, the monster turns.
Last edited by Grimwold on Thu Nov 29, 2012 6:18 pm, edited 7 times in total.
User avatar
HaunterV
Posts: 676
Joined: Mon Apr 16, 2012 9:54 pm
Location: Barrie, Ontario, Canada

Re: some scripting tricks I've come across

Post by HaunterV »

Crazy.
Grimrock Community 'FrankenDungeon 2012. Submit your entry now!: http://tinyurl.com/cnupr7h
SUBMIT YOUR ASSETS! Community Asset Pack (C.A.P.): http://tinyurl.com/bqvykrp
Behold! The HeroQuest Revival!: http://tinyurl.com/cu52ksc
User avatar
Komag
Posts: 3658
Joined: Sat Jul 28, 2012 4:55 pm
Location: Boston, USA

Re: some scripting tricks I've come across

Post by Komag »

Thank you VERY MUCH for this excellent list if info with good clear examples, very useful. Superthreaded! 8-)

(but what's with the 1,13 in the sequence pit example?)
Finished Dungeons - complete mods to play
User avatar
Grimwold
Posts: 511
Joined: Thu Sep 13, 2012 11:45 pm
Location: A Dungeon somewhere in the UK

Re: some scripting tricks I've come across

Post by Grimwold »

Komag wrote:Thank you VERY MUCH for this excellent list if info with good clear examples, very useful. Superthreaded! 8-)

(but what's with the 1,13 in the sequence pit example?)
the sub(1,13) creates a substring of the id of entity i starting at the first character and being 13 characters long.. so for any of the pits with IDs
sequence_pit_1; sequence_pit_2;sequence_pit_3;sequence_pit_4;sequence_pit_5 the string i.id:sub(1,13) will all be "sequence_pit_"
so It basically does a partial match looking at only the first 13 characters of the id.


(I will add something to this effect in the original post).
User avatar
Grimwold
Posts: 511
Joined: Thu Sep 13, 2012 11:45 pm
Location: A Dungeon somewhere in the UK

Re: some scripting tricks I've come across

Post by Grimwold »

Added the Concatenate strings entry to the OP.

Also.. Putting this here as it feels relevant, and so I can find it again (if I get chance to write a resource for the wiki).

Ahmyo wrote:Is there anything I can read to help me understand how and why this script is put together and what it all correlates to? For instance, 'onDie = function(self)' What does function(self) mean? I think I understand what its meant for, but just not what it means.. y'know what I mean?
To answer your question, a function is a way to isolate a piece of script and make it only run when you want it... usually a function will look something like

Code: Select all

function functionName(variable1,variable2)
 -- do some stuff here
end
so when we want to run that block of code, we call the function by name and pass any info we want to it via the variables.
e.g.

Code: Select all

functionName("spider",3)
With scripting hooks the format is a bit different, and the variables we use are determined by the type of hook. For example onDie will provide an entity for the actual monster that is dying.
hence

Code: Select all

onDie = function(self)
or you could write

Code: Select all

onDie = function(monster)
whatever word you put in the bracket you will be able to use to represent the monster that just died....

so a very simple example would be:

Code: Select all

onDie = function(monster)
  hudPrint(monster.name .. " just died")
end
which will print e.g. "spider just died" on the screen.

alternatively

Code: Select all

onDie = function(monster)
  hudPrint(monster.id .. " just died")
end
will print e.g. "spider_1 just died".
User avatar
SpiderFighter
Posts: 789
Joined: Thu Apr 12, 2012 4:15 pm

Re: some scripting tricks I've come across

Post by SpiderFighter »

What an unbelievable resource, Grim.

Man, I wish I understood any of it. :D
User avatar
JKos
Posts: 464
Joined: Wed Sep 12, 2012 10:03 pm
Location: Finland
Contact:

Re: some scripting tricks I've come across

Post by JKos »

Good thread. Mind if I borrow it? ;)

How to calculate originator for damageTile (and use flags argument in general)
I'm pretty sure that this isn't clear for everyone so I try to explain it.

Example

Code: Select all

local champion = party:getChampion(1)
local originator = 2 ^ (champion:getOrdinal()+1) 
local damage = math.random(2,15)				
damageTile(snail_1.level,snail_1.x,snail_1.y,(snail_1.facing + 2)%4,originator+1, 'physical',damage)
the flags field is just a number between 0-256 and when presented as a bitfield it's 8 bits.

bit 0, value = 1 : monsters recoil from the impact
bit 1, value = 2 : ongoing damage such as poison cloud
bit 2, value = 4: damage originated from champion with ordinal index 1
bit 3, value = 8: damage originated from champion with ordinal index 2
bit 4, value = 16: damage originated from champion with ordinal index 3
bit 5, value = 32: damage originated from champion with ordinal index 4
bit 6, value = 64: ignore immunities
bit 7, value = 128: halve damage of back row party members

so if we want that damage is originated from champion 1 (so that champion gets XP) we pass a number 4 as a flags argument value. You can calculate that value by using exponent of 2 (2 ^ bit number)
as you can see:

Code: Select all

  |bit|value
2 ^ 0 = 1
2 ^ 1 = 2
2 ^ 2 = 4
2 ^ 3 = 8
In the code example I also add number 1 to the champions ordinal because champions ordinal numbers start from 1 but champion flags start from bit 2.

Code: Select all

local originator = 2 ^ (champion:getOrdinal()+1) 
The result is value of bit 2, which is 4.

And in function call I also add 1, because I want to deal physical damage and use "monsters recoil from the impact" bit. If I also wanted to use for example "ignore immunities" bit
all I need to do is add number 64 to flags argument value.

1+4+64 = 69 = "monsters recoil from the impact" and "damage originated from champion with ordinal index 1" and "ignore immunities".
- LoG Framework 2http://sites.google.com/site/jkoslog2 Define hooks in runtime by entity.name or entity.id + multiple hooks support.
- cloneObject viewtopic.php?f=22&t=8450
User avatar
Grimwold
Posts: 511
Joined: Thu Sep 13, 2012 11:45 pm
Location: A Dungeon somewhere in the UK

Re: some scripting tricks I've come across

Post by Grimwold »

Great post. Thanks... I had not looked at the mysteries of the bit field, but guessed it might be a bit like unix permissions (where eXecute, Write and Read have values 1 2 and 4)
User avatar
akroma222
Posts: 1029
Joined: Thu Oct 04, 2012 10:08 am

Re: some scripting tricks I've come across

Post by akroma222 »

Super useful indeed, thanks heaps!
User avatar
cromcrom
Posts: 549
Joined: Tue Sep 11, 2012 7:16 am
Location: Chateauroux in a socialist s#!$*&% formerly known as "France"

Re: some scripting tricks I've come across

Post by cromcrom »

Multiple returns. I find this very handy when I want a function to return multiple things: Example, a function that return the gold, silver and copper price of an item (a silly function here, but only an example)

Code: Select all

--function that returns multiple values
function returnPrice()
local gold = math.random(1,10)
local silver = math.random(1,10)
local copper = math.random(1,10)
return gold,silver,copper -- multiple values must be in order
end
then use the function like below

Code: Select all

local goldPrice,silverPrice,copperPrice = returnPrice()  --the local variables (must be in same order as returned by the function

in the same idea, you can write

Code: Select all

local x,y,z = 0,0,0 --for instance
hope it helps.
A trip of a thousand leagues starts with a step.
Post Reply