Something went wrong. Try again.
an npmx inspired game for PICO-8 npicomx.vinnymac.dev
games pico-8 adventure npmx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249-- play: the play-loop orchestrators
function upd_aemb() for e in all(aemb) do e.y-=e.sp if e.y<-2 then e.y=130 e.x=rnd(128) end end endfunction drw_aemb() for e in all(aemb) do pset(e.x+sin(t()*0.5+e.ph)*4,e.y,e.y>92 and 10 or e.y>52 and 9 or e.y>26 and 8 or 5) end end
function update_play() if frz>0 then frz-=1 return end if dying then dying_t+=1 if dying_t>=60 then over_phrase=rnd(overmsg) scr=3 over_t=0 over_fade=1 dying=false music(8) end return end if trans>0 then trans-=1 end playt+=1 -- total play time (for the timed-mode credits)
if phase==0 then if dlg then -- intro monologue slides up; cage waits for it to finish dly=appr(dly,90,7) dtimer+=1 if dtimer>12 and (btnp(4) or btnp(5)) then dtimer=0 dlg+=1 local tmax=(cloud_exit and dtext_c) and #dtext_c or #dtext if dlg>tmax then dlg=nil if cloud_exit then cloud_exit=false scr=1 else enter_ready=true end end end end pdrop=appr(pdrop,0,2.5) -- crates tumble into the pile lay_pile() -- nograb eats the dismiss-frame so the button that closes the dialog -- doesn't ALSO grab a crate (the butterfly starts right over the pile). if pdrop<=0 and not dlg and not entering then phase=1 nograb=3 end end
if entering and enter_ready then bf.x+=1.5 bf.vx=0 bf.vy=0 bf.flip=false bf.fl=8 if bf.x>=40 then entering=false if lvl~=4 then shake=8 frz=4 burst(0,92) sfx(5) end -- cave door-slam (dust+shake+sound); skipped on the open cloud level if pdrop<=0 and not dlg then phase=1 nograb=3 end end end
if not entering then local ax=(btn(1) and 1 or 0)-(btn(0) and 1 or 0) local ay=(btn(3) and 1 or 0)-(btn(2) and 1 or 0) if dlg then ax=0 ay=0 end -- hold still during the intro bf.vx=appr(bf.vx,ax*1.4,ax!=0 and 0.2 or 0.12) bf.vy=appr(bf.vy,ay*1.4,ay!=0 and 0.2 or 0.12) move_bf(bf.vx,bf.vy) if lvl==4 then -- symmetric cloud-bank walls box you in: top y38, bottom y89 (mirror about screen centre) if bf.x<6 or bf.x>lvw*8-6 then bf.x=mid(6,bf.x,lvw*8-6) bf.vx=0 end if bf.y<38 or bf.y>89 then bf.y=mid(38,bf.y,89) bf.vy=0 end else local cy=lvl==1 and -4 or 20 -- L2 secret: ride OUT OF BOUNDS on top of the level (mario warp) - feet poke above the boundary, then run right to the room if bf.y<cy then bf.y=cy bf.vy=0 end if bf.y>116 then bf.y=116 bf.vy=0 end end if ax!=0 then bf.flip=ax<0 end bf.fl=(bf.carry and 8 or 6)+(abs(bf.vx)+abs(bf.vy))*1.6 -- airspeed-driven flap end if bf.iv>0 then bf.iv-=1 end add(bf.trail,{col=bf_sx(bf.fl),x=bf.x,y=bf.y,fl=bf.flip}) while #bf.trail>8 do deli(bf.trail,1) end camx=mid(0,bf.x-64,lvw*8-128)
if phase==2 then baly-=1.5 if baly<-26 then next_level() return end else if phase==1 then if nograb>0 then nograb-=1 elseif btnp(4) then do_grab_deliver() end if btnp(5) then do_drop() end lay_pile() local fs=free_slot() if fs and rnd(1)<0.04 then spawn_one(fs) end if not bf.carry and #shouts<3 and rnd(1)<0.02 then local p=rnd(pkgs) if p and p.st==0 then emit_shout(p) end end if timed then clock-=1 if clock<=0 then t_out=true over_phrase=rnd(overmsg) scr=3 over_t=0 frz=10 music(8) return end end if lvl==1 and not bf.carry and ov(bf.x,bf.y,544,64,9,9) then enter_cloud() return end if lvl>=1 and lvl~=2 then wst+=1 if wst>=(lvl==4 and 16+camx\6 or lvl==1 and 80 or 110) then -- L4: faster gusts, easing toward the sun wst=-rnd(14) -- stagger the next interval so gusts aren't evenly aligned -- L4 gusts vary in speed/wave/height per-spawn so they break up instead of stacking local amp=lvl==4 and 8+rnd(12) or lvl==1 and 9 or 14 -- keep whole gust (y ± amp) inside the open corridor (L4: the shorter y38-86 band) local lo=lvl==4 and 38 or 24 add(wisps,{x=camx+144,y=lo+amp+rnd((lvl==4 and 88 or 92)-lo-amp*2),vx=lvl==4 and -3-rnd(2.5) or lvl==1 and -2.5 or -1.0,spd=lvl==4 and 3+rnd(4) or lvl==1 and 3 or 1.5,amp=amp,ph=rnd(1)}) end for w in all(wisps) do w.x+=w.vx local wy=w.y+sin(t()*w.spd+w.ph)*w.amp -- push along the WHOLE visible gust (w.x..w.x+24), not just its head, with a -- real impulse so a fast gust actually shoves the butterfly around. if bf.x>w.x-6 and bf.x<w.x+28 and abs(bf.y-wy)<9 then bf.vx+=w.vx*(lvl==4 and 0.25 or 0.1) bf.vy+=sgn(bf.y-wy)*0.5 -- L4 shoves you hard end for em in all(ems) do if abs(w.x-em.x)<14 and abs(wy-em.y)<10 then em.douse=min((em.douse or 0)+4,16) end end if w.x<camx-32 then del(wisps,w) end end end end if bf.carry then local p=bf.carry p.x=bf.x p.y=bf.y+6 if p.devil and p.rat>0 then if p.rat_delay and p.rat_delay>0 then p.rat_delay-=1 else p.rat-=1 if diff<3 and p.rat%26==0 and #shouts<3 then emit_evil(p) end -- taunts in easy/normal if p.rat==24 then sfx(7) end if p.rat<=0 then spawn_imp(bf.x,bf.y) hurt("demon pkg") end end end end for p in all(pkgs) do if p.st==3 then p.vy=min(p.vy+0.15,3) p.vx=p.vx*0.9 p.x+=p.vx p.y+=p.vy if ov(p.x,p.y,balx,baly,14,16) then deliver(p) -- lands on the balloon = a delivery, same as 🅾/❎ elseif bird.on and ov(p.x,p.y,bird.x,bird.y,7,7) then bird.on=false birdt=60 burst(bird.x,bird.y) shake=5 frz=3 sfx(5) -- a dropped crate smashes the bird; respawn after ~2s if p.devil then purged+=1 end del(pkgs,p) else local burned=false for em in all(ems) do if em.h>7 and flame_ov(em,p.x,p.y,3,3) then burned=true break end end if burned then if p.devil then purged+=1 end add_name(p.name,p.x,p.y,true) sfx(6) del(pkgs,p) elseif solid_box(p.x-3,p.y-3,6,6) or p.y>140 then if p.devil then spawn_imp(p.x,p.y) shake=4 end del(pkgs,p) end end end end update_flames() -- bird: gated per level (birdt<0=off). first pickup arms it; auto-respawns until level exit. -- load_level resets birdt=-1; phase==1 only (never spawns during entry/delivery). if has_bird and phase==1 and not bird.on and birdt>=0 then birdt-=1 if birdt<0 then spawn_bird() birdt=0 end end update_bird() update_decor() if lvl==4 then flock(wbirds) end -- drive the cloud-level flock end
for n in all(names) do n.t+=1 n.y+=n.vy if n.t>=n.life then del(names,n) end end for n in all(shouts) do n.x+=n.vx n.y+=n.vy n.t+=1 if n.t>=n.life then del(shouts,n) end end for e in all(embers) do e.x+=e.vx e.y+=e.vy e.vy+=0.06 e.t-=1 if e.t<=0 then del(embers,e) end end upd_aemb() if shake>0 then shake-=1 end if dflash>0 then dflash-=1 end
if bf.hp<=0 and not dying then dying=true dying_t=0 music(-1) sfx(11) endend
function draw_play() local sx,sy=0,0 if shake>0 then sx,sy=rnd(4)-2,rnd(4)-2 end if lvl==4 then draw_cloud_bg() camera(camx+sx,sy) else cls(0) grad(0,128,({5,1,2,13})[lvl+1],lvl>1 and 1 or 0) for i=0,11 do -- parallax dust motes (half-scroll depth cue) pset((i*37-camx*0.5)%136-4,18+(i*53)%92,i%2<1 and 1 or 5) end if lvl==3 then for i=0,11 do -- gentle snow: cold, almost free pset((i*13+3-camx*0.15)%128,(t()*12+i*23)%120,i%4<1 and 7 or 12) end end camera(camx*0.5+sx,sy) -- half-speed parallax: candles on a distant cave wall draw_bg_candles() camera(camx+sx,sy) local c0=camx\8 map(lvox+c0,lvoy,c0*8,0,18,16) if lvl==1 then draw_portal(544,64) end -- secret orb in the room past the level if lvl==2 or lvl==3 then local b=({5,1,2,13})[lvl+1] local wx0,wx1=max(camx,lvox*8+8),min(camx+127,(lvox+lvw-1)*8-1) for x=wx0,wx1 do local w=flr((1-sin(x*.07))*1.5+(1-sin(x*.04+1.5))*.5) if mget(lvox+x\8,20)==0 then line(x,31-w,x,31,b) end if mget(lvox+x\8,27)==0 then line(x,96,x,96+w,b) end end end end if entering and lvl~=4 then rectfill(0,32,7,95,0) end -- dark doorway gap: cave levels only (looks wrong on the bright cloud level) draw_decor() draw_pile() draw_balloon(balx,baly) for p in all(pkgs) do if p~=bf.carry then draw_pkg(p) end end for e in all(embers) do if e.imp then circfill(e.x,e.y,2,8) pset(e.x-2,e.y-2,8) pset(e.x+2,e.y-2,8) pset(e.x-1,e.y,2) pset(e.x+1,e.y,2) elseif e.pf then circ(e.x,e.y,e.t/7,e.t>9 and 7 or 6) else rectfill(e.x-e.t/6,e.y-e.t/6,e.x+e.t/6,e.y+e.t/6,8+e.t%2) end end for em in all(ems) do draw_flame_nozzle(em) draw_flame(em) end local ice=lvl==3 for i=1,#sprc do local px,py,c,a=fbpos(i) circfill(c[1]+cos(a-0.12)*13,c[2]+sin(a-0.12)*13,1,ice and 12 or 9) circfill(px,py,3,ice and 7 or 8) circfill(px,py,2,ice and 12 or 9) pset(px+rnd(2)-1,py-2,ice and 7 or 10) end draw_bird() local tn=#bf.trail for i=1,tn-1,2 do -- celeste-style laggy shadow trail (tapered) local tr=bf.trail[i] local c=tn-i>4 and 1 or 13 pal2(c) local sz=7+i*5/tn sspr(tr.col,0,16,16,tr.x-sz/2,tr.y-sz/2,sz,sz,tr.fl) end pal() skin_pal() if bf.iv>0 and bf.iv%4<2 then pal2(7) end sspr(bf_sx(bf.fl),0,16,16,bf.x-6,bf.y-6,12,12,bf.flip) pal() if bf.carry then draw_pkg(bf.carry) end draw_shouts() draw_names() for w in all(wisps) do for i=0,6 do local wx=w.x+i*4 local wy=w.y+sin(t()*w.spd+w.ph+i*0.4)*w.amp if i<2 then circfill(wx,wy,2.5,7) circfill(wx,wy,1,6) elseif i<4 then circfill(wx,wy,1.5,6) else circfill(wx,wy,1,5) end end end camera() if lvl<3 then drw_aemb() end draw_hud() if dlg then draw_dialog() end if dflash>0 then fillp(0x5a5a.8) rectfill(0,0,127,127,8) fillp() end if trans>0 then wipe(1-trans/11) end -- ember dissolve reveal (11 frames ~= 367ms; 100ms slower than the 8-frame original) if dying and dying_t>=20 then fadeover(5-(dying_t-20)*0.1) endend