Add tile highlight mode, pinging

master
Auri 2021-02-10 15:02:16 -08:00
parent bde31b9e00
commit 498b7c7273
77 changed files with 1939 additions and 879 deletions

13
app/package-lock.json generated
View File

@ -2532,6 +2532,11 @@
}
}
},
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
},
"clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@ -5412,6 +5417,14 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.5.7.tgz",
"integrity": "sha512-4oEpz75t/0UNcwmcsjk+BIcDdk68oao+7kxcpc1hQPNs2Oo3ZL9xFz8UBf350mxk/VEdD41L5b4l2dE3Ug3RYg=="
},
"preact-transitioning": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/preact-transitioning/-/preact-transitioning-1.0.2.tgz",
"integrity": "sha512-WUhhUXW9T0gSN7NOumjel+A+xmNveYBlIXZcVtxT8gm6DwL1mS65I2DbAW35ffzgaDkzPm7AUTqvTJYu86g1EQ==",
"requires": {
"classnames": "^2.2.6"
}
},
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View File

@ -20,6 +20,7 @@
"js-cookie": "^2.2.1",
"phaser": "^3.51.0",
"preact": "^10.5.7",
"preact-transitioning": "^1.0.2",
"query-string": "^6.13.8",
"react-router-dom": "^5.2.0",
"socket.io-client": "^3.0.5",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,3 +1,4 @@
import * as Preact from 'preact';
import { useEffect, useContext } from 'preact/hooks';
import { AppData, AppDataSpecifier } from '../../common/AppData';
@ -33,3 +34,55 @@ export function useAppData(refresh?: AppDataSpecifier | AppDataSpecifier[], depe
return [ ctx.data, updateAppData.bind(undefined, ctx.mergeData), ctx.mergeData ];
}
/**
* Calls onCancel if a click event is triggered on an element that is not a child of the currently ref'd popup.
* Optionally, a condition function can be supplied, and the cancel test will only occur if the function returns true.
* Any dependents for the condition function can be supplied in the dependents array,
* this hook will automatically handle depending on the current popup, cancel function, and condition function.
*
* @param {Preact.RefObject<any>} roots - A ref of elements to exclude from outside-clicks.
* @param {Function} onCancel - The function to call if a click occurs outside of `popup`.
* @param {Function} condition - An optional function to determine whether or not to run the click test.
* @param {any[]} dependents - An array of dependents for the condition function.
*/
export function usePopupCancel(roots: Preact.RefObject<any> | Preact.RefObject<any>[],
onCancel: () => any, condition?: () => boolean, dependents?: any[]) {
const body = document.getElementsByTagName('body')[0];
useEffect(() => {
const rootsArray = Array.isArray(roots) ? roots : [ roots ];
if (condition && !condition()) return;
const handlePointerCancel = (e: MouseEvent | TouchEvent) => {
let x = e.target as HTMLElement;
while (x) {
for (const r of rootsArray) if (x === r.current) return;
x = x.parentNode as HTMLElement;
}
onCancel();
};
const handleFocusCancel = (e: FocusEvent) => {
let x = e.target as HTMLElement;
while (x) {
for (const r of rootsArray) if (x === r.current) return;
x = x.parentNode as HTMLElement;
}
onCancel();
};
body.addEventListener('focusin', handleFocusCancel);
body.addEventListener('mousedown', handlePointerCancel);
body.addEventListener('touchstart', handlePointerCancel);
return () => {
body.removeEventListener('focusin', handleFocusCancel);
body.removeEventListener('mousedown', handlePointerCancel);
body.removeEventListener('touchstart', handlePointerCancel);
};
}, [ onCancel, condition, ...dependents || [] ]);
}

View File

@ -1,5 +1,6 @@
import * as Preact from 'preact';
import type Phaser from 'phaser';
import { Prompt } from 'react-router-dom';
import { useState, useEffect, useRef } from 'preact/hooks';
import './Editor.sass';
@ -18,6 +19,8 @@ function pad(n: number) {
export default function Editor({ user, identifier, mapIdentifier }: Props) {
const rootRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<Phaser.Game | null>(null);
const [ dirty, setDirty ] = useState<boolean>(false);
const [ loadPercent, setLoadPercent ] = useState<number | undefined>(0);
/**
@ -31,7 +34,7 @@ export default function Editor({ user, identifier, mapIdentifier }: Props) {
setLoadPercent(0.25);
if (ignore || !rootRef.current) return;
editorRef.current = create(rootRef.current, setLoadPercent, user, identifier, mapIdentifier);
editorRef.current = create(rootRef.current, user, identifier, mapIdentifier, setLoadPercent, setDirty);
const resizeCallback = () => {
const { width, height } = rootRef.current.getBoundingClientRect();
@ -84,6 +87,7 @@ export default function Editor({ user, identifier, mapIdentifier }: Props) {
<p class='Editor-LoaderText'><small>Loading </small>{pad(Math.round(loadPercent * 100))}%</p>
</div>
}
<Prompt when={dirty} message='Are you sure you want to leave? Changes that you made may not be saved.' />
</div>
);
}

View File

@ -0,0 +1,33 @@
@use '../style/def' as *
.Popup
position: fixed
top: 0
left: 0
right: 0
bottom: 0
pointer-events: none
& > *
pointer-events: initial
&.DefaultAnim
// Important properties are used here because the wildcard selector has the lowest priority,
// and we have to ensure that these properties are set, otherwise it will not render properly.
& > *
transform: translate(-50%, 0px) scale(0.95) !important
opacity: 0 !important
transition: opacity $t-ufast, transform $t-ufast !important
&.Animate-enter-active, &.Animate-enter-done
& > *
opacity: 1 !important
transform: translate(-50%, 12px) scale(1) !important
&.Animate-enter-active:not(.Animate-enter-done), &.Animate-exit-active:not(.Animate-exit-done)
& > *
will-change: transform !important

View File

@ -0,0 +1,35 @@
import * as Preact from 'preact';
import { forwardRef } from 'preact/compat';
import { CSSTransition } from 'preact-transitioning';
import './Popup.sass';
import Portal from './Portal';
interface Props {
active: boolean;
duration?: number;
defaultAnimation?: boolean;
z?: number;
class?: string;
children: Preact.ComponentChildren;
}
const Popup = forwardRef<HTMLDivElement, Props>((props, fRef) => {
return (
<Portal to={document.querySelector('.App') ?? document.body}>
<div ref={fRef}>
<CSSTransition in={props.active} duration={props.duration ?? 150} classNames='Animate'>
<div class={('Popup ' + (props.class ?? '') + (props.defaultAnimation ? ' DefaultAnim' : '')).trim()}
style={{zIndex: props.z ?? 5}}>
{props.children}
</div>
</CSSTransition>
</div>
</Portal>
);
});
export default Popup;

View File

@ -0,0 +1,19 @@
import * as Preact from 'preact';
import { createPortal } from 'preact/compat';
import { useRef, useEffect } from 'preact/hooks';
export default function Portal(props: { to: HTMLElement; children: Preact.ComponentChildren }) {
const root = useRef<HTMLDivElement>(document.createElement('div'));
useEffect(() => {
props.to.appendChild(root.current);
return () => props.to.removeChild(root.current);
}, [ props.to ]);
return (
createPortal(
<Preact.Fragment>{props.children}</Preact.Fragment>,
root.current
)
);
};

View File

@ -1,4 +1,5 @@
@import '../../partial/Ext'
@use '../../style/def' as *
@use '../../style/ext'
.ColorPicker
width: 300px
@ -25,7 +26,12 @@
background-color: #000
.ColorPicker-SatVal
@extend %center_wrap
display: flex
width: 100%
height: 100%
flex-direction: column
justify-content: center
align-items: center
position: relative
cursor: pointer

View File

@ -1,105 +1,106 @@
// import * as Preact from 'preact';
// import { Color } from 'auriserve-api';
// import { forwardRef } from 'preact/compat';
// import { useEffect, useRef } from 'preact/hooks';
import * as Preact from 'preact';
import { forwardRef } from 'preact/compat';
import { useEffect, useRef } from 'preact/hooks';
// import './ColorPicker.sass';
import './ColorPicker.sass';
// import { WidgetProps } from './Input';
import { WidgetProps } from './Input';
// interface Props {
// parent?: HTMLElement;
// writable?: boolean;
// displayHex?: boolean;
// }
import * as Color from '../../../../common/Color';
// const ColorPicker = forwardRef<HTMLDivElement, WidgetProps & Props>((props, ref) => {
// const color = props.value ? typeof props.value === 'string' ? Color.hexToHSV(props.value) : props.value : { h: 0, s: 0, v : 0};
interface Props {
parent?: HTMLElement;
writable?: boolean;
showHex?: boolean;
}
// const mouseTarget = useRef<string>('');
// const satValElem = useRef<HTMLDivElement>(null);
// const hueElem = useRef<HTMLDivElement>(null);
const ColorPicker = forwardRef<HTMLDivElement, WidgetProps & Props>((props, ref) => {
const color = props.value ? typeof props.value === 'string' ? Color.hexToHSV(props.value) : props.value : { h: 0, s: 0, v : 0};
// const inputHex = (evt: any) => {
// const val = evt.target.value;
// if (val.length !== 7) return;
// props.setValue(Color.hexToHSV(val));
// };
const mouseTarget = useRef<string>('');
const satValElem = useRef<HTMLDivElement>(null);
const hueElem = useRef<HTMLDivElement>(null);
// const handleHueMove = (evt: MouseEvent) => {
// const bounds = hueElem.current.getBoundingClientRect();
// const hue = Math.max(Math.min((evt.clientX - bounds.left) / bounds.width, 1), 0);
// props.setValue({ ...color, h: hue });
// };
const inputHex = (evt: any) => {
const val = evt.target.value;
if (val.length !== 7) return;
props.setValue(Color.hexToHSV(val));
};
// const handleSatValMove = (evt: MouseEvent) => {
// const bounds = satValElem.current.getBoundingClientRect();
// const sat = Math.max(Math.min((evt.clientX - bounds.left) / bounds.width, 1), 0);
// const val = Math.max(Math.min((bounds.bottom - evt.clientY) / bounds.height, 1), 0);
// props.setValue({ ...color, s: sat, v: val });
// };
const handleHueMove = (evt: MouseEvent) => {
const bounds = hueElem.current.getBoundingClientRect();
const hue = Math.max(Math.min((evt.clientX - bounds.left) / bounds.width, 1), 0);
props.setValue({ ...color, h: hue });
};
// const handleMouseMove = (evt: MouseEvent) => {
// switch (mouseTarget.current) {
// default: return;
// case 'hue': return handleHueMove(evt);
// case 'satval': return handleSatValMove(evt);
// }
// };
const handleSatValMove = (evt: MouseEvent) => {
const bounds = satValElem.current.getBoundingClientRect();
const sat = Math.max(Math.min((evt.clientX - bounds.left) / bounds.width, 1), 0);
const val = Math.max(Math.min((bounds.bottom - evt.clientY) / bounds.height, 1), 0);
props.setValue({ ...color, s: sat, v: val });
};
// const handleMouseClick = (evt: MouseEvent, target: string) => {
// evt.stopImmediatePropagation();
// evt.preventDefault();
const handleMouseMove = (evt: MouseEvent) => {
switch (mouseTarget.current) {
default: return;
case 'hue': return handleHueMove(evt);
case 'satval': return handleSatValMove(evt);
}
};
// mouseTarget.current = target;
// handleMouseMove(evt);
// };
const handleMouseClick = (evt: MouseEvent, target: string) => {
evt.stopImmediatePropagation();
evt.preventDefault();
// useEffect(() => {
// const clearMouse = () => mouseTarget.current = '';
// document.body.addEventListener('mouseup', clearMouse);
// document.body.addEventListener('mousemove', handleMouseMove);
// return () => {
// document.body.removeEventListener('mouseup', clearMouse);
// document.body.removeEventListener('mousemove', handleMouseMove);
// };
// }, [ handleMouseMove ]);
mouseTarget.current = target;
handleMouseMove(evt);
};
// const hueHex = Color.HSVToHex({ h: color.h, s: 1, v: 1 });
// const fullHex = Color.HSVToHex(color);
useEffect(() => {
const clearMouse = () => mouseTarget.current = '';
document.body.addEventListener('mouseup', clearMouse);
document.body.addEventListener('mousemove', handleMouseMove);
return () => {
document.body.removeEventListener('mouseup', clearMouse);
document.body.removeEventListener('mousemove', handleMouseMove);
};
}, [ handleMouseMove ]);
// const style: any = {};
// if (props.parent) {
// style.top = props.parent.getBoundingClientRect().bottom + 'px';
// style.left = ((props.parent.getBoundingClientRect().left +
// props.parent.getBoundingClientRect().right) / 2) + 'px';
// }
const hueHex = Color.HSVToHex({ h: color.h, s: 1, v: 1 });
const fullHex = Color.HSVToHex(color);
// return (
// <div class={('ColorPicker ' + (props.writable ? 'Write ' : '' + (props.parent ? 'Absolute' : ''))).trim()}
// ref={ref} style={style}>
const style: any = {};
if (props.parent) {
style.top = props.parent.getBoundingClientRect().bottom + 'px';
style.left = ((props.parent.getBoundingClientRect().left +
props.parent.getBoundingClientRect().right) / 2) + 'px';
}
return (
<div class={('ColorPicker ' + (props.writable ? 'Write ' : '' + (props.parent ? 'Absolute' : ''))).trim()}
ref={ref} style={style}>
// <div class='ColorPicker-SatVal' ref={satValElem}
// onMouseDown={(evt) => handleMouseClick(evt, 'satval')}
// style={{ backgroundColor: hueHex }}>
<div class='ColorPicker-SatVal' ref={satValElem}
onMouseDown={(evt) => handleMouseClick(evt, 'satval')}
style={{ backgroundColor: hueHex }}>
// {props.displayHex && <p class='ColorPicker-Hex'>{fullHex}</p>}
{props.showHex && <p class='ColorPicker-Hex'>{fullHex}</p>}
// <div class='ColorPicker-Indicator' style={{ left: (color.s * 100) + '%',
// top: ((1 - color.v) * 100) + '%', backgroundColor: fullHex }} />
<div class='ColorPicker-Indicator' style={{ left: (color.s * 100) + '%',
top: ((1 - color.v) * 100) + '%', backgroundColor: fullHex }} />
// </div>
// <div class='ColorPicker-Separator' />
// <div class='ColorPicker-Hue' ref={hueElem}
// onMouseDown={(evt) => handleMouseClick(evt, 'hue')}>
// <div class='ColorPicker-Indicator' style={{ left: (color.h * 100) + '%', backgroundColor: hueHex }} />
// </div>
// {props.writable && <div class='ColorPicker-Details'>
// <div class='ColorPicker-ColorBlock' style={{ backgroundColor: fullHex }} />
// <input class='ColorPicker-ColorInput' value={fullHex} onChange={inputHex} onInput={inputHex} maxLength={7} />
// </div>}
// </div>
// );
// });
</div>
<div class='ColorPicker-Separator' />
<div class='ColorPicker-Hue' ref={hueElem}
onMouseDown={(evt) => handleMouseClick(evt, 'hue')}>
<div class='ColorPicker-Indicator' style={{ left: (color.h * 100) + '%', backgroundColor: hueHex }} />
</div>
{props.writable && <div class='ColorPicker-Details'>
<div class='ColorPicker-ColorBlock' style={{ backgroundColor: fullHex }} />
<input class='ColorPicker-ColorInput' value={fullHex} onChange={inputHex} onInput={inputHex} maxLength={7} />
</div>}
</div>
);
});
// export default ColorPicker;
export default ColorPicker;

View File

@ -16,7 +16,7 @@ export { default as Divider } from './InputDivider';
export { default as Annotation } from './InputAnnotation';
export { default as Text } from './fields/InputText';
export { default as Color } from './fields/InputColor';
// export { default as Color } from './fields/InputColor';
// export { default as Select } from './fields/InputSelect';
export { default as Numeric } from './fields/InputNumeric';
export { default as Checkbox } from './fields/InputCheckbox';

View File

@ -1,33 +1,33 @@
import * as Preact from 'preact';
// import { Color } from 'auriserve-api';
import { forwardRef } from 'preact/compat';
import { useState, useRef } from 'preact/hooks';
// import { usePopupCancel } from '../../../Hooks';
import { usePopupCancel } from '../../../Hooks';
import './InputColor.sass';
// import Popup from '../../Popup';
// import InputText from './InputText';
// import ColorPicker from '../ColorPicker';
import Popup from '../../Popup';
import InputText from './InputText';
import ColorPicker from '../ColorPicker';
import { WidgetProps } from '../Input';
import * as Color from '../../../../../common/Color';
interface Props {
writable?: boolean;
displayHex?: boolean;
showHex?: boolean;
full?: boolean;
}
const InputColor = forwardRef<HTMLInputElement, Props & WidgetProps>((props, _fRef) => {
const InputColor = forwardRef<HTMLInputElement, Props & WidgetProps>((props, fRef) => {
const inputRef = useRef<HTMLDivElement>(null);
const [ /* pickerActive */, setPickerActive ] = useState(false);
const [ pickerActive, setPickerActive ] = useState(false);
// usePopupCancel(inputRef, () => setPickerActive(false));
usePopupCancel(inputRef, () => setPickerActive(false));
return (
<div class={('InputColor ' + (props.full ? 'Full ' : '') + (props.class ?? '')).trim()}
style={props.style} onFocusCapture={() => setPickerActive(true)} ref={inputRef}>
{/* <InputText
<InputText
ref={fRef}
value={Color.HSVToHex(props.value)}
setValue={hex => props.setValue(Color.hexToHSV(hex))}
@ -36,7 +36,7 @@ const InputColor = forwardRef<HTMLInputElement, Props & WidgetProps>((props, _fR
<div class='InputColor-ColorIndicator' style={{ backgroundColor: Color.HSVToHex(props.value) }}/>
<Popup active={pickerActive} defaultAnimation={true}>
<ColorPicker {...props} parent={inputRef.current} />
</Popup>*/}
</Popup>
</div>
);
});

View File

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

View File

@ -1,2 +0,0 @@
export { default as Asset } from './AssetRoute';
export { default as Collection } from './CollectionRoute';

1
app/src/declarations.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'preact-transitioning';

View File

@ -9,6 +9,6 @@ export default interface EditorData {
assets: Asset[];
map?: string;
display: 'edit' | 'view';
onDirty: (dirty: boolean) => void;
onProgress: (progress: number | undefined) => void;
}

View File

@ -2,8 +2,8 @@ import Phaser from 'phaser';
import * as Scene from './scene/Scenes';
export default function create(root: HTMLElement, onProgress: (progress: number) => void,
user: string, identifier: string, mapIdentifier?: string) {
export default function create(root: HTMLElement, user: string, identifier: string, mapIdentifier: string | undefined,
onProgress: (progress: number) => void, onDirty: (dirty: boolean) => void) {
const bounds = root.getBoundingClientRect();
@ -19,6 +19,6 @@ export default function create(root: HTMLElement, onProgress: (progress: number)
scene: Scene.list
});
game.scene.start('InitScene', { user, onProgress, identifier, mapIdentifier });
game.scene.start('InitScene', { user, identifier, mapIdentifier, onProgress, onDirty });
return game;
}

View File

@ -17,161 +17,139 @@ const PATCH_TIMING = false;
* @returns a promise that resolves when the texture has been updated.
*/
export async function tileset(scene: Phaser.Scene, tileset_key: string, tile_size: number): Promise<void> {
return new Promise<void>(resolve => {
const s = PATCH_TIMING ? Date.now() : 0;
export function tileset(scene: Phaser.Scene, tileset_key: string, tile_size: number) {
const s = PATCH_TIMING ? Date.now() : 0;
const canvas = new Phaser.GameObjects.RenderTexture(scene, 0, 0, 10 * tile_size, 5 * tile_size);
canvas.draw(tileset_key);
const canvas = new Phaser.GameObjects.RenderTexture(scene, 0, 0, 10 * tile_size, 5 * tile_size);
canvas.draw(tileset_key);
let part: Phaser.GameObjects.Sprite | Phaser.GameObjects.RenderTexture
= new Phaser.GameObjects.Sprite(scene, 0, 0, tileset_key, '__BASE');
part.setOrigin(0, 0);
let part: Phaser.GameObjects.Sprite | Phaser.GameObjects.RenderTexture
= new Phaser.GameObjects.Sprite(scene, 0, 0, tileset_key, '__BASE');
part.setOrigin(0, 0);
function draw(source: Vec4, dest: Vec2) {
part.setCrop(source.x * tile_size, source.y * tile_size, (source.z - source.x) * tile_size, (source.w - source.y) * tile_size);
part.setPosition((dest.x - source.x) * tile_size, (dest.y - source.y) * tile_size);
canvas.draw(part);
function draw(source: Vec4, dest: Vec2) {
part.setCrop(source.x * tile_size, source.y * tile_size, (source.z - source.x) * tile_size, (source.w - source.y) * tile_size);
part.setPosition((dest.x - source.x) * tile_size, (dest.y - source.y) * tile_size);
canvas.draw(part);
}
// End Pieces and Walls
draw(new Vec4(2, 0, 3, 0.5), new Vec2(7, 0));
draw(new Vec4(2, 1.5, 3, 2), new Vec2(7, 0.5));
draw(new Vec4(2, 0, 2.5, 1), new Vec2(8, 0));
draw(new Vec4(3.5, 0, 4, 1), new Vec2(8.5, 0));
draw(new Vec4(1, 0, 2, 0.5), new Vec2(9, 0));
draw(new Vec4(0, 1.5, 1, 2), new Vec2(9, 0.5));
draw(new Vec4(2, 1, 2.5, 2), new Vec2(7, 1));
draw(new Vec4(3.5, 1, 4, 2), new Vec2(7.5, 1));
draw(new Vec4(3, 0, 4, 0.5), new Vec2(8, 1));
draw(new Vec4(3, 1.5, 4, 2), new Vec2(8, 1.5));
draw(new Vec4(0, 0, 0.5, 1), new Vec2(9, 1));
draw(new Vec4(1.5, 1, 2, 2), new Vec2(9.5, 1));
// Advanced Corners (Orange)
draw(new Vec4(6, 1, 7, 1.5), new Vec2(0, 2));
draw(new Vec4(6, 0.5, 7, 1), new Vec2(0, 2.5));
draw(new Vec4(6, 1, 6.5, 2), new Vec2(1, 2));
draw(new Vec4(5.5, 1, 6, 2), new Vec2(1.5, 2));
draw(new Vec4(5, 0.5, 6, 1), new Vec2(2, 2.5));
draw(new Vec4(5.5, 0, 6, 0.5), new Vec2(2.5, 2));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(2, 2));
draw(new Vec4(6, 0, 6.5, 1), new Vec2(0, 3));
draw(new Vec4(5.5, 0, 6, 1), new Vec2(0.5, 3));
draw(new Vec4(5, 1, 6, 1.5), new Vec2(1, 3));
draw(new Vec4(5, 0.5, 6, 1), new Vec2(1, 3.5));
draw(new Vec4(6, 0, 6.5, 0.5), new Vec2(2, 3));
draw(new Vec4(6, 0.5, 7, 1), new Vec2(2, 3.5));
draw(new Vec4(5.5, 1, 6, 1.5), new Vec2(2.5, 3));
draw(new Vec4(5.5, 1, 6, 1.5), new Vec2(0.5, 4));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(0, 4));
draw(new Vec4(6, 0.5, 6.5, 1), new Vec2(0, 4.5));
draw(new Vec4(5.5, 0.5, 6, 1), new Vec2(0.5, 4.5));
/*
* So here's why this is horrible:
* - Phaser doesn't let you copy a region of a RenderTexture to the same RenderTexture.
* - Creating a new RenderTexture and drawing the original directly onto it flips it upside down for some reason?
* - Scaling that upside-down RenderTexture to upside-right fucks with the draw() function's positioning.
*
* In other words, yeah, it's fucked man. The janky solution below is the only way I've found to make it work,
* so if future-Auri is looking at this and making a snarky comment to her friends about how she could do it
* *so much better*, please, just don't. Don't do it.
*/
part.setCrop();
part = new Phaser.GameObjects.RenderTexture(scene, 0, 0, canvas.width, canvas.height);
part.setOrigin(0, 0);
const temp = new Phaser.GameObjects.Sprite(scene, 0, 0, canvas.texture);
temp.setOrigin(0, 0);
part.draw(temp);
// Derived Forms (Pink)
draw(new Vec4(2, 0, 4, 0.5), new Vec2(3, 2));
draw(new Vec4(2, 0.5, 2.5, 1.5), new Vec2(3, 2.5));
draw(new Vec4(3.5, 0.5, 4, 1.5), new Vec2(4.5, 2.5));
draw(new Vec4(2, 1.5, 4, 2), new Vec2(3, 3.5));
draw(new Vec4(5.5, 0.5, 6.5, 1.5), new Vec2(3.5, 2.5));
draw(new Vec4(1, 0, 2, 0.5), new Vec2(5, 2));
draw(new Vec4(1, 0, 2, 0.5), new Vec2(6, 2));
draw(new Vec4(1, 0, 2, 0.5), new Vec2(7, 2));
draw(new Vec4(5, 0.5, 6.5, 1.5), new Vec2(5, 2.5));
draw(new Vec4(5.5, 0.5, 7, 1.5), new Vec2(6.5, 2.5));
draw(new Vec4(0, 1.5, 1, 2), new Vec2(5, 3.5));
draw(new Vec4(0, 1.5, 1, 2), new Vec2(6, 3.5));
draw(new Vec4(0, 1.5, 1, 2), new Vec2(7, 3.5));
draw(new Vec4(5.5, 0, 6.5, 0.5), new Vec2(8.5, 2));
draw(new Vec4(5.5, 1.5, 6.5, 2), new Vec2(8.5, 4.5));
draw(new Vec4(1.5, 1, 2, 2), new Vec2(9.5, 2));
draw(new Vec4(1.5, 1, 2, 2), new Vec2(9.5, 3));
draw(new Vec4(1.5, 1, 2, 2), new Vec2(9.5, 4));
draw(new Vec4(0, 0, 0.5, 1), new Vec2(8, 2));
draw(new Vec4(0, 0, 0.5, 1), new Vec2(8, 3));
draw(new Vec4(0, 0, 0.5, 1), new Vec2(8, 4));
draw(new Vec4(5.5, 0.5, 6.5, 1.5), new Vec2(8.5, 2.5));
draw(new Vec4(5.5, 0.5, 6.5, 1.5), new Vec2(8.5, 3.5));
draw(new Vec4(0, 2, 0.5, 3), new Vec2(4, 4));
draw(new Vec4(0.5, 2.5, 1, 3), new Vec2(4.5, 4.5));
draw(new Vec4(5.5, 1, 6, 1.5), new Vec2(4.5, 4));
draw(new Vec4(1.5, 3, 2, 4), new Vec2(5.5, 4));
draw(new Vec4(1, 3.5, 1.5, 4), new Vec2(5, 4.5));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(5, 4));
draw(new Vec4(0, 2, 0.5, 3), new Vec2(6, 4));
draw(new Vec4(0.5, 2, 1, 2.5), new Vec2(6.5, 4));
draw(new Vec4(5.5, 0.5, 6, 1), new Vec2(6.5, 4.5));
draw(new Vec4(1.5, 3, 2, 4), new Vec2(7.5, 4));
draw(new Vec4(1, 3, 1.5, 3.5), new Vec2(7, 4));
draw(new Vec4(6, 0.5, 6.5, 1), new Vec2(7, 4.5));
const tex = canvas.saveTexture(tileset_key);
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 10; j++) {
tex.add(j + i * 10, 0, j * tile_size, tile_size * 5 - (i + 1) * tile_size, tile_size, tile_size);
}
}
// End Pieces and Walls
draw(new Vec4(2, 0, 3, 0.5), new Vec2(7, 0));
draw(new Vec4(2, 1.5, 3, 2), new Vec2(7, 0.5));
draw(new Vec4(2, 0, 2.5, 1), new Vec2(8, 0));
draw(new Vec4(3.5, 0, 4, 1), new Vec2(8.5, 0));
draw(new Vec4(1, 0, 2, 0.5), new Vec2(9, 0));
draw(new Vec4(0, 1.5, 1, 2), new Vec2(9, 0.5));
draw(new Vec4(2, 1, 2.5, 2), new Vec2(7, 1));
draw(new Vec4(3.5, 1, 4, 2), new Vec2(7.5, 1));
draw(new Vec4(3, 0, 4, 0.5), new Vec2(8, 1));
draw(new Vec4(3, 1.5, 4, 2), new Vec2(8, 1.5));
draw(new Vec4(0, 0, 0.5, 1), new Vec2(9, 1));
draw(new Vec4(1.5, 1, 2, 2), new Vec2(9.5, 1));
// Advanced Corners (Orange)
draw(new Vec4(6, 1, 7, 1.5), new Vec2(0, 2));
draw(new Vec4(6, 0.5, 7, 1), new Vec2(0, 2.5));
draw(new Vec4(6, 1, 6.5, 2), new Vec2(1, 2));
draw(new Vec4(5.5, 1, 6, 2), new Vec2(1.5, 2));
draw(new Vec4(5, 0, 6, 1), new Vec2(2, 2));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(2, 2));
draw(new Vec4(6, 0, 6.5, 1), new Vec2(0, 3));
draw(new Vec4(5.5, 0, 6, 1), new Vec2(0.5, 3));
draw(new Vec4(5, 1, 6, 1.5), new Vec2(1, 3));
draw(new Vec4(5, 0.5, 6, 1), new Vec2(1, 3.5));
draw(new Vec4(6, 0, 7, 1), new Vec2(2, 3));
draw(new Vec4(5.5, 1, 6, 1.5), new Vec2(2.5, 3));
draw(new Vec4(5.5, 1, 6, 1.5), new Vec2(0.5, 4));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(0, 4));
draw(new Vec4(6, 0.5, 6.5, 1), new Vec2(0, 4.5));
draw(new Vec4(5.5, 0.5, 6, 1), new Vec2(0.5, 4.5));
/**
* So here's why this is horrible:
* - Phaser doesn't let you copy a region of a RenderTexture to the same RenderTexture.
* - Creating a new RenderTexture and drawing the original directly onto it flips it upside down for some reason?
* - Scaling that upside-down RenderTexture to upside-right fucks with the draw() function's positioning.
*
* In other words, yeah, it's fucked man. The janky solution below is the only way I've found to make it work,
* so if future-Auri is looking at this and making a snarky comment to her friends about how she could do it
* *so much better*, please, just don't. Don't do it.
*/
part.setCrop();
part = new Phaser.GameObjects.RenderTexture(scene, 0, 0, canvas.width, canvas.height);
part.setOrigin(0, 0);
const temp = new Phaser.GameObjects.Sprite(scene, 0, 0, canvas.texture);
temp.setOrigin(0, 0);
part.draw(temp);
// Derived Forms (Pink)
draw(new Vec4(2, 0, 3, 1), new Vec2(3, 2));
draw(new Vec4(5.5, 0.5, 6, 1), new Vec2(3.5, 2.5));
draw(new Vec4(3, 0, 4, 1), new Vec2(4, 2));
draw(new Vec4(6, 0.5, 6.5, 1), new Vec2(4, 2.5));
draw(new Vec4(1, 0, 2, 1), new Vec2(5, 2));
draw(new Vec4(5.5, 0.5, 6, 1), new Vec2(5.5, 2.5));
draw(new Vec4(1, 0, 2, 1), new Vec2(6, 2));
draw(new Vec4(0, 3.5, 1, 4), new Vec2(6, 2.5));
draw(new Vec4(1, 0, 2, 1), new Vec2(7, 2));
draw(new Vec4(6, 0.5, 6.5, 1), new Vec2(7, 2.5));
draw(new Vec4(0, 0, 1, 1), new Vec2(8, 2));
draw(new Vec4(5.5, 0.5, 6, 1), new Vec2(8.5, 2.5));
draw(new Vec4(1, 1, 2, 2), new Vec2(9, 2));
draw(new Vec4(6, 0.5, 6.5, 1), new Vec2(9, 2.5));
draw(new Vec4(2, 1, 3, 2), new Vec2(3, 3));
draw(new Vec4(5.5, 1, 6, 1.5), new Vec2(3.5, 3));
draw(new Vec4(3, 1, 4, 2), new Vec2(4, 3));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(4, 3));
draw(new Vec4(0, 1, 1, 2), new Vec2(5, 3));
draw(new Vec4(5.5, 1, 6, 1.5), new Vec2(5.5, 3));
draw(new Vec4(0, 1, 1, 2), new Vec2(6, 3));
draw(new Vec4(1, 2, 2, 2.5), new Vec2(6, 3));
draw(new Vec4(0, 1, 1, 2), new Vec2(7, 3));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(7, 3));
draw(new Vec4(0, 0, 1, 1), new Vec2(8, 3));
draw(new Vec4(1.5, 3, 2, 4), new Vec2(8.5, 3));
draw(new Vec4(1, 1, 2, 2), new Vec2(9, 3));
draw(new Vec4(0, 2, 0.5, 3), new Vec2(9, 3));
draw(new Vec4(0, 2, 1, 3), new Vec2(4, 4));
draw(new Vec4(5.5, 1, 6, 1.5), new Vec2(4.5, 4));
draw(new Vec4(1, 3, 2, 4), new Vec2(5, 4));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(5, 4));
draw(new Vec4(0, 2, 1, 3), new Vec2(6, 4));
draw(new Vec4(5.5, 0.5, 6, 1), new Vec2(6.5, 4.5));
draw(new Vec4(1, 3, 2, 4), new Vec2(7, 4));
draw(new Vec4(6, 0.5, 6.5, 1), new Vec2(7, 4.5));
draw(new Vec4(0, 0, 1, 1), new Vec2(8, 4));
draw(new Vec4(5.5, 1, 6, 1.5), new Vec2(8.5, 4));
draw(new Vec4(1, 1, 2, 2), new Vec2(9, 4));
draw(new Vec4(6, 1, 6.5, 1.5), new Vec2(9, 4));
canvas.snapshot((img: any) => {
scene.textures.removeKey(tileset_key);
scene.textures.addSpriteSheet(tileset_key, img, { frameWidth: tile_size, frameHeight: tile_size });
if (PATCH_TIMING) console.log(`Patched Tileset '${tileset_key}' in ${Date.now() - s} ms.`);
resolve();
});
});
if (PATCH_TIMING) console.log(`Patched Tileset '${tileset_key}' in ${Date.now() - s} ms.`);
}

View File

