Basic stuff
Detecting reloads
To detect when the dungeon is loaded, place a single instance of this object anywhere in your dungeon:
Code: Select all
defineObject{
name = "load_detector",
placement = "floor",
components = {
{
class = "Null",
onInit = function(self)
-- Whenever this function is called, the dungeon has
-- been loaded; either a new game was started, or a
-- saved game was loaded.
end,
},
},
editorIcon = 148,
minimalSaveState = true,
}
Store volatile data
The global tables - Config, Dungeon, GameMode, etc. - are not protected by metatables like all the objects in the dungeon. You can freely assign new keys or reassign existing keys in them. The line
Code: Select all
GameMode.cowString = "cow"
This means that you can use them to store volatile data: variables that won't be saved in save games, and will be lost if the player closes the game and reloads. Why would you want to do that? Consider these possibilities:
- You have unserializable values, such as GameObject or Component references, or functions with upvalues. You can re-generate these values if needed, but doing so is computationally expensive and will lag the game. If you were only using ScriptComponents etc., you would have to re-generate these values on every frame that you are using them. If you store them in a global table instead, you only need to re-generate them once per load, basically eliminating the lag problem.
- A similar situation: you have data that is serializable, but extremely large, like an array with 1,000 elements, and can be re-generated if needed. Such an array is no problem to store in memory, but saving it uses a lot of additional memory and takes a lot of time. If you store it in a global table, you don't need to worry about hurting savegame performance, at the expense of having to re-generate it on every load.
These are not impossible examples. I have encountered both situations in my own mods.
Now, the thought of doing this with the global tables should make any good programmer wince, since this is certainly not what these tables are intended for, and is not exactly intuitive. But there are some occasions where performance hacks are worth the wincing.
Overwriting standard functions in the global environment
You can't. Nothing in the global environment can be overwritten. The tables like GameMode can be modified, as you saw, but you can't actually reassign _G["GameMode"].
The only user-accessible place I can find that actually has access to the global environment is Component hooks, like onInit, onEquipItem, etc., but even these aren't allowed to actually modify the global environment (you'll get an error along the lines of "attempt to modify global environment" - thanks, metatables).
Explicit access to _G isn't allowed at all in Grimrock as far as I can tell.
However, you can stil shadow standard functions, and it's convenient. Example:
Code: Select all
orig_defineObject = defineObject
do
orig_defineObject{
name = "walltext_marker",
components = {
{
class = "MapMarker",
}
},
placement = "floor",
}
local markWalltext = function(self)
-- We recreate the note every time in case the walltext has changed. This is almost certainly
-- more efficient than a potentially very long string comparison (no way to test address AFAIK).
-- XXX: This does mean that any change to the note made by the player will be overwritten. I think
-- that's okay.
local mid = "wmapnote"..self.go.id
if findEntity(mid) then findEntity(mid):destroy() end
-- Try to put marker one square behind the walltext. This is done in case of multiple walltexts on the same square
-- (which will have different facings), and also just looks a bit better in general IMO.
-- If there's already a marker there (or the walltext is on the edge of the map), try to put the marker on the
-- same square as the walltext instead. If there's already a marker there, too, give up.
local dx,dy = getForward(self.go.facing)
local x = self.go.x+dx
local y = self.go.y+dy
local tries = 0
while true do
local free = true
if x > self.go.map:getWidth() then x = self.go.map:getWidth() end
if x < 0 then x = 0 end
if y > self.go.map:getHeight() then y = self.go.map:getHeight() end
if y < 0 then y = 0 end
for e in self.go.map:entitiesAt(x,y) do
if e.mapmarker then
free = false
break
end
end
if free then
spawn("walltext_marker",self.go.level,x,y,0,0,mid).mapmarker:setText(self:getWallText())
return
elseif tries == 0 then -- square already occupied by a note, try to make one in front of the walltext instead
x=self.go.x
y=self.go.y
else -- if both squares are occupied, give up
return
end
tries=tries+1
end
end
defineObject = function(def)
if def.components then
for i,c in ipairs(def.components) do
if c.class == "WallText" then
local orig_onShowText = c.onShowText
if orig_onShowText then
if not orig_onShowText == markWalltext then
c.onShowText = function(self)
local rval = orig_onShowText(self)
if rval ~= false then markWalltext(self) end
return rval
end
end
else
c.onShowText = markWalltext
end
end
end
end
orig_defineObject(def)
end
end
Oh, and because of the mechanism it uses, it also happens to be compatible with jKos' hook framework.
This sort of thing is extremely convenient if you want to add generic interface improvements like automatically noting wall text. Also, notice that I left orig_defineObject as a public variable, so that any file included from init.lua can use it, in case you want to specifically define an object without that wall text hook.
Remember that you can also un-shadow a variable by setting it to nil; if you use
Code: Select all
defineObject = nil
So can you shadow the "spawn" global function in the same manner to run a hook every time an object is spawned? No, not with any usefulness, because you're only shadowing it, not overwriting it. Your custom "spawn" will be out of scope everywhere useful.
Important: On a map vs. not on the map
When you start Isle of Nex, you're in a cage with a branch in front of you. This branch is on the map. On every frame, its components are updated and its ModelComponent is drawn. If you leave the level, it will still be on a map, just not on the current map. Its components will still update, but not every frame (maps other than the current one take turns updating, so it might update every second frame or every 500th frame depending on how many maps the dungeon has and how how close you are to the map), and it won't be drawn.
However, when you pick up that branch, and it becomes the mouse item, it is removed from the map. The GameObject's 'map' field is set to nil, and its components stop updating. Furthermore:
- The object cannot be retrieved by findEntity() if it is not on a map.
- Obviously, since it is not on any map, Map:entitiesAt() and Map:allEntities() won't find it either.
- The following methods will CRASH if you call them with an object that is not on a map:
GameObject:destroy()
GameObject:destroyDelayed()
GameObject:getPosition()
GameObject:getSubtileOffset()
GameObject:removeAllComponents()
GameObject:removeComponent()
GameObject:setPosition()
GameObject:setSubtileOffset()
GameObject:setWorldPosition()
GameObject:spawn()
Various component methods
Interestingly, GameObject:getElevation(), GameObject:getWorldPosition(), GameObject:getWorldPositionY(), and GameObject:getWorldRotation() will still return the position/rotation from when the object was last on a map, which I suppose has vague potential to be useful. Also, notice that GameObject:setWorldPositionY() and GameObject:setWorldRotation() and its derivatives still work on objects that are not on a map, but since calling them won't add the object back to a map, it's hard to think of a use for this.
- If an object not on a map has any connectors, and it triggers them, it will crash, because triggering a connector uses the triggering object's map. The raw hooks in the component definition will still work; they don't use the map. This means that you can imitate connector functionality without needing a map. There is already a Grimrock 2 library that can do this: viewtopic.php?f=22&t=8345
- An object that is not on a map will not run any of its components' onInit() hooks. Even if the object is added to a map later, the onInit() hooks will still not run. Hooks like ControllerComponent.onInitialActivate() will still work, however.
- An object that is not on a map does not enforce its unique id. If there is an object in a champin's inventory with the id "rock1", and you spawn a new object with the id "rock1", it won't crash; you'll end up with two objects that both have the id "rock1". You should NEVER do this, because it will result in a crash when the game is saved and loaded.
- The savegame code will not directly reach an object that is not on a map, but since it will reach inside containers and inventories, this is really only relevant when you do something that renders an object completely unretrievable like removing it from an inventory without returning it to the map (or another inventory). This is a good thing; it acts as a sort of "garbage collection" for those objects when the game is saved and loaded.
Inventories, surfaces, and sockets
It is important that, across all inventories, SurfaceComponents, SocketComponents, ContainerItemComponents, etc., you maintain the discipline of having only *one* reference to an object at a time. Should two SurfaceComponents (or two inventories, or an inventory and a SocketComponent, etc.) have a reference to the same item at the same time, one of the following will happen when the game is saved and loaded:
1. If the game is able to find the item, it will probably duplicate the object. This is obviously not something you want.
2. If the game is not able to find the item, it will crash. This is also not something you want.
Moving an item, adding it to the map, or adding it to another inventory or surface or socket or container does NOT remove it from its current inventory or surface or socket or container, if it has one. You need to either formally remove the item with removeItem(), destroy the item's object, or destroy the surface or inventory. The party using the mouse to pick up an item will also remove it from its surface or socket if it has one, but you obviously do not have the ability to make that happen with the scripting interface.
This poses a problem with SurfaceComponent because it lacks a removeItem() method. To remove an item from a SurfaceComponent via script, you must either:
1. Destroy the ItemComponent with GameObject:removeComponent(). By extension, GameObject:removeAllComponents(), GameObject:destroy(), and GameObject:destroyDelayed() will also do the job, but since those (essentially) destroy the entire object, it's probably not what you were looking for.
2. Destroy the SurfaceComponent in the same way.
Option 1. is preferable, since option 2. removes *all* items from the surface. But it's still almost useless, because after destroying the ItemComponent, you can't get it back. You can copy most of the properties of the original ItemComponent to the new one, but you cannot copy the onThrowAttackHitMonster, onEquipItem, or onUnequipItem hooks; you would need to spawn a completely new object to get the hooks back. You will encounter the same problem if you try to use option 2.
If an object has an ItemComponent, you can easily remove it from the map by adding that ItemComponent to a champion or monster's inventory, or a ContainerItemComponent, or setting it as the mouse item. However, once again, THIS DOES NOT REMOVE THE ITEM FROM SURFACES OR SOCKETS, and I just explained why that is a big problem.
Remember that you can check if an object is in a surface, container, etc. by checking GameObject:getFullId(). But make sure that you do this BEFORE you add the object to an inventory or whatever, because that will reset the full id, even though the ItemComponent is actually still in a surface/container/whatever!
So, the short version of all this is: Do not, under any circumstances, move an item that might be in a surface.
P.S. Destroying a sack does not destroy the items inside it, it just renders them inaccessible (they will be effectively "destroyed" when the game is saved and reloaded, like items that are not in any map or inventory).
Blend modes
For materials you have the following blending modes available:
"Opaque" - Texture is drawn normally. This is the only blending mode where lighting is supported. Alpha channel is ignored.
"Additive" - The rgb values of each pixel in the texture are added to the rgb values of the pixel behind it. Used for many particles, like fire and lightning. Alpha channel is ignored.
"AdditiveSrcAlpha" - Same as Additive, except the rgb values appear to be multiplied by the alpha value before being added. Very niche use, since you could just do the same thing in the image itself and use regular additive blending. I guess this offers higher color resolution?
"Modulative" - Treat the rgb values as numbers between 0 and 1.0 (e.g. 64 out of 255 is treated as 0.25098) and multiply the rgb values behind each pixel by that number, making them darker.
"PremultipliedAlpha" - Just search the Web for "premultiplied alpha" if you don't know what this is. Most modders won't have a reason to use this.
"Screen" - The counterpart to modulative blending; treat the rgb values as numbers between 0 and 1.0, and divide the rgb values behind each pixel by that number. "Division by zero" is not harmful here, it just produces values of 255.
"Translucent" - Texture is drawn with alpha blending. Remember, only opaque blending supports lighting, so this mode isn't as useful as you hoped. Mostly used for particles that have dark parts, like smoke.
If alphaTest is true in the material definition table, then pixels with alpha under the threshold (which is 50% if I recall correctly) are completely transparent and empty (and shadowmaps take this into account) whereas pixels with alpha over the threshold are completely opaque. Only Additive and Translucent blend modes can be used for particles.
There is no subtractive blend mode; you'll have to make do with modulative and translucent blending if you want to darken stuff.
Component specific stuff
DoorComponent
DoorComponent works with "floor" placement to some extent. Try the commented out definition of beach_secret_door in the asset pack. Such doors will even deal damage to monsters that they are closed on, like in Dungeon Master. Monsters killed by this do not award experience to the party.
However, there is a problem with "floor" placement doors that is the reason they aren't used in the standard assets: unless the door is sparse, items in the same square as the door cannot be picked up. No combination of ObstacleComponent and ItemConstrainBoxComponent will produce a graceful solution to this - someone can always throw an item into the center of the door. Of course, if you simply make the door sparse, this is not a problem. But some doors logically shouldn't be sparse, and sparseness affects more than just items (namely whether a monster can see through it - also whether slimes and poison cloud can attack through it, but that's bascially irrelevant for "floor" placement doors).
Also, "floor" placement doors that close on the party do not do anything special; the party remains inside the closed door, without taking damage or anything. This is obviously bad, but it's pretty easy to just, you know, not close the door when the party is standing in it.
There is also the visual problem of thick doors overlapping items when they close, but this problem is present with "wall" placement doors too, so I feel that's not really relevant (you can use ItemConstrainBoxes to minimize it too).
ModelComponent
staticShadow = true
means that with Low shadow quality, a static shadow will be created for this model. It only affects lights at higher quality levels if the camera is too far from a light source (usually only relevant on levels with SkyComponents, since the default static shadow distance is the same as the indoor occlusion distance, but some lights have staticShadowDistance of 0). If you have a stationary model (floors, walls, ceilings, rocks, etc.) it should probably have this set to true so that the game looks better in low shadow quality. If the model will be moving, you probably want to leave it off (unless the movement is very small like plants swaying in the wind). Some of the doors in the standard assets use staticShadow despite moving, resulting in lighting looking strange after they are opened (or closed, if they started open) for players with Low shadow quality; I personally think they look better without staticShadow, although that does mean that players with Low shadow quality can easily see secret doors with lights behind them.
Every ModelComponent has a bounding box computed automatically. This bounding box determines whether a model will be occluded, so it is important that you keep the bounding box correct for animated models. For example, forest_oak_cluster has a specially defined bounding box because the animation moves the trees out from the center, making the automatically computed bounding box too small.
You can see the effects of an incorrect bounding box by closely watching giant snakes. When they move, their tail leaves their bounding box for a moment, which can lead to the tail suddenly disappearing around corners.
Grimrock lets you define your own bounding box if the automatic one isn't correct:
Code: Select all
{
class = "Model",
model = "assets/models/env/forest_oak_cluster.fbx",
dissolveStart = 2,
dissolveEnd = 5,
shadowLod = 1,
staticShadow = true,
--debugDraw = true,
-- bounding box fix: automatically computed bounding box is too small because the model is built incorrectly
-- (without skinning all the trees in the cluster are in one place)
boundBox = { pos = vec(0, 5, 0), size = vec(8, 10, 8) },
},
dissolveStart = 2
dissolveEnd = 5
Some models have two meshes called "lod0_mesh" and "lod1_mesh". lod0_mesh is used when the camera is closer than dissolveStart squares (dissolveStart*3 meters). lod1_mesh is used when the camera is further away than dissolveEnd squares. When the camera is between these two distances, the models "dissolve" into each other. If the model has no lod1_mesh, then it will dissolve into nothing. This is used for small objects like grass and heather that aren't very noticeable at long distances.
Setting these dynamically works fine, but you need to remember to call ModelComponent:updateLods() after doing so.
You can still have additional meshes and they won't be affected by the dissolution to my knowledge. This is basically only useful if you want a door that doesn't open, like forest_ruins_wall_01, and want it to have two detail meshes; you can't name either one "gate", so you would add a third mesh named "gate" that is just a square inside both of the other meshes.
shadowLod = 1
If set to 0 or 1, makes the model always use its lod1_mesh to cast shadows. If set to nil, makes the model use its lod1_mesh to cast shadows if it is further away than dissolveStart, and the lod0_mesh to cast shadows if it is closer - there is no dissolving for shadows.
If set to any other value, makes the model always use its lod0_mesh to cast shadows.
Setting it dynamically with ModelComponent:setShadowLod() works for changing it to 0 or 1, as long as you call ModelComponent:updateLods() afterwards, but if you change it from 0 or 1 to another number, nothing will change unless the player saves and reloads the game. So you can lower the detail of the shadows, but not increase them again, making this function not very useful.
ItemActionComponent quirks
CastSpellComponent
Supports repeat...sort of. The actual repeating happens just fine; however, CastSpellComponent has the odd property of adding its cooldown to the user's existing cooldown on every repeat. So if your CastSpellComponent has a cooldown of 5 and a repeatCount of 4, it will effectively have a cooldown of 20. Changing the repeatDelay does not affect this; the new cooldown always adds to the existing cooldown, it doesn't replace it.
It is generally easy to bandage this by dividing the cooldown by the repeatCount - in the above example, a cooldown of 5/4 would give the desired 5 cooldown in practice. However, beware of the specific case where a player has enough cooldown modifiers to get a cooldown that is shorter than the repeatDelay, because in that case they could attack with the weapon in between the repeats.
Of course, a player actually getting a cooldown that short should never happen unless they're cheating.
Currently (as of 2.2.4), suffers from a bug: onComputeCooldown hooks don't apply to CastSpellComponent cooldown.
FirearmAttackComponent
Firearm attacks occupy an odd space between melee attacks than projectile attacks. Despite being ranged, they function like a melee attack in most respects: no projectile is created, DoorComponent.onAttackedByChampion is called, and backstabbing even works (but see MeleeAttackComponent below for why you shouldn't give firearms the light_weapon trait). However, they respect projectile colliders.
Because FirearmAttackComponent doesn't have an onHitMonster hook, and doesn't create a projectile, it is not practical to attach custom special effects to FirearmAttacks.
If you want a FirearmAttackComponent that doesn't require ammunition, just set its loadedCount to math.huge. It will then have infinite bullets loaded into it.
MeleeAttackComponent
This is the only ItemActionComponent that works properly on a dual-wielded light weapon; it is the only ItemActionComponent that does not put the other hand's weapon on cooldown when dual-wielded. If you have, for example, a dagger in one hand and a light firearm in the other (with a clip so you can fire it), you can attack with the dagger without putting the firearm on cooldown, but attacking with the firearm will put the dagger on cooldown as if the firearm were not a light weapon.
Other ItemActionComponents with attack power will still suffer the dual-wielding attack power penalty if dual wielded.
Summary of the above: Don't put "light_weapon" on items that lack a MeleeAttackComponent, and you probably don't want to put it on items that have any other ItemActionComponent with attack power, because the damage penalty will apply but not the independent cooldown - so if you wanted to make a light firearm with a bayonet, it would be optimal to pick up your off-hand weapon for a split second to attack with the firearm portion, because you will get more damage and the off-hand weapon is going on cooldown anyway.
So don't do it!
unarmedAttack just means that melee attacks made with the component will count under "Unarmed Attacks Made"
in the statistics. It doesn't do anything else of note.
RangedAttackComponent
ThrowAttackComponent
These are basically the same, so I'm combining them.
If you want to make a ranged/throw attack that doesn't use ammunition, well, you can't. RangedAttackComponent doesn't have a clip. There are workarounds, of course, like using a generic ItemActionComponent and creating the projectile yourself. You also don't get armor piercing (except for a fixed +20 from the hardcoded trait),
RunePanelComponent
Does not support repeat in a useful manner; repeating just reopens the rune panel, clearing any runes you entered if it was already open. And the item remains on cooldown once you cast one spell, even across repeats.
Does not work with the "aquatic" trait. The rune panel will open, but you can't select any runes, and any existing runes will be cleared, just as if the item weren't aquatic.
Does not support cooldown, either. The only way to modify the cooldown of spellcasting is with the champion's cooldown_rate stat.
Currently (as of 2.2.4), suffers from a bug: onComputeCooldown hooks don't apply to RunePanelComponent cooldown.
RunePanelComponent has a unique property with respect to requirements: if an item has a RunePanelComponent as its primary action, and that RunePanelComponent's requirements are not met, then that item's EquipmentItemComponent (if any) will not confer any of its bonuses to the wearer. No other Component does this. I discuss the implications of this, and a potential use, here: viewtopic.php?f=22&t=9273&p=89582#p89582 (I know I once said that dual wielding works with RunePanelComponent; I was completely mistaken. It does not.)
General ItemComponent stuff
Do not use convertToItemOnImpact. It does not work. Projectiles that collide with obstacles can spawn the new projectile on the other side of the obstacle, possibly making it inaccessible. A stack of projectiles that collides with anything will be converted into a *single* item (throw 5 fire arrows at a wall and they turn into 1).
Just don't use it, seriously. If you really want it, I strongly recommend reimplementing it yourself to fix these bugs. It shouldn't be hard to do, since ProjectileImpactComponent exists (this is pretty much the only practical use for ProjectileImpactComponent, incidentally).
Do not use ItemComponent:getFuel(). It will crash. Check for a TorchItemComponent and call that component's getFuel instead.
Under no circumstances should you manually create a ProjectileComponent on an object with an ItemComponent. It will crash when it lands. Instead, use ItemComponent:throwItem() to create a ProjectileComponent on the object (which will conveniently be named "projectile") and use that ProjectileComponent. Don't manually remove this ProjectileComponent either; do it with ItemComponent:land() if you need the item to stop flying.
Disabling an ItemComponent will prevent the party from picking up the item normally. This is useful if you want to, for example, lock an item in place in a keyhole. Automatic pickup of ammunition that has been thrown/fired will still occur, even if the ammunition's ItemComponent is disabled.