Modernise & make fun #3
7 changed files with 184 additions and 122 deletions
|
|
@ -13,10 +13,10 @@ import {
|
||||||
settings,
|
settings,
|
||||||
PlayerInformation,
|
PlayerInformation,
|
||||||
CharacterTeam,
|
CharacterTeam,
|
||||||
UpdateOtherPlayerDirections,
|
UpdateMinimap,
|
||||||
GameObject,
|
GameObject,
|
||||||
Command,
|
Command,
|
||||||
OtherPlayerDirection,
|
MinimapPlayer,
|
||||||
RemoteCallsForObject,
|
RemoteCallsForObject,
|
||||||
RemoteCallsForObjects,
|
RemoteCallsForObjects,
|
||||||
ServerAnnouncement,
|
ServerAnnouncement,
|
||||||
|
|
@ -29,7 +29,6 @@ import {
|
||||||
import { Socket } from 'socket.io';
|
import { Socket } from 'socket.io';
|
||||||
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
import { BoundingBox } from '../physics/bounding-boxes/bounding-box';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { CharacterPhysical } from '../objects/character-physical';
|
|
||||||
import { PlayerContainer } from './player-container';
|
import { PlayerContainer } from './player-container';
|
||||||
import { PlayerBase } from './player-base';
|
import { PlayerBase } from './player-base';
|
||||||
|
|
||||||
|
|
@ -142,7 +141,7 @@ export class Player extends PlayerBase {
|
||||||
this.queueCommandSend(new CreateObjectsCommand(newlyIntersecting));
|
this.queueCommandSend(new CreateObjectsCommand(newlyIntersecting));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.queueCommandSend(new UpdateOtherPlayerDirections(this.getOtherPlayers()));
|
this.queueCommandSend(new UpdateMinimap(this.getMinimapPlayers()));
|
||||||
|
|
||||||
this.queueCommandSend(
|
this.queueCommandSend(
|
||||||
new PropertyUpdatesForObjects(
|
new PropertyUpdatesForObjects(
|
||||||
|
|
@ -167,35 +166,14 @@ export class Player extends PlayerBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOtherPlayers(): Array<OtherPlayerDirection> {
|
// Every living player except this one, reported by absolute world position so
|
||||||
if (!this.character) {
|
// the client can plot the whole circular arena on its minimap.
|
||||||
return [];
|
private getMinimapPlayers(): Array<MinimapPlayer> {
|
||||||
}
|
return this.playerContainer.players
|
||||||
|
.filter((p) => p !== this && p.character?.isAlive)
|
||||||
const viewArea = calculateViewArea(this.center, this.aspectRatio, 0.9);
|
.map(
|
||||||
const bb = new BoundingBox();
|
|
||||||
bb.topLeft = viewArea.topLeft;
|
|
||||||
bb.size = viewArea.size;
|
|
||||||
|
|
||||||
const playersInViewArea = this.objectContainer
|
|
||||||
.findIntersecting(bb)
|
|
||||||
.map((o) => o.gameObject)
|
|
||||||
.filter((g) => g instanceof CharacterPhysical);
|
|
||||||
|
|
||||||
const otherPlayers = this.playerContainer.players.filter(
|
|
||||||
(p) => p.character?.isAlive && playersInViewArea.indexOf(p.character!) < 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return otherPlayers.map(
|
|
||||||
(p) =>
|
(p) =>
|
||||||
new OtherPlayerDirection(
|
new MinimapPlayer(p.character!.id, vec2.clone(p.character!.center), p.team),
|
||||||
p.character!.id,
|
|
||||||
vec2.normalize(
|
|
||||||
vec2.create(),
|
|
||||||
vec2.subtract(vec2.create(), p.character!.center, this.character!.center),
|
|
||||||
),
|
|
||||||
p.team,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -231,25 +231,27 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.other-player-arrow {
|
// Top-down radar of the whole arena (see Minimap). The circular border is
|
||||||
transition: transform 150ms;
|
// the world boundary; dots are painted onto the canvas.
|
||||||
|
.minimap {
|
||||||
@include square($large-icon);
|
top: $small-padding;
|
||||||
|
left: $small-padding;
|
||||||
|
@include square(132px);
|
||||||
|
|
||||||
|
// On narrow screens the centred scoreboard bar reaches close to the left
|
||||||
|
// edge, so drop the minimap below it to avoid a corner overlap.
|
||||||
@media (max-width: $breakpoint) {
|
@media (max-width: $breakpoint) {
|
||||||
@include square($small-icon);
|
@include square(96px);
|
||||||
|
top: calc(#{$small-padding} + 32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
mask-image: url('../static/chevron.svg');
|
border-radius: 1000px;
|
||||||
mask-size: contain;
|
background-color: rgba(0, 0, 0, 0.35);
|
||||||
|
box-shadow:
|
||||||
&.blue {
|
inset 0 0 3px 0 rgba(0, 0, 0, 0.4),
|
||||||
background-color: $bright-blue;
|
0 0 10px rgba(0, 0, 0, 0.3);
|
||||||
}
|
border: $border-width solid rgba(255, 255, 255, 0.25);
|
||||||
|
z-index: 100;
|
||||||
&.red {
|
|
||||||
background-color: $bright-red;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Off-screen pointer to the keystone "Heart", tinted by its current owner.
|
// Off-screen pointer to the keystone "Heart", tinted by its current owner.
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ import {
|
||||||
deserialize,
|
deserialize,
|
||||||
TransportEvents,
|
TransportEvents,
|
||||||
SetAspectRatioActionCommand,
|
SetAspectRatioActionCommand,
|
||||||
UpdateOtherPlayerDirections,
|
UpdateMinimap,
|
||||||
clamp,
|
|
||||||
UpdateGameState,
|
UpdateGameState,
|
||||||
GameEndCommand,
|
GameEndCommand,
|
||||||
ServerAnnouncement,
|
ServerAnnouncement,
|
||||||
|
|
@ -39,6 +38,7 @@ import { serverTimeline } from './helper/server-timeline';
|
||||||
import { localCharacterPredictor } from './helper/prediction/local-character-predictor';
|
import { localCharacterPredictor } from './helper/prediction/local-character-predictor';
|
||||||
import { Tutorial } from './tutorial';
|
import { Tutorial } from './tutorial';
|
||||||
import { Scoreboard } from './scoreboard';
|
import { Scoreboard } from './scoreboard';
|
||||||
|
import { Minimap } from './minimap';
|
||||||
|
|
||||||
export class Game extends CommandReceiver {
|
export class Game extends CommandReceiver {
|
||||||
public gameObjects = new GameObjectContainer(this);
|
public gameObjects = new GameObjectContainer(this);
|
||||||
|
|
@ -54,8 +54,8 @@ export class Game extends CommandReceiver {
|
||||||
private touchListener: TouchListener;
|
private touchListener: TouchListener;
|
||||||
|
|
||||||
private scoreboard = new Scoreboard();
|
private scoreboard = new Scoreboard();
|
||||||
|
private minimap = new Minimap();
|
||||||
private announcementText = document.createElement('h2');
|
private announcementText = document.createElement('h2');
|
||||||
private arrows: { [id: number]: HTMLElement } = {};
|
|
||||||
private keystoneArrow?: HTMLElement;
|
private keystoneArrow?: HTMLElement;
|
||||||
private socketReceiver!: CommandSocket;
|
private socketReceiver!: CommandSocket;
|
||||||
private tutorial!: Tutorial;
|
private tutorial!: Tutorial;
|
||||||
|
|
@ -82,11 +82,12 @@ export class Game extends CommandReceiver {
|
||||||
localCharacterPredictor.reset();
|
localCharacterPredictor.reset();
|
||||||
this.gameObjects = new GameObjectContainer(this);
|
this.gameObjects = new GameObjectContainer(this);
|
||||||
this.overlay.innerHTML = '';
|
this.overlay.innerHTML = '';
|
||||||
this.arrows = {};
|
|
||||||
this.keystoneArrow = undefined;
|
this.keystoneArrow = undefined;
|
||||||
|
this.lastMinimap = undefined;
|
||||||
this.isEnding = false;
|
this.isEnding = false;
|
||||||
this.lastAnnouncementText = '';
|
this.lastAnnouncementText = '';
|
||||||
this.overlay.appendChild(this.scoreboard.element);
|
this.overlay.appendChild(this.scoreboard.element);
|
||||||
|
this.overlay.appendChild(this.minimap.element);
|
||||||
this.announcementText.innerText = '';
|
this.announcementText.innerText = '';
|
||||||
this.timeScaling = 1;
|
this.timeScaling = 1;
|
||||||
this.overlay.appendChild(this.announcementText);
|
this.overlay.appendChild(this.announcementText);
|
||||||
|
|
@ -160,67 +161,11 @@ export class Game extends CommandReceiver {
|
||||||
c.lastLeapClientTimeMs,
|
c.lastLeapClientTimeMs,
|
||||||
),
|
),
|
||||||
[GameEndCommand.type]: () => (this.isEnding = true),
|
[GameEndCommand.type]: () => (this.isEnding = true),
|
||||||
[UpdateOtherPlayerDirections.type]: (c: UpdateOtherPlayerDirections) =>
|
[UpdateMinimap.type]: (c: UpdateMinimap) => (this.lastMinimap = c),
|
||||||
(this.lastOtherPlayerDirections = c),
|
|
||||||
[GameStartCommand.type]: this.initialize.bind(this),
|
[GameStartCommand.type]: this.initialize.bind(this),
|
||||||
};
|
};
|
||||||
|
|
||||||
private lastOtherPlayerDirections?: UpdateOtherPlayerDirections;
|
private lastMinimap?: UpdateMinimap;
|
||||||
private handleOtherPlayerDirections(command: UpdateOtherPlayerDirections) {
|
|
||||||
command.otherPlayerDirections.forEach((d) => {
|
|
||||||
if (!(d.id! in this.arrows)) {
|
|
||||||
const element = document.createElement('div');
|
|
||||||
this.arrows[d.id!] = element;
|
|
||||||
this.overlay.appendChild(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
const e = this.arrows[d.id!];
|
|
||||||
const direction = d.direction;
|
|
||||||
const team = d.team;
|
|
||||||
const angle = Math.atan2(direction.y, direction.x);
|
|
||||||
e.className = 'other-player-arrow ' + team;
|
|
||||||
|
|
||||||
if (!this.renderer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const width = this.renderer.canvasSize.x;
|
|
||||||
const height = this.renderer.canvasSize.y;
|
|
||||||
const aspectRatio = width / height;
|
|
||||||
const directionRatio = direction.x / direction.y;
|
|
||||||
|
|
||||||
let deltaX: number, deltaY: number;
|
|
||||||
if (aspectRatio < Math.abs(directionRatio)) {
|
|
||||||
deltaX = (width / 2) * Math.sign(direction.x);
|
|
||||||
deltaY = deltaX / directionRatio;
|
|
||||||
} else {
|
|
||||||
deltaY = (height / 2) * Math.sign(direction.y);
|
|
||||||
deltaX = deltaY * directionRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
const delta = vec2.fromValues(deltaX, deltaY);
|
|
||||||
const center = vec2.fromValues(width / 2, height / 2);
|
|
||||||
const p = vec2.add(center, center, delta);
|
|
||||||
const arrowPadding = 24;
|
|
||||||
vec2.set(
|
|
||||||
p,
|
|
||||||
clamp(p.x, arrowPadding, width - arrowPadding),
|
|
||||||
clamp(height - p.y, arrowPadding, height - arrowPadding),
|
|
||||||
);
|
|
||||||
e.style.transform = `translateX(${p.x}px) translateY(${p.y
|
|
||||||
}px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const id in this.arrows) {
|
|
||||||
if (
|
|
||||||
Object.prototype.hasOwnProperty.call(this.arrows, id) &&
|
|
||||||
command.otherPlayerDirections.find((v) => v.id?.toString() === id) === undefined
|
|
||||||
) {
|
|
||||||
this.arrows[id].parentElement?.removeChild(this.arrows[id]);
|
|
||||||
delete this.arrows[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
const noiseTexture = await renderNoise([256, 256], 2, 1);
|
const noiseTexture = await renderNoise([256, 256], 2, 1);
|
||||||
|
|
@ -332,9 +277,10 @@ export class Game extends CommandReceiver {
|
||||||
this.scoreboard.update(this.lastGameState, this.gameObjects.player?.team);
|
this.scoreboard.update(this.lastGameState, this.gameObjects.player?.team);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.lastOtherPlayerDirections) {
|
this.minimap.update(
|
||||||
this.handleOtherPlayerDirections(this.lastOtherPlayerDirections);
|
this.gameObjects.localPlayerPosition,
|
||||||
}
|
this.lastMinimap?.players ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
this.handleKeystoneArrow();
|
this.handleKeystoneArrow();
|
||||||
|
|
||||||
|
|
|
||||||
126
frontend/src/scripts/minimap.ts
Normal file
126
frontend/src/scripts/minimap.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
import { CharacterTeam, Id, settings } from 'shared';
|
||||||
|
|
||||||
|
export interface MinimapBlip {
|
||||||
|
id: Id;
|
||||||
|
position: vec2;
|
||||||
|
team: CharacterTeam;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-down radar of the whole circular arena, pinned to the top-left. The map's
|
||||||
|
// circular border is the world boundary (its radius maps to worldRadius), so the
|
||||||
|
// local player's bright dot reads as a true position in the arena and every other
|
||||||
|
// living player shows as a team-coloured dot. Owns its own <canvas>, so the Game
|
||||||
|
// just appends `element` to the overlay and feeds it `update()` each frame.
|
||||||
|
// Replaces the off-screen chevrons that used to point at other players.
|
||||||
|
export class Minimap {
|
||||||
|
public readonly element = document.createElement('canvas');
|
||||||
|
|
||||||
|
private readonly ctx: CanvasRenderingContext2D;
|
||||||
|
private readonly colors: Record<CharacterTeam, string>;
|
||||||
|
// World-space positions, smoothed per player so the 25 Hz snapshots glide
|
||||||
|
// rather than step between frames.
|
||||||
|
private readonly smoothed = new Map<Id, vec2>();
|
||||||
|
private bufferSize = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.element.className = 'minimap';
|
||||||
|
this.ctx = this.element.getContext('2d')!;
|
||||||
|
|
||||||
|
const theme = getComputedStyle(document.documentElement);
|
||||||
|
const read = (name: string, fallback: string) =>
|
||||||
|
theme.getPropertyValue(name).trim() || fallback;
|
||||||
|
this.colors = {
|
||||||
|
[CharacterTeam.blue]: read('--bright-blue', '#4069a5'),
|
||||||
|
[CharacterTeam.red]: read('--bright-red', '#d15652'),
|
||||||
|
[CharacterTeam.neutral]: read('--bright-neutral', '#ccc'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(localPosition: vec2 | undefined, players: Array<MinimapBlip>) {
|
||||||
|
const size = this.element.clientWidth;
|
||||||
|
if (size === 0) {
|
||||||
|
return; // not laid out yet
|
||||||
|
}
|
||||||
|
this.syncBufferSize(size);
|
||||||
|
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.clearRect(0, 0, size, size);
|
||||||
|
|
||||||
|
const center = size / 2;
|
||||||
|
const innerRadius = center - 5;
|
||||||
|
const scale = innerRadius / settings.worldRadius;
|
||||||
|
|
||||||
|
const toMap = (world: vec2): { x: number; y: number } => {
|
||||||
|
let dx = world.x * scale;
|
||||||
|
let dy = -world.y * scale; // world Y points up, canvas Y points down
|
||||||
|
const distance = Math.hypot(dx, dy);
|
||||||
|
if (distance > innerRadius) {
|
||||||
|
dx = (dx / distance) * innerRadius;
|
||||||
|
dy = (dy / distance) * innerRadius;
|
||||||
|
}
|
||||||
|
return { x: center + dx, y: center + dy };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Faint marker at the world's centre to aid orientation.
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.12)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(center, center, 1.5, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
const present = new Set<Id>();
|
||||||
|
for (const blip of players) {
|
||||||
|
present.add(blip.id);
|
||||||
|
let world = this.smoothed.get(blip.id);
|
||||||
|
if (world) {
|
||||||
|
vec2.lerp(world, world, blip.position, 0.3);
|
||||||
|
} else {
|
||||||
|
world = vec2.clone(blip.position);
|
||||||
|
this.smoothed.set(blip.id, world);
|
||||||
|
}
|
||||||
|
const { x, y } = toMap(world);
|
||||||
|
this.drawDot(x, y, 3, this.colors[blip.team]);
|
||||||
|
}
|
||||||
|
for (const id of this.smoothed.keys()) {
|
||||||
|
if (!present.has(id)) {
|
||||||
|
this.smoothed.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The local player rides on top, drawn in white with a ring so "you" is
|
||||||
|
// unmistakable amongst the team-coloured dots.
|
||||||
|
if (localPosition) {
|
||||||
|
const { x, y } = toMap(localPosition);
|
||||||
|
this.drawDot(x, y, 3.5, '#ffffff');
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, 6, 0, Math.PI * 2);
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.65)';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawDot(x: number, y: number, radius: number, color: string) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.shadowColor = color;
|
||||||
|
ctx.shadowBlur = radius * 2;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncBufferSize(cssSize: number) {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const target = Math.round(cssSize * dpr);
|
||||||
|
if (this.bufferSize !== target) {
|
||||||
|
this.bufferSize = target;
|
||||||
|
this.element.width = target;
|
||||||
|
this.element.height = target;
|
||||||
|
}
|
||||||
|
// Setting the buffer size resets the transform, so (re)establish it every
|
||||||
|
// frame and draw in CSS pixels regardless of the device pixel ratio.
|
||||||
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
import {
|
import {
|
||||||
Circle,
|
Circle,
|
||||||
Command,
|
Command,
|
||||||
|
|
@ -84,6 +85,16 @@ export class GameObjectContainer extends CommandReceiver {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The local player's world position, but only while the body is alive. On
|
||||||
|
// death the server deletes the character object (yet `player` keeps pointing
|
||||||
|
// at the now-stale view), so gate on the object still being present — otherwise
|
||||||
|
// the minimap would pin the "you" dot at the death spot for the whole respawn.
|
||||||
|
public get localPlayerPosition(): vec2 | undefined {
|
||||||
|
return this.player && this.objects.has(this.player.id)
|
||||||
|
? this.player.position
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public get planets(): Array<PlanetView> {
|
public get planets(): Array<PlanetView> {
|
||||||
const planets: Array<PlanetView> = [];
|
const planets: Array<PlanetView> = [];
|
||||||
this.objects.forEach((o) => {
|
this.objects.forEach((o) => {
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,25 @@ import { serializable } from '../../serialization/serializable';
|
||||||
import { Command } from '../command';
|
import { Command } from '../command';
|
||||||
|
|
||||||
@serializable
|
@serializable
|
||||||
export class OtherPlayerDirection {
|
export class MinimapPlayer {
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly id: Id,
|
public readonly id: Id,
|
||||||
public readonly direction: vec2,
|
public readonly position: vec2,
|
||||||
public readonly team: CharacterTeam,
|
public readonly team: CharacterTeam,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
return [this.id, this.direction, this.team];
|
return [this.id, this.position, this.team];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@serializable
|
@serializable
|
||||||
export class UpdateOtherPlayerDirections extends Command {
|
export class UpdateMinimap extends Command {
|
||||||
public constructor(public readonly otherPlayerDirections: Array<OtherPlayerDirection>) {
|
public constructor(public readonly players: Array<MinimapPlayer>) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
return [this.otherPlayerDirections];
|
return [this.players];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,10 +7,9 @@ export * from './commands/types/server-announcement';
|
||||||
export * from './commands/types/property-updates-for-objects';
|
export * from './commands/types/property-updates-for-objects';
|
||||||
export * from './commands/types/game-end';
|
export * from './commands/types/game-end';
|
||||||
export * from './commands/types/game-start';
|
export * from './commands/types/game-start';
|
||||||
export * from './commands/types/update-other-player-directions';
|
export * from './commands/types/update-minimap';
|
||||||
export * from './commands/types/update-game-state';
|
export * from './commands/types/update-game-state';
|
||||||
export * from './commands/command-receiver';
|
export * from './commands/command-receiver';
|
||||||
export * from './commands/types/update-other-player-directions';
|
|
||||||
export * from './commands/command-executors';
|
export * from './commands/command-executors';
|
||||||
export * from './commands/command-generator';
|
export * from './commands/command-generator';
|
||||||
export * from './commands/types/actions/move-action';
|
export * from './commands/types/actions/move-action';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue