diff --git a/app/create-homestead.js b/app/create-homestead.js index cdca4bb..0aa5375 100644 --- a/app/create-homestead.js +++ b/app/create-homestead.js @@ -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; } diff --git a/app/create-player.js b/app/create-player.js index 1723be2..5b90e87 100644 --- a/app/create-player.js +++ b/app/create-player.js @@ -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: {}, diff --git a/app/ecs-components/interlocutor.js b/app/ecs-components/interlocutor.js new file mode 100644 index 0000000..f9587b0 --- /dev/null +++ b/app/ecs-components/interlocutor.js @@ -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}); + } + }; + } +} \ No newline at end of file diff --git a/app/react-components/dom/dialogue.jsx b/app/react-components/dom/dialogue.jsx new file mode 100644 index 0000000..fdb488b --- /dev/null +++ b/app/react-components/dom/dialogue.jsx @@ -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 ( +
+ + + + {dialogue.body} +
+ ); +} diff --git a/app/react-components/dom/dialogue.module.css b/app/react-components/dom/dialogue.module.css new file mode 100644 index 0000000..519135f --- /dev/null +++ b/app/react-components/dom/dialogue.module.css @@ -0,0 +1,7 @@ +.caret { + position: absolute; + fill: #00000044; + stroke: white; + left: 50%; + top: 50%; +} diff --git a/app/react-components/dom/dialogues.jsx b/app/react-components/dom/dialogues.jsx new file mode 100644 index 0000000..4826439 --- /dev/null +++ b/app/react-components/dom/dialogues.jsx @@ -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( + { + delete dialogues[key]; + }} + scale={scale} + /> + ); + } + return
{elements}
; +} + diff --git a/app/react-components/dom/dialogues.module.css b/app/react-components/dom/dialogues.module.css new file mode 100644 index 0000000..ef7a30c --- /dev/null +++ b/app/react-components/dom/dialogues.module.css @@ -0,0 +1,4 @@ +.dialogues { + font-family: 'Times New Roman', Times, serif; + font-size: 16px; +} diff --git a/app/react-components/dom/entities.jsx b/app/react-components/dom/entities.jsx new file mode 100644 index 0000000..a7e73cd --- /dev/null +++ b/app/react-components/dom/entities.jsx @@ -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( + + ); + } + return ( + <> + {renderables} + + ); +} diff --git a/app/react-components/dom/entity.jsx b/app/react-components/dom/entity.jsx new file mode 100644 index 0000000..1366fcf --- /dev/null +++ b/app/react-components/dom/entity.jsx @@ -0,0 +1,15 @@ +import Dialogues from './dialogues.jsx'; + +export default function Entity({camera, entity, scale}) { + return ( + <> + {entity.Interlocutor && ( + + )} + + ) +} \ No newline at end of file diff --git a/app/react-components/ui.jsx b/app/react-components/ui.jsx index 34be0d9..6654498 100644 --- a/app/react-components/ui.jsx +++ b/app/react-components/ui.jsx @@ -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} /> + {showDisconnected && ( )}