This commit is contained in:
parent
74965d2b8c
commit
db5184bc37
@ -316,7 +316,7 @@ const tileBackgroundColorMapping: Record<number, string | undefined> = {
|
||||
*/
|
||||
function storeTileGrid() {
|
||||
game2048Store.rawTileGrid = tileGrid.map((row) =>
|
||||
row.map((tiles) => tiles.map((tile) => pick(tile, 'number'))),
|
||||
row.map((tiles) => (tiles.length ? pick(tiles[0], 'number') : undefined)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -735,17 +735,18 @@ onMounted(async () => {
|
||||
0,
|
||||
0,
|
||||
...game2048Store.rawTileGrid.map((row) =>
|
||||
row.map((rawTiles) =>
|
||||
rawTiles.map((rawTile) => {
|
||||
const id = maxId++;
|
||||
tileAddPromises.push(tileAddFutureMap.add(id));
|
||||
return {
|
||||
row.map((rawTile) => {
|
||||
if (!rawTile) return [];
|
||||
const id = maxId++;
|
||||
tileAddPromises.push(tileAddFutureMap.add(id));
|
||||
return [
|
||||
{
|
||||
...rawTile,
|
||||
id,
|
||||
fromOthers: false,
|
||||
};
|
||||
}),
|
||||
),
|
||||
},
|
||||
];
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (game2048Store.isInitial) {
|
||||
|
@ -9,38 +9,22 @@
|
||||
import { GAME_2048_DEBUG } from '@/env';
|
||||
import { onBeforeUnmount, ref, useTemplateRef, watch } from 'vue';
|
||||
|
||||
const { leaveDuration, enterDuration, linearEnter, reversed, ...props } = defineProps<{
|
||||
digit: number;
|
||||
const { leaveDuration, enterDuration, linearEnter, linearLeave, reversed, ...props } = defineProps<{
|
||||
digit: number | string;
|
||||
leaveDuration: number;
|
||||
enterDuration: number;
|
||||
linearEnter: boolean;
|
||||
linearLeave: boolean;
|
||||
reversed: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
up: [number];
|
||||
finish: [number];
|
||||
up: [];
|
||||
finish: [];
|
||||
}>();
|
||||
const digit = ref(props.digit);
|
||||
const div = useTemplateRef('div');
|
||||
const up = 'translateY(-100%)',
|
||||
down = 'translateY(100%)';
|
||||
let linearLeave = false;
|
||||
watch(
|
||||
() => linearEnter,
|
||||
(v) => {
|
||||
if (v) {
|
||||
linearLeave = true;
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
watch(
|
||||
() => reversed,
|
||||
() => {
|
||||
linearLeave = false;
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
watch(
|
||||
() => props.digit,
|
||||
async (d) => {
|
||||
@ -52,9 +36,8 @@ watch(
|
||||
fill: 'forwards',
|
||||
duration: leaveDuration,
|
||||
}).finished;
|
||||
linearLeave = linearEnter;
|
||||
digit.value = d;
|
||||
emit('up', d);
|
||||
emit('up');
|
||||
if (GAME_2048_DEBUG) {
|
||||
console.log('c2', linearEnter, enterDuration);
|
||||
}
|
||||
@ -63,7 +46,7 @@ watch(
|
||||
fill: 'forwards',
|
||||
duration: enterDuration,
|
||||
}).finished;
|
||||
emit('finish', d);
|
||||
emit('finish');
|
||||
},
|
||||
);
|
||||
onBeforeUnmount(() => {
|
||||
|
@ -7,12 +7,13 @@
|
||||
<transition-group :css="false" @enter="onEnter" @leave="onLeave">
|
||||
<game2048-score-digit
|
||||
ref="digitRefs"
|
||||
v-for="[i, d] of getDigits()"
|
||||
v-for="[i, d] of getDigitChars()"
|
||||
:key="i"
|
||||
:digit="Number(d)"
|
||||
:digit="d"
|
||||
:leave-duration="leaveDuration"
|
||||
:enter-duration="enterDuration"
|
||||
:linear-enter="linearEnter"
|
||||
:linear-leave="linearLeave"
|
||||
:reversed="reversed"
|
||||
@up="futureMap.resolve('leave')"
|
||||
@finish="futureMap.resolve('enter')"
|
||||
@ -51,6 +52,7 @@
|
||||
</style>
|
||||
<script lang="ts">
|
||||
export const baseDuration = (tileShowDuration + tileMoveDuration) / 2;
|
||||
// export const baseDuration = 1000;
|
||||
const fadeOutKeyframe = { fontSize: 0, scale: 0 };
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
@ -58,7 +60,7 @@ import { GAME_2048_DEBUG } from '@/env';
|
||||
import { AsyncQueue } from '@/utils/async-queue';
|
||||
import { FutureMap } from '@/utils/future';
|
||||
import { noop } from 'lodash-es';
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import Game2048ScoreDigit from './Game2048ScoreDigit.vue';
|
||||
import { tileMoveDuration, tileShowDuration } from './Game2048V2.vue';
|
||||
|
||||
@ -66,17 +68,19 @@ const props = defineProps<{
|
||||
score: number;
|
||||
}>();
|
||||
const score = ref(props.score);
|
||||
const getDigits = (n: number = score.value) => {
|
||||
const getDigitChars = (n: number = score.value) => {
|
||||
const s = String(n);
|
||||
return s.split('').map((d, i): [i: number, d: number] => [s.length - 1 - i, Number(d)]);
|
||||
return s.split('').map((d, i): [i: number, d: string] => [s.length - 1 - i, d]);
|
||||
};
|
||||
const futureMap = new FutureMap<'leave' | 'enter'>();
|
||||
const scoreDiv = ref<HTMLDivElement>();
|
||||
const scoreQueue = new AsyncQueue<number>();
|
||||
const leaveDuration = ref(baseDuration);
|
||||
const enterDuration = ref(baseDuration);
|
||||
let isUnmounted = false;
|
||||
const linearEnter = ref(false);
|
||||
const linearLeave = ref(false);
|
||||
const delta = ref(0);
|
||||
const reversed = computed(() => delta.value < 0);
|
||||
scoreQueue.watchPush(props, 'score');
|
||||
const onEnter = async (el: Element, done: () => void) => {
|
||||
await el.animate(fadeOutKeyframe, { duration: 0, fill: 'forwards' }).finished;
|
||||
@ -98,24 +102,31 @@ const onLeave = async (el: Element, done: () => void) => {
|
||||
futureMap.resolve('leave');
|
||||
done();
|
||||
};
|
||||
const reversed = ref(false);
|
||||
let isUnmounted = false;
|
||||
const getDelta = (v: number, o: number): [sameDirection: boolean, delta: number] => {
|
||||
const newDelta = v - o;
|
||||
const sameDirection = delta.value >= 0 ? newDelta >= 0 : newDelta < 0;
|
||||
return [sameDirection, newDelta];
|
||||
};
|
||||
onMounted(async () => {
|
||||
let delta = 0;
|
||||
while (!isUnmounted) {
|
||||
const s = await scoreQueue.shift().catch(noop);
|
||||
if (s === undefined) break;
|
||||
const old = score.value;
|
||||
score.value = s;
|
||||
const newDelta = s - old;
|
||||
reversed.value = newDelta < 0;
|
||||
delta = newDelta;
|
||||
let sameDirection: boolean;
|
||||
[sameDirection, delta.value] = getDelta(s, old);
|
||||
leaveDuration.value = baseDuration / (scoreQueue.size + 1 + Number(linearEnter.value));
|
||||
enterDuration.value = baseDuration / (scoreQueue.size + 1);
|
||||
let nextSameDirection = !scoreQueue.size || getDelta(scoreQueue.getImmediate(0)!, s)[0];
|
||||
linearLeave.value = linearEnter.value && sameDirection;
|
||||
linearEnter.value = !!scoreQueue.size && nextSameDirection;
|
||||
futureMap.clear();
|
||||
if (GAME_2048_DEBUG) {
|
||||
console.log('score', s, nextSameDirection);
|
||||
}
|
||||
if (!new RegExp(`.+${old}`).test(String(s))) {
|
||||
futureMap.add('leave').then(() => {
|
||||
linearEnter.value = !!scoreQueue.size && (delta >= 0 ? newDelta >= 0 : newDelta < 0);
|
||||
});
|
||||
futureMap.add('leave');
|
||||
}
|
||||
if (!new RegExp(`.+${s}$`).test(String(old))) {
|
||||
futureMap.add('enter');
|
||||
@ -126,6 +137,7 @@ onMounted(async () => {
|
||||
onBeforeUnmount(() => {
|
||||
isUnmounted = true;
|
||||
scoreQueue.cancelGetters();
|
||||
futureMap.values().forEach((v) => v.resolve());
|
||||
scoreDiv.value?.getAnimations().forEach((animation) => animation.finish());
|
||||
});
|
||||
</script>
|
||||
|
@ -78,15 +78,15 @@ export const tileMoveDuration = 110,
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { GAME_2048_DEBUG } from '@/env';
|
||||
import { useGame2048Store } from '@/stores/2048';
|
||||
import { useGame2048Store, type TileSerialized } from '@/stores/2048';
|
||||
import { chainIterables, waitRef } from '@/utils';
|
||||
import { arrayIncludes } from '@/utils/array';
|
||||
import { arrayIncludes, create2DArray, entries, iter2DArray } from '@/utils/array';
|
||||
import { setDefault } from '@/utils/map';
|
||||
import { TableView, toPos, type Pos } from '@/utils/table-view';
|
||||
import type { Exposed } from '@/utils/types';
|
||||
import { Directions } from '@/views/Game2048PageV2.vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { sample, shuffle } from 'lodash-es';
|
||||
import { pick, sample, shuffle } from 'lodash-es';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, reactive, ref, useTemplateRef, watch } from 'vue';
|
||||
const game2048Store = useGame2048Store();
|
||||
@ -215,6 +215,19 @@ watch(
|
||||
game2048Store.maxNumber = v;
|
||||
},
|
||||
);
|
||||
watch(tiles, () => {
|
||||
const grid = create2DArray<TileSerialized | undefined>(
|
||||
game2048Store.height,
|
||||
game2048Store.width,
|
||||
undefined,
|
||||
);
|
||||
Iterator.from(tiles)
|
||||
.filter((t) => t.exists)
|
||||
.forEach((tile) => {
|
||||
grid[tile.y][tile.x] = pick(tile, 'number');
|
||||
});
|
||||
game2048Store.rawTileGrid = grid;
|
||||
});
|
||||
const idleTiles = new Set(tiles);
|
||||
const createShowKeyframeCommon = (
|
||||
pos: Pos | undefined,
|
||||
@ -268,7 +281,20 @@ const addRandomTiles = (count: number) => {
|
||||
}
|
||||
return addedTiles;
|
||||
};
|
||||
const initTable = () => {
|
||||
const res: Exposed<Tile>[] = [];
|
||||
for (const [i, [x, y, t]] of entries(
|
||||
iter2DArray(game2048Store.rawTileGrid).filter(([, , t]) => t),
|
||||
)) {
|
||||
const tile = tiles[i];
|
||||
tile.setPos([y, x], 'show', true);
|
||||
tile.number = t!.number;
|
||||
res.push(tile);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
onMounted(async () => {
|
||||
const originalTiles = initTable();
|
||||
await Promise.all(
|
||||
[
|
||||
gameContainerRef.value?.animate(
|
||||
@ -290,7 +316,10 @@ onMounted(async () => {
|
||||
}),
|
||||
].flat(),
|
||||
);
|
||||
await Promise.all(addRandomTiles(2).map((t) => t.show(false)));
|
||||
await Promise.all([
|
||||
Promise.all(originalTiles.map((t) => t.show(false))),
|
||||
game2048Store.isInitial && Promise.all(addRandomTiles(2).map((t) => t.show(false))),
|
||||
]);
|
||||
animating.value = false;
|
||||
});
|
||||
const firstIndices = (d: Directions) =>
|
||||
|
@ -10,7 +10,7 @@ export const useGame2048Store = defineStore('2048', () => {
|
||||
const { key: gameKey, refresh: refreshGame } = useRefresh();
|
||||
|
||||
function create() {
|
||||
return create2DArray<TileSerialized[]>(height.value, width.value, () => []);
|
||||
return create2DArray<TileSerialized | undefined>(height.value, width.value, () => undefined);
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
@ -32,7 +32,7 @@ export const useGame2048Store = defineStore('2048', () => {
|
||||
height.value = 4;
|
||||
successNumber.value = 2048;
|
||||
watch([width, height], $reset, { flush: 'sync' });
|
||||
const rawTileGrid = useLocalStorage<TileSerialized[][][]>('2048-tile-grid', create());
|
||||
const rawTileGrid = useLocalStorage<(TileSerialized | undefined)[][]>('2048-tile-grid', create());
|
||||
watch(
|
||||
rawTileGrid,
|
||||
() => {
|
||||
|
@ -10,7 +10,9 @@ export function create2DArray<T>(
|
||||
Array.from({ length: width }, (_, x) => (cell instanceof Function ? cell(x, y) : cell)),
|
||||
);
|
||||
}
|
||||
export function* iter2DArray<T>(grid: T[][]): Generator<[number, number, T], void, unknown> {
|
||||
export function* iter2DArray<T>(
|
||||
grid: T[][],
|
||||
): Generator<[x: number, y: number, item: T], void, unknown> {
|
||||
for (let y = 0; y < grid.length; y++) {
|
||||
for (let x = 0; x < grid[y].length; x++) {
|
||||
yield [x, y, grid[y][x]];
|
||||
|
@ -22,7 +22,12 @@ export class AsyncQueue<T> {
|
||||
this._getters.push(future);
|
||||
return future.promise;
|
||||
}
|
||||
|
||||
shiftImmediate(): T | undefined {
|
||||
return this._queue.shift();
|
||||
}
|
||||
getImmediate(i: number) {
|
||||
return this._queue.get(i);
|
||||
}
|
||||
push(...values: T[]): this {
|
||||
for (const value of values) {
|
||||
const future = this._getters.shift();
|
||||
|
Loading…
x
Reference in New Issue
Block a user