From 6ffdaeff24f7ba0c906ba4d187a2d4508c252005 Mon Sep 17 00:00:00 2001 From: Milo Weinberg Date: Wed, 27 Nov 2024 21:29:23 -0500 Subject: [PATCH] Move debug to component + add achievements (missing icons) --- src/game.ts | 119 ++++++++++++++++++ src/utils.ts | 77 +++++++++++- src/views/game/GameView.vue | 60 ++++----- .../panels/achievements/AchievementsPanel.vue | 48 +++++++ src/views/game/panels/debug/DebugPanel.vue | 1 + src/views/game/panels/index.ts | 12 +- 6 files changed, 278 insertions(+), 39 deletions(-) create mode 100644 src/views/game/panels/achievements/AchievementsPanel.vue diff --git a/src/game.ts b/src/game.ts index 581df3a..e950d3c 100644 --- a/src/game.ts +++ b/src/game.ts @@ -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 = { @@ -145,3 +150,117 @@ export const AUTO_CLICKERS = { }, } as const satisfies Record; export type OwnedAutoClickers = Partial>; + +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; diff --git a/src/utils.ts b/src/utils.ts index e0ecc3e..2612977 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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 @@ -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( + source: Ref, + delayMs: number, + options?: WatchOptions | undefined, +): DeepReadonly> { + const debouncedValue = ref(source.value) as Ref; + 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( + source: Ref, + delayMs: number, + options?: WatchOptions | undefined, +): DeepReadonly> { + const debouncedValue = ref(source.value) as Ref; + 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); +} diff --git a/src/views/game/GameView.vue b/src/views/game/GameView.vue index 6e6bd73..246e1cb 100644 --- a/src/views/game/GameView.vue +++ b/src/views/game/GameView.vue @@ -1,25 +1,25 @@