How to detect difficulty

Ask for help about creating mods and scripts for Grimrock 2 or share your tips, scripts, tools and assets with other modders here. Warning: forum contains spoilers!
Post Reply
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

How to detect difficulty

Post by minmay »

Difficulty level effects
Normal is the baseline.

Easy divides monster attack cooldowns by 0.8, multiplies damage dealt by MonsterAttackComponent by 0.6, subtracts 10 from the accuracy of monster attacks, and divides MonsterMoveComponent cooldowns by 0.5.
I don't think it affects MonsterTurnComponent cooldown.

Hard divides monster attack cooldowns by 1.6, multiplies damage dealt by MonsterAttackComponent by 1.4, adds 15 to monster accuracy, and divides move cooldowns by 2.
Additionally, when playing on hard, monster movement and attack cooldowns appear to have a 40% chance of being halved a second time for a total reduction of 75%.
Note that MonsterComponent:shootProjectile() and MonsterComponent:throwItem() don't seem to be affected, nor are effects like the ice guardian's ice shards since they are not part of the actual MonsterAttackComponent effect.

Detecting difficulty level
You cannot determine the difficulty by inspecting monster or projectile cooldowns, accuracy, or attack power, as these multipliers are not reflected in the getter functions; they happen when the monster hits the party. This is the simplest method I have found:

Code: Select all

defineObject{
	name = "difficulty_detect_projectile",
	placement = "floor",
	components = {
		{
			class = "Projectile",
			radius = 0.1,
			hitEffect = "difficulty_detect_blast",
			velocity = 1,
		},
	},
}
defineObject{
	name = "difficulty_detect_blast",
	placement = "floor",
	components = {
		{
			class = "TileDamager",
			attackPower = 10,
			damageType = "pure",
			onHitChampion = function(self,champion)
				local pow = self:getAttackPower()
				if pow == 6 then
					party.stuff.setDifficulty("easy")
				elseif pow == 14 then
					party.stuff.setDifficulty("hard")
				else
					party.stuff.setDifficulty("normal")
				end
				return false
			end,
			onHitMonster = function() return false end,
			onHitObstacle = function() return false end,
			destroyObject = true,
		},
	},
}
defineObject{
	name = "party",
	baseObject = "party",
	components = {
		{
			class = "Script",
			name = "stuff",
			source = [[difficulty = "unknown"

function setDifficulty(d)
	difficulty = d
end
function getDifficulty()
	return difficulty
end
function detectDifficulty()
	local m = party:spawn("difficulty_detector")
	m.monster:performAction("basicAttack")
end
detectDifficulty()]],
		},
	},
}
defineObject{
	name = "difficulty_detector",
	baseObject = "base_monster",
	components = {
		{
			class = "Model",
			model = "assets/models/monsters/snail.fbx", -- simplest existing valid monster model (would be easy to make a new one though)
			storeSourceData = true,
			enabled = false,
		},
		{
			class = "Animation",
			animations = {idle = "assets/animations/monsters/snail/snail_idle.fbx",
			attack = "mod_assets/difficulty_detect_attack.fbx"},
			currentLevelOnly = true,
		},
		{
			class = "Monster",
			meshName = "snail_mesh",
			hitSound = "snail_hit",
			dieSound = "snail_die",
			hitEffect = "hit_goo",
			health = 100,
			capsuleHeight = 0,
			capsuleRadius = 0,
			collisionRadius = 0,
			exp = 0,
		},
		{
			class = "MonsterAttack",
			name = "basicAttack",
			attackType = "projectile",
			shootProjectile = "difficulty_detect_projectile",
			projectileHeight = 1,
			attackPower = 10,
			cooldown = 0,
			animation = "attack",
			onAttack = function(self)
				self.go:destroyDelayed()
			end,
		},
	}
}
defineAnimationEvent{
	animation = "mod_assets/difficulty_detect_attack.fbx",
	event = "attack",
	normalizedTime = 0,
}
Download difficulty_detect_attack.animation here. This is just a single-frame "animation". It's used because we don't want to add an "attack" event to an animation that is used elsewhere. Nothing about the animation itself really matters (you can probably even use a "null" animation and get away with it).
Explanation: As soon as party.stuff's source runs (which is right after the dungeon begins), a monster is spawned on the party's square and shoots a projectile with MonsterAttackComponent. This projectile will collide with the party on the next frame, and it has a hitEffect with a TileDamagerComponent. At the instant the TileDamagerComponent hits the party, its attackPower reflects the difficulty's damage multiplier. This is inspected and used to determine the difficulty.
Then you can call party.stuff.getDifficulty() to get the difficulty level. This is useful for scaling custom monster attacks that are made without MonsterAttackComponent, such as ice guardian ice shards. Or you could even make brains that get more intelligent on higher difficulties. Or change puzzles depending on difficulty level? Just remember that there is a 2 frame delay before you can be assured the difficulty has been determined, because of the projectile.
The monster and projectile are completely invisible to the player; the monster exists for less than a frame.

You might be able to get rid of that delay by using melee MonsterAttackComponents and PartyComponent.onDamage (remember to set champions' protection to 0 first), doing some number of simultaneous attacks and determining the difficulty based on the lowest damage done (less than half the attack power is Easy, greater than that but less than 0.7 times the attack power is Normal, greater than that is Hard). The problem with this is that you need many trials to make it reliable, which could introduce a real-time delay, unless perhaps you can use math.randomseed() to get a consistent result?

Note: I am fully aware that you can change the difficulty level after starting the game, either by in-game cheating or just editing your save file. You can run detectDifficulty() again periodically to keep the difficulty up to date with any cheating the player does, if this possibility bothers you.

Please don't abuse this to make dungeons that can only be played on Hard, or other stupid things like that. I'm sharing this so you can scale custom monsters correctly.
Last edited by minmay on Fri Jul 08, 2016 8:52 am, edited 1 time 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.
User avatar
Isaac
Posts: 3185
Joined: Fri Mar 02, 2012 10:02 pm

Re: How to detect difficulty

Post by Isaac »

8-)
So is this in the ORRR3?
If not, can it be (if need)?
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: How to detect difficulty

Post by minmay »

Isaac wrote:So is this in the ORRR3?
No.
Isaac wrote:If not, can it be (if need)?
I don't see why not.
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