Begin bug tracking.
[featherfall2.git] / src / yuu / ce.js
1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license http://www.gnu.org/licenses/gpl-2.0.html
4 @source: http://yukkurigames.com/yuu/
5 */
6
7 (function (yuu) {
8 "use strict";
9
10 /** yuu-ce - entity/component system for the Yuu engine
11
12 Game logic in Yuu is implemented via entities and components.
13 Entities (yuu.E) represent "things" in the world, and
14 components (yuu.C) individual properties or abilities of those
15 things. By attaching and detaching components, entities gain
16 and lose those abilities.
17
18 This system prioritizes for convenience and simplicity over
19 performance. Many common optimizations in E/C systems, like
20 component pooling and array-of-structures, are not
21 implemented. (And are of questionable value in a language like
22 JavaScript.)
23 */
24
25 var yT = this.yT || require("./yT");
26 var yf = this.yf || require("./yf");
27
28 yuu.E = yT({
29 constructor: function () {
30 /** Entity, a shell to customize with components
31
32 Entities exist as an aggregate of components (yuu.C).
33 They are used for components to talk to each other, or
34 other systems to handle systems in aggregate without
35 caring about the underlying details.
36
37 Entities expose components in two ways.
38
39 First, components may have one or more slots; when a
40 component is attached to an entity it slots itself
41 into those properties on that entity. For example, if
42 you attach a Transform component to an entity, you can
43 retrieve it via e.transform.
44
45 Second, components may have one or more message
46 taps. This allows them to listen for, and respond to,
47 messages sent to the entity. Unlike slots many
48 attached components may have the same tap.
49 */
50 this.parent = null;
51 this.children = [];
52 this.taps = {};
53 this.attach.apply(this, arguments);
54 },
55
56 addChild: function (child) { this.addChildren(child); },
57 removeChild: function (child) { this.removeChildren(child); },
58
59 addChildren: function () {
60 yf.stash("parent", this, arguments);
61 this.children = this.children.concat(yf.slice(arguments));
62 },
63
64 removeChildren: function () {
65 this.children = yf.filter(
66 yf.lacks.bind(null, arguments), this.children);
67 yf.stash("parent", null, arguments);
68 },
69
70 attach: function () {
71 /** Attach a component to this entity.
72
73 If the entity already has a component in the same slots,
74 an error will be thrown.
75 */
76 for (var j = 0; j < arguments.length; ++j) {
77 var c = arguments[j];
78 var i;
79 for (i = 0; i < c.SLOTS.length; ++i)
80 if (this[c.SLOTS[i].slot])
81 throw new Error("Entity already has a " + c.SLOTS[i]);
82 for (i = 0; i < c.SLOTS.length; ++i)
83 this[c.SLOTS[i]] = c;
84 for (i = 0; i < c.TAPS.length; ++i)
85 this.taps[c.TAPS[i]] = (this.taps[c.TAPS[i]] || [])
86 .concat(c);
87 c.entity = this;
88 c.attached(this);
89 }
90 },
91
92 detach: function () {
93 /** Detach a component from this entity */
94 for (var j = 0; j < arguments.length; ++j) {
95 var c = arguments[j];
96 var i;
97 for (i = 0; i < c.SLOTS.length; ++i)
98 if (this[c.SLOTS[i].slot] !== c)
99 throw new Error("Entity has a wrong " + c.SLOTS[i]);
100 for (i = 0; i < c.SLOTS.length; ++i)
101 delete this[c.SLOTS[i]];
102 for (i = 0; i < c.TAPS.length; ++i)
103 this.taps[c.TAPS[i]] = yf.without(this.taps[c.TAPS[i]], c);
104 c.entity = null;
105 c.detached(this);
106 }
107 },
108
109 _message: function (name, params) {
110 var taps = this.taps[name];
111 var children = this.children;
112 var i;
113 if (taps)
114 for (i = 0; i < taps.length; ++i)
115 taps[i][name].apply(taps[i], params);
116 for (i = 0; i < children.length; ++i)
117 children[i]._message(name, params);
118 },
119 message: function (name) {
120 /** Message components listening on the named tap */
121 this._message(name, yf.tail(arguments));
122 },
123 });
124
125 yuu.C = yT({
126 entity: { value: null, writable: true },
127 SLOTS: { value: [], configurable: true },
128 TAPS: { value: [], configurable: true },
129
130 attached: function (entity) { },
131 detached: function (entity) { },
132 });
133
134 yuu.DataC = yT(yuu.C, {
135 /** A component for random scratch data
136
137 Storing this in a separate component rather than on the
138 entity directly reduces the chance of naming conflicts and
139 also the number of hidden classes.
140 */
141
142 constructor: function (data) {
143 Object.assign(this, data || {});
144 },
145
146 SLOTS: ["data"]
147 });
148
149
150 yuu.Animation = yT(yuu.C, {
151 constructor: function (timeline, params, completionHandler, delay) {
152 this.timeline = yf.mapValues(yf.arrayify, timeline);
153 this.params = params;
154 this.completionHandler = completionHandler;
155 this.keys = Object.keys(timeline)
156 .sort(function (a, b) {
157 return +this._lookup(a) - +this._lookup(b);
158 }.bind(this));
159 this._t1 = +this._lookup(yf.last(this.keys)) + 1;
160 this._t = -(delay || 0);
161 this._pc = 0;
162 this._tweens = [];
163 },
164
165 attached: function () {
166 this.tick();
167 },
168
169 _lookup: function (k) {
170 return (k in this.params) ? this.params[k] : k;
171 },
172
173 set1: function (setter) {
174 var $ = this.params.$;
175 yf.ipairs.call(this, function (k, v) {
176 $[k] = this._lookup(v);
177 }, setter);
178 },
179
180 set: function (setters) {
181 yf.ipairs.call(this, function (name, setter) {
182 var $ = this._lookup(name);
183 yf.ipairs.call(this, function (k, v) {
184 $[k] = this._lookup(v);
185 }, setter);
186 }, setters);
187 },
188
189 _addTween: function (tweens, instr) {
190 var repeat = instr.repeat || 0;
191 var cycles = Math.abs(repeat) + 1;
192 var easing = yf.isFunction(instr.easing)
193 ? instr.easing
194 : yuu.Tween[(instr.easing || "ease").toUpperCase()];
195 var duration, complete;
196
197 if ("complete" in instr) {
198 complete = this._lookup(instr.complete);
199 duration = (complete - this._t) / cycles;
200 } else if ("duration" in instr) {
201 duration = this._lookup(instr.duration);
202 complete = this._t + duration * cycles;
203 }
204
205 if (isFinite(cycles)) {
206 this._tweens.push(
207 new yuu.Tween(tweens, duration, repeat, easing));
208 this._t1 = Math.max(complete + 1, this._t1);
209 } else {
210 this.entity.attach(new yuu.TweenC(
211 tweens, duration, repeat, easing));
212 }
213 },
214
215 tween1: function (tween, instr) {
216 var nt = { $: this._lookup(instr.$) || this.params.$ };
217 yf.ipairs.call(this, function (k, v) {
218 nt[k] = [nt.$[k], this._lookup(v)];
219 }, tween);
220 this._addTween([nt], instr);
221 },
222
223 tween: function (targets, instr) {
224 var tweens = [];
225 yf.ipairs.call(this, function (name, tween) {
226 var nt = { $: this._lookup(name) || this.params.$ };
227 yf.ipairs.call(this, function (k, v) {
228 nt[k] = [nt.$[k], this._lookup(v)];
229 }, tween);
230 tweens.push(nt);
231 }, targets);
232 this._addTween(tweens, instr);
233 },
234
235 tweenAll: function (tween, instr) {
236 var tweens = [];
237 var $s = this._lookup(instr.$s) || this.params.$s;
238 yf.irange.call(this, function (i) {
239 var nt = { $: $s[i] };
240 yf.ipairs.call(this, function (k, v) {
241 nt[k] = [nt.$[k], this._lookup(v)[i]];
242 }, tween);
243 tweens.push(nt);
244 }, $s.length);
245 this._addTween(tweens, instr);
246 },
247
248 event: function (name) {
249 this.params[name](this, this.params);
250 },
251
252 _dispatch: function (instr) {
253 if (instr.set1)
254 this.set1(instr.set1);
255 if (instr.set)
256 this.set(instr.set);
257 if (instr.tween1)
258 this.tween1(instr.tween1, instr);
259 if (instr.tween)
260 this.tween(instr.tween, instr);
261 if (instr.tweenAll)
262 this.tweenAll(instr.tweenAll, instr);
263 if (instr.event)
264 this.event(instr.event);
265 },
266
267 tick: function () {
268 var t = this._t;
269 var i;
270 for (var key = this.keys[this._pc];
271 this._lookup(key) <= t;
272 key = this.keys[++this._pc]) {
273 yf.each.call(this, this._dispatch, this.timeline[key]);
274 }
275
276 for (i = this._tweens.length - 1; i >= 0; --i ) {
277 if (this._tweens[i].tick())
278 this._tweens.splice(i, 1);
279 }
280
281 if (++this._t > this._t1) {
282 if (this.completionHandler)
283 this.completionHandler(this);
284 this.entity.detach(this);
285 }
286 },
287
288 tock: function (p) {
289 for (var i = this._tweens.length - 1; i >= 0; --i)
290 this._tweens[i].tock(p);
291 },
292
293 TAPS: ["tick", "tock"]
294 });
295
296 yuu.Tween = yT({
297 /** Tween object properties over time
298
299 This component changes properties over time, and can
300 handle synchronizing multiple objects and multiple
301 properties.
302
303 The `property` is either a single object with the special
304 `$` property set to the object to tween and every other
305 property set to the properties to tween with values [min,
306 max], or a list of such objects. For example, to tween a.x
307 from 0 to 1, a.y from 2 to 3, and b.z from 1 to 2, you
308 would pass
309
310 [{ $: a, x: [0, 1], y: [2, 3] },
311 { $: b, z: [1, 2] }]
312
313 The `duration` is specified in ticks (e.g. calls to
314 director.tick).
315
316 `repeat` may be a positive number to repeat the tween that
317 many times, or a negative number to cycle back to the
318 minimum (and then back to the maximum, etc.) that many
319 times. `Infinity` will repeat the tween forever and
320 `-Infinity` will cycle the tween back and forth forever.
321
322 A custom easing equation may be provided. This is a
323 function which takes a p = [0, 1] and returns the eased p.
324 */
325
326 constructor: function (props, duration, repeat, easing) {
327 this._object = [];
328 this._property = [];
329 this._a = [];
330 this._b = [];
331 this._count = 0;
332 this.duration = duration || 60;
333 this.repeat = repeat || 0;
334 this.easing = easing || yuu.Tween.LINEAR;
335 yf.each.call(this, function (oab) {
336 yf.ipairs.call(this, function (name, ab) {
337 if (name !== "$") {
338 this._object.push(oab.$);
339 this._property.push(name);
340 this._a.push(ab[0]);
341 this._b.push(ab[1]);
342 }
343 }, oab);
344 }, yf.arrayify(props));
345 this._updateAt(0);
346 },
347
348 tick: function () {
349 var t = this._count / this.duration;
350 ++this._count;
351 if (t > 1 && !this.repeat)
352 return true;
353 else if (t >= 1 && this.repeat) {
354 if (this.repeat < 0) {
355 var n = this._a;
356 this._a = this._b;
357 this._b = n;
358 }
359 this._count = 1;
360 t = 0;
361
362 if (this.repeat < 0) {
363 this.repeat++;
364 } else if (this.repeat > 0) {
365 this.repeat--;
366 }
367 }
368 this._updateAt(t);
369 },
370
371 tock: function (p) {
372 var t = (this._count + p - 1) / this.duration;
373 if (t <= 1)
374 this._updateAt(t);
375 },
376
377 _updateAt: function (t) {
378 var p = this.easing ? this.easing(t) : t;
379 for (var i = 0; i < this._object.length; ++i) {
380 // a was the existing property, b was the one provided
381 // by the user. By lerping from b to a, the user can
382 // control the lerp type in some awkward cases -
383 // e.g. CSS DOM values are all exposed as strings so a
384 // will be a string/String, but if b is provided as a
385 // number/Number, this will lerp numerically.
386 //
387 // FIXME: This still doesn't work right if the lerp is
388 // later reversed due to negative repeats.
389 var object = this._object[i];
390 var property = this._property[i];
391 var a = this._a[i];
392 var b = this._b[i];
393 object[property] = yuu.lerp(b, a, 1 - p);
394 }
395 },
396
397 count: {
398 get: function () { return this._count; },
399 set: function (v) { this._count = Math.round(v); },
400 },
401
402 duration: { value: 60, chainable: true },
403 repeat: { value: 0, chainable: true },
404 easing: { value: null, chainable: true },
405 });
406
407 yuu.TweenC = yT(yuu.C, {
408 constructor: function () {
409 this._tween = yf.construct(yuu.Tween, arguments);
410 },
411
412 tick: function () {
413 if (this._tween.tick())
414 this.entity.detach();
415 },
416
417 tock: { proxy: "_tween.tock" },
418 count: { alias: "_tween.count" },
419 duration: { alias: "_tween.duration", chainable: true },
420 repeat: { alias: "_tween.repeat", chainable: true },
421 easing: { alias: "_tween.easing", chainable: true },
422
423 TAPS: ["tick", "tock"]
424 });
425
426 yuu.Tween.LINEAR = null;
427 /** No easing */
428
429 yuu.Tween.EASE = function (p) {
430 /** Ease in and out
431
432 This equation is from _Improving Noise_ (Perlin, 2002). It
433 is symmetrical around p=0.5 and has zero first and second
434 derivatives at p=0 and p=1.
435 */
436 return p * p * p * (p * (p * 6.0 - 15.0) + 10.0);
437 };
438
439 yuu.Tween.EASE_IN = function (p) {
440 return p * p * p;
441 };
442
443 yuu.Tween.METASPRING = function (amplitude, pulsation) {
444 /** A generator for springy tweens
445
446 The amplitude controls how far from the final position the
447 spring will bounce, as a multiple of the distance between the
448 start and end. A "normal" amplitude is around 0.5 to 1.5.
449
450 The pulsation constant controls the rigidity of the spring;
451 higher pulsation results in a spring that bounces more quickly
452 and more often during a fixed interval. A "normal" pulsation
453 constant is around 15 to 30.
454 */
455 return function (p) {
456 return 1 + Math.cos(pulsation * p + Math.PI) * (1 - p) * amplitude;
457 };
458 };
459
460 yuu.Tween.STEPPED = function (segments, alpha) {
461 return function (p) {
462 p = p * segments;
463 var lower = Math.floor(p);
464 var upper = Math.floor((p + alpha));
465 if (upper > lower) {
466 var p1 = 1 - (upper - p) / alpha;
467 return (lower + p1) / segments;
468 } else {
469 return lower / segments;
470 }
471 };
472 };
473
474 yuu.Transform = yT(yuu.C, {
475 /** A 3D position, rotation (as quaternion), and scale
476
477 This also serves as an object lesson for a simple slotted
478 component.
479 */
480 constructor: function (position, rotation, scale) {
481 this._position = vec3.clone(position || [0, 0, 0]);
482 this._rotation = quat.clone(rotation || [0, 0, 0, 1]);
483 this._scale = vec3.clone(scale || [1, 1, 1]);
484 this._matrix = mat4.create();
485 this._dirty = true;
486 this._version = 0;
487 this._parentVersion = null;
488 },
489
490 SLOTS: ["transform"],
491
492 position: {
493 chainable: true,
494 get: function () { return this._position.slice(); },
495 set: function (v) { this._dirty = true;
496 vec3.copy(this._position, v); }
497 },
498 rotation: {
499 chainable: true,
500 get: function () { return this._rotation.slice(); },
501 set: function (v) { this._dirty = true;
502 quat.normalize(this._rotation, v); }
503 },
504 scale: {
505 chainable: true,
506 get: function () { return this._scale.slice(); },
507 set: function (v) { this._dirty = true;
508 vec3.copy(this._scale, v); }
509 },
510 x: {
511 chainable: true,
512 get: function () { return this._position[0]; },
513 set: function (x) { this._dirty = true; this._position[0] = x; }
514 },
515 y: {
516 chainable: true,
517 get: function () { return this._position[1]; },
518 set: function (x) { this._dirty = true; this._position[1] = x; }
519 },
520 z: {
521 chainable: true,
522 get: function () { return this._position[2]; },
523 set: function (x) { this._dirty = true; this._position[2] = x; }
524 },
525 xy: { swizzle: "xy", chainable: true },
526
527 scaleX: {
528 chainable: true,
529 get: function () { return this._scale[0]; },
530 set: function (x) { this._dirty = true; this._scale[0] = x; }
531 },
532 scaleY: {
533 chainable: true,
534 get: function () { return this._scale[1]; },
535 set: function (x) { this._dirty = true; this._scale[1] = x; }
536 },
537 scaleZ: {
538 chainable: true,
539 get: function () { return this._scale[2]; },
540 set: function (x) { this._dirty = true; this._scale[2] = x; }
541 },
542
543 worldToLocal: function (p) {
544 var x = (p.x || p[0] || 0);
545 var y = (p.y || p[1] || 0);
546 var z = (p.z || p[2] || 0);
547 var local = [x, y, z];
548 var matrix = mat4.clone(this.matrix);
549 return vec3.transformMat4(local, local, mat4.invert(matrix, matrix));
550 },
551
552 contains: function (p) {
553 p = this.worldToLocal(p);
554 return p[0] >= -0.5 && p[0] < 0.5
555 && p[1] >= -0.5 && p[1] < 0.5
556 && p[2] >= -0.5 && p[2] < 0.5;
557 },
558
559 ypr: {
560 chainable: true,
561 get: function () {
562 var q = this._rotation;
563 var x = q[0]; var sqx = x * x;
564 var y = q[1]; var sqy = y * y;
565 var z = q[2]; var sqz = z * z;
566 var w = q[3];
567 var abcd = w * x + y * z;
568 if (abcd > 0.499)
569 return [2 * Math.atan2(x, w), Math.PI / 2, 0];
570 else if (abcd < -0.499)
571 return [-2 * Math.atan2(x, w), -Math.PI / 2, 0];
572 else {
573 var adbc = w * z - x * y;
574 var acbd = w * y - x * z;
575 return [Math.atan2(2 * adbc, 1 - 2 * (sqz + sqx)),
576 Math.asin(2 * abcd),
577 Math.atan2(2 * acbd, 1 - 2 * (sqy + sqx))];
578 }
579
580 },
581 set: function (ypr) {
582 var q = this._rotation;
583 quat.identity(q);
584 quat.rotateZ(q, q, ypr[0]);
585 quat.rotateY(q, q, ypr[2]);
586 quat.rotateX(q, q, ypr[1]);
587 this._dirty = true;
588 }
589 },
590
591 yaw: { aliasSynthetic: "ypr[0]", chainable: true },
592 pitch: { aliasSynthetic: "ypr[1]", chainable: true },
593 roll: { aliasSynthetic: "ypr[2]", chainable: true },
594
595 matrix: {
596 get: function () {
597 var pt = this.entity.parent && this.entity.parent.transform;
598 var pm = pt && pt.matrix;
599 var ptVersion = pt && pt._version;
600 if (this._dirty || (ptVersion !== this._parentVersion)) {
601 var m = this._matrix;
602 mat4.identity(m);
603 mat4.fromRotationTranslation(
604 m, this._rotation, this._position);
605 mat4.scale(m, m, this._scale);
606 if (pm)
607 mat4.multiply(m, pm, m);
608 this._dirty = false;
609 this._matrix = m;
610 this._parentVersion = ptVersion;
611 this._version = (this._version + 1) | 0;
612 }
613 return this._matrix;
614 }
615 }
616 });
617
618 yuu.Ticker = yT(yuu.C, {
619 /** Set a callback to run every n ticks
620
621 If the callback returns true, it is rescheduled for
622 execution (like setInterval). If it returns false, this
623 component is removed from the entity.
624 */
625 constructor: function (callback, interval, delay) {
626 this.callback = callback;
627 this.interval = interval;
628 this._accum = 0;
629 this._count = -(delay || 0);
630 },
631
632 tick: function () {
633 this._accum += 1;
634 if (this._accum === this.interval) {
635 this._accum = 0;
636 if (!this.callback(this._count++))
637 this.entity.detach(this);
638 }
639 },
640
641 TAPS: ["tick"]
642 });
643
644 }).call(typeof exports === "undefined" ? this : exports,
645 typeof exports === "undefined"
646 ? this.yuu : (module.exports = require('./core')));