Last active
July 17, 2020 13:51
-
-
Save bbbradsmith/46e53f67f681814337001445539995ad to your computer and use it in GitHub Desktop.
Karnov Inspector v1.0 - Details: https://www.patreon.com/posts/karnov-inspector-22622436
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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