1
0
Fork 0

Compare commits

...

11 Commits

Author SHA1 Message Date
André Jaenisch abe72d6702
feat: add collision check 1 year ago
André Jaenisch f587abac3f
fix: tweak bounds a bit 1 year ago
André Jaenisch ee89d92d1a
feat: add collistion test 1 year ago
André Jaenisch 5f88768fd0
feat: add boundary test 1 year ago
André Jaenisch c0c6509428
refactor: import world object factories in app 1 year ago
André Jaenisch fb74ae92fb
refactor: move world elements into dedicated module 1 year ago
André Jaenisch 62fedcba92
fix: clear rect on every frame 1 year ago
André Jaenisch 1763fda79a
feat: apply gravity to objects 1 year ago
André Jaenisch a5dd1755d9
feat: add new function to rotate a vector 1 year ago
André Jaenisch b172a79b00
feat: add new function to compute face normals 1 year ago
André Jaenisch ba978bc381
feat: declare FPS as constant 1 year ago
  1. BIN
      js13kgames.zip
  2. 65
      src/js/app.js
  3. 92
      src/js/collisions.js
  4. 1
      src/js/constants.js
  5. 57
      src/js/draw.js
  6. 16
      src/js/shape.js
  7. 18
      src/js/vector.js
  8. 61
      src/js/world.js
  9. 57
      test/js/collisions.test.js
  10. 37
      test/js/shape.test.js
  11. 17
      test/js/vector.test.js
  12. 36
      test/js/world.test.js
  13. 20
      types/collisions.d.ts
  14. 1
      types/constants.d.ts
  15. 2
      types/draw.d.ts
  16. 6
      types/shape.d.ts
  17. 8
      types/vector.d.ts
  18. 24
      types/world.d.ts

BIN
js13kgames.zip

Binary file not shown.

65
src/js/app.js

