Separate render props and meta props of tokens, fix IO not closing on soft redirect.
This commit is contained in:
parent
504250dee3
commit
c87e9214a1
@ -7,6 +7,7 @@ import './Editor.sass';
|
|||||||
interface Props {
|
interface Props {
|
||||||
user: string;
|
user: string;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
|
mapIdentifier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pad(n: number) {
|
function pad(n: number) {
|
||||||
@ -14,7 +15,7 @@ function pad(n: number) {
|
|||||||
return '' + n;
|
return '' + n;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Editor({ user, identifier }: Props) {
|
export default function Editor({ user, identifier, mapIdentifier }: Props) {
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
const editorRef = useRef<Phaser.Game | null>(null);
|
const editorRef = useRef<Phaser.Game | null>(null);
|
||||||
const [ loadPercent, setLoadPercent ] = useState<number | undefined>(0);
|
const [ loadPercent, setLoadPercent ] = useState<number | undefined>(0);
|
||||||
@ -30,10 +31,11 @@ export default function Editor({ user, identifier }: Props) {
|
|||||||
setLoadPercent(0.25);
|
setLoadPercent(0.25);
|
||||||
if (ignore || !rootRef.current) return;
|
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 resizeCallback = () => {
|
||||||
const { width, height } = rootRef.current.getBoundingClientRect();
|
const { width, height } = rootRef.current.getBoundingClientRect();
|
||||||
|
console.log(rootRef.current);
|
||||||
editorRef.current!.scale.resize(width, height);
|
editorRef.current!.scale.resize(width, height);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import qs from 'query-string';
|
||||||
import * as Preact from 'preact';
|
import * as Preact from 'preact';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import Editor from '../Editor';
|
import Editor from '../Editor';
|
||||||
|
|
||||||
@ -7,10 +8,10 @@ import './Editor.sass';
|
|||||||
|
|
||||||
export default function EditorPage() {
|
export default function EditorPage() {
|
||||||
const { user, campaign } = useParams<{ user: string; campaign: string }>();
|
const { user, campaign } = useParams<{ user: string; campaign: string }>();
|
||||||
|
const map = qs.parse(useLocation().search).map as string | undefined;
|
||||||
return (
|
return (
|
||||||
<div class='EditorPage'>
|
<div class='EditorPage'>
|
||||||
<Editor user={user} identifier={campaign} />
|
<Editor user={user} identifier={campaign} mapIdentifier={map} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import Button from '../Button';
|
|||||||
export default function NewMapForm() {
|
export default function NewMapForm() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [ ,, mergeData ] = useAppData();
|
const [ ,, mergeData ] = useAppData();
|
||||||
const { campaign } = useParams<{ campaign: string }>();
|
const { user, campaign } = useParams<{ user: string; campaign: string }>();
|
||||||
|
|
||||||
const [ queryState, setQueryState ] = useState<'idle' | 'querying'>('idle');
|
const [ queryState, setQueryState ] = useState<'idle' | 'querying'>('idle');
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ export default function NewMapForm() {
|
|||||||
else {
|
else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
await mergeData(data);
|
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 { Socket } from 'socket.io-client';
|
||||||
import * as DB from '../../../common/DBStructs';
|
import * as DB from '../../../common/DBStructs';
|
||||||
|
|
||||||
export interface ExternalData {
|
export default interface EditorData {
|
||||||
identifier: string;
|
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
}
|
|
||||||
|
|
||||||
export default interface EditorData extends ExternalData {
|
|
||||||
campaign: DB.Campaign;
|
campaign: DB.Campaign;
|
||||||
assets: Asset[];
|
assets: Asset[];
|
||||||
map: DB.Map;
|
map?: string;
|
||||||
|
|
||||||
display: 'edit' | 'view';
|
display: 'edit' | 'view';
|
||||||
onProgress: (progress: number | undefined) => void;
|
onProgress: (progress: number | undefined) => void;
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import { io } from 'socket.io-client';
|
|
||||||
|
|
||||||
import * as Scene from './scene/Scenes';
|
import * as Scene from './scene/Scenes';
|
||||||
|
|
||||||
export default function create(root: HTMLElement, onProgress: (progress: number) => void, user: string, identifier: string) {
|
export default function create(root: HTMLElement, onProgress: (progress: number) => void,
|
||||||
|
user: string, identifier: string, mapIdentifier?: string) {
|
||||||
|
|
||||||
const bounds = root.getBoundingClientRect();
|
const bounds = root.getBoundingClientRect();
|
||||||
|
|
||||||
const socket = io();
|
|
||||||
|
|
||||||
const game = new Phaser.Game({
|
const game = new Phaser.Game({
|
||||||
disableContextMenu: true,
|
disableContextMenu: true,
|
||||||
render: { antialias: false },
|
render: { antialias: false },
|
||||||
@ -20,6 +19,6 @@ export default function create(root: HTMLElement, onProgress: (progress: number)
|
|||||||
scene: Scene.list
|
scene: Scene.list
|
||||||
});
|
});
|
||||||
|
|
||||||
game.scene.start('InitScene', { user, onProgress, identifier, socket });
|
game.scene.start('InitScene', { user, onProgress, identifier, mapIdentifier });
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Vec2 } from '../util/Vec';
|
import { Vec2 } from '../util/Vec';
|
||||||
import { Layer } from '../util/Layer';
|
import { Layer } from '../util/Layer';
|
||||||
import { TokenData } from '../map/token/Token';
|
import { TokenRenderData, TokenData } from '../map/token/Token';
|
||||||
|
|
||||||
export interface Tile {
|
export interface Tile {
|
||||||
type: 'tile';
|
type: 'tile';
|
||||||
@ -21,7 +21,7 @@ export interface PlaceToken {
|
|||||||
|
|
||||||
export interface ModifyToken {
|
export interface ModifyToken {
|
||||||
type: 'modify_token';
|
type: 'modify_token';
|
||||||
tokens: { pre: TokenData[]; post: TokenData[] };
|
tokens: { pre: TokenRenderData[]; post: TokenRenderData[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteToken {
|
export interface DeleteToken {
|
||||||
|
@ -2,23 +2,24 @@ import * as Phaser from 'phaser';
|
|||||||
import IO from 'socket.io-client';
|
import IO from 'socket.io-client';
|
||||||
|
|
||||||
import Map from '../map/Map';
|
import Map from '../map/Map';
|
||||||
// import Token from '../map/token/Token';
|
|
||||||
import type { Action } from './Action';
|
import type { Action } from './Action';
|
||||||
import ActionEvent from './ActionEvent';
|
import ActionEvent from './ActionEvent';
|
||||||
import EventHandler from '../EventHandler';
|
import EventHandler from '../EventHandler';
|
||||||
import InputManager from '../InputManager';
|
import InputManager from '../InputManager';
|
||||||
|
|
||||||
|
const SAVE_INTERVAL = 5 * 1000;
|
||||||
|
|
||||||
export default class ActionManager {
|
export default class ActionManager {
|
||||||
|
readonly event = new EventHandler<ActionEvent>();
|
||||||
|
|
||||||
private map: Map = null as any;
|
private map: Map = null as any;
|
||||||
private socket: IO.Socket = 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 head: number = -1;
|
||||||
|
private history: Action[] = [];
|
||||||
private evtHandler = new EventHandler<ActionEvent>();
|
|
||||||
|
|
||||||
private historyHeldTime: number = 0;
|
private historyHeldTime: number = 0;
|
||||||
|
private editTime: number | false = false;
|
||||||
|
|
||||||
init(_scene: Phaser.Scene, map: Map, socket: IO.Socket) {
|
init(_scene: Phaser.Scene, map: Map, socket: IO.Socket) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
@ -47,14 +48,20 @@ export default class ActionManager {
|
|||||||
this.historyHeldTime++;
|
this.historyHeldTime++;
|
||||||
}
|
}
|
||||||
else this.historyHeldTime = 0;
|
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 {
|
push(item: Action): void {
|
||||||
this.history.splice(this.head + 1, this.history.length - this.head, item);
|
this.history.splice(this.head + 1, this.history.length - this.head, item);
|
||||||
this.head = this.history.length - 1;
|
this.head = this.history.length - 1;
|
||||||
|
if (!this.editTime) this.editTime = Date.now();
|
||||||
|
|
||||||
this.socket.emit('action', item);
|
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 {
|
apply(item: Action): void {
|
||||||
@ -81,19 +88,17 @@ export default class ActionManager {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'delete_token':
|
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;
|
break;
|
||||||
|
|
||||||
case 'modify_token':
|
case 'modify_token':
|
||||||
for (let i = 0; i < item.tokens.pre.length; i++) {
|
for (let i = 0; i < item.tokens.pre.length; i++)
|
||||||
const token = this.map.tokens.getToken(item.tokens.pre[i].uuid);
|
this.map.tokens.setRender(item.tokens.pre[i].uuid, item.tokens.pre[i]);
|
||||||
if (token) token.setToken(item.tokens.pre[i]);
|
|
||||||
else this.map.tokens.createToken(item.tokens.pre[i]);
|
|
||||||
}
|
|
||||||
break;
|
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() {
|
next() {
|
||||||
@ -111,7 +116,8 @@ export default class ActionManager {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'place_token':
|
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;
|
break;
|
||||||
|
|
||||||
case 'delete_token':
|
case 'delete_token':
|
||||||
@ -119,15 +125,12 @@ export default class ActionManager {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'modify_token':
|
case 'modify_token':
|
||||||
for (let i = 0; i < item.tokens.post.length; i++) {
|
for (let i = 0; i < item.tokens.post.length; i++)
|
||||||
const token = this.map.tokens.getToken(item.tokens.post[i].uuid);
|
this.map.tokens.setRender(item.tokens.post[i].uuid, item.tokens.post[i]);
|
||||||
if (token) token.setToken(item.tokens.post[i]);
|
|
||||||
else this.map.tokens.createToken(item.tokens.post[i]);
|
|
||||||
}
|
|
||||||
break;
|
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 {
|
hasPrev(): boolean {
|
||||||
@ -137,12 +140,4 @@ export default class ActionManager {
|
|||||||
hasNext(): boolean {
|
hasNext(): boolean {
|
||||||
return this.head < this.history.length - 1;
|
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 tileSidebar: TileSidebar | null = null;
|
||||||
private tokenSidebar: TokenSidebar | null = null;
|
private tokenSidebar: TokenSidebar | null = null;
|
||||||
|
|
||||||
init(scene: Phaser.Scene, display: 'edit' | 'view', input: InputManager,
|
init(scene: Phaser.Scene, input: InputManager, mode: ModeMananger,
|
||||||
mode: ModeMananger, actions: ActionManager, map: Map, assets: Asset[]) {
|
actions: ActionManager, map: Map, assets: Asset[]) {
|
||||||
|
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
// this.actions = action;
|
|
||||||
this.inputManager = input;
|
this.inputManager = input;
|
||||||
|
|
||||||
this.camera = this.scene.cameras.add(0, 0, undefined, undefined, undefined, 'ui_camera');
|
this.camera = this.scene.cameras.add(0, 0, undefined, undefined, undefined, 'ui_camera');
|
||||||
@ -55,24 +54,17 @@ export default class InterfaceRoot {
|
|||||||
this.root.setName('root');
|
this.root.setName('root');
|
||||||
this.leftRoot = this.scene.add.container(0, 0);
|
this.leftRoot = this.scene.add.container(0, 0);
|
||||||
this.root.add(this.leftRoot);
|
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 SidebarToggler(scene, 49, 1000, input, this));
|
|
||||||
|
|
||||||
// this.leftRoot.add(new ModeSwitcher(scene, 82, 1, input, mode));
|
this.tokenSidebar = new TokenSidebar(scene, 0, 0, assets, input, mode);
|
||||||
// this.leftRoot.add(new HistoryManipulator(scene, 124, 1, history, input));
|
this.leftRoot.add(this.tokenSidebar);
|
||||||
|
|
||||||
this.tokenSidebar = new TokenSidebar(scene, 0, 0, assets, input, mode);
|
this.tileSidebar = new TileSidebar(scene, 0, 0, assets, input, mode, map);
|
||||||
this.leftRoot.add(this.tokenSidebar);
|
this.leftRoot.add(this.tileSidebar);
|
||||||
|
|
||||||
this.tileSidebar = new TileSidebar(scene, 0, 0, assets, input, mode, map);
|
this.root.add(new TokenCards(scene, { map, assets }));
|
||||||
this.leftRoot.add(this.tileSidebar);
|
this.root.add(new LayerManager(scene, { map }));
|
||||||
|
|
||||||
this.root.add(new TokenCards(scene, { map, assets }));
|
|
||||||
this.root.add(new LayerManager(scene, { map }));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.root.add(new Toolbar(scene, { mode, actions }));
|
this.root.add(new Toolbar(scene, { mode, actions }));
|
||||||
}
|
}
|
||||||
|
135
app/src/editor/interface/components/TokenCard.sass
Normal file
135
app/src/editor/interface/components/TokenCard.sass
Normal file
@ -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
|
67
app/src/editor/interface/components/TokenCard.tsx
Normal file
67
app/src/editor/interface/components/TokenCard.tsx
Normal file
@ -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 *
|
@use '../../../style/def' as *
|
||||||
|
|
||||||
.TokenCards
|
.TokenCards
|
||||||
@ -24,211 +22,3 @@
|
|||||||
|
|
||||||
.TokenCard-Thumbnail
|
.TokenCard-Thumbnail
|
||||||
margin-top: 1px * 3
|
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 * as Preact from 'preact';
|
||||||
import { bind } from './PreactComponent';
|
import { bind } from './PreactComponent';
|
||||||
import { useState, useEffect, useLayoutEffect, useRef } from 'preact/hooks';
|
import { useState, useEffect, useCallback } from 'preact/hooks';
|
||||||
|
|
||||||
import './TokenCards.sass';
|
import './TokenCards.sass';
|
||||||
|
|
||||||
import Map from '../../map/Map';
|
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 { 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 {
|
interface Props {
|
||||||
map: Map;
|
map: Map;
|
||||||
@ -146,26 +17,40 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default bind<Props>(function TokenCards({ map, assets }: 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(() => {
|
useEffect(() => {
|
||||||
const eventCb = () => {
|
const eventCb = () => {
|
||||||
setCards(JSON.parse(JSON.stringify(map.tokens.getTokenData())));
|
setCards(JSON.parse(JSON.stringify(map.tokens.getAllMeta())));
|
||||||
};
|
};
|
||||||
|
|
||||||
map.tokens.bind(eventCb);
|
map.tokens.event.bind(eventCb);
|
||||||
return () => map.tokens.unbind(eventCb);
|
return () => map.tokens.event.unbind(eventCb);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSetProps = (ind: number, data: Partial<TokenData>) => {
|
const handleSetProps = (ind: number, data: Partial<TokenMetaData>) => {
|
||||||
map.tokens.setToken({
|
map.tokens.setMeta(cards[ind].uuid, data);
|
||||||
uuid: 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(() => {
|
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) &&
|
if (evt.key[0] !== 'F' || evt.key.length !== 2 || !(evt.key.charCodeAt(1) >= '1'.charCodeAt(0) &&
|
||||||
evt.key.charCodeAt(1) <= '8'.charCodeAt(0))) return;
|
evt.key.charCodeAt(1) <= '8'.charCodeAt(0))) return;
|
||||||
|
|
||||||
@ -173,16 +58,21 @@ export default bind<Props>(function TokenCards({ map, assets }: Props) {
|
|||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
|
|
||||||
const ind = Number.parseInt(evt.key.substr(1), 10);
|
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);
|
window.addEventListener('keydown', fnKeyCallback);
|
||||||
return () => window.removeEventListener('keydown', fnCallback);
|
return () => window.removeEventListener('keydown', fnKeyCallback);
|
||||||
}, [ cards ]);
|
}, [ cards ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class='TokenCards'>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,7 @@ import TokenMode from '../../mode/TokenMode';
|
|||||||
import ModeManager from '../../mode/ModeManager';
|
import ModeManager from '../../mode/ModeManager';
|
||||||
import type InputManager from '../../InputManager';
|
import type InputManager from '../../InputManager';
|
||||||
|
|
||||||
|
import { Vec2 } from '../../util/Vec';
|
||||||
import { Asset } from '../../util/Asset';
|
import { Asset } from '../../util/Asset';
|
||||||
|
|
||||||
export default class TokenSidebar extends Sidebar {
|
export default class TokenSidebar extends Sidebar {
|
||||||
@ -45,15 +46,15 @@ export default class TokenSidebar extends Sidebar {
|
|||||||
|
|
||||||
this.spinTimer++;
|
this.spinTimer++;
|
||||||
if (this.spinTimer > 20) {
|
if (this.spinTimer > 20) {
|
||||||
let index = hoveredToken.getFrame() + 1;
|
let index = hoveredToken.getFrameIndex() + 1;
|
||||||
index %= hoveredToken.getFrameCount();
|
index %= hoveredToken.getFrameCount();
|
||||||
hoveredToken.setToken({ appearance: { sprite: hoveredToken.getToken().appearance.sprite, index }});
|
hoveredToken.setFrame(index);
|
||||||
this.spinTimer = 0;
|
this.spinTimer = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
elemUnhover(): void {
|
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 {
|
elemClick(x: number, y: number): void {
|
||||||
@ -70,12 +71,11 @@ export default class TokenSidebar extends Sidebar {
|
|||||||
|
|
||||||
if (x === 0) this.backgrounds[y].setFrame(0);
|
if (x === 0) this.backgrounds[y].setFrame(0);
|
||||||
|
|
||||||
let token = new Token(this.scene, { appearance: { sprite, index: 0 }});
|
let token = new Token(this.scene, {}, new Vec2(4 + x * 21, 4 + y * 21), sprite);
|
||||||
token.setPosition(4 + x * 21, 4 + y * 21);
|
token.setScale(1);
|
||||||
token.setScale(16);
|
|
||||||
|
|
||||||
this.sprites.push(token);
|
this.sprites.push(token);
|
||||||
this.list.push(token);
|
this.add(token);
|
||||||
|
|
||||||
this.bringToTop(this.activeSpriteCursor);
|
this.bringToTop(this.activeSpriteCursor);
|
||||||
this.bringToTop(this.hoverSpriteCursor);
|
this.bringToTop(this.hoverSpriteCursor);
|
||||||
|
79
app/src/editor/interface/components/TokenSlider.sass
Normal file
79
app/src/editor/interface/components/TokenSlider.sass
Normal file
@ -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
|
82
app/src/editor/interface/components/TokenSlider.tsx
Normal file
82
app/src/editor/interface/components/TokenSlider.tsx
Normal file
@ -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());
|
setHasNext(props.actions.hasNext());
|
||||||
};
|
};
|
||||||
|
|
||||||
props.actions.bind(actionCb);
|
props.actions.event.bind(actionCb);
|
||||||
return () => props.actions.unbind(actionCb);
|
return () => props.actions.event.unbind(actionCb);
|
||||||
}, [ props.actions ]);
|
}, [ props.actions ]);
|
||||||
|
|
||||||
const [ mode, setMode ] = useState<string>(ArchitectModeKey);
|
const [ mode, setMode ] = useState<string>(ArchitectModeKey);
|
||||||
|
@ -15,7 +15,8 @@ import { Asset } from '../util/Asset';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export default class Map {
|
export default class Map {
|
||||||
size: Vec2 = new Vec2(0, 0);
|
identifier: string = '';
|
||||||
|
size: Vec2 = new Vec2(2, 2);
|
||||||
tileStore: TileStore = new TileStore();
|
tileStore: TileStore = new TileStore();
|
||||||
|
|
||||||
tokens: TokenManager = new TokenManager();
|
tokens: TokenManager = new TokenManager();
|
||||||
@ -26,9 +27,8 @@ export default class Map {
|
|||||||
private scene: Phaser.Scene = undefined as any;
|
private scene: Phaser.Scene = undefined as any;
|
||||||
private chunks: MapChunk[][][] = [];
|
private chunks: MapChunk[][][] = [];
|
||||||
|
|
||||||
init(scene: Phaser.Scene, size: Vec2, assets: Asset[]) {
|
init(scene: Phaser.Scene, assets: Asset[]) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.size = size;
|
|
||||||
|
|
||||||
this.tokens.init(scene);
|
this.tokens.init(scene);
|
||||||
this.tileStore.init(scene.textures, assets);
|
this.tileStore.init(scene.textures, assets);
|
||||||
@ -120,7 +120,7 @@ export default class Map {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
save(): string {
|
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) {
|
load(mapData: string) {
|
||||||
const data = MapSaver.load(mapData);
|
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;
|
this.layers = data.layers;
|
||||||
if (this.layers.length === 0) this.layers.push(new MapLayer(0, this.size));
|
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 MapLayer, { LAYER_SERIALIZATION_ORDER } from './MapLayer';
|
||||||
|
|
||||||
import { Vec2 } from '../util/Vec';
|
import { Vec2 } from '../util/Vec';
|
||||||
import * as Buffer from '../util/Buffer';
|
import * as Buffer from '../util/Buffer';
|
||||||
|
|
||||||
/** Data pretaining to a deserialized map. */
|
/** JSON-serializable map data */
|
||||||
export interface DeserializedMap {
|
export interface SerializedMap {
|
||||||
size: Vec2;
|
format: string;
|
||||||
|
identifier: string;
|
||||||
|
size: { x: number; y: number };
|
||||||
|
tokens: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deserialized map data, including layer array. */
|
||||||
|
export interface DeserializedMap extends SerializedMap {
|
||||||
layers: MapLayer[];
|
layers: MapLayer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,16 +26,19 @@ export interface DeserializedMap {
|
|||||||
* @returns {string} - a serialized map string.
|
* @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 = '';
|
let mapData = '';
|
||||||
|
|
||||||
const mapMeta = {
|
const mapJson: SerializedMap = {
|
||||||
format: '1.0.0',
|
format: '1.0.0',
|
||||||
size: size
|
|
||||||
|
size,
|
||||||
|
identifier,
|
||||||
|
tokens: tokens.serializeAllTokens()
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapMetaStr = JSON.stringify(mapMeta);
|
const mapJsonStr = JSON.stringify(mapJson);
|
||||||
mapData += mapMetaStr.length + '|' + mapMetaStr;
|
mapData += mapJsonStr.length + '|' + mapJsonStr;
|
||||||
|
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
let layerStr = '';
|
let layerStr = '';
|
||||||
@ -71,17 +82,16 @@ export function load(mapData: string): DeserializedMap {
|
|||||||
const numEnd = mapData.indexOf('|');
|
const numEnd = mapData.indexOf('|');
|
||||||
const num = Number.parseInt(mapData.substr(0, numEnd), 10);
|
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);
|
mapData = mapData.substr(numEnd + num + 1);
|
||||||
|
|
||||||
const data: DeserializedMap = { ...mapMeta, layers: [] };
|
|
||||||
|
|
||||||
let layerInd = 0;
|
let layerInd = 0;
|
||||||
while (mapData.length) {
|
while (mapData.length) {
|
||||||
const numEnd = mapData.indexOf('|');
|
const numEnd = mapData.indexOf('|');
|
||||||
const num = Number.parseInt(mapData.substr(0, numEnd), 10);
|
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));
|
layer.load(mapData.slice(numEnd + 1, numEnd + 1 + num));
|
||||||
data.layers.push(layer);
|
data.layers.push(layer);
|
||||||
|
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import EventHandler from '../../EventHandler';
|
import EventHandler from '../../EventHandler';
|
||||||
|
|
||||||
|
import { Vec2 } from '../../util/Vec';
|
||||||
import { generateId } from '../../util/Helpers';
|
import { generateId } from '../../util/Helpers';
|
||||||
|
|
||||||
/** Data pretaining to a token slider. */
|
|
||||||
|
/**
|
||||||
|
* Represents a slider bar for a token.
|
||||||
|
*/
|
||||||
|
|
||||||
export interface TokenSliderData {
|
export interface TokenSliderData {
|
||||||
name: string;
|
name: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
@ -13,114 +18,192 @@ export interface TokenSliderData {
|
|||||||
current: number;
|
current: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Data pretaining to a token. */
|
|
||||||
export interface TokenData {
|
/**
|
||||||
|
* The meta information (name, sliders, note) for a token.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TokenMetaData {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
||||||
pos: { x: number; y: number };
|
|
||||||
appearance: { sprite: string; index: number };
|
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
note: string;
|
note: string;
|
||||||
|
|
||||||
sliders: TokenSliderData[];
|
sliders: TokenSliderData[];
|
||||||
|
|
||||||
pinned: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Default token data, for raw assignment. */
|
|
||||||
const DEFAULT_TOKEN_DATA: Omit<TokenData, 'uuid'> = {
|
/**
|
||||||
pos: { x: 0, y: 0 },
|
* The render information for a token, such as position and sprite.
|
||||||
appearance: { sprite: '', index: 0 },
|
*/
|
||||||
|
|
||||||
|
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: '',
|
name: '',
|
||||||
|
|
||||||
note: '',
|
note: '',
|
||||||
sliders: [],
|
sliders: []
|
||||||
|
|
||||||
pinned: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 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,
|
* Represents a token in the world, its properties are determined by its TokenData,
|
||||||
* which can be set, updated, and retrieved through public methods.
|
* which can be set, updated, and retrieved through public methods.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class Token extends Phaser.GameObjects.Container {
|
export default class Token extends Phaser.GameObjects.Sprite {
|
||||||
readonly change = new EventHandler<TokenModifyEvent>();
|
readonly on_meta = new EventHandler<TokenMetaEvent>();
|
||||||
|
readonly on_render = new EventHandler<TokenRenderEvent>();
|
||||||
private sprite: Phaser.GameObjects.Sprite;
|
|
||||||
private shadow: Phaser.GameObjects.Sprite;
|
private shadow: Phaser.GameObjects.Sprite;
|
||||||
|
|
||||||
private tokenData: TokenData;
|
private meta: TokenMetaData;
|
||||||
|
|
||||||
private hovered: boolean = false;
|
private hovered: boolean = false;
|
||||||
private selected: boolean = false;
|
private selected: boolean = false;
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, tokenData?: Partial<TokenData>) {
|
constructor(scene: Phaser.Scene, tokenData?: Partial<TokenMetaData>, pos?: Vec2, sprite?: string, index?: number) {
|
||||||
super(scene, 0, 0);
|
super(scene, 0, 0, sprite ?? '', index);
|
||||||
|
this.scene.add.existing(this);
|
||||||
|
this.setDepth(500);
|
||||||
|
|
||||||
this.tokenData = { ...DEFAULT_TOKEN_DATA, uuid: '' };
|
this.meta = { ...DEFAULT_TOKEN_META, uuid: '' };
|
||||||
this.setToken({
|
this.setMeta({
|
||||||
...DEFAULT_TOKEN_DATA,
|
...DEFAULT_TOKEN_META,
|
||||||
...tokenData ?? {},
|
...tokenData ?? {},
|
||||||
uuid: tokenData?.uuid ?? generateId(32)
|
uuid: tokenData?.uuid ?? generateId(32)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.shadow = new Phaser.GameObjects.Sprite(this.scene, -1 / 16, -1 / 16, '');
|
this.shadow = this.scene.add.sprite(this.x, this.y, sprite ?? '', index);
|
||||||
this.shadow.setOrigin(0, 0);
|
this.shadow.setOrigin(1 / 18, 1 / 18);
|
||||||
this.shadow.setScale(18 / 16 / this.shadow.width, 18 / 16 / 4 / this.shadow.height);
|
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.setAlpha(0.1, 0.1, 0.3, 0.3);
|
||||||
this.shadow.setTint(0x000000);
|
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.on('removefromscene', () => this.shadow.destroy());
|
||||||
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.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>) {
|
serialize(): TokenData {
|
||||||
if (!this.tokenData) return;
|
const data = {
|
||||||
|
uuid: this.getUUID(),
|
||||||
|
meta: this.getMeta(),
|
||||||
|
render: this.getRender()
|
||||||
|
};
|
||||||
|
|
||||||
const preSer = JSON.stringify(this.tokenData);
|
delete (data.meta as any).uuid;
|
||||||
const postSer = JSON.stringify({ ...this.tokenData, ...data });
|
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;
|
if (preSer === postSer) return;
|
||||||
|
|
||||||
const pre = JSON.parse(preSer);
|
const pre = JSON.parse(preSer);
|
||||||
const post = JSON.parse(postSer);
|
const post = JSON.parse(postSer);
|
||||||
|
|
||||||
this.tokenData = post;
|
this.meta = post;
|
||||||
this.updateAppearance();
|
this.on_meta.dispatch({ token: this, pre, post });
|
||||||
|
|
||||||
this.change.dispatch({ token: this, pre, post });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): TokenData {
|
getRender(): TokenRenderData {
|
||||||
return JSON.parse(JSON.stringify(this.tokenData));
|
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 {
|
getUUID(): string {
|
||||||
return this.tokenData.uuid;
|
return this.meta.uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFrame(): number {
|
getFrameIndex(): number {
|
||||||
return this.tokenData.appearance.index;
|
return this.frame.name as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFrameCount(): number {
|
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) {
|
setSelected(selected: boolean) {
|
||||||
@ -134,35 +217,52 @@ export default class Token extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPosition(x?: number, y?: number): this {
|
setPosition(x?: number, y?: number): this {
|
||||||
if (!this.tokenData) return this;
|
if (this.x === x && this.y === y) return this;
|
||||||
this.setToken({ pos: { x: x ?? 0, y: y ?? x ?? 0 }});
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateAppearance() {
|
setTexture(key: string, index?: string | number): this {
|
||||||
// console.log(this.tokenData);
|
Phaser.GameObjects.Sprite.prototype.setTexture.call(this, key, index);
|
||||||
Phaser.GameObjects.Container.prototype.setPosition.call(this, this.tokenData.pos.x, this.tokenData.pos.y);
|
this.setScale(18 / 16 / this.width, 18 / 16 / this.height);
|
||||||
if (!this.sprite || !this.shadow) return;
|
if (!this.shadow) return this;
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
|
this.shadow.setTexture(key, index);
|
||||||
this.shadow.setScale(18 / 16 / this.shadow.width, 18 / 16 / 4 / this.shadow.height);
|
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() {
|
private updateShader() {
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
this.sprite.setPipeline('outline');
|
this.setPipeline('outline');
|
||||||
this.sprite.pipeline.set1f('tex_size', this.sprite.texture.source[0].width);
|
this.pipeline.set1f('tex_size', this.texture.source[0].width);
|
||||||
}
|
}
|
||||||
else if (this.hovered) {
|
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 * as Phaser from 'phaser';
|
||||||
|
|
||||||
import Token, { TokenData, TokenModifyEvent } from './Token';
|
|
||||||
import EventHandler from '../../EventHandler';
|
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 = {
|
export type TokenEvent = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
} & ((
|
} & ((
|
||||||
{ type: 'modify' } & TokenModifyEvent
|
{ type: 'modify' } & TokenRenderEvent
|
||||||
) | {
|
) | {
|
||||||
type: 'create';
|
type: 'create';
|
||||||
token: Token;
|
token: Token;
|
||||||
@ -15,12 +18,11 @@ export type TokenEvent = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default class TokenManager {
|
export default class TokenManager {
|
||||||
|
readonly event = new EventHandler<TokenEvent>();
|
||||||
|
|
||||||
private scene: Phaser.Scene = null as any;
|
private scene: Phaser.Scene = null as any;
|
||||||
|
|
||||||
private tokens: Token[] = [];
|
private tokens: Token[] = [];
|
||||||
|
|
||||||
private evtHandler = new EventHandler<TokenEvent>();
|
|
||||||
|
|
||||||
init(scene: Phaser.Scene) {
|
init(scene: Phaser.Scene) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
}
|
}
|
||||||
@ -28,6 +30,9 @@ export default class TokenManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a token based on it's UUID.
|
* 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 {
|
getToken(token: string): Token | undefined {
|
||||||
@ -37,31 +42,32 @@ export default class TokenManager {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
getAllTokens(): Token[] {
|
||||||
const token = this.tokens.filter(t => t.getUUID())[0];
|
return this.tokens;
|
||||||
if (!token) return undefined;
|
|
||||||
token.setToken(data);
|
|
||||||
return token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new token with the provided token
|
* Creates a new token with the provided token data.
|
||||||
* data, and adds it to the token list.
|
*
|
||||||
|
* @param {string} data - The TokenData to create the token from.
|
||||||
|
* @returns the new token instance.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
createToken(data: Partial<TokenData>): Token {
|
createToken(pos: Vec2, meta: Partial<TokenMetaData>, sprite?: string, index?: number): Token {
|
||||||
const token = new Token(this.scene, data);
|
const token = new Token(this.scene, meta, pos, sprite, index);
|
||||||
token.change.bind(this.onChange);
|
token.on_render.bind(this.onChange);
|
||||||
this.scene.add.existing(token);
|
this.scene.add.existing(token);
|
||||||
this.tokens.push(token);
|
this.tokens.push(token);
|
||||||
|
|
||||||
this.evtHandler.dispatch({
|
this.event.dispatch({
|
||||||
type: 'create',
|
type: 'create',
|
||||||
uuid: token.getUUID(),
|
uuid: token.getUUID(),
|
||||||
token: token
|
token: token
|
||||||
@ -73,18 +79,21 @@ export default class TokenManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a token based on it's UUID or direct object.
|
* 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 {
|
deleteToken(token: string | Token): TokenData | undefined {
|
||||||
for (let i = 0; i < this.tokens.length; i++) {
|
for (let i = 0; i < this.tokens.length; i++) {
|
||||||
if (typeof token === 'string' ? this.tokens[i].getUUID() === token : this.tokens[i] === token) {
|
if (typeof token === 'string' ? this.tokens[i].getUUID() === token : this.tokens[i] === token) {
|
||||||
token = this.tokens[i];
|
token = this.tokens[i];
|
||||||
const data = token.getToken();
|
const data = token.serialize();
|
||||||
|
|
||||||
this.tokens[i].destroy();
|
token.destroy();
|
||||||
this.tokens.splice(i, 1);
|
this.tokens.splice(i, 1);
|
||||||
|
|
||||||
this.evtHandler.dispatch({
|
this.event.dispatch({
|
||||||
type: 'destroy',
|
type: 'destroy',
|
||||||
uuid: token.getUUID()
|
uuid: token.getUUID()
|
||||||
});
|
});
|
||||||
@ -95,50 +104,80 @@ export default class TokenManager {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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[] {
|
resetTokens(data?: TokenData[]): Token[] {
|
||||||
return this.tokens;
|
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[] {
|
serializeAllTokens(): TokenData[] {
|
||||||
return this.tokens.map(t => t.getToken());
|
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) {
|
getAllMeta(): TokenMetaData[] {
|
||||||
this.evtHandler.bind(cb);
|
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) {
|
setMeta(uuid: string, data: Partial<TokenMetaData>): Token | undefined {
|
||||||
this.evtHandler.unbind(cb);
|
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) => {
|
setRender(uuid: string, data: Partial<TokenRenderData>): Token | undefined {
|
||||||
this.evtHandler.dispatch({
|
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',
|
type: 'modify',
|
||||||
uuid: event.token.getUUID(),
|
uuid: event.token.getUUID(),
|
||||||
...event
|
...event
|
||||||
|
@ -172,7 +172,7 @@ export default class DrawMode extends Mode {
|
|||||||
const tilePos = cursorPos.floor();
|
const tilePos = cursorPos.floor();
|
||||||
|
|
||||||
let found: Token | undefined = undefined;
|
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;
|
const isFound = t.x === tilePos.x && t.y === tilePos.y;
|
||||||
t.setSelected(!!(isFound && highlight));
|
t.setSelected(!!(isFound && highlight));
|
||||||
if (isFound) found = t;
|
if (isFound) found = t;
|
||||||
|
@ -30,7 +30,7 @@ export default class ModeManager {
|
|||||||
|
|
||||||
private evtHandler = new EventHandler<ModeSwitchEvent>();
|
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 = {
|
this.modes = {
|
||||||
[ArchitectModeKey]: new ArchitectMode(scene, map, actions, assets),
|
[ArchitectModeKey]: new ArchitectMode(scene, map, actions, assets),
|
||||||
[TokenModeKey]: new TokenMode(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 Map from '../map/Map';
|
||||||
import InputManager from '../InputManager';
|
import InputManager from '../InputManager';
|
||||||
import ActionManager from '../action/ActionManager';
|
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 { Vec2 } from '../util/Vec';
|
||||||
import { Asset } from '../util/Asset';
|
import { Asset } from '../util/Asset';
|
||||||
@ -24,7 +24,7 @@ export default class TokenMode extends Mode {
|
|||||||
private selected: Set<Token> = new Set();
|
private selected: Set<Token> = new Set();
|
||||||
|
|
||||||
private startTilePos: Vec2 = new Vec2();
|
private startTilePos: Vec2 = new Vec2();
|
||||||
private preMove: TokenData[] | null = null;
|
private preMove: TokenRenderData[] | null = null;
|
||||||
private clickTestState: null | false | true = null;
|
private clickTestState: null | false | true = null;
|
||||||
|
|
||||||
private cursor: Phaser.GameObjects.Sprite;
|
private cursor: Phaser.GameObjects.Sprite;
|
||||||
@ -50,10 +50,10 @@ export default class TokenMode extends Mode {
|
|||||||
input.bindScrollEvent((delta: number) => {
|
input.bindScrollEvent((delta: number) => {
|
||||||
if (this.editMode !== 'move') return false;
|
if (this.editMode !== 'move') return false;
|
||||||
this.selected.forEach(token => {
|
this.selected.forEach(token => {
|
||||||
let index = token.getFrame() + delta;
|
let index = token.getFrameIndex() + delta;
|
||||||
if (index < 0) index += token.getFrameCount();
|
if (index < 0) index += token.getFrameCount();
|
||||||
index %= token.getFrameCount();
|
index %= token.getFrameCount();
|
||||||
token.setToken({ appearance: { sprite: token.getToken().appearance.sprite, index }});
|
token.setFrame(index);
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@ -64,8 +64,7 @@ export default class TokenMode extends Mode {
|
|||||||
|
|
||||||
cursorPos = cursorPos.floor();
|
cursorPos = cursorPos.floor();
|
||||||
|
|
||||||
if (this.preview.getToken().appearance.sprite !== this.placeTokenType)
|
if (this.preview.texture.key !== this.placeTokenType) this.preview.setTexture(this.placeTokenType, 0);
|
||||||
this.preview.setToken({ appearance: { sprite: this.placeTokenType, index: 0 }});
|
|
||||||
|
|
||||||
switch (this.editMode) {
|
switch (this.editMode) {
|
||||||
default: break;
|
default: break;
|
||||||
@ -112,8 +111,8 @@ export default class TokenMode extends Mode {
|
|||||||
this.hovered?.setHovered(false);
|
this.hovered?.setHovered(false);
|
||||||
this.hovered = null;
|
this.hovered = null;
|
||||||
|
|
||||||
for (let i = this.map.tokens.getTokens().length - 1; i >= 0; i--) {
|
for (let i = this.map.tokens.getAllTokens().length - 1; i >= 0; i--) {
|
||||||
let token = this.map.tokens.getTokens()[i];
|
let token = this.map.tokens.getAllTokens()[i];
|
||||||
if (cursorPos.x === token.x && cursorPos.y === token.y) {
|
if (cursorPos.x === token.x && cursorPos.y === token.y) {
|
||||||
this.hovered = token;
|
this.hovered = token;
|
||||||
this.hovered.setHovered(true);
|
this.hovered.setHovered(true);
|
||||||
@ -179,7 +178,7 @@ export default class TokenMode extends Mode {
|
|||||||
this.selected = new Set();
|
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 (token.x >= a.x && token.y >= a.y && token.x <= b.x && token.y <= b.y) {
|
||||||
|
|
||||||
if (input.keyDown('CTRL')) {
|
if (input.keyDown('CTRL')) {
|
||||||
@ -215,7 +214,7 @@ export default class TokenMode extends Mode {
|
|||||||
private handleMove(cursorPos: Vec2, input: InputManager) {
|
private handleMove(cursorPos: Vec2, input: InputManager) {
|
||||||
this.cursor.setVisible(false);
|
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) {
|
if (!this.selected.size) {
|
||||||
this.editMode = 'place';
|
this.editMode = 'place';
|
||||||
@ -226,8 +225,8 @@ export default class TokenMode extends Mode {
|
|||||||
this.editMode = 'place';
|
this.editMode = 'place';
|
||||||
|
|
||||||
if (this.clickTestState) {
|
if (this.clickTestState) {
|
||||||
const post: TokenData[] = [];
|
const post: TokenRenderData[] = [];
|
||||||
for (let t of this.selected) post.push(t.getToken());
|
for (let t of this.selected) post.push(t.getRender());
|
||||||
this.actions.push({ type: 'modify_token', tokens: { pre: this.preMove!, post } });
|
this.actions.push({ type: 'modify_token', tokens: { pre: this.preMove!, post } });
|
||||||
this.preMove = null;
|
this.preMove = null;
|
||||||
}
|
}
|
||||||
@ -252,9 +251,8 @@ export default class TokenMode extends Mode {
|
|||||||
|
|
||||||
private placeToken(cursorPos: Vec2): Token {
|
private placeToken(cursorPos: Vec2): Token {
|
||||||
const asset = this.assets.filter(a => a.identifier === this.placeTokenType)[0];
|
const asset = this.assets.filter(a => a.identifier === this.placeTokenType)[0];
|
||||||
const token = this.map.tokens.createToken({ name: asset.name, pos: cursorPos,
|
const token = this.map.tokens.createToken(cursorPos, { name: asset.name }, this.placeTokenType);
|
||||||
appearance: { sprite: this.placeTokenType, index: 0 }});
|
this.actions.push({ type: 'place_token', tokens: [ token.serialize() ] });
|
||||||
this.actions.push({ type: 'place_token', tokens: [ token.getToken() ] });
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,17 +270,15 @@ export default class TokenMode extends Mode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private moveToken(x: number, y: number, index: number): void {
|
private moveToken(x: number, y: number, index: number): void {
|
||||||
let pre: TokenData[] = [];
|
let pre: TokenRenderData[] = [];
|
||||||
let post: TokenData[] = [];
|
let post: TokenRenderData[] = [];
|
||||||
|
|
||||||
this.selected.forEach((token) => {
|
this.selected.forEach((token) => {
|
||||||
const old = token.getToken();
|
const old = token.getRender();
|
||||||
pre.push(old);
|
pre.push(old);
|
||||||
token.setToken({
|
token.setPosition(old.pos.x + x, old.pos.y + y);
|
||||||
pos: { x: old.pos.x + x, y: old.pos.y + y },
|
token.setTexture(old.appearance.sprite, index);
|
||||||
appearance: { sprite: old.appearance.sprite, index }
|
post.push(token.getRender());
|
||||||
});
|
|
||||||
post.push(token.getToken());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.actions.push({ type: 'modify_token', tokens: { pre, post } });
|
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 {
|
export default class InitScene extends Phaser.Scene {
|
||||||
|
private socket: IO.Socket = IO.io();
|
||||||
|
|
||||||
constructor() { super({key: 'InitScene'}); }
|
constructor() { super({key: 'InitScene'}); }
|
||||||
|
|
||||||
async create({ user, onProgress, identifier, socket }: {
|
async create({ user, onProgress, identifier, mapIdentifier }: InitProps) {
|
||||||
user: string; onProgress: (progress: number) => void; identifier: string; socket: IO.Socket; }) {
|
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 }
|
const { res, map } = await this.onConnect(user, identifier, mapIdentifier);
|
||||||
= 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!!!'));
|
|
||||||
|
|
||||||
this.scene.start('LoadScene', {
|
this.scene.start('LoadScene', {
|
||||||
user,
|
socket: this.socket,
|
||||||
identifier,
|
user, identifier,
|
||||||
onProgress,
|
...res, map,
|
||||||
socket,
|
|
||||||
display,
|
onProgress
|
||||||
assets: res.assets,
|
});
|
||||||
campaign: res.campaign });
|
|
||||||
|
|
||||||
this.game.scene.stop('InitScene');
|
this.game.scene.stop('InitScene');
|
||||||
this.game.scene.swapPosition('LoadScene', '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;
|
const glRenderer = this.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer;
|
||||||
glRenderer.pipelines.add('brighten', new BrightenPipeline(this.game));
|
glRenderer.pipelines.add('brighten', new BrightenPipeline(this.game));
|
||||||
glRenderer.pipelines.add('outline', new OutlinePipeline(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') {
|
if (a.type === 'token') {
|
||||||
const { width, height } = (this.textures.get(a.identifier).frames as any).__BASE;
|
const { width, height } = (this.textures.get(a.identifier).frames as any).__BASE;
|
||||||
a.dimensions = new Vec2(width, height);
|
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);
|
return Patch.tileset(this, a.identifier, a.tileSize);
|
||||||
|
|
||||||
else return new Promise<void>(resolve => resolve());
|
else return new Promise<void>(resolve => resolve());
|
||||||
})).then(() => {
|
}));
|
||||||
this.editorData.onProgress(undefined);
|
|
||||||
|
|
||||||
this.game.scene.start('MapScene', this.editorData);
|
this.editorData.onProgress(undefined);
|
||||||
this.game.scene.stop('LoadScene');
|
|
||||||
this.game.scene.swapPosition('MapScene', 'LoadScene');
|
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 Map from '../map/Map';
|
||||||
import CameraControl from '../CameraControl';
|
import CameraControl from '../CameraControl';
|
||||||
import ModeManager from '../mode/ModeManager';
|
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 { Asset } from '../util/Asset';
|
||||||
import EditorData from '../EditorData';
|
import EditorData from '../EditorData';
|
||||||
|
|
||||||
@ -22,13 +23,9 @@ export default class MapScene extends Phaser.Scene {
|
|||||||
mode: ModeManager = new ModeManager();
|
mode: ModeManager = new ModeManager();
|
||||||
interface: InterfaceRoot = new InterfaceRoot();
|
interface: InterfaceRoot = new InterfaceRoot();
|
||||||
|
|
||||||
size: Vec2 = new Vec2();
|
|
||||||
|
|
||||||
map: Map = new Map();
|
map: Map = new Map();
|
||||||
|
|
||||||
saved: string = '';
|
constructor() { super({ key: 'MapScene' }); }
|
||||||
|
|
||||||
constructor() { super({key: 'MapScene'}); }
|
|
||||||
|
|
||||||
create(data: EditorData): void {
|
create(data: EditorData): void {
|
||||||
this.assets = data.assets;
|
this.assets = data.assets;
|
||||||
@ -36,17 +33,12 @@ export default class MapScene extends Phaser.Scene {
|
|||||||
this.inputManager.init();
|
this.inputManager.init();
|
||||||
this.view.init(this.cameras.main, this.inputManager);
|
this.view.init(this.cameras.main, this.inputManager);
|
||||||
|
|
||||||
this.size = new Vec2(data.campaign.maps[0].size);
|
this.map.init(this, this.assets);
|
||||||
this.map.init(this, this.size, 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.actions.init(this, this.map, data.socket);
|
||||||
this.interface.init(this, data.display, this.inputManager, this.mode, this.actions, this.map, this.assets);
|
this.mode.init(this, this.map, this.actions, this.assets);
|
||||||
|
this.interface.init(this, this.inputManager, this.mode, this.actions, this.map, this.assets);
|
||||||
// this.sound.play('mystify', { loop: true, volume: .2 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update(): void {
|
update(): void {
|
||||||
@ -58,9 +50,6 @@ export default class MapScene extends Phaser.Scene {
|
|||||||
this.mode.update(this.view.cursorWorld, this.inputManager);
|
this.mode.update(this.view.cursorWorld, this.inputManager);
|
||||||
|
|
||||||
this.map.update();
|
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.attachedToken = token;
|
||||||
this.attachedChangeEvent = () => this.setOrigin(new Vec2(token.x + offset.x, token.y + offset.y));
|
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();
|
this.attachedDestroyEvent = () => this.detachFromToken();
|
||||||
token.on('destroy', this.attachedDestroyEvent);
|
token.on('destroy', this.attachedDestroyEvent);
|
||||||
@ -78,7 +78,7 @@ export default abstract class Shape extends Phaser.GameObjects.Container {
|
|||||||
detachFromToken() {
|
detachFromToken() {
|
||||||
if (!this.attachedToken) return;
|
if (!this.attachedToken) return;
|
||||||
this.attachedToken.setSelected(false);
|
this.attachedToken.setSelected(false);
|
||||||
this.attachedToken.change.unbind(this.attachedChangeEvent!);
|
this.attachedToken.on_render.unbind(this.attachedChangeEvent!);
|
||||||
this.attachedChangeEvent = undefined;
|
this.attachedChangeEvent = undefined;
|
||||||
this.attachedToken.off('destroy', this.attachedDestroyEvent!);
|
this.attachedToken.off('destroy', this.attachedDestroyEvent!);
|
||||||
this.attachedDestroyEvent = undefined;
|
this.attachedDestroyEvent = undefined;
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
|
const CHUNK_SIZE = 100000;
|
||||||
export function serialize(buf: ArrayBuffer): string {
|
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 {
|
export function deserialize(str: string): ArrayBuffer {
|
||||||
|
@ -53,8 +53,7 @@ export interface Map {
|
|||||||
name: string;
|
name: string;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
|
|
||||||
size: {x: number, y: number};
|
data: string;
|
||||||
layers: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetType = 'wall' | 'detail' | 'ground' | 'token';
|
export type AssetType = 'wall' | 'detail' | 'ground' | 'token';
|
||||||
|
@ -257,29 +257,49 @@ export default class Database {
|
|||||||
let mapIdentifier = this.sanitizeName(map);
|
let mapIdentifier = this.sanitizeName(map);
|
||||||
if (mapIdentifier.length < 3) 'Map name must contain at least 3 alphanumeric characters.';
|
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');
|
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.';
|
if (!exists) throw 'This campaign no longer exists.';
|
||||||
let mapExists = await collection.findOne({
|
let mapExists = await collection.findOne({
|
||||||
user: user,
|
user, identifier, maps: { $elemMatch: { identifier: mapIdentifier }}});
|
||||||
identifier: campIdentifier,
|
|
||||||
maps: {
|
|
||||||
$elemMatch: {
|
|
||||||
identifier: mapIdentifier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (mapExists) throw 'A map of this name already exists.';
|
if (mapExists) throw 'A map of this name already exists.';
|
||||||
|
|
||||||
await collection.updateOne({user: user, identifier: campIdentifier}, {
|
console.log(mapIdentifier);
|
||||||
$push: { maps: { name: map, identifier: mapIdentifier, size: { x: 64, y: 64 }, layers: '' }}});
|
|
||||||
|
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;
|
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.
|
* Get a map.
|
||||||
* Throws if the map or the campaign doesn't exist.
|
* Throws if the map or the campaign doesn't exist.
|
||||||
@ -291,7 +311,7 @@ export default class Database {
|
|||||||
|
|
||||||
async getMap(user: string, campaign: string, map: string) {
|
async getMap(user: string, campaign: string, map: string) {
|
||||||
const collection = this.db!.collection('campaigns');
|
const collection = this.db!.collection('campaigns');
|
||||||
let exists = await collection.findOne({user: user, identifier: campaign, maps: { $elemMatch: {identifier: map}}});
|
let exists = await collection.findOne({ user: user, identifier: campaign, maps: { $elemMatch: { identifier: map }}});
|
||||||
if (!exists) throw 'This map no longer exists.';
|
if (!exists) throw 'This map no longer exists.';
|
||||||
let mapObj = null;
|
let mapObj = null;
|
||||||
for (let i of exists.maps) {
|
for (let i of exists.maps) {
|
||||||
|
@ -101,6 +101,7 @@ export default class MapController {
|
|||||||
// socket.on('map_load', this.mapLoad.bind(this, user));
|
// socket.on('map_load', this.mapLoad.bind(this, user));
|
||||||
|
|
||||||
socket.on('action', this.onAction.bind(this, socket, room));
|
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));
|
// socket.on('get_campaign_assets', this.onGetCampaignAssets.bind(this, user));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,6 +109,10 @@ export default class MapController {
|
|||||||
socket.in(room).emit('action', event);
|
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) {
|
// private async mapLoad(user: string, identifier: { campaign: string, map: string }, res: (data: string) => void) {
|
||||||
// if (typeof res !== 'function' || typeof identifier !== 'object' ||
|
// if (typeof res !== 'function' || typeof identifier !== 'object' ||
|
||||||
// typeof identifier.map !== 'string' || typeof identifier.campaign !== 'string') return;
|
// typeof identifier.map !== 'string' || typeof identifier.campaign !== 'string') return;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user