Page 1 of 1

Problem with making NPC (Please, Help!!!)

Posted: Fri Jun 20, 2025 4:57 pm
by Ruslix
I want to make a "Bullet Bill Bro" (with bullet bill gun) In SMBX2, which is originally from Mario Forever. I've used Megadood's "Ice Bro" as "lua" script. But I've got a problem, whenever BB Bro's direction is "left" he shoots Bullet Bill from gun PERFECTLY,
Image
BUT whenever BB Bro's direction is "right", he shoot Bullet Bill from exact same position
Image

(After shooting Bullet Bill from right, it has 1 frame before it rotates)
Image

BB Bro's hitbox is also in the middle, which I can't change (changing his "gfxoffsetx" will make him kinda teleport)
Image

Here is a video of him as Npc (with sound):
https://drive.google.com/file/d/1b8eBIQ ... drive_link

Here is script:

Code: Select all

local npcManager = require("npcManager")
local rng = require("rng")
local npcID = NPC_ID

local firebros = {}

npcManager.setNpcSettings{
	id = npcID,
	gfxoffsety = 2,
	gfxwidth = 54,
	gfxheight = 48,
	width = 54,
	height = 48,
	frames = 3,
	framespeed = 8,
	framestyle = 1,
	nogravity=0,
	speed = 1,
	projectileid = 17,
	friendlyProjectileid = 17,
	lowjumpheight = 4,
	highjumpheight = 6,
	shotcount = 2,
	walktime = 60,
	jumptimemin = 65,
	jumptimemax = 85,
	shoottimemin = 135,
	shoottimemax = 180,
	shotspeedx = 3.5,
	shotsound = 22,
	npcheldfirerate = 0.75
}

npcManager.registerHarmTypes(
	npcID,
	{
		HARM_TYPE_JUMP,
		HARM_TYPE_FROMBELOW,
		HARM_TYPE_NPC,
		HARM_TYPE_HELD,
		HARM_TYPE_TAIL,
		HARM_TYPE_SPINJUMP,
		HARM_TYPE_SWORD,
		HARM_TYPE_LAVA
	},
	{
		[HARM_TYPE_JUMP]={id=npcID, speedX=0, speedY=0},
		[HARM_TYPE_FROMBELOW]=npcID,
		[HARM_TYPE_NPC]=npcID,
		[HARM_TYPE_HELD]=npcID,
		[HARM_TYPE_TAIL]=npcID,
		[HARM_TYPE_SPINJUMP]=10,
		[HARM_TYPE_LAVA]={id=13, xoffset=0.5, xoffsetBack = 0, yoffset=1, yoffsetBack = 1.5}
	}
)

local function spawnFireballs(v, data, heldPlayer)
	local id = NPC.config[v.id].projectileID
	if v.friendly or heldPlayer then
		id = NPC.config[v.id].friendlyProjectileID
	end
	local spawn = NPC.spawn(id,v.x - 0.5 * v.width,v.y + 12,v:mem(0x146,FIELD_WORD), false, true)
	spawn.direction = data.lockDirection
	spawn.speedX = NPC.config[v.id].shotspeedx * spawn.direction
	spawn.layerName = "Spawned NPCs"
	SFX.play(NPC.config[v.id].shotsound)
	if heldPlayer and heldPlayer.keys.up then
		spawn.speedY = -8
	end
end