@ -5,7 +5,7 @@ import Map from '../map/Map';
import type { Action } from './Action';
import ActionEvent from './ActionEvent';
import EventHandler from '../EventHandler';
import InputManager from '../InputManager';
import InputManager from '../interact/InputManager';
const SAVE_INTERVAL = 5 * 1000;
@ -21,12 +21,15 @@ export default class ActionManager {
private historyHeldTime: number = 0;
private editTime: number | false = false;
init(_scene: Phaser.Scene, map: Map, socket: IO.Socket) {
this.map = map;
// this.scene = scene;
this.socket = socket;
private onDirty: (dirty: boolean) => void = null as any;
init(_scene: Phaser.Scene, map: Map, socket: IO.Socket, onDirty: (dirty: boolean) => void) {
this.map = map;
this.socket = socket;
this.onDirty = onDirty;
this.socket.on('action', this.apply.bind(this));
window.onbeforeunload = () => this.editTime ? '' : null;
}
update(input: InputManager) {
@ -49,16 +52,17 @@ export default class ActionManager {
}
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;
}
if (this.editTime && Date.now() - SAVE_INTERVAL > this.editTime) this.saveMap();
}
push(item: Action): void {
this.history.splice(this.head + 1, this.history.length - this.head, item);
this.head = this.history.length - 1;
if (!this.editTime) this.editTime = Date.now();
if (!this.editTime) {
this.editTime = Date.now();
this.onDirty(true);
}
this.socket.emit('action', item);
this.event.dispatch({ event: 'push', head: this.head, length: this.history.length });
@ -138,4 +142,10 @@ export default class ActionManager {
hasNext(): boolean {
return this.head < this.history.length - 1;
}
private saveMap() {
this.socket.emit('serialize', this.map.identifier, this.map.save());
this.onDirty(false);
this.editTime = 0;
}
}

View File

@ -0,0 +1,205 @@
import * as Phaser from 'phaser';
import MapLayer from '../map/MapLayer';
import InputManager from './InputManager';
import ActionManager from '../action/ActionManager';
import { Vec2 } from '../util/Vec';
import { Layer } from '../util/Layer';
import { TileItem } from '../action/Action';
export default class ArchitectController {
activeTileset: number = 0;
activeLayer: Layer = 'wall';
private editMode: 'brush' | 'rect' | 'line' = 'brush';
private pointerState: 'primary' | 'secondary' | null = null;
private drawStartPos: Vec2 = new Vec2();
private layer?: MapLayer;
private drawn: TileItem[] = [];
private cursor: Phaser.GameObjects.Sprite;
private primitives: (Phaser.GameObjects.Line | Phaser.GameObjects.Sprite)[] = [];
constructor(private scene: Phaser.Scene, private actions: ActionManager) {
this.cursor = this.scene.add.sprite(0, 0, 'cursor');
this.cursor.setDepth(1000);
this.cursor.setOrigin(0, 0);
this.cursor.setScale(1 / 16);
this.cursor.setVisible(false);
}
update(cursorPos: Vec2, input: InputManager) {
if (input.getContext() !== 'map') return;
cursorPos = cursorPos.floor();
this.cursor.setPosition(cursorPos.x, cursorPos.y);
this.cursor.setVisible(cursorPos.x >= 0 && cursorPos.y >= 0 &&
cursorPos.x < ((this.layer?.size.x) ?? 0) && cursorPos.y < ((this.layer?.size.y) ?? 0));
if (input.mousePressed()) this.drawStartPos = cursorPos;
switch(this.editMode) {
default: break;
case 'brush':
this.drawBrush(cursorPos, input);
break;
case 'line':
this.drawLine(cursorPos, input);
break;
case 'rect':
this.drawRect(cursorPos, input);
break;
}
if (!input.mouseDown()) {
if (input.keyDown('SHIFT')) this.editMode = 'line';
else if (this.editMode === 'line') this.editMode = 'brush';
if (input.keyDown('CTRL')) this.editMode = 'rect';
else if (this.editMode === 'rect') this.editMode = 'brush';
}
if (input.mouseDown()) {
if (!this.pointerState) this.pointerState = input.mouseLeftDown() ? 'primary' : 'secondary';
}
else if (this.pointerState) {
this.pointerState = null;
if (this.drawn.length) {
this.actions.push({ type: 'tile', items: this.drawn });
this.drawn = [];
}
}
}
setLayer(layer?: MapLayer) {
this.layer = layer;
}
setActiveTileType(type: Layer) {
this.activeLayer = type;
}
setActiveTile(tile: number) {
this.activeTileset = tile;
}
activate() {
this.cursor.setVisible(true);
}
deactivate() {
this.cursor.setVisible(false);
}
private drawBrush(cursorPos: Vec2, input: InputManager) {
if (input.mouseDown()) {
const change = new Vec2(cursorPos.x - this.drawStartPos.x, cursorPos.y - this.drawStartPos.y);
const normalizeFactor = Math.sqrt(change.x * change.x + change.y * change.y) * 10;
change.x /= normalizeFactor; change.y /= normalizeFactor;
const place = new Vec2(this.drawStartPos.x, this.drawStartPos.y);
while (Math.abs(cursorPos.x - place.x) >= 0.1 || Math.abs(cursorPos.y - place.y) >= 0.1) {
this.drawTile(place.floor(), input.mouseLeftDown());
place.x += change.x; place.y += change.y;
}
this.drawTile(cursorPos, input.mouseLeftDown());
this.drawStartPos = cursorPos;
}
}
private drawLine(cursorPos: Vec2, input: InputManager) {
const a = new Vec2(this.drawStartPos.x, this.drawStartPos.y);
const b = new Vec2(cursorPos.x, cursorPos.y);
if (Math.abs(b.x - a.x) > Math.abs(b.y - a.y)) b.y = a.y;
else b.x = a.x;
this.primitives.forEach(v => v.destroy());
this.primitives = [];
if (input.mouseDown()) {
this.cursor.setPosition(b.x, b.y);
const line = this.scene.add.line(0, 0, a.x + 0.5, a.y + 0.5, b.x + 0.5, b.y + 0.5, 0xffffff, 1);
line.setOrigin(0, 0);
line.setDepth(300);
line.setLineWidth(0.03);
this.primitives.push(line);
const startSprite = this.scene.add.sprite(this.drawStartPos.x, this.drawStartPos.y, 'cursor');
startSprite.setOrigin(0, 0);
startSprite.setScale(1 / 16);
startSprite.setAlpha(0.5);
this.primitives.push(startSprite);
}
else if (this.pointerState) {
const change = new Vec2(b.x - a.x, b.y - a.y);
const normalizeFactor = Math.sqrt(change.x * change.x + change.y * change.y);
change.x /= normalizeFactor;
change.y /= normalizeFactor;
while (Math.abs(b.x - a.x) >= 1 || Math.abs(b.y - a.y) >= 1) {
this.drawTile(new Vec2(Math.floor(a.x), Math.floor(a.y)), this.pointerState === 'primary');
a.x += change.x;
a.y += change.y;
}
this.drawTile(new Vec2(b.x, b.y), this.pointerState === 'primary');
}
}
private drawRect(cursorPos: Vec2, input: InputManager) {
const a = new Vec2(Math.min(this.drawStartPos.x, cursorPos.x), Math.min(this.drawStartPos.y, cursorPos.y));
const b = new Vec2(Math.max(this.drawStartPos.x, cursorPos.x), Math.max(this.drawStartPos.y, cursorPos.y));
this.primitives.forEach(v => v.destroy());
this.primitives = [];
if (input.mouseDown()) {
if (!this.pointerState) this.drawStartPos = cursorPos;
const fac = 0.03;
this.primitives.push(this.scene.add.line(0, 0, a.x + fac, a.y + fac, b.x + 1 - fac, a.y + fac, 0xffffff, 1));
this.primitives.push(this.scene.add.line(0, 0, a.x + fac, a.y + fac / 2, a.x + fac, b.y + 1 - fac / 2, 0xffffff, 1));
this.primitives.push(this.scene.add.line(0, 0, a.x + fac, b.y + 1 - fac, b.x + 1 - fac, b.y + 1 - fac, 0xffffff, 1));
this.primitives.push(this.scene.add.line(0, 0, b.x + 1 - fac, a.y + fac / 2, b.x + 1 - fac, b.y + 1 - fac / 2, 0xffffff, 1));
this.primitives.forEach(v => {
(v as Phaser.GameObjects.Line).setLineWidth(0.03);
v.setOrigin(0, 0);
v.setDepth(300);
});
}
else if (this.pointerState) {
for (let i = a.x; i <= b.x; i++)
for (let j = a.y; j <= b.y; j++)
this.drawTile(new Vec2(i, j), this.pointerState === 'primary');
}
}
private drawTile(pos: Vec2, solid: boolean) {
if (!this.layer) return;
let tile = solid ? this.activeTileset : 0;
const lastTile = this.layer.getTile(this.activeLayer, pos);
if (this.layer.setTile(this.activeLayer, tile, pos)) {
this.drawn.push({
pos, tile: { pre: lastTile, post: tile },
layer: this.activeLayer, mapLayer: this.layer.index
});
}
}
}

View File

@ -1,7 +1,7 @@
import type InputManager from './InputManager';
import { Vec2 } from './util/Vec';
import { clamp } from './util/Helpers';
import { Vec2 } from '../util/Vec';
import { clamp } from '../util/Helpers';
/**
* Handles camera panning and zooming interactions.
@ -26,7 +26,7 @@ export default class CameraControl {
// this.camera.setScroll(-this.camera.width / 2.2, -this.camera.height / 2.2);
this.camera.setScroll(-this.camera.width / 2, -this.camera.height / 2);
input.bindScrollEvent(delta => {
input.bindScrollEvent((delta: number) => {
const lastZoom = this.zoomLevels[this.zoomLevel];
this.zoomLevel = clamp(this.zoomLevel + delta, 0, this.zoomLevels.length - 1);
const zoom = this.zoomLevels[this.zoomLevel];
@ -62,4 +62,10 @@ export default class CameraControl {
this.camera!.scrollY += (this.lastCursorScreen.y - this.cursorScreen.y) / this.camera!.zoom;
}
}
moveTo(pos: Vec2) {
this.camera!.scrollX = pos.x;
this.camera!.scrollY = pos.y;
this.camera!.zoom = this.zoomLevels[this.zoomLevel];
}
}

View File

@ -1,3 +1,5 @@
import { Vec2 } from '../util/Vec';
export type Context = 'map' | 'interface';
type ScrollEvent = ((delta: number) => boolean | void);
@ -11,13 +13,14 @@ export default class InputManager {
private leftMouseStateLast: boolean = false;
private rightMouseStateLast: boolean = false;
private middleMouseStateLast: boolean = false;
private mousePos: Vec2 = new Vec2(0, 0);
private scrollEvents: ScrollEvent[] = [];
private keys: {[key: string]: Phaser.Input.Keyboard.Key} = {};
private keysDown: {[key: string]: boolean } = {};
private keysDownLast: {[key: string]: boolean } = {};
private scrollEvents: ScrollEvent[] = [];
private focus: boolean = true;
constructor(private scene: Phaser.Scene) {}
@ -61,6 +64,11 @@ export default class InputManager {
evt.stopPropagation();
});
window.addEventListener('mousemove', (evt: MouseEvent) => {
this.mousePos = new Vec2(evt.x, evt.y);
});
const updateKeys = () => {
Object.values(this.keys).forEach(k => k.enabled = this.focus);
};
@ -103,6 +111,10 @@ export default class InputManager {
this.context = context;
}
getMousePos(): Vec2 {
return new Vec2(this.mousePos);
}
mouseDown(): boolean {
return this.leftMouseState || this.rightMouseState;
}

View File

@ -8,10 +8,10 @@ import TokenSidebar from './components/TokenSidebar';
import SidebarToggler from './components/SidebarToggler';
import Map from '../map/Map';
import InputManager from '../InputManager';
import ModeMananger from '../mode/ModeManager';
import { DrawModeKey } from '../mode/DrawMode';
import { TokenModeKey } from '../mode/TokenMode';
import InputManager from '../interact/InputManager';
import ActionManager from '../action/ActionManager';
import { ArchitectModeKey } from '../mode/ArchitectMode';
@ -115,6 +115,10 @@ export default class InterfaceRoot {
update(this.root);
}
setVisible(visible: boolean) {
this.root.setVisible(visible);
}
setSidebarOpen(open: boolean) {
this.scene.tweens.add({
targets: this.leftRoot,

View File

@ -0,0 +1,69 @@
import * as Phaser from 'phaser';
import { Vec2, Vec4 } from '../util/Vec';
const PAD = new Vec4(220, 32, 32, 48);
export default class Ping extends Phaser.GameObjects.Container {
private start: number = Date.now();
private rings: Phaser.GameObjects.Ellipse[] = [];
private arrow: Phaser.GameObjects.Polygon;
constructor(scene: Phaser.Scene, private pos: Vec2, tint: number) {
super(scene, 0, 0);
this.scene.add.existing(this);
for (let i = 0; i < 3; i++) {
const ring = this.scene.add.ellipse(pos.x, pos.y, 1, 1, tint, 1);
ring.setAlpha(0);
ring.setData('offset', 200 - i * 300);
this.add(ring);
this.rings.push(ring);
}
this.arrow = this.scene.add.polygon(0, 0, [ new Vec2(.17, .5), new Vec2(.17, 0), new Vec2(.5, 0),
new Vec2(0, -.5), new Vec2(-.5, 0), new Vec2(-.17, 0), new Vec2(-.17, .5)], tint);
this.arrow.setVisible(false);
this.arrow.setOrigin(0);
this.add(this.arrow);
}
update() {
const delta = Date.now() - this.start;
this.rings.forEach(r => {
const offset = r.getData('offset');
const time = delta + offset;
if (time < 0) return;
const size = Math.pow(time, 2) / 100000;
const opacity = Math.max(1 - ((time - 100) / 600), 0);
r.setScale(size);
r.setAlpha(opacity);
});
const camera = this.scene.cameras.main;
const center = new Vec2(camera.scrollX + camera.width / 2, camera.scrollY + camera.height / 2);
const bounds = new Vec4(
center.x - camera.displayWidth / 2 + PAD.x / camera.zoom,
center.y - camera.displayHeight / 2 + PAD.y / camera.zoom,
center.x + camera.displayWidth / 2 - PAD.z / camera.zoom,
center.y + camera.displayHeight / 2 - PAD.w / camera.zoom);
const diff = new Vec2(this.pos.x - center.x, this.pos.y - center.y).normalize();
const angle = Math.atan2(diff.y, diff.x);
const offscreen = this.pos.x < bounds.x || this.pos.y < bounds.y || this.pos.x > bounds.z || this.pos.y > bounds.w;
this.arrow.setVisible(offscreen);
this.arrow.setRotation(angle + Math.PI / 2);
this.arrow.setScale(48 / camera.zoom);
this.arrow.setPosition(center.x + diff.x * 16, center.y + diff.y * 16);
}
shouldDie(): boolean {
return Date.now() - this.start > 2000;
}
}

View File

@ -0,0 +1,76 @@
import * as Phaser from 'phaser';
import * as IO from 'socket.io-client';
import InputManager from '../interact/InputManager';
import Ping from './Ping';
import { Vec2 } from '../util/Vec';
import * as Color from '../../../../common/Color';
const TIMEOUT = 250;
export default class PingHandler {
private socket: IO.Socket = null as any;
private scene: Phaser.Scene = null as any;
private input: InputManager = null as any;
private clickTime: number | undefined;
private mouseDelta: Vec2 | undefined;
private lastMousePos: Vec2 | undefined;
private pings: Ping[] = [];
init(scene: Phaser.Scene, input: InputManager, socket: IO.Socket) {
this.scene = scene;
this.input = input;
this.socket = socket;
socket.on('ping', ({ pos, color }: { pos: Vec2; color: number }) => this.createPing(pos, color, false));
}
update(cursorWorld: Vec2) {
const now = Date.now();
if (this.input.mouseMiddlePressed()) {
this.clickTime = now;
this.mouseDelta = new Vec2();
this.lastMousePos = this.input.getMousePos();
}
if (this.input.mouseMiddleDown() && this.clickTime) {
const mousePos = this.input.getMousePos();
const delta = new Vec2(this.lastMousePos!.x - mousePos.x, this.lastMousePos!.y - mousePos.y);
this.mouseDelta = new Vec2(this.mouseDelta!.x + Math.abs(delta.x), this.mouseDelta!.y + Math.abs(delta.y));
this.lastMousePos = mousePos;
if (this.clickTime! < now - TIMEOUT || this.mouseDelta.length() > 16) this.reset();
}
for (let i = 0; i < this.pings.length; i++) {
const p = this.pings[i];
p.update();
if (p.shouldDie()) {
p.destroy();
this.pings.splice(i, 1);
i--;
}
};
if (this.input.mouseMiddleReleased() && this.clickTime) {
this.createPing(cursorWorld, Color.HSVToInt(this.scene.data.get('player_tint')));
this.reset();
}
}
private createPing(pos: Vec2, color: number = 0xffffff, networked: boolean = true) {
this.pings.push(new Ping(this.scene, pos, color));
if (networked) this.socket.emit('ping', { pos, color });
}
private reset() {
this.clickTime = undefined;
this.mouseDelta = undefined;
this.lastMousePos = undefined;
}
}

View File

@ -0,0 +1,18 @@
.DrawToolbar-Color
width: 60px
position: relative
.InputColor.Full
position: absolute
top: -18px
left: -18px
right: -18px
bottom: -18px
.InputColor-ColorIndicator
top: 12px
left: 12px
right: 12px
bottom: 12px
border-radius: 0
border: 3px solid white

View File

@ -5,27 +5,37 @@ import './DrawToolbar.sass';
import Button from '../../../components/Button';
import ButtonGroup from '../../../components/ButtonGroup';
import Color from '../../../components/input/fields/InputColor';
import DrawMode, { DrawModeTool, DrawModeEvent } from '../../mode/DrawMode';
import DrawMode, { DrawModeTool, DrawModeToolEvent, DrawModeColorEvent } from '../../mode/DrawMode';
interface Props {
mode: DrawMode;
}
export default function DrawToolbar(props: Props) {
const [ color, setColor ] = useState<any>(props.mode.getColor());
const [ tool, setTool ] = useState<DrawModeTool>(props.mode.getTool());
useEffect(() => {
const actionCb = (evt: DrawModeEvent) => {
setTool(evt.currentTool);
};
const toolCb = (evt: DrawModeToolEvent) => setTool(evt.currentTool);
const colorCb = (evt: DrawModeColorEvent) => setColor(evt.currentColor);
props.mode.bind(actionCb);
return () => props.mode.unbind(actionCb);
props.mode.tool.bind(toolCb);
props.mode.color.bind(colorCb);
return () => {
props.mode.tool.unbind(toolCb);
props.mode.color.unbind(colorCb);
};
}, [ props.mode ]);
return (
<Preact.Fragment>
<div class='Button DrawToolbar-Color'>
<Color full showHex value={color} setValue={c => props.mode.setColor(c)} />
</div>
<div class='Toolbar-Spacer' />
<ButtonGroup>
<Button icon='line' alt='Draw Line Art' onClick={() => props.mode.setTool('line')}
noFocus={true} inactive={tool !== 'line'} />

View File

@ -1,3 +1,7 @@
@use '../../../style/text'
@use '../../../style/slice'
@use '../../../style/def' as *
.LayerManager
display: grid
grid-gap: 6px
@ -8,7 +12,15 @@
width: 230px
height: auto
transition: width 0.2s, right 0.2s
&.Collapsed
width: 41px
right: -6px
.LayerManager-Layer
@include text.line_clamp
outline: 0
border: none
font: inherit

View File

@ -0,0 +1,99 @@
import * as Preact from 'preact';
import { bind } from './PreactComponent';
import { useState } from 'preact/hooks';
import './LayerManager.sass';
import LayerMenu from './LayerMenu';
import Button from '../../../components/Button';
import ButtonGroup from '../../../components/ButtonGroup';
import Map from '../../map/Map';
import MapLayer from '../../map/MapLayer';
interface LayerProps {
layer: MapLayer;
active: boolean;
onClick: () => void;
onEdit: () => void;
}
function Layer({ layer, active, onEdit, onClick }: LayerProps) {
const handleFocus = (e: any) => e.target.blur();
const handleEdit = () => {
onClick();
onEdit();
};
return (
<button class={('LayerManager-Layer ' + (active ? 'Active' : '')).trim()}
onClick={onClick} onContextMenu={handleEdit} onFocus={handleFocus}>
<p>{layer.name}</p>
</button>
);
}
interface Props {
map: Map;
}
export default bind<Props>(function LayerManager({ map }: Props) {
const [ editing, setEditing ] = useState<boolean>(false);
const [ collapsed, setCollapsed ] = useState<boolean>(true);
const [ layers, setLayers ] = useState<MapLayer[]>([ ...map.getLayers() ]);
const [ activeLayer, setActiveLayer ] = useState<number | undefined>(map.getActiveLayer()?.index);
const handleCollapse = () => {
setCollapsed(!collapsed);
if (!collapsed) setEditing(false);
};
const handleAddLayer = () => {
map.addLayer();
setLayers([ ...map.getLayers() ]);
setActiveLayer(map.getActiveLayer()?.index);
};
const handleRemoveLayer = () => {
if (!map.getActiveLayer()) return;
map.removeLayer(map.getActiveLayer()!.index);
setLayers([ ...map.getLayers() ]);
setActiveLayer(map.getActiveLayer()?.index);
};
const handleClickLayer = (index: number) => {
map.setActiveLayer(index);
setActiveLayer(index);
};
const handleEditLayer = () => {
setEditing(!editing);
if (!editing) setCollapsed(false);
};
const active = map.getActiveLayer();
return (
<div class={('LayerManager ' + (collapsed ? 'Collapsed' : '')).trim()}>
{editing && active && <LayerMenu
name={active.name}
canMoveUp={active.index > 0}
canMoveDown={active.index < map.getLayers().length - 1}
handleMove={() => {/* Not yet implemented.*/}}
/>}
<ButtonGroup>
{!collapsed && <Preact.Fragment>
<Button icon='add' alt='Add Layer' onClick={handleAddLayer} noFocus />
<Button icon='remove' alt='Remove Layer' onClick={handleRemoveLayer} disabled={activeLayer === undefined} noFocus />
<Button icon='edit' alt='Edit Layer' onClick={handleEditLayer} disabled={activeLayer === undefined} noFocus />
</Preact.Fragment>}
<Button icon={collapsed ? 'nav_left' : 'nav_right'} alt='Collapse Panel'
noFocus onClick={handleCollapse} />
</ButtonGroup>
{layers.map(l => <Layer layer={l} active={activeLayer === l.index}
onClick={() => handleClickLayer(l.index)} onEdit={handleEditLayer} />)}
</div>
);
});

