Compare commits
5 Commits
37918907b4
...
2c3a1bbdb6
Author | SHA1 | Date | |
---|---|---|---|
|
2c3a1bbdb6 | ||
|
872a001877 | ||
|
243574ff79 | ||
|
97791cc51e | ||
|
eae2871472 |
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -11,7 +11,8 @@
|
||||||
"@pixi/react": "^7.1.2",
|
"@pixi/react": "^7.1.2",
|
||||||
"pixi.js": "^8.1.5",
|
"pixi.js": "^8.1.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1",
|
||||||
|
"ws": "^8.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "^1.4.0",
|
"@chromatic-com/storybook": "^1.4.0",
|
||||||
|
@ -12678,7 +12679,6 @@
|
||||||
"version": "8.17.0",
|
"version": "8.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,15 +22,16 @@
|
||||||
"@storybook/react": "^8.1.3",
|
"@storybook/react": "^8.1.3",
|
||||||
"@storybook/react-vite": "^8.1.3",
|
"@storybook/react-vite": "^8.1.3",
|
||||||
"@storybook/test": "^8.1.3",
|
"@storybook/test": "^8.1.3",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"storybook": "^8.1.3",
|
"storybook": "^8.1.3",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11"
|
||||||
"@vitejs/plugin-react": "^4.3.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pixi/react": "^7.1.2",
|
"@pixi/react": "^7.1.2",
|
||||||
"pixi.js": "^8.1.5",
|
"pixi.js": "^8.1.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1",
|
||||||
|
"ws": "^8.17.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Handle lame OS key input event behavior. See: https://mzl.la/2Ob0WQE
|
// Handle lame OS key input event behavior. See: https://mzl.la/2Ob0WQE
|
||||||
// Also "up" all keys on blur.
|
// Also "up" all keys on blur.
|
||||||
export default function addKeyInputListener(target, listener) {
|
export default function addKeyListener(target, listener) {
|
||||||
let keysDown = {};
|
let keysDown = {};
|
||||||
let keyUpDelays = {};
|
let keyUpDelays = {};
|
||||||
function setAllKeysUp() {
|
function setAllKeysUp() {
|
|
@ -1,49 +0,0 @@
|
||||||
import {
|
|
||||||
Application,
|
|
||||||
Assets,
|
|
||||||
Sprite,
|
|
||||||
} from 'pixi.js';
|
|
||||||
import {createElement} from 'react';
|
|
||||||
import {createRoot} from 'react-dom/client';
|
|
||||||
|
|
||||||
import addKeyInputListener from './add-key-input-listener.js';
|
|
||||||
import Silphius from './components/silphius.jsx';
|
|
||||||
import ServerContext from './context/server.js';
|
|
||||||
|
|
||||||
// Setup server connection.
|
|
||||||
import Local from './server/local.js';
|
|
||||||
const server = new Local();
|
|
||||||
server.send({type: 'connect'});
|
|
||||||
|
|
||||||
// Handle input.
|
|
||||||
const ACTION_MAP = {
|
|
||||||
w: 'moveUp',
|
|
||||||
d: 'moveRight',
|
|
||||||
s: 'moveDown',
|
|
||||||
a: 'moveLeft',
|
|
||||||
};
|
|
||||||
const KEY_MAP = {
|
|
||||||
keyDown: 1,
|
|
||||||
keyUp: 0,
|
|
||||||
};
|
|
||||||
addKeyInputListener(document.body, ({type, payload}) => {
|
|
||||||
if (type in KEY_MAP && payload in ACTION_MAP) {
|
|
||||||
server.send({
|
|
||||||
type: 'action',
|
|
||||||
payload: {
|
|
||||||
type: ACTION_MAP[payload],
|
|
||||||
value: KEY_MAP[type],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup DOM.
|
|
||||||
createRoot(document.querySelector('.silphius'))
|
|
||||||
.render(
|
|
||||||
createElement(
|
|
||||||
ServerContext.Provider,
|
|
||||||
{value: server},
|
|
||||||
[createElement(Silphius, {key: 'silphius'})],
|
|
||||||
),
|
|
||||||
);
|
|
|
@ -1,8 +1,45 @@
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import ServerContext from '../context/server.js';
|
||||||
|
import Title from './title';
|
||||||
import Ui from './ui';
|
import Ui from './ui';
|
||||||
|
|
||||||
export default function Silphius() {
|
export default function Silphius() {
|
||||||
|
const connectionTuple = useState();
|
||||||
|
const [server, setServer] = useState();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!connectionTuple[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
async function connect() {
|
||||||
|
let Server;
|
||||||
|
switch (connectionTuple[0]) {
|
||||||
|
case 'local':
|
||||||
|
({default: Server} = await import('../server/local.js'));
|
||||||
|
break;
|
||||||
|
case 'remote':
|
||||||
|
({default: Server} = await import('../server/remote.js'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const server = new Server();
|
||||||
|
await server.connect();
|
||||||
|
server.send({type: 'connect'});
|
||||||
|
setServer(server);
|
||||||
|
}
|
||||||
|
connect();
|
||||||
|
}, [connectionTuple[0]]);
|
||||||
return (
|
return (
|
||||||
|
connectionTuple[0]
|
||||||
|
? (
|
||||||
|
server
|
||||||
|
? (
|
||||||
|
<ServerContext.Provider value={server}>
|
||||||
<Ui />
|
<Ui />
|
||||||
|
</ServerContext.Provider>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
: <Title connectionTuple={connectionTuple} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
src/components/title.jsx
Normal file
25
src/components/title.jsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import styles from './title.module.css';
|
||||||
|
|
||||||
|
export default function Title({connectionTuple}) {
|
||||||
|
return (
|
||||||
|
<div className={styles.title}>
|
||||||
|
<h1>Silphius</h1>
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
onClick={() => {
|
||||||
|
connectionTuple[1]('local')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Local
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
onClick={() => {
|
||||||
|
connectionTuple[1]('remote')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remote
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
13
src/components/title.module.css
Normal file
13
src/components/title.module.css
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.title h1 {
|
||||||
|
font-size: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title ul {
|
||||||
|
padding-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title li {
|
||||||
|
font-size: 4em;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0.25em 0;
|
||||||
|
}
|
|
@ -1,11 +1,42 @@
|
||||||
|
import {useContext, useEffect} from 'react';
|
||||||
|
|
||||||
|
import addKeyListener from '../add-key-listener.js';
|
||||||
import {RESOLUTION} from '../constants';
|
import {RESOLUTION} from '../constants';
|
||||||
|
import ServerContext from '../context/server';
|
||||||
import Dom from './dom';
|
import Dom from './dom';
|
||||||
import Pixi from './pixi';
|
import Pixi from './pixi';
|
||||||
import styles from './ui.module.css';
|
import styles from './ui.module.css';
|
||||||
|
|
||||||
const ratio = RESOLUTION[0] / RESOLUTION[1];
|
const ratio = RESOLUTION[0] / RESOLUTION[1];
|
||||||
|
|
||||||
|
// Handle input.
|
||||||
|
const ACTION_MAP = {
|
||||||
|
w: 'moveUp',
|
||||||
|
d: 'moveRight',
|
||||||
|
s: 'moveDown',
|
||||||
|
a: 'moveLeft',
|
||||||
|
};
|
||||||
|
const KEY_MAP = {
|
||||||
|
keyDown: 1,
|
||||||
|
keyUp: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export default function Ui() {
|
export default function Ui() {
|
||||||
|
// Key input.
|
||||||
|
const server = useContext(ServerContext);
|
||||||
|
useEffect(() => {
|
||||||
|
return addKeyListener(document.body, ({type, payload}) => {
|
||||||
|
if (type in KEY_MAP && payload in ACTION_MAP) {
|
||||||
|
server.send({
|
||||||
|
type: 'action',
|
||||||
|
payload: {
|
||||||
|
type: ACTION_MAP[payload],
|
||||||
|
value: KEY_MAP[type],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className={styles.ui}>
|
<div className={styles.ui}>
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
.ui {
|
.ui {
|
||||||
|
align-self: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,9 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.silphius {
|
.silphius {
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
line-height: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<link rel="stylesheet" href="./index.css">
|
<link rel="stylesheet" href="./index.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module" src="./client.js"></script>
|
<script type="module" src="./index.js"></script>
|
||||||
<div class="silphius"></div>
|
<div class="silphius"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
8
src/index.js
Normal file
8
src/index.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import {createElement} from 'react';
|
||||||
|
import {createRoot} from 'react-dom/client';
|
||||||
|
|
||||||
|
import Silphius from './components/silphius.jsx';
|
||||||
|
|
||||||
|
// Setup DOM.
|
||||||
|
createRoot(document.querySelector('.silphius'))
|
||||||
|
.render(createElement(Silphius));
|
65
src/server/engine.js
Normal file
65
src/server/engine.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
const SPEED = 100;
|
||||||
|
const TPS = 60;
|
||||||
|
|
||||||
|
const MOVE_MAP = {
|
||||||
|
'moveUp': 0,
|
||||||
|
'moveRight': 1,
|
||||||
|
'moveDown': 2,
|
||||||
|
'moveLeft': 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Engine {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.dude = {
|
||||||
|
image: './assets/bunny.png',
|
||||||
|
movement: [0, 0, 0, 0],
|
||||||
|
position: [50, 50],
|
||||||
|
};
|
||||||
|
this.frame = 0;
|
||||||
|
this.last = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
accept({type, payload}) {
|
||||||
|
switch (type) {
|
||||||
|
case 'connect': {
|
||||||
|
this.send({type: 'connected', payload: {dude: this.dude}});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'action': {
|
||||||
|
if (payload.type in MOVE_MAP) {
|
||||||
|
this.dude.movement[MOVE_MAP[payload.type]] = payload.value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data) {
|
||||||
|
throw new Exception('Engine::send is virtual!');
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
return setInterval(() => {
|
||||||
|
const elapsed = (Date.now() - this.last) / 1000;
|
||||||
|
this.last = Date.now();
|
||||||
|
this.tick(elapsed);
|
||||||
|
}, 1000 / TPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(elapsed) {
|
||||||
|
this.dude.position[0] += SPEED * elapsed * (this.dude.movement[1] - this.dude.movement[3]);
|
||||||
|
this.dude.position[1] += SPEED * elapsed * (this.dude.movement[2] - this.dude.movement[0]);
|
||||||
|
this.send({
|
||||||
|
type: 'tick',
|
||||||
|
payload: {
|
||||||
|
entities: {dude: this.dude},
|
||||||
|
elapsed,
|
||||||
|
frame: this.frame,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.frame += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,22 +1,12 @@
|
||||||
export default class Local {
|
import Server from './server.js';
|
||||||
constructor() {
|
|
||||||
this.listeners = [];
|
export default class Local extends Server {
|
||||||
this.worker = new Worker('/server/server.js');
|
async connect() {
|
||||||
|
this.worker = new Worker('/server/worker.js', {type: 'module'});
|
||||||
this.worker.onmessage = (event) => {
|
this.worker.onmessage = (event) => {
|
||||||
for (const i in this.listeners) {
|
this.accept(event.data);
|
||||||
this.listeners[i](event.data);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
addMessageListener(listener) {
|
|
||||||
this.listeners.push(listener);
|
|
||||||
}
|
|
||||||
removeMessageListener(listener) {
|
|
||||||
const index = this.listeners.indexOf(listener);
|
|
||||||
if (-1 !== index) {
|
|
||||||
this.listeners.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
send(message) {
|
send(message) {
|
||||||
this.worker.postMessage(message);
|
this.worker.postMessage(message);
|
||||||
}
|
}
|
||||||
|
|
19
src/server/remote.js
Normal file
19
src/server/remote.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Server from './server.js';
|
||||||
|
|
||||||
|
export default class Remote extends Server {
|
||||||
|
async connect() {
|
||||||
|
this.socket = new WebSocket('/ws');
|
||||||
|
this.socket.onmessage = (event) => {
|
||||||
|
for (const i in this.listeners) {
|
||||||
|
this.listeners[i](JSON.parse(event.data));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
this.socket.onopen = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
send(message) {
|
||||||
|
this.socket.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,54 +1,19 @@
|
||||||
const SPEED = 100;
|
export default class Server {
|
||||||
const TPS = 60;
|
constructor() {
|
||||||
|
this.listeners = [];
|
||||||
const dude = {
|
|
||||||
image: './assets/bunny.png',
|
|
||||||
position: [50, 50],
|
|
||||||
};
|
|
||||||
let frame = 0;
|
|
||||||
const movement = [0, 0, 0, 0];
|
|
||||||
|
|
||||||
const MOVE_MAP = {
|
|
||||||
'moveUp': 0,
|
|
||||||
'moveRight': 1,
|
|
||||||
'moveDown': 2,
|
|
||||||
'moveLeft': 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
onmessage = ({data}) => {
|
|
||||||
const {type, payload} = data;
|
|
||||||
switch (type) {
|
|
||||||
case 'connect': {
|
|
||||||
postMessage({type: 'connected', payload: {dude}});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case 'action': {
|
accept(data) {
|
||||||
if (payload.type in MOVE_MAP) {
|
for (const i in this.listeners) {
|
||||||
movement[MOVE_MAP[payload.type]] = payload.value;
|
this.listeners[i](data);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
default:
|
addMessageListener(listener) {
|
||||||
|
this.listeners.push(listener);
|
||||||
|
}
|
||||||
|
removeMessageListener(listener) {
|
||||||
|
const index = this.listeners.indexOf(listener);
|
||||||
|
if (-1 !== index) {
|
||||||
|
this.listeners.splice(index, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function tick(elapsed) {
|
|
||||||
dude.position[0] += SPEED * elapsed * (movement[1] - movement[3]);
|
|
||||||
dude.position[1] += SPEED * elapsed * (movement[2] - movement[0]);
|
|
||||||
postMessage({
|
|
||||||
type: 'tick',
|
|
||||||
payload: {
|
|
||||||
entities: {dude},
|
|
||||||
elapsed,
|
|
||||||
frame,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
frame += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let last = Date.now();
|
|
||||||
setInterval(() => {
|
|
||||||
const elapsed = (Date.now() - last) / 1000;
|
|
||||||
last = Date.now();
|
|
||||||
tick(elapsed);
|
|
||||||
}, 1000 / TPS);
|
|
||||||
|
|
19
src/server/socket.js
Normal file
19
src/server/socket.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {WebSocketServer} from 'ws';
|
||||||
|
|
||||||
|
import Engine from './engine.js';
|
||||||
|
|
||||||
|
const {
|
||||||
|
SILPHIUS_WEBSOCKET_PORT = 8080
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({port: SILPHIUS_WEBSOCKET_PORT});
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
const server = new class WorkerEngine extends Engine {
|
||||||
|
send(data) { ws.send(JSON.stringify(data)); }
|
||||||
|
}
|
||||||
|
ws.on('message', function message(data) {
|
||||||
|
server.accept(JSON.parse(data));
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
});
|
9
src/server/worker.js
Normal file
9
src/server/worker.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import Engine from './engine.js';
|
||||||
|
|
||||||
|
const server = new class WorkerEngine extends Engine {
|
||||||
|
send(data) { postMessage(data); }
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage = ({data}) => { server.accept(data); };
|
||||||
|
|
||||||
|
server.start();
|
|
@ -1,6 +1,21 @@
|
||||||
import {defineConfig} from 'vite'
|
import {defineConfig} from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
const {
|
||||||
|
SILPHIUS_WEBSOCKET_PORT = 8080
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
proxy: {
|
||||||
|
'^/ws$': {
|
||||||
|
target: `ws://127.0.0.1:${SILPHIUS_WEBSOCKET_PORT}`,
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: path => path.replace(/^\/ws/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user