Got yarnball rolling down pyramid mostly correctly.
2 files changed, 77 insertions(+), 44 deletions(-)

M build/index.html
M qc.rkt
M build/index.html +14 -12
@@ 1,13 1,13 @@ 
 <!DOCTYPE html>
 <html lang="en">
   <head> <meta charset="utf-8">
-  <title>Qube*Cat - 0.9.1 </title>
+  <title>Qube*Cat - 0.9.2 </title>
   <style>@font-face { font-family: HUD; src: url('f/ShareTechMono-Regular.ttf'); }</style>
  </head>
  <body style="margin: 0;">
     <div style="display: flex; justify-content: center;">
     <canvas id="the-canvas" width="640" height="480" tabindex="1"/>
-    <script>var VERSION = "0.9.1";</script>
+    <script>var VERSION = "0.9.2";</script>
     <script>"use strict";
 var APP={name:"Qube*Cat",nickname:"qube-cat",screen:{width:640,height:480},fps:30};
 function array_p(x){"( x -- f) Is x an Array?";return (x.constructor===Array);};

          
@@ 20,8 20,8 @@ function rest(a){return ((a.slice)(1));}
 function choose(a){"( a -- item) Choose random item from array a.";return a[((Math.floor)((a.length*((Math.random)()))))];};
 function adjust(vv,v){"( vv v -- vv') Apply correction v to vectors in vv.";return ((vv.map)(((x) => {return [((first(v))+(first(x))),((second(v))+(second(x)))];})));};
 function draw_path(cx,points){"( cx (point ...) --) Draw path into context with points.";((cx["beginPath"])());(((p) => {return ((cx["moveTo"])((first(p)),(second(p))));})((first(points))));{var a=(rest(points)),n=a.length,i=0;while((true&&true&&(i<n))){(((p) => {((cx["lineTo"])((first(p)),(second(p))));return (i+=1);})(a[i]));};undefined;}return ((cx["closePath"])());};
-var sprites=(new Image());
-((sprites)["src"]="i/sprites-cat.png");
+function make_spritesheet(path){return (((i) => {((i)["src"]=path);return i;})((new Image())));};
+var Sprites={cat:(make_spritesheet("i/sprites-cat.png")),mob:(make_spritesheet("i/sprites-mobs.png"))};
 function make_sound(path){return (((s) => {((s)["src"]=path);return s;})((new Audio())));};
 var Sounds={ponk:(make_sound("a/Interface Element 3.mp3")),aarg:(make_sound("a/Almost.mp3")),win:(make_sound("a/Game Over Good.mp3")),lose:(make_sound("a/Game Over Bad.mp3")),king:(make_sound("a/hall-of-the-mountain-king-by-kevin-macleod-from-filmmusic-io.mp3"))};
 function play(what){return (((s) => {((s)["currentTime"]=0);return ((s.play)());})(Sounds[what]));};

          
@@ 31,28 31,30 @@ function make_level(name,layout,run,base
 var Layouts={full:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,1,0,1,0,1,0,1,0,1,0,0,0,0,0,1,0,1,0,1,0,1,0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],demo:[0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,0]};
 var Levels=[(make_level("Gentle Demo",Layouts.demo,7,["#ffc2c7","#b6e5d8"],["#8fdde7","#ffff00"],10)),(make_level("Sweet Breeze",Layouts.full,15,["#ffc2c7","#b6e5d8"],["#8fdde7","#ffff00"],22)),(make_level("Light Rain",Layouts.full,15,["#eeb5eb","#c26dbc"],["#3cacae","#c8f4f9"],22)),(make_level("Thunder!",Layouts.full,15,["#000c66","#050a30"],["#0000ff","#ff3333","#7ec8e3"],22))];
 var Game={cx:false,timestamp_old:false,nlevel:0,level:false,clock:0,log:{fps:0}};
