Page 1 of 1

[SCRIPT] The Arrow solution

Posted: Sat Dec 01, 2012 10:19 am
by Diarmuid
Hey there,

This one was a bit tricky, but here it goes: fix for custom projectiles & ammo problems! 8-)

Latest Version: 1.02 (Implemented grimQ, fixed a rare bug, cleaned up code)

Using essentially the same scripting model as my goromorg shield recreation, this script:
- registers custom arrows/quarrels on impact in a table
- respawns them on monster death, cleaning up wrongly reverted ones
- prevents you from accidentaly/on purpose enchanting custom projectiles

So now you can create custom arrows, that have "arrow" ammoType, are compatible with normal bows, and don't revert.

Note that JKos's LoG framework is required for this, as it involves an extended timer and adding hooks to all monsters at once. Version 1.02 also implements Xanathar's grimQ library to optimize query functions and code.

Here's the script that has to go in a script entity named arrowTracker:
SpoilerShow

Code: Select all

-- Diarmuid's arrowTracker script, Version 1.02
-- JKos's Log Framework and Xanathar's grimQ modules required
--
-- Changelog 
-- 1.02 Implemented grimQ methods, fixed a rare bug, cleaned up code
-- 1.01 Fixed save game serialization crash
-- 1.0 Initial release

arrowsTable = {}
aClear = {}
standardAssets = {"arrow","cold_arrow","fire_arrow","poison_arrow",
			"shock_arrow", "quarrel", "cold_quarrel", "fire_quarrel", 
			"poison_quarrel","shock_quarrel"}


function register(monster, projectile)

	-- Check if projectile is an arrow/quarrel. Custom arrows/quarrels must
	--   have "arrow" or "quarrel" in the item name (uiName can be anything 
	--   of course). If not a missile weapon, let it through:
	
	if string.find(projectile.name,"arrow") == nil and string.find(projectile.name,"quarrel") == nil then
		return true
	end

	-- Check if it's a standard asset arrow/quarrel and let it through
	--   so that it can revert:
	
	if grimq.fromArray(standardAssets)
				:any(function(v) return v==projectile.name; end) == true then
		return true
	end

	-- Now that we know that the projectile is a custom projectile,
	--   store it in the table and destroy it:
	
	if arrowsTable[monster.id] == nil then
		arrowsTable[monster.id] = {}
	end
	table.insert(arrowsTable[monster.id],projectile.name)
	return true
end

function clear(monster)

	if aClear[monster.id] == nil then
		aClear[monster.id] = {}
	end
	
	table.insert(aClear[monster.id],monster.level)
	table.insert(aClear[monster.id],monster.x)
	table.insert(aClear[monster.id],monster.y)
	table.insert(aClear[monster.id],monster.facing)
	
	-- Set respawn of stored projectiles 0.1s after monster death:
	
	local clearTimer = timers:create(monster.id..'_clearTimer')
	clearTimer:setTimerInterval(0.1)
	clearTimer:addCallback(
		function(self,monsterId)
			if arrowTracker.arrowsTable[monsterId]  ~= nil then
			
				-- get stored monsters
				
				local monsterLevel = arrowTracker.aClear[monsterId][1]
				local monsterX = arrowTracker.aClear[monsterId][2]
				local monsterY = arrowTracker.aClear[monsterId][3]
				local monsterFacing = arrowTracker.aClear[monsterId][4]
					
				-- fix any custom reverted projectiles to normal models:
				
				local droppedItems = grimq.fromEntitiesAround(monsterLevel,monsterX,monsterY, 2, true):toArray()
				local arrowDestroyed
				
				for i,v in ipairs(droppedItems) do			
					arrowDestroyed = false
					if v.name == "arrow" then
						spawn("arrow",monsterLevel,monsterX,monsterY,monsterFacing)
						arrowDestroyed = true
						v:destroy()
					end
					if arrowDestroyed == false and v.name == "quarrel" then
						spawn("quarrel",monsterLevel,monsterX,monsterY,monsterFacing)
						v:destroy()
					end
				end			
					
				-- spawn stored custom projectiles and destroys equivalent number of reverted ones:
				
				for k,v in ipairs(arrowTracker.arrowsTable[monsterId]) do
					spawn(v,monsterLevel,monsterX,monsterY,monsterFacing)
					arrowDestroyed = false
					droppedItems = grimq.fromEntitiesAround(monsterLevel,monsterX,monsterY, 2, true)
									:where(grimq.isItem)
						 			:toIterator()
					if string.find(v,"arrow") ~= nil then
						for i in droppedItems do
							if i.name == "arrow" and arrowDestroyed == false then
								i:destroy()
								arrowDestroyed = true
							end
						end
					end
					if string.find(v,"quarrel") ~= nil  then
						for i in droppedItems do
							if i.name == "quarrel" and arrowDestroyed == false then
								i:destroy()
								arrowDestroyed = true
							end
						end
					end
				end
				
				-- clear tables:
				
				for k,v in ipairs(arrowTracker.arrowsTable[monsterId]) do
					table.remove(arrowTracker.arrowsTable[monsterId],k)
				end
				for k,v in ipairs(arrowTracker.aClear[monsterId]) do
					table.remove(arrowTracker.aClear[monsterId],k)
				end
			end
		end,
		{monster.id}
	)
	clearTimer:setTickLimit(1)
	clearTimer.destroy = true
	clearTimer:activate()
