--[[ 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 = "Super Mario Bros. 3 Rainbow Riding" SCRIPT_VERSION = "0.2" require "m_utils" m_require("m_utils",0) --[[ Super Mario Bros. 3 Rainbow Riding version 0.2 by miau (2009-05-09) Visit http://morphcat.de/lua/ for the most recent version and other scripts. This script turns smb3 into a new game very similar to Kirby Canvas Curse. It's still incomplete, messy and full of bugs, so don't expect too much. A future version will fix most of them... hopefully. Probably slow on old computers, you may want to turn down emulator speed anyway to decrease difficulty. Controls Left-click on mario - jump Middle-click anywhere - run Draw vertical lines to make mario change his walking direction Draw horizontal lines to help mario move over obstacles Supported roms Super Mario Bros 3 (J), Super Mario Bros 3 (U) (PRG 0), Super Mario Bros 3 (U) (PRG 1), Super Mario Bros 3 (E) Changelog Version 0.3 Reduced Mario's walking speed (see speed_reduce under configurable vars) Known Bugs/TODO list Too many to list 'em all, actually... - game objects are occasionally catapulted out of screen - bad divisions!? - mario falling through lines (clean up collision detection) - long vertically scrolling levels won't work - boss battles are glitchy - disable auto move in mini games? - make collision detection work with fire balls (fire mario or fire piranha plants) - improve map screen check - add easy mode: decrease mario's speed - improve enemy hit boxes - (add suicide button in case mario gets stuck) ]] --configurable vars local show_cursor = true --set this to true when video recording or playing in full screen mode local auto_move = true --makes mario move on his own (Kirby Canvas Curse style) local speed_reduce = true --reduce mario's speed local show_clickbox = false local Z_LSPAN = 400 --number of frames before a line will disappear, 400 frames/60 fps = 6.66 seconds -------------------------------------------------------------------------------------------- local Z_MAX = 128 --256 --maximum amount of lines on screen local NUM_SPRITES = 20 local zbuf = {} local zindex = 1 local timer = 0 local zprev = 0 local last_inp={} local spr = {} --game's original sprites local mario = {} local clickbox={x1=-4,y1=4,x2=18,y2=32} local collx = 0 local colly = 0 local paintmeter = 0 local lastpaint = -100 local jp = {} --accepts two tables containing these elements [1]=x1, [2]=y1, [3]=x2, [4]=y2 function get_line_intersection(a,b) local Asx,Asy,Bsx,Bsy,s,t local Ax1,Ay1,Ax2,Ay2 local Bx1,By1,Bx2,By2 Ax1 = a[1] Ay1 = a[2] Ax2 = a[3] Ay2 = a[4] Bx1 = b[1] By1 = b[2] Bx2 = b[3] By2 = b[4] Asx = Ax2 - Ax1 Asy = Ay2 - Ay1 Bsx = Bx2 - Bx1 Bsy = By2 - By1 s = (-Asy * (Ax1 - Bx1) + Asx * (Ay1 - By1)) / (-Bsx*Asy + Asx*Bsy) t = (Bsx * (Ay1 - By1) - Bsy * (Ax1 - Bx1)) / (-Bsx*Asy + Asx*Bsy) if(s>=0 and s<=1 and t>=0 and t<=1) then local Ix,Iy Ix = Ax1 + t * Asx Iy = Ay1 + t * Asy return Ix,Iy else return nil end end function close_to_line(Px,Py,a,range) --a[4] line segment local Ax=a[1] local Ay=a[2] local Bx=a[3] local By=a[4] local APx = Px - Ax local APy = Py - Ay local ABx = Bx - Ax local ABy = By - Ay local absq = ABx*ABx + ABy*ABy local apab = APx*ABx + APy*ABy local t = apab / absq if (t < 0.0) then t = 0.0 elseif (t > 1.0) then t = 1.0 end local Cx,Cy Cx = Ax + ABx * t Cy = Ay + ABy * t if(getdistance(Px,Py,Cx,Cy)<=range) then return true else return false end end function ride_line(s,xoffs,yoffs) local function move(zx1,zy1,zx2,zy2) local avx,avy,cvx,cvy cvx,cvy = getvdir(zx1,zy1,zx2,zy2) if(math.abs(spr[s].vx) Z_MAX) then spr[s].riding = 1 end if(spr[s].riding==firstline) then spr[s].riding=nil return end domovemagic(x,y,firstline) end end if(spr[s].riding==nil) then return end local x = spr[s].x+xoffs local y = spr[s].y+yoffs domovemagic(x,y,spr[s].riding) end function collisioncheck(s) local c=false local cvx,cvy local avx,avy local function checkpixel(xoffs,yoffs,i,j) local x = spr[s].x+xoffs local y = spr[s].y+yoffs local px2 = x+spr[s].vx local py2 = y+spr[s].vy local px1 = x local py1 = y local zx = zbuf[i].x local zy = zbuf[i].y local zx2 = zbuf[j].x local zy2 = zbuf[j].y if(spr[s].vx~=0 or spr[s].vy~=0) then if(spr[s].vx>0) then --we need at least one pixel!!? px2 = px2 + 2 elseif(spr[s].vx<0) then px2 = px2 - 2 end if(spr[s].vy>0) then py2 = py2 + 2 elseif(spr[s].vy<0) then py2 = py2 - 2 end local cx,cy = get_line_intersection({px1,py1,px2,py2},{zx,zy,zx2,zy2}) if(cx~=nil) then cx = math.floor(cx) cy = math.floor(cy) local avx,avy,cvx,cvy --coll = {--[[a={px1,py1,px2+spr[s].vx*64,py2+spr[s].vy*64},-]]b={zbuf[i].x,zbuf[i].y,zbuf[j].x,zbuf[j].y}} --debug collx = cx colly = cy avx,avy = getvdir(px1,py1,px2,py2) cvx,cvy = getvdir(zbuf[j].x,zbuf[j].y,zbuf[i].x,zbuf[i].y) local x,y x = cx-xoffs y = cy-yoffs if(spr[s].x==x and spr[s].y==y) then --FCEU.message("boo"..timer) else set_sprite_pos(s,x,y) end if(s==0) then --mario/luigi if(math.abs(cvy)==1 and math.abs(cvx) < 0.5) then change_sprite_dir(s) set_sprite_velocity(s,-avx,nil) else spr[s].riding = i set_sprite_velocity(s,0,0) end else --enemies, moving platforms end return true end else if(close_to_line(x,y,{zx,zy,zx2,zy2},4)) then if(s==0) then spr[s].riding = i set_sprite_velocity(s,0,0) end return true else spr[s].riding = nil end end return false end if(spr[s].riding) then ride_line(s,8,30) return end for i=1,Z_MAX do if(zbuf[i] and zbuf[i].connected) then --if(zbuf[i].x >= spr[s].x+hitbox.x1 and zbuf[i].y >= spr[s].y+hitbox.y1 -- and zbuf[i].x <= spr[s].x+hitbox.x2 and zbuf[i].y <= spr[s].y+hitbox.y2) then --local px = spr[s].x-spr[s].vx --local py = spr[s].y-spr[s].vy local j j = i - 1 if(j < 1) then j = Z_MAX end if(zbuf[j]) then --check if line is still valid (if nil, node disappeared) if(s==0) then --mario if(checkpixel(8,30,i,j)) then return end else if(checkpixel(0,0,i,j) or checkpixel(8,8,i,j) --[[or checkpixel(0,8,i,j) or checkpixel(8,0,i,j)-]]) then destroy_sprite(s) return end end end --end end end end function screen_to_game_pos(x,y) return x+scroll_x,y+scroll_y end function game_to_screen_pos(x,y) return x-scroll_x,y-scroll_y end function destroy_sprite(s) if(s<10) then memory.writebyte(0x660+s,0x06) --memory.writebyte(0x660+s,0x00) --set_sprite_velocity(s,10,10) else memory.writebyte(0x6C7+s-10,0x01) --set_sprite_pos(s,0,0) end end function set_sprite_velocity(s,vx,vy) if(s<10) then if(s==0) then memory.writebyte(0xD8,1) --air flag? end if(vx) then memory.writebyte(0xBD+s,vx*16) spr[s].vx = vx end if(vy) then memory.writebyte(0xCF+s,vy*16) spr[s].vy = vy end end end function set_sprite_pos(i,x,y) memory.writebyte(0x90+i,AND(x,255)) memory.writebyte(0x75+i,math.floor(x/256)) memory.writebyte(0xA2+i,AND(y,255)) memory.writebyte(0x87+i,math.floor(y/256)) spr[i].x = x spr[i].y = y spr[i].sx,spr[i].sy = game_to_screen_pos(spr[i].x,spr[i].y) end function change_sprite_dir(s) if(spr[s].dir == 1) then spr[s].dir = -1 elseif(spr[s].dir == -1) then spr[s].dir = 1 end end function add_rainbow_coord(cx,cy,connected) zbuf[zindex] = {t=timer,x=cx,y=cy,connected=connected} zprev = zbuf[zindex] zindex = zindex + 1 if(zindex>Z_MAX) then zindex = 1 end end function drawrainbow(x1,y1,x2,y2,coloffs) local cx,cy local vx,vy local color = coloffs vx,vy = getvdir(x1,y1,x2,y2) cx = x1 cy = y1 for i=1,200 do if(cx>=0 and cy>=0 and cx<=253 and cy<=253) then local rcolor = color/1.8+161 --165 gui.drawpixel(cx,cy,rcolor) gui.drawpixel(cx,cy+1,rcolor) gui.drawpixel(cx+1,cy,rcolor) gui.drawpixel(cx+1,cy+1,rcolor) gui.drawpixel(cx+2,cy,rcolor) gui.drawpixel(cx,cy+2,rcolor) gui.drawpixel(cx+2,cy+1,rcolor) gui.drawpixel(cx+1,cy+2,rcolor) gui.drawpixel(cx+2,cy+2,rcolor) end if((x2>x1 and cx>x2) or (x2y1 and cy>y2) or (y260) then mario.lastposchange = timer change_sprite_dir(0) end --load game sprites from ram for i=0,NUM_SPRITES-1 do if(i<10) then spr[i].x = memory.readbyte(0x90+i)+memory.readbyte(0x75+i)*256 spr[i].y = memory.readbyte(0xA2+i)+memory.readbyte(0x87+i)*256 spr[i].vx = memory.readbytesigned(0xBD+i)/16 spr[i].vy = memory.readbytesigned(0xCF+i)/16 spr[i].a = (memory.readbytesigned(0x660+i)~=0) spr[i].id = memory.readbytesigned(0x670+i) spr[i].sx,spr[i].sy = game_to_screen_pos(spr[i].x,spr[i].y) else --TODO: 0x5D3? local xcomp = memory.readbyte(0xFD) local ycomp = memory.readbyte(0xFC) spr[i].x = memory.readbyte(0x5C9+i-10) spr[i].y = memory.readbyte(0x5BF+i-10) spr[i].a = true spr[i].vx = 0 spr[i].vy = 0 spr[i].sx = AND(spr[i].x-xcomp+256,255) spr[i].sy = AND(spr[i].y-ycomp+256,255) spr[i].x = spr[i].sx + scroll_x spr[i].y = spr[i].sy + scroll_y end if(spr[i].a) then collisioncheck(i) end end end function update_vars() inp = input.get() jp = {} --joypad.set is buggy --[[ if(disable_input) then jp.A = false jp.B = false jp.left = false jp.right = false end]] if(auto_move) then if(mario.riding==nil) then if(mario.movetimer) then jp.B = 1 mario.movetimer = mario.movetimer - 1 if(mario.movetimer == 0) then mario.movetimer = nil end end if(mario.dir==1) then jp.right=1 if(speed_reduce) then memory.writebyte(0xBD,0x10) end elseif(mario.dir==-1) then jp.left=1 if(speed_reduce) then memory.writebyte(0xBD,0x0F0) end end --if(AND(timer,1)==0) then --automatically enter doors and pipes... not a very good idea actually :P --jp.up=1 --if(memory.readbyte(0xD8)==1) then --air flag set? try to stay in air as long as possible... makes it easier to rescue mario if collision detection screws up.. sucks in water levels -- jp.A=1 --end --else -- jp.down=1 --automatically enter pipes --end end end scroll_x = memory.readbyte(0xFD)+memory.readbyte(0x12)*256 scroll_y = memory.readbyte(0xFC)--+memory.readbyte(0x13)*256 --not quite right, long vertical scrolling levels won't work --0xD8 = 0 -> touch ground, 1 -> air update_sprites() if(inp.middleclick) then mario.movetimer = 30 end if(inp.leftclick==nil) then mario.jumping=false end if(inp.leftclick and last_inp.leftclick==nil and inp.xmouse>=mario.sx+clickbox.x1 and inp.xmouse<=mario.sx+clickbox.x2 and inp.ymouse>=mario.sy+clickbox.y1 and inp.ymouse<=mario.sy+clickbox.y2) then jp.A = 1 mario.jumping = true elseif(inp.leftclick and mario.jumping) then jp.A = 1 elseif(inp.leftclick) then if(paintmeter>0) then local x,y=screen_to_game_pos(inp.xmouse,inp.ymouse) if(last_inp.leftclick==nil) then add_rainbow_coord(x,y,false) outofpaint = nil elseif(outofpaint==nil) then if(zprev and getdistance(x,y,zprev.x,zprev.y)>8) then add_rainbow_coord(x,y,true) paintmeter = paintmeter - 2 lastpaint = timer end end else outofpaint = true end end joypad.set(1,jp) last_mario = mario last_inp = inp end function render() --bctext(0,20,string.format("Memory usage: %.2f KB",collectgarbage("count"))) local j = 0 for i=1,Z_MAX do if(zbuf[i]) then j = j + 1 end end --bctext(0,20,"Lines: "..j) --bctext(0,20,mario.x..","..mario.y) --bctext(0,30,mario.sx..","..mario.sy) --bctext(0,40,mario.vx..","..mario.vy) --bctext(0,50,"D "..mario.dir) --bctext(0,70,"scroll_x: "..scroll_x) --bctext(0,80,"scroll_y: "..scroll_y) --bctext(0,90,AND(mario.x,255)) --if(mario.riding) then -- bctext(0,100,"R") --end if(show_clickbox) then bcbox(mario.sx+clickbox.x1,mario.sy+clickbox.y1,mario.sx+clickbox.x2,mario.sy+clickbox.y2,"white") end local prev_x, prev_y, prev_connected local i=zindex local color = AND(timer/2,15) for j=1,Z_MAX do if(zbuf[i]) then local ltime = timer-zbuf[i].t if(ltime0) then drawrainbow(pmx+paintmeter,pmy,pmx,pmy,timer/6) end bcbox(pmx-2,pmy-1,pmx+102,pmy+3,"white") if(timer-lastpaint>60) then paintmeter = paintmeter + 1 if(paintmeter>100) then paintmeter=100 end end if(show_cursor) then local col2 if(inp.leftclick) then col2 = "#FFAA00" elseif(inp.rightclick) then col2 = "#0099EE" elseif(inp.middleclick) then col2 = "#AACC00" else col2 = "white" end drawcursor(inp.xmouse,inp.ymouse,"black",col2) end end function domagic() if(memory.readbyte(0x73)==0x20) then --map screen(?) update_vars() render() else bcpixel(1,10,"clear") end end initialize() gui.register(domagic) while(true) do FCEU.frameadvance() timer = timer + 1 end