Log In  
:: Unfold ::

Cart #a4343-0 | 2022-12-06 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

"Legend of the ChunChunTree" is a 2d turret-defense wave survival shooter featuring an rpg-style castbar system, tons of buttons per button, a kinetic-physics system, and the ability to simultaneously control two overloaded weapons(the bullet-hungry 'TriMG' and magical-casting 'Epica'). V1 #FreeToPlay inbrowser on #Pico8 bbs and download(click)/cart/splore.

Practice: <controls_overlay>redDots show fingerRest positions; redPoppies(blackDotWithRedDots) show keys with tap/tap2/hold states; practice waves with f,u,h,i,g,j,y (recommended).

Survival: Keep your hp>0 for 400s. Score 웃---(least dmg taken per run) is saved at end of each successful run (lower is better; post me your best[the score to beat is 웃345.5]).

Sandbox: the waves section of the code has been partitioned and commented to allow easy modification should anyone wish to create a hardmode or alternative experience [copy code into external editor to view with formatting].

Requires keyboard+mouse; Controls are referenced using QWERTY layout; any layout will work provided there is no overlap with pico8 reserved keys(pause/menu/etc); Download+Original GameJam Version: https://cerbyo.itch.io/againsthecurrent-the-legend-of-the-chunchuntree; the download/cart/splore versions of the game feature better audio and performance.

Quickstart Guide (compliment with the ingame ui + practice):

  • Firing buttons 's' and 'lb'(leftMouseBtn) each have 3 keystate(tap/tap2/hold) depending on duration of keypress(topLeftBars).
  • Casting occurs when the rectangular bar appears above and you flash yellow; lb/rb cannot be used during.
  • Shift(used with 'a' and 'd') and spacebar(dodge) each have two keystates(tap/hold).
  • The default setting of blue triMG crosshairs means holding shift decreases the turnrate of 'a' & 'd' (sniping).
  • Basic brown has 100hp. You can use its own bullets against it to the following effect(key):dmg(hitsToKill)
  • Dodging(spacebar) while staminaArc[100,-100] is yellow[49,25] assigns stamina(sta)=-100.
  • Foe that get into melee range become lickers: extra melee-damage, gain hp and speed(reverts on condition).
  • Orange armour can be burnt off or bypassed(r/tap s); but if your screen is too full their firepower can be managed through conditions/grenade/machinegunSpam.
  • Simplify: flameShield(rb)+machineGun(hold s)+dodge(spacebar) is solid build to compliment skillbar(123456qert) [see BuildList:DODGER].
  • Health(hp) regen methods:heal(6)[+30hp]/gunflame(3) on '2 lined-up foe'/lvlUp[+30hp]/spacebar aoe[+5hp].

Quickstart Techniques (gif left->right below):

  • SelfNading:dodge(space)[violet] or teleport(t)[blue] grenade impacts.
  • ChainDodging:double/triple(netshot(2))/quad(tricky!)dodges with optimized timing are good for melee range.
  • FlameCero:hold lb + volley(1)/tap lb/netshot(2).
  • BurningZombie:tap s till foe dead then hold lb to burn; grenade to reposition.
  • BlueVolley:combination of teleport(t)+volley(1)+cancel(~). Teleport(t) short distance from yourself then volley(1) into blueSparkles then just as casting is about to end you cancel(~) and then teleport(t) back to your old location and volley(1) again into the blueSparkles. Strong vs darkBlue.
  • BlueTags:shoot tags(tap s) into blueSparkles(t).
  • OrangeDrop:vs orange armour use volley(1) at 'pointBlankRange'(i.e. teleport(t)+volley(1)) or netshot(2)+pointblank+cero(tap s)).
  • Catching: flameShield(rb) converts 'all' bullets to sta; grenade it; teleport(t) and 'catch' bullets; catching trail-effects(dCannon/cero/volley/netshot/gunflame) regens ALL sta, gunflame(3) doesnt need teleport to catch.
  • Panic-mode-skills:BiteTheLip(hold spacebar); godmode(q); smoke(5)+gunflame(3) to clear screen & regen hp.
  • Hellmouth(i) Tips:[barriers]teleport behind(not melee-close tho) & use hitbox for bullet-defense; [ovens]create/pos burningZombies on the opening.

Ready for Survival? Spawn two Hellmouth(i) at once, if you can manage you are ready to (b)egin

◘Story◘ It is the year 305, an era of war. Wizards and cute cuddly animals have been fighting for years, with territory becoming heavily contested. You are a mobile-fortress-tree-unit, created by the Great Wizard ChunChun. One of many tree units created to defend territory against the furry menace...

THE MENNAcING Weasel Super Soldiers were ordinary rodents genetically modified in their early twenties by a diet of pure icecream. It was hoped that goopy stomachs and inflated cheek bones would make the body more resistant to all things magical. CODENAME:OPERATION_ICECREAM_GUT proved effectively beyond all skeptical belief, and the program quickly branched into various divisions of flavour: The YOLO black liquorice corps, the "I'll do anything that makes that guy uncomfortable" wasabi brigade, and the choco hungry basics whose leetest of members would dip themselves orange with thick heavey hardened caramel armour. But among these divisions, science found that some weasels (~.1% of the population) were born with NO TASTE BUDS! These weasels refused to eat ALL flavours of icecream...so the furry scientists developed an artificial cream....one so synethic and abnormal and devoid of all things natural and wholesome that it created a dangerous new breed of weasel known simply as..BUBBLE GUM BLUE!
Where Operation Icecream Gut offered weasels some protection against the magical arts, it made them fat, heavy, slow, and more vulnerable to traditional weaponry that normally only the whitest of wizards would dare to use. But as the war dragged on even the most based of Wizards began to equip their familiars with all manner of weaponry...
When Great Wizard ChunChun enchanted you, he gave the gift of conscience. You seemed a little, 'odd' though.. a bit, 'different' from the typical magically embued tree. For one, you had a panting tongue and bulging eyes...almost like a dog. And two, you drooled a lot -_-.
You see, self-watering, although certainly useful, was not a common trait in magical trees. And ChunChun, upon inspection of his creation, became concerned for his 'reputation'..
ChunChun had postulated the theory that your physical appearance may have implanted in your mind, sympathy, to the animal cause, or more accurately, the cause of choosing to 'not do anything' as the enemy passed by. An 'aloofness of the mind' was a less than ideal a trait for one to showcase in their magical arts.. So, better sorry than safe, ChunChun implanted in you the order TO DESTROY ALL THINGS FURRY!

