pixie_dust 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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())