This was my entry for #LOWRESJAM2018.
I am posting it here to share it with the pico8 community, and effectively "open source" the result. Feel free to remix and poke around. I've had to remove most of the comments to make it fit in the cart limits. But most of the code should be pretty self explanatory, there's nothing super fancy besides the generator and some efficient reuse of "render object" functions under the hood. The cart is nearly full, so you might need to do some serious memory hacking or make some code somehow more efficient in token use to make new AI/features. But graphics and tile layouts are all your playground too.
Since the chunk generators are based on parts of the tileset map data, even a newbie can have fun changing those and playing the results.
Let me know if you remix it into anything awesome, and enjoy! :)
Digging the premise. Not too far off from a thing I was working on once, actually...
I did hit a code block with the world generation (too much data) before I got finished with it, though.
As for feedback... the premise is fun enough, and you nailed the "weird music style" of the original pretty well. Only thing right now is that there seems to be no functional difference between the Electric Shots and the Missles - both of them effectively two-shot the spawn-shooting things, and that's the only real applicable property of them. About the only other change I'd consider is spiky/lava floors, to force the player out of that comfortable space on the ground, and to use the higher platforms. It's also possible to get stuck because some upper inclines have full-tile collision for some reason (even though the ground-level ones don't?).
I suppose the enemies per depth/area could HP creep a little higher than they currently do.
Still, the simplistic approach to world design has me reconsidering a revisit, but with less code creep... :)
Still not 100% sure what I'm doing with "sand temple;" but the other areas look legit enough. The bright green spiky area is poised to be flooded and traversed with a swim/fly upgrade. Or maybe wall-jumps and air-dashes if we wanna be really dicky about it? No, this isn't MegaMan X6 endgame here.
That looks to be quite a nice start actually! A bit more structured then my crazy "chunk id" setup, for sure.
The ground ramps literally push the player up, and I ran out of tokens to make the roof sprites do the same. The game has no concept of slopes actually, I had trouble finding a solution that didn't involve a lot of dead reckoning or multiple collision passes. I had literally about 500 tokens total for the player movement, so lots of things had to suffer. ;)
Electric shots go through walls, but I will admit it doesn't come up often enough to matter. They were originally supposed to also "stun" enemies, but the enemy types planned for the gun never made the token cut. For what it's worth, the shots DO deal more damage then missiles, just not enough to be noticed on anything besides the boss fight. :p
I had a lot more plans for additional enemy types (classic bug pipes, greemer-like wall crawlers, basically every missing metroid staple really).
I also originally had a bit more advanced room generation, but was having trouble teaching the game to generate appropriate height jumps for the power up levels. For token space, I eventually had to "flatten" the floor gen to fit into a single room type. I also had to generate the map an entire row at a time using a deck/draw system to place the powerups. With that new system I lost the ability to "teach" the generator about the order of things on a floor. Sadly the current generator doesn't know much other then the Y depth being built at the time.
Lava was another concept that had to be removed to fit the boss into the token space. I did manage to fit the heat suit, but yes, it's not really the same. ;)
Doors got the cutting floor as well. The p8 file actually still has the sprites for the doors, and some concepts of other boss attacks using sprites that haven't been colored in yet. Fun little hacker treasures from before I slammed into the token/cart compression wall.
Were I to revisit the concept again, I'd probably go for a more structured layout of room data like you have above. Then the code could build each room out of corner chunks instead of this crazy 4x4 chunk/prop system I created for this version.
Technically, if I could make the generator use the same flags as some of the player/enemy/status variables, I guess that could shave off a good amount of tokens. I had ended up too far into the code before I had thought of that solution though.
Maybe I can take a stab at doing Infinitroid 2, and structure the game better this time. Would be a good opportunity to see what I could do if the game wasn't 64x64 res as well. Maybe I can fit "morph ball" that way. :)
Really neat game. Too bad the Konami code doesn't do anything special... Pretty neat.
For slopes, I usually do a 3-point bottom-up conditional and move accordingly. (+1*facing) as the constant x, and then y+1, y, y-1 sequencially.
There's a couple potential sticky points where you can go up slopes, but if there's a wall neighboring it, you can't go back down (even though it feels like you should). Again, I typically stick to the floor because there's literally no incentive to go higher yet.
I do have to find a way to make my redux "jump" from one scene to another, preferably using a table or string for each story to determine where to go. I'd totally be down for a collab sometime. Also, I don't think you teach it so much about "in order" for the floor, so much as you have the variance carry through to the FOLLOWING floor. For instance, if there's two jump upgrades, I'd make the floor after the second one start the possibility of requiring at least one to be collected; and at least two after all four of them.
My layouts on my thing do include some utility for a small high-jump upgrade, but it's totally optional to the layouts and really just makes navigation more streamlined as opposed to "necessary;" which I think works better with the unpredictable PCG approach anyhow. That's the secret with Isaac - none of the items are necessary, but they can make things easier (or harder)! FTR, I'm actually starting off with a default 2.5 tile jump height (that extra .5 gives the player some margin of error, but not necessarily additional clearance).
Originally I had these big, open "space rooms" where I tried to use code to write tilesets dynamically, make feedback loops with items and occasional boss rooms that connected hubs... and was just WAAAAY beyond PICO-8 scope. The scope really is a challenging thing to deal with!
In retrospect, I think it was the same kind of thing you were trying to do with your "chunk ID thing," and it might just be better to use a collection of intentionally-designed spaces. I did these newer subrooms to make the ball utility and vertical space important; and doing something more patchwork like this (maybe include a few "probablistic" tiles/objects?) might free up more code space that you can use for that enemy variance; which would 100% be my priority in its place. If nothing else, the greemers with some kind of HP creep... and really, there's a lot of potential with very vanilla enemies (like vertical/horizontal space controllers, or a standstill enemy that shoots a pattern every n frames; since they're really all about controlling screen real estate more than "threatening" the player).
The Konami code does the coolest thing. The same as the other codes, determine the world around you! I really like that as a seed input, btw. I might steal that for my procedural DDR-in-progress thing.
--gentroid is a pgc metroid fangame --made in 2015 by tony "the tgr" wolverton --inspired from "gentrieve" by phr00t and self --note:working on tiles/collision --and worldgen/doorid handling. mainpower={ball,bomb,grenade,double,dash,unltd,missile,supmiss,icemiss,hook,scuba,jet,win} subpower={spread,burst,bank,laser,boost,warp,blade1,blade2,blade3,bombup,grenup,missup,smisup,imisup,shotup,battery} direction={left,right,down,up} gravity=1 jumpheight=3.5 jumpvel=-2*sqrt(gravity*jumpheight*8) maxdrop=8 maxrun=8 weapon=0 scenetype=0 topblock={32,33,34,35,36,37,38,39,40,41,42} botblock=topblock+16 biome=biome%8 tile={air,water,lava,solid,dests,desta,links,linka,fake,damage,pushl,pushr,sticky,zerog,door} hallway=1 --length of v/h corridors hwrap=0 vwrap=0 function shufflepower() add(sortpower,shot) for n=0,count(mainpower) get = rnd count(mainpower) add(sortpower,get) del(mainpower,get) n+=1 return for n=0,count(subpower) get=rnd count(subpower) add(sublist,get) if get=bombup or grenup or shotup or missup or imisup or smisup or battery then n+=1 return else del(subpower,get) n+=1 return end --room drawing functions --rooms are 12 wide, mget xs are all multiples of 1.5 --rooms are 9 tall, mget ys are all multiples of 1.125 function makestart(biome) --starting room, 2x2 hub mget(8,0) end function maketrans(biome,sortpower) --single room, 1x1 mget(0,2) end function makeprize(biome,perlin,prize) --single room, 1x1 mget(0,2) end function makevcorr(biome,hallway,playlevel) mget(2,0)--top for hallway>0--middle mget(4,0) hallway-=1 return mget(6,0)--bottom end function makehcorr(biome,hallway,playlevel) mget(2,2)--left for hallway>0--middle mget(2,4) hallway-=1 return mget(2,6)--right end function makehub(biome,playlevel) --2x2 room again mget(8,0) end function makesolve(biome,playlevel,sortpower) --2x2 room, past weapons make obstacles else mget(8,0) end function makeboss(biome,playlevel,boss) else mget(8,0) end --the game has a list of sceneid --it uses sceneid to copy map --and edit copies of map for content function drawroom(sid) return sid{biome} return sid{scenetype} return sid{playlevel} return sid{doorid} if scenetype=0 then makestart(biome) --move player to door id if scenetype=1 then return sid{sortpower} maketrans(biome,sortpower) if scenetype=2 then return sid{perlin} return sid{prize} makeprize(biome,perlin,prize) if scenetype=3 then return sid{hallway} makevcorr(biome,hallway,playlevel) if scenetype=4 then return sid{hallway} makehcorr(biome,hallway,playlevel) if scenetype=5 then makehub(biome,playlevel) if scenetype=6 then return sid{sortpower} makesolve(biome,playlevel,sortpower) if scenetype=7 then return sid{boss} makeboss(biome,playlevel,boss) end function nworld() --new world shufflepower() sid=1 --scene id, 1=start hubdoor={u1,u2,l1,l2,d2,d1,r2,r1} did=1 biome=rnd(0,6) --first biome scenetype=0 playlevel=0 --start does not generate enemies/hazards hubid=0 hubmax=0 sid+=1 --give one main weapon direction=rnd(0,3) hubdoor=rnd(0,7)=did scenetype=2 return sortpower{1} did+=1 sid+=1 --give one subweapon direction=rnd(0,3) hubdoor=rnd(0,7)=did scenetype=2 return sublist{1} did+=1 sid+=1 --make one hallway, start enemies playlevel+=1 direction=rnd(0,3) hubdoor=rnd(0,7)=did biome+=rnd(1,5) hallway=rnd(1,4) if direction=left or right then scenetype=4 else scenetype=3 did+=1 sid+=1 --first hub, main cycle begins after this hubid+=1 hubmax+=1 hubdoor+=4=did scenetype=5 did+=1 for playlevel=1 to 5 --main loop do for p=1 to 3 do --sub loop sortpower+=1 --bump up a power sublist+=1 --bump up a secret hallway=rnd(1,3) direction=rnd(0,3) hubid=rnd(2,hubmax) hubdoor+=direction*2+rnd(0,1)=did did+=1 sid+=1 --transition room scenetype=1 did+=1 sid+=1 --hallway if direction=left or right then scenetype=4 else scenetype=3 did+=1 sid+=1 --turnaround hub hubid=hubmax+1 hubmax+=1 hubdoor+=4 scenetype=5 did+=1 sid+=1 --hallway back if hubdoor%2=1 then hubdoor-=1 else hubdoor+=1 if direction%2=0 then direction-=1 else direction+=1 door=(door+1)%2 if direction=left or right then scenetype=4 else scenetype=3 did+=1 sid+=1 --prize room scenetype=2 perlin=direction prize=sortpower did+=1 sid+=1 --secret room (from hallways) scenetype=6 solve=sortpower did+=1 sid+=1 --prize for secret scenetype=2 perlin=direction prize=sublist did+=1 p+=1 return sid+=1 --transition room direction=rnd(0,3) scenetype=6 solve=sortpower-2 did+=1 sid+=1 --hallway to boss biome+=rnd(1,5) playlevel+=1 hallway=rnd(1,5) if direction=left or right then scenetype=4 else scenetype=3 did+=1 sid+=1 --boss room/new hub hubid=hubmax+1 hubmax+=1 scenetype=7 bosslives=1 --deactivate 'boss' once beaten boss=sortpower-1 did+=1 return --now, playlevel=6, endgame --insert endgame, this is final item sid+=1 scenetype=2 perlin=direction prize=win did+=1 --make a boss at the start room save world.sav end function hiprob(prob80,prob20,prob5,prob3 or =prob5,prob2 or =prob3,prob1 or =prob2) local roll=rnd(0,100) if (roll=1) return prob1 if (roll=2) return prob2 if (roll=3) return prob3 if (roll=4 or 5) return prob5 if roll<20 and roll>5 then return prob20 else return prob80 end function loprob(prob60,prob30,prob9,prob1) local roll=rnd(0,100) if (roll=1) return prob1 if roll<10 and roll>1 then return prob9 if roll<40 and roll>10 then return prob30 else return prob60 end function _init() px=92 py=64 pstate=1 paspr=0 --top sprite pbspr=8 --bottom sprite pdir=right pat=0 --state timer function _draw() --draw the world --draw the player if pstate=6 and scuba=1 then spr(paspr,px+(8*pdir),py,1,1,pdir) spr(pbspr,px,py,1,1,pdir) else spr(paspr,px,py-8,1,1,pdir) spr(pbspr,px,py,1,1,pdir) end px+=xspeed py+=yspeed --finite state machine for player function cstate(n) pstate=n pat=0 end function groundck() --check position under the player v=mget(flr((px)/8*gravity,(py+8)/8*gravity) w=mget(flr((px+7)/8*gravity,(py+8)/8*gravity) return not fget (v,0) return not fget (w,0) function wallchk() v=mget(flr((px+xspeed)/8,py+12) return not fget (v,0) function _update() b0=btn(0)--l s left b1=btn(1)--r f right b2=btn(2)--u e up b3=btn(3)--d d down b4=btn(4)--z tab jump b5=btn(5)--x q shoot b6=btn(6)--n shift select b7=btn(7)--m a start pat+=1 if hwrap=1 then (px+96)%96 if vwrap=1 then (py+72)%72 if pstate=1--standing if groundck=1 then cstate(4) if xmove<0 then pbspr=18 for xmove to 0 do xmove+=1 else if xmove>0 then pbspr=18 for xmove to 0 do xmove-=1 else pbspr=16 if b0=1 then cstate(1) if b1=1 then cstate(1) if b2=1 then --aim up paspr=1 shotdir=up else paspr=0 shotdir=pdir if b3=1 then cstate(2) if btnp(5)=1 then shoot(weapon,shotdir,4) if btnp(4)=1 then yspeed=jumpvel cstate(4) if btnp(6)=1 then weapon+=1 end if pstate=2--crouching paspr=nil pbspr=19 if xspeed<0 then xspeed+=1 if xspeed>0 then xspeed-=1 if groundck=1 then cstate(4) if b1=1 then pdir=left if b2=1 then pdir=right if b3=0 then cstate(1) if btnp(4)=1 then if ball=1 then buzz+=1 paspr=nil pbspr=83 if b4=0 then spindash(buzz) cstate(7) else yspeed=jumpvel cstate(4) if btnp(5)=1 then shoot(weapon,pdir,0) if btnp(6)=1 then weapon+=1 end if pstate=3--walking if groundck=1 then cstate(4) pbspr=16+(pat/2)%3 if boost=0 then maxrun=4 else maxrun=8 if b0=1 then pdir=left for 0 to -maxrun do xspeed-=1 if wallchk=1 then xspeed+=1 if b1=1 then pdir=right for 0 to maxrun do xspeed+=1 if wallchk=1 then xspeed-=1 if b2=1 then cstate(2) if b3=1 then paspr=2 shotdir=up if b3=0 then paspr=1 shotdir=pdir if b4=1 then yspeed=jumpvel cstate(4) if btnp(5)=1 then shoot(weapon,shotdir,4) if btnp(6)=1 then weapon+=1 end if pstate=4--airborne if yspeed<-2 then pbspr=17 if yspeed>2 then pbspr=18 if -2>yspeed>2 then paspr=19 and pbspr=nil if boost=0 then maxrun=4 else maxrun=8 if groundck=1 then if yspeed>6 then cstate(3) else cstate(1) if b0=1 then --walljump check (add) if wallchk=1 and pdir=right and yspeed<2 and b4=1 then pdir=left xspeed*=-1 yspeed=jumpvel paspr=nil pbspr=66 else pdir=left for 0 to -maxrun do xspeed-=0.5 if wallchk=1 then xspeed+=1 if b1=1 then --walljump check if wallchk=1 and pdir=left and yspeed<2 and b4=1 then pdir=right xspeed*=1 yspeed=jumpvel paspr=nil pbspr=66 else pdir=right for 0 to maxrun do xspeed+=0.5 if wallchk=1 then xspeed-=1 if b2=1 then shotdir=down paspr=67 pbspr=nil if b2=0 then shotdir=pdir cstate(4) if b3=1 then paspr=66 pbspr=nil shotdir=up if b3=0 then shotdir=pdir cstate(4) if b4=0 then if yspeed<-1 then yspeed=-1 if btnp(4)=1 then if double>0 then double-=1 yspeed=jumpvel if btnp(5)=1 then shoot(weapon,shotdir,4) if btnp(6)=1 then weapon+=1 end if pstate=5--recoil yspeed=-3 for pat=0 to 8 do if pdir=left then xspeed=2 if pdir=right then xspeed=-2 if wallchk=1 then xspeed=0 return if pat=9 then cstate(4) if pstate=6--liquid if scuba=1 then if b0=1 then xspeed+=-2 pdir=left if b0=0 then xspeed=0 if b1=1 then xspeed+=2 pdir=right if b1=0 then xspeed=0 if b2=1 then yspeed+=2 if b2=0 then yspeed=0 if b3=1 then yspeed-=2 if b3=0 then yspeed=0 if btn(5)=1 then shoot(weapon,pdir,0) else--no scuba equipped pat-=0.5 if xspeed>0 then xspeed-=0.5 if xspeed<0 then xspeed+=0.5 if yspeed<0 then yspeed+=0.5 if yspeed>0 then yspeed-=0.5 if yspeed>6 then yspeed=6 if wallchk=1 then xspeed=0 if groundck=1 then yspeed=0 if btn(5)=1 then shoot(weapon,pdir,4) end if pstate=7--rolling paspr=0 pbspr=2+pat%2 if buzz>4 buzz=4 if facing=left then xspeed=buzz*-2 else if facing=right then xspeed=buzz*2 if wallchk=1 then xspeed=0 and yspeed=3 and cstate(5) if btnp(4) then yspeed=jumpvel if btnp(5) then if bomb=1 then shoot(bomb,down,0) if btnp(6) then weapon+=1 if pstate=8--dashing if pstate=9--dead end |
I literally whittled all of that worldgen code out because of this!
The big sticking point that really changed my approach was the realization that procedrual != random. I'm not gonna get away with teaching the code how to be the designer - I have to plug the design in, and just have the code shuffle the cards. Procgen will never do the heavy lifting part FOR you, it just allows you to do more things WITH it.
Could "Electric Shot" be a triple shot, since the boss uses that firing pattern? Then just normalize the damage - now you have a power utility for the damage sponges, and a space utility for the crowd control!
[Please log in to post a comment]