32 Commits

Author SHA1 Message Date
58da8e6dc3 fix hokey pokey menu
the menu eases in, the menu eases out, the menu eases in and you shake it all about
2025-01-20 18:02:50 -08:00
8ff0732cbc draw rearm pane; exit behavior is broken
Nil transparency has screwed me over; I am uploading this version as
an illustrative example
2025-01-20 17:43:21 -08:00
c88e7c0657 I really wish Lua had real classes rather than forcing you to assemble it yourself 2025-01-20 17:09:47 -08:00
ff3552bc45 fix unit error for xp frame logic 2025-01-20 17:02:02 -08:00
2dcb95b0cd menu prototype 2025-01-20 16:59:57 -08:00
87451bbd3a incomplete start to porting ui 2025-01-12 22:58:20 -08:00
89a42e6c8b improve xp gem art 2025-01-12 21:51:32 -08:00
e2be11a2da fix order of magnitude error in XP logic 2025-01-12 21:47:11 -08:00
175099d778 make maxval work better near p8 precision limits 2024-12-29 23:44:39 -08:00
33fede4ed8 update comment about off-by-1 error 2024-12-29 23:41:47 -08:00
afa1f22170 go back to 0x0.0001 increments for xp
I foresee the 32K limit being a bigger problem than the math in vertmeter, although I will probably need to go patch vertmeter up too
2024-12-29 23:39:56 -08:00
78b200272e fix shield crash 2024-12-29 23:33:45 -08:00
42ac2abc20 groundwork for full mode switching 2024-12-29 23:26:48 -08:00
c55ea000fd whoosh animation when level up pending 2024-12-28 19:56:42 -08:00
2c1ad0a0b3 drop xp gems 2024-12-28 19:27:54 -08:00
e0b784ce7d clean up, run at 60 fps
now the bounce animation feels like it takes too long, trying to fix it
2024-12-26 17:43:04 -08:00
e1a70cc6fc dark blue, not dark gray, for pane bg 2024-12-26 17:16:48 -08:00
cbdf2a27cd fast quadratic exit feels better 2024-12-26 17:05:55 -08:00
caaf848722 fixed it 2024-12-26 16:59:43 -08:00
25f58d5cce messed up entry animation but it's a start
* wrong Y positions for everything
* both directions are "enter"
2024-12-26 16:19:52 -08:00
c15ec61494 partial prototype of object-oriented drawing and entry 2024-12-26 12:32:31 -08:00
7ff5cf97ad functioning prototype 2024-12-24 19:23:14 -08:00
f761d1a172 prototype for REARM screen UI 2024-12-24 19:04:07 -08:00
98f56328a6 off by 1 2024-12-24 18:10:48 -08:00
93792c36c9 sketch for possible REARM ui blank 2024-12-24 18:10:17 -08:00
d799947c46 it would hlep if I actually removed the old text 2024-12-24 15:06:18 -08:00
f3a84573e6 remove readme from .p8 into a separate .md file 2024-12-24 15:04:23 -08:00
1a7bc7094e HP/XP labels look much better and correctly-associated now 2024-10-27 13:16:37 -07:00
39d53c45aa xp and hp brighter and closer to meters 2024-09-07 16:36:30 -07:00
5870c129eb staggered "xp", "hp" labels in UI 2024-09-07 16:33:58 -07:00
68863280f3 stagger the bars 2024-09-07 16:17:38 -07:00
3f7c4f59c0 stretch meter area 2024-09-07 16:15:00 -07:00
3 changed files with 919 additions and 398 deletions

356
old_readme.md Normal file
View File

@ -0,0 +1,356 @@
This file contains text that used to be in the cartridge itself, but
I'm getting increasingly anxious about cartridge space so I'm moving
it out ot a separate file.
---
main loop sequence
==================
1. level_frame
2. events
3. merge new_events into events
4. update bg intangibles
5. move ships (player first)
6. move bullets (player first)
7. calculate collisions
1. pship on eship
2. ebullet on pship
3. pbullet on eship
8. update fg intangibles
9. check for end of level
draw order
----------
bottom to top:
1. intangibles_bg
2. player bullets
3. player ships
4. enemy ships
5. enemy bullets
6. intangibles_fg
notes
-----
intangibles_fg move()s after
all collisions and other moves
are processed. if an intangible
is added to the list as a result
of a collision or move, it will
itself be move()d before it is
drawn.
data-driven items
=================
guns and bullets both allow the
most common behaviors to be
expressed with data alone.
ships only need a movement
algorithm expressed.
guns
----
* t - metatable for bullet type.
fired once in the bullet's
default direction per shot.
* enemy - if true, fired bullets
are flagged as enemy bullets.
* icon - sprite index of an
8x8 sprite to display in the
hud when the player has this
gun. default is 20, a generic
crosshair bullseye thing.
* cooldown - min frames between
shots.
* ammo, maxammo - permitted
number of shots. 0 is empty
and unfireable. maxammo = 0
will cause a divide by zero
so don't do that. if nil,
ammo is infinite.
default guns manage ammo and
cooldown in shoot, then call
actually_shoot to create the
projectile. override only
actually_shoot to change
projectile logic while keeping
cooldown and ammo logic.
bullets
-------
* dx, dy - movement per frame.
player bullets use -dy
instead.
* enemyspd - multiplier for dx
and dy on enemy bullets.
default is 0.5, making enemy
shots much easier to dodge
* damage - damage per hit;
used by ships
* sprite - sprite index.
* x_off, y_off - renamed for
the next two vars. may revert
* center_off_x - the horizontal
centerpoint of the bullet,
for positioning when firing.
assume a pixel's coordinates
refer to the upper left corner
of the pixel; the center of
a 2-width bullet with an
upper left corner at 0 is 1,
not 0.5.
* top_off_y, bottom_off_y -
also for positioning when
firing. positive distance from
top or bottom edge to image.
top_off_y will usually be 0,
bottom_off_y will not be when
bullets are smaller than
the sprite box.
* width, height - measured in
full sprites (8x8 boxes), not
pixels. used for drawing.
bullets despawn when above or
below the screen (player or
enemy bullets, respectively).
by default, bullets despawn
when they hit something.
override hitship to change this.
ships
____
ships move by calculating
momentum, then offsetting their
position by that momentum, then
clamping their position to the
screen (horizontally only for
ships that autoscroll). ships
that autoscroll (slip==true)
then slide down by scrollspeed.
fractional coordinates are ok.
after movement, ships lose
momentum (ship.drag along each
axis). abs(momentum) can't
exceed ship.maxspeed.
ships gain momentum by acting
like a player pushing buttons.
the player ship actually reads
buttons for this.
act -- returns new acceleration:
dx, dy, shoot_spec, shoot_main.
dx and dy are change in momentum
in px/frame. this is controls
only -- friction is handled in
ship:move (`drag` value).
ships hitting another ship take
1 damage per frame of overlap.
ships hitting a bullet check
bullet.damage to find out how
much damage they take. damage
is applied to shields, then hp.
damaged ships flash briefly -
blue (12) if all damage was
shielded, white (7) if hp was
damaged. a ship that then has 0
or less hp calls self:die() and
tells the main game loop to
remove it.
shieldcooldown is the interval
between restoring shield points.
shieldpenalty is the delay
before restoring points after
any damage, reset to this value
on every damaging hit (whether
it is absorbed by the shield or
not) -- shield behaves like
halo and other shooters in its
heritage, where it recovers if
you avoid damage for a while.
not that there is any safe cover
in this kind of game.
ships do not repair hp on their
own. negative-damage bullets
are treated as 0, but a bullet
can choose to repair the ship
it hits in its own hitship
method, or otherwise edit it
(changing weapons, refilling
weapon ammo). powerups are
therefore a kind of bullet.
levels
======
a level is a table mapping
effective frame number to
functions. when a level starts,
it sets lframe ("level frame")
and distance to 0.
every frame, level_frame
increments lframe by 0x0.0001.
then if the level is not frozen,
it increments distance by 1.0
and runs the function in the
level table for exactly that
frame number (if any). distance
is therefore "nonfrozen frames",
and is used to trigger level
progress. lframe always
increments. ships are encouraged
to use lframe to control
animation and movement, and may
use distance to react to level
progress separately from overall
time. remember to multiply
lframe-related stuff by 0x0001.
a special sentinel value, eol,
marks the end of the level.
(the level engine doesn't know
when it's out of events, so
without eol, the level will
simply have no events forever.)
when it finds eol, level_frame
throws away the current level
and tells the main loop that it
might be done. the main loop
agrees the level is over and the
player has won when the level
has reached eol and there are
no more enemy ships, enemy
bullets, or background events
remaining. player ships, player
bullets, and intangibles are
not counted.
level freezing
--------------
the level is frozen when the
global value freeze > 0.
generally, something intending
to block level progress (a
miniboss, a minigame, etc.)
increments freeze and prepares
some means of decrementing it
when it no longer wants to block
level progress.
most commonly, we want to block
until some specific ship or
group of ships has died. for
these ships, override ship:die
to decrement freeze. make sure
to set ship.dead in any new
ship:die method so anything else
looking at it can recognize
the ship as dead.
for anything else, you probably
want an event to figure out when
to unfreeze.
levels start at 1
-----------------
distance is initialized to 0
but gets incremented before the
first time the engine looks for
events. therefore, the first
frame of the level executes
level[1]. since levelframe
executes before anything else,
level[1] sets up the first frame
drawn in the level. the player
does not see a blank world
before level[1] runs.
level[1] can therefore be used
to reconfigure the player ship,
set up backgrounds, start music,
kick off some kind of fade-in
animation, etc.
events
======
the global list "events" stores
0-argument functions which are
called every frame. if they
return true, they are removed
from the list and not run again;
if they return false, they stay
and will be called in later
frames. the level does not end
while the events table is
nonempty.
events are most commonly used
to set up something for later
(for example, blip uses an event
to remove the fx_pallete from
the flashing ship when the blip
expires), but can also be used
to implement a "level within a
level" that does something
complicated until it's done. if
you froze the level when
creating the event, remember
to thaw it (freeze -= 1) on all
paths that return true.
to do complex stuff in events,
use a closure or a metatable
that specifies __call.
to avoid editing the events
list while it is being iterated,
events that create new events
must add those events to
new_events rather than events.
new_events is only valid during
the "event execution" stage, so
events created at any other time
must go directly on events
without using new_events.
intangibles
===========
the intangibles_fg and
intangibles_bg lists contain
items with :move and :draw.
like ships and bullets, they
move during _update60 and
draw during _draw. they are
not checked for collisions.
intangibles_bg moves/draws
before anything else moves or
draws. intangibles_fg
moves/draws last. this controls
whether your intangible object
draws in front of or behind
other stuff. you probably want
intangibles_bg for decorative
elements and intangibles_fg
for explosions, score popups,
etc.
there's no scrolling background
engine but intangibles_bg could
be used to create one, including
using the map (otherwise unused
in this engine) for the purpose.
intangibles do not prevent the
level from ending. like bullets
and ships, if :move returns
true, they are dropped.

