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'>
<AppSidebar />
<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='/campaigns' component={Routes.Campaigns} />
<Route path='/campaign/:id?' component={Routes.Campaign} />
<Route path='/campaigns' component={Routes.Campaigns} />
<Redirect to='/campaigns' />
<Redirect to='/' />
</Switch>
</div>
</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 { 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 MyAssetsList from '../view/MyAssetsList';
import AssetCollectionList from '../view/AssetCollectionList';
export default function AssetsRoute() {
const history = useHistory();
const [ { assets, collections, user } ] = useAppData([ 'assets', 'collections', 'user' ]);
return (
<div class='AssetsRoute Page'>
<aside class='Page-Sidebar'>
<h2 class='Page-SidebarTitle'>Assets</h2>
{/* <Link className='Page-SidebarCategory' activeClassName='Active' exact to='/assets/'>Featured Assets</Link>
<Link className='Page-SidebarCategory' activeClassName='Active' exact to='/assets/subscribed'>Subscribed Assets</Link>*/}
<Link className='Page-SidebarCategory' activeClassName='Active' to='/assets/uploaded'>Uploaded Assets</Link>
<Link className='Page-SidebarCategory' activeClassName='Active' exact to='/assets/'>My Assets</Link>
<Link className='Page-SidebarCategory' activeClassName='Active' to='/assets/collections/'>My Collections</Link>
</aside>
<main class='AssetsRoute-Main'>
<Switch>
{/* <Route exact path='/assets/'>
<h1>Storefront</h1>
<Route exact path='/assets/'>
<AssetList assets={(assets || []).filter(a => a.user === user!.user)}
onClick={(user, identifier) => history.push(`/asset/${user}/${identifier}`)}
onNew={() => history.push('/assets/new')} />
</Route>
<Route exact path='/assets/subscribed'>
<h1>Subscribed</h1>
</Route>*/}
<Route exact path='/assets/uploaded'><MyAssetsList/></Route>
<Route exact path='/assets/collections'>
<AssetCollectionList collections={collections || []}
onClick={(user, identifier) => history.push(`/assets/collection/${user}/${identifier}`)} />
</Route>
<Route exact path='/assets/new'><NewAssetForm/></Route>
<Redirect to='/assets/uploaded' />
{/* <Redirect to='/assets/uploaded' />*/}
</Switch>
</main>
</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 Campaigns } from './CampaignsRoute';
export { default as Asset } from './AssetRoute';
export { default as Assets } from './AssetsRoute';
export { default as Collection } from './CollectionRoute';
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/def' as *
.AssetsList
.AssetList
max-width: 1000px
display: block
margin: 0 auto
margin-top: 56px
.AssetsList-Grid
.AssetList-Grid
@include grid.auto_width(160px, 16px)
.AssetsList-AssetWrap
.AssetsList-Asset
.AssetList-AssetWrap
.AssetList-Asset
width: 100%
height: 0
border: none
display: grid
user-select: none
position: relative
@ -25,7 +27,7 @@
background-color: $neutral-200
box-shadow: 0px 2px 12px 0px transparentize($neutral-000, 0.9)
.AssetsList-AssetInner
.AssetList-AssetInner
position: absolute
top: 0
left: 0
@ -34,7 +36,7 @@
display: grid
grid-template-rows: 1fr auto
.AssetsList-AssetPreview
.AssetList-AssetPreview
position: relative
padding: 8px
overflow: hidden
@ -49,14 +51,15 @@
pointer-events: none
image-rendering: pixelated
.AssetsList-AssetTitle
.AssetList-AssetTitle
@include text.line_clamp
margin: 0
font-size: 18px
padding: 12px
.AssetsList-NewAsset
.AssetList-NewAsset
width: 100%
display: flex
user-select: none
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-template-columns: 1fr 1fr
.NewAssetForm-Col2-60
@extend .NewAssetForm-Col2
grid-template-columns: 5fr 4fr
& > div:nth-child(2)
text-align: right
.NewAssetForm-UploadWrap
@extend %material_button

View File

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

View File

@ -1,7 +1,7 @@
import type MapScene from './scene/MapScene';
import Layer from './util/Layer';
import { Vec2 } from './util/Vec';
import { Layer } from './util/Layer';
export default class ArchitectMode {
scene: MapScene;
@ -16,7 +16,7 @@ export default class ArchitectMode {
pointerPrimaryDown: boolean = false;
activeTileset: number = 0;
activeLayer: Layer = Layer.wall;
activeLayer: Layer = 'wall';
manipulated: {pos: Vec2; layer: Layer; lastTile: number; tile: number}[] = [];
@ -25,19 +25,18 @@ export default class ArchitectMode {
}
init() {
// Create cursor hover sprite
this.cursor = this.scene.add.sprite(0, 0, 'cursor');
this.cursor.setScale(4, 4);
this.cursor.setDepth(1000);
this.cursor.setOrigin(0, 0);
this.cursor.setScale(1 / 16);
}
update() {
this.active = 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));
this.cursor!.setPosition(selectedTilePos.x * 64, selectedTilePos.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, selectedTilePos.y);
this.cursor!.setVisible((selectedTilePos.x >= 0 && selectedTilePos.y >= 0 &&
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;
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 = [];
@ -100,21 +99,19 @@ export default class ArchitectMode {
this.primitives.forEach((v) => {
v.setOrigin(0, 0);
v.setScale(64, 64);
v.setLineWidth(0.03);
v.setDepth(300);
v.setLineWidth(0.03);
});
this.primitives.push(this.scene.add.sprite(this.startTilePos.x * 64, this.startTilePos.y * 64,
'cursor') as any as Phaser.GameObjects.Line);
this.primitives.push(this.scene.add.sprite(this.startTilePos.x, this.startTilePos.y, 'cursor') as any);
this.primitives[1].setOrigin(0, 0);
this.primitives[1].setScale(4, 4);
this.primitives[1].setScale(1 / 16);
this.primitives[1].setAlpha(0.5);
}
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 b = new Vec2(selectedTilePos.x * 64, selectedTilePos.y * 64);
let a = new Vec2(this.startTilePos.x, this.startTilePos.y);
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;
else b.x = a.x;
@ -125,12 +122,12 @@ export default class ArchitectMode {
change.y /= normalizeFactor;
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.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 = [];
}
@ -154,7 +151,6 @@ export default class ArchitectMode {
this.primitives.forEach((v) => {
v.setOrigin(0, 0);
v.setScale(64, 64);
v.setLineWidth(0.03);
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,
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.y /= normalizeFactor;
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) {
this.placeTileAndPushManip(new Vec2(Math.floor(place.x / 64), Math.floor(place.y / 64)), this.scene.i.mouseLeftDown());
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), Math.floor(place.y)), this.scene.i.mouseLeftDown());
place.x += change.x;
place.y += change.y;
}
@ -198,12 +194,12 @@ export default class ArchitectMode {
placeTileAndPushManip(manipPos: Vec2, solid: boolean) {
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;
this.scene.map.setTile(layer, tile, manipPos.x, manipPos.y);
this.scene.map.activeLayer.setTile(layer, tile, manipPos.x, manipPos.y);
this.manipulated.push({
pos: manipPos,
@ -211,7 +207,6 @@ export default class ArchitectMode {
lastTile: lastTile,
tile: tile
});
}
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 {
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 canvas = new Phaser.GameObjects.RenderTexture(this.scene, 0, 0, 10 * tile_size, 5 * tile_size);
canvas.draw(tileset_key);
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);
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(6, 1, 6.5, 1.5), new Vec2(9, 4));
canvas.snapshot((img: any) => {
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.`);
resolve();
});
});
}
}

View File

@ -1,4 +1,3 @@
import { Vec2 } from './util/Vec';
import { generateId } from './util/Helpers';
export interface SerializedToken {
@ -10,8 +9,8 @@ export interface SerializedToken {
}
export default class Token extends Phaser.GameObjects.Container {
sprite: Phaser.GameObjects.Sprite | null = null;
shadow: Phaser.GameObjects.Sprite | null = null;
sprite: Phaser.GameObjects.Sprite;
shadow: Phaser.GameObjects.Sprite;
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) {
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.uuid = generateId(32);
@ -37,28 +50,16 @@ export default class Token extends Phaser.GameObjects.Container {
}
setTexture(tex: string) {
if (this.shadow != null) this.shadow.setTexture(tex);
else {
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.shadow.setTexture(tex);
this.sprite.setTexture(tex);
this.width = this.shadow.width * 4;
this.height = this.shadow.height * 4;
this.shadow.y = this.height - 26;
this.shadow.setScale(1 / this.shadow.width, 0.25 / this.shadow.height);
this.sprite.setScale(1 / this.sprite.width, 1 / this.sprite.height);
if (this.sprite != null) this.sprite.setTexture(tex);
else {
this.sprite = new Phaser.GameObjects.Sprite(this.scene, -4, -4, tex);
this.sprite.setOrigin(0, 0);
this.sprite.setScale(4, 4);
this.setPosition(this.x / 4, this.y / 4);
this.list.push(this.sprite);
}
this.shadow.y = this.sprite.displayHeight - this.shadow.displayHeight - 0.025;
this.width = this.sprite.displayWidth;
this.height = this.sprite.displayHeight;
}
setFrame(frame: number): void {
@ -83,11 +84,14 @@ export default class Token extends Phaser.GameObjects.Container {
this.hovered = hovered;
if (!hovered && !this.selected) {
this.sprite.resetPipeline();
// this.sprite.resetPipeline();
this.sprite.setTint(0xffffff);
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) {
@ -96,33 +100,27 @@ export default class Token extends Phaser.GameObjects.Container {
this.selected = selected;
if (!selected) {
if (!this.hovered) this.sprite.resetPipeline();
else this.sprite.setPipeline('brighten');
// if (!this.hovered) this.sprite.resetPipeline();
// else this.sprite.setPipeline('brighten');
if (!this.hovered) this.sprite.setTint(0xffffff);
else this.sprite.setTint(0x999999);
}
else {
this.sprite.setPipeline('outline');
// this.sprite.setPipeline('outline');
this.sprite.setTint(0x000000);
// @ts-ignore
// 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
serialize(): string {
return JSON.stringify(({
uuid: this.uuid,
sprite: this.sprite ? this.sprite.texture.key : '',
frame: this.currentFrame,
x: this.x / 4,
y: this.y / 4
x: this.x,
y: this.y
} as SerializedToken));
}

View File

@ -31,7 +31,7 @@ export default class TokenMode {
init() {
// Create cursor hover sprite
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.setOrigin(0, 0);
this.cursor.setVisible(false);
@ -58,15 +58,15 @@ export default class TokenMode {
update() {
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.selecting();
if (this.selectedTokens.length > 0 && !this.movingTokens) this.tokenMoveControls();
this.tokenPreview!.setPosition(selectedTilePos.x * 16, selectedTilePos.y * 16);
this.cursor!.setPosition(selectedTilePos.x * 64, selectedTilePos.y * 64);
this.tokenPreview!.setPosition(selectedTilePos.x, selectedTilePos.y);
this.cursor!.setPosition(selectedTilePos.x, selectedTilePos.y);
if (this.selectedTokenType === '') this.tokenPreview!.setVisible(false);
if (this.selectedTokenType !== '') this.cursor!.setVisible(false);
@ -80,7 +80,7 @@ export default class TokenMode {
updateRectangleSelect() {
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 = [];
@ -97,7 +97,6 @@ export default class TokenMode {
this.primitives.forEach((v) => {
v.setOrigin(0, 0);
v.setScale(64, 64);
v.setLineWidth(0.03);
v.setDepth(300);
});
@ -109,14 +108,14 @@ export default class TokenMode {
this.movingTokens = true;
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.selectedTokens.forEach(t => this.prevSerialized.push(t.serialize()));
}
createToken(): Token {
let token = new Token(this.scene, Math.floor(this.scene.view.cursorWorld.x / 4 / 16) * 16,
Math.floor(this.scene.view.cursorWorld.y / 4 / 16) * 16, this.selectedTokenType);
let token = new Token(this.scene, Math.floor(this.scene.view.cursorWorld.x),
Math.floor(this.scene.view.cursorWorld.y), this.selectedTokenType);
this.scene.add.existing(token);
this.scene.tokens.push(token);
@ -150,16 +149,16 @@ export default class TokenMode {
private tokenMoveControls(): void {
if (this.scene.i.keyPressed('UP')) {
this.moveToken(0, -16, 2);
this.moveToken(0, -1, 2);
}
if (this.scene.i.keyPressed('LEFT')) {
this.moveToken(-16, 0, 1);
this.moveToken(-1, 0, 1);
}
if (this.scene.i.keyPressed('DOWN')) {
this.moveToken(0, 16, 0);
this.moveToken(0, 1, 0);
}
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[] = [];
this.selectedTokens.forEach((token) => {
prevSerialized.push(token.serialize());
token.x += x * 4;
token.y += y * 4;
token.x += x;
token.y += y;
token.setFrame(frame);
});
@ -201,7 +200,7 @@ export default class TokenMode {
for (let i = this.scene.tokens.length - 1; i >= 0; 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;
break;
}
@ -237,7 +236,7 @@ export default class TokenMode {
}
// Start a rectangle selection
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
@ -266,7 +265,7 @@ export default class TokenMode {
this.primitives.forEach((v) => v.destroy());
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 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) {
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) {
let selected = this.scene.i.keyDown('CTRL') ? !this.selectedIncludes(token) : true;
@ -360,13 +359,13 @@ export default class TokenMode {
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);
if (offset.x === 0 && offset.y === 0) return;
this.movedTokens = true;
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();
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];
zoomLevel = 11;
zoomLevels: number[] = [ 5, 6, 8, 10, 15, 20, 25, 33, 40, 50, 60, 75, 100, 125, 150, 200, 300 ];
zoomLevel = 9;
constructor(scene: MapScene) {
this.scene = scene;
@ -22,11 +22,26 @@ export default class WorldView {
init(): void {
this.camera = this.scene.cameras.main;
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) => {
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.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() {
if (this.scene.input.activePointer.middleButtonDown()) {
this.camera!.scrollX += Math.round((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!.scrollX += (this.lastCursorScreen.x - this.cursorScreen.x) / 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 type MapScene from '../scene/MapScene';
import Layer from '../util/Layer';
import { Vec2 } from '../util/Vec';
import { Layer } from '../util/Layer';
export default class HistoryElement {
scene: MapScene;
@ -19,7 +19,7 @@ export default class HistoryElement {
console.log('Undo', this.type);
if (this.type === 'tile') {
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);
}
}
@ -69,7 +69,7 @@ export default class HistoryElement {
console.log('Redo', this.type);
if (this.type === 'tile') {
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);
}
}

View File

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

View File

@ -1,6 +1,5 @@
import type Lighting from './Lighting';
import Layer from '../util/Layer';
import { Vec2 } from '../util/Vec';
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 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) {
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 LightChunk from './LightChunk';
@ -23,9 +23,9 @@ export default class Lighting {
init(size: Vec2) {
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] = [];
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);
}
}

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 EditorData from '../EditorData';
import TilesetPatcher from '../TilesetPatcher';
import { Asset } from '../util/Asset';
import EditorData from '../EditorData';
export default class LoadScene extends Phaser.Scene {
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('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.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});
@ -43,17 +43,22 @@ export default class LoadScene extends Phaser.Scene {
this.assets = JSON.parse(this.cache.text.get('assets'));
for (let asset of this.assets) {
if (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);
if (asset.tileSize && asset.type !== 'wall' && asset.type !== 'detail')
this.load.spritesheet(asset.identifier, '/app/asset/' + asset.path, { frameWidth: asset.tileSize, frameHeight: asset.tileSize });
else this.load.image(asset.identifier, '/app/asset/' + asset.path);
}
}
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.cache.text.remove('assets');
this.game.scene.stop('LoadScene');
this.game.scene.swapPosition('MapScene', 'LoadScene');
});
}
private setup(): void {

View File

@ -9,10 +9,9 @@ import WorldView from '../WorldView';
import TokenMode from '../TokenMode';
import ArchitectMode from '../ArchitectMode';
import Map from '../map/Map';
import Token from '../Token';
import MapData from '../MapData';
import Lighting from '../lighting/Lighting';
import TilesetPatcher from '../TilesetPatcher';
// import OutlinePipeline from '../shader/OutlinePipeline';
// import BrightenPipeline from '../shader/BrightenPipeline';
@ -34,7 +33,7 @@ export default class MapScene extends Phaser.Scene {
size: Vec2 = new Vec2();
map: MapData = new MapData(this);
map: Map = new Map();
lighting: Lighting = new Lighting(this);
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('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.view.init();
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.architect.init();

View File

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

View File

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

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';
export type AppDataSpecifier = 'user' | 'campaigns' | 'assets';
export type AppDataSpecifier = 'user' | 'campaigns' | 'collections' | 'assets';
export interface AppData {
user: { user: string, name: string };
collections: DB.AssetCollection[];
campaigns: DB.Campaign[];
assets: DB.Asset[];
}

View File

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

View File

@ -14,6 +14,7 @@ const sizeOf = promisify(sizeOfRaw);
const logger = log4js.getLogger();
const PERSONAL_IDENTIFIER = '_';
const ASSET_PATH = path.join(path.dirname(path.dirname(__dirname)), 'assets');
export const uploadLimit = 2 * 1024 * 1024;
export const accountLimit = 5 * 1024 * 1024;
@ -25,7 +26,7 @@ interface BaseAssetData {
}
interface TilesetData {
type: 'ground' | 'wall'
type: 'floor' | 'wall' | 'detail'
}
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[]> {
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 = '';
while (true) {
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); }
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.
* 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':
data.assets = await this.db.getUserAssets(user);
break;
case 'collections':
data.collections = await this.db.getUserCollections(user);
break;
}
}));
return data;
};
/**
* 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) => {
@ -118,16 +123,16 @@ export default class DataRouter extends Router {
}));
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 name = req.body.name;
const identifier = req.body.identifier;
if (typeof name != 'string' || typeof identifier != 'string' ||
(type != 'token' && type != 'ground' && type != 'wall') ||
if (typeof name !== 'string' || typeof identifier !== 'string' ||
(type !== 'token' && type !== 'floor' && type !== 'wall' && type !== 'detail') ||
(type === 'token' && tokenType !== '1' && tokenType !== '4' && tokenType !== '8'))
return res.status(400)
return res.sendStatus(400);
const file = req.files?.file;
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);
}
}