-function level_set(n,state){"( n /state/ --) Set (or reset) level.";((Game)["nlevel"]=n);((Game)["level"]=Levels[n]);((Game)["level"]["state"]=(state||Game.level.state));((Game)["clock"]=0);return (player_dropping(player,Game.level.start));};
+function level_set(n,state){"( n /state/ --) Set (or reset) level.";(all_stop());((yarnball)["state"]="HIDE");((Game)["nlevel"]=n);((Game)["level"]=Levels[n]);((Game)["level"]["state"]=(state||Game.level.state));((Game)["clock"]=0);return (mob_dropping(player,Game.level.start));};
 var player={i:22,t:-1,x:0,face:0,frame:0,tick:0,state:"IDLE",score:0,falls:0,anim:{IDLE:[[0,7],[1,8],0],IDL2:[[0,15],[0,15],0],SLEP:[[9,15],[10,15],0],JUMP:[[2,3],[3,3],[4,99]],PLAY:[],PTLI:[],PTLO:[],FALL:[[5,15],[6,15],[5,5],[6,5],2],DROP:[[6,5],[7,5],[8,99]]}};
+var yarnball={i:22,target:-1,x:0,frame:0,tick:0,state:"HIDE",anim:{IDLE:[[0,99]],JUMP:[[2,10],[1,10],[2,99]],DROP:[[0,10],[2,2],[0,99]]}};
 function draw_tile(cx,offset,top,left,right){"( cx  offset  top left right --) Draw block to jump on.";(cx["fillStyle"]=left);(draw_path(cx,(adjust([[0,16],[32,32],[32,64],[0,64]],offset))));((cx.fill)());(cx["fillStyle"]=right);(draw_path(cx,(adjust([[32,64],[32,32],[64,16],[64,64]],offset))));((cx.fill)());(cx["fillStyle"]=top);(draw_path(cx,(adjust([[0,16],[32,0],[64,16],[32,32]],offset))));return ((cx.fill)());};
 function draw_tile_mini(cx,offset,top,left,right){"( cx  offset  top left right --) Draw mini-block for HUD.";{var a=[[left,[[0,4],[8,8],[8,16],[0,12]]],[right,[[8,16],[8,8],[16,4],[16,12]]],[top,[[0,4],[8,0],[16,4],[8,8]]]],n=a.length,i=0;while((true&&true&&(i<n))){(((x) => {(cx["fillStyle"]=(first(x)));(draw_path(cx,(adjust((second(x)),offset))));((cx.fill)());return (i+=1);})(a[i]));};undefined;}(cx["strokeStyle"]="black");(draw_path(cx,(adjust([[0,4],[8,0],[16,4],[16,12],[8,16],[0,12]],offset))));return ((cx.stroke)());};
 function draw_hud(cx,game,player_3,colour){"( cx game player /colour/ --) Draw information overlay.";(colour=(colour||"black"));(cx["font"]="20px HUD");(cx["fillStyle"]=colour);((cx["fillText"])(("Score: "+player_3.score),32,40));((cx["fillText"])(("Falls: "+player_3.falls),32,60));((cx["fillText"])(game.level.name,32,128));{var i=0,t=game.level.turn.length;while((true&&true&&(i<t))){(((i_4) => {(draw_tile_mini(cx,[(32+(32*i_4)),(128+20)],game.level.turn[i_4],(first(game.level.base)),(second(game.level.base))));return (i+=1);})(i));};undefined;}return (((x,y) => {(cx["font"]="10px HUD");(cx["fillStyle"]=colour);((cx["fillText"])(("fps: "+game.log.fps),x,y));return ((cx["fillText"])(("v: "+VERSION),x,(y+10)));})((APP.screen.width-64),(APP.screen.height-26)));};
 function whereis(rack,mob){"( rack mob -- (x y)) Convert mob location on rack to screen coords.";return ((((undefined===mob.target)||(-1===mob.target))===false)?(((p) => {return (((t) => {return (((dx) => {return (((dy) => {return ((() => {return [((first(p))+(mob.x*dx)),((second(p))+(mob.x*dy))];})());})(((second(t))-(second(p)))));})(((first(t))-(first(p)))));})(rack[mob.target]));})(rack[mob.i])):rack[mob.i]);};
