Use new animation hooks (but still preload sounds).
[pwl6.git] / src / pwl6.js
1 "use strict";
2
3 var storage;
4 var SIGILS;
5 var BOOK;
6 var handScene;
7 var circleScene;
8 var NOISY_BLOCKS;
9
10 var sounds;
11
12 var TOP = 0;
13 var LEFT = 1;
14 var BOTTOM = 2;
15 var RIGHT = 3;
16
17 var MenuScene, CircleScene, HandScene, BookScene, GridScene;
18
19 yuu.Texture.DEFAULTS.magFilter = yuu.Texture.DEFAULTS.minFilter = "nearest";
20
21 var TICK_ROT = quat.rotateZ(quat.create(), quat.create(), Math.PI / 30);
22 var TICK_REV = quat.invert(quat.create(), TICK_ROT);
23 var TICK_ROT2 = quat.rotateZ(quat.create(), quat.create(), Math.PI / 60);
24
25 function sawtooth (p) {
26 /** Sawtooth wave, Û = 1, T = 2π, f(0) = 0, f′(0) > 0 */
27 var _2PI = 2 * Math.PI;
28 return 2 * (p / _2PI - Math.floor(0.5 + p / _2PI));
29 }
30
31 function triangle (p) {
32 /** Triangle wave, Û = 1, T = 2π, f(0) = 0, f′(0) > 0 */
33 return 2 * Math.abs(sawtooth(p + Math.PI / 2)) - 1;
34 }
35
36 function waveshift (period, peak, xoffset, yoffset) {
37 period /= 2 * Math.PI;
38 xoffset = xoffset || 0;
39 yoffset = yoffset || 0;
40 return function (f) {
41 return function (p) {
42 return yoffset + peak * f.call(this, (p + xoffset) / period);
43 };
44 };
45 }
46
47 function cycler (scale) {
48 var f = waveshift(scale, 0.5, -Date.now(), 0.5)(triangle);
49 return function () { return f(Date.now()); };
50 }
51
52 function load () {
53 storage = ystorage.getStorage();
54 yuu.audio.storage = storage;
55
56 NOISY_BLOCKS = new yuu.ShaderProgram(null, ["@noise.glsl", "@noisyblocks"]);
57
58 SIGILS = new yuu.Material("@sigils");
59 BOOK = new yuu.Material("@book", NOISY_BLOCKS);
60 BOOK.uniforms.cut = yf.volatile(cycler(20000));
61 BOOK.uniforms.range = 0.06;
62 BOOK.texture.ready.then(function (texture) {
63 BOOK.uniforms.resolution = new Float32Array(
64 [texture.width / 4, texture.height / 4]);
65 });
66
67 sounds = {
68 tick: new yuu.Instrument("@tick"),
69 tock: new yuu.Instrument("@tock"),
70 switch: new yuu.Instrument("@switch"),
71 switchBroke: new yuu.Instrument({
72 sample: { "@switch": { duration: 0.27, offset: 0.1 } } }),
73 switchOn: new yuu.Instrument({
74 sample: { "@switch": { duration: 0.2 } } }),
75 switchOff: new yuu.Instrument({
76 sample: { "@switch": { offset: 0.2 } } }),
77 chime: new yuu.Instrument({
78 envelope: { "0": 1, "0.7": 0.2, "3": 0 },
79 modulator: {
80 envelope: { "0": 1, "0.7": 0.2, "3": 0 },
81 frequency: "x1.5",
82 }
83 }),
84 };
85
86 yuu.director.pushScene(circleScene = new CircleScene());
87 yuu.director.pushScene(handScene = new HandScene());
88 yuu.director.pushScene(new MenuScene());
89 if (!storage.getFlag("instructions")) {
90 yuu.director.entity0.attach(new yuu.Ticker(function () {
91 yuu.director.pushScene(new BookScene());
92 }, 60));
93 }
94
95 return yuu.ready(
96 [SIGILS, BOOK]
97 .concat(yf.map(yf.new_(yuu.Instrument), [
98 '@winding', '@regear', '@clicking', '@slam', '@book-appear']))
99 .concat(yf.map(yf.getter.bind(sounds), Object.keys(sounds)))
100 );
101 }
102
103 function start () {
104 yuu.director.start();
105 }
106
107 window.addEventListener("load", function() {
108 yuu.registerInitHook(load);
109 yuu.init({ backgroundColor: [0, 0, 0, 1], antialias: false }).then(start);
110 });
111
112 var PALETTE = [[ 0.76, 0.13, 0.13 ],
113 [ 0.33, 0.49, 0.71 ],
114 [ 0.45, 0.68, 0.32 ],
115 [ 0.51, 0.32, 0.63 ],
116 [ 0.89, 0.49, 0.11 ],
117 [ 1.00, 1.00, 0.30 ]];
118
119 var LEVELS = [
120 { name: "12345654321",
121 randomSlammer: [3, 5],
122 deps: "50040@easy 53535@easy 44404@easy 1302@easy 12321@easy 20124@easy"
123 },
124
125 { slammer: [1, 1], sets: "tutorial",
126 scramble: { easy: "01", hard: "0122" } },
127 { slammer: [1, 1, 1], deps: "tutorial",
128 scramble: { easy: "11", hard: "1212" } },
129 { slammer: [2, 1], deps: "tutorial", sets: "asymmetric",
130 scramble: { easy: "32", hard: "3321" } },
131 { slammer: [1, 2, 1], deps: "tutorial", sets: "unequal",
132 scramble: { easy: "112", hard: "3210" } },
133 { slammer: [2, 0], deps: "asymmetric", sets: "zero",
134 scramble: { easy: "23", hard: "032" } },
135 { slammer: [2, 0, 2], deps: "zero",
136 scramble: { easy: "11", hard: "2211" } },
137 { slammer: [1, 1, 1, 1], deps: "tutorial" },
138 { slammer: [2, 1, 1], deps: "asymmetric" },
139 { slammer: [1, 2, 1, 2], deps: "asymmetric",
140 scramble: { easy: "012" } },
141 { slammer: [1, 2, 3, 4], deps: "asymmetric", sets: "solid",
142 scramble: { easy: "110" } },
143 { slammer: [5, 0, 0, 4, 0], deps: "unequal zero",
144 scramble: { easy: "112" } },
145 { slammer: [5, 3, 5, 3, 5], deps: "unequal solid",
146 scramble: { easy: "3232" } },
147 { slammer: [4, 4, 4, 0, 4], deps: "solid zero",
148 scramble: { easy: "0321" } },
149 { slammer: [1, 3, 0, 2], deps: "unequal zero" },
150 { slammer: [1, 2, 3, 2, 1], deps: "unequal",
151 scramble: { easy: "3333" } },
152 { slammer: [2, 0, 1, 2, 4], deps: "unequal zero" },
153 ];
154
155 function levelName (level) {
156 return (level.name || level.slammer.join("")).trim();
157 }
158
159 function wonLevel (level, difficulty) {
160 if (level.sets)
161 storage.setFlag(level.sets);
162 storage.setFlag(levelName(level) + "@" + difficulty);
163 }
164
165 function hasBeaten (level, difficulty) {
166 return storage.getFlag(levelName(level) + "@" + difficulty);
167 }
168
169 function scrambleForLevel (rnd, level, difficulty) {
170 var c = difficulty === "easy" ? 0 : 1;
171 if (difficulty === "random")
172 c = rnd.randrange(2, 5);
173 var length = level.slammer.length;
174 return rnd.randrange(length * c, length * (c + 1)) + 2;
175 }
176
177 function difficultyForLevel (level) {
178 if (level.deps && !level.deps.split(" ").every(storage.getFlag, storage))
179 return null;
180 if (hasBeaten(level, "hard"))
181 return "random";
182 if (hasBeaten(level, "easy"))
183 return "hard";
184 else
185 return "easy";
186 }
187
188 function levelRandom (level, difficulty) {
189 if (difficulty === "random")
190 return yuu.random;
191 else
192 return new yuu.Random(yuu.createLCG(+level.slammer.join("")));
193 }
194
195 function generateBoard (rnd, level) {
196 var size = level.length;
197 var board = new Array(size);
198 for (var i = 0; i < size; ++i)
199 board[i] = yf.repeat(i % PALETTE.length + 1, size);
200 if (rnd.randbool())
201 yuu.transpose2d(board);
202 return board;
203 }
204
205 function generateSlammer (rnd, level) {
206 var s = new Array(level.length);
207 for (var i = 0; i < s.length; ++i)
208 s[i] = yf.repeat(0, level[i]);
209 if (rnd.randbool())
210 s.reverse();
211 return s;
212 }
213
214 var AnimationQueue = yT(yuu.C, {
215 constructor: function () {
216 this._queue = [];
217 },
218
219 attached: function () {
220 this._queue = [];
221 },
222
223 _runNext: function () {
224 var next = this._queue[0];
225 if (next && this.entity)
226 this.entity.attach(new yuu.Animation(
227 next.timeline, next.params, this._complete.bind(this)));
228 },
229
230 _complete: function () {
231 var next = this._queue.shift();
232 next.resolve();
233 this._runNext();
234 },
235
236 enqueue: function (timeline, params) {
237 return new Promise(function (resolve) {
238 this._queue.push({
239 timeline: timeline,
240 params: params,
241 resolve: resolve
242 });
243 // Chaining the promise doesn't work here because the tick
244 // between the two handlers is often long enough to render
245 // a frame with some undesirable intermediate state.
246 if (this._queue.length === 1)
247 this._runNext();
248 }.bind(this));
249 },
250
251 SLOTS: ["animationQueue"]
252 });
253
254 var SLAMMER_ROTATE = {
255 0: { tween1: { yaw: "yaw" },
256 playSound: "@clicking",
257 duration: 10 }
258 };
259
260 var ROTATE_ALL = {
261 0: { tweenAll: { yaw: "yaws" }, duration: 10 }
262 };
263
264 var SLAMMER_BOUNCE = {
265 0: { tween1: { y: 0.5 }, duration: 5, repeat: -1 }
266 };
267
268 var SLIDE_BLOCKS = {
269 0: { tweenAll: { position: "positions" },
270 duration: 8, easing: "linear" },
271 };
272
273 var SLAMMER_SLAM = {
274 0: { tween1: { y: -1.5 }, easing: "linear",
275 playSound: "@slam",
276 duration: 6 },
277 6: { event: "slideBoardBlocks" },
278 15: { event: "slam",
279 tween1: { y: 0 }, easing: "linear", duration: 8 }
280 };
281
282 var GRID_DISMISS = {
283 0: { tween1: { yaw: 2 * Math.PI, x: "x", y: "y", scale: [0.3, 0.3, 1] },
284 duration: 45 }
285 };
286
287 var GRID_FINISHED = {
288 0: { tween: { arm: { scale: [0, 0, 1], yaw: "armYaw", y: "armY" },
289 board: { y: "boardY" } },
290 duration: 45 }
291 };
292
293 function rotateCw (d) { return (--d + 4) % 4; }
294 function rotateCcw (d) { return ++d % 4; }
295 function opposite (d) { return (d + 2) % 4; }
296
297 var FlagSet = yT({
298 /** Manage a set of semaphore-like counting flags. */
299
300 constructor: function () {
301 /** Construct a flag set for the provided flags.
302
303 Flags are initialized to 0 by default.
304 */
305 this._counts = {};
306 for (var i = 0; i < arguments.length; ++i)
307 this._counts[arguments[i]] = 0;
308 },
309
310 increment: function () {
311 /** Increment the provided flags. */
312 for (var i = 0; i < arguments.length; ++i)
313 this._counts[arguments[i]]++;
314 },
315
316 decrement: function () {
317 /** Decrement the provided flags.
318
319 No underflow checks are performed. A flag with a negative
320 value is considered set exactly as a flag with a positive
321 value.
322 */
323 for (var i = 0; i < arguments.length; ++i)
324 this._counts[arguments[i]]--;
325 },
326
327 some: function () {
328 /** Return true if any of the provided flags are set. */
329 return yf.some.call(this._counts, yf.getter, arguments);
330 },
331
332 every: function () {
333 /** Return true if all of the provided flags are set. */
334 return yf.every.call(this._counts, yf.getter, arguments);
335 },
336
337 none: function () {
338 /** Return true if none of the provided flags are set. */
339 return !this.some.apply(this, arguments);
340 },
341
342 incrementer: function () {
343 /** Provide a bound 0-ary function to increment the provided flags.
344
345 Useful for wrapps around context-free callbacks.
346 */
347 var that = this, args = arguments;
348 return function () { that.increment.apply(that, args); };
349 },
350
351 decrementer: function () {
352 /** Provide a bound 0-ary function to decrement the provided flags.
353
354 Useful for wrapps around context-free callbacks.
355 */
356 var that = this, args = arguments;
357 return function () { that.decrement.apply(that, args); };
358 }
359 });
360
361 var BoardController = yT(yuu.C, {
362 constructor: function (rnd, level, colors) {
363 this.contents = generateBoard(rnd, level.slammer);
364 this.colors = colors;
365 },
366 updateChildren: function () {
367 this.entity.data.quads.forEach(function (q) {
368 q.quad.position = [q.x, q.y];
369 var i = this.contents[q.x][q.y];
370 q.quad.color = this.colors[i];
371 q.quad.texBounds = [i / 6, 0.5, (i + 1) / 6, 1.0];
372 }, this);
373 },
374 isComplete: function() {
375 var x, y;
376 var rows = true, cols = true;
377 for (x = 1; x < this.contents.length && rows; ++x)
378 for (y = 0; y < this.contents[x].length && rows; ++y)
379 rows = this.contents[x - 1][y] === this.contents[x][y];
380 for (x = 0; x < this.contents.length && cols; ++x)
381 for (y = 1; y < this.contents[x].length && cols; ++y)
382 cols = this.contents[x][y - 1] === this.contents[x][y];
383 return rows || cols;
384 },
385
386 shift: [
387 function (x, replacement) {
388 var lost = this.contents[x].pop();
389 this.contents[x].unshift(replacement);
390 return lost;
391 },
392 function (y, replacement) {
393 yuu.transpose2d(this.contents);
394 var lost = this.shift[BOTTOM].call(this, y, replacement);
395 yuu.transpose2d(this.contents);
396 return lost;
397 },
398 function (x, replacement) {
399 var lost = this.contents[x].shift();
400 this.contents[x].push(replacement);
401 return lost;
402 },
403 function (y, replacement) {
404 yuu.transpose2d(this.contents);
405 var lost = this.shift[TOP].call(this, y, replacement);
406 yuu.transpose2d(this.contents);
407 return lost;
408 }
409 ],
410
411 SLOTS: ["controller"]
412 });
413
414 var SlammerController = yT(yuu.C, {
415 constructor: function (rnd, level, colors) {
416 this.blocks = generateSlammer(rnd, level.slammer);
417 this.orientation = TOP;
418 this.colors = colors;
419 this._undoRecord = [];
420 },
421 isComplete: function() {
422 return yf.none(yf.some.bind(null, null), this.blocks);
423 },
424 updateChildren: function () {
425 this.entity.data.quads.forEach(function (q) {
426 var i = this.blocks[q.x][q.y];
427 q.quad.position = [q.x, q.y];
428 q.quad.color = this.colors[i];
429 q.quad.texBounds = [i / 6, 0.5, (i + 1) / 6, 1.0];
430 }, this);
431 },
432
433 lastUndoRecord: { get: function () {
434 return yf.last(this._undoRecord);
435 } },
436
437 clearUndoRecord: function () {
438 this._undoRecord = [];
439 },
440
441 slam: function (board) {
442 var undoable = (this.orientation !== this.lastUndoRecord);
443 var length = this.blocks.length;
444 this.orientation = opposite(this.orientation);
445 this.blocks = yf.mapr.call(this, function (a, y) {
446 return yf.map(board.shift[this.orientation].bind(board, y), a)
447 .reverse();
448 }, this.blocks, (this.orientation & 2)
449 ? yf.range(length)
450 : yf.range(length - 1, -1, -1));
451 yf.each(function (i) {
452 i.x = length - (i.x + 1);
453 }, this.entity.data.quads);
454 this.updateChildren();
455 board.updateChildren();
456 if (undoable)
457 this._undoRecord.push(this.orientation);
458 else
459 this._undoRecord.pop();
460 },
461
462 SLOTS: ["controller"]
463 });
464
465 function randSide (rnd, except) {
466 return (rnd || yuu.random).choice(
467 yf.without([TOP, LEFT, BOTTOM, RIGHT], except));
468 }
469
470 var HANDS_LEFT = {
471 0: { tween: { left: { yaw: -0.3 } }, duration: 3 },
472 3: { tween: { left: { yaw: 0.0 } }, duration: 7 },
473 };
474
475 var HANDS_RIGHT = {
476 0: { tween: { right: { yaw: -0.3 } }, duration: 3 },
477 3: { tween: { right: { yaw: 0.0 } }, duration: 7 },
478 };
479
480 var HANDS_UNDO = {
481 0: { tween: { left: { yaw: 0.2 }, right: { yaw: 0.2 } },
482 duration: 3 },
483 3: { tween: { left: { yaw: 0.0 }, right: { yaw: 0.0 } },
484 duration: 7 }
485 };
486
487 var HANDS_MENU_CHOICE = {
488 0: { tween: { left: { x: -1.3 },
489 right: { x: -1.3 } },
490 duration: 15, easing: "ease_in"
491 },
492
493 10: { tween: { left: { scaleX: 1 },
494 right: { scaleX: 1 } },
495 duration: 20 },
496
497 20: { set: { leftQuad: { color: "frontColor" },
498 rightQuad: { color: "frontColor" } },
499 tween: { left: { x: 0 }, right: { x: 0 } },
500 duration: 15
501 },
502 };
503
504 var HANDS_RETURN = {
505 0: { tween: { left: { x: -1.3 },
506 right: { x: -1.3 } },
507 duration: 20
508 },
509
510 10: { tween: { left: { scaleX: -1 },
511 right: { scaleX: -1 } },
512 duration: 20 },
513
514 20: { set: { leftQuad: { color: "backColor" },
515 rightQuad: { color: "backColor" } },
516 tween: { left: { x: -1 }, right: { x: -1 } },
517 duration: 10
518 },
519 };
520
521 var HANDS_SLAM = [
522 // TOP
523 { 0: { tween: { left: { yaw: -0.2, scaleX: 0.8, y: -0.1 },
524 right: { yaw: -0.2, scaleX: 0.8, y: -0.1 },
525 }, duration: 10, repeat: -1 },
526 },
527
528 // LEFT
529 { 0: { tween: { left: { scaleX: 0.8, x: 0.1 },
530 right: { scaleX: 0.9 },
531 }, duration: 10, repeat: -1 },
532 },
533
534 // BOTTOM
535 { 0: { tween: { left: { yaw: 0.2, scaleX: 0.8 },
536 right: { yaw: 0.2, scaleX: 0.8 },
537 }, duration: 10, repeat: -1 },
538 },
539
540 // RIGHT
541 { 0: { tween: { left: { scaleX: 0.9 },
542 right: { scaleX: 0.8, x: 0.1 },
543 }, duration: 10, repeat: -1 },
544 },
545 ];
546
547 var HANDS_ROTATE_CW = {
548 0: { tween: { left: { scaleX: 0.8 } }, duration: 5 },
549 5: { tween: { left: { scaleX: 1.0 } }, duration: 5 },
550 };
551
552 var HANDS_ROTATE_CCW = {
553 0: { tween: { right: { scaleX: 0.8 } }, duration: 5 },
554 5: { tween: { right: { scaleX: 1 } }, duration: 5 },
555 };
556
557 var BUTTONS_IN = {
558 0: { tween: { a: { x: 0 }, b: { x: 1.5 } }, duration: 25 }
559 };
560
561 var BUTTONS_OUT = {
562 0: { tween: { a: { x: -1.5 }, b: { x: 0 } }, duration: 25 }
563 };
564
565 HandScene = yT(yuu.Scene, {
566 constructor: function () {
567 yuu.Scene.call(this);
568 var hands = new yuu.Material("@hand");
569 this.left = new yuu.E(new yuu.Transform());
570 var l = new yuu.E(
571 new yuu.Transform([-0.5, 0.5, 0]),
572 new yuu.DataC({ command: "left" }),
573 this.leftQuad = new yuu.QuadC(hands));
574 this.left.addChild(l);
575 var r = new yuu.E(
576 new yuu.Transform([-0.5, 0.5, 0]),
577 new yuu.DataC({ command: "right" }),
578 this.rightQuad = new yuu.QuadC(hands));
579 this.right = new yuu.E(new yuu.Transform());
580 this.right.addChild(r);
581 var SIZE_X = yuu.random.gauss(1.2, 0.15) * 0.35;
582 var SIZE_Y = yuu.random.gauss(1.1, 0.05) * 0.51;
583 var handLeft = yuu.random.randrange(4);
584 var handRight = yuu.random.randrange(4);
585 this.leftQuad.texBounds = [handLeft / 4, 0, (handLeft + 1) / 4, 1];
586 this.rightQuad.texBounds = [handRight / 4, 0, (handRight + 1) / 4, 1];
587 this.layer0.resize(-0.75, 0, 1.5, 1.5);
588 var leftWrist = new yuu.E(
589 new yuu.Transform([-0.20, 0, 0], null,
590 [SIZE_X, SIZE_Y, 1]));
591 var rightWrist = new yuu.E(
592 new yuu.Transform([0.20, 0, 0], null,
593 [-SIZE_X, SIZE_Y, 1]));
594 leftWrist.addChild(this.left);
595 rightWrist.addChild(this.right);
596 this.addEntities(leftWrist, rightWrist);
597 this.backColor = yuu.hslToRgb(
598 (yuu.random.gauss(0.1, 0.1) + 10) % 1,
599 yuu.random.uniform(0.2, 0.7),
600 yuu.random.uniform(0.2, 0.6),
601 1.0);
602 this.leftQuad.alpha = this.rightQuad.alpha = 0.2;
603 var hsl = yuu.rgbToHsl(this.backColor);
604 hsl[2] = hsl[2].lerp(1, 0.15);
605 hsl[1] = hsl[1].lerp(0, 0.30);
606 hsl[3] = 0.4;
607 this.frontColor = yuu.hslToRgb(hsl);
608 this.leftQuad.color = this.rightQuad.color = this.frontColor;
609 this.ready = hands.ready;
610
611 function Button (i, command) {
612 return new yuu.E(
613 new yuu.Transform(),
614 new yuu.DataC({ command: command }),
615 new yuu.QuadC(SIGILS)
616 .setTexBounds([i / 6, 0, (i + 1) / 6, 0.5])
617 .setColor(PALETTE[i]));
618 }
619
620 this.helpButton = new Button(1, "help");
621 this.backButton = new Button(3, "back");
622 this.backButton.transform.x -= 1.5;
623 this.leftButtons = new yuu.E(new yuu.Transform());
624 this.leftButtons.addChildren(this.helpButton, this.backButton);
625 this.rightButton = new Button(2, "showOverlay preferences");
626 this.leftButtons.transform.scale
627 = this.rightButton.transform.scale
628 = [0.075, 0.075, 1];
629 this.entity0.addChildren(this.leftButtons, this.rightButton);
630 this.buttons = [this.helpButton, this.backButton, this.rightButton,
631 l, r];
632 },
633
634 inputs: {
635 resize: function () {
636 var base = new yuu.AABB(-0.75, 0, 0.75, 1.5);
637 var vp = base.matchAspectRatio(yuu.viewport);
638 vp.y1 -= vp.y0;
639 vp.y0 = 0;
640 this.leftButtons.transform.xy = [
641 vp.x0 + this.leftButtons.transform.scaleX,
642 vp.y1 - this.leftButtons.transform.scaleY];
643 this.rightButton.transform.xy = [
644 vp.x1 - this.rightButton.transform.scaleX,
645 vp.y1 - this.rightButton.transform.scaleY];
646 this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
647 },
648
649 mousemove: function (p) {
650 p = this.layer0.worldFromDevice(p);
651 this.cursor = "";
652 for (var i = 0; i < this.buttons.length; ++i) {
653 if (this.buttons[i].transform.contains(p)) {
654 this.cursor = "pointer";
655 }
656 }
657 },
658
659 tap: function (p) {
660 p = this.layer0.worldFromDevice(p);
661 for (var i = 0; i < this.buttons.length; ++i) {
662 if (this.buttons[i].transform.contains(p)) {
663 yuu.director.execute(this.buttons[i].data.command);
664 return true;
665 }
666 }
667 },
668
669 doubletap: function () {
670 return this.inputs.tap.apply(this, arguments);
671 },
672 },
673
674 _anim: function (timeline) {
675 this.entity0.attach(new yuu.Animation(
676 timeline, {
677 left: this.left.transform,
678 right: this.right.transform,
679 leftQuad: this.leftQuad,
680 rightQuad: this.rightQuad,
681 frontColor: this.frontColor,
682 backColor: this.backColor
683 }));
684 },
685
686 undo: function () { this._anim(HANDS_UNDO); },
687 movedLeft: function () { this._anim(HANDS_LEFT); },
688 movedRight: function () { this._anim(HANDS_RIGHT); },
689 slam: function (o) { this._anim(HANDS_SLAM[o]); },
690 rotatedCw: function () { this._anim(HANDS_ROTATE_CW); },
691 rotatedCcw: function () { this._anim(HANDS_ROTATE_CCW); },
692 menuChoice: function () {
693 this.entity0.attach(new yuu.Animation(
694 BUTTONS_IN, {
695 a: this.backButton.transform,
696 b: this.helpButton.transform
697 }));
698 this._anim(HANDS_MENU_CHOICE);
699 },
700 finished: function () {
701 this.entity0.attach(new yuu.Animation(
702 BUTTONS_OUT, {
703 a: this.backButton.transform,
704 b: this.helpButton.transform
705 }));
706 this._anim(HANDS_RETURN);
707 },
708 });
709
710 var GRID_APPEAR = {
711 0: { set1: { y: 5 },
712 tween1: { y: 0 }, duration: 10 },
713 };
714
715 GridScene = yT(yuu.Scene, {
716 constructor: function (level, difficulty) {
717 yuu.Scene.call(this);
718 this.entity0.attach(new yuu.Transform());
719 this.level = level;
720 this.difficulty = difficulty;
721 this._locks = new FlagSet("slam", "spin", "quit");
722 var rnd = levelRandom(level, difficulty);
723 var colors = yuu.random.shuffle(PALETTE.slice());
724 colors.unshift([1.0, 1.0, 1.0]);
725 this.board = new yuu.E(new BoardController(rnd, level, colors),
726 new yuu.Transform(),
727 new yuu.DataC({ quads: [] }));
728 this.slammer = new yuu.E(new SlammerController(rnd, level, colors),
729 new yuu.Transform(),
730 new yuu.DataC({ quads: [] }));
731 this.slammerHead = new yuu.E(new yuu.Transform());
732 this.slammerRoot = new yuu.E(new yuu.Transform());
733 var length = level.slammer.length;
734 var maxSize = length * length;
735 var slammerBatch = new yuu.QuadBatchC(maxSize);
736 slammerBatch.material = SIGILS;
737 var boardBatch = new yuu.QuadBatchC(maxSize);
738 boardBatch.material = SIGILS;
739 this.slammerRoot.transform.xy = [length / 2 - 0.5, length / 2 - 0.5];
740 this.slammerHead.transform.xy = [-length / 2 + 0.5, length / 2 + 2];
741 this.slammerRoot.addChild(this.slammerHead);
742 this.slammerHead.addChild(this.slammer);
743 this.slammer.attach(slammerBatch);
744 this.board.attach(boardBatch);
745 yf.irange.call(this, function (x) {
746 yf.irange.call(this, function (y) {
747 var quad = boardBatch.createQuad();
748 quad.color = colors[this.board.controller.contents[x][y]];
749 quad.position = [x, y];
750 this.board.data.quads.push({ quad: quad, x: x, y: y });
751 }, length);
752 }, length);
753
754 for (var x = 0; x < this.slammer.controller.blocks.length; ++x) {
755 for (var y = 0; y < this.slammer.controller.blocks[x].length; ++y) {
756 var quad = slammerBatch.createQuad();
757 quad.color = colors[this.slammer.controller.blocks[x][y]];
758 quad.position = [x, y];
759 this.slammer.data.quads.push({ quad: quad, x: x, y: y });
760 }
761 }
762 this.addEntities(this.board, this.slammerRoot);
763 this.scramble(rnd);
764 if (!(this.cheating = yuu.director.input.pressed["`"])) {
765 this.slammer.controller.clearUndoRecord();
766 }
767 this._dragging = 0;
768
769 this.gridBB = new yuu.AABB(-0.5, -0.5, length - 0.5, length - 0.5);
770 this.leftBB = new yuu.AABB(
771 -Infinity, this.gridBB.y0, this.gridBB.x0, this.gridBB.y1);
772 this.rightBB = new yuu.AABB(
773 this.gridBB.x1, this.gridBB.y0, Infinity, this.gridBB.y1);
774 this.topBB = new yuu.AABB(
775 this.gridBB.x0, this.gridBB.y1, this.gridBB.x1, Infinity);
776 this.bottomBB = new yuu.AABB(
777 this.gridBB.x0, -Infinity, this.gridBB.x1, this.gridBB.y0);
778 },
779
780 init: function () {
781 this._locks.increment("slam");
782 this.entity0.attach(new yuu.Animation(
783 GRID_APPEAR, { $: this.slammer.transform },
784 this._locks.decrementer("slam")));
785 },
786
787 scramble: function (rnd) {
788 var scramble = (this.level.scramble || {})[this.difficulty];
789 var slammerCon = this.slammer.controller;
790 var boardCon = this.board.controller;
791 if (!scramble) {
792 var count = scrambleForLevel(rnd, this.level, this.difficulty);
793 while (this.isComplete()) {
794 var c = count;
795 while (c--) {
796 slammerCon.orientation = randSide(rnd, slammerCon.orientation);
797 slammerCon.slam(boardCon);
798 }
799 }
800 } else {
801 for (var i =0; i < scramble.length; ++i) {
802 slammerCon.orientation = +scramble[i];
803 slammerCon.slam(boardCon);
804 }
805 }
806 slammerCon.orientation = randSide();
807 this.slammerRoot.transform.yaw = slammerCon.orientation * Math.PI / 2;
808 },
809
810 isComplete: function () {
811 return this.slammer.controller.isComplete()
812 && this.board.controller.isComplete();
813 },
814
815 rotateTo: yuu.cmd(function (orientation) {
816 return new Promise(function (resolve) {
817 if (this._locks.some("spin"))
818 return this;
819 this.slammer.controller.orientation = orientation;
820 this._locks.increment("slam");
821 var yaw0 = this.slammerRoot.transform.yaw;
822 var yaw1 = orientation * Math.PI / 2;
823
824 this.entity0.attach(new yuu.Animation(
825 SLAMMER_ROTATE, {
826 $: this.slammerRoot.transform,
827 yaw: yaw0 + yuu.normalizeRadians(yaw1 - yaw0)
828 }, function () {
829 this._locks.decrement("slam");
830 resolve(this);
831 }.bind(this)));
832 }.bind(this));
833 }, "<top/bottom/left/right>", "move the slammer to the top"),
834
835 rotateCw: yuu.cmd(function () {
836 handScene.rotatedCw();
837 circleScene.rotated();
838 this.rotateTo(rotateCw(this.slammer.controller.orientation));
839 }, "", "rotate the active piece clockwise"),
840
841 rotateCcw: yuu.cmd(function () {
842 handScene.rotatedCcw();
843 circleScene.rotated();
844 this.rotateTo(rotateCcw(this.slammer.controller.orientation));
845 }, "", "rotate the active piece counter-clockwise"),
846
847 left: yuu.cmd(function () { this.rotateCw(); }),
848 right: yuu.cmd(function () { this.rotateCcw(); }),
849
850 undo: yuu.cmd(function (v) {
851 var con = this.slammer.controller;
852 var _ = function () { this.undo(this._undo); }.bind(this);
853 if ((this._undo = v) && con.lastUndoRecord !== undefined) {
854 if (con.orientation !== con.lastUndoRecord) {
855 circleScene.reverse();
856 handScene.undo();
857 this.rotateTo(con.lastUndoRecord)
858 .then(this.slam.bind(this))
859 .then(_);
860 } else {
861 this.slam().then(_);
862 }
863 }
864 }, "", "rotate the active piece counter-clockwise"),
865
866 checkWon: function () {
867 if (this.isComplete() && !this._locks.some("quit")) {
868 this._locks.increment("quit", "slam", "spin");
869 var firstTime = !hasBeaten(this.level, this.difficulty);
870 if (!this.cheating)
871 wonLevel(this.level, this.difficulty);
872 var scene = new MenuScene(this.level);
873 yuu.director.pushScene(scene);
874 scene.didWinLevel(this.level, this.difficulty, firstTime);
875 this.entity0.attach(new yuu.Animation(
876 GRID_FINISHED, {
877 arm: this.slammerRoot.transform,
878 armYaw: this.slammerRoot.transform.yaw + 3 * Math.PI,
879 armY: this.slammerRoot.transform.y + 1.5,
880 board: this.board.transform,
881 boardY: this.level.slammer.length * 3
882 }, function () {
883 yuu.director.removeScene(this);
884 }.bind(this)
885 ));
886 }
887 },
888
889 slideBoardBlocks: function (anim, params) {
890 var dx = 0, dy = 0;
891 var orientation = this.slammer.controller.orientation;
892 switch (orientation) {
893 case LEFT: dx = 1.5; break;
894 case TOP: dy = -1.5; break;
895 case RIGHT: dx = -1.5; break;
896 case BOTTOM: dy = 1.5; break;
897 }
898 var sgnx = Math.sign(dx);
899 var sgny = Math.sign(dy);
900 var $s = [];
901 var positions = [];
902 var blocks = this.slammer.controller.blocks;
903 this.slammer.data.quads.forEach(function (q) {
904 var d = blocks[q.x].length;
905 $s.push(q.quad);
906 positions.push([q.quad.position[0], q.quad.position[1] - d]);
907 }, this);
908 this.board.data.quads.forEach(function (q) {
909 var x = orientation === TOP ? q.x : blocks.length - (q.x + 1);
910 var y = orientation === LEFT ? q.y : blocks.length - (q.y + 1);
911
912 $s.push(q.quad);
913 positions.push([q.quad.position[0] + sgnx * blocks[y].length,
914 q.quad.position[1] + sgny * blocks[x].length]);
915 }, this);
916 this.entity0.attach(new yuu.Animation(SLIDE_BLOCKS, {
917 $s: $s,
918 positions: positions
919 }));
920 },
921
922 slam: yuu.cmd(function () {
923 var r = new Promise(function (resolve, reject) {
924 if (this._locks.some("slam")) {
925 reject("slamming is locked");
926 return;
927 }
928 this._locks.increment("spin", "slam");
929 circleScene.slam();
930 handScene.slam(this.slammer.controller.orientation);
931 this.entity0.attach(new yuu.Animation(
932 SLAMMER_SLAM, {
933 $: this.slammer.transform,
934 slam: function () {
935 this._locks.decrement("spin");
936 this.slammer.controller.slam(this.board.controller);
937 this.slammerRoot.transform.yaw = Math.PI / 2 *
938 this.slammer.controller.orientation;
939 }.bind(this),
940 slideBoardBlocks: this.slideBoardBlocks.bind(this)
941 }, function () {
942 this.checkWon();
943 this._locks.decrement("slam");
944 resolve(this);
945 }.bind(this)));
946 }.bind(this));
947 return r;
948 }, "", "slam the active piece"),
949
950 back: yuu.cmd(function (x, y) {
951 if (this._locks.some("quit"))
952 return;
953 this._locks.increment("quit", "slam", "spin");
954 var scene = new MenuScene(this.level);
955 yuu.director.pushScene(scene);
956 var v = [x || yuu.random.uniform(-1, 1),
957 y || yuu.random.uniform(-1, 1)];
958 var size = this.board.controller.contents.length * 5;
959 vec2.scale(v, vec2.normalize(v, v), size);
960 this.entity0.attach(new yuu.Animation(
961 GRID_DISMISS, {
962 $: this.entity0.transform,
963 x: v[0], y: v[1]
964 }, function () {
965 yuu.director.removeScene(this);
966 }.bind(this)
967 ));
968 circleScene.lose();
969 }, "", "go back to the menu"),
970
971 slammerBB: { get: function (p) {
972 var length = this.level.slammer.length;
973 switch (this.slammer.controller.orientation) {
974 case LEFT:
975 return new yuu.AABB(-Infinity, -0.5, -1, length - 0.5);
976 case RIGHT:
977 return new yuu.AABB(length + 1, -0.5, Infinity, length - 0.5);
978 case TOP:
979 return new yuu.AABB(-0.5, length + 1, length - 0.5, Infinity);
980 case BOTTOM:
981 return new yuu.AABB(-0.5, -Infinity, length - 0.5, -1);
982 }
983 } },
984
985 _swipe: function (p0, p1) {
986 p0 = this.layer0.worldFromDevice(p0);
987 p1 = this.layer0.worldFromDevice(p1);
988 if (this.slammerBB.contains(p0)) {
989 this.slam();
990 return true;
991 }
992 if (this.gridBB.contains(p0) && !this.gridBB.contains(p1)) {
993 this.back(p1.x - p0.x, p1.y - p0.y);
994 return true;
995 }
996 },
997
998 inputs: {
999 resize: function () {
1000 var length = this.level.slammer.length;
1001 var base = new yuu.AABB(-length - 2.5, -length - 2.5,
1002 2 * length + 1.5, 2 * length + 1.5);
1003 var vp = base.matchAspectRatio(yuu.viewport);
1004 this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
1005 },
1006
1007 tap: function (p) {
1008 p = this.layer0.worldFromDevice(p);
1009 if (this.gridBB.contains(p)) {
1010 this.slam();
1011 return true;
1012 }
1013 },
1014
1015 touch: function (p) {
1016 var length = this.level.slammer.length;
1017 var middle = (length - 1) / 2;
1018 p = this.layer0.worldFromDevice(p);
1019 if (this.slammerBB.contains(p)) {
1020 this.slammer.attach(new yuu.Animation(
1021 SLAMMER_BOUNCE, { $: this.slammer.transform }));
1022 } else if (this.leftBB.contains(p)) {
1023 this.rotateTo(LEFT);
1024 handScene.rotatedCw();
1025 return true;
1026 } else if (this.rightBB.contains(p)) {
1027 this.rotateTo(RIGHT);
1028 handScene.rotatedCcw();
1029 return true;
1030 } else if (this.topBB.contains(p)) {
1031 this.rotateTo(TOP);
1032 if (p.x < middle)
1033 handScene.rotatedCw();
1034 else
1035 handScene.rotatedCcw();
1036 return true;
1037 } else if (this.bottomBB.contains(p)) {
1038 this.rotateTo(BOTTOM);
1039 if (p.x < middle)
1040 handScene.rotatedCw();
1041 else
1042 handScene.rotatedCcw();
1043 return true;
1044 }
1045 },
1046
1047 doubletap: function () {
1048 return this.inputs.tap.apply(this, arguments);
1049 },
1050
1051 hold: function (p) {
1052 p = this.layer0.worldFromDevice(p);
1053 if (this.gridBB.contains(p)) {
1054 this.undo(true);
1055 return true;
1056 }
1057 },
1058
1059 dragstart: function (p) {
1060 p = this.layer0.worldFromDevice(p);
1061 this._dragging = this.slammerBB.contains(p);
1062 },
1063
1064 drag: function (p0, p1) {
1065 var p = this.layer0.worldFromDevice(p1);
1066 if (this._dragging && !this._locks.some("slam")) {
1067 var inGrid = this.gridBB.contains(p);
1068 var length = this.level.slammer.length;
1069 var o;
1070 if (this._dragging === true && inGrid) {
1071 this.slam();
1072 } else if (p.x > 0 && p.x < length && !inGrid) {
1073 o = p.y < 0 ? BOTTOM : TOP;
1074 if (o !== this.slammer.controller.orientation) {
1075 this.rotateTo(o);
1076 this._dragging = 2;
1077 }
1078 } else if (p.y > 0 && p.y < length && !inGrid) {
1079 o = p.x < 0 ? LEFT : RIGHT;
1080 if (o !== this.slammer.controller.orientation) {
1081 this.rotateTo(o);
1082 this._dragging = 2;
1083 }
1084 }
1085 }
1086 return this._dragging;
1087 },
1088
1089 dragend: function (p0, p1) {
1090 this._dragging = false;
1091 },
1092
1093 release: function () {
1094 this.undo(false);
1095 },
1096
1097 swipeleft: function (p0, p1) {
1098 return this._swipe(p0, p1);
1099 },
1100 swiperight: function (p0, p1) {
1101 return this._swipe(p0, p1);
1102 },
1103 swipeup: function (p0, p1) {
1104 return this._swipe(p0, p1);
1105 },
1106 swipedown: function (p0, p1) {
1107 return this._swipe(p0, p1);
1108 },
1109 },
1110
1111 KEYBINDS: {
1112 w: "rotateCw",
1113 a: "rotateCcw",
1114 s: "rotateCcw",
1115 d: "rotateCw",
1116 left: "rotateCcw",
1117 right: "rotateCw",
1118 up: "rotateCw",
1119 down: "rotateCcw",
1120 shift: "slam",
1121 space: "slam",
1122 z: "slam",
1123 backspace: "+undo",
1124 c: "+undo",
1125 escape: "back",
1126 back: "back",
1127 gamepadbutton0: "slam",
1128 gamepadbutton1: "+undo",
1129 gamepadbutton2: "slam",
1130 gamepadbutton3: "+undo",
1131 gamepadbutton4: "rotateCcw",
1132 gamepadbutton5: "rotateCw",
1133 gamepadbutton8: "back",
1134 gamepadbutton14: "rotateCcw",
1135 gamepadbutton15: "rotateCw",
1136 }
1137 });
1138
1139 var MENU_APPEAR = {
1140 0: [{ set1: { x: 5, y: 5, scaleX: 0, scaleY: 0 } },
1141 { tween1: { x: 0, y: 0, scaleX: 1 }, duration: 24 },
1142 { tween1: { scaleY: 1 },
1143 duration: 55, easing: yuu.Tween.METASPRING(1, 10)}],
1144 };
1145
1146 var MENU_SLIDE = {
1147 0: { tween1: { x: "x" }, duration: "duration" }
1148 };
1149
1150 var FLASH = {
1151 0: { tween1: { luminance: 1, alpha: 1 }, duration: 32, repeat: -1 }
1152 };
1153
1154 var MENU_SLAM = {
1155 0: { tween: { cursor: { y: "mid" } },
1156 playSound: "@winding",
1157 duration: 8 },
1158 8: { tween: { cursor: { y: "line" },
1159 select: { y: -1.0 }
1160 }, duration: 12 },
1161 20: { tween: { scene: { y: 10 },
1162 select: { y: -11.5, scale: [3, 3, 1] }
1163 }, duration: 18 },
1164 38: { event: "appear",
1165 tween: { select: { y: 0, scale: [1, 0, 1] } },
1166 duration: 20 }
1167 };
1168
1169 function menuEntityForLevel (level, i) {
1170 var activated = false;
1171 function randomizeSlammer () {
1172 var min = level.randomSlammer[0];
1173 var max = level.randomSlammer[1];
1174 var size = yuu.random.randrange(min, max + 1);
1175 level.slammer = [];
1176 do {
1177 for (var i = 0; i < size; ++i)
1178 level.slammer[i] = yuu.random.randrange(0, size);
1179 } while (Math.min.apply(Math, level.slammer) === max
1180 || Math.max.apply(Math, level.slammer) === 0);
1181 }
1182
1183 function generateQuads() {
1184 batch.disposeAll();
1185 var rgb = PALETTE[i % PALETTE.length];
1186 var fit = level.slammer.length + 1;
1187 batch.createQuad().color = rgb;
1188 level.slammer.forEach(function (size, y) {
1189 var c = batch.createQuad();
1190 c.color = [0, 0, 0];
1191 c.alpha = hasBeaten(level, "easy") ? 0.5 : 1.0;
1192 c.size = [size / fit, 1 / fit];
1193 c.position = [0, -0.5 + (y + 1) / fit];
1194 });
1195 ce.data.flasher = batch.createQuad();
1196 ce.data.flasher.alpha = 0;
1197 return ce;
1198 }
1199
1200 if (level.randomSlammer)
1201 randomizeSlammer();
1202
1203 // 14 = maximum slammer size + 1 background + 1 flasher
1204 var batch = new yuu.QuadBatchC(14);
1205 var ce = new yuu.E(
1206 new yuu.Transform([2 * i, 0, 0]),
1207 batch,
1208 new yuu.DataC({
1209 activate: function () {
1210 activated = true;
1211 var scene = new GridScene(level, difficultyForLevel(level));
1212 yuu.director.insertUnderScene(scene);
1213 }
1214 })
1215 );
1216
1217 if (level.randomSlammer) {
1218 ce.attach(new yuu.Ticker(function () {
1219 if (!activated && yuu.random.randbool(0.7)) {
1220 randomizeSlammer();
1221 ++i;
1222 generateQuads();
1223 }
1224 return !activated;
1225 }, 30, 15));
1226 }
1227
1228 generateQuads();
1229 return ce;
1230 }
1231
1232 var HAND_TICK_BACK = {
1233 0: { tween1: { rotation: "rotation" }, duration: 6, repeat: -1 }
1234 };
1235
1236 MenuScene = yT(yuu.Scene, {
1237 constructor: function (initialLevel) {
1238 yuu.Scene.call(this);
1239 this.entity0.attach(new yuu.Transform(),
1240 new AnimationQueue());
1241
1242 this.pointer = new yuu.E(
1243 new yuu.Transform([5, 8, 0]),
1244 new yuu.QuadC());
1245
1246 var menu = this.menu = new yuu.E(new yuu.Transform([5, 6.5, 0]));
1247 this.addEntities(menu, this.pointer);
1248 this.availableLevels = LEVELS.filter(difficultyForLevel);
1249 this.availableLevels
1250 .map(menuEntityForLevel)
1251 .forEach(menu.addChild, menu);
1252
1253 var initialIdx = this.availableLevels.indexOf(initialLevel);
1254 this._locks = new FlagSet("slam", "move");
1255 this.activeIndex = Math.max(initialIdx, 0);
1256 menu.transform.x = 5 - 2 * this.activeIndex;
1257 this.changeActiveIndex(this.activeIndex, false);
1258 this._dragStartX = null;
1259
1260 this.entity0.attach(
1261 new yuu.Ticker(this._animation.bind(this), 60));
1262 },
1263
1264 _animation: function (count) {
1265 var length = this.availableLevels.length;
1266 var range = Math.pow(2, length);
1267 var rand = yuu.random.randrange(range);
1268 var targets = [];
1269 var yaws = [];
1270 for (var i = 0; i < length; ++i) {
1271 var child = this.menu.children[i];
1272 var level = this.availableLevels[i];
1273 var won = hasBeaten(level, "hard");
1274 if ((won || ((count ^ i) & 1)) && ((count ^ rand) & (1 << i))) {
1275 var dyaw = won
1276 ? yuu.random.randsign(Math.PI / 2)
1277 : -Math.PI / 2;
1278 targets.push(child.transform);
1279 yaws.push(child.transform.yaw + dyaw);
1280 }
1281 }
1282 if (targets.length) {
1283 this.entity0.attach(new yuu.Animation(
1284 ROTATE_ALL, { $s: targets, yaws: yaws }));
1285 }
1286 circleScene.clockTick(TICK_ROT2, HAND_TICK_BACK);
1287 sounds[["tick", "tock"][count & 1]]
1288 .createSound(yuu.audio, yuu.audio.currentTime, 0, 0.2, 1.0)
1289 .connect(yuu.audio.music);
1290
1291 return true;
1292 },
1293
1294 init: function () {
1295 circleScene.toBottom();
1296 handScene.finished();
1297 this._locks.increment("slam", "move");
1298 this.entity0.animationQueue.enqueue(
1299 MENU_APPEAR,
1300 { $: this.entity0.transform })
1301 .then(this._locks.decrementer("slam", "move"));
1302 },
1303
1304 didWinLevel: function (level, difficulty, firstTime) {
1305 var idx = this.availableLevels.indexOf(level);
1306 circleScene.win();
1307 if (firstTime)
1308 this.entity0.animationQueue.enqueue(
1309 FLASH, { $: this.menu.children[idx].data.flasher });
1310 for (var i = idx; i < this.availableLevels.length; ++i) {
1311 if (!hasBeaten(this.availableLevels[i], difficulty)) {
1312 this._locks.increment("move");
1313 this.changeActiveIndex(i, true)
1314 .then(this._locks.decrementer("move"));
1315
1316 break;
1317 }
1318 }
1319 },
1320
1321 changeActiveIndex: function (index, animate) {
1322 var oldIndex = this.activeIndex;
1323 var p;
1324 this.activeIndex = index = yf.clamp(
1325 index, 0, this.menu.children.length - 1);
1326 if (index !== oldIndex && animate) {
1327 this._locks.increment("slam");
1328 var duration = Math.ceil(8 * Math.abs(oldIndex - index));
1329 p = this.entity0.animationQueue.enqueue(
1330 MENU_SLIDE, {
1331 $: this.menu.transform,
1332 x: 5 - 2 * index,
1333 duration: duration
1334 });
1335 p.then(this._locks.decrementer("slam"));
1336 }
1337 return p || Promise.resolve();
1338 },
1339
1340 left: yuu.cmd(function () {
1341 if (!this._locks.some("move")) {
1342 sounds[this.activeIndex === 0 ? "switchBroke" : "switch"].play();
1343 handScene.movedLeft();
1344 this.changeActiveIndex(this.activeIndex - 1, true);
1345 }
1346 }, "move the cursor left"),
1347 right: yuu.cmd(function () {
1348 if (!this._locks.some("move")) {
1349 sounds[this.activeIndex === this.availableLevels.length - 1
1350 ? "switchBroke" : "switch"].play();
1351 handScene.movedRight();
1352 this.changeActiveIndex(this.activeIndex + 1, true);
1353 }
1354 }, "move the cursor right"),
1355
1356 slam: yuu.cmd(function () {
1357 if (this._locks.some("slam"))
1358 return;
1359 var activeChild = this.menu.children[this.activeIndex];
1360 this._locks.increment("slam", "move");
1361 handScene.menuChoice();
1362 circleScene.toBack();
1363 circleScene.slam();
1364 this.entity0.animationQueue.enqueue(
1365 MENU_SLAM, {
1366 cursor: this.pointer.transform,
1367 select: activeChild.transform,
1368 scene: this.entity0.transform,
1369 mid: this.pointer.transform.y - 0.5,
1370 line: this.pointer.transform.y - 1.5,
1371 appear: activeChild.data.activate
1372 }).then(function () {
1373 this._locks.decrementer("slam", "move");
1374 yuu.director.removeScene(this);
1375 }.bind(this));
1376 }, "choose the active menu item"),
1377
1378 inputs: {
1379 resize: function () {
1380 var base = new yuu.AABB(0, 0, 10, 10);
1381 var vp = base.matchAspectRatio(yuu.viewport);
1382 this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
1383 },
1384
1385 pinchout: function (p0, p1) {
1386 p0 = this.layer0.worldFromDevice(p0);
1387 p1 = this.layer0.worldFromDevice(p1);
1388 if (vec2.sqrDist(p0, p1) > 1) {
1389 this.slam();
1390 return true;
1391 }
1392 },
1393
1394 hold: function (p) {
1395 return this.inputs.dragstart.call(this, p);
1396 },
1397
1398 release: function (p) {
1399 if (this._dragStartX !== null)
1400 return this.inputs.dragend.call(this, p);
1401 },
1402
1403 dragstart: function (p) {
1404 if (this._locks.some("move"))
1405 return false;
1406 p = this.layer0.worldFromDevice(p);
1407 if (p.y > 6 && p.y < 8.5 && p.inside && this._dragStartX === null) {
1408 sounds.switchOn.play();
1409 this._locks.increment("move");
1410 this._dragStartX = this.menu.transform.x;
1411 return true;
1412 }
1413 },
1414
1415 dragdown: function (p0, p1) {
1416 p0 = this.layer0.worldFromDevice(p0);
1417 p1 = this.layer0.worldFromDevice(p1);
1418
1419 if (p0.x >= 4.5 && p0.x <= 5.5
1420 && p0.y >= 6.0 && p0.y <= 8.5
1421 && p0.y - p1.y > 1) {
1422 this.slam();
1423 return true;
1424 }
1425 },
1426
1427 drag: function (p0, p1) {
1428 if (this._dragStartX !== null) {
1429 p0 = this.layer0.worldFromDevice(p0);
1430 p1 = this.layer0.worldFromDevice(p1);
1431 this.menu.transform.x = this._dragStartX + (p1.x - p0.x);
1432 var index = Math.round((5 - this.menu.transform.x) / 2);
1433 this.changeActiveIndex(index);
1434 return true;
1435 }
1436 },
1437
1438 dragend: function (p0, p1) {
1439 if (this._dragStartX !== null) {
1440 sounds.switchOff.play();
1441 this._locks.decrement("move");
1442 this._dragStartX = null;
1443 var index = this.activeIndex;
1444 this.activeIndex = (5 - this.menu.transform.x) / 2;
1445 this.changeActiveIndex(index, true);
1446 return true;
1447 }
1448 },
1449
1450 tap: function (p) {
1451 p = this.layer0.worldFromDevice(p);
1452 if (p.y > 6 && p.y < 7 && p.inside) {
1453 var dx = Math.round((p.x - 5) / 2);
1454 if (dx === 0) this.slam();
1455 else if (dx < 0) handScene.movedLeft();
1456 else if (dx > 0) handScene.movedRight();
1457 var idx = this.activeIndex;
1458 this.changeActiveIndex(this.activeIndex + dx, true);
1459 if (idx !== this.activeIndex)
1460 sounds.switch.play();
1461 else
1462 sounds.switchBroke.play();
1463 return true;
1464 }
1465
1466 },
1467
1468 doubletap: function (p) {
1469 p = this.layer0.worldFromDevice(p);
1470 if (p.x >= 4.5 && p.x <= 5.5 && p.y >= 6.0 && p.y <= 8.5) {
1471 this.slam();
1472 return true;
1473 }
1474 },
1475 },
1476
1477 resetEverything: yuu.cmd(function () {
1478 storage.clear();
1479 yuu.director.stop();
1480 start();
1481 }, "reset all saved data"),
1482
1483 unlock: yuu.cmd(function (d) {
1484 LEVELS.forEach(function (level) { wonLevel(level, d); });
1485 yuu.director.pushPopScene(new MenuScene());
1486 }, "<difficulty>", "unlock all levels to the given difficulty"),
1487
1488 KEYBINDS: {
1489 left: "left",
1490 right: "right",
1491 up: "right",
1492 down: "left",
1493 w: "right",
1494 a: "left",
1495 s: "left",
1496 d: "right",
1497 shift: "slam",
1498 space: "slam",
1499 z: "slam",
1500 "`+r+e": "resetEverything",
1501 "`+u+e": "unlock easy",
1502 "`+u+h": "unlock hard",
1503 gamepadbutton0: "slam",
1504 gamepadbutton8: "help",
1505 gamepadbutton9: "slam",
1506 gamepadbutton13: "slam",
1507 gamepadbutton14: "left",
1508 gamepadbutton15: "right",
1509 }
1510 });
1511
1512
1513 var BOOK_APPEAR = {
1514 0: { set1: { y: 1.5, x: -1.5 },
1515 tween: { bgQuad: { alpha: 0.75 }, $: { y: 0, x: 0 }, },
1516 playSound: "@book-appear",
1517 duration: 30 }
1518 };
1519
1520 var BOOK_DISMISS = {
1521 0: { tween: { bgQuad: { alpha: 0 }, $: { y: 1.5, x: -1.5, } },
1522 playSound: "@book-dismiss",
1523 duration: 30 }
1524 };
1525
1526 var KEYBOARD_PAGE = [0.25, 0.50, 0.50, 1.00];
1527 var POINTERS_PAGE = [0.25, 0.00, 0.50, 0.50];
1528 var GAMEPAD_PAGE = [0.00, 0.00, 0.25, 0.50];
1529
1530 var BOOK_FORWARD = [
1531 { 0: { set: { page2Quad: { color: [0.2, 0.2, 0.2, 1], texBounds: "page" } },
1532 tween: { page1: { x: -1/3 / 2, scaleX: 0 },
1533 page2: { x: +1/3 / 2 },
1534 page2Quad: { color: [1, 1, 1, 1] },
1535 }, duration: 15, easing: "linear",
1536 playSound: ["@page-turn-1", "@page-turn-2", "@page-turn-3"]
1537 },
1538 15: { set: { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] },
1539 page2Quad: { z: 0, texBounds: "page" } },
1540 tween: { page1: { x: -1/3, scaleX: -2/3 },
1541 page2: { x: +1/3 }
1542 }, duration: 15, easing: "linear" },
1543 },
1544
1545 { 0: { tween: { page1: { x: -1/3 / 2 },
1546 page2: { x: +1/3 / 2, scaleX: 0 }
1547 }, duration: 15, easing: "linear",
1548 playSound: ["@page-turn-1", "@page-turn-2", "@page-turn-3"]
1549 },
1550 15: { set: { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] },
1551 page2Quad: { z: 1, texBounds: [1.00, 0.5, 0.75, 1] } },
1552 tween: { page1Quad: { color: [0.2, 0.2, 0.2, 1] },
1553 page1: { x: 0 },
1554 page2: { x: 0, scaleX: -2/3 },
1555 }, duration: 15, easing: "linear" },
1556 },
1557
1558 BOOK_DISMISS
1559 ];
1560
1561 var BOOK_BACKWARD = [
1562 { 0: { tween: { page1: { x: -1/3 / 2, scaleX: 0 },
1563 page2: { x: +1/3 / 2 },
1564 }, duration: 15, easing: "linear",
1565 playSound: ["@page-turn-1", "@page-turn-2", "@page-turn-3"]
1566 },
1567 15: { set: { page1Quad: { z: 1, texBounds: [0.50, 0.5, 0.75, 1] },
1568 page2Quad: { z: 0 } },
1569 tween: { page2Quad: { color: [0.2, 0.2, 0.2, 1] },
1570 page1: { x: 0, scaleX: 2/3 },
1571 page2: { x: 0 },
1572 }, duration: 15, easing: "linear" },
1573 },
1574
1575 { 0: { set: { page1Quad: { color: [0.2, 0.2, 0.2, 1] } },
1576 tween: { page1Quad: { color: [1.0, 1.0, 1.0, 1] },
1577 page1: { x: -1/3 / 2 },
1578 page2: { x: +1/3 / 2, scaleX: 0 }
1579 }, duration: 15, easing: "linear",
1580 playSound: ["@page-turn-1", "@page-turn-2", "@page-turn-3"]
1581 },
1582
1583 15: { set: { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] },
1584 page2Quad: { z: 0, texBounds: "page" } },
1585 tween: { page1: { x: -1/3 },
1586 page2: { x: +1/3, scaleX: 2/3 },
1587 }, duration: 15, easing: "linear" },
1588 },
1589 ];
1590
1591 BookScene = new yT(yuu.Scene, {
1592 constructor: function () {
1593 yuu.Scene.call(this);
1594 var bg = new yuu.E(
1595 new yuu.Transform().setScale([20, 20, 1]),
1596 this.bgQuad = new yuu.QuadC()
1597 .setColor([0, 0, 0, 0])
1598 .setZ(-1));
1599 this.page1 = new yuu.E(new yuu.Transform(),
1600 this.page1Quad = new yuu.QuadC(BOOK));
1601 this.page1Quad.texBounds = [0.50, 0.5, 0.75, 1];
1602 this.page1Quad.z = 1;
1603 this.page2 = new yuu.E(new yuu.Transform(),
1604 this.page2Quad = new yuu.QuadC(BOOK));
1605 this.page2Quad.texBounds = [0.25, 0.5, 0.50, 1];
1606 this.page1.transform.scale = [2/3, 1, 1];
1607 this.page2.transform.scale = [2/3, 1, 1];
1608 this.entity0.attach(new yuu.Transform());
1609 this.current = 0;
1610 this._locks = new FlagSet("turn");
1611 this.addEntities(bg, this.page1, this.page2);
1612
1613 this.ready = yuu.ready(yf.map(yf.new_(yuu.Instrument), [
1614 "@page-turn-1", "@page-turn-2", "@page-turn-3",
1615 "@book-dismiss"
1616 ]));
1617 },
1618
1619 help: yuu.cmd(function () {
1620 this.skip();
1621 }, "dismiss the help screen"),
1622
1623 licensing: yuu.cmd(function () {
1624 var licensing = document.getElementById("yuu-licensing");
1625 var parent = licensing.parentNode;
1626 var spinner = document.createElement("div");
1627 spinner.className = "yuu-spinner";
1628 spinner.id = licensing.id;
1629 parent.replaceChild(spinner, licensing);
1630 Promise.all(
1631 yf.map(yuu.GET,
1632 [yuu.PATH + "data/license.txt", "data/license.txt"]))
1633 .then(function (texts) {
1634 var text = texts.join("\n-- \n\n");
1635 var p = document.createElement("pre");
1636 p.textContent = text;
1637 p.id = spinner.id;
1638 parent.replaceChild(p, spinner);
1639 });
1640 }, "why would you ever want to run this?"),
1641
1642 init: function () {
1643 this._anim(BOOK_APPEAR);
1644 storage.setFlag("instructions");
1645 },
1646
1647 _anim: function (anim) {
1648 this._locks.increment("turn");
1649 var completion = this._locks.decrementer("turn");
1650 switch (anim) {
1651 case BOOK_DISMISS:
1652 completion = yuu.director.removeScene.bind(yuu.director, this);
1653 break;
1654 }
1655
1656 var device = yuu.director.preferredDevice();
1657 this.entity0.attach(new yuu.Animation(
1658 anim, {
1659 $: this.entity0.transform,
1660 page: device === "keyboard" ? KEYBOARD_PAGE
1661 : device === "gamepad" ? GAMEPAD_PAGE
1662 : POINTERS_PAGE,
1663 page1: this.page1.transform,
1664 page2: this.page2.transform,
1665 page1Quad: this.page1Quad,
1666 page2Quad: this.page2Quad,
1667 bgQuad: this.bgQuad
1668 }, completion));
1669 },
1670
1671 advance: yuu.cmd(function () {
1672 if (this._locks.some("turn"))
1673 return;
1674 this._anim(BOOK_FORWARD[this.current++]);
1675 }),
1676
1677 skip: yuu.cmd(function () {
1678 if (this._locks.some("turn"))
1679 return;
1680 this._anim(BOOK_DISMISS);
1681 }),
1682
1683 back: yuu.cmd(function () {
1684 if (this._locks.some("turn"))
1685 return;
1686 if (this.current > 0)
1687 this._anim(BOOK_BACKWARD[--this.current]);
1688 }),
1689
1690 LOGOTYPE: new yuu.AABB(-0.16, -0.41, 0.12, -0.33),
1691 COLOPHON: new yuu.AABB(-0.06, -0.41, 0.11, -0.28),
1692
1693 inputs: {
1694 resize: function () {
1695 var base = new yuu.AABB(-0.7, -0.55, 0.7, 0.55);
1696 var vp = base.matchAspectRatio(yuu.viewport);
1697 this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
1698 },
1699
1700 mousemove: function (p) {
1701 p = this.layer0.worldFromDevice(p);
1702 if (this.current === BOOK_FORWARD.length - 1
1703 && this.LOGOTYPE.contains(p)) {
1704 this.cursor = "pointer";
1705 } else if (this.current === 0 && this.COLOPHON.contains(p)) {
1706 this.cursor = "pointer";
1707 } else if (this.current === 0 || p.x >= -0.2) {
1708 this.cursor = "";
1709 } else {
1710 this.cursor = "W-resize";
1711 }
1712 },
1713
1714 tap: function (p) {
1715 p = this.layer0.worldFromDevice(p);
1716 if (this.current === BOOK_FORWARD.length - 1
1717 && this.LOGOTYPE.contains(p)) {
1718 yuu.openURL("https://www.yukkurigames.com/");
1719 } else if (this.current === 0 && this.COLOPHON.contains(p)) {
1720 yuu.director.showOverlay("colophon");
1721 } else if (this.current === 0 || p.x >= -0.2) {
1722 this.advance();
1723 } else {
1724 this.back();
1725 }
1726 return true;
1727 },
1728 swipeleft: function (event) { this.advance(); return true; },
1729 swiperight: function (event) { this.back(); return true; },
1730 dragleft: function (event) { this.advance(); return true; },
1731 dragright: function (event) { this.back(); return true; },
1732 swipeup: function (event) { this.skip(); return true; },
1733 dragup: function (event) { this.skip(); return true; },
1734
1735 consume: yuu.Director.prototype.GESTURES
1736 .concat(yuu.Director.prototype.CANVAS_EVENTS)
1737 },
1738
1739 KEYBINDS: {
1740 space: "advance",
1741 shift: "advance",
1742 z: "advance",
1743 x: "advance",
1744 right: "advance",
1745 left: "back",
1746 back: "skip",
1747 escape: "skip",
1748 gamepadbutton0: "advance",
1749 gamepadbutton1: "skip",
1750 gamepadbutton4: "back",
1751 gamepadbutton5: "advance",
1752 gamepadbutton8: "skip",
1753 gamepadbutton9: "skip",
1754 gamepadbutton14: "back",
1755 gamepadbutton15: "advance",
1756 }
1757 });
1758
1759 var OUTER_FLIP_TICK = {
1760 0: { tween1: { yaw: "yaw" }, duration: 15 }
1761 };
1762
1763 var CIRCLE_TO_BOTTOM = {
1764 0: { tween1: { pitch: Math.PI * 0.35, y: -0.3 }, duration: 35 }
1765 };
1766
1767 var CIRCLE_TO_BACK = {
1768 0: { tween1: { pitch: Math.PI * 0.15, y: -0.1 }, duration: 35 }
1769 };
1770
1771 var CIRCLE_INNER_RATCHET = {
1772 0: { tween1: { rotation: "rotation1" }, duration: 15,
1773 playSound: "@regear"
1774 },
1775 10: { tween1: { rotation: "rotation2" }, duration: 10 },
1776 20: { tween1: { rotation: "rotation1" }, duration: 20,
1777 easing: yuu.Tween.STEPPED(5) },
1778 40: { tween1: { rotation: "rotation2" }, duration: 15 }
1779 };
1780
1781 var CIRCLE_INNER_WIND = {
1782 0: { tween1: { rotation: "rotation1" }, duration: 8 },
1783 15: { tween1: { rotation: "rotation2" }, duration: 20 },
1784 };
1785
1786 var BACKGROUND_DRIFT = {
1787 0: [{ tween1: { yaw: Math.PI * 2 },
1788 duration: 13 * 60 * 60, repeat: -Infinity, easing: "linear" },
1789 { tween1: { scaleX: 0.5 },
1790 duration: 11 * 60 * 60, repeat: -Infinity },
1791 { tween1: { scaleY: 0.5 },
1792 duration: 7 * 60 * 60, repeat: -Infinity }]
1793 };
1794
1795 var HAND_TICK = {
1796 0: { tween1: { rotation: "rotation" }, duration: 6 }
1797 };
1798
1799 var CHIMES = [
1800 // Nearly all derived from
1801 // http://www.clockguy.com/SiteRelated/SiteReferencePages/ClockChimeTunes.html
1802 //
1803 // All transposition & transcription errors are mine.
1804
1805 { name: "Westminster",
1806 keys: ["D4", "E4"],
1807 bars: ["0 2 1 -3",
1808 "0 1 2 0",
1809 "2 0 1 -3",
1810 "2 1 0 -3",
1811 "-3 1 2 0"]
1812 },
1813
1814 { name: "Wittington",
1815 keys: ["Eb4", "E4"],
1816 bars: ["1 2 3 5 4 6 7 0",
1817 "1 3 5 7 6 4 2 0",
1818 "3 1 2 4 5 6 7 0",
1819 "4 3 5 2 6 1 7 0",
1820 "6 7 2 5 4 1 3 0",
1821 "7 1 6 2 5 3 4 0",
1822 "7 3 2 1 4 5 6 0",
1823 "7 3 6 2 5 1 4 0",
1824 "7 5 3 1 6 4 2 0",
1825 "7 5 6 4 1 3 2 0",
1826 "7 6 3 2 5 4 1 0",
1827 "7 6 5 4 3 2 1 0"]
1828 },
1829
1830 { name: "Canterbury",
1831 keys: ["D4", "E4"],
1832 bars: ["2 0 5 3 1 4",
1833 "3 5 1 4 0 2",
1834 "3 5 4 2 1 0",
1835 "5 3 1 4 2 0",
1836 "1 3 5 2 0 4",
1837 "0 5 3 1 2 4",
1838 "5 3 1 2 4 0"]
1839 },
1840
1841 { name: "Trinity",
1842 keys: ["F3", "D4"],
1843 bars: ["5 4 3 2 1 0",
1844 "2 4 3 1 2 0",
1845 "5 3 4 2 1 0",
1846 "4 3 2 1 5 2",
1847 "5 0 4 3 2 1"]
1848 },
1849
1850 /*
1851 { name: "St. Michael's",
1852 keys: ["F3", "C4"],
1853 bars: ["7 6 5 4 3 2 1 0",
1854 "7 1 2 3 6 4 5 0",
1855 "4 3 2 5 1 6 7 0",
1856 "6 7 2 3 1 4 5 0",
1857 "4 6 2 7 3 1 5 0"]
1858 },
1859 */
1860
1861 { name: "Winchester",
1862 keys: ["C4", "E4"],
1863 bars: ["5 3 1 0 2 4",
1864 "0 1 3 5 4 2",
1865 "5 3 1 4 2 0",
1866 "1 2 5 4 0 1",
1867 "5 1 3 2 4 0"]
1868 }
1869
1870 ];
1871
1872 function third (s) {
1873 return "Q " + s.replace(/([^ ]+ [^ ]+)/g, "$1 Z");
1874 }
1875
1876 function silence (s) {
1877 return "Q " + s.replace(/[^ ]+/g, "Z");
1878 }
1879
1880 var TIMES1 = ["Q", "H", "H", "H", "H.", "H.", "W", "W"];
1881 var TIMES2 = ["Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q",
1882 third, third, "Q.", "Q.",
1883 "H", "H"];
1884 var TIMES3 = ["Q", "Q", silence, silence,
1885 third, third, third, third,
1886 "Q.", "Q.",
1887 "H", "H"];
1888
1889 function deck (pack, random) {
1890 random = random || yuu.random;
1891 var stock = [];
1892 return function () {
1893 if (stock.length === 0)
1894 stock = random.shuffle(pack.slice());
1895 return stock.pop();
1896 };
1897 }
1898
1899 function generateScore () {
1900 var chimes = yuu.random.choice(CHIMES);
1901 var bar = deck(chimes.bars);
1902 function draw (t) {
1903 return yf.isFunction(t) ? t(bar()) : t + " " + bar();
1904 }
1905
1906 function line (times) {
1907 return yf.map(draw, yuu.random.shuffle(times)).join(" ");
1908 }
1909
1910 var track = "{ - W HZ " + line(TIMES1)
1911 + " { W HZ Z " + line(TIMES2)
1912 + " { W HZ Z Z I Z " + line(TIMES3);
1913 var key = yuu.random.choice(chimes.keys);
1914 yuu.log("messages", "Playing " + chimes.name + " in " + key + " major.");
1915 var score = yuu.parseScore(track, yuu.Scales.MAJOR, key);
1916 score.key = key;
1917 return score;
1918 }
1919
1920 CircleScene = yT(yuu.Scene, {
1921 constructor: function () {
1922 yuu.Scene.call(this);
1923 this.layer0.resize(-0.6, -0.6, 1.2, 1.2);
1924 var arm = this.arm = new yuu.E(new yuu.Transform());
1925 this.outer = new yuu.E(
1926 new yuu.Transform([Math.sqrt(2) / 5, -Math.sqrt(2) / 5, 0]),
1927 this.outerQuad = new yuu.QuadC(new yuu.Material("@circle-outer"))
1928 .setZ(1)
1929 .setLuminance(0.4)
1930 .setSize([0.35417, 0.35417]));
1931 arm.addChild(this.outer);
1932
1933 var rim = new yuu.E(
1934 new yuu.Transform(),
1935 this.rimQuad = new yuu.QuadC(new yuu.Material("@circle-rim"))
1936 .setLuminance(0.2));
1937 var inner = this.inner = new yuu.E(
1938 new yuu.Transform(),
1939 this.innerQuad = new yuu.QuadC(new yuu.Material("@circle-inner"))
1940 .setLuminance(0.3));
1941
1942 var NOISY_QUADS = new yuu.ShaderProgram(
1943 ["@noise.glsl", "@noisyquads"], ["yuu/@color"]);
1944
1945 var bgMat = new yuu.Material(
1946 yuu.Texture.DEFAULT, NOISY_QUADS, { range: 0.8 });
1947 bgMat.uniforms.cut = yf.volatile(cycler(100000));
1948 var DIM = 16;
1949 var batch = new yuu.QuadBatchC(DIM * DIM);
1950 batch.material = bgMat;
1951 var bg = new yuu.E(new yuu.Transform(), batch);
1952 yf.irange(function (x) {
1953 yf.irange(function (y) {
1954 var quad = batch.createQuad();
1955 quad.size = [1/4, 1/4];
1956 quad.position = [(x - DIM / 2) * 1/4,
1957 (y - DIM / 2) * 1/4];
1958 quad.color = [0.12, 0.08, 0.16];
1959 quad.texBounds = yf.repeat(x * DIM + y, 4);
1960 }, DIM);
1961 }, DIM);
1962
1963 this.entity0.addChild(bg);
1964 this.entity0.attach(new yuu.Animation(
1965 BACKGROUND_DRIFT, { $: bg.transform }));
1966
1967 this.ground = new yuu.E(new yuu.Transform());
1968 this.ground.addChildren(rim, inner, arm);
1969 this.entity0.addChild(this.ground);
1970
1971 this.music = yuu.audio.createGain();
1972 this.music.gain.value = 0.3;
1973 this.music.connect(yuu.audio.music);
1974 this._finished = false;
1975
1976 this.ready = yuu.ready([
1977 this.outerQuad.material,
1978 this.innerQuad.material,
1979 this.rimQuad.material,
1980 bgMat.ready
1981 ]);
1982 },
1983
1984 help: yuu.cmd(function () {
1985 yuu.director.pushScene(new BookScene());
1986 }, "bring up the help screen"),
1987
1988 yuu: yuu.cmd(function () {
1989 this.outerQuad.material = new yuu.Material("@circle-outer-ee");
1990 }, "yuu~"),
1991
1992 KEYBINDS: {
1993 slash: "help",
1994 f1: "help",
1995 gamepadbutton6: "help",
1996 f10: "showOverlay preferences",
1997 "shift+y+u+`": "yuu",
1998 "gamepadbutton10+gamepadbutton11": "yuu",
1999 },
2000
2001 inputs: {
2002 resize: function () {
2003 var vp = new yuu.AABB(-0.6, -0.6, 0.6, 0.6)
2004 .matchAspectRatio(yuu.viewport);
2005 this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
2006 }
2007 },
2008
2009 toBottom: function () {
2010 this.entity0.attach(
2011 new yuu.Animation(CIRCLE_TO_BOTTOM, { $: this.ground.transform }));
2012 },
2013
2014 wind: function () {
2015 var rot1 = this.inner.transform.rotation;
2016 quat.rotateZ(rot1, rot1, Math.PI / (2 * Math.E));
2017 var rot2 = quat.rotateX(quat.create(), rot1, -Math.PI / 2);
2018 quat.rotateY(rot2, rot2, Math.PI / 2);
2019 quat.rotateX(rot2, rot2, -Math.PI / 2);
2020 this.entity0.attach(
2021 new yuu.Animation(CIRCLE_INNER_WIND, {
2022 $: this.inner.transform,
2023 rotation1: rot1,
2024 rotation2: rot2
2025 }));
2026 this.tension = 0.5;
2027 this.reversed = 0;
2028 var score = [];
2029 score.key = this.score && this.score.key;
2030 this.score = score;
2031 },
2032
2033 _musicSchedule: function (count) {
2034 var t = yuu.director.currentAudioTime;
2035 var note;
2036
2037 if (this._finished) {
2038 if (this._finished === "won" && this.score.key) {
2039 var score = yuu.parseScore(
2040 yuu.random.choice([
2041 "1 3 2 Z 0 { - 1 Z 2 Z 0",
2042 "1 2 3 Z 0 { - 1 Z 3 Z 0",
2043 "0 1 2 Z 4 { - 0 Z 2 Z 4",
2044 ]),
2045 yuu.Scales.MAJOR, this.score.key);
2046 while ((note = score.shift())) {
2047 sounds.chime.createSound(
2048 yuu.audio,
2049 t + note.time / 4,
2050 note.hz,
2051 1.0, note.duration
2052 ).connect(this.music);
2053 }
2054 }
2055 this._finished = false;
2056 return false;
2057 }
2058
2059 if (!(this.score && this.score.length)) {
2060 this.score = generateScore();
2061 this.playing = 0;
2062 }
2063
2064 ++this.playing;
2065 while (this.score.length && this.score[0].time < this.playing) {
2066 note = this.score.shift();
2067 sounds.chime.createSound(
2068 yuu.audio,
2069 t + note.time % 1 + yuu.random.gauss(0, 0.015),
2070 note.hz,
2071 1.0, note.duration
2072 ).connect(this.music);
2073 }
2074
2075 if ((this.tension *= 0.95) > 1) {
2076 this.tension /= 2;
2077 sounds.winding.createSound(yuu.audio, t, 0, 1.0, 1.0)
2078 .connect(this.music);
2079 var flip = !this.outer.transform.yaw * yuu.random.randsign(Math.PI);
2080 this.entity0.attach(
2081 new yuu.Animation(OUTER_FLIP_TICK, {
2082 $: this.outer.transform,
2083 yaw: flip
2084 }));
2085 } else {
2086 [sounds.tick, sounds.tock][count & 1]
2087 .createSound(yuu.audio, t, 0, 0.5, 1.0)
2088 .connect(this.music);
2089 }
2090
2091 this.clockTick(this.reversed-- > 0 ? TICK_REV : TICK_ROT);
2092
2093 return true;
2094 },
2095
2096 clockTick: function (amount, anim) {
2097 var rot = this.arm.transform.rotation;
2098 quat.multiply(rot, rot, amount || TICK_ROT);
2099 this.arm.attach(new yuu.Animation(
2100 anim || HAND_TICK,
2101 { $: this.arm.transform, rotation: rot }));
2102 },
2103
2104 toBack: function () {
2105 this.wind();
2106 this.entity0.attach(
2107 new yuu.Animation(CIRCLE_TO_BACK, { $: this.ground.transform }));
2108
2109 this.playing = 4;
2110 this.arm.attach(
2111 new yuu.Ticker(this._musicSchedule.bind(this), 60));
2112 },
2113
2114 win: function () {
2115 this._finished = "won";
2116 this.wind();
2117 this.entity0.attach(
2118 new yuu.Animation(FLASH, { $: this.innerQuad }),
2119 new yuu.Animation(FLASH, { $: this.rimQuad }, null, 32),
2120 new yuu.Animation(FLASH, { $: this.outerQuad }, null, 48)
2121 );
2122 },
2123
2124 lose: function () {
2125 this._finished = "lose";
2126 var rot1 = this.inner.transform.rotation;
2127 quat.rotateZ(rot1, rot1, -Math.PI / Math.E);
2128 var rot2 = quat.rotateZ(quat.create(), rot1, Math.PI / Math.E);
2129 this.entity0.attach(
2130 new yuu.Animation(CIRCLE_INNER_RATCHET, {
2131 $: this.inner.transform,
2132 rotation1: rot1,
2133 rotation2: rot2
2134 }));
2135 },
2136
2137 rotated: function () {
2138 this.tension += yuu.random.uniform(0.1);
2139 },
2140
2141 slam: function () {
2142 this.tension += yuu.random.uniform(0.2);
2143 },
2144
2145 reverse: function () {
2146 this.tension -= yuu.random.uniform(0.1);
2147 this.reversed = Math.max(this.reversed, 0) + 1;
2148 }
2149
2150 });