parent
cadf1f3075
commit
940fc15b90
@ -0,0 +1,12 @@ |
|||||||
|
import './CommandEntry.css'; |
||||||
|
|
||||||
|
function CommandEntry({command, onChange, onSubmit, ...props}) { |
||||||
|
return ( |
||||||
|
<div className="commandline"> |
||||||
|
<input type="text" placeholder="Enter a command!" value={command} onChange={onChange} {...props}/> |
||||||
|
<button onClick={() => onSubmit(command)}>Do</button> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default CommandEntry; |
@ -0,0 +1,60 @@ |
|||||||
|
import { useState, useEffect } from 'react'; |
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; |
||||||
|
import { useParams, useNavigate } from 'react-router-dom'; |
||||||
|
import { apiUrl, wsUrl } from '../apiTools.jsx'; |
||||||
|
import { useFixLinks } from '../clientStuff.jsx'; |
||||||
|
import { useLoggedIn } from '../AuthProvider.jsx'; |
||||||
|
import useWebSocket, { ReadyState } from 'react-use-websocket'; |
||||||
|
|
||||||
|
import Page from '../page/Page.jsx'; |
||||||
|
import MessageFeed from './MessageFeed.jsx'; |
||||||
|
import Sidebar from './Sidebar.jsx'; |
||||||
|
import CommandEntry from './CommandEntry.jsx'; |
||||||
|
|
||||||
|
function Live({editing, ...props}) { |
||||||
|
const navigate = useNavigate(); |
||||||
|
const loggedIn = useLoggedIn(); |
||||||
|
const { pagenumber } = useParams(); |
||||||
|
const [command, setCommand] = useState(""); |
||||||
|
const [ messageHistory, setMessageHistory ] = useState([]); |
||||||
|
const [ currentNumber, setCurrentNumber ] = useState(1); |
||||||
|
|
||||||
|
const [ connecting, setConnecting ] = useState(true); |
||||||
|
const { sendMessage, lastMessage, readyState } = useWebSocket(`${wsUrl}/embody`, { |
||||||
|
onClose: () => setConnecting(false) |
||||||
|
}, connecting); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (lastMessage !== null) { |
||||||
|
setMessageHistory((prev) => prev.concat(lastMessage)); |
||||||
|
} |
||||||
|
}, [lastMessage]); |
||||||
|
|
||||||
|
useEffect(() => { sendMessage("what the fuk is up"); }, []); |
||||||
|
|
||||||
|
function handleSendMessage() { |
||||||
|
console.log("sending a message..."); |
||||||
|
sendMessage("button got clicked"); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Page editing={editing} number={currentNumber} {...props}/> |
||||||
|
<MessageFeed messages={messageHistory}> |
||||||
|
<button disabled={readyState!=ReadyState.OPEN} onClick={handleSendMessage}>send a message to the server!</button> |
||||||
|
<button disabled={connecting} onClick={() => setConnecting(true)}>Reconnect</button> |
||||||
|
</MessageFeed> |
||||||
|
<Sidebar pagenumber={pagenumber} hidden="true" sendWord={(word) => setCommand((command + " " + word).trim())}> |
||||||
|
{!editing && <li><button onClick={() => navigate(`/${pagenumber}/edit`)}>edit</button></li>} |
||||||
|
{editing && <li><button disabled={postMutation.isPending} onClick={submitChanges}>save</button></li>} |
||||||
|
{editing && <li><button onClick={() => navigate(`/${pagenumber}`)}>return</button></li>} |
||||||
|
</Sidebar> |
||||||
|
<CommandEntry |
||||||
|
command={command} |
||||||
|
onChange={(e) => setCommand(e.target.value)} |
||||||
|
onSubmit={(e) => { setCommand(""); sendMessage(e); }}/> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default Live; |
@ -0,0 +1,25 @@ |
|||||||
|
.messages-tray { |
||||||
|
transition: all 0.1s linear; |
||||||
|
margin: 0; |
||||||
|
background: lightblue; |
||||||
|
position: fixed; |
||||||
|
width: 30ch; |
||||||
|
height: 100%; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
} |
||||||
|
.messages-hidden { |
||||||
|
transform: translateX(-20ch); |
||||||
|
} |
||||||
|
|
||||||
|
.sidebar li { |
||||||
|
list-style: none; |
||||||
|
} |
||||||
|
|
||||||
|
.sidebar li button { |
||||||
|
text-transform: uppercase; |
||||||
|
} |
||||||
|
|
||||||
|
.sidebar-hidden li { |
||||||
|
display: none; |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
import { useEffect, useState, useCallback } from 'react'; |
||||||
|
import { useForm } from "react-hook-form"; |
||||||
|
import { useNavigate } from 'react-router-dom'; |
||||||
|
|
||||||
|
import './MessageFeed.css'; |
||||||
|
|
||||||
|
function MessageFeed({messages=[], children}) { |
||||||
|
const [ open, setOpen ] = useState(false); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={`messages-tray ${!open?'messages-hidden':''}`}> |
||||||
|
<button style={{float: 'right'}} onClick={() => setOpen(!open)}>{open?'Hide':'Show'}</button> |
||||||
|
<h2>Message history:</h2> |
||||||
|
<ol> |
||||||
|
{messages.map((message, idx) => |
||||||
|
<li key={idx}> |
||||||
|
{message.data} |
||||||
|
<code style={{background: "lightgrey"}}>{JSON.stringify(message)}</code> |
||||||
|
</li> |
||||||
|
)} |
||||||
|
</ol> |
||||||
|
{children} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default MessageFeed; |
@ -1,13 +0,0 @@ |
|||||||
import { useState } from 'react'; |
|
||||||
|
|
||||||
import './CommandEntry.css'; |
|
||||||
|
|
||||||
function CommandEntry({command, onChange}) { |
|
||||||
return ( |
|
||||||
<div className="commandline"> |
|
||||||
<input type="text" placeholder="Enter a command!" value={command} onChange={onChange}/> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export default CommandEntry; |
|
@ -0,0 +1,19 @@ |
|||||||
|
import { useState } from 'react'; |
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; |
||||||
|
import { useParams, useNavigate } from 'react-router-dom'; |
||||||
|
import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx'; |
||||||
|
import { useFixLinks } from '../clientStuff.jsx'; |
||||||
|
import { useLoggedIn } from '../AuthProvider.jsx'; |
||||||
|
import Page from './Page.jsx'; |
||||||
|
|
||||||
|
import './Pages.css'; |
||||||
|
|
||||||
|
function GhostPage({editing, ...props}) { |
||||||
|
const { pagenumber } = useParams(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Page number={pagenumber} editing={editing} {...props}/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default GhostPage; |
@ -1,128 +0,0 @@ |
|||||||
import { useState } from 'react'; |
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; |
|
||||||
import { useParams, useNavigate } from 'react-router-dom'; |
|
||||||
import { apiUrl, fetchPage, fetchPageAtEdit, postPage, deletePage } from '../apiTools.jsx'; |
|
||||||
import { useFixLinks } from '../clientStuff.jsx'; |
|
||||||
import { useLoggedIn } from '../AuthProvider.jsx'; |
|
||||||
import Sidebar from './Sidebar.jsx'; |
|
||||||
import CommandEntry from './CommandEntry.jsx'; |
|
||||||
|
|
||||||
import './Pages.css'; |
|
||||||
|
|
||||||
function PageWithSidebar({ editing }) { |
|
||||||
const queryClient = useQueryClient(); |
|
||||||
const navigate = useNavigate(); |
|
||||||
const { pagenumber, editid } = useParams(); |
|
||||||
const loggedIn = useLoggedIn(); |
|
||||||
const noLoad = useFixLinks(); |
|
||||||
const [command, setCommand] = useState(""); |
|
||||||
|
|
||||||
const fetchQuery = useQuery({ // fetch the currrent values |
|
||||||
queryKey: ['page', pagenumber, editid], |
|
||||||
queryFn: () => editid ? fetchPageAtEdit(pagenumber, editid) : fetchPage(pagenumber) |
|
||||||
}) |
|
||||||
|
|
||||||
const postMutation = useMutation({ // for changing the value when we're done with it |
|
||||||
mutationFn: ({id, title, description}) => postPage({id, title, description}), |
|
||||||
onSettled: async (data, error, variables) => { |
|
||||||
// Invalidate and refetch |
|
||||||
await queryClient.invalidateQueries({ queryKey: ['page', variables.id, undefined] }) |
|
||||||
}, |
|
||||||
}); |
|
||||||
|
|
||||||
const deleteMutation = useMutation({ // for changing the value when we're done with it |
|
||||||
mutationFn: (id) => deletePage(id), |
|
||||||
onSettled: async (data, error, variables) => { |
|
||||||
// Invalidate and refetch |
|
||||||
await queryClient.invalidateQueries({ queryKey: ['pages'] }) |
|
||||||
}, |
|
||||||
}); |
|
||||||
|
|
||||||
const readyToShow = !(fetchQuery.error || fetchQuery.isPending); |
|
||||||
|
|
||||||
let {id, title, description, html, time, author} = fetchQuery.data || {}; |
|
||||||
if (!title) title = "[no title]"; |
|
||||||
if (!html) html = "[body missing]"; |
|
||||||
if (!description) description = "[body missing]"; |
|
||||||
|
|
||||||
function submitChanges(e) { |
|
||||||
const newTitle = document.querySelector('span').innerHTML; |
|
||||||
const newText = document.querySelector('pre').innerHTML; |
|
||||||
postMutation.mutate({ |
|
||||||
id: pagenumber, |
|
||||||
title: newTitle, |
|
||||||
description: newText |
|
||||||
}); |
|
||||||
navigate(`/${pagenumber}`, {replace: true}) |
|
||||||
} |
|
||||||
|
|
||||||
function submitDelete(e) { |
|
||||||
e.preventDefault(); |
|
||||||
deleteMutation.mutate(pagenumber); |
|
||||||
navigate(`/`); |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<> |
|
||||||
<div className="main-column"> |
|
||||||
<header> |
|
||||||
<h1> |
|
||||||
<a href="/" {...noLoad}>🌳</a> |
|
||||||
{pagenumber}. |
|
||||||
<span |
|
||||||
contentEditable={editing} |
|
||||||
dangerouslySetInnerHTML={{__html: readyToShow ? title : "..." }} /> |
|
||||||
</h1> |
|
||||||
{readyToShow && editid && `saved at ${time} by user #${author}`} |
|
||||||
<hr/> |
|
||||||
</header> |
|
||||||
<section className="page-contents"> |
|
||||||
{ readyToShow ? |
|
||||||
( |
|
||||||
editing ? |
|
||||||
<pre |
|
||||||
contentEditable="true" |
|
||||||
dangerouslySetInnerHTML={{__html: description}} /> |
|
||||||
: |
|
||||||
<div |
|
||||||
dangerouslySetInnerHTML={{__html: html}} |
|
||||||
{...noLoad} /> |
|
||||||
) |
|
||||||
: |
|
||||||
"..." |
|
||||||
} |
|
||||||
</section> |
|
||||||
<button |
|
||||||
onClick={() => navigate(`/${pagenumber}/history`)}> |
|
||||||
History |
|
||||||
</button> |
|
||||||
{editing && ( |
|
||||||
<button |
|
||||||
disabled={postMutation.isPending} |
|
||||||
onClick={submitChanges}> |
|
||||||
{postMutation.isPending ? "Updating..." : "Update"} |
|
||||||
</button>)} |
|
||||||
{!editing && !editid && ( |
|
||||||
<button |
|
||||||
disabled={!loggedIn} |
|
||||||
onClick={() => navigate(`/${pagenumber}/edit`)}> |
|
||||||
Edit Page |
|
||||||
</button>)} |
|
||||||
{loggedIn && ( |
|
||||||
<button |
|
||||||
disabled={deleteMutation.isPending} |
|
||||||
onClick={submitDelete}> |
|
||||||
{deleteMutation.isPending ? "Deleting..." : "Delete this page and entire edit history (no backsies)"} |
|
||||||
</button>)} |
|
||||||
</div> |
|
||||||
<Sidebar pagenumber={pagenumber} hidden="true" sendWord={(word) => setCommand((command + " " + word).trim())}> |
|
||||||
{!editing && <li><button onClick={() => navigate(`/${pagenumber}/edit`)}>edit</button></li>} |
|
||||||
{editing && <li><button disabled={postMutation.isPending} onClick={submitChanges}>save</button></li>} |
|
||||||
{editing && <li><button onClick={() => navigate(`/${pagenumber}`)}>return</button></li>} |
|
||||||
</Sidebar> |
|
||||||
<CommandEntry command={command} onChange={(e) => setCommand(e.target.value)}/> |
|
||||||
</> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export default PageWithSidebar; |
|
@ -0,0 +1,47 @@ |
|||||||
|
const sqlite = require('better-sqlite3'); |
||||||
|
const db = new sqlite('the_big_db.db', { verbose: console.log }); |
||||||
|
|
||||||
|
var sockets = new Map(); |
||||||
|
|
||||||
|
function interpret(context, subject, command) { |
||||||
|
const words = command.split(' '); |
||||||
|
|
||||||
|
const verbs = findVerbs(context, subject); |
||||||
|
// first word is either a subject or a verb. either way there must be a verb.
|
||||||
|
// check if the first word is in the list of verbs.
|
||||||
|
const [first, second, third, ...rest] = words; |
||||||
|
if (second in verbs) { |
||||||
|
executeVerb(verbs.get(second), first, third, ...rest); |
||||||
|
} else { |
||||||
|
executeVerb(verbs.get(first), subject, second, third, ...rest) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function findVerbs(location, actor) { |
||||||
|
// check for verbs on actor
|
||||||
|
// check for verbs on objects in location
|
||||||
|
// check for verbs on location
|
||||||
|
const out = new Map(); |
||||||
|
out.set("look", 29); |
||||||
|
return out; |
||||||
|
} |
||||||
|
|
||||||
|
function executeVerb(verb, subject, object, ...rest) { |
||||||
|
lookUpObject(verb).fn(subject, object, ...rest) |
||||||
|
} |
||||||
|
|
||||||
|
const objectQuery = db.prepare('select * from pages where id=?'); |
||||||
|
function lookUpObject(id) { |
||||||
|
// return objectQuery.get(id);
|
||||||
|
if (id == 30) return {name: "shoofle", contents: "this is a shoofle", location: 1}; |
||||||
|
if (id == 29) return { |
||||||
|
name: "look",
|
||||||
|
contents: "send description of direct object to subject's socket",
|
||||||
|
fn: (subject, object, ...rest) => { |
||||||
|
sockets.get(subject)?.send(`you looked around! subject: ${subject} object: ${object} args: ${rest}`); |
||||||
|
console.log(`${subject} looked at ${object} with args ${rest}`); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { interpret, sockets, lookUpObject }; |
@ -0,0 +1,42 @@ |
|||||||
|
const express = require('express'); |
||||||
|
const app = express.Router(); |
||||||
|
const expressWs = require('express-ws')(app); |
||||||
|
|
||||||
|
const sqlite = require('better-sqlite3'); |
||||||
|
const db = new sqlite('the_big_db.db', { verbose: console.log }); |
||||||
|
|
||||||
|
const { loginRequired } = require('../authStuff.js'); |
||||||
|
|
||||||
|
const { interpret, sockets, lookUpObject } = require('../interpreter.js'); |
||||||
|
|
||||||
|
const clockListeners = new Set(); |
||||||
|
const clock = setInterval(() => { |
||||||
|
if (clockListeners.size == 0) return; |
||||||
|
console.log(`sending a ping to all ${clockListeners.size} connections`); |
||||||
|
clockListeners.forEach((x) => x()); |
||||||
|
}, 5000); |
||||||
|
console.log(`set up the clock: ${clock}`); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.ws('/embody', (ws, req) => { |
||||||
|
// const { playerObject } = db.prepare('select playerObject from users where id=?').get(req.session.userId)
|
||||||
|
const playerObjectId = 30; // mocked out for now!
|
||||||
|
sockets.set(playerObjectId, ws); |
||||||
|
|
||||||
|
ws.on('message', (msg) => {
|
||||||
|
const location = lookUpObject(playerObjectId).location; |
||||||
|
|
||||||
|
interpret(location, playerObjectId, msg); |
||||||
|
}); |
||||||
|
|
||||||
|
ws.on('close', () => sockets.delete(playerObjectId)); |
||||||
|
}); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = app; |
Loading…
Reference in new issue