-function draw(screen,cx){"( screen cx --) Draw current scene.";((cx["clearRect"])(0,0,screen.width,screen.height));(cx["strokeStyle"]="red");((cx["strokeRect"])(0,0,screen.width,screen.height));(((base,turn) => {return ((() => {var i=0,t=Game.level.layout.length;while((true&&true&&(i<t))){(((i_5) => {(((1===Game.level.layout[i_5])===false)?undefined:((() => {return (draw_tile(cx,Game.level.tiles[i_5],turn[Game.level.tops[i_5]],(first(base)),(second(base))));})()));return (i+=1);})(i));};undefined;return undefined;})());})(Game.level.base,Game.level.turn));(((p) => {return (((f) => {return ((() => {((("FALL"===player.state)===false)?undefined:((() => {return (p=[(first(p)),((second(p))+(first(player.x)))]);})()));((("DROP"===player.state)===false)?undefined:((() => {return (p=[(first(p)),((second(p))-(first(player.x)))]);})()));return ((cx["drawImage"])(sprites,(128*(first(f))),(128*player.face),128,128,(first(p)),(second(p)),128,128));})());})((frame_get(player))));})((whereis(Game.level.tmobs,player))));return (draw_hud(cx,Game,player));};
+function draw(screen,cx){"( screen cx --) Draw current scene.";((cx["clearRect"])(0,0,screen.width,screen.height));(cx["strokeStyle"]="red");((cx["strokeRect"])(0,0,screen.width,screen.height));(((base,turn) => {return ((() => {var i=0,t=Game.level.layout.length;while((true&&true&&(i<t))){(((i_5) => {(((1===Game.level.layout[i_5])===false)?undefined:((() => {return (draw_tile(cx,Game.level.tiles[i_5],turn[Game.level.tops[i_5]],(first(base)),(second(base))));})()));return (i+=1);})(i));};undefined;return undefined;})());})(Game.level.base,Game.level.turn));(((p) => {return (((f) => {return ((() => {((("FALL"===player.state)===false)?undefined:((() => {return (p=[(first(p)),((second(p))+(first(player.x)))]);})()));((("DROP"===player.state)===false)?undefined:((() => {return (p=[(first(p)),((second(p))-(first(player.x)))]);})()));return ((cx["drawImage"])(Sprites.cat,(128*(first(f))),(128*player.face),128,128,(first(p)),(second(p)),128,128));})());})((frame_get(player))));})((whereis(Game.level.tmobs,player))));(((!("HIDE"===yarnball.state))===false)?undefined:((() => {return (((p) => {return ((() => {((("DROP"===yarnball.state)===false)?undefined:((() => {return (p=[(first(p)),((second(p))-(first(yarnball.x)))]);})()));return ((cx["drawImage"])(Sprites.mob,0,0,128,128,(first(p)),(second(p)),128,128));})());})((whereis(Game.level.tmobs,yarnball))));})()));return (draw_hud(cx,Game,player));};
 function level_winning(level){"( level --) Level has been completed.";((level)["state"]="win");(all_stop());return (play("win"));};
 function level_done_p(level){"( level -- f) Is level completed (all cubes turned correct colour)?";return (0===((level.tops.filter)(((x) => {return (!((x<0)||(level.done===x)));})))["length"]);};
