├── .eslintrc.cjs ├── .gitignore ├── .vite └── deps_temp_287c3439 │ └── package.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── AI_HELPER.glb ├── barber.glb ├── dev7.glb ├── dev7_other.glb ├── house.glb ├── images │ ├── change_logo.png │ ├── change_me.png │ ├── favicon.svg │ ├── logo.png │ └── logo.psd ├── potsdamer_platz_1k.hdr └── vite.svg ├── rotating-cube-react.html ├── rotating-cube-webgl.html ├── rotating-cube.html ├── src ├── App.tsx ├── assets │ └── icons │ │ ├── IconAddImage.tsx │ │ ├── IconBeard.tsx │ │ ├── IconBeard1.tsx │ │ ├── IconBeard2.tsx │ │ ├── IconBeard3.tsx │ │ ├── IconBeard4.tsx │ │ ├── IconCamera.tsx │ │ ├── IconColor.tsx │ │ ├── IconContrast.tsx │ │ ├── IconFace.tsx │ │ ├── IconGlasses.tsx │ │ ├── IconGlasses2.tsx │ │ ├── IconHair1.tsx │ │ ├── IconHair2.tsx │ │ ├── IconHair3.tsx │ │ ├── IconHair4.tsx │ │ ├── IconHats.tsx │ │ ├── IconImage.tsx │ │ ├── IconLight.tsx │ │ ├── IconMenu.tsx │ │ ├── IconMoon.tsx │ │ ├── IconNo.tsx │ │ ├── IconPants1.tsx │ │ ├── IconPants2.tsx │ │ ├── IconPants3.tsx │ │ ├── IconPose.tsx │ │ ├── IconPose0.tsx │ │ ├── IconPose1.tsx │ │ ├── IconPose10.tsx │ │ ├── IconPose11.tsx │ │ ├── IconPose12.tsx │ │ ├── IconPose13.tsx │ │ ├── IconPose14.tsx │ │ ├── IconPose15.tsx │ │ ├── IconPose16.tsx │ │ ├── IconPose17.tsx │ │ ├── IconPose18.tsx │ │ ├── IconPose19.tsx │ │ ├── IconPose2.tsx │ │ ├── IconPose20.tsx │ │ ├── IconPose3.tsx │ │ ├── IconPose4.tsx │ │ ├── IconPose5.tsx │ │ ├── IconPose6.tsx │ │ ├── IconPose7.tsx │ │ ├── IconPose8.tsx │ │ ├── IconPose9.tsx │ │ ├── IconShoes1.tsx │ │ ├── IconShoes2.tsx │ │ ├── IconShoes3.tsx │ │ ├── IconSun.tsx │ │ ├── IconT1.tsx │ │ ├── IconT2.tsx │ │ ├── IconWatch.tsx │ │ └── Logo.tsx ├── components │ ├── BarberShop.tsx │ ├── Camera.tsx │ ├── Character.tsx │ ├── CharacterControls.tsx │ ├── CharacterMessage.tsx │ ├── Chatbox.tsx │ ├── ClothingShop.tsx │ ├── Ground.tsx │ ├── HelperCharacter.jsx │ ├── Lights.tsx │ ├── Loader.tsx │ ├── ManualPopup.tsx │ ├── MobileControls.tsx │ ├── MobileControlsProvider.tsx │ ├── MultiplayerManager.tsx │ ├── Portal.tsx │ ├── ProximityDetector.tsx │ ├── RemoteCharacter.tsx │ ├── RemoteCharactersManager.tsx │ ├── Roads.tsx │ ├── RotatingCube.tsx │ ├── RotatingCubePage.tsx │ ├── ShopCollision.tsx │ ├── StaticCharacterModel.tsx │ ├── SubToolbar.tsx │ ├── ThemeToggle.tsx │ ├── Toolbar.tsx │ └── ViewMode.tsx ├── contexts │ └── MultiplayerContext.tsx ├── helpers │ └── data.ts ├── hooks │ └── useMultiplayer.tsx ├── index.css ├── main.tsx ├── store │ ├── slices │ │ └── themeSlice.ts │ ├── store.ts │ └── types.ts ├── types │ └── window.d.ts └── vite-env.d.ts ├── tailwind.config.js ├── test.html ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | todo.txt 26 | -------------------------------------------------------------------------------- /.vite/deps_temp_287c3439/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | THREE.JS + React + TypeScript (React-Three-Fiber) 2 | 3 | I should update this, but fow now if you want to use it just: 4 | 1. npm install 5 | 2. npm run dev or npm run build 6 | 3. From "tsconfig.json", remove/comment out: 7 | "exclude": [ 8 | "src" 9 | ], 10 | 11 | This tells Vite to ignores all the typescript error messages and I can build/depoloy on Vercel. ( Should fix it in the future tho ) 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Simple Character Game 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ready-developer", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@react-three/drei": "^9.85.2", 14 | "@react-three/fiber": "^8.14.3", 15 | "@types/three": "^0.156.0", 16 | "@vercel/analytics": "^1.1.1", 17 | "classnames": "^2.3.2", 18 | "framer-motion": "^10.16.4", 19 | "leva": "^0.9.35", 20 | "react": "^18.2.0", 21 | "react-colorful": "^5.6.1", 22 | "react-dom": "^18.2.0", 23 | "react-responsive": "^9.0.2", 24 | "react-router-dom": "^7.4.1", 25 | "socket.io-client": "^4.8.1", 26 | "three": "^0.156.1", 27 | "zustand": "^4.4.1" 28 | }, 29 | "devDependencies": { 30 | "@types/react": "^18.2.15", 31 | "@types/react-dom": "^18.2.7", 32 | "@typescript-eslint/eslint-plugin": "^6.0.0", 33 | "@typescript-eslint/parser": "^6.0.0", 34 | "@vitejs/plugin-react": "^4.0.3", 35 | "autoprefixer": "^10.4.15", 36 | "eslint": "^8.45.0", 37 | "eslint-plugin-react-hooks": "^4.6.0", 38 | "eslint-plugin-react-refresh": "^0.4.3", 39 | "postcss": "^8.4.30", 40 | "tailwindcss": "^3.3.3", 41 | "typescript": "^5.0.2", 42 | "vite": "^4.4.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/AI_HELPER.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/AI_HELPER.glb -------------------------------------------------------------------------------- /public/barber.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/barber.glb -------------------------------------------------------------------------------- /public/dev7.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/dev7.glb -------------------------------------------------------------------------------- /public/dev7_other.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/dev7_other.glb -------------------------------------------------------------------------------- /public/house.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/house.glb -------------------------------------------------------------------------------- /public/images/change_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/images/change_logo.png -------------------------------------------------------------------------------- /public/images/change_me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/images/change_me.png -------------------------------------------------------------------------------- /public/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/images/logo.png -------------------------------------------------------------------------------- /public/images/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/images/logo.psd -------------------------------------------------------------------------------- /public/potsdamer_platz_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peeeeteer/readydeveloperme/db69565804bd7fdff4a067817f4c69088aacd5bd/public/potsdamer_platz_1k.hdr -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rotating-cube-react.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rotating White Cube - React Three Fiber 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 25 | 26 | 27 | 42 | 43 | 44 |
45 | 46 | 55 | 56 | 117 | 118 | -------------------------------------------------------------------------------- /rotating-cube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rotating White Cube 7 | 8 | 16 | 27 | 28 | 29 | 79 | 80 | -------------------------------------------------------------------------------- /src/assets/icons/IconAddImage.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconAddImage: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default IconAddImage; 21 | -------------------------------------------------------------------------------- /src/assets/icons/IconBeard.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconBeard: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default IconBeard; 21 | -------------------------------------------------------------------------------- /src/assets/icons/IconBeard1.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconBeard1: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconBeard1; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconBeard2.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconBeard2: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconBeard2; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconBeard3.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconBeard3: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconBeard3; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconBeard4.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconBeard4: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default IconBeard4; 15 | -------------------------------------------------------------------------------- /src/assets/icons/IconCamera.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconCamera: React.FC = ({ fill = "#6D7D93", ...rest }) => { 6 | return ( 7 | 15 | 20 | 27 | 34 | 35 | ); 36 | }; 37 | 38 | export default IconCamera; 39 | -------------------------------------------------------------------------------- /src/assets/icons/IconColor.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconColor: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 15 | 20 | 27 | 28 | ); 29 | }; 30 | 31 | export default IconColor; 32 | -------------------------------------------------------------------------------- /src/assets/icons/IconContrast.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconContrast: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 15 | 20 | 27 | 28 | ); 29 | }; 30 | 31 | export default IconContrast; 32 | -------------------------------------------------------------------------------- /src/assets/icons/IconFace.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconFace: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 15 | 20 | 27 | 28 | ); 29 | }; 30 | 31 | export default IconFace; 32 | -------------------------------------------------------------------------------- /src/assets/icons/IconGlasses.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconGlasses: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 9 | ); 10 | }; 11 | 12 | export default IconGlasses; 13 | -------------------------------------------------------------------------------- /src/assets/icons/IconGlasses2.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconGlasses1: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default IconGlasses1; 17 | -------------------------------------------------------------------------------- /src/assets/icons/IconHair1.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconHair1: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default IconHair1; 19 | -------------------------------------------------------------------------------- /src/assets/icons/IconHair2.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconHair2: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default IconHair2; 13 | -------------------------------------------------------------------------------- /src/assets/icons/IconHair3.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconHair3: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconHair3; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconHair4.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconHair4: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconHair4; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconHats.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconHats: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 10 | 17 | 18 | ); 19 | }; 20 | 21 | export default IconHats; 22 | -------------------------------------------------------------------------------- /src/assets/icons/IconImage.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconImage: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 15 | 22 | 23 | ); 24 | }; 25 | 26 | export default IconImage; 27 | -------------------------------------------------------------------------------- /src/assets/icons/IconLight.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconLight: React.FC = ({ fill = "#6D7D93", ...rest }) => { 6 | return ( 7 | 15 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default IconLight; 26 | -------------------------------------------------------------------------------- /src/assets/icons/IconMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconMenu: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 15 | 22 | 23 | ); 24 | }; 25 | 26 | export default IconMenu; 27 | -------------------------------------------------------------------------------- /src/assets/icons/IconMoon.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconMoon: React.FC = ({ fill = "#6D7D93", ...rest }) => { 6 | return ( 7 | 15 | 20 | 27 | 28 | ); 29 | }; 30 | 31 | export default IconMoon; 32 | -------------------------------------------------------------------------------- /src/assets/icons/IconNo.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconNo: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default IconNo; 19 | -------------------------------------------------------------------------------- /src/assets/icons/IconPants1.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPants1: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default IconPants1; 17 | -------------------------------------------------------------------------------- /src/assets/icons/IconPants2.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPants2: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconPants2; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconPants3.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPants3: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconPants3; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 15 | 19 | 25 | 26 | ); 27 | }; 28 | 29 | export default IconPose; 30 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose0.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose0: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default IconPose0; 15 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose1.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose1: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default IconPose1; 13 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose10.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose10: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default IconPose10; 17 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose11.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose11: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default IconPose11; 17 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose12.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose12: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default IconPose12; 17 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose13.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose13: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose13; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose14.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose14: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose14; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose15.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose15: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default IconPose15; 17 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose16.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose16: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default IconPose16; 17 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose17.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose17: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default IconPose17; 17 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose18.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose18: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default IconPose18; 18 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose19.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose19: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose19; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose2.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose2: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose2; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose20.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose20: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose20; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose3.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose3: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose3; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose4.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose4: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose4; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose5.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose5: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose5; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose6.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose6: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose6; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose7.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose7: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose7; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose8.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose8: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose8; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconPose9.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconPose9: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default IconPose9; 16 | -------------------------------------------------------------------------------- /src/assets/icons/IconShoes1.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconShoes1: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconShoes1; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconShoes2.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconShoes2: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconShoes2; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconShoes3.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconShoes3: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconShoes3; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconSun.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconSun: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 15 | 20 | 27 | 28 | ); 29 | }; 30 | 31 | export default IconSun; 32 | -------------------------------------------------------------------------------- /src/assets/icons/IconT1.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconT1: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default IconT1; 15 | -------------------------------------------------------------------------------- /src/assets/icons/IconT2.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconT2: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default IconT2; 14 | -------------------------------------------------------------------------------- /src/assets/icons/IconWatch.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | 3 | type Props = SVGProps; 4 | 5 | const IconWatch: React.FC = ({ fill = "#4B50EC", ...rest }) => { 6 | return ( 7 | 8 | 11 | ); 12 | }; 13 | 14 | export default IconWatch; 15 | -------------------------------------------------------------------------------- /src/components/BarberShop.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from 'react'; 2 | import { useGLTF, Shadow, Html } from '@react-three/drei'; 3 | import * as THREE from 'three'; 4 | import { GLTF } from 'three-stdlib'; 5 | 6 | // Define the type for the GLTF result 7 | type GLTFResult = GLTF & { 8 | nodes: { 9 | geometry_0: THREE.Mesh 10 | } 11 | materials: { 12 | [key: string]: THREE.Material 13 | } 14 | } 15 | 16 | interface BarberShopProps { 17 | position?: [number, number, number]; 18 | onChangeHair?: () => void; 19 | canChangeHair?: boolean; 20 | isCustomizing?: boolean; 21 | } 22 | 23 | const BarberShop = ({ 24 | position = [0, 0, 0], 25 | onChangeHair, 26 | canChangeHair = true, 27 | isCustomizing = false 28 | }: BarberShopProps) => { 29 | const groupRef = useRef(null); 30 | const [badgeHovered, setBadgeHovered] = useState(false); 31 | 32 | // Load the barber model 33 | const { nodes, materials } = useGLTF('/barber.glb') as GLTFResult; 34 | 35 | // Barber shop-specific configuration 36 | const shopScale = [5, 5, 5] as [number, number, number]; 37 | const shopRotation = [0, -Math.PI / 5.5, 0] as [number, number, number]; 38 | const shopOffset = [0, 2.5, 0] as [number, number, number]; 39 | 40 | // Colors for the badge 41 | const nearColor = '#f7c948'; // Yellow "Coming Soon" color when near 42 | const normalColor = '#8B4513'; // Light brown when not near (was #3a4673 blue) 43 | const nearOpacity = 0.5; // Opacity when near 44 | const normalOpacity = 0.7; // Opacity when not near 45 | 46 | // Handle badge click 47 | const handleBadgeClick = (e: React.MouseEvent) => { 48 | e.stopPropagation(); 49 | // Do nothing - shop is coming soon 50 | console.log("Barber shop is coming soon!"); 51 | }; 52 | 53 | // Pulse animation for the badge 54 | const [badgeScale, setBadgeScale] = useState(1); 55 | useEffect(() => { 56 | if (!canChangeHair) return; 57 | 58 | let animationFrameId: number; 59 | let time = 0; 60 | 61 | const animate = () => { 62 | time += 0.03; 63 | // Pulse between 1 and 1.1 when player is nearby 64 | if (canChangeHair) { 65 | setBadgeScale(1 + Math.sin(time) * 0.05); 66 | } 67 | animationFrameId = requestAnimationFrame(animate); 68 | }; 69 | 70 | animate(); 71 | return () => cancelAnimationFrame(animationFrameId); 72 | }, [canChangeHair]); 73 | 74 | // Get badge text based on customization state 75 | const getBadgeText = () => { 76 | if (canChangeHair) { 77 | return "Coming Soon"; 78 | } else { 79 | return "Change Hair Here"; 80 | } 81 | }; 82 | 83 | return ( 84 | 85 | {/* The barber shop model with its own positioning, rotation and scale */} 86 | { 91 | document.body.style.cursor = 'auto'; // Always show default cursor 92 | }} 93 | onPointerOut={() => { 94 | document.body.style.cursor = 'auto'; 95 | }} 96 | > 97 | 103 | 104 | 105 | {/* 2D Badge above the barber shop */} 106 | 118 | 142 | 143 | 144 | {/* Custom shadow under the barber shop */} 145 | 152 | 153 | ); 154 | }; 155 | 156 | // Preload the model to avoid flickering 157 | useGLTF.preload('/barber.glb'); 158 | 159 | export default BarberShop; -------------------------------------------------------------------------------- /src/components/CharacterControls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useFrame } from '@react-three/fiber'; 3 | import { useRef, useState, useEffect, MutableRefObject } from 'react'; 4 | import * as THREE from 'three'; 5 | // Add import for useMultiplayer hook 6 | import { useMultiplayer } from '../contexts/MultiplayerContext'; 7 | // Remove the import as we're not using it directly here 8 | // import MobileControls from './MobileControls'; 9 | 10 | // Window interface is now defined globally in src/types/window.d.ts 11 | // declare global { 12 | // interface Window { 13 | // ... removed declarations ... 14 | // } 15 | // } 16 | 17 | // Export the MovementState interface 18 | export interface MovementState { 19 | forward: boolean; 20 | turnLeft: boolean; 21 | turnRight: boolean; 22 | running: boolean; 23 | } 24 | 25 | interface CharacterControlsProps { 26 | characterRef: MutableRefObject; 27 | } 28 | 29 | const CharacterControls = ({ characterRef }: CharacterControlsProps) => { 30 | // Get socket from multiplayer context 31 | const { socket, isConnected } = useMultiplayer(); 32 | 33 | // Movement state 34 | const [movement, setMovement] = useState({ 35 | forward: false, 36 | turnLeft: false, 37 | turnRight: false, 38 | running: false 39 | }); 40 | 41 | // Character physics 42 | const characterRotation = useRef(Math.PI); 43 | const currentVelocity = useRef(new THREE.Vector3(0, 0, 0)); 44 | const isMoving = useRef(false); 45 | const lastPosition = useRef(new THREE.Vector3()); 46 | const lastSentPosition = useRef(new THREE.Vector3()); 47 | const lastSentRotation = useRef(0); 48 | 49 | // Throttling 50 | const lastUpdateTime = useRef(0); 51 | const UPDATE_INTERVAL = 1000 / 15; // 15 updates per second max 52 | 53 | // Movement configuration 54 | const walkSpeed = 0.04; 55 | const runSpeed = 0.08; 56 | const turnSpeed = 0.04; // How quickly character rotates when A/D are pressed 57 | const acceleration = 0.15; // Acceleration for forward movement 58 | const friction = 0.85; // Friction when slowing down 59 | 60 | useEffect(() => { 61 | const handleKeyDown = (e: KeyboardEvent) => { 62 | // Don't handle movement if chatbox is open or focused 63 | if (window.chatboxOpen || window.chatboxFocused || window.isCustomizingClothing) return; 64 | 65 | switch (e.key.toLowerCase()) { 66 | case 'w': 67 | setMovement(prev => ({ ...prev, forward: true })); 68 | break; 69 | case 'a': 70 | setMovement(prev => ({ ...prev, turnLeft: true })); 71 | break; 72 | case 'd': 73 | setMovement(prev => ({ ...prev, turnRight: true })); 74 | break; 75 | case 'shift': 76 | setMovement(prev => ({ ...prev, running: true })); 77 | break; 78 | } 79 | }; 80 | 81 | const handleKeyUp = (e: KeyboardEvent) => { 82 | // Don't handle movement if chatbox is open or focused 83 | if (window.chatboxOpen || window.chatboxFocused || window.isCustomizingClothing) return; 84 | 85 | switch (e.key.toLowerCase()) { 86 | case 'w': 87 | setMovement(prev => ({ ...prev, forward: false })); 88 | break; 89 | case 'a': 90 | setMovement(prev => ({ ...prev, turnLeft: false })); 91 | break; 92 | case 'd': 93 | setMovement(prev => ({ ...prev, turnRight: false })); 94 | break; 95 | case 'shift': 96 | setMovement(prev => ({ ...prev, running: false })); 97 | break; 98 | } 99 | }; 100 | 101 | // Add event listeners to global window object for keydown/keyup events 102 | window.addEventListener('keydown', handleKeyDown); 103 | window.addEventListener('keyup', handleKeyUp); 104 | 105 | // Expose movement setter for external control (mobile) 106 | if (typeof window !== 'undefined') { 107 | window.setCharacterMovement = setMovement; 108 | } 109 | 110 | return () => { 111 | window.removeEventListener('keydown', handleKeyDown); 112 | window.removeEventListener('keyup', handleKeyUp); 113 | if (typeof window !== 'undefined' && 'setCharacterMovement' in window) { 114 | // @ts-ignore: Safely remove the property 115 | window.setCharacterMovement = null; 116 | } 117 | }; 118 | }, []); 119 | 120 | // Function to send position updates to server 121 | const sendPositionUpdate = (force = false) => { 122 | if (!isConnected || !socket || !characterRef.current) return; 123 | 124 | const now = Date.now(); 125 | const position = characterRef.current.position; 126 | const rotation = characterRotation.current; 127 | 128 | // Check if we need to send an update (throttling) 129 | const timeSinceLastUpdate = now - lastUpdateTime.current; 130 | const positionChanged = lastSentPosition.current.distanceToSquared(position) > 0.01; 131 | const rotationChanged = Math.abs(lastSentRotation.current - rotation) > 0.05; 132 | 133 | // Only send updates if there's a significant change or if forced 134 | if (force || ((positionChanged || rotationChanged) && timeSinceLastUpdate > UPDATE_INTERVAL)) { 135 | // Send position and rotation to server 136 | socket.emit('updatePosition', { 137 | position: { 138 | x: position.x, 139 | y: position.y, 140 | z: position.z 141 | }, 142 | rotation: rotation, 143 | moving: isMoving.current 144 | }); 145 | 146 | // Update last sent values 147 | lastSentPosition.current.copy(position); 148 | lastSentRotation.current = rotation; 149 | lastUpdateTime.current = now; 150 | 151 | // Log (for testing Phase 2) 152 | // console.log('Multiplayer: Sent position update', { 153 | // x: parseFloat(position.x.toFixed(2)), 154 | // z: parseFloat(position.z.toFixed(2)), 155 | // r: parseFloat(rotation.toFixed(2)) 156 | // }); 157 | } 158 | }; 159 | 160 | useFrame((state, delta) => { 161 | // Check if characterRef exists and is valid 162 | if (!characterRef?.current) return; 163 | 164 | // If chatbox is open or customizing clothing, stop all movement 165 | if (window.chatboxOpen || window.isCustomizingClothing) { 166 | // Reset movement state to prevent character movement 167 | if (isMoving.current) { 168 | currentVelocity.current.set(0, 0, 0); 169 | isMoving.current = false; 170 | } 171 | return; 172 | } 173 | 174 | // Initialize last position if needed 175 | if (!lastPosition.current.x && !lastPosition.current.y && !lastPosition.current.z) { 176 | lastPosition.current.copy(characterRef.current.position); 177 | } 178 | 179 | // Track if we were moving previously 180 | const wasMoving = isMoving.current; 181 | 182 | // First handle rotation (A/D keys) 183 | if (movement.turnLeft) { 184 | characterRotation.current += turnSpeed; 185 | characterRef.current.rotation.y = characterRotation.current; 186 | } 187 | if (movement.turnRight) { 188 | characterRotation.current -= turnSpeed; 189 | characterRef.current.rotation.y = characterRotation.current; 190 | } 191 | 192 | // Then handle forward movement (W key) 193 | const isForwardMoving = movement.forward; 194 | isMoving.current = isForwardMoving; 195 | 196 | if (!isMoving.current) { 197 | // Apply friction when not moving 198 | currentVelocity.current.multiplyScalar(friction); 199 | 200 | // Apply a small velocity threshold to fully stop 201 | if (currentVelocity.current.length() < 0.001) { 202 | currentVelocity.current.set(0, 0, 0); 203 | } 204 | } else { 205 | // Calculate speed based on movement state 206 | const currentSpeed = movement.running ? runSpeed : walkSpeed; 207 | 208 | // Calculate movement direction based on character's rotation 209 | const moveDirection = new THREE.Vector3( 210 | Math.sin(characterRotation.current), 211 | 0, 212 | Math.cos(characterRotation.current) 213 | ); 214 | 215 | // Set target velocity based on direction and speed 216 | const targetVelocity = moveDirection.multiplyScalar(currentSpeed); 217 | 218 | // Smoothly accelerate toward target velocity 219 | currentVelocity.current.x += (targetVelocity.x - currentVelocity.current.x) * acceleration; 220 | currentVelocity.current.z += (targetVelocity.z - currentVelocity.current.z) * acceleration; 221 | } 222 | 223 | // Update character position 224 | characterRef.current.position.x += currentVelocity.current.x; 225 | characterRef.current.position.z += currentVelocity.current.z; 226 | 227 | // Store the current position for next frame 228 | lastPosition.current.copy(characterRef.current.position); 229 | 230 | // Check if we need to send position update 231 | // Always send when movement state changes (started/stopped moving) 232 | if (wasMoving !== isMoving.current) { 233 | sendPositionUpdate(true); 234 | } else { 235 | sendPositionUpdate(); 236 | } 237 | }); 238 | 239 | // Return null here as this component just adds behavior, not visuals 240 | return null; 241 | }; 242 | 243 | export default CharacterControls; -------------------------------------------------------------------------------- /src/components/CharacterMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Html } from '@react-three/drei'; 3 | import { useFrame } from '@react-three/fiber'; 4 | import * as THREE from 'three'; 5 | 6 | interface CharacterMessageProps { 7 | characterRef: React.RefObject; 8 | } 9 | 10 | const CharacterMessage = ({ characterRef }: CharacterMessageProps) => { 11 | const [message, setMessage] = useState(''); 12 | const [visible, setVisible] = useState(false); 13 | const groupRef = useRef(null); 14 | 15 | // Setup message handler on window 16 | useEffect(() => { 17 | window.showMessage = (newMessage: string) => { 18 | setMessage(newMessage); 19 | setVisible(true); 20 | 21 | // Hide message after 5 seconds 22 | const timer = setTimeout(() => { 23 | setVisible(false); 24 | }, 5000); 25 | 26 | return () => clearTimeout(timer); 27 | }; 28 | 29 | return () => { 30 | window.showMessage = undefined; 31 | }; 32 | }, []); 33 | 34 | // Update message position to follow character on each frame 35 | useFrame(() => { 36 | if (visible && groupRef.current && characterRef.current) { 37 | // Copy the character position to the message group 38 | groupRef.current.position.copy(characterRef.current.position); 39 | } 40 | }); 41 | 42 | if (!visible || !message) return null; 43 | 44 | return ( 45 | 46 | 53 |
63 |

