User-side implementations for MOST builtin spells

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

User-side implementations for MOST builtin spells

Post by minmay »

Unimplemented:
- Light, Darkness (I don't plan on doing these)
- Poison cloud (I can't find a way to cleanly merge CloudSpellComponents like the builtin spell does)
- Force Field (should be easy to implement, just haven't bothered)
- Meteor storm (needs a clean way to detect when the party is underwater, also I don't know quite how its spread works yet)

All of these should have identical effects to the builtin spells except where otherwise noted (fireburst, cause fear, heal).

Code: Select all

-- Builtin burst spell rules:
-- Try to target the square in front of the party.
-- If this square is solid, or is blocked by an ObstacleComponent that would
-- block the party and its parent GameObject does NOT have a component named
-- "health", target the party's square instead.
-- If the path between the party and this square is blocked by a ForceFieldComponent
-- or DoorComponent, target the party's square instead. 
--
-- This function emulates this behaviour.
--
-- NOTE: The builtin poison_cloud spell is a little different: it can be cast
-- through sparse doors (but not non-sparse ones). This behaviour in the
-- original game is intended, and easy to replicate, but since it leads to
-- abuses I don't think anyone actually wants it, so I haven't implemented it.
function getBurstSpellTarget(x,y,direction,elevation)
	local dx,dy = getForward(direction)
	local nx = x+dx
	local ny = y+dy
	local obs = party.map:checkObstacle(party,direction)
	if obs == "door" or obs == "wall" then
		return party.level,x,y,direction,elevation
	elseif obs == "obstacle" then
		for e in party.map:entitiesAt(party.x,party.y) do
			if e.elevation == party.elevation then
				for _,c in e:componentIterator() do
					if c:isEnabled() and c:getClass() == "ForceFieldComponent" then
						return party.level,x,y,direction,elevation
					end
				end
			end
		end
		for e in party.map:entitiesAt(nx,ny) do
			if e.elevation == party.elevation and not e.health then
				for _,c in e:componentIterator() do
					if c:isEnabled() then
						local cls = c:getClass()
						if cls == "ForceFieldComponent" or cls == "ObstacleComponent" and c:getBlockParty() then
							return party.level,x,y,direction,elevation
						end
					end
				end
			end
		end
	end
	return party.level,nx,ny,direction,elevation
end

function doBurstSpell(champion,x,y,direction,elevation,skill,burst,basePower,noSkillMult)
	local spl = spawn(burst,getBurstSpellTarget(x,y,direction,elevation))
	local ord = champion:getOrdinal()
	spl.tiledamager:setCastByChampion(ord)
	spl.tiledamager:setAttackPower(basePower*(noSkillMult and 1 or 1+skill*0.2))
	endInvisibility()
	return spl
end

function getProjectileSpellLaunchPosition(champion,x,y,direction,elevation)
	local ord = champion:getOrdinal()
	local left = nil
	for i = 1,4 do
		if party.party:getChampion(i):getOrdinal() == ord then
			left = i == 1 or i == 3
			break
		end
	end
	local wpos = party:getWorldPosition()
	local dx = nil
	local dz = nil
	if party.facing == 0 then
		dx = left and -0.1 or 0.1
		dz = -1
	elseif party.facing == 1 then
		dz = left and 0.1 or -0.1
		dx = -1
	elseif party.facing == 2 then
		dx = left and 0.1 or -0.1
		dz = 1
	else -- party.facing == 3
		dz = left and -0.1 or 0.1
		dx = 1
	end
	return party:getWorldPosition()+vec(dx,1.35,dz)
end

function doProjectileSpell(champion,x,y,direction,elevation,skill,projectile,basePower,noSkillMult)
	local spl = spawn(projectile,party.level,party.x,party.y,party.facing,party.elevation)
	spl.projectile:setIgnoreEntity(party)
	local ord = champion:getOrdinal()
	spl.projectile:setCastByChampion(ord)
	spl.projectile:setAttackPower(basePower*(noSkillMult and 1 or 1+skill*0.2))
	spl:setWorldPosition(getProjectileSpellLaunchPosition(champion,x,y,direction,elevation))
	endInvisibility()
	return spl
end

function endInvisibility()
	for i=1,4 do
		party.party:getChampion(i):removeCondition("invisibility")
	end
end

function doElementalShield(shield)
	for i=1,4 do
		party.party:getChampion(i):removeCondition("fire_shield")
		party.party:getChampion(i):removeCondition("frost_shield")
		party.party:getChampion(i):removeCondition("poison_shield")
		party.party:getChampion(i):removeCondition("shock_shield")
		party.party:getChampion(i):setCondition(shield)
	end
	playSound("generic_spell")
end

function hasShamanStaff(champion)
	return champion:getItem(ItemSlot.Weapon) and champion:getItem(ItemSlot.Weapon).go.name == "shaman_staff" or
		champion:getItem(ItemSlot.OffHand) and champion:getItem(ItemSlot.OffHand).go.name == "shaman_staff"
end

function castShield(champion,x,y,direction,elevation,skill)
	playSound("generic_spell")
	local dur = 30+skill*10
	champion:setConditionValue("protective_shield",30+skill*10)
end

function castDarkbolt(champion,x,y,direction,elevation,skill)
	doProjectileSpell(champion,x,y,direction,elevation,skill,"dark_bolt",10,true)
end

function castFireburst(champion,x,y,direction,elevation,skill)
	local spl = doBurstSpell(champion,x,y,direction,elevation,skill,"fireburst",10,true)
	spl.tiledamager:setDamageFlags(DamageFlags.NoLingeringEffects)
	if skill >= 3 and math.random() < 0.4 then
		for e in party.map:entitiesAt(spl.x,spl.y) do
			if e.elevation == party.elevation then
				for _,c in e:componentIterator() do
					-- NOTE: original spell doesn't have the isAlive() check,
					-- I added it to fix the bug where dead monsters get burning
					-- particles
					if c:getClass() == "MonsterComponent" and c:isAlive() then
						c:setCondition("burning")
						if e.burning then
							e.burning:setCausedByChampion(champion:getOrdinal())
							-- A second MonsterComponent wouldn't matter
							break
						end
					end
				end
			end
		end
	end
end

function castFireball(champion,x,y,direction,elevation,skill)
	doProjectileSpell(champion,x,y,direction,elevation,skill,skill < 2 and "fireball_small" or skill < 3 and "fireball_medium" or "fireball_large",30)
end

function castFireShield(champion,x,y,direction,elevation,skill)
	doElementalShield("fire_shield")
end

function castIceShards(champion,x,y,direction,elevation,skill)
	local spl = doBurstSpell(champion,x,y,direction,elevation,skill,"ice_shards",18)
	spl.iceshards:setRange(2+skill)
	if not spl.tiledamager:isEnabled() then
		playSound("spell_fizzle")
	end
end

function castDispel(champion,x,y,direction,elevation,skill)
	doProjectileSpell(champion,x,y,direction,elevation,skill,"dispel_projectile",25)
end

function castFrostbolt(champion,x,y,direction,elevation,skill)
	doProjectileSpell(champion,x,y,direction,elevation,skill,"frostbolt_"..math.clamp(skill,1,5),15)
end

function castFrostShield(champion,x,y,direction,elevation,skill)
	doElementalShield("frost_shield")
end

function castShock(champion,x,y,direction,elevation,skill)
	doBurstSpell(champion,x,y,direction,elevation,skill,"shockburst",22).tiledamager:setDamageFlags(DamageFlags.NoLingeringEffects)
end

function castInvisibility(champion,x,y,direction,elevation,skill)
	for i=1,4 do party.party:getChampion(i):setCondition("invisibility") end
	playSound("generic_spell")
end

function castLightningBolt(champion,x,y,direction,elevation,skill)
	doProjectileSpell(champion,x,y,direction,elevation,skill,skill > 1 and "lightning_bolt_greater" or "lightning_bolt",30)
end

function castShockShield(champion,x,y,direction,elevation,skill)
	doElementalShield("shock_shield")
end

function castPoisonBolt(champion,x,y,direction,elevation,skill)
	-- Yes, Shaman Staff turns 2 skill poison_bolt into poison_bolt_greater. This is true in the vanilla game too!
	-- Try it!
	if hasShamanStaff(champion) then
		skill = skill+1
	end
	doProjectileSpell(champion,x,y,direction,elevation,skill,skill >= 3 and "poison_bolt_greater" or "poison_bolt",15)
end

function castPoisonShield(champion,x,y,direction,elevation,skill)
	doElementalShield("poison_shield")
end

function castOpenDoor(champion,x,y,direction,elevation,skill)
	-- attack power doesn't matter
	doProjectileSpell(champion,x,y,direction,elevation,skill,"open_door",1,true)
end

function castDisintegrate(champion,x,y,direction,elevation,skill)
	local nl,nx,ny,nf,ne = getBurstSpellTarget(x,y,direction,elevation)
	for e in party.map:entitiesAt(nx,ny) do
		if e.elevation == party.elevation then
			for _,c in e:componentIterator() do
				if c:getClass() == "MonsterComponent" and c:isAlive() then
					c:die()
				end
			end
		end
	end
	spawn("wizard_explosion",nl,nx,ny,nf,ne)
	playSound("force_field_cast")
	endInvisibility()
end

-- NOTE: builtin cause_fear will not attempt to affect monsters that are already
-- fleeing. The user scripting interface does not have access to whether a
-- monster is fleeing, so we can't do that here.
function castCauseFear(champion,x,y,direction,elevation,skill)
	local nl,nx,ny,nf,ne = getBurstSpellTarget(x,y,direction,elevation)
	for e in party.map:entitiesAt(nx,ny) do
		if e.elevation == party.elevation then
			for _,c in e:componentIterator() do
				if c:getClass() == "MonsterComponent" and c:isAlive() and c.go.brain then
					if math.random(0,100) > c.go.brain:getMorale() then
						c.go.brain:startFleeing()
					else
						c:showDamageText("Resists","0xFFFFFF")
					end
					break
				end
			end
		end
	end
	if party.x == nx and party.y == ny then
		for i=1,4 do
			local champ = party.party:getChampion(i)
			if champ:isAlive() and (not champ:hasCondition("paralyzed")) and math.random() < 0.5 then
				champ:setCondition("paralyzed")
			end
		end
	end
	local p = spawn("particle_system",nl,nx,ny,nf,ne)
	p.particle:setParticleSystem("fear_cloud")
	p.particle:setOffset(vec(0,1.5,0))
	playSound("wand_fear")
	endInvisibility()
end

function castHeal(champion,x,y,direction,elevation,skill)
	for i=1,4 do
		local champ = party.party:getChampion(i)
		if champ:isAlive() then
			champ:regainHealth(100)
			champ:playHealingIndicator()
		end
	end
	-- NOTE: builtin spell seems to have this inside the for loop, because
	-- the sound plays 4 times if the party has 4 alive champions, which
	-- is probably a bug.
	playSound("heal_party")
end
How to use: put the above in a ScriptComponent then call it from spell definitions. Example:

Code: Select all

defineObject{
	name = "party",
	baseObject = "party",
	components = {
		{
			class = "Party",
			onInit = function(self)
				self.go.spells:loadFile("mod_assets/spells_implementation.lua")
			end,
		},
		{
			class = "Script",
			name = "spells",
		},
	},
}

defineSpell{
	name = "fireball",
	uiName = "Fireball",
	gesture = 1236,
	manaCost = 43,
	skill = "fire_magic",
	requirements = { "fire_magic", 3, "air_magic", 1 },
	icon = 61,
	spellIcon = 7,
	description = "A flaming ball of fire shoots from your fingertips causing devastating damage to your foes.",
	onCast = function(champion,x,y,direction,elevation,skill)
		party.spells.castFireball(champion,x,y,direction,elevation,skill)
	end,
}
Why this is useful: you might want the effect of casting a builtin spell without actually making a champion cast the spell (which costs energy and food, starts cooldown, has a skill requirement, etc.), or you might want to do something very similar to a builtin spell.
If you don't want to do either of those then you should ignore this thread.
Last edited by minmay on Fri Dec 02, 2016 2:25 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: User-side implementations for MOST builtin spells

Post by Isaac »

minmay wrote: Meteor storm (needs a clean way to detect when the party is underwater, also I don't know quite how its spread works yet)
Is there a clean way?

We should definitely suggest a PartyComponent:isUnderWater() method, at the next Gløgg session. ;)

So far ~as you've likely also thought of, I've checked (with reasonable success) by comparing the party.level and checking the automap for a water tile. Of course anyone could break that intentionally, but barring that, it usually works just fine.

**You know, we could make the assumption that there will someday be a built-in PartyComponent:isUnderWater(); and for the time being simply add our own (not entirely clean) version... But such that everything else written would use the boolean return value, and be none the wiser if it was a clean official method, or a user defined hack.
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: User-side implementations for MOST builtin spells

Post by minmay »

Isaac wrote:
minmay wrote: Meteor storm (needs a clean way to detect when the party is underwater, also I don't know quite how its spread works yet)
Is there a clean way?
not that I know of
Isaac wrote:So far ~as you've likely also thought of, I've checked (with reasonable success) by comparing the party.level and checking the automap for a water tile. Of course anyone could break that intentionally, but barring that, it usually works just fine.
not really, the water automap tile is also used for e.g. beach_ground_water tiles which are not underwater, and by checking by automap tile you are placing a big restriction on where you can use underwater tiles
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: User-side implementations for MOST builtin spells

Post by Isaac »

minmay wrote:not really, the water automap tile is also used for e.g. beach_ground_water tiles which are not underwater, and by checking by automap tile you are placing a big restriction on where you can use underwater tiles
Beach ground, where presumably when standing above water, the party.level is not often below zero.

As for the big restrictions, I don't doubt that you see them, or that they are valid points, or that you can illustrate several of them, but I've not noticeably experienced these restrictions ~yet; (while making underwater levels)... so that seems that it might be somewhat of a niche case to most map makers.
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: User-side implementations for MOST builtin spells

Post by minmay »

...I'll just assume you mean party:getWorldPositionY() instead of party.level. And it will be below 0 on any elevation 0 square where the heightmap is below 0. Not that that's relevant, since the party becomes underwater at y position -0.6, not 0.
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: User-side implementations for MOST builtin spells

Post by Isaac »

minmay wrote:...I'll just assume you mean party:getWorldPositionY() instead of party.level.
Indeed it was a mistake; I actually meant party.elevation... I don't think that it ever occurred to me that party:getWorldPosition() would (of course) have existed. Now I'll have to check that out, and a few more related methods. 8-)

I have a script that currently checks for the automap tile, and elevation, and (so far) consistently detects the party being underwater, and correctly detects the top of a ladder ~on a water tile as not being underwater (when it isn't, and not when it is 8-) ).

https://www.dropbox.com/s/e15vl63igde4b ... m.avi?dl=0 ~this clip doesn't show the ladder.
Last edited by Isaac on Fri Jul 08, 2016 6:00 am, edited 2 times in total.
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: User-side implementations for MOST builtin spells

Post by minmay »

Elevation is completely uninformative for determining whether something is underwater. For example on a heightmapped level you can be walking below water at elevation 0, and when the party falls from a height, their elevation is not updated until they actually hit the ground, so they could potentially be underwater even at elevation 7.
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: User-side implementations for MOST builtin spells

Post by Isaac »

minmay wrote:Elevation is completely uninformative for determining whether something is underwater. For example on a heightmapped level you can be walking below water at elevation 0, and when the party falls from a height, their elevation is not updated until they actually hit the ground, so they could potentially be underwater even at elevation 7.
Heightmaps underwater are buggy as hell to begin with; they even screw with the wave height. I understand what you mean by 'a clean way' (being greatly preferred), but the "un-clean" ways can work so long as the designer knows their limitations. I am going to try making a standard underwater check as a party component method, and see if that works out well... And if it turns out that Petri ever implements a party.party:isUnderWater(), then I won't have to change anything in my scripts. 8-)
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: User-side implementations for MOST builtin spells

Post by minmay »

Fixed getBurstSpellTarget() for force fields.
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