function firebros.onTickEndNPC(v)
	if Defines.levelFreeze then return end
	
	local data = v.data._basegame
	
	if v.despawnTimer <= 0 or (v.forcedState > 0 and (v.forcedState ~= 208 or v.forcedCounter1 > 0)) or v.isProjectile then
		data.timer = nil
		data.forceFrame = 0
		data.lockDirection = v.direction
		if data.wasHeld and data.wasHeld > 0 then
			v:kill(3)
		end
		return
	end

	local cfg = NPC.config[v.id]
	
	local walkframes = cfg.frames - 1
	local held = v:mem(0x12C, FIELD_WORD)

	local skipShooting = false
	if held < 0 then
		data.wasHeldByNPC = true
		skipShooting = true
	elseif held > 0 then
		data.wasHeldByNPC = false
	end

	if data.wasHeldByNPC and v.forcedState == 208 then
		held = -1
		data.lockDirection = v.direction
	end
	
	if data.timer == nil then
		data.timer = cfg.walktime * 0.5
		data.jumptimer = RNG.randomInt(cfg.jumptimemin, cfg.jumptimemax)
		data.walk = v.direction
		data.shottimer = RNG.randomInt(cfg.shoottimemin, cfg.shoottimemax)
		data.wasHeld = held
		if held ~= 0 then
			data.shottimer = 120
		end
		data.forceFrame = 0
		data.lockDirection = v.direction
	end
	
	local heldPlayer
	if held > 0 then
		heldPlayer = Player(held)
		data.lockDirection = heldPlayer.direction
	end

	if data.walk == 0 then
		data.walk = RNG.irandomEntry{-1, 1}
	end
	
	data.jumptimer = data.jumptimer - 1
	data.wasHeld = held
	
	if skipShooting then
		return
	end

	if held == 0 then
		if v.collidesBlockBottom then
			v.speedX = 0
			
			if data.timer % 8 == 0 then
				data.forceFrame = (data.forceFrame + 1) % walkframes
			end
			if v:mem(0x12E,FIELD_WORD) == 0 then
				if Player.getNearest(v.x, v.y).x < v.x then
					data.lockDirection = -1
				else
					data.lockDirection = 1
				end
			end
			v.speedX = data.walk * 1.2 * NPC.config[v.id].speed
			if data.shottimer > 20 then
				data.timer = data.timer + 1
			end

			if data.jumptimer <= 0 then
				if data.shottimer > 30 then
					v.speedY = -math.abs(NPC.config[v.id].lowjumpheight)
					v.speedX = 0
				end
				data.jumptimer = RNG.randomInt(cfg.jumptimemin, cfg.jumptimemax)
			else
				data.shottimer = data.shottimer - 1
			end
		else
			if data.jumptimer <= 0 then
				data.jumptimer = RNG.randomInt(cfg.jumptimemin, cfg.jumptimemax)
			elseif data.shottimer <= 20 then
				data.shottimer = data.shottimer - 1
			end
		end
	else
		if held < 0 then
			data.shottimer = data.shottimer - 1 * NPC.config[v.id].npcheldfirerate
		else
			data.shottimer = data.shottimer - 1
		end
		if data.shottimer <= 100 then
			spawnFireballs(v, data, heldPlayer)
			data.shottimer = 140
		end
		if data.shottimer <= 108 or data.shottimer >= 132 then
			data.forceFrame = walkframes
		else
			data.forceFrame = walkframes - 1
		end
	end

	if data.timer % cfg.walktime == 0 then
		data.walk = -data.walk
	end
	if data.shottimer <= 20 then
		v.speedX = 0
		data.forceFrame = walkframes
		if data.shottimer % 40 == 0 then
			spawnFireballs(v, data)
		end
		if data.shottimer == -40 * (cfg.shotcount - 1) + 20 and RNG.randomInt(0, 1) == 1 then
			v.speedY = -math.abs(NPC.config[v.id].highjumpheight)
		end
		if data.shottimer < -40 * (cfg.shotcount - 1) - 10 then
			data.shottimer = RNG.randomInt(cfg.shoottimemin, cfg.shoottimemax)
			data.jumptimer = data.jumptimer + 60
		end
	end
end

function firebros.onDrawNPC(v)
	if v:mem(0x12A, FIELD_WORD) <= 0 then return end
	
	local data = v.data._basegame
	if data.forceFrame then
		v.animationTimer = 500
		v.animationFrame = data.forceFrame
		if data.lockDirection == 1 then v.animationFrame = v.animationFrame + NPC.config[v.id].frames end
	end
end

function firebros.onInitAPI()
	npcManager.registerEvent(npcID, firebros, "onTickEndNPC")
	npcManager.registerEvent(npcID, firebros, "onDrawNPC")
end

