Looping over NPCs

Ask and answer questions about making games and related topics. Unrelated topics go in that other forum.

Moderators: marionline, SDHawk

Bluefeather42
Slime Knight
Posts: 102
Joined: Fri Jan 13, 2023 1:13 am

Looping over NPCs

Post by Bluefeather42 »

Been reading through the publicly-available scripts, and for the record, what is the particular best way of looping over NPCs to apply behavior, collisions, and the like?
Only using one NPC or one NPC type (like the "Vigilant Guard" in the LOS script) is straightforward but I know at some point I'm going to have to have more than one on a map at a time.
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7732
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Looping over NPCs

Post by Bob the Hamster »

If you need to loop over all NPC instances on the map, I recommend "next NPC reference"

https://hamsterrepublic.com/ohrrpgce/do ... creference
Bluefeather42
Slime Knight
Posts: 102
Joined: Fri Jan 13, 2023 1:13 am

Re: Looping over NPCs

Post by Bluefeather42 »

Thanks James! I'm using Next NPC Reference - borrowing how Charbile's Hatful Adventure loops over NPCs, and using the LOS script on the wiki, and I've hit a snag. when it gets to the Alter NPC, I get an invalid NPC ID. In the slice debugger it shows ref as -1. the "shadowdude" npc is a global NPC, with an ID of 1. critter:shadowdude is a constant with a value of 1. If I un-comment the "if check line of sight" statement I get an infinite loop. As a test, I replaced the line of sight check with "Delete NPC" and it executes successfully.

Code: Select all

script, critterloop, begin

	variable (ref)
	ref := Next NPC Reference()
	while (ref) do(
		if(get NPC Pool(ref) == pool:global) then (
			critterbrain(ref)
		)
		ref := Next NPC Reference()
	) 
end

# Check whether critters can see the hero
script, critterbrain, ref, begin
	variable (FV, RV, npcSl, critterID)	
	
	npcSl := get NPC Slice(ref)
	critterID := get NPC ID (ref)

	switch (critterID) do (
		case (critter:shadowdude) do (
			#if (check line of sight (NPC X (critter:shadowdude), NPC Y (critter:shadowdude), hero X (me), hero Y (me), NPC direction 		
			(critter:shadowdude)) && stealth < 20) then ( 
			alter NPC (critterID, NPCstat:move type, NPCmovetype:chaseyou)
			alter NPC (critterID, NPCstat:move speed, 5)
			#)
		)
	)
end
Looking at Alter NPC in the dictionary should I be specifying the pool?
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7732
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Looping over NPCs

Post by Bob the Hamster »

Yes, correct. If you don't tell the alter NPC command which pool, then it assumes you mean the local pool

However there is another problem. When you use alter NPC, you are changing ever copy of that NPC. That probably isn't what you want

The current code means that when any shadowdude passes the line of sight check, they will all start chasing you at once.

Instead, I suggest you create another global NPC like critter:shadowdudeangry

Then instead of alter NPC, use:

Code: Select all

 change NPC id (ref, critter:shadowdudeangry, pool:global) 
Oh! Also, for the "NPC X" and "NPC Y" you should use "ref" instead of "critter:shadowdude". Ref will give you the X Y of the specific instance of the NPC. Using the ID number gives you the X Y of the first copy on the map
Bluefeather42
Slime Knight
Posts: 102
Joined: Fri Jan 13, 2023 1:13 am

Re: Looping over NPCs

Post by Bluefeather42 »

And that's why I shouldn't program right before bed.

Anyway, a second global NPC for the "attack/angry" state is a great idea, and I've done what you suggested. I also noticed that I wasn't using the "FV" and "RV" variables and fixed that. However, "ref" is still coming up as a -1 when it gets to the line of sight script. There's only one NPC on the map. I'm also including my main loop and init script, in case there's something there.

Code: Select all

script, main loop, begin
	variable (thismap)
	thismap := current map

	while (thismap == current map) do (
		keypresshandler
		lucianloop
		critterloop
		wait (1)
	)

end

plotscript, init, begin

	stealth := 0
	LKstate := state:idle
	BaseStealth := 10
	set opacity (lookupslice (sl:map layer1), 75)
	LKsprite := lookupslice(sl:walkabout sprite)
	canshoot := true
	#critterInit
	main loop
	wait	

