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