diff --git a/package.json b/package.json index aec91fa..25edcaf 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "test": "vitest" }, "dependencies": { + "clsx": "^1.2.1", "react": "^18.0.0", "react-dom": "^18.0.0" }, diff --git a/src/common/GameStateInterface.ts b/src/common/GameStateInterface.ts index a14c3c7..0b0121a 100644 --- a/src/common/GameStateInterface.ts +++ b/src/common/GameStateInterface.ts @@ -2,10 +2,11 @@ import { Children } from 'react'; export interface GameState { antimatter: number; tickspeedPrice: number; - tickspeedDeceaseRate: number, + tickspeedDeceaseRate: number; resetGameCounter: number; galaxyCounter: number; lastSavedTime: number; + showGameSavedNotification: boolean; dims: Dim[]; } @@ -20,6 +21,6 @@ export interface Dim { export type DimProps = { nthDim: number; gs: GameState; - setGameState: (fn: (gameState: GameState) => void) => void; + dispatch: Function; children: string; }; diff --git a/src/common/initialGameState.ts b/src/common/initialGameState.ts index 08e6dba..7589ca7 100644 --- a/src/common/initialGameState.ts +++ b/src/common/initialGameState.ts @@ -6,7 +6,8 @@ let initialGameState: GameState = { tickspeedDeceaseRate: 0.11, resetGameCounter: 2, galaxyCounter: 0, - lastSavedTime: 0, + lastSavedTime: Date.now(), + showGameSavedNotification: false, dims: [ { nthDim: 0, diff --git a/src/common/reducer.tsx b/src/common/reducer.tsx new file mode 100644 index 0000000..1fe3630 --- /dev/null +++ b/src/common/reducer.tsx @@ -0,0 +1,122 @@ +import { Dim, GameState } from './GameStateInterface'; +import initialGameState from './initialGameState'; + +export const ACTIONS = { + TIMER_CALLBACK: 'TIMER_CALLBACK', + UPDATE_DIM: 'UPDATE_DIM', + UPDATE_10TH_DIM: 'UPDATE_10TH_DIM', + UPDATE_TICKSPEED_ONCE: 'UPDATE_TICKSPEED_ONCE', + UPDATE_TICKSPEED_MAX: 'UPDATE_TICKSPEED_MAX', + RESET_TO_UNLOCK_DIM: 'RESET_TO_UNLOCK_DIM', + RESET_TO_UNLOCK_TICKSPEED: 'RESET_TO_UNLOCK_TICKSPEED', + RESET_TO_INITIAL_VALUES: 'RESET_TO_INITIAL_VALUES', + POP_UP: 'POP_UP', + SAVE_DATA: 'SAVE_DATA', + TOOGLE_GAME_NOTIFICATION_OPEN: 'TOOGLE_GAME_NOTIFICATION_OPEN', + TOOGLE_GAME_NOTIFICATION_CLOSE: 'TOOGLE_GAME_NOTIFICATION_CLOSE', +}; + +export function reducer(state: GameState, action: { type: string; payload?: any }) { + switch (action.type) { + case ACTIONS.TIMER_CALLBACK: + return { + ...state, + antimatter: state.antimatter + state.dims[0].dimCount * state.dims[0].dimFactor, + dims: state.dims.map((dim: Dim, index: number) => { + return { + ...dim, + dimCount: + dim.dimCount + + ((state.dims[index + 1]?.dimCount ?? 0) * + (state.dims[index + 1]?.dimFactor ?? 0)) / + Math.pow(10, index + 1), + }; + }), + }; + + case ACTIONS.UPDATE_DIM: + return { + ...state, + antimatter: + state.antimatter - + state.dims[action.payload.nthDim].dimPrice * action.payload.quantity, + dims: state.dims.map((dim: Dim) => { + if (dim.nthDim !== action.payload.nthDim) return dim; + return { + ...dim, + dimCount: dim.dimCount + action.payload.quantity, + dimFactorCount: dim.dimFactorCount + action.payload.quantity, + }; + }), + }; + + case ACTIONS.UPDATE_10TH_DIM: + return { + ...state, + dims: state.dims.map((dim: Dim) => { + if (dim.nthDim !== action.payload.nthDim) return dim; + return { + ...dim, + dimPrice: dim.dimPrice * 10, + dimFactor: dim.dimFactor * 2, + dimFactorCount: 0, + }; + }), + }; + + case ACTIONS.UPDATE_TICKSPEED_ONCE: + return { + ...state, + antimatter: state.antimatter - state.tickspeedPrice, + tickspeedPrice: state.tickspeedPrice * 10, + }; + case ACTIONS.UPDATE_TICKSPEED_MAX: + return { + ...state, + antimatter: state.antimatter - action.payload.purchasePrice, + tickspeedPrice: state.tickspeedPrice * 10 ** action.payload.maxPurchaseQtys, + }; + + case ACTIONS.RESET_TO_UNLOCK_DIM: + return { + ...JSON.parse(initialGameState), + galaxyCounter: state.galaxyCounter, + tickspeedDeceaseRate: state.tickspeedDeceaseRate, + resetGameCounter: state.resetGameCounter + 1, + lastSavedTime: state.lastSavedTime, + }; + + case ACTIONS.RESET_TO_UNLOCK_TICKSPEED: + return { + ...JSON.parse(initialGameState), + galaxyCounter: state.galaxyCounter + 1, + tickspeedDeceaseRate: state.tickspeedDeceaseRate * 1.1, + lastSavedTime: state.lastSavedTime, + }; + + case ACTIONS.RESET_TO_INITIAL_VALUES: + return JSON.parse(initialGameState); + + case ACTIONS.SAVE_DATA: + return { + ...state, + lastSavedTime: Date.now(), + }; + + case ACTIONS.TOOGLE_GAME_NOTIFICATION_CLOSE: { + return { + ...state, + showGameSavedNotification: false, + }; + } + case ACTIONS.TOOGLE_GAME_NOTIFICATION_OPEN: { + return { + ...state, + showGameSavedNotification: true, + }; + } + + default: + return state; + } +} diff --git a/src/components/App.tsx b/src/components/App.tsx index f44f9e2..78d3f7f 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,47 +1,33 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useReducer } from 'react'; import '../styles/App.css'; -import Dimension from './Dimension'; -import GameResets from './GameResets'; -import Tickspeed from './TickSpeed'; - -import { GameState, Dim } from '../common/GameStateInterface'; +import Dimension from './Dimention/Dimension'; +import GameResets from './GameResetBtns/GameResets'; +import Tickspeed from './Tickspeed/TickSpeed'; +import DisplayAntimatter from './DisplayAntimatter/DisplayAntimatter'; import initialGameState from '../common/initialGameState'; -import DisplayAntimatter from './DisplayAntimatter'; -import { useSaveToLocalStorage } from './useSaveToLocalStorage'; + +import { useLocalStorage } from '../customeHooks/useLocalStorage'; +import { ACTIONS, reducer } from '../common/reducer'; +import GameSavedNotification from './GameSavedNotification/GameSavedNotification'; function App() { - const [gameState, setGameState] = useState(() => + const [state, dispatch] = useReducer( + reducer, JSON.parse(localStorage.getItem('data') || initialGameState) ); - /* JSON.parse(localStorage.getItem('data')) */ const timerExpiredCallback = useRef(() => {}); - const clockSpeedRef = useRef(2000); + const tickspeedRef = useRef(2000); const timerIdRef = useRef(-1); - useSaveToLocalStorage(gameState, setGameState); + useLocalStorage(state, dispatch); - timerExpiredCallback.current = () => { - setGameState((prevGS: GameState) => ({ - ...prevGS, - antimatter: prevGS.antimatter + prevGS.dims[0].dimCount * prevGS.dims[0].dimFactor, - dims: gameState.dims.map((dim: Dim, index: number) => { - return { - ...dim, - dimCount: - dim.dimCount + - ((gameState.dims[index + 1]?.dimCount ?? 0) * - (gameState.dims[index + 1]?.dimFactor ?? 0)) / - Math.pow(10, index + 1), - }; - }), - })); - }; + timerExpiredCallback.current = () => dispatch({ type: ACTIONS.TIMER_CALLBACK }); useEffect(() => { const startTimer = () => { - timerIdRef.current = setTimeout(() => { + timerIdRef.current = window.setTimeout(() => { timerExpiredCallback.current(); startTimer(); - }, clockSpeedRef.current); + }, tickspeedRef.current); }; startTimer(); @@ -51,60 +37,65 @@ function App() { return (
- + + + + gs={state} // gs = gamestate + dispatch={dispatch} + tickspeedRef={tickspeedRef}> - - {`First Dimension Cost: ${gameState.dims[0].dimPrice}`} + + {`First Dimension Cost: ${state.dims[0].dimPrice}`} - - {`Second Dimension Cost: ${gameState.dims[1].dimPrice}`} + + {`Second Dimension Cost: ${state.dims[1].dimPrice}`} { //the 3. dimension must be unlocked } - {gameState.resetGameCounter > 2 && ( - - {`Third Dimension Cost: ${gameState.dims[2].dimPrice}`} + {state.resetGameCounter > 2 && ( + + {`Third Dimension Cost: ${state.dims[2].dimPrice}`} )} - {gameState.resetGameCounter > 3 && ( - - {`Forth Dimension Cost: ${gameState.dims[2].dimPrice}`} + {state.resetGameCounter > 3 && ( + + {`Forth Dimension Cost: ${state.dims[2].dimPrice}`} )} - {gameState.resetGameCounter > 4 && ( - - {`Fifth Dimension Cost: ${gameState.dims[2].dimPrice}`} + {state.resetGameCounter > 4 && ( + + {`Fifth Dimension Cost: ${state.dims[2].dimPrice}`} )} - {gameState.resetGameCounter > 5 && ( - - {`Sixth Dimension Cost: ${gameState.dims[2].dimPrice}`} + {state.resetGameCounter > 5 && ( + + {`Sixth Dimension Cost: ${state.dims[2].dimPrice}`} )} - {gameState.resetGameCounter > 6 && ( - - {`Seventh Dimension Cost: ${gameState.dims[2].dimPrice}`} + {state.resetGameCounter > 6 && ( + + {`Seventh Dimension Cost: ${state.dims[2].dimPrice}`} )} - {gameState.resetGameCounter > 7 && ( - - {`Eight Dimension Cost: ${gameState.dims[2].dimPrice}`} + {state.resetGameCounter > 7 && ( + + {`Eight Dimension Cost: ${state.dims[2].dimPrice}`} )} - +

- +
); } export default App; +function clsx(arg0: any) { + throw new Error('Function not implemented.'); +} diff --git a/src/components/Dimension.tsx b/src/components/Dimention/Dimension.tsx similarity index 51% rename from src/components/Dimension.tsx rename to src/components/Dimention/Dimension.tsx index e06022d..0d099a4 100644 --- a/src/components/Dimension.tsx +++ b/src/components/Dimention/Dimension.tsx @@ -1,49 +1,21 @@ -import { GameState, DimProps, Dim } from '../common/GameStateInterface'; +import { DimProps } from '../../common/GameStateInterface'; +import { ACTIONS } from '../../common/reducer'; export default function Dimension(props: DimProps) { - const { nthDim, gs, setGameState } = props; + const { nthDim, gs, dispatch } = props; const handleDimBuy = (quantity: number) => { - const newDim = [gs.dims[nthDim].dimCount + quantity]; - if (gs.antimatter >= gs.dims[nthDim].dimPrice) { - /* - setDimCount(dimCount => dimCount + quantity); - setAntimatter(prevValue => prevValue - price * quantity); - setDimFactorCount(prevCount => prevCount + 1); */ - setGameState((prevGS: GameState) => ({ - ...prevGS, - antimatter: prevGS.antimatter - prevGS.dims[nthDim].dimPrice * quantity, - dims: prevGS.dims.map((dim: Dim, index: number) => { - if (dim.nthDim !== nthDim) return dim; - return { - ...dim, - dimCount: dim.dimCount + quantity, - dimFactorCount: dim.dimFactorCount + quantity, - }; - }), - })); + dispatch({ type: ACTIONS.UPDATE_DIM, payload: { nthDim: nthDim, quantity: quantity } }); } if ( ((gs.dims[nthDim].dimCount + 1) % 10 === 0 && gs.dims[nthDim].dimCount > 1) || quantity === 10 ) { - /* - setPrice(prevPrice => prevPrice * 10); - setFactor(prevFactor => prevFactor * 2) - setDimFactorCount(0) */ - setGameState((prevGS: GameState) => ({ - ...prevGS, - dims: prevGS.dims.map((dim: Dim, index: number) => { - if (dim.nthDim !== nthDim) return dim; - return { - ...dim, - dimPrice: dim.dimPrice * 10, - dimFactor: dim.dimFactor * 2, - dimFactorCount: 0, - }; - }), - })); + dispatch({ + type: ACTIONS.UPDATE_10TH_DIM, + payload: { nthDim: nthDim, quantity: quantity }, + }); } }; diff --git a/src/components/DisplayAntimatter.tsx b/src/components/DisplayAntimatter/DisplayAntimatter.tsx similarity index 87% rename from src/components/DisplayAntimatter.tsx rename to src/components/DisplayAntimatter/DisplayAntimatter.tsx index 4e4c685..750a861 100644 --- a/src/components/DisplayAntimatter.tsx +++ b/src/components/DisplayAntimatter/DisplayAntimatter.tsx @@ -1,4 +1,4 @@ -import { GameState } from '../common/GameStateInterface'; +import { GameState } from '../../common/GameStateInterface'; export default function DisplayAntimatter(props: { gameState: GameState }) { const { gameState } = props; diff --git a/src/components/GameResetBtns/GameResets.tsx b/src/components/GameResetBtns/GameResets.tsx new file mode 100644 index 0000000..cea88f2 --- /dev/null +++ b/src/components/GameResetBtns/GameResets.tsx @@ -0,0 +1,77 @@ +import { useEffect, useRef, useState } from 'react'; +import { GameState } from '../../common/GameStateInterface'; +import initialGameState from '../../common/initialGameState'; +import { ACTIONS } from '../../common/reducer'; + +export default function GameResets(props: { gs: GameState; dispatch: Function }) { + const { gs, dispatch } = props; + const [popUp, setPopUp] = useState(false); + const resetGameCounterRef = useRef(gs.resetGameCounter); + + useEffect(() => { + resetGameCounterRef.current = gs.resetGameCounter; + }, [gs.resetGameCounter]); + + const handleResetGameClick = () => { + dispatch({ type: ACTIONS.RESET_TO_UNLOCK_DIM }); + }; + + const handleGalaxyBtn = () => { + dispatch({ type: ACTIONS.RESET_TO_UNLOCK_TICKSPEED }); + }; + + const disableResetBtn = () => { + if (gs.resetGameCounter < 7 && gs.dims[gs.resetGameCounter - 1].dimCount < 20) return true; + else return false; + }; + + return ( + <> +
+

{`Dimension Shift (${gs.resetGameCounter}) `}

+

{`requires 20 ${gs.resetGameCounter}. Dimension `}

+ +
+
+

{`Antimatter Galaxies (${gs.galaxyCounter})`}

+

{`requires 80 8. Dimension `}

+ +
+
+

{`reset game`}

+

{`resets everything`}

+ +
+ {popUp && ( +
+
+ + +
+
+ )} + + ); +} diff --git a/src/components/GameResets.tsx b/src/components/GameResets.tsx deleted file mode 100644 index cdb2364..0000000 --- a/src/components/GameResets.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { GameState } from '../common/GameStateInterface'; -import initialGameState from '../common/initialGameState'; - -export default function GameResets(props: { gameState: GameState; setGameState: any }) { - const { gameState, setGameState } = props; - const resetGameCounterRef = useRef(gameState.resetGameCounter); - - useEffect(() => { - resetGameCounterRef.current = gameState.resetGameCounter; - }, [gameState.resetGameCounter]); - - const handleResetGameClick = () => { - setGameState((prevGS: GameState) => ({ - ...JSON.parse(initialGameState), - galaxyCounter: prevGS.galaxyCounter, - resetGameCounter: prevGS.resetGameCounter + 1, - lastSavedTime: prevGS.lastSavedTime, - })); - }; - - const handleGalaxyBtn = () => { - setGameState((prevGS: GameState) => ({ - ...JSON.parse(initialGameState), - galaxyCounter: prevGS.galaxyCounter + 1, - tickspeedDeceaseRate: 0.12, - resetGameCounter: prevGS.resetGameCounter, - lastSavedTime: prevGS.lastSavedTime, - })); - }; - - const disableResetBtn = () => { - if ( - gameState.resetGameCounter < 7 && - gameState.dims[gameState.resetGameCounter - 1].dimCount < 20 - ) - return true; - else return false; - }; - - return ( - <> -
-

{`Dimension Shift (${gameState.resetGameCounter}) `}

-

{`requires 20 ${gameState.resetGameCounter}. Dimension `}

- -
-
-

{`Antimatter Galaxies (${gameState.galaxyCounter})`}

-

{`requires 80 8. Dimension `}

- -
-
-

{`reset game`}

-

{`resets everything`}

- -
- - ); -} diff --git a/src/components/GameSavedNotification/GameSavedNotification.tsx b/src/components/GameSavedNotification/GameSavedNotification.tsx new file mode 100644 index 0000000..4aee238 --- /dev/null +++ b/src/components/GameSavedNotification/GameSavedNotification.tsx @@ -0,0 +1,42 @@ +import React, { useEffect, useState } from 'react'; +import { GameState } from '../../common/GameStateInterface'; +import { ACTIONS } from '../../common/reducer'; +import '../../styles/GameSavedNotification.css'; + +export default function GameSavedNotification(gs: GameState, dispatch: Function) { + let TIMER: number = 0; + function handleTimeout() { + TIMER = window.setTimeout(() => { + dispatch({ type: ACTIONS.TOOGLE_GAME_NOTIFICATION_CLOSE }); + }, 3500); + } + + useEffect(() => { + if (Date.now() <= gs.lastSavedTime + 5) { + dispatch({ type: ACTIONS.TOOGLE_GAME_NOTIFICATION_OPEN }); + } + }, [gs.lastSavedTime]); + + useEffect(() => { + console.log(gs.showGameSavedNotification); + }, [gs.showGameSavedNotification]); + + useEffect(() => { + if (gs.showGameSavedNotification) { + handleTimeout(); + } + return () => { + clearTimeout(TIMER); + }; + }, [gs.showGameSavedNotification, TIMER]); + + return ( + gs.showGameSavedNotification && ( +
+
+
Game saved !
+
+
+ ) + ); +} diff --git a/src/components/TickSpeed.tsx b/src/components/TickSpeed.tsx deleted file mode 100644 index 0111ad1..0000000 --- a/src/components/TickSpeed.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useRef, useEffect, useState } from 'react'; -import { GameState } from '../common/GameStateInterface'; - -export default function Tickspeed(props: { - gameState: GameState; - setGameState: any; - clockSpeedRef: any; -}) { - const { gameState, setGameState, clockSpeedRef } = props; - const [buyMax, setBuyMax] = useState( - Math.log10(gameState.antimatter / gameState.tickspeedPrice) - ); - // const maxPossibleBuys = useRef(~~Math.log10(gameState.antimatter / gameState.tickspeedPrice)); - const handleTickBtnClick = () => { - clockSpeedRef.current = clockSpeedRef.current * (1 - gameState.tickspeedDeceaseRate); - setGameState((prevGS: GameState) => ({ - ...prevGS, - antimatter: prevGS.antimatter - prevGS.tickspeedPrice, - tickspeedPrice: prevGS.tickspeedPrice * 10, - })); - }; - - const getMaxPurchaseQtyRecursive = (price: number, antimatter: number, quantity = 0): number => { - if (antimatter < price) { - return quantity - } else { - return getMaxPurchaseQtyRecursive(price * 10, antimatter - price, quantity + 1); - } - }; - - const maxPurchasableQuantity = getMaxPurchaseQtyRecursive( - gameState.tickspeedPrice, - gameState.antimatter, - 0 - ); - - const getCostForPurchaseQty = (firstCost: number, quantity: number): number => { - if (quantity == 1) { - return firstCost; - } else { - return firstCost + getCostForPurchaseQty(firstCost * 10, quantity - 1); - } - } - - const handleBuyMaxClick = () => { - clockSpeedRef.current = clockSpeedRef.current * (1 - gameState.tickspeedDeceaseRate) ** maxPurchasableQuantity; - setGameState((prevGS: GameState) => ({ - ...prevGS, - antimatter: prevGS.antimatter - getCostForPurchaseQty(maxPurchasableQuantity, prevGS.tickspeedPrice), - tickspeedPrice: prevGS.tickspeedPrice * 10 ** maxPurchasableQuantity, - })); - }; - - - return ( -
-

{`The current clockspeed is ${clockSpeedRef.current.toFixed( - 0 - )} ms. Reduce the tickspeed by ${gameState.tickspeedDeceaseRate * 100}%.`}

-
- - -
-
- ); -} diff --git a/src/components/Tickspeed/TickSpeed.tsx b/src/components/Tickspeed/TickSpeed.tsx new file mode 100644 index 0000000..b87a4ad --- /dev/null +++ b/src/components/Tickspeed/TickSpeed.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react'; +import { GameState } from '../../common/GameStateInterface'; +import { getCostForPurchaseQty } from './getCostForPurchaseQty'; +import { calcMaxPurchasableQty } from './calcMaxPurchasableQty'; +import { ACTIONS } from '../../common/reducer'; + +export default function Tickspeed(props: { gs: GameState; dispatch: Function; tickspeedRef: any }) { + const { gs, dispatch, tickspeedRef } = props; + + // calculte the maximal purchasable + const maxPurchaseQtys = calcMaxPurchasableQty({ + cost: gs.tickspeedPrice, + balance: gs.antimatter, + qty: 0, + }); + const purchasePrice = getCostForPurchaseQty(gs.tickspeedPrice, maxPurchaseQtys); + + const handleTickBtnClick = () => { + tickspeedRef.current = tickspeedRef.current * (1 - gs.tickspeedDeceaseRate); + dispatch({ type: ACTIONS.UPDATE_TICKSPEED_ONCE }); + }; + const handleBuyMaxClick = () => { + tickspeedRef.current = + tickspeedRef.current * (1 - gs.tickspeedDeceaseRate) ** maxPurchaseQtys; + dispatch({ + type: ACTIONS.UPDATE_TICKSPEED_MAX, + payload: { maxPurchaseQtys: maxPurchaseQtys, purchasePrice: purchasePrice }, + }); + }; + + return ( +
+

{`The current clockspeed is ${tickspeedRef.current.toFixed( + 0 + )} ms. Reduce the tickspeed by ${(gs.tickspeedDeceaseRate* 100).toFixed(1)}%.`}

+
+ + +
+
+ ); +} diff --git a/src/components/Tickspeed/calcMaxPurchasableQty.test.ts b/src/components/Tickspeed/calcMaxPurchasableQty.test.ts new file mode 100644 index 0000000..862afff --- /dev/null +++ b/src/components/Tickspeed/calcMaxPurchasableQty.test.ts @@ -0,0 +1,19 @@ +import { calcMaxPurchasableQty } from './calcMaxPurchasableQty'; +import { describe, expect, it } from 'vitest'; + +describe('maxPurchaseQty', () => { + it('return zero if we have no money', () => { + expect(calcMaxPurchasableQty({ cost: 1234242, balance: 0, qty: 0 })).toEqual(0); + }); + it('return one if we can afford only one', () => { + expect(calcMaxPurchasableQty({ cost: 10, balance: 100, qty: 0 })).toEqual(1); + }); + + it('return 2 if we can afford only 2', () => { + expect(calcMaxPurchasableQty({ cost: 10, balance: 110, qty: 0 })).toEqual(2); + }); + it('return 3 if we can afford only 3', () => { + expect(calcMaxPurchasableQty + ({ cost: 10, balance: 1110, qty: 0 })).toEqual(3); + }); +}); diff --git a/src/components/Tickspeed/calcMaxPurchasableQty.ts b/src/components/Tickspeed/calcMaxPurchasableQty.ts new file mode 100644 index 0000000..95f23c0 --- /dev/null +++ b/src/components/Tickspeed/calcMaxPurchasableQty.ts @@ -0,0 +1,10 @@ +interface calcMaxPurchasableQtyInterface { + cost: number; + balance: number; + qty: number; +} + +export const calcMaxPurchasableQty = ({ cost, balance, qty = 0 }: calcMaxPurchasableQtyInterface): number => { + if (cost > balance) return qty; + else return calcMaxPurchasableQty({ cost: cost * 10, balance: balance - cost, qty: qty + 1 }); +}; diff --git a/src/components/Tickspeed/getCostForPurchaseQty.test.tsx b/src/components/Tickspeed/getCostForPurchaseQty.test.tsx new file mode 100644 index 0000000..3a4c67f --- /dev/null +++ b/src/components/Tickspeed/getCostForPurchaseQty.test.tsx @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { getCostForPurchaseQty } from './getCostForPurchaseQty'; + +describe('getCostForPurchaseQty', () => { + it('if nothing is bought, there is no cost increase', () => { + expect(getCostForPurchaseQty(10, 0)).toEqual(0); + }); + it('if 1 Qty is bought, the cost is the initial cost', () => { + expect(getCostForPurchaseQty(10, 1)).toEqual(10); + }); + it('if 2 Qty are bought, the cost is the initial cost + 10 *initial cost', () => { + expect(getCostForPurchaseQty(10, 2)).toEqual(110); + }); + it('if 5 Qty are bought, the cost is the initial cost + 10 *initial cost + ... + 10^5*initial cost', () => { + expect(getCostForPurchaseQty(10, 2)).toEqual(111110); + }); +}); diff --git a/src/components/Tickspeed/getCostForPurchaseQty.tsx b/src/components/Tickspeed/getCostForPurchaseQty.tsx new file mode 100644 index 0000000..295c8f4 --- /dev/null +++ b/src/components/Tickspeed/getCostForPurchaseQty.tsx @@ -0,0 +1,7 @@ +export const getCostForPurchaseQty = (firstCost: number, quantity: number): number => { + if (quantity === 0) return 0; + else if (quantity === 1) { + return firstCost; + } else return firstCost + getCostForPurchaseQty(firstCost * 10, quantity - 1) +}; + \ No newline at end of file diff --git a/src/components/getMaxPurchaseQtyRecursive.test.ts b/src/components/getMaxPurchaseQtyRecursive.test.ts deleted file mode 100644 index 0e45733..0000000 --- a/src/components/getMaxPurchaseQtyRecursive.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { maxPurchaseQty } from "./getMaxPurchaseQtyRecursive"; -import {describe, expect, it} from "vitest"; - -describe("getMaxPurchaseQtyRecursive ", ()=>{ - - it('return zero if we have no money', () => { - expect(maxPurchaseQty({cost: 1234242, balance: 0})).toEqual(0) - }) - it('return one if we can afoard only one', () => { - expect(maxPurchaseQty({ cost: 100, balance: 100})).toEqual(1) - }) - -}); diff --git a/src/components/getMaxPurchaseQtyRecursive.ts b/src/components/getMaxPurchaseQtyRecursive.ts deleted file mode 100644 index cc9a8e3..0000000 --- a/src/components/getMaxPurchaseQtyRecursive.ts +++ /dev/null @@ -1,7 +0,0 @@ - - -export const maxPurchaseQty = ({cost, balance}) => { - - return 0; - -}; diff --git a/src/components/useSaveToLocalStorage.tsx b/src/components/useSaveToLocalStorage.tsx deleted file mode 100644 index 67fba85..0000000 --- a/src/components/useSaveToLocalStorage.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { Dim, GameState } from '../common/GameStateInterface'; - -export function useSaveToLocalStorage(gameState: GameState, setGameState) { - const saveLocStorageRef = useRef(-1); - const displayLocStorageRef = useRef(-1); - - // saves the created object every minuit to localStorage - - useEffect(() => { - const saveDateRecursive = (gameState: GameState) => { - if (Date.now() > gameState.lastSavedTime + 60 * 1000) { - localStorage.setItem('data', JSON.stringify(gameState)); - console.log('data saved', gameState.lastSavedTime); - setGameState((prevGS: GameState) => ({ - ...prevGS, - lastSavedTime: Date.now(), - })); - } - }; - saveDateRecursive(gameState); - - }, [gameState]); - - // this prints the saved object from local storage. It is just for dev. No final purpose - /* useEffect(() => { - const printStorage = () => { - displayLocStorageRef.current = setTimeout(() => { - let savedData= JSON.parse(localStorage.getItem('data') ?? ''); - console.log('dataRef from storage', savedData); - printStorage(); - }, 10000); - }; - printStorage(); - - return () => clearTimeout(displayLocStorageRef.current); - }, []); */ -} diff --git a/src/customeHooks/useLocalStorage.tsx b/src/customeHooks/useLocalStorage.tsx new file mode 100644 index 0000000..98659f1 --- /dev/null +++ b/src/customeHooks/useLocalStorage.tsx @@ -0,0 +1,21 @@ +import { useEffect, useRef } from 'react'; +import { Dim, GameState } from '../common/GameStateInterface'; +import { ACTIONS } from '../common/reducer'; + +export function useLocalStorage(gameState: GameState, dispatch: Function): void { + //const saveLocStorageRef = useRef(-1); + //const displayLocStorageRef = useRef(-1); + + // saves the created object every minuit to localStorage + + useEffect(() => { + const saveDateRecursive = (gameState: GameState) => { + if (Date.now() > gameState.lastSavedTime + 5 * 60 * 1000) { + localStorage.setItem('data', JSON.stringify(gameState)); + console.log('data saved', gameState.lastSavedTime); + dispatch({ type: ACTIONS.SAVE_DATA }); + } + }; + saveDateRecursive(gameState); + }, [gameState]); +} diff --git a/src/styles/App.css b/src/styles/App.css index 3ee50dc..afb338e 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -1,5 +1,6 @@ * { -/* border: 1px solid black; */ + /* border: 1px solid black; */ + box-sizing: border-box; } .App { @@ -24,7 +25,6 @@ grid-template-columns: repeat(4, 1fr); gap: 10px; grid-auto-rows: minmax(40px, auto); - } .gridContainer3Rows { @@ -42,12 +42,44 @@ align-items: center; text-align: center; } +.spaceAround { + justify-content: space-around; +} -.cols-2{ +.cols-2 { grid-template-columns: repeat(3, 1fr); margin: 2rem 5rem; } +.overlay { + position: fixed; + width: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + + z-index: 10000; +} + +.pop-up { + position: absolute; + top: 45%; + left: 0; + + width: 50%; + height: 10%; + margin-left: 25%; + margin-right: 25%; + padding: 15px; + + z-index: 100001; + background: grey; + border-radius: 10px; + + box-shadow: 5px 7px 5px rgba(0, 0, 0, 0.75); +} + .btn { font-size: calc(10px + 1vmin); padding: 0.2em 1em 0.2em 1em; @@ -60,16 +92,100 @@ background-image: linear-gradient(to right, #29323c, #485563, #2b5876, #4e4376); box-shadow: 0 4px 15px 0 rgba(45, 54, 65, 0.75); } -.btn:disabled{ +.btn:disabled { background: initial; box-shadow: initial; } -.sm{ +.sm { font-size: calc(10px + 0.5vmin); } -.btn:disabled{ +.btn:disabled { background-color: rgb(255, 254, 254); border: 1px rgb(240, 238, 238) solid; color: rgb(180, 177, 177); } + +.red { + background-color: red; + font-style: bold; + color: white; +} +.black { + background: rgba(0, 0, 0, 0.85); +} + +.snackbar-overlay { + position: fixed; + left: 0; + bottom: 0; + top: 0; + right: 0; + z-index: -1; +} + +.gameSaved { + position: absolute; + top: 2%; + right: 1%; + visibility: hidden; + width: 10%; + height: 5%; + padding: 15px; + + z-index: 90001; + background-color: rgb(2, 23, 157); + color: white; + font-weight: 700; + border-radius: 5px; + -webkit-animation: fadein 3s, fadeout 3s 3s; + animation: fadein 3s linear, fadeout 3s linear 3s; +} +.visible { + visibility: visible; +} + +/* Animations to fade the snackbar in and out */ +@-webkit-keyframes fadein { + from { + top: 0%; + opacity: 0; + } + to { + top: 2%; + opacity: 1; + } +} + +@keyframes fadein { + from { + top: -0%; + opacity: 0; + } + to { + top: 2%; + opacity: 1; + } +} + +@-webkit-keyframes fadeout { + from { + top: 2%; + opacity: 1; + } + to { + top: -3%; + opacity: 0; + } +} + +@keyframes fadeout { + from { + top: 2%; + opacity: 1; + } + to { + top: -3%; + opacity: 0; + } +} diff --git a/src/styles/GameSavedNotification.css b/src/styles/GameSavedNotification.css new file mode 100644 index 0000000..61d694f --- /dev/null +++ b/src/styles/GameSavedNotification.css @@ -0,0 +1,55 @@ +* { + box-sizing: border-box; +} + +.snackbar-overlay { + position: fixed; + left: 0; + bottom: 0; + top: 0; + right: 0; + z-index: -1; +} + +.snackbar { + position: absolute; + z-index: 1000; + top: 2%; + right: 7%; + transform: translateX(50%); + height: auto; + padding: 0.625rem 1rem; + border-radius: 0.75rem; + border: transparent; + background-color: hsl(200, 100%, 65%); + color: white; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); + + display: flex; + justify-content: center; + align-items: center; + + animation: fadein 1s, fadeout 1s 3s; +} + +@keyframes fadein { + from { + top: 0; + opacity: 0; + } + to { + top: 2%; + opacity: 1; + } +} + +@keyframes fadeout { + from { + top: 2%; + opacity: 1; + } + to { + top: 0; + opacity: 0; + } +} diff --git a/yarn.lock b/yarn.lock index 861fba5..fc0aedd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -394,6 +394,11 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.npmmirror.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"