--[[ Copyright (c) 2013 Julius Riecke This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ]] SCRIPT_TITLE = "Gradius - Bullet Hell" SCRIPT_VERSION = "1.0" require "m_utils" m_require("m_utils",0) --[[ Gradius - Bullet Hell version 1.0 by Julius Riecke Visit http://morphcat.de/lua/ for the most recent version and other scripts. Controls Press select to fire a bomb that will destroy all enemy bullets and grant you invincibility for a short period of time. Supported roms Gradius (J), Gradius (U), Gradius (E) Known bugs - dying from blue bullets doesn't trigger the death sound effect -]] --configurable vars local BOMBS_PER_LIFE = 5 local HITBOX = {-2, 4, 9, 9} --vic viper's hit box for collision detection with blue bullets -------------------------------------------------------------------------------------------- local MAX_EXSPR = 64 local timer = 0 local spr = {} local exspr = {} local exsprdata = {} local deathtimer = 0 local bombtimer = 0 local paused = 0 local bombs = BOMBS_PER_LIFE local bulletimg = "" function makebinstr(t) local str = "" for i,v in ipairs(t) do str = str..string.char(v) end return str end function initialize() --Transparency doesn't seem to work for gd images, bummer! bulletimg = makebinstr( { 0xff, 0xfe, 0, 0x6, 0, 0x6, 0x1, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2, 0x33, 0x6e, 0, 0x2, 0x33, 0x6e, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2, 0x33, 0x6e, 0, 0x4d, 0xb6, 0xff, 0, 0xdb, 0xff, 0xfc, 0, 0x2, 0x33, 0x6e, 0, 0, 0, 0, 0, 0x2, 0x33, 0x6e, 0, 0x4d, 0xb6, 0xff, 0, 0x4d, 0xb6, 0xff, 0, 0xdb, 0xff, 0xfc, 0, 0xdb, 0xff, 0xfc, 0, 0x2, 0x33, 0x6e, 0, 0x2, 0x33, 0x6e, 0, 0xdb, 0xff, 0xfc, 0, 0xdb, 0xff, 0xfc, 0, 0x4d, 0xb6, 0xff, 0, 0x4d, 0xb6, 0xff, 0, 0x2, 0x33, 0x6e, 0, 0, 0, 0, 0, 0x2, 0x33, 0x6e, 0, 0xdb, 0xff, 0xfc, 0, 0x4d, 0xb6, 0xff, 0, 0x2, 0x33, 0x6e, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2, 0x33, 0x6e, 0, 0x2, 0x33, 0x6e, 0, 0, 0, 0, 0, 0, 0, 0 } ) for i=0,31 do spr[i] = { status=0, id=0, x=0, y=0, timer=-1, } end end function createexsprite(s) for i=0,MAX_EXSPR-1 do if(exspr[i]==nil) then exspr[i]=s if(exspr[i].id==nil) then exspr[i].id=0 end if(exspr[i].x==nil) then exspr[i].x=0 end if(exspr[i].y==nil) then exspr[i].y=0 end if(exspr[i].vx==nil) then exspr[i].vx=0 end if(exspr[i].vy==nil) then exspr[i].vy=0 end if(exspr[i].timer==nil) then exspr[i].timer=0 end if(exspr[i].ai==nil) then exspr[i].ai="" end return exspr[i] end end return nil end function destroyallexsprites() exspr = {} end function destroyexsprite(i) exspr[i] = nil end function destroysprite(i) memory.writebyte(0x100+i,0x00) memory.writebyte(0x120+i,0x00) memory.writebyte(0x300+i,0x00) end function drawexsprite(id,x,y) gui.gdoverlay(x, y, bulletimg) end function getvicdistance(x,y) return getdistance(vic.x+4,vic.y+8,x,y) end function doexspriteai(s) if(s.ai=="Stray") then if(s.timer==0) then s.vx = -3 s.vy = AND(timer,0x0F) / 8 - 1 end elseif(s.ai=="AimOnce") then if(s.timer==0) then s.vx,s.vy = get_vdir(s,vic) s.vx = s.vx * 2 s.vy = s.vy * 2 --s.vy = AND(timer,0x0F) / 8 - 1 end elseif(s.ai=="AimDelayed") then if(s.timer>=30 and s.timer<=33) then local x,y = get_vdir(s,vic) s.vx = (x+s.vx) s.vy = (y+s.vy) end elseif(s.ai=="AimRight") then if(s.timer==0) then s.vx,s.vy = get_vdir(s,vic) s.vx = s.vx * 2.5 s.vy = s.vy * 1.5 end elseif(s.ai=="AimLeft") then if(s.timer==0) then s.vx,s.vy = get_vdir(s,vic) s.vx = s.vx * 1.5 s.vy = s.vy * 2.5 end elseif(s.ai=="BossPattern1") then if(s.timer==0) then s.vx = math.random(-100,0)/50 s.vy = math.random(-100,0)/70 elseif(s.timer>=30 and s.timer <=35) then s.vx=s.vx/1.1 s.vy=s.vy/1.1 elseif(s.timer==70) then local x,y = get_vdir(s,vic) s.vx = x*3 s.vy = y*3 end elseif(s.ai=="LimitedLifeSpan") then if(s.timer>120) then s.x=999 --results in death end end if(s.timer>600) then --delete extended sprites after 10 seconds in case one gets stuck on screen s.x=999 end end function doboss1ai(s) if(s.timer>80 and s.timer<320 and AND(s.timer,63)==0) then createexsprite({x=s.x,y=s.y,ai="BossPattern1"}) createexsprite({x=s.x,y=s.y,ai="BossPattern1"}) createexsprite({x=s.x,y=s.y,ai="BossPattern1"}) createexsprite({x=s.x,y=s.y,ai="BossPattern1"}) createexsprite({x=s.x,y=s.y,ai="BossPattern1"}) createexsprite({x=s.x,y=s.y,ai="BossPattern1"}) createexsprite({x=s.x,y=s.y,ai="BossPattern1"}) end if(s.timer>320 and s.timer<520) then --pattern2 a if(math.mod(s.timer,27)<=9 and AND(s.timer,3)==3) then createexsprite({x=s.x,y=s.y,vx=-0.2,ai="AimDelayed"}) end end if(s.timer>320 and s.timer<520) then --pattern2 b if(AND(s.timer,63)==0) then createexsprite({x=s.x,y=s.y,vx=-0.7,vy=-1.6}) createexsprite({x=s.x,y=s.y,vx=-1.1,vy=-0.8}) createexsprite({x=s.x,y=s.y,vx=-1.2,vy=-0}) createexsprite({x=s.x,y=s.y,vx=-1.1,vy=0.8}) createexsprite({x=s.x,y=s.y,vx=-0.7,vy=1.6}) end elseif(s.timer>520) then s.timer = 60 end end function dospriteai(s) if(s.id==0x85) then --Fan if(s.timer==30) then createexsprite({x=s.x,y=s.y,ai="Stray"}) end elseif(s.id==0x86) then --Jumper if(AND(s.timer,127)==40) then createexsprite({x=s.x,y=s.y,vx=-1.5,vy=-0.5}) createexsprite({x=s.x,y=s.y,vx=-1,vy=-1.1}) createexsprite({x=s.x,y=s.y,vx=0,vy=-1.4}) createexsprite({x=s.x,y=s.y,vx=1,vy=-1.1}) createexsprite({x=s.x,y=s.y,vx=1.5,vy=-0.5}) end elseif(s.id==0x88) then --Rugul if(AND(s.timer,63)==30) then createexsprite({x=s.x,y=s.y,ai="Stray"}) end elseif(s.id==0x84 or s.id==0x9C) then --Fose, Uska(?) if(AND(s.timer,63)==30) then createexsprite({x=s.x,y=s.y,vx=0,vy=-2}) elseif(AND(s.timer,63)==60) then createexsprite({x=s.x,y=s.y,vx=0,vy=2}) end elseif(s.id==0x89 or s.id==0x8C) then --Rush if(AND(timer,63)==5) then createexsprite({x=s.x,y=s.y,vx=-1,vy=-1}) end elseif(s.id==0x96) then --Moai elseif(s.id==0x97) then --Mother and Child if(AND(timer,7)==0) then local t = s.timer/15 createexsprite({x=s.x+16,y=s.y+16,vx=math.sin(t)-1,vy=math.cos(t),ai="LimitedLifeSpan"}) end elseif(s.id==0x8B) then --Zabu if(s.timer==5) then createexsprite({x=s.x,y=s.y,vx=0.5,ai="AimDelayed"}) end elseif(s.id==0x92 or s.id==0x93 or s.id==0x87 or s.id==0x91) then --Dee-01, Ducker --if(s.timer==5) then local t = AND(s.timer,127) if(t>60 and t<79 and AND(s.timer,3)==3) then --createexsprite({x=s.x,y=s.y,ai="AimOnce"}) createexsprite({x=s.x,y=s.y,ai="AimOnce"}) end elseif(s.id==0x98) then --boss... doboss1ai(s) end end function updatevars() paused = memory.readbyte(0x0015) if(paused==1) then return end --load original sprites from ram for i=0,31 do --if(i==0 or memory.readbyte(0x0300+i)~=0) then if(memory.readbyte(0x0300+i)~=spr[i].id) then spr[i].timer = 0 end spr[i].status=memory.readbyte(0x0100+i) --1=alive, 2=dead spr[i].id=memory.readbyte(0x0300+i) spr[i].x=memory.readbyte(0x0360+i) spr[i].y=memory.readbyte(0x0320+i) spr[i].timer=spr[i].timer+1 if(spr[i].id==0) then spr[i].timer = 0 else dospriteai(spr[i]) end if(i>3 and bombtimer>0 and getvicdistance(spr[i].x+4,spr[i].y+4)<30) then if(spr[i].id~=0x29 and spr[i].id~=0x01 and spr[i].id~=0x99 and spr[i].id~=0x98 and spr[i].id~=0x94 and spr[i].id~=0x97 and spr[i].id~=0x96 and spr[i].id~=0x1E) then --excluded: hidden bonus (0x29), power ups and moai bullets (0x01), boss (0x99+0x98), tentacle (0x94), mother (0x97), moai (0x96), gate (0x1E) destroysprite(i) end end --end end vic = spr[0] if(deathtimer>0) then if(deathtimer==120) then destroyallexsprites() deathtimer = 0 bombs = BOMBS_PER_LIFE else if(deathtimer==1) then --this is part of what gradius does to kill vic viper... --faster and without the annoying sound effect. who cares! memory.writebyte(0x4C,0x78) memory.writebyte(0x0100,0x02) --status = dead memory.writebyte(0x0160,0x00) memory.writebyte(0x0140,0x00) memory.writebyte(0x1B,0xA0) memory.writebyte(0x0120,0x2D) --chr?-- end deathtimer = deathtimer + 1 end end local jp = joypad.get(1) if(jp.select and jp.select~=last_select and bombs>0) then bombtimer = 1 bombs = bombs - 1 destroyallexsprites() --destroy bullets for i=1,31 do if(spr[i].id<4 and spr[i].id~=1) then destroysprite(i) end end end last_select = jp.select if(last_vic_status~=vic.status) then if(vic.status==2) then --vic died in the original game, start death timer to destroy all extended sprites deathtimer = 1 end end last_vic_status = vic.status --calculations on extended sprites for i=0,MAX_EXSPR-1 do if(exspr[i]~=nil) then if(bombtimer>0 and getvicdistance(exspr[i].x+4,exspr[i].y+4)<25) then destroyexsprite(i) else doexspriteai(exspr[i]) exspr[i].timer = exspr[i].timer + 1 exspr[i].x=exspr[i].x+exspr[i].vx exspr[i].y=exspr[i].y+exspr[i].vy if(exspr[i].x>255 or exspr[i].x<0 or exspr[i].y>255 or exspr[i].y<0) then --destroy exsprite exspr[i]=nil break end --collision check with vic viper if(deathtimer==0 and vic.status==1 and exspr[i].x>=vic.x+HITBOX[1] and exspr[i].x<=vic.x+HITBOX[3] and exspr[i].y>=vic.y+HITBOX[2] and exspr[i].y<=vic.y+HITBOX[4]) then deathtimer = 1 end end end end end function render() --bcbox(vic.x-2, vic.y+4, vic.x+9, vic.y+9, "#ffffff") gui.text(0, 8, string.format("bombs %d",bombs)); --gui.text(0, 8, string.format("Lives %d",memory.readbyte(0x0020))); --gui.text(0, 28, string.format("bombtimer %d",bombtimer)); --[[for i=1,31 do if(spr[i].id~=0) then gui.text(spr[i].x, spr[i].y, string.format("%X",spr[i].id)); end end--]] for i=0,MAX_EXSPR-1 do if(exspr[i]~=nil) then drawexsprite(exspr[i].id,exspr[i].x,exspr[i].y) end end if(bombtimer>0) then if(bombtimer<12) then memory.writebyte(0x11,0xFF) --monochrome screen else memory.writebyte(0x11,0x1E) end bombtimer = bombtimer + 1 for i=0,64 do local x = vic.x+4+math.sin(i)*bombtimer*4 + math.random(0,12) local y = vic.y+8+math.cos(i)*bombtimer*4 + math.random(0,12) local c = 255-bombtimer local cs = string.format("#%02x%02x00",c,c) bcpixel(x,y,cs) bcpixel(x-1,y,cs) bcpixel(x+1,y,cs) bcpixel(x,y+1,cs) bcpixel(x,y-1,cs) x = vic.x+4+math.sin(i)*(20+math.sin(bombtimer/5)) y = vic.y+8+math.cos(i)*(20+math.sin(bombtimer/5)) bcpixel(x,y,cs) bcpixel(x-1,y,cs) bcpixel(x,y-1,cs) end if(bombtimer==180) then bombtimer=0 end end end initialize() vic = spr[0] while(true) do updatevars() render() EMU.frameadvance() timer = timer + 1 end