return firebros
And here is level itself with him:
https://drive.google.com/file/d/1WUz0b8 ... drive_link


Help me please, I would appreciate it so much

Update 1: Files are not restricted anymore

Re: Problem with making NPC (Please, Help!!!)

Posted: Thu Jun 26, 2025 7:25 am
by MegaDood
Havent tested it but something like this should work

Code: Select all

local npcManager = require("npcManager")
local rng = require("rng")
local npcID = NPC_ID

local firebros = {}

npcManager.setNpcSettings{
	id = npcID,
	gfxoffsety = 2,
	gfxwidth = 54,
	gfxheight = 48,
	width = 54,
	height = 48,
	frames = 3,
	framespeed = 8,
	framestyle = 1,
	nogravity=0,
	speed = 1,
	projectileid = 17,
	friendlyProjectileid = 17,
	lowjumpheight = 4,
	highjumpheight = 6,
	shotcount = 2,
	walktime = 60,
	jumptimemin = 65,
	jumptimemax = 85,
	shoottimemin = 135,
	shoottimemax = 180,
	shotspeedx = 3.5,
	shotsound = 22,
	npcheldfirerate = 0.75
}

npcManager.registerHarmTypes(
	npcID,
	{
		HARM_TYPE_JUMP,
		HARM_TYPE_FROMBELOW,
		HARM_TYPE_NPC,
		HARM_TYPE_HELD,
		HARM_TYPE_TAIL,
		HARM_TYPE_SPINJUMP,
		HARM_TYPE_SWORD,
		HARM_TYPE_LAVA
	},
	{
		[HARM_TYPE_JUMP]={id=npcID, speedX=0, speedY=0},
		[HARM_TYPE_FROMBELOW]=npcID,
		[HARM_TYPE_NPC]=npcID,
		[HARM_TYPE_HELD]=npcID,
		[HARM_TYPE_TAIL]=npcID,
		[HARM_TYPE_SPINJUMP]=10,
		[HARM_TYPE_LAVA]={id=13, xoffset=0.5, xoffsetBack = 0, yoffset=1, yoffsetBack = 1.5}
	}
)

local function spawnFireballs(v, data, heldPlayer)
	local id = NPC.config[v.id].projectileID
	if v.friendly or heldPlayer then
		id = NPC.config[v.id].friendlyProjectileID
	end
	local spawn = NPC.spawn(id,v.x - 0.5 * v.width + ((v.direction + 1) * (54 / 2)),v.y + 12,v:mem(0x146,FIELD_WORD), false, true)
	spawn.direction = data.lockDirection
	spawn.speedX = NPC.config[v.id].shotspeedx * spawn.direction
	spawn.layerName = "Spawned NPCs"
	SFX.play(NPC.config[v.id].shotsound)
	if heldPlayer and heldPlayer.keys.up then
		spawn.speedY = -8
	end
end