223
rearm_prototype.p8 Normal file
View File

@ -0,0 +1,223 @@
pico-8 cartridge // http://www.pico-8.com
version 42
__lua__
-- vacuum gambit
-- by kistaro windrider
-- stdlib
-- generate standard "overlay"
-- constructor for type tt.
-- if tt.init is defined, generated
-- new calls tt.init(ret) after
-- ret is definitely not nil,
-- before calling setmetatable.
-- use to initialize mutables.
--
-- if there was a previous new,
-- it is invoked on the new
-- object *after* more, because
-- this works better with the
-- `more` impls i use.
function mknew(tt)
local mt,oldnew,more = {__index=tt},tt.new,rawget(tt, "init")
tt.new=function(ret)
if(not ret) ret = {}
if(more) more(ret)
if(oldnew) oldnew(ret)
setmetatable(ret, mt)
return ret
end
return tt
end
function easeoutbounce(t)
local n1=7.5625
local d1=2.75
if (t<1/d1) then
return n1*t*t;
elseif(t<2/d1) then
t-=1.5/d1
return n1*t*t+.75;
elseif(t<2.5/d1) then
t-=2.25/d1
return n1*t*t+.9375;
else
t-=2.625/d1
return n1*t*t+.984375;
end
end
-->8
-- entry points
function _draw()
cls()
draw_hud_placeholder()
left_pane:draw()
right_pane:draw()
rearm_pane_instance:draw()
end
function _init()
item=1
bfm=1
crt_frm = 1
left_pane = weapon_pane.new{}
right_pane = weapon_pane.new{
is_left=false,
s = 2,
hdr = "vulc",
body = " rate\n\n faster\n firing\n rate",
hot = function() return item == 2 end}
rearm_pane_instance = rearm_pane.new{hot=function() return item < 0 end}
end
function _update60()
crt_frm += 0.25
if (crt_frm >= 9) crt_frm = 1
if (btn(3) and item > 0 or btn(2) and item < 0) item = -item
if (btn(0)) item = 1
if (btn(1)) item = 2
if (btn() & 0xF ~= 0) and bfm >= 10 or bfm >= 30 then
bfm = 1
else
bfm += 1
end
if btnp(4) then
left_pane.pos = -1
right_pane.pos = -1
rearm_pane_instance.pos = -1
end
if btnp(5) then
left_pane.pos = 1
right_pane.pos = 1
rearm_pane_instance.pos = 1
end
left_pane:update()
right_pane:update()
rearm_pane_instance:update()
end
function draw_hud_placeholder()
rectfill(112, 0, 127, 127,0x56)
rect(112,0,127,127,7)
line(127,1,127,127,5)
line(113,127)
end
-->8
-- rearm pane drawing
crt={-91,-166,-2641,-1441,-23041,23295,-20491,24570}
function glow_box(x0, y0, x1, y1, c, cf)
for i,v in ipairs{c[1],c[2],c[1],0} do
i -= 1
rect(x0+i,y0+i,x1-i,y1-i,v)
end
fillp(crt[crt_frm&0xff])
rectfill(x0+4, y0+4, x1-4, y1-4, cf)
fillp()
end
function frame_col(hot)
if (not hot) return {4,10}
if (bfm<=16) return {14,7}
return {2,8}
end
function draw_weap_opt(x, y, c, s, hdr, body)
camera(-x,-y)
glow_box(0,0,55,100,c,1)
spr(s,5, 5)
print(hdr, 13, 8, 7)
print(body, 5, 15, 6)
camera()
end
function draw_rearm(c)
glow_box(0,101,111,127,c,1)
spr(5,15,107,4,2)
print("full ammo\nfull shield\n+50% health",54, 106, 6)
end
-->8
-- rearm pane objects
easing_pane = mknew{
-- to enter: pos = -1; to exit: pos = 1
-- runs for 32 frames in, 16 frames out
}
function easing_pane:frac()
local pos = self.pos
if (not pos) return
if (pos < 0) return 1-easeoutbounce(1+pos)
if (pos > 0) return (1-pos)*(1-pos)
return 0
end
function easing_pane:update()
local pos = self.pos
if (not pos or pos == 0) return
if (pos < 0) pos = min(pos + 0x0.05, 0)
if pos > 0 then
pos -= 0x0.1
if (pos <= 0) pos = nil
end
self.pos = pos
end
weapon_pane = mknew(easing_pane.new{
is_left = true,
s = 1,
hdr = "hull",
body = "\n +1\n max\n health",
hot = function() return item == 1 end,
})
function weapon_pane:draw()
local frac, is_left = self:frac(), self.is_left
if (not frac) return
camera(
frac * (is_left and 55 or -128) + (1-frac) * (is_left and 0 or -56),
0)
glow_box(0,0,55,100,frame_col(self:hot()),1)
spr(self.s,5, 5)
print(self.hdr, 13, 8, 7)
print(self.body, 5, 15, 6)
camera()
end
rearm_pane = mknew(easing_pane.new{})
function rearm_pane:draw()
local frac = self:frac()
if (not frac) return
camera(0, -28 * frac)
glow_box(0,101,111,127,frame_col(self:hot()),1)
spr(5,15,107,4,2)
print("full ammo\nfull shield\n+50% health",54, 106, 6)
camera()
end
__gfx__
000000000b00000000000a0007700770000aa0000444440004444444000000000000000000000000000000000000000000000000000000000000000000000000
00000000bba80880000008000aa00aa00a0880a0447777700477777a000000000000000000000000000000000000000000000000000000000000000000000000
007007000aaa28780a0000000990099008000080477aaa7a0477aaaa000000000000000000000000000000000000000000000000000000000000000000000000
0007000008a8887808000000099009900080080047a0047a047a0000000000000000000000000000000000000000000000000000000000000000000000000000
00007000088888820000a000088008800000000047a0447a047a0000000000000000000000000000000000000000000000000000000000000000000000000000
00700700008888200000800008800880a000000a47a4477a047a4440000000000000000000000000000000000000000000000000000000000000000000000000
000000000008820000a0000008800880080aa080477777a00477777a000000000000000000000000000000000000000000000000000000000000000000000000
0000000000002000008000000880088000088000477770000422aaaa222200020000020000000000000000000000000000000000000000000000000000000000
0d5000000000000000000000000000000000000047a77700022ee0002eeee002e00022e000000000000000000000000000000000000000000000000000000000
d00000000000000000000000000000000000000047a4777002ea2e002e002e02ee022ee000000000000000000000000000000000000000000000000000000000
500000000000000000000000000000000000000047a0477a22ea2e002e002e02e2e2e2e000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000047a0047a2e2222e02e222e02e02e02e000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000047a0047a2eeeeeea2eeee002e02e02e000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000aa000aa2e7aa2ea2e00e002e02e02e000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000002e0002e02e002e02e02e02e000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000e0000e00e000e00e00e00e000000000000000000000000000000000000000000000000000000000

