Compare commits
24 Commits
dd456743f8
...
6957365723
Author | SHA1 | Date | |
---|---|---|---|
|
6957365723 | ||
|
f68f8d359e | ||
|
bfdd55e44a | ||
|
49b3fc3c46 | ||
|
9833e2ba16 | ||
|
bbdfe3b813 | ||
|
d89f744003 | ||
|
05e1bb5f92 | ||
|
bd6dade614 | ||
|
a670e201d6 | ||
|
349a93ab4b | ||
|
767f014107 | ||
|
13b457210e | ||
|
da13852216 | ||
|
2c2bfcbf0c | ||
|
82fd31802b | ||
|
0d8cdff6d7 | ||
|
578e796090 | ||
|
16871b0919 | ||
|
7f8bb9755f | ||
|
4529d2e8d3 | ||
|
94685e4654 | ||
|
908a6fd986 | ||
|
5a4666ae49 |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,8 +1,8 @@
|
|||
node_modules
|
||||
|
||||
/app/data
|
||||
/.cache
|
||||
/build
|
||||
/coverage
|
||||
/data
|
||||
/dev
|
||||
.env
|
||||
|
|
|
@ -37,14 +37,18 @@ export default function traverse(node, visitor) {
|
|||
throw new Error(`node type ${node.type} not traversable. (${Object.keys(node).join(', ')})`);
|
||||
}
|
||||
visitor(node, 'enter');
|
||||
const path = TRAVERSAL_PATH[node.type];
|
||||
const children = [];
|
||||
for (const key of path) {
|
||||
children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
|
||||
}
|
||||
for (const child of children) {
|
||||
if (child) {
|
||||
traverse(child, visitor);
|
||||
for (const key of TRAVERSAL_PATH[node.type]) {
|
||||
if (Array.isArray(node[key])) {
|
||||
for (const child of node[key]) {
|
||||
if (child) {
|
||||
traverse(child, visitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (node[key]) {
|
||||
traverse(node[key], visitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
visitor(node, 'exit');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Components from '@/ecs-components/index.js';
|
||||
import Systems from '@/ecs-systems/index.js';
|
||||
import Components from '@/ecs/components/index.js';
|
||||
import Systems from '@/ecs/systems/index.js';
|
||||
|
||||
export default function createEcs(Ecs) {
|
||||
const ecs = new Ecs({Components, Systems});
|
|
@ -1,4 +1,4 @@
|
|||
import createEcs from './create-ecs.js';
|
||||
import createEcs from './ecs.js';
|
||||
|
||||
export default async function createHouse(Ecs, id) {
|
||||
const ecs = createEcs(Ecs);
|
|
@ -41,6 +41,7 @@ export default async function createPlayer(id) {
|
|||
},
|
||||
},
|
||||
Health: {health: 100},
|
||||
Light: {},
|
||||
Magnet: {strength: 24},
|
||||
Player: {},
|
||||
Position: {x: 128, y: 128},
|
|
@ -1,56 +0,0 @@
|
|||
import mdx from 'remark-mdx';
|
||||
import parse from 'remark-parse';
|
||||
import {unified} from 'unified';
|
||||
import {visitParents as visit} from 'unist-util-visit-parents';
|
||||
|
||||
const parser = unified().use(parse).use(mdx);
|
||||
|
||||
function computeParams(ancestors) {
|
||||
const params = {};
|
||||
for (let i = ancestors.length - 1; i >= 0; --i) {
|
||||
const {dialogue} = ancestors[i];
|
||||
if (dialogue) {
|
||||
if (!(dialogue.name in params)) {
|
||||
params[dialogue.name] = dialogue.params;
|
||||
}
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export function parseLetters(source) {
|
||||
const tree = parser.parse(source);
|
||||
tree.dialogue = {
|
||||
name: 'rate',
|
||||
params: {frequency: 0.05, length: 0},
|
||||
};
|
||||
const letters = [];
|
||||
visit(tree, (node, ancestors) => {
|
||||
switch (node.type) {
|
||||
case 'mdxJsxFlowElement':
|
||||
case 'mdxJsxTextElement': {
|
||||
node.dialogue = {name: node.name, params: {length: 0}};
|
||||
for (const {name, value: {value}} of node.attributes) {
|
||||
node.dialogue.params[name] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'text': {
|
||||
const params = computeParams(ancestors);
|
||||
const split = node.value.split('');
|
||||
for (let i = 0; i < split.length; ++i) {
|
||||
const indices = {};
|
||||
for (const name in params) {
|
||||
indices[name] = i + params[name].length;
|
||||
}
|
||||
letters.push({character: split[i], indices, params});
|
||||
}
|
||||
for (const name in params) {
|
||||
params[name].length += split.length;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
return letters;
|
||||
}
|
7
app/ecs/components/light.js
Normal file
7
app/ecs/components/light.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Light extends Component {
|
||||
static properties = {
|
||||
radius: {type: 'uint8'},
|
||||
};
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import {CHUNK_SIZE} from '@/constants.js';
|
||||
import Component from '@/ecs/component.js';
|
||||
import {CHUNK_SIZE} from '@/util/constants.js';
|
||||
import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js';
|
||||
|
||||
import vector2d from './helpers/vector-2d';
|
||||
|
@ -99,7 +99,9 @@ class LayerProxy {
|
|||
return this.instance.layers[this.index];
|
||||
}
|
||||
async load() {
|
||||
this.$$sourceJson = await this.Component.ecs.readJson(this.layer.source);
|
||||
this.$$sourceJson = this.layer.source
|
||||
? await this.Component.ecs.readJson(this.layer.source)
|
||||
: {};
|
||||
}
|
||||
get source() {
|
||||
return this.layer.source;
|
|
@ -1,5 +1,5 @@
|
|||
import {IRL_MINUTES_PER_GAME_DAY} from '@/constants';
|
||||
import Component from '@/ecs/component.js';
|
||||
import {IRL_MINUTES_PER_GAME_DAY} from '@/util/constants';
|
||||
|
||||
const realSecondsPerGameDay = 60 * IRL_MINUTES_PER_GAME_DAY;
|
||||
const realSecondsPerGameHour = realSecondsPerGameDay / 24;
|
|
@ -1,5 +1,5 @@
|
|||
// import {RESOLUTION} from '@/constants.js'
|
||||
import {System} from '@/ecs/index.js';
|
||||
// import {RESOLUTION} from '@/util/constants.js'
|
||||
|
||||
// const [hx, hy] = [RESOLUTION.x / 2, RESOLUTION.y / 2];
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import Ecs from '@/ecs/ecs.js';
|
||||
import {decode, encode} from '@/packets/index.js';
|
||||
import {
|
||||
CHUNK_SIZE,
|
||||
RESOLUTION,
|
||||
TPS,
|
||||
} from '@/constants.js';
|
||||
import Ecs from '@/ecs/ecs.js';
|
||||
import {decode, encode} from '@/packets/index.js';
|
||||
} from '@/util/constants.js';
|
||||
import Script from '@/util/script.js';
|
||||
|
||||
import createEcs from './create-ecs.js';
|
||||
import createForest from './create-forest.js';
|
||||
import createHomestead from './create-homestead.js';
|
||||
import createHouse from './create-house.js';
|
||||
import createPlayer from './create-player.js';
|
||||
import createEcs from './create/ecs.js';
|
||||
import createForest from './create/forest.js';
|
||||
import createHomestead from './create/homestead.js';
|
||||
import createHouse from './create/house.js';
|
||||
import createPlayer from './create/player.js';
|
||||
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
|
@ -252,6 +252,10 @@ export default class Engine {
|
|||
);
|
||||
}
|
||||
|
||||
createEcs() {
|
||||
return createEcs(this.Ecs);
|
||||
}
|
||||
|
||||
async disconnectPlayer(connection) {
|
||||
const connectedPlayer = this.connectedPlayers.get(connection);
|
||||
if (!connectedPlayer) {
|
||||
|
@ -270,7 +274,7 @@ export default class Engine {
|
|||
|
||||
async loadEcs(path) {
|
||||
this.ecses[path] = await this.Ecs.deserialize(
|
||||
createEcs(this.Ecs),
|
||||
this.createEcs(),
|
||||
await this.server.readData(path),
|
||||
);
|
||||
}
|
||||
|
@ -284,7 +288,7 @@ export default class Engine {
|
|||
if ('ENOENT' !== error.code) {
|
||||
throw error;
|
||||
}
|
||||
const homestead = createEcs(this.Ecs);
|
||||
const homestead = this.createEcs();
|
||||
for (const entity of await createHomestead(id)) {
|
||||
await homestead.create(entity);
|
||||
}
|
||||
|
@ -297,7 +301,7 @@ export default class Engine {
|
|||
['houses', `${id}`].join('/'),
|
||||
house,
|
||||
);
|
||||
const forest = createEcs(this.Ecs);
|
||||
const forest = this.createEcs();
|
||||
for (const entity of await createForest()) {
|
||||
await forest.create(entity);
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js'
|
||||
import Server from '@/net/server/server.js';
|
||||
|
||||
import Engine from './engine.js';
|
||||
|
||||
class TestServer extends Server {
|
||||
constructor() {
|
||||
super();
|
||||
this.data = {};
|
||||
}
|
||||
async readAsset() {
|
||||
return new ArrayBuffer(0);
|
||||
}
|
||||
async readData(path) {
|
||||
if (path in this.data) {
|
||||
return this.data[path];
|
||||
}
|
||||
const error = new Error();
|
||||
error.code = 'ENOENT';
|
||||
throw error;
|
||||
}
|
||||
async writeData(path, view) {
|
||||
this.data[path] = view;
|
||||
}
|
||||
}
|
||||
|
||||
test('visibility-based updates', async () => {
|
||||
// const engine = new Engine(TestServer);
|
||||
// // Connect an entity.
|
||||
// await engine.connectPlayer(0, 0);
|
||||
// const ecs = engine.ecses['homesteads/0'];
|
||||
// // Create an entity.
|
||||
// const entity = ecs.get(await ecs.create({
|
||||
// Forces: {forceX: 1},
|
||||
// Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
||||
// Sprite: {
|
||||
// anchor: {x: 0.5, y: 0.8},
|
||||
// animation: 'moving:down',
|
||||
// frame: 0,
|
||||
// frames: 8,
|
||||
// source: '/assets/dude/dude.json',
|
||||
// speed: 0.115,
|
||||
// },
|
||||
// VisibleAabb: {},
|
||||
// }));
|
||||
// const {entity: mainEntity} = engine.connectedPlayers.get(0);
|
||||
// // Tick and get update. Should be a full update.
|
||||
// engine.tick(1);
|
||||
// expect(engine.updateFor(0))
|
||||
// .to.deep.include({[mainEntity.id]: {MainEntity: {}, ...ecs.get(mainEntity.id).toJSON()}, [entity.id]: ecs.get(entity.id).toJSON()});
|
||||
// engine.setClean();
|
||||
// // Tick and get update. Should be a partial update.
|
||||
// engine.tick(1);
|
||||
// expect(engine.updateFor(0))
|
||||
// .to.deep.include({
|
||||
// [entity.id]: {
|
||||
// Position: {x: (RESOLUTION.x * 1.5) + 32 - 1},
|
||||
// VisibleAabb: {
|
||||
// x0: 1199,
|
||||
// x1: 1263,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// engine.setClean();
|
||||
// // Tick and get update. Should remove the entity.
|
||||
// engine.tick(1);
|
||||
// expect(engine.updateFor(0))
|
||||
// .to.deep.include({[entity.id]: false});
|
||||
// // Aim back toward visible area and tick. Should be a full update for that entity.
|
||||
// engine.setClean();
|
||||
// entity.Forces.forceX = -1;
|
||||
// engine.tick(1);
|
||||
// expect(engine.updateFor(0))
|
||||
// .to.deep.include({[entity.id]: ecs.get(entity.id).toJSON()});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import {CLIENT_LATENCY} from '@/constants.js';
|
||||
import {CLIENT_LATENCY} from '@/util/constants.js';
|
||||
|
||||
export default class Client {
|
||||
constructor() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {CLIENT_PREDICTION} from '@/constants.js';
|
||||
import {encode} from '@/packets/index.js';
|
||||
import {CLIENT_PREDICTION} from '@/util/constants.js';
|
||||
|
||||
import Client from './client.js';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {SERVER_LATENCY} from '@/constants.js';
|
||||
import {SERVER_LATENCY} from '@/util/constants.js';
|
||||
|
||||
export default class Server {
|
||||
constructor() {
|
||||
|
|
|
@ -2,10 +2,10 @@ import {del, get, set} from 'idb-keyval';
|
|||
|
||||
import {encode} from '@/packets/index.js';
|
||||
|
||||
import createEcs from '../../create-ecs.js';
|
||||
import '../../create-forest.js';
|
||||
import '../../create-homestead.js';
|
||||
import '../../create-player.js';
|
||||
import createEcs from '../../create/ecs.js';
|
||||
import '../../create/forest.js';
|
||||
import '../../create/homestead.js';
|
||||
import '../../create/player.js';
|
||||
|
||||
import Engine from '../../engine.js';
|
||||
import Server from './server.js';
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {useClient} from '@/context/client.js';
|
||||
|
||||
import styles from './chat.module.css';
|
||||
|
||||
export default function Chat({
|
||||
chatHistory,
|
||||
onClose,
|
||||
message,
|
||||
setChatHistory,
|
||||
setMessage,
|
||||
}) {
|
||||
const client = useClient();
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
const [historyCaret, setHistoryCaret] = useState(0);
|
||||
useEffect(() => {
|
||||
setDisabled(false);
|
||||
}, []);
|
||||
return (
|
||||
<div className={styles.chat}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
if (message) {
|
||||
client.send({
|
||||
type: 'Action',
|
||||
payload: {type: 'chat', value: message},
|
||||
});
|
||||
setChatHistory([message, ...chatHistory]);
|
||||
setMessage('');
|
||||
onClose();
|
||||
}
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
setMessage(event.target.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown': {
|
||||
if (0 === historyCaret) {
|
||||
break;
|
||||
}
|
||||
let localHistoryCaret = historyCaret - 1;
|
||||
setMessage(chatHistory[localHistoryCaret])
|
||||
setHistoryCaret(localHistoryCaret);
|
||||
if (0 === localHistoryCaret) {
|
||||
setChatHistory(chatHistory.slice(1));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
if (historyCaret === chatHistory.length - 1) {
|
||||
break;
|
||||
}
|
||||
let localHistoryCaret = historyCaret;
|
||||
let localChatHistory = chatHistory;
|
||||
if (0 === historyCaret) {
|
||||
localChatHistory = [message, ...localChatHistory];
|
||||
setChatHistory(localChatHistory);
|
||||
}
|
||||
localHistoryCaret += 1;
|
||||
setMessage(localChatHistory[localHistoryCaret])
|
||||
setHistoryCaret(localHistoryCaret);
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
onClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
maxLength="255"
|
||||
ref={(element) => {
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
value={message}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import {Sprite as PixiSprite} from '@pixi/react';
|
||||
|
||||
import {useAsset} from '@/context/assets.js';
|
||||
|
||||
export default function Sprite({entity, ...rest}) {
|
||||
const asset = useAsset(entity.Sprite.source);
|
||||
if (!asset) {
|
||||
return false;
|
||||
}
|
||||
let texture;
|
||||
if (asset.data.animations) {
|
||||
texture = asset.animations[entity.Sprite.animation][entity.Sprite.frame];
|
||||
}
|
||||
else {
|
||||
texture = asset.textures[''];
|
||||
}
|
||||
return (
|
||||
<PixiSprite
|
||||
anchor={entity.Sprite.anchor}
|
||||
scale={entity.Sprite.scale}
|
||||
texture={texture}
|
||||
x={Math.round(entity.Position.x)}
|
||||
y={Math.round(entity.Position.y)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -2,8 +2,8 @@ import {useState} from 'react';
|
|||
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
|
||||
import 'react-tabs/style/react-tabs.css';
|
||||
|
||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||
|
||||
import styles from './devtools.module.css';
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import {useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {useClient} from '@/context/client.js';
|
||||
import {useEcs} from '@/context/ecs.js';
|
||||
import useRect from '@/util/react-hooks/use-rect.js';
|
||||
import {useClient} from '@/react/context/client.js';
|
||||
import {useEcs} from '@/react/context/ecs.js';
|
||||
import useRect from '@/react/hooks/use-rect.js';
|
||||
|
||||
import styles from './tiles.module.css';
|
||||
|
47
app/react/components/dom/chat/chat.jsx
Normal file
47
app/react/components/dom/chat/chat.jsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import styles from './chat.module.css';
|
||||
|
||||
import Input from './input.jsx';
|
||||
import Messages from './messages.jsx';
|
||||
|
||||
export default function Chat({
|
||||
chatHistory,
|
||||
chatHistoryCaret,
|
||||
chatInputRef,
|
||||
chatMessages,
|
||||
onClose,
|
||||
message,
|
||||
pendingMessage,
|
||||
setChatHistoryCaret,
|
||||
setChatHistory,
|
||||
setMessage,
|
||||
setPendingMessage,
|
||||
}) {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
onMouseDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
className={styles.chat}
|
||||
>
|
||||
<Messages
|
||||
chatMessages={chatMessages}
|
||||
/>
|
||||
<Input
|
||||
chatHistory={chatHistory}
|
||||
chatHistoryCaret={chatHistoryCaret}
|
||||
onClose={onClose}
|
||||
message={message}
|
||||
chatInputRef={chatInputRef}
|
||||
pendingMessage={pendingMessage}
|
||||
setChatHistory={setChatHistory}
|
||||
setChatHistoryCaret={setChatHistoryCaret}
|
||||
setMessage={setMessage}
|
||||
setPendingMessage={setPendingMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
16
app/react/components/dom/chat/chat.module.css
Normal file
16
app/react/components/dom/chat/chat.module.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
.chat {
|
||||
background-color: #00000044;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||
max-height: 25%;
|
||||
position: absolute;
|
||||
user-select: text;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Cookbook";
|
||||
src: url("/assets/fonts/Cookbook.woff") format("woff");
|
||||
}
|
89
app/react/components/dom/chat/input.jsx
Normal file
89
app/react/components/dom/chat/input.jsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {useClient} from '@/react/context/client.js';
|
||||
|
||||
import styles from './input.module.css';
|
||||
|
||||
export default function ChatInput({
|
||||
setChatHistoryCaret,
|
||||
chatHistory,
|
||||
chatHistoryCaret,
|
||||
chatInputRef,
|
||||
message,
|
||||
pendingMessage,
|
||||
setChatHistory,
|
||||
setMessage,
|
||||
setPendingMessage,
|
||||
}) {
|
||||
const client = useClient();
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
useEffect(() => {
|
||||
setDisabled(false);
|
||||
}, []);
|
||||
return (
|
||||
<form
|
||||
className={styles.input}
|
||||
onSubmit={(event) => {
|
||||
if (message) {
|
||||
client.send({
|
||||
type: 'Action',
|
||||
payload: {type: 'chat', value: message},
|
||||
});
|
||||
setChatHistory([message, ...chatHistory]);
|
||||
setChatHistoryCaret(-1);
|
||||
setPendingMessage('');
|
||||
setMessage('');
|
||||
}
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
setMessage(event.target.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown': {
|
||||
if (-1 === chatHistoryCaret) {
|
||||
break;
|
||||
}
|
||||
else if (0 === chatHistoryCaret) {
|
||||
setMessage(pendingMessage);
|
||||
setPendingMessage('');
|
||||
}
|
||||
else {
|
||||
setMessage(chatHistory[chatHistoryCaret - 1])
|
||||
}
|
||||
setChatHistoryCaret(chatHistoryCaret - 1);
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
if (chatHistory.length - 1 === chatHistoryCaret) {
|
||||
break;
|
||||
}
|
||||
if (-1 === chatHistoryCaret) {
|
||||
setPendingMessage(message);
|
||||
}
|
||||
setMessage(chatHistory[chatHistoryCaret + 1])
|
||||
setChatHistoryCaret(chatHistoryCaret + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
maxLength="255"
|
||||
ref={(element) => {
|
||||
chatInputRef.current = element;
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
value={message}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
.chat {
|
||||
background-color: #00000044;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat form {
|
||||
.input {
|
||||
line-height: 0;
|
||||
input[type="text"] {
|
||||
background-color: #00000044;
|
||||
border: 1px solid #333333;
|
||||
color: #ffffff;
|
||||
font-family: "Cookbook";
|
||||
font-size: 16px;
|
||||
margin: 4px;
|
||||
padding: 0;
|
||||
padding-left: 4px;
|
||||
width: calc(100% - 8px);
|
||||
&:focus-visible {
|
||||
border: 1px solid #999999;
|
32
app/react/components/dom/chat/message.jsx
Normal file
32
app/react/components/dom/chat/message.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {memo, useEffect, useState} from 'react';
|
||||
|
||||
import {useRadians} from '@/react/context/radians.js';
|
||||
import {render} from '@/util/dialogue.js';
|
||||
|
||||
import styles from './message.module.css';
|
||||
|
||||
function Message({letters}) {
|
||||
const radians = useRadians();
|
||||
const [localRadians, setLocalRadians] = useState(radians);
|
||||
const [isAnimating, setIsAnimating] = useState(true);
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(() => {
|
||||
setIsAnimating(false);
|
||||
}, 2_000);
|
||||
return () => {
|
||||
clearTimeout(handle);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (isAnimating) {
|
||||
setLocalRadians(radians)
|
||||
}
|
||||
}, [isAnimating, radians]);
|
||||
return (
|
||||
<div className={styles.message}>
|
||||
{render(letters, '')(letters.length - 1, localRadians)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Message);
|
4
app/react/components/dom/chat/message.module.css
Normal file
4
app/react/components/dom/chat/message.module.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.message {
|
||||
color: #ffffff;
|
||||
margin-left: 8px;
|
||||
}
|
20
app/react/components/dom/chat/messages.jsx
Normal file
20
app/react/components/dom/chat/messages.jsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import Message from './message.jsx';
|
||||
|
||||
import styles from './messages.module.css';
|
||||
|
||||
export default function Messages({chatMessages}) {
|
||||
const messages = [];
|
||||
for (const key in chatMessages) {
|
||||
messages.push(
|
||||
<Message
|
||||
key={key}
|
||||
letters={chatMessages[key]}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={styles.messages}>
|
||||
{messages}
|
||||
</div>
|
||||
)
|
||||
}
|
5
app/react/components/dom/chat/messages.module.css
Normal file
5
app/react/components/dom/chat/messages.module.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.messages {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import {useEffect, useRef, useState} from 'react';
|
||||
import {useEffect, useMemo, useRef, useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import {useDomScale} from '@/context/dom-scale.js';
|
||||
import {TAU} from '@/util/math.js';
|
||||
import {useDomScale} from '@/react/context/dom-scale.js';
|
||||
import {useRadians} from '@/react/context/radians.js';
|
||||
import {RESOLUTION} from '@/util/constants.js';
|
||||
import {render} from '@/util/dialogue.js';
|
||||
|
||||
import styles from './dialogue.module.css';
|
||||
|
||||
|
@ -17,7 +18,7 @@ export default function Dialogue({
|
|||
const ref = useRef();
|
||||
const [dimensions, setDimensions] = useState({h: 0, w: 0});
|
||||
const [caret, setCaret] = useState(0);
|
||||
const [radians, setRadians] = useState(0);
|
||||
const radians = useRadians();
|
||||
useEffect(() => {
|
||||
return dialogue.addSkipListener(() => {
|
||||
if (caret >= dialogue.letters.length - 1) {
|
||||
|
@ -28,24 +29,6 @@ export default function Dialogue({
|
|||
}
|
||||
});
|
||||
}, [caret, dialogue]);
|
||||
useEffect(() => {
|
||||
setRadians(0);
|
||||
let handle;
|
||||
let last;
|
||||
const spin = (ts) => {
|
||||
if ('undefined' === typeof last) {
|
||||
last = ts;
|
||||
}
|
||||
const elapsed = (ts - last) / 1000;
|
||||
last = ts;
|
||||
setRadians((radians) => radians + (elapsed * TAU));
|
||||
handle = requestAnimationFrame(spin);
|
||||
};
|
||||
handle = requestAnimationFrame(spin);
|
||||
return () => {
|
||||
cancelAnimationFrame(handle);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const {params} = dialogue.letters[caret];
|
||||
let handle;
|
||||
|
@ -88,6 +71,10 @@ export default function Dialogue({
|
|||
cancelAnimationFrame(handle);
|
||||
};
|
||||
}, [dialogue, domScale, ref]);
|
||||
const localRender = useMemo(
|
||||
() => render(dialogue.letters, styles.letter),
|
||||
[dialogue.letters],
|
||||
);
|
||||
const origin = 'function' === typeof dialogue.origin
|
||||
? dialogue.origin()
|
||||
: dialogue.origin || {x: 0, y: 0};
|
||||
|
@ -175,62 +162,7 @@ export default function Dialogue({
|
|||
<polygon points="0 0, 24 0, 12 24" />
|
||||
</svg>
|
||||
<p className={styles.letters}>
|
||||
{
|
||||
dialogue.letters
|
||||
.map(({character, indices, params}, i) => {
|
||||
let color = 'inherit';
|
||||
let fade = 0;
|
||||
let fontStyle = 'normal';
|
||||
let fontWeight = 'normal';
|
||||
let left = 0;
|
||||
let opacity = 1;
|
||||
let top = 0;
|
||||
if (params.blink) {
|
||||
const {frequency = 1} = params.blink;
|
||||
opacity = (radians * (2 / frequency) % TAU) > (TAU / 2) ? opacity : 0;
|
||||
}
|
||||
if (params.fade) {
|
||||
const {frequency = 1} = params.fade;
|
||||
fade = frequency;
|
||||
}
|
||||
if (params.wave) {
|
||||
const {frequency = 1, magnitude = 3} = params.wave;
|
||||
top += magnitude * Math.cos((radians * (1 / frequency)) + TAU * indices.wave / params.wave.length);
|
||||
}
|
||||
if (params.rainbow) {
|
||||
const {frequency = 1} = params.rainbow;
|
||||
color = `hsl(${(radians * (1 / frequency)) + TAU * indices.rainbow / params.rainbow.length}rad 100 50)`;
|
||||
}
|
||||
if (params.shake) {
|
||||
const {magnitude = 1} = params.shake;
|
||||
left += (Math.random() * magnitude * 2) - magnitude;
|
||||
top += (Math.random() * magnitude * 2) - magnitude;
|
||||
}
|
||||
if (params.em) {
|
||||
fontStyle = 'italic';
|
||||
}
|
||||
if (params.strong) {
|
||||
fontWeight = 'bold';
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={styles.letter}
|
||||
key={i}
|
||||
style={{
|
||||
color,
|
||||
fontStyle,
|
||||
fontWeight,
|
||||
left: `${left}px`,
|
||||
opacity: i <= caret ? opacity : 0,
|
||||
top: `${top}px`,
|
||||
transition: `opacity ${fade}s`,
|
||||
}}
|
||||
>
|
||||
{character}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
}
|
||||
{localRender(caret, radians)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
|
@ -11,6 +11,7 @@
|
|||
border: solid 1px white;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
overflow-wrap: break-word;
|
||||
padding: 1em;
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
|
@ -26,5 +27,4 @@
|
|||
|
||||
.letter {
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import {useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import DomContext from '@/context/dom-scale.js';
|
||||
import DomContext from '@/react/context/dom-scale.js';
|
||||
import {RESOLUTION} from '@/util/constants.js';
|
||||
|
||||
import styles from './dom.module.css';
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
import {useState} from 'react';
|
||||
|
||||
import {usePacket} from '@/context/client.js';
|
||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||
import {parseLetters} from '@/dialogue.js';
|
||||
import {usePacket} from '@/react/context/client.js';
|
||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||
import {parseLetters} from '@/util/dialogue.js';
|
||||
|
||||
import Entity from './entity.jsx';
|
||||
|
||||
export default function Entities({camera, scale, setMonopolizers}) {
|
||||
export default function Entities({
|
||||
camera,
|
||||
scale,
|
||||
setChatMessages,
|
||||
setMonopolizers,
|
||||
}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
usePacket('EcsChange', async () => {
|
||||
|
@ -34,6 +39,10 @@ export default function Entities({camera, scale, setMonopolizers}) {
|
|||
for (const key in dialogue) {
|
||||
dialogues[key] = dialogue[key];
|
||||
dialogues[key].letters = parseLetters(dialogues[key].body);
|
||||
setChatMessages((chatMessages) => ({
|
||||
[[id, key].join('-')]: dialogues[key].letters,
|
||||
...chatMessages,
|
||||
}));
|
||||
const skipListeners = new Set();
|
||||
dialogues[key].addSkipListener = (listener) => {
|
||||
skipListeners.add(listener);
|
|
@ -1,8 +1,10 @@
|
|||
import {Container} from '@pixi/react';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||
// import {useRadians} from '@/react/context/radians.js';
|
||||
// import {TAU} from '@/util/math.js';
|
||||
|
||||
import Entities from './entities.jsx';
|
||||
import TargetingGhost from './targeting-ghost.jsx';
|
||||
|
@ -36,6 +38,26 @@ export default function Ecs({applyFilters, camera, monopolizers, scale}) {
|
|||
const [projected, setProjected] = useState([]);
|
||||
const [position, setPosition] = useState({x: 0, y: 0});
|
||||
const [water, setWater] = useState();
|
||||
// const radians = useRadians();
|
||||
// const [sine, setSine] = useState();
|
||||
// useEffect(() => {
|
||||
// async function buildSineFilter() {
|
||||
// const {default: SineFilter} = await import('./filters/horizontal-sine.js');
|
||||
// const sine = new SineFilter();
|
||||
// sine.frequency = 1;
|
||||
// sine.magnitude = 3;
|
||||
// setSine(sine);
|
||||
// }
|
||||
// buildSineFilter();
|
||||
// }, []);
|
||||
// useEffect(() => {
|
||||
// if (!sine) {
|
||||
// return;
|
||||
// }
|
||||
// const r = (radians / 8) % TAU;
|
||||
// sine.offset = 6 * (camera.y + r);
|
||||
// sine.magnitude = 2 * (r > Math.PI ? TAU - r : r);
|
||||
// }, [camera, radians, scale, sine]);
|
||||
useEffect(() => {
|
||||
async function buildNightFilter() {
|
||||
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix');
|
||||
|
@ -89,15 +111,16 @@ export default function Ecs({applyFilters, camera, monopolizers, scale}) {
|
|||
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.direction));
|
||||
}
|
||||
}, [ecs, mainEntity, scale]);
|
||||
// useEffect(() => {
|
||||
// setFilters(
|
||||
// applyFilters
|
||||
// ? [
|
||||
// ...(night ? [night] : [])
|
||||
// ]
|
||||
// : [],
|
||||
// );
|
||||
// }, [applyFilters, night])
|
||||
useEffect(() => {
|
||||
setFilters(
|
||||
applyFilters
|
||||
? [
|
||||
...(false && night ? [night] : []),
|
||||
// ...(sine ? [sine] : []),
|
||||
]
|
||||
: [],
|
||||
);
|
||||
}, [applyFilters, night])
|
||||
return (
|
||||
<Container
|
||||
scale={scale}
|
|
@ -1,11 +1,12 @@
|
|||
import {AdjustmentFilter} from '@pixi/filter-adjustment';
|
||||
import {GlowFilter} from '@pixi/filter-glow';
|
||||
import {Container} from '@pixi/react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useState} from 'react';
|
||||
|
||||
import {usePacket} from '@/context/client.js';
|
||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
import {usePacket} from '@/react/context/client.js';
|
||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||
import {useRadians} from '@/react/context/radians.js';
|
||||
|
||||
import Entity from './entity.jsx';
|
||||
|
||||
|
@ -13,10 +14,13 @@ export default function Entities({filters, monopolizers}) {
|
|||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [radians, setRadians] = useState(0);
|
||||
const radians = useRadians();
|
||||
const [willInteractWith, setWillInteractWith] = useState(0);
|
||||
const [interactionFilters] = useState([new AdjustmentFilter(), new GlowFilter({color: 0x0})]);
|
||||
const pulse = (Math.cos(radians) + 1) * 0.5;
|
||||
const [interactionFilters] = useState([
|
||||
new AdjustmentFilter(),
|
||||
new GlowFilter({color: 0x0}),
|
||||
]);
|
||||
const pulse = (Math.cos(radians / 4) + 1) * 0.5;
|
||||
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
||||
interactionFilters[1].outerStrength = pulse * 0.5;
|
||||
usePacket('EcsChange', async () => {
|
||||
|
@ -64,15 +68,6 @@ export default function Entities({filters, monopolizers}) {
|
|||
setWillInteractWith(main.Interacts.willInteractWith);
|
||||
}
|
||||
}, [ecs, mainEntity]);
|
||||
useEffect(() => {
|
||||
setRadians(0);
|
||||
const handle = setInterval(() => {
|
||||
setRadians((radians) => (radians + 0.1) % (Math.PI * 2))
|
||||
}, 50);
|
||||
return () => {
|
||||
clearInterval(handle);
|
||||
};
|
||||
}, []);
|
||||
const renderables = [];
|
||||
for (const id in entities) {
|
||||
const isHighlightedInteraction = 0 === monopolizers.length && id == willInteractWith;
|
|
@ -1,10 +1,11 @@
|
|||
import {Container, Graphics} from '@pixi/react';
|
||||
import {memo, useCallback} from 'react';
|
||||
|
||||
import {useDebug} from '@/context/debug.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
import {useDebug} from '@/react/context/debug.js';
|
||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||
|
||||
import Emitter from './emitter.jsx';
|
||||
import Light from './light.jsx';
|
||||
import Sprite from './sprite.jsx';
|
||||
|
||||
function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
|
||||
|
@ -66,6 +67,12 @@ function Entity({entity, ...rest}) {
|
|||
entity={entity}
|
||||
/>
|
||||
)}
|
||||
{/* {entity.Light && (
|
||||
<Light
|
||||
x={entity.Position.x}
|
||||
y={entity.Position.y}
|
||||
/>
|
||||
)} */}
|
||||
{debug && entity.Position && (
|
||||
<Crosshair x={entity.Position.x} y={entity.Position.y} />
|
||||
)}
|
32
app/react/components/pixi/extensions.js
Normal file
32
app/react/components/pixi/extensions.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {ExtensionType} from '@pixi/core';
|
||||
import {Stage as LayerStage} from '@pixi/layers';
|
||||
|
||||
import {
|
||||
AmbientLight,
|
||||
deferredLighting,
|
||||
} from './lights.js';
|
||||
|
||||
export const ApplicationStageLayers = {
|
||||
type: ExtensionType.Application,
|
||||
priority: 100,
|
||||
ref: {
|
||||
destroy: function() {},
|
||||
init: function() {
|
||||
this.stage = new LayerStage();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ApplicationStageLights = {
|
||||
type: ExtensionType.Application,
|
||||
ref: {
|
||||
destroy: function() {},
|
||||
init: function() {
|
||||
const {stage} = this;
|
||||
deferredLighting.addToStage(stage);
|
||||
// const ambientLight = new AmbientLight(0x2244cc, 0.25);
|
||||
const ambientLight = new AmbientLight(0xffffff, 1);
|
||||
stage.addChild(ambientLight);
|
||||
},
|
||||
},
|
||||
};
|
41
app/react/components/pixi/filters/horizontal-sine.frag
Normal file
41
app/react/components/pixi/filters/horizontal-sine.frag
Normal file
|
@ -0,0 +1,41 @@
|
|||
#define PI_D3 1.047197551
|
||||
|
||||
varying vec2 vTextureCoord;
|
||||
uniform vec4 filterArea;
|
||||
uniform sampler2D uSampler;
|
||||
|
||||
uniform float frequency;
|
||||
uniform bool isPixelated;
|
||||
uniform float magnitude;
|
||||
uniform float offset;
|
||||
uniform float scale;
|
||||
|
||||
vec2 denormalizeCoordinates(vec2 coord) {
|
||||
return coord * filterArea.xy + filterArea.zw;
|
||||
}
|
||||
|
||||
vec2 normalizeCoordinates( vec2 coord ) {
|
||||
return (coord - filterArea.zw) / filterArea.xy;
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
// Denormalize coordinates.
|
||||
vec2 coord = denormalizeCoordinates(vTextureCoord);
|
||||
// Calculate rotation radians.
|
||||
float rads = mod(
|
||||
frequency * ((((isPixelated ? floor(offset) : offset) * scale) + coord.y) / scale),
|
||||
6.
|
||||
);
|
||||
if (isPixelated) {
|
||||
rads = floor(rads / frequency) * frequency;
|
||||
}
|
||||
// Apply horizontal shift.
|
||||
float xOff = scale * magnitude * cos(PI_D3 * rads);
|
||||
if (isPixelated) {
|
||||
xOff = floor(xOff / scale) * scale;
|
||||
}
|
||||
coord.x += xOff;
|
||||
// Normalize and apply coordinates.
|
||||
coord = normalizeCoordinates(coord);
|
||||
gl_FragColor = texture2D(uSampler, coord);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user