I updated the tileset format two times in one commit help
- Entirely new interface wow! - Asset upload form - (Now deprecated) tileset previewer in asset upload form - Token previewer in asset upload form - New Database format for assets - New tileset format that allows better auto-patching - Auto-patching - (Partially) fix tokens - Help memaster
|
@ -109,7 +109,6 @@ module.exports = {
|
|||
"code": 150
|
||||
}
|
||||
],
|
||||
"no-bitwise": "error",
|
||||
"no-caller": "error",
|
||||
"no-console": [
|
||||
"error",
|
||||
|
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 970 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 704 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 654 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 668 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 642 B |
After Width: | Height: | Size: 591 B |
After Width: | Height: | Size: 605 B |
After Width: | Height: | Size: 609 B |
After Width: | Height: | Size: 767 B |
After Width: | Height: | Size: 635 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.3 KiB |
|
@ -6,9 +6,9 @@
|
|||
height: 100%
|
||||
overflow: auto
|
||||
position: relative
|
||||
grid-template-rows: auto 1fr
|
||||
|
||||
color: $accent-050
|
||||
font-weight: 400
|
||||
color: $neutral-950
|
||||
font-family: $font-main
|
||||
background-color: $neutral-1000
|
||||
|
||||
|
@ -32,4 +32,8 @@
|
|||
background-color: rgba(87, 87, 87, .6)
|
||||
|
||||
.App-Main
|
||||
height: 100%
|
||||
display: grid
|
||||
grid-template-columns: 80px 1fr
|
||||
|
||||
background-color: $neutral-050
|
||||
|
|
|
@ -5,7 +5,7 @@ import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-d
|
|||
|
||||
import './App.sass';
|
||||
|
||||
import AppHeader from './AppHeader';
|
||||
import AppSidebar from './AppSidebar';
|
||||
import * as Routes from './route/Routes';
|
||||
|
||||
import { AppData } from '../../../common/AppData';
|
||||
|
@ -42,22 +42,24 @@ export default function App() {
|
|||
}
|
||||
{state === 'AUTH' &&
|
||||
<div class='App'>
|
||||
<div class='App-Main'>
|
||||
<Router basename='/app'>
|
||||
<AppHeader />
|
||||
<Switch>
|
||||
<Route exact path='/:campaign/' component={Routes.Home as any} />
|
||||
<Route exact path='/:campaign/details' component={Routes.Campaign as any} />
|
||||
<Route exact path='/:campaign/:map' component={Routes.Map as any} />
|
||||
|
||||
<Route exact path='/:campaign/:map/edit' component={Routes.Editor as any} />
|
||||
{/* <Route exact path='/:campaign/play' component={Route.Play as any} />*/}
|
||||
|
||||
<Route exact path='/' component={Routes.Home as any} />
|
||||
<Redirect to='/' />
|
||||
</Switch>
|
||||
</Router>
|
||||
</div>
|
||||
<Router basename='/app'>
|
||||
<Switch>
|
||||
<Route path='/edit/:campaign/:map' component={Routes.Editor} />
|
||||
<Route>
|
||||
<div class='App-Main'>
|
||||
<AppSidebar />
|
||||
<Switch>
|
||||
<Route path='/assets' component={Routes.Assets} />
|
||||
|
||||
<Route path='/campaigns' component={Routes.Campaigns} />
|
||||
<Route path='/campaign/:id?' component={Routes.Campaign} />
|
||||
|
||||
<Redirect to='/campaigns' />
|
||||
</Switch>
|
||||
</div>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</div>
|
||||
}
|
||||
</AppDataContext.Provider>
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
.AppHeader
|
||||
display: flex
|
||||
flex-direction: row
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
|
||||
height: 64px
|
||||
width: 100%
|
||||
max-width: 1000px
|
||||
|
||||
margin: 0 auto
|
||||
padding: 12px 16px
|
||||
|
||||
user-select: none
|
||||
|
||||
h1
|
||||
margin: 0
|
||||
|
||||
color: white
|
||||
font-size: 39px
|
||||
|
||||
a
|
||||
text-decoration: none
|
||||
|
||||
&:hover, &:focus-visible
|
||||
text-decoration: underline
|
||||
|
||||
.AppHeader-Buttons
|
||||
display: flex
|
||||
flex-direction: row
|
||||
gap: 6px
|
||||
|
||||
button
|
||||
display: block
|
||||
width: 54px
|
||||
height: 54px
|
||||
|
||||
margin: 0
|
||||
padding: 0
|
||||
outline: 0
|
||||
background: transparent
|
||||
border: 3px solid transparent
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background-color: transparentize(white, 0.9)
|
||||
|
||||
&:focus-visible
|
||||
border-color: white
|
||||
|
||||
&:active
|
||||
background-color: transparentize(white, 0.8)
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
image-rendering: pixelated
|
|
@ -1,17 +0,0 @@
|
|||
import * as Preact from 'preact';
|
||||
import { NavLink as Link } from 'react-router-dom';
|
||||
|
||||
import './AppHeader.sass';
|
||||
|
||||
export default function AppHeader() {
|
||||
return (
|
||||
<div class='AppHeader'>
|
||||
<h1><Link to='/'>Virtual Dungeon</Link></h1>
|
||||
<div class='AppHeader-Buttons'>
|
||||
<button><img src='/app/static/ui/icon/settings.png' alt='Settings'/></button>
|
||||
<button><img src='/app/static/ui/icon/profile.png' alt='Profile'/></button>
|
||||
<button><img src='/app/static/ui/icon/logout.png' alt='Logout'/></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
@use '../style/def' as *
|
||||
|
||||
.AppSidebar
|
||||
position: relative
|
||||
z-index: 1
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
|
||||
box-shadow: 0px 0px 12px 0px transparentize($neutral-000, 0.9)
|
||||
background: linear-gradient(-75deg, $accent-900, $accent-600)
|
||||
|
||||
.AppSidebar-Top
|
||||
width: 100%
|
||||
|
||||
.AppSidebar-Rule
|
||||
margin: 16px 12px 12px 12px
|
||||
border: none
|
||||
border-bottom: 2px solid white
|
||||
border-radius: 1px
|
||||
|
||||
.AppSidebar-Link
|
||||
width: 100%
|
||||
height: 72px
|
||||
padding: 12px
|
||||
display: block
|
||||
position: relative
|
||||
|
||||
img
|
||||
display: block
|
||||
width: 100%
|
||||
|
||||
&.Pixel
|
||||
padding: 12px 16px
|
||||
|
||||
outline: 0
|
||||
opacity: 0.5
|
||||
image-rendering: pixelated
|
||||
|
||||
transition: opacity 0.075s, background-color 0.075s
|
||||
|
||||
&:hover, &:focus-visible
|
||||
opacity: 0.85
|
||||
|
||||
&.Active
|
||||
opacity: 1
|
||||
background-color: transparentize(white, 0.9)
|
|
@ -0,0 +1,29 @@
|
|||
import * as Preact from 'preact';
|
||||
import { NavLink as Link } from 'react-router-dom';
|
||||
|
||||
import './AppSidebar.sass';
|
||||
|
||||
export default function AppHeader() {
|
||||
return (
|
||||
<div class='AppSidebar'>
|
||||
<nav>
|
||||
<Link className='AppSidebar-Link' to='/'><img role='heading' aria-level='1' aria-label='Virtual Dungeon'
|
||||
src='/app/static/logo_small.png' alt='Virtual Dungeon' /></Link>
|
||||
|
||||
<hr class='AppSidebar-Rule' />
|
||||
|
||||
<Link className='AppSidebar-Link Pixel' activeClassName='Active' exact to='/'>
|
||||
<img src='/app/static/ui/icon/home.png' alt='Home' /></Link>
|
||||
|
||||
<Link className='AppSidebar-Link Pixel' activeClassName='Active' to='/campaigns'>
|
||||
<img src='/app/static/ui/icon/campaign.png' alt='Campaigns' /></Link>
|
||||
|
||||
<Link className='AppSidebar-Link Pixel' activeClassName='Active' to='/assets'>
|
||||
<img src='/app/static/ui/icon/assets.png' alt='Assets' /></Link>
|
||||
</nav>
|
||||
|
||||
<Link className='AppSidebar-Link Pixel' activeClassName='Active' to='/settings'>
|
||||
<img src='/app/static/ui/icon/settings_large.png' alt='Settings' /></Link>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -23,12 +23,14 @@
|
|||
|
||||
&:disabled, &.Disabled
|
||||
pointer-events: none
|
||||
|
||||
.Button-Icon
|
||||
filter: brightness(45%) sepia(1) contrast(2.25) hue-rotate(185deg)
|
||||
|
||||
.Button-Label
|
||||
color: $accent-200
|
||||
&:disabled, &.Disabled, &.Inactive
|
||||
&:not(:active):not(:focus-visible)
|
||||
.Button-Icon
|
||||
filter: brightness(45%) sepia(1) contrast(2.25) hue-rotate(185deg)
|
||||
|
||||
.Button-Label
|
||||
color: $accent-200
|
||||
|
||||
.Button-Label
|
||||
display: inline-block
|
||||
|
|
|
@ -8,6 +8,7 @@ interface Props {
|
|||
icon?: string;
|
||||
alt?: string;
|
||||
|
||||
inactive?: boolean;
|
||||
disabled?: boolean;
|
||||
|
||||
to?: string;
|
||||
|
@ -27,7 +28,8 @@ export default function Button(props: Props) {
|
|||
class='Button-Icon' alt={props.alt ?? (props.label ? '' : undefined)} />;
|
||||
const label = props.label && <span class='Button-Label'>{props.label}</span>;
|
||||
|
||||
const classes = ('Button ' + (props.class ?? '') + (props.disabled ? ' Disabled' : '')).trim();
|
||||
const classes = ('Button ' + (props.class ?? '') + (props.disabled ? ' Disabled' : '') +
|
||||
(props.inactive ? ' Inactive' : '')).trim();
|
||||
|
||||
if (props.to) return <Link className={classes} style={props.style} to={props.disabled ? '' : props.to}
|
||||
tabIndex={props.disabled ? -1 : undefined}>{icon}{label}</Link>;
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
position: relative
|
||||
|
||||
@include slice.slice('button_group_' + $sect)
|
||||
|
||||
&:disabled, &.Disabled, &.Inactive
|
||||
@include slice.slice('button_group_' + $sect + '_disabled')
|
||||
z-index: 1
|
||||
|
||||
&:active, &:focus-visible
|
||||
@include slice.slice('button_group_' + $sect + '_active')
|
||||
z-index: 2
|
||||
|
||||
&:disabled, &.Disabled
|
||||
@include slice.slice('button_group_' + $sect + '_disabled')
|
||||
z-index: 1
|
||||
|
||||
%button_group_left
|
||||
@include button_group('left')
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.Editor
|
||||
position: relative
|
||||
overflow: hidden
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import * as Preact from 'preact';
|
||||
import type Phaser from 'phaser';
|
||||
import { useRef, useEffect } from 'preact/hooks';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
|
||||
import './Editor.sass';
|
||||
|
||||
import { ExternalData } from '../editor/EditorData';
|
||||
|
||||
// // Prevent scrolling hotkeys as the app implements its own scrolling.
|
||||
// document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
// if (e.ctrlKey
|
||||
|
@ -23,6 +26,8 @@ import './Editor.sass';
|
|||
export default function Editor() {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<Phaser.Game | null>(null);
|
||||
|
||||
const { campaign, map } = useParams<{ campaign: string; map: string }>();
|
||||
|
||||
/**
|
||||
* Lazy-load the editor, display it when ready,
|
||||
|
@ -33,7 +38,21 @@ export default function Editor() {
|
|||
let ignore = false;
|
||||
import('../editor/Main').then(({ default: create }) => {
|
||||
if (ignore || !rootRef.current) return;
|
||||
editorRef.current = create(rootRef.current);
|
||||
|
||||
const data: ExternalData = {
|
||||
campaign,
|
||||
map
|
||||
};
|
||||
|
||||
editorRef.current = create(rootRef.current, data);
|
||||
|
||||
const resizeCallback = () => {
|
||||
const { width, height } = rootRef.current.getBoundingClientRect();
|
||||
editorRef.current!.scale.resize(width, height);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeCallback);
|
||||
return () => window.removeEventListener('resize', resizeCallback);
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
@ -61,6 +80,6 @@ export default function Editor() {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={rootRef} class='Editor'></div>
|
||||
<div ref={rootRef} class='Editor' />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
@use '../../style/def' as *
|
||||
@use '../../style/text'
|
||||
@use '../../style/slice'
|
||||
|
||||
.CampaignDetails
|
||||
|
||||
.CampaignDetails-Split
|
||||
display: grid
|
||||
grid-gap: 12px
|
||||
padding-top: 6px
|
||||
grid-template-columns: 300px 1fr
|
||||
|
||||
.CampaignDetails-ImageWrap
|
||||
@extend .slice_highlight
|
||||
|
||||
display: grid
|
||||
height: 200px
|
||||
|
||||
.CampaignDetails-Image
|
||||
@include slice.slice_invert
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
display: block
|
||||
|
||||
object-fit: cover
|
||||
user-select: none
|
||||
pointer-events: none
|
||||
|
||||
.CampaignDetails-Right
|
||||
display: flex
|
||||
overflow: hidden
|
||||
flex-direction: column
|
||||
justify-content: space-between
|
||||
|
||||
.CampaignDetails-Details
|
||||
.CampaignDetails-Title
|
||||
@include text.line_clamp
|
||||
|
||||
margin-top: 0
|
||||
margin-bottom: 8px
|
||||
padding-right: 6px
|
||||
|
||||
font-weight: 500
|
||||
font-size: 34px
|
||||
font-family: $font-header
|
||||
|
||||
.CampaignDetails-Description
|
||||
margin-top: 0
|
||||
color: $accent-200
|
||||
|
||||
.CampaignDetails-Actions
|
||||
float: right
|
||||
|
||||
.CampaignDetails-CharactersWrap
|
||||
@extend .slice_highlight
|
||||
|
||||
height: 64px
|
||||
|
||||
.CampaignDetails-Characters
|
||||
@include slice.slice_invert
|
||||
|
||||
display: flex
|
||||
padding: 6px
|
||||
|
||||
.CampaignDetails-Character
|
||||
width: #{18px * 3}
|
||||
height: #{18px * 3}
|
||||
margin-right: 16px
|
||||
|
||||
background-size: #{18px * 3 * 2} #{18px * 3 * 2}
|
||||
background-position: top left
|
||||
image-rendering: pixelated
|
||||
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import * as Preact from 'preact';
|
||||
|
||||
import './CampaignDetails.sass';
|
||||
|
||||
// import Button from '../Button';
|
||||
// import ButtonGroup from '../ButtonGroup';
|
||||
|
||||
import MapList from './MapList';
|
||||
|
||||
import { Campaign } from '../../../../common/DBStructs';
|
||||
|
||||
interface Props {
|
||||
campaign: Campaign;
|
||||
}
|
||||
|
||||
export default function CampaignDetails({ campaign }: Props) {
|
||||
return (
|
||||
<div class='CampaignDetails'>
|
||||
<div class='CampaignDetails-Split'>
|
||||
<div class='CampaignDetails-ImageWrap'>
|
||||
<div class='CampaignDetails-Image'>
|
||||
<img src='https://placekitten.com/400/300' alt='' />
|
||||
</div>
|
||||
</div>
|
||||
<div class='CampaignDetails-Right'>
|
||||
<div class='CampaignDetails-Details'>
|
||||
{/* <div class='CampaignDetails-Actions'>
|
||||
<ButtonGroup>
|
||||
<Button to={`${campaign.identifier}/details`} icon='edit' alt='Edit'/>
|
||||
<Button to={`${campaign.identifier}/play`} icon='play' label='Start' disabled={campaign.maps.length === 0}/>
|
||||
</ButtonGroup>
|
||||
</div>*/}
|
||||
|
||||
<h2 class='CampaignDetails-Title'>{campaign.title || 'Untitled'}</h2>
|
||||
<p class='CampaignDetails-Description'>{campaign.description || 'Lorem ipsum dolor sit amet...'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MapList maps={campaign.maps} allowNew={true}/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
@use '../../style/grid'
|
||||
@use '../../style/slice'
|
||||
@use '../../style/text'
|
||||
|
||||
.CampaignList
|
||||
& > h2
|
||||
margin-top: 8px
|
||||
margin-bottom: 16px
|
||||
|
||||
font-size: 34px
|
||||
user-select: none
|
||||
|
||||
.CampaignList-Grid
|
||||
@include grid.auto_width(220px)
|
||||
|
||||
.CampaignList-CampaignWrap
|
||||
.CampaignList-Campaign
|
||||
@extend .slice_outline_button
|
||||
|
||||
display: grid
|
||||
user-select: none
|
||||
text-decoration: none
|
||||
|
||||
.CampaignList-CampaignInner
|
||||
@include slice.slice_invert
|
||||
|
||||
display: grid
|
||||
grid-template-rows: 120px auto
|
||||
|
||||
.CampaignList-CampaignPreview
|
||||
width: 100%
|
||||
height: 100%
|
||||
object-fit: cover
|
||||
user-select: none
|
||||
pointer-events: none
|
||||
|
||||
.CampaignList-CampaignTitle
|
||||
@include text.line_clamp
|
||||
|
||||
margin: 0
|
||||
padding: 9px 6px
|
||||
|
||||
.CampaignList-Actions
|
||||
display: flex
|
||||
margin-top: 12px
|
||||
justify-content: space-between
|
|
@ -1,63 +0,0 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { useAppData } from '../../Hooks';
|
||||
import { NavLink as Link } from 'react-router-dom';
|
||||
|
||||
import './CampaignList.sass';
|
||||
|
||||
import Button from '../Button';
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
|
||||
interface Props {
|
||||
allowNew?: boolean;
|
||||
}
|
||||
|
||||
export default function CampaignsCard({ allowNew }: Props) {
|
||||
const [ { campaigns } ] = useAppData('campaigns');
|
||||
|
||||
const [ page, setPage ] = useState<number>(0);
|
||||
|
||||
const currentCampaigns = (campaigns || []).slice(page * 4, (page + 1) * 4);
|
||||
|
||||
return (
|
||||
<div class='CampaignList'>
|
||||
<h2>Campaigns</h2>
|
||||
|
||||
{campaigns === undefined && <p>Loading Campaigns...</p>}
|
||||
{campaigns !== undefined &&
|
||||
<Preact.Fragment>
|
||||
<ul class='CampaignList-Grid'>
|
||||
{currentCampaigns.map(c => <li class='CampaignList-CampaignWrap'>
|
||||
<Link className='CampaignList-Campaign' to={`/${c.identifier}`}>
|
||||
<div class='CampaignList-CampaignInner'>
|
||||
<img class='CampaignList-CampaignPreview' src='https://placekitten.com/400/300' alt='' />
|
||||
<p class='CampaignList-CampaignTitle'>{c.title || 'Untitled'}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</li>)}
|
||||
</ul>
|
||||
<div class='CampaignList-Actions'>
|
||||
<Button icon='add' alt='New Campaign' to='/new' disabled={!allowNew} />
|
||||
|
||||
{(campaigns || []).length > 4 &&
|
||||
<ButtonGroup class='HomeView-Pagination'>
|
||||
<Button icon='nav_left' alt='Next Page'
|
||||
onClick={() => setPage(page - 1)} disabled={page === 0}/>
|
||||
|
||||
{(() => {
|
||||
let elems: Preact.VNode[] = [];
|
||||
for (let i = 0; i < campaigns.length / 4; i++) elems.push(
|
||||
<Button label={(i + 1).toString()} onClick={() => setPage(i)} disabled={page === i} />);
|
||||
return elems;
|
||||
})()}
|
||||
|
||||
<Button icon='nav_right' alt='Next Page'
|
||||
onClick={() => setPage(page + 1)} disabled={page + 1 >= campaigns.length / 4} />
|
||||
</ButtonGroup>
|
||||
}
|
||||
</div>
|
||||
</Preact.Fragment>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
@use '../../style/def' as *
|
||||
@use '../../style/text'
|
||||
@use '../../style/slice'
|
||||
|
||||
.CampaignOverview
|
||||
display: grid
|
||||
grid-gap: 12px
|
||||
padding-top: 6px
|
||||
grid-template-columns: 300px 1fr
|
||||
|
||||
.CampaignOverview-ImageWrap
|
||||
@extend .slice_highlight
|
||||
|
||||
display: grid
|
||||
height: 200px
|
||||
|
||||
.CampaignOverview-Image
|
||||
@include slice.slice_invert
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
display: block
|
||||
|
||||
object-fit: cover
|
||||
user-select: none
|
||||
pointer-events: none
|
||||
|
||||
.CampaignOverview-Right
|
||||
display: flex
|
||||
overflow: hidden
|
||||
flex-direction: column
|
||||
justify-content: space-between
|
||||
|
||||
.CampaignOverview-Details
|
||||
.CampaignOverview-Title
|
||||
@include text.line_clamp
|
||||
|
||||
margin-top: 0
|
||||
margin-bottom: 8px
|
||||
padding-right: 6px
|
||||
|
||||
font-weight: 500
|
||||
font-size: 34px
|
||||
font-family: $font-header
|
||||
|
||||
.CampaignOverview-Description
|
||||
margin-top: 0
|
||||
color: $accent-200
|
||||
|
||||
.CampaignOverview-Actions
|
||||
float: right
|
||||
|
||||
.CampaignOverview-CharactersWrap
|
||||
@extend .slice_highlight
|
||||
|
||||
height: 64px
|
||||
|
||||
.CampaignOverview-Characters
|
||||
@include slice.slice_invert
|
||||
|
||||
display: flex
|
||||
padding: 6px
|
||||
|
||||
.CampaignOverview-Character
|
||||
width: #{18px * 3}
|
||||
height: #{18px * 3}
|
||||
margin-right: 16px
|
||||
|
||||
background-size: #{18px * 3 * 2} #{18px * 3 * 2}
|
||||
background-position: top left
|
||||
image-rendering: pixelated
|
|
@ -1,46 +0,0 @@
|
|||
import * as Preact from 'preact';
|
||||
|
||||
import './CampaignOverview.sass';
|
||||
|
||||
import Button from '../Button';
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
|
||||
import { Campaign } from '../../../../common/DBStructs';
|
||||
|
||||
interface Props {
|
||||
campaign: Campaign;
|
||||
}
|
||||
|
||||
export default function CampaignOverview({ campaign }: Props) {
|
||||
return (
|
||||
<div class='CampaignOverview'>
|
||||
<div class='CampaignOverview-ImageWrap'>
|
||||
<div class='CampaignOverview-Image'>
|
||||
<img src='https://placekitten.com/400/300' alt='' />
|
||||
</div>
|
||||
</div>
|
||||
<div class='CampaignOverview-Right'>
|
||||
<div class='CampaignOverview-Details'>
|
||||
<div class='CampaignOverview-Actions'>
|
||||
<ButtonGroup>
|
||||
<Button to={`${campaign.identifier}/details`} icon='edit' alt='Edit'/>
|
||||
<Button to={`${campaign.identifier}/play`} icon='play' label='Start' disabled={campaign.maps.length === 0}/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
|
||||
<h3 class='CampaignOverview-Title'>{campaign.title || 'Untitled'}</h3>
|
||||
<p class='CampaignOverview-Description'>{campaign.description || 'Lorem ipsum dolor sit amet...'}</p>
|
||||
</div>
|
||||
<div class='CampaignOverview-CharactersWrap'>
|
||||
<div class='CampaignOverview-Characters'>
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/baby_blue_dragon.png)' }} />
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/cadin_1.png)' }} />
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/dragonfolk_1.png)' }} />
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/druid_male.png)' }} />
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/naexi_human_yklwa.png)' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
@use '../../style/grid'
|
||||
@use '../../style/slice'
|
||||
@use '../../style/text'
|
||||
|
||||
.MapList
|
||||
& > h2
|
||||
margin-top: 8px
|
||||
margin-bottom: 16px
|
||||
|
||||
font-size: 34px
|
||||
user-select: none
|
||||
|
||||
.MapList-Grid
|
||||
@include grid.auto_width(220px)
|
||||
|
||||
.MapList-MapWrap
|
||||
.MapList-Map
|
||||
@extend .slice_outline_button
|
||||
|
||||
display: grid
|
||||
user-select: none
|
||||
text-decoration: none
|
||||
|
||||
.MapList-MapInner
|
||||
@include slice.slice_invert
|
||||
|
||||
display: grid
|
||||
grid-template-rows: 120px auto
|
||||
|
||||
.MapList-MapPreview
|
||||
width: 100%
|
||||
height: 100%
|
||||
object-fit: cover
|
||||
user-select: none
|
||||
pointer-events: none
|
||||
|
||||
.MapList-MapTitle
|
||||
@include text.line_clamp
|
||||
|
||||
margin: 0
|
||||
padding: 9px 6px
|
||||
|
||||
.MapList-Actions
|
||||
display: flex
|
||||
margin-top: 12px
|
||||
justify-content: space-between
|
|
@ -1,63 +0,0 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { NavLink as Link, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import './MapList.sass';
|
||||
|
||||
import Button from '../Button';
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
|
||||
import { Map } from '../../../../common/DBStructs';
|
||||
|
||||
const ITEMS_PER_PAGE = 8;
|
||||
|
||||
interface Props {
|
||||
maps: Map[];
|
||||
allowNew?: boolean;
|
||||
}
|
||||
|
||||
export default function CampaignsCard({ maps, allowNew }: Props) {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const [ page, setPage ] = useState<number>(0);
|
||||
|
||||
const currentMaps = maps.slice(page * ITEMS_PER_PAGE, (page + 1) * ITEMS_PER_PAGE);
|
||||
|
||||
return (
|
||||
<div class='MapList'>
|
||||
<h3>Maps</h3>
|
||||
|
||||
<ul class='MapList-Grid'>
|
||||
{currentMaps.map(m => <li class='MapList-MapWrap'>
|
||||
<Link className='MapList-Map' to={`/${m.identifier}`}>
|
||||
<div class='MapList-MapInner'>
|
||||
<img class='MapList-MapPreview' src='https://placekitten.com/400/300' alt='' />
|
||||
<p class='MapList-MapTitle'>{m.name || 'Untitled'}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</li>)}
|
||||
</ul>
|
||||
|
||||
<div class='MapList-Actions'>
|
||||
<Button icon='add' alt='New Map' to={`${match.url}/new`} disabled={!allowNew} />
|
||||
|
||||
{maps.length > ITEMS_PER_PAGE &&
|
||||
<ButtonGroup class='HomeView-Pagination'>
|
||||
<Button icon='nav_left' alt='Next Page'
|
||||
onClick={() => setPage(page - 1)} disabled={page === 0}/>
|
||||
|
||||
{(() => {
|
||||
let elems: Preact.VNode[] = [];
|
||||
for (let i = 0; i < maps.length / ITEMS_PER_PAGE; i++) elems.push(
|
||||
<Button label={(i + 1).toString()} onClick={() => setPage(i)} disabled={page === i} />);
|
||||
return elems;
|
||||
})()}
|
||||
|
||||
<Button icon='nav_right' alt='Next Page'
|
||||
onClick={() => setPage(page + 1)} disabled={page + 1 >= maps.length / ITEMS_PER_PAGE} />
|
||||
</ButtonGroup>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
@use '../../style/def' as *
|
||||
|
||||
.NewCampaignForm
|
||||
label
|
||||
span
|
||||
display: inline-block
|
||||
margin-top: 6px
|
||||
|
||||
color: $accent-100
|
||||
|
||||
input, textarea
|
||||
width: 100%
|
||||
padding: 6px
|
||||
display: block
|
||||
margin: 6px 0
|
||||
|
||||
outline: 0
|
||||
border: 3px solid $accent-500
|
||||
background-color: transparent
|
||||
|
||||
&::placeholder
|
||||
color: $accent-400
|
||||
|
||||
&:focus, &:focus-within
|
||||
border-color: $accent-300
|
||||
|
||||
.NewCampaignForm-Title
|
||||
font-family: $font-header
|
||||
font-size: 27px
|
||||
|
||||
.NewCampaignForm-Submit
|
||||
margin-top: 12px
|
|
@ -0,0 +1,85 @@
|
|||
@import '../../partial/Ext'
|
||||
|
||||
.ColorPicker
|
||||
width: 300px
|
||||
height: 200px
|
||||
display: grid
|
||||
user-select: none
|
||||
grid-template-rows: 1fr 12px 28px
|
||||
|
||||
border-radius: 8px
|
||||
background-color: white
|
||||
box-shadow: 0px 2px 8px 0px transparentize($neutral-950, 0.9)
|
||||
|
||||
&.Write
|
||||
height: 220px
|
||||
grid-template-rows: 1fr 12px 28px 44px
|
||||
|
||||
&.Absolute
|
||||
z-index: 5
|
||||
position: absolute
|
||||
transform: translateX(-50%)
|
||||
box-shadow: 0px 2px 12px 0px transparentize($neutral-950, 0.8)
|
||||
|
||||
.ColorPicker-Separator
|
||||
background-color: #000
|
||||
|
||||
.ColorPicker-SatVal
|
||||
@extend %center_wrap
|
||||
position: relative
|
||||
|
||||
cursor: pointer
|
||||
|
||||
border-radius: 8px 8px 0 0
|
||||
background-image: linear-gradient(0deg, #000, transparent), linear-gradient(90deg, #fff, hsla(0, 0%, 100%, 0))
|
||||
|
||||
.ColorPicker-Hex
|
||||
margin: 0
|
||||
margin-top: 16px
|
||||
|
||||
font-size: 24px
|
||||
font-family: monospace
|
||||
color: rgba(255, 255, 255, 0.6)
|
||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.2)
|
||||
|
||||
|
||||
.ColorPicker-Hue
|
||||
position: relative
|
||||
|
||||
cursor: pointer
|
||||
|
||||
border-radius: 0 0 8px 8px
|
||||
background-image: linear-gradient(90deg, red 0, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, red)
|
||||
|
||||
.ColorPicker-Indicator
|
||||
top: 50%
|
||||
|
||||
.ColorPicker-Indicator
|
||||
position: absolute
|
||||
z-index: 1
|
||||
width: 24px
|
||||
height: 24px
|
||||
|
||||
border-radius: 50%
|
||||
border: 2px solid white
|
||||
transform: translate(-50%, -50%)
|
||||
box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.4)
|
||||
|
||||
|
||||
.ColorPicker-Details
|
||||
display: grid
|
||||
padding: 8px
|
||||
grid-gap: 8px
|
||||
grid-template-columns: 32px 1fr
|
||||
|
||||
.ColorPicker-ColorBlock
|
||||
@extend %material_border
|
||||
border-color: $neutral-200
|
||||
border-radius: 4px
|
||||
|
||||
.ColorPicker-ColorInput
|
||||
@extend %material_input
|
||||
|
||||
padding-top: 3px
|
||||
padding-left: 6px
|
||||
font-family: monospace
|
|
@ -0,0 +1,105 @@
|
|||
// import * as Preact from 'preact';
|
||||
// import { Color } from 'auriserve-api';
|
||||
// import { forwardRef } from 'preact/compat';
|
||||
// import { useEffect, useRef } from 'preact/hooks';
|
||||
|
||||
// import './ColorPicker.sass';
|
||||
|
||||
// import { WidgetProps } from './Input';
|
||||
|
||||
// interface Props {
|
||||
// parent?: HTMLElement;
|
||||
// writable?: boolean;
|
||||
// displayHex?: boolean;
|
||||
// }
|
||||
|
||||
// const ColorPicker = forwardRef<HTMLDivElement, WidgetProps & Props>((props, ref) => {
|
||||
// const color = props.value ? typeof props.value === 'string' ? Color.hexToHSV(props.value) : props.value : { h: 0, s: 0, v : 0};
|
||||
|
||||
// const mouseTarget = useRef<string>('');
|
||||
// const satValElem = useRef<HTMLDivElement>(null);
|
||||
// const hueElem = useRef<HTMLDivElement>(null);
|
||||
|
||||
// const inputHex = (evt: any) => {
|
||||
// const val = evt.target.value;
|
||||
// if (val.length !== 7) return;
|
||||
// props.setValue(Color.hexToHSV(val));
|
||||
// };
|
||||
|
||||
// const handleHueMove = (evt: MouseEvent) => {
|
||||
// const bounds = hueElem.current.getBoundingClientRect();
|
||||
// const hue = Math.max(Math.min((evt.clientX - bounds.left) / bounds.width, 1), 0);
|
||||
// props.setValue({ ...color, h: hue });
|
||||
// };
|
||||
|
||||
// const handleSatValMove = (evt: MouseEvent) => {
|
||||
// const bounds = satValElem.current.getBoundingClientRect();
|
||||
// const sat = Math.max(Math.min((evt.clientX - bounds.left) / bounds.width, 1), 0);
|
||||
// const val = Math.max(Math.min((bounds.bottom - evt.clientY) / bounds.height, 1), 0);
|
||||
// props.setValue({ ...color, s: sat, v: val });
|
||||
// };
|
||||
|
||||
// const handleMouseMove = (evt: MouseEvent) => {
|
||||
// switch (mouseTarget.current) {
|
||||
// default: return;
|
||||
// case 'hue': return handleHueMove(evt);
|
||||
// case 'satval': return handleSatValMove(evt);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleMouseClick = (evt: MouseEvent, target: string) => {
|
||||
// evt.stopImmediatePropagation();
|
||||
// evt.preventDefault();
|
||||
|
||||
// mouseTarget.current = target;
|
||||
// handleMouseMove(evt);
|
||||
// };
|
||||
|
||||
// useEffect(() => {
|
||||
// const clearMouse = () => mouseTarget.current = '';
|
||||
// document.body.addEventListener('mouseup', clearMouse);
|
||||
// document.body.addEventListener('mousemove', handleMouseMove);
|
||||
// return () => {
|
||||
// document.body.removeEventListener('mouseup', clearMouse);
|
||||
// document.body.removeEventListener('mousemove', handleMouseMove);
|
||||
// };
|
||||
// }, [ handleMouseMove ]);
|
||||
|
||||
// const hueHex = Color.HSVToHex({ h: color.h, s: 1, v: 1 });
|
||||
// const fullHex = Color.HSVToHex(color);
|
||||
|
||||
// const style: any = {};
|
||||
// if (props.parent) {
|
||||
// style.top = props.parent.getBoundingClientRect().bottom + 'px';
|
||||
// style.left = ((props.parent.getBoundingClientRect().left +
|
||||
// props.parent.getBoundingClientRect().right) / 2) + 'px';
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <div class={('ColorPicker ' + (props.writable ? 'Write ' : '' + (props.parent ? 'Absolute' : ''))).trim()}
|
||||
// ref={ref} style={style}>
|
||||
|
||||
// <div class='ColorPicker-SatVal' ref={satValElem}
|
||||
// onMouseDown={(evt) => handleMouseClick(evt, 'satval')}
|
||||
// style={{ backgroundColor: hueHex }}>
|
||||
|
||||
// {props.displayHex && <p class='ColorPicker-Hex'>{fullHex}</p>}
|
||||
|
||||
// <div class='ColorPicker-Indicator' style={{ left: (color.s * 100) + '%',
|
||||
// top: ((1 - color.v) * 100) + '%', backgroundColor: fullHex }} />
|
||||
|
||||
// </div>
|
||||
// <div class='ColorPicker-Separator' />
|
||||
// <div class='ColorPicker-Hue' ref={hueElem}
|
||||
// onMouseDown={(evt) => handleMouseClick(evt, 'hue')}>
|
||||
// <div class='ColorPicker-Indicator' style={{ left: (color.h * 100) + '%', backgroundColor: hueHex }} />
|
||||
// </div>
|
||||
// {props.writable && <div class='ColorPicker-Details'>
|
||||
// <div class='ColorPicker-ColorBlock' style={{ backgroundColor: fullHex }} />
|
||||
// <input class='ColorPicker-ColorInput' value={fullHex} onChange={inputHex} onInput={inputHex} maxLength={7} />
|
||||
// </div>}
|
||||
// </div>
|
||||
// );
|
||||
// });
|
||||
|
||||
// export default ColorPicker;
|
|
@ -0,0 +1,86 @@
|
|||
@use '../../style/def' as *
|
||||
|
||||
.DatePicker
|
||||
width: 280px
|
||||
height: 250px
|
||||
display: grid
|
||||
grid-template-rows: 48px 1fr
|
||||
user-select: none
|
||||
|
||||
overflow: hidden
|
||||
border-radius: 8px
|
||||
background-color: $neutral-000
|
||||
box-shadow: 0px 2px 8px 0px transparentize($neutral-950, 0.9)
|
||||
|
||||
&.Absolute
|
||||
z-index: 5
|
||||
position: absolute
|
||||
transform: translateX(-50%)
|
||||
box-shadow: 0px 2px 12px 0px transparentize($neutral-950, 0.8)
|
||||
|
||||
.DatePicker-Header
|
||||
display: grid
|
||||
grid-template-columns: 32px 32px 1fr 32px
|
||||
grid-gap: 4px
|
||||
|
||||
text-align: center
|
||||
border-bottom: 1px solid $neutral-100
|
||||
padding: 8px
|
||||
|
||||
button
|
||||
@extend %material_button
|
||||
|
||||
height: 32px
|
||||
border-color: $neutral-100
|
||||
|
||||
p
|
||||
margin: 0
|
||||
padding: 4px 0
|
||||
line-height: 1.6
|
||||
color: $neutral-600
|
||||
|
||||
.DatePicker-Grid
|
||||
display: grid
|
||||
grid-template-columns: repeat(7, 1fr)
|
||||
padding: 6px
|
||||
|
||||
.DatePicker-Date
|
||||
@extend %material_button
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
color: $neutral-500
|
||||
background-color: $neutral-050
|
||||
border-radius: 0
|
||||
|
||||
transition: background-color $t-fast
|
||||
|
||||
&:hover, &:focus
|
||||
border-radius: 4px
|
||||
|
||||
&:nth-of-type(1)
|
||||
border-top-left-radius: 4px
|
||||
|
||||
&:nth-of-type(7)
|
||||
border-top-right-radius: 4px
|
||||
|
||||
&:nth-last-of-type(1)
|
||||
border-bottom-right-radius: 4px
|
||||
|
||||
&:nth-last-of-type(7)
|
||||
border-bottom-left-radius: 4px
|
||||
|
||||
&.CurrentMonth
|
||||
border-radius: 4px
|
||||
color: $neutral-700
|
||||
background-color: $neutral-000
|
||||
|
||||
&.CurrentDay
|
||||
color: $accent-800
|
||||
background-color: $accent-100
|
||||
|
||||
&.Selected
|
||||
background-color: $neutral-500
|
||||
border-color: $neutral-500
|
||||
color: $neutral-000
|
|
@ -0,0 +1,112 @@
|
|||
import * as Preact from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
import './DatePicker.sass';
|
||||
|
||||
import { WidgetProps } from './Input';
|
||||
|
||||
interface Props {
|
||||
parent?: HTMLElement;
|
||||
writable?: boolean;
|
||||
displayHex?: boolean;
|
||||
}
|
||||
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
|
||||
const DatePicker = forwardRef<HTMLDivElement, WidgetProps & Props>((props, ref) => {
|
||||
const [ month, setMonth ] = useState<number>(() => new Date().getMonth());
|
||||
const [ year, setYear ] = useState<number>(() => new Date().getFullYear());
|
||||
|
||||
useEffect(() => {
|
||||
setMonth(props.value.getMonth());
|
||||
setYear(props.value.getFullYear());
|
||||
}, [ props.value ]);
|
||||
|
||||
const handleNavigate = (months: number): void => {
|
||||
setMonth((((month + months) % 12) + 12) % 12);
|
||||
setYear(year + Math.floor((month + months) / 12));
|
||||
};
|
||||
|
||||
const handleSetNow = (): void => {
|
||||
const date = new Date(props.value.getTime());
|
||||
const now = new Date();
|
||||
date.setFullYear(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
props.setValue(date);
|
||||
};
|
||||
|
||||
const setDate = (day: number, month: number, year: number) => {
|
||||
const date = new Date(props.value.getTime());
|
||||
date.setFullYear(year, month, day);
|
||||
props.setValue(date);
|
||||
};
|
||||
|
||||
const style: any = {};
|
||||
if (props.parent) {
|
||||
style.top = props.parent.getBoundingClientRect().bottom + 'px';
|
||||
style.left = ((props.parent.getBoundingClientRect().left +
|
||||
props.parent.getBoundingClientRect().right) / 2) + 'px';
|
||||
}
|
||||
|
||||
let monthDate = new Date();
|
||||
monthDate.setMonth(month, 1);
|
||||
monthDate.setFullYear(year);
|
||||
|
||||
let days: Preact.VNode[] = [];
|
||||
|
||||
const firstDayOfMonth = monthDate.getDay();
|
||||
for (let i = 1; true; i += 7) {
|
||||
|
||||
// Create a date at Sunday of the `i`th week shown on the calendar.
|
||||
const day = i - firstDayOfMonth;
|
||||
let date = new Date();
|
||||
date.setMonth(month, 1);
|
||||
date.setFullYear(year);
|
||||
date.setDate(day);
|
||||
|
||||
// Break when we go past the last week of the month.
|
||||
if ((date.getMonth() > month && date.getFullYear() >= year) || date.getFullYear() > year) break;
|
||||
|
||||
const currentDate = new Date();
|
||||
|
||||
for (let j = 0; j < 7; j++) {
|
||||
const isCurrentMonth = date.getMonth() === month;
|
||||
|
||||
const isCurrentDate =
|
||||
date.getDate() === currentDate.getDate() &&
|
||||
date.getMonth() === currentDate.getMonth() &&
|
||||
date.getFullYear() === currentDate.getFullYear();
|
||||
|
||||
const isSelectedDate =
|
||||
date.getDate() === props.value.getDate() &&
|
||||
date.getMonth() === props.value.getMonth() &&
|
||||
date.getFullYear() === props.value.getFullYear();
|
||||
|
||||
const dayClasses = 'DatePicker-Date ' +
|
||||
(isCurrentDate ? ' CurrentDay' : '') +
|
||||
(isCurrentMonth ? ' CurrentMonth' : '') +
|
||||
(isSelectedDate ? ' Selected' : '');
|
||||
|
||||
const rep: [ number, number, number ] = [ date.getDate(), date.getMonth(), date.getFullYear() ];
|
||||
days.push(<button tabIndex={-1} class={dayClasses} onClick={() => setDate(...rep)}>{date.getDate()}</button>);
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={('DatePicker ' + (props.parent ? 'Absolute' : '')).trim()}
|
||||
ref={ref} style={style}>
|
||||
<div class='DatePicker-Header'>
|
||||
<button tabIndex={-1} onClick={() => handleNavigate(-1)}><</button>
|
||||
<button tabIndex={-1} onClick={() => handleSetNow()}>🕒</button>
|
||||
<p>{monthNames[month]} {year}</p>
|
||||
<button tabIndex={-1} onClick={() => handleNavigate(1)}>></button>
|
||||
</div>
|
||||
<div class='DatePicker-Grid'>{days}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default DatePicker;
|
|
@ -0,0 +1,23 @@
|
|||
export interface WidgetProps {
|
||||
value: any;
|
||||
setValue: (newValue: any) => any;
|
||||
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
|
||||
onFocusChange?: (focus: boolean) => any;
|
||||
|
||||
style?: any;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export { default as Label } from './InputLabel';
|
||||
export { default as Divider } from './InputDivider';
|
||||
export { default as Annotation } from './InputAnnotation';
|
||||
|
||||
export { default as Text } from './fields/InputText';
|
||||
export { default as Color } from './fields/InputColor';
|
||||
// export { default as Select } from './fields/InputSelect';
|
||||
export { default as Numeric } from './fields/InputNumeric';
|
||||
export { default as Checkbox } from './fields/InputCheckbox';
|
||||
export { default as DateTime } from './fields/InputDateTime';
|
|
@ -0,0 +1,29 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.InputAnnotation
|
||||
display: block
|
||||
position: relative
|
||||
|
||||
overflow: auto
|
||||
user-select: none
|
||||
|
||||
.InputAnnotation-Title
|
||||
padding-right: 3em
|
||||
margin: 16px 0 8px 0
|
||||
cursor: pointer
|
||||
|
||||
.InputAnnotation-Description
|
||||
margin: 8px 0
|
||||
|
||||
font-size: 14px
|
||||
font-weight: 400
|
||||
line-height: 1.4
|
||||
color: $neutral-500
|
||||
|
||||
.InputCheckbox
|
||||
@extend %material_checkbox
|
||||
|
||||
position: absolute
|
||||
top: 14px
|
||||
right: 0
|
|
@ -0,0 +1,21 @@
|
|||
import * as Preact from 'preact';
|
||||
|
||||
import './InputAnnotation.sass';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
|
||||
children?: Preact.ComponentChildren;
|
||||
}
|
||||
|
||||
export default function InputAnnotation(props: Props) {
|
||||
return (
|
||||
<label class='InputAnnotation'>
|
||||
<p class='InputAnnotation-Title'>{props.title}</p>
|
||||
{props.description && <p class='InputAnnotation-Description'>{props.description}</p>}
|
||||
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
@use '../../style/def' as *
|
||||
|
||||
.InputDivider
|
||||
border: none
|
||||
border-bottom: 1px solid $neutral-300
|
||||
margin: 20px 0 16px 0
|
||||
padding: 0
|
|
@ -0,0 +1,9 @@
|
|||
import * as Preact from 'preact';
|
||||
|
||||
import './InputDivider.sass';
|
||||
|
||||
export default function InputLabel() {
|
||||
return (
|
||||
<hr class='InputDivider' />
|
||||
);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/def' as *
|
||||
@use '../../style/text'
|
||||
|
||||
.InputLabel
|
||||
display: block
|
||||
overflow: auto
|
||||
position: relative
|
||||
|
||||
.InputLabel-Label
|
||||
@include text.line_clamp
|
||||
@extend %material_label
|
||||
margin: 0
|
|
@ -0,0 +1,20 @@
|
|||
import * as Preact from 'preact';
|
||||
|
||||
import './InputLabel.sass';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
|
||||
class?: string;
|
||||
style?: any;
|
||||
children?: Preact.ComponentChildren;
|
||||
}
|
||||
|
||||
export default function InputLabel(props: Props) {
|
||||
return (
|
||||
<label class={('InputLabel ' + (props.class ?? '')).trim()} style={props.style}>
|
||||
<p class='InputLabel-Label'>{props.label}</p>
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
@import "../../partial/Ext"
|
||||
|
||||
.SearchableOptionPicker
|
||||
z-index: 5
|
||||
overflow: auto
|
||||
position: absolute
|
||||
height: min-content
|
||||
min-height: 48px
|
||||
max-height: 300px
|
||||
|
||||
border-radius: 4px
|
||||
background-color: $neutral-000
|
||||
border: 1px solid $neutral-100
|
||||
// box-shadow: 0px 2px 12px 0px transparentize($neutral-950, 0.8)
|
||||
|
||||
.SearchableOptionPicker-Empty
|
||||
padding: 15px 8px 14px 8px
|
||||
|
||||
text-align: center
|
||||
background-color: $neutral-050
|
||||
|
||||
p
|
||||
margin: 0
|
||||
color: $neutral-400
|
||||
font-weight: normal
|
||||
|
||||
.InputLabel-Label
|
||||
padding-left: 8px
|
||||
|
||||
.SearchableOptionPicker-Items
|
||||
list-style-type: none
|
||||
padding: 0
|
||||
margin: 0
|
||||
|
||||
.SearchableOptionPicker-Item
|
||||
display: grid
|
||||
min-height: 48px
|
||||
padding: 0
|
||||
margin: 0
|
||||
|
||||
&.Focused
|
||||
background-color: mix($neutral-050, $neutral-100, 50%)
|
||||
|
||||
&:hover
|
||||
background-color: mix($neutral-050, $neutral-100, 25%)
|
||||
|
||||
.SearchableOptionPicker-Button
|
||||
display: grid
|
||||
padding: 0
|
||||
margin: 0
|
||||
background-color: transparent
|
||||
border: none
|
||||
|
||||
.SearchableOptionPicker-DefaultItem
|
||||
display: flex
|
||||
padding: 8px
|
||||
height: 100%
|
||||
align-items: center
|
||||
|
||||
border: none
|
||||
cursor: pointer
|
||||
text-align: left
|
||||
background-color: transparent
|
||||
|
||||
p
|
||||
margin: 0
|
||||
font-weight: normal
|
|
@ -0,0 +1,88 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useState, useEffect, useMemo } from 'preact/hooks';
|
||||
|
||||
import './SearchableOptionPicker.sass';
|
||||
|
||||
import Label from './InputLabel';
|
||||
|
||||
interface SearchProps {
|
||||
parent: HTMLElement;
|
||||
query: string;
|
||||
multi?: boolean;
|
||||
|
||||
options: string[] | {[key: string]: string };
|
||||
renderOption?: Preact.FunctionalComponent<{ option: string; ind: number; value?: any }>;
|
||||
|
||||
setSelected: (identifier: string | string[]) => any;
|
||||
}
|
||||
|
||||
function sortOptions(query: string, a: string, b: string) {
|
||||
let off = a.indexOf(query) - b.indexOf(query);
|
||||
if (off < 0) return -1;
|
||||
if (off > 0) return 1;
|
||||
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
||||
}
|
||||
|
||||
export default function SearchableOptionPicker(props: SearchProps) {
|
||||
const [ index, setIndex ] = useState<number>(0);
|
||||
|
||||
const optionsArray = useMemo(() => Array.isArray(props.options) ?
|
||||
props.options : Object.keys(props.options), [ props.options ]);
|
||||
|
||||
const query = props.query.toLowerCase().trim();
|
||||
const filteredOptions = optionsArray.filter(o => o.toLowerCase().includes(query))
|
||||
.sort((a, b) => sortOptions(query, a, b)).filter((_, i) => i < 5);
|
||||
|
||||
useEffect(() => setIndex(0), [ query ]);
|
||||
|
||||
// Interactions
|
||||
|
||||
useEffect(() => {
|
||||
const handleArrowSelect = (evt: KeyboardEvent) => {
|
||||
if (evt.key !== 'ArrowUp' && evt.key !== 'ArrowDown' && evt.key !== 'Enter') return;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (filteredOptions.length === 0) return;
|
||||
if (evt.key === 'ArrowUp') setIndex((index) => (index <= 0 ? filteredOptions.length : index) - 1);
|
||||
else if (evt.key === 'ArrowDown') setIndex((index) => (index + 1) % filteredOptions.length);
|
||||
else if (evt.key === 'Enter') props.setSelected(filteredOptions[index]);
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleArrowSelect);
|
||||
return () => window.removeEventListener('keydown', handleArrowSelect);
|
||||
});
|
||||
|
||||
const style: any = {
|
||||
top: props.parent.getBoundingClientRect().bottom + 'px',
|
||||
left: (props.parent.getBoundingClientRect().left +
|
||||
props.parent.getBoundingClientRect().width / 2) + 'px',
|
||||
width: props.parent.getBoundingClientRect().width + 'px'
|
||||
};
|
||||
|
||||
return (
|
||||
<div class='SearchableOptionPicker' style={style} onMouseDown={evt => { evt.preventDefault(); evt.stopPropagation(); }}>
|
||||
{query && <Label label={`Search results for '${query}'`} />}
|
||||
<ul class='SearchableOptionPicker-Items'>
|
||||
{filteredOptions.map((option, ind) => <li
|
||||
class={('SearchableOptionPicker-Item ' + (index === ind ? 'Focused' : '')).trim()}
|
||||
key={ind} onClick={() => props.setSelected(option)}>
|
||||
<button class='SearchableOptionPicker-Button'>
|
||||
{props.renderOption ? props.renderOption({ option, ind,
|
||||
value: !Array.isArray(props.options) && props.options[option] }) :
|
||||
<span class='SearchableOptionPicker-DefaultItem'>
|
||||
{Array.isArray(props.options) ? option : props.options[option] || option}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</li>)}
|
||||
{filteredOptions.length === 0 &&
|
||||
<div class='SearchableOptionPicker-Empty'>
|
||||
<p>No results found.</p>
|
||||
</div>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
@use '../../../style/ext'
|
||||
|
||||
.InputCheckbox
|
||||
@extend %material_checkbox
|
||||
|
||||
&.AlignRight
|
||||
position: absolute
|
||||
top: 16px
|
||||
right: 1px
|
|
@ -0,0 +1,33 @@
|
|||
import * as Preact from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
|
||||
import './InputCheckbox.sass';
|
||||
|
||||
import { WidgetProps } from '../Input';
|
||||
|
||||
interface Props extends WidgetProps {
|
||||
alignRight?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A two-state checkbox input widget.
|
||||
*/
|
||||
|
||||
const InputCheckbox = forwardRef<HTMLInputElement, Props>((props, fRef) => {
|
||||
const cb = () => props.setValue(!props.value);
|
||||
|
||||
return (
|
||||
<input
|
||||
class={('InputCheckbox ' + (props.alignRight ? 'AlignRight ' : '') + (props.class ?? '')).trim()}
|
||||
style={props.style}
|
||||
|
||||
checked={props.value}
|
||||
onChange={cb}
|
||||
|
||||
ref={fRef as any}
|
||||
type='checkbox'
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default InputCheckbox;
|
|
@ -0,0 +1,61 @@
|
|||
@use '../../../style/ext'
|
||||
@use '../../../style/def' as *
|
||||
|
||||
.InputColor
|
||||
position: relative
|
||||
|
||||
.InputText
|
||||
font-family: monospace
|
||||
padding-left: 48px
|
||||
|
||||
.InputColor-ColorIndicator
|
||||
position: absolute
|
||||
display: block
|
||||
top: 8px
|
||||
bottom: 8px
|
||||
left: 8px
|
||||
width: 32px
|
||||
|
||||
border-radius: 4px
|
||||
pointer-events: none
|
||||
box-shadow: 0px 1px 4px 0px transparentize($neutral-950, 0.7)
|
||||
|
||||
&.Full
|
||||
.InputText
|
||||
position: absolute
|
||||
opacity: 0
|
||||
font-size: 0
|
||||
cursor: pointer
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
|
||||
.InputColor-ColorIndicator
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
width: unset
|
||||
box-shadow: none
|
||||
|
||||
.InputColor-ColorPickerWrap
|
||||
position: fixed
|
||||
z-index: 5
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
.ColorPicker.ColorPicker
|
||||
transform: translate(-50%, 0px) scale(0.95)
|
||||
opacity: 0
|
||||
|
||||
transition: opacity $t-ufast, transform $t-ufast
|
||||
|
||||
&.Animate-enter-active, &.Animate-enter-done
|
||||
.ColorPicker.ColorPicker
|
||||
opacity: 1
|
||||
transform: translate(-50%, 12px) scale(1)
|
||||
|
||||
&.Animate-enter-active:not(.Animate-enter-done), &.Animate-exit-active:not(.Animate-exit-done)
|
||||
.ColorPicker
|
||||
will-change: transform
|
|
@ -0,0 +1,44 @@
|
|||
import * as Preact from 'preact';
|
||||
// import { Color } from 'auriserve-api';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useState, useRef } from 'preact/hooks';
|
||||
// import { usePopupCancel } from '../../../Hooks';
|
||||
|
||||
import './InputColor.sass';
|
||||
|
||||
// import Popup from '../../Popup';
|
||||
// import InputText from './InputText';
|
||||
// import ColorPicker from '../ColorPicker';
|
||||
|
||||
import { WidgetProps } from '../Input';
|
||||
|
||||
interface Props {
|
||||
writable?: boolean;
|
||||
displayHex?: boolean;
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
const InputColor = forwardRef<HTMLInputElement, Props & WidgetProps>((props, _fRef) => {
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const [ /* pickerActive */, setPickerActive ] = useState(false);
|
||||
|
||||
// usePopupCancel(inputRef, () => setPickerActive(false));
|
||||
|
||||
return (
|
||||
<div class={('InputColor ' + (props.full ? 'Full ' : '') + (props.class ?? '')).trim()}
|
||||
style={props.style} onFocusCapture={() => setPickerActive(true)} ref={inputRef}>
|
||||
{/* <InputText
|
||||
ref={fRef}
|
||||
value={Color.HSVToHex(props.value)}
|
||||
setValue={hex => props.setValue(Color.hexToHSV(hex))}
|
||||
max={7}
|
||||
/>
|
||||
<div class='InputColor-ColorIndicator' style={{ backgroundColor: Color.HSVToHex(props.value) }}/>
|
||||
<Popup active={pickerActive} defaultAnimation={true}>
|
||||
<ColorPicker {...props} parent={inputRef.current} />
|
||||
</Popup>*/}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default InputColor;
|
|
@ -0,0 +1,43 @@
|
|||
@use '../../../style/ext'
|
||||
@use '../../../style/def' as *
|
||||
|
||||
.InputDateTime
|
||||
@function space($chars)
|
||||
@return calc(#{$chars + 'ch'} + 6px * 2 + #{$chars + 'px'})
|
||||
|
||||
@extend %material_border
|
||||
|
||||
display: grid
|
||||
height: 48px
|
||||
overflow: hidden
|
||||
padding: 0 6px
|
||||
grid-template-columns: space(2) 0.5ch space(2) 0.5ch space(4) 1fr space(2) 0.5ch space(2)
|
||||
|
||||
border-color: $neutral-100
|
||||
background-color: $neutral-050
|
||||
|
||||
&:hover
|
||||
border-color: $neutral-200
|
||||
|
||||
&:focus, &:focus-within
|
||||
border-color: $neutral-400
|
||||
|
||||
&::placeholder
|
||||
font-weight: 400
|
||||
color: $neutral-400
|
||||
|
||||
.InputText
|
||||
padding: 12px 6px
|
||||
text-align: right
|
||||
overflow: hidden
|
||||
|
||||
border: none
|
||||
font-family: monospace
|
||||
background-color: transparentize(red, 0.8)
|
||||
background-color: transparent
|
||||
|
||||
.InputDateTime-Divider
|
||||
padding-top: 12px
|
||||
|
||||
line-height: 1.4
|
||||
color: $neutral-300
|
|
@ -0,0 +1,164 @@
|
|||
import * as Preact from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useState, useRef, useEffect, useReducer } from 'preact/hooks';
|
||||
// import { usePopupCancel } from '../../../Hooks';
|
||||
|
||||
import './InputDateTime.sass';
|
||||
|
||||
// import Popup from '../../Popup';
|
||||
import InputText from './InputText';
|
||||
// import DatePicker from '../DatePicker';
|
||||
|
||||
import { WidgetProps as Props } from '../Input';
|
||||
|
||||
type DateIdentifier = 'date' | 'month' | 'year' | 'hour' | 'minute';
|
||||
|
||||
function zeroPad(num: number, pad: number = 2): string {
|
||||
let str = '' + num;
|
||||
while (str.length < pad) str = '0' + str;
|
||||
return str;
|
||||
}
|
||||
|
||||
const InputDateTime = forwardRef<HTMLInputElement, Props>((props) => {
|
||||
const dateRef = useRef<HTMLInputElement>(null);
|
||||
const monthRef = useRef<HTMLInputElement>(null);
|
||||
const yearRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const hourRef = useRef<HTMLInputElement>(null);
|
||||
const minuteRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
// const portalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [ /* pickerActive */, setPickerActive ] = useState(false);
|
||||
// usePopupCancel([ inputRef, portalRef ], () => setPickerActive(false));
|
||||
|
||||
const [ editedDate, setEditedDate ] =
|
||||
useReducer((date, newDate: { date: string; month: string; year: string; hour: string; minute: string }) =>
|
||||
({...date, ...newDate}), { date: '00', month: '00', year: '0000', hour: '00', minute: '00' });
|
||||
|
||||
const handleResetEditedDate = () => {
|
||||
const newDate: any = {};
|
||||
|
||||
if (parseInt(editedDate.date, 10) !== props.value.getDate()) newDate.date = zeroPad(props.value.getDate());
|
||||
if (parseInt(editedDate.month, 10) !== props.value.getMonth() + 1) newDate.month = zeroPad(props.value.getMonth() + 1);
|
||||
if (parseInt(editedDate.year, 10) !== props.value.getFullYear()) newDate.year = zeroPad(props.value.getFullYear());
|
||||
if (parseInt(editedDate.hour, 10) !== props.value.getHours()) newDate.hour = zeroPad(props.value.getHours());
|
||||
if (parseInt(editedDate.minute, 10) !== props.value.getMinutes()) newDate.minute = zeroPad(props.value.getMinutes());
|
||||
|
||||
setEditedDate(newDate);
|
||||
};
|
||||
|
||||
useEffect(() => handleResetEditedDate(), [ props.value ]);
|
||||
|
||||
const setValue = (type: DateIdentifier, val: number) => {
|
||||
const newDate = new Date(props.value.getTime());
|
||||
|
||||
if (type === 'date' ) newDate.setDate(val);
|
||||
else if (type === 'month') newDate.setMonth(val - 1);
|
||||
else if (type === 'year' ) newDate.setFullYear(val);
|
||||
|
||||
else if (type === 'hour' ) newDate.setHours(val);
|
||||
else if (type === 'minute') newDate.setMinutes(val);
|
||||
|
||||
props.setValue(newDate);
|
||||
};
|
||||
|
||||
const handleSet = (val: string, type: DateIdentifier, pad: number, min: number | undefined,
|
||||
max: number | undefined, next?: Preact.RefObject<HTMLInputElement>) => {
|
||||
|
||||
setEditedDate({ [type]: val } as any);
|
||||
val = val.replace(/\D/g, '');
|
||||
setEditedDate({ [type]: val } as any);
|
||||
|
||||
if (!isNaN(parseInt(val, 10)) && (!max || parseInt(val, 10) <= max)) setValue(type, parseInt(val, 10));
|
||||
|
||||
if (val.length >= pad) {
|
||||
let numeric = parseInt(val, 10);
|
||||
if (isNaN(numeric) || numeric < (min ?? 1)) numeric = 1;
|
||||
if (max && numeric > max) numeric = max;
|
||||
|
||||
setValue(type, numeric);
|
||||
setEditedDate({ [type]: zeroPad(numeric, pad) } as any);
|
||||
|
||||
if (next?.current) window.requestAnimationFrame(() => next.current!.focus());
|
||||
};
|
||||
};
|
||||
|
||||
// const handleDatePickerSet = (newDate: Date) => {
|
||||
// props.setValue(newDate);
|
||||
// };
|
||||
|
||||
const handleFocus = (ref: Preact.RefObject<HTMLInputElement>, ..._: any) => {
|
||||
ref.current!.select();
|
||||
};
|
||||
|
||||
const handleBlur = (_: any, type: DateIdentifier, min: number | undefined,
|
||||
max: number | undefined, pad: number) => {
|
||||
|
||||
let numeric = parseInt(editedDate[type], 10);
|
||||
if (isNaN(numeric) || numeric < (min ?? 1)) numeric = 1;
|
||||
if (max && numeric > max) numeric = max;
|
||||
|
||||
setValue(type, numeric);
|
||||
setEditedDate({ [type]: zeroPad(numeric, pad) } as any);
|
||||
};
|
||||
|
||||
const handleFocusChange = (state: boolean, ...other: [ Preact.RefObject<HTMLInputElement>,
|
||||
DateIdentifier, number | undefined, number | undefined, number ]) => {
|
||||
|
||||
if (state) handleFocus(...other);
|
||||
else handleBlur(...other);
|
||||
};
|
||||
|
||||
const nextDate = new Date(props.value.getTime());
|
||||
nextDate.setMonth(nextDate.getMonth() + 1);
|
||||
nextDate.setDate(0);
|
||||
const maxMonth = nextDate.getDate();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={inputRef}
|
||||
style={props.style}
|
||||
class={('InputDateTime ' + (props.class ?? '')).trim()}
|
||||
onFocusCapture={() => setPickerActive(true)}>
|
||||
|
||||
<InputText
|
||||
value={editedDate.date} max={2} ref={dateRef}
|
||||
setValue={date => handleSet(date, 'date', 2, 1, maxMonth, monthRef)}
|
||||
onFocusChange={state => handleFocusChange(state, dateRef, 'date', 1, maxMonth, 2)} />
|
||||
|
||||
<span class='InputDateTime-Divider'>/</span>
|
||||
|
||||
<InputText
|
||||
value={editedDate.month} max={2} ref={monthRef}
|
||||
setValue={month => handleSet(month, 'month', 2, 1, 12, yearRef)}
|
||||
onFocusChange={state => handleFocusChange(state, monthRef, 'month', 1, 12, 2)} />
|
||||
|
||||
<span class='InputDateTime-Divider'>/</span>
|
||||
|
||||
<InputText
|
||||
value={editedDate.year} max={4} ref={yearRef}
|
||||
setValue={year => handleSet(year, 'year', 4, undefined, undefined, hourRef)}
|
||||
onFocusChange={state => handleFocusChange(state, yearRef, 'year', undefined, undefined, 4)} />
|
||||
|
||||
<div/>
|
||||
|
||||
<InputText
|
||||
value={editedDate.hour} max={2} ref={hourRef}
|
||||
setValue={hour => handleSet(hour, 'hour', 2, 0, 23, minuteRef)}
|
||||
onFocusChange={state => handleFocusChange(state, hourRef, 'hour', 0, 23, 2)} />
|
||||
<span class='InputDateTime-Divider'>:</span>
|
||||
<InputText
|
||||
value={editedDate.minute} max={2} ref={minuteRef}
|
||||
setValue={minute => handleSet(minute, 'minute', 2, 0, 59, undefined)}
|
||||
onFocusChange={state => handleFocusChange(state, minuteRef, 'minute', 0, 59, 2)} />
|
||||
|
||||
{/* <Popup active={pickerActive} defaultAnimation={true} ref={portalRef}>
|
||||
<DatePicker value={props.value} setValue={handleDatePickerSet} parent={inputRef.current} />
|
||||
</Popup>*/}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default InputDateTime;
|
|
@ -0,0 +1,7 @@
|
|||
@use '../../../style/ext'
|
||||
|
||||
.InputNumeric
|
||||
@extend %material_input
|
||||
|
||||
width: 100%
|
||||
padding: 12px
|
|
@ -0,0 +1,36 @@
|
|||
import * as Preact from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
|
||||
import './InputNumeric.sass';
|
||||
|
||||
import { WidgetProps as Props } from '../Input';
|
||||
|
||||
|
||||
/**
|
||||
* A numeric input widget.
|
||||
*/
|
||||
|
||||
const InputNumeric = forwardRef<HTMLInputElement, Props>((props, fRef) => {
|
||||
const cb = (evt: any) => props.setValue(evt.target.value);
|
||||
|
||||
return (
|
||||
<input
|
||||
value={props.value}
|
||||
onInput={cb}
|
||||
onChange={cb}
|
||||
|
||||
class={('InputNumeric ' + (props.class ?? '')).trim()}
|
||||
style={props.style}
|
||||
|
||||
type='number'
|
||||
ref={fRef as any}
|
||||
disabled={props.disabled}
|
||||
placeholder={props.placeholder}
|
||||
|
||||
onFocus={props.onFocusChange ? () => props.onFocusChange!(true) : undefined}
|
||||
onBlur={props.onFocusChange ? () => props.onFocusChange!(false) : undefined}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default InputNumeric;
|
|
@ -0,0 +1,38 @@
|
|||
@use '../../../style/ext'
|
||||
|
||||
.InputSelect
|
||||
@extend %material_border
|
||||
display: grid
|
||||
height: 48px
|
||||
padding: 0
|
||||
overflow: hidden
|
||||
|
||||
border-color: $neutral-100
|
||||
background-color: $neutral-050
|
||||
|
||||
&:hover
|
||||
border-color: $neutral-200
|
||||
|
||||
&:focus, &:focus-within
|
||||
border-color: $neutral-400
|
||||
|
||||
&::placeholder
|
||||
font-weight: 400
|
||||
color: $neutral-400
|
||||
|
||||
.InputSelect-Input
|
||||
padding: 8px
|
||||
width: 100%
|
||||
height: 100%
|
||||
order: 1
|
||||
|
||||
border: none
|
||||
outline: none
|
||||
background: transparent
|
||||
|
||||
&::placeholder
|
||||
font-weight: 400
|
||||
color: $neutral-400
|
||||
|
||||
&.Selected:not(:focus)::placeholder
|
||||
color: $neutral-700
|
|
@ -0,0 +1,67 @@
|
|||
import * as Preact from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useState, useRef } from 'preact/hooks';
|
||||
|
||||
import './InputSelect.sass';
|
||||
|
||||
// import Popup from '../../Popup';
|
||||
import { WidgetProps } from '../Input';
|
||||
// import SearchableOptionPicker from '../SearchableOptionPicker';
|
||||
|
||||
interface Props extends WidgetProps {
|
||||
options: { [value: string]: string };
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A select / multiselect widget.
|
||||
*/
|
||||
|
||||
const InputSelect = forwardRef<HTMLDivElement, Props>((props, fRef) => {
|
||||
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const [ search, setSearch ] = useState<string>('');
|
||||
const [ /* focused*/, setFocused ] = useState<boolean>(false);
|
||||
|
||||
const handleSearch = (evt: any) => {
|
||||
setSearch(evt.target.value ?? '');
|
||||
};
|
||||
|
||||
// const handleSet = (identifier: string) => {
|
||||
// props.setValue(identifier);
|
||||
// setSearch('');
|
||||
// };
|
||||
|
||||
const handleFocus = (focused: boolean) => {
|
||||
setFocused(focused);
|
||||
if (props.onFocusChange) props.onFocusChange(focused);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={('InputSelect ' + (props.class ?? '')).trim()} style={props.style} ref={e => {
|
||||
if (!e) return; wrapRef.current = e; if (fRef) fRef.current = e;
|
||||
}}>
|
||||
<input
|
||||
value={search}
|
||||
onInput={handleSearch}
|
||||
onChange={handleSearch}
|
||||
|
||||
type='text'
|
||||
disabled={props.disabled}
|
||||
placeholder={props.options[props.value] || props.placeholder}
|
||||
class={('InputSelect-Input ' + (props.value?.length ? 'Selected' : '')).trim()}
|
||||
|
||||
onFocus={() => handleFocus(true)}
|
||||
onBlur={() => handleFocus(false)}
|
||||
/>
|
||||
|
||||
{/* <Popup active={focused} defaultAnimation={true}>
|
||||
<SearchableOptionPicker query={search} parent={wrapRef.current}
|
||||
options={props.options}
|
||||
setSelected={selected => handleSet(selected as string)} />
|
||||
</Popup>*/}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default InputSelect;
|
|
@ -0,0 +1,13 @@
|
|||
@use '../../../style/ext'
|
||||
|
||||
.InputText
|
||||
@extend %material_input
|
||||
|
||||
width: 100%
|
||||
padding: 12px
|
||||
|
||||
&.Long
|
||||
resize: none
|
||||
|
||||
&.Code
|
||||
font-family: monospace
|
|
@ -0,0 +1,83 @@
|
|||
import * as Preact from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useRef, useLayoutEffect } from 'preact/hooks';
|
||||
|
||||
import './InputText.sass';
|
||||
|
||||
import { WidgetProps } from '../Input';
|
||||
|
||||
/**
|
||||
* Automatically scales a HTML TextArea element to the height of its content,
|
||||
* or the specified max-height, whichever is smaller. Returns a ref to attach to
|
||||
* the TextArea which should be scaled.
|
||||
*
|
||||
* @param {number} maxHeight - An optional maximum height, defaults to Infinity.
|
||||
* @param {any[]} dependents - A list of dependent variables for the TextArea's content.
|
||||
* @return {RefObject} - A RefObject to attach to the targeted TextArea.
|
||||
*/
|
||||
|
||||
function useAutoTextArea(maxHeight?: number, dependents?: any[]): Preact.RefObject<HTMLTextAreaElement> {
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!ref.current) return;
|
||||
ref.current.style.height = '';
|
||||
ref.current.style.height = Math.min(ref.current.scrollHeight + 2, maxHeight ?? Infinity) + 'px';
|
||||
}, [ ref.current, ...dependents || [] ]);
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
interface Props extends WidgetProps {
|
||||
long?: boolean;
|
||||
code?: boolean;
|
||||
maxHeight?: number;
|
||||
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A line text input widget, either a single-line input form
|
||||
* or an autoscaling textarea depending on a prop.
|
||||
*/
|
||||
|
||||
const InputText = forwardRef<HTMLInputElement | HTMLTextAreaElement, Props>((props, fRef) => {
|
||||
const ref = useAutoTextArea(props.maxHeight ?? 420, [ props.value ]);
|
||||
|
||||
const cb = (evt: any) => props.setValue(evt.target.value);
|
||||
const sharedProps = {
|
||||
value: props.value,
|
||||
onInput: cb,
|
||||
onChange: cb,
|
||||
onFocus: props.onFocusChange ? () => props.onFocusChange!(true) : undefined,
|
||||
onBlur: props.onFocusChange ? () => props.onFocusChange!(false) : undefined,
|
||||
minLength: props.min,
|
||||
maxLength: props.max,
|
||||
disabled: props.disabled,
|
||||
placeholder: props.placeholder,
|
||||
style: props.style
|
||||
};
|
||||
|
||||
return (
|
||||
props.long ?
|
||||
<textarea
|
||||
{...sharedProps}
|
||||
class={('InputText Long ' + (props.class ?? '') + (props.code ? ' Code' : '')).trim()}
|
||||
|
||||
rows={1}
|
||||
ref={(newRef) => {
|
||||
ref.current = newRef;
|
||||
if (fRef) fRef.current = newRef as any;
|
||||
}} />
|
||||
:
|
||||
<input
|
||||
{...sharedProps}
|
||||
class={('InputText Short ' + (props.class ?? '') + (props.code ? ' Code' : '')).trim()}
|
||||
|
||||
type='text'
|
||||
ref={fRef as any} />
|
||||
);
|
||||
});
|
||||
|
||||
export default InputText;
|
|
@ -0,0 +1,32 @@
|
|||
import * as Preact from 'preact';
|
||||
import { Switch, Route, Redirect, NavLink as Link } from 'react-router-dom';
|
||||
|
||||
import NewAssetForm from '../view/NewAssetForm';
|
||||
import MyAssetsList from '../view/MyAssetsList';
|
||||
|
||||
export default function AssetsRoute() {
|
||||
return (
|
||||
<div class='AssetsRoute Page'>
|
||||
<aside class='Page-Sidebar'>
|
||||
<h2 class='Page-SidebarTitle'>Assets</h2>
|
||||
|
||||
{/* <Link className='Page-SidebarCategory' activeClassName='Active' exact to='/assets/'>Featured Assets</Link>
|
||||
<Link className='Page-SidebarCategory' activeClassName='Active' exact to='/assets/subscribed'>Subscribed Assets</Link>*/}
|
||||
<Link className='Page-SidebarCategory' activeClassName='Active' to='/assets/uploaded'>Uploaded Assets</Link>
|
||||
</aside>
|
||||
<main class='AssetsRoute-Main'>
|
||||
<Switch>
|
||||
{/* <Route exact path='/assets/'>
|
||||
<h1>Storefront</h1>
|
||||
</Route>
|
||||
<Route exact path='/assets/subscribed'>
|
||||
<h1>Subscribed</h1>
|
||||
</Route>*/}
|
||||
<Route exact path='/assets/uploaded'><MyAssetsList/></Route>
|
||||
<Route exact path='/assets/new'><NewAssetForm/></Route>
|
||||
<Redirect to='/assets/uploaded' />
|
||||
</Switch>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
@use '../../style/slice'
|
||||
|
||||
.CampaignRoute
|
||||
width: 100%
|
||||
max-width: 1000px
|
||||
margin: 0 auto
|
||||
|
||||
.CampaignRoute-Card
|
||||
@extend .slice_background
|
||||
|
||||
width: auto
|
||||
margin: 32px 16px
|
||||
|
||||
&:first-of-type
|
||||
margin-top: 8px
|
|
@ -1,25 +1,50 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useAppData } from '../../Hooks';
|
||||
import { Redirect, useParams } from 'react-router-dom';
|
||||
import { Switch, Route, Redirect, NavLink as Link, useParams } from 'react-router-dom';
|
||||
|
||||
import './CampaignRoute.sass';
|
||||
|
||||
import CampaignDetails from '../card/CampaignDetails';
|
||||
import MapList from '../view/MapList';
|
||||
import NewMapForm from '../view/NewMapForm';
|
||||
import PlayerList from '../view/PlayerList';
|
||||
import CampaignOverview from '../view/CampaignOverview';
|
||||
|
||||
export default function CampaignRoute() {
|
||||
const [ { campaigns } ] = useAppData('campaigns');
|
||||
if (!campaigns) return null;
|
||||
|
||||
const { campaign: identifier } = useParams<{ campaign: string }>();
|
||||
const currentCampaign = (campaigns ?? []).filter(c => c.identifier === identifier)[0];
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const currentCampaign = (campaigns ?? []).filter(c => c.identifier === id)[0];
|
||||
|
||||
if (!currentCampaign) return <Redirect to='/campaigns/' />;
|
||||
|
||||
if (!currentCampaign) return <Redirect to='/' />;
|
||||
|
||||
return (
|
||||
<div class='CampaignRoute'>
|
||||
<div class='CampaignRoute-Card'>
|
||||
<CampaignDetails campaign={currentCampaign} />
|
||||
</div>
|
||||
<div class='CampaignRoute Page'>
|
||||
<aside class='Page-Sidebar'>
|
||||
<h2 class='Page-SidebarTitle'>{currentCampaign.title}</h2>
|
||||
|
||||
<Link className='Page-SidebarCategory' activeClassName='Active' exact to={`/campaign/${id}`}>Overview</Link>
|
||||
<Link className='Page-SidebarCategory' activeClassName='Active' to={`/campaign/${id}/players`}>Players</Link>
|
||||
<Link className='Page-SidebarCategory' activeClassName='Active' to={`/campaign/${id}/maps`}>Maps</Link>
|
||||
<Link className='Page-SidebarCategory' activeClassName='Active' to={`/campaign/${id}/assets`}>Assets</Link>
|
||||
</aside>
|
||||
<main class='CampaignRoute-Main'>
|
||||
<Switch>
|
||||
<Route exact path='/campaign/:id'><CampaignOverview campaign={currentCampaign} /></Route>
|
||||
<Route exact path='/campaign/:id/players'>
|
||||
<PlayerList players={[
|
||||
{ name: 'Player', sprite: '/app/static/token/baby_blue_dragon.png' },
|
||||
{ name: 'Player', sprite: '/app/static/token/cadin_1.png' },
|
||||
{ name: 'Player', sprite: '/app/static/token/dragonfolk_1.png' },
|
||||
{ name: 'Player', sprite: '/app/static/token/druid_male.png' },
|
||||
{ name: 'Player', sprite: '/app/static/token/naexi_human_yklwa.png' }
|
||||
]}/>
|
||||
</Route>
|
||||
<Route exact path='/campaign/:id/maps'><MapList maps={currentCampaign.maps} /></Route>
|
||||
<Route exact path='/campaign/:id/maps/new'><NewMapForm /></Route>
|
||||
<Route exact path='/campaign/:id/assets'></Route>
|
||||
|
||||
<Redirect to={`/campaign/${id}`} />
|
||||
</Switch>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.CampaignsRoute-New
|
||||
padding: 16px
|
||||
padding-top: 72px
|
||||
|
||||
.CampaignsRoute-NewCard
|
||||
@extend %card
|
||||
|
||||
width: 100%
|
||||
max-width: 1000px
|
||||
display: block
|
||||
margin: 0 auto
|
|
@ -0,0 +1,46 @@
|
|||
import * as Preact from 'preact';
|
||||
import { NavLink as Link, Switch, Route, Redirect } from 'react-router-dom';
|
||||
|
||||
import './CampaignsRoute.sass';
|
||||
|
||||
import CampaignList from '../view/CampaignList';
|
||||
import NewCampaignForm from '../view/NewCampaignForm';
|
||||
|
||||
function testActive(match: any, desire: string) {
|
||||
return match.pathname.match(/\/campaigns\/?$/g) && match.search === desire;
|
||||
}
|
||||
|
||||
export default function CampaignsRoute() {
|
||||
return (
|
||||
<div class='CampaignsRoute Page'>
|
||||
<aside class='Page-Sidebar'>
|
||||
<h2 class='Page-SidebarTitle'>Campaigns</h2>
|
||||
|
||||
<Link className='Page-SidebarCategory' activeClassName='Active'
|
||||
to='/campaigns' isActive={(_, loc) => testActive(loc, '')}>All Campaigns</Link>
|
||||
<Link className='Page-SidebarCategory' activeClassName='Active'
|
||||
to='/campaigns?owner=self' isActive={(_, loc) => testActive(loc, '?owner=self')}>My Campaigns</Link>
|
||||
<Link className='Page-SidebarCategory' activeClassName='Active'
|
||||
to='/campaigns?owner=other' isActive={(_, loc) => testActive(loc, '?owner=other')}>Joined Campaigns</Link>
|
||||
</aside>
|
||||
|
||||
<Switch>
|
||||
<Route path='/campaigns/new'>
|
||||
<main class='CampaignsRoute-New'>
|
||||
<div class='CampaignsRoute-NewCard'>
|
||||
<NewCampaignForm />
|
||||
</div>
|
||||
</main>
|
||||
</Route>
|
||||
|
||||
<Route exact path='/campaigns'>
|
||||
<main class='CampaignsRoute-Main Page-Main'>
|
||||
<CampaignList />
|
||||
</main>
|
||||
</Route>
|
||||
|
||||
<Redirect to='/campaigns' />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -5,11 +5,11 @@
|
|||
max-width: 1000px
|
||||
margin: 0 auto
|
||||
|
||||
.HomeRoute-Card
|
||||
@extend .slice_background
|
||||
.HomeRoute-Card
|
||||
@extend .slice_background
|
||||
|
||||
width: auto
|
||||
margin: 32px 16px
|
||||
width: auto
|
||||
margin: 32px 16px
|
||||
|
||||
&:first-of-type
|
||||
margin-top: 8px
|
||||
&:first-of-type
|
||||
margin-top: 8px
|
||||
|
|
|
@ -1,29 +1,13 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useAppData } from '../../Hooks';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import './HomeRoute.sass';
|
||||
|
||||
import CampaignList from '../card/CampaignList';
|
||||
import NewCampaignForm from '../card/NewCampaignForm';
|
||||
import CampaignOverview from '../card/CampaignOverview';
|
||||
|
||||
export default function HomeRoute() {
|
||||
const [ { campaigns } ] = useAppData('campaigns');
|
||||
|
||||
const { campaign: identifier } = useParams<{ campaign: string }>();
|
||||
const currentCampaign = (campaigns ?? []).filter(c => c.identifier === identifier)[0];
|
||||
|
||||
return (
|
||||
<div class='HomeRoute'>
|
||||
<div class='HomeRoute-Card'>
|
||||
<CampaignList allowNew={identifier !== 'new'} />
|
||||
<h1>Home (unused)</h1>
|
||||
</div>
|
||||
|
||||
{(currentCampaign || identifier === 'new') && <div class='HomeRoute-Card'>
|
||||
{identifier === 'new' && <NewCampaignForm />}
|
||||
{currentCampaign && <CampaignOverview campaign={currentCampaign} />}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
@use '../../style/def' as *
|
||||
@use '../../style/text'
|
||||
@use '../../style/slice'
|
||||
|
||||
.Page
|
||||
width: 100%
|
||||
height: 100%
|
||||
display: grid
|
||||
grid-template-columns: 280px 1fr
|
||||
|
||||
.Page-Sidebar
|
||||
background: $neutral-200
|
||||
padding: 0 16px
|
||||
|
||||
.Page-SidebarTitle
|
||||
@include text.line_clamp
|
||||
|
||||
padding: 16px 0 20px 0
|
||||
margin: 0
|
||||
|
||||
font-size: 34px
|
||||
|
||||
.Page-SidebarCategory
|
||||
display: block
|
||||
padding: 12px
|
||||
margin-bottom: 8px
|
||||
|
||||
font-size: 18px
|
||||
border-radius: 4px
|
||||
text-decoration: none
|
||||
background: $neutral-250
|
||||
|
||||
&:hover, &:focus-visible
|
||||
background: $neutral-300
|
||||
|
||||
&.Active
|
||||
background: linear-gradient(130deg, $accent-600, $accent-800)
|
||||
|
||||
.Page-Main
|
||||
padding: 16px
|
|
@ -1,7 +1,13 @@
|
|||
import './Route.sass';
|
||||
|
||||
export { default as Login } from './LoginRoute';
|
||||
|
||||
export { default as Home } from './HomeRoute';
|
||||
|
||||
export { default as Map } from './MapRoute';
|
||||
export { default as Campaign } from './CampaignRoute';
|
||||
export { default as Campaigns } from './CampaignsRoute';
|
||||
|
||||
export { default as Assets } from './AssetsRoute';
|
||||
|
||||
export { default as Editor } from './EditorRoute';
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/grid'
|
||||
@use '../../style/text'
|
||||
@use '../../style/slice'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.CampaignList
|
||||
max-width: 1000px
|
||||
display: block
|
||||
margin: 0 auto
|
||||
margin-top: 56px
|
||||
|
||||
.CampaignList-Grid
|
||||
@include grid.auto_width(300px, 16px)
|
||||
|
||||
.CampaignList-CampaignWrap
|
||||
.CampaignList-Campaign
|
||||
display: grid
|
||||
user-select: none
|
||||
|
||||
overflow: hidden
|
||||
border-radius: 4px
|
||||
text-decoration: none
|
||||
background-color: $neutral-200
|
||||
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
|
||||
|
||||
.CampaignList-CampaignInner
|
||||
.CampaignList-CampaignPreview
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 0
|
||||
padding-bottom: 56.25%
|
||||
|
||||
img
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
object-fit: cover
|
||||
user-select: none
|
||||
pointer-events: none
|
||||
|
||||
.CampaignList-CampaignTitle
|
||||
@include text.line_clamp
|
||||
|
||||
margin: 0
|
||||
font-size: 18px
|
||||
padding: 16px 12px
|
||||
|
||||
.CampaignList-NewCampaign
|
||||
display: flex
|
||||
user-select: none
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
overflow: hidden
|
||||
border-radius: 4px
|
||||
background-color: $neutral-100
|
||||
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
|
||||
|
||||
border: 3px dotted $neutral-300
|
||||
|
||||
min-height: 220px
|
||||
height: 100%
|
||||
|
||||
img
|
||||
width: 80px
|
||||
height: 80px
|
||||
image-rendering: pixelated
|
||||
|
||||
p
|
||||
font-size: 18px
|
||||
margin: 0
|
||||
margin-top: 4px
|
|
@ -0,0 +1,37 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useAppData } from '../../Hooks';
|
||||
import { NavLink as Link } from 'react-router-dom';
|
||||
|
||||
import './CampaignList.sass';
|
||||
|
||||
export default function CampaignList() {
|
||||
const [ { campaigns } ] = useAppData('campaigns');
|
||||
|
||||
return (
|
||||
<div class='CampaignList'>
|
||||
{campaigns === undefined && <p>Loading Campaigns...</p>}
|
||||
{campaigns !== undefined &&
|
||||
<Preact.Fragment>
|
||||
<ul class='CampaignList-Grid'>
|
||||
{campaigns.map(c => <li class='CampaignList-CampaignWrap'>
|
||||
<Link className='CampaignList-Campaign' to={`/campaign/${c.identifier}`}>
|
||||
<div class='CampaignList-CampaignInner'>
|
||||
<div class='CampaignList-CampaignPreview'>
|
||||
<img src='https://placekitten.com/400/300' alt='' />
|
||||
</div>
|
||||
<p class='CampaignList-CampaignTitle'>{c.title || 'Untitled'}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</li>)}
|
||||
<li class='CampaignList-CampaignWrap'>
|
||||
<Link className='CampaignList-NewCampaign' to='/campaigns/new'>
|
||||
<img src='/app/static/ui/icon/campaign_new.png' alt=''/>
|
||||
<p>Create Campaign</p>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</Preact.Fragment>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/text'
|
||||
@use '../../style/slice'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.CampaignOverview
|
||||
|
||||
.CampaignOverview-Carousel
|
||||
position: relative
|
||||
height: 0
|
||||
width: 100%
|
||||
margin-top: 16px
|
||||
margin-bottom: 32px
|
||||
padding-bottom: min(360px, 33.33%)
|
||||
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
.CampaignOverview-CarouselInner
|
||||
display: grid
|
||||
grid-gap: 24px
|
||||
position: absolute
|
||||
grid-template-columns: 1fr min(80%, 1000px) 1fr
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
justify-content: center
|
||||
|
||||
.CampaignOverview-CarouselImage
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
border-radius: 4px
|
||||
object-fit: cover
|
||||
|
||||
&.Unfocused
|
||||
filter: saturate(0%) contrast(0.5) brightness(0.25) sepia(1) saturate(150%) hue-rotate(185deg)
|
||||
|
||||
&:first-child
|
||||
border-radius: 0 4px 4px 0
|
||||
|
||||
&:last-child
|
||||
border-radius: 4px 0 0 4px
|
||||
|
||||
.CampaignOverview-Details
|
||||
@extend %card
|
||||
margin: 0 auto
|
||||
max-width: 1000px
|
||||
|
||||
.CampaignOverview-Title
|
||||
@include text.line_clamp
|
||||
|
||||
margin-top: 0
|
||||
margin-bottom: 8px
|
||||
padding-right: 6px
|
||||
|
||||
font-weight: 500
|
||||
font-size: 34px
|
||||
font-family: $font-header
|
||||
|
||||
.CampaignOverview-Description
|
||||
margin-top: 20px
|
||||
min-height: 6em
|
||||
color: $neutral-800
|
||||
|
||||
.CampaignOverview-Actions
|
||||
float: right
|
||||
|
||||
.CampaignOverview-CharactersWrap
|
||||
@extend .slice_highlight_neutral
|
||||
|
||||
height: 64px
|
||||
width: max-content
|
||||
|
||||
.CampaignOverview-Characters
|
||||
@include slice.slice_invert
|
||||
|
||||
display: flex
|
||||
padding: 6px 12px
|
||||
width: max-content
|
||||
gap: 12px
|
||||
|
||||
.CampaignOverview-Character
|
||||
width: #{18px * 3}
|
||||
height: #{18px * 3}
|
||||
|
||||
background-size: #{18px * 3 * 2} #{18px * 3 * 2}
|
||||
background-position: top left
|
||||
image-rendering: pixelated
|
|
@ -0,0 +1,51 @@
|
|||
import * as Preact from 'preact';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import './CampaignOverview.sass';
|
||||
|
||||
import Button from '../Button';
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
|
||||
import { Campaign } from '../../../../common/DBStructs';
|
||||
|
||||
interface Props {
|
||||
campaign: Campaign;
|
||||
}
|
||||
|
||||
export default function CampaignOverview({ campaign }: Props) {
|
||||
return (
|
||||
<div class='CampaignOverview'>
|
||||
<div class='CampaignOverview-Carousel'>
|
||||
<div class='CampaignOverview-CarouselInner'>
|
||||
<img class='CampaignOverview-CarouselImage Unfocused' src='https://placekitten.com/100/300' alt='' />
|
||||
<img class='CampaignOverview-CarouselImage' src='https://placekitten.com/800/300' alt='' />
|
||||
<img class='CampaignOverview-CarouselImage Unfocused' src='https://placekitten.com/100/300' alt='' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='CampaignOverview-Details'>
|
||||
<div class='CampaignOverview-Actions'>
|
||||
<ButtonGroup>
|
||||
<Button to={`${campaign.identifier}/details`} icon='edit' alt='Edit'/>
|
||||
<Button to={`${campaign.identifier}/play`} icon='play' label='Start' disabled={campaign.maps.length === 0}/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
|
||||
<h3 class='CampaignOverview-Title'>{campaign.title || 'Untitled'}</h3>
|
||||
<p class='CampaignOverview-Description'>{campaign.description || 'Lorem ipsum dolor sit amet...'}</p>
|
||||
|
||||
<Link to={`${campaign.identifier}/players`}>
|
||||
<div class='CampaignOverview-CharactersWrap'>
|
||||
<div class='CampaignOverview-Characters'>
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/baby_blue_dragon.png)' }} />
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/cadin_1.png)' }} />
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/dragonfolk_1.png)' }} />
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/druid_male.png)' }} />
|
||||
<div class='CampaignOverview-Character' style={{ backgroundImage: 'url(/app/static/token/naexi_human_yklwa.png)' }} />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/grid'
|
||||
@use '../../style/text'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.MapList
|
||||
// @extend %card
|
||||
max-width: 1000px
|
||||
margin: 56px auto
|
||||
|
||||
.MapList-Grid
|
||||
@include grid.auto_width(160px, 16px)
|
||||
|
||||
.MapList-MapWrap
|
||||
.MapList-Map
|
||||
height: 0
|
||||
display: grid
|
||||
user-select: none
|
||||
position: relative
|
||||
padding-bottom: 100%
|
||||
|
||||
overflow: hidden
|
||||
border-radius: 4px
|
||||
text-decoration: none
|
||||
background-color: $neutral-250
|
||||
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
|
||||
|
||||
.MapList-MapInner
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
display: grid
|
||||
grid-template-rows: 1fr auto
|
||||
|
||||
.MapList-MapPreview
|
||||
width: 100%
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
object-fit: cover
|
||||
user-select: none
|
||||
pointer-events: none
|
||||
|
||||
.MapList-MapTitle
|
||||
@include text.line_clamp
|
||||
|
||||
margin: 0
|
||||
display: block
|
||||
font-size: 18px
|
||||
padding: 10px 12px
|
||||
|
||||
.MapList-NewMap
|
||||
display: flex
|
||||
user-select: none
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
overflow: hidden
|
||||
border-radius: 4px
|
||||
background-color: $neutral-100
|
||||
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
|
||||
|
||||
border: 3px dotted $neutral-300
|
||||
|
||||
min-height: 180px
|
||||
height: 100%
|
||||
|
||||
img
|
||||
width: 80px
|
||||
height: 80px
|
||||
image-rendering: pixelated
|
||||
|
||||
p
|
||||
font-size: 18px
|
||||
margin: 0
|
||||
margin-top: 4px
|
|
@ -0,0 +1,42 @@
|
|||
import * as Preact from 'preact';
|
||||
import { NavLink as Link, useParams } from 'react-router-dom';
|
||||
|
||||
import './MapList.sass';
|
||||
|
||||
import { Map } from '../../../../common/DBStructs';
|
||||
|
||||
interface Props {
|
||||
maps: Map[];
|
||||
}
|
||||
|
||||
export default function MapList({ maps }: Props) {
|
||||
const { id: campaign } = useParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<div class='MapList'>
|
||||
{maps === undefined && <p>Loading Maps...</p>}
|
||||
{maps !== undefined &&
|
||||
<Preact.Fragment>
|
||||
<ul class='MapList-Grid'>
|
||||
{maps.map(m => <li class='MapList-MapWrap'>
|
||||
<Link className='MapList-Map' to={`/edit/${campaign}/${m.identifier}`}>
|
||||
<div class='MapList-MapInner'>
|
||||
<div class='MapList-MapPreview'>
|
||||
<img src='https://placekitten.com/400/300' alt='' />
|
||||
</div>
|
||||
<p class='MapList-MapTitle'>{m.name || 'Untitled'}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</li>)}
|
||||
<li class='MapList-MapWrap'>
|
||||
<Link className='MapList-NewMap' to={`/campaign/${campaign}/maps/new`}>
|
||||
<img src='/app/static/ui/icon/map_new.png' alt=''/>
|
||||
<p>Create Map</p>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</Preact.Fragment>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
@use '../../style/text'
|
||||
@use '../../style/grid'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.AssetsList
|
||||
max-width: 1000px
|
||||
display: block
|
||||
margin: 0 auto
|
||||
margin-top: 56px
|
||||
|
||||
.AssetsList-Grid
|
||||
@include grid.auto_width(160px, 16px)
|
||||
|
||||
.AssetsList-AssetWrap
|
||||
.AssetsList-Asset
|
||||
height: 0
|
||||
display: grid
|
||||
user-select: none
|
||||
position: relative
|
||||
padding-bottom: 100%
|
||||
|
||||
overflow: hidden
|
||||
border-radius: 4px
|
||||
text-decoration: none
|
||||
background-color: $neutral-200
|
||||
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
|
||||
|
||||
.AssetsList-AssetInner
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
display: grid
|
||||
grid-template-rows: 1fr auto
|
||||
|
||||
.AssetsList-AssetPreview
|
||||
position: relative
|
||||
padding: 8px
|
||||
overflow: hidden
|
||||
background-color: $neutral-100
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
user-select: none
|
||||
object-fit: contain
|
||||
pointer-events: none
|
||||
image-rendering: pixelated
|
||||
|
||||
.AssetsList-AssetTitle
|
||||
@include text.line_clamp
|
||||
|
||||
margin: 0
|
||||
font-size: 18px
|
||||
padding: 12px
|
||||
|
||||
.AssetsList-NewAsset
|
||||
display: flex
|
||||
user-select: none
|
||||
flex-direction: column
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
overflow: hidden
|
||||
border-radius: 4px
|
||||
background-color: $neutral-100
|
||||
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
|
||||
|
||||
border: 3px dotted $neutral-300
|
||||
|
||||
min-height: 180px
|
||||
height: 100%
|
||||
|
||||
img
|
||||
width: 80px
|
||||
height: 80px
|
||||
image-rendering: pixelated
|
||||
|
||||
p
|
||||
font-size: 18px
|
||||
margin: 0
|
||||
margin-top: 4px
|
|
@ -0,0 +1,37 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useAppData } from '../../Hooks';
|
||||
import { NavLink as Link } from 'react-router-dom';
|
||||
|
||||
import './MyAssetsList.sass';
|
||||
|
||||
export default function MyAssetsList() {
|
||||
const [ { assets } ] = useAppData('assets');
|
||||
|
||||
return (
|
||||
<div class='AssetsList'>
|
||||
{assets === undefined && <p>Loading Assets...</p>}
|
||||
{assets !== undefined &&
|
||||
<Preact.Fragment>
|
||||
<ul class='AssetsList-Grid'>
|
||||
{assets.map(a => <li class='AssetsList-AssetWrap'>
|
||||
<Link className='AssetsList-Asset' to={`/campaign/${a.identifier}`}>
|
||||
<div class='AssetsList-AssetInner'>
|
||||
<div class='AssetsList-AssetPreview'>
|
||||
<img src={'/app/asset/' + a.path} alt='' />
|
||||
</div>
|
||||
<p class='AssetsList-AssetTitle'>{a.name || 'Untitled'}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</li>)}
|
||||
<li class='AssetsList-AssetWrap'>
|
||||
<Link className='AssetsList-NewAsset' to='/assets/new'>
|
||||
<img src='/app/static/ui/icon/asset_new.png' alt=''/>
|
||||
<p>Upload Asset</p>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</Preact.Fragment>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/text'
|
||||
@use '../../style/slice'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.NewAssetForm
|
||||
@extend %card
|
||||
max-width: 1000px
|
||||
display: block
|
||||
margin: 0 auto
|
||||
margin-top: 56px
|
||||
|
||||
.NewAssetForm-Title
|
||||
@include text.line_clamp
|
||||
|
||||
margin-top: 0
|
||||
margin-bottom: 8px
|
||||
padding-right: 6px
|
||||
|
||||
font-weight: 500
|
||||
font-size: 34px
|
||||
font-family: $font-header
|
||||
|
||||
.NewAssetForm-Col2
|
||||
display: grid
|
||||
grid-gap: 16px
|
||||
grid-template-columns: 1fr 1fr
|
||||
|
||||
.NewAssetForm-UploadWrap
|
||||
@extend %material_button
|
||||
|
||||
position: relative
|
||||
height: 100px
|
||||
|
||||
cursor: pointer
|
||||
border-color: $neutral-300
|
||||
|
||||
.NewAssetForm-Upload
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
outline: 0
|
||||
opacity: 0
|
||||
|
||||
.NewAssetForm-UploadTitle
|
||||
position: absolute
|
||||
padding: 0
|
||||
margin: 0
|
||||
top: 50%
|
||||
left: 0
|
||||
right: 0
|
||||
|
||||
transform: translateY(-50%)
|
||||
text-align: center
|
||||
color: $neutral-600
|
||||
|
||||
.NewAssetForm-AssetPreview
|
||||
display: flex
|
||||
gap: 12px
|
||||
margin-top: 12px
|
||||
height: 16px * 3 * 6
|
||||
flex-direction: row
|
||||
|
||||
.NewAssetForm-ImagePreviewWrap
|
||||
@extend .slice_highlight_neutral
|
||||
|
||||
flex-grow: 1
|
||||
|
||||
.NewAssetForm-ImagePreview
|
||||
@include slice.slice_invert
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
object-fit: contain
|
||||
image-rendering: pixelated
|
||||
|
||||
.NewAssetForm-TilePreviewWrap, .NewAssetForm-TokenPreviewWrap
|
||||
@extend .slice_highlight_neutral
|
||||
|
||||
display: inline-block
|
||||
width: max-content
|
||||
flex-shrink: 0
|
||||
|
||||
.NewAssetForm-TilePreview, .NewAssetForm-TokenPreview
|
||||
@include slice.slice_invert
|
||||
|
||||
display: grid
|
||||
width: 16px * 3 * 6
|
||||
height: 16px * 3 * 6
|
||||
grid-template-rows: 1fr 1fr 1fr
|
||||
grid-template-columns: 1fr 1fr 1fr
|
||||
|
||||
background-size: 0px
|
||||
|
||||
& > div
|
||||
image-rendering: pixelated
|
||||
background-image: inherit
|
||||
background-repeat: no-repeat
|
||||
background-size: #{9 * 100%} #{7 * 100%}
|
||||
|
||||
&.wall > div
|
||||
&:nth-child(1)
|
||||
background-position: #{100% * (7/8)} 0
|
||||
&:nth-child(2)
|
||||
background-position: #{100% * (4/8)} #{100% * (3/6)}
|
||||
&:nth-child(3)
|
||||
background-position: #{100% * (8/8)} 0
|
||||
|
||||
&:nth-child(4)
|
||||
background-position: #{100% * (4/8)} #{100% * (2/6)}
|
||||
&:nth-child(5)
|
||||
background-position: 0 #{100% * (6/6)}
|
||||
&:nth-child(6)
|
||||
background-position: #{100% * (5/8)} #{100% * (2/6)}
|
||||
|
||||
&:nth-child(7)
|
||||
background-position: #{100% * (7/8)} #{100% * (1/6)}
|
||||
&:nth-child(8)
|
||||
background-position: #{100% * (5/8)} #{100% * (3/6)}
|
||||
&:nth-child(9)
|
||||
background-position: #{100% * (8/8)} #{100% * (1/6)}
|
||||
|
||||
&.ground > div
|
||||
&:nth-child(1)
|
||||
background-position: #{100% * (3/8)} 0
|
||||
&:nth-child(2)
|
||||
background-position: #{100% * (1/8)} #{100% * (2/6)}
|
||||
&:nth-child(3)
|
||||
background-position: #{100% * (5/8)} 0
|
||||
|
||||
&:nth-child(4)
|
||||
background-position: #{100% * (2/8)} #{100% * (1/6)}
|
||||
&:nth-child(5)
|
||||
background-position: 0 #{100% * (6/6)}
|
||||
&:nth-child(6)
|
||||
background-position: 0 #{100% * (1/6)}
|
||||
|
||||
&:nth-child(7)
|
||||
background-position: #{100% * (3/8)} #{100% * (2/6)}
|
||||
&:nth-child(8)
|
||||
background-position: #{100% * (1/8)} 0
|
||||
&:nth-child(9)
|
||||
background-position: #{100% * (5/8)} #{100% * (2/6)}
|
||||
|
||||
.NewAssetForm-TokenPreview
|
||||
background-repeat: no-repeat
|
||||
background-size: 100% 100%
|
||||
image-rendering: pixelated
|
||||
|
||||
&.Slice4
|
||||
animation: Token-Anim-4 2.5s infinite
|
||||
background-size: 200% 200%
|
||||
|
||||
@keyframes Token-Anim-4
|
||||
0%, 24.99999%
|
||||
background-position: 0% 0%
|
||||
25%, 49.99999%
|
||||
background-position: 100% 0%
|
||||
50%, 74.99999%
|
||||
background-position: 0% 100%
|
||||
75%, 100%
|
||||
background-position: 100% 100%
|
||||
|
||||
&.Slice8
|
||||
animation: Token-Anim-8 5s infinite
|
||||
background-size: 300% 300%
|
||||
|
||||
@keyframes Token-Anim-8
|
||||
0%, 12.49999%
|
||||
background-position: 0% 0%
|
||||
12.5%, 24.99999%
|
||||
background-position: 50% 0%
|
||||
25%, 37.49999%
|
||||
background-position: 100% 0%
|
||||
37.5%, 49.99999%
|
||||
background-position: 100% 50%
|
||||
50%, 62.49999%
|
||||
background-position: 100% 100%
|
||||
62.5%, 74.99999%
|
||||
background-position: 50% 100%
|
||||
75%, 87.49999%
|
||||
background-position: 0% 100%
|
||||
87.5%, 100%
|
||||
background-position: 0% 50%
|
||||
|
||||
.NewAssetForm-AssetDisclaimer
|
||||
margin: 0
|
||||
padding: 0
|
||||
color: $neutral-700
|
||||
margin-top: 16px
|
||||
margin-bottom: 36px
|
||||
|
||||
|
||||
.NewAssetForm-Description
|
||||
min-height: 100px
|
||||
|
||||
.NewAssetForm-Submit
|
||||
margin-top: 20px
|
|
@ -0,0 +1,148 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
import './NewAssetForm.sass';
|
||||
|
||||
import { Label, Text } from '../input/Input';
|
||||
|
||||
import Button from '../Button';
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
|
||||
import * as Format from '../../../../common/Format';
|
||||
|
||||
export default function NewAssetForm() {
|
||||
const history = useHistory();
|
||||
|
||||
const [ queryState, setQueryState ] = useState<'idle' | 'querying'>('idle');
|
||||
|
||||
const [ type, setType ] = useState<'wall' | 'ground' | 'token'>('token');
|
||||
const [ tokenType, setTokenType ] = useState<1 | 4 | 8>(4);
|
||||
|
||||
const [ file, setFile ] = useState<File | null>(null);
|
||||
const [ filePreview, setFilePreview ] = useState<string | null>(null);
|
||||
|
||||
const [ name, setName ] = useState<string>('');
|
||||
const [ identifier, setIdentifier ] = useState<string>('');
|
||||
|
||||
const handleSetType = (type: 'wall' | 'ground' | 'token') => {
|
||||
setType(type);
|
||||
};
|
||||
|
||||
const handleFileSet = (evt: any) => {
|
||||
const input = evt.target as HTMLInputElement;
|
||||
const newFile: File | undefined = input.files?.[0];
|
||||
|
||||
if (!newFile || newFile.type !== 'image/png' || !newFile.name.endsWith('.png')) return setFile(null);
|
||||
|
||||
setFile(newFile ?? null);
|
||||
setName(Format.name(newFile.name));
|
||||
setIdentifier(Format.identifier(Format.name(newFile.name)));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!file) return;
|
||||
|
||||
let set = true;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => { if (set) setFilePreview((e.target as any).result); };
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
return () => set = false;
|
||||
}, [ file ]);
|
||||
|
||||
const createCampaign = async () => {
|
||||
if (queryState !== 'idle' || !file) return;
|
||||
setQueryState('querying');
|
||||
|
||||
let data = new FormData();
|
||||
data.append('file', file);
|
||||
|
||||
data.append('type', type);
|
||||
if (type === 'token') data.append('tokenType', tokenType.toString());
|
||||
|
||||
data.append('name', name);
|
||||
data.append('identifier', identifier);
|
||||
|
||||
const res = await fetch('/data/asset/upload', {
|
||||
method: 'POST', cache: 'no-cache',
|
||||
body: data
|
||||
});
|
||||
|
||||
if (res.status === 202) {
|
||||
console.log('hellyea!');
|
||||
history.push('/assets');
|
||||
}
|
||||
else {
|
||||
console.log('hellnah', await res.text());
|
||||
setQueryState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class='NewAssetForm'>
|
||||
<h2 class='NewAssetForm-Title'>New Asset</h2>
|
||||
|
||||
<div class='NewAssetForm-Col2'>
|
||||
<div>
|
||||
<Label label='Asset Type' />
|
||||
<ButtonGroup>
|
||||
<Button icon='token' label='Token' inactive={type !== 'token'} onClick={() => handleSetType('token')}/>
|
||||
<Button icon='ground' label='Ground' inactive={type !== 'ground'} onClick={() => handleSetType('ground')}/>
|
||||
<Button icon='wall' label='Wall' inactive={type !== 'wall'} onClick={() => handleSetType('wall')}/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{type === 'token' && <div>
|
||||
<Label label='Token Type' />
|
||||
<ButtonGroup>
|
||||
<Button icon='token_1' label='Single' inactive={tokenType !== 1} onClick={() => setTokenType(1)}/>
|
||||
<Button icon='token_4' label='4 Slice' inactive={tokenType !== 4} onClick={() => setTokenType(4)}/>
|
||||
<Button icon='token_8' label='8 Slice' inactive={tokenType !== 8} onClick={() => setTokenType(8)}/>
|
||||
</ButtonGroup>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<Label label='Asset'>
|
||||
<div class='NewAssetForm-UploadWrap'>
|
||||
<input type='file' class='NewAssetForm-Upload' accept='.png' onChange={handleFileSet} />
|
||||
<p class='NewAssetForm-UploadTitle'>Drag file here or click to Select</p>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
{file && <Preact.Fragment>
|
||||
<div class='NewAssetForm-AssetPreview'>
|
||||
<div class='NewAssetForm-ImagePreviewWrap'>
|
||||
<div class='NewAssetForm-ImagePreview'>
|
||||
<img src={filePreview ?? undefined} alt='' />
|
||||
</div>
|
||||
</div>
|
||||
{type !== 'token' && <div class='NewAssetForm-TilePreviewWrap'>
|
||||
<div class={'NewAssetForm-TilePreview ' + type} style={{ backgroundImage: `url(${filePreview})` }}>
|
||||
<div /><div /><div />
|
||||
<div /><div /><div />
|
||||
<div /><div /><div />
|
||||
</div>
|
||||
</div>}
|
||||
{type === 'token' && <div class='NewAssetForm-TokenPreviewWrap'>
|
||||
<div class={'NewAssetForm-TokenPreview Slice' + tokenType.toString()} style={{ backgroundImage: `url(${filePreview})` }} />
|
||||
</div>}
|
||||
</div>
|
||||
<p class='NewAssetForm-AssetDisclaimer'>If the preview doesn't look correct,
|
||||
try changing the asset {type === 'token' ? 'or token' : ''} type!</p>
|
||||
|
||||
<div class='NewAssetForm-Col2'>
|
||||
<Label label='Asset Name'>
|
||||
<Text value={name} setValue={setName} />
|
||||
</Label>
|
||||
|
||||
<Label label='Asset Identifier'>
|
||||
<Text class='NewAssetForm-Identifier' long={true} value={identifier} setValue={setIdentifier} />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Button class='NewAssetForm-Submit' onClick={createCampaign} icon='add' label={`Create ${Format.name(type)} Asset`}
|
||||
disabled={!(name.length > 3 && identifier.length > 3)} />
|
||||
</Preact.Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
@use '../../style/def' as *
|
||||
@use '../../style/text'
|
||||
|
||||
.NewCampaignForm
|
||||
.NewCampaignForm-Title
|
||||
@include text.line_clamp
|
||||
|
||||
margin-top: 0
|
||||
margin-bottom: 8px
|
||||
padding-right: 6px
|
||||
|
||||
font-weight: 500
|
||||
font-size: 34px
|
||||
font-family: $font-header
|
||||
|
||||
|
||||
.NewCampaignForm-Description
|
||||
min-height: 100px
|
||||
|
||||
.NewCampaignForm-Submit
|
||||
margin-top: 20px
|
|
@ -5,6 +5,8 @@ import { useHistory } from 'react-router-dom';
|
|||
|
||||
import './NewCampaignForm.sass';
|
||||
|
||||
import { Label, Text } from '../input/Input';
|
||||
|
||||
import Button from '../Button';
|
||||
|
||||
import * as Format from '../../../../common/Format';
|
||||
|
@ -40,20 +42,17 @@ export default function NewCampaignForm() {
|
|||
|
||||
return (
|
||||
<div class='NewCampaignForm'>
|
||||
<label>
|
||||
<span>Campaign Title</span>
|
||||
<input class='NewCampaignForm-Title' value={title}
|
||||
onChange={(e: any) => setTitle(e.target.value )} />
|
||||
</label>
|
||||
<label>
|
||||
<span>Campaign Description</span>
|
||||
<textarea
|
||||
rows={3} placeholder='Lorem ipsum dolor sit amet...'
|
||||
class='NewCampaignForm-Description' value={description}
|
||||
onChange={(e: any) => setDescription(e.target.value)} />
|
||||
</label>
|
||||
<h2 class='NewCampaignForm-Title'>New Campaign</h2>
|
||||
|
||||
<Button class='NewCampaignForm-Submit' onClick={createCampaign} icon='add' label='Create Campaign' disabled={!title} />
|
||||
<Label label='Campaign Title'>
|
||||
<Text value={title} setValue={setTitle} />
|
||||
</Label>
|
||||
|
||||
<Label label='Campaign Description'>
|
||||
<Text class='NewCampaignForm-Description' long={true} value={description} setValue={setDescription} />
|
||||
</Label>
|
||||
|
||||
<Button class='NewCampaignForm-Submit' onClick={createCampaign} icon='add' label='Create' disabled={!title} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/text'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.NewMapForm
|
||||
padding: 16px
|
||||
padding-top: 72px
|
||||
|
||||
.NewMapForm-Title
|
||||
@include text.line_clamp
|
||||
|
||||
margin-top: 0
|
||||
margin-bottom: 8px
|
||||
padding-right: 6px
|
||||
|
||||
font-weight: 500
|
||||
font-size: 34px
|
||||
font-family: $font-header
|
||||
|
||||
.NewMapForm-Card
|
||||
@extend %card
|
||||
|
||||
width: 100%
|
||||
max-width: 1000px
|
||||
display: block
|
||||
margin: 0 auto
|
||||
|
||||
|
||||
.NewMapForm-Description
|
||||
min-height: 100px
|
||||
|
||||
.NewMapForm-Submit
|
||||
margin-top: 20px
|
|
@ -0,0 +1,59 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { useAppData } from '../../Hooks';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
|
||||
import './NewMapForm.sass';
|
||||
|
||||
import { Label, Text } from '../input/Input';
|
||||
|
||||
import Button from '../Button';
|
||||
|
||||
export default function NewMapForm() {
|
||||
const [ ,, mergeData ] = useAppData();
|
||||
const history = useHistory();
|
||||
const { id: campaign } = useParams<{ id: string }>();
|
||||
|
||||
const [ queryState, setQueryState ] = useState<'idle' | 'querying'>('idle');
|
||||
|
||||
const [ name, setName ] = useState<string>('');
|
||||
const [ description, setDescription ] = useState<string>('');
|
||||
|
||||
const createMap = async () => {
|
||||
if (queryState !== 'idle') return;
|
||||
setQueryState('querying');
|
||||
|
||||
const res = await fetch('/data/map/new', {
|
||||
method: 'POST', cache: 'no-cache',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ campaign, name, description })
|
||||
});
|
||||
|
||||
setQueryState('idle');
|
||||
|
||||
if (res.status !== 200) console.error(await res.text());
|
||||
else {
|
||||
const data = await res.json();
|
||||
await mergeData(data);
|
||||
history.push(`/campaign/${campaign}/maps`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class='NewMapForm'>
|
||||
<div class='NewMapForm-Card'>
|
||||
<h2 class='NewMapForm-Title'>New Map</h2>
|
||||
|
||||
<Label label='Map Title'>
|
||||
<Text value={name} setValue={setName} />
|
||||
</Label>
|
||||
|
||||
<Label label='Map Description'>
|
||||
<Text class='NewMapForm-Description' long={true} value={description} setValue={setDescription} />
|
||||
</Label>
|
||||
|
||||
<Button class='NewMapForm-Submit' onClick={createMap} icon='add' label='Create' disabled={!name} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/slice'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.PlayerList
|
||||
@extend %card
|
||||
max-width: 1000px
|
||||
margin: 48px auto
|
||||
|
||||
.PlayerList-List
|
||||
list-style-type: none
|
||||
padding: 0
|
||||
margin: 0
|
||||
|
||||
.PlayerList-PlayerWrap
|
||||
@extend .slice_outline_neutral
|
||||
margin-bottom: 6px
|
||||
|
||||
.PlayerList-Player
|
||||
@include slice.slice_invert
|
||||
padding: 9px
|
||||
display: grid
|
||||
grid-gap: 12px
|
||||
grid-template-columns: #{18px * 5} 1fr
|
||||
|
||||
.PlayerList-PlayerImageWrap
|
||||
@extend .slice_highlight_neutral
|
||||
|
||||
width: 18px * 5
|
||||
height: 18px * 5
|
||||
|
||||
.PlayerList-PlayerImage
|
||||
@include slice.slice_invert
|
||||
|
||||
background-size: #{18px * 5 * 2} #{18px * 5 * 2}
|
||||
background-position: top left
|
||||
image-rendering: pixelated
|
||||
|
||||
.PlayerList-PlayerName
|
||||
font-size: 30px
|
||||
margin: 8px 0
|
|
@ -0,0 +1,33 @@
|
|||
import * as Preact from 'preact';
|
||||
|
||||
import './PlayerList.sass';
|
||||
|
||||
interface Player {
|
||||
name: string;
|
||||
sprite: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
players: Player[];
|
||||
}
|
||||
|
||||
export default function PlayerList({ players }: Props) {
|
||||
return (
|
||||
<div class='PlayerList'>
|
||||
<ul class='PlayerList-List'>
|
||||
{players.map(p =>
|
||||
<li class='PlayerList-PlayerWrap'>
|
||||
<div class='PlayerList-Player'>
|
||||
<div class='PlayerList-PlayerImageWrap'>
|
||||
<div class='PlayerList-PlayerImage' style={{ backgroundImage: `url('${p.sprite}')` }}/>
|
||||
</div>
|
||||
<div class='PlayerList-PlayerDetails'>
|
||||
<h3 class='PlayerList-PlayerName'>{p.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { Asset } from './util/Asset';
|
||||
import * as DB from '../../../common/DBStructs';
|
||||
|
||||
export interface ExternalData {
|
||||
map: string;
|
||||
campaign: string;
|
||||
}
|
||||
|
||||
export default interface EditorData extends ExternalData {
|
||||
assets: Asset[];
|
||||
data: DB.Map;
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import Phaser from 'phaser';
|
||||
|
||||
import { ExternalData } from './EditorData';
|
||||
import * as Scene from './scene/Scenes';
|
||||
|
||||
export default function create(root: HTMLElement) {
|
||||
export default function create(root: HTMLElement, data: ExternalData) {
|
||||
const bounds = root.getBoundingClientRect();
|
||||
|
||||
return new Phaser.Game({
|
||||
const game = new Phaser.Game({
|
||||
title: 'Editor',
|
||||
|
||||
parent: root,
|
||||
|
@ -25,4 +26,7 @@ export default function create(root: HTMLElement) {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
game.scene.start('InitScene', data);
|
||||
return game;
|
||||
}
|
||||
|
|
|
@ -78,16 +78,16 @@ export default class MapChunk {
|
|||
|
||||
let wallTile = this.map.getTile(Layer.wall, mX, mY);
|
||||
if (this.map.getTileset(Layer.wall, mX, mY) === -1 || (wallTile < 54 || wallTile > 60)) {
|
||||
this.canvas.drawFrame(this.map.manager.groundLocations[this.map.getTileset(Layer.floor, mX, mY)].key,
|
||||
this.canvas.drawFrame(this.map.manager.groundLocations[this.map.getTileset(Layer.floor, mX, mY)].identifier,
|
||||
this.map.getTile(Layer.floor, mX, mY), x * MapChunk.TILE_SIZE, y * MapChunk.TILE_SIZE);
|
||||
|
||||
if (this.map.getTileset(Layer.overlay, mX, mY) !== -1)
|
||||
this.canvas.drawFrame(this.map.manager.overlayLocations[this.map.getTileset(Layer.overlay, mX, mY)].key,
|
||||
this.canvas.drawFrame(this.map.manager.overlayLocations[this.map.getTileset(Layer.overlay, mX, mY)].identifier,
|
||||
this.map.getTile(Layer.overlay, mX, mY), x * MapChunk.TILE_SIZE, y * MapChunk.TILE_SIZE);
|
||||
}
|
||||
|
||||
if (this.map.getTileset(Layer.wall, mX, mY) !== -1)
|
||||
this.canvas.drawFrame(this.map.manager.wallLocations[this.map.getTileset(Layer.wall, mX, mY)].key,
|
||||
this.canvas.drawFrame(this.map.manager.wallLocations[this.map.getTileset(Layer.wall, mX, mY)].identifier,
|
||||
this.map.getTile(Layer.wall, mX, mY), x * MapChunk.TILE_SIZE, y * MapChunk.TILE_SIZE);
|
||||
|
||||
if ((x % 2 === 0 && y % 2 === 0) || (x % 2 !== 0 && y % 2 !== 0))
|
||||
|
|
|
@ -91,6 +91,9 @@ export default class MapData {
|
|||
for (let j = clamp(y - 1, this.size.y - 1, 0); j <= clamp(y + 1, this.size.y - 1, 0); j++) {
|
||||
const solids = this.getTilesetsAt(Layer.wall, i, j).map(i => i !== -1);
|
||||
|
||||
// const bits = SmartTiler.bitsToIndices(solids);
|
||||
// if (i === x && j === y) console.log(bits, Math.floor(bits / 16), bits % 16);
|
||||
|
||||
const wall = SmartTiler.wall(solids, this.getTile(Layer.wall, i, j));
|
||||
if (wall !== -1) this.setTileRaw(Layer.wall, i, j, wall);
|
||||
|
||||
|
|
|
@ -1,323 +1,68 @@
|
|||
const wallField = [
|
||||
33, 33, 6, 6, 33, 33, 6, 6, 5, 5, 10, 17, 5, 5, 10, 17, 15, 15, 9, 9, 15, 15, 16, 16, 2, 2, 4, 50, 2, 2, 49, 32,
|
||||
33, 33, 6, 6, 33, 33, 6, 6, 5, 5, 10, 17, 5, 5, 10, 17, 15, 15, 9, 9, 15, 15, 16, 16, 2, 2, 4, 50, 2, 2, 49, 32,
|
||||
14, 14, 11, 11, 14, 14, 11, 11, 1, 1, 13, 48, 1, 1, 13, 48, 0, 0, 12, 12, 0, 0, 47, 47, 3, 3, 25, 46, 3, 3, 45, 20,
|
||||
14, 14, 11, 11, 14, 14, 11, 11, 8, 8, 39, 23, 8, 8, 39, 23, 0, 0, 12, 12, 0, 0, 47, 47, 41, 41, 37, 29, 41, 41, 51, 18,
|
||||
33, 33, 6, 6, 33, 33, 6, 6, 5, 5, 10, 17, 5, 5, 10, 17, 15, 15, 9, 9, 15, 15, 16, 16, 2, 2, 4, 50, 2, 2, 49, 32,
|
||||
33, 33, 6, 6, 33, 33, 6, 6, 5, 5, 10, 17, 5, 5, 10, 17, 15, 15, 9, 9, 15, 15, 16, 16, 2, 2, 4, 50, 2, 2, 49, 32,
|
||||
14, 14, 11, 11, 14, 14, 11, 11, 1, 1, 13, 48, 1, 1, 13, 48, 7, 7, 38, 38, 7, 7, 22, 22, 40, 40, 36, 42, 40, 40, 30, 19,
|
||||
14, 14, 11, 11, 14, 14, 11, 11, 8, 8, 39, 23, 8, 8, 39, 23, 7, 7, 38, 38, 7, 7, 22, 22, 31, 31, 21, 27, 31, 31, 28, 54
|
||||
];
|
||||
|
||||
const floorField = [
|
||||
54, 20, 19, 19, 18, 4, 19, 19, 11, 11, 3, 3, 51, 51, 3, 3, 9, 52, 5, 5, 9, 52, 5, 5, 39, 39, 30, 30, 39, 39, 30, 30,
|
||||
2, 12, 32, 32, 34, 6, 32, 32, 11, 11, 3, 3, 51, 51, 3, 3, 43, 38, 29, 29, 43, 38, 29, 29, 39, 39, 30, 30, 39, 39, 30, 30,
|
||||
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49,
|
||||
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49,
|
||||
0, 33, 31, 31, 14, 7, 31, 31, 42, 42, 27, 27, 36, 36, 27, 27, 9, 52, 5, 5, 9, 52, 5, 5, 39, 39, 30, 30, 39, 39, 30, 30,
|
||||
22, 15, 28, 28, 16, 37, 28, 28, 42, 42, 27, 27, 36, 36, 27, 27, 43, 38, 29, 29, 43, 38, 29, 29, 39, 39, 30, 30, 39, 39, 30, 30,
|
||||
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49,
|
||||
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49
|
||||
];
|
||||
|
||||
export function bitsToIndices(walls: boolean[]) {
|
||||
return (
|
||||
(+walls[0] << 0) +
|
||||
(+walls[1] << 1) +
|
||||
(+walls[2] << 2) +
|
||||
(+walls[3] << 3) +
|
||||
(+walls[5] << 4) +
|
||||
(+walls[6] << 5) +
|
||||
(+walls[7] << 6) +
|
||||
(+walls[8] << 7));
|
||||
}
|
||||
|
||||
export function wall(walls: boolean[], current: number): number {
|
||||
const TL = 0, T = 1, TR = 2, L = 3, R = 5, BL = 6, B = 7, BR = 8;
|
||||
|
||||
if (current === -1) return -1;
|
||||
|
||||
let empty = walls.map(b => !b);
|
||||
let tile = 54;
|
||||
|
||||
if (empty[T]) {
|
||||
if (empty[B]) {
|
||||
if (empty[L]) {
|
||||
if (empty[R]) tile = 33;
|
||||
else tile = 15;
|
||||
}
|
||||
else if (empty[R]) tile = 5;
|
||||
else tile = 2;
|
||||
}
|
||||
else if (empty[L]) {
|
||||
if (empty[R]) tile = 14;
|
||||
else if (empty[BR]) tile = 0;
|
||||
else tile = 7;
|
||||
}
|
||||
else if (empty[R]) {
|
||||
if (empty[BL]) tile = 1;
|
||||
else tile = 8;
|
||||
}
|
||||
else {
|
||||
if (empty[BL]) {
|
||||
if (empty[BR]) tile = 3;
|
||||
else tile = 40;
|
||||
}
|
||||
else if (empty[BR]) tile = 41;
|
||||
else tile = 31;
|
||||
}
|
||||
}
|
||||
else if (empty[B]) {
|
||||
if (empty[L]) {
|
||||
if (empty[R]) tile = 6;
|
||||
else if (empty[TR]) tile = 9;
|
||||
else tile = 16;
|
||||
}
|
||||
else if (empty[R]) {
|
||||
if (empty[TL]) tile = 10;
|
||||
else tile = 17;
|
||||
}
|
||||
else {
|
||||
if (empty[TL]) {
|
||||
if (empty[TR]) tile = 4;
|
||||
else tile = 49;
|
||||
}
|
||||
else if (empty[TR]) tile = 50;
|
||||
else tile = 32;
|
||||
}
|
||||
}
|
||||
else if (empty[L]) {
|
||||
if (empty[R]) tile = 11;
|
||||
else {
|
||||
if (empty[TR]) {
|
||||
if (empty[BR]) tile = 12;
|
||||
else tile = 38;
|
||||
}
|
||||
else if (empty[BR]) tile = 47;
|
||||
else tile = 22;
|
||||
}
|
||||
}
|
||||
else if (empty[R]) {
|
||||
if (empty[TL]) {
|
||||
if (empty[BL]) tile = 13;
|
||||
else tile = 39;
|
||||
}
|
||||
else if (empty[BL]) tile = 48;
|
||||
else tile = 23;
|
||||
}
|
||||
else if (empty[TL]) {
|
||||
if (empty[TR]) {
|
||||
if (empty[BL]) {
|
||||
if (empty[BR]) tile = 25;
|
||||
else tile = 36;
|
||||
}
|
||||
else if (empty[BR]) tile = 37;
|
||||
else tile = 21;
|
||||
}
|
||||
else if (empty[BL]) {
|
||||
if (empty[BR]) tile = 45;
|
||||
else tile = 30;
|
||||
}
|
||||
else if (empty[BR]) tile = 51;
|
||||
else tile = 28;
|
||||
}
|
||||
else if (empty[TR]) {
|
||||
if (empty[BL]) {
|
||||
if (empty[BR]) tile = 46;
|
||||
else tile = 42;
|
||||
}
|
||||
else if (empty[BR]) tile = 29;
|
||||
else tile = 27;
|
||||
}
|
||||
else if (empty[BL]) {
|
||||
if (empty[BR]) tile = 20;
|
||||
else tile = 19;
|
||||
}
|
||||
else if (empty[BR]) tile = 18;
|
||||
else {
|
||||
if (current >= 54 && current <= 60) return -1;
|
||||
tile = 54 + Math.floor(Math.random() * 6);
|
||||
}
|
||||
|
||||
return tile;
|
||||
const ind = wallField[bitsToIndices(walls)];
|
||||
if (ind < 54) return ind;
|
||||
return 54 + Math.floor(Math.random() * 6);
|
||||
}
|
||||
|
||||
export function overlay(overlays: boolean[], current: number): number {
|
||||
const TL = 0, T = 1, TR = 2, L = 3, R = 5, BL = 6, B = 7, BR = 8;
|
||||
|
||||
if (current === -1) return -1;
|
||||
|
||||
let empty = overlays.map(b => !b);
|
||||
let tile = 54;
|
||||
|
||||
if (empty[T]) {
|
||||
if (empty[B]) {
|
||||
if (empty[L]) {
|
||||
if (empty[R]) tile = 33;
|
||||
else tile = 15;
|
||||
}
|
||||
else if (empty[R]) tile = 5;
|
||||
else tile = 2;
|
||||
}
|
||||
else if (empty[L]) {
|
||||
if (empty[R]) tile = 14;
|
||||
else if (empty[BR]) tile = 0;
|
||||
else tile = 7;
|
||||
}
|
||||
else if (empty[R]) {
|
||||
if (empty[BL]) tile = 1;
|
||||
else tile = 8;
|
||||
}
|
||||
else {
|
||||
if (empty[BL]) {
|
||||
if (empty[BR]) tile = 3;
|
||||
else tile = 40;
|
||||
}
|
||||
else if (empty[BR]) tile = 41;
|
||||
else tile = 31;
|
||||
}
|
||||
}
|
||||
else if (empty[B]) {
|
||||
if (empty[L]) {
|
||||
if (empty[R]) tile = 6;
|
||||
else if (empty[TR]) tile = 9;
|
||||
else tile = 16;
|
||||
}
|
||||
else if (empty[R]) {
|
||||
if (empty[TL]) tile = 10;
|
||||
else tile = 17;
|
||||
}
|
||||
else {
|
||||
if (empty[TL]) {
|
||||
if (empty[TR]) tile = 4;
|
||||
else tile = 49;
|
||||
}
|
||||
else if (empty[TR]) tile = 50;
|
||||
else tile = 32;
|
||||
}
|
||||
}
|
||||
else if (empty[L]) {
|
||||
if (empty[R]) tile = 11;
|
||||
else {
|
||||
if (empty[TR]) {
|
||||
if (empty[BR]) tile = 12;
|
||||
else tile = 38;
|
||||
}
|
||||
else if (empty[BR]) tile = 47;
|
||||
else tile = 22;
|
||||
}
|
||||
}
|
||||
else if (empty[R]) {
|
||||
if (empty[TL]) {
|
||||
if (empty[BL]) tile = 13;
|
||||
else tile = 39;
|
||||
}
|
||||
else if (empty[BL]) tile = 48;
|
||||
else tile = 23;
|
||||
}
|
||||
else if (empty[TL]) {
|
||||
if (empty[TR]) {
|
||||
if (empty[BL]) {
|
||||
if (empty[BR]) tile = 25;
|
||||
else tile = 36;
|
||||
}
|
||||
else if (empty[BR]) tile = 37;
|
||||
else tile = 21;
|
||||
}
|
||||
else if (empty[BL]) {
|
||||
if (empty[BR]) tile = 45;
|
||||
else tile = 30;
|
||||
}
|
||||
else if (empty[BR]) tile = 51;
|
||||
else tile = 28;
|
||||
}
|
||||
else if (empty[TR]) {
|
||||
if (empty[BL]) {
|
||||
if (empty[BR]) tile = 46;
|
||||
else tile = 42;
|
||||
}
|
||||
else if (empty[BR]) tile = 29;
|
||||
else tile = 27;
|
||||
}
|
||||
else if (empty[BL]) {
|
||||
if (empty[BR]) tile = 20;
|
||||
else tile = 19;
|
||||
}
|
||||
else if (empty[BR]) tile = 18;
|
||||
else {
|
||||
if (current >= 54 && current <= 60) return -1;
|
||||
tile = 54 + Math.floor(Math.random() * 6);
|
||||
}
|
||||
|
||||
return tile;
|
||||
const ind = floorField[bitsToIndices(overlays)];
|
||||
if (ind < 54) return ind;
|
||||
return 54 + Math.floor(Math.random() * 6);
|
||||
}
|
||||
|
||||
export function floor(walls: boolean[], current: number): number {
|
||||
const TL = 0, T = 1, TR = 2, L = 3, C = 4, R = 5, BL = 6, B = 7, BR = 8;
|
||||
|
||||
if (current === -1) return -1;
|
||||
|
||||
let tile = 10;
|
||||
|
||||
if (walls[C]) tile = 10;
|
||||
else if (walls[B]) {
|
||||
if (walls[T]) {
|
||||
if (walls[R]) {
|
||||
if (walls[L]) tile = 49;
|
||||
else tile = 26;
|
||||
}
|
||||
else if (walls[L]) tile = 8;
|
||||
else tile = 17;
|
||||
}
|
||||
else if (walls[L]) {
|
||||
if (walls[R]) tile = 48;
|
||||
else if (walls[TR]) tile = 45;
|
||||
else tile = 21;
|
||||
}
|
||||
else if (walls[R]) {
|
||||
if (walls[TL]) tile = 47;
|
||||
else tile = 23;
|
||||
}
|
||||
else if (walls[TL]) {
|
||||
if (walls[TR]) tile = 46;
|
||||
else tile = 41;
|
||||
}
|
||||
else if (walls[TR]) tile = 40;
|
||||
else tile = 1;
|
||||
}
|
||||
else if (walls[T]) {
|
||||
if (walls[L]) {
|
||||
if (walls[R]) tile = 30;
|
||||
else if (walls[BR]) tile = 27;
|
||||
else tile = 3;
|
||||
}
|
||||
else if (walls[R]) {
|
||||
if (walls[BL]) tile = 29;
|
||||
else tile = 5;
|
||||
}
|
||||
else if (walls[BL]) {
|
||||
if (walls[BR]) tile = 28;
|
||||
else tile = 32;
|
||||
}
|
||||
else if (walls[BR]) tile = 31;
|
||||
else tile = 19;
|
||||
}
|
||||
else if (walls[L]) {
|
||||
if (walls[R]) tile = 39;
|
||||
else if (walls[TR]) {
|
||||
if (walls[BR]) tile = 36;
|
||||
else tile = 51;
|
||||
}
|
||||
else if (walls[BR]) tile = 42;
|
||||
else tile = 11;
|
||||
}
|
||||
else if (walls[R]) {
|
||||
if (walls[TL]) {
|
||||
if (walls[BL]) tile = 38;
|
||||
else tile = 52;
|
||||
}
|
||||
else if (walls[BL]) tile = 43;
|
||||
else tile = 9;
|
||||
}
|
||||
else if (walls[TL]) {
|
||||
if (walls[TR]) {
|
||||
if (walls[BL]) {
|
||||
if (walls[BR]) tile = 37;
|
||||
else tile = 6;
|
||||
}
|
||||
else if (walls[BR]) tile = 7;
|
||||
else tile = 4;
|
||||
}
|
||||
else if (walls[BL]) {
|
||||
if (walls[BR]) tile = 15;
|
||||
else tile = 12;
|
||||
}
|
||||
else if (walls[BR]) tile = 33;
|
||||
else tile = 20;
|
||||
}
|
||||
else if (walls[TR]) {
|
||||
if (walls[BL]) {
|
||||
if (walls[BR]) tile = 16;
|
||||
else tile = 34;
|
||||
}
|
||||
else if (walls[BR]) tile = 14;
|
||||
else tile = 18;
|
||||
}
|
||||
else if (walls[BL]) {
|
||||
if (walls[BR]) tile = 22;
|
||||
else tile = 2;
|
||||
}
|
||||
else if (walls[BR]) tile = 0;
|
||||
else {
|
||||
if (current >= 54 && current <= 60) return -1;
|
||||
tile = 54 + Math.floor(Math.random() * 6);
|
||||
}
|
||||
|
||||
return tile;
|
||||
const ind = floorField[bitsToIndices(walls)];
|
||||
if (ind < 54) return ind;
|
||||
return 54 + Math.floor(Math.random() * 6);
|
||||
}
|
||||
|
||||
// const l = [];
|
||||
// for (let i = 0; i < (1 << 8); ++i ) {
|
||||
// let arr = [ i & 0x001, i & 0x002, i & 0x004, i & 0x008, 0,
|
||||
// i & 0x010, i & 0x020, i & 0x040, i & 0x080 ].map(a => !!a);
|
||||
// l.push(overlay(arr, 1));
|
||||
// }
|
||||
|
||||
// let lines = [];
|
||||
// for (let i = 0; i < 16; i++) {
|
||||
// lines.push(l.slice(i * 16, (i + 1) * 16).map(n => n < 10 ? n + ', ' : n + ',').join(' '));
|
||||
// }
|
||||
|
||||
// console.log(lines.join('\n'));
|
||||
|
|