Add tile highlight mode, pinging
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Before Width: | Height: | Size: 326 B |
Before Width: | Height: | Size: 947 B |
Before Width: | Height: | Size: 967 B |
Before Width: | Height: | Size: 867 B |
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 964 B |
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.2 KiB |
|
@ -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 || [] ]);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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;
|
|
@ -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
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { default as Asset } from './AssetRoute';
|
||||
export { default as Collection } from './CollectionRoute';
|
|
@ -0,0 +1 @@
|
|||
declare module 'preact-transitioning';
|
|
@ -9,6 +9,6 @@ export default interface EditorData {
|
|||
assets: Asset[];
|
||||
map?: string;
|
||||
|
||||
display: 'edit' | 'view';
|
||||
onDirty: (dirty: boolean) => void;
|
||||
onProgress: (progress: number | undefined) => void;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.`);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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'} />
|
|
@ -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
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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>) => {
|
|
@ -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);
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)!);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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 }}});
|
||||
|
|
|
@ -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) {
|
||||
|
|