Added dragons, more robust joining, hid architect mode from players, dynamic lighting!!

master
Auri 2021-02-13 15:31:04 -08:00
parent 21bb34edd3
commit c79ff7d866
186 changed files with 1459 additions and 998 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

View File

@ -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';

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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>

View File

@ -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>
);

View File

@ -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%

View File

@ -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>

View File

@ -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

View File

@ -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>
);
}

View File

@ -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

View File

@ -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>
);
}

View File

@ -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

View File

@ -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>
);
}

View File

@ -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

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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.`);
}

View File

@ -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':

View File

@ -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 }
// });
}
}

View File

@ -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');
}
}

View File

@ -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[];

View File

@ -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;

View File

@ -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);

View File

@ -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}

View File

@ -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);
}
}

View File

@ -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));
};
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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]);
}
}
}
}

View File

@ -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);

View File

@ -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();
}

View File

@ -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';

View File

@ -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';

View File

@ -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';
/**

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
});
};
}

View File

@ -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());
}));

View File

@ -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);

View File

@ -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;
}

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Some files were not shown because too many files have changed in this diff Show More