diff options
| author | LLLL Colonq <llll@colonq> | 2026-07-14 14:20:41 -0400 |
|---|---|---|
| committer | LLLL Colonq <llll@colonq> | 2026-07-14 14:20:41 -0400 |
| commit | 951b316773265052fbc5fa546bbcf31bef277062 (patch) | |
| tree | 139ec93946d7103f5ae102bd15f964d8d977dbae /2026/games/yukievt/index.html | |
| parent | 9f8c3904e77f3a6a8a341542665c1988c8582930 (diff) | |
Update
Diffstat (limited to '2026/games/yukievt/index.html')
| -rw-r--r-- | 2026/games/yukievt/index.html | 652 |
1 files changed, 652 insertions, 0 deletions
diff --git a/2026/games/yukievt/index.html b/2026/games/yukievt/index.html new file mode 100644 index 0000000..fd23d5a --- /dev/null +++ b/2026/games/yukievt/index.html @@ -0,0 +1,652 @@ +<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+ <title>Pat The Long Yooks (YukieVT's entry for the LCOLONQ MICROJAM 2K26-A-PALOOZA)</title>
+ <style>
+ * {
+ margin: 0px;
+ padding: 0px;
+ }
+ body {
+ font-size: 10px;
+ font-family: monospace;
+ }
+ /* Stretching to fill screen while preserving aspect ratio from:
+ https://stackoverflow.com/questions/67758479/how-to-fit-html-canvas-to-screen-while-maintaining-aspect-ratio-and-not-cutting
+ */
+ .screen {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ #canvas {
+ object-fit: contain;
+ width: 100%;
+ height: 100%;
+ image-rendering: pixelated; */ From https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes */
+ }
+ </style>
+
+ <script type="module">
+ window.addEventListener("message", ev => {
+ switch (ev.data.op) {
+ case "start":
+ Difficulty = ev.data.difficulty
+ DoStart();
+ break;
+ }
+ });
+ /* window.addEventListener("click", ev => {
+ window.parent.postMessage({op: "done", win: true});
+ });*/
+ window.onload = DoInit;
+ </script>
+
+ </head>
+ <body>
+ <div class="screen">
+ <canvas id="canvas" width=240 height=160></canvas>
+ </div>
+
+ <script>
+ function ShowTextOnly(Text, Colour) {
+ ctx.fillStyle = "#000000";
+ ctx.fillRect(0, 0, 240, 160);
+ ctx.fillStyle = Colour;
+ ctx.fillText(Text, 10, 10);
+ }
+
+ function DoInit() {
+ canvas = document.getElementById("canvas");
+ ctx = canvas.getContext("2d", {alpha: false});
+ FPS = 60.0;
+ FrameInterval = 1000 / FPS;
+
+ //Audio:
+ ShowTextOnly("Loading sounds...", "#0000FF");
+ audTicks = [LoadAud("TickL.wav"), LoadAud("TickH.wav")];
+ audTimerUp = LoadAud("TimerUp.ogg");
+ audHeartPop = LoadAud("HeartPop.wav");
+ audPat = LoadAud("Pat.ogg");
+ audHappy = LoadAud("Happy.ogg");
+ audSad = LoadAud("Sad.ogg");
+ audSuccess = LoadAud("Success.ogg");
+ audFailure = LoadAud("Failure.ogg");
+ audRustleInL = LoadAud("RustleInL.ogg");
+ audRustleInR = LoadAud("RustleInR.ogg");
+ audRustleOutL = LoadAud("RustleOutL.ogg");
+ audRustleOutR = LoadAud("RustleOutR.ogg");
+
+ //Images (ii = Image Index (index in Imgs[])):
+ iiYookL = 0
+ iiYookR = 1
+ iiYookLHappy = 2
+ iiYookRHappy = 3
+ iiHandL = 4
+ iiHandR = 5
+ iiHeartSmall = 6
+ iiMouseGuide = 7
+ iiTinyDark = 8
+ iiTinyLight1 = 9
+ iiTinyLight2 = 10
+ iiSuccess = 11
+ iiFailure = 12
+ iiPatAll = 13
+ iiPatNoL = 14
+ iiPatNoR = 15
+ iiTimer = 16
+ iiTimerUp = 17
+ iiFG = 18
+ iiBG = 19
+ iiNumTotal = 20
+ ImgFNs = [];
+ ImgFNs.length = iiNumTotal;
+ ImgFNs[iiYookL] = "YookL.png";
+ ImgFNs[iiYookR] = "YookR.png";
+ ImgFNs[iiYookLHappy] = "YookLHappy.png";
+ ImgFNs[iiYookRHappy] = "YookRHappy.png";
+ ImgFNs[iiHandL] = "HandL.png";
+ ImgFNs[iiHandR] = "HandR.png";
+ ImgFNs[iiHeartSmall] = "HeartSmall.png";
+ ImgFNs[iiMouseGuide] = "MouseGuide.png";
+ ImgFNs[iiTinyDark] = "TinyDark.png";
+ ImgFNs[iiTinyLight1] = "TinyLight1.png";
+ ImgFNs[iiTinyLight2] = "TinyLight2.png";
+ ImgFNs[iiSuccess] = "Success.png";
+ ImgFNs[iiFailure] = "Failure.png";
+ ImgFNs[iiPatAll] = "PatAll.png";
+ ImgFNs[iiPatNoL] = "PatNoL.png";
+ ImgFNs[iiPatNoR] = "PatNoR.png";
+ ImgFNs[iiTimer] = "Timer.png";
+ ImgFNs[iiTimerUp] = "TimerUp.png";
+ ImgFNs[iiFG] = "FG.png";
+ ImgFNs[iiBG] = "BG.png";
+ NumImgsLoaded = 0;
+ console.log("iiNumTotal: " + iiNumTotal.toString() + ". ImgFNs: " + ImgFNs)
+
+ Imgs = [];
+ Imgs.length = iiNumTotal;
+ ShowTextOnly("Loading " + iiNumTotal + " images...", "#0000FF");
+ LoadNextImg();
+ }
+
+ function LoadAud(FNCur) {
+ audCur = new Audio(FNCur)
+ audCur.loop = false
+ audCur.muted = false
+ audCur.volume = 1.0
+ return audCur
+ }
+
+ function LoadNextImg() {
+ if (NumImgsLoaded > 0) {
+ //ctx.drawImage(Imgs[NumImgsLoaded - 1], (NumImgsLoaded - 1) * 10, 0);
+ }
+ if (NumImgsLoaded == iiNumTotal) {
+ InitFinished();
+ Difficulty = 0; //Used when debugging. Overwritten by the jam's e.g. {op: "start", difficulty: 42.0}.
+ //DoStart(); //FOR ACTUAL JAM WITH HARNESS, COMMENT THIS OUT! TO RUN ALONE, UNCOMMENT THIS.
+ return;
+ }
+ Imgs[NumImgsLoaded] = new Image();
+ Imgs[NumImgsLoaded].addEventListener("load", () => {
+ NumImgsLoaded += 1;
+ LoadNextImg();
+ });
+ Imgs[NumImgsLoaded].addEventListener("error", () => {
+ ctx.fillStyle = "#FF0000";
+ ctx.fillText("Failed to load image: " + ImgFNs[NumImgsLoaded], 10, 30);
+ });
+ Imgs[NumImgsLoaded].src = ImgFNs[NumImgsLoaded];
+ }
+
+ function InitFinished() {
+ ShowTextOnly("(Ready)", "#00FF00");
+ window.parent.postMessage({op: "ready"});
+ }
+
+
+
+ function onMouseDown(e) { //Based on https://stackoverflow.com/questions/2405771/is-right-click-a-javascript-event
+ console.log("onMouseDown, e: " + e)
+ switch (e.button) {
+ case 0:
+ LastClick = 0;
+ break;
+ case 2:
+ LastClick = 1;
+ break;
+ }
+ }
+ function ContextMenuBlocker(e) {
+ e.preventDefault();
+ }
+ function DisableContextMenu(DisableNow) {
+ if (DisableNow) {
+ document.addEventListener("contextmenu", ContextMenuBlocker);
+ } else {
+ document.removeEventListener("contextmenu", ContextMenuBlocker);
+ }
+ }
+
+ function AnimCreate(Img, Loop = false) {
+ return {Img: Img, KFs: [], IndKF: -1, IndFrame: 0, Loop: Loop, XFrom: 0, YFrom: 0, XCur: 0, YCur: 0};
+ }
+ function AnimAddKF(Anim, XNew, YNew, NumFrames = 0) {
+ Anim.KFs[Anim.KFs.length] = {X: XNew, Y: YNew, NumFrames: NumFrames};
+ }
+ function AnimDraw(Anim) { //Returns true if animation has finished playing (if it's not set to loop).
+ if (Anim.IndKF == -1) { //If starting animation for first time...
+ Anim.XFrom = Anim.KFs[0].X //Start moving from first frame's pos.
+ Anim.YFrom = Anim.KFs[0].Y
+ Anim.IndKF = 1; //Start moving towards second frame's pos.
+ }
+ if (Anim.IndKF >= Anim.KFs.length) { //If we've just reached the end of the animation...
+ if (Anim.Loop) { //If we should loop...
+ Anim.IndKF = 0; //Start moving towards first frame's pos
+ Anim.IndFrame = 0; //We're at the beginning of this new movement.
+ } else { //Else, if we should not loop...
+ return true; //Return that the animation has finished.
+ }
+ }
+
+ //Work out current position based on current keyframe and frame index, and draw the image at that position.
+ KFCur = Anim.KFs[Anim.IndKF]
+ Anim.XCur = Anim.XFrom + ((KFCur.X - Anim.XFrom) * (Anim.IndFrame / KFCur.NumFrames));
+ Anim.YCur = Anim.YFrom + ((KFCur.Y - Anim.YFrom) * (Anim.IndFrame / KFCur.NumFrames));
+ ctx.drawImage(Anim.Img, Math.round(Anim.XCur), Math.round(Anim.YCur));
+
+ //Next time, draw next frame. This may be the first frame in the next keyframe.
+ //If there is no next keyframe, that will be dealt with on the next call of this function, rather than
+ //reporting "end of animation" now, to give the just-drawn frame a chance to be displayed.
+ Anim.IndFrame ++;
+ if (Anim.IndFrame >= KFCur.NumFrames) {
+ Anim.XFrom = Anim.XCur //Next time, start a movement from the current pos (end of cur movement).
+ Anim.YFrom = Anim.YCur
+ Anim.IndKF ++; //Move on to (potentially-non-existent) next keyframe.
+ Anim.IndFrame = 0; //Next time will be the first frame of a new movement.
+ }
+ return false; //Return that the animation has not yet finished playing.
+ }
+
+ function ParticlesDraw() {
+ ctx.globalCompositeOperation = "lighter";
+ for (IndParticle = 0; IndParticle < NumParticlesMax; IndParticle ++) {
+ if (Particles[IndParticle].FramesLeft > 0) {
+ ctx.globalAlpha = Math.min(1.0, Particles[IndParticle].FramesLeft / 30.0);
+ ctx.drawImage(Imgs[iiHeartSmall], Particles[IndParticle].X, Particles[IndParticle].Y);
+ Particles[IndParticle].X += Particles[IndParticle].HSpeed;
+ Particles[IndParticle].Y += Particles[IndParticle].VSpeed;
+ Particles[IndParticle].HSpeed *= Particles[IndParticle].Momentum;
+ Particles[IndParticle].VSpeed *= Particles[IndParticle].Momentum;
+ Particles[IndParticle].FramesLeft --;
+ }
+ }
+ ctx.globalCompositeOperation = "source-over";
+ ctx.globalAlpha = 1.0;
+ }
+ function ParticleCreate(XNew, YNew, FramesLeftMin) {
+ Particles[IndParticleNext].FramesLeft = FramesLeftMin + (Math.random() * (FramesLeftMin * 0.5));
+ Particles[IndParticleNext].X = XNew;
+ Particles[IndParticleNext].Y = YNew;
+ Particles[IndParticleNext].HSpeed = -1.2 + (Math.random() * 2.4);
+ Particles[IndParticleNext].VSpeed = -0.8 + (Math.random() * -0.4);
+ IndParticleNext ++;
+ if (IndParticleNext == NumParticlesMax) {IndParticleNext = 0;}
+ }
+ function ParticlesCreateOnYook(ValYook, XYook, YYook, FramesLeftMin, NumParticlesNew) {
+ ParticleX = 0
+ if (ValYook == 0) {
+ ParticleX = XYook + 64;
+ } else {
+ ParticleX = XYook + 3;
+ }
+ for (IndRepeat = 0; IndRepeat < NumParticlesNew; IndRepeat ++) {
+ ParticleCreate(
+ ParticleX - 10 + (Math.random() * 20),
+ (YYook - 10) - 10 + (Math.random() * 20),
+ FramesLeftMin
+ );
+ }
+ }
+
+ function TiniesInit() {
+ TinyGap = 2;
+ TinyFrameInterval = 15;
+
+ TinyFramesLeft = TinyFrameInterval
+ TinyFramePolarity = 0;
+ TinyXInc = Imgs[iiTinyDark].width + TinyGap;
+ TinyTotalWidth = TinyXInc * NumYooks;
+ TinyFirstX = Math.floor((240 - TinyTotalWidth) / 2);
+ TinyY = 4
+ TinyXs = [];
+ for (IndTiny = 0; IndTiny < NumYooks; IndTiny ++) {
+ TinyXs[IndTiny] = TinyFirstX + (TinyXInc * IndTiny);
+ }
+ }
+
+ function TiniesDraw() {
+ for (IndTiny = 0; IndTiny < NumYooks; IndTiny ++) {
+ if (IndTiny < IndYook) {
+ ctx.drawImage(Imgs[iiTinyLight1 + TinyFramePolarity], TinyXs[IndTiny], TinyY);
+ } else {
+ ctx.drawImage(Imgs[iiTinyDark], TinyXs[IndTiny], TinyY);
+ }
+ }
+ if (TinyFramesLeft == 0) {
+ TinyFramePolarity = 1 - TinyFramePolarity;
+ TinyFramesLeft = TinyFrameInterval;
+ } else {
+ TinyFramesLeft --;
+ }
+ }
+
+ function TimerSetNumSecsMax(NewVal) {
+ TimerNumSecsMax = NewVal;
+ NumFramesPerTimerTile = (FPS * TimerNumSecsMax) / TimerNumTiles;
+ }
+ function TimerInit() {
+ TimerNumTiles = 8; //(Excluding an extra final "time has run out" frame in the image).
+ TimerTileWidth = Imgs[iiTimer].width / (TimerNumTiles + 1);
+ TimerTileHeight = Imgs[iiTimer].height;
+ TimerIndFrame = 0; //Increments while waiting for input. Used for input time limit.
+ TimerIndTilePrev = 0;
+
+ AnimTimerUp = AnimCreate(Imgs[iiTimerUp], true)
+ AnimAddKF(AnimTimerUp, 207, 0, 3); //Centre (usual position)
+ AnimAddKF(AnimTimerUp, 205, 0, 3); //Left
+ //AnimAddKF(AnimTimerUp, 207, 4, 2); //Centre
+ //AnimAddKF(AnimTimerUp, 209, 4, 2); //Right
+ }
+ function TimerDraw() {
+ TimerIndTile = Math.min(TimerNumTiles, Math.floor(TimerIndFrame / NumFramesPerTimerTile))
+ if (TimerIndTile < TimerNumTiles) {
+ ctx.drawImage(
+ Imgs[iiTimer],
+ TimerIndTile * TimerTileWidth, 0, TimerTileWidth, TimerTileHeight,
+ 216, 2, TimerTileWidth, TimerTileHeight
+ );
+ if (TimerIndTile != TimerIndTilePrev) {
+ if (TimerIndTile != 0) { //Don't play tick on frame index 0, because timer's not yet running (it's just rewound).
+ audTicks[TimerIndTile % 2].play(); //Play tick 0 on even numbers, tick 1 on odd numbers.
+ }
+ }
+ } else {
+ AnimDraw(AnimTimerUp);
+ if (TimerIndTile != TimerIndTilePrev) {audTimerUp.play();}
+ }
+ TimerIndTilePrev = TimerIndTile;
+ }
+
+ async function DoStart() {
+ window.parent.postMessage({op: "started", verb: "pat!"});
+ DisableContextMenu(true);
+
+ //Make an array of available particles.
+ NumParticlesMax = 20
+ Particles = [];
+ Particles.length = NumParticlesMax;
+ for (IndParticle = 0; IndParticle < NumParticlesMax; IndParticle ++) {
+ Particles[IndParticle] = {FramesLeft: 0, X: 0, Y: 0, HSpeed: 0, VSpeed: 0, Momentum: 0.97}
+ }
+ IndParticleNext = 0;
+
+ //Make an array of long yooks which can appear. 0 = on the left, 1 = on the right.
+ NumYooks = 3;
+ Yooks = [];
+ Yooks.length = NumYooks;
+ for (IndYook = 0; IndYook < NumYooks; IndYook ++) {
+ Yooks[IndYook] = Math.floor(Math.random() * 2);
+ }
+ IndYook = 0;
+ TiniesInit();
+ TimerInit();
+
+ //Different number of seconds on the timer for each yook that appears. The last one will be used if IndYook is past end of this array.
+ if (Difficulty >= 40) {
+ SecsPerYook = [2.00, 0.80, 0.50]
+ } else if (Difficulty >= 20) {
+ SecsPerYook = [2.50, 1.50, 0.80]
+ } else {
+ SecsPerYook = [3.00, 2.00, 1.50]
+ }
+
+ //Gameplay state:
+ staInitYook = 0
+ staAnimYookOn = 1
+ staInitInput = 2
+ staAwaitInput = 3
+ staInitHand = 4
+ staAnimHandOn = 5
+ staAnimHandPat = 6
+ staAnimHandOff = 7
+ staFailMain = 8
+ staFailDetail = 9
+ staAnimYookOff = 10
+ staSuccMain = 11
+ staSuccDetail = 12
+ staCur = staInitYook
+
+ AnimMouseBounce = AnimCreate(Imgs[iiMouseGuide], true);
+ AnimAddKF(AnimMouseBounce, 89, 92, 8);
+ AnimAddKF(AnimMouseBounce, 89, 96, 8);
+ SuccessHeartFrameInterval = 9;
+
+ GameLoop: while (true) {
+ //ctx.fillStyle = "#505050";
+ //ctx.fillRect(0, 0, 240, 160);
+ ctx.drawImage(Imgs[iiBG], 0, 0)
+
+ //If a long yook should be fully on the screen, draw it at its sitting position (happy, if current answer was correct and at least pat has begun).
+ if ((staCur > staAnimYookOn) && (staCur < staAnimYookOff)) {
+ IsHappy = (LastClick == ValYook) && (staCur >= staAnimHandPat)
+ ctx.drawImage((IsHappy ? Imgs[iiYookLHappy + ValYook] : Imgs[ValYook]), PosYookSitX, PosYookSitY);
+ ctx.drawImage(Imgs[iiFG], 0, 0)
+ }
+
+ switch (staCur) {
+ case staInitYook: //Prepare new long yook. Prepare "yook on/off" animations. Fall through to animating.
+ console.log("State: InitYook");
+ ValYook = Yooks[IndYook]; //IndYook has already been set to a new value either above or when patting a previous one.
+ TimerSetNumSecsMax(SecsPerYook[Math.min(SecsPerYook.length, IndYook)]) //Set how many secs are on the timer for patting this yook.
+ TimerIndFrame = 0; //Set timer back to 0 ASAP. A few breaks below might increment this, so it's reset later too.
+
+ AnimYookOn = AnimCreate(Imgs[ValYook]);
+ AnimYookOff = AnimCreate(Imgs[iiYookLHappy + ValYook]);
+ PosYookSitX = 0
+ PosYookSitY = 48
+ NumFramesYookOn = 10
+ NumFramesYookOff = 90
+
+ IndFrameMisc = 0 //For miscellaneous animations (like zooms) done in the state machine below.
+ NumFramesEndZoomDuring = 10
+ NumFramesEndZoomAfter = 40
+ NumFramesEndDetailWait = 100
+
+ if (ValYook == 0) {
+ PosYookSitX = 110 - Imgs[iiYookL].width;
+ AnimAddKF(AnimYookOn, 0 - Imgs[iiYookL].width, PosYookSitY);
+ AnimAddKF(AnimYookOff, PosYookSitX, PosYookSitY);
+ AnimAddKF(AnimYookOff, 240, PosYookSitY, NumFramesYookOff);
+ } else {
+ PosYookSitX = (240 - 110);
+ AnimAddKF(AnimYookOn, 240, PosYookSitY);
+ AnimAddKF(AnimYookOff, PosYookSitX, PosYookSitY);
+ AnimAddKF(AnimYookOff, -117, PosYookSitY, NumFramesYookOff);
+ }
+ AnimAddKF(AnimYookOn, PosYookSitX, PosYookSitY, NumFramesYookOn);
+
+ staCur = staAnimYookOn;
+ (ValYook ? audRustleInR : audRustleInL).play();
+
+ case staAnimYookOn: //Animate long yook sliding onto screen. Fall through to init input when done.
+ ctx.drawImage(Imgs[iiFG], 0, 0);
+ if (AnimDraw(AnimYookOn)) {
+ staCur = staInitInput;
+ } else {
+ break;
+ }
+
+
+ case staInitInput: //Forget any previous mouse click. Fall through to awaiting input.
+ console.log("State: InitInput");
+ LastClick = -1;
+ //Start monitoring for mouse clicks.
+ //Clicked mouse button will be 0 for LMB or 1 for RMB. Any other button is ignored. Reset to -1 when we've used the value.
+ window.addEventListener("mousedown", onMouseDown);
+
+ TimerIndFrame = 0;
+ staCur = staAwaitInput;
+
+ case staAwaitInput: //Wait for L or R click while bouncing the mouse guide image. Fall through when answered.
+ AnimDraw(AnimMouseBounce);
+ if (LastClick != -1) { //If a mouse button was clicked...
+ window.removeEventListener("mousedown", onMouseDown); //Stop monitoring for mouse clicks, so that LastClick can't keep changing.
+ staCur = staInitHand;
+ }
+ if (TimerIndTile >= TimerNumTiles) { //If timer has run out...
+ window.removeEventListener("mousedown", onMouseDown); //Stop monitoring for mouse clicks here, too.
+ staCur = staFailMain;
+ } else {
+ TimerIndFrame ++;
+ }
+ break;
+
+ case staInitHand: //Set up "hand on/off" animations. Fall through to animating.
+ console.log("State: InitHand")
+ PosHandOffX = 0; //To be set below.
+ PosHandHoverX = 0; //To be set below.
+ PosHandOffY = -10;
+ PosHandHoverY = 25;
+ if (LastClick == 0) {
+ PosHandOffX = -117;
+ PosHandHoverX = -19;
+ } else {
+ PosHandOffX = 240;
+ PosHandHoverX = 259 - 117;
+ }
+ AnimHandOn = AnimCreate(Imgs[iiHandL + LastClick]);
+ AnimAddKF(AnimHandOn, PosHandOffX, PosHandOffY);
+ AnimAddKF(AnimHandOn, PosHandHoverX, PosHandHoverY, 15);
+ AnimHandOff = AnimCreate(AnimHandOn.Img);
+ AnimAddKF(AnimHandOff, PosHandHoverX, PosHandHoverY);
+ AnimAddKF(AnimHandOff, PosHandOffX, PosHandOffY, 10);
+ staCur = staAnimHandOn;
+
+ case staAnimHandOn: //Animate hand moving onto screen.
+ AnimHandPat = null;
+ if (AnimDraw(AnimHandOn)) { //If "hand on" animation has finished...
+ if (LastClick == ValYook) { //If mouse input matched yook...
+ //Prepare a "hand pat" animation.
+ AnimHandPat = AnimCreate(Imgs[iiHandL + LastClick]);
+ AnimAddKF(AnimHandPat, PosHandHoverX, PosHandHoverY); //Current (hover)
+ AnimAddKF(AnimHandPat, PosHandHoverX, PosHandHoverY, 8); //Initial delay (hover)
+ AnimAddKF(AnimHandPat, PosHandHoverX, PosHandHoverY + 8, 4); //Down quickly (touching)
+ AnimAddKF(AnimHandPat, PosHandHoverX, PosHandHoverY , 12); //Up slowly (hover)
+ AnimAddKF(AnimHandPat, PosHandHoverX, PosHandHoverY , 3); //Wait (hover)
+ AnimAddKF(AnimHandPat, PosHandHoverX, PosHandHoverY + 8, 4); //Down quickly (touching)
+ AnimAddKF(AnimHandPat, PosHandHoverX, PosHandHoverY , 12); //Up slowly (hover)
+ //AnimAddKF(AnimHandPat, PosHandHoverX, PosHandHoverY , 3); //Wait (hover)
+
+ //Make some heart particles (hearticles) now that the "hand pat" animation is about to begin.
+ ParticlesCreateOnYook(ValYook, PosYookSitX, PosYookSitY, 60, 5)
+
+ //Queue pat sounds to play in sync with the pat animation.
+ setTimeout(() => {audPat.play();}, 12 * FrameInterval);
+ setTimeout(() => {audPat.play();}, 31 * FrameInterval);
+ audHappy.playbackRate = 0.97 + (Math.random() * 0.06);
+ audHappy.preservesPitch = false;
+ audHappy.play();
+ IndYook ++; //Prepare to move on to next long yook now (causes another "tiny" to light up).
+ }
+ staCur = staAnimHandPat; //Even if "hand pat" anim wasn't made, fall through and attempt to animate it (will skip it).
+ } else { //Else, if animation hasn't finished yet...
+ break; //Wait another frame (ONLY if animation hasn't finished yet).
+ } //Ended if "hand on" animation has finished yet or not.
+
+ case staAnimHandPat: //Animate "hand pat", if one was prepared. If not, fall through to "hand off" animation.
+ if (AnimHandPat != null) {
+ if (AnimDraw(AnimHandPat)) {
+ staCur = staAnimHandOff;
+ } else {
+ break;
+ }
+ }
+
+ case staAnimHandOff: //Animate hand moving off the screen. Fall through to success (if patted) when done.
+ if (AnimDraw(AnimHandOff)) {
+ if (LastClick == ValYook) {
+ staCur = staAnimYookOff;
+ (ValYook ? audRustleOutR : audRustleOutL).play();
+ NumFramesUntilHeart = SuccessHeartFrameInterval;
+ } else {
+ staCur = staFailMain;
+ audFailure.play();
+ IndFrameMisc = 0;
+ break;
+ }
+ } else { //Else, if animation hasn't finished yet...
+ break; //Wait another frame.
+ }
+
+ case staAnimYookOff:
+ if (AnimDraw(AnimYookOff)) {
+ if (IndYook == NumYooks) {
+ staCur = staSuccMain;
+ audSuccess.play();
+ IndFrameMisc = 0;
+ } else {
+ staCur = staInitYook;
+ }
+ }
+ ctx.drawImage(Imgs[iiFG], 0, 0);
+ if (NumFramesUntilHeart == 0) {
+ ParticlesCreateOnYook(ValYook, AnimYookOff.XCur, AnimYookOff.YCur, 30, 1);
+ audHeartPop.volume = 0.1 + (Math.random() * 0.9);
+ audHeartPop.play();
+ NumFramesUntilHeart = SuccessHeartFrameInterval;
+ } else {
+ NumFramesUntilHeart --;
+ }
+ break;
+
+ case staSuccMain: //Animate zooming in of success image. After enough time, fall through to success detail.
+ ctx.drawImage(Imgs[iiFG], 0, 0);
+ if (IndFrameMisc < NumFramesEndZoomAfter) {
+ CurScale = Math.min(1.0, IndFrameMisc / NumFramesEndZoomDuring);
+ ctx.drawImage(Imgs[iiSuccess], (1 - CurScale) * 120, (1 - CurScale) * 80, 240 * CurScale, 160 * CurScale);
+ IndFrameMisc ++;
+ break;
+ } else {
+ IndFrameMisc = 0;
+ staCur = staSuccDetail;
+ }
+
+ case staSuccDetail: //Show the success detail for a while (and spawn hearts). Afterwards, end entire game.
+ ctx.drawImage(Imgs[iiFG], 0, 0);
+ ctx.drawImage(Imgs[iiSuccess], 0, 0);
+ ctx.drawImage(Imgs[iiPatAll], 0, 0);
+ if (IndFrameMisc < NumFramesEndDetailWait) {
+ IndFrameMisc ++;
+ } else {
+ break GameLoop;
+ }
+ if (NumFramesUntilHeart == 0) {
+ ParticleCreate(16 + (Math.random() * (240 - 32)), 16 + (Math.random() * (160 - 32)) , 20);
+ NumFramesUntilHeart = SuccessHeartFrameInterval;
+ } else {
+ NumFramesUntilHeart --;
+ }
+ break;
+
+ case staFailMain: //Animate zooming in of failure image. After enough time, fall through to failure detail.
+ if (IndFrameMisc < NumFramesEndZoomAfter) {
+ CurScale = Math.min(1.0, IndFrameMisc / NumFramesEndZoomDuring);
+ ctx.drawImage(Imgs[iiFailure], (1 - CurScale) * 120, (1 - CurScale) * 80, 240 * CurScale, 160 * CurScale);
+ IndFrameMisc ++;
+ break;
+ } else {
+ IndFrameMisc = 0;
+ staCur = staFailDetail;
+ audSad.play(); //Play a sad long yook voice sound.
+ }
+
+ case staFailDetail: //Show the failure detail for a while. Afterwards, end entire game.
+ ctx.drawImage(Imgs[iiFailure], 0, 0);
+ ctx.drawImage(Imgs[iiPatNoL + ValYook], 0, 0);
+ if (IndFrameMisc < NumFramesEndDetailWait) {
+ IndFrameMisc ++;
+ } else {
+ break GameLoop;
+ }
+ break;
+
+ default:
+ ctx.fillStyle = "#FF0000"
+ ctx.fillText("Unhandled state (" + staCur + ")!", 5, 10);
+ break;
+ } //Ended switch on current state machine state.
+
+ ParticlesDraw();
+ TiniesDraw();
+ if (staCur < staAnimYookOff) {TimerDraw();} //Draw timer if current long yook isn't moving off the screen yet, or on failure.
+ await new Promise(r => setTimeout(r, FrameInterval));
+
+ } //Ended outermost infinite game loop.
+
+ ctx.fillStyle = "#00FF00"
+ ctx.fillText("(Done)", 5, 10);
+ window.parent.postMessage({op: "done", win: (LastClick == ValYook)});
+ }
+ </script>
+
+ </body>
+</html>
\ No newline at end of file |
