fun: basic dialogue
This commit is contained in:
parent
78060ded37
commit
92a77d8046
|
@ -59,5 +59,39 @@ export default async function createHomestead(id) {
|
|||
Ticking: {},
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export default async function createPlayer(id) {
|
|||
},
|
||||
Controlled: {},
|
||||
Direction: {direction: 2},
|
||||
Ecs: {path: ['forests', `${id}`].join('/')},
|
||||
Ecs: {path: ['homesteads', `${id}`].join('/')},
|
||||
Emitter: {},
|
||||
Forces: {},
|
||||
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 Dom from './dom/dom.jsx';
|
||||
import Entities from './dom/entities.jsx';
|
||||
import HotBar from './dom/hotbar.jsx';
|
||||
import Pixi from './pixi/pixi.jsx';
|
||||
import Devtools from './devtools.jsx';
|
||||
|
@ -394,6 +395,10 @@ export default function Ui({disconnected}) {
|
|||
}}
|
||||
slots={hotbarSlots}
|
||||
/>
|
||||
<Entities
|
||||
camera={camera}
|
||||
scale={scale}
|
||||
/>
|
||||
{showDisconnected && (
|
||||
<Disconnected />
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue
Block a user