feat: chat and dialogue++
This commit is contained in:
parent
dd456743f8
commit
5a4666ae49
|
@ -1,8 +1,11 @@
|
|||
import {createElement} from 'react';
|
||||
import mdx from 'remark-mdx';
|
||||
import parse from 'remark-parse';
|
||||
import {unified} from 'unified';
|
||||
import {visitParents as visit} from 'unist-util-visit-parents';
|
||||
|
||||
import {TAU} from '@/util/math.js';
|
||||
|
||||
const parser = unified().use(parse).use(mdx);
|
||||
|
||||
function computeParams(ancestors) {
|
||||
|
@ -54,3 +57,65 @@ export function parseLetters(source) {
|
|||
});
|
||||
return letters;
|
||||
}
|
||||
|
||||
export function render(letters, className) {
|
||||
return (caret, radians) => (
|
||||
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 (
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
className,
|
||||
key: i,
|
||||
style: {
|
||||
color,
|
||||
fontStyle,
|
||||
fontWeight,
|
||||
left: `${left}px`,
|
||||
opacity: i <= caret ? opacity : 0,
|
||||
position: 'relative',
|
||||
top: `${top}px`,
|
||||
transition: `opacity ${fade}s`,
|
||||
},
|
||||
},
|
||||
character,
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
28
app/react-components/dom/chat/chat.jsx
Normal file
28
app/react-components/dom/chat/chat.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import styles from './chat.module.css';
|
||||
|
||||
import Input from './input.jsx';
|
||||
import Messages from './messages.jsx';
|
||||
|
||||
export default function Chat({
|
||||
chatHistory,
|
||||
chatMessages,
|
||||
onClose,
|
||||
message,
|
||||
setChatHistory,
|
||||
setMessage,
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.chat}>
|
||||
<Messages
|
||||
chatMessages={chatMessages}
|
||||
/>
|
||||
<Input
|
||||
chatHistory={chatHistory}
|
||||
onClose={onClose}
|
||||
message={message}
|
||||
setChatHistory={setChatHistory}
|
||||
setMessage={setMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
15
app/react-components/dom/chat/chat.module.css
Normal file
15
app/react-components/dom/chat/chat.module.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
.chat {
|
||||
background-color: #00000044;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
max-height: 25%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Cookbook";
|
||||
src: url("/assets/fonts/Cookbook.woff") format("woff");
|
||||
}
|
84
app/react-components/dom/chat/input.jsx
Normal file
84
app/react-components/dom/chat/input.jsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {useClient} from '@/context/client.js';
|
||||
|
||||
import styles from './input.module.css';
|
||||
|
||||
export default function ChatInput({
|
||||
chatHistory,
|
||||
message,
|
||||
setChatHistory,
|
||||
setMessage,
|
||||
}) {
|
||||
const client = useClient();
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
const [historyCaret, setHistoryCaret] = useState(0);
|
||||
useEffect(() => {
|
||||
setDisabled(false);
|
||||
}, []);
|
||||
return (
|
||||
<form
|
||||
className={styles.input}
|
||||
onSubmit={(event) => {
|
||||
if (message) {
|
||||
client.send({
|
||||
type: 'Action',
|
||||
payload: {type: 'chat', value: message},
|
||||
});
|
||||
setChatHistory([message, ...chatHistory]);
|
||||
setMessage('');
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
maxLength="255"
|
||||
ref={(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;
|
33
app/react-components/dom/chat/message.jsx
Normal file
33
app/react-components/dom/chat/message.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {render} from '@/dialogue.js';
|
||||
import {TAU} from '@/util/math.js';
|
||||
|
||||
import styles from './message.module.css';
|
||||
|
||||
export default function Message({letters}) {
|
||||
const [radians, setRadians] = useState(0);
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className={styles.message}>
|
||||
{render(letters, '')(letters.length - 1, radians)}
|
||||
</div>
|
||||
)
|
||||
}
|
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;
|
||||
}
|
16
app/react-components/dom/chat/messages.jsx
Normal file
16
app/react-components/dom/chat/messages.jsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Message from './message.jsx';
|
||||
|
||||
import styles from './messages.module.css';
|
||||
|
||||
export default function Messages({chatMessages}) {
|
||||
return (
|
||||
<div className={styles.messages}>
|
||||
{chatMessages.map((letters, i) => (
|
||||
<Message
|
||||
key={i}
|
||||
letters={letters}
|
||||
/>
|
||||
))}
|
||||
</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,7 +1,8 @@
|
|||
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 {render} from '@/dialogue.js';
|
||||
import {TAU} from '@/util/math.js';
|
||||
|
||||
import styles from './dialogue.module.css';
|
||||
|
@ -88,6 +89,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 +180,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;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,12 @@ import {parseLetters} from '@/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,7 @@ 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) => [dialogues[key].letters, ...chatMessages]);
|
||||
const skipListeners = new Set();
|
||||
dialogues[key].addSkipListener = (listener) => {
|
||||
skipListeners.add(listener);
|
||||
|
|
|
@ -9,7 +9,7 @@ import {useEcs, useEcsTick} from '@/context/ecs.js';
|
|||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
|
||||
import Disconnected from './dom/disconnected.jsx';
|
||||
import Chat from './dom/chat.jsx';
|
||||
import Chat from './dom/chat/chat.jsx';
|
||||
import Dom from './dom/dom.jsx';
|
||||
import Entities from './dom/entities.jsx';
|
||||
import HotBar from './dom/hotbar.jsx';
|
||||
|
@ -69,6 +69,7 @@ export default function Ui({disconnected}) {
|
|||
const [message, setMessage] = useState('');
|
||||
const [chatIsOpen, setChatIsOpen] = useState(false);
|
||||
const [chatHistory, setChatHistory] = useState([]);
|
||||
const [chatMessages, setChatMessages] = useState([]);
|
||||
useEffect(() => {
|
||||
async function setEcsStuff() {
|
||||
const {default: Components} = await import('@/ecs-components/index.js');
|
||||
|
@ -97,6 +98,10 @@ export default function Ui({disconnected}) {
|
|||
}, [disconnected]);
|
||||
useEffect(() => {
|
||||
return addKeyListener(document.body, ({event, type, payload}) => {
|
||||
if ('Escape' === payload && 'keyDown' === type && chatIsOpen) {
|
||||
setChatIsOpen(false);
|
||||
return;
|
||||
}
|
||||
if (chatIsOpen) {
|
||||
return;
|
||||
}
|
||||
|
@ -323,6 +328,10 @@ export default function Ui({disconnected}) {
|
|||
<div
|
||||
className={[styles.game, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}
|
||||
onMouseDown={(event) => {
|
||||
if (chatIsOpen) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
if (devtoolsIsOpen) {
|
||||
|
@ -372,6 +381,10 @@ export default function Ui({disconnected}) {
|
|||
}
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
if (chatIsOpen) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
switch (event.button) {
|
||||
case 0:
|
||||
client.send({
|
||||
|
@ -389,6 +402,10 @@ export default function Ui({disconnected}) {
|
|||
event.preventDefault();
|
||||
}}
|
||||
onWheel={(event) => {
|
||||
if (chatIsOpen) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.deltaY > 0) {
|
||||
client.send({
|
||||
type: 'Action',
|
||||
|
@ -424,11 +441,13 @@ export default function Ui({disconnected}) {
|
|||
<Entities
|
||||
camera={camera}
|
||||
scale={scale}
|
||||
setChatMessages={setChatMessages}
|
||||
setMonopolizers={setMonopolizers}
|
||||
/>
|
||||
{chatIsOpen && (
|
||||
<Chat
|
||||
chatHistory={chatHistory}
|
||||
chatMessages={chatMessages}
|
||||
message={message}
|
||||
setChatHistory={setChatHistory}
|
||||
setMessage={setMessage}
|
||||
|
|
16
app/root.css
16
app/root.css
|
@ -10,3 +10,19 @@ html, body {
|
|||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #777 #333;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #333;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #777;
|
||||
border-radius: 20px;
|
||||
border: 3px solid #333;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user