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 handLeft
= yuu
.random
.randrange(4);
584 var handRight
= yuu
.random
.randrange(4);
585 this.leftQuad
.texBounds
= [handLeft
/ 4, 0, (handLeft
+ 1) / 4, 1];
586 this.rightQuad
.texBounds
= [handRight
/ 4, 0, (handRight
+ 1) / 4, 1];
587 this.layer0
.resize(-0.75, 0, 1.5, 1.5);
588 var leftWrist
= new yuu
.E(
589 new yuu
.Transform([-0.20, 0, 0], null,
590 [SIZE_X
, SIZE_Y
, 1]));
591 var rightWrist
= new yuu
.E(
592 new yuu
.Transform([0.20, 0, 0], null,
593 [-SIZE_X
, SIZE_Y
, 1]));
594 leftWrist
.addChild(this.left
);
595 rightWrist
.addChild(this.right
);
596 this.addEntities(leftWrist
, rightWrist
);
597 this.backColor
= yuu
.hslToRgb(
598 (yuu
.random
.gauss(0.1, 0.1) + 10) % 1,
599 yuu
.random
.uniform(0.2, 0.7),
600 yuu
.random
.uniform(0.2, 0.6),
602 this.leftQuad
.alpha
= this.rightQuad
.alpha
= 0.2;
603 var hsl
= yuu
.rgbToHsl(this.backColor
);
604 hsl
[2] = hsl
[2].lerp(1, 0.15);
605 hsl
[1] = hsl
[1].lerp(0, 0.30);
607 this.frontColor
= yuu
.hslToRgb(hsl
);
608 this.leftQuad
.color
= this.rightQuad
.color
= this.frontColor
;
609 this.ready
= hands
.ready
;
611 function Button (i
, command
) {
614 new yuu
.DataC({ command
: command
}),
615 new yuu
.QuadC(SIGILS
)
616 .setTexBounds([i
/ 6, 0, (i
+ 1) / 6, 0.5])
617 .setColor(PALETTE
[i
]));
620 this.helpButton
= new Button(1, "help");
621 this.backButton
= new Button(3, "back");
622 this.backButton
.transform
.x
-= 1.5;
623 this.leftButtons
= new yuu
.E(new yuu
.Transform());
624 this.leftButtons
.addChildren(this.helpButton
, this.backButton
);
625 this.rightButton
= new Button(2, "showOverlay preferences");
626 this.leftButtons
.transform
.scale
627 = this.rightButton
.transform
.scale
629 this.entity0
.addChildren(this.leftButtons
, this.rightButton
);
630 this.buttons
= [this.helpButton
, this.backButton
, this.rightButton
,
635 resize: function () {
636 var base
= new yuu
.AABB(-0.75, 0, 0.75, 1.5);
637 var vp
= base
.matchAspectRatio(yuu
.viewport
);
640 this.leftButtons
.transform
.xy
= [
641 vp
.x0
+ this.leftButtons
.transform
.scaleX
,
642 vp
.y1
- this.leftButtons
.transform
.scaleY
];
643 this.rightButton
.transform
.xy
= [
644 vp
.x1
- this.rightButton
.transform
.scaleX
,
645 vp
.y1
- this.rightButton
.transform
.scaleY
];
646 this.layer0
.resize(vp
.x0
, vp
.y0
, vp
.w
, vp
.h
);
649 mousemove: function (p
) {
650 p
= this.layer0
.worldFromDevice(p
);
652 for (var i
= 0; i
< this.buttons
.length
; ++i
) {
653 if (this.buttons
[i
].transform
.contains(p
)) {
654 this.cursor
= "pointer";
660 p
= this.layer0
.worldFromDevice(p
);
661 for (var i
= 0; i
< this.buttons
.length
; ++i
) {
662 if (this.buttons
[i
].transform
.contains(p
)) {
663 yuu
.director
.execute(this.buttons
[i
].data
.command
);
669 doubletap: function () {
670 return this.inputs
.tap
.apply(this, arguments
);
674 _anim: function (timeline
) {
675 this.entity0
.attach(new yuu
.Animation(
677 left
: this.left
.transform
,
678 right
: this.right
.transform
,
679 leftQuad
: this.leftQuad
,
680 rightQuad
: this.rightQuad
,
681 frontColor
: this.frontColor
,
682 backColor
: this.backColor
686 undo: function () { this._anim(HANDS_UNDO
); },
687 movedLeft: function () { this._anim(HANDS_LEFT
); },
688 movedRight: function () { this._anim(HANDS_RIGHT
); },
689 slam: function (o
) { this._anim(HANDS_SLAM
[o
]); },
690 rotatedCw: function () { this._anim(HANDS_ROTATE_CW
); },
691 rotatedCcw: function () { this._anim(HANDS_ROTATE_CCW
); },
692 menuChoice: function () {
693 this.entity0
.attach(new yuu
.Animation(
695 a
: this.backButton
.transform
,
696 b
: this.helpButton
.transform
698 this._anim(HANDS_MENU_CHOICE
);
700 finished: function () {
701 this.entity0
.attach(new yuu
.Animation(
703 a
: this.backButton
.transform
,
704 b
: this.helpButton
.transform
706 this._anim(HANDS_RETURN
);
712 tween1
: { y
: 0 }, duration
: 10 },
715 GridScene
= yT(yuu
.Scene
, {
716 constructor: function (level
, difficulty
) {
717 yuu
.Scene
.call(this);
718 this.entity0
.attach(new yuu
.Transform());
720 this.difficulty
= difficulty
;
721 this._locks
= new FlagSet("slam", "spin", "quit");
722 var rnd
= levelRandom(level
, difficulty
);
723 var colors
= yuu
.random
.shuffle(PALETTE
.slice());
724 colors
.unshift([1.0, 1.0, 1.0]);
725 this.board
= new yuu
.E(new BoardController(rnd
, level
, colors
),
727 new yuu
.DataC({ quads
: [] }));
728 this.slammer
= new yuu
.E(new SlammerController(rnd
, level
, colors
),
730 new yuu
.DataC({ quads
: [] }));
731 this.slammerHead
= new yuu
.E(new yuu
.Transform());
732 this.slammerRoot
= new yuu
.E(new yuu
.Transform());
733 var length
= level
.slammer
.length
;
734 var maxSize
= length
* length
;
735 var slammerBatch
= new yuu
.QuadBatchC(maxSize
);
736 slammerBatch
.material
= SIGILS
;
737 var boardBatch
= new yuu
.QuadBatchC(maxSize
);
738 boardBatch
.material
= SIGILS
;
739 this.slammerRoot
.transform
.xy
= [length
/ 2 - 0.5, length
/ 2 - 0.5];
740 this.slammerHead
.transform
.xy
= [-length
/ 2 + 0.5, length
/ 2 + 2];
741 this.slammerRoot
.addChild(this.slammerHead
);
742 this.slammerHead
.addChild(this.slammer
);
743 this.slammer
.attach(slammerBatch
);
744 this.board
.attach(boardBatch
);
745 yf
.irange
.call(this, function (x
) {
746 yf
.irange
.call(this, function (y
) {
747 var quad
= boardBatch
.createQuad();
748 quad
.color
= colors
[this.board
.controller
.contents
[x
][y
]];
749 quad
.position
= [x
, y
];
750 this.board
.data
.quads
.push({ quad
: quad
, x
: x
, y
: y
});
754 for (var x
= 0; x
< this.slammer
.controller
.blocks
.length
; ++x
) {
755 for (var y
= 0; y
< this.slammer
.controller
.blocks
[x
].length
; ++y
) {
756 var quad
= slammerBatch
.createQuad();
757 quad
.color
= colors
[this.slammer
.controller
.blocks
[x
][y
]];
758 quad
.position
= [x
, y
];
759 this.slammer
.data
.quads
.push({ quad
: quad
, x
: x
, y
: y
});
762 this.addEntities(this.board
, this.slammerRoot
);
764 if (!(this.cheating
= yuu
.director
.input
.pressed
["`"])) {
765 this.slammer
.controller
.clearUndoRecord();
769 this.gridBB
= new yuu
.AABB(-0.5, -0.5, length
- 0.5, length
- 0.5);
770 this.leftBB
= new yuu
.AABB(
771 -Infinity
, this.gridBB
.y0
, this.gridBB
.x0
, this.gridBB
.y1
);
772 this.rightBB
= new yuu
.AABB(
773 this.gridBB
.x1
, this.gridBB
.y0
, Infinity
, this.gridBB
.y1
);
774 this.topBB
= new yuu
.AABB(
775 this.gridBB
.x0
, this.gridBB
.y1
, this.gridBB
.x1
, Infinity
);
776 this.bottomBB
= new yuu
.AABB(
777 this.gridBB
.x0
, -Infinity
, this.gridBB
.x1
, this.gridBB
.y0
);
781 this._locks
.increment("slam");
782 this.entity0
.attach(new yuu
.Animation(
783 GRID_APPEAR
, { $: this.slammer
.transform
},
784 this._locks
.decrementer("slam")));
787 scramble: function (rnd
) {
788 var scramble
= (this.level
.scramble
|| {})[this.difficulty
];
789 var slammerCon
= this.slammer
.controller
;
790 var boardCon
= this.board
.controller
;
792 var count
= scrambleForLevel(rnd
, this.level
, this.difficulty
);
793 while (this.isComplete()) {
796 slammerCon
.orientation
= randSide(rnd
, slammerCon
.orientation
);
797 slammerCon
.slam(boardCon
);
801 for (var i
=0; i
< scramble
.length
; ++i
) {
802 slammerCon
.orientation
= +scramble
[i
];
803 slammerCon
.slam(boardCon
);
806 slammerCon
.orientation
= randSide();
807 this.slammerRoot
.transform
.yaw
= slammerCon
.orientation
* Math
.PI
/ 2;
810 isComplete: function () {
811 return this.slammer
.controller
.isComplete()
812 && this.board
.controller
.isComplete();
815 rotateTo
: yuu
.cmd(function (orientation
) {
816 return new Promise(function (resolve
) {
817 if (this._locks
.some("spin"))
819 this.slammer
.controller
.orientation
= orientation
;
820 this._locks
.increment("slam");
821 var yaw0
= this.slammerRoot
.transform
.yaw
;
822 var yaw1
= orientation
* Math
.PI
/ 2;
824 sounds
.clicking
.play();
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");
932 handScene
.slam(this.slammer
.controller
.orientation
);
933 this.entity0
.attach(new yuu
.Animation(
935 $: this.slammer
.transform
,
937 this._locks
.decrement("spin");
938 this.slammer
.controller
.slam(this.board
.controller
);
939 this.slammerRoot
.transform
.yaw
= Math
.PI
/ 2 *
940 this.slammer
.controller
.orientation
;
942 slideBoardBlocks
: this.slideBoardBlocks
.bind(this)
945 this._locks
.decrement("slam");
950 }, "", "slam the active piece"),
952 back
: yuu
.cmd(function (x
, y
) {
953 if (this._locks
.some("quit"))
955 this._locks
.increment("quit", "slam", "spin");
956 var scene
= new MenuScene(this.level
);
957 yuu
.director
.pushScene(scene
);
958 var v
= [x
|| yuu
.random
.uniform(-1, 1),
959 y
|| yuu
.random
.uniform(-1, 1)];
960 var size
= this.board
.controller
.contents
.length
* 5;
961 vec2
.scale(v
, vec2
.normalize(v
, v
), size
);
962 this.entity0
.attach(new yuu
.Animation(
964 $: this.entity0
.transform
,
967 yuu
.director
.removeScene(this);
971 }, "", "go back to the menu"),
973 slammerBB
: { get: function (p
) {
974 var length
= this.level
.slammer
.length
;
975 switch (this.slammer
.controller
.orientation
) {
977 return new yuu
.AABB(-Infinity
, -0.5, -1, length
- 0.5);
979 return new yuu
.AABB(length
+ 1, -0.5, Infinity
, length
- 0.5);
981 return new yuu
.AABB(-0.5, length
+ 1, length
- 0.5, Infinity
);
983 return new yuu
.AABB(-0.5, -Infinity
, length
- 0.5, -1);
987 _swipe: function (p0
, p1
) {
988 p0
= this.layer0
.worldFromDevice(p0
);
989 p1
= this.layer0
.worldFromDevice(p1
);
990 if (this.slammerBB
.contains(p0
)) {
994 if (this.gridBB
.contains(p0
) && !this.gridBB
.contains(p1
)) {
995 this.back(p1
.x
- p0
.x
, p1
.y
- p0
.y
);
1001 resize: function () {
1002 var length
= this.level
.slammer
.length
;
1003 var base
= new yuu
.AABB(-length
- 2.5, -length
- 2.5,
1004 2 * length
+ 1.5, 2 * length
+ 1.5);
1005 var vp
= base
.matchAspectRatio(yuu
.viewport
);
1006 this.layer0
.resize(vp
.x0
, vp
.y0
, vp
.w
, vp
.h
);
1010 p
= this.layer0
.worldFromDevice(p
);
1011 if (this.gridBB
.contains(p
)) {
1017 touch: function (p
) {
1018 var length
= this.level
.slammer
.length
;
1019 var middle
= (length
- 1) / 2;
1020 p
= this.layer0
.worldFromDevice(p
);
1021 if (this.slammerBB
.contains(p
)) {
1022 this.slammer
.attach(new yuu
.Animation(
1023 SLAMMER_BOUNCE
, { $: this.slammer
.transform
}));
1024 } else if (this.leftBB
.contains(p
)) {
1025 this.rotateTo(LEFT
);
1026 handScene
.rotatedCw();
1028 } else if (this.rightBB
.contains(p
)) {
1029 this.rotateTo(RIGHT
);
1030 handScene
.rotatedCcw();
1032 } else if (this.topBB
.contains(p
)) {
1035 handScene
.rotatedCw();
1037 handScene
.rotatedCcw();
1039 } else if (this.bottomBB
.contains(p
)) {
1040 this.rotateTo(BOTTOM
);
1042 handScene
.rotatedCw();
1044 handScene
.rotatedCcw();
1049 doubletap: function () {
1050 return this.inputs
.tap
.apply(this, arguments
);
1053 hold: function (p
) {
1054 p
= this.layer0
.worldFromDevice(p
);
1055 if (this.gridBB
.contains(p
)) {
1061 dragstart: function (p
) {
1062 p
= this.layer0
.worldFromDevice(p
);
1063 this._dragging
= this.slammerBB
.contains(p
);
1066 drag: function (p0
, p1
) {
1067 var p
= this.layer0
.worldFromDevice(p1
);
1068 if (this._dragging
&& !this._locks
.some("slam")) {
1069 var inGrid
= this.gridBB
.contains(p
);
1070 var length
= this.level
.slammer
.length
;
1072 if (this._dragging
=== true && inGrid
) {
1074 } else if (p
.x
> 0 && p
.x
< length
&& !inGrid
) {
1075 o
= p
.y
< 0 ? BOTTOM
: TOP
;
1076 if (o
!== this.slammer
.controller
.orientation
) {
1080 } else if (p
.y
> 0 && p
.y
< length
&& !inGrid
) {
1081 o
= p
.x
< 0 ? LEFT
: RIGHT
;
1082 if (o
!== this.slammer
.controller
.orientation
) {
1088 return this._dragging
;
1091 dragend: function (p0
, p1
) {
1092 this._dragging
= false;
1095 release: function () {
1099 swipeleft: function (p0
, p1
) {
1100 return this._swipe(p0
, p1
);
1102 swiperight: function (p0
, p1
) {
1103 return this._swipe(p0
, p1
);
1105 swipeup: function (p0
, p1
) {
1106 return this._swipe(p0
, p1
);
1108 swipedown: function (p0
, p1
) {
1109 return this._swipe(p0
, p1
);
1129 gamepadbutton0
: "slam",
1130 gamepadbutton1
: "+undo",
1131 gamepadbutton2
: "slam",
1132 gamepadbutton3
: "+undo",
1133 gamepadbutton4
: "rotateCcw",
1134 gamepadbutton5
: "rotateCw",
1135 gamepadbutton8
: "back",
1136 gamepadbutton14
: "rotateCcw",
1137 gamepadbutton15
: "rotateCw",
1142 0: [{ set1
: { x
: 5, y
: 5, scaleX
: 0, scaleY
: 0 } },
1143 { tween1
: { x
: 0, y
: 0, scaleX
: 1 }, duration
: 24 },
1144 { tween1
: { scaleY
: 1 },
1145 duration
: 55, easing
: yuu
.Tween
.METASPRING(1, 10)}],
1149 0: { tween1
: { x
: "x" }, duration
: "duration" }
1153 0: { tween1
: { luminance
: 1, alpha
: 1 }, duration
: 32, repeat
: -1 }
1157 0: { tween
: { cursor
: { y
: "mid" } }, duration
: 8 },
1158 8: { tween
: { cursor
: { y
: "line" },
1161 20: { tween
: { scene
: { y
: 10 },
1162 select
: { y
: -11.5, scale
: [3, 3, 1] }
1164 38: { event
: "appear",
1165 tween
: { select
: { y
: 0, scale
: [1, 0, 1] } },
1169 function menuEntityForLevel (level
, i
) {
1170 var activated
= false;
1171 function randomizeSlammer () {
1172 var min
= level
.randomSlammer
[0];
1173 var max
= level
.randomSlammer
[1];
1174 var size
= yuu
.random
.randrange(min
, max
+ 1);
1177 for (var i
= 0; i
< size
; ++i
)
1178 level
.slammer
[i
] = yuu
.random
.randrange(0, size
);
1179 } while (Math
.min
.apply(Math
, level
.slammer
) === max
1180 || Math
.max
.apply(Math
, level
.slammer
) === 0);
1183 function generateQuads() {
1185 var rgb
= PALETTE
[i
% PALETTE
.length
];
1186 var fit
= level
.slammer
.length
+ 1;
1187 batch
.createQuad().color
= rgb
;
1188 level
.slammer
.forEach(function (size
, y
) {
1189 var c
= batch
.createQuad();
1190 c
.color
= [0, 0, 0];
1191 c
.alpha
= hasBeaten(level
, "easy") ? 0.5 : 1.0;
1192 c
.size
= [size
/ fit
, 1 / fit
];
1193 c
.position
= [0, -0.5 + (y
+ 1) / fit
];
1195 ce
.data
.flasher
= batch
.createQuad();
1196 ce
.data
.flasher
.alpha
= 0;
1200 if (level
.randomSlammer
)
1203 // 14 = maximum slammer size + 1 background + 1 flasher
1204 var batch
= new yuu
.QuadBatchC(14);
1206 new yuu
.Transform([2 * i
, 0, 0]),
1209 activate: function () {
1211 var scene
= new GridScene(level
, difficultyForLevel(level
));
1212 yuu
.director
.insertUnderScene(scene
);
1217 if (level
.randomSlammer
) {
1218 ce
.attach(new yuu
.Ticker(function () {
1219 if (!activated
&& yuu
.random
.randbool(0.7)) {
1232 var HAND_TICK_BACK
= {
1233 0: { tween1
: { rotation
: "rotation" }, duration
: 6, repeat
: -1 }
1236 MenuScene
= yT(yuu
.Scene
, {
1237 constructor: function (initialLevel
) {
1238 yuu
.Scene
.call(this);
1239 this.entity0
.attach(new yuu
.Transform(),
1240 new AnimationQueue());
1242 this.pointer
= new yuu
.E(
1243 new yuu
.Transform([5, 8, 0]),
1246 var menu
= this.menu
= new yuu
.E(new yuu
.Transform([5, 6.5, 0]));
1247 this.addEntities(menu
, this.pointer
);
1248 this.availableLevels
= LEVELS
.filter(difficultyForLevel
);
1249 this.availableLevels
1250 .map(menuEntityForLevel
)
1251 .forEach(menu
.addChild
, menu
);
1253 var initialIdx
= this.availableLevels
.indexOf(initialLevel
);
1254 this._locks
= new FlagSet("slam", "move");
1255 this.activeIndex
= Math
.max(initialIdx
, 0);
1256 menu
.transform
.x
= 5 - 2 * this.activeIndex
;
1257 this.changeActiveIndex(this.activeIndex
, false);
1258 this._dragStartX
= null;
1260 this.entity0
.attach(
1261 new yuu
.Ticker(this._animation
.bind(this), 60));
1264 _animation: function (count
) {
1265 var length
= this.availableLevels
.length
;
1266 var range
= Math
.pow(2, length
);
1267 var rand
= yuu
.random
.randrange(range
);
1270 for (var i
= 0; i
< length
; ++i
) {
1271 var child
= this.menu
.children
[i
];
1272 var level
= this.availableLevels
[i
];
1273 var won
= hasBeaten(level
, "hard");
1274 if ((won
|| ((count
^ i
) & 1)) && ((count
^ rand
) & (1 << i
))) {
1276 ? yuu
.random
.randsign(Math
.PI
/ 2)
1278 targets
.push(child
.transform
);
1279 yaws
.push(child
.transform
.yaw
+ dyaw
);
1282 if (targets
.length
) {
1283 this.entity0
.attach(new yuu
.Animation(
1284 ROTATE_ALL
, { $s
: targets
, yaws
: yaws
}));
1286 circleScene
.clockTick(TICK_ROT2
, HAND_TICK_BACK
);
1287 sounds
[["tick", "tock"][count
& 1]]
1288 .createSound(yuu
.audio
, yuu
.audio
.currentTime
, 0, 0.2, 1.0)
1289 .connect(yuu
.audio
.music
);
1295 circleScene
.toBottom();
1296 handScene
.finished();
1297 this._locks
.increment("slam", "move");
1298 this.entity0
.animationQueue
.enqueue(
1300 { $: this.entity0
.transform
})
1301 .then(this._locks
.decrementer("slam", "move"));
1304 didWinLevel: function (level
, difficulty
, firstTime
) {
1305 var idx
= this.availableLevels
.indexOf(level
);
1308 this.entity0
.animationQueue
.enqueue(
1309 FLASH
, { $: this.menu
.children
[idx
].data
.flasher
});
1310 for (var i
= idx
; i
< this.availableLevels
.length
; ++i
) {
1311 if (!hasBeaten(this.availableLevels
[i
], difficulty
)) {
1312 this._locks
.increment("move");
1313 this.changeActiveIndex(i
, true)
1314 .then(this._locks
.decrementer("move"));
1321 changeActiveIndex: function (index
, animate
) {
1322 var oldIndex
= this.activeIndex
;
1324 this.activeIndex
= index
= yf
.clamp(
1325 index
, 0, this.menu
.children
.length
- 1);
1326 if (index
!== oldIndex
&& animate
) {
1327 this._locks
.increment("slam");
1328 var duration
= Math
.ceil(8 * Math
.abs(oldIndex
- index
));
1329 p
= this.entity0
.animationQueue
.enqueue(
1331 $: this.menu
.transform
,
1335 p
.then(this._locks
.decrementer("slam"));
1337 return p
|| Promise
.resolve();
1340 left
: yuu
.cmd(function () {
1341 if (!this._locks
.some("move")) {
1342 sounds
[this.activeIndex
=== 0 ? "switchBroke" : "switch"].play();
1343 handScene
.movedLeft();
1344 this.changeActiveIndex(this.activeIndex
- 1, true);
1346 }, "move the cursor left"),
1347 right
: yuu
.cmd(function () {
1348 if (!this._locks
.some("move")) {
1349 sounds
[this.activeIndex
=== this.availableLevels
.length
- 1
1350 ? "switchBroke" : "switch"].play();
1351 handScene
.movedRight();
1352 this.changeActiveIndex(this.activeIndex
+ 1, true);
1354 }, "move the cursor right"),
1356 slam
: yuu
.cmd(function () {
1357 if (this._locks
.some("slam"))
1359 var activeChild
= this.menu
.children
[this.activeIndex
];
1360 this._locks
.increment("slam", "move");
1361 handScene
.menuChoice();
1362 circleScene
.toBack();
1364 sounds
.winding
.play();
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 }, },
1520 var BOOK_DISMISS
= {
1521 0: { tween
: { bgQuad
: { alpha
: 0 }, $: { y
: 1.5, x
: -1.5, } },
1525 var KEYBOARD_PAGE
= [0.25, 0.50, 0.50, 1.00];
1526 var POINTERS_PAGE
= [0.25, 0.00, 0.50, 0.50];
1527 var GAMEPAD_PAGE
= [0.00, 0.00, 0.25, 0.50];
1529 var BOOK_FORWARD
= [
1530 { 0: { set: { page2Quad
: { color
: [0.2, 0.2, 0.2, 1], texBounds
: "page" } },
1531 tween
: { page1
: { x
: -1/3 / 2, scaleX
: 0 },
1532 page2
: { x
: +1/3 / 2 },
1533 page2Quad
: { color
: [1, 1, 1, 1] },
1534 }, duration
: 15, easing
: "linear" },
1535 15: { set: { page1Quad
: { z
: 0, texBounds
: [0.25, 0.5, 0.00, 1] },
1536 page2Quad
: { z
: 0, texBounds
: "page" } },
1537 tween
: { page1
: { x
: -1/3, scaleX: -2/3 },
1539 }, duration
: 15, easing
: "linear" },
1542 { 0: { tween
: { page1
: { x
: -1/3 / 2 },
1543 page2
: { x
: +1/3 / 2, scaleX
: 0 }
1544 }, duration
: 15, easing
: "linear" },
1545 15: { set: { page1Quad
: { z
: 0, texBounds
: [0.25, 0.5, 0.00, 1] },
1546 page2Quad
: { z
: 1, texBounds
: [1.00, 0.5, 0.75, 1] } },
1547 tween
: { page1Quad
: { color
: [0.2, 0.2, 0.2, 1] },
1549 page2
: { x
: 0, scaleX
: -2/3 },
1550 }, duration
: 15, easing
: "linear" },
1556 var BOOK_BACKWARD
= [
1557 { 0: { tween
: { page1
: { x
: -1/3 / 2, scaleX
: 0 },
1558 page2
: { x
: +1/3 / 2 },
1559 }, duration
: 15, easing
: "linear" },
1560 15: { set: { page1Quad
: { z
: 1, texBounds
: [0.50, 0.5, 0.75, 1] },
1561 page2Quad
: { z
: 0 } },
1562 tween
: { page2Quad
: { color
: [0.2, 0.2, 0.2, 1] },
1563 page1
: { x
: 0, scaleX
: 2/3 },
1565 }, duration
: 15, easing
: "linear" },
1568 { 0: { set: { page1Quad
: { color
: [0.2, 0.2, 0.2, 1] } },
1569 tween
: { page1Quad
: { color
: [1.0, 1.0, 1.0, 1] },
1570 page1
: { x
: -1/3 / 2 },
1571 page2
: { x
: +1/3 / 2, scaleX
: 0 }
1572 }, duration
: 15, easing
: "linear" },
1574 15: { set: { page1Quad
: { z
: 0, texBounds
: [0.25, 0.5, 0.00, 1] },
1575 page2Quad
: { z
: 0, texBounds
: "page" } },
1576 tween
: { page1
: { x
: -1/3 },
1577 page2
: { x
: +1/3, scaleX: 2/3 },
1578 }, duration
: 15, easing
: "linear" },
1582 BookScene
= new yT(yuu
.Scene
, {
1583 constructor: function () {
1584 yuu
.Scene
.call(this);
1586 new yuu
.Transform().setScale([20, 20, 1]),
1587 this.bgQuad
= new yuu
.QuadC()
1588 .setColor([0, 0, 0, 0])
1590 this.page1
= new yuu
.E(new yuu
.Transform(),
1591 this.page1Quad
= new yuu
.QuadC(BOOK
));
1592 this.page1Quad
.texBounds
= [0.50, 0.5, 0.75, 1];
1593 this.page1Quad
.z
= 1;
1594 this.page2
= new yuu
.E(new yuu
.Transform(),
1595 this.page2Quad
= new yuu
.QuadC(BOOK
));
1596 this.page2Quad
.texBounds
= [0.25, 0.5, 0.50, 1];
1597 this.page1
.transform
.scale
= [2/3, 1, 1];
1598 this.page2
.transform
.scale
= [2/3, 1, 1];
1599 this.entity0
.attach(new yuu
.Transform());
1601 this._locks
= new FlagSet("turn");
1602 this.addEntities(bg
, this.page1
, this.page2
);
1604 this.dismissSound
= new yuu
.Instrument("@book-dismiss");
1605 this.pageSounds
= [new yuu
.Instrument("@page-turn-1"),
1606 new yuu
.Instrument("@page-turn-2"),
1607 new yuu
.Instrument("@page-turn-3")];
1609 this.ready
= yuu
.ready([this.dismissSound
].concat(this.pageSounds
));
1612 help
: yuu
.cmd(function () {
1614 }, "dismiss the help screen"),
1616 licensing
: yuu
.cmd(function () {
1617 var licensing
= document
.getElementById("yuu-licensing");
1618 var parent
= licensing
.parentNode
;
1619 var spinner
= document
.createElement("div");
1620 spinner
.className
= "yuu-spinner";
1621 spinner
.id
= licensing
.id
;
1622 parent
.replaceChild(spinner
, licensing
);
1625 [yuu
.PATH
+ "data/license.txt", "data/license.txt"]))
1626 .then(function (texts
) {
1627 var text
= texts
.join("\n-- \n\n");
1628 var p
= document
.createElement("pre");
1629 p
.textContent
= text
;
1631 parent
.replaceChild(p
, spinner
);
1633 }, "why would you ever want to run this?"),
1636 this._anim(BOOK_APPEAR
);
1637 storage
.setFlag("instructions");
1640 _anim: function (anim
) {
1641 this._locks
.increment("turn");
1642 // FIXME: Need hooks from animations to audio
1643 var completion
= this._locks
.decrementer("turn");
1646 this.dismissSound
.play();
1647 completion
= yuu
.director
.removeScene
.bind(yuu
.director
, this);
1650 sounds
.bookAppear
.play();
1653 yuu
.random
.choice(this.pageSounds
).play();
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 10: { tween1
: { rotation
: "rotation2" }, duration
: 10 },
1775 20: { tween1
: { rotation
: "rotation1" }, duration
: 20,
1776 easing
: yuu
.Tween
.STEPPED(5) },
1777 40: { tween1
: { rotation
: "rotation2" }, duration
: 15 }
1780 var CIRCLE_INNER_WIND
= {
1781 0: { tween1
: { rotation
: "rotation1" }, duration
: 8 },
1782 15: { tween1
: { rotation
: "rotation2" }, duration
: 20 },
1785 var BACKGROUND_DRIFT
= {
1786 0: [{ tween1
: { yaw
: Math
.PI
* 2 },
1787 duration
: 13 * 60 * 60, repeat
: -Infinity
, easing
: "linear" },
1788 { tween1
: { scaleX
: 0.5 },
1789 duration
: 11 * 60 * 60, repeat
: -Infinity
},
1790 { tween1
: { scaleY
: 0.5 },
1791 duration
: 7 * 60 * 60, repeat
: -Infinity
}]
1795 0: { tween1
: { rotation
: "rotation" }, duration
: 6 }
1799 // Nearly all derived from
1800 // http://www.clockguy.com/SiteRelated/SiteReferencePages/ClockChimeTunes.html
1802 // All transposition & transcription errors are mine.
1804 { name
: "Westminster",
1813 { name
: "Wittington",
1814 keys
: ["Eb4", "E4"],
1815 bars
: ["1 2 3 5 4 6 7 0",
1829 { name
: "Canterbury",
1831 bars
: ["2 0 5 3 1 4",
1842 bars
: ["5 4 3 2 1 0",
1850 { name: "St. Michael's",
1852 bars: ["7 6 5 4 3 2 1 0",
1860 { name
: "Winchester",
1862 bars
: ["5 3 1 0 2 4",
1871 function third (s
) {
1872 return "Q " + s
.replace(/([^ ]+ [^ ]+)/g, "$1 Z");
1875 function silence (s
) {
1876 return "Q " + s
.replace(/[^ ]+/g, "Z");
1879 var TIMES1
= ["Q", "H", "H", "H", "H.", "H.", "W", "W"];
1880 var TIMES2
= ["Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q",
1881 third
, third
, "Q.", "Q.",
1883 var TIMES3
= ["Q", "Q", silence
, silence
,
1884 third
, third
, third
, third
,
1888 function deck (pack
, random
) {
1889 random
= random
|| yuu
.random
;
1891 return function () {
1892 if (stock
.length
=== 0)
1893 stock
= random
.shuffle(pack
.slice());
1898 function generateScore () {
1899 var chimes
= yuu
.random
.choice(CHIMES
);
1900 var bar
= deck(chimes
.bars
);
1902 return yf
.isFunction(t
) ? t(bar()) : t
+ " " + bar();
1905 function line (times
) {
1906 return yf
.map(draw
, yuu
.random
.shuffle(times
)).join(" ");
1909 var track
= "{ - W HZ " + line(TIMES1
)
1910 + " { W HZ Z " + line(TIMES2
)
1911 + " { W HZ Z Z I Z " + line(TIMES3
);
1912 var key
= yuu
.random
.choice(chimes
.keys
);
1913 yuu
.log("messages", "Playing " + chimes
.name
+ " in " + key
+ " major.");
1914 var score
= yuu
.parseScore(track
, yuu
.Scales
.MAJOR
, key
);
1919 CircleScene
= yT(yuu
.Scene
, {
1920 constructor: function () {
1921 yuu
.Scene
.call(this);
1922 this.layer0
.resize(-0.6, -0.6, 1.2, 1.2);
1923 var arm
= this.arm
= new yuu
.E(new yuu
.Transform());
1924 this.outer
= new yuu
.E(
1925 new yuu
.Transform([Math
.sqrt(2) / 5, -Math
.sqrt(2) / 5, 0]),
1926 this.outerQuad
= new yuu
.QuadC(new yuu
.Material("@circle-outer"))
1929 .setSize([0.35417, 0.35417]));
1930 arm
.addChild(this.outer
);
1932 var rim
= new yuu
.E(
1933 new yuu
.Transform(),
1934 this.rimQuad
= new yuu
.QuadC(new yuu
.Material("@circle-rim"))
1935 .setLuminance(0.2));
1936 var inner
= this.inner
= new yuu
.E(
1937 new yuu
.Transform(),
1938 this.innerQuad
= new yuu
.QuadC(new yuu
.Material("@circle-inner"))
1939 .setLuminance(0.3));
1941 var NOISY_QUADS
= new yuu
.ShaderProgram(
1942 ["@noise.glsl", "@noisyquads"], ["yuu/@color"]);
1944 var bgMat
= new yuu
.Material(
1945 yuu
.Texture
.DEFAULT
, NOISY_QUADS
, { range
: 0.8 });
1946 bgMat
.uniforms
.cut
= yf
.volatile(cycler(100000));
1948 var batch
= new yuu
.QuadBatchC(DIM
* DIM
);
1949 batch
.material
= bgMat
;
1950 var bg
= new yuu
.E(new yuu
.Transform(), batch
);
1951 yf
.irange(function (x
) {
1952 yf
.irange(function (y
) {
1953 var quad
= batch
.createQuad();
1954 quad
.size
= [1/4, 1/4];
1955 quad
.position
= [(x
- DIM
/ 2) * 1/4,
1956 (y
- DIM
/ 2) * 1/4];
1957 quad
.color
= [0.12, 0.08, 0.16];
1958 quad
.texBounds
= yf
.repeat(x
* DIM
+ y
, 4);
1962 this.entity0
.addChild(bg
);
1963 this.entity0
.attach(new yuu
.Animation(
1964 BACKGROUND_DRIFT
, { $: bg
.transform
}));
1966 this.ground
= new yuu
.E(new yuu
.Transform());
1967 this.ground
.addChildren(rim
, inner
, arm
);
1968 this.entity0
.addChild(this.ground
);
1970 this.music
= yuu
.audio
.createGain();
1971 this.music
.gain
.value
= 0.3;
1972 this.music
.connect(yuu
.audio
.music
);
1973 this._finished
= false;
1975 this.ready
= yuu
.ready([
1976 this.outerQuad
.material
,
1977 this.innerQuad
.material
,
1978 this.rimQuad
.material
,
1983 help
: yuu
.cmd(function () {
1984 yuu
.director
.pushScene(new BookScene());
1985 }, "bring up the help screen"),
1987 yuu
: yuu
.cmd(function () {
1988 this.outerQuad
.material
= new yuu
.Material("@circle-outer-ee");
1994 gamepadbutton6
: "help",
1995 f10
: "showOverlay preferences",
1996 "shift+y+u+`": "yuu",
1997 "gamepadbutton10+gamepadbutton11": "yuu",
2001 resize: function () {
2002 var vp
= new yuu
.AABB(-0.6, -0.6, 0.6, 0.6)
2003 .matchAspectRatio(yuu
.viewport
);
2004 this.layer0
.resize(vp
.x0
, vp
.y0
, vp
.w
, vp
.h
);
2008 toBottom: function () {
2009 this.entity0
.attach(
2010 new yuu
.Animation(CIRCLE_TO_BOTTOM
, { $: this.ground
.transform
}));
2014 var rot1
= this.inner
.transform
.rotation
;
2015 quat
.rotateZ(rot1
, rot1
, Math
.PI
/ (2 * Math
.E
));
2016 var rot2
= quat
.rotateX(quat
.create(), rot1
, -Math
.PI
/ 2);
2017 quat
.rotateY(rot2
, rot2
, Math
.PI
/ 2);
2018 quat
.rotateX(rot2
, rot2
, -Math
.PI
/ 2);
2019 this.entity0
.attach(
2020 new yuu
.Animation(CIRCLE_INNER_WIND
, {
2021 $: this.inner
.transform
,
2028 score
.key
= this.score
&& this.score
.key
;
2032 _musicSchedule: function (count
) {
2033 var t
= yuu
.director
.currentAudioTime
;
2036 if (this._finished
) {
2037 if (this._finished
=== "won" && this.score
.key
) {
2038 var score
= yuu
.parseScore(
2040 "1 3 2 Z 0 { - 1 Z 2 Z 0",
2041 "1 2 3 Z 0 { - 1 Z 3 Z 0",
2042 "0 1 2 Z 4 { - 0 Z 2 Z 4",
2044 yuu
.Scales
.MAJOR
, this.score
.key
);
2045 while ((note
= score
.shift())) {
2046 sounds
.chime
.createSound(
2051 ).connect(this.music
);
2054 this._finished
= false;
2058 if (!(this.score
&& this.score
.length
)) {
2059 this.score
= generateScore();
2064 while (this.score
.length
&& this.score
[0].time
< this.playing
) {
2065 note
= this.score
.shift();
2066 sounds
.chime
.createSound(
2068 t
+ note
.time
% 1 + yuu
.random
.gauss(0, 0.015),
2071 ).connect(this.music
);
2074 if ((this.tension
*= 0.95) > 1) {
2076 sounds
.winding
.createSound(yuu
.audio
, t
, 0, 1.0, 1.0)
2077 .connect(this.music
);
2078 var flip
= !this.outer
.transform
.yaw
* yuu
.random
.randsign(Math
.PI
);
2079 this.entity0
.attach(
2080 new yuu
.Animation(OUTER_FLIP_TICK
, {
2081 $: this.outer
.transform
,
2085 [sounds
.tick
, sounds
.tock
][count
& 1]
2086 .createSound(yuu
.audio
, t
, 0, 0.5, 1.0)
2087 .connect(this.music
);
2090 this.clockTick(this.reversed
-- > 0 ? TICK_REV
: TICK_ROT
);
2095 clockTick: function (amount
, anim
) {
2096 var rot
= this.arm
.transform
.rotation
;
2097 quat
.multiply(rot
, rot
, amount
|| TICK_ROT
);
2098 this.arm
.attach(new yuu
.Animation(
2100 { $: this.arm
.transform
, rotation
: rot
}));
2103 toBack: function () {
2105 this.entity0
.attach(
2106 new yuu
.Animation(CIRCLE_TO_BACK
, { $: this.ground
.transform
}));
2110 new yuu
.Ticker(this._musicSchedule
.bind(this), 60));
2114 this._finished
= "won";
2116 this.entity0
.attach(
2117 new yuu
.Animation(FLASH
, { $: this.innerQuad
}),
2118 new yuu
.Animation(FLASH
, { $: this.rimQuad
}, null, 32),
2119 new yuu
.Animation(FLASH
, { $: this.outerQuad
}, null, 48)
2124 this._finished
= "lose";
2125 var rot1
= this.inner
.transform
.rotation
;
2126 quat
.rotateZ(rot1
, rot1
, -Math
.PI
/ Math
.E
);
2127 var rot2
= quat
.rotateZ(quat
.create(), rot1
, Math
.PI
/ Math
.E
);
2128 this.entity0
.attach(
2129 new yuu
.Animation(CIRCLE_INNER_RATCHET
, {
2130 $: this.inner
.transform
,
2134 sounds
.regear
.play();
2137 rotated: function () {
2138 this.tension
+= yuu
.random
.uniform(0.1);
2142 this.tension
+= yuu
.random
.uniform(0.2);
2145 reverse: function () {
2146 this.tension
-= yuu
.random
.uniform(0.1);
2147 this.reversed
= Math
.max(this.reversed
, 0) + 1;