diff options
| author | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-04-22 15:58:41 -0700 |
|---|---|---|
| committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-04-22 15:58:41 -0700 |
| commit | 078eca583eec21d317e931c84db8f084bef4305d (patch) | |
| tree | 76b303cdda70a87a5febaf57726d4f8779a387a3 | |
| parent | 3555b9ff88c3872c8f2dd8a8ab02382e2a7d0cb2 (diff) | |
| download | dyl8-078eca583eec21d317e931c84db8f084bef4305d.tar.gz dyl8-078eca583eec21d317e931c84db8f084bef4305d.zip | |
Snapshot
| -rw-r--r-- | animation.lua | 8 | ||||
| -rw-r--r-- | collisions.lua | 30 | ||||
| -rw-r--r-- | dyl.lua | 69 | ||||
| -rw-r--r-- | dyl.p8 | 306 | ||||
| -rw-r--r-- | entity.lua | 208 | ||||
| -rw-r--r-- | math.lua | 36 | ||||
| -rw-r--r-- | util.lua | 52 | ||||
| -rw-r--r-- | world.lua | 5 |
8 files changed, 414 insertions, 300 deletions
diff --git a/animation.lua b/animation.lua new file mode 100644 index 0000000..0f6fba6 --- /dev/null +++ b/animation.lua @@ -0,0 +1,8 @@ +function get_frame(anim, t, theta) + len = #anim.seq + frames_passed = (t / anim.dt) + idx = flr(1 + (frames_passed % len)) + dspritey = 0 + if theta == 1 then dspritey = 16 end + return anim.seq[idx] + dspritey +end
\ No newline at end of file diff --git a/collisions.lua b/collisions.lua new file mode 100644 index 0000000..fb7aca7 --- /dev/null +++ b/collisions.lua @@ -0,0 +1,30 @@ +sw, sh = 8, 8 +function is_colliding(a, b) + ax1, bx1 = a.pos.x, b.pos.x + ax2, bx2 = ax1 + sw, bx1 + sw + ay1, by1 = a.pos.y, b.pos.y + ay2, by2 = ay1 + sh, by1 + sh + return (ax1 < bx2 and ax2 > bx1 + and ay1 < by2 and ay2 > by1) +end + +function handle_collision(a, b) +-- if b.typ == types.enemy then +-- --print(b.id,100,100) +-- end +end + +function run_collisions() + collidable = filter(World, function (e) return e.collision end) + for _ai, a in ipairs(collidable) do + for _bi, b in ipairs(collidable) do + if a.id == b.id then + goto continue + end + if is_colliding(a, b) then + handle_collision(a, b) + end + ::continue:: + end + end +end
\ No newline at end of file @@ -0,0 +1,69 @@ +sword = entity() + :b_type(Entities.Sword) + :b_render_order(1) + :b_collidable() + :b_add_state("equipped", { + animation={ + pos_y = { sequence = { 82 }, dt = 1, reflect = vec2(false, true) }, + neg_y = { sequence = { 82 }, dt = 1 }, + pos_x = { sequence = { 82 }, dt = 1 }, + neg_x = { sequence = { 82 }, dt = 1, reflect = vec2(true, false) } + } + }) + +player = entity() + :b_type(Entities.Player) + :b_render_order(0) + :b_collidable() + :b_position(vec2(30,30)) + :b_sprite_position(vec2(30,30)) + :b_add_state(States.Idle, { + animation = { + pos_y = { sequence = { 3, 19 }, dt = 1 }, + neg_y = { sequence = { 6, 22 }, dt = 1 }, + pos_x = { sequence = { 0, 1 }, dt = 1 }, + neg_x = { sequence = { 0, 1 }, dt = 1, reflect = vec2(true, false) } + } + }) + :b_add_state(States.Walk, { + animation = { + pos_x = { seq = { 2, 0 }, dt = 0.20 }, + neg_x = { seq = { 2, 0 }, dt = 0.20, reflect = vec2(true, false) }, + pos_y = { seq = { 4, 5 }, dt = 0.20 }, + neg_y = { seq = { 7, 8 }, dt = 0.20 } + } + }) + :b_state("idle") + :b_equipped({ sword }) + +_walk_speed = 35 -- powerup increase? +function handle_input() + dpos = vec2(0, 0) + if btn(0) then dpos.x -= 1 end + if btn(1) then dpos.x += 1 end + if btn(3) then dpos.y += 1 end + if btn(2) then dpos.y -= 1 end + + player.velocity = dpos:normal() * _walk_speed +end + +function _init() +end + +step_t = time() +function _update60() + cls(0) -- clear here in case we want to print to screen outside of _draw + + old_step = step_t + step_t = time() + step_dt = step_t - old_step + + handle_input() + foreach(qsort(World, function(a, b) return a.equipped != nil end), function (e) e:update(step_dt) end) + run_collisions() +end + +function _draw() + -- foreach(qsort(World, function (a, b) return a.render_order < b.render_order end), function (e) e:render() end) + foreach(World, function (e) e:render() end) +end
\ No newline at end of file @@ -1,311 +1,17 @@ pico-8 cartridge // http://www.pico-8.com version 43 __lua__ +#include util.lua +#include math.lua +#include entity.lua +#include world.lua +#include collisions.lua +#include dyl.lua ------------------------ -- don't you leave -- -- emprespresso, 2026 -- ------------------------ -walk_speed=35 -- powerup increase? -step_t=time() -step_dt=0 - -id=0 -function next_id() - i=id - id=id+1 - return i -end - -types={ - player=0, - sword=1, - enemy=2, -} - -sword={ - id=next_id(), - typ=types.sword, - theta=0, -- pi/2=1,discrete [0,1] - pos={x=10,y=10}, - spritepos={x=10,y=10}, - states={ - equip={seq={83},dt=1}, - }, - state="equip", - state_t=step_t, - reflect={x=false,y=false}, -} - -player={ - id=next_id(), - typ=types.player, - pos={x=50,y=50}, - spritepos={x=10,y=10}, - vel={x=0,y=0}, - los={x=1,y=0}, -- lineofsight - states={ - idlex={seq={0,1},dt=1}, - idlepy={seq={3,19},dt=1}, - idleny={seq={6,22},dt=1}, - walkx={seq={2,0},dt=0.20}, - walkpy={seq={4,5},dt=0.20}, - walkny={seq={7,8},dt=0.20}, - }, - state="idle", - state_t=step_t, - equipped={sword}, - reflect={x=false,y=false}, -} - -enemy={ - id=next_id(), - typ=types.enemy, - pos={x=20,y=20}, - spritepos={x=20,y=20}, - vel={x=0,y=0}, - los={x=-1,y=0}, -- lineofsight - states={ - idlex={seq={32,33},dt=1}, - idlepy={seq={32,33},dt=1}, - idleny={seq={32,33},dt=1}, - walkx={seq={32,34},dt=0.28}, - walkpy={seq={32,34},dt=0.28}, - walkny={seq={32,34},dt=0.28}, - }, - state="idle", - state_t=step_t, - reflect={x=false,y=false}, - knockback={vector={x=0,y=0},remaining=0} -} - -entities={player,sword,enemy} - -sw,sh=8,8 -function is_colliding(a,b) - ax1,bx1=a.pos.x,b.pos.x - ax2,bx2=ax1+sw,bx1+sw - ay1,by1=a.pos.y,b.pos.y - ay2,by2=ay1+sh,by1+sh - return (ax1<bx2 and ax2>bx1 and - ay1<by2 and ay2>by1) -end - -function handle_collision(a,b) - if b.typ==types.enemy then - --print(b.id,100,100) - end -end - -function run_collisions() - for _ai,a in ipairs(entities) do - for _bi,b in ipairs(entities) do - if a.id==b.id then - goto continue - end - if is_colliding(a,b) then - handle_collision(a,b) - end - ::continue:: - end - end -end - -function enter_state(entity, state) - if state==entity.state then - return - end - entity.tstate=step_t - entity.state=state -end - --- prevent cobblestoning during --- non-manhattan movement --- by "lagging" the sprite --- behind the physical position --- -function update_spritepos(entity) - -- step in only x or y. - -- we can snap to the grid - -- without cobblestoning. - if entity.vel.y==0 or entity.vel.x==0 then - entity.spritepos.x=entity.pos.x - entity.spritepos.y=entity.pos.y - return - end - - nv=normalize(entity.vel) - dx,dy=entity.pos.x-entity.spritepos.x,entity.pos.y-entity.spritepos.y - adx,ady=abs(dx),abs(dy) - - yslow=abs(entity.vel.y)<abs(entity.vel.x) - xslow=abs(entity.vel.x)<=abs(entity.vel.y) - - pushx,pushy=false,false - jerkdelta=1 - if adx>=jerkdelta and xslow - and ady>=1 then - pushx,pushy=true,true - elseif ady>=jerkdelta and yslow - and adx>=1 then - pushx,pushy=true,true - elseif xslow and ady>=1 then - pushy=true - elseif yslow and adx>=1 then - pushx=true - end - - if pushx then - func=flr - if dx<0 then func=ceil end - entity.spritepos.x=entity.spritepos.x+func(dx) - end - if pushy then - func=flr - if dy<0 then func=ceil end - entity.spritepos.y=entity.spritepos.y+func(dy) - end -end - -suffixes={"ny","","py"} -function update(entity) - if entity.vel != nil then - integral_vel=vec_scale(entity.vel,step_dt) - entity.pos=vec_add(entity.pos,integral_vel) - update_spritepos(entity) - - dx,dy=entity.vel.x,entity.vel.y - ndx,ndy=normalize_scalar(dx),normalize_scalar(dy) - - if ndy!=0 then - walksuffix=suffixes[ndy+2] - enter_state(entity,"walk"..walksuffix) - elseif ndx!=0 then - enter_state(entity,"walkx") - end - - if ndx!=0 then - entity.los={x=ndx,y=0} - end - if ndy!=0 then - entity.los={x=0,y=ndy} - end - entity.reflect.x=(entity.los.x<0) - - if ndx==0 and ndy==0 then - if entity.los.x!=0 then - enter_state(entity,"idlex") - elseif entity.los.y!=0 then - suffix=suffixes[entity.los.y+2] - enter_state(entity,"idle"..suffix) - end - end - end - - if entity.equipped!=nil then - foreach(entity.equipped, - equipped_from(entity)) - end -end - -function equipped_from(entity) - return function(equipped) - if entity.los.y!=0 then - equipped.theta=1 - else - equipped.theta=0 - end - equipped.reflect={ - x=(entity.los.x<0 or entity.los.y<0), - y=(entity.los.y>0) - } - equipped.pos.y=entity.pos.y+entity.los.y*6 - equipped.pos.x=entity.pos.x+entity.los.x*6 - equipped.spritepos.x=flr(entity.spritepos.x+entity.los.x*6) - equipped.spritepos.y=flr(entity.spritepos.y+entity.los.y*6) - end -end - -function get_frame(anim,t,theta) - len=rawlen(anim.seq) - frames_passed=(t/anim.dt) - idx=flr(1+(frames_passed%len)) - dspritey=0 - if theta==1 then dspritey=16 end - return anim.seq[idx]+dspritey -end - -function draw(entity) - a=entity.states[entity.state] - t=step_t-entity.state_t - f=get_frame(a,t,entity.theta) - spr(f, - entity.spritepos.x,entity.spritepos.y, - 1,1, - entity.reflect.x,entity.reflect.y - ) -end - - -function handle_input() - dx,dy=0,0 - if btn(⬅️) then dx=dx-1 end - if btn(➡️) then dx=dx+1 end - if btn(⬇️) then dy=dy+1 end - if btn(⬆️) then dy=dy-1 end - - player.vel= - vec_scale( - normalize({x=dx,y=dy}), - walk_speed - ) -end - -function _init() - -end - -function _update() - cls(0) - old_step=step_t - step_t=time() - step_dt=step_t-old_step - - handle_input() - run_collisions() - foreach(entities,update) -end - -function _draw() - foreach(entities,draw) -end - -function magnitude(vec) - return sqrt(vec.x*vec.x+vec.y*vec.y) -end - -function normalize_scalar(val) - if val!=0 then - return val/abs(val) - end - return val -end - -function vec_scale(vec,s) - return {x=vec.x*s,y=vec.y*s} -end - -function vec_add(a,b) - return {x=a.x+b.x,y=a.y+b.y} -end - -function normalize(vec) - m=magnitude(vec) - if m!=0 then - return {x=vec.x/m,y=vec.y/m} - end - return vec -end __gfx__ 00000000000000000000000000000000000000000000000000000c0000000c0000000c0000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005555555 diff --git a/entity.lua b/entity.lua new file mode 100644 index 0000000..dfad84f --- /dev/null +++ b/entity.lua @@ -0,0 +1,208 @@ +Entities = { + Player = 0, + Sword = 1, + Enemy = 2 +} +States = { + Walk = "walk", + Idle = "idle" +} + + +_id = 0 +function _next_id() + i = _id + _id += 1 + return i +end + +Entity = {} +Entity.__index = Entity +function Entity:b_add_state(name, state) + if self.states == nil then self.states = {} end + self.states[name] = state + if self.state == nil then self.state = name end + return self +end +function Entity:b_state(name) + assert(self.states[name] != nil) + self.state = name + return self +end +function Entity:b_render_order(ord) + self.render_order = ord + return self +end +function Entity:b_position(vec) + self.position = vec2(vec) + return self +end +function Entity:b_sprite_position(vec) + self.sprite_position = vec2(vec) + return self +end +function Entity:b_type(entity_type) + self.entity_type = type + return self +end +function Entity:b_equipped(equipped) + self.equipped = equipped + return self +end +function Entity:b_collidable() + self.collision = true + return self +end + +function Entity:transition_to(state_name) + if self:transition(self.state, state_name) then + self.state = state_name + self.state_stopwatch = 0 + end + return self +end + +function Entity:transition(from, to) + if from == States.Walk and to == States.Idle or to == States.Idle and from == States.Walk then + return true + end + return false +end + +function Entity:update(dt) + if self.state_stopwatch == nil then + self.state_stopwatch = 0 + end + self.state_stopwatch += dt + self:integrate(dt) + + self:update_sprite_position() + self:update_line_of_sight() + + if (self.velocity.x == 0 and self.velocity.x == self.velocity.y) then + self:transition_to(States.Idle) + else + self:transition_to(States.Walk) + end + + if (self.equipped != nil) then + parent = self + foreach(self.equipped, function (e) e:equipped_from(parent) end) + end +end + +function Entity:update_line_of_sight() + nv = self.velocity:apply(normalize_scalar) + if nv.y != 0 then self.line_of_sight = vec2(0, nv.y) + elseif nv.x != 0 then self.line_of_sight = vec2(nv.x, 0) + else self.line_of_sight = vec2(0, 0) end +end + +-- prevent cobblestoning during non-manhattan movement by "lagging" the sprite +-- behind the actual physical position +function Entity:update_sprite_position() + if self.sprite_position == nil then return end + + -- step in only x or y. trivial + if self.velocity == nil or self.velocity.y == 0 or self.velocity.x == 0 then + self.sprite_position.x = self.position.x + self.sprite_position.y = self.position.y + return + end + + dpos = self.position - self.sprite_position + dx, dy = dpos.x, dpos.y + adx, ady = abs(dx), abs(dy) + + yslow = abs(self.velocity.y) < abs(self.velocity.x) + xslow = abs(self.velocity.x) <= abs(self.velocity.y) + + pushx, pushy = false, false + jerkdelta = 1 + if adx >= jerkdelta and xslow and ady >= 1 then + pushx, pushy = true, true + elseif ady >= jerkdelta and yslow and adx >= 1 then + pushx, pushy = true, true + elseif xslow and ady >= 1 then + pushy = true + elseif yslow and adx >= 1 then + pushx = true + end + + if pushx then + func = flr + if dx < 0 then func = ceil end + self.sprite_position.x += func(dx) + end + if pushy then + func = flr + if dy < 0 then func = ceil end + self.sprite_position.y += func(dy) + end + + return self +end + +function Entity:integrate(dt) + if (self.velocity != nil) then + self.position = self.position + (self.velocity * dt) + end +end + +_equip_distance = 6 +function Entity:equipped_from(parent) + self.line_of_sight = vec2(parent.line_of_sight) + + offset = (parent.line_of_sight * _equip_distance) + + self.position = parent.position + offset + self.sprite_position = parent.sprite_position + offset +end + +function _get_frame(anim, t) + len = #anim.seq + frames_passed = (t / anim.dt) + idx = flr(1 + (frames_passed % len)) + return anim.seq[idx] +end + +_animation_keys = {"neg", "", "pos"} +function _get_animation_key(line_of_sight) + n_line_of_sight = line_of_sight:apply(normalize_scalar) + if n_line_of_sight.y ~= 0 then + return _animation_keys[n_line_of_sight.y + 2] .. "_y" + end + return _animation_keys[n_line_of_sight.x + 2] .. "_x" +end + +function Entity:render() + pos = self.sprite_position or self.position + + animation = self.states[self.state].animation + if (animation == nil) then return end + key = _get_animation_key(self.line_of_sight) + animation = animation[key] or animation + assert(animation[key]) + + frame = _get_frame(animation.sequence, self.state_t) + reflection = animation.reflection or vec2(false, false) + spr( + frame, + entity.sprite_position.x, entity.sprite_position.y, + 1, 1, + reflection.x, reflection.y + ) +end + +function entity() + id = _next_id() + World.add(setmetatable( + { + id = id, + position = vec2(0,0), + velocity = vec2(0,0), + collision = false + }, Entity + )) + return World[id] +end diff --git a/math.lua b/math.lua new file mode 100644 index 0000000..f745dc8 --- /dev/null +++ b/math.lua @@ -0,0 +1,36 @@ +Vec2 = {} +Vec2.__index = Vec2 +Vec2.__add = function(a, b) return vec2(a.x + b.x, a.y + b.y) end +Vec2.__unm = function(a) return vec2(-a.x, -a.y) end +Vec2.__sub = function(a, b) return a + (-b) end +Vec2.__mul = function(a, b) + if (type(b) == "number") then return vec2(a.x * b, a.y * b) end + return a.x * b.x + a.y * b.y +end +Vec2.__div = function(a, b) return vec2(a.x / b.x, a.y / b.y) end +Vec2.__tostring = function(a) return "(" .. a.x .. "," .. a.y .. ")" end + +function Vec2:magnitude() + return sqrt(self.x * self.x + self.y * self.y) +end + +function Vec2:normal() + local m = self:magnitude() + if m == 0 then return vec2(self.x, self.y) end + return vec2(self.x / m, self.y / m) +end + +function Vec2:apply(f) + return vec2(f(self.x), f(self.y)) +end + +function vec2(x, y) + if type(x) == "table" then return vec2(x.x, x.y) end + return setmetatable({ x = x, y = y }, Vec2) +end + +function normalize_scalar(x) + if x == 0 then return 0 end + if x < 0 then return -1 end + return 1 +end
\ No newline at end of file diff --git a/util.lua b/util.lua new file mode 100644 index 0000000..bb9b2e8 --- /dev/null +++ b/util.lua @@ -0,0 +1,52 @@ +-- https://pico-8.fandom.com/wiki/Qsort +function qsort(a,c,l,r) + c,l,r=c or function(a,b) return a<b end,l or 1,r or #a + if l<r then + if c(a[r],a[l]) then + a[l],a[r]=a[r],a[l] + end + local lp,k,rp,p,q=l+1,l+1,r-1,a[l],a[r] + while k<=rp do + local swaplp=c(a[k],p) + -- "if a or b then else" + -- saves a token versus + -- "if not (a or b) then" + if swaplp or c(a[k],q) then + else + while c(q,a[rp]) and k<rp do + rp-=1 + end + a[k],a[rp],swaplp=a[rp],a[k],c(a[rp],p) + rp-=1 + end + if swaplp then + a[k],a[lp]=a[lp],a[k] + lp+=1 + end + k+=1 + end + lp-=1 + rp+=1 + -- sometimes lp==rp, so + -- these two lines *must* + -- occur in sequence; + -- don't combine them to + -- save a token! + a[l],a[lp]=a[lp],a[l] + a[r],a[rp]=a[rp],a[r] + qsort(a,c,l,lp-1 ) + qsort(a,c, lp+1,rp-1 ) + qsort(a,c, rp+1,r) + end + return a +end + +function filter(a, pred) + filtered = {} + for k,v in ipairs(a) do + if pred(v) then + filtered[k] = v + end + end + return filtered +end
\ No newline at end of file diff --git a/world.lua b/world.lua new file mode 100644 index 0000000..fd793a4 --- /dev/null +++ b/world.lua @@ -0,0 +1,5 @@ +World = {} +function World.add(entity) + World[entity.id] = entity + return World +end
\ No newline at end of file |
