pixie_dust 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README +16 -0
  5. data/README.md +29 -0
  6. data/Rakefile +1 -0
  7. data/game.js +9200 -0
  8. data/lib/corelib.js +3331 -0
  9. data/lib/pixie_dust/version.rb +3 -0
  10. data/lib/pixie_dust.rb +14 -0
  11. data/pixie.json +15 -0
  12. data/pixie_dust.gemspec +29 -0
  13. data/source/active_bounds.coffee +48 -0
  14. data/source/ageable.coffee +23 -0
  15. data/source/bounded.coffee +282 -0
  16. data/source/camera.coffee +138 -0
  17. data/source/camera.fade.coffee +69 -0
  18. data/source/camera.flash.coffee +69 -0
  19. data/source/camera.rotate.coffee +11 -0
  20. data/source/camera.shake.coffee +27 -0
  21. data/source/camera.zoom.coffee +25 -0
  22. data/source/camera.zsort.coffee +13 -0
  23. data/source/clampable.coffee +61 -0
  24. data/source/collidable.coffee +126 -0
  25. data/source/collision.coffee +272 -0
  26. data/source/collision_response.coffee +28 -0
  27. data/source/color.coffee +1113 -0
  28. data/source/color_table.coffee +2534 -0
  29. data/source/controllable.coffee +66 -0
  30. data/source/cooldown.coffee +82 -0
  31. data/source/debuggable.coffee +253 -0
  32. data/source/drawable.coffee +167 -0
  33. data/source/dust_emitter.coffee +36 -0
  34. data/source/easing.coffee +38 -0
  35. data/source/emitter.coffee +7 -0
  36. data/source/emitterable.coffee +68 -0
  37. data/source/engine.coffee +274 -0
  38. data/source/engine.collision.coffee +77 -0
  39. data/source/engine.data.coffee +23 -0
  40. data/source/engine.delay.coffee +41 -0
  41. data/source/engine.fps_counter.coffee +32 -0
  42. data/source/engine.game_state.coffee +86 -0
  43. data/source/engine.joysticks.coffee +47 -0
  44. data/source/engine.keyboard.coffee +17 -0
  45. data/source/engine.levels.coffee +69 -0
  46. data/source/engine.mouse.coffee +16 -0
  47. data/source/engine.selector.coffee +166 -0
  48. data/source/engine.stats.coffee +16 -0
  49. data/source/engine.tilemap.coffee +41 -0
  50. data/source/engine_background.coffee +32 -0
  51. data/source/expirable.coffee +47 -0
  52. data/source/flickerable.coffee +78 -0
  53. data/source/follow.coffee +65 -0
  54. data/source/framerate.coffee +42 -0
  55. data/source/game_object.coffee +181 -0
  56. data/source/game_object.effect.coffee +33 -0
  57. data/source/game_object.meter.coffee +191 -0
  58. data/source/game_over.coffee +40 -0
  59. data/source/game_state.coffee +67 -0
  60. data/source/game_state.save_state.coffee +76 -0
  61. data/source/game_state.single_camera.coffee +40 -0
  62. data/source/game_state_cameras.coffee +33 -0
  63. data/source/level_state.coffee +32 -0
  64. data/source/movable.coffee +57 -0
  65. data/source/oscillator.coffee +18 -0
  66. data/source/pixie_dust.coffee +2 -0
  67. data/source/resource_loader.coffee +35 -0
  68. data/source/rotatable.coffee +38 -0
  69. data/source/sprite.coffee +181 -0
  70. data/source/text_effect.coffee +74 -0
  71. data/source/text_effect.floating.coffee +22 -0
  72. data/source/text_screen.coffee +38 -0
  73. data/source/tilemap.coffee +56 -0
  74. data/source/timed_events.coffee +78 -0
  75. data/source/title_screen.coffee +38 -0
  76. data/source/tween.coffee +70 -0
  77. data/test/active_bounds.coffee +67 -0
  78. data/test/bounded.coffee +98 -0
  79. data/test/camera.coffee +29 -0
  80. data/test/clampable.coffee +18 -0
  81. data/test/collidable.coffee +51 -0
  82. data/test/collision.coffee +70 -0
  83. data/test/color.coffee +533 -0
  84. data/test/controllable.coffee +108 -0
  85. data/test/cooldown.coffee +116 -0
  86. data/test/debuggable.coffee +71 -0
  87. data/test/drawable.coffee +31 -0
  88. data/test/emitter.coffee +0 -0
  89. data/test/emitterable.coffee +15 -0
  90. data/test/engine.coffee +228 -0
  91. data/test/engine_data.coffee +12 -0
  92. data/test/engine_delay.coffee +14 -0
  93. data/test/engine_selector.coffee +100 -0
  94. data/test/expirable.coffee +35 -0
  95. data/test/flickerable.coffee +51 -0
  96. data/test/follow.coffee +34 -0
  97. data/test/game_object.coffee +78 -0
  98. data/test/game_object_effect.coffee +17 -0
  99. data/test/metered.coffee +33 -0
  100. data/test/movable.coffee +46 -0
  101. data/test/oscillator.coffee +28 -0
  102. data/test/resource_loader.coffee +7 -0
  103. data/test/rotatable.coffee +20 -0
  104. data/test/sprite.coffee +21 -0
  105. data/test/text.coffee +25 -0
  106. data/test/timed_events.coffee +23 -0
  107. data/test/tweening.coffee +18 -0
  108. metadata +233 -0
