diff --git a/package.json b/package.json
index d6cdc78..584c20c 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
"react-modal": "^3.11.2",
"react-scripts": "3.4.0",
"shortid": "^2.2.15",
- "theme-ui": "^0.3.1"
+ "theme-ui": "^0.3.1",
+ "uuid": "^7.0.2"
},
"scripts": {
"predeploy": "yarn build",
diff --git a/src/components/AddPartyMemberButton.js b/src/components/AddPartyMemberButton.js
index a3584c7..1696b59 100644
--- a/src/components/AddPartyMemberButton.js
+++ b/src/components/AddPartyMemberButton.js
@@ -3,7 +3,7 @@ import { IconButton, Flex, Box, Label, Text } from "theme-ui";
import Modal from "./Modal";
-function AddPartyMemberButton({ streamId }) {
+function AddPartyMemberButton({ peerId }) {
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
function openModal() {
setIsAddModalOpen(true);
@@ -32,7 +32,7 @@ function AddPartyMemberButton({ streamId }) {
- {streamId}
+ {peerId}
diff --git a/src/components/GameViewSwitch.js b/src/components/GameViewSwitch.js
deleted file mode 100644
index 5837935..0000000
--- a/src/components/GameViewSwitch.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import React from "react";
-import { Flex, IconButton } from "theme-ui";
-
-function SocialIcon() {
- return (
-
-
-
- );
-}
-
-function EncounterIcon() {
- return (
-
-
-
- );
-}
-
-function GameViewSwitch({ view, onViewChange }) {
- return (
-
- onViewChange("social")}
- >
-
-
- onViewChange("encounter")}
- >
-
-
-
- );
-}
-
-export default GameViewSwitch;
diff --git a/src/components/Message.js b/src/components/Message.js
new file mode 100644
index 0000000..d01d15c
--- /dev/null
+++ b/src/components/Message.js
@@ -0,0 +1,18 @@
+import React from "react";
+import { Box, Text, Flex } from "theme-ui";
+
+function Message({ message }) {
+ const timeFormatted = new Date(message.time).toLocaleTimeString("en-US");
+
+ return (
+
+
+ {message.nickname}
+ {/* {timeFormatted} */}
+
+ {message.text}
+
+ );
+}
+
+export default Message;
diff --git a/src/components/Party.js b/src/components/Party.js
index 2a27901..e350512 100644
--- a/src/components/Party.js
+++ b/src/components/Party.js
@@ -1,78 +1,80 @@
-import React from "react";
+import React, { useState } from "react";
+import { Flex, Box, Input, Text } from "theme-ui";
+import { v4 as uuid } from "uuid";
-import { Flex, Box } from "theme-ui";
-
-import PartyVideo from "./PartyVideo";
import AddPartyMemberButton from "./AddPartyMemberButton";
-import GameViewSwitch from "./GameViewSwitch";
+import Message from "./Message";
-function Party({ streams, localStreamId, view, onViewChange }) {
- if (view === "social") {
- return (
-
-
- {Object.entries(streams).map(([id, stream]) => (
-
-
-
- ))}
-
-
-
-
-
-
-
-
- );
- } else if (view === "encounter") {
- return (
-
- {Object.entries(streams).map(([id, stream]) => (
-
-
-
- ))}
-
-
-
-
-
-
-
- );
+import { getRandomMonster } from "../helpers/monsters";
+
+function Party({ peerId, messages, onMessageSend }) {
+ const [messageText, setMessageText] = useState("");
+ const [nickname, setNickname] = useState(getRandomMonster());
+
+ function handleMessageSubmit(event) {
+ event.preventDefault();
+ if (!messageText || !peerId) {
+ return;
+ }
+ const id = uuid();
+ const time = Date.now();
+ const message = {
+ nickname,
+ id,
+ text: messageText,
+ time
+ };
+ onMessageSend(message);
+ setMessageText("");
}
- return null;
+ return (
+
+
+
+
+
+ {Object.values(messages)
+ .sort((a, b) => a.time - b.time)
+ .map(message => (
+
+ ))}
+
+ setMessageText(event.target.value)}
+ p={1}
+ disabled={!peerId}
+ />
+
+
+ {nickname}
+
+
+
+ );
}
-Party.defaultProps = { view: "social" };
-
export default Party;
diff --git a/src/components/PartyVideo.js b/src/components/PartyVideo.js
deleted file mode 100644
index 83bef85..0000000
--- a/src/components/PartyVideo.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import React, { useRef, useEffect } from "react";
-
-function PartyVideo({ stream, muted }) {
- const videoRef = useRef();
-
- useEffect(() => {
- if (videoRef.current) {
- videoRef.current.srcObject = stream;
- }
- }, [stream]);
-
- return (
-
- );
-}
-
-export default PartyVideo;
diff --git a/src/helpers/monsters.js b/src/helpers/monsters.js
new file mode 100644
index 0000000..cd64a1c
--- /dev/null
+++ b/src/helpers/monsters.js
@@ -0,0 +1,300 @@
+const monsters = [
+ "Aboleth",
+ "Acolyte",
+ "Black Dragon",
+ "Blue Dragon",
+ "Brass Dragon",
+ "Bronze Dragon",
+ "Copper Dragon",
+ "Gold Dragon",
+ "Green Dragon",
+ "Red Dragon",
+ "Silver Dragon",
+ "White Dragon",
+ "Air Elemental",
+ "Androsphinx",
+ "Animated Armor",
+ "Ankheg",
+ "Ape",
+ "Archmage",
+ "Assassin",
+ "Awakened Shrub",
+ "Awakened Tree",
+ "Axe Beak",
+ "Azer",
+ "Baboon",
+ "Badger",
+ "Balor",
+ "Bandit",
+ "Bandit Captain",
+ "Barbed Devil",
+ "Basilisk",
+ "Bat",
+ "Bearded Devil",
+ "Behir",
+ "Berserker",
+ "Black Bear",
+ "Black Pudding",
+ "Blink Dog",
+ "Blood Hawk",
+ "Boar",
+ "Bone Devil",
+ "Brown Bear",
+ "Bugbear",
+ "Bulette",
+ "Camel",
+ "Cat",
+ "Centaur",
+ "Chain Devil",
+ "Chimera",
+ "Chuul",
+ "Clay Golem",
+ "Cloaker",
+ "Cloud Giant",
+ "Cockatrice",
+ "Commoner",
+ "Constrictor Snake",
+ "Couatl",
+ "Crab",
+ "Crocodile",
+ "Cult Fanatic",
+ "Cultist",
+ "Darkmantle",
+ "Death Dog",
+ "Deep Gnome",
+ "Deer",
+ "Deva",
+ "Dire Wolf",
+ "Diseased Giant Rat",
+ "Djinni",
+ "Doppelganger",
+ "Draft Horse",
+ "Dragon Turtle",
+ "Dretch",
+ "Drider",
+ "Drow",
+ "Druid",
+ "Dryad",
+ "Duergar",
+ "Dust Mephit",
+ "Eagle",
+ "Earth Elemental",
+ "Efreeti",
+ "Elephant",
+ "Elk",
+ "Erinyes",
+ "Ettercap",
+ "Ettin",
+ "Fire Elemental",
+ "Fire Giant",
+ "Flesh Golem",
+ "Flying Snake",
+ "Flying Sword",
+ "Frog",
+ "Frost Giant",
+ "Gargoyle",
+ "Gelatinous Cube",
+ "Ghast",
+ "Ghost",
+ "Ghoul",
+ "Giant Ape",
+ "Giant Badger",
+ "Giant Bat",
+ "Giant Boar",
+ "Giant Centipede",
+ "Giant Constrictor Snake",
+ "Giant Crab",
+ "Giant Crocodile",
+ "Giant Eagle",
+ "Giant Elk",
+ "Giant Fire Beetle",
+ "Giant Frog",
+ "Giant Goat",
+ "Giant Hyena",
+ "Giant Lizard",
+ "Giant Octopus",
+ "Giant Owl",
+ "Giant Poisonous Snake",
+ "Giant Rat",
+ "Giant Scorpion",
+ "Giant Sea Horse",
+ "Giant Shark",
+ "Giant Spider",
+ "Giant Toad",
+ "Giant Vulture",
+ "Giant Wasp",
+ "Giant Weasel",
+ "Giant Wolf Spider",
+ "Gibbering Mouther",
+ "Glabrezu",
+ "Gladiator",
+ "Gnoll",
+ "Goat",
+ "Goblin",
+ "Gorgon",
+ "Gray Ooze",
+ "Green Hag",
+ "Grick",
+ "Griffon",
+ "Grimlock",
+ "Guard",
+ "Guardian Naga",
+ "Gynosphinx",
+ "Harpy",
+ "Hawk",
+ "Hell Hound",
+ "Hezrou",
+ "Hill Giant",
+ "Hippogriff",
+ "Hobgoblin",
+ "Homunculus",
+ "Horned Devil",
+ "Hunter Shark",
+ "Hydra",
+ "Hyena",
+ "Ice Devil",
+ "Ice Mephit",
+ "Imp",
+ "Incubus",
+ "Invisible Stalker",
+ "Iron Golem",
+ "Jackal",
+ "Killer Whale",
+ "Knight",
+ "Kobold",
+ "Kraken",
+ "Lamia",
+ "Lemure",
+ "Lich",
+ "Lion",
+ "Lizard",
+ "Lizardfolk",
+ "Mage",
+ "Magma Mephit",
+ "Magmin",
+ "Mammoth",
+ "Manticore",
+ "Marilith",
+ "Mastiff",
+ "Medusa",
+ "Merfolk",
+ "Merrow",
+ "Mimic",
+ "Minotaur",
+ "Minotaur Skeleton",
+ "Mule",
+ "Mummy",
+ "Mummy Lord",
+ "Nalfeshnee",
+ "Night Hag",
+ "Nightmare",
+ "Noble",
+ "Ochre Jelly",
+ "Octopus",
+ "Ogre",
+ "Ogre Zombie",
+ "Oni",
+ "Orc",
+ "Otyugh",
+ "Owl",
+ "Owlbear",
+ "Panther",
+ "Pegasus",
+ "Phase Spider",
+ "Pit Fiend",
+ "Planetar",
+ "Plesiosaurus",
+ "Poisonous Snake",
+ "Polar Bear",
+ "Pony",
+ "Priest",
+ "Pseudodragon",
+ "Purple Worm",
+ "Quasit",
+ "Quipper",
+ "Rakshasa",
+ "Rat",
+ "Raven",
+ "Reef Shark",
+ "Remorhaz",
+ "Rhinoceros",
+ "Riding Horse",
+ "Roc",
+ "Roper",
+ "Rug of Smothering",
+ "Rust Monster",
+ "Saber-Toothed Tiger",
+ "Sahuagin",
+ "Salamander",
+ "Satyr",
+ "Scorpion",
+ "Scout",
+ "Sea Hag",
+ "Sea Horse",
+ "Shadow",
+ "Shambling Mound",
+ "Shield Guardian",
+ "Shrieker",
+ "Skeleton",
+ "Solar",
+ "Specter",
+ "Spider",
+ "Spirit Naga",
+ "Sprite",
+ "Spy",
+ "Steam Mephit",
+ "Stirge",
+ "Stone Giant",
+ "Stone Golem",
+ "Storm Giant",
+ "Succubus",
+ "Swarm of Bats",
+ "Swarm of Beetles",
+ "Swarm of Centipedes",
+ "Swarm of Insects",
+ "Swarm of Poisonous Snakes",
+ "Swarm of Quippers",
+ "Swarm of Rats",
+ "Swarm of Ravens",
+ "Swarm of Spiders",
+ "Swarm of Wasps",
+ "Tarrasque",
+ "Thug",
+ "Tiger",
+ "Treant",
+ "Tribal Warrior",
+ "Triceratops",
+ "Troll",
+ "Tyrannosaurus Rex",
+ "Unicorn",
+ "Vampire",
+ "Vampire Spawn",
+ "Veteran",
+ "Violet Fungus",
+ "Vrock",
+ "Vulture",
+ "Warhorse",
+ "Warhorse Skeleton",
+ "Water Elemental",
+ "Weasel",
+ "Werebear",
+ "Wereboar",
+ "Wererat",
+ "Weretiger",
+ "Werewolf",
+ "Wight",
+ "Will-o'-Wisp",
+ "Winter Wolf",
+ "Wolf",
+ "Worg",
+ "Wraith",
+ "Wyvern",
+ "Xorn",
+ "Zombie"
+];
+
+export default monsters;
+
+export function getRandomMonster() {
+ return monsters[Math.floor(Math.random() * monsters.length)];
+}
diff --git a/src/helpers/useSession.js b/src/helpers/useSession.js
index b888089..549da6e 100644
--- a/src/helpers/useSession.js
+++ b/src/helpers/useSession.js
@@ -1,18 +1,10 @@
-import { useEffect, useState, useRef } from "react";
+import { useEffect, useState } from "react";
import Peer from "peerjs";
-const getUserMedia =
- navigator.getUserMedia ||
- navigator.webkitGetUserMedia ||
- navigator.mozGetUserMedia;
-
function useSession(onConnectionOpen, onConnectionSync) {
const [peerId, setPeerId] = useState(null);
const [peer, setPeer] = useState(null);
const [connections, setConnections] = useState({});
- const [streams, setStreams] = useState({});
- // Keep a ref to the streams in order to clean them up on unmount
- const streamsRef = useRef(streams);
function addConnection(connection) {
console.log("Adding connection", connection.peer);
@@ -28,59 +20,15 @@ function useSession(onConnectionOpen, onConnectionSync) {
});
}
- function addStream(stream, id) {
- console.log("Adding stream", id);
- setStreams(prevStreams => {
- console.log("Streams", {
- ...prevStreams,
- [id]: stream
- });
- return {
- ...prevStreams,
- [id]: stream
- };
- });
- }
-
useEffect(() => {
console.log("Creating peer");
setPeer(new Peer());
-
- return () => {
- console.log("Cleaning up streams");
- for (let stream of Object.values(streamsRef.current)) {
- for (let track of stream.getTracks()) {
- track.stop();
- }
- }
- };
}, []);
- // Update stream refs
- useEffect(() => {
- console.log("Syncing stream ref to stream state", streams);
- streamsRef.current = streams;
- }, [streams, streamsRef]);
-
useEffect(() => {
function handleOpen(id) {
console.log("Peer open", id);
setPeerId(id);
-
- console.log("Getting user data");
- getUserMedia(
- {
- video: {
- frameRate: { ideal: 15, max: 20 },
- width: { max: 256 },
- height: { max: 144 }
- },
- audio: false
- },
- stream => {
- addStream(stream, id);
- }
- );
}
function handleConnection(connection) {
@@ -90,7 +38,7 @@ function useSession(onConnectionOpen, onConnectionSync) {
if (metadata.sync) {
connection.send({
id: "sync",
- data: Object.keys(connections)
+ data: { connections: Object.keys(connections) }
});
if (onConnectionSync) {
onConnectionSync(connection);
@@ -118,25 +66,6 @@ function useSession(onConnectionOpen, onConnectionSync) {
});
}
- function handleCall(call) {
- console.log("incoming call", call.peer);
- call.answer(streams[peerId]);
- call.on("stream", remoteStream => {
- addStream(remoteStream, call.peer);
- });
- function removeStream() {
- setStreams(prevStreams => {
- const { [call.peer]: old, ...rest } = prevStreams;
- return rest;
- });
- }
- call.on("close", removeStream);
- call.on("error", error => {
- console.error("Media Connection error", error);
- removeStream();
- });
- }
-
function handleError(error) {
console.error("Peer error", error);
}
@@ -147,23 +76,13 @@ function useSession(onConnectionOpen, onConnectionSync) {
peer.on("open", handleOpen);
peer.on("connection", handleConnection);
- peer.on("call", handleCall);
peer.on("error", handleError);
return () => {
peer.removeListener("open", handleOpen);
peer.removeListener("connection", handleConnection);
- peer.removeListener("call", handleCall);
peer.removeListener("error", handleError);
};
- }, [peer, peerId, connections, onConnectionOpen, onConnectionSync, streams]);
-
- function call(connectionId) {
- console.log("Calling", connectionId);
- const call = peer.call(connectionId, streams[peerId]);
- call.on("stream", stream => {
- addStream(stream, connectionId);
- });
- }
+ }, [peer, peerId, connections, onConnectionOpen, onConnectionSync]);
function connectTo(connectionId) {
console.log("Connecting to", connectionId);
@@ -177,7 +96,8 @@ function useSession(onConnectionOpen, onConnectionSync) {
connection.on("open", () => {
connection.on("data", data => {
if (data.id === "sync") {
- for (let syncId of data.data) {
+ const { connections: syncConnections } = data.data;
+ for (let syncId of syncConnections) {
console.log("Syncing to", syncId);
if (connectionId === syncId || syncId in connections) {
continue;
@@ -186,7 +106,6 @@ function useSession(onConnectionOpen, onConnectionSync) {
metadata: { sync: false }
});
addConnection(syncConnection);
- call(syncId);
}
}
});
@@ -194,10 +113,9 @@ function useSession(onConnectionOpen, onConnectionSync) {
onConnectionOpen(connection);
}
});
- call(connectionId);
}
- return { peer, peerId, streams, connections, connectTo };
+ return { peer, peerId, connections, connectTo };
}
export default useSession;
diff --git a/src/routes/Game.js b/src/routes/Game.js
index d6a486d..ef67c01 100644
--- a/src/routes/Game.js
+++ b/src/routes/Game.js
@@ -20,21 +20,16 @@ function Game() {
const { gameId } = useContext(GameContext);
const handleConnectionOpenCallback = useCallback(handleConnectionOpen);
const handleConnectionSyncCallback = useCallback(handleConnectionSync);
- const { peerId, connections, connectTo, streams } = useSession(
+ const { peerId, connections, connectTo } = useSession(
handleConnectionOpenCallback,
handleConnectionSyncCallback
);
useEffect(() => {
- if (
- gameId !== null &&
- peerId !== null &&
- streams[peerId] &&
- !(gameId in connections)
- ) {
+ if (gameId !== null && peerId !== null && !(gameId in connections)) {
connectTo(gameId);
}
- }, [gameId, peerId, connectTo, streams, connections]);
+ }, [gameId, peerId, connectTo, connections]);
const [mapSource, setMapSource] = useState(null);
const mapDataRef = useRef(null);
@@ -71,6 +66,18 @@ function Game() {
}
}
+ const [messages, setMessages] = useState({});
+ function handleMessageSend(message) {
+ setMessages(prevMessages => ({
+ ...prevMessages,
+ [message.id]: message
+ }));
+ for (let connection of Object.values(connections)) {
+ const data = { [message.id]: message };
+ connection.send({ id: "message", data });
+ }
+ }
+
function handleConnectionOpen(connection) {
connection.on("data", data => {
if (data.id === "map") {
@@ -89,6 +96,12 @@ function Game() {
omit(prevMapTokens, Object.keys(data.data))
);
}
+ if (data.id === "message") {
+ setMessages(prevMessages => ({
+ ...prevMessages,
+ ...data.data
+ }));
+ }
});
}
@@ -97,43 +110,29 @@ function Game() {
connection.send({ id: "map", data: mapDataRef.current });
}
connection.send({ id: "tokenEdit", data: mapTokens });
+ connection.send({ id: "message", data: messages });
}
- const [gameView, setGameView] = useState("social");
-
return (
- {gameView === "encounter" && (
-
- )}
- {gameView === "encounter" && (
-
- )}
+
+
-
);
}
diff --git a/src/routes/Home.js b/src/routes/Home.js
index 5f86cd8..43b7680 100644
--- a/src/routes/Home.js
+++ b/src/routes/Home.js
@@ -1,6 +1,6 @@
import React, { useContext } from "react";
import { navigate } from "hookrouter";
-import { Container, Flex, Button, Image, Heading } from "theme-ui";
+import { Container, Flex, Button, Image, Text } from "theme-ui";
import GameContext from "../contexts/GameContext";
@@ -27,9 +27,9 @@ function Home() {
justifyContent: "center"
}}
>
-
+
Owlbear Rodeo
-
+