Skip to content

Commit

Permalink
Move debug to component + add achievements (missing icons)
Browse files Browse the repository at this point in the history
  • Loading branch information
Iapetus-11 committed Nov 28, 2024
1 parent a953bf2 commit e0f695b
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 39 deletions.
119 changes: 119 additions & 0 deletions src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ export type GameState = {
clicks: number;
userClicks: number;
fractionClicks: number;

tacos: number;
totalTacos: number;

ownedToppings: OwnedToppings;
ownedAutoClickers: OwnedAutoClickers;

ownedSkins: (keyof typeof SKINS)[];
selectedSkin: keyof typeof SKINS;

unlockedAchievements: (keyof typeof ACHIEVEMENTS)[];
};

export type SkinDefinition = {
Expand Down Expand Up @@ -145,3 +150,117 @@ export const AUTO_CLICKERS = {
},
} as const satisfies Record<string, AutoClickerDefinition>;
export type OwnedAutoClickers = Partial<Record<keyof typeof AUTO_CLICKERS, number>>;

export type AchievementDefinition = {
description: string;
icon: string;
condition: (state: GameState) => boolean;
};
export const ACHIEVEMENTS = {
'Taco Enjoyer': {
description: 'Earn a total of 256 tacos',
icon: '/art/tacos/goldy.png',
condition(state) {
return state.totalTacos >= 256;
},
},
'Taco Muncher': {
description: 'Earn a total of 1024 tacos',
icon: '/art/tacos/goldy.png',
condition(state) {
return state.totalTacos >= 1024;
},
},
'Taco Devourer': {
description: 'Earn a total of 8192 tacos',
icon: '/art/tacos/goldy.png',
condition(state) {
return state.totalTacos >= 8192;
},
},
'Taco Assassin': {
description: 'Earn a total of 131072 tacos',
icon: '/art/tacos/goldy.png',
condition(state) {
return state.totalTacos >= 131072;
},
},
'Taco Destroyer': {
description: 'Earn a total of 4194304 tacos',
icon: '/art/tacos/goldy.png',
condition(state) {
return state.totalTacos >= 4194304;
},
},
'Taco Annihilator': {
description: 'Earn a total of 268435456 tacos',
icon: '/art/tacos/goldy.png',
condition(state) {
return state.totalTacos >= 268435456;
},
},

'Big Spender I': {
description: 'Spend 8192 tacos on skins, toppings, or automation',
icon: '/art/tacos/goldy.png',
condition(state) {
return Math.floor(state.totalTacos - state.tacos) >= 8192;
},
},
'Big Spender II': {
description: 'Spend 262144 tacos on skins, toppings, or automation',
icon: '/art/tacos/goldy.png',
condition(state) {
return Math.floor(state.totalTacos - state.tacos) >= 262144;
},
},
'Big Spender III': {
description: 'Spend 16777216 tacos on skins, toppings, or automation',
icon: '/art/tacos/goldy.png',
condition(state) {
return Math.floor(state.totalTacos - state.tacos) >= 16777216;
},
},
'Big Spender IV': {
description: 'Spend 1073741824 tacos on skins, toppings, or automation',
icon: '/art/tacos/goldy.png',
condition(state) {
return Math.floor(state.totalTacos - state.tacos) >= 1073741824;
},
},

'Scrooge McDuck': {
description: 'Hoard 16777216 tacos or more at once',
icon: '/art/tacos/goldy.png',
condition(state) {
return state.tacos > 16777216;
},
},

'Costume Party': {
description: 'Unlock 3 or more skins',
icon: '/art/tacos/goldy.png',
condition(state) {
return state.ownedSkins.length >= 3;
},
},

'Gifted Student': {
description: 'Unlock 3 or more achievements',
icon: '/art/tacos/goldy.png',
condition(state) {
type PartialGameStateToAvoidCircularTypes = { unlockedAchievements: string[] };
return (state as PartialGameStateToAvoidCircularTypes).unlockedAchievements.length >= 3;
},
},
'Achievement Achievement': {
description: 'Unlock 12 or more achievements',
icon: '/art/tacos/goldy.png',
condition(state) {
type PartialGameStateToAvoidCircularTypes = { unlockedAchievements: string[] };
return (
(state as PartialGameStateToAvoidCircularTypes).unlockedAchievements.length >= 12
);
},
},
} as const satisfies Record<string, AchievementDefinition>;
77 changes: 75 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import type { Ref, UnwrapRef } from 'vue';
import type { DeepReadonly, Ref, UnwrapRef, WatchOptions } from 'vue';
import { onBeforeUnmount, onMounted, readonly, ref, watch } from 'vue';