Now ChunChun was not considered a wise wizard by normal wizarding conventions. But he wasn't stupid either. He knew his soft fluffy beard hair could be mistaken for the quality of fur. So he spent his dying moments writing this fact down on your barked skin as the last of his lifeforce oozed out of him all over his humiliating creation:

..So became the legend of the ChunChunTree, a glorious creation embued with godlike powers by the
 magnanimous high-wizard whose name will forever be remembered in fondness over any and all other 
singular moments in ti-...

..At least, that was the end of the script ChunChun had scribbled frantically on you as he gurgled his final breath.

Prophetic, Legendary, Accomplished! The words were not lost on you as the ages rolled by. Unfortunately, at the moment, it seems you are stuck in the present. The 'ages rolling by' were a long ways off, even more so with the heavy attractive stench of wizard blood all over you. 'You', a makeshift weapons platform, who is presently stuck in the current of a war you were never meant to survive..

You are the ChunChunTree. The enemy is coming. Prepare yourself!

Full Manual:


(w)BeeShield: reflects 'all' bullets adding a small dmg multiplier
(a|d)Turning: adjusts angle of fire
(s tap)Tag: bypasses orange armour; if no foe along current trajectory bullet turns towards nearest foe(ea turn drains life exploding on 0); creates zombie(block foe/bullet; burningZombie -50% life); rnd chance to inflict confusion/immobilize.
(s tap2)dCannon: fires two high-kinetic 50dmg shots [see: kinetics]
(s hold)Machinegun: decreases foe accuracy 'on-hit'; grants sta on-kill; rnd chance bypasses orange armour
(shift tap): toggles between blue(slow hold-TurnRate) and yellow(fast hold-TurnRate) crosshairs
(shift hold): modifies turnrate when using a|d based on crosshair colour
Rage(red triMG/eyes/bullets): TriMG attacks become red,unblockable,2Xdmg(tags 3Xdmg),increased kinetic-values (max 15s).

(rb/lAlt/lCMD)FlameShield: absorbs all bullets+grants sta
(middle/c)Pulse: low dmg aoe that destroys enemy bullets; pulsing purple indicator around the hp-orb when available: can be spammed or used 2-4x individually; cd[10s] activates after all charges used.
(lb tap)Cero:cd[0.6s]pushes foe backwards; bonus dmg against immobilized foe; flameCero
(lb tap2)Grenade: increased dmg based on number of foe hit
(lb hold)Flame: burns orange armour; lights foe/cero/zombie onFire; types:green,purple(catchFireFaster+lowerBurnDmg),yellow(grantHP),violet(~2xdmg),blue(~10xdmg)
Alacrity(purple Epica): Half's cooldowns(red-text); Epica's modifier: 0.3cd on cero(2XfireRate) ; adds +1 to number of foe hit by grenade; enables purpleFlame and yellowFlameCero.


(~)Cancel:/cd[5s] aka ~ tilde key: cancels any casting(yellow flashing/rect box overhead)
(1)Volley:/casting[3s]/cd[15s] During casting fires 46 lesser-cero; flameCero
(2/lCtrl)Netshot:/5dmg/casting[.25s]/cd[18s]Dodge after cast; On-hit:6s immobilize;+alacrity; flameCero
(3)Gunflame:/100dmg/piercing/confusion/casting[0.5s]/cd[16s] If, On-hit, there is a second foe along the current bullet trajectory gunflame turns red and grants +5hp continuing to the second foe, otherwise refires at rnd foe onScreen. Process repeats until gunflame misses[Can kill you].
(4)Gunbash:/100dps/confusion/cd[9s]/duration[~1s] On-hit +alacrity(perFrame), flings foe in cursor's direction with +bodyDmg[pinball mechanic]
(5)Smoke:/casting[.75s]/cd[60s]/duration[15s] Blinds 'most' foe causing them to mistake the player position.

Orb Display:
Orange[100,50]/yellow[49,25]/grey staminaArc[100,-100]
PurplePulsingRing(indicates if pulse(mb/c) available)
RedOrb/text## indicate current hp[99,0]

(tap)Dodge:-50sta(=-100sta if yellowArc[49,25]); DodgedBullet(violet+friendlyFireOn+~2Xdmg); meleeDmg & single +5hp steal from foe inRange
(hold)BiteTheLip: ~70% chance to Dodge attack; slowly drains hp if >10hp; enables alacrity; friendlyFireOn for all fired bullet.

(6)Heal:/casting[5s]cd[25s] +30 hp; resets gunflame(3) cooldown
(q)God:/cd[30s]/duration[6s](turn lightgreen) immunity to all damage
(e)Harden:/cd[40s]/duration[15s](turn pink) halfs incoming damage; +1 energy +1s alacrity when hit.
(r)Rage:/casting[2s]/cd[20s] TriMG attacks become red,unblockable,2x dmg(tags do 3xdmg) for +10s
(t)Teleport:/cd[1s] teleport to cursor's location; Icon changes to "R". Press again to return to original location cd[20s]. During both teleports the player leaves behind blueSparkles. 'Any' bullets passing through blueSparkles will turn blue receiving a massive damage multiplier.