end
...

script, critterloop, begin

	variable (ref)
	ref := Next NPC Reference()
	while (ref) do(
		if(get NPC Pool(ref) == pool:global) then (
			critterbrain(ref)
		)
		ref := Next NPC Reference()
	) 
end

switch (get NPC ID(ref)) do (
		case (critter:shadowdude) do (
			FV := 6
			RV := 3
			#delete NPC (ref)
			if (check line of sight (NPC X (ref), NPC Y (ref), hero X (me), hero Y (me), NPC direction (ref), FV, RV) && stealth < 20) then (
				change npc ID (ref, critter:shadowdudeangry, pool:global)
			)
		)
	)
end
Last edited by Bluefeather42 on Sat May 31, 2025 5:25 pm, edited 1 time in total.
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7732
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Looping over NPCs

Post by Bob the Hamster »

Yes, NPC references are always negative numbers, so with one NPC on the map, -1 is exactly what you should expect to see for the ref variable
Bluefeather42
Slime Knight
Posts: 102
Joined: Fri Jan 13, 2023 1:13 am

Re: Looping over NPCs

Post by Bluefeather42 »

Gotcha. I was thinking of "Get NPC ID", but I'm still getting an infinite loop when it hits the Line of Sight check.
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7732
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Looping over NPCs

Post by Bob the Hamster »

Do you want to post the line of sight script? Maybe there is something inside the script causing the problem?
Bluefeather42
Slime Knight
Posts: 102
Joined: Fri Jan 13, 2023 1:13 am

Re: Looping over NPCs

Post by Bluefeather42 »

Here it is. I don't think I changed anything from the one on the wiki besides using FV and RV instead of global variables.

Code: Select all

script, check line of sight, x1, y1, x2, y2, critterdir, FV, RV, begin
  variable (distx, disty, hero dir)
  distx := x2 -- x1
  disty := y2 -- y1

  # Special check to prevent divide-by-zero
  if (distx == 0 && disty == 0) then (exit returning (true))

  # Figure out in which direction from the shadowdude the hero is
  # (The result is -1 if they are diagonal)

  hero dir := -1
  if (disty << 0 && abs(distx) << abs(disty)) then (hero dir := up)
  if (disty >> 0 && abs(distx) << abs(disty)) then (hero dir := down)
  if (distx << 0 && abs(disty) << abs(distx)) then (hero dir := left)
  if (distx >> 0 && abs(disty) << abs(distx)) then (hero dir := right)

  # How far can the shadowdude see in this direction?
  variable (view dist)
  if (critterdir == hero dir) then (
    view dist := FV
  ) else (
    view dist := RV
  )

  # If the hero is too far away, stop. (Pythagoras)
  # We add half a tile for smoother circles, then multiply both sides by 4
  if (4 * distx^2 + 4 * disty^2 >> (2 * view dist + 1)^2) then (exit returning (false))

  # Now check for view obstructions

  # x100 and y100 are x and y measured in 100ths of a tile
  variable (x100, y100, x100 step, y100 step, num steps, i)
  if (abs(distx) >> abs(disty)) then (
    # Step one tile in X direction and a fraction of a tile in Y direction at a time
    # (The --1 is a trick to prevent asymmetrical fields of view. Difficult to explain)
    x100 step := sign(distx) * 100
    y100 step := sign(disty) * (abs(100 * disty / distx) -- 1)
    num steps := abs(distx)
  ) else (
    # Step one tile in Y direction and a fraction of a tile in X direction at a time
    x100 step := sign(distx) * (abs(100 * distx / disty) -- 1)
    y100 step := sign(disty) * 100
    num steps := abs(disty)
  )

  # Walk from the hero back to the shadowdude. We check the tile that the hero is standing on,
  # but not the one the shadowdude is standing on
  x100 := x2 * 100
  y100 := y2 * 100
  for (i, 1, num steps) do (
    # (x100 + 50) / 100 rounds to the nearest tile
    if (read zone (zone:LOS obstruct, (x100 + 50) / 100, (y100 + 50) / 100) <> 0) then (exit returning (false))

    x100 -= x100 step
    y100 -= y100 step
  )

  # If we get here, the hero is in view
  return (true)
end
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7732
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Looping over NPCs

Post by Bob the Hamster »

Ah, okay. I guess I don't understand where the current problem is
Bluefeather42
Slime Knight
Posts: 102
Joined: Fri Jan 13, 2023 1:13 am

Re: Looping over NPCs

Post by Bluefeather42 »

I'll have to keep fiddling with it, then.

EDIT: Have not delved into the LOS code, but I think the game can work with having one creature on screen at a time. That's my back up plan.
Bluefeather42
Slime Knight
Posts: 102
Joined: Fri Jan 13, 2023 1:13 am

Re: Looping over NPCs

Post by Bluefeather42 »

Finally getting to mess with this again, I switched to the latest nightly and it's gotten worse. Critterloop goes into an infinite loop no matter what, regardless if the line of sight script is triggered or not. I'm rolling back to the last stable version and going from there.

Update: That didn't help. Posting the scripts.

Code: Select all

define constant(12345, sli:dataslice)
include,globvar.hss
include,util.hss


# NOTE 2-11-25:  There is a HUGE problem somewhere in the projectile scripts that is causing
# issues with the 'main' loop - the only wait command left in the scripts.  Originally I had
# to insert a couple of "waits" to get the shooting to actually function.  I'm not sure how to
# replace them with timers, and I'm not sure if the projectile scripts aren't completely borked
# when used in a shooter game like this.  I'm not sure exactly WHAT it is, but it's only occurring 
# when shooting, so the error is somewhere in the projectile scripts.  
# Need to look at CHA, shooter game, Don't Eat Soap, anything that has some sort of projectile 
# mechanics.  
# UPDATE 10:07 AM - The problem was in the "canshoot" timer at the bottom of "arrow eachtick"
# the game now works without error, but now it creates about a billion arrows if CTRL is held
# down. 
#5-2-2025:  CAN ONLY HAVE A SINGLE WHILE LOOP RUNNING, THIS NEEDS TO CONTROL THE WHOLE ACTION
# GAME SEGMENTS

################################################################################
# Projectiles - From the OHRRPGCE wiki.  Modified by removing the "arc" functionality.  
# Start a new projectile on an arcing path.
# Speed is the ground speed in pixels per tick.
# x distance and y distance is how far you want to shoot the projectile, relative
# to the initial position (like "move slice by").
# z distance is used when shooting at a target with a different height, e.g. aiming
# at the top of a tree or up a cliff. Positive z distance means shooting upwards

script, fire projectile, slice, speed, x distance, y distance, begin
    variable(dist, time)

    dist := sqrt(xdistance ^ 2 + ydistance ^ 2)

    # Calculate time-in-air, in ticks
    time := (dist + speed / 2) / speed
    if (time <= 0) then (time := 1)

    # Save some other necessary data
    prj:x100 := slicex(slice) * 100
    prj:y100 := slicey(slice) * 100
    prj:v_x100 := xdistance * 100 / time
    prj:v_y100 := ydistance * 100 / time
    prj:time := time
    save projectile data(create data slice(slice))
end

# Update the position of a projectile slice. Needs to be called every tick.
# Returns true if still in flight, or false if the projectile has reached the end.
script, projectile eachtick, slice, begin
    variable(dataslice)
    dataslice := lookup slice(sli:dataslice, slice)
    if (dataslice == false) then (script error($99="Couldn't find projectile data slice"))

    load projectile data(dataslice)
    if (prj:time <= 0) then (exit returning(false))  # Already finished
    prj:time -= 1
    prj:x100 += prj:v_x100
    prj:y100 += prj:v_y100
    
    save projectile data(dataslice)

    # Add 50 to round to nearest pixel
    put slice(slice, (prj:x100 + 50) / 100, (prj:y100 + 50) / 100)
    return (prj:time > 0)
end


# Set this to the correct map layer, above the hero
define constant(sl:map layer3, projectile layer)

define constant(80, arrow distance)

script, vecX, dir, dist, begin
    switch (dir) do (
        case (up, down) do (return (0))
        case (right) do (return (dist))
        case (left) do (return (-1 * dist))
    )
end

script, vecY, dir, dist, begin
    switch (dir) do (
        case (left, right) do (return (0))
        case (down) do (return (dist))
        case (up) do (return (-1 * dist))
    )
