Modding infodump

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!
minmay
Posts: 2789
Joined: Mon Sep 23, 2013 2:24 am

Modding infodump

Post by minmay »

Here's a collection of terribly formatted crap I've written about Grimrock 2 modding that I couldn't think of a better place for.

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,
}
This is useful for many things, since not everything is exactly preserved across save-load. For example, particle systems start over when the game is reloaded.

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"
works fine. Furthermore, these global tables are not saved when the game is saved, but some of them (notably Config and GameMode) persist even across games, and are only lost when the game is exited. Some of them (notably Config) are even accessible from init.lua and anything it imports.
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
Paste this at the top of your init.lua (before importing anything) and now every WallTextComponent will automatically create a map note with its text when the player reads it - and the original onShowText hook, if any, is completely preserved, thanks to the magic of upvalues. And using upvalues here is okay, because these hooks never get serialized; they are part of the asset definition. It doesn't cause any strange or bad behaviour with any combination of saving/reloading/removeComponent/createComponent/restarting the game executable, I've tested it.
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
after that, the custom defineObject is removed, and the original becomes visible again.

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) },
},
I suppose this is also a good time to mention that debugDraw for models draws the bounding box.

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.
Last edited by minmay on Tue Jun 21, 2016 7:38 pm, edited 2 times in total.
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.
minmay
Posts: 2789
Joined: Mon Sep 23, 2013 2:24 am

Re: Modding infodump

Post by minmay »

Combat mechanics
Protection
Player protection only applies to melee attacks. Monster protection applies to both melee attacks and physical projectiles, but not to TileDamagerComponents (e.g. all spells) or conditions (burning, poison). The effect of protection on damage is replicated by this code:

Code: Select all

function applyProtection(damage, protection, pierce)
	protection = protection-pierce
	if protection <= 0 then
		return damage
	elseif protection >= 1000 then
		return 0
	else
		return math.max(1,damage-math.floor((math.random()+0.5)*protection))
	end
end
Note that this provides an easy way to make a monster or player specifically immune to protection-affected attacks: give them at least 1000+[maximum available pierce in your dungeon] protection. Also note that the distribution is slightly distorted at protection value of 1.

Evasion
Evasion is complicated by a hidden property on each monster and champion, called "luck". Every monster and player starts with 0 luck. Whenever a champion misses a monster with an attack (attacking empty space, obstacles, etc. does nothing), their luck increases by 3 to a maximum of 15. As soon as that champion scores a hit on any monster, the champion's luck resets to 0.
Whenever a monster misses a champion, the monster's luck increases by 5 to a maximum of 20. Whenever a monster hits a champion, the monster's luck decreases by 5 to a minimum of -20.
So what does luck mean? Luck simply gets added to the champion's or monster's accuracy when they try to attack. This means that if a champion or monster misses, their next attack actually becomes more likely to hit, and if a champion or monster hits, their next attack actually becomes less likely to hit.

I think the effect of a monster's evasion can be replicated by this code (returns true if monster is hit, false otherwise):

Code: Select all

function checkHitMonster(monster, accuracy, evasion)
	if monster.go.sleep or monster.go.frozen then
		return true
	else
		return evasion < 1000 and math.random() >= math.clamp(60+accuracy-evasion,5,95)/100
	end
end
To account for luck you would just add the champion's luck to the accuracy before calling this function. However, the user scripting interface doesn't actually have access to luck, so you'd need to keep track of it yourself - gross.

Having a component named "frozen" will inflict most of the effects of freezing even if it is not a FreezingMonsterComponent, however, a component named "sleep" will not have the other effects of SleepingMonsterComponent (no "Zzz" text, the monster can still act, damaging the monster won't remove the component). So if you are willing to prevent a monster from getting the actual sleep condition, you can make a monster that the player can never miss by adding e.g. a NullComponent named "sleep".

A champion's accuracy is decreased by 50 if they have the "blind" or "head_wound" conditions. If they have both conditions, it's still only decreased by 50 total, not 100.

Now, the effect of evasion for players is a little different. It only affects melee MonsterAttackComponent attacks, champions always get hit by projectiles. An interesting special case: if the MonsterAttackComponent's accuracy is at least 100, or is false (not nil, not 0, but false) then the attack will always hit, ignoring evasion entirely.
This is where I'd like to post code for reproducing player evasion, but I can't actually figure out what the formula for player evasion is. There's something weird about the curve that I haven't figured out yet, in addition to luck.

I do know that a monster's accuracy is increased by 15 on Hard, decreased by 10 on Easy, and decreased by 30 if the monster is blinded (from darkbolt).

Everything else
The result of rollDamage([value]) is uniformly distributed between floor([value]*0.5) and floor([value]*1.5). Therefore, the mean of rollDamage(4) is 4, but the mean of rollDamage(3) is 2.5.

When making a melee attack, it potentially hits monsters/obstacles/doors if they are "in front" of the party. Melee attacks do not care about the capsule, as far as I can tell. The same happens with TileDamagers.
Projectiles collide with monsters if the monster's capsule intersects the projectile. They do not care about the monster object's actual position, as far as I can tell.
Firearm attacks seem to behave like projectiles in this respect, which is a little unexpected since they behave so much like melee attacks otherwise.

I mainly bring this up to correct a common misconception: a lot of people think that xeloroids, mosquito swarms, and spiders are able to enter the party's tile. This is false. They never leave their original tile while attacking. However, their attack animations cause their capsules to intersect the party's square during parts of their attack; therefore, throwing a bomb/fireball/whatever at them can cause an explosion in the party's square, damaging the party and dealing no damage to the monster - because the monster is not in the party's square.

It is possible for monsters to move into the party's square if their MonsterComponent has the swarm flag. The only monster in the standard assets with this flag is rat_swarm. This behaviour is completely unrelated to everything I mentioned above, and people need to realize that!
(You can also teleport monsters into the party's square, and vice versa, if you are a bad modder, but that's pretty obvious).

Traits/skills/races/classes with hardcoded effects
Traits and skills given certain names (not uiNames) have special, hardcoded effects in addition to whatever effects you define. These are:
"aggressive": Attack power increased by 4.
"farmer": When champions with the farmer trait consume items with a nutritionValue, they gain experience. The amount of experience gained is (level^1.1*0.19+X)*(nutritionValue*0.5+250) xp, where X = 0.3 if their level is below 11, X = 0.8 if their level is 11 or 12, and X = 1.8 if their level is 13 or greater. Champions with the "farmer" trait do not gain experience from killing monsters, but still gain experience from digging up chests, scripts calling Champion:gainExp(), etc.
"alchemist": After every X tiles walked, if the champion has any ItemComponents in their inventory with parent GameObjects having one of six specific names, they will receive another one of said item. It will try to place the new item in an empty slot, but if it cannot, it will just increase the size of an existing stack (resulting in a crash if, for some reason, you've made these items unstackable). The six object names and X values are: blooddrop_cap = 850, etherweed = 930, mudwort = 1950, falconskyre = 2500, blackmoss = 3700, crystal_flower = 4500.
"insectoid" (race only): Preferred food is horned_fruit.
"lizardman" (race only): Preferred food is turtle_eggs.
"ratling" (race only): Preferred food is cheese.
"skilled": Receives one extra skill point at the start of the game.
"rage": When the champion's health dips below 20% of their maximum health, the value of their "rage" condition is increased by 20.
"quick": The cooldown of spell casts is multiplied by 0.85. Yes, 0.85, not 0.9. There is no such compensation for uncanny_speed, which has no hardcoded effects and does not affect spell cooldown!
"mutation": When the champion levels up, one of their stats increases by 1. A champion-specific PRNG is used to pick the stat. The same PRNG is used to pick stats from preferred food. This PRNG is not used for anything else. This prevents you from saving/loading to get your preferred stat.
"two_handed_mastery": The champion can wield 2-handed items as if they were 1-handed.
"light_armor_proficiency": The champion does not suffer the evasion penalty from items with the light_armor trait.
"heavy_armor_proficiency": The champion does not suffer the evasion penalty from items with the heavy_armor trait.
"armor_expert": For the purpose of determining load, the weight of equipped items with the light_armor or heavy_armor traits is halved.
"shield_expert": The evasion bonus of equipped EquipmentItemComponents whose item has the "shield" trait is increased by 50%.
"staff_defence": If the champion's left or right hand contains an item with a component named "runepanel", their protection and all resistances are increased by 10.
"improved_alchemy": CraftPotionComponent creates potion_greater_healing in place of potion_healing and potion_greater_energy in place of potion_energy.
"bomb_expert": CraftPotionComponent sets the stack size to 3 for any created fire_bomb, shock_bomb, poison_bomb, or frost_bomb.
"backstab": MeleeAttackComponent and FirearmAttackComponent attacks can backstab if their ItemComponent has the "dagger" trait.
"assassin": MeleeAttackComponent and FirearmAttackComponent attacks can backstab if their ItemComponent has the "light_weapon" trait.
"firearm_mastery": FirearmAttackComponent never jams or backfires.
"dual_wield": If items in both hands have the "light_weapon" trait and at least one has the "dagger" trait, the champion is considered to be dual-wielding.
"improved_dual_wield": If items in both hands have the "light_weapon" trait, the champion is considered to be dual-wielding.
"rogue_dual_wield": The base damage penalty for dual wielding is 25% instead of 40%.
"piercing_arrows": RangedAttackComponent attacks gain 20 pierce.
"double_throw": ThrowAttackComponent attacks will attempt to throw from the other hand after a short delay.
"reach": All melee attacks have reaching.
"leadership": All champions gain +1 to all stats while this champion is alive.
"nightstalker": While the time of day is greater than or equal to 1, the champion gains 5 vitality; they lose 5 vitality when the time of day is less than 1.
"melee_specialist": MeleeAttackComponent buildup time is decreased by 50% and energy cost decreased by 25%.
"hand_caster": Unarmed attacks function as RunePanelComponents instead of MeleeAttackComponents.
"firearm_expert": Firearm jam and backfire chance is halved.
"light_weapons": Increases the damage of MeleeAttackComponents whose items have the "light_weapon" trait by 20% per level.
"heavy_weapons": Increases the damage of MeleeAttackComponents whose items have the "heavy_weapon" trait by 20% per level, unless they have the "light_weapon" trait too.
"missile_weapons": Increases the damage of RangedAttackComponents by 20% per level.
"throwing": Increases the damage of ThrowAttackComponents by 20% per level.
"firearms": Increases the range of FirearmAttackComponents by 1 per level. Doesn't actually seem to affect backfire/jam chance in itself?
"armors": Increases the protection from equipped EquipmentItemComponents by 5% per level.
"concentration": Used by the builtin "force_field" and "shield" spells. "darkbolt", "light", and "darkness" do not depend on skill.
"fire_magic": Used by the builtin fire spells.
"air_magic": Used by the builtin air spells.
"water_magic": Used by the builtin water spells.
"earth_magic": Used by the builtin earth spells.

Water
Here is how to determine whether the party is underwater: if they are in a tile with the underwater flag (such as forest_underwater), and their world Y position is below -0.6, they are underwater. Otherwise, they are not.
WaterSurfaceComponent, WaterSurfaceMeshComponent, etc. are strictly visual. They have no effects on gameplay. You cannot change the -0.6 water height, to my knowledge.

Code: Select all

defineObject{
	name = "water_surface_example",
	baseObject = "base_floor_decoration",
	components = {
		{ -- All default values are shown here
			-- updates global reflection and refraction maps
			class = "WaterSurface",

			-- This vector is the color of the fog that appears while the
			-- party is underwater. Values above 1 have the same effect as
			-- 1, values below 0 and NaN have the same effect as 0.
			fogColor = vec(0,0,0),

			-- This is how "dense" the fog is, i.e. how quickly it becomes
			-- completely opaque. I am not sure how this is scaled; a value
			-- of 0 (or lower, or NaN) disables the fog entirely, but a
			-- value of 1 does not make the fog reach full opacity immediately
			-- (it takes about 3 meters from the camera to do so).
			--
			-- Interestingly, fog seems to require that a WaterSurfaceMesh be
			-- present on the object, otherwise it doesn't do anything.
			fogDensity = 0,

			-- planeY is the offset of the reflection plane (the plane
			-- about which reflections are calculated). Logically, this
			-- should be the same as the y position of the WaterSurfaceMesh
			-- or whatever other model you're using with the water material,
			-- but there are reasons to set it to other values (it's actually
			-- set to other values often in the standard assets).
			planeY = -1.2,

			-- This vector modifies the color of the reflected scene.
			-- Negative values work.
			reflectionColor = vec(1,1,1),

			-- This vector modifies the color of the refracted scene. (The
			-- refracted scene means the objects that are *beneath*
			-- the water.) Negative values work.
			refractionColor = vec(1,1,1),
		},
		{ -- These aren't defaults
			-- builds a continuous mesh from underwater tiles
			class = "WaterSurfaceMesh",

			-- Material used while the party is above water.
			material = "water_surface_calm",

			-- Material used while the party is underwater.
			underwaterMaterial = "water_surface_underwater",

			-- This is how you change the "water level"; by offsetting the
			-- WaterSurfaceMeshComponent. Remember to adjust the
			-- WaterSurfaceComponent's planeY value. Also, be aware that
			-- positive y offsets can cause very strange behaviour with
			-- 
			offset = vec(0,-1,0),
		},
	},
	dontAdjustHeight = true,
	editorIcon = 264,
}
WaterSurfaceMeshComponent:getWaterLevel() returns the y position of the mesh. This is not synonymous with the y component of the WaterSurfaceMeshComponent's offset; for example, if you let the mesh generate and then move the parent object up by 1 meter, the mesh will move up by 1 meter, and the y component of its offset is the same, but the water level has increased by 1 meter and getWaterLevel() will reflect that.
There's no setWaterLevel() but you don't really need that because you can just change the offset instead.

I have seen people place more than one WaterSurfaceComponent on one level. Don't do that. It doesn't work properly. There should be only one WaterSurfaceComponent on a level at a time. Removing one WaterSurfaceComponent and replacing it with another one should be fine, but I haven't tested it very well.

The water shader

The water shader only works on materials named "ocean_water", "water_surface_calm", or "water_surface_underwater". (All three names seem to behave identically otherwise). Other material names will give bad, useless results.
So if you want to use more than 3 water materials in one mod, take advantage of MaterialEx:setTexture() and MaterialEx:setParam() to change those 3 materials dynamically.
Remember that you need to set the textures/params whenever the game is reloaded, because they revert to their defaults in the defineMaterial table upon reloading.
If you need more than 3 different water materials to be visible to the player at once, then you're out of luck, but I doubt you will ever need that.

Code: Select all

defineMaterial{
	name = "ocean_water",
	shader = "ocean_water",
	diffuseMap = "assets/textures/env/ocean_foam_dif.tga",
	normalMap = "assets/textures/env/ocean_normal.tga",
	displacementMap = "assets/textures/env/ocean_disp.tga",
	doubleSided = false,
	lighting = true,
	alphaTest = false,
	blendMode = "Translucent",
	textureAddressMode = "Wrap",
	glossiness = 80,
	depthBias = 0,
	texOffset = 0,
	foamOffset = 0,
	foamAmount = 1,
	waveAmplitude = 50,
	onUpdate = function(self, time)
		self:setParam("texOffset", time*0.2)
	end,
}
I don't think the lighting = true part will ever matter unless you change the blendMode to "Opaque" (and doing so is pretty much useless). Yes, the shader "supports" all blend modes, although most of them aren't useful in this context.

Parameters you can change with setParam() here that will have a useful effect: texOffset, foamOffset, foamAmount, waveAmplitude.

The colour of the foam is determined by the sky's atmospheric colour (which you can change, but it's hard; see the Sky section below). foamAmount basically multiplies the colour of the diffuse map. So at 0, it's completely invisible, at -1 it inverts the colour, and at math.huge or -math.huge it turns the entire water surface almost completely black or white. At NaN it turns it completely black.

waveAmplitude is the degree to which the vertices of the mesh are permuted to make waves. It interacts oddly with the y position of the mesh (and heightmaps?), so watch out for that.

Changing the normal and displacement maps is a good way to make water look like different materials, such as a sheet of ice or a mirror.

Remember that you can use the water materials on custom models, not just WaterSurfaceMeshComponents. This is how the ocean water is done in the standard assets; ocean_water.model is
a large rectangle split into 80,886 vertices so that the shader makes good-looking waves in it, and the vertices are denser in the parts of the rectangle that are close to the shore,
giving higher-resolution waves. Also note that the beach_ground_water tile doesn't have the underwater flag; it just has a splashy move sound and the "water" automap tile.

WaterSurfaceMeshComponent only builds its mesh on tiles that do have the "underwater" flag. Suppose you want to exploit it to make a "flooded" dungeon where all the floor is under
a shallow layer of water, but the party doesn't actually go underwater. You'd use tiles like this:

Code: Select all

defineTile{
	name = "dungeon_floor_flooded",
	editorIcon = 192,
	color = {120,120,180,255},
	builder = "dungeon",
	floor = {
		"dungeon_floor_dirt_01", 1,
	},
	ceiling = {
		"dungeon_ceiling", 1,
	},
	wall = {
		"dungeon_wall_01", 35,
		"dungeon_wall_02", 35,
		"dungeon_wall_drain", 2,
	},
	pillar = {
		"dungeon_pillar", 1,
	},
	ceilingEdgeVariations = true,
	ceilingShaft = "dungeon_ceiling_shaft",
	underwater = true, -- so that WaterSurfaceMeshComponent builds the mesh over it
	moveSound = "party_move_wade",
}
and a WaterSurfaceMeshComponent like this:

Code: Select all

defineObject{
	name = "water_surface_flooded_dungeon",
	baseObject = "base_floor_decoration",
	components = {
		{
			class = "WaterSurface",
			planeY = 0.4, -- Notice that the planeY is the same as the y offset of the mesh
			reflectionColor = vec(0.77, 0.9, 1.0) * 0.9,
			refractionColor = vec(1,1,1),
		},
		{
			class = "WaterSurfaceMesh",
			material = "water_surface_calm",
			underwaterMaterial = "water_surface_underwater",
			offset = vec(0,0.4,0),
		},
	},
	dontAdjustHeight = true,
	editorIcon = 264,
}
and a material definition to fix the wave amplitude (default is way too high for this height):

Code: Select all

defineMaterial{
	name = "water_surface_calm",
	shader = "ocean_water",
	diffuseMap = "assets/textures/env/ocean_foam_dif.tga",
	normalMap = "assets/textures/env/ocean_normal.tga",
	displacementMap = "assets/textures/env/ocean_disp.tga",
	doubleSided = false,
	lighting = true,
	alphaTest = false,
	blendMode = "Translucent",
	textureAddressMode = "Wrap",
	glossiness = 80,
	depthBias = 0,
	texOffset = 0,
	foamOffset = 0,
	foamAmount = 0,
	waveAmplitude = 0.04,	
	onUpdate = function(self, time)
		self:setParam("texOffset", time*0.03)
	end,
}
and then you would not allow the party or monsters to go below -0.6 world y position (which is easy to do, just don't use floor elevations below 0). The result would look like this: http://i.imgur.com/hJpzz1r.jpg

Sky

Shamefully, I haven't seen a single mod yet that did anything significant with the sky shader, despite the range of possibilities you have - you can give the sky arbitrary clouds, change the speed at which they rotate, change the colours, etc.

But first, understand the components in a sky:

Code: Select all

defineObject{
	name = "forest_day_sky",
	components = {
		-- There is nothing special about the sky models. You are free to use
		-- custom models for the sky with any shape and UVs that you like.
		{
			class = "Model",
			model = "assets/models/env/sky.fbx",
			sortOffset = 200000,	-- force water rendering before other transparent surfaces
			renderHack = "Sky",
		},
		{
			class = "Model",
			name = "nightSky",
			model = "assets/models/env/sky.fbx",
			material = "night_sky",
			sortOffset = 199999,	-- force water rendering before other transparent surfaces
			renderHack = "Sky",
		},
		{
			class = "Model",
			name = "stars",
			model = "assets/models/env/stars.fbx",
			sortOffset = 199998,	-- stars are rendered after night sky
			renderHack = "Sky",
		},
		{
			class = "Light",
			-- Directional lights don't need a SkyComponent to control them; you can put them on any object you like
			-- (but the uses for them are rather narrow).
			type = "directional",
			castShadow = true,
		},
		{
			class = "Light",
			-- Same thing for ambient lights. Note that both the brightness/colour of the ambient light and the SkyComponent's ambientIntensity
			-- will influence how bright the light appears to be.
			name = "ambient",
			type = "ambient",
		},
		-- I have added the default values for all the SkyComponent fields here.
		{
			class = "Sky",
			ambientIntensity = 1,
			-- farClip is two things.
			-- First, farClip is the scale of the sky model. I don't call
			-- it a skybox because it isn't a box.
			-- The model follows the party on the world x and z axes, but not
			-- on the y axis; it is possible for the party to ascend above
			-- the sky model or descend below it.
			-- The default sky model is about 75% of a sphere with a
			-- diameter of exactly 1, so the farClip will be equal to the
			-- diameter.
			--
			-- Second, farClip is the distance from the camera at which
			-- the sky will draw. Anything that is not within this distance
			-- from the party - even if the party has ascended above the sky
			-- model like I mentioned - is occluded, and replaced with the sky
			-- if the sky is "behind" it, or replaced with black otherwise.
			-- This means farClip can be used as a maximum draw distance.
			-- You may be wondering why I bother to distinguish between
			-- these two effects at all; aren't they the same as long as
			-- the party is inside the sky model, which they always should be?
			-- Well, no, and you'll find out why soon.
			--
			-- It's fine to change it dynamically. Remember, a 32x32 level
			-- is 96x96 meters.
			farClip = 130,

			-- There are four fog modes. "linear_lit" is the default, and
			-- it only extends to a distance of about 129.884297 meters;
			-- anything further from the camera than that will be
			-- unaffected by the fog. When this happens to a normal model,
			-- it's very ugly, but it has the advantage of allowing
			-- fog to be used without affecting the sky at all, so long as
			-- the sky's farClip is above approximately 129.884297.
			-- (Remember, farClip is "the distance from the party at
			-- which the sky will draw"; fog will *always* affect every
			-- pixel of the sky the same way, because the sky is always
			-- treated as being that distance away, even if parts of the
			-- sky model are actually closer or further away).
			--
			-- "dense" is the other one that most people know about, used
			-- in places like Keelbreach Bog.
			--
			-- But there are two more, overlooked fog modes:
			-- "linear" is the same as "linear_lit" except it continues
			-- beyond range 128.
			--
			-- "exp" seems to completely ignore fog range unless it's set
			-- to turn fog off entirely, and instead forces a very short
			-- range of about 3 meters; it is not useful as far as I can
			-- tell.
			--
			-- No matter which mode you choose, fog does not perform
			-- occlusion. Try farClip for that.
			fogMode = "linear_lit",

			-- The first number is the distance in meters from the camera
			-- at which the fog begins; the second is the distance at
			-- which it reaches 100% opacity.
			-- If you set both numbers to the same value, e.g.
			-- fogRange = {1,1}, then fog will be disabled entirely
			-- (unless the fog mode is "exp").
			-- Works backwards! If you set it to {8,1} then a halo of fog
			-- will follow the camera, with anything closer than 1 meter
			-- being fully fogged up, and anything more than 8 meters away
			-- being completely clear.
			-- Fine to change dynamically.
			fogRange = {5, 300},

			-- tonemapSaturation multiplies the saturation of all pixels
			-- in the 3d scene (GUI elements are not affected). A value
			-- of 1 is the default, and doesn't change colours at all
			-- (since 1 is the multiplicative identity).
			--
			-- A value of 0 will make the scene completely greyscale.
			--
			-- A sufficiently high value (such as math.huge) will make the
			-- world render with only the 3 primary colours at maximum
			-- brightness (and their combinations). You don't need to worry
			-- about wrapping.
			--
			-- Negative values work, and will have the effect of inverting
			-- the hues on screen (because that's what happens when you
			-- multiply saturation by a negative number). So a
			-- tonemapSaturation of -1 would invert the
			-- hues of the pixels without changing their saturation.
			-- A tonemapSaturation of -2 looks just like an inverted
			-- version of tonemapSaturation = 2.
			--
			-- Finally, a tonemapSaturation of NaN will turn the entire 3D
			-- view completely black. This is probably not safe, and is
			-- completely useless in any case. If you want to turn the
			-- screen black, you should put a CameraComponent inside a
			-- black cube and point it outside the level; that way you
			-- will render as little as possible, no point in rendering
			-- things that won't be seen.
			--
			-- Fine to change dynamically.
			tonemapSaturation = 1,

			-- Okay, now one last thing. For both fog and farClip, the
			-- distance used isn't the real, visual, Euclidean distance
			-- between two points. Instead, it seems to be the Chebyshev
			-- distance, with the axes adopting the camera's rotation.
			--
			-- Along with fog, this is why I emphasized that farClip does
			-- two things.
			--
			-- Suppose the sky has a farClip of 100. Imagine a cube that
			-- is centered on the camera. Also imagine that this cube has
			-- the same rotation as the camera. Each edge of the cube is
			-- 200 meters long (radius of 100 meters).
			--
			-- Anything inside this cube is drawn. Anything outside this
			-- cube is replaced with the sky (or black).
			--
			-- So the sky's farClip actually has a different shape than
			-- the sky's default spherical model. Fog is the same. This shape
			-- is also different from what you're used to seeing in the
			-- rest of the game and life in general (Euclidean distance).
			-- If you make your fog too noticeable (short range and/or
			-- extreme colours) the player will likely see the "square"
			-- shape of it.
		},
		{ -- LensFlareComponent has no fields or methods available. It just
		-- handles the lens flare that appears when you look at the sun.
		-- This is the only component on skies that is optional; you can
		-- completely remove it from the definition and the sky will still
		-- work without complaining (and, obviously, it will not have a lens
		-- flare).
			class = "LensFlare",
		},
	},
	placement = "floor",
	editorIcon = 100,
	reflectionMode = "always", -- Even if you set this to "never", water will
	-- still adopt the sky's atmospheric colour. This can be annoying.
}
That's the basics of skies. Now for some more stuff about skies.
Above, I used "camera" and "party" interchangeably. Generally a bad idea, since the default camera doesn't even have the same position/rotation as the party object does.
Here's the truth. If you use a custom camera instead of the default one, SkyComponent doesn't really know what to do. The sky model still follows the camera on the x and z axes, albeit with some jerkiness, but it does something very strange with the y axis. The higher the camera's y position, the lower the sky model's y position. It doesn't just remain stationary as the camera ascends like it does for the default camera; the sky model actually moves downwards.
Furthermore, farClip doesn't work. It still scales the model, but no actual clipping takes place; the sky will always be behind everything and never occlude anything. And fog treats the sky as if it's a 100 meters away, regardless of the farClip value and (I believe) the model.

So, uh, watch out for that if you're using custom cameras I guess.

By the way, you might have noticed that on levels with no enabled SkyComponent or FogParamsComponent, if you're using the default camera, everything more than a 20 Chebyshev meters away from the party fades into black. This is because all levels have fog by default! The color of this fog is 0,0,0 and its range appears to be {20,25}. However, if you try to increase this fog range, or disable it, like:

Code: Select all

party:createComponent("FogParams"):setFogRange{50,100}
or

Code: Select all

party:createComponent("FogParams"):setFogRange{1,1}
you'll find that everything over 25 Chebyshev meters away is still completely solid color (whatever the appropriate fog color is for 25 meters, and by default that is of course 0,0,0) - and actually occluded, whereas fog itself does not occlude. Obviously, this is quite useful for performance, and possibly ambience, in indoor levels. But what if you want to get rid of this effect? You can't do it with FogParamsComponent, but adding a SkyComponent will do the job. If there's an enabled SkyComponent on the level, this occlusion behavior will disappear. An almost-minimal example:

Code: Select all

defineObject{
	name = "remove_indoor_fog",
	components = {
-- The choice of trap_rune.model is not important, it's just the smallest model file in the standard assets. If you have a "null" model, using that instead would be slightly better.
-- All of these components are mandatory, the sky will crash if any are removed.
-- The game actually re-enables the ModelComponents on its own but since the models have no vertices or triangles this shouldn't matter.
		{
			class = "Model",
			model = "mod_assets/null.fbx",
			enabled = false,
		},
		{
			class = "Model",
			name = "nightSky",
			model = "mod_assets/null.fbx",
			enabled = false,
		},
		{
			class = "Model",
			name = "stars",
			model = "mod_assets/null.fbx",
			enabled = false,
		},
		{
			class = "Light",
			enabled = false,
		},
		{
			class = "Light",
			name = "ambient",
			enabled = false,
		},
		{
			class = "Sky",
			farClip = 4096,
			fogRange = {1, 1},
			fogColor1 = vec(0,0,0),
			fogColor2 = vec(0,0,0),
			fogColor3 = vec(0,0,0),
		},
	},
	placement = "floor",
	editorIcon = 100,
	reflectionMode = "always",
}
Download null.model here (right click and "save link as"). This is just a model with no vertices or triangles, for use when a ModelComponent is required (like it is here) but you don't actually want to draw a model. It has a "gate" node so that it can be used as a dummy model for DoorComponent. Of course, you could use sky.model and it would be fine, but why make the game render all those triangles when it doesn't have to?
There are a few things to be aware of if you're doing this to get rid of the default indoor fog:
1. By removing that default 25 meter occlusion, you are obviously going to hurt performance. This will be an especially awful performance hit if your wallset is missing occluders (i.e. it was built incorrectly, in which case you should fix it anyway :P nobody likes broken wallsets). A very easy way to still get some occlusion is to change the farClip to your desired draw distance, and use fogRange to make a smooth fade to black (or any other fog color you desire), e.g. farClip = 48 and fogRange = {24,48}. But remember what I said above: that will only occlude while the default camera is the active one. It won't help performance with custom cameras.
2. The default static shadow distance for LightComponents is not infinity. It's about...25! So shadow detail level changes will become apparent in your indoor levels when they previously weren't.
3. If your level has water, the reflection will use the fogColor of the sky in any pixels where it can't find anything to reflect. The sky models themselves don't appear to count as something to reflect for this purpose (but the clouds do, or at least appear to????). This is why I set the fogColor to 0,0,0 in the example object. If you set it to 1,0,1, your water meshes will seem to have magenta edges.

Anyway, on to the sky shader itself! The sky shader only works on materials that are named "day_sky" and "night_sky". So if you want more than one kind of sky in your dungeon, take advantage of MaterialEx:setTexture() and MaterialEx:setParam() to change the textures as needed when the party changes levels. You'll also need to make sure to call MaterialEx:setTexture() when the party reloads the game (seeing a pattern here?) since changes made in the onUpdate hook aren't saved; you have to reapply them.

Code: Select all

local cloudsOnUpdate = function(self,time)
	self:setTexture("rayleighTex","assets/textures/common/black.tga")
	self:setTexture("mieTex","assets/textures/common/black.tga")
	self:setParam("time",time*0.04)
	self:setTexcoordScaleOffset(math.sin(time)+2,math.sin(time)+2,0,0)
end
defineMaterial{
	name = "day_sky",
	clouds0Map = "assets/textures/env/sky_cloudy_0.tga",	-- stratos
	clouds1Map = "assets/textures/env/sky_cloudy_1.tga",
	clouds2Map = "assets/textures/env/sky_cloudy_2.tga",
	clouds3Map = "assets/textures/env/sky_cloudy_3.tga",
	cloudsRim1Map = "assets/textures/env/sky_cloudy_rim_1.tga",
	cloudsRim2Map = "assets/textures/env/sky_cloudy_rim_2.tga",
	cloudsRim3Map = "assets/textures/env/sky_cloudy_rim_3.tga",
	doubleSided = false,
	lighting = false,
	alphaTest = false,
	blendMode = "Translucent",
	textureAddressMode = "Wrap",
	glossiness = 30,
	depthBias = 0,
	shader = "sky",
	onUpdate = cloudsOnUpdate,
}

defineMaterial{
	name = "night_sky",
	clouds0Map = "assets/textures/env/sky_cloudy_0.tga",	-- stratos
	clouds1Map = "assets/textures/env/sky_cloudy_1.tga",
	clouds2Map = "assets/textures/env/sky_cloudy_2.tga",
	clouds3Map = "assets/textures/env/sky_cloudy_3.tga",
	cloudsRim1Map = "assets/textures/env/sky_cloudy_rim_1.tga",
	cloudsRim2Map = "assets/textures/env/sky_cloudy_rim_2.tga",
	cloudsRim3Map = "assets/textures/env/sky_cloudy_rim_3.tga",
	doubleSided = false,
	lighting = false,
	alphaTest = false,
	blendMode = "Translucent",
	textureAddressMode = "Wrap",
	glossiness = 30,
	depthBias = 0,
	shader = "sky",
	onUpdate = cloudsOnUpdate,
}
This onUpdate hook does a couple of things. First, it makes the clouds spin around way faster than normal; that's the effect of the "time" param.

Second, it basically kills the atmospheric scattering aspect of the shader and gives you flat colors - that's the effect of changing rayleighTex and mieTex to black.dds. You can use self:setTexture("clouds0Map",...) etc. in the same way to change all the cloud maps.

Here is how to get the original atmospheric scattering back:

Code: Select all

self:setTexture("rayleighTex","assets/atmosphere/rayleigh_scatter_opt.dds")
self:setTexture("mieTex","assets/atmosphere/mie_scatter_opt.dds")
These textures must be referred to as .dds, not .tga! They are not normal textures; they are 64x64x64 volume maps in D3DFMT_A16B16G16R16F.
I've tried R8G8B8A8 volume maps with no luck (results are the same as using a non-volume texture like black.dds) so I think you're stuck finding or writing a program that can write floating-point volume maps. The format itself is simple since you will probably only be concerned with writing one pixel format, but actually generating a nice-looking map could be another matter.
The point is, you can give your sky wacky colours if you try hard enough!

Changing the clouds is easier, and in combination with the "time" param, could make some really great-looking stormy skies. Or just completely clear skies, if that's what you're into. Remember that you can also change the sky model, use MaterialEx:setTexcoordScaleOffset() on the sky materials, or even make an animated sky model with bones...

Other cool params of varying usefulness:

cloudHighlightColor - Multiplies the color of cloud highlights. Negative values work. This is surely something that should be a vector instead of a number, but we can't pass vectors to setParam(), so your ability to manipulate the cloud colors is limited to multiplying all three rgb values by the same scalar.
Be aware that the default value for this changes depending on the time of day (this is easily visible because a value of 1 behaves the same as the default during the day, but not during sunrise/sunset/night) so be careful.

cloudShadowColor - Like cloudHighlightColor, but for the shadowy parts of clouds instead of the highlighty parts.

mieConstant - Changes the atmospheric colours. I'm not interested in explaining Maxwell's equations, so instead I'll just say that it lets you change the atmospheric colours and you want to use values very close to 0. I think this is another param that is a non-number type by default because setting it to 0 gives very different results than letting the default value take over.

miePhaseConstant - Basically lets you control the size of the atmospheric glow around the sun.

rayleighConstant - Changes the atmospheric colours, "similar" to mieConstant but completely different. Again, values close to 0, not a number type by default.

skyDiffuseScaleBias - hard to explain, sort of lets you "blank out" regions of the sky. Probably useless in practice.

skyOpacity - exactly what it says on the tin. Value ranges from 0 to 1.

skySaturation - Also pretty much what it says on the tin. Works with negative numbers, in case you wanted a red sky.

sunDir - forces the position of the sun to the sunrise position and effectively scales it (sunDir of 1.1 gives you a huge sun). I think this should be a vector or matrix but we can only pass numbers. Probably useless in practice because of this.

sunIntensity - THIS is how you want to scale the sun. Works with negative numbers, and offers an easy way to change the intensity of the sky colours and clouds.

The nice thing about these params is that they reset to their default values every frame, so you need to set them to the custom value on every frame in the onUpdate() hook (which is exactly what the example onUpdate() hook above does) for your changes to be visible.
Why is that nice? It means that if you change one of the params with a non-number default value, like cloudHighlightColor, you can still get the default value back if you simply stop changing it. If it didn't reset automatically, you wouldn't be able to get it back without reloading the dungeon.

Other shaders with material-specific effects

The portal shader only works with materials named "portal" and it appears to only update its displacement buffer if an enabled PortalComponent is in sight OR a disabled PortalComponent that was previously enabled in sight, is still in sight.
Last edited by minmay on Sun Oct 16, 2016 2:24 am, edited 5 times in total.
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.
minmay
Posts: 2789
Joined: Mon Sep 23, 2013 2:24 am

Re: Modding infodump

Post by minmay »

Large Levels

Grimrock 2 supports levels of any size. Sort of.

First of all, to get levels that are not 32x32, you need to edit your dungeon.lua. This is a plaintext file, so don't be afraid to edit it. All you need to do is change the "width" and/or "height" fields in the newMap() call and adjust the sizes of the layers in the loadLayer() calls appropriately. Here is the simplest example of a 64x64 level:

Code: Select all

--- level 3 ---

newMap{
	name = "Big Level",
	width = 64,
	height = 64,
	levelCoord = {0,0,0},
	ambientTrack = "dungeon",
	tiles = {
		"forest_ground",
	}
}

loadLayer("tiles", {
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
})
-- end of level (If you have a reflection map, heightmap, or elevation layer, you obviously need to change those too, or you can
-- delete them to reset all reflection/height/elevation to default)
Before we go further, you should remember that computers do not have infinite speed. The more objects you place on a level, the longer it will take the computer to update all of them. The more triangles, lights, etc. you place on a level, the longer it will take the GPU to render them. So please be wary of making large levels.
If you do choose to make levels of nonstandard sizes, you need to be aware of a few issues, owing to the fact that non-32x32 level sizes aren't really supported.

Automap only works with 32x32 levels
As you know, the automap is divided into a grid. Each square in the grid holds exactly 32x32 automap tiles, no more, no less. These will always be the top left 32x32 squares on a level, and they will always be aligned to the top left. If you have a 16x16 level, then it will appear in the top left 16x16 tiles of the corresponding automap square, and the remaining 3/4ths of the square will be empty.
If you have a 64x64 level, then the top left 32x32 tiles of the level will appear in the automap, and the rest of the tiles will not appear at all; the other grid spaces are reserved for other maps, and the 64x64 level is not allowed to bleed into them. Even the party icon won't be visible, although the automap will still center on it. You could fix this particular issue by maintaining "dummy" levels adjacent to the large level, and updating their automaps as the party moves around.

Map markers are where things get really screwy. From experimentation, it appears that when you hover the mouse over the automap, the game looks at the relative distance of the mouse from the edges of the square in the automap grid. Then it picks the tile that is the closest to being the same relative distance from the edges of the actual level. That tile is the one the mouse is considered to be hovering over, for the purposes of placing and reading map markers - but not for the purposes of displaying map markers.
Now, this works very nicely when the level and the automap square are the same size. But I just told you that the automap square is always 32x32. Can you guess what happens when the level is not 32x32? If I have a 64x64 level, and I click at tile 10,10 on the automap, it thinks I clicked on tile 20,20 on the actual level, so it places the map marker at 20,20, far away from where I actually clicked. For a 16x16 level, it would place it at 5x5.
And since it doesn't do this for *displaying* map markers - they display at their real position, like MapGraphicsComponent and terrain tiles and everything else - it ends up being very visible, and very annoying, to the player.

You can easily change the map_marker object to position itself correctly:

Code: Select all

defineObject{
	name = "map_marker",
	components = {
		{
			class = "MapMarker",
			onInit = function(self)
				local w = self.go.map:getWidth()
				local newx = math.min(w,math.floor(self.go.x*(32/w)))
				local h = self.go.map:getHeight()
				local newy = math.min(h,math.floor(self.go.y*(32/h)))
				self.go:setPosition(newx,newy,self.go.facing,self.go.elevation,self.go.level)
			end,
		}
	},
	placement = "floor",
	editorIcon = 100,
	automapIcon = 144,
}
but this doesn't really help at all, because the player still can't hover the mouse over the marker to read/modify/delete it, because the game thinks they're hovering the mouse over the original wrong position. I cannot think of a reasonable way to fix this.
(It also results in the text box being in the wrong place for a single frame
when the marker is first placed.)

So, barring a very unlikely patch, you might want to consider re-implementing the automap instead, using the user scripting interface. The functionality shouldn't be too challenging ( assets/textures/gui/automap/map_maper.dds is the background and assets/textures/gui/automap/tiles.dds has all the tiles), just drudgerous, and performance will be an issue. I'd suggest the following:
- you don't need to know which actual tile is on a square, because you have Map:getAutomapTile(x,y)
- assets/textures/gui/automap/tiles.dds is a grid of 60x60 icons, but the grid is offset by -15 pixels on both the x and y axes. It's really easy to get any particular image from the game because of GameMode.showImage(), so I'm just going to go ahead and give you a watermarked png of it: http://i.imgur.com/u0C5fwZ.png . It has transparency, so you'll need to open it in your image editor and not just your browser.
- because there are so many ways for automap icons to change, you will probably need use allEntities() every time the map is opened
- oh, and there's no GameObject:getAutomapTile(), so you'll want to make a lookup table for those. Fortunately you can generate it automatically by shadowing defineObject() as covered earlier in this post.
- because of how slow this is (32x32 level is 1024 getAutomapTile() function calls even before the allEntities() iteration), you'll want to recompute the automap only when the map is actually opened, instead of every frame it's drawn, which means caching the map "image". This might be a good place to use the "Store volatile data" trick earlier in the post. Also, you'll probably want to only update that automap "image" for levels that the party has actually visited since the last time they opened the map, so that you don't have to do every level in the whole dungeon.

Q: will you do that for me
A: no, but i might need it myself at some point, if so i'll do it at that point

Level coordinate system assumes all levels are the same size
This is probably so obvious it doesn't need to be mentioned, but stairs, exits, pits, etc. pick automatic targets that don't necessarily make sense when levels have different sizes. Use level coordinates carefully.

Heightmaps do not position themselves correctly if the height of the level is not 32
For example, on a level with a height of 40, the heightmap is positioned 24 meters lower than it should be; you need to increase its z position by 24 meters (because you added 8 to the default height and each square is 3 meters). At first glance, this appears easy to fix:

Code: Select all

defineObject{
	name = "forest_heightmap",
	components = {
		{
			class = "Heightmap",
			onInit = function(self)
				self:setOffset(vec(0,0,(self.go.map:getHeight()-32)*3))
			end,
		}
	},
	placement = "floor",
	editorIcon = 0,
	reflectionMode = "always",
}
(if you want HeightmapComponents on objects that don't face north, modify that onInit hook as appropriate, I'm too lazy)

Unfortunately, while doing this is relatively easy, it does nothing to fix another problem: the heightmap used by clampToGroundPlane particles is offset in the same way, and moving the HeightmapComponent or parent object *doesn't* fix it, even if you do so as soon as it is spawned and before the heightmap mesh is actually generated.

The heightmap for walking around, projectile collision, etc. seems to be fine (and isn't even affected by changing the offset of the HeightmapComponent or the position of its parent object, or its transformation matrix).

(Large levels only) Noise layer is hugely amplified at y positions past 31
If you're using noise on your heightmapped levels (as you should), you'll find that it produces extremely jagged terrain (spikes several meters tall) at y positions greater than 31. x positions greater than 31 seem to be fine. You have two options to avoid this:
EASY OPTION THAT DOESN'T LET YOU USE NOISE:
Just leave your noise layer at 0 for all tiles with a y position greater than 30. (Noise affects the adjacent squares as well as the square it's on, so noise at y position 31 will still produce nasty spikes on the terrain at y position 32.)

LESS EASY OPTION THAT DOES LET YOU USE NOISE:
2. Copy your large level's heightmap, tiles and noise layers, then split those copies into segments that are 32 tiles tall (at most), then put those copies in additional "dummy" levels that are 32 tiles tall (at most), have a HeightmapComponent create a heightmap there, then move the HeightmapComponents to the large level and set their world positions so that they tile together to make the full-size heightmap you originally wanted, with correct noise. This needs to be repeated every time the player loads the game, because heightmap meshes are re-generated every time the player loads the game. The world position of the parent object is also reset to 0 every time, but the offset of the HeightmapComponent, if any, is not. Furthermore, when you create a new HeightmapComponent, the heightmap mesh for it will not be generated until the level is (re-)entered or the game is loaded.
So you need to:
1. make said dummy levels and place said heightmaps
2. teleport the party to all dummy levels (you can teleport them back to their original location on the same frame, so they don't need to see the dummy levels, and if you've been reading carefully, you'll already note a way to hide the dummy levels from the automap) to generate the heightmap meshes
3. move the heightmap objects to their correct positions to compose the large heightmap mesh you wanted
4. every time the game is loaded (if you don't know how to detect when the game is reloaded, read the beginning of this post), destroy the small heightmaps, create new ones on the dummy levels, and repeat steps 2 and 3
At least, that's a rough estimate of what you need to do (I haven't extensively tested it myself yet).

ExitComponent assumes all levels are 32x32
Using an ExitComponent facing west always puts you at x position 31 on the next level, and using an ExitComponent facing north always puts you at y position 31 on the next level. For levels bigger than 32x32, this is no big deal, since you can just hack around it with a teleporter. It's worse for levels smaller than 32x32, since x 31 or y 31 is an invalid map position in that case - you might prefer to make your own ExitComponent implementation for those.

(Large levels only) Editor floodfill implementation is questionable
If you use floodfill on a sufficiently large area, you will get a stack overflow and crash to desktop. This doesn't happen with 32x32 levels because it's impossible to make a large enough area without increasing the level size. The workaround is to be careful with floodfill, and only use it on "small" areas in large levels.

(Large levels only?) Fog limitation
If you're using linear_lit fog, the 128 clip distance will be a serious problem for large levels. See the SkyComponent section above and consider using "linear" fog instead.

Code from other parties may assume 32x32 maps, or square maps
Again, this is probably too obvious to bear mentioning, but if you're using stuff you downloaded from Nexus or whatever without actually looking at the code, it's quite possible that it assumes default sized maps (e.g. custom spells that assume 31 is the highest legal x or y position). So if you're using third-party stuff, at a bare minimum you should grep for "31" and "32".
Note: I can vouch that the Grimrock 2 Dungeon Master Resource and MinAssets both work correctly with all map sizes, because I wrote every line of code in those, and I was aware from the start that Grimrock 2 supports arbitrary map sizes.






That's all for now, I split it across 3 posts because I don't know how much I'm going to add to this (60,000 character limit and there are currently about 74k total between the three posts)
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
Drakkan
Posts: 1318
Joined: Mon Dec 31, 2012 12:25 am

Re: Modding infodump

Post by Drakkan »

Altough I do not understand majority of this, I can only bow before your skills and honor this legacy for future modders. thanks.
Breath from the unpromising waters.
Eye of the Atlantis
User avatar
AndakRainor
Posts: 674
Joined: Thu Nov 20, 2014 5:18 pm

Re: Modding infodump

Post by AndakRainor »

Very nice! Have you considered adding some of the precious information you know to https://github.com/JKos/log2doc/wiki ?

I understand it would be a huge work, but I think the weakness of both the scripting reference and the wiki is that they only show some functions profiles and generally no detail at all about valid parameters, important non-intuitive behaviors of the function, etc...

Hopefully we also have the asset pack to get more, and experiment for the rest, but it could decrease recurring questions people have in this forum.

For example, I had to use SkyComponent:getFogMode() to learn that "linear_lit" was the name of the default fog mode, I only found "dense" for other skies in the asset pack. Not sure what I missed but I wonder where you got "linear" and "exp" from...

Important: On a map vs. not on the map

[...]

- 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.
Do you mean that those items are NOT garbage collected during the game session? If so, excuse me I have to puke, and I will definitively have to review large portions of my code. I use a lot of items I don't keep anywhere, but use only as local variables to read some of their default properties, even every frame for some of them... And when I need to destroy a "real" item in the game I always check its map component before calling a destroy function. If a removeItemFromSlot function for example is not enough to free the memory for this item then I have lots of memory leaks.

Inventories, surfaces, and sockets

[...]

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.
I use an object in my mod to store champions items, used to recruit/remove champions from the party. I don't understand what you are saying about the problems of option 2. When my "inventory" object is asked to remove an item it contains, I destroy and recreate its SurfaceComponent, then insert back all items it still contains.

Sky

Shamefully, I haven't seen a single mod yet that did anything significant with the sky shader, despite the range of possibilities you have - you can give the sky arbitrary clouds, change the speed at which they rotate, change the colours, etc.
Don't know if it is meaningful or not, but I made a memory puzzle with a switch that changes tonemapSaturation from 1 to -1 and shows / hides half the magic bridges of the map.

Also, something horrible I discovered when experimenting with particle systems to simulate clouds below the party for a map with no ground; particles are z-ordered "among them" for rendering by the position of their respective GameObject. So for example, with my very large zone filled with clouds particles, an air elemental will incorrectly appear behind the horizon clouds if the object emitting the clouds is behind the party.

May be I will need a sky with a custom texture for the bottom half of the sky's sphere model at some point if I want to improve that part of my mod.
Gabula
Posts: 4
Joined: Thu Mar 17, 2016 8:01 pm

Re: Modding infodump

Post by Gabula »

Wow, awesome thread dude. Massive thanks for sharing your wisdom with all of us. :!:
Echoplex
Posts: 63
Joined: Tue Nov 04, 2014 10:59 pm

Re: Modding infodump

Post by Echoplex »

I have seen people place more than one WaterSurfaceComponent on one level. Don't do that. It doesn't work properly. There should be only one WaterSurfaceComponent on a level at a time. Removing one WaterSurfaceComponent and replacing it with another one should be fine, but I haven't tested it very well.
Interesting you brought this up. I'm running into a similar issue where I have a water entity for a lake that's on the same map as an ocean entity. I've only observed visual issues so far and in testing but am brainstorming a a way to overcome this. Since I have a tunnel leading into another area on the map that separates the lake and ocean im thinking of having triggers that checks for the water entities or beach entities and removes/respawns them depending on the direction traveled through the tunnel so when the player is on the original map, there's only 1 water entity at any given time. I haven't tried this or understand if this will cause any issues but its the only idea on the napkin at this time. Anyway thanks for the information dump.
User avatar
THOM
Posts: 1280
Joined: Wed Nov 20, 2013 11:35 pm
Location: Germany - Cologne
Contact:

Re: Modding infodump

Post by THOM »

What Kind of issues do you get by using more than one waterobject?

Therfore they are not big enough to cover the whole map I have to use serveral objects on one Level.
THOM formaly known as tschrage
_______________________________________________
My MOD (LoG1): Castle Ringfort Thread
My MOD (LoG2): Journey To Justice Thread | Download
Echoplex
Posts: 63
Joined: Tue Nov 04, 2014 10:59 pm

Re: Modding infodump

Post by Echoplex »

So far I've seen only visual issues. Which ever entity is placed first seems to take precedence. For example if you place an ocean entity first then a water surface, you will still see your water surface however when you jump in, the water will be crystal clear as the surface is no longer applying underwater effects. Alternatively if you place the water surface first then the ocean, the water works fine however when you enter a beach water spot, the water will change colors to match the water surface.
minmay
Posts: 2789
Joined: Mon Sep 23, 2013 2:24 am

Re: Modding infodump

Post by minmay »

I figured the reason to avoid multiple WaterSurfaceComponents would be obvious after the rest of the stuff I said about water, so I didn't mention it... only one reflection buffer exists (if there were several, how would the game decide which one to use?), so only one reflectionColor/refractionColor/planeY can take effect at a time, and obviously only one underwater fog density/underwater fog color can take effect at a time. If you place multiple WaterSurfaceComponents on the same level, the best-case scenario is that the game chooses one and ignores the other one; the worse scenario is that it switches between the two reflection buffers/colors/fog/etc. in some way you can't control, and/or it hurts performance. So placing a second (or third, or fourth, etc.) WaterSurfaceComponent on a level does absolutely nothing at best.
THOM wrote:What Kind of issues do you get by using more than one waterobject?

Therfore they are not big enough to cover the whole map I have to use serveral objects on one Level.
Huh? That doesn't require placing multiple WaterSurfaceComponents. Placing multiple WaterSurfaceComponents wouldn't even be useful for that. What's wrong with using multiple models and a single WaterSurfaceComponent? Or, in your case, just changing the model you're using, since apparently your model isn't the shape you actually wanted.

None of this has anything to do with using several water materials at once. Using all three materials at once should be fine.
Echoplex wrote:So far I've seen only visual issues.
Of course they're "only" visual issues. WaterSurfaceComponent has exclusively visual effects! It doesn't affect game logic at all.
AndakRainor wrote:Very nice! Have you considered adding some of the precious information you know to https://github.com/JKos/log2doc/wiki ?
I've been adding function details and such already. I'm too lazy to rewrite the sky tutorial etc. to be readable enough to belong on the wiki; if someone else wants to, that would be great. Sorry...
AndakRainor wrote:Do you mean that those items are NOT garbage collected during the game session? If so, excuse me I have to puke, and I will definitively have to review large portions of my code. I use a lot of items I don't keep anywhere, but use only as local variables to read some of their default properties, even every frame for some of them... And when I need to destroy a "real" item in the game I always check its map component before calling a destroy function. If a removeItemFromSlot function for example is not enough to free the memory for this item then I have lots of memory leaks.
It's hard to test this since Grimrock doesn't offer a way to force garbage collection that I know of, but from what testing I have done, the answer appears to be no, they don't get garbage collected. They will disappear when the game is saved and reloaded, of course.
If you need an object every frame, I'd suggest spawning it once and putting it on a map or in a ContainerItemComponent, instead of spawning and destroying it every frame - you don't save any memory by doing that, and the impact on saving performance by one object (or ten) is vanishingly small.
AndakRainor wrote:Also, something horrible I discovered when experimenting with particle systems to simulate clouds below the party for a map with no ground; particles are z-ordered "among them" for rendering by the position of their respective GameObject. So for example, with my very large zone filled with clouds particles, an air elemental will incorrectly appear behind the horizon clouds if the object emitting the clouds is behind the party.
I can't reproduce this. For me, that exact situation works perfectly. Are you sure you aren't doing something weird with sortOffset and/or depth bias? For that matter, you may be able to use sortOffset to fix it (I'm not sure exactly how it works w/r/t particles, it might only be relevant for triangles/particles that are coplanar and intersecting...).
AndakRainor wrote:I use an object in my mod to store champions items, used to recruit/remove champions from the party. I don't understand what you are saying about the problems of option 2. When my "inventory" object is asked to remove an item it contains, I destroy and recreate its SurfaceComponent, then insert back all items it still contains.
It's the same problem with destroying the ItemComponent: the re-created SurfaceComponent won't have any hooks. Of course it will be fine if you don't use hooks.
It sounds like ContainerItemComponent would work better for this since you wouldn't have to add the object to the map at all in the process.
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