Misc: v(toggle the background(bg) on/off); n(add tree); m(remove tree); n+m(reposition trees); ArrowKeyLeft/right(toggle cursors for epica); ArrowKeyUp/Down(toggle bg if bg on); comma(music on/off)

Leveling:(Sfx+redSparkles+spriteChange) levelUp grants +30hp, & up to lvl5 grants small maxHp boost up to 99hp.


Each bullet-type inflicts a kinetic value on a foe; this includes both an initial on-hit jiggle and a total kinetic which contains the values of all bullets from the last ~8frames of foe-life. The higher the value the farther+longer the body will fly backwards on death. Bodies flying backwards inflict damage and force on all foes that make contact. Higher ranges of kinetic value grant +exp and are represented by unique body and corpse sprites (original colour(3type)->+red->+red+blue->+purple->+flying->yellow). i.e. firing machinegun directly at foe without missing from 100%->0% hp(high kinetic value) vs missing sporadically(low kinetic value). You can gain up to an extra +6exp per kill, making 1 brown worth the exp of 4. Allows for something to be said about chronic overkill should you wish to train both guns on a single target.


Orange:200hp;armoured;slow;heavy-burst-machinegun;attacks fleeing foe;orange/armour removed via flames(turning brown);cero:explode unless pointBlank; flameCero cause onFire visually but no effects until brown.
Green:50hp;fast;higher chance to stop and blend in with trees
Commando/navyblue:150hp;variable curved movement;slow-firerate;decreased accuracy
Black:50hp;fastest;don't fire
Blue/bubbleGumBlue:500hp;increased firerate;decreased accuracy;circular movement
Hellmouth:indestructible tunnel opening appearing at set locations for preset duration; spawns browns; last few seconds has chance to spawn blacks.

Foe States(turns friendlyFireOn [except stopped/melee]; see smoke(5) for blinded):
☻Confused(white halo):walks back and forth firing in random directions
☻Immobilized(purple circle):stuck inplace for short time, changes fire direction
☻KnockedDown:upside down, still moves when onFire/body, changes fire direction
☻onFire:(rnd fireResist## drained via flames) Jiggles towards a rnd foe onScreen(if any), accuracy decreased, any foes touching may also catch fire.
☻Flee:(rnd chance if hp<rnd#) foe will whimper & attempt to limp away from the player position until they get offScreen, if foe remains onscreen for ~2s you get a rage-bonus on-kill, the longer the foe remains the larger the bonus(i.e. netshot/teleport/grenade can help). Orange will shoot any fleeing foe (hitting anything in the way). In SurvivalMode Hornblows cause all foe to flee.
☻Stopped:(rnd chance foe stops moving a rnd radius from player) stopped foe remain stationary until they are drained below a certain hp or displaced with certain bullets like cero; certain foe have increased/decreased chances of stopping(green/black).
☻Licking:foe in melee range will lick you, dealing extra damage, gaining hp+speed
☻Zombie:tag kills; duration:40s(onFire halves); low fireResist; blocks foe/bullet; reposition via grenade/cero

~~~~~~~~~~~~~~~~~~~~~~~ BuildList ~~~~~~~~~~~~~~~~~~~~~~~


  • No 's' or 'lb', just shields+skillbar+etc


  • Tags+flame in unison as primary dmg
  • Reposition zombies via grenade/cero/tele for defense
  • Counter large wave with flameVolley and blueTags


  • Use dCannon+rage as primary dmg
  • Compliment with grenade spam to knock foes down to patiently wait their turn


  • FlameShield(rb) as block to +sta
  • Grenade/cero/flame/shoot the flameShield to +sta
  • MachineGun as primary dmg to +sta
  • Dodge frequently to: create dodgedBullets(violet), clear licking foe +hp, gunflame+dodge to clear+hp


  • Machinegun+flame 24/7 [Note:suffers performance issues, use optimizations]
  • Rage+alacrity to boost effectiveness against specialty waves

~~~~~~~~~~~~~~~~~~~~~~~ Optimizations ~~~~~~~~~~~~~~~~~~~~~~~~

Remove Trees(m); Remove Background(v); Limit usage of the MachineGun(hold s)+flame(hold lb) at the same time esp during camera-move segments; kill foes fast and intuitively i.e. use grenades vs clumps, dcannon to snipe, gunflame to clear the screen, etc; casting smoke(5) will significantly improve performance when many foe onscreen(esp during camera-move segments).

https://www.youtube.com/watch?v=CZ6k9J8ebhU [Run: 345.5]

https://www.youtube.com/watch?v=4kFJHOE9AMk [twitterPromo]

Desire a more arcadey and roguelite experience with secret unlockable character/buffs/stats/weapon? Consider Teatime!


P#121717 2022-12-08 01:34 ( Edited 2022-12-11 12:37)

:: Unfold ::

I'm going to start with my general understanding and label questions [Q#] with the hope someone can fill in the blanks. Consider the discussion of 'cost' in terms of speed/cpu. I'm hoping there's a general concept I'm missing that will cleanup the flurry of questions around [Q2], regardless please bear with me:

General code is written topdown like the below where you define the function at the top and then you can simply call said function below that point to access the block inside. I don't fully understand the cost, in terms of lua itself, associated with defining 'function a()end' (the pico8 wiki has general pico8 cycle costs). My understanding though is the program won't look inside of the function 'until' I call it and so there is simply some generic predefined 'base cost'(cpu cycles) to defining a function and this cost is not affected by the number of parameters or whatever is inside the function....tldr it's always a flat cost each time the compiler/better-word runs down that part of the page and sees you want to define a function. So this usually amounts to the general idea of 'define the function once at the top of the code' and then call it as many times as u need where u need to after:

function a()end
for i=1,100 do a() end
--vs: for i=1,100 do function a()end a()end; defining the same function 100 times is bad.

However, the defining of function, like most parts of lua, can be optimized through the limiting of its scope. Again my understanding of scope is limited(puns), but as a functional explanation it amounts to 'smaller scope of an item = less cpu cost of the item'. Turning functions local makes it apply to 'just the' remaining part of the block it is currently in, and you can further limit the scope of a larger block with 'do end'.

--code above--
local function a()end
for i=1,100 do a() end
--code below--

I think I got the general idea above. My main confusion lies when you toss the idea of a gameloop into this picture. I'm assuming there is no secret difference to how _update() and _draw() handle functions outside of the idea _draw() might skip frames, so Let's take the example below which seems to be how most games orient things. My understanding in pico8 being: everything runs top-to-bottom 1x at the start of a game(unless I'm doing function calls in the middle of the ocean, the stuff in the functions will not be run yet)....then everything inside _init() runs 1x...then everything inside _update() and then _draw() runs over and over xx times per second.

function _update()a()b()end
function a() end
function b() end

If we wanted to define the above functions as local, the next example below shows the change in order necessary to do so...and there seems to be no change in cost(ctrl+p testing values in pico8) between a local function a() or global function b()...since they seemingly have the same level of scope and are only defined 1x each (is my understanding as to why). [Q1] So that trope I hear from the love2d community and sometimes in pico8 to 'just redundantly define everything as local' is wrong....but doesn't inflict penalty.... so it's really just a habit builder to protect against mistakes where u otherwise would find benefit from limiting the scope...?

local function a(x) return x^4 end
function _update()for i=1,10000 do a(10)b(10)end end
function b(x) return x^4 end

Okay so we now have base cases for gameloop and nongameloop above. So Let's look below where the cases overlap. In these situations I'm left confused and often wondering if there is some kind of benefit to 'defining the function within a block within the gameloop'... or just to revert to the above where u toss the function outside everything with all the other global functions..aka where it only gets defined 1x at the start of the game and never again. Take this info() example:

function _draw()info()end
function info()
if time()==2 then print("<5")end
if time()==3 then print("<5")end
if time()>5 then print(">5")end

[Q2]Let's say I wanted to keep everything in this truly horrible format, but create a function for the redundant print("<5") blocks. If the function I want to use is: function p()print("<5")end
Which (A/B/C/D/?) position should I use to define function p()... and why?

function _draw() 
--D) local function p()print("<5")end

--C) local function p()print("<5")end --if [Q1] was correct then this position and A are the same thing

function info()
--B) do local function p()print("<5")end
if time()==2 then p()end
if time()==3 then p()end
--B) end
if time()>5 then print(">5")end

