app/console/entity.rb

the library's own source, console 0.2.0 · back to Console::EntityStore

# Console::Entity -- a flat, fast entity store.
#
# Entities are plain Hashes with `x`, `y`, `w`, `h`, `kind` and a unique `id`.
# Keeping them Hash-shaped matters: DragonRuby renders Hash primitives faster
# than class instances, and a hash entity can be passed straight to
# `args.outputs.sprites <<` when it has a `path`.
#
#   spawn Bullet, kind: :bullet, x: 100, y: 100,
#                     sprite: 'star', vx: 4, vy: 0
#   each_entity :bullet do |b|
#     move b, vx: b.vx, vy: b.vy
#     despawn b if off_screen?(b)
#   end
#
# `kind` is the tag used for querying, animation lookup (sprites/<kind>/<state>)
# and collision groups.
module Console
  class EntityStore
    attr_reader :entities
    attr_accessor :next_id

    def initialize
      @entities = []
      @by_id = {}
      @next_id = 1
    end

    def size
      @entities.size
    end

    # Create an entity. `klass` may be a Class (which must respond to
    # `defaults` or take a Hash) or any Symbol used purely as documentation.
    #
    # Returns the entity Hash.
    def spawn(klass = nil, attributes = {})
      kind = attributes[:kind]
      kind ||= klass if klass.is_a?(Symbol)
      e = { id: @next_id }
      @next_id += 1

      if klass.is_a?(Class)
        if klass.respond_to?(:spawn_defaults)
          e.merge! klass.spawn_defaults
        end
        if klass.respond_to?(:defaults)
          e.merge! klass.defaults
        end
      end

      e[:kind] = kind.to_sym if kind
      e.merge! attributes
      e[:kind] = kind.to_sym if kind && !e[:kind]
      e[:x] ||= 0.0
      e[:y] ||= 0.0
      e[:w] ||= 16
      e[:h] ||= 16

      @entities << e
      @by_id[e[:id]] = e
      e
    end

    def despawn(entity)
      return false unless entity
      id = entity[:id]
      @by_id.delete id
      @entities.delete entity
      true
    end

    def despawn_if(&block)
      doomed = @entities.select(&block)
      doomed.each { |e| despawn e }
      doomed.size
    end

    def [](id)
      @by_id[id]
    end

    def clear(kind = nil)
      if kind.nil?
        @entities = []
        @by_id = {}
        return
      end
      @entities.select { |e| e[:kind] == kind.to_sym }.each { |e| despawn e }
    end

    def count(kind = nil)
      return @entities.size if kind.nil?
      @entities.count { |e| e[:kind] == kind.to_sym }
    end

    def any?(kind)
      @entities.any? { |e| e[:kind] == kind.to_sym }
    end

    def all(kind = nil)
      return @entities.dup if kind.nil?
      @entities.select { |e| e[:kind] == kind.to_sym }
    end

    def each(kind = nil, &block)
      if kind.nil?
        @entities.each(&block)
      else
        @entities.each { |e| block.call e if e[:kind] == kind.to_sym }
      end
      @entities.size
    end

    def each_with_kind(kind, &block)
      each kind, &block
    end

    # --- queries ---------------------------------------------------------

    # All entities of `kind` intersecting `rect`.
    def colliding(rect, kind = nil)
      out = []
      @entities.each do |e|
        next if kind && e[:kind] != kind.to_sym
        next if e[:dead]
        next unless e.intersect_rect? rect
        out << e
      end
      out
    end

    # The first entity of `kind` intersecting `rect`.
    def colliding_one(rect, kind = nil)
      @entities.each do |e|
        next if kind && e[:kind] != kind.to_sym
        next if e[:dead]
        return e if e.intersect_rect? rect
      end
      nil
    end

    def nearest(rect, kind = nil)
      best = nil
      best_d = nil
      c = Geom.center rect
      @entities.each do |e|
        next if kind && e[:kind] != kind.to_sym
        d = Geometry.distance c, Geom.center(e)
        if best_d.nil? || d < best_d
          best = e
          best_d = d
        end
      end
      best
    end

    # --- motion ----------------------------------------------------------

    def move(entity, vx: 0, vy: 0)
      entity[:x] = entity[:x] + vx
      entity[:y] = entity[:y] + vy
      entity
    end

    def move_to(entity, x, y)
      entity[:x] = x
      entity[:y] = y
      entity
    end

    def toward(entity, target, speed)
      c = Geom.center entity
      t = Geom.center target
      a = Geometry.angle c, t
      entity[:vx] = Math.sin(a.to_radians) * speed
      entity[:vy] = Math.cos(a.to_radians) * speed
      entity
    end

    # Distance from a point to the centre of `rect`.
    def center(rect)
      Geom.center rect
    end
  end

  # Sprite-resolution + animation glue for entities.
  module EntityDraw
    extend self

    # Resolve an entity's sprite. Prefers an active animation, then
    # `entity[:sprite]`, then `entity[:path]`, then the generated placeholder.
    def sprite_path(console, entity)
      if entity[:sprite_frames] && entity[:sprite_frames].size > 0
        i = entity[:sprite_frame] || 0
        return entity[:sprite_frames][i % entity[:sprite_frames].size]
      end
      return entity[:sprite] if entity[:sprite]
      return entity[:path] if entity[:path]
      return entity[:kind] if entity[:kind]
      :solid
    end

    # Draw an entity, honouring an active animation, tint, alpha, flips and
    # rotation. Routes through Draw so the camera transform applies.
    #
    #   draw_entity entity
    #   draw_entity entity, angle: spin
    def draw_entity(console, entity, spec = {})
      prim = { x: entity[:x], y: entity[:y],
               w: entity[:w], h: entity[:h] }

      # An active animation wins; otherwise fall back to the entity's own
      # sprite, then its path, then its kind. Without the fallback an entity
      # with no matching animation folder renders as an empty box.
      frame = console.anim.primitive_props entity
      if frame
        prim = prim.merge frame
      else
        prim[:path] = sprite_path(console, entity)
      end

      %i[flip_horizontally flip_vertically angle angle_anchor_x
         angle_anchor_y source_x source_y source_w source_h z].each do |k|
        prim[k] = entity[k] if entity[k]
      end

      if entity[:tint] || entity[:alpha]
        prim = prim.merge Palette.to_hash(entity[:tint] || :white, entity[:alpha])
      end

      prim = prim.merge(spec) if spec && spec.size > 0
      console.draw.sprite prim
    end
  end
end