fixes, features

This commit is contained in:
Evert Prants 2022-10-20 18:01:56 +03:00
parent d0434ffebb
commit 3fdc4e805c
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
4 changed files with 123 additions and 57 deletions

View File

@ -12,8 +12,14 @@
<Bubble v-if="traversing" :bubble="traversing" :radius="radius" /> <Bubble v-if="traversing" :bubble="traversing" :radius="radius" />
<Shooter v-if="next" :center="next" :radius="radius" :angle="angle" /> <Shooter v-if="next" :center="next" :radius="radius" :angle="angle" />
<ScoreDisplay :score="score" /> <ScoreDisplay
<GameOver v-if="gameOver" :score="score">{{ :score="score"
:hiscore="hiscore"
:shots-left="shotsLeft"
:streak="streak"
:max-streak="maxStreak"
/>
<GameOver v-if="gameOver" :score="score" :streak="maxStreak">{{
victory ? 'You won!' : 'Game over.' victory ? 'You won!' : 'Game over.'
}}</GameOver> }}</GameOver>
</div> </div>
@ -38,9 +44,12 @@ const radius = ref(32);
const diameter = computed(() => radius.value * 2); const diameter = computed(() => radius.value * 2);
const colors = ref(5); const colors = ref(5);
const angle = ref(90); const angle = ref(90);
const streak = ref(0);
const maxStreak = ref(0);
const shotsLeft = ref(4); const shotsLeft = ref(4);
const offset = ref(0); const offset = ref(0);
const score = ref(0); const score = ref(0);
const hiscore = ref(0);
const fitment = computed(() => Math.floor(width / diameter.value)); const fitment = computed(() => Math.floor(width / diameter.value));
const gameOver = ref(false); const gameOver = ref(false);
@ -69,23 +78,36 @@ function createRow(column: number, generate = false) {
generate ? field.value.push(...entries) : field.value.unshift(...entries); generate ? field.value.push(...entries) : field.value.unshift(...entries);
} }
function addRow() { function checkHeightLoss() {
field.value.forEach((item) => {
item.y += diameter.value - 8;
});
offset.value += 1;
createRow(0);
const lowestPart = field.value.reduce<number>( const lowestPart = field.value.reduce<number>(
(last, current) => (last < current.y ? current.y : last), (last, current) => (last < current.y ? current.y : last),
0 0
); );
if (lowestPart > height - diameter.value * 2) { if (lowestPart > height - diameter.value * 2) {
gameOver.value = true; gameOver.value = true;
alert('Game over!');
} }
} }
function saveHiscore(score: number) {
const currentHigh = Number(localStorage.getItem('highscore'));
if ((currentHigh && currentHigh < score) || !currentHigh) {
localStorage.setItem('highscore', score.toString());
hiscore.value = score;
}
}
function addRow() {
field.value.forEach((item) => {
item.y += diameter.value - 8;
});
offset.value += 1;
createRow(0);
checkHeightLoss();
}
function generate() { function generate() {
for (let column = 0; column < 5; column++) { for (let column = 0; column < 5; column++) {
createRow(column, true); createRow(column, true);
@ -97,9 +119,12 @@ function reset() {
field.value = []; field.value = [];
offset.value = 0; offset.value = 0;
generate(); generate();
saveHiscore(score.value);
score.value = 0; score.value = 0;
gameOver.value = false; gameOver.value = false;
victory.value = false; victory.value = false;
streak.value = 0;
maxStreak.value = 0;
determineNext(); determineNext();
} }
@ -220,6 +245,30 @@ function determineImmediateNeighbors(item: BubbleType) {
return neighbors; return neighbors;
} }
function accountForAllCeilingAttached() {
let accountedFor: BubbleType[] = [];
const startingPoint = field.value.filter(
(item) => item.y === 0 && !item.exiting
);
const accountNeighbors = (item: BubbleType) => {
if (!accountedFor.includes(item)) {
accountedFor.push(item);
const neighbors = determineImmediateNeighbors(item);
if (neighbors.length) {
for (const neighbor of neighbors) {
if (accountedFor.includes(neighbor)) continue;
accountNeighbors(neighbor);
}
}
}
};
for (const item of startingPoint) {
accountNeighbors(item);
}
return accountedFor;
}
function checkForPops(item: BubbleType, friendlies: BubbleType[] = []) { function checkForPops(item: BubbleType, friendlies: BubbleType[] = []) {
const neighbors = determineImmediateNeighbors(item); const neighbors = determineImmediateNeighbors(item);
const same = neighbors const same = neighbors
@ -237,33 +286,6 @@ function checkForPops(item: BubbleType, friendlies: BubbleType[] = []) {
return friendlies; return friendlies;
} }
function findIsolatedGroups(groups: BubbleType[][] = []) {
for (const item of field.value) {
if (groups.some((group) => group.includes(item))) {
continue;
}
const neighbors = determineImmediateNeighbors(item);
if (neighbors.length) {
let setgroup;
for (const neighbor of neighbors) {
if (groups.some((group) => group.includes(neighbor))) {
setgroup = groups.find((item) => item.includes(neighbor));
break;
}
}
if (setgroup) {
setgroup.push(item);
} else {
groups.push([item, ...neighbors]);
}
} else {
groups.push([item]);
}
}
return groups;
}
function animateRemovals() { function animateRemovals() {
const toRemove = field.value.filter((item) => item.exiting); const toRemove = field.value.filter((item) => item.exiting);
if (!toRemove.length) return; if (!toRemove.length) return;
@ -285,24 +307,20 @@ function animateRemovals() {
} }
function determineIsolated() { function determineIsolated() {
const groups = findIsolatedGroups([]); const accountedFor = accountForAllCeilingAttached();
let failedGroups = groups.filter(
(group) => !group.some((item) => item.y === 0)
);
if (failedGroups.length) {
let fallCount = 0; let fallCount = 0;
field.value field.value
.filter((item) => failedGroups.some((group) => group.includes(item))) .filter((item) => !accountedFor.includes(item))
.forEach((i) => { .forEach((i) => {
i.exiting = true; i.exiting = true;
fallCount++; fallCount++;
}); });
score.value += fallCount * 10; score.value += fallCount * 10;
}
animateRemovals(); if (fallCount > 0) animateRemovals();
return fallCount;
} }
function stopDead() { function stopDead() {
@ -320,9 +338,15 @@ function stopDead() {
if (chain.length >= 3) { if (chain.length >= 3) {
score.value += chain.length; score.value += chain.length;
field.value = field.value.filter((item) => !chain.includes(item)); field.value = field.value.filter((item) => !chain.includes(item));
determineIsolated(); const dropped = determineIsolated();
streak.value += chain.length + dropped;
if (streak.value > maxStreak.value) {
maxStreak.value = streak.value;
}
} else { } else {
shotsLeft.value -= 1; shotsLeft.value -= 1;
streak.value = 0;
} }
if (shotsLeft.value < 1) { if (shotsLeft.value < 1) {
@ -335,7 +359,10 @@ function stopDead() {
if (!field.value.filter((x) => !x.exiting).length) { if (!field.value.filter((x) => !x.exiting).length) {
victory.value = true; victory.value = true;
gameOver.value = true; gameOver.value = true;
return;
} }
checkHeightLoss();
} }
let steps = 0; let steps = 0;
@ -402,6 +429,12 @@ function normalize(vector: { x: number; y: number }) {
onMounted(() => { onMounted(() => {
generate(); generate();
determineNext(); determineNext();
const score = Number(localStorage.getItem('highscore'));
if (score) {
hiscore.value = score;
}
function determineVectors(e: MouseEvent) { function determineVectors(e: MouseEvent) {
const x = e.clientX - fieldRef.value!.offsetLeft; const x = e.clientX - fieldRef.value!.offsetLeft;
const y = e.clientY - fieldRef.value!.offsetTop; const y = e.clientY - fieldRef.value!.offsetTop;

View File

@ -2,10 +2,11 @@
<div class="game-over"> <div class="game-over">
<h2><slot></slot></h2> <h2><slot></slot></h2>
<p>Score: {{ score }}</p> <p>Score: {{ score }}</p>
<p>Best streak: {{ streak }}</p>
<small>Click to restart</small> <small>Click to restart</small>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ score: number }>(); defineProps<{ score: number; streak: number }>();
</script> </script>

View File

@ -1,7 +1,23 @@
<template> <template>
<div class="game-score">Score: {{ score }}</div> <div class="game-score">
<span>
<span>Score: {{ score }}</span>
<span>Hiscore: {{ hiscore }}</span>
</span>
<span>
<span>Current streak: {{ streak }}</span>
<span>Max streak: {{ maxStreak }}</span>
</span>
<span>Shots left: {{ shotsLeft }}</span>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ score: number }>(); defineProps<{
score: number;
hiscore: number;
shotsLeft: number;
streak: number;
maxStreak: number;
}>();
</script> </script>

View File

@ -51,8 +51,15 @@ body {
} }
&-score { &-score {
display: flex;
width: 100%;
justify-content: space-between;
position: absolute; position: absolute;
bottom: -1.5rem; bottom: -3rem;
& > span {
display: flex;
flex-direction: column;
}
} }
&-pointer { &-pointer {
@ -88,6 +95,15 @@ body {
font-size: 2rem; font-size: 2rem;
pointer-events: none; pointer-events: none;
z-index: 4; z-index: 4;
p:first-of-type {
margin-bottom: 0;
font-size: 2.5rem;
}
small {
color: rgb(9, 255, 0);
}
} }
&-object { &-object {