fun: basic dialogue

This commit is contained in:
cha0s 2024-07-12 02:10:22 -05:00
parent 78060ded37
commit 92a77d8046
10 changed files with 301 additions and 1 deletions

View File

@ -59,5 +59,39 @@ export default async function createHomestead(id) {
Ticking: {}, Ticking: {},
VisibleAabb: {}, VisibleAabb: {},
}); });
entities.push({
Collider: {
bodies: [
{
impassable: 1,
points: [
{x: -11, y: -7},
{x: 10, y: -7},
{x: 10, y: 5},
{x: -11, y: 5},
],
},
],
},
Interactive: {
interacting: 1,
interactScript: `
subject.Interlocutor.dialogue({
body: 'Hey what is up :)',
origin: subject.Position.toJSON(),
position: {x: subject.Position.x, y: subject.Position.y - 32},
})
`,
},
Interlocutor: {},
Position: {x: 200, y: 200},
Sprite: {
anchorX: 0.5,
anchorY: 0.7,
source: '/assets/chest.json',
},
Ticking: {},
VisibleAabb: {},
});
return entities; return entities;
} }

View File

@ -15,7 +15,7 @@ export default async function createPlayer(id) {
}, },
Controlled: {}, Controlled: {},
Direction: {direction: 2}, Direction: {direction: 2},
Ecs: {path: ['forests', `${id}`].join('/')}, Ecs: {path: ['homesteads', `${id}`].join('/')},
Emitter: {}, Emitter: {},
Forces: {}, Forces: {},
Interacts: {}, Interacts: {},

View File

@ -0,0 +1,24 @@
import Component from '@/ecs/component.js';
export default class Interlocutor extends Component {
mergeDiff(original, update) {
const merged = {};
if (update.dialogue) {
merged.dialogue = {
...original.dialogue,
...update.dialogue,
}
}
return merged;
}
instanceFromSchema() {
const Component = this;
return class InterlocutorInstance extends super.instanceFromSchema() {
dialogues = {};
id = 0;
dialogue(specification) {
Component.markChange(this.entity, 'dialogue', {[this.id++]: specification});
}
};
}
}

View File

@ -0,0 +1,140 @@
import {useEffect, useRef, useState} from 'react';
import {RESOLUTION} from '@/constants.js';
import {useDomScale} from '@/context/dom-scale.js';
import styles from './dialogue.module.css';
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});
useEffect(() => {
const {ttl = 5} = dialogue;
if (!ttl) {
return;
}
setTimeout(() => {
onClose();
}, ttl * 1000);
}, [dialogue, onClose]);
useEffect(() => {
let handle;
function track() {
if (ref.current) {
const {height, width} = ref.current.getBoundingClientRect();
setDimensions({h: height / domScale, w: width / domScale});
}
handle = requestAnimationFrame(track);
}
handle = requestAnimationFrame(track);
return () => {
cancelAnimationFrame(handle);
};
}, [dialogue, domScale, ref]);
const origin = 'function' === typeof dialogue.origin
? dialogue.origin()
: dialogue.origin || {x: 0, y: 0};
const bounds = {
x: dimensions.w / (2 * scale),
y: dimensions.h / (2 * scale),
};
const left = Math.max(
Math.min(
dialogue.position.x * scale - camera.x,
RESOLUTION.x - bounds.x * scale - 16,
),
bounds.x * scale + 16,
);
const top = Math.max(
Math.min(
dialogue.position.y * scale - camera.y,
RESOLUTION.y - bounds.y * scale - 16,
),
bounds.y * scale + 88,
);
const offsetPosition = {
x: ((dialogue.position.x * scale - camera.x) - left) / scale,
y: ((dialogue.position.y * scale - camera.y) - top) / scale,
};
const difference = {
x: origin.x - dialogue.position.x + offsetPosition.x,
y: origin.y - dialogue.position.y + offsetPosition.y,
};
const within = {
x: Math.abs(difference.x) < bounds.x,
y: Math.abs(difference.y) < bounds.y,
};
const caretPosition = {
x: Math.max(-bounds.x, Math.min(origin.x - dialogue.position.x + offsetPosition.x, bounds.x)),
y: Math.max(-bounds.y, Math.min(origin.y - dialogue.position.y + offsetPosition.y, bounds.y)),
};
let caretRotation = Math.atan2(
difference.y - caretPosition.y,
difference.x - caretPosition.x,
);
caretRotation += Math.PI * 1.5;
if (within.x) {
caretPosition.y = bounds.y * Math.sign(difference.y);
if (within.y) {
if (Math.sign(difference.y) > 0) {
caretRotation = Math.PI;
}
else {
caretRotation = 0;
}
}
}
else if (within.y) {
caretPosition.x = bounds.x * Math.sign(difference.x);
}
caretPosition.x *= scale;
caretPosition.y *= scale;
caretPosition.x += -Math.sin(caretRotation) * (CARET_SIZE / 2);
caretPosition.y += Math.cos(caretRotation) * (CARET_SIZE / 2);
return (
<div
ref={ref}
style={{
backgroundColor: '#00000044',
border: 'solid 1px white',
borderRadius: '8px',
color: 'white',
padding: '1em',
position: 'absolute',
left: `${left}px`,
margin: '0',
top: `${top}px`,
transform: 'translate(-50%, -50%)',
userSelect: 'none',
whiteSpace: 'nowrap',
}}
>
<svg
className={styles.caret}
viewBox="0 0 24 24"
width={CARET_SIZE}
height={CARET_SIZE}
style={{
transform: `
translate(
calc(-50% + ${caretPosition.x}px),
calc(-50% + ${caretPosition.y}px)
)
rotate(${caretRotation}rad)
`,
}}
>
<polygon points="0 0, 24 0, 12 24" />
</svg>
{dialogue.body}
</div>
);
}

