chore: initial
This commit is contained in:
commit
37918907b4
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/indev
|
||||||
|
/node_modules
|
15
.storybook/main.js
Normal file
15
.storybook/main.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||||
|
const config = {
|
||||||
|
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@chromatic-com/storybook',
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
13
.storybook/preview.js
Normal file
13
.storybook/preview.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/** @type { import('@storybook/react').Preview } */
|
||||||
|
const preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
54
.vscode/launch.json
vendored
Normal file
54
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Silphius Dev",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"resolveSourceMapLocations": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "dev", "--", "--host", "0.0.0.0"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Storybook Dev",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"resolveSourceMapLocations": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "storybook", "--", "--no-open"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Chrome",
|
||||||
|
"url": "",
|
||||||
|
"webRoot": "${workspaceFolder}",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://localhost:6006",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Silphius",
|
||||||
|
"configurations": [
|
||||||
|
"Silphius Dev",
|
||||||
|
"Storybook Dev",
|
||||||
|
"Chrome",
|
||||||
|
],
|
||||||
|
"stopAll": true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
12726
package-lock.json
generated
Normal file
12726
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "silphius",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "npm run vite",
|
||||||
|
"build": "npm run vite build",
|
||||||
|
"preview": "npm run vite preview",
|
||||||
|
"start": "npm run dev",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"storybook:build": "storybook build",
|
||||||
|
"vite": "vite --config vite.config.js src"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@chromatic-com/storybook": "^1.4.0",
|
||||||
|
"@storybook/addon-essentials": "^8.1.3",
|
||||||
|
"@storybook/addon-interactions": "^8.1.3",
|
||||||
|
"@storybook/addon-links": "^8.1.3",
|
||||||
|
"@storybook/addon-onboarding": "^8.1.3",
|
||||||
|
"@storybook/blocks": "^8.1.3",
|
||||||
|
"@storybook/react": "^8.1.3",
|
||||||
|
"@storybook/react-vite": "^8.1.3",
|
||||||
|
"@storybook/test": "^8.1.3",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"storybook": "^8.1.3",
|
||||||
|
"vite": "^5.2.11",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@pixi/react": "^7.1.2",
|
||||||
|
"pixi.js": "^8.1.5",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
}
|
||||||
|
}
|
48
src/add-key-input-listener.js
Normal file
48
src/add-key-input-listener.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// Handle lame OS key input event behavior. See: https://mzl.la/2Ob0WQE
|
||||||
|
// Also "up" all keys on blur.
|
||||||
|
export default function addKeyInputListener(target, listener) {
|
||||||
|
let keysDown = {};
|
||||||
|
let keyUpDelays = {};
|
||||||
|
function setAllKeysUp() {
|
||||||
|
for (const i in keyUpDelays) {
|
||||||
|
clearTimeout(keyUpDelays[i]);
|
||||||
|
}
|
||||||
|
keyUpDelays = {};
|
||||||
|
for (const key in keysDown) {
|
||||||
|
listener({type: 'keyUp', payload: key});
|
||||||
|
}
|
||||||
|
keysDown = {};
|
||||||
|
}
|
||||||
|
function onBlur() {
|
||||||
|
setAllKeysUp();
|
||||||
|
}
|
||||||
|
function onKeyDown(event) {
|
||||||
|
const {key} = event;
|
||||||
|
if (keysDown[key]) {
|
||||||
|
if (keyUpDelays[key]) {
|
||||||
|
clearTimeout(keyUpDelays[key]);
|
||||||
|
delete keyUpDelays[key];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keysDown[key] = true;
|
||||||
|
listener({type: 'keyDown', payload: key});
|
||||||
|
}
|
||||||
|
function onKeyUp(event) {
|
||||||
|
const {key} = event;
|
||||||
|
keyUpDelays[key] = setTimeout(() => {
|
||||||
|
delete keyUpDelays[key];
|
||||||
|
delete keysDown[key];
|
||||||
|
listener({type: 'keyUp', payload: key});
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
window.addEventListener('blur', onBlur);
|
||||||
|
window.addEventListener('keyup', onKeyUp);
|
||||||
|
target.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => {
|
||||||
|
setAllKeysUp();
|
||||||
|
target.removeEventListener('keydown', onKeyDown);
|
||||||
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
|
window.removeEventListener('blur', onBlur);
|
||||||
|
};
|
||||||
|
}
|
BIN
src/assets/bunny.png
Normal file
BIN
src/assets/bunny.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 449 B |
BIN
src/assets/potion.png
Normal file
BIN
src/assets/potion.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
49
src/client.js
Normal file
49
src/client.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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'})],
|
||||||
|
),
|
||||||
|
);
|
36
src/components/dom.jsx
Normal file
36
src/components/dom.jsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import {useEffect, useRef, useState} from 'react';
|
||||||
|
|
||||||
|
import {RESOLUTION} from '../constants';
|
||||||
|
import styles from './dom.module.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dom autoscales with resolution.
|
||||||
|
*/
|
||||||
|
export default function Dom({children}) {
|
||||||
|
const ref = useRef();
|
||||||
|
const [scale, setScale] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onResize() {
|
||||||
|
const {parentNode} = ref.current;
|
||||||
|
const {width} = parentNode.getBoundingClientRect();
|
||||||
|
setScale(width / RESOLUTION[0]);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
onResize();
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
}
|
||||||
|
}, [ref.current]);
|
||||||
|
return (
|
||||||
|
<div className={styles.dom} ref={ref}>
|
||||||
|
{scale > 0 && (
|
||||||
|
<style>{`.${styles.dom}{--scale:${scale}}`}</style>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
8
src/components/dom.module.css
Normal file
8
src/components/dom.module.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.dom {
|
||||||
|
height: calc(100% / var(--scale));
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
transform: scale(var(--scale));
|
||||||
|
transform-origin: top left;
|
||||||
|
width: calc(100% / var(--scale));
|
||||||
|
}
|
24
src/components/entities.jsx
Normal file
24
src/components/entities.jsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import {
|
||||||
|
Sprite,
|
||||||
|
} from '@pixi/react';
|
||||||
|
import {useContext, useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import ServerContext from '../context/server';
|
||||||
|
|
||||||
|
export default function Entities({entities}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
Object.values(entities)
|
||||||
|
.map(({image, position: [x, y]}, i) => (
|
||||||
|
<Sprite
|
||||||
|
image={image}
|
||||||
|
key={i}
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
28
src/components/hotbar.jsx
Normal file
28
src/components/hotbar.jsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import Slot from './slot';
|
||||||
|
|
||||||
|
import styles from './hotbar.module.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hotbar. 10 slots of inventory with an active selection.
|
||||||
|
*/
|
||||||
|
export default function Hotbar({active, onActivate, slots}) {
|
||||||
|
const Slots = slots.map((slot, i) => (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
[styles.slotWrapper, active === i && styles.active]
|
||||||
|
.filter(Boolean).join(' ')
|
||||||
|
}
|
||||||
|
onClick={() => onActivate(i)}
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<Slot {...slot} />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.hotbar}
|
||||||
|
>
|
||||||
|
{Slots}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
25
src/components/hotbar.module.css
Normal file
25
src/components/hotbar.module.css
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.hotbar {
|
||||||
|
border: 2px solid #999999;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
left: 135px;
|
||||||
|
line-height: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slotWrapper {
|
||||||
|
border: 2px solid #999999;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
48
src/components/pixi.jsx
Normal file
48
src/components/pixi.jsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import {
|
||||||
|
Stage,
|
||||||
|
Container,
|
||||||
|
} from '@pixi/react';
|
||||||
|
import {useContext, useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import {RESOLUTION} from '../constants.js';
|
||||||
|
import ServerContext from '../context/server';
|
||||||
|
import Entities from './entities';
|
||||||
|
import styles from './pixi.module.css';
|
||||||
|
|
||||||
|
export default function Pixi() {
|
||||||
|
const server = useContext(ServerContext);
|
||||||
|
const [entities, setEntities] = useState({});
|
||||||
|
useEffect(() => {
|
||||||
|
function onMessage(message) {
|
||||||
|
const {type, payload} = message;
|
||||||
|
switch (type) {
|
||||||
|
case 'connected': {
|
||||||
|
setEntities(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'tick': {
|
||||||
|
setEntities(payload.entities);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.addMessageListener(onMessage);
|
||||||
|
return () => {
|
||||||
|
server.removeMessageListener(onMessage);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Stage
|
||||||
|
className={styles.stage}
|
||||||
|
width={RESOLUTION[0]}
|
||||||
|
height={RESOLUTION[1]}
|
||||||
|
options={{
|
||||||
|
background: 0x1099bb,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Entities entities={entities} />
|
||||||
|
</Stage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
7
src/components/pixi.module.css
Normal file
7
src/components/pixi.module.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.stage {
|
||||||
|
height: 100% !important;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
8
src/components/silphius.jsx
Normal file
8
src/components/silphius.jsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import Ui from './ui';
|
||||||
|
|
||||||
|
export default function Silphius() {
|
||||||
|
return (
|
||||||
|
<Ui />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
0
src/components/silphius.module.css
Normal file
0
src/components/silphius.module.css
Normal file
29
src/components/slot.jsx
Normal file
29
src/components/slot.jsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import styles from './slot.module.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An inventory slot. Displays an item image and the quantity of the item if > 1.
|
||||||
|
*/
|
||||||
|
export default function Slot({image, onClick, qty = 1}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.slot}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.slotInner}
|
||||||
|
style={image ? {backgroundImage: `url(${image})`} : {}}
|
||||||
|
>
|
||||||
|
{qty > 1 && (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
[styles.qty, `q-${Math.round(Math.log10(qty))}`]
|
||||||
|
.filter(Boolean).join(' ')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{qty}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
45
src/components/slot.module.css
Normal file
45
src/components/slot.module.css
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
.slot {
|
||||||
|
--size: 35px;
|
||||||
|
--base: calc(var(--size) / 5);
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
padding: var(--base);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slotInner {
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
height: calc(var(--base) * 5);
|
||||||
|
position: relative;
|
||||||
|
width: calc(var(--base) * 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty {
|
||||||
|
bottom: calc(var(--base) / -1.25);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: calc(var(--base) * 2);
|
||||||
|
line-height: 1;
|
||||||
|
position: absolute;
|
||||||
|
right: calc(var(--base) / -1.25);
|
||||||
|
text-shadow:
|
||||||
|
0px -1px 0px white,
|
||||||
|
1px 0px 0px white,
|
||||||
|
0px 1px 0px white,
|
||||||
|
-1px 0px 0px white
|
||||||
|
;
|
||||||
|
&:global(.q-2) {
|
||||||
|
font-size: calc(var(--base) * 1.75);
|
||||||
|
}
|
||||||
|
&:global(.q-3) {
|
||||||
|
font-size: calc(var(--base) * 1.5);
|
||||||
|
}
|
||||||
|
&:global(.q-4) {
|
||||||
|
font-size: calc(var(--base) * 1.25);
|
||||||
|
}
|
||||||
|
}
|
30
src/components/ui.jsx
Normal file
30
src/components/ui.jsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import {RESOLUTION} from '../constants';
|
||||||
|
import Dom from './dom';
|
||||||
|
import Pixi from './pixi';
|
||||||
|
import styles from './ui.module.css';
|
||||||
|
|
||||||
|
const ratio = RESOLUTION[0] / RESOLUTION[1];
|
||||||
|
|
||||||
|
export default function Ui() {
|
||||||
|
return (
|
||||||
|
<div className={styles.ui}>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@media (max-aspect-ratio: ${ratio}) { .${styles.ui} { width: 100%; } }
|
||||||
|
@media (min-aspect-ratio: ${ratio}) { .${styles.ui} { height: 100%; } }
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<Pixi />
|
||||||
|
<Dom>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
height: '225px',
|
||||||
|
width: '400px',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</Dom>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
3
src/components/ui.module.css
Normal file
3
src/components/ui.module.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.ui {
|
||||||
|
position: relative;
|
||||||
|
}
|
4
src/constants.js
Normal file
4
src/constants.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const RESOLUTION = [
|
||||||
|
800,
|
||||||
|
450,
|
||||||
|
];
|
3
src/context/server.js
Normal file
3
src/context/server.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import {createContext} from 'react';
|
||||||
|
|
||||||
|
export default createContext();
|
16
src/index.css
Normal file
16
src/index.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
html, body {
|
||||||
|
background-color: #333333;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.silphius {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
|
}
|
12
src/index.html
Normal file
12
src/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<link rel="stylesheet" href="./index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" src="./client.js"></script>
|
||||||
|
<div class="silphius"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
24
src/server/local.js
Normal file
24
src/server/local.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export default class Local {
|
||||||
|
constructor() {
|
||||||
|
this.listeners = [];
|
||||||
|
this.worker = new Worker('/server/server.js');
|
||||||
|
this.worker.onmessage = (event) => {
|
||||||
|
for (const i in this.listeners) {
|
||||||
|
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) {
|
||||||
|
this.worker.postMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
54
src/server/server.js
Normal file
54
src/server/server.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
const SPEED = 100;
|
||||||
|
const TPS = 60;
|
||||||
|
|
||||||
|
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': {
|
||||||
|
if (payload.type in MOVE_MAP) {
|
||||||
|
movement[MOVE_MAP[payload.type]] = payload.value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
52
src/stories/dom-decorator.jsx
Normal file
52
src/stories/dom-decorator.jsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import {useEffect, useRef, useState} from 'react';
|
||||||
|
|
||||||
|
import Dom from '../components/dom';
|
||||||
|
import {RESOLUTION} from '../constants';
|
||||||
|
|
||||||
|
function Decorator({children, style}) {
|
||||||
|
const ref = useRef();
|
||||||
|
const [scale, setScale] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onResize() {
|
||||||
|
const {parentNode} = ref.current;
|
||||||
|
const {width} = parentNode.getBoundingClientRect();
|
||||||
|
setScale(width / RESOLUTION[0]);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
onResize();
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
}
|
||||||
|
}, [ref.current]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#1099bb',
|
||||||
|
opacity: 0 === scale ? 0 : 1,
|
||||||
|
position: 'relative',
|
||||||
|
height: `calc(${RESOLUTION[1]}px * ${scale})`,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dom>
|
||||||
|
<div style={style}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Dom>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(options = {}) {
|
||||||
|
return function decorate(Decorating) {
|
||||||
|
return (
|
||||||
|
<Decorator style={options.style}>
|
||||||
|
<Decorating />
|
||||||
|
</Decorator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
47
src/stories/hotbar.stories.js
Normal file
47
src/stories/hotbar.stories.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import {useArgs} from '@storybook/preview-api';
|
||||||
|
import {fn} from '@storybook/test';
|
||||||
|
import {createElement} from 'react';
|
||||||
|
|
||||||
|
import potion from '../assets/potion.png';
|
||||||
|
import Hotbar from '../components/hotbar';
|
||||||
|
import Dom from '../components/dom';
|
||||||
|
import DomDecorator from './dom-decorator';
|
||||||
|
|
||||||
|
const slots = Array(10).fill({});
|
||||||
|
slots[2] = {image: potion, qty: 24};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Dom/Inventory/Hotbar',
|
||||||
|
component: Hotbar,
|
||||||
|
decorators: [
|
||||||
|
(Hotbar, ctx) => {
|
||||||
|
const [, updateArgs] = useArgs();
|
||||||
|
const {onActivate} = ctx.args;
|
||||||
|
ctx.args.onActivate = (i) => {
|
||||||
|
updateArgs({active: i});
|
||||||
|
if (onActivate) {
|
||||||
|
onActivate(i);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Hotbar();
|
||||||
|
},
|
||||||
|
DomDecorator(),
|
||||||
|
],
|
||||||
|
tags: ['autodocs'],
|
||||||
|
args: {
|
||||||
|
active: 0,
|
||||||
|
onActivate: fn(),
|
||||||
|
slots,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
active: {
|
||||||
|
control: {
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 9,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = {};
|
68
src/stories/slot.stories.js
Normal file
68
src/stories/slot.stories.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import {createElement} from 'react';
|
||||||
|
|
||||||
|
import potion from '../assets/potion.png';
|
||||||
|
import Slot from '../components/slot';
|
||||||
|
import DomDecorator from './dom-decorator';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Dom/Inventory/Slot',
|
||||||
|
component: Slot,
|
||||||
|
decorators: [
|
||||||
|
DomDecorator({
|
||||||
|
style: {
|
||||||
|
border: '2px solid #999',
|
||||||
|
lineHeight: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
image: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
qty: {
|
||||||
|
control: {
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 9999,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
image: potion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Single = {
|
||||||
|
args: {
|
||||||
|
qty: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OneDigit = {
|
||||||
|
args: {
|
||||||
|
qty: 9,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TwoDigit = {
|
||||||
|
args: {
|
||||||
|
qty: 99,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThreeDigit = {
|
||||||
|
args: {
|
||||||
|
qty: 999,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FourDigit = {
|
||||||
|
args: {
|
||||||
|
qty: 9999,
|
||||||
|
},
|
||||||
|
};
|
6
vite.config.js
Normal file
6
vite.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import {defineConfig} from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user