Fix chunk rendering, improve load times, standardize editor units.

This commit is contained in:
Auri 2021-01-05 21:56:20 -08:00
parent ca2ec263f6
commit e2cf12c9f7
69 changed files with 1204 additions and 735 deletions

BIN
app/res/tileset/hole.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
app/res/ui/icon/detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 654 B

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

View File

@ -49,12 +49,16 @@ export default function App() {
<div class='App-Main'> <div class='App-Main'>
<AppSidebar /> <AppSidebar />
<Switch> <Switch>
<Redirect exact path='/' to='/campaigns' />
<Route path='/assets/collection/:user/:id' component={Routes.Collection} />
<Route path='/asset/:user/:id' component={Routes.Asset} />
<Route path='/assets' component={Routes.Assets} /> <Route path='/assets' component={Routes.Assets} />
<Route path='/campaigns' component={Routes.Campaigns} />
<Route path='/campaign/:id?' component={Routes.Campaign} /> <Route path='/campaign/:id?' component={Routes.Campaign} />
<Route path='/campaigns' component={Routes.Campaigns} />
<Redirect to='/campaigns' /> <Redirect to='/' />
</Switch> </Switch>
</div> </div>
</Route> </Route>

View File

@ -0,0 +1,57 @@
import * as Preact from 'preact';
import { useAppData } from '../../Hooks';
import { Switch, Route, Redirect, useParams, useHistory } from 'react-router-dom';
import Button from '../Button';
export default function AssetRoute() {
const [ { assets } ] = useAppData('assets');
if (!assets) return null;
const history = useHistory();
const { id } = useParams<{ id: string }>();
const currentAsset = (assets ?? []).filter(a => a.identifier === id)[0];
if (!currentAsset) return <Redirect to='/assets/' />;
const handleDeleteAsset = () => {
fetch('/data/asset/delete', {
method: 'POST', cache: 'no-cache',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ identifier: id })
});
history.push('/assets');
};
return (
<div class='AssetRoute Page'>
<aside class='Page-Sidebar'>
<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>
{/* <Route exact path='/campaign/:id'><CampaignOverview campaign={currentCampaign} /></Route>
<Route exact path='/campaign/:id/players'>
<PlayerList players={[
{ name: 'Player', sprite: '/app/static/token/baby_blue_dragon.png' },
{ name: 'Player', sprite: '/app/static/token/cadin_1.png' },
{ name: 'Player', sprite: '/app/static/token/dragonfolk_1.png' },
{ name: 'Player', sprite: '/app/static/token/druid_male.png' },
{ name: 'Player', sprite: '/app/static/token/naexi_human_yklwa.png' }
]}/>
</Route>
<Route exact path='/campaign/:id/maps'><MapList maps={currentCampaign.maps} /></Route>
<Route exact path='/campaign/:id/maps/new'><NewMapForm /></Route>
<Route exact path='/campaign/:id/assets'></Route>
<Redirect to={`/campaign/${id}`} />*/}
</Switch>
</main>
</div>
);
}

View File

@ -1,30 +1,37 @@
import * as Preact from 'preact'; import * as Preact from 'preact';
import { Switch, Route, Redirect, NavLink as Link } from 'react-router-dom'; import { useAppData } from '../../Hooks';
import { Switch, Route, NavLink as Link, useHistory } from 'react-router-dom';
import AssetList from '../view/AssetList';
import NewAssetForm from '../view/NewAssetForm'; import NewAssetForm from '../view/NewAssetForm';
import MyAssetsList from '../view/MyAssetsList'; import AssetCollectionList from '../view/AssetCollectionList';
export default function AssetsRoute() { export default function AssetsRoute() {
const history = useHistory();
const [ { assets, collections, user } ] = useAppData([ 'assets', 'collections', 'user' ]);
return ( return (
<div class='AssetsRoute Page'> <div class='AssetsRoute Page'>
<aside class='Page-Sidebar'> <aside class='Page-Sidebar'>
<h2 class='Page-SidebarTitle'>Assets</h2> <h2 class='Page-SidebarTitle'>Assets</h2>
{/* <Link className='Page-SidebarCategory' activeClassName='Active' exact to='/assets/'>Featured Assets</Link> <Link className='Page-SidebarCategory' activeClassName='Active' exact to='/assets/'>My Assets</Link>
<Link className='Page-SidebarCategory' activeClassName='Active' exact to='/assets/subscribed'>Subscribed Assets</Link>*/} <Link className='Page-SidebarCategory' activeClassName='Active' to='/assets/collections/'>My Collections</Link>
<Link className='Page-SidebarCategory' activeClassName='Active' to='/assets/uploaded'>Uploaded Assets</Link>
</aside> </aside>
<main class='AssetsRoute-Main'> <main class='AssetsRoute-Main'>
<Switch> <Switch>
{/* <Route exact path='/assets/'> <Route exact path='/assets/'>
<h1>Storefront</h1> <AssetList assets={(assets || []).filter(a => a.user === user!.user)}
onClick={(user, identifier) => history.push(`/asset/${user}/${identifier}`)}
onNew={() => history.push('/assets/new')} />
</Route> </Route>
<Route exact path='/assets/subscribed'>
<h1>Subscribed</h1> <Route exact path='/assets/collections'>
</Route>*/} <AssetCollectionList collections={collections || []}
<Route exact path='/assets/uploaded'><MyAssetsList/></Route> onClick={(user, identifier) => history.push(`/assets/collection/${user}/${identifier}`)} />
</Route>
<Route exact path='/assets/new'><NewAssetForm/></Route> <Route exact path='/assets/new'><NewAssetForm/></Route>
<Redirect to='/assets/uploaded' /> {/* <Redirect to='/assets/uploaded' />*/}
</Switch> </Switch>
</main> </main>
</div> </div>

View File

@ -0,0 +1,59 @@
import * as Preact from 'preact';
import { useState } from 'preact/hooks';
import { useAppData } from '../../Hooks';
import { Redirect, useParams } from 'react-router-dom';
import AssetList from '../view/AssetList';
export default function CollectionRoute() {
const [ { collections, assets },, mergeData ] = useAppData([ 'collections', 'assets' ]);
if (!collections) return null;
const [ adding, setAdding ] = useState<boolean>(false);
const { id } = useParams<{ id: string }>();
const currentCollection = (collections ?? []).filter(c => c.identifier === id)[0];
if (!currentCollection) return <Redirect to='/assets/collections/' />;
const collectionAssets = (assets || []).filter(({ user, identifier }) => currentCollection.items.includes(user + ':' + identifier));
const handleAddingAsset = () => {
setAdding(true);
};
const handleAddAsset = async (user: string, identifier: string) => {
const res = await fetch('/data/collection/add', {
method: 'POST', cache: 'no-cache',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ collection: currentCollection.identifier, asset: user + ':' + identifier })
});
if (res.status !== 200) console.error(await res.text());
else {
const data = await res.json();
await mergeData(data);
}
setAdding(false);
};
return (
<div class='CollectionRoute Page'>
<aside class='Page-Sidebar'>
<h2 class='Page-SidebarTitle'>{currentCollection.name}</h2>
{/* <Link className='Page-SidebarCategory' activeClassName='Active' exact to={`/campaign/${id}`}>Overview</Link>
<Link className='Page-SidebarCategory' activeClassName='Active' to={`/campaign/${id}/players`}>Players</Link>
<Link className='Page-SidebarCategory' activeClassName='Active' to={`/campaign/${id}/maps`}>Maps</Link>
<Link className='Page-SidebarCategory' activeClassName='Active' to={`/campaign/${id}/assets`}>Assets</Link>*/}
</aside>
<main class='CollectionRoute-Main'>
{!adding && <AssetList assets={collectionAssets} onClick={() => {/**/}} onNew={handleAddingAsset} newText='Add Asset' />}
{adding && <Preact.Fragment>
<h3>Add Asset</h3>
<AssetList assets={assets || []} onClick={handleAddAsset} />
</Preact.Fragment>}
</main>
</div>
);
}

View File

@ -8,6 +8,8 @@ export { default as Map } from './MapRoute';
export { default as Campaign } from './CampaignRoute'; export { default as Campaign } from './CampaignRoute';
export { default as Campaigns } from './CampaignsRoute'; export { default as Campaigns } from './CampaignsRoute';
export { default as Asset } from './AssetRoute';
export { default as Assets } from './AssetsRoute'; export { default as Assets } from './AssetsRoute';
export { default as Collection } from './CollectionRoute';
export { default as Editor } from './EditorRoute'; export { default as Editor } from './EditorRoute';

View File

@ -0,0 +1,87 @@
@use '../../style/text'
@use '../../style/grid'
@use '../../style/def' as *
.AssetCollectionList
max-width: 1000px
display: block
margin: 0 auto
margin-top: 56px
.AssetCollectionList-Grid
@include grid.auto_width(160px, 16px)
.AssetCollectionList-CollectionWrap
.AssetCollectionList-Collection
width: 100%
height: 0
border: none
display: grid
user-select: none
position: relative
padding-bottom: 100%
overflow: hidden
border-radius: 4px
text-decoration: none
background-color: $neutral-200
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
.AssetCollectionList-CollectionInner
position: absolute
top: 0
left: 0
width: 100%
height: 100%
display: grid
grid-template-rows: 1fr auto
.AssetCollectionList-CollectionPreview
position: relative
padding: 8px
overflow: hidden
background-color: $neutral-100
img
width: 100%
height: 100%
user-select: none
object-fit: contain
pointer-events: none
image-rendering: pixelated
.AssetCollectionList-CollectionTitle
@include text.line_clamp
margin: 0
font-size: 18px
padding: 12px
.AssetCollectionList-NewCollection
width: 100%
display: flex
user-select: none
flex-direction: column
justify-content: center
align-items: center
overflow: hidden
border-radius: 4px
background-color: $neutral-100
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
border: 3px dotted $neutral-300
min-height: 180px
height: 100%
img
width: 80px
height: 80px
image-rendering: pixelated
p
font-size: 18px
margin: 0
margin-top: 4px

View File

@ -0,0 +1,40 @@
import * as Preact from 'preact';
import './AssetCollectionList.sass';
import { AssetCollection } from '../../../../common/DBStructs';
interface Props {
collections: AssetCollection[];
onNew?: () => void;
onClick: (user: string, identifier: string) => void;
}
export default function AssetCollectionList({ collections, onNew, onClick }: Props) {
return (
<div class='AssetCollectionList'>
{collections === undefined && <p>Loading Collections...</p>}
{collections !== undefined &&
<Preact.Fragment>
<ul class='AssetCollectionList-Grid'>
{collections.map(c => <li class='AssetCollectionList-CollectionWrap'>
<button class='AssetCollectionList-Collection' onClick={() => onClick(c.user, c.identifier)}>
<div class='AssetCollectionList-CollectionInner'>
<div class='AssetCollectionList-CollectionPreview'>
</div>
<p class='AssetCollectionList-CollectionTitle'>{c.name || 'Untitled'}</p>
</div>
</button>
</li>)}
<li class='AssetCollectionList-CollectionWrap'>
{onNew && <button onClick={onNew} className='AssetCollectionList-NewCollection'>
<img src='/app/static/ui/icon/asset_new.png' alt=''/>
<p>Create New Collection</p>
</button>}
</li>
</ul>
</Preact.Fragment>
}
</div>
);
}

View File

@ -2,18 +2,20 @@
@use '../../style/grid' @use '../../style/grid'
@use '../../style/def' as * @use '../../style/def' as *
.AssetsList .AssetList
max-width: 1000px max-width: 1000px
display: block display: block
margin: 0 auto margin: 0 auto
margin-top: 56px margin-top: 56px
.AssetsList-Grid .AssetList-Grid
@include grid.auto_width(160px, 16px) @include grid.auto_width(160px, 16px)
.AssetsList-AssetWrap .AssetList-AssetWrap
.AssetsList-Asset .AssetList-Asset
width: 100%
height: 0 height: 0
border: none
display: grid display: grid
user-select: none user-select: none
position: relative position: relative
@ -25,7 +27,7 @@
background-color: $neutral-200 background-color: $neutral-200
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9) box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
.AssetsList-AssetInner .AssetList-AssetInner
position: absolute position: absolute
top: 0 top: 0
left: 0 left: 0
@ -34,7 +36,7 @@
display: grid display: grid
grid-template-rows: 1fr auto grid-template-rows: 1fr auto
.AssetsList-AssetPreview .AssetList-AssetPreview
position: relative position: relative
padding: 8px padding: 8px
overflow: hidden overflow: hidden
@ -49,14 +51,15 @@
pointer-events: none pointer-events: none
image-rendering: pixelated image-rendering: pixelated
.AssetsList-AssetTitle .AssetList-AssetTitle
@include text.line_clamp @include text.line_clamp
margin: 0 margin: 0
font-size: 18px font-size: 18px
padding: 12px padding: 12px
.AssetsList-NewAsset .AssetList-NewAsset
width: 100%
display: flex display: flex
user-select: none user-select: none
flex-direction: column flex-direction: column

View File

@ -0,0 +1,43 @@
import * as Preact from 'preact';
import './AssetList.sass';
import { Asset } from '../../../../common/DBStructs';
interface Props {
assets: Asset[];
newText?: string;
onNew?: () => void;
onClick: (user: string, identifier: string) => void;
}
export default function AssetList({ assets, newText, onNew, onClick }: Props) {
return (
<div class='AssetList'>
{assets === undefined && <p>Loading Assets...</p>}
{assets !== undefined &&
<Preact.Fragment>
<ul class='AssetList-Grid'>
{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>
<p class='AssetList-AssetTitle'>{a.name || 'Untitled'}</p>
</div>
</button>
</li>)}
<li class='AssetList-AssetWrap'>
{onNew && <button onClick={onNew} className='AssetList-NewAsset'>
<img src='/app/static/ui/icon/asset_new.png' alt=''/>
<p>{newText ?? 'Upload Asset'}</p>
</button>}
</li>
</ul>
</Preact.Fragment>
}
</div>
);
}