--A) function p()print("<5")end

I would think B would be the correct position....but then I consider the fact that B is a function being defined every single frame in a loop....and the code where we call p() is only actually running for two frames in the game. I would think any potential saved overhead those two frames might be undone every other frame by this fact, and that A) would be the appropriate answer. But what if info() only ran for specific frames and every time it did run the p() was called without exception? I would think then B is indeed the correct answer since it mimicks the nongameloop ex at the top of the page.... But that still assumes the cost per defining each frame is still less than the amount saved by calling a local function....wud that change based on how many times I need to call that local function.
What if info() was in a for-loop that ran a thousand times in one of those combinations....what if it was just p() itself?

function _draw() 
--D) local function p()print("<5")end; [Q3] is D now the best position?
 for i=1,1000 do p() end

[Q4]What if environments come into play...a local function defined in the current environment each frame of the gameloop vs a global function linked through a metatable from another environment that's defined just 1x outside the gameloop. Here's an earlier example I had: If the rest of the game is defined in the environment of _G = _ENV _G.__index = _G and if add_bullet(...) is called each time in _update when the player presses the fire button...and there are variable chances of the bullets fired actually being of the type (10%/50%/90%) that call the function trimg_xy() if at all... where should I place the function trimg_xy? in A. or B. or C. or other?

I guess all these questions comes back to: [Q5] how much does this all compare to the cost of defining a function 1 or more times every frame within the gameloop vs 1x outside of it? Cause that's effectively what this is...do we define a function 1x like A) outside of everything.....or define it xtimes every frame....and is there a different payout depending on which per each conceivable scenario.

P#121503 2022-11-28 03:29 ( Edited 2022-11-28 03:58)

:: Unfold ::

I have a stationary object firing bullets at foe(enemy object). The problem is the game is running at 30fps, and the bullets are travelling at a pretty fast speed, so fast that they sometimes endup on the other side of the foe, but not so fast that they passthrough foe. I'm using circular collision and the foe have a circle of radius 4 with origin placed at the center of their circular-sprite. The bullets are small with radius of 1 or 2. Everytime a bullet is colliding within the foe's circle the game takes the angle between the bullet and the foe (the bullet-foe-angle) and applies a force in that direction onto the foe. Normally this would mean any bullets fired at the foe push the foe in a 'general direction away' from the bullet's point of origin but not necessarily in the direction of the bullet's projectory(unless it's a dead-on hit). But a bullet which first appears on the other side of the foe's origin... is going to push it forwards, towards the bullet's point of origin. Also of note is that the last bullet to hit the foe before it hits 0 hp determines the direction of the foe's ragdoll/corpse effect(any small bullet bump basically gets pronounced for a second or two). Each object has a dx,dy variable tabulating all the forces applies on it that frame then added to their x,y.

My main consideration is finding a fix that has a low cpu cost, but don't let it limit the discussion.

So the OP's issue is caused primarily by two things since spd/fps changes are off the table:
a) the bullet first appearing in the wrong side of the foe's circle-Hitbox
b) the fact I use the direction from the bullet to the center of the foe (bullet-foe-angle) to determine the dx+= dy+= that's applied when the bullet hits the foe.