-function player_landing(p,target){"( p target -- p) Land player p on target.";((p)["i"]=target);((p)["target"]=-1);(state_set(p,"IDLE"));(((tops) => {return (tops[p.i]=1);})(Game.level.tops));(play("ponk"));(((level_done_p(Game.level))===false)?undefined:((() => {return (level_winning(Game.level));})()));return p;};
-function player_falling(p,start){"( p start -- p) Set up player to start falling.";((p)["falls"]=(p.falls+1));((p)["i"]=start);((p)["target"]=-1);((p)["x"]=[0,1]);(state_set(p,"FALL"));(play("aarg"));return p;};
-function player_dropping(p,target){"( p target -- p) Set up to drop from height.";((p)["i"]=target);((p)["target"]=-1);((p)["x"]=[150,10]);(state_set(p,"DROP"));return p;};
+function mob_dropping(m,target){"( m target -- m) Set up to drop from height.";((m)["i"]=target);((m)["target"]=-1);((m)["x"]=[150,10]);return (state_set(m,"DROP"));};
+function player_landing(p,target){"( p target --) Land player p on target.";((p)["i"]=target);((p)["target"]=-1);(state_set(p,"IDLE"));(((tops) => {return (tops[p.i]=1);})(Game.level.tops));(play("ponk"));return (((level_done_p(Game.level))===false)?undefined:((() => {return (level_winning(Game.level));})()));};
+function player_falling(p,start){"( p start --) Set up player to start falling.";((p)["falls"]=(p.falls+1));((p)["i"]=start);((p)["target"]=-1);((p)["x"]=[0,1]);(state_set(p,"FALL"));return (play("aarg"));};
+function yarnball_landing(m,target){((m)["i"]=target);((m)["target"]=(target+Game.level.run+(choose([-1,1]))));((m)["x"]=0);return (state_set(m,"JUMP"));};
 function frame_get(m,n){"( m /n/ -- (s d)|f|state) Get next frame from current animation.";return m.anim[m.state][(n||m.frame)];};
 function frame_set(m,f){"( m f -- m) Set m.frame to f. Reset ticks.";((m)["frame"]=f);((m)["tick"]=0);return m;};
 function state_set(m,s){"( m s -- m) Set new state for mob.";((m)["state"]=s);return (frame_set(m,0));};
-function animate(){"( --) Handle animation loops.";((player)["tick"]=(player.tick+1));(((frame) => {return ((() => {return ((((second(frame))<player.tick)===false)?undefined:((() => {return (((fn) => {return (((next) => {return ((() => {return (((array_p(next))===false)?(((string_p(next))===false)?((() => {return (frame_set(player,next));})()):((() => {((player)["state"]=frame);return (frame_set(player,0));})())):((() => {return (frame_set(player,fn));})()));})());})((frame_get(player,fn))));})((player.frame+1)));})()));})());})((frame_get(player))));return ((("JUMP"===player.state)===false)?((("FALL"===player.state)===false)?((("DROP"===player.state)===false)?undefined:((() => {((player)["x"]=[((first(player.x))-(second(player.x))),(second(player.x))]);return (((zero_p((first(player.x))))===false)?undefined:((() => {return (player_landing(player,Game.level.start));})()));})())):((() => {((player)["x"]=[((first(player.x))+(second(player.x))),(2+(second(player.x)))]);return (((320<(first(player.x)))===false)?undefined:((() => {return (player_dropping(player,Game.level.start));})()));})())):((() => {((player)["x"]=(player.x+0.1));return (((1.0<=player.x)===false)?undefined:((() => {return (((zero_p(Game.level.layout[player.target]))===false)?(player_landing(player,player.target)):(player_falling(player,player.target)));})()));})()));};
+function animate(){"( --) Handle animation loops.";((player)["tick"]=(player.tick+1));(((frame) => {return ((() => {return ((((second(frame))<player.tick)===false)?undefined:((() => {return (((fn) => {return (((next) => {return ((() => {return (((array_p(next))===false)?(((string_p(next))===false)?((() => {return (frame_set(player,next));})()):((() => {((player)["state"]=frame);return (frame_set(player,0));})())):((() => {return (frame_set(player,fn));})()));})());})((frame_get(player,fn))));})((player.frame+1)));})()));})());})((frame_get(player))));((("JUMP"===player.state)===false)?((("FALL"===player.state)===false)?((("DROP"===player.state)===false)?undefined:((() => {((player)["x"]=[((first(player.x))-(second(player.x))),(second(player.x))]);return (((zero_p((first(player.x))))===false)?undefined:((() => {return (player_landing(player,Game.level.start));})()));})())):((() => {((player)["x"]=[((first(player.x))+(second(player.x))),(2+(second(player.x)))]);return (((320<(first(player.x)))===false)?undefined:((() => {return (mob_dropping(player,Game.level.start));})()));})())):((() => {((player)["x"]=(player.x+0.1));return (((1.0<=player.x)===false)?undefined:((() => {return (((zero_p(Game.level.layout[player.target]))===false)?(player_landing(player,player.target)):(player_falling(player,player.target)));})()));})()));((yarnball)["tick"]=(yarnball.tick+1));return (((("HIDE"===yarnball.state)&&(!(player.i===Game.level.start))&&(0.75>((Math.random)())))===false)?((("DROP"===yarnball.state)===false)?((("JUMP"===yarnball.state)===false)?undefined:((() => {((yarnball)["x"]=(yarnball.x+0.05));return (((1.0<=yarnball.x)===false)?undefined:((() => {return (((zero_p(Game.level.layout[yarnball.target]))===false)?(yarnball_landing(yarnball,yarnball.target)):(state_set(yarnball,"HIDE")));})()));})())):((() => {((yarnball)["x"]=[((first(yarnball.x))-(second(yarnball.x))),(second(yarnball.x))]);return (((zero_p((first(yarnball.x))))===false)?undefined:((() => {return (yarnball_landing(yarnball,Game.level.start));})()));})())):((() => {return (mob_dropping(yarnball,Game.level.start));})()));};
 var d2face={sw:0,se:1,ne:3,nw:2};
 function dir2loc(start,d){"( start d -> location) Figure out new location from start and direction.";return (((run) => {return ((("sw"===d)===false)?((("se"===d)===false)?((("ne"===d)===false)?((("nw"===d)===false)?undefined:((() => {return (start-run-(-1));})())):((() => {return (start-run-1);})())):((() => {return (start+run+1);})())):((() => {return (start+run+-1);})()));})(Game.level.run));};
 function jumping(mob,d){"( mob d --) Set mob up for jump.";((mob)["state"]="JUMP");((mob)["x"]=0.0);((mob)["target"]=(dir2loc(mob.i,d)));((mob)["face"]=d2face[d]);return (frame_set(mob,0));};
 function game_loop(timestamp){(((cx) => {return (((fps_limit) => {return (((delta) => {return (((seconds_passed) => {return (((fps) => {return ((() => {return (((delta>=fps_limit)===false)?undefined:((() => {((Game)["log"]["fps"]=fps);((Game)["timestamp_old"]=timestamp);((Game)["level"]["tick"]=(Game.level.tick+1));(draw(APP.screen,cx));return (animate());})()));})());})(((Math.round)((1.0/seconds_passed)))));})((delta/1000)));})((timestamp-Game["timestamp_old"])));})((1000/APP.fps)));})(Game.cx));return ((window["requestAnimationFrame"])(game_loop));};
