language: JavaScript
6.8 KB / 280 lines / 243 loc
// 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 }}}