jmou/babka-is-you
git clone https://github.com/jmou/babka-is-you.git
git clone git@github.com:jmou/babka-is-you.git
Loading…
(top)/nodes.js
language: JavaScript
6.8 KB / 280 lines / 243 loc
History
View raw
// CURTAIN {{{
// CURTAIN }}}
// ACT1SCENE1 {{{

class ECS {
  constructor() {
    this.maxEntity = 0;
    this.components = {};
  }

  addEntity(...components) {
    const entity = ++this.maxEntity;
    for (const c of components) {
      this.attach(entity, c);
    }
    return entity;
  }

  attach(entity, component) {
    (this.components[component.constructor.name] ||= {})[entity] = component;
  }
}

class ComponentBounds {
  constructor(x, y, width, height) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
  }
}

class ComponentWhiteBox {}

class SystemRender {
  constructor(world, canvas) {
    this.world = world;
    this.ctx = canvas.getContext('2d');
  }

  render() {
    const { width, height } = this.ctx.canvas;
    this.ctx.clearRect(0, 0, width, height);
    for (const entity in this.world.components.ComponentWhiteBox || []) {
      const { x, y, width, height } = this.world.components.ComponentBounds[entity];
      this.ctx.fillStyle = 'white';
      this.ctx.fillRect(x, y, width, height);
    }
  }
}

let world;

window.onload = function() {
  world = new ECS();
  const canvas = document.getElementById('game');
  const render = new SystemRender(world, canvas);
  world.addEntity(
    new ComponentBounds(100, 100, 80, 50),
    new ComponentWhiteBox(),
  );
  render.render();
};

// ACT1SCENE1 }}}
// ACT1SCENE2 {{{

class ComponentDrag {}

class SystemInput {
  constructor(world, canvas) {
    this.world = world;
    this.canvas = canvas;
    this.transition('default');
    canvas.addEventListener('mousedown', this.mousedown.bind(this));
    canvas.addEventListener('mousemove', this.mousemove.bind(this));
    canvas.addEventListener('mouseup', this.mouseup.bind(this));
  }

  transition(state, extra) {
    this.state = state;
    this.extra = extra;
  }

  localcoord(event) {
    const rect = event.target.getBoundingClientRect();
    // Does not account for padding;
    const x = event.x - rect.left;
    const y = event.y - rect.top;
    const scale = this.canvas.getBoundingClientRect().height / this.canvas.height;
    return { x: Math.floor(x / scale), y: Math.floor(y / scale) };
  }

  mousedown(event) {
    const { x, y } = this.localcoord(event);
    if (this.state == 'default') {
      if (!event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey &&
          event.button == 0) {  // unmodified primary
        const entity = this.hit(x, y);
        this.transition('mousedown', { x, y, entity });
      }
    }
  }

  mousemove(event) {
    const { x, y } = this.localcoord(event);
    switch (this.state) {
      case 'mousedown': {  // drag
        this.transition('drag', {
          lastX: this.extra.x,
          lastY: this.extra.y,
          entity: this.extra.entity,
        });
      }  // fallthrough

      case 'drag': {
        if (!event.buttons) {  // stale mouse drag (outside of canvas)
          this.mouseup(event);
          break;
        }
        const dx = x - this.extra.lastX;
        const dy = y - this.extra.lastY;
        this.extra.lastX = x;
        this.extra.lastY = y;
        this.drag(dx, dy, this.extra.entity);
        break;
      }
    }
  }

  mouseup(event) {
    const { x, y } = this.localcoord(event);
    switch (this.state) {
      case 'mousedown': {
        this.click(x, y, this.extra.entity);
        break;
      }
      case 'drag': {
        this.transition('default');
        break;
      }
    }
  }

  click(x, y, entity) {
    this.world.addEntity(
      new ComponentBounds(x, y, 80, 50),
      new ComponentWhiteBox(),
      new ComponentDrag(),
    );
  }

  drag(dx, dy, entity) {
    if (entity in this.world.components.ComponentDrag) {
      const bounds = this.world.components.ComponentBounds[entity];
      bounds.x += dx;
      bounds.y += dy;
    }
  }

  hit(x, y) {
    for (const entity in this.world.components.ComponentDrag || []) {
      const bounds = this.world.components.ComponentBounds[entity];
      if (bounds && bounds.x < x && x < bounds.x + bounds.width &&
          bounds.y < y && y < bounds.y + bounds.height) {
        return entity;
      }
    }
  }
}

window.onload = function() {
  world = new ECS();
  const canvas = document.getElementById('game');
  const render = new SystemRender(world, canvas);

  new SystemInput(world, canvas);
  function tick() {
    render.render();
    requestAnimationFrame(tick);
  }
  tick();
};

// ACT1SCENE2 }}}
// INTERMISSION {{{

class World extends ECS {
  constructor() {
    super();
    this.listeners = {};
  }

  listen(type, listener) {
    (this.listeners[type] ||= []).push(listener);
  }

  signal(type, data) {
    for (const listener of this.listeners[type] || []) {
      listener(data);
    }
  }

  detach(entity, componentClass) {
    delete this.components[componentClass.name]?.[entity];
  }

  destroy(entity) {
    for (const c of Object.values(this.components)) {
      delete c[entity];
    }
  }

  queryComponent(componentClass) {
    const components = this.components[componentClass.name] || {};
    return Object.entries(components);
  }

  cast(entity, componentClass) {
    return this.components[componentClass.name]?.[entity];
  }
}

// INTERMISSION }}}
// POSTMATTER {{{

function rot13(s) {
  let r = [];
  for (const c of s) {
    r.push(c < 'a' ? c : String.fromCharCode((c.charCodeAt(0) - 97 + 13) % 26 + 97));
  }
  return r.join('');
}

function yourImage(src) {
  const img = document.createElement('img');
  img.src = src;
  img.addEventListener('load', () => {
    for (const entity in world.components.ComponentYou) {
      world.attach(entity, new ComponentSprite(img));
    }
    world.signal('recomposite');
  });
}

function babka() {
  for (const [entity, subject] of world.queryComponent(ComponentSubject)) {
    let standin;
    switch (subject.noun) {
      case 'baba': standin = 'babka'; break;
      case 'rock': standin = 'scone'; break;
      case 'wall': standin = 'tin'; break;
      case 'water': standin = 'cup'; break;
      case 'flag': standin = 'flour'; break;
    }
    const img = document.createElement('img');
    img.src = standin + '.png';
    img.addEventListener('load', () => {
      world.attach(entity, new ComponentSprite(img));
      world.signal('recomposite');
    });
  }
}

document.addEventListener('keydown', e => {
  if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
    return;
  switch (e.key) {
    case 'r': { loadLevel(false); break; }
    case 'w': { loadLevel(true); break; }
    case 's': { const x = alert; alert = console.log; loadLevel(true); alert = x; break; }
    case 'h': { document.body.classList.toggle('hit'); break; }
    case 'l': { document.body.classList.toggle('rules'); break; }
    case 'b': { yourImage('babka.png'); break; }
    case 'n': { loadLevel(false); babka(); break; }
  }
});

// set fdm=marker foldtext=v:folddashes.substitute(getline(v:foldstart),'//\ ','','')
// POSTMATTER }}}