Skip to content

Commit 00b167d

Browse files
committed
feat(game): added coins, level rewards and paid hints
1 parent f8fde6b commit 00b167d

File tree

14 files changed

+159
-60
lines changed

14 files changed

+159
-60
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './use-coins';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { create } from 'zustand';
2+
import { persist } from 'zustand/middleware';
3+
4+
export const useCoins = create(
5+
persist<{
6+
coins: number;
7+
addCoins: (coins: number) => void;
8+
spendCoins: (coins: number) => void;
9+
lastReward: number;
10+
reward: (reward: number) => void;
11+
clearLastReward: () => void;
12+
}>(
13+
(set, get) => ({
14+
addCoins: (coins) => set((state) => ({ coins: state.coins + coins })),
15+
clearLastReward: () => set({ lastReward: 0 }),
16+
coins: 0,
17+
lastReward: 0,
18+
reward: (reward) => set({ coins: get().coins + reward, lastReward: reward }),
19+
spendCoins: (coins) => set((state) => ({ coins: state.coins - coins })),
20+
}),
21+
{ name: 'coins' },
22+
),
23+
);

src/features/game/hooks/use-game/use-game.ts

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { useDebouncedCallback } from 'use-debounce';
33

44
import { VECTOR_ZERO } from '../../../engine/constants';
55
import { Entity, isDice, isMovable } from '../../../engine/types/entities';
6-
import { getIsSameVector } from '../../../engine/utils/get-is-same-vector';
6+
import { getReward } from '../../utils';
77
import { useBoard } from '../use-board';
8+
import { useCoins } from '../use-coins';
89
import { useGameState } from '../use-game-state';
910
import { useHighscores } from '../use-highscores';
1011
import { useGameAnimation } from './use-game-animation';
@@ -19,10 +20,10 @@ const getSnapshot = (entities: Entity[]) =>
1920
JSON.stringify(getSortedMovables(entities));
2021

2122
export const useGame = ({ disabled }: { disabled?: boolean }) => {
22-
const { countMove, level, moves, setScreen } = useGameState();
23-
const { save } = useHighscores();
23+
const { countMove, level, maxMoves, moves, setScreen } = useGameState();
24+
const { highscores, save } = useHighscores();
25+
const { reward } = useCoins();
2426

25-
// const [entities, setEntities] = useState<Entity[]>([...boardEntities]);
2627
const { entities, setEntities } = useBoard();
2728
const [isLocked, setIsLocked] = useState(false);
2829
const isMounted = useRef(false);
@@ -33,6 +34,22 @@ export const useGame = ({ disabled }: { disabled?: boolean }) => {
3334
setEntities,
3435
});
3536

37+
const highscore = highscores.find(({ levelId }) => levelId === level);
38+
39+
const showWinScreen = useDebouncedCallback(() => {
40+
setScreen('won');
41+
42+
const currentReward = getReward({ maxMoves, moves });
43+
const previousReward = highscore?.moves
44+
? getReward({ maxMoves, moves: highscore.moves })
45+
: 0;
46+
47+
const amount = Math.max(currentReward - previousReward, 0);
48+
reward(amount);
49+
50+
save({ levelId: level, moves });
51+
}, 500);
52+
3653
useEffect(() => {
3754
isMounted.current = true;
3855

@@ -64,8 +81,6 @@ export const useGame = ({ disabled }: { disabled?: boolean }) => {
6481
void (async () => {
6582
await animate();
6683

67-
setIsLocked(false);
68-
6984
setEntities((current) => {
7085
const result = current.map((entity) =>
7186
isMovable(entity)
@@ -81,54 +96,34 @@ export const useGame = ({ disabled }: { disabled?: boolean }) => {
8196
if (snapshot.current !== getSnapshot(result) && snapshot.current !== '') {
8297
snapshot.current = '';
8398
countMove();
99+
100+
if (result.length > 0) {
101+
const dices = result.filter(isDice);
102+
103+
const allOnTarget = dices.every((dice) => dice.isOnTarget);
104+
105+
if (allOnTarget) {
106+
showWinScreen();
107+
} else {
108+
setIsLocked(false);
109+
}
110+
}
84111
}
85112

86113
return result;
87114
});
88115
})();
89116
}
90-
}, [animate, countMove, disabled, isAnimating, isLocked, setEntities]);
91-
92-
const showWinScreen = useDebouncedCallback(() => {
93-
setScreen('won');
94-
save({ levelId: level, moves: moves + 1 });
95-
}, 500);
96-
97-
const [isComplete, setIsComplete] = useState(false);
98-
99-
useEffect(() => {
100-
if (isComplete) {
101-
return;
102-
}
103-
104-
if (disabled) {
105-
return;
106-
}
107-
108-
if (entities.length === 0) {
109-
return;
110-
}
111-
112-
const dices = entities.filter(isDice);
113-
const isAnyMoving = dices.some(
114-
(dice) => !getIsSameVector(dice.velocity, VECTOR_ZERO),
115-
);
116-
117-
if (isAnyMoving) {
118-
return;
119-
}
120-
121-
const allOnTarget = dices.every((dice) => dice.isOnTarget);
122-
123-
if (allOnTarget) {
124-
setIsComplete(true);
125-
showWinScreen();
126-
}
127-
}, [disabled, entities, isComplete, showWinScreen]);
128-
129-
useEffect(() => {
130-
setIsComplete(false);
131-
}, [entities]);
117+
}, [
118+
animate,
119+
countMove,
120+
disabled,
121+
entities,
122+
isAnimating,
123+
isLocked,
124+
setEntities,
125+
showWinScreen,
126+
]);
132127

