Skip to content

Instantly share code, notes, and snippets.

@timetocode
Created June 13, 2016 02:50
Show Gist options
  • Save timetocode/02f46f155bbb582afe293ec580744757 to your computer and use it in GitHub Desktop.
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.
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)
})
})
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