Browse Source

a bunch of stuff

master
mortie 6 years ago
parent
commit
80bc3e6892

+ 55
- 11
js/Entity.js View File

@@ -1,42 +1,78 @@
import Rect from './Rect.js';
import Vec2 from './Vec2.js';
import Rect from "./Rect.js";

export class Trait {
constructor(entity, name) {
constructor(entity, name, deps = []) {
this.name = name;
this.entity = entity;
this.enabled = true;
this.deps = deps;
}

_init() {
let missing = [];
this.deps.forEach(d => {
if (!this.entity.has(d))
missing.push(d);
});

if (missing.length > 0)
throw new Error("Missing dependency traits: "+missing.join(", "));

this.init();
}

init() {}
update(dt) {}
postUpdate(dt) {}
}

export default class Entity {
constructor(level) {
this.level = level;
this.bounds = new Rect();
this.velocity = new Vec2();
this.t = {};

this.t = {
physics: null,
keyboardController: null,
collider: null,
platform: null,
};

this.traits = [];
this.updateTraits = [];
this.postUpdateTraits = [];
}

init() {
_init() {
for (let trait of this.traits) {
trait.init();
trait._init();
}

this.init();
}

update(dt) {
for (let trait of this.traits) {
_update(dt) {
for (let trait of this.updateTraits) {
if (trait.enabled)
trait.update(dt);
}

this.bounds.pos.x += this.velocity.x * dt;
this.bounds.pos.y += this.velocity.y * dt;
this.update(dt);
}

_postUpdate(dt) {
for (let trait of this.postUpdateTraits) {
if (trait.enabled)
trait.postUpdate(dt);
}

this.postUpdate(dt);
}

init() {}
update(dt) {}
postUpdate(dt) {}

draw(ctx) {}

has(name) {
@@ -45,7 +81,15 @@ export default class Entity {
}

addTrait(t) {
if (this.t[t.name] === undefined)
throw new Error("Invalid trait:", t);

this.t[t.name] = t;
this.traits.push(t);

if (t.update !== Trait.prototype.update)
this.updateTraits.push(t);
if (t.postUpdate !== Trait.prototype.postUpdate)
this.postUpdateTraits.push(t);
}
}

+ 45
- 31
js/Level.js View File

@@ -1,35 +1,64 @@
import Player from './entities/Player.js';
import structures from './structures.js';
import structures from "./structures.js";

export default class Level {
constructor(canvas) {
this.step = 1 / 120;
this.stepLimit = 2;

this.canvas = canvas;
this.ctx = canvas.getContext("2d");
this.lastTime = null;
this.raf = null;
this.timeAcc = 0;
this.entities = [];
this.colliders = [];

this.evts = [];
this.structures = [];

this.structure = structures.floor(2, 2, 1);
this.evts = [];
}

spawn(ent, x, y) {
spawnEntity(ent, x, y) {
ent.bounds.pos.set(x, y);

this.entities.push(ent);
ent.init();
if (ent.has("collider"))
this.colliders.push(ent);

ent._init();
}

spawnStructure(structure, x, y) {
structure.bounds.pos.set(x, y);
this.structures.push(structure);
structure.init();
}

physics(dt) {
this.entities.forEach(ent =>
ent.update(dt));
this.entities.forEach(ent => {
ent.bounds.pos.x += ent.velocity.x * dt;
ent.bounds.pos.y += ent.velocity.y * dt;
ent._update(dt));
this.entities.forEach(ent =>
ent._postUpdate(dt));

// Collide with structures
this.structures.forEach(s => {
if (!s.attrs.collides)
return;

this.colliders.forEach(ent => {
let bounds = s.collidesWith(ent.bounds);
if (!bounds)
return;

ent.t.collider.collideStructure(bounds);
});
});

// Collide with entities
this.colliders.forEach(ent => {
this.colliders.forEach(ent2 => {
if (ent === ent2 || !ent.bounds.intersects(ent2.bounds))
return;

ent.t.collider.collideEntity(ent2);
});
});
}

@@ -39,29 +68,14 @@ export default class Level {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

this.entities.forEach(ent => ent.draw(this.ctx));
this.structure.draw(this.ctx);
this.structures.forEach(struct => struct.draw(this.ctx));
}

update(time) {
if (this.lastTime != null) {
let dt = (time - this.lastTime) / 1000;

this.timeAcc += dt;
if (this.timeAcc > this.stepLimit) {
console.warn(
"Attempt to simulate "+this.timeAcc.toFixed(2)+" "+
"seconds, which is over the limit ("+this.stepLimit+"s). "+
"Resetting accumulator.");
this.timeAcc = dt;
}

let nticks = 0;
while (this.timeAcc >= this.step) {
nticks += 1;
this.physics(this.step);
this.timeAcc -= this.step;
}

this.physics(dt);
this.draw();
}


+ 81
- 9
js/Rect.js View File

@@ -1,4 +1,4 @@
import Vec2 from './Vec2.js';
import Vec2 from "./Vec2.js";

export default class Rect {
constructor(pos = new Vec2(), size = new Vec2()) {
@@ -7,33 +7,91 @@ export default class Rect {
}

draw(ctx) {
ctx.moveTo(this.left, this.top);
ctx.lineTo(this.right, this.top);
ctx.lineTo(this.right, this.bottom);
ctx.lineTo(this.left, this.bottom);
ctx.moveTo(this.pixelLeft, this.pixelTop);
ctx.lineTo(this.pixelRight, this.pixelTop);
ctx.lineTo(this.pixelRight, this.pixelBottom);
ctx.lineTo(this.pixelLeft, this.pixelBottom);
ctx.closePath();
ctx.stroke();
}

intersects(other) {
return (
(this.left <= other.right && this.right >= other.left) &&
(this.top <= other.bottom && this.bottom >= other.top));
}

contains(other) {
return (
(this.left <= other.left && this.right >= other.right) &&
(this.top <= other.top && this.bottom >= other.bottom));
}

get top() {
return this.pos.y;
}
set top(n) {
this.pos.y = n;
}
get pixelTop() {
return this.pos.pixelY;
}
set pixelTop(y) {
this.pos.pixelY = y;
}
resizeTopTo(y) {
let diff = y - this.top;
this.top = y;
this.size.y += diff;
}
pixelResizeTopTo(y) {
let diff = y - this.pixelTop;
this.pixelTop = y;
this.size.pixelY += diff;
}

get bottom() {
return this.pos.y + this.size.y;
}
set bottom(n) {
this.pos.y = n - this.size.y;
set bottom(y) {
this.pos.y = y - this.size.y;
}
get pixelBottom() {
return this.pos.pixelY + this.size.pixelY;
}
set pixelBottom(y) {
this.pos.pixelY = y + this.size.pixelY;
}
resizeBottomTo(y) {
let diff = y - this.bottom;
this.size.y += diff;
}
pixelResizeBottomTo(y) {
let diff = y - this.pixelBottom;
this.size.pixelY += diff;
}

get left() {
return this.pos.x;
}
set left(n) {
this.pos.x = n;
set left(x) {
this.pos.x = x;
}
get pixelLeft() {
return this.pos.pixelX;
}
set pixelLeft(x) {
this.pos.pixelX = x;
}
resizeLeftTo(x) {
let diff = x - this.left;
this.left = x;
this.size.x += diff;
}
pixelResizeLeftTo(x) {
let diff = x - this.pixelLeft;
this.pixelLeft = x;
this.size.pixelX += diff;
}

get right() {
@@ -42,4 +100,18 @@ export default class Rect {
set right(n) {
this.pos.x = n - this.size.x;
}
get pixelRight() {
return this.pos.pixelX + this.size.pixelX;
}
set pixelRight(x) {
this.pos.pixelX = x + this.size.pixelX;
}
resizeRightTo(x) {
let diff = x - this.right;
this.size.x += diff;
}
pixelResizeRightTo(x) {
let diff = x - this.pixelRight;
this.size.pixelX += diff;
}
}

+ 1
- 1
js/SpriteSheet.js View File

@@ -1,5 +1,5 @@
export default class SpriteSheet {
constructor(url, tilew = 1, tileh = 1, scale = 1) {
constructor(url, tilew, tileh, scale = 1) {
this.url = url;
this.tilew = tilew;
this.tileh = tileh;

+ 137
- 29
js/Structure.js View File

@@ -1,52 +1,160 @@
import assets from './assets.js';
import Rect from './Rect.js';
import Vec2 from './Vec2.js';
import assets from "./assets.js";
import Rect from "./Rect.js";
import Vec2 from "./Vec2.js";

function findBounds(ctx, arr, bounds) {
function findBounds(arr, bounds) {
arr.forEach(e => {
if (e instanceof Array) {
drawArr(ctx, e);
} else {
let right = (e.x + 1) * assets.tiles.tileWidth;
let bottom = (e.y + 1) * assets.tiles.tileHeight;

if (bounds.size.x < right)
bounds.size.x = right;
if (bounds.size.y < bottom)
bounds.size.y = bottom;
}
let right = (e.x + 1) * assets.tiles.tileWidth;
let bottom = (e.y + 1) * assets.tiles.tileHeight;

if (bounds.size.x < right)
bounds.size.x = right;
if (bounds.size.y < bottom)
bounds.size.y = bottom;
});
}

function drawArr(ctx, arr) {
function rightOrBelow(r1, r2) {
// r2 right of r1
if (
(r1.top === r2.top && r1.bottom == r2.bottom) &&
(r1.right + 1 >= r2.left && r1.left <= r2.left))
return true;

// r2 below r1
if (
(r1.right === r2.right && r1.left == r2.left) &&
(r1.bottom + 1 >= r2.top && r1.top <= r2.top))
return true;

return false;
}

function continuous(r1, r2) {
return rightOrBelow(r1, r2) || rightOrBelow(r2, r1);
}

function findGeometry(bounds, arr, geometry) {
let allBounds = [];
arr.forEach(e => {
if (e instanceof Array) {
drawArr(ctx, e);
} else {
assets.tiles.drawTile(ctx, e.tile, e.x, e.y);
let eBounds = new Rect(bounds.pos.clone());

eBounds.pos.pixelX += e.x * assets.tiles.tileWidth;
eBounds.pos.pixelY += e.y * assets.tiles.tileWidth;
eBounds.size.pixelX = assets.tiles.tileWidth;
eBounds.size.pixelY = assets.tiles.tileWidth;

allBounds.push(eBounds);
});

function runPass() {
if (allBounds.lenght === 1) {
geometry.push(allBounds[0]);
allBounds.splice(0, 1);
return;
}

// Find continuous bounds
let changed = false;
geometry.forEach((g, gi) => {
allBounds.forEach((b, bi) => {
if (continuous(g, b)) {
if (b.left < g.left)
g.pixelResizeLeftTo(b.pixelLeft);
if (b.right > g.right)
g.pixelResizeRightTo(b.pixelRight);
if (b.top < g.top)
g.pixelResizeTopTo(b.pixelTop);
if (b.bottom > g.bottom)
g.pixelResizeBottomTo(b.pixelBottom);

allBounds.splice(bi, 1);
changed = true;
}
});
});

// If no continuous blocks were found, we need a new rectangle
if (!changed) {
geometry.push(allBounds[0]);
allBounds.splice(0, 1);
}
}

while (allBounds.length > 0) {
runPass();
}
}

function drawToCanvas(ctx, arr) {
arr.forEach(e => {
assets.tiles.drawTile(ctx, e.tile, e.x, e.y);
});
}

function flattened(arr, newArr = []) {
arr.forEach(e => {
if (e instanceof Array)
flattened(e, newArr);
else
newArr.push(e);
});
return newArr;
}

export default class Structure {
constructor(x, y, arr) {
console.log(arr);
constructor(attrs, nestedArr) {
this.attrs = {
collides: false,
}

for (let a of attrs)
this.setAttr(a);

this.nestedArr = nestedArr;
this.bounds = new Rect();
this.geometry = [];
this.can = document.createElement("canvas");
this.bounds = new Rect(new Vec2(
x * assets.tiles.tileWidth, y * assets.tiles.tileHeight));
}

let ctx = this.can.getContext("2d");
findBounds(ctx, arr, this.bounds);
init() {
var arr = flattened(this.nestedArr);

findBounds(arr, this.bounds);
this.can.width = this.bounds.size.x;
this.can.height = this.bounds.size.y;

assets.tiles.whenReady(() => drawArr(ctx, arr));
console.log(this.bounds.pos, this.bounds.size);
if (this.attrs.collides) {
findGeometry(this.bounds, arr, this.geometry);
}

let ctx = this.can.getContext("2d");
assets.tiles.whenReady(() => drawToCanvas(ctx, arr));
}

draw(ctx) {
ctx.drawImage(
this.can, this.bounds.pos.x, this.bounds.pos.y,
this.can, this.bounds.pos.pixelX, this.bounds.pos.pixelY,
this.bounds.size.x, this.bounds.size.y);
}

setAttr(attr) {
if (this.attrs[attr] == null)
throw new Error("Invalid attribute:", attr);

this.attrs[attr] = true;
return this;
}

collidesWith(rect) {
if (!this.bounds.intersects(rect))
return null;

for (let bounds of this.geometry) {
if (bounds.intersects(rect))
return bounds;
}

return null;
}
}

+ 20
- 0
js/Vec2.js View File

@@ -1,3 +1,5 @@
let meter = 32;

export default class Vec2 {
constructor(x = 0, y = 0) {
this.x = x;
@@ -8,4 +10,22 @@ export default class Vec2 {
this.x = x;
this.y = y;
}

clone() {
return new Vec2(this.x, this.y);
}

get pixelX() {
return Math.floor(this.x * meter);
}
set pixelX(x) {
this.x = Math.floor(x) / meter;
}

get pixelY() {
return Math.floor(this.y * meter);
}
set pixelY(y) {
this.y = Math.floor(y) / meter;
}
}

+ 1
- 1
js/assets.js View File

@@ -1,4 +1,4 @@
import SpriteSheet from './SpriteSheet.js';
import SpriteSheet from "./SpriteSheet.js";

export default {
tiles: new SpriteSheet("assets/tiles.png", 32, 32, 2)

+ 18
- 0
js/entities/Platform.js View File

@@ -0,0 +1,18 @@
import Entity from "../Entity.js";

import TPlatform from "../traits/TPlatform.js";
import TCollider from "../traits/TCollider.js";

export default class Platform extends Entity {
constructor(level) {
super(level);

this.bounds.size.set(5, 1);
this.addTrait(new TCollider(this));
this.addTrait(new TPlatform(this));
}

draw(ctx) {
this.bounds.draw(ctx);
}
}

+ 8
- 7
js/entities/Player.js View File

@@ -1,16 +1,17 @@
import Entity from '../Entity.js';
import Vec2 from '../Vec2.js';
import Entity from "../Entity.js";

import KeyboardControls from '../traits/KeyboardControls.js';
import Physics from '../traits/Physics.js';
import TKeyboardController from "../traits/TKeyboardController.js";
import TPhysics from "../traits/TPhysics.js";
import TCollider from "../traits/TCollider.js";

export default class Player extends Entity {
constructor(level) {
super(level);

this.bounds.size.set(20, 20);
this.addTrait(new KeyboardControls(this));
this.addTrait(new Physics(this));
this.bounds.size.set(1, 2);
this.addTrait(new TCollider(this));
this.addTrait(new TKeyboardController(this));
this.addTrait(new TPhysics(this));
}

draw(ctx) {

+ 8
- 4
js/main.js View File

@@ -1,12 +1,16 @@
import Level from './Level';
import Player from './entities/Player';
import Vec2 from './Vec2';
import Level from "./Level.js";
import Player from "./entities/Player.js";
import Platform from "./entities/Platform.js";
import structures from "./structures.js";
import Vec2 from "./Vec2.js";

let canvas = document.getElementById("canvas");

let level = new Level(canvas);

level.spawn(new Player(level), 20, 20);
level.spawnEntity(new Player(level), 10, 1);
level.spawnEntity(new Platform(level), 16, 4);
level.spawnStructure(structures.floor(4), 4, 4);

level.start();


+ 6
- 5
js/structures.js View File

@@ -1,13 +1,14 @@
import Structure from './Structure.js';
import Structure from "./Structure.js";

export default {
floor: (x, y, width) => {
floor: (width) => {
let attrs = [ "collides" ];
if (width <= 1) {
return new Structure(x, y, [ { x: 0, y: 0, tile: "grass-lr" }]);
return new Structure(attrs, [ { x: 0, y: 0, tile: "grass-lr" }]);
} else {
return new Structure(x, y, [
return new Structure(attrs, [
{ x: 0, y: 0, tile: "grass-l", },
Array.from({ length: width - 2 }, (x, i) =>
Array.from({ length: width - 2 }, (_, i) =>
({ x: i + 1, y: 0, tile: "grass" })),
{ x: width - 1, y: 0, tile: "grass-r" },
]);

+ 0
- 34
js/traits/KeyboardControls.js View File

@@ -1,34 +0,0 @@
import {Trait} from '../Entity.js';

export default class KeyboardControls extends Trait {
constructor(entity) {
super(entity, "keyboardControls");

this.speed = 500;
this.map = {
KeyA: 'left',
KeyD: 'right',
};

this.pressed = {};
}

onkey(evt) {
let name = this.map[evt.code];
if (name == null) return;
evt.preventDefault();
this.pressed[name] = evt.type === 'keydown';
}

init() {
window.addEventListener("keydown", e => this.onkey(e));
window.addEventListener("keyup", e => this.onkey(e));
}

update(dt) {
if (this.pressed.left)
this.entity.velocity.x -= this.speed * dt;
if (this.pressed.right)
this.entity.velocity.x += this.speed * dt;
}
}

+ 0
- 13
js/traits/Physics.js View File

@@ -1,13 +0,0 @@
import {Trait} from '../Entity.js';

export default class Physics extends Trait {
constructor(entity) {
super(entity, "physics");

this.gravity = 9.81;
}

update(dt) {
this.entity.velocity.y += this.gravity * dt;
}
}

+ 27
- 0
js/traits/TCollider.js View File

@@ -0,0 +1,27 @@
import {Trait} from "../Entity.js";

export default class Collider extends Trait {
constructor(entity) {
super(entity, "collider");

this.collides = false;
this.cEntity = null;
this.cStructure = null;
}

collideEntity(e) {
this.cEntity = e;
this.collides = true;
}

collideStructure(s) {
this.cStructure = s;
this.collides = true;
}

postUpdate() {
this.cEntity = null;
this.cStructure = null;
this.collides = false;
}
}

+ 62
- 0
js/traits/TKeyboardController.js View File

@@ -0,0 +1,62 @@
import {Trait} from "../Entity.js";

export default class TKeyboardController extends Trait {
constructor(entity) {
super(entity, "keyboardController", [ "physics" ]);

this.speed = 50;
this.speedAir = 20;

this.jump = 8;
this.jumpTimeMax = 0.4;
this.updrift = 100;

this.map = {
KeyA: 'left',
KeyD: 'right',
Space: "jump",
};

this.pressed = {};

this.jumpTime = 0;
this.jumping = false;
}

onkey(evt) {
let name = this.map[evt.code];
if (name == null) return;
evt.preventDefault();
this.pressed[name] = evt.type === 'keydown';
}

init() {
window.addEventListener("keydown", e => this.onkey(e));
window.addEventListener("keyup", e => this.onkey(e));
}

update(dt) {
let phys = this.entity.t.physics;

let speed = phys.onGround ? this.speed : this.speedAir;

if (this.pressed.left)
phys.relVelocity.x -= speed * dt;
if (this.pressed.right)
phys.relVelocity.x += speed * dt;
if (phys.onGround && this.pressed.jump) {
phys.relVelocity.y -= this.jump;
this.jumpTime = this.jumpTimeMax;
this.jumping = true;
phys.onGround = false;
}

if (phys.onGround || !this.pressed.jump || this.jumpTime <= 0)
this.jumping = false;

if (this.jumping) {
phys.relVelocity.y -= this.updrift * this.jumpTime * dt;
this.jumpTime -= dt;
}
}
}

+ 84
- 0
js/traits/TPhysics.js View File

@@ -0,0 +1,84 @@
import {Trait} from "../Entity.js";
import Vec2 from "../Vec2.js";

export default class TPhysics extends Trait {
constructor(entity) {
super(entity, "physics");

this.gravity = 40;
this.groundFriction = 6;
this.airFriction = 2;

this.velocity = new Vec2();
this.relVelocity = new Vec2();

this.onGround = false;
this.prevRelativeTo = null;
this.relativeTo = null;
}

update(dt) {
let collider = this.entity.t.collider;

this.prevRelativeTo = this.relativeTo;
this.relativeTo = null;
this.onGround = false;

if (
this.entity.has("collider") &&
collider.collides &&
this.relVelocity.y >= 0) {

// Structures are static; just teleport us to the top of them
if (collider.cStructure) {
this.relVelocity.y = 0;
this.entity.bounds.bottom = collider.cStructure.top;
this.onGround = true;

// If we're colliding with an entity, and that entity is
// a platform, teleport us to the top of them.
} else if (collider.cEntity.has("platform")) {
this.relVelocity.y = 0;
this.entity.bounds.bottom = collider.cEntity.bounds.top;
this.onGround = true;

if (collider.cEntity.has("physics")) {
let cPhys = collider.cEntity.t.physics;
this.relativeTo = cPhys;
}
}
}

this.relVelocity.y += this.gravity * dt;

// If we just started riding something, adjust relative velocity
if (!this.prevRelativeTo && this.relativeTo) {
this.relVelocity.x = this.velocity.x - this.relativeTo.velocity.x;
this.relVelocity.y = this.velocity.y - this.relativeTo.velocity.y;

// If we just stopped riding something, adjust relative velocity
} else if (this.prevRelativeTo && !this.relativeTo) {
this.relVelocity.x = this.velocity.x + this.relativeTo.velocity.x;
this.relVelocity.y = this.velocity.y + this.relativeTo.velocity.y;
}

// Apply friction
var fric = this.onGround ? this.groundFriction : this.airFriction;
var xRatio = 1 / (1 + (dt * fric));
this.relVelocity.x *= xRatio;

// Update velocity
if (this.relativeTo) {
this.velocity.x = this.relVelocity.x + this.relativeTo.velocity.x;
this.velocity.y = this.relVelocity.y + this.relativeTo.velocity.y;
} else {
this.velocity.x = this.relVelocity.x;
this.velocity.y = this.relVelocity.y;
}
}

postUpdate(dt) {
this.entity.bounds.pos.x += this.velocity.x * dt;
this.entity.bounds.pos.y += this.velocity.y * dt;
}
}

+ 7
- 0
js/traits/TPlatform.js View File

@@ -0,0 +1,7 @@
import {Trait} from "../Entity.js";

export default class TPlatform extends Trait {
constructor(entity) {
super(entity, "platform", [ "collider" ]);
}
}

Loading…
Cancel
Save