├── .gitignore
├── LICENSE.md
├── README.md
└── www
├── .gitignore
├── .nvmrc
├── _redirects
├── package.json
├── public
├── Demon.glb
├── Knight_Golden_Male.glb
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
├── wall.gltf.glb
└── wallCorner.gltf.glb
├── src
├── 3d
│ ├── components
│ │ ├── Floor
│ │ │ └── Floor.tsx
│ │ ├── WallCornerMesh
│ │ │ └── WallCornerMesh.tsx
│ │ └── WallMesh
│ │ │ └── WallMesh.tsx
│ └── models
│ │ ├── Demon
│ │ └── Demon.tsx
│ │ ├── Knight
│ │ └── Knight.tsx
│ │ ├── Wall
│ │ └── Wall.tsx
│ │ └── WallCorner
│ │ └── WallCorner.tsx
├── App.css
├── App.test.tsx
├── App.tsx
├── custom.d.ts
├── game
│ └── components
│ │ ├── Camera
│ │ └── Camera.tsx
│ │ ├── DevMenu
│ │ └── DevMenu.tsx
│ │ ├── Game
│ │ ├── Game.tsx
│ │ └── components
│ │ │ ├── AttackColliders
│ │ │ ├── AttackColliders.tsx
│ │ │ └── components
│ │ │ │ └── AttackCollider
│ │ │ │ └── AttackCollider.tsx
│ │ │ ├── AttackUIContainer
│ │ │ ├── AttackUIContainer.tsx
│ │ │ └── components
│ │ │ │ └── AttackUI
│ │ │ │ └── AttackUI.tsx
│ │ │ ├── GameAI
│ │ │ ├── GameAI.tsx
│ │ │ └── hooks
│ │ │ │ └── ai.ts
│ │ │ └── GameUI
│ │ │ ├── GameUI.tsx
│ │ │ └── components
│ │ │ └── StatsUI
│ │ │ ├── Health
│ │ │ └── Health.tsx
│ │ │ ├── Juice
│ │ │ └── Juice.tsx
│ │ │ └── StatsUI.tsx
│ │ ├── Joystick
│ │ └── Joystick.tsx
│ │ ├── Lights
│ │ └── Lights.tsx
│ │ ├── Mob
│ │ ├── Mob.tsx
│ │ ├── components
│ │ │ ├── MobPhysics
│ │ │ │ └── MobPhysics.tsx
│ │ │ ├── MobTargetTracking
│ │ │ │ └── MobTargetTracking.tsx
│ │ │ ├── MobUI
│ │ │ │ └── MobUI.tsx
│ │ │ └── MobVisuals
│ │ │ │ └── MobVisuals.tsx
│ │ ├── data.ts
│ │ └── hooks
│ │ │ └── brain.ts
│ │ ├── MobsManager
│ │ └── MobsManager.tsx
│ │ ├── PhysWall
│ │ └── PhysWall.tsx
│ │ ├── Player
│ │ ├── Player.tsx
│ │ ├── components
│ │ │ ├── PlayerDebug
│ │ │ │ └── PlayerDebug.tsx
│ │ │ ├── PlayerUI
│ │ │ │ └── PlayerUI.tsx
│ │ │ └── PlayerVisuals
│ │ │ │ └── PlayerVisuals.tsx
│ │ └── hooks
│ │ │ ├── attack.ts
│ │ │ ├── camera.ts
│ │ │ ├── collisions.ts
│ │ │ ├── controls.ts
│ │ │ ├── effects.ts
│ │ │ ├── physics.ts
│ │ │ └── state.ts
│ │ ├── Room
│ │ ├── Room.tsx
│ │ └── components
│ │ │ └── RoomWall
│ │ │ └── RoomWall.tsx
│ │ └── TestBox
│ │ └── TestBox.tsx
├── index.css
├── index.tsx
├── logo.svg
├── mobs
│ └── components
│ │ └── LargeMob
│ │ └── LargeMob.tsx
├── physics
│ ├── bodies.ts
│ ├── cache.ts
│ ├── collisions
│ │ ├── collisions.ts
│ │ ├── data.ts
│ │ ├── filters.ts
│ │ └── types.ts
│ ├── components
│ │ └── Physics
│ │ │ ├── Physics.tsx
│ │ │ ├── data.ts
│ │ │ ├── hooks.ts
│ │ │ └── worker.ts
│ └── world.ts
├── react-app-env.d.ts
├── reportWebVitals.ts
├── setupTests.ts
├── shared.ts
├── state
│ ├── dev.ts
│ ├── inputs.ts
│ ├── mobs.ts
│ ├── player.ts
│ ├── positions.ts
│ └── refs.ts
├── temp
│ ├── ai.ts
│ └── rooms.ts
├── ui
│ ├── colors.ts
│ └── global.ts
├── utils
│ ├── angles.ts
│ ├── color.ts
│ ├── common.ts
│ ├── models.ts
│ ├── numbers.ts
│ └── responsive.ts
└── workers
│ └── physics
│ ├── functions.ts
│ ├── physicsWorker.ts
│ └── types.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Simon Hales
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Warning
2 |
3 | This is not an example of well written code, especially with some of my more recent commits. Currently I'm in a quick and hacky phase whilst I think about and learn some new elements of game development. Eventually I will be refactoring to make everything better.
4 |
5 | Demo https://example-react-three-game.netlify.app/
6 |
7 | 3d character models by [@quaternius](https://twitter.com/quaternius)
8 |
9 | Other 3d models by [@KayLousberg](https://twitter.com/KayLousberg)
10 |
11 | 2d physics https://piqnt.com/planck.js/
12 |
13 | Physics implementation based upon https://github.com/pmndrs/use-cannon
--------------------------------------------------------------------------------
/www/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .idea
--------------------------------------------------------------------------------
/www/.nvmrc:
--------------------------------------------------------------------------------
1 | 14.15.0
--------------------------------------------------------------------------------
/www/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
2 |
--------------------------------------------------------------------------------
/www/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "www",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@react-hook/window-size": "^3.0.7",
7 | "@react-three/drei": "^2.2.7",
8 | "@testing-library/jest-dom": "^5.11.4",
9 | "@testing-library/react": "^11.1.0",
10 | "@testing-library/user-event": "^12.1.10",
11 | "@types/jest": "^26.0.15",
12 | "@types/node": "^12.0.0",
13 | "@types/react": "^16.9.53",
14 | "@types/react-dom": "^16.9.8",
15 | "@types/styled-components": "^5.1.4",
16 | "hotkeys-js": "^3.8.1",
17 | "nipplejs": "simonghales/nipplejs",
18 | "planck-js": "^0.3.22",
19 | "react": "^17.0.1",
20 | "react-dom": "^17.0.1",
21 | "react-full-screen": "^0.3.1",
22 | "react-hooks-use-previous": "^1.1.1",
23 | "react-icons": "^3.11.0",
24 | "react-scripts": "4.0.0",
25 | "react-three-fiber": "^5.3.0",
26 | "styled-components": "^5.2.1",
27 | "styled-reset": "^4.3.1",
28 | "three": "^0.122.0",
29 | "typescript": "^4.0.3",
30 | "valtio": "^0.4.3",
31 | "web-vitals": "^0.2.4",
32 | "worker-loader": "^3.0.5"
33 | },
34 | "scripts": {
35 | "start": "react-scripts start",
36 | "build": "CI='' GENERATE_SOURCEMAP=false react-scripts build && cp _redirects build",
37 | "test": "react-scripts test",
38 | "eject": "react-scripts eject"
39 | },
40 | "eslintConfig": {
41 | "extends": [
42 | "react-app",
43 | "react-app/jest"
44 | ]
45 | },
46 | "browserslist": {
47 | "production": [
48 | ">0.2%",
49 | "not dead",
50 | "not op_mini all"
51 | ],
52 | "development": [
53 | "last 1 chrome version",
54 | "last 1 firefox version",
55 | "last 1 safari version"
56 | ]
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/www/public/Demon.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simonghales/example-react-three-game/ac4e4b848d3ce84d909a11d88c2d42354ffbd849/www/public/Demon.glb
--------------------------------------------------------------------------------
/www/public/Knight_Golden_Male.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simonghales/example-react-three-game/ac4e4b848d3ce84d909a11d88c2d42354ffbd849/www/public/Knight_Golden_Male.glb
--------------------------------------------------------------------------------
/www/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simonghales/example-react-three-game/ac4e4b848d3ce84d909a11d88c2d42354ffbd849/www/public/favicon.ico
--------------------------------------------------------------------------------
/www/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 | React App
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/www/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simonghales/example-react-three-game/ac4e4b848d3ce84d909a11d88c2d42354ffbd849/www/public/logo192.png
--------------------------------------------------------------------------------
/www/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simonghales/example-react-three-game/ac4e4b848d3ce84d909a11d88c2d42354ffbd849/www/public/logo512.png
--------------------------------------------------------------------------------
/www/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/www/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/www/public/wall.gltf.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simonghales/example-react-three-game/ac4e4b848d3ce84d909a11d88c2d42354ffbd849/www/public/wall.gltf.glb
--------------------------------------------------------------------------------
/www/public/wallCorner.gltf.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simonghales/example-react-three-game/ac4e4b848d3ce84d909a11d88c2d42354ffbd849/www/public/wallCorner.gltf.glb
--------------------------------------------------------------------------------
/www/src/3d/components/Floor/Floor.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Plane} from "@react-three/drei";
3 | import {radians} from "../../../utils/angles";
4 |
5 | const size = 100
6 |
7 | const floorColor = '#2f313c'
8 |
9 | const Floor: React.FC = () => {
10 | return (
11 | <>
12 |
13 |
14 |
15 | {/**/}
16 | >
17 | );
18 | };
19 |
20 | export default Floor;
--------------------------------------------------------------------------------
/www/src/3d/components/WallCornerMesh/WallCornerMesh.tsx:
--------------------------------------------------------------------------------
1 | import React, {Suspense} from "react";
2 | import Wall from "../../models/Wall/Wall";
3 | import {Box} from "@react-three/drei";
4 | import WallCorner from "../../models/WallCorner/WallCorner";
5 | import {radians} from "../../../utils/angles";
6 |
7 | const WallCornerMesh: React.FC = ({rotation = [0, 0, 0], ...props}) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default WallCornerMesh;
--------------------------------------------------------------------------------
/www/src/3d/components/WallMesh/WallMesh.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@react-three/drei";
2 | import React, {Suspense} from "react";
3 | import Wall from "../../models/Wall/Wall";
4 |
5 | const WallMesh: React.FC = (props) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | };
17 |
18 | export default WallMesh;
--------------------------------------------------------------------------------
/www/src/3d/models/Demon/Demon.tsx:
--------------------------------------------------------------------------------
1 | /*
auto-generated by: https://github.com/pmndrs/gltfjsx
*/
import React, {useRef, useState, useEffect} from 'react'
import {useFrame} from 'react-three-fiber'
import {useGLTF} from '@react-three/drei/useGLTF'
import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader'
import {
AnimationAction,
AnimationMixer,
Bone, Group,
LoopOnce,
MeshStandardMaterial,
MeshToonMaterial,
SkinnedMesh
} from "three";
import {hexStringToCode} from "../../../utils/color";
import {SkeletonUtils} from "three/examples/jsm/utils/SkeletonUtils";
import {setMaterials, setShadows} from "../../../utils/models";
import {Event} from "three/src/core/EventDispatcher";
type GLTFResult = GLTF & {
nodes: {
Demon001: SkinnedMesh
Body: Bone
Head: Bone
}
materials: {
Texture: MeshStandardMaterial
}
}
type ActionName =
| 'Bite_Front'
| 'Bite_InPlace'
| 'Dance'
| 'Death'
| 'HitRecieve'
| 'Idle'
| 'Jump'
| 'No'
| 'Walk'
| 'Yes'
type GLTFActions = Record
const lightOrangeIndividualMaterial = new MeshToonMaterial({
color: hexStringToCode("#630721"),
skinning: true,
});
lightOrangeIndividualMaterial.color.convertSRGBToLinear();
export default function Demon({isDead, lastHit, lastAttacked, ...props}: JSX.IntrinsicElements['group'] & {
isDead: boolean,
lastHit: number,
lastAttacked: number,
}) {
const group = useRef()
const {nodes, materials, animations, scene} = useGLTF('/Demon.glb') as GLTFResult
const [geometry]: any = useState(() => {
const clonedScene = SkeletonUtils.clone(scene)
setMaterials(clonedScene, {
Texture: lightOrangeIndividualMaterial
})
setShadows(clonedScene)
return clonedScene
})
const currentAnimationRef = useRef<{
key: string | null,
animation: any,
finished: boolean,
}>({
key: null,
animation: null,
finished: false,
})
const actions = useRef()
const [mixer] = useState(() => new AnimationMixer(nodes.Demon001))
useFrame((state, delta) => mixer.update(delta))
useEffect(() => {
actions.current = {
Bite_Front: mixer.clipAction(animations[0], group.current),
Bite_InPlace: mixer.clipAction(animations[1], group.current),
Dance: mixer.clipAction(animations[2], group.current),
Death: mixer.clipAction(animations[3], group.current),
HitRecieve: mixer.clipAction(animations[4], group.current),
Idle: mixer.clipAction(animations[5], group.current),
Jump: mixer.clipAction(animations[6], group.current),
No: mixer.clipAction(animations[7], group.current),
Walk: mixer.clipAction(animations[8], group.current),
Yes: mixer.clipAction(animations[9], group.current),
}
actions.current.Death.loop = LoopOnce
actions.current.Death.clampWhenFinished = true
actions.current.HitRecieve.loop = LoopOnce
actions.current.HitRecieve.clampWhenFinished = true
actions.current.Bite_Front.loop = LoopOnce
actions.current.Bite_Front.clampWhenFinished = true
return () => animations.forEach((clip) => mixer.uncacheClip(clip))
}, [])
useEffect(() => {
let unsubscribe = () => {}
const calculateAnimation = () => {
if (!actions.current) return
const currentAnimation = currentAnimationRef.current
const duration = 0.2
const quickDuration = 0.05
const playAnimation = (animation: any, fadeInDuration: number, fadeDuration: number, key: string | null) => {
if (currentAnimation.animation) {
currentAnimation.animation.fadeOut(fadeDuration)
} else {
fadeInDuration = 0
}
animation
.reset()
.setEffectiveWeight(1)
.fadeIn(fadeInDuration)
.play();
currentAnimation.animation = animation
currentAnimation.key = key
currentAnimation.finished = false
}
const isAttacking = lastAttacked > Date.now() - 100
const isHit = lastHit > Date.now() - 100
const hitKey = lastHit.toString()
const attackKey = lastAttacked.toString()
const processAnimation = (key: string, animation: any) => {
if (!actions.current) return
if (currentAnimation.animation && currentAnimation.key === key) {
} else {
playAnimation(animation, quickDuration, quickDuration, key)
}
const onFinished = (event: Event) => {
mixer.removeEventListener('finished', onFinished)
if (actions.current && event.action === animation) {
currentAnimation.finished = true
calculateAnimation()
}
}
mixer.addEventListener('finished', onFinished)
unsubscribe = () => {
mixer.removeEventListener('finished', onFinished)
}
}
if (isDead) {
playAnimation(actions.current.Death, duration, duration, null)
} else if (isAttacking || (currentAnimation.key === attackKey && !currentAnimation.finished)) {
processAnimation(attackKey, actions.current.Bite_Front)
} else if (isHit || (currentAnimation.key === hitKey && !currentAnimation.finished)) {
processAnimation(hitKey, actions.current.HitRecieve)
} else {
playAnimation(actions.current.Dance, duration, duration, null)
}
}
calculateAnimation()
return () => {
unsubscribe()
}
}, [isDead, lastHit, lastAttacked])
return (
)
}
useGLTF.preload('/Demon.glb')
--------------------------------------------------------------------------------
/www/src/3d/models/Knight/Knight.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | auto-generated by: https://github.com/pmndrs/gltfjsx
3 | */
4 | import React, { useRef, useState, useEffect } from 'react'
5 | import { useFrame } from 'react-three-fiber'
6 | import { useGLTF } from '@react-three/drei/useGLTF'
7 |
8 | import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
9 | import {AnimationAction, AnimationMixer, Bone, Group, LoopOnce, MeshToonMaterial, SkinnedMesh} from "three";
10 | import {hexStringToCode} from "../../../utils/color";
11 | import {Event} from "three/src/core/EventDispatcher";
12 |
13 | type GLTFResult = GLTF & {
14 | nodes: {
15 | Cube004: SkinnedMesh
16 | ['Cube.004_1']: SkinnedMesh
17 | ['Cube.004_2']: SkinnedMesh
18 | ['Cube.004_3']: SkinnedMesh
19 | ['Cube.004_4']: SkinnedMesh
20 | Bone: Bone
21 | }
22 | materials: {
23 | Armor: THREE.MeshStandardMaterial
24 | Armor_Dark: THREE.MeshStandardMaterial
25 | Skin: THREE.MeshStandardMaterial
26 | Detail: THREE.MeshStandardMaterial
27 | Red: THREE.MeshStandardMaterial
28 | }
29 | }
30 |
31 | const redMaterial = new MeshToonMaterial({
32 | color: hexStringToCode("#45555a"),
33 | skinning: true,
34 | });
35 | redMaterial.color.convertSRGBToLinear();
36 |
37 | const detailMaterial = new MeshToonMaterial({
38 | color: hexStringToCode("#ffffff"),
39 | skinning: true,
40 | });
41 | detailMaterial.color.convertSRGBToLinear();
42 |
43 | const skinMaterial = new MeshToonMaterial({
44 | color: hexStringToCode("#131313"),
45 | skinning: true,
46 | });
47 | skinMaterial.color.convertSRGBToLinear();
48 |
49 | const armorMaterial = new MeshToonMaterial({
50 | color: hexStringToCode("#434e5f"),
51 | skinning: true,
52 | });
53 | armorMaterial.color.convertSRGBToLinear();
54 |
55 | const armorDarkMaterial = new MeshToonMaterial({
56 | color: hexStringToCode("#242937"),
57 | skinning: true,
58 | });
59 | armorDarkMaterial.color.convertSRGBToLinear();
60 |
61 | type ActionName = 'Idle' | 'PickUp' | 'Punch' | 'RecieveHit' | 'Run' | 'SitDown' | 'Walk'
62 | type GLTFActions = Record
63 |
64 | export default function Knight({moving, recharging, running, lastDamaged, lastAttack, ...props}: JSX.IntrinsicElements['group'] & {
65 | moving: boolean,
66 | recharging: boolean,
67 | running: boolean,
68 | lastDamaged: number,
69 | lastAttack: number,
70 | }) {
71 | const group = useRef()
72 | const { nodes, materials, animations } = useGLTF('/Knight_Golden_Male.glb') as GLTFResult
73 |
74 | const actions = useRef()
75 | const [mixer] = useState(() => new AnimationMixer(nodes['Cube.004_4']))
76 |
77 | const currentAnimationRef = useRef<{
78 | key: string | null,
79 | animation: any,
80 | finished: boolean,
81 | }>({
82 | key: null,
83 | animation: null,
84 | finished: false,
85 | })
86 |
87 | useFrame((state, delta) => mixer.update(delta))
88 | useEffect(() => {
89 | actions.current = {
90 | Idle: mixer.clipAction(animations[0], group.current),
91 | PickUp: mixer.clipAction(animations[1], group.current),
92 | Punch: mixer.clipAction(animations[2], group.current),
93 | RecieveHit: mixer.clipAction(animations[3], group.current),
94 | Run: mixer.clipAction(animations[4], group.current),
95 | SitDown: mixer.clipAction(animations[5], group.current),
96 | Walk: mixer.clipAction(animations[6], group.current),
97 | }
98 | actions.current.Punch.loop = LoopOnce
99 | actions.current.Punch.clampWhenFinished = true
100 | actions.current.Punch.timeScale = 1.2
101 | actions.current.RecieveHit.loop = LoopOnce
102 | actions.current.RecieveHit.clampWhenFinished = true
103 | actions.current.RecieveHit.timeScale = 1.2
104 | actions.current.SitDown.loop = LoopOnce
105 | actions.current.SitDown.clampWhenFinished = true
106 | return () => animations.forEach((clip) => mixer.uncacheClip(clip))
107 | }, [])
108 |
109 |
110 | useEffect(() => {
111 |
112 | let unsubscribe = () => {}
113 |
114 | const calculateAnimation = () => {
115 |
116 | if (!actions.current) return
117 |
118 | const currentAnimation = currentAnimationRef.current
119 |
120 | const duration = 0.2
121 | const quickDuration = 0.05
122 |
123 | const playAnimation = (animation: any, fadeInDuration: number, fadeDuration: number, key: string | null) => {
124 | if (currentAnimation.animation) {
125 | currentAnimation.animation.fadeOut(fadeDuration)
126 | } else {
127 | fadeInDuration = 0
128 | }
129 | animation
130 | .reset()
131 | .setEffectiveWeight(1)
132 | .fadeIn(fadeInDuration)
133 | .play();
134 | currentAnimation.animation = animation
135 | currentAnimation.key = key
136 | currentAnimation.finished = false
137 | }
138 |
139 | const isHit = lastDamaged > Date.now() - 100
140 | const isAttacking = lastAttack > Date.now() - 100
141 | const attackKey = lastAttack.toString()
142 | const hitKey = lastDamaged.toString()
143 |
144 | const processAnimation = (key: string, animation: any) => {
145 |
146 | if (!actions.current) return
147 |
148 | if (currentAnimation.animation && currentAnimation.key === key) {
149 |
150 | } else {
151 | playAnimation(animation, quickDuration, quickDuration, key)
152 | }
153 |
154 | const onFinished = (event: Event) => {
155 | mixer.removeEventListener('finished', onFinished)
156 | if (actions.current && event.action === animation) {
157 | currentAnimation.finished = true
158 | calculateAnimation()
159 | }
160 | }
161 |
162 | mixer.addEventListener('finished', onFinished)
163 |
164 | unsubscribe = () => {
165 | mixer.removeEventListener('finished', onFinished)
166 | }
167 | }
168 |
169 | if (isAttacking || (currentAnimation.key === attackKey && !currentAnimation.finished)) {
170 |
171 | processAnimation(attackKey, actions.current.Punch)
172 |
173 | } else if (isHit || (currentAnimation.key === hitKey && !currentAnimation.finished)) {
174 |
175 | processAnimation(hitKey, actions.current.RecieveHit)
176 |
177 | } else if (recharging) {
178 |
179 | processAnimation('recharging', actions.current.SitDown)
180 |
181 | } else {
182 |
183 | if (moving) {
184 |
185 | if (running) {
186 | playAnimation(actions.current.Run, duration, duration, null)
187 | } else {
188 | playAnimation(actions.current.Walk, duration, duration, null)
189 | }
190 |
191 | } else {
192 | playAnimation(actions.current.Idle, duration, duration, null)
193 | }
194 |
195 | }
196 |
197 | }
198 |
199 | calculateAnimation()
200 |
201 | return () => {
202 | unsubscribe()
203 | }
204 |
205 | }, [moving, recharging, running, lastAttack, lastDamaged])
206 |
207 | return (
208 |
209 |
210 |
211 |
216 |
221 |
226 |
231 |
232 | )
233 | }
234 |
235 | useGLTF.preload('/Knight_Golden_Male.glb')
236 |
--------------------------------------------------------------------------------
/www/src/3d/models/Wall/Wall.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | auto-generated by: https://github.com/pmndrs/gltfjsx
3 | */
4 | import React, { useRef } from 'react'
5 | import { useGLTF } from '@react-three/drei/useGLTF'
6 |
7 | import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
8 | import {Group, Mesh, MeshStandardMaterial} from "three";
9 |
10 | type GLTFResult = GLTF & {
11 | nodes: {
12 | Cube4530: Mesh
13 | ['Cube.4530_1']: Mesh
14 | }
15 | materials: {
16 | Stone: MeshStandardMaterial
17 | StoneDark: MeshStandardMaterial
18 | }
19 | }
20 |
21 | export default function Wall(props: JSX.IntrinsicElements['group']) {
22 | const group = useRef()
23 | const { nodes, materials } = useGLTF('/wall.gltf.glb') as GLTFResult
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | useGLTF.preload('/wall.gltf.glb')
35 |
--------------------------------------------------------------------------------
/www/src/3d/models/WallCorner/WallCorner.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | auto-generated by: https://github.com/pmndrs/gltfjsx
3 | */
4 | import React, { useRef } from 'react'
5 | import { useGLTF } from '@react-three/drei/useGLTF'
6 |
7 | import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
8 | import {Group, Mesh, MeshStandardMaterial} from "three";
9 |
10 | type GLTFResult = GLTF & {
11 | nodes: {
12 | Cube4531: Mesh
13 | ['Cube.4531_1']: Mesh
14 | }
15 | materials: {
16 | Stone: MeshStandardMaterial
17 | StoneDark: MeshStandardMaterial
18 | }
19 | }
20 |
21 | export default function WallCorner(props: JSX.IntrinsicElements['group']) {
22 | const group = useRef()
23 | const { nodes, materials } = useGLTF('/wallCorner.gltf.glb') as GLTFResult
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | useGLTF.preload('/wallCorner.gltf.glb')
35 |
--------------------------------------------------------------------------------
/www/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/www/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | render();
7 | const linkElement = screen.getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/www/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Game from "./game/components/Game/Game";
3 | import {GlobalStyle} from "./ui/global";
4 |
5 | function App() {
6 | return (
7 | <>
8 |
9 |
10 | >
11 | );
12 | }
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/www/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@react-hook/window-size"
2 | declare module "worker-loader!*" {
3 | class WebpackWorker extends Worker {
4 | constructor();
5 | }
6 |
7 | export default WebpackWorker;
8 | }
--------------------------------------------------------------------------------
/www/src/game/components/Camera/Camera.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useLayoutEffect, useRef, useState} from "react";
2 | import {useWindowSize} from "@react-hook/window-size";
3 | import {useFrame, useResource, useThree} from "react-three-fiber";
4 | import {gameRefs} from "../../../state/refs";
5 | import {Object3D, Vector3} from "three";
6 | import {cameraPosition, playerPosition} from "../../../state/positions";
7 | import {numLerp} from "../../../utils/numbers";
8 | import {useProxy} from "valtio";
9 | import {devState} from "../../../state/dev";
10 | import {useEnemiesInRange, usePlayerHasTarget} from "../../../state/player";
11 | import {useIsPortrait} from "../../../utils/responsive";
12 |
13 | const data = {
14 | atRest: true,
15 | atRestTimestamp: 0,
16 | previousXDiff: 0,
17 | previousYDiff: 0,
18 | }
19 |
20 | const useAllowedMovementOffset = (): [number, number] => {
21 | const portrait = useIsPortrait()
22 | return portrait ? [1.5, 3] : [3.5, 2.5]
23 | }
24 |
25 | const useCameraOffset = (): [y: number, z: number, x: number,] => {
26 | const portrait = useIsPortrait()
27 | return portrait ? [100, 100, 100] : [75, 75, 75]
28 | }
29 |
30 | const useCameraShadowBounds = (portrait: boolean): [left: number, right: number, top: number, bottom: number] => {
31 | if (portrait) {
32 | return [
33 | 75,
34 | 130,
35 | -10,
36 | 50
37 | ]
38 | }
39 | return [
40 | 0,
41 | 100,
42 | -10,
43 | 50
44 | ]
45 | }
46 |
47 | const Camera: React.FC = () => {
48 | const lightRef: any = useResource()
49 | const ref = useRef()
50 | const {setDefaultCamera} = useThree()
51 | const targetLocked = usePlayerHasTarget()
52 | const inDanger = useEnemiesInRange()
53 | const [allowedX, allowedY] = useAllowedMovementOffset()
54 | const [cameraYOffset, cameraZOffset, cameraXOffset] = useCameraOffset()
55 | const portrait = useIsPortrait()
56 | const [shadowLeft, shadowRight, shadowTop, shadowBottom] = useCameraShadowBounds(portrait)
57 |
58 | useEffect(() => void setDefaultCamera(ref.current), [])
59 |
60 | useEffect(() => {
61 | ref.current.lookAt(0, 2, 0)
62 |
63 | if (!ref.current) return
64 | if (!lightRef.current) return
65 | const light = lightRef.current
66 | const camera = ref.current
67 |
68 | light.target.position.x = camera.position.x
69 | light.target.position.y = camera.position.y
70 | light.target.position.z = camera.position.z
71 | light.target.updateMatrixWorld()
72 |
73 | }, [])
74 |
75 | useFrame(() => {
76 | if (!ref.current) return
77 | if (!lightRef.current) return
78 | const light = lightRef.current
79 | const camera = ref.current
80 | const {
81 | x,
82 | z: y,
83 | } = camera.position
84 |
85 | let newX = x
86 | let newY = y
87 |
88 | const isTargetLocked = targetLocked
89 |
90 | const playerXDiff = Math.round((playerPosition.x - playerPosition.previousX) * (isTargetLocked ? 2500 : 5000))
91 | const playerYDiff = Math.round((playerPosition.y - playerPosition.previousY) * (isTargetLocked ? 2500 : 5000))
92 |
93 | const moving = playerYDiff !== 0 || playerXDiff !== 0
94 |
95 | const cameraXDiff = (x - playerPosition.x) + cameraXOffset
96 | const cameraYDiff = (y - playerPosition.y) + cameraZOffset
97 |
98 | let movedSufficiently = inDanger || !data.atRest || (Math.abs(cameraXDiff) > allowedX || Math.abs(cameraYDiff) > allowedY) || (Math.abs(playerXDiff) > 500 || Math.abs(playerYDiff) > 500)
99 |
100 | if (movedSufficiently) {
101 |
102 | const adjustedXDiff = numLerp(playerXDiff, data.previousXDiff, 0.9)
103 | const adjustedYDiff = numLerp(playerYDiff, data.previousYDiff, 0.9)
104 |
105 | if (adjustedXDiff === 0 && adjustedYDiff === 0) {
106 | // todo...
107 | }
108 |
109 | data.previousXDiff = adjustedXDiff
110 | data.previousYDiff = adjustedYDiff
111 |
112 | newX = playerPosition.x + (adjustedXDiff * 0.01) - cameraXOffset
113 | newY = playerPosition.y + (adjustedYDiff * 0.01) - cameraZOffset
114 | }
115 |
116 | if (isTargetLocked) {
117 |
118 | newX = numLerp(newX, playerPosition.targetX - cameraXOffset, 0.33)
119 | newY = numLerp(newY, playerPosition.targetY - cameraZOffset, 0.33)
120 |
121 | }
122 |
123 | let xDiff = Math.abs(x - newX)
124 | let yDiff = Math.abs(y - newY)
125 |
126 | if (x !== newX || y !== newY) {
127 | camera.position.x = numLerp(x, newX, 0.05)
128 | camera.position.z = numLerp(y, newY, 0.05)
129 | light.target.position.x = camera.position.x
130 | light.target.position.y = camera.position.y
131 | light.target.position.z = camera.position.z
132 | light.target.updateMatrixWorld()
133 | cameraPosition.previousX = x
134 | cameraPosition.previousY = y
135 | }
136 |
137 | // not at rest if camera moving, or camera was moving and player is still moving
138 |
139 | if ((xDiff > 0.05 || yDiff > 0.05) || (!data.atRest && moving) || isTargetLocked) {
140 | data.atRest = false
141 | data.atRestTimestamp = 0
142 | } else {
143 | if (!data.atRest) {
144 | if (!data.atRestTimestamp) {
145 | data.atRestTimestamp = Date.now() + 250
146 | } else if (Date.now() > data.atRestTimestamp) {
147 | data.atRest = true
148 | }
149 | }
150 | }
151 |
152 | })
153 |
154 | return (
155 |
156 |
170 | {/*{light.current && }*/}
171 |
172 | );
173 | };
174 |
175 | export default Camera;
--------------------------------------------------------------------------------
/www/src/game/components/DevMenu/DevMenu.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import {useProxy} from "valtio";
4 | import {devState} from "../../../state/dev";
5 |
6 | const StyledContainer = styled.div`
7 | position: absolute;
8 | bottom: 10px;
9 | left: 10px;
10 | `;
11 |
12 | const StyledFullscreen = styled.button`
13 | `;
14 |
15 | const DevMenu: React.FC<{
16 | onFullscreen: () => void,
17 | }> = ({onFullscreen}) => {
18 |
19 | const localDevState = useProxy(devState)
20 |
21 | return (
22 |
23 |
24 |
25 | FULLSCREEN
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default DevMenu;
--------------------------------------------------------------------------------
/www/src/game/components/Game/Game.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useRef, useState} from "react";
2 | import {Canvas} from "react-three-fiber";
3 | import {Box, Stats} from "@react-three/drei";
4 | import Floor from "../../../3d/components/Floor/Floor";
5 | import styled from "styled-components";
6 | import Player from "../Player/Player";
7 | import Joystick, {nippleManager} from "../Joystick/Joystick";
8 | import {FullScreen, useFullScreenHandle} from "react-full-screen";
9 | import Lights from "../Lights/Lights";
10 | import Camera from "../Camera/Camera";
11 | import DevMenu from "../DevMenu/DevMenu";
12 | import Physics from "../../../physics/components/Physics/Physics";
13 | import Mob from "../Mob/Mob";
14 | import GameUI from "./components/GameUI/GameUI";
15 | import AttackColliders from "./components/AttackColliders/AttackColliders";
16 | import nipplejs from "nipplejs";
17 | import TestBox from "../TestBox/TestBox";
18 | import Wall from "../../../3d/models/Wall/Wall";
19 | import WallCorner from "../../../3d/models/WallCorner/WallCorner";
20 | import PhysWall from "../PhysWall/PhysWall";
21 | import Room from "../Room/Room";
22 | import GameAI from "./components/GameAI/GameAI";
23 | import MobsManager from "../MobsManager/MobsManager";
24 | import AttackUIContainer from "./components/AttackUIContainer/AttackUIContainer";
25 |
26 |
27 | export const STATS_CSS_CLASS = 'stats'
28 |
29 | const StyledContainer = styled.div`
30 | position: fixed;
31 | top: 0;
32 | left: 0;
33 | right: 0;
34 | bottom: 0;
35 | `;
36 |
37 | const Game: React.FC = () => {
38 |
39 | const handle = useFullScreenHandle()
40 | const {active, enter} = handle
41 |
42 |
43 | return (
44 |
45 |
46 |
47 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | );
70 | };
71 |
72 | export default Game;
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/AttackColliders/AttackColliders.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {useProxy} from "valtio";
3 | import {attackColliders} from "../../../Player/hooks/attack";
4 | import AttackCollider from "./components/AttackCollider/AttackCollider";
5 |
6 | const AttackColliders: React.FC = () => {
7 |
8 | const colliders = useProxy(attackColliders).colliders
9 |
10 | return <>
11 | {
12 | colliders.map(({id, x, y, vX, vY, expires}) => (
13 |
14 | ))
15 | }
16 | >;
17 | };
18 |
19 | export default AttackColliders;
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/AttackColliders/components/AttackCollider/AttackCollider.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback, useEffect, useState} from "react";
2 | import {Cylinder, Sphere} from "@react-three/drei";
3 | import {attackColliders} from "../../../../../Player/hooks/attack";
4 | import {useBody} from "../../../../../../../physics/components/Physics/hooks";
5 | import {BodyShape, BodyType} from "../../../../../../../physics/bodies";
6 | import {Vec2} from "planck-js";
7 | import {COLLISION_FILTER_GROUPS} from "../../../../../../../physics/collisions/filters";
8 | import {getMobHealthManager} from "../../../../../../../state/mobs";
9 | import {PhysicsCacheKeys} from "../../../../../../../physics/cache";
10 | import {playerTargets} from "../../../../../../../state/player";
11 |
12 | const tempVec2 = Vec2(0, 0)
13 |
14 | const AttackCollider: React.FC<{
15 | id: number,
16 | x: number,
17 | y: number,
18 | vX: number,
19 | vY: number,
20 | expires: number,
21 | }> = ({id, x, y, vX, vY, expires}) => {
22 |
23 | const [mounted, setMounted] = useState(false)
24 |
25 | useEffect(() => {
26 | setTimeout(() => {
27 | setMounted(true)
28 | }, 20)
29 | })
30 |
31 | const size = 1.25
32 |
33 | const onCollideStart = useCallback(({mobID}: {
34 | mobID: number,
35 | }) => {
36 | playerTargets.lastAttacked = mobID
37 | const manager = getMobHealthManager(mobID)
38 | if (!manager) return
39 | manager.health = manager.health - 25
40 | manager.lastHit = Date.now()
41 | }, [])
42 |
43 | const [ref, api] = useBody(() => ({
44 | type: BodyType.dynamic,
45 | position: Vec2(x, y),
46 | fixtures: [{
47 | shape: BodyShape.circle,
48 | radius: size,
49 | fixtureOptions: {
50 | isSensor: true,
51 | filterCategoryBits: COLLISION_FILTER_GROUPS.attackCollider,
52 | filterMaskBits: COLLISION_FILTER_GROUPS.attackReceiver,
53 | },
54 | }],
55 | }), {
56 | onCollideStart,
57 | cacheKey: PhysicsCacheKeys.PUNCH,
58 | })
59 |
60 | useEffect(() => {
61 |
62 | tempVec2.set(vX, vY)
63 | api.setLinearVelocity(tempVec2)
64 |
65 | const now = Date.now()
66 |
67 | let timeout = setTimeout(() => {
68 | let index = attackColliders.colliders.findIndex((collider) => collider.id === id)
69 | if (index >= 0) {
70 | attackColliders.colliders.splice(index, 1)
71 | }
72 | }, expires - now)
73 |
74 | return () => {
75 | clearTimeout(timeout)
76 | }
77 |
78 | }, [id, expires])
79 |
80 | return null
81 |
82 | return (
83 |
84 | {
85 | mounted && (
86 |
87 |
89 |
90 | )
91 | }
92 |
93 | );
94 | };
95 |
96 | export default AttackCollider;
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/AttackUIContainer/AttackUIContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, {useLayoutEffect, useRef} from "react";
2 | import {containerPortal} from "../../../Player/components/PlayerUI/PlayerUI";
3 |
4 | const AttackUIContainer: React.FC = () => {
5 |
6 | const containerRef = useRef(null)
7 |
8 | useLayoutEffect(() => {
9 | containerPortal.ref = containerRef
10 | }, [])
11 |
12 | return ();
13 | };
14 |
15 | export default AttackUIContainer;
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/AttackUIContainer/components/AttackUI/AttackUI.tsx:
--------------------------------------------------------------------------------
1 | import React, {TouchEventHandler, useCallback, useEffect, useRef} from "react";
2 | import styled, {css} from "styled-components";
3 | import nipplejs, {JoystickManager} from "nipplejs";
4 | import {proxy, useProxy} from "valtio";
5 | import {useWindowSize} from "@react-hook/window-size";
6 |
7 | export enum AttackContainerSize {
8 | LARGE = 'LARGE',
9 | MEDIUM = 'MEDIUM',
10 | SMALL = 'SMALL'
11 | }
12 |
13 | const getAttackContainerSize = (size: AttackContainerSize): number => {
14 | switch (size) {
15 | case AttackContainerSize.LARGE:
16 | return 180
17 | case AttackContainerSize.MEDIUM:
18 | return 130
19 | default:
20 | return 100
21 | }
22 | }
23 |
24 | const StyledContainer = styled.div<{
25 | containerSize: number
26 | }>`
27 | position: relative;
28 | padding:2px;
29 | width: ${props => props.containerSize}px;
30 | height: ${props => props.containerSize}px;
31 | `;
32 |
33 | const StyledInner = styled.div`
34 | width: 100%;
35 | height: 100%;
36 | border: 3px solid rgba(255,0,0,0.5);
37 | pointer-events: none;
38 | `;
39 |
40 | export const attackStateProxy = proxy({
41 | attackEngaged: false,
42 | })
43 |
44 | export const attackInputData = {
45 | xVel: 0,
46 | yVel: 0,
47 | lastReleased: 0,
48 | nextAvailable: 0,
49 | }
50 |
51 | export const attackBuffer: number[] = []
52 |
53 | const getParentXY = (event: any): [number, number] => {
54 | const parentTransform = event.target.parentElement.parentElement.style.transform
55 | const transformSplit = parentTransform.split("(")[1].split(",")
56 | const parentX = Number(transformSplit[0].trim().replace('px', ''))
57 | const parentY = Number(transformSplit[1].trim().replace('px', ''))
58 | return [parentX, parentY]
59 | }
60 |
61 | const AttackUI: React.FC<{
62 | size: AttackContainerSize,
63 | }> = ({size}) => {
64 | const ref = useRef()
65 | const containerSize = getAttackContainerSize(size)
66 |
67 | const calcOffset = useCallback((x: number, y: number, parentX: number, parentY: number) => {
68 |
69 | const containerCenterX = parentX
70 | const containerCenterY = parentY
71 |
72 | // get angle
73 |
74 | const angle = Math.atan2((x - containerCenterX), (y - containerCenterY))
75 |
76 | const xVector = Math.cos(angle)
77 | const yVector = Math.sin(angle)
78 |
79 | const xDistance = Math.abs(x - containerCenterX)
80 | const yDistance = Math.abs(y - containerCenterY)
81 |
82 | if (xDistance < 10 && yDistance < 10) {
83 | // attackInputData.xVel = 0
84 | // attackInputData.yVel = 0
85 | return
86 | }
87 |
88 | attackInputData.xVel = xVector * -1
89 | attackInputData.yVel = yVector
90 |
91 | }, [])
92 |
93 | const onStart = useCallback((event: any) => {
94 |
95 | attackStateProxy.attackEngaged = true
96 |
97 | let x = 0
98 | let y = 0
99 |
100 | if (event.type === "mousedown") {
101 |
102 | x = event.clientX
103 | y = event.clientY
104 |
105 | } else {
106 | x = event.changedTouches[0].clientX
107 | y = event.changedTouches[0].clientY
108 | }
109 |
110 | const [parentX, parentY] = getParentXY(event)
111 |
112 | calcOffset(x, y, parentX, parentY)
113 |
114 | }, [])
115 |
116 | const onEnd = useCallback((event: any) => {
117 |
118 | let x = 0
119 | let y = 0
120 |
121 | if (event.type === "mouseup") {
122 |
123 | x = event.clientX
124 | y = event.clientY
125 |
126 | } else {
127 | x = event.changedTouches[0].clientX
128 | y = event.changedTouches[0].clientY
129 | }
130 |
131 | const [parentX, parentY] = getParentXY(event)
132 |
133 | calcOffset(x, y, parentX, parentY)
134 |
135 | const now = Date.now()
136 |
137 | if (!attackInputData.nextAvailable || now >= attackInputData.nextAvailable) {
138 | attackBuffer.push(now)
139 | attackInputData.nextAvailable = now + 350
140 | }
141 |
142 | attackInputData.lastReleased = now
143 | attackStateProxy.attackEngaged = false
144 |
145 | }, [])
146 |
147 | const onMove = useCallback((event: any) => {
148 |
149 | let x = 0
150 | let y = 0
151 |
152 | if (event.type === "mousemove") {
153 |
154 | if (!attackStateProxy.attackEngaged) return
155 |
156 | x = event.clientX
157 | y = event.clientY
158 |
159 | } else {
160 | x = event.targetTouches[0].clientX
161 | y = event.targetTouches[0].clientY
162 | }
163 |
164 | const [parentX, parentY] = getParentXY(event)
165 |
166 | calcOffset(x, y, parentX, parentY)
167 |
168 | }, [size])
169 |
170 | return (
171 |
172 |
173 |
174 | );
175 | };
176 |
177 | export default AttackUI;
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/GameAI/GameAI.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import {useGameAiHandler} from "./hooks/ai";
3 |
4 | const GameAI: React.FC = () => {
5 |
6 | useGameAiHandler()
7 |
8 | return null
9 | }
10 |
11 | export default GameAI
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/GameAI/hooks/ai.ts:
--------------------------------------------------------------------------------
1 | import {useEffect} from "react";
2 | import {processMobsAI} from "../../../../../../temp/ai";
3 |
4 | export const useGameAiHandler = () => {
5 |
6 | useEffect(() => {
7 |
8 | setInterval(() => {
9 | processMobsAI()
10 | }, 1000 / 30)
11 |
12 | }, [])
13 |
14 | }
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/GameUI/GameUI.tsx:
--------------------------------------------------------------------------------
1 | import React, {useCallback} from "react";
2 | import styled, {css} from "styled-components";
3 | import {GiPrayer, GiPunch, GiRun} from "react-icons/all";
4 | import {InputKeys, inputsState, RAW_INPUTS} from "../../../../../state/inputs";
5 | import StatsUI from "./components/StatsUI/StatsUI";
6 | import {usePlayerCanRecharge} from "../../../../../state/player";
7 |
8 | const StyledWrapper = styled.div`
9 | user-select: none;
10 | `;
11 |
12 | const StyledContainer = styled.div`
13 | position: absolute;
14 | bottom: 10px;
15 | right: 10px;
16 | display: flex;
17 | flex-direction: column;
18 | align-items: flex-end;
19 | `;
20 |
21 | const cssDisabled = css`
22 | opacity: 0.25;
23 | `
24 |
25 | const StyledAttackButton = styled.div<{
26 | disabled?: boolean,
27 | }>`
28 | width: 100px;
29 | height: 100px;
30 | border: 2px solid white;
31 | display: flex;
32 | justify-content: center;
33 | align-items: center;
34 | color: white;
35 | cursor: pointer;
36 |
37 | &:not(:first-child) {
38 | margin-left: 10px;
39 | }
40 |
41 | ${props => props.disabled ? cssDisabled : ''};
42 |
43 | `;
44 |
45 | const StyledButtons = styled.div`
46 | margin-top: 10px;
47 | display: flex;
48 | `;
49 |
50 | const GameUI: React.FC = () => {
51 |
52 | const canRecharge = usePlayerCanRecharge()
53 |
54 | const onPunchMouseDown = useCallback(() => {
55 | inputsState[InputKeys.PUNCH].raw = true
56 | }, [])
57 |
58 | const onPunchMouseUp = useCallback(() => {
59 | inputsState[InputKeys.PUNCH].raw = false
60 | }, [])
61 |
62 | const onRunMouseDown = useCallback(() => {
63 | inputsState[InputKeys.SHIFT].raw = true
64 | }, [])
65 |
66 | const onRunMouseUp = useCallback(() => {
67 | inputsState[InputKeys.SHIFT].raw = false
68 | }, [])
69 |
70 | const onRechargeMouseDown = useCallback(() => {
71 | inputsState[InputKeys.RECHARGE].raw = true
72 | }, [])
73 |
74 | const onRechargeMouseUp = useCallback(() => {
75 | inputsState[InputKeys.RECHARGE].raw = false
76 | }, [])
77 |
78 | return (
79 |
80 |
81 |
82 | {/**/}
84 | {/* */}
85 | {/**/}
86 |
87 |
89 |
90 |
91 | {/**/}
93 | {/* */}
94 | {/**/}
95 |
96 |
97 |
98 | );
99 | };
100 |
101 | export default GameUI;
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/GameUI/components/StatsUI/Health/Health.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled, {css} from "styled-components";
3 | import {useProxy} from "valtio";
4 | import {playerHealth} from "../../../../../../../../state/player";
5 | import {GiHearts} from "react-icons/all";
6 | import {COLORS} from "../../../../../../../../ui/colors";
7 |
8 | const StyledContainer = styled.div``
9 |
10 | const StyledHeartsContainer = styled.div`
11 | display: flex;
12 | `
13 |
14 | const StyledHeart = styled.div`
15 | position: relative;
16 | `
17 |
18 | const StyledHeartBg = styled.div<{
19 | full: boolean,
20 | }>`
21 | color: rgba(0,0,0,0.5);
22 | `
23 |
24 | const cssHalf = css`
25 | clip-path: inset(0 50% 0 0);
26 | `
27 |
28 | const cssFull = css`
29 | clip-path: inset(0 0 0 0);
30 | `
31 |
32 | const StyledHeartFg = styled.div<{
33 | full: boolean,
34 | half: boolean,
35 | }>`
36 | position: absolute;
37 | top: 0;
38 | left: 0;
39 | bottom: 0;
40 | right: 0;
41 | color: ${COLORS.health};
42 | clip-path: inset(0 100% 0 0);
43 | ${props => props.full ? cssFull : ''};
44 | ${props => props.half ? cssHalf : ''};
45 | transition: all 200ms ease;
46 | `
47 |
48 | const Health: React.FC = () => {
49 | const healthProxy = useProxy(playerHealth)
50 | return (
51 |
52 |
53 | {
54 | Array.from({length: healthProxy.maxHealth}).map((_, index) => {
55 | const full = healthProxy.health >= index + 1
56 | const half = healthProxy.health === index + 0.5
57 | return (
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | )
67 | })
68 | }
69 |
70 |
71 | )
72 | }
73 |
74 | export default Health
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/GameUI/components/StatsUI/Juice/Juice.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useRef, useState} from "react";
2 | import styled from "styled-components";
3 | import {subscribe} from "valtio";
4 | import {JUICE_RECHARGE_COST, playerCanRecharge, playerEnergy, playerJuice} from "../../../../../../../../state/player";
5 |
6 | const StyledContainer = styled.div``;
7 |
8 | const StyledBar = styled.div<{
9 | full: boolean,
10 | }>`
11 | width: 100%;
12 | max-width: 200px;
13 | height: 14px;
14 | border: 2px solid white;
15 | overflow: hidden;
16 | position: relative;
17 | opacity: ${props => props.full ? '1' : '0.5'};
18 | transition: opacity 250ms ease;
19 | `;
20 |
21 | const StyledBarInner = styled.div`
22 | position: absolute;
23 | top: 0;
24 | left: 0;
25 | right: 0;
26 | bottom: 0;
27 | background-color: white;
28 | transform: translate(-100%, 0);
29 | transition: transform ease 200ms;
30 | `;
31 |
32 | const Juice: React.FC = () => {
33 | const barRef = useRef()
34 | const [full, setFull] = useState(() => playerCanRecharge(false))
35 |
36 | useEffect(() => {
37 |
38 | const calcJuice = () => {
39 | if (barRef.current) {
40 | const juice = playerJuice.juice
41 | const juicePercent = 100 - juice
42 | barRef.current.style.transform = `translate(-${juicePercent}%, 0)`
43 | setFull(playerCanRecharge(false))
44 | }
45 | }
46 |
47 | calcJuice()
48 |
49 | const unsubscribe = subscribe(playerJuice, () => {
50 | calcJuice()
51 | })
52 | return () => {
53 | unsubscribe()
54 | }
55 | }, [])
56 |
57 | return (
58 |
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default Juice;
--------------------------------------------------------------------------------
/www/src/game/components/Game/components/GameUI/components/StatsUI/StatsUI.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useRef, useState} from "react";
2 | import styled from "styled-components";
3 | import {subscribe, useProxy} from "valtio";
4 | import {playerEnergy} from "../../../../../../../state/player";
5 | import Health from "./Health/Health";
6 | import Juice from "./Juice/Juice";
7 |
8 | const StyledTopLeftContainer = styled.div`
9 | position: absolute;
10 | top: 10px;
11 | left: 10px;
12 | z-index: 9999999;
13 | `
14 |
15 | const StyledContainer = styled.div`
16 | position: absolute;
17 | top: 10px;
18 | left: 10px;
19 | right: 10px;
20 | display: flex;
21 | justify-content: center;
22 | z-index: 9999999;
23 | `;
24 |
25 | const StyledBar = styled.div<{
26 | full: boolean,
27 | }>`
28 | width: 100%;
29 | max-width: 200px;
30 | height: 14px;
31 | border: 2px solid white;
32 | opacity: ${props => props.full ? 0 : 0.5};
33 | overflow: hidden;
34 | position: relative;
35 | transition: opacity 300ms ${props => props.full ? "200ms" : ""} ease;
36 | `;
37 |
38 | const StyledBarInner = styled.div`
39 | position: absolute;
40 | top: 0;
41 | left: 0;
42 | right: 0;
43 | bottom: 0;
44 | background-color: white;
45 | transform: translate(0%, 0);
46 | transition: transform linear 110ms;
47 | `;
48 |
49 | const StatsUI: React.FC = () => {
50 | const barRef = useRef()
51 | const [full, setFull] = useState(playerEnergy.energy === 100)
52 |
53 | useEffect(() => {
54 | const unsubscribe = subscribe(playerEnergy, () => {
55 | if (barRef.current) {
56 | const energy = playerEnergy.energy
57 | const energyPercent = 100 - energy
58 | barRef.current.style.transform = `translate(-${energyPercent}%, 0)`
59 | setFull(energy >= 100)
60 | }
61 | })
62 | return () => {
63 | unsubscribe()
64 | }
65 | }, [])
66 |
67 | return (
68 | <>
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | >
79 | );
80 | };
81 |
82 | export default StatsUI;
--------------------------------------------------------------------------------
/www/src/game/components/Joystick/Joystick.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useRef} from "react";
2 | import nipplejs, {JoystickManager} from 'nipplejs';
3 | import styled from "styled-components";
4 |
5 | const StyledContainer = styled.div`
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | right: 0;
10 | bottom: 0;
11 |
12 | .nipple {
13 | pointer-events: none;
14 | }
15 |
16 | `;
17 |
18 | export let nippleManager: JoystickManager;
19 |
20 | export const inputData = {
21 | lastTouchStart: 0,
22 | lastTouchEnd: 0,
23 | }
24 |
25 | const Joystick: React.FC = ({children}) => {
26 |
27 | const ref = useRef()
28 |
29 | useEffect(() => {
30 |
31 | nippleManager = nipplejs.create({
32 | zone: ref.current,
33 | });
34 |
35 | }, [])
36 |
37 | return (
38 | inputData.lastTouchStart = Date.now()} onTouchEnd={() => inputData.lastTouchEnd = Date.now()}>
39 | {children}
40 |
41 | );
42 | };
43 |
44 | export default Joystick;
--------------------------------------------------------------------------------
/www/src/game/components/Lights/Lights.tsx:
--------------------------------------------------------------------------------
1 | import React, {useLayoutEffect, useRef} from "react";
2 | import {Canvas} from "react-three-fiber";
3 | import {gameRefs} from "../../../state/refs";
4 |
5 | const Lights: React.FC = () => {
6 | return (
7 | <>
8 |
9 | >
10 | );
11 | };
12 |
13 | export default Lights;
--------------------------------------------------------------------------------
/www/src/game/components/Mob/Mob.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useRef, useState} from "react";
2 | import MobPhysics from "./components/MobPhysics/MobPhysics";
3 | import MobVisuals from "./components/MobVisuals/MobVisuals";
4 | import {Object3D} from "three";
5 | import MobTargetTracking from "./components/MobTargetTracking/MobTargetTracking";
6 | import {useProxy} from "valtio";
7 | import {playerTargets, usePlayerTarget} from "../../../state/player";
8 | import {deleteMobHealthManager, getMobHealthManager, initMobHealthManager} from "../../../state/mobs";
9 | import {addMob, removeMob, updateMob} from "../../../temp/ai";
10 | import {useMobBrain} from "./hooks/brain";
11 | import {MOB_VARIANT} from "./data";
12 |
13 | const useIsTargeted = (id: number): boolean => {
14 | const targetID = usePlayerTarget()
15 | return targetID === id
16 | }
17 |
18 | const useIsInAttackRange = (id: number): boolean => {
19 | const {attackRange} = useProxy(playerTargets)
20 | return attackRange.includes(id)
21 | }
22 |
23 | const useIsDead = (id: number): boolean => {
24 |
25 | const managerProxy = useProxy(getMobHealthManager(id))
26 | return managerProxy.health <= 0
27 |
28 | }
29 |
30 | const MobInner: React.FC<{
31 | id: number,
32 | x: number,
33 | y: number,
34 | variant: MOB_VARIANT,
35 | }> = ({id, x, y, variant}) => {
36 |
37 | const localRef = useRef(new Object3D())
38 | const isTargeted = useIsTargeted(id)
39 | const inAttackRange = useIsInAttackRange(id)
40 | const isDead = useIsDead(id)
41 | const managerProxy = useProxy(getMobHealthManager(id))
42 |
43 | useEffect(() => {
44 |
45 | updateMob(id, {
46 | alive: !isDead,
47 | })
48 |
49 | }, [id, isDead])
50 |
51 | return (
52 | <>
53 | {
54 | !isDead && (
55 |
56 | )
57 | }
58 |
59 | {
60 | isTargeted && (
61 |
62 | )
63 | }
64 | >
65 | );
66 | };
67 |
68 | const Mob: React.FC<{
69 | id: number,
70 | x: number,
71 | y: number,
72 | variant?: MOB_VARIANT,
73 | }> = ({id, x, y, variant = MOB_VARIANT.small}) => {
74 |
75 | const [mounted, setMounted] = useState(false)
76 |
77 | useEffect(() => {
78 | addMob(id)
79 | initMobHealthManager(id, variant)
80 | setMounted(true)
81 |
82 | return () => {
83 | deleteMobHealthManager(id)
84 | removeMob(id)
85 | }
86 | }, [])
87 |
88 | if (!mounted) return null
89 |
90 | return
91 | }
92 |
93 | export default Mob;
--------------------------------------------------------------------------------
/www/src/game/components/Mob/components/MobPhysics/MobPhysics.tsx:
--------------------------------------------------------------------------------
1 | import React, {MutableRefObject, useCallback} from "react";
2 | import {Object3D} from "three";
3 | import {useBody} from "../../../../../physics/components/Physics/hooks";
4 | import {BodyShape, BodyType} from "../../../../../physics/bodies";
5 | import {Vec2} from "planck-js";
6 | import {COLLISION_FILTER_GROUPS} from "../../../../../physics/collisions/filters";
7 | import {FixtureType} from "../../../../../physics/collisions/types";
8 | import {updateMob} from "../../../../../temp/ai";
9 | import {useMobBrain} from "../../hooks/brain";
10 | import {MOB_VARIANT} from "../../data";
11 |
12 | const MobPhysics: React.FC<{
13 | x: number,
14 | y: number,
15 | id: number,
16 | variant: MOB_VARIANT,
17 | localRef: MutableRefObject,
18 | }> = ({x, y, id, localRef, variant}) => {
19 |
20 | const onCollideStart = useCallback(({type}: any) => {
21 | if (type !== undefined && type === FixtureType.PLAYER_RANGE) {
22 | updateMob(id, {
23 | inPlayerRange: true,
24 | })
25 | }
26 | }, [id])
27 |
28 | const onCollideEnd = useCallback(({type}: any) => {
29 | if (type !== undefined && type === FixtureType.PLAYER_RANGE) {
30 | updateMob(id, {
31 | inPlayerRange: false,
32 | })
33 | }
34 | }, [id])
35 |
36 | const size = 0.75
37 |
38 | const [ref, api] = useBody(() => ({
39 | type: BodyType.dynamic,
40 | position: Vec2(x, y),
41 | linearDamping: variant === MOB_VARIANT.large ? 10 : 4,
42 | fixtures: [{
43 | shape: BodyShape.circle,
44 | radius: size * (variant === MOB_VARIANT.large ? 2 : 0.75),
45 | fixtureOptions: {
46 | density: variant === MOB_VARIANT.large ? 50 : 40,
47 | isSensor: false,
48 | filterCategoryBits: COLLISION_FILTER_GROUPS.mob | COLLISION_FILTER_GROUPS.attackReceiver | COLLISION_FILTER_GROUPS.physical,
49 | userData: {
50 | mobID: id,
51 | mobVariant: variant,
52 | type: FixtureType.MOB,
53 | }
54 | },
55 | }],
56 | }), {
57 | fwdRef: localRef,
58 | debug: 'mob',
59 | onCollideStart,
60 | onCollideEnd,
61 | })
62 |
63 |
64 | useMobBrain(id, api, ref, variant)
65 |
66 | return null;
67 | };
68 |
69 | export default MobPhysics;
--------------------------------------------------------------------------------
/www/src/game/components/Mob/components/MobTargetTracking/MobTargetTracking.tsx:
--------------------------------------------------------------------------------
1 | import React, {MutableRefObject, useEffect} from "react";
2 | import {Object3D} from "three";
3 | import {useFrame} from "react-three-fiber";
4 | import {playerPosition} from "../../../../../state/positions";
5 |
6 | const MobTargetTracking: React.FC<{
7 | localRef: MutableRefObject
8 | }> = ({localRef}) => {
9 |
10 | useFrame(() => {
11 | playerPosition.targetX = localRef.current.position.x
12 | playerPosition.targetY = localRef.current.position.z
13 | })
14 |
15 | return null;
16 | };
17 |
18 | export default MobTargetTracking;
--------------------------------------------------------------------------------
/www/src/game/components/Mob/components/MobUI/MobUI.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Html} from "@react-three/drei";
3 | import styled, {css} from "styled-components";
4 | import {useProxy} from "valtio";
5 | import {getMobHealthManager} from "../../../../../state/mobs";
6 | import {MOB_VARIANT} from "../../data";
7 |
8 | const cssHide = css`
9 | opacity: 0;
10 | `
11 |
12 | const cssLargeVariant = css`
13 | width: 75px;
14 | height: 15px;
15 | `
16 |
17 | const StyledContainer = styled.div<{
18 | hide: boolean,
19 | variant: MOB_VARIANT,
20 | }>`
21 | pointer-events: none;
22 | width: 50px;
23 | height: 10px;
24 | background-color: rgba(0,0,0,0.25);
25 | position: relative;
26 | overflow: hidden;
27 | transition: opacity 500ms 250ms ease;
28 | opacity: 0.75;
29 | ${props => props.hide ? cssHide : ''};
30 | ${props => props.variant === MOB_VARIANT.large ? cssLargeVariant : ''};
31 | `;
32 |
33 | const StyledAmount = styled.div<{
34 | amount: number,
35 | }>`
36 | position: absolute;
37 | top: 0;
38 | left: 0;
39 | right: 0;
40 | bottom: 0;
41 | background-color: #980623;
42 | transition: all 200ms ease;
43 | transform: translateX(${props => (props.amount - 100)}%);
44 | `;
45 |
46 | const MobUI: React.FC<{
47 | id: number,
48 | variant: MOB_VARIANT,
49 | }> = ({id, variant}) => {
50 | const managerProxy = useProxy(getMobHealthManager(id))
51 | const yOffset = variant === MOB_VARIANT.large ? 1.75 : 0.75
52 | return (
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default MobUI;
--------------------------------------------------------------------------------
/www/src/game/components/Mob/components/MobVisuals/MobVisuals.tsx:
--------------------------------------------------------------------------------
1 | import React, {MutableRefObject, Suspense, useCallback, useRef} from "react";
2 | import Demon from "../../../../../3d/models/Demon/Demon";
3 | import {Object3D} from "three";
4 | import {useFrame} from "react-three-fiber";
5 | import {playerPosition} from "../../../../../state/positions";
6 | import {lerpRadians, PI, PI_TIMES_TWO} from "../../../../../utils/numbers";
7 | import MobUI from "../MobUI/MobUI";
8 | import {Cylinder} from "@react-three/drei";
9 | import {playerTargets} from "../../../../../state/player";
10 | import {MOB_VARIANT} from "../../data";
11 |
12 | const MobVisuals: React.FC<{
13 | lastHit: number,
14 | lastAttacked: number,
15 | x: number,
16 | y: number,
17 | localRef: MutableRefObject,
18 | isDead: boolean,
19 | id: number,
20 | targeted: boolean,
21 | inAttackRange: boolean,
22 | variant: MOB_VARIANT,
23 | }> = ({localRef, variant, lastHit, lastAttacked, x, y, isDead, id, targeted, inAttackRange}) => {
24 |
25 | const clickedTimestampsRef = useRef({
26 | lastClicked: 0,
27 | })
28 |
29 | const onClick = useCallback(() => {
30 | const now = Date.now()
31 | const lastClicked = clickedTimestampsRef.current.lastClicked
32 |
33 | if (lastClicked > now - 500) {
34 | playerTargets.lastAttacked = id
35 | }
36 |
37 | clickedTimestampsRef.current.lastClicked = now
38 | }, [id])
39 |
40 | useFrame((state, delta) => {
41 | if (isDead) return
42 | const targetX = playerPosition.x
43 | const targetY = playerPosition.y
44 | const x = localRef.current.position.x
45 | const y = localRef.current.position.z
46 | const angle = Math.atan2((targetX - x), (targetY - y))
47 |
48 | let prevAngle = localRef.current.rotation.y // convert to low equivalent angle
49 | if (prevAngle > PI) {
50 | prevAngle -= PI_TIMES_TWO
51 | }
52 |
53 | localRef.current.rotation.y = lerpRadians(prevAngle, angle, 2.5 * delta)
54 | })
55 |
56 | const scale = variant === MOB_VARIANT.large ? 2 : 0.75
57 |
58 | return (
59 |
60 |
61 |
62 |
63 |
64 |
65 | {/**/}
66 | {/* */}
68 | {/**/}
69 |
70 |
71 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default MobVisuals;
--------------------------------------------------------------------------------
/www/src/game/components/Mob/data.ts:
--------------------------------------------------------------------------------
1 | export enum MOB_VARIANT {
2 | small,
3 | large
4 | }
--------------------------------------------------------------------------------
/www/src/game/components/Mob/hooks/brain.ts:
--------------------------------------------------------------------------------
1 | import {useFrame} from "react-three-fiber";
2 | import {useEffect, useState} from "react";
3 | import {getMob, MobAIGoal} from "../../../../temp/ai";
4 | import {BodyApi} from "../../../../physics/components/Physics/hooks";
5 | import {playerPosition} from "../../../../state/positions";
6 | import {Vec2} from "planck-js";
7 | import {useProxy} from "valtio";
8 | import {getMobHealthManager} from "../../../../state/mobs";
9 | import {dealPlayerDamage, playerState, playerTargets} from "../../../../state/player";
10 | import {coroutine} from "../../Player/Player";
11 | import {DIAGONAL} from "../../../../utils/common";
12 | import {MOB_VARIANT} from "../data";
13 |
14 | const attackPlayerCoroutine = function* (attackWaitDuration: number, attackWaitHitDuration: number, damage: number) {
15 |
16 | const started = Date.now()
17 |
18 | // wait 250ms
19 | while (started > Date.now() - attackWaitDuration) {
20 | yield null
21 | }
22 |
23 | const attackStarted = Date.now()
24 |
25 | yield true
26 |
27 | // wait 100ms
28 | while (attackStarted > Date.now() - attackWaitHitDuration) {
29 | yield null
30 | }
31 |
32 | if (Date.now() > attackStarted + 300) {
33 | console.log('attack is outdated')
34 | } else {
35 | dealPlayerDamage(damage)
36 | }
37 |
38 | }
39 |
40 | const velocity = Vec2(0, 0)
41 | const position = Vec2(0, 0)
42 |
43 | export const useMobBrain = (id: number, api: BodyApi, ref: any, variant: MOB_VARIANT) => {
44 | const [localState] = useState(() => ({
45 | attackInitiated: false,
46 | attackPending: false,
47 | }))
48 | const [coroutineManager] = useState<{
49 | attack: any,
50 | }>(() => ({
51 | attack: null,
52 | }))
53 | const [hits] = useState<{
54 | [id: number]: boolean,
55 | }>(() => ({}))
56 | const [manager] = useState(() => getMobHealthManager(id))
57 | const managerProxy = useProxy(manager)
58 |
59 | const {lastHit} = managerProxy
60 |
61 | const [previousVelocities] = useState(() => ({
62 | xVel: 0,
63 | yVel: 0,
64 | }))
65 | const [mobData] = useState(() => getMob(id))
66 |
67 | useFrame((state, delta) => {
68 |
69 | let xVel = 0
70 | let yVel = 0
71 |
72 | const x = ref.current.position.x
73 | const y = ref.current.position.z
74 |
75 | const now = Date.now()
76 |
77 | if (manager.stunned && !hits[manager.lastHit]) {
78 |
79 | velocity.set(manager.attackVector[0] * 150, manager.attackVector[1] * 150)
80 | // api.applyForceToCenter(velocity)
81 | position.set(x, y)
82 | api.applyLinearImpulse(velocity, position)
83 | hits[manager.lastHit] = true
84 |
85 | } else if (manager.lastHit > now - 500) {
86 |
87 | // do nothing...
88 |
89 | } else {
90 |
91 | if (mobData.goal === MobAIGoal.ATTACK) {
92 |
93 | const xDistance = x - playerPosition.x
94 | const yDistance = y - playerPosition.y
95 |
96 | const requiredDistance = variant === MOB_VARIANT.large ? 3 : 2
97 |
98 | if (Math.abs(xDistance) <= requiredDistance && Math.abs(yDistance) <= requiredDistance) {
99 |
100 | if (manager.lastAttacked < now - 1500 && !localState.attackPending && !localState.attackInitiated && !playerState.invincible) {
101 |
102 | localState.attackPending = true
103 | const waitDuration = variant === MOB_VARIANT.large ? 500 : 250
104 | const damage = variant === MOB_VARIANT.large ? 2 : 0.5
105 | coroutineManager.attack = coroutine(attackPlayerCoroutine, [waitDuration, 250, damage])
106 |
107 | }
108 |
109 | if (coroutineManager.attack) {
110 |
111 | const response = coroutineManager.attack()
112 |
113 | if (response.value) {
114 | manager.lastAttacked = now
115 | localState.attackPending = false
116 | localState.attackInitiated = true
117 | }
118 |
119 | if (response.done) {
120 | localState.attackInitiated = false
121 | coroutineManager.attack = null
122 | if (playerTargets.lastHitBy === null || !playerTargets.inRange.includes(playerTargets.lastHitBy)) {
123 | playerTargets.lastHitBy = id
124 | }
125 | }
126 |
127 | }
128 |
129 | } else {
130 |
131 | localState.attackPending = false
132 |
133 | if (coroutineManager.attack && !localState.attackInitiated) {
134 | coroutineManager.attack = null
135 | }
136 |
137 | if (x > playerPosition.x) {
138 | xVel = -1
139 | } else if (x < playerPosition.x) {
140 | xVel = 1
141 | }
142 | if (y > playerPosition.y) {
143 | yVel = -1
144 | } else if (y < playerPosition.y) {
145 | yVel = 1
146 | }
147 |
148 | if (xVel !== 0 && yVel !== 0) {
149 | xVel = xVel * DIAGONAL
150 | yVel = yVel * DIAGONAL
151 | }
152 |
153 | if (variant === MOB_VARIANT.large) {
154 | xVel = xVel * 20
155 | yVel = yVel * 20
156 | }
157 |
158 | }
159 |
160 | } else if (mobData.goal === MobAIGoal.IDLE) {
161 | // return to original position
162 | }
163 |
164 | }
165 |
166 | // if (previousVelocities.xVel !== xVel || previousVelocities.yVel !== yVel) {
167 | velocity.set(xVel * 300, yVel * 300)
168 | api.applyForceToCenter(velocity)
169 | // api.setLinearVelocity(velocity)
170 | previousVelocities.xVel = xVel
171 | previousVelocities.yVel = yVel
172 | // }
173 |
174 | mobData.x = ref.current.position.x
175 | mobData.y = ref.current.position.z
176 |
177 | })
178 |
179 | useEffect(() => {
180 |
181 | if (lastHit > 0) {
182 |
183 | const expires = 150 - (Date.now() - lastHit)
184 |
185 | if (expires <= 0) return
186 |
187 | manager.stunned = true
188 | // init a coroutine for backwards velocity movement?
189 |
190 | let timeout = setTimeout(() => {
191 | manager.stunned = false
192 | }, expires)
193 |
194 | return () => {
195 | clearTimeout(timeout)
196 | }
197 |
198 | }
199 |
200 | }, [lastHit])
201 |
202 | }
--------------------------------------------------------------------------------
/www/src/game/components/MobsManager/MobsManager.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Mob from "../Mob/Mob";
3 | import Physics from "../../../physics/components/Physics/Physics";
4 | import LargeMob from "../../../mobs/components/LargeMob/LargeMob";
5 |
6 | const MobsManager: React.FC = () => {
7 | return (
8 | <>
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | >
17 | );
18 | };
19 |
20 | export default MobsManager;
--------------------------------------------------------------------------------
/www/src/game/components/PhysWall/PhysWall.tsx:
--------------------------------------------------------------------------------
1 | import React, {Suspense, useEffect} from "react"
2 | import Wall from "../../../3d/models/Wall/Wall";
3 | import {useBody} from "../../../physics/components/Physics/hooks";
4 | import {BodyShape, BodyType} from "../../../physics/bodies";
5 | import {Vec2} from "planck-js";
6 | import {COLLISION_FILTER_GROUPS} from "../../../physics/collisions/filters";
7 | import WallMesh from "../../../3d/components/WallMesh/WallMesh";
8 | import WallCornerMesh from "../../../3d/components/WallCornerMesh/WallCornerMesh";
9 | import {radians} from "../../../utils/angles";
10 |
11 | const PhysWall: React.FC = () => {
12 |
13 | useBody(() => ({
14 | type: BodyType.static,
15 | position: Vec2(0, 16),
16 | fixtures: [
17 | {
18 | shape: BodyShape.box,
19 | hx: 33,
20 | hy: 1,
21 | fixtureOptions: {
22 | filterCategoryBits: COLLISION_FILTER_GROUPS.barrier,
23 | filterMaskBits: COLLISION_FILTER_GROUPS.player,
24 | },
25 | }
26 | ]
27 | }), {})
28 |
29 | useBody(() => ({
30 | type: BodyType.static,
31 | position: Vec2(0, -16),
32 | fixtures: [
33 | {
34 | shape: BodyShape.box,
35 | hx: 33,
36 | hy: 1,
37 | fixtureOptions: {
38 | filterCategoryBits: COLLISION_FILTER_GROUPS.barrier,
39 | filterMaskBits: COLLISION_FILTER_GROUPS.player,
40 | },
41 | }
42 | ]
43 | }), {})
44 |
45 | useBody(() => ({
46 | type: BodyType.static,
47 | position: Vec2(16, 0),
48 | fixtures: [
49 | {
50 | shape: BodyShape.box,
51 | hx: 1,
52 | hy: 33,
53 | fixtureOptions: {
54 | filterCategoryBits: COLLISION_FILTER_GROUPS.barrier,
55 | filterMaskBits: COLLISION_FILTER_GROUPS.player,
56 | },
57 | }
58 | ]
59 | }), {})
60 |
61 | useBody(() => ({
62 | type: BodyType.static,
63 | position: Vec2(-16, 0),
64 | fixtures: [
65 | {
66 | shape: BodyShape.box,
67 | hx: 1,
68 | hy: 33,
69 | fixtureOptions: {
70 | filterCategoryBits: COLLISION_FILTER_GROUPS.barrier,
71 | filterMaskBits: COLLISION_FILTER_GROUPS.player,
72 | },
73 | }
74 | ]
75 | }), {})
76 |
77 | return (
78 | <>
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | >
120 | )
121 | }
122 |
123 | export default PhysWall
--------------------------------------------------------------------------------
/www/src/game/components/Player/Player.tsx:
--------------------------------------------------------------------------------
1 | import React, {Suspense, useCallback, useEffect, useRef} from "react";
2 | import {inputData, nippleManager} from "../Joystick/Joystick";
3 | import {useFrame} from "react-three-fiber";
4 | import {radians, rotateVector} from "../../../utils/angles";
5 | import {gameRefs} from "../../../state/refs";
6 | import {playerPosition} from "../../../state/positions";
7 | import {usePlayerControls} from "./hooks/controls";
8 | import {InputKeys, inputsState} from "../../../state/inputs";
9 | import {lerpRadians, numLerp, PI, PI_TIMES_TWO} from "../../../utils/numbers";
10 | import {DIAGONAL} from "../../../utils/common";
11 | import {Vec2} from "planck-js";
12 | import {usePlayerPhysics} from "./hooks/physics";
13 | import PlayerVisuals, {playerVisualState} from "./components/PlayerVisuals/PlayerVisuals";
14 | import PlayerDebug from "./components/PlayerDebug/PlayerDebug";
15 | import {
16 | JUICE_RECHARGE_COST, playerCanRecharge,
17 | playerEnergy,
18 | playerJuice,
19 | playerState,
20 | rechargePlayer,
21 | usePlayerHasTarget,
22 | usePlayerInCombat
23 | } from "../../../state/player";
24 | import {usePlayerCollisionsHandler} from "./hooks/collisions";
25 | import {usePlayerEffectsHandler} from "./hooks/effects";
26 | import {usePlayerStateHandler} from "./hooks/state";
27 | import PlayerUI from "./components/PlayerUI/PlayerUI";
28 | import {attackInputData, attackStateProxy} from "../Game/components/AttackUIContainer/components/AttackUI/AttackUI";
29 |
30 | export const coroutine = (f: any, params: any[] = []) => {
31 | const o = f(...params); // instantiate the coroutine
32 | return function (x: any) {
33 | return o.next(x);
34 | };
35 | };
36 |
37 | enum RechargeState {
38 | PENDING,
39 | ACTIVATED
40 | }
41 |
42 | const beginPreRechargeProcess = () => {
43 | playerState.preRecharging = true
44 | }
45 |
46 | const beginRechargeProcess = () => {
47 | playerState.recharging = true
48 | }
49 |
50 | const endRechargeProcess = () => {
51 | playerState.preRecharging = false
52 | playerState.recharging = false
53 | }
54 |
55 | const rechargeCoroutine = function* () {
56 | const start = Date.now()
57 | const wait = start + 500
58 | const completion = wait + 750
59 | beginPreRechargeProcess()
60 | let rechargePressed = true
61 | while (Date.now() < wait) {
62 | if (!rechargePressed || !playerState.preRecharging) {
63 | endRechargeProcess()
64 | return
65 | }
66 | rechargePressed = yield RechargeState.PENDING
67 | }
68 | beginRechargeProcess()
69 | while (Date.now() < completion) {
70 | yield RechargeState.ACTIVATED
71 | }
72 | rechargePlayer()
73 | endRechargeProcess()
74 | }
75 |
76 | const rollCooldownCoroutine = function* () {
77 | let wait = Date.now() + 500;
78 | playerVisualState.rollCooldown = true
79 | while (Date.now() < wait) {
80 | yield null;
81 | }
82 | playerVisualState.rollCooldown = false
83 | }
84 |
85 | const rollCoroutine = function* () {
86 | let wait = Date.now() + 500;
87 | playerVisualState.rolling = true
88 | while (Date.now() < wait) {
89 | yield null;
90 | }
91 | playerVisualState.rolling = false
92 | }
93 |
94 | const rechargeManager: {
95 | rechargeCoroutine: any,
96 | } = {
97 | rechargeCoroutine: null,
98 | }
99 |
100 | const rollManager: {
101 | lastRolled: number,
102 | rollCoroutine: any,
103 | cooldownCoroutine: any,
104 | } = {
105 | lastRolled: 0,
106 | rollCoroutine: null,
107 | cooldownCoroutine: null,
108 | }
109 |
110 | const nippleState = {
111 | active: false,
112 | }
113 |
114 | const playerLocalState = {
115 | xVelocity: 0,
116 | yVelocity: 0,
117 | rollXVelocity: 0,
118 | rollYVelocity: 0,
119 | }
120 |
121 | const playerJoystickVelocity = {
122 | x: 0,
123 | y: 0,
124 | previousX: 0,
125 | previousY: 0,
126 | targetAngle: 0,
127 | }
128 |
129 | const WALKING_SPEED = 5
130 | const COMBAT_WALKING_SPEED = WALKING_SPEED * 1.25
131 | const RUNNING_SPEED = WALKING_SPEED * 2
132 | const ROLLING_SPEED = RUNNING_SPEED
133 |
134 | const tempVec2 = Vec2(0, 0)
135 |
136 | const Player: React.FC = () => {
137 |
138 | const [ref, api, largeColliderRef, largeColliderApi] = usePlayerPhysics()
139 |
140 | usePlayerCollisionsHandler(api)
141 | usePlayerControls()
142 | usePlayerEffectsHandler()
143 | usePlayerStateHandler()
144 | const targetLocked = usePlayerHasTarget()
145 | const inCombat = usePlayerInCombat()
146 |
147 | useEffect(() => {
148 | gameRefs.player = ref.current
149 | }, [])
150 |
151 | useEffect(() => {
152 |
153 | nippleManager?.on("start", () => {
154 | nippleState.active = true
155 | if (inputData.lastTouchStart > Date.now() - 450) {
156 | inputsState[InputKeys.SHIFT].rawLastPressed = Date.now()
157 | inputsState[InputKeys.SHIFT].raw = true
158 | }
159 | })
160 |
161 | nippleManager?.on("end", () => {
162 | nippleState.active = false
163 | playerJoystickVelocity.previousX = 0
164 | playerJoystickVelocity.previousY = 0
165 | playerJoystickVelocity.x = 0
166 | playerJoystickVelocity.y = 0
167 | inputsState[InputKeys.SHIFT].raw = false
168 | })
169 |
170 | nippleManager?.on("move", (_, data) => {
171 | const {x, y} = data.vector
172 | playerJoystickVelocity.previousX = playerJoystickVelocity.x
173 | playerJoystickVelocity.previousY = playerJoystickVelocity.y
174 | if (Math.abs(x) < 0.1 && Math.abs(y) < 0.1) {
175 | playerJoystickVelocity.x = 0
176 | playerJoystickVelocity.y = 0
177 | return
178 | }
179 | playerJoystickVelocity.x = x * -1
180 | playerJoystickVelocity.y = y
181 | })
182 |
183 | }, [])
184 |
185 | const applyVelocity = useCallback((x: number, y: number) => {
186 | tempVec2.set(x, y)
187 | api.setLinearVelocity(tempVec2)
188 | largeColliderApi.setLinearVelocity(tempVec2)
189 | playerLocalState.xVelocity = x
190 | playerLocalState.yVelocity = y
191 | }, [api, largeColliderApi])
192 |
193 | useFrame(({gl, scene, camera}, delta) => {
194 | if (!ref.current) return
195 |
196 | const {
197 | previousX,
198 | previousY
199 | } = playerPosition
200 |
201 | playerPosition.previousX = playerPosition.x
202 | playerPosition.previousY = playerPosition.y
203 | //largeColliderApi.setAngle(ref.current.rotation.y)
204 |
205 | const {x, z: y} = ref.current.position
206 |
207 | tempVec2.set(x, y)
208 | largeColliderApi.setPosition(tempVec2)
209 |
210 | let xVel = numLerp(playerJoystickVelocity.previousX, playerJoystickVelocity.x, 0.75)
211 | let yVel = numLerp(playerJoystickVelocity.previousY, playerJoystickVelocity.y, 0.75)
212 | let energy = playerEnergy.energy
213 |
214 | if (!nippleState.active) {
215 | let up = inputsState[InputKeys.UP].active
216 | let right = inputsState[InputKeys.RIGHT].active
217 | let down = inputsState[InputKeys.DOWN].active
218 | let left = inputsState[InputKeys.LEFT].active
219 | xVel = left ? 1 : right ? -1 : 0
220 | yVel = up ? 1 : down ? -1 : 0
221 |
222 | if (xVel !== 0 && yVel !== 0) {
223 | xVel = xVel * DIAGONAL
224 | yVel = yVel * DIAGONAL
225 | }
226 |
227 | }
228 |
229 | let rechargeAttempt = inputsState[InputKeys.RECHARGE].active && playerCanRecharge()
230 | let canMove = !rechargeAttempt
231 | let isRechargingActivated = false
232 |
233 | const [adjustedXVel, adjustedYVel] = rotateVector(xVel, yVel, -45)
234 |
235 | xVel = adjustedXVel
236 | yVel = adjustedYVel
237 |
238 | if (rechargeManager.rechargeCoroutine) {
239 |
240 | const rechargeResponse = rechargeManager.rechargeCoroutine(rechargeAttempt)
241 |
242 | if (rechargeResponse.done) {
243 | rechargeManager.rechargeCoroutine = null
244 | } else if (rechargeResponse.value === RechargeState.ACTIVATED) {
245 | isRechargingActivated = true
246 | }
247 |
248 | } else if (rechargeAttempt) {
249 | rechargeManager.rechargeCoroutine = coroutine(rechargeCoroutine)
250 |
251 | }
252 |
253 | if (isRechargingActivated) {
254 | xVel = 0
255 | yVel = 0
256 | canMove = false
257 | }
258 |
259 | const ongoingRoll = !!rollManager.rollCoroutine
260 | const canRoll = canMove && (inCombat && rollManager.lastRolled < Date.now() - 1000 && !playerVisualState.rollCooldown && energy >= 33) || ongoingRoll
261 |
262 | const isRolling = canRoll && inputsState[InputKeys.SHIFT].active
263 | const isMoving = canMove && (xVel !== 0 || yVel !== 0)
264 | const isRunning = canMove && inputsState[InputKeys.SHIFT].active && !inCombat && energy > 0
265 |
266 | if (!!rollManager.cooldownCoroutine) {
267 | if (rollManager.cooldownCoroutine().done) {
268 | rollManager.cooldownCoroutine = null
269 | }
270 | }
271 |
272 |
273 | if (ongoingRoll) {
274 |
275 | let speed = ROLLING_SPEED
276 |
277 | const adjustedXVel = xVel * speed
278 | const adjustedYVel = yVel * speed
279 |
280 | if (nippleState.active) {
281 | xVel = numLerp(playerLocalState.rollXVelocity, adjustedXVel, 0.1)
282 | yVel = numLerp(playerLocalState.rollYVelocity, adjustedYVel, 0.1)
283 | } else {
284 | xVel = playerLocalState.rollXVelocity
285 | yVel = playerLocalState.rollYVelocity
286 | }
287 |
288 |
289 | const response = rollManager.rollCoroutine()
290 |
291 | if (response.done) {
292 | rollManager.rollCoroutine = null
293 | rollManager.cooldownCoroutine = coroutine(rollCooldownCoroutine)
294 | }
295 |
296 | playerLocalState.rollXVelocity = xVel
297 | playerLocalState.rollYVelocity = yVel
298 |
299 | applyVelocity(xVel, yVel)
300 |
301 | } else if (isMoving) {
302 |
303 | const preSpeedXVel = xVel
304 | const preSpeedYVel = yVel
305 |
306 | let speed = isRolling ? ROLLING_SPEED : isRunning ? RUNNING_SPEED : inCombat ? COMBAT_WALKING_SPEED : WALKING_SPEED
307 |
308 | const adjustedXVel = xVel * speed
309 | const adjustedYVel = yVel * speed
310 |
311 | if (isRolling) {
312 |
313 | const rollAngle = Math.atan2(preSpeedYVel, preSpeedXVel)
314 |
315 | const rollX = Math.cos(rollAngle) * speed
316 | const rollY = Math.sin(rollAngle) * speed
317 |
318 | playerLocalState.rollXVelocity = rollX
319 | playerLocalState.rollYVelocity = rollY
320 |
321 | rollManager.rollCoroutine = coroutine(rollCoroutine)
322 | rollManager.lastRolled = Date.now()
323 | energy -= 33
324 |
325 | } else if (isRunning) {
326 |
327 | const energyUsed = delta * 15
328 |
329 | energy -= energyUsed
330 |
331 | }
332 |
333 | applyVelocity(adjustedXVel, adjustedYVel)
334 | } else {
335 | applyVelocity(0, 0)
336 | }
337 |
338 | let prevAngle = ref.current.rotation.y // convert to low equivalent angle
339 | if (prevAngle > PI) {
340 | prevAngle -= PI_TIMES_TWO
341 | }
342 |
343 | const isTargetLocked = targetLocked
344 |
345 | const attackInputActive = attackStateProxy.attackEngaged
346 |
347 | const applyAttackAngle = () => {
348 | const [attackXVel, attackYVel] = rotateVector(attackInputData.xVel, attackInputData.yVel, 45)
349 | const angle = Math.atan2(-attackYVel, attackXVel) - radians(270)
350 | playerJoystickVelocity.targetAngle = angle
351 |
352 | if (prevAngle !== angle) {
353 | ref.current.rotation.y = lerpRadians(prevAngle, angle, 10 * delta)
354 | }
355 | }
356 |
357 | if (!ongoingRoll && (attackInputActive)) {
358 |
359 | applyAttackAngle()
360 |
361 | } else if (isTargetLocked && !ongoingRoll) {
362 |
363 | const targetX = playerPosition.targetX
364 | const targetY = playerPosition.targetY
365 | const angle = Math.atan2((targetX - x), (targetY - y))
366 | ref.current.rotation.y = lerpRadians(prevAngle, angle, 10 * delta)
367 | playerJoystickVelocity.targetAngle = angle
368 |
369 | } else if (!ongoingRoll && (attackInputData.lastReleased > Date.now() - 1000)) {
370 |
371 | applyAttackAngle()
372 |
373 | } else {
374 |
375 | if (isMoving) {
376 | const angle = Math.atan2(-yVel, xVel) - radians(270)
377 | playerJoystickVelocity.targetAngle = angle
378 | }
379 |
380 | if (prevAngle !== playerJoystickVelocity.targetAngle) {
381 | ref.current.rotation.y = lerpRadians(prevAngle, playerJoystickVelocity.targetAngle, 10 * delta)
382 | }
383 |
384 | }
385 |
386 | if (playerVisualState.moving !== isMoving) {
387 | playerVisualState.moving = isMoving
388 | }
389 |
390 | if (playerVisualState.running !== isRunning) {
391 | playerVisualState.running = isRunning
392 | }
393 |
394 | if (energy < 0) {
395 | energy = 0
396 | }
397 |
398 | playerEnergy.energy = energy
399 |
400 | playerPosition.x = x
401 | playerPosition.y = y
402 | playerPosition.angle = ref.current.rotation.y
403 |
404 | largeColliderApi.setAngle(ref.current.rotation.y * -1)
405 |
406 | //console.log('angle', ref.current.rotation.y)
407 |
408 | gl.render(scene, camera)
409 |
410 | }, 100)
411 |
412 | return (
413 | <>
414 |
415 |
416 |
417 |
418 |
419 | >
420 | );
421 | };
422 |
423 | export default Player;
--------------------------------------------------------------------------------
/www/src/game/components/Player/components/PlayerDebug/PlayerDebug.tsx:
--------------------------------------------------------------------------------
1 | import React, {MutableRefObject} from "react";
2 | import {Object3D} from "three";
3 | import {Box, Cylinder} from "@react-three/drei";
4 | import {useProxy} from "valtio";
5 | import {devState} from "../../../../../state/dev";
6 | import {boxSize, largeColliderRadius, smallColliderRadius} from "../../hooks/physics";
7 | import {radians} from "../../../../../utils/angles";
8 |
9 | const PlayerDebug: React.FC<{
10 | largeColliderRef: MutableRefObject,
11 | }> = ({largeColliderRef}) => {
12 |
13 | return null
14 |
15 | return (
16 |
17 |
18 |
20 |
21 |
22 |
24 |
25 |
26 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default PlayerDebug;
--------------------------------------------------------------------------------
/www/src/game/components/Player/components/PlayerUI/PlayerUI.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Html} from "@react-three/drei";
3 | import styled, {css} from "styled-components";
4 | import AttackUI, {AttackContainerSize} from "../../../Game/components/AttackUIContainer/components/AttackUI/AttackUI";
5 | import {useEnemiesInCloseRange, useEnemiesInRange} from "../../../../../state/player";
6 |
7 | const cssSmaller = css`
8 | width: 100px;
9 | height: 100px;
10 | `
11 |
12 | const cssMedium = css`
13 | width: 150px;
14 | height: 150px;
15 | `
16 |
17 | const StyledRegion = styled.div<{
18 | smaller: boolean,
19 | large: boolean,
20 | }>`
21 | width: 200px;
22 | height: 200px;
23 | ${props => props.smaller ? cssSmaller : !props.large ? cssMedium : ''};
24 | `;
25 |
26 | // todo - change region size depending upon whether enemies are nearby or not
27 |
28 | export const containerPortal: {
29 | ref: any,
30 | } = {
31 | ref: null,
32 | }
33 |
34 | const PlayerUI: React.FC = () => {
35 |
36 | const enemiesInRange = useEnemiesInRange()
37 | const enemiesInCloseRange = useEnemiesInCloseRange()
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default PlayerUI;
--------------------------------------------------------------------------------
/www/src/game/components/Player/components/PlayerVisuals/PlayerVisuals.tsx:
--------------------------------------------------------------------------------
1 | import React, {Suspense, useEffect, useState} from "react";
2 | import Knight from "../../../../../3d/models/Knight/Knight";
3 | import {proxy, useProxy} from "valtio";
4 | import {attackState} from "../../hooks/attack";
5 | import {playerHealth, playerState} from "../../../../../state/player";
6 |
7 | export const playerVisualState = proxy({
8 | rollCooldown: false,
9 | rolling: false,
10 | moving: false,
11 | running: false,
12 | })
13 |
14 | const PlayerVisuals: React.FC = () => {
15 |
16 | const {recharging, preRecharging} = useProxy(playerState)
17 | const localPlayerState = useProxy(playerVisualState)
18 | const {lastAttack} = useProxy(attackState)
19 | const {lastDamaged} = useProxy(playerHealth)
20 |
21 | return (
22 |
23 | console.log('down?', event)} onPointerOver={() => console.log('over')} recharging={recharging || preRecharging} lastDamaged={lastDamaged} lastAttack={lastAttack} moving={localPlayerState.moving} running={localPlayerState.running} position={[0, localPlayerState.rolling ? -1.5 : 0, 0]}/>
24 |
25 | );
26 | };
27 |
28 | export default PlayerVisuals;
--------------------------------------------------------------------------------
/www/src/game/components/Player/hooks/attack.ts:
--------------------------------------------------------------------------------
1 | import {useFrame} from "react-three-fiber";
2 | import {proxy, useProxy} from "valtio";
3 | import {InputKeys, inputsState} from "../../../../state/inputs";
4 | import {playerPosition} from "../../../../state/positions";
5 | import {playerVisualState} from "../components/PlayerVisuals/PlayerVisuals";
6 | import {playerTargets} from "../../../../state/player";
7 | import {dealDamageToMob, getMobHealthManager} from "../../../../state/mobs";
8 | import {getMobPosition} from "../../../../temp/ai";
9 | import {attackBuffer, attackInputData} from "../../Game/components/AttackUIContainer/components/AttackUI/AttackUI";
10 |
11 | let attackCount = 0
12 |
13 | export const attackState = proxy({
14 | lastAttack: 0,
15 | })
16 |
17 | export type AttackCollider = {
18 | id: number,
19 | x: number,
20 | y: number,
21 | vX: number,
22 | vY: number,
23 | expires: number,
24 | }
25 |
26 | export const attackColliders = proxy<{
27 | colliders: AttackCollider[],
28 | }>({
29 | colliders: [],
30 | })
31 |
32 | const handleAttack = () => {
33 |
34 | const canAttack = !playerVisualState.rolling
35 |
36 | if (!canAttack) return
37 |
38 | attackState.lastAttack = Date.now()
39 | setTimeout(() => {
40 | const hasEnemies = playerTargets.attackRange.length > 0
41 | if (hasEnemies) {
42 | playerTargets.attackRange.forEach((mobID, index) => {
43 | dealDamageToMob(mobID, index === 0)
44 | })
45 | } else {
46 | attackInputData.nextAvailable = attackInputData.nextAvailable + 350
47 | }
48 | }, 200)
49 |
50 | }
51 |
52 | export const usePlayerAttackHandler = () => {
53 |
54 | useFrame(() => {
55 | if (attackBuffer.length > 0) {
56 | handleAttack()
57 | attackBuffer.length = 0
58 | } else if (inputsState[InputKeys.PUNCH].released) {
59 | handleAttack()
60 | }
61 | }, )
62 |
63 | }
--------------------------------------------------------------------------------
/www/src/game/components/Player/hooks/camera.ts:
--------------------------------------------------------------------------------
1 | import {useFrame, useThree} from "react-three-fiber";
2 | import {useEffect, useLayoutEffect} from "react";
3 | import {gameRefs} from "../../../../state/refs";
4 | import {Vector3} from "three";
5 |
6 | let vector = new Vector3()
7 |
8 | export const usePlayerCamera = (playerRef: any) => {
9 |
10 | }
--------------------------------------------------------------------------------
/www/src/game/components/Player/hooks/collisions.ts:
--------------------------------------------------------------------------------
1 | import {useProxy} from "valtio";
2 | import {playerVisualState} from "../components/PlayerVisuals/PlayerVisuals";
3 | import {useEffect} from "react";
4 | import {BodyApi} from "../../../../physics/components/Physics/hooks";
5 | import {COLLISION_FILTER_GROUPS} from "../../../../physics/collisions/filters";
6 |
7 | export const usePlayerCollisionsHandler = (api: BodyApi) => {
8 |
9 | const rolling = useProxy(playerVisualState).rolling
10 |
11 | useEffect(() => {
12 |
13 | // if (rolling) {
14 | // api.updateBody({
15 | // fixtureUpdate: {
16 | // maskBits: 0
17 | // }
18 | // })
19 | // } else {
20 | // api.updateBody({
21 | // fixtureUpdate: {
22 | // maskBits: COLLISION_FILTER_GROUPS.mob,
23 | // }
24 | // })
25 | // }
26 |
27 | }, [rolling])
28 |
29 | }
--------------------------------------------------------------------------------
/www/src/game/components/Player/hooks/controls.ts:
--------------------------------------------------------------------------------
1 | import {useEffect} from "react";
2 | import hotkeys from "hotkeys-js";
3 | import {useFrame} from "react-three-fiber";
4 | import {InputKeys, inputsState} from "../../../../state/inputs";
5 | import {usePlayerAttackHandler} from "./attack";
6 |
7 | export const usePlayerControls = () => {
8 |
9 | const inputs = Object.values(inputsState)
10 |
11 | useEffect(() => {
12 | hotkeys('*', '', () => {})
13 | }, [])
14 |
15 | useFrame(() => {
16 |
17 | inputs.forEach(inputState => {
18 | let pressed = false
19 | if (inputState.raw !== undefined) {
20 | pressed = inputState.raw
21 | }
22 | inputState.keys.forEach(key => {
23 | if (hotkeys.isPressed(key)) {
24 | pressed = true
25 | }
26 | })
27 | if (pressed) {
28 | if (!inputState.active) {
29 | inputState.active = true
30 | inputState.pressed = true
31 | inputState.released = false
32 | } else if (inputState.pressed) {
33 | inputState.pressed = false
34 | }
35 | } else {
36 | if (inputState.active) {
37 | inputState.active = false
38 | inputState.pressed = false
39 | inputState.released = true
40 | } else if (inputState.released) {
41 | inputState.released = false
42 | }
43 | }
44 | })
45 |
46 | }, 1)
47 |
48 | usePlayerAttackHandler()
49 |
50 | }
--------------------------------------------------------------------------------
/www/src/game/components/Player/hooks/effects.ts:
--------------------------------------------------------------------------------
1 | import {useEffect} from "react";
2 | import {subscribe} from "valtio";
3 | import {playerEnergy} from "../../../../state/player";
4 |
5 | let energyLastUsed = 0
6 | let rechargeInterval: any;
7 | let rechargeTimeout: any;
8 | const rechargeDelay = 1500
9 |
10 | const restartEnergyRechargeProcess = () => {
11 |
12 | energyLastUsed = Date.now()
13 |
14 | if (rechargeInterval) {
15 | clearInterval(rechargeInterval)
16 | rechargeInterval = null
17 | }
18 |
19 | if (!rechargeTimeout) {
20 |
21 | console.log('starting a timeout...')
22 |
23 | const startTimeout = (delay: number) => {
24 |
25 | rechargeTimeout = setTimeout(() => {
26 |
27 | const now = Date.now()
28 |
29 | if (now >= energyLastUsed + rechargeDelay) {
30 |
31 | rechargeInterval = setInterval(() => {
32 |
33 | let energy = playerEnergy.energy
34 |
35 | energy += 7
36 |
37 | if (energy >= 100) {
38 | energy = 100
39 | clearInterval(rechargeInterval)
40 | }
41 |
42 | playerEnergy.energy = energy
43 |
44 | }, 100)
45 |
46 | clearTimeout(rechargeTimeout)
47 | rechargeTimeout = null
48 | } else {
49 | startTimeout((energyLastUsed + rechargeDelay) - now)
50 | }
51 |
52 | }, delay)
53 |
54 | }
55 |
56 | startTimeout(rechargeDelay)
57 |
58 |
59 | }
60 |
61 | }
62 |
63 | export const usePlayerEffectsHandler = () => {
64 |
65 | useEffect(() => {
66 |
67 | let previousEnergy = playerEnergy.energy
68 |
69 | const unsubscribe = subscribe(playerEnergy, () => {
70 | const energy = playerEnergy.energy
71 | if (energy < previousEnergy) {
72 | restartEnergyRechargeProcess()
73 | }
74 | previousEnergy = energy
75 | })
76 | return () => {
77 | unsubscribe()
78 | }
79 |
80 | }, [])
81 |
82 | }
--------------------------------------------------------------------------------
/www/src/game/components/Player/hooks/physics.ts:
--------------------------------------------------------------------------------
1 | import {useBody} from "../../../../physics/components/Physics/hooks";
2 | import {BodyShape, BodyType} from "../../../../physics/bodies";
3 | import {Vec2} from "planck-js";
4 | import {useCallback} from "react";
5 | import {COLLISION_FILTER_GROUPS} from "../../../../physics/collisions/filters";
6 | import {
7 | addToPlayerAttackRange,
8 | addToPlayerCloseRange, addToPlayerFocusedRange,
9 | playerTargets, removeFromPlayerAttackRange,
10 | removeFromPlayerCloseRange, removeFromPlayerFocusedRange,
11 | removePlayerFromRange
12 | } from "../../../../state/player";
13 | import {FixtureType} from "../../../../physics/collisions/types";
14 | import {MOB_VARIANT} from "../../Mob/data";
15 |
16 | export const largeColliderRadius = 12
17 | export const smallColliderRadius = 4.5
18 | export const boxSize = {
19 | width: 2.5,
20 | length: 2.2,
21 | offset: 1.1,
22 | }
23 |
24 | const tempVec2 = Vec2(0, 0)
25 |
26 | export const usePlayerPhysics = () => {
27 |
28 | const onCollideStart = useCallback(() => {
29 | // console.log('player collide start')
30 | }, [])
31 |
32 | const onCollideEnd = useCallback(() => {
33 | // console.log('player collide end')
34 | }, [])
35 |
36 | const onLargeCollideStart = useCallback((data: any, fixtureIndex: number) => {
37 | const {mobID, type, mobVariant} = data
38 | if (type === FixtureType.MOB) {
39 | if (fixtureIndex === 0) {
40 | playerTargets.inRange.push(mobID)
41 | if (mobVariant === MOB_VARIANT.large) {
42 | addToPlayerFocusedRange(mobID)
43 | }
44 | } else if (fixtureIndex === 1) {
45 | addToPlayerCloseRange(mobID)
46 | } else if (fixtureIndex === 2) {
47 | addToPlayerAttackRange(mobID)
48 | }
49 | }
50 | }, [])
51 |
52 | const onLargeCollideEnd = useCallback(({mobID, mobVariant}: {mobID: number, mobVariant: MOB_VARIANT}, fixtureIndex: number) => {
53 | if (fixtureIndex === 0) {
54 | if (mobVariant === MOB_VARIANT.large) {
55 | removeFromPlayerFocusedRange(mobID)
56 | }
57 | removePlayerFromRange(mobID)
58 | } else if (fixtureIndex === 1) {
59 | removeFromPlayerCloseRange(mobID)
60 | } else if (fixtureIndex === 2) {
61 | removeFromPlayerAttackRange(mobID)
62 | }
63 | }, [])
64 |
65 | const [ref, api] = useBody(() => ({
66 | type: BodyType.dynamic,
67 | position: Vec2(0, 0),
68 | linearDamping: 4,
69 | fixtures: [{
70 | shape: BodyShape.circle,
71 | radius: 0.75,
72 | fixtureOptions: {
73 | density: 20,
74 | filterCategoryBits: COLLISION_FILTER_GROUPS.player | COLLISION_FILTER_GROUPS.physical,
75 | }
76 | }],
77 | }), {
78 | onCollideStart,
79 | onCollideEnd
80 | })
81 |
82 | const [largeColliderRef, largeColliderApi] = useBody(() => ({
83 | type: BodyType.dynamic,
84 | fixedRotation: false,
85 | position: Vec2(0, 0),
86 | fixtures: [
87 | {
88 | shape: BodyShape.circle,
89 | radius: largeColliderRadius,
90 | fixtureOptions: {
91 | isSensor: true,
92 | filterCategoryBits: COLLISION_FILTER_GROUPS.playerTrigger,
93 | filterMaskBits: COLLISION_FILTER_GROUPS.mob,
94 | userData: {
95 | type: FixtureType.PLAYER_RANGE,
96 | }
97 | },
98 | }, {
99 | shape: BodyShape.circle,
100 | radius: smallColliderRadius,
101 | fixtureOptions: {
102 | isSensor: true,
103 | filterCategoryBits: COLLISION_FILTER_GROUPS.playerTrigger,
104 | filterMaskBits: COLLISION_FILTER_GROUPS.mob,
105 | },
106 | }, {
107 | shape: BodyShape.box,
108 | hx: boxSize.width,
109 | hy: boxSize.length,
110 | center: [0, boxSize.offset],
111 | fixtureOptions: {
112 | isSensor: true,
113 | filterCategoryBits: COLLISION_FILTER_GROUPS.playerTrigger,
114 | filterMaskBits: COLLISION_FILTER_GROUPS.mob,
115 | },
116 | }],
117 | }), {
118 | applyAngle: true,
119 | onCollideStart: onLargeCollideStart,
120 | onCollideEnd: onLargeCollideEnd,
121 | })
122 |
123 | return [ref, api, largeColliderRef, largeColliderApi]
124 |
125 | }
--------------------------------------------------------------------------------
/www/src/game/components/Player/hooks/state.ts:
--------------------------------------------------------------------------------
1 | import {useProxy} from "valtio";
2 | import {playerHealth, playerState} from "../../../../state/player";
3 | import {useEffect} from "react";
4 |
5 | export const usePlayerStateHandler = () => {
6 |
7 | const {lastDamaged, health} = useProxy(playerHealth)
8 |
9 | useEffect(() => {
10 |
11 | if (lastDamaged > 0) {
12 |
13 | playerState.invincible = true
14 |
15 | const timeout = setTimeout(() => {
16 |
17 | playerState.invincible = false
18 |
19 | }, 1000)
20 |
21 | return () => {
22 | clearTimeout(timeout)
23 | }
24 |
25 | }
26 |
27 | }, [lastDamaged])
28 |
29 | }
--------------------------------------------------------------------------------
/www/src/game/components/Room/Room.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import {RoomDirection, roomWalls} from "../../../temp/rooms";
3 | import RoomWall from "./components/RoomWall/RoomWall";
4 | import {useBody} from "../../../physics/components/Physics/hooks";
5 | import {BodyShape, BodyType} from "../../../physics/bodies";
6 | import {Vec2} from "planck-js";
7 | import {COLLISION_FILTER_GROUPS} from "../../../physics/collisions/filters";
8 | import {radians} from "../../../utils/angles";
9 |
10 | const Room: React.FC = () => {
11 |
12 | const [x, y] = [5, 5]
13 |
14 | const walls = roomWalls
15 |
16 | useBody(() => ({
17 | type: BodyType.static,
18 | position: Vec2(x, y),
19 | fixtures: walls.map((wall) => {
20 | const horizontal = wall.direction === RoomDirection.NORTH || wall.direction === RoomDirection.SOUTH
21 | const xLength = horizontal ? Math.abs(wall.start[0] - wall.end[0]) : 1.2
22 | const yLength = horizontal ? 1.2 : Math.abs(wall.start[1] - wall.end[1])
23 | let xStart = wall.start[0] + xLength / 2
24 | let yStart = wall.start[1] + yLength / 2
25 | switch (wall.direction) {
26 | case RoomDirection.NORTH:
27 | yStart -= 1.25
28 | break
29 | case RoomDirection.EAST:
30 | xStart -= 1.25
31 | break
32 | case RoomDirection.SOUTH:
33 | yStart += 0
34 | break
35 | case RoomDirection.WEST:
36 | xStart += 0
37 | break
38 | }
39 | return {
40 | shape: BodyShape.box,
41 | hx: xLength,
42 | hy: yLength,
43 | center: [xStart, yStart],
44 | fixtureOptions: {
45 | filterCategoryBits: COLLISION_FILTER_GROUPS.barrier,
46 | filterMaskBits: COLLISION_FILTER_GROUPS.physical,
47 | }
48 | }
49 | }),
50 | }), {})
51 |
52 | return (
53 |
54 | {walls.map((wall, index) => (
55 |
56 | ))}
57 |
58 | )
59 | }
60 |
61 | export default Room
--------------------------------------------------------------------------------
/www/src/game/components/Room/components/RoomWall/RoomWall.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@react-three/drei";
2 | import React, {Suspense} from "react"
3 | import {RoomDirection, RoomWallMdl} from "../../../../../temp/rooms";
4 | import {radians} from "../../../../../utils/angles";
5 | import Wall from "../../../../../3d/models/Wall/Wall";
6 |
7 | const IndividualWall: React.FC<{
8 | index: number,
9 | position: number,
10 | vertical: boolean,
11 | }> = ({position, index, vertical}) => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | const RoomWall: React.FC<{
25 | wall: RoomWallMdl,
26 | index: number,
27 | }> = ({wall, index}) => {
28 | const horizontal = wall.start[0] !== wall.end[0];
29 | let wallLength = Math.abs(
30 | wall.start[horizontal ? 0 : 1] - wall.end[horizontal ? 0 : 1]
31 | );
32 | const numberOfWalls = Math.ceil(wallLength / 4);
33 | const wallRemainder = 4 - (wallLength % 4);
34 | let xStart = wall.start[0]
35 | let yStart = horizontal ? wall.start[1] : wall.start[1] + wallLength
36 | switch (wall.direction) {
37 | case RoomDirection.NORTH:
38 | yStart -= 0.725
39 | break
40 | case RoomDirection.EAST:
41 | xStart -= 0.725
42 | break
43 | case RoomDirection.SOUTH:
44 | yStart += 0.725
45 | break
46 | case RoomDirection.WEST:
47 | xStart += 0.725
48 | break
49 | }
50 | return (
51 |
52 | {Array.from({ length: numberOfWalls }).map((_, wallIndex) => {
53 | let position = wallIndex * 4;
54 | if (wallIndex === numberOfWalls - 1 && wallRemainder !== 4) {
55 | position -= wallRemainder;
56 | }
57 | return ;
58 | })}
59 |
60 | )
61 | }
62 |
63 | export default RoomWall
--------------------------------------------------------------------------------
/www/src/game/components/TestBox/TestBox.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from "react";
2 | import {boxSize} from "../Player/hooks/physics";
3 | import {radians} from "../../../utils/angles";
4 | import {Box} from "@react-three/drei";
5 | import {useBody} from "../../../physics/components/Physics/hooks";
6 | import {BodyShape, BodyType} from "../../../physics/bodies";
7 | import {Vec2} from "planck-js";
8 | import {COLLISION_FILTER_GROUPS} from "../../../physics/collisions/filters";
9 |
10 | const TestBox: React.FC = () => {
11 |
12 | const [ref, api] = useBody(() => ({
13 | type: BodyType.dynamic,
14 | fixedRotation: false,
15 | position: Vec2(0, 0),
16 | fixtures: [
17 | {
18 | shape: BodyShape.box,
19 | hx: boxSize.width,
20 | hy: boxSize.length,
21 | center: [0, boxSize.offset],
22 | fixtureOptions: {
23 | isSensor: true,
24 | filterMaskBits: COLLISION_FILTER_GROUPS.player,
25 | },
26 | }
27 | ]
28 | }), {
29 | applyAngle: true,
30 | onCollideStart: () => console.log('collision start'),
31 | onCollideEnd: () => console.log('collision end'),
32 | })
33 |
34 | useEffect(() => {
35 | api.setAngle(radians(45))
36 | }, [])
37 |
38 | return (
39 |
40 |
42 |
43 | );
44 | };
45 |
46 | export default TestBox;
--------------------------------------------------------------------------------
/www/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/www/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import reportWebVitals from './reportWebVitals';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
13 | // If you want to start measuring performance in your app, pass a function
14 | // to log results (for example: reportWebVitals(console.log))
15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
16 | reportWebVitals();
17 |
--------------------------------------------------------------------------------
/www/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/www/src/mobs/components/LargeMob/LargeMob.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Mob from "../../../game/components/Mob/Mob";
3 | import {MOB_VARIANT} from "../../../game/components/Mob/data";
4 |
5 | const LargeMob: React.FC = () => {
6 | return (
7 |
8 | );
9 | };
10 |
11 | export default LargeMob;
--------------------------------------------------------------------------------
/www/src/physics/bodies.ts:
--------------------------------------------------------------------------------
1 | import {BodyDef, Body, Box, Circle, FixtureOpt, Vec2} from "planck-js";
2 | import {dynamicBodiesUuids, existingBodies, planckWorld} from "../shared";
3 | import {Shape} from "planck-js/lib/shape";
4 | import {syncBodies} from "../workers/physics/functions";
5 | import {activeCollisionListeners} from "./collisions/data";
6 | import {addCachedBody, getCachedBody, PhysicsCacheKeys} from "./cache";
7 |
8 | export enum BodyType {
9 | static = 'static',
10 | kinematic = 'kinematic',
11 | dynamic = 'dynamic'
12 | }
13 |
14 | export enum BodyShape {
15 | box = 'box',
16 | circle = 'circle',
17 | }
18 |
19 | type BasicBodyProps = Partial & {
20 | fixtures: {
21 | shape: BodyShape,
22 | fixtureOptions: Partial,
23 | hx?: number,
24 | hy?: number,
25 | center?: [number, number],
26 | radius?: number,
27 | }[],
28 | }
29 |
30 | type AddBoxBodyProps = BasicBodyProps & {
31 | }
32 |
33 | type AddCircleBodyProps = BasicBodyProps & {
34 | }
35 |
36 | export type AddBodyDef = BasicBodyProps | AddBoxBodyProps | AddCircleBodyProps
37 |
38 | export type AddBodyProps = AddBodyDef & {
39 | uuid: string,
40 | listenForCollisions: boolean,
41 | cacheKey?: PhysicsCacheKeys
42 | }
43 |
44 | // todo - add support for multiple fixtures...
45 |
46 | export const addBody = ({uuid, cacheKey, listenForCollisions, fixtures = [], ...props}: AddBodyProps) => {
47 |
48 | const existingBody = existingBodies.get(uuid)
49 |
50 | if (existingBody) {
51 | return existingBody
52 | }
53 |
54 | if (listenForCollisions) {
55 | activeCollisionListeners[uuid] = true
56 | }
57 |
58 | /*
59 | fixtureOptions = {
60 | userData: {
61 | uuid,
62 | ...fixtureOptions?.userData
63 | },
64 | ...fixtureOptions,
65 | }
66 | */
67 |
68 | const bodyDef: BodyDef = {
69 | type: BodyType.static,
70 | fixedRotation: true,
71 | ...props,
72 | }
73 |
74 | const {type} = bodyDef
75 |
76 | let body: Body | null = null;
77 |
78 | if (cacheKey) {
79 | const cachedBody = getCachedBody(cacheKey)
80 | if (cachedBody) {
81 |
82 | if (fixtures && fixtures.length > 0) {
83 |
84 | let bodyFixture = cachedBody.getFixtureList()
85 |
86 | fixtures.forEach((fixture, fixtureIndex) => {
87 |
88 | let fixtureOptions = fixture.fixtureOptions
89 |
90 | fixtureOptions = {
91 | userData: {
92 | uuid,
93 | fixtureIndex,
94 | ...fixtureOptions?.userData
95 | },
96 | ...fixtureOptions,
97 | }
98 |
99 | if (bodyFixture) {
100 |
101 | if (fixtureOptions) {
102 | bodyFixture.setUserData(fixtureOptions.userData)
103 | }
104 |
105 | bodyFixture = bodyFixture.getNext()
106 | }
107 |
108 | })
109 |
110 | }
111 |
112 | const {position, angle} = props
113 |
114 | if (position) {
115 | cachedBody.setPosition(position)
116 | }
117 |
118 | if (angle) {
119 | cachedBody.setAngle(angle)
120 | }
121 |
122 | cachedBody.setActive(true)
123 |
124 | body = cachedBody
125 |
126 | }
127 | }
128 |
129 | if (!body) {
130 |
131 | body = planckWorld.createBody(bodyDef)
132 |
133 | if (fixtures && fixtures.length > 0) {
134 |
135 | fixtures.forEach(({shape, fixtureOptions, hx, hy, radius, center}, fixtureIndex) => {
136 |
137 | fixtureOptions = {
138 | ...fixtureOptions,
139 | userData: {
140 | uuid,
141 | fixtureIndex,
142 | ...fixtureOptions?.userData
143 | },
144 | }
145 |
146 | let bodyShape: Shape;
147 |
148 | switch (shape) {
149 | case BodyShape.box:
150 | bodyShape = Box((hx as number) / 2, (hy as number) / 2, center ? Vec2(center[0], center[1]): undefined) as unknown as Shape
151 | break;
152 | case BodyShape.circle:
153 | bodyShape = Circle((radius as number)) as unknown as Shape
154 | break;
155 | default:
156 | throw new Error(`Unhandled body shape ${shape}`)
157 | }
158 |
159 | if (fixtureOptions) {
160 | if (body) {
161 | body.createFixture(bodyShape, fixtureOptions as FixtureOpt)
162 | }
163 | } else {
164 | if (body) {
165 | body.createFixture(bodyShape)
166 | }
167 | }
168 |
169 | })
170 |
171 |
172 | }
173 |
174 | }
175 |
176 | if (type !== BodyType.static) {
177 | dynamicBodiesUuids.push(uuid)
178 | syncBodies()
179 | }
180 |
181 | existingBodies.set(uuid, body)
182 |
183 | return body
184 |
185 | }
186 |
187 | export type RemoveBodyProps = {
188 | uuid: string,
189 | cacheKey?: PhysicsCacheKeys
190 | }
191 |
192 | const tempVec = Vec2(0, 0)
193 |
194 | export const removeBody = ({uuid, cacheKey}: RemoveBodyProps) => {
195 | const index = dynamicBodiesUuids.indexOf(uuid)
196 | if (index > -1) {
197 | dynamicBodiesUuids.splice(index, 1)
198 | syncBodies()
199 | }
200 | const body = existingBodies.get(uuid)
201 | if (!body) {
202 | console.warn(`Body not found for ${uuid}`)
203 | return
204 | }
205 | existingBodies.delete(uuid)
206 | if (cacheKey) {
207 | tempVec.set(-1000, -1000)
208 | body.setPosition(tempVec)
209 | tempVec.set(0, 0)
210 | body.setLinearVelocity(tempVec)
211 | body.setActive(false)
212 | addCachedBody(cacheKey, body)
213 | } else {
214 | planckWorld.destroyBody(body)
215 | }
216 | }
217 |
218 | export type SetBodyProps = {
219 | uuid: string,
220 | method: string,
221 | methodParams: any[],
222 | }
223 |
224 | export const setBody = ({uuid, method, methodParams}: SetBodyProps) => {
225 | const body = existingBodies.get(uuid)
226 | if (!body) {
227 | console.warn(`Body not found for ${uuid}`)
228 | return
229 | }
230 | switch (method) {
231 | //case 'setAngle':
232 | // const [angle] = methodParams
233 | // body.setTransform(body.getPosition(), angle)
234 | // break;
235 | case 'setLinearVelocity':
236 | // console.log('methodParams', methodParams[0].x, methodParams[0].y);
237 | (body as any)[method](...methodParams)
238 | break;
239 | default:
240 | (body as any)[method](...methodParams)
241 | }
242 | }
243 |
244 | export type UpdateBodyData = {
245 | fixtureUpdate?: {
246 | groupIndex?: number,
247 | categoryBits?: number,
248 | maskBits?: number,
249 | }
250 | }
251 |
252 | export type UpdateBodyProps = {
253 | uuid: string,
254 | data: UpdateBodyData,
255 | }
256 |
257 | export const updateBody = ({uuid, data}: UpdateBodyProps) => {
258 | const body = existingBodies.get(uuid)
259 | if (!body) {
260 | console.warn(`Body not found for ${uuid}`)
261 | return
262 | }
263 | const {fixtureUpdate} = data
264 | if (fixtureUpdate) {
265 | const fixture = body.getFixtureList()
266 | if (fixture) {
267 | const {
268 | groupIndex,
269 | categoryBits,
270 | maskBits
271 | } = fixtureUpdate
272 | if (
273 | groupIndex !== undefined || categoryBits !== undefined || maskBits !== undefined
274 | ) {
275 | const originalGroupIndex = fixture.getFilterGroupIndex()
276 | const originalCategoryBits = fixture.getFilterCategoryBits()
277 | const originalMaskBits = fixture.getFilterMaskBits()
278 | fixture.setFilterData({
279 | groupIndex: groupIndex !== undefined ? groupIndex : originalGroupIndex,
280 | categoryBits: categoryBits !== undefined ? categoryBits : originalCategoryBits,
281 | maskBits: maskBits !== undefined ? maskBits : originalMaskBits,
282 | })
283 | }
284 | }
285 | }
286 | }
--------------------------------------------------------------------------------
/www/src/physics/cache.ts:
--------------------------------------------------------------------------------
1 | import {Body} from "planck-js";
2 |
3 | export enum PhysicsCacheKeys {
4 | PUNCH = 'PUNCH'
5 | }
6 |
7 | export const cachedBodies: {
8 | [key: string]: Body[],
9 | } = {}
10 |
11 | export const getCachedBody = (key: PhysicsCacheKeys): Body | null => {
12 | const bodies = cachedBodies[key]
13 | if (bodies && bodies.length > 0) {
14 | const body = bodies.pop()
15 | if (body) {
16 | return body
17 | }
18 | }
19 | return null
20 | }
21 |
22 | export const addCachedBody = (key: PhysicsCacheKeys, body: Body) => {
23 | if (cachedBodies[key]) {
24 | cachedBodies[key].push(body)
25 | } else {
26 | cachedBodies[key] = [body]
27 | }
28 | }
--------------------------------------------------------------------------------
/www/src/physics/collisions/collisions.ts:
--------------------------------------------------------------------------------
1 | import {Fixture} from "planck-js";
2 | import {FixtureUserData} from "./types";
3 | import {sendCollisionBeginEvent, sendCollisionEndEvent} from "../../workers/physics/functions";
4 | import {activeCollisionListeners} from "./data";
5 |
6 | const getFixtureData = (fixture: Fixture): FixtureUserData | null => {
7 | const userData = fixture.getUserData() as null | FixtureUserData
8 | return userData || null
9 | }
10 |
11 | const getFixtureUuid = (data: FixtureUserData | null): string => {
12 | if (data && data['uuid']) {
13 | return data.uuid
14 | }
15 | return ''
16 | }
17 |
18 | const getFixtureIndex = (data: FixtureUserData | null): number => {
19 | if (data) {
20 | return data.fixtureIndex
21 | }
22 | return -1
23 | }
24 |
25 | export const handleBeginCollision = (fixtureA: Fixture, fixtureB: Fixture) => {
26 | const aData = getFixtureData(fixtureA)
27 | const bData = getFixtureData(fixtureB)
28 | const aUUID = getFixtureUuid(aData)
29 | const bUUID = getFixtureUuid(bData)
30 |
31 | if (aUUID && activeCollisionListeners[aUUID]) {
32 | sendCollisionBeginEvent(aUUID, bData, getFixtureIndex(aData))
33 | }
34 |
35 | if (bUUID && activeCollisionListeners[bUUID]) {
36 | sendCollisionBeginEvent(bUUID, aData, getFixtureIndex(bData))
37 | }
38 | }
39 |
40 | export const handleEndCollision = (fixtureA: Fixture, fixtureB: Fixture) => {
41 | const aData = getFixtureData(fixtureA)
42 | const bData = getFixtureData(fixtureB)
43 | const aUUID = getFixtureUuid(aData)
44 | const bUUID = getFixtureUuid(bData)
45 |
46 | if (aUUID && activeCollisionListeners[aUUID]) {
47 | sendCollisionEndEvent(aUUID, bData, getFixtureIndex(aData))
48 | }
49 |
50 | if (bUUID && activeCollisionListeners[bUUID]) {
51 | sendCollisionEndEvent(bUUID, aData, getFixtureIndex(bData))
52 | }
53 | }
--------------------------------------------------------------------------------
/www/src/physics/collisions/data.ts:
--------------------------------------------------------------------------------
1 | export const activeCollisionListeners: {
2 | [uuid: string]: boolean,
3 | } = {}
--------------------------------------------------------------------------------
/www/src/physics/collisions/filters.ts:
--------------------------------------------------------------------------------
1 | export const COLLISION_FILTER_GROUPS = {
2 | player: 0x0001,
3 | playerTrigger: 0x0002,
4 | mob: 0x0004,
5 | attackCollider: 0x0008,
6 | attackReceiver: 0x0016,
7 | barrier: 0x0032,
8 | physical: 0x0064,
9 | }
--------------------------------------------------------------------------------
/www/src/physics/collisions/types.ts:
--------------------------------------------------------------------------------
1 |
2 | export enum FixtureType {
3 | PLAYER_RANGE,
4 | MOB
5 | }
6 |
7 | export type FixtureUserData = {
8 | uuid: string,
9 | fixtureIndex: number,
10 | type: FixtureType,
11 | [key: string]: any,
12 | }
--------------------------------------------------------------------------------
/www/src/physics/components/Physics/Physics.tsx:
--------------------------------------------------------------------------------
1 | import React, {ReactElement, useEffect} from "react";
2 | import {gamePhysicsWorker} from "./worker";
3 | import {buffers, handleBeginCollision, handleEndCollision, storedPhysicsData} from "./data";
4 | import {WorkerMessageType, WorkerOwnerMessageType} from "../../../workers/physics/types";
5 |
6 | const Physics: React.FC = ({children}) => {
7 |
8 | useEffect(() => {
9 |
10 | const loop = () => {
11 | if(buffers.positions.byteLength !== 0 && buffers.angles.byteLength !== 0) {
12 | gamePhysicsWorker.postMessage({ type: WorkerMessageType.STEP, ...buffers }, [buffers.positions.buffer, buffers.angles.buffer])
13 | }
14 | }
15 |
16 | gamePhysicsWorker.postMessage({
17 | type: WorkerMessageType.INIT,
18 | props: {
19 | }
20 | })
21 |
22 | loop()
23 |
24 | gamePhysicsWorker.onmessage = (event: MessageEvent) => {
25 |
26 | const type = event.data.type
27 |
28 | switch (type) {
29 | case WorkerOwnerMessageType.FRAME:
30 |
31 | if (event.data.bodies) {
32 | storedPhysicsData.bodies = event.data.bodies.reduce(
33 | (acc: { [key: string]: number }, id: string) => ({
34 | ...acc,
35 | [id]: (event.data as any).bodies.indexOf(id)
36 | }),
37 | {}
38 | )
39 | }
40 |
41 | const positions = event.data.positions as Float32Array
42 | const angles = event.data.angles as Float32Array
43 | buffers.positions = positions
44 | buffers.angles = angles
45 | requestAnimationFrame(loop);
46 | break
47 | case WorkerOwnerMessageType.SYNC_BODIES:
48 | storedPhysicsData.bodies = event.data.bodies.reduce(
49 | (acc: { [key: string]: number }, id: string) => ({
50 | ...acc,
51 | [id]: (event.data as any).bodies.indexOf(id)
52 | }),
53 | {}
54 | )
55 | break
56 | case WorkerOwnerMessageType.BEGIN_COLLISION:
57 | handleBeginCollision(event.data.props as any)
58 | break
59 | case WorkerOwnerMessageType.END_COLLISION:
60 | handleEndCollision(event.data.props as any)
61 | break
62 | }
63 |
64 | }
65 |
66 | }, [])
67 |
68 | return children as ReactElement;
69 | };
70 |
71 | export default Physics;
--------------------------------------------------------------------------------
/www/src/physics/components/Physics/data.ts:
--------------------------------------------------------------------------------
1 | import {Object3D} from "three";
2 |
3 | export type Buffers = { positions: Float32Array; angles: Float32Array }
4 |
5 | export const maxNumberOfDynamicPhysicObjects = 100
6 |
7 | export const buffers = {
8 | positions: new Float32Array(maxNumberOfDynamicPhysicObjects * 2),
9 | angles: new Float32Array(maxNumberOfDynamicPhysicObjects),
10 | }
11 |
12 | export const collisionStartedEvents: {
13 | [key: string]: (data: any, fixtureIndex: number) => void,
14 | } = {}
15 |
16 | export const collisionEndedEvents: {
17 | [key: string]: (data: any, fixtureIndex: number) => void,
18 | } = {}
19 |
20 | export type CollisionEventProps = {
21 | uuid: string,
22 | fixtureIndex: number,
23 | data: {
24 | uuid: string,
25 | }
26 | }
27 |
28 | export const handleBeginCollision = (data: CollisionEventProps) => {
29 | if (collisionStartedEvents[data.uuid]) {
30 | collisionStartedEvents[data.uuid](data.data, data.fixtureIndex)
31 | }
32 | }
33 |
34 | export const handleEndCollision = (data: CollisionEventProps) => {
35 | if (collisionEndedEvents[data.uuid]) {
36 | collisionEndedEvents[data.uuid](data.data, data.fixtureIndex)
37 | }
38 | }
39 |
40 | export const storedPhysicsData: {
41 | bodies: {
42 | [uuid: string]: number,
43 | }
44 | } = {
45 | bodies: {},
46 | }
47 |
48 | export const applyPositionAngle = (object: Object3D | null, index: number, applyAngle: boolean = false, debug?: string) => {
49 | if (index !== undefined && buffers.positions.length && !!object) {
50 | const start = index * 2
51 | const position = buffers.positions.slice(start, start + 2)
52 | object.position.x = position[0]
53 | object.position.z = position[1]
54 | if (debug) {
55 | // console.log('debug', debug, position)
56 | }
57 | if (applyAngle) {
58 | object.rotation.y = buffers.angles[index] * -1
59 | }
60 | } else {
61 | // console.warn('no match?')
62 | }
63 | }
--------------------------------------------------------------------------------
/www/src/physics/components/Physics/hooks.ts:
--------------------------------------------------------------------------------
1 | import {Object3D} from "three";
2 | import {MutableRefObject, useLayoutEffect, useMemo, useRef, useState} from "react";
3 | import {workerAddBody, workerRemoveBody, workerSetBody, workerUpdateBody} from "./worker";
4 | import {AddBodyDef, BodyType, UpdateBodyData} from "../../bodies";
5 | import {Vec2} from "planck-js";
6 | import {useFrame} from "react-three-fiber";
7 | import {applyPositionAngle, buffers, collisionEndedEvents, collisionStartedEvents, storedPhysicsData} from "./data";
8 | import {PhysicsCacheKeys} from "../../cache";
9 |
10 | export type BodyApi = {
11 | applyForceToCenter: (vec: Vec2) => void,
12 | applyLinearImpulse: (vec: Vec2, pos: Vec2) => void,
13 | setPosition: (vec: Vec2) => void,
14 | setLinearVelocity: (vec: Vec2) => void,
15 | setAngle: (angle: number) => void,
16 | updateBody: (data: UpdateBodyData) => void,
17 | }
18 |
19 | export const useBody = (propsFn: () => AddBodyDef, {
20 | applyAngle = false,
21 | cacheKey,
22 | uuid: passedUUID,
23 | fwdRef,
24 | onCollideEnd,
25 | onCollideStart,
26 | debug
27 | }: {
28 | applyAngle?: boolean,
29 | cacheKey?: PhysicsCacheKeys,
30 | uuid?: string,
31 | fwdRef?: MutableRefObject,
32 | onCollideStart?: (data: any, fixtureIndex: number) => void,
33 | onCollideEnd?: (data: any, fixtureIndex: number) => void,
34 | debug?: string
35 | }): [any, BodyApi] => {
36 | const localRef = useRef((null as unknown) as Object3D)
37 | const ref = fwdRef ? fwdRef : localRef
38 | const [uuid] = useState(() => {
39 | if (passedUUID) return passedUUID
40 | if (!ref.current) {
41 | ref.current = new Object3D()
42 | }
43 | return ref.current.uuid
44 | })
45 | const [isDynamic] = useState(() => {
46 | const props = propsFn()
47 | return props.type !== BodyType.static
48 | })
49 |
50 | useLayoutEffect(() => {
51 |
52 | const props = propsFn()
53 |
54 | ref.current.position.x = props.position?.x || 0
55 | ref.current.position.z = props.position?.y || 0
56 |
57 | const listenForCollisions = !!onCollideStart || !!onCollideEnd
58 |
59 | if (listenForCollisions) {
60 | collisionStartedEvents[uuid] = onCollideStart ? onCollideStart : () => {}
61 | collisionEndedEvents[uuid] = onCollideEnd ? onCollideEnd : () => {}
62 | }
63 |
64 | workerAddBody({
65 | uuid,
66 | listenForCollisions,
67 | cacheKey,
68 | ...props,
69 | })
70 |
71 | return () => {
72 |
73 | if (listenForCollisions) {
74 | delete collisionStartedEvents[uuid]
75 | delete collisionEndedEvents[uuid]
76 | }
77 |
78 | workerRemoveBody({uuid, cacheKey})
79 | }
80 |
81 | }, [])
82 |
83 | useFrame(() => {
84 | if (!isDynamic) {
85 | return
86 | }
87 | if (ref.current && buffers.positions.length && buffers.angles.length) {
88 | const index = storedPhysicsData.bodies[uuid]
89 | applyPositionAngle(ref.current, index, applyAngle, debug)
90 | }
91 | })
92 |
93 | const api = useMemo(() => {
94 |
95 | const getUUID = () => uuid
96 |
97 | return {
98 | applyForceToCenter: (vec) => {
99 | workerSetBody({uuid: getUUID(), method: 'applyForceToCenter', methodParams: [vec, true]})
100 | },
101 | applyLinearImpulse: (vec, pos) => {
102 | workerSetBody({uuid: getUUID(), method: 'applyLinearImpulse', methodParams: [vec, pos, true]})
103 | },
104 | setPosition: (vec) => {
105 | workerSetBody({uuid: getUUID(), method: 'setPosition', methodParams: [vec]})
106 | },
107 | setLinearVelocity: (vec) => {
108 | workerSetBody({uuid: getUUID(), method: 'setLinearVelocity', methodParams: [vec]})
109 | },
110 | updateBody: (data: UpdateBodyData) => {
111 | workerUpdateBody({uuid: getUUID(), data})
112 | },
113 | setAngle: (angle: number) => {
114 | workerSetBody({uuid: getUUID(), method: 'setAngle', methodParams: [angle]})
115 | }
116 | }
117 | }, [])
118 |
119 | return [ref, api]
120 | }
--------------------------------------------------------------------------------
/www/src/physics/components/Physics/worker.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-webpack-loader-syntax */
2 | import Worker from "worker-loader!../../../workers/physics/physicsWorker";
3 | import {WorkerMessageType} from "../../../workers/physics/types";
4 | import {AddBodyProps, RemoveBodyProps, SetBodyProps, UpdateBodyProps} from "../../bodies";
5 |
6 | export const gamePhysicsWorker = new Worker()
7 |
8 | export const workerAddBody = (props: AddBodyProps) => {
9 | gamePhysicsWorker.postMessage({
10 | type: WorkerMessageType.ADD_BODY,
11 | props: props
12 | })
13 | }
14 |
15 | export const workerRemoveBody = (props: RemoveBodyProps) => {
16 | gamePhysicsWorker.postMessage({
17 | type: WorkerMessageType.REMOVE_BODY,
18 | props
19 | })
20 | }
21 |
22 | export const workerSetBody = (props: SetBodyProps) => {
23 | gamePhysicsWorker.postMessage({
24 | type: WorkerMessageType.SET_BODY,
25 | props,
26 | })
27 | }
28 |
29 | export const workerUpdateBody = (props: UpdateBodyProps) => {
30 | gamePhysicsWorker.postMessage({
31 | type: WorkerMessageType.UPDATE_BODY,
32 | props,
33 | })
34 | }
--------------------------------------------------------------------------------
/www/src/physics/world.ts:
--------------------------------------------------------------------------------
1 | import {dynamicBodiesUuids, existingBodies, planckWorld} from "../shared";
2 | import {handleBeginCollision, handleEndCollision} from "./collisions/collisions";
3 |
4 | let lastUpdate = 0
5 |
6 | export const stepWorld = (positions: Float32Array, angles: Float32Array) => {
7 |
8 | var now = Date.now();
9 | var delta = !lastUpdate ? 1 / 60 : (now - lastUpdate) / 1000;
10 | planckWorld.step(delta)
11 | lastUpdate = now;
12 |
13 | dynamicBodiesUuids.forEach((uuid, index) => {
14 | const body = existingBodies.get(uuid)
15 | if (!body) return
16 | const position = body.getPosition()
17 | const angle = body.getAngle()
18 | const velocity = body.getLinearVelocity()
19 | positions[2 * index + 0] = position.x
20 | positions[2 * index + 1] = position.y
21 | angles[index] = angle
22 | })
23 |
24 | }
25 |
26 | export const initPhysicsListeners = () => {
27 |
28 | planckWorld.on("begin-contact", (contact) => {
29 | const fixtureA = contact.getFixtureA()
30 | const fixtureB = contact.getFixtureB()
31 | handleBeginCollision(fixtureA, fixtureB)
32 | })
33 |
34 | planckWorld.on("end-contact", (contact) => {
35 | const fixtureA = contact.getFixtureA();
36 | const fixtureB = contact.getFixtureB();
37 | handleEndCollision(fixtureA, fixtureB)
38 | })
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/www/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/www/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | }
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/www/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/www/src/shared.ts:
--------------------------------------------------------------------------------
1 | import {World, Vec2, Body} from "planck-js";
2 |
3 | export const planckWorld = World({
4 | allowSleep: true,
5 | gravity: Vec2(0, 0),
6 | })
7 |
8 |
9 | export let unsyncedBodies = false
10 | export let bodiesLastUpdated = 0
11 |
12 | export const updateBodiesLastUpdated = () => {
13 | bodiesLastUpdated = Date.now()
14 | unsyncedBodies = true
15 | }
16 |
17 | export const setBodiesSynced = () => {
18 | unsyncedBodies = false
19 | }
20 |
21 | export const dynamicBodiesUuids: string[] = []
22 |
23 | export const existingBodies = new Map()
--------------------------------------------------------------------------------
/www/src/state/dev.ts:
--------------------------------------------------------------------------------
1 | import {proxy} from "valtio";
2 |
3 | export const devState = proxy({
4 | targetLocked: false,
5 | inDanger: false,
6 | })
--------------------------------------------------------------------------------
/www/src/state/inputs.ts:
--------------------------------------------------------------------------------
1 |
2 | export const RAW_INPUTS = {
3 | punch: false,
4 | }
5 |
6 | export type InputState = {
7 | keys: number[],
8 | active: boolean,
9 | pressed: boolean,
10 | released: boolean,
11 | raw?: boolean,
12 | rawLastPressed?: number,
13 | }
14 |
15 | export enum InputKeys {
16 | RIGHT,
17 | LEFT,
18 | UP,
19 | DOWN,
20 | SHIFT,
21 | PUNCH,
22 | RECHARGE,
23 | }
24 |
25 | export const inputsState: Record = {
26 | [InputKeys.PUNCH]: {
27 | keys: [32],
28 | active: false,
29 | pressed: false,
30 | released: false,
31 | raw: false,
32 | },
33 | [InputKeys.RIGHT]: {
34 | keys: [68, 39],
35 | active: false,
36 | pressed: false,
37 | released: false,
38 | },
39 | [InputKeys.LEFT]: {
40 | keys: [65, 37],
41 | active: false,
42 | pressed: false,
43 | released: false,
44 | },
45 | [InputKeys.UP]: {
46 | keys: [87, 38],
47 | active: false,
48 | pressed: false,
49 | released: false,
50 | },
51 | [InputKeys.DOWN]: {
52 | keys: [83, 40],
53 | active: false,
54 | pressed: false,
55 | released: false,
56 | },
57 | [InputKeys.SHIFT]: {
58 | keys: [16],
59 | active: false,
60 | pressed: false,
61 | released: false,
62 | },
63 | [InputKeys.RECHARGE]: {
64 | keys: [17],
65 | active: false,
66 | pressed: false,
67 | released: false,
68 | },
69 | }
--------------------------------------------------------------------------------
/www/src/state/mobs.ts:
--------------------------------------------------------------------------------
1 | import {proxy} from "valtio";
2 | import {increasePlayerJuice, playerTargets} from "./player";
3 | import {getMobPosition} from "../temp/ai";
4 | import {playerPosition} from "./positions";
5 | import {MOB_VARIANT} from "../game/components/Mob/data";
6 |
7 | const getMobVariantHealth = (variant: MOB_VARIANT): {
8 | health: number,
9 | maxHealth: number,
10 | } => {
11 |
12 | if (variant === MOB_VARIANT.large) {
13 | return {
14 | health: 250,
15 | maxHealth: 250,
16 | }
17 | }
18 |
19 | return {
20 | health: 100,
21 | maxHealth: 100,
22 | }
23 | }
24 |
25 | export type MobHealth = {
26 | stunned: boolean,
27 | health: number,
28 | maxHealth: number,
29 | lastHit: number,
30 | lastAttacked: number,
31 | attackVector: [number, number]
32 | }
33 |
34 | export const mobsHealthManager: {
35 | [id: number]: MobHealth,
36 | } = {}
37 |
38 | export const initMobHealthManager = (id: number, variant: MOB_VARIANT): MobHealth => {
39 |
40 | const {health, maxHealth} = getMobVariantHealth(variant)
41 |
42 | const manager = proxy({
43 | stunned: false,
44 | health,
45 | maxHealth,
46 | lastHit: 0,
47 | lastAttacked: 0,
48 | attackVector: [0, 0]
49 | })
50 | mobsHealthManager[id] = manager
51 | return manager
52 | }
53 |
54 | export const deleteMobHealthManager = (id: number) => {
55 | delete mobsHealthManager[id]
56 | }
57 |
58 | export const getMobHealthManager = (id: number): MobHealth => {
59 | const manager = mobsHealthManager[id]
60 | if (!manager) throw new Error(`Mob manager to available: ${id}`)
61 | return manager
62 | }
63 |
64 | export const dealDamageToMob = (mobID: number, lockOn: boolean) => {
65 | const manager = getMobHealthManager(mobID)
66 | if (!manager) return
67 | if (manager.stunned) return
68 | // if (lockOn) {
69 | // playerTargets.lastAttacked = mobID
70 | // }
71 | //let newHealth = manager.health - 34
72 | let newHealth = manager.health - 34
73 | if (newHealth < 0) {
74 | newHealth = 0
75 | }
76 | manager.health = newHealth
77 | //manager.health = manager.health - 1
78 | manager.lastHit = Date.now()
79 | const [enemyX, enemyY] = getMobPosition(mobID)
80 |
81 | const angle = Math.atan2(playerPosition.y - enemyY, playerPosition.x - enemyX)
82 | const xVector = Math.cos(angle) * -1
83 | const yVector = Math.sin(angle) * -1
84 | manager.attackVector = [xVector, yVector]
85 |
86 | increasePlayerJuice(20)
87 |
88 | }
--------------------------------------------------------------------------------
/www/src/state/player.ts:
--------------------------------------------------------------------------------
1 | import {proxy, useProxy} from "valtio";
2 |
3 | export const playerJuice = proxy<{
4 | juice: number,
5 | }>({
6 | juice: 100,
7 | })
8 |
9 | export const increasePlayerJuice = (juice: number) => {
10 | const currentJuice = playerJuice.juice
11 | if (currentJuice >= 100) return
12 | let newJuice = currentJuice + juice
13 | if (newJuice > 100) {
14 | newJuice = 100
15 | }
16 | playerJuice.juice = newJuice
17 | }
18 |
19 | export const playerState = proxy<{
20 | invincible: boolean,
21 | preRecharging: boolean,
22 | recharging: boolean,
23 | }>({
24 | invincible: false,
25 | preRecharging: false,
26 | recharging: false,
27 | })
28 |
29 | export const playerEnergy = proxy<{
30 | energy: number,
31 | }>({
32 | energy: 100,
33 | })
34 |
35 | export const playerHealth = proxy<{
36 | maxHealth: number,
37 | health: number,
38 | lastDamaged: number,
39 | }>({
40 | maxHealth: 4,
41 | health: 4,
42 | lastDamaged: 0,
43 | })
44 |
45 | export const playerHasFullHealth = (): boolean => {
46 | return playerHealth.health >= playerHealth.maxHealth
47 | }
48 |
49 | export const JUICE_RECHARGE_COST = 50
50 |
51 | export const playerCanRecharge = (checkHealth: boolean = true): boolean => {
52 | const validJuice = playerJuice.juice >= JUICE_RECHARGE_COST
53 | if (checkHealth) {
54 | return validJuice && !playerHasFullHealth()
55 | }
56 | return validJuice
57 | }
58 |
59 | export const usePlayerCanRecharge = (): boolean => {
60 | const {juice} = useProxy(playerJuice)
61 | const {health, maxHealth} = useProxy(playerHealth)
62 | return juice >= JUICE_RECHARGE_COST && health < maxHealth
63 | }
64 |
65 | export const rechargePlayer = () => {
66 | if (playerHealth.health >= playerHealth.maxHealth) {
67 | console.warn(`Player health already full.`)
68 | return
69 | }
70 | if (!playerCanRecharge()) {
71 | console.warn(`Can't recharge the player, there's no juice.`)
72 | return
73 | }
74 | let newJuice = playerJuice.juice - JUICE_RECHARGE_COST
75 | if (newJuice < 0) {
76 | newJuice = 0
77 | }
78 | playerJuice.juice = newJuice
79 | let newHealth = playerHealth.health + 1
80 | if (newHealth > playerHealth.maxHealth) {
81 | newHealth = playerHealth.maxHealth
82 | }
83 | playerHealth.health = newHealth
84 | }
85 |
86 | export const dealPlayerDamage = (damage: number) => {
87 | if (playerState.invincible) {
88 | return
89 | }
90 | let newPlayerHealth = playerHealth.health - damage
91 | if (newPlayerHealth < 0) {
92 | newPlayerHealth = 0
93 | }
94 | playerHealth.health = newPlayerHealth
95 | playerHealth.lastDamaged = Date.now()
96 | playerState.preRecharging = false
97 | }
98 |
99 | export const playerTargets = proxy<{
100 | attackRange: number[],
101 | closeRange: number[],
102 | inRange: number[],
103 | focusedInRange: number[],
104 | targetID: number | null,
105 | lastAttacked: number | null,
106 | lastHitBy: number | null,
107 | }>({
108 | attackRange: [],
109 | closeRange: [],
110 | inRange: [],
111 | focusedInRange: [],
112 | targetID: null,
113 | lastAttacked: null,
114 | lastHitBy: null,
115 | })
116 |
117 | export const getPlayerTargetedEnemy = (): number | null => {
118 | const {lastAttacked, inRange} = playerTargets
119 | if (lastAttacked !== null && inRange.includes(lastAttacked)) {
120 | return lastAttacked
121 | }
122 | return null
123 | }
124 |
125 | export const useEnemiesInRange = (): boolean => {
126 | const {inRange: targets} = useProxy(playerTargets)
127 | return targets.length > 0
128 | }
129 |
130 | export const useEnemiesInCloseRange = (): boolean => {
131 | const {closeRange} = useProxy(playerTargets)
132 | return closeRange.length > 0
133 | }
134 |
135 | export const usePlayerTarget = (): number | null => {
136 | const {inRange, lastAttacked, attackRange, lastHitBy, focusedInRange} = useProxy(playerTargets)
137 |
138 | if (lastAttacked !== null && inRange.includes(lastAttacked)) {
139 | return lastAttacked
140 | }
141 |
142 | if (attackRange.length > 0) {
143 |
144 | if (lastHitBy !== null && attackRange.includes(lastHitBy)) {
145 | return lastHitBy
146 | }
147 |
148 | return attackRange[0]
149 | }
150 |
151 | if (focusedInRange.length > 0) {
152 | return focusedInRange[0]
153 | }
154 |
155 | return null
156 | }
157 |
158 | export const usePlayerHasTarget = (): boolean => {
159 | const target = usePlayerTarget()
160 | return target !== null
161 | }
162 |
163 | export const usePlayerInCombat = (): boolean => {
164 | const target = usePlayerTarget()
165 | const {closeRange} = useProxy(playerTargets)
166 | return target !== null || closeRange.length > 0
167 | }
168 |
169 | export const removePlayerFromRange = (mobID: number) => {
170 | const index = playerTargets.inRange.indexOf(mobID)
171 | if (index >= 0) {
172 | playerTargets.inRange.splice(index, 1)
173 | }
174 | }
175 |
176 | export const addToPlayerFocusedRange = (mobID: number) => {
177 | playerTargets.focusedInRange.push(mobID)
178 | }
179 |
180 | export const removeFromPlayerFocusedRange = (mobID: number) => {
181 | const index = playerTargets.focusedInRange.indexOf(mobID)
182 | if (index >= 0) {
183 | playerTargets.focusedInRange.splice(index, 1)
184 | }
185 | }
186 |
187 | export const addToPlayerCloseRange = (mobID: number) => {
188 | playerTargets.closeRange.push(mobID)
189 | }
190 |
191 | export const removeFromPlayerCloseRange = (mobID: number) => {
192 | const index = playerTargets.closeRange.indexOf(mobID)
193 | if (index >= 0) {
194 | playerTargets.closeRange.splice(index, 1)
195 | }
196 | }
197 |
198 | export const addToPlayerAttackRange = (mobID: number) => {
199 | playerTargets.attackRange.push(mobID)
200 | }
201 |
202 | export const removeFromPlayerAttackRange = (mobID: number) => {
203 | const index = playerTargets.attackRange.indexOf(mobID)
204 | if (index >= 0) {
205 | playerTargets.attackRange.splice(index, 1)
206 | }
207 | }
--------------------------------------------------------------------------------
/www/src/state/positions.ts:
--------------------------------------------------------------------------------
1 | export const playerPosition = {
2 | x: 0,
3 | y: 0,
4 | previousX: 0,
5 | previousY: 0,
6 | targetX: 0,
7 | targetY: 0,
8 | angle: 0,
9 | }
10 |
11 | export const cameraPosition = {
12 | previousX: 0,
13 | previousY: 0,
14 | }
--------------------------------------------------------------------------------
/www/src/state/refs.ts:
--------------------------------------------------------------------------------
1 | export const gameRefs = {
2 | player: null,
3 | mainLight: null,
4 | }
--------------------------------------------------------------------------------
/www/src/temp/ai.ts:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | each frame, iterate over active ai, process their goals
4 |
5 | */
6 |
7 | export enum MobAIGoal {
8 | IDLE,
9 | ATTACK
10 | }
11 |
12 | export type MobData = {
13 | alive: boolean,
14 | inPlayerRange: boolean,
15 | goal: number,
16 | x: number,
17 | y: number,
18 | }
19 |
20 | export const mobsMap = new Map()
21 |
22 | export const addMob = (id: number) => {
23 | mobsMap.set(id, {
24 | alive: true,
25 | inPlayerRange: false,
26 | goal: MobAIGoal.IDLE,
27 | x: 0,
28 | y: 0,
29 | })
30 | }
31 |
32 | export const removeMob = (id: number) => {
33 | mobsMap.delete(id)
34 | }
35 |
36 | export const getMob = (id: number): MobData => {
37 | const mob = mobsMap.get(id)
38 | if (!mob) throw new Error(`Mob ${id} not found`)
39 | return mob
40 | }
41 |
42 | export const getMobPosition = (id: number): [number, number] => {
43 | const mob = getMob(id)
44 | return [mob.x, mob.y]
45 | }
46 |
47 | const processMob = (id: number, data: MobData) => {
48 | if (!data.alive) return
49 | if (data.inPlayerRange) {
50 | data.goal = MobAIGoal.ATTACK
51 | } else {
52 | data.goal = MobAIGoal.IDLE
53 | }
54 | }
55 |
56 | export const processMobsAI = () => {
57 | mobsMap.forEach((value, id) => {
58 | processMob(id, value)
59 | })
60 | }
61 |
62 | export const updateMob = (id: number, data: Partial) => {
63 | const originalData = mobsMap.get(id)
64 | if (!originalData) return
65 | Object.entries(data).forEach(([key, value]) => {
66 | (originalData as any)[key] = value
67 | })
68 | }
--------------------------------------------------------------------------------
/www/src/temp/rooms.ts:
--------------------------------------------------------------------------------
1 | type RoomDoor = [[number, number], [number, number]];
2 |
3 | const room: {
4 | width: number;
5 | height: number;
6 | door: RoomDoor;
7 | } = {
8 | width: 30,
9 | height: 25,
10 | door: [
11 | [5, 0],
12 | [8, 0]
13 | ]
14 | };
15 |
16 | export enum RoomDirection {
17 | NORTH,
18 | EAST,
19 | SOUTH,
20 | WEST
21 | }
22 |
23 | export type RoomWallMdl = {
24 | direction: RoomDirection,
25 | start: [number, number];
26 | end: [number, number];
27 | };
28 |
29 | const spaceIsClear = (x: number, y: number, door: RoomDoor): boolean => {
30 | return !(
31 | x >= door[0][0] &&
32 | x <= door[1][0] &&
33 | y >= door[0][1] &&
34 | y <= door[1][1]
35 | );
36 | };
37 |
38 | const getWalls = (
39 | [startX, startY]: [number, number],
40 | [endX, endY]: [number, number],
41 | door: RoomDoor,
42 | direction: RoomDirection,
43 | ): RoomWallMdl[] => {
44 | let walls: RoomWallMdl[] = [];
45 |
46 | let horizontal = startX !== endX;
47 |
48 | let currentWall: null | {
49 | start: [number, number];
50 | } = null;
51 |
52 | const addCurrentWall = (x: number, y: number) => {
53 | if (currentWall) {
54 | walls.push({
55 | direction,
56 | start: currentWall.start,
57 | end: [x, y]
58 | });
59 | currentWall = null;
60 | }
61 | };
62 |
63 | const processSpot = (x: number, y: number) => {
64 | if (spaceIsClear(x, y, door)) {
65 | if (!currentWall) {
66 | currentWall = {
67 | start: [x, y]
68 | };
69 | }
70 | } else {
71 | if (currentWall) {
72 | addCurrentWall(x, y);
73 | }
74 | }
75 | };
76 |
77 | if (horizontal) {
78 | const start = startX < endX ? startX : endX;
79 | const end = endX > startX ? endX : startX;
80 | const y = startY;
81 | for (let x = start; x < end; x++) {
82 | processSpot(x, y);
83 | }
84 | if (currentWall) {
85 | addCurrentWall(end, y);
86 | }
87 | } else {
88 | const x = startX;
89 | const start = startY < endY ? startY : endY;
90 | const end = endY > startY ? endY : startY;
91 | for (let y = start; y < end; y++) {
92 | processSpot(x, y);
93 | }
94 | if (currentWall) {
95 | addCurrentWall(x, end);
96 | }
97 | }
98 |
99 | return walls;
100 | };
101 |
102 | const getRoomWalls = (): RoomWallMdl[] => {
103 | let walls: RoomWallMdl[] = [];
104 |
105 | walls.push(...getWalls([0, 0], [room.width, 0], room.door, RoomDirection.NORTH));
106 | walls.push(
107 | ...getWalls([room.width, 0], [room.width, room.height], room.door, RoomDirection.EAST)
108 | );
109 | walls.push(
110 | ...getWalls([room.width, room.height], [0, room.height], room.door, RoomDirection.SOUTH)
111 | );
112 | walls.push(...getWalls([0, room.height], [0, 0], room.door, RoomDirection.WEST));
113 |
114 | return walls;
115 | };
116 |
117 | export const roomWalls = getRoomWalls();
--------------------------------------------------------------------------------
/www/src/ui/colors.ts:
--------------------------------------------------------------------------------
1 | export const COLORS = {
2 | health: '#cd011b',
3 | }
--------------------------------------------------------------------------------
/www/src/ui/global.ts:
--------------------------------------------------------------------------------
1 | import reset from "styled-reset";
2 | import {createGlobalStyle} from "styled-components";
3 | import {STATS_CSS_CLASS} from "../game/components/Game/Game";
4 |
5 | export const GlobalStyle = createGlobalStyle`
6 | ${reset};
7 |
8 | body {
9 | background-color: #040608;
10 | color: #FFFFFF;
11 | font-family: 'Open Sans', sans-serif;
12 | font-size: 15px;
13 | line-height: 1.2;
14 | }
15 |
16 | canvas {
17 | -webkit-touch-callout:none;
18 | -webkit-user-select:none;
19 | -khtml-user-select:none;
20 | -moz-user-select:none;
21 | -ms-user-select:none;
22 | user-select:none;
23 | outline:0;
24 | -webkit-tap-highlight-color:rgba(255,255,255,0);
25 | }
26 |
27 | body {
28 | -webkit-touch-callout:none;
29 | -webkit-user-select:none;
30 | -khtml-user-select:none;
31 | -moz-user-select:none;
32 | -ms-user-select:none;
33 | user-select:none;
34 | outline:0;
35 | -webkit-tap-highlight-color:rgba(255,255,255,0);
36 | }
37 |
38 | * {
39 | box-sizing: border-box;
40 | -webkit-font-smoothing: antialiased;
41 | -moz-osx-font-smoothing: grayscale;
42 | }
43 |
44 | a {
45 | text-decoration: none;
46 | }
47 |
48 | strong {
49 | font-weight: bold;
50 | }
51 |
52 | .${STATS_CSS_CLASS} {
53 | top: unset !important;
54 | left: 0px !important;
55 | bottom: 0px !important;
56 | }
57 |
58 | `;
59 |
--------------------------------------------------------------------------------
/www/src/utils/angles.ts:
--------------------------------------------------------------------------------
1 | export const radians = (degrees: number): number => {
2 | return (degrees * Math.PI) / 180
3 | }
4 |
5 | export const rotateVector = (x: number, y: number, degrees: number): [number, number] => {
6 | const rad = radians(degrees)
7 | const sin = Math.sin(rad)
8 | const cos = Math.cos(rad)
9 |
10 | const adjustedX = (cos * x) - (sin * y)
11 | const adjustedY = (sin * x) + (cos * y)
12 |
13 | return [adjustedX, adjustedY]
14 | }
15 |
--------------------------------------------------------------------------------
/www/src/utils/color.ts:
--------------------------------------------------------------------------------
1 | export const hexStringToCode = (string: string) => {
2 | return parseInt(string.replace(/^#/, ""), 16);
3 | };
4 |
--------------------------------------------------------------------------------
/www/src/utils/common.ts:
--------------------------------------------------------------------------------
1 | export const DIAGONAL = 0.707
--------------------------------------------------------------------------------
/www/src/utils/models.ts:
--------------------------------------------------------------------------------
1 | const MATERIALS: {
2 | [key: string]: any
3 | } = {}
4 |
5 | interface Mesh {
6 | material: {
7 | name: string
8 | }
9 | }
10 |
11 | const setMaterial = (mesh: Mesh, materials: any) => {
12 | const { name } = mesh.material
13 | const matchedMaterial: any = materials[name.toString()]
14 | if (matchedMaterial) {
15 | // eslint-disable-next-line no-param-reassign
16 | mesh.material = matchedMaterial
17 | } else {
18 | console.warn(`no material matched for: ${name.toString()}`)
19 | }
20 | }
21 |
22 | const recursiveMeshes = (child: any): any[] => {
23 | let meshes: any[] = []
24 |
25 | if (child.type === 'SkinnedMesh' || child.type === 'Mesh') {
26 | meshes.push(child)
27 | } else {
28 | if (child.children) {
29 | child.children.forEach((subChild: any) => {
30 | meshes = meshes.concat(recursiveMeshes(subChild))
31 | })
32 | }
33 | }
34 |
35 | return meshes
36 | }
37 |
38 | const getSkinnedMeshes = (scene: any): any[] => {
39 |
40 | let meshes: any[] = []
41 |
42 | scene.children.forEach((child: any) => {
43 | meshes = meshes.concat(recursiveMeshes(child))
44 | })
45 |
46 | return meshes
47 | }
48 |
49 | export const setMaterials = (scene: any, materials: any = MATERIALS) => {
50 | const skinnedMeshes = getSkinnedMeshes(scene)
51 | skinnedMeshes.forEach((mesh: any) => {
52 | setMaterial(mesh, materials)
53 | })
54 | }
55 |
56 | export const setShadows = (scene: any) => {
57 | const skinnedMeshes = getSkinnedMeshes(scene)
58 | skinnedMeshes.forEach((mesh: any) => {
59 | // eslint-disable-next-line no-param-reassign
60 | mesh.castShadow = true
61 | // eslint-disable-next-line no-param-reassign
62 | mesh.receiveShadow = true
63 | })
64 | }
--------------------------------------------------------------------------------
/www/src/utils/numbers.ts:
--------------------------------------------------------------------------------
1 | export const plusOrMinus = (num: number): number => {
2 | if (num > 0.5) return 1
3 | return -1
4 | }
5 |
6 | export const numLerp = (v0: number, v1: number, t: number): number => {
7 | return v0*(1-t)+v1*t
8 | }
9 |
10 | export const PI = 3.14159265359
11 | export const PI_TIMES_TWO = 6.28318530718
12 |
13 | export const lerpRadians = (a: number, b: number, lerpFactor: number): number => // Lerps from angle a to b (both between 0.f and PI_TIMES_TWO), taking the shortest path
14 | {
15 | let result: number;
16 | let diff: number = b - a;
17 | if (diff < -PI)
18 | {
19 | // lerp upwards past PI_TIMES_TWO
20 | b += PI_TIMES_TWO;
21 | result = numLerp(a, b, lerpFactor);
22 | if (result >= PI_TIMES_TWO)
23 | {
24 | result -= PI_TIMES_TWO;
25 | }
26 | }
27 | else if (diff > PI)
28 | {
29 | // lerp downwards past 0
30 | b -= PI_TIMES_TWO;
31 | result = numLerp(a, b, lerpFactor);
32 | if (result < 0)
33 | {
34 | result += PI_TIMES_TWO;
35 | }
36 | }
37 | else
38 | {
39 | // straight lerp
40 | result = numLerp(a, b, lerpFactor);
41 | }
42 |
43 | return result;
44 | }
--------------------------------------------------------------------------------
/www/src/utils/responsive.ts:
--------------------------------------------------------------------------------
1 | import {useWindowSize} from "@react-hook/window-size";
2 |
3 | export const useIsPortrait = (): boolean => {
4 | const [width, height] = useWindowSize()
5 | return height > width
6 | }
--------------------------------------------------------------------------------
/www/src/workers/physics/functions.ts:
--------------------------------------------------------------------------------
1 | import {WorkerOwnerMessageType} from "./types";
2 | import {dynamicBodiesUuids, updateBodiesLastUpdated} from "../../shared";
3 |
4 | /* eslint-disable-next-line no-restricted-globals */
5 | const selfWorker = self as unknown as Worker
6 |
7 | export const syncBodies = () => {
8 | updateBodiesLastUpdated()
9 | /*selfWorker.postMessage(({
10 | type: WorkerOwnerMessageType.SYNC_BODIES,
11 | bodies: dynamicBodiesUuids
12 | }))*/
13 | }
14 |
15 | export const sendCollisionBeginEvent = (uuid: string, data: any, fixtureIndex: number) => {
16 | selfWorker.postMessage(({
17 | type: WorkerOwnerMessageType.BEGIN_COLLISION,
18 | props: {
19 | uuid,
20 | data,
21 | fixtureIndex,
22 | }
23 | }))
24 | }
25 |
26 | export const sendCollisionEndEvent = (uuid: string, data: any, fixtureIndex: number) => {
27 | selfWorker.postMessage(({
28 | type: WorkerOwnerMessageType.END_COLLISION,
29 | props: {
30 | uuid,
31 | data,
32 | fixtureIndex,
33 | }
34 | }))
35 | }
--------------------------------------------------------------------------------
/www/src/workers/physics/physicsWorker.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | import {WorkerMessageType, WorkerOwnerMessageType} from "./types";
4 | import {initPhysicsListeners, stepWorld} from "../../physics/world";
5 | import {syncBodies} from "./functions";
6 | import {addBody, removeBody, setBody, updateBody} from "../../physics/bodies";
7 | import {dynamicBodiesUuids, unsyncedBodies} from "../../shared";
8 |
9 | const selfWorker = self as unknown as Worker
10 |
11 | const init = () => {
12 | syncBodies()
13 | initPhysicsListeners()
14 | }
15 |
16 | init()
17 |
18 | const step = (positions: Float32Array, angles: Float32Array) => {
19 |
20 | stepWorld(positions, angles)
21 |
22 | const data: any = {
23 | type: WorkerOwnerMessageType.FRAME,
24 | positions,
25 | angles,
26 | }
27 |
28 | if (unsyncedBodies) {
29 | data['bodies'] = dynamicBodiesUuids
30 | }
31 |
32 | selfWorker.postMessage(data, [positions.buffer, angles.buffer])
33 |
34 | }
35 |
36 | self.onmessage = (event: MessageEvent) => {
37 | const {type, props = {}} = event.data as {
38 | type: WorkerMessageType,
39 | props: any,
40 | };
41 | switch (type) {
42 | case WorkerMessageType.STEP:
43 | const {positions, angles} = event.data
44 | step(positions, angles)
45 | break;
46 | case WorkerMessageType.ADD_BODY:
47 | addBody(props)
48 | break;
49 | case WorkerMessageType.REMOVE_BODY:
50 | removeBody(props)
51 | break;
52 | case WorkerMessageType.SET_BODY:
53 | setBody(props)
54 | break;
55 | case WorkerMessageType.UPDATE_BODY:
56 | updateBody(props)
57 | break;
58 | }
59 | }
60 |
61 | export {};
--------------------------------------------------------------------------------
/www/src/workers/physics/types.ts:
--------------------------------------------------------------------------------
1 | export enum WorkerMessageType {
2 | INIT,
3 | STEP,
4 | ADD_BODY,
5 | REMOVE_BODY,
6 | SET_BODY,
7 | UPDATE_BODY,
8 | }
9 |
10 | export enum WorkerOwnerMessageType {
11 | FRAME,
12 | SYNC_BODIES,
13 | BEGIN_COLLISION,
14 | END_COLLISION,
15 | }
--------------------------------------------------------------------------------
/www/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------