Ideas to remedy:

  1. Since the object firing bullets is stationary I could take the angle from the object to the foe, rather than the bullet to the foe. This works...but it's the same effect regardless of where a stationary foe is hit(dead-center/left/right/etc). And if something is dx+=1 and dy+=0, aka sidestepping a player's position, and they get hit by bullets from that player...should the bullet bump them slightly in the direction of the bullet's projectory? If it's a deadon hit...ya...but if it's grazing their side its usually a bit weird looking to have them be bumped with the bullet projectory a bit. The dx are all added up so the sidestep movement is still the dominant force but still. But with the original bullet-foe-angle method the graze would send them uniquely upwards a bit instead and the deadon hit would send them with the bullet projectory which seems overall better/unique looking from my testing. There's also the consideration of the ragdolling where any seemingly small and weird bump direction gets pronounced via a multiplier for a brief period of time.

  2. I keep the bullet-foe-angle method and give the foe a second circle of larger radius, say 8. Once the bullet enters this larger circle the angle between the two is taken and recorded for that specific bullet-foe relationship. The larger circle then never runs again for that specific relationship, and when the smaller radius 4 circle is triggered I use that recorded angle. This seems to be a best of both worlds approach....it will still suffer a bit from (1)'s methodology...but it would solve the OP. Personally I don't like this method, cause it makes it so every bullet has to have a little bit of the (1) problem as a cost to fix the few bullets that fall under the OP's problem.

  3. If the foe is always facing the player when they are fired at...then I can maybe identify when bullets are 'on the wrong side' of the foe's circle. Then I can maybe do some kind of reflection(like how when u make a circle u make a small ~45 degree arc then reflect it 8 ways to make the full circle) to, in-effect, teleport the bullet a few pixels backwards along it's projectory enough so it sends the foe in generally the correct direction as intended with the bullet-foe angle method.

  4. Something with the dot product I could use....? Not sure. Apparently it can tell when two objects are facing eachother...I made a demo a while back proving to myself it does...maybe something there could help...I'm thinking it would not work right here, but just an idea.

Would appreciate any ideas/methods that could be thought of to help solve this.

P#117726 2022-09-20 21:42 ( Edited 2022-09-20 21:46)

:: Unfold ::

Cart #bodoguwofo-1 | 2022-09-23 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

Teatime!v2 is a top-down 2-handed-keyboard roguelite shooter featuring experimental controls, 6 weaponTypes, 3 playable characters, and quirky mechanics. Free-to-play inbrowser on itch.io and the pico8 bbs.

Goal: Keep your cupHp and playerHp above 0 for 300s while preventing no-more than 10 leaf from entering your cup. When the timer hits 0, you are given 10s, from times [0,-10], to drink the tea before it spoils.

[LongPlay: Fewer leaf in your Cup will mean a higher score and potential bonuses]

a|d: angle of fire
s: fire; (tap)shotgun/sniper, (hold1)bomb/kinesis, (hold2)mg/poison
f: (tap)grab-nearby/drop cup; (hold)drink from heldCup
e: drop fakeCup
shift*: (tap)toggle altWeapons; (hold) +/- turnrate while using a|d
*spacebar/q added as alts

Right_Hand: arrowKeys movement up/down/left/right
k:return from game to menu(avoid during screenshake; offsets correct on replay)

See the itch page for full-manual/video/listOfAllSecretUnlockables(Wants!): https://cerbyo.itch.io/teatime
The machinegun & impact sfx may have an echo on the webbuild. Consider playing from splore(recommended), the.p8.png cart file, or binaries off itch if it's a problem.

+Chippy's Gun & Greg's Blend now hidden unlockable-items for all characters(via menu on unlocked) 
+cupDrop damage changed from -25cupHp to -50cupHp
+'k' ingame returns you to the character-select screen 
+obj-init/function optimizations

P#117549 2022-09-17 04:16 ( Edited 2022-12-11 12:40)

:: Unfold ::

Because there doesn't seem to be a method to specify the audio channels with "\a" I am trying to transfer my sound effects to the sfx editor.

I don't understand how to do this though. Let's take this example:
s16...spd set to 16
no volume is specified so the fourth column is 5 by default
x3...final 5th column set to 3 for all notes
a0...note a(press n cause idk how to type it in manually) and octave 0

In the sfx editor that gets me, with spd=16, this:
a 0053

?"\as16x3a0" does not sound like that at all....so idk what I'm missing here. I need to figureout how to do it for notes like ?"\as9x3a-2i0dd1" which includes "-" being a sharp and idk how to input a sharp at all, # seems to be the keys above n's row.

Would appreciate an explanation on what I am missing here, and if there is a way to transfer it without manually going through it like this.

P#114180 2022-07-11 00:25 ( Edited 2022-07-11 00:28)

:: Unfold ::

I'm trying to setup a new menuitem (i.e. on enter menu) for muting the music. (1) the code below will mute properly but won't display the updated value of the switch i.e. "Music:on" will always display. Putting menuitem() in _draw will fix this...allowing for "Music:off" to display, but not when the callback is true (menu doesnt auto-close).

(2), I'm concerned if I should be calling menuitem() in _draw or not to begin with (if its better cost-wise elsewhere). I am not overly familiar with cartdata and dset/dget but was trying to store a value there and check that in draw instead....not sure if that's the right way, but its not working atm anyways.

How should I ideally set this up such that music:on/off can be seen and toggled within the menu, with the callback returning true (menu not exiting on click)?

switch=true --true is music_on

function _draw() cls(1)
    if dget(1)==0 then switch=true else switch=false end --not working, was trying to update display for menu