View File

@ -0,0 +1,22 @@
@use '../../../style/text'
@use '../../../style/slice'
@use '../../../style/def' as *
.LayerMenu
position: absolute
top: 0
right: calc(100% + 6px)
width: 400px
.LayerMenu-Top
display: flex
gap: 6px
.LayerMenu-Actions
flex-grow: 0
.Button
width: 60px
h1
margin: 0px 0

View File

@ -0,0 +1,32 @@
import * as Preact from 'preact';
import './LayerMenu.sass';
import Button from '../../../components/Button';
import ButtonGroup from '../../../components/ButtonGroup';
import { Text as InputText } from '../../../components/input/Input';
interface Props {
name: string;
canMoveUp: boolean;
canMoveDown: boolean;
handleMove: (off: number) => void;
}
export default function TokenCard(props: Props) {
return (
<div class='LayerMenu'>
<div class='LayerMenu-Top'>
<InputText value={props.name} setValue={() => {/* Not yet implemented.*/}} />
<div class='LayerMenu-Actions'>
<ButtonGroup>
<Button icon='nav_up' alt='Move up' disabled={!props.canMoveUp} />
<Button icon='nav_down' alt='Move down' disabled={!props.canMoveDown} />
</ButtonGroup>
</div>
</div>
</div>
);
}

View File

@ -1,7 +1,7 @@
import * as Phaser from 'phaser';
import Component from './Component';
import InputManager from '../../InputManager';
import InputManager from '../../interact/InputManager';
import { clamp } from '../../util/Helpers';
import { Vec2, Vec4 } from '../../util/Vec';

View File

