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 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 },
81 envelope
: { "0": 1, "0.7": 0.2, "3": 0 },
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());
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
)))
105 yuu
.director
.start();
108 window
.addEventListener("load", function() {
109 yuu
.registerInitHook(load
);
110 yuu
.init({ backgroundColor
: [0, 0, 0, 1], antialias
: false }).then(start
);
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 ]];
121 { name
: "12345654321",
122 randomSlammer
: [3, 5],
123 deps
: "50040@easy 53535@easy 44404@easy 1302@easy 12321@easy 20124@easy"
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" },
156 function levelName (level
) {
157 return (level
.name
|| level
.slammer
.join("")).trim();
160 function wonLevel (level
, difficulty
) {
162 storage
.setFlag(level
.sets
);
163 storage
.setFlag(levelName(level
) + "@" + difficulty
);
166 function hasBeaten (level
, difficulty
) {
167 return storage
.getFlag(levelName(level
) + "@" + difficulty
);
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;
178 function difficultyForLevel (level
) {
179 if (level
.deps
&& !level
.deps
.split(" ").every(storage
.getFlag
, storage
))
181 if (hasBeaten(level
, "hard"))
183 if (hasBeaten(level
, "easy"))
189 function levelRandom (level
, difficulty
) {
190 if (difficulty
=== "random")
193 return new yuu
.Random(yuu
.createLCG(+level
.slammer
.join("")));
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
);
202 yuu
.transpose2d(board
);
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
]);
215 var AnimationQueue
= yT(yuu
.C
, {
216 constructor: function () {
220 attached: function () {
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)));
231 _complete: function () {
232 var next
= this._queue
.shift();
237 enqueue: function (timeline
, params
) {
238 return new Promise(function (resolve
) {
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)
252 SLOTS
: ["animationQueue"]
255 var SLAMMER_ROTATE
= {
256 0: { tween1
: { yaw
: "yaw" },
257 playSound
: "@clicking",
262 0: { tweenAll
: { yaw
: "yaws" }, duration
: 10 }
265 var SLAMMER_BOUNCE
= {
266 0: { tween1
: { y
: 0.5 }, duration
: 5, repeat
: -1 }
270 0: { tweenAll
: { position
: "positions" },
271 duration
: 8, easing
: "linear" },
275 0: { tween1
: { y
: -1.5 }, easing
: "linear",
278 6: { event
: "slideBoardBlocks" },
280 tween1
: { y
: 0 }, easing
: "linear", duration
: 8 }
284 0: { tween1
: { yaw
: 2 * Math
.PI
, x
: "x", y
: "y", scale
: [0.3, 0.3, 1] },
288 var GRID_FINISHED
= {
289 0: { tween
: { arm
: { scale
: [0, 0, 1], yaw
: "armYaw", y
: "armY" },
290 board
: { y
: "boardY" } },
294 function rotateCw (d
) { return (--d
+ 4) % 4; }
295 function rotateCcw (d
) { return ++d
% 4; }
296 function opposite (d
) { return (d
+ 2) % 4; }
299 /** Manage a set of semaphore-like counting flags. */
301 constructor: function () {
302 /** Construct a flag set for the provided flags.
304 Flags are initialized to 0 by default.
307 for (var i
= 0; i
< arguments
.length
; ++i
)
308 this._counts
[arguments
[i
]] = 0;
311 increment: function () {
312 /** Increment the provided flags. */
313 for (var i
= 0; i
< arguments
.length
; ++i
)
314 this._counts
[arguments
[i
]]++;
317 decrement: function () {
318 /** Decrement the provided flags.
320 No underflow checks are performed. A flag with a negative
321 value is considered set exactly as a flag with a positive
324 for (var i
= 0; i
< arguments
.length
; ++i
)
325 this._counts
[arguments
[i
]]--;
329 /** Return true if any of the provided flags are set. */
330 return yf
.some
.call(this._counts
, yf
.getter
, arguments
);
334 /** Return true if all of the provided flags are set. */
335 return yf
.every
.call(this._counts
, yf
.getter
, arguments
);
339 /** Return true if none of the provided flags are set. */
340 return !this.some
.apply(this, arguments
);
343 incrementer: function () {
344 /** Provide a bound 0-ary function to increment the provided flags.
346 Useful for wrapps around context-free callbacks.
348 var that
= this, args
= arguments
;
349 return function () { that
.increment
.apply(that
, args
); };
352 decrementer: function () {
353 /** Provide a bound 0-ary function to decrement the provided flags.
355 Useful for wrapps around context-free callbacks.
357 var that
= this, args
= arguments
;
358 return function () { that
.decrement
.apply(that
, args
); };
362 var BoardController
= yT(yuu
.C
, {
363 constructor: function (rnd
, level
, colors
) {
364 this.contents
= generateBoard(rnd
, level
.slammer
);
365 this.colors
= colors
;
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];
375 isComplete: function() {
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
];
388 function (x
, replacement
) {
389 var lost
= this.contents
[x
].pop();
390 this.contents
[x
].unshift(replacement
);
393 function (y
, replacement
) {
394 yuu
.transpose2d(this.contents
);
395 var lost
= this.shift
[BOTTOM
].call(this, y
, replacement
);
396 yuu
.transpose2d(this.contents
);
399 function (x
, replacement
) {
400 var lost
= this.contents
[x
].shift();
401 this.contents
[x
].push(replacement
);
404 function (y
, replacement
) {
405 yuu
.transpose2d(this.contents
);
406 var lost
= this.shift
[TOP
].call(this, y
, replacement
);
407 yuu
.transpose2d(this.contents
);
412 SLOTS
: ["controller"]
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
= [];
422 isComplete: function() {
423 return yf
.none(yf
.some
.bind(null, null), this.blocks
);
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];
434 lastUndoRecord
: { get: function () {
435 return yf
.last(this._undoRecord
);
438 clearUndoRecord: function () {
439 this._undoRecord
= [];
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
)
449 }, this.blocks
, (this.orientation
& 2)
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();
458 this._undoRecord
.push(this.orientation
);
460 this._undoRecord
.pop();
463 SLOTS
: ["controller"]
466 function randSide (rnd
, except
) {
467 return (rnd
|| yuu
.random
).choice(
468 yf
.without([TOP
, LEFT
, BOTTOM
, RIGHT
], except
));
472 0: { tween
: { left
: { yaw
: -0.3 } }, duration
: 3 },
473 3: { tween
: { left
: { yaw
: 0.0 } }, duration
: 7 },
477 0: { tween
: { right
: { yaw
: -0.3 } }, duration
: 3 },
478 3: { tween
: { right
: { yaw
: 0.0 } }, duration
: 7 },
482 0: { tween
: { left
: { yaw
: 0.2 }, right
: { yaw
: 0.2 } },
484 3: { tween
: { left
: { yaw
: 0.0 }, right
: { yaw
: 0.0 } },
488 var HANDS_MENU_CHOICE
= {
489 0: { tween
: { left
: { x
: -1.3 },
490 right
: { x
: -1.3 } },
491 duration
: 15, easing
: "ease_in"
494 10: { tween
: { left
: { scaleX
: 1 },
495 right
: { scaleX
: 1 } },
498 20: { set: { leftQuad
: { color
: "frontColor" },
499 rightQuad
: { color
: "frontColor" } },
500 tween
: { left
: { x
: 0 }, right
: { x
: 0 } },
506 0: { tween
: { left
: { x
: -1.3 },
507 right
: { x
: -1.3 } },
511 10: { tween
: { left
: { scaleX
: -1 },
512 right
: { scaleX
: -1 } },
515 20: { set: { leftQuad
: { color
: "backColor" },
516 rightQuad
: { color
: "backColor" } },
517 tween
: { left
: { x
: -1 }, right
: { x
: -1 } },
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 },
530 { 0: { tween
: { left
: { scaleX
: 0.8, x
: 0.1 },
531 right
: { scaleX
: 0.9 },
532 }, duration
: 10, repeat
: -1 },
536 { 0: { tween
: { left
: { yaw
: 0.2, scaleX
: 0.8 },
537 right
: { yaw
: 0.2, scaleX
: 0.8 },
538 }, duration
: 10, repeat
: -1 },
542 { 0: { tween
: { left
: { scaleX
: 0.9 },
543 right
: { scaleX
: 0.8, x
: 0.1 },
544 }, duration
: 10, repeat
: -1 },
548 var HANDS_ROTATE_CW
= {
549 0: { tween
: { left
: { scaleX
: 0.8 } }, duration
: 5 },
550 5: { tween
: { left
: { scaleX
: 1.0 } }, duration
: 5 },
553 var HANDS_ROTATE_CCW
= {
554 0: { tween
: { right
: { scaleX
: 0.8 } }, duration
: 5 },
555 5: { tween
: { right
: { scaleX
: 1 } }, duration
: 5 },
559 0: { tween
: { a
: { x
: 0 }, b
: { x
: 1.5 } }, duration
: 25 }
563 0: { tween
: { a
: { x
: -1.5 }, b
: { x
: 0 } }, duration
: 25 }
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());
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
);
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),
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);
608 this.frontColor
= yuu
.hslToRgb(hsl
);
609 this.leftQuad
.color
= this.rightQuad
.color
= this.frontColor
;
610 this.ready
= hands
.ready
;
612 function Button (i
, command
) {
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
]));
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
630 this.entity0
.addChildren(this.leftButtons
, this.rightButton
);
631 this.buttons
= [this.helpButton
, this.backButton
, this.rightButton
,
636 resize: function () {
637 var base
= new yuu
.AABB(-0.75, 0, 0.75, 1.5);
638 var vp
= base
.matchAspectRatio(yuu
.viewport
);
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
);
650 mousemove: function (p
) {
651 p
= this.layer0
.worldFromDevice(p
);
653 for (var i
= 0; i
< this.buttons
.length
; ++i
) {
654 if (this.buttons
[i
].transform
.contains(p
)) {
655 this.cursor
= "pointer";
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
);
670 doubletap: function () {
671 return this.inputs
.tap
.apply(this, arguments
);
675 _anim: function (timeline
) {
676 this.entity0
.attach(new yuu
.Animation(
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
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(
696 a
: this.backButton
.transform
,
697 b
: this.helpButton
.transform
699 this._anim(HANDS_MENU_CHOICE
);
701 finished: function () {
702 this.entity0
.attach(new yuu
.Animation(
704 a
: this.backButton
.transform
,
705 b
: this.helpButton
.transform
707 this._anim(HANDS_RETURN
);
713 tween1
: { y
: 0 }, duration
: 10 },
716 GridScene
= yT(yuu
.Scene
, {
717 constructor: function (level
, difficulty
) {
718 yuu
.Scene
.call(this);
719 this.entity0
.attach(new yuu
.Transform());
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
),
728 new yuu
.DataC({ quads
: [] }));
729 this.slammer
= new yuu
.E(new SlammerController(rnd
, level
, colors
),
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
});
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
});
763 this.addEntities(this.board
, this.slammerRoot
);
765 if (!(this.cheating
= yuu
.director
.input
.pressed
["`"])) {
766 this.slammer
.controller
.clearUndoRecord();
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
);
782 this._locks
.increment("slam");
783 this.entity0
.attach(new yuu
.Animation(
784 GRID_APPEAR
, { $: this.slammer
.transform
},
785 this._locks
.decrementer("slam")));
788 scramble: function (rnd
) {
789 var scramble
= (this.level
.scramble
|| {})[this.difficulty
];
790 var slammerCon
= this.slammer
.controller
;
791 var boardCon
= this.board
.controller
;
793 var count
= scrambleForLevel(rnd
, this.level
, this.difficulty
);
794 while (this.isComplete()) {
797 slammerCon
.orientation
= randSide(rnd
, slammerCon
.orientation
);
798 slammerCon
.slam(boardCon
);
802 for (var i
=0; i
< scramble
.length
; ++i
) {
803 slammerCon
.orientation
= +scramble
[i
];
804 slammerCon
.slam(boardCon
);
807 slammerCon
.orientation
= randSide();
808 this.slammerRoot
.transform
.yaw
= slammerCon
.orientation
* Math
.PI
/ 2;
811 isComplete: function () {
812 return this.slammer
.controller
.isComplete()
813 && this.board
.controller
.isComplete();
816 rotateTo
: yuu
.cmd(function (orientation
) {
817 return new Promise(function (resolve
) {
818 if (this._locks
.some("spin"))
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;
825 this.entity0
.attach(new yuu
.Animation(
827 $: this.slammerRoot
.transform
,
828 yaw
: yaw0
+ yuu
.normalizeRadians(yaw1
- yaw0
)
830 this._locks
.decrement("slam");
834 }, "<top/bottom/left/right>", "move the slammer to the top"),
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"),
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"),
848 left
: yuu
.cmd(function () { this.rotateCw(); }),
849 right
: yuu
.cmd(function () { this.rotateCcw(); }),
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();
858 this.rotateTo(con
.lastUndoRecord
)
859 .then(this.slam
.bind(this))
865 }, "", "rotate the active piece counter-clockwise"),
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
);
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(
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
884 yuu
.director
.removeScene(this);
890 slideBoardBlocks: function (anim
, params
) {
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;
899 var sgnx
= Math
.sign(dx
);
900 var sgny
= Math
.sign(dy
);
903 var blocks
= this.slammer
.controller
.blocks
;
904 this.slammer
.data
.quads
.forEach(function (q
) {
905 var d
= blocks
[q
.x
].length
;
907 positions
.push([q
.quad
.position
[0], q
.quad
.position
[1] - d
]);
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);
914 positions
.push([q
.quad
.position
[0] + sgnx
* blocks
[y
].length
,
915 q
.quad
.position
[1] + sgny
* blocks
[x
].length
]);
917 this.entity0
.attach(new yuu
.Animation(SLIDE_BLOCKS
, {
923 slam
: yuu
.cmd(function () {
924 var r
= new Promise(function (resolve
, reject
) {
925 if (this._locks
.some("slam")) {
926 reject("slamming is locked");
929 this._locks
.increment("spin", "slam");
931 handScene
.slam(this.slammer
.controller
.orientation
);
932 this.entity0
.attach(new yuu
.Animation(
934 $: this.slammer
.transform
,
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
;
941 slideBoardBlocks
: this.slideBoardBlocks
.bind(this)
944 this._locks
.decrement("slam");
949 }, "", "slam the active piece"),
951 back
: yuu
.cmd(function (x
, y
) {
952 if (this._locks
.some("quit"))
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(
963 $: this.entity0
.transform
,
966 yuu
.director
.removeScene(this);
970 }, "", "go back to the menu"),
972 slammerBB
: { get: function (p
) {
973 var length
= this.level
.slammer
.length
;
974 switch (this.slammer
.controller
.orientation
) {
976 return new yuu
.AABB(-Infinity
, -0.5, -1, length
- 0.5);
978 return new yuu
.AABB(length
+ 1, -0.5, Infinity
, length
- 0.5);
980 return new yuu
.AABB(-0.5, length
+ 1, length
- 0.5, Infinity
);
982 return new yuu
.AABB(-0.5, -Infinity
, length
- 0.5, -1);
986 _swipe: function (p0
, p1
) {
987 p0
= this.layer0
.worldFromDevice(p0
);
988 p1
= this.layer0
.worldFromDevice(p1
);
989 if (this.slammerBB
.contains(p0
)) {
993 if (this.gridBB
.contains(p0
) && !this.gridBB
.contains(p1
)) {
994 this.back(p1
.x
- p0
.x
, p1
.y
- p0
.y
);
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
);
1009 p
= this.layer0
.worldFromDevice(p
);
1010 if (this.gridBB
.contains(p
)) {
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();
1027 } else if (this.rightBB
.contains(p
)) {
1028 this.rotateTo(RIGHT
);
1029 handScene
.rotatedCcw();
1031 } else if (this.topBB
.contains(p
)) {
1034 handScene
.rotatedCw();
1036 handScene
.rotatedCcw();
1038 } else if (this.bottomBB
.contains(p
)) {
1039 this.rotateTo(BOTTOM
);
1041 handScene
.rotatedCw();
1043 handScene
.rotatedCcw();
1048 doubletap: function () {
1049 return this.inputs
.tap
.apply(this, arguments
);
1052 hold: function (p
) {
1053 p
= this.layer0
.worldFromDevice(p
);
1054 if (this.gridBB
.contains(p
)) {
1060 dragstart: function (p
) {
1061 p
= this.layer0
.worldFromDevice(p
);
1062 this._dragging
= this.slammerBB
.contains(p
);
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
;
1071 if (this._dragging
=== true && inGrid
) {
1073 } else if (p
.x
> 0 && p
.x
< length
&& !inGrid
) {
1074 o
= p
.y
< 0 ? BOTTOM
: TOP
;
1075 if (o
!== this.slammer
.controller
.orientation
) {
1079 } else if (p
.y
> 0 && p
.y
< length
&& !inGrid
) {
1080 o
= p
.x
< 0 ? LEFT
: RIGHT
;
1081 if (o
!== this.slammer
.controller
.orientation
) {
1087 return this._dragging
;
1090 dragend: function (p0
, p1
) {
1091 this._dragging
= false;
1094 release: function () {
1098 swipeleft: function (p0
, p1
) {
1099 return this._swipe(p0
, p1
);
1101 swiperight: function (p0
, p1
) {
1102 return this._swipe(p0
, p1
);
1104 swipeup: function (p0
, p1
) {
1105 return this._swipe(p0
, p1
);
1107 swipedown: function (p0
, p1
) {
1108 return this._swipe(p0
, p1
);
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",
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)}],
1148 0: { tween1
: { x
: "x" }, duration
: "duration" }
1152 0: { tween1
: { luminance
: 1, alpha
: 1 }, duration
: 32, repeat
: -1 }
1156 0: { tween
: { cursor
: { y
: "mid" } },
1157 playSound
: "@winding",
1159 8: { tween
: { cursor
: { y
: "line" },
1162 20: { tween
: { scene
: { y
: 10 },
1163 select
: { y
: -11.5, scale
: [3, 3, 1] }
1165 38: { event
: "appear",
1166 tween
: { select
: { y
: 0, scale
: [1, 0, 1] } },
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);
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);
1184 function generateQuads() {
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
];
1196 ce
.data
.flasher
= batch
.createQuad();
1197 ce
.data
.flasher
.alpha
= 0;
1201 if (level
.randomSlammer
)
1204 // 14 = maximum slammer size + 1 background + 1 flasher
1205 var batch
= new yuu
.QuadBatchC(14);
1207 new yuu
.Transform([2 * i
, 0, 0]),
1210 activate: function () {
1212 var scene
= new GridScene(level
, difficultyForLevel(level
));
1213 yuu
.director
.insertUnderScene(scene
);
1218 if (level
.randomSlammer
) {
1219 ce
.attach(new yuu
.Ticker(function () {
1220 if (!activated
&& yuu
.random
.randbool(0.7)) {
1233 var HAND_TICK_BACK
= {
1234 0: { tween1
: { rotation
: "rotation" }, duration
: 6, repeat
: -1 }
1237 MenuScene
= yT(yuu
.Scene
, {
1238 constructor: function (initialLevel
) {
1239 yuu
.Scene
.call(this);
1240 this.entity0
.attach(new yuu
.Transform(),
1241 new AnimationQueue());
1243 this.pointer
= new yuu
.E(
1244 new yuu
.Transform([5, 8, 0]),
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
);
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;
1261 this.entity0
.attach(
1262 new yuu
.Ticker(this._animation
.bind(this), 60));
1265 _animation: function (count
) {
1266 var length
= this.availableLevels
.length
;
1267 var range
= Math
.pow(2, length
);
1268 var rand
= yuu
.random
.randrange(range
);
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
))) {
1277 ? yuu
.random
.randsign(Math
.PI
/ 2)
1279 targets
.push(child
.transform
);
1280 yaws
.push(child
.transform
.yaw
+ dyaw
);
1283 if (targets
.length
) {
1284 this.entity0
.attach(new yuu
.Animation(
1285 ROTATE_ALL
, { $s
: targets
, yaws
: yaws
}));
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
);
1296 circleScene
.toBottom();
1297 handScene
.finished();
1298 this._locks
.increment("slam", "move");
1299 this.entity0
.animationQueue
.enqueue(
1301 { $: this.entity0
.transform
})
1302 .then(this._locks
.decrementer("slam", "move"));
1305 didWinLevel: function (level
, difficulty
, firstTime
) {
1306 var idx
= this.availableLevels
.indexOf(level
);
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"));
1322 changeActiveIndex: function (index
, animate
) {
1323 var oldIndex
= this.activeIndex
;
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(
1332 $: this.menu
.transform
,
1336 p
.then(this._locks
.decrementer("slam"));
1338 return p
|| Promise
.resolve();
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);
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);
1355 }, "move the cursor right"),
1357 slam
: yuu
.cmd(function () {
1358 if (this._locks
.some("slam"))
1360 var activeChild
= this.menu
.children
[this.activeIndex
];
1361 this._locks
.increment("slam", "move");
1362 handScene
.menuChoice();
1363 circleScene
.toBack();
1365 this.entity0
.animationQueue
.enqueue(
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);
1377 }, "choose the active menu item"),
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
);
1386 pinchout: function (p0
, p1
) {
1387 p0
= this.layer0
.worldFromDevice(p0
);
1388 p1
= this.layer0
.worldFromDevice(p1
);
1389 if (vec2
.sqrDist(p0
, p1
) > 1) {
1395 hold: function (p
) {
1396 return this.inputs
.dragstart
.call(this, p
);
1399 release: function (p
) {
1400 if (this._dragStartX
!== null)
1401 return this.inputs
.dragend
.call(this, p
);
1404 dragstart: function (p
) {
1405 if (this._locks
.some("move"))
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
;
1416 dragdown: function (p0
, p1
) {
1417 p0
= this.layer0
.worldFromDevice(p0
);
1418 p1
= this.layer0
.worldFromDevice(p1
);
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) {
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
);
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);
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();
1463 sounds
.switchBroke
.play();
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) {
1478 resetEverything
: yuu
.cmd(function () {
1480 yuu
.director
.stop();
1482 }, "reset all saved data"),
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"),
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",
1515 0: { set1
: { y
: 1.5, x
: -1.5 },
1516 tween
: { bgQuad
: { alpha
: 0.75 }, $: { y
: 0, x
: 0 }, },
1517 playSound
: "@book-appear",
1521 var BOOK_DISMISS
= {
1522 0: { tween
: { bgQuad
: { alpha
: 0 }, $: { y
: 1.5, x
: -1.5, } },
1523 playSound
: "@book-dismiss",
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];
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"]
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 },
1543 }, duration
: 15, easing
: "linear" },
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"]
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] },
1555 page2
: { x
: 0, scaleX
: -2/3 },
1556 }, duration
: 15, easing
: "linear" },
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"]
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 },
1573 }, duration
: 15, easing
: "linear" },
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"]
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" },
1592 BookScene
= new yT(yuu
.Scene
, {
1593 constructor: function () {
1594 yuu
.Scene
.call(this);
1596 new yuu
.Transform().setScale([20, 20, 1]),
1597 this.bgQuad
= new yuu
.QuadC()
1598 .setColor([0, 0, 0, 0])
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());
1611 this._locks
= new FlagSet("turn");
1612 this.addEntities(bg
, this.page1
, this.page2
);
1614 this.ready
= yuu
.ready(yf
.map(yf
.new_(yuu
.Instrument
), [
1615 "@page-turn-1", "@page-turn-2", "@page-turn-3",
1620 help
: yuu
.cmd(function () {
1622 }, "dismiss the help screen"),
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
);
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
;
1639 parent
.replaceChild(p
, spinner
);
1641 }, "why would you ever want to run this?"),
1644 this._anim(BOOK_APPEAR
);
1645 storage
.setFlag("instructions");
1648 _anim: function (anim
) {
1649 this._locks
.increment("turn");
1650 var completion
= this._locks
.decrementer("turn");
1653 completion
= yuu
.director
.removeScene
.bind(yuu
.director
, this);
1657 var device
= yuu
.director
.preferredDevice();
1658 this.entity0
.attach(new yuu
.Animation(
1660 $: this.entity0
.transform
,
1661 page
: device
=== "keyboard" ? KEYBOARD_PAGE
1662 : device
=== "gamepad" ? GAMEPAD_PAGE
1664 page1
: this.page1
.transform
,
1665 page2
: this.page2
.transform
,
1666 page1Quad
: this.page1Quad
,
1667 page2Quad
: this.page2Quad
,
1672 advance
: yuu
.cmd(function () {
1673 if (this._locks
.some("turn"))
1675 this._anim(BOOK_FORWARD
[this.current
++]);
1678 skip
: yuu
.cmd(function () {
1679 if (this._locks
.some("turn"))
1681 this._anim(BOOK_DISMISS
);
1684 back
: yuu
.cmd(function () {
1685 if (this._locks
.some("turn"))
1687 if (this.current
> 0)
1688 this._anim(BOOK_BACKWARD
[--this.current
]);
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),
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
);
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) {
1711 this.cursor
= "W-resize";
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) {
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; },
1736 consume
: yuu
.Director
.prototype.GESTURES
1737 .concat(yuu
.Director
.prototype.CANVAS_EVENTS
)
1749 gamepadbutton0
: "advance",
1750 gamepadbutton1
: "skip",
1751 gamepadbutton4
: "back",
1752 gamepadbutton5
: "advance",
1753 gamepadbutton8
: "skip",
1754 gamepadbutton9
: "skip",
1755 gamepadbutton14
: "back",
1756 gamepadbutton15
: "advance",
1760 var OUTER_FLIP_TICK
= {
1761 0: { tween1
: { yaw
: "yaw" }, duration
: 15 }
1764 var CIRCLE_TO_BOTTOM
= {
1765 0: { tween1
: { pitch
: Math
.PI
* 0.35, y
: -0.3 }, duration
: 35 }
1768 var CIRCLE_TO_BACK
= {
1769 0: { tween1
: { pitch
: Math
.PI
* 0.15, y
: -0.1 }, duration
: 35 }
1772 var CIRCLE_INNER_RATCHET
= {
1773 0: { tween1
: { rotation
: "rotation1" }, duration
: 15,
1774 playSound
: "@regear"
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 }
1782 var CIRCLE_INNER_WIND
= {
1783 0: { tween1
: { rotation
: "rotation1" }, duration
: 8 },
1784 15: { tween1
: { rotation
: "rotation2" }, duration
: 20 },
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
}]
1797 0: { tween1
: { rotation
: "rotation" }, duration
: 6 }
1801 // Nearly all derived from
1802 // http://www.clockguy.com/SiteRelated/SiteReferencePages/ClockChimeTunes.html
1804 // All transposition & transcription errors are mine.
1806 { name
: "Westminster",
1815 { name
: "Wittington",
1816 keys
: ["Eb4", "E4"],
1817 bars
: ["1 2 3 5 4 6 7 0",
1831 { name
: "Canterbury",
1833 bars
: ["2 0 5 3 1 4",
1844 bars
: ["5 4 3 2 1 0",
1852 { name: "St. Michael's",
1854 bars: ["7 6 5 4 3 2 1 0",
1862 { name
: "Winchester",
1864 bars
: ["5 3 1 0 2 4",
1873 function third (s
) {
1874 return "Q " + s
.replace(/([^ ]+ [^ ]+)/g, "$1 Z");
1877 function silence (s
) {
1878 return "Q " + s
.replace(/[^ ]+/g, "Z");
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.",
1885 var TIMES3
= ["Q", "Q", silence
, silence
,
1886 third
, third
, third
, third
,
1890 function deck (pack
, random
) {
1891 random
= random
|| yuu
.random
;
1893 return function () {
1894 if (stock
.length
=== 0)
1895 stock
= random
.shuffle(pack
.slice());
1900 function generateScore () {
1901 var chimes
= yuu
.random
.choice(CHIMES
);
1902 var bar
= deck(chimes
.bars
);
1904 return yf
.isFunction(t
) ? t(bar()) : t
+ " " + bar();
1907 function line (times
) {
1908 return yf
.map(draw
, yuu
.random
.shuffle(times
)).join(" ");
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
);
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"))
1931 .setSize([0.35417, 0.35417]));
1932 arm
.addChild(this.outer
);
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));
1943 var NOISY_QUADS
= new yuu
.ShaderProgram(
1944 ["@noise.glsl", "@noisyquads"], ["yuu/@color"]);
1946 var bgMat
= new yuu
.Material(
1947 yuu
.Texture
.DEFAULT
, NOISY_QUADS
, { range
: 0.8 });
1948 bgMat
.uniforms
.cut
= yf
.volatile(cycler(100000));
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);
1964 this.entity0
.addChild(bg
);
1965 this.entity0
.attach(new yuu
.Animation(
1966 BACKGROUND_DRIFT
, { $: bg
.transform
}));
1968 this.ground
= new yuu
.E(new yuu
.Transform());
1969 this.ground
.addChildren(rim
, inner
, arm
);
1970 this.entity0
.addChild(this.ground
);
1972 this.music
= yuu
.audio
.createGain();
1973 this.music
.gain
.value
= 0.3;
1974 this.music
.connect(yuu
.audio
.music
);
1975 this._finished
= false;
1977 this.ready
= yuu
.ready([
1978 this.outerQuad
.material
,
1979 this.innerQuad
.material
,
1980 this.rimQuad
.material
,
1985 help
: yuu
.cmd(function () {
1986 yuu
.director
.pushScene(new BookScene());
1987 }, "bring up the help screen"),
1989 yuu
: yuu
.cmd(function () {
1990 this.outerQuad
.material
= new yuu
.Material("@circle-outer-ee");
1996 gamepadbutton6
: "help",
1997 f10
: "showOverlay preferences",
1998 "shift+y+u+`": "yuu",
1999 "gamepadbutton10+gamepadbutton11": "yuu",
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
);
2010 toBottom: function () {
2011 this.entity0
.attach(
2012 new yuu
.Animation(CIRCLE_TO_BOTTOM
, { $: this.ground
.transform
}));
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
,
2030 score
.key
= this.score
&& this.score
.key
;
2034 _musicSchedule: function (count
) {
2035 var t
= yuu
.director
.currentAudioTime
;
2038 if (this._finished
) {
2039 if (this._finished
=== "won" && this.score
.key
) {
2040 var score
= yuu
.parseScore(
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",
2046 yuu
.Scales
.MAJOR
, this.score
.key
);
2047 while ((note
= score
.shift())) {
2048 sounds
.chime
.createSound(
2053 ).connect(this.music
);
2056 this._finished
= false;
2060 if (!(this.score
&& this.score
.length
)) {
2061 this.score
= generateScore();
2066 while (this.score
.length
&& this.score
[0].time
< this.playing
) {
2067 note
= this.score
.shift();
2068 sounds
.chime
.createSound(
2070 t
+ note
.time
% 1 + yuu
.random
.gauss(0, 0.015),
2073 ).connect(this.music
);
2076 if ((this.tension
*= 0.95) > 1) {
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
,
2087 [sounds
.tick
, sounds
.tock
][count
& 1]
2088 .createSound(yuu
.audio
, t
, 0, 0.5, 1.0)
2089 .connect(this.music
);
2092 this.clockTick(this.reversed
-- > 0 ? TICK_REV
: TICK_ROT
);
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(
2102 { $: this.arm
.transform
, rotation
: rot
}));
2105 toBack: function () {
2107 this.entity0
.attach(
2108 new yuu
.Animation(CIRCLE_TO_BACK
, { $: this.ground
.transform
}));
2112 new yuu
.Ticker(this._musicSchedule
.bind(this), 60));
2116 this._finished
= "won";
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)
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
,
2138 rotated: function () {
2139 this.tension
+= yuu
.random
.uniform(0.1);
2143 this.tension
+= yuu
.random
.uniform(0.2);
2146 reverse: function () {
2147 this.tension
-= yuu
.random
.uniform(0.1);
2148 this.reversed
= Math
.max(this.reversed
, 0) + 1;