Entities = { Player = 0, Sword = 1, Enemy = 2, Bow = 3, Particle = 4, Wife = 5 } States = { Walk = "walk", Idle = "idle", Active = "active", Equipped = "equipped", Slashing = "slashing", Drawing = "drawing", Drawn = "drawn" } Entity = {} Entity.__index = Entity function Entity:is_in_iframe() return self.knockback ~= nil and self.knockback.time >= 0 end function Entity:transition_to(state_name) assert(self.states[state_name]) if self.state ~= state_name then self.state = state_name self.state_stopwatch = 0 end return self end function Entity:kill() self.life_time = -1 end function Entity:update(dt) self:integrate(dt) self:update_line_of_sight() self:update_sprite_position() if (self.equipped != nil) then parent = self for id, entityDist in pairs(self.equipped) do entityDist.entity:equipped_from(parent, entityDist.distance) end end assert(self.transition_state) self:transition_state() if self.life_time ~= nil then self.life_time -= dt end if self.health ~= nil and self.health <= 0 then self:kill() 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) end end -- prevent cobblestoning during non-manhattan movement by "lagging" the sprite -- behind the actual physical position -- this part was painful. 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:take_damage(direction, damage_spec) if self.health == nil or self:is_in_iframe() then return end self.health -= damage_spec.amount if damage_spec.knockback ~= nil then self.knockback = { velocity = direction * damage_spec.knockback.magnitude, remaining_time = damage_spec.knockback.time, time = damage_spec.knockback.time } end end function Entity:integrate(dt) self.state_stopwatch += dt if self.knockback ~= nil then if self.knockback.remaining_time <= 0 then self.knockback = nil self.velocity = vec2(0, 0) else self.velocity = self.knockback.velocity * (self.knockback.remaining_time / self.knockback.time) self.knockback.remaining_time -= dt end end if self.velocity ~= nil and self.position ~= nil then self.position = self.position + (self.velocity * dt) end end _equipped_item_distance = 6 function Entity:equip(that, dist) dist = dist or _equipped_item_distance self.equipped[that.id] = { entity = that, distance = dist } that:transition_to(States.Equipped) end function Entity:equipped_from(parent, dist) self.line_of_sight = vec2(parent.line_of_sight) offset = (parent.line_of_sight * dist) self.position = parent.position + offset self.sprite_position = parent.sprite_position + offset end -- -1 0 1 _animation_keys = { "neg", "pos", "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(screen_position) animation = self.states[self.state].animation if (animation == nil) then return end if (self.line_of_sight ~= nil) then key = _get_animation_key(self.line_of_sight) animation = animation[key] or animation end assert(animation) frames_passed = flr(self.state_stopwatch / animation.dt) frame = animation.sequence[1 + (frames_passed % #animation.sequence)] assert(frame) reflection = animation.reflect or vec2(false, false) spr( frame, screen_position.x, screen_position.y, 1, 1, reflection.x, reflection.y ) end EntityBuilder = {} EntityBuilder.__index = EntityBuilder function EntityBuilder:new(world) return setmetatable( { build_context = { world = world }, velocity = vec2(0, 0), collision = false }, EntityBuilder ) end function EntityBuilder:b_add_state(name, state) if self.states == nil then self.states = {} end self.states[name] = state if self.state == nil then self:b_state(name) end return self end function EntityBuilder:b_state(name) assert(self.states[name] != nil) self.state = name self.state_stopwatch = 0 return self end function EntityBuilder:b_health(health) self.health = health return self end function EntityBuilder:b_damage(damage_spec) self.damage = damage_spec return self end function EntityBuilder:b_render_order(ord) self.render_order = ord return self end function EntityBuilder:b_position(vec) self.position = vec2(vec) return self end function EntityBuilder:b_sprite_position(vec) self.sprite_position = vec2(vec) return self end function EntityBuilder:b_type(entity_type) self.entity_type = entity_type return self end function EntityBuilder:b_equipped(equipped) self.equipped = equipped return self end function EntityBuilder:b_collidable() self.collision = true return self end function EntityBuilder:b_live_for(t) self.life_time = t return self end function EntityBuilder:b_line_of_sight(vec) self.line_of_sight = vec2(vec) return self end function EntityBuilder:build() self.equipped = {} return self.build_context.world.add(setmetatable(self, Entity)) end