View File

@ -124,11 +124,12 @@ function linked_list:pop_front()
end
function _init()
mode = game_mode
init_blip_pals()
wipe_level()
primary_ship.main_gun = zap_gun_p.new() -- redundant?
load_level(example_level_csv)
state = game
game_state = game
pal(2,129)
pal()
end
@ -157,6 +158,7 @@ function init_hpcols()
end
function wipe_level()
xpwhoosh = nil
primary_ship = player.new()
init_hpcols()
pships = linked_list.new()
@ -171,7 +173,7 @@ function wipe_level()
end
function _update60()
updategame()
mode:update()
end
function call_f(x)
@ -183,6 +185,10 @@ function call_move(x)
end
function updategame()
if (primary_ship.xp >= primary_ship.xptarget) and (lframe - primary_ship.last_xp_frame > 0x0.000f) and (not primary_ship.dead) then
mode = rearm_mode.new()
return _update60()
end
leveldone = level_frame()
events:vore(new_events)
events:strip(call_move)
@ -248,20 +254,41 @@ function updategame()
intangibles_fg:strip(call_move)
if leveldone and not eships.next and not ebullets.next and not events.next then
state = win
game_state = win
end
if (not pships.next) state = lose
if (not pships.next) game_state = lose
if primary_ship.xp >= primary_ship.xptarget then
if not xpwhoosh then
xpwhoosh = 0
else
xpwhoosh += 1
if (xpwhoosh > 60) xpwhoosh = 0
end
else
xpwhoosh = nil
end
end
function _draw()
mode:draw()
end
function drawgame_top()
camera()
fillp(0)
drawgame()
if (state == game) fadelvl = -45
if (state == win) dropshadow("win",50,61,11)
if (state == lose) dropshadow("fail",48,61,8)
if (game_state == game) fadelvl = -45
if (game_state == win) dropshadow("win",50,61,11)
if (game_state == lose) dropshadow("fail",48,61,8)
fadescreen()
end
game_mode = {
update = updategame,
draw = drawgame_top,
}
fadetable = split"0,1.5,1025.5,1029.5,1285.5,1413.5,9605.5,9637.5,-23130.5,-23066.5,-18970.5,-18954.5,-2570.5,-2568.5,-520.5,-8.5,-0.5"
function fadescreen()
@ -337,30 +364,44 @@ function drawhud()
line(113,127)
draw_gun_info("❎",1,116,3,primary_ship.main_gun)
draw_gun_info("🅾️",1,116,31,primary_ship.special_gun)
draw_gun_info("🅾️",1,116,29,primary_ship.special_gun)
dropshadow("x h",114,59,1)
inset(114,66,119,125)
inset(114,57,119,118)
rectfill(119,57,124,58,13)
inset(120,64,125,125)
rectfill(114,124,120,125,7)
print("XP",119,55,1)
print("HP",114,122,1)
fillp(0x5a5a)
vertmeter(115,67,118,124,primary_ship.xp, primary_ship.xptarget, powcols)
inset(120,66,125,125)
-- 57 px vertically
if xpwhoosh then
clip(115,58,4,60)
rectfill(115,58,118,117,0xaa)
local voff = 5*xpwhoosh+6
rectfill(115,118-voff,118,117-voff+10,0xbb)
rectfill(115,118-voff+11,118,117-voff+20,0xba)
clip()
else
vertmeter(115,58,118,117,primary_ship.xp, primary_ship.xptarget, powcols)
end
-- 60 px vertically. note that
-- there was at one point an
-- off-by-one and I'm not sure
-- it's actually fixed
local mxs, cs, mxh, ch = primary_ship.maxshield, primary_ship.shield, primary_ship.maxhp, primary_ship.hp
if (mxs > 0) and (mxh > 0) then
local split = 57 * (mxs / (mxs + mxh)) \ 1 + 66
local split = 59 * (mxs / (mxs + mxh)) \ 1 + 64
line(121, split, 124, split, 0xba)
vertmeter(121,67,124,split-1,cs, mxs,shlcols)
vertmeter(121,65,124,split-1,cs, mxs,shlcols)
vertmeter(121,split+1,124,124,ch, mxh, hpcols)
elseif mxs > 0 then
vertmeter(121,67,124,124,cs,mxs,shlcols)
vertmeter(121,65,124,124,cs,mxs,shlcols)
elseif mxh > 0 then
vertmeter(121,67,124,124,ch,mxh,hpcols)
vertmeter(121,65,124,124,ch,mxh,hpcols)
else
print("!", 122, 94, 9)
print("!", 121, 93, 8)
print("!", 122, 93, 9)
print("!", 121, 92, 8)
end
fillp(0)
fillp(0)
end
function draw_gun_info(lbl,fgc,x,y,gun)
@ -387,8 +428,13 @@ end
function vertmeter(x0,y0,x1,y1,val,maxval,cols)
if ((val <= 0) or (maxval <= 0)) return
if val < 0x0.001 or maxval < 0x0.001 then
val *= 16
maxval *= 16
end
val=min(val, maxval)
local h = y1-y0
local px = val/maxval * h \ 1
local px = val*h/maxval \ 1
local ncols = #cols
local firstcol = ((h-px)*ncols\h)+1
local lastbottom = y0+(h*firstcol\ncols)
@ -407,7 +453,7 @@ function inset(x0,y0,x1,y1)
-- fillp
rect(x0,y0,x1,y1,119)
line(x1,y0,x0,y0,85)
line(x0,y1,85)
line(x0,y1-1,85)
end
function dropshadow(str, x, y, col)
@ -427,6 +473,7 @@ ship_m = mknew{
maxshield = 0,
shieldcooldown = 0x0.003c,--1s
shieldpenalty = 0x0.012c, --5s
shield_refresh_ready = 0,
slip = true, -- most enemies slide
@ -443,7 +490,27 @@ ship_m = mknew{
function ship_m:die()
self.dead = true
if (self.hp < 0) boom(self.x+self.size*4, self.y+self.size*4,12*self.size, self.boss)
if (self.hp >= 0) return
-- blow up and drop xp
local sz4 = self.size * 4
local cx, cy, xp, z = self.x + sz4, self.y + sz4, self.xp or 0, 0
boom(cx, cy, 3*sz4, self.boss)
if xp > 0x0.01f3 then -- dec 499
-- spawn a huge gem with all
-- overage XP, min 100
spawn_xp_at(cx, cy, 0, xp-0x0.018f)
xp = 0x0.018f -- dec 399
z += 1
end
-- 100, 25, 5, 1
for gsz in all{0x0.0064, 0x0.0019, 0x0.0005, 0x0.0001} do
while xp >= gsz do
spawn_xp_at(cx, cy, z, gsz)
xp -= gsz
z += 1
end
end
end
function ship_m:calc_velocity(v0, t)
@ -626,7 +693,7 @@ function bullet_base:move()
self.x += self.dx
self.y += self.dy
if (self.f) self.f -= 1
if (self.y > 128) or (self.y < -8 * self.height) or (self.f and self.f < 0) then
if (self.y > 145) or (self.y < -8 * self.height) or (self.f and self.f < 0) then
self:die()
return true
end
@ -895,9 +962,10 @@ player = mknew(ship_m.new{
shield = 2, -- regenerates
maxshield = 2,
-- xp, increments of 0x0.01
-- xp in increments of 0x0.0001
xp = 0,
xptarget = 0x0.05,
xptarget = 0x0.0004,
last_xp_frame = 0,
level = 1,
-- gun
@ -954,8 +1022,8 @@ frownie = mknew(ship_m.new{
sparks = smokespark,
sparkodds = 8,
-- health
hp = 0.5, -- enemy ships need no max hp
xp = 0x0.0001,
-- position
x=60, -- x and y are for upper left corner
@ -977,6 +1045,7 @@ frownie = mknew(ship_m.new{
blocky = mknew(frownie.new{
sprite = 10,
hp = 1.5,
xp = 0x0.0002,
hurt = {
x_off = 0,
y_off = 0,
@ -996,6 +1065,7 @@ blocky = mknew(frownie.new{
spewy = mknew(frownie.new{
sprite=26,
xp = 0x0.0003,
hurt = {
x_off=0,
y_off=1,
@ -1016,6 +1086,7 @@ spewy = mknew(frownie.new{
chasey = mknew(ship_m.new{
sprite = 5,
xp = 0x0.0004,
size = 1,
hurt = {
x_off = 1,
@ -1050,6 +1121,7 @@ end
xl_chasey=mknew(chasey.new{
size=2,
xp = 0x0.000a,
maxspd=1.25,
hurt = {
x_off = 2,
@ -1413,362 +1485,6 @@ example_level_csv=[[1,spawn_frownie
720,spawn_blocking_boss_chasey
721,eol]]
-->8
-- readme.md
--[[
main loop sequence
==================
1. level_frame
2. events
3. merge new_events into events
4. update bg intangibles
5. move ships (player first)
6. move bullets (player first)
7. calculate collisions
1. pship on eship
2. ebullet on pship
3. pbullet on eship
8. update fg intangibles
9. check for end of level
draw order
----------
bottom to top:
1. intangibles_bg
2. player bullets
3. player ships
4. enemy ships
5. enemy bullets
6. intangibles_fg
notes
-----
intangibles_fg move()s after
all collisions and other moves
are processed. if an intangible
is added to the list as a result
of a collision or move, it will
itself be move()d before it is
drawn.
data-driven items
=================
guns and bullets both allow the
most common behaviors to be
expressed with data alone.
ships only need a movement
algorithm expressed.
guns
----
* t - metatable for bullet type.
fired once in the bullet's
default direction per shot.
* enemy - if true, fired bullets
are flagged as enemy bullets.
* icon - sprite index of an
8x8 sprite to display in the
hud when the player has this
gun. default is 20, a generic
crosshair bullseye thing.
* cooldown - min frames between
shots.
* ammo, maxammo - permitted
number of shots. 0 is empty
and unfireable. maxammo = 0
will cause a divide by zero
so don't do that. if nil,
ammo is infinite.
default guns manage ammo and
cooldown in shoot, then call
actually_shoot to create the
projectile. override only
actually_shoot to change
projectile logic while keeping
cooldown and ammo logic.
bullets
-------
* dx, dy - movement per frame.
player bullets use -dy
instead.
* enemyspd - multiplier for dx
and dy on enemy bullets.
default is 0.5, making enemy
shots much easier to dodge
* damage - damage per hit;
used by ships
* sprite - sprite index.
* x_off, y_off - renamed for
the next two vars. may revert
* center_off_x - the horizontal
centerpoint of the bullet,
for positioning when firing.
assume a pixel's coordinates
refer to the upper left corner
of the pixel; the center of
a 2-width bullet with an
upper left corner at 0 is 1,
not 0.5.
* top_off_y, bottom_off_y -
also for positioning when
firing. positive distance from
top or bottom edge to image.
top_off_y will usually be 0,
bottom_off_y will not be when
bullets are smaller than
the sprite box.
* width, height - measured in
full sprites (8x8 boxes), not
pixels. used for drawing.
bullets despawn when above or
below the screen (player or
enemy bullets, respectively).
by default, bullets despawn
when they hit something.
override hitship to change this.
ships
____
ships move by calculating
momentum, then offsetting their
position by that momentum, then
clamping their position to the
screen (horizontally only for
ships that autoscroll). ships
that autoscroll (slip==true)
then slide down by scrollspeed.
fractional coordinates are ok.
after movement, ships lose
momentum (ship.drag along each
axis). abs(momentum) can't
exceed ship.maxspeed.
ships gain momentum by acting
like a player pushing buttons.
the player ship actually reads
buttons for this.
act -- returns new acceleration:
dx, dy, shoot_spec, shoot_main.
dx and dy are change in momentum
in px/frame. this is controls
only -- friction is handled in
ship:move (`drag` value).
ships hitting another ship take
1 damage per frame of overlap.
ships hitting a bullet check
bullet.damage to find out how
much damage they take. damage
is applied to shields, then hp.
damaged ships flash briefly -
blue (12) if all damage was
shielded, white (7) if hp was
damaged. a ship that then has 0
or less hp calls self:die() and
tells the main game loop to
remove it.
shieldcooldown is the interval
between restoring shield points.
shieldpenalty is the delay
before restoring points after
any damage, reset to this value
on every damaging hit (whether
it is absorbed by the shield or
not) -- shield behaves like
halo and other shooters in its
heritage, where it recovers if
you avoid damage for a while.
not that there is any safe cover
in this kind of game.
ships do not repair hp on their
own. negative-damage bullets
are treated as 0, but a bullet
can choose to repair the ship
it hits in its own hitship
method, or otherwise edit it
(changing weapons, refilling
weapon ammo). powerups are
therefore a kind of bullet.
levels
======
a level is a table mapping
effective frame number to
functions. when a level starts,
it sets lframe ("level frame")
and distance to 0.
every frame, level_frame
increments lframe by 0x0.0001.
then if the level is not frozen,
it increments distance by 1.0
and runs the function in the
level table for exactly that
frame number (if any). distance
is therefore "nonfrozen frames",
and is used to trigger level
progress. lframe always
increments. ships are encouraged
to use lframe to control
animation and movement, and may
use distance to react to level
progress separately from overall
time. remember to multiply
lframe-related stuff by 0x0001.
a special sentinel value, eol,
marks the end of the level.
(the level engine doesn't know
when it's out of events, so
without eol, the level will
simply have no events forever.)
when it finds eol, level_frame
throws away the current level
and tells the main loop that it
might be done. the main loop
agrees the level is over and the
player has won when the level
has reached eol and there are
no more enemy ships, enemy
bullets, or background events
remaining. player ships, player
bullets, and intangibles are
not counted.
level freezing
--------------
the level is frozen when the
global value freeze > 0.
generally, something intending
to block level progress (a
miniboss, a minigame, etc.)
increments freeze and prepares
some means of decrementing it
when it no longer wants to block
level progress.
most commonly, we want to block
until some specific ship or
group of ships has died. for
these ships, override ship:die
to decrement freeze. make sure
to set ship.dead in any new
ship:die method so anything else
looking at it can recognize
the ship as dead.
for anything else, you probably
want an event to figure out when
to unfreeze.
levels start at 1
-----------------
distance is initialized to 0
but gets incremented before the
first time the engine looks for
events. therefore, the first
frame of the level executes
level[1]. since levelframe
executes before anything else,
level[1] sets up the first frame
drawn in the level. the player
does not see a blank world
before level[1] runs.
level[1] can therefore be used
to reconfigure the player ship,
set up backgrounds, start music,
kick off some kind of fade-in
animation, etc.
events
======
the global list "events" stores
0-argument functions which are
called every frame. if they
return true, they are removed
from the list and not run again;
if they return false, they stay
and will be called in later
frames. the level does not end
while the events table is
nonempty.
events are most commonly used
to set up something for later
(for example, blip uses an event
to remove the fx_pallete from
the flashing ship when the blip
expires), but can also be used
to implement a "level within a
level" that does something
complicated until it's done. if
you froze the level when
creating the event, remember
to thaw it (freeze -= 1) on all
paths that return true.
to do complex stuff in events,
use a closure or a metatable
that specifies __call.
to avoid editing the events
list while it is being iterated,
events that create new events
must add those events to
new_events rather than events.
new_events is only valid during
the "event execution" stage, so
events created at any other time
must go directly on events
without using new_events.
intangibles
===========
the intangibles_fg and
intangibles_bg lists contain
items with :move and :draw.
like ships and bullets, they
move during _update60 and
draw during _draw. they are
not checked for collisions.
intangibles_bg moves/draws
before anything else moves or
draws. intangibles_fg
moves/draws last. this controls
whether your intangible object
draws in front of or behind
other stuff. you probably want
intangibles_bg for decorative
elements and intangibles_fg
for explosions, score popups,
etc.
there's no scrolling background
engine but intangibles_bg could
be used to create one, including
using the map (otherwise unused
in this engine) for the purpose.
intangibles do not prevent the
level from ending. like bullets
and ships, if :move returns
true, they are dropped.
]]
-->8
-- standard events
@ -1853,6 +1569,61 @@ end
-->8
-- powerups
xp_gem = mknew(bullet_base.new{
dx = 0,
dy = 0.75,
width=1, -- not used for spr but
height=1,-- bullet_base uses it
category = enemy_blt_cat,
damage = 0,
hurt = {
x_off = -2,
y_off = -2,
width = 8,
height = 8,
},
x_off = 2,
y_off = 2,
})
function xp_gem:draw()
local s,qx,qy = self.qsprite,0,0
-- sprite map position:
-- sprite id to x and y,
-- offset shifts specific low
-- bits of lframe up to the the
-- bit with value 4 as a cheap
-- way to pick an anim frame
if (lframe&0x0.003 == 0) qx, qy = (lframe&0x0.0004)<<16, (lframe&0x0.0008)<<15
sspr(
(s%16<<3)+qx,
(s\16<<3)+qy,
4, 4,
self.x, self.y
)
end
-- todo: "magnetic" behavior
-- when near player ship
function xp_gem:hitship(ship)
if (ship ~= primary_ship) return false
primary_ship.xp += self.val
primary_ship.last_xp_frame = lframe
return true
end
-- small gems for 1, 5, 25
-- exactly; else huge
function spawn_xp_at(x, y, off, amt)
x += rnd(off+off)-off
y += rnd(off+off)-off
xp_gem.new{
qsprite=amt == 0x0.0001 and 32 or amt == 0x0.0005 and 33 or amt == 0x0.0019 and 34 or 35,
val = amt,
}:spawn_at(mid(x, 0, 124),mid(y,-4,125))
end
powerup = mknew(bullet_base.new{
-- animated sprite array: "sprites"
-- to draw under or over anim,
@ -1868,7 +1639,7 @@ powerup = mknew(bullet_base.new{
-- but powerups should feel
-- easy to pick up
dx = 0,
dy = 1.5, -- 0.75 after enemyspd
dy = 0.75,
category = enemy_blt_cat, -- collides with player ship
damage = 0,
@ -1879,11 +1650,6 @@ powerup = mknew(bullet_base.new{
-- sprite indexes for "sheen" animation
sheen8x8 = split"2,54,55,56,57,58,59,60,61"
-- todo: draw two sprites
-- on top of each other here
-- so all powerups can share
-- the "sheen" animation?
function powerup:draw()
spr(self.sprites[max(1,
((lframe<<16)\self.anim_speed)
@ -1990,6 +1756,162 @@ function spawn_spec_gun_at(x, y, gunt)
}
gun_p:spawn_at(x, y)
end
-->8
-- rearm screen
rearm_mode = mknew{
sel=1,
bfm=1,
crt_frm = 1,
pos=-1,
init=function(this)
poke(0x5f5c, 255) --no btnp repeat
rearm_mode.shuffle(this)
end,
}
crt={-91,-166,-2641,-1441,-23041,23295,-20491,24570}
function rearm_mode:glow_box(x0, y0, x1, y1, c, cf)
for i,v in ipairs{c[1],c[2],c[1],0} do
i -= 1
rect(x0+i,y0+i,x1-i,y1-i,v)
end
fillp(crt[self.crt_frm&0xff])
rectfill(x0+4, y0+4, x1-4, y1-4, cf)
fillp()
end
function easeoutbounce(t)
local n1=7.5625
local d1=2.75
if (t<1/d1) then
return n1*t*t;
elseif(t<2/d1) then
t-=1.5/d1
return n1*t*t+.75;
elseif(t<2.5/d1) then
t-=2.25/d1
return n1*t*t+.9375;
else
t-=2.625/d1
return n1*t*t+.984375;
end
end
function rearm_mode:frame_col(hot)
if (not hot) return {4,10}
if (self.bfm<=16) return {14,7}
return {2,8}
end
function rearm_mode:draw_option(id)
local rec = self.options[id]
self:glow_box(0,0,55,100,self:frame_col(self.sel == id),1)
spr(rec.s,5, 5)
print(rec.hdr, 13, 8, 7)
print(rec.body, 5, 15, 6)
end
function rearm_mode:pos_frac()
local pos = self.pos
if (not pos) return
if (pos < 0) return 1-easeoutbounce(1+pos)
if (pos > 0) return (1-pos)*(1-pos)
return 0
end
function rearm_mode:shuffle()
-- these will be placeholders
-- until the upgrade deck
-- is a thing that exists
self.options = {{
s=1,
hdr=" hull",
body = "\n +1\n max\n health",
action = function() end,
},{
s=37,
hdr=" vulc",
body = "\nplaceholder",
action = function() end,
}}
end
function rearm_mode:draw()
drawgame_top()
local frac = self:pos_frac()
camera(frac * 55, 0)
self:draw_option(1)
camera(frac * -128 + (1-frac) * -56, 0)
self:draw_option(2)
camera(0, -28 * frac)
self:glow_box(0,101,111,127,self:frame_col(self.sel < 0),1)
spr(96,15,107,4,2)
print("full ammo\nfull shield\n+50% health",54, 106, 6)
end
function rearm_mode:update_pos()
local pos = self.pos
if (not pos) return
if (pos == 0) then
if (primary_ship.xp < primary_ship.xptarget) self.pos = 1
xpwhoosh = nil
return
end
if (pos < 0) pos = min(pos + 0x0.05, 0)
if pos > 0 then
pos -= 0x0.1
if (pos <= 0) pos = 999
end
self.pos = pos
end
function rearm_mode:update()
self:update_pos()
if self.pos > 1 then
mode = game_mode
return -- do not advance frame
end
local sel, bfm = self.sel, self.bfm
if (btn(3) and sel > 0 or btn(2) and sel < 0) sel=-sel
if (btn(0)) sel = 1
if (btn(1)) sel = 2
if (btn()&0xF ~= 0) and bfm >= 10 or bfm >= 30 then
bfm = 1
else
bfm += 1
end
self.bfm = bfm
if primary_ship.xp < primary_ship.xptarget then
sel = 0
elseif btnp(4) or btnp(5) and self.pos == 0 then
if sel < 0 then
-- todo: sound: rearm
primary_ship.shield = primary_ship.maxshield
-- todo: rewrite for three guns
if (primary_ship.special_gun) primary_ship.special_gun.ammo = primary_ship.special_gun.max_ammo
primary_ship.hp = min(primary_ship.maxhp, primary_ship.hp + primary_ship.maxhp/2)
primary_ship.xp -= primary_ship.xptarget / 2
else
local c = self.options[sel]
if c then
-- todo: sound: upgrade
c:action()
primary_ship.xp -= primary_ship.xptarget
primary_ship.xptarget += primary_ship.level * 0x0.0002
primary_ship.level += 1
if (primary_ship.xp >= primary_ship.xptarget) self:shuffle()
end
end
end
self.sel = sel
end
__gfx__
00000000000650000000000000000000bb0b50b59909209200cc0c00000000003b00000082000000e00e8002e00e800200333300002222000000000000000000
00000000006765000000000000cccc00b50b3055920940220c0000c000bbbb0037000000a2000000e0e8880240e8480403bbbb30028888200000000000000000
@ -2007,14 +1929,14 @@ __gfx__
000000005666657576667650000000000b0000b000000000000000000000000000000000000dd0009092220200000000c111111d656667650000000000000000
0000000056565066665656500000000000bbbb0000000000000000000000000000000000000000000090020000000000c111111d650650650000000000000000
00000000565000566500065000000000b000000b000000000000000000000000000000000000000000a00a0000000000cddddddd650000650000000000000000
000000000000000000000000000000000000000000a0008000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000090008000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000800a0000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000080090000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000a080000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000009080000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000
060007000600070006600770766c777c0000000000a0008000000000000000000000000000000000000000000000000000000000000000000000000000000000
6cd07cd06cd07cd06ccd7ccd6ccd7ccd000000000090008000000000000000000000000000000000000000000000000000000000000000000000000000000000
0d000d006cd07cd06ccd7ccd6ccd7ccd0000000000800a0000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000d000d000dd00dd0cdd1cdd0000000000080090000000000000000000000000000000000000000000000000000000000000000000000000000000000
0600060006000600066006607667766c00000000000a080000000000000000000000000000000000000000000000000000000000000000000000000000000000
67d06c7067d06c70677d6cc7677d6cc7000000000009080000000000000000000000000000000000000000000000000000000000000000000000000000000000
0d00070067d06c7067cd6cc767cd6cc7000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000d0007000dd007707dd1c771000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000cccccccccccccccc77000000007700000000770000000077000000000000000000000000000000000000000000000000
00000000000000000000000000000000c116611dc11ee11d70000000077000000007700000000770000000070000000000000000000000000000000000000000
00000000000000000000000000000000c1611c1dc11ee11d00000000770000000077000000007700000000770000000700000000000000000000000000000000
@ -2035,6 +1957,26 @@ c1111111111d0000c1111111111d0000c1111111111d0000c111eeee111d0000c11e2222e11d0000
c1111111111d0000c1111111111d0000c1111111111d0000c1111111111d0000c111eeee111d0000c1ee2222ee1d0000ce22111122ed0000c2111111112d0000
c1111111111d0000c1111111111d0000c1111111111d0000c1111111111d0000c1111111111d0000c111eeee111d0000ceee2222eeed0000c2221111222d0000
cddddddddddd0000cddddddddddd0000cddddddddddd0000cddddddddddd0000cddddddddddd0000cddddddddddd0000cddddddddddd0000cddddddddddd0000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
04444400044444440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
447777700477777a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
477aaa7a0477aaaa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
47a0047a047a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
47a0447a047a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
47a4477a047a44400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
477777a00477777a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
477770000422aaaa2222000200000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
47a77700022ee0002eeee002e00022e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
47a4777002ea2e002e002e02ee022ee0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
47a0477a22ea2e002e002e02e2e2e2e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
47a0047a2e2222e02e222e02e02e02e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
47a0047a2eeeeeea2eeee002e02e02e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0aa000aa2e7aa2ea2e00e002e02e02e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000002e0002e02e002e02e02e02e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000e0000e00e000e00e00e00e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
__label__
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007777777777777777
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007666666666666665