17 var MenuScene
, CircleScene
, HandScene
, BookScene
, GridScene
;
19 yuu
.Texture
.DEFAULTS
.magFilter
= yuu
.Texture
.DEFAULTS
.minFilter
= "nearest";
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);
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
));
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;
36 function waveshift (period
, peak
, xoffset
, yoffset
) {
37 period
/= 2 * Math
.PI
;
38 xoffset
= xoffset
|| 0;
39 yoffset
= yoffset
|| 0;
42 return yoffset
+ peak
* f
.call(this, (p
+ xoffset
) / period
);
47 function cycler (scale
) {
48 var f
= waveshift(scale
, 0.5, -Date
.now(), 0.5)(triangle
);
49 return function () { return f(Date
.now()); };
53 storage
= ystorage
.getStorage();
54 yuu
.audio
.storage
= storage
;
56 NOISY_BLOCKS
= new yuu
.ShaderProgram(null, ["@noise.glsl", "@noisyblocks"]);
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]);
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 },
80 envelope
: { "0": 1, "0.7": 0.2, "3": 0 },
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());
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
)))
104 yuu
.director
.start();
107 window
.addEventListener("load", function() {
108 yuu
.registerInitHook(load
);
109 yuu
.init({ backgroundColor
: [0, 0, 0, 1], antialias
: false }).then(start
);
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 ]];
120 { name
: "12345654321",
121 randomSlammer
: [3, 5],
122 deps
: "50040@easy 53535@easy 44404@easy 1302@easy 12321@easy 20124@easy"
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" },
155 function levelName (level
) {
156 return (level
.name
|| level
.slammer
.join("")).trim();
159 function wonLevel (level
, difficulty
) {
161 storage
.setFlag(level
.sets
);
162 storage
.setFlag(levelName(level
) + "@" + difficulty
);
165 function hasBeaten (level
, difficulty
) {
166 return storage
.getFlag(levelName(level
) + "@" + difficulty
);
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;
177 function difficultyForLevel (level
) {
178 if (level
.deps
&& !level
.deps
.split(" ").every(storage
.getFlag
, storage
))
180 if (hasBeaten(level
, "hard"))
182 if (hasBeaten(level
, "easy"))
188 function levelRandom (level
, difficulty
) {
189 if (difficulty
=== "random")
192 return new yuu
.Random(yuu
.createLCG(+level
.slammer
.join("")));
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
);
201 yuu
.transpose2d(board
);
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
]);
214 var AnimationQueue
= yT(yuu
.C
, {
215 constructor: function () {
219 attached: function () {
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)));
230 _complete: function () {
231 var next
= this._queue
.shift();
236 enqueue: function (timeline
, params
) {
237 return new Promise(function (resolve
) {
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)
251 SLOTS
: ["animationQueue"]
254 var SLAMMER_ROTATE
= {
255 0: { tween1
: { yaw
: "yaw" },
256 playSound
: "@clicking",
261 0: { tweenAll
: { yaw
: "yaws" }, duration
: 10 }
264 var SLAMMER_BOUNCE
= {
265 0: { tween1
: { y
: 0.5 }, duration
: 5, repeat
: -1 }
269 0: { tweenAll
: { position
: "positions" },
270 duration
: 8, easing
: "linear" },
274 0: { tween1
: { y
: -1.5 }, easing
: "linear",
277 6: { event
: "slideBoardBlocks" },
279 tween1
: { y
: 0 }, easing
: "linear", duration
: 8 }
283 0: { tween1
: { yaw
: 2 * Math
.PI
, x
: "x", y
: "y", scale
: [0.3, 0.3, 1] },
287 var GRID_FINISHED
= {
288 0: { tween
: { arm
: { scale
: [0, 0, 1], yaw
: "armYaw", y
: "armY" },
289 board
: { y
: "boardY" } },
293 function rotateCw (d
) { return (--d
+ 4) % 4; }
294 function rotateCcw (d
) { return ++d
% 4; }
295 function opposite (d
) { return (d
+ 2) % 4; }
298 /** Manage a set of semaphore-like counting flags. */
300 constructor: function () {
301 /** Construct a flag set for the provided flags.
303 Flags are initialized to 0 by default.
306 for (var i
= 0; i
< arguments
.length
; ++i
)
307 this._counts
[arguments
[i
]] = 0;
310 increment: function () {
311 /** Increment the provided flags. */
312 for (var i
= 0; i
< arguments
.length
; ++i
)
313 this._counts
[arguments
[i
]]++;
316 decrement: function () {
317 /** Decrement the provided flags.
319 No underflow checks are performed. A flag with a negative
320 value is considered set exactly as a flag with a positive
323 for (var i
= 0; i
< arguments
.length
; ++i
)
324 this._counts
[arguments
[i
]]--;
328 /** Return true if any of the provided flags are set. */
329 return yf
.some
.call(this._counts
, yf
.getter
, arguments
);
333 /** Return true if all of the provided flags are set. */
334 return yf
.every
.call(this._counts
, yf
.getter
, arguments
);
338 /** Return true if none of the provided flags are set. */
339 return !this.some
.apply(this, arguments
);
342 incrementer: function () {
343 /** Provide a bound 0-ary function to increment the provided flags.
345 Useful for wrapps around context-free callbacks.
347 var that
= this, args
= arguments
;
348 return function () { that
.increment
.apply(that
, args
); };
351 decrementer: function () {
352 /** Provide a bound 0-ary function to decrement the provided flags.
354 Useful for wrapps around context-free callbacks.
356 var that
= this, args
= arguments
;
357 return function () { that
.decrement
.apply(that
, args
); };
361 var BoardController
= yT(yuu
.C
, {
362 constructor: function (rnd
, level
, colors
) {
363 this.contents
= generateBoard(rnd
, level
.slammer
);
364 this.colors
= colors
;
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];
374 isComplete: function() {
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
];
387 function (x
, replacement
) {
388 var lost
= this.contents
[x
].pop();
389 this.contents
[x
].unshift(replacement
);
392 function (y
, replacement
) {
393 yuu
.transpose2d(this.contents
);
394 var lost
= this.shift
[BOTTOM
].call(this, y
, replacement
);
395 yuu
.transpose2d(this.contents
);
398 function (x
, replacement
) {
399 var lost
= this.contents
[x
].shift();
400 this.contents
[x
].push(replacement
);
403 function (y
, replacement
) {
404 yuu
.transpose2d(this.contents
);
405 var lost
= this.shift
[TOP
].call(this, y
, replacement
);
406 yuu
.transpose2d(this.contents
);
411 SLOTS
: ["controller"]
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
= [];
421 isComplete: function() {
422 return yf
.none(yf
.some
.bind(null, null), this.blocks
);
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];
433 lastUndoRecord
: { get: function () {
434 return yf
.last(this._undoRecord
);
437 clearUndoRecord: function () {
438 this._undoRecord
= [];
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
)
448 }, this.blocks
, (this.orientation
& 2)
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();
457 this._undoRecord
.push(this.orientation
);
459 this._undoRecord
.pop();
462 SLOTS
: ["controller"]
465 function randSide (rnd
, except
) {
466 return (rnd
|| yuu
.random
).choice(
467 yf
.without([TOP
, LEFT
, BOTTOM
, RIGHT
], except
));
471 0: { tween
: { left
: { yaw
: -0.3 } }, duration
: 3 },
472 3: { tween
: { left
: { yaw
: 0.0 } }, duration
: 7 },
476 0: { tween
: { right
: { yaw
: -0.3 } }, duration
: 3 },
477 3: { tween
: { right
: { yaw
: 0.0 } }, duration
: 7 },
481 0: { tween
: { left
: { yaw
: 0.2 }, right
: { yaw
: 0.2 } },
483 3: { tween
: { left
: { yaw
: 0.0 }, right
: { yaw
: 0.0 } },
487 var HANDS_MENU_CHOICE
= {
488 0: { tween
: { left
: { x
: -1.3 },
489 right
: { x
: -1.3 } },
490 duration
: 15, easing
: "ease_in"
493 10: { tween
: { left
: { scaleX
: 1 },
494 right
: { scaleX
: 1 } },
497 20: { set: { leftQuad
: { color
: "frontColor" },
498 rightQuad
: { color
: "frontColor" } },
499 tween
: { left
: { x
: 0 }, right
: { x
: 0 } },
505 0: { tween
: { left
: { x
: -1.3 },
506 right
: { x
: -1.3 } },
510 10: { tween
: { left
: { scaleX
: -1 },
511 right
: { scaleX
: -1 } },
514 20: { set: { leftQuad
: { color
: "backColor" },
515 rightQuad
: { color
: "backColor" } },
516 tween
: { left
: { x
: -1 }, right
: { x
: -1 } },
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 },
529 { 0: { tween
: { left
: { scaleX
: 0.8, x
: 0.1 },
530 right
: { scaleX
: 0.9 },
531 }, duration
: 10, repeat
: -1 },
535 { 0: { tween
: { left
: { yaw
: 0.2, scaleX
: 0.8 },
536 right
: { yaw
: 0.2, scaleX
: 0.8 },
537 }, duration
: 10, repeat
: -1 },
541 { 0: { tween
: { left
: { scaleX
: 0.9 },
542 right
: { scaleX
: 0.8, x
: 0.1 },
543 }, duration
: 10, repeat
: -1 },
547 var HANDS_ROTATE_CW
= {
548 0: { tween
: { left
: { scaleX
: 0.8 } }, duration
: 5 },
549 5: { tween
: { left
: { scaleX
: 1.0 } }, duration
: 5 },
552 var HANDS_ROTATE_CCW
= {
553 0: { tween
: { right
: { scaleX
: 0.8 } }, duration
: 5 },
554 5: { tween
: { right
: { scaleX
: 1 } }, duration
: 5 },
558 0: { tween
: { a
: { x
: 0 }, b
: { x
: 1.5 } }, duration
: 25 }
562 0: { tween
: { a
: { x
: -1.5 }, b
: { x
: 0 } }, duration
: 25 }
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());
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
);
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),
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);
607 this.frontColor
= yuu
.hslToRgb(hsl
);
608 this.leftQuad
.color
= this.rightQuad
.color
= this.frontColor
;
609 this.ready
= hands
.ready
;
611 function Button (i
, command
) {
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
]));
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
629 this.entity0
.addChildren(this.leftButtons
, this.rightButton
);
630 this.buttons
= [this.helpButton
, this.backButton
, this.rightButton
,
635 resize: function () {
636 var base
= new yuu
.AABB(-0.75, 0, 0.75, 1.5);
637 var vp
= base
.matchAspectRatio(yuu
.viewport
);
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
);
649 mousemove: function (p
) {
650 p
= this.layer0
.worldFromDevice(p
);
652 for (var i
= 0; i
< this.buttons
.length
; ++i
) {
653 if (this.buttons
[i
].transform
.contains(p
)) {
654 this.cursor
= "pointer";
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
);
669 doubletap: function () {
670 return this.inputs
.tap
.apply(this, arguments
);
674 _anim: function (timeline
) {
675 this.entity0
.attach(new yuu
.Animation(
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
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(
695 a
: this.backButton
.transform
,
696 b
: this.helpButton
.transform
698 this._anim(HANDS_MENU_CHOICE
);
700 finished: function () {
701 this.entity0
.attach(new yuu
.Animation(
703 a
: this.backButton
.transform
,
704 b
: this.helpButton
.transform
706 this._anim(HANDS_RETURN
);
712 tween1
: { y
: 0 }, duration
: 10 },
715 GridScene
= yT(yuu
.Scene
, {
716 constructor: function (level
, difficulty
) {
717 yuu
.Scene
.call(this);
718 this.entity0
.attach(new yuu
.Transform());
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
),
727 new yuu
.DataC({ quads
: [] }));
728 this.slammer
= new yuu
.E(new SlammerController(rnd
, level
, colors
),
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
});
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
});
762 this.addEntities(this.board
, this.slammerRoot
);
764 if (!(this.cheating
= yuu
.director
.input
.pressed
["`"])) {
765 this.slammer
.controller
.clearUndoRecord();
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
);
781 this._locks
.increment("slam");
782 this.entity0
.attach(new yuu
.Animation(
783 GRID_APPEAR
, { $: this.slammer
.transform
},
784 this._locks
.decrementer("slam")));
787 scramble: function (rnd
) {
788 var scramble
= (this.level
.scramble
|| {})[this.difficulty
];
789 var slammerCon
= this.slammer
.controller
;
790 var boardCon
= this.board
.controller
;
792 var count
= scrambleForLevel(rnd
, this.level
, this.difficulty
);
793 while (this.isComplete()) {
796 slammerCon
.orientation
= randSide(rnd
, slammerCon
.orientation
);
797 slammerCon
.slam(boardCon
);
801 for (var i
=0; i
< scramble
.length
; ++i
) {
802 slammerCon
.orientation
= +scramble
[i
];
803 slammerCon
.slam(boardCon
);
806 slammerCon
.orientation
= randSide();
807 this.slammerRoot
.transform
.yaw
= slammerCon
.orientation
* Math
.PI
/ 2;
810 isComplete: function () {
811 return this.slammer
.controller
.isComplete()
812 && this.board
.controller
.isComplete();
815 rotateTo
: yuu
.cmd(function (orientation
) {
816 return new Promise(function (resolve
) {
817 if (this._locks
.some("spin"))
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;
824 this.entity0
.attach(new yuu
.Animation(
826 $: this.slammerRoot
.transform
,
827 yaw
: yaw0
+ yuu
.normalizeRadians(yaw1
- yaw0
)
829 this._locks
.decrement("slam");
833 }, "<top/bottom/left/right>", "move the slammer to the top"),
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"),
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"),
847 left
: yuu
.cmd(function () { this.rotateCw(); }),
848 right
: yuu
.cmd(function () { this.rotateCcw(); }),
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();
857 this.rotateTo(con
.lastUndoRecord
)
858 .then(this.slam
.bind(this))
864 }, "", "rotate the active piece counter-clockwise"),
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
);
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(
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
883 yuu
.director
.removeScene(this);
889 slideBoardBlocks: function (anim
, params
) {
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;
898 var sgnx
= Math
.sign(dx
);
899 var sgny
= Math
.sign(dy
);
902 var blocks
= this.slammer
.controller
.blocks
;
903 this.slammer
.data
.quads
.forEach(function (q
) {
904 var d
= blocks
[q
.x
].length
;
906 positions
.push([q
.quad
.position
[0], q
.quad
.position
[1] - d
]);
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);
913 positions
.push([q
.quad
.position
[0] + sgnx
* blocks
[y
].length
,
914 q
.quad
.position
[1] + sgny
* blocks
[x
].length
]);
916 this.entity0
.attach(new yuu
.Animation(SLIDE_BLOCKS
, {
922 slam
: yuu
.cmd(function () {
923 var r
= new Promise(function (resolve
, reject
) {
924 if (this._locks
.some("slam")) {
925 reject("slamming is locked");
928 this._locks
.increment("spin", "slam");
930 handScene
.slam(this.slammer
.controller
.orientation
);
931 this.entity0
.attach(new yuu
.Animation(
933 $: this.slammer
.transform
,
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
;
940 slideBoardBlocks
: this.slideBoardBlocks
.bind(this)
943 this._locks
.decrement("slam");
948 }, "", "slam the active piece"),
950 back
: yuu
.cmd(function (x
, y
) {
951 if (this._locks
.some("quit"))
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(
962 $: this.entity0
.transform
,
965 yuu
.director
.removeScene(this);
969 }, "", "go back to the menu"),
971 slammerBB
: { get: function (p
) {
972 var length
= this.level
.slammer
.length
;
973 switch (this.slammer
.controller
.orientation
) {
975 return new yuu
.AABB(-Infinity
, -0.5, -1, length
- 0.5);
977 return new yuu
.AABB(length
+ 1, -0.5, Infinity
, length
- 0.5);
979 return new yuu
.AABB(-0.5, length
+ 1, length
- 0.5, Infinity
);
981 return new yuu
.AABB(-0.5, -Infinity
, length
- 0.5, -1);
985 _swipe: function (p0
, p1
) {
986 p0
= this.layer0
.worldFromDevice(p0
);
987 p1
= this.layer0
.worldFromDevice(p1
);
988 if (this.slammerBB
.contains(p0
)) {
992 if (this.gridBB
.contains(p0
) && !this.gridBB
.contains(p1
)) {
993 this.back(p1
.x
- p0
.x
, p1
.y
- p0
.y
);
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
);
1008 p
= this.layer0
.worldFromDevice(p
);
1009 if (this.gridBB
.contains(p
)) {
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();
1026 } else if (this.rightBB
.contains(p
)) {
1027 this.rotateTo(RIGHT
);
1028 handScene
.rotatedCcw();
1030 } else if (this.topBB
.contains(p
)) {
1033 handScene
.rotatedCw();
1035 handScene
.rotatedCcw();
1037 } else if (this.bottomBB
.contains(p
)) {
1038 this.rotateTo(BOTTOM
);
1040 handScene
.rotatedCw();
1042 handScene
.rotatedCcw();
1047 doubletap: function () {
1048 return this.inputs
.tap
.apply(this, arguments
);
1051 hold: function (p
) {
1052 p
= this.layer0
.worldFromDevice(p
);
1053 if (this.gridBB
.contains(p
)) {
1059 dragstart: function (p
) {
1060 p
= this.layer0
.worldFromDevice(p
);
1061 this._dragging
= this.slammerBB
.contains(p
);
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
;
1070 if (this._dragging
=== true && inGrid
) {
1072 } else if (p
.x
> 0 && p
.x
< length
&& !inGrid
) {
1073 o
= p
.y
< 0 ? BOTTOM
: TOP
;
1074 if (o
!== this.slammer
.controller
.orientation
) {
1078 } else if (p
.y
> 0 && p
.y
< length
&& !inGrid
) {
1079 o
= p
.x
< 0 ? LEFT
: RIGHT
;
1080 if (o
!== this.slammer
.controller
.orientation
) {
1086 return this._dragging
;
1089 dragend: function (p0
, p1
) {
1090 this._dragging
= false;
1093 release: function () {
1097 swipeleft: function (p0
, p1
) {
1098 return this._swipe(p0
, p1
);
1100 swiperight: function (p0
, p1
) {
1101 return this._swipe(p0
, p1
);
1103 swipeup: function (p0
, p1
) {
1104 return this._swipe(p0
, p1
);
1106 swipedown: function (p0
, p1
) {
1107 return this._swipe(p0
, p1
);
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",
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)}],
1147 0: { tween1
: { x
: "x" }, duration
: "duration" }
1151 0: { tween1
: { luminance
: 1, alpha
: 1 }, duration
: 32, repeat
: -1 }
1155 0: { tween
: { cursor
: { y
: "mid" } },
1156 playSound
: "@winding",
1158 8: { tween
: { cursor
: { y
: "line" },
1161 20: { tween
: { scene
: { y
: 10 },
1162 select
: { y
: -11.5, scale
: [3, 3, 1] }
1164 38: { event
: "appear",
1165 tween
: { select
: { y
: 0, scale
: [1, 0, 1] } },
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);
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);
1183 function generateQuads() {
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
];
1195 ce
.data
.flasher
= batch
.createQuad();
1196 ce
.data
.flasher
.alpha
= 0;
1200 if (level
.randomSlammer
)
1203 // 14 = maximum slammer size + 1 background + 1 flasher
1204 var batch
= new yuu
.QuadBatchC(14);
1206 new yuu
.Transform([2 * i
, 0, 0]),
1209 activate: function () {
1211 var scene
= new GridScene(level
, difficultyForLevel(level
));
1212 yuu
.director
.insertUnderScene(scene
);
1217 if (level
.randomSlammer
) {
1218 ce
.attach(new yuu
.Ticker(function () {
1219 if (!activated
&& yuu
.random
.randbool(0.7)) {
1232 var HAND_TICK_BACK
= {
1233 0: { tween1
: { rotation
: "rotation" }, duration
: 6, repeat
: -1 }
1236 MenuScene
= yT(yuu
.Scene
, {
1237 constructor: function (initialLevel
) {
1238 yuu
.Scene
.call(this);
1239 this.entity0
.attach(new yuu
.Transform(),
1240 new AnimationQueue());
1242 this.pointer
= new yuu
.E(
1243 new yuu
.Transform([5, 8, 0]),
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
);
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;
1260 this.entity0
.attach(
1261 new yuu
.Ticker(this._animation
.bind(this), 60));
1264 _animation: function (count
) {
1265 var length
= this.availableLevels
.length
;
1266 var range
= Math
.pow(2, length
);
1267 var rand
= yuu
.random
.randrange(range
);
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
))) {
1276 ? yuu
.random
.randsign(Math
.PI
/ 2)
1278 targets
.push(child
.transform
);
1279 yaws
.push(child
.transform
.yaw
+ dyaw
);
1282 if (targets
.length
) {
1283 this.entity0
.attach(new yuu
.Animation(
1284 ROTATE_ALL
, { $s
: targets
, yaws
: yaws
}));
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
);
1295 circleScene
.toBottom();
1296 handScene
.finished();
1297 this._locks
.increment("slam", "move");
1298 this.entity0
.animationQueue
.enqueue(
1300 { $: this.entity0
.transform
})
1301 .then(this._locks
.decrementer("slam", "move"));
1304 didWinLevel: function (level
, difficulty
, firstTime
) {
1305 var idx
= this.availableLevels
.indexOf(level
);
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"));
1321 changeActiveIndex: function (index
, animate
) {
1322 var oldIndex
= this.activeIndex
;
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(
1331 $: this.menu
.transform
,
1335 p
.then(this._locks
.decrementer("slam"));
1337 return p
|| Promise
.resolve();
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);
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);
1354 }, "move the cursor right"),
1356 slam
: yuu
.cmd(function () {
1357 if (this._locks
.some("slam"))
1359 var activeChild
= this.menu
.children
[this.activeIndex
];
1360 this._locks
.increment("slam", "move");
1361 handScene
.menuChoice();
1362 circleScene
.toBack();
1364 this.entity0
.animationQueue
.enqueue(
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);
1376 }, "choose the active menu item"),
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
);
1385 pinchout: function (p0
, p1
) {
1386 p0
= this.layer0
.worldFromDevice(p0
);
1387 p1
= this.layer0
.worldFromDevice(p1
);
1388 if (vec2
.sqrDist(p0
, p1
) > 1) {
1394 hold: function (p
) {
1395 return this.inputs
.dragstart
.call(this, p
);
1398 release: function (p
) {
1399 if (this._dragStartX
!== null)
1400 return this.inputs
.dragend
.call(this, p
);
1403 dragstart: function (p
) {
1404 if (this._locks
.some("move"))
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
;
1415 dragdown: function (p0
, p1
) {
1416 p0
= this.layer0
.worldFromDevice(p0
);
1417 p1
= this.layer0
.worldFromDevice(p1
);
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) {
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
);
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);
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();
1462 sounds
.switchBroke
.play();
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) {
1477 resetEverything
: yuu
.cmd(function () {
1479 yuu
.director
.stop();
1481 }, "reset all saved data"),
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"),
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",
1514 0: { set1
: { y
: 1.5, x
: -1.5 },
1515 tween
: { bgQuad
: { alpha
: 0.75 }, $: { y
: 0, x
: 0 }, },
1516 playSound
: "@book-appear",
1520 var BOOK_DISMISS
= {
1521 0: { tween
: { bgQuad
: { alpha
: 0 }, $: { y
: 1.5, x
: -1.5, } },
1522 playSound
: "@book-dismiss",
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];
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"]
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 },
1542 }, duration
: 15, easing
: "linear" },
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"]
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] },
1554 page2
: { x
: 0, scaleX
: -2/3 },
1555 }, duration
: 15, easing
: "linear" },
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"]
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 },
1572 }, duration
: 15, easing
: "linear" },
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"]
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" },
1591 BookScene
= new yT(yuu
.Scene
, {
1592 constructor: function () {
1593 yuu
.Scene
.call(this);
1595 new yuu
.Transform().setScale([20, 20, 1]),
1596 this.bgQuad
= new yuu
.QuadC()
1597 .setColor([0, 0, 0, 0])
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());
1610 this._locks
= new FlagSet("turn");
1611 this.addEntities(bg
, this.page1
, this.page2
);
1613 this.ready
= yuu
.ready(yf
.map(yf
.new_(yuu
.Instrument
), [
1614 "@page-turn-1", "@page-turn-2", "@page-turn-3",
1619 help
: yuu
.cmd(function () {
1621 }, "dismiss the help screen"),
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
);
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
;
1638 parent
.replaceChild(p
, spinner
);
1640 }, "why would you ever want to run this?"),
1643 this._anim(BOOK_APPEAR
);
1644 storage
.setFlag("instructions");
1647 _anim: function (anim
) {
1648 this._locks
.increment("turn");
1649 var completion
= this._locks
.decrementer("turn");
1652 completion
= yuu
.director
.removeScene
.bind(yuu
.director
, this);
1656 var device
= yuu
.director
.preferredDevice();
1657 this.entity0
.attach(new yuu
.Animation(
1659 $: this.entity0
.transform
,
1660 page
: device
=== "keyboard" ? KEYBOARD_PAGE
1661 : device
=== "gamepad" ? GAMEPAD_PAGE
1663 page1
: this.page1
.transform
,
1664 page2
: this.page2
.transform
,
1665 page1Quad
: this.page1Quad
,
1666 page2Quad
: this.page2Quad
,
1671 advance
: yuu
.cmd(function () {
1672 if (this._locks
.some("turn"))
1674 this._anim(BOOK_FORWARD
[this.current
++]);
1677 skip
: yuu
.cmd(function () {
1678 if (this._locks
.some("turn"))
1680 this._anim(BOOK_DISMISS
);
1683 back
: yuu
.cmd(function () {
1684 if (this._locks
.some("turn"))
1686 if (this.current
> 0)
1687 this._anim(BOOK_BACKWARD
[--this.current
]);
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),
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
);
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) {
1710 this.cursor
= "W-resize";
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) {
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; },
1735 consume
: yuu
.Director
.prototype.GESTURES
1736 .concat(yuu
.Director
.prototype.CANVAS_EVENTS
)
1748 gamepadbutton0
: "advance",
1749 gamepadbutton1
: "skip",
1750 gamepadbutton4
: "back",
1751 gamepadbutton5
: "advance",
1752 gamepadbutton8
: "skip",
1753 gamepadbutton9
: "skip",
1754 gamepadbutton14
: "back",
1755 gamepadbutton15
: "advance",
1759 var OUTER_FLIP_TICK
= {
1760 0: { tween1
: { yaw
: "yaw" }, duration
: 15 }
1763 var CIRCLE_TO_BOTTOM
= {
1764 0: { tween1
: { pitch
: Math
.PI
* 0.35, y
: -0.3 }, duration
: 35 }
1767 var CIRCLE_TO_BACK
= {
1768 0: { tween1
: { pitch
: Math
.PI
* 0.15, y
: -0.1 }, duration
: 35 }
1771 var CIRCLE_INNER_RATCHET
= {
1772 0: { tween1
: { rotation
: "rotation1" }, duration
: 15,
1773 playSound
: "@regear"
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 }
1781 var CIRCLE_INNER_WIND
= {
1782 0: { tween1
: { rotation
: "rotation1" }, duration
: 8 },
1783 15: { tween1
: { rotation
: "rotation2" }, duration
: 20 },
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
}]
1796 0: { tween1
: { rotation
: "rotation" }, duration
: 6 }
1800 // Nearly all derived from
1801 // http://www.clockguy.com/SiteRelated/SiteReferencePages/ClockChimeTunes.html
1803 // All transposition & transcription errors are mine.
1805 { name
: "Westminster",
1814 { name
: "Wittington",
1815 keys
: ["Eb4", "E4"],
1816 bars
: ["1 2 3 5 4 6 7 0",
1830 { name
: "Canterbury",
1832 bars
: ["2 0 5 3 1 4",
1843 bars
: ["5 4 3 2 1 0",
1851 { name: "St. Michael's",
1853 bars: ["7 6 5 4 3 2 1 0",
1861 { name
: "Winchester",
1863 bars
: ["5 3 1 0 2 4",
1872 function third (s
) {
1873 return "Q " + s
.replace(/([^ ]+ [^ ]+)/g, "$1 Z");
1876 function silence (s
) {
1877 return "Q " + s
.replace(/[^ ]+/g, "Z");
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.",
1884 var TIMES3
= ["Q", "Q", silence
, silence
,
1885 third
, third
, third
, third
,
1889 function deck (pack
, random
) {
1890 random
= random
|| yuu
.random
;
1892 return function () {
1893 if (stock
.length
=== 0)
1894 stock
= random
.shuffle(pack
.slice());
1899 function generateScore () {
1900 var chimes
= yuu
.random
.choice(CHIMES
);
1901 var bar
= deck(chimes
.bars
);
1903 return yf
.isFunction(t
) ? t(bar()) : t
+ " " + bar();
1906 function line (times
) {
1907 return yf
.map(draw
, yuu
.random
.shuffle(times
)).join(" ");
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
);
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"))
1930 .setSize([0.35417, 0.35417]));
1931 arm
.addChild(this.outer
);
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));
1942 var NOISY_QUADS
= new yuu
.ShaderProgram(
1943 ["@noise.glsl", "@noisyquads"], ["yuu/@color"]);
1945 var bgMat
= new yuu
.Material(
1946 yuu
.Texture
.DEFAULT
, NOISY_QUADS
, { range
: 0.8 });
1947 bgMat
.uniforms
.cut
= yf
.volatile(cycler(100000));
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);
1963 this.entity0
.addChild(bg
);
1964 this.entity0
.attach(new yuu
.Animation(
1965 BACKGROUND_DRIFT
, { $: bg
.transform
}));
1967 this.ground
= new yuu
.E(new yuu
.Transform());
1968 this.ground
.addChildren(rim
, inner
, arm
);
1969 this.entity0
.addChild(this.ground
);
1971 this.music
= yuu
.audio
.createGain();
1972 this.music
.gain
.value
= 0.3;
1973 this.music
.connect(yuu
.audio
.music
);
1974 this._finished
= false;
1976 this.ready
= yuu
.ready([
1977 this.outerQuad
.material
,
1978 this.innerQuad
.material
,
1979 this.rimQuad
.material
,
1984 help
: yuu
.cmd(function () {
1985 yuu
.director
.pushScene(new BookScene());
1986 }, "bring up the help screen"),
1988 yuu
: yuu
.cmd(function () {
1989 this.outerQuad
.material
= new yuu
.Material("@circle-outer-ee");
1995 gamepadbutton6
: "help",
1996 f10
: "showOverlay preferences",
1997 "shift+y+u+`": "yuu",
1998 "gamepadbutton10+gamepadbutton11": "yuu",
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
);
2009 toBottom: function () {
2010 this.entity0
.attach(
2011 new yuu
.Animation(CIRCLE_TO_BOTTOM
, { $: this.ground
.transform
}));
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
,
2029 score
.key
= this.score
&& this.score
.key
;
2033 _musicSchedule: function (count
) {
2034 var t
= yuu
.director
.currentAudioTime
;
2037 if (this._finished
) {
2038 if (this._finished
=== "won" && this.score
.key
) {
2039 var score
= yuu
.parseScore(
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",
2045 yuu
.Scales
.MAJOR
, this.score
.key
);
2046 while ((note
= score
.shift())) {
2047 sounds
.chime
.createSound(
2052 ).connect(this.music
);
2055 this._finished
= false;
2059 if (!(this.score
&& this.score
.length
)) {
2060 this.score
= generateScore();
2065 while (this.score
.length
&& this.score
[0].time
< this.playing
) {
2066 note
= this.score
.shift();
2067 sounds
.chime
.createSound(
2069 t
+ note
.time
% 1 + yuu
.random
.gauss(0, 0.015),
2072 ).connect(this.music
);
2075 if ((this.tension
*= 0.95) > 1) {
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
,
2086 [sounds
.tick
, sounds
.tock
][count
& 1]
2087 .createSound(yuu
.audio
, t
, 0, 0.5, 1.0)
2088 .connect(this.music
);
2091 this.clockTick(this.reversed
-- > 0 ? TICK_REV
: TICK_ROT
);
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(
2101 { $: this.arm
.transform
, rotation
: rot
}));
2104 toBack: function () {
2106 this.entity0
.attach(
2107 new yuu
.Animation(CIRCLE_TO_BACK
, { $: this.ground
.transform
}));
2111 new yuu
.Ticker(this._musicSchedule
.bind(this), 60));
2115 this._finished
= "won";
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)
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
,
2137 rotated: function () {
2138 this.tension
+= yuu
.random
.uniform(0.1);
2142 this.tension
+= yuu
.random
.uniform(0.2);
2145 reverse: function () {
2146 this.tension
-= yuu
.random
.uniform(0.1);
2147 this.reversed
= Math
.max(this.reversed
, 0) + 1;