Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 898 lines (734 sloc) 22.611 kb
3111001 @stephank Adding some docs.
authored
1 ###!
2 'Out' game, © 2011 Stéphan 'kosinus' Kochen.
3 Made for Ludum Dare 22. Licensed GPLv3, see the LICENSE file.
4 ###
5
6
162eb7f @stephank Initial commit, playable prototype.
authored
7 { PI } = Math
8 TwoPI = PI * 2
4f04b54 @stephank More browser compatibility fixes.
authored
9
10 unless window.requestAnimationFrame
11 for prefix in ['moz', 'webkit', 'ms', 'o']
12 if window["#{prefix}RequestAnimationFrame"]
13 window.requestAnimationFrame = window["#{prefix}RequestAnimationFrame"]
14 window.cancelRequestAnimationFrame = window["#{prefix}CancelRequestAnimationFrame"]
15 break
162eb7f @stephank Initial commit, playable prototype.
authored
16
17
18 buildCanvas = (options={}) ->
19
20 # Building the main canvas.
21 if options.id?
22 x = y = 0
23 w = 100
24 h = 75
25
26 element = document.getElementById options.id
27 xScale = element.width / 100
28 yScale = element.height / 75
29
30 # Building an off-screen buffer.
31 else
32 [x, y, w, h] = options.area ? [0, 0, 100, 75]
33 { xScale, yScale } = mainCanvas
34
35 element = document.createElement 'canvas'
36 element.width = Math.ceil w * xScale
37 element.height = Math.ceil h * yScale
38
39 # Set up the coordinate system.
40 context = element.getContext '2d'
41 context.translate 0.5, 0.5
42 context.scale xScale, yScale
43
44 # Reposition the area / change the offset.
45 reposition = (newX, newY) ->
46 context.restore()
47 context.save()
48 x = newX
49 y = newY
50 context.translate -x, -y
51
52 # Initial position / offset.
53 context.save()
54 reposition x, y
55
56 # Convenience for blending to another canvas.
57 blend = (c) -> c.context.drawImage element, x, y, w, h
58
59 # All canvasses are passed around in this manner.
60 { element, context, xScale, yScale, reposition, blend }
61
62 # Canvas element on the page.
63 mainCanvas = buildCanvas id: 'game'
64 [bodyTag] = document.getElementsByTagName 'body'
65 bodyTag.appendChild mainCanvas.element
66
67 # Game state.
68 levels = require './levels'
69 state = window.state = {}
70
71
72 # Wraps a bitmap containing collision data.
73 class CollisionMap
74
75 constructor: (x, y, @w, @h) ->
76 { @xScale, @yScale } = mainCanvas
77 @reposition x, y
78 @stride = Math.ceil w * @xScale / 8
79 height = Math.ceil h * @yScale
80 @bitmap = new Uint8Array @stride * height
81
82 reposition: (@x, @y) ->
83 @ex = @x + @w
84 @ey = @y + @h
85
86 mapPixel: (x, y) ->
87 idx = y * @stride + Math.floor(x / 8)
88 bit = 1 << (x % 8)
89 { idx, bit }
90
91 map: (x, y) ->
92 if x < @x or y < @y or x > @ex or @y > @ey
93 return { idx: -1, bit: -1 }
94 x = Math.round (x - @x) * @xScale
95 y = Math.round (y - @y) * @yScale
96 return @mapPixel x, y
97
98 getPixel: (x, y) ->
99 { idx, bit } = @mapPixel x, y
100 return !!(@bitmap[idx] & bit)
101
102 get: (x, y) ->
103 { idx, bit } = @map x, y
104 return idx isnt -1 and !!(@bitmap[idx] & bit)
105
106 setPixel: (x, y) ->
107 { idx, bit } = @mapPixel x, y
108 @bitmap[idx] |= bit
109
110 set: (x, y) ->
111 { idx, bit } = @map x, y
112 @bitmap[idx] |= bit unless idx is -1
113
114 unsetPixel: (x, y) ->
115 { idx, bit } = @mapPixel x, y
116 @bitmap[idx] &= ~bit
117
118 unset: (x, y) ->
119 { idx, bit } = @map x, y
120 @bitmap[idx] &= ~bit unless idx is -1
121
122 putImageData: ({ data, width, height }, valueMap) ->
123 for y in [0...height]
124 for x in [0...width]
125 base = (y * width + x) * 4
126 r = data[base + 0]
127 g = data[base + 1]
128 b = data[base + 2]
129 a = data[base + 3]
130 if valueMap r, g, b, a
131 @setPixel x, y
132 else
133 @unsetPixel x, y
134 return
135
136
137 # You!
138 class Player
139
140 size = 0.7
141 moveDivisorSteps = [1, 2, 4, 8]
142 speed = 0.30
143 diagSpeed = Math.sqrt(speed * speed / 2)
144
145 bodyTestOffsets = for a in [0...360] by 20
146 { x: Math.sin(a) * size, y: Math.cos(a) * size }
147 bodyTest = (x, y, map) ->
148 for o in bodyTestOffsets
149 ox = x + o.x
150 oy = y + o.y
151 return yes if map.get ox, oy
152 return no
153
154 constructor: ([@x, @y]) ->
155 @movement = { x: 0, y: 0 }
156 @actionResult = null
157
158 move: ->
159 dist = speed
160 { x, y } = @movement
161 if x isnt 0 and y isnt 0 then dist = diagSpeed
162
163 state.tutorial = 2 if state.tutorial is 1 and x > 0
164
165 # Über cheesy solid tests follow.
166 if x isnt 0
167 for div in moveDivisorSteps
168 newX = @x + x * (dist / div)
169 unless bodyTest newX, @y, state.level.collisionMap
170 @x = newX
171 break
172
173 if y isnt 0
174 for div in moveDivisorSteps
175 newY = @y + y * (dist / div)
176 unless bodyTest @x, newY, state.level.collisionMap
177 @y = newY
178 break
179
180 @actionResult = @determineAction()
181
182 testSentries: ->
183 found = no
184
185 for sentry in state.sentries when not sentry.disabled
186 sentry.hostile = no
187
188 dx = sentry.x - @x
189 dy = sentry.y - @y
190 continue if Math.sqrt(dx*dx + dy*dy) > sentry.radius
191
192 if bodyTest @x, @y, sentry.getVisibilityMap()
193 found = sentry.hostile = yes
194
195 return found
196
197 # Determine the result of pressing action at this time.
198 determineAction: ->
199 next = null
200 placed = []
201 for charge in state.charges when not charge.triggered
202 if charge.placed
203 placed.push charge
204 else
205 next = charge
206
207 if next
208 # Find the nearest wall within range.
209 closest = { edge: null, dist: Infinity }
210 state.level.eachEdgeNear @x, @y, 2, (edge, dist) =>
211 { v1, v2, n } = edge
212 # Check against current selection.
213 return if dist >= closest.dist
214 # Get the point on the wall closest to the player.
215 p = { x: @x + n.x*dist, y: @y + n.y*dist }
216 # Make sure the point is not too close to a corner.
217 return if Math.abs(v1.x - p.x) < 0.5 and Math.abs(v1.y - p.y) < 0.5
218 return if Math.abs(v2.x - p.x) < 0.5 and Math.abs(v2.y - p.y) < 0.5
219 # Enforce a minimum distance between charges.
220 for o in state.charges when charge.placed
221 return if Math.abs(o.x - p.x) < 2 and Math.abs(o.y - p.y) < 2
222 # Finally, calculate the angle the charge should face.
223 a = Math.atan2 @y-p.y, @x-p.x
224 # We're good, this is our best choice so far.
225 closest = { p, a, dist }
226
227 if closest.dist < 2
228 if state.tutorial is 2 and state.sentries[0].x - @x < 5
229 state.tutorial = 3
230
231 return ['place', next, closest.p, closest.a]
232
233 if placed.length > 0
234 return ['trigger', placed]
235
236 return null
237
238 # Perform action.
239 action: ->
240 return no unless @actionResult
241
242 [type] = @actionResult
243 switch type
244
245 when 'place'
246 [type, charge, p, a] = @actionResult
247 charge.place p, a
248
249 if state.tutorial is 3
250 state.tutorial = 4
251
252 when 'trigger'
253 [type, charges] = @actionResult
254 charge.trigger() for charge in charges
255
256 # Check failure.
257 missed = no
258 for sentry in state.sentries
259 unless sentry.disabled
260 sentry.hostile = yes
261 missed = yes
262
263 # FIXME: Don't really want state changes here, but oh well.
264 state.counter = 0
265 state.fsm = if missed then 'fail' else 'win'
266
267 else
268 return no
269
270 @actionResult = @determineAction()
271 return yes
272
273 draw: ->
274 c = mainCanvas.context
275 c.lineWidth = 0.2
276 c.fillStyle = '#fa3'
277 c.strokeStyle = '#eee'
278 c.beginPath()
279 c.arc @x, @y, size, 0, TwoPI
280 c.fill()
281 c.stroke()
282
283
284 # Charges are the primary weapon.
285 class Charge
286
287 effectRadius = 12
288 size = PI / 40
289 step = size * 2
290 rotStep = step / 30
291
292 constructor: ->
293 @placed = no
294 @triggered = no
295
296 place: ({ @x, @y }, face) ->
297 @placed = yes
298 @faceStart = face - PI/2
299 @faceEnd = face + PI/2
300 @rot = 0
301
302 trigger: ->
303 for sentry in state.sentries when not sentry.disabled
304 dx = sentry.x - @x
305 dy = sentry.y - @y
306 length = Math.sqrt(dx*dx + dy*dy)
307 sentry.disabled = yes if length < effectRadius
308 @placed = no
309 @triggered = yes
310
311 drawEffectArea: ->
312 return unless @placed
313 c = mainCanvas.context
314 c.strokeStyle = '#c60'
315 c.lineWidth = 0.3
316 for base in [0..TwoPI] by step
317 a = base + @rot
318 c.beginPath()
319 c.arc @x, @y, effectRadius, a, a+size
320 c.stroke()
321 @rot = (@rot + rotStep) % step
322
323 drawBody: ->
324 return unless @placed
325 c = mainCanvas.context
326 c.fillStyle = '#fff'
327 c.beginPath()
328 c.moveTo @x, @y
329 c.arc @x, @y, 0.4, @faceStart, @faceEnd
330 c.fill()
331
332
333 # The enemy: mobile sentries.
334 class Sentry
335
336 constructor: (options, @instructions) ->
337 { @x, @y } = options
338 @look = (options.look ? 0) * PI/180
339 @radius = options.radius ? 15
340 @moveSpeed = options.moveSpeed ? 0.18
341 @turnSpeed = options.turnSpeed ? 0.07
342 @halfFov = (options.fov ? 90) * PI/360
343
344 @disabled = no
345 @sightStyle = '#ff0'
346
347 bufSize = @radius * 2
348 @drawbuf = buildCanvas area: [0, 0, bufSize, bufSize]
349 @vismap = new CollisionMap 0, 0, bufSize, bufSize
350 @updatePos()
351 @updateFov()
352
353 @ip = 0
354 @state = 'next'
355 @targetX = @targetY = @targetLook = @waitRemaining = null
356
357 # Update variables that depend on position.
358 updatePos: ->
359 sx = @x - @radius
360 sy = @y - @radius
361 @drawbuf.reposition sx, sy
362 @vismap.reposition sx, sy
363
364 # Update variables that depend on FoV and look angle.
365 updateFov: ->
366 @fovStart = @look - @halfFov
367 @fovEnd = @look + @halfFov
368
369 # Run through sentry instructions.
370 move: ->
371 return if @disabled or @instructions.length is 0
372
373 # 'next' is an inbetween state for incrementing the instruction pointer
374 # and fetching the next instruction.
375 if @state is 'next'
376 [@state, args...] = @instructions[@ip]
377 @ip = (@ip + 1) % @instructions.length
378 switch @state
379 when 'move' then [@targetX, @targetY] = args
380 when 'turn' then @targetLook = args[0] * PI/180
381 when 'wait' then [@waitRemaining] = args
382
383 switch @state
384 when 'move'
385 dx = @targetX - @x
386 dy = @targetY - @y
387 dist = Math.sqrt(dx*dx + dy*dy)
388 if Math.abs(dist) < @moveSpeed
389 @x = @targetX
390 @y = @targetY
391 @state = 'next'
392 else
393 factor = @moveSpeed / dist
394 @x += dx * factor
395 @y += dy * factor
396 @updatePos()
397
398 when 'wait'
399 @waitRemaining--
400 @state = 'next' if @waitRemaining is 0
401
402 when 'turn'
403 @turnTo @targetLook
404
405 # Player has been detected, this is the animation of the sentry homing in
406 # on the player's location.
407 homeOnPlayer: ->
408 # Narrow the FoV for a focus effect.
409 @targetFov ?= @halfFov / 2
410 df = @targetFov - @halfFov
411 @halfFov += df * 0.2
412
413 # Turn towards the player.
414 { player } = state
415 @turnTo Math.atan2 player.y - @y, player.x - @x
416
417 # Turning helper.
418 turnTo: (a) ->
419 da = a - @look
420 da -= TwoPI if da > PI
421 da += TwoPI if da < -PI
422 if Math.abs(da) < @turnSpeed
423 @look = a
424 @state = 'next'
425 else
426 turnSpeed = @turnSpeed
427 turnSpeed *= -1 if da < 0
428 @look = (@look + turnSpeed) % TwoPI
429 @updateFov()
430
431 # Fill the visibility drawing buffer with the given style.
432 fillDrawbuf: (style) ->
433 c = @drawbuf.context
434 c.clearRect 0, 0, 100, 75
435
436 c.globalCompositeOperation = 'source-over'
437 state.level.drawOcclusion @x, @y, c, maxDist: @radius
438
439 c.globalCompositeOperation = 'source-out'
440 c.fillStyle = style
441 c.beginPath()
442 c.moveTo @x, @y
443 c.arc @x, @y, @radius, @fovStart, @fovEnd
444 c.fill()
445
446 # Draw visible area to the canvas.
447 drawVisibility: ->
448 return if @disabled
449 @fillDrawbuf '#433'
450 @drawbuf.blend mainCanvas
451
452 # Draw body to the canvas.
453 drawBody: ->
454 c = mainCanvas.context
455 c.lineWidth = 0.2
456
457 c.fillStyle = '#666'
458 c.strokeStyle = '#999'
459 c.beginPath()
460 c.arc @x, @y, 0.8, 0, TwoPI
461 c.fill()
462 c.stroke()
463
464 return if @disabled
465 c.strokeStyle = @sightStyle
466 c.beginPath()
467 c.arc @x, @y, 0.8, @fovStart, @fovEnd
468 c.stroke()
469
470 # Return a bitmap of visibility data.
471 getVisibilityMap: ->
472 @fillDrawbuf '#fff'
473 { width, height } = @drawbuf.element
474 data = @drawbuf.context.getImageData 0, 0, width, height
475 @vismap.putImageData data, (r, g, b, a) -> a > 128
476 return @vismap
477
478
479
480 # Deals with structure/walls in the level.
481 class LevelStructure
482
483 constructor: (walls) ->
484 @walls = for [type, coords...] in walls
485
486 # Slice up the coords array.
487 coords = for x, i in coords by 2
488 y = coords[i+1]
489 { x, y }
490
491 # Build edge structures.
492 edges = for v1, i in coords
493 # Need two vertices.
494 if coords.length - i < 2
495 break if type is 'flat'
496 # Account for the closing edge.
497 v2 = coords[0]
498 else
499 v2 = coords[i+1]
500
501 # We also store the vector v1v2.
502 v = { x: v2.x-v1.x, y: v2.y-v1.y }
503 # And the length of the wall.
504 length = Math.sqrt(v.x*v.x + v.y*v.y)
505 # And a normal unit vector.
506 n = { x: -v.y / length, y: v.x / length }
507
508 { v1, v2, v, length, n }
509
510 { type, coords, edges }
511
512 @collisionMap = @buildCollisionMap()
513
514 # Build a bitmap used when collision testing.
515 buildCollisionMap: ->
516 canvas = buildCanvas()
517 c = canvas.context
518 c.lineWidth = 0.3
519 c.strokeStyle = '#000'
520
521 # Draw black for solids.
522 c.fillStyle = '#000'
523 c.fillRect 0, 0, 100, 75
524 for { type, coords } in @walls
525 c.beginPath()
526 for { x, y }, i in coords
527 if i is 0
528 c.moveTo x, y
529 else
530 c.lineTo x, y
531 switch type
532 when 'hollow'
533 c.closePath()
534 c.fillStyle = '#fff'
535 c.fill()
536 when 'solid'
537 c.closePath()
538 c.fillStyle = '#000'
539 c.fill()
540 c.stroke()
541
542 # Create a bitmap.
543 { width, height } = canvas.element
544 data = c.getImageData 0, 0, width, height
545 bitmap = new CollisionMap 0, 0, 100, 75
546 bitmap.putImageData data, (r, g, b, a) -> r < 128
547 return bitmap
548
549 # Draw the level background.
550 drawBase: ->
551 c = mainCanvas.context
552
553 for { type, coords } in @walls when type isnt 'flat'
554 c.beginPath()
555 for { x, y }, i in coords
556 if i is 0
557 c.moveTo x, y
558 else
559 c.lineTo x, y
560 c.closePath()
561 c.fillStyle = switch type
562 when 'hollow' then '#300'
563 when 'solid' then '#000'
564 c.fill()
565 return
566
567 # Draw lines for walls.
568 drawWalls: ->
569 c = mainCanvas.context
570 c.lineWidth = 0.3
571 c.strokeStyle = '#b44'
572
573 c.beginPath()
574 for { type, coords } in @walls
575 for { x, y }, i in coords
576 if i is 0
577 c.moveTo x, y
578 else
579 c.lineTo x, y
580 c.closePath() unless type is 'flat'
581 c.stroke()
582
583 # Find wall edges near a point.
584 eachEdgeNear: (x, y, maxDist, iter) ->
585 for wall in @walls
586 for edge in wall.edges
587 { v, v1, v2, n } = edge
588 # Quick bounding box check.
589 continue if Math.min(v1.x, v2.x) - x > maxDist
590 continue if Math.min(v1.y, v2.y) - y > maxDist
591 continue if x - Math.max(v1.x, v2.x) > maxDist
592 continue if y - Math.max(v1.y, v2.y) > maxDist
593 # Line distance check.
594 # Project a vector from the line to the target, onto the normal.
595 r = { x: v1.x-x, y: v1.y-y }
596 dist = n.x*r.x + n.y*r.y
597 iter edge, dist if Math.abs(dist) <= maxDist
598 return
599
600 # Draw black on canvas context `c` for occlusion as seen from point `(x,y)`.
601 drawOcclusion: (x, y, c, options={}) ->
602 maxDist = options.maxDist ? null
603
604 c.fillStyle = '#000'
605
606 extend = (v) ->
607 dx = v.x - x
608 dy = v.y - y
609 dist = Math.sqrt(dx*dx + dy*dy)
610 ux = dx / dist
611 uy = dy / dist
612 { x: v.x + ux * 10000, y: v.y + uy * 10000 }
613
614 castFromEdge = ({ v1, v2 }) ->
615 # Extrude this edge away from the light source.
616 v3 = extend v2
617 v4 = extend v1
618
619 # Fill shadow.
620 c.beginPath()
621 c.moveTo v1.x, v1.y
622 c.lineTo v2.x, v2.y
623 c.lineTo v3.x, v3.y
624 c.lineTo v4.x, v4.y
625 c.closePath()
626 c.fill()
627
628 if options.maxDist?
629 @eachEdgeNear x, y, options.maxDist, castFromEdge
630 else
631 for wall in @walls
632 for edge in wall.edges
633 castFromEdge edge
634 return
635
636
637 # Input handling.
638 controller =
639 initialize: ->
640 @up = @down = @left = @right = @action = 0
4f04b54 @stephank More browser compatibility fixes.
authored
641 document.onkeydown = (e) => @keyDown e
642 document.onkeyup = (e) => @keyUp e
162eb7f @stephank Initial commit, playable prototype.
authored
643
644 keyDown: (e) ->
645 switch e.keyCode
646 when 37 then @left = -1
647 when 38 then @up = -1
648 when 39 then @right = +1
649 when 40 then @down = +1
650 when 32 then @action = 1 if @action is 0
651 @updateMovement()
652
653 keyUp: (e) ->
654 switch e.keyCode
655 when 37 then @left = 0
656 when 38 then @up = 0
657 when 39 then @right = 0
658 when 40 then @down = 0
659 when 32 then @action = 0
660 @updateMovement()
661
662 updateMovement: ->
663 state.player?.movement =
664 x: @left + @right
665 y: @up + @down
666
667 dequeue: ->
668 return if @action isnt 1
669 @action = 2
670
671 switch state.fsm
672 when 'title' then state.title.dismiss()
673 when 'play' then state.player.action()
674
675
676 # Main loop and state handling.
677 #
678 # There's a global FSM that determines what's happening. States are:
679 # title, levelZoom, play, fail, win, end
680 gameLoop = window.gameLoop =
681 interval: null
682
683 initialize: ->
684 state.fsm = 'title'
685 state.counter = 0
686
687 @initialize = ->
688
689 start: ->
690 return if @interval
691 @initialize()
4f04b54 @stephank More browser compatibility fixes.
authored
692 @interval = setInterval (=> @tick()), 25
162eb7f @stephank Initial commit, playable prototype.
authored
693
694 stop: ->
695 clearInterval @interval if @interval
4f04b54 @stephank More browser compatibility fixes.
authored
696 cancelRequestAnimationFrame? @frameRequest if @frameRequest
162eb7f @stephank Initial commit, playable prototype.
authored
697 @interval = @frameRequest = null
698
699 # Start up a new level.
700 loadLevel: (idx) ->
701 state.levelIdx = idx
702 level = levels[idx]
703
704 state.title = level.title
705 state.tutorial = level.tutorial or 0
706 state.level = new LevelStructure level.walls
707 state.player = new Player level.start
708 state.charges = for i in [0...level.charges]
709 new Charge
710 state.sentries = for [options, waypoints...] in level.sentries
711 new Sentry options, waypoints
712
713 state.fsm = 'levelZoom'
714 state.counter = 1
715
716 # Restart the level.
717 reloadLevel: ->
718 @loadLevel state.levelIdx
719
720 # Start the next level, or go to end.
721 nextLevel: ->
722 idx = state.levelIdx + 1
723 if levels[idx]
724 @loadLevel idx
725 else
726 state.fsm = 'end'
727 state.counter = 0
728
729 # Simulate a tick.
730 tick: ->
731 switch state.fsm
732
733 when 'title'
734 state.counter++
735 @loadLevel 0 if state.counter is 100
736
737 when 'play'
738 state.player.move()
739 for sentry in state.sentries
740 sentry.move()
741 if state.player.testSentries()
742 state.fsm = 'fail'
743 state.counter = 0
744 else
745 controller.dequeue()
746
747 when 'fail'
748 sightStyle = if state.counter % 10 > 5 then '#ff0' else '#f00'
749 for sentry in state.sentries when sentry.hostile
750 sentry.homeOnPlayer()
751 sentry.sightStyle = sightStyle
752
753 state.counter++
754 @reloadLevel() if state.counter is 100
755
756 when 'win'
757 state.counter++
758 @nextLevel() if state.counter is 100
759
4f04b54 @stephank More browser compatibility fixes.
authored
760 if window.requestAnimationFrame
761 @frameRequest ||= requestAnimationFrame => @frame()
762 else
763 @frame()
162eb7f @stephank Initial commit, playable prototype.
authored
764
765 # Render a frame.
766 frame: ->
767 @frameRequest = null
768
769 c = mainCanvas.context
770 c.clearRect(0, 0, 100, 75)
771
772 switch state.fsm
773
774 when 'title'
775 c.fillStyle = '#fff'
776 c.font = 'bold italic 12px monospace'
777 c.textAlign = 'center'
778 c.textBaseline = 'middle'
779 c.fillText 'out', 50, 37.5
780
781 c.globalCompositeOperation = 'source-atop'
782 x = state.counter * 1.7 - 30
783 g = c.createLinearGradient x, 0, x + 30, 30
784 g.addColorStop 0.00, '#000'
785 g.addColorStop 0.49, '#ccc'
786 g.addColorStop 0.50, '#777'
787 g.addColorStop 1.00, '#000'
788 c.fillStyle = g
789 c.fillRect 0, 0, 100, 75
790 c.globalCompositeOperation = 'source-over'
791
792 when 'levelZoom'
793 state.counter = state.counter / 6 * 5
794 scale = 1 - state.counter
795 offset = state.counter / 2
796 c.save()
797 c.translate(offset * 100, offset * 75)
798 c.scale(scale, scale)
799 c.globalAlpha = scale
800 @drawLevel()
801 c.restore()
802
803 if state.counter < 0.01
804 state.fsm = 'play'
805 # Call this here so that, at level start,
806 # all input flags are off.
807 controller.initialize()
808
809 when 'play'
810 @drawLevel()
811
812 when 'fail'
813 @drawLevel()
814
815 when 'win'
816 a = (100 - Math.max(60, state.counter)) / 40
817 c.globalAlpha = a
818 @drawLevel()
819
820 a = Math.min(10, state.counter) / 10
821 a = (100 - Math.max(80, state.counter)) / 20 if a is 1
822 c.globalAlpha = a
823 c.fillStyle = "#fff"
824 c.font = 'bold italic 12px monospace'
825 c.textAlign = 'center'
826 c.textBaseline = 'middle'
827 c.fillText 'cleared!!', 50, 37.5
828
829 c.globalAlpha = 1
830
831 when 'end'
832 c.fillStyle = '#fff'
833 c.font = 'bold italic 12px monospace'
834 c.textAlign = 'center'
835 c.textBaseline = 'middle'
836 c.fillText 'fin', 50, 37.5
837
838 # Helper for drawing all of the level.
839 drawLevel: ->
840 state.level.drawBase()
841 for sentry in state.sentries
842 sentry.drawVisibility()
843 for charge in state.charges
844 charge.drawEffectArea()
845 charge.drawBody()
846 state.player.draw()
847 for sentry in state.sentries
848 sentry.drawBody()
849 state.level.drawWalls()
850 @drawHud()
851
852 drawHud: ->
853 numCharges = 0
854 for charge in state.charges
855 numCharges++ unless charge.placed or charge.triggered
856
857 c = mainCanvas.context
858 c.fillStyle = "#ccc"
859 c.font = 'bold 2px monospace'
860
861 c.textBaseline = 'top'
862 c.textAlign = 'left'
863 c.fillText "level: #{state.levelIdx + 1}", 2, 2
864 c.textAlign = 'right'
865 c.fillText "charges: #{numCharges}", 98, 2
866 c.textAlign = 'center'
867 c.fillText state.title, 50, 2
868
869 c.textBaseline = 'bottom'
870
871 if state.tutorial
872 text = switch state.tutorial
873 when 1
874 "Move to the right using the arrow keys"
875 when 2
876 "Keep moving right, up to the wall behind the sentry"
877 when 3
878 "Press the spacebar to deploy an EMP charge here"
879 when 4
880 "Press the spacebar again to trigger the charge"
881 when 5
882 "Place multiple charges, trigger once to destroy both sentries"
883 if text
884 c.fillText text, 50, 70
885
886 text = switch state.player?.actionResult?[0]
887 when 'place'
888 'place charge'
889 when 'trigger'
890 c.fillStyle = '#f80'
891 'trigger charges'
892 if text
893 c.fillText "[space] = #{text}", 50, 73
894
895
896 # Entry!
897 gameLoop.start()
Something went wrong with that request. Please try again.