end


################################################################################
# Here is one way to run the scripts above: by using a timer.
# Each projectile in the air will need a different timer and a different global variable.

defineconstant(1000, sli:projectile)
defineconstant(15, timer:arrow1)
globalvariable(1, arrow1)
globalvariable(2, canshoot)

script, fire arrow1, begin
	set timer(4, 1, 8, set hero picture (0, 0))
	arrow1 := load walkabout sprite(1)
	setparent(arrow1, lookupslice(projectile layer))
	put slice(arrow1, heropixelx, heropixely)
	if (hero direction(0) == 0) then (
		set sprite frame(arrow1, 1)
	) else if (hero direction(0) == 1) then (
		set sprite frame(arrow1, 3)
	) else if (hero direction(0) == 2) then (
		set sprite frame(arrow1, 5)
	) else (
		set sprite frame(arrow1, 7)
	)
	variable(x, y)
	x := vecX(herodirection, arrow distance)  # Find the position 60 pixels ahead of the player
	y := vecY(herodirection, arrow distance)
	set timer(2, 0, 1, fire projectile(arrow1, 8, x, y))  # 8 pixel/tick, timer added by me 
	set timer(timer:arrow1, 0, 1, @arrow1 eachtick)
	stealth -= 10 
	

end

script, arrow1 eachtick, timer id, begin
	if (projectile eachtick(arrow1)) then (
	## Still in the air, run timer again
	set timer(timer id, 0)  # Restarts the timer with a count of 0 (next tick)
	) else (
		set timer(timer:arrow1, 0, 2, freeslice(arrow1))
		canshoot := true
	)


end 

script, lucianloop, begin

	switch (hero is walking) do (
		case(true) do (
			LKstate := state:walk
		) 
		case(false) do ( 
			LKstate := state:idle
		)
	)
	
	stealthloop

end

script, main loop, begin
	variable (thismap)
	thismap := current map

	while (thismap == current map) do (
		keypresshandler
		lucianloop
		critterloop
		wait (1)
	)
	
end

plotscript, init, begin

	stealth := 0
	LKstate := state:idle
	BaseStealth := 10
	set opacity (lookupslice (sl:map layer1), 75)
	LKsprite := lookupslice(sl:walkabout sprite)
	canshoot := true
	#critterInit
	main loop
	
end

#script, critterInit, begin
	# Taken from Charbile's Hatful Adventure by KyleKrack, with many thanks
	#variable (ref)

	#ref := next NPC Reference()
	#while (ref) do (
		#if (get NPC Pool(ref) == pool:global) then(
			
	
#end
	
script, stealthloop, begin

variable(shadow)
#ambient light is temporary, setting here to make the equation work
ambientlight := 0
############# Set opacity of Lucian's sprite based on lighting zone ###################################################

	if (read zone (2, hero X, hero Y)) then (
		set opacity (LKsprite, 50)
		shadow := 10
	) else if (read zone (3, hero X, hero Y)) then (
		set opacity (LKsprite, 25)
		shadow := 20
	) else (
		set opacity (LKsprite, 100)
		shadow := 0
	)

######################## Update stealth #############################################################################

	switch (LKstate) do (
		case(state:walk) do (
			stealth := (BaseStealth + shadow + ambientlight -- 20)
		) 
		case(state:idle) do ( 
			stealth := (BaseStealth + shadow + ambientlight)
		)
	)

	if (stealth <= 0) then (
		stealth := 0
	)

end

plotscript, keypresshandler, begin
#Note:  Wait commands should probably be replaced with timers!
	if(key is pressed(key:Ctrl)) then (
		if (canshoot == true) then (
			canshoot := false
			set hero picture (0, 2)
			set timer (6, 0, 2, @fire arrow1)
		)
	)
	
	if(key is pressed(key:p)) then (
		show text box (1)
	)
	
	
end

script, shootarrow, begin
	#set timer(4, 1, 2, @fire arrow1)
	fire arrow1
	set timer(4, 1, 8, set hero picture (0, 0))
end