function firebros.onTickEndNPC(v)
	if Defines.levelFreeze then return end
	
	local data = v.data._basegame
	
	if v.despawnTimer <= 0 or (v.forcedState > 0 and (v.forcedState ~= 208 or v.forcedCounter1 > 0)) or v.isProjectile then
		data.timer = nil
		data.forceFrame = 0
		data.lockDirection = v.direction
		if data.wasHeld and data.wasHeld > 0 then
			v:kill(3)
		end
		return
	end

	local cfg = NPC.config[v.id]
	
	local walkframes = cfg.frames - 1
	local held = v:mem(0x12C, FIELD_WORD)

	local skipShooting = false
	if held < 0 then
		data.wasHeldByNPC = true
		skipShooting = true
	elseif held > 0 then
		data.wasHeldByNPC = false
	end

	if data.wasHeldByNPC and v.forcedState == 208 then
		held = -1
		data.lockDirection = v.direction
	end
	
	if data.timer == nil then
		data.timer = cfg.walktime * 0.5
		data.jumptimer = RNG.randomInt(cfg.jumptimemin, cfg.jumptimemax)
		data.walk = v.direction
		data.shottimer = RNG.randomInt(cfg.shoottimemin, cfg.shoottimemax)
		data.wasHeld = held
		if held ~= 0 then
			data.shottimer = 120
		end
		data.forceFrame = 0
		data.lockDirection = v.direction
	end
	
	local heldPlayer
	if held > 0 then
		heldPlayer = Player(held)
		data.lockDirection = heldPlayer.direction
	end

	if data.walk == 0 then
		data.walk = RNG.irandomEntry{-1, 1}
	end
	
	data.jumptimer = data.jumptimer - 1
	data.wasHeld = held
	
	if skipShooting then
		return
	end

	if held == 0 then
		if v.collidesBlockBottom then
			v.speedX = 0
			
			if data.timer % 8 == 0 then
				data.forceFrame = (data.forceFrame + 1) % walkframes
			end
			if v:mem(0x12E,FIELD_WORD) == 0 then
				if Player.getNearest(v.x, v.y).x < v.x then
					data.lockDirection = -1
				else
					data.lockDirection = 1
				end
			end
			v.speedX = data.walk * 1.2 * NPC.config[v.id].speed
			if data.shottimer > 20 then
				data.timer = data.timer + 1
			end

			if data.jumptimer <= 0 then
				if data.shottimer > 30 then
					v.speedY = -math.abs(NPC.config[v.id].lowjumpheight)
					v.speedX = 0
				end
				data.jumptimer = RNG.randomInt(cfg.jumptimemin, cfg.jumptimemax)
			else
				data.shottimer = data.shottimer - 1
			end
		else
			if data.jumptimer <= 0 then
				data.jumptimer = RNG.randomInt(cfg.jumptimemin, cfg.jumptimemax)
			elseif data.shottimer <= 20 then
				data.shottimer = data.shottimer - 1
			end
		end
	else
		if held < 0 then
			data.shottimer = data.shottimer - 1 * NPC.config[v.id].npcheldfirerate
		else
			data.shottimer = data.shottimer - 1
		end
		if data.shottimer <= 100 then
			spawnFireballs(v, data, heldPlayer)
			data.shottimer = 140
		end
		if data.shottimer <= 108 or data.shottimer >= 132 then
			data.forceFrame = walkframes
		else
			data.forceFrame = walkframes - 1
		end
	end

	if data.timer % cfg.walktime == 0 then
		data.walk = -data.walk
	end
	if data.shottimer <= 20 then
		v.speedX = 0
		data.forceFrame = walkframes
		if data.shottimer % 40 == 0 then
			spawnFireballs(v, data)
		end
		if data.shottimer == -40 * (cfg.shotcount - 1) + 20 and RNG.randomInt(0, 1) == 1 then
			v.speedY = -math.abs(NPC.config[v.id].highjumpheight)
		end
		if data.shottimer < -40 * (cfg.shotcount - 1) - 10 then
			data.shottimer = RNG.randomInt(cfg.shoottimemin, cfg.shoottimemax)
			data.jumptimer = data.jumptimer + 60
		end
	end
end

function firebros.onDrawNPC(v)
	if v:mem(0x12A, FIELD_WORD) <= 0 then return end
	
	local data = v.data._basegame
	if data.forceFrame then
		v.animationTimer = 500
		v.animationFrame = data.forceFrame
		if data.lockDirection == 1 then v.animationFrame = v.animationFrame + NPC.config[v.id].frames end
	end
end

function firebros.onInitAPI()
	npcManager.registerEvent(npcID, firebros, "onTickEndNPC")
	npcManager.registerEvent(npcID, firebros, "onDrawNPC")
end

return firebros
Essentially I put ((v.direction + 1) * (54 / 2)) in the line that spawns the NPC. This takes the spawned NPC, which is currently perfect facing left, and offsets it by 54 pixels to the right when facing the other way. The reason I did 54/2 is because it multiplies this number by the other direction + 1, which becomes 2.

Re: Problem with making NPC (Please, Help!!!)

Posted: Fri Jun 27, 2025 5:38 am
by Ruslix
I appreciate that you helped me, but now BB Bro has likely a chance system whenever he can shoot from his Gun or behind his back in both directions