├── .gitignore ├── README.md ├── debug.log ├── frontend ├── .gitignore ├── README.md ├── components │ ├── Inventory.tsx │ ├── Turtle.tsx │ ├── TurtleSwitcher.tsx │ └── World.tsx ├── next-env.d.ts ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ └── index.tsx ├── public │ ├── otherturtle.blend │ ├── otherturtle.glb │ ├── otheruv.png │ ├── turtle.blend │ ├── turtle.glb │ └── uv.png ├── src │ └── theme.ts ├── tsconfig.json └── yarn.lock ├── package.json ├── src ├── index.ts ├── turtle.ts └── world.ts ├── tsconfig.json ├── tslint.json ├── turtle └── startup.lua ├── world.json ├── yarn-error.log └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Turtle Control 2 | ### Fork of [ottomated/turtle-gambit](https://github.com/ottomated/turtle-gambit) 3 | The plan of this fork is to update the dependencies as most of the current dependencies of the [ottomated/turtle-gambit](https://github.com/ottomated/turtle-gambit) repo is no longer maintained or supported, which causes issues during installation. 4 | 5 | # Usage 6 | ### Prerequisites 7 | - NodeJS 8 | - Yarn Package manager 9 | > Note: The bottom 2 are optional but are recommended if there is more than 1 turtle 10 | - GPS Cluster in world [Setup Instructions](http://www.computercraft.info/forums2/index.php?/topic/3088-how-to-guide-gps-global-position-system/) 11 | - Turtle with modem 12 | 13 | ### Setup 14 | #### Package installation 15 | The first step is to download the node packages, to do this run `yarn` in the repo folder. After successfully downloading the packages go into the `frontend` folder and run the `yarn` command again. 16 | 17 | #### Pastebin setup 18 | Next you need to setup your turtle script, it is recommended that you have a pastebin account so you are able to edit the paste in the future. Copy the script from `startup.lua` into pastebin, but before uploading it you need to make some changes. Find the line which declares the `websocketServer` variable, you need to change that to your ip address if you are portforwarding or if you are using a tunneling service change it to the address which has been assigned to you. 19 | 20 | #### Getting the pastebin ID 21 | After uploading your new script copy the pastebin id from the url, it should be a 8 character long alphanumerical string. 22 | For example if the url is `https://pastebin.com/JhWuptAS` 23 | The pastebin id would be `JhWuptAS` 24 | This will be needed later so just keep that in your clipboard or in notepad. 25 | 26 | #### Starting up the servers 27 | The first server you will be starting up is the backend server which is located in the repo folder. To start it run the command `yarn dev`, if it starts up successfully you should see the console saying `Started Turtle Control WS Server on port 5757 and port 5758` which means the server has started. Now to run the frontend server you need to go into the `frontend` folder and run the same command again `yarn dev`. After that the frontend server should have successfully started up, to confirm this go to [localhost:3000](https://localhost:3000) which will show a gray screen ( this is expected ). 28 | 29 | #### Connecting your turtles 30 | > Note: if you are connecting to localhost ComputerCraft may block the connection as localhost is not allowed on default, this can be fixed by changing your ComputerCraft config file https://github.com/cc-tweaked/CC-Tweaked/discussions/626 31 | 32 | Now go to your turtle terminal and type in the command `pastebin get startup` . Replace the `` with the pastebin ID you had created, after that run the `reboot` command, once it finished rebooting you would see the terminal saying `Turtle Control OS, originally created by Ottomated. Modified by PrintedScript`, which has mean you have successfully connected your turtle! 🎉🎉 33 | 34 | #### Controlling your turtle 35 | You can go to [localhost:3000](https://localhost:3000) which will show your turtle. 36 | Controls 37 | - W ( Forward ) 38 | - S ( Backwards ) 39 | - A ( Turn Left ) 40 | - D ( Turn Right ) 41 | - Space ( Up ) 42 | - Left Shift ( Down ) 43 | 44 | ### Mitosis 45 | > Note: This process is still a bit messy and not currently supported, it is recommended to do this manually 46 | #### Materials needed 47 | - Mining turtle 48 | - Disk Drive 49 | - Floppy Disk 50 | 51 | #### Setup 52 | First you need to clone your turtle startup script onto your floppy disk, you can do this by running the command from any computer or turtle which is connected to the disk drive 53 | `pastebin get disk/startup` 54 | Once done place your floppy disk and disk drive into your turtle 55 | 56 | #### Cloning 57 | Before going through mitosis make sure your turtle has fuel in its first slot and another turtle in its inventory, now you can click the `UNDERGO MITOSIS` button and your turtle should start placing down its disk drive and the new turtle. After a few seconds you should see a new turtle appear in your control panel, which means your turtle has successfully connected to your server. 58 | 59 | ### Troubleshooting 60 | #### Location is at 0,0,0 61 | Make sure your turtle has fuel in it as it is needed to determine its direction. Put any type of fuel source in its inventory, then on the control panel click that slot and then click the refuel button, after that click the refresh info button and the turtle will attempt to recalibrate its position and direction. 62 | 63 | #### ERR_PACKAGE_PATH_NOT_EXPORTED 64 | As of now I have not figured out how to fix this issue and seem to only appear on Windows machines, if you do know how to fix this issue please create a new issue and I will try to add it to this README 65 | 66 | # TODO List 67 | - [x] Use websockets to allow the frontend to communicate to the backend without having to use Carlo 68 | - [ ] Update dependencies to the latest release 69 | - [ ] Add more automation to the turtles 70 | - [x] GPS support for turtles 71 | - [ ] Some sort of authentication for the control panel? 72 | - [x] Stop sending entire world data to frontend for one block update ( Only send what is updated ) 73 | - [x] Remove block from database if turtle can go into it 74 | - [ ] Change the database 75 | - [ ] Combine turtle and frontend websocket into one websocket 76 | -------------------------------------------------------------------------------- /debug.log: -------------------------------------------------------------------------------- 1 | [1120/183326.479:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 2 | [1120/204905.802:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 3 | [1120/222506.283:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 4 | [1120/234127.576:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 5 | [1121/012540.708:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 6 | [1121/021010.707:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 7 | [1121/142532.002:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 8 | [1121/143553.245:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 9 | [1121/155937.617:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 10 | [1121/162455.543:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 11 | [1121/163833.780:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 12 | [1121/171217.788:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 13 | [1121/174932.629:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 14 | [1121/181207.172:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 15 | [1121/193659.416:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 16 | [1121/201515.989:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 17 | [1122/172427.761:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 18 | [1122/190312.500:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 19 | [1122/191337.607:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 20 | [1122/195440.703:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) 21 | -------------------------------------------------------------------------------- /frontend/.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Next.js example 2 | 3 | This is a really simple project that shows the usage of Next.js with TypeScript. 4 | 5 | ## Deploy your own 6 | 7 | Deploy the example using [Vercel](https://vercel.com): 8 | 9 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/with-typescript) 10 | 11 | ## How to use it? 12 | 13 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: 14 | 15 | ```bash 16 | npx create-next-app --example with-typescript with-typescript-app 17 | # or 18 | yarn create next-app --example with-typescript with-typescript-app 19 | ``` 20 | 21 | Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). 22 | 23 | ## Notes 24 | 25 | This example shows how to integrate the TypeScript type system into Next.js. Since TypeScript is supported out of the box with Next.js, all we have to do is to install TypeScript. 26 | 27 | ``` 28 | npm install --save-dev typescript 29 | ``` 30 | 31 | To enable TypeScript's features, we install the type declarations for React and Node. 32 | 33 | ``` 34 | npm install --save-dev @types/react @types/react-dom @types/node 35 | ``` 36 | 37 | When we run `next dev` the next time, Next.js will start looking for any `.ts` or `.tsx` files in our project and builds it. It even automatically creates a `tsconfig.json` file for our project with the recommended settings. 38 | 39 | Next.js has built-in TypeScript declarations, so we'll get autocompletion for Next.js' modules straight away. 40 | 41 | A `type-check` script is also added to `package.json`, which runs TypeScript's `tsc` CLI in `noEmit` mode to run type-checking separately. You can then include this, for example, in your `test` scripts. 42 | -------------------------------------------------------------------------------- /frontend/components/Inventory.tsx: -------------------------------------------------------------------------------- 1 | import Grid from '@material-ui/core/Grid'; 2 | import Paper from '@material-ui/core/Paper'; 3 | import Menu from '@material-ui/core/Menu'; 4 | import MenuItem from '@material-ui/core/MenuItem'; 5 | import Tooltip from '@material-ui/core/Tooltip'; 6 | import makeStyles from '@material-ui/core/styles/makeStyles'; 7 | import React, { useState } from 'react'; 8 | import { Turtle } from '../pages'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import { hashCode } from './World'; 11 | import Color from 'color'; 12 | 13 | const useStyles = makeStyles(() => ({ 14 | 15 | inventory: { 16 | position: 'absolute', 17 | top: 100, 18 | left: 0, 19 | background: '#252525', 20 | height: 200, 21 | width: 200, 22 | zIndex: 10, 23 | borderRadius: 5, 24 | overflow: 'hidden' 25 | }, 26 | inventoryItem: { 27 | width: '25%', 28 | height: '25%', 29 | '& .MuiPaper-root': { 30 | height: '100%', 31 | width: '100%', 32 | border: '2px solid transparent', 33 | '&.selected': { 34 | borderColor: 'white' 35 | } 36 | }, 37 | cursor: 'pointer' 38 | } 39 | })); 40 | 41 | const initialState = { 42 | mouseX: null, 43 | mouseY: null, 44 | slot: 0 45 | }; 46 | 47 | interface InventoryProps { 48 | turtle: Turtle; 49 | } 50 | 51 | export default function Inventory({ turtle }: InventoryProps) { 52 | const classes = useStyles(); 53 | const [state, setState] = useState<{ 54 | mouseX: null | number; 55 | mouseY: null | number; 56 | slot: number; 57 | }>(initialState); 58 | 59 | const handleClick = (event: React.MouseEvent, slot: number) => { 60 | event.preventDefault(); 61 | setState({ 62 | mouseX: event.clientX - 2, 63 | mouseY: event.clientY - 4, 64 | slot 65 | }); 66 | }; 67 | 68 | const handleClose = (amount: 'all' | 'half' | 'one') => { 69 | turtle.moveItems(state.slot, amount); 70 | setState(initialState); 71 | }; 72 | 73 | let menuItems = []; 74 | if (state.slot === turtle.selectedSlot) { 75 | menuItems = [ 76 | { 77 | turtle.equip('left'); setState({ ...initialState, slot: turtle.selectedSlot }); 78 | }}>Equip Left, 79 | { 80 | turtle.equip('right'); setState({ ...initialState, slot: turtle.selectedSlot }); 81 | }}>Equip Right 82 | ]; 83 | } else { 84 | menuItems = [ 85 | handleClose('all')}>Move All, 86 | handleClose('half')}>Move Half, 87 | handleClose('one')}>Move One 88 | ]; 89 | } 90 | 91 | return ( 92 | 93 | 104 | {menuItems} 105 | 106 | { 107 | turtle.inventory.map((item, i) => ( 108 | 109 | handleClick(ev, i + 1)} className={i + 1 === turtle.selectedSlot ? 'selected' : ''} style={{ 110 | background: item ? Color({ 111 | h: hashCode(item.name + ':' + item.damage) % 360, 112 | s: 60, 113 | l: 40 114 | }).toString() : undefined 115 | }} onClick={() => turtle.selectSlot(i + 1)}> 116 | {item && 117 | 118 | {item.count} 119 | 120 | } 121 | 122 | 123 | )) 124 | } 125 | 126 | ); 127 | } -------------------------------------------------------------------------------- /frontend/components/Turtle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useRef, useEffect } from 'react'; 2 | import { Turtle, BlockDirection } from '../pages'; 3 | import Button from '@material-ui/core/Button'; 4 | import ButtonGroup, { ButtonGroupProps } from '@material-ui/core/ButtonGroup'; 5 | import ArrowDownward from '@material-ui/icons/ArrowDownward'; 6 | import ArrowUpward from '@material-ui/icons/ArrowUpward'; 7 | import CircularProgress, { CircularProgressProps } from '@material-ui/core/CircularProgress'; 8 | import Box from '@material-ui/core/Box'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import { MuiThemeProvider, createMuiTheme, makeStyles } from '@material-ui/core/styles'; 11 | import Inventory from './Inventory'; 12 | import Dialog from '@material-ui/core/Dialog'; 13 | import DialogContent from '@material-ui/core/DialogContent'; 14 | import DialogTitle from '@material-ui/core/DialogTitle'; 15 | import TextField from '@material-ui/core/TextField'; 16 | import InputAdornment from '@material-ui/core/InputAdornment'; 17 | import SvgIcon from '@material-ui/core/SvgIcon'; 18 | import IconButton from '@material-ui/core/IconButton'; 19 | import DialogActions from '@material-ui/core/DialogActions/DialogActions'; 20 | import TurtleSwitcher from './TurtleSwitcher'; 21 | import { DialogContentText } from '@material-ui/core'; 22 | 23 | export interface TurtlePageProps { 24 | turtle: Turtle; 25 | enabled: boolean; 26 | setDisableEvents: (_: boolean) => void; 27 | } 28 | 29 | const useStyles = makeStyles(theme => ({ 30 | toolbar: { 31 | display: 'flex', 32 | justifyContent: 'start', 33 | alignItems: 'center', 34 | background: '#252525', 35 | height: 100, 36 | width: '100%', 37 | }, 38 | groups: { 39 | display: 'flex', 40 | alignItems: 'center', 41 | justifyContent: 'start', 42 | '&>*': { 43 | marginLeft: theme.spacing(1), 44 | marginRight: theme.spacing(1), 45 | } 46 | } 47 | })); 48 | 49 | function CircularProgressWithLabel(props: CircularProgressProps & { label: any }) { 50 | return ( 51 | 52 | 53 | 54 | {props.label} 55 | 56 | 57 | ); 58 | } 59 | 60 | 61 | export default function TurtlePage({ turtle, enabled, setDisableEvents }: TurtlePageProps) { 62 | const [signText, setSignText] = useState(null); 63 | const [commandText, setCommandText] = useState(null); 64 | const [commandResult, setCommandResult] = useState(null); 65 | const [mineLength, setMineLength] = useState(''); 66 | const currentSignDirection = useRef(BlockDirection.FORWARD); 67 | const classes = useStyles({ enabled }); 68 | 69 | const placeBlock = (dir: BlockDirection) => { 70 | if (turtle.inventory[turtle.selectedSlot - 1]?.name === 'minecraft:sign') { 71 | currentSignDirection.current = dir; 72 | setSignText(''); 73 | } else { 74 | turtle.place(dir); 75 | } 76 | } 77 | 78 | useEffect(() => { 79 | setDisableEvents(signText !== null || commandText !== null || commandResult !== null); 80 | }, [signText, commandText]); 81 | 82 | return ( 83 | <> 84 | setSignText(null)}> 85 | Sign Text 86 | 87 | setSignText(ev.target.value)} variant="outlined" /> 88 | 89 | 90 | 91 | 95 | 96 | 97 | setCommandText(null)}> 98 | Command 99 | 100 | setCommandText(ev.target.value)} variant="outlined" /> 101 | 102 | 103 | 104 | 108 | 109 | 110 | setCommandResult(null)}> 111 | Command Result 112 | 113 | {commandResult} 114 | 115 | 116 | 117 | 118 | 119 |
120 | 121 |
122 | 123 | 124 | 127 | 128 | 129 | 130 | 131 | 134 | 135 | 136 | 137 | 138 | 141 | 142 | 143 | 144 | 145 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
161 | X: {turtle.x} Y: {turtle.y} Z: {turtle.z} 162 | setMineLength(ev.target.value)} 168 | InputProps={{ 169 | endAdornment: 170 | turtle.mineTunnel('down', parseInt(mineLength))}> 171 | 172 | 173 | turtle.mineTunnel('forward', parseInt(mineLength))}> 174 | 175 | 176 | 177 | 178 | turtle.mineTunnel('up', parseInt(mineLength))}> 179 | 180 | 181 | 182 | }} 183 | /> 184 | 185 |
186 |
187 | 188 | 189 |
190 | 191 | ); 192 | } 193 | 194 | function ColoredButtonGroup({ groupColor, ...props }: { groupColor: string } & ButtonGroupProps) { 195 | const theme = useMemo(() => createMuiTheme({ 196 | palette: { 197 | primary: { 198 | main: groupColor 199 | } 200 | }, 201 | }), [groupColor]); 202 | return ( 203 | 204 | 205 | 206 | ); 207 | 208 | } 209 | -------------------------------------------------------------------------------- /frontend/components/TurtleSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { MenuItem, Select } from '@material-ui/core'; 2 | import { useContext } from 'react'; 3 | import { TurtleContext } from '../pages'; 4 | 5 | 6 | export default function TurtleSwitcher() { 7 | const [index, setIndex, turtles] = useContext(TurtleContext); 8 | return ( 9 | 14 | ) 15 | } -------------------------------------------------------------------------------- /frontend/components/World.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas, MeshProps, extend, useFrame, useThree, ReactThreeFiber, useLoader } from '@react-three/fiber'; 2 | import { useRef, useState, useMemo, useEffect, Suspense, HTMLProps, RefObject, SetStateAction, Dispatch, useContext } from 'react'; 3 | import { BoxGeometry, Vector3, Quaternion, Euler, Raycaster, Vector2, Object3D } from 'three'; 4 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; 5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 6 | import { Turtle, TurtleContext, World } from '../pages'; 7 | import useEventListener from '@use-it/event-listener'; 8 | import Color from 'color'; 9 | import Tooltip from '@material-ui/core/Tooltip'; 10 | 11 | extend({ OrbitControls }); 12 | declare global { 13 | namespace JSX { 14 | interface IntrinsicElements { 15 | orbitControls: ReactThreeFiber.Object3DNode 16 | } 17 | } 18 | } 19 | 20 | export const hashCode = function (s: string): number { 21 | return s.split("").reduce(function (a, b) { a = ((a << 5) - a) + b.charCodeAt(0); return a & a }, 0); 22 | } 23 | 24 | function Controls({ target }: { target: [number, number, number] }) { 25 | // const controls = useInterpolate('target', target);\ 26 | const controls = useRef(null); 27 | const { camera, gl } = useThree(); 28 | useFrame(() => controls.current.update()); 29 | return ( 30 | 31 | ); 32 | } 33 | 34 | function useInterpolate(property: 'position' | 'target', position: [number, number, number], rotation?: [number, number, number]) { 35 | const ref = useRef(null); 36 | useFrame(() => { 37 | if (ref.current) { 38 | const current = ref.current[property]; 39 | const newPos = current.lerp(new Vector3(position[0], position[1], position[2]), 0.3); 40 | ref.current[property].x = newPos.x; 41 | ref.current[property].y = newPos.y; 42 | ref.current[property].z = newPos.z; 43 | if (rotation) { 44 | const currentR = ref.current.quaternion; 45 | const targetR = new Quaternion(); 46 | targetR.setFromEuler(new Euler(rotation[0], rotation[1], rotation[2])); 47 | const newRot = currentR.slerp(targetR, 0.3); 48 | ref.current.rotation.setFromQuaternion(newRot); 49 | } 50 | } 51 | }); 52 | return ref; 53 | } 54 | 55 | function Model({ url, position, rotation, name }: { url: string, name: string, position: [number, number, number], rotation: [number, number, number] }) { 56 | //const GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader').GLTFLoader; 57 | const obj = useLoader(GLTFLoader, url) as any; 58 | const ref = useInterpolate('position', position, rotation); 59 | return ( 60 | <> 61 | 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | 74 | function OtherTurtles({ turtles, switchTurtle }: { turtles: Turtle[], switchTurtle: Function }) { 75 | //const GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader').GLTFLoader; 76 | const obj = useLoader(GLTFLoader, "/otherturtle.glb") as any; 77 | 78 | return ( 79 | <> 80 | {turtles.map((turtle) => )} 81 | 82 | ); 83 | } 84 | 85 | function OtherTurtle({ obj, turtle, switchTurtle }: { obj: any, turtle: Turtle, switchTurtle: Function }) { 86 | const geom = useMemo(() => obj.scene.clone(true), []); 87 | return <> 88 | 93 | switchTurtle(turtle)} 95 | visible={false} 96 | position={[turtle.x, turtle.y, turtle.z]} 97 | name={turtle.label} 98 | scale={[1, 1, 1]} 99 | > 100 | 101 | 102 | ; 103 | } 104 | 105 | function TooltipRaycaster({ mouse, setHovered }: { mouse: RefObject<{ x: number, y: number }>, setHovered: Dispatch> }) { 106 | const { camera, scene, size } = useThree(); 107 | const ray = useRef(null); 108 | 109 | useFrame(() => { 110 | if (!ray.current || !mouse.current) return; 111 | let pos = new Vector2(); 112 | pos.x = (mouse.current.x / size.width) * 2 - 1; 113 | pos.y = - (mouse.current.y / size.height) * 2 + 1; 114 | 115 | ray.current.setFromCamera(pos, camera); 116 | var intersects = ray.current.intersectObjects(scene.children); 117 | let object: Object3D | null = null; 118 | for (let i = 0; i < intersects.length; i++) { 119 | object = intersects[i].object; 120 | if (object.name) break; 121 | } 122 | if (object) { 123 | setHovered(object.name); 124 | } else { 125 | setHovered(''); 126 | } 127 | }); 128 | 129 | return ; 130 | } 131 | 132 | export default function WorldRenderer({ turtle, world, disableEvents, ...props }: { turtle?: Turtle, world: World, disableEvents: boolean } & HTMLProps) { 133 | 134 | const [, setTurtleIndex, turtles] = useContext(TurtleContext); 135 | const position = useRef({ x: 0, y: 0 }); 136 | const popperRef = useRef(null); 137 | const [hovered, setHovered] = useState(''); 138 | const [showWholeWorld, setShowWholeWorld] = useState(false); 139 | const [dontShowStone, setDontShowStone] = useState(false); 140 | 141 | const disableEventsRef = useRef(disableEvents); 142 | useEffect(() => { 143 | disableEventsRef.current = disableEvents; 144 | }, [disableEvents]); 145 | const currentTurtleRef = useRef(turtle); 146 | useEffect(() => { 147 | currentTurtleRef.current = turtle; 148 | }, [turtle]); 149 | 150 | useEventListener('keyup', (ev: KeyboardEvent) => { 151 | if (disableEventsRef.current || !currentTurtleRef.current) return; 152 | let moved = false; 153 | if (ev.code === 'KeyW') { 154 | moved = true; 155 | currentTurtleRef.current.forward(); 156 | } else if (ev.code === 'KeyA') { 157 | moved = true; 158 | currentTurtleRef.current.turnLeft(); 159 | } else if (ev.code === 'KeyS') { 160 | moved = true; 161 | currentTurtleRef.current.back(); 162 | } else if (ev.code === 'KeyD') { 163 | moved = true; 164 | currentTurtleRef.current.turnRight(); 165 | } else if (ev.code === 'Space') { 166 | moved = true; 167 | currentTurtleRef.current.up(); 168 | } else if (ev.code === 'ShiftLeft') { 169 | moved = true; 170 | currentTurtleRef.current.down(); 171 | } else if (ev.code === 'KeyV') { 172 | moved = true; 173 | setShowWholeWorld(w => !w); 174 | } else if (ev.code === 'KeyB') { 175 | moved = true; 176 | setDontShowStone(w => !w); 177 | } 178 | if (moved) { 179 | ev.stopPropagation(); 180 | ev.preventDefault(); 181 | } 182 | }); 183 | return ( 184 | ({ 192 | top: position.current.y + 100, 193 | left: position.current.x, 194 | right: position.current.x, 195 | bottom: position.current.y + 100, 196 | width: 0, 197 | height: 0, 198 | x: 0, 199 | y: 0, 200 | toJSON: () => { } 201 | }), 202 | } 203 | }} 204 | onMouseMove={(ev) => { 205 | // console.log(scene); 206 | // // mouse.x = (ev.clientX / size.width) * 2 - 1; 207 | // // mouse.y = - (ev.clientY / size.height) * 2 + 1; 208 | // ray.setFromCamera(mouse, camera) 209 | // var intersects = ray.intersectObjects(scene.children); 210 | // if (intersects.length > 0) { 211 | // console.log(intersects[0].object.name); 212 | // } else { 213 | // console.log("Nothing"); 214 | // } 215 | // console.log(ev.clientY); 216 | position.current = { x: ev.clientX, y: ev.clientY - 100 }; 217 | if (popperRef.current) 218 | popperRef.current.update(); 219 | }} 220 | > 221 |
222 | 223 | 224 | 225 | 226 | { 227 | turtle && 228 | 229 | 230 | 231 | } 232 | {Object.keys(world).map(k => { 233 | let positions = k.split(',').map(p => parseInt(p)) as [number, number, number]; 234 | let { name, metadata } = world[k]; 235 | if (dontShowStone && name ==='minecraft:stone') { 236 | return null; 237 | } 238 | if (!showWholeWorld && turtle && (Math.pow(positions[0] - turtle.x, 2) + Math.pow(positions[1] - turtle.y, 2) + Math.pow(positions[2] - turtle.z, 2)) > 1000) { 239 | return null; 240 | } 241 | return { 243 | let checkEqual = (t: Turtle, positions: number[], x: number, y: number, z: number) => t.x === positions[0] + x && t.y === positions[1] + y && t.z === positions[2] + z; 244 | for (let x = -1; x <= 1; x++) { 245 | for (let y = -1; y <= 1; y++) { 246 | for (let z = -1; z <= 1; z++) { 247 | if (checkEqual(t, positions, x, y, z)) return true; 248 | } 249 | } 250 | } 251 | return false; 252 | })} 253 | key={k} position={positions} name={name + ':' + metadata} color={Color({ 254 | h: hashCode(name + ':' + metadata) % 360, 255 | s: 60, 256 | l: 40, 257 | }).toString()} /> 258 | }).filter(b => b)} 259 | 260 | { 261 | setTurtleIndex(turtle.id); 262 | }} turtles={turtles.filter(t => t.id !== turtle?.id)} /> 263 | 264 | 265 |
266 |
267 | ) 268 | } 269 | 270 | function Box(props: MeshProps & { color: string, name: string, transparent: boolean }) { 271 | //if (props.name.includes('computercraft:turtle_expanded')) return null; 272 | if (props.name.includes('computercraft:turtle')) return null; 273 | // This reference will give us direct access to the mesh 274 | //const mesh = useRef() 275 | 276 | // Set up state for the hovered and active state 277 | 278 | // Rotate mesh every frame, this is outside of React without overhead 279 | // useFrame(() => { 280 | // if (mesh.current) mesh.current.rotation.x = mesh.current.rotation.y += 0.01 281 | // if (lines.current) lines.current.rotation.x = lines.current.rotation.y += 0.01 282 | // }) 283 | 284 | const geom = useMemo(() => new BoxGeometry(1, 1, 1), []); 285 | 286 | return ( 287 | <> 288 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | ) 302 | } -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-typescript", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next", 6 | "build": "next build && next export", 7 | "start": "next start", 8 | "type-check": "tsc" 9 | }, 10 | "dependencies": { 11 | "@emotion/react": "^11.1.1", 12 | "@emotion/server": "^11.0.0", 13 | "@emotion/styled": "^11.0.0", 14 | "@material-ui/core": "^4.11.0", 15 | "@material-ui/icons": "^4.9.1", 16 | "@react-three/fiber": "^8.9.1", 17 | "@types/color": "^3.0.1", 18 | "@types/three": "^0.146.0", 19 | "@use-it/event-listener": "^0.1.6", 20 | "babel-preset-env": "^1.7.0", 21 | "color": "^3.1.3", 22 | "next": "latest", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "three": "^0.146.0", 26 | "three-obj-loader-es6-module": "^1.0.1" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^12.12.21", 30 | "@types/react": "^16.9.16", 31 | "@types/react-dom": "^16.9.4", 32 | "typescript": "4.3.2" 33 | }, 34 | "license": "MIT" 35 | } 36 | -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import { AppProps } from 'next/app'; 4 | import { ThemeProvider } from '@material-ui/core/styles'; 5 | import { CacheProvider } from '@emotion/react'; 6 | import CssBaseline from '@material-ui/core/CssBaseline'; 7 | import createCache from '@emotion/cache'; 8 | import theme from '../src/theme'; 9 | 10 | export const cache = createCache({ key: 'css' }); 11 | 12 | export default function MyApp(props: AppProps) { 13 | const { Component, pageProps } = props; 14 | 15 | React.useEffect(() => { 16 | // Remove the server-side injected CSS. 17 | const jssStyles = document.querySelector('#jss-server-side'); 18 | if (jssStyles) { 19 | jssStyles.parentElement!.removeChild(jssStyles); 20 | } 21 | }, []); 22 | 23 | return ( 24 | 25 | 26 | Turtle Control Panel 27 | 28 | 29 | 30 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} 31 | 32 | 33 | 34 | 35 | ); 36 | } -------------------------------------------------------------------------------- /frontend/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | import { ServerStyleSheets } from '@material-ui/core/styles'; 4 | import createEmotionServer from '@emotion/server/create-instance'; 5 | import theme from '../src/theme'; 6 | import { cache } from './_app'; 7 | 8 | const { extractCritical } = createEmotionServer(cache); 9 | 10 | export default class MyDocument extends Document { 11 | render() { 12 | return ( 13 | 14 | 15 | {/* PWA primary color */} 16 | 17 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | // `getInitialProps` belongs to `_document` (instead of `_app`), 32 | // it's compatible with static-site generation (SSG). 33 | MyDocument.getInitialProps = async (ctx) => { 34 | // Resolution order 35 | // 36 | // On the server: 37 | // 1. app.getInitialProps 38 | // 2. page.getInitialProps 39 | // 3. document.getInitialProps 40 | // 4. app.render 41 | // 5. page.render 42 | // 6. document.render 43 | // 44 | // On the server with error: 45 | // 1. document.getInitialProps 46 | // 2. app.render 47 | // 3. page.render 48 | // 4. document.render 49 | // 50 | // On the client 51 | // 1. app.getInitialProps 52 | // 2. page.getInitialProps 53 | // 3. app.render 54 | // 4. page.render 55 | 56 | // Render app and page and get the context of the page with collected side effects. 57 | const sheets = new ServerStyleSheets(); 58 | const originalRenderPage = ctx.renderPage; 59 | 60 | ctx.renderPage = () => 61 | originalRenderPage({ 62 | enhanceApp: (App) => (props) => sheets.collect(), 63 | }); 64 | 65 | const initialProps = await Document.getInitialProps(ctx); 66 | const styles = extractCritical(initialProps.html); 67 | 68 | return { 69 | ...initialProps, 70 | // Styles fragment is rendered after the app and page rendering finish. 71 | styles: [ 72 | ...React.Children.toArray(initialProps.styles), 73 | sheets.getStyleElement(), 74 |