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. :)
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]