/**
* Recursively adds missing keys to target from defaults
Expand Down Expand Up @@ -124,3 +124,76 @@ export function useInterval(callback: () => void, timeout: number): void {
onMounted(() => (interval = setInterval(callback, timeout)));
onBeforeUnmount(() => clearTimeout(interval!));
}

/**
* @param source The value that should be debounced when changed
* @param delayMs The delay in milliseconds that should be waited when debouncing
* @returns The debounced value of 'source'
*/
export function debouncedRef<T>(
source: Ref<T>,
delayMs: number,
options?: WatchOptions<false> | undefined,
): DeepReadonly<Ref<T>> {
const debouncedValue = ref(source.value) as Ref<T>;
let timeout: number | undefined;

watch(
source,
() => {
if (timeout) clearTimeout(timeout);

timeout = setTimeout(() => {
if (Array.isArray(source.value)) {
debouncedValue.value = [...source.value] as T;
} else if (typeof source.value === 'object') {
debouncedValue.value = { ...source.value } as T;
} else {
debouncedValue.value = source.value;
}
}, delayMs);
},
options,
);

return readonly(debouncedValue);
}

/**
* @param source The value that should be throttled when changed
* @param delayMs The delay in milliseconds that should be waited when throttling
* @returns The debounced value of 'source'
*/
export function throttledRef<T>(
source: Ref<T>,
delayMs: number,
options?: WatchOptions<false> | undefined,
): DeepReadonly<Ref<T>> {
const debouncedValue = ref(source.value) as Ref<T>;
let timeout: number | undefined;
let lastUpdate = performance.now();

watch(
source,
() => {
const delta = performance.now() - lastUpdate;

if (delta < delayMs && timeout) clearTimeout(timeout);

timeout = setTimeout(() => {
if (Array.isArray(source.value)) {
debouncedValue.value = [...source.value] as T;
} else if (typeof source.value === 'object') {
debouncedValue.value = { ...source.value } as T;
} else {
debouncedValue.value = source.value;
}

lastUpdate = performance.now();
}, delayMs - delta);
},
options,
);

return readonly(debouncedValue);
}
60 changes: 24 additions & 36 deletions src/views/game/GameView.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
<script setup lang="ts">
import { AUTO_CLICKERS, type GameState, SKINS, TOPPINGS } from '@/game';
import {
AutoClickersPanel,
PanelSection,
SkinsPanel,
StatsPanel,
ToppingsPanel,
} from './panels';
import { computed, ref } from 'vue';
import { useInterval, usePersistedRef } from '@/utils';
import { ACHIEVEMENTS, AUTO_CLICKERS, type GameState, SKINS, TOPPINGS } from '@/game';
import { AutoClickersPanel, DebugPanel, SkinsPanel, StatsPanel, ToppingsPanel } from './panels';
import { computed, ref, watch } from 'vue';
import { throttledRef, useInterval, usePersistedRef } from '@/utils';
import AchievementsPanel from './panels/achievements/AchievementsPanel.vue';
const state = usePersistedRef<GameState>('game:state', {
clicks: 0,
userClicks: 0,
fractionClicks: 0,
tacos: 0,
totalTacos: 0,
ownedToppings: {},
ownedAutoClickers: {},
ownedSkins: ['The Original'],
selectedSkin: 'The Original',
unlockedAchievements: [],
});
const tacoAnimationState = ref(false);
Expand Down Expand Up @@ -84,10 +84,18 @@
state.value.userClicks += 1;
}
function debugYeetData() {
window.localStorage.removeItem('game:state');
window.location.reload();
}
watch(throttledRef(state, 2000, { deep: true }), () => {
const unlockedAchievements: Set<string> = new Set(state.value.unlockedAchievements);
const lockedAchievements = Object.entries(ACHIEVEMENTS).filter(
([name]) => !unlockedAchievements.has(name),
);
for (const [achievementName, achievement] of lockedAchievements) {
if (achievement.condition(state.value)) {
state.value.unlockedAchievements.push(achievementName as keyof typeof ACHIEVEMENTS);
}
}
});
</script>