end

function enchantArrows(caster,spell)
	local arrowCast = true
	
	-- Check if caster has a normal asset arrow or quarrel for enchanting, 
	--   if not, prevent casting	
	
	if spell == "enchant_fire_arrow" or spell == "enchant_frost_arrow" 
	  or spell == "enchant_shock_arrow" or spell == "enchant_poison_arrow" then
		local currentItem
		arrowCast = false
		currentItem = caster:getItem(7)
		if currentItem ~= nil and grimq.fromArray(standardAssets)
				:any(function(v) return v==currentItem.name; end) then
			arrowCast = true
		end
		currentItem = caster:getItem(8)
		if currentItem ~= nil and grimq.fromArray(standardAssets)
				:any(function(v) return v==currentItem.name; end) then
			arrowCast = true
		end
		if arrowCast == false then
			hudPrint("Wrong ammo type!")
		end
	end
	
	return arrowCast
end
Then you must define the following hooks:
SpoilerShow

Code: Select all

fw.addHooks('monsters','arrows',{
	onProjectileHit = function(self,projectile,damage,type)
		return arrowTracker.register(self,projectile)
	end,
	onDie = function(self)
		return arrowTracker.clear(self)
	end,
	}
)

fw.addHooks('party','arrows',{	
	onCastSpell = function(caster, spell)
		return arrowTracker.enchantArrows(caster,spell)
	end,
	}
)
If you have custom monsters, don't forget to update fw monster list.

There's comments in the code indicating what's going on, the only thing you must watch out for is that custom arrows/quarrels must have "arrow" or "quarrel" in their definition name. So if you want, let's say, a "slayer" like in DM, use name = "arrow_slayer" and uiName = "slayer".

Enjoy, feedback welcome.

Re: The Arrow solution

Posted: Sat Dec 01, 2012 2:14 pm
by JKos
Nice one. But one warning about the callbacks, you should't pass any entity as callback argument because it will be stored in a table and that causes serialization error on save game. Instead of that you should pass the entity id and use findEntity to fetch the actual entity in callback. I will add this warning to the documentation.

Re: The Arrow solution

Posted: Sat Dec 01, 2012 4:39 pm
by Diarmuid
Thank you very much, now I understand this serialization thing better.

I couldn't just pass the monster.id, though, because the monster didn't exist anymore when the callback was called, so I had to find a workaround with a second table to store the data. I've also fixed some arrow search&replace script to make it more robust, for some reason the randomly generated id's don't work the same way in the editor and in the game.

Anyway, it should be fixed now.

Re: The Arrow solution

Posted: Tue Dec 04, 2012 6:54 pm
by Diarmuid
Hi there,

I updated this script to work with Xanathar's wonderful grimQ library. This cleaned up the code a lot, and allowed me to fix a rare bug: when a spider was killed during his attack, he somehow dropped an arrow on the party square as he was inbetween. I guess the same could happen if a monster was killed during movement... Now the search&destroy functions look at a radius of 2 squares around the monster onDie hook to prevent such occurances.