diff --git a/assets/scripts/core/game-scene.js b/assets/scripts/core/game-scene.js
index 9fdead19..0256f1f2 100644
--- a/assets/scripts/core/game-scene.js
+++ b/assets/scripts/core/game-scene.js
@@ -7396,6 +7396,15 @@ _showwippopup() {
this._endCamTween = null;
this._spaceWasDown = false;
this._physicsFrame = 0;
+ if (this._mirrorTween) {
+ this._mirrorTween.stop();
+ this._mirrorTween = null;
+ }
+ if (this._mirrorAnim) {
+ this._mirrorAnim.p = 0;
+ } else {
+ this._mirrorAnim = { p: 0 };
+ }
}
_restartLevel() {
this._attempts++;
@@ -7648,6 +7657,15 @@ _showwippopup() {
this._state.gravity = checkpoint.gravity;
this._state.jumpPower = checkpoint.jumpPower;
this._state.mirrored = checkpoint.mirrored;
+ if (this._mirrorTween) {
+ this._mirrorTween.stop();
+ this._mirrorTween = null;
+ }
+ if (this._mirrorAnim) {
+ this._mirrorAnim.p = checkpoint.mirrored ? 1 : 0;
+ } else {
+ this._mirrorAnim = { p: checkpoint.mirrored ? 1 : 0 };
+ }
this._state.isDashing = checkpoint.isDashing;
this._state.dashYVelocity = checkpoint.dashYVelocity;
this._state.ballShouldRotate = checkpoint.ballShouldRotate || false;
@@ -7922,7 +7940,9 @@ _showwippopup() {
}
_updateBackground() {
- this._bg.tilePositionX += (this._cameraX - this._prevCameraX) * this._bgSpeedX;
+ const isMirrored = this._mirrorAnim ? (this._mirrorAnim.p >= 0.5) : this._state.mirrored;
+ const deltaX = this._cameraX - this._prevCameraX;
+ this._bg.tilePositionX += isMirrored ? -deltaX * this._bgSpeedX : deltaX * this._bgSpeedX;
this._prevCameraX = this._cameraX;
let tileY = this._bgInitY - this._cameraY * this._bgSpeedY;
if (this._bgMirrorTileHeight > 0) {
@@ -8655,34 +8675,80 @@ _showwippopup() {
this._applyMirrorEffect();
}
-_applyMirrorEffect() {
- const isMirrored = this._state.mirrored;
- const containers = [this._level.additiveContainer, this._level.container, this._level.topContainer];
- if (isMirrored) {
- for (const c of containers) {
- c.scaleX = -1;
- c.x = this._cameraX + screenWidth;
- }
- for (const tile of this._level._groundTiles) {
- tile.x = screenWidth - tile.x - this._level._tileW;
- tile.setFlipX(true);
- }
- for (const tile of this._level._ceilingTiles) {
- tile.x = screenWidth - tile.x - this._level._tileW;
- tile.setFlipX(true);
- }
- } else {
- for (const c of containers) {
- if (c.scaleX !== 1) c.scaleX = 1;
- }
- for (const tile of this._level._groundTiles) {
- tile.setFlipX(false);
- }
- for (const tile of this._level._ceilingTiles) {
- tile.setFlipX(false);
+ toggleMirror(isMirrored) {
+ const currentP = this._mirrorAnim
+ ? this._mirrorAnim.p
+ : (this._state.mirrored ? 1 : 0);
+
+ this._state.mirrored = isMirrored;
+ if (this._state2) this._state2.mirrored = isMirrored;
+
+ if (!this._mirrorAnim) {
+ this._mirrorAnim = { p: currentP };
+ }
+
+ // Freeze ground/bg at current positions when transition starts
+ if (currentP > 0.001 && currentP < 0.999) {
+ this._mirrorFrozenTiles = true;
+ } else if (currentP <= 0.001 || currentP >= 0.999) {
+ this._mirrorFrozenTiles = false;
+ }
+
+ if (this._editorPlaytestActive) {
+ this._mirrorAnim.p = isMirrored ? 1 : 0;
+ this._attemptsLabel.setVisible(isMirrored);
+ return;
+ }
+
+ if (this._mirrorTween) {
+ this._mirrorTween.stop();
+ this._mirrorTween = null;
+ }
+ this._mirrorTween = this.tweens.add({
+ targets: this._mirrorAnim,
+ p: isMirrored ? 1 : 0,
+ duration: 400,
+ ease: "Sine.easeInOut",
+ onComplete: () => {
+ this._mirrorTween = null;
+ this._mirrorFrozenTiles = false;
+ this._attemptsLabel.setVisible(this._state.mirrored);
}
+ });
+ }
+
+ _applyMirrorEffect() {
+ const p = this._mirrorAnim ? this._mirrorAnim.p : (this._state.mirrored ? 1 : 0);
+ const containers = [this._level.additiveContainer, this._level.container, this._level.topContainer];
+ const scaleX = 1 - 2 * p;
+ const containerX = screenWidth / 2 * (1 - scaleX) - this._cameraX * scaleX;
+
+ for (const c of containers) {
+ c.scaleX = scaleX;
+ c.x = containerX;
}
- this._bg.setFlipX(isMirrored);
+
+ this._bg.setFlipX(false);
+
+ const isFlipped = p >= 0.5;
+
+ for (let i = 0; i < this._level._groundTiles.length; i++) {
+ const tile = this._level._groundTiles[i];
+ const tileScreenX = tile._worldX - this._cameraX;
+ tile.x = isFlipped ? (screenWidth - tileScreenX - this._level._tileW) : tileScreenX;
+
+ const ground2Tile = this._level._ground2Tiles?.[i];
+ if (ground2Tile) ground2Tile.x = tile.x;
+
+ const ceilingTile = this._level._ceilingTiles[i];
+ ceilingTile.x = tile.x;
+
+ const ceiling2Tile = this._level._ceiling2Tiles?.[i];
+ if (ceiling2Tile) ceiling2Tile.x = tile.x;
+ }
+
+ const isAnimating = this._mirrorTween !== null;
+ this._attemptsLabel.setVisible(isAnimating ? false : this._state.mirrored);
}
_getDualSharedSignature(state) {
if (!state) return "0|0";
@@ -9012,7 +9078,8 @@ _applyMirrorEffect() {
}
_getMirrorXOffset(xOffset) {
- return this._state.mirrored ? screenWidth - xOffset : xOffset;
+ const p = this._mirrorAnim ? this._mirrorAnim.p : (this._state.mirrored ? 1 : 0);
+ return xOffset + (screenWidth - 2 * xOffset) * p;
}
_enableDualMode() {
if (this._isDual) return;
diff --git a/assets/scripts/core/level.js b/assets/scripts/core/level.js
index 087b3249..dd98c181 100644
--- a/assets/scripts/core/level.js
+++ b/assets/scripts/core/level.js
@@ -1393,7 +1393,9 @@ window.LevelObject = class LevelObject {
maxWorldX = groundTile._worldX;
this._maxGroundWorldX = maxWorldX;
}
- let tileScreenX = groundTile._worldX - cameraX;
+ const isMirrored = this._scene._mirrorAnim ? (this._scene._mirrorAnim.p >= 0.5) : this._scene._state.mirrored;
+ const normalX = groundTile._worldX - cameraX;
+ const tileScreenX = isMirrored ? (screenWidth - normalX - tileWidth) : normalX;
groundTile.x = tileScreenX;
groundTile.y = leftTileIndex;
const ground2Tile = this._ground2Tiles?.[i];
diff --git a/assets/scripts/core/player.js b/assets/scripts/core/player.js
index 0ee23f03..41f7a0df 100644
--- a/assets/scripts/core/player.js
+++ b/assets/scripts/core/player.js
@@ -2430,6 +2430,35 @@ if (this.p.isFlying || this.p.isUfo) {
});
}
+ spawnPortalCircle(color, gameObj) {
+ if (this._scene?._editorPlaytestActive || window.enableLDM || !this._scene) return;
+ const scene = this._scene;
+ const graphics = scene.add.graphics().setDepth(15).setBlendMode(S);
+ this._gameLayer.topContainer.add(graphics);
+ const maxRadius = 75;
+ const startX = gameObj ? Number(gameObj.x) : (scene._playerWorldX || 0);
+ const startY = gameObj ? b(Number(gameObj.y)) : b(this.p.y || 30);
+ graphics.setPosition(startX, startY);
+ const state = { progress: 0 };
+ scene.tweens.add({
+ targets: state,
+ progress: 1,
+ duration: 350,
+ ease: "Quad.Out",
+ onUpdate: () => {
+ const radius = 8 + state.progress * (maxRadius - 8);
+ const alpha = Math.max(0, 1 - state.progress);
+ graphics.clear();
+ graphics.setAlpha(1);
+ graphics.lineStyle(4, color, alpha);
+ graphics.strokeCircle(0, 0, radius);
+ },
+ onComplete: () => {
+ graphics.destroy();
+ }
+ });
+ }
+
playGravityEffect(toUp) {
if (this._scene?._editorPlaytestActive || window.enableLDM || !this._scene) return;
const scene = this._scene;
@@ -4417,17 +4446,19 @@ if (this.p.isFlying || this.p.isUfo) {
if (!this._isObjectActivated(gameObj)) {
this._setObjectActivated(gameObj, true);
this._playPortalShine(gameObj);
- if (!this._scene?._editorPlaytestActive) {
- this.p.mirrored = true;
- }
+ this.spawnPortalCircle(0xff9900, gameObj);
+ if (this._streakManager) this._streakManager.reset();
+ this.p.mirrored = true;
+ this._scene?.toggleMirror?.(true);
}
} else if (_colType === "portal_mirror_off") {
if (!this._isObjectActivated(gameObj)) {
this._setObjectActivated(gameObj, true);
this._playPortalShine(gameObj);
- if (!this._scene?._editorPlaytestActive) {
- this.p.mirrored = false;
- }
+ this.spawnPortalCircle(0x00ffff, gameObj);
+ if (this._streakManager) this._streakManager.reset();
+ this.p.mirrored = false;
+ this._scene?.toggleMirror?.(false);
}
} else if (_colType === "portal_mini_on") {
if (!this._isObjectActivated(gameObj)) {
diff --git a/index.html b/index.html
index 8d033615..37a7e00c 100644
--- a/index.html
+++ b/index.html
@@ -45,7 +45,7 @@
-
+