@ -3,7 +3,7 @@ import type * as Phaser from 'phaser';
import Component from './Component';
import type InterfaceRoot from '../InterfaceRoot';
import type InputManager from '../../InputManager';
import type InputManager from '../../interact/InputManager';
export default class SidebarToggler extends Component {
private button: Phaser.GameObjects.Sprite;

View File

@ -4,8 +4,8 @@ import Sidebar from './Sidebar';
import Map from '../../map/Map';
import ModeManager from '../../mode/ModeManager';
import type InputManager from '../../InputManager';
import ArchitectMode from '../../mode/ArchitectMode';
import type InputManager from '../../interact/InputManager';
import { Asset } from '../../util/Asset';
@ -52,17 +52,19 @@ export default class TileSidebar extends Sidebar {
}
elemClick(x: number, y: number): void {
const controller = (this.mode.active as ArchitectMode).controller;
if (!controller) return;
if (y < 4) {
(this.mode.active as ArchitectMode).activeTileset = this.map.tileStore.indices[this.walls[x + (y - 1) * 3]];
(this.mode.active as ArchitectMode).activeLayer = 'wall';
controller.setActiveTile(this.map.tileStore.indices[this.walls[x + (y - 1) * 3]]);
controller.setActiveTileType('wall');
}
else if (y < 8) {
(this.mode.active as ArchitectMode).activeTileset = this.map.tileStore.indices[this.floors[x + (y - 5) * 3]];
(this.mode.active as ArchitectMode).activeLayer = 'floor';
controller.setActiveTile(this.map.tileStore.indices[this.floors[x + (y - 5) * 3]]);
controller.setActiveTileType('floor');
}
else {
(this.mode.active as ArchitectMode).activeTileset = this.map.tileStore.indices[this.details[x + (y - 9) * 3]];
(this.mode.active as ArchitectMode).activeLayer = 'detail';
controller.setActiveTile(this.map.tileStore.indices[this.details[x + (y - 9) * 3]]);
controller.setActiveTileType('detail');
}
}

View File

@ -10,6 +10,7 @@
height: 450px
top: 0px
overflow: visible
pointer-events: initial
transition: top $t-fast
@ -61,6 +62,8 @@
.TokenCard-Inner
@include slice.slice_invert(3, 4px)
overflow: visible
.TokenCard-Top
display: grid
grid-gap: 9px

View File

@ -23,7 +23,10 @@ export default function TokenCard(props: TokenCardProps) {
// icon.dimensions!.x / icon.tileSize;
const handleAddSlider = () => {
props.setProps({ sliders: [ ...props.sliders, { name: 'Untitled', max: 10, current: 10, icon: 1 } ]});
props.setProps({ sliders: [
...props.sliders,
{ name: 'Untitled', color: { h: .95, s: .59, v: .94 }, max: 10, current: 10, icon: 1 }
]});
};
const handleUpdateSlider = (ind: number, data: Partial<TokenSliderData>) => {

View File

@ -5,7 +5,7 @@ import Sidebar from './Sidebar';
import Token from '../../map/token/Token';
import TokenMode from '../../mode/TokenMode';
import ModeManager from '../../mode/ModeManager';
import type InputManager from '../../InputManager';
import type InputManager from '../../interact/InputManager';
import { Vec2 } from '../../util/Vec';
import { Asset } from '../../util/Asset';
@ -72,8 +72,7 @@ export default class TokenSidebar extends Sidebar {
if (x === 0) this.backgrounds[y].setFrame(0);
let token = new Token(this.scene, '', 50, new Vec2(4 + x * 21, 4 + y * 21), sprite);
token.setScale(1);
token.shadow?.destroy();
token.setScale(16);
this.sprites.push(token);
this.add(token);

View File

@ -3,10 +3,17 @@
.TokenSlider
display: flex
position: relative
gap: 6px
.TokenSlider-IconWrap
@extend .slice_highlight
outline: 0
padding: 0
&:hover, &:focus-visible
filter: brightness(120%)
.TokenSlider-Icon
@include slice.slice_invert
@ -14,10 +21,10 @@
height: 36px
border-radius: 0
background-size: 900%
background-size: 1300%
image-rendering: crisp-edges
image-rendering: pixelated
background-image: url(/app/static/icon/slider_icons.png)
background-image: url(/app/static/editor/slider_icons.png)
.TokenSlider-Slider
@extend .slice_outline_white
@ -72,6 +79,41 @@
opacity: .6
padding: 3px
.TokenSlider-Options
@extend .slice_background
z-index: 1
position: absolute
bottom: 14 * 3px
width: calc(100% + 12px)
left: -6px
overflow: visible
.TokenSlider-OptionsInner
@include slice.slice_invert
width: 100%
overflow: visible
h3
margin: 12px 6px 6px 6px
font-size: 25px
.ColorPicker
width: calc(100% + 18px * 2)
.TokenSlider-IconSelector
display: grid
margin: 0
padding: 6px
grid-gap: 3px
width: calc(100% + 18px * 2)
grid-template-columns: repeat(auto-fill, 36px)
grid-template-rows: 36px
list-style-type: none
.SlideNumericInput-Tester
opacity: 0 !important
top: -10000px !important

View File

@ -3,9 +3,12 @@ import { useState, useEffect, useLayoutEffect, useRef } from 'preact/hooks';
import './TokenSlider.sass';
import ColorPicker from '../../../components/input/ColorPicker';
import { TokenSliderData } from '../../map/token/Token';
import { clamp } from '../../util/Helpers';
import * as Color from '../../../../../common/Color';
function SliderNumericInput(props: { min: number; max: number;
value: number; setValue: (val: number) => void; class?: string; }) {
@ -51,20 +54,31 @@ interface Props extends TokenSliderData {
}
export default function TokenSlider(props: Props) {
const [ showOptions, setShowOptions ] = useState<boolean>(false);
const handleChangeName = (e: any) => {
const name: string = e.target.value;
props.setProps({ name });
};
let icons: Preact.VNode[] = [];
for (let i = 0; i < 16; i++) {
icons.push(
<button class='TokenSlider-IconWrap' onClick={() => props.setProps({ icon: i })}>
<div class='TokenSlider-Icon' style={{ backgroundPosition: `${i * (100 / 12)}% 0`}} />
</button>
);
}
return (
<div class='TokenSlider'>
<div class='TokenSlider-IconWrap'>
<div class='TokenSlider-Icon' style={{ backgroundPosition: `${(props.icon || 0) * (100 / 8)}% 0`}} />
</div>
<button class='TokenSlider-IconWrap' onClick={() => setShowOptions(o => !o)}>
<div class='TokenSlider-Icon' style={{ backgroundPosition: `${(props.icon || 0) * (100 / 12)}% 0`}} />
</button>
<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-Bar' style={{ backgroundColor: Color.HSVToHex(props.color),
width: 'calc(' + Math.min((props.current - (props.min || 0)) / props.max, 1) * 100 + '% - 6px)'}}/>
<div class='TokenSlider-BarContent'>
<input class='TokenSlider-Input TokenSlider-BarText TokenSlider-Title'
value={props.name} onChange={handleChangeName}/>
@ -77,6 +91,17 @@ export default function TokenSlider(props: Props) {
</div>
</div>
</div>
{showOptions && <div class='TokenSlider-Options'>
<div class='TokenSlider-OptionsInner'>
<h3>{props.name}</h3>
<ColorPicker value={props.color} setValue={color => props.setProps({ color })} />
<ul class='TokenSlider-IconSelector'>
{icons}
</ul>
</div>
</div>}
</div>
);
}

View File

@ -61,19 +61,16 @@ export default bind<Props>(function LayerManager(props: Props) {
<div class='Toolbar-Separator' />
{(mode === ArchitectModeKey || mode === TokenModeKey) &&
<Preact.Fragment>
<ButtonGroup>
<Button icon='undo' alt='Undo' noFocus={true}
disabled={!hasPrev} onClick={() => props.actions.prev()} />
<Button icon='redo' alt='Redo' noFocus={true}
disabled={!hasNext} onClick={() => props.actions.next()} />
</ButtonGroup>
<div class='Toolbar-Spacer' />
</Preact.Fragment>
}
<ButtonGroup>
<Button icon='undo' alt='Undo' noFocus={true}
disabled={!hasPrev} onClick={() => props.actions.prev()} />
<Button icon='redo' alt='Redo' noFocus={true}
disabled={!hasNext} onClick={() => props.actions.next()} />
</ButtonGroup>
{/* {(mode !== DrawModeKey) &&
<div class='Toolbar-Spacer' />
{/* (mode !== DrawModeKey) &&
<Preact.Fragment>
<ButtonGroup>
<Button icon='ruler' alt='Measure Distance'
@ -88,7 +85,7 @@ export default bind<Props>(function LayerManager(props: Props) {
</ButtonGroup>
<div class='Toolbar-Spacer' />
</Preact.Fragment>
}*/}
*/}
{mode === TokenModeKey &&
<Preact.Fragment>

View File

@ -1,66 +0,0 @@
import * as Preact from 'preact';
import { bind } from './PreactComponent';
import { useState } from 'preact/hooks';
import './LayerManager.sass';
import Button from '../../../components/Button';
import ButtonGroup from '../../../components/ButtonGroup';
import Map from '../../map/Map';
import MapLayer from '../../map/MapLayer';
interface LayerProps {
layer: MapLayer;
active: boolean;
onClick: () => void;
}
function Layer({ layer, active, onClick }: LayerProps) {
const handleFocus = (e: any) => e.target.blur();
return (
<button class={('LayerManager-Layer ' + (active ? 'Active' : '')).trim()} onClick={onClick} onFocus={handleFocus}>
<p>{layer.name}</p>
</button>
);
}
interface Props {
map: Map;
}
export default bind<Props>(function LayerManager({ map }: Props) {
const [ layers, setLayers ] = useState<MapLayer[]>([ ...map.getLayers() ]);
const [ activeLayer, setActiveLayer ] = useState<number | undefined>(map.getActiveLayer()?.index);
const handleAddLayer = () => {
map.addLayer();
setLayers([ ...map.getLayers() ]);
setActiveLayer(map.getActiveLayer()?.index);
};
const handleRemoveLayer = () => {
// map.removeLayer(i);
setLayers([ ...map.getLayers() ]);
setActiveLayer(map.getActiveLayer()?.index);
};
const handleClickLayer = (index: number) => {
map.setActiveLayer(index);
setActiveLayer(index);
};
return (
<div class='LayerManager'>
<ButtonGroup>
<Button icon='nav_down' alt='Move layer down' disabled={activeLayer === 0} noFocus />
<Button icon='nav_up' alt='Move layer up' disabled={activeLayer === layers.length - 1} noFocus />
<Button icon='remove' alt='Remove layer' onClick={handleRemoveLayer} noFocus />
<Button icon='add' alt='Add layer' onClick={handleAddLayer} noFocus />
</ButtonGroup>
{layers.map(l => <Layer layer={l} active={activeLayer === l.index} onClick={() => handleClickLayer(l.index)} />)}
</div>
);
});

View File

@ -0,0 +1,116 @@
import * as Phaser from 'phaser';
import MapLayer from './MapLayer';
import { Vec2 } from '../util/Vec';
export const TILE_SIZE = 16;
export const CHUNK_SIZE = 32;
export const DIRTY_LIMIT = (CHUNK_SIZE * CHUNK_SIZE) / 2;
/**
* A highlight / checkerboard map overlay.
*/
export default class HighlightMapChunk extends Phaser.GameObjects.RenderTexture {
private dirtyList: Vec2[] = [];
private fullyDirty: boolean = true;
private tile: Phaser.GameObjects.Sprite;
constructor(scene: Phaser.Scene, private pos: Vec2, readonly layer: MapLayer) {
super(scene, CHUNK_SIZE * pos.x - 2 / TILE_SIZE, CHUNK_SIZE * pos.y - 2 / TILE_SIZE,
CHUNK_SIZE * TILE_SIZE + 4, CHUNK_SIZE * TILE_SIZE + 4);
this.setScale(1 / TILE_SIZE);
this.setOrigin(0, 0);
this.tile = new Phaser.GameObjects.Sprite(this.scene, 0, 0, '');
this.tile.setVisible(false);
this.tile.setOrigin(0);
scene.add.existing(this);
}
/**
* Indicates that a position on the chunk is dirty so it will be re-rendered.
*
* @param {Vec2} pos - The position that is dirtied.
*/
setDirty(pos: Vec2): void {
if (!this.fullyDirty) {
for (let v of this.dirtyList) if (v.equals(pos)) return;
this.dirtyList.push(pos);
if (this.dirtyList.length > DIRTY_LIMIT) {
this.fullyDirty = true;
this.dirtyList = [];
}
}
}
/**
* Redraws all dirty tiles on the chunk.
*
* @returns {boolean} - A boolean indicating if tiles have changed since the last render.
*/
redraw(): boolean {
// const time = Date.now();
this.tile.setVisible(true);
if (this.fullyDirty) {
for (let i = 0; i < CHUNK_SIZE * CHUNK_SIZE; i++) {
let x = i % CHUNK_SIZE;
let y = Math.floor(i / CHUNK_SIZE);
if (x + this.pos.x * CHUNK_SIZE >= this.layer.size.x ||
y + this.pos.y * CHUNK_SIZE >= this.layer.size.y) continue;
this.drawTile(x, y);
}
this.fullyDirty = false;
// console.log('MapChunk took', (Date.now() - time), 'ms');
return true;
}
if (this.dirtyList.length === 0) return false;
for (let elem of this.dirtyList) this.drawTile(elem.x, elem.y);
this.dirtyList = [];
// console.log('MapChunk took', (Date.now() - time), 'ms');
this.tile.setVisible(false);
return true;
}
/**
* Redraws the tile at the specified position,
* based on the current data on the MapLayer.
*
* @param {number} x - The x position to draw at.
* @param {number} y - The y position to draw at.
*/
private drawTile(x: number, y: number): void {
const pos = new Vec2(x + this.pos.x * CHUNK_SIZE, y + this.pos.y * CHUNK_SIZE);
let highlightTint = this.layer.getTile('wall', pos);
let highlightIndex = this.layer.getTileIndex('wall', pos);
this.erase('erase_tile', x * TILE_SIZE + 2, y * TILE_SIZE + 2);
if (highlightTint > 0) {
this.tile.setPosition(x * TILE_SIZE + 2, y * TILE_SIZE + 2);
this.tile.setTexture('user_highlight', highlightIndex);
this.tile.setTint(highlightTint);
this.draw(this.tile);
}
if ((x % 2 === 0 && y % 2 === 0) || (x % 2 !== 0 && y % 2 !== 0))
this.drawFrame('grid_tile', 0, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
}
}

View File

@ -3,8 +3,9 @@ import * as Phaser from 'phaser';
import MapLayer from './MapLayer';
import TileStore from './TileStore';
import * as MapSaver from './MapSaver';
import MapChunk, { CHUNK_SIZE } from './MapChunk';
import TokenManager from './token/TokenManager';
import MapChunk, { CHUNK_SIZE } from './MapChunk';
import HighlightMapChunk from './HighlightMapChunk';
import { Vec2 } from '../util/Vec';
import { Asset } from '../util/Asset';
@ -22,14 +23,16 @@ export default class Map {
tokens: TokenManager = new TokenManager();
private layers: MapLayer[] = [];
private highlightLayer?: MapLayer;
private activeLayer?: MapLayer = undefined;
private scene: Phaser.Scene = undefined as any;
private chunks: MapChunk[][][] = [];
private highlights: HighlightMapChunk[][] = [];
init(scene: Phaser.Scene, assets: Asset[]) {
this.scene = scene;
this.tokens.init(scene);
this.tileStore.init(scene.textures, assets);
}
@ -50,6 +53,13 @@ export default class Map {
}
}
}
for (let chunkRow of this.highlights) {
for (let chunk of chunkRow) {
if (Date.now() - start > 4) return;
chunk.redraw();
}
}
}
@ -67,6 +77,7 @@ export default class Map {
*/
getLayer(layer: number): MapLayer | undefined {
if (layer === -1) return this.highlightLayer;
return this.layers[layer];
}
@ -80,6 +91,15 @@ export default class Map {
}
/**
* Gets the highlight layer.
*/
getHighlightLayer(): MapLayer | undefined {
return this.highlightLayer;
}
/**
* Sets the active layer to the layer or index specified.
*/
@ -87,7 +107,7 @@ export default class Map {
setActiveLayer(l: MapLayer | number) {
if (l instanceof MapLayer) l = l.index;
this.activeLayer = this.layers[l];
this.chunks.forEach((a, i) => a.forEach(cA => cA.forEach(c => c.setShadow(i > l))));
this.updateLayerVisibility();
}
@ -110,6 +130,21 @@ export default class Map {
}
/**
* Removes a map layer at the specfied index.
*
* @param {number} index - the index that the layer should be removed at.
*/
removeLayer(index: number) {
const layer = this.layers[index];
this.layers.splice(index, 1);
for (let i = index; i < this.layers.length; i++) this.layers[i].index = i;
if (this.activeLayer === layer) this.setActiveLayer(this.layers[Math.min(Math.max(index, 0), this.layers.length - 1)]);
this.updateMapChunks();
}
/**
* Returns a serialized map string representing the map.
*
@ -140,9 +175,12 @@ export default class Map {
l.init(this.handleDirty.bind(this, l.index));
this.createMapChunks(l);
});
this.createHighlightLayer();
this.activeLayer = this.layers[0];
}
/**
* Creates a visual representation of the map.
*
@ -164,6 +202,58 @@ export default class Map {
}
/**
* Refreshes the highlight chunks for the map.
*
* @param {Phaser.Scene} scene - The scene to add the chunks to.
*/
private createHighlightLayer() {
this.highlightLayer = new MapLayer(-1, this.size);
this.highlightLayer.init((x: number, y: number) =>
this.highlights[Math.floor(y / CHUNK_SIZE)][Math.floor(x / CHUNK_SIZE)].setDirty(new Vec2(x % CHUNK_SIZE, y % CHUNK_SIZE)));
this.highlights.forEach(cR => cR.forEach(c => c.destroy()));
this.highlights = [];
for (let i = 0; i < Math.ceil(this.size.y / CHUNK_SIZE); i++) {
this.highlights[i] = [];
for (let j = 0; j < Math.ceil(this.size.x / CHUNK_SIZE); j++) {
const chunk = new HighlightMapChunk(this.scene, new Vec2(j, i), this.highlightLayer);
this.highlights[i][j] = chunk;
}
}
}
/**
* Removes orphaned MapChunks and updates their depth.
*/
private updateMapChunks() {
for (let i = 0; i < this.chunks.length; i++) {
const chunks = this.chunks[i];
if (!this.layers.includes(chunks[0][0].layer)) {
// Kill the chunks, the layer is dead.
chunks.forEach(cA => cA.forEach(c => c.destroy()));
this.chunks.splice(i--, 1);
}
else chunks.forEach(cA => cA.forEach(c => c.updateDepth()));
}
}
/**
* Shows or hides layers based on the active layer.
*/
private updateLayerVisibility() {
const index = (this.activeLayer?.index) ?? 0;
this.chunks.forEach((a, i) => a.forEach(cA => cA.forEach(c => c.setShadow(i > index))));
}
/**
* Marks a position as dirty in the relevant map chunk.
* Passed down to MapLayer instances.

View File

@ -17,13 +17,18 @@ export const DIRTY_LIMIT = (CHUNK_SIZE * CHUNK_SIZE) / 2;
export default class MapChunk extends Phaser.GameObjects.RenderTexture {
private dirtyList: Vec2[] = [];
private fullyDirty: boolean = true;
private tile: Phaser.GameObjects.Sprite;
constructor(scene: Phaser.Scene, private pos: Vec2, private layer: MapLayer, private tileStore: TileStore) {
constructor(scene: Phaser.Scene, private pos: Vec2, readonly layer: MapLayer, private tileStore: TileStore) {
super(scene, CHUNK_SIZE * pos.x - 2 / TILE_SIZE, CHUNK_SIZE * pos.y - 2 / TILE_SIZE,
CHUNK_SIZE * TILE_SIZE + 4, CHUNK_SIZE * TILE_SIZE + 4);
this.setScale(1 / TILE_SIZE);
this.setOrigin(0, 0);
this.setDepth(this.layer.index * 25);
this.updateDepth();
this.tile = new Phaser.GameObjects.Sprite(this.scene, 0, 0, '');
this.tile.setVisible(false);
this.tile.setOrigin(0);
scene.add.existing(this);
}
@ -58,6 +63,15 @@ export default class MapChunk extends Phaser.GameObjects.RenderTexture {
}
/**
* Updates the depth of the chunk based on the layer's index.
*/
updateDepth() {
this.setDepth(-1000 + this.layer.index * 25);
}
/**
* Redraws all dirty tiles on the chunk.
*
@ -67,6 +81,7 @@ export default class MapChunk extends Phaser.GameObjects.RenderTexture {
redraw(): boolean {
// const time = Date.now();
this.tile.setVisible(true);
if (this.fullyDirty) {
for (let i = 0; i < CHUNK_SIZE * CHUNK_SIZE; i++) {
let x = i % CHUNK_SIZE;
@ -89,6 +104,7 @@ export default class MapChunk extends Phaser.GameObjects.RenderTexture {
this.dirtyList = [];
// console.log('MapChunk took', (Date.now() - time), 'ms');
this.tile.setVisible(false);
return true;
}
@ -113,141 +129,26 @@ export default class MapChunk extends Phaser.GameObjects.RenderTexture {
let detailTile = this.layer.getTile('detail', pos);
let detailTileIndex = this.layer.getTileIndex('detail', pos);
if (floorTile > 0)
this.drawFrame(this.tileStore.floorTiles[floorTile].identifier, floorTileIndex, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
if (floorTile > 0) {
this.tile.setPosition(x * TILE_SIZE + 2, y * TILE_SIZE + 2);
this.tile.setTexture(this.tileStore.floorTiles[floorTile].identifier, floorTileIndex);
this.draw(this.tile);
}
else {
this.erase('erase_tile', x * TILE_SIZE + 2, y * TILE_SIZE + 2);
if (wallTile === 0 && detailTile === 0) return;
}
if (detailTile > 0)
this.drawFrame(this.tileStore.detailTiles[detailTile].identifier, detailTileIndex, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
if (detailTile > 0) {
this.tile.setPosition(x * TILE_SIZE + 2, y * TILE_SIZE + 2);
this.tile.setTexture(this.tileStore.detailTiles[detailTile].identifier, detailTileIndex);
this.draw(this.tile);
}
if (wallTile > 0)
this.drawFrame(this.tileStore.wallTiles[wallTile].identifier, wallTileIndex, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
if ((x % 2 === 0 && y % 2 === 0) || (x % 2 !== 0 && y % 2 !== 0))
this.drawFrame('grid_tile', 0, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
if (wallTile > 0) {
this.tile.setPosition(x * TILE_SIZE + 2, y * TILE_SIZE + 2);
this.tile.setTexture(this.tileStore.wallTiles[wallTile].identifier, wallTileIndex);
this.draw(this.tile);
}
}
}
// function createCanvas(textures: Phaser.Textures.TextureManager): Phaser.Textures.CanvasTexture {
// const size = new Vec2(CHUNK_SIZE * TILE_SIZE + 4, CHUNK_SIZE * TILE_SIZE + 4);
// const canvas = document.createElement('canvas');
// canvas.width = size.x;
// canvas.height = size.y;
// return textures.addCanvas('', canvas, true);
// }
// /**
// * A visual representation of a chunk of a MapLayer.
// */
// export default class MapChunk extends Phaser.GameObjects.Image {
// private canvas: Phaser.Textures.CanvasTexture;
// private dirtyList: Vec2[] = [];
// private fullyDirty: boolean = true;
// constructor(scene: Phaser.Scene, private pos: Vec2, private layer: MapLayer, private tileStore: TileStore) {
// super(scene, CHUNK_SIZE * pos.x - 2 / TILE_SIZE, CHUNK_SIZE * pos.y - 2 / TILE_SIZE, createCanvas(scene.textures));
// this.scene.add.existing(this);
// this.setScale(1 / TILE_SIZE);
// this.setOrigin(0, 0);
// this.canvas = (this.texture as Phaser.Textures.CanvasTexture);
// }
// /**
// * Indicates that a position on the chunk is dirty so it will be re-rendered.
// *
// * @param {Vec2} pos - The position that is dirtied.
// */
// setDirty(pos: Vec2): void {
// if (!this.fullyDirty) {
// for (let v of this.dirtyList) if (v.equals(pos)) return;
// this.dirtyList.push(pos);
// if (this.dirtyList.length > DIRTY_LIMIT) {
// this.fullyDirty = true;
// this.dirtyList = [];
// }
// }
// }
// /**
// * Redraws all dirty tiles on the chunk.
// *
// * @returns {boolean} - A boolean indicating if tiles have changed since the last render.
// */
// redraw(): boolean {
// const time = Date.now();
// if (this.fullyDirty) {
// for (let i = 0; i < CHUNK_SIZE * CHUNK_SIZE; i++) {
// let x = i % CHUNK_SIZE;
// let y = Math.floor(i / CHUNK_SIZE);
// if (x + this.pos.x * CHUNK_SIZE >= this.layer.size.x ||
// y + this.pos.y * CHUNK_SIZE >= this.layer.size.y) continue;
// this.drawTile(x, y);
// }
// this.fullyDirty = false;
// console.log('MapChunk took', (Date.now() - time), 'ms');
// return true;
// }
// if (this.dirtyList.length === 0) return false;
// for (let elem of this.dirtyList) this.drawTile(elem.x, elem.y);
// this.dirtyList = [];
// console.log('MapChunk took', (Date.now() - time), 'ms');
// return true;
// }
// /**
// * Redraws the tile at the specified position,
// * based on the current data on the MapLayer.
// *
// * @param {number} x - The x position to draw at.
// * @param {number} y - The y position to draw at.
// */
// private drawTile(x: number, y: number): void {
// let mX = x + this.pos.x * CHUNK_SIZE;
// let mY = y + this.pos.y * CHUNK_SIZE;
// let wallTile = this.layer.getTile('wall', mX, mY);
// let wallTileIndex = this.layer.getTileIndex('wall', mX, mY);
// let floorTile = this.layer.getTile('floor', mX, mY);
// let floorTileIndex = this.layer.getTileIndex('floor', mX, mY);
// let detailTile = this.layer.getTile('detail', mX, mY);
// let detailTileIndex = this.layer.getTileIndex('detail', mX, mY);
// if (floorTile > 0)
// this.canvas.drawFrame(this.tileStore.floorTiles[floorTile].identifier, floorTileIndex, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
// else {
// this.canvas.context.clearRect(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE, TILE_SIZE);
// // this.erase('erase_tile', x * TILE_SIZE + 2, y * TILE_SIZE + 2);
// if (wallTile === 0 && detailTile === 0) return;
// }
// if (detailTile > 0)
// this.canvas.drawFrame(this.tileStore.detailTiles[detailTile].identifier, detailTileIndex, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
// if (wallTile > 0)
// this.canvas.drawFrame(this.tileStore.wallTiles[wallTile].identifier, wallTileIndex, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
// if ((x % 2 === 0 && y % 2 === 0) || (x % 2 !== 0 && y % 2 !== 0))
// this.canvas.drawFrame('grid_tile', 0, x * TILE_SIZE + 2, y * TILE_SIZE + 2);
// }
// }

View File

@ -44,7 +44,7 @@ export default class MapLayer {
private data: { [ key in Layer ]: LayerData } = {
wall: { tiles: [], tilesets: [] }, floor: { tiles: [], tilesets: [] }, detail: { tiles: [], tilesets: [] } };
constructor(readonly index: number, public size: Vec2) {
constructor(public index: number, public size: Vec2) {
const createLayerData = (startTile: number | (() => number), startTileset: number): LayerData => {
let layer: LayerData = { tiles: [], tilesets: [] };
@ -139,6 +139,7 @@ export default class MapLayer {
init(onDirty: (x: number, y: number) => void) {
this.onDirty = onDirty;
}
/**
* Sets a tile to the tileset provided, automatically smart-tiling as needed.

View File

@ -1,11 +1,12 @@
import EventHandler from '../../EventHandler';
import { Vec2 } from '../../util/Vec';
import * as Color from '../../../../../common/Color';
/** Represents a slider bar for a token. */
export interface TokenSliderData {
name: string;
color?: string;
color: Color.HSV;
icon?: number;
min?: number;
@ -59,32 +60,39 @@ export interface TokenRenderEvent {
* which can be set, updated, and retrieved through public methods.
*/
export default class Token extends Phaser.GameObjects.Sprite {
export default class Token extends Phaser.GameObjects.Container {
readonly on_meta = new EventHandler<TokenMetaEvent>();
readonly on_render = new EventHandler<TokenRenderEvent>();
readonly shadow?: Phaser.GameObjects.Sprite;
private sprite: Phaser.GameObjects.Sprite;
private shadow: Phaser.GameObjects.Sprite;
private hovered: boolean = false;
private selected: boolean = false;
constructor(scene: Phaser.Scene, readonly uuid: string, layer: number, pos?: Vec2, sprite?: string, index?: number) {
super(scene, 0, 0, sprite ?? '', index);
this.scene.add.existing(this);
this.setDepth(layer * 25 + 10);
private bars: Phaser.GameObjects.GameObject[] = [];
// private meta: TokenMetaData = { name: '', note: '', sliders: [] };
this.shadow = this.scene.add.sprite(this.x, this.y, sprite ?? '', index);
constructor(scene: Phaser.Scene, readonly uuid: string, layer: number, pos?: Vec2, sprite?: string, index?: number) {
super(scene, 0, 0);
this.scene.add.existing(this);
this.setDepth(-1000 + layer * 25 + 1);
this.setPosition(pos?.x ?? 0, pos?.y ?? 0);
this.shadow = this.scene.add.sprite(0, 0, sprite ?? '', index);
this.shadow.setOrigin(1 / 18, 1 / 18);
this.shadow.setScale(18 / 16 / this.shadow.width, 18 / 16 / 4 / this.shadow.height);
this.shadow.setAlpha(0.1, 0.1, 0.3, 0.3);
this.shadow.setTint(0x000000);
this.shadow.setDepth(this.depth - 1);
this.add(this.shadow);
this.scene.events.on('shutdown', () => console.log('yoo!'));
this.sprite = this.scene.add.sprite(0, 0, sprite ?? '', index);
this.sprite.setOrigin(1 / 18, 1 / 18);
this.sprite.setScale(18 / 16 / this.sprite.width, 18 / 16 / this.sprite.height);
this.add(this.sprite);
this.setOrigin(1 / 18, 1 / 18);
this.setScale(18 / 16 / this.width, 18 / 16 / this.height);
this.setPosition(pos?.x ?? 0, pos?.y ?? 0);
this.shadow.y = this.sprite.displayHeight - this.shadow.displayHeight - 0.125;
}
@ -95,8 +103,8 @@ export default class Token extends Phaser.GameObjects.Sprite {
getRenderData(): TokenRenderData {
return {
pos: new Vec2(this.x, this.y),
layer: Math.floor(this.depth / 25),
appearance: { sprite: this.texture?.key, index: this.frame?.name as any }
layer: Math.floor((this.depth + 1000) / 25),
appearance: { sprite: this.sprite.texture?.key, index: this.sprite.frame?.name as any }
};
}
@ -111,12 +119,22 @@ export default class Token extends Phaser.GameObjects.Sprite {
}
/**
* Sets the token's metadata.
*/
setMetaData(meta: TokenMetaData) {
// this.meta = meta;
this.updateSliders(meta.sliders);
}
/**
* Returns the current frame index being used by this token.
*/
getFrameIndex(): number {
return this.frame.name as any;
return this.sprite.frame.name as any;
}
@ -125,7 +143,7 @@ export default class Token extends Phaser.GameObjects.Sprite {
*/
getFrameCount(): number {
return Object.keys(this.texture.frames).length - 1;
return Object.keys(this.sprite.texture.frames).length - 1;
}
@ -156,26 +174,27 @@ export default class Token extends Phaser.GameObjects.Sprite {
setPosition(x?: number, y?: number): this {
if (this.x === x && this.y === y) return this;
if (!this.sprite) return Phaser.GameObjects.Sprite.prototype.setPosition.call(this, x, y) as any;
const pre = this.getRenderData();
Phaser.GameObjects.Sprite.prototype.setPosition.call(this, x, y);
const post = this.getRenderData();
this.on_render?.dispatch({ token: this, uuid: this.uuid, pre, post });
this.shadow?.setPosition(x, y! + this.displayHeight - this.shadow.displayHeight - 0.125);
if (this.sprite) this.on_render?.dispatch({ token: this, uuid: this.uuid, pre, post });
return this;
}
setFrame(frame: number): this {
const pre = this.getRenderData();
Phaser.GameObjects.Sprite.prototype.setFrame.call(this, frame);
this.sprite.setFrame(frame);
this.shadow.setFrame(frame);
const post = this.getRenderData();
this.on_render?.dispatch({ token: this, uuid: this.uuid, pre, post });
this.shadow?.setFrame(frame);
return this;
}
setTexture(key: string, index?: string | number): this {
const pre = this.getRenderData();
Phaser.GameObjects.Sprite.prototype.setTexture.call(this, key, index);
this.sprite.setTexture(key, index);
const post = this.getRenderData();
this.on_render?.dispatch({ token: this, uuid: this.uuid, pre, post });
this.setScale(18 / 16 / this.width, 18 / 16 / this.height);
@ -183,17 +202,11 @@ export default class Token extends Phaser.GameObjects.Sprite {
this.shadow.setTexture(key, index);
this.shadow.setScale(18 / 16 / this.shadow.width, 18 / 16 / 4 / this.shadow.height);
this.shadow.y = this.y + this.displayHeight - this.shadow.displayHeight - 0.125;
this.shadow.y = this.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;
}
/**
* Updates the shader pipelines of the token.
@ -201,12 +214,56 @@ export default class Token extends Phaser.GameObjects.Sprite {
private updateShader() {
if (this.selected) {
this.setPipeline('outline');
this.pipeline.set1f('tex_size', this.texture.source[0].width);
this.sprite.setPipeline('outline');
this.sprite.pipeline.set1f('tex_size', this.sprite.texture.source[0].width);
}
else if (this.hovered) {
this.setPipeline('brighten');
this.sprite.setPipeline('brighten');
}
else this.resetPipeline();
else this.sprite.resetPipeline();
}
/**
* Updates slider indicators
*/
private updateSliders(sliders: TokenSliderData[]) {
this.bars.forEach(bar => bar.destroy());
if (sliders.length === 0) return;
const SLIDER_WIDTH = 32 / 16;
const SLIDER_HEIGHT = 6 / 16;
const STROKE_WIDTH = 1 / 32;
let Y_OFFSET = -(SLIDER_HEIGHT + STROKE_WIDTH) * sliders.length - 4 / 16;
const outline = this.scene.add.rectangle((1 - SLIDER_WIDTH) / 2, Y_OFFSET,
SLIDER_WIDTH, (SLIDER_HEIGHT + STROKE_WIDTH) * sliders.length - STROKE_WIDTH, 0xffffff);
outline.setStrokeStyle(STROKE_WIDTH * 2, 0xffffff);
outline.setOrigin(0);
this.bars.push(outline);
this.add(outline);
sliders.forEach(s => {
const bg = this.scene.add.rectangle((1 - SLIDER_WIDTH) / 2, Y_OFFSET, SLIDER_WIDTH, SLIDER_HEIGHT, 0x19216c);
bg.setOrigin(0);
this.bars.push(bg);
this.add(bg);
const fg = this.scene.add.rectangle((1 - SLIDER_WIDTH) / 2, Y_OFFSET,
Math.min((s.current / s.max) * SLIDER_WIDTH, SLIDER_WIDTH), SLIDER_HEIGHT, Color.HSVToInt(s.color));
fg.setOrigin(0);
this.bars.push(fg);
this.add(fg);
const icon = this.scene.add.sprite(-.5, Y_OFFSET, 'ui_slider_icons', s.icon);
icon.setOrigin(0);
icon.setScale((1 / 12) * SLIDER_HEIGHT);
this.bars.push(icon);
this.add(icon);
Y_OFFSET += SLIDER_HEIGHT + STROKE_WIDTH;
});
}
}

View File

@ -73,12 +73,12 @@ export default class TokenManager {
createToken(uuid: string, layer: number, pos: Vec2, meta?: Partial<TokenMetaData>, sprite?: string, index?: number): Token {
uuid = uuid || generateId(32);
this.setMeta(uuid, meta);
const token = new Token(this.scene, uuid, layer, pos, sprite, index);
token.on_render.bind(this.onChange);
this.scene.add.existing(token);
this.tokens.push(token);
this.setMeta(uuid, meta);
this.event.dispatch({
type: 'create',
@ -104,7 +104,6 @@ export default class TokenManager {
const data = token.getRenderData();
this.tokens.splice(this.tokens.indexOf(token), 1);
token.shadow?.destroy();
token.destroy();
this.event.dispatch({
@ -189,6 +188,7 @@ export default class TokenManager {
setMeta(uuid: string, data?: Partial<TokenMetaData>) {
this.meta.set(uuid, { ...DEFAULT_METADATA, ...this.meta.get(uuid) ?? {}, ...data ?? {} });
this.tokens.filter(t => t.uuid === uuid)[0]?.setMetaData(this.meta.get(uuid)!);
}

View File

@ -1,200 +1,35 @@
import * as Phaser from 'phaser';
import * as IO from 'socket.io-client';
import Mode from './Mode';
import Map from '../map/Map';
import InputManager from '../InputManager';
import { TileItem } from '../action/Action';
import InputManager from '../interact/InputManager';
import ActionManager from '../action/ActionManager';
import ArchitectController from '../interact/ArchitectController';
import { Vec2 } from '../util/Vec';
import { Layer } from '../util/Layer';
import { Asset } from '../util/Asset';
export const ArchitectModeKey = 'ARCHITECT';
export default class ArchitectMode extends Mode {
readonly controller: ArchitectController;
activeTileset: number = 0;
activeLayer: Layer = 'wall';
private editMode: 'brush' | 'rect' | 'line' = 'brush';
private pointerState: 'primary' | 'secondary' | null = null;
private drawStartPos: Vec2 = new Vec2();
private drawn: TileItem[] = [];
private cursor: Phaser.GameObjects.Sprite;
private primitives: (Phaser.GameObjects.Line | Phaser.GameObjects.Sprite)[] = [];
constructor(scene: Phaser.Scene, map: Map, actions: ActionManager, assets: Asset[]) {
super(scene, map, actions, assets);
this.cursor = this.scene.add.sprite(0, 0, 'cursor');
this.cursor.setDepth(1000);
this.cursor.setOrigin(0, 0);
this.cursor.setScale(1 / 16);
this.cursor.setVisible(false);
constructor(scene: Phaser.Scene, map: Map, socket: IO.Socket, actions: ActionManager, assets: Asset[]) {
super(scene, map, socket, actions, assets);
this.controller = new ArchitectController(scene, actions);
}
update(cursorPos: Vec2, input: InputManager) {
if (input.getContext() !== 'map') return;
cursorPos = cursorPos.floor();
this.cursor.setPosition(cursorPos.x, cursorPos.y);
this.cursor.setVisible(cursorPos.x >= 0 && cursorPos.y >= 0 &&
cursorPos.x < this.map.size.x && cursorPos.y < this.map.size.y);
if (input.mousePressed()) this.drawStartPos = cursorPos;
switch(this.editMode) {
default: break;
case 'brush':
this.drawBrush(cursorPos, input);
break;
case 'line':
this.drawLine(cursorPos, input);
break;
case 'rect':
this.drawRect(cursorPos, input);
break;
}
if (!input.mouseDown()) {
if (input.keyDown('SHIFT')) this.editMode = 'line';
else if (this.editMode === 'line') this.editMode = 'brush';
if (input.keyDown('CTRL')) this.editMode = 'rect';
else if (this.editMode === 'rect') this.editMode = 'brush';
}
if (input.mouseDown()) {
if (!this.pointerState) this.pointerState = input.mouseLeftDown() ? 'primary' : 'secondary';
}
else if (this.pointerState) {
this.pointerState = null;
if (this.drawn.length) {
this.actions.push({ type: 'tile', items: this.drawn });
this.drawn = [];
}
}
this.controller.setLayer(this.map.getActiveLayer());
this.controller.update(cursorPos, input);
}
activate() {
this.cursor.setVisible(true);
this.controller.activate();
}
deactivate() {
this.cursor.setVisible(false);
}
private drawBrush(cursorPos: Vec2, input: InputManager) {
if (input.mouseDown()) {
const change = new Vec2(cursorPos.x - this.drawStartPos.x, cursorPos.y - this.drawStartPos.y);
const normalizeFactor = Math.sqrt(change.x * change.x + change.y * change.y) * 10;
change.x /= normalizeFactor; change.y /= normalizeFactor;
const place = new Vec2(this.drawStartPos.x, this.drawStartPos.y);
while (Math.abs(cursorPos.x - place.x) >= 0.1 || Math.abs(cursorPos.y - place.y) >= 0.1) {
this.drawTile(place.floor(), input.mouseLeftDown());
place.x += change.x; place.y += change.y;
}
this.drawTile(cursorPos, input.mouseLeftDown());
this.drawStartPos = cursorPos;
}
}
private drawLine(cursorPos: Vec2, input: InputManager) {
const a = new Vec2(this.drawStartPos.x, this.drawStartPos.y);
const b = new Vec2(cursorPos.x, cursorPos.y);
if (Math.abs(b.x - a.x) > Math.abs(b.y - a.y)) b.y = a.y;
else b.x = a.x;
this.primitives.forEach(v => v.destroy());
this.primitives = [];
if (input.mouseDown()) {
this.cursor.setPosition(b.x, b.y);
const line = this.scene.add.line(0, 0, a.x + 0.5, a.y + 0.5, b.x + 0.5, b.y + 0.5, 0xffffff, 1);
line.setOrigin(0, 0);
line.setDepth(300);
line.setLineWidth(0.03);
this.primitives.push(line);
const startSprite = this.scene.add.sprite(this.drawStartPos.x, this.drawStartPos.y, 'cursor');
startSprite.setOrigin(0, 0);
startSprite.setScale(1 / 16);
startSprite.setAlpha(0.5);
this.primitives.push(startSprite);
}
else if (this.pointerState) {
const change = new Vec2(b.x - a.x, b.y - a.y);
const normalizeFactor = Math.sqrt(change.x * change.x + change.y * change.y);
change.x /= normalizeFactor;
change.y /= normalizeFactor;
while (Math.abs(b.x - a.x) >= 1 || Math.abs(b.y - a.y) >= 1) {
this.drawTile(new Vec2(Math.floor(a.x), Math.floor(a.y)), this.pointerState === 'primary');
a.x += change.x;
a.y += change.y;
}
this.drawTile(new Vec2(b.x, b.y), this.pointerState === 'primary');
}
}
private drawRect(cursorPos: Vec2, input: InputManager) {
const a = new Vec2(Math.min(this.drawStartPos.x, cursorPos.x), Math.min(this.drawStartPos.y, cursorPos.y));
const b = new Vec2(Math.max(this.drawStartPos.x, cursorPos.x), Math.max(this.drawStartPos.y, cursorPos.y));
this.primitives.forEach(v => v.destroy());
this.primitives = [];
if (input.mouseDown()) {
if (!this.pointerState) this.drawStartPos = cursorPos;
const fac = 0.03;
this.primitives.push(this.scene.add.line(0, 0, a.x + fac, a.y + fac, b.x + 1 - fac, a.y + fac, 0xffffff, 1));
this.primitives.push(this.scene.add.line(0, 0, a.x + fac, a.y + fac / 2, a.x + fac, b.y + 1 - fac / 2, 0xffffff, 1));
this.primitives.push(this.scene.add.line(0, 0, a.x + fac, b.y + 1 - fac, b.x + 1 - fac, b.y + 1 - fac, 0xffffff, 1));
this.primitives.push(this.scene.add.line(0, 0, b.x + 1 - fac, a.y + fac / 2, b.x + 1 - fac, b.y + 1 - fac / 2, 0xffffff, 1));
this.primitives.forEach(v => {
(v as Phaser.GameObjects.Line).setLineWidth(0.03);
v.setOrigin(0, 0);
v.setDepth(300);
});
}
else if (this.pointerState) {
for (let i = a.x; i <= b.x; i++)
for (let j = a.y; j <= b.y; j++)
this.drawTile(new Vec2(i, j), this.pointerState === 'primary');
}
}
private drawTile(pos: Vec2, solid: boolean) {
const layer = this.map.getActiveLayer();
if (!layer) return;
let tile = solid ? this.activeTileset : 0;
const lastTile = layer.getTile(this.activeLayer, pos);
if (layer.setTile(this.activeLayer, tile, pos)) {
this.drawn.push({
pos, tile: { pre: lastTile, post: tile },
layer: this.activeLayer, mapLayer: layer.index
});
}
this.controller.deactivate();
}
}

View File

@ -1,58 +1,95 @@
import * as Phaser from 'phaser';
import * as IO from 'socket.io-client';
import Mode from './Mode';
import Map from '../map/Map';
import GameMap from '../map/Map';
import Shape from '../shape/Shape';
import Token from '../map/token/Token';
import InputManager from '../InputManager';
import EventHandler from '../EventHandler';
import InputManager from '../interact/InputManager';
import ActionManager from '../action/ActionManager';
import ArchitectController from '../interact/ArchitectController';
import Cone from '../shape/Cone';
import Circle from '../shape/Circle';
import { Vec2 } from '../util/Vec';
import { Asset } from '../util/Asset';
import * as Color from '../../../../common/Color';
export const DrawModeKey = 'DRAW';
export type DrawModeTool = 'line' | 'tile' | 'circle' | 'cone';
export interface DrawModeEvent {
export interface DrawModeToolEvent {
currentTool: DrawModeTool;
};
// const PLAYER_TINT = 0xffee99;
const PLAYER_TINT = 0x99ffee;
export interface DrawModeColorEvent {
currentColor: Color.HSV;
}
export default class DrawMode extends Mode {
private drawTool: DrawModeTool = 'circle';
readonly tool = new EventHandler<DrawModeToolEvent>();
readonly color = new EventHandler<DrawModeColorEvent>();
private drawTool: DrawModeTool = 'tile';
private current?: Cone | Circle;
private editContext: 'move' | 'scale' = 'scale';
private drawings: Set<Cone | Circle> = new Set();
private ownedDrawings: Map<string, Cone | Circle> = new Map();
private otherDrawings: Map<string, Cone | Circle> = new Map();
private drawingRoot: Phaser.GameObjects.Container;
private controller: ArchitectController;
private evtHandler = new EventHandler<DrawModeEvent>();
constructor(scene: Phaser.Scene, map: GameMap, socket: IO.Socket, actions: ActionManager, assets: Asset[]) {
super(scene, map, socket, actions, assets);
constructor(scene: Phaser.Scene, map: Map, actions: ActionManager, assets: Asset[]) {
super(scene, map, actions, assets);
this.controller = new ArchitectController(scene, actions);
this.drawingRoot = this.scene.add.container(0, 0);
this.drawingRoot.setDepth(10000);
this.socket.on('update_drawing', (uuid: string, type: string, data: string) => {
let drawing = this.otherDrawings.get(uuid);
if (!drawing) {
if (type === 'cone') drawing = new Cone(this.scene, new Vec2(), type);
else if (type === 'circle') drawing = new Circle(this.scene, new Vec2(), type);
else return;
this.drawingRoot.add(drawing);
this.otherDrawings.set(uuid, drawing);
}
drawing.deserialize(data);
});
this.socket.on('delete_drawing', (uuid: string) => {
this.otherDrawings.get(uuid)?.destroy();
this.otherDrawings.delete(uuid);
});
}
update(cursorPos: Vec2, input: InputManager) {
// Switch modes
if (input.keyPressed('E')) this.setTool('circle');
else if (input.keyPressed('C')) this.setTool('cone');
else if (input.keyPressed('T')) this.setTool('tile');
else if (input.keyPressed('I')) this.setTool('line');
// Update and select active shape.
if (!this.current) {
for (let d of this.drawings.values()) {
if (!this.current && this.drawTool !== 'tile') {
for (let d of this.ownedDrawings.values()) {
const interact = d.updateInteractions(cursorPos);
if (interact) {
if (input.mouseRightDown()) {
this.drawings.delete(d);
this.ownedDrawings.delete(d.uuid);
this.socket.emit('delete_drawing', d.uuid);
d.destroy();
}
else if (input.mouseLeftPressed()) {
@ -66,6 +103,11 @@ export default class DrawMode extends Mode {
}
}
for (let d of [ ...this.ownedDrawings.values(), this.current ]) {
if (!d) continue;
if (d.getAndClearDirty()) this.socket.emit('update_drawing', d.uuid, d.type, d.serialize());
}
// Edit or create current shape.
switch (this.current?.type ?? this.drawTool) {
@ -80,19 +122,27 @@ export default class DrawMode extends Mode {
case 'cone':
this.handleCone(cursorPos, input);
break;
case 'tile':
this.handleTile(cursorPos, input);
break;
}
// Commit or destroy active shape.
if (this.current) {
if (input.keyPressed('SPACE')) {
this.current.setTint(PLAYER_TINT);
this.drawings.add(this.current);
this.current.setTint(Color.HSVToInt(this.getColor()));
this.ownedDrawings.set(this.current.uuid, this.current);
}
if (input.mouseLeftReleased()) {
this.current.showIndicators(false);
if (!this.drawings.has(this.current)) this.current.destroy();
if (!this.ownedDrawings.has(this.current.uuid)) {
this.socket.emit('delete_drawing', this.current.uuid);
this.current.destroy();
}
this.current = undefined;
}
}
@ -104,28 +154,46 @@ export default class DrawMode extends Mode {
setTool(tool: DrawModeTool) {
this.drawTool = tool;
this.evtHandler.dispatch({ currentTool: tool });
if (tool === 'tile') {
this.controller.activate();
this.ownedDrawings.forEach(d => {
d.showIndicators(false);
d.showHighlight(false);
d.showHandles(false);
});
}
else this.controller.deactivate();
this.tool.dispatch({ currentTool: tool });
}
activate() { /* No activation logic */ }
getColor(): Color.HSV {
return this.scene.data.get('player_tint');
}
setColor(color: Color.HSV) {
if (color.v === 0) color.v = 0.01;
this.scene.data.set('player_tint', color);
this.color.dispatch({ currentColor: color });
}
activate() {
if (this.drawTool === 'tile') this.controller.activate();
this.controller.setLayer(this.map.getHighlightLayer());
}
deactivate() {
if (this.current && !this.drawings.has(this.current)) this.current.destroy();
if (this.current && !this.ownedDrawings.has(this.current.uuid)) this.current.destroy();
this.current = undefined;
this.drawings.forEach(d => {
this.ownedDrawings.forEach(d => {
d.showHandles(false);
d.showHighlight(false);
d.showIndicators(false);
});
}
bind(cb: (evt: DrawModeEvent) => boolean | void) {
this.evtHandler.bind(cb);
}
unbind(cb: (evt: DrawModeEvent) => boolean | void) {
this.evtHandler.unbind(cb);
this.controller.deactivate();
}
private handleCircle(cursorPos: Vec2, input: InputManager) {
@ -161,7 +229,7 @@ export default class DrawMode extends Mode {
}
if (input.mouseLeftReleased() && this.editContext === 'move') {
if (!this.drawings.has(this.current)) return;
if (!this.ownedDrawings.has(this.current.uuid)) return;
const token = this.findPotentialAttach(circle, cursorPos);
if (token) circle.attachToToken(token);
}
@ -243,9 +311,15 @@ export default class DrawMode extends Mode {
}
if (input.mouseLeftReleased() && this.editContext === 'move') {
if (!this.drawings.has(this.current)) return;
if (!this.ownedDrawings.has(this.current.uuid)) return;
const token = this.findPotentialAttach(cone, cursorPos);
if (token) cone.attachToToken(token);
}
}
private handleTile(cursorPos: Vec2, input: InputManager) {
this.controller.update(cursorPos, input);
this.controller.setActiveTileType('wall');
this.controller.setActiveTile(Color.HSVToInt(this.getColor()));
}
}

View File

@ -1,5 +1,7 @@
import * as IO from 'socket.io-client';
import type Map from '../map/Map';
import InputManager from '../InputManager';
import InputManager from '../interact/InputManager';
import ActionManager from '../action/ActionManager';
import { Vec2 } from '../util/Vec';
@ -11,7 +13,8 @@ import { Asset } from '../util/Asset';
*/
export default abstract class Mode {
constructor(protected scene: Phaser.Scene, protected map: Map, protected actions: ActionManager, protected assets: Asset[]) {}
constructor(protected scene: Phaser.Scene, protected map: Map, protected socket: IO.Socket,
protected actions: ActionManager, protected assets: Asset[]) {}
abstract update(cursorPos: Vec2, input: InputManager): void;

View File

@ -1,9 +1,10 @@
import type * as Phaser from 'phaser';
import * as IO from 'socket.io-client';
import type Mode from './Mode';
import type Map from '../map/Map';
import InputManager from '../InputManager';
import EventHandler from '../EventHandler';
import InputManager from '../interact/InputManager';
import ActionManager from '../action/ActionManager';
import DrawMode, { DrawModeKey } from './DrawMode';
@ -30,11 +31,11 @@ export default class ModeManager {
private evtHandler = new EventHandler<ModeSwitchEvent>();
init(scene: Phaser.Scene, map: Map, actions: ActionManager, assets: Asset[]) {
init(scene: Phaser.Scene, map: Map, socket: IO.Socket, actions: ActionManager, assets: Asset[]) {
this.modes = {
[ArchitectModeKey]: new ArchitectMode(scene, map, actions, assets),
[TokenModeKey]: new TokenMode(scene, map, actions, assets),
[DrawModeKey]: new DrawMode(scene, map, actions, assets)
[ArchitectModeKey]: new ArchitectMode(scene, map, socket, actions, assets),
[TokenModeKey]: new TokenMode(scene, map, socket, actions, assets),
[DrawModeKey]: new DrawMode(scene, map, socket, actions, assets)
};
this.activate(Object.keys(this.modes)[0]);

View File

@ -1,8 +1,9 @@
import * as Phaser from 'phaser';
import * as IO from 'socket.io-client';
import Mode from './Mode';
import Map from '../map/Map';
import InputManager from '../InputManager';
import InputManager from '../interact/InputManager';
import ActionManager from '../action/ActionManager';
import Token, { TokenRenderData } from '../map/token/Token';
@ -30,8 +31,8 @@ export default class TokenMode extends Mode {
private cursor: Phaser.GameObjects.Sprite;
private primitives: (Phaser.GameObjects.Line | Phaser.GameObjects.Sprite)[] = [];
constructor(scene: Phaser.Scene, map: Map, actions: ActionManager, assets: Asset[]) {
super(scene, map, actions, assets);
constructor(scene: Phaser.Scene, map: Map, socket: IO.Socket, actions: ActionManager, assets: Asset[]) {
super(scene, map, socket, actions, assets);
this.cursor = this.scene.add.sprite(0, 0, 'cursor');
this.cursor.setDepth(1000);
@ -64,7 +65,8 @@ export default class TokenMode extends Mode {
cursorPos = cursorPos.floor();
if (this.preview.texture.key !== this.placeTokenType) this.preview.setTexture(this.placeTokenType, 0);
if (this.preview.getRenderData().appearance.sprite !== this.placeTokenType)
this.preview.setTexture(this.placeTokenType, 0);
switch (this.editMode) {
default: break;

View File

@ -14,6 +14,7 @@ interface InitProps {
identifier: string;
mapIdentifier?: string;
onDirty: (dirty: boolean) => void;
onProgress: (progress: number) => void;
}
@ -22,18 +23,19 @@ export default class InitScene extends Phaser.Scene {
constructor() { super({key: 'InitScene'}); }
async create({ user, onProgress, identifier, mapIdentifier }: InitProps) {
async create({ user, onProgress, onDirty, identifier, mapIdentifier }: InitProps) {
this.socket.on('disconnect', this.onDisconnect);
this.game.events.addListener('destroy', this.onDestroy);
const { res, map } = await this.onConnect(user, identifier, mapIdentifier);
const res = await this.onConnect(user, identifier, mapIdentifier);
this.scene.start('LoadScene', {
socket: this.socket,
user, identifier,
...res, map,
...res,
onProgress
onProgress,
onDirty
});
this.game.scene.stop('InitScene');
@ -51,15 +53,14 @@ export default class InitScene extends Phaser.Scene {
};
private onConnect = async (user: string, identifier: string, mapIdentifier: string | undefined):
Promise<{ res: { campaign: Campaign; assets: Asset[] }; map: string | undefined }> => {
Promise<{ campaign: Campaign; assets: Asset[]; map: string }> => {
let res: { state: true; campaign: Campaign; assets: Asset[] } | { state: false; error?: string }
let res: { state: true; campaign: Campaign; assets: Asset[]; map: string } | { 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[] };
if (res.state) res.map = (mapIdentifier ? res.campaign.maps.filter(m => m.identifier === mapIdentifier)[0] : res.campaign.maps[0]).data;
else res = await emit(this.socket, 'room_join', { user, identifier }) as { state: true; campaign: Campaign; assets: Asset[]; map: string };
return { res, map };
return res;
};
}

View File

@ -27,6 +27,7 @@ export default class LoadScene extends Phaser.Scene {
this.load.image('cursor');
this.load.image('grid_tile');
this.load.image('erase_tile');
this.load.image('user_highlight');
this.load.setPrefix('ui_');
@ -42,13 +43,13 @@ export default class LoadScene extends Phaser.Scene {
this.load.spritesheet('select_cursor', undefined, {frameWidth: 21, frameHeight: 18});
this.load.spritesheet('sidebar_toggle', undefined, {frameWidth: 30, frameHeight: 18});
this.load.spritesheet('slider_icons', undefined, {frameWidth: 12, frameHeight: 12});
this.load.setPrefix('');
this.load.setPath('');
this.load.image('shader_light_mask', '/static/editor/light_mask.png');
this.load.audio('mystify', '/static/mus_mystify.wav');
this.load.setPath('/asset/');
for (let a of this.editorData!.assets) {
@ -73,11 +74,13 @@ export default class LoadScene extends Phaser.Scene {
}
else if (a.type === 'wall' || a.type === 'detail')
return Patch.tileset(this, a.identifier, a.tileSize);
Patch.tileset(this, a.identifier, a.tileSize);
else return new Promise<void>(resolve => resolve());
return new Promise<void>(resolve => resolve());
}));
await Patch.tileset(this, 'user_highlight', 16);
this.editorData.onProgress(undefined);
this.game.scene.start('MapScene', this.editorData);

View File

@ -1,43 +1,49 @@
import * as Phaser from 'phaser';
import InputManager from '../InputManager';
import PingHandler from '../interact/PingHandler';
import ActionManager from '../action/ActionManager';
import InterfaceRoot from '../interface/InterfaceRoot';
import InputManager from '../interact/InputManager';
import InterfaceRoot from '../interact/InterfaceRoot';
import Map from '../map/Map';
import CameraControl from '../CameraControl';
import ModeManager from '../mode/ModeManager';
// import { SerializedMap } from '../map/MapSaver';
import CameraControl from '../interact/CameraController';
// import { Vec2 } from '../util/Vec';
import { Vec2 } from '../util/Vec';
import { Asset } from '../util/Asset';
import EditorData from '../EditorData';
export default class MapScene extends Phaser.Scene {
assets: Asset[] = [];
view: CameraControl = new CameraControl();
actions: ActionManager = new ActionManager();
inputManager: InputManager = new InputManager(this);
private view: CameraControl = new CameraControl();
private actions: ActionManager = new ActionManager();
private inputManager: InputManager = new InputManager(this);
mode: ModeManager = new ModeManager();
interface: InterfaceRoot = new InterfaceRoot();
private mode: ModeManager = new ModeManager();
private pingHandler = new PingHandler();
private interface: InterfaceRoot = new InterfaceRoot();
map: Map = new Map();
private map: Map = new Map();
constructor() { super({ key: 'MapScene' }); }
create(data: EditorData): void {
this.data.set('player_tint', { h: Math.random(), s: 1, v: 1 });
this.assets = data.assets;
this.inputManager.init();
this.pingHandler.init(this, this.inputManager, data.socket);
this.view.init(this.cameras.main, this.inputManager);
this.map.init(this, this.assets);
if (data.map) this.map.load(data.map);
this.actions.init(this, this.map, data.socket);
this.mode.init(this, this.map, this.actions, this.assets);
data.socket.on('get_map', (res: (map: string) => void) => res(this.map.save()));
this.actions.init(this, this.map, data.socket, data.onDirty);
this.mode.init(this, this.map, data.socket, this.actions, this.assets);
this.interface.init(this, this.inputManager, this.mode, this.actions, this.map, this.assets);
}
@ -47,9 +53,28 @@ export default class MapScene extends Phaser.Scene {
this.interface.update();
this.actions.update(this.inputManager);
this.pingHandler.update(this.view.cursorWorld);
this.mode.update(this.view.cursorWorld, this.inputManager);
this.map.update();
if (this.inputManager.keyPressed('X')) {
const cam = this.cameras.main;
const snapSize = new Vec2(512, 512);
const pos = new Vec2(cam.scrollX, cam.scrollY);
this.interface.setVisible(false);
cam.setSize(snapSize.x, snapSize.y);
cam.centerOn(this.map.size.x / 2, this.map.size.y / 2);
cam.setZoom(cam.width / this.map.size.x);
this.game.renderer.snapshotArea(0, 0, snapSize.x, snapSize.y, (_: any) => {
this.interface.setVisible(true);
cam.setSize(this.game.renderer.width, this.game.renderer.height);
this.view.moveTo(pos);
});
}
}
}

View File

@ -5,6 +5,12 @@ import Shape, { ShapeIntersect, HANDLE_SIZE } from './Shape';
import { Vec2 } from '../util/Vec';
import { clamp } from '../util/Helpers';
export interface SerializedCircle {
origin: { x: number; y: number };
end: { x: number; y: number };
tint: number;
}
const UNFOCUSED_FILL_ALPHA = 0.05;
const FOCUSED_FILL_ALPHA = 0.15;
const UNFOCUSED_STROKE_ALPHA = 0.6;
@ -13,7 +19,7 @@ const FOCUSED_STROKE_ALPHA = 1;
export default class Circle extends Shape {
readonly type = 'circle';
private end: Vec2 = new Vec2(0);
private end: Vec2;
private intersects: boolean = false;
@ -26,14 +32,15 @@ export default class Circle extends Shape {
private moveHandle: Phaser.GameObjects.Ellipse;
private scaleHandle: Phaser.GameObjects.Ellipse;
constructor(scene: Phaser.Scene, protected origin: Vec2) {
super(scene, origin);
constructor(scene: Phaser.Scene, protected origin: Vec2, uuid?: string) {
super(scene, origin, uuid);
this.end = origin;
this.midLine = this.scene.add.line();
this.circle = this.scene.add.ellipse(this.origin.x, this.origin.y);
this.indicator = this.scene.add.text(this.origin.x, this.origin.y, '',
{ fontFamily: 'monospace', fontSize: '32px', align: 'center' });
{ fontFamily: 'sans-serif', fontSize: '32px', align: 'center' });
this.moveHandle = this.scene.add.ellipse(0, 0, HANDLE_SIZE * 2, HANDLE_SIZE * 2, 0xffffff);
this.scaleHandle = this.scene.add.ellipse(0, 0, HANDLE_SIZE * 2, HANDLE_SIZE * 2, 0xffffff);
@ -50,6 +57,23 @@ export default class Circle extends Shape {
this.moveHandle.setAlpha(.5);
}
serialize() {
const data: SerializedCircle = {
origin: this.origin,
end: this.end,
tint: this.tint
};
return JSON.stringify(data);
}
deserialize(d: string) {
const data = JSON.parse(d) as SerializedCircle;
this.setOrigin(new Vec2(data.origin));
this.setEnd(new Vec2(data.end));
this.setTint(data.tint);
}
setOrigin(origin: Vec2) {
if (this.origin.equals(origin)) return;
const offset = new Vec2(origin.x - this.origin.x, origin.y - this.origin.y);
@ -57,11 +81,14 @@ export default class Circle extends Shape {
this.origin = origin;
this.updatePrimitives();
this.dirty = true;
}
setEnd(end: Vec2) {
if (this.end.equals(end)) return;
const lastRadius = new Vec2(this.end.x - this.origin.x, this.end.y - this.origin.y).length();
const diff = new Vec2(end.x - this.origin.x, end.y - this.origin.y);
const dir = diff.normalize();
let radius = diff.length();
@ -70,6 +97,7 @@ export default class Circle extends Shape {
this.end = new Vec2(this.origin.x + dir.x * radius, this.origin.y + dir.y * radius);
this.updatePrimitives();
if (Math.abs(lastRadius - radius) > 0.01) this.dirty = true;
}
getRadius(): number {
@ -118,8 +146,8 @@ export default class Circle extends Shape {
const diff = new Vec2(this.end.x - this.origin.x, this.end.y - this.origin.y);
let radius = diff.length();
const fill = this.intersects || this.showingHighlight ? FOCUSED_FILL_ALPHA : UNFOCUSED_FILL_ALPHA;
const stroke = this.intersects || this.showingHighlight ? FOCUSED_STROKE_ALPHA : UNFOCUSED_STROKE_ALPHA;
const fill = this.showingHighlight ? FOCUSED_FILL_ALPHA : UNFOCUSED_FILL_ALPHA;
const stroke = this.showingHighlight ? FOCUSED_STROKE_ALPHA : UNFOCUSED_STROKE_ALPHA;
this.circle.setSize(radius * 2, radius * 2);
this.circle.setOrigin(0.5);

View File

@ -5,6 +5,12 @@ import Shape, { ShapeIntersect, HANDLE_SIZE } from './Shape';
import { Vec2 } from '../util/Vec';
import { clamp } from '../util/Helpers';
export interface SerializedCone {
origin: { x: number; y: number };
end: { x: number; y: number };
tint: number;
}
const UNFOCUSED_FILL_ALPHA = 0.05;
const FOCUSED_FILL_ALPHA = 0.15;
const UNFOCUSED_STROKE_ALPHA = 0.6;
@ -27,16 +33,15 @@ export default class Cone extends Shape {
private moveHandle: Phaser.GameObjects.Ellipse;
private scaleHandle: Phaser.GameObjects.Ellipse;
constructor(scene: Phaser.Scene, protected origin: Vec2) {
super(scene, origin);
constructor(scene: Phaser.Scene, protected origin: Vec2, uuid?: string) {
super(scene, origin, uuid);
this.end = origin;
this.midLine = this.scene.add.line();
this.triangle = this.scene.add.polygon(0, 0, [ new Vec2(0, 0) ]);
this.indicator = this.scene.add.text(0, 0, '',
{ fontFamily: 'monospace', fontSize: '32px', align: 'center' });
{ fontFamily: 'sans-serif', fontSize: '32px', align: 'center' });
this.moveHandle = this.scene.add.ellipse(0, 0, HANDLE_SIZE * 2, HANDLE_SIZE * 2, 0xffffff);
this.scaleHandle = this.scene.add.ellipse(0, 0, HANDLE_SIZE * 2, HANDLE_SIZE * 2, 0xffffff);
@ -52,12 +57,30 @@ export default class Cone extends Shape {
this.moveHandle.setAlpha(.5);
}
serialize() {
const data: SerializedCone = {
origin: this.origin,
end: this.end,
tint: this.tint
};
return JSON.stringify(data);
}
deserialize(d: string) {
const data = JSON.parse(d) as SerializedCone;
this.setOrigin(new Vec2(data.origin));
this.setEnd(new Vec2(data.end));
this.setTint(data.tint);
}
setOrigin(origin: Vec2) {
if (this.origin.equals(origin)) return;
const offset = new Vec2(origin.x - this.origin.x, origin.y - this.origin.y);
this.end = new Vec2(this.end.x + offset.x, this.end.y + offset.y);
this.origin = origin;
this.dirty = true;
this.updatePrimitives();
}
@ -76,12 +99,14 @@ export default class Cone extends Shape {
let dir = new Vec2(Math.cos(angle), Math.sin(angle));
this.end = new Vec2(this.origin.x + dir.x * len, this.origin.y + dir.y * len);
this.dirty = true;
this.updatePrimitives();
}
setTint(tint: number = 0xffffff) {
if (tint === this.tint) return;
this.tint = tint;
this.dirty = true;
this.updatePrimitives();
}
@ -155,7 +180,7 @@ export default class Cone extends Shape {
this.midLine.setLineWidth(.03);
this.midLine.setOrigin(0);
this.indicator.setText(`${Math.round(len * 25) / 5}ft\n${Math.round(angle * (180 / Math.PI))}°`);
this.indicator.setText(`${Math.round(len * 50) / 10}ft\n${Math.round(angle * (180 / Math.PI))}°`);
this.indicator.setPosition(this.origin.x + diff.x / 1.55 - this.indicator.displayWidth / 2,
this.origin.y + diff.y / 1.55 - this.indicator.displayHeight / 2);
this.indicator.setOrigin(0);

View File

@ -3,14 +3,18 @@ import * as Phaser from 'phaser';
import Token from '../map/token/Token';
import { Vec2 } from '../util/Vec';
import { generateId } from '../util/Helpers';
export type ShapeIntersect = 'shape' | 'move' | 'scale' | false;
export const HANDLE_SIZE = .25;
export default abstract class Shape extends Phaser.GameObjects.Container {
readonly uuid: string = generateId(32);
protected tint: number = 0xffffff;
protected dirty: boolean = true;
protected showingHandles: boolean = false;
protected showingHighlight: boolean = false;
protected showingIndicators: boolean = false;
@ -19,8 +23,9 @@ export default abstract class Shape extends Phaser.GameObjects.Container {
protected attachedChangeEvent?: () => void;
protected attachedDestroyEvent?: () => void;
constructor(scene: Phaser.Scene, protected origin: Vec2) {
constructor(scene: Phaser.Scene, protected origin: Vec2, uuid?: string) {
super(scene, 0, 0);
if (uuid) this.uuid = uuid;
this.on('destroy', () => this.detachFromToken());
}
@ -28,7 +33,9 @@ export default abstract class Shape extends Phaser.GameObjects.Container {
setOrigin(origin: Vec2) {
if (this.origin.equals(origin)) return;
this.origin = origin;
this.updatePrimitives();
this.dirty = true;
}
getOrigin(): Vec2 {
@ -38,7 +45,9 @@ export default abstract class Shape extends Phaser.GameObjects.Container {
setTint(tint: number = 0xffffff) {
if (tint === this.tint) return;
this.tint = tint;
this.updatePrimitives();
this.dirty = true;
}
showHandles(show?: boolean) {
@ -85,12 +94,22 @@ export default abstract class Shape extends Phaser.GameObjects.Container {
this.attachedToken = undefined;
}
getAndClearDirty() {
const dirty = this.dirty;
this.dirty = false;
return dirty;
}
protected intersectsHandle(pos: Vec2, handlePos: Vec2, epsilon: number = .01): boolean {
const cursor = new Phaser.Geom.Circle(pos.x, pos.y, epsilon);
const handle = new Phaser.Geom.Circle(handlePos.x, handlePos.y, HANDLE_SIZE);
return Phaser.Geom.Intersects.CircleToCircle(cursor, handle);
}
abstract serialize(): string;
abstract deserialize(data: string): void;
protected abstract intersectsShape(cursorPos: Vec2, epsilon: number): boolean;
protected abstract updateInteractions(cursorPos: Vec2): ShapeIntersect;

View File

@ -27,8 +27,9 @@ $slice-cell-size: 6px
&:active, &:focus-visible
@include slice('button_active')
&:disabled, &.Disabled
@include slice('button_disabled')
&:disabled, &.Disabled, &.Inactive
&:not(:active):not(:focus-visible)
@include slice('button_disabled')
.slice_highlight
@include slice('highlight')

165
common/Color.ts Normal file
View File

@ -0,0 +1,165 @@
/**
* HSV Struct
* all fields are from 0-1.
* undefined a == 1
*/
export type HSV = {
h: number;
s: number;
v: number;
a?: number;
}
/**
* RGB Struct
* r, g, b are from 0-255
* a is from 0-1, undefined == 1
*/
export type RGB = {
r: number;
g: number;
b: number;
a?: number;
}
/**
* Converts an HSV Color to RGB.
* Source: https://stackoverflow.com/questions/17242144/#comment24984878_17242144
*
* @param {HSV} hsv - The HSV value to convert.
* @returns {RGB} the RGB representation.
*/
export function HSVToRGB(hsv: HSV = { h: 0, s: 0, v: 0}): RGB {
let r: number = 0, g: number = 0, b: number = 0;
let i = Math.floor(hsv.h * 6);
let f = hsv.h * 6 - i;
let p = hsv.v * (1 - hsv.s);
let q = hsv.v * (1 - f * hsv.s);
let t = hsv.v * (1 - (1 - f) * hsv.s);
switch(i % 6) {
default: break;
case 0: r = hsv.v; g = t; b = p; break;
case 1: r = q; g = hsv.v; b = p; break;
case 2: r = p; g = hsv.v; b = t; break;
case 3: r = p; g = q; b = hsv.v; break;
case 4: r = t; g = p; b = hsv.v; break;
case 5: r = hsv.v; g = p; b = q; break;
}
return { r: r * 255, g: g * 255, b: b * 255 };
}
/**
* Converts an RGB Color to HSV.
* Source: https://stackoverflow.com/questions/8022885/rgb-to-hsv-color-in-javascript
*
* @param {RGB} rgb - The RGB value to convert.
* @returns {HSV} the HSV representation.
*/
export function RGBToHSV(rgb: RGB = { r: 0, g: 0, b: 0}): HSV {
let rabs, gabs, babs, rr, gg, bb, h = 0, s, v: any, diff: any, diffc;
rabs = rgb.r / 255;
gabs = rgb.g / 255;
babs = rgb.b / 255;
v = Math.max(rabs, gabs, babs);
diff = v - Math.min(rabs, gabs, babs);
diffc = (c: any) => (v - c) / 6 / diff + 1 / 2;
if (diff === 0) h = s = 0;
else {
s = diff / v;
rr = diffc(rabs);
gg = diffc(gabs);
bb = diffc(babs);
if (rabs === v) h = bb - gg;
else if (gabs === v) h = (1 / 3) + rr - bb;
else if (babs === v) h = (2 / 3) + gg - rr;
if (h < 0) h += 1;
else if (h > 1) h -= 1;
}
return { h, s, v };
}
/**
* Converts a numeric value from 0-255
* to a hexadecimal string from 00-ff.
*/
function componentToHex(c: number) {
let hex = Math.round(c).toString(16);
return hex.length === 1 ? '0' + hex : hex;
}
/**
* Converts an RGB Color to a Hex string.
* Source: https://stackoverflow.com/a/5624139
*
* @param {RGB} rgb - The RGB value to convert.
* @returns {string} the hexadecimal string representation.
*/
export function RGBToHex(rgb: RGB = { r: 0, g: 0, b: 0}): string {
return '#' + componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b);
}
/**
* Converts a Hex string to an RGB Color.
*
* @param {string} hex - The hexadecimal string to convert.
* @returns {RGB} the RGB representation.
*/
export function hexToRGB(hex: string): RGB {
let r = parseInt('0x' + hex[1] + hex[2], 16);
let g = parseInt('0x' + hex[3] + hex[4], 16);
let b = parseInt('0x' + hex[5] + hex[6], 16);
return { r, g, b };
}
/**
* Converts an HSV Color to a Hex string.
*
* @param {HSV} hsv - The HSV value to convert.
* @returns {string} the hexadecimal string representation.
*/
export function HSVToHex(hsv: HSV = { h: 0, s: 0, v: 0}): string {
return RGBToHex(HSVToRGB(hsv));
}
/**
* Converts a Hex string to an RGB Color.
*
* @param {string} hex - The hexadecimal string to convert.
* @returns {RGB} the RGB representation.
*/
export function hexToHSV(hex: string): HSV {
return RGBToHSV(hexToRGB(hex));
}
/**
* Converts a HSV color to a hex integer value.
*
* @param {HSV} color - The color to convert.
* @returns {number} the RGB representation of the color in an int.
*/
export function HSVToInt(color: HSV) {
return parseInt(HSVToHex(color).substr(1), 16);
}

13
package-lock.json generated
View File

@ -615,6 +615,11 @@
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
},
"cli-boxes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
@ -1873,6 +1878,14 @@
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true
},
"preact-transitioning": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/preact-transitioning/-/preact-transitioning-1.0.2.tgz",
"integrity": "sha512-WUhhUXW9T0gSN7NOumjel+A+xmNveYBlIXZcVtxT8gm6DwL1mS65I2DbAW35ffzgaDkzPm7AUTqvTJYu86g1EQ==",
"requires": {
"classnames": "^2.2.6"
}
},
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",

View File

@ -267,9 +267,7 @@ export default class Database {
user, identifier, maps: { $elemMatch: { identifier: mapIdentifier }}});
if (mapExists) throw 'A map of this name already exists.';
console.log(mapIdentifier);
const stub = JSON.stringify({ format: '1.0.0', identifier: mapIdentifier, size: { x: 32, y: 32 }, tokens: [] });
const stub = JSON.stringify({ format: '1.0.0', identifier: mapIdentifier, size: { x: 64, y: 64 }, tokens: [] });
const data = stub.length + '|' + stub;
await collection.updateOne({ user, identifier }, { $push: { maps: { name: map, identifier: mapIdentifier, data }}});

View File

@ -4,9 +4,14 @@ import { parse as parseCookies } from 'cookie';
import Database from './Database';
import { Asset, Campaign } from '../../common/DBStructs';
interface RoomData {
owner: IO.Socket;
players: IO.Socket[];
}
export default class MapController {
private io: IO.Server = null as any;
private activeRooms: Set<string> = new Set();
private activeRooms: Map<string, RoomData> = new Map();
constructor(private db: Database) {}
@ -47,10 +52,12 @@ export default class MapController {
if (this.activeRooms.has(identifier)) throw 'Room already open.';
if (!socket.disconnected) {
this.activeRooms.add(identifier);
this.activeRooms.set(identifier, { owner: socket, players: [] });
socket.join(identifier);
socket.on('disconnecting', this.destroyRoom.bind(this, socket, identifier));
this.initRoomOwner(socket, user, camID);
this.bindEvents(socket, identifier);
this.bindOwnerEvents(socket, user, camID);
res({ state: true, assets: await this.db.getCampaignAssets(user, camID), campaign });
}
@ -65,19 +72,22 @@ export default class MapController {
}
private async joinRoom(socket: IO.Socket, _: string, { user: camUser, identifier: camID }: { user: string, identifier: string },
res: (res: { state: true; assets: Asset[]; campaign: Campaign } | { state: false; error?: string }) => void) {
res: (res: { state: true; assets: Asset[]; campaign: Campaign; map: string } | { state: false; error?: string }) => void) {
if (typeof res !== 'function') return;
try {
if (typeof camID !== 'string' || typeof camUser !== 'string') throw 'Missing required parameters.';
const campaign = await this.db.getCampaign(camUser, camID);
const identifier = camUser + ':' + camID;
if (!this.activeRooms.has(identifier)) throw 'Room is not open.';
const room = this.activeRooms.get(identifier);
if (!room) throw 'Room is not open.';
socket.join(identifier);
// this.initRoom(socket, user, camIdentifier);
this.bindEvents(socket, identifier);
room.players.push(socket);
res({ state: true, assets: await this.db.getCampaignAssets(camUser, camID), campaign });
const map = await this.getMapFromOwner(room.owner);
res({ state: true, assets: await this.db.getCampaignAssets(camUser, camID), campaign, map });
}
catch (error) {
if (typeof error === 'string') res({ state: false, error });
@ -95,14 +105,43 @@ export default class MapController {
this.activeRooms.delete(identifier);
}
private initRoomOwner(socket: IO.Socket, user: string, campaign: string) {
private bindOwnerEvents(socket: IO.Socket, user: string, campaign: string) {
const room = user + ':' + campaign;
// socket.on('map_load', this.mapLoad.bind(this, user));
socket.on('action', this.onAction.bind(this, socket, room));
socket.on('serialize', this.onSerialize.bind(this, user, campaign));
// socket.on('get_campaign_assets', this.onGetCampaignAssets.bind(this, user));
}
private bindEvents(socket: IO.Socket, room: string) {
socket.on('ping', this.onPing.bind(this, socket, room));
socket.on('update_drawing', this.onUpdateDrawing.bind(this, socket, room));
socket.on('delete_drawing', this.onDeleteDrawing.bind(this, socket, room));
}
private async getMapFromOwner(owner: IO.Socket): Promise<string> {
return new Promise<string>((resolve, reject) => {
owner.emit('get_map', (map: string) => {
if (typeof map !== 'string') reject();
resolve(map);
});
});
}
private onPing(socket: IO.Socket, room: string, data: { pos: { x: number, y: number }, color: number }) {
if (typeof data.pos !== 'object' || typeof data.pos.x !== 'number' ||
typeof data.pos.y !== 'number' || typeof data.color !== 'number') return;
socket.in(room).emit('ping', data);
}
private onUpdateDrawing(socket: IO.Socket, room: string, uuid: string, type: string, data: string) {
if (typeof uuid !== 'string' || typeof type !== 'string' || typeof data !== 'string') return;
socket.in(room).emit('update_drawing', uuid, type, data);
}
private onDeleteDrawing(socket: IO.Socket, room: string, uuid: string) {
if (typeof uuid !== 'string') return;
socket.in(room).emit('delete_drawing', uuid);
}
private onAction(socket: IO.Socket, room: string, event: any) {