Skip to content

Instantly share code, notes, and snippets.

@basicxman
Created April 21, 2012 03:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save basicxman/2433562 to your computer and use it in GitHub Desktop.
Save basicxman/2433562 to your computer and use it in GitHub Desktop.
class Asteroid extends GameObject
constructor: (engine) ->
super(engine)
@updateOffScene = true
@drawOffScene = true
@production = Math.ceil(Math.random() * 20)
@fuel = Math.ceil(Math.random() * 20)
@research = Math.ceil(Math.random() * 20)
@width = 99
@height = 100
@angle = 0
@speed = Math.random() * 0.1
@indicator = new Indicator("rgba(255, 255, 255, 0.3)", this)
if Math.round(Math.random())
@x = Math.floor(Math.random() * @engine.gW)
@y = if Math.floor(Math.random()) then @height else @engine.gH
else
@y = Math.floor(Math.random() * @engine.gH)
@x = if Math.floor(Math.random()) then @width else @engine.gW
rise = @engine.gH / 2 - @y + Math.floor(Math.random() * 150) - 300
run = @engine.gW / 2 - @x + Math.floor(Math.random() * 150) - 300
@speedX = run * 0.001
@speedY = rise * 0.001
@points = []
@points.push [48, 72]
@points.push [24, 72]
@points.push [12, 54]
@points.push [0, 36]
@points.push [12, 18]
@points.push [24, 0]
@points.push [48, 0]
@points.push [60, 18]
@points.push [72, 36]
@points.push [60, 54]
for i in [0...10]
@points[i][0] -= 50 + Math.floor(Math.random() * 10) - 20
@points[i][1] -= 50 + Math.floor(Math.random() * 10) - 20
for i in [0...Math.floor(Math.random() * 3)]
index = Math.floor(Math.random() * @points.length)
@points.splice(index, 1)
save: ->
super "Asteroid"
update: ->
@vX = @x - Math.abs(@engine.dX)
@vY = @y - Math.abs(@engine.dY)
if @speedX > 0
@angle += @speedX * @speed * 0.05
else
@angle -= @speedX * @speed * 0.05
@angle %= Math.PI * 2
@x += @speedX * @speed
@y += @speedY * @speed
if @x > @engine.gW or @y > @engine.gH or @x < 0 or @y < 0
return -1
this.checkIfInScene()
a = Math.abs(@engine.gH / 2 - @y)
b = Math.abs(@engine.gW / 2 - @x)
c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2))
# Only display indicators for asteroids within 1500 pixels.
@outOfRange = c > 1500
@engine.asteroidsInRange++ if not @outOfRange
if not @inScene and not @outOfRange
@indicator.update()
draw: (ctx) ->
ctx.save()
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)"
ctx.fillStyle = "rgba(255, 255, 255, 0.3)"
if @inScene
ctx.translate(@vX + @width / 2, @vY + @height / 2)
ctx.rotate(@angle)
ctx.beginPath()
ctx.moveTo(@points[0][0], @points[0][1])
for point in @points
ctx.lineTo(point[0], point[1])
ctx.closePath()
ctx.stroke()
else if not @outOfRange
@indicator.draw(ctx)
ctx.restore()
coffee --watch \
--join game.js \
--compile \
game_object.coffee \
indicator.coffee \
asteroid.coffee \
space_station.coffee \
game.coffee \
game_engine.coffee
class Game
constructor: (@engine) ->
if localStorage.data?
@storage = $.parseJSON(localStorage.data)
else
@storage = {}
if not @storage.games?
this.initStorage()
if @storage.games.length == 0
$("#current-games").html("<h3 class='none-available'>No current games available.</h3>");
else
for i in [0...@storage.games.length]
game = @storage.games[i]
tempGameSave = $("#game-save-template").clone().attr("id", i)
lastSaveTime = new Date(game.lastSaveTime)
tempGameSave.children(".screenshot").attr("src", game.screenshot)
tempGameSave.children(".commander-name").text(game.commanderName)
tempGameSave.children(".game-save-time").text(lastSaveTime.toDateString())
tempGameSave.children(".faction").text(game.faction)
tempGameSave.appendTo($("#current-games"))
$("#start-game").click =>
@gameIndex = @storage.games.length
commanderName = $("#commander-name").val()
faction = "Soviet"
@storage.games.push {
commanderName: $("#commander-name").val()
, faction: "Soviet"
, lastSaveTime: new Date().getTime()
, g: {}
, objects: []
}
this.startGame()
$(".current-game").click ->
window.game.game.gameIndex = $(this).attr("id")
window.game.game.startGame()
$("#game-selection").fadeIn()
initStorage: ->
@storage.games = []
save: ->
localStorage.data = JSON.stringify(@storage)
startGame: ->
@engine.storage = @storage.games[@gameIndex]
@engine.load()
if @engine.storage.objects.length == 0
@engine.objects.push(new SpaceStation(@engine))
$("#game-selection").fadeOut()
@engine.gameStarted = true
@engine.pan(0, 0)
@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
border-radius: $radius;
}
@mixin box-shadow($args) {
-webkit-box-shadow: $args;
-moz-box-shadow: $args;
box-shadow: $args;
}
body, div, canvas, label, input, h3, h4 {
margin: 0px;
padidng: 0px;
}
.clear { clear: both; }
#c {
width: 1024px;
margin: 30px auto;
canvas {
position: absolute;
}
}
#bg { z-index: 1; }
#fg { z-index: 2; }
div#game-selection {
display: none;
font-family: Audiowide;
width: 1024px;
margin: 80px auto;
background-color: rgba(73, 110, 145, 0.8);
color: #fff;
padding: 40px;
@include border-radius(10px);
div#current-games {
padding: 0px 10px;
.current-game {
width: 220px;
float: left;
cursor: pointer;
&:hover {
.screenshot {
$shadow: -1px 1px 20px rgba(0, 0, 0, 0.8), 1px 1px 20px rgba(0, 0, 0, 0.8);
@include box-shadow($shadow);
}
}
.screenshot {
background-color: #000;
margin-bottom: 8px;
$shadow: -1px -1px 20px rgba(255, 255, 255, 0.3), 1px 1px 20px rgba(255, 255, 255, 0.3);
@include box-shadow($shadow);
}
.faction {
top: 130px;
margin-left: 10px;
position: absolute;
}
}
}
div#new-game {
padding: 10px;
input {
padding: 4px;
line-height: 18px;
height: 18px;
font-size: 13px;
border: 1px solid #ccc;
color: #555;
@include border-radius(3px);
}
}
div#game-save-template { display: none; }
}
div#container {
width: 100%;
z-index: 3;
position: absolute;
}
class GameEngine
constructor: ->
@bg = document.getElementById("bg").getContext("2d")
@fg = document.getElementById("fg").getContext("2d")
@gW = 5000
@gH = 3750
@vW = @fg.canvas.width
@vH = @fg.canvas.height
@dX = (@gW - @vW) / -2
@dY = (@gH - @vH) / -2
@decay = 1.05
@panMultiplier = -8
@deadband = 0.005
@objects = []
this.registerTimer("save", 5000)
this.registerTimer("spawnAsteroid", 30000)
this.addEventListeners()
this.initCanvas()
this.startAnimation()
@game = new Game(this)
# Game initialization.
addEventListeners: ->
@fg.canvas.addEventListener "mousedown", (e) =>
this.mouseDown(e)
@fg.canvas.addEventListener "mousemove", (e) =>
this.mouseMove(e)
for event in ["mouseup", "mouseout"]
@fg.canvas.addEventListener event, ((e) => this.resetDrag())
initCanvas: ->
@bg.canvas.style.background = "#000"
startAnimation: ->
anim = =>
this.update()
this.draw()
requestAnimFrame(anim)
anim()
load: ->
for property, value of @storage.g
this[property] = value
for object in @storage.objects
eval("obj = new #{object.class}(this)")
obj.load(object)
@objects.push(obj)
save: ->
@storage.screenshot = @fg.canvas.toDataURL()
@storage.g.dX = @dX
@storage.g.dY = @dY
@storage.objects = []
for object in @objects
@storage.objects.push(object.save())
@game.save()
@saveText = "Saved."
# Events
mouseDown: (e) ->
# Only the left mouse button should be used for dragging.
if e.button != 0
return
# Regardless of whether we reach a drag event in 100mS, momentum should
# stop from any previous pan event.
@velX = @velY = undefined
@timeout = setTimeout((=> this.startDrag(e)), 100)
mouseMove: (e) ->
if not @isDragging
return
# Store a circular buffer of three movement events, using the current event
# and previous event isn't representative enough for velocity calculations.
this.bufferAdvance(@bufDragTime, e.timeStamp)
this.bufferAdvance(@bufDragX, e.offsetX)
this.bufferAdvance(@bufDragY, e.offsetY)
if @bufDragX[2]? and @bufDragX[1]?
this.pan(@bufDragX[1] - @bufDragX[2], @bufDragY[1] - @bufDragY[2])
startDrag: ->
@isDragging = true
this.bufferReset()
resetDrag: ->
if @bufDragTime? and @bufDragTime[0]? and @bufDragTime[2]?
t = @bufDragTime[2] - @bufDragTime[0]
rise = @bufDragY[2] - @bufDragY[0]
run = @bufDragX[2] - @bufDragX[0]
@velX = run / t
@velY = rise / t
clearTimeout(@timeout)
@isDragging = false
this.bufferReset()
pan: (x, y) ->
# Change position offset and ensure map boundaries are met.
@dX = Math.min(Math.max(@dX - x, -(@gW - @vW)), 0)
@dY = Math.min(Math.max(@dY - y, -(@gH - @vH)), 0)
# Update current viewport coordinates and ask objects to update their draw
# state.
@vX1 = -@dX
@vY1 = -@dY
@vX2 = @vX1 + @vW
@vY2 = @vY1 + @vH
for obj in @objects
obj.checkIfInScene()
panMomentum: ->
if @velX == undefined or @velY == undefined
return
this.pan(@panMultiplier * @velX, @panMultiplier * @velY)
@velX /= @decay
@velY /= @decay
if (Math.abs(@velX) < @deadband)
@velX = undefined
if (Math.abs(@velY) < @deadband)
@velY = undefined
# Utilities
registerTimer: (func, interval) ->
if not @timers?
@timers = []
@timers.push [func, interval, new Date().getTime()]
checkTimers: ->
return if not @gameStarted
curTime = new Date().getTime()
for timer in @timers
if curTime - timer[2] >= timer[1]
timer[2] = curTime
this[timer[0]]()
bufferReset: ->
@bufDragTime = [undefined, undefined, undefined]
@bufDragX = [undefined, undefined, undefined]
@bufDragY = [undefined, undefined, undefined]
bufferAdvance: (buffer, value) ->
buffer[0] = buffer[1]
buffer[1] = buffer[2]
buffer[2] = value
updateSaveText: ->
if @saveCountdown == 0
@saveText = ""
@saveCountdown = undefined
if @saveText == "Saved."
if @saveCountdown?
@saveCountdown -= 1
else
@saveCountdown = 45
@saveOpacity = 1.0
if @saveCountdown < 30
@saveOpacity = @saveCountdown / 30.0
spawnAsteroid: ->
@objects.push(new Asteroid(this))
# Periodic calls.
update: ->
this.panMomentum()
@asteroidsInRange = 0
for object in @objects
if object.inScene or object.updateOffScene
if object.update() == -1
_len--
@objects.splice(_i, 1)
this.checkTimers()
this.updateSaveText()
draw: ->
@fg.clearRect(0, 0, @vW, @vH)
for object in @objects
if object.inScene or object.drawOffScene
object.draw(@fg)
@fg.fillStyle = "#fff"
@fg.font = "14px Audiowide"
@fg.fillText("#{Math.floor(Math.abs(@dX))}, #{Math.floor(Math.abs(@dY))}", 5, 15)
@fg.fillText("Asteroids in Range: #{@asteroidsInRange}", 5, 34)
if @saveText?
@fg.fillStyle = "rgba(255, 255, 255, #{@saveOpacity})"
@fg.font = "18px Audiowide"
@fg.fillText(@saveText, @vW - @fg.measureText(@saveText).width - 20, @vH - 20)
window.game = new GameEngine
class GameObject
constructor: (@engine) ->
@inScene = false
@updateOffScene = false
@drawOffScene = false
@blacklist = ["engine", "blacklist", "indicator", "inScene", "updateOffScene", "drawOffScene"]
checkIfInScene: ->
@inScene = (@x >= @engine.vX1 - @width / 2) and (@x < @engine.vX2 + @width / 2) and (@y >= @engine.vY1 - @height / 2) and (@y < @engine.vY2 + @height / 2)
save: (className) ->
obj = { class: className }
for property, value of this
if not (property in @blacklist) and typeof value isnt "function"
obj[property] = this[property]
return obj
load: (obj) ->
for property, value of obj
this[property] = value
class Indicator
constructor: (@colour, @object) ->
update: ->
@minX = 64
@maxX = @object.engine.vW - 64
@minY = 0
@maxY = @object.engine.vH
@vX = Math.min(Math.max(@object.vX, @minX), @maxX)
@vY = Math.min(Math.max(@object.vY, @minY), @maxY)
if @vX == @minX and @vY == @minY
@angle = 3 * Math.PI / 4
@vY += 24
@vX += 4
else if @vX == @maxX and @vY == @minY
@angle = -3 * Math.PI / 4
@vY += 70
@vX += 40
else if @vX == @maxX and @vY == @maxY
@angle = -Math.PI / 4
@vY -= 24
@vX -= 4
else if @vX == @minX and @vY == @maxY
@angle = Math.PI / 4
@vY -= 70
@vX -= 40
else if @vX == @minX
@angle = Math.PI / 2
else if @vX == @maxX
@angle = -Math.PI / 2
else if @vY == @minY
@angle = Math.PI
@vY += 64
else
@angle = 0
@vY -= 64
draw: (ctx) ->
ctx.translate(@vX, @vY)
ctx.rotate(@angle)
ctx.fillStyle = @colour
ctx.beginPath()
ctx.arc(32, 20, 20, (5 * Math.PI) / 6, Math.PI / 5, false)
ctx.lineTo(32, 64)
ctx.closePath()
ctx.fill()
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Space</title>
<link href='http://fonts.googleapis.com/css?family=Audiowide' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" href="game.css" />
</head>
<body>
<div id="c">
<canvas id="bg" width="1024" height="768"></canvas>
<canvas id="fg" width="1024" height="768">Your browser does not support this game.</canvas>
</div>
<div id="container">
<div id="game-selection">
<div id="current-games">
</div>
<div class="clear"></div>
<div id="new-game">
<label for="commander-name">Commander Name:</label><br />
<input type="text" id="commander-name" />
<br />
<button type="button" id="start-game">Start</button>
</div>
<div id="game-save-template" class="current-game">
<img class="screenshot" width="200" />
<h3 class="commander-name"></h3>
<h4 class="game-save-time"></h4>
<div class="faction">
</div>
</div>
</div>
</div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="requestAnimFrame.js"></script>
<script type="text/javascript" src="game.js"></script>
</body>
</html>
sass: sass --watch game.scss:game.css
coffee: ./compile
class SpaceStation extends GameObject
constructor: (engine) ->
super(engine)
@updateOffScene = true
@drawOffScene = true
@health = 1000
@width = 100
@height = 100
@x = engine.gW / 2
@y = engine.gH / 2
@indicator = new Indicator("rgba(119, 187, 255, 0.5)", this)
save: ->
super "SpaceStation"
update: ->
@vX = @x - Math.abs(@engine.dX)
@vY = @y - Math.abs(@engine.dY)
if not @inScene
@indicator.update()
draw: (ctx) ->
ctx.save()
if @inScene
ctx.strokeStyle = "rgba(119, 187, 255, 0.5)"
ctx.lineWidth = 5
ctx.translate(@vX, @vY)
ctx.rotate(Math.PI / 4)
ctx.beginPath()
ctx.arc(0, 0, 10, 0, Math.PI * 2, true)
ctx.stroke()
ctx.strokeRect(@width / -2, @height / -2, @width, @height)
else
@indicator.draw(ctx)
ctx.restore()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment