diff --git a/src/components/game2048/Game2048V2.vue b/src/components/game2048/Game2048V2.vue index 6adedd3..76de990 100644 --- a/src/components/game2048/Game2048V2.vue +++ b/src/components/game2048/Game2048V2.vue @@ -12,7 +12,6 @@
([game2048Store.height, game2048Store.width]); type TileRenderState = { number: number; - exists: boolean; fromPos: Pos; toPos: Pos; showPos: Pos; zIndex: number; + shouldMove: boolean; + shouldHide: boolean; + shouldShow: boolean; }; class Tile { private static lastId = 0; - readonly id: number; + readonly id: number = Tile.lastId++; private _x!: number; private _y!: number; - private _exists: boolean; number = 0; zIndex = 0; + fromOthers = false; renderState: TileRenderState = { number: 0, - exists: false, fromPos: [0, 0], toPos: [0, 0], showPos: [0, 0], zIndex: 0, + shouldMove: false, + shouldHide: false, + shouldShow: false, }; constructor() { - this.id = Tile.lastId++; - this._exists = false; this.setPos([0, 0], 'move', false); } get x() { @@ -111,20 +112,14 @@ class Tile { get el() { return gameContainerRef.value?.querySelector(`.tile[data-tile-id="${this.id}"]`); } - get exists() { - return this._exists; + get exists(): boolean { + return !idleTiles.has(this); } - set exists(value) { - if (!value && table.get(this.y, this.x) === this) { + remove() { + if (table.get(this.y, this.x) === this) { table.delete(this.y, this.x); } - this._exists = value; - if (value) { - idleTiles.delete(this); - } else { - idleTiles.add(this); - this.renderState.zIndex = 0; - } + idleTiles.add(this); } setPos([y, x]: Pos, type: 'move' | 'show', changeTable: boolean) { if (changeTable && table.get(this.y, this.x) === this) { @@ -134,6 +129,7 @@ class Tile { this._x = x; if (changeTable) { table.set(y, x, this); + idleTiles.delete(this); } if (type === 'move') { this.renderState.fromPos = this.renderState.toPos; @@ -147,48 +143,49 @@ class Tile { this.el?.getAnimations().forEach((animation) => animation.finish()); return this; } - async show(fromOthers: boolean) { + async animate() { this.finish(); - // 避免新增块和移除块复用带来的位置冲突问题, - // 所以把设置toPos放在这 - this.renderState.toPos = [...this.renderState.showPos]; - this.exists = true; - this.renderState.exists = true; - this.renderState.number = this.number; - const posStyle = getTilePositionStyle(this.renderState.showPos); - await this.el?.animate( - fromOthers - ? [ - { opacity: 1, transform: `scale(0.5)`, ...posStyle }, - { transform: `scale(1.1)`, offset: 0.8 }, - { opacity: 1, transform: `scale(1)`, ...posStyle }, - ] - : Object.assign( - createShowKeyframeCommon(this.renderState.showPos, { opacity: false }), - posStyle, - ), - createKeyframeOptions({ duration: 500 }), - ).finished; - return this; - } - hide() { - this.finish(); - this.renderState.exists = false; - return this; - } - async move() { - this.finish(); - this.renderState.zIndex = this.zIndex; - this.zIndex = 0; - const ext = { zIndex: this.renderState.zIndex }; - await this.el?.animate( - [ - Object.assign(getTilePositionStyle(this.renderState.fromPos), ext), - Object.assign(getTilePositionStyle(this.renderState.toPos), ext), - ], - createKeyframeOptions({ easing: 'ease-in', duration: 500 }), - ).finished; - return this; + const { shouldMove, shouldHide, shouldShow } = this.renderState; + if (shouldMove) { + this.renderState.shouldMove = false; + this.renderState.zIndex = this.zIndex; + this.zIndex = 0; + const ext = { zIndex: this.renderState.zIndex }; + await this.el?.animate( + [ + Object.assign(getTilePositionStyle(this.renderState.fromPos), ext), + Object.assign(getTilePositionStyle(this.renderState.toPos), ext), + ], + createKeyframeOptions({ easing: 'ease-in', duration: tileMoveDuration }), + ).finished; + } + if (shouldHide) { + this.renderState.shouldHide = false; + await this.el?.animate({ display: 'none' }, createKeyframeOptions()).finished; + } + if (shouldShow) { + this.renderState.shouldShow = false; + this.renderState.toPos = [...this.renderState.showPos]; + this.renderState.number = this.number; + await this.el?.animate({ display: 'block' }, createKeyframeOptions()).finished; + const posStyle = getTilePositionStyle(this.renderState.showPos); + await this.el?.animate( + this.fromOthers + ? [ + { transform: `scale(0.5)`, ...posStyle }, + { transform: `scale(1.1)`, offset: 0.8 }, + { transform: `scale(1)`, ...posStyle }, + ] + : Object.assign( + createShowKeyframeCommon(this.renderState.showPos, { opacity: false }), + posStyle, + ), + createKeyframeOptions({ + duration: tileShowDuration, + delay: shouldMove ? 0 : tileMoveDuration, + }), + ).finished; + } } toString() { return `Tile#${this.id}`; @@ -212,7 +209,7 @@ const createKeyframeOptions = ({ duration, delay, ...options -}: KeyframeAnimationOptions): KeyframeAnimationOptions => ({ +}: KeyframeAnimationOptions = {}): KeyframeAnimationOptions => ({ easing: 'ease', fill: 'both', duration: !breaking ? duration : 0, @@ -239,6 +236,8 @@ const tileBackgroundColorMapping: Record = { 1024: 'rgb(237, 197, 63)', 2048: 'rgb(237, 194, 46)', }; +const tileMoveDuration = 150, + tileShowDuration = 150; const getTilePositionStyle = ([y, x]: Pos) => ({ left: `${borderWidth + x * (tileWidth + borderWidth)}px`, top: `${borderWidth + y * (tileWidth + borderWidth)}px`, @@ -253,11 +252,13 @@ const backgroundTilesRef = useTemplateRef('backgroundTiles'); const addRandomTiles = (count: number) => { const emptyPositions = shuffle(table.getEmptyPositions()); const addedTiles: Exposed[] = []; - for (const [, tile] of zip(Array(count), Array.from(idleTiles))) { + for (const tile of Iterator.from(idleTiles).take(count)) { const pos = emptyPositions.pop(); if (!pos) break; tile.number = sample([2, 4]); tile.setPos(pos, 'show', true); + tile.fromOthers = false; + tile.renderState.shouldShow = true; addedTiles.push(tile); } return addedTiles; @@ -281,7 +282,8 @@ onMounted(async () => { .flat() .map((a) => a?.finished), ); - await Promise.all(addRandomTiles(2).map((t) => t.show(false))); + fading = false; + await Promise.all(addRandomTiles(2).map((t) => t.animate())); }); const firstIndices = (d: Directions) => Array.from( @@ -331,10 +333,30 @@ const secondIndices = (d: Directions) => })(), ); const once = true; +let fading = true; const animating = ref(false); let breaking = false; +const canMove = () => { + if (table.filledSize < width.value * height.value) return true; + for (const xFirst of [false, true]) { + for (const line of table.iter(xFirst)) { + let prev: Tile | undefined; + for (const tile of Iterator.from(line).filter((t) => !!t)) { + if (!prev) { + prev = tile; + continue; + } + if (prev.number === tile.number) { + return true; + } + prev = tile; + } + } + } + return false; +}; const move = async (key: Directions) => { - if (breaking) return; + if (fading || breaking || !canMove()) return; if (animating.value) { breaking = true; tiles.forEach((t) => t.finish()); @@ -345,7 +367,7 @@ const move = async (key: Directions) => { const first = Array.from(firstIndices(key)), second = Array.from(secondIndices(key).toReversed()), xFirst = arrayIncludes([Directions.UP, Directions.DOWN], key); - const tilesToShow = new Set(), + const tilesToReshow = new Set(), tilesToHide = new Set(), tilesToMove = new Set(), relationMap = new Map(); @@ -366,10 +388,11 @@ const move = async (key: Directions) => { if (prevTile.number === tile.number) { finished = false; changed = true; - prevTile.exists = false; + prevTile.remove(); tile.number *= 2; + tile.fromOthers = true; setDefault(relationMap, tile, []).push(prevTile); - tilesToShow.add(tile); + tilesToReshow.add(tile); tilesToHide.add(prevTile); prev = once ? undefined : tile; } else { @@ -377,12 +400,12 @@ const move = async (key: Directions) => { } } } while (!once && !finished); - const dumpIndexIt = Iterator.from(second); + let i = 0; let zIndex = second.length - 1; for (const s of second) { const tile = table.get(f, s, xFirst); if (!tile) continue; - const dumpIndex = dumpIndexIt.next().value!, + const dumpIndex = second[i++], pos = toPos(f, dumpIndex, xFirst); if (s !== dumpIndex) { changed = true; @@ -397,13 +420,19 @@ const move = async (key: Directions) => { } } } - const tilesToAdd = changed ? addRandomTiles(2) : []; - await Promise.all(Iterator.from(tilesToMove).map((t) => t.move())); - Iterator.from(tilesToHide).forEach((t) => t.hide()); - await Promise.all([ - ...Iterator.from(tilesToShow).map((t) => t.show(true)), - tilesToAdd.map((t) => t.show(false)), - ]); + if (tilesToMove.size) { + addRandomTiles(2); + } + tilesToMove.forEach((t) => { + t.renderState.shouldMove = true; + }); + tilesToHide.forEach((t) => { + t.renderState.shouldHide = true; + }); + tilesToReshow.forEach((t) => { + t.renderState.shouldShow = true; + }); + await Promise.all([tiles.map((t) => t.animate())]); animating.value = false; }; useEventListener(document, 'keydown', async (e) => { diff --git a/src/utils/table-view.ts b/src/utils/table-view.ts index 2b06559..e362e4b 100644 --- a/src/utils/table-view.ts +++ b/src/utils/table-view.ts @@ -20,6 +20,9 @@ export class TableView { this.size = [...arg]; } } + get filledSize() { + return this.map.size; + } get(first: number, second: number, xFirst: boolean = false) { return this.map.get(key(first, second, xFirst)); }