fun: dialog++

This commit is contained in:
cha0s 2024-07-13 03:02:55 -05:00
parent 84b1c49527
commit 4a0b42fbe5
8 changed files with 3213 additions and 267 deletions

View File

@ -77,7 +77,7 @@ export default async function createHomestead(id) {
interacting: 1,
interactScript: `
subject.Interlocutor.dialogue({
body: 'Hey what is up :)',
body: '<strong>Hey</strong><rate frequency={1}> </rate><rate frequency={0}><shake>what</shake></rate> <em>is</em> <wave frequency={0.5}>uu<rainbow frequency={2}>uu<blink>uu</blink><fade frequency={5}>uu<shake magnitude={4}>uu</shake>uuu</fade></rainbow>uup</wave>? ^_^',
origin: subject.Position.toJSON(),
position: {x: subject.Position.x, y: subject.Position.y - 32},
})

56
app/dialogue.js Normal file
View File

@ -0,0 +1,56 @@
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

@ -2,6 +2,7 @@ import {useEffect, useRef, useState} from 'react';
import {RESOLUTION} from '@/constants.js';
import {useDomScale} from '@/context/dom-scale.js';
import {TAU} from '@/util/math.js';
import styles from './dialogue.module.css';
@ -10,21 +11,60 @@ const CARET_SIZE = 12;
export default function Dialogue({
camera,
dialogue,
onClose,
scale,
}) {
const domScale = useDomScale();
const ref = useRef();
const [dimensions, setDimensions] = useState({h: 0, w: 0});
const [caret, setCaret] = useState(0);
const [radians, setRadians] = useState(0);
useEffect(() => {
const {ttl = 5} = dialogue;
if (!ttl) {
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;
if (caret >= dialogue.letters.length - 1) {
const {linger = 5} = dialogue;
if (!linger) {
dialogue.onClose();
return;
}
setTimeout(() => {
onClose();
}, ttl * 1000);
}, [dialogue, onClose]);
handle = setTimeout(() => {
dialogue.onClose();
}, linger * 1000);
}
else {
let jump = caret;
while (0 === dialogue.letters[jump].params.rate.frequency && jump < dialogue.letters.length - 1) {
jump += 1;
}
setCaret(jump);
if (jump < dialogue.letters.length - 1) {
handle = setTimeout(() => {
setCaret(caret + 1);
}, params.rate.frequency * 1000)
}
}
return () => {
clearTimeout(handle);
}
}, [caret, dialogue]);
useEffect(() => {
let handle;
function track() {
@ -134,7 +174,62 @@ export default function Dialogue({
>
<polygon points="0 0, 24 0, 12 24" />
</svg>
{dialogue.body}
{
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>
);
})
}
</div>
);
}

View File

@ -5,3 +5,8 @@
left: 50%;
top: 50%;
}
.letter {
opacity: 0;
position: relative;
}

View File

@ -10,9 +10,6 @@ export default function Dialogues({camera, dialogues, scale}) {
camera={camera}
dialogue={dialogues[key]}
key={key}
onClose={() => {
delete dialogues[key];
}}
scale={scale}
/>
);

View File

@ -1,5 +1,7 @@
import {useState} from 'react';
import {useEcs, useEcsTick} from '@/context/ecs.js';
import {parseLetters} from '@/dialogue.js';
import Entity from './entity.jsx';
@ -23,8 +25,18 @@ export default function Entities({camera, scale}) {
updatedEntities[id] = ecs.get(id);
const {dialogue} = update.Interlocutor || {};
if (dialogue) {
const {dialogues} = updatedEntities[id].Interlocutor;
for (const key in dialogue) {
updatedEntities[id].Interlocutor.dialogues[key] = dialogue[key];
updatedEntities[id] = ecs.rebuild(id);
dialogues[key] = dialogue[key];
dialogues[key].letters = parseLetters(dialogues[key].body);
dialogues[key].onClose = () => {
delete dialogues[key];
setEntities((entities) => ({
...entities,
[id]: ecs.rebuild(id),
}));
};
}
}
}

3283
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,11 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-tabs": "^6.0.2",
"remark-mdx": "^3.0.1",
"remark-parse": "^11.0.0",
"simplex-noise": "^4.0.1",
"unified": "^11.0.5",
"unist-util-visit-parents": "^6.0.1",
"ws": "^8.17.0"
},
"devDependencies": {