View File

@ -1,37 +0,0 @@
import * as Preact from 'preact';
import { useAppData } from '../../Hooks';
import { NavLink as Link } from 'react-router-dom';
import './MyAssetsList.sass';
export default function MyAssetsList() {
const [ { assets } ] = useAppData('assets');
return (
<div class='AssetsList'>
{assets === undefined && <p>Loading Assets...</p>}
{assets !== undefined &&
<Preact.Fragment>
<ul class='AssetsList-Grid'>
{assets.map(a => <li class='AssetsList-AssetWrap'>
<Link className='AssetsList-Asset' to={`/campaign/${a.identifier}`}>
<div class='AssetsList-AssetInner'>
<div class='AssetsList-AssetPreview'>
<img src={'/app/asset/' + a.path} alt='' />
</div>
<p class='AssetsList-AssetTitle'>{a.name || 'Untitled'}</p>
</div>
</Link>
</li>)}
<li class='AssetsList-AssetWrap'>
<Link className='AssetsList-NewAsset' to='/assets/new'>
<img src='/app/static/ui/icon/asset_new.png' alt=''/>
<p>Upload Asset</p>
</Link>
</li>
</ul>
</Preact.Fragment>
}
</div>
);
}

View File

@ -26,6 +26,13 @@
grid-gap: 16px grid-gap: 16px
grid-template-columns: 1fr 1fr 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 .NewAssetForm-UploadWrap
@extend %material_button @extend %material_button

View File

