Better animation for Atomic
[xogo.git] / base_rules.js
index 3fac51b..107b412 100644 (file)
@@ -3,6 +3,21 @@ import { ArrayFun } from "/utils/array.js";
 import PiPo from "/utils/PiPo.js";
 import Move from "/utils/Move.js";
 
+// Helper class for move animation
+class TargetObj {
+
+  constructor(callOnComplete) {
+    this.value = 0;
+    this.target = 0;
+    this.callOnComplete = callOnComplete;
+  }
+  increment() {
+    if (++this.value == this.target)
+      this.callOnComplete();
+  }
+
+};
+
 // NOTE: x coords: top to bottom (white perspective); y: left to right
 // NOTE: ChessRules is aliased as window.C, and variants as window.V
 export default class ChessRules {
@@ -391,17 +406,21 @@ export default class ChessRules {
     // Fen string fully describes the game state
     if (!o.fen)
       o.fen = this.genRandInitFen(o.seed);
-    const fenParsed = this.parseFen(o.fen);
-    this.board = this.getBoard(fenParsed.position);
-    this.turn = fenParsed.turn;
-    this.movesCount = parseInt(fenParsed.movesCount, 10);
-    this.setOtherVariables(fenParsed);
+    this.re_initFromFen(o.fen);
 
     // Graphical (can use variables defined above)
     this.containerId = o.element;
     this.graphicalInit();
   }
 
+  re_initFromFen(fen, oldBoard) {
+    const fenParsed = this.parseFen(fen);
+    this.board = oldBoard || this.getBoard(fenParsed.position);
+    this.turn = fenParsed.turn;
+    this.movesCount = parseInt(fenParsed.movesCount, 10);
+    this.setOtherVariables(fenParsed);
+  }
+
   // Turn position fen into double array ["wb","wp","bk",...]
   getBoard(position) {
     const rows = position.split("/");
@@ -433,7 +452,6 @@ export default class ChessRules {
       this.initReserves(fenParsed.reserve);
     if (this.options["crazyhouse"])
       this.initIspawn(fenParsed.ispawn);
-    this.subTurn = 1; //may be unused
     if (this.options["teleport"]) {
       this.subTurnTeleport = 1;
       this.captured = null;
@@ -443,6 +461,9 @@ export default class ChessRules {
       this.enlightened = ArrayFun.init(this.size.x, this.size.y, false);
       this.updateEnlightened();
     }
+    this.subTurn = 1; //may be unused
+    if (!this.moveStack) //avoid resetting (unwanted)
+      this.moveStack = [];
   }
 
   updateEnlightened() {
@@ -930,7 +951,7 @@ export default class ChessRules {
       if (cd) {
         const move = this.doClick(cd);
         if (move)
-          this.playPlusVisual(move);
+          this.buildMoveStack(move, r);
         else {
           const [x, y] = Object.values(cd);
           if (typeof x != "number")
@@ -984,7 +1005,7 @@ export default class ChessRules {
         if (moves.length >= 2)
           this.showChoices(moves, r);
         else if (moves.length == 1)
-          this.playPlusVisual(moves[0], r);
+          this.buildMoveStack(moves[0], r);
       }
       curPiece.remove();
     };
@@ -1025,7 +1046,7 @@ export default class ChessRules {
     const callback = (m) => {
       chessboard.style.opacity = "1";
       container.removeChild(choices);
-      this.playPlusVisual(m, r);
+      this.buildMoveStack(m, r);
     }
     for (let i=0; i < moves.length; i++) {
       let choice = document.createElement("div");
@@ -1313,7 +1334,7 @@ export default class ChessRules {
       moves = this.capturePostProcess(moves, oppCol);
 
     if (this.options["atomic"])
-      this.atomicPostProcess(moves, oppCol);
+      this.atomicPostProcess(moves, color, oppCol);
 
     if (
       moves.length > 0 &&
@@ -1344,7 +1365,7 @@ export default class ChessRules {
     });
   }
 
-  atomicPostProcess(moves, oppCol) {
+  atomicPostProcess(moves, color, oppCol) {
     moves.forEach(m => {
       if (
         this.board[m.end.x][m.end.y] != "" &&
@@ -1361,15 +1382,22 @@ export default class ChessRules {
           [1, 0],
           [1, 1]
         ];
+        let mNext = new Move({
+          start: m.end,
+          end: m.end,
+          appear: [],
+          vanish: []
+        });
         for (let step of steps) {
           let x = m.end.x + step[0];
           let y = this.getY(m.end.y + step[1]);
           if (
             this.onBoard(x, y) &&
             this.board[x][y] != "" &&
+            (x != m.start.x || y != m.start.y) &&
             this.getPieceType(x, y) != "p"
           ) {
-            m.vanish.push(
+            mNext.vanish.push(
               new PiPo({
                 p: this.getPiece(x, y),
                 c: this.getColor(x, y),
@@ -1379,8 +1407,18 @@ export default class ChessRules {
             );
           }
         }
-        if (!this.options["rifle"])
-          m.appear.pop(); //nothing appears
+        if (!this.options["rifle"]) {
+          // The moving piece also vanish
+          mNext.vanish.unshift(
+            new PiPo({
+              x: m.end.x,
+              y: m.end.y,
+              c: color,
+              p: this.getPiece(m.start.x, m.start.y)
+            })
+          );
+        }
+        m.next = mNext;
       }
     });
   }
@@ -2128,35 +2166,42 @@ export default class ChessRules {
       this.subTurnTeleport = 1;
       this.captured = null;
     }
-    if (this.options["balance"]) {
-      if (![1, 3].includes(this.movesCount))
-        this.turn = oppCol;
-    }
-    else {
-      if (
+    if (
+      !move.next &&
+      (
         (
           this.options["doublemove"] &&
           this.movesCount >= 1 &&
           this.subTurn == 1
-        ) ||
+        )
+        ||
         (this.options["progressive"] && this.subTurn <= this.movesCount)
+      )
+    ) {
+      const oppKingPos = this.searchKingPos(oppCol);
+      if (
+        oppKingPos[0] >= 0 &&
+        (
+          this.options["taking"] ||
+          !this.underCheck(oppKingPos, color)
+        )
       ) {
-        const oppKingPos = this.searchKingPos(oppCol);
-        if (
-          oppKingPos[0] >= 0 &&
-          (
-            this.options["taking"] ||
-            !this.underCheck(oppKingPos, color)
-          )
-        ) {
-          this.subTurn++;
-          return;
-        }
+        this.subTurn++;
+        return;
       }
+    }
+    if (this.isLastMove(move)) {
       this.turn = oppCol;
+      this.movesCount++;
+      this.subTurn = 1;
     }
-    this.movesCount++;
-    this.subTurn = 1;
+  }
+
+  isLastMove(move) {
+    return (
+      (this.options["balance"] && ![1, 3].includes(this.movesCount)) ||
+      !move.next
+    );
   }
 
   // "Stop at the first move found"
@@ -2232,12 +2277,41 @@ export default class ChessRules {
       this.graphUpdateEnlightened();
   }
 
-  playPlusVisual(move, r) {
+  // TODO: send stack receive stack, or allow incremental? (good/bad points)
+  buildMoveStack(move, r) {
+    this.moveStack.push(move);
+    this.computeNextMove(move);
     this.play(move);
-    this.playVisual(move, r);
-    this.afterPlay(move); //user method
+    const newTurn = this.turn;
+    if (this.moveStack.length == 1)
+      this.playVisual(move, r);
+    if (move.next) {
+      this.gameState = {
+        fen: this.getFen(),
+        board: JSON.parse(JSON.stringify(this.board)) //easier
+      };
+      this.buildMoveStack(move.next, r);
+    }
+    else {
+      if (this.moveStack.length == 1) {
+        // Usual case (one normal move)
+        this.afterPlay(this.moveStack, newTurn, {send: true, res: true});
+        this.moveStack = []
+      }
+      else {
+        this.afterPlay(this.moveStack, newTurn, {send: true, res: false});
+        this.re_initFromFen(this.gameState.fen, this.gameState.board);
+        this.playReceivedMove(this.moveStack.slice(1), () => {
+          this.afterPlay(this.moveStack, newTurn, {send: false, res: true});
+          this.moveStack = []
+        });
+      }
+    }
   }
 
+  // Implemented in variants using (automatic) moveStack
+  computeNextMove(move) {}
+
   getMaxDistance(r) {
     // Works for all rectangular boards:
     return Math.sqrt(r.width ** 2 + r.height ** 2);
@@ -2247,41 +2321,37 @@ export default class ChessRules {
     return (typeof x == "string" ? this.r_pieces : this.g_pieces)[x][y];
   }
 
-  animate(move, callback) {
-    if (this.noAnimate || move.noAnimate) {
-      callback();
-      return;
-    }
-    let initPiece = this.getDomPiece(move.start.x, move.start.y);
-    // NOTE: cloning generally not required, but light enough, and simpler
+  animateMoving(start, end, drag, segments, cb) {
+    let initPiece = this.getDomPiece(start.x, start.y);
+    // NOTE: cloning often not required, but light enough, and simpler
     let movingPiece = initPiece.cloneNode();
     initPiece.style.opacity = "0";
     let container =
       document.getElementById(this.containerId)
     const r = container.querySelector(".chessboard").getBoundingClientRect();
-    if (typeof move.start.x == "string") {
+    if (typeof start.x == "string") {
       // Need to bound width/height (was 100% for reserve pieces)
       const pieceWidth = this.getPieceWidth(r.width);
       movingPiece.style.width = pieceWidth + "px";
       movingPiece.style.height = pieceWidth + "px";
     }
     const maxDist = this.getMaxDistance(r);
-    const apparentColor = this.getColor(move.start.x, move.start.y);
-    const pieces = this.pieces(apparentColor, move.start.x, move.start.y);
-    if (move.drag) {
-      const startCode = this.getPiece(move.start.x, move.start.y);
+    const apparentColor = this.getColor(start.x, start.y);
+    const pieces = this.pieces(apparentColor, start.x, start.y);
+    if (drag) {
+      const startCode = this.getPiece(start.x, start.y);
       C.RemoveClass_es(movingPiece, pieces[startCode]["class"]);
-      C.AddClass_es(movingPiece, pieces[move.drag.p]["class"]);
-      if (apparentColor != move.drag.c) {
+      C.AddClass_es(movingPiece, pieces[drag.p]["class"]);
+      if (apparentColor != drag.c) {
         movingPiece.classList.remove(C.GetColorClass(apparentColor));
-        movingPiece.classList.add(C.GetColorClass(move.drag.c));
+        movingPiece.classList.add(C.GetColorClass(drag.c));
       }
     }
     container.appendChild(movingPiece);
     const animateSegment = (index, cb) => {
       // NOTE: move.drag could be generalized per-segment (usage?)
-      const [i1, j1] = move.segments[index][0];
-      const [i2, j2] = move.segments[index][1];
+      const [i1, j1] = segments[index][0];
+      const [i2, j2] = segments[index][1];
       const dep = this.getPixelPosition(i1, j1, r);
       const arr = this.getPixelPosition(i2, j2, r);
       movingPiece.style.transitionDuration = "0s";
@@ -2297,24 +2367,64 @@ export default class ChessRules {
         setTimeout(cb, duration * 1000);
       }, 50);
     };
-    if (!move.segments) {
-      move.segments = [
-        [[move.start.x, move.start.y], [move.end.x, move.end.y]]
-      ];
-    }
     let index = 0;
     const animateSegmentCallback = () => {
-      if (index < move.segments.length)
+      if (index < segments.length)
         animateSegment(index++, animateSegmentCallback);
       else {
         movingPiece.remove();
         initPiece.style.opacity = "1";
-        callback();
+        cb();
       }
     };
     animateSegmentCallback();
   }
 
+  // Input array of objects with at least fields x,y (e.g. PiPo)
+  animateFading(arr, cb) {
+    const animLength = 350; //TODO: 350ms? More? Less?
+    arr.forEach(v => {
+      let fadingPiece = this.getDomPiece(v.x, v.y);
+      fadingPiece.style.transitionDuration = (animLength / 1000) + "s";
+      fadingPiece.style.opacity = "0";
+    });
+    setTimeout(cb, animLength);
+  }
+
+  animate(move, callback) {
+    if (this.noAnimate || move.noAnimate) {
+      callback();
+      return;
+    }
+    let segments = move.segments;
+    if (!segments)
+      segments = [ [[move.start.x, move.start.y], [move.end.x, move.end.y]] ];
+    let targetObj = new TargetObj(callback);
+    if (move.start.x != move.end.x || move.start.y != move.end.y) {
+      targetObj.target++;
+      this.animateMoving(move.start, move.end, move.drag, segments,
+                         () => targetObj.increment());
+    }
+    if (move.vanish.length > move.appear.length) {
+      const arr = move.vanish.slice(move.appear.length)
+        // Ignore disappearing pieces hidden by some appearing ones:
+        .filter(v => move.appear.every(a => a.x != v.x || a.y != v.y));
+      if (arr.length > 0) {
+        targetObj.target++;
+        this.animateFading(arr, () => targetObj.increment());
+      }
+    }
+    targetObj.target +=
+      this.customAnimate(move, segments, () => targetObj.increment());
+    if (targetObj.target == 0)
+      callback();
+  }
+
+  // Potential other animations (e.g. for Suction variant)
+  customAnimate(move, segments, cb) {
+    return 0; //nb of targets
+  }
+
   playReceivedMove(moves, callback) {
     const launchAnimation = () => {
       const r = container.querySelector(".chessboard").getBoundingClientRect();