summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--animation.lua8
-rw-r--r--collisions.lua30
-rw-r--r--dyl.lua69
-rw-r--r--dyl.p8306
-rw-r--r--entity.lua208
-rw-r--r--math.lua36
-rw-r--r--util.lua52
-rw-r--r--world.lua5
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
diff --git a/dyl.lua b/dyl.lua
new file mode 100644
index 0000000..f7dce11
--- /dev/null
+++ b/dyl.lua
@@ -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
diff --git a/dyl.p8 b/dyl.p8
index 95f2e59..eb1464a 100644
--- a/dyl.p8
+++ b/dyl.p8
@@ -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