diff --git a/app.json b/app.json
index 30aceda..c4e698a 100644
--- a/app.json
+++ b/app.json
@@ -3,7 +3,7 @@
"name": "suuudokuuu",
"slug": "suuudokuuu",
"scheme": "myapp",
- "version": "1.3.5",
+ "version": "1.4.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
diff --git a/ios/suuudokuuu/Info.plist b/ios/suuudokuuu/Info.plist
index 745a7e6..de3ee74 100644
--- a/ios/suuudokuuu/Info.plist
+++ b/ios/suuudokuuu/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.3.5
+ 1.4.0
CFBundleSignature
????
CFBundleURLTypes
diff --git a/ios/suuudokuuu/Supporting/Expo.plist b/ios/suuudokuuu/Supporting/Expo.plist
index 7eac422..91501c4 100644
--- a/ios/suuudokuuu/Supporting/Expo.plist
+++ b/ios/suuudokuuu/Supporting/Expo.plist
@@ -9,7 +9,7 @@
EXUpdatesLaunchWaitMs
0
EXUpdatesRuntimeVersion
- 1.3.5
+ 1.4.0
EXUpdatesURL
https://u.expo.dev/4a70028a-5f9e-4ab6-9389-82d8b8b6c833
diff --git a/readme.md b/readme.md
index 497bda3..fd7238b 100644
--- a/readme.md
+++ b/readme.md
@@ -22,7 +22,7 @@ Sudoku game to help Ukraine win the war against Russia.
- [ ] add number flying to its stop?
- [ ] add more fun to winner page(ZSU, Ukraine, donation CTA)
- [ ] add more fun to looser page(ZSU, Ukraine, donation CTA)
- - [ ] animation when finishing full row/col/group(score multiplies)
+ - [x] animation when finishing full row/col/group(score multiplies)
- [ ] add successful run count and longest run count history on main screen?
- [ ] add donation CTA on main screen
- [x] add game logic:
diff --git a/src/@generic/constants/animation.constant.ts b/src/@generic/constants/animation.constant.ts
new file mode 100644
index 0000000..8ee089e
--- /dev/null
+++ b/src/@generic/constants/animation.constant.ts
@@ -0,0 +1 @@
+export const animationDurationConstant = 150;
diff --git a/src/@generic/styles/media-queries.ts b/src/@generic/styles/media-queries.ts
index 9985385..5e6f78c 100644
--- a/src/@generic/styles/media-queries.ts
+++ b/src/@generic/styles/media-queries.ts
@@ -2,4 +2,4 @@ import { Dimensions } from 'react-native';
export const isMobileScreen = () => Dimensions.get('screen').width < 600;
export const isTabletScreen = () => Dimensions.get('screen').width > 600;
-export const isDesktopScreen = () => Dimensions.get('screen').width > 1000;
+export const isDesktopScreen = () => Dimensions.get('screen').width > 1200;
diff --git a/src/game/components/available-values-item/available-values-item.styles.ts b/src/game/components/available-values-item/available-values-item.styles.ts
index 62c7818..e904037 100644
--- a/src/game/components/available-values-item/available-values-item.styles.ts
+++ b/src/game/components/available-values-item/available-values-item.styles.ts
@@ -1,6 +1,6 @@
import { StyleSheet } from 'react-native';
-import { Colors } from '../../../@generic/styles/theme';
+import { Colors } from '../../../@generic';
import { CellFontSizeConstant, CellSizeConstant } from '../../constants/dimensions.contant';
const progressHeight = 2;
diff --git a/src/game/components/field-cell/cell.styles.ts b/src/game/components/field-cell/cell.styles.ts
index 8751936..7346172 100644
--- a/src/game/components/field-cell/cell.styles.ts
+++ b/src/game/components/field-cell/cell.styles.ts
@@ -1,6 +1,6 @@
import { StyleSheet } from 'react-native';
-import { Colors } from '../../../@generic/styles/theme';
+import { Colors } from '../../../@generic';
import { CellSizeConstant, CellFontSizeConstant } from '../../constants/dimensions.contant';
// TODO: Add style theming support
diff --git a/src/game/components/field-cell/cell.tsx b/src/game/components/field-cell/cell.tsx
index 12b6c1a..e4e379f 100644
--- a/src/game/components/field-cell/cell.tsx
+++ b/src/game/components/field-cell/cell.tsx
@@ -1,10 +1,13 @@
import { cs, type OnEventFn } from '@rnw-community/shared';
-import { memo } from 'react';
-import { Pressable, Text } from 'react-native';
-import Reanimated, { interpolateColor, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
+import { memo, useEffect } from 'react';
+import { Pressable } from 'react-native';
+import Animated, { useSharedValue } from 'react-native-reanimated';
+import Reanimated, { interpolate, interpolateColor, useAnimatedStyle, useDerivedValue, withTiming } from 'react-native-reanimated';
import { Colors } from '../../../@generic';
+import { animationDurationConstant } from '../../../@generic/constants/animation.constant';
import { BlankCellValueConstant } from '../../constants/blank-cell-value.constant';
+import { CellFontSizeConstant } from '../../constants/dimensions.contant';
import { FieldGroupWidthConstant, FieldSizeConstant } from '../../constants/field.constant';
import { type CellInterface } from '../../interfaces/cell.interface';
@@ -19,6 +22,7 @@ const isGroupEnd = (index: number): boolean => {
interface Props {
cell: CellInterface;
onSelect: OnEventFn;
+ isScored: boolean;
isActive: boolean;
isActiveValue: boolean;
isCellHighlighted: boolean;
@@ -34,17 +38,43 @@ const getCellBgColor = (isActiveValue: boolean, isCellHighlighted: boolean) => {
return Colors.white;
};
-const CellComponent = ({ cell, onSelect, isActive, isActiveValue, isCellHighlighted }: Props) => {
+// TODO: We need animations logic improvements
+const CellComponent = ({ cell, onSelect, isActive, isActiveValue, isCellHighlighted, isScored = true }: Props) => {
const value = cell.value === BlankCellValueConstant ? '' : cell.value.toString();
const isLastRow = cell.y === 8;
const isLastCol = cell.x === 8;
const backgroundColor = getCellBgColor(isActiveValue, isCellHighlighted);
- const progress = useSharedValue(0);
- const animatedStyles = useAnimatedStyle(() => ({
- backgroundColor: interpolateColor(progress.value, [0, 1], [backgroundColor, Colors.cell.active])
- }));
- progress.value = withTiming(isActive ? 1 : 0, { duration: 200 });
+ const activeAnimation = useDerivedValue(() => {
+ return withTiming(isActive ? 1 : 0, { duration: animationDurationConstant });
+ });
+ const scoreAnimation = useSharedValue(0);
+
+ useEffect(() => {
+ if (isScored) {
+ scoreAnimation.value = 0;
+ scoreAnimation.value = withTiming(1, { duration: 8 * animationDurationConstant });
+ }
+ }, [isScored]);
+
+ const cellAnimatedStyles = useAnimatedStyle(
+ () => ({
+ backgroundColor: interpolateColor(activeAnimation.value, [0, 1], [backgroundColor, Colors.cell.active])
+ }),
+ [backgroundColor]
+ );
+ const textAnimatedStyles = useAnimatedStyle(
+ () => ({
+ color: interpolateColor(scoreAnimation.value, [0, 0.5, 1], [Colors.black, Colors.cell.highlightedText, Colors.black]),
+ fontSize: interpolate(
+ scoreAnimation.value,
+ [0, 0.5, 1],
+ [CellFontSizeConstant, CellFontSizeConstant * 2, CellFontSizeConstant]
+ ),
+ transform: [{ rotate: `${interpolate(scoreAnimation.value, [0, 1], [0, 360])}deg` }]
+ }),
+ [backgroundColor]
+ );
const handlePress = () => void onSelect(isActive ? undefined : cell);
@@ -54,13 +84,18 @@ const CellComponent = ({ cell, onSelect, isActive, isActiveValue, isCellHighligh
cs(isGroupEnd(cell.y), styles.cellGroupYEnd),
cs(isLastRow, styles.cellLastRow),
cs(isLastCol, styles.cellLastCol),
- animatedStyles
+ cellAnimatedStyles
+ ];
+ const textStyles = [
+ styles.cellText,
+ cs(isActiveValue, styles.cellActiveValueText),
+ cs(isActive, styles.cellActiveText),
+ textAnimatedStyles
];
- const textStyles = [styles.cellText, cs(isActiveValue, styles.cellActiveValueText), cs(isActive, styles.cellActiveText)];
return (
- {value}
+ {value}
);
};
diff --git a/src/game/components/field/field.styles.ts b/src/game/components/field/field.styles.ts
index 33f4741..73ec632 100644
--- a/src/game/components/field/field.styles.ts
+++ b/src/game/components/field/field.styles.ts
@@ -1,6 +1,6 @@
import { Platform, StyleSheet } from 'react-native';
-import { isMobileScreen } from '../../../@generic';
+import { isMobileScreen, isTabletScreen } from '../../../@generic';
export const FieldStyles = StyleSheet.create({
row: {
@@ -10,7 +10,7 @@ export const FieldStyles = StyleSheet.create({
alignItems: 'center',
flex: 5,
flexDirection: 'column',
- justifyContent: isMobileScreen() ? 'center' : 'flex-end',
+ justifyContent: isMobileScreen() || isTabletScreen() ? 'center' : 'flex-end',
margin: 'auto',
...(Platform.OS === 'web' && {
flex: 7
diff --git a/src/game/components/field/field.tsx b/src/game/components/field/field.tsx
index e29bf5b..d787e3a 100644
--- a/src/game/components/field/field.tsx
+++ b/src/game/components/field/field.tsx
@@ -17,19 +17,24 @@ const isSameCell = (cell: CellInterface, selectedCell?: CellInterface): boolean
const isSameCellValue = (cell: CellInterface, selectedCell?: CellInterface): boolean =>
isDefined(selectedCell) && cell.value === selectedCell?.value && cell.value !== BlankCellValueConstant;
+const isScoredCell = (cell: CellInterface, scoredCells?: CellInterface): boolean =>
+ isDefined(scoredCells) && (scoredCells.x === cell.x || scoredCells.y === cell.y || scoredCells.group === cell.group);
+
interface Props {
+ scoredCells?: CellInterface;
field: FieldInterface;
selectedCell?: CellInterface;
onSelect: OnEventFn;
}
-export const Field = ({ field, selectedCell, onSelect }: Props) => {
+export const Field = ({ field, selectedCell, onSelect, scoredCells }: Props) => {
return (
{field.map(row => (
{row.map(cell => (
{
const mistakes = useAppSelector(gameMistakesSelector);
const currentScore = useAppSelector(gameScoreSelector);
const startedAt = useAppSelector(gameStartedAtSelector);
+ const scoredCells = useAppSelector(gameScoredCellsSelector);
const handleWin = async () => {
if (!hasBlankCells(field)[0]) {
- await hapticImpact(ImpactFeedbackStyle.Heavy);
- router.push('winner');
+ // HINT: We need to wait for the animation to finish
+ setTimeout(() => void router.push('winner'), 10 * animationDurationConstant);
}
};
const handleLose = async () => {
if (mistakes >= MaxMistakesConstant) {
- await hapticImpact(ImpactFeedbackStyle.Heavy);
router.push('loser');
}
};
@@ -80,7 +80,7 @@ export const GameScreen = () => {
|
-
+
diff --git a/src/game/interfaces/game.interface.ts b/src/game/interfaces/game.interface.ts
index 17f4dbe..b9dbc6a 100644
--- a/src/game/interfaces/game.interface.ts
+++ b/src/game/interfaces/game.interface.ts
@@ -1,5 +1,4 @@
-import { InitialDateConstant } from '../../@generic/constants/date.constant';
-import { DifficultyEnum } from '../../@generic/enums/difficulty.enum';
+import { InitialDateConstant, DifficultyEnum } from '../../@generic';
import { type CellInterface } from './cell.interface';
import { type FieldInterface } from './field.interface';
@@ -18,7 +17,7 @@ export interface GameInterface {
filledField: FieldInterface;
gameField: FieldInterface;
selectedCell?: CellInterface;
- selectedValue?: number;
+ scoredCells?: CellInterface;
}
export const emptyGame: GameInterface = {
@@ -31,5 +30,6 @@ export const emptyGame: GameInterface = {
filledField: [],
gameField: [],
mistakes: 0,
- score: 0
+ score: 0,
+ scoredCells: undefined
};
diff --git a/src/game/store/actions/game-select-value.action.ts b/src/game/store/actions/game-select-value.action.ts
index 7a29027..5fa40da 100644
--- a/src/game/store/actions/game-select-value.action.ts
+++ b/src/game/store/actions/game-select-value.action.ts
@@ -1,14 +1,26 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { isDefined } from '@rnw-community/shared';
import * as Haptics from 'expo-haptics';
+import { ImpactFeedbackStyle } from 'expo-haptics';
import { type AppDispatch, type RootState } from '../../../@app-root';
-import { hapticNotification } from '../../../@generic';
-import { historyRecordAction } from '../../../history/store/history.actions';
+import { hapticImpact, hapticNotification } from '../../../@generic';
+import { historyRecordAction } from '../../../history';
import { BlankCellValueConstant } from '../../constants/blank-cell-value.constant';
+import { MaxMistakesConstant } from '../../constants/max-mistakes.constant';
import { calculateScore } from '../../utils/calculate-score.util';
+import { createCell } from '../../utils/create-cell.util';
import { hasBlankCells } from '../../utils/field/has-blank-cells.util';
-import { gameFinishAction, gameIncreaseScoreAction, gameMadeAMistakeAction, gameSetValueAction } from '../game.actions';
+import { hasValueInColumn } from '../../utils/field/has-value-in-column.util';
+import { hasValueInGroup } from '../../utils/field/has-value-in-group.util';
+import { hasValueInRow } from '../../utils/field/has-value-in-row.util';
+import {
+ gameFinishAction,
+ gameIncreaseScoreAction,
+ gameMadeAMistakeAction,
+ gameSetScoredCellsAction,
+ gameSetValueAction
+} from '../game.actions';
export const gameSelectValueAction = createAsyncThunk(
'game/selectValue',
@@ -27,10 +39,27 @@ export const gameSelectValueAction = createAsyncThunk= MaxMistakesConstant) {
+ await hapticImpact(ImpactFeedbackStyle.Heavy);
+ }
}
}
diff --git a/src/game/store/game.actions.ts b/src/game/store/game.actions.ts
index b00a6a1..5affbc3 100644
--- a/src/game/store/game.actions.ts
+++ b/src/game/store/game.actions.ts
@@ -7,5 +7,6 @@ export const {
madeAMistake: gameMadeAMistakeAction,
finish: gameFinishAction,
reset: gameResetAction,
- increaseScore: gameIncreaseScoreAction
+ increaseScore: gameIncreaseScoreAction,
+ setScoredCells: gameSetScoredCellsAction
} = gameSlice.actions;
diff --git a/src/game/store/game.selectors.ts b/src/game/store/game.selectors.ts
index 7fb2562..313504e 100644
--- a/src/game/store/game.selectors.ts
+++ b/src/game/store/game.selectors.ts
@@ -11,3 +11,4 @@ export const gameMistakesSelector = createSelector(gameSelector, state => state.
export const gameStartedAtSelector = createSelector(gameSelector, state => new Date(state.startedAt));
export const gameEndedAtSelector = createSelector(gameSelector, state => new Date(state.endedAt));
export const gameScoreSelector = createSelector(gameSelector, state => state.score);
+export const gameScoredCellsSelector = createSelector(gameSelector, state => state.scoredCells);
diff --git a/src/game/store/game.slice.ts b/src/game/store/game.slice.ts
index cd7d716..81af0bd 100644
--- a/src/game/store/game.slice.ts
+++ b/src/game/store/game.slice.ts
@@ -40,6 +40,9 @@ export const gameSlice = createSlice({
},
madeAMistake: state => {
state.mistakes++;
+ },
+ setScoredCells: (state, action: PayloadAction) => {
+ state.scoredCells = action.payload;
}
}
});
diff --git a/src/game/utils/field/has-value-in-column.util.ts b/src/game/utils/field/has-value-in-column.util.ts
index 65c9b1e..f72e119 100644
--- a/src/game/utils/field/has-value-in-column.util.ts
+++ b/src/game/utils/field/has-value-in-column.util.ts
@@ -2,8 +2,8 @@ import { type CellInterface } from '../../interfaces/cell.interface';
import { type FieldInterface } from '../../interfaces/field.interface';
export const hasValueInColumn = (cell: CellInterface, field: FieldInterface): boolean => {
- for (let row = 0; row < field.length; row++) {
- if (field[row][cell.x].value === cell.value) {
+ for (let y = 0; y < field.length; y++) {
+ if (field[y][cell.x].value === cell.value) {
return true;
}
}