Compare commits

...

24 Commits

Author SHA1 Message Date
cha0s
6957365723 chore: tidy 2024-07-20 07:21:59 -05:00
cha0s
f68f8d359e refactor: hooks 2024-07-20 07:07:22 -05:00
cha0s
bfdd55e44a perf: unroll children 2024-07-20 05:38:44 -05:00
cha0s
49b3fc3c46 refactor: data location 2024-07-20 05:10:20 -05:00
cha0s
9833e2ba16 refactor: ecs 2024-07-20 05:07:39 -05:00
cha0s
bbdfe3b813 refactor: filters 2024-07-20 05:07:33 -05:00
cha0s
d89f744003 refactor: session 2024-07-20 04:42:10 -05:00
cha0s
05e1bb5f92 refactor: constants 2024-07-20 04:41:00 -05:00
cha0s
bd6dade614 refactor: react 2024-07-20 04:32:33 -05:00
cha0s
a670e201d6 refactor: creators 2024-07-20 04:19:39 -05:00
cha0s
349a93ab4b refactor: tidy 2024-07-20 03:46:38 -05:00
cha0s
767f014107 refactor: tidy 2024-07-20 02:59:40 -05:00
cha0s
13b457210e fix: test 2024-07-20 02:32:50 -05:00
cha0s
da13852216 fun: no lights... promise 2024-07-19 01:27:47 -05:00
cha0s
2c2bfcbf0c flow: lights and normals 2024-07-18 04:18:06 -05:00
cha0s
82fd31802b refactor: extensions 2024-07-17 20:43:29 -05:00
cha0s
0d8cdff6d7 fun: light 2024-07-17 05:07:50 -05:00
cha0s
578e796090 refactor: @pixi/layers 2024-07-16 03:34:55 -05:00
cha0s
16871b0919 fun: sine 2024-07-14 21:44:46 -05:00
cha0s
7f8bb9755f refactor: message keys 2024-07-14 21:44:33 -05:00
cha0s
4529d2e8d3 refactor: radians 2024-07-14 21:44:15 -05:00
cha0s
94685e4654 fix: handle broken parse 2024-07-14 17:57:47 -05:00
cha0s
908a6fd986 fix: chat state and ux 2024-07-14 17:31:11 -05:00
cha0s
5a4666ae49 feat: chat and dialogue++ 2024-07-14 07:24:15 -05:00
138 changed files with 1025 additions and 494 deletions

2
.gitignore vendored
View File

@ -1,8 +1,8 @@
node_modules node_modules
/app/data
/.cache /.cache
/build /build
/coverage /coverage
/data
/dev /dev
.env .env

View File

@ -37,15 +37,19 @@ export default function traverse(node, visitor) {
throw new Error(`node type ${node.type} not traversable. (${Object.keys(node).join(', ')})`); throw new Error(`node type ${node.type} not traversable. (${Object.keys(node).join(', ')})`);
} }
visitor(node, 'enter'); visitor(node, 'enter');
const path = TRAVERSAL_PATH[node.type]; for (const key of TRAVERSAL_PATH[node.type]) {
const children = []; if (Array.isArray(node[key])) {
for (const key of path) { for (const child of node[key]) {
children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
}
for (const child of children) {
if (child) { if (child) {
traverse(child, visitor); traverse(child, visitor);
} }
} }
}
else {
if (node[key]) {
traverse(node[key], visitor);
}
}
}
visitor(node, 'exit'); visitor(node, 'exit');
} }

View File

