diff --git a/.envrc b/.envrc index 3550a30..c3792f6 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ +mkdir .direnv use flake diff --git a/visions/server/Taskfile.yml b/visions/server/Taskfile.yml new file mode 100644 index 0000000..8785126 --- /dev/null +++ b/visions/server/Taskfile.yml @@ -0,0 +1,14 @@ +version: '3' + +tasks: + build: + cmds: + - cargo build + + test: + cmds: + - cargo watch -x test + + dev: + cmds: + - cargo watch -x run diff --git a/visions/server/src/asset_db.rs b/visions/server/src/asset_db.rs index d32fa82..4e724e5 100644 --- a/visions/server/src/asset_db.rs +++ b/visions/server/src/asset_db.rs @@ -1,12 +1,11 @@ use std::{ - collections::{hash_map::Iter, HashMap}, - fmt::{self, Display}, - io::Read, + collections::{hash_map::Iter, HashMap}, fmt::{self, Display}, fs, io::Read, path::PathBuf }; use mime::Mime; use serde::{Deserialize, Serialize}; use thiserror::Error; +use typeshare::typeshare; #[derive(Debug, Error)] pub enum Error { @@ -32,8 +31,15 @@ impl From for Error { } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[typeshare] pub struct AssetId(String); +impl AssetId { + pub fn as_str<'a>(&'a self) -> &'a str { + &self.0 + } +} + impl Display for AssetId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "AssetId({})", self.0) @@ -73,19 +79,25 @@ pub struct FsAssets { } impl FsAssets { - pub fn new() -> Self { - Self { - assets: HashMap::new(), - } - } + pub fn new(path: PathBuf) -> Self { + let dir = fs::read_dir(path).unwrap(); + let mut assets = HashMap::new(); - fn assets<'a>(&'a self) -> impl Iterator { - self.assets.keys() + for dir_ent in dir { + println!("{:?}", dir_ent); + let path = dir_ent.unwrap().path(); + let file_name = path.file_name().unwrap().to_str().unwrap(); + assets.insert(AssetId::from(file_name), path.to_str().unwrap().to_owned()); + } + Self { + assets, + } } } impl Assets for FsAssets { fn assets<'a>(&'a self) -> AssetIter<'a> { + println!("FsAssets assets: {:?}", self.assets); AssetIter(self.assets.iter()) } diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index d157a65..d8c8b69 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -99,6 +99,7 @@ impl Core { } pub fn available_images(&self) -> Vec { + println!("available_images"); self.0 .read() .unwrap() diff --git a/visions/server/src/handlers.rs b/visions/server/src/handlers.rs index ae31f61..4597d5a 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -63,7 +63,7 @@ pub async fn handle_available_images(core: Core) -> impl Reply { let image_paths: Vec = core .available_images() .into_iter() - .map(|path| format!("{}", path)) + .map(|path| format!("{}", path.as_str())) .collect(); Ok(Response::builder() @@ -116,6 +116,7 @@ pub async fn handle_connect_websocket( client_id: String, ) -> impl Reply { ws.on_upgrade(move |socket| { + println!("upgrading websocket"); let core = core.clone(); async move { let (mut ws_sender, _) = socket.split(); diff --git a/visions/server/src/main.rs b/visions/server/src/main.rs index 36d5323..4f433c6 100644 --- a/visions/server/src/main.rs +++ b/visions/server/src/main.rs @@ -5,7 +5,7 @@ use handlers::{ }; use std::{ convert::Infallible, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, }; use warp::{ // header, @@ -96,7 +96,7 @@ async fn handle_rejection(err: warp::Rejection) -> Result { { path: "/", element: websocketUrl ? :
+ }, + { + path: "/candela", + element: + }, + { + path: "/design", + element: } ]); return ( diff --git a/visions/ui/src/components/Guages/SimpleGuage.css b/visions/ui/src/components/Guages/SimpleGuage.css new file mode 100644 index 0000000..e69de29 diff --git a/visions/ui/src/components/Guages/SimpleGuage.tsx b/visions/ui/src/components/Guages/SimpleGuage.tsx new file mode 100644 index 0000000..c258c02 --- /dev/null +++ b/visions/ui/src/components/Guages/SimpleGuage.tsx @@ -0,0 +1,8 @@ +import React from 'react'; + +interface GuageProps { + current: number, + max: number, +} + +export const SimpleGuage = ({ current, max }: GuageProps) => <> {current} / {max} diff --git a/visions/ui/src/components/Tabletop/Tabletop.css b/visions/ui/src/components/Tabletop/Tabletop.css index 19f7799..9e47269 100644 --- a/visions/ui/src/components/Tabletop/Tabletop.css +++ b/visions/ui/src/components/Tabletop/Tabletop.css @@ -1,7 +1,3 @@ -.playing-field__background { - flex-grow: 1; -} - -.playing-field__background > img { +.tabletop > img { max-width: 100%; } diff --git a/visions/ui/src/components/Tabletop/Tabletop.tsx b/visions/ui/src/components/Tabletop/Tabletop.tsx index 466bb76..efa21bf 100644 --- a/visions/ui/src/components/Tabletop/Tabletop.tsx +++ b/visions/ui/src/components/Tabletop/Tabletop.tsx @@ -10,5 +10,5 @@ interface TabletopElementProps { export const TabletopElement = ({ backgroundColor, backgroundUrl }: TabletopElementProps) => { const tabletopColorStyle = `rgb(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue})`; - return
{backgroundUrl && playing field}
+ return
{backgroundUrl && playing field}
} diff --git a/visions/ui/src/components/Thumbnail/Thumbnail.tsx b/visions/ui/src/components/Thumbnail/Thumbnail.tsx index c63f4dd..7f5f5ab 100644 --- a/visions/ui/src/components/Thumbnail/Thumbnail.tsx +++ b/visions/ui/src/components/Thumbnail/Thumbnail.tsx @@ -8,7 +8,7 @@ interface ThumbnailProps { onclick?: () => void; } -export const ThumbnailComponent = ({ id, url, onclick }: ThumbnailProps) => { +export const ThumbnailElement = ({ id, url, onclick }: ThumbnailProps) => { const clickHandler = () => { if (onclick) { onclick(); } } diff --git a/visions/ui/src/components/index.ts b/visions/ui/src/components/index.ts new file mode 100644 index 0000000..64551ca --- /dev/null +++ b/visions/ui/src/components/index.ts @@ -0,0 +1,5 @@ +import { ThumbnailElement } from './Thumbnail/Thumbnail' +import { TabletopElement } from './Tabletop/Tabletop' +import { SimpleGuage } from './Guages/SimpleGuage' + +export default { ThumbnailElement, TabletopElement, SimpleGuage } diff --git a/visions/ui/src/plugins/Candela/Charsheet.css b/visions/ui/src/plugins/Candela/Charsheet.css new file mode 100644 index 0000000..d3d2440 --- /dev/null +++ b/visions/ui/src/plugins/Candela/Charsheet.css @@ -0,0 +1,110 @@ +.charsheet__header { + display: flex; +} + +.charsheet__header > div { + margin: 8px; + width: 33%; +} + +.charsheet__body { + display: flex; +} + +.charsheet__body > div { + margin: 8px; + width: 33%; +} + +.action-group { + position: relative; + border: 2px solid black; + border-radius: 4px; + + padding-left: 8px; + padding-bottom: 8px; +} + +.action-group:before { + content: " "; + position: absolute; + z-index: -1; + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border: 2px solid black; +} + +.action-group__header { + display: flex; + font-size: xx-large; + justify-content: space-between; + margin: 4px; + margin-left: -4px; + padding-left: 4px; + background-color: black; + color: white; +} + +.action-group__action { + margin: 2px; +} + +.action-group__dots { + margin: 0px; + padding: 0px; + padding-left: 16px; +} + +.action-group__guage-column { + width: 10px; + height: 100%; + margin: 2px; +} + +.action-group__guage-top { + position: relative; + background-color: white; + margin-bottom: 2px; +} + +.action-group__guage-top:before { + content: " "; + position: absolute; + z-index: -1; + top: 2px; + right: 2px; + bottom: 2px; + left: 2px; + border: 1px solid black; +} + +.action-group__guage-top_filled { + background-color: black; + margin-bottom: 2px; +} + +.action-group__guage-bottom { + background-color: white; + height: 8px; +} + +.action-group__guage-bottom_filled { + background-color: black; + height: 8px; +} + +.action-group__guage-bottom:before { + content: " "; + position: absolute; + z-index: -1; + top: 1px; + left: 1px; + right: 1px; + bottom: 1px; +} + +.action-group__guage { + display: flex; +} diff --git a/visions/ui/src/plugins/Candela/Charsheet.tsx b/visions/ui/src/plugins/Candela/Charsheet.tsx new file mode 100644 index 0000000..c6095f4 --- /dev/null +++ b/visions/ui/src/plugins/Candela/Charsheet.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { assertNever } from '.'; +import './Charsheet.css'; +import { DriveGuage } from './DriveGuage/DriveGuage'; +import { Charsheet, Nerve, Cunning, Intuition } from './types'; + +interface CharsheetProps { + sheet: Charsheet, +} + +interface ActionElementProps { + name: string, + gilded: boolean, + value: number, +} + +const ActionElement = ({ name, gilded, value }: ActionElementProps) => { + let dots = []; + for (let i = 0; i < value; i++) { + dots.push("\u25ef"); + } + let diamond = gilded ? "\u25c6" : "\u25c7"; + return (
+

{diamond} {name}

+
{dots}
+
); +} + +interface ActionGroupElementProps { + group: Nerve | Cunning | Intuition; +} + +const ActionGroupElement = ({ group }: ActionGroupElementProps) => { + var title; + var elements = []; + + switch (group.type_) { + case "nerve": { + title =
Nerve
+ elements.push(); + elements.push(); + elements.push(); + break + } + case "cunning": { + title =
Cunning
+ elements.push(); + elements.push(); + elements.push(); + break + } + case "intuition": { + title =
Intuition
+ elements.push(); + elements.push(); + elements.push(); + break + } + default: { + assertNever(group); + } + } + + return (
+ {title} + {elements} +
) +} + +interface AbilitiesElementProps { + role: string + role_abilities: string[] + specialty: string + specialty_abilities: string[] +} + +const AbilitiesElement = ({ role, role_abilities, specialty, specialty_abilities }: AbilitiesElementProps) => { + return (
+

ROLE: {role}

+
    + {role_abilities.map((ability) =>
  • {ability}
  • )} +
+

SPECIALTY: {role}

+
    + {specialty_abilities.map((ability) =>
  • {ability}
  • )} +
+
+ ); +} + +const CharsheetElement_ = ({ sheet }: CharsheetProps) => { + return (
+
+
Candela Obscura
+
+

{sheet.name}

+

{sheet.pronouns}

+

{sheet.circle}

+
+
+

{sheet.style}

+

{sheet.catalyst}

+

{sheet.question}

+
+
+
+
+ + + +
+
+
Marks, Scars, Relationships
+
+
); +} + +export const CharsheetElement = () => { + const sheet = { + type_: 'Candela', + name: "Soren Jensen", + pronouns: 'he/him', + circle: 'Circle of the Bluest Sky', + style: 'dapper gentleman', + catalyst: 'a cursed book', + question: 'What were the contents of that book?', + nerve: { + type_: "nerve", + drives: { current: 1, max: 2 }, + resistances: { current: 0, max: 3 }, + move: { gilded: false, score: 2 }, + strike: { gilded: false, score: 1 }, + control: { gilded: true, score: 0 }, + } as Nerve, + cunning: { + type_: "cunning", + drives: { current: 1, max: 1 }, + resistances: { current: 0, max: 3 }, + sway: { gilded: false, score: 0 }, + read: { gilded: false, score: 0 }, + hide: { gilded: false, score: 0 }, + } as Cunning, + intuition: { + type_: "intuition", + drives: { current: 0, max: 0 }, + resistances: { current: 0, max: 3 }, + survey: { gilded: false, score: 0 }, + focus: { gilded: false, score: 0 }, + sense: { gilded: false, score: 0 }, + } as Intuition, + role: 'Slink', + role_abilities: [ + 'Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?', + ], + specialty: 'Detective', + specialty_abilities: [ + "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you've deduced.", + ], + }; + + return +} diff --git a/visions/ui/src/plugins/Candela/CharsheetPanel.css b/visions/ui/src/plugins/Candela/CharsheetPanel.css new file mode 100644 index 0000000..bca2e0a --- /dev/null +++ b/visions/ui/src/plugins/Candela/CharsheetPanel.css @@ -0,0 +1,24 @@ +.candela-panel-actions { + border: 1px solid black; + padding: 4px; + margin: 2px; +} + +.candela-panel-actions__header { + display: flex; + padding: 2px; + padding-left: 4px; + padding-right: 4px; + justify-content: space-between; + background-color: black; + color: white; +} + +.candela-panel-actions__action { + display: flex; + justify-content: space-between; +} + +.candela-panel-actions__action_gilded { + font-weight: bold; +} diff --git a/visions/ui/src/plugins/Candela/CharsheetPanel.tsx b/visions/ui/src/plugins/Candela/CharsheetPanel.tsx new file mode 100644 index 0000000..ea3d82c --- /dev/null +++ b/visions/ui/src/plugins/Candela/CharsheetPanel.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import { assertNever } from '.'; +import { SimpleGuage } from '../../components/Guages/SimpleGuage'; +import { Charsheet, Nerve, Cunning, Intuition } from './types'; +import './CharsheetPanel.css'; +import classNames from 'classnames'; + +interface CharsheetPanelProps { + sheet: Charsheet; +} + +interface ActionElementProps { + name: string, + gilded: boolean, + value: number, +} + +const ActionElement = ({ name, gilded, value }: ActionElementProps) => { + const className = gilded ? "candela-panel-actions__action_gilded" : "candela-panel-actions__action"; + return (
{name}
{value}
); +} + + +interface ActionGroupElementProps { + group: Nerve | Cunning | Intuition; +} + +const ActionGroupElement = ({ group }: ActionGroupElementProps) => { + var title; + var elements = []; + + switch (group.type_) { + case "nerve": { + title =
Nerve
+ elements.push(); + elements.push(); + elements.push(); + break + } + case "cunning": { + title =
Cunning
+ elements.push(); + elements.push(); + elements.push(); + break + } + case "intuition": { + title =
Intuition
+ elements.push(); + elements.push(); + elements.push(); + break + } + default: { + assertNever(group); + } + } + + return (
+ {title} + {elements} +
) +} + + +const CharsheetPanelElement_ = ({ sheet }: CharsheetPanelProps) => { + return (
+
+

{sheet.name} ({sheet.pronouns})

+

{sheet.specialty}

+
+ +
+ + + +
+ +
+
    + {sheet.role_abilities.map((ability) =>
  • {ability}
  • )} + {sheet.specialty_abilities.map((ability) =>
  • {ability}
  • )} +
+
+
); +} + +export const CharsheetPanelElement = () => { + const sheet = { + type_: 'Candela', + name: "Soren Jensen", + pronouns: 'he/him', + circle: 'Circle of the Bluest Sky', + style: 'dapper gentleman', + catalyst: 'a cursed book', + question: 'What were the contents of that book?', + nerve: { + type_: "nerve", + drives: { current: 1, max: 2 }, + resistances: { current: 0, max: 3 }, + move: { gilded: false, score: 2 }, + strike: { gilded: false, score: 1 }, + control: { gilded: true, score: 0 }, + } as Nerve, + cunning: { + type_: "cunning", + drives: { current: 1, max: 1 }, + resistances: { current: 0, max: 3 }, + sway: { gilded: false, score: 0 }, + read: { gilded: false, score: 0 }, + hide: { gilded: false, score: 0 }, + } as Cunning, + intuition: { + type_: "intuition", + drives: { current: 0, max: 0 }, + resistances: { current: 0, max: 3 }, + survey: { gilded: false, score: 0 }, + focus: { gilded: false, score: 0 }, + sense: { gilded: false, score: 0 }, + } as Intuition, + role: 'Slink', + role_abilities: [ + 'Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?', + ], + specialty: 'Detective', + specialty_abilities: [ + "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you've deduced.", + ], + }; + + return +} diff --git a/visions/ui/src/plugins/Candela/DriveGuage/DriveGuage.css b/visions/ui/src/plugins/Candela/DriveGuage/DriveGuage.css new file mode 100644 index 0000000..74cbac1 --- /dev/null +++ b/visions/ui/src/plugins/Candela/DriveGuage/DriveGuage.css @@ -0,0 +1,44 @@ +.drive-guages_on-light { + background-color: white; +} + +.drive-guage { + display: flex; +} + +.drive-guage__element { + border: 1px solid black; + margin: 1px; + background-color: white; +} + +.drive-guage__spacer { + margin: 1px; +} + +.drive-guage__top { + margin: 1px; + width: 8px; + border: 1px solid black; + background-color: white; +} + +.drive-guage__top_filled { + background-color: black; +} + +.drive-guage__bottom { + margin: 1px; + border: 1px solid black; + width: 8px; + height: 8px; + background-color: white; +} + +.drive-guage__bottom_filled { + background-color: black; +} + +.drive-guages_on-dark { + background-color: black; +} diff --git a/visions/ui/src/plugins/Candela/DriveGuage/DriveGuage.tsx b/visions/ui/src/plugins/Candela/DriveGuage/DriveGuage.tsx new file mode 100644 index 0000000..aee61cc --- /dev/null +++ b/visions/ui/src/plugins/Candela/DriveGuage/DriveGuage.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import './DriveGuage.css'; + +interface Guage { + current: number; + max: number; +} + +export const DriveGuage = ({ current, max }: Guage) => { + let components = []; + for (let i = 0; i < 9; i++) { + components.push(
+ {i < current ?
 
: +
 
} + {i < max ?
 
: +
 
} +
) + } + components.splice(3, 0,
 
); + components.splice(7, 0,
 
); + return (
+ {components} +
) +} diff --git a/visions/ui/src/plugins/Candela/index.tsx b/visions/ui/src/plugins/Candela/index.tsx new file mode 100644 index 0000000..818120b --- /dev/null +++ b/visions/ui/src/plugins/Candela/index.tsx @@ -0,0 +1,9 @@ +import { CharsheetElement } from './Charsheet'; +import { CharsheetPanelElement } from './CharsheetPanel'; + +export function assertNever(value: never) { + throw new Error("Unexpected value: " + value); +} + +export default { CharsheetElement, CharsheetPanelElement }; + diff --git a/visions/ui/src/plugins/Candela/types.ts b/visions/ui/src/plugins/Candela/types.ts new file mode 100644 index 0000000..555fefa --- /dev/null +++ b/visions/ui/src/plugins/Candela/types.ts @@ -0,0 +1,65 @@ + +export type Guage = { + current: number, + max: number, +} + +export type Action = { + gilded: boolean, + score: number, +} + +export type Actions = { [key: string]: Action } + +export type ActionGroup = { + drives: Guage, + resistances: Guage, + actions: Actions, +} + +export type Nerve = { + type_: "nerve", + drives: Guage, + resistances: Guage, + move: Action, + strike: Action, + control: Action, +} + +export type Cunning = { + type_: "cunning", + drives: Guage, + resistances: Guage, + sway: Action, + read: Action, + hide: Action, +} + +export type Intuition = { + type_: "intuition", + drives: Guage, + resistances: Guage, + survey: Action, + focus: Action, + sense: Action, +} + +export type Charsheet = { + type_: string, + name: string, + pronouns: string + circle: string + style: string, + catalyst: string, + question: string, + + nerve: Nerve, + cunning: Cunning, + intuition: Intuition, + + role: string, + role_abilities: string[], + specialty: string, + specialty_abilities: string[], +} + diff --git a/visions/ui/src/views/Design/Design.css b/visions/ui/src/views/Design/Design.css new file mode 100644 index 0000000..96ef6a1 --- /dev/null +++ b/visions/ui/src/views/Design/Design.css @@ -0,0 +1,4 @@ +.section { + border: 2px solid black; + border-radius: 4px; +} diff --git a/visions/ui/src/views/Design/Design.tsx b/visions/ui/src/views/Design/Design.tsx new file mode 100644 index 0000000..45b248e --- /dev/null +++ b/visions/ui/src/views/Design/Design.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { DriveGuage } from '../../plugins/Candela/DriveGuage/DriveGuage'; + +const DriveGuages = () => { + return (
+
+ +
+
+ +
+
); +} + +export const DesignPage = () => { + return (
+ +
); +} diff --git a/visions/ui/src/views/GmView/GmView.tsx b/visions/ui/src/views/GmView/GmView.tsx index 9b8550f..cc697f3 100644 --- a/visions/ui/src/views/GmView/GmView.tsx +++ b/visions/ui/src/views/GmView/GmView.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { Client, PlayingField } from '../../client'; import { TabletopElement } from '../../components/Tabletop/Tabletop'; -import { ThumbnailComponent } from '../../components/Thumbnail/Thumbnail'; +import { ThumbnailElement } from '../../components/Thumbnail/Thumbnail'; import { WebsocketContext } from '../../components/WebsocketProvider'; import './GmView.css'; @@ -20,7 +20,7 @@ export const GmView = ({ client }: GmViewProps) => { const backgroundUrl = tabletop.backgroundImage ? client.imageUrl(tabletop.backgroundImage) : undefined; return (
- {images.map((imageName) => { client.setBackgroundImage(imageName); }} />)} + {images.map((imageName) => { client.setBackgroundImage(imageName); }} />)}
) diff --git a/visions/ui/src/views/PlayerView/PlayerView.css b/visions/ui/src/views/PlayerView/PlayerView.css index 4e91924..daf3bec 100644 --- a/visions/ui/src/views/PlayerView/PlayerView.css +++ b/visions/ui/src/views/PlayerView/PlayerView.css @@ -4,15 +4,12 @@ } .player-view__left-panel { - flex-grow: 0; min-width: 100px; max-width: 20%; } .player-view__right-panel { - flex-grow: 0; - min-width: 100px; - max-width: 20%; + width: 25%; } diff --git a/visions/ui/src/views/PlayerView/PlayerView.tsx b/visions/ui/src/views/PlayerView/PlayerView.tsx index c4c351e..586d46f 100644 --- a/visions/ui/src/views/PlayerView/PlayerView.tsx +++ b/visions/ui/src/views/PlayerView/PlayerView.tsx @@ -3,6 +3,7 @@ import './PlayerView.css'; import { WebsocketContext } from '../../components/WebsocketProvider'; import { Client } from '../../client'; import { TabletopElement } from '../../components/Tabletop/Tabletop'; +import Candela from '../../plugins/Candela'; interface PlayerViewProps { client: Client; @@ -15,10 +16,9 @@ export const PlayerView = ({ client }: PlayerViewProps) => { const tabletopColorStyle = `rgb(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue})`; const backgroundUrl = tabletop.backgroundImage ? client.imageUrl(tabletop.backgroundImage) : undefined; - return (
-
Left Side
- -
Right Side
+ return (
+
+
) }