mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-03 17:00:53 +00:00
ENH - animate minesweeper markers
This commit is contained in:
Binary file not shown.
@@ -1,15 +1,23 @@
|
||||
import flagUrl from '@/assets/audio/minesweeper/flag.wav?url'
|
||||
import clearUrl from '@/assets/audio/minesweeper/clear.wav?url'
|
||||
import mineUrl from '@/assets/audio/minesweeper/mine.wav?url'
|
||||
import placeUrl from '@/assets/audio/minesweeper/place.wav?url'
|
||||
import revealUrl from '@/assets/audio/minesweeper/reveal.wav?url'
|
||||
import winUrl from '@/assets/audio/minesweeper/win.wav?url'
|
||||
|
||||
export type MinesweeperSound = 'clear' | 'flag' | 'mine' | 'reveal' | 'win'
|
||||
export type MinesweeperSound =
|
||||
| 'clear'
|
||||
| 'flag'
|
||||
| 'mine'
|
||||
| 'place'
|
||||
| 'reveal'
|
||||
| 'win'
|
||||
|
||||
const soundUrls: Record<MinesweeperSound, string> = {
|
||||
clear: clearUrl,
|
||||
flag: flagUrl,
|
||||
mine: mineUrl,
|
||||
place: placeUrl,
|
||||
reveal: revealUrl,
|
||||
win: winUrl,
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ const minesRemaining = computed(() =>
|
||||
)
|
||||
const roundEffect = ref<'explosion' | 'victory' | null>(null)
|
||||
const resultVisible = ref(true)
|
||||
const markerEffectCellId = ref<number | null>(null)
|
||||
const markerEffectKey = ref(0)
|
||||
const blastStyle = computed<CSSProperties>(() => {
|
||||
const currentGame = minesweeper.game
|
||||
const explodedCell = currentGame?.cells.find(
|
||||
@@ -48,11 +50,24 @@ const blastStyle = computed<CSSProperties>(() => {
|
||||
'--blast-top': `${((explodedCell.row + 0.5) / currentGame.height) * 100}%`,
|
||||
} as CSSProperties
|
||||
})
|
||||
const markerStyle = computed<CSSProperties>(() => {
|
||||
const currentGame = minesweeper.game
|
||||
const markedCell = currentGame?.cells.find(
|
||||
(cell) => cell.id === markerEffectCellId.value,
|
||||
)
|
||||
if (!currentGame || !markedCell) return {}
|
||||
|
||||
return {
|
||||
'--marker-left': `${((markedCell.column + 0.5) / currentGame.width) * 100}%`,
|
||||
'--marker-top': `${((markedCell.row + 0.5) / currentGame.height) * 100}%`,
|
||||
} as CSSProperties
|
||||
})
|
||||
let clockTimer: ReturnType<typeof setInterval> | undefined
|
||||
let longPressTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let suppressNextClick = false
|
||||
let suppressTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let effectTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let markerEffectTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function formatTime(milliseconds: number): string {
|
||||
const seconds = Math.floor(milliseconds / 1000)
|
||||
@@ -113,7 +128,21 @@ function startRoundEffect(
|
||||
|
||||
function toggleFlag(cellId: number): void {
|
||||
const result = minesweeper.toggleFlag(cellId)
|
||||
if (result?.changed) playMinesweeperSound('flag', minesweeper.soundEnabled)
|
||||
if (!result?.changed) return
|
||||
|
||||
const markedCell = result.state.cells.find((cell) => cell.id === cellId)
|
||||
if (markedCell?.isFlagged) {
|
||||
if (markerEffectTimer) clearTimeout(markerEffectTimer)
|
||||
markerEffectCellId.value = cellId
|
||||
markerEffectKey.value += 1
|
||||
playMinesweeperSound('place', minesweeper.soundEnabled)
|
||||
markerEffectTimer = setTimeout(() => {
|
||||
markerEffectCellId.value = null
|
||||
markerEffectTimer = undefined
|
||||
}, 720)
|
||||
} else {
|
||||
playMinesweeperSound('flag', minesweeper.soundEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
function beginLongPress(cellId: number): void {
|
||||
@@ -143,8 +172,11 @@ function restart(): void {
|
||||
if (!minesweeper.game) return
|
||||
|
||||
if (effectTimer) clearTimeout(effectTimer)
|
||||
if (markerEffectTimer) clearTimeout(markerEffectTimer)
|
||||
effectTimer = undefined
|
||||
markerEffectTimer = undefined
|
||||
roundEffect.value = null
|
||||
markerEffectCellId.value = null
|
||||
resultVisible.value = true
|
||||
minesweeper.start(minesweeper.game.difficulty)
|
||||
}
|
||||
@@ -159,6 +191,7 @@ onBeforeUnmount(() => {
|
||||
if (longPressTimer) clearTimeout(longPressTimer)
|
||||
if (suppressTimer) clearTimeout(suppressTimer)
|
||||
if (effectTimer) clearTimeout(effectTimer)
|
||||
if (markerEffectTimer) clearTimeout(markerEffectTimer)
|
||||
minesweeper.pause()
|
||||
minesweeper.persist()
|
||||
})
|
||||
@@ -281,6 +314,7 @@ onBeforeUnmount(() => {
|
||||
'minesweeper-cell--flagged': cell.isFlagged,
|
||||
'minesweeper-cell--mine': cell.isMine && cell.isRevealed,
|
||||
'minesweeper-cell--exploded': game.explodedCellId === cell.id,
|
||||
'minesweeper-cell--marking': markerEffectCellId === cell.id,
|
||||
[`minesweeper-cell--number-${cell.adjacentMines}`]:
|
||||
cell.isRevealed && !cell.isMine && cell.adjacentMines > 0,
|
||||
}"
|
||||
@@ -297,6 +331,20 @@ onBeforeUnmount(() => {
|
||||
<strong v-else-if="cell.isRevealed && cell.adjacentMines > 0">{{ cell.adjacentMines }}</strong>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="markerEffectCellId !== null"
|
||||
:key="markerEffectKey"
|
||||
class="minesweeper-marker-drop"
|
||||
:style="markerStyle"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="minesweeper-marker-drop__trail"></span>
|
||||
<span class="minesweeper-marker-drop__bomb">
|
||||
<Bomb :size="24" fill="currentColor" />
|
||||
</span>
|
||||
<span class="minesweeper-marker-drop__impact"></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="roundEffect === 'explosion'"
|
||||
class="minesweeper-blast"
|
||||
@@ -512,6 +560,7 @@ onBeforeUnmount(() => {
|
||||
.minesweeper-board--expert .minesweeper-cell { border-radius: 6px; font-size: 11px; }
|
||||
.minesweeper-cell--revealed { color: #477078; background: #eef5e9; box-shadow: inset 0 1px 3px rgb(39 83 80 / 12%); animation: minesweeper-reveal 170ms ease-out; }
|
||||
.minesweeper-cell--flagged { color: #f35f58; background: linear-gradient(145deg, #5bcec5, #2ca5a4); }
|
||||
.minesweeper-cell--marking > svg { animation: minesweeper-marker-flag 720ms ease-out; }
|
||||
.minesweeper-cell--mine { color: #263c51; background: #dce5dd; }
|
||||
.minesweeper-cell--exploded { color: #fff; background: radial-gradient(circle, #263238 0 31%, #743e37 34% 55%, #df584e 58%); box-shadow: inset 0 0 9px #050b0c, 0 0 12px #ff5a3c; animation: minesweeper-explode 720ms ease-out; }
|
||||
.minesweeper-cell--number-1 { color: #3275ce; }
|
||||
@@ -551,6 +600,58 @@ onBeforeUnmount(() => {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.minesweeper-marker-drop {
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
top: var(--marker-top);
|
||||
left: var(--marker-left);
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.minesweeper-marker-drop__bomb {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: -19px;
|
||||
border: 2px solid #667985;
|
||||
border-radius: 50%;
|
||||
color: #172c36;
|
||||
background: radial-gradient(circle at 35% 28%, #687b83, #263c45 48%, #12272f 75%);
|
||||
box-shadow: inset -4px -5px 8px rgb(0 0 0 / 35%), 0 5px 9px rgb(11 42 45 / 42%);
|
||||
animation: minesweeper-bomb-drop 720ms cubic-bezier(0.2, 0.8, 0.35, 1) forwards;
|
||||
}
|
||||
|
||||
.minesweeper-marker-drop__trail {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 7px;
|
||||
height: 95px;
|
||||
margin: -87px 0 0 27px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(to bottom, transparent, #ffe38a 45%, #ff6c42 83%, transparent);
|
||||
filter: blur(2px);
|
||||
transform: rotate(35deg);
|
||||
transform-origin: bottom;
|
||||
animation: minesweeper-bomb-trail 570ms ease-out forwards;
|
||||
}
|
||||
|
||||
.minesweeper-marker-drop__impact {
|
||||
position: absolute;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin: -17px;
|
||||
border: 4px solid #8df3df;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 12px #4bc4bd;
|
||||
opacity: 0;
|
||||
animation: minesweeper-marker-impact 720ms ease-out forwards;
|
||||
}
|
||||
|
||||
.minesweeper-blast__flash,
|
||||
.minesweeper-blast__ring,
|
||||
.minesweeper-blast__core,
|
||||
@@ -675,6 +776,33 @@ onBeforeUnmount(() => {
|
||||
82% { transform: translate(2px, 1px); }
|
||||
}
|
||||
|
||||
@keyframes minesweeper-bomb-drop {
|
||||
0% { opacity: 0; transform: translate(105px, -185px) scale(0.55) rotate(160deg); }
|
||||
12% { opacity: 1; }
|
||||
66% { transform: translate(0, 0) scale(1) rotate(355deg); }
|
||||
78% { transform: translate(0, -13px) scale(0.9) rotate(370deg); }
|
||||
90% { opacity: 1; transform: translate(0, 0) scale(0.78) rotate(380deg); }
|
||||
100% { opacity: 0; transform: translate(0, 0) scale(0.35) rotate(390deg); }
|
||||
}
|
||||
|
||||
@keyframes minesweeper-bomb-trail {
|
||||
0% { opacity: 0; scale: 0.4; }
|
||||
18% { opacity: 0.9; }
|
||||
72%, 100% { opacity: 0; scale: 1; }
|
||||
}
|
||||
|
||||
@keyframes minesweeper-marker-impact {
|
||||
0%, 62% { opacity: 0; transform: scale(0.2); }
|
||||
68% { opacity: 1; }
|
||||
100% { opacity: 0; transform: scale(2.2); }
|
||||
}
|
||||
|
||||
@keyframes minesweeper-marker-flag {
|
||||
0%, 68% { opacity: 0; transform: scale(0.35) rotate(-18deg); }
|
||||
82% { opacity: 1; transform: scale(1.28) rotate(5deg); }
|
||||
100% { opacity: 1; transform: scale(1) rotate(0); }
|
||||
}
|
||||
|
||||
@keyframes minesweeper-screen-flash {
|
||||
0% { opacity: 0.82; }
|
||||
18% { opacity: 0.22; }
|
||||
@@ -746,6 +874,8 @@ button:active { transform: scale(0.96); }
|
||||
.minesweeper-game--exploding .minesweeper-board,
|
||||
.minesweeper-game--exploding::after,
|
||||
.minesweeper-blast > *,
|
||||
.minesweeper-marker-drop > *,
|
||||
.minesweeper-cell--marking > svg,
|
||||
.minesweeper-celebration i,
|
||||
.minesweeper-game--victory .minesweeper-cell--flagged,
|
||||
.minesweeper-cell--revealed,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sky Phone</title>
|
||||
<script type="module" crossorigin src="./assets/sky-index-CgcZvXN7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/sky-index-yBie1g9x.css">
|
||||
<script type="module" crossorigin src="./assets/sky-index-lJl_5aH2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/sky-index-D9eWeoNZ.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user