@@ -1,4 +1,5 @@ | |||
import Rect from "./Rect.js"; | |||
import Vec2 from "./Vec2.js"; | |||
export class Trait { | |||
constructor(entity, name, deps = []) { | |||
@@ -30,7 +31,8 @@ export default class Entity { | |||
constructor(level, layerName) { | |||
this.layerName = layerName; | |||
this.level = level; | |||
this.bounds = new Rect(); | |||
this.pos = new Vec2(); | |||
this.bounds = new Rect(this.pos); | |||
this.time = 0; | |||
this.traitOrder = [ | |||
@@ -38,6 +40,7 @@ export default class Entity { | |||
"platform", | |||
"wall", | |||
"keyboardController", | |||
"behavior", | |||
"physics", | |||
]; | |||
this.t = { | |||
@@ -45,6 +48,7 @@ export default class Entity { | |||
platform: null, | |||
wall: null, | |||
keyboardController: null, | |||
behavior: null, | |||
physics: null, | |||
}; | |||
@@ -79,7 +83,6 @@ export default class Entity { | |||
trait.update(dt); | |||
} | |||
this.update(dt); | |||
this.time += dt; | |||
} | |||
@@ -88,14 +91,9 @@ export default class Entity { | |||
if (trait.enabled) | |||
trait.postUpdate(dt); | |||
} | |||
this.postUpdate(dt); | |||
} | |||
init() {} | |||
update(dt) {} | |||
postUpdate(dt) {} | |||
draw(ctx) {} | |||
has(name) { |
@@ -24,10 +24,9 @@ export default class Level { | |||
} | |||
spawnEntity(ent, x, y) { | |||
ent.bounds.pos.set(x, y); | |||
ent.pos.set(x, y); | |||
let layerIdx = this.layerOrder[ent.layerName]; | |||
console.log(ent.layerName, this.layerOrder, layerIdx); | |||
if (layerIdx == null) | |||
throw new Error("Unknown layer name: "+ent.layerName); | |||
@@ -43,7 +42,7 @@ export default class Level { | |||
} | |||
spawnStructure(structure, x, y) { | |||
structure.bounds.pos.set(x, y); | |||
structure.pos.set(x, y); | |||
this.structures.push(structure); | |||
structure.init(); | |||
} | |||
@@ -62,11 +61,10 @@ export default class Level { | |||
return; | |||
this.colliders.forEach(ent => { | |||
let bounds = s.collidesWith(ent.bounds); | |||
if (!bounds) | |||
if (!s.bounds.intersects(ent.bounds)) | |||
return; | |||
ent.t.collider.collideStructure(s, bounds); | |||
ent.t.collider.collideStructure(s); | |||
}); | |||
}); | |||
@@ -35,58 +35,6 @@ function continuous(r1, r2) { | |||
return rightOrBelow(r1, r2) || rightOrBelow(r2, r1); | |||
} | |||
function findGeometry(bounds, arr, geometry) { | |||
let allBounds = []; | |||
arr.forEach(e => { | |||
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 flattened(arr, newArr = []) { | |||
arr.forEach(e => { | |||
if (e instanceof Array) | |||
@@ -110,22 +58,18 @@ export default class Structure { | |||
this.setAttr(a); | |||
this.nestedArr = nestedArr; | |||
this.bounds = new Rect(); | |||
this.geometry = []; | |||
this.pos = new Vec2(); | |||
this.bounds = new Rect(this.pos); | |||
} | |||
init() { | |||
var arr = flattened(this.nestedArr); | |||
findBounds(arr, this.bounds); | |||
if (this.attrs.wall || this.attrs.platform) { | |||
findGeometry(this.bounds, arr, this.geometry); | |||
} | |||
} | |||
draw(ctx) { | |||
this.texture.draw(ctx, this.bounds.pos.pixelX, this.bounds.pos.pixelY); | |||
this.texture.draw(ctx, this.pos.pixelX, this.pos.pixelY); | |||
} | |||
setAttr(attr) { | |||
@@ -135,16 +79,4 @@ export default class Structure { | |||
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; | |||
} | |||
} |
@@ -6,33 +6,99 @@ export default class Tile { | |||
} | |||
} | |||
Tile.createLine = function(width, name, x = 0, y = 0) { | |||
if (width <= 1) { | |||
return [ new Tile(x, y, name+"-lr") ]; | |||
function realNames(base, override = {}) { | |||
return { | |||
topLeft: override.topLeft || base+"-top-l", | |||
top: override.top || base+"-top", | |||
topRight: override.topRight || base+"-top-r", | |||
topBoth: override.topBoth || base+"-top-lr", | |||
left: override.left || base+"-l", | |||
mid: override.mid || base, | |||
right: override.right || base+"-r", | |||
both: override.both || base+"-lr", | |||
bottomLeft: override.bottomLeft || base+"-bottom-l", | |||
bottom: override.bottom || base+"-bottom", | |||
bottomRight: override.bottomRight || base+"-bottom-r", | |||
bottomBoth: override.bottomBoth || base+"-bottom-lr", | |||
}; | |||
} | |||
function getName(x, y, size, names) { | |||
let name = null; | |||
if (size.x <= 1) { | |||
if (y === 0) | |||
name = names.topBoth; | |||
else if (y === size.y - 1) | |||
name = names.bottomBoth; | |||
else | |||
name = names.both; | |||
} else { | |||
return [ | |||
new Tile(x, y, name+"-l"), | |||
Array.from({ length: width - 2 }, (_, i) => | |||
new Tile(x + i + 1, y, name)), | |||
new Tile(x + width - 1, y, name+"-r"), | |||
]; | |||
if (y === 0) { | |||
if (x === 0) | |||
name = names.topLeft; | |||
else if (x === size.x - 1) | |||
name = names.topRight; | |||
else | |||
name = names.top; | |||
} else if (y === size.y - 1) { | |||
if (x === 0) | |||
name = names.bottomLeft; | |||
else if (x === size.x - 1) | |||
name = names.bottomRight; | |||
else | |||
name = names.bottom; | |||
} else { | |||
if (x === 0) | |||
name = names.left; | |||
else if (x === size.x - 1) | |||
name = names.right; | |||
else | |||
name = names.mid; | |||
} | |||
} | |||
if (!name) | |||
name = "--invalid-"+x+"-"+y+"--"; | |||
return name; | |||
} | |||
Tile.createBox = function(width, height, nameTop, nameMid, nameBottom) { | |||
if (height <= 1) { | |||
return Tile.createLine(width, nameTop); | |||
} else if (height <= 2) { | |||
return [ | |||
Tile.createLine(width, nameTop), | |||
Tile.createLine(width, nameBottom), | |||
]; | |||
} else { | |||
return [ | |||
Tile.createLine(width, nameTop), | |||
Array.from({ length: height - 2 }, (_, i) => | |||
Tile.createLine(width, nameMid, 0, i + 1)), | |||
Tile.createLine(width, nameBottom, 0, height - 1), | |||
]; | |||
Tile.createBox = function(pos, size, base, names) { | |||
names = realNames(base, names); | |||
let arr = []; | |||
for (let x = 0; x < size.x; ++x) { | |||
for (let y = 0; y < size.y; ++y) { | |||
arr.push(new Tile( | |||
pos.x + x, pos.y + y, getName(x, y, size, names))); | |||
} | |||
} | |||
return arr; | |||
} | |||
function getNameLine(x, width, names) { | |||
if (width === 1) | |||
return names.both; | |||
else if (x === 0) | |||
return names.left; | |||
else if (x === width - 1) | |||
return names.right; | |||
else | |||
return names.mid; | |||
} | |||
Tile.createLine = function(pos, width, base, names) { | |||
names = realNames(base, names); | |||
let arr = []; | |||
for (let x = 0; x < width; ++x) { | |||
arr.push(new Tile( | |||
pos.x + x, pos.y, getNameLine(x, width, names))); | |||
} | |||
return arr; | |||
} |
@@ -29,3 +29,5 @@ export default class Vec2 { | |||
this.y = Math.floor(y) / meter; | |||
} | |||
} | |||
Vec2.zero = new Vec2(); |
@@ -23,5 +23,5 @@ export default { | |||
.defineTile("broken-platform-lr", 3, 3), | |||
entities: new SpriteSheet("assets/entities.png", 19, 32, 2) | |||
.defineTile("player-head", 0, 0), | |||
.defineTile("player", 0, 0), | |||
} |
@@ -1,87 +1,103 @@ | |||
import Entity from "../Entity.js"; | |||
import {Trait} from "../Entity.js"; | |||
import Texture from "../Texture.js"; | |||
import Tile from "../Tile.js"; | |||
import Shaker from "../Shaker.js"; | |||
import Vec2 from "../Vec2.js"; | |||
import assets from "../assets.js"; | |||
import TPlatform from "../traits/TPlatform.js"; | |||
import TCollider from "../traits/TCollider.js"; | |||
import TPhysics from "../traits/TPhysics.js"; | |||
export default class BrokenPlatform extends Entity { | |||
constructor(level, width = 5) { | |||
super(level, "platform"); | |||
this.bounds.size.set(width, 0.5); | |||
this.addTrait(new TCollider(this)); | |||
this.addTrait(new TPlatform(this)); | |||
this.addTrait(new TPhysics(this)); | |||
this.texture = new Texture(assets.tiles, | |||
Tile.createLine(this.bounds.size.x, "broken-platform")); | |||
class Behavior extends Trait { | |||
constructor(entity) { | |||
super(entity, "behavior"); | |||
this.shaker = new Shaker(); | |||
this.shake = 0; | |||
this.shakeAccel = 14; | |||
this.shaking = false; | |||
this.floating = true; | |||
this.reliableTime = 1; | |||
this.resetTime = 4; | |||
this.state = 0; | |||
this.timer = 0; | |||
} | |||
init() { | |||
this.initialPos = this.bounds.pos.clone(); | |||
this.initialPos = this.entity.pos.clone(); | |||
} | |||
reset() { | |||
this.shake = 0; | |||
this.shaking = false; | |||
this.floating = true; | |||
this.t.physics.velocity.set(0, 0); | |||
this.bounds.pos.set(this.initialPos.x, this.initialPos.y); | |||
} | |||
startShake() { | |||
this.shake = 0; | |||
this.shaking = true | |||
setTimeout(() => { | |||
this.floating = false; | |||
this.shaking = false; | |||
this.entity.t.physics.velocity.set(0, 0); | |||
this.entity.pos.set(this.initialPos.x, this.initialPos.y); | |||
this.entity.t.physics.moved = true; | |||
setTimeout(() => { | |||
this.reset(); | |||
}, this.resetTime * 1000); | |||
}, this.reliableTime * 1000); | |||
this.state = 0; | |||
this.timer = 0; | |||
} | |||
update(dt) { | |||
if (!this.shaking && !this.falling) { | |||
this.t.collider.entities.forEach(e => { | |||
if (this.state === 0) { | |||
this.entity.t.collider.entities.forEach(e => { | |||
let fall = | |||
e.has("physics") && | |||
e.bounds.intersectSide(this.bounds) === "top"; | |||
e.bounds.intersectSide(this.entity.bounds) === "top"; | |||
if (fall) { | |||
this.startShake(); | |||
this.timer = this.reliableTime; | |||
this.state = 1; | |||
} | |||
}); | |||
} | |||
if (this.floating) | |||
this.t.physics.velocity.y -= this.t.physics.gravity * dt; | |||
if (this.shaking) { | |||
if (this.state !== 2) | |||
this.entity.t.physics.velocity.y -= | |||
this.entity.t.physics.gravity * dt; | |||
if (this.state === 1) { | |||
this.shaker.shake(this.shake); | |||
this.shake += this.shakeAccel * dt; | |||
this.timer -= dt; | |||
if (this.timer <= 0) { | |||
this.state = 2; | |||
this.timer = this.resetTime; | |||
} | |||
} else if (this.state === 2) { | |||
this.timer -= dt; | |||
if (this.timer <= 0) { | |||
this.reset(); | |||
} | |||
} | |||
this.shaker.update(dt); | |||
} | |||
} | |||
export default class BrokenPlatform extends Entity { | |||
constructor(level, width = 5) { | |||
super(level, "platform"); | |||
this.bounds.size.set(width, 0.5); | |||
this.addTrait(new TCollider(this)); | |||
this.addTrait(new TPlatform(this)); | |||
this.addTrait(new TPhysics(this)); | |||
this.addTrait(new Behavior(this)); | |||
this.texture = new Texture(assets.tiles, Tile.createLine( | |||
Vec2.zero, this.bounds.size.x, | |||
"broken-platform")); | |||
} | |||
draw(ctx) { | |||
let shaker = this.t.behavior.shaker; | |||
this.texture.draw( | |||
ctx, | |||
this.bounds.pos.pixelX + this.shaker.vec.pixelX, | |||
this.bounds.pos.pixelY + this.shaker.vec.pixelY); | |||
this.pos.pixelX + shaker.vec.pixelX, | |||
this.pos.pixelY + shaker.vec.pixelY); | |||
} | |||
} |
@@ -17,7 +17,7 @@ export default class FloatingPlatform extends Entity { | |||
this.addTrait(new TPhysics(this)); | |||
this.texture = new Texture(assets.tiles, | |||
Tile.createLine(this.bounds.size.x, "platform")); | |||
Tile.createSidedLine(this.bounds.size.x, "platform")); | |||
this.targetSpeed = 3; | |||
this.accel = 5; | |||
@@ -26,7 +26,7 @@ export default class FloatingPlatform extends Entity { | |||
} | |||
init() { | |||
this.targetTop = this.bounds.pos.y; | |||
this.targetTop = this.pos.y; | |||
this.targetBottom = this.targetTop + this.distance; | |||
this.t.physics.gravity = 0; | |||
@@ -36,9 +36,9 @@ export default class FloatingPlatform extends Entity { | |||
update(dt) { | |||
let phys = this.t.physics; | |||
if (this.bounds.pos.y <= this.targetTop) | |||
if (this.pos.y <= this.targetTop) | |||
this.dir = 1; | |||
else if (this.bounds.pos.y >= this.targetBottom) | |||
else if (this.pos.y >= this.targetBottom) | |||
this.dir = -1; | |||
if (this.dir == 1) { | |||
@@ -51,6 +51,6 @@ export default class FloatingPlatform extends Entity { | |||
} | |||
draw(ctx) { | |||
this.texture.drawAt(ctx, this.bounds.pos); | |||
this.texture.drawAt(ctx, this.pos); | |||
} | |||
} |
@@ -17,16 +17,16 @@ export default class Player extends Entity { | |||
this.addTrait(new TPhysics(this)); | |||
this.texture = new Texture(assets.entities, [ | |||
new Tile(0, 0, "player-head"), | |||
new Tile(0, 0, "player"), | |||
]); | |||
} | |||
draw(ctx) { | |||
let x = | |||
this.bounds.pos.pixelX - | |||
this.pos.pixelX - | |||
assets.entities.scale * 1.5 - | |||
this.bounds.size.pixelX / 2 | |||
this.texture.draw(ctx, x, this.bounds.pos.pixelY); | |||
this.texture.draw(ctx, x, this.pos.pixelY); | |||
} | |||
} |
@@ -11,10 +11,12 @@ let canvas = document.getElementById("canvas"); | |||
let level = new Level(canvas); | |||
level.spawnEntity(new Player(level), 10, 1); | |||
level.spawnEntity(new FloatingPlatform(level), 16, 4); | |||
level.spawnStructure(structures.floor(8, 6), 4, 4); | |||
level.spawnEntity(new BrokenPlatform(level), 27, 4); | |||
level.spawnStructure(structures.ground(new Vec2(22, 6)), 8, 8); | |||
level.spawnStructure(structures.groundPillar(new Vec2(2, 3)), 14, 6); | |||
level.spawnStructure(structures.groundPillar(new Vec2(1, 5)), 20, 4); | |||
level.spawnEntity(new BrokenPlatform(level), 22, 6); | |||
level.start(); | |||
@@ -1,8 +1,25 @@ | |||
import Structure from "./Structure.js"; | |||
import Vec2 from "./Vec2.js"; | |||
import Tile from "./Tile.js"; | |||
export default { | |||
floor: (width, height = 1)=> new Structure( | |||
ground: size => new Structure( | |||
[ "wall" ], | |||
Tile.createBox(width, height, "ground-top", "ground", "ground")), | |||
Tile.createBox( | |||
Vec2.zero, size, "ground", { | |||
bottomLeft: "ground-l", | |||
bottom: "ground", | |||
bottomRight: "ground-r", | |||
bottomBoth: "ground", | |||
})), | |||
groundPillar: size => new Structure( | |||
[ "wall" ], | |||
Tile.createBox( | |||
Vec2.zero, size, "ground", { | |||
bottomLeft: "ground", | |||
bottom: "ground", | |||
bottomRight: "ground", | |||
bottomBoth: "ground", | |||
})), | |||
}; |
@@ -15,16 +15,14 @@ export default class Collider extends Trait { | |||
this.entities.push(e); | |||
} | |||
collideStructure(s, b) { | |||
collideStructure(s) { | |||
this.collides = true; | |||
this.structures.push(s); | |||
this.structureBounds.push(b); | |||
} | |||
postUpdate() { | |||
this.entities.length = 0; | |||
this.structures.length = 0; | |||
this.structureBounds.length = 0; | |||
this.collides = false; | |||
} | |||
} |
@@ -9,8 +9,8 @@ export default class TKeyboardController extends Trait { | |||
this.jump = 8; | |||
this.jumpTimeMax = 0.4; | |||
this.updrift = 100; | |||
this.jumpLeeway = 0.2; | |||
this.updrift = 87; | |||
this.jumpLeeway = 0.1; | |||
this.map = { | |||
KeyA: 'left', | |||
@@ -54,7 +54,7 @@ export default class TKeyboardController extends Trait { | |||
if (this.pressed.jump && canJump) { | |||
phys.velocity.y -= this.jump; | |||
this.entity.bounds.pos.y += phys.velocity.y * dt; | |||
this.entity.pos.y += phys.velocity.y * dt; | |||
this.jumpTime = this.jumpTimeMax; | |||
this.jumping = true; | |||
this.jumped = true; |
@@ -1,8 +1,6 @@ | |||
import {Trait} from "../Entity.js"; | |||
import Vec2 from "../Vec2.js"; | |||
let zeroVector = new Vec2(0, 0); | |||
export default class TPhysics extends Trait { | |||
constructor(entity) { | |||
super(entity, "physics"); | |||
@@ -16,6 +14,7 @@ export default class TPhysics extends Trait { | |||
this.onGround = false; | |||
this.groundBounds = null; | |||
this.groundVelocity = null; | |||
this.moved = false; | |||
} | |||
collideTop(bounds, velocity) { | |||
@@ -28,8 +27,15 @@ export default class TPhysics extends Trait { | |||
collideWall(bounds, velocity) { | |||
let side = this.entity.bounds.intersectSide(bounds); | |||
if (side === "top") | |||
return this.collideTop(bounds, velocity); | |||
if (side === "top") { | |||
this.collideTop(bounds, velocity); | |||
} else if (side === "left" && this.velocity.x > velocity.x) { | |||
this.velocity.x = velocity.x; | |||
this.entity.bounds.right = bounds.left; | |||
} else if (side === "right" && this.velocity.x < velocity.x) { | |||
this.velocity.x = velocity.x; | |||
this.entity.bounds.left = bounds.right; | |||
} | |||
} | |||
collidePlatform(bounds, velocity) { | |||
@@ -42,13 +48,13 @@ export default class TPhysics extends Trait { | |||
update(dt) { | |||
// Collide | |||
if (this.entity.has("collider")) { | |||
if (this.entity.has("collider") && !this.moved) { | |||
let collider = this.entity.t.collider; | |||
this.groundBounds = null; | |||
this.groundVelocity = null; | |||
collider.entities.forEach(e => { | |||
let vel = zeroVector; | |||
let vel = Vec2.zero; | |||
if (e.has("physics")) | |||
vel = e.t.physics.velocity; | |||
@@ -57,19 +63,18 @@ export default class TPhysics extends Trait { | |||
else if (e.has("platform")) | |||
this.collidePlatform(e.bounds, vel); | |||
}); | |||
collider.structures.forEach((s, i) => { | |||
let bounds = collider.structureBounds[i]; | |||
collider.structures.forEach((s) => { | |||
if (s.attrs.wall) | |||
this.collideWall(bounds, zeroVector); | |||
this.collideWall(s.bounds, Vec2.zero); | |||
else if (s.attrs.platform) | |||
this.collidePlatform(bounds, zeroVector); | |||
this.collidePlatform(s.bounds, Vec2.zero); | |||
}); | |||
} | |||
this.onGround = | |||
(this.groundVelocity && this.velocity.y >= this.groundVelocity.y); | |||
this.groundVelocity && this.velocity.y >= this.groundVelocity.y; | |||
if (this.onGround) { | |||
if (this.onGround && !this.moved) { | |||
this.timeLastOnGround = this.entity.time; | |||
this.velocity.y = this.groundVelocity.y; | |||
} | |||
@@ -86,10 +91,12 @@ export default class TPhysics extends Trait { | |||
postUpdate(dt) { | |||
// Move | |||
this.entity.bounds.pos.x += this.velocity.x * dt; | |||
this.entity.bounds.pos.y += this.velocity.y * dt; | |||
this.entity.pos.x += this.velocity.x * dt; | |||
this.entity.pos.y += this.velocity.y * dt; | |||
if (this.onGround) | |||
if (this.onGround && !this.moved) | |||
this.entity.bounds.bottom = this.groundBounds.top; | |||
this.moved = false; | |||
} | |||
} |