Added dragons, more robust joining, hid architect mode from players, dynamic lighting!!
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 701 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 708 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 702 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 706 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 703 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 703 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 705 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 685 B |
|
@ -15,6 +15,9 @@ export { default as Label } from './InputLabel';
|
|||
export { default as Divider } from './InputDivider';
|
||||
export { default as Annotation } from './InputAnnotation';
|
||||
|
||||
export { default as Button } from './fields/InputButton';
|
||||
export { default as SelectRow, InputSelectRowItem as SelectRowItem } from './fields/InputSelectRow';
|
||||
|
||||
export { default as Text } from './fields/InputText';
|
||||
// export { default as Color } from './fields/InputColor';
|
||||
// export { default as Select } from './fields/InputSelect';
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
@use '../../../style/ext'
|
||||
@use '../../../style/def' as *
|
||||
|
||||
.InputButton
|
||||
padding: 14px 16px
|
||||
|
||||
outline: 0
|
||||
border: none
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
border-radius: 4px
|
||||
position: relative
|
||||
text-decoration: none
|
||||
background-color: $accent-700
|
||||
|
||||
transition: background-color $t-fast, color $t-fast
|
||||
|
||||
&.AltLabel
|
||||
padding: 16px
|
||||
|
||||
font-weight: 500
|
||||
font-size: 14px
|
||||
letter-spacing: 1px
|
||||
text-transform: uppercase
|
||||
|
||||
&:not(.Disabled)
|
||||
&:hover, &:focus-visible
|
||||
background-color: $accent-600
|
||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.15)
|
||||
|
||||
&:active
|
||||
background-color: $accent-500
|
||||
box-shadow: 0px 2px 12px 0 rgba(0, 0, 0, 0.2)
|
||||
|
||||
&.AltColor
|
||||
background-color: $neutral-300
|
||||
|
||||
&:not(.Disabled)
|
||||
&:hover, &:focus-visible
|
||||
background-color: $neutral-400
|
||||
|
||||
&:active
|
||||
background-color: $neutral-500
|
||||
|
||||
|
||||
&.Disabled
|
||||
pointer-events: none
|
||||
background-color: $neutral-250
|
||||
color: $neutral-600
|
||||
|
||||
|
||||
// $curve: cubic-bezier(0.1, 0.43, 0.43, 1.02)
|
||||
|
||||
// &::after
|
||||
// content: " "
|
||||
// display: block
|
||||
// position: absolute
|
||||
// user-select: none
|
||||
// pointer-events: none
|
||||
|
||||
// // Compensate for 1px border.
|
||||
// top: -1px
|
||||
// left: -1px
|
||||
// right: -1px
|
||||
// bottom: -1px
|
||||
|
||||
// margin: 4px
|
||||
// transform: scale(0.87)
|
||||
|
||||
// border-radius: inherit
|
||||
// background: transparentize($neutral-400, 1)
|
||||
|
||||
// transition: background $t-med $curve, transform $t-slow $curve $t-ufast, margin $t-slow $curve
|
||||
|
||||
// &:not(:disabled)
|
||||
// &:hover, &:focus, &:focus-within
|
||||
// &::after
|
||||
// margin: 0px
|
||||
// transform: scale(1)
|
||||
// background: transparentize($neutral-400, 1 - .15)
|
||||
|
||||
// transition: background $t-fast $curve, transform $t-fast $curve, margin $t-fast $curve
|
||||
|
||||
// &:active
|
||||
// transition: border-color $t-fast
|
||||
|
||||
// &:focus, &:focus-within
|
||||
// border-color: $neutral-400
|
||||
|
||||
// &:disabled
|
||||
// cursor: auto
|
||||
// opacity: 0.65
|
||||
// color: $neutral-500
|
|
@ -0,0 +1,41 @@
|
|||
import * as Preact from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import './InputButton.sass';
|
||||
|
||||
interface Props {
|
||||
[key: string]: any;
|
||||
|
||||
to?: string;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
|
||||
// icon?: string;
|
||||
label?: string;
|
||||
altLabel?: boolean;
|
||||
altColor?: boolean;
|
||||
children?: Preact.ComponentChildren;
|
||||
}
|
||||
|
||||
/**
|
||||
* A clean button that can be a link or a true button.
|
||||
*/
|
||||
|
||||
const InputButton = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props>((props, ref) => {
|
||||
props.class = ('InputButton ' + (props.altLabel ? 'AltLabel ' : '') + (props.altColor ? 'AltColor ' : '')
|
||||
+ (props.disabled ? 'Disabled ' : '') + props.class ?? '').trim();
|
||||
|
||||
if (props.to) {
|
||||
props.className = props.class;
|
||||
return (
|
||||
<NavLink ref={ref as any} {...props as any}>{props.label}{props.children}</NavLink>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<button ref={ref as any} {...props}>{props.label}{props.children}</button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default InputButton;
|
|
@ -0,0 +1,65 @@
|
|||
@use '../../../style/ext'
|
||||
@use '../../../style/def' as *
|
||||
|
||||
.InputSelectRow
|
||||
display: inline-block
|
||||
position: relative
|
||||
|
||||
padding: 6px
|
||||
border-radius: 4px
|
||||
background-color: $neutral-250
|
||||
|
||||
.InputSelectRow-Highlight
|
||||
top: 6px
|
||||
z-index: 0
|
||||
position: absolute
|
||||
|
||||
height: 36px
|
||||
border-radius: 4px
|
||||
background-color: $accent-600
|
||||
|
||||
$bez: cubic-bezier(0.33, 0.65, 0.62, 1.11)
|
||||
transition: left $t-fast $bez, width $t-fast $bez, transform $t-ufast, color $t-fast
|
||||
|
||||
&:active
|
||||
.InputSelectRow-Highlight
|
||||
transform: scale(0.9)
|
||||
background-color: $accent-500
|
||||
|
||||
&.Disabled
|
||||
.InputSelectRow-Highlight
|
||||
background-color: $neutral-400
|
||||
|
||||
.InputSelectRowItem
|
||||
position: relative
|
||||
|
||||
padding: 10px
|
||||
margin-right: 16px
|
||||
|
||||
outline: 0
|
||||
border: none
|
||||
font-size: 14px
|
||||
font-weight: 500
|
||||
user-select: none
|
||||
border-radius: 4px
|
||||
letter-spacing: 1px
|
||||
color: $neutral-800
|
||||
text-transform: uppercase
|
||||
background-color: transparent
|
||||
|
||||
transition: color $t-fast, background-color $t-fast
|
||||
|
||||
&:last-child
|
||||
margin-right: 0
|
||||
|
||||
&:focus-visible
|
||||
background-color: transparentize($neutral-600, 0.8)
|
||||
|
||||
&:disabled
|
||||
color: $neutral-600
|
||||
|
||||
&:active, &.Selected
|
||||
color: $neutral-1000
|
||||
|
||||
&:disabled
|
||||
color: $neutral-900
|
|
@ -0,0 +1,84 @@
|
|||
import * as Preact from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useState, useEffect, useRef, useContext } from 'preact/hooks';
|
||||
|
||||
import './InputSelectRow.sass';
|
||||
|
||||
import { WidgetProps } from '../Input';
|
||||
|
||||
interface InputSelectRowContextData {
|
||||
selected: string;
|
||||
onSelect: (name: string) => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const InputSelectRowContext = Preact.createContext<InputSelectRowContextData>(
|
||||
{ selected: '', onSelect: () => { /* Nothing */ }, disabled: false });
|
||||
|
||||
interface ItemProps {
|
||||
[key: string]: any;
|
||||
|
||||
label: string;
|
||||
name: any;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
export const InputSelectRowItem = forwardRef<HTMLButtonElement, ItemProps>((props, ref) => {
|
||||
const ctx = useContext(InputSelectRowContext);
|
||||
|
||||
return (
|
||||
<button ref={ref} {...props} onClick={() => ctx.onSelect(props.name)} data-name={props.name}
|
||||
class={'InputSelectRowItem ' + (props.name === ctx.selected ? 'Selected' : '')} disabled={ctx.disabled} >
|
||||
{props.label}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
interface Props extends WidgetProps {
|
||||
children: Preact.ComponentChildren;
|
||||
}
|
||||
|
||||
/**
|
||||
* A row of buttons, where a single one can be selected.
|
||||
*/
|
||||
|
||||
const InputSelectRow = forwardRef<HTMLDivElement, Props>((props, fRef) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [ ctx, setCtx ] = useState<InputSelectRowContextData>(
|
||||
{ selected: props.value, onSelect: props.setValue, disabled: props.disabled ?? false });
|
||||
|
||||
const [ highlight, setHighlight ] = useState<{ left: number; width: number }>({ left: 0, width: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
setCtx(ctx => ({ ...ctx, selected: props.value, onSelect: props.setValue, disabled: props.disabled ?? false }));
|
||||
}, [ props.value, props.setValue, props.disabled ]);
|
||||
|
||||
const handleRef = (e: HTMLDivElement | null) => {
|
||||
ref.current = e!;
|
||||
if (fRef) fRef.current = e!;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const wrap = ref.current.getBoundingClientRect();
|
||||
const button = (ref.current.querySelector(`[data-name='${props.value}']`) as any).getBoundingClientRect();
|
||||
|
||||
setHighlight({ left: button.left - wrap.left, width: button.width });
|
||||
}, [ props.value ]);
|
||||
|
||||
return (
|
||||
<InputSelectRowContext.Provider value={ctx}>
|
||||
<div ref={handleRef} class={('InputSelectRow ' + (props.disabled ? 'Disabled ' : '') + (props.class ?? '')).trim()}>
|
||||
<div class='InputSelectRow-Highlight' style={{ left: highlight.left + 'px', width: highlight.width + 'px' }} />
|
||||
{props.children}
|
||||
</div>
|
||||
</InputSelectRowContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export default InputSelectRow;
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
width: 100%
|
||||
padding: 12px
|
||||
transition: color $t-fast
|
||||
|
||||
&.Long
|
||||
resize: none
|
||||
|
@ -14,4 +15,4 @@
|
|||
font-family: monospace
|
||||
|
||||
&[disabled]
|
||||
color: $neutral-400
|
||||
color: $neutral-500
|
||||
|
|
|
@ -1,19 +1,32 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
import { useAppData } from '../../Hooks';
|
||||
import { NavLink as Link, Switch, Route, Redirect, useParams } from 'react-router-dom';
|
||||
import { NavLink as Link, Switch, Route, Redirect, useParams, useHistory } from 'react-router-dom';
|
||||
|
||||
import './AssetCollectionPage.sass';
|
||||
|
||||
import AssetPage from './AssetPage';
|
||||
import AssetList from '../view/AssetList';
|
||||
import NewAssetForm from '../view/NewAssetForm';
|
||||
import AssetUploader from '../view/AssetUploader';
|
||||
|
||||
import { Asset } from '../../../../common/DBStructs';
|
||||
|
||||
export default function AssetCollectionPage() {
|
||||
const history = useHistory();
|
||||
const [ { assets, collections } ] = useAppData([ 'assets', 'collections' ]);
|
||||
if (!collections || !assets) return null;
|
||||
|
||||
const { collection: coll } = useParams<{ user: string; collection: string }>();
|
||||
const collection = (collections ?? []).filter(c => c.identifier === coll)[0];
|
||||
|
||||
const displayedAssets: Asset[] = useMemo(() => {
|
||||
if (!collections || !assets || !collection) return [];
|
||||
return assets.filter(a => collection.items.includes(a.user + ':' + a.identifier))
|
||||
.sort((a, b) => a.identifier < b.identifier ? -1 : 1);
|
||||
}, [ collections, collection, assets ]);
|
||||
|
||||
if (!assets || !collections) return null;
|
||||
|
||||
|
||||
if (!collections) return <Redirect to='/a/' />;
|
||||
|
||||
return (
|
||||
|
@ -32,27 +45,29 @@ export default function AssetCollectionPage() {
|
|||
</aside>
|
||||
<main class='Page-Main'>
|
||||
<div class='AssetCollectionPage-Top'>
|
||||
<div class='AssetCollectionPage-TopBackgroundImage' style={{ backgroundImage: 'url(https://www.minecraft.net/content/dam/minecraft/java-snapshots/1-15-main-folder/19w40a/header.png)' }} />
|
||||
<div class='AssetCollectionPage-TopBackgroundImage' style={{ backgroundImage:
|
||||
'url(https://www.minecraft.net/content/dam/minecraft/java-snapshots/1-15-main-folder/19w40a/header.png)' }} />
|
||||
<div class='AssetCollectionPage-TopBackdropBlur' />
|
||||
<div class='AssetCollectionPage-HeaderWrap'>
|
||||
<div class='AssetCollectionPage-Header'>
|
||||
<h2>{collection.name}</h2>
|
||||
<p>{collection.identifier !== '_' ?
|
||||
collection.description :
|
||||
'This is a special collection containing all assets you\'ve uploaded to Virtual Dungeon. ' +
|
||||
'Deleting assets here will remove them from your account entirely, clearing them from any ' +
|
||||
'other collections they may be a part of.'}</p>
|
||||
collection.description :
|
||||
'This is a special collection containing all assets you\'ve uploaded to Virtual Dungeon. ' +
|
||||
'Deleting assets here will remove them from your account entirely, clearing them from any ' +
|
||||
'other collections they may be a part of.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='Page-Padding-Small'>
|
||||
<Switch>
|
||||
<Route exact path='/u/:user/a/:collection'>
|
||||
<AssetList assets={assets.filter(a => collection.items.includes(a.user + ':' + a.identifier))} />
|
||||
<AssetList assets={displayedAssets} onClick={(_, i) => history.push(i)} />
|
||||
</Route>
|
||||
<Route path='/u/:user/a/:collection/:asset'>
|
||||
<NewAssetForm/>
|
||||
<Route path='/u/:user/a/:collection/upload'>
|
||||
<AssetUploader/>
|
||||
</Route>
|
||||
<Route path='/u/:user/a/:collection/:asset' component={AssetPage} />
|
||||
</Switch>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useAppData } from '../../Hooks';
|
||||
import { Switch, Route, Redirect, useParams, useHistory } from 'react-router-dom';
|
||||
import { Redirect, useParams, useHistory } from 'react-router-dom';
|
||||
|
||||
import Button from '../Button';
|
||||
|
||||
|
@ -9,8 +9,8 @@ export default function AssetRoute() {
|
|||
if (!assets) return null;
|
||||
|
||||
const history = useHistory();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const currentAsset = (assets ?? []).filter(a => a.identifier === id)[0];
|
||||
const { asset } = useParams<{ asset: string }>();
|
||||
const currentAsset = (assets ?? []).filter(a => a.identifier === asset)[0];
|
||||
|
||||
if (!currentAsset) return <Redirect to='/assets/' />;
|
||||
|
||||
|
@ -18,10 +18,10 @@ export default function AssetRoute() {
|
|||
fetch('/data/asset/delete', {
|
||||
method: 'POST', cache: 'no-cache',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ identifier: id })
|
||||
body: JSON.stringify({ identifier: asset })
|
||||
});
|
||||
|
||||
history.push('/assets');
|
||||
history.push('../');
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -30,12 +30,8 @@ export default function AssetRoute() {
|
|||
<h2 class='Page-SidebarTitle'>{currentAsset.name}</h2>
|
||||
</aside>
|
||||
<main class='AssetRoute-Main'>
|
||||
<Switch>
|
||||
<Route>
|
||||
<img src={'/app/asset/' + currentAsset.path} role='presentational' alt=''/>
|
||||
<Button onClick={handleDeleteAsset} label='Delete'/>
|
||||
</Route>
|
||||
</Switch>
|
||||
<img src={'/app/asset/' + currentAsset.path} role='presentational' alt=''/>
|
||||
<Button onClick={handleDeleteAsset} label='Delete'/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,63 +3,54 @@
|
|||
@use '../../style/def' as *
|
||||
|
||||
.AssetList
|
||||
max-width: 1000px
|
||||
max-width: 1200px
|
||||
display: block
|
||||
margin: 0 auto
|
||||
|
||||
.AssetList-Grid
|
||||
@include grid.auto_width(160px, 6px)
|
||||
@include grid.auto_width(16px * 8, 8px)
|
||||
|
||||
.AssetList-AssetWrap
|
||||
.AssetList-Asset
|
||||
height: 0
|
||||
width: 100%
|
||||
display: grid
|
||||
padding: 0
|
||||
position: relative
|
||||
|
||||
outline: 0
|
||||
border: none
|
||||
overflow: hidden
|
||||
user-select: none
|
||||
border-radius: 4px
|
||||
padding-bottom: 100%
|
||||
text-decoration: none
|
||||
background: transparent
|
||||
background-color: transparent
|
||||
|
||||
.AssetList-AssetInner
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
.AssetPreview
|
||||
padding: 8px
|
||||
background-color: $neutral-150
|
||||
|
||||
transition: background-color $t-fast
|
||||
|
||||
.AssetList-AssetTitle
|
||||
@include text.line_clamp
|
||||
|
||||
margin: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
display: grid
|
||||
grid-template-rows: 1fr auto
|
||||
|
||||
background-color: $neutral-100
|
||||
|
||||
.AssetList-AssetPreview
|
||||
position: relative
|
||||
padding: 8px
|
||||
overflow: hidden
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
user-select: none
|
||||
object-fit: contain
|
||||
pointer-events: none
|
||||
image-rendering: crisp-edges
|
||||
image-rendering: pixelated
|
||||
// padding: 2px 8px 8px 8px
|
||||
padding: 10px 0 8px 0
|
||||
|
||||
font-size: 16px
|
||||
text-align: left
|
||||
border-radius: 4px
|
||||
color: $neutral-700
|
||||
transition: color $t-fast
|
||||
|
||||
&:hover, &:focus-visible
|
||||
.AssetList-AssetTitle
|
||||
@include text.line_clamp
|
||||
|
||||
margin: 0
|
||||
padding: 12px
|
||||
|
||||
font-size: 18px
|
||||
border-radius: 4px
|
||||
background-color: $neutral-150
|
||||
font-weight: 500
|
||||
color: $neutral-900
|
||||
|
||||
.AssetPreview
|
||||
background-color: $neutral-250
|
||||
|
||||
.AssetList-NewAsset
|
||||
width: 100%
|
||||
|
|
|
@ -2,6 +2,8 @@ import * as Preact from 'preact';
|
|||
|
||||
import './AssetList.sass';
|
||||
|
||||
import AssetPreview from './AssetPreview';
|
||||
|
||||
import { Asset } from '../../../../common/DBStructs';
|
||||
|
||||
interface Props {
|
||||
|
@ -34,9 +36,7 @@ export default function AssetList({ assets, newText, onNew, onClick }: Props) {
|
|||
{assets.map(a => <li class='AssetList-AssetWrap'>
|
||||
<button class='AssetList-Asset' onClick={() => onClick?.(a.user, a.identifier)}>
|
||||
<div class='AssetList-AssetInner'>
|
||||
<div class='AssetList-AssetPreview'>
|
||||
<img src={'/app/asset/' + a.path} role='presentational' alt='' loading='lazy'/>
|
||||
</div>
|
||||
<AssetPreview type={a.type} tokenType={(a as any).tokenType} path={`/app/asset/${a.path}`} animate='hover' />
|
||||
<p class='AssetList-AssetTitle'>{a.name || 'Untitled'}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.AssetPreview
|
||||
display: inline-block
|
||||
width: 100%
|
||||
padding: 8px
|
||||
border-radius: 4px
|
||||
background-color: $neutral-250
|
||||
|
||||
.AssetPreview-Inner
|
||||
display: grid
|
||||
width: 100%
|
||||
aspect-ratio: 1
|
||||
background-size: 0px
|
||||
|
||||
& > div
|
||||
image-rendering: pixelated
|
||||
background-image: var(--bg)
|
||||
background-repeat: no-repeat
|
||||
background-size: 100%
|
||||
|
||||
&.Tile
|
||||
grid-template-rows: repeat(3, minmax(0, 1fr))
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr))
|
||||
|
||||
& > div
|
||||
min-height: 0
|
||||
background-size: #{7 * 100.5%} #{2 * 100.5%}
|
||||
|
||||
&.wall > div, &.detail > div
|
||||
&:nth-child(1)
|
||||
background-position: #{100% * (2/6)} 0
|
||||
&:nth-child(2)
|
||||
background-position: #{100% * (1/6)} 0
|
||||
&:nth-child(3)
|
||||
background-position: #{100% * (3/6)} 0
|
||||
|
||||
&:nth-child(4)
|
||||
background-position: 0 0
|
||||
&:nth-child(5)
|
||||
background-position: #{100% * (4/6)} 100%
|
||||
&:nth-child(6)
|
||||
background-position: #{100% * (1/6)} 100%
|
||||
|
||||
&:nth-child(7)
|
||||
background-position: #{100% * (2/6)} 100%
|
||||
&:nth-child(8)
|
||||
background-position: 0 100%
|
||||
&:nth-child(9)
|
||||
background-position: #{100% * (3/6)} 100%
|
||||
|
||||
&.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)}
|
||||
|
||||
&.Token
|
||||
|
||||
@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%
|
||||
|
||||
@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%
|
||||
|
||||
&.Slice4 div
|
||||
background-size: 200% 200%
|
||||
animation: Token-Anim-4 1000000000s infinite
|
||||
animation-play-state: paused
|
||||
|
||||
&.Slice8 div
|
||||
animation: Token-Anim-8 1000000000s infinite
|
||||
background-size: 300% 300%
|
||||
animation-play-state: paused
|
||||
|
||||
|
||||
&.Anim, &.AnimHover:hover
|
||||
&.Slice4 div
|
||||
animation: Token-Anim-4 2.5s infinite
|
||||
animation-play-state: running
|
||||
|
||||
&.Slice8 div
|
||||
animation: Token-Anim-8 5s infinite
|
||||
animation-play-state: running
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import * as Preact from 'preact';
|
||||
|
||||
import './AssetPreview.sass';
|
||||
|
||||
interface TileProps {
|
||||
type: 'wall' | 'floor' | 'detail';
|
||||
}
|
||||
|
||||
interface TokenProps {
|
||||
type: 'token';
|
||||
tokenType: 1 | 4 | 8;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
path: string;
|
||||
animate?: false | true | 'hover';
|
||||
} & (TileProps | TokenProps);
|
||||
|
||||
export default function AssetPreview(props: Props) {
|
||||
const animClass = props.animate === true ? 'Anim' : props.animate === 'hover' ? 'AnimHover' : '';
|
||||
return (
|
||||
<div class='AssetPreview'>
|
||||
{props.type === 'token' ?
|
||||
<div class={'AssetPreview-Inner Token Slice' + (props.tokenType || 4).toString() + ' ' + animClass}>
|
||||
<div style={{ backgroundImage: `url(${props.path})` }} />
|
||||
</div> :
|
||||
<div class={'AssetPreview-Inner Tile ' + props.type} style={{ '--bg': `url(${props.path})` } as any}>
|
||||
<div /><div /><div />
|
||||
<div /><div /><div />
|
||||
<div /><div /><div />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.AssetUploadForm
|
||||
@extend %card
|
||||
padding-top: 12px
|
||||
display: block
|
||||
margin: 0 auto
|
||||
|
||||
.AssetUploadForm-Col2
|
||||
display: grid
|
||||
grid-gap: 16px
|
||||
grid-template-columns: 1fr 1fr
|
||||
|
||||
.InputSelectRow
|
||||
display: block
|
||||
|
||||
.AssetUploadForm-ActionRow
|
||||
gap: 8px
|
||||
display: flex
|
||||
margin-top: 20px
|
||||
|
||||
.AssetUploadForm-AssetPreview
|
||||
display: flex
|
||||
gap: 12px
|
||||
margin-top: 12px
|
||||
height: 16px * 3 * 5
|
||||
flex-direction: row
|
||||
|
||||
.AssetUploadForm-ImagePreview
|
||||
flex-grow: 1
|
||||
|
||||
padding: 8px
|
||||
border-radius: 4px
|
||||
background-color: $neutral-250
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
object-fit: contain
|
||||
image-rendering: crisp-edges
|
||||
image-rendering: pixelated
|
||||
|
||||
.AssetPreview
|
||||
width: 16px * 3 * 5
|
||||
|
||||
.AssetUploadForm-Info
|
||||
margin: 0
|
||||
padding: 0
|
||||
margin: 12px 0 6px 0
|
||||
|
||||
color: $neutral-600
|
||||
font-size: (14 / 16) * 1em
|
|
@ -0,0 +1,117 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useState, useRef, useEffect } from 'preact/hooks';
|
||||
|
||||
import './AssetUploadForm.sass';
|
||||
|
||||
import AssetPreview from './AssetPreview';
|
||||
import { Label, Text, Numeric, Button, SelectRow, SelectRowItem } from '../input/Input';
|
||||
|
||||
import * as Format from '../../../../common/Format';
|
||||
import { UploadData } from '../../../../common/DBStructs';
|
||||
|
||||
interface Props {
|
||||
file: File;
|
||||
preview: string;
|
||||
|
||||
uploading: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: (data: UploadData) => void;
|
||||
}
|
||||
|
||||
export default function AssetUploadForm({ file, preview, uploading, onCancel, onSubmit }: Props) {
|
||||
const firstInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [ data, setData ] = useState<UploadData>({
|
||||
name: '', identifier: '',
|
||||
type: 'token', tokenType: 4, tileSize: { x: 1, y: 1 }
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setData({ ...data,
|
||||
name: Format.name(file.name),
|
||||
identifier: ''
|
||||
});
|
||||
firstInputRef.current?.focus();
|
||||
}, [ file ]);
|
||||
|
||||
const handleSetType = (type: 'wall' | 'floor' | 'detail' | 'token') => {
|
||||
const newData = { ...data, type: type } as any;
|
||||
|
||||
if (newData.type === 'token') {
|
||||
// Delete token-specific data from the object.
|
||||
newData.tokenType = 4;
|
||||
newData.tileSize = { x: 4, y: 4 };
|
||||
}
|
||||
else {
|
||||
// Delete tile-specific data from the object.
|
||||
delete newData.tokenType;
|
||||
delete newData.tileSize;
|
||||
}
|
||||
|
||||
setData(newData);
|
||||
};
|
||||
|
||||
const handleSetIdentifier = (identifier: string) => {
|
||||
setData({ ...data, identifier: Format.identifier(identifier) });
|
||||
};
|
||||
|
||||
return (
|
||||
<div class='AssetUploadForm'>
|
||||
<Label label='Asset Type' />
|
||||
<SelectRow value={data.type} setValue={handleSetType} disabled={uploading}>
|
||||
<SelectRowItem label='Token' name='token' />
|
||||
<SelectRowItem label='Floor' name='floor' />
|
||||
<SelectRowItem label='Detail' name='detail' />
|
||||
<SelectRowItem label='Wall' name='wall' />
|
||||
</SelectRow>
|
||||
|
||||
<div class='AssetUploadForm-AssetPreview'>
|
||||
<div class='AssetUploadForm-ImagePreview'>
|
||||
<img src={preview ?? undefined} alt='' />
|
||||
</div>
|
||||
<AssetPreview {...data} path={preview} animate />
|
||||
</div>
|
||||
<p class='AssetUploadForm-Info'>If the preview doesn't look correct,
|
||||
try changing the asset type{data.type === 'token' ? ' or sprite layout' : ''}.</p>
|
||||
|
||||
<div class='AssetUploadForm-Col2'>
|
||||
<Label label='Name'>
|
||||
<Text ref={firstInputRef} value={data.name} setValue={(name) => setData({ ...data, name})} disabled={uploading}/>
|
||||
</Label>
|
||||
|
||||
<Label label='Identifier'>
|
||||
<Text class='AssetUploadForm-Identifier' long={true}
|
||||
value={data.identifier} placeholder={Format.identifier(data.name)}
|
||||
setValue={handleSetIdentifier} disabled={uploading}/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{data.type === 'token' &&
|
||||
<div class='AssetUploadForm-Col2'>
|
||||
<div>
|
||||
<Label label='Sprite Layout' />
|
||||
<SelectRow value={data.tokenType} setValue={t => setData({ ...data, tokenType: t })} disabled={uploading}>
|
||||
<SelectRowItem label='1×1' name={1} />
|
||||
<SelectRowItem label='2×2' name={4} />
|
||||
<SelectRowItem label='3×3' name={8} />
|
||||
</SelectRow>
|
||||
</div>
|
||||
<Label label='Size In Tiles'>
|
||||
<Numeric value={data.tileSize.x} setValue={x => setData({ ...data, tileSize: { x, y: x }})} disabled={uploading} />
|
||||
</Label>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class='AssetUploadForm-ActionRow'>
|
||||
<Button class='AssetUploadForm-Submit' altLabel onClick={() => onSubmit(data)}
|
||||
icon='add' label='Upload Asset' disabled={uploading ||
|
||||
!(data.name.trim().length >= 3 &&
|
||||
((data.identifier.replace(/_/g, ' ').trim().length === 0
|
||||
&& Format.identifier(data.name.trim()).length >= 3)
|
||||
|| data.identifier.replace(/_/g, ' ').trim().length >= 3))} />
|
||||
<Button class='AssetUploadForm-Cancel' altLabel altColor onClick={onCancel}
|
||||
label='Cancel' disabled={uploading} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.AssetUploader
|
||||
max-width: $wrap-small
|
||||
display: block
|
||||
margin: 0 auto
|
||||
|
||||
.AssetUploader-ActionBar
|
||||
gap: 8px
|
||||
display: flex
|
||||
margin-top: 16px
|
||||
|
||||
.AssetUploader-UploadWrap
|
||||
@extend %material_button
|
||||
|
||||
display: flex
|
||||
height: 200px
|
||||
position: relative
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
cursor: pointer
|
||||
border-color: $neutral-300
|
||||
|
||||
.AssetUploader-Upload
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
outline: 0
|
||||
opacity: 0
|
||||
cursor: pointer
|
||||
|
||||
.AssetUploader-UploadTitle
|
||||
line-height: 1.6
|
||||
text-align: center
|
||||
color: $neutral-600
|
||||
font-size: (18 / 16) * 1rem
|
||||
|
||||
.AssetUploader-Progress
|
||||
position: relative
|
||||
margin-bottom: 24px
|
||||
width: 100%
|
||||
height: 30px
|
||||
|
||||
overflow: hidden
|
||||
border-radius: 4px
|
||||
background-color: $neutral-200
|
||||
|
||||
.AssetUploader-ProgressBar
|
||||
height: 100%
|
||||
border-radius: 0 4px 4px 0
|
||||
background-color: $accent-600
|
||||
|
||||
transition: width $t-fast
|
||||
|
||||
.AssetUploader-ProgressText
|
||||
position: absolute
|
||||
top: 6px
|
||||
left: 6px
|
||||
margin: 0
|
||||
|
||||
.AssetUploader-ResultsCard
|
||||
@extend %card
|
||||
padding-top: 12px
|
||||
display: block
|
||||
margin: 0 auto
|
||||
|
||||
.AssetUploader-ResultsStatus
|
||||
color: $neutral-800
|
||||
margin-bottom: 12px
|
|
@ -0,0 +1,116 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
import './AssetUploader.sass';
|
||||
|
||||
import { Button } from '../input/Input';
|
||||
import AssetUploadForm from './AssetUploadForm';
|
||||
|
||||
import { UploadData } from '../../../../common/DBStructs';
|
||||
|
||||
|
||||
/**
|
||||
* Handles uploading assets.
|
||||
*/
|
||||
|
||||
export default function AssetUploader() {
|
||||
const [ files, setFiles ] = useState<File[]>([]);
|
||||
const [ filePreview, setFilePreview ] = useState<string>('');
|
||||
const [ uploadStates, setUploadStates ] = useState<{ success: number; fail: number }>({ success: 0, fail: 0 });
|
||||
|
||||
const [ uploading, setUploading ] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length < 1) return;
|
||||
let set = true;
|
||||
setFilePreview('');
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(files[0]);
|
||||
|
||||
reader.onload = e => {
|
||||
if (set) setFilePreview((e.target as any).result);
|
||||
};
|
||||
|
||||
return () => set = false;
|
||||
}, [ files ]);
|
||||
|
||||
const handleReset = () => {
|
||||
setFiles([]);
|
||||
setUploadStates({ success: 0, fail: 0 });
|
||||
};
|
||||
|
||||
const handleFileSelect = (evt: any) => {
|
||||
const newFiles = (Array.from((evt.target as HTMLInputElement).files ?? []) as File[])
|
||||
.filter(f => f.type === 'image/png' || f.name.endsWith('.png'));
|
||||
setFiles(newFiles);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
const newFiles = [ ...files ];
|
||||
newFiles.splice(0, 1);
|
||||
setFiles(newFiles);
|
||||
setUploadStates(s => ({ ...s, fail: s.fail + 1 }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (data: UploadData) => {
|
||||
if (uploading) return;
|
||||
setUploading(true);
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append('file', files[0]);
|
||||
formData.append('data', JSON.stringify(data));
|
||||
|
||||
const res = await fetch('/data/asset/upload',
|
||||
{ method: 'POST', cache: 'no-cache', body: formData });
|
||||
|
||||
if (res.status === 200) {
|
||||
setUploadStates(s => ({ ...s, success: s.success + 1 }));
|
||||
}
|
||||
else {
|
||||
console.error(await res.text());
|
||||
setUploadStates(s => ({ ...s, fail: s.fail + 1 }));
|
||||
}
|
||||
|
||||
const newFiles = [ ...files ];
|
||||
newFiles.splice(0, 1);
|
||||
setFiles(newFiles);
|
||||
setUploading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class='AssetUploader'>
|
||||
{files.length === 0 && <Preact.Fragment>
|
||||
{uploadStates.success + uploadStates.fail === 0 && <Preact.Fragment>
|
||||
<div class='AssetUploader-UploadWrap'>
|
||||
<input type='file' id='fileUpload' class='AssetUploader-Upload' accept='.png' multiple onChange={handleFileSelect} />
|
||||
<label for='fileUpload' class='AssetUploader-UploadTitle'>Click or drag files here to upload!<br />
|
||||
<small>(Tip: You can select multiple assets at once to batch-upload)</small></label>
|
||||
</div>
|
||||
<div class='AssetUploader-ActionBar'>
|
||||
<Button label='Back to Assets' altLabel altColor to='../' />
|
||||
</div>
|
||||
</Preact.Fragment>}
|
||||
{uploadStates.success + uploadStates.fail > 0 && <Preact.Fragment>
|
||||
<div class='AssetUploader-ResultsCard'>
|
||||
<p class='AssetUploader-ResultsStatus'>Uploaded {uploadStates.success} asset{uploadStates.success !== 1 ? 's' : ''}.</p>
|
||||
<div class='AssetUploader-ActionBar'>
|
||||
<Button label='Upload More' altLabel onClick={handleReset} />
|
||||
<Button label='Back' altLabel altColor to='../' />
|
||||
</div>
|
||||
</div>
|
||||
</Preact.Fragment>}
|
||||
</Preact.Fragment>}
|
||||
|
||||
{/* {files.length > 1 && <div class='AssetUploader-Progress'>
|
||||
<div class='AssetUploader-ProgressBar' style={{ width: current / files.length * 100 + '%'}} />
|
||||
<p class='AssetUploader-ProgressText'>{`Asset ${current + 1} / ${files.length}`}</p>
|
||||
</div>}*/}
|
||||
|
||||
{files.length !== 0 && <div>
|
||||
<AssetUploadForm file={files[0]} preview={filePreview}
|
||||
uploading={uploading} onCancel={handleCancel} onSubmit={handleSubmit}/>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
@use '../../style/ext'
|
||||
@use '../../style/text'
|
||||
@use '../../style/slice'
|
||||
@use '../../style/def' as *
|
||||
|
||||
.NewAssetForm
|
||||
@extend %card
|
||||
max-width: 1000px
|
||||
display: block
|
||||
margin: 0 auto
|
||||
|
||||
.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-Col2-60
|
||||
@extend .NewAssetForm-Col2
|
||||
grid-template-columns: 5fr 4fr
|
||||
|
||||
& > div:nth-child(2)
|
||||
text-align: right
|
||||
|
||||
.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: crisp-edges
|
||||
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: crisp-edges
|
||||
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
|
|
@ -1,151 +0,0 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useAppData } from '../../Hooks';
|
||||
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 [ ,, mergeData ] = useAppData();
|
||||
|
||||
const [ queryState, setQueryState ] = useState<'idle' | 'querying'>('idle');
|
||||
|
||||
const [ type, setType ] = useState<'wall' | 'floor' | 'detail' | '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' | 'floor' | 'detail' | '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 uploadAsset = 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 === 200) {
|
||||
mergeData(await res.json());
|
||||
history.push('/a/');
|
||||
}
|
||||
else {
|
||||
console.error(await res.text());
|
||||
setQueryState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class='NewAssetForm'>
|
||||
<h2 class='NewAssetForm-Title'>New Asset</h2>
|
||||
|
||||
<div class='NewAssetForm-Col2-60'>
|
||||
<div>
|
||||
<Label label='Asset Type' />
|
||||
<ButtonGroup>
|
||||
<Button icon='token' label='Token' inactive={type !== 'token'} onClick={() => handleSetType('token')}/>
|
||||
<Button icon='detail' label='Detail' inactive={type !== 'detail'} onClick={() => handleSetType('detail')}/>
|
||||
<Button icon='floor' label='Floor' inactive={type !== 'floor'} onClick={() => handleSetType('floor')}/>
|
||||
<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={uploadAsset} icon='add' label={`Create ${Format.name(type)} Asset`}
|
||||
disabled={!(name.length > 3 && identifier.length > 3)} />
|
||||
</Preact.Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
import { Asset } from './util/Asset';
|
||||
// import { Asset } from './util/Asset';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import * as DB from '../../../common/DBStructs';
|
||||
|
||||
export default interface EditorData {
|
||||
socket: Socket;
|
||||
state: 'owner' | 'player';
|
||||
|
||||
campaign: DB.Campaign;
|
||||
assets: Asset[];
|
||||
map?: string;
|
||||
assets: DB.Asset[];
|
||||
campaign: DB.Campaign;
|
||||
|
||||
onDirty: (dirty: boolean) => void;
|
||||
onProgress: (progress: number | undefined) => void;
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as Phaser from 'phaser';
|
|||
|
||||
import { Vec2, Vec4 } from './util/Vec';
|
||||
|
||||
const PATCH_TIMING = true;
|
||||
const PATCH_TIMING = false;
|
||||
|
||||
/**
|
||||
* Patches a partial tileset into a full tileset by combining parts of other textures.
|
||||
|
@ -20,17 +20,17 @@ const PATCH_TIMING = true;
|
|||
export function tileset(scene: Phaser.Scene, tileset_key: string, tile_size: number) {
|
||||
const s = PATCH_TIMING ? Date.now() : 0;
|
||||
|
||||
const canvas = new Phaser.GameObjects.RenderTexture(scene, 0, 0, 10 * tile_size, 5 * tile_size);
|
||||
canvas.draw(tileset_key);
|
||||
const img = scene.textures.get(tileset_key).source[0].image as HTMLImageElement;
|
||||
scene.textures.removeKey(tileset_key);
|
||||
|
||||
let part: Phaser.GameObjects.Sprite | Phaser.GameObjects.RenderTexture
|
||||
= new Phaser.GameObjects.Sprite(scene, 0, 0, tileset_key, '__BASE');
|
||||
part.setOrigin(0, 0);
|
||||
const canvas = scene.textures.createCanvas(tileset_key, 10 * tile_size, 5 * tile_size);
|
||||
canvas.draw(0, 0, img);
|
||||
|
||||
const ctx = canvas.getContext();
|
||||
function draw(source: Vec4, dest: Vec2) {
|
||||
part.setCrop(source.x * tile_size, source.y * tile_size, (source.z - source.x) * tile_size, (source.w - source.y) * tile_size);
|
||||
part.setPosition((dest.x - source.x) * tile_size, (dest.y - source.y) * tile_size);
|
||||
canvas.draw(part);
|
||||
const data = ctx.getImageData(source.x * tile_size, source.y * tile_size,
|
||||
(source.z - source.x) * tile_size, (source.w - source.y) * tile_size);
|
||||
ctx.putImageData(data, dest.x * tile_size, dest.y * tile_size);
|
||||
}
|
||||
|
||||
// End Pieces and Walls
|
||||
|
@ -80,24 +80,6 @@ export function tileset(scene: Phaser.Scene, tileset_key: string, tile_size: num
|
|||
draw(new Vec4(6, 0.5, 6.5, 1), new Vec2(0, 4.5));
|
||||
draw(new Vec4(5.5, 0.5, 6, 1), new Vec2(0.5, 4.5));
|
||||
|
||||
/*
|
||||
* So here's why this is horrible:
|
||||
* - Phaser doesn't let you copy a region of a RenderTexture to the same RenderTexture.
|
||||
* - Creating a new RenderTexture and drawing the original directly onto it flips it upside down for some reason?
|
||||
* - Scaling that upside-down RenderTexture to upside-right fucks with the draw() function's positioning.
|
||||
*
|
||||
* In other words, yeah, it's fucked man. The janky solution below is the only way I've found to make it work,
|
||||
* so if future-Auri is looking at this and making a snarky comment to her friends about how she could do it
|
||||
* *so much better*, please, just don't. Don't do it.
|
||||
*/
|
||||
|
||||
part.setCrop();
|
||||
part = new Phaser.GameObjects.RenderTexture(scene, 0, 0, canvas.width, canvas.height);
|
||||
part.setOrigin(0, 0);
|
||||
const temp = new Phaser.GameObjects.Sprite(scene, 0, 0, canvas.texture);
|
||||
temp.setOrigin(0, 0);
|
||||
part.draw(temp);
|
||||
|
||||
// Derived Forms (Pink)
|
||||
|
||||
draw(new Vec4(2, 0, 4, 0.5), new Vec2(3, 2));
|
||||
|
@ -141,13 +123,46 @@ export function tileset(scene: Phaser.Scene, tileset_key: string, tile_size: num
|
|||
draw(new Vec4(1.5, 3, 2, 4), new Vec2(7.5, 4));
|
||||
draw(new Vec4(1, 3, 1.5, 3.5), new Vec2(7, 4));
|
||||
draw(new Vec4(6, 0.5, 6.5, 1), new Vec2(7, 4.5));
|
||||
|
||||
const tex = canvas.saveTexture(tileset_key);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let j = 0; j < 10; j++) {
|
||||
tex.add(j + i * 10, 0, j * tile_size, tile_size * 5 - (i + 1) * tile_size, tile_size, tile_size);
|
||||
}
|
||||
}
|
||||
|
||||
// for (let i = 0; i < canvas.width; i++) {
|
||||
// for (let j = 0; j < canvas.height; j++) {
|
||||
// const data = ctx.getImageData(i, j, 1, 1).data;
|
||||
// let color = false;
|
||||
// for (let k = 0; k < 3; k++) if (data[k] !== 0) {
|
||||
// color = true;
|
||||
// break;
|
||||
// }
|
||||
// if (color) ctx.clearRect(i, j, 1, 1);
|
||||
// }
|
||||
// }
|
||||
|
||||
canvas.refresh();
|
||||
// console.log(scene.textures.get(tileset_key));
|
||||
|
||||
for (let i = 0; i < 5; i++)
|
||||
for (let j = 0; j < 10; j++)
|
||||
canvas.add(j + i * 10, 0, j * tile_size, i * tile_size, tile_size, tile_size);
|
||||
|
||||
if (PATCH_TIMING) console.log(`Patched Tileset '${tileset_key}' in ${Date.now() - s} ms.`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
|
||||
export function floor(scene: Phaser.Scene, tileset_key: string, tile_size: number) {
|
||||
const s = PATCH_TIMING ? Date.now() : 0;
|
||||
|
||||
const img = scene.textures.get(tileset_key).source[0].image as HTMLImageElement;
|
||||
scene.textures.removeKey(tileset_key);
|
||||
|
||||
const canvas = scene.textures.createCanvas(tileset_key, 9 * tile_size, 7 * tile_size);
|
||||
canvas.draw(0, 0, img);
|
||||
canvas.refresh();
|
||||
|
||||
for (let j = 0; j < 9; j++)
|
||||
for (let i = 0; i < 7; i++)
|
||||
canvas.add(j + i * 9, 0, j * tile_size, i * tile_size, tile_size, tile_size);
|
||||
|
||||
if (PATCH_TIMING) console.log(`Patched Tileset '${tileset_key}' in ${Date.now() - s} ms.`);
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ export default class ActionManager {
|
|||
|
||||
case 'delete_token':
|
||||
item.tokens.forEach(t => this.map.tokens.createToken(t.uuid, t.layer,
|
||||
t.pos as any, {}, t.appearance.sprite, t.appearance.index));
|
||||
t.pos as any, {}, t.implicitScale, t.appearance.sprite, t.appearance.index));
|
||||
break;
|
||||
|
||||
case 'modify_token':
|
||||
|
@ -120,7 +120,7 @@ export default class ActionManager {
|
|||
|
||||
case 'place_token':
|
||||
item.tokens.forEach(t => this.map.tokens.createToken(t.uuid, t.layer,
|
||||
t.pos as any, {}, t.appearance.sprite, t.appearance.index));
|
||||
t.pos as any, {}, t.implicitScale, t.appearance.sprite, t.appearance.index));
|
||||
break;
|
||||
|
||||
case 'delete_token':
|
||||
|
|
|
@ -15,7 +15,7 @@ import InputManager from '../interact/InputManager';
|
|||
import ActionManager from '../action/ActionManager';
|
||||
import { ArchitectModeKey } from '../mode/ArchitectMode';
|
||||
|
||||
import { Asset } from '../util/Asset';
|
||||
import { Asset } from '../../../../common/DBStructs';
|
||||
|
||||
const UI_OFFSET = -10000;
|
||||
const CLOSED_SIDEBAR_OFFSET = -68;
|
||||
|
@ -25,7 +25,7 @@ export default class InterfaceRoot {
|
|||
|
||||
private scene: Phaser.Scene = null as any;
|
||||
|
||||
private lastMode: string = ArchitectModeKey;
|
||||
private lastMode: string = '';
|
||||
|
||||
private mode: ModeMananger = null as any;
|
||||
// private actions: ActionManager = null as any;
|
||||
|
@ -53,7 +53,7 @@ export default class InterfaceRoot {
|
|||
|
||||
this.root = this.scene.add.container(UI_OFFSET, 0);
|
||||
this.root.setName('root');
|
||||
this.leftRoot = this.scene.add.container(0, 0);
|
||||
this.leftRoot = this.scene.add.container(CLOSED_SIDEBAR_OFFSET, 0);
|
||||
this.root.add(this.leftRoot);
|
||||
|
||||
this.leftRoot.add(new SidebarToggler(scene, 49, 1000, input, this));
|
||||
|
@ -89,9 +89,11 @@ export default class InterfaceRoot {
|
|||
let uiHovered = testActive(this.root);
|
||||
this.inputManager.setContext(uiHovered ? 'interface' : 'map');
|
||||
|
||||
if (this.inputManager.keyPressed('TAB')) this.mode.activate(
|
||||
this.mode.getActive() === ArchitectModeKey ? TokenModeKey :
|
||||
this.mode.getActive() === TokenModeKey ? DrawModeKey : ArchitectModeKey);
|
||||
if (this.inputManager.keyPressed('TAB')) {
|
||||
const modes = this.mode.getModes();
|
||||
const currentInd = (modes.indexOf(this.mode.getActive()) + 1) % modes.length;
|
||||
this.mode.activate(modes[currentInd]);
|
||||
}
|
||||
|
||||
if (this.lastMode !== this.mode.getActive()) {
|
||||
this.lastMode = this.mode.getActive();
|
||||
|
@ -127,21 +129,7 @@ export default class InterfaceRoot {
|
|||
repeat: 0,
|
||||
|
||||
x: (open ? 0 : CLOSED_SIDEBAR_OFFSET)
|
||||
// {
|
||||
// from: (open ? CLOSED_SIDEBAR_OFFSET : 0),
|
||||
// to:
|
||||
// }
|
||||
});
|
||||
|
||||
// this.scene.tweens.add({
|
||||
// targets: [this.tileSidebar, this.tokenSidebar],
|
||||
// ease: 'Cubic',
|
||||
// duration: 225,
|
||||
// repeat: 0,
|
||||
|
||||
// // alpha: { from: (open ? 0 : 2.5), to: (open ? 1 : 0) }
|
||||
// alpha: (open ? 1 : 0)
|
||||
// });
|
||||
}
|
||||
|
||||
private displayArchitectMode() {
|
||||
|
@ -153,21 +141,12 @@ export default class InterfaceRoot {
|
|||
ease: 'Cubic',
|
||||
duration: 0,
|
||||
|
||||
// x: { from: CLOSED_SIDEBAR_OFFSET, to: 0 }
|
||||
alpha: { from: 0, to: 1 }
|
||||
});
|
||||
}
|
||||
|
||||
private hideArchitectMode() {
|
||||
if (!this.tileSidebar) return;
|
||||
// this.scene.tweens.add({
|
||||
// targets: this.tileSidebar,
|
||||
// ease: 'Cubic',
|
||||
// duration: 300,
|
||||
|
||||
// // x: { from: 0, to: CLOSED_SIDEBAR_OFFSET / 2 }
|
||||
// alpha: { from: 1, to: 0 }
|
||||
// });
|
||||
}
|
||||
|
||||
private displayTokenMode() {
|
||||
|
@ -179,20 +158,11 @@ export default class InterfaceRoot {
|
|||
ease: 'Cubic',
|
||||
duration: 0,
|
||||
|
||||
// x: { from: CLOSED_SIDEBAR_OFFSET, to: 0 }
|
||||
alpha: { from: 0, to: 1 }
|
||||
});
|
||||
}
|
||||
|
||||
private hideTokenMode() {
|
||||
if (!this.tokenSidebar) return;
|
||||
// this.scene.tweens.add({
|
||||
// targets: this.tokenSidebar,
|
||||
// ease: 'Cubic',
|
||||
// duration: 300,
|
||||
|
||||
// // x: { from: 0, to: CLOSED_SIDEBAR_OFFSET / 2 }
|
||||
// alpha: { from: 1, to: 0 }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,9 @@ import ModeManager from '../../mode/ModeManager';
|
|||
import ArchitectMode from '../../mode/ArchitectMode';
|
||||
import type InputManager from '../../interact/InputManager';
|
||||
|
||||
import { Asset } from '../../util/Asset';
|
||||
import { Asset } from '../../../../../common/DBStructs';
|
||||
|
||||
export default class TileSidebar extends Sidebar {
|
||||
|
||||
walls: string[] = [];
|
||||
floors: string[] = [];
|
||||
details: string[] = [];
|
||||
|
@ -55,15 +54,15 @@ export default class TileSidebar extends Sidebar {
|
|||
const controller = (this.mode.active as ArchitectMode).controller;
|
||||
if (!controller) return;
|
||||
if (y < 4) {
|
||||
controller.setActiveTile(this.map.tileStore.indices[this.walls[x + (y - 1) * 3]]);
|
||||
controller.setActiveTile(this.map.tileStore.getTile('wall', this.walls[x + (y - 1) * 3])!.ind);
|
||||
controller.setActiveTileType('wall');
|
||||
}
|
||||
else if (y < 8) {
|
||||
controller.setActiveTile(this.map.tileStore.indices[this.floors[x + (y - 5) * 3]]);
|
||||
controller.setActiveTile(this.map.tileStore.getTile('floor', this.floors[x + (y - 5) * 3])!.ind);
|
||||
controller.setActiveTileType('floor');
|
||||
}
|
||||
else {
|
||||
controller.setActiveTile(this.map.tileStore.indices[this.details[x + (y - 9) * 3]]);
|
||||
controller.setActiveTile(this.map.tileStore.getTile('detail', this.details[x + (y - 9) * 3])!.ind);
|
||||
controller.setActiveTileType('detail');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import TokenSlider from './TokenSlider';
|
|||
import Text from '../../../components/input/fields/InputText';
|
||||
import { TokenSliderData, TokenMetaData } from '../../map/token/Token';
|
||||
|
||||
import { Asset } from '../../util/Asset';
|
||||
import { Asset } from '../../../../../common/DBStructs';
|
||||
|
||||
interface TokenCardProps extends TokenMetaData {
|
||||
assets: Asset[];
|
||||
|
|
|
@ -9,7 +9,7 @@ import Map from '../../map/Map';
|
|||
import TokenCard from './TokenCard';
|
||||
import { TokenMetaData } from '../../map/token/Token';
|
||||
|
||||
import { Asset } from '../../util/Asset';
|
||||
import { Asset } from '../../../../../common/DBStructs';
|
||||
|
||||
interface Props {
|
||||
map: Map;
|
||||
|
|
|
@ -8,7 +8,7 @@ import ModeManager from '../../mode/ModeManager';
|
|||
import type InputManager from '../../interact/InputManager';
|
||||
|
||||
import { Vec2 } from '../../util/Vec';
|
||||
import { Asset } from '../../util/Asset';
|
||||
import { Asset } from '../../../../../common/DBStructs';
|
||||
|
||||
export default class TokenSidebar extends Sidebar {
|
||||
spinTimer: number = 0;
|
||||
|
@ -71,7 +71,7 @@ export default class TokenSidebar extends Sidebar {
|
|||
|
||||
if (x === 0) this.backgrounds[y].setFrame(0);
|
||||
|
||||
let token = new Token(this.scene, '', 50, new Vec2(4 + x * 21, 4 + y * 21), sprite);
|
||||
let token = new Token(this.scene, '', 50, new Vec2(4 + x * 21, 4 + y * 21), 1, sprite);
|
||||
token.setScale(16);
|
||||
this.sprites.push(token);
|
||||
this.add(token);
|
||||
|
|
|
@ -33,7 +33,7 @@ export default bind<Props>(function LayerManager(props: Props) {
|
|||
return () => props.actions.event.unbind(actionCb);
|
||||
}, [ props.actions ]);
|
||||
|
||||
const [ mode, setMode ] = useState<string>(ArchitectModeKey);
|
||||
const [ mode, setMode ] = useState<string>(props.mode.getActive());
|
||||
|
||||
useEffect(() => {
|
||||
const modeCb = (evt: ModeSwitchEvent) => {
|
||||
|
@ -51,8 +51,8 @@ export default bind<Props>(function LayerManager(props: Props) {
|
|||
return (
|
||||
<div class='Toolbar'>
|
||||
<ButtonGroup class='Toolbar-ModeSelector'>
|
||||
<Button icon='architect' alt='Build Map' noFocus={true}
|
||||
inactive={mode !== ArchitectModeKey} onClick={() => handleSetMode(ArchitectModeKey)} />
|
||||
{props.mode.hasMode(ArchitectModeKey) && <Button icon='architect' alt='Build Map' noFocus={true}
|
||||
inactive={mode !== ArchitectModeKey} onClick={() => handleSetMode(ArchitectModeKey)} />}
|
||||
<Button icon='token' alt='Manage Tokens' noFocus={true}
|
||||
inactive={mode !== TokenModeKey} onClick={() => handleSetMode(TokenModeKey)} />
|
||||
<Button icon='draw' alt='Draw Markup' noFocus={true}
|
||||
|
|
|
@ -1,90 +1,26 @@
|
|||
import * as Phaser from 'phaser';
|
||||
|
||||
import MapLayer from './MapLayer';
|
||||
import TileStore from './TileStore';
|
||||
import MapChunk, { TILE_SIZE, CHUNK_SIZE } from './MapChunk';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
|
||||
export const TILE_SIZE = 16;
|
||||
export const CHUNK_SIZE = 32;
|
||||
export const DIRTY_LIMIT = (CHUNK_SIZE * CHUNK_SIZE) / 2;
|
||||
|
||||
import * as Color from '../../../../common/Color';
|
||||
|
||||
/**
|
||||
* A highlight / checkerboard map overlay.
|
||||
*/
|
||||
|
||||
export default class HighlightMapChunk extends Phaser.GameObjects.RenderTexture {
|
||||
private dirtyList: Vec2[] = [];
|
||||
private fullyDirty: boolean = true;
|
||||
private tile: Phaser.GameObjects.Sprite;
|
||||
|
||||
constructor(scene: Phaser.Scene, private pos: Vec2, readonly layer: MapLayer) {
|
||||
super(scene, CHUNK_SIZE * pos.x - 2 / TILE_SIZE, CHUNK_SIZE * pos.y - 2 / TILE_SIZE,
|
||||
CHUNK_SIZE * TILE_SIZE + 4, CHUNK_SIZE * TILE_SIZE + 4);
|
||||
this.setScale(1 / TILE_SIZE);
|
||||
this.setOrigin(0, 0);
|
||||
|
||||
this.tile = new Phaser.GameObjects.Sprite(this.scene, 0, 0, '');
|
||||
this.tile.setVisible(false);
|
||||
this.tile.setOrigin(0);
|
||||
|
||||
scene.add.existing(this);
|
||||
export default class HighlightMapChunk extends MapChunk {
|
||||
constructor(scene: Phaser.Scene, pos: Vec2, layer: MapLayer, tileStore: TileStore) {
|
||||
super(scene, pos, layer, tileStore);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indicates that a position on the chunk is dirty so it will be re-rendered.
|
||||
*
|
||||
* @param {Vec2} pos - The position that is dirtied.
|
||||
* Updates the depth of the chunk based on the layer's index.
|
||||
*/
|
||||
|
||||
setDirty(pos: Vec2): void {
|
||||
if (!this.fullyDirty) {
|
||||
for (let v of this.dirtyList) if (v.equals(pos)) return;
|
||||
this.dirtyList.push(pos);
|
||||
|
||||
if (this.dirtyList.length > DIRTY_LIMIT) {
|
||||
this.fullyDirty = true;
|
||||
this.dirtyList = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Redraws all dirty tiles on the chunk.
|
||||
*
|
||||
* @returns {boolean} - A boolean indicating if tiles have changed since the last render.
|
||||
*/
|
||||
|
||||
redraw(): boolean {
|
||||
// const time = Date.now();
|
||||
|
||||
this.tile.setVisible(true);
|
||||
if (this.fullyDirty) {
|
||||
for (let i = 0; i < CHUNK_SIZE * CHUNK_SIZE; i++) {
|
||||
let x = i % CHUNK_SIZE;
|
||||
let y = Math.floor(i / CHUNK_SIZE);
|
||||
|
||||
if (x + this.pos.x * CHUNK_SIZE >= this.layer.size.x ||
|
||||
y + this.pos.y * CHUNK_SIZE >= this.layer.size.y) continue;
|
||||
|
||||
this.drawTile(x, y);
|
||||
}
|
||||
|
||||
this.fullyDirty = false;
|
||||
// console.log('MapChunk took', (Date.now() - time), 'ms');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.dirtyList.length === 0) return false;
|
||||
|
||||
for (let elem of this.dirtyList) this.drawTile(elem.x, elem.y);
|
||||
this.dirtyList = [];
|
||||
|
||||
// console.log('MapChunk took', (Date.now() - time), 'ms');
|
||||
this.tile.setVisible(false);
|
||||
return true;
|
||||
updateDepth() {
|
||||
this.setDepth(10000);
|
||||
}
|
||||
|
||||
|
||||
|
@ -96,21 +32,28 @@ export default class HighlightMapChunk extends Phaser.GameObjects.RenderTexture
|
|||
* @param {number} y - The y position to draw at.
|
||||
*/
|
||||
|
||||
private drawTile(x: number, y: number): void {
|
||||
protected drawTile(x: number, y: number): void {
|
||||
const pos = new Vec2(x + this.pos.x * CHUNK_SIZE, y + this.pos.y * CHUNK_SIZE);
|
||||
|
||||
let highlightTint = this.layer.getTile('wall', pos);
|
||||
let highlightIndex = this.layer.getTileIndex('wall', pos);
|
||||
let tint = this.layer.getTile('wall', pos);
|
||||
let index = this.layer.getTileIndex('wall', pos);
|
||||
|
||||
this.erase('erase_tile', x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
if (highlightTint > 0) {
|
||||
this.tile.setPosition(x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
this.tile.setTexture('user_highlight', highlightIndex);
|
||||
this.tile.setTint(highlightTint);
|
||||
this.draw(this.tile);
|
||||
this.chunkCtx.clearRect(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE, TILE_SIZE);
|
||||
if (tint > 0) {
|
||||
const user_ctx = (this.scene.textures.get('user_highlight') as Phaser.Textures.CanvasTexture).getContext();
|
||||
const data = user_ctx.getImageData((index % 10) * TILE_SIZE, Math.floor(index / 10) * TILE_SIZE, TILE_SIZE, TILE_SIZE);
|
||||
|
||||
this.tmpCtx.putImageData(data, 0, 0);
|
||||
this.tmpCtx.globalCompositeOperation = 'source-in';
|
||||
this.tmpCtx.fillStyle = Color.intToHex(tint);
|
||||
this.tmpCtx.fillRect(0, 0, TILE_SIZE, TILE_SIZE);
|
||||
this.tmpCtx.globalCompositeOperation = 'source-over';
|
||||
|
||||
this.chunkCtx.drawImage(this.tmp, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
}
|
||||
|
||||
|
||||
const grid_image = (this.scene.textures.get('grid_tile').source[0].image);
|
||||
if ((x % 2 === 0 && y % 2 === 0) || (x % 2 !== 0 && y % 2 !== 0))
|
||||
this.drawFrame('grid_tile', 0, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
this.chunkCtx.drawImage(grid_image, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,37 +3,43 @@ import * as Phaser from 'phaser';
|
|||
import MapLayer from './MapLayer';
|
||||
import TileStore from './TileStore';
|
||||
import * as MapSaver from './MapSaver';
|
||||
import Lighting from './lighting/Lighting';
|
||||
import TokenManager from './token/TokenManager';
|
||||
import MapChunk, { CHUNK_SIZE } from './MapChunk';
|
||||
import HighlightMapChunk from './HighlightMapChunk';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import { Asset } from '../util/Asset';
|
||||
import { Asset } from '../../../../common/DBStructs';
|
||||
|
||||
const MAX_ACCEPTABLE_DELAY = 250;
|
||||
|
||||
/**
|
||||
* Main map controller that manages the map data and chunks.
|
||||
*/
|
||||
|
||||
export default class Map {
|
||||
identifier: string = '';
|
||||
size: Vec2 = new Vec2(2, 2);
|
||||
tileStore: TileStore = new TileStore();
|
||||
identifier = '';
|
||||
size = new Vec2(2, 2);
|
||||
|
||||
tokens: TokenManager = new TokenManager();
|
||||
lighting = new Lighting();
|
||||
tileStore = new TileStore();
|
||||
tokens = new TokenManager();
|
||||
|
||||
private activeLayer?: MapLayer;
|
||||
private layers: MapLayer[] = [];
|
||||
private highlightLayer?: MapLayer;
|
||||
private activeLayer?: MapLayer = undefined;
|
||||
|
||||
private scene: Phaser.Scene = undefined as any;
|
||||
|
||||
private chunks: MapChunk[][][] = [];
|
||||
private highlights: HighlightMapChunk[][] = [];
|
||||
|
||||
init(scene: Phaser.Scene, assets: Asset[]) {
|
||||
this.scene = scene;
|
||||
|
||||
|
||||
this.tokens.init(scene);
|
||||
this.lighting.init(scene, this);
|
||||
this.tileStore.init(scene.textures, assets);
|
||||
}
|
||||
|
||||
|
@ -48,18 +54,19 @@ export default class Map {
|
|||
for (let layer of this.chunks) {
|
||||
for (let chunkRow of layer) {
|
||||
for (let chunk of chunkRow) {
|
||||
if (Date.now() - start > 4) return;
|
||||
if (Date.now() - start > MAX_ACCEPTABLE_DELAY) return;
|
||||
chunk.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let chunkRow of this.highlights) {
|
||||
for (let chunk of chunkRow) {
|
||||
if (Date.now() - start > 4) return;
|
||||
if (Date.now() - start > MAX_ACCEPTABLE_DELAY) return;
|
||||
chunk.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
this.lighting.update();
|
||||
}
|
||||
|
||||
|
||||
|
@ -219,7 +226,7 @@ export default class Map {
|
|||
for (let i = 0; i < Math.ceil(this.size.y / CHUNK_SIZE); i++) {
|
||||
this.highlights[i] = [];
|
||||
for (let j = 0; j < Math.ceil(this.size.x / CHUNK_SIZE); j++) {
|
||||
const chunk = new HighlightMapChunk(this.scene, new Vec2(j, i), this.highlightLayer);
|
||||
const chunk = new HighlightMapChunk(this.scene, new Vec2(j, i), this.highlightLayer, this.tileStore);
|
||||
this.highlights[i][j] = chunk;
|
||||
}
|
||||
}
|
||||
|
@ -263,6 +270,7 @@ export default class Map {
|
|||
*/
|
||||
|
||||
private handleDirty = (layerIndex: number, x: number, y: number) => {
|
||||
this.lighting.tileUpdatedAt(x, y);
|
||||
this.chunks[layerIndex][Math.floor(y / CHUNK_SIZE)][Math.floor(x / CHUNK_SIZE)].setDirty(new Vec2(x % CHUNK_SIZE, y % CHUNK_SIZE));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,33 +4,56 @@ import MapLayer from './MapLayer';
|
|||
import TileStore from './TileStore';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import { Layer } from '../util/Layer';
|
||||
|
||||
export const TILE_SIZE = 16;
|
||||
export const CHUNK_SIZE = 32;
|
||||
export const DIRTY_LIMIT = (CHUNK_SIZE * CHUNK_SIZE) / 2;
|
||||
export const DIRTY_LIMIT = (CHUNK_SIZE * CHUNK_SIZE) / 3;
|
||||
|
||||
const ORDER: Layer[] = [ 'floor', 'detail', 'wall' ];
|
||||
|
||||
|
||||
/**
|
||||
* A visual representation of a chunk of a MapLayer.
|
||||
* Creates a canvas for a MapChunk.
|
||||
*/
|
||||
|
||||
export default class MapChunk extends Phaser.GameObjects.RenderTexture {
|
||||
function createCanvas(textures: Phaser.Textures.TextureManager): Phaser.Textures.CanvasTexture {
|
||||
const size = new Vec2(CHUNK_SIZE * TILE_SIZE + 4, CHUNK_SIZE * TILE_SIZE + 4);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size.x;
|
||||
canvas.height = size.y;
|
||||
return textures.addCanvas('', canvas, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A visual chunk of a MapLayer.
|
||||
*/
|
||||
|
||||
export default class MapChunk extends Phaser.GameObjects.Image {
|
||||
protected chunk: Phaser.Textures.CanvasTexture;
|
||||
protected chunkCtx: CanvasRenderingContext2D;
|
||||
protected tmp: HTMLCanvasElement;
|
||||
protected tmpCtx: CanvasRenderingContext2D;;
|
||||
|
||||
private dirtyList: Vec2[] = [];
|
||||
private fullyDirty: boolean = true;
|
||||
private tile: Phaser.GameObjects.Sprite;
|
||||
|
||||
constructor(scene: Phaser.Scene, private pos: Vec2, readonly layer: MapLayer, private tileStore: TileStore) {
|
||||
super(scene, CHUNK_SIZE * pos.x - 2 / TILE_SIZE, CHUNK_SIZE * pos.y - 2 / TILE_SIZE,
|
||||
CHUNK_SIZE * TILE_SIZE + 4, CHUNK_SIZE * TILE_SIZE + 4);
|
||||
constructor(scene: Phaser.Scene, protected pos: Vec2, readonly layer: MapLayer, private tileStore: TileStore) {
|
||||
super(scene, CHUNK_SIZE * pos.x - 2 / TILE_SIZE, CHUNK_SIZE * pos.y - 2 / TILE_SIZE, createCanvas(scene.textures));
|
||||
this.scene.add.existing(this);
|
||||
this.setScale(1 / TILE_SIZE);
|
||||
this.setOrigin(0, 0);
|
||||
|
||||
this.chunk = (this.texture as Phaser.Textures.CanvasTexture);
|
||||
this.chunkCtx = this.chunk.getContext();
|
||||
|
||||
this.tmp = document.createElement('canvas');
|
||||
this.tmpCtx = this.tmp.getContext('2d')!;
|
||||
this.tmp.width = TILE_SIZE;
|
||||
this.tmp.height = TILE_SIZE;
|
||||
|
||||
this.updateDepth();
|
||||
|
||||
this.tile = new Phaser.GameObjects.Sprite(this.scene, 0, 0, '');
|
||||
this.tile.setVisible(false);
|
||||
this.tile.setOrigin(0);
|
||||
|
||||
scene.add.existing(this);
|
||||
}
|
||||
|
||||
|
||||
|
@ -79,32 +102,27 @@ export default class MapChunk extends Phaser.GameObjects.RenderTexture {
|
|||
*/
|
||||
|
||||
redraw(): boolean {
|
||||
if (this.dirtyList.length === 0 && !this.fullyDirty) return false;
|
||||
|
||||
// const time = Date.now();
|
||||
|
||||
this.tile.setVisible(true);
|
||||
if (this.fullyDirty) {
|
||||
this.fullyDirty = false;
|
||||
for (let i = 0; i < CHUNK_SIZE * CHUNK_SIZE; i++) {
|
||||
let x = i % CHUNK_SIZE;
|
||||
let y = Math.floor(i / CHUNK_SIZE);
|
||||
|
||||
if (x + this.pos.x * CHUNK_SIZE >= this.layer.size.x ||
|
||||
y + this.pos.y * CHUNK_SIZE >= this.layer.size.y) continue;
|
||||
|
||||
this.drawTile(x, y);
|
||||
}
|
||||
|
||||
this.fullyDirty = false;
|
||||
// console.log('MapChunk took', (Date.now() - time), 'ms');
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
for (let elem of this.dirtyList) this.drawTile(elem.x, elem.y);
|
||||
this.dirtyList = [];
|
||||
}
|
||||
|
||||
if (this.dirtyList.length === 0) return false;
|
||||
|
||||
for (let elem of this.dirtyList) this.drawTile(elem.x, elem.y);
|
||||
this.dirtyList = [];
|
||||
|
||||
this.chunk.refresh();
|
||||
// console.log('MapChunk took', (Date.now() - time), 'ms');
|
||||
this.tile.setVisible(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -117,38 +135,41 @@ export default class MapChunk extends Phaser.GameObjects.RenderTexture {
|
|||
* @param {number} y - The y position to draw at.
|
||||
*/
|
||||
|
||||
private drawTile(x: number, y: number): void {
|
||||
protected drawTile(x: number, y: number): void {
|
||||
const pos = new Vec2(x + this.pos.x * CHUNK_SIZE, y + this.pos.y * CHUNK_SIZE);
|
||||
|
||||
let wallTile = this.layer.getTile('wall', pos);
|
||||
let wallTileIndex = this.layer.getTileIndex('wall', pos);
|
||||
for (let i = 0; i < ORDER.length; i++) {
|
||||
const l = ORDER[i];
|
||||
|
||||
let floorTile = this.layer.getTile('floor', pos);
|
||||
let floorTileIndex = this.layer.getTileIndex('floor', pos);
|
||||
let tile = this.layer.getTile(l, pos);
|
||||
let index = this.layer.getTileIndex(l, pos);
|
||||
|
||||
let detailTile = this.layer.getTile('detail', pos);
|
||||
let detailTileIndex = this.layer.getTileIndex('detail', pos);
|
||||
if (tile > 0) {
|
||||
// The amount of columns in the tile patch image. 9 for floors, 10 for walls & details.
|
||||
const W = i === 0 ? 9 : 10;
|
||||
|
||||
if (floorTile > 0) {
|
||||
this.tile.setPosition(x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
this.tile.setTexture(this.tileStore.floorTiles[floorTile].identifier, floorTileIndex);
|
||||
this.draw(this.tile);
|
||||
}
|
||||
else {
|
||||
this.erase('erase_tile', x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
if (wallTile === 0 && detailTile === 0) return;
|
||||
}
|
||||
|
||||
if (detailTile > 0) {
|
||||
this.tile.setPosition(x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
this.tile.setTexture(this.tileStore.detailTiles[detailTile].identifier, detailTileIndex);
|
||||
this.draw(this.tile);
|
||||
}
|
||||
|
||||
if (wallTile > 0) {
|
||||
this.tile.setPosition(x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
this.tile.setTexture(this.tileStore.wallTiles[wallTile].identifier, wallTileIndex);
|
||||
this.draw(this.tile);
|
||||
const inf = this.tileStore.getTile(l, tile);
|
||||
if (!inf) {
|
||||
// console.error('Tile not found!', tile);
|
||||
continue;
|
||||
}
|
||||
const tex = this.scene.textures.get(inf.identifier);
|
||||
const ctx = (tex as Phaser.Textures.CanvasTexture).getContext();
|
||||
|
||||
const data = ctx.getImageData((index % W) * TILE_SIZE, Math.floor(index / W) * TILE_SIZE, TILE_SIZE, TILE_SIZE);
|
||||
if (i === 0) {
|
||||
// Don't need to use the temporary canvas for floors, as they should completely overwrite the pixels blow.
|
||||
this.chunkCtx.putImageData(data, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
}
|
||||
else {
|
||||
this.tmpCtx.putImageData(data, 0, 0);
|
||||
this.chunkCtx.drawImage(this.tmp, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
|
||||
}
|
||||
}
|
||||
else if (i === 0) {
|
||||
// Clear away the previous texture if no ground exists.
|
||||
this.chunkCtx.clearRect(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE, TILE_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import * as Phaser from 'phaser';
|
||||
|
||||
import { Layer } from '../util/Layer';
|
||||
import { Asset, AssetType } from '../util/Asset';
|
||||
import { Asset, TileAssetType } from '../../../../common/DBStructs';
|
||||
|
||||
interface TileInfo {
|
||||
res: number;
|
||||
ind: number;
|
||||
identifier: string;
|
||||
type: TileAssetType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,36 +14,45 @@ interface TileInfo {
|
|||
*/
|
||||
|
||||
export default class TileStore {
|
||||
indices: { [tileset_key: string]: number } = {};
|
||||
wallTiles: { [index: number]: TileInfo } = {};
|
||||
floorTiles: { [index: number]: TileInfo } = {};
|
||||
detailTiles: { [index: number]: TileInfo } = {};
|
||||
|
||||
private currentInd: { [layer in Layer]: number } = { wall: 1, floor: 1, detail: 1 };
|
||||
|
||||
private indices: { [ type in TileAssetType]: { [ identifier: string ]: number }} = { floor: {}, wall: {}, detail: {} };
|
||||
private tiles: { [ type in TileAssetType]: TileInfo[] } = { floor: [], wall: [], detail: [] };
|
||||
|
||||
/**
|
||||
* Initializes tilesets from a list of assets.
|
||||
*/
|
||||
|
||||
init(textures: Phaser.Textures.TextureManager, assets: Asset[]) {
|
||||
for (const tileset of assets.filter(a => a.type !== 'token'))
|
||||
this.addTileset(textures, tileset.type, tileset.identifier);
|
||||
for (const tile of assets.filter(a => a.type !== 'token'))
|
||||
this.addTile(textures, tile.type as TileAssetType, tile.identifier);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds the specified tileset to the map.
|
||||
* Gets the info for a tile by its index or identifier.
|
||||
* This method is O(1) regardless of the parameter type.
|
||||
*
|
||||
* @param {string | number} identifierOrIndex - The identifier or index of the tile to retrieve.
|
||||
* @returns a TileInfo instance if the tile exists, or undefined.
|
||||
*/
|
||||
|
||||
private addTileset(textures: Phaser.Textures.TextureManager, layer: AssetType, identifier: string): void {
|
||||
const ind = this.currentInd[layer as Layer]++;
|
||||
getTile(type: TileAssetType, identiferOrindex: string | number): TileInfo | undefined {
|
||||
if (typeof identiferOrindex === 'string') identiferOrindex = this.indices[type][identiferOrindex];
|
||||
return this.tiles[type][identiferOrindex - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified tile to the TileStore.
|
||||
*
|
||||
* @param {TextureManager} textures - The Phaser Texture Manager containing the asset.
|
||||
* @param {type} - The tile type that is being added.
|
||||
* @param {identifier} - The identifier of the tile.
|
||||
*/
|
||||
|
||||
private addTile(textures: Phaser.Textures.TextureManager, type: TileAssetType, identifier: string): void {
|
||||
const ind = this.tiles[type].length + 1;
|
||||
const res = textures.get(identifier).getSourceImage(0).width / 9;
|
||||
|
||||
if (layer === 'wall') this.wallTiles[ind] = { res, ind, identifier };
|
||||
else if (layer === 'floor') this.floorTiles[ind] = { res, ind, identifier };
|
||||
else if (layer === 'detail') this.detailTiles[ind] = { res, ind, identifier };
|
||||
|
||||
this.indices[identifier] = ind;
|
||||
this.tiles[type].push({ ind, res, identifier, type });
|
||||
this.indices[type][identifier] = ind;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,43 +4,65 @@ import type LightSource from './LightSource';
|
|||
|
||||
import { Vec2 } from '../../util/Vec';
|
||||
|
||||
export const CHUNK_SIZE = 512;
|
||||
export const CHUNK_TILE_SIZE = 32;
|
||||
export const CHUNK_RESOLUTION = 16;
|
||||
|
||||
export default class LightChunk extends Phaser.GameObjects.RenderTexture {
|
||||
pos: Vec2;
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||
super(scene, x * CHUNK_SIZE, y * CHUNK_SIZE, CHUNK_SIZE);
|
||||
/**
|
||||
* Creates a canvas for a LightChunk.
|
||||
*/
|
||||
|
||||
function createCanvas(textures: Phaser.Textures.TextureManager): Phaser.Textures.CanvasTexture {
|
||||
const size = new Vec2(CHUNK_TILE_SIZE * CHUNK_RESOLUTION + 2, CHUNK_TILE_SIZE * CHUNK_RESOLUTION + 2);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size.x;
|
||||
canvas.height = size.y;
|
||||
return textures.addCanvas('', canvas, true);
|
||||
}
|
||||
|
||||
export default class LightChunk extends Phaser.GameObjects.Image {
|
||||
private canvas: Phaser.Textures.CanvasTexture;
|
||||
private context: CanvasRenderingContext2D;
|
||||
|
||||
constructor(scene: Phaser.Scene, public pos: Vec2) {
|
||||
super(scene, pos.x * CHUNK_TILE_SIZE - 1 / CHUNK_RESOLUTION,
|
||||
pos.y * CHUNK_TILE_SIZE - 1 / CHUNK_RESOLUTION, createCanvas(scene.textures));
|
||||
|
||||
this.pos = new Vec2(x, y);
|
||||
|
||||
this.setScale(4);
|
||||
this.setOrigin(0, 0);
|
||||
this.setAlpha(0);
|
||||
this.canvas = (this.texture as Phaser.Textures.CanvasTexture);
|
||||
this.context = this.canvas.getContext();
|
||||
|
||||
this.build([]);
|
||||
this.setScale(1 / CHUNK_RESOLUTION);
|
||||
this.setOrigin(0);
|
||||
this.setAlpha(0.6);
|
||||
}
|
||||
|
||||
build(sourceGfx: {src: LightSource; gfx: Phaser.GameObjects.RenderTexture}[]) {
|
||||
|
||||
// Reset
|
||||
|
||||
const reset = new Phaser.GameObjects.Rectangle(this.scene, 0, 0, CHUNK_SIZE, CHUNK_SIZE, 0x000000);
|
||||
reset.setDisplayOrigin(0, 0);
|
||||
reset.setOrigin(0, 0);
|
||||
this.draw(reset);
|
||||
reset.destroy();
|
||||
applySources(sources: LightSource[]) {
|
||||
// const t = Date.now();
|
||||
|
||||
// Draw Sources
|
||||
// this.context.fillStyle = '#001133';
|
||||
this.context.fillStyle = '#003311';
|
||||
// this.context.fillStyle = '#332211';
|
||||
this.context.fillRect(1, 1, this.canvas.width, this.canvas.height);
|
||||
|
||||
for (let source of sourceGfx) {
|
||||
let lp = new Vec2(source.src.x - this.pos.x * CHUNK_SIZE, source.src.y - this.pos.y * CHUNK_SIZE);
|
||||
if ((lp.x + source.src.radius > 0 || lp.x - source.src.radius < CHUNK_SIZE) &&
|
||||
(lp.y + source.src.radius > 0 || lp.y - source.src.radius < CHUNK_SIZE)) {
|
||||
|
||||
source.gfx.setPosition(lp.x - source.src.radius, lp.y - source.src.radius);
|
||||
this.draw(source.gfx);
|
||||
this.context.globalCompositeOperation = 'destination-out';
|
||||
for (let source of sources) {
|
||||
let lp = new Vec2(source.pos.x - this.pos.x * CHUNK_TILE_SIZE, source.pos.y - this.pos.y * CHUNK_TILE_SIZE);
|
||||
if ((lp.x + source.radius > 0 || lp.x - source.radius < CHUNK_TILE_SIZE) &&
|
||||
(lp.y + source.radius > 0 || lp.y - source.radius < CHUNK_TILE_SIZE)) {
|
||||
this.context.drawImage(source.getCanvas(),
|
||||
(lp.x - source.radius + 0.5) * CHUNK_RESOLUTION + 1,
|
||||
(lp.y - source.radius + 0.5) * CHUNK_RESOLUTION + 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.context.globalCompositeOperation = 'source-over';
|
||||
this.context.clearRect(0, 0, this.canvas.width, 1);
|
||||
this.context.clearRect(0, this.canvas.height - 1, this.canvas.width, this.canvas.height);
|
||||
this.context.clearRect(0, 0, 1, this.canvas.height);
|
||||
this.context.clearRect(this.canvas.width - 1, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
this.canvas.refresh();
|
||||
|
||||
// console.log(Date.now() - t);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,70 +2,72 @@ import * as Phaser from 'phaser';
|
|||
|
||||
import Map from '../Map';
|
||||
|
||||
import { CHUNK_RESOLUTION } from './LightChunk';
|
||||
|
||||
import { Vec2 } from '../../util/Vec';
|
||||
|
||||
export default class LightSource {
|
||||
radius: number = 32;
|
||||
intensity: number = 1.0;
|
||||
const TRACE_POINTS = 360;
|
||||
const TRACE_STEP = 0.05;
|
||||
|
||||
constructor(private scene: Phaser.Scene, private map: Map, public x: number, public y: number) {}
|
||||
export default class LightSource {
|
||||
|
||||
private canvas = document.createElement('canvas');
|
||||
|
||||
constructor(private scene: Phaser.Scene, private map: Map,
|
||||
public pos: Vec2, public radius: number, public intensity: number) {
|
||||
this.setRadius(radius);
|
||||
this.rebuildCanvas();
|
||||
}
|
||||
|
||||
setRadius(radius: number) {
|
||||
this.radius = radius;
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.width = this.radius * CHUNK_RESOLUTION * 2;
|
||||
this.canvas.height = this.radius * CHUNK_RESOLUTION * 2;
|
||||
}
|
||||
|
||||
setIntensity(intensity: number) {
|
||||
this.intensity = intensity;
|
||||
}
|
||||
|
||||
createGfx(): Phaser.GameObjects.RenderTexture {
|
||||
let start = new Vec2(Math.floor(this.x / 16), Math.floor(this.y / 16));
|
||||
rebuildCanvas() {
|
||||
let points: Vec2[] = [];
|
||||
for (let i = 0; i < TRACE_POINTS; i++) {
|
||||
const angle = i * (360 / TRACE_POINTS) * (Math.PI / 180);
|
||||
const dir = new Vec2(Math.cos(angle) * TRACE_STEP, Math.sin(angle) * TRACE_STEP);
|
||||
|
||||
for (let i = 0; i < 288; i++) {
|
||||
let ray = new Vec2(0.5, 0.5);
|
||||
let dir = new Vec2(Math.cos(i * 1.25 * (Math.PI / 180)) / 32, Math.sin(i * 1.25 * (Math.PI / 180)) / 32);
|
||||
|
||||
let dist = 0;
|
||||
while (this.map.getActiveLayer()!.getTile('wall', new Vec2(start.x + ray.x, start.y + ray.y).floor()) === -1 &&
|
||||
(dist = Math.sqrt(Math.pow(ray.x, 2) + Math.pow(ray.y, 2))) < this.radius / 16) {
|
||||
|
||||
let ray = new Vec2(0, 0);
|
||||
|
||||
while (ray.length() < this.radius && this.map.getActiveLayer()!.getTile(
|
||||
'wall', new Vec2(this.pos.x + ray.x + 0.5, this.pos.y + ray.y + 0.5).floor()) === 0) {
|
||||
ray.x += dir.x;
|
||||
ray.y += dir.y;
|
||||
}
|
||||
|
||||
ray.x += dir.x * 0.3;
|
||||
ray.y += dir.y * 0.3;
|
||||
ray.x += dir.x * ((this.radius / 16) - dist) * 0.5;
|
||||
ray.y += dir.y * ((this.radius / 16) - dist) * 0.5;
|
||||
|
||||
points.push(new Vec2(ray.x * 4, ray.y * 4));
|
||||
points.push(ray);
|
||||
}
|
||||
|
||||
let render = new Phaser.GameObjects.RenderTexture(this.scene, 0, 0, this.radius * 2, this.radius * 2);
|
||||
const ctx = this.canvas.getContext('2d')!;
|
||||
|
||||
let gfx = new Phaser.GameObjects.Graphics(this.scene, {x: this.radius, y: this.radius});
|
||||
gfx.setScale(4, 4);
|
||||
gfx.fillStyle(0xffffff, this.intensity / 3);
|
||||
gfx.fillPoints(points, true);
|
||||
gfx.setAlpha(0.5);
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
gfx.scaleX += 0.02;
|
||||
gfx.scaleY += 0.02;
|
||||
render.draw(gfx);
|
||||
}
|
||||
ctx.globalAlpha = this.intensity;
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo((this.radius + points[0].x) * CHUNK_RESOLUTION, (this.radius + points[0].y) * CHUNK_RESOLUTION);
|
||||
for (let i = 1; i < points.length; i++)
|
||||
ctx.lineTo((this.radius + points[i].x) * CHUNK_RESOLUTION, (this.radius + points[i].y) * CHUNK_RESOLUTION);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
let spr = new Phaser.GameObjects.Sprite(this.scene, 0, 0, 'shader_light_mask');
|
||||
spr.setScale(this.radius / 128, this.radius / 128);
|
||||
spr.setOrigin(0, 0);
|
||||
spr.setBlendMode(Phaser.BlendModes.ERASE);
|
||||
render.draw(spr);
|
||||
ctx.globalCompositeOperation = 'destination-in';
|
||||
const tex = this.scene.textures.get('shader_light_mask');
|
||||
const img = tex.source[0].image;
|
||||
ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height);
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
spr.destroy();
|
||||
gfx.destroy();
|
||||
|
||||
render.setBlendMode(Phaser.BlendModes.ERASE);
|
||||
return render;
|
||||
getCanvas(): HTMLCanvasElement {
|
||||
return this.canvas;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,70 +2,60 @@ import * as Phaser from 'phaser';
|
|||
|
||||
import Map from '../Map';
|
||||
import LightSource from './LightSource';
|
||||
import { CHUNK_SIZE as MAP_CHUNK_SIZE } from '../MapChunk';
|
||||
import LightChunk, { CHUNK_SIZE as LIGHT_CHUNK_SIZE } from './LightChunk';
|
||||
import LightChunk, { CHUNK_TILE_SIZE } from './LightChunk';
|
||||
|
||||
import { clamp } from '../../util/Helpers';
|
||||
import { Vec2, Vec4 } from '../../util/Vec';
|
||||
import { Vec2 } from '../../util/Vec';
|
||||
|
||||
export default class Lighting {
|
||||
private map: Map | null = null;
|
||||
private scene: Phaser.Scene | null = null;
|
||||
private map: Map = null as any;
|
||||
private scene: Phaser.Scene = null as any;
|
||||
|
||||
private chunks: LightChunk[][] = [];
|
||||
private sources: LightSource[] = [];
|
||||
private dirtySources: Set<LightSource> = new Set();
|
||||
private dirtyChunks: Set<LightChunk> = new Set();
|
||||
|
||||
init(scene: Phaser.Scene, map: Map) {
|
||||
this.scene = scene;
|
||||
this.map = map;
|
||||
|
||||
for (let i = 0; i < Math.ceil(map.size.y / (MAP_CHUNK_SIZE * 2)); i++) {
|
||||
this.chunks[i] = [];
|
||||
for (let j = 0; j < Math.ceil(map.size.x / (MAP_CHUNK_SIZE * 2)); j++) {
|
||||
this.chunks[i][j] = new LightChunk(scene, j, i);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
let x = Math.floor(Math.random() * 300) * 16;
|
||||
let y = Math.floor(Math.random() * 300) * 16;
|
||||
this.addLightSource(x, y, 12*16, 1);
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.dirtyChunks.size > 0) {
|
||||
let sources: Set<LightSource> = new Set();
|
||||
|
||||
for (let chunk of this.dirtyChunks) {
|
||||
let chunkBounds = new Vec4(chunk.pos.x * LIGHT_CHUNK_SIZE, chunk.pos.y * LIGHT_CHUNK_SIZE,
|
||||
(chunk.pos.x + 1) * LIGHT_CHUNK_SIZE, (chunk.pos.y + 1) * LIGHT_CHUNK_SIZE);
|
||||
|
||||
for (let source of this.sources) {
|
||||
let sourceBounds = new Vec4(source.x - source.radius, source.y - source.radius,
|
||||
source.x + source.radius, source.y + source.radius);
|
||||
if (chunkBounds.z >= sourceBounds.x && chunkBounds.x <= sourceBounds.z &&
|
||||
chunkBounds.y <= sourceBounds.w && chunkBounds.w >= sourceBounds.y) sources.add(source);
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < Math.ceil(map.size.y / CHUNK_TILE_SIZE); i++) {
|
||||
this.chunks[i] = [];
|
||||
for (let j = 0; j < Math.ceil(map.size.x / CHUNK_TILE_SIZE); j++) {
|
||||
const chunk = new LightChunk(scene, new Vec2(j, i));
|
||||
this.chunks[i][j] = chunk;
|
||||
this.scene.add.existing(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
let sourceGfx = Array.from(sources).map((src) => ({ src: src, gfx: src.createGfx() }));
|
||||
for (let chunk of this.dirtyChunks) chunk.build(sourceGfx);
|
||||
sourceGfx.forEach((s) => s.gfx.destroy());
|
||||
this.dirtyChunks.clear();
|
||||
}
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const pos = new Vec2(Math.floor(Math.random() * 64), Math.floor(Math.random() * 64));
|
||||
this.addLight(pos, Math.random() * 7 + 5, Math.random() + 0.3);
|
||||
}
|
||||
|
||||
this.chunks.forEach(cA => cA.forEach(c => c.applySources(this.sources)));
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
this.dirtySources.forEach(s => s.rebuildCanvas());
|
||||
this.dirtyChunks.forEach(c => c.applySources(this.sources));
|
||||
this.dirtySources.clear();
|
||||
this.dirtyChunks.clear();
|
||||
}
|
||||
|
||||
tileUpdatedAt(x: number, y: number) {
|
||||
for (let source of this.sources) {
|
||||
if ((x * 16 >= source.x - source.radius && x * 16 <= source.x + source.radius) &&
|
||||
(y * 16 >= source.y - source.radius && y * 16 <= source.y + source.radius)) {
|
||||
|
||||
const minChunkPos = new Vec2(clamp(Math.floor((source.x - source.radius) / LIGHT_CHUNK_SIZE), 0, this.chunks[0].length - 1),
|
||||
clamp(Math.floor((source.y - source.radius) / LIGHT_CHUNK_SIZE), 0, this.chunks.length - 1));
|
||||
const maxChunkPos = new Vec2(clamp(Math.ceil((source.x + source.radius) / LIGHT_CHUNK_SIZE), 0, this.chunks[0].length - 1),
|
||||
clamp(Math.ceil((source.y + source.radius) / LIGHT_CHUNK_SIZE), 0, this.chunks.length - 1));
|
||||
for (let s of this.sources) {
|
||||
if ((x >= s.pos.x - s.radius && x <= s.pos.x + s.radius) &&
|
||||
(y >= s.pos.y - s.radius && y <= s.pos.y + s.radius)) {
|
||||
this.dirtySources.add(s);
|
||||
|
||||
const minChunkPos = new Vec2(clamp(Math.floor((s.pos.x - s.radius) / CHUNK_TILE_SIZE), 0, this.chunks[0].length - 1),
|
||||
clamp(Math.floor((s.pos.y - s.radius) / CHUNK_TILE_SIZE), 0, this.chunks.length - 1));
|
||||
const maxChunkPos = new Vec2(clamp(Math.ceil((s.pos.x + s.radius) / CHUNK_TILE_SIZE), 0, this.chunks[0].length - 1),
|
||||
clamp(Math.ceil((s.pos.y + s.radius) / CHUNK_TILE_SIZE), 0, this.chunks.length - 1));
|
||||
|
||||
for (let i = minChunkPos.x; i <= maxChunkPos.x; i++) {
|
||||
for (let j = minChunkPos.y; j <= maxChunkPos.y; j++) {
|
||||
|
@ -76,21 +66,17 @@ export default class Lighting {
|
|||
}
|
||||
}
|
||||
|
||||
addLightSource(x: number, y: number, radius?: number, intensity?: number ) {
|
||||
let s = new LightSource(this.scene!, this.map!, x, y);
|
||||
if (radius !== undefined) s.setRadius(radius);
|
||||
if (intensity !== undefined) s.setIntensity(intensity);
|
||||
addLight(pos: Vec2, radius?: number, intensity?: number ) {
|
||||
const s = new LightSource(this.scene, this.map, pos, radius ?? 5, intensity ?? 1);
|
||||
this.sources.push(s);
|
||||
|
||||
const minChunkPos = new Vec2(clamp(Math.floor((s.x - s.radius) / LIGHT_CHUNK_SIZE), 0, this.chunks[0].length - 1),
|
||||
clamp(Math.floor((s.y - s.radius) / LIGHT_CHUNK_SIZE), 0, this.chunks.length - 1));
|
||||
const maxChunkPos = new Vec2(clamp(Math.ceil((s.x + s.radius) / LIGHT_CHUNK_SIZE), 0, this.chunks[0].length - 1),
|
||||
clamp(Math.ceil((s.y + s.radius) / LIGHT_CHUNK_SIZE), 0, this.chunks.length - 1));
|
||||
const minChunkPos = new Vec2(clamp(Math.floor((s.pos.x - s.radius) / CHUNK_TILE_SIZE), 0, this.chunks[0].length - 1),
|
||||
clamp(Math.floor((s.pos.y - s.radius) / CHUNK_TILE_SIZE), 0, this.chunks.length - 1));
|
||||
const maxChunkPos = new Vec2(clamp(Math.ceil((s.pos.x + s.radius) / CHUNK_TILE_SIZE), 0, this.chunks[0].length - 1),
|
||||
clamp(Math.ceil((s.pos.y + s.radius) / CHUNK_TILE_SIZE), 0, this.chunks.length - 1));
|
||||
|
||||
for (let i = minChunkPos.x; i <= maxChunkPos.x; i++) {
|
||||
for (let j = minChunkPos.y; j <= maxChunkPos.y; j++) {
|
||||
for (let i = minChunkPos.x; i <= maxChunkPos.x; i++)
|
||||
for (let j = minChunkPos.y; j <= maxChunkPos.y; j++)
|
||||
this.dirtyChunks.add(this.chunks[j][i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export interface TokenMetaData {
|
|||
/** The render information for a token, such as position and sprite. */
|
||||
export interface TokenRenderData {
|
||||
layer: number;
|
||||
implicitScale: number;
|
||||
pos: { x: number; y: number };
|
||||
appearance: { sprite: string; index: number };
|
||||
}
|
||||
|
@ -73,7 +74,8 @@ export default class Token extends Phaser.GameObjects.Container {
|
|||
private bars: Phaser.GameObjects.GameObject[] = [];
|
||||
// private meta: TokenMetaData = { name: '', note: '', sliders: [] };
|
||||
|
||||
constructor(scene: Phaser.Scene, readonly uuid: string, layer: number, pos?: Vec2, sprite?: string, index?: number) {
|
||||
constructor(scene: Phaser.Scene, readonly uuid: string, layer: number, pos?: Vec2,
|
||||
public implicitScale: number = 1, sprite?: string, index?: number) {
|
||||
super(scene, 0, 0);
|
||||
this.scene.add.existing(this);
|
||||
|
||||
|
@ -81,17 +83,15 @@ export default class Token extends Phaser.GameObjects.Container {
|
|||
this.setPosition(pos?.x ?? 0, pos?.y ?? 0);
|
||||
|
||||
this.shadow = this.scene.add.sprite(0, 0, sprite ?? '', index);
|
||||
this.shadow.setOrigin(1 / 18, 1 / 18);
|
||||
this.shadow.setScale(18 / 16 / this.shadow.width, 18 / 16 / 4 / this.shadow.height);
|
||||
this.shadow.setAlpha(0.1, 0.1, 0.3, 0.3);
|
||||
this.shadow.setTint(0x000000);
|
||||
this.add(this.shadow);
|
||||
|
||||
this.sprite = this.scene.add.sprite(0, 0, sprite ?? '', index);
|
||||
this.sprite.setOrigin(1 / 18, 1 / 18);
|
||||
this.sprite.setScale(18 / 16 / this.sprite.width, 18 / 16 / this.sprite.height);
|
||||
this.add(this.sprite);
|
||||
|
||||
this.updateScale();
|
||||
|
||||
this.shadow.y = this.sprite.displayHeight - this.shadow.displayHeight - 0.125;
|
||||
}
|
||||
|
||||
|
@ -103,6 +103,7 @@ export default class Token extends Phaser.GameObjects.Container {
|
|||
getRenderData(): TokenRenderData {
|
||||
return {
|
||||
pos: new Vec2(this.x, this.y),
|
||||
implicitScale: this.implicitScale,
|
||||
layer: Math.floor((this.depth + 1000) / 25),
|
||||
appearance: { sprite: this.sprite.texture?.key, index: this.sprite.frame?.name as any }
|
||||
};
|
||||
|
@ -114,7 +115,9 @@ export default class Token extends Phaser.GameObjects.Container {
|
|||
*/
|
||||
|
||||
setRenderData(render: Partial<TokenRenderData>) {
|
||||
this.implicitScale = render.implicitScale ?? 1;
|
||||
if (render.pos) this.setPosition(render.pos.x, render.pos.y);
|
||||
if (render.implicitScale) this.updateScale();
|
||||
if (render.appearance) this.setTexture(render.appearance.sprite, render.appearance.index);
|
||||
}
|
||||
|
||||
|
@ -197,17 +200,33 @@ export default class Token extends Phaser.GameObjects.Container {
|
|||
this.sprite.setTexture(key, index);
|
||||
const post = this.getRenderData();
|
||||
this.on_render?.dispatch({ token: this, uuid: this.uuid, pre, post });
|
||||
this.setScale(18 / 16 / this.width, 18 / 16 / this.height);
|
||||
if (!this.shadow) return this;
|
||||
|
||||
this.shadow.setTexture(key, index);
|
||||
this.shadow.setScale(18 / 16 / this.shadow.width, 18 / 16 / 4 / this.shadow.height);
|
||||
this.updateScale();
|
||||
this.shadow.y = this.displayHeight - this.shadow.displayHeight - 0.125;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the scale of the token.
|
||||
*/
|
||||
|
||||
private updateScale() {
|
||||
const frameWidth = this.sprite.width;
|
||||
const scaleFactor = frameWidth / (frameWidth - 2) / frameWidth * this.implicitScale;
|
||||
|
||||
this.sprite.setScale(scaleFactor, scaleFactor);
|
||||
this.shadow.setScale(scaleFactor, scaleFactor / 4);
|
||||
|
||||
this.shadow.setOrigin(1 / frameWidth, 1 / frameWidth);
|
||||
this.sprite.setOrigin(1 / frameWidth, 1 / frameWidth);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the shader pipelines of the token.
|
||||
*/
|
||||
|
@ -237,8 +256,9 @@ export default class Token extends Phaser.GameObjects.Container {
|
|||
const STROKE_WIDTH = 1 / 32;
|
||||
|
||||
let Y_OFFSET = -(SLIDER_HEIGHT + STROKE_WIDTH) * sliders.length - 4 / 16;
|
||||
const X_OFFSET = (this.implicitScale / 2 - SLIDER_WIDTH / 2);
|
||||
|
||||
const outline = this.scene.add.rectangle((1 - SLIDER_WIDTH) / 2, Y_OFFSET,
|
||||
const outline = this.scene.add.rectangle(X_OFFSET, Y_OFFSET,
|
||||
SLIDER_WIDTH, (SLIDER_HEIGHT + STROKE_WIDTH) * sliders.length - STROKE_WIDTH, 0xffffff);
|
||||
outline.setStrokeStyle(STROKE_WIDTH * 2, 0xffffff);
|
||||
outline.setOrigin(0);
|
||||
|
@ -246,18 +266,18 @@ export default class Token extends Phaser.GameObjects.Container {
|
|||
this.add(outline);
|
||||
|
||||
sliders.forEach(s => {
|
||||
const bg = this.scene.add.rectangle((1 - SLIDER_WIDTH) / 2, Y_OFFSET, SLIDER_WIDTH, SLIDER_HEIGHT, 0x19216c);
|
||||
const bg = this.scene.add.rectangle(X_OFFSET, Y_OFFSET, SLIDER_WIDTH, SLIDER_HEIGHT, 0x19216c);
|
||||
bg.setOrigin(0);
|
||||
this.bars.push(bg);
|
||||
this.add(bg);
|
||||
|
||||
const fg = this.scene.add.rectangle((1 - SLIDER_WIDTH) / 2, Y_OFFSET,
|
||||
const fg = this.scene.add.rectangle(X_OFFSET, Y_OFFSET,
|
||||
Math.min((s.current / s.max) * SLIDER_WIDTH, SLIDER_WIDTH), SLIDER_HEIGHT, Color.HSVToInt(s.color));
|
||||
fg.setOrigin(0);
|
||||
this.bars.push(fg);
|
||||
this.add(fg);
|
||||
|
||||
const icon = this.scene.add.sprite(-.5, Y_OFFSET, 'ui_slider_icons', s.icon);
|
||||
const icon = this.scene.add.sprite(X_OFFSET + 1/24, Y_OFFSET, 'ui_slider_icons', s.icon);
|
||||
icon.setOrigin(0);
|
||||
icon.setScale((1 / 12) * SLIDER_HEIGHT);
|
||||
this.bars.push(icon);
|
||||
|
|
|
@ -71,10 +71,11 @@ export default class TokenManager {
|
|||
* @returns the new token instance.
|
||||
*/
|
||||
|
||||
createToken(uuid: string, layer: number, pos: Vec2, meta?: Partial<TokenMetaData>, sprite?: string, index?: number): Token {
|
||||
createToken(uuid: string, layer: number, pos: Vec2, meta?: Partial<TokenMetaData>,
|
||||
scale: number = 1, sprite?: string, index?: number): Token {
|
||||
uuid = uuid || generateId(32);
|
||||
|
||||
const token = new Token(this.scene, uuid, layer, pos, sprite, index);
|
||||
|
||||
const token = new Token(this.scene, uuid, layer, pos, scale, sprite, index);
|
||||
token.on_render.bind(this.onChange);
|
||||
this.scene.add.existing(token);
|
||||
this.tokens.push(token);
|
||||
|
@ -127,7 +128,7 @@ export default class TokenManager {
|
|||
while (this.tokens.length > 0) this.deleteToken(this.tokens[this.tokens.length - 1]);
|
||||
data?.forEach(d => this.createToken(
|
||||
d.uuid, d.render.layer, new Vec2(d.render.pos as any), d.meta,
|
||||
d.render.appearance.sprite, d.render.appearance.index));
|
||||
d.render.implicitScale, d.render.appearance.sprite, d.render.appearance.index));
|
||||
return this.getAllTokens();
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import ActionManager from '../action/ActionManager';
|
|||
import ArchitectController from '../interact/ArchitectController';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import { Asset } from '../util/Asset';
|
||||
import { Asset } from '../../../../common/DBStructs';
|
||||
|
||||
export const ArchitectModeKey = 'ARCHITECT';
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ import Cone from '../shape/Cone';
|
|||
import Circle from '../shape/Circle';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import { Asset } from '../util/Asset';
|
||||
import * as Color from '../../../../common/Color';
|
||||
import { Asset } from '../../../../common/DBStructs';
|
||||
|
||||
export const DrawModeKey = 'DRAW';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import InputManager from '../interact/InputManager';
|
|||
import ActionManager from '../action/ActionManager';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import { Asset } from '../util/Asset';
|
||||
import { Asset } from '../../../../common/DBStructs';
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,7 +12,7 @@ import TokenMode, { TokenModeKey } from './TokenMode';
|
|||
import ArchitectMode, { ArchitectModeKey } from './ArchitectMode';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import { Asset } from '../util/Asset';
|
||||
import { Asset } from '../../../../common/DBStructs';
|
||||
|
||||
export interface ModeSwitchEvent {
|
||||
from: string;
|
||||
|
@ -31,12 +31,12 @@ export default class ModeManager {
|
|||
|
||||
private evtHandler = new EventHandler<ModeSwitchEvent>();
|
||||
|
||||
init(scene: Phaser.Scene, map: Map, socket: IO.Socket, actions: ActionManager, assets: Asset[]) {
|
||||
this.modes = {
|
||||
[ArchitectModeKey]: new ArchitectMode(scene, map, socket, actions, assets),
|
||||
[TokenModeKey]: new TokenMode(scene, map, socket, actions, assets),
|
||||
[DrawModeKey]: new DrawMode(scene, map, socket, actions, assets)
|
||||
};
|
||||
init(scene: Phaser.Scene, state: 'owner' | 'player',
|
||||
map: Map, socket: IO.Socket, actions: ActionManager, assets: Asset[]) {
|
||||
|
||||
if (state === 'owner') this.modes[ArchitectModeKey] = new ArchitectMode(scene, map, socket, actions, assets);
|
||||
this.modes[TokenModeKey] = new TokenMode(scene, map, socket, actions, assets);
|
||||
this.modes[DrawModeKey] = new DrawMode(scene, map, socket, actions, assets);
|
||||
|
||||
this.activate(Object.keys(this.modes)[0]);
|
||||
}
|
||||
|
@ -49,6 +49,14 @@ export default class ModeManager {
|
|||
this.modeStr = mode;
|
||||
}
|
||||
|
||||
getModes(): string[] {
|
||||
return Object.keys(this.modes);
|
||||
}
|
||||
|
||||
hasMode(mode: string): boolean {
|
||||
return this.modes[mode] !== undefined;
|
||||
}
|
||||
|
||||
getActive(): string {
|
||||
return this.modeStr;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import ActionManager from '../action/ActionManager';
|
|||
import Token, { TokenRenderData } from '../map/token/Token';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import { Asset } from '../util/Asset';
|
||||
import { Asset, TokenAsset } from '../../../../common/DBStructs';
|
||||
|
||||
export const TokenModeKey = 'TOKEN';
|
||||
|
||||
|
@ -109,13 +109,18 @@ export default class TokenMode extends Mode {
|
|||
if (this.selected.size) this.keyboardMoveToken(input);
|
||||
|
||||
// Find the currently hovered token.
|
||||
if (!this.hovered || this.hovered.x !== cursorPos.x || this.hovered.y !== cursorPos.y) {
|
||||
if (!this.hovered || cursorPos.x < this.hovered.x || cursorPos.y < this.hovered.y
|
||||
|| cursorPos.x >= this.hovered.x + this.hovered.implicitScale
|
||||
|| cursorPos.y >= this.hovered.y + this.hovered.implicitScale) {
|
||||
this.hovered?.setHovered(false);
|
||||
this.hovered = null;
|
||||
|
||||
for (let i = this.map.tokens.getAllTokens().length - 1; i >= 0; i--) {
|
||||
let token = this.map.tokens.getAllTokens()[i];
|
||||
if (cursorPos.x === token.x && cursorPos.y === token.y) {
|
||||
// console.log([ token.x, token.y, token.implicitScale, token.x + token.implicitScale, token.y + token.implicitScale ]);
|
||||
if (cursorPos.x >= token.x && cursorPos.y >= token.y
|
||||
&& cursorPos.x < token.x + token.implicitScale && cursorPos.y < token.y + token.implicitScale) {
|
||||
|
||||
this.hovered = token;
|
||||
this.hovered.setHovered(true);
|
||||
break;
|
||||
|
@ -251,9 +256,9 @@ export default class TokenMode extends Mode {
|
|||
}
|
||||
|
||||
private placeToken(cursorPos: Vec2): Token {
|
||||
const asset = this.assets.filter(a => a.identifier === this.placeTokenType)[0];
|
||||
const asset = this.assets.filter(a => a.identifier === this.placeTokenType)[0] as TokenAsset;
|
||||
const token = this.map.tokens.createToken('', this.map.getActiveLayer()?.index ?? 0,
|
||||
cursorPos, { name: asset.name }, this.placeTokenType);
|
||||
cursorPos, { name: asset.name }, Number.parseInt(asset.tileSize.x as any, 10), this.placeTokenType);
|
||||
this.actions.push({ type: 'place_token', tokens: [{ uuid: token.uuid, ...token.getRenderData() }] });
|
||||
return token;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as Phaser from 'phaser';
|
||||
import * as IO from 'socket.io-client';
|
||||
|
||||
import EditorData from '../EditorData';
|
||||
import { Asset, Campaign } from '../../../../common/DBStructs';
|
||||
|
||||
async function emit<R = any>(socket: IO.Socket, event: string, data?: any): Promise<R> {
|
||||
|
@ -9,6 +10,10 @@ async function emit<R = any>(socket: IO.Socket, event: string, data?: any): Prom
|
|||
});
|
||||
}
|
||||
|
||||
type Error = { state: false; error?: string };
|
||||
type SocketData = Omit<EditorData, 'socket' | 'onDirty' | 'onProgress'>;
|
||||
type Response = { state: true; campaign: Campaign; assets: Asset[]; map?: string };
|
||||
|
||||
interface InitProps {
|
||||
user: string;
|
||||
identifier: string;
|
||||
|
@ -20,6 +25,7 @@ interface InitProps {
|
|||
|
||||
export default class InitScene extends Phaser.Scene {
|
||||
private socket: IO.Socket = IO.io();
|
||||
private status: Phaser.GameObjects.Text = null as any;
|
||||
|
||||
constructor() { super({key: 'InitScene'}); }
|
||||
|
||||
|
@ -27,16 +33,30 @@ export default class InitScene extends Phaser.Scene {
|
|||
this.socket.on('disconnect', this.onDisconnect);
|
||||
this.game.events.addListener('destroy', this.onDestroy);
|
||||
|
||||
const res = await this.onConnect(user, identifier, mapIdentifier);
|
||||
this.status = this.add.text(8, 12, 'Establishing connection to server...',
|
||||
{ fontFamily: 'sans-serif', fontSize: '20px', color: '#666' });
|
||||
|
||||
let data: SocketData | undefined;
|
||||
const res = await emit<Response | Error>(this.socket, 'room_init', identifier);
|
||||
if (res.state) {
|
||||
data = { ...res, state: 'owner', map:
|
||||
(res.campaign.maps.filter(m => m.identifier === mapIdentifier)[0] ?? res.campaign.maps[0]).data };
|
||||
}
|
||||
else {
|
||||
this.status.setText(this.status.text + '\nAttempting to join game...');
|
||||
try { data = await this.attemptJoin(user, identifier); }
|
||||
catch (e) {
|
||||
this.status.setText(this.status.text + '\nFailed to join game: ' + e + '\nReload to try again.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.start('LoadScene', {
|
||||
socket: this.socket,
|
||||
user, identifier,
|
||||
...res,
|
||||
|
||||
onProgress,
|
||||
onDirty
|
||||
});
|
||||
// user, identifier,
|
||||
onProgress, onDirty,
|
||||
...data
|
||||
} as EditorData);
|
||||
|
||||
this.game.scene.stop('InitScene');
|
||||
this.game.scene.swapPosition('LoadScene', 'InitScene');
|
||||
|
@ -52,15 +72,28 @@ export default class InitScene extends Phaser.Scene {
|
|||
console.log('Disconnected!!!');
|
||||
};
|
||||
|
||||
private onConnect = async (user: string, identifier: string, mapIdentifier: string | undefined):
|
||||
Promise<{ campaign: Campaign; assets: Asset[]; map: string }> => {
|
||||
|
||||
let res: { state: true; campaign: Campaign; assets: Asset[]; map: string } | { state: false; error?: string }
|
||||
= await emit(this.socket, 'room_init', identifier);
|
||||
private attemptJoin = async (user: string, identifier: string,
|
||||
maxAttempts: number = 3, delay: number = 1000): Promise<SocketData> => {
|
||||
return new Promise<SocketData>((resolve, reject) => {
|
||||
let attempts = 0;
|
||||
|
||||
if (res.state) res.map = (mapIdentifier ? res.campaign.maps.filter(m => m.identifier === mapIdentifier)[0] : res.campaign.maps[0]).data;
|
||||
else res = await emit(this.socket, 'room_join', { user, identifier }) as { state: true; campaign: Campaign; assets: Asset[]; map: string };
|
||||
const makeQuery = async () => {
|
||||
const res = await emit<Response | Error>(this.socket, 'room_join', { user, identifier });
|
||||
if (res.state) {
|
||||
this.status.setText(this.status.text + '\nSuccessfully connected.');
|
||||
resolve({ ...res, state: 'player' });
|
||||
}
|
||||
else {
|
||||
this.status.setText(this.status.text + '\n' + res.error);
|
||||
if (attempts++ <= maxAttempts) {
|
||||
this.status.setText(this.status.text + ' Retrying.');
|
||||
setTimeout(makeQuery, delay);
|
||||
}
|
||||
else reject('Timed out.');
|
||||
}
|
||||
};
|
||||
|
||||
return res;
|
||||
makeQuery();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ import BrightenPipeline from '../shader/BrightenPipeline';
|
|||
|
||||
import EditorData from '../EditorData';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
|
||||
export default class LoadScene extends Phaser.Scene {
|
||||
editorData: EditorData = null as any;
|
||||
|
||||
|
@ -53,10 +51,11 @@ export default class LoadScene extends Phaser.Scene {
|
|||
this.load.setPath('/asset/');
|
||||
|
||||
for (let a of this.editorData!.assets) {
|
||||
if (a.type === 'token' || a.type === 'floor')
|
||||
this.load.spritesheet(a.identifier, a.path, { frameWidth: a.tileSize, frameHeight: a.tileSize });
|
||||
|
||||
else if (a.type === 'wall' || a.type === 'detail')
|
||||
if (a.type === 'token') {
|
||||
const fw = a.imageSize.x / (a.tokenType === 4 ? 2 : a.tokenType === 8 ? 3 : 1);
|
||||
this.load.spritesheet(a.identifier, a.path, { frameWidth: fw, frameHeight: fw });
|
||||
}
|
||||
else if (a.type === 'wall' || a.type === 'detail' || a.type === 'floor')
|
||||
this.load.image(a.identifier, a.path);
|
||||
}
|
||||
}
|
||||
|
@ -67,14 +66,16 @@ export default class LoadScene extends Phaser.Scene {
|
|||
glRenderer.pipelines.add('outline', new OutlinePipeline(this.game));
|
||||
|
||||
await Promise.all(this.editorData!.assets.map(a => {
|
||||
if (a.type === 'token') {
|
||||
const { width, height } = (this.textures.get(a.identifier).frames as any).__BASE;
|
||||
a.dimensions = new Vec2(width, height);
|
||||
return Patch.sprite(this, a.identifier, Math.floor(a.dimensions!.x / a.tileSize));
|
||||
}
|
||||
if (a.type === 'token')
|
||||
return Patch.sprite(this, a.identifier, a.tokenType === 4 ? 2 : a.tokenType === 8 ? 3 : 1);
|
||||
|
||||
else if (a.type === 'wall' || a.type === 'detail')
|
||||
Patch.tileset(this, a.identifier, a.tileSize);
|
||||
// TODO: Tilesize needs to come back!)
|
||||
Patch.tileset(this, a.identifier, 16);
|
||||
|
||||
else if (a.type === 'floor')
|
||||
// TODO: Tilesize needs to come back!
|
||||
Patch.floor(this, a.identifier, 16);
|
||||
|
||||
return new Promise<void>(resolve => resolve());
|
||||
}));
|
||||
|
|
|
@ -10,7 +10,7 @@ import ModeManager from '../mode/ModeManager';
|
|||
import CameraControl from '../interact/CameraController';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import { Asset } from '../util/Asset';
|
||||
import { Asset } from '../../../../common/DBStructs';
|
||||
import EditorData from '../EditorData';
|
||||
|
||||
export default class MapScene extends Phaser.Scene {
|
||||
|
@ -39,15 +39,10 @@ export default class MapScene extends Phaser.Scene {
|
|||
|
||||
this.map.init(this, this.assets);
|
||||
if (data.map) this.map.load(data.map);
|
||||
|
||||
let s = this.add.sprite(2, 2, 'wall_dungeon', '__BASE');
|
||||
s.setOrigin(0);
|
||||
s.setScale(1/16);
|
||||
|
||||
data.socket.on('get_map', (res: (map: string) => void) => res(this.map.save()));
|
||||
|
||||
this.actions.init(this, this.map, data.socket, data.onDirty);
|
||||
this.mode.init(this, this.map, data.socket, this.actions, this.assets);
|
||||
this.mode.init(this, data.state, this.map, data.socket, this.actions, this.assets);
|
||||
this.interface.init(this, this.inputManager, this.mode, this.actions, this.map, this.assets);
|
||||
}
|
||||
|
||||
|
@ -69,7 +64,7 @@ export default class MapScene extends Phaser.Scene {
|
|||
const pos = new Vec2(cam.scrollX, cam.scrollY);
|
||||
|
||||
this.interface.setVisible(false);
|
||||
cam.setSize(snapSize.x, snapSize.y);
|
||||
cam.setSize(Math.min(snapSize.x, this.map.size.x * 16), Math.min(snapSize.y, this.map.size.y * 16));
|
||||
cam.centerOn(this.map.size.x / 2, this.map.size.y / 2);
|
||||
cam.setZoom(cam.width / this.map.size.x);
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import { Vec2 } from './Vec';
|
||||
|
||||
export type AssetType = 'floor' | 'detail' | 'wall' | 'token';
|
||||
|
||||
export interface Asset {
|
||||
type: AssetType;
|
||||
name: string;
|
||||
|
||||
identifier: string;
|
||||
path: string;
|
||||
size: number;
|
||||
|
||||
tileSize: number;
|
||||
dimensions?: Vec2;
|
||||
}
|
|
@ -45,7 +45,7 @@ $t-slow: 0.5s
|
|||
|
||||
$wrap-wide: 1400px
|
||||
$wrap-med: 1000px
|
||||
$wrap-small: 700px
|
||||
$wrap-small: 800px
|
||||
|
||||
$m-wide: 1400px
|
||||
$m-med: 1000px
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
%material_input
|
||||
@extend %material_border
|
||||
border-color: $neutral-300
|
||||
background-color: $neutral-100
|
||||
border-color: $neutral-250
|
||||
background-color: $neutral-250
|
||||
font-weight: 400
|
||||
line-height: 1.4em
|
||||
outline: 0
|
||||
|
@ -34,7 +34,7 @@
|
|||
|
||||
&::placeholder
|
||||
font-weight: 400
|
||||
color: $neutral-400
|
||||
color: $neutral-500
|
||||
|
||||
%material_checkbox
|
||||
$disabled-accent: $neutral-500
|
||||
|
|
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 282 B |
After Width: | Height: | Size: 331 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 282 B |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 806 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 276 B |
After Width: | Height: | Size: 591 B |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 276 B |