Now recognizes when levels completed.
5 files changed, 61 insertions(+), 28 deletions(-)

A => build/a/Game Over Bad.mp3
A => build/a/Game Over Good.mp3
M build/index.html
M qc.rkt
M spec.org
A => build/a/Game Over Bad.mp3 +0 -0

        
A => build/a/Game Over Good.mp3 +0 -0

        
M build/index.html +8 -6
@@ 23,13 23,13 @@ function draw_path(cx,points){"( cx (poi
 var sprites=(new Image());
 ((sprites)["src"]="i/sprites-cat.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")),king:(make_sound("a/hall-of-the-mountain-king-by-kevin-macleod-from-filmmusic-io.mp3"))};
+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]));};
 function all_stop(){return ((() => {var a=((Object.values)(Sounds)),n=a.length,i=0;while((true&&true&&(i<n))){(((s) => {((s.pause)());((s)["currentTime"]=0);return (i+=1);})(a[i]));};undefined;return undefined;})());};
 function arrange(rack,run,origin){"( rack run origin -- (p0 p1 p2 ...)) Compute tile coordinates for rack locations.";return (((x) => {return (((y) => {return ((() => {return (((a) => {{var i=0,t=rack.length;while((true&&true&&(i<t))){(((i_1) => {(((zero_p((i_1%run)))===false)?(x=(x+32)):((x=(first(origin))),(y=(y+48))));((a.push)([x,y]));return (i+=1);})(i));};undefined;}return a;})([]));})());})((second(origin))));})((first(origin))));};
-function make_level(name,layout,run,base,turned,start){"( name layout run  base turned  start -- level) Create level.";return (((tiles) => {return {name:name,layout:layout,tiles:tiles,tmobs:(adjust(tiles,[-32,-96])),run:run,base:base,turn:turned,start:start,tops:(((a) => {{var a_2=layout,n=a_2.length,i=0;while((true&&true&&(i<n))){(((x) => {((a.push)((x-1)));return (i+=1);})(a_2[i]));};undefined;}return a;})([])),done:turned.length,state:"play"};})((arrange(layout,run,[66,32]))));};
-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,1,0,1,0,1,0,1,0]};
-var Levels=[(make_level("Gentle Demo",Layouts.demo,3,["#ffc2c7","#b6e5d8"],["#8fdde7","#ffff00"],1)),(make_level("Gentle 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))];
+function make_level(name,layout,run,base,turned,start,method){"( name layout run  base turned  start /method/ -- level) Create level.";return (((offset) => {return (((tiles) => {return ((() => {return {name:name,layout:layout,tiles:tiles,tmobs:(adjust(tiles,[-32,-96])),run:run,base:base,turn:turned,start:start,tops:(((a) => {{var a_2=layout,n=a_2.length,i=0;while((true&&true&&(i<n))){(((x) => {((a.push)((x-1)));return (i+=1);})(a_2[i]));};undefined;}return a;})([])),done:(turned.length-1),method:(method||"advance"),state:"play",tick:0};})());})((arrange(layout,run,offset))));})([((APP.screen.width-(32*run))/2),(APP.screen.height-16-(48*(layout.length/run)))]));};
+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));};
 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]]}};

          
@@ 38,7 38,9 @@ function draw_tile_mini(cx,offset,top,le
 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 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"));return p;};
+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 frame_get(m,n){"( m /n/ -- (s d)|f|state) Get next frame from current animation.";return m.anim[m.state][(n||m.frame)];};

          
@@ 50,7 52,7 @@ function dir2loc(start,d){"( start d -> 
 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);(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) => {return (((idle) => {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(2));(((canvas) => {return ((() => {return ((Game)["cx"]=((canvas["getContext"])("2d")));})());})(((document["getElementById"])("the-canvas"))));(play("king"));return ((window["requestAnimationFrame"])(game_loop));};
+function init(){(level_set(0));(((canvas) => {return ((() => {return ((Game)["cx"]=((canvas["getContext"])("2d")));})());})(((document["getElementById"])("the-canvas"))));(play("king"));return ((window["requestAnimationFrame"])(game_loop));};
 (init());
 </script>
 </body>

          
M qc.rkt +45 -16
@@ 6,7 6,7 @@ 
               (nickname "qube-cat")
               (description "Cat on a hot, cubular roof!")
               (author "oofoe@cjmunday.com")
-              (version "0.9.0")
+              (version "0.9.1")
               (license "GPL-3.0-or-later")))
 
 ; Utilities

          
@@ 99,6 99,8 @@ 
      (object [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")]
              ))
    (define (play what) (let ([s (ref Sounds what)]) (:= s.currentTime 0) (s.play)))

          
@@ 119,10 121,14 @@ 
          (list x y))
        ))
 
-   (define (make-level name  layout run  base turned  start)
-     "( name layout run  base turned  start -- level) Create level."
+   (define (make-level name  layout run  base turned  start  method)
+     "( name layout run  base turned  start /method/ -- level) Create level."
 
-     (let ([tiles (arrange layout run '(66 32))])
+     (let* ([offset (list (/ (- APP.screen.width (* 32 run)) 2)
+                          (- APP.screen.height 16 (* 48 (/ layout.length run))))]
+            ; [tiles (arrange layout run '(66 32))]
+            [tiles (arrange layout run offset)]
+            )
        (object [name name]
                [layout layout]
                [tiles  tiles]

          
@@ 134,9 140,11 @@ 
                [start start] ; Where player should start.
 
                [tops  (for/array ([x in-array layout]) (- x 1))]
-               [done  turned.length] ; All tops must be this value to clear.
+               [done  (- turned.length 1)] ; All tops must be this value to clear.
+               [method (or method 'advance)] ; How to handle landing on qube: advance or toggle
 
-               [state 'play] ; play, win, lose, wait
+               [state 'play] ; play, win, lose, wait?
+               [tick 0]
                )))
 
    (define Layouts

          
@@ 149,14 157,16 @@ 
                      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 1 0
-                     1 0 1
-                     0 1 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)]))
    
    (define Levels
-     (list (make-level "Gentle Demo" Layouts.demo 3
-                       '("#ffc2c7" "#b6e5d8") '("#8fdde7" "#ffff00")  1)
-           (make-level "Gentle Breeze" Layouts.full 15
+     (list (make-level "Gentle Demo" Layouts.demo 7
+                       '("#ffc2c7" "#b6e5d8") '("#8fdde7" "#ffff00")  10)
+           (make-level "Sweet Breeze" Layouts.full 15
                        '("#ffc2c7" "#b6e5d8") '("#8fdde7" "#ffff00")  22)
            ; "Light Rain" should have toggle semantic on cubes.
            (make-level "Light Rain" Layouts.full 15

          
@@ 326,6 336,25 @@ 
 
    ; Animation
 
+   (define (level-winning level)
+     "( level --) Level has been completed."
+
+     (:= level.state 'win)
+     (all-stop) ; Kill all audio.
+     (play 'win)
+     )
+
+   (define (level-done? level)
+     "( level -- f) Is level completed (all cubes turned correct colour)?"
+
+     (= 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 (player-landing p target)
      "( p target -- p) Land player p on target."

          
@@ 337,7 366,9 @@ 
      (let ([tops Game.level.tops]) ; Have to do this for syntax reasons...
        (:= tops p.i 1))
      (play 'ponk)
-
+     (when (level-done? Game.level)
+       (level-winning Game.level))
+     
      p)
 
    (define (player-falling p start)

          
@@ 485,9 516,7 @@ 
    ; Start the Show...
      
    (define (init)
-     (level-set 2)
-     ;; (:= Game.level (ref Levels 2))
-     ;; (player-dropping player Game.level.start)
+     (level-set 1)
      
      (let* ((canvas (document.getElementById "the-canvas")))
        (:= Game.cx (canvas.getContext "2d")))

          
M spec.org +8 -6
@@ 113,7 113,7 @@ 
 
   itch.io full-screen play not supported at this time.
 
-* Tasks [53%]
+* Tasks [58%]
   - [X] Get animation working.
   - [X] Get transfer between blocks working.
   - [X] Add guard to layout.

          
@@ 135,19 135,21 @@ 
   - [X] HUD qube change guide.
   - [X] Music
   - [X] Build yarnball.
+  - [ ] Get yarnball drop working.
+  - [ ] Randomly generated levels.
+  - [ ] Cat chases yarnball off pyramid on collission.
+  - [ ] Countdown clock.
   - [ ] When cat returns from fall, choose sw or se orientation.
-  - [ ] Level states.
-  - [ ] Figure out when done.
+  - [X] Level states.
+  - [X] Figure out when done.
   - [ ] Reward on end of level?
-  - [ ] Get yarnball drop working.
   - [ ] Build breakables.
   - [ ] Get breakables working.
   - [X] Level colour schemes.
   - [ ] Level intro screens.
-  - [ ] Levels
+  - [X] Levels
   - [ ] Title
   - [ ] Logo
-  - [ ] Countdown clock.
   - [ ] Credits
   - [ ] Figure out portal.
   - [ ] PTIN