Created
June 13, 2016 02:50
-
-
Save timetocode/02f46f155bbb582afe293ec580744757 to your computer and use it in GitHub Desktop.
Example of the in-progress development of a projectile system paired with unit tests.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Projectile = require('../Projectile') | |
var Vector2 = require('../../../nengi/Vector2') | |
var Map = require('../../map/Map') | |
var WallType = require('../../map/WallType') | |
describe('Projectile', function() { | |
var projectile = null | |
var mockClient = null | |
var mockTarget = null | |
var mockWeapon = null | |
var map = null | |
beforeEach(function() { | |
mockClient = { | |
connection: null, | |
entity: { id: 1, x: 0, y: 0 } | |
} | |
mockTarget = { id: 2, x: 50, y: 50 } | |
mockWeapon = { | |
projectileSpeed: 200, | |
projectileDurability: 1, | |
maxDistance: 1000, | |
damage: 20 | |
} | |
map = new Map(25, 25, 25) | |
projectile = new Projectile( | |
mockClient, | |
mockTarget, | |
mockWeapon, | |
map | |
) | |
}) | |
afterEach(function() { | |
projectile = null | |
}) | |
it('moves until max distance is reached', function() { | |
// enough time for the projectile to travel very far | |
projectile.update(9999) | |
// should only have travelled max distance, +/- float accuracy issues | |
expect(projectile.distanceTravelled).toBeCloseTo(projectile.weapon.maxDistance, 9) | |
}) | |
// this may be a bad test b/c it is so similar to the code that it tests | |
it('travels at weapon.projectileSpeed', function() { | |
var speed = mockWeapon.projectileSpeed | |
var delta = 60/1000 | |
// move the projectile by one delta | |
projectile.update(delta) | |
// estimate the result | |
var expectedX = 0 | |
var expectedY = 0 | |
var travelDirection = new Vector2( | |
mockTarget.x - mockClient.entity.x, | |
mockTarget.y - mockClient.entity.y | |
) | |
travelDirection.normalize() | |
expectedX = travelDirection.x * speed * delta | |
expectedY = travelDirection.y * speed * delta | |
expect(projectile.x).toEqual(expectedX) | |
expect(projectile.y).toEqual(expectedY) | |
}) | |
it('generates a line segment travelled per frame', function() { | |
var speed = mockWeapon.projectileSpeed | |
var delta = 60/1000 | |
var expectedX1 = 0 | |
var expectedY1 = 0 | |
var expectedX2 = 0 | |
var expectedY2 = 0 | |
// move 10 frames and check the path of the projectile each time | |
for (var i = 0; i < 10; i++) { | |
// move the projectile by one delta | |
projectile.update(delta) | |
var lineSegment = projectile.toLineSegment() | |
// estimate the result | |
var travelDirection = new Vector2( | |
mockTarget.x - mockClient.entity.x, | |
mockTarget.y - mockClient.entity.y | |
) | |
travelDirection.normalize() | |
expectedX1 = expectedX2 | |
expectedY1 = expectedY2 | |
expectedX2 += travelDirection.x * speed * delta | |
expectedY2 += travelDirection.y * speed * delta | |
expect(lineSegment.x1).toEqual(expectedX1) | |
expect(lineSegment.y1).toEqual(expectedY1) | |
expect(lineSegment.x2).toEqual(expectedX2) | |
expect(lineSegment.y2).toEqual(expectedY2) | |
} | |
}) | |
it('is stopped by wall if the wall is stronger than the projectile', function() { | |
map.setWall(1, 1, WallType.NanoSteel) | |
projectile.update(999) | |
// projectile should stop at the the corner of the wall [25, 25] | |
expect(projectile.x).toEqual(25) | |
expect(projectile.y).toEqual(25) | |
}) | |
it('destroys wall and continues on if projectile is stronger than wall', function() { | |
projectile.durability = 999 | |
map.setWall(1, 1, WallType.NanoSteel) | |
projectile.update(999) | |
// will travel max distance, blew right through the wall | |
expect(projectile.distanceTravelled).toBeCloseTo(projectile.weapon.maxDistance, 9) | |
// expect the wall at [1, 1] to be destroyed | |
expect(map.getWall(1, 1).type).toBe(WallType.None) | |
}) | |
it('will double KO a wall of same durability', function() { | |
projectile.durability = 20 | |
map.setWall(1, 1, WallType.NanoSteel) // NanoSteel has 20 durability | |
projectile.update(999) | |
// projectile should stop at the the corner of the wall [25, 25] | |
expect(projectile.x).toEqual(25) | |
expect(projectile.y).toEqual(25) | |
expect(projectile.durability).toEqual(0) | |
// expect the wall at [1, 1] to be destroyed also | |
expect(map.getWall(1, 1).type).toBe(WallType.None) | |
}) | |
// this test is disabled | |
xit('will hit an entity', function() { | |
var mockEntity = { | |
id: 1234, | |
hp: 200, | |
x: 25, | |
y: 25, | |
radius: 10 | |
} | |
mockClient.ping = 100 | |
mockInstance.addEntity(entity) | |
projectile.instance = mockInstance | |
// advance the simulation enough frames that there is a history | |
mockInstance.update(1/20) | |
mockInstance.update(1/20) | |
mockInstance.update(1/20) | |
mockInstance.update(1/20) | |
mockInstance.update(1/20) | |
// move the projectile | |
projectile.update(20) | |
// the entity should be missing one hit worth of damage | |
expect(mockEntity.hp).toEqual(200 - projectile.weapon.damage) | |
}) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Vector2 = require('../../nengi/Vector2') | |
function Projectile(client, targetPoint, weapon, map) { | |
this.client = client | |
this.weapon = weapon | |
this.map = map | |
/* setup projectile position, which initially is the client.entity's position */ | |
// position | |
this.x = client.entity.x | |
this.y = client.entity.y | |
// start point | |
this.originalX = this.x | |
this.originalY = this.y | |
// position one frame ago | |
this.previousX = this.x | |
this.previousY = this.y | |
// initial direction of travel | |
this.direction = new Vector2(targetPoint.x - this.x, targetPoint.y - this.y) | |
this.direction.normalize() | |
this.distanceTravelled = 0 | |
this.durability = weapon.projectileDurability | |
} | |
Projectile.prototype.update = function(delta) { | |
// save previous position | |
this.previousX = this.x | |
this.previousY = this.y | |
// then move | |
this.x += this.direction.x * this.weapon.projectileSpeed * delta | |
this.y += this.direction.y * this.weapon.projectileSpeed * delta | |
this.distanceTravelled = this._calculateDistanceTravelled() | |
// make sure projetile does not exceed max distance | |
if (this.distanceTravelled > this.weapon.maxDistance) { | |
this.direction.multiplyScalar(this.weapon.maxDistance) | |
this.x = this.direction.x | |
this.y = this.direction.y | |
this.distanceTravelled = this._calculateDistanceTravelled() | |
} | |
var lineSegment = this.toLineSegment() | |
var raycastVsWalls = this.map.raycast( | |
lineSegment.x1, lineSegment.y1, | |
lineSegment.x2, lineSegment.y2, | |
9999 | |
) | |
if (raycastVsWalls.hit) { | |
var wall = this.map.getWall(raycastVsWalls.gx, raycastVsWalls.gy) | |
var wallStrength = wall.durability | |
var projectileStrength = this.durability | |
var isWallDestroyed = this.map.damageWallAndReturnTrueIfDestroyed( | |
raycastVsWalls.gx, | |
raycastVsWalls.gy, | |
projectileStrength | |
) | |
// TODO: create a network message if the wall is destroyed | |
this.durability -= wallStrength | |
if (this.durability <= 0) { | |
// destroyed by impact with wall, stop moving | |
// TODO: remove projectile from game | |
this.x = raycastVsWalls.x | |
this.y = raycastVsWalls.y | |
} | |
} | |
//TODO: after the projectile's path has been affected by walls | |
// check for lagcompensated collisions against entities | |
/* pseudo code | |
var dx = Math.abs(lineSegment.x2 - lineSegment.x1) | |
var dy = Math.abs(lineSegment.y2 - lineSegment.y1) | |
var areaOfPotentialCollision = { | |
x: lineSegment.x1, | |
y: lineSegment.y1, | |
halfWidth: dx, | |
halfHeight: dy | |
} | |
// get anything near the projectile's path | |
var nearbyEntityProxies = this.historian.getLagCompensatedEntitiesInArea( | |
client.calculateTotalLatency(), | |
areaOfPotentialCollision | |
) | |
for (var i = 0; i < nearbyEntityProxies.length; i++) { | |
var nearbyEntityProxy = nearbyEntityProxies[i] | |
// did the projectile hit this entity? | |
var isCollision = lineSegmentVsCircleCollision( | |
lineSegment, { | |
x: nearbyEntityProxy.x, | |
nearbyEntityProxy.y, | |
radius: 10 | |
} | |
) | |
if (isCollision) { | |
var entity = this.instance.getEntity(nearbyEntityProxy.id) | |
// make sure entity exists, compensation references entities slightly | |
// in the past, it is possible they have been removed | |
if (entity) { | |
entity.hp -= this.weapon.calculateDamageAtDistance(this.distanceTravelled) | |
} | |
} | |
} | |
*/ | |
} | |
// returns the lineSegment of the projectiles path since update() waqs last called | |
Projectile.prototype.toLineSegment = function() { | |
return { | |
x1: this.previousX, | |
y1: this.previousY, | |
x2: this.x, | |
y2: this.y | |
} | |
} | |
Projectile.prototype._calculateDistanceTravelled = function() { | |
return new Vector2(this.originalX - this.x, this.originalY - this.y).length | |
} | |
module.exports = Projectile |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment