├── .expo
├── README.md
└── devices.json
├── .gitignore
├── .watchmanconfig
├── App.js
├── README.md
├── Soccer.js
├── app.json
├── components
├── Emoji.js
└── Score.js
├── eas.json
├── images.js
├── images
├── baseball.png
├── basketball.png
├── football.png
├── golf.png
├── hockey.png
├── soccer.png
├── soccerGame.gif
├── sportballs.png
└── tennis.png
├── index.android.js
├── package.json
├── tsconfig.json
├── useCachedResources.ts
└── yarn.lock
/.expo/README.md:
--------------------------------------------------------------------------------
1 | > Why do I have a folder named ".expo" in my project?
2 | The ".expo" folder is created when an Expo project is started using "expo start" command.
3 | > What do the files contain?
4 | - "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
5 | - "settings.json": contains the server configuration that is used to serve the application manifest.
6 | > Should I commit the ".expo" folder?
7 | No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
8 | Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
9 |
--------------------------------------------------------------------------------
/.expo/devices.json:
--------------------------------------------------------------------------------
1 | {
2 | "devices": []
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 |
25 | # Android/IJ
26 | #
27 | *.iml
28 | .idea
29 | .gradle
30 | local.properties
31 |
32 | # node.js
33 | #
34 | node_modules/
35 | npm-debug.log
36 |
37 | # BUCK
38 | buck-out/
39 | \.buckd/
40 | android/app/libs
41 | android/keystores/debug.keystore
42 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 |
2 | import useCachedResources from './useCachedResources';
3 | import React from 'react';
4 | import Soccer from './Soccer';
5 |
6 |
7 | export default function App() {
8 | const isLoadingComplete = useCachedResources();
9 |
10 | if (!isLoadingComplete) {
11 | return null
12 | } else {
13 | return (
14 |
15 | );
16 | }
17 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-soccer
2 | React Native clone of the Facebook Messenger soccer game
3 |
4 | 
5 |
6 | ## Installation
7 | * `git clone https://github.com/nitishp/react-native-soccer`
8 | * `cd react-native-soccer`
9 | * `react-native run-android`
10 |
11 | This is my first real React Native project. Feel free to give me any feedback!
12 |
--------------------------------------------------------------------------------
/Soccer.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {View,Text, StyleSheet,Dimensions,Image,Modal,TouchableOpacity,TouchableWithoutFeedback} from 'react-native';
3 | import * as Haptics from 'expo-haptics';
4 | import Score from './components/Score';
5 | import Emoji from './components/Emoji';
6 | import * as All from './images';
7 |
8 | const LC_IDLE = 0;
9 | const LC_RUNNING = 1;
10 | const LC_TAPPED = 2;
11 | const GRAVITY = 0.6;
12 | const TAPPED_VELOCITY = 20;
13 | const ROTATION_FACTOR = 7;
14 | const SCREEN_HEIGHT = Dimensions.get('window').height;
15 | const SCREEN_WIDTH = Dimensions.get('window').width;
16 | const BALL_WIDTH = SCREEN_WIDTH * 0.33;
17 | const BALL_HEIGHT = SCREEN_WIDTH * 0.33;
18 | const FLOOR_Y = SCREEN_HEIGHT - BALL_HEIGHT;
19 | const FLOOR_X = SCREEN_WIDTH / 2;
20 | const SCORE_Y = SCREEN_HEIGHT / 6;
21 | const EMOJI_Y = SCREEN_HEIGHT / 3;
22 | const sports = ['soccer', 'baseball', 'basketball', 'football', 'golf', 'tennis', 'hockey']
23 |
24 | function Sports({sport, set}) {
25 | return (
26 | set(sport)}>
27 |
28 |
29 | )
30 | }
31 |
32 | class Soccer extends Component {
33 | constructor(props) {
34 | super(props);
35 | this.interval = null;
36 | this.state = {
37 | x: FLOOR_X,
38 | y: FLOOR_Y,
39 | vx: 0,
40 | vy: 0,
41 | lifeCycle: LC_IDLE,
42 | score: 0,
43 | scored: false,
44 | lost: false,
45 | rotate: 0,
46 | sport: All.soccer,
47 | visible: true,
48 | button: true
49 | };
50 | }
51 |
52 | componentDidMount() {
53 | this.interval = setInterval(this.update.bind(this), 1000 / 60);
54 | }
55 |
56 | componentWillUnmount() {
57 | if(this.interval) {
58 | clearInterval(this.interval);
59 | }
60 | }
61 |
62 | onTap(event) {
63 | Haptics.selectionAsync()
64 | this.setState({button: false})
65 | if(this.state.lifeCycle === LC_TAPPED) {
66 | this.setState({
67 | lifeCycle: LC_RUNNING,
68 | scored: false,
69 | });
70 | }
71 | else {
72 | let centerX = BALL_WIDTH / 2;
73 | let centerY = BALL_HEIGHT / 2;
74 | let velocityX = ((centerX - event.locationX) / SCREEN_WIDTH) * TAPPED_VELOCITY;
75 | let velocityY = -TAPPED_VELOCITY;
76 | this.setState({
77 | vx: velocityX,
78 | vy: velocityY,
79 | score: this.state.score + 1,
80 | lifeCycle: LC_TAPPED,
81 | scored: true,
82 | lost: false,
83 | });
84 | }
85 | return false;
86 | }
87 |
88 | updatePosition(nextState) {
89 | nextState.x += nextState.vx;
90 | nextState.y += nextState.vy;
91 | nextState.rotate += ROTATION_FACTOR * nextState.vx;
92 | // Hit the left wall
93 | if(nextState.x < BALL_WIDTH / 2) {
94 | Haptics.selectionAsync()
95 | nextState.vx = -nextState.vx;
96 | nextState.x = BALL_WIDTH / 2;
97 | }
98 | // Hit the right wall
99 | if(nextState.x > SCREEN_WIDTH - BALL_WIDTH / 2) {
100 | Haptics.selectionAsync()
101 | nextState.vx = -nextState.vx;
102 | nextState.x = SCREEN_WIDTH - BALL_WIDTH / 2;
103 | }
104 | // Reset after falling down
105 | if(nextState.y > SCREEN_HEIGHT + BALL_HEIGHT) {
106 | Haptics.selectionAsync()
107 | nextState.y = FLOOR_Y;
108 | nextState.x = FLOOR_X;
109 | nextState.lifeCycle = LC_IDLE;
110 | nextState.score = 0;
111 | nextState.lost = true;
112 | nextState.scored = false;
113 | }
114 | }
115 |
116 | updateVelocity(nextState) {
117 | nextState.vy += GRAVITY;
118 | }
119 |
120 | update() {
121 | if(this.state.lifeCycle === LC_IDLE) {
122 | this.setState({button: true})
123 | return;
124 | }
125 | let nextState = Object.assign({}, this.state);
126 | this.updatePosition(nextState);
127 | this.updateVelocity(nextState);
128 | this.setState(nextState);
129 | }
130 |
131 | setSport(sport){
132 | Haptics.selectionAsync()
133 | this.setState({visible: false, sport: All[`${sport}`]})
134 | }
135 |
136 | render() {
137 | var position = {
138 | left: this.state.x - (BALL_WIDTH / 2),
139 | top: this.state.y - (BALL_HEIGHT / 2),
140 | }
141 | var rotation = {
142 | transform: [
143 | {rotate: this.state.rotate + 'deg'},
144 | ],
145 | }
146 | return (
147 |
148 |
149 |
150 | this.onTap(event.nativeEvent)}
152 | onPressIn={(event) => this.onTap(event.nativeEvent)}
153 | onPressOut={(event) => this.onTap(event.nativeEvent)}>
154 |
155 |
156 |
157 |
158 |
159 | Choose your sport
160 |
161 |
162 | {sports.map(sport => {return this.setSport(sport)} sport={sport} /> })}
163 |
164 |
165 |
166 | {this.state.button == true && {this.setState({visible: true}), Haptics.selectionAsync()}}
167 | style={{padding: 10, height: 55, width: 55, position: 'absolute', bottom: 30, right: 30, borderColor: '#e7e7e6', borderWidth: 0.5, shadowColor: "#555a74",
168 | shadowOffset: { height: 0.5, width: 0.5 },
169 | shadowOpacity: 0.5,
170 | shadowRadius: 0.5,
171 | backgroundColor: "white",
172 | borderRadius: 5,
173 | elevation: 8,
174 | justifyContent: 'center',
175 | alignItems: 'center'}}>}
176 |
177 | );
178 | }
179 | }
180 |
181 | const styles = StyleSheet.create({
182 | ball: {
183 | width: BALL_WIDTH,
184 | height: BALL_HEIGHT,
185 | },
186 | });
187 |
188 | export default Soccer;
189 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "Tap It Up Sports",
4 | "slug": "Tap-It-Up-Sports",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./images/soccer.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./images/soccer.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "updates": {
15 | "fallbackToCacheTimeout": 0
16 | },
17 | "assetBundlePatterns": [
18 | "**/*"
19 | ],
20 | "ios": {
21 | "supportsTablet": true,
22 | "bundleIdentifier": "com.konjo.tapitupsports",
23 | "buildNumber": "6",
24 | "infoPlist": {
25 | "ITSAppUsesNonExemptEncryption": false
26 | }
27 | },
28 | "android": {
29 | "versionCode": 6,
30 | "adaptiveIcon": {
31 | "foregroundImage": "./images/soccer.png",
32 | "backgroundColor": "#ffffff"
33 | },
34 | "package": "com.konjo.tapitupsports"
35 | },
36 | "extra": {
37 | "eas": {
38 | "projectId": "78531fa9-4fcc-44cc-a3ae-5dd1bd5b427d"
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/components/Emoji.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Text,
3 | View,
4 | StyleSheet,
5 | Dimensions,
6 | Animated} from 'react-native';
7 |
8 | class Emoji extends Component {
9 |
10 | constructor(props) {
11 | super(props);
12 | this.scored = ['👍', '👏', '👋', '😎', '💪'];
13 | this.missed = ['😢', '😭', '😔', '😡', '😠'];
14 | this.state = {
15 | opacity: new Animated.Value(0),
16 | };
17 | }
18 |
19 | render() {
20 | let randomIndex = Math.floor(Math.random() * 5);
21 | let emojiChar = "";
22 | if(this.props.lost === true) {
23 | emojiChar = this.missed[randomIndex];
24 | }
25 | else {
26 | emojiChar = this.scored[randomIndex];
27 | }
28 | let windowWidth = Dimensions.get('window').width;
29 | let position = {
30 | width: windowWidth,
31 | top: this.props.y,
32 | opacity: this.state.opacity,
33 | }
34 | return (
35 |
36 |
37 | {emojiChar}
38 |
39 |
40 | );
41 | }
42 |
43 | shouldComponentUpdate(nextProps, nextState) {
44 | return (nextProps.scored !== this.props.scored)
45 | || (nextProps.lost !== this.props.lost);
46 | }
47 |
48 | componentDidUpdate() {
49 | this.state.opacity.setValue(1);
50 | Animated.timing(
51 | this.state.opacity,
52 | {
53 | toValue: 0,
54 | duration: 1000,
55 | useNativeDriver: true
56 | }
57 | ).start();
58 | }
59 | }
60 |
61 | const styles = StyleSheet.create({
62 | container: {
63 | position: 'absolute',
64 | justifyContent: 'flex-start',
65 | alignItems: 'center',
66 | },
67 |
68 | emoji: {
69 | flex: 1,
70 | fontSize: 25,
71 | }
72 | });
73 |
74 | export default Emoji;
75 |
--------------------------------------------------------------------------------
/components/Score.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {View, Text, StyleSheet, Dimensions, Animated} from 'react-native';
3 |
4 | class Score extends Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | this.animationConfig = {
9 | toValue: 1.0,
10 | duration: 200,
11 | useNativeDriver: true
12 | }
13 | this.state = {
14 | bounceValue : new Animated.Value(0),
15 | }
16 | this.animation = Animated.timing(this.state.bounceValue, this.animationConfig);
17 | }
18 |
19 | render() {
20 | let windowWidth = Dimensions.get('window').width;
21 | let containerPosition = {
22 | top: this.props.y,
23 | width: windowWidth,
24 | transform: [
25 | {scale: this.state.bounceValue},
26 | ],
27 | }
28 |
29 | return (
30 |
31 |
32 | {this.props.score}
33 |
34 |
35 | );
36 | }
37 |
38 | componentDidUpdate() {
39 | this.bounce();
40 | }
41 |
42 | shouldComponentUpdate(nextProps, nextState) {
43 | return nextProps.scored === true && nextProps.scored !== this.props.scored;
44 | }
45 |
46 | componentDidMount() {
47 | this.bounce();
48 | }
49 |
50 | bounce() {
51 | this.state.bounceValue.setValue(0.5);
52 | this.animation.start();
53 | }
54 | }
55 |
56 | const styles = StyleSheet.create({
57 | container: {
58 | position: 'absolute',
59 | justifyContent: 'flex-start',
60 | alignItems: 'center',
61 | },
62 |
63 | score: {
64 | fontSize: 100,
65 | fontWeight: '100',
66 | flex: 1,
67 | }
68 | });
69 |
70 | export default Score;
71 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 3.1.1"
4 | },
5 | "build": {
6 | "development": {
7 | "distribution": "internal",
8 | "android": {
9 | "gradleCommand": ":app:assembleDebug"
10 | },
11 | "ios": {
12 | "resourceClass": "m1-medium",
13 | "buildConfiguration": "Debug"
14 | }
15 | },
16 | "preview": {
17 | "distribution": "internal"
18 | },
19 | "production": {
20 | "ios": {
21 | "resourceClass": "m1-medium"
22 | }
23 | }
24 | },
25 | "submit": {
26 | "production": {}
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/images.js:
--------------------------------------------------------------------------------
1 |
2 | export const soccer = require('./images/soccer.png');
3 | export const baseball = require('./images/baseball.png');
4 | export const basketball = require('./images/basketball.png');
5 | export const football = require('./images/football.png');
6 | export const golf = require('./images/golf.png');
7 | export const tennis = require('./images/tennis.png');
8 | export const hockey = require('./images/hockey.png');
9 |
--------------------------------------------------------------------------------
/images/baseball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/baseball.png
--------------------------------------------------------------------------------
/images/basketball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/basketball.png
--------------------------------------------------------------------------------
/images/football.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/football.png
--------------------------------------------------------------------------------
/images/golf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/golf.png
--------------------------------------------------------------------------------
/images/hockey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/hockey.png
--------------------------------------------------------------------------------
/images/soccer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/soccer.png
--------------------------------------------------------------------------------
/images/soccerGame.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/soccerGame.gif
--------------------------------------------------------------------------------
/images/sportballs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/sportballs.png
--------------------------------------------------------------------------------
/images/tennis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nitishp/react-native-soccer/912e595fb87f889d3fb3660834d16555f278147e/images/tennis.png
--------------------------------------------------------------------------------
/index.android.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sample React Native App
3 | * https://github.com/facebook/react-native
4 | * @flow
5 | */
6 |
7 | import React, { Component } from 'react';
8 | import {
9 | AppRegistry,
10 | } from 'react-native';
11 |
12 | import Soccer from './Soccer';
13 |
14 | AppRegistry.registerComponent('Soccer', () => Soccer);
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "keepup",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@types/react": "~18.0.24",
13 | "@types/react-native": "~0.70.6",
14 | "expo": "^47.0.13",
15 | "expo-haptics": "^12.0.1",
16 | "expo-splash-screen": "^0.17.5",
17 | "metro-core": "^0.74.1",
18 | "react": "18.1.0",
19 | "react-native": "0.70.5",
20 | "typescript": "^4.6.3"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {},
3 | "extends": "expo/tsconfig.base"
4 | }
5 |
--------------------------------------------------------------------------------
/useCachedResources.ts:
--------------------------------------------------------------------------------
1 | import * as SplashScreen from 'expo-splash-screen';
2 | import { useEffect, useState } from 'react';
3 |
4 | export default function useCachedResources() {
5 | const [isLoadingComplete, setLoadingComplete] = useState(false);
6 |
7 | useEffect(() => {
8 | async function loadResourcesAndDataAsync() {
9 | try {
10 | SplashScreen.preventAutoHideAsync();
11 | } catch (e) {
12 | console.warn(e);
13 | } finally {
14 | setLoadingComplete(true);
15 | SplashScreen.hideAsync();
16 | }
17 | }
18 |
19 | loadResourcesAndDataAsync();
20 | }, []);
21 |
22 | return isLoadingComplete;
23 | }
24 |
--------------------------------------------------------------------------------