fun: basic dialogue
This commit is contained in:
parent
78060ded37
commit
92a77d8046
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {},
|
||||||
|
|
24
app/ecs-components/interlocutor.js
Normal file
24
app/ecs-components/interlocutor.js
Normal 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});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
140
app/react-components/dom/dialogue.jsx
Normal file
140
app/react-components/dom/dialogue.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
7
app/react-components/dom/dialogue.module.css
Normal file
7
app/react-components/dom/dialogue.module.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.caret {
|
||||||
|
position: absolute;
|
||||||
|
fill: #00000044;
|
||||||
|
stroke: white;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
}
|
22
app/react-components/dom/dialogues.jsx
Normal file
22
app/react-components/dom/dialogues.jsx
Normal 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>;
|
||||||
|
}
|
||||||
|
|
4
app/react-components/dom/dialogues.module.css
Normal file
4
app/react-components/dom/dialogues.module.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.dialogues {
|
||||||
|
font-family: 'Times New Roman', Times, serif;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
49
app/react-components/dom/entities.jsx
Normal file
49
app/react-components/dom/entities.jsx
Normal 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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
15
app/react-components/dom/entity.jsx
Normal file
15
app/react-components/dom/entity.jsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 />
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user