133-
return { entities, isLocked };
128+
return { isLocked };
134129
};

src/features/game/utils/get-reward.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const getReward = ({
2+
maxMoves,
3+
moves,
4+
}: {
5+
moves: number;
6+
maxMoves: number;
7+
}) => {
8+
if (moves >= maxMoves) {
9+
return 0;
10+
}
11+
12+
return (maxMoves - moves) * 50;
13+
};

src/features/game/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './get-reward';

src/features/ui/components/board-view/board-view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const BoardView = ({ board }: { board: Board }) => {
5353
position: 'absolute',
5454
top: '50%',
5555
transform: `scale(${scale}) translate(${(-(width - 1) * TILE_SIZE) / 2}px, ${
56-
(-(height - 1) * TILE_SIZE) / 2
56+
(-(height - 0.25) * TILE_SIZE) / 2
5757
}px)`,
5858
}}
5959
>

src/features/ui/components/button/button.module.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
gap: 0.5rem;
1616
}
1717

18+
.button svg {
19+
color: var(--color-yellow);
20+
}
21+
1822
.button:active:not(:disabled) {
1923
background-color: #323232;
2024
text-shadow: 0 0 10px white;

src/features/ui/components/game-view/game-view.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
22

33
import { getIdEntities } from '../../../editor/utils/get-id-entities';
44
import { useBoard } from '../../../game/hooks/use-board';
5+
import { useCoins } from '../../../game/hooks/use-coins';
56
import { useControls } from '../../../game/hooks/use-controls';
67
import { useGame } from '../../../game/hooks/use-game';
78
import { useGameState } from '../../../game/hooks/use-game-state';
@@ -13,19 +14,27 @@ import { SwipeArea } from '../swipe-area';
1314
export const GameView = () => {
1415
const { level } = useGameState();
1516
const { entities, setEntities } = useBoard();
17+
const { clearLastReward } = useCoins();
1618

1719
const { isFullyVisible } = useScreen();
20+
1821
const disabled = !isFullyVisible;
1922

20-
useGame({
23+
const { isLocked } = useGame({
2124
disabled,
2225
});
2326

24-
const { swipeProps } = useControls({ disabled, setEntities });
27+
const controlsDisabled = isLocked || disabled;
28+
29+
const { swipeProps } = useControls({
30+
disabled: controlsDisabled,
31+
setEntities,
32+
});
2533

2634
useEffect(() => {
2735
setEntities(getIdEntities(level));
28-
}, [level, setEntities]);
36+
clearLastReward();
37+
}, [clearLastReward, level, setEntities]);
2938

3039
return (
3140
<>

src/features/ui/components/hint/hint.module.css

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@
44
left: 0;
55
right: 0;
66
display: flex;
7+
flex-direction: row;
78
justify-content: center;
89
align-items: center;
9-
height: 96px;
10-
background-color: red;
10+
gap: 1rem;
11+
padding: 1rem;
12+
}
13+
14+
.info {
15+
display: flex;
16+
align-items: center;
17+
color: white;
18+
gap: 0.5rem;
19+
}
20+
21+
.coin {
22+
color: var(--color-yellow);
1123
}

src/features/ui/components/hint/hint.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { isMovable, Vector } from '../../../engine/types/entities';
22
import { getResolution } from '../../../engine/utils/get-resolution';
33
import { useBoard } from '../../../game/hooks/use-board';
4+
import { useCoins } from '../../../game/hooks/use-coins';
45
import { getArrowSymbol } from '../../utils/get-arrow-symbol';
56
import { Button } from '../button';
67
import { Icon } from '../icon';
78
import $$ from './hint.module.css';
89

10+
const COST = 100;
11+
912
export const Hint = () => {
1013
const { entities, setEntities } = useBoard();
1114

15+
const { coins, spendCoins } = useCoins();
16+
1217
const resolve = async () => {
1318
let resolution = await getResolution({ entities });
1419
let velocity: Vector | undefined;
@@ -25,6 +30,7 @@ export const Hint = () => {
2530
}
2631

2732
if (velocity) {
33+
spendCoins(COST);
2834
setEntities((current) =>
2935
current.map((entity) => {
3036
if (isMovable(entity)) {
@@ -48,11 +54,11 @@ export const Hint = () => {
4854
return (
4955
<div className={$$.hint}>
5056
<Button
57+
disabled={coins < COST}
5158
type="button"
5259
onClick={handleClick}
5360
>
54-
<Icon name="hint" />
55-
Hint
61+
<Icon name="hint" /> ${COST}
5662
</Button>
5763
</div>
5864
);

src/features/ui/components/icon/icon.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ const ICONS = {
2222
arrowTop: (
2323
<path d="M11 8.414V18h2V8.414l4.293 4.293 1.414-1.414L12 4.586l-6.707 6.707 1.414 1.414z" />
2424
),
25+
coin: (
26+
<path
27+
d="M17.813 3.838C17.6282 3.57906 17.3843 3.36796 17.1015 3.22221C16.8187 3.07647 16.5052 3.00029 16.187 3H7.81304C7.16904 3 6.56104 3.313 6.14604 3.899L2.14604 10.48C2.03427 10.6636 1.9847 10.8784 2.0047 11.0924C2.0247 11.3064 2.11319 11.5083 2.25704 11.668L11.257 21.668C11.3503 21.7729 11.4647 21.8568 11.5927 21.9143C11.7207 21.9718 11.8595 22.0016 11.9998 22.0017C12.1402 22.0018 12.2789 21.9722 12.407 21.9149C12.5351 21.8575 12.6496 21.7737 12.743 21.669L21.743 11.669C21.8872 11.5094 21.9759 11.3075 21.9959 11.0934C22.0159 10.8793 21.9661 10.6645 21.854 10.481L17.813 3.838ZM12 19.505L5.24504 12H18.754L12 19.505ZM4.77704 10L7.81304 5L16.145 4.938L19.222 10H4.77704Z"
28+
fill="currentColor"
29+
/>
30+
),
2531
complete: (
2632
<>
2733
<path

src/features/ui/components/navigation/navigation.module.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@
3232
height: 100%;
3333
}
3434

35+
.row {
36+
display: flex;
37+
flex-direction: row;
38+
justify-content: center;
39+
align-items: center;
40+
gap: 3rem;
41+
}
42+
43+
.column {
44+
display: flex;
45+
flex-direction: column;
46+
justify-content: center;
47+
align-items: center;
48+
}
49+
3550
.title {
3651
font-size: 1rem;
3752
color: var(--color-white);

src/features/ui/components/navigation/navigation.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useHotkeys } from 'react-hotkeys-hook';
22

33
import { useEpisodes } from '../../../editor/hooks/use-episodes';
4+
import { useCoins } from '../../../game/hooks/use-coins';
45
import { useGameState } from '../../../game/hooks/use-game-state';
56
import { Button } from '../button';
67
import { Icon } from '../icon';
@@ -55,6 +56,7 @@ const Levels = () => {
5556

5657
const Game = () => {
5758
const { maxMoves, moves, restart, screen } = useGameState();
59+
const { coins } = useCoins();
5860

5961
const isVisible = screen === 'game';
6062

@@ -66,8 +68,16 @@ const Game = () => {
6668
<div className={$$.leftAction}>
6769
<BackButton label="Levels" />
6870
</div>
69-
<div className={$$.subtitle}>Moves</div>
70-
<div className={$$.title}>{maxMoves - moves}</div>
71+
<div className={$$.row}>
72+
<div className={$$.column}>
73+
<div className={$$.subtitle}>Moves</div>
74+
<div className={$$.title}>{maxMoves - moves}</div>
75+
</div>
76+
<div className={$$.column}>
77+
<div className={$$.subtitle}>Coins</div>
78+
<div className={$$.title}>${coins}</div>
79+
</div>
80+
</div>
7181
<div className={$$.rightAction}>
7282
<Button onClick={restart}>
7383
<Icon name="restart" />

0 commit comments

Comments
 (0)