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 regear
: new yuu
.Instrument("@regear"),
71 winding
: new yuu
.Instrument("@winding"),
72 slam
: new yuu
.Instrument("@slam"),
73 switch: new yuu
.Instrument("@switch"),
74 clicking
: new yuu
.Instrument("@clicking"),
75 bookAppear
: new yuu
.Instrument("@book-appear"),
76 switchBroke
: new yuu
.Instrument({
77 sample
: { "@switch": { duration
: 0.27, offset
: 0.1 } } }),
78 switchOn
: new yuu
.Instrument({
79 sample
: { "@switch": { duration
: 0.2 } } }),
80 switchOff
: new yuu
.Instrument({
81 sample
: { "@switch": { offset
: 0.2 } } }),
82 chime
: new yuu
.Instrument({
83 envelope
: { "0": 1, "0.7": 0.2, "3": 0 },
85 envelope
: { "0": 1, "0.7": 0.2, "3": 0 },
91 yuu
.director
.pushScene(circleScene
= new CircleScene());
92 yuu
.director
.pushScene(handScene
= new HandScene());
93 yuu
.director
.pushScene(new MenuScene());
94 if (!storage
.getFlag("instructions")) {
95 yuu
.director
.entity0
.attach(new yuu
.Ticker(function () {
96 yuu
.director
.pushScene(new BookScene());
102 .concat(yf
.map(yf
.getter
.bind(sounds
), Object
.keys(sounds
)))
107 yuu
.director
.start();
110 window
.addEventListener("load", function() {
111 yuu
.registerInitHook(load
);
112 yuu
.init({ backgroundColor
: [0, 0, 0, 1], antialias
: false }).then(start
);
115 var PALETTE
= [[ 0.76, 0.13, 0.13 ],
116 [ 0.33, 0.49, 0.71 ],
117 [ 0.45, 0.68, 0.32 ],
118 [ 0.51, 0.32, 0.63 ],
119 [ 0.89, 0.49, 0.11 ],
120 [ 1.00, 1.00, 0.30 ]];
123 { name
: "12345654321",
124 randomSlammer
: [3, 5],
125 deps
: "50040@easy 53535@easy 44404@easy 1302@easy 12321@easy 20124@easy"
128 { slammer
: [1, 1], sets
: "tutorial",
129 scramble
: { easy
: "01", hard
: "0122" } },
130 { slammer
: [1, 1, 1], deps
: "tutorial",
131 scramble
: { easy
: "11", hard
: "1212" } },
132 { slammer
: [2, 1], deps
: "tutorial", sets
: "asymmetric",
133 scramble
: { easy
: "32", hard
: "3321" } },
134 { slammer
: [1, 2, 1], deps
: "tutorial", sets
: "unequal",
135 scramble
: { easy
: "112", hard
: "3210" } },
136 { slammer
: [2, 0], deps
: "asymmetric", sets
: "zero",
137 scramble
: { easy
: "23", hard
: "032" } },
138 { slammer
: [2, 0, 2], deps
: "zero",
139 scramble
: { easy
: "11", hard
: "2211" } },
140 { slammer
: [1, 1, 1, 1], deps
: "tutorial" },
141 { slammer
: [2, 1, 1], deps
: "asymmetric" },
142 { slammer
: [1, 2, 1, 2], deps
: "asymmetric",
143 scramble
: { easy
: "012" } },
144 { slammer
: [1, 2, 3, 4], deps
: "asymmetric", sets
: "solid",
145 scramble
: { easy
: "110" } },
146 { slammer
: [5, 0, 0, 4, 0], deps
: "unequal zero",
147 scramble
: { easy
: "112" } },
148 { slammer
: [5, 3, 5, 3, 5], deps
: "unequal solid",
149 scramble
: { easy
: "3232" } },
150 { slammer
: [4, 4, 4, 0, 4], deps
: "solid zero",
151 scramble
: { easy
: "0321" } },
152 { slammer
: [1, 3, 0, 2], deps
: "unequal zero" },
153 { slammer
: [1, 2, 3, 2, 1], deps
: "unequal",
154 scramble
: { easy
: "3333" } },
155 { slammer
: [2, 0, 1, 2, 4], deps
: "unequal zero" },
158 function levelName (level
) {
159 return (level
.name
|| level
.slammer
.join("")).trim();
162 function wonLevel (level
, difficulty
) {
164 storage
.setFlag(level
.sets
);
165 storage
.setFlag(levelName(level
) + "@" + difficulty
);
168 function hasBeaten (level
, difficulty
) {
169 return storage
.getFlag(levelName(level
) + "@" + difficulty
);
172 function scrambleForLevel (rnd
, level
, difficulty
) {
173 var c
= difficulty
=== "easy" ? 0 : 1;
174 if (difficulty
=== "random")
175 c
= rnd
.randrange(2, 5);
176 var length
= level
.slammer
.length
;
177 return rnd
.randrange(length
* c
, length
* (c
+ 1)) + 2;
180 function difficultyForLevel (level
) {
181 if (level
.deps
&& !level
.deps
.split(" ").every(storage
.getFlag
, storage
))
183 if (hasBeaten(level
, "hard"))
185 if (hasBeaten(level
, "easy"))
191 function levelRandom (level
, difficulty
) {
192 if (difficulty
=== "random")
195 return new yuu
.Random(yuu
.createLCG(+level
.slammer
.join("")));
198 function generateBoard (rnd
, level
) {
199 var size
= level
.length
;
200 var board
= new Array(size
);
201 for (var i
= 0; i
< size
; ++i
)
202 board
[i
] = yf
.repeat(i
% PALETTE
.length
+ 1, size
);
204 yuu
.transpose2d(board
);
208 function generateSlammer (rnd
, level
) {
209 var s
= new Array(level
.length
);
210 for (var i
= 0; i
< s
.length
; ++i
)
211 s
[i
] = yf
.repeat(0, level
[i
]);
217 var AnimationQueue
= yT(yuu
.C
, {
218 constructor: function () {
222 attached: function () {
226 _runNext: function () {
227 var next
= this._queue
[0];
228 if (next
&& this.entity
)
229 this.entity
.attach(new yuu
.Animation(
230 next
.timeline
, next
.params
, this._complete
.bind(this)));
233 _complete: function () {
234 var next
= this._queue
.shift();
239 enqueue: function (timeline
, params
) {
240 return new Promise(function (resolve
) {
246 // FIXME: Simply chaining the promise doesn't work here
247 // because the tick between the two handlers is often long
248 // enough to render a frame, and that frame will have some
249 // undesirable intermediate state.
250 if (this._queue
.length
=== 1)
255 SLOTS
: ["animationQueue"]
258 var SLAMMER_ROTATE
= {
259 0: { tween1
: { yaw
: "yaw" }, duration
: 10 }
263 0: { tweenAll
: { yaw
: "yaws" }, duration
: 10 }
266 var SLAMMER_BOUNCE
= {
267 0: { tween1
: { y
: 0.5 }, duration
: 5, repeat
: -1 }
271 0: { tweenAll
: { position
: "positions" },
272 duration
: 8, easing
: "linear" },
276 0: { tween1
: { y
: -1.5 }, easing
: "linear", duration
: 6 },
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 hand
= yuu
.random
.randrange(3);
584 this.leftQuad
.texBounds
= this.rightQuad
.texBounds
= [
585 hand
/ 2.99, 0, (hand
+ 1) / 3.01, 1];
586 this.layer0
.resize(-0.75, 0, 1.5, 1.5);
587 var leftWrist
= new yuu
.E(
588 new yuu
.Transform([-0.20, 0, 0], null,
589 [SIZE_X
, SIZE_Y
, 1]));
590 var rightWrist
= new yuu
.E(
591 new yuu
.Transform([0.20, 0, 0], null,
592 [-SIZE_X
, SIZE_Y
, 1]));
593 leftWrist
.addChild(this.left
);
594 rightWrist
.addChild(this.right
);
595 this.addEntities(leftWrist
, rightWrist
);
596 this.backColor
= yuu
.hslToRgb(
597 (yuu
.random
.gauss(0.1, 0.1) + 10) % 1,
598 yuu
.random
.uniform(0.2, 0.7),
599 yuu
.random
.uniform(0.2, 0.6),
601 this.leftQuad
.alpha
= this.rightQuad
.alpha
= 0.2;
602 var hsl
= yuu
.rgbToHsl(this.backColor
);
603 hsl
[2] = hsl
[2].lerp(1, 0.15);
604 hsl
[1] = hsl
[1].lerp(0, 0.30);
606 this.frontColor
= yuu
.hslToRgb(hsl
);
607 this.leftQuad
.color
= this.rightQuad
.color
= this.frontColor
;
608 this.ready
= hands
.ready
;
610 function Button (i
, command
) {
613 new yuu
.DataC({ command
: command
}),
614 new yuu
.QuadC(SIGILS
)
615 .setTexBounds([i
/ 6, 0, (i
+ 1) / 6, 0.5])
616 .setColor(PALETTE
[i
]));
619 this.helpButton
= new Button(1, "help");
620 this.backButton
= new Button(3, "back");
621 this.backButton
.transform
.x
-= 1.5;
622 this.leftButtons
= new yuu
.E(new yuu
.Transform());
623 this.leftButtons
.addChildren(this.helpButton
, this.backButton
);
624 this.rightButton
= new Button(2, "showOverlay preferences");
625 this.leftButtons
.transform
.scale
626 = this.rightButton
.transform
.scale
628 this.entity0
.addChildren(this.leftButtons
, this.rightButton
);
629 this.buttons
= [this.helpButton
, this.backButton
, this.rightButton
,
634 resize: function () {
635 var base
= new yuu
.AABB(-0.75, 0, 0.75, 1.5);
636 var vp
= base
.matchAspectRatio(yuu
.viewport
);
639 this.leftButtons
.transform
.xy
= [
640 vp
.x0
+ this.leftButtons
.transform
.scaleX
,
641 vp
.y1
- this.leftButtons
.transform
.scaleY
];
642 this.rightButton
.transform
.xy
= [
643 vp
.x1
- this.rightButton
.transform
.scaleX
,
644 vp
.y1
- this.rightButton
.transform
.scaleY
];
645 this.layer0
.resize(vp
.x0
, vp
.y0
, vp
.w
, vp
.h
);
648 mousemove: function (p
) {
649 p
= this.layer0
.worldFromDevice(p
);
651 for (var i
= 0; i
< this.buttons
.length
; ++i
) {
652 if (this.buttons
[i
].transform
.contains(p
)) {
653 this.cursor
= "pointer";
659 p
= this.layer0
.worldFromDevice(p
);
660 for (var i
= 0; i
< this.buttons
.length
; ++i
) {
661 if (this.buttons
[i
].transform
.contains(p
)) {
662 yuu
.director
.execute(this.buttons
[i
].data
.command
);
668 doubletap: function () {
669 return this.inputs
.tap
.apply(this, arguments
);
673 _anim: function (timeline
) {
674 this.entity0
.attach(new yuu
.Animation(
676 left
: this.left
.transform
,
677 right
: this.right
.transform
,
678 leftQuad
: this.leftQuad
,
679 rightQuad
: this.rightQuad
,
680 frontColor
: this.frontColor
,
681 backColor
: this.backColor
685 undo: function () { this._anim(HANDS_UNDO
); },
686 movedLeft: function () { this._anim(HANDS_LEFT
); },
687 movedRight: function () { this._anim(HANDS_RIGHT
); },
688 slam: function (o
) { this._anim(HANDS_SLAM
[o
]); },
689 rotatedCw: function () { this._anim(HANDS_ROTATE_CW
); },
690 rotatedCcw: function () { this._anim(HANDS_ROTATE_CCW
); },
691 menuChoice: function () {
692 this.entity0
.attach(new yuu
.Animation(
694 a
: this.backButton
.transform
,
695 b
: this.helpButton
.transform
697 this._anim(HANDS_MENU_CHOICE
);
699 finished: function () {
700 this.entity0
.attach(new yuu
.Animation(
702 a
: this.backButton
.transform
,
703 b
: this.helpButton
.transform
705 this._anim(HANDS_RETURN
);
711 tween1
: { y
: 0 }, duration
: 10 },
714 GridScene
= yT(yuu
.Scene
, {
715 constructor: function (level
, difficulty
) {
716 yuu
.Scene
.call(this);
717 this.entity0
.attach(new yuu
.Transform());
719 this.difficulty
= difficulty
;
720 this._locks
= new FlagSet("slam", "spin", "quit");
721 var rnd
= levelRandom(level
, difficulty
);
722 var colors
= yuu
.random
.shuffle(PALETTE
.slice());
723 colors
.unshift([1.0, 1.0, 1.0]);
724 this.board
= new yuu
.E(new BoardController(rnd
, level
, colors
),
726 new yuu
.DataC({ quads
: [] }));
727 this.slammer
= new yuu
.E(new SlammerController(rnd
, level
, colors
),
729 new yuu
.DataC({ quads
: [] }));
730 this.slammerHead
= new yuu
.E(new yuu
.Transform());
731 this.slammerRoot
= new yuu
.E(new yuu
.Transform());
732 var length
= level
.slammer
.length
;
733 var maxSize
= length
* length
;
734 var slammerBatch
= new yuu
.QuadBatchC(maxSize
);
735 slammerBatch
.material
= SIGILS
;
736 var boardBatch
= new yuu
.QuadBatchC(maxSize
);
737 boardBatch
.material
= SIGILS
;
738 this.slammerRoot
.transform
.xy
= [length
/ 2 - 0.5, length
/ 2 - 0.5];
739 this.slammerHead
.transform
.xy
= [-length
/ 2 + 0.5, length
/ 2 + 2];
740 this.slammerRoot
.addChild(this.slammerHead
);
741 this.slammerHead
.addChild(this.slammer
);
742 this.slammer
.attach(slammerBatch
);
743 this.board
.attach(boardBatch
);
744 yf
.irange
.call(this, function (x
) {
745 yf
.irange
.call(this, function (y
) {
746 var quad
= boardBatch
.createQuad();
747 quad
.color
= colors
[this.board
.controller
.contents
[x
][y
]];
748 quad
.position
= [x
, y
];
749 this.board
.data
.quads
.push({ quad
: quad
, x
: x
, y
: y
});
753 for (var x
= 0; x
< this.slammer
.controller
.blocks
.length
; ++x
) {
754 for (var y
= 0; y
< this.slammer
.controller
.blocks
[x
].length
; ++y
) {
755 var quad
= slammerBatch
.createQuad();
756 quad
.color
= colors
[this.slammer
.controller
.blocks
[x
][y
]];
757 quad
.position
= [x
, y
];
758 this.slammer
.data
.quads
.push({ quad
: quad
, x
: x
, y
: y
});
761 this.addEntities(this.board
, this.slammerRoot
);
763 if (!(this.cheating
= yuu
.director
.input
.pressed
["`"])) {
764 this.slammer
.controller
.clearUndoRecord();
768 this.gridBB
= new yuu
.AABB(-0.5, -0.5, length
- 0.5, length
- 0.5);
769 this.leftBB
= new yuu
.AABB(
770 -Infinity
, this.gridBB
.y0
, this.gridBB
.x0
, this.gridBB
.y1
);
771 this.rightBB
= new yuu
.AABB(
772 this.gridBB
.x1
, this.gridBB
.y0
, Infinity
, this.gridBB
.y1
);
773 this.topBB
= new yuu
.AABB(
774 this.gridBB
.x0
, this.gridBB
.y1
, this.gridBB
.x1
, Infinity
);
775 this.bottomBB
= new yuu
.AABB(
776 this.gridBB
.x0
, -Infinity
, this.gridBB
.x1
, this.gridBB
.y0
);
780 this._locks
.increment("slam");
781 this.entity0
.attach(new yuu
.Animation(
782 GRID_APPEAR
, { $: this.slammer
.transform
},
783 this._locks
.decrementer("slam")));
786 scramble: function (rnd
) {
787 var scramble
= (this.level
.scramble
|| {})[this.difficulty
];
788 var slammerCon
= this.slammer
.controller
;
789 var boardCon
= this.board
.controller
;
791 var count
= scrambleForLevel(rnd
, this.level
, this.difficulty
);
792 while (this.isComplete()) {
795 slammerCon
.orientation
= randSide(rnd
, slammerCon
.orientation
);
796 slammerCon
.slam(boardCon
);
800 for (var i
=0; i
< scramble
.length
; ++i
) {
801 slammerCon
.orientation
= +scramble
[i
];
802 slammerCon
.slam(boardCon
);
805 slammerCon
.orientation
= randSide();
806 this.slammerRoot
.transform
.yaw
= slammerCon
.orientation
* Math
.PI
/ 2;
809 isComplete: function () {
810 return this.slammer
.controller
.isComplete()
811 && this.board
.controller
.isComplete();
814 rotateTo
: yuu
.cmd(function (orientation
) {
815 return new Promise(function (resolve
) {
816 if (this._locks
.some("spin"))
818 this.slammer
.controller
.orientation
= orientation
;
819 this._locks
.increment("slam");
820 var yaw0
= this.slammerRoot
.transform
.yaw
;
821 var yaw1
= orientation
* Math
.PI
/ 2;
823 sounds
.clicking
.play();
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");
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" } }, duration
: 8 },
1157 8: { tween
: { cursor
: { y
: "line" },
1160 20: { tween
: { scene
: { y
: 10 },
1161 select
: { y
: -11.5, scale
: [3, 3, 1] }
1163 38: { event
: "appear",
1164 tween
: { select
: { y
: 0, scale
: [1, 0, 1] } },
1168 function menuEntityForLevel (level
, i
) {
1169 var activated
= false;
1170 function randomizeSlammer () {
1171 var min
= level
.randomSlammer
[0];
1172 var max
= level
.randomSlammer
[1];
1173 var size
= yuu
.random
.randrange(min
, max
+ 1);
1176 for (var i
= 0; i
< size
; ++i
)
1177 level
.slammer
[i
] = yuu
.random
.randrange(0, size
);
1178 } while (Math
.min
.apply(Math
, level
.slammer
) === max
1179 || Math
.max
.apply(Math
, level
.slammer
) === 0);
1182 function generateQuads() {
1184 var rgb
= PALETTE
[i
% PALETTE
.length
];
1185 var fit
= level
.slammer
.length
+ 1;
1186 batch
.createQuad().color
= rgb
;
1187 level
.slammer
.forEach(function (size
, y
) {
1188 var c
= batch
.createQuad();
1189 c
.color
= [0, 0, 0];
1190 c
.alpha
= hasBeaten(level
, "easy") ? 0.5 : 1.0;
1191 c
.size
= [size
/ fit
, 1 / fit
];
1192 c
.position
= [0, -0.5 + (y
+ 1) / fit
];
1194 ce
.data
.flasher
= batch
.createQuad();
1195 ce
.data
.flasher
.alpha
= 0;
1199 if (level
.randomSlammer
)
1202 // 14 = maximum slammer size + 1 background + 1 flasher
1203 var batch
= new yuu
.QuadBatchC(14);
1205 new yuu
.Transform([2 * i
, 0, 0]),
1208 activate: function () {
1210 var scene
= new GridScene(level
, difficultyForLevel(level
));
1211 yuu
.director
.insertUnderScene(scene
);
1216 if (level
.randomSlammer
) {
1217 ce
.attach(new yuu
.Ticker(function () {
1218 if (!activated
&& yuu
.random
.randbool(0.7)) {
1231 var HAND_TICK_BACK
= {
1232 0: { tween1
: { rotation
: "rotation" }, duration
: 6, repeat
: -1 }
1235 MenuScene
= yT(yuu
.Scene
, {
1236 constructor: function (initialLevel
) {
1237 yuu
.Scene
.call(this);
1238 this.entity0
.attach(new yuu
.Transform(),
1239 new AnimationQueue());
1241 this.pointer
= new yuu
.E(
1242 new yuu
.Transform([5, 8, 0]),
1245 var menu
= this.menu
= new yuu
.E(new yuu
.Transform([5, 6.5, 0]));
1246 this.addEntities(menu
, this.pointer
);
1247 this.availableLevels
= LEVELS
.filter(difficultyForLevel
);
1248 this.availableLevels
1249 .map(menuEntityForLevel
)
1250 .forEach(menu
.addChild
, menu
);
1252 var initialIdx
= this.availableLevels
.indexOf(initialLevel
);
1253 this._locks
= new FlagSet("slam", "move");
1254 this.activeIndex
= Math
.max(initialIdx
, 0);
1255 menu
.transform
.x
= 5 - 2 * this.activeIndex
;
1256 this.changeActiveIndex(this.activeIndex
, false);
1257 this._dragStartX
= null;
1259 this.entity0
.attach(
1260 new yuu
.Ticker(this._animation
.bind(this), 60));
1263 _animation: function (count
) {
1264 var length
= this.availableLevels
.length
;
1265 var range
= Math
.pow(2, length
);
1266 var rand
= yuu
.random
.randrange(range
);
1269 for (var i
= 0; i
< length
; ++i
) {
1270 var child
= this.menu
.children
[i
];
1271 var level
= this.availableLevels
[i
];
1272 var won
= hasBeaten(level
, "hard");
1273 if ((won
|| ((count
^ i
) & 1)) && ((count
^ rand
) & (1 << i
))) {
1275 ? yuu
.random
.randsign(Math
.PI
/ 2)
1277 targets
.push(child
.transform
);
1278 yaws
.push(child
.transform
.yaw
+ dyaw
);
1281 if (targets
.length
) {
1282 this.entity0
.attach(new yuu
.Animation(
1283 ROTATE_ALL
, { $s
: targets
, yaws
: yaws
}));
1285 circleScene
.clockTick(TICK_ROT2
, HAND_TICK_BACK
);
1286 sounds
[["tick", "tock"][count
& 1]]
1287 .createSound(yuu
.audio
, yuu
.audio
.currentTime
, 0, 0.2, 1.0)
1288 .connect(yuu
.audio
.music
);
1294 circleScene
.toBottom();
1295 handScene
.finished();
1296 this._locks
.increment("slam", "move");
1297 this.entity0
.animationQueue
.enqueue(
1299 { $: this.entity0
.transform
})
1300 .then(this._locks
.decrementer("slam", "move"));
1303 didWinLevel: function (level
, difficulty
, firstTime
) {
1304 var idx
= this.availableLevels
.indexOf(level
);
1307 this.entity0
.animationQueue
.enqueue(
1308 FLASH
, { $: this.menu
.children
[idx
].data
.flasher
});
1309 for (var i
= idx
; i
< this.availableLevels
.length
; ++i
) {
1310 if (!hasBeaten(this.availableLevels
[i
], difficulty
)) {
1311 this._locks
.increment("move");
1312 this.changeActiveIndex(i
, true)
1313 .then(this._locks
.decrementer("move"));
1320 changeActiveIndex: function (index
, animate
) {
1321 var oldIndex
= this.activeIndex
;
1323 this.activeIndex
= index
= yf
.clamp(
1324 index
, 0, this.menu
.children
.length
- 1);
1325 if (index
!== oldIndex
&& animate
) {
1326 this._locks
.increment("slam");
1327 var duration
= Math
.ceil(8 * Math
.abs(oldIndex
- index
));
1328 p
= this.entity0
.animationQueue
.enqueue(
1330 $: this.menu
.transform
,
1334 p
.then(this._locks
.decrementer("slam"));
1336 return p
|| Promise
.resolve();
1339 left
: yuu
.cmd(function () {
1340 if (!this._locks
.some("move")) {
1341 sounds
[this.activeIndex
=== 0 ? "switchBroke" : "switch"].play();
1342 handScene
.movedLeft();
1343 this.changeActiveIndex(this.activeIndex
- 1, true);
1345 }, "move the cursor left"),
1346 right
: yuu
.cmd(function () {
1347 if (!this._locks
.some("move")) {
1348 sounds
[this.activeIndex
=== this.availableLevels
.length
- 1
1349 ? "switchBroke" : "switch"].play();
1350 handScene
.movedRight();
1351 this.changeActiveIndex(this.activeIndex
+ 1, true);
1353 }, "move the cursor right"),
1355 slam
: yuu
.cmd(function () {
1356 if (this._locks
.some("slam"))
1358 var activeChild
= this.menu
.children
[this.activeIndex
];
1359 this._locks
.increment("slam", "move");
1360 handScene
.menuChoice();
1361 circleScene
.toBack();
1363 sounds
.winding
.play();
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 }, },
1519 var BOOK_DISMISS
= {
1520 0: { tween
: { bgQuad
: { alpha
: 0 }, $: { y
: 1.5, x
: -1.5, } },
1524 var KEYBOARD_PAGE
= [0.25, 0.50, 0.50, 1.00];
1525 var POINTERS_PAGE
= [0.25, 0.00, 0.50, 0.50];
1526 var GAMEPAD_PAGE
= [0.00, 0.00, 0.25, 0.50];
1528 var BOOK_FORWARD
= [
1529 { 0: { set: { page2Quad
: { color
: [0.2, 0.2, 0.2, 1], texBounds
: "page" } },
1530 tween
: { page1
: { x
: -1/3 / 2, scaleX
: 0 },
1531 page2
: { x
: +1/3 / 2 },
1532 page2Quad
: { color
: [1, 1, 1, 1] },
1533 }, duration
: 15, easing
: "linear" },
1534 15: { set: { page1Quad
: { z
: 0, texBounds
: [0.25, 0.5, 0.00, 1] },
1535 page2Quad
: { z
: 0, texBounds
: "page" } },
1536 tween
: { page1
: { x
: -1/3, scaleX: -2/3 },
1538 }, duration
: 15, easing
: "linear" },
1541 { 0: { tween
: { page1
: { x
: -1/3 / 2 },
1542 page2
: { x
: +1/3 / 2, scaleX
: 0 }
1543 }, duration
: 15, easing
: "linear" },
1544 15: { set: { page1Quad
: { z
: 0, texBounds
: [0.25, 0.5, 0.00, 1] },
1545 page2Quad
: { z
: 1, texBounds
: [1.00, 0.5, 0.75, 1] } },
1546 tween
: { page1Quad
: { color
: [0.2, 0.2, 0.2, 1] },
1548 page2
: { x
: 0, scaleX
: -2/3 },
1549 }, duration
: 15, easing
: "linear" },
1555 var BOOK_BACKWARD
= [
1556 { 0: { tween
: { page1
: { x
: -1/3 / 2, scaleX
: 0 },
1557 page2
: { x
: +1/3 / 2 },
1558 }, duration
: 15, easing
: "linear" },
1559 15: { set: { page1Quad
: { z
: 1, texBounds
: [0.50, 0.5, 0.75, 1] },
1560 page2Quad
: { z
: 0 } },
1561 tween
: { page2Quad
: { color
: [0.2, 0.2, 0.2, 1] },
1562 page1
: { x
: 0, scaleX
: 2/3 },
1564 }, duration
: 15, easing
: "linear" },
1567 { 0: { set: { page1Quad
: { color
: [0.2, 0.2, 0.2, 1] } },
1568 tween
: { page1Quad
: { color
: [1.0, 1.0, 1.0, 1] },
1569 page1
: { x
: -1/3 / 2 },
1570 page2
: { x
: +1/3 / 2, scaleX
: 0 }
1571 }, duration
: 15, easing
: "linear" },
1573 15: { set: { page1Quad
: { z
: 0, texBounds
: [0.25, 0.5, 0.00, 1] },
1574 page2Quad
: { z
: 0, texBounds
: "page" } },
1575 tween
: { page1
: { x
: -1/3 },
1576 page2
: { x
: +1/3, scaleX: 2/3 },
1577 }, duration
: 15, easing
: "linear" },
1581 BookScene
= new yT(yuu
.Scene
, {
1582 constructor: function () {
1583 yuu
.Scene
.call(this);
1585 new yuu
.Transform().setScale([20, 20, 1]),
1586 this.bgQuad
= new yuu
.QuadC()
1587 .setColor([0, 0, 0, 0])
1589 this.page1
= new yuu
.E(new yuu
.Transform(),
1590 this.page1Quad
= new yuu
.QuadC(BOOK
));
1591 this.page1Quad
.texBounds
= [0.50, 0.5, 0.75, 1];
1592 this.page1Quad
.z
= 1;
1593 this.page2
= new yuu
.E(new yuu
.Transform(),
1594 this.page2Quad
= new yuu
.QuadC(BOOK
));
1595 this.page2Quad
.texBounds
= [0.25, 0.5, 0.50, 1];
1596 this.page1
.transform
.scale
= [2/3, 1, 1];
1597 this.page2
.transform
.scale
= [2/3, 1, 1];
1598 this.entity0
.attach(new yuu
.Transform());
1600 this._locks
= new FlagSet("turn");
1601 this.addEntities(bg
, this.page1
, this.page2
);
1603 this.dismissSound
= new yuu
.Instrument("@book-dismiss");
1604 this.pageSounds
= [new yuu
.Instrument("@page-turn-1"),
1605 new yuu
.Instrument("@page-turn-2"),
1606 new yuu
.Instrument("@page-turn-3")];
1608 this.ready
= yuu
.ready([this.dismissSound
].concat(this.pageSounds
));
1611 help
: yuu
.cmd(function () {
1613 }, "dismiss the help screen"),
1615 licensing
: yuu
.cmd(function () {
1616 var licensing
= document
.getElementById("yuu-licensing");
1617 var parent
= licensing
.parentNode
;
1618 var spinner
= document
.createElement("div");
1619 spinner
.className
= "yuu-spinner";
1620 spinner
.id
= licensing
.id
;
1621 parent
.replaceChild(spinner
, licensing
);
1624 [yuu
.PATH
+ "data/license.txt", "data/license.txt"]))
1625 .then(function (texts
) {
1626 var text
= texts
.join("\n-- \n\n");
1627 var p
= document
.createElement("pre");
1628 p
.textContent
= text
;
1630 parent
.replaceChild(p
, spinner
);
1632 }, "why would you ever want to run this?"),
1635 this._anim(BOOK_APPEAR
);
1636 storage
.setFlag("instructions");
1639 _anim: function (anim
) {
1640 this._locks
.increment("turn");
1641 // FIXME: Need hooks from animations to audio
1642 var completion
= this._locks
.decrementer("turn");
1645 this.dismissSound
.play();
1646 completion
= yuu
.director
.removeScene
.bind(yuu
.director
, this);
1649 sounds
.bookAppear
.play();
1652 yuu
.random
.choice(this.pageSounds
).play();
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("http://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 10: { tween1
: { rotation
: "rotation2" }, duration
: 10 },
1774 20: { tween1
: { rotation
: "rotation1" }, duration
: 20,
1775 easing
: yuu
.Tween
.STEPPED(5) },
1776 40: { tween1
: { rotation
: "rotation2" }, duration
: 15 }
1779 var CIRCLE_INNER_WIND
= {
1780 0: { tween1
: { rotation
: "rotation1" }, duration
: 8 },
1781 15: { tween1
: { rotation
: "rotation2" }, duration
: 20 },
1784 var BACKGROUND_DRIFT
= {
1785 0: [{ tween1
: { yaw
: Math
.PI
* 2 },
1786 duration
: 13 * 60 * 60, repeat
: -Infinity
, easing
: "linear" },
1787 { tween1
: { scaleX
: 0.5 },
1788 duration
: 11 * 60 * 60, repeat
: -Infinity
},
1789 { tween1
: { scaleY
: 0.5 },
1790 duration
: 7 * 60 * 60, repeat
: -Infinity
}]
1794 0: { tween1
: { rotation
: "rotation" }, duration
: 6 }
1798 // Nearly all derived from
1799 // http://www.clockguy.com/SiteRelated/SiteReferencePages/ClockChimeTunes.html
1801 // All transposition & transcription errors are mine.
1803 { name
: "Westminster",
1812 { name
: "Wittington",
1813 keys
: ["Eb4", "E4"],
1814 bars
: ["1 2 3 5 4 6 7 0",
1828 { name
: "Canterbury",
1830 bars
: ["2 0 5 3 1 4",
1841 bars
: ["5 4 3 2 1 0",
1849 { name: "St. Michael's",
1851 bars: ["7 6 5 4 3 2 1 0",
1859 { name
: "Winchester",
1861 bars
: ["5 3 1 0 2 4",
1870 function third (s
) {
1871 return "Q " + s
.replace(/([^ ]+ [^ ]+)/g, "$1 Z");
1874 function silence (s
) {
1875 return "Q " + s
.replace(/[^ ]+/g, "Z");
1878 var TIMES1
= ["Q", "H", "H", "H", "H.", "H.", "W", "W"];
1879 var TIMES2
= ["Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q",
1880 third
, third
, "Q.", "Q.",
1882 var TIMES3
= ["Q", "Q", silence
, silence
,
1883 third
, third
, third
, third
,
1887 function deck (pack
, random
) {
1888 random
= random
|| yuu
.random
;
1890 return function () {
1891 if (stock
.length
=== 0)
1892 stock
= random
.shuffle(pack
.slice());
1897 function generateScore () {
1898 var chimes
= yuu
.random
.choice(CHIMES
);
1899 var bar
= deck(chimes
.bars
);
1901 return yf
.isFunction(t
) ? t(bar()) : t
+ " " + bar();
1904 function line (times
) {
1905 return yf
.map(draw
, yuu
.random
.shuffle(times
)).join(" ");
1908 var track
= "{ - W HZ " + line(TIMES1
)
1909 + " { W HZ Z " + line(TIMES2
)
1910 + " { W HZ Z Z I Z " + line(TIMES3
);
1911 var key
= yuu
.random
.choice(chimes
.keys
);
1912 yuu
.log("messages", "Playing " + chimes
.name
+ " in " + key
+ " major.");
1913 var score
= yuu
.parseScore(track
, yuu
.Scales
.MAJOR
, key
);
1918 CircleScene
= yT(yuu
.Scene
, {
1919 constructor: function () {
1920 yuu
.Scene
.call(this);
1921 this.layer0
.resize(-0.6, -0.6, 1.2, 1.2);
1922 var arm
= this.arm
= new yuu
.E(new yuu
.Transform());
1923 this.outer
= new yuu
.E(
1924 new yuu
.Transform([Math
.sqrt(2) / 5, -Math
.sqrt(2) / 5, 0]),
1925 this.outerQuad
= new yuu
.QuadC(new yuu
.Material("@circle-outer"))
1928 .setSize([0.35417, 0.35417]));
1929 arm
.addChild(this.outer
);
1931 var rim
= new yuu
.E(
1932 new yuu
.Transform(),
1933 this.rimQuad
= new yuu
.QuadC(new yuu
.Material("@circle-rim"))
1934 .setLuminance(0.2));
1935 var inner
= this.inner
= new yuu
.E(
1936 new yuu
.Transform(),
1937 this.innerQuad
= new yuu
.QuadC(new yuu
.Material("@circle-inner"))
1938 .setLuminance(0.3));
1940 var NOISY_QUADS
= new yuu
.ShaderProgram(
1941 ["@noise.glsl", "@noisyquads"], ["yuu/@color"]);
1943 var bgMat
= new yuu
.Material(
1944 yuu
.Texture
.DEFAULT
, NOISY_QUADS
, { range
: 0.8 });
1945 bgMat
.uniforms
.cut
= yf
.volatile(cycler(100000));
1947 var batch
= new yuu
.QuadBatchC(DIM
* DIM
);
1948 batch
.material
= bgMat
;
1949 var bg
= new yuu
.E(new yuu
.Transform(), batch
);
1950 yf
.irange(function (x
) {
1951 yf
.irange(function (y
) {
1952 var quad
= batch
.createQuad();
1953 quad
.size
= [1/4, 1/4];
1954 quad
.position
= [(x
- DIM
/ 2) * 1/4,
1955 (y
- DIM
/ 2) * 1/4];
1956 quad
.color
= [0.12, 0.08, 0.16];
1957 quad
.texBounds
= yf
.repeat(x
* DIM
+ y
, 4);
1961 this.entity0
.addChild(bg
);
1962 this.entity0
.attach(new yuu
.Animation(
1963 BACKGROUND_DRIFT
, { $: bg
.transform
}));
1965 this.ground
= new yuu
.E(new yuu
.Transform());
1966 this.ground
.addChildren(rim
, inner
, arm
);
1967 this.entity0
.addChild(this.ground
);
1969 this.music
= yuu
.audio
.createGain();
1970 this.music
.gain
.value
= 0.3;
1971 this.music
.connect(yuu
.audio
.music
);
1972 this._finished
= false;
1974 this.ready
= yuu
.ready([
1975 this.outerQuad
.material
,
1976 this.innerQuad
.material
,
1977 this.rimQuad
.material
,
1982 help
: yuu
.cmd(function () {
1983 yuu
.director
.pushScene(new BookScene());
1984 }, "bring up the help screen"),
1986 yuu
: yuu
.cmd(function () {
1987 this.outerQuad
.material
= new yuu
.Material("@circle-outer-ee");
1993 gamepadbutton6
: "help",
1994 f10
: "showOverlay preferences",
1995 "shift+y+u+`": "yuu",
1996 "gamepadbutton10+gamepadbutton11": "yuu",
2000 resize: function () {
2001 var vp
= new yuu
.AABB(-0.6, -0.6, 0.6, 0.6)
2002 .matchAspectRatio(yuu
.viewport
);
2003 this.layer0
.resize(vp
.x0
, vp
.y0
, vp
.w
, vp
.h
);
2007 toBottom: function () {
2008 this.entity0
.attach(
2009 new yuu
.Animation(CIRCLE_TO_BOTTOM
, { $: this.ground
.transform
}));
2013 var rot1
= this.inner
.transform
.rotation
;
2014 quat
.rotateZ(rot1
, rot1
, Math
.PI
/ (2 * Math
.E
));
2015 var rot2
= quat
.rotateX(quat
.create(), rot1
, -Math
.PI
/ 2);
2016 quat
.rotateY(rot2
, rot2
, Math
.PI
/ 2);
2017 quat
.rotateX(rot2
, rot2
, -Math
.PI
/ 2);
2018 this.entity0
.attach(
2019 new yuu
.Animation(CIRCLE_INNER_WIND
, {
2020 $: this.inner
.transform
,
2027 score
.key
= this.score
&& this.score
.key
;
2031 _musicSchedule: function (count
) {
2032 var t
= yuu
.director
.currentAudioTime
;
2035 if (this._finished
) {
2036 if (this._finished
=== "won" && this.score
.key
) {
2037 var score
= yuu
.parseScore(
2039 "1 3 2 Z 0 { - 1 Z 2 Z 0",
2040 "1 2 3 Z 0 { - 1 Z 3 Z 0",
2041 "0 1 2 Z 4 { - 0 Z 2 Z 4",
2043 yuu
.Scales
.MAJOR
, this.score
.key
);
2044 while ((note
= score
.shift())) {
2045 sounds
.chime
.createSound(
2050 ).connect(this.music
);
2053 this._finished
= false;
2057 if (!(this.score
&& this.score
.length
)) {
2058 this.score
= generateScore();
2063 while (this.score
.length
&& this.score
[0].time
< this.playing
) {
2064 note
= this.score
.shift();
2065 sounds
.chime
.createSound(
2067 t
+ note
.time
% 1 + yuu
.random
.gauss(0, 0.015),
2070 ).connect(this.music
);
2073 if ((this.tension
*= 0.95) > 1) {
2075 sounds
.winding
.createSound(yuu
.audio
, t
, 0, 1.0, 1.0)
2076 .connect(this.music
);
2077 var flip
= !this.outer
.transform
.yaw
* yuu
.random
.randsign(Math
.PI
);
2078 this.entity0
.attach(
2079 new yuu
.Animation(OUTER_FLIP_TICK
, {
2080 $: this.outer
.transform
,
2084 [sounds
.tick
, sounds
.tock
][count
& 1]
2085 .createSound(yuu
.audio
, t
, 0, 0.5, 1.0)
2086 .connect(this.music
);
2089 this.clockTick(this.reversed
-- > 0 ? TICK_REV
: TICK_ROT
);
2094 clockTick: function (amount
, anim
) {
2095 var rot
= this.arm
.transform
.rotation
;
2096 quat
.multiply(rot
, rot
, amount
|| TICK_ROT
);
2097 this.arm
.attach(new yuu
.Animation(
2099 { $: this.arm
.transform
, rotation
: rot
}));
2102 toBack: function () {
2104 this.entity0
.attach(
2105 new yuu
.Animation(CIRCLE_TO_BACK
, { $: this.ground
.transform
}));
2109 new yuu
.Ticker(this._musicSchedule
.bind(this), 60));
2113 this._finished
= "won";
2115 this.entity0
.attach(
2116 new yuu
.Animation(FLASH
, { $: this.innerQuad
}),
2117 new yuu
.Animation(FLASH
, { $: this.rimQuad
}, null, 32),
2118 new yuu
.Animation(FLASH
, { $: this.outerQuad
}, null, 48)
2123 this._finished
= "lose";
2124 var rot1
= this.inner
.transform
.rotation
;
2125 quat
.rotateZ(rot1
, rot1
, -Math
.PI
/ Math
.E
);
2126 var rot2
= quat
.rotateZ(quat
.create(), rot1
, Math
.PI
/ Math
.E
);
2127 this.entity0
.attach(
2128 new yuu
.Animation(CIRCLE_INNER_RATCHET
, {
2129 $: this.inner
.transform
,
2133 sounds
.regear
.play();
2136 rotated: function () {
2137 this.tension
+= yuu
.random
.uniform(0.1);
2141 this.tension
+= yuu
.random
.uniform(0.2);
2144 reverse: function () {
2145 this.tension
-= yuu
.random
.uniform(0.1);
2146 this.reversed
= Math
.max(this.reversed
, 0) + 1;