Predator Camouflage

Did a short little script to allow my bot to look like any prop its standing next to.

Video (or it didn't happen): https://youtu.be/frnP8V9ZPW8

Script...

-----------------------------------------------------------------------------
--	File	 : /units/ual0204/ual0204_script.lua
--
--	Author(s): EbolaSoup, Resin Smoker, Optimus Prime, Vissroid
--
--	Summary  : Aeon T2 Sniper Bot
--
--	Copyright © 2024 4DFAF, All rights reserved.
-----------------------------------------------------------------------------

-- Misc Lua called
local AWalkingLandUnit = import('/lua/aeonunits.lua').AWalkingLandUnit
local EffectUtils = import('/lua/EffectUtilities.lua')
local EffectTemplate = import('/lua/effecttemplates.lua')
local Custom_4D_EffectTemplate = import('/mods/4DFAF/lua/4D_EffectTemplates.lua')
local utilities = import('/lua/utilities.lua')

local myDebug = false

-- Weapon Local lua called
local SniperWeapon = import('/lua/aeonweapons.lua').ADFDisruptorCannonWeapon

-- Bones for weapon recoil effects
local weaponBones = { 'sniper_rifle', 'sniper_barrel' }

ual0204 = Class(AWalkingLandUnit) {
	Weapons = {
		-- Shield Piercing Rifle
		Sniper_Piercing_Rifle = Class(SniperWeapon) {
			PlayFxMuzzleSequence = function(self, muzzle)
				for k, v in weaponBones do
				   local steamEffects = EffectUtils.CreateBoneEffects( self.unit, v, self.unit:GetArmy(), EffectTemplate.WeaponSteam01 )
				end
				local groundEffects = EffectUtils.CreateBoneEffects( self.unit, 'ual0204', self.unit:GetArmy(), Custom_4D_EffectTemplate.ConcussionRing )
				if self.unit.CamoEntity then
					self.unit.CamoEntity:Destroy()
					self.unit.CamoEntity = nil
					self.unit:DisableIntel('Cloak')
					self.unit:SetVizToFocusPlayer('Always')
					self.unit:SetVizToAllies('Always')
				end
				SniperWeapon.PlayFxMuzzleSequence(self)
			end,
		},
	},

	OnCreate = function(self,builder,layer)
		AWalkingLandUnit.OnCreate(self)
		self:DisableIntel('Cloak')
		self.CamoEntity = nil
	end,

	OnMotionHorzEventChange = function(self, new, old)
		if myDebug then WARN('OnMotionHorzEventChange') end
		if self and not self:IsDead() then
			if new == 'Stopped' then
				if myDebug then WARN('	Stopped') end
				local propTbl = self:GetPropsInRadius(2)
				local propBp = propTbl[1].prop.Blueprint
				if myDebug then WARN('	prop blueprint '..repr(propBp) ) end
				local mesh = propBp.Display.MeshBlueprint or nil
				local scale = propBp.Display.UniformScale
				if myDebug then WARN('	Mesh / Scale: ', mesh, scale) end
				if mesh then
					self:EnableIntel('Cloak')
					self:SetVizToFocusPlayer('Never')
					self:SetVizToAllies('Never')
					self.CreateCamoEntity(self, mesh, scale)
				end
			else
				if myDebug then WARN('	moving') end
				if self.CamoEntity then
					self.CamoEntity:Destroy()
					self.CamoEntity = nil
					self:DisableIntel('Cloak')
					self:SetVizToFocusPlayer('Always')
					self:SetVizToAllies('Always')
				end

			end
		end
		AWalkingLandUnit.OnMotionHorzEventChange(self, new, old)
	end,

	CreateCamoEntity = function(self, mesh, scale)
		if myDebug then WARN('CreateCloakEntity') end
		ent = import('/lua/sim/Entity.lua').Entity({Owner = self,})
		ent:AttachBoneTo( -1, self, 'ual0204' )
		ent:SetMesh(mesh)
		ent:SetDrawScale(scale or 1)
		ent:SetVizToFocusPlayer('Always')
		ent:SetVizToAllies('Always')
		ent:SetVizToNeutrals('Intel')
		ent:SetVizToEnemies('Intel')
		self.CamoEntity = ent
		self.Trash:Add(ent)
	end,

	GetPropsInRadius = function(self, radius)
		if myDebug then WARN('GetPropsInRadius') end
		radius = radius or 1
		local pos = self:GetPosition()
		local props = GetReclaimablesInRect(Rect(pos[1] - radius, pos[3] - radius, pos[1] + radius, pos[3] + radius))
		if table.getn(props) then
			local propsByDist = {}
			for a, b in props do
				if not b:BeenDestroyed() then
					if not IsUnit(b) and not b.IsWreckage then
						table.insert(propsByDist, {dist = utilities.XZDistanceTwoVectors(pos, b:GetPosition()), prop = b})
					end
				end
			end
			if myDebug then WARN('	num props after filter: ', table.getn(propsByDist)) end
			table.sort(propsByDist, sort_by('dist'))
			return propsByDist
		else
			if myDebug then WARN('	no props in radius') end
			return false
		end
	end,

}
TypeClass = ual0204

Still a lot of work to do on this concept, but at least the hard part is done.

Resin

I absolutely love it 👍

A work of art is never finished, merely abandoned

@jip Thanks! Will tie it into to units movement and its attack. So it could remain unseen until it moves are fires. Which will force it to decloak. Figured this would be way cooler than just another phase mesh swapout.

Woot got everything working! I love having the terrain specific stealth.

Updated the script above. Only need to have the weapon charge timer (RenderFireClock) display correctly. Currently it works off of the weapons rate of fire but as this unit must charge to fire, I need to make something custom for it to display correctly.

OK here is a proper fire clock that uses the weapons energy drain as its basis.

OnCreate = function(self,builder,layer)
	AWalkingLandUnit.OnCreate(self)
	self:DisableIntel('Cloak')
	self.CamoEntity = nil
	self:SetWorkProgress(100)
end,
	
RenderChargeClockThread = function(self, eRequired, eDrain)
	local clockTime = math.round(10 * (eRequired / eDrain))
	local totalTime = clockTime
	while clockTime >= 0 and not self:BeenDestroyed() and not self:IsDead() do
		self:SetWorkProgress(1 - clockTime / totalTime)
		clockTime = clockTime - 1
		WaitSeconds(0.1)
	end
end,

Its called from the weapons PlayFxMuzzleSequence via...

	-- Reset the units reload bar
	local eDrain = self.EnergyDrainPerSecond
	local eRequired = self.EnergyRequired
	self.unit:ForkThread(self.unit.RenderChargeClockThread, eRequired, eDrain)

While it does work, I've noticed that there is a short delay between when the clock finished and when the weapon fires. I'm thinking that the weapons internal reload may have some delays involved that don't show properly in the units UI. Currently the unit shows as 10 seconds but with that delay I'd say its more like 11 seconds.

10f2075e-4f3a-4ec4-8c60-5ebc20cf1aa0-image.png

Not making this part of the Cybran faction is a criminal oversight, however.

"Design is an iterative process. The required number of iterations is one more than the number you have currently done. This is true at any point in time."

See all my projects:

@indexlibrorum IKR... Anyways, I've been pondering making the script into a superclass, so it can later be applied to other units quickly. Including the nasty Cybrans.

@indexlibrorum

Ok made the script into a SuperClass so it can be easily added to other units, including your Nasty Cybrans!

----------------------------------------------------------------------------
--	File		  : /mods/4DFAF/lua/4D_TerrainCamo/4D_TerrainCamo.lua
--
--	Author		  : Resin_Smoker
--
--	Summary 	  : Allows a unit to take the appearance of nearby props.
--

--	Copyright © 2024 4DFAF,  All rights reserved.
----------------------------------------------------------------------------
--
--	Add the following within a units PlayFxMuzzleSequence or other OnFire type event to remove Terrain Camo	
--
-- 	-- Remove the Camo entity and cloaking during the units weapon firing event
-- 	if self.unit.TerrainCamoEntity then
--		self.unit.TerrainCamoEntity:Destroy()
--		self.unit.TerrainCamoEntity = nil
--		if self.TerrainCamo_Unit_BP.Intel.Cloak then self.unit:DisableIntel('Cloak') end
--		if self.TerrainCamo_Unit_BP.Intel.RadarStealth then self.unit:DisableIntel('RadarStealth') end
--		self.unit:SetVizToFocusPlayer('Always')
--		self.unit:SetVizToAllies('Always')
--	end
--
--	Add these two line within the units lua script, adjusting the local unit info called as nessisary
--
--	local CLandUnit = import('/lua/cybranunits.lua').CLandUnit
--	ClandUnit = import('/mods/4DFAF/lua/CustomAbilities/4D_TerrainCamo/4D_TerrainCamo.lua').TerrainCamo( CLandUnit )
--
----------------------------------------------------------------------------

-- Misc Lua called
local utilities = import('/lua/utilities.lua')

-- Set flag "true" to see script progress within the error log
local myDebug = false

### Start of TerrainCamo(SuperClass) ###
function TerrainCamo(SuperClass)
	return Class(SuperClass) {
	
	OnCreate = function(self,builder,layer)
		SuperClass.OnCreate(self)
		self.TerrainCamo_Unit_BP = self:GetBlueprint()
		
		-- Disable unit cloak / stealth until TerrainCamo goes active
		if self.TerrainCamo_Unit_BP.Intel.Cloak then
			self:DisableIntel('Cloak')
		end
		if self.TerrainCamo_Unit_BP.Intel.RadarStealth then
			self:DisableIntel('RadarStealth')
		end
		
		-- Global for our camo entity		
		self.TerrainCamoEntity = nil
	end,	
	
	
	OnMotionHorzEventChange = function(self, new, old)
		if myDebug then WARN('OnMotionHorzEventChange') end
		if self and not self:IsDead() then
			
			-- When the units stops, engage cloak / camo
			if new == 'Stopped' then
				if myDebug then WARN('	Stopped') end
				local propTbl = self:GetPropsInRadius(2)
				local propBp = propTbl[1].prop.Blueprint
				if myDebug then WARN('	prop blueprint '..repr(propBp) ) end
				local mesh = propBp.Display.MeshBlueprint or nil
				local scale = propBp.Display.UniformScale
				if myDebug then WARN('	Mesh / Scale: ', mesh, scale) end
				if mesh and scale then
					if self.TerrainCamo_Unit_BP.Intel.Cloak then self:EnableIntel('Cloak') end
					if self.TerrainCamo_Unit_BP.Intel.RadarStealth then self:EnableIntel('RadarStealth') end					
					self:SetVizToFocusPlayer('Never')
					self:SetVizToAllies('Never')
					self:ForkThread(self.CreateFlashFX)
					self.CreateTerrainCamoEntity(self, mesh, scale)
				end
			else
				-- When unit moves, remove cloak / camo
				if myDebug then WARN('	moving') end
				if self.TerrainCamoEntity then
					self.TerrainCamoEntity:Destroy()
					self.TerrainCamoEntity = nil
					if self.TerrainCamo_Unit_BP.Intel.Cloak then self:DisableIntel('Cloak') end
					if self.TerrainCamo_Unit_BP.Intel.RadarStealth then self:DisableIntel('RadarStealth') end					
					self:SetVizToFocusPlayer('Always')
					self:SetVizToAllies('Always')
				end

			end
		end
		SuperClass.OnMotionHorzEventChange(self, new, old)
	end,
		
	CreateFlashFX = function(self)
		--	Simple FX to make the transition nicer looking
		self:PlayUnitSound('EnhanceStart')
		local fx = CreateAttachedEmitter(self, -1, self.Army, '/effects/emitters/aeon_sacrifice_02_emit.bp'):ScaleEmitter(1)
		WaitTicks(5)
		fx:Destroy()
	end,

	CreateTerrainCamoEntity = function(self, mesh, scale)
		if myDebug then WARN('CreateCloakEntity') end	
		ent = import('/lua/sim/Entity.lua').Entity({Owner = self,})
		ent:AttachBoneTo( -1, self, 0 )	
		ent:SetMesh(mesh)	
		ent:SetDrawScale(scale or 1)
		ent:SetVizToFocusPlayer('Always')
		ent:SetVizToAllies('Always')
		ent:SetVizToNeutrals('Intel')
		ent:SetVizToEnemies('Intel')
		self.TerrainCamoEntity = ent
		self.Trash:Add(ent)
	end,
	
	GetPropsInRadius = function(self, radius)
		if myDebug then WARN('GetPropsInRadius') end
		radius = radius or 1
		local pos = self:GetPosition()
		local props = GetReclaimablesInRect(Rect(pos[1] - radius, pos[3] - radius, pos[1] + radius, pos[3] + radius))
		if table.getn(props) then
			local propsByDist = {}
			for a, b in props do
				if not b:BeenDestroyed() then
					if not IsUnit(b) and not b.IsWreckage then
						table.insert(propsByDist, {dist = utilities.XZDistanceTwoVectors(pos, b:GetPosition()), prop = b})
					end
				end
			end
			if myDebug then WARN('	num props after filter: ', table.getn(propsByDist)) end
			table.sort(propsByDist, sort_by('dist'))
			return propsByDist
		else
			if myDebug then WARN('	no props in radius') end
			return false
		end
	end,	

}
end
### End of TerrainCamo(SuperClass) ###

I've added this ability to the 4DFAF URL0216 Insurgent and will remove it from the Predator. It will instead have a custom unit mesh for cloaking. (need to make it)

Made some improvements to the Terrain Camo. It now has a phase out effect before the units takes on the mesh of the nearby trees.