@ -1,5 +1,5 @@
import Components from '@/ecs-components/index.js'; import Components from '@/ecs/components/index.js';
import Systems from '@/ecs-systems/index.js'; import Systems from '@/ecs/systems/index.js';
export default function createEcs(Ecs) { export default function createEcs(Ecs) {
const ecs = new Ecs({Components, Systems}); const ecs = new Ecs({Components, Systems});

View File

@ -1,4 +1,4 @@
import createEcs from './create-ecs.js'; import createEcs from './ecs.js';
export default async function createHouse(Ecs, id) { export default async function createHouse(Ecs, id) {
const ecs = createEcs(Ecs); const ecs = createEcs(Ecs);

View File

@ -41,6 +41,7 @@ export default async function createPlayer(id) {
}, },
}, },
Health: {health: 100}, Health: {health: 100},
Light: {},
Magnet: {strength: 24}, Magnet: {strength: 24},
Player: {}, Player: {},
Position: {x: 128, y: 128}, Position: {x: 128, y: 128},

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
import Component from '@/ecs/component.js';
export default class Light extends Component {
static properties = {
radius: {type: 'uint8'},
};
}

View File

@ -1,5 +1,5 @@
import {CHUNK_SIZE} from '@/constants.js';
import Component from '@/ecs/component.js'; import Component from '@/ecs/component.js';
import {CHUNK_SIZE} from '@/util/constants.js';
import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js'; import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js';
import vector2d from './helpers/vector-2d'; import vector2d from './helpers/vector-2d';
@ -99,7 +99,9 @@ class LayerProxy {
return this.instance.layers[this.index]; return this.instance.layers[this.index];
} }
async load() { 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() { get source() {
return this.layer.source; return this.layer.source;

View File

@ -1,5 +1,5 @@
import {IRL_MINUTES_PER_GAME_DAY} from '@/constants';
import Component from '@/ecs/component.js'; import Component from '@/ecs/component.js';
import {IRL_MINUTES_PER_GAME_DAY} from '@/util/constants';
const realSecondsPerGameDay = 60 * IRL_MINUTES_PER_GAME_DAY; const realSecondsPerGameDay = 60 * IRL_MINUTES_PER_GAME_DAY;
const realSecondsPerGameHour = realSecondsPerGameDay / 24; const realSecondsPerGameHour = realSecondsPerGameDay / 24;

View File

@ -1,5 +1,5 @@
// import {RESOLUTION} from '@/constants.js'
import {System} from '@/ecs/index.js'; import {System} from '@/ecs/index.js';
// import {RESOLUTION} from '@/util/constants.js'
// const [hx, hy] = [RESOLUTION.x / 2, RESOLUTION.y / 2]; // const [hx, hy] = [RESOLUTION.x / 2, RESOLUTION.y / 2];

View File

@ -1,17 +1,17 @@
import Ecs from '@/ecs/ecs.js';
import {decode, encode} from '@/packets/index.js';
import { import {
CHUNK_SIZE, CHUNK_SIZE,
RESOLUTION, RESOLUTION,
TPS, TPS,
} from '@/constants.js'; } from '@/util/constants.js';
import Ecs from '@/ecs/ecs.js';
import {decode, encode} from '@/packets/index.js';
import Script from '@/util/script.js'; import Script from '@/util/script.js';
import createEcs from './create-ecs.js'; import createEcs from './create/ecs.js';
import createForest from './create-forest.js'; import createForest from './create/forest.js';
import createHomestead from './create-homestead.js'; import createHomestead from './create/homestead.js';
import createHouse from './create-house.js'; import createHouse from './create/house.js';
import createPlayer from './create-player.js'; import createPlayer from './create/player.js';
import {LRUCache} from 'lru-cache'; import {LRUCache} from 'lru-cache';
@ -252,6 +252,10 @@ export default class Engine {
); );
} }
createEcs() {
return createEcs(this.Ecs);
}
async disconnectPlayer(connection) { async disconnectPlayer(connection) {
const connectedPlayer = this.connectedPlayers.get(connection); const connectedPlayer = this.connectedPlayers.get(connection);
if (!connectedPlayer) { if (!connectedPlayer) {
@ -270,7 +274,7 @@ export default class Engine {
async loadEcs(path) { async loadEcs(path) {
this.ecses[path] = await this.Ecs.deserialize( this.ecses[path] = await this.Ecs.deserialize(
createEcs(this.Ecs), this.createEcs(),
await this.server.readData(path), await this.server.readData(path),
); );
} }
@ -284,7 +288,7 @@ export default class Engine {
if ('ENOENT' !== error.code) { if ('ENOENT' !== error.code) {
throw error; throw error;
} }
const homestead = createEcs(this.Ecs); const homestead = this.createEcs();
for (const entity of await createHomestead(id)) { for (const entity of await createHomestead(id)) {
await homestead.create(entity); await homestead.create(entity);
} }
@ -297,7 +301,7 @@ export default class Engine {
['houses', `${id}`].join('/'), ['houses', `${id}`].join('/'),
house, house,
); );
const forest = createEcs(this.Ecs); const forest = this.createEcs();
for (const entity of await createForest()) { for (const entity of await createForest()) {
await forest.create(entity); await forest.create(entity);
} }

View File

@ -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()});
});

View File

@ -1,4 +1,4 @@
import {CLIENT_LATENCY} from '@/constants.js'; import {CLIENT_LATENCY} from '@/util/constants.js';
export default class Client { export default class Client {
constructor() { constructor() {

View File

@ -1,5 +1,5 @@
import {CLIENT_PREDICTION} from '@/constants.js';
import {encode} from '@/packets/index.js'; import {encode} from '@/packets/index.js';
import {CLIENT_PREDICTION} from '@/util/constants.js';
import Client from './client.js'; import Client from './client.js';

View File

@ -1,4 +1,4 @@
import {SERVER_LATENCY} from '@/constants.js'; import {SERVER_LATENCY} from '@/util/constants.js';
export default class Server { export default class Server {
constructor() { constructor() {

View File

@ -2,10 +2,10 @@ import {del, get, set} from 'idb-keyval';
import {encode} from '@/packets/index.js'; import {encode} from '@/packets/index.js';
import createEcs from '../../create-ecs.js'; import createEcs from '../../create/ecs.js';
import '../../create-forest.js'; import '../../create/forest.js';
import '../../create-homestead.js'; import '../../create/homestead.js';
import '../../create-player.js'; import '../../create/player.js';
import Engine from '../../engine.js'; import Engine from '../../engine.js';
import Server from './server.js'; import Server from './server.js';

View File

@ -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>
);
}

View File

@ -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}
/>
);
}

View File

@ -2,8 +2,8 @@ import {useState} from 'react';
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'; import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
import 'react-tabs/style/react-tabs.css'; import 'react-tabs/style/react-tabs.css';
import {useEcs, useEcsTick} from '@/context/ecs.js'; import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import {useMainEntity} from '@/context/main-entity.js'; import {useMainEntity} from '@/react/context/main-entity.js';
import styles from './devtools.module.css'; import styles from './devtools.module.css';

View File

@ -1,8 +1,8 @@
import {useEffect, useRef, useState} from 'react'; import {useEffect, useRef, useState} from 'react';
import {useClient} from '@/context/client.js'; import {useClient} from '@/react/context/client.js';
import {useEcs} from '@/context/ecs.js'; import {useEcs} from '@/react/context/ecs.js';
import useRect from '@/util/react-hooks/use-rect.js'; import useRect from '@/react/hooks/use-rect.js';
import styles from './tiles.module.css'; import styles from './tiles.module.css';

View 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>
);
}

View 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");
}

View 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>
);
}