View File

@ -0,0 +1,7 @@
.caret {
position: absolute;
fill: #00000044;
stroke: white;
left: 50%;
top: 50%;
}

View File

@ -0,0 +1,22 @@
import styles from './dialogues.module.css';
import Dialogue from './dialogue.jsx';
export default function Dialogues({camera, dialogues, scale}) {
const elements = [];
for (const key in dialogues) {
elements.push(
<Dialogue
camera={camera}
dialogue={dialogues[key]}
key={key}
onClose={() => {
delete dialogues[key];
}}
scale={scale}
/>
);
}
return <div className={styles.dialogues}>{elements}</div>;
}

View File

@ -0,0 +1,4 @@
.dialogues {
font-family: 'Times New Roman', Times, serif;
font-size: 16px;
}

View File

@ -0,0 +1,49 @@
import {useState} from 'react';
import {useEcs, useEcsTick} from '@/context/ecs.js';
import Entity from './entity.jsx';
export default function Entities({camera, scale}) {
const [ecs] = useEcs();
const [entities, setEntities] = useState({});
useEcsTick((payload) => {
if (!ecs) {
return;
}
const updatedEntities = {...entities};
for (const id in payload) {
if ('1' === id) {
continue;
}
const update = payload[id];
if (false === update) {
delete updatedEntities[id];
continue;
}
updatedEntities[id] = ecs.get(id);
const {dialogue} = update.Interlocutor || {};
if (dialogue) {
for (const key in dialogue) {
updatedEntities[id].Interlocutor.dialogues[key] = dialogue[key];
}
}
}
setEntities(updatedEntities);
}, [ecs, entities]);
const renderables = [];
for (const id in entities) {
renderables.push(
<Entity
camera={camera}
entity={entities[id]}
key={id}
scale={scale}
/>
);
}
return (
<>
{renderables}
</>
);
}

View File

@ -0,0 +1,15 @@
import Dialogues from './dialogues.jsx';
export default function Entity({camera, entity, scale}) {
return (
<>
{entity.Interlocutor && (
<Dialogues
camera={camera}
dialogues={entity.Interlocutor.dialogues}
scale={scale}
/>
)}
</>
)
}

View File

@ -10,6 +10,7 @@ import {useMainEntity} from '@/context/main-entity.js';
import Disconnected from './dom/disconnected.jsx'; import Disconnected from './dom/disconnected.jsx';
import Dom from './dom/dom.jsx'; import Dom from './dom/dom.jsx';
import Entities from './dom/entities.jsx';
import HotBar from './dom/hotbar.jsx'; import HotBar from './dom/hotbar.jsx';
import Pixi from './pixi/pixi.jsx'; import Pixi from './pixi/pixi.jsx';
import Devtools from './devtools.jsx'; import Devtools from './devtools.jsx';
@ -394,6 +395,10 @@ export default function Ui({disconnected}) {
}} }}
slots={hotbarSlots} slots={hotbarSlots}
/> />
<Entities
camera={camera}
scale={scale}
/>
{showDisconnected && ( {showDisconnected && (
<Disconnected /> <Disconnected />
)} )}