-((window["addEventListener"])("keyup",((e) => {((console.log)(e.key));return (((idle) => {return ((("play"===Game.level.state)===false)?(((("win"===Game.level.state)||("wait"===Game.level.state))===false)?((("lose"===Game.level.state)===false)?undefined:((() => {return (((("Enter"===e.key)||(" "===e.key))===false)?undefined:((() => {return (level_set(Game.nlevel));})()));})())):((() => {return (((("Enter"===e.key)||(" "===e.key))===false)?undefined:((() => {return (level_set((Game.nlevel+1)));})()));})())):((() => {return (((idle&&("z"===e.key))===false)?(((idle&&("x"===e.key))===false)?(((idle&&("a"===e.key))===false)?(((idle&&("s"===e.key))===false)?undefined:((() => {return (jumping(player,"nw"));})())):((() => {return (jumping(player,"ne"));})())):((() => {return (jumping(player,"se"));})())):((() => {return (jumping(player,"sw"));})()));})()));})((("IDLE"===player.state)||("IDL2"===player.state)||("SLEP"===player.state))));})));
-function init(){(level_set(0));(((canvas) => {return ((() => {return ((Game)["cx"]=((canvas["getContext"])("2d")));})());})(((document["getElementById"])("the-canvas"))));(play("king"));return ((window["requestAnimationFrame"])(game_loop));};
+((window["addEventListener"])("keyup",((e) => {return (((idle) => {return ((("play"===Game.level.state)===false)?(((("win"===Game.level.state)||("wait"===Game.level.state))===false)?((("lose"===Game.level.state)===false)?undefined:((() => {return (((("Enter"===e.key)||(" "===e.key))===false)?undefined:((() => {return (level_set(Game.nlevel));})()));})())):((() => {return (((("Enter"===e.key)||(" "===e.key))===false)?undefined:((() => {return (level_set((Game.nlevel+1)));})()));})())):((() => {return (((idle&&("z"===e.key))===false)?(((idle&&("x"===e.key))===false)?(((idle&&("a"===e.key))===false)?(((idle&&("s"===e.key))===false)?undefined:((() => {return (jumping(player,"nw"));})())):((() => {return (jumping(player,"ne"));})())):((() => {return (jumping(player,"se"));})())):((() => {return (jumping(player,"sw"));})()));})()));})((("IDLE"===player.state)||("IDL2"===player.state)||("SLEP"===player.state))));})));
+function init(){(level_set(0));(((canvas) => {return ((() => {return ((Game)["cx"]=((canvas["getContext"])("2d")));})());})(((document["getElementById"])("the-canvas"))));return ((window["requestAnimationFrame"])(game_loop));};
 (init());
 </script>
 </body>

          
