Separate render props and meta props of tokens, fix IO not closing on soft redirect.
parent
504250dee3
commit
c87e9214a1
|
@ -7,6 +7,7 @@ import './Editor.sass';
|
|||
interface Props {
|
||||
user: string;
|
||||
identifier: string;
|
||||
mapIdentifier?: string;
|
||||
}
|
||||
|
||||
function pad(n: number) {
|
||||
|
@ -14,7 +15,7 @@ function pad(n: number) {
|
|||
return '' + n;
|
||||
}
|
||||
|
||||
export default function Editor({ user, identifier }: Props) {
|
||||
export default function Editor({ user, identifier, mapIdentifier }: Props) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<Phaser.Game | null>(null);
|
||||
const [ loadPercent, setLoadPercent ] = useState<number | undefined>(0);
|
||||
|
@ -30,10 +31,11 @@ export default function Editor({ user, identifier }: Props) {
|
|||
setLoadPercent(0.25);
|
||||
if (ignore || !rootRef.current) return;
|
||||
|
||||
editorRef.current = create(rootRef.current, setLoadPercent, user, identifier);
|
||||
editorRef.current = create(rootRef.current, setLoadPercent, user, identifier, mapIdentifier);
|
||||
|
||||
const resizeCallback = () => {
|
||||
const { width, height } = rootRef.current.getBoundingClientRect();
|
||||
console.log(rootRef.current);
|
||||
editorRef.current!.scale.resize(width, height);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import qs from 'query-string';
|
||||
import * as Preact from 'preact';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
|
||||
import Editor from '../Editor';
|
||||
|
||||
|
@ -7,10 +8,10 @@ import './Editor.sass';
|
|||
|
||||
export default function EditorPage() {
|
||||
const { user, campaign } = useParams<{ user: string; campaign: string }>();
|
||||
|
||||
const map = qs.parse(useLocation().search).map as string | undefined;
|
||||
return (
|
||||
<div class='EditorPage'>
|
||||
<Editor user={user} identifier={campaign} />
|
||||
<Editor user={user} identifier={campaign} mapIdentifier={map} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import Button from '../Button';
|
|||
export default function NewMapForm() {
|
||||
const history = useHistory();
|
||||
const [ ,, mergeData ] = useAppData();
|
||||
const { campaign } = useParams<{ campaign: string }>();
|
||||
const { user, campaign } = useParams<{ user: string; campaign: string }>();
|
||||
|
||||
const [ queryState, setQueryState ] = useState<'idle' | 'querying'>('idle');
|
||||
|
||||
|
@ -35,7 +35,7 @@ export default function NewMapForm() {
|
|||
else {
|
||||
const data = await res.json();
|
||||
await mergeData(data);
|
||||
history.push(`/campaign/${campaign}/maps`);
|
||||
history.push(`/u/${user}/c/${campaign}/maps`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,15 +2,12 @@ import { Asset } from './util/Asset';
|
|||
import { Socket } from 'socket.io-client';
|
||||
import * as DB from '../../../common/DBStructs';
|
||||
|
||||
export interface ExternalData {
|
||||
identifier: string;
|
||||
export default interface EditorData {
|
||||
socket: Socket;
|
||||
}
|
||||
|
||||
export default interface EditorData extends ExternalData {
|
||||
campaign: DB.Campaign;
|
||||
assets: Asset[];
|
||||
map: DB.Map;
|
||||
map?: string;
|
||||
|
||||
display: 'edit' | 'view';
|
||||
onProgress: (progress: number | undefined) => void;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import Phaser from 'phaser';
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
import * as Scene from './scene/Scenes';
|
||||
|
||||
export default function create(root: HTMLElement, onProgress: (progress: number) => void, user: string, identifier: string) {
|
||||
const bounds = root.getBoundingClientRect();
|
||||
export default function create(root: HTMLElement, onProgress: (progress: number) => void,
|
||||
user: string, identifier: string, mapIdentifier?: string) {
|
||||
|
||||
const socket = io();
|
||||
const bounds = root.getBoundingClientRect();
|
||||
|
||||
const game = new Phaser.Game({
|
||||
disableContextMenu: true,
|
||||
|
@ -20,6 +19,6 @@ export default function create(root: HTMLElement, onProgress: (progress: number)
|
|||
scene: Scene.list
|
||||
});
|
||||
|
||||
game.scene.start('InitScene', { user, onProgress, identifier, socket });
|
||||
game.scene.start('InitScene', { user, onProgress, identifier, mapIdentifier });
|
||||
return game;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Vec2 } from '../util/Vec';
|
||||
import { Layer } from '../util/Layer';
|
||||
import { TokenData } from '../map/token/Token';
|
||||
import { TokenRenderData, TokenData } from '../map/token/Token';
|
||||
|
||||
export interface Tile {
|
||||
type: 'tile';
|
||||
|
@ -21,7 +21,7 @@ export interface PlaceToken {
|
|||
|
||||
export interface ModifyToken {
|
||||
type: 'modify_token';
|
||||
tokens: { pre: TokenData[]; post: TokenData[] };
|
||||
tokens: { pre: TokenRenderData[]; post: TokenRenderData[] };
|
||||
}
|
||||
|
||||
export interface DeleteToken {
|
||||
|
|
|
@ -2,23 +2,24 @@ import * as Phaser from 'phaser';
|
|||
import IO from 'socket.io-client';
|
||||
|
||||
import Map from '../map/Map';
|
||||
// import Token from '../map/token/Token';
|
||||
import type { Action } from './Action';
|
||||
import ActionEvent from './ActionEvent';
|
||||
import EventHandler from '../EventHandler';
|
||||
import InputManager from '../InputManager';
|
||||
|
||||
const SAVE_INTERVAL = 5 * 1000;
|
||||
|
||||
export default class ActionManager {
|
||||
readonly event = new EventHandler<ActionEvent>();
|
||||
|
||||
private map: Map = null as any;
|
||||
private socket: IO.Socket = null as any;
|
||||
// private scene: Phaser.Scene = null as any;
|
||||
|
||||
private history: Action[] = [];
|
||||
private head: number = -1;
|
||||
|
||||
private evtHandler = new EventHandler<ActionEvent>();
|
||||
private history: Action[] = [];
|
||||
|
||||
private historyHeldTime: number = 0;
|
||||
private editTime: number | false = false;
|
||||
|
||||
init(_scene: Phaser.Scene, map: Map, socket: IO.Socket) {
|
||||
this.map = map;
|
||||
|
@ -47,14 +48,20 @@ export default class ActionManager {
|
|||
this.historyHeldTime++;
|
||||
}
|
||||
else this.historyHeldTime = 0;
|
||||
|
||||
if (this.editTime && Date.now() - SAVE_INTERVAL > this.editTime) {
|
||||
this.socket.emit('serialize', this.map.identifier, this.map.save());
|
||||
this.editTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
push(item: Action): void {
|
||||
this.history.splice(this.head + 1, this.history.length - this.head, item);
|
||||
this.head = this.history.length - 1;
|
||||
if (!this.editTime) this.editTime = Date.now();
|
||||
|
||||
this.socket.emit('action', item);
|
||||
this.evtHandler.dispatch({ event: 'push', head: this.head, length: this.history.length });
|
||||
this.event.dispatch({ event: 'push', head: this.head, length: this.history.length });
|
||||
}
|
||||
|
||||
apply(item: Action): void {
|
||||
|
@ -81,19 +88,17 @@ export default class ActionManager {
|
|||
break;
|
||||
|
||||
case 'delete_token':
|
||||
item.tokens.forEach(t => this.map.tokens.createToken(t));
|
||||
item.tokens.forEach(t => this.map.tokens.createToken(t.render.pos as any,
|
||||
{ uuid: t.uuid, ...t.meta }, t.render.appearance.sprite, t.render.appearance.index));
|
||||
break;
|
||||
|
||||
case 'modify_token':
|
||||
for (let i = 0; i < item.tokens.pre.length; i++) {
|
||||
const token = this.map.tokens.getToken(item.tokens.pre[i].uuid);
|
||||
if (token) token.setToken(item.tokens.pre[i]);
|
||||
else this.map.tokens.createToken(item.tokens.pre[i]);
|
||||
}
|
||||
for (let i = 0; i < item.tokens.pre.length; i++)
|
||||
this.map.tokens.setRender(item.tokens.pre[i].uuid, item.tokens.pre[i]);
|
||||
break;
|
||||
}
|
||||
|
||||
this.evtHandler.dispatch({ event: 'prev', head: this.head, length: this.history.length });
|
||||
this.event.dispatch({ event: 'prev', head: this.head, length: this.history.length });
|
||||
}
|
||||
|
||||
next() {
|
||||
|
@ -111,7 +116,8 @@ export default class ActionManager {
|
|||
break;
|
||||
|
||||
case 'place_token':
|
||||
item.tokens.forEach(t => this.map.tokens.createToken(t));
|
||||
item.tokens.forEach(t => this.map.tokens.createToken(t.render.pos as any,
|
||||
{ uuid: t.uuid, ...t.meta }, t.render.appearance.sprite, t.render.appearance.index));
|
||||
break;
|
||||
|
||||
case 'delete_token':
|
||||
|
@ -119,15 +125,12 @@ export default class ActionManager {
|
|||
break;
|
||||
|
||||
case 'modify_token':
|
||||
for (let i = 0; i < item.tokens.post.length; i++) {
|
||||
const token = this.map.tokens.getToken(item.tokens.post[i].uuid);
|
||||
if (token) token.setToken(item.tokens.post[i]);
|
||||
else this.map.tokens.createToken(item.tokens.post[i]);
|
||||
}
|
||||
for (let i = 0; i < item.tokens.post.length; i++)
|
||||
this.map.tokens.setRender(item.tokens.post[i].uuid, item.tokens.post[i]);
|
||||
break;
|
||||
}
|
||||
|
||||
this.evtHandler.dispatch({ event: 'next', head: this.head, length: this.history.length });
|
||||
this.event.dispatch({ event: 'next', head: this.head, length: this.history.length });
|
||||
}
|
||||
|
||||
hasPrev(): boolean {
|
||||
|
@ -137,12 +140,4 @@ export default class ActionManager {
|
|||
hasNext(): boolean {
|
||||
return this.head < this.history.length - 1;
|
||||
}
|
||||
|
||||
bind(cb: (evt: ActionEvent) => boolean | void) {
|
||||
this.evtHandler.bind(cb);
|
||||
}
|
||||
|
||||
unbind(cb: (evt: ActionEvent) => boolean | void) {
|
||||
this.evtHandler.unbind(cb);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,12 +38,11 @@ export default class InterfaceRoot {
|
|||
private tileSidebar: TileSidebar | null = null;
|
||||
private tokenSidebar: TokenSidebar | null = null;
|
||||
|
||||
init(scene: Phaser.Scene, display: 'edit' | 'view', input: InputManager,
|
||||
mode: ModeMananger, actions: ActionManager, map: Map, assets: Asset[]) {
|
||||
init(scene: Phaser.Scene, input: InputManager, mode: ModeMananger,
|
||||
actions: ActionManager, map: Map, assets: Asset[]) {
|
||||
|
||||
this.mode = mode;
|
||||
this.scene = scene;
|
||||
// this.actions = action;
|
||||
this.inputManager = input;
|
||||
|
||||
this.camera = this.scene.cameras.add(0, 0, undefined, undefined, undefined, 'ui_camera');
|
||||
|
@ -55,15 +54,9 @@ export default class InterfaceRoot {
|
|||
this.root.setName('root');
|
||||
this.leftRoot = this.scene.add.container(0, 0);
|
||||
this.root.add(this.leftRoot);
|
||||
// this.rightRoot = this.scene.add.container(this.camera.displayWidth, this.camera.displayHeight);
|
||||
// this.root.add(this.rightRoot);
|
||||
|
||||
if (display === 'edit') {
|
||||
this.leftRoot.add(new SidebarToggler(scene, 49, 1000, input, this));
|
||||
|
||||
// this.leftRoot.add(new ModeSwitcher(scene, 82, 1, input, mode));
|
||||
// this.leftRoot.add(new HistoryManipulator(scene, 124, 1, history, input));
|
||||
|
||||
this.tokenSidebar = new TokenSidebar(scene, 0, 0, assets, input, mode);
|
||||
this.leftRoot.add(this.tokenSidebar);
|
||||
|
||||
|
@ -72,7 +65,6 @@ export default class InterfaceRoot {
|
|||
|
||||
this.root.add(new TokenCards(scene, { map, assets }));
|
||||
this.root.add(new LayerManager(scene, { map }));
|
||||
}
|
||||
|
||||
this.root.add(new Toolbar(scene, { mode, actions }));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
@use '../../../style/text'
|
||||
@use '../../../style/slice'
|
||||
@use '../../../style/def' as *
|
||||
|
||||
.TokenCards
|
||||
.TokenCard
|
||||
@extend .slice_background
|
||||
position: relative
|
||||
width: 340px
|
||||
height: 450px
|
||||
top: 0px
|
||||
|
||||
pointer-events: initial
|
||||
transition: top $t-fast
|
||||
|
||||
.TokenCard-Pin
|
||||
position: absolute
|
||||
top: 4px * 3
|
||||
right: -1px * 3
|
||||
width: 12px * 3
|
||||
height: 12px * 3
|
||||
padding: 0
|
||||
margin: 0
|
||||
|
||||
opacity: 0
|
||||
border: none
|
||||
outline: none
|
||||
background-size: cover
|
||||
image-rendering: crisp-edges
|
||||
image-rendering: pixelated
|
||||
background-color: transparent
|
||||
background-image: url(/app/static/icon/pin_up.png)
|
||||
|
||||
&:hover, &:focus-within, &.Pinned
|
||||
top: -406px
|
||||
transition: top $t-med
|
||||
|
||||
.TokenCard-Pin
|
||||
opacity: .5
|
||||
|
||||
&:hover, &:focus-visible
|
||||
opacity: 1
|
||||
|
||||
.TokenCard-Name
|
||||
margin-top: 12px !important
|
||||
|
||||
.TokenCard-Synopsis
|
||||
opacity: 0 !important
|
||||
|
||||
.TokenCard-Thumbnail
|
||||
margin-top: 1px * 3
|
||||
|
||||
&.Pinned
|
||||
.TokenCard-Pin
|
||||
background-image: url(/app/static/icon/pin_down.png)
|
||||
opacity: 1
|
||||
|
||||
&:hover, &:focus-visible
|
||||
opacity: .5
|
||||
|
||||
.TokenCard-Inner
|
||||
@include slice.slice_invert(3, 4px)
|
||||
|
||||
.TokenCard-Top
|
||||
display: grid
|
||||
grid-gap: 9px
|
||||
height: 19px * 3
|
||||
|
||||
overflow: hidden
|
||||
margin: 2px * 3 0
|
||||
grid-template-columns: #{18px * 3} 1fr
|
||||
|
||||
.TokenCard-Thumbnail
|
||||
@extend .slice_highlight
|
||||
|
||||
width: 18px * 3
|
||||
height: 18px * 3
|
||||
|
||||
transition: margin $t-fast
|
||||
|
||||
.TokenCard-ThumbnailInner
|
||||
@include slice.slice_invert
|
||||
padding: 1px * 3
|
||||
|
||||
background-position: 1px * 3 1px * 3
|
||||
background-clip: content-box
|
||||
image-rendering: crisp-edges
|
||||
image-rendering: pixelated
|
||||
|
||||
.TokenCard-TopDetails
|
||||
width: 100%
|
||||
overflow: hidden
|
||||
|
||||
.TokenCard-Name
|
||||
@include text.line_clamp
|
||||
margin: -6px 0 0 0
|
||||
|
||||
color: $neutral-1000
|
||||
transition: margin $t-fast
|
||||
|
||||
.TokenCard-Synopsis
|
||||
@include text.line_clamp
|
||||
|
||||
margin: 3px 0
|
||||
|
||||
opacity: 1
|
||||
color: $accent-200
|
||||
transition: opacity $t-fast
|
||||
|
||||
.TokenCard-Sliders
|
||||
display: grid
|
||||
grid-gap: 6px
|
||||
margin-top: 15px
|
||||
|
||||
.TokenCard-NewSlider
|
||||
@extend .slice_highlight
|
||||
|
||||
span
|
||||
display: block
|
||||
margin: -10px
|
||||
|
||||
|
||||
.TokenCard-Note
|
||||
margin-top: 15px
|
||||
padding: 6px
|
||||
border-color: $accent-300
|
||||
background-color: transparent
|
||||
|
||||
color: $neutral-1000
|
||||
|
||||
&:focus
|
||||
border-color: $accent-050
|
||||
|
||||
&::placeholder
|
||||
color: $accent-300
|
|
@ -0,0 +1,67 @@
|
|||
import * as Preact from 'preact';
|
||||
|
||||
import './TokenCard.sass';
|
||||
|
||||
import TokenSlider from './TokenSlider';
|
||||
import Text from '../../../components/input/fields/InputText';
|
||||
import { TokenSliderData, TokenMetaData } from '../../map/token/Token';
|
||||
|
||||
import { Asset } from '../../util/Asset';
|
||||
|
||||
interface TokenCardProps extends TokenMetaData {
|
||||
assets: Asset[];
|
||||
pinned: boolean;
|
||||
|
||||
setProps: (data: Partial<TokenMetaData>) => void;
|
||||
setPinned: (pinned: boolean) => void;
|
||||
}
|
||||
|
||||
export default function TokenCard(props: TokenCardProps) {
|
||||
const icon: string = '';
|
||||
// props.assets.filter(a => a.identifier === props.appearance.sprite)[0];
|
||||
const scale = 4;
|
||||
// icon.dimensions!.x / icon.tileSize;
|
||||
|
||||
const handleAddSlider = () => {
|
||||
props.setProps({ sliders: [ ...props.sliders, { name: 'Untitled', max: 10, current: 10, icon: 1 } ]});
|
||||
};
|
||||
|
||||
const handleUpdateSlider = (ind: number, data: Partial<TokenSliderData>) => {
|
||||
const newSliders: TokenSliderData[] = [ ...props.sliders ];
|
||||
newSliders[ind] = { ...newSliders[ind], ...data };
|
||||
props.setProps({ sliders: newSliders });
|
||||
};
|
||||
|
||||
const handleTogglePin = (e: MouseEvent) => {
|
||||
props.setPinned(!props.pinned);
|
||||
if (props.pinned) (e.target as any).blur();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div class={('TokenCard ' + (props.pinned ? 'Pinned' : '')).trim()}>
|
||||
<div class='TokenCard-Inner'>
|
||||
<button class='TokenCard-Pin'
|
||||
onClick={handleTogglePin} />
|
||||
<div class='TokenCard-Top'>
|
||||
<div class='TokenCard-Thumbnail'>
|
||||
<div class='TokenCard-ThumbnailInner' style={{
|
||||
backgroundImage: `url("/app/asset/${icon}")`,
|
||||
backgroundSize: (48 * scale) + 'px'
|
||||
}} />
|
||||
</div>
|
||||
<div class='TokenCard-TopDetails'>
|
||||
<h1 class='TokenCard-Name'>{props.name}</h1>
|
||||
<p class='TokenCard-Synopsis'>{props.note}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='TokenCard-Sliders'>
|
||||
{props.sliders.map((s, i) => <TokenSlider {...s} setProps={(props) => handleUpdateSlider(i, props)} />)}
|
||||
<button class='TokenCard-NewSlider' onClick={handleAddSlider} ><span>New Slider</span></button>
|
||||
</div>
|
||||
<Text class='TokenCard-Note' long={true} minRows={2} placeholder='Enter notes here...'
|
||||
value={props.note} setValue={note => props.setProps({ note })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
@use '../../../style/text'
|
||||
@use '../../../style/slice'
|
||||
@use '../../../style/def' as *
|
||||
|
||||
.TokenCards
|
||||
|
@ -24,211 +22,3 @@
|
|||
|
||||
.TokenCard-Thumbnail
|
||||
margin-top: 1px * 3
|
||||
|
||||
.TokenCard
|
||||
@extend .slice_background
|
||||
position: relative
|
||||
width: 340px
|
||||
height: 450px
|
||||
top: 0px
|
||||
|
||||
pointer-events: initial
|
||||
transition: top $t-fast
|
||||
|
||||
.TokenCard-Pin
|
||||
position: absolute
|
||||
top: 4px * 3
|
||||
right: -1px * 3
|
||||
width: 12px * 3
|
||||
height: 12px * 3
|
||||
padding: 0
|
||||
margin: 0
|
||||
|
||||
opacity: 0
|
||||
border: none
|
||||
outline: none
|
||||
background-size: cover
|
||||
image-rendering: crisp-edges
|
||||
image-rendering: pixelated
|
||||
background-color: transparent
|
||||
background-image: url(/app/static/icon/pin_up.png)
|
||||
|
||||
&:hover, &:focus-within, &.Pinned
|
||||
top: -406px
|
||||
transition: top $t-med
|
||||
|
||||
.TokenCard-Pin
|
||||
opacity: .5
|
||||
|
||||
&:hover, &:focus-visible
|
||||
opacity: 1
|
||||
|
||||
.TokenCard-Name
|
||||
margin-top: 12px !important
|
||||
|
||||
.TokenCard-Synopsis
|
||||
opacity: 0 !important
|
||||
|
||||
.TokenCard-Thumbnail
|
||||
margin-top: 1px * 3
|
||||
|
||||
&.Pinned
|
||||
.TokenCard-Pin
|
||||
background-image: url(/app/static/icon/pin_down.png)
|
||||
opacity: 1
|
||||
|
||||
&:hover, &:focus-visible
|
||||
opacity: .5
|
||||
|
||||
.TokenCard-Inner
|
||||
@include slice.slice_invert(3, 4px)
|
||||
|
||||
.TokenCard-Top
|
||||
display: grid
|
||||
grid-gap: 9px
|
||||
height: 19px * 3
|
||||
|
||||
overflow: hidden
|
||||
margin: 2px * 3 0
|
||||
grid-template-columns: #{18px * 3} 1fr
|
||||
|
||||
.TokenCard-Thumbnail
|
||||
@extend .slice_highlight
|
||||
|
||||
width: 18px * 3
|
||||
height: 18px * 3
|
||||
|
||||
transition: margin $t-fast
|
||||
|
||||
.TokenCard-ThumbnailInner
|
||||
@include slice.slice_invert
|
||||
padding: 1px * 3
|
||||
|
||||
background-position: 1px * 3 1px * 3
|
||||
background-clip: content-box
|
||||
image-rendering: crisp-edges
|
||||
image-rendering: pixelated
|
||||
|
||||
.TokenCard-TopDetails
|
||||
width: 100%
|
||||
overflow: hidden
|
||||
|
||||
.TokenCard-Name
|
||||
@include text.line_clamp
|
||||
margin: -6px 0 0 0
|
||||
|
||||
color: $neutral-1000
|
||||
transition: margin $t-fast
|
||||
|
||||
.TokenCard-Synopsis
|
||||
@include text.line_clamp
|
||||
|
||||
margin: 3px 0
|
||||
|
||||
opacity: 1
|
||||
color: $accent-200
|
||||
transition: opacity $t-fast
|
||||
|
||||
.TokenCard-Sliders
|
||||
display: grid
|
||||
grid-gap: 6px
|
||||
margin-top: 15px
|
||||
|
||||
.TokenCard-NewSlider
|
||||
@extend .slice_highlight
|
||||
|
||||
span
|
||||
display: block
|
||||
margin: -10px
|
||||
|
||||
|
||||
.TokenCard-Note
|
||||
margin-top: 15px
|
||||
padding: 6px
|
||||
border-color: $accent-300
|
||||
background-color: transparent
|
||||
|
||||
color: $neutral-1000
|
||||
|
||||
&:focus
|
||||
border-color: $accent-050
|
||||
|
||||
&::placeholder
|
||||
color: $accent-300
|
||||
|
||||
.TokenSlider
|
||||
display: flex
|
||||
gap: 6px
|
||||
|
||||
.TokenSlider-IconWrap
|
||||
@extend .slice_highlight
|
||||
|
||||
.TokenSlider-Icon
|
||||
@include slice.slice_invert
|
||||
width: 36px
|
||||
height: 36px
|
||||
|
||||
border-radius: 0
|
||||
background-size: 900%
|
||||
image-rendering: crisp-edges
|
||||
image-rendering: pixelated
|
||||
background-image: url(/app/static/icon/slider_icons.png)
|
||||
|
||||
.TokenSlider-Slider
|
||||
@extend .slice_outline_white
|
||||
flex-grow: 1
|
||||
|
||||
.TokenSlider-SliderInner
|
||||
@include slice.slice_invert
|
||||
border-radius: 0
|
||||
position: relative
|
||||
|
||||
.TokenSlider-Bar
|
||||
position: absolute
|
||||
height: calc(100% - 6px)
|
||||
clip-path: polygon(0 3px, 3px 3px, 3px 0, calc(100% - 3px) 0, calc(100% - 3px) 3px, 100% 3px, 100% calc(100% - 3px), calc(100% - 3px) calc(100% - 3px), calc(100% - 3px) 100%, 3px 100%, 3px calc(100% - 3px), 0 calc(100% - 3px))
|
||||
|
||||
.TokenSlider-BarContent
|
||||
position: absolute
|
||||
display: flex
|
||||
justify-content: center
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
padding: 6px
|
||||
|
||||
.TokenSlider-Input
|
||||
padding: 0 2px
|
||||
margin: 0
|
||||
outline: 0
|
||||
border: none
|
||||
background: transparent
|
||||
|
||||
height: 24px
|
||||
|
||||
&:focus
|
||||
background: transparentize(black, .8)
|
||||
|
||||
.TokenSlider-Title
|
||||
width: 100%
|
||||
flex-shrink: 1
|
||||
min-width: 0
|
||||
|
||||
.TokenSlider-BarText
|
||||
font-weight: 500
|
||||
color: $neutral-1000
|
||||
text-shadow: 0px 1px 2px $accent-800, 0px 1px 4px transparentize($accent-800, 0.8)
|
||||
|
||||
.TokenSlider-Value, .TokenSlider-Max
|
||||
width: min-content
|
||||
|
||||
span
|
||||
opacity: .6
|
||||
padding: 3px
|
||||
|
||||
.SlideNumericInput-Tester
|
||||
position: absolute !important
|
||||
top: -10000 !important
|
||||
left: -10000 !important
|
||||
opacity: 0 !important
|
||||
|
|
|
@ -1,144 +1,15 @@
|
|||
import * as Preact from 'preact';
|
||||
import { bind } from './PreactComponent';
|
||||
import { useState, useEffect, useLayoutEffect, useRef } from 'preact/hooks';
|
||||
import { useState, useEffect, useCallback } from 'preact/hooks';
|
||||
|
||||
import './TokenCards.sass';
|
||||
|
||||
import Map from '../../map/Map';
|
||||
import Text from '../../../components/input/fields/InputText';
|
||||
import { TokenSliderData, TokenData } from '../../map/token/Token';
|
||||
|
||||
import TokenCard from './TokenCard';
|
||||
import { TokenMetaData } from '../../map/token/Token';
|
||||
|
||||
import { Asset } from '../../util/Asset';
|
||||
import { clamp } from '../../util/Helpers';
|
||||
|
||||
|
||||
function SliderNumericInput(props: { min: number; max: number;
|
||||
value: number; setValue: (val: number) => void; class?: string; }) {
|
||||
|
||||
const [ value, setValue ] = useState<string>(props.value + '');
|
||||
useEffect(() => { if ((value === '') !== (props.value === 0)) setValue(props.value + ''); }, [ props.value ]);
|
||||
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const [ width, setWidth ] = useState<number>(0);
|
||||
useLayoutEffect(() => setWidth(ref.current!.getBoundingClientRect().width), [ value ]);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
const newValue = clamp(props.value + (e.key === 'ArrowUp' ? 1 : -1) *
|
||||
(e.ctrlKey ? 5 : 1) * (e.shiftKey ? 10 : 1), props.min, props.max);
|
||||
props.setValue(newValue);
|
||||
setValue(newValue + '');
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (e: any) => {
|
||||
setValue(e.target.value);
|
||||
const numeric = Number.parseInt(e.target.value, 10);
|
||||
props.setValue(Number.isNaN(numeric) ? 0 : numeric);
|
||||
};
|
||||
|
||||
const handleBlur = () => setValue(props.value + '');
|
||||
|
||||
return (
|
||||
<Preact.Fragment>
|
||||
<input type='text' class={props.class} style={{ width: width + 'px' }} value={value}
|
||||
onKeyDown={handleKeyDown} onInput={handleInput} onChange={handleInput} onBlur={handleBlur} />
|
||||
<span ref={ref} class={('SlideNumericInput-Tester ' + (props.class ?? '')).trim()}>{value}</span>
|
||||
</Preact.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface TokenSliderProps extends TokenSliderData {
|
||||
setProps: (data: Partial<TokenSliderData>) => void;
|
||||
}
|
||||
|
||||
function TokenSlider(props: TokenSliderProps) {
|
||||
const handleChangeName = (e: any) => {
|
||||
const name: string = e.target.value;
|
||||
props.setProps({ name });
|
||||
};
|
||||
|
||||
return (
|
||||
<div class='TokenSlider'>
|
||||
<div class='TokenSlider-IconWrap'>
|
||||
<div class='TokenSlider-Icon' style={{ backgroundPosition: `${(props.icon || 0) * (100 / 8)}% 0`}} />
|
||||
</div>
|
||||
<div class='TokenSlider-Slider'>
|
||||
<div class='TokenSlider-SliderInner'>
|
||||
<div class='TokenSlider-Bar' style={{ backgroundColor: props.color ?? '#f06292',
|
||||
width: 'calc(' + ((props.current - (props.min || 0)) / props.max) * 100 + '% - 6px)'}}/>
|
||||
<div class='TokenSlider-BarContent'>
|
||||
<input class='TokenSlider-Input TokenSlider-BarText TokenSlider-Title'
|
||||
value={props.name} onChange={handleChangeName}/>
|
||||
|
||||
<SliderNumericInput class='TokenSlider-Input TokenSlider-BarText' value={props.current}
|
||||
min={props.min ?? 0} max={props.max} setValue={current => props.setProps({ current })} />
|
||||
<span class='TokenSlider-BarText'>/</span>
|
||||
<SliderNumericInput class='TokenSlider-Input TokenSlider-BarText' value={props.max}
|
||||
min={0} max={Number.POSITIVE_INFINITY} setValue={max => props.setProps({ max })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TokenCardProps extends TokenData {
|
||||
assets: Asset[];
|
||||
|
||||
setProps: (data: Partial<TokenData>) => void;
|
||||
}
|
||||
|
||||
function TokenCard(props: TokenCardProps) {
|
||||
const icon: Asset | undefined = props.assets.filter(a => a.identifier === props.appearance.sprite)[0];
|
||||
const scale = icon.dimensions!.x / icon.tileSize;
|
||||
|
||||
const handleAddSlider = () => {
|
||||
props.setProps({ sliders: [ ...props.sliders, { name: 'Untitled', max: 10, current: 10, icon: 1 } ]});
|
||||
};
|
||||
|
||||
const handleUpdateSlider = (ind: number, data: Partial<TokenSliderData>) => {
|
||||
const newSliders: TokenSliderData[] = [ ...props.sliders ];
|
||||
newSliders[ind] = { ...newSliders[ind], ...data };
|
||||
props.setProps({ sliders: newSliders });
|
||||
};
|
||||
|
||||
const handleTogglePin = (e: MouseEvent) => {
|
||||
props.setProps({ pinned: !props.pinned });
|
||||
if (props.pinned) (e.target as any).blur();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div class={('TokenCard ' + (props.pinned ? 'Pinned' : '')).trim()}>
|
||||
<div class='TokenCard-Inner'>
|
||||
<button class='TokenCard-Pin'
|
||||
onClick={handleTogglePin} />
|
||||
<div class='TokenCard-Top'>
|
||||
<div class='TokenCard-Thumbnail'>
|
||||
<div class='TokenCard-ThumbnailInner' style={{
|
||||
backgroundImage: `url("/app/asset/${icon.path}")`,
|
||||
backgroundSize: (48 * scale) + 'px'
|
||||
}} />
|
||||
</div>
|
||||
<div class='TokenCard-TopDetails'>
|
||||
<h1 class='TokenCard-Name'>{props.name}</h1>
|
||||
<p class='TokenCard-Synopsis'>{props.note}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='TokenCard-Sliders'>
|
||||
{props.sliders.map((s, i) => <TokenSlider {...s} setProps={(props) => handleUpdateSlider(i, props)} />)}
|
||||
<button class='TokenCard-NewSlider' onClick={handleAddSlider} ><span>New Slider</span></button>
|
||||
</div>
|
||||
<Text class='TokenCard-Note' long={true} minRows={2} placeholder='Enter notes here...'
|
||||
value={props.note} setValue={note => props.setProps({ note })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
map: Map;
|
||||
|
@ -146,26 +17,40 @@ interface Props {
|
|||
}
|
||||
|
||||
export default bind<Props>(function TokenCards({ map, assets }: Props) {
|
||||
const [ cards, setCards ] = useState<TokenData[]>(map.tokens.getTokenData());
|
||||
const [ cards, setCards ] = useState<TokenMetaData[]>(map.tokens.getAllMeta());
|
||||
const [ pinned, setPinned ] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventCb = () => {
|
||||
setCards(JSON.parse(JSON.stringify(map.tokens.getTokenData())));
|
||||
setCards(JSON.parse(JSON.stringify(map.tokens.getAllMeta())));
|
||||
};
|
||||
|
||||
map.tokens.bind(eventCb);
|
||||
return () => map.tokens.unbind(eventCb);
|
||||
map.tokens.event.bind(eventCb);
|
||||
return () => map.tokens.event.unbind(eventCb);
|
||||
}, []);
|
||||
|
||||
const handleSetProps = (ind: number, data: Partial<TokenData>) => {
|
||||
map.tokens.setToken({
|
||||
uuid: cards[ind].uuid,
|
||||
...data
|
||||
});
|
||||
const handleSetProps = (ind: number, data: Partial<TokenMetaData>) => {
|
||||
map.tokens.setMeta(cards[ind].uuid, data);
|
||||
};
|
||||
|
||||
const handleSetPinned = useCallback((uuid: string, pin?: boolean) => {
|
||||
setPinned(pinned => {
|
||||
pin = pin ?? !pinned.includes(uuid);
|
||||
if (pin) {
|
||||
if (!pinned.includes(uuid)) return [ ...pinned, uuid ];
|
||||
return pinned;
|
||||
}
|
||||
else {
|
||||
const ind = pinned.indexOf(uuid);
|
||||
const n = [ ...pinned ];
|
||||
n.splice(ind, 1);
|
||||
return n;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fnCallback = (evt: KeyboardEvent) => {
|
||||
const fnKeyCallback = (evt: KeyboardEvent) => {
|
||||
if (evt.key[0] !== 'F' || evt.key.length !== 2 || !(evt.key.charCodeAt(1) >= '1'.charCodeAt(0) &&
|
||||
evt.key.charCodeAt(1) <= '8'.charCodeAt(0))) return;
|
||||
|
||||
|
@ -173,16 +58,21 @@ export default bind<Props>(function TokenCards({ map, assets }: Props) {
|
|||
evt.stopPropagation();
|
||||
|
||||
const ind = Number.parseInt(evt.key.substr(1), 10);
|
||||
if (cards.length >= ind) handleSetProps(ind - 1, { pinned: !cards[ind - 1].pinned });
|
||||
if (cards.length >= ind) handleSetPinned(cards[ind - 1].uuid);
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', fnCallback);
|
||||
return () => window.removeEventListener('keydown', fnCallback);
|
||||
window.addEventListener('keydown', fnKeyCallback);
|
||||
return () => window.removeEventListener('keydown', fnKeyCallback);
|
||||
}, [ cards ]);
|
||||
|
||||
return (
|
||||
<div class='TokenCards'>
|
||||
{cards.map((c, i) => <TokenCard {...c} assets={assets} setProps={u => handleSetProps(i, u)} />)}
|
||||
{cards.map((c, i) => <TokenCard {...c}
|
||||
assets={assets}
|
||||
pinned={pinned.includes(c.uuid)}
|
||||
setProps={u => handleSetProps(i, u)}
|
||||
setPinned={p => handleSetPinned(c.uuid, p)}
|
||||
/>)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import TokenMode from '../../mode/TokenMode';
|
|||
import ModeManager from '../../mode/ModeManager';
|
||||
import type InputManager from '../../InputManager';
|
||||
|
||||
import { Vec2 } from '../../util/Vec';
|
||||
import { Asset } from '../../util/Asset';
|
||||
|
||||
export default class TokenSidebar extends Sidebar {
|
||||
|
@ -45,15 +46,15 @@ export default class TokenSidebar extends Sidebar {
|
|||
|
||||
this.spinTimer++;
|
||||
if (this.spinTimer > 20) {
|
||||
let index = hoveredToken.getFrame() + 1;
|
||||
let index = hoveredToken.getFrameIndex() + 1;
|
||||
index %= hoveredToken.getFrameCount();
|
||||
hoveredToken.setToken({ appearance: { sprite: hoveredToken.getToken().appearance.sprite, index }});
|
||||
hoveredToken.setFrame(index);
|
||||
this.spinTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
elemUnhover(): void {
|
||||
this.sprites.forEach(t => t.setToken({ appearance: { sprite: t.getToken().appearance.sprite, index: 0 }}));
|
||||
this.sprites.forEach(t => t.setFrame(0));
|
||||
}
|
||||
|
||||
elemClick(x: number, y: number): void {
|
||||
|
@ -70,12 +71,11 @@ export default class TokenSidebar extends Sidebar {
|
|||
|
||||
if (x === 0) this.backgrounds[y].setFrame(0);
|
||||
|
||||
let token = new Token(this.scene, { appearance: { sprite, index: 0 }});
|
||||
token.setPosition(4 + x * 21, 4 + y * 21);
|
||||
token.setScale(16);
|
||||
let token = new Token(this.scene, {}, new Vec2(4 + x * 21, 4 + y * 21), sprite);
|
||||
token.setScale(1);
|
||||
|
||||
this.sprites.push(token);
|
||||
this.list.push(token);
|
||||
this.add(token);
|
||||
|
||||
this.bringToTop(this.activeSpriteCursor);
|
||||
this.bringToTop(this.hoverSpriteCursor);
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
@use '../../../style/slice'
|
||||
@use '../../../style/def' as *
|
||||
|
||||
.TokenSlider
|
||||
display: flex
|
||||
gap: 6px
|
||||
|
||||
.TokenSlider-IconWrap
|
||||
@extend .slice_highlight
|
||||
|
||||
.TokenSlider-Icon
|
||||
@include slice.slice_invert
|
||||
width: 36px
|
||||
height: 36px
|
||||
|
||||
border-radius: 0
|
||||
background-size: 900%
|
||||
image-rendering: crisp-edges
|
||||
image-rendering: pixelated
|
||||
background-image: url(/app/static/icon/slider_icons.png)
|
||||
|
||||
.TokenSlider-Slider
|
||||
@extend .slice_outline_white
|
||||
flex-grow: 11
|
||||
|
||||
.TokenSlider-SliderInner
|
||||
@include slice.slice_invert
|
||||
border-radius: 0
|
||||
position: relative
|
||||
|
||||
.TokenSlider-Bar
|
||||
position: absolute
|
||||
height: calc(100% - 6px)
|
||||
clip-path: polygon(0 3px, 3px 3px, 3px 0, calc(100% - 3px) 0, calc(100% - 3px) 3px, 100% 3px, 100% calc(100% - 3px), calc(100% - 3px) calc(100% - 3px), calc(100% - 3px) 100%, 3px 100%, 3px calc(100% - 3px), 0 calc(100% - 3px))
|
||||
|
||||
.TokenSlider-BarContent
|
||||
position: absolute
|
||||
display: flex
|
||||
justify-content: center
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
padding: 6px
|
||||
|
||||
.TokenSlider-Input
|
||||
padding: 0 2px
|
||||
margin: 0
|
||||
outline: 0
|
||||
border: none
|
||||
background: transparent
|
||||
|
||||
height: 24px
|
||||
|
||||
&:focus
|
||||
background: transparentize(black, .8)
|
||||
|
||||
.TokenSlider-Title
|
||||
width: 100%
|
||||
flex-shrink: 1
|
||||
min-width: 0
|
||||
|
||||
.TokenSlider-BarText
|
||||
font-weight: 500
|
||||
color: $neutral-1000
|
||||
text-shadow: 0px 1px 2px $accent-800, 0px 1px 4px transparentize($accent-800, 0.8)
|
||||
|
||||
.TokenSlider-Value, .TokenSlider-Max
|
||||
width: min-content
|
||||
|
||||
span
|
||||
opacity: .6
|
||||
padding: 3px
|
||||
|
||||
.SlideNumericInput-Tester
|
||||
opacity: 0 !important
|
||||
top: -10000px !important
|
||||
left: -10000px !important
|
||||
position: absolute !important
|
|
@ -0,0 +1,82 @@
|
|||
import * as Preact from 'preact';
|
||||
import { useState, useEffect, useLayoutEffect, useRef } from 'preact/hooks';
|
||||
|
||||
import './TokenSlider.sass';
|
||||
|
||||
import { TokenSliderData } from '../../map/token/Token';
|
||||
|
||||
import { clamp } from '../../util/Helpers';
|
||||
|
||||
function SliderNumericInput(props: { min: number; max: number;
|
||||
value: number; setValue: (val: number) => void; class?: string; }) {
|
||||
|
||||
const [ value, setValue ] = useState<string>(props.value + '');
|
||||
useEffect(() => { if ((value === '') !== (props.value === 0)) setValue(props.value + ''); }, [ props.value ]);
|
||||
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const [ width, setWidth ] = useState<number>(0);
|
||||
useLayoutEffect(() => setWidth(ref.current!.getBoundingClientRect().width), [ value ]);
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
const newValue = clamp(props.value + (e.key === 'ArrowUp' ? 1 : -1) *
|
||||
(e.ctrlKey ? 5 : 1) * (e.shiftKey ? 10 : 1), props.min, props.max);
|
||||
props.setValue(newValue);
|
||||
setValue(newValue + '');
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (e: any) => {
|
||||
setValue(e.target.value);
|
||||
const numeric = Number.parseInt(e.target.value, 10);
|
||||
props.setValue(Number.isNaN(numeric) ? 0 : numeric);
|
||||
};
|
||||
|
||||
const handleBlur = () => setValue(props.value + '');
|
||||
|
||||
return (
|
||||
<Preact.Fragment>
|
||||
<input type='text' class={props.class} style={{ width: width + 'px' }} value={value}
|
||||
onKeyDown={handleKeyDown} onInput={handleInput} onChange={handleInput} onBlur={handleBlur} />
|
||||
<span ref={ref} class={('SlideNumericInput-Tester ' + (props.class ?? '')).trim()}>{value}</span>
|
||||
</Preact.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props extends TokenSliderData {
|
||||
setProps: (data: Partial<TokenSliderData>) => void;
|
||||
}
|
||||
|
||||
export default function TokenSlider(props: Props) {
|
||||
const handleChangeName = (e: any) => {
|
||||
const name: string = e.target.value;
|
||||
props.setProps({ name });
|
||||
};
|
||||
|
||||
return (
|
||||
<div class='TokenSlider'>
|
||||
<div class='TokenSlider-IconWrap'>
|
||||
<div class='TokenSlider-Icon' style={{ backgroundPosition: `${(props.icon || 0) * (100 / 8)}% 0`}} />
|
||||
</div>
|
||||
<div class='TokenSlider-Slider'>
|
||||
<div class='TokenSlider-SliderInner'>
|
||||
<div class='TokenSlider-Bar' style={{ backgroundColor: props.color ?? '#f06292',
|
||||
width: 'calc(' + ((props.current - (props.min || 0)) / props.max) * 100 + '% - 6px)'}}/>
|
||||
<div class='TokenSlider-BarContent'>
|
||||
<input class='TokenSlider-Input TokenSlider-BarText TokenSlider-Title'
|
||||
value={props.name} onChange={handleChangeName}/>
|
||||
|
||||
<SliderNumericInput class='TokenSlider-Input TokenSlider-BarText' value={props.current}
|
||||
min={props.min ?? 0} max={props.max} setValue={current => props.setProps({ current })} />
|
||||
<span class='TokenSlider-BarText'>/</span>
|
||||
<SliderNumericInput class='TokenSlider-Input TokenSlider-BarText' value={props.max}
|
||||
min={0} max={Number.POSITIVE_INFINITY} setValue={max => props.setProps({ max })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -29,8 +29,8 @@ export default bind<Props>(function LayerManager(props: Props) {
|
|||
setHasNext(props.actions.hasNext());
|
||||
};
|
||||
|
||||
props.actions.bind(actionCb);
|
||||
return () => props.actions.unbind(actionCb);
|
||||
props.actions.event.bind(actionCb);
|
||||
return () => props.actions.event.unbind(actionCb);
|
||||
}, [ props.actions ]);
|
||||
|
||||
const [ mode, setMode ] = useState<string>(ArchitectModeKey);
|
||||
|
|
|
@ -15,7 +15,8 @@ import { Asset } from '../util/Asset';
|
|||
*/
|
||||
|
||||
export default class Map {
|
||||
size: Vec2 = new Vec2(0, 0);
|
||||
identifier: string = '';
|
||||
size: Vec2 = new Vec2(2, 2);
|
||||
tileStore: TileStore = new TileStore();
|
||||
|
||||
tokens: TokenManager = new TokenManager();
|
||||
|
@ -26,9 +27,8 @@ export default class Map {
|
|||
private scene: Phaser.Scene = undefined as any;
|
||||
private chunks: MapChunk[][][] = [];
|
||||
|
||||
init(scene: Phaser.Scene, size: Vec2, assets: Asset[]) {
|
||||
init(scene: Phaser.Scene, assets: Asset[]) {
|
||||
this.scene = scene;
|
||||
this.size = size;
|
||||
|
||||
this.tokens.init(scene);
|
||||
this.tileStore.init(scene.textures, assets);
|
||||
|
@ -120,7 +120,7 @@ export default class Map {
|
|||
*/
|
||||
|
||||
save(): string {
|
||||
return MapSaver.save(this.size, this.layers);
|
||||
return MapSaver.save(this.size, this.identifier, this.layers, this.tokens);
|
||||
}
|
||||
|
||||
|
||||
|
@ -132,9 +132,10 @@ export default class Map {
|
|||
|
||||
load(mapData: string) {
|
||||
const data = MapSaver.load(mapData);
|
||||
this.size = data.size;
|
||||
this.size = new Vec2(data.size);
|
||||
this.identifier = data.identifier;
|
||||
|
||||
// this.chunks.forEach(cI => cI.forEach(cA => cA.forEach(c => c.destroy())));
|
||||
this.tokens.resetTokens(data.tokens);
|
||||
|
||||
this.layers = data.layers;
|
||||
if (this.layers.length === 0) this.layers.push(new MapLayer(0, this.size));
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import TokenManager from './token/TokenManager';
|
||||
import MapLayer, { LAYER_SERIALIZATION_ORDER } from './MapLayer';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import * as Buffer from '../util/Buffer';
|
||||
|
||||
/** Data pretaining to a deserialized map. */
|
||||
export interface DeserializedMap {
|
||||
size: Vec2;
|
||||
/** JSON-serializable map data */
|
||||
export interface SerializedMap {
|
||||
format: string;
|
||||
identifier: string;
|
||||
size: { x: number; y: number };
|
||||
tokens: any[];
|
||||
}
|
||||
|
||||
/** Deserialized map data, including layer array. */
|
||||
export interface DeserializedMap extends SerializedMap {
|
||||
layers: MapLayer[];
|
||||
}
|
||||
|
||||
|
@ -18,16 +26,19 @@ export interface DeserializedMap {
|
|||
* @returns {string} - a serialized map string.
|
||||
*/
|
||||
|
||||
export function save(size: Vec2, layers: MapLayer[]): string {
|
||||
export function save(size: Vec2, identifier: string, layers: MapLayer[], tokens: TokenManager): string {
|
||||
let mapData = '';
|
||||
|
||||
const mapMeta = {
|
||||
const mapJson: SerializedMap = {
|
||||
format: '1.0.0',
|
||||
size: size
|
||||
|
||||
size,
|
||||
identifier,
|
||||
tokens: tokens.serializeAllTokens()
|
||||
};
|
||||
|
||||
const mapMetaStr = JSON.stringify(mapMeta);
|
||||
mapData += mapMetaStr.length + '|' + mapMetaStr;
|
||||
const mapJsonStr = JSON.stringify(mapJson);
|
||||
mapData += mapJsonStr.length + '|' + mapJsonStr;
|
||||
|
||||
for (const layer of layers) {
|
||||
let layerStr = '';
|
||||
|
@ -71,17 +82,16 @@ export function load(mapData: string): DeserializedMap {
|
|||
const numEnd = mapData.indexOf('|');
|
||||
const num = Number.parseInt(mapData.substr(0, numEnd), 10);
|
||||
|
||||
const mapMeta = JSON.parse(mapData.slice(numEnd + 1, numEnd + 1 + num));
|
||||
const mapJson = JSON.parse(mapData.slice(numEnd + 1, numEnd + 1 + num));
|
||||
const data: DeserializedMap = { ...mapJson, layers: [] };
|
||||
mapData = mapData.substr(numEnd + num + 1);
|
||||
|
||||
const data: DeserializedMap = { ...mapMeta, layers: [] };
|
||||
|
||||
let layerInd = 0;
|
||||
while (mapData.length) {
|
||||
const numEnd = mapData.indexOf('|');
|
||||
const num = Number.parseInt(mapData.substr(0, numEnd), 10);
|
||||
|
||||
const layer = new MapLayer(layerInd++, data.size);
|
||||
const layer = new MapLayer(layerInd++, new Vec2(data.size));
|
||||
layer.load(mapData.slice(numEnd + 1, numEnd + 1 + num));
|
||||
data.layers.push(layer);
|
||||
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import EventHandler from '../../EventHandler';
|
||||
|
||||
import { Vec2 } from '../../util/Vec';
|
||||
import { generateId } from '../../util/Helpers';
|
||||
|
||||
/** Data pretaining to a token slider. */
|
||||
|
||||
/**
|
||||
* Represents a slider bar for a token.
|
||||
*/
|
||||
|
||||
export interface TokenSliderData {
|
||||
name: string;
|
||||
color?: string;
|
||||
|
@ -13,114 +18,192 @@ export interface TokenSliderData {
|
|||
current: number;
|
||||
}
|
||||
|
||||
/** Data pretaining to a token. */
|
||||
export interface TokenData {
|
||||
uuid: string;
|
||||
|
||||
pos: { x: number; y: number };
|
||||
appearance: { sprite: string; index: number };
|
||||
/**
|
||||
* The meta information (name, sliders, note) for a token.
|
||||
*/
|
||||
|
||||
export interface TokenMetaData {
|
||||
uuid: string;
|
||||
|
||||
name: string;
|
||||
note: string;
|
||||
|
||||
sliders: TokenSliderData[];
|
||||
|
||||
pinned: boolean;
|
||||
}
|
||||
|
||||
/** Default token data, for raw assignment. */
|
||||
const DEFAULT_TOKEN_DATA: Omit<TokenData, 'uuid'> = {
|
||||
pos: { x: 0, y: 0 },
|
||||
appearance: { sprite: '', index: 0 },
|
||||
|
||||
/**
|
||||
* The render information for a token, such as position and sprite.
|
||||
*/
|
||||
|
||||
export interface TokenRenderData {
|
||||
uuid: string;
|
||||
|
||||
pos: { x: number; y: number };
|
||||
appearance: { sprite: string; index: number };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Full token data, for serialization or info passing.
|
||||
*/
|
||||
|
||||
export interface TokenData {
|
||||
uuid: string;
|
||||
render: Omit<TokenRenderData, 'uuid'>;
|
||||
meta: Omit<TokenMetaData, 'uuid'>;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Event object emitted when a token's meta data is modified.
|
||||
*/
|
||||
|
||||
export interface TokenMetaEvent {
|
||||
token: Token;
|
||||
pre: TokenMetaData;
|
||||
post: TokenMetaData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Event object emitted when a token's texture changes, or it is moved.
|
||||
*/
|
||||
|
||||
export interface TokenRenderEvent {
|
||||
token: Token;
|
||||
pos: { pre: Vec2; post: Vec2 };
|
||||
appearance: { pre: { sprite: string; index: number }; post: { sprite: string; index: number }};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default token meta data.
|
||||
*/
|
||||
|
||||
const DEFAULT_TOKEN_META: Omit<TokenMetaData, 'uuid'> = {
|
||||
name: '',
|
||||
|
||||
note: '',
|
||||
sliders: [],
|
||||
|
||||
pinned: false
|
||||
sliders: []
|
||||
};
|
||||
|
||||
/** Event emitted when a token data is modified. */
|
||||
export interface TokenModifyEvent {
|
||||
token: Token;
|
||||
pre: TokenData;
|
||||
post: TokenData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a token in the world, its properties are determined by its TokenData,
|
||||
* which can be set, updated, and retrieved through public methods.
|
||||
*/
|
||||
|
||||
export default class Token extends Phaser.GameObjects.Container {
|
||||
readonly change = new EventHandler<TokenModifyEvent>();
|
||||
export default class Token extends Phaser.GameObjects.Sprite {
|
||||
readonly on_meta = new EventHandler<TokenMetaEvent>();
|
||||
readonly on_render = new EventHandler<TokenRenderEvent>();
|
||||
|
||||
private sprite: Phaser.GameObjects.Sprite;
|
||||
private shadow: Phaser.GameObjects.Sprite;
|
||||
|
||||
private tokenData: TokenData;
|
||||
private meta: TokenMetaData;
|
||||
|
||||
private hovered: boolean = false;
|
||||
private selected: boolean = false;
|
||||
|
||||
constructor(scene: Phaser.Scene, tokenData?: Partial<TokenData>) {
|
||||
super(scene, 0, 0);
|
||||
constructor(scene: Phaser.Scene, tokenData?: Partial<TokenMetaData>, pos?: Vec2, sprite?: string, index?: number) {
|
||||
super(scene, 0, 0, sprite ?? '', index);
|
||||
this.scene.add.existing(this);
|
||||
this.setDepth(500);
|
||||
|
||||
this.tokenData = { ...DEFAULT_TOKEN_DATA, uuid: '' };
|
||||
this.setToken({
|
||||
...DEFAULT_TOKEN_DATA,
|
||||
this.meta = { ...DEFAULT_TOKEN_META, uuid: '' };
|
||||
this.setMeta({
|
||||
...DEFAULT_TOKEN_META,
|
||||
...tokenData ?? {},
|
||||
uuid: tokenData?.uuid ?? generateId(32)
|
||||
});
|
||||
|
||||
this.shadow = new Phaser.GameObjects.Sprite(this.scene, -1 / 16, -1 / 16, '');
|
||||
this.shadow.setOrigin(0, 0);
|
||||
this.shadow = this.scene.add.sprite(this.x, this.y, sprite ?? '', index);
|
||||
this.shadow.setOrigin(1 / 18, 1 / 18);
|
||||
this.shadow.setScale(18 / 16 / this.shadow.width, 18 / 16 / 4 / this.shadow.height);
|
||||
this.shadow.setAlpha(0.1, 0.1, 0.3, 0.3);
|
||||
this.shadow.setTint(0x000000);
|
||||
this.list.push(this.shadow);
|
||||
this.shadow.setDepth(this.depth - 1);
|
||||
|
||||
this.sprite = new Phaser.GameObjects.Sprite(this.scene, -1 / 16, -1 / 16, '');
|
||||
this.sprite.setOrigin(0, 0);
|
||||
this.sprite.setScale(18 / 16 / this.sprite.width, 18 / 16 / this.sprite.height);
|
||||
this.setPosition(this.x, this.y);
|
||||
this.list.push(this.sprite);
|
||||
this.on('removefromscene', () => this.shadow.destroy());
|
||||
|
||||
this.updateAppearance();
|
||||
this.setOrigin(1 / 18, 1 / 18);
|
||||
this.setScale(18 / 16 / this.width, 18 / 16 / this.height);
|
||||
this.setPosition(pos?.x ?? 0, pos?.y ?? 0);
|
||||
}
|
||||
|
||||
setToken(data: Partial<TokenData>) {
|
||||
if (!this.tokenData) return;
|
||||
serialize(): TokenData {
|
||||
const data = {
|
||||
uuid: this.getUUID(),
|
||||
meta: this.getMeta(),
|
||||
render: this.getRender()
|
||||
};
|
||||
|
||||
const preSer = JSON.stringify(this.tokenData);
|
||||
const postSer = JSON.stringify({ ...this.tokenData, ...data });
|
||||
delete (data.meta as any).uuid;
|
||||
delete (data.render as any).uuid;
|
||||
return data;
|
||||
};
|
||||
|
||||
getMeta(): TokenMetaData {
|
||||
return JSON.parse(JSON.stringify(this.meta));
|
||||
}
|
||||
|
||||
setMeta(data: Partial<TokenMetaData>) {
|
||||
if (!this.meta) return;
|
||||
|
||||
const preSer = JSON.stringify(this.meta);
|
||||
const postSer = JSON.stringify({ ...this.meta, ...data });
|
||||
|
||||
if (preSer === postSer) return;
|
||||
|
||||
const pre = JSON.parse(preSer);
|
||||
const post = JSON.parse(postSer);
|
||||
|
||||
this.tokenData = post;
|
||||
this.updateAppearance();
|
||||
|
||||
this.change.dispatch({ token: this, pre, post });
|
||||
this.meta = post;
|
||||
this.on_meta.dispatch({ token: this, pre, post });
|
||||
}
|
||||
|
||||
getToken(): TokenData {
|
||||
return JSON.parse(JSON.stringify(this.tokenData));
|
||||
getRender(): TokenRenderData {
|
||||
return {
|
||||
uuid: this.getUUID(),
|
||||
pos: new Vec2(this.x, this.y),
|
||||
appearance: { sprite: this.texture.key, index: this.frame.name as any }
|
||||
};
|
||||
}
|
||||
|
||||
setRender(render: Partial<TokenRenderData>) {
|
||||
if (render.pos) this.setPosition(render.pos.x, render.pos.y);
|
||||
if (render.appearance) this.setTexture(render.appearance.sprite, render.appearance.index);
|
||||
}
|
||||
|
||||
getUUID(): string {
|
||||
return this.tokenData.uuid;
|
||||
return this.meta.uuid;
|
||||
}
|
||||
|
||||
getFrame(): number {
|
||||
return this.tokenData.appearance.index;
|
||||
getFrameIndex(): number {
|
||||
return this.frame.name as any;
|
||||
}
|
||||
|
||||
getFrameCount(): number {
|
||||
return Object.keys(this.sprite.texture.frames).length - 1;
|
||||
return Object.keys(this.texture.frames).length - 1;
|
||||
}
|
||||
|
||||
setFrame(frame: number): this {
|
||||
const pos = new Vec2(this.x, this.y);
|
||||
const lastFrame = this.frame?.name as any || 0;
|
||||
Phaser.GameObjects.Sprite.prototype.setFrame.call(this, frame);
|
||||
|
||||
if (this.on_render) this.on_render.dispatch({
|
||||
token: this,
|
||||
pos: { pre: pos, post: pos },
|
||||
appearance: {
|
||||
pre: { sprite: this.texture.key, index: lastFrame },
|
||||
post: { sprite: this.texture.key, index: frame }
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.shadow) return this;
|
||||
this.shadow.setFrame(frame);
|
||||
return this;
|
||||
}
|
||||
|
||||
setSelected(selected: boolean) {
|
||||
|
@ -134,35 +217,52 @@ export default class Token extends Phaser.GameObjects.Container {
|
|||
}
|
||||
|
||||
setPosition(x?: number, y?: number): this {
|
||||
if (!this.tokenData) return this;
|
||||
this.setToken({ pos: { x: x ?? 0, y: y ?? x ?? 0 }});
|
||||
if (this.x === x && this.y === y) return this;
|
||||
const pre = new Vec2(this.x, this.y);
|
||||
const post = new Vec2(x, y);
|
||||
|
||||
Phaser.GameObjects.Sprite.prototype.setPosition.call(this, x, y);
|
||||
|
||||
if (this.on_render) this.on_render.dispatch({
|
||||
token: this,
|
||||
pos: { pre, post },
|
||||
appearance: {
|
||||
pre: { sprite: this.texture.key, index: this.getFrameIndex() },
|
||||
post: { sprite: this.texture.key, index: this.getFrameIndex() }
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.shadow) return this;
|
||||
this.shadow.setPosition(x, y! + this.displayHeight - this.shadow.displayHeight - 0.125);
|
||||
return this;
|
||||
}
|
||||
|
||||
private updateAppearance() {
|
||||
// console.log(this.tokenData);
|
||||
Phaser.GameObjects.Container.prototype.setPosition.call(this, this.tokenData.pos.x, this.tokenData.pos.y);
|
||||
if (!this.sprite || !this.shadow) return;
|
||||
|
||||
this.sprite.setTexture(this.tokenData.appearance.sprite);
|
||||
this.sprite.setFrame(this.tokenData.appearance.index);
|
||||
this.shadow.setTexture(this.tokenData.appearance.sprite);
|
||||
this.shadow.setFrame(this.tokenData.appearance.index);
|
||||
setTexture(key: string, index?: string | number): this {
|
||||
Phaser.GameObjects.Sprite.prototype.setTexture.call(this, key, index);
|
||||
this.setScale(18 / 16 / this.width, 18 / 16 / this.height);
|
||||
if (!this.shadow) return this;
|
||||
|
||||
this.shadow.setTexture(key, index);
|
||||
this.shadow.setScale(18 / 16 / this.shadow.width, 18 / 16 / 4 / this.shadow.height);
|
||||
this.sprite.setScale(18 / 16 / this.sprite.width, 18 / 16 / this.sprite.height);
|
||||
this.shadow.y = this.y + this.displayHeight - this.shadow.displayHeight - 0.125;
|
||||
|
||||
this.shadow.y = this.sprite.displayHeight - this.shadow.displayHeight - 0.125;
|
||||
return this;
|
||||
}
|
||||
|
||||
setVisible(visible: boolean): this {
|
||||
Phaser.GameObjects.Sprite.prototype.setVisible.call(this, visible);
|
||||
this.shadow.setVisible(visible);
|
||||
return this;
|
||||
}
|
||||
|
||||
private updateShader() {
|
||||
if (this.selected) {
|
||||
this.sprite.setPipeline('outline');
|
||||
this.sprite.pipeline.set1f('tex_size', this.sprite.texture.source[0].width);
|
||||
this.setPipeline('outline');
|
||||
this.pipeline.set1f('tex_size', this.texture.source[0].width);
|
||||
}
|
||||
else if (this.hovered) {
|
||||
this.sprite.setPipeline('brighten');
|
||||
this.setPipeline('brighten');
|
||||
}
|
||||
else this.sprite.resetPipeline();
|
||||
else this.resetPipeline();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import * as Phaser from 'phaser';
|
||||
|
||||
import Token, { TokenData, TokenModifyEvent } from './Token';
|
||||
import EventHandler from '../../EventHandler';
|
||||
import Token, { TokenData, TokenMetaData, TokenRenderData, TokenRenderEvent } from './Token';
|
||||
|
||||
import { Vec2 } from '../../util/Vec';
|
||||
|
||||
/** Token Event emitted by the TokenManager */
|
||||
export type TokenEvent = {
|
||||
uuid: string;
|
||||
} & ((
|
||||
{ type: 'modify' } & TokenModifyEvent
|
||||
{ type: 'modify' } & TokenRenderEvent
|
||||
) | {
|
||||
type: 'create';
|
||||
token: Token;
|
||||
|
@ -15,12 +18,11 @@ export type TokenEvent = {
|
|||
});
|
||||
|
||||
export default class TokenManager {
|
||||
readonly event = new EventHandler<TokenEvent>();
|
||||
|
||||
private scene: Phaser.Scene = null as any;
|
||||
|
||||
private tokens: Token[] = [];
|
||||
|
||||
private evtHandler = new EventHandler<TokenEvent>();
|
||||
|
||||
init(scene: Phaser.Scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
@ -28,6 +30,9 @@ export default class TokenManager {
|
|||
|
||||
/**
|
||||
* Gets a token based on it's UUID.
|
||||
*
|
||||
* @param {string} token - The token's UUID string.
|
||||
* @returns the token instance if it exists, or undefined.
|
||||
*/
|
||||
|
||||
getToken(token: string): Token | undefined {
|
||||
|
@ -39,29 +44,30 @@ export default class TokenManager {
|
|||
|
||||
|
||||
/**
|
||||
* Sets the data for a token, based on a UUID internally within the data structure.
|
||||
* Gets a list of token instances stored within this manager.
|
||||
*
|
||||
* @returns the array of token game objects.
|
||||
*/
|
||||
|
||||
setToken(data: Partial<TokenData>): Token | undefined {
|
||||
const token = this.tokens.filter(t => t.getUUID())[0];
|
||||
if (!token) return undefined;
|
||||
token.setToken(data);
|
||||
return token;
|
||||
getAllTokens(): Token[] {
|
||||
return this.tokens;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new token with the provided token
|
||||
* data, and adds it to the token list.
|
||||
* Creates a new token with the provided token data.
|
||||
*
|
||||
* @param {string} data - The TokenData to create the token from.
|
||||
* @returns the new token instance.
|
||||
*/
|
||||
|
||||
createToken(data: Partial<TokenData>): Token {
|
||||
const token = new Token(this.scene, data);
|
||||
token.change.bind(this.onChange);
|
||||
createToken(pos: Vec2, meta: Partial<TokenMetaData>, sprite?: string, index?: number): Token {
|
||||
const token = new Token(this.scene, meta, pos, sprite, index);
|
||||
token.on_render.bind(this.onChange);
|
||||
this.scene.add.existing(token);
|
||||
this.tokens.push(token);
|
||||
|
||||
this.evtHandler.dispatch({
|
||||
this.event.dispatch({
|
||||
type: 'create',
|
||||
uuid: token.getUUID(),
|
||||
token: token
|
||||
|
@ -73,18 +79,21 @@ export default class TokenManager {
|
|||
|
||||
/**
|
||||
* Deletes a token based on it's UUID or direct object.
|
||||
*
|
||||
* @param {string | Token} token - Either a token's UUID string, or the token instance itself.
|
||||
* @returns the token's data prior to deletion, or undefined if the token was not found.
|
||||
*/
|
||||
|
||||
deleteToken(token: string | Token): TokenData | undefined {
|
||||
for (let i = 0; i < this.tokens.length; i++) {
|
||||
if (typeof token === 'string' ? this.tokens[i].getUUID() === token : this.tokens[i] === token) {
|
||||
token = this.tokens[i];
|
||||
const data = token.getToken();
|
||||
const data = token.serialize();
|
||||
|
||||
this.tokens[i].destroy();
|
||||
token.destroy();
|
||||
this.tokens.splice(i, 1);
|
||||
|
||||
this.evtHandler.dispatch({
|
||||
this.event.dispatch({
|
||||
type: 'destroy',
|
||||
uuid: token.getUUID()
|
||||
});
|
||||
|
@ -97,48 +106,78 @@ export default class TokenManager {
|
|||
|
||||
|
||||
/**
|
||||
* Returns the array of token game objects.
|
||||
* Deletes all tokens, and optionally deserializes new ones into the scene.
|
||||
*
|
||||
* @param {TokenData[]} to - New tokens to deserialize.
|
||||
* @returns an array of the new token instances.
|
||||
*/
|
||||
|
||||
getTokens(): Token[] {
|
||||
return this.tokens;
|
||||
resetTokens(data?: TokenData[]): Token[] {
|
||||
while (this.tokens.length > 0) this.deleteToken(this.tokens[this.tokens.length - 1]);
|
||||
data?.forEach(d => this.createToken(new Vec2(d.render.pos as any), d.meta,
|
||||
d.render.appearance.sprite, d.render.appearance.index));
|
||||
return this.getAllTokens();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets an array of token data, frozen at the time of the function call.
|
||||
* Returns the serialized form of all tokens.
|
||||
*
|
||||
* @returns a TokenData[] of all token instances.
|
||||
*/
|
||||
|
||||
getTokenData(): TokenData[] {
|
||||
return this.tokens.map(t => t.getToken());
|
||||
serializeAllTokens(): TokenData[] {
|
||||
return this.tokens.map(t => t.serialize());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bind a callback to token change events.
|
||||
* Gets serialized token data for all the tokens within this manager.
|
||||
* This data is cloned, and safe to modify.
|
||||
*
|
||||
* @returns an array of cloned TokenData objects corresponding to each token.
|
||||
*/
|
||||
|
||||
bind(cb: (evt: TokenEvent) => boolean | void) {
|
||||
this.evtHandler.bind(cb);
|
||||
getAllMeta(): TokenMetaData[] {
|
||||
return this.tokens.map(t => t.getMeta());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unbind a callback from token change events.
|
||||
* Updates a token with the specified token data.
|
||||
*
|
||||
* @param {Partial<TokenMetaData>} data - The data to update the token with.
|
||||
* @returns the token instance, if it exists.
|
||||
*/
|
||||
|
||||
unbind(cb: (evt: TokenEvent) => boolean | void) {
|
||||
this.evtHandler.unbind(cb);
|
||||
setMeta(uuid: string, data: Partial<TokenMetaData>): Token | undefined {
|
||||
const token = this.getToken(uuid);
|
||||
if (!token) return undefined;
|
||||
token.setMeta(data);
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Callback to be called by Tokens when they change,
|
||||
* triggers an event dispatch.
|
||||
*
|
||||
*/
|
||||
|
||||
private onChange = (event: TokenModifyEvent) => {
|
||||
this.evtHandler.dispatch({
|
||||
setRender(uuid: string, data: Partial<TokenRenderData>): Token | undefined {
|
||||
const token = this.getToken(uuid);
|
||||
if (!token) return undefined;
|
||||
token.setRender(data);
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called by Tokens upon changing, triggers an event dispatch.
|
||||
*
|
||||
* @param {TokenModifyEvent} event - The event object from the token's change.
|
||||
*/
|
||||
|
||||
private onChange = (event: TokenRenderEvent) => {
|
||||
this.event.dispatch({
|
||||
type: 'modify',
|
||||
uuid: event.token.getUUID(),
|
||||
...event
|
||||
|
|
|
@ -172,7 +172,7 @@ export default class DrawMode extends Mode {
|
|||
const tilePos = cursorPos.floor();
|
||||
|
||||
let found: Token | undefined = undefined;
|
||||
this.map.tokens.getTokens().forEach(t => {
|
||||
this.map.tokens.getAllTokens().forEach(t => {
|
||||
const isFound = t.x === tilePos.x && t.y === tilePos.y;
|
||||
t.setSelected(!!(isFound && highlight));
|
||||
if (isFound) found = t;
|
||||
|
|
|
@ -30,7 +30,7 @@ export default class ModeManager {
|
|||
|
||||
private evtHandler = new EventHandler<ModeSwitchEvent>();
|
||||
|
||||
init(scene: Phaser.Scene, _modes: 'edit' | 'view', map: Map, actions: ActionManager, assets: Asset[]) {
|
||||
init(scene: Phaser.Scene, map: Map, actions: ActionManager, assets: Asset[]) {
|
||||
this.modes = {
|
||||
[ArchitectModeKey]: new ArchitectMode(scene, map, actions, assets),
|
||||
[TokenModeKey]: new TokenMode(scene, map, actions, assets),
|
||||
|
|
|
@ -4,7 +4,7 @@ import Mode from './Mode';
|
|||
import Map from '../map/Map';
|
||||
import InputManager from '../InputManager';
|
||||
import ActionManager from '../action/ActionManager';
|
||||
import Token, { TokenData } from '../map/token/Token';
|
||||
import Token, { TokenData, TokenRenderData } from '../map/token/Token';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
import { Asset } from '../util/Asset';
|
||||
|
@ -24,7 +24,7 @@ export default class TokenMode extends Mode {
|
|||
private selected: Set<Token> = new Set();
|
||||
|
||||
private startTilePos: Vec2 = new Vec2();
|
||||
private preMove: TokenData[] | null = null;
|
||||
private preMove: TokenRenderData[] | null = null;
|
||||
private clickTestState: null | false | true = null;
|
||||
|
||||
private cursor: Phaser.GameObjects.Sprite;
|
||||
|
@ -50,10 +50,10 @@ export default class TokenMode extends Mode {
|
|||
input.bindScrollEvent((delta: number) => {
|
||||
if (this.editMode !== 'move') return false;
|
||||
this.selected.forEach(token => {
|
||||
let index = token.getFrame() + delta;
|
||||
let index = token.getFrameIndex() + delta;
|
||||
if (index < 0) index += token.getFrameCount();
|
||||
index %= token.getFrameCount();
|
||||
token.setToken({ appearance: { sprite: token.getToken().appearance.sprite, index }});
|
||||
token.setFrame(index);
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
@ -64,8 +64,7 @@ export default class TokenMode extends Mode {
|
|||
|
||||
cursorPos = cursorPos.floor();
|
||||
|
||||
if (this.preview.getToken().appearance.sprite !== this.placeTokenType)
|
||||
this.preview.setToken({ appearance: { sprite: this.placeTokenType, index: 0 }});
|
||||
if (this.preview.texture.key !== this.placeTokenType) this.preview.setTexture(this.placeTokenType, 0);
|
||||
|
||||
switch (this.editMode) {
|
||||
default: break;
|
||||
|
@ -112,8 +111,8 @@ export default class TokenMode extends Mode {
|
|||
this.hovered?.setHovered(false);
|
||||
this.hovered = null;
|
||||
|
||||
for (let i = this.map.tokens.getTokens().length - 1; i >= 0; i--) {
|
||||
let token = this.map.tokens.getTokens()[i];
|
||||
for (let i = this.map.tokens.getAllTokens().length - 1; i >= 0; i--) {
|
||||
let token = this.map.tokens.getAllTokens()[i];
|
||||
if (cursorPos.x === token.x && cursorPos.y === token.y) {
|
||||
this.hovered = token;
|
||||
this.hovered.setHovered(true);
|
||||
|
@ -179,7 +178,7 @@ export default class TokenMode extends Mode {
|
|||
this.selected = new Set();
|
||||
}
|
||||
|
||||
for (const token of this.map.tokens.getTokens()) {
|
||||
for (const token of this.map.tokens.getAllTokens()) {
|
||||
if (token.x >= a.x && token.y >= a.y && token.x <= b.x && token.y <= b.y) {
|
||||
|
||||
if (input.keyDown('CTRL')) {
|
||||
|
@ -215,7 +214,7 @@ export default class TokenMode extends Mode {
|
|||
private handleMove(cursorPos: Vec2, input: InputManager) {
|
||||
this.cursor.setVisible(false);
|
||||
|
||||
if (!this.preMove) this.preMove = Array.from(this.selected).map(t => t.getToken());
|
||||
if (!this.preMove) this.preMove = Array.from(this.selected).map(t => t.getRender());
|
||||
|
||||
if (!this.selected.size) {
|
||||
this.editMode = 'place';
|
||||
|
@ -226,8 +225,8 @@ export default class TokenMode extends Mode {
|
|||
this.editMode = 'place';
|
||||
|
||||
if (this.clickTestState) {
|
||||
const post: TokenData[] = [];
|
||||
for (let t of this.selected) post.push(t.getToken());
|
||||
const post: TokenRenderData[] = [];
|
||||
for (let t of this.selected) post.push(t.getRender());
|
||||
this.actions.push({ type: 'modify_token', tokens: { pre: this.preMove!, post } });
|
||||
this.preMove = null;
|
||||
}
|
||||
|
@ -252,9 +251,8 @@ export default class TokenMode extends Mode {
|
|||
|
||||
private placeToken(cursorPos: Vec2): Token {
|
||||
const asset = this.assets.filter(a => a.identifier === this.placeTokenType)[0];
|
||||
const token = this.map.tokens.createToken({ name: asset.name, pos: cursorPos,
|
||||
appearance: { sprite: this.placeTokenType, index: 0 }});
|
||||
this.actions.push({ type: 'place_token', tokens: [ token.getToken() ] });
|
||||
const token = this.map.tokens.createToken(cursorPos, { name: asset.name }, this.placeTokenType);
|
||||
this.actions.push({ type: 'place_token', tokens: [ token.serialize() ] });
|
||||
return token;
|
||||
}
|
||||
|
||||
|
@ -272,17 +270,15 @@ export default class TokenMode extends Mode {
|
|||
}
|
||||
|
||||
private moveToken(x: number, y: number, index: number): void {
|
||||
let pre: TokenData[] = [];
|
||||
let post: TokenData[] = [];
|
||||
let pre: TokenRenderData[] = [];
|
||||
let post: TokenRenderData[] = [];
|
||||
|
||||
this.selected.forEach((token) => {
|
||||
const old = token.getToken();
|
||||
const old = token.getRender();
|
||||
pre.push(old);
|
||||
token.setToken({
|
||||
pos: { x: old.pos.x + x, y: old.pos.y + y },
|
||||
appearance: { sprite: old.appearance.sprite, index }
|
||||
});
|
||||
post.push(token.getToken());
|
||||
token.setPosition(old.pos.x + x, old.pos.y + y);
|
||||
token.setTexture(old.appearance.sprite, index);
|
||||
post.push(token.getRender());
|
||||
});
|
||||
|
||||
this.actions.push({ type: 'modify_token', tokens: { pre, post } });
|
||||
|
|
|
@ -9,32 +9,57 @@ async function emit<R = any>(socket: IO.Socket, event: string, data?: any): Prom
|
|||
});
|
||||
}
|
||||
|
||||
interface InitProps {
|
||||
user: string;
|
||||
identifier: string;
|
||||
mapIdentifier?: string;
|
||||
|
||||
onProgress: (progress: number) => void;
|
||||
}
|
||||
|
||||
export default class InitScene extends Phaser.Scene {
|
||||
private socket: IO.Socket = IO.io();
|
||||
|
||||
constructor() { super({key: 'InitScene'}); }
|
||||
|
||||
async create({ user, onProgress, identifier, socket }: {
|
||||
user: string; onProgress: (progress: number) => void; identifier: string; socket: IO.Socket; }) {
|
||||
async create({ user, onProgress, identifier, mapIdentifier }: InitProps) {
|
||||
this.socket.on('disconnect', this.onDisconnect);
|
||||
this.game.events.addListener('destroy', this.onDestroy);
|
||||
|
||||
let res: { state: true; campaign: Campaign; assets: Asset[] } | { state: false; error?: string }
|
||||
= await emit(socket, 'room_init', identifier);
|
||||
let display = res.state ? 'edit' : 'view';
|
||||
|
||||
if (!res.state) res =
|
||||
await emit(socket, 'room_join', { user, identifier }) as { state: true; campaign: Campaign; assets: Asset[] };
|
||||
|
||||
socket.on('disconnect', () => console.log('Disconnected!!!'));
|
||||
const { res, map } = await this.onConnect(user, identifier, mapIdentifier);
|
||||
|
||||
this.scene.start('LoadScene', {
|
||||
user,
|
||||
identifier,
|
||||
onProgress,
|
||||
socket,
|
||||
display,
|
||||
assets: res.assets,
|
||||
campaign: res.campaign });
|
||||
socket: this.socket,
|
||||
user, identifier,
|
||||
...res, map,
|
||||
|
||||
onProgress
|
||||
});
|
||||
|
||||
this.game.scene.stop('InitScene');
|
||||
this.game.scene.swapPosition('LoadScene', 'InitScene');
|
||||
}
|
||||
}
|
||||
|
||||
private onDestroy = async () => {
|
||||
this.socket.off('disconnect', this.onDisconnect);
|
||||
this.socket.disconnect();
|
||||
console.log('Destroyed!');
|
||||
};
|
||||
|
||||
private onDisconnect = async () => {
|
||||
console.log('Disconnected!!!');
|
||||
};
|
||||
|
||||
private onConnect = async (user: string, identifier: string, mapIdentifier: string | undefined):
|
||||
Promise<{ res: { campaign: Campaign; assets: Asset[] }; map: string | undefined }> => {
|
||||
|
||||
let res: { state: true; campaign: Campaign; assets: Asset[] } | { state: false; error?: string }
|
||||
= await emit(this.socket, 'room_init', identifier);
|
||||
|
||||
let map: string | undefined;
|
||||
if (res.state) map = (mapIdentifier ? res.campaign.maps.filter(m => m.identifier === mapIdentifier)[0] : res.campaign.maps[0]).data;
|
||||
else res = await emit(this.socket, 'room_join', { user, identifier }) as { state: true; campaign: Campaign; assets: Asset[] };
|
||||
|
||||
return { res, map };
|
||||
};
|
||||
}
|
||||
|
|
|
@ -60,12 +60,12 @@ export default class LoadScene extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
|
||||
create() {
|
||||
async create() {
|
||||
const glRenderer = this.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer;
|
||||
glRenderer.pipelines.add('brighten', new BrightenPipeline(this.game));
|
||||
glRenderer.pipelines.add('outline', new OutlinePipeline(this.game));
|
||||
|
||||
Promise.all(this.editorData!.assets.map(a => {
|
||||
await Promise.all(this.editorData!.assets.map(a => {
|
||||
if (a.type === 'token') {
|
||||
const { width, height } = (this.textures.get(a.identifier).frames as any).__BASE;
|
||||
a.dimensions = new Vec2(width, height);
|
||||
|
@ -76,13 +76,13 @@ export default class LoadScene extends Phaser.Scene {
|
|||
return Patch.tileset(this, a.identifier, a.tileSize);
|
||||
|
||||
else return new Promise<void>(resolve => resolve());
|
||||
})).then(() => {
|
||||
}));
|
||||
|
||||
this.editorData.onProgress(undefined);
|
||||
|
||||
this.game.scene.start('MapScene', this.editorData);
|
||||
this.game.scene.stop('LoadScene');
|
||||
this.game.scene.swapPosition('MapScene', 'LoadScene');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,9 @@ import InterfaceRoot from '../interface/InterfaceRoot';
|
|||
import Map from '../map/Map';
|
||||
import CameraControl from '../CameraControl';
|
||||
import ModeManager from '../mode/ModeManager';
|
||||
// import { SerializedMap } from '../map/MapSaver';
|
||||
|
||||
import { Vec2 } from '../util/Vec';
|
||||
// import { Vec2 } from '../util/Vec';
|
||||
import { Asset } from '../util/Asset';
|
||||
import EditorData from '../EditorData';
|
||||
|
||||
|
@ -22,12 +23,8 @@ export default class MapScene extends Phaser.Scene {
|
|||
mode: ModeManager = new ModeManager();
|
||||
interface: InterfaceRoot = new InterfaceRoot();
|
||||
|
||||
size: Vec2 = new Vec2();
|
||||
|
||||
map: Map = new Map();
|
||||
|
||||
saved: string = '';
|
||||
|
||||
constructor() { super({ key: 'MapScene' }); }
|
||||
|
||||
create(data: EditorData): void {
|
||||
|
@ -36,17 +33,12 @@ export default class MapScene extends Phaser.Scene {
|
|||
this.inputManager.init();
|
||||
this.view.init(this.cameras.main, this.inputManager);
|
||||
|
||||
this.size = new Vec2(data.campaign.maps[0].size);
|
||||
this.map.init(this, this.size, this.assets);
|
||||
this.map.init(this, this.assets);
|
||||
if (data.map) this.map.load(data.map);
|
||||
|
||||
const s = JSON.stringify({ size: new Vec2(32, 32) });
|
||||
this.map.load(s.length + '|' + s);
|
||||
|
||||
this.mode.init(this, data.display, this.map, this.actions, this.assets);
|
||||
this.actions.init(this, this.map, data.socket);
|
||||
this.interface.init(this, data.display, this.inputManager, this.mode, this.actions, this.map, this.assets);
|
||||
|
||||
// this.sound.play('mystify', { loop: true, volume: .2 });
|
||||
this.mode.init(this, this.map, this.actions, this.assets);
|
||||
this.interface.init(this, this.inputManager, this.mode, this.actions, this.map, this.assets);
|
||||
}
|
||||
|
||||
update(): void {
|
||||
|
@ -58,9 +50,6 @@ export default class MapScene extends Phaser.Scene {
|
|||
this.mode.update(this.view.cursorWorld, this.inputManager);
|
||||
|
||||
this.map.update();
|
||||
|
||||
if (this.inputManager.keyPressed('S')) this.saved = this.map.save();
|
||||
if (this.inputManager.keyPressed('L')) this.map.load(this.saved);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ export default abstract class Shape extends Phaser.GameObjects.Container {
|
|||
|
||||
this.attachedToken = token;
|
||||
this.attachedChangeEvent = () => this.setOrigin(new Vec2(token.x + offset.x, token.y + offset.y));
|
||||
token.change.bind(this.attachedChangeEvent);
|
||||
token.on_render.bind(this.attachedChangeEvent);
|
||||
|
||||
this.attachedDestroyEvent = () => this.detachFromToken();
|
||||
token.on('destroy', this.attachedDestroyEvent);
|
||||
|
@ -78,7 +78,7 @@ export default abstract class Shape extends Phaser.GameObjects.Container {
|
|||
detachFromToken() {
|
||||
if (!this.attachedToken) return;
|
||||
this.attachedToken.setSelected(false);
|
||||
this.attachedToken.change.unbind(this.attachedChangeEvent!);
|
||||
this.attachedToken.on_render.unbind(this.attachedChangeEvent!);
|
||||
this.attachedChangeEvent = undefined;
|
||||
this.attachedToken.off('destroy', this.attachedDestroyEvent!);
|
||||
this.attachedDestroyEvent = undefined;
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
const CHUNK_SIZE = 100000;
|
||||
export function serialize(buf: ArrayBuffer): string {
|
||||
return String.fromCharCode.apply(null, new Uint16Array(buf) as any);
|
||||
let str = '';
|
||||
for (let i = 0; i < buf.byteLength / CHUNK_SIZE; i++) {
|
||||
const slice = new Uint16Array(buf.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE));
|
||||
str += String.fromCharCode.apply(undefined, new Uint16Array(slice) as any);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export function deserialize(str: string): ArrayBuffer {
|
||||
|
|
|
@ -53,8 +53,7 @@ export interface Map {
|
|||
name: string;
|
||||
identifier: string;
|
||||
|
||||
size: {x: number, y: number};
|
||||
layers: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export type AssetType = 'wall' | 'detail' | 'ground' | 'token';
|
||||
|
|
|
@ -257,29 +257,49 @@ export default class Database {
|
|||
let mapIdentifier = this.sanitizeName(map);
|
||||
if (mapIdentifier.length < 3) 'Map name must contain at least 3 alphanumeric characters.';
|
||||
|
||||
let campIdentifier = this.sanitizeName(campaign);
|
||||
let identifier = this.sanitizeName(campaign);
|
||||
|
||||
const collection = this.db!.collection('campaigns');
|
||||
|
||||
let exists = await collection.findOne({user: user, identifier: campIdentifier});
|
||||
let exists = await collection.findOne({ user, identifier });
|
||||
if (!exists) throw 'This campaign no longer exists.';
|
||||
let mapExists = await collection.findOne({
|
||||
user: user,
|
||||
identifier: campIdentifier,
|
||||
maps: {
|
||||
$elemMatch: {
|
||||
identifier: mapIdentifier
|
||||
}
|
||||
}
|
||||
});
|
||||
user, identifier, maps: { $elemMatch: { identifier: mapIdentifier }}});
|
||||
if (mapExists) throw 'A map of this name already exists.';
|
||||
|
||||
await collection.updateOne({user: user, identifier: campIdentifier}, {
|
||||
$push: { maps: { name: map, identifier: mapIdentifier, size: { x: 64, y: 64 }, layers: '' }}});
|
||||
console.log(mapIdentifier);
|
||||
|
||||
const stub = JSON.stringify({ format: '1.0.0', identifier: mapIdentifier, size: { x: 32, y: 32 }, tokens: [] });
|
||||
const data = stub.length + '|' + stub;
|
||||
|
||||
await collection.updateOne({ user, identifier }, { $push: { maps: { name: map, identifier: mapIdentifier, data }}});
|
||||
return mapIdentifier;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates a map data to the serialized string provided.
|
||||
* Throws if the campaign or map doesn't exist.
|
||||
*
|
||||
* @param {string} user - The user identifier.
|
||||
* @param {string} campaign - The campaign identifier.
|
||||
* @param {string} map - The map identifier.
|
||||
* @param {string} data - The new map data string.
|
||||
* ...wow, all strings :p
|
||||
*/
|
||||
|
||||
async setMap(user: string, identifier: string, map: string, data: string) {
|
||||
const maps: DB.Map[] | null = (await this.db!.collection('campaigns').findOne({ user, identifier })).maps;
|
||||
if (!maps) throw 'Campaign doesn\'t exist';
|
||||
|
||||
const mapObj: DB.Map | undefined = maps.filter(m => m.identifier === map)[0];
|
||||
if (!mapObj) throw 'Map doesn\'t exist.';
|
||||
mapObj.data = data;
|
||||
|
||||
await this.db!.collection('campaigns').updateOne({ user, identifier }, { $set: { maps } });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a map.
|
||||
* Throws if the map or the campaign doesn't exist.
|
||||
|
|
|
@ -101,6 +101,7 @@ export default class MapController {
|
|||
// socket.on('map_load', this.mapLoad.bind(this, user));
|
||||
|
||||
socket.on('action', this.onAction.bind(this, socket, room));
|
||||
socket.on('serialize', this.onSerialize.bind(this, user, campaign));
|
||||
// socket.on('get_campaign_assets', this.onGetCampaignAssets.bind(this, user));
|
||||
}
|
||||
|
||||
|
@ -108,6 +109,10 @@ export default class MapController {
|
|||
socket.in(room).emit('action', event);
|
||||
}
|
||||
|
||||
private onSerialize(user: string, campaign: string, map: string, data: string) {
|
||||
this.db.setMap(user, campaign, map, data);
|
||||
}
|
||||
|
||||
// private async mapLoad(user: string, identifier: { campaign: string, map: string }, res: (data: string) => void) {
|
||||
// if (typeof res !== 'function' || typeof identifier !== 'object' ||
|
||||
// typeof identifier.map !== 'string' || typeof identifier.campaign !== 'string') return;
|
||||
|
|
Loading…
Reference in New Issue