function _init()
    menuitem(2,"music"..(switch and ":on" or ":off"),
        switch= not switch
     if switch then
        dset(1,1) end
     return true

(3) Does menuitem() being in _init() allow it to persist throughout the gameloop...so that any change to the enter menu calls it?...but it's not updating the value of switch while I'm in the menu itself....only after I close the menu...I don't understand how this works in relation to the gameloop (normally I expect to have to call the function in _draw/_update to run and update itself each frame). Menuitem() is a callback function that runs on its own loop in the background once first called so its ideally called once in init...? Do I have to store switch somewhere menuitem can access (like outside the gameloop cause the gameloop dont run in menu)...?

Here's the same system as above, but storing the value of switch in cartdata, the end result is the exact same as above though...still no dynamic updating while in menu.


function _draw() cls(1)

function _init()
    (switch==0 and ":on" or ":off"),
     if switch==1 then
     else --switch==0 aka on
        dset(3,1) end
     return true
P#114087 2022-07-08 23:16 ( Edited 2022-07-08 23:45)

:: Unfold ::

I have a foe taking damage from multiple bullet types on the same frame. I'm able to inflict 'impact' movement to the foe via the bullets update function (i.e. the foe jiggles when struck and still alive). The problem though is the death movement. I want to send the foe flying a certain amount depending on what kills them. Instead of doing calculations inside the bullet update like I had for the jiggles... I had been sending an id of the last struck bullet type to the foe's update, where it could then do the operations. This works fine for single shot deaths. However this doesn't look great when they get hit by like 10 things at once on a frame and they do the kinetic animation for the wimpiest bullet they r hit with. So I need a system of identifying basically the bullets (the type and number of each type) from the last 10 frames that have hit...and define that into some kind of kinetic energy multiplier to determine how far the body flies.
Question is how do I go about this, and is there a better method?

Another method would be I could potentially do this in the bullet updates....but that seems messey cause I'd have multiple bullets surviving and checking eachother out in what seems like costly back and forth ways for extra frames after they r supposed to be dead. Maybe I'm missing something here though.

In Summary, my setup is like this, and I need some way of passing 'how many bullets of each type 1/2/3' have hit the foe over the last 10 frames or so. Presently only the last bullet info, calculated on the final frame of life, is passed over. I could just create a var x=0 and add +10 for type 1, +20 for type 2 etc over 30 frames...giving me a kinetic multiplier. But I endup with just a number that works fine for the 10 frames...but then there's no way on the 11th frame to remove the 1st frames data and repeat the process so I'm always the latest 10 frames of data. I need some kind of table setup.

function _update()
for all obj in bullets do obj:update end
for all obj in foes do obj:update end

bullets={{update=function() ... end},{..},..{..}}
foes={{update=function() ... end},{..},..{..}}
P#110811 2022-04-25 04:09

:: Unfold ::

Okay so I'm looking to make an orb animation like this: https://jsfiddle.net/aycn3fzd/
I'm not really sure if the solution is within that post or not, I'm having a hard time converting it.
Regardless, I'll explain what I have and the issue I'm having with completing this task.
Here's what I have atm:

I want an orb that will fill/unfill based on the percent of something left. So the generic red hp orb for example. A player has x hp. That hp as a percent x/100 will influence the percent of which the orb is filled from bottom to top red. You can see in the gif above I have this working for 'part' of the circle. The problem is the other sections.

My method for drawing the circle in this example is drawing the 0->15degrees side arc using sin/cos and reflecting it 8x...I posted earlier 3-4 other circle drawing methods, but I'm using the cos()/sin() one here for accuracy's sake. To fill in the circle I am using the line() method and extending it to the other side so I only need 4 instances. I don't fully understand the rect() method and have had issues getting it to look right (I assume u draw a square right center of the circle but the corners usually stickout and look off...maybe that assumption is the problem).

Anyways, drawing the lines for every y value of the circle I endup with the inverted version of the red picture there, which when reflected again gets the yellow version of itself there. Each of these shapes and their inverted version, when positioned properly will create a nice circfill() mimick:

So the red one is working in the first gif. The problem is the orange ones...when I apply the method they shrink width-wise and i need them shrinking height-wise. Looking at how I derive them, its clear why they are doing that, its cause I'm deriving everything from the original 0->12.5 degree segment and I draw that segment with horizontal lines. One solution might be to redraw it with vertical lines and use that for 2 of the sections and the horizontal for the other 2...but I have no idea how to do that and from my testing question if that is possible. Here we see the problem below:

The effect worked above by deleting the red lines....making it go up/down. If we delete the brown lines we affect the width when we want to affect the height. Maybe I could draw ellipse halves at the top and bottom and draw those using the elipsefill() builtin thing and dynamically change the radius...it would be cheating sorta, but might work.

The third issue is how to determine 'when' one section should be shrinking and the others should be statically fully displayed or fully cleared from the screen. I prefer the idea of doing this with 1 circle such that when the orb is drained...u can see the gameplay behind rather than a second fully displayed 'background' circfill().

percent=100 --how full the circle/orb is as a percent
function _draw()cls()
    if percent>0 then percent-=1 end --orb draining from full
function orbfill(cx,cy,r,c,q)
poke(0x5f25,c) --set color
q=q or (r>15 and .005 or .01) --pixels per arc, .01 default
    for i=0,.125,q do --angle runs from 0->.125 then reflects this arc 8x
        local x,y=r*cos(i),r*sin(i)
        --circ() method
        pset(cx-y,cy-x)--miry=x .5
        pset(cx-y,cy+x)--mir .75
        pset(cx+y,cy-x)--mir .25
        pset(cx+y,cy+x)--mir .625
        pset(cx+x,cy-y) --x flip
        pset(cx-x,cy+y) --y flip
        pset(cx-x,cy-y) --xy flip
        --circfill() method
        -- line(cx-x,cy-y,cx+x,cy-y)
        -- line(cx-x,cy+y,cx+x,cy+y)
        -- line(cx+y,cy-x,cx+y,cy+x)
        -- line(cx-y,cy-x,cx-y,cy+x)
--[[So the above is just the normal circ()/circfill() function. 
My method for the animation is the idea we have zones that activate based on the 'percent' var. 
Then we take a percent of a percent when we are in each section to run through 0 to var*12.5 degrees of arc
....where the var will drain that 12.5 to 0 based on the percentages...which decreases the number of 
horizontal line stacks. This presents problems already as to how to get this working nicely. 
The above loop is just to show the starting point, it can be removed.
--diameter is 2*r=60; r=30
--heights of segments: 8.7868 top; upper 21.2132; lower 21.2132; low is 8.7868
--21.2132*2+8.7868*2=60 --ttl height of circle with r=30

21.2132/60 --0.35355333333333333
8.7868/60 --0.14644666666666667
--lets say its 15%+35%+35%+15%
--top segment decreases
--upper mid decreases
--lower mid decreases
--lower segment decreases

If percent is above a tier, then that tier would need to remain fully displayed at 100%.
@percent==100 all four tiers and segments are 100%.; percent==90 the bottom 3 are full, top 1 draining

--outline for method:
if percent>85 then
    for i=0,k,q do --0->.125 then reflects
        local x,y=r*cos(i),r*sin(i)
--      line(cx-x,cy+y,cx+x,cy+y) --upper mid
--      line(cx-x,cy-y,cx+x,cy-y) --lower mid
--the first gif works based on this:
elseif percent<=85 then--(21.2132*2+8.7868)/60 then --0.8535533333333334
    local rat=(percent-50)/35 
--a percent of a percent: percent=85%->50% transposed on i=0->12.5 degrees
--we make that 30% section of percent into a 100% segment and use to determine the deg out of 12.5
    for i=0,k*rat,q do 
        local x,y=r*cos(i),r*sin(i)

P#109591 2022-04-02 20:05 ( Edited 2022-04-02 21:24)

:: Unfold ::

Cart #jfowobora-1 | 2022-04-01 | Code ▽ | Embed ▽ | No License

I've been looking at how to draw circles that mimic the elegance of circ(). From the bbs that led me to

So I tested
Minsky/Midpoint/x,y=sin(),cos() against circ()
The verdict was the midpoint algorithm was the best alt method. But none of them r close to being as efficient as circ()..so I don't understand...how does circ() work? I especially don't understand cause the previous bbs link to the Minsky said it was superior to circ()...my tests say its not...but the posts r 5 years old so...has circ() been updated since maybe? Or what am I missing? All I did in my test is draw 100 circles and look at the values via ctrl+p...and the findings were explicit.

I'm really just looking at how to draw circles that look as good as circ()+efficiently+accuracy, since circ()/elipse() omit some of the custom arc-like shapes u might want to make or more custom circles like dotted etc that I need to use. You can of course, when able, for static circles just do the math calculations 1x outside the gameloop, then draw the coordinates via pset() in the gameloop...but many times you need dynamic circles running each frame looking clear as can be.

I'm also a bit upset by how circ() smooths its coordinates. The sin()/cos() method creates a much much more accurate circle hitbox. With circ() the collisions are always off, particularly in the right-side, when relying on it for the visual representation of where things are...and no you can't blame rounding error. I guess since I use sin()/cos() to make the imaginary hitboxes that would make sense that drawing the circles via sin()/cos() creates a more accurate representation. But u can't use midpoint circle algorithm, or whatever circ() is using, to make an efficient collision detection algorithm...is what I would presume?

P#109489 2022-04-01 01:29 ( Edited 2022-04-01 01:51)

:: Unfold ::

id=stat(31) identifies keypress_q as id="q" and keypress_a as id="a". Btn(5,1) identifies itself as keypress_q and keypress_a. How do I separate keypress_q to work via id=stat(31)="q" and btn(5,1) to work 'only' with keypress_a?

if btn(5,1) and id~="q" then... --input for keypress_a
if id=="q" then... --input for keypress_q

That 'would' work...but the problem is stat(31) works like btnp() when I need it to work like btn()...but the poke methods of making btnp() work like btn() don't seem to apply to stat(31)! So I'm presently stuck in a situation where I can be pressing keypress_q and yet id~="q"! The obvious way around this is to just use id=="a" for keypress_a; id=="q" for keypress_q....but again, they work like btnp, so this doesn't work if I want to use keypress_a for standard smooth wasd player movement.

The intent is to basically utilize the existing buttons of player 0 and player 1....and add the functionality of stat(31) to get even more keys. I don't necessarily need the other keys to have the same functionality as btn(), they can just be 'single tap' style keys...I'll use them as like rpg skills that go on cooldown right when they are used. Player movement via wasd though needs the functionality of btn(). But again, the problem is the overlap....specifically with those btn(4/5) keys since they have more than 1 natural input (seen below), when I want to choose just 1 natural input and use stat(31) to utilize the other.

For the sake of reminder I'll include the keys below to give an idea of the overlap, specifically btn(4/5):

P0: 0left;1right;2up;3down;4zcn;5xvm
p1: 0s/1f/2e/3d/4wtabshift/5aq

Chosen usage for p1: a:btn(5,1)|d:btn(3,1)|w:btn(4,1)|s:btn(0,1)|f:btn(1,1)|e:btn(2,1)

I guess p1:btn(5) is the only case of overlap...since I can't seem to get stat(31) to work for tab/shift/backspace...mainly I don't get anything showing up when I printh(id) with those inputs. I get "9" for backspace...but only keypress_9 activates the 'if id=="9"' case. stat(31) is also sort of weird...not sure if buggy is the right word. Is it single thread or something? If you 'also' print(stat(31)) to the screen it negatively affects id=stat(31), which won't work as reliably....I think stat(31) can only be called directly once per frame? Or something to that effect

P#109207 2022-03-26 18:47 ( Edited 2022-03-26 19:18)

:: Unfold ::

I need some help creating an absolute value function that utilizes bits to work.
My current understanding is pico8 is either 32 or 16 places. I don't quite understand which value I would use here.

If someone can clarify that would be good. For the sake of explanation, lets say its 6 bit.

000 010 --binary form of 2 in 6 bit
111 101 --binary form of -2 in 6 bit 

So my understanding is the formula is something like:

mask=v>>number_of_bits_in_v -1

where number_of_bits_in_v=6 and v=2

And the idea is in the case of v=-2 we are adding one to it..cause all the bits to shift over. So given the above formula is subracting one....I have to know whether the number is positive or negative ahead of time and use either -1 or +1? Not sure if that'll be an issue or not.

This leads to a function like:

function absolute(v,ttl_bits)
local mask=v>>ttl_bits-1
return (v+mask)^mask

--given pico8 is 32...bits? i would use...?
absolute(2,32) --2? only it doesn't...it gives me a 0 if v is negative, 1 if v is positive.

The goal is to make it function like the built-in abs(-2) --2
And if there is a way for this to work like it does for decimals too...like abs(-2.432) --2.432

P#108221 2022-03-08 00:55 ( Edited 2022-03-08 01:00)

:: Unfold ::

Cart #tubisapame-0 | 2022-01-28 | Code ▽ | Embed ▽ | No License

I'm trying to figureout how to create parabola/bezier-curve/quadratic (i.e. grenade lobbing) movement based on the player's velocity dx/dy. I'm familiar how to do this for a player's speed, and the cart shows the setup for speed as a functioning baseline. In the cart I defined a player who can move around with the arrow keys. When they hit x they bunny style hop moving to the right using the quad() function. The system freezes the movement of the player while the hop completes, so its always the same movement pattern.

Speed relies on absolute movement and coordinates...with quad() the player will start somewhere defined p0...move along a curve defined by point p1 and endup at p2. The cart math is based on augmenting the x/y player values directly. Velocity is different though, and I can't figureout how to make the same example work for the dx/dy in the cart.

My intent is to make a bunny hop according to the direction of movement defined by dx/dy. So if the player is moving and they press x, the player movement freezes for a time, and the current values of dx/dy will judge the direction and magnitude of movement to be completed according to some general curve outline (bunny hop). And since I'm now dealing with velocity vectors, for this system to allow me to apply other forces onto the movement like gravity etc which should affect the bunny hop movement dynamically rather than scripted like in the cart. Without the quadratic part the effect can be achieved with just straightup multipliers dx*=1.1 (i.e. velocity increases if I press x for a time)..but that's not what I want.

Velocity+quadratics....how to? Would appreciate help with the math here or something to get me started. If there are some search terms of articles to link to, that's good too. I'm having a hard time finding the information online, could use better keywords. I found it hard to create the quad() function to begin with cause I was relying on quadratics tutorials when what I wanted was bezier curves...so I'm hoping I'm just missing a keyword like in that case.

P#105861 2022-01-28 23:24 ( Edited 2022-01-28 23:28)

:: Unfold ::

Nothing I try seems very active. I went through...slack...twitter.....here....stack exchange. And the time delay between getting any answers, if any, is excruciatingly ineffective. Are there more active communities I could get help from that I haven't tried?

Here and there I have little questions I need help with. Mainly syntax related, I don't need somsone to go through ALL my code or anything just 1 liners here and there. For small questions while writing your code, what is the best way to get answers asap? Was hoping for a chatbox or irs or something.

P#66018 2019-07-22 01:34

:: Unfold ::

I'm having a really hard time with initiating if statements using multiple booleans. It seems I initiate one but then it stays and I can't figure out how to write them properly if I'm dealing with multiple.

In this example I'm trying to set 3 gamescreens with a timer for the 2nd.
The default screen is titlescreen; the initated one on button press is titlescreen1; and then i want gamescreen to happen automatically when the timer finishes.

Problem is gamescreen doesn't initiate and I don't understand why.


function _init()

function time_lapse()
if time() - time_diff > 1 and countdown > 0 then

function _update()
if btn(4) then titlescreen1=true end
if titlescreen1==true then time_lapse()
if countdown==0 then gamescreen=true end
elseif gamescreen==true then

function _draw()
print("one is happening",33,33,14)
if titlescreen1==true then cls() print(countdown,15,15,14)
elseif gamescreen==true then
print("gamescreen is active",33,33,14)

P#65997 2019-07-21 08:43

:: Unfold ::

Basic movement can be facilitated through:
if btn(0) then x-=1 end

But when I add sfx:

if btn(0) then x-=1 and sfx(0) end

It always generates an error as soon as that button 0 is pressed. Why is that? If x then y and z end. Is there something wrong with that syntax? Or is it related to sfx not working in this way?

And why is it when I clarify with brackets it doesn't even run!? Am I clarifying that my syntax is wrong before teh game starts? That's funny.
if btn(0) then (x-=1 and sfx(0)) end

P#65973 2019-07-20 14:59 ( Edited 2019-07-20 15:01)

:: Unfold ::

Edit: not sure how I'm expected to display multiplication using the syntax here, but assume there's a X or star or whatever between 15 and i etc.

I was trying to initiate this statement here, which I can do:
for i=1,4 do
print(i, 15i-14, 15i-14, 14)

But then I wanted to move the diagonal array of 4 numbers with the arrow keys around the screen. I tried the code below but keep getting errors. I'm guessing my argument is setup wrong and I can't call i as the same variable in all these statements and make it work...? Can someone help me understand the immediate problems with the code, and then afterwards if viable suggest an alternative method of doing so. I'm still learning so my immediate concern is figuring out what is wrong here specifically.

function _init()
j= {

function _update()
if btn(0) then j.x-=1 end
if btn(1) then j.x+=1 end
if btn(2) then j.y-=1 end
if btn(3) then j.y+=1 end

function _draw()
for i=1,4 do

Even in the API demo I copy the code for the similar statment, but when i run it on its own it doesn't work. I don't understand why this here doesn't work on its own either:

for i=0,15 do
x = x + 6 + flr(i/10)*4

P#65880 2019-07-16 08:50 ( Edited 2019-07-16 09:03)

Follow Lexaloffle:          
Generated 2023-10-03 14:54:30 | 0.136s | Q:34