diff --git a/src/BounceBoard.js b/src/BounceBoard.js new file mode 100644 index 0000000..fcac99d --- /dev/null +++ b/src/BounceBoard.js @@ -0,0 +1,44 @@ +import { Vector } from 'kontra'; + +import { acc } from './constants'; +import { lineIntersection } from './utils'; + +export class BounceBoard { + p1; + p2; + level; + + constructor({ p1, p2, level }) { + this.p1 = Vector(p1.x, p1.y); + this.p2 = Vector(p2.x, p2.y); + this.level = level; + } + update() { + this.handlePlayerCollision(this.level.player); + } + + render(ctx) { + ctx.beginPath(); + ctx.lineWidth = 4; + ctx.strokeStyle = acc; + // TODO (johnedvard) add bounce effect when hitting line + ctx.moveTo(this.p1.x, this.p1.y); + ctx.lineTo(this.p2.x, this.p2.y); + + ctx.stroke(); + ctx.restore(); + } + + handlePlayerCollision(player) { + const intersectionPoint = lineIntersection( + this.p1, + this.p2, + player.oldPos, + player.currPos + ); + if (intersectionPoint) { + player.oldPos = Vector(player.oldPos.x - 100, player.oldPos.y + 100); + player.currPos = Vector(player.oldPos.x + 100, player.oldPos.y - 100); + } + } +} diff --git a/src/Brick.js b/src/Brick.js index c8a33af..b66cf35 100644 --- a/src/Brick.js +++ b/src/Brick.js @@ -1,10 +1,18 @@ +import { getDirection, moveBehavior, setBehavior } from './behavior'; + export class Brick { x; y; level; width = 32; height = 32; - constructor(x, y, { level }) { + speed = 1; + constructor(x, y, { level, behavior, distance }) { + this.direction = getDirection(behavior, distance); + this.distance = Math.abs(distance); + this.behavior = behavior; + this.orgX = x; + this.orgY = y; this.x = x; this.y = y; this.level = level; @@ -18,7 +26,19 @@ export class Brick { height: 12, }; } - update() {} + update() { + const { axis, newDirection, multiplier } = moveBehavior({ + behavior: this.behavior, + distance: this.distance, + direction: this.direction, + x: this.x, + y: this.y, + orgX: this.orgX, + orgY: this.orgY, + }); + this.direction = newDirection; + this[axis] += this.speed * multiplier; + } render(ctx) { if (!ctx) return; diff --git a/src/Game.js b/src/Game.js index 1508c88..f80730a 100644 --- a/src/Game.js +++ b/src/Game.js @@ -31,6 +31,7 @@ export class Game { }, render: function () { if (!game.level) return; + context.save(); game.level.render(game.context); }, }); @@ -39,10 +40,13 @@ export class Game { this.listenForGameEvents(); } - loadLevel(levelId) { - this.level = new Level({ levelId, game: this }); + loadLevel({ levelId, levelData }) { + if (levelId) { + this.level = new Level({ levelId, game: this }); + } else if (levelData) { + this.level = new Level({ game: this, levelData: JSON.parse(levelData) }); + } } - addPointerListeners() { onPointer('down', () => { this.isDragging = true; @@ -59,19 +63,18 @@ export class Game { } onStartNextLevel = () => { this.level.destroy(); - this.loadLevel(this.level.levelId + 1); + this.loadLevel({ levelId: this.level.levelId + 1 }); }; onStartLevel = ({ levelId, levelData }) => { if (this.level) { this.level.destroy(); } - if (levelId) { - this.loadLevel(levelId); - } else if (levelData) { - this.level = new Level({ game: this, levelData: JSON.parse(levelData) }); - } + this.loadLevel({ levelId, levelData }); }; onReStartLevel = () => { - this.loadLevel(this.level.levelId); + this.loadLevel({ + levelId: this.level.levelId, + levelData: this.level.levelData, + }); }; } diff --git a/src/Level.js b/src/Level.js index 8726213..9d35678 100644 --- a/src/Level.js +++ b/src/Level.js @@ -1,9 +1,10 @@ +import { BounceBoard } from './BounceBoard'; import { Brick } from './Brick'; import { Goal } from './Goal'; import { Heart } from './Heart'; import { Player } from './Player'; import { Saw } from './Saw'; -import { lineIntersection } from './utils'; +import { isBoxCollision } from './utils'; export class Level { player; @@ -11,8 +12,13 @@ export class Level { goals = []; hearts = []; bricks = []; + bounceBoards = []; isLevelLoaded = false; + levelId; + levelData; constructor({ game, levelId, levelData }) { + this.levelId = levelId; + this.levelData = levelData; this.game = game; if (levelData) { setTimeout(() => { @@ -32,6 +38,7 @@ export class Level { this.createGoals(levelData); this.createHearts(levelData); this.createBricks(levelData); + this.createBounceBoards(levelData); this.isLevelLoaded = true; } @@ -61,6 +68,9 @@ export class Level { this.bricks.forEach((brick) => { brick.render(ctx); }); + this.bounceBoards.forEach((board) => { + board.render(ctx); + }); this.player.render(ctx); this.goals.forEach((goal) => { goal.render(ctx); @@ -83,6 +93,9 @@ export class Level { this.bricks.forEach((brick) => { brick.update(); }); + this.bounceBoards.forEach((board) => { + board.update(); + }); } createGoals(levelData) { @@ -99,6 +112,7 @@ export class Level { } createSaws(levelData) { + if (!levelData.s) return; levelData.s.forEach((saw) => { // TODO (johnedvard) Add actual saw behaviour this.saws.push( @@ -108,14 +122,31 @@ export class Level { } createHearts(levelData) { + if (!levelData.h) return; levelData.h.forEach((heart) => { this.hearts.push(new Heart(heart.x, heart.y, { level: this })); }); } createBricks(levelData) { + if (!levelData.b) return; levelData.b.forEach((brick) => { - this.bricks.push(new Brick(brick.x, brick.y, { level: this })); + this.bricks.push( + new Brick(brick.x, brick.y, { + behavior: brick.b, + distance: brick.d, + level: this, + }) + ); + }); + } + + createBounceBoards(levelData) { + if (!levelData.bb) return; + levelData.bb.forEach((board) => { + this.bounceBoards.push( + new BounceBoard({ p1: board.p1, p2: board.p2, level: this }) + ); }); } @@ -125,13 +156,11 @@ export class Level { for (let i = 0; i < rope.length - 2; i++) { this.saws.forEach((saw) => { if ( - // TODO (johnedvard) add to y-axis if saw is up down - lineIntersection( - { x: saw.x - 5, y: saw.y - 5 }, - { x: saw.x + 5, y: saw.y + 5 }, - { x: rope.nodes[i].x, y: rope.nodes[i].y }, - { x: rope.nodes[i + 1].x, y: rope.nodes[i + 1].y } - ) + isBoxCollision(rope.nodes[i], { + width: saw.width * saw.scale, + height: saw.height * saw.scale, + ...saw, + }) ) { this.player.rope.cutRope(i); } diff --git a/src/Player.js b/src/Player.js index 6be604f..61aeafd 100644 --- a/src/Player.js +++ b/src/Player.js @@ -1,33 +1,42 @@ import skull from 'data-url:./assets/img/skull.png'; -import { getPointer, Sprite, on } from 'kontra'; +import { getPointer, Sprite, on, Vector } from 'kontra'; import { PlayerControls } from './PlayerControls'; import { fgc2 } from './constants'; import { ARCADIAN_HEAD_SELECTED, CUT_ROPE, GOAL_COLLISION } from './gameEvents'; import { Rope } from './Rope'; import { getSelectedArcadian } from './store'; import { createSprite } from './utils'; +import { getDirection, moveBehavior, setBehavior } from './behavior'; export class Player { game; - rope = []; // list of pointmasses + rope = []; playerControls; scale = 4; - sprite = { render: () => {}, x: 0, y: 0 }; // draw sprite on pointmass position + sprite = { render: () => {}, x: 0, y: 0 }; headSprite = { render: () => {}, x: 0, y: 0 }; // From Arcadian API hasWon = false; headImg; headOffset = { x: 10, y: 38 }; isLeft = false; isRopeCut = false; + anchorNodeSpeed = 1; + anchorNodeDirection; + anchorNodeOrgPos; constructor({ game, levelData }) { + levelData.p = levelData.p || {}; + this.anchorNodeDirection = getDirection(levelData.p.b, levelData.p.d); + this.distance = Math.abs(levelData.p.d); + this.behavior = levelData.p.b; this.game = game; const ropeLength = levelData.r; const startX = levelData.p.x; const startY = levelData.p.y; + this.anchorNodeOrgPos = Vector(startX, startY); this.headImg = getSelectedArcadian().img || { width: 0, height: 0 }; - this.createRope({ startX, startY, ropeLength }); + this.createRope({ startX, startY, ropeLength, levelData }); createSprite({ x: startX, y: startY, @@ -139,10 +148,26 @@ export class Player { this.rope.endNode.pos.y - this.headImg.height + +this.headOffset.y; this.updateRope(); + this.updateAnchorNode(); // this.dragRope(); // TODO (johnedvard) Only enable in local and beta env this.playerControls.updateControls(); } + updateAnchorNode() { + const anchorNode = this.rope.anchorNode; + const { axis, newDirection, multiplier } = moveBehavior({ + behavior: this.behavior, + distance: this.distance, + direction: this.anchorNodeDirection, + x: anchorNode.pos.x, + y: anchorNode.pos.y, + orgX: this.anchorNodeOrgPos.x, + orgY: this.anchorNodeOrgPos.y, + }); + this.anchorNodeDirection = newDirection; + anchorNode.pos[axis] += this.anchorNodeSpeed * multiplier; + } + climbRope() { this.rope.climbRope(); } @@ -171,4 +196,18 @@ export class Player { onCutRope = ({ rope }) => { this.isRopeCut = true; }; + + get currPos() { + return this.rope.endNode.pos; + } + get oldPos() { + return this.rope.endNode.oldPos; + } + + set currPos(pos) { + this.rope.endNode.currPos = pos; + } + set oldPos(pos) { + this.rope.endNode.oldPos = pos; + } } diff --git a/src/Rope.js b/src/Rope.js index f29dbab..917ab19 100644 --- a/src/Rope.js +++ b/src/Rope.js @@ -1,4 +1,5 @@ import { emit, Vector } from 'kontra'; + import { fgc2, gravity, RESTING_DISTANCE } from './constants'; import { CUT_ROPE } from './gameEvents'; import { gameHeight, gameWidth } from './store'; @@ -48,7 +49,7 @@ export class Rope { if (n === this.nodes[0]) n.pos = this.anchor; const vxy = n.pos.subtract(n.oldPos); n.oldPos = n.pos; - n.pos = n.pos.add(vxy).add(Vector(0, gravity)); + n.pos = n.pos.add(vxy).add(Vector(0, gravity * n.mass)); }); } constrainNodes() { @@ -150,4 +151,9 @@ export class Rope { get endNode() { return this.nodes[this.nodes.length - 1]; } + + get anchorNode() { + if (!this.nodes.length) return {}; + return this.nodes[0]; + } } diff --git a/src/Saw.js b/src/Saw.js index 4050cd8..0274c2e 100644 --- a/src/Saw.js +++ b/src/Saw.js @@ -1,6 +1,6 @@ import saw2 from 'data-url:./assets/img/saw3.png'; -import { BACK_FORTH, UP_DOWN } from './sawBehavior'; +import { moveBehavior, getDirection } from './behavior'; import { createSprite } from './utils'; export class Saw { @@ -13,60 +13,40 @@ export class Saw { speed = 1; scale = 4; rotSpeed = 0.2; + width = 8; + height = 8; level; - direction = 'e'; // n,s,e,w constructor(x, y, { behavior, distance, level }) { + this.direction = getDirection(behavior, distance); + this.distance = Math.abs(distance); + this.behavior = behavior; this.x = x; this.y = y; this.orgX = x; this.orgY = y; - this.distance = 200; - this.behavior = behavior; this.level = level; createSprite({ x: this.x, y: this.y, + width: this.width, + height: this.height, scale: this.scale, imgSrc: saw2, }).then((sprite) => (this.sprite = sprite)); } update() { - this.moveDistance(this.behavior, this.distance); - } - moveDistance(behavior, distance) { - let axis = ''; - let multiplier = 1; - switch (behavior) { - case UP_DOWN: - axis = 'y'; - break; - case BACK_FORTH: - axis = 'x'; - break; - } - - switch (this.direction) { - case 'n': - multiplier = -1; - break; - case 's': - multiplier = 1; - break; - case 'e': - multiplier = 1; - if (this.orgX + distance < this.x) { - this.direction = 'w'; - } - break; - case 'w': - multiplier = -1; - if (this.orgX - distance > this.x) { - this.direction = 'e'; - } - break; - } + const { axis, newDirection, multiplier } = moveBehavior({ + behavior: this.behavior, + distance: this.distance, + direction: this.direction, + x: this.x, + y: this.y, + orgX: this.orgX, + orgY: this.orgY, + }); + this.direction = newDirection; this[axis] += this.speed * multiplier; this.sprite.x = this.x; this.sprite.y = this.y; diff --git a/src/VerletNode.js b/src/VerletNode.js index 4a96e4f..db7bae9 100644 --- a/src/VerletNode.js +++ b/src/VerletNode.js @@ -5,6 +5,7 @@ export class VerletNode { oldPos; width = 2; height = 2; + mass = 0.03; constructor({ x, y }) { this.pos = Vector(x, y); this.oldPos = Vector(x, y); diff --git a/src/arcadianApi.js b/src/arcadianApi.js index be4a4ab..2456e71 100644 --- a/src/arcadianApi.js +++ b/src/arcadianApi.js @@ -47,6 +47,8 @@ export async function queryArcadian(id) { export const fetchArcadianHeads = () => { return new Promise((resolve) => { const promises = []; + promises.push(queryArcadian(92)); + promises.push(queryArcadian(101)); for (let i = 1; i < 46; i++) { if (i === 2 || i === 13) continue; const promise = queryArcadian(i); diff --git a/src/behavior.js b/src/behavior.js new file mode 100644 index 0000000..c5c887d --- /dev/null +++ b/src/behavior.js @@ -0,0 +1,56 @@ +export const BACK_FORTH = 'ew'; +export const UP_DOWN = 'ns'; + +export const moveBehavior = ({ + behavior, + direction, + distance, + orgX, + orgY, + x, + y, +}) => { + let axis = ''; + let multiplier = 1; + let newDirection = direction; + switch (behavior) { + case UP_DOWN: + axis = 'y'; + break; + case BACK_FORTH: + axis = 'x'; + break; + } + + switch (direction) { + case 'n': + multiplier = -1; + break; + case 's': + multiplier = 1; + break; + case 'e': + multiplier = 1; + if (orgX + distance < x) { + newDirection = 'w'; + } + break; + case 'w': + multiplier = -1; + if (orgX - distance > x) { + newDirection = 'e'; + } + break; + } + return { axis, multiplier, newDirection }; +}; + +export const getDirection = (behavior, distance) => { + let direction = ''; + if (distance < 0) { + direction = behavior === BACK_FORTH ? 'w' : 'n'; + } else { + direction = behavior === BACK_FORTH ? 'e' : 's'; + } + return direction; +}; diff --git a/src/constants.js b/src/constants.js index 1fee028..8c56d60 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,7 +1,8 @@ -export const gravity = 1.1; +export const gravity = 9; export const bgc = '#211e20'; export const bgc2 = '#555568'; export const fgc = '#a0a08b'; export const fgc2 = '#e9efec'; +export const acc = '#e20fa0'; export const RESTING_DISTANCE = 7; diff --git a/src/level/level4.json b/src/level/level4.json new file mode 100644 index 0000000..ae0167a --- /dev/null +++ b/src/level/level4.json @@ -0,0 +1,17 @@ +{ + "p": { "x": 272, "y": 255 }, + "r": 30, + "s": [ + { "x": 300, "y": 400, "b": "ew", "d": 100 }, + { "x": 464, "y": 350, "b": "ew", "d": -200 }, + { "x": 100, "y": 450, "b": "ew", "d": 100 } + ], + "b": [ + { "x": 260, "y": 220 }, + { "x": 460, "y": 290 }, + { "x": 460, "y": 322 }, + { "x": 460, "y": 354 } + ], + "g": [{ "x": 600, "y": 700 }], + "h": [{ "x": 200, "y": 400 }] +} diff --git a/src/level/level5.json b/src/level/level5.json new file mode 100644 index 0000000..d773a6a --- /dev/null +++ b/src/level/level5.json @@ -0,0 +1,12 @@ +{ + "p": { "x": 272, "y": 255, "b": "ew", "d": 100 }, + "r": 30, + "s": [{ "x": 100, "y": 450, "b": "ew", "d": 100 }], + "b": [ + { "x": 260, "y": 220, "b": "ew", "d": 100 }, + { "x": 460, "y": 320, "b": "ew", "d": 170 }, + { "x": 600, "y": 520, "b": "ew", "d": 80 } + ], + "g": [{ "x": 600, "y": 700 }], + "h": [{ "x": 200, "y": 400 }] +} diff --git a/src/sawBehavior.js b/src/sawBehavior.js deleted file mode 100644 index dd41ffd..0000000 --- a/src/sawBehavior.js +++ /dev/null @@ -1,2 +0,0 @@ -export const BACK_FORTH = 'ew'; -export const UP_DOWN = 'ns'; diff --git a/src/utils.js b/src/utils.js index da99950..7149323 100644 --- a/src/utils.js +++ b/src/utils.js @@ -23,8 +23,8 @@ export const lineIntersection = (p1, p2, p3, p4) => { if (v < 0.0 || v > 1.0) return null; // intersection point not between p3 and p4 const intersectionX = p1.x + u * (p2.x - p1.x); const intersectionY = p1.y + u * (p2.y - p1.y); - let intersection = Vector(intersectionX, intersectionY); - return intersection; + if (Number.isNaN(intersectionX) || Number.isNaN(intersectionY)) return null; + return Vector(intersectionX, intersectionY); }; export const doesOwnNft = (seriesId, nftTokensForOwner) => {