View File

@ -1,17 +1,15 @@
.chat {
background-color: #00000044;
position: absolute;
bottom: 0;
width: 100%;
}
.chat form { .input {
line-height: 0; line-height: 0;
input[type="text"] { input[type="text"] {
background-color: #00000044; background-color: #00000044;
border: 1px solid #333333; border: 1px solid #333333;
color: #ffffff; color: #ffffff;
font-family: "Cookbook";
font-size: 16px;
margin: 4px; margin: 4px;
padding: 0;
padding-left: 4px;
width: calc(100% - 8px); width: calc(100% - 8px);
&:focus-visible { &:focus-visible {
border: 1px solid #999999; border: 1px solid #999999;

View 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);

View File

@ -0,0 +1,4 @@
.message {
color: #ffffff;
margin-left: 8px;
}

View 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>
)
}

View File

@ -0,0 +1,5 @@
.messages {
display: flex;
flex-direction: column-reverse;
overflow-y: auto;
}

View File

@ -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 '@/react/context/dom-scale.js';
import {useDomScale} from '@/context/dom-scale.js'; import {useRadians} from '@/react/context/radians.js';
import {TAU} from '@/util/math.js'; import {RESOLUTION} from '@/util/constants.js';
import {render} from '@/util/dialogue.js';
import styles from './dialogue.module.css'; import styles from './dialogue.module.css';
@ -17,7 +18,7 @@ export default function Dialogue({
const ref = useRef(); const ref = useRef();
const [dimensions, setDimensions] = useState({h: 0, w: 0}); const [dimensions, setDimensions] = useState({h: 0, w: 0});
const [caret, setCaret] = useState(0); const [caret, setCaret] = useState(0);
const [radians, setRadians] = useState(0); const radians = useRadians();
useEffect(() => { useEffect(() => {
return dialogue.addSkipListener(() => { return dialogue.addSkipListener(() => {
if (caret >= dialogue.letters.length - 1) { if (caret >= dialogue.letters.length - 1) {
@ -28,24 +29,6 @@ export default function Dialogue({
} }
}); });
}, [caret, 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(() => { useEffect(() => {
const {params} = dialogue.letters[caret]; const {params} = dialogue.letters[caret];
let handle; let handle;
@ -88,6 +71,10 @@ export default function Dialogue({
cancelAnimationFrame(handle); cancelAnimationFrame(handle);
}; };
}, [dialogue, domScale, ref]); }, [dialogue, domScale, ref]);
const localRender = useMemo(
() => render(dialogue.letters, styles.letter),
[dialogue.letters],
);
const origin = 'function' === typeof dialogue.origin const origin = 'function' === typeof dialogue.origin
? dialogue.origin() ? dialogue.origin()
: dialogue.origin || {x: 0, y: 0}; : dialogue.origin || {x: 0, y: 0};
@ -175,62 +162,7 @@ export default function Dialogue({
<polygon points="0 0, 24 0, 12 24" /> <polygon points="0 0, 24 0, 12 24" />
</svg> </svg>
<p className={styles.letters}> <p className={styles.letters}>
{ {localRender(caret, radians)}
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>
);
})
}
</p> </p>
</div> </div>
); );

View File

@ -11,6 +11,7 @@
border: solid 1px white; border: solid 1px white;
border-radius: 8px; border-radius: 8px;
color: white; color: white;
overflow-wrap: break-word;
padding: 1em; padding: 1em;
position: fixed; position: fixed;
margin: 0; margin: 0;
@ -26,5 +27,4 @@
.letter { .letter {
opacity: 0; opacity: 0;
position: relative;
} }

View File

@ -1,7 +1,7 @@
import {useEffect, useRef, useState} from 'react'; import {useEffect, useRef, useState} from 'react';
import {RESOLUTION} from '@/constants.js'; import DomContext from '@/react/context/dom-scale.js';
import DomContext from '@/context/dom-scale.js'; import {RESOLUTION} from '@/util/constants.js';
import styles from './dom.module.css'; import styles from './dom.module.css';

View File

@ -1,12 +1,17 @@
import {useState} from 'react'; import {useState} from 'react';
import {usePacket} from '@/context/client.js'; import {usePacket} from '@/react/context/client.js';
import {useEcs, useEcsTick} from '@/context/ecs.js'; import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import {parseLetters} from '@/dialogue.js'; import {parseLetters} from '@/util/dialogue.js';
import Entity from './entity.jsx'; import Entity from './entity.jsx';
export default function Entities({camera, scale, setMonopolizers}) { export default function Entities({
camera,
scale,
setChatMessages,
setMonopolizers,
}) {
const [ecs] = useEcs(); const [ecs] = useEcs();
const [entities, setEntities] = useState({}); const [entities, setEntities] = useState({});
usePacket('EcsChange', async () => { usePacket('EcsChange', async () => {
@ -34,6 +39,10 @@ export default function Entities({camera, scale, setMonopolizers}) {
for (const key in dialogue) { for (const key in dialogue) {
dialogues[key] = dialogue[key]; dialogues[key] = dialogue[key];
dialogues[key].letters = parseLetters(dialogues[key].body); dialogues[key].letters = parseLetters(dialogues[key].body);
setChatMessages((chatMessages) => ({
[[id, key].join('-')]: dialogues[key].letters,
...chatMessages,
}));
const skipListeners = new Set(); const skipListeners = new Set();
dialogues[key].addSkipListener = (listener) => { dialogues[key].addSkipListener = (listener) => {
skipListeners.add(listener); skipListeners.add(listener);

View File

@ -1,8 +1,10 @@
import {Container} from '@pixi/react'; import {Container} from '@pixi/react';
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {useEcs, useEcsTick} from '@/context/ecs.js'; import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import {useMainEntity} from '@/context/main-entity.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 Entities from './entities.jsx';
import TargetingGhost from './targeting-ghost.jsx'; import TargetingGhost from './targeting-ghost.jsx';
@ -36,6 +38,26 @@ export default function Ecs({applyFilters, camera, monopolizers, scale}) {
const [projected, setProjected] = useState([]); const [projected, setProjected] = useState([]);
const [position, setPosition] = useState({x: 0, y: 0}); const [position, setPosition] = useState({x: 0, y: 0});
const [water, setWater] = useState(); 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(() => { useEffect(() => {
async function buildNightFilter() { async function buildNightFilter() {
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix'); 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)); setProjected(Wielder.activeItem()?.project(Position.tile, Direction.direction));
} }
}, [ecs, mainEntity, scale]); }, [ecs, mainEntity, scale]);
// useEffect(() => { useEffect(() => {
// setFilters( setFilters(
// applyFilters applyFilters
// ? [ ? [
// ...(night ? [night] : []) ...(false && night ? [night] : []),
// ] // ...(sine ? [sine] : []),
// : [], ]
// ); : [],
// }, [applyFilters, night]) );
}, [applyFilters, night])
return ( return (
<Container <Container
scale={scale} scale={scale}

View File

@ -1,11 +1,12 @@
import {AdjustmentFilter} from '@pixi/filter-adjustment'; import {AdjustmentFilter} from '@pixi/filter-adjustment';
import {GlowFilter} from '@pixi/filter-glow'; import {GlowFilter} from '@pixi/filter-glow';
import {Container} from '@pixi/react'; import {Container} from '@pixi/react';
import {useEffect, useState} from 'react'; import {useState} from 'react';
import {usePacket} from '@/context/client.js'; import {usePacket} from '@/react/context/client.js';
import {useEcs, useEcsTick} from '@/context/ecs.js'; import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import {useMainEntity} from '@/context/main-entity.js'; import {useMainEntity} from '@/react/context/main-entity.js';
import {useRadians} from '@/react/context/radians.js';
import Entity from './entity.jsx'; import Entity from './entity.jsx';
@ -13,10 +14,13 @@ export default function Entities({filters, monopolizers}) {
const [ecs] = useEcs(); const [ecs] = useEcs();
const [entities, setEntities] = useState({}); const [entities, setEntities] = useState({});
const [mainEntity] = useMainEntity(); const [mainEntity] = useMainEntity();
const [radians, setRadians] = useState(0); const radians = useRadians();
const [willInteractWith, setWillInteractWith] = useState(0); const [willInteractWith, setWillInteractWith] = useState(0);
const [interactionFilters] = useState([new AdjustmentFilter(), new GlowFilter({color: 0x0})]); const [interactionFilters] = useState([
const pulse = (Math.cos(radians) + 1) * 0.5; new AdjustmentFilter(),
new GlowFilter({color: 0x0}),
]);
const pulse = (Math.cos(radians / 4) + 1) * 0.5;
interactionFilters[0].brightness = (pulse * 0.75) + 1; interactionFilters[0].brightness = (pulse * 0.75) + 1;
interactionFilters[1].outerStrength = pulse * 0.5; interactionFilters[1].outerStrength = pulse * 0.5;
usePacket('EcsChange', async () => { usePacket('EcsChange', async () => {
@ -64,15 +68,6 @@ export default function Entities({filters, monopolizers}) {
setWillInteractWith(main.Interacts.willInteractWith); setWillInteractWith(main.Interacts.willInteractWith);
} }
}, [ecs, mainEntity]); }, [ecs, mainEntity]);
useEffect(() => {
setRadians(0);
const handle = setInterval(() => {
setRadians((radians) => (radians + 0.1) % (Math.PI * 2))
}, 50);
return () => {
clearInterval(handle);
};
}, []);
const renderables = []; const renderables = [];
for (const id in entities) { for (const id in entities) {
const isHighlightedInteraction = 0 === monopolizers.length && id == willInteractWith; const isHighlightedInteraction = 0 === monopolizers.length && id == willInteractWith;

View File

@ -1,10 +1,11 @@
import {Container, Graphics} from '@pixi/react'; import {Container, Graphics} from '@pixi/react';
import {memo, useCallback} from 'react'; import {memo, useCallback} from 'react';
import {useDebug} from '@/context/debug.js'; import {useDebug} from '@/react/context/debug.js';
import {useMainEntity} from '@/context/main-entity.js'; import {useMainEntity} from '@/react/context/main-entity.js';
import Emitter from './emitter.jsx'; import Emitter from './emitter.jsx';
import Light from './light.jsx';
import Sprite from './sprite.jsx'; import Sprite from './sprite.jsx';
function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) { function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
@ -66,6 +67,12 @@ function Entity({entity, ...rest}) {
entity={entity} entity={entity}
/> />
)} )}
{/* {entity.Light && (
<Light
x={entity.Position.x}
y={entity.Position.y}
/>
)} */}
{debug && entity.Position && ( {debug && entity.Position && (
<Crosshair x={entity.Position.x} y={entity.Position.y} /> <Crosshair x={entity.Position.x} y={entity.Position.y} />
)} )}

View 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);
},
},
};

View 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