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