@@ -0,0 +1,27 @@
1
+ Camera.Shake = (I, self) ->
2
+ Object.reverseMerge I,
3
+ shakeIntensity: 20
4
+ shakeCooldown: 0
5
+
6
+ defaultParams =
7
+ duration: 10
8
+ intensity: 20
9
+
10
+ self.bind "afterUpdate", ->
11
+ I.shakeCooldown = I.shakeCooldown.approach(0, 1)
12
+
13
+ self.transformFilterChain (transform) ->
14
+ if I.shakeCooldown > 0
15
+ transform.tx += signedRand(I.shakeIntensity)
16
+ transform.ty += signedRand(I.shakeIntensity)
17
+
18
+ return transform
19
+
20
+ shake: (options={}) ->
21
+ {duration, intensity} = Object.reverseMerge(options, defaultParams)
22
+
23
+ I.shakeCooldown = duration * I.zoom
24
+ I.shakeIntensity = intensity * I.zoom
25
+
26
+ self
27
+
@@ -0,0 +1,25 @@
1
+ Camera.Zoom = (I, self) ->
2
+ Object.reverseMerge I,
3
+ maxZoom: 10
4
+ minZoom: 0.1
5
+ zoom: 1
6
+
7
+ self.transformFilterChain (transform) ->
8
+ transform.scale(I.zoom, I.zoom, self.position())
9
+
10
+ clampZoom = (value) ->
11
+ value.clamp(I.minZoom, I.maxZoom)
12
+
13
+ zoomIn: (percentage) ->
14
+ self.zoom clampZoom(I.zoom * (1 + percentage))
15
+
16
+ zoomOut: (percentage) ->
17
+ self.zoom clampZoom(I.zoom * (1 - percentage))
18
+
19
+ zoom: (value) ->
20
+ if value?
21
+ I.zoom = clampZoom(value)
22
+
23
+ return self
24
+ else
25
+ return I.zoom
@@ -0,0 +1,13 @@
1
+ Camera.ZSort = (I, self) ->
2
+ Object.reverseMerge I,
3
+ zSort: true
4
+
5
+ self.objectFilterChain (objects) ->
6
+ if I.zSort
7
+ objects.sort (a, b) ->
8
+ a.I.zIndex - b.I.zIndex
9
+
10
+ objects
11
+
12
+ return {}
13
+
@@ -0,0 +1,61 @@
1
+ ###*
2
+ The `Clampable` module provides helper methods to clamp object properties. This module is included by default in `GameObject`
3
+
4
+ player = GameObject
5
+ x: 40
6
+ y: 30
7
+
8
+ player.include Clampable
9
+
10
+ @name Clampable
11
+ @module
12
+ @constructor
13
+ @param {Object} I Instance variables
14
+ @param {Core} self Reference to including object
15
+ ###
16
+ Clampable = (I={}, self) ->
17
+ Object.reverseMerge I,
18
+ clampData: {}
19
+
20
+ self.bind "afterUpdate", ->
21
+ for property, data of I.clampData
22
+ I[property] = I[property].clamp(data.min, data.max)
23
+
24
+ ###*
25
+ Keep an objects attributes within a given range.
26
+
27
+ # Player's health will be within [0, 100] at the end of every update
28
+ player.clamp
29
+ health:
30
+ min: 0
31
+ max: 100
32
+
33
+ # Score can only be positive
34
+ player.clamp
35
+ score:
36
+ min: 0
37
+
38
+ @name clamp
39
+ @methodOf Clampable#
40
+ @param {Object} data
41
+ ###
42
+ clamp: (data) ->
43
+ Object.extend(I.clampData, data)
44
+
45
+ ###*
46
+ Helper to clamp the `x` and `y` properties of the object to be within a given bounds.
47
+
48
+ @name clampToBounds
49
+ @methodOf Clampable#
50
+ @param {Rectangle} [bounds] The bounds to clamp the object's position within. Defaults to the app size if none given.
51
+ ###
52
+ clampToBounds: (bounds) ->
53
+ bounds ||= Rectangle x: 0, y: 0, width: App.width, height: App.height
54
+
55
+ self.clamp
56
+ x:
57
+ min: bounds.x + I.width/2
58
+ max: bounds.width - I.width/2
59
+ y:
60
+ min: bounds.y + I.height/2
61
+ max: bounds.height - I.height/2
@@ -0,0 +1,126 @@
1
+ ( ->
2
+ Collidable = (I, self) ->
3
+ # Set some default properties
4
+ Object.reverseMerge I,
5
+ allowCollisions: ANY
6
+ immovable: false
7
+ touching: NONE
8
+ velocity: Point(0, 0)
9
+ mass: 1
10
+ elasticity: 0
11
+
12
+ self.attrAccessor(
13
+ "immovable",
14
+ "velocity",
15
+ "mass",
16
+ "elasticity",
17
+ )
18
+
19
+ solid: (newSolid) ->
20
+ if newSolid?
21
+ if newSolid
22
+ I.allowCollisions = ANY
23
+ else
24
+ I.allowCollisions = NONE
25
+ else
26
+ I.allowCollisions
27
+
28
+ (exports ? this)["Collidable"] = Collidable
29
+
30
+ {NONE, LEFT, RIGHT, UP, DOWN} = Object.extend Collidable,
31
+ NONE: 0x0000
32
+ LEFT: 0x0001
33
+ RIGHT: 0x0010
34
+ UP: 0x0100
35
+ DOWN: 0x1000
36
+
37
+ {ANY, FLOOR, WALL, CEILING} = Object.extend Collidable,
38
+ FLOOR: DOWN
39
+ WALL: LEFT | RIGHT
40
+ CEILING: UP
41
+ ANY: LEFT | RIGHT | UP | DOWN
42
+
43
+ Object.extend Collidable,
44
+ separate: (a, b) ->
45
+ return if a.immovable() && b.immovable()
46
+
47
+ aBounds = a.bounds()
48
+ bBounds = b.bounds()
49
+
50
+ aVelocity = a.velocity()
51
+ bVelocity = b.velocity()
52
+
53
+ deltaVelocity = aVelocity.subtract(bVelocity)
54
+
55
+ overlap = Point(0, 0)
56
+
57
+ if Collision.rectangular(aBounds, bBounds)
58
+ if deltaVelocity.x > 0
59
+ overlap.x = aBounds.x + aBounds.width - bBounds.x
60
+ if !(a.I.allowCollisions & RIGHT) || !(b.I.allowCollisions & LEFT)
61
+ overlap.x = 0
62
+ else
63
+ a.I.touching |= RIGHT
64
+ b.I.touching |= LEFT
65
+
66
+ else if deltaVelocity.x < 0
67
+ overlap.x = aBounds.x - bBounds.width - bBounds.x
68
+ if !(a.I.allowCollisions & LEFT) || !(b.I.allowCollisions & RIGHT)
69
+ overlap.x = 0
70
+ else
71
+ a.I.touching |= LEFT
72
+ b.I.touching |= RIGHT
73
+
74
+ if deltaVelocity.y > 0
75
+ overlap.y = aBounds.y + aBounds.height - bBounds.y
76
+ if !(a.I.allowCollisions & DOWN) || !(b.I.allowCollisions & UP)
77
+ overlap.y = 0
78
+ else
79
+ a.I.touching |= DOWN
80
+ b.I.touching |= UP
81
+
82
+ else if deltaVelocity.y < 0
83
+ overlap.y = aBounds.y - bBounds.height - bBounds.y
84
+ if !(a.I.allowCollisions & UP) || !(b.I.allowCollisions & DOWN)
85
+ overlap.y = 0
86
+ else
87
+ a.I.touching |= UP
88
+ b.I.touching |= DOWN
89
+
90
+ unless overlap.equal(Point.ZERO)
91
+ if !a.immovable() and !b.immovable()
92
+ a.changePosition(overlap.scale(-0.5))
93
+ b.changePosition(overlap.scale(+0.5))
94
+
95
+ # Elastic collision
96
+ relativeVelocity = aVelocity.subtract(bVelocity)
97
+
98
+ aMass = a.mass()
99
+ bMass = b.mass()
100
+ totalMass = bMass + aMass
101
+
102
+ normal = overlap.norm()
103
+
104
+ pushA = normal.scale(-2 * (relativeVelocity.dot(normal) * (bMass / totalMass)))
105
+ pushB = normal.scale(+2 * (relativeVelocity.dot(normal) * (aMass / totalMass)))
106
+ average = pushA.add(pushB).scale(0.5)
107
+
108
+ pushA.subtract$(average).scale(a.elasticity())
109
+ pushB.subtract$(average).scale(b.elasticity())
110
+
111
+ a.I.velocity = average.add(pushA)
112
+ b.I.velocity = average.add(pushB)
113
+
114
+ else if !a.immovable()
115
+ a.changePosition(overlap.scale(-1))
116
+
117
+ a.I.velocity = bVelocity.subtract(aVelocity.scale(a.elasticity()))
118
+
119
+ else if !b.immovable()
120
+ b.changePosition(overlap)
121
+
122
+ b.I.velocity = aVelocity.subtract(bVelocity.scale(b.elasticity()))
123
+
124
+ return true
125
+ )()
126
+
@@ -0,0 +1,272 @@
1
+ ( ->
2
+ # Assume game objects
3
+ collides = (a, b) ->
4
+ # TODO: Be smart about auto-detecting collision types
5
+ Collision.rectangular(a.bounds(), b.bounds())
6
+
7
+ ###*
8
+ Collision holds many useful class methods for checking geometric overlap of various objects.
9
+
10
+ @name Collision
11
+ @namespace
12
+ ###
13
+ Collision =
14
+ ###*
15
+ Collision holds many useful class methods for checking geometric overlap of various objects.
16
+
17
+ player = engine.add
18
+ class: "Player"
19
+ x: 0
20
+ y: 0
21
+ width: 10
22
+ height: 10
23
+
24
+ enemy = engine.add
25
+ class: "Enemy"
26
+ x: 5
27
+ y: 5
28
+ width: 10
29
+ height: 10
30
+
31
+ enemy2 = engine.add
32
+ class: "Enemy"
33
+ x: -5
34
+ y: -5
35
+ width: 10
36
+ height: 10
37
+
38
+ Collision.collide(player, enemy, (p, e) -> ...)
39
+ # => callback is called once
40
+
41
+ Collision.collide(player, [enemy, enemy2], (p, e) -> ...)
42
+ # => callback is called twice
43
+
44
+ Collision.collide("Player", "Enemy", (p, e) -> ...)
45
+ # => callback is also called twice
46
+
47
+ @name collide
48
+ @methodOf Collision
49
+ @param {Object|Array|String} groupA An object or set of objects to check collisions with
50
+ @param {Object|Array|String} groupB An object or set of objects to check collisions with
51
+ @param {Function} callback The callback to call when an object of groupA collides
52
+ with an object of groupB: (a, b) ->
53
+ @param {Function} [detectionMethod] An optional detection method to determine when two
54
+ objects are colliding.
55
+ ###
56
+ collide: (groupA, groupB, callback, detectionMethod=collides) ->
57
+ if Object.isString(groupA)
58
+ groupA = engine.find(groupA)
59
+ else
60
+ groupA = [].concat(groupA)
61
+
62
+ if Object.isString(groupB)
63
+ groupB = engine.find(groupB)
64
+ else
65
+ groupB = [].concat(groupB)
66
+
67
+ groupA.each (a) ->
68
+ groupB.each (b) ->
69
+ callback(a, b) if detectionMethod(a, b)
70
+
71
+ ###*
72
+ Takes two bounds objects and returns true if they collide (overlap), false otherwise.
73
+ Bounds objects have x, y, width and height properties.
74
+
75
+ player = GameObject
76
+ x: 0
77
+ y: 0
78
+ width: 10
79
+ height: 10
80
+
81
+ enemy = GameObject
82
+ x: 5
83
+ y: 5
84
+ width: 10
85
+ height: 10
86
+
87
+ Collision.rectangular(player, enemy)
88
+ # => true
89
+
90
+ Collision.rectangular(player, {x: 50, y: 40, width: 30, height: 30})
91
+ # => false
92
+
93
+ @name rectangular
94
+ @methodOf Collision
95
+ @param {Object} a The first rectangle
96
+ @param {Object} b The second rectangle
97
+ @returns {Boolean} true if the rectangles overlap, false otherwise
98
+ ###
99
+ rectangular: (a, b) ->
100
+ a.x < b.x + b.width &&
101
+ a.x + a.width > b.x &&
102
+ a.y < b.y + b.height &&
103
+ a.y + a.height > b.y
104
+
105
+ ###*
106
+ Takes two circle objects and returns true if they collide (overlap), false otherwise.
107
+ Circle objects have x, y, and radius.
108
+
109
+ player = GameObject
110
+ x: 5
111
+ y: 5
112
+ radius: 10
113
+
114
+ enemy = GameObject
115
+ x: 10
116
+ y: 10
117
+ radius: 10
118
+
119
+ farEnemy = GameObject
120
+ x: 500
121
+ y: 500
122
+ radius: 30
123
+
124
+ Collision.circular(player, enemy)
125
+ # => true
126
+
127
+ Collision.circular(player, farEnemy)
128
+ # => false
129
+
130
+ @name circular
131
+ @methodOf Collision
132
+ @param {Object} a The first circle
133
+ @param {Object} b The second circle
134
+ @returns {Boolean} true is the circles overlap, false otherwise
135
+ ###
136
+ circular: (a, b) ->
137
+ r = a.radius + b.radius
138
+ dx = b.x - a.x
139
+ dy = b.y - a.y
140
+
141
+ r * r >= dx * dx + dy * dy
142
+
143
+ ###*
144
+ Detects whether a line intersects a circle.
145
+
146
+ circle = engine.add
147
+ class: "circle"
148
+ x: 50
149
+ y: 50
150
+ radius: 10
151
+
152
+ Collision.rayCircle(Point(0, 0), Point(1, 0), circle)
153
+ # => true
154
+
155
+ @name rayCircle
156
+ @methodOf Collision
157
+ @param {Point} source The starting position
158
+ @param {Point} direction A vector from the point
159
+ @param {Object} target The circle
160
+ @returns {Boolean} true if the line intersects the circle, false otherwise
161
+ ###
162
+ rayCircle: (source, direction, target) ->
163
+ radius = target.radius()
164
+ target = target.position()
165
+
166
+ laserToTarget = target.subtract(source)
167
+
168
+ projectionLength = direction.dot(laserToTarget)
169
+
170
+ if projectionLength < 0
171
+ return false # object is behind
172
+
173
+ projection = direction.scale(projectionLength)
174
+
175
+ intersection = source.add(projection)
176
+ intersectionToTarget = target.subtract(intersection)
177
+ intersectionToTargetLength = intersectionToTarget.length()
178
+
179
+ if intersectionToTargetLength < radius
180
+ hit = true
181
+
182
+ if hit
183
+ dt = Math.sqrt(radius * radius - intersectionToTargetLength * intersectionToTargetLength)
184
+
185
+ hit = direction.scale(projectionLength - dt).add(source)
186
+
187
+ ###*
188
+ Detects whether a line intersects a rectangle.
189
+
190
+ rect = engine.add
191
+ class: "circle"
192
+ x: 50
193
+ y: 50
194
+ width: 20
195
+ height: 20
196
+
197
+ Collision.rayRectangle(Point(0, 0), Point(1, 0), rect)
198
+ # => true
199
+
200
+ @name rayRectangle
201
+ @methodOf Collision
202
+ @param {Point} source The starting position
203
+ @param {Point} direction A vector from the point
204
+ @param {Object} target The rectangle
205
+ @returns {Boolean} true if the line intersects the rectangle, false otherwise
206
+ ###
207
+ rayRectangle: (source, direction, target) ->
208
+ unless target.xw? and target.yw?
209
+ if target.width? and target.height?
210
+ xw = target.width/2
211
+ yw = target.height/2
212
+
213
+ # Convert from bounds rect to centeredBounds rect
214
+ return Collision.rayRectangle source, direction,
215
+ x: target.x + xw
216
+ y: target.y + yw
217
+ xw: xw
218
+ yw: yw
219
+ else
220
+ error "Bounds object isn't a rectangle"
221
+
222
+ return
223
+
224
+ xw = target.xw
225
+ yw = target.yw
226
+
227
+ if source.x < target.x
228
+ xval = target.x - xw
229
+ else
230
+ xval = target.x + xw
231
+
232
+ if source.y < target.y
233
+ yval = target.y - yw
234
+ else
235
+ yval = target.y + yw
236
+
237
+ if direction.x == 0
238
+ p0 = Point(target.x - xw, yval)
239
+ p1 = Point(target.x + xw, yval)
240
+
241
+ t = (yval - source.y) / direction.y
242
+ else if direction.y == 0
243
+ p0 = Point(xval, target.y - yw)
244
+ p1 = Point(xval, target.y + yw)
245
+
246
+ t = (xval - source.x) / direction.x
247
+ else
248
+ tX = (xval - source.x) / direction.x
249
+ tY = (yval - source.y) / direction.y
250
+
251
+ # TODO: These special cases are gross!
252
+ if (tX < tY || (-xw < source.x - target.x < xw)) && !(-yw < source.y - target.y < yw)
253
+ p0 = Point(target.x - xw, yval)
254
+ p1 = Point(target.x + xw, yval)
255
+
256
+ t = tY
257
+ else
258
+ p0 = Point(xval, target.y - yw)
259
+ p1 = Point(xval, target.y + yw)
260
+
261
+ t = tX
262
+
263
+ if t > 0
264
+ areaPQ0 = direction.cross(p0.subtract(source))
265
+ areaPQ1 = direction.cross(p1.subtract(source))
266
+
267
+ if areaPQ0 * areaPQ1 < 0
268
+ hit = direction.scale(t).add(source)
269
+
270
+ (exports ? this)["Collision"] = Collision
271
+ )()
272
+
@@ -0,0 +1,28 @@
1
+ CollisionResponse = (I={}, self) ->
2
+ # Handle multi-include
3
+ self.unbind ".Movable"
4
+
5
+ self.bind 'update.Movable', (elapsedTime) ->
6
+ t = (elapsedTime * I.velocity.x).abs()
7
+ unit = I.velocity.x.sign()
8
+
9
+ # x dimension
10
+ t.times ->
11
+ if self.collide(unit, 0, ".solid")
12
+ I.velocity.x = 0
13
+ else
14
+ I.x += unit
15
+
16
+ t = (elapsedTime * I.velocity.y).abs()
17
+ unit = I.velocity.y.sign()
18
+ # y dimension
19
+ t.times ->
20
+ if self.collide(0, unit, ".solid")
21
+ I.velocity.y = 0
22
+ else
23
+ I.y += unit
24
+
25
+ self.extend
26
+ collide: (xOffset, yOffset, className) ->
27
+ engine.find(className).inject false, (hit, block) ->
28
+ hit || Collision.rectangular(self.bounds(xOffset, yOffset), block.bounds())