M qc.rkt +63 -32
@@ 21,7 21,7 @@ 
 
 ; Configure Urlang
 (current-urlang-run?                           #f) ; run using Node?              No: use browser
-(current-urlang-echo?                          #t) ; print generated JavaScript?  Yes
+(current-urlang-echo?                          #f) ; print generated JavaScript?  Yes
 (current-urlang-console.log-module-level-expr? #f) ; print top-level expression?  Yes (see console)
 (current-urlang-beautify?                      #f) ; invoke js-beautify
 

          
@@ 90,9 90,11 @@ 
 
    ; Note it's possible that these might not be loaded properly by the time
    ; the loop starts, but I'm going to ignore that for now.
-   
-   (define sprites (new Image))
-   (:= sprites.src "i/sprites-cat.png")
+
+   (define (make-spritesheet path) (let ([i (new Image)]) (:= i.src path) i))
+   (define Sprites
+     (object [cat (make-spritesheet "i/sprites-cat.png")]
+             [mob (make-spritesheet "i/sprites-mobs.png")]))
 
    (define (make-sound path)  (let ([s (new Audio)]) (:= s.src path) s))
    (define Sounds

          
@@ 189,12 191,14 @@ 
 
    (define (level-set n state)
      "( n /state/ --) Set (or reset) level."
-     
+
+     (all-stop)
+     (:= yarnball.state 'HIDE)
      (:= Game.nlevel n)
      (:= Game.level (ref Levels n))
      (:= Game.level.state (or state Game.level.state))
      (:= Game.clock 0)
-     (player-dropping player Game.level.start))
+     (mob-dropping player Game.level.start))
      
 
    (define player

          
@@ 222,7 226,7 @@ 
                            )]))
 
    (define yarnball
-     (object [i 22] [t -1] [x 0]
+     (object [i 22] [target -1] [x 0]
              [frame 0] [tick 0]
              [state 'HIDE]
              [anim (object [IDLE '((0 99))]

          
@@ 335,11 339,21 @@ 
          (:= p (list (first p) (- (second p) (first player.x)))))
        
        ; (cx.strokeRect (first p) (second p) 128 128) ; Debug.
-       (cx.drawImage sprites
+       (cx.drawImage Sprites.cat
                           (* 128 (first f)) (* 128 player.face)  128 128
                           (first p) (second p)                   128 128)
        )
 
+     ; Yarnball draw.
+     (when (not (= 'HIDE yarnball.state))
+       (let* ([p (whereis Game.level.tmobs yarnball)])
+         (when (= 'DROP yarnball.state)
+           (:= p (list (first p) (- (second p) (first yarnball.x)))))
+         (cx.drawImage Sprites.mob
+                       0 0 128 128
+                       (first p) (second p) 128 128)
+         ))
+
      (draw-hud cx Game player)
      )
 

          
@@ 360,14 374,16 @@ 
      (= 0 (ref (level.tops.filter (λ (x) (not (or (< x 0) (= level.done x)))))
                'length)))
 
-     ;; (let* ([only (level.tops.filter (λ (x) (>= 0 x)))]
-     ;;        [done (only.filter (λ (x) (= level.done x)))]
-     ;;        )
-     ;;   (= only.length done.length)))
 
-
+   (define (mob-dropping m target)
+     "( m target -- m) Set up to drop from height."
+     
+     (:= m.i target) (:= m.target -1)
+     (:= m.x '(150 10))
+     (state-set m 'DROP))
+   
    (define (player-landing p target)
-     "( p target -- p) Land player p on target."
+     "( p target --) Land player p on target."
 
      (:= p.i target)
      (:= p.target -1)

          
@@ 377,30 393,24 @@ 
        (:= tops p.i 1))
      (play 'ponk)
      (when (level-done? Game.level)
-       (level-winning Game.level))
-     
-     p)
+       (level-winning Game.level)))
 
    (define (player-falling p start)
-     "( p start -- p) Set up player to start falling."
+     "( p start --) Set up player to start falling."
 
      (:= p.falls (+ p.falls 1))
      (:= p.i start)
      (:= p.target -1)
      (:= p.x '(0 1)); Counter for -y offset.
      (state-set p 'FALL)
-     (play 'aarg)
-
-     p)
+     (play 'aarg))
 
-   (define (player-dropping p target)
-     "( p target -- p) Set up to drop from height."
+   (define (yarnball-landing m target)
+     (:= m.i target)
+     (:= m.target (+ target Game.level.run (choose '(-1 1))))
+     (:= m.x 0)
+     (state-set m 'JUMP))
 
-     (:= p.i target) (:= p.target -1)
-     (:= p.x '(150 10))
-     (state-set p 'DROP)
-     p)
-   
      
    (define (frame-get m n)
      "( m /n/ -- (s d)|f|state) Get next frame from current animation."

          
@@ 424,6 434,7 @@ 
    (define (animate)
      "( --) Handle animation loops."
 
+     ; Player
      (:= player.tick (+ player.tick 1))
 
      ; If the current frame has timed out, I get the next

          
@@ 449,13 460,34 @@ 
             (:= player.x (list (+ (first player.x) (second player.x))
                                (+ 2 (second player.x))))
             (when (< 320 (first player.x))
-              (player-dropping player Game.level.start))]
+              (mob-dropping player Game.level.start))]
            [(= 'DROP player.state)
             (:= player.x (list (- (first player.x) (second player.x))
                                (second player.x)))
             (when (zero? (first player.x)) (player-landing player Game.level.start))]
            )
 
+     ; Yarnball
+     (:= yarnball.tick (+ yarnball.tick 1))
+
+     (cond [(and (= 'HIDE yarnball.state)
+                 (not (= player.i Game.level.start))
+                 (> 0.75 (Math.random)))
+            (mob-dropping yarnball Game.level.start)]
+           [(= 'DROP yarnball.state)
+            (:= yarnball.x (list (- (first yarnball.x) (second yarnball.x))
+                                 (second yarnball.x)))
+            (when (zero? (first yarnball.x)) (yarnball-landing yarnball Game.level.start))
+            ]
+           [(= 'JUMP yarnball.state)
+            (:= yarnball.x (+ yarnball.x 0.05))
+            (when (<= 1.0 yarnball.x)
+              (if (zero? (ref Game.level.layout yarnball.target))
+                  (state-set yarnball 'HIDE)
+                  (yarnball-landing yarnball yarnball.target)))]
+           ; Could do a DROP state...
+           )
+
      ; ...more animations to follow...
      )
 

          
@@ 507,8 539,7 @@ 
    
    (window.addEventListener
     "keyup" (λ (e)
-
-              (console.log e.key)
+              ;; (console.log e.key) ; Debug.
               (let ([idle (or (= 'IDLE player.state)
                               (= 'IDL2 player.state)
                               (= 'SLEP player.state))]

          
@@ 537,7 568,7 @@ 
      (let* ((canvas (document.getElementById "the-canvas")))
        (:= Game.cx (canvas.getContext "2d")))
 
-     (play 'king)
+     ;; (play 'king) ;; Figure this out later...
      (window.requestAnimationFrame game-loop)
      )