Skip to content

Instantly share code, notes, and snippets.

@bbbradsmith
Last active July 17, 2020 13:51
Show Gist options
  • Save bbbradsmith/46e53f67f681814337001445539995ad to your computer and use it in GitHub Desktop.
Save bbbradsmith/46e53f67f681814337001445539995ad to your computer and use it in GitHub Desktop.
-- Karnov Inspector lua script for FCEUX
-- rainwarrior 2018 #Karnovember
-- http://rainwarrior.ca
-- Press H for help
VERSION = "v1.0"
-- Modes and input handling to toggle them
MODE_HELP = 1
MODE_STAT = 2
MODE_TILE = 3
MODE_HITS = 4
MODE_HURT = 5
MODE_BOMB = 6
MODE_HIDE = 7
MODE_MAX = MODE_HIDE
SHOW_MAPPER = false -- banking info for debugging the mapper
COLOR_JIN = "#00FF00FF" -- colour of Jinborov's coordinate crosshairs
mode = {}
mode[MODE_HELP] = true
mode[MODE_STAT] = true
mode[MODE_TILE] = true
mode[MODE_HITS] = true
mode[MODE_HURT] = true
mode[MODE_BOMB] = true
mode[MODE_HIDE] = false
key_last = input.get()
function handle_input()
key = input.get()
if key.H and not key_last.H then mode[MODE_HELP] = not mode[MODE_HELP] end
if key.J and not key_last.J then mode[MODE_STAT] = not mode[MODE_STAT] end
if key.K and not key_last.K then mode[MODE_TILE] = not mode[MODE_TILE] end
if key.L and not key_last.L then mode[MODE_HITS] = not mode[MODE_HITS] end
if key.V and not key_last.V then mode[MODE_HURT] = not mode[MODE_HURT] end
if key.B and not key_last.B then mode[MODE_BOMB] = not mode[MODE_BOMB] end
if key.N and not key_last.N then mode[MODE_HIDE] = not mode[MODE_HIDE] end
key_last = key
end
-- Bank tracker for Mapper 206 (or MMC3)
bank = { 0, 0, 0, 0, 0, 0, 0, 0 }
bank_select = 1
function bank_write(a,s,v)
-- note: the v parameter is not supported in FCEUX 2.2.3 (need r3266 or later)
-- but is not important for the inspector.
if AND(a,1) == 0 then
bank_select = AND(v,7) + 1
else
bank[bank_select] = v
end
end
if SHOW_MAPPER then
memory.registerwrite(0x8000,0x7FFF,bank_write)
end
-- End Boss detection
function during_endboss()
if memory.readbyte(0x61) ~= 8 then return false end -- stage 9
if memory.readbyte(0x9F) ~= 0x90 then return false end -- scroll
if memory.readbyte(0xA0) ~= 0x00 then return false end -- scroll
return true
end
-- Help
function mode_help()
if not mode[MODE_HELP] then return end
local C1 = "#FFFFFFFF"
local C2 = "#00000044"
local l=1
gui.text(8*1,8*1,"Karnov Inspector " .. VERSION .. " -- rainwarrior 2018",C1,C2)
l=3
gui.text(8*2-1,8*l," - Help",C1,C2) ; l=l+1
gui.text(8*2-1,8*l," - Stats",C1,C2) ; l=l+1
gui.text(8*2-1,8*l," - Tiles",C1,C2) ; l=l+1
gui.text(8*2-1,8*l," - Touch Hitbox",C1,C2) ; l=l+1
gui.text(8*2-1,8*l," - Shoot Hitbox",C1,C2) ; l=l+1
gui.text(8*2-1,8*l," - Bomb Triggers",C1,C2) ; l=l+1
gui.text(8*2-1,8*l," - Hide Screen",C1,C2) ; l=l+1
mkeys = {"H","J","K","L","V","B","N"}
l=3
for i=1,MODE_MAX do
c = "#888888FF"
if mode[i] then c = "#FFFF00FF" end
gui.text(8*1,8*l,mkeys[i],c,C2) ; l=l+1
end
end
-- Stats
function mode_stat()
if not mode[MODE_STAT] then return end
-- Jinborov's on-screen coordinates
jin_x = memory.readbyte(0x63)
jin_y = memory.readbyte(0x64)
-- $0C/0D are the scroll X/Y parameters, $0E seems to be a coarse Y scroll
scroll_x = memory.readbyte(0x0C)
scroll_y = (memory.readbyte(0x0E) * 240 / 16) + memory.readbyte(0x0D)
local C1 = "#00FFFFFF"
local C2 = "#00000044"
-- 9F/A0 are tile scroll coordinates
tile_x = memory.readbyte(0x9F)
tile_y = memory.readbyte(0xA0)
-- show position stats
gui.text(8,8*28,string.format("J: %3d/%3d S: %3d/%3d M: %02X/%02X",
jin_x,jin_y,
scroll_x,scroll_y,
tile_x,tile_y
),C1,C2)
if SHOW_MAPPER then
gui.text(8,8*29,string.format("MAPPER: %02X %02X %02X %02X %02X %02X %02X %02X",bank[1],bank[2],bank[3],bank[4],bank[5],bank[6],bank[7],bank[8]),C1,C2)
end
-- Jinborov's foot is his "real" coordinate
gui.line(jin_x-1,jin_y ,jin_x+1,jin_y ,COLOR_JIN)
gui.line(jin_x ,jin_y ,jin_x ,jin_y+3 ,COLOR_JIN)
end
-- Tiles
-- 00 empty
-- 01 solid
-- 02 solid-top
-- 03 solid-top-sides
-- 08 natural ladder
-- 09 placed ladder
-- 0A solid-bottom (water surface)
-- 0B underwater
-- 0C exploded
-- 10 spawn mask stage 1
-- 11 spawn wing stage 1
-- 18 mask opportunity
-- 19 mask opportunity
-- 1C wing opportunity
-- 20 spawn orb
-- 21 spawn orb
-- 22 spawn green sabre attackers
-- 23 spawn blue knights
-- 24 spawn starman
-- 25 spawn birds
-- 26 spawn bats
-- 27 spawn jumper (descending)
-- 28 spawn jumper (running, stage 4)
-- 28 spawn centipede lamia
-- 29 spawn fish head
-- 2A spawn jumper (running, stage 7)
-- 2B spawn mummy/floor (stage 7)
-- 2C trapdoor (stage 7)
-- 2D final boss side door
-- 2F final boss
function mode_tile()
if not mode[MODE_TILE] then return end
-- gui.box(0,0,255,239,"#000000FF","#000000FF") -- to test box colours more easily
-- visual offset from current scroll position
off_x = AND(memory.readbyte(0x0C),0x0F)
off_y = AND(memory.readbyte(0x0D),0x0F)
-- $C4 indicates scrolling: A0 = left, B0 = right, 80 = up, 90 = down, 00 = none
-- $92 and $9F seem to indicate X index shift, $90 seems to indicate it for Y
shift_tx = memory.readbyte(0x9F) -- use this when moving right or stopped
if AND(memory.readbyte(0xC4),0xF0) == 0xA0 then shift_tx = memory.readbyte(0x92) end -- use this when moving left
shift_ty = memory.readbyte(0x93) + 0 -- +11 is equivalent to a -16 on screen Y
if AND(memory.readbyte(0xC4),0xF0) == 0x90 then shift_ty = shift_ty + 11 end
-- draw the tiles currently stored in 16x12 buffer at $040C
for y=0,11 do
for x=0,15 do
tx = AND((x + shift_tx),0x0F)
ty = (y + shift_ty) % 12
tile = memory.readbyte(0x040C + tx + (ty * 16))
if tile ~= 0 then
bx = (x*16) - off_x
by = (y*16) - off_y
ct = "#FFFFFFAA"
c = "#FF00FF88"
if (tile == 0x0B) then c = "#0000FF00" ; ct = "#FFFFFF22" -- underwater
elseif (tile < 0x10) then c = "#0000FF88" -- collision
elseif (tile < 0x1A) then c = "#00FF0088" -- secret trigger
elseif (tile == 0x1E) then c = "#00FF0088" -- spawn orb
elseif (tile == 0x1F) then c = "#00FF0088" -- spawn orb
elseif (tile < 0x20) then c = "#FFFF0088" -- item opportunity
elseif (tile < 0x22) then c = "#00FF0088" -- spawn orb
elseif (tile < 0x30) then c = "#FF000088" -- enemy spawn
elseif (tile >= 0x80) then c = "#FFFFFF44" ; ct = "#FFFFFF88" -- pickup
end
gui.box(bx,by,bx+15,by+15,c,c)
gui.text(bx+2,by+4,string.format("%02X",tile),ct,"#00000000")
end
end
end
-- coordinates to show interaction with tiles
jin_x = memory.readbyte(0x63)
jin_y = memory.readbyte(0x64)
-- midpoint (shows left/right extent on ground)
gui.line(jin_x-3,jin_y-16,jin_x+3,jin_y-16,COLOR_JIN)
gui.line(jin_x ,jin_y-19,jin_x ,jin_y-13,COLOR_JIN)
-- foot (where you touch the ground)
gui.line(jin_x-1,jin_y ,jin_x+1,jin_y ,COLOR_JIN)
gui.line(jin_x ,jin_y ,jin_x ,jin_y+3 ,COLOR_JIN)
-- foot 2 (where you can push over an edge in midair)
gui.line(jin_x-3,jin_y-4 ,jin_x+3,jin_y-4 ,COLOR_JIN)
-- head (where your jump stops if you hit a ceiling)
gui.line(jin_x-1,jin_y-24,jin_x+1,jin_y-24,COLOR_JIN)
gui.line(jin_x ,jin_y-27,jin_x ,jin_y-24,COLOR_JIN)
end
-- Bombs
-- this is just hard-coded, it is not part of the level data
-- each level has a custom routine to check against bombs
-- The routines are in a jump table at bank B:AE93 (following a JSR at B:AE90)
bomb_table = {
{}, -- routine B:AF3C, no bombs
{},
{},
{0x12,0x12,0x13,0x14,0x15,0x14,0x17,0x14,0x17,0x16,
0x19,0x16,0x1B,0x16,0x4E,0x0F,0x4E,0x11,0x4D,0x17,
0x4B,0x17,0x4F,0x17,0x51,0x17,0x53,0x17,0x5C,0x0E,
0x5C,0x10,0x38,0x0F}, -- routine B:AEA5 uses table at B:AEC2
{},
{0x35,0x23,0x36,0x23,0x37,0x23}, -- routine B:AEE4
{},
{0x51,0x0B,0x5B,0x08,0x5C,0x08,0x5D,0x08,0x5E,0x08}, -- routine B:AEF5 uses table at B:AF12
{0x5F,0x0C}, -- routine B:AF1C
}
function mode_bomb()
if not mode[MODE_BOMB] then return end
-- visual offset from current scroll position
off_x = AND(memory.readbyte(0x0C),0x0F)
off_y = AND(memory.readbyte(0x0D),0x0F)
-- current stage
stage = memory.readbyte(0x61) + 1
if stage > table.getn(bomb_table) then return end -- shouldn't happen
-- bomb coordinate offset
coord_off_x = memory.readbyte(0x9F)
coord_off_y = memory.readbyte(0xA0) + 1
-- correct offset if scrolling
if AND(memory.readbyte(0xC4),0xF0) == 0xA0 then coord_off_x = coord_off_x - 1 end
if AND(memory.readbyte(0xC4),0xF0) == 0x80 then coord_off_y = coord_off_y - 1 end
for y=0,11 do
for x=0,15 do
bx = (x*16) - off_x
by = (y*16) - off_y
coord_x = AND(x + coord_off_x, 255)
coord_y = AND(y + coord_off_y, 255)
bomb = false
for i=1,table.getn(bomb_table[stage]),2 do
if coord_x == bomb_table[stage][i+0] and coord_y == bomb_table[stage][i+1] then
bomb = true
end
end
if bomb then
cb = "#FFFF00CC"
ct = "#FF0000CC"
gui.box(bx,by,bx+15,by+15,"#00000000",cb)
gui.text(bx+2,by+7,"B",cb,"#00000000")
end
end
end
end
-- Hitboxes
HIT_Y = -16 -- offset Jin's vertical hit visualization for convenience
function hitbox_enemy(i)
-- based on collision routine at C969
et = memory.readbyte(0x650+(i*16))
if AND(et,0x80) == 0 then return end -- enemy not active
et = AND(et,0x3F) -- enemy type
es = memory.readbyte(0x651+(i*16)) -- enemy status
ex = memory.readbyte(0x652+(i*16)) -- enemy X
ey = memory.readbyte(0x653+(i*16)) -- enemy Y
bys = memory.readbyte(0xCA50+(et*4)) -- enemy hitbox Y size
bxr = memory.readbyte(0xCA51+(et*4)) -- enemy hitbox X right
bxl = memory.readbyte(0xCA52+(et*4)) -- enemy hitbox X left
bxs = memory.readbyte(0xCA53+(et*4)) -- enemy hitbox X size
jin_state = AND(memory.readbyte(0x62),0x1F) -- jin's state
if AND(es,0x0F) < 4 then return end -- not in a colliding state ?
-- X hitbox
tx1 = AND(ex + bxr, 0xFF)
if AND(es,0x80) ~= 0 then tx1 = AND(ex + bxl, 0xFF) end -- facing left
tx0 = tx1 - (bxs-1)
-- Y hitbox
ty1 = AND(ey + 24, 0xFF)
ty0 = ty1 - (bys-1)
if jin_state == 6 or jin_state == 7 then -- ducking
ty1 = AND(ey + 16, 0xFF)
ty0 = ty1 - (bys-9)
end
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
--gui.text(ex-16,ey-16,string.format("%02X %02X",et,es),"#FFFFFF88","#00000088")
end
function hitbox_dragon()
if during_endboss() then return end -- dragon variables reused by end boss
-- based on collision routine at B:AF6F
if AND(memory.readbyte(0x6CF),0x8F) ~= 0x80 then return end -- dragon state?
if memory.readbyte(0x6DD) ~= 0 then return end -- dragon high X (offscreen)
if memory.readbyte(0x6DE) ~= 0 then return end -- dragon high Y (offscreen)
ex = memory.readbyte(0x6DB) -- dragon X
ey = memory.readbyte(0x6DC) -- dragon Y
tx1 = AND(ex + 0x0C, 0xFF)
ty1 = AND(ey + 0x10, 0xFF)
tx0 = tx1 - (0x18-1)
ty0 = ty1 - (0x18-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
end
function hitbox_endboss()
-- based on collision routine at C:A97D
dc = AND(memory.readbyte(0x6CF),0x8F) -- dragon state?
if dc < 0x82 or dc > 0x85 then return end
ex = memory.readbyte(0x6DA)
ey = memory.readbyte(0x6DB)
tx1 = AND(ex + 0x04, 0xFF)
ty1 = AND(ey + 0x18, 0xFF)
tx0 = tx1 - (0x08-1)
ty0 = ty1 - (0x30-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
end
function hitbox_endboss_fireball()
-- based on collision routine at C:ACE2
if AND(memory.readbyte(0x706),0x80) == 0 then return end -- fireball on
ex = memory.readbyte(0x707)
ey = memory.readbyte(0x708)
tx1 = AND(ex + 0x08, 0xFF)
ty1 = AND(ey + 0x18, 0xFF)
tx0 = tx1 - (0x10-1)
ty0 = ty1 - (0x20-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
end
function hitbox_orb()
-- based on routine at B:AD61
if AND(memory.readbyte(0xD8),0x80) == 0 then return end -- orb inactive
ex = memory.readbyte(0xD9)
ey = memory.readbyte(0xDA)
tx0 = AND(ex - 0x0A, 0xFF)
ty0 = AND(ey + 0x10, 0xFF)
tx1 = tx0 + (0x14-1)
ty1 = ty0 + (0x10-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#00FF0022","#00FF0088")
gui.line(ex-1,ey,ex+1,ey,"#00FF00FF")
gui.line(ex,ey-1,ex,ey+1,"#00FF00FF")
end
function mode_hits()
if not mode[MODE_HITS] then return end
for i=0,5 do
hitbox_enemy(i)
end
hitbox_dragon()
if during_endboss() then
hitbox_endboss()
hitbox_endboss_fireball()
end
hitbox_orb()
-- midpoint to make hitbox interaction clear
jin_x = memory.readbyte(0x63)
jin_y = memory.readbyte(0x64)
gui.line(jin_x-3,jin_y+HIT_Y ,jin_x+3,jin_y+HIT_Y ,COLOR_JIN)
gui.line(jin_x ,jin_y+HIT_Y-3,jin_x ,jin_y+HIT_Y+3,COLOR_JIN)
-- I believe the shield status is bit 4 of $65
-- but the shield does not seem to have a hitbox;
-- instead it will cancel damage if you're facing
-- the opposite direction of a bullet.
end
-- Hurtboxes
COLOR_SHOT = "#FFFF00FF"
HURT_Y = 0x0C -- offset hurt locations for convenience
-- List of hurtbox heights for enemies that have them.
-- This is a parameter loaded to A before callilng CC8F,
-- which is loaded by different code for different enemies,
-- so I had to find them all and make this table.
-- (Actual height is 0x12 + 16 * hurt_size.)
hurt_size = {}
hurt_size[0x00] = 0x00
hurt_size[0x01] = 0x01
hurt_size[0x02] = 0x00
hurt_size[0x03] = 0x01
hurt_size[0x04] = 0x01
hurt_size[0x06] = 0x01
hurt_size[0x07] = 0x01
hurt_size[0x08] = 0x01
hurt_size[0x09] = 0x04
hurt_size[0x0A] = 0x01
hurt_size[0x0B] = 0x01
hurt_size[0x0C] = 0x01
hurt_size[0x0D] = 0x02
hurt_size[0x0E] = 0x00
hurt_size[0x0F] = 0x00
hurt_size[0x10] = 0x00
hurt_size[0x11] = 0x00
hurt_size[0x12] = 0x01
hurt_size[0x13] = 0x01
hurt_size[0x14] = 0x01
hurt_size[0x15] = 0x00
hurt_size[0x16] = 0x01
hurt_size[0x17] = 0xFF -- dinosaur
hurt_size[0x19] = 0xFF -- lamia
hurt_size[0x1A] = 0x00
hurt_size[0x1B] = 0x01
hurt_size[0x1D] = 0x01
hurt_size[0x1E] = 0x01
hurt_size[0x1F] = 0x01
hurt_size[0x21] = 0xFF -- ghidorah
hurt_size[0x22] = 0x01
hurt_size[0x23] = 0x00
hurt_size[0x24] = 0xFF -- lamia copy (stage 9)
hurt_size[0x26] = 0x04
hurt_size[0x28] = 0x00
hurt_size[0x29] = 0x00
hurt_size[0x34] = 0x00
hurt_size[0x3A] = 0x01
-- logs parameter to CC8F function, determining hurtbox height
LOG_HURT_SIZE = false
log_hurt_size = {}
function log_hurt_size_()
a = memory.getregister("a") -- hurt size
x = memory.readbyte(0x44) -- enemy index * 16
et = AND(memory.readbyte(0x650+x),0x3F) -- enemy type
hl = log_hurt_size[et]
if hl ~= nil then
for i,v in ipairs(hl) do
if v == a then return end
end
table.insert(hl,a)
else
hl = {a}
log_hurt_size[et] = hl
end
s = string.format("hurt_size[0x%02X] =", et)
for i,v in ipairs(hl) do
s = s .. string.format(" 0x%02X",v)
end
emu.print(s)
end
if LOG_HURT_SIZE then memory.registerexec(0xCC8F,log_hurt_size_) end
function hurt_enemy(i)
-- based on routine at CC8F
et = memory.readbyte(0x650+(i*16))
if AND(et,0x80) == 0 then return end -- enemy not active
et = AND(et,0x3F) -- enemy type
es = memory.readbyte(0x651+(i*16)) -- enemy status
ex = memory.readbyte(0x652+(i*16)) -- enemy X
ey = memory.readbyte(0x653+(i*16)) -- enemy Y
ed = memory.readbyte(0x655+(i*16)) -- enemy damage
if hurt_size[et] == nil then return end -- not a hurting enemy
if AND(es,0x0F) < 0x04 then return end -- not yet hurtable
if (et == 0x14) and AND(es,0x0F) == 0x04 then return end -- mimic enemy hiding
hr = 0x12 + (16 * hurt_size[et])
hp = memory.readbyte(0xCDAD + et) + 1 - AND(ed,0x1F)
if et == 0x17 then -- dinosaur boss is special
hr = 0x10
ey = 0x0C + ey - 0x38
ex = ex + 0x08
if AND(es,0x80) ~= 0 then ex = ex - 0x10 end -- facing
elseif et == 0x19 or et == 0x24 then -- lamia boss
hr = 0x10
ey = 0x0C + ey - 0x28
if AND(es,0x0F) == 0x07 then ey = ey - 0x20 end -- standing
elseif et == 0x21 then -- ghidorah boss
hr = 0x10
ey = 0x0C + ey - 0x40
if AND(es,0x80) ~= 0 then ex = ex + 0x08 end -- facing
end
tx1 = AND(ex+0x04, 0xFF)
ty1 = AND(ey-0x0C, 0xFF)
tx0 = tx1-(0x08-1)
ty0 = ty1-(hr-1)
ex = memory.readbyte(0x652+(i*16)) -- restore ex if overwritten above
ey = memory.readbyte(0x653+(i*16)) -- restory ey if overwritten above
gui.box(tx0,ty0+HURT_Y,tx1,ty1+HURT_Y,"#00FFFF22","#00FFFFCC")
gui.line(ex-1,ey,ex+1,ey,"#00FFFFFF")
gui.line(ex,ey-1,ex,ey+1,"#00FFFFFF")
gui.text(ex+2,ey+2,string.format("%d",hp),"#00FFFFCC","#00000044")
-- bomb hitbox if active
if AND(memory.readbyte(0xC0),0x80) == 0 then return end
bx0 = ex-0x10
by0 = ey
bx1 = bx0 + (0x20-1)
by1 = by0 + (0x10-1)
gui.box(bx0,by0,bx1,by1,"#FFFF0022","#FFFF0088")
end
function hurt_enemies()
for i=0,5 do
hurt_enemy(i)
end
-- bomb active
if AND(memory.readbyte(0xC0),0x80) ~= 0 then
bx = memory.readbyte(0xC1)
by = memory.readbyte(0xC2)
gui.line(bx-1,by,bx+1,by,"#FFFF00FF")
gui.line(bx,by-1,bx,by+1,"#FFFF00FF")
end
end
function hurt_shot()
for i=0,8 do
ss = memory.readbyte(0x6E+(i*3))
sx = memory.readbyte(0x6F+(i*3))
sy = memory.readbyte(0x70+(i*3)) + HURT_Y
if AND(ss,0x80) == 0x80 then
gui.line(sx-1,sy ,sx+1,sy ,COLOR_SHOT)
gui.line(sx ,sy-1,sx ,sy+1,COLOR_SHOT)
end
end
end
function hurt_dragon()
if during_endboss() then return end -- dragon variables reused by end boss
-- based on collision routine at B:AFA6
if AND(memory.readbyte(0x6CF),0x8F) ~= 0x80 then return end -- dragon state?
if memory.readbyte(0x6DD) ~= 0 then return end -- dragon high X (offscreen)
if memory.readbyte(0x6DE) ~= 0 then return end -- dragon high Y (offscreen)
ex = memory.readbyte(0x6DB) -- dragon X
ey = memory.readbyte(0x6DC) -- dragon Y
hp = 0x05 - memory.readbyte(0x6D0)
tx1 = AND(ex + 0x04, 0xFF)
ty1 = AND(ey - 0x0C, 0xFF)
tx0 = tx1 - (0x08-1)
ty0 = ty1 - (0x10-1)
gui.box(tx0,ty0+HURT_Y,tx1,ty1+HURT_Y,"#00FFFF22","#00FFFFCC")
gui.line(ex-1,ey,ex+1,ey,"#00FFFFFF")
gui.line(ex,ey-1,ex,ey+1,"#00FFFFFF")
gui.text(ex+2,ey+2,string.format("%d",hp),"#00FFFFCC","#00000044")
end
endboss_sequence = { 0,1,2,0,2,0,1,2 } -- table at C:ACB3
function hurt_endboss()
-- based on C:A9CA
dc = AND(memory.readbyte(0x6CF),0x8F) -- dragon state?
if dc < 0x82 or dc > 0x85 then return end
ds = memory.readbyte(0x6D0) % 8 -- sequence
hp = 10 - memory.readbyte(0x6D2 + endboss_sequence[ds+1])
ex = memory.readbyte(0x6DA)
ey = memory.readbyte(0x6DB)
tx1 = AND(ex + 0x08, 0xFF)
ty1 = AND(ey - 0x08, 0xFF)
tx0 = tx1 - (0x18-1)
ty0 = ty1 - (0x20-1)
gui.box(tx0,ty0+HURT_Y,tx1,ty1+HURT_Y,"#00FFFF22","#00FFFFCC")
gui.line(ex-1,ey,ex+1,ey,"#00FFFFFF")
gui.line(ex,ey-1,ex,ey+1,"#00FFFFFF")
gui.text(ex+2,ey+2,string.format("%d",hp),"#00FFFFCC","#00000044")
end
function mode_hurt()
if not mode[MODE_HURT] then return end
hurt_enemies()
hurt_dragon()
if during_endboss() then
hurt_endboss()
end
hurt_shot()
end
-- Hide Screen
function mode_hide()
if not mode[MODE_HIDE] then return end
gui.box(0,0,255,191,"#555555FF","#555555FF") -- grey screen for testing
end
-- Main loop
function modes()
handle_input()
mode_hide()
mode_tile()
mode_bomb()
mode_hits()
mode_hurt()
mode_stat()
mode_help()
end
memory.registerexec(0xC1DB, modes) -- NMI
while (true) do
FCEU.frameadvance()
end
-- end of file
-- Karnov Inspector lua script for Bizhawk
-- rainwarrior 2018 #Karnovember
-- http://rainwarrior.ca
-- Press H for help
VERSION = "v1.0"
-- helper functions for FCEUX script conversion
function col(c)
c = tonumber(string.sub(c,2,9),16)
return bit.rshift(c,8) + bit.lshift(bit.band(c,0xFF),24)
end
function gui_line(x0,y0,x1,y1,c)
gui.drawLine(x0,y0,x1,y1,col(c))
end
function gui_text(x,y,s,c0,c1)
gui.pixelText(x,y,s,col(c0),col(c1))
end
function gui_box(x0,y0,x1,y1,c0,c1)
gui.drawBox(x0,y0,x1,y1,null,col(c0))
gui.drawBox(x0,y0,x1,y1,col(c1),null)
end
function AND(a,b)
return bit.band(a,b)
end
function table_getn(t)
return #t
end
gui.line = gui_line
gui.text = gui_text
gui.box = gui_box
table.getn = table_getn
-- Modes and input handling to toggle them
MODE_HELP = 1
MODE_STAT = 2
MODE_TILE = 3
MODE_HITS = 4
MODE_HURT = 5
MODE_BOMB = 6
MODE_HIDE = 7
MODE_MAX = MODE_HIDE
SHOW_MAPPER = false -- banking info for debugging the mapper
COLOR_JIN = "#00FF00FF" -- colour of Jinborov's coordinate crosshairs
mode = {}
mode[MODE_HELP] = true
mode[MODE_STAT] = true
mode[MODE_TILE] = true
mode[MODE_HITS] = true
mode[MODE_HURT] = true
mode[MODE_BOMB] = true
mode[MODE_HIDE] = false
key_last = input.get()
function handle_input()
key = input.get()
if key.H and not key_last.H then mode[MODE_HELP] = not mode[MODE_HELP] end
if key.J and not key_last.J then mode[MODE_STAT] = not mode[MODE_STAT] end
if key.K and not key_last.K then mode[MODE_TILE] = not mode[MODE_TILE] end
if key.L and not key_last.L then mode[MODE_HITS] = not mode[MODE_HITS] end
if key.V and not key_last.V then mode[MODE_HURT] = not mode[MODE_HURT] end
if key.B and not key_last.B then mode[MODE_BOMB] = not mode[MODE_BOMB] end
if key.N and not key_last.N then mode[MODE_HIDE] = not mode[MODE_HIDE] end
key_last = key
end
-- End Boss detection
function during_endboss()
if memory.readbyte(0x61) ~= 8 then return false end -- stage 9
if memory.readbyte(0x9F) ~= 0x90 then return false end -- scroll
if memory.readbyte(0xA0) ~= 0x00 then return false end -- scroll
return true
end
-- Help
function mode_help()
if not mode[MODE_HELP] then return end
local C1 = "#FFFFFFFF"
local C2 = "#00000044"
local l=1
gui.text(8*1,8*1,"Karnov Inspector " .. VERSION .. " -- rainwarrior 2018",C1,C2)
l=3
gui.text(8*2-3,8*l," - Help",C1,C2) ; l=l+1
gui.text(8*2-3,8*l," - Stats",C1,C2) ; l=l+1
gui.text(8*2-3,8*l," - Tiles",C1,C2) ; l=l+1
gui.text(8*2-3,8*l," - Touch Hitbox",C1,C2) ; l=l+1
gui.text(8*2-3,8*l," - Shoot Hitbox",C1,C2) ; l=l+1
gui.text(8*2-3,8*l," - Bomb Triggers",C1,C2) ; l=l+1
gui.text(8*2-3,8*l," - Hide Screen",C1,C2) ; l=l+1
mkeys = {"H","J","K","L","V","B","N"}
l=3
for i=1,MODE_MAX do
c = "#888888FF"
if mode[i] then c = "#FFFF00FF" end
gui.text(8*1,8*l,mkeys[i],c,C2) ; l=l+1
end
end
-- Stats
function mode_stat()
if not mode[MODE_STAT] then return end
-- Jinborov's on-screen coordinates
jin_x = memory.readbyte(0x63)
jin_y = memory.readbyte(0x64)
-- $0C/0D are the scroll X/Y parameters, $0E seems to be a coarse Y scroll
scroll_x = memory.readbyte(0x0C)
scroll_y = (memory.readbyte(0x0E) * 240 / 16) + memory.readbyte(0x0D)
local C1 = "#00FFFFFF"
local C2 = "#00000044"
-- 9F/A0 are tile scroll coordinates
tile_x = memory.readbyte(0x9F)
tile_y = memory.readbyte(0xA0)
-- show position stats
gui.text(8,8*28,string.format("J: %3d/%3d S: %3d/%3d M: %02X/%02X",
jin_x,jin_y,
scroll_x,scroll_y,
tile_x,tile_y
),C1,C2)
if SHOW_MAPPER then
gui.text(8,8*29,string.format("MAPPER: %02X %02X %02X %02X %02X %02X %02X %02X",bank[1],bank[2],bank[3],bank[4],bank[5],bank[6],bank[7],bank[8]),C1,C2)
end
-- Jinborov's foot is his "real" coordinate
gui.line(jin_x-1,jin_y ,jin_x+1,jin_y ,COLOR_JIN)
gui.line(jin_x ,jin_y ,jin_x ,jin_y+3 ,COLOR_JIN)
end
-- Tiles
-- 00 empty
-- 01 solid
-- 02 solid-top
-- 03 solid-top-sides
-- 08 natural ladder
-- 09 placed ladder
-- 0A solid-bottom (water surface)
-- 0B underwater
-- 0C exploded
-- 10 spawn mask stage 1
-- 11 spawn wing stage 1
-- 18 mask opportunity
-- 19 mask opportunity
-- 1C wing opportunity
-- 20 spawn orb
-- 21 spawn orb
-- 22 spawn green sabre attackers
-- 23 spawn blue knights
-- 24 spawn starman
-- 25 spawn birds
-- 26 spawn bats
-- 27 spawn jumper (descending)
-- 28 spawn jumper (running, stage 4)
-- 28 spawn centipede lamia
-- 29 spawn fish head
-- 2A spawn jumper (running, stage 7)
-- 2B spawn mummy/floor (stage 7)
-- 2C trapdoor (stage 7)
-- 2D final boss side door
-- 2F final boss
function mode_tile()
if not mode[MODE_TILE] then return end
-- gui.box(0,0,255,239,"#000000FF","#000000FF") -- to test box colours more easily
-- visual offset from current scroll position
off_x = AND(memory.readbyte(0x0C),0x0F)
off_y = AND(memory.readbyte(0x0D),0x0F)
-- $C4 indicates scrolling: A0 = left, B0 = right, 80 = up, 90 = down, 00 = none
-- $92 and $9F seem to indicate X index shift, $90 seems to indicate it for Y
shift_tx = memory.readbyte(0x9F) -- use this when moving right or stopped
if AND(memory.readbyte(0xC4),0xF0) == 0xA0 then shift_tx = memory.readbyte(0x92) end -- use this when moving left
shift_ty = memory.readbyte(0x93) + 0 -- +11 is equivalent to a -16 on screen Y
if AND(memory.readbyte(0xC4),0xF0) == 0x90 then shift_ty = shift_ty + 11 end
-- draw the tiles currently stored in 16x12 buffer at $040C
for y=0,11 do
for x=0,15 do
tx = AND((x + shift_tx),0x0F)
ty = (y + shift_ty) % 12
tile = memory.readbyte(0x040C + tx + (ty * 16))
if tile ~= 0 then
bx = (x*16) - off_x
by = (y*16) - off_y
ct = "#FFFFFFAA"
c = "#FF00FF88"
if (tile == 0x0B) then c = "#0000FF00" ; ct = "#FFFFFF22" -- underwater
elseif (tile < 0x10) then c = "#0000FF88" -- collision
elseif (tile < 0x1A) then c = "#00FF0088" -- secret trigger
elseif (tile == 0x1E) then c = "#00FF0088" -- spawn orb
elseif (tile == 0x1F) then c = "#00FF0088" -- spawn orb
elseif (tile < 0x20) then c = "#FFFF0088" -- item opportunity
elseif (tile < 0x22) then c = "#00FF0088" -- spawn orb
elseif (tile < 0x30) then c = "#FF000088" -- enemy spawn
elseif (tile >= 0x80) then c = "#FFFFFF44" ; ct = "#FFFFFF88" -- pickup
end
gui.box(bx,by,bx+15,by+15,c,c)
gui.text(bx+2,by+4,string.format("%02X",tile),ct,"#00000000")
end
end
end
-- coordinates to show interaction with tiles
jin_x = memory.readbyte(0x63)
jin_y = memory.readbyte(0x64)
-- midpoint (shows left/right extent on ground)
gui.line(jin_x-3,jin_y-16,jin_x+3,jin_y-16,COLOR_JIN)
gui.line(jin_x ,jin_y-19,jin_x ,jin_y-13,COLOR_JIN)
-- foot (where you touch the ground)
gui.line(jin_x-1,jin_y ,jin_x+1,jin_y ,COLOR_JIN)
gui.line(jin_x ,jin_y ,jin_x ,jin_y+3 ,COLOR_JIN)
-- foot 2 (where you can push over an edge in midair)
gui.line(jin_x-3,jin_y-4 ,jin_x+3,jin_y-4 ,COLOR_JIN)
-- head (where your jump stops if you hit a ceiling)
gui.line(jin_x-1,jin_y-24,jin_x+1,jin_y-24,COLOR_JIN)
gui.line(jin_x ,jin_y-27,jin_x ,jin_y-24,COLOR_JIN)
end
-- Bombs
-- this is just hard-coded, it is not part of the level data
-- each level has a custom routine to check against bombs
-- The routines are in a jump table at bank B:AE93 (following a JSR at B:AE90)
bomb_table = {
{}, -- routine B:AF3C, no bombs
{},
{},
{0x12,0x12,0x13,0x14,0x15,0x14,0x17,0x14,0x17,0x16,
0x19,0x16,0x1B,0x16,0x4E,0x0F,0x4E,0x11,0x4D,0x17,
0x4B,0x17,0x4F,0x17,0x51,0x17,0x53,0x17,0x5C,0x0E,
0x5C,0x10,0x38,0x0F}, -- routine B:AEA5 uses table at B:AEC2
{},
{0x35,0x23,0x36,0x23,0x37,0x23}, -- routine B:AEE4
{},
{0x51,0x0B,0x5B,0x08,0x5C,0x08,0x5D,0x08,0x5E,0x08}, -- routine B:AEF5 uses table at B:AF12
{0x5F,0x0C}, -- routine B:AF1C
}
function mode_bomb()
if not mode[MODE_BOMB] then return end
-- visual offset from current scroll position
off_x = AND(memory.readbyte(0x0C),0x0F)
off_y = AND(memory.readbyte(0x0D),0x0F)
-- current stage
stage = memory.readbyte(0x61) + 1
if stage > table.getn(bomb_table) then return end -- shouldn't happen
-- bomb coordinate offset
coord_off_x = memory.readbyte(0x9F)
coord_off_y = memory.readbyte(0xA0) + 1
-- correct offset if scrolling
if AND(memory.readbyte(0xC4),0xF0) == 0xA0 then coord_off_x = coord_off_x - 1 end
if AND(memory.readbyte(0xC4),0xF0) == 0x80 then coord_off_y = coord_off_y - 1 end
for y=0,11 do
for x=0,15 do
bx = (x*16) - off_x
by = (y*16) - off_y
coord_x = AND(x + coord_off_x, 255)
coord_y = AND(y + coord_off_y, 255)
bomb = false
for i=1,table.getn(bomb_table[stage]),2 do
if coord_x == bomb_table[stage][i+0] and coord_y == bomb_table[stage][i+1] then
bomb = true
end
end
if bomb then
cb = "#FFFF00CC"
ct = "#FF0000CC"
gui.box(bx,by,bx+15,by+15,"#00000000",cb)
gui.text(bx+2,by+7,"B",cb,"#00000000")
end
end
end
end
-- Hitboxes
HIT_Y = -16 -- offset Jin's vertical hit visualization for convenience
function hitbox_enemy(i)
-- based on collision routine at C969
et = memory.readbyte(0x650+(i*16))
if AND(et,0x80) == 0 then return end -- enemy not active
et = AND(et,0x3F) -- enemy type
es = memory.readbyte(0x651+(i*16)) -- enemy status
ex = memory.readbyte(0x652+(i*16)) -- enemy X
ey = memory.readbyte(0x653+(i*16)) -- enemy Y
bys = memory.readbyte(0xCA50+(et*4)) -- enemy hitbox Y size
bxr = memory.readbyte(0xCA51+(et*4)) -- enemy hitbox X right
bxl = memory.readbyte(0xCA52+(et*4)) -- enemy hitbox X left
bxs = memory.readbyte(0xCA53+(et*4)) -- enemy hitbox X size
jin_state = AND(memory.readbyte(0x62),0x1F) -- jin's state
if AND(es,0x0F) < 4 then return end -- not in a colliding state ?
-- X hitbox
tx1 = AND(ex + bxr, 0xFF)
if AND(es,0x80) ~= 0 then tx1 = AND(ex + bxl, 0xFF) end -- facing left
tx0 = tx1 - (bxs-1)
-- Y hitbox
ty1 = AND(ey + 24, 0xFF)
ty0 = ty1 - (bys-1)
if jin_state == 6 or jin_state == 7 then -- ducking
ty1 = AND(ey + 16, 0xFF)
ty0 = ty1 - (bys-9)
end
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
--gui.text(ex-16,ey-16,string.format("%02X %02X",et,es),"#FFFFFF88","#00000088")
end
function hitbox_dragon()
if during_endboss() then return end -- dragon variables reused by end boss
-- based on collision routine at B:AF6F
if AND(memory.readbyte(0x6CF),0x8F) ~= 0x80 then return end -- dragon state?
if memory.readbyte(0x6DD) ~= 0 then return end -- dragon high X (offscreen)
if memory.readbyte(0x6DE) ~= 0 then return end -- dragon high Y (offscreen)
ex = memory.readbyte(0x6DB) -- dragon X
ey = memory.readbyte(0x6DC) -- dragon Y
tx1 = AND(ex + 0x0C, 0xFF)
ty1 = AND(ey + 0x10, 0xFF)
tx0 = tx1 - (0x18-1)
ty0 = ty1 - (0x18-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
end
function hitbox_endboss()
-- based on collision routine at C:A97D
dc = AND(memory.readbyte(0x6CF),0x8F) -- dragon state?
if dc < 0x82 or dc > 0x85 then return end
ex = memory.readbyte(0x6DA)
ey = memory.readbyte(0x6DB)
tx1 = AND(ex + 0x04, 0xFF)
ty1 = AND(ey + 0x18, 0xFF)
tx0 = tx1 - (0x08-1)
ty0 = ty1 - (0x30-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
end
function hitbox_endboss_fireball()
-- based on collision routine at C:ACE2
if AND(memory.readbyte(0x706),0x80) == 0 then return end -- fireball on
ex = memory.readbyte(0x707)
ey = memory.readbyte(0x708)
tx1 = AND(ex + 0x08, 0xFF)
ty1 = AND(ey + 0x18, 0xFF)
tx0 = tx1 - (0x10-1)
ty0 = ty1 - (0x20-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
end
function hitbox_orb()
-- based on routine at B:AD61
if AND(memory.readbyte(0xD8),0x80) == 0 then return end -- orb inactive
ex = memory.readbyte(0xD9)
ey = memory.readbyte(0xDA)
tx0 = AND(ex - 0x0A, 0xFF)
ty0 = AND(ey + 0x10, 0xFF)
tx1 = tx0 + (0x14-1)
ty1 = ty0 + (0x10-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#00FF0022","#00FF0088")
gui.line(ex-1,ey,ex+1,ey,"#00FF00FF")
gui.line(ex,ey-1,ex,ey+1,"#00FF00FF")
end
function mode_hits()
if not mode[MODE_HITS] then return end
for i=0,5 do
hitbox_enemy(i)
end
hitbox_dragon()
if during_endboss() then
hitbox_endboss()
hitbox_endboss_fireball()
end
hitbox_orb()
-- midpoint to make hitbox interaction clear
jin_x = memory.readbyte(0x63)
jin_y = memory.readbyte(0x64)
gui.line(jin_x-3,jin_y+HIT_Y ,jin_x+3,jin_y+HIT_Y ,COLOR_JIN)
gui.line(jin_x ,jin_y+HIT_Y-3,jin_x ,jin_y+HIT_Y+3,COLOR_JIN)
-- I believe the shield status is bit 4 of $65
-- but the shield does not seem to have a hitbox;
-- instead it will cancel damage if you're facing
-- the opposite direction of a bullet.
end
-- Hurtboxes
COLOR_SHOT = "#FFFF00FF"
HURT_Y = 0x0C -- offset hurt locations for convenience
-- List of hurtbox heights for enemies that have them.
-- This is a parameter loaded to A before callilng CC8F,
-- which is loaded by different code for different enemies,
-- so I had to find them all and make this table.
-- (Actual height is 0x12 + 16 * hurt_size.)
hurt_size = {}
hurt_size[0x00] = 0x00
hurt_size[0x01] = 0x01
hurt_size[0x02] = 0x00
hurt_size[0x03] = 0x01
hurt_size[0x04] = 0x01
hurt_size[0x06] = 0x01
hurt_size[0x07] = 0x01
hurt_size[0x08] = 0x01
hurt_size[0x09] = 0x04
hurt_size[0x0A] = 0x01
hurt_size[0x0B] = 0x01
hurt_size[0x0C] = 0x01
hurt_size[0x0D] = 0x02
hurt_size[0x0E] = 0x00
hurt_size[0x0F] = 0x00
hurt_size[0x10] = 0x00
hurt_size[0x11] = 0x00
hurt_size[0x12] = 0x01
hurt_size[0x13] = 0x01
hurt_size[0x14] = 0x01
hurt_size[0x15] = 0x00
hurt_size[0x16] = 0x01
hurt_size[0x17] = 0xFF -- dinosaur
hurt_size[0x19] = 0xFF -- lamia
hurt_size[0x1A] = 0x00
hurt_size[0x1B] = 0x01
hurt_size[0x1D] = 0x01
hurt_size[0x1E] = 0x01
hurt_size[0x1F] = 0x01
hurt_size[0x21] = 0xFF -- ghidorah
hurt_size[0x22] = 0x01
hurt_size[0x23] = 0x00
hurt_size[0x24] = 0xFF -- lamia copy (stage 9)
hurt_size[0x26] = 0x04
hurt_size[0x28] = 0x00
hurt_size[0x29] = 0x00
hurt_size[0x34] = 0x00
hurt_size[0x3A] = 0x01
-- logs parameter to CC8F function, determining hurtbox height
LOG_HURT_SIZE = false
log_hurt_size = {}
function log_hurt_size_()
a = memory.getregister("a") -- hurt size
x = memory.readbyte(0x44) -- enemy index * 16
et = AND(memory.readbyte(0x650+x),0x3F) -- enemy type
hl = log_hurt_size[et]
if hl ~= nil then
for i,v in ipairs(hl) do
if v == a then return end
end
table.insert(hl,a)
else
hl = {a}
log_hurt_size[et] = hl
end
s = string.format("hurt_size[0x%02X] =", et)
for i,v in ipairs(hl) do
s = s .. string.format(" 0x%02X",v)
end
emu.print(s)
end
if LOG_HURT_SIZE then memory.registerexec(0xCC8F,log_hurt_size_) end
function hurt_enemy(i)
-- based on routine at CC8F
et = memory.readbyte(0x650+(i*16))
if AND(et,0x80) == 0 then return end -- enemy not active
et = AND(et,0x3F) -- enemy type
es = memory.readbyte(0x651+(i*16)) -- enemy status
ex = memory.readbyte(0x652+(i*16)) -- enemy X
ey = memory.readbyte(0x653+(i*16)) -- enemy Y
ed = memory.readbyte(0x655+(i*16)) -- enemy damage
if hurt_size[et] == nil then return end -- not a hurting enemy
if AND(es,0x0F) < 0x04 then return end -- not yet hurtable
if (et == 0x14) and AND(es,0x0F) == 0x04 then return end -- mimic enemy hiding
hr = 0x12 + (16 * hurt_size[et])
hp = memory.readbyte(0xCDAD + et) + 1 - AND(ed,0x1F)
if et == 0x17 then -- dinosaur boss is special
hr = 0x10
ey = 0x0C + ey - 0x38
ex = ex + 0x08
if AND(es,0x80) ~= 0 then ex = ex - 0x10 end -- facing
elseif et == 0x19 or et == 0x24 then -- lamia boss
hr = 0x10
ey = 0x0C + ey - 0x28
if AND(es,0x0F) == 0x07 then ey = ey - 0x20 end -- standing
elseif et == 0x21 then -- ghidorah boss
hr = 0x10
ey = 0x0C + ey - 0x40
if AND(es,0x80) ~= 0 then ex = ex + 0x08 end -- facing
end
tx1 = AND(ex+0x04, 0xFF)
ty1 = AND(ey-0x0C, 0xFF)
tx0 = tx1-(0x08-1)
ty0 = ty1-(hr-1)
ex = memory.readbyte(0x652+(i*16)) -- restore ex if overwritten above
ey = memory.readbyte(0x653+(i*16)) -- restory ey if overwritten above
gui.box(tx0,ty0+HURT_Y,tx1,ty1+HURT_Y,"#00FFFF22","#00FFFFCC")
gui.line(ex-1,ey,ex+1,ey,"#00FFFFFF")
gui.line(ex,ey-1,ex,ey+1,"#00FFFFFF")
gui.text(ex+2,ey+2,string.format("%d",hp),"#00FFFFCC","#00000044")
-- bomb hitbox if active
if AND(memory.readbyte(0xC0),0x80) == 0 then return end
bx0 = ex-0x10
by0 = ey
bx1 = bx0 + (0x20-1)
by1 = by0 + (0x10-1)
gui.box(bx0,by0,bx1,by1,"#FFFF0022","#FFFF0088")
end
function hurt_enemies()
for i=0,5 do
hurt_enemy(i)
end
-- bomb active
if AND(memory.readbyte(0xC0),0x80) ~= 0 then
bx = memory.readbyte(0xC1)
by = memory.readbyte(0xC2)
gui.line(bx-1,by,bx+1,by,"#FFFF00FF")
gui.line(bx,by-1,bx,by+1,"#FFFF00FF")
end
end
function hurt_shot()
for i=0,8 do
ss = memory.readbyte(0x6E+(i*3))
sx = memory.readbyte(0x6F+(i*3))
sy = memory.readbyte(0x70+(i*3)) + HURT_Y
if AND(ss,0x80) == 0x80 then
gui.line(sx-1,sy ,sx+1,sy ,COLOR_SHOT)
gui.line(sx ,sy-1,sx ,sy+1,COLOR_SHOT)
end
end
end
function hurt_dragon()
if during_endboss() then return end -- dragon variables reused by end boss
-- based on collision routine at B:AFA6
if AND(memory.readbyte(0x6CF),0x8F) ~= 0x80 then return end -- dragon state?
if memory.readbyte(0x6DD) ~= 0 then return end -- dragon high X (offscreen)
if memory.readbyte(0x6DE) ~= 0 then return end -- dragon high Y (offscreen)
ex = memory.readbyte(0x6DB) -- dragon X
ey = memory.readbyte(0x6DC) -- dragon Y
hp = 0x05 - memory.readbyte(0x6D0)
tx1 = AND(ex + 0x04, 0xFF)
ty1 = AND(ey - 0x0C, 0xFF)
tx0 = tx1 - (0x08-1)
ty0 = ty1 - (0x10-1)
gui.box(tx0,ty0+HURT_Y,tx1,ty1+HURT_Y,"#00FFFF22","#00FFFFCC")
gui.line(ex-1,ey,ex+1,ey,"#00FFFFFF")
gui.line(ex,ey-1,ex,ey+1,"#00FFFFFF")
gui.text(ex+2,ey+2,string.format("%d",hp),"#00FFFFCC","#00000044")
end
endboss_sequence = { 0,1,2,0,2,0,1,2 } -- table at C:ACB3
function hurt_endboss()
-- based on C:A9CA
dc = AND(memory.readbyte(0x6CF),0x8F) -- dragon state?
if dc < 0x82 or dc > 0x85 then return end
ds = memory.readbyte(0x6D0) % 8 -- sequence
hp = 10 - memory.readbyte(0x6D2 + endboss_sequence[ds+1])
ex = memory.readbyte(0x6DA)
ey = memory.readbyte(0x6DB)
tx1 = AND(ex + 0x08, 0xFF)
ty1 = AND(ey - 0x08, 0xFF)
tx0 = tx1 - (0x18-1)
ty0 = ty1 - (0x20-1)
gui.box(tx0,ty0+HURT_Y,tx1,ty1+HURT_Y,"#00FFFF22","#00FFFFCC")
gui.line(ex-1,ey,ex+1,ey,"#00FFFFFF")
gui.line(ex,ey-1,ex,ey+1,"#00FFFFFF")
gui.text(ex+2,ey+2,string.format("%d",hp),"#00FFFFCC","#00000044")
end
function mode_hurt()
if not mode[MODE_HURT] then return end
hurt_enemies()
hurt_dragon()
if during_endboss() then
hurt_endboss()
end
hurt_shot()
end
-- Hide Screen
function mode_hide()
if not mode[MODE_HIDE] then return end
gui.box(0,0,255,191,"#555555FF","#555555FF") -- grey screen for testing
end
-- Main loop
function modes()
handle_input()
mode_hide()
mode_tile()
mode_bomb()
mode_hits()
mode_hurt()
mode_stat()
mode_help()
end
event.onmemoryexecute(modes, 0xC1DB) -- NMI
while true do
emu.frameadvance()
end
-- end of file
-- Karnov Inspector lua script for Mesen
-- rainwarrior 2018 #Karnovember
-- http://rainwarrior.ca
-- Press H for help
VERSION = "v1.0"
-- helper functions for FCEUX script conversion
function memory_readbyte(a)
return emu.read(a,emu.memType.cpu)
end
function col(c)
c = tonumber(string.sub(c,2,9),16)
return (c >> 8) | ((0xFF - (c & 0xFF)) << 24)
end
function gui_line(x0,y0,x1,y1,c)
emu.drawLine(x0,y0,x1,y1,col(c))
end
function gui_text(x,y,s,c0,c1)
emu.drawString(x,y,s,col(c0),col(c1))
end
function gui_box(x0,y0,x1,y1,c0,c1)
emu.drawRectangle(x0,y0,1+x1-x0,1+y1-y0,col(c0),true)
emu.drawRectangle(x0,y0,1+x1-x0,1+y1-y0,col(c1),false)
end
function AND(a,b)
return a & b
end
function table_getn(t)
return #t
end
memory = {}
memory.readbyte = memory_readbyte
gui = {}
gui.line = gui_line
gui.text = gui_text
gui.box = gui_box
table.getn = table_getn
-- Modes and input handling to toggle them
MODE_HELP = 1
MODE_STAT = 2
MODE_TILE = 3
MODE_HITS = 4
MODE_HURT = 5
MODE_BOMB = 6
MODE_HIDE = 7
MODE_MAX = MODE_HIDE
SHOW_MAPPER = false -- banking info for debugging the mapper
COLOR_JIN = "#00FF00FF" -- colour of Jinborov's coordinate crosshairs
mode = {}
mode[MODE_HELP] = true
mode[MODE_STAT] = true
mode[MODE_TILE] = true
mode[MODE_HITS] = true
mode[MODE_HURT] = true
mode[MODE_BOMB] = true
mode[MODE_HIDE] = false
mkeys = {"H","J","K","L","V","B","N"}
keys = {}
for i=1,MODE_MAX do
keys[i] = false
end
function handle_input()
for i=1,MODE_MAX do
k = emu.isKeyPressed(mkeys[i])
if keys[i] == false and k == true then
mode[i] = not mode[i]
end
keys[i] = k
end
end
-- End Boss detection
function during_endboss()
if memory.readbyte(0x61) ~= 8 then return false end -- stage 9
if memory.readbyte(0x9F) ~= 0x90 then return false end -- scroll
if memory.readbyte(0xA0) ~= 0x00 then return false end -- scroll
return true
end
-- Help
function mode_help()
if not mode[MODE_HELP] then return end
local C1 = "#FFFFFFFF"
local C2 = "#00000044"
local l=1
gui.text(8*1,8*1,"Karnov Inspector " .. VERSION .. " -- rainwarrior 2018",C1,C2)
l=3
gui.text(8*2,8*l," - Help",C1,C2) ; l=l+1
gui.text(8*2,8*l," - Stats",C1,C2) ; l=l+1
gui.text(8*2,8*l," - Tiles",C1,C2) ; l=l+1
gui.text(8*2,8*l," - Touch Hitbox",C1,C2) ; l=l+1
gui.text(8*2,8*l," - Shoot Hitbox",C1,C2) ; l=l+1
gui.text(8*2,8*l," - Bomb Triggers",C1,C2) ; l=l+1
gui.text(8*2,8*l," - Hide Screen",C1,C2) ; l=l+1
l=3
for i=1,MODE_MAX do
c = "#888888FF"
if mode[i] then c = "#FFFF00FF" end
gui.text(8*1,8*l,mkeys[i],c,C2) ; l=l+1
end
end
-- Stats
function mode_stat()
if not mode[MODE_STAT] then return end
-- Jinborov's on-screen coordinates
jin_x = memory.readbyte(0x63)
jin_y = memory.readbyte(0x64)
-- $0C/0D are the scroll X/Y parameters, $0E seems to be a coarse Y scroll
scroll_x = memory.readbyte(0x0C)
scroll_y = (memory.readbyte(0x0E) * 240 / 16) + memory.readbyte(0x0D)
local C1 = "#00FFFFFF"
local C2 = "#00000044"
-- 9F/A0 are tile scroll coordinates
tile_x = memory.readbyte(0x9F)
tile_y = memory.readbyte(0xA0)
-- show position stats
gui.text(8,8*28,string.format("J: %3d/%3d S: %3d/%3d M: %02X/%02X",
jin_x,jin_y,
scroll_x,scroll_y,
tile_x,tile_y
),C1,C2)
if SHOW_MAPPER then
gui.text(8,8*29,string.format("MAPPER: %02X %02X %02X %02X %02X %02X %02X %02X",bank[1],bank[2],bank[3],bank[4],bank[5],bank[6],bank[7],bank[8]),C1,C2)
end
-- Jinborov's foot is his "real" coordinate
gui.line(jin_x-1,jin_y ,jin_x+1,jin_y ,COLOR_JIN)
gui.line(jin_x ,jin_y ,jin_x ,jin_y+3 ,COLOR_JIN)
end
-- Tiles
-- 00 empty
-- 01 solid
-- 02 solid-top
-- 03 solid-top-sides
-- 08 natural ladder
-- 09 placed ladder
-- 0A solid-bottom (water surface)
-- 0B underwater
-- 0C exploded
-- 10 spawn mask stage 1
-- 11 spawn wing stage 1
-- 18 mask opportunity
-- 19 mask opportunity
-- 1C wing opportunity
-- 20 spawn orb
-- 21 spawn orb
-- 22 spawn green sabre attackers
-- 23 spawn blue knights
-- 24 spawn starman
-- 25 spawn birds
-- 26 spawn bats
-- 27 spawn jumper (descending)
-- 28 spawn jumper (running, stage 4)
-- 28 spawn centipede lamia
-- 29 spawn fish head
-- 2A spawn jumper (running, stage 7)
-- 2B spawn mummy/floor (stage 7)
-- 2C trapdoor (stage 7)
-- 2D final boss side door
-- 2F final boss
function mode_tile()
if not mode[MODE_TILE] then return end
-- gui.box(0,0,255,239,"#000000FF","#000000FF") -- to test box colours more easily
-- visual offset from current scroll position
off_x = AND(memory.readbyte(0x0C),0x0F)
off_y = AND(memory.readbyte(0x0D),0x0F)
-- $C4 indicates scrolling: A0 = left, B0 = right, 80 = up, 90 = down, 00 = none
-- $92 and $9F seem to indicate X index shift, $90 seems to indicate it for Y
shift_tx = memory.readbyte(0x9F) -- use this when moving right or stopped
if AND(memory.readbyte(0xC4),0xF0) == 0xA0 then shift_tx = memory.readbyte(0x92) end -- use this when moving left
shift_ty = memory.readbyte(0x93) + 0 -- +11 is equivalent to a -16 on screen Y
if AND(memory.readbyte(0xC4),0xF0) == 0x90 then shift_ty = shift_ty + 11 end
-- draw the tiles currently stored in 16x12 buffer at $040C
for y=0,11 do
for x=0,15 do
tx = AND((x + shift_tx),0x0F)
ty = (y + shift_ty) % 12
tile = memory.readbyte(0x040C + tx + (ty * 16))
if tile ~= 0 then
bx = (x*16) - off_x
by = (y*16) - off_y
ct = "#FFFFFFAA"
c = "#FF00FF88"
if (tile == 0x0B) then c = "#0000FF00" ; ct = "#FFFFFF22" -- underwater
elseif (tile < 0x10) then c = "#0000FF88" -- collision
elseif (tile < 0x1A) then c = "#00FF0088" -- secret trigger
elseif (tile == 0x1E) then c = "#00FF0088" -- spawn orb
elseif (tile == 0x1F) then c = "#00FF0088" -- spawn orb
elseif (tile < 0x20) then c = "#FFFF0088" -- item opportunity
elseif (tile < 0x22) then c = "#00FF0088" -- spawn orb
elseif (tile < 0x30) then c = "#FF000088" -- enemy spawn
elseif (tile >= 0x80) then c = "#FFFFFF44" ; ct = "#FFFFFF88" -- pickup
end
gui.box(bx,by,bx+15,by+15,c,c)
gui.text(bx+2,by+4,string.format("%02X",tile),ct,"#00000000")
end
end
end
-- coordinates to show interaction with tiles
jin_x = memory.readbyte(0x63)
jin_y = memory.readbyte(0x64)
-- midpoint (shows left/right extent on ground)
gui.line(jin_x-3,jin_y-16,jin_x+3,jin_y-16,COLOR_JIN)
gui.line(jin_x ,jin_y-19,jin_x ,jin_y-13,COLOR_JIN)
-- foot (where you touch the ground)
gui.line(jin_x-1,jin_y ,jin_x+1,jin_y ,COLOR_JIN)
gui.line(jin_x ,jin_y ,jin_x ,jin_y+3 ,COLOR_JIN)
-- foot 2 (where you can push over an edge in midair)
gui.line(jin_x-3,jin_y-4 ,jin_x+3,jin_y-4 ,COLOR_JIN)
-- head (where your jump stops if you hit a ceiling)
gui.line(jin_x-1,jin_y-24,jin_x+1,jin_y-24,COLOR_JIN)
gui.line(jin_x ,jin_y-27,jin_x ,jin_y-24,COLOR_JIN)
end
-- Bombs
-- this is just hard-coded, it is not part of the level data
-- each level has a custom routine to check against bombs
-- The routines are in a jump table at bank B:AE93 (following a JSR at B:AE90)
bomb_table = {
{}, -- routine B:AF3C, no bombs
{},
{},
{0x12,0x12,0x13,0x14,0x15,0x14,0x17,0x14,0x17,0x16,
0x19,0x16,0x1B,0x16,0x4E,0x0F,0x4E,0x11,0x4D,0x17,
0x4B,0x17,0x4F,0x17,0x51,0x17,0x53,0x17,0x5C,0x0E,
0x5C,0x10,0x38,0x0F}, -- routine B:AEA5 uses table at B:AEC2
{},
{0x35,0x23,0x36,0x23,0x37,0x23}, -- routine B:AEE4
{},
{0x51,0x0B,0x5B,0x08,0x5C,0x08,0x5D,0x08,0x5E,0x08}, -- routine B:AEF5 uses table at B:AF12
{0x5F,0x0C}, -- routine B:AF1C
}
function mode_bomb()
if not mode[MODE_BOMB] then return end
-- visual offset from current scroll position
off_x = AND(memory.readbyte(0x0C),0x0F)
off_y = AND(memory.readbyte(0x0D),0x0F)
-- current stage
stage = memory.readbyte(0x61) + 1
if stage > table.getn(bomb_table) then return end -- shouldn't happen
-- bomb coordinate offset
coord_off_x = memory.readbyte(0x9F)
coord_off_y = memory.readbyte(0xA0) + 1
-- correct offset if scrolling
if AND(memory.readbyte(0xC4),0xF0) == 0xA0 then coord_off_x = coord_off_x - 1 end
if AND(memory.readbyte(0xC4),0xF0) == 0x80 then coord_off_y = coord_off_y - 1 end
for y=0,11 do
for x=0,15 do
bx = (x*16) - off_x
by = (y*16) - off_y
coord_x = AND(x + coord_off_x, 255)
coord_y = AND(y + coord_off_y, 255)
bomb = false
for i=1,table.getn(bomb_table[stage]),2 do
if coord_x == bomb_table[stage][i+0] and coord_y == bomb_table[stage][i+1] then
bomb = true
end
end
if bomb then
cb = "#FFFF00CC"
ct = "#FF0000CC"
gui.box(bx,by,bx+15,by+15,"#00000000",cb)
gui.text(bx+2,by+7,"B",cb,"#00000000")
end
end
end
end
-- Hitboxes
HIT_Y = -16 -- offset Jin's vertical hit visualization for convenience
function hitbox_enemy(i)
-- based on collision routine at C969
et = memory.readbyte(0x650+(i*16))
if AND(et,0x80) == 0 then return end -- enemy not active
et = AND(et,0x3F) -- enemy type
es = memory.readbyte(0x651+(i*16)) -- enemy status
ex = memory.readbyte(0x652+(i*16)) -- enemy X
ey = memory.readbyte(0x653+(i*16)) -- enemy Y
bys = memory.readbyte(0xCA50+(et*4)) -- enemy hitbox Y size
bxr = memory.readbyte(0xCA51+(et*4)) -- enemy hitbox X right
bxl = memory.readbyte(0xCA52+(et*4)) -- enemy hitbox X left
bxs = memory.readbyte(0xCA53+(et*4)) -- enemy hitbox X size
jin_state = AND(memory.readbyte(0x62),0x1F) -- jin's state
if AND(es,0x0F) < 4 then return end -- not in a colliding state ?
-- X hitbox
tx1 = AND(ex + bxr, 0xFF)
if AND(es,0x80) ~= 0 then tx1 = AND(ex + bxl, 0xFF) end -- facing left
tx0 = tx1 - (bxs-1)
-- Y hitbox
ty1 = AND(ey + 24, 0xFF)
ty0 = ty1 - (bys-1)
if jin_state == 6 or jin_state == 7 then -- ducking
ty1 = AND(ey + 16, 0xFF)
ty0 = ty1 - (bys-9)
end
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
--gui.text(ex-16,ey-16,string.format("%02X %02X",et,es),"#FFFFFF88","#00000088")
end
function hitbox_dragon()
if during_endboss() then return end -- dragon variables reused by end boss
-- based on collision routine at B:AF6F
if AND(memory.readbyte(0x6CF),0x8F) ~= 0x80 then return end -- dragon state?
if memory.readbyte(0x6DD) ~= 0 then return end -- dragon high X (offscreen)
if memory.readbyte(0x6DE) ~= 0 then return end -- dragon high Y (offscreen)
ex = memory.readbyte(0x6DB) -- dragon X
ey = memory.readbyte(0x6DC) -- dragon Y
tx1 = AND(ex + 0x0C, 0xFF)
ty1 = AND(ey + 0x10, 0xFF)
tx0 = tx1 - (0x18-1)
ty0 = ty1 - (0x18-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
end
function hitbox_endboss()
-- based on collision routine at C:A97D
dc = AND(memory.readbyte(0x6CF),0x8F) -- dragon state?
if dc < 0x82 or dc > 0x85 then return end
ex = memory.readbyte(0x6DA)
ey = memory.readbyte(0x6DB)
tx1 = AND(ex + 0x04, 0xFF)
ty1 = AND(ey + 0x18, 0xFF)
tx0 = tx1 - (0x08-1)
ty0 = ty1 - (0x30-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
end
function hitbox_endboss_fireball()
-- based on collision routine at C:ACE2
if AND(memory.readbyte(0x706),0x80) == 0 then return end -- fireball on
ex = memory.readbyte(0x707)
ey = memory.readbyte(0x708)
tx1 = AND(ex + 0x08, 0xFF)
ty1 = AND(ey + 0x18, 0xFF)
tx0 = tx1 - (0x10-1)
ty0 = ty1 - (0x20-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#FF000022","#FF000088")
gui.line(ex-1,ey,ex+1,ey,"#FF00FFFF")
gui.line(ex,ey-1,ex,ey+1,"#FF00FFFF")
end
function hitbox_orb()
-- based on routine at B:AD61
if AND(memory.readbyte(0xD8),0x80) == 0 then return end -- orb inactive
ex = memory.readbyte(0xD9)
ey = memory.readbyte(0xDA)
tx0 = AND(ex - 0x0A, 0xFF)
ty0 = AND(ey + 0x10, 0xFF)
tx1 = tx0 + (0x14-1)
ty1 = ty0 + (0x10-1)
gui.box(tx0,ty0+HIT_Y,tx1,ty1+HIT_Y,"#00FF0022","#00FF0088")
gui.line(ex-1,ey,ex+1,ey,"#00FF00FF")
gui.line(ex,ey-1,ex,ey+1,"#00FF00FF")
end
function mode_hits()
if not mode[MODE_HITS] then return end
for i=0,5 do
hitbox_enemy(i)
end
hitbox_dragon()
if during_endboss() then
hitbox_endboss()
hitbox_endboss_fireball()
end
hitbox_orb()
-- midpoint to make hitbox interaction clear
jin_x = memory.readbyte(0x63)
jin_y = memory.readbyte(0x64)
gui.line(jin_x-3,jin_y+HIT_Y ,jin_x+3,jin_y+HIT_Y ,COLOR_JIN)
gui.line(jin_x ,jin_y+HIT_Y-3,jin_x ,jin_y+HIT_Y+3,COLOR_JIN)
-- I believe the shield status is bit 4 of $65
-- but the shield does not seem to have a hitbox;
-- instead it will cancel damage if you're facing
-- the opposite direction of a bullet.
end
-- Hurtboxes
COLOR_SHOT = "#FFFF00FF"
HURT_Y = 0x0C -- offset hurt locations for convenience
-- List of hurtbox heights for enemies that have them.
-- This is a parameter loaded to A before callilng CC8F,
-- which is loaded by different code for different enemies,
-- so I had to find them all and make this table.
-- (Actual height is 0x12 + 16 * hurt_size.)
hurt_size = {}
hurt_size[0x00] = 0x00
hurt_size[0x01] = 0x01
hurt_size[0x02] = 0x00
hurt_size[0x03] = 0x01
hurt_size[0x04] = 0x01
hurt_size[0x06] = 0x01
hurt_size[0x07] = 0x01
hurt_size[0x08] = 0x01
hurt_size[0x09] = 0x04
hurt_size[0x0A] = 0x01
hurt_size[0x0B] = 0x01
hurt_size[0x0C] = 0x01
hurt_size[0x0D] = 0x02
hurt_size[0x0E] = 0x00
hurt_size[0x0F] = 0x00
hurt_size[0x10] = 0x00
hurt_size[0x11] = 0x00
hurt_size[0x12] = 0x01
hurt_size[0x13] = 0x01
hurt_size[0x14] = 0x01
hurt_size[0x15] = 0x00
hurt_size[0x16] = 0x01
hurt_size[0x17] = 0xFF -- dinosaur
hurt_size[0x19] = 0xFF -- lamia
hurt_size[0x1A] = 0x00
hurt_size[0x1B] = 0x01
hurt_size[0x1D] = 0x01
hurt_size[0x1E] = 0x01
hurt_size[0x1F] = 0x01
hurt_size[0x21] = 0xFF -- ghidorah
hurt_size[0x22] = 0x01
hurt_size[0x23] = 0x00
hurt_size[0x24] = 0xFF -- lamia copy (stage 9)
hurt_size[0x26] = 0x04
hurt_size[0x28] = 0x00
hurt_size[0x29] = 0x00
hurt_size[0x34] = 0x00
hurt_size[0x3A] = 0x01
-- logs parameter to CC8F function, determining hurtbox height
LOG_HURT_SIZE = false
log_hurt_size = {}
function log_hurt_size_()
a = memory.getregister("a") -- hurt size
x = memory.readbyte(0x44) -- enemy index * 16
et = AND(memory.readbyte(0x650+x),0x3F) -- enemy type
hl = log_hurt_size[et]
if hl ~= nil then
for i,v in ipairs(hl) do
if v == a then return end
end
table.insert(hl,a)
else
hl = {a}
log_hurt_size[et] = hl
end
s = string.format("hurt_size[0x%02X] =", et)
for i,v in ipairs(hl) do
s = s .. string.format(" 0x%02X",v)
end
emu.print(s)
end
if LOG_HURT_SIZE then memory.registerexec(0xCC8F,log_hurt_size_) end
function hurt_enemy(i)
-- based on routine at CC8F
et = memory.readbyte(0x650+(i*16))
if AND(et,0x80) == 0 then return end -- enemy not active
et = AND(et,0x3F) -- enemy type
es = memory.readbyte(0x651+(i*16)) -- enemy status
ex = memory.readbyte(0x652+(i*16)) -- enemy X
ey = memory.readbyte(0x653+(i*16)) -- enemy Y
ed = memory.readbyte(0x655+(i*16)) -- enemy damage
if hurt_size[et] == nil then return end -- not a hurting enemy
if AND(es,0x0F) < 0x04 then return end -- not yet hurtable
if (et == 0x14) and AND(es,0x0F) == 0x04 then return end -- mimic enemy hiding
hr = 0x12 + (16 * hurt_size[et])
hp = memory.readbyte(0xCDAD + et) + 1 - AND(ed,0x1F)
if et == 0x17 then -- dinosaur boss is special
hr = 0x10
ey = 0x0C + ey - 0x38
ex = ex + 0x08
if AND(es,0x80) ~= 0 then ex = ex - 0x10 end -- facing
elseif et == 0x19 or et == 0x24 then -- lamia boss
hr = 0x10
ey = 0x0C + ey - 0x28
if AND(es,0x0F) == 0x07 then ey = ey - 0x20 end -- standing
elseif et == 0x21 then -- ghidorah boss
hr = 0x10
ey = 0x0C + ey - 0x40
if AND(es,0x80) ~= 0 then ex = ex + 0x08 end -- facing
end
tx1 = AND(ex+0x04, 0xFF)
ty1 = AND(ey-0x0C, 0xFF)
tx0 = tx1-(0x08-1)
ty0 = ty1-(hr-1)
ex = memory.readbyte(0x652+(i*16)) -- restore ex if overwritten above
ey = memory.readbyte(0x653+(i*16)) -- restory ey if overwritten above
gui.box(tx0,ty0+HURT_Y,tx1,ty1+HURT_Y,"#00FFFF22","#00FFFFCC")
gui.line(ex-1,ey,ex+1,ey,"#00FFFFFF")
gui.line(ex,ey-1,ex,ey+1,"#00FFFFFF")
gui.text(ex+2,ey+2,string.format("%d",hp),"#00FFFFCC","#00000044")
-- bomb hitbox if active
if AND(memory.readbyte(0xC0),0x80) == 0 then return end
bx0 = ex-0x10
by0 = ey
bx1 = bx0 + (0x20-1)
by1 = by0 + (0x10-1)
gui.box(bx0,by0,bx1,by1,"#FFFF0022","#FFFF0088")
end
function hurt_enemies()
for i=0,5 do
hurt_enemy(i)
end
-- bomb active
if AND(memory.readbyte(0xC0),0x80) ~= 0 then
bx = memory.readbyte(0xC1)
by = memory.readbyte(0xC2)
gui.line(bx-1,by,bx+1,by,"#FFFF00FF")
gui.line(bx,by-1,bx,by+1,"#FFFF00FF")
end
end
function hurt_shot()
for i=0,8 do
ss = memory.readbyte(0x6E+(i*3))
sx = memory.readbyte(0x6F+(i*3))
sy = memory.readbyte(0x70+(i*3)) + HURT_Y
if AND(ss,0x80) == 0x80 then
gui.line(sx-1,sy ,sx+1,sy ,COLOR_SHOT)
gui.line(sx ,sy-1,sx ,sy+1,COLOR_SHOT)
end
end
end
function hurt_dragon()
if during_endboss() then return end -- dragon variables reused by end boss
-- based on collision routine at B:AFA6
if AND(memory.readbyte(0x6CF),0x8F) ~= 0x80 then return end -- dragon state?
if memory.readbyte(0x6DD) ~= 0 then return end -- dragon high X (offscreen)
if memory.readbyte(0x6DE) ~= 0 then return end -- dragon high Y (offscreen)
ex = memory.readbyte(0x6DB) -- dragon X
ey = memory.readbyte(0x6DC) -- dragon Y
hp = 0x05 - memory.readbyte(0x6D0)
tx1 = AND(ex + 0x04, 0xFF)
ty1 = AND(ey - 0x0C, 0xFF)
tx0 = tx1 - (0x08-1)
ty0 = ty1 - (0x10-1)
gui.box(tx0,ty0+HURT_Y,tx1,ty1+HURT_Y,"#00FFFF22","#00FFFFCC")
gui.line(ex-1,ey,ex+1,ey,"#00FFFFFF")
gui.line(ex,ey-1,ex,ey+1,"#00FFFFFF")
gui.text(ex+2,ey+2,string.format("%d",hp),"#00FFFFCC","#00000044")
end
endboss_sequence = { 0,1,2,0,2,0,1,2 } -- table at C:ACB3
function hurt_endboss()
-- based on C:A9CA
dc = AND(memory.readbyte(0x6CF),0x8F) -- dragon state?
if dc < 0x82 or dc > 0x85 then return end
ds = memory.readbyte(0x6D0) % 8 -- sequence
hp = 10 - memory.readbyte(0x6D2 + endboss_sequence[ds+1])
ex = memory.readbyte(0x6DA)
ey = memory.readbyte(0x6DB)
tx1 = AND(ex + 0x08, 0xFF)
ty1 = AND(ey - 0x08, 0xFF)
tx0 = tx1 - (0x18-1)
ty0 = ty1 - (0x20-1)
gui.box(tx0,ty0+HURT_Y,tx1,ty1+HURT_Y,"#00FFFF22","#00FFFFCC")
gui.line(ex-1,ey,ex+1,ey,"#00FFFFFF")
gui.line(ex,ey-1,ex,ey+1,"#00FFFFFF")
gui.text(ex+2,ey+2,string.format("%d",hp),"#00FFFFCC","#00000044")
end
function mode_hurt()
if not mode[MODE_HURT] then return end
hurt_enemies()
hurt_dragon()
if during_endboss() then
hurt_endboss()
end
hurt_shot()
end
-- Hide Screen
function mode_hide()
if not mode[MODE_HIDE] then return end
gui.box(0,0,255,191,"#555555FF","#555555FF") -- grey screen for testing
end
-- Main loop
function modes()
emu.clearScreen()
handle_input()
mode_hide()
mode_tile()
mode_bomb()
mode_hits()
mode_hurt()
mode_stat()
mode_help()
end
emu.addEventCallback(modes, emu.eventType.nmi)
-- end of file
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment