After converting the functions from ASM to lua and debugging, it turns out that the predictor is not good at estimating the exact time when an animal leaves the screen ("exit prediction") except for the initial batch of animals or for when the animal is on the move. One reason I can for this is object placement: animals loaded in slots after the capsule will load one frame earlier than those loaded before the capsule; this 1-frame difference can change the direction that the animal will travel, and this will affect exit time. Another factor (which I account for) is that an animal loaded after the capsule will take one extra frame to register as missing. I have no idea what other factors are at play; but the fact is that the exit prediction is off by up to plus/minus 3 frames in my tests. The wait until the animals are spawned/start moving works perfectly, only the endpoint prediction that fails -- unless the animal is already on the move.
The only ways out of this are simulating the object placement or emulating all frames until the animals leave. I be looking into the latter; in any case, here is the current version of the script:
Download predict-animals-v2.luaLanguage: lua
local v_objspace = 0xFFFFD000
local c_objsize = 0x40
-- Object SST constants.
local obRender = 0x1
local obX = 0x8
local obY = 0xC
local obVelX = 0x10
local obVelY = 0x12
local obFrame = 0x1A
local obHeight = 0x16
local obWidth = 0x17
local obTimeFrame = 0x1E
local obRoutine = 0x24
-- A few important RAM locations.
local v_random = 0xFFFFF636
local v_zone = 0xFFFFFE10
local v_vbla_byte = 0xFFFFFE0F
local v_collindex = 0xFFFFF796
local v_lvllayout = 0xFFFFA400
local AngleMap = 0x00062900
local CollArray1 = 0x00062A00
-- Function to find the capsule'e switch
local function find_capsule()
for addr=v_objspace + c_objsize,v_objspace + 0x40 * c_objsize,c_objsize do
if memory.readbyte(addr) == 0x3e and memory.readbyte(addr+obRoutine) > 2 then
return addr
end
end
return nil
end
-- Function to enumerate all animals that matter for the capsule
local function enum_animals()
local animals = {}
for addr=v_objspace + c_objsize,v_objspace + 0x40 * c_objsize,c_objsize do
if memory.readbyte(addr)==0x28 and memory.readbyte(addr+obRoutine) > 0 then
table.insert(animals, addr)
end
end
return animals
end
local function speed2color(speed, min, max)
local green = math.floor(((speed - min) * 255) / (max - min))
local red = math.floor(((max - speed) * 255) / (max - min))
local blue, alpha = 0, 255
return {red, green, blue, alpha}
end
local minspd, maxspd = 0x140, 0x300
-- Names for animals.
local animals = {[0]={"Rabbit" , speed2color(0x200, minspd, maxspd), -0x200, -0x400},
[1]={"Chicken" , speed2color(0x200, minspd, maxspd), -0x200, -0x300},
[2]={"Eagle" , speed2color(0x180, minspd, maxspd), -0x180, -0x300},
[3]={"Seal" , speed2color(0x140, minspd, maxspd), -0x140, -0x180},
[4]={"Pig" , speed2color(0x1C0, minspd, maxspd), -0x1C0, -0x300},
[5]={"Flicky" , speed2color(0x300, minspd, maxspd), -0x300, -0x400},
[6]={"Squirrel", speed2color(0x280, minspd, maxspd), -0x280, -0x380}}
-- Map between zone ID and animal names.
local typelist = {[0]={[0]=0, [1]=5},
[1]={[0]=2, [1]=3},
[2]={[0]=6, [1]=3},
[3]={[0]=4, [1]=5},
[4]={[0]=4, [1]=1},
[5]={[0]=0, [1]=1}}
-- Adddress of capsule switch in RAM.
local capsule_switch = nil
-- Internal variables.
local seed = 0
local last_seed = -1
local last_vbla = -1
-- Lua version of the Sonic 1 random number generator.
local function GetRandom()
local d1 = seed or 0x2A6D365A
local d0 = d1 -- move.l d1,d0
d1 = SHIFT(d1, -2) -- asl.l #2,d1
d1 = d1 + d0 -- add.l d0,d1
d1 = SHIFT(d1, -3) -- asl.l #3,d1
d1 = d1 + d0 -- add.l d0,d1
d0 = OR(AND(d1, 0x0000FFFF), AND(d0, 0xFFFF0000)) -- move.w d1,d0
d1 = OR(SHIFT(AND(d1, 0x0000FFFF), -16), SHIFT(AND(d1, 0xFFFF0000), 16)) -- swap d1
d0 = OR(AND(d1 + d0, 0x0000FFFF), AND(d0, 0xFFFF0000)) -- add.w d1,d0
d1 = OR(SHIFT(AND(d0, 0x0000FFFF), -16), SHIFT(AND(d1, 0xFFFF0000), 16)) -- move.w d0,d1 \n swap d1
seed = d1 -- move.l d1,(v_random).w
return d0
end
-- Lua version of the Sonic 1 function to find the nearest tile.
local function FindNearestTile(objRender, objX, objY)
-- move.w d2,d0
-- lsr.w #1,d0
-- andi.w #$380,d0
-- move.w d3,d1
-- lsr.w #8,d1
-- andi.w #$7F,d1
-- add.w d1,d0
local index = AND(SHIFT(objY, 1), 0x380) + AND(SHIFT(objX, 8), 0x7F)
-- Later: moveq #-1,d1
local tilenum = memory.readbyte(v_lvllayout + index) -- lea (v_lvllayout).w,a1 \n move.b (a1,d0.w),d1
if AND(tilenum, 0x80) ~= 0 then -- bmi.s @specialtile
-- @specialtile:
tilenum = AND(tilenum, 0x7F) -- andi.w #$7F,d1
if AND(objRender, 0x40) ~= 0 then -- btst #6,obRender(a0) \n beq.s @treatasnormal
tilenum = tilenum + 1 -- addq.w #1,d1
if tilenum == 0x29 then -- cmpi.w #$29,d1 \n bne.s @treatasnormal
tilenum = 0x51 -- move.w #$51,d1
end
end
-- @treatasnormal:
tilenum = AND(tilenum - 1, 0xFF) -- subq.b #1,d1
if AND(tilenum, 0x80) ~= 0 then -- ext.w d1
tilenum = OR(tilenum, 0xFF00)
end
tilenum = OR(SHIFT(AND(tilenum, 0x7F), -9), SHIFT(AND(tilenum, 0xFF80), 7)) -- ror.w #7,d1
-- move.w d2,d0
-- add.w d0,d0
-- andi.w #$1E0,d0
-- add.w d0,d1
-- move.w d3,d0
-- lsr.w #3,d0
-- andi.w #$1E,d0
-- add.w d0,d1
return OR(AND(tilenum + AND(2 * objY, 0x1E0) + AND(SHIFT(objX, 3), 0x1E), 0xFFFF), 0xFFFF0000)
end
if tilenum > 0 then
tilenum = AND(tilenum - 1, 0xFF) -- subq.b #1,d1
if AND(tilenum, 0x80) ~= 0 then -- ext.w d1
tilenum = OR(tilenum, 0xFF00)
end
tilenum = OR(SHIFT(AND(tilenum, 0x7F), -9), SHIFT(AND(tilenum, 0xFF80), 7)) -- ror.w #7,d1
-- move.w d2,d0
-- add.w d0,d0
-- andi.w #$1E0,d0
-- add.w d0,d1
-- move.w d3,d0
-- lsr.w #3,d0
-- andi.w #$1E,d0
-- add.w d0,d1
return OR(AND(tilenum + AND(2 * objY, 0x1E0) + AND(SHIFT(objX, 3), 0x1E), 0xFFFF), 0xFFFF0000)
end
-- @blanktile:
return 0xFFFFFF00 -- movea.l d1,a1 \n rts
end
-- Lua version of the Sonic 1 secondary function to find the floor.
local function FindFloor2(objRender, objX, objY, solidbit, delta, initangle, flipmask)
local angle = initangle
local tileaddr = FindNearestTile(objRender, objX, objY) -- bsr.s FindNearestTile
local floordist = 0
local tile = memory.readword(tileaddr) -- move.w (a1),d0 \n move.w d0,d4
-- andi.w #$7FF,d0 \n beq.s @isblank2 \n btst d5,d4 \n bne.s @issolid
local tileid = AND(tile, 0x7FF)
if tileid ~= 0 and AND(tile, solidbit) ~= 0 then
-- @issolid:
local colptr = memory.readlong(v_collindex) -- movea.l (v_collindex).w,a2
local block = memory.readbyte(colptr + tileid) -- move.b (a2,d0.w),d0
if block == 0 then -- beq.s @isblank2
-- @isblank2
return 0xF - AND(objY, 0xF), tileaddr, angle
end
angle = memory.readbytesigned(AngleMap + block) -- lea (AngleMap).l,a2 \n move.b (a2,d0.w),(a4)
block = SHIFT(block, -4) -- lsl.w #4,d0
local xcopy = objX -- move.w d3,d1
if AND(tile, BIT(0xB)) ~= 0 then -- btst #$B,d4 \n beq.s @noflip
xcopy = -objX-1 -- not.w d1
angle = -angle -- neg.b (a4)
end
-- @noflip:
if AND(tile, BIT(0xC)) ~= 0 then --- btst #$C,d4 \n beq.s @noflip2
angle = -0x80 - angle -- addi.b #$40,(a4) \n neg.b (a4) \n subi.b #$40,(a4)
end
-- @noflip2:
xcopy = AND(objX, 0xF) + block -- andi.w #$F,d1 \n add.w d0,d1
local colhgt = memory.readbytesigned(CollArray1 + xcopy) -- lea (CollArray1).l,a2 \n move.b (a2,d1.w),d0 \n ext.w d0
tile = XOR(tile, flipmask) -- eor.w d6,d4
if AND(tile, BIT(0xC)) ~= 0 then -- btst #$C,d4 \n beq.s @noflip3
colhgt = -colhgt -- neg.w d0
end
-- @noflip3:
if colhgt == 0 then -- tst.w d0 \n beq.s @isblank2
-- @isblank2
return 0xF - AND(objY, 0xF), tileaddr, angle
elseif colhgt < 0 then -- bmi.s @negfloor
-- @negfloor:
local dist = AND(objY, 0xF) -- move.w d2,d1 \n andi.w #$F,d1
if dist + colhgt < 0 then -- add.w d1,d0 \n bpl.w @isblank2
return -dist-1, tileaddr, angle -- not.w d1 \n rts
end
else
local dist = AND(objY, 0xF) -- move.w d2,d1 \n andi.w #$F,d1
return 0xF - (dist + colhgt), tileaddr, angle -- add.w d1,d0 \n move.w #$F,d1 \n sub.w d0,d1 \n rts
end
end
-- @isblank2:
-- move.w #$F,d1
-- move.w d2,d0
-- andi.w #$F,d0
-- sub.w d0,d1
-- rts
return 0xF - AND(objY, 0xF), tileaddr, angle
end
-- Lua version of the Sonic 1 function to find the floor.
local function FindFloor(objRender, objX, objY, solidbit, delta, initangle, flipmask)
local angle = initangle
local tileaddr = FindNearestTile(objRender, objX, objY) -- bsr.s FindNearestTile
local floordist = 0
local tile = memory.readword(tileaddr) -- move.w (a1),d0 \n move.w d0,d4
-- andi.w #$7FF,d0 \n beq.s @isblank \n btst d5,d4 \n bne.s @issolid
local tileid = AND(tile, 0x7FF)
if tileid ~= 0 and AND(tile, solidbit) ~= 0 then
-- @issolid:
local colptr = memory.readlong(v_collindex) -- movea.l (v_collindex).w,a2
local block = memory.readbyte(colptr + tileid) -- move.b (a2,d0.w),d0
if block == 0 then -- beq.s @isblank
-- @isblank
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY + delta, solidbit, delta, initangle, flipmask)
return floordist + 0x10, tileaddr, angle
end
angle = memory.readbytesigned(AngleMap + block) -- lea (AngleMap).l,a2 \n move.b (a2,d0.w),(a4)
block = SHIFT(block, -4) -- lsl.w #4,d0
local xcopy = objX -- move.w d3,d1
if AND(tile, BIT(0xB)) ~= 0 then -- btst #$B,d4 \n beq.s @noflip
xcopy = -objX-1 -- not.w d1
angle = -angle -- neg.b (a4)
end
-- @noflip:
if AND(tile, BIT(0xC)) ~= 0 then --- btst #$C,d4 \n beq.s @noflip2
angle = -0x80 - angle -- addi.b #$40,(a4) \n neg.b (a4) \n subi.b #$40,(a4)
end
-- @noflip2:
xcopy = AND(objX, 0xF) + block -- andi.w #$F,d1 \n add.w d0,d1
local colhgt = memory.readbytesigned(CollArray1 + xcopy) -- lea (CollArray1).l,a2 \n move.b (a2,d1.w),d0 \n ext.w d0
tile = XOR(tile, flipmask) -- eor.w d6,d4
if AND(tile, BIT(0xC)) ~= 0 then -- btst #$C,d4 \n beq.s @noflip3
colhgt = -colhgt -- neg.w d0
end
-- @noflip3:
if colhgt == 0 then -- tst.w d0 \n beq.s @isblank
-- @isblank
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY + delta, solidbit, delta, initangle, flipmask)
return floordist + 0x10, tileaddr, angle
elseif colhgt < 0 then -- bmi.s @negfloor
-- @negfloor:
local dist = AND(objY, 0xF) -- move.w d2,d1 \n andi.w #$F,d1
if dist + colhgt < 0 then -- add.w d1,d0 \n bpl.w @isblank
-- @maxfloor
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY - delta, solidbit, delta, initangle, flipmask)
return floordist - 0x10, tileaddr, angle
end
elseif colhgt == 0x10 then -- cmpi.b #$10,d0 \n beq.s @maxfloor
-- @maxfloor:
-- sub.w a3,d2
-- bsr.w FindFloor2
-- add.w a3,d2
-- subi.w #$10,d1
-- rts
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY - delta, solidbit, delta, initangle, flipmask)
return floordist - 0x10, tileaddr, angle
else
-- move.w d2,d1
-- andi.w #$F,d1
-- add.w d1,d0
-- move.w #$F,d1
-- sub.w d0,d1
-- rts
return 0xF - (AND(objY, 0xF) + colhgt), tileaddr, angle
end
end
-- @isblank:
-- add.w a3,d2
-- bsr.w FindFloor2 ; try tile below the nearest
-- sub.w a3,d2
-- addi.w #$10,d1 ; return distance to floor
-- rts
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY + delta, solidbit, delta, initangle, flipmask) -- beq.s @isblank
return floordist + 0x10, tileaddr, angle
end
-- Lua version of the Sonic 1 function to find distance to the floor.
local function ObjFloorDist(objX, objY, objH, objR)
--local objX = memory.readword(obj+obX) -- move.w obX(a0),d3
--local objY = memory.readword(obj+obY) -- move.w obY(a0),d2
--local objH = memory.readbytesigned(obj+obHeight) -- moveq #0,d0 \n move.b obHeight(a0),d0 \n ext.w d0
--local objR = memory.readbyte(obj+obRender)
--objY = objY + objH -- add.w d0,d2
-- lea (v_anglebuffer).w,a4
-- move.b #0,(a4)
-- movea.w #$10,a3
-- move.w #0,d6
-- moveq #$D,d5
local floordist, tileaddr, angle = FindFloor(objR, objX, objY + objH, BIT(0xD), 0x10, 0, 0) -- bsr.w FindFloor
if AND(angle, 1) ~= 0 then
angle = 0 -- move.b (v_anglebuffer).w,d3 \n btst #0,d3 \n beq.s locret_14E4E \n move.b #0,d3
end
return floordist, tileaddr, angle -- locret_14E4E: rts
end
-- Lua version of the Sonic 1 function to make an object "fall".
local function ObjectFall(objX, objY, objVX, objVY)
--local memory.readlong(obj+obX) -- move.l obX(a0),d2
--local memory.readlong(obj+obY) -- move.l obY(a0),d3
--local memory.readwordsigned(obj+obVelX) -- move.w obVelX(a0),d0 \n ext.l d0
objX = AND(objX + SHIFT(objVX, -8), 0xFFFFFFFF) -- asl.l #8,d0 \n add.l d0,d2
--local memory.readwordsigned(obj+obVelY) -- move.w obVelY(a0),d0 \n ext.l d0
objY = AND(objY + SHIFT(objVY, -8), 0xFFFFFFFF) -- asl.l #8,d0 \n add.l d0,d3
objVY = objVY + 0x38 -- addi.w #$38,obVelY(a0)
return objX, objY, objVX, objVY -- move.l d2,obX(a0) \n move.l d3,obY(a0) \n rts
end
-- Lua version of the Sonic 1 function loc_912A.
local function animal_fall(objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla)
-- tst.b obRender(a0)
-- bpl.w DeleteObject
objX, objY, objVX, objVY = ObjectFall(objX, objY, objVX, objVY) -- bsr.w ObjectFall
if objVY >= 0 then -- tst.w obVelY(a0) \n bmi.s loc_9180
local floordist = ObjFloorDist(SHIFT(objX, 16), SHIFT(objY, 16), objH, objR) -- jsr ObjFloorDist
if floordist < 0 then -- tst.w d1 \n bpl.s loc_9180
objY = objY + floordist -- add.w d1,obY(a0)
objVX = objNVX -- move.w $32(a0),obVelX(a0)
objVY = objNVY -- move.w $34(a0),obVelY(a0)
-- move.b #1,obFrame(a0)
-- move.b $30(a0),d0
-- add.b d0,d0
-- addq.b #4,d0
-- move.b d0,obRoutine(a0)
-- tst.b (v_bossstatus).w
-- beq.s loc_9180
if AND(vbla, BIT(4)) ~= 0 then -- btst #4,(v_vbla_byte).w \n beq.s loc_9180
objVX = -objVX -- neg.w obVelX(a0)
objR = XOR(objR, BIT(0)) -- bchg #0,obRender(a0)
end
end
end
-- loc_9180:
-- bra.w DisplaySprite
return objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla
end
local function simulate_animal_delay_fall_escape(obj, objX, objY, objVX, objVY, objNVX, objNVY, objWait, vbla, capX)
local objH = 0xC
local objR = 0x85
local objW = 8
local initvbla = vbla
-- If we have a real object:
if obj ~= nil then
objX = memory.readlong(obj+obX)
objY = memory.readlong(obj+obY)
objVX = memory.readwordsigned(obj+obVelX)
objVY = memory.readwordsigned(obj+obVelY)
objNVX = memory.readwordsigned(obj+0x32)
objNVY = memory.readwordsigned(obj+0x34)
objWait = memory.readword(obj+0x36) - 1
end
-- Simulate wait if needed
if objWait > 0 then
vbla = vbla + objWait + 1
end
-- Simulate falling
while objVX == 0 do
vbla = vbla + 1
objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla =
animal_fall(objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla)
end
-- Compute time to get offscreen
local le = SHIFT(capX - 160 - objW, -8)
local re = SHIFT(capX + 160 + objW, -8)
-- Lets use relative position here.
objX = SHIFT(objX, 8)
local lastvbla = vbla
if objVX > 0 then
vbla = vbla + math.abs(math.floor((re - objX + objVX - 1) / objVX)) + 1
else
vbla = vbla + math.abs(math.floor((objX - le - objVX - 1) / -objVX)) + 1
end
return objVX, vbla
end
function print_animal_objects(vbla)
local animal_list = enum_animals()
if #animal_list > 0 then
row = 4
gui.drawtext(4, row, "Spawned animals:")
row = row + 8
gui.drawtext(12, row, "Animal Wait D Exit at")
row = row + 8
local capX = memory.readword(capsule_switch+obX)
local list1 = {}
local list2 = {}
for i,m in pairs(animal_list) do
local ty = memory.readbyte(m+0x30)
local delay = memory.readword(m+0x36)
local vx, endvbla = simulate_animal_delay_fall_escape(m, nil, nil, nil, nil, nil,
nil, nil, vbla, capX)
local dir = (vx > 0 and ">") or "<"
local endpt = movie.framecount() + endvbla - vbla + 1
table.insert((delay == 0 and list2) or list1, {endpt, function(rw)
gui.drawtext(12, rw,
string.format("%-8s %4d %s %7d", animals[ty][1], delay, dir,
endpt), animals[ty][2])
end})
end
table.sort(list1, function(item1, item2)
return item1[1] < item2[1]
end)
table.sort(list2, function(item1, item2)
return item1[1] < item2[1]
end)
for i,m in pairs(list1) do
m[2](row)
row = row + 8
end
gui.drawtext(12, row, "========= Free =========")
row = row + 8
for i,m in pairs(list2) do
m[2](row)
row = row + 8
end
end
end
function predict_animals()
-- Try to find a capsule switch
capsule_switch = find_capsule()
if capsule_switch == nil then
-- If it was not found, so update variables and leave
last_seed = memory.readlong(v_random)
last_vbla = memory.readbyte(v_vbla_byte)
return
end
-- Update random seed to RAM value
seed = memory.readlong(v_random)
-- Capsule switch's routine counter
local rout = memory.readbyte(capsule_switch+obRoutine)
-- V-Int counter, used for pseudo-random numbers
local vbla = memory.readbyte(v_vbla_byte)
-- Sometimes, the V-Int counter does not update; doing this "manual"
-- update corrects the issue, which causes misprediction.
if last_vbla == vbla then
vbla = vbla + 1
end
local savevbla = vbla
-- Special case:
if rout == 4 then
-- Switch not pressed
gui.drawtext(4, 4, "Capsule not broken yet.")
last_seed = seed
last_vbla = savevbla
return
end
local row = 0
-- Draw container box.
gui.drawbox(0, 0, 223, 28*8-1, { 0, 0, 0, 128}, {255, 255, 255, 255})
if rout == 0xE then
-- All animals spawned and moving to leave screen
gui.drawtext(128, 12, "Waiting for animals\nto leave screen.", {255, 255, 0, 255})
print_animal_objects(vbla)
last_seed = seed
last_vbla = savevbla
return
end
-- Get animal list for current zone.
local zone = memory.readbyte(v_zone)
local types = typelist[zone]
if rout <= 0xA then
-- Explosions are being generated. There are two parts to this:
-- Part 1: counting the explosions
local explosions_left = 0
-- Time left for explosion generation
local countdown = memory.readword(capsule_switch + obTimeFrame)
local dt = 7 - AND(vbla, 7)
for i = 1, countdown + 1 do
-- Has an explosion been generated?
if AND(vbla + i - 1, 7) == 0 then
-- If yes, we need to account for it: each explosion needs
-- one random number.
explosions_left = explosions_left + 1
local rand = GetRandom()
end
-- Fake frame advance.
end
-- If we have explosions left, we have opportunities for manipulating
-- the animal pattern.
if explosions_left > 0 then
gui.drawtext(112, 180,
string.format("Next oportunity to change\nanimal pattern: %d frame%s",
dt, (dt == 1 and "") or "s"), {255, 255, 0, 255})
end
row = 4
gui.drawtext(4, row, string.format("Explosions left: %d", explosions_left))
row = row + 16
gui.drawtext(4, row, "Initial animals:")
row = row + 8
gui.drawtext(12, row, "Animal Wait D Exit at")
row = row + 8
-- Build sequence of random numbers to be used for the initial batch
-- of animals. Do this so we can print them in ascending order of delay.
local randseq = {}
for i = 1, 8 do
local rand = GetRandom()
table.insert(randseq, 1, rand)
end
local capX = memory.readword(capsule_switch+obX)
local capY = memory.readword(capsule_switch+obY) + 0x20
-- Part 2: Initial animals
for i = 1, 8 do
-- Random number determines kind of animal.
local rand = randseq[1]
table.remove(randseq, 1)
local ty = types[AND(rand, 1)]
local objX = SHIFT(capX + 0x1C - 7 * i, -16)
local objY = SHIFT(capY, -16)
local delay = countdown + 90 + 8 * i
local vx, endvbla = simulate_animal_delay_fall_escape(nil, objX, objY, 0, -0x400,
animals[ty][3], animals[ty][4],
delay, vbla, capX)
local dir = (vx > 0 and ">") or "<"
gui.drawtext(12, row,
string.format("%-8s %4d %s %7d", animals[ty][1], delay, dir,
movie.framecount() + endvbla - vbla),
animals[ty][2])
row = row + 8
end
end
print_animal_objects(vbla)
if rout <= 0xC then
-- Single animals are being generated.
-- Part 3: Pseudo-random animal generation
row = 4
local saverow = row
row = row + 8
gui.drawtext(124, row, "Animal Spawn D Exit at")
row = row + 8
local animals_left = 0
local countdown = (rout < 0xC and memory.readword(capsule_switch+obTimeFrame)) or 0
local dt = 7 - AND(vbla + countdown, 7)
local timer = (rout == 0xC and memory.readword(capsule_switch+obTimeFrame)) or 150
local capX = memory.readword(capsule_switch+obX)
local capY = memory.readword(capsule_switch+obY) + ((rout < 0xC and 0x20) or 0)
for i = 1, timer + 1 do
if AND(vbla + countdown + i - 1, 7) == 0 then
-- First random number is used to determine the position
-- offset from the switch.
local rand = AND(GetRandom(), 0x1F) - 6
local pos = (seed > 0 and rand) or -rand
-- Second random number determines animal type.
rand = GetRandom()
local ty = types[AND(rand, 1)]
if countdown + i - 2 > 0 then
local objX = SHIFT(capX + pos, -16)
local objY = SHIFT(capY, -16)
local delay = countdown + i + 12
local vx, endvbla = simulate_animal_delay_fall_escape(nil, objX, objY, 0, -0x400,
animals[ty][3], animals[ty][4],
delay, vbla, capX)
local dir = (vx > 0 and ">") or "<"
gui.drawtext(124, row,
string.format("%-8s %4d %s %7d", animals[ty][1], delay-14, dir,
movie.framecount() + endvbla - vbla + (rout < 0xC and 1 or 0)),
animals[ty][2])
row = row + 8
animals_left = animals_left + 1
end
end
end
-- If we have animals still to be generated, and we are not in the
-- process of making explosions, we have an opportunity to change
-- if the next animal will be loaded or not.
if rout == 0xC and animals_left > 0 then
gui.drawtext(112, 180,
string.format("Next oportunity to prevent\nanimal spawn: %d frame%s",
dt, (dt == 1 and "") or "s"), {255, 255, 0, 255})
end
-- If there were any animals left, say so
if animals_left > 0 then
gui.drawtext(116, saverow, string.format("Animal spawns: %d left", animals_left))
end
end
last_seed = memory.readlong(v_random)
last_vbla = savevbla
return
end
gens.registerafter(predict_animals)
savestate.registerload(predict_animals)