@ -16,7 +16,7 @@ export default function NewAssetForm() {
const [ queryState, setQueryState ] = useState<'idle' | 'querying'>('idle'); const [ queryState, setQueryState ] = useState<'idle' | 'querying'>('idle');
const [ type, setType ] = useState<'wall' | 'ground' | 'token'>('token'); const [ type, setType ] = useState<'wall' | 'floor' | 'detail' | 'token'>('token');
const [ tokenType, setTokenType ] = useState<1 | 4 | 8>(4); const [ tokenType, setTokenType ] = useState<1 | 4 | 8>(4);
const [ file, setFile ] = useState<File | null>(null); const [ file, setFile ] = useState<File | null>(null);
@ -25,7 +25,7 @@ export default function NewAssetForm() {
const [ name, setName ] = useState<string>(''); const [ name, setName ] = useState<string>('');
const [ identifier, setIdentifier ] = useState<string>(''); const [ identifier, setIdentifier ] = useState<string>('');
const handleSetType = (type: 'wall' | 'ground' | 'token') => { const handleSetType = (type: 'wall' | 'floor' | 'detail' | 'token') => {
setType(type); setType(type);
}; };
@ -69,12 +69,9 @@ export default function NewAssetForm() {
body: data body: data
}); });
if (res.status === 202) { if (res.status === 200) history.push('/assets');
console.log('hellyea!');
history.push('/assets');
}
else { else {
console.log('hellnah', await res.text()); console.error(await res.text());
setQueryState('idle'); setQueryState('idle');
} }
}; };
@ -83,12 +80,13 @@ export default function NewAssetForm() {
<div class='NewAssetForm'> <div class='NewAssetForm'>
<h2 class='NewAssetForm-Title'>New Asset</h2> <h2 class='NewAssetForm-Title'>New Asset</h2>
<div class='NewAssetForm-Col2'> <div class='NewAssetForm-Col2-60'>
<div> <div>
<Label label='Asset Type' /> <Label label='Asset Type' />
<ButtonGroup> <ButtonGroup>
<Button icon='token' label='Token' inactive={type !== 'token'} onClick={() => handleSetType('token')}/> <Button icon='token' label='Token' inactive={type !== 'token'} onClick={() => handleSetType('token')}/>
<Button icon='ground' label='Ground' inactive={type !== 'ground'} onClick={() => handleSetType('ground')}/> <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')}/> <Button icon='wall' label='Wall' inactive={type !== 'wall'} onClick={() => handleSetType('wall')}/>
</ButtonGroup> </ButtonGroup>
</div> </div>

View File

@ -1,7 +1,7 @@
import type MapScene from './scene/MapScene'; import type MapScene from './scene/MapScene';
import Layer from './util/Layer';
import { Vec2 } from './util/Vec'; import { Vec2 } from './util/Vec';
import { Layer } from './util/Layer';
export default class ArchitectMode { export default class ArchitectMode {
scene: MapScene; scene: MapScene;
@ -16,7 +16,7 @@ export default class ArchitectMode {
pointerPrimaryDown: boolean = false; pointerPrimaryDown: boolean = false;
activeTileset: number = 0; activeTileset: number = 0;
activeLayer: Layer = Layer.wall; activeLayer: Layer = 'wall';
manipulated: {pos: Vec2; layer: Layer; lastTile: number; tile: number}[] = []; manipulated: {pos: Vec2; layer: Layer; lastTile: number; tile: number}[] = [];
@ -25,19 +25,18 @@ export default class ArchitectMode {
} }
init() { init() {
// Create cursor hover sprite
this.cursor = this.scene.add.sprite(0, 0, 'cursor'); this.cursor = this.scene.add.sprite(0, 0, 'cursor');
this.cursor.setScale(4, 4);
this.cursor.setDepth(1000); this.cursor.setDepth(1000);
this.cursor.setOrigin(0, 0); this.cursor.setOrigin(0, 0);
this.cursor.setScale(1 / 16);
} }
update() { update() {
this.active = true; this.active = true;
this.cursor!.setVisible(true); this.cursor!.setVisible(true);
let selectedTilePos = new Vec2(Math.floor(this.scene.view.cursorWorld.x / 64), Math.floor(this.scene.view.cursorWorld.y / 64)); let selectedTilePos = new Vec2(Math.floor(this.scene.view.cursorWorld.x), Math.floor(this.scene.view.cursorWorld.y));
this.cursor!.setPosition(selectedTilePos.x * 64, selectedTilePos.y * 64); this.cursor!.setPosition(selectedTilePos.x, selectedTilePos.y);
this.cursor!.setVisible((selectedTilePos.x >= 0 && selectedTilePos.y >= 0 && this.cursor!.setVisible((selectedTilePos.x >= 0 && selectedTilePos.y >= 0 &&
selectedTilePos.x < this.scene.map.size.x && selectedTilePos.y < this.scene.map.size.y)); selectedTilePos.x < this.scene.map.size.x && selectedTilePos.y < this.scene.map.size.y));
@ -91,7 +90,7 @@ export default class ArchitectMode {
if (Math.abs(b.x - a.x) > Math.abs(b.y - a.y)) b.y = a.y; if (Math.abs(b.x - a.x) > Math.abs(b.y - a.y)) b.y = a.y;
else b.x = a.x; else b.x = a.x;
this.cursor!.setPosition(b.x * 64, b.y * 64); this.cursor!.setPosition(b.x, b.y);
this.primitives.forEach((v) => v.destroy()); this.primitives.forEach((v) => v.destroy());
this.primitives = []; this.primitives = [];
@ -100,21 +99,19 @@ export default class ArchitectMode {
this.primitives.forEach((v) => { this.primitives.forEach((v) => {
v.setOrigin(0, 0); v.setOrigin(0, 0);
v.setScale(64, 64);
v.setLineWidth(0.03);
v.setDepth(300); v.setDepth(300);
v.setLineWidth(0.03);
}); });
this.primitives.push(this.scene.add.sprite(this.startTilePos.x * 64, this.startTilePos.y * 64, this.primitives.push(this.scene.add.sprite(this.startTilePos.x, this.startTilePos.y, 'cursor') as any);
'cursor') as any as Phaser.GameObjects.Line);
this.primitives[1].setOrigin(0, 0); this.primitives[1].setOrigin(0, 0);
this.primitives[1].setScale(4, 4); this.primitives[1].setScale(1 / 16);
this.primitives[1].setAlpha(0.5); this.primitives[1].setAlpha(0.5);
} }
else if (!this.scene.i.mouseLeftDown() && !this.scene.i.mouseRightDown() && this.pointerDown) { else if (!this.scene.i.mouseLeftDown() && !this.scene.i.mouseRightDown() && this.pointerDown) {
let a = new Vec2(this.startTilePos.x * 64, this.startTilePos.y * 64); let a = new Vec2(this.startTilePos.x, this.startTilePos.y);
let b = new Vec2(selectedTilePos.x * 64, selectedTilePos.y * 64); let b = new Vec2(selectedTilePos.x, selectedTilePos.y);
if (Math.abs(b.x - a.x) > Math.abs(b.y - a.y)) b.y = a.y; if (Math.abs(b.x - a.x) > Math.abs(b.y - a.y)) b.y = a.y;
else b.x = a.x; else b.x = a.x;
@ -125,12 +122,12 @@ export default class ArchitectMode {
change.y /= normalizeFactor; change.y /= normalizeFactor;
while (Math.abs(b.x - a.x) >= 1 || Math.abs(b.y - a.y) >= 1) { while (Math.abs(b.x - a.x) >= 1 || Math.abs(b.y - a.y) >= 1) {
this.placeTileAndPushManip(new Vec2(Math.floor(a.x / 64), Math.floor(a.y / 64)), this.pointerPrimaryDown); this.placeTileAndPushManip(new Vec2(Math.floor(a.x), Math.floor(a.y)), this.pointerPrimaryDown);
a.x += change.x; a.x += change.x;
a.y += change.y; a.y += change.y;
} }
this.placeTileAndPushManip(new Vec2(b.x / 64, b.y / 64), this.pointerPrimaryDown); this.placeTileAndPushManip(new Vec2(b.x, b.y), this.pointerPrimaryDown);
this.primitives.forEach((v) => v.destroy()); this.primitives.forEach((v) => v.destroy());
this.primitives = []; this.primitives = [];
} }
@ -154,7 +151,6 @@ export default class ArchitectMode {
this.primitives.forEach((v) => { this.primitives.forEach((v) => {
v.setOrigin(0, 0); v.setOrigin(0, 0);
v.setScale(64, 64);
v.setLineWidth(0.03); v.setLineWidth(0.03);
v.setDepth(300); v.setDepth(300);
}); });
@ -180,14 +176,14 @@ export default class ArchitectMode {
let change = new Vec2(this.scene.view.cursorWorld.x - this.scene.view.lastCursorWorld.x, let change = new Vec2(this.scene.view.cursorWorld.x - this.scene.view.lastCursorWorld.x,
this.scene.view.cursorWorld.y - this.scene.view.lastCursorWorld.y); this.scene.view.cursorWorld.y - this.scene.view.lastCursorWorld.y);
let normalizeFactor = Math.sqrt(change.x * change.x + change.y * change.y); let normalizeFactor = Math.sqrt(change.x * change.x + change.y * change.y) * 10;
change.x /= normalizeFactor; change.x /= normalizeFactor;
change.y /= normalizeFactor; change.y /= normalizeFactor;
let place = new Vec2(this.scene.view.lastCursorWorld.x, this.scene.view.lastCursorWorld.y); let place = new Vec2(this.scene.view.lastCursorWorld.x, this.scene.view.lastCursorWorld.y);
while (Math.abs(this.scene.view.cursorWorld.x - place.x) >= 1 || Math.abs(this.scene.view.cursorWorld.y - place.y) >= 1) { while (Math.abs(this.scene.view.cursorWorld.x - place.x) >= 0.1 || Math.abs(this.scene.view.cursorWorld.y - place.y) >= 0.1) {
this.placeTileAndPushManip(new Vec2(Math.floor(place.x / 64), Math.floor(place.y / 64)), this.scene.i.mouseLeftDown()); this.placeTileAndPushManip(new Vec2(Math.floor(place.x), Math.floor(place.y)), this.scene.i.mouseLeftDown());
place.x += change.x; place.x += change.x;
place.y += change.y; place.y += change.y;
} }
@ -198,12 +194,12 @@ export default class ArchitectMode {
placeTileAndPushManip(manipPos: Vec2, solid: boolean) { placeTileAndPushManip(manipPos: Vec2, solid: boolean) {
let tile = solid ? this.activeTileset : -1; let tile = solid ? this.activeTileset : -1;
let layer = (tile === -1 && this.activeLayer === Layer.floor) ? Layer.wall : this.activeLayer; let layer = (tile === -1 && this.activeLayer === 'floor') ? 'wall' : this.activeLayer;
let lastTile = this.scene.map.getTileset(layer, manipPos.x, manipPos.y); let lastTile = this.scene.map.activeLayer.getTile(layer, manipPos.x, manipPos.y);
if (tile === lastTile) return; if (tile === lastTile) return;
this.scene.map.setTile(layer, tile, manipPos.x, manipPos.y); this.scene.map.activeLayer.setTile(layer, tile, manipPos.x, manipPos.y);
this.manipulated.push({ this.manipulated.push({
pos: manipPos, pos: manipPos,
@ -211,7 +207,6 @@ export default class ArchitectMode {
lastTile: lastTile, lastTile: lastTile,
tile: tile tile: tile
}); });
} }
cleanup() { cleanup() {

View File

@ -1,96 +0,0 @@
import MapData from './MapData';
import Layer from './util/Layer';
import { Vec2 } from './util/Vec';
export default class MapChunk {
static CHUNK_SIZE = 16;
static TILE_SIZE = 16;
static DIRTY_LIMIT = 32;
private pos: Vec2;
private map: MapData;
private canvas: Phaser.GameObjects.RenderTexture;
private dirtyList: Vec2[] = [];
private fullyDirty: boolean = false;
constructor(map: MapData, x: number, y: number) {
this.pos = new Vec2(x, y);
this.canvas = map.scene.add.renderTexture(
x * MapChunk.CHUNK_SIZE * MapChunk.TILE_SIZE * 4, y * MapChunk.CHUNK_SIZE * MapChunk.TILE_SIZE * 4,
MapChunk.CHUNK_SIZE * MapChunk.TILE_SIZE, MapChunk.CHUNK_SIZE * MapChunk.TILE_SIZE);
this.map = map;
this.canvas.setScale(4);
this.canvas.setOrigin(0, 0);
for (let i = 0; i < MapChunk.CHUNK_SIZE * MapChunk.CHUNK_SIZE; i++) {
let x = i % MapChunk.CHUNK_SIZE;
let y = Math.floor(i / MapChunk.CHUNK_SIZE);
let mX = x + this.pos.x * MapChunk.CHUNK_SIZE;
let mY = y + this.pos.y * MapChunk.CHUNK_SIZE;
if (mX >= this.map.size.x || mY >= this.map.size.y) continue;
this.drawTile(x, y);
}
}
dirty(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 > MapChunk.DIRTY_LIMIT) {
this.fullyDirty = true;
this.dirtyList = [];
}
}
}
rebuild(): boolean {
if (this.fullyDirty) {
for (let i = 0; i < MapChunk.CHUNK_SIZE * MapChunk.CHUNK_SIZE; i++) {
let x = i % MapChunk.CHUNK_SIZE;
let y = Math.floor(i / MapChunk.CHUNK_SIZE);
let mX = x + this.pos.x * MapChunk.CHUNK_SIZE;
let mY = y + this.pos.y * MapChunk.CHUNK_SIZE;
if (mX >= this.map.size.x || mY >= this.map.size.y) continue;
this.drawTile(x, y);
}
this.fullyDirty = false;
return true;
}
if (this.dirtyList.length === 0) return false;
for (let elem of this.dirtyList) this.drawTile(elem.x, elem.y);
this.dirtyList = [];
return true;
}
private drawTile(x: number, y: number): void {
let mX = x + this.pos.x * MapChunk.CHUNK_SIZE;
let mY = y + this.pos.y * MapChunk.CHUNK_SIZE;
let wallTile = this.map.getTile(Layer.wall, mX, mY);
if (this.map.getTileset(Layer.wall, mX, mY) === -1 || (wallTile < 54 || wallTile > 60)) {
this.canvas.drawFrame(this.map.manager.groundLocations[this.map.getTileset(Layer.floor, mX, mY)].identifier,
this.map.getTile(Layer.floor, mX, mY), x * MapChunk.TILE_SIZE, y * MapChunk.TILE_SIZE);
if (this.map.getTileset(Layer.overlay, mX, mY) !== -1)
this.canvas.drawFrame(this.map.manager.overlayLocations[this.map.getTileset(Layer.overlay, mX, mY)].identifier,
this.map.getTile(Layer.overlay, mX, mY), x * MapChunk.TILE_SIZE, y * MapChunk.TILE_SIZE);
}
if (this.map.getTileset(Layer.wall, mX, mY) !== -1)
this.canvas.drawFrame(this.map.manager.wallLocations[this.map.getTileset(Layer.wall, mX, mY)].identifier,
this.map.getTile(Layer.wall, mX, mY), x * MapChunk.TILE_SIZE, y * MapChunk.TILE_SIZE);
if ((x % 2 === 0 && y % 2 === 0) || (x % 2 !== 0 && y % 2 !== 0))
this.canvas.drawFrame('grid_tile', 0, x * MapChunk.TILE_SIZE, y * MapChunk.TILE_SIZE);
}
}

View File

@ -1,198 +0,0 @@
import MapChunk from './MapChunk';
import * as SmartTiler from './SmartTiler';
import type MapScene from './scene/MapScene';
import TilesetManager from './TilesetManager';
import Layer from './util/Layer';
import { Vec2 } from './util/Vec';
import { Asset } from './util/Asset';
import { clamp } from './util/Helpers';
export default class MapData {
scene: MapScene;
size: Vec2 = new Vec2(0, 0);
savedMapData: number[][] = [];
manager: TilesetManager;
private layers: {[key: number]: { tiles: number[][]; tilesets: number[][] }} = {};
private chunks: MapChunk[][] = [];
constructor(scene: MapScene) {
this.scene = scene;
this.manager = new TilesetManager(scene);
}
init(size: Vec2, assets: Asset[]) {
this.size = size;
this.manager.init(assets);
this.registerLayer(Layer.floor, () => Math.floor(Math.random() * 6) + 54, 0);
this.registerLayer(Layer.wall, 0, -1);
this.registerLayer(Layer.overlay, 0, -1);
for (let i = 0; i < Math.ceil(size.y / MapChunk.CHUNK_SIZE); i++) {
this.chunks[i] = [];
for (let j = 0; j < Math.ceil(size.x / MapChunk.CHUNK_SIZE); j++) {
this.chunks[i][j] = new MapChunk(this, j, i);
}
}
}
update(): void {
let start = Date.now();
for (let arr of this.chunks) for (let chunk of arr) {
chunk.rebuild();
if (Date.now() - start > 10) break;
}
if (this.scene.i.keyPressed('S')) this.saveMap();
if (this.scene.i.keyPressed('L')) this.loadMap(this.savedMapData);
}
setTile(layer: Layer, tileset: number, xx: number | Vec2, yy?: number): boolean {
let x: number, y: number;
if (xx instanceof Vec2) { x = xx.x; y = xx.y; }
else { x = xx; y = yy!; }
if (x < 0 || y < 0 || x >= this.size.x || y >= this.size.y) return false;
let oldTileset = this.getTileset(layer, x, y);
if (oldTileset === tileset) return false;
this.setTileset(layer, x, y, tileset);
this.smartTile(x, y);
return true;
}
setTileset(key: number, x: number | Vec2, a?: number, b?: number): void {
if (x instanceof Vec2) this.layers[key].tilesets[x.y][x.x] = a!;
else this.layers[key].tilesets[a!][x] = b!;
}
getTile(key: number, xx: number | Vec2, yy?: number): number {
let x: number, y: number;
if (xx instanceof Vec2) { x = xx.x; y = xx.y; }
else { x = xx; y = yy!; }
return this.layers[key].tiles[clamp(y, 0, this.size.y - 1)][clamp(x, 0, this.size.x - 1)];
}
getTileset(key: number, xx: number | Vec2, yy?: number): number {
let x: number, y: number;
if (xx instanceof Vec2) { x = xx.x; y = xx.y; }
else { x = xx; y = yy!; }
return this.layers[key].tilesets[clamp(y, 0, this.size.y - 1)][clamp(x, 0, this.size.x - 1)];
}
private smartTile(x: number, y: number): void {
for (let i = clamp(x - 1, this.size.x - 1, 0); i <= clamp(x + 1, this.size.x - 1, 0); i++) {
for (let j = clamp(y - 1, this.size.y - 1, 0); j <= clamp(y + 1, this.size.y - 1, 0); j++) {
const solids = this.getTilesetsAt(Layer.wall, i, j).map(i => i !== -1);
// const bits = SmartTiler.bitsToIndices(solids);
// if (i === x && j === y) console.log(bits, Math.floor(bits / 16), bits % 16);
const wall = SmartTiler.wall(solids, this.getTile(Layer.wall, i, j));
if (wall !== -1) this.setTileRaw(Layer.wall, i, j, wall);
const floor = SmartTiler.floor(solids, this.getTile(Layer.floor, i, j));
if (floor !== -1) this.setTileRaw(Layer.floor, i, j, floor);
const overlay = SmartTiler.overlay(this.getTilesetsAt(Layer.overlay, i, j)
.map(t => t === this.getTileset(Layer.overlay, i, j)), this.getTileset(Layer.overlay, i, j));
if (overlay !== -1) this.setTileRaw(Layer.overlay, i, j, overlay);
}
}
// this.scene.lighting.tileUpdatedAt(x, y);
}
private setTileRaw(key: number, x: number | Vec2, a?: number, b?: number, c?: number): void {
if (x instanceof Vec2) {
this.layers[key].tiles[x.y][x.x] = a!;
if (b !== undefined) this.setTileset(key, x, b);
this.chunks[Math.floor(x.y / MapChunk.CHUNK_SIZE)][Math.floor(x.x / MapChunk.CHUNK_SIZE)]
.dirty(new Vec2(x.x % MapChunk.CHUNK_SIZE, x.y % MapChunk.CHUNK_SIZE));
}
else {
this.layers[key].tiles[a!][x] = b!;
if (c !== undefined) this.setTileset(key, x, a, c);
this.chunks[Math.floor(a! / MapChunk.CHUNK_SIZE)][Math.floor(x / MapChunk.CHUNK_SIZE)]
.dirty(new Vec2(x % MapChunk.CHUNK_SIZE, a! % MapChunk.CHUNK_SIZE));
}
}
private getTilesetsAt(layer: Layer, x: number, y: number): number[] {
let tilesets: number[] = [];
for (let i = -1; i <= 1; i++)
for (let j = -1; j <= 1; j++)
tilesets.push(this.getTileset(layer, clamp(x + j, 0, this.size.x - 1), clamp(y + i, 0, this.size.y - 1)));
return tilesets;
}
private registerLayer(key: number, startTile: any = 0, startTileset: number = -1): void {
let layer: {tiles: number[][]; tilesets: number[][]} = { tiles: [], tilesets: [] };
for (let i = 0; i < this.size.y; i++) {
layer.tiles[i] = [];
layer.tilesets[i] = [];
for (let j = 0; j < this.size.x; j++) {
let tile = typeof(startTile) === 'number' ? startTile : startTile();
layer.tiles[i][j] = tile;
layer.tilesets[i][j] = startTileset;
}
}
this.layers[key] = layer;
}
private saveMap() {
let mapData: number[][] = [];
for (let k = 0; k < 3; k++) {
let tile = 0;
let count = 0;
mapData[k] = [];
for (let i = 0; i < this.size.x * this.size.y; i++) {
let x = i % this.size.x;
let y = Math.floor(i / this.size.x);
if (this.getTileset(k, x, y) === tile) count++;
else {
if (i !== 0) {
mapData[k].push(tile);
mapData[k].push(count);
}
tile = this.getTileset(k, x, y);
count = 1;
}
}
}
this.savedMapData = mapData;
}
private loadMap(dat: number[][]) {
for (let k = 0; k < 3; k++) {
let offset = 0;
for (let i = 0; i < dat[k].length / 2; i++) {
let tile = dat[k][i * 2];
let count = dat[k][i * 2 + 1];
for (let t = 0; t < count; t++) {
let x = (offset + t) % this.size.x;
let y = Math.floor((offset + t) / this.size.x);
this.setTile(k, tile, x, y);
}
offset += count;
}
}
}
}

View File

@ -1,68 +0,0 @@
const wallField = [
33, 33, 6, 6, 33, 33, 6, 6, 5, 5, 10, 17, 5, 5, 10, 17, 15, 15, 9, 9, 15, 15, 16, 16, 2, 2, 4, 50, 2, 2, 49, 32,
33, 33, 6, 6, 33, 33, 6, 6, 5, 5, 10, 17, 5, 5, 10, 17, 15, 15, 9, 9, 15, 15, 16, 16, 2, 2, 4, 50, 2, 2, 49, 32,
14, 14, 11, 11, 14, 14, 11, 11, 1, 1, 13, 48, 1, 1, 13, 48, 0, 0, 12, 12, 0, 0, 47, 47, 3, 3, 25, 46, 3, 3, 45, 20,
14, 14, 11, 11, 14, 14, 11, 11, 8, 8, 39, 23, 8, 8, 39, 23, 0, 0, 12, 12, 0, 0, 47, 47, 41, 41, 37, 29, 41, 41, 51, 18,
33, 33, 6, 6, 33, 33, 6, 6, 5, 5, 10, 17, 5, 5, 10, 17, 15, 15, 9, 9, 15, 15, 16, 16, 2, 2, 4, 50, 2, 2, 49, 32,
33, 33, 6, 6, 33, 33, 6, 6, 5, 5, 10, 17, 5, 5, 10, 17, 15, 15, 9, 9, 15, 15, 16, 16, 2, 2, 4, 50, 2, 2, 49, 32,
14, 14, 11, 11, 14, 14, 11, 11, 1, 1, 13, 48, 1, 1, 13, 48, 7, 7, 38, 38, 7, 7, 22, 22, 40, 40, 36, 42, 40, 40, 30, 19,
14, 14, 11, 11, 14, 14, 11, 11, 8, 8, 39, 23, 8, 8, 39, 23, 7, 7, 38, 38, 7, 7, 22, 22, 31, 31, 21, 27, 31, 31, 28, 54
];
const floorField = [
54, 20, 19, 19, 18, 4, 19, 19, 11, 11, 3, 3, 51, 51, 3, 3, 9, 52, 5, 5, 9, 52, 5, 5, 39, 39, 30, 30, 39, 39, 30, 30,
2, 12, 32, 32, 34, 6, 32, 32, 11, 11, 3, 3, 51, 51, 3, 3, 43, 38, 29, 29, 43, 38, 29, 29, 39, 39, 30, 30, 39, 39, 30, 30,
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49,
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49,
0, 33, 31, 31, 14, 7, 31, 31, 42, 42, 27, 27, 36, 36, 27, 27, 9, 52, 5, 5, 9, 52, 5, 5, 39, 39, 30, 30, 39, 39, 30, 30,
22, 15, 28, 28, 16, 37, 28, 28, 42, 42, 27, 27, 36, 36, 27, 27, 43, 38, 29, 29, 43, 38, 29, 29, 39, 39, 30, 30, 39, 39, 30, 30,
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49,
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49
];
export function bitsToIndices(walls: boolean[]) {
return (
(+walls[0] << 0) +
(+walls[1] << 1) +
(+walls[2] << 2) +
(+walls[3] << 3) +
(+walls[5] << 4) +
(+walls[6] << 5) +
(+walls[7] << 6) +
(+walls[8] << 7));
}
export function wall(walls: boolean[], current: number): number {
if (current === -1) return -1;
const ind = wallField[bitsToIndices(walls)];
if (ind < 54) return ind;
return 54 + Math.floor(Math.random() * 6);
}
export function overlay(overlays: boolean[], current: number): number {
if (current === -1) return -1;
const ind = floorField[bitsToIndices(overlays)];
if (ind < 54) return ind;
return 54 + Math.floor(Math.random() * 6);
}
export function floor(walls: boolean[], current: number): number {
if (current === -1) return -1;
const ind = floorField[bitsToIndices(walls)];
if (ind < 54) return ind;
return 54 + Math.floor(Math.random() * 6);
}
// const l = [];
// for (let i = 0; i < (1 << 8); ++i ) {
// let arr = [ i & 0x001, i & 0x002, i & 0x004, i & 0x008, 0,
// i & 0x010, i & 0x020, i & 0x040, i & 0x080 ].map(a => !!a);
// l.push(overlay(arr, 1));
// }
// let lines = [];
// for (let i = 0; i < 16; i++) {
// lines.push(l.slice(i * 16, (i + 1) * 16).map(n => n < 10 ? n + ', ' : n + ',').join(' '));
// }
// console.log(lines.join('\n'));

View File

@ -1,39 +0,0 @@
import type MapScene from './scene/MapScene';
import Layer from './util/Layer';
import { Asset } from './util/Asset';
export default class TilesetManager {
scene: MapScene;
wallLocations: {[index: number]: { res: number; ind: number; identifier: string }} = {};
groundLocations: {[index: number]: { res: number; ind: number; identifier: string }} = {};
overlayLocations: {[index: number]: { res: number; ind: number; identifier: string }} = {};
indexes: {[tileset_key: string]: number} = {};
private currentWallInd: number = 0;
private currentGroundInd: number = 0;
private currentOverlayInd: number = 0;
constructor(scene: MapScene) {
this.scene = scene;
}
init(assets: Asset[]) {
for (let tileset of assets.filter(a => a.type === 'wall' )) this.addTileset(tileset.identifier, Layer.wall);
for (let tileset of assets.filter(a => a.type === 'ground' )) this.addTileset(tileset.identifier, Layer.floor);
// for (let tileset of assets.filter(a => a.type === 'overlay')) this.addTileset(tileset.key, Layer.overlay);
}
private addTileset(identifier: string, layer: Layer): void {
let res = this.scene.textures.get(identifier).getSourceImage(0).width / 9;
let ind = (layer === Layer.wall ? this.currentWallInd : layer === Layer.floor ? this.currentGroundInd : this.currentOverlayInd);
this[layer === Layer.wall ? 'wallLocations' : layer === Layer.floor ?
'groundLocations' : 'overlayLocations'][ind] = { res, ind, identifier };
this.indexes[identifier] = ind;
if (layer === Layer.wall) this.currentWallInd++;
else if (layer === Layer.floor) this.currentGroundInd++;
else this.currentOverlayInd++;
}
}

View File

@ -5,14 +5,15 @@ import { Vec2, Vec4 } from './util/Vec';
export default class TilesetPatcher { export default class TilesetPatcher {
constructor(private scene: Phaser.Scene) {} constructor(private scene: Phaser.Scene) {}
patch(tileset_key: string, tile_size: number): void { async patch(tileset_key: string, tile_size: number): Promise<void> {
return new Promise<void>(resolve => {
const s = Date.now(); const s = Date.now();
const canvas = new Phaser.GameObjects.RenderTexture(this.scene, 0, 0, 10 * tile_size, 5 * tile_size); const canvas = new Phaser.GameObjects.RenderTexture(this.scene, 0, 0, 10 * tile_size, 5 * tile_size);
canvas.draw(tileset_key); canvas.draw(tileset_key);
let part: Phaser.GameObjects.Sprite | Phaser.GameObjects.RenderTexture let part: Phaser.GameObjects.Sprite | Phaser.GameObjects.RenderTexture
= new Phaser.GameObjects.Sprite(this.scene, 0, 0, tileset_key); = new Phaser.GameObjects.Sprite(this.scene, 0, 0, tileset_key, '__BASE');
part.setOrigin(0, 0); part.setOrigin(0, 0);
function draw(source: Vec4, dest: Vec2) { function draw(source: Vec4, dest: Vec2) {
@ -150,9 +151,14 @@ export default class TilesetPatcher {
draw(new Vec4(1, 1, 2, 2), new Vec2(9, 4)); draw(new Vec4(1, 1, 2, 2), new Vec2(9, 4));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(9, 4)); draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(9, 4));
canvas.snapshot((img: any) => {
this.scene.textures.removeKey(tileset_key); this.scene.textures.removeKey(tileset_key);
canvas.saveTexture(tileset_key); this.scene.textures.addSpriteSheet(tileset_key, img, { frameWidth: tile_size, frameHeight: tile_size });
console.log(`Patched '${tileset_key}' in ${Date.now() - s} ms.`); console.log(`Patched '${tileset_key}' in ${Date.now() - s} ms.`);
resolve();
});
});
} }
} }

View File

@ -1,4 +1,3 @@
import { Vec2 } from './util/Vec';
import { generateId } from './util/Helpers'; import { generateId } from './util/Helpers';
export interface SerializedToken { export interface SerializedToken {
@ -10,8 +9,8 @@ export interface SerializedToken {
} }
export default class Token extends Phaser.GameObjects.Container { export default class Token extends Phaser.GameObjects.Container {
sprite: Phaser.GameObjects.Sprite | null = null; sprite: Phaser.GameObjects.Sprite;
shadow: Phaser.GameObjects.Sprite | null = null; shadow: Phaser.GameObjects.Sprite;
currentFrame: number = 0; currentFrame: number = 0;
@ -25,6 +24,20 @@ export default class Token extends Phaser.GameObjects.Container {
constructor(scene: Phaser.Scene, x: number, y: number, tex: string) { constructor(scene: Phaser.Scene, x: number, y: number, tex: string) {
super(scene, x, y); super(scene, x, y);
this.shadow = new Phaser.GameObjects.Sprite(this.scene, 0, 0, '');
this.shadow.setOrigin(0, 0);
this.shadow.setScale(1 / this.shadow.width, 0.25 / this.shadow.height);
this.shadow.setTint(0x000000);
this.shadow.setAlpha(0.1, 0.1, 0.3, 0.3);
this.list.push(this.shadow);
this.sprite = new Phaser.GameObjects.Sprite(this.scene, 0, 0, '');
this.sprite.setOrigin(0, 0);
this.sprite.setScale(1 / this.sprite.width, 1 / this.sprite.height);
this.setPosition(this.x, this.y);
this.list.push(this.sprite);
this.setTexture(tex); this.setTexture(tex);
this.uuid = generateId(32); this.uuid = generateId(32);
@ -37,28 +50,16 @@ export default class Token extends Phaser.GameObjects.Container {
} }
setTexture(tex: string) { setTexture(tex: string) {
if (this.shadow != null) this.shadow.setTexture(tex); this.shadow.setTexture(tex);
else { this.sprite.setTexture(tex);
this.shadow = new Phaser.GameObjects.Sprite(this.scene, -4, -4, tex);
this.shadow.setOrigin(0, 0);
this.shadow.setScale(4, 1);
this.shadow.setTint(0x000000);
this.shadow.setAlpha(0.1, 0.1, 0.3, 0.3);
this.list.push(this.shadow);
}
this.width = this.shadow.width * 4; this.shadow.setScale(1 / this.shadow.width, 0.25 / this.shadow.height);
this.height = this.shadow.height * 4; this.sprite.setScale(1 / this.sprite.width, 1 / this.sprite.height);
this.shadow.y = this.height - 26;
if (this.sprite != null) this.sprite.setTexture(tex); this.shadow.y = this.sprite.displayHeight - this.shadow.displayHeight - 0.025;
else {
this.sprite = new Phaser.GameObjects.Sprite(this.scene, -4, -4, tex); this.width = this.sprite.displayWidth;
this.sprite.setOrigin(0, 0); this.height = this.sprite.displayHeight;
this.sprite.setScale(4, 4);
this.setPosition(this.x / 4, this.y / 4);
this.list.push(this.sprite);
}
} }
setFrame(frame: number): void { setFrame(frame: number): void {
@ -83,11 +84,14 @@ export default class Token extends Phaser.GameObjects.Container {
this.hovered = hovered; this.hovered = hovered;
if (!hovered && !this.selected) { if (!hovered && !this.selected) {
this.sprite.resetPipeline(); // this.sprite.resetPipeline();
this.sprite.setTint(0xffffff);
return; return;
} }
if (!this.selected) this.sprite.setPipeline('brighten'); if (!this.selected) this.sprite.setTint(0x999999);
// if (!this.selected) this.sprite.setPipeline('brighten');
} }
setSelected(selected: boolean) { setSelected(selected: boolean) {
@ -96,33 +100,27 @@ export default class Token extends Phaser.GameObjects.Container {
this.selected = selected; this.selected = selected;
if (!selected) { if (!selected) {
if (!this.hovered) this.sprite.resetPipeline(); // if (!this.hovered) this.sprite.resetPipeline();
else this.sprite.setPipeline('brighten'); // else this.sprite.setPipeline('brighten');
if (!this.hovered) this.sprite.setTint(0xffffff);
else this.sprite.setTint(0x999999);
} }
else { else {
this.sprite.setPipeline('outline'); // this.sprite.setPipeline('outline');
this.sprite.setTint(0x000000);
// @ts-ignore // @ts-ignore
// this.sprite.pipeline.setFloat1('tex_size', this.sprite.texture.source[0].width); // this.sprite.pipeline.setFloat1('tex_size', this.sprite.texture.source[0].width);
} }
} }
setPosition(x?: number, y?: number, z?: number, w?: number): this {
Phaser.GameObjects.Container.prototype.setPosition.call(this, (x || 0) * 4, (y || 0) * 4, z, w);
return this;
}
getPosition(): Vec2 {
return new Vec2(this.x / 4, this.y / 4);
}
// Serialization Methods // Serialization Methods
serialize(): string { serialize(): string {
return JSON.stringify(({ return JSON.stringify(({
uuid: this.uuid, uuid: this.uuid,
sprite: this.sprite ? this.sprite.texture.key : '', sprite: this.sprite ? this.sprite.texture.key : '',
frame: this.currentFrame, frame: this.currentFrame,
x: this.x / 4, x: this.x,
y: this.y / 4 y: this.y
} as SerializedToken)); } as SerializedToken));
} }

View File

@ -31,7 +31,7 @@ export default class TokenMode {
init() { init() {
// Create cursor hover sprite // Create cursor hover sprite
this.cursor = this.scene.add.sprite(0, 0, 'cursor'); this.cursor = this.scene.add.sprite(0, 0, 'cursor');
this.cursor.setScale(4, 4); this.cursor.setScale(1 / 16, 1 / 16);
this.cursor.setDepth(1000); this.cursor.setDepth(1000);
this.cursor.setOrigin(0, 0); this.cursor.setOrigin(0, 0);
this.cursor.setVisible(false); this.cursor.setVisible(false);
@ -58,15 +58,15 @@ export default class TokenMode {
update() { update() {
this.active = true; this.active = true;
let selectedTilePos = new Vec2(Math.floor(this.scene.view.cursorWorld.x / 64), Math.floor(this.scene.view.cursorWorld.y / 64)); let selectedTilePos = new Vec2(Math.floor(this.scene.view.cursorWorld.x), Math.floor(this.scene.view.cursorWorld.y));
if (this.movingTokens) this.moving(); if (this.movingTokens) this.moving();
if (!this.movingTokens) this.selecting(); if (!this.movingTokens) this.selecting();
if (this.selectedTokens.length > 0 && !this.movingTokens) this.tokenMoveControls(); if (this.selectedTokens.length > 0 && !this.movingTokens) this.tokenMoveControls();
this.tokenPreview!.setPosition(selectedTilePos.x * 16, selectedTilePos.y * 16); this.tokenPreview!.setPosition(selectedTilePos.x, selectedTilePos.y);
this.cursor!.setPosition(selectedTilePos.x * 64, selectedTilePos.y * 64); this.cursor!.setPosition(selectedTilePos.x, selectedTilePos.y);
if (this.selectedTokenType === '') this.tokenPreview!.setVisible(false); if (this.selectedTokenType === '') this.tokenPreview!.setVisible(false);
if (this.selectedTokenType !== '') this.cursor!.setVisible(false); if (this.selectedTokenType !== '') this.cursor!.setVisible(false);
@ -80,7 +80,7 @@ export default class TokenMode {
updateRectangleSelect() { updateRectangleSelect() {
const cursor = this.scene.view.cursorWorld; const cursor = this.scene.view.cursorWorld;
let selectedTilePos = new Vec2(Math.floor(cursor.x / 64), Math.floor(cursor.y / 64)); let selectedTilePos = new Vec2(Math.floor(cursor.x), Math.floor(cursor.y));
this.primitives.forEach((v) => v.destroy()); this.primitives.forEach((v) => v.destroy());
this.primitives = []; this.primitives = [];
@ -97,7 +97,6 @@ export default class TokenMode {
this.primitives.forEach((v) => { this.primitives.forEach((v) => {
v.setOrigin(0, 0); v.setOrigin(0, 0);
v.setScale(64, 64);
v.setLineWidth(0.03); v.setLineWidth(0.03);
v.setDepth(300); v.setDepth(300);
}); });
@ -109,14 +108,14 @@ export default class TokenMode {
this.movingTokens = true; this.movingTokens = true;
const cursor = this.scene.view.cursorWorld; const cursor = this.scene.view.cursorWorld;
this.tileGrabPos = new Vec2(Math.floor(cursor.x / 64), Math.floor(cursor.y / 64)); this.tileGrabPos = new Vec2(Math.floor(cursor.x), Math.floor(cursor.y));
this.prevSerialized = []; this.prevSerialized = [];
this.selectedTokens.forEach(t => this.prevSerialized.push(t.serialize())); this.selectedTokens.forEach(t => this.prevSerialized.push(t.serialize()));
} }
createToken(): Token { createToken(): Token {
let token = new Token(this.scene, Math.floor(this.scene.view.cursorWorld.x / 4 / 16) * 16, let token = new Token(this.scene, Math.floor(this.scene.view.cursorWorld.x),
Math.floor(this.scene.view.cursorWorld.y / 4 / 16) * 16, this.selectedTokenType); Math.floor(this.scene.view.cursorWorld.y), this.selectedTokenType);
this.scene.add.existing(token); this.scene.add.existing(token);
this.scene.tokens.push(token); this.scene.tokens.push(token);
@ -150,16 +149,16 @@ export default class TokenMode {
private tokenMoveControls(): void { private tokenMoveControls(): void {
if (this.scene.i.keyPressed('UP')) { if (this.scene.i.keyPressed('UP')) {
this.moveToken(0, -16, 2); this.moveToken(0, -1, 2);
} }
if (this.scene.i.keyPressed('LEFT')) { if (this.scene.i.keyPressed('LEFT')) {
this.moveToken(-16, 0, 1); this.moveToken(-1, 0, 1);
} }
if (this.scene.i.keyPressed('DOWN')) { if (this.scene.i.keyPressed('DOWN')) {
this.moveToken(0, 16, 0); this.moveToken(0, 1, 0);
} }
if (this.scene.i.keyPressed('RIGHT')) { if (this.scene.i.keyPressed('RIGHT')) {
this.moveToken(16, 0, 3); this.moveToken(1, 0, 3);
} }
} }
@ -175,8 +174,8 @@ export default class TokenMode {
let prevSerialized: string[] = []; let prevSerialized: string[] = [];
this.selectedTokens.forEach((token) => { this.selectedTokens.forEach((token) => {
prevSerialized.push(token.serialize()); prevSerialized.push(token.serialize());
token.x += x * 4; token.x += x;
token.y += y * 4; token.y += y;
token.setFrame(frame); token.setFrame(frame);
}); });
@ -201,7 +200,7 @@ export default class TokenMode {
for (let i = this.scene.tokens.length - 1; i >= 0; i--) { for (let i = this.scene.tokens.length - 1; i >= 0; i--) {
let token = this.scene.tokens[i]; let token = this.scene.tokens[i];
if (cursor.x >= token.x && cursor.y >= token.y && cursor.x <= token.x + token.width - 8 && cursor.y <= token.y + token.height - 8) { if (cursor.x >= token.x && cursor.y >= token.y && cursor.x <= token.x + token.width && cursor.y <= token.y + token.height) {
this.hoveredToken = token; this.hoveredToken = token;
break; break;
} }
@ -237,7 +236,7 @@ export default class TokenMode {
} }
// Start a rectangle selection // Start a rectangle selection
else { else {
this.startTilePos = new Vec2(Math.floor(cursor.x / 64), Math.floor(cursor.y / 64)); this.startTilePos = new Vec2(Math.floor(cursor.x), Math.floor(cursor.y));
} }
} }
// Selecting existing token to move // Selecting existing token to move
@ -266,7 +265,7 @@ export default class TokenMode {
this.primitives.forEach((v) => v.destroy()); this.primitives.forEach((v) => v.destroy());
this.primitives = []; this.primitives = [];
let selectedTilePos = new Vec2(Math.floor(cursor.x / 64), Math.floor(cursor.y / 64)); let selectedTilePos = new Vec2(Math.floor(cursor.x), Math.floor(cursor.y));
let a = new Vec2(Math.min(this.startTilePos.x, selectedTilePos.x), Math.min(this.startTilePos.y, selectedTilePos.y)); let a = new Vec2(Math.min(this.startTilePos.x, selectedTilePos.x), Math.min(this.startTilePos.y, selectedTilePos.y));
let b = new Vec2(Math.max(this.startTilePos.x, selectedTilePos.x), Math.max(this.startTilePos.y, selectedTilePos.y)); let b = new Vec2(Math.max(this.startTilePos.x, selectedTilePos.x), Math.max(this.startTilePos.y, selectedTilePos.y));
@ -278,7 +277,7 @@ export default class TokenMode {
} }
for (let token of this.scene.tokens) { for (let token of this.scene.tokens) {
let tokenTilePos = new Vec2(Math.floor(token.x / 64), Math.floor(token.y / 64)); let tokenTilePos = new Vec2(Math.floor(token.x), Math.floor(token.y));
if (tokenTilePos.x >= a.x && tokenTilePos.y >= a.y && tokenTilePos.x <= b.x && tokenTilePos.y <= b.y) { if (tokenTilePos.x >= a.x && tokenTilePos.y >= a.y && tokenTilePos.x <= b.x && tokenTilePos.y <= b.y) {
let selected = this.scene.i.keyDown('CTRL') ? !this.selectedIncludes(token) : true; let selected = this.scene.i.keyDown('CTRL') ? !this.selectedIncludes(token) : true;
@ -360,13 +359,13 @@ export default class TokenMode {
return; return;
} }
let newTileGrabPos = new Vec2(Math.floor(cursor.x / 64), Math.floor(cursor.y / 64)); let newTileGrabPos = new Vec2(Math.floor(cursor.x), Math.floor(cursor.y));
let offset = new Vec2(newTileGrabPos.x - this.tileGrabPos.x, newTileGrabPos.y - this.tileGrabPos.y); let offset = new Vec2(newTileGrabPos.x - this.tileGrabPos.x, newTileGrabPos.y - this.tileGrabPos.y);
if (offset.x === 0 && offset.y === 0) return; if (offset.x === 0 && offset.y === 0) return;
this.movedTokens = true; this.movedTokens = true;
this.tileGrabPos = newTileGrabPos; this.tileGrabPos = newTileGrabPos;
this.selectedTokens.forEach(tkn => tkn.setPosition(tkn.x / 4 + offset.x * 16, tkn.y / 4 + offset.y * 16)); this.selectedTokens.forEach(tkn => tkn.setPosition(tkn.x + offset.x, tkn.y + offset.y));
} }
} }
} }

View File

@ -12,8 +12,8 @@ export default class WorldView {
cursorWorld: Vec2 = new Vec2(); cursorWorld: Vec2 = new Vec2();
lastCursorWorld: Vec2 = new Vec2(); lastCursorWorld: Vec2 = new Vec2();
zoomLevels: number[] = [10, 17, 25, 33, 40, 50, 60, 67, 75, 80, 90, 100, 110, 125, 150, 175, 200, 250, 300, 400, 500]; zoomLevels: number[] = [ 5, 6, 8, 10, 15, 20, 25, 33, 40, 50, 60, 75, 100, 125, 150, 200, 300 ];
zoomLevel = 11; zoomLevel = 9;
constructor(scene: MapScene) { constructor(scene: MapScene) {
this.scene = scene; this.scene = scene;
@ -22,11 +22,26 @@ export default class WorldView {
init(): void { init(): void {
this.camera = this.scene.cameras.main; this.camera = this.scene.cameras.main;
this.camera.setBackgroundColor('#090d24'); this.camera.setBackgroundColor('#090d24');
this.camera.setZoom(this.zoomLevels[this.zoomLevel]);
this.camera.setScroll(-this.camera.width / 2.2, -this.camera.height / 2.2);
this.scene.i.bindScrollEvent((delta: number) => { this.scene.i.bindScrollEvent((delta: number) => {
if (!this.scene.token.movingTokens && !this.scene.ui.uiActive) { if (!this.scene.token.movingTokens && !this.scene.ui.uiActive) {
const lastZoom = this.zoomLevels[this.zoomLevel];
this.zoomLevel = clamp(this.zoomLevel + delta, 0, this.zoomLevels.length - 1); this.zoomLevel = clamp(this.zoomLevel + delta, 0, this.zoomLevels.length - 1);
this.camera!.setZoom(this.zoomLevels[this.zoomLevel] / 100); const zoom = this.zoomLevels[this.zoomLevel];
this.scene.tweens.add({
targets: this.camera,
zoom: { from: lastZoom, to: zoom },
ease: 'Cubic',
duration: 150,
repeat: 0
});
// this.scene.tweens.add({ targets: this.camera!, duration: 150, props: { zoom: this.zoomLevels[this.zoomLevel / 100]}});
// this.camera!.setZoom(this.zoomLevels[this.zoomLevel] / 100);
} }
}); });
} }
@ -47,8 +62,8 @@ export default class WorldView {
private pan() { private pan() {
if (this.scene.input.activePointer.middleButtonDown()) { if (this.scene.input.activePointer.middleButtonDown()) {
this.camera!.scrollX += Math.round((this.lastCursorScreen.x - this.cursorScreen.x) / this.camera!.zoom); this.camera!.scrollX += (this.lastCursorScreen.x - this.cursorScreen.x) / this.camera!.zoom;
this.camera!.scrollY += Math.round((this.lastCursorScreen.y - this.cursorScreen.y) / this.camera!.zoom); this.camera!.scrollY += (this.lastCursorScreen.y - this.cursorScreen.y) / this.camera!.zoom;
} }
} }
} }

View File

@ -1,8 +1,8 @@
import Token from '../Token'; import Token from '../Token';
import type MapScene from '../scene/MapScene'; import type MapScene from '../scene/MapScene';
import Layer from '../util/Layer';
import { Vec2 } from '../util/Vec'; import { Vec2 } from '../util/Vec';
import { Layer } from '../util/Layer';
export default class HistoryElement { export default class HistoryElement {
scene: MapScene; scene: MapScene;
@ -19,7 +19,7 @@ export default class HistoryElement {
console.log('Undo', this.type); console.log('Undo', this.type);
if (this.type === 'tile') { if (this.type === 'tile') {
for (let tile of this.data as {pos: Vec2; layer: Layer; lastTile: number; tile: number}[]) { for (let tile of this.data as {pos: Vec2; layer: Layer; lastTile: number; tile: number}[]) {
this.scene.map.setTile(tile.layer, tile.lastTile, tile.pos.x, tile.pos.y); this.scene.map.activeLayer.setTile(tile.layer, tile.lastTile, tile.pos.x, tile.pos.y);
this.scene.lighting.tileUpdatedAt(tile.pos.x, tile.pos.y); this.scene.lighting.tileUpdatedAt(tile.pos.x, tile.pos.y);
} }
} }
@ -69,7 +69,7 @@ export default class HistoryElement {
console.log('Redo', this.type); console.log('Redo', this.type);
if (this.type === 'tile') { if (this.type === 'tile') {
for (let tile of this.data as {pos: Vec2; layer: Layer; lastTile: number; tile: number}[]) { for (let tile of this.data as {pos: Vec2; layer: Layer; lastTile: number; tile: number}[]) {
this.scene.map.setTile(tile.layer, tile.tile, tile.pos.x, tile.pos.y); this.scene.map.activeLayer.setTile(tile.layer, tile.tile, tile.pos.x, tile.pos.y);
this.scene.lighting.tileUpdatedAt(tile.pos.x, tile.pos.y); this.scene.lighting.tileUpdatedAt(tile.pos.x, tile.pos.y);
} }
} }

View File

@ -1,14 +1,13 @@
import UISidebar from './UISidebar'; import UISidebar from './UISidebar';
import type MapScene from '../../scene/MapScene'; import type MapScene from '../../scene/MapScene';
import Layer from '../../util/Layer';
import { Asset } from '../../util/Asset'; import { Asset } from '../../util/Asset';
export default class UITileSidebar extends UISidebar { export default class UITileSidebar extends UISidebar {
walls: string[] = []; walls: string[] = [];
grounds: string[] = []; grounds: string[] = [];
overlays: string[] = []; details: string[] = [];
constructor(scene: MapScene, x: number, y: number, assets: Asset[]) { constructor(scene: MapScene, x: number, y: number, assets: Asset[]) {
super(scene, x, y); super(scene, x, y);
@ -30,18 +29,18 @@ export default class UITileSidebar extends UISidebar {
this.list.push(add_ground); this.list.push(add_ground);
this.sprites.push(add_ground); this.sprites.push(add_ground);
for (let tileset of assets.filter((a) => a.type === 'ground')) for (let tileset of assets.filter((a) => a.type === 'floor'))
this.addGround(tileset.identifier); this.addGround(tileset.identifier);
let add_overlay = new Phaser.GameObjects.Sprite(this.scene, 9 + x * 21 * 3, 9 + 9 * 21 * 3, 'ui_sidebar_browse'); let add_detail = new Phaser.GameObjects.Sprite(this.scene, 9 + x * 21 * 3, 9 + 9 * 21 * 3, 'ui_sidebar_browse');
add_overlay.setName('add_overlay'); add_detail.setName('add_detail');
add_overlay.setScale(3); add_detail.setScale(3);
add_overlay.setOrigin(0, 0); add_detail.setOrigin(0, 0);
this.list.push(add_overlay); this.list.push(add_detail);
this.sprites.push(add_overlay); this.sprites.push(add_detail);
for (let tileset of assets.filter((a) => a.type === 'ground')) for (let tileset of assets.filter((a) => a.type === 'detail'))
this.addOverlay(tileset.identifier); this.addDetail(tileset.identifier);
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
if (i % 4 !== 0) this.backgrounds[i].setFrame(0); if (i % 4 !== 0) this.backgrounds[i].setFrame(0);
@ -50,21 +49,21 @@ export default class UITileSidebar extends UISidebar {
elemClick(x: number, y: number): void { elemClick(x: number, y: number): void {
if (y < 4) { if (y < 4) {
this.scene.architect.activeTileset = this.scene.map.manager.indexes[this.walls[x + y * 3]]; this.scene.architect.activeTileset = this.scene.map.tileStore.indices[this.walls[x + y * 3]];
this.scene.architect.activeLayer = Layer.wall; this.scene.architect.activeLayer = 'wall';
} }
else if (y < 8) { else if (y < 8) {
this.scene.architect.activeTileset = this.scene.map.manager.indexes[this.grounds[x + (y - 4) * 3]]; this.scene.architect.activeTileset = this.scene.map.tileStore.indices[this.grounds[x + (y - 4) * 3]];
this.scene.architect.activeLayer = Layer.floor; this.scene.architect.activeLayer = 'floor';
} }
else { else {
this.scene.architect.activeTileset = this.scene.map.manager.indexes[this.overlays[x + (y - 8) * 3]]; this.scene.architect.activeTileset = this.scene.map.tileStore.indices[this.details[x + (y - 8) * 3]];
this.scene.architect.activeLayer = Layer.overlay; this.scene.architect.activeLayer = 'detail';
} }
} }
private addWall(tileset: string): void { private addWall(tileset: string): void {
this.addTilesetSprite(tileset, this.walls.length % 3, Math.floor(this.walls.length / 3) + 1, 17); this.addTilesetSprite(tileset, this.walls.length % 3, Math.floor(this.walls.length / 3) + 1, 13);
(this.getByName('add_wall') as Phaser.GameObjects.Sprite).x = 9 + ((this.walls.length + 1) % 3 * 21 * 3); (this.getByName('add_wall') as Phaser.GameObjects.Sprite).x = 9 + ((this.walls.length + 1) % 3 * 21 * 3);
(this.getByName('add_wall') as Phaser.GameObjects.Sprite).y = 9 + (Math.floor((this.walls.length + 1) / 3 + 1) * 21 * 3); (this.getByName('add_wall') as Phaser.GameObjects.Sprite).y = 9 + (Math.floor((this.walls.length + 1) / 3 + 1) * 21 * 3);
this.walls.push(tileset); this.walls.push(tileset);
@ -77,11 +76,11 @@ export default class UITileSidebar extends UISidebar {
this.grounds.push(tileset); this.grounds.push(tileset);
} }
private addOverlay(tileset: string): void { private addDetail(tileset: string): void {
this.addTilesetSprite(tileset, this.overlays.length % 3, Math.floor(this.overlays.length / 3) + 9, 33); this.addTilesetSprite(tileset, this.details.length % 3, Math.floor(this.details.length / 3) + 9, 33);
(this.getByName('add_overlay') as Phaser.GameObjects.Sprite).x = 9 + ((this.overlays.length + 1) % 3 * 21 * 3); (this.getByName('add_detail') as Phaser.GameObjects.Sprite).x = 9 + ((this.details.length + 1) % 3 * 21 * 3);
(this.getByName('add_overlay') as Phaser.GameObjects.Sprite).y = 9 + (Math.floor((this.overlays.length + 1) / 3 + 9) * 21 * 3); (this.getByName('add_detail') as Phaser.GameObjects.Sprite).y = 9 + (Math.floor((this.details.length + 1) / 3 + 9) * 21 * 3);
this.overlays.push(tileset); this.details.push(tileset);
} }
private addTilesetSprite(key: string, x: number, y: number, frame: number) { private addTilesetSprite(key: string, x: number, y: number, frame: number) {

View File

@ -74,7 +74,7 @@ export default class UITokenSidebar extends UISidebar {
let token = new Token(this.scene, 0, 0, sprite); let token = new Token(this.scene, 0, 0, sprite);
Phaser.GameObjects.Sprite.prototype.setPosition.call(token, 12 + x * 21 * 3, 12 + y * 21 * 3); Phaser.GameObjects.Sprite.prototype.setPosition.call(token, 12 + x * 21 * 3, 12 + y * 21 * 3);
token.setScale(3 / 4); token.setScale(3);
this.sprites.push(token); this.sprites.push(token);
this.list.push(token); this.list.push(token);

View File

@ -1,6 +1,5 @@
import type Lighting from './Lighting'; import type Lighting from './Lighting';
import Layer from '../util/Layer';
import { Vec2 } from '../util/Vec'; import { Vec2 } from '../util/Vec';
export default class LightSource { export default class LightSource {
@ -36,7 +35,7 @@ export default class LightSource {
let dir = new Vec2(Math.cos(i * 1.25 * (Math.PI / 180)) / 32, Math.sin(i * 1.25 * (Math.PI / 180)) / 32); 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; let dist = 0;
while (this.light.scene.map.getTileset(Layer.wall, Math.floor(start.x + ray.x), Math.floor(start.y + ray.y)) === -1 && while (this.light.scene.map.activeLayer.getTile('wall', Math.floor(start.x + ray.x), Math.floor(start.y + ray.y)) === -1 &&
(dist = Math.sqrt(Math.pow(ray.x, 2) + Math.pow(ray.y, 2))) < this.radius / 16) { (dist = Math.sqrt(Math.pow(ray.x, 2) + Math.pow(ray.y, 2))) < this.radius / 16) {
ray.x += dir.x; ray.x += dir.x;

View File

@ -1,4 +1,4 @@
import MapChunk from '../MapChunk'; import { CHUNK_SIZE } from '../map/MapChunk';
import type MapScene from '../scene/MapScene'; import type MapScene from '../scene/MapScene';
import LightChunk from './LightChunk'; import LightChunk from './LightChunk';
@ -23,9 +23,9 @@ export default class Lighting {
init(size: Vec2) { init(size: Vec2) {
this.size = size; this.size = size;
for (let i = 0; i < Math.ceil(size.y / (MapChunk.CHUNK_SIZE * 2)); i++) { for (let i = 0; i < Math.ceil(size.y / (CHUNK_SIZE * 2)); i++) {
this.chunks[i] = []; this.chunks[i] = [];
for (let j = 0; j < Math.ceil(size.x / (MapChunk.CHUNK_SIZE * 2)); j++) { for (let j = 0; j < Math.ceil(size.x / (CHUNK_SIZE * 2)); j++) {
this.chunks[i][j] = new LightChunk(this, j, i); this.chunks[i][j] = new LightChunk(this, j, i);
} }
} }

99
app/src/editor/map/Map.ts Executable file
View File

@ -0,0 +1,99 @@
import * as Phaser from 'phaser';
import MapLayer from './MapLayer';
import TileStore from './TileStore';
import MapChunk, { CHUNK_SIZE } from './MapChunk';
import { Vec2 } from '../util/Vec';
import { Asset } from '../util/Asset';
export default class MapData {
tileStore: TileStore = new TileStore();
size: Vec2 = new Vec2(0, 0);
activeLayer: MapLayer = {} as MapLayer;
private layers: MapLayer[] = [];
private chunks: MapChunk[][] = [];
init(scene: Phaser.Scene, size: Vec2, assets: Asset[]) {
this.size = size;
this.tileStore.init(scene.textures, assets);
this.layers.push(new MapLayer(size, this.handleDirty));
this.activeLayer = this.layers[0];
for (let i = 0; i < Math.ceil(size.y / CHUNK_SIZE); i++) {
this.chunks[i] = [];
for (let j = 0; j < Math.ceil(size.x / CHUNK_SIZE); j++) {
this.chunks[i][j] = new MapChunk(scene, new Vec2(j, i), this.activeLayer, this.tileStore);
}
}
}
update(): void {
let start = Date.now();
for (let arr of this.chunks) {
for (let chunk of arr) {
chunk.redraw();
if (Date.now() - start > 10) break;
}
}
// if (this.scene.i.keyPressed('S')) this.saveMap();
// if (this.scene.i.keyPressed('L')) this.loadMap(this.savedMapData);
}
private handleDirty = (x: number, y: number) => {
this.chunks[Math.floor(y / CHUNK_SIZE)][Math.floor(x / CHUNK_SIZE)].setDirty(new Vec2(x % CHUNK_SIZE, y % CHUNK_SIZE));
};
// private saveMap() {
// let mapData: number[][] = [];
// for (let k = 0; k < 3; k++) {
// let tile = 0;
// let count = 0;
// mapData[k] = [];
// for (let i = 0; i < this.size.x * this.size.y; i++) {
// let x = i % this.size.x;
// let y = Math.floor(i / this.size.x);
// if (this.getTileset(k, x, y) === tile) count++;
// else {
// if (i !== 0) {
// mapData[k].push(tile);
// mapData[k].push(count);
// }
// tile = this.getTileset(k, x, y);
// count = 1;
// }
// }
// }
// this.savedMapData = mapData;
// }
// private loadMap(dat: number[][]) {
// for (let k = 0; k < 3; k++) {
// let offset = 0;
// for (let i = 0; i < dat[k].length / 2; i++) {
// let tile = dat[k][i * 2];
// let count = dat[k][i * 2 + 1];
// for (let t = 0; t < count; t++) {
// let x = (offset + t) % this.size.x;
// let y = Math.floor((offset + t) / this.size.x);
// this.setTile(k, tile, x, y);
// }
// offset += count;
// }
// }
// }
}

114
app/src/editor/map/MapChunk.ts Executable file
View File

@ -0,0 +1,114 @@
import * as Phaser from 'phaser';
import MapLayer from './MapLayer';
import TileStore from './TileStore';
import { Vec2 } from '../util/Vec';
export const TILE_SIZE = 16;
export const CHUNK_SIZE = 32;
export const DIRTY_LIMIT = (CHUNK_SIZE * CHUNK_SIZE) / 2;
/**
* A visual representation of a chunk of a MapLayer.
*/
export default class MapChunk extends Phaser.GameObjects.RenderTexture {
private dirtyList: Vec2[] = [];
private fullyDirty: boolean = true;
constructor(scene: Phaser.Scene, private pos: Vec2, private 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);
this.setScale(1 / TILE_SIZE);
this.setOrigin(0, 0);
scene.add.existing(this);
}
/**
* Indicates that a position on the chunk is dirty so it will be re-rendered.
*
* @param {Vec2} pos - The position that is dirtied.
*/
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 {
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;
return true;
}
if (this.dirtyList.length === 0) return false;
for (let elem of this.dirtyList) this.drawTile(elem.x, elem.y);
this.dirtyList = [];
return true;
}
/**
* Redraws the tile at the specified position,
* based on the current data on the MapLayer.
*
* @param {number} x - The x position to draw at.
* @param {number} y - The y position to draw at.
*/
private drawTile(x: number, y: number): void {
let mX = x + this.pos.x * CHUNK_SIZE;
let mY = y + this.pos.y * CHUNK_SIZE;
let wallTile = this.layer.getTile('wall', mX, mY);
let wallTileIndex = this.layer.getTileIndex('wall', mX, mY);
let floorTile = this.layer.getTile('floor', mX, mY);
let floorTileIndex = this.layer.getTileIndex('floor', mX, mY);
let detailTile = this.layer.getTile('detail', mX, mY);
let detailTileIndex = this.layer.getTileIndex('detail', mX, mY);
if (floorTile !== -1)
this.drawFrame(this.tileStore.floorTiles[floorTile].identifier, floorTileIndex, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
if (detailTile !== -1)
this.drawFrame(this.tileStore.detailTiles[detailTile].identifier, detailTileIndex, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
if (wallTile !== -1)
this.drawFrame(this.tileStore.wallTiles[wallTile].identifier, wallTileIndex, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
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);
}
}

View File

@ -0,0 +1,250 @@
import { Vec2 } from '../util/Vec';
import { Layer } from '../util/Layer';
import { clamp } from '../util/Helpers';
const WALL_FIELD = [
4, 4, 17, 17, 4, 4, 17, 17, 18, 18, 34, 13, 18, 18, 34, 13, 7, 7, 33, 33, 7, 7, 12, 12, 9, 9, 36, 35, 9, 9, 37, 10,
4, 4, 17, 17, 4, 4, 17, 17, 18, 18, 34, 13, 18, 18, 34, 13, 7, 7, 33, 33, 7, 7, 12, 12, 9, 9, 36, 35, 9, 9, 37, 10,
8, 8, 19, 19, 8, 8, 19, 19, 24, 24, 39, 29, 24, 24, 39, 29, 23, 23, 38, 38, 23, 23, 28, 28, 26, 26, 40, 47, 26, 26, 46, 30,
8, 8, 19, 19, 8, 8, 19, 19, 3, 3, 49, 11, 3, 3, 49, 11, 23, 23, 38, 38, 23, 23, 28, 28, 25, 25, 45, 31, 25, 25, 22, 5,
4, 4, 17, 17, 4, 4, 17, 17, 18, 18, 34, 13, 18, 18, 34, 13, 7, 7, 33, 33, 7, 7, 12, 12, 9, 9, 36, 35, 9, 9, 37, 10,
4, 4, 17, 17, 4, 4, 17, 17, 18, 18, 34, 13, 18, 18, 34, 13, 7, 7, 33, 33, 7, 7, 12, 12, 9, 9, 36, 35, 9, 9, 37, 10,
8, 8, 19, 19, 8, 8, 19, 19, 24, 24, 39, 29, 24, 24, 39, 29, 2, 2, 48, 48, 2, 2, 0, 0, 27, 27, 44, 32, 27, 27, 20, 6,
8, 8, 19, 19, 8, 8, 19, 19, 3, 3, 49, 11, 3, 3, 49, 11, 2, 2, 48, 48, 2, 2, 0, 0, 1, 1, 21, 15, 1, 1, 16, 14
];
const FLOOR_FIELD = [
54, 20, 19, 19, 18, 4, 19, 19, 11, 11, 3, 3, 51, 51, 3, 3, 9, 52, 5, 5, 9, 52, 5, 5, 39, 39, 30, 30, 39, 39, 30, 30,
2, 12, 32, 32, 34, 6, 32, 32, 11, 11, 3, 3, 51, 51, 3, 3, 43, 38, 29, 29, 43, 38, 29, 29, 39, 39, 30, 30, 39, 39, 30, 30,
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49,
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49,
0, 33, 31, 31, 14, 7, 31, 31, 42, 42, 27, 27, 36, 36, 27, 27, 9, 52, 5, 5, 9, 52, 5, 5, 39, 39, 30, 30, 39, 39, 30, 30,
22, 15, 28, 28, 16, 37, 28, 28, 42, 42, 27, 27, 36, 36, 27, 27, 43, 38, 29, 29, 43, 38, 29, 29, 39, 39, 30, 30, 39, 39, 30, 30,
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49,
1, 41, 17, 17, 40, 46, 17, 17, 21, 21, 8, 8, 45, 45, 8, 8, 23, 47, 26, 26, 23, 47, 26, 26, 48, 48, 49, 49, 48, 48, 49, 49
];
type LayerData = { tiles: number[][]; tilesets: number[][] };
export default class MapLayer {
private data: { [ key in Layer ]: LayerData } = {
wall: { tiles: [], tilesets: [] }, floor: { tiles: [], tilesets: [] }, detail: { tiles: [], tilesets: [] } };
constructor(public size: Vec2, private onDirty: (x: number, y: number) => void) {
const createLayerData = (startTile: number | (() => number), startTileset: number): LayerData => {
let layer: LayerData = { tiles: [], tilesets: [] };
for (let i = 0; i < this.size.y; i++) {
layer.tiles[i] = [];
layer.tilesets[i] = [];
for (let j = 0; j < this.size.x; j++) {
let tile = typeof(startTile) === 'number' ? startTile : startTile();
layer.tiles[i][j] = tile;
layer.tilesets[i][j] = startTileset;
}
}
return layer;
};
this.data.wall = createLayerData(0, -1);
this.data.floor = createLayerData(() => Math.floor(Math.random() * 6) + 54, 0);
this.data.detail = createLayerData(0, -1);
}
/**
* Convert a 3x3 array of wall states into a numeric value between 0 and 255.
*
* @param {boolean} walls - The walls
*/
static bitsToIndices(walls: boolean[]): number {
return (
(+walls[0] << 0) +
(+walls[1] << 1) +
(+walls[2] << 2) +
(+walls[3] << 3) +
(+walls[5] << 4) +
(+walls[6] << 5) +
(+walls[7] << 6) +
(+walls[8] << 7));
}
/**
* Returns a tile index for a wall based on it's surrounding walls.
*
* @param {boolean[]} walls - Surrounding walls boolean array.
* @param {number} current - The current wall value.
*/
static wall(walls: boolean[], current: number): number {
if (current === -1) return -1;
const ind = WALL_FIELD[MapLayer.bitsToIndices(walls)];
if (ind < 54) return ind;
return 54 + Math.floor(Math.random() * 6);
}
/**
* Returns a tile index for a floor based on it's surrounding walls.
*
* @param {boolean[]} walls - Surrounding walls boolean array.
* @param {number} current - The current floor value.
*/
static floor(walls: boolean[], current: number): number {
if (current === -1) return -1;
const ind = FLOOR_FIELD[MapLayer.bitsToIndices(walls)];
if (ind < 54) return ind;
return 54 + Math.floor(Math.random() * 6);
}
/**
* Returns a tile index for a detail based on it's surrounding details.
*
* @param {boolean[]} details - Surrounding details boolean array.
* @param {number} current - The current floor value.
*/
static detail(details: boolean[], current: number): number {
if (current === -1) return -1;
const ind = WALL_FIELD[MapLayer.bitsToIndices(details)];
if (ind < 54) return ind;
return 54 + Math.floor(Math.random() * 6);
}
/**
* Sets a tile to the tileset provided, automatically smart-tiling as needed.
*
* @param {Layer} layer - The internal layer to set the tile at.
* @param {number} tileset - The tileset to set the tile to.
* @param {number | Vec2} x - Either the x value of the position to set the tile at, or a vector for the full position.
* @param {number} y - The y value of the position if the x value is a number.
*
* @returns {boolean} - True if the tileset was changed, false otherwise.
*/
setTile(layer: Layer, tileset: number, x: number | Vec2, y?: number): boolean {
if (x instanceof Vec2) { y = x.y; x = x.x; }
if (x < 0 || y! < 0 || x >= this.size.x || y! >= this.size.y) return false;
if (this.setTileset(layer, x, y!, tileset)) {
// this.setTileIndex(layer, x, y!, 3);
this.autoTile(x, y!);
return true;
}
return false;
}
/**
* Gets the tileset at the specified position.
*
* @param layer - The internal layer to get the tileset at.
* @param {number} x - Either the x value of the position to get the tile set at, or a vector for the full position.
* @param {number} y - The y value of the position if the x value is a number.
*/
getTile(layer: Layer, x: number | Vec2, y?: number): number {
if (x instanceof Vec2) { y = x.y; x = x.x; }
return this.data[layer].tilesets[clamp(y!, 0, this.size.y - 1)][clamp(x, 0, this.size.x - 1)];
}
/**
* Gets the current tile index at a position.
*
* @param {Layer} layer - The internal layer to get the tile at.
* @param {number | Vec2} x - Either the x value of the position to get the tile at, or a vector for the full position.
* @param {number} y - The y value of the position if the x value is a number.
*
* @returns {number} - The tile index at the position specified.
*/
getTileIndex(layer: Layer, x: number | Vec2, y?: number): number {
if (x instanceof Vec2) { y = x.y; x = x.x; }
return this.data[layer].tiles[clamp(y!, 0, this.size.y - 1)][clamp(x, 0, this.size.x - 1)];
}
/**
* Sets a tile to the one provided.
*
* @param {Layer} layer - The layer to set the tileset at.
* @param {number | Vec2} x - Either the x value of the position to set the tileset at, or a vector for the full position.
* @param {number} y - Either the y value of the position if x is a number, or the tileset to set.
* @param {number} tile - The tileset to set if the x value is a number.
*
* @returns {boolean} - True if the tileset was changed, false otherwise.
*/
private setTileset(layer: Layer, x: number | Vec2, y: number, tile?: number): boolean {
if (x instanceof Vec2) { tile = y; y = x.y; x = x.x; };
const oldTileset = this.getTile(layer, x, y);
if (oldTileset === tile!) return false;
this.data[layer].tilesets[y][x] = tile!;
return true;
}
/**
* Sets the tile at the specified position to the index provided.
*/
private setTileIndex(layer: Layer, x: number, y: number, index: number): void {
this.data[layer].tiles[y][x] = index;
this.onDirty(x, y);
}
/**
* Automatically updates the tile indexes surrounding a position.
*
* @param {number} x - The x value of the position to center around.
* @param {number} y - The y value of the position to center around.
*/
private autoTile(x: number, y: number): void {
for (let i = clamp(x - 1, this.size.x - 1, 0); i <= clamp(x + 1, this.size.x - 1, 0); i++) {
for (let j = clamp(y - 1, this.size.y - 1, 0); j <= clamp(y + 1, this.size.y - 1, 0); j++) {
const solids = this.getTilesAround('wall', i, j).map(i => i !== -1);
const wall = MapLayer.wall(solids, this.getTileIndex('wall', i, j));
if (wall !== -1) this.setTileIndex('wall', i, j, wall);
const floor = MapLayer.floor(solids, this.getTileIndex('floor', i, j));
if (floor !== -1) this.setTileIndex('floor', i, j, floor);
const detail = MapLayer.detail(this.getTilesAround('detail', i, j).map(i => i !== -1), -1);
if (detail !== -1) this.setTileIndex('detail', i, j, detail);
}
}
}
/**
* Gets the 9 tiles in a 3x3 grid around the position specified.
*
* @param {Layer} layer - The internal layer to get the tileset at.
* @param {number} x - The x value of the position to center around.
* @param {number} y - The y value of the position to center around.
*
* @returns {number[]} a nine-element long array of the tiles around.
*/
private getTilesAround(layer: Layer, x: number, y: number): number[] {
let tilesets: number[] = [];
for (let i = -1; i <= 1; i++)
for (let j = -1; j <= 1; j++)
tilesets.push(this.getTile(layer, clamp(x + j, 0, this.size.x - 1), clamp(y + i, 0, this.size.y - 1)));
return tilesets;
}
}

49
app/src/editor/map/TileStore.ts Executable file
View File

@ -0,0 +1,49 @@
import * as Phaser from 'phaser';
import { Layer } from '../util/Layer';
import { Asset, AssetType } from '../util/Asset';
interface TileInfo {
res: number;
ind: number;
identifier: string;
}
/**
* Stores a map of tileset indexes to tiles.
*/
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: 0, floor: 0, detail: 0 };
/**
* 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);
}
/**
* Adds the specified tileset to the map.
*/
private addTileset(textures: Phaser.Textures.TextureManager, layer: AssetType, identifier: string): void {
const ind = this.currentInd[layer as Layer]++;
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;
}
}

View File

@ -1,7 +1,9 @@
import * as Phaser from 'phaser'; import * as Phaser from 'phaser';
import EditorData from '../EditorData'; import TilesetPatcher from '../TilesetPatcher';
import { Asset } from '../util/Asset'; import { Asset } from '../util/Asset';
import EditorData from '../EditorData';
export default class LoadScene extends Phaser.Scene { export default class LoadScene extends Phaser.Scene {
loaderOutline: Phaser.GameObjects.Sprite | null = null; loaderOutline: Phaser.GameObjects.Sprite | null = null;
@ -24,8 +26,6 @@ export default class LoadScene extends Phaser.Scene {
this.load.image('cursor', '/app/static/cursor.png'); this.load.image('cursor', '/app/static/cursor.png');
this.load.image('grid_tile', '/app/static/grid_tile.png'); this.load.image('grid_tile', '/app/static/grid_tile.png');
this.load.image('tileset_partial', '/app/static/tileset/water_new.png');
this.load.image('tileset_template', '/app/static/tileset_template.png');
this.load.image('ui_button_grid', '/app/static/ui/button_grid.png'); this.load.image('ui_button_grid', '/app/static/ui/button_grid.png');
this.load.spritesheet('ui_button_side_menu', '/app/static/ui/button_side_menu.png', {frameWidth: 21, frameHeight: 18}); this.load.spritesheet('ui_button_side_menu', '/app/static/ui/button_side_menu.png', {frameWidth: 21, frameHeight: 18});
this.load.spritesheet('ui_history_manipulation', '/app/static/ui/history_manipulation.png', {frameWidth: 39, frameHeight: 18}); this.load.spritesheet('ui_history_manipulation', '/app/static/ui/history_manipulation.png', {frameWidth: 39, frameHeight: 18});
@ -43,17 +43,22 @@ export default class LoadScene extends Phaser.Scene {
this.assets = JSON.parse(this.cache.text.get('assets')); this.assets = JSON.parse(this.cache.text.get('assets'));
for (let asset of this.assets) { for (let asset of this.assets) {
if (asset.tileSize) this.load.spritesheet(asset.identifier, '/app/asset/' + asset.path, if (asset.tileSize && asset.type !== 'wall' && asset.type !== 'detail')
{ frameWidth: asset.tileSize, frameHeight: asset.tileSize }); this.load.spritesheet(asset.identifier, '/app/asset/' + asset.path, { frameWidth: asset.tileSize, frameHeight: asset.tileSize });
else this.load.image(asset.identifier, asset.path); else this.load.image(asset.identifier, '/app/asset/' + asset.path);
} }
} }
create(): void { create(): void {
const t = new TilesetPatcher(this);
Promise.all(this.assets.filter(a => a.type === 'wall' || a.type === 'detail')
.map(async (a) => await t.patch(a.identifier, a.tileSize)))
.then(() => {
this.game.scene.start('MapScene', { ...this.editorData, data: JSON.parse(this.cache.text.get('data')), assets: this.assets }); this.game.scene.start('MapScene', { ...this.editorData, data: JSON.parse(this.cache.text.get('data')), assets: this.assets });
this.cache.text.remove('assets'); this.cache.text.remove('assets');
this.game.scene.stop('LoadScene'); this.game.scene.stop('LoadScene');
this.game.scene.swapPosition('MapScene', 'LoadScene'); this.game.scene.swapPosition('MapScene', 'LoadScene');
});
} }
private setup(): void { private setup(): void {

View File

@ -9,10 +9,9 @@ import WorldView from '../WorldView';
import TokenMode from '../TokenMode'; import TokenMode from '../TokenMode';
import ArchitectMode from '../ArchitectMode'; import ArchitectMode from '../ArchitectMode';
import Map from '../map/Map';
import Token from '../Token'; import Token from '../Token';
import MapData from '../MapData';
import Lighting from '../lighting/Lighting'; import Lighting from '../lighting/Lighting';
import TilesetPatcher from '../TilesetPatcher';
// import OutlinePipeline from '../shader/OutlinePipeline'; // import OutlinePipeline from '../shader/OutlinePipeline';
// import BrightenPipeline from '../shader/BrightenPipeline'; // import BrightenPipeline from '../shader/BrightenPipeline';
@ -34,7 +33,7 @@ export default class MapScene extends Phaser.Scene {
size: Vec2 = new Vec2(); size: Vec2 = new Vec2();
map: MapData = new MapData(this); map: Map = new Map();
lighting: Lighting = new Lighting(this); lighting: Lighting = new Lighting(this);
mode: number = 0; mode: number = 0;
@ -48,18 +47,11 @@ export default class MapScene extends Phaser.Scene {
// webRenderer.pipelines.add('outline', new OutlinePipeline(this.game)); // webRenderer.pipelines.add('outline', new OutlinePipeline(this.game));
// webRenderer.pipelines.add('brighten', new BrightenPipeline(this.game)); // webRenderer.pipelines.add('brighten', new BrightenPipeline(this.game));
const t = new TilesetPatcher(this);
t.patch('tileset_partial', 16);
const s = this.add.sprite(300, 300, 'tileset_partial');
s.setOrigin(0, 0);
s.setScale(4);
this.i.init(); this.i.init();
this.view.init(); this.view.init();
this.size = new Vec2(data.data.size); this.size = new Vec2(data.data.size);
this.map.init(this.size, this.assets!); this.map.init(this, this.size, this.assets!);
this.ui.init(this.assets!); this.ui.init(this.assets!);
this.architect.init(); this.architect.init();

View File

@ -1,6 +1,6 @@
import { Vec2 } from './Vec'; import { Vec2 } from './Vec';
export type AssetType = 'ground' | 'wall' | 'token'; export type AssetType = 'floor' | 'detail' | 'wall' | 'token';
export interface Asset { export interface Asset {
type: AssetType; type: AssetType;

View File

@ -1,7 +1 @@
enum Layer { export type Layer = 'floor' | 'wall' | 'detail';
floor = 0,
wall = 1,
overlay = 2
};
export default Layer;

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 843 B

View File

Before

Width:  |  Height:  |  Size: 843 B

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1004 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,9 +1,10 @@
import * as DB from './DBStructs'; import * as DB from './DBStructs';
export type AppDataSpecifier = 'user' | 'campaigns' | 'assets'; export type AppDataSpecifier = 'user' | 'campaigns' | 'collections' | 'assets';
export interface AppData { export interface AppData {
user: { user: string, name: string }; user: { user: string, name: string };
collections: DB.AssetCollection[];
campaigns: DB.Campaign[]; campaigns: DB.Campaign[];
assets: DB.Asset[]; assets: DB.Asset[];
} }

View File

@ -41,7 +41,7 @@ export interface Map {
tiles: string; tiles: string;
} }
export type AssetType = 'wall' | 'ground' | 'token'; export type AssetType = 'wall' | 'detail' | 'ground' | 'token';
export interface AssetCollection { export interface AssetCollection {
_id?: ObjectID; _id?: ObjectID;

View File

@ -14,6 +14,7 @@ const sizeOf = promisify(sizeOfRaw);
const logger = log4js.getLogger(); const logger = log4js.getLogger();
const PERSONAL_IDENTIFIER = '_'; const PERSONAL_IDENTIFIER = '_';
const ASSET_PATH = path.join(path.dirname(path.dirname(__dirname)), 'assets');
export const uploadLimit = 2 * 1024 * 1024; export const uploadLimit = 2 * 1024 * 1024;
export const accountLimit = 5 * 1024 * 1024; export const accountLimit = 5 * 1024 * 1024;
@ -25,7 +26,7 @@ interface BaseAssetData {
} }
interface TilesetData { interface TilesetData {
type: 'ground' | 'wall' type: 'floor' | 'wall' | 'detail'
} }
interface TokenData { interface TokenData {
@ -310,11 +311,43 @@ export default class Database {
/** /**
* Get a users's uploaded assets. * Gets a users's uploaded assets.
*/ */
async getUserAssets(user: string): Promise<DB.Asset[]> { async getUserAssets(user: string): Promise<DB.Asset[]> {
return await this.db!.collection('assets').find({ user: user }).toArray(); return await this.db!.collection('assets').find({ user }).toArray();
}
/**
* Gets a user's collections.
*/
async getUserCollections(user: string): Promise<DB.AssetCollection[]> {
return await this.db!.collection('collections').find({ user }).toArray();
}
/**
* Adds an asset to a user's collection
*
* @param user - The user that owns the collection.
* @param collection - The collection to insert an item into.
* @param asset - An Asset string to add to the collection.
*/
async addCollectionAsset(user: string, collection: string, asset: string) {
const res = await this.db!.collection('assets').find({
user: asset.slice(0, asset.indexOf(':')),
identifier: asset.slice(asset.indexOf(':') + 1)
});
if (!res) throw 'Asset doesn\'t exist.';
const matched = await this.db!.collection('collections')
.updateOne({ user: user, identifier: collection }, { $addToSet: { items: asset }});
if (!matched.matchedCount) throw 'Collection doesn\'t exist.';
} }
@ -342,7 +375,7 @@ export default class Database {
let assetName = '', assetPath = ''; let assetName = '', assetPath = '';
while (true) { while (true) {
assetName = crypto.createHash('md5').update(data.identifier + await crypto.randomBytes(8)).digest('hex') + '.png'; assetName = crypto.createHash('md5').update(data.identifier + await crypto.randomBytes(8)).digest('hex') + '.png';
assetPath = path.join(path.dirname(path.dirname(__dirname)), 'assets', assetName); assetPath = path.join(ASSET_PATH, assetName);
try { await fs.access(assetPath, fsc.R_OK | fsc.W_OK); } try { await fs.access(assetPath, fsc.R_OK | fsc.W_OK); }
catch (e) { if (e.code === 'ENOENT') break; } catch (e) { if (e.code === 'ENOENT') break; }
} }
@ -378,6 +411,26 @@ export default class Database {
} }
/**
* Deletes a user's asset, removing it from the filesystem.
*
* @param {string} user - The user identifier.
* @param {string} identifier - The asset identifier.
*/
async deleteAsset(user: string, identifier: string): Promise<void> {
const asset: DB.Asset | null = await this.db!.collection('assets').findOne({ user, identifier });
if (!asset) return;
try { await fs.unlink(path.join(ASSET_PATH, asset.path)) } catch {}
await this.db!.collection('assets').remove({ user, identifier });
const query = user + ':' + identifier;
await this.db!.collection('collections').updateMany(
{ items: { $all: [ query ] } }, { $pull: { items: query } });
}
/** /**
* Creates and returns an authentication token for a user using a username / password pair. * Creates and returns an authentication token for a user using a username / password pair.
* Throws if the username and password do not refer to a valid user. * Throws if the username and password do not refer to a valid user.

View File

@ -35,12 +35,17 @@ export default class DataRouter extends Router {
case 'assets': case 'assets':
data.assets = await this.db.getUserAssets(user); data.assets = await this.db.getUserAssets(user);
break; break;
case 'collections':
data.collections = await this.db.getUserCollections(user);
break;
} }
})); }));
return data; return data;
}; };
/** /**
* Attempts to authenticate a user from a username and password. * Attempts to authenticate a user from a username and password.
*/ */
@ -97,7 +102,7 @@ export default class DataRouter extends Router {
/** /**
* Creates a new campaign. * Creates a new map within a campaign.
*/ */
this.router.post('/map/new', this.authRoute(async (user, req, res) => { this.router.post('/map/new', this.authRoute(async (user, req, res) => {
@ -118,16 +123,16 @@ export default class DataRouter extends Router {
})); }));
this.router.post('/asset/upload/', this.authRoute(async (user, req, res) => { this.router.post('/asset/upload/', this.authRoute(async (user, req, res) => {
const type: 'ground' | 'token' | 'wall' = req.body.type; const type: 'floor' | 'token' | 'detail' | 'wall' = req.body.type;
const tokenType: '1' | '4' | '8' = req.body.tokenType; const tokenType: '1' | '4' | '8' = req.body.tokenType;
const name = req.body.name; const name = req.body.name;
const identifier = req.body.identifier; const identifier = req.body.identifier;
if (typeof name != 'string' || typeof identifier != 'string' || if (typeof name !== 'string' || typeof identifier !== 'string' ||
(type != 'token' && type != 'ground' && type != 'wall') || (type !== 'token' && type !== 'floor' && type !== 'wall' && type !== 'detail') ||
(type === 'token' && tokenType !== '1' && tokenType !== '4' && tokenType !== '8')) (type === 'token' && tokenType !== '1' && tokenType !== '4' && tokenType !== '8'))
return res.status(400) return res.sendStatus(400);
const file = req.files?.file; const file = req.files?.file;
if (!file || Array.isArray(file)) return res.sendStatus(400); if (!file || Array.isArray(file)) return res.sendStatus(400);
@ -144,6 +149,33 @@ export default class DataRouter extends Router {
})); }));
/**
* Deletes an asset from the database & filesystem.
*/
this.router.post('/asset/delete/', this.authRoute(async (user, req, res) => {
const identifier = req.body.identifier;
if (typeof identifier !== 'string') res.sendStatus(400);
else {
await this.db.deleteAsset(user, identifier);
res.sendStatus(200);
}
}));
/**
* Adds an asset to a collection.
*/
this.router.post('/collection/add', this.authRoute(async (user, req, res) => {
if (typeof req.body.collection !== 'string' || typeof req.body.asset !== 'string') throw 'Invalid parameters.';
await this.db.addCollectionAsset(user, req.body.collection, req.body.asset);
res.send(await getAppData(user, 'collections'));
}));
this.app.use('/data', this.router); this.app.use('/data', this.router);
} }
} }