This commit is contained in:
parent
e5e3864964
commit
18ec5baee1
@ -12,7 +12,6 @@
|
||||
<div
|
||||
v-for="tile of tiles"
|
||||
:key="tile.id"
|
||||
v-show="tile.renderState.exists"
|
||||
class="tile"
|
||||
:data-tile-id="tile.id"
|
||||
:style="[getNumberTileStyle(tile.renderState)]"
|
||||
@ -75,31 +74,33 @@ const { width, height } = storeToRefs(game2048Store);
|
||||
const table = new TableView<Tile>([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<number, string | undefined> = {
|
||||
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<Tile>[] = [];
|
||||
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<Tile>(),
|
||||
const tilesToReshow = new Set<Tile>(),
|
||||
tilesToHide = new Set<Tile>(),
|
||||
tilesToMove = new Set<Tile>(),
|
||||
relationMap = new Map<Tile, Tile[]>();
|
||||
@ -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) => {
|
||||
|
@ -20,6 +20,9 @@ export class TableView<T> {
|
||||
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));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user