Skip to content

Instantly share code, notes, and snippets.

@ggoodman
Created September 6, 2011 20:56
Show Gist options
  • Save ggoodman/1198937 to your computer and use it in GitHub Desktop.
Save ggoodman/1198937 to your computer and use it in GitHub Desktop.
Slamikaze html5 game proof of concept

SLAMIKAZE

With your guns disabled and no other means of fighting back, your hopes of survival are all but lost. The enemy are savage, suicidal beasts who will follow you blindly to the furthest reaches of the universe.

Only one hope remains; if you are quick and clever, there might just be a chance... good luck.

How to play

  • Your ship (blue square) follows your mouse cursor closely
  • Enemy ships (red squares) follow your mouse cursor as fast as their weaker ships allow
  • Avoid enemy ships while causing them to collide in order to flee to safety

Try it now!

With the help of http://bl.ocks.org, you can easily try this proof of concept without any setup required.

[http://bl.ocks.org/1198937](TRY ME)

About

Slamikaze was made as a proof of concept and as a testing bed for a little coffee-script graphics engine for use with the html5 canvas element.

<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<title>Slamikaze</title>
<style type="text/css">
html, body {
width: 100%;
}
body { margin:0;padding:0;}
#canvas {
background-color: black;
}
#fps {
position: absolute;
font-family: monospace;
top: 0;
left: 0;
font-weight: bold;
color: white;
}
#debug {
position: absolute;
font-family: monospace;
bottom: 0;
left: 0;
right: 0;
color: white;
}
</style>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script type="text/javascript" src="http://coffeescript.org/extras/coffee-script.js"></script>
<script type="text/coffeescript" src="limber.coffee"></script>
<script type="text/coffeescript" src="slamikaze.coffee"></script>
</head>
<body>
<div id="fps"></div>
<pre id="debug"></pre>
<canvas id="canvas"></canvas>
</body>
</html>
(window or exports).limber = limber =
version: "0.0.2"
geometry: {}
component: {}
entity: {}
trait: {}
engine: {}
#convenience methods
vector: (x, y) -> new limber.geometry.Vector(x, y)
box: (x0, y0, x1, y1) -> new limber.geometry.Box(x0, y0, x1, y1)
class limber.EventEmitter
on: (event, cb) ->
@listeners ||= {}
@listeners[event] ||= []
@listeners[event].push(cb)
@
echo: (emitter, event, asEvent) ->
emitter.on event, @trigger(asEvent or event)
emit: (event, args...) ->
@listeners ||= {}
cb.apply(this, args) for cb in @listeners[event] when cb if @listeners[event]
@
removeListener: (event, cb) ->
@listeners ||= {}
if listeners = @listeners[event]
index = listeners.indexOf(cb)
unless index == -1
listeners[index] = null
listeners.splice(index, 1)
@
removeAllListeners: ->
@listeners[i] = null for i in @listeners if @listeners
@
trigger: (event) ->
self = this
(args...) -> self.emit(event, args...)
class limber.Timer
constructor: ->
@lastTick = @currTick = @+ new Date
tick: ->
[@currTick, @lastTick] = [+ new Date, @currTick]
@delta = @currTick - @lastTick
#-----------#
# COMPONENT #
#-----------#
# limber.Component is the basic component used in the engine
class limber.component.Component extends limber.EventEmitter
attach: (component) ->
@children ||= []
@children.push(component)
@emit "attach", component
component.emit("attached", this)
@
attachTo: (component) ->
component.attach(this)
@
detach: (component) ->
@children ||= []
unless -1 == (index = @children.indexOf(component))
@emit "detach", component.emit("detached", this)
@children[index] = null
@children.splice(index, 1)
@
class limber.component.FPS extends limber.component.Component
constructor: (id) ->
@el = document.getElementById(id)
@size = 20
@index = 0
@sum = 0
@sample = []
@sample[i] = 0 for i in [0 ... @size]
@on "attached", (component) ->
component.on "update", @update
component.on "render", @render
update: (engine) =>
@sum -= @sample[@index]
@sum += engine.timer.delta
@sample[@index] = engine.timer.delta
@index += 1
@index = 0 if @index == @size
render: (engine) =>
@el.innerHTML = Math.floor(1000 / (@sum / @size)) + " FPS"
#--------- #
# GEOMETRY #
#----------#
class limber.geometry.Vector
constructor: (x, y) ->
if x instanceof limber.geometry.Vector
@x = x.x
@y = x.y
else if Array.isArray(x)
@x = x[0]
@y = x[1]
else
@x = x or 0
@y = y or 0
clone: -> new limber.geometry.Vector(this)
reset: ->
@x = 0
@y = 0
@
scale: (scalar) ->
@x *= scalar
@y *= scalar
@
shrink: (scalar) ->
@x /= scalar
@y /= scalar
@
product: (x, y) ->
vector = new limber.geometry.Vector(x, y)
@x *= vector.x
@y *= vector.y
@
add: (x, y) ->
vector = new limber.geometry.Vector(x, y)
@x += vector.x
@y += vector.y
@
subtract: (x, y) ->
vector = new limber.geometry.Vector(x, y)
@x -= vector.x
@y -= vector.y
@
normalize: ->
if @x == 0 and @y
@y = if @y > 0 then 1 else -1
else if @y == 0 and @y
@x = if @x > 0 then 1 else -1
else if (magnitude = @magnitude())
@x = @x / magnitude
@y = @y / magnitude
@
reflect: (x, y) ->
normal = new limber.geometry.Vector(x, y).normalize().rotateRight()
@add(normal.scale(-2 * @dot(normal)))
@
dot: (x, y) ->
if x instanceof limber.geometry.Vector then return @x * x.x + @y * x.y
else return @x * x + @y * y
magnitude: -> Math.sqrt(@x * @x + @y * @y)
magnitudeSq: -> @x * @x + @y * @y
flipX: ->
@x = - @x
@
flipY: ->
@y = - @y
@
flip: ->
@x = - @x
@y = - @y
@
rotateRight: ->
[@x, @y] = [@y, - @x]
@
rotateLeft: ->
[@x, @y] = [- @y, @x]
@
class limber.geometry.Projection
constructor: (min, max) ->
if min instanceof limber.geometry.Projection
@min = min.min
@max = min.max
else
@min = min
@max = max
overlap: (x, y) ->
proj = new limber.geometry.Projection(x, y)
overlap = 0
unless @max < proj.min or @min > proj.max
if @min > proj.min and @min < proj.max then overlap = proj.max - @min
else if @max < proj.max and @max > proj.min then overlap = proj.min - @max
else
test = proj.max - proj.min
overlap = @max - @min
overlap = test if test < overlap
overlap = Number.MAX_VALUE if overlap is Number.POSITIVE_INFINITY
overlap = -Number.MAX_VALUE if overlap is Number.NEGATIVE_INFINITY
#proj = null
overlap
class limber.geometry.MTV
constructor: (axis, @overlap) ->
@axis = limber.vector(axis)
flip: ->
@axis.flip()
@
class limber.geometry.ConvexHull
constructor: (vertices...) ->
@vertices = for vertex in vertices then limber.vector(vertex)
add: (x, y) ->
vertex.add(x, y) for vertex in @vertices
@
subtract: (x, y) ->
vertex.subtract(x, y) for vertex in @vertices
@
testCollision: (convex) ->
return @testPolygonPolygon(convex)
test = "test"
test +=
if this instanceof limber.geometry.Polygon then "Polygon"
else if this instanceof limber.geometry.Circle then "Circle"
test +=
if convex instanceof limber.geometry.Polygon then "Polygon"
else if convex instanceof limber.geometry.Circle then "Circle"
throw new Error("Collisions method not implemented: #{test}") unless this[test]
method = this[test]
method.call(this, convex)
testPolygonPolygon: (convex) ->
minOverlap = Number.POSITIVE_INFINITY
minAxis = null
multAxes = false
axes = @getFaceNormals()
axes.concat(convex.getFaceNormals()) unless this instanceof limber.geometry.AABB and convex instanceof limber.geometry.AABB
for axis in axes
proj1 = @projectOnto(axis)
proj2 = convex.projectOnto(axis)
overlap = proj1.overlap(proj2)
return unless overlap
if overlap == minOverlap
minAxis.add(axis)
multAxes = true
else if overlap < minOverlap
multAxes = false
minOverlap = overlap
minAxis = axis.clone()
minAxis.normalize() if multAxes
new limber.geometry.MTV(minAxis, minOverlap)
projectOnto: (x, y) ->
return new limber.geometry.Projection(0, 0) unless @vertices and @vertices.length
axis = limber.vector(x, y)
min = Number.POSITIVE_INFINITY
max = Number.NEGATIVE_INFINITY
for vertex in @vertices
d = axis.dot(vertex)
min = if d < min then d else min
max = if d > max then d else max
return new limber.geometry.Projection(min, max)
getFaceNormals: ->
@normals or @normals = for vertex, i in @vertices
@vertices[(i + 1) % @vertices.length].clone().subtract(vertex).rotateRight().normalize()
addNormal: (x, y) ->
@normals ||= []
@normals.push(limber.vector(x, y))
@
addVertex: (x, y) ->
@vertices ||= []
@vertices.push(limber.vector(x, y))
@
getVertices: -> @vertices or []
class limber.geometry.Polygon extends limber.geometry.ConvexHull
class limber.geometry.AABB extends limber.geometry.Polygon
constructor: (x0, y0, x1, y1) ->
if x0 instanceof limber.geometry.AABB
@bl = x0.bl.clone()
@tr = x0.tr.clone()
else if arguments.length == 2
@bl = new limber.geometry.Vector(x0)
@tr = new limber.geometry.Vector(y0)
else if arguments.length == 4
@bl = new limber.geometry.Vector(x0, y0)
@tr = new limber.geometry.Vector(x1, y1)
@addVertex(@bl.x, @bl.y)
@addVertex(@bl.x, @tr.y)
@addVertex(@tr.x, @tr.y)
@addVertex(@tr.x, @bl.y)
@normals = [limber.vector(1, 0), limber.vector(0, 1)]
clone: -> new limber.geometry.AABB(@bl, @tr)
add: (x, y) ->
@bl.add(x, y)
@tr.add(x, y)
super(x, y)
subtract: (x, y) ->
@bl.subtract(x, y)
@tr.subtract(x, y)
super(x, y)
getFaceNormals: -> @normals
toVector: -> @tr.clone().subtract(@bl)
class limber.geometry.Wall extends limber.geometry.AABB
constructor: (x, y, offset, yoff) ->
#NOTE: ONLY WORKS FOR AABB WALLS FOR NOW
if x
@addVertex(offset, -Number.MAX_VALUE)
@addVertex(offset, +Number.MAX_VALUE)
@addVertex(-x * Number.MAX_VALUE, yoff)
else
@addVertex(-Number.MAX_VALUE, offset)
@addVertex(+Number.MAX_VALUE, offset)
@addVertex(yoff, -y * Number.MAX_VALUE)
@addNormal(x, y)
normal = null
# Special case for Walls
testPolygonCircle: (circle) ->
class limber.geometry.Circle extends limber.geometry.ConvexHull
#--------#
# ENGINE #
#--------#
class limber.engine.Canvas2D extends limber.component.Component
constructor: (id, width, height) ->
@canvas = document.getElementById(id)
@canvas.addEventListener("mousemove", @onMouseMove, false)
#WebGL2D.enable(@canvas)
@canvas.width = width
@canvas.height = height
@context = @canvas.getContext("2d")
@timer = new limber.Timer
@mousePosition = limber.vector()
@context.translate(0, height)
@context.scale(1, -1)
animate: ->
renderer = this
canvas = @canvas
nextFrame = window.requestAnimationFrame or
window.webkitRequestAnimationFrame or
window.mozRequestAnimationFrame or
window.oRequestAnimationFrame or
window.msRequestAnimationFrame or
null
@timer.tick()
innerFrame = ->
renderer.timer.tick()
renderer.emit("update", renderer)
renderer.context.clearRect(0, 0, canvas.width, canvas.height)
renderer.emit("render", renderer)
if nextFrame != null
recursiveFrame = ->
innerFrame()
nextFrame(recursiveFrame)
nextFrame(recursiveFrame)
else
ONE_FRAME_TIME = 1000.0 / 60.0
setInterval(innerFrame, ONE_FRAME_TIME)
onMouseMove: (e) =>
@mousePosition =
if e.pageX or e.pageY then limber.vector(e.pageX, e.pageY)
else limber.vector(e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft, e.clientY + document.body.scrollTop + document.documentElement.scrollTop)
@mousePosition
.flipY().add(0, @canvas.height) # To switch to our coordinate scale
.subtract(@canvas.offsetLeft, @canvas.offsetTop)
#--------#
# ENTITY #
#--------#
class limber.entity.Entity extends limber.component.Component
constructor: (args...) ->
self = this
update = (args...) -> self.emit "update", args...
render = (args...) -> self.emit "render", args...
@components = []
@on "update", @update
@on "render", @render
@on "attached", (component) ->
component.on "update", update
component.on "render", render
self.components.push(component)
@on "detached", (component) ->
component.removeListener "update", update
component.removeListener "render", render
@initialize(args...)
destroy: ->
@emit "detached", component for component in @components
@emit "destroy", this
#@mixout(trait) for provide, trait of @traits if @traits
@removeAllListeners()
mixin: (trait) ->
provides = if Array.isArray(trait.provides) then trait.provides else [trait.provides]
requires = if Array.isArray(trait.requires) then trait.requires else [trait.requires]
@traits ||= {}
@requireTraits(requires)
trait.emit "mixin", this
trait.augment(this)
@traits[name] = trait for name in provides
@
mixout: (trait) ->
provides = if Array.isArray(trait.provides) then trait.provides else [trait.provides]
trait.emit "mixout", this
@traits ||= {}
for provide in provides
@traits[provide] = null
delete @traits[provide]
@
requireTraits: (requires...) ->
requires = requires[0] if Array.isArray(requires[0])
for name in requires
throw new Error("Missing required trait: #{name}") unless @traits[name]
@
attached: -> @
detached: -> @
initialize: -> @
update: (component) -> @
render: (component) -> @
#-------#
# TRAIT #
#-------#
class limber.trait.Trait extends limber.EventEmitter
provides: []
requires: []
constructor: (args...) ->
self = this
@initialize(args...)
destroy: ->
@removeAllListeners()
initialize: -> @
augment: -> @
class limber.trait.Position extends limber.trait.Trait
provides: "position"
initialize: (x, y) -> @position = limber.vector(x, y)
augment: (entity) -> entity.position = @position
class limber.trait.Velocity extends limber.trait.Trait
provides: "velocity"
requires: "position"
initialize: (x, y) -> @velocity = limber.vector(x, y)
augment: (entity) ->
entity.velocity = @velocity
entity.on "update", (engine) ->
@position.add @velocity.clone().scale(engine.timer.delta / 1000)
class limber.trait.Acceleration extends limber.trait.Trait
provides: "acceleration"
requires: "velocity"
initialize: (x, y) -> @acceleration = limber.vector(x, y)
augment: (entity) ->
entity.acceleration = @acceleration
entity.on "update", (engine) ->
@velocity.add @acceleration
@acceleration.reset()
class limber.trait.AABBBody extends limber.trait.Trait
provides: "body"
initialize: (x0, y0, x1, y1) -> @aabb = new limber.geometry.AABB(x0, y0, x1, y1)
augment: (entity) -> entity.body = @aabb
class limber.trait.Bounded extends limber.trait.Trait
requires: ["position", "body"]
constructor: (x0, y0, x1, y1) ->
boundaries = [
new limber.geometry.Wall(1, 0, x0, y1)
new limber.geometry.Wall(0, 1, y0, x1)
new limber.geometry.Wall(-1, 0, x1, y0)
new limber.geometry.Wall(0, -1, y1, x0)
]
self = this
@on "augment", (entity) ->
entity.on "update", (engine) ->
entity = @
bounds = entity.body.clone().add(entity.position)
for wall in boundaries
if mtv = bounds.testCollision(wall)
@emit "collision", engine, mtv, wall
class limber.trait.Wrapped extends limber.trait.Trait
requires: ["position", "body"]
initialize: (@x0, @y0, @x1, @y1) ->
augment: (entity) ->
self = this
entity.on "update", (engine) ->
if @position.x < self.x0 then @position.x = self.x1 - self.x0 - entity.position.x
else if @position.x > self.x1 then @position.x = self.x0 - self.x1 + entity.position.x
if @position.y < self.y0 then @position.y = self.y1 - self.y0 - entity.position.y
else if @position.y > self.y1 then @position.y = self.y0 - self.y1 + entity.position.x
@
class limber.trait.Comparator extends limber.trait.Trait
provides: "comparator"
initialize: -> @setup = false
augment: (entity) ->
self = this
entity.on "render", (engine) ->
self.seenInFrame = []
entity.emit "comparator:teardown", engine, this
entity.on "update", (engine) ->
self.seenInFrame or self.seenInFrame = []
entity.emit "comparator:setup", engine, this
entity.emit "comparator:compare", engine, this, other for other in self.seenInFrame
self.seenInFrame.push(this)
class limber.trait.CollisionDetection extends limber.trait.Trait
provides: "area"
requires: ["position", "body", "comparator"]
augment: (entity) ->
entity.on "comparator:setup", @setup
entity.on "comparator:compare", @compare
setup: (engine, entity) ->
entity.area = entity.body.clone().add(entity.position)
compare: (engine, entity, other) ->
if mtv = entity.area.testCollision(other.area)
entity.emit "collision", engine, mtv, other
other.emit "collision", engine, mtv.flip(), entity
class limber.trait.CollisionAvoidance extends limber.trait.Trait
requires: ["position", "velocity", "acceleration", "body", "comparator"]
provides: "avoidance"
initialize: (@short, @far, @closeWidth, @farWidth) ->
augment: (entity) ->
entity.on "comparator:setup", @setup
entity.on "comparator:compare", @compare
setup: (engine, entity) =>
#console.log "Avoid.setup", arguments...
accel = new limber.geometry.Vector
vel = entity.velocity.clone()
dir = vel.clone().normalize()
pos = entity.position.clone().add(vel.clone().scale(@short))
normal = dir.clone().rotateRight()
ahead = @far - @short
w = (@farWidth - @closeWidth) / 2
entity.avoidance = new limber.geometry.Polygon
entity.avoidance.addVertex(pos.add(normal.clone().scale(@closeWidth / 2))) #side, right
entity.avoidance.addVertex(pos.add(vel.clone().scale(ahead).add(normal.clone().scale(w)))) #front, right
entity.avoidance.addVertex(pos.add(normal.clone().scale(- @farWidth))) #front, left
entity.avoidance.addVertex(pos.add(vel.clone().scale(- ahead).add(normal.clone().scale(w)))) #side, left
entity.avoidance.avoid = false
compare: (engine, entity, other) =>
#console.log "Avoid.compare", arguments...
if mtv = entity.avoidance.testCollision(other.avoidance)
entity.avoidance.avoid = true
entity.acceleration.add(mtv.axis.scale(mtv.overlap))
other.avoidance.avoid = true
other.acceleration.subtract(mtv.axis) #Already scaled above
class limber.trait.Flocking extends limber.trait.Trait
requires: ["position", "velocity", "acceleration"]
initialize: ->
@flock = []
augment: (entity) ->
self = this
self.flock.push(entity)
entity.on "update", -> self.steer(entity)
steer: (entity) ->
approach = new limber.geometry.Vector(0, 0)
avoid = new limber.geometry.Vector(0, 0)
match = new limber.geometry.Vector(0, 0)
accel = new limber.geometry.Vector(0, 0)
approach.n = avoid.n = match.n = 0
for boid in @flock when boid != entity
dist = boid.position.clone().subtract(entity.position)
dist2 = dist.magnitudeSq()
if dist2 < 10000
approach.add(dist)
approach.n++
if dist2 < 1000
avoid.subtract(dist)
avoid.n++
if dist2 < 10000
match.add(boid.velocity.clone())
match.n++
approach.shrink(approach.n) if approach.n
avoid.shrink(avoid.n) if avoid.n
match.shrink(match.n) if match.n
accel
.add(approach).shrink(20)
.add(avoid)
.add(match).shrink(10)
.normalize()
.scale(20)
#unless not @once and accel.magnitude()
# @once = true
# console.log "WTF", accel
entity.acceleration.add(accel)
class limber.trait.CollisionResponse extends limber.trait.Trait
requires: "velocity"
constructor: ->
@on "augment", (entity) ->
entity.on "render", -> @collided = false
entity.on "collision", (engine, mtv, other) ->
@collided = true
@position.add(mtv.axis.clone().scale(mtv.overlap / 2))
class limber.trait.RandomAcceleration extends limber.trait.Trait
requires: "acceleration"
constructor: (@speed = 20) ->
augment: (entity) ->
speed = @speed
twiceSpeed = speed * 2
entity.on "update", (engine) ->
@acceleration.add(Math.random() * twiceSpeed - speed, Math.random() * twiceSpeed - speed)
class limber.trait.ConstrainVelocity extends limber.trait.Trait
requires: "velocity"
constructor: (@min = 20, @max = 200) ->
augment: (entity) ->
min = @min
max = @max
minSq = min * min
maxSq = max * max
entity.on "update", (engine) ->
v2 = @velocity.magnitudeSq()
if v2 > maxSq then @velocity.scale(Math.sqrt(v2) / maxSq)
else if v2 < minSq then @velocity.scale(min / Math.sqrt(v2))
class limber.trait.ConstrainAcceleration extends limber.trait.Trait
requires: "velocity"
constructor: (@max = 20) ->
augment: (entity) ->
max = @max
maxSq = max * max
entity.on "update", (engine) ->
v2 = @acceleration.magnitudeSq()
if v2 > maxSq then @acceleration.normalize().scale(max)
class limber.trait.AnimatedSprite extends limber.trait.Trait
@imgCache = []
requires: ["position", "velocity"]
provides: "sprite"
initialize: (src, @frameW, @frameH, @w, @h, @framesPerRow = 1, @frameCount = 1, @frameTime = 100) ->
cache = limber.trait.AnimatedSprite.imgCache
unless cache[src]
cache[src] = new Image
cache[src].src = src
@image = cache[src]
@frame = 0
@xOff = 0
@yOff = 0
@nextFrame = @frameTime
augment: (entity) ->
self = this
render = (engine) ->
engine.context.save()
engine.context.translate(entity.position.x, entity.position.y)
engine.context.rotate(Math.atan2(entity.velocity.y, entity.velocity.x) + Math.PI / 2)
engine.context.drawImage(self.image, self.frameW * self.xOff, self.frameH * self.yOff, self.frameW, self.frameH, - self.w / 2, - self.h / 2, self.w, self.h)
engine.context.restore()
update = (engine) ->
self.nextFrame -= engine.timer.delta
if self.nextFrame <= 0
self.frame += 1
if self.frame == self.frameCount
self.frame = 0
@emit "sprite:reset", this
self.nextFrame = self.frameTime + self.nextFrame
self.xOff = self.frame % self.framesPerRow
self.yOff = Math.floor(self.frame / self.framesPerRow)
entity.on "render", render
entity.on "update", update
entity.on "destroy", (entity) ->
entity.removeListener "render", render
entity.removeListener "update", update
class MouseFollow extends limber.trait.Trait
requires: ["position", "velocity", "acceleration"]
augment: (entity) ->
entity.on "update", (engine) ->
accel = engine.mousePosition.clone().subtract(@position)
accel.scale(1000 / accel.magnitudeSq())
@acceleration.add(accel)
class Enemy extends limber.entity.Entity
body: new limber.trait.AABBBody(-10, -10, 10, 10)
initialize: (x, y, vx, vy, comp, colDet) ->
@mixin new limber.trait.Position(x, y)
@mixin new limber.trait.Velocity(vx, vy)
@mixin new limber.trait.Acceleration(0, 0)
@mixin @body
@mixin comp
@mixin colDet
@mixin new MouseFollow
@mixin new limber.trait.ConstrainVelocity(20, 400)
@mixin new limber.trait.ConstrainAcceleration(100)
@mixin new limber.trait.AnimatedSprite("http://bittyjava.files.wordpress.com/2009/11/martian-sprite-60.png?w=240&h=110", 60, 55, 20, 20, 4, 8, 100)
class Ship extends limber.entity.Entity
initialize: (x, y, comp, colDet) ->
@mixin new limber.trait.Position(x, y)
@mixin new limber.trait.AABBBody(-5, -5, 5, 5)
@mixin comp
@mixin colDet
@on "update", (engine) ->
@position = engine.mousePosition.clone()
@on "render", (engine) ->
engine.context.save()
engine.context.fillStyle = "blue"
engine.context.fillRect(@position.x - 5, @position.y - 5, 10, 10)
#engine.context.fill()
engine.context.restore()
class Explosion extends limber.entity.Entity
initialize: (pos, vel) ->
@mixin new limber.trait.Position(pos)
@mixin new limber.trait.Velocity(vel)
@mixin new limber.trait.AnimatedSprite("http://members.gamedev.net/dfgames/explosion.png", 64, 64, 32, 32, 5, 25, 50)
@on "sprite:reset", @destroy
$ ->
engine = new limber.engine.Canvas2D("canvas", 960, 500)
comparator = new limber.trait.Comparator
collisionDetection = new limber.trait.CollisionDetection
engine.attach new limber.component.FPS("fps")
ship = new Ship(400, 400, comparator, collisionDetection)
ship.on "collision", ->
engine.attach new Explosion(@position)
@destroy()
document.getElementById("debug").innerHTML =
"You lost. Refresh to try again."
engine.attach ship
enemies = 20
for i in [0 ... 20]
enemy = new Enemy(Math.random() * 960, Math.random() * 500, Math.random() * 40 - 20, Math.random() * 40 - 20, comparator, collisionDetection)
enemy.on "collision", (engine, entity, other) ->
engine.attach new Explosion(@position, @velocity)
@destroy()
if --enemies == 0
document.getElementById("debug").innerHTML =
"You won in " + Math.floor((+new Date() - start) / 100) / 10 + " seconds"
engine.attach enemy
start = + new Date()
engine.animate()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment