// Trailblazer Prime
// v0.0.9 (beta)

// MacBernick (2013-2014)
// Lady Eklipse (2020)
// Komame (2026)

#pragma mode( separator(.,;) integer(h32) )

// General Settings
showFPS := 0;
minSpeed := 0.1;
maxSpeed := 0.6;
ballSpeed := 0.22;
accel := 0.001;
textureHeight := 16;
   
// Globals
trackMatrix, camPosition, ballPosition, ballHeight, tileSize, ballLimits, textureOffsetCoef, fps;
rayTable89, ballLaneWidth, jumpSpeed, jumps, lightsPos;
cRayTanDist, cRayLimitNear, cRayLimitFar; 
cOrange := #FFAA11, cBlue := #11AAFF;
jumping, starting, finish, falling;
startTimer, mainTimer, trackTimer, finishTime;
selectedTrack;

scLf := CHAR(10);
te_tck;

rayTable, xEnds, jumpTable, rayTexTable, fadeColors;
embossMatrix, embossRowHas, laneX, rayTanHalf, rayShiftCoef;

// Render strip cache: 60 visible strips instead of 120 rays
renderX1, renderX2, renderY1, renderY2, renderTex, renderShiftCoef;

shadowColorsTable;

frameShift60, lastViewShift, rayToStripIndex, embossTileRay, embossShiftIndex;


// Entry point
EXPORT trailblazer()
BEGIN
  LOCAL settings := {HSeparator, AAngle, AFormat, ADigits};
  LOCAL retMessage := "Thank you!";

  RECT_P(0);
  IFERR
    HSeparator := 0;
    AAngle := 2;
    AFormat := 2;
    ADigits := 2;
    preInit();

    WHILE 1 DO
      IF NOT gameMenu() THEN
        BREAK;
      END;
      init();
      mainLoop();
    END;
  THEN
    IF Ans <> 66 THEN
      retMessage := STRINGFROMID(654 + Ans);
	END;
  END;

  // Leaving Program
  MAKELIST(EXPR("DIMGROB(G" + STRING(X,1) + ",0,0)"),X,1,3);
  {'HSeparator', 'AAngle', 'AFormat', 'ADigits'} := settings;
  {
    'trackMatrix', 'te_tck', 'xEnds', 'rayTable', 'rayTexTable', 'jumpTable',
    'fadeColors', 'embossMatrix', 'embossRowHas', 'laneX', 'rayTanHalf',
    'rayShiftCoef', 'renderX1', 'renderX2', 'renderY1', 'renderY2',
    'renderTex', 'renderShiftCoef', 'frameShift60', 'lastViewShift',
    'rayToStripIndex', 'embossTileRay', 'shadowColorsTable', 'embossShiftIndex'
  } := MAKELIST(0,X,1,24);
  RETURN retMessage;
END;


// Menu
gameMenu()
BEGIN
  LOCAL play := 0, menuSel, quitGame := 0;
  WHILE NOT play DO
    RECT_P(0);
    RECT_P(0, 30, 319, 72, cOrange);

    TEXTOUT_P("TRAILBLAZER", 12, 32, 7, 0);
    TEXTOUT_P("-   P      R      I      M      E   -", 12, 59, 1, 0);
    TEXTOUT_P("TRAILBLAZER", 10, 30, 7, #FFFFFF);
    TEXTOUT_P("-   P      R      I      M      E   -", 10, 57, 1, #FFFFFF);

    TEXTOUT_P("track : " + selectedTrack, 166, 200, 3, cOrange);

    TEXTOUT_P("v0.0.9 (beta) by MacBernick and others (2013-2026)", 5, 228, 1, #555555);

    // display menu
    menuSel := selectList({"Play", "Select track", "Track editor", "Quit [Esc]"});

    CASE
      // Esc or Quit
      IF menuSel == -1 OR menuSel == 4 THEN
        quitGame := 1;
        BREAK;
      END;

      // Play
      IF menuSel == 1 THEN
        play := 1;
      END;

      // Select Track
      IF menuSel == 2 THEN
        LOCAL tracks := getDefaultTracks();
        LOCAL lst := MAKELIST(tracks(R, 2), R, 1, SIZE(tracks));
        LOCAL usrLst := getUserTracks();

        // concat default and user tracks lists
        lst := CONCAT(lst, MAKELIST(usrLst(R) + " : " + EXPR(usrLst(R) + "(2)"), R, 1, SIZE(usrLst)));

        LOCAL sel := selectList(lst);

        IF sel > 0 THEN // user selected a track
          IF sel <= SIZE(tracks) THEN // default tracks
            trackMatrix := tracks(sel, 4);
            selectedTrack := tracks(sel, 2);
          ELSE  // user tracks
            trackMatrix := EXPR(usrLst(sel - SIZE(tracks)) + "(4)");
            selectedTrack := EXPR(usrLst(sel - SIZE(tracks)) + "(2)")
          END;
        ELSE // user pressed Esc
          BREAK;
        END;
      END;

      // Track Editor
      IF menuSel == 3 THEN
        trackEdit();
      END;

    END;
  END;

  IF quitGame THEN RETURN 0; END;

  // start game
  RECT_P(0);
  TEXTOUT_P("Preparing Track...", 166, 200, 3, cOrange);
  RETURN 1;
END;


// Main Loop
mainLoop()
BEGIN
   LOCAL looping := 1, time := 0, frames := 0, revControls := 0, currentTile := 1, fallTime := 0;
   LOCAL kQuit, kUp, kDown, kLeft, kRight, kEnter;

   LOCAL currentSpeed := minSpeed, camEndPosition := (SIZE(trackMatrix) - 2) * tileSize;

   mainTimer := 0;
   starting := 1;
   finish := 0;
   finishTime := 0;

   LOCAL fpsmainTimer := TICKS;
   startTimer := fpsmainTimer;
   trackTimer := startTimer;

   WHILE looping DO
      time := TICKS;

      kQuit := ISKEYDOWN(4);
      kUp := ISKEYDOWN(2);
      kDown := ISKEYDOWN(12);
      kLeft := ISKEYDOWN(7);
      kRight := ISKEYDOWN(8);
      kEnter := ISKEYDOWN(30);

      IF kQuit THEN // Back to title screen
         looping := 0;
      END;

      IF NOT falling AND NOT starting AND NOT finish THEN

         // Keys

         // Calculate movement vector (1 for right, -1 for left, 0 for none)
         LOCAL moveDir := kRight - kLeft;
         
         IF revControls THEN 
            moveDir := -moveDir; 
         END;

         IF moveDir <> 0 THEN
            ballPosition := ballPosition + (moveDir * ballSpeed * mainTimer);
            // Clamp position
            IF ballPosition < 0 THEN ballPosition := 0; END;
            IF ballPosition > 294 THEN ballPosition := 294; END;
         END;

         IF (kUp OR currentTile == 3) AND currentTile <> 4 THEN // Speed Up
            currentSpeed := currentSpeed + accel * mainTimer;
            IF currentSpeed > maxSpeed THEN currentSpeed := maxSpeed; END;
         END;
         IF (kDown OR currentTile == 4) AND currentTile <> 3 THEN // Slow Down
            currentSpeed := currentSpeed - accel * mainTimer;
            IF currentSpeed < minSpeed THEN currentSpeed := minSpeed; END;
         END;
         IF NOT jumping AND kEnter AND jumps > 0 THEN // Jump
            jumping := 1;
            jumps := jumps - 1;
         END;

      ELSE

         IF NOT starting AND NOT finish THEN
            currentSpeed := currentSpeed - accel * mainTimer;
            IF currentSpeed < 0 THEN currentSpeed := 0; END;
            IF TICKS - fallTime >= 2000 THEN 
               falling := 0;
               jumping := 1;
               currentSpeed := 0.3;
            END;
         END;

      END;

      // Render Frame
      doRender();
      dashboard();

      // Display Frame
      BLIT(G1);

      // Move Camera
      IF NOT starting  AND NOT finish THEN
         camPosition := camPosition + currentSpeed * mainTimer;
         IF camPosition >= camEndPosition THEN 
            finish := 1;
            finishTime := TICKS - trackTimer;
         END;
      END;

      IF NOT jumping AND NOT falling THEN
         IF ballPosition >= ballLimits(1) AND ballPosition <= ballLimits(2) THEN
             currentTile := trackMatrix(
                IP( (rayTable89 + camPosition) / 64 + 1 ),
                IP( (ballPosition - ballLimits(1)) / ballLaneWidth + 1 ) );
         ELSE
            currentTile := 0;
         END;
      ELSE
         currentTile := 99;
      END;

      IF NOT finish THEN
         CASE
            IF currentTile == 5 AND NOT revControls THEN
               revControls := 1;
            END;
            IF (currentTile == 1 OR currentTile == 2) AND revControls THEN
               revControls := 0;
            END;
            IF (currentTile == 0) THEN
               falling := 1;
               fallTime := TICKS;
            END;
            IF currentTile == 6 THEN
               jumping := 1;
            END;
         END;
      ELSE
         IF NOT jumping THEN jumping := 1; END;       
      END;

      IF starting AND TICKS - startTimer >= 3000 THEN
         starting := 0;
         trackTimer := TICKS;
      END;

// Frame counter
IF showFPS THEN
   frames := frames + 1;

   LOCAL fpsNow := TICKS;
   LOCAL fpsElapsed := fpsNow - fpsmainTimer;

   IF fpsElapsed >= 1000 THEN
      fps := frames * 1000 / fpsElapsed;
      fpsmainTimer := fpsNow;
      frames := 0;
   END;
END;

      mainTimer := TICKS - time;
   END;
END;


// Updates lateral perspective shift only when visible integer ball X changes.
// Keeps the small 60-value cache, but avoids the huge 295x60 list.
updateFrameShift60(ballX)
BEGIN
  LOCAL viewShift;

  // ballPosition is clamped to 0..294 in mainLoop().
  // Round to nearest visible pixel.
  viewShift := IP(ballX + 0.5) - 147;

  IF viewShift <> lastViewShift THEN
    frameShift60 := MAKELIST(viewShift * renderShiftCoef(R), R, 1, 60);
    lastViewShift := viewShift;
  END;
END;



// Render Frame	
doRender()
BEGIN
  LOCAL ballDrawX := 147;

  // Clear top part of frame
  RECT_P(G1, 0, 0, 319, 141, 0);

  // Display falling ball
  IF falling THEN
    BLIT_P(G1, ballDrawX, ballHeight, "ball");
	RECT_P(G1, ballDrawX, 141, ballDrawX + 26, 141 + ballHeight, #1110FF);

    ballHeight := ballHeight + 0.1 * mainTimer;
  END;   

  LOCAL camTex := camPosition / textureOffsetCoef;

// Update cached lateral shift only when visible ball X changes.
updateFrameShift60(ballPosition);

// Draw screen-space emboss/front edges.
// Uses global frameShift60 cache.
drawEmbossRects();

// Render Track using cached 60-strip tables
MAKELIST(
  BLIT_P(G1,
    renderX1(R) - frameShift60(R),
    renderY1(R),
    renderX2(R) - frameShift60(R),
    renderY2(R),
    G3,
    0,
    renderTex(R) + camTex,
    160,
    renderTex(R) + camTex + 1),
  R, 1, 60
);

  // Fade Out
  MAKELIST(RECT_P(G1, 0, R, 319, R, fadeColors(R - 19)), R, 20, 30);

  // Display NOT falling ball
  IF NOT falling THEN

    // Jumping ?
    IF jumping THEN
      jumping := jumping + jumpSpeed * mainTimer;

      IF jumping <= 61 THEN
        ballHeight := jumpTable(IP(jumping));
      ELSE
        jumping := 0;
        ballHeight := 85;
      END;
    END;

    // Ball and Shadow
    LOCAL shadowColor := #70000000h; // (112 << 24) 

    IF jumping > 0 AND jumping <= 61 THEN
      shadowColor := shadowColorsTable(IP(jumping));
    END;

    RECT_P(G1, ballDrawX,     110, ballDrawX + 26, 112, shadowColor);
    RECT_P(G1, ballDrawX + 5, 109, ballDrawX + 21, 113, shadowColor);
    BLIT_P(G1, ballDrawX, ballHeight, "ball");
  END;

  // Lights
  IF starting THEN
    LOCAL l1 := 0, l2 := 0, l3 := 0, startTime := TICKS - startTimer;

    IF startTime >= 750 THEN l1 := 14; END;
    IF startTime >= 1500 THEN l2 := 14; END;
    IF startTime >= 2250 THEN l3 := 14; END;

    BLIT_P(G1, 137, lightsPos, "lights", l1, 0, l1 + 14, 16, #FF00FF);
    BLIT_P(G1, 153, lightsPos, "lights", l2, 0, l2 + 14, 16, #FF00FF);
    BLIT_P(G1, 169, lightsPos, "lights", l3, 0, l3 + 14, 16, #FF00FF);
  ELSE
    IF lightsPos > -16 THEN
      BLIT_P(G1, 137, lightsPos, "lights", 28, 0, 42, 16, #FF00FF);
      BLIT_P(G1, 153, lightsPos, "lights", 28, 0, 42, 16, #FF00FF);
      BLIT_P(G1, 169, lightsPos, "lights", 28, 0, 42, 16, #FF00FF);
      lightsPos := lightsPos - 0.16 * mainTimer;
    END;
  END;

  // Finish HUD
  IF finish THEN
    TEXTOUT_P("Finish", G1, 10, 14, 3, #FFFFFF);
    //TEXTOUT_P("Time " + finishTime / 1000, G1, 10, 34, 1, #FFFFFF);
    TEXTOUT_P("[Esc] to continue", G1, 10, 34, 1, #FFAA11);
  END;

  // Display FPS
  IF showFPS THEN TEXTOUT_P("FPS: " + fps, G1, 2, 2, 1, #FF0000); END;
END;


// Dashboard Render
dashboard()
BEGIN
   LOCAL shownTime := 0;

   RECT_P(G1, 0, 141, 319, 144, #1110FF);
   RECT_P(G1, 8, 176, 120, 207, #1110FF);

   IF starting THEN
      shownTime := 0;
   ELSE
      IF finish THEN
         shownTime := finishTime;
      ELSE
         shownTime := TICKS - trackTimer;
      END;
   END;

   TEXTOUT_P("time : " + shownTime / 1000, G1, 10, 175, 3, #FFFF00);
   TEXTOUT_P("jumps : " + STRING(jumps,1,0), G1, 10, 190, 3, #FFFF00);
END;


// Selection List
selectList(lst)
BEGIN

   LOCAL r, lstOffset := 0, loop := 1, curs := 1, color, output;
   
   WHILE loop DO
      RECT_P(0, 92, 320, 194, 0);
      FOR r FROM 1 TO 6 DO
         IF r > SIZE(lst) THEN BREAK; END;
         IF r + lstOffset == curs THEN
            color := cBlue;
         ELSE
            color := cOrange;
         END;
         TEXTOUT_P(lst(r + lstOffset), 50, 75 + r * 17, 1, color, 200, 0);
      END;

      LOCAL key := 0;
      WHILE key <> -1 DO
         key := WAIT();
         IF key > -1 THEN BREAK; END;
      END;

      CASE
         IF key == 2 THEN
           curs := MAX(curs - 1, 1);
           IF curs - lstOffset < 1 THEN
              lstOffset := lstOffset - 1;
           END;
         END;
         IF key == 12 THEN
           curs := MIN(curs + 1, SIZE(lst));
           IF curs - lstOffset > 6 THEN
              lstOffset := lstOffset + 1;
           END;
         END;
         IF key == 30 THEN
           output := curs;
           loop := 0;
         END;
         IF key == 4 THEN
            output := -1;
            loop := 0;
         END;
      END;
      
   END;

   RETURN output;
END;


// fetch user track list in F0..F9
getUserTracks()
BEGIN
  LOCAL r, lst := {};

  FOR r FROM 0 TO 9 DO
    IF SIZE(EXPR("L" + r)) == 4 THEN 
      IF TYPE(EXPR("L" + r + "(1)")) == 1 THEN 
        IF EXPR("L" + r + "(1)")  == #5442544B THEN
          lst(0) := "L" + r;
        END;
      END;
    END;
  END;

  RETURN lst;
END;


// convert a track matrix into track list
tckMat2Lst()
BEGIN
  RETURN MAKELIST(
    MAKELIST(
      te_tck(R,S), 
      S, 1, 5), 
    R, 1, rowDim(te_tck));
END;


// convert a track list into track matrix
tckLst2Mat(lst)
BEGIN
  RETURN MAKEMAT(lst(I, J), SIZE(lst), 5);
END;


// get emboss type for track tile
trackEmbossAt(r, s)
BEGIN
  IF r == 1 OR trackMatrix(r - 1, s) <> 0 THEN
    RETURN 0;
  END;

  LOCAL tile := trackMatrix(r, s);

  IF tile == 1 OR tile == 5 OR tile == 6 THEN
    RETURN 1;
  END;

  IF tile == 2 OR tile == 3 OR tile == 4 THEN
    RETURN 2;
  END;

  RETURN 0;
END;


// Build emboss cache for current track.
// embossMatrix(r, s):
// 0 = no emboss
// 1 = normal/orange-like emboss
// 2 = blue/special-like emboss
//
// embossRowHas(r):
// 0 = this row has no emboss
// 1 = this row has at least one emboss segment
buildEmbossCache()
BEGIN
   LOCAL n := SIZE(trackMatrix);
   LOCAL r, s, tile, emboss;

   embossMatrix := MAKEMAT(0, n, 5);
   embossRowHas := MAKELIST(0, R, 1, n);

   FOR r FROM 2 TO n DO
      FOR s FROM 1 TO 5 DO
         emboss := 0;

         // Emboss exists only when tile r has a tile,
         // and the tile directly before it is a hole.
         IF trackMatrix(r - 1, s) == 0 THEN

            tile := trackMatrix(r, s);

            IF tile == 1 OR tile == 5 OR tile == 6 THEN
               emboss := 1;
            ELSE
               IF tile == 2 OR tile == 3 OR tile == 4 THEN
                  emboss := 2;
               END;
            END;

            IF emboss <> 0 THEN
               embossMatrix(r, s) := emboss;
               embossRowHas(r) := 1;
            END;
         END;
      END;
   END;
END;


// Draw emboss/front edges in screen space, not in track texture
drawEmbossRects()
BEGIN
  LOCAL nearDist := camPosition + cRayLimitNear;
  LOCAL farDist := camPosition + cRayLimitFar;

  LOCAL r0 := MAX(2, IP(nearDist / tileSize));
  LOCAL r1 := MIN(SIZE(trackMatrix), IP(farDist / tileSize) + 2);

  LOCAL r, s, d, ray, y, tileRay;
  LOCAL x1, x2, emboss, currentShift, rr;

  FOR r FROM r0 TO r1 DO
    IF embossRowHas(r) THEN
      // Edge between tile r and the hole before it
      d := (r - 1) * tileSize - camPosition;

      // INLINED screenRayForDistFast logic
      // Check boundaries using cached global constants
      IF d >= cRayLimitNear AND d <= cRayLimitFar THEN
        
        // Optimized math: cRayTanDist precalculated
        rr := (14400 - d * cRayTanDist) / (120 + d * rayTanHalf);
        ray := IP(rr);
        
        IF ray < rr THEN ray := ray + 1; END;
        IF ray < 1 THEN ray := 1; END;
        IF ray > 119 THEN ray := 119; END;

        y := ray + 20;


        // Match the same projection strip used by the track renderer.
        tileRay := embossTileRay(ray);
        // Cache the shift for the entire row.
        currentShift := frameShift60(embossShiftIndex(ray));

        // Render each block
        FOR s FROM 1 TO 5 DO
          emboss := embossMatrix(r, s);

          IF emboss <> 0 THEN
            // Math here is now just simple subtraction
            x1 := laneX(tileRay, s) - currentShift;
            x2 := laneX(tileRay, s + 1) - currentShift;

            BLIT_P(G1,
              x1, y,
              x2, y + 6,
              G4,
              (emboss - 1) * 32,
              0,
              emboss * 32,
              6);
          END;
        END;
      END;
    END;
  END;
END;


// Init some values and do some Math
init()
BEGIN

   RECT_P(G1,0);
   ballPosition := 147;
   ballHeight := 85;
   camPosition := 0;
   falling := 0;
   lightsPos := 40; 
   jumps := 5;
   jumping := 0;
   fps := 0;


   // Force frameShift60 rebuild on first rendered frame
   lastViewShift := 999999;

   buildEmbossCache();

   LOCAL r, s, tile, x, y;

   // Track Megagrob
   DIMGROB_P(G3, 160, SIZE(trackMatrix) * textureHeight, #FF000000);

   FOR r FROM 1 TO SIZE(trackMatrix) DO
    
      FOR s FROM 1 TO 5 DO

         tile := trackMatrix(r, s);

         IF tile > 0 THEN

            x := (s - 1) * 32;
            y := (r - 1) * textureHeight;

            // tile
            BLIT_P(G3, 
               x,
               y,
               "tiles",
               (tile - 1) * 32,
               0,
               tile * 32,
               textureHeight);

         END;

      END;

   END;
   RECT_P(G1, 0, 141, #1110FF);
END;


getDefaultTracks()
BEGIN
  RETURN {
   {0, "Tricky", {}, 
   {{1,1,1,1,1},{2,1,2,1,2},{1,1,1,1,1},{1,1,1,1,0},{0,1,1,1,0},{0,1,1,0,0},{0,0,1,0,0},{0,0,1,1,0},{0,1,1,1,0},{0,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{0,0,0,6,0},{0,0,0,0,0},{1,1,1,1,1},{1,1,1,1,1},{2,2,2,2,2},{6,0,0,0,0},{0,0,0,0,0},{1,1,1,1,1},{3,3,3,3,3},{3,3,3,3,3},{0,0,6,0,0},{0,0,0,0,0},{3,3,3,3,3},{3,3,3,3,3},{3,3,3,3,3},{6,0,0,0,0},{0,0,0,0,0},{1,1,1,1,1},{4,4,4,4,4},{0,4,0,4,0},{0,4,0,4,0},{4,4,4,4,4},{4,4,4,4,4},{3,4,3,4,3},{4,3,4,3,4},{3,4,3,4,3},{5,5,5,5,5},{5,5,5,5,5},{3,4,3,4,3},{3,4,3,4,3},{4,3,4,3,4},{4,3,4,3,4},{3,4,3,4,3},{3,0,3,0,3},{4,3,4,3,4},{4,3,0,3,4},{3,4,0,3,4},{3,4,0,3,4},{0,4,0,3,0},{0,4,0,3,0},{0,4,0,3,0},{0,4,0,3,0},{0,4,0,3,0},{0,0,0,3,0},{0,4,0,3,0},{0,4,1,3,0},{0,3,1,4,0},{0,3,0,4,0},{0,3,0,4,0},{0,3,0,4,0},{0,3,0,4,0},{0,3,0,4,0},{0,3,0,4,0},{0,3,0,4,0},{0,1,1,1,0},{0,1,0,1,0},{0,1,1,1,0},{1,1,1,1,1},{1,2,1,2,1},{2,1,2,1,2},{1,2,1,2,1},{2,1,2,1,2},{1,0,1,2,1},{2,1,2,1,2},{1,2,1,0,1},{2,1,2,1,2},{1,2,1,2,1},{0,1,0,1,0},{1,2,1,2,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{0,1,0,1,0},{0,1,0,1,0},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{2,2,2,2,2},{1,1,1,1,1},{2,2,2,2,2},{1,1,1,1,1},{2,0,6,0,2},{1,1,1,1,1},{0,0,0,0,0},{1,0,4,0,1},{1,0,4,0,1},{1,1,1,1,1},{1,1,1,1,1},{3,3,0,0,0},{3,3,0,0,0},{3,3,0,0,0},{3,3,3,3,3},{3,3,3,3,3},{3,3,3,3,3},{0,0,0,3,0},{0,0,0,3,0},{0,0,0,3,0},{3,3,3,3,0},{3,3,3,3,0},{3,3,3,3,0},{0,3,0,0,0},{0,1,0,0,0},{1,1,1,0,0},{1,1,1,0,0},{1,0,1,0,0},{1,0,1,0,0},{1,0,1,0,0},{1,0,1,0,0},{1,1,1,1,1}}
   }, 
   {0, "Hello, World !", {}, 
   {{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{2,1,2,1,2},{1,2,1,2,1},{2,1,2,1,2},{1,2,1,2,1},{1,1,2,1,1},{0,1,0,1,0},{2,2,1,2,2},{0,1,0,1,0},{1,1,2,1,1},{0,1,1,1,0},{0,0,1,0,0},{0,0,5,0,0},{0,0,0,0,0},{0,2,0,2,0},{0,0,0,0,0},{0,6,0,6,0},{0,0,0,0,0},{1,1,1,1,1},{1,0,1,0,1},{1,1,2,1,1},{1,0,1,0,1},{1,1,1,1,1},{0,5,0,5,0},{0,0,0,0,0},{0,0,5,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,0,0},{0,3,3,0,0},{0,3,0,0,0},{0,3,3,3,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,3,3,0},{0,0,0,0,0},{0,0,1,0,0},{0,0,0,0,0},{0,3,0,3,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,3,3,0},{0,0,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,3,3,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,3,3,0},{0,0,0,3,0},{0,0,0,3,0},{0,0,0,0,0},{2,1,2,1,2},{1,2,1,2,1},{2,1,2,1,2},{1,2,1,2,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{2,1,2,1,2},{1,2,1,2,1},{2,1,2,1,2},{1,2,1,2,1},{1,1,2,1,1},{0,1,0,1,0},{2,2,1,2,2},{0,1,0,1,0},{1,1,2,1,1},{0,1,1,1,0},{0,0,1,0,0},{0,0,5,0,0},{0,0,0,0,0},{0,2,0,2,0},{0,0,0,0,0},{0,6,0,6,0},{0,0,0,0,0},{1,1,1,1,1},{1,0,1,0,1},{1,1,2,1,1},{1,0,1,0,1},{1,1,1,1,1},{0,5,0,5,0},{0,0,0,0,0},{0,0,5,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,0,0},{0,3,3,0,0},{0,3,0,0,0},{0,3,3,3,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,3,3,0},{0,0,0,0,0},{0,0,1,0,0},{0,0,0,0,0},{0,3,0,3,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,0,3,0},{0,3,3,3,0},{0,0,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,3,3,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,3,0,0,0},{0,0,0,0,0},{0,3,3,3,0},{0,3,0,3,0},{0,3,3,3,0},{0,0,0,3,0},{0,0,0,3,0},{0,0,0,0,0},{2,1,2,1,2},{1,2,1,2,1},{2,1,2,1,2},{1,2,1,2,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1}}
   },
   {0, "Goodbye, World !", {}, 
   {{1,1,1,1,1},{1,1,1,1,1},{1,1,1,1,1},{1,1,2,1,1},{1,2,2,2,1},{2,2,2,2,2},{0,2,0,2,0},{2,2,2,2,2},{1,2,2,2,1},{1,1,2,1,1},{2,2,2,2,2},{1,2,2,2,1},{1,1,2,1,1},{2,2,2,2,2},{0,2,2,2,0},{0,0,2,0,0},{0,0,6,0,0},{0,0,0,0,0},{1,0,0,0,1},{1,0,1,0,1},{2,2,2,2,2},{2,2,2,2,2},{2,2,0,0,0},{2,2,0,0,0},{2,2,2,2,2},{2,2,2,2,2},{0,0,0,2,2},{0,0,0,2,2},{2,2,2,2,2},{2,2,2,2,2},{2,2,0,0,0},{2,2,0,0,0},{2,2,2,2,2},{2,2,2,2,2},{4,0,0,2,2},{4,0,0,2,2},{4,4,4,4,4},{4,4,4,4,4},{1,1,1,1,1},{0,0,6,0,0},{0,0,0,0,0},{2,2,0,1,1},{1,1,0,2,2},{2,2,0,1,1},{0,3,3,3,0},{0,4,3,4,0},{0,3,4,3,0},{0,4,3,4,0},{0,1,2,1,0},{0,6,0,0,0},{0,0,0,0,0},{1,1,1,1,1},{1,1,1,1,1},{2,1,2,1,2},{2,0,2,0,2},{1,2,1,2,1},{0,2,0,2,0},{2,1,2,1,2},{2,0,2,0,2},{1,1,2,1,1},{0,2,0,2,0},{1,2,1,2,1},{2,0,2,0,2},{2,2,1,2,2},{2,1,1,1,2},{1,1,1,1,1},{0,0,6,0,0},{0,0,0,0,0},{0,0,0,0,0},{1,1,1,1,1},{1,1,1,1,1},{0,1,1,1,0},{0,0,1,0,0},{0,0,1,0,0},{0,1,1,1,0},{1,1,0,1,1},{1,0,0,0,1},{1,1,1,1,1},{1,2,1,2,1},{1,0,1,0,1},{1,0,0,0,1},{1,0,0,0,0},{1,0,0,0,0},{1,1,0,0,0},{0,1,1,0,0},{0,0,1,1,0},{0,0,0,1,1},{0,0,0,0,6},{0,0,0,0,0},{0,0,0,0,0},{0,0,6,0,0},{0,0,6,0,0},{0,6,0,0,0},{0,0,0,0,0},{0,0,6,0,0},{0,0,6,0,0},{0,0,0,0,0},{0,0,0,0,0},{0,0,1,0,0},{0,0,1,2,0},{0,1,1,2,0},{0,1,0,2,0},{0,1,0,2,2},{0,1,0,0,2},{0,1,0,0,2},{0,0,0,0,2},{0,0,0,0,6},{0,0,0,0,0},{0,0,0,1,0},{0,0,0,1,0},{0,0,1,1,1},{0,0,1,0,1},{0,0,3,0,4},{0,0,3,0,4},{0,0,3,0,4},{0,0,3,0,4},{0,0,3,0,4},{0,0,3,0,4},{0,0,3,0,4},{0,0,3,0,4},{0,0,1,0,1},{0,0,6,0,6},{0,0,0,0,0},{0,0,0,0,0},{1,2,1,2,1},{1,0,1,0,1},{1,2,0,2,1},{1,0,1,0,1},{1,2,1,2,1}}
   }
  };
END;


// Pre init
preInit()
BEGIN
  LOCAL tracks := getDefaultTracks();

  trackMatrix := tracks(1,4);
  selectedTrack := tracks(1,2);

  tileSize := 64;
  textureOffsetCoef := textureHeight / 4;
  jumpSpeed := 0.15;

  LOCAL vFOV := 30;

  LOCAL obsDist := 120 / TAN(vFOV / 2);

  // Projection Tables
  LOCAL tanHalf := TAN(vFOV / 2);
  rayTanHalf := tanHalf;

  rayTable := MAKELIST(
    (28800 - 120 * (R + 120)) / ((R + 120) * tanHalf),
    R, 1, 120
  );

  rayTexTable := MAKELIST(
    rayTable(R) / textureOffsetCoef,
    R, 1, 120
  );

  xEnds := MAKEMAT(
    obsDist / (obsDist + rayTable(I)) * ((2 * J - 3) * 2.5 * tileSize) + 160,
    120, 2
  );
  laneX := MAKEMAT(
    xEnds(I, 1) + (J - 1) * (xEnds(I, 2) - xEnds(I, 1)) / 5,
    120, 6
  );

  // Perspective shift coefficient.
  // At ray 89 the shift equals ballPosition - 147.
  // Farther rows shift less, nearer rows shift more.
  rayShiftCoef := MAKELIST(
    (xEnds(R, 2) - xEnds(R, 1)) / (xEnds(89, 2) - xEnds(89, 1)),
    R, 1, 120
  );

  // Cache only the 60 rendered strips: rays 1,3,5,...,119
  renderX1 := MAKELIST(xEnds(2 * R - 1, 1), R, 1, 60);
  renderX2 := MAKELIST(xEnds(2 * R - 1, 2), R, 1, 60);
  renderY1 := MAKELIST(2 * R + 19, R, 1, 60); // ray + 20
  renderY2 := MAKELIST(2 * R + 21, R, 1, 60); // ray + 22
  renderTex := MAKELIST(rayTexTable(2 * R - 1), R, 1, 60);
  renderShiftCoef := MAKELIST(rayShiftCoef(2 * R - 1), R, 1, 60);

  // Used by emboss: maps ray 1..119 to render strip index 1..60
  rayToStripIndex := MAKELIST(IP((R + 1) / 2), R, 1, 119);

  // Used by emboss: maps raw ray to matching odd render ray.
  // Old runtime logic:
  // even ray -> ray - 1
  // odd ray  -> ray - 2
  // clamped to 1
  embossTileRay := MAKELIST(
    MAX(1, 2 * IP((R - 2) / 2) + 1),
    R, 1, 119
  );

  embossShiftIndex := MAKELIST(
    rayToStripIndex(embossTileRay(R)),
    R, 1, 119
  );

  // Initial shift cache
  frameShift60 := MAKELIST(0, R, 1, 60);
  lastViewShift := 999999;

  ballLimits := {xEnds(89, 1) - 13, xEnds(89, 2) - 13};

  rayTable89 := rayTable(89);
  ballLaneWidth := (ballLimits(2) - ballLimits(1)) / 5;

  // Jump height table
  jumpTable := MAKELIST((R²-60*R+1275)/15, R, 0, 60); // (1/15) * (X-30)^2 + 25

  shadowColorsTable := MAKELIST(BITSL(IP(112 + (85 - jumpTable(R)) * 1.2), 24), R, 1, 61);

  fadeColors := MAKELIST(
    RGB(0, 0, 0, IP(R * 255 / 10)),
    R, 0, 10
  );

  // Precalculated constants for ray math
  cRayTanDist := 120 * rayTanHalf;
  cRayLimitNear := rayTable(119);
  cRayLimitFar := rayTable(1);

  DIMGROB_P(G1, 320, 240);
  DIMGROB_P(G2, 192, 16);
  
  // Prerender emboss
  SUBGROB_P("tiles", 0, 5, 64, 11, G4);
  RECT_P(G4,#9FFFFFFF);
  //RECT_P(G4,0,0,31,3,#7F4FFF4F);
  //RECT_P(G4,32,0,63,3,#7FFF4F4F);
  MAKELIST(LINE_P(G4,0,X,63,X,RGB(0,0,0,211-(X+1)*22)),X,0,5);
END;


// GFX
//ICON ball 89504E470D0A1A0A0000000D494844520000001A0000001A0203000000D424A45D00000009504C5445FF00FFFFFFFF000000A8BFC50A0000000374524E53FF000044EEF0330000004A4944415478DA636060D05AD5C000044C53433340B46668681848606A68686802501848854630307082E83004AD0AA2431BE0F454309D40368D6E1EBA7D3077C0DC057727CCDD107F00007ADF34DBBB329ABD0000000049454E44AE426082;

ICON ball 89504E470D0A1A0A0000000D494844520000001A0000001A0203000000D424A45D00000009504C5445FF00FFFFFFFF606060C82704D50000000374524E53FF000044EEF0330000004A4944415478DA636060D05AD5C000044C53433340B46668681848606A68686802501848854630307082E83004AD0AA2431BE0F454309D40368D6E1EBA7D3077C0DC057727CCDD107F00007ADF34DBBB329ABD0000000049454E44AE426082;

ICON tiles 89504E470D0A1A0A0000000D49484452000000C000000010080600000040E27028000018CA494441547801A595097456E599C79F7BBF2F21620259D9570191B00B02B2888888A0888AADBBD3D6A5A3B6D66AED54ADD3EA8CD6D6766A15AB3DED6051B46E45DC8A8808B8A08445C322FB1E59432004C292E4DEF93DDFFF72AA3316E99873BEBCF7BEF75D9EE5BF04A93BCC82FEFC4E328BDE304B5D6F16AFE3F94DE60698FE0EF1DC95B9B98CDD197F6D16DEC07353D66EE37B2DBF8EFC363157CA9862CD2C9E3BF0FD43D6FE80F7171987F3AD8EB93D8C3BF8C56607979A3DB7C66C4227B31ABE3DBDD2AC75AED9396DCD1EE3DB3DC3CC166C365BC59EE68DCD86B5322B63EF19ADD99F369BB8C8EC8A93CD0A72CC26AFE09938D7559B7563DD74CECA6F64D6F244B32D356627E79BED3A68D683BB5E5EAC6F2378FE74AB596933B303E4B978A7D9E6FD66435A9A354EEBAE06E23CAF3DE71147B702B3A6D966733E33CB0ACD4612E79106B307169AFD6B4FB397D69A5D490C732ACC86B7D619AF6CD09E4B3A53136AF3C22A6228E4CE16CAE1D94FC8B79D59711E65D9A73CCB7799F56E4E8938FB5DE22B20D64F98EBDF5C79FC957B06F0FC48B9D96FCF35DBCEB7EDF4E160BD5917BECFDC623690EFB31857EF357B68A8D987DB947F7F726DCA79F3781F7B397750C3D4BFD3A329C4D3543D0AA84B4CBE1608024109EFE41C57AAAF41CBE4DB7EAD8BA979EA326AF52CD38E852CC613926F4B781EC1483F833EEA7DF40EEB6F676E35B8B8847DDC1F8EE11D2C18B5326A10AF17F6C2417C7F86F142F6F762AE8AFD4F2B4E2BE6471F523F63CDBDC9DDE41080A1683E9FC6F3BE9D678F817AD86E7E5D38DBF17D36BFF45B2CEACBE6295AE0800E4732AE1511FCF27823E3280EF9400DB31ACD19C50E005F4C43831E0A2C60AF55685D5CA15F502492658AC5DEA01B67FD81771A3577BE00DF92842703D8DE24D485040A29DE63049D4D72453CAFA189FF360440726F37C093972D902C21A1AB01DE13007A37C57A87FBFE7496D94A08531799756822B0859CB11C20BD4F7EA50522C92840B7F7B09E1D140EB23F2C07A8347FDF11B312F67C76C06CD14E91C8C9E7603F91E6AE259E1E00A1729700B6B356A45C02400EB1A617395711CF419E4B20E70C80702985DFC55C73CE5DB557F7FAD90F0D11604B89B39CFD9DC97FEB01D5A512C06E8014E95080EF409C1F41A816C433AE9F08F5BBB7CDDAE64A40A6019A31E4B5A946356AC4F77ECD34BF99B90B11BAFDAC2BCED138F07CCE1828409803967C82BE122723162B1428E3BFD1B3457F76C4D8B1FF62CB9A73B5C55B8413274A064BBC07053ACF49104D656C9790823A1ADFE2950948F3B4D7DA0863E74D196FCD9A35B3A14387DAB871E38E79FB8B2FBE68F3E6CDB3BABA3A7BF1DBCF5A04BE03F01138E8970B8771B9EE75AC075924173DC9C37031C5D917EF101BA3E9527EDB27C03B70336B08CA08343895F99962522621570F40644D440A0344E1453C7F9224078082215297FA5BC5D20504B1B65AA08A28FA46EE7A8FB9F3294AF742A9E102E2E905314214E74FCB05D4AAC322C50BE791347715D1D003F5526E3FE703CEE80AC1761E94223A805D1D4FE09CF7968B180E82D7364AA933CE831AD600BC614B044427C10D10FBAA191A9D0013970898B580E774147C0F712C0234E7D2CC6DEC5D53ADE712271971D7D68B08C580EB83DEE4C3FD4D5E07D0E4924BADFBD2E4DB01F06F00A2B1FFED0D8ABB6D9EC8D186B89F45252FEA24372924E63D9C772E24CF2B151E0FF0FDBA892298D7D2DDEE2CCEFDE3A78AC51DCCEB57433E79F46DEA02D5A4277329D687A709F4DEC3F06A29B6419AFA2B3F07F8141B438A9B6E756CFC371074C356FD8E12E2CDAB2D7A8AA3BAF35625750F7124839C11C40A5CC51B1277E9258770077102C41F9AFDE4C8DD367AF4681B366C981DCFDFF4E9D36DF9F2E57667C73B84DD2D7286788FDCCE453DE36AE49D4A7D478CF0E28734392E4B54FF6359980793610AC5358A1FCFE17980D8EBECB2952A563842CF19A08709DB297CF49C120D502B03B0D10B7A0FCF9385DD8E62971609E4B95902E6F88E52696FA683BE6F89C095C7F7D1DCDBB5506A76CD29A82D671C41E93B3615D0BCF9EE0A0E0607E96E00DAB32D405F233274E4DC4DBBA5D4EE180E0EBFD3DD02AE5AE72B507D08311DBBDFC5DA3100A55B9EEE680729B3D9D3B348EF0E52276E1F1AD882331691C3489A56CCB3F13DE2F76BE25F45C3BB7EC3EC1CFA37E73E1CA63377429E96ACDD0A4E2E4164A62012D5C4B7BE5A847C7CA954DD49E2E44D714E3EC4BF0B40DC7F95C07F427BF0C85D216BE64390B67345924ED4A205B1ADDDA3462F25DFA59512809D00705BADEAE7A2F108B5B16D89D0A5D4B7FA6B675BF42AE049D3886C589B453153003FCCFB6AF4851E6C0BED49F6457FF92E8A3BC1C2CB26A3702C391D683830DFE0F9DBECD92AD1CD801ED21A4784D428269F80DE5F9873B1F5EDDBD78A8A8A8E8B00B5B5B5565D5D6DAFD5BE62C128CE99AF7EB81B05F433AE11F6E2C5EE00EFF0006923C6D44DB2A0864719296EF491589BD9D83BB12B570AEC323C93719A981BB855952870778178BD8A192F90FD5820EBCBB0CE15FA5D8F923320D2A16952506F9C037220F76FE75B7B1A316713E0A189DF451D179409A4AEEE7B01F558E27BBB422A3C7FBB80ECC07485CC81B8D32062B702916147AD08E1AEBE728FD638515A00CC1D07A5969BF609386F6E363BE9764007A997AC305B86BBAC5CABD86F464167A2262DD977F1191084F98D3522D3FE3AB94F639AF818B9A4C9BF93BBD8C9668321FF4EE228C2692BB96F59954A32805C4B88791EF56A46CC0BE943E7A6722FAFC195807235F18E68A37D7301CAE3D7FE1DFC9FFF3BF4999CE03E3056474FC6F552D38F10DFDD90E67AFA3863B36AE244F05AB92B35E59E803853DFA73EF4A16EE43280D8D9B2E6E558DD200A1B641F17E88EF597F552A1D55DB29F265558EA7A54ABA970E6EA1C904B7C444299BA9167EA1D503373E3410C627E466E0B877C62BD7BF73EAEFB56AF5E6DDD6775B5987DE137397B3A673A775A26C0FF58621ED2C3945B91DB52003A6272379A1953C8F05BCCD526AEF001F3041902C486FF646C2F56B985782383114AC636CB49EC043944E6DC9C843869394600400284242C950A9C390FDEF0ED64985905B057E1125B3963FA06A9FF2E1A9F471C357552C78100BE4DAE14F1208D4EE3367D982BCC964B5413E78A4A11C9CF1BC4B7CDD4BE35E4DCC7193B6B45B6D751A26E85025D4CEE7D017CC09A4680A696DC6ABB01C604C067A258C5AC9DCF5DF4C73611FF82F7002F796633378A5C9F61EF3272DD8B720DC7412F1F6FD69B333AB615F8ED71B3F24AB95A7B54FB00206CC739BF2E138933206E902ACFA1710F70670BC0DA983C0FD4A92E0F5DF3E5E0F7BFA34EF016F75D4F6EC510D088EDF06111BE4F73E59E9D92AB847C6B0310EFBF32112BB05E7FA9C09FBA3D27D3B7E8F94BF956F8F5D05FBFD5523FFCBDA5463D60D1D4072D5E788DA5BE3951DF703A43C48281C4E318DAAD1E446F4B34A3D798C7390D61ECB2B8AB1516167EA50B9497975B595999BDD6E815A93E226B103B9CC0E88E88D3042D744FE6EEAC323122E69214F946332DA3E4BE2923990E76D4347425CF93553941BC40C158A9BCED4FC00E60E235ACBD4017454B38F34A1537FE50AC8BE6887911F10534EBAD2D02E9945552A5EAC302AC2BA203E2241A7B84449AD0B48246D493E77D803C8BA695A28CCBD99F45337321566DBD9E9F25BE8B4E325B03D886C2FA0ACEDB09914602D44F016140714F647D9AF1B935DA3B8ED8DF0178C3583F7F875CE3ECFBE40451F4C522BB2B7CDE198E2AFDA93DBEB82EA791C05FF26791EE85B5225B0ECFA585120FBF3BBF916274773BDC20020C071C35474468178147BEFD8FC1FFF9BFA34E30F5350980D7CCEFF09ADD3847F5E85B22E118458DB2BA0A88F5375088745B4B4D00FCC3890D41AABB8AE0C3265F8F00002B7B459E458868402DEB2EF044AB708276C21931069D187155A37F31C2122020316E1778AE6E40EE0288DBC2415FED02AEFE3DB67495885F2DD175A01BF958417296FF9C476028483FC1C28B5938CD322A1F8C362939008C6689416E49F13CCD05A85A4C21331675A2BE05FD12D6929B3B41ECC0F006172AC1CCE5FECDED67A3EECB240F23DF7F1895A4C6CB20D0119A3F9AA43FDA464F72A5CEFE6DC10EA9BD936124A05FC0FCFB5B05E431EDA5A4B3497202B14C5F2FC5730077C9D7DE6B7A996DA91208FFB649CA3D18D23561FC1504BEAAABE6D6A0B229803183FC3B71D70E8037F89E2F27C157FD1D05FF2588C1C3C3CC86E00C5B897B2F359EBF5D80FCC199CC11774E4A407757E841632A6ACC16ED52BE3DC9AF5FCFE303FFFF26C1F388CC5CEA340280572270ABF6C8013C37AF9DC730721C3D21DF863FEEB7F45DB916D3AF700CF93ECA9ADE12AF702823750DCEE5F0ED123B57EBF853E65A27B8A1C6D11B3CE334414F7D0B7D0E370DC74A58C3C1CCB3AFFEE6439635892639F98833C2788222DD1524187375C89C3F4602EC98FCF196BBECFEFBEF3F66EE0F3EF8A0FD34BC1366BBBA73E7F798A49ED16CCE59CCF3513ED3DB7088F7FB5B96B14A5764676006D814309E2D06C5EF2821B7A1B09F401B2F672E95106397C811004CDB2AA5774B0BDA49E9E37589636425A4384549BA530458F0ED8BA5889B08720FC55F572DB576C577203743199A37966AADE55B1FD4767BB55C631CF72FDC2ED51C859B6C867CFD51B8D534288FFD7B0F4BE96753BC43E456C079FB21E198BE66AF52DC7AF6354AE9DCC300BC2151E021AE929C57EBAE883B1EA4214D72A5DEFF0CF85B3E05D8C8B12D7B03EE3D813C5F5D2FC27A6EF9DC7990B85AE529F68DFCCA71A8BA486EF0E71566F75EF2CF81DFFFD2343924FF67A9D98D9BE5A6EE027EEE4AFA37B495DCB117A0FB0D750CBA73DFD42B2C75F3EF2DDEA03E06B8598C1005FD794718E255028DF731709083195767FF1E13A7E58954BE36B3C67B9D2BECB838BAFA1B6E6D0853F4CE55E07BA280CD59616709A51326730F73F127ECC1A10C729CF797F1F61FA7DF6F37DD74137704C7CC7DF0E0C1D663472F4BCD4ADBF24E4B33CE12E302410B91CF8538E8C85D2324C641EA16FE8D95727B90310DB0588964549A4699274253634FAE98719154DF0F8A6624A4C94ED815CB3E336ABF4F20B7B6627026005430AE11119CE16B20D3A10681FCADCD028C03CDF3745274A0B0FD69FE248872210599C49EEB68D83B14330C448CF5DCB3FD80C8D283C24FA22177A2AE6538D1FC1D728DE720DCC5586D1F8A5B4F6C5B6B33BCCF9CF121246A932B90F873161FF2C9B7905F19F15E09D1F65E737C4E7014FCD37FA2D8DC71AE2B15B13A13FFF235BA7337E45A4D3DCF22B60E9CFB5F08C7F88E2286EF190E48C78FFA72F0A71A4B80E2866422D67374E4CB9DE0AD37E5002B007F97A6AAED36EAD58AFE96A2E0C1486AF22F04660D96BAE9140B4E630E11A81BF9AA9AF875FEC2624BDD7D6EC62542CEADBB1C44874D2CFD40BB4CFFAD80BB4E15310C370CFA08FC4E02771BEB62F6D0CADFDA2DB7DC6261181ED795114D9A3C79B27DB7DD772CFA4062ECE766EA54CED828C13ABD4C8517F3E2856B2A163ABB5DF5336CE772577CABD43757722B16B803ECABE10E72B991F7F52289B3D7F79BAB080D89E730BA452ED0F9F17C39825BA113C211583027E3761940B8F276E1377D9DECDF15B303E332403CAC9540B4FBB0D6670180D1146E2B0E340010ED25DEED14B12D84190BD0B65789C7FEE7EB4FE7DC02125FB24BAEE0CD7797C9035C1144EB92AFF7D3017B15EAF8D90101D863AAE4EC8580A849BF633BC151F0B77F016233BE8782F6A55E035BC8D96AAA159393FC3D54B647914059EAAEB65BA4AB40A16A7185EF5DF445F03BE853902370B1A171CFFF801C9E4414FE46BEAF9BCD40C442BE8769A9B313E2A8134C02644D67AA867F5D2782374ECB2527529CD4F7BDA78F5ABCF0418BCBAEA1B71305A4376E032CD8795697FFFF8FEAA72EFB3DD603C97E06F8B33A5BEAD2A20C9E1C13E1198CE589F2A7F573E771B7485D2FDCCD2C9E61DFC9BADE9A3469725C04D8BD7BB70D7F66A8CE1FC079EF0BCBD16CDE4FD11DEE52E6784D4F657218787D4220B67A594FBC440A1FD3A0C0EFA5587155627D802770BB9AC7336B8222098513279CC0E82A5FC6F35811C93A2536748EC8E267C61F8B00D3617B9F62597F15C936C7AE771D944D6F06980300E4931FC9C60701A4459C5D8ACAE7A405C4129EDF5E2DB2F8DF9B10EBDA6E008E78F641ECD7B1B9FF3EDFEC19EEA98FA4F44700C7708839BB4277EE67DDC9052280BBC01EE60613F3B4A57290BBFBCBA5FCEEF6B77DB9131C05FFEA5F50CB58774FE08C89D4F19402C5DCB69D946E2F44C8676EED36E5DD91FAF6A20607109619C4FFCBAB05FEDC6E9F537AEE7BFD6E88833B6C8124A796E80E07F14728E538FAB082FE9450A7DB1F675FF6DF9DA176839CE06588F2C92E11CDDDE720BDBEF51E895A3C9791BDF5F771784385654D3BC5EA2E989F80F86BFCD56FB2F46FFB5AFD0F05FEF4233916B4E53E006E6E2EE411205A117849DDCC388339C4304014A2978108E661F43D06031B466EB1366DDA1CF3BACACA4A6B31B9C442143F9AC5395EC31EEC5FC447706A2E8C84124084D8C534FD84943D182D4664C0DD4185738B0A284ABC5D07C514324089CC2D1882353C40D03FE2A29798A36E6E5B4E0CDB26E0C7CB1989371CA5664414396895B805E40900C83D7F1250F200DE4A9A720E36F9E85B228583D301E90A3E173BBF17A22EAC9082B9523A515CD98B73D4D48701F9B5006713C42964AE11E099B945E46873A2C0B1B65A67BA0ACFE1CC1CD6A4B860047136667E134D7971AD00790E67CD5DA5BBCE05BCCF50A7EE00B9F5AD5F24C151F06FFCA500E980F63BD673D6DEC3229EC77794BC7B016E4F9AF169955CC6EFDC901076D24310B12767D727A0FF09E7560BE42E02039A2B7F2781BB66A53B2700C9E7DBE21DCAD9737380E713CB8D8F8A0CEE0C352BCC66D1EFA5BB45D259D4E6EE87B9A746BDCEF499FED5FF888B6398DA402353455F8F001181A69A6514341C185A78398F072404D63E017BE7046B38AC7D2622C60B85B9680E63777E4E52FAB261CC3F268183BFE54C0A5A27C1CEB80CB905A7B297DCC36BE5063177044E2AB014A45F4EC0F92A0BC6696306E4DC11BFCBB337E3037240F9E2590A3A9A9AB017E004A54AC6D7656CAC46248A01A301CE00A58D51B5E0647D0F072AD898C48D86CF9A62567D580AECB6ECEADBCBD50CA5DBCEB94338631D000803295C0BCEEE51A8B521CD9E46512EEC8AB26D1321DAA32627F7352B4331AA8F4851BF41815F5863F6F38102E86B1079003D39314BA02C3E41EAEBA06875A2EEDA46934E024853D74104C0BFA452043A1289AC8D6F1409B2B304FE9588418F22B95694388003DC09F91C779FDE426470523BE8D702D0B369EC2EEE2FA1DEB301E32F006B0E02F1EA5DAAD1EA0A398F3BDD6707446A27605E3EB5A810793B52B79739BFA0916AD7355F02D0A2B1468F7D0B630748F98DC7889F58EFBD49847207F8E925E4FB4D89980B92714780E8C7B321C2CFDCBE136B7536C6756ADAB1FE32E0490B54EAB4A5EFC8B600618BAB12A0E70BECF1AE043B793ADAEF8F9E668E9C82E6C256FCB17064E46B2E9EC4BCE1D4FF4B820CF8A753D830013CF8093A26846A95906A8184D9311E6F6529221EA47E2A1B0A4FD3C51910A774A1AB41BC380130875AD3A40E4B05F80C8B5D3DD6C81DC2337807F0D133626C309E775429A6D94193E47C2F80C74E23E3F7CC9E5F02D77245825A92EBD74CCA7C26E7BCB11110B2EFEC7602FB216AFF2EDF5AE70AE82FAF37EB562087E847C196ED1600B200E2FC95B805E03A0F37ABA5B8A59CF7D232A9B383C515DAC1DA9F7DE534E200770F3EC9EC690A7E16F135A5482BF64899B702BE5B07A1A6D3CD6EC35A37B0AF09FB5BFF985613D7A607E5306DB8F7BD0D22C0DF36097C1E7F39E4398D7B9E5A294217E5287E27F12B1B443CCF373B9463D5458AEF7C6CBA9C3EF46E9E741960DA6A81741663DB5CB982D7AB2DE7EDA386EB88F7BA53A57E8BA87D3BE66755883C7B109DC3106A2720488772A63B2F4FC0414EE1050920E95DF42273903FFC167308505CCEFA873E4F887FF407E0EFCFCEDCEF2073A007DC1DEF4FC4112CC41C130E12981D7B19AE108341CAE835D69FCE3859A30B66E46E70821C2ABC4CE76C6EBFCDD2E9B41D3A742833B6FBB465E65CC759BC4504F23B5DF13338062B414952C31D1269A3EF4116AC88E7F3001BE20F19C772E1241363B18DA050C5C928F95ABD075D13162DF95C129589CA7B82A86F304224096864BC5A17466F31D280C05DF524916BEE6201A2805F0A2417039A05CB54CA4280DA09004FE1BCABB0C38701E7691064799540EACE30B0B9ECBE92E60E6F2DD0F72A12707DBE2B04390552A46878D52129790580EE5924C798BF4360BEA2ABDE3D06DFE7A474E09FDF410072125D08B0A62D16988602E4E7A947BB5C01C9D7BFCDBAD1ED744619E766A744D05FB1676C7BB94BA3949CC9C1EF64FC7999D9EF86D1589E674390113DE82F394C422806B714B92B68E45C407E1BCEF6F12EE5D109315A4F8E5752F329ABE44C37D0B3051B05F2559037BF91E2713772D7F2C3DC35A6AD97880CC2951A8D629A3BA369C47021E3E3BC77977A3AE03220394D607672C48B78EFA69E079C11116738817744B0E1299E07F04C6D833EC287AF0F2F15C6622717F5B07D12C778968433C0A1AD99F0E7229AC1196E104702BBE32DFE40730D4F52DFDB12017D97B9E13A2F9E9B007DB704DA45DCDF9D5419827FC2D9603BA676863819B589E9D1FF00F9B6D6A5D94E36770000000049454E44AE426082;

ICON lights 89504E470D0A1A0A0000000D494844520000002A000000100403000000920CF0A30000001E504C5445FF00FFFFFFFF383838FF7D7DAFFF79878787FF000079000066FF00317B00334B68E30000000A74524E530000000000000000000073A0186800000091494441547801ADD0BB0DC3300C45517A83D0807FAD804CA015B4800AADA0D4AAB88252BAE4B679245B97A99E70701B9188167ED1C3247B2C9C634A4C4A606006038BB121D8106C08A6D5B5EEAEE374BD69336CF5E84019D704EA4D6B03B6BA4BCF22E3D45914BAA5F6861E5D3ED06BEA57A36DD14AB4FA9F96BD65F696D95BC68FD1DA57D1DAA08D43B41889D1E713627E826B4F4B228F2CA90000000049454E44AE426082;


// Track Editor

te_keyRepeatRate := 120;

te_curCol;
te_curRow;
te_tckOfst;
te_tkName := "";


te_chooseSaveSlot()
BEGIN
  LOCAL r, slot, label, name;
  LOCAL choice := 0, chooseList := {}, nameList := {};

  FOR r FROM 0 TO 9 DO
    slot := "L" + r;
    name := "";
    label := slot + " : <empty>";

    IF SIZE(EXPR(slot)) == 4 THEN
      IF TYPE(EXPR(slot + "(1)")) == 1 THEN
        IF EXPR(slot + "(1)") == #5442544B THEN
          name := EXPR(slot + "(2)");
          label := slot + " : " + name;
        END;
      END;
    END;

    chooseList(0) := label;
    nameList(0) := name;
  END;

  CHOOSE(choice, "Save track into ...", chooseList);

  IF choice <= 0 THEN
    RETURN {};
  END;

  RETURN {"L" + (choice - 1), nameList(choice)};
END;


trackEdit()
BEGIN
  LOCAL keyReady := 1, keyTick := 0, r;
  LOCAL kSave := 0, kNew := 0, okBox;
  LOCAL loop := 1;

  te_curCol := 1;
  te_curRow := 1;    
  te_tck := tckLst2Mat(trackMatrix);
  te_tkName := selectedTrack;
  te_tckOfst := 0;

  RECT_P(G1,0);
  RECT_P(G2,0);

  FOR r FROM 0 TO 15 DO
    BLIT_P(G2, 
      0, 15 - r,
      "tiles",
      0, r,
      192, r + 1);
  END;
 
  WHILE loop DO

    IF ISKEYDOWN(4) THEN loop := 0; END;

    IF ISKEYDOWN(2) AND keyReady THEN 
      te_curRow := MIN(te_curRow + 1, rowDim(te_tck));
      keyReady := 0;
      keyTick := TICKS; 
    END;

    IF ISKEYDOWN(12) AND keyReady THEN
      te_curRow := MAX(te_curRow - 1, 1);
      keyReady := 0;
      keyTick := TICKS; 
    END;

    IF ISKEYDOWN(7) AND keyReady THEN 
      te_curCol := MAX(te_curCol - 1, 1);
      keyReady := 0;
      keyTick := TICKS; 
    END;

    IF ISKEYDOWN(8) AND keyReady THEN
      te_curCol := MIN(te_curCol + 1, 5);
      keyReady := 0;
      keyTick := TICKS; 
    END;

    IF ISKEYDOWN(50) AND keyReady THEN
      ADDROW(te_tck, te_tck(te_curRow), te_curRow);  
      keyReady := 0;
      keyTick := TICKS; 
    END;

    IF ISKEYDOWN(45) AND keyReady THEN
      IF rowDim(te_tck) > 1 THEN
        DELROW(te_tck, te_curRow);
      END; 
      keyReady := 0;
      keyTick := TICKS; 
    END;

    IF ISKEYDOWN(47) THEN te_tck(te_curRow, te_curCol) := 0; END;
    IF ISKEYDOWN(42) THEN te_tck(te_curRow, te_curCol) := 1; END;
    IF ISKEYDOWN(43) THEN te_tck(te_curRow, te_curCol) := 2; END;
    IF ISKEYDOWN(44) THEN te_tck(te_curRow, te_curCol) := 3; END;
    IF ISKEYDOWN(37) THEN te_tck(te_curRow, te_curCol) := 4; END;
    IF ISKEYDOWN(38) THEN te_tck(te_curRow, te_curCol) := 5; END;
    IF ISKEYDOWN(39) THEN te_tck(te_curRow, te_curCol) := 6; END;

    // save
    IF ISKEYDOWN(23) AND NOT kSave THEN
      kSave := 1;
    END;

    IF kSave AND NOT ISKEYDOWN(23) THEN

      IF rowDim(te_tck) < 5 THEN
        MSGBOX(scLf + "    Track is too short.    " + scLf +
         "    Minimum length is 5.    " + scLf + " ");
      ELSE

        LOCAL pick := te_chooseSaveSlot();

        IF SIZE(pick) > 0 THEN
          LOCAL slot := pick(1);
          LOCAL Name := pick(2);

          okBox := INPUT(Name, "Enter Track Name");

          IF okBox THEN
            te_tkName := Name;

            LOCAL savedTrack := {#5442544B, te_tkName, 0, tckMat2Lst()};
            EXPR(slot + ":=" + STRING(savedTrack));

            // Make saved track active
            selectedTrack := te_tkName;
            trackMatrix := savedTrack(4);

            MSGBOX(scLf + "    Track saved in " + slot + "    " + scLf + " ");
          END;
        END;
      END;
      kSave := 0;
    END;


    // new
    IF ISKEYDOWN(21) AND NOT kNew THEN
      kNew := 1;
    END;

    IF kNew AND NOT ISKEYDOWN(21) THEN
      okBox := MSGBOX(scLf + "    Erase current track ?    " + scLf + "    Really !?" + scLf + " ", 1);
    
      IF okBox THEN 
        te_tck := [[1,1,1,1,1]];
        te_tkName := "";
      END;

      kNew := 0;
    END;

    IF NOT keyReady AND TICKS - keyTick ≥ te_keyRepeatRate THEN
      keyReady := 1;
    END;

    CASE
      IF te_curRow - te_tckOfst > 15 THEN
        te_tckOfst := te_tckOfst + 1; 
      END;
      IF te_curRow - te_tckOfst < 1 THEN
        te_tckOfst := te_tckOfst - 1; 
      END;
    END;

    te_render();

    te_curRow := MIN(te_curRow, rowDim(te_tck));

    BLIT(G1);
  END; 
END;


te_render()
BEGIN
  LOCAL r, s;
  LOCAL x1, x2, y1, y2;
  LOCAL endRow;

  RECT_P(G1, #888888);

  TEXTOUT_P("Length : " + rowDim(te_tck), G1, 155, 10);
  TEXTOUT_P("Cur Row : " + te_curRow, G1, 155, 30);

  endRow := MIN(te_tckOfst + 15, rowDim(te_tck)); 

  FOR r FROM 1 + te_tckOfst TO endRow DO
    FOR s FROM 1 TO 5 DO
      x1 := (s - 1) * 16 + 32;
      x2 := s * 16 - 1 + 32;
      y1 := 240 - (r - 1 - te_tckOfst) * 16; 
      y2 := 240 - (r - te_tckOfst) * 16; 

      IF te_tck(r, s) == 0 THEN
        RECT_P(G1, x1, y2, x2, y1 - 1, 0);
      ELSE
        BLIT_P(
          G1,
          x1, y2,
          x2 + 1, y1, 
          G2,
          (te_tck(r, s) - 1) * 32, 0,
          te_tck(r, s) * 32, 16);
      END; 
    END;
  END;

  x1 := (te_curCol - 1) * 16 + 32;
  x2 := te_curCol * 16 - 1 + 32;
  y1 := 240 - (te_curRow - 1 - te_tckOfst) * 16; 
  y2 := 240 - (te_curRow - te_tckOfst) * 16; 

  RECT_P(G1, x1, y2, x2, y1 - 1, #FFFFFF, #7FFFFFFF);

  FOR r FROM 1 TO 6 DO
    BLIT_P(G1,
      290, (r - 1) * 20 + 10,
      306, (r - 1) * 20 + 26,
      G2,
      (r - 1) * 32, 0,
      r * 32, 16);

    TEXTOUT_P("[" + r + "]", G1,
      268, (r - 1) * 20 + 12, 2);
  END;

  RECT_P(G1, 290, 130, 305, 145, 0);

  TEXTOUT_P("[0]", G1, 268, 132, 2); 
  TEXTOUT_P("[-] : del row", G1, 150, 100, 2);
  TEXTOUT_P("[+] : add row", G1, 150, 120, 2);
  TEXTOUT_P("[SIN] : new track", G1, 150, 140, 2);
  TEXTOUT_P("[TAN] : save", G1, 150, 160, 2);
  TEXTOUT_P("[Esc] : quit", G1, 150, 180, 2);
END;