From 90d98efdaf7147bf7c704af722c96b95d101b7e8 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Sun, 5 Mar 2023 21:30:20 +0000 Subject: feat: enemies chase entities, get stunned and fly off the screen This commit introduces the following features: - enemies have a simple target system, chasing a `Pos`. This can be a moving target (e.g., giving them the player's `Pos` reference) or still. - there is a simple status system: entities hit may be "stunned" and, when dead, are "phantom" (i.e., they do not collide with other entities). - when killed, entities fly off the screen. --- raccoon.lua | 298 +++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 203 insertions(+), 95 deletions(-) diff --git a/raccoon.lua b/raccoon.lua index df57514..18939a1 100644 --- a/raccoon.lua +++ b/raccoon.lua @@ -242,16 +242,18 @@ function Pos:init(x,y) self.y = y or 0 return self end -function Pos:on_ground() - -- Check center tile - local center = mget((self.x + 8/2)//8, (self.y + 8)//8) - if fget(center, 0) then return center end - -- Check left tile - local left = mget((self.x + 1)//8, (self.y + 8)//8) - if fget(left, 0) then return left end - -- Check right tile - local right = mget((self.x + 8 - 1)//8, (self.y + 8)//8) - if fget(right, 0) then return right end +function Pos:on_ground(phantom) + if not phantom then + -- Check center tile + local center = mget((self.x + 8/2)//8, (self.y + 8)//8) + if fget(center, 0) then return center end + -- Check left tile + local left = mget((self.x + 1)//8, (self.y + 8)//8) + if fget(left, 0) then return left end + -- Check right tile + local right = mget((self.x + 8 - 1)//8, (self.y + 8)//8) + if fget(right, 0) then return right end + end -- In mid air return nil end @@ -259,6 +261,17 @@ function Pos:to_screen(lpos) return self.x - lpos.x, self.y - lpos.y end +Health = Component("Health") +function Health:init(max) + self.cur = max + self.max = max + return self +end +function Health:damage(d) + self.cur = math.max(0,self.cur-d) + return self.cur +end + Movement = Component("Movement") function Movement:init(accel, max) -- direction facing @@ -274,6 +287,12 @@ function Movement:is_moving() return self.cur.x ~= 0 or self.cur.y ~= 0 end +MovementAI = Component("MovementAI") +function MovementAI:init() + -- TODO + return self +end + Pixel = Component("Pixel") function Pixel:init(c) self.color = c @@ -317,12 +336,26 @@ function Entities:init(list) return self end -local Action = Component() -function Action:init(a,b) - self.a = a - self.b = b +Status = Component("Status") +Status.keys = { "phantom", "stunned" } +function Status:init() + for _,s in ipairs(Status.keys) do self[s] = nil end return self end +function Status:tick() + for _,s in ipairs(Status.keys) do + if self[s] then + self[s] = self[s] - 1 + if self[s] < 0 then + self[s] = nil + end + end + end +end +for _,s in ipairs(Status.keys) do + Status["set_"..s] = function(self,d) self[s] = 60 * (d or 1); return self end + Status["is_"..s] = function(self) return self[s] end +end CircleEffect = Component("CircleEffect") function CircleEffect:init(r2, r1, time, color) @@ -362,13 +395,17 @@ function Particles:spawn(pos, entities) end end -function ForceField:init(force,range,instant) ForceField = Component("ForceField") +function ForceField:init(force,range,is_target,instant) self.force = force or 1 self.range = range or 1 + self.is_target = is_target or function(_) return true end self.instant = instant return self end +function ForceField.by_id(id) + return function(e) return e.id ~= id end +end Metadata = Component("Metadata") function Metadata:init(name) @@ -376,6 +413,30 @@ function Metadata:init(name) return self end +Action = Component("Action") +function Action:init(a,b) + self.a = a + self.b = b + return self +end +function Action.forcefield(entity,game) + local pos = entity:get_pos() + table.insert( + game.entities, + Entity():with({ + Pos(pos.x,pos.y), + ForceField(2,10,ForceField.by_id(entity.id),true), + CircleEffect(20)})) +end + +Target = Component("Target") +function Target:init(pos, prox, sticky) + self.pos = pos + self.prox = prox or 5 + self.sticky = sticky + return self +end + local ForceFieldSystem = System(Pos,ForceField) function ForceFieldSystem:exec(entity,enemies) local pos = entity:get_pos() @@ -385,14 +446,31 @@ function ForceFieldSystem:exec(entity,enemies) local pos1 = e:get_pos() local dist,vec = util.distance(pos,pos1) if dist < ff.range then - if e.has_component[Metadata] then - local meta = e.get_component[Metadata] - trace("Hit " .. meta.name .. " at position " .. pos1.x .. " " .. pos1.y) - else - trace("Hit entity at position " .. pos1.x .. " " .. pos1.y) + local death_blow = 1 + if e:has_status() then + e:get_status():set_stunned(1) + end + if e:has_health() then + local h = e:get_health():damage(2) + if h == 0 then + if e:has_status() then + e:get_status():set_stunned(3) + e:get_status():set_phantom(3) + end + death_blow = 2 + math.random(2) + end + end + if e:has_movement() then + local mov = e:get_movement() + mov.cur.x = mov.cur.x + util.signum(vec[1]) * death_blow * ff.force + mov.cur.y = mov.cur.y - death_blow * ff.force end - mov.cur.x = mov.cur.x + util.signum(vec[1]) * ff.force - mov.cur.y = mov.cur.y - ff.force + -- if e:has_metadata() then + -- local meta = e:get_metadata() + -- trace("Hit " .. meta.name .. " at position " .. pos1.x .. " " .. pos1.y) + -- else + -- trace("Hit entity at position " .. pos1.x .. " " .. pos1.y) + -- end end end end @@ -418,72 +496,68 @@ end local MovementSystem = System(Pos, Movement) function MovementSystem:exec(entity) - local pos = entity.get_component[Pos] - local move = entity.get_component[Movement] - if entity.has_component[Input] then - local input = entity.get_component[Input] - local left, right = input.raw[3], input.raw[4] - -- local debug - -- if left then debug = " left, " else debug = " ____, " end - -- if right then debug = debug .. "right" else debug = debug .. "_____" end - if left and not right then - if move.dir then - move.cur.x = 0 - end - move.dir = false - move.cur.x = math.max(-move.max.x, move.cur.x - move.accel.x) - elseif right and not left then - if not move.dir then - move.cur.x = 0 + local rm, components = false, {} + + local pos = entity:get_pos() + local move = entity:get_movement() + local status = Status() + if entity:has_status() then + status = entity:get_status() + end + -- Horizontal movement + local left, right = false, false + if not status:is_stunned() then + if entity:has_input() then + local input = entity:get_input() + left, right = input.raw[3], input.raw[4] + elseif entity:has_movementai() then + if entity:has_target() then + local target = entity:get_target() + local delta = target.pos.x - pos.x + if -target.prox < delta and delta < target.prox and not target.sticky then + table.insert(components, target) + else + left, right = delta < 0, delta > 0 + end end - move.dir = true - move.cur.x = math.min(move.max.x, move.cur.x + move.accel.x) - else - -- TODO end - -- horizontal movement - --move:update_speedx(input.raw[3], input.raw[4]) - -- if input.raw[3] then - -- if speed.right then - -- speed.cur.x = 0 - -- end - -- speed.right = false - -- speed.cur.x = math.max(-speed.max, speed.cur.x - speed.accel) - -- elseif input.raw[4] then - -- if not speed.right then - -- speed.cur.x = 0 - -- end - -- speed.right = true - -- speed.cur.x = math.min(speed.max, speed.cur.x + speed.accel) - -- end - if entity.has_component[Jump] then - local jump = entity.get_component[Jump] - if jump.jumping then - move.cur.y = move.cur.y + .2 - if pos:on_ground() then - jump.jumping = false - end --- if jump.cur < jump.pwr then --- jump.cur = jump.cur + 1 --- speed.cur.y = - (5 / jump.cur) --- else --- jump.jumping = false --- end - elseif input.rawp[1] and pos:on_ground() then - move.cur.y = -4 - jump.jumping = true --- if jump.cur > 0 then --- jump.cur = jump.cur - 1 --- elseif speed.on_ground then --- end + end + if left and not right then + if move.dir then + move.cur.x = 0 + end + move.dir = false + move.cur.x = math.max(-move.max.x, move.cur.x - move.accel.x) + elseif right and not left then + if not move.dir then + move.cur.x = 0 + end + move.dir = true + move.cur.x = math.min(move.max.x, move.cur.x + move.accel.x) + end + -- Vertical movement + if entity:has_jump() and not status:is_stunned() then + local jump = entity:get_jump() + local up = false + if entity:has_input() then + local input = entity:get_input() + up = input.rawp[1] + end + if jump.jumping then + move.cur.y = move.cur.y + .2 + if pos:on_ground() then + jump.jumping = false end + elseif up and pos:on_ground() then + move.cur.y = -4 + jump.jumping = true end - else - -- AI end + -- Gravity + move.cur.y = math.min(2, move.cur.y + .2) -- Horizontal friction local friction = .1 - if pos:on_ground() then + if pos:on_ground(status:is_phantom()) then friction = .2 end if move.cur.x > 0 then @@ -491,25 +565,25 @@ function MovementSystem:exec(entity) else move.cur.x = math.min(0, move.cur.x + friction) end - -- Gravity - move.cur.y = math.min(2, move.cur.y + .2) -- -- collision check local posx = pos.x + move.cur.x local posy = pos.y + move.cur.y - -- -- movement on the X local collx = false - for y = pos.y//8,(pos.y+7)//8 do - collx = collx or fget(mget(posx//8,y),0) or fget(mget((posx+7)//8,y),0) - end - -- movement on the Y local colly = false - for x = pos.x//8,(pos.x+7)//8 do - colly = colly or fget(mget(x,posy//8),0) or fget(mget(x,(posy+7)//8),0) + if not status:is_phantom() then + -- -- movement on the X + for y = pos.y//8,(pos.y+7)//8 do + collx = collx or fget(mget(posx//8,y),0) or fget(mget((posx+7)//8,y),0) + end + -- movement on the Y + for x = pos.x//8,(pos.x+7)//8 do + colly = colly or fget(mget(x,posy//8),0) or fget(mget(x,(posy+7)//8),0) + end + -- local cx, cy + -- if collx then cx = "true" else cx = "false" end + -- if colly then cy = "true" else cy = "false" end + -- print("Collision: "..cx..", "..cy) end - -- local cx, cy - -- if collx then cx = "true" else cx = "false" end - -- if colly then cy = "true" else cy = "false" end - -- print("Collision: "..cx..", "..cy) if not collx then pos.x = posx end @@ -519,6 +593,23 @@ function MovementSystem:exec(entity) else move.cur.y = 0 end + + if rm then components = {} end + return rm or next(components), components -- next(table) used as a "not empty" check +end + +local StatusSystem = System(Status) +function StatusSystem:exec(entity) + entity:get_status():tick() +end + +local BoundarySystem = System(Pos,Health) +function BoundarySystem:exec(entity,lpos) + local health = entity:get_health() + if health.cur <= 0 then + local x, y = entity:get_pos():to_screen(lpos) + return x < 0 or y < 0 or x > util.screen.width or y > util.screen.height + end end local DrawingSystem = System(Pos) @@ -617,6 +708,16 @@ function ParticlesSystem:exec(entity, game) end end +local TargetSystem = System(Movement,MovementAI) +function TargetSystem:exec(entity) + -- TODO to be removed + if not entity:has_target() then + local x, y = math.random(util.screen.width), math.random(util.screen.height) + trace("New target: "..x..", "..y) + entity:with(Target(Pos(x,y))) + end +end + local player = Entity():with({ Metadata("Giulia"), Sprite(256), Pos(100,10), Health(100), Status(), @@ -663,13 +764,20 @@ function TIC() cls(0) InputSystem:run(game.entities) + TargetSystem:run(game.entities) + + StatusSystem:run(game.entities) + MovementSystem:run(game.entities) ActionSystem:run(game.entities, game) - EffectSystem:run(game.entities) ForceFieldSystem:run(game.entities, game.entities) + PosAnimSystem:run(game.entities) + EffectSystem:run(game.entities) ParticlesSystem:run(game.entities, game) + LevelSystem:run(game.levels, game) + BoundarySystem:run(game.entities, game.levels[game.level]:get_pos()) DrawingSystem:draw(game) -- cgit v1.2.3