<style scoped>
Expand All @@ -109,28 +117,8 @@
:variety-bonus="varietyBonus"
:auto-clicks-per-second="autoClicksPerSecond"
/>
<PanelSection title="Debug">
<div class="flex flex-col space-y-4 bg-white bg-opacity-50">
<p>{{ tacoAnimationState }}</p>

<code class="whitespace-pre">
Game State: <span class="select-text">{{ state }}</span>
</code>
</div>

<div class="flex gap-2">
<button type="button" @click="debugYeetData()" class="w-full bg-red-500 p-1.5">
Yeet Data
</button>
<button
type="button"
@click="state.tacos += 1000"
class="w-full bg-green-500 p-1.5"
>
Add 1k
</button>
</div>
</PanelSection>
<AchievementsPanel :state="state" />
<DebugPanel v-model:state="state" :taco-animation-state="tacoAnimationState" />
</div>

<div class="mt-10 flex flex-col max-lg:my-auto max-lg:pt-[7.5vw] lg:col-span-2 lg:-mt-32">
Expand Down
48 changes: 48 additions & 0 deletions src/views/game/panels/achievements/AchievementsPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { ACHIEVEMENTS, type GameState } from '@/game';
import { computed } from 'vue';
import { faCheck } from '@fortawesome/pro-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import PanelSection from '../PanelSection.vue';
const props = defineProps<{ state: GameState }>();
const unlockedAchievements = computed(() => new Set(props.state.unlockedAchievements) as Set<string>);
</script>

<template>
<PanelSection title="Achievements">
<div
v-for="[achievementName, achievement] in Object.entries(ACHIEVEMENTS)"
:key="achievementName"
class="flex items-center space-x-1.5 bg-white p-1 bg-opacity-60"
>
<span
class="flex min-h-[56px] min-w-[56px] items-center justify-center"
:class="{ 'opacity-50': !unlockedAchievements.has(achievementName) }"
>
<img
:src="achievement.icon"
:alt="achievementName"
class="h-full max-h-[56px] max-w-[56px] p-2"
/>
</span>

<span class="flex flex-col text-left">
<span
class="flex items-center text-sm font-semibold text-gray-600"
:class="{ 'text-opacity-50': !unlockedAchievements.has(achievementName) }"
>
<span>{{ achievementName }}</span>
<FontAwesomeIcon v-if="unlockedAchievements.has(achievementName)" :icon="faCheck" size="lg" class="-mt-0.5 ml-1.5 text-purple-600" />
</span>
<p
class="text-xs text-gray-500"
:class="{ 'text-opacity-75': !unlockedAchievements.has(achievementName) }"
>
{{ achievement.description }}
</p>
</span>
</div>
</PanelSection>
</template>
1 change: 1 addition & 0 deletions src/views/game/panels/debug/DebugPanel.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { GameState } from '@/game';
import PanelSection from '../PanelSection.vue';
defineProps<{
tacoAnimationState: boolean;
Expand Down
12 changes: 11 additions & 1 deletion src/views/game/panels/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import AchievementsPanel from './achievements/AchievementsPanel.vue';
import AutoClickersPanel from './auto-clickers/AutoClickersPanel.vue';
import DebugPanel from './debug/DebugPanel.vue';
import PanelSection from './PanelSection.vue';
import SkinsPanel from './skins/SkinsPanel.vue';
import StatsPanel from './stats/StatsPanel.vue';
import ToppingsPanel from './toppings/ToppingsPanel.vue';

export { AutoClickersPanel, PanelSection, SkinsPanel, StatsPanel, ToppingsPanel };
export {
AchievementsPanel,
AutoClickersPanel,
DebugPanel,
PanelSection,
SkinsPanel,
StatsPanel,
ToppingsPanel,
};

0 comments on commit e0f695b

Please sign in to comment.