{message}

64 |
65 | 66 |
67 | ); 68 | }; 69 | 70 | export default CharacterMessage; -------------------------------------------------------------------------------- /src/components/ClothingShop.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from 'react'; 2 | import { useGLTF, Shadow, Html } from '@react-three/drei'; 3 | import * as THREE from 'three'; 4 | import { GLTF } from 'three-stdlib'; 5 | import { useStore } from '../store/store'; // Import useStore 6 | import SubToolbar from './SubToolbar'; // Import SubToolbar 7 | import { Tool } from '../helpers/data'; // Import Tool type if not already 8 | 9 | // Define the type for the GLTF result 10 | type GLTFResult = GLTF & { 11 | nodes: { 12 | geometry_0: THREE.Mesh 13 | } 14 | materials: { 15 | [key: string]: THREE.Material 16 | } 17 | } 18 | 19 | interface ClothingShopProps { 20 | position?: [number, number, number]; 21 | onChangeClothing?: () => void; 22 | canChangeClothing?: boolean; 23 | isCustomizing?: boolean; 24 | // Add props needed for customization UI 25 | tool?: Tool; 26 | selected?: Record; 27 | subToolColors?: any[]; // Use a more specific type if available 28 | onClickItem?: (item: any) => void; // Use a more specific type if available 29 | onChangeColor?: (color: any) => void; // Use a more specific type if available 30 | onExitCustomization?: () => void; 31 | isMobile?: boolean; // Pass isMobile detection 32 | } 33 | 34 | const ClothingShop = ({ 35 | position = [0, 0, 0], 36 | onChangeClothing, 37 | canChangeClothing = true, 38 | isCustomizing = false, // Add default value 39 | tool, 40 | selected, 41 | subToolColors, 42 | onClickItem, 43 | onChangeColor, 44 | onExitCustomization, 45 | isMobile = false // Add default value 46 | }: ClothingShopProps) => { 47 | const groupRef = useRef(null); 48 | const [badgeHovered, setBadgeHovered] = useState(false); 49 | 50 | // Load the house model 51 | const { nodes, materials } = useGLTF('/house.glb') as GLTFResult; 52 | 53 | // House-specific configuration - modify these values to adjust the house only 54 | const houseScale = [5, 5, 5] as [number, number, number]; // Scale the house 55 | const houseRotation = [0, 0, 0] as [number, number, number]; // Rotate house 20 degrees to the left 56 | const houseOffset = [0, 2.5, 0] as [number, number, number]; // Additional offset for the house within the group 57 | 58 | // Original ClothingShop colors 59 | const nearColor = '#22aa22'; // Bright green when near 60 | const normalColor = '#3a4673'; // Dark greyish blue when not near (was #ff4444 red) 61 | const nearOpacity = 0.5; // Opacity when near 62 | const normalOpacity = 0.7; // Opacity when not near 63 | 64 | // Get badge text based on customization state 65 | const getBadgeText = () => { 66 | if (isCustomizing) { 67 | return "Exit Clothing"; // Show "Exit Clothing" when customizing 68 | } else if (canChangeClothing) { 69 | return "Change Clothing"; 70 | } else { 71 | return "Change Clothing Here"; // Or maybe just hide the badge entirely if not usable? 72 | } 73 | }; 74 | 75 | // Handle badge click - toggles customization mode 76 | const handleBadgeClick = (e: React.MouseEvent) => { 77 | e.stopPropagation(); 78 | 79 | // If customizing, clicking the badge should exit customization 80 | if (isCustomizing && onExitCustomization) { 81 | onExitCustomization(); 82 | } 83 | // If not customizing, ONLY allow interaction when canChangeClothing is true (player is near) 84 | else if (canChangeClothing && onChangeClothing) { 85 | // Call onChangeClothing to position character and enter customization 86 | onChangeClothing(); 87 | } 88 | // Ignore clicks when too far away (when button says "Change Clothing Here") 89 | }; 90 | 91 | // Pulse animation for the badge 92 | const [badgeScale, setBadgeScale] = useState(1); 93 | useEffect(() => { 94 | if (!canChangeClothing) return; 95 | 96 | let animationFrameId: number; 97 | let time = 0; 98 | 99 | const animate = () => { 100 | time += 0.03; 101 | // Pulse between 1 and 1.1 when player is nearby 102 | if (canChangeClothing) { 103 | setBadgeScale(1 + Math.sin(time) * 0.05); 104 | } 105 | animationFrameId = requestAnimationFrame(animate); 106 | }; 107 | 108 | animate(); 109 | return () => cancelAnimationFrame(animationFrameId); 110 | }, [canChangeClothing]); 111 | 112 | return ( 113 | 114 | {/* The house model with its own positioning, rotation and scale */} 115 | { 121 | // Don't show pointer cursor when hovering over the house 122 | document.body.style.cursor = 'auto'; 123 | }} 124 | onPointerOut={() => { 125 | document.body.style.cursor = 'auto'; 126 | }} 127 | > 128 | 134 | 135 | 136 | {/* 2D Badge above the house */} 137 | 159 | 183 | 184 | 185 | {/* Custom shadow under the house */} 186 | 193 | 194 | {/* Customization UI - Show only when customizing */} 195 | {isCustomizing && ( 196 | 210 |
216 | {/* Render SubToolbar - It now handles its own layout */} 217 | {tool && selected && subToolColors && onClickItem && onChangeColor && ( 218 | 226 | )} 227 |
228 | 229 | )} 230 |
231 | ); 232 | }; 233 | 234 | // Preload the model to avoid flickering 235 | useGLTF.preload('/house.glb'); 236 | 237 | export default ClothingShop; -------------------------------------------------------------------------------- /src/components/Ground.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid } from "@react-three/drei"; 3 | 4 | export default function Ground({ theme, visible }: { theme: string, visible: boolean }) { 5 | 6 | // const { cellColor, sectionColor } = useControls('Grid', { cellColor: '#DFAD06', sectionColor: '#C19400' }) 7 | 8 | // Dark mode, c: #484848 ,s: #4e4e4e // Light c:#1922ad s:#4737ad 9 | if (visible === false) return null 10 | 11 | if (theme === "light") { 12 | const gridConfig = { 13 | cellSize: 0, // 0,5 14 | cellThickness: 0.5, 15 | cellColor: "#1922ad", 16 | sectionSize: 1, // 3 17 | sectionThickness: 1, 18 | sectionColor: "#4737ad", 19 | fadeDistance: 45, 20 | fadeStrength: 0.8, 21 | followCamera: false, 22 | infiniteGrid: true 23 | } 24 | return 25 | } 26 | 27 | else { 28 | const gridConfig = { 29 | cellSize: 0, // 0,5 30 | cellThickness: 0.5, 31 | cellColor: "#484848", 32 | sectionSize: 1, // 3 33 | sectionThickness: 1, 34 | sectionColor: "#4e4e4e", 35 | fadeDistance: 45, 36 | fadeStrength: 0.8, 37 | followCamera: false, 38 | infiniteGrid: true 39 | } 40 | return 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/components/Lights.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Environment } from '@react-three/drei' 3 | import { useControls } from 'leva' 4 | import * as THREE from 'three' 5 | 6 | const Lights = ({ selected }: { selected?: { lights?: string } }) => { 7 | // Light 1 Controls 8 | const { spotLightIntensitiy1, spotLightPosition1, spotLightIntensitiy2, spotLightPosition2 } = useControls('Lights1', { 9 | spotLightIntensitiy1: { 10 | value: 90, 11 | min: 50, 12 | max: 350, 13 | step: 1, 14 | }, 15 | spotLightPosition1: { 16 | value: [0, 5, -5], 17 | step: 0.1, 18 | }, 19 | spotLightIntensitiy2: { 20 | value: 150, 21 | min: 50, 22 | max: 350, 23 | step: 1, 24 | }, 25 | spotLightPosition2: { 26 | value: [-3, 5, 10], 27 | step: 0.1, 28 | }, 29 | }) 30 | 31 | // Lights2 Controls 32 | const { 33 | pointLightIntensitiy1, pointLightColor1, pointLightPosition1, 34 | pointLightIntensitiy2, pointLightColor2, pointLightPosition2, 35 | pointLightIntensitiy3, pointLightColor3, pointLightPosition3 } 36 | = useControls('Lights2', 37 | { 38 | pointLightIntensitiy1: { 39 | value: 10, 40 | min: 0, 41 | max: 100, 42 | step: 1, 43 | }, 44 | pointLightColor1: { 45 | value: "#ffedbf", 46 | }, 47 | pointLightPosition1: { 48 | value: [2, 3, 0], 49 | step: 0.1, 50 | }, 51 | pointLightIntensitiy2: { 52 | value: 10, 53 | min: 0, 54 | max: 100, 55 | step: 1, 56 | }, 57 | pointLightColor2: { 58 | value: "#ffedbf", 59 | }, 60 | pointLightPosition2: { 61 | value: [-2, 3, 0], 62 | step: 0.1, 63 | }, 64 | pointLightIntensitiy3: { 65 | value: 10, 66 | min: 0, 67 | max: 100, 68 | step: 1, 69 | }, 70 | pointLightColor3: { 71 | value: "#ffedbf", 72 | }, 73 | pointLightPosition3: { 74 | value: [0, 1, 3], 75 | step: 0.1, 76 | }, 77 | }) 78 | 79 | const Lights1 = () => { 80 | return ( 81 | <> 82 | 83 | 84 | 91 | 98 | 99 | ); 100 | } 101 | 102 | const Lights2 = () => { 103 | return ( 104 | <> 105 | 112 | 118 | 124 | 130 | 131 | ); 132 | } 133 | 134 | // Default lighting setup if no specific option is selected 135 | const DefaultLights = () => { 136 | return ( 137 | <> 138 | 139 | 140 | 147 | 148 | ); 149 | } 150 | 151 | return ( 152 | <> 153 | {(!selected?.lights || selected.lights === "lights_0") && } 154 | {selected?.lights === "lights_1" && } 155 | {selected?.lights === "lights_2" && } 156 | 157 | ); 158 | } 159 | 160 | export default Lights -------------------------------------------------------------------------------- /src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { useProgress } from "@react-three/drei"; 2 | import React, { useEffect, useState, useRef } from "react"; 3 | import classNames from "classnames"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { useMediaQuery } from "react-responsive"; 6 | 7 | import { useStore } from "../store/store"; 8 | 9 | interface LoaderProps { 10 | // Callback function when loading is finished and username is submitted 11 | onLoadedSubmit?: (username: string) => void; 12 | } 13 | 14 | const Loader: React.FC = ({ onLoadedSubmit }) => { 15 | const theme = useStore((state) => state.theme); 16 | const [isActive, setIsActive] = useState(true); 17 | const [dummyProgress, setDummyProgress] = useState(0); 18 | const { progress, total } = useProgress(); 19 | const [isLoadComplete, setIsLoadComplete] = useState(false); 20 | const [username, setUsername] = useState(''); 21 | const inputRef = useRef(null); 22 | 23 | const isMobile = useMediaQuery({ query: '(max-width: 768px)' }); 24 | 25 | const value = dummyProgress || Math.ceil(progress); 26 | const isProgressFinished = value >= 100; 27 | 28 | useEffect(() => { 29 | let timeout: number | null = null; 30 | if (total === 0) { 31 | timeout = setTimeout(() => { 32 | setDummyProgress(100); 33 | }, 500); 34 | } 35 | return () => { 36 | if (timeout) clearTimeout(timeout); 37 | }; 38 | }, [total]); 39 | 40 | useEffect(() => { 41 | if (isProgressFinished && !isLoadComplete) { 42 | const timer = setTimeout(() => { 43 | setIsLoadComplete(true); 44 | inputRef.current?.focus(); 45 | }, 500); 46 | return () => clearTimeout(timer); 47 | } 48 | }, [isProgressFinished, isLoadComplete]); 49 | 50 | const handleSubmit = (e?: React.FormEvent) => { 51 | e?.preventDefault(); 52 | const trimmedUsername = username.trim(); 53 | if (onLoadedSubmit && isLoadComplete && trimmedUsername) { 54 | console.log("Loader submitting username:", trimmedUsername); 55 | onLoadedSubmit(trimmedUsername); 56 | setIsActive(false); 57 | } else if (!onLoadedSubmit && isLoadComplete) { 58 | setIsActive(false); 59 | } 60 | }; 61 | 62 | return ( 63 | 64 | {isActive && ( 65 | 83 | 92 | 99 | 100 | 101 | 115 | 116 | 117 | 118 | 126 | 133 | 137 | 138 | 139 | 140 | {onLoadedSubmit && ( 141 | 157 |
158 |

