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
/app/data
/.cache
/build
/coverage
/data
/dev
.env

View File

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

View File

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

View File

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

View File

@ -41,6 +41,7 @@ export default async function createPlayer(id) {
},
},
Health: {health: 100},
Light: {},
Magnet: {strength: 24},
Player: {},
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 {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;

View File

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

View File

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

View File

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

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 {
constructor() {

View File

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

View File

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

View File

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

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 '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';

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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