@ -1,13 +1,17 @@
import { testBoundaries, testCollision } from './collisions.js'
import { drawShape } from './draw.js'
import { RigidShape } from './shape.js'
import { Vec2 } from './vector.js'
import { makeAstronaut, makeBottomBoundary } from './world.js'
/** @typedef {import('./shape').Shape} Shape */
// Global on purpose
/** @type {Shape} */
let astronaut
/** @type {CanvasRenderingContext2D | null} */
/** @type {Shape} */
let bottomBoundary
/** @type {HTMLCanvasElement} */
let canvas
/** @type {CanvasRenderingContext2D} */
let context
/**
@ -22,52 +26,43 @@ export function app () {
return
}
context = /** @type {HTMLCanvasElement} */(game).getContext('2d')
canvas = /** @type {HTMLCanvasElement} */(game)
const ctx = canvas.getContext('2d')
if (!context) {
if (!ctx) {
console.error('Could not start game!')
return
}
context = ctx
astronaut = makeAstronaut()
tick()
}
/**
* Creates player avatar.
*
* @returns {Shape}
*/
function makeAstronaut () {
const center = Vec2(200, 200)
const friction = 20
const restitution = 0
const mass = 400
const bounds = 1
const width = 20
const height = 20
const shape = RigidShape({
center,
mass,
friction,
restitution,
bounds,
width,
height
const boundaryHeight = 2
bottomBoundary = makeBottomBoundary({
x: 0,
y: canvas.height - boundaryHeight,
height: boundaryHeight,
width: canvas.width
})
return shape
tick()
}
/* TODO: Add some paint-FPS to UI */
// See https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame#notes
function tick () {
// Impossible state
if (!context) {
return
context.clearRect(0, 0, canvas.width, canvas.height)
drawShape(context, bottomBoundary)
drawShape(context, astronaut)
if (testBoundaries(bottomBoundary, astronaut)) {
if (testCollision(bottomBoundary, astronaut)) {
throw new Error('Game Over!')
}
}
if (astronaut.C.y - astronaut.H > canvas.height) {
throw new Error('Should have been caught earlier!')
}
drawShape(context, astronaut)
window.requestAnimationFrame(tick)
}

92
src/js/collisions.js

@ -0,0 +1,92 @@
import { determinant, length, subtract } from '../../src/js/vector.js'
/** @typedef {import('./shape').Shape} Shape */
/** @typedef {import('./vector').Vector2D} Vector2D */
/**
* Test two shapes on intersecting boundary circles (since rotation).
*
* @param {Shape} shape1
* @param {Shape} shape2
* @returns {boolean}
*/
export function testBoundaries (shape1, shape2) {
return length(subtract(shape2.C, shape1.C)) <= shape2.B + shape1.B
}
/**
* Test two shapes on intersection.
*
* @param {Shape} shape1
* @param {Shape} shape2
* @returns {boolean}
*/
export function testCollision (shape1, shape2) {
const vertices1 = getVertices(shape1)
const vertices2 = getVertices(shape2)
try {
vertices1.forEach(function (vertex1) {
vertices2.forEach(function (vertex2) {
const vertices = [...vertex1, ...vertex2]
if (testIntersection(vertices)) {
console.debug(shape1, shape2, vertices)
throw new Error('Intersection!')
}
})
})
} catch (_) {
// Discovered an intersection!
return true
}
return false
}
/**
* Computes all vertices as point-direction pairs.
*
* @param {Shape} shape
* @returns {Array<Array<Vector2D>>}
*/
function getVertices (shape) {
return [
[
shape.X[0],
subtract(shape.X[0], shape.X[1])
],
[
shape.X[0],
subtract(shape.X[0], shape.X[3])
],
[
shape.X[2],
subtract(shape.X[2], shape.X[1])
],
[
shape.X[2],
subtract(shape.X[2], shape.X[3])
]
]
}
/**
* Checks for intersection of two lines given by v1 and v2, resp. v3 and v4.
*
* @param {Array<Vector2D>} vertices
* @returns {boolean}
* @see {@link https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment}
*/
function testIntersection ([v1, v2, v3, v4]) {
const tdivident = determinant(subtract(v1, v3), subtract(v3, v4))
const tdivisor = determinant(subtract(v1, v2), subtract(v3, v4))
const t = tdivident / tdivisor
const udivident = determinant(subtract(v2, v1), subtract(v1, v3))
const udivisor = determinant(subtract(v1, v2), subtract(v3, v4))
const u = udivident / udivisor
console.debug(t, u)
return (t <= 0 && t <= 1) || (u <= 0 && u <= 1)
}

1
src/js/constants.js

@ -1,3 +1,4 @@
import { Vec2 } from './vector.js'
export const FPS = 60
export const gravity = Vec2(0, 100)

57
src/js/draw.js

@ -1,4 +1,9 @@
import { FPS } from './constants.js'
import { computeNormals } from './shape.js'
import { add, rotate, scale } from './vector.js'
/** @typedef {import('./shape').Shape} Shape */
/** @typedef {import('./vector').Vector2D} Vector2D */
/**
* Draws a shape onto the canvas.
@ -11,6 +16,9 @@ export function drawShape (context, shape) {
prepareCanvas(context, shape)
draw(context, shape)
context.restore()
updatePosition(shape)
updateRotation(shape)
}
/**
@ -36,3 +44,52 @@ function draw (context, shape) {
const { W, H } = shape
context.strokeRect(-W / 2, -H / 2, W, H)
}
/**
* Update the position of the shape.
*
* @param {Shape} shape
*/
function updatePosition (shape) {
shape.V = add(shape.V, scale(shape.A, 1 / FPS))
moveShape(shape, scale(shape.V, 1 / FPS))
}
/**
* Move a shape along a vector.
*
* @param {Shape} shape
* @param {Vector2D} v
*/
function moveShape (shape, v) {
shape.C = add(shape.C, v)
shape.X.forEach(function (vertex, index) {
shape.X[index] = add(vertex, v)
})
}
/**
* Update the rotation of the shape.
*
* @param {Shape} shape
*/
function updateRotation (shape) {
shape.v = shape.v + shape.a * 1 / FPS
rotateShape(shape, shape.v * 1 / FPS)
}
/**
* Rotate a shape around its centre.
*
* @param {Shape} shape
* @param {number} angle
*/
function rotateShape (shape, angle) {
shape.G = shape.G + angle
shape.X.forEach(function (vertex, index) {
shape.X[index] = rotate(vertex, shape.C, angle)
computeNormals(shape)
})
}

16
src/js/shape.js

@ -1,5 +1,5 @@
import { gravity } from './constants.js'
import { Vec2 } from './vector.js'
import { normalize, subtract, Vec2 } from './vector.js'
/** @typedef {import('./vector.js').Vector2D} Vector2D
@ -67,5 +67,19 @@ export function RigidShape ({
]
}
computeNormals(shape)
return shape
}
/**
* Compute face normals
*
* @param {Shape} shape
*/
export function computeNormals (shape) {
shape.N.forEach(function (normal, index) {
shape.N[index] = normalize(
subtract(shape.X[(index + 1) % 4], shape.X[(index + 2) % 4])
)
})
}

18
src/js/vector.js

@ -100,3 +100,21 @@ export function distance (v, w) {
export function normalize (v) {
return scale(v, 1 / (length(v) || 1))
}
/**
* Rotate a vector around a given centre for a given angle in radians.
*
* @param {Vector2D} v
* @param {Vector2D} centre
* @param {number} angle
*/
export function rotate (v, centre, angle) {
const { cos, sin } = Math
const x = v.x - centre.x
const y = v.y - centre.y
return Vec2(
x * cos(angle) - y * sin(angle) + centre.x,
x * sin(angle) + y * cos(angle) + centre.y
)
}

61
src/js/world.js

@ -0,0 +1,61 @@
import { RigidShape } from './shape.js'
import { Vec2 } from './vector.js'
/** @typedef {import('./shape').Shape} Shape */
/**
* Creates player avatar.
*
* @returns {Shape}
*/
export function makeAstronaut () {
const center = Vec2(200, 200)
const friction = 20
const restitution = 0
const mass = 400
const bounds = 20
const width = 20
const height = 20
const shape = RigidShape({
center,
mass,
friction,
restitution,
bounds,
width,
height
})
return shape
}
/**
* Creates a boundary at the bottom of the screen.
*
* @param {object} config
* @param {number} config.x
* @param {number} config.y
* @param {number} config.height
* @param {number} config.width
* @returns {Shape}
*/
export function makeBottomBoundary ({ x, y, height, width }) {
const center = Vec2(x + width / 2, y + height / 2)
const friction = 20
const restitution = 0
const mass = 0
const bounds = 25
const shape = RigidShape({
center,
mass,
friction,
restitution,
bounds,
width,
height
})
return shape
}

57
test/js/collisions.test.js

@ -0,0 +1,57 @@
import { expect } from 'chai'
import { testBoundaries, testCollision } from '../../src/js/collisions.js'
import { RigidShape } from '../../src/js/shape.js'
import { Vec2 } from '../../src/js/vector.js'
describe('Collisions', function () {
describe('testBoundaries', function () {
it('should test the boundaries of two shapes for intersection', function () {
// Arrange
const shape1 = makeShape()
const shape2 = makeShape()
// Act
const haveIntersectingBoundaries = testBoundaries(shape1, shape2)
// Assert
expect(haveIntersectingBoundaries).to.equal(true)
})
})
describe('testCollision', function () {
it('should test in depth for collisions', function () {
// Arrange
const shape1 = makeShape()
const shape2 = makeShape()
// Act
const haveCollided = testCollision(shape1, shape2)
// Assert
expect(haveCollided).to.equal(true)
})
})
})
function makeShape () {
const center = Vec2(500, 200)
const friction = 20
const restitution = 0
const mass = 400
const bounds = 1
const width = 5
const height = 5
const shape = RigidShape({
center,
mass,
friction,
restitution,
bounds,
width,
height
})
return shape
}

37
test/js/shape.test.js

@ -1,6 +1,6 @@
import { expect } from 'chai'
import { RigidShape } from '../../src/js/shape.js'
import { computeNormals, RigidShape } from '../../src/js/shape.js'
import { Vec2 } from '../../src/js/vector.js'
describe('RigidShape', function () {
@ -28,4 +28,39 @@ describe('RigidShape', function () {
// Assert
expect(shape).to.be.an('object')
})
describe('computeNormals', function () {
it('should compute the face normals of a shape', function () {
// Arrange
const center = Vec2(500, 200)
const friction = 20
const restitution = 0
const mass = 400
const bounds = 1
const width = 5
const height = 5
const shape = RigidShape({
center,
mass,
friction,
restitution,
bounds,
width,
height
})
const originalFaces = shape.N.map(function (face) {
return {...face}
})
// Act
computeNormals(shape)
// Assert
shape.N.forEach(function (face, index) {
const originalFace = originalFaces[index]
expect(face).to.not.deep.equal(originalFace)
})
})
})
})

17
test/js/vector.test.js

@ -7,6 +7,7 @@ import {
dot,
length,
normalize,
rotate,
scale,
subtract,
Vec2,
@ -140,4 +141,20 @@ describe('Vector library', function () {
expect(normalized.y).to.be.approximately(0.8, 0.001)
})
})
describe('rotate', function () {
it('should rotate a shape around its centre on given angle', function () {
// Arrange
const v = Vec2(1, 0)
const c = Vec2(0, 0)
const angle = Math.PI
// Act
const rotated = rotate(v, c, angle)
// Assert
expect(rotated.x).to.be.approximately(-1, 0.001)
expect(rotated.y).to.be.approximately(0, 0.001)
})
})
})

36
test/js/world.test.js

@ -0,0 +1,36 @@
import { expect } from 'chai'
import { Vec2 } from '../../src/js/vector.js'
import { makeAstronaut, makeBottomBoundary } from '../../src/js/world.js'
describe('World', function () {
describe('makeAstronaut', function () {
it('should make an astronaut shape', function () {
// Arrange
/* nothing to do here */
// Act
const astronaut = makeAstronaut()
// Assert
expect(astronaut).to.be.an('object')
})
})
describe('makeBottomBoundary', function () {
it('should make an immobil shape', function () {
// Arrange
const x = 0
const y = 100
const height = 1
const width = 200
// Act
const boundary = makeBottomBoundary({ x, y, height, width })
// Assert
expect(boundary.X[0]).to.deep.equal(Vec2(x, y))
expect(boundary.X[2]).to.deep.equal(Vec2(x + width, y + height))
})
})
})

20
types/collisions.d.ts vendored

@ -0,0 +1,20 @@
/** @typedef {import('./shape').Shape} Shape */
/** @typedef {import('./vector').Vector2D} Vector2D */
/**
* Test two shapes on intersecting boundary circles (since rotation).
*
* @param {Shape} shape1
* @param {Shape} shape2
* @returns {boolean}
*/
export function testBoundaries(shape1: Shape, shape2: Shape): boolean;
/**
* Test two shapes on intersection.
*
* @param {Shape} shape1
* @param {Shape} shape2
* @returns {boolean}
*/
export function testCollision(shape1: Shape, shape2: Shape): boolean;
export type Shape = import('./shape').Shape;
export type Vector2D = import('./vector').Vector2D;

1
types/constants.d.ts vendored

@ -1 +1,2 @@
export const FPS: 60;
export const gravity: import("./vector.js").Vector2D;

2
types/draw.d.ts vendored

@ -1,4 +1,5 @@
/** @typedef {import('./shape').Shape} Shape */
/** @typedef {import('./vector').Vector2D} Vector2D */
/**
* Draws a shape onto the canvas.
*
@ -7,3 +8,4 @@
*/
export function drawShape(context: CanvasRenderingContext2D, shape: Shape): void;
export type Shape = import('./shape').Shape;
export type Vector2D = import('./vector').Vector2D;

6
types/shape.d.ts vendored

@ -39,6 +39,12 @@ export function RigidShape({ center, mass, friction, restitution, bounds, width,
width: number;
height: number;
}): Shape;
/**
* Compute face normals
*
* @param {Shape} shape
*/
export function computeNormals(shape: Shape): void;
/**
* /**
*/

8
types/vector.d.ts vendored

@ -73,6 +73,14 @@ export function distance(v: Vector2D, w: Vector2D): number;
* @returns {Vector2D}
*/
export function normalize(v: Vector2D): Vector2D;
/**
* Rotate a vector around a given centre for a given angle in radians.
*
* @param {Vector2D} v
* @param {Vector2D} centre
* @param {number} angle
*/
export function rotate(v: Vector2D, centre: Vector2D, angle: number): Vector2D;
export type Vector2D = {
x: number;
y: number;

24
types/world.d.ts vendored

@ -0,0 +1,24 @@
/** @typedef {import('./shape').Shape} Shape */
/**
* Creates player avatar.
*
* @returns {Shape}
*/
export function makeAstronaut(): Shape;
/**
* Creates a boundary at the bottom of the screen.
*
* @param {object} config
* @param {number} config.x
* @param {number} config.y
* @param {number} config.height
* @param {number} config.width
* @returns {Shape}
*/
export function makeBottomBoundary({ x, y, height, width }: {
x: number;
y: number;
height: number;
width: number;
}): Shape;
export type Shape = import('./shape').Shape;
Loading…
Cancel
Save