Choose Your Developer Name

163 | setUsername(e.target.value.slice(0, 16))} 168 | maxLength={16} 169 | placeholder="Enter username..." 170 | disabled={!isLoadComplete} 171 | style={{ 172 | padding: isMobile ? '15px 20px' : '10px 15px', 173 | fontSize: isMobile ? '1.4em' : '1em', 174 | border: 'none', 175 | borderRadius: '8px', 176 | marginBottom: '20px', 177 | width: isMobile ? '85%' : '250px', 178 | maxWidth: '350px', 179 | background: 'rgba(255, 255, 255, 0.1)', 180 | color: 'white', 181 | opacity: isLoadComplete ? 1 : 0.5, 182 | cursor: isLoadComplete ? 'text' : 'not-allowed' 183 | }} 184 | required 185 | /> 186 |
187 | 208 |
209 |
210 | )} 211 |
212 | )} 213 |
214 | ); 215 | }; 216 | 217 | export default Loader; 218 | -------------------------------------------------------------------------------- /src/components/ManualPopup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { motion, AnimatePresence } from "framer-motion"; 3 | import classNames from "classnames"; 4 | 5 | import { useStore } from "../store/store"; 6 | 7 | type Props = { 8 | isOpen: boolean; 9 | onClickClose: () => void; 10 | }; 11 | 12 | const ManualPopup: React.FC = ({ isOpen, onClickClose }) => { 13 | const theme = useStore((state) => state.theme); 14 | 15 | return ( 16 | 17 | {isOpen && ( 18 | 31 | e.stopPropagation()} 45 | > 46 | 54 | 55 |
56 |

65 | Ready Developer Me 66 | . 67 |

68 |
75 |

76 | The easiest way to get talk to other Developers. 77 |

    78 |
  1. Customize the appearance.
  2. 79 |
  3. Chat and Choose poses while talking
  4. 80 |
  5. Profit?
  6. 81 |
82 |

83 | 84 |

85 | The renders work great with the{" "} 86 | 92 | 3D Portfolio Template 93 | {" "} 94 | I made. Grab the code from{" "} 95 | 101 | GitHub 102 | {" "} 103 | and make your own Portfolio Website in minutes. 104 |

105 | 106 |

107 | 113 | See the source code for Ready Developer Me. 114 | 115 |

116 | 117 |
118 |

119 | Created by{" "} 120 | 126 | llo7d 127 | 128 |

129 |
130 |
131 |
132 |
133 |
134 | )} 135 |
136 | ); 137 | }; 138 | 139 | export default ManualPopup; 140 | -------------------------------------------------------------------------------- /src/components/MobileControls.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useMediaQuery } from 'react-responsive'; 3 | 4 | interface MobileControlsProps { 5 | onMove: (direction: 'forward' | 'none') => void; 6 | onTurn: (direction: 'left' | 'right' | 'none') => void; 7 | onRun: (isRunning: boolean) => void; 8 | } 9 | 10 | const mobileControlsStyle: React.CSSProperties = { 11 | position: 'fixed', 12 | bottom: '20px', 13 | left: 0, 14 | right: 0, 15 | display: 'flex', 16 | justifyContent: 'center', 17 | alignItems: 'center', 18 | zIndex: 1000, 19 | pointerEvents: 'none', 20 | }; 21 | 22 | const controlButtonStyle: React.CSSProperties = { 23 | width: '70px', 24 | height: '70px', 25 | borderRadius: '50%', 26 | background: 'rgba(255, 255, 255, 0.25)', 27 | backdropFilter: 'blur(4px)', 28 | border: '2px solid rgba(255, 255, 255, 0.5)', 29 | color: 'white', 30 | margin: '0 10px', 31 | display: 'flex', 32 | alignItems: 'center', 33 | justifyContent: 'center', 34 | pointerEvents: 'auto', 35 | WebkitTapHighlightColor: 'transparent', 36 | touchAction: 'manipulation', 37 | transition: 'transform 0.1s ease-out, background 0.1s ease-out', 38 | cursor: 'pointer', 39 | outline: 'none', 40 | }; 41 | 42 | const activeButtonStyle: React.CSSProperties = { 43 | ...controlButtonStyle, 44 | background: 'rgba(255, 255, 255, 0.4)', 45 | transform: 'scale(1.1)', 46 | }; 47 | 48 | const forwardButtonStyle: React.CSSProperties = { 49 | ...controlButtonStyle, 50 | position: 'absolute', 51 | bottom: '80px', 52 | left: '50%', 53 | transform: 'translateX(-50%)', 54 | }; 55 | 56 | const activeForwardButtonStyle: React.CSSProperties = { 57 | ...forwardButtonStyle, 58 | background: 'rgba(255, 255, 255, 0.4)', 59 | transform: 'translateX(-50%) scale(1.1)', 60 | }; 61 | 62 | const leftButtonStyle: React.CSSProperties = { 63 | ...controlButtonStyle, 64 | position: 'absolute', 65 | bottom: '20px', 66 | left: 'calc(50% - 80px)', 67 | }; 68 | 69 | const activeLeftButtonStyle: React.CSSProperties = { 70 | ...leftButtonStyle, 71 | background: 'rgba(255, 255, 255, 0.4)', 72 | transform: 'scale(1.1)', 73 | }; 74 | 75 | const rightButtonStyle: React.CSSProperties = { 76 | ...controlButtonStyle, 77 | position: 'absolute', 78 | bottom: '20px', 79 | right: 'calc(50% - 80px)', 80 | }; 81 | 82 | const activeRightButtonStyle: React.CSSProperties = { 83 | ...rightButtonStyle, 84 | background: 'rgba(255, 255, 255, 0.4)', 85 | transform: 'scale(1.1)', 86 | }; 87 | 88 | const runButtonStyle: React.CSSProperties = { 89 | ...controlButtonStyle, 90 | position: 'absolute', 91 | bottom: '20px', 92 | right: '20px', 93 | background: 'rgba(52, 152, 219, 0.4)', 94 | }; 95 | 96 | const activeRunButtonStyle: React.CSSProperties = { 97 | ...runButtonStyle, 98 | background: 'rgba(52, 152, 219, 0.7)', 99 | transform: 'scale(1.1)', 100 | }; 101 | 102 | const svgStyle: React.CSSProperties = { 103 | width: '30px', 104 | height: '30px', 105 | }; 106 | 107 | const MobileControls: React.FC = ({ onMove, onTurn, onRun }) => { 108 | const isMobileScreen = useMediaQuery({ query: '(max-width: 768px)' }); 109 | const isTabletScreen = useMediaQuery({ query: '(max-width: 1024px)' }); 110 | const [isTouchDevice, setIsTouchDevice] = useState(false); 111 | const [showControls, setShowControls] = useState(false); 112 | 113 | // Button active states 114 | const [forwardActive, setForwardActive] = useState(false); 115 | const [leftActive, setLeftActive] = useState(false); 116 | const [rightActive, setRightActive] = useState(false); 117 | const [runActive, setRunActive] = useState(false); 118 | 119 | // Detect if device has touch capability 120 | useEffect(() => { 121 | const detectTouch = () => { 122 | const hasTouchScreen = ( 123 | 'ontouchstart' in window || 124 | navigator.maxTouchPoints > 0 || 125 | (navigator as any).msMaxTouchPoints > 0 126 | ); 127 | setIsTouchDevice(hasTouchScreen); 128 | }; 129 | 130 | detectTouch(); 131 | 132 | // Also listen for touch events to detect mid-session 133 | const touchHandler = () => { 134 | if (!isTouchDevice) { 135 | setIsTouchDevice(true); 136 | window.removeEventListener('touchstart', touchHandler); 137 | } 138 | }; 139 | 140 | window.addEventListener('touchstart', touchHandler); 141 | 142 | return () => { 143 | window.removeEventListener('touchstart', touchHandler); 144 | }; 145 | }, [isTouchDevice]); 146 | 147 | // Show controls on mobile devices or touch devices 148 | useEffect(() => { 149 | setShowControls(isMobileScreen || isTabletScreen || isTouchDevice); 150 | }, [isMobileScreen, isTabletScreen, isTouchDevice]); 151 | 152 | // Don't render anything if we don't need to show controls 153 | if (!showControls) return null; 154 | 155 | return ( 156 |
157 | {/* Forward button */} 158 | 185 | 186 | {/* Turn left button */} 187 | 214 | 215 | {/* Turn right button */} 216 | 243 | 244 | {/* Run button */} 245 | 272 |
273 | ); 274 | }; 275 | 276 | export default MobileControls; -------------------------------------------------------------------------------- /src/components/MobileControlsProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { useMediaQuery } from 'react-responsive'; 3 | 4 | // Redefine MovementState to match CharacterControls 5 | interface MovementState { 6 | forward: boolean; 7 | turnLeft: boolean; 8 | turnRight: boolean; 9 | running: boolean; 10 | } 11 | 12 | // Window interface is now defined globally in src/types/window.d.ts 13 | // declare global { 14 | // interface Window { 15 | // ... removed declarations ... 16 | // } 17 | // } 18 | 19 | // Container styles 20 | const mobileControlsStyle: React.CSSProperties = { 21 | position: 'fixed', 22 | bottom: 0, 23 | left: 0, 24 | right: 0, 25 | display: 'flex', 26 | justifyContent: 'space-between', 27 | alignItems: 'center', 28 | zIndex: 1000, 29 | pointerEvents: 'none', 30 | height: '180px', 31 | padding: '0 20px 20px 20px', 32 | }; 33 | 34 | // Joystick styles 35 | const joystickContainerStyle: React.CSSProperties = { 36 | position: 'relative', 37 | width: '120px', 38 | height: '120px', 39 | borderRadius: '50%', 40 | background: 'rgba(255, 255, 255, 0.15)', 41 | backdropFilter: 'blur(10px)', 42 | border: '2px solid rgba(255, 255, 255, 0.3)', 43 | display: 'flex', 44 | justifyContent: 'center', 45 | alignItems: 'center', 46 | pointerEvents: 'auto', 47 | touchAction: 'none', 48 | }; 49 | 50 | const joystickStyle: React.CSSProperties = { 51 | width: '50px', 52 | height: '50px', 53 | borderRadius: '50%', 54 | background: 'rgba(255, 255, 255, 0.7)', 55 | position: 'absolute', 56 | transition: 'transform 0.1s ease-out', 57 | }; 58 | 59 | const MobileControlsProvider: React.FC = () => { 60 | const isMobileScreen = useMediaQuery({ query: '(max-width: 768px)' }); 61 | const isTabletScreen = useMediaQuery({ query: '(max-width: 1024px)' }); 62 | const [isTouchDevice, setIsTouchDevice] = useState(false); 63 | const [showControls, setShowControls] = useState(false); 64 | const [joystickHidden, setJoystickHidden] = useState(false); 65 | 66 | // Joystick state 67 | const joystickRef = useRef(null); 68 | const knobRef = useRef(null); 69 | const [joystickActive, setJoystickActive] = useState(false); 70 | const [joystickPosition, setJoystickPosition] = useState({ x: 0, y: 0 }); 71 | const [initialTouchPos, setInitialTouchPos] = useState({ x: 0, y: 0 }); 72 | const joystickBounds = useRef({ radius: 35 }); 73 | 74 | // Detect if device has touch capability 75 | useEffect(() => { 76 | const detectTouch = () => { 77 | const hasTouchScreen = ( 78 | 'ontouchstart' in window || 79 | navigator.maxTouchPoints > 0 || 80 | (navigator as any).msMaxTouchPoints > 0 81 | ); 82 | setIsTouchDevice(hasTouchScreen); 83 | }; 84 | 85 | detectTouch(); 86 | 87 | // Also listen for touch events to detect mid-session 88 | const touchHandler = () => { 89 | if (!isTouchDevice) { 90 | setIsTouchDevice(true); 91 | window.removeEventListener('touchstart', touchHandler); 92 | } 93 | }; 94 | 95 | window.addEventListener('touchstart', touchHandler); 96 | 97 | return () => { 98 | window.removeEventListener('touchstart', touchHandler); 99 | }; 100 | }, [isTouchDevice]); 101 | 102 | // Show controls on mobile devices or touch devices 103 | useEffect(() => { 104 | setShowControls(isMobileScreen || isTabletScreen || isTouchDevice); 105 | }, [isMobileScreen, isTabletScreen, isTouchDevice]); 106 | 107 | // Check if joystick should be hidden (when in chatbox, etc.) 108 | useEffect(() => { 109 | const checkJoystickVisibility = () => { 110 | setJoystickHidden(window.hideJoystick === true); 111 | }; 112 | 113 | // Check immediately and set up interval 114 | checkJoystickVisibility(); 115 | const interval = setInterval(checkJoystickVisibility, 100); 116 | 117 | return () => { 118 | clearInterval(interval); 119 | }; 120 | }, []); 121 | 122 | // Movement handlers - updates the window.setCharacterMovement function 123 | const updateMovement = (newState: Partial) => { 124 | if (window.setCharacterMovement && typeof window.setCharacterMovement === 'function') { 125 | window.setCharacterMovement(prevState => ({ 126 | ...prevState, 127 | ...newState, 128 | // Always set running to false on mobile to prevent sprinting 129 | running: false 130 | })); 131 | } 132 | }; 133 | 134 | // Handle joystick touch/drag start 135 | const handleJoystickStart = (e: React.TouchEvent | React.MouseEvent) => { 136 | setJoystickActive(true); 137 | 138 | let clientX: number, clientY: number; 139 | 140 | if ('touches' in e) { 141 | // Touch event 142 | clientX = e.touches[0].clientX; 143 | clientY = e.touches[0].clientY; 144 | } else { 145 | // Mouse event 146 | clientX = e.clientX; 147 | clientY = e.clientY; 148 | 149 | // Add window listeners for mouse events 150 | window.addEventListener('mousemove', handleJoystickMove as any); 151 | window.addEventListener('mouseup', handleJoystickEnd as any); 152 | } 153 | 154 | if (joystickRef.current) { 155 | const rect = joystickRef.current.getBoundingClientRect(); 156 | const centerX = rect.left + rect.width / 2; 157 | const centerY = rect.top + rect.height / 2; 158 | 159 | setInitialTouchPos({ x: centerX, y: centerY }); 160 | setJoystickPosition({ x: 0, y: 0 }); // Reset position 161 | } 162 | }; 163 | 164 | // Handle joystick movement 165 | const handleJoystickMove = (e: React.TouchEvent | React.MouseEvent) => { 166 | if (!joystickActive) return; 167 | 168 | let clientX: number, clientY: number; 169 | 170 | if ('touches' in e) { 171 | // Touch event 172 | clientX = e.touches[0].clientX; 173 | clientY = e.touches[0].clientY; 174 | } else { 175 | // Mouse event 176 | clientX = e.clientX; 177 | clientY = e.clientY; 178 | } 179 | 180 | // Calculate the distance from the initial position 181 | const deltaX = clientX - initialTouchPos.x; 182 | const deltaY = clientY - initialTouchPos.y; 183 | 184 | // Calculate distance from center 185 | const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); 186 | 187 | // Limit the joystick movement to the container bounds 188 | const maxDistance = joystickBounds.current.radius; 189 | const limitedDistance = Math.min(distance, maxDistance); 190 | 191 | // Calculate the angle 192 | const angle = Math.atan2(deltaY, deltaX); 193 | 194 | // Calculate the limited position 195 | const limitedX = limitedDistance * Math.cos(angle); 196 | const limitedY = limitedDistance * Math.sin(angle); 197 | 198 | setJoystickPosition({ x: limitedX, y: limitedY }); 199 | 200 | // Determine movement direction 201 | // - Forward/backward based on Y position 202 | // - Turn left/right based on X position 203 | const forward = limitedY < -10; 204 | const turnLeft = limitedX < -10; 205 | const turnRight = limitedX > 10; 206 | 207 | // Update movement state - running is forced to false in updateMovement 208 | updateMovement({ 209 | forward, 210 | turnLeft, 211 | turnRight 212 | }); 213 | }; 214 | 215 | // Handle joystick release 216 | const handleJoystickEnd = () => { 217 | setJoystickActive(false); 218 | setJoystickPosition({ x: 0, y: 0 }); 219 | 220 | // Reset all movement states 221 | updateMovement({ 222 | forward: false, 223 | turnLeft: false, 224 | turnRight: false 225 | }); 226 | 227 | // Remove window listeners for mouse events 228 | window.removeEventListener('mousemove', handleJoystickMove as any); 229 | window.removeEventListener('mouseup', handleJoystickEnd as any); 230 | }; 231 | 232 | // Don't render anything if we don't need to show controls or if joystick is hidden 233 | if (!showControls || joystickHidden) return null; 234 | 235 | return ( 236 |
237 | {/* Virtual joystick */} 238 |
246 |
253 |
254 |
255 | ); 256 | }; 257 | 258 | export default MobileControlsProvider; -------------------------------------------------------------------------------- /src/components/MultiplayerManager.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useMultiplayer } from '../contexts/MultiplayerContext'; 3 | 4 | interface MultiplayerManagerProps { 5 | visible?: boolean; 6 | } 7 | 8 | const MultiplayerManager: React.FC = ({ visible = true }) => { 9 | const { 10 | isConnected, 11 | connectionStatus, 12 | lastConnectionEvent, 13 | playerCount, 14 | remotePlayersMap, 15 | positionUpdateCount, 16 | appearanceUpdateCount 17 | } = useMultiplayer(); 18 | 19 | // Track appearance update changes for visual feedback 20 | const [lastAppearanceCount, setLastAppearanceCount] = useState(0); 21 | const [showAppearanceUpdate, setShowAppearanceUpdate] = useState(false); 22 | 23 | // Add state for button clicks 24 | const [showColorClicked, setShowColorClicked] = useState(false); 25 | const [showItemClicked, setShowItemClicked] = useState(false); 26 | 27 | // Listen for color and item change events 28 | useEffect(() => { 29 | // Create event listeners for our custom events 30 | const handleColorClick = () => { 31 | setShowColorClicked(true); 32 | setTimeout(() => setShowColorClicked(false), 800); 33 | }; 34 | 35 | const handleItemClick = () => { 36 | setShowItemClicked(true); 37 | setTimeout(() => setShowItemClicked(false), 800); 38 | }; 39 | 40 | // Add event listeners 41 | window.addEventListener('mp:colorChange', handleColorClick); 42 | window.addEventListener('mp:itemChange', handleItemClick); 43 | 44 | // Clean up 45 | return () => { 46 | window.removeEventListener('mp:colorChange', handleColorClick); 47 | window.removeEventListener('mp:itemChange', handleItemClick); 48 | }; 49 | }, []); 50 | 51 | // Flash an indicator when appearance updates come in 52 | useEffect(() => { 53 | if (appearanceUpdateCount > lastAppearanceCount) { 54 | setLastAppearanceCount(appearanceUpdateCount); 55 | setShowAppearanceUpdate(true); 56 | 57 | // Clear the highlight after 2 seconds 58 | const timer = setTimeout(() => { 59 | setShowAppearanceUpdate(false); 60 | }, 2000); 61 | 62 | return () => clearTimeout(timer); 63 | } 64 | }, [appearanceUpdateCount, lastAppearanceCount]); 65 | 66 | if (!visible) return null; 67 | 68 | return ( 69 |
84 | {/* Multiplayer Status UI - Temporarily commented out 85 |
86 | Multiplayer Status:{' '} 87 | 91 | {connectionStatus} 92 | 93 |
94 | 95 | {lastConnectionEvent && ( 96 |
97 | Last Event: {lastConnectionEvent} 98 |
99 | )} 100 | 101 |
102 | Players Online: {playerCount} 103 |
104 | 105 |
106 | Position Updates: {positionUpdateCount} 107 |
108 | 109 |
118 | Appearance Updates: {appearanceUpdateCount} 119 | {showAppearanceUpdate && ✓ New} 120 |
121 | 122 |
123 |
130 | Color Changed 131 |
132 |
139 | Item Changed 140 |
141 |
142 | 143 | {remotePlayersMap.size > 0 && ( 144 |
145 | Remote Players: 146 |
    153 | {Array.from(remotePlayersMap.entries()).map(([id, player]) => ( 154 |
  • 155 |
    {id.slice(0, 6)}... 156 | - Pos: ({player.position.x.toFixed(1)}, {player.position.z.toFixed(1)}) 157 |
    158 |
    159 | {player.moving && 160 | Moving 161 | } 162 | {player.colors && 163 | 167 | Customized 168 | 169 | } 170 |
    171 |
  • 172 | ))} 173 |
174 |
175 | )} 176 | 177 |
178 | {isConnected ? 'Connected to websocket server' : 'Not connected'} 179 |
180 | */} 181 |
182 | ); 183 | }; 184 | 185 | export default MultiplayerManager; -------------------------------------------------------------------------------- /src/components/ProximityDetector.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber'; 2 | import { MutableRefObject, useEffect, useRef, useState } from 'react'; 3 | import * as THREE from 'three'; 4 | 5 | interface ProximityDetectorProps { 6 | target: THREE.Vector3; 7 | characterRef: MutableRefObject; 8 | threshold: number; 9 | onNear: (isNear: boolean) => void; 10 | } 11 | 12 | const ProximityDetector = ({ target, characterRef, threshold, onNear }: ProximityDetectorProps) => { 13 | // Track last known state and position 14 | const lastNearState = useRef(false); 15 | const lastPosition = useRef(new THREE.Vector3()); 16 | const isMoving = useRef(false); 17 | 18 | useFrame(() => { 19 | if (!characterRef.current) return; 20 | 21 | // Get current position 22 | const position = characterRef.current.position; 23 | 24 | // Check if the character is moving 25 | if (lastPosition.current.x !== 0 || lastPosition.current.y !== 0 || lastPosition.current.z !== 0) { 26 | const moveDistance = lastPosition.current.distanceTo(position); 27 | isMoving.current = moveDistance > 0.001; 28 | } 29 | 30 | // Calculate distance to target (shop) 31 | const distance = position.distanceTo(target); 32 | const isNear = distance < threshold; 33 | 34 | // Force update when: 35 | // 1. Near/far state has changed, OR 36 | // 2. Character is moving AND is near the shop (to ensure responsive UI updates while moving) 37 | if (isNear !== lastNearState.current || (isMoving.current && isNear)) { 38 | lastNearState.current = isNear; 39 | onNear(isNear); 40 | } 41 | 42 | // Store current position for next frame 43 | lastPosition.current.copy(position); 44 | }); 45 | 46 | return null; // This is an invisible utility component 47 | }; 48 | 49 | export default ProximityDetector; -------------------------------------------------------------------------------- /src/components/RemoteCharactersManager.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useMultiplayer } from '../contexts/MultiplayerContext'; 3 | import RemoteCharacter from './RemoteCharacter'; 4 | import * as THREE from 'three'; 5 | 6 | const RemoteCharactersManager: React.FC = () => { 7 | const { remotePlayersMap, socket } = useMultiplayer(); 8 | 9 | // Function to fix shared references by forcing deep copies 10 | const fixSharedReferences = (map: Map) => { 11 | console.log('*** ATTEMPTING TO FIX SHARED REFERENCES ***'); 12 | 13 | // Create a completely new map with fresh deep copies 14 | const fixedMap = new Map(); 15 | 16 | Array.from(map.entries()).forEach(([id, player]: [string, any]) => { 17 | // Create completely new deep copies of all data 18 | const newColors = player.colors ? JSON.parse(JSON.stringify(player.colors)) : null; 19 | const newSelected = player.selected ? JSON.parse(JSON.stringify(player.selected)) : null; 20 | 21 | // Create new position vector 22 | const newPosition = new THREE.Vector3( 23 | player.position.x, 24 | player.position.y, 25 | player.position.z 26 | ); 27 | 28 | // Create completely fresh player object 29 | const newPlayer = { 30 | id, 31 | username: player.username, 32 | position: newPosition, 33 | rotation: player.rotation, 34 | moving: player.moving || false, 35 | colors: newColors, 36 | selected: newSelected, 37 | message: player.message ? {...player.message} : undefined 38 | }; 39 | 40 | fixedMap.set(id, newPlayer); 41 | console.log(`Fixed player ${id.slice(0,6)} with new color reference`); 42 | }); 43 | 44 | return fixedMap; 45 | }; 46 | 47 | // Log when remote players change to help with debugging 48 | useEffect(() => { 49 | console.log(`Remote players updated: ${remotePlayersMap.size} players found`); 50 | 51 | if (remotePlayersMap.size > 0) { 52 | console.log('Remote player IDs:', Array.from(remotePlayersMap.keys())); 53 | 54 | // Enhanced debugging: Log each player's colors to check for uniqueness 55 | const players = Array.from(remotePlayersMap.entries()); 56 | console.log('Appearance data dump for debugging:'); 57 | 58 | // Create an object to track all colors for comparison 59 | const colorObjects = new Map(); 60 | 61 | players.forEach(([id, player]) => { 62 | console.log(`Player ${id.slice(0,6)} colors reference: ${player.colors ? JSON.stringify(player.colors.slice(0, 2)) : 'undefined'}`); 63 | 64 | // Track colors array object identity 65 | if (player.colors) { 66 | colorObjects.set(id, player.colors); 67 | } 68 | }); 69 | 70 | // Check for shared references across different players (which would cause the issue) 71 | let sharedReferencesDetected = false; 72 | 73 | if (colorObjects.size > 1) { 74 | console.log('Checking for shared color object references (would indicate a bug):'); 75 | const entries = Array.from(colorObjects.entries()); 76 | 77 | for (let i = 0; i < entries.length; i++) { 78 | for (let j = i + 1; j < entries.length; j++) { 79 | const [id1, colors1] = entries[i]; 80 | const [id2, colors2] = entries[j]; 81 | 82 | const isSharedReference = colors1 === colors2; // Object identity check 83 | console.log(`${id1.slice(0,6)} and ${id2.slice(0,6)} share color reference: ${isSharedReference}`); 84 | 85 | if (isSharedReference) { 86 | sharedReferencesDetected = true; 87 | console.warn('WARNING: Multiple players sharing same color object reference. This will cause color synchronization issues!'); 88 | } 89 | } 90 | } 91 | } 92 | 93 | // If we detect shared references, we could force a refresh of appearances from the server 94 | // This is a last resort fix if our other changes don't work 95 | if (sharedReferencesDetected && socket) { 96 | console.error('CRITICAL: Shared references detected - sending appearance refresh request'); 97 | socket.emit('requestAppearanceRefresh'); 98 | } 99 | } 100 | }, [remotePlayersMap, socket]); 101 | 102 | // Only render if we have remote players 103 | if (remotePlayersMap.size === 0) return null; 104 | 105 | return ( 106 | <> 107 | {Array.from(remotePlayersMap.entries()).map(([id, player]) => ( 108 | 119 | ))} 120 | 121 | ); 122 | }; 123 | 124 | export default RemoteCharactersManager; -------------------------------------------------------------------------------- /src/components/Roads.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as THREE from 'three'; 3 | 4 | const Roads = () => { 5 | // Simple road parameters 6 | const roadColor = '#777777'; // Grey color for roads 7 | const roadHeight = 0.05; // Very thin roads 8 | 9 | return ( 10 | 11 | {/* Road to Shops */} 12 | 17 | {/* Width, height, length */} 18 | 19 | 20 | 21 | {/* Road to Helper */} 22 | 27 | 28 | 29 | 30 | 31 | {/* Road near spawn point */} 32 | 36 | 37 | 38 | 39 | 40 | {/* Additional road to clothing shop */} 41 | 46 | 47 | 48 | 49 | 50 | {/* Additional road to barber shop */} 51 | 56 | 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default Roads; -------------------------------------------------------------------------------- /src/components/RotatingCube.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { useFrame } from '@react-three/fiber'; 3 | import { BoxGeometry, Mesh, MeshBasicMaterial } from 'three'; 4 | 5 | const RotatingCube: React.FC = () => { 6 | const meshRef = useRef(null); 7 | 8 | // Rotate the cube on each frame 9 | useFrame(() => { 10 | if (meshRef.current) { 11 | meshRef.current.rotation.x += 0.01; 12 | meshRef.current.rotation.y += 0.01; 13 | } 14 | }); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default RotatingCube; -------------------------------------------------------------------------------- /src/components/RotatingCubePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Canvas } from '@react-three/fiber'; 3 | import { Link } from 'react-router-dom'; 4 | import RotatingCube from './RotatingCube'; 5 | 6 | const RotatingCubePage: React.FC = () => { 7 | return ( 8 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
27 | 35 | Back to Home 36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default RotatingCubePage; -------------------------------------------------------------------------------- /src/components/ShopCollision.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber'; 2 | import { MutableRefObject, useRef, useEffect } from 'react'; 3 | import * as THREE from 'three'; 4 | 5 | interface ShopCollisionProps { 6 | shopPosition: THREE.Vector3; 7 | characterRef: MutableRefObject; 8 | shopSize: { width: number; height: number; depth: number }; 9 | doorSize: { width: number; height: number }; 10 | doorPosition: { x: number; z: number }; 11 | } 12 | 13 | // This component prevents the character from walking through walls of the shop 14 | // but allows them to enter through the door 15 | const ShopCollision = ({ 16 | shopPosition, 17 | characterRef, 18 | shopSize, 19 | doorSize, 20 | doorPosition 21 | }: ShopCollisionProps) => { 22 | const lastValidPosition = useRef(new THREE.Vector3()); 23 | const collisionInitialized = useRef(false); 24 | 25 | // Calculate shop boundaries with a padding for more reliable collision 26 | const padding = 0.1; // Additional padding to prevent slipping through walls 27 | const characterRadius = 1.0; // Increased character radius to make collision more reliable 28 | 29 | const minX = shopPosition.x - shopSize.width/2 - padding; 30 | const maxX = shopPosition.x + shopSize.width/2 + padding; 31 | const minZ = shopPosition.z - shopSize.depth/2 - padding; 32 | const maxZ = shopPosition.z + shopSize.depth/2 + padding; 33 | 34 | // Door boundaries 35 | const doorMinX = shopPosition.x + doorPosition.x - doorSize.width/2 + padding; 36 | const doorMaxX = shopPosition.x + doorPosition.x + doorSize.width/2 - padding; 37 | const doorZ = shopPosition.z + doorPosition.z; 38 | 39 | // Debug flag - set to true if you want to see collision messages 40 | const debug = false; 41 | 42 | // Initialize the last valid position 43 | useEffect(() => { 44 | if (characterRef.current && !collisionInitialized.current) { 45 | lastValidPosition.current.copy(characterRef.current.position); 46 | collisionInitialized.current = true; 47 | if (debug) console.log("Collision initialized"); 48 | } 49 | }, [characterRef]); 50 | 51 | useFrame(() => { 52 | if (!characterRef.current || !collisionInitialized.current) return; 53 | 54 | const character = characterRef.current; 55 | const position = character.position.clone(); 56 | 57 | // Check collision 58 | const isColliding = isCollidingWithShop(position); 59 | 60 | if (debug && isColliding) { 61 | console.log("Collision detected: ", position); 62 | } 63 | 64 | // If position is valid, store it 65 | if (!isColliding) { 66 | lastValidPosition.current.copy(position); 67 | return; 68 | } 69 | 70 | // If position is inside walls, revert to last valid position 71 | character.position.copy(lastValidPosition.current); 72 | }); 73 | 74 | // Check if a position is colliding with the shop walls 75 | const isCollidingWithShop = (position: THREE.Vector3) => { 76 | // First check if the position is within collision range of the shop 77 | const expanded = { 78 | minX: minX - characterRadius, 79 | maxX: maxX + characterRadius, 80 | minZ: minZ - characterRadius, 81 | maxZ: maxZ + characterRadius 82 | }; 83 | 84 | if (position.x < expanded.minX || position.x > expanded.maxX || 85 | position.z < expanded.minZ || position.z > expanded.maxZ) { 86 | return false; // Not close enough to collide 87 | } 88 | 89 | // Check if the position is inside the house 90 | const isInside = 91 | position.x > minX && position.x < maxX && 92 | position.z > minZ && position.z < maxZ; 93 | 94 | // Check if position is in the doorway 95 | const isInDoorway = 96 | Math.abs(position.z - doorZ) < characterRadius && 97 | position.x >= doorMinX && position.x <= doorMaxX; 98 | 99 | // Check wall collisions 100 | const distanceToFrontWall = Math.abs(position.z - maxZ); 101 | const distanceToBackWall = Math.abs(position.z - minZ); 102 | const distanceToLeftWall = Math.abs(position.x - minX); 103 | const distanceToRightWall = Math.abs(position.x - maxX); 104 | 105 | const collidesWithFrontWall = distanceToFrontWall < characterRadius && 106 | position.x > minX && position.x < maxX && position.z > maxZ; 107 | 108 | const collidesWithBackWall = distanceToBackWall < characterRadius && 109 | position.x > minX && position.x < maxX && position.z < minZ; 110 | 111 | const collidesWithLeftWall = distanceToLeftWall < characterRadius && 112 | position.z > minZ && position.z < maxZ && position.x < minX; 113 | 114 | const collidesWithRightWall = distanceToRightWall < characterRadius && 115 | position.z > minZ && position.z < maxZ && position.x > maxX; 116 | 117 | // If we're in the doorway, no collision 118 | if (isInDoorway) { 119 | return false; 120 | } 121 | 122 | // If completely inside the house (not near walls), no collision 123 | if (isInside && 124 | distanceToFrontWall > characterRadius && 125 | distanceToBackWall > characterRadius && 126 | distanceToLeftWall > characterRadius && 127 | distanceToRightWall > characterRadius) { 128 | return false; 129 | } 130 | 131 | // If we collide with any wall, report collision 132 | if (collidesWithFrontWall || collidesWithBackWall || 133 | collidesWithLeftWall || collidesWithRightWall) { 134 | return true; 135 | } 136 | 137 | // Final check: are we inside the shop walls but not in doorway? 138 | return isInside && !isInDoorway; 139 | }; 140 | 141 | return null; // Invisible utility component 142 | }; 143 | 144 | export default ShopCollision; -------------------------------------------------------------------------------- /src/components/StaticCharacterModel.tsx: -------------------------------------------------------------------------------- 1 | import { Html } from "@react-three/drei"; 2 | import React from "react"; 3 | import Character from "./Character"; 4 | 5 | export default function StaticCharacterModel() { 6 | const defaultColors = [ 7 | { color: '#1F2937' }, // Hair 8 | { color: '#1F2937' }, // Beard 9 | { color: '#15803d' }, // Shirt cuffs - darker green 10 | { color: '#22c55e' }, // Shirt main - bright green 11 | { color: '#1F2937' }, // Pants main 12 | { color: '#374151' }, // Pants bottom 13 | { color: '#111827' }, // Belt 14 | { color: '#111827' }, // Shoes 15 | { color: '#1F2937' }, // Shoes accent 16 | { color: '#000000' }, // Shoes sole 17 | { color: '#000000' }, // Watch 18 | { color: '#000000' } // Hat 19 | ]; 20 | 21 | const staticSelected = { 22 | pose: "pose_pc02", 23 | hair: "hair_1", 24 | face: "default", 25 | glasses: "glasses_1", 26 | logo: null, 27 | hats: null 28 | }; 29 | 30 | return ( 31 | 32 | 38 | 53 | Supabase AI Helper 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useStore } from "../store/store"; 3 | import classNames from "classnames"; 4 | 5 | import IconSun from "../assets/icons/IconSun"; 6 | import IconMoon from "../assets/icons/IconMoon"; 7 | 8 | type Props = Record; 9 | 10 | const ThemeToggle: React.FC = () => { 11 | const theme = useStore((state) => state.theme); 12 | const setTheme = useStore((state) => state.setTheme); 13 | 14 | return ( 15 |
24 | 40 | 56 |
57 | ); 58 | }; 59 | 60 | export default ThemeToggle; 61 | -------------------------------------------------------------------------------- /src/components/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from "react"; 2 | import { useStore } from "../store/store"; 3 | import classNames from "classnames"; 4 | 5 | type SubTool = { 6 | id: string; 7 | icon: React.FC>; 8 | }; 9 | 10 | type Tool = { 11 | id: string; 12 | label: string; 13 | icon: React.FC>; 14 | items: SubTool[]; 15 | }; 16 | 17 | type Props = { 18 | toolId: string; 19 | items: Tool[]; 20 | onClickItem: (tool: Tool) => void; 21 | }; 22 | 23 | const Toolbar: React.FC = ({ toolId, items, onClickItem }) => { 24 | const theme = useStore((state) => state.theme); 25 | 26 | return ( 27 |
28 | {items.map((tool) => { 29 | const isActive = toolId === tool.id; 30 | const Icon = tool.icon; 31 | 32 | return ( 33 |
34 | {isActive && ( 35 |

44 | {tool.label} 45 |

46 | )} 47 | 65 |
66 | ); 67 | })} 68 |
69 | ); 70 | }; 71 | 72 | export default Toolbar; 73 | -------------------------------------------------------------------------------- /src/components/ViewMode.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useStore } from "../store/store"; 3 | import classNames from "classnames"; 4 | 5 | import IconCamera from "../assets/icons/IconCamera"; 6 | 7 | type Mode = "front" | "side" | "close_up" | "free" | "third_person"; 8 | 9 | type Props = { 10 | mode: Mode; 11 | onClickMode: (mode: Mode) => void; 12 | }; 13 | 14 | const ViewMode: React.FC = ({ mode, onClickMode }) => { 15 | const theme = useStore((state) => state.theme); 16 | 17 | return ( 18 |
19 |
28 | 49 | 70 | 91 | 112 |
113 | 114 |
115 |
116 |

124 | Front 125 |

126 |
127 |
128 |

136 | Side 137 |

138 |
139 |
140 |

148 | Close Up 149 |

150 |
151 |
152 |

160 | Walk Mode 161 |

162 |
163 |
164 |
165 | ); 166 | }; 167 | 168 | export default ViewMode; 169 | -------------------------------------------------------------------------------- /src/helpers/data.ts: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | import IconPose from "../assets/icons/IconPose"; 4 | import IconColor from "../assets/icons/IconColor"; 5 | import IconFace from "../assets/icons/IconFace"; 6 | import IconImage from "../assets/icons/IconImage"; 7 | import IconNo from "../assets/icons/IconNo"; 8 | import IconBeard from "../assets/icons/IconBeard"; 9 | import IconGlasses from "../assets/icons/IconGlasses"; 10 | import IconPose1 from "../assets/icons/IconPose1"; 11 | import IconHair1 from "../assets/icons/IconHair1"; 12 | import IconHair2 from "../assets/icons/IconHair2"; 13 | import IconHair3 from "../assets/icons/IconHair3"; 14 | import IconHair4 from "../assets/icons/IconHair4"; 15 | import IconBeard1 from "../assets/icons/IconBeard1"; 16 | import IconBeard2 from "../assets/icons/IconBeard2"; 17 | import IconBeard3 from "../assets/icons/IconBeard3"; 18 | import IconBeard4 from "../assets/icons/IconBeard4"; 19 | import IconLight from "../assets/icons/IconLight"; 20 | import IconAddImage from "../assets/icons/IconAddImage"; 21 | import IconPose2 from "../assets/icons/IconPose2"; 22 | import IconPose3 from "../assets/icons/IconPose3"; 23 | import IconPose4 from "../assets/icons/IconPose4"; 24 | import IconPose5 from "../assets/icons/IconPose5"; 25 | import IconPose6 from "../assets/icons/IconPose6"; 26 | import IconPose7 from "../assets/icons/IconPose7"; 27 | import IconPose8 from "../assets/icons/IconPose8"; 28 | import IconPose9 from "../assets/icons/IconPose9"; 29 | import IconPose10 from "../assets/icons/IconPose10"; 30 | import IconPose11 from "../assets/icons/IconPose11"; 31 | import IconPose12 from "../assets/icons/IconPose12"; 32 | import IconPose13 from "../assets/icons/IconPose13"; 33 | import IconPose14 from "../assets/icons/IconPose14"; 34 | import IconPose15 from "../assets/icons/IconPose15"; 35 | import IconPose16 from "../assets/icons/IconPose16"; 36 | import IconPose17 from "../assets/icons/IconPose17"; 37 | import IconPose18 from "../assets/icons/IconPose18"; 38 | import IconPose19 from "../assets/icons/IconPose19"; 39 | import IconPose20 from "../assets/icons/IconPose20"; 40 | import IconT1 from "../assets/icons/IconT1"; 41 | import IconT2 from "../assets/icons/IconT2"; 42 | import IconPants1 from "../assets/icons/IconPants1"; 43 | import IconPants2 from "../assets/icons/IconPants2"; 44 | import IconPants3 from "../assets/icons/IconPants3"; 45 | import IconShoes1 from "../assets/icons/IconShoes1"; 46 | import IconShoes2 from "../assets/icons/IconShoes2"; 47 | import IconShoes3 from "../assets/icons/IconShoes3"; 48 | import IconWatch from "../assets/icons/IconWatch"; 49 | import IconHats from "../assets/icons/IconHats"; 50 | import IconPose0 from "../assets/icons/IconPose0"; 51 | import IconGlasses1 from "../assets/icons/IconGlasses2"; 52 | 53 | // Type definition for ToolItem 54 | export type ToolItem = { 55 | id: string; 56 | icon: React.FC>; 57 | name?: string; 58 | color?: string; 59 | }; 60 | 61 | // Type definition for Tool 62 | export type Tool = { 63 | id: string; 64 | icon: React.FC>; 65 | items: ToolItem[]; 66 | title: string; 67 | }; 68 | 69 | export const getToolbarData = (): Tool[] => { 70 | return [ 71 | { 72 | id: "pose", 73 | title: "Poses", 74 | icon: IconPose, 75 | items: [ 76 | { 77 | id: `Default`, 78 | name: "Default", 79 | icon: IconPose0, 80 | }, 81 | { 82 | id: `pose_crossed_arm`, 83 | name: "CrossedArm", 84 | icon: IconPose1, 85 | }, 86 | { 87 | id: `pose_confident`, 88 | name: "Confident", 89 | icon: IconPose2, 90 | }, 91 | { 92 | id: `pose_character_stop`, 93 | name: "CharacterStop", 94 | icon: IconPose3, 95 | }, 96 | { 97 | id: `pose_confused`, 98 | name: "Confused", 99 | icon: IconPose4, 100 | }, 101 | 102 | { 103 | id: `pose_happy_open_arm`, 104 | name: "HappyOpenArm", 105 | icon: IconPose6, 106 | }, 107 | { 108 | id: `pose_jump_happy`, 109 | name: "JumpHappy", 110 | icon: IconPose5, 111 | }, 112 | { 113 | id: `pose_on_phone`, 114 | name: "OnPhone", 115 | icon: IconPose7, 116 | }, 117 | { 118 | id: `pose_pc01`, 119 | name: "PC01", 120 | icon: IconPose8, 121 | }, 122 | { 123 | id: `pose_pc02`, 124 | name: "PC02", 125 | icon: IconPose9, 126 | }, 127 | { 128 | id: `pose_pointing_down`, 129 | name: "PointingDown", 130 | icon: IconPose10, 131 | }, 132 | { 133 | id: `pose_pointing_left`, 134 | name: "PointingLeft", 135 | icon: IconPose11, 136 | }, 137 | { 138 | id: `pose_pointing_right`, 139 | name: "PointingRight", 140 | icon: IconPose12, 141 | }, 142 | { 143 | id: `pose_pointing_up`, 144 | name: "PointingUp", 145 | icon: IconPose13, 146 | }, 147 | { 148 | id: `pose_sitting_happy`, 149 | name: "SittingHappy", 150 | icon: IconPose14, 151 | }, 152 | { 153 | id: `pose_sitting_sad`, 154 | name: "SittingSad", 155 | icon: IconPose15, 156 | }, 157 | { 158 | id: `pose_standing1`, 159 | name: "Standing1", 160 | icon: IconPose16, 161 | }, 162 | { 163 | id: `pose_standing_sad`, 164 | name: "StandingSad", 165 | icon: IconPose17, 166 | }, 167 | { 168 | id: `pose_standing_thinking`, 169 | name: "StandingThinking", 170 | icon: IconPose18, 171 | }, 172 | { 173 | id: `pose_waving`, 174 | name: "Waving", 175 | icon: IconPose19, 176 | }, 177 | { 178 | id: `pose_welcome`, 179 | name: "Welcome", 180 | icon: IconPose20, 181 | }, 182 | ] 183 | }, 184 | 185 | { 186 | id: "tool_2", 187 | title: "Colors", 188 | icon: IconColor, 189 | items: [ 190 | { 191 | id: `tool_2_item_1`, 192 | icon: IconHair1, 193 | }, 194 | { 195 | id: `tool_2_item_2`, 196 | icon: IconBeard, 197 | }, 198 | { 199 | id: `tool_2_item_3`, 200 | icon: IconT1, 201 | }, 202 | { 203 | id: `tool_2_item_4`, 204 | icon: IconT2, 205 | }, 206 | { 207 | id: `tool_2_item_5`, 208 | icon: IconPants1, 209 | }, 210 | ] 211 | }, 212 | 213 | 214 | { 215 | id: "hair", 216 | title: "Hair", 217 | icon: IconHair1, 218 | items: [ 219 | { 220 | id: `hair_none`, 221 | icon: IconNo, 222 | color: "green", 223 | name: "None" 224 | }, 225 | { 226 | id: `hair_1`, 227 | icon: IconHair1, 228 | color: "green", 229 | name: "GEO_Hair_01" 230 | }, 231 | { 232 | id: `hair_2`, 233 | icon: IconHair2, 234 | color: "green", 235 | name: "GEO_Hair_02" 236 | }, 237 | { 238 | id: `hair_3`, 239 | icon: IconHair3, 240 | color: "green", 241 | name: "GEO_Hair_03" 242 | }, 243 | { 244 | id: `hair_4`, 245 | icon: IconHair4, 246 | color: "green", 247 | name: "GEO_Hair_04" 248 | }, 249 | ], 250 | }, 251 | 252 | { 253 | id: "beard", 254 | title: "Beard", 255 | icon: IconBeard1, 256 | items: [ 257 | { 258 | 259 | id: `beard_none`, 260 | icon: IconNo, 261 | color: "green", 262 | name: "None" 263 | }, 264 | { 265 | 266 | id: `beard_1`, 267 | icon: IconBeard1, 268 | color: "green", 269 | name: "GEO_Beard_01" 270 | }, 271 | { 272 | id: `beard_2`, 273 | icon: IconBeard2, 274 | color: "green", 275 | name: "GEO_Beard_02" 276 | }, 277 | { 278 | id: `beard_3`, 279 | icon: IconBeard3, 280 | color: "green", 281 | name: "GEO_Beard_03" 282 | }, 283 | { 284 | id: `beard_4`, 285 | icon: IconBeard4, 286 | color: "green", 287 | name: "GEO_Beard_04" 288 | }, 289 | ], 290 | }, 291 | 292 | { 293 | id: "face", 294 | title: "Face", 295 | icon: IconFace, 296 | items: [ 297 | { 298 | id: `default`, 299 | icon: IconFace, 300 | name: "default" 301 | }, 302 | { 303 | id: `round`, 304 | icon: IconFace, 305 | name: "round" 306 | }, 307 | { 308 | id: `square`, 309 | icon: IconFace, 310 | name: "square" 311 | }, 312 | 313 | ] 314 | }, 315 | { 316 | id: "glasses", 317 | title: "Glasses", 318 | icon: IconGlasses, 319 | items: 320 | [{ 321 | id: `glasses_none`, 322 | icon: IconNo, 323 | name: "glasses_none" 324 | }, 325 | { 326 | id: "glasses_1", 327 | icon: IconGlasses, 328 | name: "glasses_1" 329 | }, 330 | { 331 | id: "glasses_2", 332 | icon: IconGlasses, 333 | name: "glasses_2" 334 | }, 335 | { 336 | id: "glasses_3", 337 | icon: IconGlasses1, 338 | name: "glasses_3" 339 | }, 340 | { 341 | id: "glasses_4", 342 | icon: IconGlasses1, 343 | name: "glasses_4" 344 | }, 345 | ] 346 | }, 347 | { 348 | id: "lights", 349 | title: "Lights", 350 | icon: IconLight, 351 | items: 352 | [ 353 | { 354 | 355 | id: "lights_0", 356 | icon: IconNo, 357 | name: "none", 358 | }, 359 | { 360 | 361 | id: "lights_1", 362 | icon: IconLight, 363 | name: "lights_1", 364 | }, 365 | { 366 | id: "lights_2", 367 | icon: IconLight, 368 | name: "lights_2", 369 | }, 370 | ] 371 | }, 372 | { 373 | id: "hats", 374 | title: "Hats", 375 | icon: IconHats, 376 | items: 377 | [{ 378 | id: "hat_none", 379 | icon: IconNo, 380 | name: "logo_none" 381 | }, 382 | { 383 | id: "hat_1", 384 | icon: IconHats, 385 | name: "logo_1", 386 | }, 387 | 388 | ] 389 | }, 390 | { 391 | id: "logo", 392 | title: "Logo", 393 | icon: IconImage, 394 | items: 395 | [{ 396 | id: "logo_none", 397 | icon: IconNo, 398 | name: "logo_none" 399 | }, 400 | { 401 | id: "logo_1", 402 | icon: IconImage, 403 | name: "logo_1", 404 | }, 405 | { 406 | id: "logo_upload", 407 | icon: IconAddImage, 408 | name: "logo_upload", 409 | }, 410 | ] 411 | }, 412 | ]; 413 | }; 414 | -------------------------------------------------------------------------------- /src/hooks/useMultiplayer.tsx: -------------------------------------------------------------------------------- 1 | // Existing listeners... 2 | socket.on('userMoved', (data: { id: string; position: any; rotation: number; moving: boolean }) => { 3 | setRemotePlayers(prev => ( 4 | prev.map(p => p.id === data.id ? { ...p, position: data.position, rotation: data.rotation, moving: data.moving } : p) 5 | )); 6 | }); 7 | 8 | // --- NEW LISTENER --- 9 | socket.on('userAppearanceChanged', (data: { id: string; colors: any[]; selected: any }) => { 10 | console.log(`[Socket] Appearance update received for ${data.id.slice(0,6)}`); 11 | setRemotePlayers(prev => ( 12 | prev.map(p => 13 | p.id === data.id 14 | ? { ...p, colors: data.colors, selected: data.selected } 15 | : p 16 | ) 17 | )); 18 | }); 19 | // --- END NEW LISTENER --- 20 | 21 | socket.on('userLeft', (id: string) => { 22 | console.log('[Socket] User left:', id); 23 | setRemotePlayers(prev => prev.filter(p => p.id !== id)); 24 | }); -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .scroll-light::-webkit-scrollbar, 6 | .scroll-dark::-webkit-scrollbar { 7 | @apply w-1; 8 | } 9 | 10 | .scroll-light::-webkit-scrollbar-track { 11 | @apply bg-neutral-10; 12 | } 13 | 14 | .scroll-light::-webkit-scrollbar-thumb { 15 | @apply rounded-full bg-neutral-30; 16 | } 17 | 18 | .scroll-dark::-webkit-scrollbar-track { 19 | @apply bg-[#2A2B2F]; 20 | } 21 | 22 | .scroll-dark::-webkit-scrollbar-thumb { 23 | @apply rounded-full bg-neutral-80; 24 | } 25 | 26 | .progress-bar { 27 | transition-property: width; 28 | transition-duration: 0.3s; 29 | transition-timing-function: ease-in-out; 30 | } 31 | 32 | .progress-marker-transition { 33 | transition-property: left; 34 | transition-duration: 0.3s; 35 | transition-timing-function: ease-in-out; 36 | } 37 | 38 | /* Hide scrollbars completely */ 39 | .scrollbar-hidden::-webkit-scrollbar { 40 | display: none; 41 | } 42 | 43 | .scrollbar-hidden { 44 | -ms-overflow-style: none; /* IE and Edge */ 45 | scrollbar-width: none; /* Firefox */ 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { BrowserRouter, Routes, Route } from 'react-router-dom' 4 | import App from './App.tsx' 5 | import RotatingCubePage from './components/RotatingCubePage.tsx' 6 | import './index.css' 7 | 8 | ReactDOM.createRoot(document.getElementById('root')!).render( 9 | 10 | 11 | 12 | } /> 13 | } /> 14 | 15 | 16 | , 17 | ) 18 | -------------------------------------------------------------------------------- /src/store/slices/themeSlice.ts: -------------------------------------------------------------------------------- 1 | import { StoreSlice } from "../types"; 2 | 3 | type Theme = "light" | "dark"; 4 | 5 | type Slice = { 6 | theme: Theme; 7 | setTheme: (theme: Theme) => void; 8 | }; 9 | 10 | const createThemeSlice: StoreSlice = (set) => ({ 11 | theme: "dark", 12 | setTheme: (theme) => set({ theme }), 13 | }); 14 | 15 | export default createThemeSlice; 16 | -------------------------------------------------------------------------------- /src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { createJSONStorage, persist } from "zustand/middleware"; 3 | 4 | import createThemeSlice from "./slices/themeSlice"; 5 | 6 | const createRootSlice = (set, get) => ({ 7 | ...createThemeSlice(set, get), 8 | }); 9 | 10 | export const useStore = create( 11 | persist(createRootSlice, { 12 | name: "storage", 13 | storage: createJSONStorage(() => localStorage), 14 | partialize: (state) => ({ 15 | theme: state.theme, 16 | }), 17 | }) 18 | ); 19 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | import type { GetState, SetState } from "zustand"; 2 | 3 | export type StoreSlice< 4 | T extends Record, 5 | E extends Record = T 6 | > = ( 7 | set: SetState, 8 | get: GetState 9 | ) => T; 10 | -------------------------------------------------------------------------------- /src/types/window.d.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import React from 'react'; 3 | import { MovementState } from '../components/CharacterControls'; // Assuming MovementState is exported 4 | 5 | // Define a comprehensive interface for all custom window properties 6 | interface CustomWindow { 7 | // Chat visibility functions 8 | forceHideGameChat?: boolean; 9 | hideGameMessaging?: boolean; 10 | hideJoystick?: boolean; 11 | gameChatConfig?: any; 12 | chatboxOpen?: boolean; 13 | inChatTransition?: boolean; 14 | directlyHideChatUI?: (hide: boolean) => void; 15 | 16 | // Camera and UI config 17 | cameraConfig?: any; 18 | helperUIConfig?: any; 19 | isColorPickerDragging?: boolean; 20 | 21 | // Helper character interaction functions 22 | startHelperInteraction?: () => void; 23 | startClothingShopInteraction?: () => void; 24 | startBarberShopInteraction?: () => void; 25 | endCharacterInteraction?: () => void; 26 | helperAnimations?: { idle: any; talk: any }; 27 | animationTransitionTimeout?: number; 28 | 29 | // Customization state 30 | isCustomizingClothing?: boolean; 31 | 32 | // Character control functions 33 | setCharacterPose?: (pose: string) => void; 34 | setCharacterMovement?: React.Dispatch>; 35 | chatboxFocused?: boolean; 36 | characterRef?: React.MutableRefObject; 37 | 38 | // Responsive sizing functions 39 | calculateResponsivePositions?: () => void; 40 | helperUIPositionListenerAdded?: boolean; 41 | chatResizeListenerAdded?: boolean; 42 | 43 | // Debugging 44 | logAnimStates?: () => string; 45 | } 46 | 47 | // Extend the global Window interface 48 | declare global { 49 | interface Window extends CustomWindow {} 50 | } 51 | 52 | // Export an empty object to make this a module (avoids isolatedModules error) 53 | export {}; -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: "#4B50EC", 8 | neutral: { 9 | 10: "#f8f9fb", 10 | 20: "#f1f5f9", 11 | 30: "#e2e8f0", 12 | 40: "#cbd5e1", 13 | 50: "#94a3b8", 14 | 60: "#64748b", 15 | 70: "#475569", 16 | 80: "#334155", 17 | 90: "#1e293b", 18 | 100: "#1c1d22", 19 | }, 20 | }, 21 | }, 22 | }, 23 | plugins: [], 24 | }; 25 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket Test 5 | 6 | 7 | 8 |

WebSocket Test

9 |
Connecting...
10 |
11 | 12 | 13 |
14 |
15 | 16 | 44 | 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": [ 26 | "src" 27 | ], 28 | "exclude": [ 29 | "src" 30 | ], 31 | "references": [ 32 | { 33 | "path": "./tsconfig.node.json" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------