Disable text selection in preferences.
[pwl6.git] / src / yuu / ce.js
1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license https://www.gnu.org/licenses/gpl-2.0.html
4 @source: https://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 var animationOps = [];
151
152 var Animation = yuu.Animation = yT(yuu.C, {
153 constructor: function (timeline, params, completionHandler, delay) {
154 this.timeline = yf.mapValues(yf.arrayify, timeline);
155 this.params = params;
156 this.completionHandler = completionHandler;
157 this.keys = Object.keys(timeline)
158 .sort(function (a, b) {
159 return +this._lookup(a) - +this._lookup(b);
160 }.bind(this));
161 this._t1 = +this._lookup(yf.last(this.keys)) + 1;
162 this._t = -(delay || 0);
163 this._pc = 0;
164 this._tweens = [];
165 },
166
167 attached: function () {
168 this.tick();
169 },
170
171 _lookup: function (k) {
172 return (k in this.params) ? this.params[k] : k;
173 },
174
175 set1: function (setter) {
176 var $ = this.params.$;
177 yf.ipairs.call(this, function (k, v) {
178 $[k] = this._lookup(v);
179 }, setter);
180 },
181
182 set: function (setters) {
183 yf.ipairs.call(this, function (name, setter) {
184 var $ = this._lookup(name);
185 yf.ipairs.call(this, function (k, v) {
186 $[k] = this._lookup(v);
187 }, setter);
188 }, setters);
189 },
190
191 _addTween: function (tweens, instr) {
192 var repeat = instr.repeat || 0;
193 var cycles = Math.abs(repeat) + 1;
194 var easing = yf.isFunction(instr.easing)
195 ? instr.easing
196 : yuu.Tween[(instr.easing || "ease").toUpperCase()];
197 var duration, complete;
198
199 if ("complete" in instr) {
200 complete = this._lookup(instr.complete);
201 duration = (complete - this._t) / cycles;
202 } else if ("duration" in instr) {
203 duration = this._lookup(instr.duration);
204 complete = this._t + duration * cycles;
205 }
206
207 if (isFinite(cycles)) {
208 this._tweens.push(
209 new yuu.Tween(tweens, duration, repeat, easing));
210 this._t1 = Math.max(complete + 1, this._t1);
211 } else {
212 this.entity.attach(new yuu.TweenC(
213 tweens, duration, repeat, easing));
214 }
215 },
216
217 tween1: function (tween, instr) {
218 var nt = { $: this._lookup(instr.$) || this.params.$ };
219 yf.ipairs.call(this, function (k, v) {
220 nt[k] = [nt.$[k], this._lookup(v)];
221 }, tween);
222 this._addTween([nt], instr);
223 },
224
225 tween: function (targets, instr) {
226 var tweens = [];
227 yf.ipairs.call(this, function (name, tween) {
228 var nt = { $: this._lookup(name) || this.params.$ };
229 yf.ipairs.call(this, function (k, v) {
230 nt[k] = [nt.$[k], this._lookup(v)];
231 }, tween);
232 tweens.push(nt);
233 }, targets);
234 this._addTween(tweens, instr);
235 },
236
237 tweenAll: function (tween, instr) {
238 var tweens = [];
239 var $s = this._lookup(instr.$s) || this.params.$s;
240 yf.irange.call(this, function (i) {
241 var nt = { $: $s[i] };
242 yf.ipairs.call(this, function (k, v) {
243 nt[k] = [nt.$[k], this._lookup(v)[i]];
244 }, tween);
245 tweens.push(nt);
246 }, $s.length);
247 this._addTween(tweens, instr);
248 },
249
250 event: function (name) {
251 this.params[name](this, this.params);
252 },
253
254 _dispatch: function (instr) {
255 if (instr.set1)
256 this.set1(instr.set1);
257 if (instr.set)
258 this.set(instr.set);
259 if (instr.tween1)
260 this.tween1(instr.tween1, instr);
261 if (instr.tween)
262 this.tween(instr.tween, instr);
263 if (instr.tweenAll)
264 this.tweenAll(instr.tweenAll, instr);
265 if (instr.event)
266 this.event(instr.event);
267 animationOps.forEach(function (f) {
268 if (instr[f.name])
269 f.call(this, instr[f.name], instr);
270 }, this);
271 },
272
273 tick: function () {
274 var t = this._t;
275 var i;
276 for (var key = this.keys[this._pc];
277 this._lookup(key) <= t;
278 key = this.keys[++this._pc]) {
279 yf.each.call(this, this._dispatch, this.timeline[key]);
280 }
281
282 for (i = this._tweens.length - 1; i >= 0; --i ) {
283 if (this._tweens[i].tick())
284 this._tweens.splice(i, 1);
285 }
286
287 if (++this._t > this._t1) {
288 if (this.completionHandler)
289 this.completionHandler(this);
290 this.entity.detach(this);
291 }
292 },
293
294 tock: function (p) {
295 for (var i = this._tweens.length - 1; i >= 0; --i)
296 this._tweens[i].tock(p);
297 },
298
299 TAPS: ["tick", "tock"]
300 });
301
302 Animation.registerOperation = function (f) {
303 animationOps.push(f);
304 };
305
306 yuu.Tween = yT({
307 /** Tween object properties over time
308
309 This component changes properties over time, and can
310 handle synchronizing multiple objects and multiple
311 properties.
312
313 The `property` is either a single object with the special
314 `$` property set to the object to tween and every other
315 property set to the properties to tween with values [min,
316 max], or a list of such objects. For example, to tween a.x
317 from 0 to 1, a.y from 2 to 3, and b.z from 1 to 2, you
318 would pass
319
320 [{ $: a, x: [0, 1], y: [2, 3] },
321 { $: b, z: [1, 2] }]
322
323 The `duration` is specified in ticks (e.g. calls to
324 director.tick).
325
326 `repeat` may be a positive number to repeat the tween that
327 many times, or a negative number to cycle back to the
328 minimum (and then back to the maximum, etc.) that many
329 times. `Infinity` will repeat the tween forever and
330 `-Infinity` will cycle the tween back and forth forever.
331
332 A custom easing equation may be provided. This is a
333 function which takes a p = [0, 1] and returns the eased p.
334 */
335
336 constructor: function (props, duration, repeat, easing) {
337 this._object = [];
338 this._property = [];
339 this._a = [];
340 this._b = [];
341 this._count = 0;
342 this.duration = duration || 60;
343 this.repeat = repeat || 0;
344 this.easing = easing || yuu.Tween.LINEAR;
345 yf.each.call(this, function (oab) {
346 yf.ipairs.call(this, function (name, ab) {
347 if (name !== "$") {
348 this._object.push(oab.$);
349 this._property.push(name);
350 this._a.push(ab[0]);
351 this._b.push(ab[1]);
352 }
353 }, oab);
354 }, yf.arrayify(props));
355 this._updateAt(0);
356 },
357
358 tick: function () {
359 var t = this._count / this.duration;
360 ++this._count;
361 if (t > 1 && !this.repeat)
362 return true;
363 else if (t >= 1 && this.repeat) {
364 if (this.repeat < 0) {
365 var n = this._a;
366 this._a = this._b;
367 this._b = n;
368 }
369 this._count = 1;
370 t = 0;
371
372 if (this.repeat < 0) {
373 this.repeat++;
374 } else if (this.repeat > 0) {
375 this.repeat--;
376 }
377 }
378 this._updateAt(t);
379 },
380
381 tock: function (p) {
382 var t = (this._count + p - 1) / this.duration;
383 if (t <= 1)
384 this._updateAt(t);
385 },
386
387 _updateAt: function (t) {
388 var p = this.easing ? this.easing(t) : t;
389 for (var i = 0; i < this._object.length; ++i) {
390 // a was the existing property, b was the one provided
391 // by the user. By lerping from b to a, the user can
392 // control the lerp type in some awkward cases -
393 // e.g. CSS DOM values are all exposed as strings so a
394 // will be a string/String, but if b is provided as a
395 // number/Number, this will lerp numerically.
396 //
397 // FIXME: This still doesn't work right if the lerp is
398 // later reversed due to negative repeats.
399 var object = this._object[i];
400 var property = this._property[i];
401 var a = this._a[i];
402 var b = this._b[i];
403 object[property] = yuu.lerp(b, a, 1 - p);
404 }
405 },
406
407 count: {
408 get: function () { return this._count; },
409 set: function (v) { this._count = Math.round(v); },
410 },
411
412 duration: { value: 60, chainable: true },
413 repeat: { value: 0, chainable: true },
414 easing: { value: null, chainable: true },
415 });
416
417 yuu.TweenC = yT(yuu.C, {
418 constructor: function () {
419 this._tween = yf.construct(yuu.Tween, arguments);
420 },
421
422 tick: function () {
423 if (this._tween.tick())
424 this.entity.detach();
425 },
426
427 tock: { proxy: "_tween.tock" },
428 count: { alias: "_tween.count" },
429 duration: { alias: "_tween.duration", chainable: true },
430 repeat: { alias: "_tween.repeat", chainable: true },
431 easing: { alias: "_tween.easing", chainable: true },
432
433 TAPS: ["tick", "tock"]
434 });
435
436 yuu.Tween.LINEAR = null;
437 /** No easing */
438
439 yuu.Tween.EASE = function (p) {
440 /** Ease in and out
441
442 This equation is from _Improving Noise_ (Perlin, 2002). It
443 is symmetrical around p=0.5 and has zero first and second
444 derivatives at p=0 and p=1.
445 */
446 return p * p * p * (p * (p * 6.0 - 15.0) + 10.0);
447 };
448
449 yuu.Tween.EASE_IN = function (p) {
450 return p * p * p;
451 };
452
453 yuu.Tween.METASPRING = function (amplitude, pulsation) {
454 /** A generator for springy tweens
455
456 The amplitude controls how far from the final position the
457 spring will bounce, as a multiple of the distance between the
458 start and end. A "normal" amplitude is around 0.5 to 1.5.
459
460 The pulsation constant controls the rigidity of the spring;
461 higher pulsation results in a spring that bounces more quickly
462 and more often during a fixed interval. A "normal" pulsation
463 constant is around 15 to 30.
464 */
465 return function (p) {
466 return 1 + Math.cos(pulsation * p + Math.PI) * (1 - p) * amplitude;
467 };
468 };
469
470 yuu.Tween.STEPPED = function (segments, alpha) {
471 return function (p) {
472 p = p * segments;
473 var lower = Math.floor(p);
474 var upper = Math.floor((p + alpha));
475 if (upper > lower) {
476 var p1 = 1 - (upper - p) / alpha;
477 return (lower + p1) / segments;
478 } else {
479 return lower / segments;
480 }
481 };
482 };
483
484 yuu.Transform = yT(yuu.C, {
485 /** A 3D position, rotation (as quaternion), and scale
486
487 This also serves as an object lesson for a simple slotted
488 component.
489 */
490 constructor: function (position, rotation, scale) {
491 this._position = vec3.clone(position || [0, 0, 0]);
492 this._rotation = quat.clone(rotation || [0, 0, 0, 1]);
493 this._scale = vec3.clone(scale || [1, 1, 1]);
494 this._matrix = mat4.create();
495 this._dirty = true;
496 this._version = 0;
497 this._parentVersion = null;
498 },
499
500 SLOTS: ["transform"],
501
502 position: {
503 chainable: true,
504 get: function () { return this._position.slice(); },
505 set: function (v) { this._dirty = true;
506 vec3.copy(this._position, v); }
507 },
508 rotation: {
509 chainable: true,
510 get: function () { return this._rotation.slice(); },
511 set: function (v) { this._dirty = true;
512 quat.normalize(this._rotation, v); }
513 },
514 scale: {
515 chainable: true,
516 get: function () { return this._scale.slice(); },
517 set: function (v) { this._dirty = true;
518 vec3.copy(this._scale, v); }
519 },
520 x: {
521 chainable: true,
522 get: function () { return this._position[0]; },
523 set: function (x) { this._dirty = true; this._position[0] = x; }
524 },
525 y: {
526 chainable: true,
527 get: function () { return this._position[1]; },
528 set: function (x) { this._dirty = true; this._position[1] = x; }
529 },
530 z: {
531 chainable: true,
532 get: function () { return this._position[2]; },
533 set: function (x) { this._dirty = true; this._position[2] = x; }
534 },
535 xy: { swizzle: "xy", chainable: true },
536
537 scaleX: {
538 chainable: true,
539 get: function () { return this._scale[0]; },
540 set: function (x) { this._dirty = true; this._scale[0] = x; }
541 },
542 scaleY: {
543 chainable: true,
544 get: function () { return this._scale[1]; },
545 set: function (x) { this._dirty = true; this._scale[1] = x; }
546 },
547 scaleZ: {
548 chainable: true,
549 get: function () { return this._scale[2]; },
550 set: function (x) { this._dirty = true; this._scale[2] = x; }
551 },
552
553 worldToLocal: function (p) {
554 var x = (p.x || p[0] || 0);
555 var y = (p.y || p[1] || 0);
556 var z = (p.z || p[2] || 0);
557 var local = [x, y, z];
558 var matrix = mat4.clone(this.matrix);
559 return vec3.transformMat4(local, local, mat4.invert(matrix, matrix));
560 },
561
562 contains: function (p) {
563 p = this.worldToLocal(p);
564 return p[0] >= -0.5 && p[0] < 0.5
565 && p[1] >= -0.5 && p[1] < 0.5
566 && p[2] >= -0.5 && p[2] < 0.5;
567 },
568
569 ypr: {
570 chainable: true,
571 get: function () {
572 var q = this._rotation;
573 var x = q[0]; var sqx = x * x;
574 var y = q[1]; var sqy = y * y;
575 var z = q[2]; var sqz = z * z;
576 var w = q[3];
577 var abcd = w * x + y * z;
578 if (abcd > 0.499)
579 return [2 * Math.atan2(x, w), Math.PI / 2, 0];
580 else if (abcd < -0.499)
581 return [-2 * Math.atan2(x, w), -Math.PI / 2, 0];
582 else {
583 var adbc = w * z - x * y;
584 var acbd = w * y - x * z;
585 return [Math.atan2(2 * adbc, 1 - 2 * (sqz + sqx)),
586 Math.asin(2 * abcd),
587 Math.atan2(2 * acbd, 1 - 2 * (sqy + sqx))];
588 }
589
590 },
591 set: function (ypr) {
592 var q = this._rotation;
593 quat.identity(q);
594 quat.rotateZ(q, q, ypr[0]);
595 quat.rotateY(q, q, ypr[2]);
596 quat.rotateX(q, q, ypr[1]);
597 this._dirty = true;
598 }
599 },
600
601 yaw: { aliasSynthetic: "ypr[0]", chainable: true },
602 pitch: { aliasSynthetic: "ypr[1]", chainable: true },
603 roll: { aliasSynthetic: "ypr[2]", chainable: true },
604
605 matrix: {
606 get: function () {
607 var pt = this.entity.parent && this.entity.parent.transform;
608 var pm = pt && pt.matrix;
609 var ptVersion = pt && pt._version;
610 if (this._dirty || (ptVersion !== this._parentVersion)) {
611 var m = this._matrix;
612 mat4.identity(m);
613 mat4.fromRotationTranslation(
614 m, this._rotation, this._position);
615 mat4.scale(m, m, this._scale);
616 if (pm)
617 mat4.multiply(m, pm, m);
618 this._dirty = false;
619 this._matrix = m;
620 this._parentVersion = ptVersion;
621 this._version = (this._version + 1) | 0;
622 }
623 return this._matrix;
624 }
625 }
626 });
627
628 yuu.Ticker = yT(yuu.C, {
629 /** Set a callback to run every n ticks
630
631 If the callback returns true, it is rescheduled for
632 execution (like setInterval). If it returns false, this
633 component is removed from the entity.
634 */
635 constructor: function (callback, interval, delay) {
636 this.callback = callback;
637 this.interval = interval;
638 this._accum = 0;
639 this._count = -(delay || 0);
640 },
641
642 tick: function () {
643 this._accum += 1;
644 if (this._accum === this.interval) {
645 this._accum = 0;
646 if (!this.callback(this._count++))
647 this.entity.detach(this);
648 }
649 },
650
651 TAPS: ["tick"]
652 });
653
654 }).call(typeof exports === "undefined" ? this : exports,
655 typeof exports === "undefined"
656 ? this.yuu : (module.exports = require('./core')));