# "critterloop" and check line of sight slightly modified from the Line of Sight script on the OHR wiki 
# and Charbile's Hatful Adventure by KyleKrack, with many thanks to TMC, James Paige, and KyleKrack
script, critterloop, begin

	variable (ref)
	ref := Next NPC Reference()
	while (ref) do(
		if(get NPC Pool(ref) == pool:global) then (
			critterbrain (ref)
		)
	)
	ref := Next NPC Reference()
	 
end

# Check whether critters can see the hero
script, critterbrain, ref, begin
	variable (npcSl)	
	
	npcSl := get NPC Slice(ref)

	switch (get NPC ID(ref)) do (
		case (critter:shadowdude) do (
			delete npc(ref)
		)
	)
end

# Check whether tile #2 is in view from tile #1, when facing a certain direction.
# Returns true or false.
script, check line of sight, x1, y1, x2, y2, critterdir, FV, RV, begin
  variable (distx, disty, hero dir)
  distx := x2 -- x1
  disty := y2 -- y1

  # Special check to prevent divide-by-zero
  if (distx == 0 && disty == 0) then (exit returning (true))

  # Figure out in which direction from the shadowdude the hero is
  # (The result is -1 if they are diagonal)

  hero dir := -1
  if (disty << 0 && abs(distx) << abs(disty)) then (hero dir := up)
  if (disty >> 0 && abs(distx) << abs(disty)) then (hero dir := down)
  if (distx << 0 && abs(disty) << abs(distx)) then (hero dir := left)
  if (distx >> 0 && abs(disty) << abs(distx)) then (hero dir := right)

  # How far can the shadowdude see in this direction?
  variable (view dist)
  if (critterdir == hero dir) then (
    view dist := FV
  ) else (
    view dist := RV
  )

  # If the hero is too far away, stop. (Pythagoras)
  # We add half a tile for smoother circles, then multiply both sides by 4
  if (4 * distx^2 + 4 * disty^2 >> (2 * view dist + 1)^2) then (exit returning (false))

  # Now check for view obstructions

  # x100 and y100 are x and y measured in 100ths of a tile
  variable (x100, y100, x100 step, y100 step, num steps, i)
  if (abs(distx) >> abs(disty)) then (
    # Step one tile in X direction and a fraction of a tile in Y direction at a time
    # (The --1 is a trick to prevent asymmetrical fields of view. Difficult to explain)
    x100 step := sign(distx) * 100
    y100 step := sign(disty) * (abs(100 * disty / distx) -- 1)
    num steps := abs(distx)
  ) else (
    # Step one tile in Y direction and a fraction of a tile in X direction at a time
    x100 step := sign(distx) * (abs(100 * distx / disty) -- 1)
    y100 step := sign(disty) * 100
    num steps := abs(disty)
  )

  # Walk from the hero back to the shadowdude. We check the tile that the hero is standing on,
  # but not the one the shadowdude is standing on
  x100 := x2 * 100
  y100 := y2 * 100
  for (i, 1, num steps) do (
    # (x100 + 50) / 100 rounds to the nearest tile
    if (read zone (zone:LOS obstruct, (x100 + 50) / 100, (y100 + 50) / 100) <> 0) then (exit returning (false))

    x100 -= x100 step
    y100 -= y100 step
  )

  # If we get here, the hero is in view
  return (true)
end
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7732
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Looping over NPCs

Post by Bob the Hamster »

In critterloop, this line here:

Code: Select all

ref := Next NPC Reference()
Is outside of the while loop. It needs to be moved inside
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7732
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Looping over NPCs

Post by Bob the Hamster »

Oh, and you need to pass the previous ref as an argument

Code: Select all

ref := Next NPC Reference(ref)
User avatar
Bob the Hamster
Lord of the Slimes
Posts: 7732
Joined: Tue Oct 16, 2007 2:34 pm
Location: Hamster Republic (Ontario Enclave)
Contact:

Re: Looping over NPCs

Post by Bob the Hamster »

Oh, and since you might delete the NPC inside critterbrain you need to do this:

Code: Select all

script, critterloop, begin

	variable (ref, next ref)
	ref := Next NPC Reference()
	while (ref) do(
	        next ref := Next NPC Reference(ref)
		if(get NPC Pool(ref) == pool:global) then (
			critterbrain (ref)
		)
		ref := next ref
	)
	 
end
Post Reply