mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-25 22:25:53 +02:00
Archive commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import './bootstrap-common';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { AdminAppModule } from './components/admin/admin.module';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AdminAppModule, { preserveWhitespaces: true });
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'focus-visible';
|
||||
import 'core-js/es';
|
||||
import 'core-js/stable/promise/finally';
|
||||
import 'core-js/proposals/reflect-metadata';
|
||||
import 'zone.js/dist/zone';
|
||||
import 'zone.js/dist/long-stack-trace-zone';
|
||||
import 'canvas-toBlob';
|
||||
import './client/polyfils';
|
||||
import { enableProdMode } from '@angular/core';
|
||||
|
||||
if (document.body.getAttribute('data-debug') !== 'true' || localStorage.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.hot) {
|
||||
module.hot.accept();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import './bootstrap-common';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { AppModule } from './components/app/app.module';
|
||||
import { host, local } from './client/data';
|
||||
|
||||
if (DEVELOPMENT || local || host === `${location.protocol}//${location.host}/`) {
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import './bootstrap-common';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { ToolsAppModule } from './components/tools/tools.module';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(ToolsAppModule, { preserveWhitespaces: true });
|
||||
@@ -0,0 +1,13 @@
|
||||
import './bootstrap-common';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { AppModule } from './components/app/app.module';
|
||||
import { host, local } from './client/data';
|
||||
|
||||
if (DEVELOPMENT || local || host === `${location.protocol}//${location.host}/`) {
|
||||
if (window.opener && window.opener.postMessage && window.URL) {
|
||||
const path = location.href.replace(host, '');
|
||||
window.opener.postMessage({ type: 'loaded-page', path }, '*');
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
import { compact } from 'lodash';
|
||||
import {
|
||||
Expression, ExpressionButtonAction, CommandButtonAction, ActionButtonAction, ItemButtonAction,
|
||||
ColorShadow, Eye, Muzzle, Iris, ButtonActionSlot, ExpressionExtra, ButtonAction, ChatType, BodyAnimation,
|
||||
Action, isPartyChat, EntityButtonAction, defaultDrawOptions
|
||||
} from '../common/interfaces';
|
||||
import * as sprites from '../generated/sprites';
|
||||
import { createExpression } from './clientUtils';
|
||||
import { encodeExpression, decodeExpression } from '../common/encoders/expressionEncoder';
|
||||
import { PonyTownGame } from './game';
|
||||
import { boopAction, upAction, downAction, turnHeadAction } from './playerActions';
|
||||
import { ACTIONS_LIMIT, COMMAND_ACTION_TIME_DELAY } from '../common/constants';
|
||||
import { cloneDeep, hasFlag } from '../common/utils';
|
||||
import { boop, defaultHeadFrame, stand, sneeze, yawn, lie, sit, fly, laugh } from './ponyAnimations';
|
||||
import { createDefaultPony, syncLockedPonyInfo, toPalette, mockPaletteManager } from '../common/ponyInfo';
|
||||
import {
|
||||
ACTION_EXPRESSION_EYE_COLOR, ACTION_EXPRESSION_BG, ACTION_ACTION_COAT_COLOR, WHITE, HEARTS_COLOR,
|
||||
ACTION_COMMAND_BG, BLACK, ACTION_ACTION_BG, ACTION_ITEM_BG, blushColor, ENTITY_ITEM_BG, TRANSPARENT
|
||||
} from '../common/colors';
|
||||
import { resizeCanvasWithRatio, getPixelRatio, disableImageSmoothing } from './canvasUtils';
|
||||
import { drawCanvas, ContextSpriteBatch } from '../graphics/contextSpriteBatch';
|
||||
import { defaultPonyState, defaultDrawPonyOptions } from './ponyHelpers';
|
||||
import { drawHead, drawPony } from './ponyDraw';
|
||||
import { parseColor, toGrayscale, colorToHexRGB } from '../common/color';
|
||||
import { rect, addRects, centerPoint } from '../common/rect';
|
||||
import { drawTextAligned, HAlign, VAlign } from '../graphics/spriteFont';
|
||||
import { fontPal } from '../client/fonts';
|
||||
import { isPonyLying, isPonySitting, isPonyStanding, isPonyFlying } from '../common/entityUtils';
|
||||
import { canPonyFly } from '../common/pony';
|
||||
import { apple2, createAnEntity } from '../common/entities';
|
||||
import { fakePaletteManager } from '../common/mixins';
|
||||
import { spriteSheetsLoaded } from './spriteUtils';
|
||||
import { getEntityTypesFromName } from '../components/services/model';
|
||||
import { toWorldY, toWorldX } from '../common/positionUtils';
|
||||
|
||||
const CANVAS_SIZE = 29;
|
||||
const ICON_SIZE = 16;
|
||||
|
||||
const headX = -26;
|
||||
const headY = -30;
|
||||
const headlessBoopFrame = { ...cloneDeep(boop.frames[7]), head: 0 };
|
||||
const headlessBoop: BodyAnimation = { name: '', loop: false, fps: 1, frames: [headlessBoopFrame] };
|
||||
|
||||
function createPony(coatColor: string, wings = false, horn = false) {
|
||||
const info = createDefaultPony();
|
||||
info.coatFill = coatColor;
|
||||
info.mane!.type = 0;
|
||||
info.backMane!.type = 0;
|
||||
info.tail!.type = 0;
|
||||
info.eyeColorRight = ACTION_EXPRESSION_EYE_COLOR;
|
||||
|
||||
if (wings) {
|
||||
info.wings!.type = 1;
|
||||
}
|
||||
|
||||
if (horn) {
|
||||
info.horn!.type = 1;
|
||||
}
|
||||
|
||||
syncLockedPonyInfo(info);
|
||||
return toPalette(info, mockPaletteManager);
|
||||
}
|
||||
|
||||
function createState() {
|
||||
const state = defaultPonyState();
|
||||
state.blushColor = blushColor(parseColor(ACTION_ACTION_COAT_COLOR));
|
||||
return state;
|
||||
}
|
||||
|
||||
function colorToGrayscale(value: string) {
|
||||
return colorToHexRGB(toGrayscale(parseColor(value)));
|
||||
}
|
||||
|
||||
const ACTION_ACTION_BG_DISABLED = toGrayscale(parseColor(ACTION_ACTION_BG));
|
||||
const expressionPony = createPony(ACTION_EXPRESSION_BG);
|
||||
const actionPony = createPony(ACTION_ACTION_COAT_COLOR);
|
||||
const actionPonyWithHorn = createPony(ACTION_ACTION_COAT_COLOR, false, true);
|
||||
const actionPonyWithWings = createPony(ACTION_ACTION_COAT_COLOR, true);
|
||||
const actionPonyDisabled = createPony(colorToGrayscale(ACTION_ACTION_COAT_COLOR));
|
||||
const actionPonyWithWingsDisabled = createPony(colorToGrayscale(ACTION_ACTION_COAT_COLOR), true);
|
||||
const defaultPalette = mockPaletteManager.addArray(sprites.defaultPalette);
|
||||
export const actionExpressionDefaultPalette = mockPaletteManager.add(Array.from(sprites.defaultPalette));
|
||||
expressionPony.defaultPalette = actionExpressionDefaultPalette;
|
||||
expressionPony.defaultPalette.colors[4] = 0xe16200ff; // tongue color
|
||||
|
||||
export function expressionButtonAction(expression: Expression | undefined): ExpressionButtonAction {
|
||||
return { type: 'expression', expression, title: expression ? '' : 'Reset expression' };
|
||||
}
|
||||
|
||||
export function commandButtonAction(command: string, icon: string): CommandButtonAction {
|
||||
return { type: 'command', command, title: command, icon };
|
||||
}
|
||||
|
||||
export function actionButtonAction(action: string, title: string, sendAction = Action.None): ActionButtonAction {
|
||||
return { type: 'action', action, title, sendAction };
|
||||
}
|
||||
|
||||
export function itemButtonAction(icon: ColorShadow, count?: number): ItemButtonAction {
|
||||
return { type: 'item', icon, count };
|
||||
}
|
||||
|
||||
export function entityButtonAction(entity: string): EntityButtonAction {
|
||||
return { type: 'entity', entity, title: entity };
|
||||
}
|
||||
|
||||
const actionActions = [
|
||||
actionButtonAction('boop', 'Boop'),
|
||||
actionButtonAction('down', 'Sit down / Land'),
|
||||
actionButtonAction('up', 'Stand up / Fly up'),
|
||||
actionButtonAction('turn-head', 'Turn head'),
|
||||
actionButtonAction('sneeze', 'Sneeze', Action.Sneeze),
|
||||
actionButtonAction('sleep', 'Sleep', Action.Sleep),
|
||||
actionButtonAction('yawn', 'Yawn', Action.Yawn),
|
||||
actionButtonAction('love', 'Love', Action.Love),
|
||||
actionButtonAction('laugh', 'Laugh', Action.Laugh),
|
||||
actionButtonAction('blush', 'Blush', Action.Blush),
|
||||
actionButtonAction('drop', 'Drop item', Action.Drop),
|
||||
actionButtonAction('drop-toy', 'Drop toy', Action.DropToy),
|
||||
actionButtonAction('magic', 'Magic', Action.Magic),
|
||||
actionButtonAction('switch-tool', 'Switch tool', Action.SwitchTool),
|
||||
actionButtonAction('switch-entity', 'Switch item to place'),
|
||||
actionButtonAction('switch-entity-rev', 'Switch item to place (reverse)'),
|
||||
actionButtonAction('switch-tile', 'Switch tile to place'),
|
||||
];
|
||||
|
||||
const commandActions = [
|
||||
commandButtonAction('/roll', '🎲'),
|
||||
commandButtonAction('/gifts', '🎁'),
|
||||
commandButtonAction('/candies', '🍬'),
|
||||
commandButtonAction('/clovers', '🍀'),
|
||||
commandButtonAction('/toys', '🎅'),
|
||||
commandButtonAction('/eggs', '🥚'),
|
||||
];
|
||||
|
||||
const additionalActionsActions = [
|
||||
expressionButtonAction(undefined),
|
||||
];
|
||||
|
||||
function getActionAction(action: string) {
|
||||
return actionActions.find(a => a.action === action);
|
||||
}
|
||||
|
||||
function getCommandAction(command: string) {
|
||||
return commandActions.find(a => a.command === command);
|
||||
}
|
||||
|
||||
export function createButtionActionActions() {
|
||||
return [...actionActions, ...additionalActionsActions];
|
||||
}
|
||||
|
||||
export function createButtonCommandActions() {
|
||||
return [...commandActions];
|
||||
}
|
||||
|
||||
export function createDefaultButtonActions(): ButtonActionSlot[] {
|
||||
return DEVELOPMENT ? [
|
||||
{ action: getActionAction('boop') },
|
||||
{ action: getActionAction('down') },
|
||||
{ action: getActionAction('up') },
|
||||
{ action: getActionAction('turn-head') },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Closed, Eye.Closed, Muzzle.Smile)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral3, Muzzle.Smile)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Scrunch, Iris.Forward, Iris.Up)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Angry, Eye.Angry, Muzzle.Scrunch)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Neutral3, Eye.Neutral3, Muzzle.Flat, Iris.Left, Iris.Left)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.X, Eye.X, Muzzle.Flat)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Neutral4, Eye.Neutral4, Muzzle.Flat)) },
|
||||
{ action: undefined },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Shocked, Iris.Shocked)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left)) },
|
||||
{ action: getCommandAction('/roll') },
|
||||
{ action: itemButtonAction(sprites.flower_2, 14) },
|
||||
{ action: itemButtonAction(sprites.apple_1, 5) },
|
||||
{ action: itemButtonAction(sprites.pumpkin_default) },
|
||||
{ action: itemButtonAction(sprites.tree_1, 2) },
|
||||
{
|
||||
action: expressionButtonAction(
|
||||
createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Shocked, Iris.Shocked, ExpressionExtra.Tears))
|
||||
},
|
||||
{
|
||||
action: expressionButtonAction(
|
||||
createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, ExpressionExtra.Cry))
|
||||
},
|
||||
{
|
||||
action: expressionButtonAction(
|
||||
createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, ExpressionExtra.Hearts))
|
||||
},
|
||||
{
|
||||
action: expressionButtonAction(
|
||||
createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, ExpressionExtra.Zzz))
|
||||
},
|
||||
{
|
||||
action: expressionButtonAction(
|
||||
createExpression(
|
||||
Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left,
|
||||
ExpressionExtra.Zzz | ExpressionExtra.Cry | ExpressionExtra.Hearts | ExpressionExtra.Blush))
|
||||
},
|
||||
] : [
|
||||
{ action: getActionAction('boop') },
|
||||
{ action: getActionAction('down') },
|
||||
{ action: getActionAction('up') },
|
||||
{ action: getActionAction('turn-head') },
|
||||
{ action: getActionAction('magic') },
|
||||
{ action: expressionButtonAction(undefined) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Closed, Eye.Closed, Muzzle.Smile)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral3, Muzzle.Smile)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Scrunch, Iris.Forward, Iris.Up)) },
|
||||
{ action: expressionButtonAction(createExpression(Eye.Angry, Eye.Angry, Muzzle.Scrunch)) },
|
||||
];
|
||||
}
|
||||
|
||||
export function serializeActions(slots: ButtonActionSlot[]): string {
|
||||
const serialized = slots.slice(0, ACTIONS_LIMIT).map(serializeAction);
|
||||
|
||||
while (serialized.length && !serialized[serialized.length - 1]) {
|
||||
serialized.pop();
|
||||
}
|
||||
|
||||
return JSON.stringify(serialized);
|
||||
}
|
||||
|
||||
export function deserializeActions(data: string): ButtonActionSlot[] {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
return json.slice(0, ACTIONS_LIMIT).map(deserializeAction);
|
||||
} catch (e) {
|
||||
DEVELOPMENT && console.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function serializeAction({ action }: ButtonActionSlot): any {
|
||||
if (action) {
|
||||
switch (action.type) {
|
||||
case 'action':
|
||||
return { act: action.action };
|
||||
case 'command':
|
||||
return { cmd: action.command };
|
||||
case 'expression':
|
||||
return { exp: encodeExpression(action.expression) };
|
||||
case 'entity':
|
||||
return { ent: action.entity };
|
||||
default:
|
||||
DEVELOPMENT && console.warn(`Missing serialization for ${JSON.stringify(action)}`);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function deserializeAction(data: any): ButtonActionSlot {
|
||||
if (data) {
|
||||
if ('act' in data || 'action' in data) {
|
||||
return { action: getActionAction(data.act || data.action) };
|
||||
} else if ('cmd' in data || 'command' in data) {
|
||||
return { action: getCommandAction(data.cmd || data.command) };
|
||||
} else if ('exp' in data || 'expression' in data) {
|
||||
const expression = decodeExpression(data.exp || data.expression | 0);
|
||||
return { action: expressionButtonAction(expression) };
|
||||
} else if ('ent' in data || 'entity' in data) {
|
||||
return { action: entityButtonAction(data.ent || data.entity) };
|
||||
} else {
|
||||
DEVELOPMENT && console.warn(`Missing deserialization for ${JSON.stringify(data)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { action: undefined };
|
||||
}
|
||||
|
||||
const lastCommandCalls: { [key: string]: number; } = {};
|
||||
|
||||
export function useAction(game: PonyTownGame, action: ButtonAction | undefined) {
|
||||
if (action) {
|
||||
switch (action.type) {
|
||||
case 'expression':
|
||||
game.send(server => server.expression(encodeExpression(action.expression)));
|
||||
break;
|
||||
case 'action':
|
||||
if (action.sendAction) {
|
||||
game.send(server => server.action(action.sendAction));
|
||||
} else {
|
||||
switch (action.action) {
|
||||
case 'boop':
|
||||
boopAction(game);
|
||||
break;
|
||||
case 'up':
|
||||
upAction(game);
|
||||
break;
|
||||
case 'down':
|
||||
downAction(game);
|
||||
break;
|
||||
case 'turn-head':
|
||||
turnHeadAction(game);
|
||||
break;
|
||||
case 'switch-entity':
|
||||
game.send(server => server.action(Action.SwitchToPlaceTool));
|
||||
game.changePlaceEntity(false);
|
||||
break;
|
||||
case 'switch-entity-rev':
|
||||
game.send(server => server.action(Action.SwitchToPlaceTool));
|
||||
game.changePlaceEntity(true);
|
||||
break;
|
||||
case 'switch-tile':
|
||||
game.send(server => server.action(Action.SwitchToTileTool));
|
||||
game.changePlaceTile(false);
|
||||
break;
|
||||
default:
|
||||
console.log('Action not supported: ', action.action);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'command':
|
||||
const now = performance.now();
|
||||
const lastCall = lastCommandCalls[action.command] | 0;
|
||||
|
||||
if ((now - lastCall) > COMMAND_ACTION_TIME_DELAY) {
|
||||
lastCommandCalls[action.command] = now;
|
||||
const chatType = isPartyChat(game.lastChatMessageType) ? ChatType.Party : ChatType.Say;
|
||||
game.send(server => server.say(0, action.command, chatType));
|
||||
}
|
||||
break;
|
||||
case 'entity':
|
||||
if (BETA) {
|
||||
game.editor.type = action.entity;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('Action type not supported: ', action.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRedrawAction(action: ButtonAction | undefined, state: any, game: PonyTownGame) {
|
||||
if (action !== state.action) {
|
||||
return true;
|
||||
} else if (action) {
|
||||
switch (action.type) {
|
||||
case 'action': {
|
||||
switch (action.action) {
|
||||
case 'up':
|
||||
return state.draw !== getUpDrawFunc(game);
|
||||
case 'down':
|
||||
return state.draw !== getDownDrawFunc(game);
|
||||
// case 'turn-head':
|
||||
// return state.right !== (game.player && isHeadFacingRight(game.player));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const canvasCache = new Map<string, HTMLCanvasElement>();
|
||||
const palette = mockPaletteManager.addArray(sprites.fontPalette);
|
||||
const emojiPalette = mockPaletteManager.addArray(sprites.emojiPalette);
|
||||
|
||||
function drawCanvasCached(key: string, action: (batch: ContextSpriteBatch) => void) {
|
||||
const canvas = canvasCache.get(key) || drawCanvas(ICON_SIZE, ICON_SIZE, sprites.paletteSpriteSheet, undefined, action);
|
||||
canvasCache.set(key, canvas);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
export function drawAction(canvas: HTMLCanvasElement, action: ButtonAction | undefined, state: any, game: PonyTownGame) {
|
||||
if (resizeCanvasWithRatio(canvas, CANVAS_SIZE, CANVAS_SIZE)) {
|
||||
state.action = 0;
|
||||
}
|
||||
|
||||
if (!spriteSheetsLoaded || !shouldRedrawAction(action, state, game))
|
||||
return;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context)
|
||||
return;
|
||||
|
||||
state.action = action;
|
||||
|
||||
context.save();
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
disableImageSmoothing(context);
|
||||
|
||||
const scale = 2 * getPixelRatio();
|
||||
const bufferSize = ICON_SIZE;
|
||||
|
||||
if (action) {
|
||||
switch (action.type) {
|
||||
case 'expression': {
|
||||
const buffer = drawCanvas(bufferSize, bufferSize, sprites.paletteSpriteSheet, undefined, batch => {
|
||||
const state = { ...createState(), expression: action.expression };
|
||||
const options = { ...defaultDrawPonyOptions(), noEars: true };
|
||||
drawHead(batch, expressionPony, headX, headY, undefined, defaultHeadFrame, state, options, false, 0);
|
||||
|
||||
if (action.expression) {
|
||||
const extra = action.expression.extra;
|
||||
|
||||
if (hasFlag(extra, ExpressionExtra.Zzz)) {
|
||||
batch.drawSprite(sprites.emote_sleep1.frames[13], WHITE, defaultPalette, headX + 15, headY + 3);
|
||||
}
|
||||
|
||||
if (hasFlag(extra, ExpressionExtra.Hearts)) {
|
||||
batch.drawSprite(sprites.emote_hearts.frames[41], HEARTS_COLOR, defaultPalette, headX + 8, headY + 22);
|
||||
}
|
||||
|
||||
if (hasFlag(extra, ExpressionExtra.Cry)) {
|
||||
batch.drawSprite(sprites.emote_cry2.frames[4], WHITE, defaultPalette, headX, headY);
|
||||
} else if (hasFlag(extra, ExpressionExtra.Tears)) {
|
||||
batch.drawSprite(sprites.emote_tears.frames[0], WHITE, defaultPalette, headX, headY);
|
||||
}
|
||||
} else {
|
||||
const color = parseColor(ACTION_EXPRESSION_BG);
|
||||
batch.drawRect(color, 0, 3, 15, 5);
|
||||
batch.drawRect(color, 0, 8, 3, 1);
|
||||
batch.drawRect(color, 8, 8, 4, 1);
|
||||
}
|
||||
});
|
||||
|
||||
context.fillStyle = ACTION_EXPRESSION_BG;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.scale(scale, scale);
|
||||
context.drawImage(buffer, 0, 0);
|
||||
break;
|
||||
}
|
||||
case 'command': {
|
||||
const buffer = drawCanvasCached(`command:${action.icon}`, batch => {
|
||||
const bounds = rect(0, 0, 15, 15);
|
||||
const options = { palette, emojiPalette };
|
||||
drawTextAligned(batch, action.icon, fontPal, BLACK, bounds, HAlign.Center, VAlign.Middle, options);
|
||||
});
|
||||
|
||||
context.fillStyle = ACTION_COMMAND_BG;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.scale(scale, scale);
|
||||
context.drawImage(buffer, -0.5, 0.5);
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
let buffer: HTMLCanvasElement;
|
||||
|
||||
if (action.action === 'up' || action.action === 'down') {
|
||||
state.draw = action.action === 'up' ? getUpDrawFunc(game) : getDownDrawFunc(game);
|
||||
|
||||
buffer = drawCanvasCached(`action:${action.action}:${state.draw}`, batch => {
|
||||
state.draw && getDrawFuncByName(state.draw)(batch);
|
||||
});
|
||||
} else {
|
||||
buffer = drawCanvasCached(`action:${action.action}`, batch => {
|
||||
switch (action.action) {
|
||||
case 'boop': {
|
||||
const state = { ...createState(), animation: headlessBoop, animationFrame: 0 };
|
||||
drawPony(batch, actionPony, state, 25, 32, defaultDrawPonyOptions());
|
||||
break;
|
||||
}
|
||||
case 'turn-head': {
|
||||
// state.right = game.player && isHeadFacingRight(game.player);
|
||||
const ponyState = { ...createState(), animation: stand };
|
||||
drawPony(batch, actionPony, ponyState, 15, 40, defaultDrawPonyOptions());
|
||||
|
||||
// if (!state.right) {
|
||||
// context.translate(context.canvas.width, 0);
|
||||
// context.scale(-1, 1);
|
||||
// }
|
||||
break;
|
||||
}
|
||||
case 'sneeze': {
|
||||
const state = { ...createState(), headAnimation: sneeze, headAnimationFrame: 3 };
|
||||
drawPony(batch, actionPony, state, 17, 40, defaultDrawPonyOptions());
|
||||
break;
|
||||
}
|
||||
case 'sleep': {
|
||||
const state = { ...createState(), expression: createExpression(Eye.Closed, Eye.Closed, Muzzle.Neutral) };
|
||||
drawPony(batch, actionPony, state, 18, 40, defaultDrawPonyOptions());
|
||||
batch.drawSprite(sprites.emote_sleep1.frames[13], WHITE, defaultPalette, headX + 15, headY + 3);
|
||||
break;
|
||||
}
|
||||
case 'drop': {
|
||||
const state = { ...createState(), holding: fakePaletteManager(() => apple2(0, 0)) };
|
||||
drawPony(batch, actionPony, state, 20, 40, defaultDrawPonyOptions());
|
||||
batch.drawSprite(sprites.arrow_down, BLACK, defaultPalette, 1, 3);
|
||||
break;
|
||||
}
|
||||
case 'drop-toy': {
|
||||
const state = { ...createState() };
|
||||
const options = { ...defaultDrawPonyOptions(), toy: 17 };
|
||||
drawPony(batch, actionPony, state, 18, 52, options);
|
||||
batch.drawSprite(sprites.arrow_down, BLACK, defaultPalette, 1, 3);
|
||||
break;
|
||||
}
|
||||
case 'yawn': {
|
||||
const state = { ...createState(), headAnimation: yawn, headAnimationFrame: 3 };
|
||||
drawPony(batch, actionPony, state, 17, 40, defaultDrawPonyOptions());
|
||||
break;
|
||||
}
|
||||
case 'laugh': {
|
||||
const state = { ...createState(), headAnimation: laugh, headAnimationFrame: 3 };
|
||||
drawPony(batch, actionPony, state, 17, 38, defaultDrawPonyOptions());
|
||||
break;
|
||||
}
|
||||
case 'blush': {
|
||||
const state = {
|
||||
...createState(), expression: createExpression(
|
||||
Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush)
|
||||
};
|
||||
drawPony(batch, actionPony, state, 17, 40, defaultDrawPonyOptions());
|
||||
break;
|
||||
}
|
||||
case 'love': {
|
||||
batch.drawSprite(sprites.emote_hearts.frames[10], 0xbc414fff, defaultPalette, headX + 2, headY + 18);
|
||||
break;
|
||||
}
|
||||
case 'magic': {
|
||||
const state = { ...createState(), headAnimation: laugh, headAnimationFrame: 3 };
|
||||
drawPony(batch, actionPonyWithHorn, state, 17, 48, defaultDrawPonyOptions());
|
||||
batch.drawSprite(sprites.magic_icon, WHITE, defaultPalette, 4, 2);
|
||||
break;
|
||||
}
|
||||
case 'switch-tool': {
|
||||
const palette = mockPaletteManager.addArray(sprites.tools_icon.palettes![0]);
|
||||
batch.drawSprite(sprites.tools_icon.color, WHITE, palette, 0, 2);
|
||||
break;
|
||||
}
|
||||
case 'switch-entity': {
|
||||
const palette = mockPaletteManager.addArray(sprites.hammer.palettes![0]);
|
||||
batch.drawSprite(sprites.hammer.color, WHITE, palette, 2, 2);
|
||||
break;
|
||||
}
|
||||
case 'switch-entity-rev': {
|
||||
const palette = mockPaletteManager.addArray(sprites.hammer.palettes![0]);
|
||||
batch.drawSprite(sprites.hammer.color, WHITE, palette, 2, 3);
|
||||
batch.drawSprite(sprites.arrow_left, BLACK, defaultPalette, 1, 1);
|
||||
break;
|
||||
}
|
||||
case 'switch-tile': {
|
||||
const palette = mockPaletteManager.addArray(sprites.hammer.palettes![0]);
|
||||
batch.drawSprite(sprites.hammer.color, WHITE, palette, 2, 2);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid action: ${action.action}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
context.fillStyle = ACTION_ACTION_BG;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.scale(scale, scale);
|
||||
context.drawImage(buffer, 0, 0);
|
||||
break;
|
||||
}
|
||||
case 'item': {
|
||||
const buffer = drawCanvas(bufferSize, bufferSize, sprites.paletteSpriteSheet, undefined, batch => {
|
||||
const palette = mockPaletteManager.addArray(action.icon.palettes![0]);
|
||||
const sprite = action.icon.color;
|
||||
batch.drawSprite(sprite, WHITE, palette,
|
||||
Math.round((15 - sprite.w) / 2), Math.round((15 - sprite.h) / 2) + 1);
|
||||
});
|
||||
|
||||
context.fillStyle = ACTION_ITEM_BG;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.scale(scale, scale);
|
||||
context.drawImage(buffer, -0.5, -0.5);
|
||||
break;
|
||||
}
|
||||
case 'entity': {
|
||||
if (BETA) {
|
||||
const types = getEntityTypesFromName(action.entity) || [];
|
||||
const size = bufferSize * scale;
|
||||
const entities = types.map(type => createAnEntity(type, 0, 0, 0, {}, mockPaletteManager, game));
|
||||
// createAnEntity(type, 0, toWorldX(size / 2 - 2), toWorldY(size * 0.75), {}, mockPaletteManager));
|
||||
|
||||
const bounds = compact(entities.map(e => e.bounds)).reduce(addRects, rect(0, 0, 0, 0));
|
||||
const center = centerPoint(bounds);
|
||||
|
||||
const buffer = drawCanvas(size, size, sprites.paletteSpriteSheet, undefined, batch => {
|
||||
for (const entity of entities) {
|
||||
if (entity.draw) {
|
||||
entity.x += toWorldX(size / 2 - 2 - center.x);
|
||||
entity.y += toWorldY(size / 2 - center.y);
|
||||
entity.draw(batch, { ...defaultDrawOptions, shadowColor: TRANSPARENT });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
context.fillStyle = ENTITY_ITEM_BG;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
context.drawImage(buffer, 0, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawLie(batch: ContextSpriteBatch) {
|
||||
const state = { ...createState(), animation: lie };
|
||||
drawPony(batch, actionPony, state, -6, 15, defaultDrawPonyOptions());
|
||||
}
|
||||
|
||||
function drawLieDisabled(batch: ContextSpriteBatch) {
|
||||
const state = { ...createState(), animation: lie };
|
||||
batch.drawRect(ACTION_ACTION_BG_DISABLED, 0, 0, 50, 50);
|
||||
drawPony(batch, actionPonyDisabled, state, -6, 15, defaultDrawPonyOptions());
|
||||
}
|
||||
|
||||
function drawSit(batch: ContextSpriteBatch) {
|
||||
const state = { ...createState(), animation: sit };
|
||||
drawPony(batch, actionPony, state, -6, 15, defaultDrawPonyOptions());
|
||||
}
|
||||
|
||||
function drawStand(batch: ContextSpriteBatch) {
|
||||
const state = { ...createState(), animation: stand };
|
||||
drawPony(batch, actionPony, state, -1, 15, defaultDrawPonyOptions());
|
||||
}
|
||||
|
||||
function drawFly(batch: ContextSpriteBatch) {
|
||||
const state = { ...createState(), animation: fly };
|
||||
drawPony(batch, actionPonyWithWings, state, 0, 30, defaultDrawPonyOptions());
|
||||
}
|
||||
|
||||
function drawFlyDisabled(batch: ContextSpriteBatch) {
|
||||
const state = { ...createState(), animation: fly };
|
||||
batch.drawRect(ACTION_ACTION_BG_DISABLED, 0, 0, 50, 50);
|
||||
drawPony(batch, actionPonyWithWingsDisabled, state, 0, 30, defaultDrawPonyOptions());
|
||||
}
|
||||
|
||||
function getDrawFuncByName(name: string) {
|
||||
switch (name) {
|
||||
case 'lie': return drawLie;
|
||||
case 'sit': return drawSit;
|
||||
case 'stand': return drawStand;
|
||||
case 'fly': return drawFly;
|
||||
case 'flyDisabled': return drawFlyDisabled;
|
||||
case 'lieDisabled': return drawLieDisabled;
|
||||
default:
|
||||
throw new Error(`Invalid name: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getUpDrawFunc(game: PonyTownGame) {
|
||||
const player = game.player;
|
||||
|
||||
if (player) {
|
||||
if (isPonyLying(player)) {
|
||||
return 'sit';
|
||||
} else if (isPonySitting(player)) {
|
||||
return 'stand';
|
||||
} else if (isPonyStanding(player) && canPonyFly(player)) {
|
||||
return 'fly';
|
||||
}
|
||||
}
|
||||
|
||||
return 'flyDisabled';
|
||||
}
|
||||
|
||||
function getDownDrawFunc(game: PonyTownGame) {
|
||||
const player = game.player;
|
||||
|
||||
if (player) {
|
||||
if (isPonySitting(player)) {
|
||||
return 'lie';
|
||||
} else if (isPonyStanding(player)) {
|
||||
return 'sit';
|
||||
} else if (isPonyFlying(player)) {
|
||||
return 'stand';
|
||||
}
|
||||
}
|
||||
|
||||
return 'lieDisabled';
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
/* istanbul ignore next */
|
||||
export let createCanvas = (width: number, height: number): HTMLCanvasElement => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width | 0;
|
||||
canvas.height = height | 0;
|
||||
return canvas;
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
export let loadImage = (src: string): Promise<HTMLImageElement | ImageBitmap> => {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.addEventListener('load', () => resolve(img));
|
||||
img.addEventListener('error', () => reject(new Error(`Error loading image (${src})`)));
|
||||
img.src = src;
|
||||
});
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
function canUseImageBitmap() {
|
||||
return typeof fetch === 'function' &&
|
||||
typeof createImageBitmap === 'function' &&
|
||||
!/yabrowser/i.test(navigator.userAgent); // disabled due to yandex browser bug
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (canUseImageBitmap()) {
|
||||
loadImage = src => fetch(src)
|
||||
.then(response => response.blob())
|
||||
.then(createImageBitmap);
|
||||
}
|
||||
|
||||
export function setup(methods: {
|
||||
createCanvas(width: number, height: number): HTMLCanvasElement;
|
||||
loadImage(src: string): Promise<HTMLImageElement>;
|
||||
}) {
|
||||
createCanvas = methods.createCanvas;
|
||||
loadImage = methods.loadImage;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const getPixelRatio = SERVER ? () => 1 : () => window.devicePixelRatio;
|
||||
|
||||
export function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number) {
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
export function resizeCanvasWithRatio(canvas: HTMLCanvasElement, width: number, height: number, updateStyle = true) {
|
||||
const ratio = getPixelRatio();
|
||||
const w = Math.round(width * ratio);
|
||||
const h = Math.round(height * ratio);
|
||||
let resized = false;
|
||||
|
||||
if (canvas.width !== w || canvas.height !== h) {
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
resized = true;
|
||||
}
|
||||
|
||||
if (updateStyle && (canvas.style.width !== width + 'px' || canvas.style.height !== height + 'px')) {
|
||||
canvas.style.width = width + 'px';
|
||||
canvas.style.height = height + 'px';
|
||||
resized = true;
|
||||
}
|
||||
|
||||
return resized;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function canvasToSource(canvas: HTMLCanvasElement) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
canvas.toBlob(blob => {
|
||||
if (blob) {
|
||||
resolve(URL.createObjectURL(blob));
|
||||
} else {
|
||||
reject(new Error('Failed to convert canvas'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function saveCanvas(canvas: HTMLCanvasElement, name: string) {
|
||||
canvas.toBlob(blob => blob && saveAs(blob, name));
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function disableImageSmoothing(context: CanvasRenderingContext2D) {
|
||||
if ('imageSmoothingEnabled' in context) {
|
||||
context.imageSmoothingEnabled = false;
|
||||
} else {
|
||||
(context as any).webkitImageSmoothingEnabled = false;
|
||||
(context as any).mozImageSmoothingEnabled = false;
|
||||
(context as any).msImageSmoothingEnabled = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import { NgZone } from '@angular/core';
|
||||
import { Method, SocketClient, Bin, getMethods } from 'ag-sockets/dist/browser';
|
||||
import {
|
||||
MapInfo, WorldState, PartyMember, PartyFlags, Action, NotificationFlags, Pony, LeaveReason,
|
||||
SayData, MapState, defaultMapState, Apply, InfoFlags, PonyData, FriendStatusData, WorldMap
|
||||
} from '../common/interfaces';
|
||||
import { hasFlag, findById } from '../common/utils';
|
||||
import { isPony } from '../common/pony';
|
||||
import { setTileAtRegion, findEntityById, createWorldMap, removeRegions, updateMapState } from '../common/worldMap';
|
||||
import { GameService } from '../components/services/gameService';
|
||||
import { PonyTownGame } from './game';
|
||||
import { supportsLetAndConst, isInIncognitoMode } from './clientUtils';
|
||||
import { savePlayerPosition, setAclCookie } from './sec';
|
||||
import { updateParty } from './partyUtils';
|
||||
import { addNotification, removeNotification, markGameAsLoaded, resetGameFields, isSelected } from './gameUtils';
|
||||
import { Model } from '../components/services/model';
|
||||
import { decodeUpdate } from '../common/encoders/updateDecoder';
|
||||
import {
|
||||
updatePonyInfoWithPoof, subscribeRegion, handleUpdates, handleUpdateEntity, handleRemoveEntity, handleSays,
|
||||
handleEntityInfo, handleUpdatePonies, filterEntityName, handleUpdateFriends
|
||||
} from './handlers';
|
||||
import { nameToHTML } from './emoji';
|
||||
|
||||
const BinEntityId = Bin.U32;
|
||||
const BinEntityPlayerState = Bin.U8;
|
||||
const BinNotificationId = Bin.U16;
|
||||
const BinSayDatas = [BinEntityId, Bin.Str, Bin.U8];
|
||||
|
||||
function findPonyById(map: WorldMap, id: number) {
|
||||
const entity = findEntityById(map, id);
|
||||
return entity && isPony(entity) ? entity : undefined;
|
||||
}
|
||||
|
||||
export class ClientActions implements SocketClient {
|
||||
constructor(private gameService: GameService, private game: PonyTownGame, private model: Model, private zone: NgZone) {
|
||||
}
|
||||
private apply: Apply = func => this.zone.run(func);
|
||||
connected() {
|
||||
resetGameFields(this.game);
|
||||
this.game.map = createWorldMap();
|
||||
this.game.player = undefined;
|
||||
this.game.joined();
|
||||
this.apply(() => this.gameService.joined());
|
||||
|
||||
const supportsWasm = typeof WebAssembly !== 'undefined';
|
||||
const info = 0 |
|
||||
(isInIncognitoMode ? InfoFlags.Incognito : 0) |
|
||||
(supportsWasm ? InfoFlags.SupportsWASM : 0) |
|
||||
(supportsLetAndConst() ? InfoFlags.SupportsLetAndConst : 0);
|
||||
|
||||
this.game.send(server => server.actionParam2(Action.Info, info));
|
||||
}
|
||||
disconnected() {
|
||||
resetGameFields(this.game);
|
||||
this.apply(() => this.gameService.disconnected());
|
||||
}
|
||||
invalidVersion() {
|
||||
DEVELOPMENT && !TESTS && console.error('Invalid version');
|
||||
}
|
||||
@Method({ binary: [Bin.U32] })
|
||||
queue(place: number) {
|
||||
this.game.placeInQueue = place;
|
||||
}
|
||||
@Method({ binary: [Bin.Obj, Bin.Bool] })
|
||||
worldState(state: WorldState, initial: boolean) {
|
||||
this.game.placeInQueue = 0;
|
||||
this.game.setWorldState(state, initial);
|
||||
}
|
||||
@Method({ binary: [Bin.Obj, Bin.Obj] })
|
||||
mapState(info: MapInfo, state: MapState) {
|
||||
this.game.map = createWorldMap(info, state);
|
||||
this.game.player = undefined;
|
||||
this.game.setupMap();
|
||||
updateMapState(this.game.map, defaultMapState, this.game.map.state);
|
||||
}
|
||||
@Method({ binary: [Bin.Obj] })
|
||||
mapUpdate(state: MapState) {
|
||||
const prevState = this.game.map.state;
|
||||
this.game.map.state = state;
|
||||
updateMapState(this.game.map, prevState, this.game.map.state);
|
||||
}
|
||||
@Method({ binary: [] })
|
||||
mapSwitching() {
|
||||
this.game.loaded = false;
|
||||
this.game.placeInQueue = 0;
|
||||
|
||||
if (this.game.player) {
|
||||
this.game.player.vx = 0;
|
||||
this.game.player.vy = 0;
|
||||
}
|
||||
}
|
||||
@Method({ binary: [Bin.I32, Bin.I32, Bin.U8Array] })
|
||||
mapTest(width: number, height: number, buffer: Uint8Array) {
|
||||
const data = new Uint32Array(width * height);
|
||||
(new Uint8Array(data.buffer)).set(buffer);
|
||||
this.game.minimap = { width, height, data };
|
||||
}
|
||||
@Method({ binary: [BinEntityId, Bin.Str, Bin.Str, Bin.Str, Bin.U16] })
|
||||
myEntity(id: number, name: string, info: string, characterId: string, crc: number) {
|
||||
this.game.playerId = id;
|
||||
this.game.playerName = name;
|
||||
this.game.playerInfo = info;
|
||||
this.game.playerCRC = crc;
|
||||
|
||||
const pony = findById(this.model.ponies, characterId);
|
||||
|
||||
if (pony) {
|
||||
this.model.selectPony(pony);
|
||||
}
|
||||
|
||||
if (this.game.party) {
|
||||
this.game.party.members.forEach(m => m.self = m.id === id);
|
||||
this.game.onPartyUpdate.next();
|
||||
}
|
||||
|
||||
const entity = findEntityById(this.game.map, id) as Pony | undefined;
|
||||
|
||||
if (entity) {
|
||||
entity.name = name;
|
||||
updatePonyInfoWithPoof(this.game, entity, info, crc);
|
||||
}
|
||||
|
||||
this.game.onActionsUpdate.next();
|
||||
}
|
||||
@Method({ binary: [[Bin.U8], [Bin.U8Array], Bin.U8Array, [Bin.U8Array], BinSayDatas] })
|
||||
update(unsubscribes: number[], subscribes: Uint8Array[], updates: Uint8Array | null, regions: Uint8Array[], says: SayData[]) {
|
||||
removeRegions(this.game.map, unsubscribes);
|
||||
|
||||
for (const subscribe of subscribes) {
|
||||
subscribeRegion(this.game, subscribe);
|
||||
}
|
||||
|
||||
if (subscribes.length) {
|
||||
markGameAsLoaded(this.game);
|
||||
}
|
||||
|
||||
if (updates) {
|
||||
handleUpdates(this.game, updates);
|
||||
}
|
||||
|
||||
for (const region of regions) {
|
||||
const { x, y, updates, removes, tiles } = decodeUpdate(region);
|
||||
|
||||
for (const update of updates) {
|
||||
handleUpdateEntity(this.game, update);
|
||||
}
|
||||
|
||||
for (const id of removes) {
|
||||
handleRemoveEntity(this.game, id);
|
||||
}
|
||||
|
||||
for (const tile of tiles) {
|
||||
setTileAtRegion(this.game.map, x, y, tile.x, tile.y, tile.type);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, message, type] of says) {
|
||||
handleSays(this.game, id, message, type);
|
||||
}
|
||||
}
|
||||
@Method({ binary: [Bin.F32, Bin.F32, Bin.Bool] })
|
||||
fixPosition(x: number, y: number, safe: boolean) {
|
||||
if (DEVELOPMENT && !TESTS && !safe) {
|
||||
console.error(`fix position (${x.toFixed(2)}, ${y.toFixed(2)})`);
|
||||
}
|
||||
|
||||
const player = this.game.player;
|
||||
|
||||
if (player) {
|
||||
player.x = x;
|
||||
player.y = y;
|
||||
savePlayerPosition();
|
||||
}
|
||||
|
||||
this.game.send(server => server.fixedPosition());
|
||||
}
|
||||
@Method({ binary: [BinEntityId, Bin.U8, Bin.Obj] })
|
||||
actionParam(id: number, action: Action, param: any) {
|
||||
switch (action) {
|
||||
case Action.ACL:
|
||||
if (id === this.game.playerId && param) {
|
||||
setAclCookie(param);
|
||||
}
|
||||
break;
|
||||
case Action.FriendsCRC:
|
||||
this.game.nextFriendsCRC = 0;
|
||||
break;
|
||||
default:
|
||||
DEVELOPMENT && !TESTS && console.error(`actionParam: Invalid action: ${action}`);
|
||||
}
|
||||
}
|
||||
@Method({ binary: [Bin.U8] })
|
||||
left(reason: LeaveReason) {
|
||||
this.game.player = undefined;
|
||||
this.game.map = createWorldMap();
|
||||
this.apply(() => this.gameService.left('clientActions.left', reason));
|
||||
}
|
||||
@Method({ binary: [BinNotificationId, BinEntityId, Bin.Str, Bin.Str, Bin.Str, Bin.U8] })
|
||||
addNotification(id: number, entityId: number, name: string, message: string, note: string, flags: NotificationFlags) {
|
||||
const defaultCharacter = hasFlag(flags, NotificationFlags.Supporter) ? this.game.supporterPony : this.game.offlinePony;
|
||||
const pony = (entityId && findPonyById(this.game.map, entityId)) || defaultCharacter;
|
||||
|
||||
const filteredName = filterEntityName(this.game, name, hasFlag(flags, NotificationFlags.NameBad));
|
||||
message = message.replace(/#NAME#/g, nameToHTML(filteredName || ''));
|
||||
|
||||
this.apply(() => addNotification(this.game, { id, message, note, pony, flags, open: false, fresh: true }));
|
||||
}
|
||||
@Method({ binary: [BinNotificationId] })
|
||||
removeNotification(id: number) {
|
||||
this.apply(() => removeNotification(this.game, id));
|
||||
}
|
||||
@Method({ binary: [BinEntityId, BinEntityId] })
|
||||
updateSelection(currentId: number, newId: number) {
|
||||
if (isSelected(this.game, currentId)) {
|
||||
this.game.select(newId ? findPonyById(this.game.map, newId) : undefined);
|
||||
}
|
||||
}
|
||||
@Method({ binary: [[BinEntityId, Bin.U8]] })
|
||||
updateParty(party: [number, PartyFlags][] | undefined) {
|
||||
const members = party && party.map<PartyMember>(([id, flags]) => ({
|
||||
id,
|
||||
pony: findPonyById(this.game.map, id) || this.game.fallbackPonies.get(id),
|
||||
self: id === this.game.playerId,
|
||||
leader: hasFlag(flags, PartyFlags.Leader),
|
||||
pending: hasFlag(flags, PartyFlags.Pending),
|
||||
offline: hasFlag(flags, PartyFlags.Offline),
|
||||
}));
|
||||
|
||||
if (members) {
|
||||
const missing = members.filter(p => !p.pony).map(p => p.id);
|
||||
|
||||
if (missing.length) {
|
||||
this.game.send(server => server.getPonies(missing));
|
||||
}
|
||||
}
|
||||
|
||||
this.apply(() => {
|
||||
this.game.party = updateParty(this.game.party, members);
|
||||
this.game.onPartyUpdate.next();
|
||||
});
|
||||
}
|
||||
@Method({ binary: [[BinEntityId, Bin.Obj, Bin.U8Array, Bin.U8Array, BinEntityPlayerState, Bin.Bool]] })
|
||||
updatePonies(ponies: PonyData[]) {
|
||||
handleUpdatePonies(this.game, ponies);
|
||||
}
|
||||
@Method({ binary: [Bin.Obj, Bin.Bool] })
|
||||
updateFriends(friends: FriendStatusData[], removeMissing: boolean) {
|
||||
handleUpdateFriends(this.game, friends, removeMissing);
|
||||
}
|
||||
@Method({ binary: [BinEntityId, Bin.Str, Bin.U32, Bin.Bool] })
|
||||
entityInfo(id: number, name: string, crc: number, nameBad: boolean) {
|
||||
handleEntityInfo(this.game, id, name, crc, nameBad);
|
||||
}
|
||||
@Method({ binary: [Bin.Obj] })
|
||||
entityList(value: { name: string; x: number; y: number; }[]) {
|
||||
if (DEVELOPMENT || BETA) {
|
||||
const list = value.map(({ name, x, y }) => `${name}(${x.toFixed(2)}, ${y.toFixed(2)})`).join('\n');
|
||||
console.log(`ENTITIES:\n${list}`);
|
||||
}
|
||||
}
|
||||
@Method({ binary: [Bin.Obj] })
|
||||
testPositions(data: { frame: number; x: number | undefined; y: number | undefined; moved: boolean; }[]) {
|
||||
if (DEVELOPMENT) {
|
||||
const round = (x: number) => Math.round(x * 100);
|
||||
const same = (ax = 0, ay = 0, bx = 0, by = 0) => round(ax) === round(bx) && round(ay) === round(by);
|
||||
const fmt = (x: number | undefined) => (x === undefined ? '-' : x.toFixed(2)).padStart(5);
|
||||
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
if (data[i - 1].frame !== (data[i].frame - 1)) {
|
||||
data.splice(i, 0, { frame: data[i - 1].frame + 1, x: undefined, y: undefined, moved: false });
|
||||
}
|
||||
}
|
||||
|
||||
const clientIndex = this.game.positions.findIndex(p => p.moved);
|
||||
const serverIndex = data.findIndex(p => p.moved);
|
||||
const offset = serverIndex - clientIndex;
|
||||
|
||||
const dat = data.map((p, i) => {
|
||||
const pt = this.game.positions[i - offset] || { x: undefined, y: undefined };
|
||||
return { frame: p.frame, ax: p.x, ay: p.y, bx: pt.x, by: pt.y, serverMoved: p.moved, clientMoved: pt.moved };
|
||||
});
|
||||
|
||||
const log = dat.map(({ frame, ax, ay, bx, by, serverMoved, clientMoved }, i) =>
|
||||
`${frame.toString().padStart(7)} | ` +
|
||||
`${fmt(ax)}, ${fmt(ay)} ${serverMoved ? 'M' : ' '} | ` +
|
||||
`${fmt(bx)}, ${fmt(by)} ${clientMoved ? 'M' : ' '} | ` +
|
||||
`${same(ax, ay, bx, by) ? '= ' : ' '} ` +
|
||||
`${i > 0 && dat[i - 1].frame !== (frame - 1) ? 'I ' : ' '}`)
|
||||
.join('\n');
|
||||
|
||||
console.log(
|
||||
` frame | server | client | \n` +
|
||||
`-----------------------------------------------\n` +
|
||||
`${log}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (DEVELOPMENT) {
|
||||
getMethods(ClientActions)
|
||||
.filter(m => !m.options.binary)
|
||||
.forEach(m => console.error(`Missing binary encoding for ClientActions.${m.name}()`));
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Method } from 'ag-sockets/dist/browser';
|
||||
import { AdminModel } from '../components/services/adminModel';
|
||||
import { ModelTypes } from '../common/adminInterfaces';
|
||||
import { ModelSubscriber } from '../components/services/modelSubscriber';
|
||||
|
||||
export interface ClientUpdate {
|
||||
type: ModelTypes;
|
||||
id: string;
|
||||
update: any;
|
||||
}
|
||||
|
||||
export class ClientAdminActions {
|
||||
constructor(private model: AdminModel) {
|
||||
}
|
||||
connected() {
|
||||
this.model.initialize(true);
|
||||
this.model.connectedToSocket();
|
||||
}
|
||||
disconnected() {
|
||||
this.model.updateTitle();
|
||||
}
|
||||
@Method()
|
||||
updates(updates: ClientUpdate[]) {
|
||||
for (const { type, id, update } of updates) {
|
||||
const model = this.model[type] as ModelSubscriber<any>;
|
||||
|
||||
if (model) {
|
||||
model.update(id, update);
|
||||
} else {
|
||||
console.error(`Invalid model type "${type}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
import { clamp } from 'lodash';
|
||||
import {
|
||||
SocialSite, SocialSiteInfo, Eye, Muzzle, Iris, ExpressionExtra, Expression, ServerInfo, ServerFeatureFlags,
|
||||
AccountData, AccountDataFlags
|
||||
} from '../common/interfaces';
|
||||
import {
|
||||
PLAYER_NAME_MAX_LENGTH, SAY_MAX_LENGTH, SAYS_TIME_MIN, SAYS_TIME_MAX, isChatlogRangeUnlimited, SUPPORTER_REWARDS,
|
||||
PAST_SUPPORTER_REWARDS
|
||||
} from '../common/constants';
|
||||
import { matcher, isSurrogate, fromSurrogate, isLowSurrogate } from '../common/stringUtils';
|
||||
import { oauthProviders } from './data';
|
||||
import { Subject } from '../../../node_modules/rxjs';
|
||||
import { PonyTownGame } from './game';
|
||||
import { toScreenX, toScreenY } from '../common/positionUtils';
|
||||
import { hasFlag } from '../common/utils';
|
||||
|
||||
export const matchCyrillic = /[\u0400-\u04FF]/g;
|
||||
export const containsCyrillic = matcher(matchCyrillic);
|
||||
|
||||
const otherValid = [
|
||||
'♂♀⚲⚥⚧☿♁⚨⚩⚦⚢⚣⚤', // gender symbols
|
||||
'™®♥♦♣♠❥♡♢♤♧ღஐ·´°•◦✿❀◆◇◈◉◊。¥€«»,:■□—', // other
|
||||
'〈〉「」『』【】《》♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼№●○◌★☆✰✦✧▪▫・', // other 2
|
||||
'\u1160\u3000\u3164', // spaces (replaced later)
|
||||
].join('').split('').reduce((set, c) => (set.add(c.charCodeAt(0)), set), new Set<number>());
|
||||
|
||||
export function isValid(c: number): boolean {
|
||||
return (c >= 0x0020 && c <= 0x007e) // latin
|
||||
|| (c >= 0x00a0 && c <= 0x00ff) // latin 1 supplement
|
||||
|| (c >= 0x0100 && c <= 0x017F) // Latin Extended-A
|
||||
|| (c >= 0x0180 && c <= 0x024F) // Latin Extended-B
|
||||
|| (c >= 0x1e00 && c <= 0x1eff) // Latin Extended Additional
|
||||
|| (c >= 0x0370 && c <= 0x03FF) // Greek and Coptic
|
||||
|| (c >= 0x0400 && c <= 0x0481) || (c >= 0x048A && c <= 0x04FF) // cyrillic
|
||||
|| (c >= 0x3041 && c <= 0x3096) // hiragana
|
||||
|| (c >= 0x30A0 && c <= 0x30FF) // hatakana
|
||||
|| (c >= 0x3400 && c <= 0x4DB5) || (c >= 0x4E00 && c <= 0x9FCB) || (c >= 0xF900 && c <= 0xFA6A) // kanji
|
||||
|| (c >= 0x2F00 && c <= 0x2FDF) // Kangxi Radicals
|
||||
|| (c >= 0x3000 && c <= 0x302D) // CJK Symbols and Punctuation
|
||||
|| (c >= 0x1D00 && c <= 0x1D7F) // Phonetic Extensions
|
||||
|| (c >= 0x0250 && c <= 0x02AF) // IPA Extensions
|
||||
|| (c >= 0xA720 && c <= 0xA7FF) // Latin Extended-D
|
||||
|| (c >= 0x0E00 && c <= 0x0E7F) // Thai
|
||||
|| (c >= 0xff01 && c <= 0xff5e) // Romaji (replaced later)
|
||||
|| (c >= 0x2200 && c <= 0x22FF) // Mathematical Operators
|
||||
|| (c >= 0x25A0 && c <= 0x25FF) // Geometric Shapes
|
||||
|| (c >= 0x2600 && c <= 0x26ff) || (c >= 0x2700 && c <= 0x27bf) || (c >= 0x2b00 && c <= 0x2bef) // emoji
|
||||
|| (c >= 0x1f600 && c <= 0x1f64f) || (c >= 0x1f680 && c <= 0x1f6f6) || (c >= 0x1f300 && c <= 0x1f5ff) // emoji
|
||||
|| (c >= 0x231a && c <= 0x231b) || (c >= 0x23e9 && c <= 0x23fa) // emoji
|
||||
|| (c >= 0x1f900 && c <= 0x1f9ff) // Supplemental Symbols and Pictographs
|
||||
|| otherValid.has(c) // other symbols
|
||||
;
|
||||
}
|
||||
|
||||
export function isValid2(c: number): boolean {
|
||||
return (c >= 0x2b0 && c <= 0x2ff) // Spacing Modifier Letters
|
||||
|| (c >= 0x531 && c <= 0x556) || (c >= 0x559 && c <= 0x55f) || (c >= 0x561 && c <= 0x587)
|
||||
|| (c >= 0x589 && c <= 0x58a) || (c >= 0x58c && c <= 0x58f) // Armenian
|
||||
|| (c >= 0x591 && c <= 0x5c7) || (c >= 0x5d0 && c <= 0x5ea) || (c >= 0x5f0 && c <= 0x5f4) // Hebrew
|
||||
|| (c >= 0x600 && c <= 0x6ff) // Arabic
|
||||
|| (c >= 0x7c0 && c <= 0x7fa) // NKo
|
||||
|| (c >= 0x900 && c <= 0x97f) // Devanagari
|
||||
|| (c === 0xb90) || (c === 0xb9c) // Tamil
|
||||
|| (c >= 0xc85 && c <= 0xc8c) || (c >= 0xc8e && c <= 0xc90) || (c >= 0xc91 && c <= 0xca8)
|
||||
|| (c >= 0xcaa && c <= 0xcb3) || (c >= 0xcb5 && c <= 0xcb9) || (c >= 0xce6 && c <= 0xcef) // Kannada
|
||||
|| (c >= 0x10a0 && c <= 0x10c5) || (c === 0x10c7) || (c === 0x10cd) || (c >= 0x10d0 && c <= 0x10ff) // Georgian
|
||||
|| (c >= 0x1100 && c <= 0x11ff) || (c >= 0x3130 && c <= 0x318f) || (c >= 0xac00 && c <= 0xd7af) // Hangul
|
||||
|| (c >= 0x1400 && c <= 0x167f) // Unified Canadian Aboriginal Syllabics
|
||||
|| (c >= 0x2010 && c <= 0x2027) || (c >= 0x2030 && c <= 0x205e) // General Punctuation
|
||||
|| (c >= 0x20a0 && c <= 0x20bf) // Currency Symbols
|
||||
|| (c >= 0x2100 && c <= 0x214f) // Letterlike Symbols
|
||||
|| (c >= 0x2150 && c <= 0x218b) // Number Forms
|
||||
|| (c >= 0x2300 && c <= 0x239a) || (c >= 0x23b4 && c <= 0x23fa) // Miscellaneous Technical
|
||||
|| (c >= 0x2500 && c <= 0x257f) // Box Drawing
|
||||
|| (c >= 0x2800 && c <= 0x28ff) // Braille Patterns
|
||||
|| (c >= 0x3000 && c <= 0x303f) // CJK Symbols and Punctuation
|
||||
|| (c >= 0x3105 && c <= 0x312d) // Bopomofo
|
||||
|| (c >= 0xfe30 && c <= 0xfe4f) // CJK Compatibility Forms
|
||||
|| (c >= 0xff01 && c <= 0xffef) // Halfwidth and Fullwidth Forms
|
||||
// || (c >= 0x1f170 && c < 0x1f189) // Enclosed Alphanumeric Supplement [a-z]
|
||||
|| (c >= 0x1f000 && c <= 0x1f02b) // Mahjong Tiles
|
||||
|| (c >= 0x1f0a0 && c <= 0x1f0ae) || (c >= 0x1f0b1 && c <= 0x1f0bf) || (c >= 0x1f0c1 && c <= 0x1f0cf)
|
||||
|| (c >= 0x1f0d1 && c <= 0x1f0df) || (c >= 0x1f0e0 && c <= 0x1f0f5) // Playing Cards
|
||||
|| (c >= 0x1f1e6 && c <= 0x1f1ff) // Enclosed Alphanumeric Supplement (regional indicators)
|
||||
;
|
||||
}
|
||||
|
||||
function isInvalid(c: number): boolean {
|
||||
return c === 0x1f595 // middle finger emoji
|
||||
|| c === 0x00ad // soft hyphen
|
||||
;
|
||||
}
|
||||
|
||||
function isValidForName(c: number): boolean {
|
||||
return isValid(c) && !isInvalid(c);
|
||||
}
|
||||
|
||||
function isValidForMessage(c: number): boolean {
|
||||
return (isValid(c) || isValid2(c)) && !isInvalid(c);
|
||||
}
|
||||
|
||||
export const matchRomaji = /[\uff01-\uff5e]/g;
|
||||
|
||||
const matchOtherWhitespace = /[\u1160\u2800\u3000\u3164\uffa0]+/g;
|
||||
|
||||
export function replaceRomaji(match: string): string {
|
||||
return String.fromCharCode(match.charCodeAt(0) - 0xfee0);
|
||||
}
|
||||
|
||||
export function cleanName(name: string | undefined): string {
|
||||
return filterString(name, isValidForName)
|
||||
.replace(matchOtherWhitespace, ' ') // whitespace characters
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(matchRomaji, replaceRomaji)
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function cleanMessage(text: string | undefined): string {
|
||||
return filterString(text, isValidForMessage)
|
||||
.replace(matchOtherWhitespace, ' ') // whitespace characters
|
||||
.replace(/[\r\n]/g, '')
|
||||
.replace(matchRomaji, replaceRomaji)
|
||||
.trim()
|
||||
.substr(0, SAY_MAX_LENGTH);
|
||||
}
|
||||
|
||||
export function filterString(value: string | undefined, filter: (code: number) => boolean): string {
|
||||
value = value || '';
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
let code = value.charCodeAt(i);
|
||||
let size = 1;
|
||||
let invalidSurrogate = false;
|
||||
|
||||
if (isSurrogate(code) && (i + 1) < value.length) {
|
||||
const extra = value.charCodeAt(i + 1);
|
||||
|
||||
if (isLowSurrogate(extra)) {
|
||||
code = fromSurrogate(code, extra);
|
||||
i++;
|
||||
size++;
|
||||
} else {
|
||||
invalidSurrogate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidSurrogate || !filter(code)) {
|
||||
i -= size;
|
||||
value = value.substr(0, i + 1) + value.substr(i + size + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function validatePonyName(name: string | undefined): boolean {
|
||||
return !!name && !!name.length && name.length <= PLAYER_NAME_MAX_LENGTH && !/^[.,_-]+$/.test(name);
|
||||
}
|
||||
|
||||
export function toSocialSiteInfo({ id, name, url, provider }: SocialSite): SocialSiteInfo {
|
||||
const oauth = oauthProviders.find(p => p.id === provider);
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
url,
|
||||
icon: oauth && oauth.id,
|
||||
color: oauth && oauth.color,
|
||||
};
|
||||
}
|
||||
|
||||
function isMultipleMatch(message: string, last: string): boolean {
|
||||
const minMessageLength = 4;
|
||||
|
||||
if (message.length >= minMessageLength && last.length >= minMessageLength) {
|
||||
let current = last;
|
||||
|
||||
while (current.length < message.length) {
|
||||
current += last;
|
||||
}
|
||||
|
||||
return message === current.substr(0, SAY_MAX_LENGTH);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function checkTrailing(message: string, last: string) {
|
||||
return message.indexOf(last) === 0 && (message.length - last.length) < 3;
|
||||
}
|
||||
|
||||
function isTrailingMatch(message: string, last: string) {
|
||||
const minMessageLength = 5;
|
||||
|
||||
if (message.length > last.length && last.length > minMessageLength) {
|
||||
return checkTrailing(message, last);
|
||||
} else if (message.length < last.length && message.length > minMessageLength) {
|
||||
return checkTrailing(last, message);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isSpamMessage(message: string, lastMessages: string[]): boolean {
|
||||
if (!/^\//.test(message) && lastMessages.length) {
|
||||
return lastMessages.some(last => message === last || isMultipleMatch(message, last) || isTrailingMatch(message, last));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSaysTime(message: string): number {
|
||||
return SAYS_TIME_MIN + clamp(message.length / SAY_MAX_LENGTH, 0, 1) * (SAYS_TIME_MAX - SAYS_TIME_MIN);
|
||||
}
|
||||
|
||||
export function createExpression(
|
||||
right: Eye, left: Eye, muzzle: Muzzle, rightIris = Iris.Forward, leftIris = Iris.Forward, extra = ExpressionExtra.None
|
||||
): Expression {
|
||||
return { right, left, muzzle, rightIris, leftIris, extra };
|
||||
}
|
||||
|
||||
export const isAndroidBrowser = (() => {
|
||||
const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent;
|
||||
|
||||
// Android browser
|
||||
// Mozilla/5.0 (Linux; U; Android 4.4.2; es-ar; LG-D375AR Build/KOT49I)
|
||||
// AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36
|
||||
if (/Android /.test(ua) && /AppleWebKit/.test(ua) && (!/chrome/i.test(ua) || /Chrome\/30\./.test(ua))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const isBrowserOutdated = (() => {
|
||||
const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent;
|
||||
|
||||
// Safari <= 8
|
||||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)
|
||||
// AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25
|
||||
const safari = /Version\/(\d+)\.[0-9.]+ Safari/.exec(ua);
|
||||
|
||||
if (safari && parseInt(safari[1], 10) <= 8) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Android browser
|
||||
// Mozilla/5.0 (Linux; U; Android 4.4.2; es-ar; LG-D375AR Build/KOT49I)
|
||||
// AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36
|
||||
if (isAndroidBrowser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!supportsLetAndConst()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
|
||||
export function getLocale() {
|
||||
return (navigator.languages ? navigator.languages[0] : navigator.language) || 'en-US';
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function isLanguage(lang: string) {
|
||||
const languages = navigator.languages || [navigator.language];
|
||||
return languages.some(l => l === lang);
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function sortServersForRussian(a: ServerInfo, b: ServerInfo) {
|
||||
if (a.flag === 'ru' && a.flag !== b.flag) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.flag === 'ru' && a.flag !== b.flag) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.id.localeCompare(b.id);
|
||||
}
|
||||
|
||||
export function readFileAsText(file: File) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e: any) => resolve(e.target && e.target.result || '');
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function isFileSaverSupported() {
|
||||
try {
|
||||
return !!new Blob;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export let isInIncognitoMode = false;
|
||||
|
||||
export function setIsIncognitoMode(value: boolean) {
|
||||
isInIncognitoMode = value;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
function checkIncognitoMode(wnd: any) {
|
||||
if (!wnd || !wnd.chrome)
|
||||
return;
|
||||
|
||||
const fs = wnd.RequestFileSystem || wnd.webkitRequestFileSystem;
|
||||
|
||||
if (!fs)
|
||||
return;
|
||||
|
||||
fs(wnd.TEMPORARY, 100, () => { }, () => isInIncognitoMode = true);
|
||||
}
|
||||
|
||||
let focused = true;
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function isFocused() {
|
||||
return focused;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (typeof window !== 'undefined') {
|
||||
checkIncognitoMode(window);
|
||||
window.addEventListener('focus', () => focused = true);
|
||||
window.addEventListener('blur', () => focused = false);
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function isStandalone() {
|
||||
return !!window.matchMedia('(display-mode: standalone)').matches ||
|
||||
(window.navigator as any).standalone === true; // safari
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function supportsLetAndConst() {
|
||||
try {
|
||||
return (new Function('let x = true; return x;'))();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function registerServiceWorker(url: string, onUpdate: () => void) {
|
||||
try {
|
||||
if ('serviceWorker' in navigator && typeof navigator.serviceWorker.register === 'function') {
|
||||
let hadWorker = false;
|
||||
|
||||
navigator.serviceWorker.register(url)
|
||||
.then(worker => {
|
||||
hadWorker = !!worker.active;
|
||||
|
||||
worker.addEventListener('updatefound', () => {
|
||||
if (hadWorker) {
|
||||
onUpdate();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (hadWorker) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function unregisterServiceWorker() {
|
||||
if ('serviceWorker' in navigator && typeof navigator.serviceWorker.getRegistrations === 'function') {
|
||||
return navigator.serviceWorker.getRegistrations()
|
||||
.then(registrations => {
|
||||
for (const registration of registrations) {
|
||||
registration.unregister();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function attachDebugMethod(name: string, method: any) {
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any)[name] = method;
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function updateRangeIndicator(range: number | undefined, { player, scale, camera }: PonyTownGame) {
|
||||
const e = document.getElementById('range-indicator')!;
|
||||
|
||||
if (player && !isChatlogRangeUnlimited(range)) {
|
||||
const x = (toScreenX(player.x) - camera.x) * scale;
|
||||
const y = (toScreenY(player.y) - camera.actualY) * scale;
|
||||
const w = toScreenX(range!) * scale * 2;
|
||||
const h = toScreenY(range!) * scale * 2;
|
||||
e.style.width = `${w}px`;
|
||||
e.style.height = `${h}px`;
|
||||
e.style.left = `${-w / 2}px`;
|
||||
e.style.top = `${-h / 2}px`;
|
||||
e.style.transform = `translate3d(${x}px, ${y}px, 0)`;
|
||||
e.style.display = 'block';
|
||||
} else {
|
||||
e.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function checkIframeKey(iframeId: string, expectedKey: string) {
|
||||
try {
|
||||
const iframe = document.getElementById(iframeId) as HTMLIFrameElement;
|
||||
const doc = iframe && iframe.contentWindow && iframe.contentWindow.document;
|
||||
const key = doc && doc.body && doc.body.getAttribute('data-key');
|
||||
return key === expectedKey;
|
||||
} catch (e) {
|
||||
if (DEVELOPMENT) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let flags: ServerFeatureFlags = {};
|
||||
|
||||
export const featureFlagsChanged = new Subject<ServerFeatureFlags>();
|
||||
|
||||
export function initFeatureFlags(newFlags: ServerFeatureFlags) {
|
||||
flags = newFlags;
|
||||
featureFlagsChanged.next(newFlags);
|
||||
}
|
||||
|
||||
export function hasFeatureFlag(flag: keyof ServerFeatureFlags) {
|
||||
return !!flags[flag];
|
||||
}
|
||||
|
||||
export function hardReload() {
|
||||
unregisterServiceWorker()
|
||||
.then(() => location.reload(true));
|
||||
}
|
||||
|
||||
const LOGGING = false;
|
||||
|
||||
let logger = (_: string) => { };
|
||||
|
||||
export function initLogger(newLogger: (message: string) => void) {
|
||||
if (LOGGING) {
|
||||
logger = newLogger;
|
||||
}
|
||||
}
|
||||
|
||||
export function log(message: string) {
|
||||
if (LOGGING) {
|
||||
logger(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function isSupporterOrPastSupporter(account: AccountData | undefined) {
|
||||
return !!account && (!!account.supporter || hasFlag(account.flags, AccountDataFlags.PastSupporter));
|
||||
}
|
||||
|
||||
export function supporterTitle(account: AccountData | undefined) {
|
||||
if (account && account.supporter) {
|
||||
return `Supporter Tier ${account.supporter}`;
|
||||
} else if (account && hasFlag(account.flags, AccountDataFlags.PastSupporter)) {
|
||||
return 'Past supporter';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function supporterClass(account: AccountData | undefined) {
|
||||
if (account && account.supporter) {
|
||||
return `supporter-${account.supporter}`;
|
||||
} else if (account && hasFlag(account.flags, AccountDataFlags.PastSupporter)) {
|
||||
return 'supporter-past';
|
||||
} else {
|
||||
return 'd-none';
|
||||
}
|
||||
}
|
||||
|
||||
export function supporterRewards(account: AccountData | undefined) {
|
||||
if (account && account.supporter) {
|
||||
return SUPPORTER_REWARDS[account.supporter];
|
||||
} else if (account && hasFlag(account.flags, AccountDataFlags.PastSupporter)) {
|
||||
return PAST_SUPPORTER_REWARDS;
|
||||
} else {
|
||||
return SUPPORTER_REWARDS[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
export interface Credit {
|
||||
name: string;
|
||||
title: string;
|
||||
avatarIndex: number;
|
||||
links: string[];
|
||||
}
|
||||
|
||||
export interface Contributor {
|
||||
name: string;
|
||||
links?: string[];
|
||||
}
|
||||
|
||||
export interface Contributors {
|
||||
group: string;
|
||||
contributors: Contributor[];
|
||||
}
|
||||
|
||||
export const CREDITS: Credit[] = [
|
||||
// example:
|
||||
// {
|
||||
// name: 'Your name',
|
||||
// title: 'Your role on the team',
|
||||
// avatarIndex: 0, // place of the avatar in /assets/images/avatars.jpg
|
||||
// links: ['https://twitter.com/your_twitter_handle'],
|
||||
// },
|
||||
];
|
||||
|
||||
export const CONTRIBUTORS: Contributors[] = [
|
||||
{
|
||||
group: 'Artists & Animators',
|
||||
contributors: [
|
||||
{ name: 'Shino', links: ['https://www.deviantart.com/shinodage'] },
|
||||
{ name: 'ChiraChan', links: ['https://www.deviantart.com/chiramii-chan', 'https://chirachan-art.tumblr.com/'] },
|
||||
{ name: 'Goodly', links: ['https://www.deviantart.com/goodlyay'] },
|
||||
{ name: 'TioRafaJP', links: ['https://www.deviantart.com/tiorafajp', 'https://www.youtube.com/user/RafaelJP2'] },
|
||||
{ name: 'ShareMyShipment', links: ['https://www.deviantart.com/sharemyshipment'] },
|
||||
{ name: 'Velenor', links: ['https://www.deviantart.com/velenor'] },
|
||||
{ name: 'OtakuAP', links: ['https://www.deviantart.com/otakuap'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Programmers & Artists',
|
||||
contributors: [
|
||||
{ name: 'CyberPon3', links: ['https://www.deviantart.com/cyberpon3'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Artists',
|
||||
contributors: [
|
||||
{ name: 'Disastral' },
|
||||
{ name: 'Meno', links: ['https://www.deviantart.com/menojar'] },
|
||||
{ name: 'Paulpeoples', links: ['https://www.deviantart.com/paulpeopless'] },
|
||||
{ name: 'Velvet-Frost', links: ['https://www.deviantart.com/velvet-frost'] },
|
||||
{ name: 'Jet7Wave', links: ['https://www.deviantart.com/jetwave'] },
|
||||
{ name: 'Lalieri', links: ['https://lalieri.tumblr.com/'] },
|
||||
{ name: 'Ruef-bae', links: ['https://www.deviantart.com/ruef-bae'] },
|
||||
{ name: 'Alchemist3rd' },
|
||||
{ name: 'Firecracker' },
|
||||
{ name: 'ZippySqrl', links: ['https://www.deviantart.com/zippysqrl'] },
|
||||
{ name: 'Karnel333' },
|
||||
{ name: 'Wellfugzee' },
|
||||
{ name: 'ScribblesHeart', links: ['https://www.deviantart.com/scribblesdesu'] },
|
||||
{ name: 'dsp2003', links: ['https://dsp2003.tumblr.com/', 'http://www.deviantart.com/dsp2003'] },
|
||||
{ name: 'MysticBlare', links: ['https://twitter.com/MysticBlare'] },
|
||||
{ name: 'Towmacow Waffles', links: ['https://www.deviantart.com/towmacowwaffles'] },
|
||||
{ name: 'OrchidPony', links: ['https://www.deviantart.com/orchidpony'] },
|
||||
{ name: 'Cherry Cerise', links: ['https://www.deviantart.com/cherryceriseart'] },
|
||||
{ name: 'Radio' },
|
||||
{ name: 'Ultimate Fluff' },
|
||||
{ name: 'SC', links: ['https://0somecunt0.tumblr.com/tagged/sfw'] },
|
||||
{ name: 'SailorDolpin', links: ['https://vk.com/id324582699'] },
|
||||
{ name: 'Deeraw', links: ['https://www.deviantart.com/deerdraw', 'https://twitter.com/TheOnlyDeeraw'] },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,73 @@
|
||||
import { toByteArray } from 'base64-js';
|
||||
import { ClientOptions, createBinaryReader, readObject } from 'ag-sockets/dist/browser';
|
||||
import { OAuthProvider } from '../common/interfaces';
|
||||
|
||||
/* istanbul ignore next */
|
||||
function attr(name: string): string | undefined {
|
||||
return typeof document !== 'undefined' ? (document.body.getAttribute(name) || undefined) : undefined;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
function data(id: string): string | undefined {
|
||||
const element = typeof document !== 'undefined' ? document.getElementById(id) : undefined;
|
||||
return element ? element.innerHTML : undefined;
|
||||
}
|
||||
|
||||
function json<T>(id: string, def: string): T {
|
||||
return JSON.parse(data(id) || def);
|
||||
}
|
||||
|
||||
export let isMobile = false;
|
||||
export const sw = attr('data-sw') === 'true';
|
||||
export const host = attr('data-host')!;
|
||||
export const local = attr('data-local') === 'true';
|
||||
export const token = attr('data-token');
|
||||
export const version = attr('data-version');
|
||||
export const isPublic = attr('data-public') === 'true';
|
||||
export const supporterLink = attr('data-supporter-link');
|
||||
export const twitterLink = attr('data-twitter-link');
|
||||
export const contactEmail = attr('data-email');
|
||||
export const copyrightName = attr('data-copyright');
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const oauthProviders = json<OAuthProvider[]>('oauth-providers', '[]')
|
||||
.map(a => <OAuthProvider>{ ...a, url: `/auth/${a.id}` });
|
||||
/* istanbul ignore next */
|
||||
export const signUpProviders = oauthProviders.filter(i => !i.connectOnly);
|
||||
/* istanbul ignore next */
|
||||
export const signInProviders = oauthProviders.filter(i => i.connectOnly);
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function socketOptions(): ClientOptions {
|
||||
const options = data('socket-options');
|
||||
|
||||
if (options) {
|
||||
const buffer = toByteArray(options);
|
||||
const reader = createBinaryReader(buffer);
|
||||
return readObject(reader);
|
||||
} else {
|
||||
throw new Error('Missing socket options');
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
function setMobile() {
|
||||
isMobile = true;
|
||||
window.removeEventListener('touchstart', setMobile);
|
||||
document.body.classList.add('is-mobile');
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (typeof window !== 'undefined') {
|
||||
if (!/windows/i.test(navigator.userAgent)) {
|
||||
window.addEventListener('touchstart', setMobile);
|
||||
}
|
||||
|
||||
if (/Trident/.test(navigator.userAgent)) {
|
||||
document.body.classList.add('is-msie');
|
||||
}
|
||||
|
||||
if (/YaBrowser/.test(navigator.userAgent)) {
|
||||
document.body.classList.add('is-yandex');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
import {
|
||||
Entity, DrawOptions, Camera, PaletteSpriteBatch, SpriteBatch, TileSets, Engine, Pony, WorldMap, EntityState
|
||||
} from '../common/interfaces';
|
||||
import { isBoundsVisible } from '../common/camera';
|
||||
import { drawBounds, drawPixelText, drawBoundsOutline, drawOutlineRect, drawWorldBounds } from '../graphics/graphicsUtils';
|
||||
import { ORANGE, BLUE, PURPLE, BLACK, RED, WHITE, CYAN, HOTPINK, GRAY } from '../common/colors';
|
||||
import { toScreenX, toScreenY, toWorldX, toWorldY } from '../common/positionUtils';
|
||||
import { tileWidth, tileHeight, PONY_TYPE, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from '../common/constants';
|
||||
import { forEachRegion, getAnyBounds, getRegion, isInWaterAt } from '../common/worldMap';
|
||||
import { drawPonyEntity, drawPonyEntityLight, drawPonyEntityLightSprite } from '../common/pony';
|
||||
import { getInteractBounds, sortEntities, isHidden, getSitOnBounds } from '../common/entityUtils';
|
||||
import { drawTiles, drawTilesNew, drawTilesDebugInfo } from './tileUtils';
|
||||
import { withAlphaFloat } from '../common/color';
|
||||
import { timeStart, timeEnd } from './timing';
|
||||
|
||||
const SELECTED_ENTITY_BOUNDS = withAlphaFloat(ORANGE, 0.5);
|
||||
|
||||
function drawEntities(batch: PaletteSpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) {
|
||||
const drawHidden = options.drawHidden;
|
||||
let entitiesDrawn = 0;
|
||||
|
||||
for (const entity of entities) {
|
||||
if ((!isHidden(entity) || drawHidden) && isBoundsVisible(camera, entity.bounds, entity.x, entity.y)) {
|
||||
if (entity.type === PONY_TYPE) {
|
||||
drawPonyEntity(batch, entity as Pony, options);
|
||||
entitiesDrawn++;
|
||||
} else if (entity.draw !== undefined) {
|
||||
entity.draw(batch, options);
|
||||
entitiesDrawn++;
|
||||
}
|
||||
} else {
|
||||
if (entity.type === PONY_TYPE) {
|
||||
const pony = entity as Pony;
|
||||
|
||||
if (pony.batch !== undefined) {
|
||||
batch.releaseBatch(pony.batch);
|
||||
pony.batch = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entitiesDrawn;
|
||||
}
|
||||
|
||||
export function drawEntityLights(batch: SpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) {
|
||||
const drawHidden = options.drawHidden;
|
||||
|
||||
for (const entity of entities) {
|
||||
if (DEVELOPMENT && (entity.type !== PONY_TYPE && !entity.drawLight)) {
|
||||
console.error('Cannot draw entity light', entity);
|
||||
}
|
||||
|
||||
if ((!isHidden(entity) || drawHidden) && isBoundsVisible(camera, entity.lightBounds, entity.x, entity.y)) {
|
||||
if (entity.type === PONY_TYPE) {
|
||||
drawPonyEntityLight(batch, entity as Pony, options);
|
||||
} else {
|
||||
entity.drawLight!(batch, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function drawEntityLightSprites(batch: SpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) {
|
||||
const drawHidden = options.drawHidden;
|
||||
|
||||
for (const entity of entities) {
|
||||
if (DEVELOPMENT && (entity.type !== PONY_TYPE && !entity.drawLightSprite)) {
|
||||
console.error('Cannot draw entity light sprite', entity);
|
||||
}
|
||||
|
||||
if ((!isHidden(entity) || drawHidden) && isBoundsVisible(camera, entity.lightSpriteBounds, entity.x, entity.y)) {
|
||||
if (entity.type === PONY_TYPE) {
|
||||
drawPonyEntityLightSprite(batch, entity as Pony, options);
|
||||
} else {
|
||||
entity.drawLightSprite!(batch, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function hasDrawLight(entity: Entity) {
|
||||
if (entity.type === PONY_TYPE) {
|
||||
const pony = entity as Pony;
|
||||
return (pony.ponyState.holding !== undefined && pony.ponyState.holding.drawLight !== undefined) ||
|
||||
((pony.state & EntityState.Magic) !== 0);
|
||||
} else {
|
||||
return entity.drawLight !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasLightSprite(entity: Entity) {
|
||||
if (entity.type === PONY_TYPE) {
|
||||
const pony = entity as Pony;
|
||||
return (pony.ponyState.holding !== undefined && pony.ponyState.holding.drawLightSprite !== undefined);
|
||||
} else {
|
||||
return entity.drawLightSprite !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function drawMap(
|
||||
batch: PaletteSpriteBatch, map: WorldMap, camera: Camera, player: Pony, options: DrawOptions,
|
||||
tileSets: TileSets, selectedEntities: Entity[],
|
||||
) {
|
||||
TIMING && timeStart('forEachRegion');
|
||||
if (BETA && options.engine === Engine.Whiteness) {
|
||||
batch.drawRect(WHITE, 0, 0, toScreenX(map.width), toScreenY(map.height));
|
||||
} else if (BETA && options.engine === Engine.LayeredTiles) {
|
||||
forEachRegion(map, region => drawTilesNew(batch, region, camera, map, tileSets, options));
|
||||
} else {
|
||||
forEachRegion(map, region => drawTiles(batch, region, camera, map, tileSets, options));
|
||||
}
|
||||
TIMING && timeEnd();
|
||||
|
||||
TIMING && timeStart('sortEntities');
|
||||
sortEntities(map.entitiesDrawable);
|
||||
TIMING && timeEnd();
|
||||
|
||||
TIMING && timeStart('drawEntities');
|
||||
const entitiesDrawn = drawEntities(batch, map.entitiesDrawable, camera, options);
|
||||
TIMING && timeEnd();
|
||||
|
||||
if (BETA || TOOLS) {
|
||||
forEachRegion(map, region => drawTilesDebugInfo(batch, region, camera, options));
|
||||
}
|
||||
|
||||
if (BETA && options.debug.showHelpers) {
|
||||
drawDebugHelpers(batch, map.entities, options);
|
||||
}
|
||||
|
||||
if (BETA) {
|
||||
for (const entity of selectedEntities) {
|
||||
const bounds = getAnyBounds(entity);
|
||||
drawBoundsOutline(batch, entity, bounds, SELECTED_ENTITY_BOUNDS, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (BETA && options.debug.showHelpers) {
|
||||
drawOutlineRect(batch, PURPLE, getInteractBounds(player));
|
||||
drawOutlineRect(batch, 0xff000066, getSitOnBounds(player));
|
||||
}
|
||||
|
||||
if (BETA && options.showColliderMap) {
|
||||
drawDebugCollider(batch, map, camera);
|
||||
batch.drawRect(PURPLE, toScreenX(player.x) - 1, toScreenY(player.y), 3, 1);
|
||||
batch.drawRect(PURPLE, toScreenX(player.x), toScreenY(player.y) - 1, 1, 3);
|
||||
}
|
||||
|
||||
if (BETA && options.showHeightmap) {
|
||||
drawDebugInWater(batch, map, camera);
|
||||
}
|
||||
|
||||
return entitiesDrawn;
|
||||
}
|
||||
|
||||
// debug
|
||||
|
||||
function drawDebugHelpers(batch: PaletteSpriteBatch, entities: Entity[], options: DrawOptions) {
|
||||
const textColor = 0x000000b2;
|
||||
const show = options.debug;
|
||||
|
||||
for (const e of entities) {
|
||||
batch.globalAlpha = 0.3;
|
||||
show.bounds && drawBounds(batch, e, e.bounds, ORANGE);
|
||||
show.cover && drawBounds(batch, e, e.coverBounds, BLUE);
|
||||
show.interact && drawBounds(batch, e, e.interactBounds, PURPLE);
|
||||
show.trigger && drawWorldBounds(batch, e, e.triggerBounds, CYAN);
|
||||
|
||||
if (show.collider) {
|
||||
batch.globalAlpha = 0.5;
|
||||
|
||||
const x = Math.floor(e.x * tileWidth);
|
||||
const y = Math.floor(e.y * tileHeight);
|
||||
|
||||
if (e.colliders !== undefined) {
|
||||
for (const collider of e.colliders) {
|
||||
const x1 = x + collider.x;
|
||||
const x2 = x + collider.x + collider.w;
|
||||
const y1 = y + collider.y;
|
||||
const y2 = y + collider.y + collider.h;
|
||||
batch.drawRect(collider.tall ? RED : HOTPINK, x1, y1, x2 - x1, y2 - y1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
batch.globalAlpha = 1;
|
||||
batch.drawRect(BLACK, toScreenX(e.x), toScreenY(e.y), 1, 1); // anchor
|
||||
|
||||
if (show.id) {
|
||||
drawPixelText(batch, toScreenX(e.x) + 2, toScreenY(e.y) + 2, textColor, e.id.toFixed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawDebugInWater(batch: PaletteSpriteBatch, map: WorldMap, camera: Camera) {
|
||||
const color = withAlphaFloat(ORANGE, 0.4);
|
||||
|
||||
forEachRegion(map, region => {
|
||||
const sx = toScreenX(region.x * REGION_SIZE);
|
||||
const sy = toScreenY(region.y * REGION_SIZE);
|
||||
const w = REGION_WIDTH;
|
||||
const h = REGION_HEIGHT;
|
||||
|
||||
const cameraLeft = camera.x;
|
||||
const cameraRight = camera.x + camera.w;
|
||||
const cameraTop = camera.actualY;
|
||||
const cameraBottom = camera.actualY + camera.h;
|
||||
|
||||
if (sx > cameraRight || sy > cameraBottom || (sx + w) < cameraLeft || (sy + h) < cameraTop)
|
||||
return;
|
||||
|
||||
for (let y = 0; y < h; y++) {
|
||||
if ((sy + y + 1) < cameraTop || (sy + y) > cameraBottom)
|
||||
continue;
|
||||
|
||||
for (let x = 0; x < w; x++) {
|
||||
if ((sx + x + 1) < cameraLeft || (sx + x) > cameraRight)
|
||||
continue;
|
||||
|
||||
const tx = x;
|
||||
|
||||
while (isInWaterAt(map, toWorldX(sx + x + 0.5), toWorldY(sy + y + 0.5)) && x < w) {
|
||||
x++;
|
||||
}
|
||||
|
||||
if (x > tx) {
|
||||
batch.drawRect(color, sx + tx, sy + y, x - tx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawDebugCollider(batch: PaletteSpriteBatch, map: WorldMap, camera: Camera) {
|
||||
const color = withAlphaFloat(PURPLE, 0.4);
|
||||
|
||||
forEachRegion(map, ({ x, y, collider }) => {
|
||||
const sx = toScreenX(x * REGION_SIZE);
|
||||
const sy = toScreenY(y * REGION_SIZE);
|
||||
const w = REGION_WIDTH;
|
||||
const h = REGION_HEIGHT;
|
||||
|
||||
const cameraLeft = camera.x;
|
||||
const cameraRight = camera.x + camera.w;
|
||||
const cameraTop = camera.actualY;
|
||||
const cameraBottom = camera.actualY + camera.h;
|
||||
|
||||
if (sx > cameraRight || sy > cameraBottom || (sx + w) < cameraLeft || (sy + h) < cameraTop)
|
||||
return;
|
||||
|
||||
for (let y = 0; y < h; y++) {
|
||||
if ((sy + y + 1) < cameraTop || (sy + y) > cameraBottom)
|
||||
continue;
|
||||
|
||||
for (let x = 0; x < w; x++) {
|
||||
if ((sx + x + 1) < cameraLeft || (sx + x) > cameraRight)
|
||||
continue;
|
||||
|
||||
const tx = x;
|
||||
|
||||
while (collider[x + y * w] !== 0 && x < w) {
|
||||
x++;
|
||||
}
|
||||
|
||||
if (x > tx) {
|
||||
batch.drawRect(color, sx + tx, sy + y, x - tx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function drawDebugRegions(batch: SpriteBatch, map: WorldMap, player: Pony, { w, h }: Camera) {
|
||||
const rw = 10;
|
||||
const rh = 8;
|
||||
const width = rw * map.regionsX;
|
||||
const height = rh * map.regionsY;
|
||||
const x = w - width - 10;
|
||||
const y = h - height - 30;
|
||||
|
||||
for (let i = 0; i < map.regionsY; i++) {
|
||||
for (let j = 0; j < map.regionsX; j++) {
|
||||
if (getRegion(map, j, i)) {
|
||||
const inside = j === Math.floor(player.x / REGION_SIZE) && i === Math.floor(player.y / REGION_SIZE);
|
||||
batch.drawRect(inside ? ORANGE : RED, x + rw * j, y + rh * i, rw, rh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i <= map.regionsY; i++) {
|
||||
batch.drawRect(GRAY, x, y + rh * i, width + 1, 1);
|
||||
}
|
||||
|
||||
for (let i = 0; i <= map.regionsX; i++) {
|
||||
batch.drawRect(GRAY, x + rw * i, y, 1, height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { escape } from 'lodash';
|
||||
import { Sprite } from '../common/interfaces';
|
||||
import { canvasToSource } from './canvasUtils';
|
||||
import { drawCanvas } from '../graphics/contextSpriteBatch';
|
||||
import { WHITE } from '../common/colors';
|
||||
import { normalSpriteSheet } from '../generated/sprites';
|
||||
import { includes } from '../common/utils';
|
||||
|
||||
export interface Emoji {
|
||||
names: string[];
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export const emojis: Emoji[] = [
|
||||
// faces
|
||||
['🙂', 'face', 'tiny', 'tinyface', 'slight_smile'],
|
||||
['😵', 'derp', 'dizzy_face'],
|
||||
['😠', 'angry'],
|
||||
['😐', 'neutral', 'neutral_face'],
|
||||
['😑', 'expressionless'],
|
||||
['😆', 'laughing'],
|
||||
['😍', 'heart_eyes'],
|
||||
['😟', 'worried'],
|
||||
['🤔', 'thinking'],
|
||||
['🙃', 'upside_down'],
|
||||
['😈', 'evil', 'smiling_imp'],
|
||||
['👿', 'imp', 'angry_evil'],
|
||||
['👃', 'nose', 'c'],
|
||||
|
||||
// cat faces
|
||||
['🐱', 'cat'],
|
||||
['😺', 'smiley_cat'],
|
||||
['😸', 'smile_cat'],
|
||||
['😹', 'joy_cat'],
|
||||
['😻', 'heart_eyes_cat'],
|
||||
['😼', 'smirk_cat'],
|
||||
['😽', 'kissing_cat'],
|
||||
['🙀', 'scream_cat'],
|
||||
['😿', 'cryingcat', 'crying_cat_face'],
|
||||
['😾', 'pouting_cat'],
|
||||
|
||||
// hearts
|
||||
['❤', 'heart'],
|
||||
['💙', 'blue_heart', 'meno'],
|
||||
['💚', 'green_heart', 'chira'],
|
||||
['💛', 'yellow_heart'],
|
||||
['💜', 'purple_heart'],
|
||||
['🖤', 'black_heart', 'shino'],
|
||||
['💔', 'broken_heart'],
|
||||
['💖', 'sparkling_heart'],
|
||||
['💗', 'heartpulse'],
|
||||
['💕', 'two_hearts'],
|
||||
|
||||
// food / objects
|
||||
['🥌', 'rock', 'stone'],
|
||||
['🍕', 'pizza'],
|
||||
['🍎', 'apple'],
|
||||
['🍏', 'gapple', 'green_apple'],
|
||||
['🍊', 'orange', 'tangerine'],
|
||||
['🍐', 'pear'],
|
||||
['🥭', 'mango'],
|
||||
['🥕', 'carrot'],
|
||||
['🍇', 'grapes'],
|
||||
['🍌', 'banana'],
|
||||
['⛏', 'pick'],
|
||||
['🥚', 'egg'],
|
||||
['💮', 'flower', 'white_flower'],
|
||||
['🌸', 'cherry_blossom'],
|
||||
['🍬', 'candy'],
|
||||
['🍡', 'candy_cane'],
|
||||
['🍭', 'lollipop'],
|
||||
['⭐', 'star'],
|
||||
['🌟', 'star2'],
|
||||
['🌠', 'shooting_star'],
|
||||
['⚡', 'zap'],
|
||||
['❄', 'snow', 'snowflake'],
|
||||
['⛄', 'snowpony', 'snowman'],
|
||||
['🏀', 'pumpkin'],
|
||||
['🎃', 'jacko', 'jack_o_lantern'],
|
||||
['🌲', 'evergreen_tree', 'pinetree'],
|
||||
['🎄', 'christmas_tree'],
|
||||
['🕯', 'candle'],
|
||||
['🎅', 'santa_hat', 'santa_claus'],
|
||||
['💐', 'holly'],
|
||||
['🌿', 'mistletoe'],
|
||||
['🎲', 'die', 'dice', 'game_die'],
|
||||
['✨', 'sparkles'],
|
||||
['🎁', 'gift', 'present'],
|
||||
['🔥', 'fire'],
|
||||
['🎵', 'musical_note'],
|
||||
['🎶', 'notes'],
|
||||
['🌈', 'rainbow'],
|
||||
['🐾', 'feet', 'paw', 'paws'],
|
||||
['👑', 'crown'],
|
||||
['💎', 'gem'],
|
||||
['☘', 'shamrock', 'clover'],
|
||||
['🍀', 'four_leaf_clover'],
|
||||
['🍪', 'cookie'],
|
||||
|
||||
// animals
|
||||
['🦋', 'butterfly'],
|
||||
['🦇', 'bat'],
|
||||
['🕷', 'spider'],
|
||||
['👻', 'ghost'],
|
||||
['🐈', 'cat2'],
|
||||
|
||||
// other
|
||||
['™', 'tm'],
|
||||
['♂', 'male'],
|
||||
['♀', 'female'],
|
||||
['⚧', 'trans', 'transgender'],
|
||||
].map(createEmoji);
|
||||
|
||||
export const emojiMap = new Map<string, string>();
|
||||
export const emojiNames = emojis.slice().sort().map(e => `:${e.names[0]}:`);
|
||||
emojis.forEach(e => e.names.forEach(name => emojiMap.set(`:${name}:`, e.symbol)));
|
||||
|
||||
export function findEmoji(name: string): Emoji | undefined {
|
||||
return emojis.find(e => name === e.symbol || includes(e.names, name));
|
||||
}
|
||||
|
||||
export function replaceEmojis(text: string | undefined): string {
|
||||
return (text || '').replace(/:[a-z0-9_]+:/ig, match => emojiMap.get(match) || match);
|
||||
}
|
||||
|
||||
function createEmoji([symbol, ...names]: string[]): Emoji {
|
||||
return { symbol, names: [...names, ...names.filter(n => /_/.test(n)).map(n => n.replace(/_/g, ''))] };
|
||||
}
|
||||
|
||||
const emojiImages = new Map<Sprite, string>();
|
||||
const emojiImagePromises = new Map<Sprite, Promise<string>>();
|
||||
|
||||
export function getEmojiImageAsync(sprite: Sprite, callback: (str: string) => void) {
|
||||
const src = emojiImages.get(sprite);
|
||||
|
||||
if (src) {
|
||||
callback(src);
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = emojiImagePromises.get(sprite);
|
||||
|
||||
if (promise) {
|
||||
promise.then(callback);
|
||||
return;
|
||||
}
|
||||
|
||||
const width = sprite.w + sprite.ox;
|
||||
// const height = sprite.h + sprite.oy;
|
||||
const canvas = drawCanvas(width, 10, normalSpriteSheet, undefined, batch => batch.drawSprite(sprite, WHITE, 0, 0));
|
||||
const newPromise = canvasToSource(canvas);
|
||||
emojiImagePromises.set(sprite, newPromise);
|
||||
|
||||
newPromise
|
||||
.then(src => {
|
||||
emojiImages.set(sprite, src);
|
||||
emojiImagePromises.delete(sprite);
|
||||
return src;
|
||||
})
|
||||
.then(callback);
|
||||
}
|
||||
|
||||
const emojisRegex = new RegExp(`(${[
|
||||
...emojis.map(e => e.symbol),
|
||||
'♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎',
|
||||
].join('|')})`, 'g');
|
||||
|
||||
export function splitEmojis(text: string) {
|
||||
return text.split(emojisRegex);
|
||||
}
|
||||
|
||||
export function hasEmojis(text: string) {
|
||||
return emojisRegex.test(text);
|
||||
}
|
||||
|
||||
export function nameToHTML(name: string) {
|
||||
return escape(name);
|
||||
}
|
||||
|
||||
export interface AutocompleteState {
|
||||
lastEmoji?: string;
|
||||
}
|
||||
|
||||
const names = emojiNames.slice().sort();
|
||||
|
||||
export function autocompleteMesssage(message: string, shift: boolean, state: AutocompleteState): string {
|
||||
return message.replace(/:[a-z0-9_]+:?$/, match => {
|
||||
state.lastEmoji = state.lastEmoji || match;
|
||||
const matches = names.filter(e => e.indexOf(state.lastEmoji!) === 0);
|
||||
const index = matches.indexOf(match);
|
||||
const offset = index === -1 ? 0 : (index + matches.length + (shift ? -1 : 1)) % matches.length;
|
||||
return matches[offset] || match;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { SpriteFont, createSpriteFont } from '../graphics/spriteFont';
|
||||
import * as sprites from '../generated/sprites';
|
||||
|
||||
export let font: SpriteFont;
|
||||
export let fontPal: SpriteFont;
|
||||
export let fontSmall: SpriteFont;
|
||||
export let fontSmallPal: SpriteFont;
|
||||
export let fontMono: SpriteFont;
|
||||
export let fontMonoPal: SpriteFont;
|
||||
|
||||
export function createFonts() {
|
||||
font = createSpriteFont(sprites.font, sprites.emoji, 3);
|
||||
font.lineSpacing = 3;
|
||||
font.letterShiftY = -2;
|
||||
|
||||
fontPal = createSpriteFont(sprites.fontPal, sprites.emojiPal, 3);
|
||||
fontPal.lineSpacing = 3;
|
||||
fontPal.letterShiftY = -2;
|
||||
|
||||
fontSmall = createSpriteFont(sprites.fontSmall, [], 2);
|
||||
fontSmall.lineSpacing = 4;
|
||||
fontSmall.letterShiftY = -2;
|
||||
fontSmall.letterHeightReal += 2;
|
||||
|
||||
fontSmallPal = createSpriteFont(sprites.fontSmallPal, [], 2);
|
||||
fontSmallPal.lineSpacing = 4;
|
||||
fontSmallPal.letterShiftY = -2;
|
||||
fontSmallPal.letterHeightReal += 2;
|
||||
|
||||
fontMono = createSpriteFont(sprites.fontMono, [], 4);
|
||||
fontMono.lineSpacing = 4;
|
||||
fontMono.letterShiftY = -2;
|
||||
fontMono.letterHeightReal += 2;
|
||||
|
||||
fontMonoPal = createSpriteFont(sprites.fontMonoPal, [], 4);
|
||||
fontMonoPal.lineSpacing = 4;
|
||||
fontMonoPal.letterShiftY = -2;
|
||||
fontMonoPal.letterHeightReal += 2;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
export interface Game {
|
||||
fps: number;
|
||||
load(): any;
|
||||
init(): void;
|
||||
update(delta: number, now: number, last: number): void;
|
||||
draw(): void;
|
||||
}
|
||||
|
||||
export interface GameLoop {
|
||||
started: Promise<void>;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
let gameLoop: GameLoop | undefined = undefined;
|
||||
|
||||
export function startGameLoop(game: Game, onError = (e: Error) => console.error(e)): GameLoop {
|
||||
let handle: any;
|
||||
let backup: any;
|
||||
let cancelled = false;
|
||||
|
||||
let last = Math.round(performance.now());
|
||||
let lastFps = last;
|
||||
let frames = 0;
|
||||
let fps = 0;
|
||||
|
||||
function step(now: number, draw: boolean) {
|
||||
if (draw) {
|
||||
handle = requestAnimationFrame(onFrame);
|
||||
}
|
||||
|
||||
frames++;
|
||||
|
||||
if ((now - lastFps) > 1000) {
|
||||
fps = frames * 1000 / (now - lastFps);
|
||||
frames = 0;
|
||||
lastFps = now;
|
||||
}
|
||||
|
||||
try {
|
||||
game.fps = fps;
|
||||
game.update((now - last) / 1000, now, last);
|
||||
|
||||
if (draw) {
|
||||
game.draw();
|
||||
}
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
|
||||
last = now;
|
||||
}
|
||||
|
||||
function onTimer() {
|
||||
step(Math.round(performance.now()), false);
|
||||
backup = setTimeout(onTimer, 1000 / 10);
|
||||
}
|
||||
|
||||
function onFrame() {
|
||||
clearTimeout(backup);
|
||||
step(Math.round(performance.now()), true);
|
||||
backup = setTimeout(onTimer, 1000 / 10);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
cancelAnimationFrame(handle);
|
||||
clearTimeout(backup);
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
if (gameLoop) {
|
||||
gameLoop.cancel();
|
||||
}
|
||||
|
||||
const started = Promise.resolve()
|
||||
.then(() => game.load())
|
||||
.then(() => {
|
||||
if (cancelled) {
|
||||
throw new Error('Cancelled (loop)');
|
||||
} else {
|
||||
game.init();
|
||||
handle = requestAnimationFrame(onFrame);
|
||||
backup = setTimeout(onTimer, 1000 / 10);
|
||||
}
|
||||
});
|
||||
|
||||
gameLoop = { started, cancel };
|
||||
|
||||
return gameLoop;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Notification } from '../common/interfaces';
|
||||
import { PonyTownGame } from './game';
|
||||
import { removeById } from '../common/utils';
|
||||
|
||||
export function addNotification({ notifications }: PonyTownGame, notification: Notification) {
|
||||
const open = notifications.length === 0;
|
||||
|
||||
notifications.push(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.open = open;
|
||||
notification.fresh = false;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
export function removeNotification({ notifications }: PonyTownGame, id: number) {
|
||||
const notification = removeById(notifications, id);
|
||||
|
||||
if (notification && notification.open && notifications.length) {
|
||||
notifications[0].open = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function resetGameFields(game: PonyTownGame) {
|
||||
game.loaded = false;
|
||||
game.placeInQueue = 0;
|
||||
game.playerId = undefined;
|
||||
game.playerName = undefined;
|
||||
game.playerInfo = undefined;
|
||||
game.playerCRC = undefined;
|
||||
game.party = undefined;
|
||||
game.whisperTo = undefined;
|
||||
game.messageQueue = [];
|
||||
game.lastWhisperFrom = undefined;
|
||||
game.onPartyUpdate.next();
|
||||
game.fallbackPonies.clear();
|
||||
}
|
||||
|
||||
export function markGameAsLoaded(game: PonyTownGame) {
|
||||
if (!game.loaded) {
|
||||
game.loaded = true;
|
||||
game.fullyLoaded = false;
|
||||
setTimeout(() => game.fullyLoaded = true, 300);
|
||||
}
|
||||
}
|
||||
|
||||
export function isSelected(game: PonyTownGame, id: number) {
|
||||
return game.selected && game.selected.id === id;
|
||||
}
|
||||
@@ -0,0 +1,802 @@
|
||||
import { compact, escapeRegExp, repeat } from 'lodash';
|
||||
import { createBinaryReader, readUint8, readUint32, readUint16 } from 'ag-sockets/dist/browser';
|
||||
import { decodeString } from 'ag-sockets/dist/utf8';
|
||||
import {
|
||||
Entity, Region, Pony, EntityState, PonyOptions, MessageType, isNonIgnorableMessage, EntityPlayerState,
|
||||
EntityOrPonyOptions, isPublicMessage, FakeEntity, Action, WorldMap, DoAction, UpdateType, DecodedUpdate,
|
||||
PonyData, WorldStateFlags, FriendStatusData, FriendStatusFlags, isWhisper, isWhisperTo,
|
||||
} from '../common/interfaces';
|
||||
import { bitmask, setFlag, findById, distance, hasFlag, distanceXY, invalidEnum, removeItem } from '../common/utils';
|
||||
import { isChatVisible } from '../common/camera';
|
||||
import { createRegion, worldToRegionX, worldToRegionY } from '../common/region';
|
||||
import { createAnEntity, poof, poof2 } from '../common/entities';
|
||||
import { getPonyState, setPonyState, isPonyFlying, addChatBubble, isHidden } from '../common/entityUtils';
|
||||
import {
|
||||
isPony, createPony, setPonyExpression, updatePonyInfo, updatePonyHold, doPonyAction, hasHeadAnimation,
|
||||
setHeadAnimation,
|
||||
doBoopPonyAction
|
||||
} from '../common/pony';
|
||||
import { PonyTownGame } from './game';
|
||||
import { setupPlayer, savePlayerPosition } from './sec';
|
||||
import { PONY_INFO_KEY, FLY_DELAY, isChatlogRangeUnlimited, SECOND, PONY_TYPE } from '../common/constants';
|
||||
import { getSaysTime, containsCyrillic } from './clientUtils';
|
||||
import { dismissSays } from '../graphics/graphicsUtils';
|
||||
import { decodeUpdate, readOneUpdate } from '../common/encoders/updateDecoder';
|
||||
import { updateEntityVelocity } from '../common/entityUtils';
|
||||
import { decodePonyInfo } from '../common/compressPony';
|
||||
import { mockPaletteManager } from '../common/ponyInfo';
|
||||
import { yawn, laugh, sneeze } from './ponyAnimations';
|
||||
import {
|
||||
findEntityById, getRegionGlobal, setTile, removeEntity, addEntity, removeEntityDirectly, setRegion,
|
||||
addEntityToMapRegion, switchEntityRegion, getRegionUnsafe, addOrRemoveFromEntityList,
|
||||
} from '../common/worldMap';
|
||||
import { isSelected } from './gameUtils';
|
||||
import { compareFriends } from '../components/services/model';
|
||||
import { canCollideWith } from '../common/collision';
|
||||
import { hasDrawLight, hasLightSprite } from './draw';
|
||||
|
||||
function log(message: string) {
|
||||
if (DEVELOPMENT && !TESTS) {
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddEntity(game: PonyTownGame, region: Region, update: DecodedUpdate, initial: boolean) {
|
||||
const {
|
||||
id, type = 0, x = 0, y = 0, vx = 0, vy = 0, state = 0, playerState = 0, options = {},
|
||||
name, filterName, info, crc = 0, action // , expression
|
||||
} = update;
|
||||
|
||||
const filteredName = filterEntityName(game, name, filterName);
|
||||
const entity = createEntityOrPony(game, type, id, x, y, options, crc, filteredName, info, state);
|
||||
|
||||
entity.id = id;
|
||||
entity.x = x;
|
||||
entity.y = y;
|
||||
entity.vx = vx;
|
||||
entity.vy = vy;
|
||||
entity.playerState = playerState || EntityPlayerState.None;
|
||||
|
||||
addEntityToMapRegion(game.map, region, entity);
|
||||
|
||||
if (isPony(entity)) {
|
||||
if (id === game.playerId) {
|
||||
game.apply(() => setupPlayer(game, entity));
|
||||
}
|
||||
|
||||
if (isSelected(game, id)) {
|
||||
game.select(entity);
|
||||
}
|
||||
|
||||
if (game.whisperTo && game.whisperTo.id === id) {
|
||||
game.whisperTo = entity;
|
||||
}
|
||||
|
||||
if (!initial) {
|
||||
game.onPonyAddOrUpdate.next(entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (action !== undefined) {
|
||||
handleAction(game, id, action);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleUpdateEntity(game: PonyTownGame, update: DecodedUpdate) {
|
||||
const {
|
||||
id, x, y, vx, vy, state, playerState, expression, options, switchRegion,
|
||||
name, filterName, info, crc = 0, action
|
||||
} = update;
|
||||
|
||||
const filteredName = filterEntityName(game, name, filterName);
|
||||
const entity = findEntityByIdInGame(game, id);
|
||||
|
||||
if (entity) {
|
||||
const isPlayer = id === game.playerId;
|
||||
|
||||
if (x !== undefined && y !== undefined) {
|
||||
if (switchRegion) {
|
||||
if (DEVELOPMENT && isPlayer && !getRegionGlobal(game.map, x, y)) {
|
||||
console.error(`Switching player to unsubscribed region`);
|
||||
}
|
||||
|
||||
switchEntityRegion(game.map, entity, x, y);
|
||||
}
|
||||
|
||||
if (!isPlayer) {
|
||||
// if (DEVELOPMENT && isPony(entity)) {
|
||||
// const dx = entity.x - x;
|
||||
// const dy = entity.y - y;
|
||||
// console.log(`adjust x: [${num(dx)}] (${ms(dx)}) y: [${num(dy)}] (${ms(dy)})`);
|
||||
// }
|
||||
|
||||
entity.x = x;
|
||||
entity.y = y;
|
||||
updateEntityVelocity(game.map, entity, vx, vy);
|
||||
|
||||
if (canCollideWith(entity)) {
|
||||
const rx = worldToRegionX(entity.x, game.map);
|
||||
const ry = worldToRegionY(entity.y, game.map);
|
||||
|
||||
for (let y = -1; y <= 1; y++) {
|
||||
for (let x = -1; x <= 1; x++) {
|
||||
const region = getRegionUnsafe(game.map, rx + x, ry + y);
|
||||
|
||||
if (region) {
|
||||
region.colliderDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (distanceXY(entity.x, entity.y, x, y) > 8) {
|
||||
log(`Fixing player position (${entity.x}, ${entity.y}) => (${x}, ${y})`);
|
||||
entity.x = x;
|
||||
entity.y = y;
|
||||
savePlayerPosition();
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== undefined) {
|
||||
updateEntityStateInternal(game, entity, state);
|
||||
}
|
||||
|
||||
if (playerState !== undefined) {
|
||||
updateEntityPlayerStateInternal(game, entity, playerState);
|
||||
}
|
||||
|
||||
if (expression !== undefined && isPony(entity)) {
|
||||
setPonyExpression(entity, expression);
|
||||
}
|
||||
|
||||
if (options != null) {
|
||||
updateEntityOptionsInternal(entity, options, game);
|
||||
}
|
||||
|
||||
if (filteredName !== undefined && !isPlayer) {
|
||||
entity.name = filteredName;
|
||||
}
|
||||
|
||||
if (info !== undefined && !isPlayer) {
|
||||
const ponyInfo = bitmask(info, PONY_INFO_KEY);
|
||||
|
||||
if (entity.fake) {
|
||||
(entity as Pony).palettePonyInfo = decodePonyInfo(ponyInfo, mockPaletteManager);
|
||||
} else {
|
||||
updatePonyInfoWithPoof(game, entity, ponyInfo, crc);
|
||||
}
|
||||
}
|
||||
|
||||
if (action !== undefined) {
|
||||
handleAction(game, id, action);
|
||||
}
|
||||
|
||||
applyIfSelected(game, id);
|
||||
} else {
|
||||
log(`handleUpdateEntity: missing entity: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleUpdatePonies(game: PonyTownGame, ponies: PonyData[]) {
|
||||
for (const [id, options = {}, name, info, playerState, nameBad] of ponies) {
|
||||
const decodedName = name && decodeString(name) || undefined;
|
||||
const filteredName = filterEntityName(game, decodedName, nameBad);
|
||||
const decodedInfo = info ? bitmask(info, PONY_INFO_KEY) : '';
|
||||
const pony = createPonyEntity(game, id, options, filteredName, decodedInfo, EntityState.None);
|
||||
pony.playerState = playerState;
|
||||
game.fallbackPonies.set(pony.id, pony);
|
||||
}
|
||||
|
||||
const missing = game.party && game.party.members.filter(p => !p.pony);
|
||||
|
||||
if (missing && missing.length) {
|
||||
game.apply(() => missing.forEach(p => p.pony = game.fallbackPonies.get(p.id)));
|
||||
}
|
||||
}
|
||||
|
||||
function createPonyEntity(
|
||||
game: PonyTownGame, id: number, options: PonyOptions, name: string | undefined, info: string | Uint8Array,
|
||||
state: EntityState
|
||||
) {
|
||||
if (!game.webgl) {
|
||||
throw new Error('WebGL not initialized');
|
||||
}
|
||||
|
||||
const pony = createPony(id, state, info, game.webgl.palettes.defaultPalette, game.paletteManager);
|
||||
|
||||
if (name) {
|
||||
pony.name = name;
|
||||
}
|
||||
|
||||
updateEntityOptionsInternal(pony, options, game);
|
||||
|
||||
// bypass name/info filtering for player pony
|
||||
if (id === game.playerId) {
|
||||
if (game.playerName) {
|
||||
pony.name = game.playerName;
|
||||
}
|
||||
|
||||
if (game.playerInfo) {
|
||||
pony.crc = game.playerCRC;
|
||||
updatePonyInfo(pony, game.playerInfo, game.applyChanges);
|
||||
}
|
||||
}
|
||||
|
||||
return pony;
|
||||
}
|
||||
|
||||
function updateEntityStateInternal(game: PonyTownGame, entity: Entity, state: EntityState) {
|
||||
if (entity === game.player) {
|
||||
const right = game.rightOverride;
|
||||
const headTurned = game.headTurnedOverride;
|
||||
const stateOverride = game.stateOverride;
|
||||
|
||||
if (right !== undefined) {
|
||||
state = setFlag(state, EntityState.FacingRight, right);
|
||||
game.rightOverride = undefined;
|
||||
}
|
||||
|
||||
if (headTurned !== undefined) {
|
||||
state = setFlag(state, EntityState.HeadTurned, headTurned);
|
||||
game.headTurnedOverride = undefined;
|
||||
}
|
||||
|
||||
if (stateOverride !== undefined) {
|
||||
if (stateOverride !== getPonyState(state)) {
|
||||
state = setPonyState(state, stateOverride);
|
||||
}
|
||||
|
||||
game.stateOverride = undefined;
|
||||
}
|
||||
|
||||
game.onActionsUpdate.next();
|
||||
}
|
||||
|
||||
const wasPonyFlying = isPonyFlying(entity);
|
||||
const hadLight = hasDrawLight(entity);
|
||||
const hadLightSprite = hasLightSprite(entity);
|
||||
|
||||
entity.state = state;
|
||||
|
||||
if (!wasPonyFlying && isPonyFlying(entity) && isPony(entity)) {
|
||||
entity.inTheAirDelay = FLY_DELAY;
|
||||
}
|
||||
|
||||
const hasLight = hasDrawLight(entity);
|
||||
const hasLightSprite1 = hasLightSprite(entity);
|
||||
|
||||
addOrRemoveFromEntityList(game.map.entitiesLight, entity, hadLight, hasLight);
|
||||
addOrRemoveFromEntityList(game.map.entitiesLightSprite, entity, hadLightSprite, hasLightSprite1);
|
||||
}
|
||||
|
||||
function updateEntityPlayerStateInternal(game: PonyTownGame, entity: Entity, playerState: EntityPlayerState) {
|
||||
if (!entity.fake && !isHidden(entity) && hasFlag(playerState, EntityPlayerState.Hidden)) {
|
||||
playEffect(game, entity, poof.type);
|
||||
|
||||
if (isSelected(game, entity.id)) {
|
||||
game.select(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
entity.playerState = playerState;
|
||||
}
|
||||
|
||||
function findEntityByIdInGame(game: PonyTownGame, id: number) {
|
||||
let entity = findEntityById(game.map, id);
|
||||
|
||||
if (!entity && isSelected(game, id)) {
|
||||
entity = game.selected;
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
function applyIfSelected(game: PonyTownGame, id: number) {
|
||||
if (isSelected(game, id)) {
|
||||
game.applyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
export function handleUpdates(game: PonyTownGame, updates: Uint8Array) {
|
||||
const reader = createBinaryReader(updates);
|
||||
|
||||
while (reader.offset < reader.view.byteLength) {
|
||||
const type = readUint8(reader) as UpdateType;
|
||||
|
||||
switch (type) {
|
||||
case UpdateType.None:
|
||||
log(`handleUpdates (none)`);
|
||||
break;
|
||||
case UpdateType.AddEntity: {
|
||||
const update = readOneUpdate(reader)!;
|
||||
const { x = 0, y = 0 } = update;
|
||||
const region = getRegionGlobal(game.map, x, y);
|
||||
|
||||
if (region) {
|
||||
handleAddEntity(game, region, update, false);
|
||||
} else {
|
||||
log(`handleUpdates (add): missing region at ${x} ${y}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UpdateType.UpdateEntity: {
|
||||
const update = readOneUpdate(reader)!;
|
||||
handleUpdateEntity(game, update);
|
||||
break;
|
||||
}
|
||||
case UpdateType.RemoveEntity: {
|
||||
const id = readUint32(reader);
|
||||
handleRemoveEntity(game, id);
|
||||
break;
|
||||
}
|
||||
case UpdateType.UpdateTile: {
|
||||
const x = readUint16(reader);
|
||||
const y = readUint16(reader);
|
||||
const type = readUint8(reader);
|
||||
setTile(game.map, x, y, type);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
invalidEnum(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePonyInfoWithPoof(game: PonyTownGame, entity: Entity, info: string | Uint8Array, crc: number) {
|
||||
const update = (pony: Pony) => {
|
||||
pony.crc = crc;
|
||||
updatePonyInfo(pony, info, game.applyChanges);
|
||||
game.onPonyAddOrUpdate.next(pony);
|
||||
};
|
||||
|
||||
if (entity && isPony(entity)) {
|
||||
if (isHidden(entity)) {
|
||||
update(entity);
|
||||
} else {
|
||||
playEffect(game, entity, poof2.type);
|
||||
setTimeout(() => update(entity), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleRemoveEntity(game: PonyTownGame, id: number) {
|
||||
const entity = findEntityById(game.map, id);
|
||||
|
||||
if (entity) {
|
||||
removeEntity(game.map, entity);
|
||||
} else {
|
||||
log(`handleRemoveEntity: Missing entity: ${id}`);
|
||||
}
|
||||
|
||||
if (id === game.playerId) {
|
||||
log(`handleRemoveEntity: Removing player`);
|
||||
}
|
||||
|
||||
if (entity && entity.type === PONY_TYPE) {
|
||||
playEffect(game, entity, poof.type);
|
||||
}
|
||||
|
||||
if (isSelected(game, id)) {
|
||||
setTimeout(() => {
|
||||
if (isSelected(game, id)) {
|
||||
game.select(undefined);
|
||||
}
|
||||
}, 15 * SECOND);
|
||||
}
|
||||
}
|
||||
|
||||
function findPonyById(map: WorldMap, id: number) {
|
||||
const entity = findEntityById(map, id);
|
||||
return entity && isPony(entity) ? entity : undefined;
|
||||
}
|
||||
|
||||
export function handleAction(game: PonyTownGame, id: number, action: Action) {
|
||||
const pony = findPonyById(game.map, id);
|
||||
|
||||
if (pony) {
|
||||
switch (action) {
|
||||
case Action.Boop:
|
||||
doBoopPonyAction(game, pony);
|
||||
break;
|
||||
case Action.HoldPoof:
|
||||
doPonyAction(pony, DoAction.HoldPoof);
|
||||
break;
|
||||
case Action.Yawn:
|
||||
if (!hasHeadAnimation(pony)) {
|
||||
setHeadAnimation(pony, yawn);
|
||||
}
|
||||
break;
|
||||
case Action.Laugh:
|
||||
if (!hasHeadAnimation(pony)) {
|
||||
setHeadAnimation(pony, laugh);
|
||||
}
|
||||
break;
|
||||
case Action.Sneeze:
|
||||
if (!hasHeadAnimation(pony)) {
|
||||
setHeadAnimation(pony, sneeze);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log(`handleAction: Invalid action: ${action}`);
|
||||
}
|
||||
} else {
|
||||
log(`handleAction: Missing entity: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function playEffect(game: PonyTownGame, target: Entity, type: number) {
|
||||
if (isHidden(target))
|
||||
return;
|
||||
|
||||
try {
|
||||
const entity = createAnEntity(type, 0, target.x, target.y, {}, game.paletteManager, game);
|
||||
addEntity(game.map, entity);
|
||||
setTimeout(() => removeEntityDirectly(game.map, entity), 1000);
|
||||
} catch (e) {
|
||||
DEVELOPMENT && console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function findEntityOrMockByAnyMeans(game: PonyTownGame, id: number) {
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let entity: Entity | FakeEntity | undefined = findEntityById(game.map, id);
|
||||
|
||||
if (!entity && game.party) {
|
||||
const member = findById(game.party.members, id);
|
||||
entity = member && member.pony;
|
||||
}
|
||||
|
||||
if (!entity) {
|
||||
const friend = game.model.friends && game.model.friends.find(f => f.entityId === id);
|
||||
|
||||
if (friend) {
|
||||
entity = { fake: true, type: PONY_TYPE, id: friend.entityId, name: friend.actualName, crc: friend.crc };
|
||||
}
|
||||
}
|
||||
|
||||
if (!entity) {
|
||||
entity = game.findEntityFromChatLog(id);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
export function findBestEntityByName(game: PonyTownGame, name: string): Entity | FakeEntity | undefined {
|
||||
const regex = new RegExp(`^${escapeRegExp(name)}$`, 'i');
|
||||
|
||||
if (game.model.friends) {
|
||||
for (const friend of game.model.friends) {
|
||||
if (friend.online && friend.entityId && regex.test(friend.actualName)) {
|
||||
return { fake: true, type: PONY_TYPE, id: friend.entityId, name: friend.actualName, crc: friend.crc };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result: Entity | FakeEntity | undefined = undefined;
|
||||
|
||||
if (game.player) {
|
||||
for (const entity of game.map.entities) {
|
||||
if (entity.type === PONY_TYPE && entity.id !== game.playerId && !isHidden(entity) && entity.name && regex.test(entity.name)) {
|
||||
if (!result || (distance(game.player, entity) < distance(game.player, result))) {
|
||||
result = entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = game.findEntityFromChatLogByName(name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findMatchingEntityNames(game: PonyTownGame, match: string): string[] {
|
||||
const result: string[] = [];
|
||||
const ids = new Set<number>();
|
||||
const regex = new RegExp(`^${escapeRegExp(match)}`, 'i');
|
||||
|
||||
if (game.model.friends) {
|
||||
for (const friend of game.model.friends) {
|
||||
if (friend.online && friend.entityId && friend.actualName && regex.test(friend.actualName)) {
|
||||
ids.add(friend.entityId);
|
||||
result.push(friend.actualName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const entity of game.map.entities) {
|
||||
if (
|
||||
entity.type === PONY_TYPE &&
|
||||
entity.id !== game.playerId &&
|
||||
entity.name &&
|
||||
!isHidden(entity) &&
|
||||
regex.test(entity.name) &&
|
||||
!ids.has(entity.id)
|
||||
) {
|
||||
result.push(entity.name);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let cachedFilter: string | undefined = undefined;
|
||||
let cachedRegex: RegExp | undefined = undefined;
|
||||
|
||||
export function containsFilteredWords(message: string, filter: string | undefined) {
|
||||
if (cachedFilter !== filter) {
|
||||
if (filter) {
|
||||
const words = compact(filter.replace(/[,]/g, ' ').split(/[\r\n\t ]+/g).map(x => x.trim()));
|
||||
cachedRegex = new RegExp(`(^| )(${words.map(escapeRegExp).join('|')})($| )`, 'i');
|
||||
} else {
|
||||
cachedRegex = undefined;
|
||||
}
|
||||
|
||||
cachedFilter = filter;
|
||||
}
|
||||
|
||||
return cachedRegex && cachedRegex.test(message);
|
||||
}
|
||||
|
||||
export function handleSays(game: PonyTownGame, id: number, message: string, type: MessageType) {
|
||||
const entity = findEntityOrMockByAnyMeans(game, id);
|
||||
|
||||
if (entity) {
|
||||
handleSay(game, entity, message, type);
|
||||
} else {
|
||||
DEVELOPMENT && console.warn('incomplete say');
|
||||
game.incompleteSays.push({ id, message, type, time: Date.now() });
|
||||
game.send(server => server.actionParam2(Action.RequestEntityInfo, id));
|
||||
}
|
||||
}
|
||||
|
||||
function isFriendEntityId(game: PonyTownGame, id: number) {
|
||||
if (game.model.friends) {
|
||||
for (const friend of game.model.friends) {
|
||||
if (friend.entityId === id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldShowChatMessage(game: PonyTownGame, entity: Entity | FakeEntity, message: string, type: MessageType): boolean {
|
||||
if (entity === game.player)
|
||||
return true;
|
||||
|
||||
if (isWhisperTo(type))
|
||||
return true;
|
||||
|
||||
if (isWhisper(type) && isFriendEntityId(game, entity.id))
|
||||
return true;
|
||||
|
||||
if (isPublicMessage(type) && !entity.fake && !isChatVisible(game.camera, entity))
|
||||
return false;
|
||||
|
||||
if (isNonIgnorableMessage(type))
|
||||
return true;
|
||||
|
||||
if (game.settings.account.filterCyrillic && containsCyrillic(message))
|
||||
return false;
|
||||
|
||||
if (game.settings.account.ignorePublicChat && isPublicMessage(type))
|
||||
return false;
|
||||
|
||||
if (isWhisper(type) && game.settings.account.ignoreNonFriendWhispers)
|
||||
return false;
|
||||
|
||||
if (containsFilteredWords(message, game.settings.account.filterWords))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isChatInRange(entity: Entity, player: Entity | undefined, range: number | undefined) {
|
||||
return player === undefined || isChatlogRangeUnlimited(range) || distance(entity, player) < range!;
|
||||
}
|
||||
|
||||
function shouldShowChatMessageInChatlog(game: PonyTownGame, entity: Entity | FakeEntity, type: MessageType) {
|
||||
if (entity.type !== PONY_TYPE)
|
||||
return false;
|
||||
|
||||
if (entity.fake)
|
||||
return true;
|
||||
|
||||
if (!isPublicMessage(type))
|
||||
return true;
|
||||
|
||||
if (!isChatInRange(entity, game.player, game.settings.account.chatlogRange))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function handleSay(game: PonyTownGame, entity: Entity | FakeEntity, message: string, type: MessageType) {
|
||||
if (!shouldShowChatMessage(game, entity, message, type))
|
||||
return;
|
||||
|
||||
if (type === MessageType.Dismiss || message === '.') {
|
||||
if (!entity.fake && entity.says) {
|
||||
dismissSays(entity.says);
|
||||
}
|
||||
} else {
|
||||
const bubbleEntity = isWhisperTo(type) ? game.player : entity;
|
||||
|
||||
if (bubbleEntity && !bubbleEntity.fake && game.map.entitiesById.has(bubbleEntity.id)) {
|
||||
const total = getSaysTime(message);
|
||||
addChatBubble(game.map, bubbleEntity, { message, type, total, timer: total, created: Date.now() });
|
||||
}
|
||||
|
||||
if (isWhisper(type)) {
|
||||
const friend = game.model.friends && game.model.friends.find(f => f.entityId === entity.id);
|
||||
game.lastWhisperFrom = { entityId: entity.id, accountId: friend && friend.accountId };
|
||||
}
|
||||
|
||||
if (shouldShowChatMessageInChatlog(game, entity, type)) {
|
||||
const { id, name = '', crc } = entity;
|
||||
game.messageQueue.push({ id, crc, name, message, type });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleEntityInfo(game: PonyTownGame, id: number, name: string, crc: number, nameBad: boolean) {
|
||||
name = filterEntityName(game, name, nameBad)!;
|
||||
|
||||
for (let i = 0; i < game.incompleteSays.length;) {
|
||||
const say = game.incompleteSays[i];
|
||||
|
||||
if (say.id === id) {
|
||||
game.incompleteSays.splice(i, 1);
|
||||
const entity: FakeEntity = { fake: true, type: PONY_TYPE, id, name, crc };
|
||||
handleSay(game, entity, say.message, say.type);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeRegion(game: PonyTownGame, data: Uint8Array) {
|
||||
const { x, y, updates, tileData } = decodeUpdate(data);
|
||||
const region = createRegion(x, y, tileData!);
|
||||
const initial = !game.loaded;
|
||||
setRegion(game.map, x, y, region);
|
||||
|
||||
for (const update of updates) {
|
||||
handleAddEntity(game, region, update, initial);
|
||||
}
|
||||
}
|
||||
|
||||
export function filterEntityName({ settings, worldFlags }: PonyTownGame, name: string | undefined, nameBad: boolean) {
|
||||
if (name && nameBad && (settings.account.filterSwearWords || hasFlag(worldFlags, WorldStateFlags.Safe))) {
|
||||
return repeat('*', name.length);
|
||||
} else if (name && containsFilteredWords(name, settings.account.filterWords)) {
|
||||
return repeat('?', name.length);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
function createEntityOrPony(
|
||||
game: PonyTownGame, type: number, id: number, x: number, y: number, options: EntityOrPonyOptions,
|
||||
crc: number, name: string | undefined, info: Uint8Array | undefined, state: EntityState
|
||||
): Entity {
|
||||
if (type === PONY_TYPE) {
|
||||
const entity = createPonyEntity(game, id, options, name, info ? bitmask(info, PONY_INFO_KEY) : '', state);
|
||||
const member = game.party && game.party.members.find(p => p.id === id);
|
||||
entity.crc = crc;
|
||||
|
||||
if (member) {
|
||||
game.apply(() => member.pony = entity);
|
||||
}
|
||||
|
||||
if (isSelected(game, id)) {
|
||||
game.select(entity);
|
||||
}
|
||||
|
||||
return entity;
|
||||
} else {
|
||||
const entity = createAnEntity(type, id, x, y, options, game.paletteManager, game);
|
||||
|
||||
entity.state = state;
|
||||
|
||||
if (name) {
|
||||
entity.name = name;
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
function updateEntityOptionsInternal(entity: Entity, options: Partial<EntityOrPonyOptions>, game: PonyTownGame) {
|
||||
Object.assign(entity, options);
|
||||
|
||||
if (isPony(entity) && 'hold' in options) {
|
||||
updatePonyHold(entity, game);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleUpdateFriends(game: PonyTownGame, friends: FriendStatusData[], removeMissing: boolean) {
|
||||
if (!game.model.friends)
|
||||
return;
|
||||
|
||||
for (const { accountId, accountName, status, entityId, name, info, crc, nameBad = false } of friends) {
|
||||
let friend = game.model.friends.find(f => f.accountId === accountId);
|
||||
|
||||
if (hasFlag(status, FriendStatusFlags.Remove)) {
|
||||
if (friend) {
|
||||
removeItem(game.model.friends, friend);
|
||||
}
|
||||
} else {
|
||||
if (!friend) {
|
||||
friend = {
|
||||
accountId,
|
||||
accountName: '',
|
||||
online: false,
|
||||
name: undefined,
|
||||
nameBad: false,
|
||||
pony: undefined,
|
||||
entityId: 0,
|
||||
crc: 0,
|
||||
ponyInfo: undefined,
|
||||
actualName: '',
|
||||
};
|
||||
|
||||
game.model.friends.push(friend);
|
||||
}
|
||||
|
||||
friend.online = hasFlag(status, FriendStatusFlags.Online);
|
||||
|
||||
if (accountName !== undefined) {
|
||||
friend.accountName = accountName;
|
||||
}
|
||||
|
||||
if (entityId !== undefined) {
|
||||
if (game.lastWhisperFrom && game.lastWhisperFrom.accountId === friend.accountId) {
|
||||
game.lastWhisperFrom.entityId = entityId;
|
||||
}
|
||||
|
||||
game.onEntityIdUpdate.next({ old: friend.entityId, new: entityId });
|
||||
|
||||
friend.entityId = entityId;
|
||||
}
|
||||
|
||||
if (name !== undefined) {
|
||||
friend.name = name;
|
||||
friend.nameBad = nameBad;
|
||||
friend.actualName = filterEntityName(game, name, nameBad) || '';
|
||||
}
|
||||
|
||||
if (crc !== undefined) {
|
||||
friend.crc = crc;
|
||||
}
|
||||
|
||||
if (info !== undefined) {
|
||||
friend.pony = info;
|
||||
friend.ponyInfo = decodePonyInfo(info, mockPaletteManager);
|
||||
}
|
||||
|
||||
if (friend.entityId && game.whisperTo && game.whisperTo.id === friend.entityId) {
|
||||
game.whisperTo.name = friend.actualName;
|
||||
game.whisperTo.crc = friend.crc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removeMissing) {
|
||||
for (let i = game.model.friends.length - 1; i >= 0; i--) {
|
||||
if (!friends.find(f => f.accountId === game.model.friends![i].accountId)) {
|
||||
game.model.friends.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
DEVELOPMENT && console.log('Refreshing friend list');
|
||||
}
|
||||
|
||||
game.model.friends.sort(compareFriends);
|
||||
game.apply(() => { });
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { hasEmojis, splitEmojis, findEmoji, getEmojiImageAsync } from './emoji';
|
||||
import { Dict } from '../common/interfaces';
|
||||
import { font } from './fonts';
|
||||
import { getCharacterSprite } from '../graphics/spriteFont';
|
||||
|
||||
export function createHtmlNodes(value: string | undefined, scale: number): Node[] {
|
||||
return value ? splitEmojis(value).map(x => {
|
||||
const sprite = hasEmojis(x) && font && getCharacterSprite(x, font);
|
||||
|
||||
if (sprite) {
|
||||
const emote = findEmoji(x);
|
||||
const img = document.createElement('img');
|
||||
img.className = 'pixelart';
|
||||
img.style.display = 'inline-block';
|
||||
img.style.visibility = 'hidden';
|
||||
img.style.width = `${(sprite.w + sprite.ox) * scale}px`;
|
||||
img.style.height = `${10 * scale}px`;
|
||||
|
||||
if (emote) {
|
||||
img.setAttribute('aria-label', emote.names[0]);
|
||||
}
|
||||
|
||||
getEmojiImageAsync(sprite, src => {
|
||||
img.alt = x;
|
||||
img.src = src;
|
||||
img.style.visibility = 'visible';
|
||||
});
|
||||
|
||||
return img;
|
||||
} else {
|
||||
return document.createTextNode(x);
|
||||
}
|
||||
}) : [];
|
||||
}
|
||||
|
||||
export function textNode(text: string) {
|
||||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
export function element(
|
||||
tag: string, className?: string, nodes?: (Node | undefined)[], attrs?: Dict<any>, events?: Dict<() => any>
|
||||
) {
|
||||
const element = document.createElement(tag);
|
||||
|
||||
if (className) {
|
||||
element.className = className;
|
||||
}
|
||||
|
||||
if (nodes !== undefined) {
|
||||
appendAllNodes(element, nodes);
|
||||
}
|
||||
|
||||
if (attrs !== undefined) {
|
||||
Object.keys(attrs).forEach(key => element.setAttribute(key, attrs[key]));
|
||||
}
|
||||
|
||||
if (events !== undefined) {
|
||||
Object.keys(events).forEach(key => element.addEventListener(key, events[key]));
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
export function appendAllNodes(element: Element, nodes: (Node | undefined)[]) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
|
||||
if (node !== undefined) {
|
||||
element.appendChild(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function removeAllNodes(element: Element) {
|
||||
let child: Node | null;
|
||||
|
||||
while (child = element.lastChild) {
|
||||
element.removeChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeFirstChild(element: HTMLElement) {
|
||||
let child: Node | null;
|
||||
|
||||
if (child = element.firstChild) {
|
||||
element.removeChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeElement(element: HTMLElement) {
|
||||
element.parentElement && element.parentElement.removeChild(element);
|
||||
}
|
||||
|
||||
export function replaceNodes(element: HTMLElement, text: string) {
|
||||
while (element.lastChild && element.lastChild !== element.firstChild) {
|
||||
element.removeChild(element.lastChild);
|
||||
}
|
||||
|
||||
let firstChild = element.firstChild;
|
||||
|
||||
if (!firstChild) {
|
||||
element.appendChild(firstChild = textNode(''));
|
||||
}
|
||||
|
||||
if (hasEmojis(text)) {
|
||||
firstChild.nodeValue = '';
|
||||
appendAllNodes(element, createHtmlNodes(text, 2));
|
||||
} else {
|
||||
firstChild.nodeValue = text;
|
||||
}
|
||||
}
|
||||
|
||||
export function findParentElement(element: HTMLElement, selector: string) {
|
||||
const elements = Array.from(document.querySelectorAll(selector));
|
||||
let current = element.parentElement;
|
||||
|
||||
while (current && elements.indexOf(current) === -1) {
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
export function findFocusableElements(root: HTMLElement) {
|
||||
const elements = root.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
return Array.from(elements) as HTMLElement[];
|
||||
}
|
||||
|
||||
export function focusFirstElement(root: HTMLElement) {
|
||||
const elements = findFocusableElements(root);
|
||||
|
||||
if (elements.length) {
|
||||
elements[0].focus();
|
||||
return elements[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function focusElement(root: HTMLElement, selector: string) {
|
||||
const target = root.querySelector(selector) as HTMLElement | null;
|
||||
|
||||
if (target) {
|
||||
target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export function focusElementAfterTimeout(root: HTMLElement, selector: string) {
|
||||
setTimeout(() => focusElement(root, selector), 10);
|
||||
}
|
||||
|
||||
export function isParentOf(parent: Element, child: Element) {
|
||||
for (let current = child.parentElement; current; current = current.parentElement) {
|
||||
if (current === parent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function showTextInNewTab(text: string) {
|
||||
const wnd = window.open()!;
|
||||
const pre = wnd.document.createElement('pre');
|
||||
pre.innerText = text;
|
||||
wnd.document.body.appendChild(pre);
|
||||
}
|
||||
|
||||
export function addStyle(style: string) {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.appendChild(document.createTextNode(style));
|
||||
document.head.appendChild(styleElement);
|
||||
return styleElement;
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import {
|
||||
faCrown,
|
||||
faPlug,
|
||||
faGamepad,
|
||||
faMobile,
|
||||
faTablet,
|
||||
faTv,
|
||||
} from '../generated/fa-icons';
|
||||
|
||||
export {
|
||||
faHashtag,
|
||||
faCog,
|
||||
faCogs,
|
||||
faMinus,
|
||||
faPlus,
|
||||
faCheck,
|
||||
faFlag,
|
||||
faStickyNote,
|
||||
faCertificate,
|
||||
faGlobe,
|
||||
faGamepad,
|
||||
faDesktop,
|
||||
faQuestionCircle,
|
||||
faInfo,
|
||||
faSync,
|
||||
faUserSecret,
|
||||
faTrash,
|
||||
faLock,
|
||||
faApple,
|
||||
faEdit,
|
||||
faImage,
|
||||
faLaughBeam,
|
||||
faLanguage,
|
||||
faCircle,
|
||||
faEyeSlash,
|
||||
faEnvelope,
|
||||
faFont,
|
||||
faCompressArrowsAlt,
|
||||
faIdBadge,
|
||||
faFilter,
|
||||
faEraser,
|
||||
faBell,
|
||||
faClock,
|
||||
faComment,
|
||||
faComments,
|
||||
faCommentSlash,
|
||||
faHdd,
|
||||
faMicrochip,
|
||||
faUser,
|
||||
faUsers,
|
||||
faUserFriends,
|
||||
faSpinner,
|
||||
faBan,
|
||||
faMicrophoneSlash,
|
||||
faFileAlt,
|
||||
faTimes,
|
||||
faSearch,
|
||||
faClipboard,
|
||||
faChevronUp,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faStar,
|
||||
faAngleDoubleUp,
|
||||
faAngleDoubleDown,
|
||||
faAngleDoubleLeft,
|
||||
faAngleDoubleRight,
|
||||
faPlay,
|
||||
faRedo,
|
||||
faSave,
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowUp,
|
||||
faArrowDown,
|
||||
faEyeDropper,
|
||||
faPaintBrush,
|
||||
faEllipsisV,
|
||||
faExclamationCircle,
|
||||
faUserPlus,
|
||||
faUserMinus,
|
||||
faUserTimes,
|
||||
faSignOutAlt,
|
||||
faStepForward,
|
||||
faVolumeOff,
|
||||
faVolumeDown,
|
||||
faVolumeUp,
|
||||
faHome,
|
||||
faStop,
|
||||
faRetweet,
|
||||
faFile,
|
||||
faCopy,
|
||||
faShare,
|
||||
faCode,
|
||||
faTerminal,
|
||||
faClone,
|
||||
faPause,
|
||||
faCrosshairs,
|
||||
faFileImage,
|
||||
faHeart,
|
||||
faPlusCircle,
|
||||
faMinusCircle,
|
||||
faInfoCircle,
|
||||
faCaretUp,
|
||||
faCaretSquareUp,
|
||||
faCaretSquareDown,
|
||||
faCheckCircle,
|
||||
faWrench,
|
||||
faDrawPolygon,
|
||||
faUserCog,
|
||||
faSlidersH,
|
||||
faExchangeAlt,
|
||||
faDatabase,
|
||||
faHorseHead,
|
||||
faMapMarkerAlt,
|
||||
faChartPie,
|
||||
faCalendar,
|
||||
} from '../generated/fa-icons';
|
||||
|
||||
import {
|
||||
faPatreon,
|
||||
faDeviantart,
|
||||
faTwitter,
|
||||
faTumblr,
|
||||
faFacebook,
|
||||
faGithub,
|
||||
faVk,
|
||||
faGoogle,
|
||||
faChrome,
|
||||
faInternetExplorer,
|
||||
faEdge,
|
||||
faAndroid,
|
||||
faFirefox,
|
||||
faSafari,
|
||||
faOpera,
|
||||
faWindows,
|
||||
faApple,
|
||||
faLinux,
|
||||
faAmilia,
|
||||
faYandexInternational,
|
||||
} from '../generated/fa-icons';
|
||||
|
||||
export {
|
||||
faPatreon,
|
||||
faDeviantart,
|
||||
faTwitter,
|
||||
faTumblr,
|
||||
faGithub,
|
||||
} from '../generated/fa-icons';
|
||||
|
||||
export const partyLeaderIcon = faCrown;
|
||||
export const offlineIcon = faPlug;
|
||||
export const emptyIcon = {
|
||||
prefix: 'fas',
|
||||
iconName: 'empty-icon',
|
||||
icon: [512, 512, [], 'ffff', ''],
|
||||
};
|
||||
|
||||
export const oauthIcons: { [key: string]: any; } = {
|
||||
patreon: faPatreon,
|
||||
deviantart: faDeviantart,
|
||||
twitter: faTwitter,
|
||||
tumblr: faTumblr,
|
||||
facebook: faFacebook,
|
||||
github: faGithub,
|
||||
vkontakte: faVk,
|
||||
google: faGoogle,
|
||||
};
|
||||
|
||||
export const uaIcons: { [key: string]: any; } = {
|
||||
// browser
|
||||
'Chrome': faChrome,
|
||||
'Chromium': faChrome,
|
||||
'IE': faInternetExplorer,
|
||||
'Edge': faEdge,
|
||||
'Android Browser': faAndroid,
|
||||
'Firefox': faFirefox,
|
||||
'Safari': faSafari,
|
||||
'Mobile Safari': faSafari,
|
||||
'Opera': faOpera,
|
||||
'Opera Mini': faOpera,
|
||||
'Amigo': faAmilia,
|
||||
'YaBrowser': faYandexInternational,
|
||||
// os
|
||||
'Windows': faWindows,
|
||||
'Windows Phone': faWindows,
|
||||
'Android': faAndroid,
|
||||
'iOS': faApple,
|
||||
'Mac OS': faApple,
|
||||
'Arch': faLinux,
|
||||
'CentOS': faLinux,
|
||||
'Fedora': faLinux,
|
||||
'FreeBSD': faLinux,
|
||||
'OpenBSD': faLinux,
|
||||
'Debian': faLinux,
|
||||
'Ubuntu': faLinux,
|
||||
'Linux': faLinux,
|
||||
'Chromium OS': faChrome,
|
||||
'Firefox OS': faFirefox,
|
||||
'Playstation': faGamepad,
|
||||
'Nintendo': faGamepad,
|
||||
// device
|
||||
'console': faGamepad,
|
||||
'mobile': faMobile,
|
||||
'tablet': faTablet,
|
||||
'smarttv': faTv,
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
import { GAMEPAD_MAPPINGS, GamepadAxes, GamepadButtons, GamepadMapping } from '../../generated/gamepad-mappings';
|
||||
import { Key, InputController } from './input';
|
||||
import { InputManager } from './inputManager';
|
||||
import { isFocused } from '../clientUtils';
|
||||
|
||||
interface GamepadInstance {
|
||||
gamepad: Gamepad;
|
||||
mapping: GamepadMapping;
|
||||
}
|
||||
|
||||
const JOYSTICK_THRESHHOLD = 0.2;
|
||||
|
||||
function createGamepad(gamepad: Gamepad): GamepadInstance {
|
||||
const mapping = detectMapping(gamepad.id, navigator.userAgent);
|
||||
return { gamepad, mapping };
|
||||
}
|
||||
|
||||
function isCompatible(mapping: any, id: string, browser: string) {
|
||||
for (let i = 0; i < mapping.supported.length; i++) {
|
||||
const supported = mapping.supported[i];
|
||||
|
||||
if (id.indexOf(supported.id) !== -1 && browser.indexOf(supported.os) !== -1 && browser.indexOf(browser) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function detectMapping(id: string, browser: string) {
|
||||
for (let i = 0; i < GAMEPAD_MAPPINGS.length; i++) {
|
||||
if (isCompatible(GAMEPAD_MAPPINGS[i], id, browser)) {
|
||||
return GAMEPAD_MAPPINGS[i];
|
||||
}
|
||||
}
|
||||
|
||||
return GAMEPAD_MAPPINGS[0];
|
||||
}
|
||||
|
||||
function axis({ mapping, gamepad }: GamepadInstance, name: GamepadAxes) {
|
||||
const axe = mapping.axes[name] as any;
|
||||
return axe ? gamepad.axes[axe.index] : 0;
|
||||
}
|
||||
function button({ mapping, gamepad }: GamepadInstance, name: GamepadButtons) {
|
||||
const button = mapping.buttons[name] as any;
|
||||
|
||||
if (!button) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (button.index !== undefined) {
|
||||
return gamepad.buttons[button.index] && gamepad.buttons[button.index].pressed;
|
||||
}
|
||||
|
||||
if (button.axis !== undefined) {
|
||||
if (button.direction < 0) {
|
||||
return gamepad.axes[button.axis] < -0.75;
|
||||
} else {
|
||||
return gamepad.axes[button.axis] > 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export class GamePadController implements InputController {
|
||||
private initialized = false;
|
||||
private gamepadIndex = -1;
|
||||
private zeroed1 = false;
|
||||
private zeroed2 = false;
|
||||
constructor(private manager: InputManager) {
|
||||
}
|
||||
initialize() {
|
||||
if (!this.initialized) {
|
||||
this.initialized = true;
|
||||
window.addEventListener('gamepadconnected', this.gamepadconnected);
|
||||
window.addEventListener('gamepaddisconnected', this.gamepaddisconnected);
|
||||
this.scanGamepads();
|
||||
}
|
||||
}
|
||||
release() {
|
||||
this.initialized = false;
|
||||
window.removeEventListener('gamepadconnected', this.gamepadconnected);
|
||||
window.removeEventListener('gamepaddisconnected', this.gamepaddisconnected);
|
||||
}
|
||||
update() {
|
||||
if (this.manager.disabledGamepad || !isFocused() || this.gamepadIndex === -1)
|
||||
return;
|
||||
|
||||
const gamepads = navigator.getGamepads();
|
||||
const gamepad = gamepads[this.gamepadIndex];
|
||||
|
||||
if (!gamepad) {
|
||||
this.scanGamepads();
|
||||
return;
|
||||
}
|
||||
|
||||
const pad = createGamepad(gamepad);
|
||||
|
||||
this.zeroed1 = readAxis(
|
||||
this.manager, Key.GAMEPAD_AXIS1_X, Key.GAMEPAD_AXIS1_Y,
|
||||
axis(pad, GamepadAxes.LeftStickX), axis(pad, GamepadAxes.LeftStickY), this.zeroed1);
|
||||
this.zeroed2 = readAxis(
|
||||
this.manager, Key.GAMEPAD_AXIS2_X, Key.GAMEPAD_AXIS2_Y,
|
||||
axis(pad, GamepadAxes.RightStickX), axis(pad, GamepadAxes.RightStickY), this.zeroed2);
|
||||
|
||||
this.manager.setValue(Key.GAMEPAD_BUTTON_X, button(pad, GamepadButtons.X) ? 1 : 0);
|
||||
this.manager.setValue(Key.GAMEPAD_BUTTON_Y, button(pad, GamepadButtons.Y) ? 1 : 0);
|
||||
this.manager.setValue(Key.GAMEPAD_BUTTON_A, button(pad, GamepadButtons.A) ? 1 : 0);
|
||||
this.manager.setValue(Key.GAMEPAD_BUTTON_B, button(pad, GamepadButtons.B) ? 1 : 0);
|
||||
|
||||
this.manager.setValue(Key.GAMEPAD_BUTTON_DOWN, button(pad, GamepadButtons.DpadDown) ? 1 : 0);
|
||||
this.manager.setValue(Key.GAMEPAD_BUTTON_LEFT, button(pad, GamepadButtons.DpadLeft) ? 1 : 0);
|
||||
this.manager.setValue(Key.GAMEPAD_BUTTON_RIGHT, button(pad, GamepadButtons.DpadRight) ? 1 : 0);
|
||||
this.manager.setValue(Key.GAMEPAD_BUTTON_UP, button(pad, GamepadButtons.DpadUp) ? 1 : 0);
|
||||
}
|
||||
clear() {
|
||||
}
|
||||
private scanGamepads() {
|
||||
const gamepads = navigator.getGamepads();
|
||||
|
||||
// Using regular loop because of issues with iterating over gamepads
|
||||
for (let i = 0; i < gamepads.length; i++) {
|
||||
const gamepad = gamepads[i];
|
||||
|
||||
if (gamepad) {
|
||||
this.gamepadIndex = gamepad.index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.gamepadIndex = -1;
|
||||
}
|
||||
private gamepadconnected = (e: Event) => {
|
||||
this.gamepadIndex = (e as GamepadEvent).gamepad.index;
|
||||
}
|
||||
private gamepaddisconnected = (e: Event) => {
|
||||
if (this.gamepadIndex === (e as GamepadEvent).gamepad.index) {
|
||||
this.scanGamepads();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readAxis(manager: InputManager, keyX: Key, keyY: Key, axisX: number, axisY: number, zeroed: boolean): boolean {
|
||||
const dist = Math.sqrt(axisX * axisX + axisY * axisY);
|
||||
|
||||
if (dist > JOYSTICK_THRESHHOLD) {
|
||||
const scaledDist = Math.min((dist - JOYSTICK_THRESHHOLD) / (1 - JOYSTICK_THRESHHOLD), 1);
|
||||
const theta = Math.atan2(axisY, axisX);
|
||||
manager.setValue(keyX, Math.cos(theta) * scaledDist);
|
||||
manager.setValue(keyY, Math.sin(theta) * scaledDist);
|
||||
return false;
|
||||
} else if (!zeroed) {
|
||||
manager.setValue(keyX, 0);
|
||||
manager.setValue(keyY, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return zeroed;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
export const enum Key {
|
||||
// Keyboard
|
||||
BACKSPACE = 8,
|
||||
TAB = 9,
|
||||
ENTER = 13,
|
||||
SHIFT = 16,
|
||||
CTRL = 17,
|
||||
ALT = 18,
|
||||
PAUSE = 19,
|
||||
CAPS_LOCK = 20,
|
||||
ESCAPE = 27,
|
||||
SPACE = 32,
|
||||
PAGE_UP = 33,
|
||||
PAGE_DOWN = 34,
|
||||
END = 35,
|
||||
HOME = 36,
|
||||
LEFT = 37,
|
||||
UP = 38,
|
||||
RIGHT = 39,
|
||||
DOWN = 40,
|
||||
INSERT = 45,
|
||||
DELETE = 46,
|
||||
KEY_0 = 48,
|
||||
KEY_1 = 49,
|
||||
KEY_2 = 50,
|
||||
KEY_3 = 51,
|
||||
KEY_4 = 52,
|
||||
KEY_5 = 53,
|
||||
KEY_6 = 54,
|
||||
KEY_7 = 55,
|
||||
KEY_8 = 56,
|
||||
KEY_9 = 57,
|
||||
KEY_A = 65,
|
||||
KEY_B = 66,
|
||||
KEY_C = 67,
|
||||
KEY_D = 68,
|
||||
KEY_E = 69,
|
||||
KEY_F = 70,
|
||||
KEY_G = 71,
|
||||
KEY_H = 72,
|
||||
KEY_I = 73,
|
||||
KEY_J = 74,
|
||||
KEY_K = 75,
|
||||
KEY_L = 76,
|
||||
KEY_M = 77,
|
||||
KEY_N = 78,
|
||||
KEY_O = 79,
|
||||
KEY_P = 80,
|
||||
KEY_Q = 81,
|
||||
KEY_R = 82,
|
||||
KEY_S = 83,
|
||||
KEY_T = 84,
|
||||
KEY_U = 85,
|
||||
KEY_V = 86,
|
||||
KEY_W = 87,
|
||||
KEY_X = 88,
|
||||
KEY_Y = 89,
|
||||
KEY_Z = 90,
|
||||
LEFT_META = 91,
|
||||
RIGHT_META = 92,
|
||||
SELECT = 93,
|
||||
NUMPAD_0 = 96,
|
||||
NUMPAD_1 = 97,
|
||||
NUMPAD_2 = 98,
|
||||
NUMPAD_3 = 99,
|
||||
NUMPAD_4 = 100,
|
||||
NUMPAD_5 = 101,
|
||||
NUMPAD_6 = 102,
|
||||
NUMPAD_7 = 103,
|
||||
NUMPAD_8 = 104,
|
||||
NUMPAD_9 = 105,
|
||||
MULTIPLY = 106,
|
||||
ADD = 107,
|
||||
SUBTRACT = 109,
|
||||
DECIMAL = 110,
|
||||
DIVIDE = 111,
|
||||
F1 = 112,
|
||||
F2 = 113,
|
||||
F3 = 114,
|
||||
F4 = 115,
|
||||
F5 = 116,
|
||||
F6 = 117,
|
||||
F7 = 118,
|
||||
F8 = 119,
|
||||
F9 = 120,
|
||||
F10 = 121,
|
||||
F11 = 122,
|
||||
F12 = 123,
|
||||
NUM_LOCK = 144,
|
||||
SCROLL_LOCK = 145,
|
||||
SEMICOLON = 186,
|
||||
EQUALS = 187,
|
||||
COMMA = 188,
|
||||
DASH = 189,
|
||||
PERIOD = 190,
|
||||
FORWARD_SLASH = 191,
|
||||
GRAVE_ACCENT = 192,
|
||||
OPEN_BRACKET = 219,
|
||||
BACK_SLASH = 220,
|
||||
CLOSE_BRACKET = 221,
|
||||
SINGLE_QUOTE = 222,
|
||||
// Mouse
|
||||
MOUSE_X = 300,
|
||||
MOUSE_Y,
|
||||
MOUSE_BUTTON1,
|
||||
MOUSE_BUTTON2,
|
||||
MOUSE_BUTTON3,
|
||||
MOUSE_WHEEL_X,
|
||||
MOUSE_WHEEL_Y,
|
||||
// Gamepad
|
||||
GAMEPAD_AXIS1_X,
|
||||
GAMEPAD_AXIS1_Y,
|
||||
GAMEPAD_AXIS2_X,
|
||||
GAMEPAD_AXIS2_Y,
|
||||
GAMEPAD_BUTTON_A,
|
||||
GAMEPAD_BUTTON_B,
|
||||
GAMEPAD_BUTTON_X,
|
||||
GAMEPAD_BUTTON_Y,
|
||||
GAMEPAD_BUTTON_L1,
|
||||
GAMEPAD_BUTTON_R1,
|
||||
GAMEPAD_BUTTON_L2,
|
||||
GAMEPAD_BUTTON_R2,
|
||||
GAMEPAD_BUTTON_START,
|
||||
GAMEPAD_BUTTON_SELECT,
|
||||
GAMEPAD_BUTTON_ANALOG1,
|
||||
GAMEPAD_BUTTON_ANALOG2,
|
||||
GAMEPAD_BUTTON_UP,
|
||||
GAMEPAD_BUTTON_DOWN,
|
||||
GAMEPAD_BUTTON_LEFT,
|
||||
GAMEPAD_BUTTON_RIGHT,
|
||||
// Touch
|
||||
TOUCH,
|
||||
TOUCH_CLICK,
|
||||
TOUCH_SECOND_CLICK,
|
||||
// Other
|
||||
MAX_VALUE,
|
||||
}
|
||||
|
||||
export interface InputController {
|
||||
initialize(element: HTMLElement): void;
|
||||
release(): void;
|
||||
update(): void;
|
||||
clear(): void;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { clamp } from 'lodash';
|
||||
import { KeyboardController } from './keyboard';
|
||||
import { MouseController } from './mouse';
|
||||
import { TouchController } from './touch';
|
||||
import { GamePadController } from './gamepad';
|
||||
import { InputController, Key } from './input';
|
||||
import { array, times } from '../../common/utils';
|
||||
|
||||
type Handler = (input: Key, value: number) => boolean | void;
|
||||
|
||||
const KEYS = Key.MAX_VALUE;
|
||||
|
||||
export class InputManager {
|
||||
disabledGamepad = false;
|
||||
disabledKeyboard = false;
|
||||
disableArrows = false;
|
||||
usingTouch = false;
|
||||
private state: number[];
|
||||
private prevState: number[];
|
||||
private actions: Handler[][];
|
||||
private controllers: InputController[] = [];
|
||||
constructor() {
|
||||
this.state = array(KEYS, 0);
|
||||
this.prevState = array(KEYS, 0);
|
||||
this.actions = times(KEYS, () => []);
|
||||
}
|
||||
get axisX() {
|
||||
const axisX = this.getRange(Key.GAMEPAD_AXIS1_X);
|
||||
const left = this.disableArrows ? this.getState(Key.KEY_A) : this.getState(Key.LEFT, Key.KEY_A);
|
||||
const right = this.disableArrows ? this.getState(Key.KEY_D) : this.getState(Key.RIGHT, Key.KEY_D);
|
||||
const x = axisX + (left ? -1 : (right ? 1 : 0));
|
||||
return clamp(x, -1, 1);
|
||||
}
|
||||
get axisY() {
|
||||
const axisY = this.getRange(Key.GAMEPAD_AXIS1_Y);
|
||||
const up = this.disableArrows ? this.getState(Key.KEY_W) : this.getState(Key.UP, Key.KEY_W);
|
||||
const down = this.disableArrows ? this.getState(Key.KEY_S) : this.getState(Key.DOWN, Key.KEY_S);
|
||||
const y = axisY + (up ? -1 : (down ? 1 : 0));
|
||||
return clamp(y, -1, 1);
|
||||
}
|
||||
get isMovementFromButtons() {
|
||||
const up = this.getState(Key.UP, Key.KEY_W);
|
||||
const down = this.getState(Key.DOWN, Key.KEY_S);
|
||||
const left = this.getState(Key.LEFT, Key.KEY_A);
|
||||
const right = this.getState(Key.RIGHT, Key.KEY_D);
|
||||
return up || down || left || right;
|
||||
}
|
||||
get axis2X() {
|
||||
return clamp(this.getRange(Key.GAMEPAD_AXIS2_X), -1, 1);
|
||||
}
|
||||
get axis2Y() {
|
||||
return clamp(this.getRange(Key.GAMEPAD_AXIS2_Y), -1, 1);
|
||||
}
|
||||
get pointerX() {
|
||||
return this.getRange(Key.MOUSE_X);
|
||||
}
|
||||
get pointerY() {
|
||||
return this.getRange(Key.MOUSE_Y);
|
||||
}
|
||||
get wheelX() {
|
||||
return this.getRange(Key.MOUSE_WHEEL_X);
|
||||
}
|
||||
get wheelY() {
|
||||
return this.getRange(Key.MOUSE_WHEEL_Y);
|
||||
}
|
||||
initialize(element: HTMLElement) {
|
||||
this.controllers = [
|
||||
new KeyboardController(this),
|
||||
new MouseController(this),
|
||||
new TouchController(this),
|
||||
new GamePadController(this),
|
||||
];
|
||||
|
||||
this.controllers.forEach(c => c.initialize(element));
|
||||
this.clear();
|
||||
}
|
||||
release() {
|
||||
this.controllers.forEach(c => c.release());
|
||||
this.controllers = [];
|
||||
this.clear();
|
||||
}
|
||||
update() {
|
||||
for (const controller of this.controllers) {
|
||||
controller.update();
|
||||
}
|
||||
}
|
||||
end() {
|
||||
for (let i = 0; i < KEYS; i++) {
|
||||
this.prevState[i] = this.state[i];
|
||||
}
|
||||
|
||||
this.setValue(Key.TOUCH_CLICK, 0);
|
||||
this.setValue(Key.TOUCH_SECOND_CLICK, 0);
|
||||
this.setValue(Key.MOUSE_WHEEL_X, 0);
|
||||
this.setValue(Key.MOUSE_WHEEL_Y, 0);
|
||||
}
|
||||
clear() {
|
||||
for (let i = 0; i < KEYS; i++) {
|
||||
this.state[i] = 0;
|
||||
this.prevState[i] = 0;
|
||||
}
|
||||
|
||||
for (const controller of this.controllers) {
|
||||
controller.clear();
|
||||
}
|
||||
}
|
||||
onPressed(inputs: Key[] | Key, handler: () => void) {
|
||||
this.onAction(inputs, (_, v) => {
|
||||
if (v === 1) {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
}
|
||||
onReleased(inputs: Key[] | Key, handler: () => void) {
|
||||
this.onAction(inputs, (_, v) => {
|
||||
if (v === 0) {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
}
|
||||
isPressed(key: Key) {
|
||||
return this.state[key] !== 0;
|
||||
}
|
||||
wasPressed(key: Key): boolean {
|
||||
return this.state[key] === 1 && this.prevState[key] === 0;
|
||||
}
|
||||
private onAction(inputs: Key[] | Key, handler: Handler) {
|
||||
const inputsArray = Array.isArray(inputs) ? inputs : [inputs];
|
||||
|
||||
for (const i of inputsArray) {
|
||||
this.actions[i].push(handler);
|
||||
}
|
||||
}
|
||||
private getState(...inputs: Key[]): boolean {
|
||||
for (const i of inputs) {
|
||||
if (this.state[i] !== 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
private getRange(input: Key): number {
|
||||
return this.state[input];
|
||||
}
|
||||
setValue(input: Key, value: number): boolean {
|
||||
if (input < 0 || input >= KEYS) {
|
||||
console.warn(`Input out of range: ${input}`);
|
||||
} else if (this.state[input] !== value) {
|
||||
this.state[input] = value;
|
||||
|
||||
if (this.actions[input] && this.actions[input].length) {
|
||||
for (const action of this.actions[input]) {
|
||||
action(input, value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
addValue(input: Key, value: number) {
|
||||
if (input < 0 || input >= KEYS) {
|
||||
console.warn(`Input out of range: ${input}`);
|
||||
} else {
|
||||
this.state[input] += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { InputController, Key } from './input';
|
||||
import { InputManager } from './inputManager';
|
||||
import { removeItem, includes } from '../../common/utils';
|
||||
|
||||
const firefox = !SERVER && /firefox/i.test(navigator.userAgent);
|
||||
|
||||
function isKeyEventInvalid(e: KeyboardEvent) {
|
||||
return e.target && /^(input|textarea|select)$/i.test((e.target as HTMLElement).tagName);
|
||||
}
|
||||
|
||||
function allowKey(key: number) {
|
||||
return key === Key.ESCAPE || key === Key.F5 || key === Key.F12 || key === Key.F11 || key === Key.TAB;
|
||||
}
|
||||
|
||||
function fixKeyCode(key: number) {
|
||||
if (firefox) {
|
||||
if (key === 173) return Key.DASH;
|
||||
if (key === 61) return Key.EQUALS;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
const iosKeyToKeyCode: { [key: string]: number | undefined; } = {
|
||||
UIKeyInputEscape: Key.ESCAPE,
|
||||
UIKeyInputUpArrow: Key.UP,
|
||||
UIKeyInputLeftArrow: Key.LEFT,
|
||||
UIKeyInputRightArrow: Key.RIGHT,
|
||||
UIKeyInputDownArrow: Key.DOWN,
|
||||
};
|
||||
|
||||
const iosHandledKeyCodes = [Key.ESCAPE, Key.UP, Key.LEFT, Key.RIGHT, Key.DOWN];
|
||||
|
||||
export class KeyboardController implements InputController {
|
||||
private initialized = false;
|
||||
private stack: number[] = [];
|
||||
constructor(private manager: InputManager) {
|
||||
}
|
||||
initialize() {
|
||||
if (!this.initialized) {
|
||||
this.initialized = true;
|
||||
window.addEventListener('keydown', this.keydown);
|
||||
window.addEventListener('keyup', this.keyup);
|
||||
window.addEventListener('blur', this.blur);
|
||||
}
|
||||
}
|
||||
release() {
|
||||
this.initialized = false;
|
||||
window.removeEventListener('keydown', this.keydown);
|
||||
window.removeEventListener('keyup', this.keyup);
|
||||
window.removeEventListener('blur', this.blur);
|
||||
this.clear();
|
||||
}
|
||||
update() {
|
||||
}
|
||||
clear() {
|
||||
this.stack.length = 0;
|
||||
}
|
||||
private keydown = (e: KeyboardEvent) => {
|
||||
if (!this.manager.disabledKeyboard && !isKeyEventInvalid(e)) {
|
||||
const code = fixKeyCode(e.keyCode);
|
||||
this.manager.setValue(code, 1);
|
||||
|
||||
if (!allowKey(code)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
if (!includes(this.stack, code) && !includes(iosHandledKeyCodes, code)) {
|
||||
this.stack.push(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
private keyup = (e: KeyboardEvent) => {
|
||||
let code = fixKeyCode(e.keyCode);
|
||||
|
||||
// fix keyCode on iOS bluetooth keyboard
|
||||
if (code === 0) {
|
||||
code = iosKeyToKeyCode[e.key] || 0;
|
||||
|
||||
if (code === 0) {
|
||||
code = this.stack.pop() || 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.manager.setValue(code, 0)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
removeItem(this.stack, code);
|
||||
}
|
||||
private blur = () => {
|
||||
this.manager.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Key, InputController } from './input';
|
||||
import { InputManager } from './inputManager';
|
||||
import { clamp } from '../../common/utils';
|
||||
|
||||
const MOUSE_BUTTONS = [Key.MOUSE_BUTTON1, Key.MOUSE_BUTTON3, Key.MOUSE_BUTTON2];
|
||||
|
||||
export class MouseController implements InputController {
|
||||
private initialized = false;
|
||||
private element?: HTMLElement;
|
||||
constructor(private manager: InputManager) {
|
||||
}
|
||||
initialize(element: HTMLElement) {
|
||||
if (!this.initialized) {
|
||||
this.initialized = true;
|
||||
this.element = element;
|
||||
element.addEventListener('mousemove', this.mousemove);
|
||||
element.addEventListener('mousedown', this.mousedown);
|
||||
element.addEventListener('mouseup', this.mouseup);
|
||||
element.addEventListener('mousewheel', this.mousewheel);
|
||||
element.addEventListener('contextmenu', this.contextmenu);
|
||||
element.addEventListener('click', this.click);
|
||||
window.addEventListener('blur', this.blur);
|
||||
}
|
||||
}
|
||||
release() {
|
||||
this.initialized = false;
|
||||
|
||||
if (this.element) {
|
||||
this.element.removeEventListener('mousemove', this.mousemove);
|
||||
this.element.removeEventListener('mousedown', this.mousedown);
|
||||
this.element.removeEventListener('mouseup', this.mouseup);
|
||||
this.element.removeEventListener('mousewheel', this.mousewheel);
|
||||
this.element.removeEventListener('contextmenu', this.contextmenu);
|
||||
this.element.removeEventListener('click', this.click);
|
||||
this.element = undefined;
|
||||
}
|
||||
|
||||
window.removeEventListener('blur', this.blur);
|
||||
}
|
||||
update() {
|
||||
}
|
||||
clear() {
|
||||
}
|
||||
private mousemove = (e: MouseEvent) => {
|
||||
if (this.element) {
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
this.manager.setValue(Key.MOUSE_X, Math.floor(e.clientX - rect.left));
|
||||
this.manager.setValue(Key.MOUSE_Y, Math.floor(e.clientY - rect.top));
|
||||
}
|
||||
}
|
||||
private mousedown = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.manager.usingTouch = false;
|
||||
|
||||
const button = MOUSE_BUTTONS[e.button];
|
||||
|
||||
if (button) {
|
||||
this.manager.setValue(button, 1);
|
||||
}
|
||||
}
|
||||
private mouseup = (e: MouseEvent) => {
|
||||
const button = MOUSE_BUTTONS[e.button];
|
||||
|
||||
if (button) {
|
||||
this.manager.setValue(button, 0);
|
||||
}
|
||||
}
|
||||
private mousewheel: any = (e: MouseWheelEvent) => {
|
||||
this.manager.addValue(Key.MOUSE_WHEEL_X, clamp(e.deltaX, -1, 1));
|
||||
this.manager.addValue(Key.MOUSE_WHEEL_Y, clamp(e.deltaY, -1, 1));
|
||||
}
|
||||
private contextmenu = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
private click = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
private blur = () => {
|
||||
for (const button of MOUSE_BUTTONS) {
|
||||
this.manager.setValue(button, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { Key, InputController } from './input';
|
||||
import { Point } from '../../common/interfaces';
|
||||
import { setTransform } from '../../common/utils';
|
||||
import { InputManager } from './inputManager';
|
||||
|
||||
function getTouch(e: TouchEvent, id: number) {
|
||||
if (id !== -1) {
|
||||
for (let i = 0; i < e.changedTouches.length; ++i) {
|
||||
const touch = e.changedTouches.item(i);
|
||||
|
||||
if (touch && touch.identifier === id) {
|
||||
return touch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const TOUCH_DEADZONE = 15;
|
||||
const TOUCH_MAX = 100;
|
||||
|
||||
export class TouchController implements InputController {
|
||||
private initialized = false;
|
||||
private touchId = -1;
|
||||
private touch2Id = -1;
|
||||
private touchStart: Point = { x: 0, y: 0 };
|
||||
private touchCurrent: Point = { x: 0, y: 0 };
|
||||
private touchIsDrag = false;
|
||||
private tapInvalidated = false;
|
||||
private origin?: HTMLElement;
|
||||
private position?: HTMLElement;
|
||||
private originShown = false;
|
||||
private originTransform?: string;
|
||||
private positionShown = false;
|
||||
private positionTransform?: string;
|
||||
private element?: HTMLElement;
|
||||
constructor(private manager: InputManager) {
|
||||
}
|
||||
initialize(element: HTMLElement) {
|
||||
if (!this.initialized) {
|
||||
this.initialized = true;
|
||||
this.element = element;
|
||||
this.origin = document.getElementById('touch-origin')!;
|
||||
this.position = document.getElementById('touch-position')!;
|
||||
|
||||
element.addEventListener('touchstart', this.touchstart);
|
||||
element.addEventListener('touchmove', this.touchmove);
|
||||
element.addEventListener('touchend', this.touchend);
|
||||
window.addEventListener('touchend', this.blur);
|
||||
window.addEventListener('blur', this.blur);
|
||||
}
|
||||
}
|
||||
release() {
|
||||
this.initialized = false;
|
||||
|
||||
if (this.element) {
|
||||
this.element.removeEventListener('touchstart', this.touchstart);
|
||||
this.element.removeEventListener('touchmove', this.touchmove);
|
||||
this.element.removeEventListener('touchend', this.touchend);
|
||||
this.element = undefined;
|
||||
}
|
||||
|
||||
window.removeEventListener('touchend', this.blur);
|
||||
window.removeEventListener('blur', this.blur);
|
||||
}
|
||||
update() {
|
||||
const showOrigin = this.touchIsDrag && this.touchId !== -1;
|
||||
const showPosition = this.touchId !== -1;
|
||||
|
||||
if (this.origin && this.position) {
|
||||
if (this.originShown !== showOrigin) {
|
||||
this.originShown = showOrigin;
|
||||
this.origin.style.display = showOrigin ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (this.positionShown !== showPosition) {
|
||||
this.positionShown = showPosition;
|
||||
this.position.style.display = showPosition ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (showOrigin) {
|
||||
const transform = `translate3d(${this.touchStart.x - 50}px, ${this.touchStart.y - 50}px, 0px)`;
|
||||
|
||||
if (this.originTransform !== transform) {
|
||||
this.originTransform = transform;
|
||||
setTransform(this.origin, transform);
|
||||
}
|
||||
}
|
||||
|
||||
if (showPosition) {
|
||||
const transform = `translate3d(${this.touchCurrent.x - 25}px, ${this.touchCurrent.y - 25}px, 0px)`;
|
||||
|
||||
if (this.positionTransform !== transform) {
|
||||
this.positionTransform = transform;
|
||||
setTransform(this.position, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clear() {
|
||||
}
|
||||
private reset() {
|
||||
this.touch2Id = -1;
|
||||
this.resetTouch();
|
||||
}
|
||||
private resetTouch() {
|
||||
this.touchId = -1;
|
||||
this.touchStart = this.touchCurrent = { x: 0, y: 0 };
|
||||
this.touchIsDrag = false;
|
||||
this.manager.setValue(Key.TOUCH, 0);
|
||||
this.updateInput();
|
||||
}
|
||||
private updateInput() {
|
||||
const dy = this.touchStart.y - this.touchCurrent.y;
|
||||
const dx = this.touchStart.x - this.touchCurrent.x;
|
||||
const theta = Math.atan2(dy, dx);
|
||||
const dist = Math.sqrt(dy * dy + dx * dx);
|
||||
|
||||
if (dist > TOUCH_DEADZONE) {
|
||||
const scaledDist = Math.min((dist - TOUCH_DEADZONE) / (TOUCH_MAX - TOUCH_DEADZONE), 1);
|
||||
this.touchIsDrag = true;
|
||||
this.manager.setValue(Key.GAMEPAD_AXIS1_X, -Math.cos(theta) * scaledDist);
|
||||
this.manager.setValue(Key.GAMEPAD_AXIS1_Y, -Math.sin(theta) * scaledDist);
|
||||
} else {
|
||||
this.manager.setValue(Key.GAMEPAD_AXIS1_X, 0);
|
||||
this.manager.setValue(Key.GAMEPAD_AXIS1_Y, 0);
|
||||
}
|
||||
}
|
||||
private touchstart = (e: any) => {
|
||||
e.cancellable && e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.manager.usingTouch = true;
|
||||
|
||||
if (this.touchId === -1) {
|
||||
const touch = e.changedTouches.item(0);
|
||||
|
||||
if (touch) {
|
||||
this.tapInvalidated = false;
|
||||
this.touchId = touch.identifier;
|
||||
this.touchStart = this.touchCurrent = this.getTouchXY(touch);
|
||||
this.manager.setValue(Key.MOUSE_X, this.touchStart.x);
|
||||
this.manager.setValue(Key.MOUSE_Y, this.touchStart.y);
|
||||
this.manager.setValue(Key.TOUCH, 1);
|
||||
}
|
||||
} else if (this.touch2Id === -1) {
|
||||
const touch = e.changedTouches.item(0);
|
||||
|
||||
if (touch) {
|
||||
this.tapInvalidated = true;
|
||||
this.touch2Id = touch.identifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
private touchmove = (e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const touch = getTouch(e, this.touchId);
|
||||
|
||||
if (touch) {
|
||||
this.touchCurrent = this.getTouchXY(touch);
|
||||
this.manager.setValue(Key.MOUSE_X, this.touchCurrent.x);
|
||||
this.manager.setValue(Key.MOUSE_Y, this.touchCurrent.y);
|
||||
this.updateInput();
|
||||
}
|
||||
}
|
||||
private touchend = (e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const touch = getTouch(e, this.touchId);
|
||||
|
||||
if (touch) {
|
||||
if (!this.touchIsDrag && !this.tapInvalidated) {
|
||||
this.manager.setValue(Key.MOUSE_X, this.touchStart.x);
|
||||
this.manager.setValue(Key.MOUSE_Y, this.touchStart.y);
|
||||
this.manager.setValue(Key.TOUCH_CLICK, 1);
|
||||
}
|
||||
|
||||
this.resetTouch();
|
||||
}
|
||||
|
||||
const touch2 = getTouch(e, this.touch2Id);
|
||||
|
||||
if (touch2) {
|
||||
this.manager.setValue(Key.TOUCH_SECOND_CLICK, 1);
|
||||
this.touch2Id = -1;
|
||||
}
|
||||
}
|
||||
private blur = () => {
|
||||
this.reset();
|
||||
}
|
||||
private getTouchXY(touch: Touch) {
|
||||
const { left, top } = this.element!.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
x: touch.clientX - left,
|
||||
y: touch.clientY - top,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { remove } from 'lodash';
|
||||
import { PartyMember, PartyInfo, Pony } from '../common/interfaces';
|
||||
import { PonyTownGame } from './game';
|
||||
|
||||
export function updateParty(current: PartyInfo | undefined, info: PartyMember[] | undefined): PartyInfo | undefined {
|
||||
if (!info || !info.length) {
|
||||
return undefined;
|
||||
} else {
|
||||
const party = current || {
|
||||
leaderId: 0,
|
||||
members: [],
|
||||
};
|
||||
|
||||
remove(party.members, p => !info.some(m => p.id === m.id));
|
||||
|
||||
info.forEach(m => {
|
||||
const existing = party.members.find(x => m.id === x.id);
|
||||
|
||||
if (existing) {
|
||||
Object.assign(existing, m);
|
||||
} else {
|
||||
party.members.push(m);
|
||||
}
|
||||
|
||||
if (m.leader) {
|
||||
party.leaderId = m.id;
|
||||
}
|
||||
});
|
||||
|
||||
return party;
|
||||
}
|
||||
}
|
||||
|
||||
export function isPonyInParty(party: PartyInfo | undefined, pony: Pony, pending: boolean) {
|
||||
return !!party && party.members.some(m => m.pony === pony && (pending || !m.pending));
|
||||
}
|
||||
|
||||
export function isPartyLeader(game: PonyTownGame): boolean {
|
||||
return game.party !== undefined && game.player !== undefined && game.player.id === game.party.leaderId;
|
||||
}
|
||||
|
||||
export function isInParty(game: PonyTownGame): boolean {
|
||||
return game.party !== undefined && game.party.members.length > 0;
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { isCommand, processCommand, hasFlag, includes, point } from '../common/utils';
|
||||
import { canPonyLie, canPonyFlyUp, canPonyStand, canPonySit, doBoopPonyAction } from '../common/pony';
|
||||
import { PonyTownGame } from './game';
|
||||
import {
|
||||
setPonyState, canBoop, isPonyLying, isPonyFlying, isPonyStanding, isPonySitting, getInteractBounds,
|
||||
isFacingRight, closestEntity, entityInRange
|
||||
} from '../common/entityUtils';
|
||||
import { EntityState, Action, Pony, ChatType, EntityFlags, Point, TileType } from '../common/interfaces';
|
||||
import { FLY_DELAY } from '../common/constants';
|
||||
import { randomString } from '../common/stringUtils';
|
||||
import { pickEntitiesByRect, pickAnyEntities } from '../common/worldMap';
|
||||
import { centerPoint } from '../common/rect';
|
||||
import { pointToWorld, roundPositionX, roundPositionY } from '../common/positionUtils';
|
||||
import { hammer, shovel } from '../common/entities';
|
||||
|
||||
export function handleActionCommand(message: string, game: PonyTownGame): boolean {
|
||||
if (isCommand(message)) {
|
||||
const { command = '' } = processCommand(message);
|
||||
const player = game.player;
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
if (command === 'spammessages') {
|
||||
let i = 0;
|
||||
setInterval(() => game.send(server => server.say(0, randomString(5) + ` #${i++}`, ChatType.Say)), 100);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
switch (command.toLowerCase()) {
|
||||
case 'testerrorreporting':
|
||||
throw new Error('test error');
|
||||
case 'disablepixelratio':
|
||||
game.togglePixelRatio();
|
||||
return true;
|
||||
case 'lie':
|
||||
case 'lay':
|
||||
if (player) {
|
||||
if (isPonyLying(player)) {
|
||||
sitAction(player, game);
|
||||
} else {
|
||||
lieAction(player, game);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case 'sit':
|
||||
if (player) {
|
||||
if (isPonyFlying(player)) {
|
||||
standAction(player, game);
|
||||
} else {
|
||||
sitAction(player, game);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case 'stand':
|
||||
if (player) {
|
||||
standAction(player, game);
|
||||
}
|
||||
return true;
|
||||
case 'fly':
|
||||
if (player) {
|
||||
if (isPonyFlying(player)) {
|
||||
standAction(player, game);
|
||||
} else {
|
||||
flyAction(player, game);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function upAction(game: PonyTownGame) {
|
||||
const player = game.player;
|
||||
|
||||
if (player) {
|
||||
if (isPonyLying(player)) {
|
||||
sitAction(player, game);
|
||||
} else if (isPonySitting(player)) {
|
||||
standAction(player, game);
|
||||
} else if (isPonyStanding(player)) {
|
||||
flyAction(player, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function downAction(game: PonyTownGame) {
|
||||
const player = game.player;
|
||||
|
||||
if (player) {
|
||||
if (isPonySitting(player)) {
|
||||
lieAction(player, game);
|
||||
} else if (isPonyStanding(player)) {
|
||||
sitAction(player, game);
|
||||
} else if (isPonyFlying(player)) {
|
||||
standAction(player, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function sitAction(player: Pony, game: PonyTownGame) {
|
||||
if (canPonySit(player, game.map) && game.send(server => server.action(Action.Sit))) {
|
||||
player.state = setPonyState(player.state, EntityState.PonySitting);
|
||||
game.stateOverride = EntityState.PonySitting;
|
||||
game.onActionsUpdate.next();
|
||||
}
|
||||
}
|
||||
|
||||
export function standAction(player: Pony, game: PonyTownGame) {
|
||||
if (canPonyStand(player, game.map) && game.send(server => server.action(Action.Stand))) {
|
||||
player.state = setPonyState(player.state, EntityState.PonyStanding);
|
||||
game.stateOverride = EntityState.PonyStanding;
|
||||
game.onActionsUpdate.next();
|
||||
}
|
||||
}
|
||||
|
||||
export function lieAction(player: Pony, game: PonyTownGame) {
|
||||
if (canPonyLie(player, game.map) && game.send(server => server.action(Action.Lie))) {
|
||||
player.state = setPonyState(player.state, EntityState.PonyLying);
|
||||
game.stateOverride = EntityState.PonyLying;
|
||||
game.onActionsUpdate.next();
|
||||
}
|
||||
}
|
||||
|
||||
export function flyAction(player: Pony, game: PonyTownGame) {
|
||||
if (canPonyFlyUp(player) && game.send(server => server.action(Action.Fly))) {
|
||||
player.state = setPonyState(player.state, EntityState.PonyFlying);
|
||||
player.inTheAirDelay = FLY_DELAY;
|
||||
game.stateOverride = EntityState.PonyFlying;
|
||||
game.onActionsUpdate.next();
|
||||
}
|
||||
}
|
||||
|
||||
export function boopAction(game: PonyTownGame) {
|
||||
if (game.player && canBoop(game.player) && game.send(server => server.action(Action.Boop))) {
|
||||
doBoopPonyAction(game, game.player);
|
||||
}
|
||||
}
|
||||
|
||||
export function turnHeadAction(game: PonyTownGame) {
|
||||
if (game.player && game.send(server => server.action(Action.TurnHead))) {
|
||||
game.player.state = game.player.state ^ EntityState.HeadTurned;
|
||||
game.headTurnedOverride = hasFlag(game.player.state, EntityState.HeadTurned);
|
||||
game.onActionsUpdate.next();
|
||||
}
|
||||
}
|
||||
|
||||
export function interact(game: PonyTownGame, shift: boolean) {
|
||||
const player = game.player;
|
||||
|
||||
if (player) {
|
||||
const bounds = getInteractBounds(player);
|
||||
const entities = pickEntitiesByRect(game.map, bounds, true, false);
|
||||
const center = centerPoint(bounds);
|
||||
center.x += (bounds.w / 4) * (isFacingRight(player) ? -1 : 1);
|
||||
const entity = closestEntity(pointToWorld(center), entities);
|
||||
|
||||
if (entity && entityInRange(entity, player)) {
|
||||
game.send(server => server.interact(entity.id));
|
||||
} else if (player.hold === hammer.type) {
|
||||
game.changePlaceEntity(shift);
|
||||
} else if (player.hold === shovel.type) {
|
||||
game.changePlaceTile(shift);
|
||||
} else if (player.ponyState.holding && hasFlag(player.ponyState.holding.flags, EntityFlags.Usable)) {
|
||||
game.send(server => server.use());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleWall(game: PonyTownGame, hover: Point) {
|
||||
const x = hover.x | 0;
|
||||
const y = hover.y | 0;
|
||||
const dx = hover.x - x;
|
||||
const dy = hover.y - y;
|
||||
|
||||
if (dx > dy) {
|
||||
if ((dx + dy) < 1) {
|
||||
game.send(server => server.changeTile(x, y, TileType.WallH));
|
||||
} else {
|
||||
game.send(server => server.changeTile(x + 1, y, TileType.WallV));
|
||||
}
|
||||
} else {
|
||||
if ((dx + dy) < 1) {
|
||||
game.send(server => server.changeTile(x, y, TileType.WallV));
|
||||
} else {
|
||||
game.send(server => server.changeTile(x, y + 1, TileType.WallH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function editorSelectEntities(game: PonyTownGame, hover: Point, shift: boolean) {
|
||||
game.apply(() => {
|
||||
const entities = pickAnyEntities(game.map, hover);
|
||||
|
||||
if (shift) {
|
||||
const entity = entities.filter(e => !includes(game.editor.selectedEntities, e))[0];
|
||||
entity && game.editor.selectedEntities.push(entity);
|
||||
} else {
|
||||
const index = entities.findIndex(e => includes(game.editor.selectedEntities, e));
|
||||
const entity = entities[(index + 1) % entities.length];
|
||||
game.editor.selectedEntities = entity ? [entity] : [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function editorDragEntities(game: PonyTownGame, hover: Point, buttonPressed: boolean) {
|
||||
if (buttonPressed) {
|
||||
const dx = hover.x - game.editor.draggingStart.x;
|
||||
const dy = hover.y - game.editor.draggingStart.y;
|
||||
|
||||
game.editor.selectedEntities.forEach(e => {
|
||||
e.x = roundPositionX(e.draggingStart!.x + dx);
|
||||
e.y = roundPositionY(e.draggingStart!.y + dy);
|
||||
});
|
||||
} else {
|
||||
game.apply(() => game.editor.draggingEntities = false);
|
||||
game.send(server => server.editorAction({
|
||||
type: 'move',
|
||||
entities: game.editor.selectedEntities.map(({ id, x, y }) => ({ id, x, y })),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export function editorMoveEntities(game: PonyTownGame, hover: Point) {
|
||||
game.editor.draggingEntities = true;
|
||||
game.editor.draggingStart = hover;
|
||||
game.editor.selectedEntities.forEach(e => e.draggingStart = point(e.x, e.y));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// <reference path="../../typings/my.d.ts" />
|
||||
|
||||
// Safari <= 8.4, Android
|
||||
try {
|
||||
if (!('performance' in window && 'now' in performance)) {
|
||||
(window as any).performance = Date;
|
||||
}
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
if (!('getGamepads' in navigator)) {
|
||||
(window.navigator as any).getGamepads = () => [];
|
||||
}
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
if (!('requestAnimationFrame' in window)) {
|
||||
(window as any).requestAnimationFrame = (callback: any) => setTimeout(() => callback(performance.now()), 1000 / 60) as any;
|
||||
}
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
if (!('cancelAnimationFrame' in window)) {
|
||||
(window as any).cancelAnimationFrame = clearTimeout;
|
||||
}
|
||||
} catch { }
|
||||
|
||||
// IE <= 10
|
||||
try {
|
||||
if (!('devicePixelRatio' in window)) {
|
||||
(window as any).devicePixelRatio = 1;
|
||||
}
|
||||
} catch { }
|
||||
@@ -0,0 +1,547 @@
|
||||
import { BodyAnimation, BodyAnimationFrame, HeadAnimation, HeadAnimationFrame, BodyShadow } from '../common/interfaces';
|
||||
import { repeat, flatten } from '../common/utils';
|
||||
|
||||
// body animations
|
||||
|
||||
export function createBodyFrame([
|
||||
body = 0, head = 0, wing = 0, tail = 0,
|
||||
frontLeg = 0, frontFarLeg = 0, backLeg = 0, backFarLeg = 0,
|
||||
bodyX = 0, bodyY = 0, headX = 0, headY = 0,
|
||||
frontLegX = 0, frontLegY = 0, frontFarLegX = 0, frontFarLegY = 0,
|
||||
backLegX = 0, backLegY = 0, backFarLegX = 0, backFarLegY = 0
|
||||
]: number[]): Readonly<BodyAnimationFrame> {
|
||||
return {
|
||||
body, head, wing, tail,
|
||||
frontLeg, frontFarLeg, backLeg, backFarLeg,
|
||||
bodyX, bodyY, headX, headY,
|
||||
frontLegX, frontLegY, frontFarLegX, frontFarLegY,
|
||||
backLegX, backLegY, backFarLegX, backFarLegY
|
||||
};
|
||||
}
|
||||
|
||||
export function createBodyAnimation(
|
||||
name: string, fps: number, loop: boolean, frames: number[][], shadowOffsets?: number[][]
|
||||
): Readonly<BodyAnimation> {
|
||||
if (shadowOffsets && shadowOffsets.length !== frames.length) {
|
||||
throw new Error(`Incorrect frame count for shadowOffsets for ${name}`);
|
||||
}
|
||||
|
||||
const shadow = shadowOffsets && shadowOffsets.map<BodyShadow>(([frame, offset]) => ({ frame, offset }));
|
||||
|
||||
return { name, loop, fps, frames: frames.map(createBodyFrame), shadow };
|
||||
}
|
||||
|
||||
export const stand = createBodyAnimation('stand', 24, true, [
|
||||
[1, 1, 0, 0, 1, 1, 1, 1],
|
||||
]);
|
||||
|
||||
export const swim = createBodyAnimation('swim', 4, true, [
|
||||
[1, 1, 0, 0, 8, 10, 6, 5, 0, 14],
|
||||
[1, 1, 0, 0, 8, 10, 6, 5, 0, 13],
|
||||
[1, 1, 0, 0, 8, 10, 6, 5, 0, 12],
|
||||
[1, 1, 0, 0, 8, 10, 6, 5, 0, 13]
|
||||
]);
|
||||
|
||||
export const trotToSwim = createBodyAnimation('trot-to-swim', 24, false, [
|
||||
[1, 1, 0, 0, 8, 10, 6, 5, 0, 2],
|
||||
[1, 1, 0, 0, 8, 10, 6, 5, 0, 8],
|
||||
[1, 1, 0, 0, 8, 10, 6, 5, 0, 10],
|
||||
[1, 1, 0, 0, 8, 10, 6, 5, 0, 16]
|
||||
]);
|
||||
|
||||
export const swimToTrot = createBodyAnimation('swim-to-trot', 24, false, [
|
||||
[1, 1, 0, 0, 8, 10, 6, 5, 0, 12],
|
||||
[1, 1, 0, 0, 12, 3, 4, 23, 0, 8],
|
||||
[1, 1, 0, 0, 14, 26, 3, 24, 0, 4],
|
||||
[1, 1, 0, 0, 18, 27, 2, 5]
|
||||
]);
|
||||
|
||||
export const flyToSwim = createBodyAnimation('fly-to-swim', 16, false, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -12],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -8],
|
||||
[1, 1, 6, 0, 8, 10, 6, 5, 0, -2],
|
||||
[1, 1, 7, 0, 8, 10, 6, 5, 0, 4],
|
||||
[1, 1, 11, 0, 8, 10, 6, 5, 0, 10],
|
||||
[1, 1, 1, 0, 8, 10, 6, 5, 0, 14]
|
||||
]);
|
||||
|
||||
export const flyToSwimBug = createBodyAnimation('fly-to-swim-bug', 16, false, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -12],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -8],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -2],
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, 4],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, 10],
|
||||
[1, 1, 1, 0, 8, 10, 6, 5, 0, 14]
|
||||
]);
|
||||
|
||||
export const swimToFly = createBodyAnimation('swim-to-fly', 16, false, [
|
||||
[1, 1, 11, 0, 8, 10, 6, 5, 0, 13],
|
||||
[1, 1, 12, 0, 8, 10, 6, 5, 0, 14, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1],
|
||||
[2, 1, 3, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[2, 1, 4, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[2, 1, 5, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[1, 1, 6, 0, 8, 10, 6, 1, 0, 13],
|
||||
[1, 1, 7, 0, 6, 7, 5, 5, 0, -7, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
|
||||
[1, 1, 8, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 9, 0, 8, 10, 6, 5, 0, -17],
|
||||
[1, 1, 10, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 11, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 12, 0, 8, 10, 6, 5, 0, -17]
|
||||
]);
|
||||
|
||||
export const swimToFlyBug = createBodyAnimation('swim-to-fly-bug', 16, false, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, 13],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, 14, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1],
|
||||
[2, 1, 5, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[2, 1, 4, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[2, 1, 5, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[1, 1, 4, 0, 8, 10, 6, 1, 0, 13],
|
||||
[1, 1, 5, 0, 6, 7, 5, 5, 0, -7, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -17],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17]
|
||||
]);
|
||||
|
||||
//const trotSkew = [-1, 0, 1, 0, -1, -2, -3, -2, -1, 0, 1, 0, -1, -2, -3, -2].map(x => (x + 2) * 0.25);
|
||||
|
||||
export const trot = createBodyAnimation('trot', 24, true, [
|
||||
[1, 1, 0, 0, 2, 10, 2, 10, 0, 1, 0, -1],
|
||||
[1, 1, 0, 0, 3, 11, 3, 11],
|
||||
[1, 1, 0, 0, 4, 12, 4, 12, 0, -1],
|
||||
[1, 1, 0, 0, 5, 13, 5, 13, 0, -2],
|
||||
[1, 1, 0, 0, 6, 14, 6, 14, 0, -2],
|
||||
[1, 1, 0, 0, 7, 15, 7, 15, 0, -2],
|
||||
[1, 1, 0, 0, 8, 16, 8, 16, 0, -1],
|
||||
[1, 1, 0, 0, 9, 17, 9, 17],
|
||||
[1, 1, 0, 0, 10, 2, 10, 2, 0, 1, 0, -1],
|
||||
[1, 1, 0, 0, 11, 3, 11, 3],
|
||||
[1, 1, 0, 0, 12, 4, 12, 4, 0, -1],
|
||||
[1, 1, 0, 0, 13, 5, 13, 5, 0, -2],
|
||||
[1, 1, 0, 0, 14, 6, 14, 6, 0, -2],
|
||||
[1, 1, 0, 0, 15, 7, 15, 7, 0, -2],
|
||||
[1, 1, 0, 0, 16, 8, 16, 8, 0, -1],
|
||||
[1, 1, 0, 0, 17, 9, 17, 9],
|
||||
]);
|
||||
|
||||
export const boop = createBodyAnimation('boop', 24, false, [
|
||||
[1, 1, 0, 0, 1, 1, 1, 1],
|
||||
[1, 1, 0, 0, 18, 1, 1, 1],
|
||||
[1, 1, 0, 0, 19, 1, 1, 1],
|
||||
[1, 1, 0, 0, 20, 1, 1, 1],
|
||||
[1, 1, 0, 0, 21, 1, 1, 1],
|
||||
[1, 1, 0, 0, 22, 28, 18, 18, -1],
|
||||
[1, 1, 0, 0, 23, 26, 19, 19, -2, -1],
|
||||
...repeat(5, [1, 1, 0, 0, 23, 27, 20, 20, -3, -1]),
|
||||
[1, 1, 0, 0, 23, 26, 19, 19, -2, -1],
|
||||
[1, 1, 0, 0, 22, 1, 1, 1],
|
||||
[1, 1, 0, 0, 24, 1, 1, 1],
|
||||
[1, 1, 0, 0, 25, 1, 1, 1],
|
||||
[1, 1, 0, 0, 18, 1, 1, 1],
|
||||
[1, 1, 0, 0, 1, 1, 1, 1],
|
||||
]);
|
||||
|
||||
export const boopSit = createBodyAnimation('boop-sit', 24, false, [
|
||||
[9, 1, 2, 2, 34, 34, 26, 26],
|
||||
[9, 1, 2, 2, 13, 34, 26, 26, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -2],
|
||||
[9, 1, 2, 2, 19, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
|
||||
[9, 1, 2, 2, 20, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
|
||||
[9, 1, 2, 2, 21, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
|
||||
[9, 1, 2, 2, 22, 34, 26, 26, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -2],
|
||||
[9, 1, 2, 2, 23, 34, 26, 26, 0, -1, 0, 0, -1, -1, 0, 1, 0, 1, 0, -1],
|
||||
...repeat(5, [9, 1, 2, 2, 23, 34, 26, 26, -1, -2, 0, 0, -2, -2, 1, 2, 1, 2, 1]),
|
||||
[9, 1, 2, 2, 23, 34, 26, 26, 0, -1, 0, 0, -1, -1, 0, 1, 0, 1, 0, -1],
|
||||
[9, 1, 2, 2, 22, 34, 26, 26, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -2],
|
||||
[9, 1, 2, 2, 24, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
|
||||
[9, 1, 2, 2, 25, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
|
||||
[9, 1, 2, 2, 12, 34, 26, 26, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -2],
|
||||
[9, 1, 2, 2, 34, 34, 26, 26],
|
||||
], repeat(18, [0, 6]));
|
||||
|
||||
export const boopLie = createBodyAnimation('boop-lie', 24, false, [
|
||||
[15, 1, 0, 2, 38, 38, 26, 26],
|
||||
...repeat(2, [15, 1, 0, 2, 24, 38, 26, 26, 0, 0, 0, 0, 0, 1]),
|
||||
[15, 1, 0, 2, 21, 38, 26, 26, 0, 0, 0, 0, 0, 1],
|
||||
[15, 1, 0, 2, 22, 38, 26, 26, 0, 0, 0, 0, 0, 1],
|
||||
[15, 1, 0, 2, 23, 38, 26, 26, 0, 0, -1, -1, 0, 1],
|
||||
[12, 1, 0, 2, 23, 37, 26, 26, -1, 0, 0, 0, -1, 1, 0, 0, 1, 0, 1],
|
||||
...repeat(4, [12, 1, 0, 2, 23, 37, 26, 26, -1, 0, 0, 0, -2, 1, 0, 0, 1, 0, 1]),
|
||||
[15, 1, 0, 2, 23, 38, 26, 26, 0, 0, 0, 0, 0, 1],
|
||||
[15, 1, 0, 2, 22, 38, 26, 26, 0, 0, 0, 0, 0, 1],
|
||||
[15, 1, 0, 2, 21, 38, 26, 26, 0, 0, 0, 0, 0, 1],
|
||||
], repeat(14, [3, 3]));
|
||||
|
||||
export const boopSwim = createBodyAnimation('boop-swim', 24, false, [
|
||||
[1, 1, 0, 0, 1, 10, 6, 5, 0, 13],
|
||||
[1, 1, 0, 0, 18, 10, 6, 5, 0, 13],
|
||||
[1, 1, 0, 0, 19, 10, 6, 5, 0, 13],
|
||||
[1, 1, 0, 0, 20, 10, 6, 5, 0, 13],
|
||||
[1, 1, 0, 0, 21, 10, 6, 5, 0, 12],
|
||||
[1, 1, 0, 0, 22, 9, 6, 5, -1, 12],
|
||||
[1, 1, 0, 0, 23, 8, 6, 5, -2, 11],
|
||||
...repeat(5, [1, 1, 0, 0, 23, 8, 6, 5, -3, 11]),
|
||||
[1, 1, 0, 0, 23, 9, 6, 5, -2, 11],
|
||||
[1, 1, 0, 0, 22, 10, 6, 5, 0, 13],
|
||||
[1, 1, 0, 0, 24, 10, 6, 5, 0, 13],
|
||||
[1, 1, 0, 0, 25, 10, 6, 5, 0, 14],
|
||||
[1, 1, 0, 0, 18, 10, 6, 5, 0, 14],
|
||||
[1, 1, 0, 0, 1, 10, 6, 5, 0, 14]
|
||||
]);
|
||||
|
||||
export const sit = createBodyAnimation('sit', 24, true, [
|
||||
[9, 1, 2, 2, 34, 34, 26, 26],
|
||||
], [[0, 6]]);
|
||||
|
||||
const sitShadow = [0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 6].map(offset => [0, offset]);
|
||||
|
||||
export const sitDown = createBodyAnimation('sit-down', 24, false, [
|
||||
[1, 1, 0, 0, 1, 1, 1, 1],
|
||||
...repeat(2, [2, 1, 0, 0, 29, 29, 1, 1]),
|
||||
...repeat(2, [3, 1, 0, 0, 30, 30, 21, 21]),
|
||||
[4, 1, 0, 0, 31, 31, 22, 22],
|
||||
[5, 1, 0, 1, 32, 32, 23, 23],
|
||||
[6, 1, 1, 2, 33, 33, 24, 24],
|
||||
[7, 1, 2, 2, 34, 34, 25, 25],
|
||||
[8, 1, 2, 2, 34, 34, 25, 25],
|
||||
[9, 1, 2, 2, 34, 34, 26, 26],
|
||||
], sitShadow);
|
||||
|
||||
export const standUp = createBodyAnimation('stand-up', 24, false, [
|
||||
[9, 1, 2, 2, 34, 34, 26, 26],
|
||||
[8, 1, 2, 2, 34, 34, 25, 25],
|
||||
[7, 1, 2, 2, 34, 34, 25, 25],
|
||||
[6, 1, 1, 2, 33, 33, 24, 24],
|
||||
[5, 1, 0, 1, 32, 32, 23, 23],
|
||||
[4, 1, 0, 0, 31, 31, 22, 22],
|
||||
...repeat(2, [3, 1, 0, 0, 30, 30, 21, 21]),
|
||||
[1, 1, 0, 0, 1, 1, 1, 1],
|
||||
], sitShadow.slice(2).reverse());
|
||||
|
||||
export const sitToTrot = createBodyAnimation('sit-to-trot', 24, false, [
|
||||
[7, 1, 2, 2, 34, 35, 24, 25, 0, -1, 0, 0, 0, 2, 1, 1, 0, -1],
|
||||
[6, 1, 1, 2, 27, 36, 23, 24, 0, -2, 0, 0, 0, -2, 0, 0, 0, 1],
|
||||
[5, 1, 0, 1, 5, 13, 5, 23, 0, -2],
|
||||
[4, 1, 0, 0, 6, 14, 6, 5, 0, -2],
|
||||
[3, 1, 0, 0, 7, 15, 7, 15, 0, -2],
|
||||
[2, 1, 0, 0, 8, 16, 8, 16, 0, -1],
|
||||
], [[0, 6], [0, 5], [0, 4], [0, 3], [0, 1], [0, 0]]);
|
||||
|
||||
export const lie = createBodyAnimation('lie', 24, true, [
|
||||
[15, 1, 0, 2, 38, 38, 26, 26],
|
||||
], [[3, 3]]);
|
||||
|
||||
const lieShadow = [[0, 6], [0, 6], [1, 5], [2, 4], [3, 3], [3, 3], [3, 3]];
|
||||
|
||||
export const lieDown = createBodyAnimation('lie-down', 24, false, [
|
||||
[9, 1, 2, 2, 34, 34, 26, 26],
|
||||
[10, 1, 2, 2, 35, 34, 26, 26, 0, 0, 0, 0, 0, 0, 0, -1],
|
||||
[11, 1, 1, 2, 36, 36, 26, 26, 0, 0, 0, 0, 0, 0, 1],
|
||||
[12, 1, 1, 2, 37, 37, 26, 26, 0, 0, 0, 0, 0, 0, 1],
|
||||
[13, 1, 0, 2, 38, 38, 26, 26, 0, 0, 0, 0, 0, 0, 1],
|
||||
...repeat(2, [14, 1, 0, 2, 38, 38, 26, 26]),
|
||||
], lieShadow);
|
||||
|
||||
export const sitUp = createBodyAnimation('sit-up', 24, false, [
|
||||
...repeat(2, [14, 1, 0, 2, 38, 38, 26, 26]),
|
||||
[13, 1, 0, 2, 38, 38, 26, 26],
|
||||
[12, 1, 1, 2, 37, 37, 26, 26],
|
||||
[11, 1, 1, 2, 36, 36, 26, 26],
|
||||
[10, 1, 2, 2, 35, 34, 26, 26, 0, 0, 0, 0, 0, 0, 0, -1],
|
||||
[9, 1, 2, 2, 34, 34, 26, 26],
|
||||
], lieShadow.slice().reverse());
|
||||
|
||||
export const lieToTrot = createBodyAnimation('lie-to-trot', 24, false, [
|
||||
[1, 1, 0, 1, 36, 37, 24, 25, 4, 8, 0, 1, 0, 0, 0, 0, 0, 1],
|
||||
[1, 1, 0, 0, 30, 12, 23, 24, 2, 5, 0, 0, 0, -3],
|
||||
[1, 1, 0, 0, 5, 13, 5, 23, 1, 1],
|
||||
[1, 1, 0, 0, 6, 14, 6, 21, 0, 0, 0, -1],
|
||||
[1, 1, 0, 0, 7, 15, 7, 15, 0, -1, 0, -1],
|
||||
[1, 1, 0, 0, 8, 16, 8, 16, 0, -2],
|
||||
], [[2, 1], [1, 0], ...repeat(4, [0, 0])]);
|
||||
|
||||
export const fly = createBodyAnimation('fly', 16, true, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -16],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 6, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 7, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 8, 0, 8, 10, 6, 5, 0, -17],
|
||||
[1, 1, 9, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 10, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 11, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 12, 0, 8, 10, 6, 5, 0, -17],
|
||||
]);
|
||||
|
||||
export const boopFly = createBodyAnimation('boop-fly', 16, false, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -16],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 5, 0, 20, 10, 6, 5, 0, -14],
|
||||
[1, 1, 6, 0, 21, 10, 6, 5, 0, -14],
|
||||
[1, 1, 7, 0, 22, 10, 6, 5, -1, -15],
|
||||
[1, 1, 8, 0, 23, 10, 5, 5, -1, -17, -1, 0, -1, -1, 2],
|
||||
[1, 1, 9, 0, 23, 10, 4, 5, -1, -18, -1, 0, -1, -1, 2],
|
||||
[1, 1, 10, 0, 23, 10, 4, 4, -1, -18, -1, 0, -1, -1, 2],
|
||||
[1, 1, 11, 0, 23, 10, 4, 3, -1, -18, -1, 0, -1, -1, 2],
|
||||
[1, 1, 12, 0, 22, 10, 4, 3, 0, -17, -1, 0, 0, 0, 2],
|
||||
[1, 1, 3, 0, 21, 10, 5, 4, 0, -16, 0, 0, 0, 0, 2],
|
||||
[1, 1, 4, 0, 14, 10, 6, 5, 0, -15, 0, 0, 0, 0, 2]
|
||||
]);
|
||||
|
||||
export const boopFlyBug = createBodyAnimation('boop-fly-bug', 20, false, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -16],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 5, 0, 20, 10, 6, 5, 0, -14],
|
||||
[1, 1, 4, 0, 21, 10, 6, 5, 0, -14],
|
||||
[1, 1, 3, 0, 22, 10, 6, 5, -1, -15],
|
||||
[1, 1, 4, 0, 23, 10, 5, 5, -1, -17, -1, 0, -1, -1, 2],
|
||||
[1, 1, 5, 0, 23, 10, 4, 5, -1, -18, -1, 0, -1, -1, 2],
|
||||
[1, 1, 4, 0, 23, 10, 4, 4, -1, -18, -1, 0, -1, -1, 2],
|
||||
[1, 1, 3, 0, 23, 10, 4, 3, -1, -18, -1, 0, -1, -1, 2],
|
||||
[1, 1, 4, 0, 22, 10, 4, 3, 0, -17, -1, 0, 0, 0, 2],
|
||||
[1, 1, 5, 0, 21, 10, 5, 4, 0, -16, 0, 0, 0, 0, 2],
|
||||
[1, 1, 4, 0, 14, 10, 6, 5, 0, -15, 0, 0, 0, 0, 2]
|
||||
]);
|
||||
|
||||
export const flyUp = createBodyAnimation('fly-up', 16, false, [
|
||||
[1, 1, 11, 0, 1, 1, 1, 1],
|
||||
[1, 1, 12, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1],
|
||||
[2, 1, 3, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[2, 1, 4, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[2, 1, 5, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[1, 1, 6, 0, 1, 1, 1, 1],
|
||||
[1, 1, 7, 0, 6, 7, 5, 5, 0, -10, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
|
||||
[1, 1, 8, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 9, 0, 8, 10, 6, 5, 0, -17],
|
||||
[1, 1, 10, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 11, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 12, 0, 8, 10, 6, 5, 0, -17],
|
||||
]);
|
||||
|
||||
export const trotToFly = createBodyAnimation('trot-to-fly', 20, false, [
|
||||
[1, 1, 11, 0, 6, 14, 6, 14, 0, -2],
|
||||
[1, 1, 12, 0, 7, 15, 7, 15, 0, -2],
|
||||
[1, 1, 3, 0, 8, 16, 8, 16, 0, -1],
|
||||
[1, 1, 4, 0, 9, 17, 9, 17, 0, 1, 0, 1, 0, 0, 0, -1, 0, -1],
|
||||
[1, 1, 5, 0, 10, 2, 10, 2, 0, 3, 0, 1, 0, 0, 0, -2, 0, -2],
|
||||
[1, 1, 6, 0, 11, 3, 11, 3],
|
||||
[1, 1, 7, 0, 11, 4, 11, 4, 0, -10],
|
||||
[1, 1, 8, 0, 10, 5, 9, 5, 0, -15],
|
||||
[1, 1, 9, 0, 9, 10, 6, 6, 0, -17],
|
||||
[1, 1, 10, 0, 8, 10, 6, 7, 0, -18],
|
||||
[1, 1, 11, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 12, 0, 8, 10, 6, 5, 0, -17]
|
||||
]);
|
||||
|
||||
export const trotToFlyBug = createBodyAnimation('trot-to-fly-bug', 20, false, [
|
||||
[1, 1, 3, 0, 6, 14, 6, 14, 0, -2],
|
||||
[1, 1, 4, 0, 7, 15, 7, 15, 0, -2],
|
||||
[1, 1, 5, 0, 8, 16, 8, 16, 0, -1],
|
||||
[1, 1, 3, 0, 9, 17, 9, 17, 0, 1, 0, 1, 0, 0, 0, -1, 0, -1],
|
||||
[1, 1, 4, 0, 10, 2, 10, 2, 0, 3, 0, 1, 0, 0, 0, -2, 0, -2],
|
||||
[1, 1, 5, 0, 11, 3, 11, 3],
|
||||
[1, 1, 3, 0, 11, 4, 11, 4, 0, -10],
|
||||
[1, 1, 4, 0, 10, 5, 9, 5, 0, -15],
|
||||
[1, 1, 5, 0, 8, 10, 6, 6, 0, -17],
|
||||
[1, 1, 3, 0, 8, 10, 6, 7, 0, -18],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -17]
|
||||
]);
|
||||
|
||||
export const flyToTrot = createBodyAnimation('fly-to-trot', 20, false, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 10, 0, -16],
|
||||
[1, 1, 4, 0, 8, 11, 5, 11, 0, -15],
|
||||
[1, 1, 5, 0, 8, 12, 4, 12, 0, -12],
|
||||
[1, 1, 6, 0, 7, 13, 5, 13, 0, -8],
|
||||
[1, 1, 6, 0, 7, 14, 6, 14, 0, -6],
|
||||
[1, 1, 6, 0, 7, 15, 7, 15, 0, -4],
|
||||
[1, 1, 7, 0, 8, 16, 8, 16, 0, -1],
|
||||
[1, 1, 11, 0, 9, 17, 9, 17],
|
||||
[1, 1, 0, 0, 10, 2, 10, 2, 0, 3, 0, -1, 0, 0, 0, -2, 0, -2]
|
||||
|
||||
// [1, 1, 4, 0, 8, 10, 6, 10, 0, -16],
|
||||
// [1, 1, 5, 0, 8, 10, 6, 10, 0, -18],
|
||||
// [1, 1, 7, 0, 8, 11, 5, 11, 0, -20],
|
||||
// [1, 1, 8, 0, 10, 12, 4, 12, 0, -21],
|
||||
// [1, 1, 9, 0, 11, 13, 4, 13, 0, -20],
|
||||
// [1, 1, 10, 0, 12, 14, 5, 14, 0, -18],
|
||||
// [1, 1, 11, 0, 13, 15, 7, 15, 0, -14, 0, -1],
|
||||
// [1, 1, 4, 0, 14, 16, 8, 16, 0, 0, 0, -1],
|
||||
// [1, 1, 5, 0, 2, 17, 9, 17, 0, 2, 0, 0, 0, -1, 0, -2, 0, -2, 0, -2],
|
||||
// [1, 1, 6, 0, 3, 2, 10, 2, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 2]
|
||||
]);
|
||||
|
||||
export const flyToTrotBug = createBodyAnimation('fly-to-trot-bug', 20, false, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 10, 0, -16],
|
||||
[1, 1, 4, 0, 8, 11, 5, 11, 0, -15],
|
||||
[1, 1, 5, 0, 8, 12, 4, 12, 0, -12],
|
||||
[1, 1, 4, 0, 7, 13, 5, 13, 0, -8],
|
||||
[1, 1, 3, 0, 7, 14, 6, 14, 0, -6],
|
||||
[1, 1, 4, 0, 7, 15, 7, 15, 0, -4],
|
||||
[1, 1, 5, 0, 8, 16, 8, 16, 0, -1],
|
||||
[1, 1, 4, 0, 9, 17, 9, 17],
|
||||
[1, 1, 3, 0, 10, 2, 10, 2, 0, 3, 0, -1, 0, 0, 0, -2, 0, -2]
|
||||
|
||||
// [1, 1, 4, 0, 8, 10, 6, 10, 0, -16],
|
||||
// [1, 1, 5, 0, 8, 10, 6, 10, 0, -18],
|
||||
// [1, 1, 4, 0, 8, 11, 5, 11, 0, -20],
|
||||
// [1, 1, 3, 0, 10, 12, 4, 12, 0, -21],
|
||||
// [1, 1, 4, 0, 11, 13, 4, 13, 0, -20],
|
||||
// [1, 1, 5, 0, 12, 14, 5, 14, 0, -18],
|
||||
// [1, 1, 4, 0, 13, 15, 7, 15, 0, -14, 0, -1],
|
||||
// [1, 1, 3, 0, 14, 16, 8, 16, 0, 0, 0, -1],
|
||||
// [1, 1, 4, 0, 2, 17, 9, 17, 0, 2, 0, 0, 0, -1, 0, -2, 0, -2, 0, -2],
|
||||
// [1, 1, 5, 0, 3, 2, 10, 2, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 2]
|
||||
]);
|
||||
|
||||
export const flyDown = createBodyAnimation('fly-down', 16, false, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -12],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -10],
|
||||
[1, 1, 6, 0, 8, 10, 6, 5, 0, -8],
|
||||
[1, 1, 7, 0, 8, 10, 6, 5, 0, -6],
|
||||
[1, 1, 11, 0, 8, 10, 6, 5, 0, -4],
|
||||
[1, 1, 1, 0, 8, 10, 6, 5, 0, -2]
|
||||
]);
|
||||
|
||||
export const flyBug = createBodyAnimation('fly-bug', 24, true, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -16],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -16],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -16],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17],
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -17],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -17],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17]
|
||||
]);
|
||||
|
||||
export const flyUpBug = createBodyAnimation('fly-up-bug', 16, false, [
|
||||
[1, 1, 3, 0, 1, 1, 1, 1],
|
||||
[1, 1, 4, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1],
|
||||
[2, 1, 5, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[2, 1, 4, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[2, 1, 5, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
|
||||
[1, 1, 4, 0, 1, 1, 1, 1],
|
||||
[1, 1, 5, 0, 6, 7, 5, 5, 0, -10, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -17],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -18],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17]
|
||||
]);
|
||||
|
||||
export const flyDownBug = createBodyAnimation('fly-down-bug', 16, false, [
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -12],
|
||||
[1, 1, 5, 0, 8, 10, 6, 5, 0, -10],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -8],
|
||||
[1, 1, 3, 0, 8, 10, 6, 5, 0, -6],
|
||||
[1, 1, 4, 0, 8, 10, 6, 5, 0, -4],
|
||||
[1, 1, 1, 0, 8, 10, 6, 5, 0, -2]
|
||||
]);
|
||||
|
||||
export const swing = createBodyAnimation('swing', 12, false, [
|
||||
...repeat(1, [1, 1, 0, 0, 1, 1, 1, 1]),
|
||||
...repeat(3, [2, 1, 0, 0, 12, 17, 11, 11, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]),
|
||||
]);
|
||||
|
||||
export const flyAnims = [undefined, fly, fly, fly, flyBug];
|
||||
export const flyUpAnims = [undefined, flyUp, flyUp, flyUp, flyUpBug];
|
||||
export const flyDownAnims = [undefined, flyDown, flyDown, flyDown, flyDownBug];
|
||||
|
||||
export const animations = [
|
||||
stand, trot, boop, boopSit, boopLie, boopSwim, boopFly, boopFlyBug, sit, sitDown, standUp, lie, lieDown, sitUp,
|
||||
fly, flyBug, flyUp, flyUpBug, flyDown, flyDownBug, sitToTrot, lieToTrot, flyToTrot, flyToTrotBug,
|
||||
swim, trotToSwim, swimToTrot, flyToSwim, swimToFly,
|
||||
];
|
||||
|
||||
export const sitDownUp = mergeAnimations('sit', 24, false, [...repeat(12, stand), sitDown, ...repeat(12, sit), standUp]);
|
||||
export const lieDownUp = mergeAnimations('lie', 24, false, [...repeat(12, sit), lieDown, ...repeat(12, lie), sitUp]);
|
||||
|
||||
export function mergeAnimations(name: string, fps: number, loop: boolean, animations: BodyAnimation[]): BodyAnimation {
|
||||
return {
|
||||
name,
|
||||
fps,
|
||||
loop,
|
||||
frames: flatten(animations.map(a => a.frames)),
|
||||
shadow: flatten(animations.map(a => a.shadow || a.frames.map(() => ({ frame: 0, offset: 0 })))),
|
||||
};
|
||||
}
|
||||
|
||||
// head animations
|
||||
|
||||
export function createHeadFrame([headX = 0, headY = 0, left = 0, right = 0, mouth = 0]: number[]): HeadAnimationFrame {
|
||||
return { headX, headY, left, right, mouth };
|
||||
}
|
||||
|
||||
export function createHeadAnimation(name: string, fps: number, loop: boolean, frames: number[][]): HeadAnimation {
|
||||
return { name, fps, loop, frames: frames.map(createHeadFrame) };
|
||||
}
|
||||
|
||||
export const smile = createHeadAnimation('smile', 24, true, [
|
||||
[0, 0, 1, 1, 0],
|
||||
]);
|
||||
|
||||
export const nom = createHeadAnimation('nom', 12, true, [
|
||||
[0, 0, 1, 1, 0],
|
||||
[0, 0, 1, 1, 25],
|
||||
]);
|
||||
|
||||
export const laugh = createHeadAnimation('laugh', 8, false, [
|
||||
...repeat(4, [0, 0, 14, 14, 5], [0, 1, 14, 14, 5]),
|
||||
]);
|
||||
|
||||
export const yawn = createHeadAnimation('yawn', 12, false, [
|
||||
[0, 0, 3, 3, 8],
|
||||
...repeat(18, [1, -1, 12, 12, 16]),
|
||||
...repeat(8, [0, 0, 12, 12, 12]),
|
||||
[0, 0, 18, 18, 2],
|
||||
]);
|
||||
|
||||
export const surprise = createHeadAnimation('surprise', 8, false, [
|
||||
[0, 1, 6, 6, 1],
|
||||
...repeat(10, [0, 0, 1, 1, 12]),
|
||||
]);
|
||||
|
||||
export const excite = createHeadAnimation('excite', 8, false, [
|
||||
[0, 1, 6, 6, 0],
|
||||
...repeat(10, [0, 0, 1, 1, 5]),
|
||||
]);
|
||||
|
||||
export const surpriseSad = createHeadAnimation('surpriseSad', 8, false, [
|
||||
[0, 1, 15, 15, 8],
|
||||
...repeat(8, [0, 0, 15, 15, 8]),
|
||||
]);
|
||||
|
||||
export const sneeze = createHeadAnimation('sneeze', 12, false, [
|
||||
[0, 0, 18, 18, 8],
|
||||
...repeat(2, [1, -1, 18, 18, 16]),
|
||||
...repeat(8, [-1, 1, 23, 23, 13]),
|
||||
...repeat(4, [0, 0, 18, 18, 7]),
|
||||
]);
|
||||
|
||||
export const headAnimations = [
|
||||
smile, nom, laugh, yawn, surprise, surpriseSad, sneeze, excite,
|
||||
];
|
||||
|
||||
// default animations
|
||||
|
||||
export const defaultBodyAnimation = createBodyAnimation('default', 24, true, [[1, 1, 0, 0, 1, 1, 1, 1]]);
|
||||
export const defaultHeadAnimation = createHeadAnimation('default', 24, true, [[0, 0, -1, -1, -1]]);
|
||||
|
||||
export const defaultBodyFrame = defaultBodyAnimation.frames[0];
|
||||
export const defaultHeadFrame = defaultHeadAnimation.frames[0];
|
||||
@@ -0,0 +1,790 @@
|
||||
import {
|
||||
PonyEye, PonyState, PalettePonyInfo, PaletteSpriteSet, Palette, HeadAnimationFrame,
|
||||
Eye, Iris, ColorExtraSets, ExpressionExtra, BodyAnimationFrame, DrawPonyOptions, Muzzle, BodyShadow, DrawOptions,
|
||||
NoDraw, PaletteSpriteBatch, defaultDrawOptions, PonyStateFlags, PaletteManager, isEyeSleeping, Matrix2D,
|
||||
} from '../common/interfaces';
|
||||
import { WHITE, SHINES_COLOR, FAR_COLOR, TRANSPARENT, fillToOutlineColor } from '../common/colors';
|
||||
import { toInt, hasFlag, repeat, flatten, point } from '../common/utils';
|
||||
import * as sprites from '../generated/sprites';
|
||||
import * as offsets from '../common/offsets';
|
||||
import { defaultHeadAnimation, defaultBodyFrame, defaultHeadFrame } from './ponyAnimations';
|
||||
import { toWorldX, toWorldY } from '../common/positionUtils';
|
||||
import {
|
||||
frontHooves, PONY_WIDTH, PONY_HEIGHT, wings, chestBehind, tails, chest, neckAccessories, waistAccessories,
|
||||
SLEEVED_ACCESSORIES, blinkFrames, flipIris, claws, Sets, backAccessories, SLEEVED_BACK_ACCESSORIES,
|
||||
CHEST_ACCESSORIES_IN_FRONT, flipFaceAccessoryType, flipFaceAccessoryPattern, backLegSleeves,
|
||||
NO_MANE_HEAD_ACCESSORIES, backHoovesInFront, frontHoovesInFront
|
||||
} from './ponyUtils';
|
||||
import { HEAD_ACCESSORY_OFFSETS, EAR_ACCESSORY_OFFSETS, EXTRA_ACCESSORY_OFFSETS } from '../common/offsets';
|
||||
import { createMat2D, identityMat2D, translateMat2D, copyMat2D, rotateMat2D, scaleMat2D } from '../common/mat2d';
|
||||
import { darkenForOutline } from '../common/ponyInfo';
|
||||
|
||||
type Batch = PaletteSpriteBatch;
|
||||
type Info = Readonly<PalettePonyInfo>;
|
||||
type State = Readonly<PonyState>;
|
||||
type Options = Readonly<DrawPonyOptions>;
|
||||
|
||||
const holdingDrawOptions: DrawOptions = {
|
||||
...defaultDrawOptions,
|
||||
shadowColor: TRANSPARENT,
|
||||
};
|
||||
|
||||
function checker(parts: number[]) {
|
||||
const set = new Set(parts);
|
||||
return (part: number) => set.has(part);
|
||||
}
|
||||
|
||||
function partChecker(parts: number[]) {
|
||||
const check = checker(parts);
|
||||
return (set: PaletteSpriteSet | undefined) => set !== undefined && !!set.type && check(set.type);
|
||||
}
|
||||
|
||||
const SHADOW_OX = 20;
|
||||
const SHADOW_OY = 64;
|
||||
const FAR_OX = -3;
|
||||
const FAR_OY = -1;
|
||||
const FAR_WING_OX = -4;
|
||||
const FAR_WING_OY = 0;
|
||||
|
||||
const hasNoMane = checker(NO_MANE_HEAD_ACCESSORIES);
|
||||
const behindBackAccessory = partChecker([1]);
|
||||
const sleevedAccessory = partChecker(SLEEVED_ACCESSORIES);
|
||||
const sleevedBackAccessory = partChecker(SLEEVED_BACK_ACCESSORIES);
|
||||
const chestAccessoryInFront = partChecker(CHEST_ACCESSORIES_IN_FRONT);
|
||||
|
||||
const pointZero = point(0, 0);
|
||||
|
||||
const headFlipOffsetX = 35.5;
|
||||
const headFlipOffsetY = 42;
|
||||
|
||||
const headTransform = createMat2D();
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return value > min ? (value < max ? value : max) : min;
|
||||
}
|
||||
|
||||
function at<T>(items: T[], index: any): T | undefined {
|
||||
// if (DEVELOPMENT) {
|
||||
// if (items.length === 0) {
|
||||
// console.warn(`Empty array at: ${getStackLocation(2)} / ${getStackLocation(3)}`);
|
||||
// } else if (index < 0 || index >= items.length) {
|
||||
// console.warn(`Index out of range ${index} of 0..${items.length} at: ${getStackLocation(2)} / ${getStackLocation(3)}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
return items[clamp(index | 0, 0, items.length - 1)];
|
||||
}
|
||||
|
||||
function att<T>(items: T[] | undefined, index: any): T | undefined {
|
||||
return items && items[clamp(index | 0, 0, items.length - 1)];
|
||||
}
|
||||
|
||||
function atDef<T>(items: T[] | undefined, index: number, def: T): T {
|
||||
return (items && items.length > 0 && index >= 0 && index < items.length) ? items[index | 0] : def;
|
||||
}
|
||||
|
||||
export function getPonyAnimationFrame<T>({ frames }: { frames: T[] }, frame: number, defaultFrame: T): T {
|
||||
return frames.length > 0 ? frames[Math.max(0, frame) % frames.length] : defaultFrame;
|
||||
}
|
||||
|
||||
function getHeadXY(x: number, y: number, turned: boolean, frame: BodyAnimationFrame, headFrame: HeadAnimationFrame) {
|
||||
const headOffset = at(offsets.headOffsets, frame.body)!;
|
||||
const headX = x + frame.headX + (headFrame.headX * (turned ? -1 : 1)) + headOffset.x;
|
||||
const headY = y + frame.headY + headFrame.headY + headOffset.y;
|
||||
return { headX, headY };
|
||||
}
|
||||
|
||||
export function getPonyHeadPosition(state: State, ponyX: number, ponyY: number) {
|
||||
const frame = getPonyAnimationFrame(state.animation, state.animationFrame, defaultBodyFrame);
|
||||
const headFrame = getPonyAnimationFrame(state.headAnimation || defaultHeadAnimation, state.headAnimationFrame, defaultHeadFrame);
|
||||
const baseX = ponyX - PONY_WIDTH / 2;
|
||||
const baseY = ponyY - PONY_HEIGHT;
|
||||
const x = baseX + frame.bodyX;
|
||||
const y = baseY + frame.bodyY;
|
||||
const { headX, headY } = getHeadXY(x, y, state.headTurned, frame, headFrame);
|
||||
return { x: headX, y: headY };
|
||||
}
|
||||
|
||||
export function createHeadTransform(
|
||||
originalTransform: Matrix2D | undefined, headX: number, headY: number, { headTilt, headTurned }: State
|
||||
) {
|
||||
if (originalTransform !== undefined) {
|
||||
copyMat2D(headTransform, originalTransform);
|
||||
} else {
|
||||
identityMat2D(headTransform);
|
||||
}
|
||||
|
||||
translateMat2D(headTransform, headTransform, headX + headFlipOffsetX, headY + headFlipOffsetY);
|
||||
|
||||
if (headTilt) {
|
||||
rotateMat2D(headTransform, headTransform, headTilt * 0.1);
|
||||
}
|
||||
|
||||
scaleMat2D(headTransform, headTransform, headTurned ? -1 : 1, 1);
|
||||
translateMat2D(headTransform, headTransform, -headFlipOffsetX, -headFlipOffsetY);
|
||||
return headTransform;
|
||||
}
|
||||
|
||||
export function getHeadY(frame: BodyAnimationFrame, headFrame: HeadAnimationFrame): number {
|
||||
const headOffset = offsets.headOffsets[frame.body];
|
||||
return frame.bodyY + frame.headY + headFrame.headY + headOffset.y;
|
||||
}
|
||||
|
||||
const defaultShadow: BodyShadow = { frame: 0, offset: 0 };
|
||||
const hairOffsets = [
|
||||
0, 0, 0, 0,
|
||||
-1, -1, 0, 0,
|
||||
0, 0, 0, -1,
|
||||
-1, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
...repeat(100, 0),
|
||||
];
|
||||
|
||||
function draw(options: Options, flag: NoDraw) {
|
||||
if (TOOLS) {
|
||||
return !hasFlag(options.no, flag);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const headOffsetsX = [0, 1, 1, 1, 1, 1, 0];
|
||||
const headOffsetsY = [0, 0, 1, 1, 0, 0, 0];
|
||||
|
||||
let toys: (PaletteSpriteSet | undefined)[] = [];
|
||||
|
||||
export function initializeToys(paletteManager: PaletteManager) {
|
||||
function set(type: number, pattern: number, colors: number[]): PaletteSpriteSet {
|
||||
const palette = [
|
||||
TRANSPARENT,
|
||||
...flatten(colors.map(color => [color, darkenForOutline(fillToOutlineColor(color))]))
|
||||
];
|
||||
|
||||
return { type, pattern, palette: paletteManager.add(palette) };
|
||||
}
|
||||
|
||||
toys = [
|
||||
undefined,
|
||||
// hat
|
||||
set(2, 0, [0xffffffff, 0xff2525ff]),
|
||||
set(2, 0, [0xffffffff, 0x22ac22ff]),
|
||||
set(2, 0, [0xffffffff, 0x2e58f4ff]),
|
||||
set(2, 0, [0xffffffff, 0xff71ffff]),
|
||||
// snowpony
|
||||
set(3, 0, [0xffffffff, 0x000000ff, 0xff9100ff, 0xff0000ff]),
|
||||
set(4, 0, [0xffffffff, 0x000000ff, 0xff9100ff, 0xff0000ff]),
|
||||
set(4, 0, [0x404040ff, 0xff0000ff, 0x000000ff, 0xff0000ff]),
|
||||
// gift
|
||||
set(6, 0, [0xcf1717ff, 0xecd132ff]),
|
||||
set(6, 0, [0xdfc588ff, 0xe7559bff]),
|
||||
set(6, 0, [0x7fc484ff, 0x4a79daff]),
|
||||
set(6, 0, [0xd56a69ff, 0x62ab64ff]),
|
||||
set(6, 0, [0xe586dfff, 0x9553c1ff]),
|
||||
// hanging thing
|
||||
set(5, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0xb9c0d8ff, 0xffd94fff, 0xeca242ff]), // bell
|
||||
set(7, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0xb9c0d8ff, 0xc0ccc4ff]), // mistletoe
|
||||
set(17, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0x000000ff, 0xe7b86fff, 0x3f1d0fff]), // cookie
|
||||
set(10, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0x000000ff]), // spider
|
||||
// teddy
|
||||
set(8, 0, [0xa86230ff, 0xdfbe8bff, TRANSPARENT, TRANSPARENT]), // brown
|
||||
set(8, 0, [0xa86230ff, 0xdfbe8bff, 0xffa500ff, 0xffffffff]), // brown angel
|
||||
set(8, 0, [0x474444ff, 0x96623eff, TRANSPARENT, TRANSPARENT]), // black
|
||||
set(8, 0, [0x474444ff, 0x96623eff, 0xffa500ff, 0xffffffff]), // black angel
|
||||
set(9, 1, [0xa86230ff, 0xdfbe8bff, 0xff0000ff, TRANSPARENT, 0xf5f5f5ff]), // brown clothes
|
||||
set(9, 1, [0x474444ff, 0x96623eff, 0xff0000ff, TRANSPARENT, 0xf5f5f5ff]), // black clothes
|
||||
set(9, 0, [0xdce5edff, 0xffffffff, 0xff0000ff, 0xdfbe8bff, 0x645137ff]), // white santa
|
||||
// xmas tree
|
||||
set(11, 0, [0x1a9b2fff, 0x56c7ffff, 0xde4d68ff, 0xf1d224ff]),
|
||||
set(11, 1, [0x1a9b2fff, 0xf1d224ff, 0x1a9b2fff, 0xde4d68ff]),
|
||||
// deer
|
||||
set(12, 0, [0x7b4b24ff, 0xcf0e0eff, 0xbfaa8cff]),
|
||||
set(16, 0, [0x7b4b24ff, 0xcf0e0eff, 0xbfaa8cff, 0xffffffff, 0x56c7ffff, 0xf1d224ff]),
|
||||
// candy horns
|
||||
set(13, 0, [0xffffffff, 0xff1b1bff, TRANSPARENT, TRANSPARENT]), // one
|
||||
set(13, 0, [0xffffffff, 0xff1b1bff, 0xffffffff, 0xff1b1bff]), // two
|
||||
set(13, 0, [0x58df6aff, 0xffffffff, 0x3387e9ff, 0xffffffff]), // two (alt)
|
||||
// star
|
||||
set(14, 0, [0xffd94fff, 0xeca242ff]),
|
||||
// halo
|
||||
set(15, 0, [0xffd94fff, 0xeca242ff]),
|
||||
];
|
||||
|
||||
if (DEVELOPMENT && toys.length > 33) {
|
||||
console.error('too many toys', toys.length);
|
||||
}
|
||||
}
|
||||
|
||||
const zeroPoint = point(0, 0);
|
||||
const wakes = [
|
||||
{ ox: 21, oy: 60, behind: sprites.pony_wake_4, front: sprites.pony_wake_3 },
|
||||
{ ox: 24, oy: 60, behind: sprites.pony_wake_6, front: sprites.pony_wake_5 },
|
||||
{ ox: 18, oy: 51, behind: sprites.pony_wake_2, front: sprites.pony_wake_1 },
|
||||
];
|
||||
|
||||
const wakeIndices = [0, 2, 1, 0, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1];
|
||||
|
||||
function getWakeIndex(info: Info) {
|
||||
const tail = info.tail && info.tail.type || 0;
|
||||
return wakeIndices[tail];
|
||||
}
|
||||
|
||||
export function drawPony(batch: Batch, info: Info, state: State, ponyX: number, ponyY: number, options: Options) {
|
||||
const frame = getPonyAnimationFrame(state.animation, state.animationFrame, defaultBodyFrame);
|
||||
const headFrame = getPonyAnimationFrame(state.headAnimation || defaultHeadAnimation, state.headAnimationFrame, defaultHeadFrame);
|
||||
const baseX = ponyX - PONY_WIDTH / 2;
|
||||
const baseY = ponyY - PONY_HEIGHT;
|
||||
const x = baseX + frame.bodyX;
|
||||
const y = baseY + frame.bodyY;
|
||||
const body = frame.body;
|
||||
const { headX, headY } = getHeadXY(x, y, state.headTurned, frame, headFrame);
|
||||
const frontLegOffset = at(offsets.frontLegOffsets, body)!;
|
||||
const backLegOffset = at(offsets.backLegOffsets, body)!;
|
||||
const wingOffset = at(offsets.wingOffsets, body)!;
|
||||
const chestOffset = at(offsets.chestAccessoryOffsets, body)!;
|
||||
const chestX = x + chestOffset.x;
|
||||
const chestY = y + chestOffset.y;
|
||||
const shadow = atDef(state.animation.shadow, state.animationFrame, defaultShadow);
|
||||
const backOffset = at(offsets.backAccessoryOffsets, body)!;
|
||||
const wing = at(wings, frame.wing);
|
||||
const flipped = options.flipped;
|
||||
|
||||
const headOffset = clamp(state.headTurn, 0, headOffsetsX.length - 1);
|
||||
const headOffsetX = headOffsetsX[headOffset];
|
||||
const headOffsetY = headOffsetsY[headOffset];
|
||||
|
||||
const headTotalY = headY + headOffsetY;
|
||||
const headTransform = createHeadTransform(undefined, headX + headOffsetX, headTotalY, state);
|
||||
const headCropY = 42 - ((headTotalY + headFlipOffsetY) - baseY);
|
||||
const cropW = 80;
|
||||
const cropH = 65;
|
||||
|
||||
const shadowX = baseX + shadow.offset + SHADOW_OX;
|
||||
const shadowY = baseY + SHADOW_OY;
|
||||
|
||||
const wake = wakes[getWakeIndex(info)];
|
||||
const wakeX = baseX + wake.ox;
|
||||
const wakeY = baseY + wake.oy;
|
||||
const wakeFrame = Math.floor(options.gameTime * 7 / 1000) % wake.behind.frames.length;
|
||||
const swimming = options.swimming;
|
||||
|
||||
if (swimming) {
|
||||
batch.drawSprite(wake.behind.frames[wakeFrame], WHITE, info.waterPalette, wakeX, wakeY);
|
||||
batch.crop(-40, -70, cropW, cropH);
|
||||
}
|
||||
|
||||
// selection
|
||||
if (options.selected) {
|
||||
const sprite = at(sprites.ponySelections, shadow.frame);
|
||||
sprite && batch.drawSprite(sprite, WHITE, info.defaultPalette, shadowX, shadowY);
|
||||
}
|
||||
|
||||
// shadow
|
||||
if (options.shadow) {
|
||||
const sprite = at(sprites.ponyShadows, shadow.frame);
|
||||
sprite && batch.drawSprite(sprite, options.shadowColor, info.defaultPalette, shadowX, shadowY);
|
||||
}
|
||||
|
||||
// head accessory
|
||||
const bounce = BETA && options.bounce;
|
||||
const maneOffsetY = bounce ? hairOffsets[state.animationFrame] : 0;
|
||||
const maneBehindOffsetY = bounce ? hairOffsets[state.animationFrame] : 0;
|
||||
const hatOffsetY = maneBehindOffsetY;
|
||||
let hatOffset = at(HEAD_ACCESSORY_OFFSETS, info.mane ? info.mane.type : 0)!;
|
||||
const noMane = !info.mane || hasNoMane(info.mane.type);
|
||||
|
||||
if (info.headAccessory !== undefined && info.headAccessory.type === 20) {
|
||||
hatOffset = zeroPoint;
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.Behind)) {
|
||||
// far wing
|
||||
drawSet(batch, wing, info.wings, x + FAR_WING_OX + wingOffset.x, y + FAR_WING_OY + wingOffset.y, FAR_COLOR);
|
||||
|
||||
batch.save();
|
||||
batch.multiplyTransform(headTransform);
|
||||
|
||||
if (swimming) {
|
||||
// batch.drawRect(0xffff0066, 0, headCropY, cropW, cropH);
|
||||
batch.crop(0, headCropY, cropW, cropH);
|
||||
}
|
||||
|
||||
if (noMane) {
|
||||
drawSet(batch, sprites.headAccessoriesBehind, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE);
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.FarEar)) {
|
||||
drawSet(batch, sprites.earAccessoriesBehind, info.earAccessory, 0, 0, WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.Body) && draw(options, NoDraw.FarEar)) {
|
||||
drawSet(batch, sprites.earsFar, info.ears, 0, 0, draw(options, NoDraw.FarEarShade) ? FAR_COLOR : WHITE);
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.Behind)) {
|
||||
drawSet(batch, sprites.hornsBehind, info.horn, 0, 0, WHITE);
|
||||
|
||||
if (!noMane) {
|
||||
drawSet(batch, sprites.headAccessoriesBehind, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE);
|
||||
}
|
||||
|
||||
drawSet(batch, sprites.backBehindManes, info.backMane, 0, maneBehindOffsetY, WHITE);
|
||||
|
||||
if (!state.headTurned) {
|
||||
drawSet(batch, sprites.behindManes, info.mane, 0, maneBehindOffsetY, WHITE);
|
||||
}
|
||||
|
||||
batch.restore();
|
||||
|
||||
// chest accessory behind
|
||||
drawSet(batch, chestBehind[body], info.chestAccessory, chestX, chestY, WHITE);
|
||||
}
|
||||
|
||||
// legs
|
||||
const behindX = x + FAR_OX;
|
||||
const behindY = y + FAR_OY;
|
||||
const hasTailAccessory = behindBackAccessory(info.backAccessory);
|
||||
const hasSleeves = sleevedAccessory(info.chestAccessory);
|
||||
const hasBackSleeves = sleevedBackAccessory(info.backAccessory);
|
||||
|
||||
const frontBehindX = behindX + frontLegOffset.x + frame.frontFarLegX;
|
||||
const frontBehindY = behindY + frontLegOffset.y + frame.frontFarLegY;
|
||||
const backBehindX = behindX + backLegOffset.x + frame.backFarLegX;
|
||||
const backBehindY = behindY + backLegOffset.y + frame.backFarLegY;
|
||||
|
||||
// far leg back
|
||||
if (draw(options, NoDraw.BackFarLeg)) {
|
||||
drawLeg(batch, backBehindX, backBehindY, frame.backFarLeg, sprites.backLegs,
|
||||
sprites.backLegHooves, sprites.backLegAccessories, info.backLegs,
|
||||
flipped ? info.backLegAccessory : info.backLegAccessoryRight, info.backHooves, backHoovesInFront, FAR_COLOR,
|
||||
undefined, false, 0, 0);
|
||||
}
|
||||
|
||||
// far leg front
|
||||
if (draw(options, NoDraw.FrontFarLeg)) {
|
||||
drawLeg(batch, frontBehindX, frontBehindY, frame.frontFarLeg, sprites.frontLegs,
|
||||
frontHooves, sprites.frontLegAccessories, info.frontLegs,
|
||||
flipped ? info.frontLegAccessory : info.frontLegAccessoryRight, info.frontHooves, frontHoovesInFront, FAR_COLOR,
|
||||
undefined, false, 0, 0);
|
||||
}
|
||||
|
||||
// far leg back sleeve
|
||||
if (draw(options, NoDraw.FarSleeves) && hasBackSleeves) {
|
||||
drawSet(batch, at(sprites.backLegSleeves, frame.backFarLeg), info.backAccessory, backBehindX, backBehindY, FAR_COLOR);
|
||||
}
|
||||
|
||||
// far leg front sleeve
|
||||
if (draw(options, NoDraw.FarSleeves) && hasSleeves) {
|
||||
drawSet(batch, at(sprites.frontLegSleeves, frame.frontFarLeg), info.sleeveAccessory, frontBehindX, frontBehindY, FAR_COLOR);
|
||||
}
|
||||
|
||||
// tail
|
||||
const tailOffset = at(offsets.tailOffsets, body)!;
|
||||
const tailX = x + tailOffset.x;
|
||||
const tailY = y + tailOffset.y;
|
||||
const failFrame = hasFlag(state.flags, PonyStateFlags.CurlTail) ? 1 : frame.tail;
|
||||
|
||||
drawSet(batch, at(tails, failFrame), info.tail, tailX, tailY, WHITE);
|
||||
|
||||
// tail accessory
|
||||
if (draw(options, NoDraw.BackAccessory) && hasTailAccessory) {
|
||||
drawSet(batch, backAccessories[body], info.backAccessory, x + backOffset.x, y + backOffset.y, WHITE);
|
||||
}
|
||||
|
||||
// body
|
||||
if (draw(options, NoDraw.Body) && draw(options, NoDraw.BodyOnly)) {
|
||||
drawSet(batch, sprites.body[body], info.body, x, y, WHITE);
|
||||
}
|
||||
|
||||
// neck accessory
|
||||
const frontNeckAccessory = true; // hasPart(info.neckAccessory, FRONT_NECK_ACCESSORIES);
|
||||
|
||||
// if (!frontNeckAccessory) {
|
||||
// drawSpriteSet(context, neckAccessories[frame.body], info.neckAccessory, x, y);
|
||||
// }
|
||||
|
||||
const frontX = x + frontLegOffset.x + frame.frontLegX;
|
||||
const frontY = y + frontLegOffset.y + frame.frontLegY;
|
||||
const backX = x + backLegOffset.x + frame.backLegX;
|
||||
const backY = y + backLegOffset.y + frame.backLegY;
|
||||
const cmOffset = at(offsets.cmOffsets, body)!;
|
||||
const hooves = (TOOLS && options.useAllHooves) ? sprites.frontLegHooves : frontHooves;
|
||||
|
||||
// close legs back
|
||||
if (draw(options, NoDraw.BackLeg)) {
|
||||
drawLeg(
|
||||
batch, backX, backY, frame.backLeg, sprites.backLegs, sprites.backLegHooves, sprites.backLegAccessories,
|
||||
info.backLegs, flipped ? info.backLegAccessoryRight : info.backLegAccessory, info.backHooves, backHoovesInFront, WHITE,
|
||||
info.cmPalette, flipped && !!info.cmFlip, x + cmOffset.x, y + cmOffset.y);
|
||||
}
|
||||
|
||||
// back accessory
|
||||
if (draw(options, NoDraw.BackAccessory) && !hasTailAccessory) {
|
||||
drawSet(batch, backAccessories[body], info.backAccessory, x + backOffset.x, y + backOffset.y, WHITE);
|
||||
}
|
||||
|
||||
// close leg back sleeves
|
||||
if (draw(options, NoDraw.CloseSleeves) && hasBackSleeves) {
|
||||
drawSet(batch, at(backLegSleeves, frame.backLeg), info.backAccessory, backX, backY, WHITE);
|
||||
}
|
||||
|
||||
const isChestAccessoryInFront = chestAccessoryInFront(info.chestAccessory);
|
||||
|
||||
// chest accessory
|
||||
if (!isChestAccessoryInFront && draw(options, NoDraw.Front)) {
|
||||
drawSet(batch, chest[body], info.chestAccessory, chestX, chestY, WHITE);
|
||||
}
|
||||
|
||||
// close legs front
|
||||
if (draw(options, NoDraw.FrontLeg)) {
|
||||
drawLeg(
|
||||
batch, frontX, frontY, frame.frontLeg, sprites.frontLegs, hooves, sprites.frontLegAccessories,
|
||||
info.frontLegs, flipped ? info.frontLegAccessoryRight : info.frontLegAccessory, info.frontHooves, frontHoovesInFront, WHITE,
|
||||
undefined, false, 0, 0);
|
||||
}
|
||||
|
||||
// close legs front sleeves
|
||||
if (draw(options, NoDraw.CloseSleeves) && hasSleeves) {
|
||||
drawSet(batch, at(sprites.frontLegSleeves, frame.frontLeg), info.sleeveAccessory, frontX, frontY, WHITE);
|
||||
}
|
||||
|
||||
// chest accessory
|
||||
if (isChestAccessoryInFront && draw(options, NoDraw.Front)) {
|
||||
drawSet(batch, chest[body], info.chestAccessory, chestX, chestY, WHITE);
|
||||
}
|
||||
|
||||
// close legs back (2)
|
||||
if (draw(options, NoDraw.BackLeg)) {
|
||||
drawLeg(
|
||||
batch, backX, backY, frame.backLeg, sprites.backLegs2, sprites.backLegHooves2, sprites.backLegAccessories2,
|
||||
info.backLegs, flipped ? info.backLegAccessoryRight : info.backLegAccessory, info.backHooves, backHoovesInFront, WHITE,
|
||||
undefined, false, 0, 0);
|
||||
}
|
||||
|
||||
// close legs back sleeves (2)
|
||||
if (draw(options, NoDraw.CloseSleeves) && hasBackSleeves) {
|
||||
drawSet(batch, at(sprites.backLegSleeves2, frame.backLeg), info.backAccessory, backX, backY, WHITE);
|
||||
}
|
||||
|
||||
// neck accessory
|
||||
if (frontNeckAccessory) {
|
||||
const neckOffset = at(offsets.neckAccessoryOffsets, body)!;
|
||||
drawSet(batch, neckAccessories[body], info.neckAccessory, x + neckOffset.x, y + neckOffset.y, WHITE);
|
||||
}
|
||||
|
||||
// waist accessory
|
||||
const waistFrame = frame.wing > 2 ? 16 : body;
|
||||
const waistOffset = at(offsets.waistAccessoryOffsets, waistFrame)!;
|
||||
drawSet(batch, waistAccessories[waistFrame], info.waistAccessory, x + waistOffset.x, y + waistOffset.y, WHITE);
|
||||
|
||||
// wings
|
||||
drawSet(batch, wing, info.wings, x + wingOffset.x, y + wingOffset.y, WHITE);
|
||||
|
||||
// head
|
||||
const headTurned = state.headTurned;
|
||||
const headFlip = headTurned ? !flipped : flipped;
|
||||
const headSprite = headFlip ? sprites.head0[frame.head] : sprites.head1[frame.head];
|
||||
|
||||
if (swimming) {
|
||||
batch.clearCrop();
|
||||
}
|
||||
|
||||
batch.save();
|
||||
batch.multiplyTransform(headTransform);
|
||||
|
||||
if (swimming) {
|
||||
batch.crop(0, headCropY, cropW, cropH);
|
||||
}
|
||||
|
||||
if (headTurned) {
|
||||
drawSet(batch, sprites.behindManes, info.mane, 0, maneBehindOffsetY, WHITE);
|
||||
}
|
||||
|
||||
drawHead(batch, info, 0, 0, headSprite, headFrame, state, options, headFlip, maneOffsetY);
|
||||
drawSet(batch, sprites.headAccessories, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE);
|
||||
|
||||
batch.restore();
|
||||
|
||||
if (swimming) {
|
||||
batch.drawSprite(wake.front.frames[wakeFrame], WHITE, info.waterPalette, wakeX, wakeY);
|
||||
}
|
||||
}
|
||||
|
||||
export function drawHead(
|
||||
batch: Batch, info: Info, x: number, y: number, headSprites: ColorExtraSets, headFrame: HeadAnimationFrame,
|
||||
{ blinkFrame, expression, holding, blushColor, drawFaceExtra }: State,
|
||||
options: Options, flip: boolean, maneOffsetY: number,
|
||||
) {
|
||||
const extraOffset = at(EXTRA_ACCESSORY_OFFSETS, info.mane && info.mane.type) || pointZero;
|
||||
const extraX = x + extraOffset.x;
|
||||
const extraY = y + extraOffset.y;
|
||||
const toy = att(toys, options.toy);
|
||||
|
||||
if (toy !== undefined) {
|
||||
drawSet(batch, sprites.extraAccessoriesBehind, toy, extraX, extraY, WHITE);
|
||||
} else if (options.extra && draw(options, NoDraw.Behind)) {
|
||||
drawSet(batch, sprites.extraAccessoriesBehind, info.extraAccessory, extraX, extraY, WHITE);
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.Body)) {
|
||||
if (draw(options, NoDraw.Head)) {
|
||||
drawSet(batch, headSprites, info.head, x, y, WHITE);
|
||||
}
|
||||
|
||||
let eyeLeftBase = -1;
|
||||
let eyeRightBase = -1;
|
||||
let irisLeft = Iris.Forward;
|
||||
let irisRight = Iris.Forward;
|
||||
|
||||
if (expression !== undefined) {
|
||||
if (hasFlag(expression.extra, ExpressionExtra.Blush)) {
|
||||
batch.drawSprite(sprites.blush, blushColor, info.defaultPalette, x, y);
|
||||
}
|
||||
|
||||
eyeLeftBase = expression.left;
|
||||
eyeRightBase = expression.right;
|
||||
irisLeft = expression.leftIris;
|
||||
irisRight = expression.rightIris;
|
||||
|
||||
// make sure eyes are closed if sleeping
|
||||
if (hasFlag(expression.extra, ExpressionExtra.Zzz)) {
|
||||
if (!isEyeSleeping(eyeLeftBase)) {
|
||||
eyeLeftBase = Eye.Closed;
|
||||
}
|
||||
|
||||
if (!isEyeSleeping(eyeRightBase)) {
|
||||
eyeRightBase = Eye.Closed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eyeRight = getEyeFrame(info.eyeOpennessRight || 1, eyeRightBase, headFrame.right, blinkFrame);
|
||||
const eyeLeft = getEyeFrame(info.eyeOpennessLeft || 1, eyeLeftBase, headFrame.left, blinkFrame);
|
||||
const eyeFrameLeft = flip ? eyeRight : eyeLeft;
|
||||
const eyeFrameRight = flip ? eyeLeft : eyeRight;
|
||||
const eyeColorLeft = flip ? info.eyeColorRight : info.eyeColorLeft;
|
||||
const eyeColorRight = flip ? info.eyeColorLeft : info.eyeColorRight;
|
||||
const eyePaletteLeft = flip ? info.eyePalette : info.eyePaletteLeft;
|
||||
const eyePaletteRight = flip ? info.eyePaletteLeft : info.eyePalette;
|
||||
const eyeIrisLeft = flip ? flipIris(irisRight) : irisLeft;
|
||||
const eyeIrisRight = flip ? flipIris(irisLeft) : irisRight;
|
||||
const eyeLeftSprites = sprites.eyeLeft;
|
||||
const eyeRightSprites = sprites.eyeRight;
|
||||
|
||||
if (draw(options, NoDraw.Eyes)) {
|
||||
drawEye(
|
||||
batch, att(at(eyeLeftSprites, eyeFrameLeft), info.eyelashes),
|
||||
eyeIrisLeft, info, eyeColorLeft, eyePaletteLeft, x, y);
|
||||
drawEye(
|
||||
batch, att(at(eyeRightSprites, eyeFrameRight), info.eyelashes),
|
||||
eyeIrisRight, info, eyeColorRight, eyePaletteRight, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.Front)) {
|
||||
drawSet(batch, sprites.facialHairBehind, info.facialHair, x, y, WHITE);
|
||||
}
|
||||
|
||||
if (drawFaceExtra !== undefined) {
|
||||
drawFaceExtra(batch);
|
||||
}
|
||||
|
||||
const faceAccessory = info.faceAccessory;
|
||||
let faceAccessoryType = 0;
|
||||
let faceAccessoryPattern = 0;
|
||||
|
||||
if (faceAccessory !== undefined) {
|
||||
faceAccessoryType = flip ? flipFaceAccessoryType(faceAccessory.type) : faceAccessory.type;
|
||||
faceAccessoryPattern = flip ? flipFaceAccessoryPattern(faceAccessoryType, faceAccessory.pattern) : faceAccessory.pattern;
|
||||
|
||||
if (draw(options, NoDraw.FaceAccessory1)) {
|
||||
drawTypePattern(
|
||||
batch, sprites.faceAccessories, faceAccessoryType, faceAccessoryPattern,
|
||||
faceAccessory.palette, faceAccessory.extraPalette, x, y, WHITE);
|
||||
|
||||
// if (info.faceAccessoryExtraPalette) {
|
||||
// drawTypePattern(
|
||||
// batch, sprites.faceAccessoriesExtra, faceAccessoryType, faceAccessoryPattern,
|
||||
// info.faceAccessoryExtraPalette, x, y, WHITE);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.Body) && draw(options, NoDraw.Nose)) {
|
||||
const muzzle = holding ?
|
||||
Muzzle.Smile :
|
||||
headFrame.mouth === -1 ?
|
||||
(expression ? expression.muzzle : info.muzzle) :
|
||||
headFrame.mouth;
|
||||
|
||||
const noses = at(sprites.noses, muzzle);
|
||||
const nose = att(noses, info.nose && info.nose.type)![0];
|
||||
|
||||
nose.mouth && batch.drawSprite(nose.mouth, WHITE, info.defaultPalette, x, y);
|
||||
|
||||
if (holding !== undefined && holding.draw !== undefined) {
|
||||
holding.x = toWorldX(x + toInt(holding.pickableX));
|
||||
holding.y = toWorldY(y + toInt(holding.pickableY));
|
||||
holding.draw(batch, holdingDrawOptions);
|
||||
}
|
||||
|
||||
drawSet(batch, noses, info.nose, x, y, WHITE);
|
||||
|
||||
if (info.fangs && nose.fangs) {
|
||||
batch.drawSprite(nose.fangs, WHITE, info.defaultPalette, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.Front2)) {
|
||||
drawSet(batch, sprites.facialHair, info.facialHair, x, y, WHITE);
|
||||
}
|
||||
|
||||
const skipTopAndFrontMane = info.headAccessory !== undefined && info.headAccessory.type === 20;
|
||||
|
||||
if (draw(options, NoDraw.FrontMane)) {
|
||||
drawSet(batch, sprites.backFrontManes, info.backMane, x, y + maneOffsetY, WHITE);
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.TopMane) && !skipTopAndFrontMane) {
|
||||
drawSet(batch, sprites.topManes, info.mane, x, y, WHITE);
|
||||
}
|
||||
|
||||
if (toy !== undefined) {
|
||||
drawSet(batch, sprites.extraAccessories, toy, extraX, extraY, WHITE);
|
||||
} else if (options.extra && draw(options, NoDraw.Front)) {
|
||||
drawSet(batch, sprites.extraAccessories, info.extraAccessory, extraX, extraY, WHITE);
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.Front)) {
|
||||
drawSet(batch, sprites.horns, info.horn, x, y, WHITE);
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.Body) && draw(options, NoDraw.CloseEar) && !options.noEars) {
|
||||
drawSet(batch, sprites.ears, info.ears, x, y, WHITE);
|
||||
}
|
||||
|
||||
if (faceAccessory !== undefined && draw(options, NoDraw.FaceAccessory2)) {
|
||||
drawTypePattern(
|
||||
batch, sprites.faceAccessories2, faceAccessoryType, faceAccessoryPattern,
|
||||
faceAccessory.palette, faceAccessory.extraPalette, x, y, WHITE);
|
||||
|
||||
// if (info.faceAccessoryExtraPalette) {
|
||||
// drawTypePattern(
|
||||
// batch, sprites.faceAccessories2Extra, faceAccessoryType, faceAccessoryPattern,
|
||||
// info.faceAccessoryExtraPalette, x, y, WHITE);
|
||||
// }
|
||||
}
|
||||
|
||||
const earAccessoryOffset = at(EAR_ACCESSORY_OFFSETS, info.ears && info.ears.type)!;
|
||||
const frontEarAccessory = false; // info.earAccessory !== undefined && info.earAccessory.type === 13;
|
||||
|
||||
if (!frontEarAccessory && draw(options, NoDraw.Front) && draw(options, NoDraw.CloseEar)) {
|
||||
drawSet(batch, sprites.earAccessories, info.earAccessory, x + earAccessoryOffset.x, y + earAccessoryOffset.y, WHITE);
|
||||
}
|
||||
|
||||
if (draw(options, NoDraw.FrontMane) && !skipTopAndFrontMane) {
|
||||
drawSet(batch, sprites.frontManes, info.mane, x, y + maneOffsetY, WHITE);
|
||||
}
|
||||
|
||||
if (frontEarAccessory && draw(options, NoDraw.Front) && draw(options, NoDraw.CloseEar)) {
|
||||
drawSet(batch, sprites.earAccessories, info.earAccessory, x + earAccessoryOffset.x, y + earAccessoryOffset.y, WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
function drawLeg(
|
||||
batch: Batch, x: number, y: number, frame: number, leg: Sets, hoof: Sets, sock: Sets,
|
||||
legSet: PaletteSpriteSet | undefined, sockSet: PaletteSpriteSet | undefined, hoofSet: PaletteSpriteSet | undefined,
|
||||
hoovesInFront: boolean[], color: number, cmPalette: Palette | undefined, cmFlip: boolean, cmX: number, cmY: number
|
||||
) {
|
||||
const hoofInFront = hoofSet !== undefined && !!hoovesInFront[hoofSet.type];
|
||||
|
||||
drawSet(batch, at(leg, frame), legSet, x, y, color);
|
||||
|
||||
if (!hoofInFront) {
|
||||
drawSet(batch, at(hoof, frame), hoofSet, x, y, color);
|
||||
}
|
||||
|
||||
// CM
|
||||
if (cmPalette !== undefined) {
|
||||
const cm = cmFlip ? sprites.cmsFlip : sprites.cms;
|
||||
batch.drawSprite(cm, WHITE, cmPalette, cmX, cmY);
|
||||
}
|
||||
|
||||
drawSet(batch, at(sock, frame), sockSet, x, y, color);
|
||||
|
||||
if (hoofInFront) {
|
||||
const hasClaws = hoofSet && hoofSet.type === 3 && hoof === frontHooves;
|
||||
const hasSocks = !!(sockSet && sockSet.type > 0);
|
||||
const hoofSprites = (hasClaws && hasSocks) ? claws : hoof;
|
||||
drawSet(batch, at(hoofSprites, frame), hoofSet, x, y, color);
|
||||
}
|
||||
}
|
||||
|
||||
function getEyeFrame(base: Eye, expression: Eye, anim: Eye, blinkFrame: number) {
|
||||
if (anim !== -1)
|
||||
return anim;
|
||||
|
||||
const frame = expression === -1 ? base : expression;
|
||||
const blink = blinkFrames[frame];
|
||||
|
||||
if (blinkFrame > 1 && blink) {
|
||||
const frameOffset = 6 - blinkFrame;
|
||||
|
||||
if (frameOffset < blink.length) {
|
||||
return blink[blink.length - frameOffset - 1];
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
function drawEye(
|
||||
batch: Batch, eye: PonyEye | undefined, iris: Iris, info: Info, palette: Palette | undefined, eyePalette: Palette,
|
||||
x: number, y: number
|
||||
) {
|
||||
if (eye !== undefined) {
|
||||
if (info.eyeshadow === true) {
|
||||
eye.shadow && batch.drawSprite(eye.shadow, WHITE, info.eyeshadowColor, x, y);
|
||||
eye.shine && batch.drawSprite(eye.shine, SHINES_COLOR, info.defaultPalette, x, y);
|
||||
}
|
||||
|
||||
eye.base && batch.drawSprite(eye.base, WHITE, eyePalette, x, y);
|
||||
const sprite = at(eye.irises, iris);
|
||||
sprite && batch.drawSprite(sprite, WHITE, palette, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
function drawSet(
|
||||
batch: Batch, sprites: ColorExtraSets, set: PaletteSpriteSet | undefined, x: number, y: number, tint: number
|
||||
) {
|
||||
if (set !== undefined) {
|
||||
const patterns = att(sprites, set.type);
|
||||
|
||||
if (patterns !== undefined) {
|
||||
const patternSprite = at(patterns, set.pattern);
|
||||
|
||||
if (patternSprite !== undefined) {
|
||||
batch.drawSprite(patternSprite.color, tint, set.palette, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawTypePattern(
|
||||
batch: Batch, sprites: ColorExtraSets, type: number, pattern: number, palette: Palette, extraPalette: Palette | undefined,
|
||||
x: number, y: number, tint: number
|
||||
) {
|
||||
const patterns = att(sprites, type);
|
||||
|
||||
if (patterns !== undefined) {
|
||||
const patternSprite = at(patterns, pattern);
|
||||
|
||||
if (patternSprite !== undefined) {
|
||||
batch.drawSprite(patternSprite.color, tint, palette, x, y);
|
||||
|
||||
if (patternSprite.extra !== undefined && extraPalette !== undefined) {
|
||||
batch.drawSprite(patternSprite.extra, WHITE, extraPalette, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { DrawPonyOptions, NoDraw, PonyState, PonyStateFlags } from '../common/interfaces';
|
||||
import { SHADOW_COLOR, blushColor } from '../common/colors';
|
||||
import { stand } from './ponyAnimations';
|
||||
|
||||
const defaultBlushColor = blushColor(0);
|
||||
|
||||
export function defaultPonyState(): PonyState {
|
||||
return {
|
||||
animation: stand,
|
||||
animationFrame: 0,
|
||||
headAnimation: undefined,
|
||||
headAnimationFrame: 0,
|
||||
headTurned: false,
|
||||
headTilt: 0,
|
||||
headTurn: 0,
|
||||
blinkFrame: 0,
|
||||
blushColor: defaultBlushColor,
|
||||
holding: undefined,
|
||||
expression: undefined,
|
||||
drawFaceExtra: undefined,
|
||||
flags: PonyStateFlags.None,
|
||||
};
|
||||
}
|
||||
|
||||
export function isStateEqual(a: PonyState, b: PonyState) {
|
||||
return a.animation === b.animation &&
|
||||
a.animationFrame === b.animationFrame &&
|
||||
a.headAnimation === b.headAnimation &&
|
||||
a.headAnimationFrame === b.headAnimationFrame &&
|
||||
a.headTurned === b.headTurned &&
|
||||
a.headTilt === b.headTilt &&
|
||||
a.headTurn === b.headTurn &&
|
||||
a.blinkFrame === b.blinkFrame &&
|
||||
a.blushColor === b.blushColor &&
|
||||
a.holding === b.holding &&
|
||||
a.expression === b.expression &&
|
||||
a.drawFaceExtra === b.drawFaceExtra &&
|
||||
a.flags === b.flags;
|
||||
}
|
||||
|
||||
export function defaultDrawPonyOptions(): DrawPonyOptions {
|
||||
return {
|
||||
flipped: false,
|
||||
selected: false,
|
||||
shadow: false,
|
||||
extra: false,
|
||||
toy: 0,
|
||||
swimming: false,
|
||||
bounce: false,
|
||||
shadowColor: SHADOW_COLOR,
|
||||
noEars: false,
|
||||
no: NoDraw.None,
|
||||
useAllHooves: false,
|
||||
gameTime: 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { animatorState as state, animatorTransition as transition, anyState, AnimatorState } from '../common/animator';
|
||||
import {
|
||||
stand, sit, sitDown, standUp, lie, lieDown, sitUp, flyBug, fly, flyUp, flyDown, flyUpBug, flyDownBug,
|
||||
trot, boop, boopSit, swim, sitToTrot, lieToTrot, boopLie, trotToFly, trotToFlyBug, boopFly, boopFlyBug,
|
||||
flyToTrot, flyToTrotBug, swing, swimToTrot, trotToSwim, swimToFly, flyToSwim, boopSwim, swimToFlyBug, flyToSwimBug
|
||||
} from './ponyAnimations';
|
||||
import { BodyAnimation } from '../common/interfaces';
|
||||
|
||||
function n(value: string) {
|
||||
return (DEVELOPMENT || SERVER) ? value : '';
|
||||
}
|
||||
|
||||
export const standing = state(n('standing'), stand);
|
||||
export const trotting = state(n('trotting'), trot);
|
||||
|
||||
export const swimming = state(n('swimming'), swim);
|
||||
export const swimmingToTrotting = state(n('swimming-to-trotting'), swimToTrot);
|
||||
export const trottingToSwimming = state(n('trotting-to-swimming'), trotToSwim);
|
||||
export const swimmingToFlying = state(n('swimming-to-flying'), swimToFly, { bug: swimToFlyBug });
|
||||
export const flyingToSwimming = state(n('flying-to-swimming'), flyToSwim, { bug: flyToSwimBug });
|
||||
|
||||
export const booping = state(n('booping'), boop);
|
||||
export const boopingSitting = state(n('booping-sitting'), boopSit);
|
||||
export const boopingLying = state(n('booping-lying'), boopLie);
|
||||
export const boopingFlying = state(n('booping-flying'), boopFly, { bug: boopFlyBug });
|
||||
export const boopingSwimming = state(n('booping-swimming'), boopSwim);
|
||||
|
||||
export const sitting = state(n('sitting'), sit);
|
||||
export const sittingDown = state(n('sitting-down'), sitDown);
|
||||
export const standingUp = state(n('standing-up'), standUp);
|
||||
export const sittingToTrotting = state(n('sitting-to-trotting'), sitToTrot);
|
||||
|
||||
export const lying = state(n('lying'), lie);
|
||||
export const lyingDown = state(n('lying-down'), lieDown);
|
||||
export const sittingUp = state(n('sitting-up'), sitUp);
|
||||
export const lyingToTrotting = state(n('lying-to-trotting'), lieToTrot);
|
||||
|
||||
export const hovering = state(n('hovering'), fly, { bug: flyBug });
|
||||
export const flying = state(n('flying'), fly, { bug: flyBug });
|
||||
export const flyingUp = state(n('flying-up'), flyUp, { bug: flyUpBug });
|
||||
export const flyingDown = state(n('flying-down'), flyDown, { bug: flyDownBug });
|
||||
|
||||
export const trottingToFlying = state(n('trotting-to-flying'), trotToFly, { bug: trotToFlyBug });
|
||||
export const flyingToTrotting = state(n('flying-to-trotting'), flyToTrot, { bug: flyToTrotBug });
|
||||
|
||||
export const swinging = state(n('swinging'), swing);
|
||||
|
||||
export const ponyStates = [
|
||||
anyState, standing, trotting, swimming, swimmingToTrotting, trottingToSwimming,
|
||||
booping, boopingSitting, boopingLying, boopingFlying,
|
||||
sitting, sittingDown, standingUp, sittingToTrotting,
|
||||
lying, lyingDown, sittingUp, lyingToTrotting,
|
||||
hovering, flying, flyingUp, flyingDown, trottingToFlying, flyingToTrotting,
|
||||
swinging, swimmingToFlying, flyingToSwimming, boopingSwimming,
|
||||
];
|
||||
|
||||
transition(hovering, flyingDown, { exitAfter: 0 });
|
||||
transition(flyingDown, standing);
|
||||
transition(standing, sittingDown, { exitAfter: 0 });
|
||||
transition(sittingDown, sitting);
|
||||
transition(sitting, lyingDown, { exitAfter: 0 });
|
||||
transition(lyingDown, lying);
|
||||
|
||||
transition(lying, sittingUp, { exitAfter: 0 });
|
||||
transition(sittingUp, sitting);
|
||||
transition(sitting, standingUp, { exitAfter: 0 });
|
||||
transition(standingUp, standing);
|
||||
transition(standing, flyingUp, { exitAfter: 0 });
|
||||
transition(flyingUp, hovering);
|
||||
// transition(flyingUp, trottingToFlying, { exitAfter: 0, keepTime: true });
|
||||
|
||||
transition(sitting, sittingToTrotting, { exitAfter: 0, onlyDirectTo: trotting });
|
||||
transition(sittingToTrotting, trotting, { enterTime: 6.1 / 16 });
|
||||
transition(sittingToTrotting, standing);
|
||||
|
||||
transition(lying, lyingToTrotting, { exitAfter: 0, onlyDirectTo: trotting });
|
||||
transition(lyingToTrotting, trotting, { enterTime: 6.1 / 16 });
|
||||
transition(lyingToTrotting, standing, { exitAfter: 5 / 6 });
|
||||
|
||||
transition(trotting, trottingToFlying, { exitAfter: 4 / 16 });
|
||||
transition(trottingToFlying, flying);
|
||||
|
||||
transition(flying, flyingToTrotting, { exitAfter: 0 });
|
||||
transition(flyingToTrotting, trotting, { enterTime: 6 / 16 });
|
||||
|
||||
transition(swimming, swimmingToTrotting, { exitAfter: 0 });
|
||||
transition(swimming, swimmingToFlying, { exitAfter: 0 });
|
||||
transition(swimmingToTrotting, trotting);
|
||||
transition(swimmingToTrotting, standing);
|
||||
transition(trotting, trottingToSwimming, { exitAfter: 0 });
|
||||
transition(standing, trottingToSwimming, { exitAfter: 0 });
|
||||
transition(trottingToSwimming, swimming);
|
||||
transition(swimmingToFlying, hovering);
|
||||
transition(swimmingToFlying, flying);
|
||||
transition(flying, flyingToSwimming, { exitAfter: 0 });
|
||||
transition(hovering, flyingToSwimming, { exitAfter: 0 });
|
||||
transition(flyingToSwimming, swimming);
|
||||
|
||||
transition(trotting, standing, { exitAfter: 0 });
|
||||
transition(flying, hovering, { exitAfter: 0, keepTime: true });
|
||||
|
||||
transition(boopingSwimming, swimming);
|
||||
transition(swimming, boopingSwimming, { exitAfter: 0 });
|
||||
|
||||
transition(booping, standing);
|
||||
transition(standing, booping, { exitAfter: 0 });
|
||||
transition(boopingSitting, sitting);
|
||||
transition(sitting, boopingSitting, { exitAfter: 0 });
|
||||
transition(boopingLying, lying);
|
||||
transition(lying, boopingLying, { exitAfter: 0 });
|
||||
transition(boopingFlying, hovering, { enterTime: 1.1 / 10 });
|
||||
transition(hovering, boopingFlying, { exitAfter: 0 });
|
||||
|
||||
// transition(anyState, trottingToSwimming, { exitAfter: 0 });
|
||||
|
||||
transition(anyState, trotting, { exitAfter: 0, keepTime: true });
|
||||
transition(anyState, flying, { exitAfter: 0, keepTime: true });
|
||||
|
||||
transition(standing, swinging, { exitAfter: 0 });
|
||||
transition(swinging, standing);
|
||||
|
||||
export function isFlyingUp(state: AnimatorState<BodyAnimation> | undefined) {
|
||||
return state === flyingUp || state === trottingToFlying || state === swimmingToFlying;
|
||||
}
|
||||
|
||||
export function isFlyingDown(state: AnimatorState<BodyAnimation> | undefined) {
|
||||
return state === flyingDown || state === flyingToTrotting || state === flyingToSwimming;
|
||||
}
|
||||
|
||||
export function isSwimmingState(state: AnimatorState<BodyAnimation> | undefined) {
|
||||
return state === swimming || state === trottingToSwimming || state === swimmingToTrotting ||
|
||||
state === flyingToSwimming || state === swimmingToFlying || state === boopingSwimming;
|
||||
}
|
||||
|
||||
export function isFlyingUpOrDown(state: AnimatorState<BodyAnimation> | undefined) {
|
||||
return isFlyingUp(state) || isFlyingDown(state);
|
||||
}
|
||||
|
||||
export function isSittingDown(state: AnimatorState<BodyAnimation> | undefined) {
|
||||
return state === sittingDown;
|
||||
}
|
||||
export function isSittingUp(state: AnimatorState<BodyAnimation> | undefined) {
|
||||
return state === sittingUp;
|
||||
}
|
||||
|
||||
export function toBoopState(state: AnimatorState<BodyAnimation>) {
|
||||
switch (state) {
|
||||
case standing: return booping;
|
||||
case sitting: return boopingSitting;
|
||||
case lying: return boopingLying;
|
||||
case hovering: return boopingFlying;
|
||||
case swimming: return boopingSwimming;
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/// <reference path="../../typings/my.d.ts" />
|
||||
|
||||
import { range, dropRight, compact, max, zip } from 'lodash';
|
||||
import {
|
||||
Eye, Iris, Muzzle, ExpressionExtra, Sprite, ColorExtraSets, PonyInfoBase, SpriteSetBase, ColorExtra, ColorExtraSet
|
||||
} from '../common/interfaces';
|
||||
import * as sprites from '../generated/sprites';
|
||||
import { HEAD_ACCESSORY_OFFSETS, EXTRA_ACCESSORY_OFFSETS, EAR_ACCESSORY_OFFSETS } from '../common/offsets';
|
||||
|
||||
export const PONY_WIDTH = 80;
|
||||
export const PONY_HEIGHT = 70;
|
||||
export const BLINK_FRAMES = [2, 6, 6, 4, 2];
|
||||
export const SLEEVED_ACCESSORIES = [2, 3, 4];
|
||||
export const SLEEVED_BACK_ACCESSORIES = [5, 6];
|
||||
export const CHEST_ACCESSORIES_IN_FRONT = [1];
|
||||
export const NO_MANE_HEAD_ACCESSORIES = [0, 16];
|
||||
// export const FRONT_NECK_ACCESSORIES = [2, 10];
|
||||
|
||||
export type Sprites = (Sprite | undefined)[];
|
||||
export type Sets = ColorExtraSets[]; // [frame][type][pattern]
|
||||
|
||||
export const headCenter = [
|
||||
undefined,
|
||||
[[0].map(i => sprites.head[2]![0]![i])],
|
||||
];
|
||||
|
||||
export const claws: Sets = sprites.frontLegHooves
|
||||
.map(f => f && [undefined, undefined, undefined, f[4], undefined, undefined]);
|
||||
export const frontHooves: Sets = sprites.frontLegHooves
|
||||
.map(f => f && [...f.slice(0, 4), ...f.slice(5)]);
|
||||
|
||||
export const frontHoovesInFront = [false, false, true, true, false, false];
|
||||
export const backHoovesInFront = [false, false, true, false, false];
|
||||
|
||||
const bodyFrames = sprites.body.length;
|
||||
|
||||
export const wings = createCompleteSets(sprites.wings, 3 + 10);
|
||||
export const tails = createCompleteSets(sprites.tails, 3);
|
||||
export const chest = createCompleteSets(sprites.chestAccessories, bodyFrames);
|
||||
export const chestBehind = createCompleteSets(sprites.chestAccessoriesBehind, bodyFrames);
|
||||
export const backAccessories = createCompleteSets(sprites.backAccessories, bodyFrames);
|
||||
sprites.neckAccessories.forEach(f => f && f.pop()); // TEMP: remove headphones
|
||||
export const neckAccessories = createCompleteSets(sprites.neckAccessories, bodyFrames);
|
||||
export const waistAccessories = createCompleteSets(sprites.waistAccessories, bodyFrames + 1);
|
||||
|
||||
function frameType(sets: Sets, frame: number, type: number) {
|
||||
const set = sets[frame];
|
||||
return set && set[type];
|
||||
}
|
||||
|
||||
function createCompleteSets(sets: Sets, frameCount: number): Sets {
|
||||
const typeCount = sets.reduce((max, s) => Math.max(max, s ? s.length : 0), 0);
|
||||
const typeRange = range(0, typeCount);
|
||||
const result: Sets = [];
|
||||
|
||||
for (let frame = 0; frame < frameCount; frame++) {
|
||||
result.push(typeRange.map(type => frameType(sets, frame, type) || frameType(result, frame - 1, type)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function canFly(info: PonyInfoBase<any, SpriteSetBase>) {
|
||||
const type = info.wings && info.wings.type || 0;
|
||||
return type > 0;
|
||||
}
|
||||
|
||||
export function canMagic(info: PonyInfoBase<any, SpriteSetBase>) {
|
||||
const type = info.horn && info.horn.type || 0;
|
||||
return type === 1 || type === 2 || type === 3 || type === 14;
|
||||
}
|
||||
|
||||
export function flipIris(iris: Iris): Iris {
|
||||
if (iris === Iris.Left || iris === Iris.UpLeft) {
|
||||
return iris + 1;
|
||||
} else if (iris === Iris.Right || iris === Iris.UpRight) {
|
||||
return iris - 1;
|
||||
} else {
|
||||
return iris;
|
||||
}
|
||||
}
|
||||
|
||||
export function flipFaceAccessoryType(type: number) {
|
||||
if (type === 6) return 7;
|
||||
if (type === 7) return 6;
|
||||
|
||||
if (type === 9) return 10;
|
||||
if (type === 10) return 9;
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
export function flipFaceAccessoryPattern(type: number, pattern: number) {
|
||||
if (type === 2) { // dark glasses
|
||||
if (pattern === 1) return 2;
|
||||
if (pattern === 2) return 1;
|
||||
} else if (type === 11) { // large dark glasses
|
||||
if (pattern === 1) return 2;
|
||||
if (pattern === 2) return 1;
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
export const defaultExpression = {
|
||||
left: Eye.Neutral,
|
||||
leftIris: Iris.Forward,
|
||||
right: Eye.Neutral,
|
||||
rightIris: Iris.Forward,
|
||||
muzzle: Muzzle.Neutral,
|
||||
extra: ExpressionExtra.None,
|
||||
};
|
||||
|
||||
export const blinkFrames: Eye[][] = [];
|
||||
|
||||
function setupBlinkFrames(frames: Eye[]) {
|
||||
dropRight(frames, 1).forEach((f, i) => blinkFrames[f] = blinkFrames[f] || frames.slice(i + 1));
|
||||
}
|
||||
|
||||
setupBlinkFrames([Eye.Neutral, Eye.Neutral2, Eye.Neutral3, Eye.Neutral4, Eye.Neutral5, Eye.Closed]);
|
||||
setupBlinkFrames([Eye.Frown, Eye.Frown2, Eye.Frown3, Eye.Frown4, Eye.Closed]);
|
||||
setupBlinkFrames([Eye.Sad, Eye.Sad2, Eye.Sad3, Eye.Sad4, Eye.Neutral5, Eye.Closed]);
|
||||
setupBlinkFrames([Eye.Angry, Eye.Angry2, Eye.Neutral4, Eye.Neutral5, Eye.Closed]);
|
||||
|
||||
// sets
|
||||
|
||||
function mergeColorExtras(sprites: (ColorExtra | undefined)[]): ColorExtra | undefined {
|
||||
const filtered = compact(sprites);
|
||||
|
||||
return {
|
||||
...filtered[0],
|
||||
colors: max(filtered.map(x => x.colors || 0)),
|
||||
colorMany: filtered.length > 1 ? filtered.map(x => x.color) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeSprites(sets: ColorExtraSet[]): ColorExtraSet {
|
||||
return zip(...sets).map(mergeColorExtras);
|
||||
}
|
||||
|
||||
function mergeSpriteSets(...sets: ColorExtraSets[]): ColorExtraSets {
|
||||
return zip(...sets).map(mergeSprites);
|
||||
}
|
||||
|
||||
export const backLegSleeves: Sets = sprites.backLegSleeves
|
||||
.map(sets => sets && [undefined, undefined, undefined, undefined, undefined, ...sets]);
|
||||
|
||||
// TEMP: remove summer hat
|
||||
sprites.headAccessoriesBehind.pop();
|
||||
sprites.headAccessories.pop();
|
||||
|
||||
export const mergedManes = mergeSpriteSets(sprites.behindManes, sprites.topManes, sprites.frontManes)!;
|
||||
export const mergedBackManes = mergeSpriteSets(sprites.backBehindManes, sprites.backFrontManes)!;
|
||||
export const mergedFacialHair = mergeSpriteSets(sprites.facialHairBehind, sprites.facialHair)!;
|
||||
export const mergedEarAccessories = mergeSpriteSets(sprites.earAccessoriesBehind, sprites.earAccessories)!;
|
||||
export const mergedHeadAccessories = mergeSpriteSets(sprites.headAccessoriesBehind, sprites.headAccessories)!;
|
||||
export const mergedFaceAccessories = mergeSpriteSets(sprites.faceAccessories, sprites.faceAccessories2)!;
|
||||
export const mergedChestAccessories = mergeSpriteSets(sprites.chestAccessoriesBehind[1], sprites.chestAccessories[1])!;
|
||||
export const mergedBackAccessories = mergeSpriteSets(
|
||||
backAccessories[1], [undefined, undefined, undefined, undefined, undefined, ...sprites.backLegSleeves[1]!])!;
|
||||
export const mergedExtraAccessories = mergeSpriteSets(sprites.extraAccessoriesBehind, sprites.extraAccessories)!
|
||||
.slice(0, DEVELOPMENT ? 100 : 2);
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
assertSizes('HEAD_ACCESSORY_OFFSETS', HEAD_ACCESSORY_OFFSETS, mergedManes);
|
||||
assertSizes('EXTRA_ACCESSORY_OFFSETS', EXTRA_ACCESSORY_OFFSETS, mergedManes);
|
||||
assertSizes('EAR_ACCESSORY_OFFSETS', EAR_ACCESSORY_OFFSETS, sprites.ears);
|
||||
assertSizes('frontHoovesInFront', frontHoovesInFront, frontHooves[1]!);
|
||||
assertSizes('backHoovesInFront', backHoovesInFront, sprites.backLegHooves[1]!);
|
||||
}
|
||||
|
||||
function assertSizes(name: string, a: any[], b: any[]) {
|
||||
if (a.length !== b.length) {
|
||||
throw new Error(`Invalid ${name} length (${a.length} !== ${b.length})`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { REV } from '../generated/rev';
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function getUrl(name: string): string {
|
||||
if (DEVELOPMENT)
|
||||
return `/assets/${name}`;
|
||||
|
||||
if (!REV[name])
|
||||
throw new Error(`Cannot find file url (${name})`);
|
||||
|
||||
return `/assets/${name.replace(/(\.\S+)$/, `-${REV[name]}$1`)}`;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { PonyTownGame } from './game';
|
||||
import { Pony, EntityFlags } from '../common/interfaces';
|
||||
import { setFlag, fromNow } from '../common/utils';
|
||||
import { fixCollision, isStaticCollision } from '../common/collision';
|
||||
import { WEEK } from '../common/constants';
|
||||
|
||||
let currentPlayer: Pony | undefined;
|
||||
let setX = 0;
|
||||
let setY = 0;
|
||||
|
||||
export function setupPlayer(game: PonyTownGame, player: Pony) {
|
||||
const pony = player;
|
||||
pony.flags = setFlag(pony.flags, EntityFlags.Interactive, false);
|
||||
|
||||
if (isStaticCollision(player, game.map, false)) {
|
||||
fixCollision(player, game.map);
|
||||
}
|
||||
|
||||
game.setPlayer(pony);
|
||||
currentPlayer = player;
|
||||
savePlayerPosition();
|
||||
}
|
||||
|
||||
export function savePlayerPosition() {
|
||||
if (currentPlayer) {
|
||||
setX = currentPlayer.x;
|
||||
setY = currentPlayer.y;
|
||||
}
|
||||
}
|
||||
|
||||
export function restorePlayerPosition() {
|
||||
if (currentPlayer) {
|
||||
if (currentPlayer.x !== setX || currentPlayer.y !== setY) {
|
||||
currentPlayer.x = setX;
|
||||
currentPlayer.y = setY;
|
||||
DEVELOPMENT && console.warn('Restoring player position');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Account creation lock
|
||||
export const setAclCookie = (acl: string) => {
|
||||
document.cookie = `acl=${acl}; expires=${fromNow(WEEK).toUTCString()}; path=/`;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as sprites from '../generated/sprites';
|
||||
import { SpriteAnimation } from '../common/animationPlayer';
|
||||
import { AnimatedRenderable } from '../common/mixins';
|
||||
import { Sprite } from '../common/interfaces';
|
||||
|
||||
export const zzzAnimation1 = createSpriteAnimation(
|
||||
sprites.emote_sleep1, 8, 8, 4, 7, true, sprites.emote_sleep1_flip.frames);
|
||||
export const zzzAnimation2 = createSpriteAnimation(
|
||||
sprites.emote_sleep2, 12, 13, 13, 12, true, sprites.emote_sleep2_flip.frames);
|
||||
export const zzzAnimations = [zzzAnimation1, zzzAnimation2];
|
||||
|
||||
export const cryAnimation = createSpriteAnimation(sprites.emote_cry2, 12, 0, 13, 0);
|
||||
|
||||
export const tearsAnimation = createSpriteAnimation(sprites.emote_tears, 12, 0, 1, 0);
|
||||
|
||||
export const heartsAnimation = createSpriteAnimation(sprites.emote_hearts, 12, 18, 18, 9, true);
|
||||
|
||||
sprites.emote_sneeze.frames.unshift(sprites.emptySprite, sprites.emptySprite);
|
||||
export const sneezeAnimation = createSpriteAnimation(sprites.emote_sneeze, 8, 0, 4, 0, false);
|
||||
|
||||
sprites.hold_poof.frames.push(sprites.emptySprite);
|
||||
export const holdPoofAnimation = createSpriteAnimation(sprites.hold_poof, 12, 0, 4, 0, false);
|
||||
|
||||
export const magicAnimation = createSpriteAnimation(sprites.magic2, 8, 2, 6, 0, true);
|
||||
|
||||
function createSpriteAnimation(
|
||||
{ frames, palette }: AnimatedRenderable, fps: number, start: number, middle: number, end: number, loop = true,
|
||||
flipFrames?: Sprite[],
|
||||
): SpriteAnimation {
|
||||
return { start, middle, end, fps, palette, frames, loop, flipFrames };
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { once, noop } from 'lodash';
|
||||
import { ColorExtra, ColorExtraSets, PonyEye, SpriteSheet, Sprite } from '../common/interfaces';
|
||||
import { spriteSheets } from '../generated/sprites';
|
||||
import { loadImage, createCanvas } from '../client/canvasUtils';
|
||||
import { getUrl } from './rev';
|
||||
import { createFonts } from './fonts';
|
||||
|
||||
export function createSprite(x: number, y: number, w: number, h: number, ox: number, oy: number, type: number): Sprite {
|
||||
return { x, y, w, h, ox, oy, type };
|
||||
}
|
||||
|
||||
export function addTitles(sprites: ColorExtraSets, titles: string[]): ColorExtraSets {
|
||||
return sprites && sprites.map((ns, i) =>
|
||||
ns && ns.map(s => s && { color: s.color, colors: s.colors, title: titles[i], label: titles[i] }));
|
||||
}
|
||||
|
||||
export function addLabels(sprites: ColorExtraSets, labels: string[]) {
|
||||
sprites && sprites.forEach((s, i) => s && s[0] ? s[0]!.label = labels[i] : undefined);
|
||||
return sprites;
|
||||
}
|
||||
|
||||
export function createEyeSprite(eye: PonyEye | undefined, iris: number, defaultPalette: Uint32Array): ColorExtra | undefined {
|
||||
return eye && { color: eye.irises[iris]!, colors: 2, extra: eye.base, palettes: [defaultPalette] };
|
||||
}
|
||||
|
||||
export function getColorCount(sprite: ColorExtra | undefined): number {
|
||||
return sprite && sprite.colors ? Math.floor((sprite.colors - 1) / 2) : 0;
|
||||
}
|
||||
|
||||
export function createSpriteUtils() {
|
||||
createFonts();
|
||||
}
|
||||
|
||||
type LoadImage = (src: string) => Promise<HTMLImageElement | ImageBitmap>;
|
||||
|
||||
function getImageData(img: HTMLImageElement | ImageBitmap) {
|
||||
const canvas = createCanvas(img.width, img.height);
|
||||
const context = canvas.getContext('2d')!;
|
||||
context.drawImage(img, 0, 0);
|
||||
return context.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
function loadSpriteSheet(sheet: SpriteSheet, loadImage: LoadImage) {
|
||||
return Promise.all([
|
||||
loadImage(sheet.src!),
|
||||
sheet.srcA ? loadImage(sheet.srcA) : Promise.resolve(undefined)
|
||||
])
|
||||
.then(([img, imgA]) => {
|
||||
sheet.data = getImageData(img);
|
||||
|
||||
if (imgA) {
|
||||
const alpha = getImageData(imgA);
|
||||
const alphaData = alpha.data;
|
||||
const sheedData = sheet.data.data;
|
||||
|
||||
for (let i = 0; i < sheedData.length; i += 4) {
|
||||
sheedData[i + 3] = alphaData[i];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function loadSpriteSheets(sheets: SpriteSheet[], loadImage: LoadImage) {
|
||||
return Promise.all(sheets.map(s => loadSpriteSheet(s, loadImage))).then(noop);
|
||||
}
|
||||
|
||||
export let spriteSheetsLoaded = false;
|
||||
|
||||
export function loadAndInitSheets(sheets: SpriteSheet[], loadImage: LoadImage) {
|
||||
return loadSpriteSheets(sheets, loadImage)
|
||||
.then(createSpriteUtils)
|
||||
.then(() => true)
|
||||
.catch(e => (console.error(e), false))
|
||||
.then(loaded => spriteSheetsLoaded = loaded);
|
||||
}
|
||||
|
||||
export function loadImageFromUrl(url: string) {
|
||||
return loadImage(getUrl(url));
|
||||
}
|
||||
|
||||
export const loadAndInitSpriteSheets = once(() => loadAndInitSheets(spriteSheets, loadImageFromUrl));
|
||||
@@ -0,0 +1,602 @@
|
||||
import {
|
||||
PaletteManager, Season, TileSets, Region, TileType, Camera, PaletteSpriteBatch, DrawOptions, WorldMap, IMap,
|
||||
Sprite, MapType
|
||||
} from '../common/interfaces';
|
||||
import * as sprites from '../generated/sprites';
|
||||
import { getRegionTile, getRegionElevation } from '../common/region';
|
||||
import { getRegionGlobal } from '../common/worldMap';
|
||||
import { tileWidth, tileHeight, tileElevation, WATER_FPS, REGION_SIZE, WATER_HEIGHT } from '../common/constants';
|
||||
import { clamp, toInt, at, invalidEnumReturn } from '../common/utils';
|
||||
import { isAreaVisible } from '../common/camera';
|
||||
import { WHITE } from '../common/colors';
|
||||
import { releasePalette } from '../graphics/paletteManager';
|
||||
import { drawPixelText } from '../graphics/graphicsUtils';
|
||||
import { toScreenX, toScreenY, toWorldZ } from '../common/positionUtils';
|
||||
|
||||
const TILE_COUNTS = [[0, 4], [2, 3], [4, 3], [6, 3], [8, 3], [13, 3], [14, 3], [47, 4]];
|
||||
export const TILE_COUNT_MAP: number[] = [];
|
||||
export const TILE_MAP_MAP: number[] = [];
|
||||
|
||||
TILE_COUNTS.forEach(([tile, count]) => {
|
||||
while (TILE_COUNT_MAP.length < (tile + 1)) {
|
||||
TILE_COUNT_MAP.push(1);
|
||||
}
|
||||
|
||||
TILE_COUNT_MAP[tile] = count;
|
||||
});
|
||||
|
||||
let tileIndex = 0;
|
||||
|
||||
for (let i = 0; i <= 47; i++) {
|
||||
TILE_MAP_MAP.push(tileIndex);
|
||||
tileIndex += TILE_COUNT_MAP[i];
|
||||
}
|
||||
|
||||
// 1 | 2 | 4
|
||||
// ----+----+----
|
||||
// 8 | | 16
|
||||
// ----+----+----
|
||||
// 32 | 64 | 128
|
||||
export const TILE_MAP = [
|
||||
46, 46, 22, 22, 46, 46, 22, 22, 21, 21, // 0-9
|
||||
17, 11, 21, 21, 17, 11, 19, 19, 18, 18, // 10-19
|
||||
19, 19, 12, 12, 14, 14, 24, 28, 14, 14, // 20-29
|
||||
30, 6, 46, 46, 22, 22, 46, 46, 22, 22, // 30-39
|
||||
21, 21, 17, 11, 21, 21, 17, 11, 19, 19, // 40-49
|
||||
18, 18, 19, 19, 12, 12, 14, 14, 24, 28, // 50-59
|
||||
14, 14, 30, 6, 20, 20, 13, 13, 20, 20, // 60-69
|
||||
13, 13, 16, 16, 23, 32, 16, 16, 23, 32, // 70-79
|
||||
15, 15, 25, 25, 15, 15, 34, 34, 26, 26, // 80-89
|
||||
45, 41, 26, 26, 42, 36, 20, 20, 13, 13, // 90-99
|
||||
20, 20, 13, 13, 10, 10, 31, 4, 10, 10, // 100-109
|
||||
31, 4, 15, 15, 25, 25, 15, 15, 34, 34, // 110-119
|
||||
27, 27, 43, 37, 27, 27, 35, 5, 46, 46, // 120-129
|
||||
22, 22, 46, 46, 22, 22, 21, 21, 17, 11, // 130-139
|
||||
21, 21, 17, 11, 19, 19, 18, 18, 19, 19, // 140-149
|
||||
12, 12, 14, 14, 24, 28, 14, 14, 30, 6, // 150-159
|
||||
46, 46, 22, 22, 46, 46, 22, 22, 21, 21, // 160-169
|
||||
17, 11, 21, 21, 17, 11, 19, 19, 18, 18, // 170-179
|
||||
19, 19, 12, 12, 14, 14, 24, 28, 14, 14, // 180-189
|
||||
30, 6, 20, 20, 13, 13, 20, 20, 13, 13, // 190-199
|
||||
16, 16, 23, 32, 16, 16, 23, 32, 9, 9, // 200-209
|
||||
33, 33, 9, 9, 8, 8, 29, 29, 44, 39, // 210-219
|
||||
29, 29, 38, 7, 20, 20, 13, 13, 20, 20, // 220-229
|
||||
13, 13, 10, 10, 31, 4, 10, 10, 31, 4, // 230-239
|
||||
9, 9, 33, 33, 9, 9, 8, 8, 2, 2, // 240-249
|
||||
40, 3, 2, 2, 1, 0 // 250-255
|
||||
];
|
||||
|
||||
const enum TileTypeNumber {
|
||||
None = 0,
|
||||
Grass = 1,
|
||||
Water = 2,
|
||||
Wood = 3,
|
||||
GrassNew = 4,
|
||||
Water2 = 5,
|
||||
Water3 = 6,
|
||||
Water4 = 7,
|
||||
Ice = 8,
|
||||
SnowOnIce = 9,
|
||||
Stone = 10,
|
||||
Stone2 = 11,
|
||||
Boat = 12,
|
||||
}
|
||||
|
||||
const waterFrames: number[] = [
|
||||
TileTypeNumber.Water, TileTypeNumber.Water2, TileTypeNumber.Water3, TileTypeNumber.Water4
|
||||
];
|
||||
|
||||
export function updateTileSets(
|
||||
paletteManager: PaletteManager, tileSets: TileSets | undefined, season: Season, mapType: MapType
|
||||
) {
|
||||
if (tileSets) {
|
||||
tileSets.forEach(t => releasePalette(t.palette));
|
||||
}
|
||||
|
||||
return createTileSets(paletteManager, season, mapType);
|
||||
}
|
||||
|
||||
export function createTileSets(paletteManager: PaletteManager, season: Season, mapType: MapType): TileSets {
|
||||
const isWinter = season === Season.Winter;
|
||||
const isAutumn = season === Season.Autumn;
|
||||
const isCave = mapType === MapType.Cave;
|
||||
|
||||
const grassTiles = isCave ? sprites.caveTiles : (isWinter ? sprites.snowTiles : sprites.grassTiles);
|
||||
const grassPalette = grassTiles.palettes[isCave ? 0 : (isAutumn ? 1 : 0)];
|
||||
const icePaletteIndex = isWinter ? 2 : (isAutumn ? 1 : 0);
|
||||
const waterPaletteIndex = isCave ? 3 : (isWinter ? 2 : (isAutumn ? 1 : 0));
|
||||
const waterPalette = sprites.waterTiles1.palettes[waterPaletteIndex];
|
||||
|
||||
// indexes equal to TileTypeNumber values
|
||||
return [
|
||||
{ // 0
|
||||
sprites: [sprites.tile_none.color],
|
||||
palette: paletteManager.addArray(sprites.tile_none.palettes![0]),
|
||||
},
|
||||
{ // 1
|
||||
sprites: grassTiles.sprites,
|
||||
palette: paletteManager.addArray(grassPalette),
|
||||
},
|
||||
{ // 2
|
||||
sprites: sprites.waterTiles1.sprites,
|
||||
palette: paletteManager.addArray(waterPalette),
|
||||
},
|
||||
{ // 3
|
||||
sprites: sprites.woodTiles.sprites,
|
||||
palette: paletteManager.addArray(sprites.woodTiles.palettes[0]),
|
||||
},
|
||||
{ // 4
|
||||
sprites: sprites.grassTilesNew.sprites,
|
||||
palette: paletteManager.addArray(sprites.grassTilesNew.palettes[0]),
|
||||
},
|
||||
// water frames
|
||||
{ // 5
|
||||
sprites: sprites.waterTiles2.sprites,
|
||||
palette: paletteManager.addArray(waterPalette),
|
||||
},
|
||||
{ // 6
|
||||
sprites: sprites.waterTiles3.sprites,
|
||||
palette: paletteManager.addArray(waterPalette),
|
||||
},
|
||||
{ // 7
|
||||
sprites: sprites.waterTiles4.sprites,
|
||||
palette: paletteManager.addArray(waterPalette),
|
||||
},
|
||||
// ice
|
||||
{ // 8
|
||||
sprites: sprites.iceTiles.sprites,
|
||||
palette: paletteManager.addArray(sprites.iceTiles.palettes[icePaletteIndex]),
|
||||
},
|
||||
// snow on ice
|
||||
{ // 9
|
||||
sprites: sprites.snowOnIceTiles.sprites,
|
||||
palette: paletteManager.addArray(sprites.snowOnIceTiles.palettes[0]),
|
||||
},
|
||||
// stone
|
||||
{ // 10
|
||||
sprites: sprites.stoneTiles.sprites,
|
||||
palette: paletteManager.addArray(sprites.stoneTiles.palettes[0]),
|
||||
},
|
||||
// stone 2
|
||||
{ // 11
|
||||
sprites: sprites.stone2Tiles.sprites,
|
||||
palette: paletteManager.addArray(sprites.stone2Tiles.palettes[0]),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function drawTiles(
|
||||
batch: PaletteSpriteBatch, region: Region, camera: Camera, map: WorldMap, tileSets: TileSets, options: DrawOptions
|
||||
) {
|
||||
const { tileIndices } = region;
|
||||
const { tileTime } = map;
|
||||
const regionX = region.x * REGION_SIZE;
|
||||
const regionY = region.y * REGION_SIZE;
|
||||
|
||||
if (isAreaVisible(camera, regionX * tileWidth, regionY * tileHeight, REGION_SIZE * tileWidth, REGION_SIZE * tileHeight)) {
|
||||
const minX = clamp(Math.floor(camera.x / tileWidth - regionX), 0, REGION_SIZE);
|
||||
const minY = clamp(Math.floor(camera.actualY / tileHeight - regionY), 0, REGION_SIZE);
|
||||
const maxX = clamp(Math.ceil((camera.x + camera.w) / tileWidth - regionX), 0, REGION_SIZE);
|
||||
const maxY = clamp(Math.ceil((camera.actualY + camera.h) / tileHeight - regionY), 0, REGION_SIZE);
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const tileIndex = tileIndices[x | (y << 3)];
|
||||
|
||||
if (tileIndex === -1) {
|
||||
options.error(`Uninitialized tile index at (${x}, ${y}) ` +
|
||||
`region: (${region.x}, ${region.y}, ${region.tilesDirty}, ${region.lastTileUpdate}) ` +
|
||||
`now: ${performance.now()}`);
|
||||
region.tilesDirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const tileTypeNumber = tileIndex >>> 8;
|
||||
const isWater = tileTypeNumber === TileTypeNumber.Water || tileTypeNumber === TileTypeNumber.Boat;
|
||||
const tileSpriteIndex = tileIndex & 0xff;
|
||||
const tileSetIndex = isWater ? at(waterFrames, toInt(tileTime) % waterFrames.length)! : tileTypeNumber;
|
||||
const tileSet = tileSets[tileSetIndex];
|
||||
|
||||
if (!tileSet) {
|
||||
options.error(`Missing tileset: position: (${x}, ${y}) tile: (${getRegionTile(region, x, y)}) ` +
|
||||
`info: (${tileIndex}, ${tileSetIndex}, ${tileTime}, ${tileSpriteIndex}, ${JSON.stringify(waterFrames)})`);
|
||||
tileIndices[x | (y << 3)] = -1;
|
||||
region.tilesDirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const rx = (x + regionX) * tileWidth;
|
||||
const ry = (y + regionY) * tileHeight;
|
||||
|
||||
if (DEVELOPMENT && !tileSet.sprites[tileSpriteIndex]) {
|
||||
console.error('Missing sprite', tileSetIndex, tileSpriteIndex);
|
||||
}
|
||||
|
||||
batch.drawSprite(tileSet.sprites[tileSpriteIndex], WHITE, tileSet.palette, rx, ry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function drawTilesDebugInfo(batch: PaletteSpriteBatch, region: Region, camera: Camera, options: DrawOptions) {
|
||||
const { tileIndices } = region;
|
||||
const regionX = region.x * REGION_SIZE;
|
||||
const regionY = region.y * REGION_SIZE;
|
||||
|
||||
if (isAreaVisible(camera, regionX * tileWidth, regionY * tileHeight, REGION_SIZE * tileWidth, REGION_SIZE * tileHeight)) {
|
||||
const minX = clamp(Math.floor(camera.x / tileWidth - regionX), 0, REGION_SIZE);
|
||||
const minY = clamp(Math.floor(camera.actualY / tileHeight - regionY), 0, REGION_SIZE);
|
||||
const maxX = clamp(Math.ceil((camera.x + camera.w) / tileWidth - regionX), 0, REGION_SIZE);
|
||||
const maxY = clamp(Math.ceil((camera.actualY + camera.h) / tileHeight - regionY), 0, REGION_SIZE);
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const tileIndex = tileIndices[x | (y << 3)];
|
||||
|
||||
if (tileIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tileTypeNumber = tileIndex >>> 8;
|
||||
const tileSpriteIndex = tileIndex & 0xff;
|
||||
const rx = (x + regionX) * tileWidth;
|
||||
const ry = (y + regionY) * tileHeight;
|
||||
|
||||
if (options.tileIndices) {
|
||||
drawPixelText(batch, rx + 2, ry + 2, 0x000000ff, `${tileTypeNumber}:${tileSpriteIndex}`);
|
||||
drawPixelText(batch, rx + 2, ry + 2 + 7, 0x555555ff, `${region.tiles[x + REGION_SIZE * y]}`);
|
||||
}
|
||||
|
||||
if (options.tileGrid) {
|
||||
batch.drawRect(y !== 0 ? 0x00000011 : 0x00000022, rx, ry, tileWidth, 1);
|
||||
batch.drawRect(x !== 0 ? 0x00000011 : 0x00000022, rx, ry + 1, 1, tileHeight - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function drawTilesNew(
|
||||
batch: PaletteSpriteBatch, region: Region, camera: Camera, map: WorldMap, tileSets: TileSets, options: DrawOptions
|
||||
) {
|
||||
const regionX = region.x * REGION_SIZE;
|
||||
const regionY = region.y * REGION_SIZE;
|
||||
const TILE_COLOR = 0x666666ff;
|
||||
const TILE_FRONT_COLOR = 0x5e5e5eff;
|
||||
const OUTLINE_2_COLOR = 0xffffff22;
|
||||
const OUTLINE_COLOR = 0x00000022;
|
||||
|
||||
if (isAreaVisible(camera, regionX * tileWidth, regionY * tileHeight, REGION_SIZE * tileWidth, REGION_SIZE * tileHeight)) {
|
||||
const minX = clamp(Math.floor(camera.x / tileWidth - regionX), 0, REGION_SIZE);
|
||||
const minY = clamp(Math.floor(camera.y / tileHeight - regionY), 0, REGION_SIZE);
|
||||
const maxX = clamp(Math.ceil((camera.x + camera.w) / tileWidth - regionX), 0, REGION_SIZE);
|
||||
const maxY = clamp(Math.ceil((camera.y + camera.h) / tileHeight - regionY), 0, REGION_SIZE);
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const elevation = getRegionElevation(region, x, y);
|
||||
const cliffTop = getRegionElevation(region, x, y - 1) < elevation;
|
||||
const cliffLeft = getRegionElevation(region, x - 1, y) < elevation;
|
||||
const cliffRight = getRegionElevation(region, x + 1, y) < elevation;
|
||||
const cliffBottom = getRegionElevation(region, x, y + 1) < elevation;
|
||||
const elevDiff = Math.max(0, elevation - getRegionElevation(region, x, y + 1));
|
||||
const tx = (x + regionX) * tileWidth;
|
||||
const ty = (y + regionY) * tileHeight - elevation * tileElevation;
|
||||
|
||||
batch.drawRect(TILE_COLOR, tx, ty, tileWidth, tileHeight);
|
||||
|
||||
if (elevation) {
|
||||
batch.drawRect(TILE_FRONT_COLOR, tx, ty + tileHeight, tileWidth, elevDiff * tileElevation);
|
||||
|
||||
if (cliffLeft) {
|
||||
batch.drawRect(OUTLINE_COLOR, tx, ty + tileHeight, 1, elevation * tileElevation);
|
||||
}
|
||||
|
||||
if (cliffRight) {
|
||||
batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, ty + tileHeight, 1, elevation * tileElevation);
|
||||
}
|
||||
|
||||
if (cliffBottom) {
|
||||
batch.drawRect(OUTLINE_2_COLOR, tx, ty + tileHeight - 1, tileWidth, 1);
|
||||
batch.drawRect(OUTLINE_2_COLOR, tx, (ty + tileHeight + elevDiff * tileElevation) - 1, tileWidth, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (cliffTop) {
|
||||
batch.drawRect(OUTLINE_2_COLOR, tx, ty, tileWidth, 1);
|
||||
}
|
||||
|
||||
if (cliffLeft) {
|
||||
batch.drawRect(OUTLINE_2_COLOR, tx, ty, 1, tileHeight);
|
||||
}
|
||||
|
||||
if (cliffRight) {
|
||||
batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, ty, 1, tileHeight);
|
||||
}
|
||||
|
||||
if (cliffBottom) {
|
||||
batch.drawRect(OUTLINE_COLOR, tx, ty + tileHeight - 1, tileWidth, 1);
|
||||
}
|
||||
|
||||
if (options.gridLines) {
|
||||
batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, ty, 1, tileHeight);
|
||||
batch.drawRect(OUTLINE_COLOR, tx, ty + tileHeight - 1, tileWidth - 1, 1);
|
||||
// drawPixelText(batch, tx + 1, ty + 1, OUTLINE_COLOR, elevation.toString(10));
|
||||
}
|
||||
|
||||
const rx = x + regionX;
|
||||
const ry = y + regionY;
|
||||
|
||||
const tileIndex = region.tileIndices[x | (y << 3)];
|
||||
// const tileTypeNumber = tileIndex >> 8;
|
||||
const tileOffset = tileIndex & 0xff;
|
||||
const grass = tileSets[3];
|
||||
const baseX = (region.x * REGION_SIZE) | 0;
|
||||
const baseY = (region.y * REGION_SIZE) | 0;
|
||||
|
||||
if (getTileNormal(region.tiles, baseX, baseY, x, y, map, TileType.None) === TileType.Grass) {
|
||||
batch.drawSprite(grass.sprites[tileOffset], WHITE, grass.palette, rx * tileWidth, ry * tileHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTileIndices(region: Region, map: IMap<Region | undefined>) {
|
||||
for (let y = 0, i = 0; y < REGION_SIZE; y++) {
|
||||
for (let x = 0; x < REGION_SIZE; x++ , i++) {
|
||||
if (region.tileIndices[i] === -1) {
|
||||
region.tileIndices[i] = getTileIndex(region, i, x, y, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
region.tilesDirty = false;
|
||||
region.lastTileUpdate = performance.now();
|
||||
}
|
||||
|
||||
function tileTypeNumber(type: TileType) {
|
||||
switch (type) {
|
||||
case TileType.Water:
|
||||
case TileType.WalkableWater:
|
||||
return TileTypeNumber.Water;
|
||||
case TileType.Wood:
|
||||
return TileTypeNumber.Wood;
|
||||
case TileType.Ice:
|
||||
case TileType.WalkableIce:
|
||||
return TileTypeNumber.Ice;
|
||||
case TileType.SnowOnIce:
|
||||
return TileTypeNumber.SnowOnIce;
|
||||
case TileType.Stone:
|
||||
return TileTypeNumber.Stone;
|
||||
case TileType.Stone2:
|
||||
return TileTypeNumber.Stone2;
|
||||
case TileType.Boat:
|
||||
return TileTypeNumber.Boat;
|
||||
case TileType.Grass:
|
||||
case TileType.Dirt:
|
||||
case TileType.ElevatedDirt:
|
||||
return TileTypeNumber.Grass;
|
||||
case TileType.None:
|
||||
case TileType.WallH:
|
||||
case TileType.WallV:
|
||||
return TileTypeNumber.None;
|
||||
default:
|
||||
return invalidEnumReturn(type, TileTypeNumber.None);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTile(type: TileType, base: TileType) {
|
||||
switch (type) {
|
||||
case TileType.SnowOnIce:
|
||||
return base === TileType.SnowOnIce ? type : TileType.Ice;
|
||||
case TileType.WalkableIce:
|
||||
return TileType.Ice;
|
||||
case TileType.WalkableWater:
|
||||
case TileType.Boat:
|
||||
return TileType.Water;
|
||||
case TileType.ElevatedDirt:
|
||||
return TileType.Dirt;
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTileBase(type: TileType) {
|
||||
switch (type) {
|
||||
case TileType.WalkableIce:
|
||||
return TileType.Ice;
|
||||
case TileType.WalkableWater:
|
||||
case TileType.Boat:
|
||||
return TileType.Water;
|
||||
case TileType.ElevatedDirt:
|
||||
return TileType.Dirt;
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
function getTileNormal(
|
||||
tiles: Uint8Array, baseX: number, baseY: number, x: number, y: number, map: IMap<Region | undefined>, base: TileType
|
||||
) {
|
||||
if (x >= 0 && y >= 0 && x < REGION_SIZE && y < REGION_SIZE) {
|
||||
return normalizeTile(tiles[x | (y << 3)], base);
|
||||
} else {
|
||||
const mapX = clamp(x + baseX, 0, map.width - 1);
|
||||
const mapY = clamp(y + baseY, 0, map.height - 1);
|
||||
const region = getRegionGlobal(map, mapX, mapY);
|
||||
|
||||
if (region !== undefined) {
|
||||
const regionX = mapX - region.x * REGION_SIZE;
|
||||
const regionY = mapY - region.y * REGION_SIZE;
|
||||
return normalizeTile(region.tiles[regionX | (regionY << 3)], base);
|
||||
} else {
|
||||
return TileType.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTileIndex(region: Region, index: number, x: number, y: number, map: IMap<Region | undefined>): number {
|
||||
const tiles = region.tiles;
|
||||
const type = tiles[x | (y << 3)] as TileType;
|
||||
const tileType = tileTypeNumber(type);
|
||||
let baseTileIndex = 0;
|
||||
|
||||
if (type === TileType.Dirt || type === TileType.ElevatedDirt) {
|
||||
baseTileIndex = 47;
|
||||
} else if (type !== TileType.None) {
|
||||
let topLeft = 0, top = 0, topRight = 0, left = 0, right = 0, bottomLeft = 0, bottom = 0, bottomRight = 0;
|
||||
|
||||
if (x > 1 && y > 1 && x < (REGION_SIZE - 1) && y < (REGION_SIZE - 1)) {
|
||||
topLeft = normalizeTile(tiles[(x - 1) | (y - 1) << 3], type);
|
||||
top = normalizeTile(tiles[(x) | (y - 1) << 3], type);
|
||||
topRight = normalizeTile(tiles[(x + 1) | (y - 1) << 3], type);
|
||||
left = normalizeTile(tiles[(x - 1) | (y) << 3], type);
|
||||
right = normalizeTile(tiles[(x + 1) | (y) << 3], type);
|
||||
bottomLeft = normalizeTile(tiles[(x - 1) | (y + 1) << 3], type);
|
||||
bottom = normalizeTile(tiles[(x) | (y + 1) << 3], type);
|
||||
bottomRight = normalizeTile(tiles[(x + 1) | (y + 1) << 3], type);
|
||||
} else {
|
||||
const baseX = (region.x * REGION_SIZE) | 0;
|
||||
const baseY = (region.y * REGION_SIZE) | 0;
|
||||
topLeft = getTileNormal(tiles, baseX, baseY, x - 1, y - 1, map, type);
|
||||
top = getTileNormal(tiles, baseX, baseY, x, y - 1, map, type);
|
||||
topRight = getTileNormal(tiles, baseX, baseY, x + 1, y - 1, map, type);
|
||||
left = getTileNormal(tiles, baseX, baseY, x - 1, y, map, type);
|
||||
right = getTileNormal(tiles, baseX, baseY, x + 1, y, map, type);
|
||||
bottomLeft = getTileNormal(tiles, baseX, baseY, x - 1, y + 1, map, type);
|
||||
bottom = getTileNormal(tiles, baseX, baseY, x, y + 1, map, type);
|
||||
bottomRight = getTileNormal(tiles, baseX, baseY, x + 1, y + 1, map, type);
|
||||
}
|
||||
|
||||
const normalized = normalizeTileBase(type);
|
||||
const index = 0
|
||||
| ((topLeft === normalized) ? 1 : 0)
|
||||
| ((top === normalized) ? 2 : 0)
|
||||
| ((topRight === normalized) ? 4 : 0)
|
||||
| ((left === normalized) ? 8 : 0)
|
||||
| ((right === normalized) ? 16 : 0)
|
||||
| ((bottomLeft === normalized) ? 32 : 0)
|
||||
| ((bottom === normalized) ? 64 : 0)
|
||||
| ((bottomRight === normalized) ? 128 : 0);
|
||||
|
||||
baseTileIndex = TILE_MAP[index];
|
||||
}
|
||||
|
||||
const tileCount = type !== TileType.None ? TILE_COUNT_MAP[baseTileIndex] : 1;
|
||||
const tileIndex = TILE_MAP_MAP[baseTileIndex] + (tileCount > 1 ? region.randoms[index] % tileCount : 0);
|
||||
return (tileType << 8) | tileIndex;
|
||||
}
|
||||
|
||||
const tileIndices = [
|
||||
47, 47, 0, 0, 13, 19, 21, 20, 15, 16,
|
||||
47, 47, 0, 0, 13, 13, 45, 22, 18, 17,
|
||||
9, 2, 2, 2, 10, 14, 14, 14, 35, 36,
|
||||
8, 5, null, 7, 4, 27, 26, 29, 37, 38,
|
||||
8, null, 46, null, 4, 28, 24, 30, 39, 40,
|
||||
8, 3, null, 1, 4, 23, 31, 32, 41, 42,
|
||||
12, 6, 6, 6, 11, 25, 33, 34, 43, 44,
|
||||
];
|
||||
|
||||
let tileHeightMaps = new Map<number, number[]>();
|
||||
let tileHeightMapsInitialized = false;
|
||||
|
||||
function valueToHeight(value: number, bottom: number, top: number) {
|
||||
return bottom + ((value / 255) * (top - bottom));
|
||||
}
|
||||
|
||||
export function initializeTileHeightmaps() {
|
||||
if (tileHeightMapsInitialized)
|
||||
return;
|
||||
|
||||
function createTileHeightMaps(sprite: Sprite, tileType: TileTypeNumber, bottom: number, top: number) {
|
||||
const sheetData = sprites.normalSpriteSheet.data!;
|
||||
const tiles: number[][] = [];
|
||||
|
||||
for (let ty = 0; ty < 7; ty++) {
|
||||
for (let tx = 0; tx < 10; tx++) {
|
||||
const tile: number[] = [];
|
||||
const baseX = tx * tileWidth + sprite.x;
|
||||
const baseY = ty * tileHeight + sprite.y;
|
||||
|
||||
for (let y = 0, i = 0; y < tileHeight; y++) {
|
||||
for (let x = 0; x < tileWidth; x++ , i++) {
|
||||
const sx = baseX + x;
|
||||
const sy = baseY + y;
|
||||
const value = sheetData.data[(sx + sy * sheetData.width) * 4];
|
||||
tile.push(valueToHeight(value, bottom, top));
|
||||
}
|
||||
}
|
||||
|
||||
tiles.push(tile);
|
||||
}
|
||||
}
|
||||
|
||||
tileHeightMapsInitialized = true;
|
||||
|
||||
const counts = new Uint8Array(100);
|
||||
|
||||
for (let i = 0; i < tileIndices.length; i++) {
|
||||
const index = tileIndices[i];
|
||||
|
||||
if (index !== null) {
|
||||
const key = (tileType << 8) | (TILE_MAP_MAP[index] + counts[index]);
|
||||
tileHeightMaps.set(key, tiles[i]);
|
||||
counts[index]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createTileHeightMaps(sprites.dirt_water_heightmap, TileTypeNumber.Water, -0.25, 0);
|
||||
createTileHeightMaps(sprites.dirt_ice_heightmap, TileTypeNumber.Ice, -0.2, 0);
|
||||
createTileHeightMaps(sprites.dirt_stone_cave_height_map, TileTypeNumber.Grass, 0.2, 0);
|
||||
}
|
||||
|
||||
const waterHeight = WATER_HEIGHT.map(toWorldZ);
|
||||
|
||||
export function isInWater(tileIndex: number, x: number, y: number) {
|
||||
const tileType = (tileIndex & 0xff00) >> 8;
|
||||
|
||||
if (tileType === TileTypeNumber.Water) {
|
||||
const heightMaps = tileHeightMaps.get(tileIndex);
|
||||
|
||||
if (heightMaps !== undefined) {
|
||||
const tx = clamp(toScreenX(x - Math.floor(x)), 0, tileWidth - 1) | 0;
|
||||
const ty = clamp(toScreenY(y - Math.floor(y)), 0, tileHeight - 1) | 0;
|
||||
return heightMaps[tx + ty * tileWidth] === -0.25;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getTileHeight(
|
||||
tileType: TileType, tileIndex: number, x: number, y: number, gameTime: number, mapType: MapType
|
||||
) {
|
||||
const typeNumber = (tileIndex & 0xff00) >> 8;
|
||||
|
||||
if (
|
||||
typeNumber === TileTypeNumber.Ice ||
|
||||
typeNumber === TileTypeNumber.Water ||
|
||||
(mapType === MapType.Cave && tileType === TileType.Grass)
|
||||
) {
|
||||
if (tileType !== TileType.WalkableWater && tileType !== TileType.WalkableIce) {
|
||||
const heightMaps = tileHeightMaps.get(tileIndex);
|
||||
|
||||
if (heightMaps !== undefined) {
|
||||
const tx = clamp(toScreenX(x - Math.floor(x)), 0, tileWidth - 1) | 0;
|
||||
const ty = clamp(toScreenY(y - Math.floor(y)), 0, tileHeight - 1) | 0;
|
||||
return heightMaps[tx + ty * tileWidth];
|
||||
}
|
||||
}
|
||||
} else if (typeNumber === TileTypeNumber.SnowOnIce) {
|
||||
return -0.2;
|
||||
} else if (tileType === TileType.ElevatedDirt) {
|
||||
return 0.5;
|
||||
} else if (typeNumber === TileTypeNumber.Boat) {
|
||||
const frame = ((gameTime / 1000) * WATER_FPS) | 0;
|
||||
return waterHeight[frame % waterHeight.length];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
interface TimingEntry {
|
||||
time: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface TimingResult {
|
||||
name: string;
|
||||
count: number;
|
||||
selfTime: number;
|
||||
totalTime: number;
|
||||
selfPercent: number;
|
||||
totalPercent: number;
|
||||
}
|
||||
|
||||
const ENABLED = false;
|
||||
const ENTRIES_LIMIT = 8000;
|
||||
|
||||
const entries: TimingEntry[] = [];
|
||||
let entriesCount = 0;
|
||||
|
||||
if (TIMING && ENABLED) {
|
||||
for (let i = 0; i < ENTRIES_LIMIT; i++) {
|
||||
entries.push({ time: 0, name: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
export function timeStart(name: string) {
|
||||
if (TIMING && ENABLED) {
|
||||
if (entriesCount < ENTRIES_LIMIT) {
|
||||
const entry = entries[entriesCount];
|
||||
entry.time = performance.now();
|
||||
entry.name = name;
|
||||
entriesCount++;
|
||||
} else {
|
||||
console.warn(`exceeded timing entry limit`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function timeEnd() {
|
||||
if (TIMING && ENABLED) {
|
||||
if (entriesCount < ENTRIES_LIMIT) {
|
||||
const entry = entries[entriesCount];
|
||||
entry.time = performance.now();
|
||||
entry.name = undefined;
|
||||
entriesCount++;
|
||||
} else {
|
||||
console.warn(`exceeded timing entry limit`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function timeReset() {
|
||||
if (TIMING && ENABLED) {
|
||||
entriesCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function timingCollate(): TimingResult[] {
|
||||
if (TIMING && ENABLED && entriesCount > 0) {
|
||||
interface Entry extends TimingEntry {
|
||||
excludedTime: number;
|
||||
}
|
||||
|
||||
const listings: TimingResult[] = [];
|
||||
const startStack: Entry[] = [];
|
||||
|
||||
for (let i = 0; i < entriesCount; i++) {
|
||||
const entry = entries[i];
|
||||
|
||||
if (entry.name !== undefined) {
|
||||
startStack.push({ ...entry, excludedTime: 0 });
|
||||
} else {
|
||||
const start = startStack.pop()!;
|
||||
const name = start.name!;
|
||||
const time = entry.time - start.time;
|
||||
let listing = listings.find(l => l.name === name);
|
||||
|
||||
if (!listing) {
|
||||
listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 };
|
||||
listings.push(listing);
|
||||
}
|
||||
|
||||
listing.count++;
|
||||
listing.selfTime += (time - start.excludedTime);
|
||||
listing.totalTime += time;
|
||||
|
||||
if (startStack.length) {
|
||||
startStack[startStack.length - 1].excludedTime += time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const firstTime = entries[0].time;
|
||||
const lastTime = entries[entriesCount - 1].time;
|
||||
const totalTime = lastTime - firstTime;
|
||||
|
||||
for (const listing of listings) {
|
||||
listing.selfPercent = 100 * listing.selfTime / totalTime;
|
||||
listing.totalPercent = 100 * listing.totalTime / totalTime;
|
||||
}
|
||||
|
||||
return listings.sort((a, b) => b.selfTime - a.selfTime);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { spriteShader, paletteLayersShader, lightShader } from '../generated/shaders';
|
||||
import { FrameBuffer, createFrameBuffer, disposeFrameBuffer } from '../graphics/webgl/frameBuffer';
|
||||
import { SpriteSheet, CommonPalettes, PaletteManager, Camera } from '../common/interfaces';
|
||||
import { Shader, createShader, disposeShader } from '../graphics/webgl/shader';
|
||||
import { PaletteSpriteBatch, PALETTE_BATCH_BYTES_PER_VERTEX } from '../graphics/paletteSpriteBatch';
|
||||
import { getWebGLContext, getRenderTargetSize, unbindAllTexturesAndBuffers } from '../graphics/webgl/webglUtils';
|
||||
import { createCommonPalettes } from '../graphics/graphicsUtils';
|
||||
import { createTexturesForSpriteSheets, disposeTexturesForSpriteSheets } from '../graphics/spriteSheetUtils';
|
||||
import * as sprites from '../generated/sprites';
|
||||
import { SpriteBatch } from '../graphics/spriteBatch';
|
||||
import { BATCH_SIZE_MAX } from '../common/constants';
|
||||
|
||||
export interface WebGL {
|
||||
gl: WebGLRenderingContext;
|
||||
frameBuffer: FrameBuffer | undefined;
|
||||
frameBufferSheet: SpriteSheet;
|
||||
spriteShader: Shader;
|
||||
lightShader: Shader;
|
||||
paletteShader: Shader;
|
||||
spriteBatch: SpriteBatch;
|
||||
paletteBatch: PaletteSpriteBatch;
|
||||
palettes: CommonPalettes;
|
||||
failedFBO: boolean;
|
||||
renderer: string;
|
||||
}
|
||||
|
||||
const spriteShaderSource = spriteShader;
|
||||
const paletteShaderSource = paletteLayersShader;
|
||||
const lightShaderSource = lightShader;
|
||||
|
||||
function createIndices(capacity: number) {
|
||||
const numIndices = (capacity * 6) | 0;
|
||||
const indices = new Uint16Array(numIndices);
|
||||
|
||||
for (let i = 0, j = 0; i < numIndices; j = (j + 4) | 0) {
|
||||
indices[i++] = (j + 0) | 0;
|
||||
indices[i++] = (j + 1) | 0;
|
||||
indices[i++] = (j + 2) | 0;
|
||||
indices[i++] = (j + 0) | 0;
|
||||
indices[i++] = (j + 2) | 0;
|
||||
indices[i++] = (j + 3) | 0;
|
||||
}
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
export function initWebGL(canvas: HTMLCanvasElement, paletteManager: PaletteManager, camera: Camera): WebGL {
|
||||
const gl = getWebGLContext(canvas);
|
||||
return initWebGLResources(gl, paletteManager, camera);
|
||||
}
|
||||
|
||||
export function initWebGLResources(gl: WebGLRenderingContext, paletteManager: PaletteManager, camera: Camera): WebGL {
|
||||
let renderer = '';
|
||||
let failedFBO = false;
|
||||
let frameBuffer: FrameBuffer | undefined;
|
||||
let frameBufferSheet: SpriteSheet = { texture: undefined, sprites: [], palette: false };
|
||||
|
||||
try {
|
||||
const size = getRenderTargetSize(camera.w, camera.h);
|
||||
frameBuffer = createFrameBuffer(gl, size, size);
|
||||
frameBufferSheet.texture = frameBuffer.texture;
|
||||
} catch (e) {
|
||||
DEVELOPMENT && console.warn(e);
|
||||
failedFBO = true;
|
||||
}
|
||||
|
||||
createTexturesForSpriteSheets(gl, sprites.spriteSheets);
|
||||
const palettes = createCommonPalettes(paletteManager);
|
||||
|
||||
const paletteShader = createShader(gl, paletteShaderSource);
|
||||
const spriteShader = createShader(gl, spriteShaderSource);
|
||||
const lightShader = createShader(gl, lightShaderSource);
|
||||
|
||||
const VERTICES_PER_SPRITE = 4;
|
||||
const buffer = new ArrayBuffer(BATCH_SIZE_MAX * VERTICES_PER_SPRITE * PALETTE_BATCH_BYTES_PER_VERTEX);
|
||||
const vertexBuffer = gl.createBuffer();
|
||||
|
||||
if (!vertexBuffer) {
|
||||
throw new Error(`Failed to allocate vertex buffer`);
|
||||
}
|
||||
|
||||
const indexBuffer = gl.createBuffer();
|
||||
|
||||
if (!indexBuffer) {
|
||||
throw new Error(`Failed to allocate index buffer`);
|
||||
}
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, createIndices(BATCH_SIZE_MAX), gl.STATIC_DRAW);
|
||||
|
||||
const vertexBuffer2 = gl.createBuffer();
|
||||
|
||||
if (!vertexBuffer2) {
|
||||
throw new Error(`Failed to allocate vertex buffer (2)`);
|
||||
}
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer2);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW);
|
||||
|
||||
const spriteBatch = new SpriteBatch(gl, BATCH_SIZE_MAX, buffer, vertexBuffer2, indexBuffer);
|
||||
const paletteBatch = new PaletteSpriteBatch(gl, BATCH_SIZE_MAX, buffer, vertexBuffer, indexBuffer);
|
||||
spriteBatch.rectSprite = sprites.pixel;
|
||||
paletteBatch.rectSprite = sprites.pixel2;
|
||||
paletteBatch.defaultPalette = palettes.defaultPalette;
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
|
||||
|
||||
paletteManager.init(gl);
|
||||
|
||||
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
|
||||
if (debugInfo) {
|
||||
renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
||||
}
|
||||
|
||||
return {
|
||||
gl, paletteShader, spriteShader, lightShader, spriteBatch, paletteBatch,
|
||||
frameBuffer, frameBufferSheet, palettes, failedFBO, renderer,
|
||||
};
|
||||
}
|
||||
|
||||
export function disposeWebGL(webgl: WebGL) {
|
||||
const { gl } = webgl;
|
||||
|
||||
unbindAllTexturesAndBuffers(gl);
|
||||
disposeTexturesForSpriteSheets(gl, sprites.spriteSheets);
|
||||
disposeFrameBuffer(gl, webgl.frameBuffer);
|
||||
disposeShader(gl, webgl.lightShader);
|
||||
disposeShader(gl, webgl.spriteShader);
|
||||
disposeShader(gl, webgl.paletteShader);
|
||||
webgl.spriteBatch.dispose();
|
||||
webgl.paletteBatch.dispose();
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
BASE_CHARACTER_LIMIT, ADDITIONAL_CHARACTERS_SUPPORTER1, ADDITIONAL_CHARACTERS_SUPPORTER2,
|
||||
ADDITIONAL_CHARACTERS_SUPPORTER3, ADDITIONAL_CHARACTERS_PAST_SUPPORTER
|
||||
} from './constants';
|
||||
import { AccountDataFlags } from './interfaces';
|
||||
import { hasFlag } from './utils';
|
||||
|
||||
export interface AccountRoles {
|
||||
roles?: string[] | undefined;
|
||||
}
|
||||
|
||||
export interface AccountSupporter extends AccountRoles {
|
||||
supporter?: number | undefined;
|
||||
supporterInvited?: boolean;
|
||||
flags?: AccountDataFlags;
|
||||
}
|
||||
|
||||
export function hasRole(account: AccountRoles | undefined, role: string): boolean {
|
||||
return !!(account && account.roles && account.roles.indexOf(role) !== -1);
|
||||
}
|
||||
|
||||
export function isAdmin(account: AccountRoles): boolean {
|
||||
return hasRole(account, 'admin') || hasRole(account, 'superadmin');
|
||||
}
|
||||
|
||||
export function isMod(account: AccountRoles): boolean {
|
||||
return hasRole(account, 'mod') || isAdmin(account);
|
||||
}
|
||||
|
||||
export function isDev(account: AccountRoles): boolean {
|
||||
return hasRole(account, 'dev');
|
||||
}
|
||||
|
||||
export function meetsRequirement(account: AccountSupporter, require: string | undefined): boolean {
|
||||
return !require || hasRole(account, require) || meetsSupporterRequirement(account, require);
|
||||
}
|
||||
|
||||
function meetsSupporterRequirement(account: AccountSupporter, require: string): boolean {
|
||||
const level = account.supporter || 0;
|
||||
const modOrDev = isMod(account) || isDev(account);
|
||||
|
||||
if (require === 'inv') {
|
||||
return modOrDev || level >= 1 || !!account.supporterInvited;
|
||||
} else if (require === 'sup1') {
|
||||
return modOrDev || level >= 1;
|
||||
} else if (require === 'sup2') {
|
||||
return modOrDev || level >= 2;
|
||||
} else if (require === 'sup3') {
|
||||
return modOrDev || level >= 3;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCharacterLimit(account: AccountSupporter) {
|
||||
switch (account.supporter || 0) {
|
||||
case 1: return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_SUPPORTER1;
|
||||
case 2: return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_SUPPORTER2;
|
||||
case 3: return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_SUPPORTER3;
|
||||
default:
|
||||
if (hasFlag(account.flags, AccountDataFlags.PastSupporter)) {
|
||||
return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_PAST_SUPPORTER;
|
||||
} else {
|
||||
return BASE_CHARACTER_LIMIT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getSupporterInviteLimit(account: AccountSupporter) {
|
||||
if (isMod(account) || isDev(account)) {
|
||||
return 100;
|
||||
} else {
|
||||
switch (account.supporter) {
|
||||
case 1: return 1;
|
||||
case 2: return 5;
|
||||
case 3: return 10;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,923 @@
|
||||
import {
|
||||
PonyInfo, AccountSettings, AccountData, PonyObject, AccountCounters, ServerFeatureFlags, Dict, Subscription
|
||||
} from './interfaces';
|
||||
|
||||
export const ITEM_LIMIT = 1000;
|
||||
|
||||
export const ROLES = ['superadmin', 'admin', 'mod', 'dev'];
|
||||
|
||||
export const SERVER_LABELS: { [key: string]: string; } = {
|
||||
'dev': 'badge-test',
|
||||
'test': 'badge-test',
|
||||
'main': 'badge-none',
|
||||
'main-ru': 'badge-none',
|
||||
'safe': 'badge-success',
|
||||
'safe-ru': 'badge-success',
|
||||
'safe-pr': 'badge-success',
|
||||
'safe-sp': 'badge-success',
|
||||
};
|
||||
|
||||
export const enum Suspicious {
|
||||
No,
|
||||
Yes,
|
||||
Very,
|
||||
}
|
||||
|
||||
export const enum CharacterFlags {
|
||||
None = 0,
|
||||
BadCM = 1,
|
||||
HideSupport = 4,
|
||||
RespawnAtSpawn = 8,
|
||||
ForbiddenName = 16,
|
||||
}
|
||||
|
||||
// NOTE: also update createLoginServerStatus() (internal-login.ts)
|
||||
export interface GeneralSettings {
|
||||
isPageOffline?: boolean;
|
||||
|
||||
canCreateAccounts?: boolean;
|
||||
blockWebView?: boolean;
|
||||
reportPotentialDuplicates?: boolean;
|
||||
autoMergeDuplicates?: boolean;
|
||||
|
||||
suspiciousNames?: string;
|
||||
suspiciousPonies?: string;
|
||||
suspiciousMessages?: string;
|
||||
suspiciousSafeMessages?: string;
|
||||
suspiciousSafeWholeMessages?: string;
|
||||
suspiciousSafeInstantMessages?: string;
|
||||
suspiciousSafeInstantWholeMessages?: string;
|
||||
suspiciousAuths?: string;
|
||||
patreonToken?: string;
|
||||
}
|
||||
|
||||
// NOTE: also update createLoginServerStatus()
|
||||
export const LOGIN_SERVER_SETTINGS: { id: keyof GeneralSettings; label: string; }[] = [
|
||||
{ id: 'canCreateAccounts', label: 'Can create accounts' },
|
||||
{ id: 'blockWebView', label: 'Block web view' },
|
||||
{ id: 'reportPotentialDuplicates', label: 'Report potential duplicates' },
|
||||
{ id: 'autoMergeDuplicates', label: 'Auto-merge duplicates' },
|
||||
];
|
||||
|
||||
export interface ServerLiveSettings {
|
||||
updating: boolean;
|
||||
shutdown: boolean;
|
||||
}
|
||||
|
||||
export interface GameServerSettings {
|
||||
isServerOffline?: boolean;
|
||||
filterSwears?: boolean;
|
||||
autoBanSwearing?: boolean;
|
||||
autoBanSpamming?: boolean;
|
||||
doubleTimeouts?: boolean;
|
||||
reportSpam?: boolean;
|
||||
reportSwears?: boolean;
|
||||
reportTeleporting?: boolean;
|
||||
logLagging?: boolean;
|
||||
logTeleporting?: boolean;
|
||||
logFixingPosition?: boolean;
|
||||
hideSwearing?: boolean;
|
||||
kickSwearing?: boolean;
|
||||
kickSwearingToSpawn?: boolean;
|
||||
blockJoining?: boolean;
|
||||
kickTeleporting?: boolean;
|
||||
fixTeleporting?: boolean;
|
||||
kickLagging?: boolean;
|
||||
reportSitting?: boolean;
|
||||
}
|
||||
|
||||
export interface Settings extends GeneralSettings {
|
||||
servers: Dict<GameServerSettings>;
|
||||
}
|
||||
|
||||
export const SERVER_SETTINGS: { id: keyof GameServerSettings; label: string; }[] = [
|
||||
{ id: 'filterSwears', label: 'Swear filter' },
|
||||
{ id: 'autoBanSwearing', label: 'Auto-Timeout for swearing' },
|
||||
{ id: 'autoBanSpamming', label: 'Auto-Timeout for spam' },
|
||||
{ id: 'doubleTimeouts', label: 'Double timeouts duration' },
|
||||
{ id: 'reportSpam', label: 'Report spam' },
|
||||
{ id: 'reportSwears', label: 'Report swearing' },
|
||||
{ id: 'reportTeleporting', label: 'Report teleporting' },
|
||||
{ id: 'logLagging', label: 'Log lagging' },
|
||||
{ id: 'logTeleporting', label: 'Log teleporting' },
|
||||
{ id: 'logFixingPosition', label: 'Log fixing position' },
|
||||
{ id: 'hideSwearing', label: 'Hide swearing' },
|
||||
{ id: 'kickSwearing', label: 'Kick for swearing' },
|
||||
{ id: 'kickSwearingToSpawn', label: 'Reset swearing to spawn' },
|
||||
{ id: 'blockJoining', label: 'Block joining' },
|
||||
{ id: 'kickTeleporting', label: 'Kick teleporting players' },
|
||||
{ id: 'fixTeleporting', label: 'Fix teleporting players' },
|
||||
{ id: 'kickLagging', label: 'Kick lagging players' },
|
||||
{ id: 'reportSitting', label: 'Report sitting' },
|
||||
];
|
||||
|
||||
export interface InternalCommonApi {
|
||||
reloadSettings(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface InternalApi extends InternalCommonApi {
|
||||
state(): Promise<GameServerState>;
|
||||
stats(): Promise<ServerStats>;
|
||||
statsTable(stats: Stats): Promise<StatsTable>;
|
||||
action(action: string, accountId: string): Promise<void>;
|
||||
join(accountId: string, ponyId: string): Promise<string>;
|
||||
kick(accountId: string | undefined, characterId: string | undefined): Promise<boolean>;
|
||||
kickAll(): Promise<void>;
|
||||
accountChanged(accountId: string): Promise<void>;
|
||||
accountMerged(accountId: string, mergeId: string): Promise<void>;
|
||||
accountStatus(accountId: string): Promise<AccountStatus>;
|
||||
accountAround(accountId: string): Promise<AroundEntry[]>;
|
||||
accountHidden(accountId: string): Promise<HidingStats>;
|
||||
notifyUpdate(): Promise<void>;
|
||||
cancelUpdate(): Promise<void>;
|
||||
shutdownServer(value: boolean): Promise<void>;
|
||||
getTimings(): Promise<any[]>;
|
||||
teleportTo(adminAccountId: string, targetAccountId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface InternalLoginApi extends InternalCommonApi {
|
||||
state(): Promise<LoginServerStatus>;
|
||||
loginServerStats(): Promise<RequestStats[]>;
|
||||
updateLiveSettings(update: Partial<ServerLiveSettings>): Promise<void>;
|
||||
mergeAccounts(id: string, withId: string, reason: string, allowAdmin: boolean, creatingDuplicates: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
id: string;
|
||||
port: number;
|
||||
path: string;
|
||||
local: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
flag: string;
|
||||
host?: string;
|
||||
alert?: string;
|
||||
require?: string;
|
||||
flags: ServerFeatureFlags;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface GameServerState {
|
||||
id: string;
|
||||
path: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
flag: string;
|
||||
host?: string;
|
||||
alert?: string;
|
||||
require?: string;
|
||||
world?: {
|
||||
mapSize: number;
|
||||
regionSize: number;
|
||||
};
|
||||
flags: ServerFeatureFlags;
|
||||
|
||||
dead: boolean;
|
||||
maps: number;
|
||||
online: number;
|
||||
onMain: number;
|
||||
queued: number;
|
||||
shutdown: boolean;
|
||||
settings: GameServerSettings;
|
||||
}
|
||||
|
||||
export interface InternalServerState {
|
||||
id: string;
|
||||
api: InternalCommonApi;
|
||||
}
|
||||
|
||||
export interface InternalLoginServerState extends InternalServerState {
|
||||
api: InternalLoginApi;
|
||||
state: LoginServerStatus;
|
||||
}
|
||||
|
||||
export interface InternalGameServerState extends InternalServerState {
|
||||
api: InternalApi;
|
||||
state: GameServerState;
|
||||
}
|
||||
|
||||
export interface ServerStatus {
|
||||
diskSpace: string;
|
||||
memoryUsage: string;
|
||||
certificateExpiration: string;
|
||||
lastPatreonUpdate: string;
|
||||
}
|
||||
|
||||
export interface LoginServerStatus extends GeneralSettings {
|
||||
updating: boolean;
|
||||
dead: boolean;
|
||||
}
|
||||
|
||||
export interface AdminState {
|
||||
status: ServerStatus;
|
||||
loginServers: LoginServerStatus[];
|
||||
gameServers: GameServerState[];
|
||||
}
|
||||
|
||||
export interface MemoryStatus {
|
||||
total: number;
|
||||
used: number;
|
||||
free: number;
|
||||
}
|
||||
|
||||
export interface OriginInfoBase {
|
||||
ip: string;
|
||||
country: string;
|
||||
last?: Date;
|
||||
}
|
||||
|
||||
export interface MergeItemData {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface MergeHideData {
|
||||
id: string;
|
||||
name: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface MergeAccountData {
|
||||
name: string;
|
||||
note: string;
|
||||
flags: AccountFlags;
|
||||
emails: string[];
|
||||
ignores: string[];
|
||||
counters: AccountCounters;
|
||||
auths: MergeItemData[];
|
||||
characters: MergeItemData[];
|
||||
state: AccountState;
|
||||
birthdate?: Date;
|
||||
settings?: AccountSettings;
|
||||
friends?: string[];
|
||||
hides?: MergeHideData[];
|
||||
}
|
||||
|
||||
export interface MergeData {
|
||||
account: MergeAccountData;
|
||||
merge: MergeAccountData;
|
||||
}
|
||||
|
||||
export interface MergeInfo {
|
||||
_id?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
//code: number;
|
||||
date: Date;
|
||||
reason?: string;
|
||||
data?: MergeData;
|
||||
split?: boolean;
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
message: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface AccountDetails {
|
||||
merges: MergeInfo[];
|
||||
supporterLog: LogEntry[];
|
||||
banLog: LogEntry[];
|
||||
invitesReceived: SupporterInvite[];
|
||||
invitesSent: SupporterInvite[];
|
||||
state: AccountState;
|
||||
}
|
||||
|
||||
export interface AuthDetails {
|
||||
id: string;
|
||||
lastUsed: string | undefined;
|
||||
}
|
||||
|
||||
export interface BannedMuted {
|
||||
mute?: number;
|
||||
shadow?: number;
|
||||
ban?: number;
|
||||
}
|
||||
|
||||
export interface Document extends Timestamps {
|
||||
_id: string;
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
// bases
|
||||
|
||||
export interface TimestampsBase {
|
||||
createdAt?: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ChatMessageBase {
|
||||
createdAt: Date;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const accountCounters = [
|
||||
{ name: 'spam', label: 'spam' },
|
||||
{ name: 'swears', label: 'swearing' },
|
||||
{ name: 'timeouts', label: 'timeouts' },
|
||||
{ name: 'inviteLimit', label: 'party limits' },
|
||||
{ name: 'friendLimit', label: 'friend limits' },
|
||||
];
|
||||
|
||||
export const enum AccountFlags {
|
||||
None = 0,
|
||||
BlockPartyInvites = 1,
|
||||
CreatingDuplicates = 2,
|
||||
DuplicatesNotification = 4,
|
||||
BlockMerging = 16,
|
||||
BlockFriendRequests = 256,
|
||||
}
|
||||
|
||||
export const accountFlags = [
|
||||
{ value: AccountFlags.BlockPartyInvites, name: 'BlockPartyInvites', label: 'block party invites' },
|
||||
{ value: AccountFlags.CreatingDuplicates, name: 'CreatingDuplicates', label: 'creating duplicates' },
|
||||
{ value: AccountFlags.DuplicatesNotification, name: 'DuplicatesNotification', label: 'duplicates notification' },
|
||||
{ value: AccountFlags.BlockMerging, name: 'BlockMerging', label: 'block merging' },
|
||||
{ value: AccountFlags.BlockFriendRequests, name: 'BlockFriendRequests', label: 'block friend requests' },
|
||||
];
|
||||
|
||||
export const enum PatreonFlags {
|
||||
None = 0,
|
||||
Supporter1 = 1,
|
||||
Supporter2 = 2,
|
||||
Supporter3 = 3,
|
||||
}
|
||||
|
||||
export const enum SupporterFlags {
|
||||
None = 0,
|
||||
Supporter1 = 1,
|
||||
Supporter2 = 2,
|
||||
Supporter3 = 3,
|
||||
SupporterMask = 0x0003,
|
||||
IgnorePatreon = 0x0080,
|
||||
PastSupporter = 0x0100,
|
||||
ForcePastSupporter = 0x0200,
|
||||
IgnorePastSupporter = 0x0400,
|
||||
}
|
||||
|
||||
export const supporterFlags = [
|
||||
{ value: SupporterFlags.IgnorePatreon, label: 'ignore data from patreon' },
|
||||
];
|
||||
|
||||
// NOTE: also update mergeStates (merge.ts)
|
||||
export interface AccountState {
|
||||
gifts?: number;
|
||||
candies?: number;
|
||||
clovers?: number;
|
||||
toys?: number;
|
||||
eggs?: number;
|
||||
}
|
||||
|
||||
export interface AccountAlert {
|
||||
expires: Date;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AccountBase<ID> extends TimestampsBase, BannedMuted {
|
||||
name: string;
|
||||
birthdate?: Date;
|
||||
birthyear?: number;
|
||||
emails?: string[];
|
||||
roles: string[];
|
||||
origins: OriginInfoBase[];
|
||||
settings?: AccountSettings;
|
||||
note: string;
|
||||
noteUpdated?: Date;
|
||||
lastVisit: Date;
|
||||
lastUserAgent?: string;
|
||||
lastBrowserId?: string;
|
||||
lastOnline?: Date;
|
||||
lastCharacter?: ID;
|
||||
ignores?: string[];
|
||||
flags: AccountFlags;
|
||||
counters?: AccountCounters;
|
||||
characterCount: number;
|
||||
patreon?: PatreonFlags;
|
||||
supporter?: SupporterFlags;
|
||||
supporterLog?: LogEntry[];
|
||||
supporterTotal?: number;
|
||||
supporterDeclinedSince?: Date;
|
||||
merges?: MergeInfo[];
|
||||
banLog?: LogEntry[];
|
||||
state?: AccountState;
|
||||
alert?: AccountAlert;
|
||||
savedMap?: string;
|
||||
}
|
||||
|
||||
export interface AuthBase<ID> extends TimestampsBase {
|
||||
account?: ID;
|
||||
openId?: string;
|
||||
provider: string;
|
||||
name: string;
|
||||
url: string;
|
||||
emails?: string[];
|
||||
disabled?: boolean;
|
||||
banned?: boolean;
|
||||
pledged?: number;
|
||||
lastUsed?: Date;
|
||||
}
|
||||
|
||||
export interface OriginBase extends TimestampsBase, OriginInfoBase, BannedMuted {
|
||||
}
|
||||
|
||||
export const enum CharacterStateFlags {
|
||||
None = 0,
|
||||
Right = 1,
|
||||
Extra = 2,
|
||||
}
|
||||
|
||||
export interface CharacterState {
|
||||
x: number;
|
||||
y: number;
|
||||
map?: string;
|
||||
toy?: number;
|
||||
flags?: CharacterStateFlags;
|
||||
hold?: string;
|
||||
}
|
||||
|
||||
export interface CharacterBase<ID> extends TimestampsBase {
|
||||
account: ID;
|
||||
site?: ID;
|
||||
tag?: string;
|
||||
name: string;
|
||||
desc?: string;
|
||||
info?: string;
|
||||
flags: CharacterFlags;
|
||||
lastUsed?: Date;
|
||||
creator?: string;
|
||||
state?: { [key: string]: CharacterState | undefined; };
|
||||
}
|
||||
|
||||
export interface EventBase<ID> extends TimestampsBase {
|
||||
account?: ID;
|
||||
pony?: ID;
|
||||
type: string;
|
||||
server: string;
|
||||
message: string;
|
||||
desc: string;
|
||||
origin?: OriginInfoBase;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface SupporterInviteBase<ID> extends TimestampsBase {
|
||||
source: ID;
|
||||
target: ID;
|
||||
name: string;
|
||||
info: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface FriendRequestBase<ID> {
|
||||
source: ID;
|
||||
target: ID;
|
||||
}
|
||||
|
||||
export interface HideRequestBase<ID> {
|
||||
source: ID;
|
||||
target: ID;
|
||||
name: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export const eventFields: (keyof Event)[] = [
|
||||
'_id', 'updatedAt', 'createdAt', 'type', 'server', 'message', 'desc', 'count', 'origin', 'account', 'pony'
|
||||
];
|
||||
|
||||
// models
|
||||
|
||||
export interface OriginInfo extends OriginInfoBase {
|
||||
}
|
||||
|
||||
export interface Timestamps extends TimestampsBase {
|
||||
}
|
||||
|
||||
export interface AccountStatus {
|
||||
online: boolean;
|
||||
server?: string;
|
||||
map?: string;
|
||||
incognito?: boolean;
|
||||
character?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
userAgent?: string;
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
export interface DuplicatesInfo {
|
||||
count: number;
|
||||
name: boolean;
|
||||
emails: boolean;
|
||||
browserId: boolean;
|
||||
generatedAt: number;
|
||||
perma: boolean;
|
||||
}
|
||||
|
||||
export type ListListener<T> = (items: T[]) => void;
|
||||
|
||||
export interface IObservableList<T, V> {
|
||||
hasSubscribers(): boolean;
|
||||
trigger(): void;
|
||||
push(item: T): void;
|
||||
pushOrdered(item: T, compare: (a: T, b: T) => number): void;
|
||||
remove(item: T): boolean;
|
||||
replace(list: T[]): void;
|
||||
subscribe(listener: ListListener<V>): Subscription;
|
||||
}
|
||||
|
||||
export interface PonyIdDateName {
|
||||
id: string;
|
||||
date: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Account extends AccountBase<string>, Document {
|
||||
nameLower?: string;
|
||||
auths?: Auth[];
|
||||
originsRefs?: OriginRef[];
|
||||
ignoredByLimit?: number;
|
||||
ignoresLimit?: number;
|
||||
ignoresCount?: number;
|
||||
duplicatesLimit?: number;
|
||||
totalPledged?: number;
|
||||
|
||||
ponies?: Character[];
|
||||
invitesReceived?: SupporterInvite[];
|
||||
invitesSent?: SupporterInvite[];
|
||||
|
||||
authsList?: IObservableList<Auth, string>;
|
||||
poniesList?: IObservableList<Character, PonyIdDateName>;
|
||||
originsList?: IObservableList<OriginRef, OriginInfoBase>;
|
||||
}
|
||||
|
||||
export interface Auth extends AuthBase<string>, Document {
|
||||
}
|
||||
|
||||
export interface Character extends CharacterBase<string>, Document {
|
||||
ponyInfo?: PonyInfo;
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
export interface Origin extends OriginBase, Document {
|
||||
accounts?: Account[];
|
||||
accountsCount?: number;
|
||||
}
|
||||
|
||||
export interface OriginRef {
|
||||
origin: Origin;
|
||||
last: Date;
|
||||
}
|
||||
|
||||
export interface Event extends EventBase<string>, Document {
|
||||
deleted?: boolean;
|
||||
descHTML?: any;
|
||||
}
|
||||
|
||||
export interface ChatEvent {
|
||||
event: Event;
|
||||
account: Account | undefined;
|
||||
}
|
||||
|
||||
export interface SupporterInvite extends SupporterInviteBase<string>, Document {
|
||||
}
|
||||
|
||||
export interface FriendRequest extends FriendRequestBase<string>, Document {
|
||||
}
|
||||
|
||||
// other
|
||||
|
||||
export interface UpdateOrigin extends OriginInfo, BannedMuted {
|
||||
}
|
||||
|
||||
export interface AccountUpdate extends BannedMuted {
|
||||
age?: number;
|
||||
name?: string;
|
||||
note?: string;
|
||||
flags?: number;
|
||||
supporter?: number;
|
||||
}
|
||||
|
||||
export interface BaseValues {
|
||||
updatedAt?: string;
|
||||
createdAt?: string;
|
||||
lastVisit?: string;
|
||||
}
|
||||
|
||||
export interface LiveResponse {
|
||||
updates: any[][];
|
||||
deletes: string[];
|
||||
base: BaseValues;
|
||||
more: boolean;
|
||||
}
|
||||
|
||||
export interface RequestStats {
|
||||
path: string;
|
||||
count: number;
|
||||
average: string;
|
||||
total: string;
|
||||
order: string;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface UserCountStats {
|
||||
count: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface LoginStats {
|
||||
requests: RequestStats[];
|
||||
userCounts: UserCountStats[];
|
||||
}
|
||||
|
||||
export interface ItemCounts {
|
||||
accounts: number;
|
||||
characters: number;
|
||||
auths: number;
|
||||
origins: number;
|
||||
}
|
||||
|
||||
export interface FindPonyQuery {
|
||||
search?: string;
|
||||
orderBy?: string;
|
||||
}
|
||||
|
||||
export interface AuthUpdate {
|
||||
disabled?: boolean;
|
||||
banned?: boolean;
|
||||
pledged?: number;
|
||||
}
|
||||
|
||||
export interface AccountPonies {
|
||||
account: string;
|
||||
count: number;
|
||||
ponies: any[][];
|
||||
}
|
||||
|
||||
export interface AccountPoniesResponse {
|
||||
base: BaseValues;
|
||||
accounts: AccountPonies[];
|
||||
}
|
||||
|
||||
export interface PoniesResponse {
|
||||
base: BaseValues;
|
||||
ponies: any[][];
|
||||
}
|
||||
|
||||
export interface PonyCreator {
|
||||
_id: string;
|
||||
name: string;
|
||||
creator: string;
|
||||
}
|
||||
|
||||
export interface AccountOrigins {
|
||||
accountId: string;
|
||||
ips: string[];
|
||||
}
|
||||
|
||||
export interface ServerStats {
|
||||
actions: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
countBin: number;
|
||||
countStr: number;
|
||||
average: string;
|
||||
total: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface DuplicateInfoEntry {
|
||||
account: string;
|
||||
userAgent: string;
|
||||
ponies: string[];
|
||||
}
|
||||
|
||||
export interface AroundEntry {
|
||||
account: string;
|
||||
distance: number;
|
||||
party: boolean;
|
||||
}
|
||||
|
||||
export const enum Stats {
|
||||
Country,
|
||||
Support,
|
||||
Maps,
|
||||
}
|
||||
|
||||
export type StatsTable = string[][];
|
||||
|
||||
export interface OriginStats {
|
||||
uniqueOrigins: number;
|
||||
duplicateOrigins: number;
|
||||
singleOrigins: number;
|
||||
totalOrigins: number;
|
||||
totalOriginsIP4: number;
|
||||
totalOriginsIP6: number;
|
||||
distribution: number[];
|
||||
}
|
||||
|
||||
export interface OtherStats {
|
||||
totalIgnores: number;
|
||||
authsWithEmptyAccount: number;
|
||||
authsWithMissingAccount: number;
|
||||
}
|
||||
|
||||
export interface Around {
|
||||
account: Account;
|
||||
distance: number;
|
||||
party: boolean;
|
||||
}
|
||||
|
||||
export interface DuplicateBase {
|
||||
indenticalEmail: boolean;
|
||||
emails: number;
|
||||
origins: number;
|
||||
ponies?: string[];
|
||||
userAgent?: string;
|
||||
browserId?: boolean;
|
||||
birthdate: boolean;
|
||||
perma: boolean;
|
||||
name: number;
|
||||
note: number;
|
||||
lastVisit: Date;
|
||||
}
|
||||
|
||||
export interface DuplicateResult extends DuplicateBase {
|
||||
account: string;
|
||||
}
|
||||
|
||||
export interface Duplicate extends DuplicateBase {
|
||||
account: Account;
|
||||
}
|
||||
|
||||
export interface FindAccountQuery {
|
||||
search: string;
|
||||
showOnly: string;
|
||||
not: boolean;
|
||||
page: number;
|
||||
itemsPerPage: number;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export interface FindAccountResult {
|
||||
accounts: string[];
|
||||
page: number;
|
||||
totalItems: number;
|
||||
}
|
||||
|
||||
export interface AdminCacheEntry<T> {
|
||||
query: string;
|
||||
result: T;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface AdminCache {
|
||||
findAccounts?: AdminCacheEntry<Account[]>;
|
||||
}
|
||||
|
||||
export interface ClearOrignsOptions {
|
||||
old?: boolean;
|
||||
singles?: boolean;
|
||||
trim?: boolean;
|
||||
veryOld?: boolean;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export interface PatreonReward {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface PatreonPledge {
|
||||
user: string;
|
||||
reward: string;
|
||||
total: number;
|
||||
declinedSince?: string;
|
||||
account?: string;
|
||||
}
|
||||
|
||||
export interface PatreonData {
|
||||
rewards: PatreonReward[];
|
||||
pledges: PatreonPledge[];
|
||||
}
|
||||
|
||||
export interface HidingStats {
|
||||
account: string;
|
||||
hidden: string[];
|
||||
hiddenBy: string[];
|
||||
permaHidden: string[];
|
||||
permaHiddenBy: string[];
|
||||
}
|
||||
|
||||
export const enum TimingEntryType {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
export interface TimingEntry {
|
||||
type: TimingEntryType;
|
||||
time: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export type ModelTypes =
|
||||
'accounts' | 'auths' | 'origins' | 'ponies' | 'accountAuths' | 'accountOrigins' | 'accountPonies';
|
||||
|
||||
export interface IAdminServerActions {
|
||||
// subscribing
|
||||
subscribe(model: ModelTypes, id: string): void;
|
||||
unsubscribe(model: ModelTypes, id: string): void;
|
||||
// other
|
||||
getSignedAccount(): Promise<AccountData>;
|
||||
getCounts(): Promise<ItemCounts>;
|
||||
getState(): Promise<AdminState>;
|
||||
updateSettings(update: Partial<Settings>): Promise<void>;
|
||||
updateGameServerSettings(serverId: string, update: Partial<GameServerSettings>): Promise<void>;
|
||||
fetchServerStats(serverId: string): Promise<ServerStats>;
|
||||
fetchServerStatsTable(serverId: string, stats: Stats): Promise<StatsTable>;
|
||||
notifyUpdate(serverId: string): Promise<void>;
|
||||
shutdownServers(serverId: string): Promise<void>;
|
||||
resetUpdating(serverId: string): Promise<void>;
|
||||
report(accountId: string): Promise<void>;
|
||||
action(action: string, accountId: string): Promise<void>;
|
||||
kick(accountId: string): Promise<void>;
|
||||
kickAll(serverId: string): Promise<void>;
|
||||
getChat(search: string, date: string, caseInsensitive: boolean): Promise<string>;
|
||||
getChatForAccounts(accountIds: string[], date: string): Promise<string>;
|
||||
getRequestStats(): Promise<LoginStats>;
|
||||
updatePatreon(): Promise<void>;
|
||||
resetSupporter(accountId: string): Promise<void>;
|
||||
getLastPatreonData(): Promise<PatreonData | undefined>;
|
||||
updatePastSupporters(): Promise<void>;
|
||||
// live
|
||||
get(endPoint: 'events', id: string): Promise<any>;
|
||||
getAll(endPoint: 'events', timestamp?: string): Promise<LiveResponse>;
|
||||
assignAccount(endPoint: 'events', id: string, account: string): Promise<void>;
|
||||
removeItem(endPoint: 'events', id: string): Promise<void>;
|
||||
// events
|
||||
removeEvent(id: string): Promise<void>;
|
||||
// origins
|
||||
updateOrigin(origin: UpdateOrigin): Promise<void>;
|
||||
getOriginStats(): Promise<OriginStats>;
|
||||
getOtherStats(): Promise<OtherStats>;
|
||||
clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions): Promise<void>;
|
||||
clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions): Promise<void>;
|
||||
// characters
|
||||
getPony(id: string): Promise<Character | undefined>;
|
||||
getPonyInfo(id: string): Promise<PonyObject | null>;
|
||||
getPoniesCreators(accountId: string): Promise<PonyCreator[]>;
|
||||
getPoniesForAccount(accountId: string): Promise<Character[]>;
|
||||
getDetailsForAccount(accountId: string): Promise<AccountDetails>;
|
||||
findPonies(query: FindPonyQuery, page: number, skipTotalCount: boolean): Promise<{ items: string[]; totalCount: number; }>;
|
||||
createPony(account: string, name: string, info: string): Promise<void>;
|
||||
assignPony(ponyId: string, accountId: string): Promise<void>;
|
||||
removePony(id: string): Promise<void>;
|
||||
removePoniesAboveLimit(accountId: string): Promise<void>;
|
||||
removeAllPonies(accountId: string): Promise<void>;
|
||||
// auths
|
||||
getAuth(id: string): Promise<Auth | undefined>;
|
||||
getAuthsForAccount(accountId: string): Promise<Auth[]>;
|
||||
fetchAuthDetails(auths: string[]): Promise<AuthDetails[]>;
|
||||
updateAuth(id: string, update: AuthUpdate): Promise<void>;
|
||||
assignAuth(authId: string, accountId: string): Promise<void>;
|
||||
removeAuth(id: string): Promise<void>;
|
||||
// accounts
|
||||
getAccount(id: string): Promise<Account | undefined>;
|
||||
findAccounts(query: FindAccountQuery): Promise<FindAccountResult>;
|
||||
createAccount(name: string): Promise<string>;
|
||||
getAccountsByEmails(emails: string[]): Promise<Dict<string[]>>;
|
||||
getAccountsByOrigin(ip: string): Promise<string[]>;
|
||||
setName(accountId: string, name: string): Promise<void>;
|
||||
setAge(accountId: string, age: number): Promise<void>;
|
||||
setRole(accountId: string, role: string, set: boolean): Promise<void>;
|
||||
updateAccount(accountId: string, update: AccountUpdate, message?: string): Promise<void>;
|
||||
timeoutAccount(accountId: string, timeout: number): Promise<void>;
|
||||
updateAccountCounter(accountId: string, name: keyof AccountCounters, value: number): Promise<void>;
|
||||
removeAllOrigins(accountId: string): Promise<void>;
|
||||
removeOriginsForAccount(accountId: string, ips: string[]): Promise<void>;
|
||||
removeOriginsForAccounts(origins: AccountOrigins[]): Promise<void>;
|
||||
addOriginToAccount(accountId: string, origin: OriginInfo): Promise<void>;
|
||||
mergeAccounts(accountId: string, withId: string): Promise<void>;
|
||||
unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData): Promise<void>;
|
||||
addEmail(accountId: string, email: string): Promise<void>;
|
||||
removeEmail(accountId: string, email: string): Promise<void>;
|
||||
removeIgnore(accountId: string, ignore: string): Promise<void>;
|
||||
addIgnores(accountId: string, ignores: string[]): Promise<void>;
|
||||
removeFriend(accountId: string, friendId: string): Promise<void>;
|
||||
addFriend(accountId: string, friendId: string): Promise<void>;
|
||||
setAccountState(accountId: string, state: AccountState): Promise<void>;
|
||||
getAccountStatus(accountId: string): Promise<AccountStatus[]>;
|
||||
getAccountAround(accountId: string): Promise<AroundEntry[]>;
|
||||
getAccountHidden(accountId: string): Promise<HidingStats>;
|
||||
getAccountFriends(accountId: string): Promise<string[]>;
|
||||
removeAccount(accountId: string): Promise<void>;
|
||||
setAlert(accountId: string, message: string, expiresIn: number): Promise<void>;
|
||||
getIgnoresAndIgnoredBy(accountId: string): Promise<{ ignores: string[]; ignoredBy: string[]; }>;
|
||||
getAllDuplicatesQuickInfo(accountId: string): Promise<DuplicatesInfo>;
|
||||
getAllDuplicates(accountId: string): Promise<DuplicateResult[]>;
|
||||
getDuplicateEntries(force: boolean): Promise<string[]>;
|
||||
clearSessions(accountId: string): Promise<void>;
|
||||
// other
|
||||
getTimings(serverId: string): Promise<any[]>;
|
||||
teleportTo(accountId: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
import * as moment from 'moment';
|
||||
import { escape, escapeRegExp, startsWith, range, uniq, compact } from 'lodash';
|
||||
import { fromNow, toInt, hasFlag, compareDates, removeItem, includes } from './utils';
|
||||
import { DAY } from './constants';
|
||||
import {
|
||||
Account, OriginInfo, Document, OriginRef, SupporterFlags, BannedMuted, AccountBase, LogEntry,
|
||||
DuplicateResult, DuplicateBase, Duplicate, Auth, MergeInfo
|
||||
} from './adminInterfaces';
|
||||
import { hasRole } from './accountUtils';
|
||||
import { filterBadWordsPartial } from './swears';
|
||||
import { faPlusCircle, faClock, faMinusCircle, faCaretSquareUp, faCaretSquareDown } from '../client/icons';
|
||||
import { element, textNode } from '../client/htmlUtils';
|
||||
|
||||
interface UpdatedAt {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export const compareUpdatedAt = (a: UpdatedAt, b: UpdatedAt) => compareDates(a.updatedAt, b.updatedAt);
|
||||
export const compareOrigins = (a: OriginInfo, b: OriginInfo) => a.ip.localeCompare(b.ip);
|
||||
export const compareOriginRefs = (a: OriginRef, b: OriginRef) =>
|
||||
compareDates(b.last, a.last) || compareOrigins(a.origin, b.origin);
|
||||
export const compareByName = <T extends { name: string; }>(a: T, b: T) => (a.name || '').localeCompare(b.name || '');
|
||||
export const getId = (item: Document) => item._id;
|
||||
export const tagBad = (s: string) => `<span class='bad'>${s}</span>`;
|
||||
|
||||
export function compareAccounts(a: Account, b: Account) {
|
||||
return compareDates(a.createdAt, b.createdAt);
|
||||
}
|
||||
|
||||
export function compareAuths(a: Auth, b: Auth) {
|
||||
const aDeleted = a.disabled || a.banned || false;
|
||||
const bDeleted = b.disabled || b.banned || false;
|
||||
|
||||
if (aDeleted && !bDeleted) {
|
||||
return 1;
|
||||
} else if (!aDeleted && bDeleted) {
|
||||
return -1;
|
||||
} else {
|
||||
return compareByName(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
export function highlightWords(text?: string) {
|
||||
text = text || '';
|
||||
text = filterBadWordsPartial(text, tagBad);
|
||||
return text;
|
||||
}
|
||||
|
||||
export function getAge(birthdate: Date) {
|
||||
return moment().diff(birthdate, 'years');
|
||||
}
|
||||
|
||||
// chat & events
|
||||
|
||||
function enc(text?: string): string {
|
||||
return escape(text || '');
|
||||
}
|
||||
|
||||
function encWithHighlight(text?: string): string {
|
||||
return highlightWords(enc(text || ''));
|
||||
}
|
||||
|
||||
export function formatEventDesc(text: string): string {
|
||||
return encWithHighlight(text).replace(/\[([a-z0-f]{24})\]/g, `<a tabindex onclick="goToAccount('$1')">[$1]</a>`);
|
||||
}
|
||||
|
||||
function getMessageTag(message: string) {
|
||||
if (/^\/p /.test(message)) {
|
||||
return 'party';
|
||||
} else if (/^\/w /.test(message)) {
|
||||
return 'whisper';
|
||||
} else if (/^\/s[s123] /.test(message)) {
|
||||
return 'supporter';
|
||||
} else if (/^\//.test(message)) {
|
||||
return 'command';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceSwears(element: HTMLElement) {
|
||||
const text = element.textContent;
|
||||
|
||||
if (text) {
|
||||
const replaced = encWithHighlight(text);
|
||||
|
||||
if (text !== replaced) {
|
||||
element.innerHTML = replaced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatChatLine(l: string): HTMLElement {
|
||||
// 00:00:01 [system] Timed out for swearing
|
||||
// 00:00:01 [patreon] fetched patreon data
|
||||
// 00:00:01 [dev][Autumn Leafs] hello world
|
||||
// 00:00:01 [dev][Autumn Leafs][muted] hello world
|
||||
// 00:00:01 [dev][Autumn Leafs][ignored] hello world
|
||||
// 00:00:01 [dev-pl][Autumn Leafs][ignored] hello world
|
||||
// 00:00:01 [57a3dc6f2f0019a161cdebf6][dev][Autumn Leafs][ignored] hello world
|
||||
// 00:00:01 [1][dev][Autumn Leafs][ignored] hello world
|
||||
// 00:00:01 [1:merged][dev][Autumn Leafs][ignored] hello world
|
||||
// 00:00:01 [merged][dev][Autumn Leafs][ignored] hello world
|
||||
// 00:00:01 [merged][dev][main][Autumn Leafs][ignored] hello world
|
||||
|
||||
/* tslint:disable:max-line-length */
|
||||
const regex = /^([0-9:]+) (\[(?:merged|\d+|\d+:merged|[a-z0-9]{24})\])?\[([a-z0-9_-]+)\](?:\[([a-z0-9_-]+)\])?((?:\[.*?\])?)(?:\[(muted|ignored|ignorepub)\])?\t(.*)$/;
|
||||
const m = regex.exec(l);
|
||||
|
||||
if (m) {
|
||||
const [, time, accountId, server, map, name, mutedIgnored, message] = m;
|
||||
const messageTag = server === 'system' ? 'system' : getMessageTag(message);
|
||||
const modTag = mutedIgnored ? ' message-muted' : '';
|
||||
|
||||
return element('div', 'chatlog-line', [
|
||||
element('span', 'time', [], { 'data-text': time }),
|
||||
accountId ? element('span', 'account-id', [textNode(accountId)]) : undefined,
|
||||
element('span', `server server-${server.replace(/-.+$/g, '')}`, [textNode(`[${server}]`)]),
|
||||
map ? element('span', `map map-${map}`, [textNode(`[${map}]`)]) : undefined,
|
||||
element('span', mutedIgnored ? `name ${mutedIgnored}` : `name`, [textNode(name)]),
|
||||
textNode(' '),
|
||||
element('span', `message message-${messageTag}${modTag}`, [textNode(message)]),
|
||||
textNode(' '),
|
||||
element('a', 'chat-translate', [], undefined, { click: translateChat }),
|
||||
]);
|
||||
} else {
|
||||
return element('div', '', [textNode(highlightWords(l))]);
|
||||
}
|
||||
}
|
||||
|
||||
function translateChat(this: HTMLElement) {
|
||||
const lines: string[] = [];
|
||||
let parent = this.parentElement;
|
||||
|
||||
for (let i = 0; parent && i < 10; i++) {
|
||||
lines.push(parent.querySelector('.message')!.textContent!);
|
||||
parent = parent.nextElementSibling as HTMLElement;
|
||||
}
|
||||
|
||||
window.open(`https://translate.google.com/#auto/en/${encodeURIComponent(lines.join('\n'))}`);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).goToAccount = (accountId: string) => {
|
||||
window.dispatchEvent(new CustomEvent('go-to-account', { detail: accountId }));
|
||||
};
|
||||
}
|
||||
|
||||
export function formatChat(chat: string): HTMLElement[] {
|
||||
return (chat || '<no messages>')
|
||||
.trim()
|
||||
.split(/\r?\n/g)
|
||||
.reverse()
|
||||
.map(formatChatLine);
|
||||
}
|
||||
|
||||
export interface ChatDate {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function createChatDate(date: moment.Moment): ChatDate {
|
||||
return {
|
||||
value: date.toISOString(),
|
||||
label: date.format('MMMM Do YYYY'),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDateRange(startDate: string | Date, days: number): ChatDate[] {
|
||||
return range(days, 0)
|
||||
.map(d => moment(startDate).subtract(d, 'days'))
|
||||
.map(createChatDate);
|
||||
}
|
||||
|
||||
// filtering
|
||||
|
||||
export function filterAccounts(items: Account[], search: string, showOnly: string, not: boolean) {
|
||||
if (search) {
|
||||
items = items.filter(createFilter(search));
|
||||
}
|
||||
|
||||
const filter = createFilter2(showOnly);
|
||||
|
||||
if (filter) {
|
||||
if (not) {
|
||||
items = items.filter(i => !filter(i));
|
||||
} else {
|
||||
items = items.filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function createFilter(search: string): (account: Account) => boolean {
|
||||
const regex = new RegExp(escapeRegExp(search), 'i');
|
||||
|
||||
function test(value: string): boolean {
|
||||
return !!value && regex.test(value);
|
||||
}
|
||||
|
||||
function testAuth(auth: Auth) {
|
||||
return test(auth.name) || auth.provider === search || auth.url === search;
|
||||
}
|
||||
|
||||
function testMerge(merge: MergeInfo) {
|
||||
return merge.id === search;
|
||||
}
|
||||
|
||||
function filter(account: Account): boolean {
|
||||
if (account._id === search)
|
||||
return true;
|
||||
if (test(account.name))
|
||||
return true;
|
||||
if (test(account.note))
|
||||
return true;
|
||||
if (account.roles && account.roles.some(test))
|
||||
return true;
|
||||
if (account.emails && account.emails.some(test))
|
||||
return true;
|
||||
if (account.auths && account.auths.some(testAuth))
|
||||
return true;
|
||||
if (account.merges && account.merges.some(testMerge))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function prefixWith(prefix: string, action: (phrase: string) => (account: Account) => boolean) {
|
||||
return startsWith(search, prefix) ? action(search.substr(prefix.length)) : undefined;
|
||||
}
|
||||
|
||||
function prefixWithRegex(prefix: string, action: (regex: RegExp) => (account: Account) => boolean) {
|
||||
return prefixWith(prefix, phrase => action(new RegExp(escapeRegExp(phrase), 'i')));
|
||||
}
|
||||
|
||||
function prefixWithNumber(prefix: string, action: (count: number) => (account: Account) => boolean) {
|
||||
return prefixWith(prefix, phrase => action(+phrase));
|
||||
}
|
||||
|
||||
const exactMatch = (phrase: string) => (account: Account) => account.nameLower === phrase;
|
||||
const isOld = (max: number) => (account: Account) => !account.lastVisit || account.lastVisit.getTime() < max;
|
||||
|
||||
return prefixWithRegex('name:', regex => account => regex.test(account.name))
|
||||
|| prefixWithRegex('note:', regex => account => regex.test(account.note))
|
||||
|| prefixWithRegex('email:', regex => account => !!account.emails && account.emails.some(e => regex.test(e)))
|
||||
|| prefixWith('role:', role => account => hasRole(account, role))
|
||||
|| prefixWith('exact:', phrase => exactMatch(phrase.toLowerCase()))
|
||||
|| prefixWith('disabled!', () => account => !!account.auths && account.auths.some(a => !!a.disabled))
|
||||
|| prefixWith('locked!', () => account => !!account.auths && account.auths.some(a => !!a.banned))
|
||||
|| prefixWithNumber('ignores:', count => account => (account.ignoresCount || 0) >= count)
|
||||
|| prefixWithNumber('ponies:', count => account => account.characterCount >= count)
|
||||
|| prefixWithNumber('auths:', count => account => !!account.auths && account.auths.length >= count)
|
||||
|| prefixWithNumber('old:', days => isOld(fromNow(-days * DAY).getTime()))
|
||||
|| prefixWithNumber('spam:', count => account => !!account.counters && account.counters.spam! >= count)
|
||||
|| prefixWithNumber('swearing:', count => account => !!account.counters && account.counters.swears! >= count)
|
||||
|| prefixWithNumber('timeouts:', count => account => !!account.counters && account.counters.timeouts! >= count)
|
||||
|| prefixWithNumber('limits:', count => account => !!account.counters && account.counters.inviteLimit! >= count)
|
||||
|| filter;
|
||||
}
|
||||
|
||||
function hasAnyBan(account: Account) {
|
||||
return isBanned(account) || isMuted(account) || isShadowed(account);
|
||||
}
|
||||
|
||||
export function createPotentialDuplicatesFilter(getAccountsByBrowserId: (id: string) => Account[] | undefined): (account: Account) => boolean {
|
||||
return i => {
|
||||
const name = i.nameLower;
|
||||
|
||||
if (name === 'anonymous' || !i.lastBrowserId)
|
||||
return false;
|
||||
|
||||
const accounts = getAccountsByBrowserId(i.lastBrowserId);
|
||||
|
||||
if (accounts !== undefined && accounts.length > 1) {
|
||||
for (const a of accounts) {
|
||||
if (a !== i && a.nameLower === name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export function createFilter2(showOnly: string): ((account: Account) => boolean) | undefined {
|
||||
const now = Date.now();
|
||||
|
||||
if (showOnly === 'banned') {
|
||||
return hasAnyBan;
|
||||
} else if (showOnly === 'timed out') {
|
||||
return i => !!((i.mute && i.mute > now) || (i.shadow && i.shadow > now) || (i.ban && i.ban > now));
|
||||
} else if (showOnly === 'with flags') {
|
||||
return i => !!i.flags;
|
||||
} else if (showOnly === 'notes') {
|
||||
return i => !!i.note;
|
||||
} else if (showOnly === 'supporters') {
|
||||
return i => !!(i.patreon || i.supporter || i.supporterDeclinedSince);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPotentialDuplicates(account: Account, getAccountsByBrowserId: (id: string) => Account[] | undefined) {
|
||||
const accounts = account.lastBrowserId ? getAccountsByBrowserId(account.lastBrowserId) : undefined;
|
||||
const name = account.nameLower;
|
||||
|
||||
if (accounts !== undefined && accounts.length > 1 && name !== 'anonymous') {
|
||||
return accounts.filter(a => a !== account && a.nameLower === name);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// duplicates
|
||||
|
||||
export function compareDuplicates(a: DuplicateBase, b: DuplicateBase): number {
|
||||
if (a.note !== b.note)
|
||||
return b.note - a.note;
|
||||
if (a.emails !== b.emails)
|
||||
return b.emails - a.emails;
|
||||
if (a.name !== b.name)
|
||||
return b.name - a.name;
|
||||
if (a.browserId !== b.browserId)
|
||||
return a.browserId ? -1 : 1;
|
||||
if (a.origins !== b.origins)
|
||||
return b.origins - a.origins;
|
||||
if (a.ponies !== b.ponies)
|
||||
return (b.ponies ? b.ponies.length : 0) - (a.ponies ? a.ponies.length : 0);
|
||||
return b.lastVisit.getTime() - a.lastVisit.getTime();
|
||||
}
|
||||
|
||||
export function emailName(email: string): string {
|
||||
return email.substr(0, email.indexOf('@')).toLowerCase();
|
||||
}
|
||||
|
||||
export function createEmailMatcher(emails: string[]): ((email: string) => boolean) | undefined {
|
||||
if (!emails || !emails.length) {
|
||||
return undefined;
|
||||
} else {
|
||||
const match = emails.map(emailName).map(escapeRegExp).join('|');
|
||||
const regex = new RegExp(`^(?:${match})@`, 'i');
|
||||
return email => regex.test(email);
|
||||
}
|
||||
}
|
||||
|
||||
export function createDuplicate(account: Account, base: Account): Duplicate {
|
||||
const indenticalEmail = account.emails && base.emails && account.emails.some(e => base.emails!.indexOf(e) !== -1);
|
||||
|
||||
const isMatch = createEmailMatcher(base.emails || []);
|
||||
const duplicateEmails = isMatch && account.emails
|
||||
&& account.emails.reduce((sum, e) => sum + (isMatch(e) ? 1 : 0), 0);
|
||||
|
||||
const duplicateOrigins = base.originsRefs && account.originsRefs
|
||||
&& account.originsRefs.reduce((sum, o) => sum + (base.originsRefs!.some(r => o.origin.ip === r.origin.ip) ? 1 : 0), 0);
|
||||
|
||||
const name = account.nameLower !== 'anonymous' && account.nameLower === base.nameLower;
|
||||
|
||||
const note = (account.note && account.note.indexOf(base._id) !== -1)
|
||||
|| (base.note && base.note.indexOf(account._id) !== -1);
|
||||
|
||||
const browserId = !!account.lastBrowserId && account.lastBrowserId === base.lastBrowserId;
|
||||
const birthdate = !!(base.birthdate && account.birthdate && base.birthdate.getTime() === account.birthdate.getTime());
|
||||
|
||||
return {
|
||||
account,
|
||||
name: name ? 1 : 0,
|
||||
note: note ? 1 : 0,
|
||||
indenticalEmail: !!indenticalEmail,
|
||||
emails: toInt(duplicateEmails),
|
||||
origins: toInt(duplicateOrigins),
|
||||
lastVisit: account.lastVisit || new Date(0),
|
||||
browserId,
|
||||
birthdate,
|
||||
perma: isPermaBanned(account) || isPermaShadowed(account),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDuplicateResult(account: Account, base: Account): DuplicateResult {
|
||||
return { ...createDuplicate(account, base), account: account._id };
|
||||
}
|
||||
|
||||
export function pushOrdered<T>(items: T[], item: T, compare: (a: T, b: T) => number) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (compare(items[i], item) >= 0) {
|
||||
items.splice(i, 0, item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
export function duplicatesCollector(duplicates: string[]) {
|
||||
const set = new Set();
|
||||
|
||||
return (item: string) => {
|
||||
if (set.has(item)) {
|
||||
duplicates.push(item);
|
||||
} else {
|
||||
set.add(item);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function patreonSupporterLevel(account: AccountBase<any>) {
|
||||
return account.patreon! & 0xf;
|
||||
}
|
||||
|
||||
export function supporterLevel(account: AccountBase<any>) {
|
||||
const flags = account.supporter!;
|
||||
const ignore = hasFlag(flags, SupporterFlags.IgnorePatreon);
|
||||
const patreonSupporter = patreonSupporterLevel(account);
|
||||
const flagsSupporter = flags & 0xf;
|
||||
return Math.max(ignore ? 0 : patreonSupporter, flagsSupporter);
|
||||
}
|
||||
|
||||
export function isPastSupporter(account: AccountBase<any>) {
|
||||
const flags = account.supporter!;
|
||||
return (hasFlag(flags, SupporterFlags.PastSupporter) || hasFlag(flags, SupporterFlags.ForcePastSupporter)) &&
|
||||
!hasFlag(flags, SupporterFlags.IgnorePastSupporter);
|
||||
}
|
||||
|
||||
const fieldToAction: { [key: string]: string | undefined; } = {
|
||||
mute: 'Muted',
|
||||
shadow: 'Shadowed',
|
||||
ban: 'Banned',
|
||||
};
|
||||
|
||||
export function banMessage(field: string, value: number) {
|
||||
const action = fieldToAction[field] || 'Did';
|
||||
|
||||
if (value === 0) {
|
||||
return `Un${action.toLowerCase()}`;
|
||||
} else if (value === -1) {
|
||||
return action;
|
||||
} else {
|
||||
return `${action} for (${moment.duration(value - Date.now()).humanize()})`;
|
||||
}
|
||||
}
|
||||
|
||||
export function isActive(value: number | undefined): boolean {
|
||||
return !!value && (value === -1 || value > Date.now());
|
||||
}
|
||||
|
||||
export function isPerma(value: number | undefined): boolean {
|
||||
return value === -1;
|
||||
}
|
||||
|
||||
export function isTemporarilyActive(value: number | undefined): boolean {
|
||||
return !!value && value > Date.now();
|
||||
}
|
||||
|
||||
export function isMuted(account: BannedMuted): boolean {
|
||||
return isActive(account.mute);
|
||||
}
|
||||
|
||||
export function isShadowed(account: BannedMuted): boolean {
|
||||
return isActive(account.shadow);
|
||||
}
|
||||
|
||||
export function isBanned(account: BannedMuted): boolean {
|
||||
return isActive(account.ban);
|
||||
}
|
||||
|
||||
export function isPermaShadowed(account: BannedMuted): boolean {
|
||||
return isPerma(account.shadow);
|
||||
}
|
||||
|
||||
export function isPermaBanned(account: BannedMuted): boolean {
|
||||
return isPerma(account.ban);
|
||||
}
|
||||
|
||||
export function isTemporarilyBanned(account: BannedMuted): boolean {
|
||||
return isTemporarilyActive(account.ban);
|
||||
}
|
||||
|
||||
export interface SupporterChange {
|
||||
message: string;
|
||||
date: Date;
|
||||
icon: any;
|
||||
class: string;
|
||||
}
|
||||
|
||||
export function createSupporterChanges(entries: LogEntry[]): SupporterChange[] {
|
||||
const changes = entries.map(l => ({
|
||||
message: l.message,
|
||||
level: +((/\d+/.exec(l.message) || ['0'])[0]),
|
||||
added: /added/i.test(l.message),
|
||||
date: new Date(l.date),
|
||||
icon: /added/i.test(l.message) ? faPlusCircle : (/decline/i.test(l.message) ? faClock : faMinusCircle),
|
||||
class: /added/i.test(l.message) ? 'text-success' : (/decline/i.test(l.message) ? 'text-warning' : 'text-danger'),
|
||||
}));
|
||||
|
||||
for (let i = 1; i < changes.length; i++) {
|
||||
const prev = changes[i - 1];
|
||||
const current = changes[i];
|
||||
|
||||
if (current.date.getMonth() !== prev.date.getMonth()) {
|
||||
current.class += ' border-left border-success pl-2';
|
||||
}
|
||||
|
||||
if (current.added && prev.added) {
|
||||
if (current.level > prev.level) {
|
||||
current.icon = faCaretSquareUp;
|
||||
current.class = 'text-info';
|
||||
} else if (current.level < prev.level) {
|
||||
current.icon = faCaretSquareDown;
|
||||
current.class = 'text-info';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
export function getIdsFromNote(note: string | undefined) {
|
||||
return note ? uniq(note.match(/[0-9a-f]{24}/g)) : [];
|
||||
}
|
||||
|
||||
export function addToMap<T>(map: Map<string, T[]>, key: string, item: T) {
|
||||
const items = map.get(key);
|
||||
|
||||
if (items) {
|
||||
items.push(item);
|
||||
} else {
|
||||
map.set(key, [item]);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeFromMap<T>(map: Map<string, T[]>, key: string, item: T) {
|
||||
const items = map.get(key);
|
||||
|
||||
if (items) {
|
||||
removeItem(items, item);
|
||||
|
||||
if (items.length === 0) {
|
||||
map.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePonies(ponies: string, filterIds?: string[]) {
|
||||
return compact(ponies
|
||||
.split(/\n\r?/g)
|
||||
.map(x => /\[system\] removed pony \[([a-f0-9]{24})\] "(.+)" (\S+)/.exec(x)))
|
||||
.map(([_, id, name, info]) => ({ id, name, info }))
|
||||
.filter(({ id }) => !filterIds || includes(filterIds, id));
|
||||
}
|
||||
|
||||
export function createIdStore() {
|
||||
const idsMap = new Map<string, string>();
|
||||
|
||||
return (id: string) => {
|
||||
const result = idsMap.get(id);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
} else {
|
||||
idsMap.set(id, id);
|
||||
return id;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getTranslationUrl(text: string) {
|
||||
return `https://translate.google.com/#view=home&op=translate&sl=auto&tl=en&text=${encodeURIComponent(text)}`;
|
||||
// return `https://translate.google.com/#auto/en/${encodeURIComponent(text)}`;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { sample } from 'lodash';
|
||||
import { Sprite, Palette, PaletteSpriteBatch } from './interfaces';
|
||||
import { drawSpriteCropped } from '../graphics/graphicsUtils';
|
||||
import { includes } from './utils';
|
||||
import { WHITE } from './colors';
|
||||
|
||||
const enum AnimationPhase {
|
||||
Starting,
|
||||
Playing,
|
||||
Ending,
|
||||
}
|
||||
|
||||
export interface SpriteAnimation {
|
||||
loop: boolean;
|
||||
start: number;
|
||||
middle: number;
|
||||
end: number;
|
||||
fps: number;
|
||||
palette: Uint32Array;
|
||||
frames: Sprite[];
|
||||
flipFrames?: Sprite[];
|
||||
}
|
||||
|
||||
export interface AnimationPlayer {
|
||||
nextAnimation: SpriteAnimation | undefined;
|
||||
currentAnimation: SpriteAnimation | undefined;
|
||||
time: number;
|
||||
frame: number;
|
||||
phase: AnimationPhase;
|
||||
dirty: boolean;
|
||||
palette: Palette;
|
||||
}
|
||||
|
||||
export function createAnimationPlayer(palette: Palette): AnimationPlayer {
|
||||
return {
|
||||
nextAnimation: undefined,
|
||||
currentAnimation: undefined,
|
||||
time: 0,
|
||||
frame: 0,
|
||||
phase: AnimationPhase.Starting,
|
||||
dirty: true,
|
||||
palette,
|
||||
};
|
||||
}
|
||||
|
||||
export function isAnimationPlaying(player: AnimationPlayer) {
|
||||
return player.currentAnimation !== undefined;
|
||||
}
|
||||
|
||||
export function playOneOfAnimations(player: AnimationPlayer, animations: SpriteAnimation[]) {
|
||||
if (player.phase === AnimationPhase.Ending || !includes(animations, player.currentAnimation)) {
|
||||
playAnimation(player, sample(animations));
|
||||
}
|
||||
}
|
||||
|
||||
export function playAnimation(player: AnimationPlayer, animation: SpriteAnimation | undefined) {
|
||||
if (player.currentAnimation !== animation) {
|
||||
if (player.currentAnimation) {
|
||||
if (player.nextAnimation !== animation || player.phase !== AnimationPhase.Ending) {
|
||||
player.nextAnimation = animation;
|
||||
player.time = (player.frame + 1) / player.currentAnimation.fps;
|
||||
player.phase = AnimationPhase.Ending;
|
||||
}
|
||||
} else {
|
||||
player.currentAnimation = animation;
|
||||
player.time = 0;
|
||||
player.phase = AnimationPhase.Starting;
|
||||
}
|
||||
player.dirty = true;
|
||||
} else if (player.phase === AnimationPhase.Ending) {
|
||||
player.nextAnimation = animation;
|
||||
player.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAnimation(player: AnimationPlayer, delta: number) {
|
||||
if (player.currentAnimation !== undefined) {
|
||||
player.time += delta;
|
||||
const { start, middle, end, fps, loop } = player.currentAnimation;
|
||||
|
||||
let extraFrame = Math.floor(player.time * fps);
|
||||
|
||||
if (player.phase === AnimationPhase.Starting && extraFrame > start) {
|
||||
player.phase = loop ? AnimationPhase.Playing : AnimationPhase.Ending;
|
||||
player.dirty = true;
|
||||
}
|
||||
|
||||
if (player.phase === AnimationPhase.Playing) {
|
||||
extraFrame = start + ((extraFrame - start) % middle);
|
||||
}
|
||||
|
||||
if (player.phase === AnimationPhase.Ending && extraFrame > (start + middle + end)) {
|
||||
player.currentAnimation = undefined;
|
||||
player.dirty = true;
|
||||
|
||||
if (player.nextAnimation !== undefined) {
|
||||
const nextAnimation = player.nextAnimation;
|
||||
player.nextAnimation = undefined;
|
||||
playAnimation(player, nextAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
if (player.frame !== extraFrame) {
|
||||
player.frame = extraFrame;
|
||||
player.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function drawAnimation(
|
||||
batch: PaletteSpriteBatch, player: AnimationPlayer, x: number, y: number, color = WHITE, flip = false, maxY = 0
|
||||
) {
|
||||
const animation = player.currentAnimation;
|
||||
|
||||
if (animation !== undefined) {
|
||||
const frames = (flip && animation.flipFrames) ? animation.flipFrames : animation.frames;
|
||||
|
||||
if (player.frame < frames.length) {
|
||||
const frame = frames[player.frame];
|
||||
|
||||
if (DEVELOPMENT && !frame) {
|
||||
throw new Error('Undefined frame in sprite animation');
|
||||
}
|
||||
|
||||
if (!frame) // TEMP
|
||||
return;
|
||||
|
||||
if (maxY === 0) {
|
||||
batch.drawSprite(frame, color, player.palette, x, y);
|
||||
} else {
|
||||
drawSpriteCropped(batch, frame, color, player.palette, x, y, maxY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
export interface Animation {
|
||||
fps: number;
|
||||
loop: boolean;
|
||||
frames: any[];
|
||||
}
|
||||
|
||||
export interface AnimatorTransition<T extends Animation> {
|
||||
state: AnimatorState<T>;
|
||||
exitAfter?: number;
|
||||
enterTime?: number;
|
||||
keepTime?: boolean;
|
||||
onlyDirectTo?: AnimatorState<T>;
|
||||
}
|
||||
|
||||
export interface AnimatorState<T extends Animation = Animation> {
|
||||
name: string;
|
||||
animation: T;
|
||||
variants: { [key: string]: T; };
|
||||
from: AnimatorTransition<T>[];
|
||||
}
|
||||
|
||||
export function animatorState<T extends Animation>(
|
||||
name: string, animation: T, variants: { [key: string]: T; } = {}
|
||||
): AnimatorState<T> {
|
||||
return { name, animation, variants, from: [] };
|
||||
}
|
||||
|
||||
export function animatorTransition<T extends Animation>(
|
||||
from: AnimatorState<T>, to: AnimatorState<T>, options: Partial<AnimatorTransition<T>> = {}
|
||||
) {
|
||||
to.from.push({ state: from, ...options });
|
||||
}
|
||||
|
||||
export const anyState = animatorState<any>('any', { fps: 1, loop: false, frames: [] });
|
||||
|
||||
export interface Animator<T extends Animation> {
|
||||
state: AnimatorState<T> | undefined;
|
||||
target: AnimatorState<T> | undefined;
|
||||
next: AnimatorTransition<T> | undefined;
|
||||
time: number;
|
||||
variant: string;
|
||||
}
|
||||
|
||||
export function createAnimator<T extends Animation>(): Animator<T> {
|
||||
return {
|
||||
time: 0,
|
||||
variant: '',
|
||||
state: undefined,
|
||||
target: undefined,
|
||||
next: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAnimation<T extends Animation>(animator: Animator<T>) {
|
||||
return animator.state && getAnimationForState(animator.state, animator.variant);
|
||||
}
|
||||
|
||||
export function getAnimationFrame<T extends Animation>(animator: Animator<T>) {
|
||||
const animation = getAnimation(animator);
|
||||
return animation ? Math.floor(animator.time * animation.fps) % animation.frames.length : 0;
|
||||
}
|
||||
|
||||
export function resetAnimatorState<T extends Animation>(animator: Animator<T>) {
|
||||
animator.state = undefined;
|
||||
animator.target = undefined;
|
||||
animator.next = undefined;
|
||||
}
|
||||
|
||||
export function setAnimatorState<T extends Animation>(animator: Animator<T>, state: AnimatorState<T>) {
|
||||
if (animator.target !== state) {
|
||||
if (animator.state !== state) {
|
||||
if (animator.state === undefined) {
|
||||
animator.state = state;
|
||||
} else {
|
||||
animator.target = state;
|
||||
}
|
||||
} else {
|
||||
animator.target = undefined;
|
||||
}
|
||||
|
||||
animator.next = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAnimator<T extends Animation>(animator: Animator<T>, delta: number) {
|
||||
const time = animator.time;
|
||||
animator.time += delta;
|
||||
|
||||
if (animator.target !== undefined && animator.state !== undefined && animator.state !== animator.target) {
|
||||
const animation = getAnimationForState(animator.state, animator.variant);
|
||||
const animationLength = animation.frames.length / animation.fps;
|
||||
const frameBefore = Math.floor(time / animationLength);
|
||||
const frameAfter = Math.floor(animator.time / animationLength);
|
||||
const frameTimeAfter = (animator.time % animationLength) / animationLength;
|
||||
|
||||
let animationEnded = frameBefore !== frameAfter;
|
||||
let switched = false;
|
||||
|
||||
do {
|
||||
switched = false;
|
||||
const transition = animator.next = animator.next || findTransition(animator.state, animator.target);
|
||||
|
||||
if (transition !== undefined) {
|
||||
const exitAfter = transition.exitAfter === undefined ? 1 : transition.exitAfter;
|
||||
|
||||
if (frameTimeAfter >= exitAfter || animationEnded) {
|
||||
if (!transition.keepTime) {
|
||||
animator.time = transition.enterTime || 0;
|
||||
} else {
|
||||
animator.time = animator.time % animationLength;
|
||||
}
|
||||
|
||||
setCurrentState(animator, transition.state);
|
||||
|
||||
switched = true;
|
||||
animationEnded = false;
|
||||
}
|
||||
}
|
||||
} while (switched && animator.target);
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentState<T extends Animation>(animator: Animator<T>, state: AnimatorState<T>) {
|
||||
animator.next = undefined;
|
||||
animator.state = state;
|
||||
|
||||
if (state === animator.target) {
|
||||
animator.target = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getAnimationForState<T extends Animation>(state: AnimatorState<T>, variant: string) {
|
||||
return state.variants[variant] || state.animation;
|
||||
}
|
||||
|
||||
function findTransition<T extends Animation>(
|
||||
current: AnimatorState<T>, target: AnimatorState<T>
|
||||
): AnimatorTransition<T> | undefined {
|
||||
return findTransMinMax(current, target, 0, 1)
|
||||
|| findTrans(anyState, target, target, 0, 0, [])
|
||||
|| findTransMinMax(current, target, 2, 10);
|
||||
}
|
||||
|
||||
function findTransMinMax<T extends Animation>(
|
||||
current: AnimatorState<T>, target: AnimatorState<T>, min: number, max: number
|
||||
): AnimatorTransition<T> | undefined {
|
||||
for (let i = min; i <= max; i++) {
|
||||
const trans = findTrans(current, target, target, 0, i, [current]);
|
||||
|
||||
if (trans !== undefined) {
|
||||
return trans;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findTrans<T extends Animation>(
|
||||
current: AnimatorState<T>, target: AnimatorState<T>, finalTarget: AnimatorState<T>,
|
||||
depth: number, maxDepth: number, done: AnimatorState<T>[]
|
||||
): AnimatorTransition<T> | undefined {
|
||||
if (done.indexOf(target) === -1) {
|
||||
done.push(target);
|
||||
|
||||
for (const from of target.from) {
|
||||
if (from.state === current && (from.onlyDirectTo === undefined || from.onlyDirectTo === finalTarget)) {
|
||||
return { ...from, state: target };
|
||||
}
|
||||
}
|
||||
|
||||
if (depth < maxDepth) {
|
||||
for (const from of target.from) {
|
||||
const trans = findTrans(current, from.state, finalTarget, depth + 1, maxDepth, done);
|
||||
|
||||
if (trans !== undefined) {
|
||||
return trans;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { BinaryWriter, getWriterBuffer, createBinaryWriter, resizeWriter } from 'ag-sockets/dist/browser';
|
||||
|
||||
export function writeBinary(write: (writer: BinaryWriter) => void): Uint8Array {
|
||||
const writer = createBinaryWriter();
|
||||
|
||||
do {
|
||||
try {
|
||||
write(writer);
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e instanceof RangeError || /DataView/.test(e.message)) {
|
||||
resizeWriter(writer);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} while (true);
|
||||
|
||||
return getWriterBuffer(writer);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
export type WriteBits = (value: number, bits: number) => void;
|
||||
export type ReadBits = (bits: number) => number;
|
||||
|
||||
export function numberToBitCount(value: number) {
|
||||
value = value >>> 0;
|
||||
|
||||
for (let mask = 0xffffffff >>> 0, bits = 0; mask; mask = (mask << 1) >>> 0, bits++) {
|
||||
if ((value & mask) === 0) {
|
||||
return bits;
|
||||
}
|
||||
}
|
||||
|
||||
return 32;
|
||||
}
|
||||
|
||||
export function countBits(value: number) {
|
||||
value = value >>> 0;
|
||||
|
||||
let bits = 0;
|
||||
|
||||
while (value) {
|
||||
bits += value & 1;
|
||||
value = value >>> 1;
|
||||
}
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
export function bitWriter(writes: (writer: WriteBits) => void): Uint8Array {
|
||||
let buffer = new Uint8Array(16);
|
||||
let length = 0;
|
||||
let byte = 0;
|
||||
let byteBits = 0;
|
||||
|
||||
function writeByte(value: number) {
|
||||
if (buffer.length <= length) {
|
||||
const newBuffer = new Uint8Array(buffer.length * 2);
|
||||
newBuffer.set(buffer);
|
||||
buffer = newBuffer;
|
||||
}
|
||||
|
||||
buffer[length] = value;
|
||||
length++;
|
||||
}
|
||||
|
||||
writes((value, bits) => {
|
||||
if (bits < 0 || bits > 32) {
|
||||
throw new Error('Invalid bit count');
|
||||
}
|
||||
|
||||
while (bits) {
|
||||
const revByteBits = 8 - byteBits;
|
||||
const writeBits = revByteBits < bits ? revByteBits : bits;
|
||||
const write = (value >> (bits - writeBits)) & (0xff >> (8 - writeBits));
|
||||
byte |= write << (revByteBits - writeBits);
|
||||
byteBits += writeBits;
|
||||
bits -= writeBits;
|
||||
|
||||
if (byteBits === 8) {
|
||||
writeByte(byte);
|
||||
byte = 0;
|
||||
byteBits = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (byteBits) {
|
||||
writeByte(byte);
|
||||
byteBits = 0;
|
||||
byte = 0;
|
||||
}
|
||||
|
||||
return buffer.subarray(0, length);
|
||||
}
|
||||
|
||||
export function bitReader(buffer: Uint8Array): ReadBits {
|
||||
let offset = 0;
|
||||
|
||||
return bitReaderCustom(() => {
|
||||
if (buffer.length <= offset) {
|
||||
throw new Error('Reading past end');
|
||||
}
|
||||
|
||||
return buffer[offset++];
|
||||
});
|
||||
}
|
||||
|
||||
export function bitReaderCustom(readByte: () => number): ReadBits {
|
||||
let byte = 0;
|
||||
let byteBits = 0;
|
||||
|
||||
return bits => {
|
||||
if (bits < 0 || bits > 32) {
|
||||
throw new Error('Invalid bit count');
|
||||
}
|
||||
|
||||
let result = 0;
|
||||
|
||||
while (bits) {
|
||||
if (!byteBits) {
|
||||
byte = readByte();
|
||||
byteBits = 8;
|
||||
}
|
||||
|
||||
const readBits = byteBits < bits ? byteBits : bits;
|
||||
const read = (byte >> (byteBits - readBits)) & (0xff >> (8 - readBits));
|
||||
result = (result << readBits) | read;
|
||||
bits -= readBits;
|
||||
byteBits -= readBits;
|
||||
}
|
||||
|
||||
return result >>> 0;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Entity, Rect, Point, Size, Camera } from './interfaces';
|
||||
import { CAMERA_WIDTH_MAX, CAMERA_WIDTH_MIN, CAMERA_HEIGHT_MAX, CAMERA_HEIGHT_MIN } from './constants';
|
||||
import { clamp, intersect, pointInXYWH, pointInRect, lerp } from './utils';
|
||||
import { toScreenX, toScreenY, toWorldX, toWorldY } from './positionUtils';
|
||||
import { getChatBallonXY } from '../graphics/graphicsUtils';
|
||||
|
||||
const cameraPadding = 0.3;
|
||||
export const characterHeight = 25;
|
||||
|
||||
export function createCamera(): Camera {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
offset: 0,
|
||||
shift: 0,
|
||||
shiftTarget: 0,
|
||||
shiftRatio: 0,
|
||||
actualY: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function setupCamera(camera: Camera, x: number, y: number, width: number, height: number, map: Size) {
|
||||
camera.w = clamp(width, CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX);
|
||||
camera.h = clamp(height, CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX);
|
||||
camera.x = clamp(x, 0, toScreenX(map.width) - camera.w);
|
||||
camera.y = clamp(y, 0, toScreenY(map.height) - camera.h);
|
||||
}
|
||||
|
||||
export function updateCamera(camera: Camera, player: Point, map: Size) {
|
||||
const cameraWith = camera.w;
|
||||
const cameraHeight = camera.h;
|
||||
const cameraHeightShifted = Math.ceil(camera.h - camera.offset);
|
||||
|
||||
const playerX = toScreenX(player.x);
|
||||
const playerY = toScreenY(player.y);
|
||||
|
||||
const mapWidth = toScreenX(map.width);
|
||||
const mapHeight = toScreenY(map.height);
|
||||
|
||||
const minX = Math.min(0, (mapWidth - cameraWith) / 2);
|
||||
const minY = Math.min(0, (mapHeight - cameraHeight) / 2);
|
||||
const minYShifted = Math.min(0, (mapHeight - cameraHeightShifted) / 2);
|
||||
const maxX = Math.max(mapWidth - cameraWith, minX);
|
||||
const maxY = Math.max(mapHeight - cameraHeight, minY);
|
||||
const maxYShifted = Math.max(mapHeight - cameraHeightShifted, minY);
|
||||
|
||||
const hSpace = Math.floor(cameraWith * cameraPadding);
|
||||
const vSpace = Math.floor(cameraHeight * cameraPadding);
|
||||
const vSpaceShifted = Math.floor(cameraHeightShifted * cameraPadding);
|
||||
|
||||
const hPad = (cameraWith - hSpace) / 2;
|
||||
const vPad = (cameraHeight - vSpace) / 2;
|
||||
const vPadShifted = (cameraHeightShifted - vSpaceShifted) / 2;
|
||||
|
||||
const minCamX = clamp(playerX - (hSpace + hPad), minX, maxX);
|
||||
const maxCamX = clamp(playerX - hPad, minX, maxX);
|
||||
const minCamY = clamp(playerY - (vSpace + vPad) - characterHeight, minY, maxY);
|
||||
const maxCamY = clamp(playerY - vPad - characterHeight, minY, maxY);
|
||||
const minCamYShifted = clamp(playerY - (vSpaceShifted + vPadShifted) - characterHeight, minYShifted, maxYShifted);
|
||||
const maxCamYShifted = clamp(playerY - vPadShifted - characterHeight, minYShifted, maxYShifted);
|
||||
|
||||
camera.x = Math.floor(clamp(camera.x, minCamX, maxCamX));
|
||||
camera.y = Math.floor(clamp(camera.y, minCamY, maxCamY));
|
||||
camera.shiftTarget = Math.floor(clamp(camera.shiftTarget, minCamYShifted, maxCamYShifted));
|
||||
camera.actualY = calculateCameraY(camera);
|
||||
}
|
||||
|
||||
export function centerCameraOn(camera: Camera, point: Point) {
|
||||
camera.x = Math.floor(toScreenX(point.x) - camera.w / 2);
|
||||
camera.y = Math.floor((toScreenY(point.y) - camera.h / 2) - characterHeight);
|
||||
camera.shiftTarget = Math.floor((toScreenY(point.y) - Math.ceil(camera.h - camera.offset) / 2) - characterHeight);
|
||||
}
|
||||
|
||||
export function calculateCameraY(camera: Camera) {
|
||||
return Math.round(lerp(camera.y, camera.shiftTarget - camera.offset, camera.shiftRatio));
|
||||
}
|
||||
|
||||
export function isWorldPointVisible(camera: Camera, point: Point): boolean {
|
||||
return pointInRect(toScreenX(point.x), toScreenY(point.y), camera);
|
||||
}
|
||||
|
||||
export function isWorldPointWithPaddingVisible(camera: Camera, point: Point, padding: number): boolean {
|
||||
return pointInXYWH(
|
||||
toScreenX(point.x), toScreenY(point.y),
|
||||
camera.x - padding, camera.actualY - padding, camera.w + 2 * padding, camera.h + 2 * padding);
|
||||
}
|
||||
|
||||
export function isAreaVisible(camera: Camera, x: number, y: number, w: number, h: number): boolean {
|
||||
return intersect(camera.x, camera.actualY, camera.w, camera.h, x, y, w, h);
|
||||
}
|
||||
|
||||
export function isRectVisible(camera: Camera, rect: Rect): boolean {
|
||||
return intersect(camera.x, camera.actualY, camera.w, camera.h, rect.x, rect.y, rect.w, rect.h);
|
||||
}
|
||||
|
||||
export function isBoundsVisible(camera: Camera, bounds: Rect | undefined, x: number, y: number): boolean {
|
||||
return bounds !== undefined &&
|
||||
isAreaVisible(camera, toScreenX(x) + bounds.x, toScreenY(y) + bounds.y, bounds.w, bounds.h);
|
||||
}
|
||||
|
||||
export function isEntityVisible(camera: Camera, entity: Entity): boolean {
|
||||
return isBoundsVisible(camera, entity.bounds, entity.x, entity.y);
|
||||
}
|
||||
|
||||
function isChatBaloonAboveScreenTop(camera: Camera, entity: Entity) {
|
||||
return getChatBallonXY(entity, camera).y <= -5;
|
||||
}
|
||||
|
||||
export function isChatVisible(camera: Camera, entity: Entity): boolean {
|
||||
return isBoundsVisible(camera, entity.bounds, entity.x, entity.y)
|
||||
&& !isChatBaloonAboveScreenTop(camera, entity);
|
||||
}
|
||||
|
||||
export function screenToWorld(camera: Camera, point: Point): Point {
|
||||
return {
|
||||
x: toWorldX(point.x + camera.x),
|
||||
y: toWorldY(point.y + camera.actualY),
|
||||
};
|
||||
}
|
||||
|
||||
export function worldToScreen(camera: Camera, point: Point): Point {
|
||||
return {
|
||||
x: Math.floor(toScreenX(point.x) - camera.x),
|
||||
y: Math.floor(toScreenY(point.y) - camera.actualY),
|
||||
};
|
||||
}
|
||||
|
||||
// export function mapDepth(camera: Camera, y: number): number {
|
||||
// return (toScreenY(y) - camera.actualY) - camera.maxDepth;
|
||||
// }
|
||||
@@ -0,0 +1,306 @@
|
||||
import { Entity, IMap, Region, EntityFlags } from './interfaces';
|
||||
import { clamp } from './utils';
|
||||
import { toWorldX, toWorldY } from './positionUtils';
|
||||
import { getRegionGlobal, isInWaterAt } from './worldMap';
|
||||
import { tileWidth, tileHeight, PONY_TYPE, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from './constants';
|
||||
import { isInTheAir, isFlying } from './entityUtils';
|
||||
|
||||
let isCollidingCount = 0;
|
||||
let isCollidingObjectCount = 0;
|
||||
|
||||
export function getCollisionStats() {
|
||||
const stats = { isCollidingCount, isCollidingObjectCount };
|
||||
isCollidingCount = 0;
|
||||
isCollidingObjectCount = 0;
|
||||
return stats;
|
||||
}
|
||||
|
||||
export function isOutsideMap<T>(x: number, y: number, map: IMap<T>): boolean {
|
||||
return x < 0 || y < 0 || x >= map.width || y >= map.height;
|
||||
}
|
||||
|
||||
export function canCollideWith(entity: Entity): boolean {
|
||||
return (entity.flags & EntityFlags.CanCollideWith) !== 0;
|
||||
}
|
||||
|
||||
export function isStaticCollision<T>(entity: Entity, map: IMap<T>, forceOnGround = false) {
|
||||
if (DEVELOPMENT && entity.type !== PONY_TYPE) {
|
||||
console.error(`isStaticCollision: non-pony entity`);
|
||||
}
|
||||
|
||||
const flying = !forceOnGround && isInTheAir(entity);
|
||||
return isPonyColliding(entity.x, entity.y, map as any, flying);
|
||||
}
|
||||
|
||||
export function fixCollision<T>(entity: Entity, map: IMap<T>) {
|
||||
if (DEVELOPMENT && entity.type !== PONY_TYPE) {
|
||||
console.error(`fixCollision: non-pony entity`);
|
||||
}
|
||||
|
||||
const flying = isInTheAir(entity);
|
||||
|
||||
for (let x = -1; x <= 1; x++) {
|
||||
for (let y = -1; y <= 1; y++) {
|
||||
const tx = entity.x + x;
|
||||
const ty = entity.y + y;
|
||||
|
||||
if (!isPonyColliding(tx, ty, map as any, flying)) {
|
||||
entity.x += x;
|
||||
entity.y += y;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPonyColliding<T extends Region | undefined>(x: number, y: number, map: IMap<T>, flying: boolean): boolean {
|
||||
if (isOutsideMap(x, y, map)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const region = getRegionGlobal(map, x, y);
|
||||
|
||||
if (region === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const rx = clamp(Math.floor((x - region.x * REGION_SIZE) * tileWidth), 0, REGION_WIDTH);
|
||||
const ry = clamp(Math.floor((y - region.y * REGION_SIZE) * tileHeight), 0, REGION_HEIGHT);
|
||||
const pixel = region.collider[rx + ry * REGION_WIDTH];
|
||||
const mask = flying ? 2 : 1;
|
||||
|
||||
return (pixel & mask) !== 0;
|
||||
}
|
||||
|
||||
function isColliding(x: number, y: number, mask: number, map: IMap<Region | undefined>) {
|
||||
if (x < 0 || x >= (map.width * tileWidth) || y < 0 || y >= (map.height * tileHeight)) {
|
||||
return true;
|
||||
} else {
|
||||
const regionX = (x / REGION_WIDTH) | 0;
|
||||
const regionY = (y / REGION_HEIGHT) | 0;
|
||||
const region = map.regions[regionX + regionY * map.regionsX];
|
||||
|
||||
if (region === undefined) {
|
||||
return true;
|
||||
} else {
|
||||
const insideX = (x % REGION_WIDTH) | 0;
|
||||
const insideY = (y % REGION_HEIGHT) | 0;
|
||||
return (region.collider[insideX + insideY * REGION_WIDTH] & mask) !== 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePosition(entity: Entity, delta: number, map: IMap<Region | undefined>) {
|
||||
const ex = entity.x;
|
||||
const ey = entity.y;
|
||||
const speed = (!isFlying(entity) && isInWaterAt(map, ex, ey)) ? 0.5 : 1.0;
|
||||
const destX = ex + entity.vx * speed * delta;
|
||||
const destY = ey + entity.vy * speed * delta;
|
||||
|
||||
if ((entity.flags & EntityFlags.CanCollide) === 0) {
|
||||
entity.x = destX;
|
||||
entity.y = destY;
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEVELOPMENT && entity.type !== PONY_TYPE) {
|
||||
console.error(`updatePosition: non-pony entity`);
|
||||
}
|
||||
|
||||
const flying = isInTheAir(entity);
|
||||
const mask = flying ? 2 : 1;
|
||||
|
||||
const srcX = ex * tileWidth;
|
||||
const srcY = ey * tileHeight;
|
||||
let dstX = destX * tileWidth;
|
||||
let dstY = destY * tileHeight;
|
||||
|
||||
const x0 = Math.floor(srcX) | 0;
|
||||
const y0 = Math.floor(srcY) | 0;
|
||||
const x1 = Math.floor(dstX) | 0;
|
||||
const y1 = Math.floor(dstY) | 0;
|
||||
|
||||
const minX = Math.min(x0, x1) | 0;
|
||||
const maxX = Math.max(x0, x1) | 0;
|
||||
const minY = Math.min(y0, y1) | 0;
|
||||
const maxY = Math.max(y0, y1) | 0;
|
||||
|
||||
let x = x0 | 0;
|
||||
let y = y0 | 0;
|
||||
|
||||
let actualX = x | 0;
|
||||
let actualY = y | 0;
|
||||
|
||||
if (isColliding(actualX, actualY, mask, map)) {
|
||||
if (!isOutsideMap(destX, destY, map)) {
|
||||
entity.x = destX;
|
||||
entity.y = destY;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const a = (dstY - srcY) / (dstX - srcX);
|
||||
const b = srcY - a * srcX;
|
||||
const useGt = srcY < dstY;
|
||||
|
||||
let stepXT = 0 | 0, stepYT = 0 | 0;
|
||||
let stepXF = 0 | 0, stepYF = 0 | 0;
|
||||
let ox = 0, oy = 0;
|
||||
|
||||
const shiftRight = srcX <= dstX;
|
||||
const shiftLeft = srcX >= dstX;
|
||||
const shiftUp = srcY >= dstY;
|
||||
const shiftDown = srcY <= dstY;
|
||||
const horizontalOrVertical = srcX === dstX || srcY === dstY;
|
||||
|
||||
if (srcX < dstX) {
|
||||
if (srcY < dstY) {
|
||||
ox = 1;
|
||||
oy = 1;
|
||||
stepYT = 1 | 0;
|
||||
stepXF = 1 | 0;
|
||||
} else {
|
||||
ox = 1;
|
||||
stepYT = -1 | 0;
|
||||
stepXF = 1 | 0;
|
||||
}
|
||||
} else if (srcX > dstX) {
|
||||
if (srcY < dstY) {
|
||||
oy = 1;
|
||||
stepYT = 1 | 0;
|
||||
stepXF = -1 | 0;
|
||||
} else {
|
||||
stepYT = -1 | 0;
|
||||
stepXF = -1 | 0;
|
||||
}
|
||||
} else {
|
||||
if (srcY < dstY) {
|
||||
stepYF = stepYT = 1 | 0;
|
||||
} else {
|
||||
stepYF = stepYT = -1 | 0;
|
||||
}
|
||||
}
|
||||
|
||||
let steps = 1000;
|
||||
|
||||
for (; steps; steps--) {
|
||||
const fx = a * (x + ox) + b;
|
||||
const fy = y + oy;
|
||||
|
||||
let tx = 0 | 0;
|
||||
let ty = 0 | 0;
|
||||
|
||||
if (useGt ? (fx > fy) : (fx < fy)) {
|
||||
tx = (tx + stepXT) | 0;
|
||||
ty = (ty + stepYT) | 0;
|
||||
} else {
|
||||
tx = (tx + stepXF) | 0;
|
||||
ty = (ty + stepYF) | 0;
|
||||
}
|
||||
|
||||
x = (x + tx) | 0;
|
||||
y = (y + ty) | 0;
|
||||
|
||||
if (x < minX || x > maxX || y < minY || y > maxY) {
|
||||
break;
|
||||
}
|
||||
|
||||
let actualNX = (actualX + tx) | 0;
|
||||
let actualNY = (actualY + ty) | 0;
|
||||
let collides = isColliding(actualNX, actualNY, mask, map);
|
||||
let canMove = false;
|
||||
|
||||
if (collides) {
|
||||
if (tx !== 0) {
|
||||
let canShiftUp = false;
|
||||
let canShiftDown = false;
|
||||
|
||||
if (
|
||||
shiftUp && (canShiftUp = !isColliding(actualX, actualY - 1, mask, map)) &&
|
||||
!isColliding(actualNX, actualY - 1, mask, map)
|
||||
) {
|
||||
actualNX = actualX;
|
||||
actualNY -= 1;
|
||||
dstY -= 1;
|
||||
collides = false;
|
||||
} else if (
|
||||
shiftDown && (canShiftDown = !isColliding(actualX, actualY + 1, mask, map)) &&
|
||||
!isColliding(actualNX, actualY + 1, mask, map)
|
||||
) {
|
||||
actualNX = actualX;
|
||||
actualNY += 1;
|
||||
dstY += 1;
|
||||
collides = false;
|
||||
} else if (shiftUp && canShiftUp && !isColliding(actualNX, actualY - 2, mask, map)) {
|
||||
actualNX = actualX;
|
||||
actualNY -= 1;
|
||||
dstY -= 1;
|
||||
collides = false;
|
||||
} else if (shiftDown && canShiftDown && !isColliding(actualNX, actualY + 2, mask, map)) {
|
||||
actualNX = actualX;
|
||||
actualNY += 1;
|
||||
dstY += 1;
|
||||
collides = false;
|
||||
}
|
||||
|
||||
canMove = canShiftUp || canShiftDown;
|
||||
} else {
|
||||
let canShiftLeft = false;
|
||||
let canShiftRight = false;
|
||||
|
||||
if (
|
||||
shiftLeft && (canShiftLeft = !isColliding(actualX - 1, actualY, mask, map)) &&
|
||||
!isColliding(actualX - 1, actualNY, mask, map)
|
||||
) {
|
||||
actualNX -= 1;
|
||||
actualNY = actualY;
|
||||
dstX -= 1;
|
||||
collides = false;
|
||||
} else if (
|
||||
shiftRight && (canShiftRight = !isColliding(actualX + 1, actualY, mask, map)) &&
|
||||
!isColliding(actualX + 1, actualNY, mask, map)
|
||||
) {
|
||||
actualNX += 1;
|
||||
actualNY = actualY;
|
||||
dstX += 1;
|
||||
collides = false;
|
||||
} else if (shiftLeft && canShiftLeft && !isColliding(actualX - 2, actualNY, mask, map)) {
|
||||
actualNX -= 1;
|
||||
actualNY = actualY;
|
||||
dstX -= 1;
|
||||
collides = false;
|
||||
} else if (shiftRight && canShiftRight && !isColliding(actualX + 2, actualNY, mask, map)) {
|
||||
actualNX += 1;
|
||||
actualNY = actualY;
|
||||
dstX += 1;
|
||||
collides = false;
|
||||
}
|
||||
|
||||
canMove = canShiftLeft || canShiftRight;
|
||||
}
|
||||
}
|
||||
|
||||
if (!collides) {
|
||||
actualX = actualNX;
|
||||
actualY = actualNY;
|
||||
} else if (!canMove || horizontalOrVertical) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const epsilon = 1 / 1024;
|
||||
const left = Math.min(x0, actualX);
|
||||
const right = Math.max(x0 + 1, actualX + 1) - epsilon;
|
||||
const top = Math.min(y0, actualY);
|
||||
const bottom = Math.max(y0 + 1, actualY + 1) - epsilon;
|
||||
|
||||
entity.x = toWorldX(clamp(dstX, left, right));
|
||||
entity.y = toWorldY(clamp(dstY, top, bottom));
|
||||
|
||||
if (DEVELOPMENT && steps <= 0) {
|
||||
console.error('Overflow collision steps');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
import { isString } from 'lodash';
|
||||
import { clamp } from './utils';
|
||||
|
||||
export const colorNames: { [key: string]: string | undefined } = {
|
||||
aliceblue: 'f0f8ff',
|
||||
antiquewhite: 'faebd7',
|
||||
aqua: '00ffff',
|
||||
aquamarine: '7fffd4',
|
||||
azure: 'f0ffff',
|
||||
beige: 'f5f5dc',
|
||||
bisque: 'ffe4c4',
|
||||
black: '000000',
|
||||
blanchedalmond: 'ffebcd',
|
||||
blue: '0000ff',
|
||||
blueviolet: '8a2be2',
|
||||
brown: 'a52a2a',
|
||||
burlywood: 'deb887',
|
||||
cadetblue: '5f9ea0',
|
||||
chartreuse: '7fff00',
|
||||
chocolate: 'd2691e',
|
||||
coral: 'ff7f50',
|
||||
cornflowerblue: '6495ed',
|
||||
cornsilk: 'fff8dc',
|
||||
crimson: 'dc143c',
|
||||
cyan: '00ffff',
|
||||
darkblue: '00008b',
|
||||
darkcyan: '008b8b',
|
||||
darkgoldenrod: 'b8860b',
|
||||
darkgray: 'a9a9a9',
|
||||
darkgreen: '006400',
|
||||
darkkhaki: 'bdb76b',
|
||||
darkmagenta: '8b008b',
|
||||
darkolivegreen: '556b2f',
|
||||
darkorange: 'ff8c00',
|
||||
darkorchid: '9932cc',
|
||||
darkred: '8b0000',
|
||||
darksalmon: 'e9967a',
|
||||
darkseagreen: '8fbc8f',
|
||||
darkslateblue: '483d8b',
|
||||
darkslategray: '2f4f4f',
|
||||
darkturquoise: '00ced1',
|
||||
darkviolet: '9400d3',
|
||||
deeppink: 'ff1493',
|
||||
deepskyblue: '00bfff',
|
||||
dimgray: '696969',
|
||||
dodgerblue: '1e90ff',
|
||||
feldspar: 'd19275',
|
||||
firebrick: 'b22222',
|
||||
floralwhite: 'fffaf0',
|
||||
forestgreen: '228b22',
|
||||
fuchsia: 'ff00ff',
|
||||
gainsboro: 'dcdcdc',
|
||||
ghostwhite: 'f8f8ff',
|
||||
gold: 'ffd700',
|
||||
goldenrod: 'daa520',
|
||||
gray: '808080',
|
||||
green: '008000',
|
||||
greenyellow: 'adff2f',
|
||||
honeydew: 'f0fff0',
|
||||
hotpink: 'ff69b4',
|
||||
indianred: 'cd5c5c',
|
||||
indigo: '4b0082',
|
||||
ivory: 'fffff0',
|
||||
khaki: 'f0e68c',
|
||||
lavender: 'e6e6fa',
|
||||
lavenderblush: 'fff0f5',
|
||||
lawngreen: '7cfc00',
|
||||
lemonchiffon: 'fffacd',
|
||||
lightblue: 'add8e6',
|
||||
lightcoral: 'f08080',
|
||||
lightcyan: 'e0ffff',
|
||||
lightgoldenrodyellow: 'fafad2',
|
||||
lightgrey: 'd3d3d3',
|
||||
lightgreen: '90ee90',
|
||||
lightpink: 'ffb6c1',
|
||||
lightsalmon: 'ffa07a',
|
||||
lightseagreen: '20b2aa',
|
||||
lightskyblue: '87cefa',
|
||||
lightslateblue: '8470ff',
|
||||
lightslategray: '778899',
|
||||
lightsteelblue: 'b0c4de',
|
||||
lightyellow: 'ffffe0',
|
||||
lime: '00ff00',
|
||||
limegreen: '32cd32',
|
||||
linen: 'faf0e6',
|
||||
magenta: 'ff00ff',
|
||||
maroon: '800000',
|
||||
mediumaquamarine: '66cdaa',
|
||||
mediumblue: '0000cd',
|
||||
mediumorchid: 'ba55d3',
|
||||
mediumpurple: '9370d8',
|
||||
mediumseagreen: '3cb371',
|
||||
mediumslateblue: '7b68ee',
|
||||
mediumspringgreen: '00fa9a',
|
||||
mediumturquoise: '48d1cc',
|
||||
mediumvioletred: 'c71585',
|
||||
midnightblue: '191970',
|
||||
mintcream: 'f5fffa',
|
||||
mistyrose: 'ffe4e1',
|
||||
moccasin: 'ffe4b5',
|
||||
navajowhite: 'ffdead',
|
||||
navy: '000080',
|
||||
oldlace: 'fdf5e6',
|
||||
olive: '808000',
|
||||
olivedrab: '6b8e23',
|
||||
orange: 'ffa500',
|
||||
orangered: 'ff4500',
|
||||
orchid: 'da70d6',
|
||||
palegoldenrod: 'eee8aa',
|
||||
palegreen: '98fb98',
|
||||
paleturquoise: 'afeeee',
|
||||
palevioletred: 'd87093',
|
||||
papayawhip: 'ffefd5',
|
||||
peachpuff: 'ffdab9',
|
||||
peru: 'cd853f',
|
||||
pink: 'ffc0cb',
|
||||
plum: 'dda0dd',
|
||||
powderblue: 'b0e0e6',
|
||||
purple: '800080',
|
||||
red: 'ff0000',
|
||||
rosybrown: 'bc8f8f',
|
||||
royalblue: '4169e1',
|
||||
saddlebrown: '8b4513',
|
||||
salmon: 'fa8072',
|
||||
sandybrown: 'f4a460',
|
||||
seagreen: '2e8b57',
|
||||
seashell: 'fff5ee',
|
||||
sienna: 'a0522d',
|
||||
silver: 'c0c0c0',
|
||||
skyblue: '87ceeb',
|
||||
slateblue: '6a5acd',
|
||||
slategray: '708090',
|
||||
snow: 'fffafa',
|
||||
springgreen: '00ff7f',
|
||||
steelblue: '4682b4',
|
||||
tan: 'd2b48c',
|
||||
teal: '008080',
|
||||
thistle: 'd8bfd8',
|
||||
tomato: 'ff6347',
|
||||
turquoise: '40e0d0',
|
||||
violet: 'ee82ee',
|
||||
violetred: 'd02090',
|
||||
wheat: 'f5deb3',
|
||||
white: 'ffffff',
|
||||
whitesmoke: 'f5f5f5',
|
||||
yellow: 'ffff00',
|
||||
yellowgreen: '9acd32'
|
||||
};
|
||||
|
||||
const TRANSPARENT = 0x00000000 >>> 0;
|
||||
const BLACK = 0x000000ff >>> 0;
|
||||
|
||||
export interface HSVA {
|
||||
h: number;
|
||||
s: number;
|
||||
v: number;
|
||||
a: number;
|
||||
}
|
||||
|
||||
export interface RGB {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
export interface RGBA extends RGB {
|
||||
a: number;
|
||||
}
|
||||
|
||||
export function getR(color: number) {
|
||||
return (color >> 24) & 0xff;
|
||||
}
|
||||
|
||||
export function getG(color: number) {
|
||||
return (color >> 16) & 0xff;
|
||||
}
|
||||
|
||||
export function getB(color: number) {
|
||||
return (color >> 8) & 0xff;
|
||||
}
|
||||
|
||||
export function getAlpha(color: number) {
|
||||
return color & 0xff;
|
||||
}
|
||||
|
||||
export function withAlpha(color: number, alpha: number) {
|
||||
return (color & 0xffffff00) | (alpha & 0xff);
|
||||
}
|
||||
|
||||
export function withAlphaFloat(color: number, alpha: number) {
|
||||
return (color & 0xffffff00) | ((alpha * 255) & 0xff);
|
||||
}
|
||||
|
||||
// to
|
||||
|
||||
export function colorToRGBA(color: number): RGBA {
|
||||
return {
|
||||
r: getR(color),
|
||||
g: getG(color),
|
||||
b: getB(color),
|
||||
a: getAlpha(color),
|
||||
};
|
||||
}
|
||||
|
||||
export function colorToHSVA(color: number, h?: number): HSVA {
|
||||
return rgb2hsv(getR(color), getG(color), getB(color), getAlpha(color) / 255, h);
|
||||
}
|
||||
|
||||
export function colorToCSS(color: number): string {
|
||||
const alpha = getAlpha(color);
|
||||
|
||||
if (alpha === 0xff) {
|
||||
return `#${colorToHexRGB(color)}`;
|
||||
} else {
|
||||
return `rgba(${getR(color)},${getG(color)},${getB(color)},${alpha / 255})`;
|
||||
}
|
||||
}
|
||||
|
||||
function toHex(value: number, length: number): string {
|
||||
return value.toString(16).padStart(length, '0');
|
||||
}
|
||||
|
||||
export function colorToHexRGB(color: number) {
|
||||
return toHex(color >>> 8, 6);
|
||||
}
|
||||
|
||||
export function colorToFloatArray(color: number): Float32Array {
|
||||
const result = new Float32Array(4);
|
||||
colorToExistingFloatArray(result, color);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function colorToExistingFloatArray(array: Float32Array, color: number) {
|
||||
array[0] = getR(color) / 255;
|
||||
array[1] = getG(color) / 255;
|
||||
array[2] = getB(color) / 255;
|
||||
array[3] = getAlpha(color) / 255;
|
||||
}
|
||||
|
||||
const int8 = new Int8Array(4);
|
||||
const int32 = new Int32Array(int8.buffer, 0, 1);
|
||||
const float32 = new Float32Array(int8.buffer, 0, 1);
|
||||
|
||||
export function colorToFloat(color: number): number {
|
||||
const int = (getAlpha(color) << 24) | (getB(color) << 16) | (getG(color) << 8) | getR(color);
|
||||
int32[0] = int & 0xfeffffff;
|
||||
return float32[0];
|
||||
}
|
||||
|
||||
export function colorToFloatAlpha(color: number, alpha: number /* 0-1 */): number {
|
||||
const int = (((getAlpha(color) * alpha) & 0xff) << 24) | (getB(color) << 16) | (getG(color) << 8) | getR(color);
|
||||
int32[0] = int & 0xfeffffff;
|
||||
return float32[0];
|
||||
}
|
||||
|
||||
// from
|
||||
|
||||
export function colorFromRGBA(r: number, g: number, b: number, a: number /* 0-255 */) {
|
||||
return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0;
|
||||
}
|
||||
|
||||
export function colorFromHSVA(h: number, s: number, v: number, a: number /* 0-1 */) {
|
||||
const { r, g, b } = hsv2rgb(h, s, v);
|
||||
return colorFromRGBA(r, g, b, a * 255);
|
||||
}
|
||||
|
||||
export function colorFromHSVAObject({ h, s, v, a }: HSVA) {
|
||||
return colorFromHSVA(h, s, v, a);
|
||||
}
|
||||
|
||||
// parse
|
||||
|
||||
export function parseColorFast(str: string): number {
|
||||
if (!isString(str))
|
||||
return TRANSPARENT;
|
||||
|
||||
const int = parseInt(str, 16);
|
||||
|
||||
if (str.length !== 6 || isNaN(int) || int < 0) {
|
||||
return parseColorWithAlpha(str, 1);
|
||||
} else {
|
||||
return (((int << 8) | 0xff) >>> 0);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseColor(str: string): number {
|
||||
if (!isString(str))
|
||||
return TRANSPARENT;
|
||||
|
||||
str = str.trim().toLowerCase();
|
||||
|
||||
if (str === '' || str === 'none' || str === 'transparent')
|
||||
return TRANSPARENT;
|
||||
|
||||
str = colorNames[str] || str;
|
||||
|
||||
const m = /(\d+)[ ,]+(\d+)[ ,]+(\d+)(?:[ ,]+(\d*\.?\d+))?/.exec(str);
|
||||
|
||||
if (m) {
|
||||
return colorFromRGBA(
|
||||
parseInt(m[1], 10),
|
||||
parseInt(m[2], 10),
|
||||
parseInt(m[3], 10),
|
||||
m[4] ? parseFloat(m[4]) * 255 : 255);
|
||||
}
|
||||
|
||||
const n = /[0-9a-f]+/i.exec(str);
|
||||
|
||||
if (n) {
|
||||
const s = n[0];
|
||||
|
||||
if (s.length === 3) {
|
||||
return colorFromRGBA(
|
||||
parseInt(s.charAt(0), 16) * 0x11,
|
||||
parseInt(s.charAt(1), 16) * 0x11,
|
||||
parseInt(s.charAt(2), 16) * 0x11, 255);
|
||||
} else {
|
||||
return colorFromRGBA(
|
||||
parseInt(s.substr(0, 2), 16),
|
||||
parseInt(s.substr(2, 2), 16),
|
||||
parseInt(s.substr(4, 2), 16),
|
||||
s.length >= 8 ? parseInt(s.substr(6, 2), 16) : 255);
|
||||
}
|
||||
}
|
||||
|
||||
return BLACK;
|
||||
}
|
||||
|
||||
export function parseColorWithAlpha(str: string, alpha: number /* 0-1 */): number {
|
||||
return ((parseColor(str) & 0xffffff00) | ((alpha * 255) & 0xff)) >>> 0;
|
||||
}
|
||||
|
||||
// utils
|
||||
|
||||
export function toGrayscale(color: number) {
|
||||
const c = Math.round(clamp(getR(color) * 0.2126 + getG(color) * 0.7152 + getB(color) * 0.0722, 0, 255)) | 0;
|
||||
const a = getAlpha(color);
|
||||
return colorFromRGBA(c, c, c, a);
|
||||
}
|
||||
|
||||
export function makeTransparent(color: number, factor: number /* 0-1 */): number {
|
||||
return ((color & 0xffffff00) | ((getAlpha(color) * factor) & 0xff)) >>> 0;
|
||||
}
|
||||
|
||||
export function multiplyColor(color: number, factor: number /* 0-1 */): number {
|
||||
return colorFromRGBA(
|
||||
clamp(getR(color) * factor, 0, 255),
|
||||
clamp(getG(color) * factor, 0, 255),
|
||||
clamp(getB(color) * factor, 0, 255),
|
||||
getAlpha(color)
|
||||
);
|
||||
}
|
||||
|
||||
export function lerpColors(a: number, b: number, factor: number): number {
|
||||
const f = factor;
|
||||
const t = 1 - factor;
|
||||
|
||||
return colorFromRGBA(
|
||||
getR(a) * t + getR(b) * f,
|
||||
getG(a) * t + getG(b) * f,
|
||||
getB(a) * t + getB(b) * f,
|
||||
getAlpha(a) * t + getAlpha(b) * f
|
||||
);
|
||||
}
|
||||
|
||||
/// r, g, b = <0, 255>, a = <0, 1>
|
||||
export function rgb2hsv(r: number, g: number, b: number, a: number /* 0-1 */, h = 0): HSVA {
|
||||
r = r / 255;
|
||||
g = g / 255;
|
||||
b = b / 255;
|
||||
h = h / 360;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
const v = max;
|
||||
const d = max - min;
|
||||
const s = max === 0 ? 0 : d / max;
|
||||
|
||||
if (max !== min) {
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return { h: h * 360, s, v, a };
|
||||
}
|
||||
|
||||
/// h = <0, 360>; s, v = <0, 1>
|
||||
export function hsv2rgb(h: number, s: number, v: number): RGB {
|
||||
h = Math.max(0, Math.min(360, h === 360 ? 0 : h));
|
||||
s = Math.max(0, Math.min(1, s));
|
||||
v = Math.max(0, Math.min(1, v));
|
||||
|
||||
let r = v;
|
||||
let g = v;
|
||||
let b = v;
|
||||
|
||||
if (s !== 0) {
|
||||
h /= 60;
|
||||
|
||||
const i = Math.floor(h);
|
||||
const f = h - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - s * f);
|
||||
const t = v * (1 - s * (1 - f));
|
||||
|
||||
switch (i) {
|
||||
case 0:
|
||||
r = v;
|
||||
g = t;
|
||||
b = p;
|
||||
break;
|
||||
case 1:
|
||||
r = q;
|
||||
g = v;
|
||||
b = p;
|
||||
break;
|
||||
case 2:
|
||||
r = p;
|
||||
g = v;
|
||||
b = t;
|
||||
break;
|
||||
case 3:
|
||||
r = p;
|
||||
g = q;
|
||||
b = v;
|
||||
break;
|
||||
case 4:
|
||||
r = t;
|
||||
g = p;
|
||||
b = v;
|
||||
break;
|
||||
default:
|
||||
r = v;
|
||||
g = p;
|
||||
b = q;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
r: Math.round(r * 255),
|
||||
g: Math.round(g * 255),
|
||||
b: Math.round(b * 255),
|
||||
};
|
||||
}
|
||||
|
||||
export function h2rgb(h: number): RGB {
|
||||
h /= 60;
|
||||
let r = 0, g = 0, b = 0;
|
||||
const i = Math.floor(h);
|
||||
const f = h - i;
|
||||
const q = (1 - f);
|
||||
const t = (1 - (1 - f));
|
||||
|
||||
switch (i) {
|
||||
case 0:
|
||||
r = 1;
|
||||
g = t;
|
||||
break;
|
||||
case 1:
|
||||
r = q;
|
||||
g = 1;
|
||||
break;
|
||||
case 2:
|
||||
g = 1;
|
||||
b = t;
|
||||
break;
|
||||
case 3:
|
||||
g = q;
|
||||
b = 1;
|
||||
break;
|
||||
case 4:
|
||||
r = t;
|
||||
b = 1;
|
||||
break;
|
||||
default:
|
||||
r = 1;
|
||||
b = q;
|
||||
}
|
||||
|
||||
return {
|
||||
r: Math.round(r * 255),
|
||||
g: Math.round(g * 255),
|
||||
b: Math.round(b * 255)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { MessageType, Season, TileType } from './interfaces';
|
||||
import { colorFromHSVA, colorToHexRGB, parseColorFast, colorToHSVA, withAlphaFloat } from './color';
|
||||
import { invalidEnum, invalidEnumReturn } from './utils';
|
||||
import { darkenForOutline } from './ponyInfo';
|
||||
|
||||
// basic
|
||||
|
||||
export const TRANSPARENT = 0;
|
||||
export const WHITE = 0xffffffff;
|
||||
export const BLACK = 0x000000ff;
|
||||
export const ORANGE = 0xffa500ff;
|
||||
export const BLUE = 0x0000ffff;
|
||||
export const GREEN = 0x00ff00ff;
|
||||
export const YELLOW = 0xffff00ff;
|
||||
export const MAGENTA = 0xff00ffff;
|
||||
export const CYAN = 0x00ffffff;
|
||||
export const GRAY = 0x444444ff;
|
||||
export const RED = 0xff0000ff;
|
||||
export const HOTPINK = 0xff69b4ff;
|
||||
export const PURPLE = 0x800080ff;
|
||||
|
||||
// messages
|
||||
|
||||
export const BG_COLOR = 0x333333ff;
|
||||
export const ADMIN_COLOR = 0xff69b4ff;
|
||||
export const MOD_COLOR = 0xb689ffff;
|
||||
export const SYSTEM_COLOR = 0xbbbbbbff;
|
||||
export const MESSAGE_COLOR = 0x333333ff;
|
||||
export const ANNOUNCEMENT_COLOR = 0xf0e68Cff;
|
||||
export const PARTY_COLOR = 0x71daffff;
|
||||
export const THINKING_COLOR = 0xafafafff;
|
||||
export const PARTY_THINKING_COLOR = 0x5da9c4ff;
|
||||
export const OUTLINE_COLOR = withAlphaFloat(BLACK, 0.4);
|
||||
export const PATREON_COLOR = 0xf86754ff;
|
||||
export const WHISPER_COLOR = 0xffa1dfff;
|
||||
export const FRIENDS_COLOR = 0x71ff7fff;
|
||||
|
||||
export const SUPPORTER1_COLOR = PATREON_COLOR;
|
||||
export const SUPPORTER2_COLOR = 0xffa32bff;
|
||||
export const SUPPORTER3_COLOR = 0xffcf00ff;
|
||||
|
||||
export const SUPPORTER2_BANDS = [0xffdfc1ff, 0xffcd99ff, 0xff9f3bff, 0xd97e09ff];
|
||||
export const SUPPORTER3_BANDS = [0xffffffff, 0xfffda4ff, 0xffea3bff, 0xfdbb0bff];
|
||||
|
||||
// game
|
||||
|
||||
export const SHADOW_COLOR = withAlphaFloat(BLACK, 0.3);
|
||||
export const CLOUD_SHADOW_COLOR = withAlphaFloat(BLACK, 0.2);
|
||||
export const SHINES_COLOR = withAlphaFloat(WHITE, 0.4);
|
||||
export const FAR_COLOR = colorFromHSVA(0, 0, 0.8, 1);
|
||||
export const GRASS_COLOR = 0x90ee90ff;
|
||||
export const HEARTS_COLOR = 0xf15f9dff;
|
||||
|
||||
export const CAVE_LIGHT = 0x090c21ff; // 0x253f76ff;
|
||||
export const CAVE_SHADOW = 0x00000055;
|
||||
|
||||
export let ACTION_EXPRESSION_BG = '#e7aa4e';
|
||||
export const ACTION_EXPRESSION_EYE_COLOR = '#b17a00';
|
||||
export const ACTION_ACTION_BG = '#dc9d82';
|
||||
export const ACTION_ACTION_COAT_COLOR = '#d9835e';
|
||||
export const ACTION_COMMAND_BG = '#5fb7b3';
|
||||
export const ACTION_ITEM_BG = '#cecf59';
|
||||
export const ENTITY_ITEM_BG = '#dc76bc';
|
||||
|
||||
export const MAGIC_ALPHA = 150;
|
||||
|
||||
export function updateActionColor(color: string) {
|
||||
if (DEVELOPMENT) {
|
||||
ACTION_EXPRESSION_BG = color;
|
||||
}
|
||||
}
|
||||
|
||||
// utils
|
||||
|
||||
export function getMessageColor(type: MessageType): number {
|
||||
switch (type) {
|
||||
case MessageType.Chat: return WHITE;
|
||||
case MessageType.System: return SYSTEM_COLOR;
|
||||
case MessageType.Admin: return ADMIN_COLOR;
|
||||
case MessageType.Mod: return MOD_COLOR;
|
||||
case MessageType.Party: return PARTY_COLOR;
|
||||
case MessageType.Thinking: return THINKING_COLOR;
|
||||
case MessageType.PartyThinking: return PARTY_THINKING_COLOR;
|
||||
case MessageType.Supporter1: return SUPPORTER1_COLOR;
|
||||
case MessageType.Supporter2: return SUPPORTER2_COLOR;
|
||||
case MessageType.Supporter3: return SUPPORTER3_COLOR;
|
||||
case MessageType.Whisper:
|
||||
case MessageType.WhisperTo:
|
||||
return WHISPER_COLOR;
|
||||
case MessageType.Announcement:
|
||||
case MessageType.PartyAnnouncement:
|
||||
case MessageType.WhisperAnnouncement:
|
||||
case MessageType.WhisperToAnnouncement:
|
||||
return ANNOUNCEMENT_COLOR;
|
||||
case MessageType.Dismiss: return TRANSPARENT;
|
||||
default:
|
||||
return invalidEnumReturn(type, WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
export function fillToOutline(color: string | undefined): string | undefined {
|
||||
return color ? colorToHexRGB(fillToOutlineColor(parseColorFast(color))) : undefined;
|
||||
}
|
||||
|
||||
export function fillToOutlineWithDarken(color: string | undefined): string | undefined {
|
||||
return color ? colorToHexRGB(darkenForOutline(fillToOutlineColor(parseColorFast(color)))) : undefined;
|
||||
}
|
||||
|
||||
export function fillToOutlineColor(color: number): number {
|
||||
const { h, s, v, a } = colorToHSVA(color);
|
||||
return colorFromHSVA(h, Math.min(s * 1.3, 1), v * 0.7, a);
|
||||
}
|
||||
|
||||
const LIGHT_BLUSH = 0xff89aeff;
|
||||
const DARK_BLUSH = 0xc90040ff;
|
||||
|
||||
export function blushColor(coat: number): number {
|
||||
const { h, s, v } = colorToHSVA(coat);
|
||||
|
||||
if (
|
||||
(h < 15 && s > 0.2 && s < 0.7 && v > 0.85) ||
|
||||
(h > 15 && h < 50 && s > 0.2 && v > 0.85) ||
|
||||
(h > 280 && s > 0.2 && s < 0.7 && v > 0.85)
|
||||
) {
|
||||
return DARK_BLUSH;
|
||||
} else {
|
||||
return LIGHT_BLUSH;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTileColor(tile: TileType, season: Season) {
|
||||
switch (tile) {
|
||||
case TileType.Dirt:
|
||||
case TileType.ElevatedDirt:
|
||||
if (season === Season.Autumn) {
|
||||
return 0xedd29eff;
|
||||
} else if (season === Season.Winter) {
|
||||
return 0xd9c2a1ff;
|
||||
} else {
|
||||
return 0xf5d99bff;
|
||||
}
|
||||
case TileType.Water:
|
||||
case TileType.WalkableWater:
|
||||
case TileType.Boat:
|
||||
return 0x6dbdecff;
|
||||
case TileType.Grass:
|
||||
if (season === Season.Autumn) {
|
||||
return 0xddcf71ff;
|
||||
} else if (season === Season.Winter) {
|
||||
return 0xe1ebf8ff;
|
||||
} else {
|
||||
return 0x7cc991ff;
|
||||
}
|
||||
case TileType.Ice:
|
||||
case TileType.WalkableIce:
|
||||
return 0xc1dcecff;
|
||||
case TileType.SnowOnIce:
|
||||
return 0xe4eefbff;
|
||||
case TileType.Wood:
|
||||
return 0xd7ac7eff;
|
||||
case TileType.Stone:
|
||||
return 0x9da6abff;
|
||||
case TileType.Stone2:
|
||||
return 0xa0a691ff;
|
||||
case TileType.None:
|
||||
case TileType.WallH:
|
||||
case TileType.WallV:
|
||||
return BLACK;
|
||||
default:
|
||||
invalidEnum(tile);
|
||||
return BLACK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { toByteArray } from 'base64-js';
|
||||
import { bitWriter, bitReader } from './bitUtils';
|
||||
import { REGION_SIZE } from './constants';
|
||||
|
||||
function getBitsForNumber(value: number) {
|
||||
let bits = 0;
|
||||
let max = value - 1;
|
||||
|
||||
while (max > 0) {
|
||||
bits++;
|
||||
max >>= 1;
|
||||
}
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
export function compressTiles(tiles: Uint8Array): Uint8Array {
|
||||
const types: number[] = [];
|
||||
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const tile = tiles[i];
|
||||
|
||||
if (types.indexOf(tile) === -1) {
|
||||
types.push(tile);
|
||||
}
|
||||
}
|
||||
|
||||
const bitsPerTile = getBitsForNumber(types.length);
|
||||
const bitsPerRun = 4;
|
||||
|
||||
return bitWriter(write => {
|
||||
write(types.length, 8);
|
||||
|
||||
for (const type of types) {
|
||||
write(type, 8);
|
||||
}
|
||||
|
||||
if (types.length > 1) {
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const value = tiles[i];
|
||||
let count = 1;
|
||||
|
||||
if (i === (tiles.length - 1)) {
|
||||
write(count | 0b1000, bitsPerRun);
|
||||
write(types.indexOf(value), bitsPerTile);
|
||||
} else {
|
||||
i++;
|
||||
|
||||
if (value === tiles[i]) {
|
||||
while (i < tiles.length && count < 0b111 && tiles[i] === value) {
|
||||
i++;
|
||||
count++;
|
||||
}
|
||||
|
||||
i--;
|
||||
|
||||
write(count, bitsPerRun);
|
||||
write(types.indexOf(value), bitsPerTile);
|
||||
} else {
|
||||
let last = tiles[i];
|
||||
let last2 = last;
|
||||
let pushLast = true;
|
||||
const values = [value];
|
||||
count++;
|
||||
|
||||
for (i++; i < tiles.length; i++) {
|
||||
last2 = tiles[i];
|
||||
|
||||
if (last2 === last) {
|
||||
i -= 2;
|
||||
count--;
|
||||
pushLast = false;
|
||||
break;
|
||||
} else if (count === 0b111) {
|
||||
i -= 1;
|
||||
break;
|
||||
} else {
|
||||
values.push(last);
|
||||
count++;
|
||||
last = last2;
|
||||
}
|
||||
}
|
||||
|
||||
write(count | 0b1000, bitsPerRun);
|
||||
|
||||
for (const v of values) {
|
||||
write(types.indexOf(v), bitsPerTile);
|
||||
}
|
||||
|
||||
if (pushLast) {
|
||||
write(types.indexOf(last), bitsPerTile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function decompressTiles(data: Uint8Array): Uint8Array {
|
||||
const size = REGION_SIZE * REGION_SIZE;
|
||||
const result = new Uint8Array(size);
|
||||
const read = bitReader(data);
|
||||
const typesCount = read(8);
|
||||
const types: number[] = [];
|
||||
|
||||
for (let i = 0; i < typesCount; i++) {
|
||||
types.push(read(8));
|
||||
}
|
||||
|
||||
if (types.length === 1) {
|
||||
result.fill(types[0]);
|
||||
} else {
|
||||
const bitsPerTile = getBitsForNumber(typesCount);
|
||||
const bitsPerRun = 4;
|
||||
|
||||
for (let i = 0; i < size;) {
|
||||
const value = read(bitsPerRun);
|
||||
|
||||
if ((value & 0b1000) === 0) {
|
||||
const count = value;
|
||||
const entry = read(bitsPerTile);
|
||||
|
||||
for (let j = 0; j < count; j++) {
|
||||
result[i] = types[entry];
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
const count = value & 0b0111;
|
||||
|
||||
for (let j = 0; j < count; j++) {
|
||||
result[i] = types[read(bitsPerTile)];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function deserializeTiles(tiles: string) {
|
||||
const decodedTiles = toByteArray(tiles);
|
||||
const result: number[] = [];
|
||||
|
||||
for (let i = 0; i < decodedTiles.length; i += 2) {
|
||||
let count = decodedTiles[i];
|
||||
const tile = decodedTiles[i + 1];
|
||||
|
||||
while (count > 0) {
|
||||
result.push(tile);
|
||||
count--;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
import { findLastIndex, isString, isBoolean, isNumber, merge } from 'lodash';
|
||||
import { fromByteArray, toByteArray } from 'base64-js';
|
||||
import { PonyInfoNumber, SpriteSet, PonyInfo, PonyInfoBase, PaletteManager, PalettePonyInfo, ColorExtraSet } from './interfaces';
|
||||
import { syncLockedPonyInfoNumber, syncLockedPonyInfo, createBasePony, toPaletteNumber } from './ponyInfo';
|
||||
import { bitWriter, bitReader, ReadBits, WriteBits, countBits, numberToBitCount } from './bitUtils';
|
||||
import { BLACK, WHITE, TRANSPARENT } from './colors';
|
||||
import { at, toInt, pushUniq, array, clamp, includes, att } from './utils';
|
||||
import { getColorCount } from '../client/spriteUtils';
|
||||
import * as sprites from '../generated/sprites';
|
||||
import { parseColorFast, colorToHexRGB } from './color';
|
||||
import {
|
||||
SLEEVED_ACCESSORIES, frontHooves, mergedFacialHair, mergedBackAccessories, mergedManes,
|
||||
mergedBackManes, mergedExtraAccessories, mergedHeadAccessories
|
||||
} from '../client/ponyUtils';
|
||||
import { CM_SIZE } from './constants';
|
||||
|
||||
export const VERSION = 3;
|
||||
|
||||
interface FieldDefinition<T> {
|
||||
name: keyof PonyInfo;
|
||||
default?: T;
|
||||
omit?: (info: PonyInfoBase<any, SpriteSet<any>>) => boolean;
|
||||
dontSave?: boolean;
|
||||
}
|
||||
|
||||
interface SetDefinition extends FieldDefinition<PrecompressedSet> {
|
||||
preserveOnZero?: boolean;
|
||||
sets: ColorExtraSet[];
|
||||
minColors?: number;
|
||||
// defaultLockFills?: boolean[];
|
||||
// defaultLockOutlines?: boolean[];
|
||||
}
|
||||
|
||||
export interface PrecompressedSet {
|
||||
type: number;
|
||||
pattern: number;
|
||||
colors: number;
|
||||
fillLocks: number;
|
||||
fills: number[];
|
||||
outlineLocks: number;
|
||||
outlines: number[];
|
||||
}
|
||||
|
||||
export interface Precompressed {
|
||||
version: number;
|
||||
colors: number[];
|
||||
setFields: (PrecompressedSet | undefined)[];
|
||||
colorFields: number[];
|
||||
numberFields: number[];
|
||||
booleanFields: boolean[];
|
||||
cm: number[];
|
||||
}
|
||||
|
||||
const identity = <T>(x: T) => x;
|
||||
const not = <T>(x: T) => !x;
|
||||
|
||||
function emptyOrUnlocked<T>(set: SpriteSet<T> | undefined): boolean {
|
||||
return !set || !set.type || !set.lockFills || set.lockFills.every(x => !x);
|
||||
}
|
||||
|
||||
function emptyOrZeroLocked<T>(set: SpriteSet<T> | undefined, customOutlines: boolean): boolean {
|
||||
return !set || (
|
||||
set.type === 0 && set.pattern === 0 && set.lockFills !== undefined && set.lockFills[0] === true &&
|
||||
(!customOutlines || (set.lockOutlines !== undefined && set.lockOutlines[0] === true)));
|
||||
}
|
||||
|
||||
function empty<T>(set: SpriteSet<T> | undefined): boolean {
|
||||
return !set || !set.type;
|
||||
}
|
||||
|
||||
function omitMane(info: PonyInfoNumber) {
|
||||
return empty(info.mane) && emptyOrUnlocked(info.backMane)
|
||||
&& emptyOrUnlocked(info.tail) && emptyOrUnlocked(info.facialHair);
|
||||
}
|
||||
|
||||
function omitHead(info: PonyInfoNumber): boolean {
|
||||
return emptyOrZeroLocked(info.head, !!info.customOutlines);
|
||||
}
|
||||
|
||||
function omitSleeves(info: PonyInfoNumber) {
|
||||
return !info.chestAccessory || !includes(SLEEVED_ACCESSORIES, toInt(info.chestAccessory.type));
|
||||
}
|
||||
|
||||
function omitFrontHooves(info: PonyInfoNumber) {
|
||||
return empty(info.frontHooves) && emptyOrUnlocked(info.backHooves);
|
||||
}
|
||||
|
||||
function readTimes(read: ReadBits, count: number, bitsPerItem: number): number[] {
|
||||
const result: number[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
result[i] = read(bitsPerItem);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// NOTE: do not reorder or remove
|
||||
const setFields: SetDefinition[] = [
|
||||
{ name: 'extraAccessory', sets: mergedExtraAccessories!, preserveOnZero: true },
|
||||
{ name: 'nose', sets: sprites.noses[0]!, preserveOnZero: true },
|
||||
{ name: 'ears', sets: sprites.ears!, preserveOnZero: true },
|
||||
{ name: 'mane', sets: mergedManes!, preserveOnZero: true, minColors: 1, omit: omitMane },
|
||||
{ name: 'backMane', sets: mergedBackManes! },
|
||||
{ name: 'tail', sets: sprites.tails[0]! },
|
||||
{ name: 'horn', sets: sprites.horns! },
|
||||
{ name: 'wings', sets: sprites.wings[0]! },
|
||||
{ name: 'frontHooves', sets: frontHooves[1]!, preserveOnZero: true, minColors: 1, omit: omitFrontHooves },
|
||||
{ name: 'backHooves', sets: sprites.backLegHooves[1]! },
|
||||
{ name: 'facialHair', sets: mergedFacialHair! },
|
||||
{ name: 'headAccessory', sets: mergedHeadAccessories },
|
||||
{ name: 'earAccessory', sets: sprites.earAccessories! },
|
||||
{ name: 'faceAccessory', sets: sprites.faceAccessories! },
|
||||
{ name: 'neckAccessory', sets: sprites.neckAccessories[1]! },
|
||||
{ name: 'frontLegAccessory', sets: sprites.frontLegAccessories[1]! },
|
||||
{ name: 'backLegAccessory', sets: sprites.backLegAccessories[1]!, omit: info => !!info.lockBackLegAccessory },
|
||||
{ name: 'backAccessory', sets: mergedBackAccessories! },
|
||||
{ name: 'waistAccessory', sets: sprites.waistAccessories[1]! },
|
||||
{ name: 'chestAccessory', sets: sprites.chestAccessories[1]! },
|
||||
{ name: 'sleeveAccessory', sets: sprites.frontLegSleeves[1]!, preserveOnZero: true, omit: omitSleeves },
|
||||
{ name: 'head', sets: sprites.head0[1]!, preserveOnZero: true, omit: omitHead },
|
||||
{
|
||||
name: 'frontLegAccessoryRight',
|
||||
sets: sprites.frontLegAccessories[1]!,
|
||||
omit: info => !info.unlockFrontLegAccessory,
|
||||
},
|
||||
{
|
||||
name: 'backLegAccessoryRight',
|
||||
sets: sprites.backLegAccessories[1]!,
|
||||
omit: info => !info.unlockBackLegAccessory || !!info.lockBackLegAccessory,
|
||||
},
|
||||
];
|
||||
|
||||
const booleanFields: FieldDefinition<boolean>[] = [
|
||||
{ name: 'customOutlines' },
|
||||
{ name: 'lockEyes' },
|
||||
{ name: 'lockEyeColor' },
|
||||
{ name: 'lockCoatOutline', omit: info => !info.customOutlines },
|
||||
{
|
||||
name: 'lockBackLegAccessory', omit: info =>
|
||||
empty(info.frontLegAccessory) && empty(info.backLegAccessory) &&
|
||||
empty(info.frontLegAccessoryRight) && empty(info.backLegAccessoryRight)
|
||||
},
|
||||
{ name: 'eyeshadow' },
|
||||
{ name: 'cmFlip', omit: info => info.cm === undefined || info.cm.every(not) },
|
||||
{ name: 'unlockEyeWhites' },
|
||||
{ name: 'freeOutlines' },
|
||||
{ name: 'unlockFrontLegAccessory' },
|
||||
{ name: 'unlockBackLegAccessory', omit: info => !!info.lockBackLegAccessory },
|
||||
{ name: 'unlockEyelashColor' },
|
||||
{ name: 'darkenLockedOutlines', omit: info => !info.freeOutlines },
|
||||
];
|
||||
|
||||
const numberFields: FieldDefinition<number>[] = [
|
||||
{ name: 'eyelashes' },
|
||||
{ name: 'eyeOpennessRight' },
|
||||
{ name: 'eyeOpennessLeft', omit: info => !!info.lockEyes },
|
||||
{ name: 'fangs' },
|
||||
{ name: 'muzzle' },
|
||||
{ name: 'freckles', dontSave: true }, // TODO: remove
|
||||
];
|
||||
|
||||
const colorFields: FieldDefinition<number>[] = [
|
||||
{ name: 'coatFill' },
|
||||
{ name: 'coatOutline', omit: info => !info.customOutlines || !!info.lockCoatOutline },
|
||||
{ name: 'eyeColorRight' },
|
||||
{ name: 'eyeColorLeft', omit: info => !!info.lockEyeColor },
|
||||
{ name: 'eyeWhites', default: WHITE },
|
||||
{ name: 'eyeshadowColor', omit: info => !info.eyeshadow },
|
||||
{ name: 'frecklesColor', omit: info => !info.freckles, dontSave: true }, // TODO: remove
|
||||
{ name: 'eyeWhitesLeft', default: WHITE, omit: info => !info.unlockEyeWhites },
|
||||
{ name: 'eyelashColor' },
|
||||
{ name: 'eyelashColorLeft', omit: info => !info.unlockEyelashColor },
|
||||
{ name: 'magicColor', default: WHITE },
|
||||
];
|
||||
|
||||
const omittableFields: FieldDefinition<any>[] = [
|
||||
...setFields,
|
||||
...booleanFields,
|
||||
...numberFields,
|
||||
...colorFields,
|
||||
].filter(f => !!f.omit);
|
||||
|
||||
const VERSION_BITS = 6; // max 63
|
||||
const COLORS_LENGTH_BITS = 10; // max 1024
|
||||
const BOOLEAN_FIELDS_LENGTH_BITS = 4; // max 15
|
||||
const NUMBER_FIELDS_LENGTH_BITS = 4; // max 15
|
||||
const COLOR_FIELDS_LENGTH_BITS = 4; // max 15
|
||||
const SET_FIELDS_LENGTH_BITS = 5; // max 31
|
||||
const CM_LENGTH_BITS = 5; // max 31
|
||||
const NUMBERS_BITS = 6; // max 63
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (DEVELOPMENT) {
|
||||
(function () {
|
||||
function verifyFields(obj: any, lengthBits: number, defs: FieldDefinition<any>[], verify: (field: any) => boolean) {
|
||||
const missing = Object.keys(obj)
|
||||
.filter(key => verify(obj[key]))
|
||||
.filter(key => defs.every(d => d.name !== key));
|
||||
|
||||
const unnecessary = defs
|
||||
.filter(({ name }) => !verify(obj[name]));
|
||||
|
||||
if (missing.length || unnecessary.length) {
|
||||
throw new Error(`Incorrect fields (${missing} / ${unnecessary})`);
|
||||
}
|
||||
|
||||
if (lengthBits < countBits(defs.length)) {
|
||||
throw new Error(`Incorrect field length bits (${lengthBits}/${countBits(defs.length)})`);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPony = createBasePony();
|
||||
verifyFields(defaultPony, SET_FIELDS_LENGTH_BITS, setFields, f => f.type !== undefined);
|
||||
verifyFields(defaultPony, COLOR_FIELDS_LENGTH_BITS, colorFields, isString);
|
||||
verifyFields(defaultPony, NUMBER_FIELDS_LENGTH_BITS, numberFields, isNumber);
|
||||
verifyFields(defaultPony, BOOLEAN_FIELDS_LENGTH_BITS, booleanFields, isBoolean);
|
||||
|
||||
if (setFields.some(f => !f.sets)) {
|
||||
throw new Error(`Undefined set in set field (${setFields.find(f => !f.sets)!.name})`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function trimRight<T>(items: T[]) {
|
||||
const index = findLastIndex(items, x => !!x);
|
||||
return (index !== (items.length - 1)) ? items.slice(0, index + 1) : items;
|
||||
}
|
||||
|
||||
export function precompressCM<T>(cm: (T | undefined)[] | undefined, addColor: (color: T | undefined) => number): number[] {
|
||||
const result: number[] = [];
|
||||
|
||||
if (cm) {
|
||||
let length = CM_SIZE * CM_SIZE;
|
||||
|
||||
while (length > 0 && !cm[length - 1]) {
|
||||
length--;
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result.push(addColor(cm[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// lock sets
|
||||
|
||||
export function compressLockSet(set: boolean[] | undefined, count: number): number {
|
||||
const locks = set && set.slice ? set.slice(0, count) : [];
|
||||
return locks.reduce((result, l, i) => result | (l ? (1 << i) : 0), 0);
|
||||
}
|
||||
|
||||
export function decompressLockSet(set: number, count: number, defaultValues: boolean[]): boolean[] {
|
||||
const result: boolean[] = [];
|
||||
|
||||
for (let i = 0; i < MAX_COLORS; i++) {
|
||||
result[i] = i < count ? !!(set & (1 << i)) : defaultValues[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// colors
|
||||
|
||||
export function precompressColorSet<T>(
|
||||
set: (T | undefined)[] | undefined, count: number, locks: number, defaultColor: T, addColor: (color: T) => number
|
||||
): number[] {
|
||||
const result: number[] = [];
|
||||
|
||||
if (set) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
if ((locks & (1 << i)) === 0) {
|
||||
const color = set[i];
|
||||
result.push(!color || color === defaultColor ? 0 : addColor(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function postdecompressColorSet<T>(
|
||||
colors: number[], count: number, locks: number, colorList: number[], parseColor: (color: number) => T
|
||||
): T[] {
|
||||
const result: T[] = [];
|
||||
|
||||
for (let i = 0, j = 0; i < count; i++) {
|
||||
const locked = (locks & (1 << i)) !== 0;
|
||||
result.push(parseColor((locked ? 0 : colorList[colors[j++] - 1]) || BLACK));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// set
|
||||
|
||||
const MAX_COLORS = 6;
|
||||
const ALL_UNLOCKED = array(MAX_COLORS, false);
|
||||
const ALL_LOCKED = array(MAX_COLORS, true);
|
||||
|
||||
export function precompressSet<T>(
|
||||
set: SpriteSet<T> | undefined, def: SetDefinition, customOutlines: boolean, defaultColor: T, addColor: (color: T) => number
|
||||
): PrecompressedSet | undefined {
|
||||
if (!set)
|
||||
return undefined;
|
||||
|
||||
const type = clamp(toInt(set.type), 0, def.sets.length - 1);
|
||||
|
||||
if (type === 0 && !def.preserveOnZero)
|
||||
return undefined;
|
||||
|
||||
const patterns = at(def.sets, type);
|
||||
const pattern = clamp(toInt(set.pattern), 0, patterns ? patterns.length - 1 : 0);
|
||||
const sprite = att(patterns, pattern);
|
||||
const colors = Math.max(getColorCount(sprite), def.minColors || 0);
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (type === 0 && pattern === 0 && colors === 0)
|
||||
return undefined;
|
||||
|
||||
const fillLocks = compressLockSet(set.lockFills, colors);
|
||||
const fills = precompressColorSet(set.fills, colors, fillLocks, defaultColor, addColor);
|
||||
const outlineLocks = customOutlines ? compressLockSet(set.lockOutlines, colors) : 0;
|
||||
const outlines = customOutlines ? precompressColorSet(set.outlines, colors, outlineLocks, defaultColor, addColor) : [];
|
||||
|
||||
return { type, pattern, colors, fillLocks, fills, outlineLocks, outlines };
|
||||
}
|
||||
|
||||
export function postdecompressSet<T>(
|
||||
set: PrecompressedSet, _def: SetDefinition, customOutlines: boolean, colorList: number[], parseColor: (color: number) => T
|
||||
): SpriteSet<T> | undefined {
|
||||
return {
|
||||
type: set.type,
|
||||
pattern: set.pattern,
|
||||
lockFills: decompressLockSet(set.fillLocks, set.colors, /*def.defaultLockFills ||*/ ALL_UNLOCKED),
|
||||
fills: postdecompressColorSet(set.fills, set.colors, set.fillLocks, colorList, parseColor),
|
||||
lockOutlines: customOutlines ?
|
||||
decompressLockSet(set.outlineLocks, set.colors, /*def.defaultLockOutlines ||*/ ALL_LOCKED) :
|
||||
ALL_LOCKED,
|
||||
outlines: customOutlines ? postdecompressColorSet(set.outlines, set.colors, set.outlineLocks, colorList, parseColor) : [],
|
||||
};
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
function precompressFields<TDef extends FieldDefinition<TResult>, TValue, TResult>(
|
||||
data: any, defs: TDef[], defaultValue: TResult, encode: (value: TValue | undefined, def: TDef) => TResult
|
||||
): TResult[] {
|
||||
return trimRight(defs.map(def => {
|
||||
if (def.dontSave || (def.omit && def.omit(data))) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return encode(data[def.name], def);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function postdecompressFields<TDef extends FieldDefinition<TValue>, TValue, TResult>(
|
||||
result: any, defs: TDef[], values: (TValue | undefined)[], defaultValue: TValue, decode: (value: TValue, def: TDef) => TResult
|
||||
) {
|
||||
for (let i = 0; i < defs.length; i++) {
|
||||
const def = defs[i];
|
||||
const value = i >= values.length ? undefined : values[i];
|
||||
result[def.name] = decode(value === undefined ? defaultValue : value, def);
|
||||
}
|
||||
}
|
||||
|
||||
// pony
|
||||
|
||||
type Info<T> = PonyInfoBase<T, SpriteSet<T>>;
|
||||
|
||||
export function precompressPony<T>(info: Info<T>, defaultColor: T, parseColor: (color: T) => number): Precompressed {
|
||||
const colors: number[] = [];
|
||||
const customOutlines = !!info.customOutlines;
|
||||
const addColor = (color: T | undefined) => {
|
||||
const c = color === undefined ? 0 : parseColor(color);
|
||||
return c === 0 ? 0 : pushUniq(colors, c);
|
||||
};
|
||||
|
||||
return {
|
||||
version: VERSION,
|
||||
colors,
|
||||
booleanFields: precompressFields(info, booleanFields, false as boolean, x => !!x),
|
||||
numberFields: precompressFields(info, numberFields, 0, toInt),
|
||||
colorFields: precompressFields(info, colorFields, 0,
|
||||
(x: T | undefined, def) => (x === undefined || parseColor(x) === (def.default || BLACK)) ? 0 : addColor(x)),
|
||||
setFields: precompressFields(info, setFields, undefined,
|
||||
(x: SpriteSet<T> | undefined, def: SetDefinition) => precompressSet(x, def, customOutlines, defaultColor, addColor)),
|
||||
cm: precompressCM(info.cm, addColor),
|
||||
};
|
||||
}
|
||||
|
||||
const frecklesToPattern = [0, 1, 1, 2, 2, 2, 1];
|
||||
const frecklesToColor: number[][] = [[], [1], [1, 2], [2], [1], [1, 2], [2]];
|
||||
|
||||
function fixVersion<T>(result: Info<T>, data: Precompressed, parseColor: (color: number) => T) {
|
||||
if (data.version < 3) {
|
||||
result.head = {
|
||||
type: 0,
|
||||
pattern: frecklesToPattern[result.freckles || 0] || 0,
|
||||
fills: [result.coatFill],
|
||||
outlines: [result.coatOutline],
|
||||
lockFills: [true, true, true, true, true, true],
|
||||
lockOutlines: [true, true, true, true, true, true],
|
||||
};
|
||||
|
||||
frecklesToColor[result.freckles || 0].forEach(index => {
|
||||
result.head!.fills![index] = result.frecklesColor || parseColor(BLACK);
|
||||
result.head!.lockFills![index] = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createPostDecompressPony() {
|
||||
return new Function('postdecompressSet', 'setFields', 'ommitableFields', 'fixVersion', [
|
||||
'function identity(x) { return x; }',
|
||||
'function getColor(colors, i) { return (i >= 0 && i < colors.length) ? colors[i] : 0; }',
|
||||
'function getCM(cm, colors) {',
|
||||
' var result = [];',
|
||||
' for(var i = 0; i < cm.length; i++) { result.push(getColor(colors, cm[i] - 1) || 0); }',
|
||||
' return result;',
|
||||
'}',
|
||||
...omittableFields.map((def, i) => `var omit_${def.name} = ommitableFields[${i}].omit;`),
|
||||
'return function (data) {',
|
||||
' var dataColors = data.colors;',
|
||||
' var bools = data.booleanFields;',
|
||||
' var numbers = data.numberFields;',
|
||||
' var colors = data.colorFields;',
|
||||
' var sets = data.setFields;',
|
||||
' var result = {};',
|
||||
...booleanFields.map((def, i) => ` result.${def.name} = bools.length > ${i} ? bools[${i}] : false;`),
|
||||
...numberFields.map((def, i) => ` result.${def.name} = numbers.length > ${i} ? numbers[${i}] : 0;`),
|
||||
...colorFields.map((def, i) => ` result.${def.name} = colors.length > ${i} ? ` +
|
||||
`getColor(dataColors, colors[${i}] - 1) || ${def.default || BLACK} : ${def.default || BLACK};`),
|
||||
' var customOutlines = !!result.customOutlines;',
|
||||
...setFields.map((def, i) => ` result.${def.name} = sets.length > ${i} && sets[${i}] !== undefined ? ` +
|
||||
`postdecompressSet(sets[${i}], setFields[${i}], customOutlines, data.colors, identity) : undefined;`),
|
||||
` result.cm = data.cm.length ? getCM(data.cm, dataColors) : undefined;`,
|
||||
...omittableFields.map(def => ` if (omit_${def.name}(result)) result.${def.name} = undefined;`),
|
||||
' fixVersion(result, data, identity);',
|
||||
' return result;',
|
||||
'};',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
export const fastPostdecompressPony = createPostDecompressPony()(
|
||||
postdecompressSet, setFields, omittableFields, fixVersion);
|
||||
|
||||
export function postdecompressPony<T>(data: Precompressed, parseColor: (color: number) => T): Info<T> {
|
||||
// NOTE: when updating also update createPostDecompressPony()
|
||||
|
||||
const result: Info<T> = {} as any;
|
||||
postdecompressFields(result, booleanFields, data.booleanFields, false as boolean, identity);
|
||||
postdecompressFields(result, numberFields, data.numberFields, 0 as number, identity);
|
||||
postdecompressFields(result, colorFields, data.colorFields, 0 as number,
|
||||
(x, def) => parseColor(data.colors[x - 1] || def.default || BLACK));
|
||||
const customOutlines = !!result.customOutlines;
|
||||
postdecompressFields(result, setFields, data.setFields, undefined,
|
||||
(x, def) => x === undefined ? undefined : postdecompressSet(x, def, customOutlines, data.colors, parseColor));
|
||||
|
||||
result.cm = data.cm.length ? data.cm.map(x => parseColor(data.colors[x - 1] || TRANSPARENT)) : undefined;
|
||||
|
||||
omittableFields.forEach(def => {
|
||||
if (def.omit && def.omit(result)) {
|
||||
result[def.name] = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
fixVersion(result, data, parseColor);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// set
|
||||
|
||||
const TYPE_BITS = 5; // max 31
|
||||
const PATTERN_BITS = 4; // max 15
|
||||
const COLORS_BITS = 3; // max 7
|
||||
|
||||
export function writeSet(write: WriteBits, colorBits: number, customOutlines: boolean, set: PrecompressedSet | undefined) {
|
||||
write(set ? 1 : 0, 1);
|
||||
|
||||
if (set) {
|
||||
write(set.type, TYPE_BITS);
|
||||
write(set.pattern, PATTERN_BITS);
|
||||
write(set.colors - 1, COLORS_BITS);
|
||||
write(set.fillLocks, set.colors);
|
||||
set.fills.forEach(c => write(c, colorBits));
|
||||
|
||||
if (customOutlines) {
|
||||
write(set.outlineLocks, set.colors);
|
||||
set.outlines.forEach(c => write(c, colorBits));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function readSet(read: ReadBits, colorBits: number, customOutlines: boolean): PrecompressedSet | undefined {
|
||||
const has = read(1);
|
||||
|
||||
if (has) {
|
||||
const type = read(TYPE_BITS);
|
||||
const pattern = read(PATTERN_BITS);
|
||||
const colors = read(COLORS_BITS) + 1;
|
||||
const fillLocks = read(colors);
|
||||
const fills = readTimes(read, colors - countBits(fillLocks), colorBits);
|
||||
const outlineLocks = customOutlines ? read(colors) : 0;
|
||||
const outlines = customOutlines ? readTimes(read, colors - countBits(outlineLocks), colorBits) : [];
|
||||
return { type, pattern, colors, fillLocks, fills, outlineLocks, outlines };
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
function writeFields<T>(write: WriteBits, lengthBits: number, fields: T[], writeField: (value: T) => void) {
|
||||
write(fields.length, lengthBits);
|
||||
fields.forEach(writeField);
|
||||
}
|
||||
|
||||
function readFields<T>(read: ReadBits, lengthBits: number, readField: (read: ReadBits) => T): T[] {
|
||||
const length = read(lengthBits);
|
||||
const result: T[] = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result.push(readField(read));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// pony
|
||||
|
||||
export function writePony(write: WriteBits, data: Precompressed) {
|
||||
const colorBits = Math.max(numberToBitCount(data.colors.length), 1);
|
||||
const customOutlines = !!data.booleanFields[0];
|
||||
write(data.version, VERSION_BITS);
|
||||
writeFields(write, COLORS_LENGTH_BITS, data.colors, x => write(x >> 8, 24));
|
||||
writeFields(write, BOOLEAN_FIELDS_LENGTH_BITS, data.booleanFields, x => write(x ? 1 : 0, 1));
|
||||
writeFields(write, NUMBER_FIELDS_LENGTH_BITS, data.numberFields, x => write(x, NUMBERS_BITS));
|
||||
writeFields(write, COLOR_FIELDS_LENGTH_BITS, data.colorFields, x => write(x, colorBits));
|
||||
writeFields(write, SET_FIELDS_LENGTH_BITS, data.setFields, x => writeSet(write, colorBits, customOutlines, x));
|
||||
writeFields(write, CM_LENGTH_BITS, data.cm, x => write(x, colorBits));
|
||||
}
|
||||
|
||||
const readColorValue = (read: ReadBits) => ((read(24) << 8) | 0xff) >>> 0;
|
||||
const readBoolean = (read: ReadBits) => !!read(1);
|
||||
const readBits = (bits: number) => (read: ReadBits) => read(bits);
|
||||
const readNumber = readBits(NUMBERS_BITS);
|
||||
|
||||
export function readPony(read: ReadBits): Precompressed {
|
||||
const version = read(VERSION_BITS);
|
||||
const colors = readFields(read, COLORS_LENGTH_BITS, readColorValue);
|
||||
const colorBits = Math.max(numberToBitCount(colors.length), 1);
|
||||
const readColor = readBits(colorBits);
|
||||
const booleanFields = readFields(read, BOOLEAN_FIELDS_LENGTH_BITS, readBoolean);
|
||||
const customOutlines = !!booleanFields[0];
|
||||
const numberFields = readFields(read, NUMBER_FIELDS_LENGTH_BITS, readNumber);
|
||||
const colorFields = readFields(read, COLOR_FIELDS_LENGTH_BITS, readColor);
|
||||
const setFields = readFields(read, SET_FIELDS_LENGTH_BITS, read => readSet(read, colorBits, customOutlines));
|
||||
const cm = readFields(read, CM_LENGTH_BITS, readColor);
|
||||
return { version, colors, booleanFields, numberFields, colorFields, setFields, cm };
|
||||
}
|
||||
|
||||
function writePonyToString(data: Precompressed): string {
|
||||
return fromByteArray(bitWriter(write => writePony(write, data)));
|
||||
}
|
||||
|
||||
function readPonyFromBuffer(info: Uint8Array): Precompressed {
|
||||
return readPony(bitReader(info));
|
||||
}
|
||||
|
||||
function readPonyFromString(info: string): Precompressed {
|
||||
return info ? readPonyFromBuffer(toByteArray(info)) : {
|
||||
version: VERSION,
|
||||
colors: [],
|
||||
booleanFields: [],
|
||||
numberFields: [],
|
||||
colorFields: [],
|
||||
setFields: [],
|
||||
cm: [],
|
||||
};
|
||||
}
|
||||
|
||||
// compress
|
||||
|
||||
export function compressPony(info: PonyInfoNumber): string {
|
||||
return writePonyToString(precompressPony(info, BLACK, identity));
|
||||
}
|
||||
|
||||
export function decompressPony(info: string | Uint8Array): PonyInfoNumber {
|
||||
const data = typeof info === 'string' ? readPonyFromString(info) : readPonyFromBuffer(info);
|
||||
const pony = fastPostdecompressPony(data); // postdecompressPony(data, identity);
|
||||
return syncLockedPonyInfoNumber(pony);
|
||||
}
|
||||
|
||||
// compress (string)
|
||||
|
||||
function parseColorFastSafe(color: string): number {
|
||||
return color ? parseColorFast(color) : TRANSPARENT;
|
||||
}
|
||||
|
||||
function colorToString(color: number): string {
|
||||
return color ? colorToHexRGB(color) : '';
|
||||
}
|
||||
|
||||
export function compressPonyString(info: PonyInfo): string {
|
||||
return writePonyToString(precompressPony(info, '000000', parseColorFastSafe));
|
||||
}
|
||||
|
||||
export function decompressPonyString(info: string, editable = false): PonyInfo {
|
||||
const data = readPonyFromString(info);
|
||||
const pony = postdecompressPony(data, colorToString);
|
||||
const result = editable ? merge(createBasePony(), pony) : pony;
|
||||
return syncLockedPonyInfo(result);
|
||||
}
|
||||
|
||||
// decode
|
||||
|
||||
export function decodePonyInfo(info: string | Uint8Array, paletteManager: PaletteManager): PalettePonyInfo {
|
||||
return toPaletteNumber(decompressPony(info), paletteManager);
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { Season, Holiday } from './interfaces';
|
||||
|
||||
export const SEASON: Season = Season.Summer;
|
||||
export const HOLIDAY: Holiday = Holiday.None;
|
||||
|
||||
export const SECOND = 1000;
|
||||
export const MINUTE = SECOND * 60;
|
||||
export const HOUR = MINUTE * 60;
|
||||
export const DAY = HOUR * 24;
|
||||
export const WEEK = DAY * 7;
|
||||
export const MONTH = DAY * 30;
|
||||
export const YEAR = DAY * 365;
|
||||
|
||||
export const BATCH_SIZE_MAX = 10000;
|
||||
|
||||
export const MAX_VELOCITY = 16; // do not change
|
||||
|
||||
export const PONY_TYPE = 1;
|
||||
export const PONY_SPEED_TROT = 4; // tiles per sec
|
||||
export const PONY_SPEED_WALK = 2; // tiles per sec
|
||||
|
||||
export const SAYS_TIME_MIN = 5; // sec
|
||||
export const SAYS_TIME_MAX = 8; // sec
|
||||
|
||||
export const TILE_CHANGE_RANGE = 5;
|
||||
|
||||
export const EXPRESSION_TIMEOUT = 7000; // ms
|
||||
export const FLY_DELAY = 0.4; // sec
|
||||
|
||||
export const SERVER_FPS = 10;
|
||||
|
||||
export const AFK_TIMEOUT = 15 * MINUTE;
|
||||
export const REMOVE_TIMEOUT = 15 * MINUTE;
|
||||
export const REMOVE_INTERVAL = 1 * MINUTE;
|
||||
export const MAP_DISCARD_TIMEOUT = 15 * MINUTE;
|
||||
export const MAP_SWITCH_DELAY = 1 * SECOND;
|
||||
export const MAP_SWITCHES_PER_UPDATE = 1;
|
||||
export const JOINS_PER_UPDATE = 1;
|
||||
|
||||
export const DEFAULT_CHATLOG_OPACITY = 35;
|
||||
export const MAX_CHATLOG_RANGE = 11;
|
||||
export const MIN_CHATLOG_RANGE = 2;
|
||||
|
||||
export function isChatlogRangeUnlimited(range: number | undefined) {
|
||||
return !range || range < MIN_CHATLOG_RANGE || range >= MAX_CHATLOG_RANGE;
|
||||
}
|
||||
|
||||
export const WATER_FPS = 6;
|
||||
export const WATER_HEIGHT = [0, -1, -2, -1];
|
||||
|
||||
export const CM_SIZE = 5;
|
||||
export const MIN_SCALE = 1;
|
||||
export const MAX_SCALE = 4;
|
||||
|
||||
export const SAY_MAX_LENGTH = 64;
|
||||
export const PLAYER_NAME_MAX_LENGTH = 20;
|
||||
export const PLAYER_DESC_MAX_LENGTH = 40;
|
||||
export const ACCOUNT_NAME_MIN_LENGTH = 1;
|
||||
export const ACCOUNT_NAME_MAX_LENGTH = 32;
|
||||
export const MAX_FILTER_WORDS_LENGTH = 1000;
|
||||
export const PARTY_LIMIT = 30;
|
||||
export const FRIENDS_LIMIT = 100;
|
||||
|
||||
export const HIDE_LIMIT = 1000;
|
||||
export const UNHIDE_TIMEOUT = HOUR;
|
||||
export const MIN_HIDE_TIME = HOUR;
|
||||
export const MAX_HIDE_TIME = 10 * DAY;
|
||||
|
||||
export const SWAP_TIMEOUT = 1000;
|
||||
export const MAP_LOAD_SAVE_TIMEOUT = 5000;
|
||||
|
||||
export const HIDES_PER_PAGE = 20;
|
||||
|
||||
export const LATEST_CHARACTER_LIMIT = 10;
|
||||
export const BASE_CHARACTER_LIMIT = 1000;
|
||||
export const ADDITIONAL_CHARACTERS_SUPPORTER1 = 200;
|
||||
export const ADDITIONAL_CHARACTERS_SUPPORTER2 = 300;
|
||||
export const ADDITIONAL_CHARACTERS_SUPPORTER3 = 450;
|
||||
export const ADDITIONAL_CHARACTERS_PAST_SUPPORTER = 100;
|
||||
|
||||
export const ACTIONS_LIMIT = 50;
|
||||
export const COMMAND_ACTION_TIME_DELAY = 1000;
|
||||
|
||||
export const ENTITY_TYPE_LIMIT = 0xffff;
|
||||
|
||||
export const HOUSE_ENTITY_LIMIT = 150;
|
||||
|
||||
export const CAMERA_WIDTH_MIN = 64;
|
||||
export const CAMERA_WIDTH_MAX = 0xbff; // 3071
|
||||
export const CAMERA_HEIGHT_MIN = 64;
|
||||
export const CAMERA_HEIGHT_MAX = 0x7ff; // 2047
|
||||
|
||||
export const blinkFps = 24;
|
||||
export const tileWidth = 32;
|
||||
export const tileHeight = 24;
|
||||
export const tileElevation = 20; // 24;
|
||||
|
||||
export const REGION_SIZE = 8; // in tiles
|
||||
export const REGION_WIDTH = REGION_SIZE * tileWidth;
|
||||
export const REGION_HEIGHT = REGION_SIZE * tileHeight;
|
||||
export const REGION_BORDER = 1; // in tiles
|
||||
export const TILES_RESTORE_MIN_SEC = 1; // max: TILES_RESTORE_MAX_SEC - 1
|
||||
export const TILES_RESTORE_MAX_SEC = 10; // max: 255
|
||||
|
||||
export const PONY_INFO_KEY = 0x76;
|
||||
|
||||
export const MIN_ADULT_AGE = 18;
|
||||
export const REQUEST_DATE_OF_BIRTH = true;
|
||||
|
||||
export const TIMEOUTS = [
|
||||
{ value: MINUTE * 5, label: '5 minutes' },
|
||||
{ value: MINUTE * 10, label: '10 minutes' },
|
||||
{ value: MINUTE * 30, label: '30 minutes' },
|
||||
{ value: HOUR * 1, label: '1 hour' },
|
||||
{ value: HOUR * 5, label: '5 hours' },
|
||||
{ value: HOUR * 10, label: '10 hours' },
|
||||
{ value: HOUR * 24, label: '24 hours' },
|
||||
{ value: DAY * 2, label: '2 days' },
|
||||
{ value: DAY * 5, label: '5 days' },
|
||||
];
|
||||
|
||||
export const MONTH_NAMES_EN = [
|
||||
'January',
|
||||
'February ',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
export const OFFLINE_PONY = 'DAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA==';
|
||||
export const SUPPORTER_PONY = 'CAfz9PUFLUnapSD/1wD5aFT////+hHM2QIJkJ8AQLkkADAA6jXrsBT1Iw+wBMJOqoW1C2oW1AAI=';
|
||||
|
||||
// patreon reward tier IDs
|
||||
export const rewardLevel1 = '2255086';
|
||||
export const rewardLevel2 = '2411886';
|
||||
export const rewardLevel3 = '2411888';
|
||||
|
||||
const SUPPORTER_REWARDS_COMMON = [
|
||||
`In-game supporter tag`,
|
||||
`Supporter chat color`,
|
||||
];
|
||||
|
||||
const SUPPORTER_REWARDS_MORE = [
|
||||
`Access to patreon posts`,
|
||||
`Early access to new and experimental features`,
|
||||
];
|
||||
|
||||
export const SUPPORTER_REWARDS = [
|
||||
[],
|
||||
[
|
||||
...SUPPORTER_REWARDS_COMMON,
|
||||
`${ADDITIONAL_CHARACTERS_SUPPORTER1} additional slots for saving ponies`,
|
||||
],
|
||||
[
|
||||
...SUPPORTER_REWARDS_COMMON,
|
||||
...SUPPORTER_REWARDS_MORE,
|
||||
`${ADDITIONAL_CHARACTERS_SUPPORTER2} additional slots for saving ponies`,
|
||||
],
|
||||
[
|
||||
...SUPPORTER_REWARDS_COMMON,
|
||||
...SUPPORTER_REWARDS_MORE,
|
||||
`${ADDITIONAL_CHARACTERS_SUPPORTER3} additional slots for saving ponies`,
|
||||
],
|
||||
];
|
||||
|
||||
export const SUPPORTER_REWARDS_LIST = [
|
||||
...SUPPORTER_REWARDS_COMMON,
|
||||
...SUPPORTER_REWARDS_MORE,
|
||||
`Additional slots for saving ponies`,
|
||||
];
|
||||
|
||||
export const PAST_SUPPORTER_REWARDS = [
|
||||
`${ADDITIONAL_CHARACTERS_PAST_SUPPORTER} additional slots for saving ponies`,
|
||||
];
|
||||
|
||||
export const GENERAL_RULES = [
|
||||
`Be kind to others`,
|
||||
`Don't spam`,
|
||||
`Don't use multiple accounts`,
|
||||
`Don't modify the game with hacks or scripts`,
|
||||
`Don't encourage behaviour violating the rules`,
|
||||
`Violation of the rules may result in temporary or permanent ban`,
|
||||
];
|
||||
@@ -0,0 +1,251 @@
|
||||
export const countryCodeToName: { [key: string]: string | undefined; } = {
|
||||
AD: `Andorra`,
|
||||
AE: `United Arab Emirates (the)`,
|
||||
AF: `Afghanistan`,
|
||||
AG: `Antigua and Barbuda`,
|
||||
AI: `Anguilla`,
|
||||
AL: `Albania`,
|
||||
AM: `Armenia`,
|
||||
AO: `Angola`,
|
||||
AQ: `Antarctica`,
|
||||
AR: `Argentina`,
|
||||
AS: `American Samoa`,
|
||||
AT: `Austria`,
|
||||
AU: `Australia`,
|
||||
AW: `Aruba`,
|
||||
AX: `Åland Islands`,
|
||||
AZ: `Azerbaijan`,
|
||||
BA: `Bosnia and Herzegovina`,
|
||||
BB: `Barbados`,
|
||||
BD: `Bangladesh`,
|
||||
BE: `Belgium`,
|
||||
BF: `Burkina Faso`,
|
||||
BG: `Bulgaria`,
|
||||
BH: `Bahrain`,
|
||||
BI: `Burundi`,
|
||||
BJ: `Benin`,
|
||||
BL: `Saint Barthélemy`,
|
||||
BM: `Bermuda`,
|
||||
BN: `Brunei Darussalam`,
|
||||
BO: `Bolivia (Plurinational State of)`,
|
||||
BQ: `Bonaire, Sint Eustatius and Saba`,
|
||||
BR: `Brazil`,
|
||||
BS: `Bahamas (the)`,
|
||||
BT: `Bhutan`,
|
||||
BV: `Bouvet Island`,
|
||||
BW: `Botswana`,
|
||||
BY: `Belarus`,
|
||||
BZ: `Belize`,
|
||||
CA: `Canada`,
|
||||
CC: `Cocos (Keeling) Islands (the)`,
|
||||
CD: `Congo (the Democratic Republic of the)`,
|
||||
CF: `Central African Republic (the)`,
|
||||
CG: `Congo (the)`,
|
||||
CH: `Switzerland`,
|
||||
CI: `Côte d'Ivoire`,
|
||||
CK: `Cook Islands (the)`,
|
||||
CL: `Chile`,
|
||||
CM: `Cameroon`,
|
||||
CN: `China`,
|
||||
CO: `Colombia`,
|
||||
CR: `Costa Rica`,
|
||||
CU: `Cuba`,
|
||||
CV: `Cabo Verde`,
|
||||
CW: `Curaçao`,
|
||||
CX: `Christmas Island`,
|
||||
CY: `Cyprus`,
|
||||
CZ: `Czechia`,
|
||||
DE: `Germany`,
|
||||
DJ: `Djibouti`,
|
||||
DK: `Denmark`,
|
||||
DM: `Dominica`,
|
||||
DO: `Dominican Republic (the)`,
|
||||
DZ: `Algeria`,
|
||||
EC: `Ecuador`,
|
||||
EE: `Estonia`,
|
||||
EG: `Egypt`,
|
||||
EH: `Western Sahara*`,
|
||||
ER: `Eritrea`,
|
||||
ES: `Spain`,
|
||||
ET: `Ethiopia`,
|
||||
FI: `Finland`,
|
||||
FJ: `Fiji`,
|
||||
FK: `Falkland Islands (the) [Malvinas]`,
|
||||
FM: `Micronesia (Federated States of)`,
|
||||
FO: `Faroe Islands (the)`,
|
||||
FR: `France`,
|
||||
GA: `Gabon`,
|
||||
GB: `United Kingdom of Great Britain and Northern Ireland (the)`,
|
||||
GD: `Grenada`,
|
||||
GE: `Georgia`,
|
||||
GF: `French Guiana`,
|
||||
GG: `Guernsey`,
|
||||
GH: `Ghana`,
|
||||
GI: `Gibraltar`,
|
||||
GL: `Greenland`,
|
||||
GM: `Gambia (the)`,
|
||||
GN: `Guinea`,
|
||||
GP: `Guadeloupe`,
|
||||
GQ: `Equatorial Guinea`,
|
||||
GR: `Greece`,
|
||||
GS: `South Georgia and the South Sandwich Islands`,
|
||||
GT: `Guatemala`,
|
||||
GU: `Guam`,
|
||||
GW: `Guinea-Bissau`,
|
||||
GY: `Guyana`,
|
||||
HK: `Hong Kong`,
|
||||
HM: `Heard Island and McDonald Islands`,
|
||||
HN: `Honduras`,
|
||||
HR: `Croatia`,
|
||||
HT: `Haiti`,
|
||||
HU: `Hungary`,
|
||||
ID: `Indonesia`,
|
||||
IE: `Ireland`,
|
||||
IL: `Israel`,
|
||||
IM: `Isle of Man`,
|
||||
IN: `India`,
|
||||
IO: `British Indian Ocean Territory (the)`,
|
||||
IQ: `Iraq`,
|
||||
IR: `Iran (Islamic Republic of)`,
|
||||
IS: `Iceland`,
|
||||
IT: `Italy`,
|
||||
JE: `Jersey`,
|
||||
JM: `Jamaica`,
|
||||
JO: `Jordan`,
|
||||
JP: `Japan`,
|
||||
KE: `Kenya`,
|
||||
KG: `Kyrgyzstan`,
|
||||
KH: `Cambodia`,
|
||||
KI: `Kiribati`,
|
||||
KM: `Comoros (the)`,
|
||||
KN: `Saint Kitts and Nevis`,
|
||||
KP: `Korea (the Democratic People's Republic of)`,
|
||||
KR: `Korea (the Republic of)`,
|
||||
KW: `Kuwait`,
|
||||
KY: `Cayman Islands (the)`,
|
||||
KZ: `Kazakhstan`,
|
||||
LA: `Lao People's Democratic Republic (the)`,
|
||||
LB: `Lebanon`,
|
||||
LC: `Saint Lucia`,
|
||||
LI: `Liechtenstein`,
|
||||
LK: `Sri Lanka`,
|
||||
LR: `Liberia`,
|
||||
LS: `Lesotho`,
|
||||
LT: `Lithuania`,
|
||||
LU: `Luxembourg`,
|
||||
LV: `Latvia`,
|
||||
LY: `Libya`,
|
||||
MA: `Morocco`,
|
||||
MC: `Monaco`,
|
||||
MD: `Moldova (the Republic of)`,
|
||||
ME: `Montenegro`,
|
||||
MF: `Saint Martin (French part)`,
|
||||
MG: `Madagascar`,
|
||||
MH: `Marshall Islands (the)`,
|
||||
MK: `Macedonia (the former Yugoslav Republic of)`,
|
||||
ML: `Mali`,
|
||||
MM: `Myanmar`,
|
||||
MN: `Mongolia`,
|
||||
MO: `Macao`,
|
||||
MP: `Northern Mariana Islands (the)`,
|
||||
MQ: `Martinique`,
|
||||
MR: `Mauritania`,
|
||||
MS: `Montserrat`,
|
||||
MT: `Malta`,
|
||||
MU: `Mauritius`,
|
||||
MV: `Maldives`,
|
||||
MW: `Malawi`,
|
||||
MX: `Mexico`,
|
||||
MY: `Malaysia`,
|
||||
MZ: `Mozambique`,
|
||||
NA: `Namibia`,
|
||||
NC: `New Caledonia`,
|
||||
NE: `Niger (the)`,
|
||||
NF: `Norfolk Island`,
|
||||
NG: `Nigeria`,
|
||||
NI: `Nicaragua`,
|
||||
NL: `Netherlands (the)`,
|
||||
NO: `Norway`,
|
||||
NP: `Nepal`,
|
||||
NR: `Nauru`,
|
||||
NU: `Niue`,
|
||||
NZ: `New Zealand`,
|
||||
OM: `Oman`,
|
||||
PA: `Panama`,
|
||||
PE: `Peru`,
|
||||
PF: `French Polynesia`,
|
||||
PG: `Papua New Guinea`,
|
||||
PH: `Philippines (the)`,
|
||||
PK: `Pakistan`,
|
||||
PL: `Poland`,
|
||||
PM: `Saint Pierre and Miquelon`,
|
||||
PN: `Pitcairn`,
|
||||
PR: `Puerto Rico`,
|
||||
PS: `Palestine, State of`,
|
||||
PT: `Portugal`,
|
||||
PW: `Palau`,
|
||||
PY: `Paraguay`,
|
||||
QA: `Qatar`,
|
||||
RE: `Réunion`,
|
||||
RO: `Romania`,
|
||||
RS: `Serbia`,
|
||||
RU: `Russian Federation (the)`,
|
||||
RW: `Rwanda`,
|
||||
SA: `Saudi Arabia`,
|
||||
SB: `Solomon Islands`,
|
||||
SC: `Seychelles`,
|
||||
SD: `Sudan (the)`,
|
||||
SE: `Sweden`,
|
||||
SG: `Singapore`,
|
||||
SH: `Saint Helena, Ascension and Tristan da Cunha`,
|
||||
SI: `Slovenia`,
|
||||
SJ: `Svalbard and Jan Mayen`,
|
||||
SK: `Slovakia`,
|
||||
SL: `Sierra Leone`,
|
||||
SM: `San Marino`,
|
||||
SN: `Senegal`,
|
||||
SO: `Somalia`,
|
||||
SR: `Suriname`,
|
||||
SS: `South Sudan`,
|
||||
ST: `Sao Tome and Principe`,
|
||||
SV: `El Salvador`,
|
||||
SX: `Sint Maarten (Dutch part)`,
|
||||
SY: `Syrian Arab Republic`,
|
||||
SZ: `Swaziland`,
|
||||
TC: `Turks and Caicos Islands (the)`,
|
||||
TD: `Chad`,
|
||||
TF: `French Southern Territories (the)`,
|
||||
TG: `Togo`,
|
||||
TH: `Thailand`,
|
||||
TJ: `Tajikistan`,
|
||||
TK: `Tokelau`,
|
||||
TL: `Timor-Leste`,
|
||||
TM: `Turkmenistan`,
|
||||
TN: `Tunisia`,
|
||||
TO: `Tonga`,
|
||||
TR: `Turkey`,
|
||||
TT: `Trinidad and Tobago`,
|
||||
TV: `Tuvalu`,
|
||||
TW: `Taiwan (Province of China)`,
|
||||
TZ: `Tanzania, United Republic of`,
|
||||
UA: `Ukraine`,
|
||||
UG: `Uganda`,
|
||||
UM: `United States Minor Outlying Islands (the)`,
|
||||
US: `United States of America (the)`,
|
||||
UY: `Uruguay`,
|
||||
UZ: `Uzbekistan`,
|
||||
VA: `Holy See (the)`,
|
||||
VC: `Saint Vincent and the Grenadines`,
|
||||
VE: `Venezuela (Bolivarian Republic of)`,
|
||||
VG: `Virgin Islands (British)`,
|
||||
VI: `Virgin Islands (U.S.)`,
|
||||
VN: `Viet Nam`,
|
||||
VU: `Vanuatu`,
|
||||
WF: `Wallis and Futuna`,
|
||||
WS: `Samoa`,
|
||||
YE: `Yemen`,
|
||||
YT: `Mayotte`,
|
||||
ZA: `South Africa`,
|
||||
ZM: `Zambia`,
|
||||
ZW: `Zimbabwe`,
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { MessageType } from './interfaces';
|
||||
|
||||
export const sampleMessages: { name: string; message: string; id?: number; type?: MessageType; }[] = [];
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
sampleMessages.push(
|
||||
{ name: 'Soubi', message: 'Me lo hubieras dicho al menos.', type: MessageType.Party },
|
||||
{ name: 'Doggy', message: 'Mira un menor' },
|
||||
{ name: 'carry *br*', message: 'menos frama vai...nunca te falei isso' },
|
||||
{ name: 'Doggy', message: 'A uste le gustan menores' },
|
||||
{ name: 'Doggy', message: 'Pero no soy menor de edad' },
|
||||
{ name: 'springtrap girl(:3)', message: 'pelo menos desincalho' },
|
||||
{ name: 'Foxy (Menina)', message: 'que vida em a metade do povo sabe menos nois ;-;' },
|
||||
{ name: 'Mangle (br)', message: 'mais pelo menos meus pais ta aqui em casa', type: MessageType.Party },
|
||||
{ name: 'Experiment-z30 (ESP)', message: 'Con una menos en la clase' },
|
||||
{ name: '🌙 Ň✞ĦŦΜΔŘ€ ŞΔŇŞ 🌙', message: 'aceita q doi menos' },
|
||||
{ name: '⭐✨柊|Lihan|柊✨⭐', message: 'quanta frescura no rabo mano, aceita que doi menos' },
|
||||
{ name: '🌙 Ň✞ĦŦΜΔŘ€ ŞΔŇŞ 🌙', message: 'aceita q doi menos' },
|
||||
{ name: '⭐✨柊|Lihan|柊✨⭐', message: 'quanta frescura no rabo mano, aceita que doi menos' },
|
||||
{ name: 'Ilenos Pijama', message: 'Muito menos participar' },
|
||||
{ name: 'luz(br)', message: 'pelo menos fan nao vai comer todos os lanches' },
|
||||
{ name: '=tord=', message: 'quien saque menos' },
|
||||
{ name: 'ladybug', message: 'mais ou menos .-.' },
|
||||
{ name: 'springtrap girl(:3)', message: 'pelo menos desincalho' },
|
||||
{ name: 'Foxy (Menina)', message: 'que vida em a metade do povo sabe menos nois ;-;' },
|
||||
{ name: 'Experiment-z30 (ESP)', message: 'Con una menos en la clase' },
|
||||
{ name: 'Ilenos Pijama', message: 'Muito menos participar' },
|
||||
{ name: 'Mangle (br)', message: 'mais pelo menos meus pais ta aqui em casa', type: MessageType.Party },
|
||||
{ name: 'luz(br)', message: 'pelo menos fan nao vai comer todos os lanches' },
|
||||
{
|
||||
message: '/help - show help\n/roll [[min-]max] - randomize a number\n/s - say\n/p - party chat\n/t - thinking baloon',
|
||||
name: '', type: MessageType.System,
|
||||
},
|
||||
{ name: 'Molley', message: 'Some admin message here', type: MessageType.Admin },
|
||||
{ name: 'Dolleyert', message: 'Some moderator message here', type: MessageType.Mod },
|
||||
{ name: '', message: 'The server will restart soon', type: MessageType.Announcement },
|
||||
{ name: 'Molley', message: '🎲 rolled 5 of 100', type: MessageType.Announcement },
|
||||
{ name: 'Molley', message: 'Some thinki👻n👻g 🍎 mes<b>aaa</b>sage', type: MessageType.Thinking },
|
||||
{ name: 'Molley', message: 'Some party thinking message', type: MessageType.PartyThinking },
|
||||
{ name: 'Molley', message: 'Some supporter 🙂 message 1', type: MessageType.Supporter1 },
|
||||
{ name: 'Molley', message: 'Some supporter 🙂 message 2', type: MessageType.Supporter2, id: 1 },
|
||||
{ name: 'Molley', message: 'Some supporter 🙂 message 3', type: MessageType.Supporter3, id: 2 },
|
||||
{ name: 'Molley', message: 'Some whisper message', type: MessageType.Whisper, id: 2 },
|
||||
{ name: 'WWWWWWWWWWWWWWWWWWWW', message: 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW' },
|
||||
{ name: 'WWWWWWWWWWWWWWWWWWWW', message: 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW' },
|
||||
{ name: 'tord ⚧☿♁⚨⚩⚦⚢⚣⚤', message: 'quien saque menos ⚧☿♁⚨⚩⚦⚢⚣⚤' },
|
||||
{ name: 'more symbols', message: '♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎' },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Expression, ExpressionExtra } from '../interfaces';
|
||||
import { hasFlag } from '../utils';
|
||||
|
||||
export const EMPTY_EXPRESSION = 0x1fffffff;
|
||||
|
||||
export function encodeExpression(expression: Expression | undefined): number {
|
||||
if (!expression)
|
||||
return EMPTY_EXPRESSION;
|
||||
|
||||
const { extra, rightIris, leftIris, right, left, muzzle } = expression;
|
||||
|
||||
// bits: 5 | 4 | 4 | 5 | 5 | 5 = 28/32
|
||||
return ((extra << 23) | (rightIris << 19) | (leftIris << 15) | (right << 10) | (left << 5) | muzzle) >>> 0;
|
||||
}
|
||||
|
||||
export function decodeExpression(value: number): Expression | undefined {
|
||||
value = value >>> 0;
|
||||
|
||||
if (value === EMPTY_EXPRESSION)
|
||||
return undefined;
|
||||
|
||||
const muzzle = value & 0x1f;
|
||||
const left = (value >> 5) & 0x1f;
|
||||
const right = (value >> 10) & 0x1f;
|
||||
const leftIris = (value >> 15) & 0xf;
|
||||
const rightIris = (value >> 19) & 0xf;
|
||||
const extra = (value >> 23) & 0x1f;
|
||||
|
||||
return { muzzle, left, right, leftIris, rightIris, extra };
|
||||
}
|
||||
|
||||
export function isCancellableExpression(expression: Expression) {
|
||||
return hasFlag(expression.extra, ExpressionExtra.Zzz);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
BinaryWriter, BinaryReader, writeInt16, readInt16, createBinaryReader, readUint16, readLength,
|
||||
readUint32, readUint8, readObject, readUint8Array
|
||||
} from 'ag-sockets/dist/browser';
|
||||
import { decodeString } from 'ag-sockets/dist/utf8';
|
||||
import { DecodedUpdate, DecodedRegionUpdate, TileUpdate, UpdateFlags } from '../interfaces';
|
||||
import { tileWidth, tileHeight, MAX_VELOCITY } from '../constants';
|
||||
|
||||
export function writeVelocity(writer: BinaryWriter, value: number) {
|
||||
if (value >= MAX_VELOCITY || value <= -MAX_VELOCITY) {
|
||||
throw new Error(`Exceeded max velocity (${value})`);
|
||||
}
|
||||
|
||||
writeInt16(writer, (value * 0x8000) / MAX_VELOCITY);
|
||||
}
|
||||
|
||||
export function readVelocity(reader: BinaryReader) {
|
||||
return (readInt16(reader) * MAX_VELOCITY) / 0x8000;
|
||||
}
|
||||
|
||||
export function writeCoordX(writer: BinaryWriter, value: number) {
|
||||
writeInt16(writer, (value * tileWidth) | 0);
|
||||
}
|
||||
|
||||
export function writeCoordY(writer: BinaryWriter, value: number) {
|
||||
writeInt16(writer, (value * tileHeight) | 0);
|
||||
}
|
||||
|
||||
export function readCoordX(reader: BinaryReader) {
|
||||
return readInt16(reader) / tileWidth;
|
||||
}
|
||||
|
||||
export function readCoordY(reader: BinaryReader) {
|
||||
return readInt16(reader) / tileHeight;
|
||||
}
|
||||
|
||||
export function emptyUpdate(id: number): DecodedUpdate {
|
||||
return {
|
||||
id,
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
state: undefined,
|
||||
expression: undefined,
|
||||
type: undefined,
|
||||
options: undefined,
|
||||
crc: undefined,
|
||||
name: undefined,
|
||||
filterName: false,
|
||||
info: undefined,
|
||||
action: undefined,
|
||||
switchRegion: false,
|
||||
playerState: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeUpdate(data: Uint8Array): DecodedRegionUpdate {
|
||||
const reader = createBinaryReader(data);
|
||||
const x = readUint16(reader);
|
||||
const y = readUint16(reader);
|
||||
const updates: DecodedUpdate[] = [];
|
||||
let update: DecodedUpdate | undefined;
|
||||
|
||||
while (update = readOneUpdate(reader)) {
|
||||
updates.push(update);
|
||||
}
|
||||
|
||||
const removesLength = readLength(reader);
|
||||
const removes: number[] = [];
|
||||
|
||||
for (let i = 0; i < removesLength; i++) {
|
||||
removes.push(readUint32(reader));
|
||||
}
|
||||
|
||||
const tilesLength = readLength(reader);
|
||||
const tiles: TileUpdate[] = [];
|
||||
|
||||
for (let i = 0; i < tilesLength; i++) {
|
||||
tiles.push({
|
||||
x: readUint8(reader),
|
||||
y: readUint8(reader),
|
||||
type: readUint8(reader),
|
||||
});
|
||||
}
|
||||
|
||||
const tileData = readUint8Array(reader);
|
||||
|
||||
return { x, y, updates, removes, tiles, tileData };
|
||||
}
|
||||
|
||||
export function readOneUpdate(reader: BinaryReader): DecodedUpdate | undefined {
|
||||
if (reader.offset >= reader.view.byteLength)
|
||||
return undefined;
|
||||
|
||||
const flags = readUint16(reader);
|
||||
|
||||
if (flags === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const id = readUint32(reader);
|
||||
const update = emptyUpdate(id);
|
||||
|
||||
update.switchRegion = (flags & UpdateFlags.SwitchRegion) !== 0;
|
||||
|
||||
if ((flags & UpdateFlags.Position) !== 0) {
|
||||
update.x = readCoordX(reader);
|
||||
update.y = readCoordY(reader);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Velocity) !== 0) {
|
||||
update.vx = readVelocity(reader);
|
||||
update.vy = readVelocity(reader);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.State) !== 0) {
|
||||
update.state = readUint8(reader);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Expression) !== 0) {
|
||||
update.expression = readUint32(reader);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Type) !== 0) {
|
||||
update.type = readUint16(reader);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Options) !== 0) {
|
||||
update.options = readObject(reader);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Info) !== 0) {
|
||||
update.crc = readUint16(reader);
|
||||
update.info = readUint8Array(reader)!;
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Action) !== 0) {
|
||||
update.action = readUint8(reader);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Name) !== 0) {
|
||||
update.name = decodeString(readUint8Array(reader)) || undefined;
|
||||
update.filterName = (flags & UpdateFlags.NameBad) !== 0;
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.PlayerState) !== 0) {
|
||||
update.playerState = readUint8(reader);
|
||||
}
|
||||
|
||||
return update;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
BinaryWriter, writeUint32, writeUint16, writeUint8, writeObject, writeUint8Array, writeLength
|
||||
} from 'ag-sockets/dist/browser';
|
||||
import { UpdateFlags, EntityPlayerState, Action } from '../interfaces';
|
||||
import { writeBinary } from '../binaryUtils';
|
||||
import { ServerEntity, IClient, ServerRegion } from '../../server/serverInterfaces';
|
||||
import { isEntityShadowed } from '../../server/entityUtils';
|
||||
import { getRegionTiles } from '../../server/serverRegion';
|
||||
import { writeCoordX, writeVelocity, writeCoordY } from './updateDecoder';
|
||||
import { getPlayerState } from '../../server/playerUtils';
|
||||
import { logger } from '../../server/logger';
|
||||
|
||||
function getOptionsOrUndefined(entity: ServerEntity) {
|
||||
return (entity.options !== undefined && Object.keys(entity.options).length > 0) ? entity.options : undefined;
|
||||
}
|
||||
|
||||
export function writeOneUpdate(
|
||||
writer: BinaryWriter, entity: ServerEntity, flags: UpdateFlags, x: number, y: number, vx: number, vy: number,
|
||||
options: any, action: Action, playerState: EntityPlayerState
|
||||
) {
|
||||
if (DEVELOPMENT && flags === 0) {
|
||||
logger.error(`Writing empty update`);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Position) !== 0) {
|
||||
flags |= UpdateFlags.State;
|
||||
|
||||
if (vx || vy) {
|
||||
flags |= UpdateFlags.Velocity;
|
||||
}
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Name) !== 0 && entity.nameBad === true) {
|
||||
flags |= UpdateFlags.NameBad;
|
||||
}
|
||||
|
||||
writeUint16(writer, flags);
|
||||
writeUint32(writer, entity.id);
|
||||
|
||||
if ((flags & UpdateFlags.Position) !== 0) {
|
||||
writeCoordX(writer, x);
|
||||
writeCoordY(writer, y);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Velocity) !== 0) {
|
||||
writeVelocity(writer, vx);
|
||||
writeVelocity(writer, vy);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.State) !== 0) {
|
||||
writeUint8(writer, entity.state);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Expression) !== 0) {
|
||||
writeUint32(writer, entity.options!.expr!);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Type) !== 0) {
|
||||
writeUint16(writer, entity.type);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Options) !== 0) {
|
||||
writeObject(writer, options);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Info) !== 0) {
|
||||
writeUint16(writer, entity.crc!);
|
||||
writeUint8Array(writer, entity.encryptedInfoSafe!);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Action) !== 0) {
|
||||
writeUint8(writer, action!);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.Name) !== 0) {
|
||||
writeUint8Array(writer, entity.encodedName!);
|
||||
}
|
||||
|
||||
if ((flags & UpdateFlags.PlayerState) !== 0) {
|
||||
writeUint8(writer, playerState!);
|
||||
}
|
||||
}
|
||||
|
||||
export function writeOneEntity(writer: BinaryWriter, entity: ServerEntity, client: IClient) {
|
||||
const { x, y, vx, vy } = entity;
|
||||
// TODO: const expression = !!entity.options && !!entity.options.expr; // instead of in options
|
||||
const options = getOptionsOrUndefined(entity);
|
||||
const playerState = getPlayerState(client, entity);
|
||||
|
||||
let flags = UpdateFlags.Position | UpdateFlags.State | UpdateFlags.Type;
|
||||
|
||||
if (entity.encryptedInfoSafe !== undefined) {
|
||||
flags |= UpdateFlags.Info;
|
||||
}
|
||||
|
||||
if (entity.encodedName !== undefined) {
|
||||
flags |= UpdateFlags.Name;
|
||||
}
|
||||
|
||||
if (playerState !== 0) {
|
||||
flags |= UpdateFlags.PlayerState;
|
||||
}
|
||||
|
||||
if (options !== undefined) {
|
||||
flags |= UpdateFlags.Options;
|
||||
}
|
||||
|
||||
writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, Action.None, playerState);
|
||||
}
|
||||
|
||||
export function writeUpdate(writer: BinaryWriter, region: ServerRegion) {
|
||||
const { x, y, entityUpdates, entityRemoves, tileUpdates } = region;
|
||||
|
||||
writeUint16(writer, x);
|
||||
writeUint16(writer, y);
|
||||
|
||||
for (const { entity, flags, x, y, vx, vy, options, action, playerState } of entityUpdates) {
|
||||
writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, action, playerState);
|
||||
}
|
||||
|
||||
writeUint16(writer, 0); // end marker
|
||||
|
||||
writeLength(writer, entityRemoves.length);
|
||||
|
||||
for (const remove of entityRemoves) {
|
||||
writeUint32(writer, remove);
|
||||
}
|
||||
|
||||
writeLength(writer, tileUpdates.length);
|
||||
|
||||
for (const { x, y, type: tile } of tileUpdates) {
|
||||
writeUint8(writer, x);
|
||||
writeUint8(writer, y);
|
||||
writeUint8(writer, tile);
|
||||
}
|
||||
|
||||
writeUint8Array(writer, null); // tile data
|
||||
}
|
||||
|
||||
export function writeRegion(writer: BinaryWriter, region: ServerRegion, client: IClient) {
|
||||
const { x, y, entities } = region;
|
||||
|
||||
writeUint16(writer, x);
|
||||
writeUint16(writer, y);
|
||||
|
||||
for (const entity of entities) {
|
||||
if (!isEntityShadowed(entity) || entity === client.pony) {
|
||||
writeOneEntity(writer, entity, client);
|
||||
}
|
||||
}
|
||||
|
||||
writeUint16(writer, 0); // end marker
|
||||
|
||||
writeLength(writer, 0); // removes
|
||||
writeLength(writer, 0); // tile updates
|
||||
writeUint8Array(writer, getRegionTiles(region)); // tile data
|
||||
}
|
||||
|
||||
// For testing
|
||||
export function encodeUpdateSimple(region: ServerRegion) {
|
||||
return writeBinary(writer => writeUpdate(writer, region));
|
||||
}
|
||||
|
||||
// For testing
|
||||
export function encodeRegionSimple(region: ServerRegion, client: IClient) {
|
||||
return writeBinary(writer => writeRegion(writer, region, client));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,225 @@
|
||||
import { sort } from 'timsort';
|
||||
import {
|
||||
Entity, EntityState, BodyAnimation, Point, EntityFlags, IMap, Pony, Says, EntityPlayerState, WorldMap
|
||||
} from './interfaces';
|
||||
import { hasFlag, distance, pushUniq, setFlag } from './utils';
|
||||
import { stand, sit, lie, fly, flyBug, swim } from '../client/ponyAnimations';
|
||||
import { releasePony, isPony } from './pony';
|
||||
import { toScreenX, toScreenY } from './positionUtils';
|
||||
import { releasePalette } from '../graphics/paletteManager';
|
||||
import { rect } from './rect';
|
||||
import { addOrRemoveFromEntityList } from './worldMap';
|
||||
import { PONY_TYPE } from './constants';
|
||||
import { isStaticCollision } from './collision';
|
||||
|
||||
export function releaseEntity(entity: Entity) {
|
||||
if (isPony(entity)) {
|
||||
releasePony(entity);
|
||||
}
|
||||
|
||||
if (entity.palettes !== undefined) {
|
||||
for (const palette of entity.palettes) {
|
||||
releasePalette(palette);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function addChatBubble(map: WorldMap, entity: Entity, says: Says) {
|
||||
entity.says = says;
|
||||
pushUniq(map.entitiesWithChat, entity);
|
||||
}
|
||||
|
||||
export function updateEntityVelocity(map: WorldMap, entity: Entity, vx: number, vy: number) {
|
||||
const wasMoving = isMoving(entity);
|
||||
entity.vx = vx;
|
||||
entity.vy = vy;
|
||||
const isMovingNow = isMoving(entity);
|
||||
addOrRemoveFromEntityList(map.entitiesMoving, entity, wasMoving, isMovingNow);
|
||||
}
|
||||
|
||||
export function compareEntities(a: Entity, b: Entity) {
|
||||
return (toScreenY(a.y) - toScreenY(b.y))
|
||||
|| (a.order - b.order)
|
||||
|| (b.id - a.id)
|
||||
|| (toScreenX(a.x) - toScreenX(b.x))
|
||||
|| (toScreenY(a.z) - toScreenY(b.z)
|
||||
);
|
||||
}
|
||||
|
||||
export function sortEntities(entities: Entity[]) {
|
||||
sort(entities, compareEntities);
|
||||
}
|
||||
|
||||
export function closestEntity(point: Point, entities: Entity[]): Entity | undefined {
|
||||
return entities.reduce((best, entity) => distance(point, entity) < distance(point, best) ? entity : best, entities[0]);
|
||||
}
|
||||
|
||||
export function getBoopRect(entity: Entity) {
|
||||
const right = hasFlag(entity.state, EntityState.FacingRight);
|
||||
const sitting = isPonySitting(entity);
|
||||
return rect(entity.x + (right ? 0.6 : -0.9) * (sitting ? 0.6 : 1), entity.y - 0.2, 0.3, 0.4);
|
||||
}
|
||||
|
||||
export function isMoving(entity: Entity) {
|
||||
return entity.vx !== 0 || entity.vy !== 0;
|
||||
}
|
||||
|
||||
export function isDrawable(entity: Entity) {
|
||||
return entity.type === PONY_TYPE || entity.draw !== undefined;
|
||||
}
|
||||
|
||||
export function canLand<T>(entity: Entity, map: IMap<T>) {
|
||||
return !isStaticCollision(entity, map, true);
|
||||
}
|
||||
|
||||
export function canStand<T>(entity: Entity, map: IMap<T>) {
|
||||
return !isPonyStanding(entity) && isPonyLandedOrCanLand(entity, map);
|
||||
}
|
||||
|
||||
export function canSit<T>(entity: Entity, map: IMap<T>) {
|
||||
return !isPonySitting(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity);
|
||||
}
|
||||
|
||||
export function canLie<T>(entity: Entity, map: IMap<T>) {
|
||||
return !isPonyLying(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity);
|
||||
}
|
||||
|
||||
export function entityInRange(entity: Entity, player: Entity) {
|
||||
return (!entity.interactRange || distance(player, entity) < entity.interactRange);
|
||||
}
|
||||
|
||||
export function getInteractBounds(pony: Pony) {
|
||||
const boundsWidth = 1;
|
||||
const boundsHeight = 1;
|
||||
const boundsOffset = 0.5 + (isPonySitting(pony) ? -0.3 : (isPonyLying(pony) ? -0.2 : 0));
|
||||
|
||||
return rect(
|
||||
toScreenX(isFacingRight(pony) ? (pony.x + boundsOffset) : (pony.x - boundsOffset - boundsWidth)),
|
||||
toScreenY(pony.y - boundsHeight / 2),
|
||||
toScreenX(boundsWidth),
|
||||
toScreenY(boundsHeight));
|
||||
}
|
||||
|
||||
export const SIT_ON_BOUNDS_WIDTH = 1.2;
|
||||
export const SIT_ON_BOUNDS_HEIGHT = 0.5;
|
||||
export const SIT_ON_BOUNDS_OFFSET = 0.4;
|
||||
|
||||
export function getSitOnBounds(pony: Pony) {
|
||||
const width = SIT_ON_BOUNDS_WIDTH;
|
||||
const height = SIT_ON_BOUNDS_HEIGHT;
|
||||
const offset = isFacingRight(pony) ? -SIT_ON_BOUNDS_OFFSET : (SIT_ON_BOUNDS_OFFSET - SIT_ON_BOUNDS_WIDTH);
|
||||
return rect(toScreenX(pony.x + offset), toScreenY(pony.y - SIT_ON_BOUNDS_HEIGHT / 2), toScreenX(width), toScreenY(height));
|
||||
}
|
||||
|
||||
// pony state
|
||||
|
||||
export function isIdleAnimation(animation: BodyAnimation) {
|
||||
return animation === stand || animation === sit || animation === lie || animation === fly ||
|
||||
animation === flyBug || animation === swim;
|
||||
}
|
||||
|
||||
export function isIdle(pony: Pony) {
|
||||
return !isMoving(pony) && isIdleAnimation(pony.ponyState.animation);
|
||||
}
|
||||
|
||||
export function canBoop(pony: Pony) {
|
||||
return isIdle(pony);
|
||||
}
|
||||
|
||||
export function canBoop2(entity: Entity) {
|
||||
return !isMoving(entity) && (isPonyStanding(entity) || isPonySitting(entity) || isPonyLying(entity) || isPonyFlying(entity));
|
||||
}
|
||||
|
||||
// entity player state
|
||||
|
||||
export function isHidden(entity: Entity) {
|
||||
return (entity.playerState & EntityPlayerState.Hidden) !== 0;
|
||||
}
|
||||
|
||||
export function isIgnored(entity: Entity) {
|
||||
return (entity.playerState & EntityPlayerState.Ignored) !== 0;
|
||||
}
|
||||
|
||||
export function isFriend(entity: Entity) {
|
||||
return (entity.playerState & EntityPlayerState.Friend) !== 0;
|
||||
}
|
||||
|
||||
export function isInTheAir(entity: Entity) {
|
||||
return isFlying(entity) && (entity.inTheAirDelay === undefined || entity.inTheAirDelay <= 0);
|
||||
}
|
||||
|
||||
// entity state
|
||||
|
||||
export function isFlying(entity: Entity) {
|
||||
return (entity.state & EntityState.Flying) !== 0;
|
||||
}
|
||||
|
||||
export function isFacingRight(entity: Entity) {
|
||||
return (entity.state & EntityState.FacingRight) !== 0;
|
||||
}
|
||||
|
||||
export function hasHeadTurned(entity: Entity) {
|
||||
return (entity.state & EntityState.HeadTurned) !== 0;
|
||||
}
|
||||
|
||||
export function isHeadFacingRight(entity: Entity) {
|
||||
const headTurned = hasHeadTurned(entity);
|
||||
const facingRight = isFacingRight(entity);
|
||||
return facingRight ? !headTurned : headTurned;
|
||||
}
|
||||
|
||||
export function getPonyState(state: EntityState): EntityState {
|
||||
return state & EntityState.PonyStateMask;
|
||||
}
|
||||
|
||||
export function setPonyState(state: EntityState, set: EntityState) {
|
||||
state = (state & ~EntityState.PonyStateMask) | set;
|
||||
state = setFlag(state, EntityState.Flying, set === EntityState.PonyFlying);
|
||||
return state;
|
||||
}
|
||||
|
||||
export function isSittingState(state: EntityState) {
|
||||
return getPonyState(state) === EntityState.PonySitting;
|
||||
}
|
||||
|
||||
export function isLyingState(state: EntityState) {
|
||||
return getPonyState(state) === EntityState.PonyLying;
|
||||
}
|
||||
|
||||
export function isPonyWalking(entity: Entity) {
|
||||
return getPonyState(entity.state) === EntityState.PonyWalking;
|
||||
}
|
||||
|
||||
export function isPonyTrotting(entity: Entity) {
|
||||
return getPonyState(entity.state) === EntityState.PonyTrotting;
|
||||
}
|
||||
|
||||
export function isPonySitting(entity: Entity) {
|
||||
return getPonyState(entity.state) === EntityState.PonySitting;
|
||||
}
|
||||
|
||||
export function isPonyStanding(entity: Entity) {
|
||||
return getPonyState(entity.state) === EntityState.PonyStanding;
|
||||
}
|
||||
|
||||
export function isPonyLying(entity: Entity) {
|
||||
return getPonyState(entity.state) === EntityState.PonyLying;
|
||||
}
|
||||
|
||||
export function isPonyFlying(entity: Entity) {
|
||||
return getPonyState(entity.state) === EntityState.PonyFlying;
|
||||
}
|
||||
|
||||
export function isPonyLandedOrCanLand<T>(entity: Entity, map: IMap<T>) {
|
||||
return !isPonyFlying(entity) || canLand(entity, map);
|
||||
}
|
||||
|
||||
// entity flags
|
||||
|
||||
export function isDecal(entity: Entity) {
|
||||
return (entity.flags & EntityFlags.Decal) !== 0;
|
||||
}
|
||||
|
||||
export function isCritter(entity: Entity) {
|
||||
return (entity.flags & EntityFlags.Critter) !== 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const WEBGL_CREATION_ERROR = 'Failed to create WebGL context';
|
||||
export const ACCESS_ERROR = 'Access denied';
|
||||
export const ACCOUNT_ERROR = 'Invalid account';
|
||||
export const NOT_FOUND_ERROR = 'Not found';
|
||||
export const OFFLINE_ERROR = 'Server is offline';
|
||||
export const PROTECTION_ERROR = 'DDOS protection error, reload the page to continue';
|
||||
export const VERSION_ERROR = 'Invalid version';
|
||||
export const BROWSER_NOT_SUPPORTED_ERROR = 'Your browser is not supported';
|
||||
export const NAME_ERROR = 'Invalid name';
|
||||
export const CHARACTER_SAVING_ERROR = 'Error saving character';
|
||||
export const CHARACTER_LIMIT_ERROR = 'Character limit reached';
|
||||
export const NOT_AUTHENTICATED_ERROR = 'Not authenticated';
|
||||
@@ -0,0 +1,352 @@
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import { Muzzle, Eye, Expression, Iris, ExpressionExtra, Dict } from './interfaces';
|
||||
import { createPlainMap } from './utils';
|
||||
|
||||
const double = (items: string[]) => items.map(x => x + x);
|
||||
const prefix = (items: string[], fix: string) => items.map(x => fix + x);
|
||||
const suffix = (items: string[], fix: string) => items.map(x => x + fix);
|
||||
|
||||
export const THREE_LETTER_WORDS = [
|
||||
'ace', 'act', 'ama', 'amp', 'amo', 'amu', 'amy', 'ana', 'ane', 'and', 'ant', 'any', 'ape', 'app', 'apo',
|
||||
'apt', 'ava', 'ave', 'avo', 'awe', 'awn', 'awp', 'axe',
|
||||
'boa', 'bob', 'bod', 'bog', 'bon', 'boo', 'bop', 'bot', 'boy', 'bub', 'bud', 'bug', 'bup', 'but', 'bun', 'buy',
|
||||
'dad', 'doe', 'dog', 'dot', 'doy', 'dna', 'dub', 'dud', 'due', 'dun', 'dug', 'duo', 'dup', 'dva', 'dvd',
|
||||
'eco', 'ecu', 'eme', 'emu', 'emo', 'eon', 'end', 'eng', 'eva', 'eve', 'exe', 'exp',
|
||||
'gnu', 'goa', 'god', 'gog', 'gon', 'goo', 'got', 'gud', 'gut', 'gun', 'guv', 'guy',
|
||||
'nnn', 'nog', 'non', 'noo', 'nop', 'not', 'nun', 'nut', 'nub',
|
||||
'oca', 'omo', 'one', 'ooo', 'oot', 'ope', 'opt', 'oud', 'out', 'ova', 'owe', 'own', 'oxo', 'oxe', 'omg',
|
||||
'pay', 'pnp', 'pod', 'pon', 'poo', 'pop', 'pot', 'pov', 'ppp', 'pub', 'pud', 'pug', 'pup', 'pun', 'put', 'pvp',
|
||||
'qqq', 'que', 'qua',
|
||||
'tnt', 'ton', 'top', 'tod', 'toe', 'tog', 'too', 'toy', 'tub', 'tug', 'tun', 'twa', 'two',
|
||||
'uuu', 'una', 'und', 'uno', 'ump', 'upo', 'uva',
|
||||
'voe', 'voy', 'vpn', 'vug', 'vvv',
|
||||
'yay', 'yob', 'yod', 'yon', 'you', 'yup',
|
||||
];
|
||||
|
||||
export const TWO_LETTER_WORDS = [
|
||||
'ox', 'ex', 'by', 'my', 'up', 'of', 'if', 'me', 'ow', 'am', 'we', 'uh', 'um', 'be', 'em', 'bi', 'oh',
|
||||
'go', 'eh', 'ah', 'ye', 'ya', 'he', 'hi', 'ho', 'ha', 'yo', 'us', 'on', 'id', 'an', 'do', 'no',
|
||||
'as', 'at', 'it', 'is', 'or', 'so', 'to', 'pc',
|
||||
];
|
||||
|
||||
const threeLetterWords = new RegExp(`^(${THREE_LETTER_WORDS.join('|')})$`);
|
||||
const twoLetterWords = new RegExp(`^(${TWO_LETTER_WORDS.join('|')})$`);
|
||||
|
||||
// vertical :)
|
||||
|
||||
const smilesRight = [')', ']', '}', '>'];
|
||||
const smilesLeft = ['(', '[', '{', '<', 'C', 'c'];
|
||||
const flatBoth = ['|', 'i', 'l'];
|
||||
const concernedBoth = ['/', '\\', 's', 'S', '?'];
|
||||
const muzzlesBoth = [
|
||||
[Muzzle.Scrunch, 't', 'T', 'I'],
|
||||
[Muzzle.Blep, 'P', 'p', 'd'],
|
||||
[Muzzle.FlatBlep, 'b'],
|
||||
[Muzzle.Flat, ...flatBoth],
|
||||
[Muzzle.Concerned, ...concernedBoth],
|
||||
[Muzzle.ConcernedOpen, '0', 'v'],
|
||||
[Muzzle.ConcernedOpen2, 'O'],
|
||||
[Muzzle.Oh, 'o'],
|
||||
[Muzzle.Kiss, '*', 'x', 'X'],
|
||||
[Muzzle.NeutralPant, 'L'],
|
||||
[Muzzle.SmilePant, 'Q'],
|
||||
[Muzzle.FrownOpen, 'V'],
|
||||
[Muzzle.NeutralOpen2, 'u', 'n'],
|
||||
[Muzzle.NeutralOpen3, 'U'],
|
||||
[Muzzle.NeutralTeeth, ...double(flatBoth)],
|
||||
[Muzzle.ConcernedTeeth, ...double(concernedBoth)],
|
||||
];
|
||||
|
||||
export const muzzlesRight = createMap<Muzzle>([
|
||||
...muzzlesBoth,
|
||||
[Muzzle.Smile, '3', ...smilesRight],
|
||||
[Muzzle.Frown, ...smilesLeft],
|
||||
[Muzzle.SmileOpen, 'D'],
|
||||
[Muzzle.SmileOpen2, 'DD'],
|
||||
[Muzzle.SmileOpen3, 'DDD'],
|
||||
[Muzzle.SmileTeeth, ...double(smilesRight)],
|
||||
[Muzzle.FrownTeeth, ...double(smilesLeft)],
|
||||
]);
|
||||
|
||||
export const muzzlesLeft = createMap<Muzzle>([
|
||||
...muzzlesBoth,
|
||||
[Muzzle.Smile, ...smilesLeft],
|
||||
[Muzzle.Frown, ...smilesRight],
|
||||
[Muzzle.ConcernedOpen2, 'D'],
|
||||
[Muzzle.ConcernedOpen3, 'DD', 'DDD'],
|
||||
[Muzzle.SmileTeeth, ...double(smilesLeft)],
|
||||
[Muzzle.FrownTeeth, ...double(smilesRight)],
|
||||
]);
|
||||
|
||||
const neutralEyes = [';', ':', '=', '%', '8'];
|
||||
|
||||
const verticalEyesBoth = [
|
||||
[Eye.Neutral, ...neutralEyes],
|
||||
[Eye.X, 'X', 'x'],
|
||||
[Eye.Neutral3, 'B'],
|
||||
[Eye.Lines, '|'],
|
||||
];
|
||||
|
||||
export const verticalEyesRight = createMap<Eye>([
|
||||
...verticalEyesBoth,
|
||||
[Eye.Angry, ...prefix(neutralEyes, '>')],
|
||||
[Eye.Angry2, '>B'],
|
||||
[Eye.Sad, ...prefix(neutralEyes, '<')],
|
||||
[Eye.Sad2, '<B'],
|
||||
[Eye.Frown, ...prefix(neutralEyes, '|')],
|
||||
[Eye.Frown2, '|B'],
|
||||
]);
|
||||
|
||||
export const verticalEyesLeft = createMap<Eye>([
|
||||
...verticalEyesBoth,
|
||||
[Eye.Angry, ...suffix(neutralEyes, '<')],
|
||||
[Eye.Sad, ...suffix(neutralEyes, '>')],
|
||||
[Eye.Frown, ...suffix(neutralEyes, '|')],
|
||||
]);
|
||||
|
||||
// horizontal -_-
|
||||
|
||||
export const horizontalMuzzles = createMap<Muzzle>([
|
||||
[Muzzle.Smile, 'c', 'C', 'v', 'V', 'u', 'U', 'w', 'W', '👃'],
|
||||
[Muzzle.SmilePant, 'Q', 'P'],
|
||||
[Muzzle.Frown, 'n', 'm', '^'],
|
||||
[Muzzle.Neutral, '-', '//'],
|
||||
[Muzzle.NeutralPant, 'q', 'p'],
|
||||
[Muzzle.Flat, '_'],
|
||||
[Muzzle.Kiss, '.', ',', '*', 'x', 'X', '3'],
|
||||
[Muzzle.Concerned, '~'],
|
||||
[Muzzle.ConcernedOpen, 'o'],
|
||||
[Muzzle.ConcernedOpen2, 'A', 'O', '0'],
|
||||
]);
|
||||
|
||||
const horizontalEyes = [
|
||||
[Eye.Neutral, `'`, '.', '0', '°', 'o', 'O', 'e', 'g', '9', '6', 'd', 'b'],
|
||||
[Eye.Neutral4, '='],
|
||||
[Eye.Closed, '-', 'v', 'V', 'u', 'U', 'y', 'Y'],
|
||||
[Eye.ClosedHappy, 'n'],
|
||||
[Eye.ClosedHappy2, '^'],
|
||||
[Eye.Sad, 'q', 'Q', 'p', 'P', ';', ':', ','],
|
||||
[Eye.Peaceful, 't', 'T'],
|
||||
[Eye.Frown, 'ô', 'Ô', 'õ', 'Õ', 'ō', 'Ō', 'ŏ', 'Ŏ'],
|
||||
[Eye.Frown2, 'a'],
|
||||
];
|
||||
|
||||
export const horizontalEyesLeft = createMap<Eye>([
|
||||
...horizontalEyes,
|
||||
[Eye.Neutral2, '>'],
|
||||
[Eye.X, '<'],
|
||||
[Eye.Sad, 'ò', 'Ò'],
|
||||
[Eye.Angry, 'ó', 'Ó'],
|
||||
]);
|
||||
|
||||
export const horizontalEyesRight = createMap<Eye>([
|
||||
...horizontalEyes,
|
||||
[Eye.Neutral2, '<'],
|
||||
[Eye.X, '>'],
|
||||
[Eye.Sad, 'ó', 'Ó'],
|
||||
[Eye.Angry, 'ò', 'Ò'],
|
||||
]);
|
||||
|
||||
const horizontalIrises = createMap<Iris>([
|
||||
[Iris.Up, '9'],
|
||||
[Iris.UpLeft, 'e'],
|
||||
[Iris.UpRight, 'g'],
|
||||
[Iris.Right, '<', 'd'],
|
||||
[Iris.Left, '>', 'b'],
|
||||
]);
|
||||
|
||||
const muzzleToEye: Eye[] = [];
|
||||
muzzleToEye[Muzzle.Frown] = Eye.Sad;
|
||||
muzzleToEye[Muzzle.FrownOpen] = Eye.Sad;
|
||||
muzzleToEye[Muzzle.ConcernedOpen2] = Eye.Sad;
|
||||
muzzleToEye[Muzzle.ConcernedOpen3] = Eye.Sad;
|
||||
|
||||
const neutralToSmile: Muzzle[] = [];
|
||||
neutralToSmile[Muzzle.ConcernedOpen] = Muzzle.SmileOpen2;
|
||||
neutralToSmile[Muzzle.ConcernedOpen2] = Muzzle.SmileOpen3;
|
||||
|
||||
function any(obj: object) {
|
||||
return `(${Object.keys(obj).map(escapeRegExp).join('|')})`;
|
||||
}
|
||||
|
||||
const bigEyes = /[O0ÒÓÔÕŌŎQ]/;
|
||||
const cryingEye = /[;pqPQTyY]/;
|
||||
const tears = "(['`,]?)";
|
||||
const tearsRegex = /['`,]/;
|
||||
const verticalRightRegex = new RegExp(`^${any(verticalEyesRight)}${tears}-?${any(muzzlesRight)}$`);
|
||||
const verticalLeftRegex = new RegExp(`^${any(muzzlesLeft)}-?${tears}${any(verticalEyesLeft)}$`);
|
||||
const horizontalRegex = new RegExp(`^${any(horizontalEyesRight)}(//)?${any(horizontalMuzzles)}(//)?${any(horizontalEyesLeft)}$`);
|
||||
|
||||
function matchVertical(
|
||||
text: string, regex: RegExp, flip: boolean, muzzleMap: Dict<Muzzle>, eyesMap: Dict<Eye>
|
||||
): Expression | undefined {
|
||||
if (/^([|]{2,}|BS|8x|x8|x-?x|\d+)$/i.test(text))
|
||||
return undefined;
|
||||
|
||||
const match = regex.exec(text);
|
||||
|
||||
if (!match)
|
||||
return undefined;
|
||||
|
||||
const eyesStr = flip ? match[3] : match[1];
|
||||
const muzzleStr = flip ? match[1] : match[3];
|
||||
const muzzle = muzzleMap[muzzleStr];
|
||||
const veye = eyesMap[eyesStr];
|
||||
const eye = veye === Eye.Neutral && !/[OV]/.test(muzzleStr) ? (muzzleToEye[muzzle] || veye) : veye;
|
||||
const blink = /;/.test(eyesStr);
|
||||
const tear = blink && muzzleToEye[muzzle] === Eye.Sad;
|
||||
const left = tear ? (/[<>]/.test(eyesStr) ? eye : Eye.Sad2) : (blink && flip ? Eye.Closed : eye);
|
||||
const right = tear ? (/[<>]/.test(eyesStr) ? eye : Eye.Sad2) : (blink && !flip ? Eye.Closed : eye);
|
||||
const shocked = /8/.test(eyesStr);
|
||||
const rightIris = shocked ? Iris.Shocked : Iris.Forward;
|
||||
const leftIris = shocked ? Iris.Shocked : (/%/.test(eyesStr) ? Iris.Up : Iris.Forward);
|
||||
const extra = (tearsRegex.test(match[2]) || tear) ? ExpressionExtra.Tears : ExpressionExtra.None;
|
||||
|
||||
return { right, left, muzzle, rightIris, leftIris, extra };
|
||||
}
|
||||
|
||||
function matchHorizontal(text: string): Expression | undefined {
|
||||
if (/\.\.|--|vv|uu|qq|pp|nn|^\d+$/i.test(text)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (/[a-zA-Z][a-z][a-z]|[A-Z]{3}/.test(text)) {
|
||||
const clear = text.replace(/[^a-z]/ig, '').toLowerCase();
|
||||
|
||||
if (clear.length === 3 && threeLetterWords.test(clear)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (/[a-z][a-z][.,*-]/i.test(text)) {
|
||||
const clear = text.replace(/[^a-z]/ig, '').toLowerCase();
|
||||
|
||||
if (clear.length === 2 && twoLetterWords.test(clear)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const match = horizontalRegex.exec(text);
|
||||
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [, rightStr, rightBlush, muzzleStr, leftBlush, leftStr] = match;
|
||||
|
||||
if ((rightBlush || leftBlush) && rightBlush !== leftBlush) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const leftEye = horizontalEyesLeft[leftStr];
|
||||
const rightEye = horizontalEyesRight[rightStr];
|
||||
const muzzle = horizontalMuzzles[muzzleStr];
|
||||
const same = rightStr === leftStr;
|
||||
const lookingToSide = same && /[<>]/.test(rightStr);
|
||||
const shocked = bigEyes.test(leftStr) && bigEyes.test(rightStr) && rightStr !== '0' && leftStr !== '0';
|
||||
const lookingDown = (same && rightStr === '6') || (rightStr === 'b' && leftStr === 'd');
|
||||
const unamused = !lookingDown && same && rightStr === '-' && /[.,_]/.test(muzzleStr);
|
||||
const left = (lookingToSide || (leftStr === 'o' && bigEyes.test(rightStr))) ? Eye.Neutral2 : leftEye;
|
||||
const right = (lookingToSide || (rightStr === 'o' && bigEyes.test(leftStr))) ? Eye.Neutral2 : rightEye;
|
||||
const blush = /[/][/]/.test(muzzleStr) || (rightBlush && rightBlush === leftBlush);
|
||||
const cry = cryingEye.test(leftStr) || cryingEye.test(rightStr);
|
||||
|
||||
return {
|
||||
left: unamused ? Eye.Frown2 : left,
|
||||
right: unamused ? Eye.Frown2 : right,
|
||||
muzzle: same && (leftEye === Eye.ClosedHappy || leftEye === Eye.ClosedHappy2) ? (neutralToSmile[muzzle] || muzzle) : muzzle,
|
||||
rightIris: lookingDown ? Iris.Down : (shocked ? Iris.Shocked : (horizontalIrises[rightStr] || Iris.Forward)),
|
||||
leftIris: lookingDown ? Iris.Down : (shocked ? Iris.Shocked : (horizontalIrises[leftStr] || Iris.Forward)),
|
||||
extra: (blush ? ExpressionExtra.Blush : ExpressionExtra.None) | (cry ? ExpressionExtra.Cry : ExpressionExtra.None),
|
||||
};
|
||||
}
|
||||
|
||||
export function expression(
|
||||
right: Eye, left: Eye, muzzle: Muzzle, rightIris = Iris.Forward, leftIris = Iris.Forward, extra = ExpressionExtra.None
|
||||
): Expression {
|
||||
return { right, left, muzzle, rightIris, leftIris, extra };
|
||||
}
|
||||
|
||||
const constants = createPlainMap<() => Expression | undefined>({
|
||||
'^^': () => expression(Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.Smile),
|
||||
'))': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile),
|
||||
'((': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Frown),
|
||||
'>>': () => expression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Left, Iris.Left),
|
||||
'<<': () => expression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Right),
|
||||
'🙂': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile),
|
||||
'😵': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Up, Iris.Forward),
|
||||
'😐': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Flat),
|
||||
'😑': () => expression(Eye.Lines, Eye.Lines, Muzzle.Flat),
|
||||
'😆': () => expression(Eye.X, Eye.X, Muzzle.SmileOpen),
|
||||
'😟': () => expression(Eye.Sad, Eye.Sad, Muzzle.Neutral),
|
||||
'😠': () => expression(Eye.Angry, Eye.Angry, Muzzle.Smile),
|
||||
'🤔': () => expression(Eye.Neutral, Eye.Frown2, Muzzle.Kiss),
|
||||
'😈': () => expression(Eye.Angry, Eye.Angry, Muzzle.Smile, Iris.Up, Iris.Forward),
|
||||
'👿': () => expression(Eye.Angry, Eye.Angry, Muzzle.SmileTeeth),
|
||||
});
|
||||
|
||||
function matchOther(text: string): Expression | undefined {
|
||||
if (/^A{5,}\.*$/.test(text)) {
|
||||
return expression(Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3, Iris.Shocked, Iris.Shocked);
|
||||
} else if (/^a{5,}\.*$/.test(text)) {
|
||||
return expression(Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3);
|
||||
} else if (/^z{3,}\.*$/i.test(text)) {
|
||||
return expression(Eye.Closed, Eye.Closed, Muzzle.Neutral);
|
||||
} else {
|
||||
return constants[text] && constants[text]();
|
||||
}
|
||||
}
|
||||
|
||||
export function matchExpression(text: string): Expression | undefined {
|
||||
if (/тот/ui.test(text)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
text = replaceRussian(text)
|
||||
.replace(/D{4,}/, 'DDD')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\/{3,}/g, '//');
|
||||
|
||||
return matchVertical(text, verticalRightRegex, false, muzzlesRight, verticalEyesRight)
|
||||
|| matchVertical(text, verticalLeftRegex, true, muzzlesLeft, verticalEyesLeft)
|
||||
|| matchHorizontal(text)
|
||||
|| matchOther(text);
|
||||
}
|
||||
|
||||
export function parseExpression(text: string): Expression | undefined {
|
||||
const emoteMatch = /(?:^| )(\S+)\s*$/.exec(text);
|
||||
const emote = emoteMatch && emoteMatch[1].trim();
|
||||
return emote ? matchExpression(emote) : undefined;
|
||||
}
|
||||
|
||||
function createMap<T>(values: any[][]): Dict<T> {
|
||||
return values.reduce((obj: Dict<T>, [exp, ...values]) => (values.forEach(v => obj[v] = exp), obj), Object.create(null));
|
||||
}
|
||||
|
||||
const charMap = createPlainMap<string>({
|
||||
'З': '3', 'з': '3', 'Э': '3', 'э': '3',
|
||||
'А': 'A', 'а': 'a', 'Д': 'A', 'д': 'A',
|
||||
'В': 'B', 'в': 'B',
|
||||
'Г': 'L',
|
||||
'М': 'M', 'м': 'M',
|
||||
'О': 'O', 'о': 'o',
|
||||
'П': 'n', 'п': 'n',
|
||||
'Р': 'P', 'р': 'p',
|
||||
'С': 'C', 'с': 'c',
|
||||
'Т': 'T', 'т': 'T',
|
||||
'Х': 'X', 'х': 'x',
|
||||
'Ш': 'W', 'ш': 'w',
|
||||
'Ь': 'b', 'ь': 'b',
|
||||
'е': 'e',
|
||||
'у': 'y', 'У': 'Y',
|
||||
});
|
||||
|
||||
const charRegex = new RegExp(`[${Object.keys(charMap).join('')}]`, 'g');
|
||||
|
||||
function mapChar(x: string) {
|
||||
return charMap[x];
|
||||
}
|
||||
|
||||
function replaceRussian(text: string): string {
|
||||
return text.replace(charRegex, mapChar);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import { Eye, Muzzle, Iris, ExpressionExtra } from './interfaces';
|
||||
import { THREE_LETTER_WORDS, TWO_LETTER_WORDS } from './expressionUtils';
|
||||
|
||||
type Result = undefined
|
||||
| [Eye, Eye, Muzzle]
|
||||
| [Eye, Eye, Muzzle, Iris, Iris]
|
||||
| [Eye, Eye, Muzzle, Iris, Iris, ExpressionExtra];
|
||||
|
||||
export const expressions: [string, Result][] = [
|
||||
// invalid
|
||||
['', undefined],
|
||||
['a', undefined],
|
||||
['123', undefined],
|
||||
[':::', undefined],
|
||||
['XDK', undefined],
|
||||
['fooXD', undefined],
|
||||
[':) hey', undefined],
|
||||
['тот', undefined],
|
||||
// in text
|
||||
[' :) ', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['hi :)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
// horizontal (right)
|
||||
[':-)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
[':)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['=)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
[':]', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
[':>', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
[':}', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
[':3', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['rus :з', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['rus :э', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
[':(', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
|
||||
[':[', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
|
||||
[':C', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
|
||||
[':c', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
|
||||
['rus :С', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
|
||||
['rus :с', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
|
||||
[':<', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
|
||||
[':{', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
|
||||
[':I', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
|
||||
[':t', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
|
||||
[':T', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
|
||||
['rus :Т', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
|
||||
['rus :т', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
|
||||
[':P', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
|
||||
[':p', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
|
||||
[':d', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
|
||||
[':b', [Eye.Neutral, Eye.Neutral, Muzzle.FlatBlep]],
|
||||
['rus :Р', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
|
||||
['rus :р', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
|
||||
[':D', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen]],
|
||||
[':DD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen2]],
|
||||
[':DDD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen3]],
|
||||
[':DDDDD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen3]],
|
||||
[':O', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen2]],
|
||||
[':0', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen]],
|
||||
[':o', [Eye.Neutral, Eye.Neutral, Muzzle.Oh]],
|
||||
[':|', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
[':l', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
[':i', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
[':v', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen]],
|
||||
[':V', [Eye.Neutral, Eye.Neutral, Muzzle.FrownOpen]],
|
||||
[':u', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen2]],
|
||||
[':n', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen2]],
|
||||
[':U', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen3]],
|
||||
[':*', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
|
||||
[':x', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
|
||||
[':X', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
|
||||
[':/', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
|
||||
[':\\', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
|
||||
[':S', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
|
||||
[':s', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
|
||||
[':?', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
|
||||
['>:(', [Eye.Angry, Eye.Angry, Muzzle.Frown]],
|
||||
['>:<', [Eye.Angry, Eye.Angry, Muzzle.Frown]],
|
||||
['<:>', [Eye.Sad, Eye.Sad, Muzzle.Smile]],
|
||||
['XD', [Eye.X, Eye.X, Muzzle.SmileOpen]],
|
||||
['xD', [Eye.X, Eye.X, Muzzle.SmileOpen]],
|
||||
[':L', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralPant]],
|
||||
[':Q', [Eye.Neutral, Eye.Neutral, Muzzle.SmilePant]],
|
||||
['B)', [Eye.Neutral3, Eye.Neutral3, Muzzle.Smile]],
|
||||
['8)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Shocked, Iris.Shocked]],
|
||||
['>8)', [Eye.Angry, Eye.Angry, Muzzle.Smile, Iris.Shocked, Iris.Shocked]],
|
||||
['<:)', [Eye.Sad, Eye.Sad, Muzzle.Smile]],
|
||||
['>B)', [Eye.Angry2, Eye.Angry2, Muzzle.Smile]],
|
||||
['<B)', [Eye.Sad2, Eye.Sad2, Muzzle.Smile]],
|
||||
['|:)', [Eye.Frown, Eye.Frown, Muzzle.Smile]],
|
||||
['|B)', [Eye.Frown2, Eye.Frown2, Muzzle.Smile]],
|
||||
['|)', [Eye.Lines, Eye.Lines, Muzzle.Smile]],
|
||||
[':))', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]],
|
||||
[':]]', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]],
|
||||
[':||', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralTeeth]],
|
||||
[':((', [Eye.Neutral, Eye.Neutral, Muzzle.FrownTeeth]],
|
||||
[':[[', [Eye.Neutral, Eye.Neutral, Muzzle.FrownTeeth]],
|
||||
['://', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedTeeth]],
|
||||
[':SS', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedTeeth]],
|
||||
[';)', [Eye.Closed, Eye.Neutral, Muzzle.Smile]],
|
||||
[';(', [Eye.Sad2, Eye.Sad2, Muzzle.Frown, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
|
||||
['>;(', [Eye.Angry, Eye.Angry, Muzzle.Frown, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
|
||||
['%)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Up]],
|
||||
[`c':`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
|
||||
[`:')`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
|
||||
[`:'(`, [Eye.Sad, Eye.Sad, Muzzle.Frown, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
|
||||
[`=,)`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
|
||||
['=`)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
|
||||
// TODO: :@ :y :'9
|
||||
// horizontal (left)
|
||||
['(-:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['|:', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
['(:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['[:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['c:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['C:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['rus с:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['rus С:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['):', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
|
||||
['D:', [Eye.Sad, Eye.Sad, Muzzle.ConcernedOpen2]],
|
||||
['DD:', [Eye.Sad, Eye.Sad, Muzzle.ConcernedOpen3]],
|
||||
['DDDDD:', [Eye.Sad, Eye.Sad, Muzzle.ConcernedOpen3]],
|
||||
['D:<', [Eye.Angry, Eye.Angry, Muzzle.ConcernedOpen2]],
|
||||
['D8<', [Eye.Angry, Eye.Angry, Muzzle.ConcernedOpen2, Iris.Shocked, Iris.Shocked]],
|
||||
['):<', [Eye.Angry, Eye.Angry, Muzzle.Frown]],
|
||||
['v:', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen]],
|
||||
['/:', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
|
||||
['(:>', [Eye.Sad, Eye.Sad, Muzzle.Smile]],
|
||||
['(:|', [Eye.Frown, Eye.Frown, Muzzle.Smile]],
|
||||
['(|', [Eye.Lines, Eye.Lines, Muzzle.Smile]],
|
||||
['((:', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]],
|
||||
['[[:', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]],
|
||||
['||:', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralTeeth]],
|
||||
[')):', [Eye.Neutral, Eye.Neutral, Muzzle.FrownTeeth]],
|
||||
['(;', [Eye.Neutral, Eye.Closed, Muzzle.Smile]],
|
||||
// horizontal (invalid)
|
||||
['||', undefined],
|
||||
['|||', undefined],
|
||||
['>||', undefined],
|
||||
['>xD', undefined],
|
||||
['(X<', undefined],
|
||||
['x-x', undefined],
|
||||
// vertical
|
||||
['-_-', [Eye.Frown2, Eye.Frown2, Muzzle.Flat]],
|
||||
['-.-', [Eye.Frown2, Eye.Frown2, Muzzle.Kiss]],
|
||||
['-,-', [Eye.Frown2, Eye.Frown2, Muzzle.Kiss]],
|
||||
['^_^', [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.Flat]],
|
||||
['-_^', [Eye.Closed, Eye.ClosedHappy2, Muzzle.Flat]],
|
||||
['o_O', [Eye.Neutral2, Eye.Neutral, Muzzle.Flat]],
|
||||
['0_o', [Eye.Neutral, Eye.Neutral2, Muzzle.Flat]],
|
||||
['o_o', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
['o,o', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
|
||||
['ono', [Eye.Neutral, Eye.Neutral, Muzzle.Frown]],
|
||||
['O_O', [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
|
||||
['OoO', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen, Iris.Shocked, Iris.Shocked]],
|
||||
['0_0', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
['°_°', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
['0.0', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
|
||||
['._.', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
[',_,', [Eye.Sad, Eye.Sad, Muzzle.Flat]],
|
||||
['v_V', [Eye.Closed, Eye.Closed, Muzzle.Flat]],
|
||||
['u_U', [Eye.Closed, Eye.Closed, Muzzle.Flat]],
|
||||
['n_n', [Eye.ClosedHappy, Eye.ClosedHappy, Muzzle.Flat]],
|
||||
['>_<', [Eye.X, Eye.X, Muzzle.Flat, Iris.Left, Iris.Right]],
|
||||
['>c<', [Eye.X, Eye.X, Muzzle.Smile, Iris.Left, Iris.Right]],
|
||||
[`'c'`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['-C-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
|
||||
['rus -с-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
|
||||
['-v-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
|
||||
['-V-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
|
||||
['-U-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
|
||||
['-u-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
|
||||
['-w-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
|
||||
['-W-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
|
||||
[`-👃-`, [Eye.Closed, Eye.Closed, Muzzle.Smile]],
|
||||
[`'_'`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
[`-*-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
|
||||
[`-x-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
|
||||
[`-X-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
|
||||
[`>x<`, [Eye.X, Eye.X, Muzzle.Kiss, Iris.Left, Iris.Right]],
|
||||
[`-o-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen]],
|
||||
[`-O-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen2]],
|
||||
[`-0-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen2]],
|
||||
[`^o^`, [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.SmileOpen2]],
|
||||
[`^O^`, [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.SmileOpen3]],
|
||||
[`-n-`, [Eye.Closed, Eye.Closed, Muzzle.Frown]],
|
||||
[`-m-`, [Eye.Closed, Eye.Closed, Muzzle.Frown]],
|
||||
[`-^-`, [Eye.Closed, Eye.Closed, Muzzle.Frown]],
|
||||
[`-~-`, [Eye.Closed, Eye.Closed, Muzzle.Concerned]],
|
||||
[`-3-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
|
||||
[`rus -з-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
|
||||
[`rus -э-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
|
||||
[`-q-`, [Eye.Closed, Eye.Closed, Muzzle.NeutralPant]],
|
||||
[`-p-`, [Eye.Closed, Eye.Closed, Muzzle.NeutralPant]],
|
||||
[`-P-`, [Eye.Closed, Eye.Closed, Muzzle.SmilePant]],
|
||||
[`-Q-`, [Eye.Closed, Eye.Closed, Muzzle.SmilePant]],
|
||||
[`-A-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen2]],
|
||||
[`q-q`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`p-p`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`p-q`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`rus р-р`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`;-;`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`:-:`, [Eye.Sad, Eye.Sad, Muzzle.Neutral]],
|
||||
[`P-P`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`rus Р-Р`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`t-t`, [Eye.Peaceful, Eye.Peaceful, Muzzle.Neutral]],
|
||||
[`Т_Т`, [Eye.Peaceful, Eye.Peaceful, Muzzle.Flat, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`rus Т_т`, [Eye.Peaceful, Eye.Peaceful, Muzzle.Flat, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`Q-Q`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Shocked, Iris.Shocked, ExpressionExtra.Cry]],
|
||||
[`y-y`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`Y-Y`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`у-у`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`У-У`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
|
||||
[`ò_ó`, [Eye.Angry, Eye.Angry, Muzzle.Flat]],
|
||||
[`ó_ò`, [Eye.Sad, Eye.Sad, Muzzle.Flat]],
|
||||
[`ô_ô`, [Eye.Frown, Eye.Frown, Muzzle.Flat]],
|
||||
[`õ_õ`, [Eye.Frown, Eye.Frown, Muzzle.Flat]],
|
||||
[`ō_ō`, [Eye.Frown, Eye.Frown, Muzzle.Flat]],
|
||||
[`ŏ_ŏ`, [Eye.Frown, Eye.Frown, Muzzle.Flat]],
|
||||
[`Ò_Ó`, [Eye.Angry, Eye.Angry, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
|
||||
[`Ô_Ô`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
|
||||
[`Õ_Õ`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
|
||||
[`Ō_Ō`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
|
||||
[`Ŏ_Ŏ`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
|
||||
[`=_=`, [Eye.Neutral4, Eye.Neutral4, Muzzle.Flat]],
|
||||
[`a_a`, [Eye.Frown2, Eye.Frown2, Muzzle.Flat]],
|
||||
[`e_e`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.UpLeft, Iris.UpLeft]],
|
||||
[`е_е`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.UpLeft, Iris.UpLeft]],
|
||||
[`g_g`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.UpRight, Iris.UpRight]],
|
||||
[`9_9`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Up, Iris.Up]],
|
||||
[`6_9`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Forward, Iris.Up]],
|
||||
['>_>', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Left, Iris.Left]],
|
||||
['<_<', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Right]],
|
||||
['<_>', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left]],
|
||||
['d_d', [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Right, Iris.Right]],
|
||||
['b_b', [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Left, Iris.Left]],
|
||||
['twO', [Eye.Peaceful, Eye.Neutral, Muzzle.Smile]],
|
||||
['o//o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
|
||||
['o/////o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
|
||||
['o\\\\o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
|
||||
['o\\\\\\o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
|
||||
['>//<', [Eye.X, Eye.X, Muzzle.Neutral, Iris.Left, Iris.Right, ExpressionExtra.Blush]],
|
||||
['-//v//-', [Eye.Closed, Eye.Closed, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
|
||||
['-///v///-', [Eye.Closed, Eye.Closed, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
|
||||
[';//v//;', [Eye.Sad, Eye.Sad, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush | ExpressionExtra.Cry]],
|
||||
[`6_6`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Down, Iris.Down]],
|
||||
['6.6', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss, Iris.Down, Iris.Down]],
|
||||
['bcd', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Down, Iris.Down]],
|
||||
// TODO: o-o' o-o' ~_~ @_@ o=o oyo *_* (amazed) -_-/ -_-\ D_D
|
||||
// vertical (short)
|
||||
['^^', [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.Smile]],
|
||||
['))', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['((', [Eye.Neutral, Eye.Neutral, Muzzle.Frown]],
|
||||
['<<', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Right]],
|
||||
['>>', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Left, Iris.Left]],
|
||||
// vertical (invalid)
|
||||
...[
|
||||
'---', '...', '000', 'QQQ', 'One', 'Up.', 'UP.',
|
||||
...THREE_LETTER_WORDS,
|
||||
...THREE_LETTER_WORDS.map(x => x.toUpperCase()),
|
||||
...TWO_LETTER_WORDS.map(x => x + '.'),
|
||||
...TWO_LETTER_WORDS.map(x => x + ','),
|
||||
...TWO_LETTER_WORDS.map(x => x + '-'),
|
||||
...TWO_LETTER_WORDS.map(x => x + '*'),
|
||||
].map(x => [x, undefined] as [string, any]),
|
||||
['BS', undefined],
|
||||
['x8', undefined],
|
||||
['8x', undefined],
|
||||
['xx', undefined],
|
||||
['-//c-', undefined],
|
||||
['-c//-', undefined],
|
||||
['030', undefined],
|
||||
['80', undefined],
|
||||
// other
|
||||
[`aaaaa`, [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3]],
|
||||
[`AAAAAA`, [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3, Iris.Shocked, Iris.Shocked]],
|
||||
[`aaaaa...`, [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3]],
|
||||
[`zzz`, [Eye.Closed, Eye.Closed, Muzzle.Neutral]],
|
||||
[`ZZZZZ`, [Eye.Closed, Eye.Closed, Muzzle.Neutral]],
|
||||
[`zzz...`, [Eye.Closed, Eye.Closed, Muzzle.Neutral]],
|
||||
// emoji
|
||||
['🙂', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
|
||||
['😵', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Up, Iris.Forward]],
|
||||
['😐', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
|
||||
['😑', [Eye.Lines, Eye.Lines, Muzzle.Flat]],
|
||||
['😆', [Eye.X, Eye.X, Muzzle.SmileOpen]],
|
||||
['😟', [Eye.Sad, Eye.Sad, Muzzle.Neutral]],
|
||||
['😠', [Eye.Angry, Eye.Angry, Muzzle.Smile]],
|
||||
['🤔', [Eye.Neutral, Eye.Frown2, Muzzle.Kiss]],
|
||||
['😈', [Eye.Angry, Eye.Angry, Muzzle.Smile, Iris.Up, Iris.Forward]],
|
||||
['👿', [Eye.Angry, Eye.Angry, Muzzle.SmileTeeth]],
|
||||
// unsafe faces
|
||||
[':L', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralPant]],
|
||||
[':Q', [Eye.Neutral, Eye.Neutral, Muzzle.SmilePant]],
|
||||
// safe replacements
|
||||
[':u', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen2]],
|
||||
[':DD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen2]],
|
||||
];
|
||||
@@ -0,0 +1,164 @@
|
||||
import { fromPairs } from 'lodash';
|
||||
import { matchRomaji, replaceRomaji } from '../client/clientUtils';
|
||||
import { flatten } from './utils';
|
||||
|
||||
const MAX_REPEATS = 16; // needs to be even for emoji
|
||||
|
||||
export const ipRegexText = '(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})';
|
||||
export const ipExceptionRegex = /\d\.\d\.\d\.\d/ui;
|
||||
export const urlExceptionRegex = /^(battle|paint|f(im|an)fiction)\.net$/ui;
|
||||
export const urlRegexTexts = [
|
||||
'https?:?//\\S+',
|
||||
'\\bwww\\.[^. ]\\S+',
|
||||
'\\S+[^. ]\\. *(c[o0]m|net)\\b',
|
||||
'\\S+[^. ] *\\.(c[o0]m|net)\\b',
|
||||
'(^| )[a-z][a-z0-9]{2,}[.,][a-z]{2,3}(/[a-z0-9_?=+-]+)+\\b',
|
||||
];
|
||||
|
||||
export function trimRepeatedLetters(test: string): string {
|
||||
if (test.length > MAX_REPEATS && (/^.?(.)\1+$/u.test(test) || /^.?(..)\1+$/u.test(test))) {
|
||||
return test.substr(0, MAX_REPEATS) + '…';
|
||||
} else {
|
||||
return test;
|
||||
}
|
||||
}
|
||||
|
||||
function createCharacterMap(data: string[][]): { [key: string]: string; } {
|
||||
const mappings = data.map(([to, from]) => from.split(/ /g).map(x => [x, to]));
|
||||
return fromPairs(flatten(mappings));
|
||||
}
|
||||
|
||||
const characters = createCharacterMap([
|
||||
[`'`, 'Ъ ъ Ь ь'],
|
||||
['a', 'á ă ắ ặ ằ ẳ ẵ ǎ â ấ ậ ầ ẩ ẫ ä ǟ ȧ ǡ ạ ȁ à ả ȃ ā ą ᶏ ẚ å ǻ ḁ ⱥ ã ɐ ₐ А а @ α'],
|
||||
['A', 'Á Ă Ắ Ặ Ằ Ẳ Ẵ Ǎ Â Ấ Ậ Ầ Ẩ Ẫ Ä Ǟ Ȧ Ǡ Ạ Ȁ À Ả Ȃ Ā Ą Å Ǻ Ḁ Ⱥ Ã Ɐ ᴀ'],
|
||||
['aa', 'ꜳ'],
|
||||
['AA', 'Ꜳ'],
|
||||
['ae', 'æ ǽ ǣ ᴂ'],
|
||||
['AE', 'Æ Ǽ Ǣ ᴁ'],
|
||||
['ao', 'ꜵ'],
|
||||
['AO', 'Ꜵ'],
|
||||
['au', 'ꜷ'],
|
||||
['AU', 'Ꜷ'],
|
||||
['av', 'ꜹ ꜻ'],
|
||||
['AV', 'Ꜹ Ꜻ'],
|
||||
['ay', 'ꜽ'],
|
||||
['AY', 'Ꜽ'],
|
||||
['b', 'ḃ ḅ ɓ ḇ ᵬ ᶀ ƀ ƃ б'],
|
||||
['B', 'Ḃ Ḅ Ɓ Ḇ Ƀ Ƃ ʙ ᴃ Б'],
|
||||
['c', 'ć č ç ḉ ĉ ɕ ċ ƈ ȼ ↄ ꜿ'],
|
||||
['C', 'Ć Č Ç Ḉ Ĉ Ċ Ƈ Ȼ Ꜿ ᴄ'],
|
||||
['ch', 'ч'],
|
||||
['CH', 'Ч'],
|
||||
['d', 'ď ḑ ḓ ȡ ḋ ḍ ɗ ᶑ ḏ ᵭ ᶁ đ ɖ ƌ ꝺ д'],
|
||||
['D', 'Ď Ḑ Ḓ Ḋ Ḍ Ɗ Ḏ Dz Dž Đ Ƌ Ꝺ ᴅ Д'],
|
||||
['dz', 'dz dž'],
|
||||
['DZ', 'DZ DŽ'],
|
||||
['e', 'é ĕ ě ȩ ḝ ê ế ệ ề ể ễ ḙ ë ė ẹ ȅ è ẻ ȇ ē ḗ ḕ ⱸ ę ᶒ ɇ ẽ ḛ ɛ ᶓ ɘ ǝ ₑ е э ε'],
|
||||
['E', 'É Ĕ Ě Ȩ Ḝ Ê Ế Ệ Ề Ể Ễ Ḙ Ë Ė Ẹ Ȅ È Ẻ Ȇ Ē Ḗ Ḕ Ę Ɇ Ẽ Ḛ Ɛ Ǝ ᴇ ⱻ Е Э'],
|
||||
['et', 'ꝫ'],
|
||||
['ET', 'Ꝫ'],
|
||||
['f', 'ḟ ƒ ᵮ ᶂ ꝼ ф'],
|
||||
['F', 'Ḟ Ƒ Ꝼ ꜰ Ф'],
|
||||
['ff', 'ff'],
|
||||
['ffi', 'ffi'],
|
||||
['ffl', 'ffl'],
|
||||
['fi', 'fi'],
|
||||
['fl', 'fl'],
|
||||
['g', 'ǵ ğ ǧ ģ ĝ ġ ɠ ḡ ᶃ ǥ ᵹ ɡ ᵷ г'],
|
||||
['G', 'Ǵ Ğ Ǧ Ģ Ĝ Ġ Ɠ Ḡ Ǥ Ᵹ ɢ ʛ Г'],
|
||||
['h', 'ḫ ȟ ḩ ĥ ⱨ ḧ ḣ ḥ ɦ ẖ ħ ɥ ʮ ʯ х'],
|
||||
['H', 'Ḫ Ȟ Ḩ Ĥ Ⱨ Ḧ Ḣ Ḥ Ħ ʜ Х'],
|
||||
['hv', 'ƕ'],
|
||||
['i', 'ı í ĭ ǐ î ï ḯ ị ȉ ì ỉ ȋ ī į ᶖ ɨ ĩ ḭ ᴉ ᵢ й ы и ι'],
|
||||
['I', 'Í Ĭ Ǐ Î Ï Ḯ İ Ị Ȉ Ì Ỉ Ȋ Ī Į Ɨ Ĩ Ḭ ɪ Й Ы И'],
|
||||
['ij', 'ij'],
|
||||
['IJ', 'IJ'],
|
||||
['is', 'ꝭ'],
|
||||
['IS', 'Ꝭ'],
|
||||
['j', 'ȷ ɟ ʄ ǰ ĵ ʝ ɉ ⱼ'],
|
||||
['J', 'Ĵ Ɉ ᴊ'],
|
||||
['k', 'ḱ ǩ ķ ⱪ ꝃ ḳ ƙ ḵ ᶄ ꝁ ꝅ ʞ к'],
|
||||
['K', 'Ḱ Ǩ Ķ Ⱪ Ꝃ Ḳ Ƙ Ḵ Ꝁ Ꝅ ᴋ К'],
|
||||
['l', 'ĺ ƚ ɬ ľ ļ ḽ ȴ ḷ ḹ ⱡ ꝉ ḻ ŀ ɫ ᶅ ɭ ł ꞁ л'],
|
||||
['L', 'Ĺ Ƚ Ľ Ļ Ḽ Ḷ Ḹ Ⱡ Ꝉ Ḻ Ŀ Ɫ Lj Ł Ꞁ ʟ ᴌ Л'],
|
||||
['lj', 'lj'],
|
||||
['LJ', 'LJ'],
|
||||
['m', 'ḿ ṁ ṃ ɱ ᵯ ᶆ ɯ ɰ м'],
|
||||
['M', 'Ḿ Ṁ Ṃ Ɱ Ɯ ᴍ М'],
|
||||
['n', 'ń ň ņ ṋ ȵ ṅ ṇ ǹ ɲ ṉ ƞ ᵰ ᶇ ɳ ñ н η'],
|
||||
['N', 'Ń Ň Ņ Ṋ Ṅ Ṇ Ǹ Ɲ Ṉ Ƞ Nj Ñ ɴ ᴎ Н'],
|
||||
['nj', 'nj'],
|
||||
['NJ', 'NJ'],
|
||||
['o', 'ɵ ó ŏ ǒ ô ố ộ ồ ổ ỗ ö ȫ ȯ ȱ ọ ő ȍ ò ỏ ơ ớ ợ ờ ở ỡ ȏ ꝋ ꝍ ⱺ ō ṓ ṑ ǫ ǭ ø ǿ õ ṍ ṏ ȭ ɔ ᶗ ᴑ ᴓ ₒ о'],
|
||||
['O', 'Ó Ŏ Ǒ Ô Ố Ộ Ồ Ổ Ỗ Ö Ȫ Ȯ Ȱ Ọ Ő Ȍ Ò Ỏ Ơ Ớ Ợ Ờ Ở Ỡ Ȏ Ꝋ Ꝍ Ō Ṓ Ṑ Ɵ Ǫ Ǭ Ø Ǿ Õ Ṍ Ṏ Ȭ Ɔ ᴏ ᴐ О'],
|
||||
['oe', 'ᴔ œ'],
|
||||
['OE', 'Œ ɶ'],
|
||||
['oi', 'ƣ'],
|
||||
['OI', 'Ƣ'],
|
||||
['oo', 'ꝏ'],
|
||||
['OO', 'Ꝏ'],
|
||||
['ou', 'ȣ'],
|
||||
['OU', 'Ȣ ᴕ'],
|
||||
['p', 'ṕ ṗ ꝓ ƥ ᵱ ᶈ ꝕ ᵽ ꝑ п'],
|
||||
['P', 'Ṕ Ṗ Ꝓ Ƥ Ꝕ Ᵽ Ꝑ ᴘ П'],
|
||||
['q', 'ꝙ ʠ ɋ ꝗ'],
|
||||
['Q', 'Ꝙ Ꝗ'],
|
||||
['r', 'ꞃ ŕ ř ŗ ṙ ṛ ṝ ȑ ɾ ᵳ ȓ ṟ ɼ ᵲ ᶉ ɍ ɽ ɿ ɹ ɻ ɺ ⱹ ᵣ р'],
|
||||
['R', 'Ꞃ Ŕ Ř Ŗ Ṙ Ṛ Ṝ Ȑ Ȓ Ṟ Ɍ Ɽ ʁ ʀ ᴙ ᴚ Р ®'],
|
||||
['s', 'ꞅ ſ ẜ ẛ ẝ ś ṥ š ṧ ş ŝ ș ṡ ṣ ṩ ʂ ᵴ ᶊ ȿ с'],
|
||||
['S', 'Ꞅ Ś Ṥ Š Ṧ Ş Ŝ Ș Ṡ Ṣ Ṩ ꜱ С $'],
|
||||
['sch', 'щ'],
|
||||
['SCH', 'Щ'],
|
||||
['sh', 'ш'],
|
||||
['SH', 'Ш'],
|
||||
['ss', 'ß'],
|
||||
['st', 'st'],
|
||||
['t', 'ꞇ ť ţ ṱ ț ȶ ẗ ⱦ ṫ ṭ ƭ ṯ ᵵ ƫ ʈ ŧ ʇ т'],
|
||||
['T', 'Ꞇ Ť Ţ Ṱ Ț Ⱦ Ṫ Ṭ Ƭ Ṯ Ʈ Ŧ ᴛ Т'],
|
||||
['th', 'ᵺ'],
|
||||
['ts', 'ц'],
|
||||
['TS', 'Ц'],
|
||||
['tz', 'ꜩ'],
|
||||
['TZ', 'Ꜩ'],
|
||||
['u', 'ᴝ ú ŭ ǔ û ṷ ü ǘ ǚ ǜ ǖ ṳ ụ ű ȕ ù ủ ư ứ ự ừ ử ữ ȗ ū ṻ ų ᶙ ů ũ ṹ ṵ ᵤ у'],
|
||||
['U', 'Ú Ŭ Ǔ Û Ṷ Ü Ǘ Ǚ Ǜ Ǖ Ṳ Ụ Ű Ȕ Ù Ủ Ư Ứ Ự Ừ Ử Ữ Ȗ Ū Ṻ Ų Ů Ũ Ṹ Ṵ ᴜ У'],
|
||||
['ue', 'ᵫ'],
|
||||
['um', 'ꝸ'],
|
||||
['v', 'ʌ ⱴ ꝟ ṿ ʋ ᶌ ⱱ ṽ ᵥ в'],
|
||||
['V', 'Ʌ Ꝟ Ṿ Ʋ Ṽ ᴠ В'],
|
||||
['vy', 'ꝡ'],
|
||||
['VY', 'Ꝡ'],
|
||||
['w', 'ʍ ẃ ŵ ẅ ẇ ẉ ẁ ⱳ ẘ'],
|
||||
['W', 'Ẃ Ŵ Ẅ Ẇ Ẉ Ẁ Ⱳ ᴡ'],
|
||||
['x', 'ẍ ẋ ᶍ ₓ'],
|
||||
['X', 'Ẍ Ẋ'],
|
||||
['y', 'ʎ ý ŷ ÿ ẏ ỵ ỳ ƴ ỷ ỿ ȳ ẙ ɏ ỹ'],
|
||||
['Y', 'Ý Ŷ Ÿ Ẏ Ỵ Ỳ Ƴ Ỷ Ỿ Ȳ Ɏ Ỹ ʏ'],
|
||||
['ya', 'я'],
|
||||
['Ya', 'Я'],
|
||||
['yo', 'ё'],
|
||||
['YO', 'Ё'],
|
||||
['yu', 'ю'],
|
||||
['YU', 'Ю'],
|
||||
['z', 'ź ž ẑ ʑ ⱬ ż ẓ ȥ ẕ ᵶ ᶎ ʐ ƶ ɀ з'],
|
||||
['Z', 'Ź Ž Ẑ Ⱬ Ż Ẓ Ȥ Ẕ Ƶ ᴢ З'],
|
||||
['zh', 'ж'],
|
||||
['ZH', 'Ж'],
|
||||
]);
|
||||
|
||||
const nonAscii = /[^A-Za-z0-9]/g;
|
||||
|
||||
export function latinize(text: string): string {
|
||||
return text
|
||||
.replace(matchRomaji, replaceRomaji)
|
||||
.replace(nonAscii, x => characters[x] || x);
|
||||
}
|
||||
|
||||
export function latinize2(name: string): string {
|
||||
return latinize(name
|
||||
.replace(/[ǫ]/ui, 'q')
|
||||
.replace(/[с]/ui, 'c')
|
||||
.replace(/[н]|\|-\|/ui, 'h')
|
||||
.replace(/[лпий]/ui, 'n'));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
import { Matrix2D } from './interfaces';
|
||||
|
||||
export function createMat2D(): Matrix2D {
|
||||
const out = new Float32Array(6);
|
||||
out[0] = 1;
|
||||
out[3] = 1;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function identityMat2D(out: Matrix2D) {
|
||||
out[0] = 1;
|
||||
out[1] = 0;
|
||||
out[2] = 0;
|
||||
out[3] = 1;
|
||||
out[4] = 0;
|
||||
out[5] = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function copyMat2D(out: Matrix2D, a: Matrix2D) {
|
||||
out.set(a);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function setMat2D(out: Matrix2D, a: number, b: number, c: number, d: number, tx: number, ty: number) {
|
||||
out[0] = a;
|
||||
out[1] = b;
|
||||
out[2] = c;
|
||||
out[3] = d;
|
||||
out[4] = tx;
|
||||
out[5] = ty;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function mulMat2D(out: Matrix2D, a: Matrix2D, b: Matrix2D) {
|
||||
const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5];
|
||||
const b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5];
|
||||
out[0] = a0 * b0 + a2 * b1;
|
||||
out[1] = a1 * b0 + a3 * b1;
|
||||
out[2] = a0 * b2 + a2 * b3;
|
||||
out[3] = a1 * b2 + a3 * b3;
|
||||
out[4] = a0 * b4 + a2 * b5 + a4;
|
||||
out[5] = a1 * b4 + a3 * b5 + a5;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function translateMat2D(out: Matrix2D, a: Matrix2D, x: number, y: number) {
|
||||
const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5];
|
||||
out[0] = a0;
|
||||
out[1] = a1;
|
||||
out[2] = a2;
|
||||
out[3] = a3;
|
||||
out[4] = a0 * x + a2 * y + a4;
|
||||
out[5] = a1 * x + a3 * y + a5;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function rotateMat2D(out: Matrix2D, a: Matrix2D, rad: number) {
|
||||
const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5];
|
||||
const s = Math.sin(rad);
|
||||
const c = Math.cos(rad);
|
||||
out[0] = a0 * c + a2 * s;
|
||||
out[1] = a1 * c + a3 * s;
|
||||
out[2] = a0 * -s + a2 * c;
|
||||
out[3] = a1 * -s + a3 * c;
|
||||
out[4] = a4;
|
||||
out[5] = a5;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function scaleMat2D(out: Matrix2D, a: Matrix2D, x: number, y: number) {
|
||||
const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5];
|
||||
out[0] = a0 * x;
|
||||
out[1] = a1 * x;
|
||||
out[2] = a2 * y;
|
||||
out[3] = a3 * y;
|
||||
out[4] = a4;
|
||||
out[5] = a5;
|
||||
return out;
|
||||
}
|
||||
|
||||
const temp = createMat2D();
|
||||
|
||||
export function skewX(out: Matrix2D, a: Matrix2D, angle: number): Matrix2D {
|
||||
setMat2D(temp, 1, 0, Math.tan(angle), 1, 0, 0);
|
||||
mulMat2D(out, a, temp);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function skewY(out: Matrix2D, a: Matrix2D, angle: number): Matrix2D {
|
||||
setMat2D(temp, 1, Math.tan(angle), 0, 1, 0, 0);
|
||||
mulMat2D(out, a, temp);
|
||||
return out;
|
||||
}
|
||||
|
||||
const tempMatrix = createMat2D();
|
||||
|
||||
export function skewTransform(base: Matrix2D | undefined, skew: number, ox: number, oy: number, x: number, y: number): Matrix2D {
|
||||
identityMat2D(tempMatrix);
|
||||
|
||||
if (skew) {
|
||||
translateMat2D(tempMatrix, tempMatrix, ox + x, oy + y);
|
||||
skewY(tempMatrix, tempMatrix, skew);
|
||||
translateMat2D(tempMatrix, tempMatrix, -ox, -oy);
|
||||
} else {
|
||||
translateMat2D(tempMatrix, tempMatrix, x, y);
|
||||
}
|
||||
|
||||
if (base !== undefined) {
|
||||
mulMat2D(tempMatrix, base, tempMatrix);
|
||||
}
|
||||
|
||||
return tempMatrix;
|
||||
}
|
||||
|
||||
export function isIdentity(m: Matrix2D) {
|
||||
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
|
||||
}
|
||||
|
||||
export function isTranslation(m: Matrix2D) {
|
||||
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Matrix4 } from './interfaces';
|
||||
|
||||
export function createMat4(): Matrix4 {
|
||||
const out = new Float32Array(16);
|
||||
out[0] = 1;
|
||||
out[5] = 1;
|
||||
out[10] = 1;
|
||||
out[15] = 1;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function ortho(out: Matrix4, left: number, right: number, bottom: number, top: number, near: number, far: number) {
|
||||
const lr = 1 / (left - right);
|
||||
const bt = 1 / (bottom - top);
|
||||
const nf = 1 / (near - far);
|
||||
out[0] = -2 * lr;
|
||||
out[1] = 0;
|
||||
out[2] = 0;
|
||||
out[3] = 0;
|
||||
out[4] = 0;
|
||||
out[5] = -2 * bt;
|
||||
out[6] = 0;
|
||||
out[7] = 0;
|
||||
out[8] = 0;
|
||||
out[9] = 0;
|
||||
out[10] = 2 * nf;
|
||||
out[11] = 0;
|
||||
out[12] = (left + right) * lr;
|
||||
out[13] = (top + bottom) * bt;
|
||||
out[14] = (far + near) * nf;
|
||||
out[15] = 1;
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,850 @@
|
||||
import { clamp } from 'lodash';
|
||||
import * as sprites from '../generated/sprites';
|
||||
import {
|
||||
EntityPart, Sprite, Rect, SpriteBatch, PaletteManager, Palette, PaletteRenderable, PaletteSpriteBatch,
|
||||
DrawOptions, getAnimationFromEntityState, EntityState, SignEntityOptions, EntityFlags, Collider, MixinEntity,
|
||||
Season,
|
||||
} from './interfaces';
|
||||
import { at, att, hasFlag, invalidEnum } from './utils';
|
||||
import { WHITE, BLACK, RED } from './colors';
|
||||
import { toScreenX, toWorldX, toWorldY, toScreenYWithZ } from './positionUtils';
|
||||
import { rect, addRects, addRect } from './rect';
|
||||
import { SECOND } from './constants';
|
||||
import { mockPaletteManager } from './ponyInfo';
|
||||
import { releasePalette } from '../graphics/paletteManager';
|
||||
|
||||
interface Renderable {
|
||||
color?: Sprite;
|
||||
shadow?: Sprite;
|
||||
}
|
||||
|
||||
export interface AnimatedRenderable {
|
||||
frames: Sprite[];
|
||||
shadow?: Sprite;
|
||||
palette: Uint32Array;
|
||||
}
|
||||
|
||||
export interface AnimatedRenderable1 {
|
||||
frames: (Sprite | undefined)[];
|
||||
}
|
||||
|
||||
const predefinedSteps = [
|
||||
[],
|
||||
[1],
|
||||
[3, 1],
|
||||
[3, 2, 1],
|
||||
[4, 2, 1, 1],
|
||||
[5, 3, 2, 1, 1],
|
||||
[9, 5, 3, 2, 1, 1],
|
||||
[14, 9, 5, 3, 2, 1, 1],
|
||||
];
|
||||
|
||||
let paletteManager: PaletteManager | undefined;
|
||||
|
||||
export function createPalette(palette: Uint32Array | undefined): Palette | undefined {
|
||||
return palette && paletteManager && paletteManager.addArray(palette);
|
||||
}
|
||||
|
||||
export function setPaletteManager(manager: PaletteManager | undefined) {
|
||||
paletteManager = manager;
|
||||
}
|
||||
|
||||
export function fakePaletteManager<T>(action: () => T): T {
|
||||
const tempPaletteManager = paletteManager;
|
||||
paletteManager = mockPaletteManager;
|
||||
const result = action();
|
||||
paletteManager = tempPaletteManager;
|
||||
return result;
|
||||
}
|
||||
|
||||
function getBounds(sprite: Sprite | undefined, ox: number, oy: number): Rect {
|
||||
return sprite ? rect(sprite.ox + ox, sprite.oy + oy, sprite.w, sprite.h) : rect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
export function getRenderableBounds({ color, shadow }: Renderable, dx: number, dy: number): Rect {
|
||||
if (color && shadow) {
|
||||
return addRects(getBounds(color, -dx, -dy), getBounds(shadow, -dx, -dy));
|
||||
} else if (color) {
|
||||
return getBounds(color, -dx, -dy);
|
||||
} else if (shadow) {
|
||||
return getBounds(shadow, -dx, -dy);
|
||||
} else {
|
||||
return rect(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function getBoundsForFrames(frames: (Sprite | undefined)[], dx: number, dy: number) {
|
||||
return frames.reduce((bounds, f) => f ? addRects(bounds, getBounds(f, dx, dy)) : bounds, rect(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
export function pickable(pickableX: number, pickableY: number): EntityPart {
|
||||
return { pickableX, pickableY };
|
||||
}
|
||||
|
||||
export function mixPickable(pickableX: number, pickableY: number): MixinEntity {
|
||||
return base => {
|
||||
base.pickableX = pickableX;
|
||||
base.pickableY = pickableY;
|
||||
};
|
||||
}
|
||||
|
||||
export function mixTrigger(tileX: number, tileY: number, tileW: number, tileH: number, tall: boolean): MixinEntity {
|
||||
const x = toWorldX(tileX);
|
||||
const y = toWorldY(tileY);
|
||||
const w = toWorldX(tileW);
|
||||
const h = toWorldY(tileH);
|
||||
const bounds = rect(x, y, w, h);
|
||||
|
||||
return base => {
|
||||
base.triggerBounds = bounds;
|
||||
base.triggerTall = tall;
|
||||
base.triggerOn = false;
|
||||
};
|
||||
}
|
||||
|
||||
export function collider(x: number, y: number, w: number, h: number, tall = true, exact = false): Collider {
|
||||
return { x, y, w, h, tall, exact };
|
||||
}
|
||||
|
||||
export const ponyColliders = roundedColliderList(-12, -4, 25, 7, 2);
|
||||
export const ponyCollidersBounds = getColliderBounds(ponyColliders);
|
||||
|
||||
function getColliderBounds(colliders: Collider[]) {
|
||||
const bounds = rect(0, 0, 0, 0);
|
||||
|
||||
for (const collider of colliders) {
|
||||
addRect(bounds, collider);
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
function roundedColliderList(x: number, y: number, w: number, h: number, stepsCount: number, tall = true) {
|
||||
const list: Collider[] = [];
|
||||
const steps = predefinedSteps[stepsCount];
|
||||
|
||||
if (DEVELOPMENT && !steps) {
|
||||
console.error('Invalid step count', steps);
|
||||
}
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
list.push(collider(x + steps[i], y + i, w - steps[i] * 2, 1, tall));
|
||||
}
|
||||
|
||||
list.push(collider(x, y + steps.length, w, h - steps.length * 2, tall));
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const ii = steps.length - (i + 1);
|
||||
list.push(collider(x + steps[ii], y + h - steps.length + i, w - steps[ii] * 2, 1, tall));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
export function mixColliderRect(x: number, y: number, w: number, h: number, tall = true, exact = false): MixinEntity {
|
||||
return mixColliders(collider(x, y, w, h, tall, exact));
|
||||
}
|
||||
|
||||
export function mixColliderRounded(x: number, y: number, w: number, h: number, stepsCount: number, tall = true): MixinEntity {
|
||||
return mixColliders(...roundedColliderList(x, y, w, h, stepsCount, tall));
|
||||
}
|
||||
|
||||
export function mixColliders(...list: Collider[]): MixinEntity {
|
||||
const bounds = getColliderBounds(list);
|
||||
|
||||
return base => {
|
||||
base.flags |= EntityFlags.CanCollideWith;
|
||||
base.colliders = list;
|
||||
base.collidersBounds = bounds;
|
||||
};
|
||||
}
|
||||
|
||||
export function taperColliderSE(x: number, y: number, w: number, h: number, tall?: boolean) {
|
||||
const colliders: Collider[] = [];
|
||||
|
||||
for (let iy = 0, ix = w - 2; iy < h; iy++ , ix -= ((iy % 3) ? 1 : 2)) {
|
||||
colliders.push(collider(x + ix, y + iy, w - ix, 1, tall));
|
||||
}
|
||||
|
||||
return colliders;
|
||||
}
|
||||
|
||||
export function taperColliderSW(x: number, y: number, w: number, h: number, tall?: boolean) {
|
||||
const colliders: Collider[] = [];
|
||||
|
||||
for (let iy = 0, ix = w - 2; iy < h; iy++ , ix -= ((iy % 3) ? 1 : 2)) {
|
||||
colliders.push(collider(x, y + iy, w - ix, 1, tall));
|
||||
}
|
||||
|
||||
return colliders;
|
||||
}
|
||||
|
||||
export function taperColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) {
|
||||
const colliders: Collider[] = [];
|
||||
|
||||
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
|
||||
colliders.push(collider(x, y + iy, w - ix, 1, tall));
|
||||
}
|
||||
|
||||
return colliders;
|
||||
}
|
||||
|
||||
export function taperColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) {
|
||||
const colliders: Collider[] = [];
|
||||
|
||||
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
|
||||
colliders.push(collider(x + ix, y + iy, w - ix, 1, tall));
|
||||
}
|
||||
|
||||
return colliders;
|
||||
}
|
||||
|
||||
export function skewColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) {
|
||||
const colliders: Collider[] = [];
|
||||
|
||||
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
|
||||
colliders.push(collider(x - ix, y + iy, w, 1, tall));
|
||||
}
|
||||
|
||||
return colliders;
|
||||
}
|
||||
|
||||
export function skewColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) {
|
||||
const colliders: Collider[] = [];
|
||||
|
||||
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
|
||||
colliders.push(collider(x + ix, y + iy, w, 1, tall));
|
||||
}
|
||||
|
||||
return colliders;
|
||||
}
|
||||
|
||||
export function triangleColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) {
|
||||
const colliders: Collider[] = [];
|
||||
|
||||
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
|
||||
colliders.push(collider(x - ix, y + iy, w + ix, 1, tall));
|
||||
}
|
||||
|
||||
return colliders;
|
||||
}
|
||||
|
||||
export function triangleColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) {
|
||||
const colliders: Collider[] = [];
|
||||
|
||||
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
|
||||
colliders.push(collider(x, y + iy, w + ix, 1, tall));
|
||||
}
|
||||
|
||||
return colliders;
|
||||
}
|
||||
|
||||
export function mixInteract(x: number, y: number, w: number, h: number, interactRange?: number): MixinEntity {
|
||||
const interactBounds = rect(x, y, w, h);
|
||||
|
||||
return base => {
|
||||
base.flags |= EntityFlags.Interactive;
|
||||
base.interactBounds = interactBounds;
|
||||
base.interactRange = interactRange;
|
||||
};
|
||||
}
|
||||
|
||||
export function mixInteractAt(interactRange?: number): MixinEntity {
|
||||
return base => {
|
||||
base.flags |= EntityFlags.Interactive;
|
||||
base.interactRange = interactRange;
|
||||
};
|
||||
}
|
||||
|
||||
export function mixMinimap(color: number, rect: Rect, order = 1): MixinEntity {
|
||||
const minimap = { color, rect, order };
|
||||
return base => base.minimap = minimap;
|
||||
}
|
||||
|
||||
export interface AnimatedMixinOptions {
|
||||
color?: number;
|
||||
repeat?: boolean;
|
||||
animations?: number[][];
|
||||
lightSprite?: AnimatedRenderable1;
|
||||
useGameTime?: boolean;
|
||||
flipped?: boolean;
|
||||
}
|
||||
|
||||
export function mixAnimation(
|
||||
anim: AnimatedRenderable, fps: number, dx: number, dy: number,
|
||||
{ color = WHITE, repeat = true, animations, lightSprite, useGameTime, flipped = false }: AnimatedMixinOptions = {}
|
||||
): MixinEntity {
|
||||
const bounds = getBoundsForFrames(anim.frames, -dx, -dy);
|
||||
const lightSpriteBounds = lightSprite ? getBoundsForFrames(lightSprite.frames, -dx, -dy) : rect(0, 0, 0, 0);
|
||||
|
||||
if (SERVER && !TESTS) {
|
||||
return base => base.bounds = bounds;
|
||||
}
|
||||
|
||||
return base => {
|
||||
const defaultPalette = anim.shadow && createPalette(sprites.defaultPalette);
|
||||
const palette = createPalette(anim.palette);
|
||||
|
||||
let time = repeat ? Math.random() * 5 : 0;
|
||||
let animation = 0;
|
||||
let lastFrame = 0;
|
||||
|
||||
const getFrame = (options: DrawOptions) => {
|
||||
let frameNumber = Math.floor(time * fps);
|
||||
|
||||
if (useGameTime) {
|
||||
frameNumber = Math.floor((options.gameTime / 1000) * fps);
|
||||
}
|
||||
|
||||
if (animations) {
|
||||
if (repeat) {
|
||||
frameNumber = frameNumber % animations[animation].length;
|
||||
}
|
||||
|
||||
return at(animations[animation], frameNumber) || 0;
|
||||
} else {
|
||||
return repeat ? (frameNumber % anim.frames.length) : Math.min(frameNumber, anim.frames.length - 1);
|
||||
}
|
||||
};
|
||||
|
||||
base.bounds = bounds;
|
||||
base.palettes = [];
|
||||
defaultPalette && base.palettes.push(defaultPalette);
|
||||
palette && base.palettes.push(palette);
|
||||
|
||||
base.update = function (delta: number) {
|
||||
time += delta;
|
||||
|
||||
const anim = getAnimationFromEntityState(this.state);
|
||||
|
||||
if (animations && anim !== animation) {
|
||||
animation = anim;
|
||||
time = 0;
|
||||
}
|
||||
|
||||
const frameNumber = Math.floor(time * fps);
|
||||
|
||||
if (lastFrame !== frameNumber) {
|
||||
lastFrame = frameNumber;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
|
||||
const frame = getFrame(options);
|
||||
const frameSprite = anim.frames[frame];
|
||||
const x = toScreenX(this.x);
|
||||
const y = toScreenYWithZ(this.y, this.z);
|
||||
|
||||
batch.save();
|
||||
batch.translate(x, y);
|
||||
|
||||
if (hasFlag(this.state, EntityState.FacingRight) || flipped) {
|
||||
batch.scale(-1, 1);
|
||||
}
|
||||
|
||||
batch.translate(-dx, -dy);
|
||||
anim.shadow && batch.drawSprite(anim.shadow, options.shadowColor, defaultPalette, 0, 0);
|
||||
frameSprite && batch.drawSprite(frameSprite, color, palette, 0, 0);
|
||||
batch.restore();
|
||||
};
|
||||
|
||||
if (lightSprite) {
|
||||
base.lightSpriteColor = WHITE;
|
||||
base.lightSpriteBounds = lightSpriteBounds;
|
||||
base.drawLightSprite = function (batch, options) {
|
||||
const frame = getFrame(options);
|
||||
const frameSprite = lightSprite.frames[frame];
|
||||
const x = toScreenX(this.x);
|
||||
const y = toScreenYWithZ(this.y, this.z);
|
||||
|
||||
batch.save();
|
||||
batch.translate(x, y);
|
||||
|
||||
if (hasFlag(this.state, EntityState.FacingRight) || flipped) {
|
||||
batch.scale(-1, 1);
|
||||
}
|
||||
|
||||
batch.translate(-dx, -dy);
|
||||
batch.drawSprite(frameSprite, this.lightSpriteColor!, 0, 0);
|
||||
batch.restore();
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function mixDrawWindow(
|
||||
sprite: PaletteRenderable, dx: number, dy: number, paletteIndex: number,
|
||||
padLeft: number, padTop: number, padRight: number, padBottom: number,
|
||||
): MixinEntity {
|
||||
const bounds = getRenderableBounds(sprite, dx, dy);
|
||||
|
||||
return base => {
|
||||
base.bounds = bounds;
|
||||
|
||||
if (!SERVER || TESTS) {
|
||||
const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette);
|
||||
const palette = createPalette(att(sprite.palettes, paletteIndex));
|
||||
base.palettes = [];
|
||||
defaultPalette && base.palettes.push(defaultPalette);
|
||||
palette && base.palettes.push(palette);
|
||||
|
||||
base.draw = function (batch, options) {
|
||||
const baseX = toScreenX(this.x + (this.ox || 0));
|
||||
const baseY = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0));
|
||||
const x = baseX - dx;
|
||||
const y = baseY - dy;
|
||||
|
||||
if (sprite.shadow !== undefined) {
|
||||
batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y);
|
||||
}
|
||||
|
||||
if (sprite.color !== undefined) {
|
||||
batch.drawRect(options.lightColor,
|
||||
x + padLeft, y + padTop, sprite.color.w - (padLeft + padRight), sprite.color.h - (padTop + padBottom));
|
||||
batch.drawSprite(sprite.color, WHITE, palette, x, y);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function mixDraw(sprite: PaletteRenderable, dx: number, dy: number, paletteIndex = 0): MixinEntity {
|
||||
const bounds = getRenderableBounds(sprite, dx, dy);
|
||||
|
||||
return base => {
|
||||
base.bounds = bounds;
|
||||
|
||||
if (!SERVER || TESTS) {
|
||||
const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette);
|
||||
const palette = createPalette(att(sprite.palettes, paletteIndex));
|
||||
base.palettes = [];
|
||||
defaultPalette && base.palettes.push(defaultPalette);
|
||||
palette && base.palettes.push(palette);
|
||||
|
||||
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
|
||||
const x = toScreenX(this.x + (this.ox || 0)) - dx;
|
||||
const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy;
|
||||
const opacity = 1 - 0.6 * (this.coverLifting || 0);
|
||||
|
||||
if (sprite.shadow !== undefined) {
|
||||
batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y);
|
||||
}
|
||||
|
||||
batch.globalAlpha = opacity;
|
||||
|
||||
if (sprite.color !== undefined) {
|
||||
batch.drawSprite(sprite.color, WHITE, palette, x, y);
|
||||
}
|
||||
|
||||
batch.globalAlpha = 1;
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface MixDraw {
|
||||
sprite: PaletteRenderable;
|
||||
dx: number;
|
||||
dy: number;
|
||||
palette: number;
|
||||
}
|
||||
|
||||
export interface MixDrawSeasonal {
|
||||
summer: MixDraw;
|
||||
autumn?: Partial<MixDraw>;
|
||||
winter?: Partial<MixDraw>;
|
||||
spring?: Partial<MixDraw>;
|
||||
}
|
||||
|
||||
function addBounds(bounds: Rect, setup: MixDraw) {
|
||||
addRect(bounds, getRenderableBounds(setup.sprite, setup.dx, setup.dy));
|
||||
}
|
||||
|
||||
export function mixDrawSeasonal(setup: MixDrawSeasonal): MixinEntity {
|
||||
const bounds = rect(0, 0, 0, 0);
|
||||
const summer = setup.summer;
|
||||
const autumn = { ...summer, ...setup.autumn };
|
||||
const winter = { ...summer, ...setup.winter };
|
||||
const spring = { ...summer, ...setup.spring };
|
||||
|
||||
addBounds(bounds, summer);
|
||||
addBounds(bounds, autumn);
|
||||
addBounds(bounds, winter);
|
||||
addBounds(bounds, spring);
|
||||
|
||||
return (base, _, worldState) => {
|
||||
base.bounds = bounds;
|
||||
|
||||
if (!SERVER || TESTS) {
|
||||
let season = Season.Summer;
|
||||
let { sprite, dx, dy, palette: paletteIndex } = setup.summer;
|
||||
let defaultPalette: Palette | undefined = undefined;
|
||||
let palette: Palette | undefined = undefined;
|
||||
|
||||
const setupSeason = (newSeason: Season) => {
|
||||
season = newSeason;
|
||||
let set: MixDraw;
|
||||
|
||||
switch (season) {
|
||||
case Season.Summer:
|
||||
set = summer;
|
||||
break;
|
||||
case Season.Autumn:
|
||||
set = autumn;
|
||||
break;
|
||||
case Season.Winter:
|
||||
set = winter;
|
||||
break;
|
||||
case Season.Spring:
|
||||
set = spring;
|
||||
break;
|
||||
default:
|
||||
invalidEnum(season);
|
||||
return;
|
||||
}
|
||||
|
||||
sprite = set.sprite;
|
||||
dx = set.dx;
|
||||
dy = set.dy;
|
||||
paletteIndex = set.palette;
|
||||
|
||||
if (base.palettes) {
|
||||
for (const palette of base.palettes) {
|
||||
releasePalette(palette);
|
||||
}
|
||||
}
|
||||
|
||||
defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette);
|
||||
palette = createPalette(att(sprite.palettes, paletteIndex));
|
||||
base.palettes = [];
|
||||
defaultPalette && base.palettes.push(defaultPalette);
|
||||
palette && base.palettes.push(palette);
|
||||
};
|
||||
|
||||
setupSeason(worldState.season);
|
||||
|
||||
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
|
||||
const x = toScreenX(this.x + (this.ox || 0)) - dx;
|
||||
const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy;
|
||||
const opacity = 1 - 0.6 * (this.coverLifting || 0);
|
||||
|
||||
if (sprite.shadow !== undefined) {
|
||||
batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y);
|
||||
}
|
||||
|
||||
batch.globalAlpha = opacity;
|
||||
|
||||
if (sprite.color !== undefined) {
|
||||
batch.drawSprite(sprite.color, WHITE, palette, x, y);
|
||||
}
|
||||
|
||||
batch.globalAlpha = 1;
|
||||
|
||||
if (season !== options.season) {
|
||||
setupSeason(options.season);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function splitSprite(sprite: Sprite, x: number, w: number, h: number) {
|
||||
const result: Sprite[] = [];
|
||||
|
||||
for (let y = 0; y < sprite.h; y += h) {
|
||||
result.push({ x: sprite.x + x, y: sprite.y + y, w, h, ox: sprite.ox, oy: sprite.oy, type: sprite.type });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const poles = [
|
||||
{ sprite: sprites.direction_pole_3, dy: -39 },
|
||||
{ sprite: sprites.direction_pole_4, dy: -50 },
|
||||
{ sprite: sprites.direction_pole_5, dy: -61 },
|
||||
];
|
||||
|
||||
const shadowLeft = sprites.direction_shadow_left.shadow;
|
||||
const shadowRight = sprites.direction_shadow_right.shadow;
|
||||
const leftSprites = splitSprite(sprites.direction_left_right.color, 0, 17, 10);
|
||||
const rightSprites = splitSprite(sprites.direction_left_right.color, 17, 17, 10);
|
||||
|
||||
const dirUpDown = [
|
||||
{
|
||||
shadowUp: sprites.direction_shadow_up_left.shadow,
|
||||
shadowDown: sprites.direction_shadow_down_right.shadow,
|
||||
spriteUp: sprites.direction_up_left.color,
|
||||
spriteDown: sprites.direction_down_right.color,
|
||||
shadowUpDX: -6, shadowUpDY: -9,
|
||||
shadowDownDX: -1, shadowDownDY: 3,
|
||||
upDX: -6, upDY: -7,
|
||||
downDX: -1, downDY: 5,
|
||||
},
|
||||
{
|
||||
shadowUp: sprites.direction_shadow_up_right.shadow,
|
||||
shadowDown: sprites.direction_shadow_down_left.shadow,
|
||||
spriteUp: sprites.direction_up_right.color,
|
||||
spriteDown: sprites.direction_down_left.color,
|
||||
shadowUpDX: 1, shadowUpDY: -9,
|
||||
shadowDownDX: -4, shadowDownDY: 3,
|
||||
upDX: 1, upDY: -7,
|
||||
downDX: -5, downDY: 4,
|
||||
},
|
||||
];
|
||||
|
||||
export function mixDrawDirectionSign(): MixinEntity {
|
||||
const poleDX = -4;
|
||||
const leftDX = -20;
|
||||
const rightDX = 3;
|
||||
const plateDY = 2;
|
||||
const leftRightStep = 11;
|
||||
const upDownStep = 11;
|
||||
|
||||
return (base, options = {}) => {
|
||||
const { sign: { r = 0, w = [], e = [], s = [], n = [] } = {} } = options as SignEntityOptions;
|
||||
const max = clamp(Math.max(w.length, e.length, s.length, n.length), 3, 5);
|
||||
const boundsH = 7 + max * 11;
|
||||
|
||||
base.bounds = rect(-20, -boundsH, 40, boundsH);
|
||||
base.options = options;
|
||||
|
||||
if (SERVER && !TESTS)
|
||||
return;
|
||||
|
||||
const {
|
||||
shadowUp, shadowDown, spriteUp, spriteDown, upDX, upDY, downDX, downDY,
|
||||
shadowUpDX, shadowUpDY, shadowDownDX, shadowDownDY,
|
||||
} = dirUpDown[r];
|
||||
const leftShadow = !!w.length;
|
||||
const rightShadow = !!e.length;
|
||||
const upShadow = !!n.length;
|
||||
const downShadow = !!s.length;
|
||||
const pole = poles[max - 3];
|
||||
|
||||
const defaultPalette = pole.sprite.shadow && createPalette(sprites.defaultPalette);
|
||||
const palette = createPalette(att(pole.sprite.palettes, 0));
|
||||
base.palettes = [];
|
||||
defaultPalette && base.palettes.push(defaultPalette);
|
||||
palette && base.palettes.push(palette);
|
||||
|
||||
base.draw = function (batch, options) {
|
||||
const x = toScreenX(this.x);
|
||||
const y = toScreenYWithZ(this.y, this.z);
|
||||
|
||||
batch.drawSprite(pole.sprite.shadow, options.shadowColor, defaultPalette, x + poleDX, y + pole.dy);
|
||||
leftShadow && batch.drawSprite(shadowLeft, options.shadowColor, defaultPalette, x - 18, y - 1);
|
||||
rightShadow && batch.drawSprite(shadowRight, options.shadowColor, defaultPalette, x + 4, y - 1);
|
||||
upShadow && batch.drawSprite(shadowUp, options.shadowColor, defaultPalette, x + shadowUpDX, y + shadowUpDY);
|
||||
downShadow && batch.drawSprite(shadowDown, options.shadowColor, defaultPalette, x + shadowDownDX, y + shadowDownDY);
|
||||
|
||||
for (let i = n.length - 1; i >= 0; i--) {
|
||||
if (n[i] !== -1) {
|
||||
batch.drawSprite(spriteUp, WHITE, palette, x + upDX, y + pole.dy + upDY + i * upDownStep);
|
||||
}
|
||||
}
|
||||
|
||||
batch.drawSprite(pole.sprite.color, WHITE, palette, x + poleDX, y + pole.dy);
|
||||
|
||||
for (let i = 0; i < w.length; i++) {
|
||||
if (w[i] !== -1) {
|
||||
const sprite = leftSprites[w[i]];
|
||||
sprite && batch.drawSprite(sprite, WHITE, palette, x + leftDX, y + pole.dy + plateDY + i * leftRightStep);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < e.length; i++) {
|
||||
if (e[i] !== -1) {
|
||||
const sprite = rightSprites[e[i]];
|
||||
sprite && batch.drawSprite(rightSprites[e[i]], WHITE, palette, x + rightDX, y + pole.dy + plateDY + i * leftRightStep);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = s.length - 1; i >= 0; i--) {
|
||||
if (s[i] !== -1) {
|
||||
batch.drawSprite(spriteDown, WHITE, palette, x + downDX, y + pole.dy + downDY + i * upDownStep);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function mixLight(color: number, dx: number, dy: number, w: number, h: number): MixinEntity {
|
||||
return base => {
|
||||
if (!SERVER || TESTS) {
|
||||
base.lightOn = true;
|
||||
base.lightColor = color;
|
||||
base.lightScale = 1;
|
||||
base.lightTarget = 1;
|
||||
base.lightScaleAdjust = 1;
|
||||
base.lightBounds = rect(-(dx + w / 2), -(dy + h / 2), w, h);
|
||||
base.drawLight = function (batch: SpriteBatch) {
|
||||
if (!this.lightOn)
|
||||
return;
|
||||
|
||||
const x = toScreenX(this.x);
|
||||
const y = toScreenYWithZ(this.y, this.z);
|
||||
const s = this.lightScale! * this.lightScaleAdjust!;
|
||||
const width = w * s;
|
||||
const height = h * s;
|
||||
const color = this.lightColor!;
|
||||
|
||||
batch.drawImage(color, -1, -1, 2, 2, x - (dx + width / 2), y - (dy + height / 2), width, height);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function mixLightSprite(sprite: Sprite, color: number, dx: number, dy: number): MixinEntity {
|
||||
return base => {
|
||||
if (!SERVER || TESTS) {
|
||||
base.lightSpriteOn = true;
|
||||
base.lightSpriteX = dx;
|
||||
base.lightSpriteY = dy;
|
||||
base.lightSpriteColor = color;
|
||||
base.lightSpriteBounds = getBounds(sprite, -dx, -dy);
|
||||
base.drawLightSprite = function (batch: SpriteBatch) {
|
||||
if (!this.lightSpriteOn)
|
||||
return;
|
||||
|
||||
const x = toScreenX(this.x) - this.lightSpriteX!;
|
||||
const y = toScreenYWithZ(this.y, this.z) - this.lightSpriteY!;
|
||||
|
||||
batch.drawSprite(sprite, this.lightSpriteColor || BLACK, x, y);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function mixDrawRain(): MixinEntity {
|
||||
const sprite = sprites.rainfall.color; // 110x477
|
||||
const bounds = rect(toScreenX(-4), -sprite.h, toScreenX(8), sprite.h);
|
||||
|
||||
return base => {
|
||||
base.bounds = bounds;
|
||||
|
||||
if (SERVER && !TESTS)
|
||||
return;
|
||||
|
||||
let time = 0;
|
||||
const palette = createPalette(sprites.defaultPalette);
|
||||
base.palettes = [palette];
|
||||
|
||||
// update(delta: number) {
|
||||
// time += delta * 1000;
|
||||
|
||||
// if (time > 200) {
|
||||
// time -= 200;
|
||||
// }
|
||||
// },
|
||||
|
||||
base.draw = function (batch: PaletteSpriteBatch) {
|
||||
const x = toScreenX(this.x) + bounds.x;
|
||||
const y = toScreenYWithZ(this.y, this.z) - sprite.h + Math.floor(time);
|
||||
batch.drawImage(sprite.type, RED, palette, sprite.x, sprite.y, sprite.w, sprite.h, x, y, sprite.w, sprite.h);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function mixDrawShadow(sprite: PaletteRenderable, dx: number, dy: number, shadowColor?: number): MixinEntity {
|
||||
const bounds = getRenderableBounds(sprite, dx, dy);
|
||||
|
||||
return base => {
|
||||
base.bounds = bounds;
|
||||
|
||||
if (!SERVER || TESTS) {
|
||||
const defaultPalette = createPalette(sprites.defaultPalette);
|
||||
base.palettes = [defaultPalette];
|
||||
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
|
||||
const x = toScreenX(this.x + (this.ox || 0)) - dx;
|
||||
const y = toScreenYWithZ(this.y + (this.oy || 0), this.z) - dy;
|
||||
const color = shadowColor === undefined ? options.shadowColor : shadowColor;
|
||||
sprite.shadow && batch.drawSprite(sprite.shadow, color, defaultPalette, x, y);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function mixBobbing(bobsFps: number, bobs: number[]): MixinEntity {
|
||||
return base => {
|
||||
base.flags |= EntityFlags.Bobbing;
|
||||
base.bobsFps = bobsFps;
|
||||
base.bobs = bobs;
|
||||
};
|
||||
}
|
||||
|
||||
let fullWalls = true;
|
||||
|
||||
export function toggleWalls() {
|
||||
fullWalls = !fullWalls;
|
||||
}
|
||||
|
||||
export function mixDrawWall(
|
||||
full: PaletteRenderable, half: PaletteRenderable, dx: number, dy: number, dy2: number
|
||||
): MixinEntity {
|
||||
const fullBounds = getRenderableBounds(full, dx, dy);
|
||||
// const halfBounds = getRenderableBounds(half, dx, dy2);
|
||||
|
||||
return base => {
|
||||
base.bounds = fullBounds; // fullWalls ? fullBounds : halfBounds
|
||||
|
||||
if (SERVER && !TESTS)
|
||||
return;
|
||||
|
||||
const fullPalette = createPalette(att(full.palettes, 0));
|
||||
const halfPalette = createPalette(att(half.palettes, 0));
|
||||
|
||||
base.palettes = [];
|
||||
fullPalette && base.palettes.push(fullPalette);
|
||||
halfPalette && base.palettes.push(halfPalette);
|
||||
|
||||
base.draw = function (batch: PaletteSpriteBatch) {
|
||||
const sprite = fullWalls ? full : half;
|
||||
const palette = fullWalls ? fullPalette : halfPalette;
|
||||
const x = toScreenX(this.x) - dx;
|
||||
const y = toScreenYWithZ(this.y, this.z) - (fullWalls ? dy : dy2);
|
||||
sprite.color && batch.drawSprite(sprite.color, WHITE, palette, x, y);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function mixDrawSpider(
|
||||
sprite: PaletteRenderable, dx: number, dy: number
|
||||
): MixinEntity {
|
||||
const heightOffset = 30;
|
||||
const spriteColor = sprite.color;
|
||||
const baseBounds = getRenderableBounds(sprite, dx, dy);
|
||||
|
||||
if (!spriteColor)
|
||||
throw new Error('Missing sprite');
|
||||
|
||||
return base => {
|
||||
const { height, time } = base.options as { height: number; time: number; };
|
||||
const bounds = { ...baseBounds };
|
||||
bounds.y -= (height + heightOffset);
|
||||
bounds.h += height;
|
||||
base.bounds = bounds;
|
||||
|
||||
if (SERVER && !TESTS)
|
||||
return;
|
||||
|
||||
const palette = createPalette(sprite.palettes && sprite.palettes[0]);
|
||||
base.palettes = [palette];
|
||||
|
||||
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
|
||||
const t = options.gameTime / SECOND - time;
|
||||
const h = clamp(Math.sin(t / 4) * 4, 0, 1) * height;
|
||||
|
||||
if (h < height) {
|
||||
const lineLength = height - h - 4;
|
||||
const x = toScreenX(this.x) - dx;
|
||||
const y = toScreenYWithZ(this.y, this.z) - dy - heightOffset - h;
|
||||
|
||||
batch.drawRect(0x181818ff, x + 2, y - lineLength, 1, lineLength + 1);
|
||||
batch.drawSprite(spriteColor, WHITE, palette, x, y);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { EntityState, Point, Rect, Entity } from './interfaces';
|
||||
import { PONY_SPEED_TROT, PONY_SPEED_WALK, tileWidth, tileHeight } from './constants';
|
||||
import { clamp, hasFlag } from './utils';
|
||||
import { toWorldX, toWorldY } from './positionUtils';
|
||||
import { rect } from './rect';
|
||||
|
||||
const DIRS = [
|
||||
[0, -1], // 0
|
||||
[0.5, -1],
|
||||
[1, -1],
|
||||
[1, -0.5],
|
||||
[1, 0], // 4
|
||||
[1, 0.5],
|
||||
[1, 1],
|
||||
[0.5, 1],
|
||||
[0, 1], // 8
|
||||
[-0.5, 1],
|
||||
[-1, 1],
|
||||
[-1, 0.5],
|
||||
[-1, 0], // 12
|
||||
[-1, -0.5],
|
||||
[-1, -1],
|
||||
[-0.5, -1],
|
||||
];
|
||||
|
||||
const SECA = 0xcd3003ca;
|
||||
const SECB = 0x5b903a62;
|
||||
const SECC = 0x1c267e56;
|
||||
const SECD = 0x1921ba6f;
|
||||
const SECE = 0x0000bc0e;
|
||||
const PI2 = Math.PI * 2;
|
||||
const DIRS_ANGLE = DIRS.length / PI2;
|
||||
|
||||
export function flagsToSpeed(flags: EntityState): number {
|
||||
const state = flags & EntityState.PonyStateMask;
|
||||
|
||||
if (state === EntityState.PonyTrotting) {
|
||||
return PONY_SPEED_TROT;
|
||||
} else if (state === EntityState.PonyWalking) {
|
||||
return PONY_SPEED_WALK;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function dirToVector(dir: number): Point {
|
||||
const [x, y] = DIRS[(dir | 0) % DIRS.length];
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
export function vectorToDir(x: number, y: number): number {
|
||||
const angle = Math.atan2(x, -y);
|
||||
return Math.round((angle < 0 ? angle + PI2 : angle) * DIRS_ANGLE) % DIRS.length;
|
||||
}
|
||||
|
||||
export interface Movement {
|
||||
x: number;
|
||||
y: number;
|
||||
dir: number;
|
||||
flags: EntityState;
|
||||
time: number;
|
||||
camera: Rect;
|
||||
}
|
||||
|
||||
export const POSITION_MIN = 0;
|
||||
export const POSITION_MAX = 100000;
|
||||
|
||||
export function encodeMovement(
|
||||
x: number, y: number, dir: number, flags: EntityState, time: number, camera: Rect
|
||||
): [number, number, number, number, number] {
|
||||
const pixelX = Math.floor(clamp(x, POSITION_MIN, POSITION_MAX) * tileWidth);
|
||||
const pixelY = Math.floor(clamp(y, POSITION_MIN, POSITION_MAX) * tileHeight);
|
||||
const camX = ((pixelX - camera.x) & 0xfff) >>> 0;
|
||||
const camY = ((pixelY - camera.y) & 0xfff) >>> 0;
|
||||
const camW = (camera.w & 0xfff) >>> 0;
|
||||
const camH = (camera.h & 0xfff) >>> 0;
|
||||
|
||||
const a = pixelX | ((dir & 0xff) << 24);
|
||||
const b = pixelY | ((flags & 0xff) << 24);
|
||||
const c = time;
|
||||
const d = (camX << 20) | (camY << 8) | (camW >>> 4);
|
||||
const e = ((camW & 0xf) << 12) | camH;
|
||||
|
||||
return [
|
||||
(a ^ SECA) >>> 0,
|
||||
(b ^ SECB) >>> 0,
|
||||
(c ^ SECC) >>> 0,
|
||||
(d ^ SECD) >>> 0,
|
||||
(e ^ SECE) >>> 0,
|
||||
];
|
||||
}
|
||||
|
||||
export function decodeMovement(a: number, b: number, c: number, d: number, e: number): Movement {
|
||||
a = (a >>> 0) ^ SECA;
|
||||
b = (b >>> 0) ^ SECB;
|
||||
c = (c >>> 0) ^ SECC;
|
||||
d = (d >>> 0) ^ SECD;
|
||||
e = (e >>> 0) ^ SECE;
|
||||
|
||||
const pixelX = a & 0xffffff;
|
||||
const pixelY = b & 0xffffff;
|
||||
|
||||
const x = toWorldX(pixelX + 0.5);
|
||||
const y = toWorldY(pixelY + 0.5);
|
||||
const dir = (a >>> 24) & 0xff;
|
||||
const flags = (b >>> 24) & 0xff;
|
||||
const time = c;
|
||||
|
||||
const camX = pixelX - ((d >>> 20) & 0xfff);
|
||||
const camY = pixelY - ((d >>> 8) & 0xfff);
|
||||
const camW = ((d & 0xff) << 4) | ((e >>> 12) & 0xf);
|
||||
const camH = e & 0xfff;
|
||||
|
||||
return { x, y, dir, flags, time, camera: rect(camX, camY, camW, camH) };
|
||||
}
|
||||
|
||||
export function isMovingRight(vx: number, right: boolean): boolean {
|
||||
return vx < 0 ? false : (vx > 0 ? true : right);
|
||||
}
|
||||
|
||||
export function shouldBeFacingRight(entity: Entity): boolean {
|
||||
return isMovingRight(entity.vx, hasFlag(entity.state, EntityState.FacingRight));
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
type Pt = [number, number];
|
||||
|
||||
const createPoint = ([x, y]: Pt): Point => ({ x, y });
|
||||
const createPoints = (pts: Pt[]) => pts.map(createPoint);
|
||||
|
||||
export const cmOffsets: Point[] = [];
|
||||
export const headOffsets: Point[] = [];
|
||||
export const tailOffsets: Point[] = [];
|
||||
export const wingOffsets: Point[] = [];
|
||||
export const frontLegOffsets: Point[] = [];
|
||||
export const backLegOffsets: Point[] = [];
|
||||
export const neckAccessoryOffsets: Point[] = [];
|
||||
export const backAccessoryOffsets: Point[] = [];
|
||||
export const waistAccessoryOffsets: Point[] = [];
|
||||
export const chestAccessoryOffsets: Point[] = [];
|
||||
|
||||
function offsets(
|
||||
_index: number, cm: Pt, head: Pt, tail: Pt, wing: Pt, frontLeg: Pt, backLeg: Pt,
|
||||
neckAccessory: Pt, backAccessory: Pt, waistAccessory: Pt, chestAccessory: Pt
|
||||
) {
|
||||
cmOffsets.push(createPoint(cm));
|
||||
headOffsets.push(createPoint(head));
|
||||
tailOffsets.push(createPoint(tail));
|
||||
wingOffsets.push(createPoint(wing));
|
||||
frontLegOffsets.push(createPoint(frontLeg));
|
||||
backLegOffsets.push(createPoint(backLeg));
|
||||
neckAccessoryOffsets.push(createPoint(neckAccessory));
|
||||
backAccessoryOffsets.push(createPoint(backAccessory));
|
||||
waistAccessoryOffsets.push(createPoint(waistAccessory));
|
||||
chestAccessoryOffsets.push(createPoint(chestAccessory));
|
||||
}
|
||||
|
||||
// stand: cm head tail wing frontLeg backLeg neck back waist chest
|
||||
offsets(0, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 0], [0, 0]);
|
||||
|
||||
// sit: cm head tail wing frontLeg backLeg neck back waist chest
|
||||
offsets(1, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 0], [0, 0]);
|
||||
offsets(2, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 1], [1, 1]);
|
||||
offsets(3, [1, 1], [0, 1], [1, 1], [2, 1], [2, 0], [2, 0], /***/[2, 1], [1, 1], [0, 1], [2, 2]);
|
||||
offsets(4, [4, 2], [2, 3], [4, 3], [5, 2], [5, 2], [5, 2], /***/[4, 3], [4, 3], [3, 3], [5, 4]);
|
||||
offsets(5, [7, 6], [4, 6], [7, 7], [7, 4], [7, 4], [7, 6], /***/[6, 5], [6, 6], [5, 5], [7, 6]);
|
||||
offsets(6, [8, 9], [7, 8], [9, 14], [8, 7], [9, 5], [8, 8], /***/[9, 8], [8, 7], [8, 7], [9, 8]);
|
||||
offsets(7, [8, 12], [8, 8], [9, 15], [9, 9], [9, 5], [8, 11], /***/[9, 7], [8, 8], [8, 7], [9, 7]);
|
||||
offsets(8, [8, 12], [9, 7], [9, 15], [9, 9], [9, 5], [8, 11], /***/[9, 7], [8, 8], [8, 7], [9, 7]);
|
||||
offsets(9, [8, 11], [9, 6], [9, 14], [9, 8], [9, 5], [8, 11], /***/[9, 6], [8, 8], [8, 7], [9, 6]);
|
||||
|
||||
// lie: cm head tail wing frontLeg backLeg neck back waist chest
|
||||
offsets(10, [8, 11], [8, 6], [9, 14], [9, 9], [9, 6], [8, 11], /***/[9, 6], [8, 8], [8, 7], [9, 6]);
|
||||
offsets(11, [8, 11], [7, 7], [9, 14], [8, 9], [8, 6], [8, 11], /***/[8, 7], [8, 8], [7, 7], [8, 6]);
|
||||
offsets(12, [8, 11], [6, 9], [9, 14], [7, 10], [7, 7], [8, 11], /***/[7, 9], [8, 8], [6, 8], [7, 7]);
|
||||
offsets(13, [8, 11], [6, 11], [9, 14], [7, 10], [6, 9], [8, 11], /***/[6, 11], [8, 8], [5, 10], [6, 9]);
|
||||
offsets(14, [8, 11], [7, 12], [9, 14], [7, 11], [6, 9], [8, 11], /***/[7, 12], [8, 8], [6, 10], [7, 10]);
|
||||
offsets(15, [8, 11], [7, 11], [9, 14], [7, 11], [6, 9], [8, 11], /***/[7, 11], [8, 8], [6, 10], [7, 9]);
|
||||
offsets(16, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 0], [0, 0]);
|
||||
|
||||
export const EAR_ACCESSORY_OFFSETS = createPoints([
|
||||
[0, 0], // 0
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0], // 5
|
||||
]);
|
||||
|
||||
export const EXTRA_ACCESSORY_OFFSETS = createPoints([
|
||||
[0, 9], // 0
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
[0, 2], // 5
|
||||
[0, 1],
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
[0, 3],
|
||||
[0, 2], // 10
|
||||
[0, 2],
|
||||
[0, 3],
|
||||
[0, 1],
|
||||
[0, 1],
|
||||
[0, 1], // 15
|
||||
[0, 9],
|
||||
[0, 3],
|
||||
[0, 3],
|
||||
[0, 3],
|
||||
[0, 3], // 20
|
||||
[0, 3],
|
||||
[0, 2],
|
||||
[0, 3],
|
||||
[0, 3],
|
||||
[0, 3], // 25
|
||||
[0, 2],
|
||||
[0, 3],
|
||||
[-1, 3],
|
||||
[0, 3],
|
||||
[0, 3], // 30
|
||||
[0, 3],
|
||||
]);
|
||||
|
||||
export const HEAD_ACCESSORY_OFFSETS = createPoints([
|
||||
[0, 0], // 0
|
||||
[0, -5],
|
||||
[0, -5],
|
||||
[0, -4],
|
||||
[0, -4],
|
||||
[0, -4], // 5
|
||||
[0, -4],
|
||||
[1, -4],
|
||||
[0, -4],
|
||||
[0, -3],
|
||||
[0, -4], // 10
|
||||
[0, -4],
|
||||
[0, -3],
|
||||
[1, -5],
|
||||
[0, -4],
|
||||
[0, -4], // 15
|
||||
[0, 0],
|
||||
[0, -4],
|
||||
[0, -4],
|
||||
[0, -4],
|
||||
[0, -4], // 20
|
||||
[0, -4],
|
||||
[0, -5],
|
||||
[0, -5],
|
||||
[0, -4],
|
||||
[1, -3], // 25
|
||||
[0, -4],
|
||||
[0, -4],
|
||||
[0, -4],
|
||||
[0, -4],
|
||||
[0, -4], // 30
|
||||
[0, -3],
|
||||
]);
|
||||
@@ -0,0 +1,678 @@
|
||||
import { PONY_WIDTH, PONY_HEIGHT, BLINK_FRAMES, canFly } from '../client/ponyUtils';
|
||||
import { stand, sneeze, defaultHeadAnimation, defaultBodyFrame, defaultHeadFrame } from '../client/ponyAnimations';
|
||||
import {
|
||||
PaletteSpriteBatch, Pony, BodyAnimation, EntityState, SpriteBatch, ExpressionExtra, HeadAnimation, Palette,
|
||||
PaletteManager, DrawOptions, Rect, EntityFlags, IMap, Entity, DoAction, Muzzle, Expression, isEyeSleeping,
|
||||
Iris, EntityPlayerState,
|
||||
} from './interfaces';
|
||||
import { hasFlag, setFlag } from './utils';
|
||||
import { blinkFps, PONY_TYPE } from './constants';
|
||||
import { releasePalettes } from './ponyInfo';
|
||||
import { createAnEntity, boopSplashRight, boopSplashLeft } from './entities';
|
||||
import {
|
||||
createAnimationPlayer, isAnimationPlaying, drawAnimation, playAnimation, updateAnimation, playOneOfAnimations
|
||||
} from './animationPlayer';
|
||||
import { blushColor, WHITE, MAGIC_ALPHA, HEARTS_COLOR } from './colors';
|
||||
import { encodeExpression, decodeExpression } from './encoders/expressionEncoder';
|
||||
import { toScreenX, toWorldX, toWorldY, toScreenYWithZ } from './positionUtils';
|
||||
import { getPonyAnimationFrame, getHeadY, drawPony, getPonyHeadPosition, createHeadTransform } from '../client/ponyDraw';
|
||||
import {
|
||||
isPonySitting, isPonyFlying, isPonyLying, isPonyStanding, isPonyLandedOrCanLand, isIdle, isIdleAnimation,
|
||||
isFacingRight, releaseEntity
|
||||
} from './entityUtils';
|
||||
import {
|
||||
getAnimation, getAnimationFrame, setAnimatorState, updateAnimator, createAnimator, AnimatorState,
|
||||
resetAnimatorState
|
||||
} from './animator';
|
||||
import {
|
||||
trotting, flying, hovering, toBoopState, isFlyingUpOrDown, isFlyingDown, isSittingDown, isSittingUp, swinging,
|
||||
standing, sitting, lying, swimming, isSwimmingState, swimmingToFlying,
|
||||
} from '../client/ponyStates';
|
||||
import { decodePonyInfo } from './compressPony';
|
||||
import { defaultPonyState, defaultDrawPonyOptions, isStateEqual } from '../client/ponyHelpers';
|
||||
import {
|
||||
sneezeAnimation, holdPoofAnimation, heartsAnimation, tearsAnimation, cryAnimation, zzzAnimations, magicAnimation
|
||||
} from '../client/spriteAnimations';
|
||||
import { rect } from './rect';
|
||||
import { addOrRemoveFromEntityList } from './worldMap';
|
||||
import { hasDrawLight, hasLightSprite } from '../client/draw';
|
||||
import { ponyColliders, ponyCollidersBounds } from './mixins';
|
||||
import { PonyTownGame } from '../client/game';
|
||||
import { playEffect } from '../client/handlers';
|
||||
import * as sprites from '../generated/sprites';
|
||||
import { withAlpha } from './color';
|
||||
|
||||
const flyY = 15;
|
||||
const lightExtentX = 100;
|
||||
const lightExtentY = 70;
|
||||
const emptyBounds = rect(0, 0, 0, 0);
|
||||
const bounds = rect(-PONY_WIDTH / 2, -PONY_HEIGHT, PONY_WIDTH, PONY_HEIGHT + 5);
|
||||
const boundsFly = rect(bounds.x, bounds.y - flyY, bounds.w, bounds.h + flyY);
|
||||
const lightBounds = makeLightBounds(bounds);
|
||||
const lightBoundsFly = makeLightBounds(boundsFly);
|
||||
const interactBounds = rect(-20, -50, 40, 50);
|
||||
const interactBoundsFly = rect(interactBounds.x, interactBounds.y - flyY, interactBounds.w, interactBounds.h);
|
||||
const defaultExpr = encodeExpression(undefined);
|
||||
|
||||
export function createPony(
|
||||
id: number, state: EntityState, info: string | Uint8Array | undefined, defaultPalette: Palette,
|
||||
paletteManager: PaletteManager
|
||||
): Pony {
|
||||
const pony: Pony = {
|
||||
id,
|
||||
state,
|
||||
playerState: EntityPlayerState.None,
|
||||
type: PONY_TYPE,
|
||||
flags: EntityFlags.Movable | EntityFlags.CanCollide | EntityFlags.Interactive,
|
||||
expr: defaultExpr,
|
||||
ponyState: defaultPonyState(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
info,
|
||||
order: 0,
|
||||
timestamp: 0,
|
||||
colliders: ponyColliders,
|
||||
collidersBounds: ponyCollidersBounds,
|
||||
selected: false,
|
||||
extra: false,
|
||||
toy: 0,
|
||||
swimming: false,
|
||||
ex: false, // extended data indicator, sent in extended option
|
||||
inTheAirDelay: 0,
|
||||
name: undefined,
|
||||
tag: undefined,
|
||||
site: undefined,
|
||||
modInfo: undefined,
|
||||
hold: 0,
|
||||
palettePonyInfo: undefined,
|
||||
headAnimation: undefined,
|
||||
batch: undefined,
|
||||
discardBatch: false,
|
||||
headTime: Math.random() * 5,
|
||||
blinkTime: 0,
|
||||
nextBlink: Math.random() * 5,
|
||||
currentExpression: defaultExpr,
|
||||
drawingOptions: { ...defaultDrawPonyOptions(), shadow: true, bounce: BETA },
|
||||
zzzEffect: createAnimationPlayer(defaultPalette),
|
||||
cryEffect: createAnimationPlayer(defaultPalette),
|
||||
sneezeEffect: createAnimationPlayer(defaultPalette),
|
||||
holdPoofEffect: createAnimationPlayer(defaultPalette),
|
||||
heartsEffect: createAnimationPlayer(defaultPalette),
|
||||
magicEffect: createAnimationPlayer(paletteManager.addArray(sprites.magic2.palette)),
|
||||
animator: createAnimator<BodyAnimation>(),
|
||||
lastX: 0,
|
||||
lastY: 0,
|
||||
lastRight: false,
|
||||
lastState: defaultPonyState(),
|
||||
initialized: false,
|
||||
doAction: DoAction.None,
|
||||
bounds: bounds,
|
||||
interactBounds: interactBounds,
|
||||
chatBounds: interactBounds,
|
||||
lightBounds: emptyBounds,
|
||||
lightSpriteBounds: emptyBounds,
|
||||
paletteManager,
|
||||
lastBoopSplash: 0,
|
||||
magicColor: 0,
|
||||
};
|
||||
|
||||
pony.ponyState.drawFaceExtra = batch => drawFaceExtra(batch, pony);
|
||||
|
||||
return pony;
|
||||
}
|
||||
|
||||
export function isPony(entity: Entity): entity is Pony {
|
||||
return entity.type === PONY_TYPE;
|
||||
}
|
||||
|
||||
export function isPonyOnTheGround(pony: Pony) {
|
||||
return !isPonyFlying(pony) && !isFlyingUpOrDown(pony.animator.state);
|
||||
}
|
||||
|
||||
export function getPaletteInfo(pony: Pony) {
|
||||
return ensurePonyInfoDecoded(pony);
|
||||
}
|
||||
|
||||
export function releasePony(pony: Pony) {
|
||||
if (pony.ponyState.holding) {
|
||||
releaseEntity(pony.ponyState.holding);
|
||||
}
|
||||
|
||||
releasePalettePonyInfo(pony);
|
||||
}
|
||||
|
||||
export function canPonyFly(pony: Pony) {
|
||||
return !!pony.palettePonyInfo && canFly(pony.palettePonyInfo);
|
||||
}
|
||||
|
||||
export function canPonyLie<T>(pony: Pony, map: IMap<T>) {
|
||||
return !isPonyLying(pony) && (isIdle(pony) || isSittingDown(pony.animator.state) || isFlyingDown(pony.animator.state)) &&
|
||||
isPonyLandedOrCanLand(pony, map);
|
||||
}
|
||||
export function canPonySit<T>(pony: Pony, map: IMap<T>) {
|
||||
return !isPonySitting(pony) && (isIdle(pony) || isFlyingDown(pony.animator.state)) &&
|
||||
isPonyLandedOrCanLand(pony, map);
|
||||
}
|
||||
export function canPonyStand<T>(pony: Pony, map: IMap<T>) {
|
||||
return !isPonyStanding(pony) && (isIdleAnimation(pony.ponyState.animation) || isSittingUp(pony.animator.state)) &&
|
||||
isPonyLandedOrCanLand(pony, map);
|
||||
}
|
||||
export function canPonyFlyUp(pony: Pony) {
|
||||
return !isPonyFlying(pony) && canPonyFly(pony) && !isFlyingUpOrDown(pony.animator.state);
|
||||
}
|
||||
|
||||
export function getPonyChatHeight(pony: Pony) {
|
||||
const baseHeight = 2;
|
||||
const state = pony.ponyState;
|
||||
|
||||
if (pony.animator.state === trotting) {
|
||||
return baseHeight;
|
||||
} else if (pony.animator.state === flying || pony.animator.state === hovering) {
|
||||
return baseHeight - 16;
|
||||
} else {
|
||||
const frame = getPonyAnimationFrame(state.animation, state.animationFrame, defaultBodyFrame);
|
||||
const animation = state.headAnimation || defaultHeadAnimation;
|
||||
const headFrame = getPonyAnimationFrame(animation, state.headAnimationFrame, defaultHeadFrame);
|
||||
return baseHeight + getHeadY(frame, headFrame);
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePonyInfo(pony: Pony, info: string | Uint8Array, apply: () => void) {
|
||||
pony.info = info;
|
||||
|
||||
if (pony.palettePonyInfo !== undefined) {
|
||||
releasePalettePonyInfo(pony);
|
||||
ensurePonyInfoDecoded(pony);
|
||||
pony.discardBatch = true;
|
||||
|
||||
if (isPonyFlying(pony) && !canPonyFly(pony)) {
|
||||
DEVELOPMENT && console.warn('Force land');
|
||||
pony.state = setFlag(pony.state, EntityState.PonyFlying, false);
|
||||
resetAnimatorState(pony.animator);
|
||||
}
|
||||
|
||||
apply();
|
||||
}
|
||||
}
|
||||
|
||||
export function ensurePonyInfoDecoded(pony: Pony) {
|
||||
if (pony.info !== undefined && pony.palettePonyInfo === undefined) {
|
||||
pony.palettePonyInfo = decodePonyInfo(pony.info, pony.paletteManager);
|
||||
const wingType = pony.palettePonyInfo.wings && pony.palettePonyInfo.wings.type || 0;
|
||||
pony.animator.variant = wingType === 4 ? 'bug' : '';
|
||||
pony.ponyState.blushColor = blushColor(pony.palettePonyInfo.coatPalette.colors[1]);
|
||||
pony.magicColor = withAlpha(pony.palettePonyInfo.magicColorValue, MAGIC_ALPHA);
|
||||
}
|
||||
|
||||
return pony.palettePonyInfo!;
|
||||
}
|
||||
|
||||
export function invalidatePalettesForPony(pony: Pony) {
|
||||
pony.discardBatch = true;
|
||||
}
|
||||
|
||||
export function doBoopPonyAction(game: PonyTownGame, pony: Pony) {
|
||||
doPonyAction(pony, DoAction.Boop);
|
||||
|
||||
if (pony.swimming && pony.lastBoopSplash < performance.now()) {
|
||||
if (isFacingRight(pony)) {
|
||||
playEffect(game, pony, boopSplashRight.type);
|
||||
} else {
|
||||
playEffect(game, pony, boopSplashLeft.type);
|
||||
}
|
||||
|
||||
pony.lastBoopSplash = performance.now() + 800;
|
||||
}
|
||||
}
|
||||
|
||||
export function doPonyAction(pony: Pony, action: DoAction) {
|
||||
pony.doAction = action;
|
||||
}
|
||||
|
||||
export function setPonyExpression(pony: Pony, expr: number) {
|
||||
pony.expr = expr;
|
||||
}
|
||||
|
||||
export function hasExtendedInfo(pony: Pony) {
|
||||
return pony.ex;
|
||||
}
|
||||
|
||||
export function hasHeadAnimation(pony: Pony) {
|
||||
return pony.headAnimation !== undefined;
|
||||
}
|
||||
|
||||
export function setHeadAnimation(pony: Pony, headAnimation: HeadAnimation | undefined) {
|
||||
if (pony.headAnimation !== headAnimation) {
|
||||
pony.headTime = 0;
|
||||
pony.headAnimation = headAnimation;
|
||||
}
|
||||
}
|
||||
|
||||
export function drawPonyEntity(batch: PaletteSpriteBatch, pony: Pony, drawOptions: DrawOptions) {
|
||||
if (pony.discardBatch && pony.batch !== undefined) {
|
||||
batch.releaseBatch(pony.batch);
|
||||
pony.batch = undefined;
|
||||
pony.discardBatch = false;
|
||||
}
|
||||
|
||||
if (pony.batch !== undefined) {
|
||||
batch.drawBatch(pony.batch);
|
||||
} else if (pony.palettePonyInfo !== undefined) {
|
||||
let swimming = false;
|
||||
|
||||
if (isSwimmingState(pony.animator.state)) {
|
||||
if (pony.animator.state === swimmingToFlying) {
|
||||
swimming = pony.animator.time < 0.4;
|
||||
} else {
|
||||
swimming = true;
|
||||
}
|
||||
}
|
||||
|
||||
const createBatch = pony.vx === 0 && pony.vy === 0 && !swimming;
|
||||
const right = isFacingRight(pony);
|
||||
|
||||
if (createBatch) {
|
||||
batch.startBatch();
|
||||
}
|
||||
|
||||
batch.save();
|
||||
transformBatch(batch, pony);
|
||||
|
||||
const options = pony.drawingOptions;
|
||||
options.flipped = right;
|
||||
options.selected = pony.selected === true;
|
||||
options.extra = pony.extra;
|
||||
options.toy = pony.toy;
|
||||
options.swimming = swimming;
|
||||
options.shadow = !pony.swimming;
|
||||
options.gameTime = drawOptions.gameTime + pony.id * 0.1;
|
||||
|
||||
options.shadowColor = drawOptions.shadowColor;
|
||||
|
||||
const ponyState = pony.ponyState;
|
||||
drawPony(batch, pony.palettePonyInfo, ponyState, 0, 0, options);
|
||||
|
||||
if (
|
||||
isAnimationPlaying(pony.zzzEffect) || isAnimationPlaying(pony.sneezeEffect) ||
|
||||
isAnimationPlaying(pony.holdPoofEffect) || isAnimationPlaying(pony.heartsEffect) ||
|
||||
isAnimationPlaying(pony.magicEffect)
|
||||
) {
|
||||
const { x, y } = getPonyHeadPosition(pony.ponyState, 0, 0);
|
||||
const right = isFacingRight(pony);
|
||||
const flip = right ? !ponyState.headTurned : ponyState.headTurned;
|
||||
batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState));
|
||||
drawAnimation(batch, pony.zzzEffect, 0, 0, WHITE, flip);
|
||||
drawAnimation(batch, pony.sneezeEffect, 0, 0, WHITE, flip);
|
||||
drawAnimation(batch, pony.holdPoofEffect, 0, 0, WHITE, flip);
|
||||
drawAnimation(batch, pony.heartsEffect, 0, 0, HEARTS_COLOR, flip);
|
||||
|
||||
if (pony.magicEffect.currentAnimation !== undefined) {
|
||||
drawAnimation(batch, pony.magicEffect, 0, 0, pony.magicColor, flip);
|
||||
const sprite = sprites.magic3.frames[pony.magicEffect.frame];
|
||||
sprite && batch.drawSprite(sprite, WHITE, pony.heartsEffect.palette, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
batch.restore();
|
||||
|
||||
if (createBatch) {
|
||||
pony.batch = batch.finishBatch();
|
||||
pony.lastX = toScreenX(pony.x);
|
||||
pony.lastY = toScreenYWithZ(pony.y, pony.z);
|
||||
pony.lastRight = right;
|
||||
pony.zzzEffect.dirty = false;
|
||||
pony.cryEffect.dirty = false;
|
||||
pony.sneezeEffect.dirty = false;
|
||||
pony.holdPoofEffect.dirty = false;
|
||||
pony.heartsEffect.dirty = false;
|
||||
pony.magicEffect.dirty = false;
|
||||
Object.assign(pony.lastState, ponyState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const magickLightSizes = [
|
||||
0, 1.02, // fade-in
|
||||
0.97, 0.94, 0.91, 0.94, 0.97, 1.00, // loop
|
||||
0.97, 0.94, 0.91, // fade-out
|
||||
];
|
||||
|
||||
export function drawPonyEntityLight(batch: SpriteBatch, pony: Pony, options: DrawOptions) {
|
||||
const ponyState = pony.ponyState;
|
||||
const holding = ponyState.holding;
|
||||
const drawHolding = holding !== undefined && holding.drawLight !== undefined;
|
||||
const drawMagic = pony.magicEffect.currentAnimation !== undefined;
|
||||
const draw = drawHolding || drawMagic;
|
||||
|
||||
if (draw) {
|
||||
batch.save();
|
||||
transformBatch(batch, pony);
|
||||
const { x, y } = getPonyHeadPosition(ponyState, 0, 0);
|
||||
batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState));
|
||||
|
||||
if (drawHolding) {
|
||||
holding!.x = toWorldX(holding!.pickableX!);
|
||||
holding!.y = toWorldY(holding!.pickableY!);
|
||||
holding!.drawLight!(batch, options);
|
||||
}
|
||||
|
||||
if (drawMagic) {
|
||||
const size = 200 * (magickLightSizes[pony.magicEffect.frame] || 0);
|
||||
batch.drawImage(WHITE, -1, -1, 2, 2, 30 - size / 2, 27 - size / 2, size, size);
|
||||
}
|
||||
|
||||
batch.restore();
|
||||
}
|
||||
}
|
||||
|
||||
export function drawPonyEntityLightSprite(batch: SpriteBatch, pony: Pony, options: DrawOptions) {
|
||||
const ponyState = pony.ponyState;
|
||||
const holding = ponyState.holding;
|
||||
const drawHolding = holding !== undefined && holding.drawLightSprite !== undefined;
|
||||
// const drawMagic = pony.magicEffect.currentAnimation !== undefined;
|
||||
const draw = drawHolding; // || drawMagic;
|
||||
|
||||
if (draw) {
|
||||
batch.save();
|
||||
transformBatch(batch, pony);
|
||||
const { x, y } = getPonyHeadPosition(ponyState, 0, 0);
|
||||
batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState));
|
||||
|
||||
if (drawHolding) {
|
||||
holding!.x = toWorldX(holding!.pickableX!);
|
||||
holding!.y = toWorldY(holding!.pickableY!);
|
||||
holding!.drawLightSprite!(batch, options);
|
||||
}
|
||||
|
||||
// if (drawMagic) {
|
||||
// const frame = pony.magicEffect.frame;
|
||||
// const sprite = sprites.magic2_light.frames[frame];
|
||||
// batch.drawSprite(sprite, WHITE, 0, 0);
|
||||
// }
|
||||
|
||||
batch.restore();
|
||||
}
|
||||
}
|
||||
|
||||
export function flagsToState(state: EntityState, moving: boolean, isSwimming: boolean): AnimatorState<BodyAnimation> {
|
||||
const ponyState = state & EntityState.PonyStateMask;
|
||||
|
||||
if (isSwimming) {
|
||||
return swimming;
|
||||
} else if (moving) {
|
||||
if (ponyState === EntityState.PonyFlying) {
|
||||
return flying;
|
||||
} else {
|
||||
return trotting;
|
||||
}
|
||||
} else {
|
||||
switch (ponyState) {
|
||||
case EntityState.PonyStanding: return standing;
|
||||
case EntityState.PonyWalking: return trotting;
|
||||
case EntityState.PonyTrotting: return trotting;
|
||||
case EntityState.PonySitting: return sitting;
|
||||
case EntityState.PonyLying: return lying;
|
||||
case EntityState.PonyFlying: return hovering;
|
||||
default:
|
||||
throw new Error(`Invalid pony state (${ponyState})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePonyEntity(pony: Pony, delta: number, gameTime: number, safe: boolean) {
|
||||
// update state
|
||||
const state = pony.ponyState;
|
||||
const walking = pony.vx !== 0 || pony.vy !== 0;
|
||||
const animationState = flagsToState(pony.state, walking, pony.swimming);
|
||||
|
||||
if (pony.inTheAirDelay > 0) {
|
||||
pony.inTheAirDelay -= delta;
|
||||
}
|
||||
|
||||
if (pony.doAction !== DoAction.None) {
|
||||
switch (pony.doAction) {
|
||||
case DoAction.Boop:
|
||||
setAnimatorState(pony.animator, toBoopState(animationState) || animationState);
|
||||
break;
|
||||
case DoAction.Swing:
|
||||
setAnimatorState(pony.animator, swinging);
|
||||
break;
|
||||
case DoAction.HoldPoof:
|
||||
playAnimation(pony.holdPoofEffect, holdPoofAnimation);
|
||||
break;
|
||||
default:
|
||||
if (DEVELOPMENT) {
|
||||
console.error(`Invalid DoAction: ${pony.doAction}`);
|
||||
}
|
||||
}
|
||||
|
||||
pony.doAction = DoAction.None;
|
||||
} else {
|
||||
setAnimatorState(pony.animator, animationState);
|
||||
}
|
||||
|
||||
// head
|
||||
pony.headTime += delta;
|
||||
|
||||
if (pony.headAnimation !== undefined) {
|
||||
const frame = Math.floor(pony.headTime * pony.headAnimation.fps);
|
||||
|
||||
if (frame >= pony.headAnimation.frames.length && !pony.headAnimation.loop) {
|
||||
pony.headAnimation = undefined;
|
||||
state.headAnimationFrame = 0;
|
||||
} else {
|
||||
state.headAnimationFrame = frame % pony.headAnimation.frames.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.headAnimation !== pony.headAnimation) {
|
||||
state.headAnimation = pony.headAnimation;
|
||||
|
||||
if (pony.headAnimation === sneeze) {
|
||||
playAnimation(pony.sneezeEffect, sneezeAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
// effects / expressions
|
||||
if (pony.currentExpression !== pony.expr) {
|
||||
updatePonyExpression(pony, pony.expr, safe);
|
||||
}
|
||||
|
||||
if ((pony.state & EntityState.Magic) !== 0) {
|
||||
playAnimation(pony.magicEffect, magicAnimation);
|
||||
} else {
|
||||
playAnimation(pony.magicEffect, undefined);
|
||||
}
|
||||
|
||||
updateAnimation(pony.zzzEffect, delta);
|
||||
updateAnimation(pony.cryEffect, delta);
|
||||
updateAnimation(pony.sneezeEffect, delta);
|
||||
updateAnimation(pony.holdPoofEffect, delta);
|
||||
updateAnimation(pony.heartsEffect, delta);
|
||||
updateAnimation(pony.magicEffect, delta);
|
||||
|
||||
// holding
|
||||
const holdingUpdated =
|
||||
state.holding !== undefined &&
|
||||
state.holding.update !== undefined &&
|
||||
state.holding.update(delta, gameTime);
|
||||
|
||||
// blink
|
||||
pony.blinkTime += delta;
|
||||
|
||||
if ((pony.blinkTime - pony.nextBlink) > 1) {
|
||||
pony.nextBlink = pony.blinkTime + Math.random() * 2 + 3;
|
||||
}
|
||||
|
||||
// update animator
|
||||
updateAnimator(pony.animator, delta);
|
||||
|
||||
// update state
|
||||
const blinkFrame = Math.floor((pony.blinkTime - pony.nextBlink) * blinkFps);
|
||||
state.blinkFrame = blinkFrame < BLINK_FRAMES.length ? BLINK_FRAMES[blinkFrame] : 1;
|
||||
state.headTurned = (pony.state & EntityState.HeadTurned) !== 0;
|
||||
state.animation = getAnimation(pony.animator) || stand;
|
||||
state.animationFrame = getAnimationFrame(pony.animator);
|
||||
|
||||
// randomize animator time at startup
|
||||
if (!pony.initialized) {
|
||||
pony.initialized = true;
|
||||
updateAnimator(pony.animator, Math.random() * 2);
|
||||
}
|
||||
|
||||
// discard batch if outdated
|
||||
if (pony.batch !== undefined) {
|
||||
const options = pony.drawingOptions;
|
||||
const right = isFacingRight(pony);
|
||||
|
||||
if (
|
||||
holdingUpdated ||
|
||||
toScreenX(pony.x) !== pony.lastX || toScreenYWithZ(pony.y, pony.z) !== pony.lastY ||
|
||||
pony.lastRight !== right ||
|
||||
pony.zzzEffect.dirty || pony.cryEffect.dirty || pony.sneezeEffect.dirty || pony.holdPoofEffect.dirty ||
|
||||
pony.heartsEffect.dirty || pony.magicEffect.dirty ||
|
||||
options.flipped !== right || options.selected !== pony.selected || options.extra !== pony.extra ||
|
||||
options.toy !== pony.toy ||
|
||||
!isStateEqual(pony.lastState, state)
|
||||
) {
|
||||
pony.discardBatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
// update bounds
|
||||
const flying = isPonyFlying(pony);
|
||||
const flyingUpOrDown = isFlyingUpOrDown(pony.animator.state);
|
||||
const flyingOrFlyingUpOrDown = flying || flyingUpOrDown;
|
||||
|
||||
pony.bounds = flyingOrFlyingUpOrDown ? boundsFly : bounds;
|
||||
pony.interactBounds = flying ? interactBoundsFly : interactBounds;
|
||||
pony.lightBounds = flyingOrFlyingUpOrDown ? lightBoundsFly : lightBounds;
|
||||
pony.lightSpriteBounds = flyingOrFlyingUpOrDown ? lightBoundsFly : lightBounds;
|
||||
}
|
||||
|
||||
export function updatePonyHold(pony: Pony, game: PonyTownGame) {
|
||||
const ponyState = pony.ponyState;
|
||||
const hadLight = hasDrawLight(pony);
|
||||
const hadLightSprite = hasLightSprite(pony);
|
||||
|
||||
if (pony.hold !== 0) {
|
||||
if (ponyState.holding === undefined) {
|
||||
ponyState.holding = createAnEntity(pony.hold, 0, 0, 0, {}, pony.paletteManager, game);
|
||||
} else if (ponyState.holding.type !== pony.hold) {
|
||||
releaseEntity(ponyState.holding);
|
||||
ponyState.holding = createAnEntity(pony.hold, 0, 0, 0, {}, pony.paletteManager, game);
|
||||
}
|
||||
} else if (ponyState.holding !== undefined) {
|
||||
releaseEntity(ponyState.holding);
|
||||
ponyState.holding = undefined;
|
||||
}
|
||||
|
||||
const hasLight = hasDrawLight(pony);
|
||||
const hasLightSprite1 = hasLightSprite(pony);
|
||||
|
||||
addOrRemoveFromEntityList(game.map.entitiesLight, pony, hadLight, hasLight);
|
||||
addOrRemoveFromEntityList(game.map.entitiesLightSprite, pony, hadLightSprite, hasLightSprite1);
|
||||
}
|
||||
|
||||
function filterExpression(expression: Expression) {
|
||||
const extra = expression.extra;
|
||||
const blush = hasFlag(extra, ExpressionExtra.Blush);
|
||||
|
||||
if (
|
||||
blush ||
|
||||
hasFlag(extra, ExpressionExtra.Hearts) ||
|
||||
hasFlag(extra, ExpressionExtra.Cry) ||
|
||||
isEyeSleeping(expression.left) ||
|
||||
isEyeSleeping(expression.right)
|
||||
) {
|
||||
if (expression.muzzle === Muzzle.SmilePant || expression.muzzle === Muzzle.NeutralPant) {
|
||||
expression.muzzle = Muzzle.Neutral;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
blush ||
|
||||
expression.muzzle === Muzzle.SmilePant ||
|
||||
expression.muzzle === Muzzle.NeutralPant
|
||||
) {
|
||||
if (expression.leftIris === Iris.Up || expression.rightIris === Iris.Up) {
|
||||
expression.leftIris = Iris.Forward;
|
||||
expression.rightIris = Iris.Forward;
|
||||
}
|
||||
|
||||
if (expression.muzzle === Muzzle.SmilePant) {
|
||||
expression.muzzle = Muzzle.SmileOpen;
|
||||
} else if (expression.muzzle === Muzzle.NeutralPant) {
|
||||
expression.muzzle = Muzzle.NeutralOpen2;
|
||||
}
|
||||
}
|
||||
|
||||
if (blush) {
|
||||
if (expression.muzzle === Muzzle.SmileOpen2) {
|
||||
expression.muzzle = Muzzle.SmileOpen;
|
||||
} else if (expression.muzzle === Muzzle.FrownOpen) {
|
||||
expression.muzzle = Muzzle.ConcernedOpen;
|
||||
} else if (expression.muzzle === Muzzle.NeutralOpen2) {
|
||||
expression.muzzle = Muzzle.Oh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updatePonyExpression(pony: Pony, expr: number, safe: boolean) {
|
||||
const expression = decodeExpression(expr);
|
||||
pony.currentExpression = pony.expr;
|
||||
pony.ponyState.expression = expression;
|
||||
|
||||
if (expression && safe) {
|
||||
filterExpression(expression);
|
||||
}
|
||||
|
||||
const extra = (expression && expression.extra) || 0;
|
||||
|
||||
if (hasFlag(extra, ExpressionExtra.Cry)) {
|
||||
playAnimation(pony.cryEffect, cryAnimation);
|
||||
} else if (hasFlag(extra, ExpressionExtra.Tears)) {
|
||||
playAnimation(pony.cryEffect, tearsAnimation);
|
||||
} else {
|
||||
playAnimation(pony.cryEffect, undefined);
|
||||
}
|
||||
|
||||
if (hasFlag(extra, ExpressionExtra.Zzz)) {
|
||||
playOneOfAnimations(pony.zzzEffect, zzzAnimations);
|
||||
} else {
|
||||
playAnimation(pony.zzzEffect, undefined);
|
||||
}
|
||||
|
||||
if (hasFlag(extra, ExpressionExtra.Hearts)) {
|
||||
playAnimation(pony.heartsEffect, heartsAnimation);
|
||||
} else {
|
||||
playAnimation(pony.heartsEffect, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function transformBatch(batch: SpriteBatch | PaletteSpriteBatch, entity: Entity) {
|
||||
batch.translate(toScreenX(entity.x), toScreenYWithZ(entity.y, entity.z));
|
||||
batch.scale(isFacingRight(entity) ? -1 : 1, 1);
|
||||
}
|
||||
|
||||
function releasePalettePonyInfo(pony: Pony) {
|
||||
if (pony.palettePonyInfo !== undefined) {
|
||||
releasePalettes(pony.palettePonyInfo);
|
||||
pony.palettePonyInfo = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function makeLightBounds({ x, y, w, h }: Rect) {
|
||||
return rect(x - lightExtentX, y - lightExtentY, w + lightExtentX * 2, h + lightExtentY * 2);
|
||||
}
|
||||
|
||||
function drawFaceExtra(batch: PaletteSpriteBatch, pony: Pony) {
|
||||
if (isAnimationPlaying(pony.cryEffect)) {
|
||||
const flip = isFacingRight(pony) ? !pony.ponyState.headTurned : pony.ponyState.headTurned;
|
||||
const maxY = isPonyLying(pony) ? 62 : (isPonySitting(pony) ? 65 : 0);
|
||||
drawAnimation(batch, pony.cryEffect, 0, 0, WHITE, flip, maxY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
import * as sprites from '../generated/sprites';
|
||||
import { releasePalette, createPalette } from '../graphics/paletteManager';
|
||||
import {
|
||||
PonyInfo, SpriteSet, PalettePonyInfo, PaletteSpriteSet, PaletteManager, Palette, ColorExtraSets, PonyInfoBase,
|
||||
PonyInfoNumber, ColorExtra
|
||||
} from './interfaces';
|
||||
import { toInt, array, includes, att } from './utils';
|
||||
import { CM_SIZE } from './constants';
|
||||
import { parseColorFast, getR, getAlpha, colorFromRGBA, getG, getB, colorToHexRGB } from './color';
|
||||
import { BLACK, fillToOutline, fillToOutlineColor, WHITE, TRANSPARENT, fillToOutlineWithDarken } from './colors';
|
||||
import {
|
||||
mergedManes, mergedBackManes, mergedFacialHair, mergedEarAccessories, mergedChestAccessories,
|
||||
SLEEVED_ACCESSORIES, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories
|
||||
} from '../client/ponyUtils';
|
||||
|
||||
const MAX_COLORS = 6;
|
||||
const FILLS = ['1e90ff', '32cd32', 'da70d6', 'dc143c', '7fffd4'];
|
||||
|
||||
const frontHooves = sprites.frontLegHooves[1] as ColorExtraSets;
|
||||
const backHooves = sprites.backLegHooves[1] as ColorExtraSets;
|
||||
const frontLegAccessories = sprites.frontLegAccessories[1] as ColorExtraSets;
|
||||
const backLegAccessories = sprites.backLegAccessories[1] as ColorExtraSets;
|
||||
const frontLegSleeves = sprites.frontLegSleeves[1] as ColorExtraSets;
|
||||
|
||||
type Arr<T> = (T | undefined)[] | undefined;
|
||||
type PonyInfoGeneric<T> = PonyInfoBase<T, SpriteSet<T>>;
|
||||
|
||||
export const mockPaletteManager: PaletteManager = {
|
||||
add(colors: number[]): Palette {
|
||||
return this.addArray(new Uint32Array(colors));
|
||||
},
|
||||
addArray(colors: Uint32Array): Palette {
|
||||
return createPalette(colors);
|
||||
},
|
||||
init() {
|
||||
}
|
||||
};
|
||||
|
||||
export function spriteSet(type: number, lockFirstFill = true, fill = 'ffd700', otherFills = FILLS): SpriteSet<string> {
|
||||
if (otherFills.length !== (MAX_COLORS - 1))
|
||||
throw new Error('Invalid fills count');
|
||||
|
||||
const fills = [fill, ...otherFills];
|
||||
const outlines = fills.map(fillToOutline);
|
||||
|
||||
return {
|
||||
type,
|
||||
pattern: 0,
|
||||
fills,
|
||||
outlines,
|
||||
lockFills: [lockFirstFill, ...array(MAX_COLORS - 1, false)],
|
||||
lockOutlines: array(MAX_COLORS, true),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDefaultPony(): PonyInfo {
|
||||
const pony = createBasePony();
|
||||
pony.mane!.type = 2;
|
||||
pony.backMane!.type = 1;
|
||||
pony.tail!.type = 1;
|
||||
return pony;
|
||||
}
|
||||
|
||||
export function createBasePony(): PonyInfo {
|
||||
return syncLockedPonyInfo({
|
||||
head: spriteSet(0, true, 'ff0000', ['800000', '32cd32', 'da70d6', 'dc143c', '7fffd4']),
|
||||
nose: spriteSet(0, true, 'ff0000', ['800000', '32cd32', 'da70d6', 'dc143c', '7fffd4']),
|
||||
ears: spriteSet(0, true, 'ff0000'),
|
||||
horn: spriteSet(0, true, 'ff0000'),
|
||||
wings: spriteSet(0, true, 'ff0000'),
|
||||
frontHooves: spriteSet(0, false, 'ffa500', ['ffff00', '32cd32', 'da70d6', 'dc143c', '7fffd4']),
|
||||
backHooves: spriteSet(0, true, 'ffa500'),
|
||||
|
||||
mane: spriteSet(0, false),
|
||||
backMane: spriteSet(0),
|
||||
tail: spriteSet(0),
|
||||
facialHair: spriteSet(0),
|
||||
|
||||
headAccessory: spriteSet(0, false, 'ee82ee'),
|
||||
earAccessory: spriteSet(0, false, '808080'),
|
||||
faceAccessory: spriteSet(0, false, '000000'),
|
||||
neckAccessory: spriteSet(0, false, 'ee82ee'),
|
||||
frontLegAccessory: spriteSet(0, false, 'ee82ee'),
|
||||
backLegAccessory: spriteSet(0, false, 'ee82ee'),
|
||||
frontLegAccessoryRight: spriteSet(0, false, 'ee82ee'),
|
||||
backLegAccessoryRight: spriteSet(0, false, 'ee82ee'),
|
||||
lockBackLegAccessory: true,
|
||||
unlockFrontLegAccessory: false,
|
||||
unlockBackLegAccessory: false,
|
||||
backAccessory: spriteSet(0, false, 'ee82ee'),
|
||||
waistAccessory: spriteSet(0, false, '95856f', ['674b43', '4f4f4f', '525252', 'c37850', '8a3d34']),
|
||||
chestAccessory: spriteSet(0, false, 'ee82ee'),
|
||||
sleeveAccessory: spriteSet(0, true, 'ee82ee'),
|
||||
extraAccessory: {
|
||||
...spriteSet(0, true, 'ff0000', ['daa520', 'ffd700', 'ffd700', 'ffd700', 'ffd700']),
|
||||
lockFills: array(5, true),
|
||||
},
|
||||
|
||||
coatFill: 'ff0000',
|
||||
coatOutline: '8b0000',
|
||||
lockCoatOutline: true,
|
||||
|
||||
eyelashes: 0,
|
||||
eyeColorLeft: 'daa520',
|
||||
eyeColorRight: 'daa520',
|
||||
eyeWhitesLeft: 'ffffff',
|
||||
eyeWhites: 'ffffff',
|
||||
eyeOpennessLeft: 1,
|
||||
eyeOpennessRight: 1,
|
||||
eyeshadow: false,
|
||||
eyeshadowColor: '000000',
|
||||
lockEyes: true,
|
||||
lockEyeColor: true,
|
||||
unlockEyeWhites: false,
|
||||
unlockEyelashColor: false,
|
||||
eyelashColor: '000000',
|
||||
eyelashColorLeft: '000000',
|
||||
|
||||
fangs: 0,
|
||||
muzzle: 0,
|
||||
freckles: 0,
|
||||
frecklesColor: '8b0000',
|
||||
magicColor: 'ffffff',
|
||||
|
||||
cm: [],
|
||||
cmFlip: false,
|
||||
|
||||
customOutlines: false,
|
||||
freeOutlines: false,
|
||||
darkenLockedOutlines: false,
|
||||
});
|
||||
}
|
||||
|
||||
// sync
|
||||
|
||||
type FillToOutline<T> = (fill: T | undefined) => T | undefined;
|
||||
|
||||
export function getBaseFill<T>(set?: SpriteSet<T>): T | undefined {
|
||||
return set && set.fills && set.fills[0];
|
||||
}
|
||||
|
||||
export function getBaseOutline<T>(set?: SpriteSet<T>): T | undefined {
|
||||
return set && set.outlines && set.outlines[0];
|
||||
}
|
||||
|
||||
export function syncLockedSpriteSet<T>(
|
||||
set: SpriteSet<T> | undefined, customOutlines: boolean, fillToOutline: FillToOutline<T>, baseFill?: T,
|
||||
baseOutline?: T
|
||||
) {
|
||||
if (set === undefined)
|
||||
return;
|
||||
|
||||
const fills = set.fills;
|
||||
|
||||
if (!fills)
|
||||
return;
|
||||
|
||||
const lockFills = set.lockFills;
|
||||
|
||||
if (lockFills) {
|
||||
for (let i = 0; i < lockFills.length; i++) {
|
||||
if (lockFills[i]) {
|
||||
fills[i] = i === 0 ? baseFill : fills[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const outlines = set.outlines;
|
||||
const lockOutlines = set.lockOutlines;
|
||||
|
||||
if (outlines && lockOutlines) {
|
||||
for (let i = 0; i < lockOutlines.length; i++) {
|
||||
if (!customOutlines) {
|
||||
lockOutlines[i] = true;
|
||||
}
|
||||
|
||||
if (lockOutlines[i]) {
|
||||
if (i === 0 && baseOutline && lockFills && lockFills[i]) {
|
||||
outlines[i] = baseOutline;
|
||||
} else {
|
||||
outlines[i] = fillToOutline(fills[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncLockedSpritesSet2<T>(
|
||||
set: SpriteSet<T> | undefined, fillToOutline: FillToOutline<T>, baseFills: (T | undefined)[],
|
||||
baseOutlines: (T | undefined)[]
|
||||
) {
|
||||
if (set && set.fills && set.lockFills) {
|
||||
set.lockFills.forEach((locked, i) => {
|
||||
if (locked) {
|
||||
set.fills![i] = baseFills[i];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (set && set.fills && set.outlines && set.lockOutlines) {
|
||||
set.lockOutlines.forEach((locked, i) => {
|
||||
if (locked) {
|
||||
if (baseOutlines[i] && set.lockFills && set.lockFills[i]) {
|
||||
set.outlines![i] = baseOutlines[i];
|
||||
} else {
|
||||
set.outlines![i] = fillToOutline(set.fills![i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getFillOf2<T>(set: SpriteSet<T> | undefined, defaultColor: T): T | undefined {
|
||||
return set && set.type && set.fills && set.fills[0] || defaultColor;
|
||||
}
|
||||
|
||||
function getOutlineOf2<T>(set: SpriteSet<T> | undefined, defaultColor: T): T | undefined {
|
||||
return set && set.type && set.outlines && set.outlines[0] || defaultColor;
|
||||
}
|
||||
|
||||
function syncLockedBasePonyInfo<T>(
|
||||
info: PonyInfoGeneric<T>, fillToOutline: FillToOutline<T>, defaultColor: T
|
||||
): PonyInfoGeneric<T> {
|
||||
const customOutlines = !!info.customOutlines;
|
||||
|
||||
if (!customOutlines || info.lockCoatOutline) {
|
||||
info.coatOutline = fillToOutline(info.coatFill);
|
||||
}
|
||||
|
||||
if (info.lockEyes) {
|
||||
info.eyeOpennessLeft = info.eyeOpennessRight;
|
||||
}
|
||||
|
||||
if (info.lockEyeColor) {
|
||||
info.eyeColorLeft = info.eyeColorRight;
|
||||
}
|
||||
|
||||
if (!info.unlockEyeWhites) {
|
||||
info.eyeWhitesLeft = info.eyeWhites;
|
||||
}
|
||||
|
||||
if (!info.unlockEyelashColor) {
|
||||
info.eyelashColorLeft = info.eyelashColor;
|
||||
}
|
||||
|
||||
syncLockedSpriteSet<T>(info.head, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
|
||||
syncLockedSpriteSet<T>(info.nose, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
|
||||
syncLockedSpriteSet<T>(info.ears, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
|
||||
syncLockedSpriteSet<T>(info.horn, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
|
||||
syncLockedSpriteSet<T>(info.wings, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
|
||||
syncLockedSpriteSet<T>(info.frontHooves, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
|
||||
syncLockedSpriteSet<T>(
|
||||
info.backHooves, customOutlines, fillToOutline, getBaseFill(info.frontHooves), getBaseOutline(info.frontHooves));
|
||||
|
||||
syncLockedSpriteSet<T>(info.mane, customOutlines, fillToOutline);
|
||||
|
||||
const baseManeFill = getBaseFill(info.mane);
|
||||
const baseManeOutline = getBaseOutline(info.mane);
|
||||
|
||||
syncLockedSpriteSet<T>(info.backMane, customOutlines, fillToOutline, baseManeFill, baseManeOutline);
|
||||
syncLockedSpriteSet<T>(info.tail, customOutlines, fillToOutline, baseManeFill, baseManeOutline);
|
||||
syncLockedSpriteSet<T>(info.facialHair, customOutlines, fillToOutline, baseManeFill, baseManeOutline);
|
||||
|
||||
syncLockedSpriteSet<T>(info.headAccessory, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.earAccessory, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.faceAccessory, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.neckAccessory, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.frontLegAccessory, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.backLegAccessory, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.frontLegAccessoryRight, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.backLegAccessoryRight, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.backAccessory, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.waistAccessory, customOutlines, fillToOutline);
|
||||
syncLockedSpriteSet<T>(info.chestAccessory, customOutlines, fillToOutline);
|
||||
|
||||
if (info.chestAccessory && !info.sleeveAccessory && includes(SLEEVED_ACCESSORIES, info.chestAccessory.type)) {
|
||||
info.sleeveAccessory = {
|
||||
type: 0,
|
||||
pattern: 0,
|
||||
fills: [],
|
||||
outlines: [],
|
||||
lockFills: array(MAX_COLORS, true),
|
||||
lockOutlines: array(MAX_COLORS, true),
|
||||
};
|
||||
}
|
||||
|
||||
syncLockedSpriteSet<T>(
|
||||
info.sleeveAccessory, customOutlines, fillToOutline, getBaseFill(info.chestAccessory), getBaseOutline(info.chestAccessory));
|
||||
|
||||
syncLockedSpritesSet2<T>(info.extraAccessory, fillToOutline, [
|
||||
info.coatFill,
|
||||
info.eyeColorRight,
|
||||
getFillOf2(info.mane, defaultColor),
|
||||
getFillOf2(info.backMane, defaultColor),
|
||||
getFillOf2(info.tail, defaultColor),
|
||||
], [
|
||||
info.coatOutline,
|
||||
info.eyeColorRight,
|
||||
getOutlineOf2(info.mane, defaultColor),
|
||||
getOutlineOf2(info.backMane, defaultColor),
|
||||
getOutlineOf2(info.tail, defaultColor),
|
||||
]);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
export function syncLockedPonyInfo(info: PonyInfo): PonyInfo {
|
||||
const darkenLocked = !!info.freeOutlines && !!info.darkenLockedOutlines;
|
||||
const fillToOutlineFunc = darkenLocked ? fillToOutlineWithDarken : fillToOutline;
|
||||
return syncLockedBasePonyInfo<string>(info, fillToOutlineFunc, '000000');
|
||||
}
|
||||
|
||||
function fillToOutlineSafe(color: number | undefined) {
|
||||
return fillToOutlineColor((color === undefined || color === 0) ? BLACK : color);
|
||||
}
|
||||
|
||||
function fillToOutlineSafeWithDarken(color: number | undefined) {
|
||||
return darkenForOutline(fillToOutlineColor((color === undefined || color === 0) ? BLACK : color));
|
||||
}
|
||||
|
||||
export function syncLockedPonyInfoNumber(info: PonyInfoNumber): PonyInfoNumber {
|
||||
const darkenLocked = !!info.freeOutlines && !!info.darkenLockedOutlines;
|
||||
const fillToOutlineFunc = darkenLocked ? fillToOutlineSafeWithDarken : fillToOutlineSafe;
|
||||
return syncLockedBasePonyInfo<number>(info, fillToOutlineFunc, BLACK);
|
||||
}
|
||||
|
||||
// PalettePonyInfo
|
||||
|
||||
function parseFast(color: string | undefined): number {
|
||||
return color ? parseColorFast(color) : BLACK;
|
||||
}
|
||||
|
||||
function parseCMColor(color: string): number {
|
||||
return color ? parseColorFast(color) : TRANSPARENT;
|
||||
}
|
||||
|
||||
export function toColorList(colors: (string | undefined)[]): Uint32Array {
|
||||
const result = new Uint32Array(colors.length + 1);
|
||||
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
result[i + 1] = parseFast(colors[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function darkenForOutline(color: number) {
|
||||
const mult = (159 / 255);
|
||||
const r = (mult * getR(color)) | 0;
|
||||
const g = (mult * getG(color)) | 0;
|
||||
const b = (mult * getB(color)) | 0;
|
||||
const a = getAlpha(color);
|
||||
return colorFromRGBA(r, g, b, a);
|
||||
}
|
||||
|
||||
function getColorsGeneric(
|
||||
fillColors: Arr<string>, outlineColors: Arr<string>, defaultColor: string, length: number, darken: boolean
|
||||
): string[] {
|
||||
const fills = fillColors || [];
|
||||
const outlines = outlineColors || [];
|
||||
const colors = array(length * 2, defaultColor);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
colors[i * 2] = fills[i] || defaultColor;
|
||||
|
||||
if (darken) {
|
||||
colors[i * 2 + 1] = outlines[i] ? colorToHexRGB(darkenForOutline(parseColorFast(outlines[i]!))) : defaultColor;
|
||||
} else {
|
||||
colors[i * 2 + 1] = outlines[i] || defaultColor;
|
||||
}
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
export function getColorsFromSet({ fills, outlines }: SpriteSet<string>, defaultColor: string, darken: boolean): string[] {
|
||||
const length = Math.max(fills ? fills.length : 0, outlines ? outlines.length : 0);
|
||||
return getColorsGeneric(fills, outlines, defaultColor, length, darken);
|
||||
}
|
||||
|
||||
export function toColorListNumber(colors: (number | undefined)[]): Uint32Array {
|
||||
const result = new Uint32Array(colors.length + 1);
|
||||
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
result[i + 1] = colors[i] || BLACK;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export type GetColorsForSet<T> = (set: SpriteSet<T>, count: number, darken: boolean) => Uint32Array;
|
||||
|
||||
export const getColorsForSet: GetColorsForSet<string> = (set, count, darken) => {
|
||||
const t = getColorsGeneric(set.fills, set.outlines, '000000', count, darken);
|
||||
return toColorList(t);
|
||||
};
|
||||
|
||||
const emptyArray: number[] = [];
|
||||
|
||||
export const getColorsForSetNumber: GetColorsForSet<number> = (set, length, darken) => {
|
||||
const fills = set.fills || emptyArray;
|
||||
const outlines = set.outlines || emptyArray;
|
||||
const result = new Uint32Array(length * 2 + 1);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result[((i << 1) + 1) | 0] = i < fills.length ? (fills[i] || BLACK) : BLACK;
|
||||
|
||||
if (darken) {
|
||||
result[((i << 1) + 2) | 0] = i < outlines.length ? darkenForOutline(outlines[i] || BLACK) : BLACK;
|
||||
} else {
|
||||
result[((i << 1) + 2) | 0] = i < outlines.length ? (outlines[i] || BLACK) : BLACK;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
function getExtraPalette(pattern: ColorExtra | undefined, manager: PaletteManager): Palette | undefined {
|
||||
const extraPalette = pattern && pattern.palettes && pattern.palettes[0];
|
||||
return extraPalette && manager.addArray(new Uint32Array(extraPalette));
|
||||
}
|
||||
|
||||
export function toPaletteSet<T>(
|
||||
set: SpriteSet<T>, sets: ColorExtraSets, manager: PaletteManager, getColorsForSet: GetColorsForSet<T>,
|
||||
hasExtra: boolean, darken: boolean
|
||||
): PaletteSpriteSet | undefined {
|
||||
const pattern = att(att(sets, set.type), set.pattern);
|
||||
const colorCount = pattern !== undefined && pattern.colors !== undefined ? ((pattern.colors - 1) >> 1) : 0;
|
||||
const colors = getColorsForSet(set, colorCount, darken);
|
||||
|
||||
return {
|
||||
type: toInt(set.type),
|
||||
pattern: toInt(set.pattern),
|
||||
palette: manager.addArray(colors),
|
||||
extraPalette: hasExtra ? getExtraPalette(pattern, manager) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function createCMPalette<T>(
|
||||
cm: T[] | undefined, manager: PaletteManager, parseColor: (color: T) => number
|
||||
): Palette | undefined {
|
||||
const size = CM_SIZE * CM_SIZE;
|
||||
|
||||
if (cm === undefined || cm.length === 0 || cm.length > size)
|
||||
return undefined;
|
||||
|
||||
const result = new Uint32Array(size);
|
||||
|
||||
for (let i = 0; i < cm.length; i++) {
|
||||
result[i] = parseColor(cm[i]);
|
||||
}
|
||||
|
||||
return manager.addArray(result);
|
||||
}
|
||||
|
||||
export type ToSet<T> = (set: SpriteSet<T> | undefined, sets: ColorExtraSets, extra?: boolean) => PaletteSpriteSet | undefined;
|
||||
|
||||
const defaultPalette = new Uint32Array(sprites.defaultPalette);
|
||||
|
||||
export const createToPaletteSet =
|
||||
<T>(manager: PaletteManager, getColorsForSet: GetColorsForSet<T>, extra: boolean, darken: boolean): ToSet<T> =>
|
||||
(set, sets) => set === undefined ? undefined : toPaletteSet(set, sets, manager, getColorsForSet, extra, darken);
|
||||
|
||||
export function toPaletteGeneric<T>(
|
||||
info: PonyInfoGeneric<T>, manager: PaletteManager, toColorList: (color: (T | undefined)[]) => Uint32Array,
|
||||
getColorsForSet: GetColorsForSet<T>, blackColor: T, whiteColor: T, parseCMColor: (color: T) => number
|
||||
): PalettePonyInfo {
|
||||
const darken = !info.freeOutlines;
|
||||
const toSet = createToPaletteSet(manager, getColorsForSet, false, darken);
|
||||
const toSetExtra = createToPaletteSet(manager, getColorsForSet, true, darken);
|
||||
|
||||
const defaultSet = { type: 0, pattern: 0, fills: [info.coatFill], outlines: [info.coatOutline] };
|
||||
// const defaultSet = { type: 0, pattern: 1, fills: [info.coatFill, whiteColor], outlines: [info.coatOutline, blackColor] };
|
||||
|
||||
return {
|
||||
body: toSet(defaultSet, sprites.body[1]),
|
||||
head: toSet(info.head || defaultSet, sprites.head0[1]),
|
||||
nose: toSet(info.nose, sprites.noses[0]),
|
||||
ears: toSet(info.ears || defaultSet, sprites.ears),
|
||||
horn: toSet(info.horn, sprites.horns),
|
||||
wings: toSet(info.wings, sprites.wings[0]),
|
||||
frontLegs: toSet(defaultSet, sprites.frontLegs[1]),
|
||||
backLegs: toSet(defaultSet, sprites.backLegs[1]),
|
||||
frontHooves: toSet(info.frontHooves, frontHooves),
|
||||
backHooves: toSet(info.backHooves, backHooves),
|
||||
|
||||
mane: toSet(info.mane, mergedManes),
|
||||
backMane: toSet(info.backMane, mergedBackManes),
|
||||
tail: toSet(info.tail, sprites.tails[0]),
|
||||
facialHair: toSet(info.facialHair, mergedFacialHair),
|
||||
|
||||
headAccessory: toSet(info.headAccessory, mergedHeadAccessories),
|
||||
earAccessory: toSet(info.earAccessory, mergedEarAccessories),
|
||||
faceAccessory: toSetExtra(info.faceAccessory, sprites.faceAccessories),
|
||||
// faceAccessoryExtraPalette: getExtraPartPalette(info.faceAccessory, sprites.faceAccessoriesExtra, manager),
|
||||
neckAccessory: toSet(info.neckAccessory, sprites.neckAccessories[1]),
|
||||
frontLegAccessory: toSet(
|
||||
info.frontLegAccessory, frontLegAccessories),
|
||||
backLegAccessory: toSet(
|
||||
info.lockBackLegAccessory ? info.frontLegAccessory : info.backLegAccessory, backLegAccessories),
|
||||
frontLegAccessoryRight: toSet(
|
||||
info.unlockFrontLegAccessory ? info.frontLegAccessoryRight : info.frontLegAccessory, frontLegAccessories),
|
||||
backLegAccessoryRight: toSet(
|
||||
info.lockBackLegAccessory ?
|
||||
(info.unlockFrontLegAccessory ? info.frontLegAccessoryRight : info.frontLegAccessory) :
|
||||
(info.unlockBackLegAccessory ? info.backLegAccessoryRight : info.backLegAccessory), backLegAccessories),
|
||||
lockBackLegAccessory: info.lockBackLegAccessory,
|
||||
unlockFrontLegAccessory: info.unlockFrontLegAccessory,
|
||||
unlockBackLegAccessory: info.unlockBackLegAccessory,
|
||||
backAccessory: toSet(info.backAccessory, mergedBackAccessories),
|
||||
waistAccessory: toSet(info.waistAccessory, sprites.waistAccessories[1]),
|
||||
chestAccessory: toSet(info.chestAccessory, mergedChestAccessories),
|
||||
sleeveAccessory: toSet(info.sleeveAccessory, frontLegSleeves),
|
||||
extraAccessory: toSet(info.extraAccessory, mergedExtraAccessories),
|
||||
|
||||
coatPalette: manager.addArray(toColorList([info.coatFill, info.coatOutline])),
|
||||
coatFill: undefined,
|
||||
coatOutline: undefined,
|
||||
lockCoatOutline: !!info.lockCoatOutline,
|
||||
|
||||
eyelashes: toInt(info.eyelashes),
|
||||
eyePaletteLeft: manager.addArray(toColorList([
|
||||
info.eyeWhitesLeft || whiteColor,
|
||||
info.eyelashColor || blackColor
|
||||
])),
|
||||
eyePalette: manager.addArray(toColorList([
|
||||
info.eyeWhites || whiteColor,
|
||||
(info.unlockEyelashColor ? info.eyelashColorLeft : info.eyelashColor) || blackColor
|
||||
])),
|
||||
eyeColorLeft: manager.addArray(toColorList([info.eyeColorLeft])),
|
||||
eyeColorRight: manager.addArray(toColorList([info.eyeColorRight])),
|
||||
eyeWhitesLeft: undefined,
|
||||
eyeWhites: undefined,
|
||||
eyeOpennessLeft: toInt(info.eyeOpennessLeft),
|
||||
eyeOpennessRight: toInt(info.eyeOpennessRight),
|
||||
eyeshadow: info.eyeshadow,
|
||||
eyeshadowColor: manager.addArray(toColorList([info.eyeshadowColor])),
|
||||
lockEyes: !!info.lockEyes,
|
||||
lockEyeColor: !!info.lockEyeColor,
|
||||
unlockEyeWhites: !!info.unlockEyeWhites,
|
||||
unlockEyelashColor: !!info.unlockEyelashColor,
|
||||
eyelashColor: undefined,
|
||||
eyelashColorLeft: undefined,
|
||||
|
||||
fangs: toInt(info.fangs),
|
||||
muzzle: toInt(info.muzzle),
|
||||
freckles: 0, // remove
|
||||
frecklesColor: undefined, // TODO: remove
|
||||
magicColor: undefined,
|
||||
magicColorValue: typeof info.magicColor === 'string' ? parseColorFast(info.magicColor) : toInt(info.magicColor),
|
||||
|
||||
cm: undefined,
|
||||
cmFlip: !!info.cmFlip,
|
||||
cmPalette: createCMPalette<T>(info.cm, manager, parseCMColor),
|
||||
|
||||
customOutlines: !!info.customOutlines,
|
||||
freeOutlines: !!info.freeOutlines,
|
||||
darkenLockedOutlines: !!info.darkenLockedOutlines,
|
||||
defaultPalette: manager.addArray(defaultPalette),
|
||||
waterPalette: manager.addArray(sprites.pony_wake_1.palette),
|
||||
};
|
||||
}
|
||||
|
||||
export function toPalette(info: PonyInfo, manager = mockPaletteManager): PalettePonyInfo {
|
||||
return toPaletteGeneric(info, manager, toColorList, getColorsForSet, '000000', 'ffffff', parseCMColor);
|
||||
}
|
||||
|
||||
export function toPaletteNumber(info: PonyInfoNumber, manager = mockPaletteManager): PalettePonyInfo {
|
||||
return toPaletteGeneric<number>(info, manager, toColorListNumber, getColorsForSetNumber, BLACK, WHITE, x => x);
|
||||
}
|
||||
|
||||
export function releasePalettes(info: PalettePonyInfo): void {
|
||||
for (const key of Object.keys(info)) {
|
||||
const value = (info as any)[key]; // undefined | number | string | PaletteSpriteSet | Palette;
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
if ('refs' in value) {
|
||||
const palette = value as Palette;
|
||||
releasePalette(palette);
|
||||
} else if ('palette' in value) {
|
||||
const set = value as PaletteSpriteSet;
|
||||
releasePalette(set.palette);
|
||||
releasePalette(set.extraPalette);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { tileWidth, tileHeight, tileElevation } from './constants';
|
||||
import { Point, Rect } from './interfaces';
|
||||
|
||||
export function toScreenX(x: number) {
|
||||
return Math.floor(x * tileWidth) | 0;
|
||||
}
|
||||
|
||||
export function toScreenY(y: number) {
|
||||
return Math.floor(y * tileHeight) | 0;
|
||||
}
|
||||
|
||||
export function toScreenYWithZ(y: number, z: number) {
|
||||
return Math.floor(y * tileHeight - z * tileElevation) | 0;
|
||||
}
|
||||
|
||||
export function toWorldX(x: number) {
|
||||
return x / tileWidth;
|
||||
}
|
||||
|
||||
export function toWorldY(y: number) {
|
||||
return y / tileHeight;
|
||||
}
|
||||
|
||||
export function toWorldZ(z: number) {
|
||||
return z / tileElevation;
|
||||
}
|
||||
|
||||
export function pointToScreen({ x, y }: Point): Point {
|
||||
return {
|
||||
x: toScreenX(x),
|
||||
y: toScreenY(y),
|
||||
};
|
||||
}
|
||||
|
||||
export function pointToWorld({ x, y }: Point): Point {
|
||||
return {
|
||||
x: toWorldX(x),
|
||||
y: toWorldY(y),
|
||||
};
|
||||
}
|
||||
|
||||
export function rectToScreen({ x, y, w, h }: Rect): Rect {
|
||||
return {
|
||||
x: toScreenX(x),
|
||||
y: toScreenY(y),
|
||||
w: toScreenX(w),
|
||||
h: toScreenY(h),
|
||||
};
|
||||
}
|
||||
|
||||
export function roundPositionX(x: number) {
|
||||
return Math.floor(x * tileWidth) / tileWidth;
|
||||
}
|
||||
|
||||
export function roundPositionY(y: number) {
|
||||
return Math.floor(y * tileHeight) / tileHeight;
|
||||
}
|
||||
|
||||
export function roundPositionXMidPixel(x: number) {
|
||||
return (Math.floor(x * tileWidth) + 0.5) / tileWidth;
|
||||
}
|
||||
|
||||
export function roundPositionYMidPixel(y: number) {
|
||||
return (Math.floor(y * tileHeight) + 0.5) / tileHeight;
|
||||
}
|
||||
|
||||
export function roundPosition(point: Point) {
|
||||
point.x = roundPositionX(point.x);
|
||||
point.y = roundPositionY(point.y);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Rect, Point } from './interfaces';
|
||||
import { intersect } from './utils';
|
||||
|
||||
export function rect(x: number, y: number, w: number, h: number): Rect {
|
||||
return { x, y, w, h };
|
||||
}
|
||||
|
||||
export function centerPoint(rect: Rect): Point {
|
||||
return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 };
|
||||
}
|
||||
|
||||
export function copyRect(dst: Rect, src: Rect) {
|
||||
dst.x = src.x;
|
||||
dst.y = src.y;
|
||||
dst.w = src.w;
|
||||
dst.h = src.h;
|
||||
}
|
||||
|
||||
export function withBorder({ x, y, w, h }: Rect, border: number) {
|
||||
return rect(x - border, y - border, w + border * 2, h + border * 2);
|
||||
}
|
||||
|
||||
export function withPadding({ x, y, w, h }: Rect, top: number, right: number, bottom: number, left: number) {
|
||||
return rect(x - top, y - left, w + left + right, h + top + bottom);
|
||||
}
|
||||
|
||||
export function rectsIntersect(a: Rect, b: Rect): boolean {
|
||||
return intersect(a.x, a.y, a.w, a.h, b.x, b.y, b.w, b.h);
|
||||
}
|
||||
|
||||
export function addRect(a: Rect, b: Rect) {
|
||||
const x = Math.min(a.x, b.x);
|
||||
const y = Math.min(a.y, b.y);
|
||||
|
||||
a.w = Math.max(a.x + a.w, b.x + b.w) - x;
|
||||
a.h = Math.max(a.y + a.h, b.y + b.h) - y;
|
||||
a.x = x;
|
||||
a.y = y;
|
||||
}
|
||||
|
||||
export function addRects(a: Rect, b: Rect): Rect {
|
||||
const x = Math.min(a.x, b.x);
|
||||
const y = Math.min(a.y, b.y);
|
||||
|
||||
return {
|
||||
x, y,
|
||||
w: Math.max(a.x + a.w, b.x + b.w) - x,
|
||||
h: Math.max(a.y + a.h, b.y + b.h) - y,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { TileType, Region, IMap } from './interfaces';
|
||||
import { clamp } from './utils';
|
||||
import { tileWidth, tileHeight, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from './constants';
|
||||
import { getRegion } from './worldMap';
|
||||
import { toScreenX, toScreenY } from './positionUtils';
|
||||
import { ponyColliders, ponyCollidersBounds } from './mixins';
|
||||
import { decompressTiles } from './compress';
|
||||
|
||||
const { min, max, floor } = Math;
|
||||
|
||||
export function createRegion(x: number, y: number, tileData?: Uint8Array): Region {
|
||||
const size = REGION_SIZE;
|
||||
const tiles = tileData ? decompressTiles(tileData) : new Uint8Array(size * size);
|
||||
const tileIndices = new Int16Array(size * size);
|
||||
const randoms = new Uint8Array(size * size);
|
||||
// const elevation = new Uint8Array(size * size);
|
||||
const collider = new Uint8Array(size * size * tileWidth * tileHeight);
|
||||
|
||||
if (!tileData) {
|
||||
tiles.fill(TileType.Dirt);
|
||||
}
|
||||
|
||||
tileIndices.fill(-1);
|
||||
|
||||
for (let i = 0; i < randoms.length; i++) {
|
||||
randoms[i] = (Math.random() * 256) | 0;
|
||||
}
|
||||
|
||||
return {
|
||||
x, y, tiles, tileIndices,
|
||||
randoms,
|
||||
// elevation,
|
||||
entities: [],
|
||||
colliders: [],
|
||||
collider,
|
||||
colliderDirty: true,
|
||||
tilesDirty: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRegionTile(region: Region, x: number, y: number): TileType {
|
||||
return region.tiles[x | (y << 3)];
|
||||
}
|
||||
|
||||
export function setRegionTile(region: Region, x: number, y: number, type: TileType) {
|
||||
region.tiles[x | (y << 3)] = type;
|
||||
}
|
||||
|
||||
export function getRegionTileIndex(region: Region, x: number, y: number) {
|
||||
return region.tileIndices[x | (y << 3)];
|
||||
}
|
||||
|
||||
export function setRegionTileDirty(region: Region, x: number, y: number) {
|
||||
region.tileIndices[x | (y << 3)] = -1;
|
||||
region.tilesDirty = true;
|
||||
}
|
||||
|
||||
export function getRegionElevation(_region: Region, _x: number, _y: number) {
|
||||
return 0; // region.elevation[x | (y << 3)];
|
||||
}
|
||||
|
||||
export function setRegionElevation(_region: Region, _x: number, _y: number, _value: number) {
|
||||
// region.elevation[x | (y << 3)] = value;
|
||||
}
|
||||
|
||||
export function worldToRegionX<T>(x: number, map: IMap<T>) {
|
||||
return clamp(floor(x / REGION_SIZE), 0, map.regionsX - 1);
|
||||
}
|
||||
|
||||
export function worldToRegionY<T>(y: number, map: IMap<T>) {
|
||||
return clamp(floor(y / REGION_SIZE), 0, map.regionsY - 1);
|
||||
}
|
||||
|
||||
export function invalidateRegionsCollider<T extends Region | undefined>(region: Region, map: IMap<T>) {
|
||||
const minY = max(0, region.y - 1);
|
||||
const maxY = min(map.regionsY - 1, region.y + 1);
|
||||
const minX = max(0, region.x - 1);
|
||||
const maxX = min(map.regionsX - 1, region.x + 1);
|
||||
|
||||
for (let ry = minY; ry <= maxY; ry++) {
|
||||
for (let rx = minX; rx <= maxX; rx++) {
|
||||
const r = getRegion(map, rx, ry);
|
||||
|
||||
if (r) {
|
||||
r.colliderDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generateRegionCollider<T extends Region | undefined>(region: Region, map: IMap<T>) {
|
||||
const regionCollider = region.collider;
|
||||
const tileTypes = region.tiles;
|
||||
|
||||
region.colliderDirty = false;
|
||||
regionCollider.fill(0);
|
||||
|
||||
for (let ty = 0, i = 0; ty < REGION_SIZE; ty++) {
|
||||
for (let tx = 0; tx < REGION_SIZE; tx++ , i++) {
|
||||
const type = tileTypes[i];
|
||||
|
||||
if (type === TileType.None) {
|
||||
const x0 = (tx * tileWidth) | 0;
|
||||
const y0 = (ty * tileHeight) | 0;
|
||||
const x1 = (x0 + tileWidth) | 0;
|
||||
const y1 = (y0 + tileHeight) | 0;
|
||||
|
||||
for (let y = y0; y < y1; y++) {
|
||||
for (let x = x0; x < x1; x++) {
|
||||
regionCollider[(x + ((y * REGION_WIDTH) | 0)) | 0] = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const minY = max(0, region.y - 1);
|
||||
const maxY = min(map.regionsY - 1, region.y + 1);
|
||||
const minX = max(0, region.x - 1);
|
||||
const maxX = min(map.regionsX - 1, region.x + 1);
|
||||
|
||||
const pBounds = ponyCollidersBounds;
|
||||
const pbX0 = pBounds.x | 0;
|
||||
const pbY0 = pBounds.y | 0;
|
||||
const pbX1 = (pbX0 + pBounds.w) | 0;
|
||||
const pbY1 = (pbY0 + pBounds.h) | 0;
|
||||
|
||||
const baseX = region.x * REGION_SIZE;
|
||||
const baseY = region.y * REGION_SIZE;
|
||||
|
||||
for (let ry = minY; ry <= maxY; ry++) {
|
||||
for (let rx = minX; rx <= maxX; rx++) {
|
||||
const r = getRegion(map, rx, ry);
|
||||
|
||||
if (r === undefined)
|
||||
continue;
|
||||
|
||||
for (const entity of r.colliders) {
|
||||
const entityX = toScreenX(entity.x - baseX) | 0;
|
||||
const entityY = toScreenY(entity.y - baseY) | 0;
|
||||
|
||||
const cBounds = entity.collidersBounds!;
|
||||
const ecbX = entityX + cBounds.x;
|
||||
const ecbY = entityY + cBounds.y;
|
||||
|
||||
if (
|
||||
(ecbX + pbX0) > REGION_WIDTH || (ecbY + pbY0) > REGION_HEIGHT ||
|
||||
(ecbX + cBounds.w + pbX1) < 0 || (ecbY + cBounds.h + pbY1) < 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const c of entity.colliders!) {
|
||||
const value = (c.tall ? 3 : 1) | 0;
|
||||
const baseX0 = (entityX + c.x) | 0;
|
||||
const baseY0 = (entityY + c.y) | 0;
|
||||
const baseX1 = (baseX0 + c.w) | 0;
|
||||
const baseY1 = (baseY0 + c.h) | 0;
|
||||
|
||||
if (c.exact) {
|
||||
const x0 = (baseX0 < 0 ? 0 : baseX0) | 0;
|
||||
const y0 = (baseY0 < 0 ? 0 : baseY0) | 0;
|
||||
const x1 = (baseX1 > REGION_WIDTH ? REGION_WIDTH : baseX1) | 0;
|
||||
const y1 = (baseY1 > REGION_HEIGHT ? REGION_HEIGHT : baseY1) | 0;
|
||||
|
||||
if (x1 > x0 && y1 > y0) {
|
||||
for (let y = y0 | 0; y < y1; y = (y + 1) | 0) {
|
||||
const oy = (y * REGION_WIDTH) | 0;
|
||||
|
||||
for (let x = x0 | 0; x < x1; x = (x + 1) | 0) {
|
||||
regionCollider[(x + oy) | 0] |= value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const pc of ponyColliders) {
|
||||
const tx0 = (baseX0 + pc.x) | 0;
|
||||
const ty0 = (baseY0 + pc.y) | 0;
|
||||
const tx1 = (baseX1 + ((pc.x + pc.w) | 0)) | 0;
|
||||
const ty1 = (baseY1 + ((pc.y + pc.h) | 0)) | 0;
|
||||
|
||||
const x0 = (tx0 < 0 ? 0 : tx0) | 0;
|
||||
const y0 = (ty0 < 0 ? 0 : ty0) | 0;
|
||||
const x1 = (tx1 > REGION_WIDTH ? REGION_WIDTH : tx1) | 0;
|
||||
const y1 = (ty1 > REGION_HEIGHT ? REGION_HEIGHT : ty1) | 0;
|
||||
|
||||
if (x1 > x0 && y1 > y0) {
|
||||
for (let y = y0 | 0; y < y1; y = (y + 1) | 0) {
|
||||
const oy = (y * REGION_WIDTH) | 0;
|
||||
|
||||
for (let x = x0 | 0; x < x1; x = (x + 1) | 0) {
|
||||
regionCollider[(x + oy) | 0] |= value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import { LogArgument } from 'rollbar';
|
||||
import { CHARACTER_LIMIT_ERROR } from './errors';
|
||||
|
||||
const IGNORE = new RegExp([
|
||||
// adware / extensions
|
||||
'plantsandplay', 'anyplacetrivial', 'surfbuyermac', 'hotshoppymac', 'GM_getValue', '__gCrWeb.autofill',
|
||||
'.com/affs', 'advpartners', 'tlscdn', 'yaaknaa', 'mecash', 'digitaloptout',
|
||||
'Script error', 'NS_ERROR_', 'davebestdeals', 'mflcdn', `'feedConf' of null`, 'n46gd0nenr1az.ru',
|
||||
'googst2.ru', 'downloader12.ru', 'adsafeprotected', 'gobobr.info', 'elt.parentNode',
|
||||
`getElementsByTagName('video')`, 'chrome-extension', 'bestpriceninja', `'tgt' of null`,
|
||||
'jh8hrfnvs.ru', 'OperaIce', 'blueblockgames', 'adguard.com', 'kaspersky', 'igamesecrets.com',
|
||||
'Unexpected identifier', 'UnknownError', 'diableNightMode', 'Unexpected end of script',
|
||||
'Internal Server Error', 'hilitor', 'kejnoj7.ru', 'v207.info', 'inj_js',
|
||||
'object is not a function', `'Float32Array' is undefined`, 'vertamedia', '.ru/', 'v24s.net',
|
||||
'window.document.location is null', 'mediaonspot', 'ydpi.pw', 'moz-extension', 'trafficanalytics',
|
||||
'amazonaws', 'adtelligent', 'searchsens.info', 'solid-waste.top', 'cdn.immereeako.info',
|
||||
'technologiecoloniale.com', 'cloudcnfare.com', 'MyAppGet', 'rugged-r.top', `Can't find variable: webkit`,
|
||||
`rgvqcsxqge.com`, 'all_small_polls', `Cannot read property 'document' of undefined`, `extAbbr is not defined`,
|
||||
'__gCrWeb', 'DOMBnbPlug',
|
||||
|
||||
// GPU errors
|
||||
`Failed to execute 'shaderSource'`,
|
||||
'compiling shader',
|
||||
'Failed to create WebGL context',
|
||||
'CONTEXT_LOST_WEBGL',
|
||||
'Framebuffer unsupported',
|
||||
'Framebuffer failed for unspecified reason',
|
||||
'Недостаточно ресурсов памяти для завершения операции.',
|
||||
'Failed to initialize graphics device (Shader error)',
|
||||
'Failed to initialize graphics device (Failed to create WebGL context)',
|
||||
'Failed to initialize graphics device (Failed to create texture)',
|
||||
'Shader error',
|
||||
|
||||
// GPU halt
|
||||
'GPU device instance has been suspended',
|
||||
'Die GPU-Geräteinstanz wurde angehalten',
|
||||
'GPU zostało zawieszone',
|
||||
`GPU приостановлен`,
|
||||
'GPU se ha suspendido',
|
||||
'GPU aygıt örneği askıya alınmış',
|
||||
'GPU-enhetsinstansen har försatts',
|
||||
|
||||
// other
|
||||
'androidInterface is not defined',
|
||||
'/images/',
|
||||
'out of memory',
|
||||
'object is not a function',
|
||||
'Array buffer allocation failed',
|
||||
'Server is offline',
|
||||
'Failed to register a ServiceWorker',
|
||||
'Permission denied to access property',
|
||||
'Not enough storage is available',
|
||||
'Failed to initialize graphics device',
|
||||
'Not enough memory resources',
|
||||
'Ikke nok minneressurser tilgjengelig', // out of memory
|
||||
'suficientes recursos de memoria',
|
||||
'Onvoldoende geheugenbronnen',
|
||||
`Cannot read property 'version' of undefined`,
|
||||
|
||||
'Maximum call stack size exceeded', // howler error on chrome mobile
|
||||
|
||||
// user errors
|
||||
CHARACTER_LIMIT_ERROR,
|
||||
'Too many requests',
|
||||
'Saving in progress',
|
||||
'Too many requests, please try again in',
|
||||
'Already waiting for join request',
|
||||
|
||||
// server
|
||||
'Range Not Satisfiable', 'Precondition Failed',
|
||||
].map(escapeRegExp).join('|'), 'i');
|
||||
|
||||
export interface Person {
|
||||
id: string;
|
||||
username: string;
|
||||
custom?: any;
|
||||
}
|
||||
|
||||
function getLabel(arg: LogArgument | undefined) {
|
||||
if (typeof arg === 'string') {
|
||||
return arg;
|
||||
} else if (arg && 'message' in arg) {
|
||||
return arg.message + (arg.stack || '');
|
||||
} else {
|
||||
return arg ? arg.toString() : '';
|
||||
}
|
||||
}
|
||||
|
||||
export function isIgnoredMessage(message: string) {
|
||||
return IGNORE.test(message);
|
||||
}
|
||||
|
||||
export function isIgnoredError(error: Error) {
|
||||
return isIgnoredMessage(error.message || `${error}` || '') || isIgnoredMessage(error.stack || '');
|
||||
}
|
||||
|
||||
export function rollbarCheckIgnore(_isUncaught: boolean, args: LogArgument[], _payload: object): boolean {
|
||||
return (Array.isArray(args) ? args : [args])
|
||||
.map(getLabel)
|
||||
.some(isIgnoredMessage);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { escapeRegExp, compact, isMatchWith } from 'lodash';
|
||||
import { PonyInfoNumber, PonyInfo } from './interfaces';
|
||||
import { urlRegexTexts, ipRegexText } from './filterUtils';
|
||||
import { AuthBase, GeneralSettings, Suspicious, GameServerSettings } from './adminInterfaces';
|
||||
import { parseColorFast } from './color';
|
||||
|
||||
// suspicious
|
||||
|
||||
export const urlRegex = new RegExp(urlRegexTexts.join('|'), 'ui');
|
||||
export const ipRegex = new RegExp(ipRegexText, 'ui');
|
||||
|
||||
function createRegExpFromList(list: string | undefined, wholeWords = false): RegExp | undefined {
|
||||
const lines = list && compact(list.split(/\r?\n/).map(x => x.trim()));
|
||||
|
||||
if (lines && lines.length) {
|
||||
const combined = lines.map(escapeRegExp).join('|');
|
||||
|
||||
if (wholeWords) {
|
||||
return new RegExp(`\\b(${combined})\\b`, 'ui');
|
||||
} else {
|
||||
return new RegExp(combined, 'ui');
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const createCachedTest = (wholeWords = false) => {
|
||||
let cachedList: string | undefined = undefined;
|
||||
let cachedRegex: RegExp | undefined = undefined;
|
||||
|
||||
return (list: string | undefined, value: string) => {
|
||||
if (cachedList !== list) {
|
||||
cachedList = list;
|
||||
cachedRegex = createRegExpFromList(list, wholeWords);
|
||||
}
|
||||
|
||||
return cachedRegex ? cachedRegex.test(value) : false;
|
||||
};
|
||||
};
|
||||
|
||||
export const createIsSuspiciousMessage = (general: GeneralSettings) => {
|
||||
const test = createCachedTest();
|
||||
const testSafe = createCachedTest();
|
||||
const testWhole = createCachedTest(true);
|
||||
const testSafeInstant = createCachedTest();
|
||||
const testWholeInstant = createCachedTest(true);
|
||||
|
||||
return (text: string, { filterSwears }: GameServerSettings): Suspicious => {
|
||||
if (test(general.suspiciousMessages, text))
|
||||
return Suspicious.Very;
|
||||
|
||||
if (filterSwears) {
|
||||
if (testSafeInstant(general.suspiciousSafeInstantMessages, text) ||
|
||||
testWholeInstant(general.suspiciousSafeInstantWholeMessages, text)) {
|
||||
return Suspicious.Very;
|
||||
}
|
||||
|
||||
if (testSafe(general.suspiciousSafeMessages, text) ||
|
||||
testWhole(general.suspiciousSafeWholeMessages, text)) {
|
||||
return Suspicious.Yes;
|
||||
}
|
||||
}
|
||||
|
||||
return Suspicious.No;
|
||||
};
|
||||
};
|
||||
|
||||
export const createIsSuspiciousName =
|
||||
(settings: GeneralSettings) => {
|
||||
const test = createCachedTest();
|
||||
return (name: string) => test(settings.suspiciousNames, name);
|
||||
};
|
||||
|
||||
export const createIsSuspiciousAuth =
|
||||
(settings: GeneralSettings) => {
|
||||
const test = createCachedTest();
|
||||
return ({ name, emails = [] }: AuthBase<any>) =>
|
||||
test(settings.suspiciousAuths, name) ||
|
||||
emails.some(email => test(settings.suspiciousAuths, email));
|
||||
};
|
||||
|
||||
// pony
|
||||
|
||||
function tryParseJSON(value: string): any {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function createMatchesFromList(list: string | undefined): Partial<PonyInfo>[] {
|
||||
return compact((list || '').split(/\n/g).map(x => x.trim()).map(tryParseJSON));
|
||||
}
|
||||
|
||||
export const createIsSuspiciousPony =
|
||||
(settings: GeneralSettings) =>
|
||||
(info: PonyInfoNumber) => {
|
||||
const matches = createMatchesFromList(settings.suspiciousPonies);
|
||||
return matches.some(match => matchPony(info, match));
|
||||
};
|
||||
|
||||
function matchPony(info: PonyInfoNumber, match: Partial<PonyInfo>) {
|
||||
return isMatchWith(info, match, comparePonyInfoFields);
|
||||
}
|
||||
|
||||
function comparePonyInfoFields(a: any, b: any): boolean {
|
||||
if (typeof a === 'number' && typeof b === 'string') {
|
||||
return a === parseColorFast(b);
|
||||
} else {
|
||||
return undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
// forbidden messages
|
||||
|
||||
export function isForbiddenMessage(_message: string): boolean {
|
||||
// NOTE: uncomment, to filter offensive messages
|
||||
// if (/niggers$/.test(_message) || /faggots?/.test(_message)) return true;
|
||||
|
||||
// NOTE: add more filters here
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// forbidden name
|
||||
|
||||
export function isForbiddenName(_value: string): boolean {
|
||||
// NOTE: uncomment, to filter offensive names
|
||||
// if (/niggers$/.test(_value) || /faggots?/.test(_value) || /hitler/.test(_value)) return true;
|
||||
|
||||
// NOTE: uncomment, to filter links in names
|
||||
// if (ipRegex.test(_value) && !ipExceptionRegex.test(_value)) return true;
|
||||
// if (urlRegex.test(_value) && !urlExceptionRegex.test(_value)) return true;
|
||||
|
||||
// NOTE: add more filters here
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,882 @@
|
||||
import { range, times } from 'lodash';
|
||||
import { PonyInfo, Point, PonyState, DrawPonyOptions, PonyInfoNumber, SpriteSet, PalettePonyInfo, NoDraw } from './interfaces';
|
||||
import * as offsets from './offsets';
|
||||
import { defaultPonyState } from '../client/ponyHelpers';
|
||||
import { WHITE, BLACK, ORANGE, BLUE, CYAN, RED } from './colors';
|
||||
import { createBodyFrame } from '../client/ponyAnimations';
|
||||
import { setFlag, repeat } from './utils';
|
||||
|
||||
type OnFrame = (pony: PonyInfoNumber, state: PonyState, options: DrawPonyOptions, x: number, y: number, pattern: number) => void;
|
||||
|
||||
export interface SheetLayer {
|
||||
name: string;
|
||||
set?: string;
|
||||
setOverride?: string;
|
||||
options?: Partial<DrawPonyOptions>;
|
||||
patterns?: number;
|
||||
drawBlack?: boolean;
|
||||
shiftY?: number;
|
||||
head?: boolean;
|
||||
noFace?: boolean;
|
||||
body?: boolean;
|
||||
frontLeg?: boolean;
|
||||
backLeg?: boolean;
|
||||
frontFarLeg?: boolean;
|
||||
backFarLeg?: boolean;
|
||||
extra?: keyof PalettePonyInfo; // extra field name
|
||||
fieldName?: keyof PonyInfo;
|
||||
setup?: (pony: PonyInfoNumber, state: PonyState) => void;
|
||||
frame?: OnFrame;
|
||||
frameSet?: (set: SpriteSet<number>, x: number, y: number, pattern: number) => void;
|
||||
importMirrored?: { fieldName: string; offsetX: number };
|
||||
}
|
||||
|
||||
export interface Spacer {
|
||||
spacer: true;
|
||||
}
|
||||
|
||||
export interface Sheet {
|
||||
name: string;
|
||||
file?: string;
|
||||
skipImport?: boolean;
|
||||
alert?: string;
|
||||
spacer?: boolean;
|
||||
|
||||
rows?: number;
|
||||
|
||||
width: number;
|
||||
height: number;
|
||||
offset: number;
|
||||
offsetY?: number;
|
||||
|
||||
padLeft?: number;
|
||||
padTop?: number;
|
||||
|
||||
offsets?: Point[];
|
||||
importOffsets?: Point[];
|
||||
fieldName?: keyof PonyInfo;
|
||||
|
||||
groups?: string[][]; // groups for filling-in missing palette colors (used for manes)
|
||||
setsWithEmpties?: string[]; // skipping slots (used for manes)
|
||||
empties?: number[];
|
||||
|
||||
frame?: OnFrame;
|
||||
layers: SheetLayer[];
|
||||
|
||||
masks?: {
|
||||
name: string,
|
||||
layerName: string,
|
||||
mask: string,
|
||||
reverse?: boolean,
|
||||
maskFile?: string;
|
||||
}[];
|
||||
|
||||
state?: PonyState;
|
||||
extra?: boolean;
|
||||
single?: boolean; // single frame (no animation)
|
||||
duplicateFirstFrame?: number;
|
||||
wrap?: number;
|
||||
paletteOffsetY?: number;
|
||||
}
|
||||
|
||||
interface BodyFrame {
|
||||
body: number;
|
||||
front: number;
|
||||
back: number;
|
||||
wing: number;
|
||||
tail: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_COLOR = 0xdec078ff;
|
||||
export const SPECIAL_COLOR = ORANGE;
|
||||
|
||||
const headFrames: BodyFrame[] = [
|
||||
{ body: 1, front: 1, back: 1, wing: 0, tail: 0 },
|
||||
];
|
||||
|
||||
const bodyFrames: BodyFrame[] = [
|
||||
{ body: 0, front: 1, back: 1, wing: 0, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 0, tail: 0 },
|
||||
{ body: 2, front: 29, back: 1, wing: 0, tail: 0 },
|
||||
{ body: 3, front: 30, back: 21, wing: 0, tail: 0 },
|
||||
{ body: 4, front: 31, back: 22, wing: 0, tail: 0 },
|
||||
{ body: 5, front: 32, back: 23, wing: 0, tail: 1 },
|
||||
{ body: 6, front: 33, back: 24, wing: 1, tail: 2 },
|
||||
{ body: 7, front: 34, back: 25, wing: 2, tail: 2 },
|
||||
{ body: 8, front: 34, back: 25, wing: 2, tail: 2 },
|
||||
{ body: 9, front: 34, back: 26, wing: 2, tail: 2 },
|
||||
{ body: 10, front: 35, back: 26, wing: 2, tail: 2 },
|
||||
{ body: 11, front: 36, back: 26, wing: 1, tail: 2 },
|
||||
{ body: 12, front: 37, back: 26, wing: 1, tail: 2 },
|
||||
{ body: 13, front: 38, back: 26, wing: 0, tail: 2 },
|
||||
{ body: 14, front: 38, back: 26, wing: 0, tail: 2 },
|
||||
{ body: 15, front: 38, back: 26, wing: 0, tail: 2 },
|
||||
];
|
||||
|
||||
const waistFrames: BodyFrame[] = [
|
||||
...bodyFrames,
|
||||
{ body: 1, front: 1, back: 1, wing: 3, tail: 0 },
|
||||
];
|
||||
|
||||
const wingFrames: BodyFrame[] = [
|
||||
{ body: 1, front: 1, back: 1, wing: 0, tail: 0 },
|
||||
{ body: 6, front: 33, back: 24, wing: 1, tail: 0 },
|
||||
{ body: 9, front: 34, back: 26, wing: 2, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 3, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 4, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 5, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 6, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 7, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 8, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 9, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 10, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 11, tail: 0 },
|
||||
{ body: 1, front: 1, back: 1, wing: 12, tail: 0 },
|
||||
];
|
||||
|
||||
const exampleCM = [
|
||||
BLUE, BLUE, BLUE, BLUE, BLUE,
|
||||
BLUE, CYAN, CYAN, CYAN, BLUE,
|
||||
BLUE, CYAN, CYAN, CYAN, BLUE,
|
||||
BLUE, CYAN, CYAN, CYAN, BLUE,
|
||||
BLUE, BLUE, BLUE, BLUE, BLUE,
|
||||
];
|
||||
|
||||
const frontLegsCount = 39;
|
||||
const backLegsCount = 27;
|
||||
|
||||
const frontLegsSheet = {
|
||||
width: 55,
|
||||
height: 60,
|
||||
offset: 50,
|
||||
state: state(frontLegsCount, range(0, frontLegsCount)),
|
||||
};
|
||||
|
||||
const backLegsSheet = {
|
||||
width: 55,
|
||||
height: 60,
|
||||
offset: 55,
|
||||
state: state(backLegsCount, undefined, undefined, range(0, backLegsCount)),
|
||||
};
|
||||
|
||||
const bodySheet = {
|
||||
width: 60,
|
||||
height: 60,
|
||||
offset: 50,
|
||||
state: stateFromFrames(bodyFrames),
|
||||
};
|
||||
|
||||
const chestSheet = {
|
||||
width: 60,
|
||||
height: 60,
|
||||
offset: 60,
|
||||
state: stateFromFrames(bodyFrames),
|
||||
};
|
||||
|
||||
const waistSheet = {
|
||||
...chestSheet,
|
||||
state: stateFromFrames(waistFrames),
|
||||
};
|
||||
|
||||
const singleFrameSheet = {
|
||||
...bodySheet,
|
||||
state: stateFromFrames(headFrames),
|
||||
};
|
||||
|
||||
const headSheet = {
|
||||
width: 60,
|
||||
height: 75,
|
||||
offset: 60,
|
||||
offsetY: 20,
|
||||
state: stateFromFrames(headFrames),
|
||||
};
|
||||
|
||||
const bodyLayer: SheetLayer = {
|
||||
name: '<body>', body: true, head: true, frontLeg: true, backLeg: true, frontFarLeg: true, backFarLeg: true,
|
||||
};
|
||||
const muzzleLayer: SheetLayer = { name: '<muzzle>', setup: pony => pony.nose = defaultSet() };
|
||||
const frontLegLayer: SheetLayer = { name: '<front leg>', frontLeg: true, setup: pony => pony.coatFill = SPECIAL_COLOR };
|
||||
const backLegLayer: SheetLayer = { name: '<back leg>', backLeg: true, setup: pony => pony.coatFill = SPECIAL_COLOR };
|
||||
|
||||
export const sheets: (Sheet | Spacer)[] = [
|
||||
// front legs
|
||||
{
|
||||
...frontLegsSheet,
|
||||
name: 'front legs',
|
||||
file: 'front-legs',
|
||||
frame: (_pony, state, _options, _x, y) => {
|
||||
if (y > 0) {
|
||||
state.animation.frames.forEach(f => f.frontLeg = 0);
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
{ ...bodyLayer, frontLeg: false },
|
||||
{
|
||||
name: 'front', set: 'frontLegs', frontLeg: true,
|
||||
frame: (pony, _state, _options, _x, _y, pattern) => {
|
||||
pony.coatFill = pattern === 0 ? RED : WHITE;
|
||||
pony.coatOutline = pattern === 0 ? RED : WHITE;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...frontLegsSheet,
|
||||
name: 'front legs - hooves',
|
||||
file: 'front-legs-hooves',
|
||||
fieldName: 'frontHooves',
|
||||
layers: [
|
||||
{ ...bodyLayer, frontLeg: false },
|
||||
frontLegLayer,
|
||||
{
|
||||
name: 'front', set: 'frontLegHooves', frontLeg: true, options: { useAllHooves: true },
|
||||
setup: pony => pony.coatFill = BLACK
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...frontLegsSheet,
|
||||
name: 'front legs - socks',
|
||||
file: 'front-legs-accessories',
|
||||
fieldName: 'frontLegAccessory',
|
||||
layers: [
|
||||
{ ...bodyLayer, frontLeg: false },
|
||||
frontLegLayer,
|
||||
{ name: 'front', set: 'frontLegAccessories', frontLeg: true, setup: pony => pony.coatFill = BLACK },
|
||||
],
|
||||
},
|
||||
{
|
||||
...frontLegsSheet,
|
||||
name: 'front legs - sleeves',
|
||||
file: 'front-legs-sleeves',
|
||||
fieldName: 'sleeveAccessory',
|
||||
layers: [
|
||||
{ ...bodyLayer, frontLeg: false },
|
||||
frontLegLayer,
|
||||
{
|
||||
name: 'front', set: 'frontLegSleeves', frontLeg: true, options: { no: NoDraw.FarSleeves }, setup: pony => {
|
||||
pony.chestAccessory = ignoreSet(2);
|
||||
pony.coatFill = BLACK;
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// back legs
|
||||
{
|
||||
...backLegsSheet,
|
||||
alert: 'Does not export mask layer',
|
||||
name: 'back legs',
|
||||
file: 'back-legs',
|
||||
masks: [
|
||||
{
|
||||
name: 'backLegs2',
|
||||
layerName: 'front',
|
||||
mask: 'mask',
|
||||
},
|
||||
],
|
||||
frame: (_pony, state, _options, _x, y) => {
|
||||
if (y > 0) {
|
||||
state.animation.frames.forEach(f => f.backLeg = 0);
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
// TODO: mask layer
|
||||
{ ...bodyLayer, backLeg: false },
|
||||
{
|
||||
name: 'front', set: 'backLegs', backLeg: true,
|
||||
frame: (pony, _state, _options, _x, _y, pattern) => {
|
||||
pony.coatFill = pattern === 0 ? RED : WHITE;
|
||||
pony.coatOutline = pattern === 0 ? RED : WHITE;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...backLegsSheet,
|
||||
name: 'back legs - hooves',
|
||||
file: 'back-legs-hooves',
|
||||
fieldName: 'backHooves',
|
||||
masks: [
|
||||
{
|
||||
name: 'backLegHooves2',
|
||||
layerName: 'front',
|
||||
mask: 'mask',
|
||||
maskFile: 'back-legs',
|
||||
},
|
||||
],
|
||||
layers: [
|
||||
{ ...bodyLayer, backLeg: false },
|
||||
backLegLayer,
|
||||
{ name: 'front', set: 'backLegHooves', backLeg: true, setup: pony => pony.coatFill = BLACK },
|
||||
],
|
||||
},
|
||||
{
|
||||
...backLegsSheet,
|
||||
name: 'back legs - socks',
|
||||
file: 'back-legs-accessories',
|
||||
fieldName: 'backLegAccessory',
|
||||
masks: [
|
||||
{
|
||||
name: 'backLegAccessories2',
|
||||
layerName: 'front',
|
||||
mask: 'mask',
|
||||
maskFile: 'back-legs',
|
||||
},
|
||||
],
|
||||
layers: [
|
||||
{ ...bodyLayer, backLeg: false },
|
||||
backLegLayer,
|
||||
{ name: 'front', set: 'backLegAccessories', backLeg: true, setup: pony => pony.coatFill = BLACK },
|
||||
],
|
||||
},
|
||||
{
|
||||
...backLegsSheet,
|
||||
name: 'back legs - sleeves',
|
||||
file: 'back-legs-sleeves',
|
||||
fieldName: 'backAccessory',
|
||||
rows: 2,
|
||||
masks: [
|
||||
{
|
||||
name: 'backLegSleeves2',
|
||||
layerName: 'front',
|
||||
mask: 'mask',
|
||||
maskFile: 'back-legs',
|
||||
},
|
||||
],
|
||||
layers: [
|
||||
{ ...bodyLayer, backLeg: false },
|
||||
backLegLayer,
|
||||
{
|
||||
name: 'front', set: 'backLegSleeves', setOverride: 'backAccessories', patterns: 2,
|
||||
options: { no: NoDraw.BackAccessory | NoDraw.FarSleeves },
|
||||
setup: pony => pony.coatFill = BLACK,
|
||||
frameSet: (set, _x, y, pattern) => {
|
||||
set.type = y === 0 ? 5 : -1;
|
||||
set.pattern = pattern;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'body',
|
||||
file: 'body',
|
||||
// fieldName: 'body', // TODO: uncomment when body set is added
|
||||
frame: (_pony, _state, options, _x, y) => {
|
||||
// fix for missing body set
|
||||
if (y > 0) {
|
||||
options.no = setFlag(options.no, NoDraw.BodyOnly, true);
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
{ name: '<far legs>', frontFarLeg: true, backFarLeg: true },
|
||||
{
|
||||
name: 'body', body: true, set: 'body',
|
||||
frame: (pony, _state, _options, _x, _y, pattern) => {
|
||||
pony.coatFill = pattern === 0 ? RED : WHITE;
|
||||
pony.coatOutline = pattern === 0 ? RED : WHITE;
|
||||
},
|
||||
},
|
||||
{ name: '<front leg>', frontLeg: true },
|
||||
{ name: '<back leg>', backLeg: true },
|
||||
{ name: '<head>', head: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'body - wings',
|
||||
file: 'body-wings',
|
||||
fieldName: 'wings',
|
||||
width: 80,
|
||||
height: 70,
|
||||
offset: 70,
|
||||
offsetY: 10,
|
||||
state: stateFromFrames(wingFrames),
|
||||
layers: [
|
||||
bodyLayer,
|
||||
{ name: 'front', set: 'wings', options: { no: NoDraw.Behind } },
|
||||
],
|
||||
importOffsets: [1, 6, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1].map(i => offsets.wingOffsets[i]),
|
||||
},
|
||||
{
|
||||
name: 'body - tails',
|
||||
file: 'tails',
|
||||
fieldName: 'tail',
|
||||
width: 80,
|
||||
height: 70,
|
||||
offset: 70,
|
||||
padLeft: 20,
|
||||
state: stateFromFrames([1, 5, 9].map(i => bodyFrames[i])),
|
||||
layers: [
|
||||
{ name: 'behind-body', set: 'tails' },
|
||||
bodyLayer,
|
||||
],
|
||||
importOffsets: [1, 5, 9].map(i => offsets.tailOffsets[i]),
|
||||
},
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'body - neck accessory',
|
||||
file: 'neck-accessory',
|
||||
fieldName: 'neckAccessory',
|
||||
layers: [
|
||||
{ ...bodyLayer, head: false },
|
||||
{ name: 'front', set: 'neckAccessories' },
|
||||
{ name: '<head>', head: true },
|
||||
],
|
||||
importOffsets: offsets.neckAccessoryOffsets,
|
||||
},
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'body - chest accessory',
|
||||
file: 'body-chest-accessory',
|
||||
fieldName: 'chestAccessory',
|
||||
layers: [
|
||||
{ name: 'behind', set: 'chestAccessoriesBehind', options: { no: NoDraw.Front } },
|
||||
bodyLayer,
|
||||
{ name: 'front', set: 'chestAccessories', options: { no: NoDraw.Behind } },
|
||||
frontLegLayer,
|
||||
],
|
||||
importOffsets: offsets.chestAccessoryOffsets,
|
||||
},
|
||||
{
|
||||
...chestSheet,
|
||||
name: 'body - back accessory',
|
||||
file: 'body-back-accessory',
|
||||
fieldName: 'backAccessory',
|
||||
// masks: [
|
||||
// { name: 'backAccessories1', layerName: 'front', mask: 'mask' },
|
||||
// { name: 'backAccessories2', layerName: 'front', mask: 'mask', reverse: true },
|
||||
// ],
|
||||
layers: [
|
||||
bodyLayer,
|
||||
{ name: 'front', set: 'backAccessories', options: { no: NoDraw.Sleeves } },
|
||||
],
|
||||
importOffsets: offsets.backAccessoryOffsets,
|
||||
},
|
||||
{
|
||||
...waistSheet,
|
||||
name: 'body - waist accessory',
|
||||
file: 'body-waist-accessory',
|
||||
fieldName: 'waistAccessory',
|
||||
layers: [
|
||||
bodyLayer,
|
||||
{ name: 'front', set: 'waistAccessories' },
|
||||
{
|
||||
name: '<wing>', options: { no: NoDraw.Behind }, frame: (pony, _state, _options, x) => {
|
||||
pony.wings = x === 16 ? specialSet(1) : ignoreSet();
|
||||
},
|
||||
},
|
||||
],
|
||||
importOffsets: offsets.waistAccessoryOffsets,
|
||||
},
|
||||
|
||||
// head
|
||||
{
|
||||
...headSheet,
|
||||
name: 'head',
|
||||
file: 'head',
|
||||
fieldName: 'head',
|
||||
state: stateFromFrames(times(2, i => ({ body: 1, front: 1, back: 1, wing: 0, head: i, tail: 0 }))),
|
||||
layers: [
|
||||
{ ...bodyLayer, options: { no: NoDraw.Head | NoDraw.Eyes | NoDraw.CloseEar | NoDraw.Nose } },
|
||||
{ name: 'front', set: 'head', head: true, drawBlack: false, options: { no: NoDraw.Ears | NoDraw.Nose | NoDraw.Eyes } },
|
||||
{ name: '<face>', head: true, options: { no: NoDraw.Head | NoDraw.FarEar } },
|
||||
],
|
||||
},
|
||||
{
|
||||
...singleFrameSheet,
|
||||
name: 'head - ears',
|
||||
file: 'ears',
|
||||
fieldName: 'ears',
|
||||
single: true,
|
||||
wrap: 8,
|
||||
paletteOffsetY: 30,
|
||||
layers: [
|
||||
{
|
||||
name: 'behind', set: 'earsFar', head: true, noFace: true, drawBlack: false,
|
||||
options: { no: NoDraw.CloseEar | NoDraw.FarEarShade }
|
||||
},
|
||||
{ ...bodyLayer, options: { no: NoDraw.Ears } },
|
||||
{
|
||||
name: 'front', set: 'ears', head: true, noFace: true, drawBlack: false,
|
||||
options: { no: NoDraw.FarEar },
|
||||
// importMirrored: { fieldName: 'ears2', offsetX: 0 },
|
||||
},
|
||||
],
|
||||
// TODO: add <hair> layer(s)
|
||||
},
|
||||
{
|
||||
...headSheet,
|
||||
name: 'head - horns',
|
||||
file: 'horns',
|
||||
fieldName: 'horn',
|
||||
single: true,
|
||||
wrap: 8,
|
||||
layers: [
|
||||
{ name: '<far ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.CloseEar } },
|
||||
{ name: 'behind', set: 'hornsBehind', options: { no: NoDraw.Front } },
|
||||
{ ...bodyLayer, options: { no: NoDraw.Ears } },
|
||||
{
|
||||
...bodyLayer, name: '<body with mane>', options: { no: NoDraw.Ears | NoDraw.FrontMane },
|
||||
setup: pony => {
|
||||
pony.mane = specialSet(1);
|
||||
pony.backMane = specialSet(1);
|
||||
}
|
||||
},
|
||||
{ name: 'front', set: 'horns', options: { no: NoDraw.Behind } },
|
||||
{ name: '<ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } },
|
||||
{
|
||||
name: '<front mane>', head: true, noFace: true, drawBlack: false,
|
||||
options: { no: NoDraw.Ears | NoDraw.Behind | NoDraw.TopMane },
|
||||
setup: pony => pony.mane = specialSet(1),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...headSheet,
|
||||
name: 'head - manes',
|
||||
file: 'manes',
|
||||
fieldName: 'mane',
|
||||
groups: [
|
||||
['frontManes', 'topManes', 'behindManes'],
|
||||
['backFrontManes', 'backBehindManes'],
|
||||
],
|
||||
setsWithEmpties: ['backFrontManes', 'backBehindManes'],
|
||||
empties: [3, 10, 13],
|
||||
single: true,
|
||||
wrap: 8,
|
||||
layers: [
|
||||
{ name: 'behind', set: 'behindManes', options: { no: NoDraw.FrontMane | NoDraw.TopMane } },
|
||||
{ name: 'back-behind', set: 'backBehindManes', fieldName: 'backMane', options: { no: NoDraw.FrontMane } },
|
||||
{ ...bodyLayer, options: { no: NoDraw.CloseEar } },
|
||||
{ name: 'back', set: 'backFrontManes', fieldName: 'backMane', options: { no: NoDraw.Behind } },
|
||||
{ name: 'top', set: 'topManes', options: { no: NoDraw.FrontMane | NoDraw.Behind } },
|
||||
{ name: '<horn>', setup: pony => pony.horn = specialSet(1) },
|
||||
{ name: '<ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } },
|
||||
{ name: 'front', set: 'frontManes', options: { no: NoDraw.TopMane | NoDraw.Behind } },
|
||||
],
|
||||
},
|
||||
{
|
||||
...singleFrameSheet,
|
||||
name: 'head - facial hair',
|
||||
file: 'facial-hair',
|
||||
fieldName: 'facialHair',
|
||||
single: true,
|
||||
wrap: 8,
|
||||
paletteOffsetY: 35,
|
||||
layers: [
|
||||
{ ...bodyLayer, options: { no: NoDraw.Nose } },
|
||||
{ name: 'front', set: 'facialHairBehind' },
|
||||
muzzleLayer,
|
||||
{ name: 'front-2', set: 'facialHair' },
|
||||
],
|
||||
},
|
||||
{
|
||||
...singleFrameSheet,
|
||||
name: 'head - ear accessory',
|
||||
file: 'ear-accessory',
|
||||
fieldName: 'earAccessory',
|
||||
single: true,
|
||||
wrap: 8,
|
||||
layers: [
|
||||
{ name: 'behind', set: 'earAccessoriesBehind', options: { no: NoDraw.Front } },
|
||||
{ ...bodyLayer },
|
||||
{ name: 'front', set: 'earAccessories', options: { no: NoDraw.Behind } },
|
||||
],
|
||||
},
|
||||
{
|
||||
...headSheet,
|
||||
name: 'head - head accessory',
|
||||
file: 'head-accessory',
|
||||
fieldName: 'headAccessory',
|
||||
single: true,
|
||||
wrap: 8,
|
||||
layers: [
|
||||
{ name: '<far ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.CloseEar } },
|
||||
{ name: 'behind', set: 'headAccessoriesBehind' },
|
||||
{ ...bodyLayer, options: { no: NoDraw.FarEar } },
|
||||
{
|
||||
...bodyLayer, name: '<body with mane>', shiftY: 5, options: { no: NoDraw.FarEar }, setup: pony => {
|
||||
pony.mane = specialSet(1);
|
||||
pony.backMane = specialSet(1);
|
||||
}
|
||||
},
|
||||
{ name: 'front', set: 'headAccessories' },
|
||||
],
|
||||
},
|
||||
{
|
||||
...headSheet,
|
||||
name: 'head - face accessory',
|
||||
file: 'face-accessory',
|
||||
fieldName: 'faceAccessory',
|
||||
single: true,
|
||||
extra: true,
|
||||
wrap: 8,
|
||||
layers: [
|
||||
{ ...bodyLayer, options: { no: NoDraw.CloseEar } },
|
||||
{ name: 'front', set: 'faceAccessories', extra: 'faceAccessory', options: { no: NoDraw.FaceAccessory2 } },
|
||||
{ name: '<ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } },
|
||||
{ name: 'front-2', set: 'faceAccessories2', options: { no: NoDraw.FaceAccessory1 } },
|
||||
muzzleLayer,
|
||||
{ name: '<horn>', setup: pony => pony.horn = specialSet(1) },
|
||||
],
|
||||
},
|
||||
{
|
||||
...headSheet,
|
||||
name: 'head - extra accessory',
|
||||
file: 'extra-accessory',
|
||||
fieldName: 'extraAccessory',
|
||||
single: true,
|
||||
wrap: 8,
|
||||
paletteOffsetY: 45,
|
||||
layers: [
|
||||
{
|
||||
name: 'behind', set: 'extraAccessoriesBehind', options: { extra: true, no: NoDraw.Front },
|
||||
setup: pony => pony.mane = ignoreSet(1)
|
||||
},
|
||||
{
|
||||
...bodyLayer, setup: pony => {
|
||||
pony.mane = specialSet(1);
|
||||
pony.backMane = specialSet(1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'front', set: 'extraAccessories', options: { extra: true, no: NoDraw.Behind },
|
||||
setup: pony => pony.mane = ignoreSet(1)
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
spacer: true,
|
||||
},
|
||||
|
||||
// offsets
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'offset - front legs',
|
||||
offsets: offsets.frontLegOffsets,
|
||||
state: stateFromFrames(bodyFrames.map(f => ({ ...f, front: 1 }))),
|
||||
layers: [bodyLayer],
|
||||
},
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'offset - back legs',
|
||||
offsets: offsets.backLegOffsets,
|
||||
state: stateFromFrames(bodyFrames.map(f => ({ ...f, back: 1 }))),
|
||||
layers: [bodyLayer],
|
||||
},
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'offset - wings',
|
||||
fieldName: 'wings',
|
||||
offsets: offsets.wingOffsets,
|
||||
layers: [
|
||||
bodyLayer,
|
||||
{ name: 'front', set: 'wings', options: { no: NoDraw.Behind } },
|
||||
],
|
||||
duplicateFirstFrame: bodyFrames.length,
|
||||
},
|
||||
{
|
||||
width: 80,
|
||||
height: 70,
|
||||
offset: 70,
|
||||
state: stateFromFrames(bodyFrames),
|
||||
name: 'offset - tails',
|
||||
fieldName: 'tail',
|
||||
offsets: offsets.tailOffsets,
|
||||
layers: [
|
||||
{ name: 'behindBody', set: 'tails' },
|
||||
bodyLayer,
|
||||
],
|
||||
duplicateFirstFrame: bodyFrames.length,
|
||||
},
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'offset - head',
|
||||
offsets: offsets.headOffsets,
|
||||
layers: [bodyLayer],
|
||||
},
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'offset - cm',
|
||||
offsets: offsets.cmOffsets,
|
||||
layers: [
|
||||
{ ...bodyLayer, setup: pony => pony.cm = exampleCM },
|
||||
],
|
||||
},
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'offset - neck accessory',
|
||||
fieldName: 'neckAccessory',
|
||||
offsets: offsets.neckAccessoryOffsets,
|
||||
layers: [
|
||||
{ ...bodyLayer, head: false },
|
||||
{ name: 'front', set: 'neckAccessories' },
|
||||
{ name: '<head>', head: true },
|
||||
],
|
||||
importOffsets: offsets.neckAccessoryOffsets,
|
||||
},
|
||||
{
|
||||
...bodySheet,
|
||||
name: 'offset - chest accessory',
|
||||
fieldName: 'chestAccessory',
|
||||
offsets: offsets.chestAccessoryOffsets,
|
||||
layers: [
|
||||
{ name: 'behind', set: 'chestAccessoriesBehind', options: { no: NoDraw.Front } },
|
||||
bodyLayer,
|
||||
{ name: 'front', set: 'chestAccessories', options: { no: NoDraw.Behind } },
|
||||
],
|
||||
},
|
||||
{
|
||||
...waistSheet,
|
||||
name: 'offset - waist accessory',
|
||||
fieldName: 'waistAccessory',
|
||||
offsets: offsets.waistAccessoryOffsets,
|
||||
layers: [
|
||||
bodyLayer,
|
||||
{ name: 'front', set: 'waistAccessories' },
|
||||
{
|
||||
name: '<wing>', options: { no: NoDraw.Behind }, frame: (pony, _state, _options, x) => {
|
||||
pony.wings = x === 16 ? specialSet(1) : ignoreSet();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...chestSheet,
|
||||
name: 'offset - back accessory',
|
||||
fieldName: 'backAccessory',
|
||||
offsets: offsets.backAccessoryOffsets,
|
||||
layers: [
|
||||
{ name: '<tail>', setup: pony => pony.tail = specialSet(2) },
|
||||
bodyLayer,
|
||||
{ name: 'front', set: 'backAccessories' },
|
||||
],
|
||||
},
|
||||
{
|
||||
width: 55,
|
||||
height: 40,
|
||||
offset: 55,
|
||||
offsetY: 10,
|
||||
state: stateFromFrames(repeat(offsets.HEAD_ACCESSORY_OFFSETS.length, bodyFrames[1])),
|
||||
name: 'offset - hats',
|
||||
rows: 19,
|
||||
offsets: offsets.HEAD_ACCESSORY_OFFSETS,
|
||||
layers: [
|
||||
{
|
||||
...bodyLayer,
|
||||
frame: (pony, _state, _options, x, y) => {
|
||||
pony.mane = specialSet(x);
|
||||
// pony.backMane = specialSet(x === 0 ? 0 : 1);
|
||||
pony.headAccessory = whiteSet(y + 1);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
width: 55,
|
||||
height: 40,
|
||||
offset: 55,
|
||||
offsetY: 10,
|
||||
state: stateFromFrames(repeat(offsets.EAR_ACCESSORY_OFFSETS.length, bodyFrames[1])),
|
||||
name: 'offset - earrings',
|
||||
rows: 13,
|
||||
offsets: offsets.EAR_ACCESSORY_OFFSETS,
|
||||
layers: [
|
||||
{
|
||||
...bodyLayer,
|
||||
frame: (pony, _state, _options, x, y) => {
|
||||
pony.ears = defaultSet(x);
|
||||
pony.earAccessory = whiteSet(y + 1);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
width: 55,
|
||||
height: 40,
|
||||
offset: 55,
|
||||
offsetY: 10,
|
||||
state: stateFromFrames(repeat(offsets.EXTRA_ACCESSORY_OFFSETS.length, bodyFrames[1])),
|
||||
name: 'offset - extra',
|
||||
rows: 17,
|
||||
offsets: offsets.EXTRA_ACCESSORY_OFFSETS,
|
||||
layers: [
|
||||
{
|
||||
...bodyLayer,
|
||||
frame: (pony, _state, options, x, y) => {
|
||||
pony.mane = specialSet(x);
|
||||
pony.extraAccessory = createSet(y + 1, WHITE, 7);
|
||||
options.extra = true;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function stateFromFrames(frames: BodyFrame[]) {
|
||||
const front = frames.map(f => f.front);
|
||||
const back = frames.map(f => f.back);
|
||||
const body = frames.map(f => f.body);
|
||||
const wing = frames.map(f => f.wing);
|
||||
const tail = frames.map(f => f.tail);
|
||||
return state(frames.length, front, front, back, back, undefined, body, wing, tail);
|
||||
}
|
||||
|
||||
function state(
|
||||
frames: number, frontLegs?: number[], frontFarLegs?: number[], backLegs?: number[], backFarLegs?: number[],
|
||||
head?: number[], body?: number[], wing?: (number | undefined)[], tail?: number[]
|
||||
): PonyState {
|
||||
const state = defaultPonyState();
|
||||
state.blushColor = 0;
|
||||
state.animation = {} as any;
|
||||
const ones = times(frames, () => 1);
|
||||
const zeros = times(frames, () => 0);
|
||||
|
||||
state.animation = {
|
||||
name: '',
|
||||
loop: false,
|
||||
fps: 24,
|
||||
frames: times(frames, i => ({
|
||||
...createBodyFrame([]),
|
||||
head: (head || ones)[i],
|
||||
body: (body || ones)[i],
|
||||
wing: (wing && wing[i]) || 0,
|
||||
tail: (tail || zeros)[i],
|
||||
frontLeg: (frontLegs || ones)[i],
|
||||
frontFarLeg: (frontFarLegs || ones)[i],
|
||||
backLeg: (backLegs || ones)[i],
|
||||
backFarLeg: (backFarLegs || ones)[i],
|
||||
})),
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export function ignoreSet(type = 0): SpriteSet<number> {
|
||||
return createSet(type, BLACK);
|
||||
}
|
||||
|
||||
function defaultSet(type = 0): SpriteSet<number> {
|
||||
return createSet(type, DEFAULT_COLOR);
|
||||
}
|
||||
|
||||
function specialSet(type = 0): SpriteSet<number> {
|
||||
return createSet(type, SPECIAL_COLOR);
|
||||
}
|
||||
|
||||
function whiteSet(type = 0): SpriteSet<number> {
|
||||
return createSet(type, WHITE);
|
||||
}
|
||||
|
||||
function createSet(type: number, color: number, count = 2): SpriteSet<number> {
|
||||
return {
|
||||
type,
|
||||
fills: times(count, () => color),
|
||||
lockFills: times(count, () => false),
|
||||
outlines: times(count, () => color),
|
||||
lockOutlines: times(count, () => true),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
const lowercaseCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789_';
|
||||
const uppercaseCharacters = lowercaseCharacters + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const CARRIAGERETURN = '\r'.charCodeAt(0);
|
||||
|
||||
export function randomString(length: number, useUpperCase = false): string {
|
||||
const characters = useUpperCase ? uppercaseCharacters : lowercaseCharacters;
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters[(Math.random() * characters.length) | 0];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isSurrogate(code: number): boolean {
|
||||
return code >= 0xd800 && code <= 0xdbff;
|
||||
}
|
||||
|
||||
export function isLowSurrogate(code: number): boolean {
|
||||
return (code & 0xfc00) === 0xdc00;
|
||||
}
|
||||
|
||||
export function fromSurrogate(high: number, low: number): number {
|
||||
return (((high & 0x3ff) << 10) + (low & 0x3ff) + 0x10000) | 0;
|
||||
}
|
||||
|
||||
export function charsToCodes(text: string) {
|
||||
const chars: number[] = [];
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
let code = text.charCodeAt(i);
|
||||
|
||||
if (isSurrogate(code) && (i + 1) < text.length) {
|
||||
const extra = text.charCodeAt(i + 1);
|
||||
|
||||
if (isLowSurrogate(extra)) {
|
||||
code = fromSurrogate(code, extra);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
chars.push(code);
|
||||
}
|
||||
|
||||
return chars;
|
||||
}
|
||||
|
||||
export function stringToCodes(buffer: Uint32Array, text: string): number {
|
||||
const textLength = text.length | 0;
|
||||
let length = 0 | 0;
|
||||
|
||||
for (let i = 0; i < textLength; i = (i + 1) | 0) {
|
||||
let code = text.charCodeAt(i) | 0;
|
||||
|
||||
if (isSurrogate(code) && ((i + 1) | 0) < textLength) {
|
||||
const extra = text.charCodeAt(i + 1) | 0;
|
||||
|
||||
if (isLowSurrogate(extra)) {
|
||||
code = fromSurrogate(code, extra) | 0;
|
||||
i = (i + 1) | 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (isVisibleChar(code)) {
|
||||
buffer[length] = code;
|
||||
length = (length + 1) | 0;
|
||||
}
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
export let codesBuffer = new Uint32Array(32);
|
||||
|
||||
export function stringToCodesTemp(text: string) {
|
||||
while (text.length > codesBuffer.length) {
|
||||
codesBuffer = new Uint32Array(codesBuffer.length * 2);
|
||||
}
|
||||
|
||||
return stringToCodes(codesBuffer, text);
|
||||
}
|
||||
|
||||
export function matcher(regex: RegExp) {
|
||||
return (text: string): boolean => !!text && regex.test(text);
|
||||
}
|
||||
|
||||
export function isVisibleChar(code: number) {
|
||||
return code !== CARRIAGERETURN && !(code >= 0xfe00 && code <= 0xfe0f);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
import { CharacterTag, FontPalettes } from './interfaces';
|
||||
import { hasRole, AccountRoles } from './accountUtils';
|
||||
import { MOD_COLOR, ADMIN_COLOR, PATREON_COLOR, ANNOUNCEMENT_COLOR, WHITE } from './colors';
|
||||
|
||||
const placeholder = { id: '', tagClass: '', label: '' };
|
||||
const tags: { [key: string]: CharacterTag; } = {
|
||||
'mod': { ...placeholder, name: 'moderator', className: 'mod', color: MOD_COLOR },
|
||||
'dev': { ...placeholder, name: 'developer', className: 'dev', color: ADMIN_COLOR },
|
||||
'dev:art': { ...placeholder, name: 'dev artist', className: 'dev', color: ADMIN_COLOR },
|
||||
'dev:music': { ...placeholder, name: 'dev musician', className: 'dev', color: ADMIN_COLOR },
|
||||
'sup1': { ...placeholder, name: 'supporter', className: 'sup1', color: PATREON_COLOR },
|
||||
'sup2': { ...placeholder, name: 'supporter', className: 'sup2', color: WHITE },
|
||||
'sup3': { ...placeholder, name: 'supporter', className: 'sup3', color: WHITE },
|
||||
'hidden': { ...placeholder, name: 'hidden', className: 'hidden', color: ANNOUNCEMENT_COLOR },
|
||||
};
|
||||
|
||||
Object.keys(tags).forEach(id => {
|
||||
const tag = tags[id];
|
||||
tag.id = id;
|
||||
tag.label = `<${tag.name.toUpperCase()}>`;
|
||||
tag.tagClass = `tag-${tag.className}`;
|
||||
});
|
||||
|
||||
export const emptyTag: CharacterTag = { id: '', name: 'no tag', label: '', className: '', tagClass: '', color: 0 };
|
||||
|
||||
export function getAllTags() {
|
||||
return Object.keys(tags).map(key => tags[key]);
|
||||
}
|
||||
|
||||
export function getTag(id: string | undefined): CharacterTag | undefined {
|
||||
return id ? tags[id] : undefined;
|
||||
}
|
||||
|
||||
export function getTagPalette(tag: CharacterTag, palettes: FontPalettes) {
|
||||
switch (tag.id) {
|
||||
case 'sup2': return palettes.supporter2;
|
||||
case 'sup3': return palettes.supporter3;
|
||||
default: return palettes.white;
|
||||
}
|
||||
}
|
||||
|
||||
export function canUseTag(account: AccountRoles, tag: string) {
|
||||
if (tag === 'mod') {
|
||||
return hasRole(account, 'mod');
|
||||
} else if (tag === 'dev' || /^dev:/.test(tag)) {
|
||||
return hasRole(account, 'dev');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAvailableTags(account: AccountRoles): CharacterTag[] {
|
||||
return getAllTags().filter(tag => canUseTag(account, tag.id));
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { lerpColors, withAlphaFloat } from './color';
|
||||
import { WHITE, SHADOW_COLOR, BLACK } from './colors';
|
||||
import { MINUTE } from './constants';
|
||||
import { Season } from './interfaces';
|
||||
|
||||
const DAY_START = 4.75; // 04:45
|
||||
const DAY_END = 20.25; // 20:15
|
||||
const SUN_EASE = 1.5; // 01:30
|
||||
const SUN_HALF = SUN_EASE / 2;
|
||||
const SUN_GAP = SUN_EASE / 4;
|
||||
|
||||
export const HOUR_LENGTH = 2 * MINUTE; // 48 min -> 24 hours
|
||||
export const DAY_LENGTH = HOUR_LENGTH * 24;
|
||||
|
||||
const getTimeOfDay = (time: number) => time % DAY_LENGTH;
|
||||
const getHourOfDay = (timeOfDay: number) => timeOfDay * 24 / DAY_LENGTH;
|
||||
|
||||
export function getHour(time: number) {
|
||||
const timeOfDay = getTimeOfDay(time);
|
||||
const hourOfDay = getHourOfDay(timeOfDay);
|
||||
return hourOfDay;
|
||||
}
|
||||
|
||||
export function formatHourMinutes(time: number): string {
|
||||
const timeOfDay = getTimeOfDay(time);
|
||||
const minutesInDay = 60 * 24;
|
||||
const totalMinutes = Math.floor(timeOfDay * minutesInDay / DAY_LENGTH);
|
||||
const minutes = totalMinutes % 60;
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const isHour = (test: (hour: number) => boolean) => (time: number) => {
|
||||
return test(getHour(time));
|
||||
};
|
||||
|
||||
export const isDay = isHour(hour => hour > DAY_START && hour <= DAY_END);
|
||||
export const isNight = (time: number) => !isDay(time);
|
||||
|
||||
export const isFullDay = isHour(hour => hour > (DAY_START + SUN_HALF) && hour <= (DAY_END - SUN_HALF));
|
||||
export const isFullNight = isHour(hour => hour < (DAY_START - SUN_HALF) || hour >= (DAY_END + SUN_HALF));
|
||||
|
||||
export const isSunRaising = isHour(hour => hour > (DAY_START - SUN_HALF) && hour <= (DAY_START + SUN_HALF));
|
||||
export const isSunSetting = isHour(hour => hour > (DAY_END - SUN_HALF) && hour <= (DAY_END + SUN_HALF));
|
||||
|
||||
export const isDayTime = isHour(hour => hour > DAY_START && hour < (DAY_END - SUN_HALF));
|
||||
export const isNightTime = isHour(hour => hour < (DAY_START - SUN_HALF) || hour > DAY_END);
|
||||
|
||||
// light color
|
||||
|
||||
export interface LightData {
|
||||
lightColors: number[];
|
||||
shadowColors: number[];
|
||||
lightStops: number[];
|
||||
}
|
||||
|
||||
export function createLightData(season: Season): LightData {
|
||||
const lightDay = WHITE;
|
||||
const lightNight = season === Season.Winter ? 0x253f76ff : 0x2b3374ff;
|
||||
|
||||
const sunrise1 = 0x853d7dff;
|
||||
const sunrise2 = 0xc96161ff;
|
||||
const sunrise3 = 0xeeb7a0ff;
|
||||
|
||||
const sunset1 = sunrise3;
|
||||
const sunset2 = sunrise2;
|
||||
const sunset3 = sunrise1;
|
||||
|
||||
const shadowAlphaMultiplier = season === Season.Winter ? 0.7 : 1;
|
||||
const shadowDay = withAlphaFloat(BLACK, 0.3 * shadowAlphaMultiplier);
|
||||
const shadowNight = withAlphaFloat(BLACK, 0.2 * shadowAlphaMultiplier);
|
||||
const shadowSunset = withAlphaFloat(BLACK, 0.25 * shadowAlphaMultiplier);
|
||||
const shadowSunrise = withAlphaFloat(BLACK, 0.25 * shadowAlphaMultiplier);
|
||||
|
||||
const lightPoints = [
|
||||
// night
|
||||
{ time: 0, light: lightNight, shadow: shadowNight },
|
||||
|
||||
// transition to day
|
||||
{ time: DAY_START - SUN_HALF, light: lightNight, shadow: shadowNight },
|
||||
{ time: DAY_START - SUN_HALF + SUN_GAP, light: sunrise1, shadow: shadowSunrise },
|
||||
{ time: DAY_START - SUN_HALF + SUN_GAP * 2, light: sunrise2, shadow: shadowSunrise },
|
||||
{ time: DAY_START - SUN_HALF + SUN_GAP * 3, light: sunrise3, shadow: shadowSunrise },
|
||||
{ time: DAY_START + SUN_HALF, light: lightDay, shadow: shadowDay },
|
||||
|
||||
// transition to night
|
||||
{ time: DAY_END - SUN_HALF, light: lightDay, shadow: shadowDay },
|
||||
{ time: DAY_END - SUN_HALF + SUN_GAP, light: sunset1, shadow: shadowSunset },
|
||||
{ time: DAY_END - SUN_HALF + SUN_GAP * 2, light: sunset2, shadow: shadowSunset },
|
||||
{ time: DAY_END - SUN_HALF + SUN_GAP * 3, light: sunset3, shadow: shadowSunset },
|
||||
{ time: DAY_END + SUN_HALF, light: lightNight, shadow: shadowNight },
|
||||
|
||||
// night
|
||||
{ time: 24, light: lightNight, shadow: shadowNight },
|
||||
];
|
||||
|
||||
const lightColors = lightPoints.map(l => l.light);
|
||||
const shadowColors = lightPoints.map(l => l.shadow);
|
||||
const lightStops = lightPoints.map(l => l.time);
|
||||
|
||||
return { lightColors, shadowColors, lightStops };
|
||||
}
|
||||
|
||||
export function getLightColor(data: LightData, time: number): number {
|
||||
return getColorForTime(time, data.lightStops, data.lightColors, WHITE);
|
||||
}
|
||||
|
||||
export function getShadowColor(data: LightData, time: number): number {
|
||||
return getColorForTime(time, data.lightStops, data.shadowColors, SHADOW_COLOR);
|
||||
}
|
||||
|
||||
function getColorForTime(time: number, stops: number[], colors: number[], defaultColor: number) {
|
||||
const timeOfDay = getTimeOfDay(time);
|
||||
const hourOfDay = getHourOfDay(timeOfDay);
|
||||
|
||||
for (let i = 1; i < stops.length; i++) {
|
||||
if (stops[i] >= hourOfDay) {
|
||||
const from = stops[i - 1];
|
||||
const to = stops[i];
|
||||
const fromLight = colors[i - 1];
|
||||
const toLight = colors[i];
|
||||
return lerpColors(fromLight, toLight, (hourOfDay - from) / (to - from));
|
||||
}
|
||||
}
|
||||
|
||||
return defaultColor;
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Point, Rect, Entity, Dict } from './interfaces';
|
||||
import { tileWidth, tileHeight, SECOND, MINUTE, HOUR, DAY } from './constants';
|
||||
import { ACCESS_ERROR, NOT_FOUND_ERROR, OFFLINE_ERROR, PROTECTION_ERROR } from './errors';
|
||||
|
||||
// enum
|
||||
|
||||
export function invalidEnum(value: never) {
|
||||
if (DEVELOPMENT) {
|
||||
throw new Error(`Invalid enum value: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function invalidEnumReturn<T>(value: never, ret: T): T {
|
||||
if (DEVELOPMENT && !TESTS) {
|
||||
throw new Error(`Invalid enum value: ${value}`);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// date
|
||||
|
||||
export function fromDate(date: Date, duration: number): Date {
|
||||
date.setTime(date.getTime() + duration);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function fromNow(duration: number): Date {
|
||||
return fromDate(new Date(), duration);
|
||||
}
|
||||
|
||||
export function compareDates(a?: Date, b?: Date) {
|
||||
return a ? (b ? a.getTime() - b.getTime() : 1) : (b ? -1 : 0);
|
||||
}
|
||||
|
||||
export function maxDate(a?: Date, b?: Date) {
|
||||
return (compareDates(a, b) > 0 ? a : b) || a || b;
|
||||
}
|
||||
|
||||
export function minDate(a?: Date, b?: Date) {
|
||||
return (compareDates(a, b) < 0 ? a : b) || a || b;
|
||||
}
|
||||
|
||||
export function formatDuration(duration: number) {
|
||||
const s = Math.floor(duration / SECOND) % 60;
|
||||
const m = Math.floor(duration / MINUTE) % 60;
|
||||
const h = Math.floor(duration / HOUR) % 24;
|
||||
const d = Math.floor(duration / DAY);
|
||||
|
||||
if (d > 0) {
|
||||
return h ? `${d}d ${h}h` : `${d}d`;
|
||||
} else if (h > 0) {
|
||||
return m ? `${h}h ${m}m` : `${h}h`;
|
||||
} else if (m > 0) {
|
||||
return s ? `${m}m ${s}s` : `${m}m`;
|
||||
} else {
|
||||
return `${s}s`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatISODate(date: Date) {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function parseISODate(value: string) {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||
let day = 0;
|
||||
let month = 0;
|
||||
let year = 0;
|
||||
|
||||
if (match) {
|
||||
year = parseInt(match[1], 10);
|
||||
month = parseInt(match[2], 10);
|
||||
day = parseInt(match[3], 10);
|
||||
}
|
||||
|
||||
return { day, month, year };
|
||||
}
|
||||
|
||||
export function createValidBirthDate(day: number, month: number, year: number) {
|
||||
const date = new Date(0);
|
||||
const currentYear = (new Date()).getFullYear();
|
||||
date.setFullYear(year, month - 1, day);
|
||||
|
||||
if (
|
||||
date.getFullYear() === year && date.getMonth() === (month - 1) && date.getDate() === day &&
|
||||
year >= (currentYear - 120) && year < currentYear
|
||||
) {
|
||||
return date;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// color
|
||||
|
||||
export function parseSpriteColor(str: string): number {
|
||||
return str === '0' ? 0 : (str.length === 6 ? (((parseInt(str, 16) << 8) | 0xff) >>> 0) : (parseInt(str, 16) >>> 0));
|
||||
}
|
||||
|
||||
// numbers
|
||||
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
return value > min ? (value < max ? value : max) : min;
|
||||
}
|
||||
|
||||
export function lerp(a: number, b: number, t: number) {
|
||||
return a + t * (b - a);
|
||||
}
|
||||
|
||||
export function normalize(x: number, y: number): Point {
|
||||
const d = Math.sqrt(x * x + y * y);
|
||||
return { x: x / d, y: y / d };
|
||||
}
|
||||
|
||||
export function computeCRC(colors: Uint32Array): number {
|
||||
let crc = 0;
|
||||
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
crc ^= colors[i];
|
||||
|
||||
for (let j = 0; j < 8; j++) {
|
||||
crc = (crc & 1) ? ((crc >>> 1) ^ 0x82f63b78) : (crc >>> 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc >>> 0;
|
||||
}
|
||||
|
||||
export function computeFriendsCRC(friends: string[]) {
|
||||
if (!friends.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
friends.sort();
|
||||
const data = new Uint32Array(friends.length * 3);
|
||||
|
||||
for (let i = 0; i < friends.length; i++) {
|
||||
const id = friends[i];
|
||||
data[i * 3] = parseInt(id.substr(0, 8), 16);
|
||||
data[i * 3 + 1] = parseInt(id.substr(8, 8), 16);
|
||||
data[i * 3 + 2] = parseInt(id.substr(16, 8), 16);
|
||||
}
|
||||
|
||||
return computeCRC(data);
|
||||
}
|
||||
|
||||
export function lerpColor(a: number[] | Float32Array, b: number[] | Float32Array, t: number) {
|
||||
a[0] = t * b[0] + (1 - t) * a[0];
|
||||
a[1] = t * b[1] + (1 - t) * a[1];
|
||||
a[2] = t * b[2] + (1 - t) * a[2];
|
||||
a[3] = t * b[3] + (1 - t) * a[3];
|
||||
}
|
||||
|
||||
// common
|
||||
|
||||
export function toInt(value: any): number {
|
||||
return value | 0;
|
||||
}
|
||||
|
||||
export function dispose<T extends { dispose(): void; }>(obj: T | undefined): undefined {
|
||||
obj && obj.dispose();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function cloneDeep<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
// enums
|
||||
|
||||
export function hasFlag(value: number | undefined, flag: number): boolean {
|
||||
return (value! & flag) === flag;
|
||||
}
|
||||
|
||||
export function setFlag(value: number | undefined, flag: number, on: boolean): number {
|
||||
return (value! & ~flag) | (on ? flag : 0);
|
||||
}
|
||||
|
||||
export function flagsToString(value: number, flags: { value: number; name: string; }[], none = 'None') {
|
||||
return flags
|
||||
.filter(flag => hasFlag(value, flag.value))
|
||||
.map(flag => flag.name).join(' | ') || none;
|
||||
}
|
||||
|
||||
// collections
|
||||
|
||||
export function includes<T>(array: T[] | undefined, item: T): boolean {
|
||||
return array !== undefined && array.indexOf(item) !== -1;
|
||||
}
|
||||
|
||||
export function array<T>(size: number, defaultValue: T) {
|
||||
const result: T[] = [];
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
result.push(defaultValue);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function repeat<T>(count: number, ...values: T[]): T[] {
|
||||
const result: T[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(...values);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function times<T>(count: number, action: (index: number) => T) {
|
||||
const result: T[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(action(i));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function last<T>(array: T[]): T | undefined {
|
||||
return array.length > 0 ? array[array.length - 1] : undefined;
|
||||
}
|
||||
|
||||
export function flatten<T>(arrays: T[][]): T[] {
|
||||
return ([] as T[]).concat(...arrays);
|
||||
}
|
||||
|
||||
export function at<T>(items: T[], index: any): T | undefined {
|
||||
return items[clamp(index | 0, 0, items.length - 1)];
|
||||
}
|
||||
|
||||
export function att<T>(items: T[] | null | undefined, index: any): T | undefined {
|
||||
return items ? items[clamp(index | 0, 0, items.length - 1)] : undefined;
|
||||
}
|
||||
|
||||
export function findById<U, T extends { id: U }>(items: T[], id: U): T | undefined {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].id === id) {
|
||||
return items[i];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function findIndexById<U, T extends { id: U }>(items: T[], id: U): number {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].id === id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function removeItem<T>(items: T[], item: T): boolean {
|
||||
const index = items.indexOf(item);
|
||||
|
||||
if (index !== -1) {
|
||||
items.splice(index, 1);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeItemFast<T>(items: T[], item: T): boolean {
|
||||
const index = items.indexOf(item);
|
||||
|
||||
if (index !== -1) {
|
||||
items[index] = items[items.length - 1];
|
||||
items.pop();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeById<U, T extends { id: U }>(items: T[], id: U): T | undefined {
|
||||
const index = findIndexById(items, id);
|
||||
|
||||
if (index !== -1) {
|
||||
const item = items[index];
|
||||
items.splice(index, 1);
|
||||
return item;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function arraysEqual<T>(a: T[], b: T[]): boolean {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function pushUniq<T>(array: T[], item: T) {
|
||||
const index = array.indexOf(item);
|
||||
|
||||
if (index === -1) {
|
||||
array.push(item);
|
||||
return array.length;
|
||||
} else {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function createPlainMap<T>(values: Dict<T>): Dict<T> {
|
||||
return Object.keys(values).reduce((obj: Dict<T>, key: string) => (obj[key] = values[key], obj), Object.create(null));
|
||||
}
|
||||
|
||||
// rects / points
|
||||
|
||||
export function point(x: number, y: number): Point {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
export function contains(x: number, y: number, bounds: Rect, point: Point): boolean {
|
||||
const bx = bounds.x / tileWidth + x;
|
||||
const by = bounds.y / tileHeight + y;
|
||||
const bw = bounds.w / tileWidth;
|
||||
const bh = bounds.h / tileHeight;
|
||||
return point.x > bx && point.x < bx + bw && point.y > by && point.y < by + bh;
|
||||
}
|
||||
|
||||
export function containsPoint(dx: number, dy: number, rect: Rect, px: number, py: number): boolean {
|
||||
return pointInXYWH(px, py, rect.x + dx, rect.y + dy, rect.w, rect.h);
|
||||
}
|
||||
|
||||
export function containsPointWitBorder(dx: number, dy: number, rect: Rect, px: number, py: number, border: number): boolean {
|
||||
return pointInXYWH(px, py, rect.x + dx - border, rect.y + dy - border, rect.w + border * 2, rect.h + border * 2);
|
||||
}
|
||||
|
||||
export function pointInRect(x: number, y: number, rect: Rect) {
|
||||
return x > rect.x && x < rect.x + rect.w && y > rect.y && y < rect.y + rect.h;
|
||||
}
|
||||
|
||||
export function pointInXYWH(px: number, py: number, rx: number, ry: number, rw: number, rh: number) {
|
||||
return px > rx && px < rx + rw && py > ry && py < ry + rh;
|
||||
}
|
||||
|
||||
export function randomPoint({ x, y, w, h }: Rect): Point {
|
||||
return {
|
||||
x: x + w * Math.random(),
|
||||
y: y + h * Math.random(),
|
||||
};
|
||||
}
|
||||
|
||||
export function lengthOfXY(dx: number, dy: number): number {
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
export function distanceXY(ax: number, ay: number, bx: number, by: number): number {
|
||||
return lengthOfXY(ax - bx, ay - by);
|
||||
}
|
||||
|
||||
export function distanceSquaredXY(ax: number, ay: number, bx: number, by: number): number {
|
||||
const dx = ax - bx;
|
||||
const dy = ay - by;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
export function distance(a: Point, b: Point): number {
|
||||
return distanceXY(a.x, a.y, b.x, b.y);
|
||||
}
|
||||
|
||||
export function entitiesIntersect(a: Entity, b: Entity): boolean {
|
||||
const aBounds = a.bounds;
|
||||
const bBounds = b.bounds;
|
||||
|
||||
if (!aBounds || !bBounds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ax = a.x * tileWidth + aBounds.x;
|
||||
const ay = a.y * tileHeight + aBounds.y;
|
||||
const bx = b.x * tileWidth + bBounds.x;
|
||||
const by = b.y * tileHeight + bBounds.y;
|
||||
|
||||
return intersect(ax, ay, aBounds.w, aBounds.h, bx, by, bBounds.w, bBounds.h);
|
||||
}
|
||||
|
||||
export function collidersIntersect(ax: number, ay: number, a: Rect, bx: number, by: number, b: Rect): boolean {
|
||||
const axmin = Math.floor((ax + a.x) * tileWidth) | 0;
|
||||
const axmax = Math.ceil((ax + a.x + a.w) * tileWidth) | 0;
|
||||
const aymin = Math.floor((ay + a.y) * tileHeight) | 0;
|
||||
const aymax = Math.ceil((ay + a.y + a.h) * tileHeight) | 0;
|
||||
|
||||
const bxmin = Math.floor((bx + b.x) * tileWidth) | 0;
|
||||
const bxmax = Math.ceil((bx + b.x + b.w) * tileWidth) | 0;
|
||||
const bymin = Math.floor((by + b.y) * tileHeight) | 0;
|
||||
const bymax = Math.ceil((by + b.y + b.h) * tileHeight) | 0;
|
||||
|
||||
return axmin < bxmax && axmax > bxmin && aymin < bymax && aymax > bymin;
|
||||
}
|
||||
|
||||
export function boundsIntersect(
|
||||
ax: number, ay: number, a: Rect | undefined, bx: number, by: number, b: Rect | undefined
|
||||
): boolean {
|
||||
return !!(a && b && intersect(
|
||||
ax * tileWidth + a.x, ay * tileHeight + a.y, a.w, a.h,
|
||||
bx * tileWidth + b.x, by * tileHeight + b.y, b.w, b.h));
|
||||
}
|
||||
|
||||
export function intersect(
|
||||
ax: number, ay: number, aw: number, ah: number, bx: number, by: number, bw: number, bh: number
|
||||
): boolean {
|
||||
return ax <= (bx + bw) && (ax + aw) >= bx && ay <= (by + bh) && (ay + ah) >= by;
|
||||
}
|
||||
|
||||
// requests
|
||||
|
||||
export type RequestError = Error & { status?: number; text?: string; };
|
||||
|
||||
export function createError(status: number, data: string | { error: string; }): Error {
|
||||
if (status > 500 && status < 600) {
|
||||
return new Error(PROTECTION_ERROR);
|
||||
// } else if (status === 400) {
|
||||
// return new Error('Bad Request');
|
||||
} else if (status === 403) {
|
||||
return new Error(ACCESS_ERROR);
|
||||
} else if (status === 404) {
|
||||
return new Error(NOT_FOUND_ERROR);
|
||||
} else if (typeof data === 'string') {
|
||||
return new Error(data || OFFLINE_ERROR);
|
||||
} else {
|
||||
return new Error((data && data.error) || OFFLINE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
export function delay(timeout: number) {
|
||||
return new Promise<void>(resolve => setTimeout(resolve, timeout));
|
||||
}
|
||||
|
||||
export function observableToPromise<T>(observable: Observable<T>) {
|
||||
return observable.toPromise()
|
||||
.catch(({ status, error }: HttpErrorResponse) => {
|
||||
const text = error && error.text;
|
||||
|
||||
try {
|
||||
error = JSON.parse(error);
|
||||
} catch { }
|
||||
|
||||
const e: RequestError = createError(status || 0, error);
|
||||
e.status = status;
|
||||
e.text = text;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
// other
|
||||
|
||||
function setTransformDefault(element: HTMLElement | undefined, transform: string) {
|
||||
if (element) {
|
||||
element.style.transform = transform;
|
||||
}
|
||||
}
|
||||
|
||||
function setTransformSafari(element: HTMLElement | undefined, transform: string) {
|
||||
if (element) {
|
||||
(element.style as any).webkitTransform = transform;
|
||||
}
|
||||
}
|
||||
|
||||
export const setTransform = (typeof document !== 'undefined' && 'transform' in document.body.style) ?
|
||||
setTransformDefault : setTransformSafari;
|
||||
|
||||
export class ObjectCache<T> {
|
||||
private cache: T[] = [];
|
||||
constructor(private limit: number, private ctor: () => T) {
|
||||
}
|
||||
get(): T {
|
||||
return this.cache.pop() || this.ctor();
|
||||
}
|
||||
put(item: T) {
|
||||
if (this.cache.length < this.limit) {
|
||||
this.cache.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function bitmask(data: Uint8Array, key: number) {
|
||||
if (key) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] = data[i] ^ key;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function isCommand(text: string) {
|
||||
return /^\//.test(text);
|
||||
}
|
||||
|
||||
export function processCommand(text: string) {
|
||||
text = text.substr(1);
|
||||
const space = text.indexOf(' ');
|
||||
const command = (space === -1 ? text : text.substr(0, space)).trim() as string | undefined;
|
||||
const args = space === -1 ? '' : text.substr(space + 1).trim();
|
||||
return { command, args };
|
||||
}
|
||||
|
||||
// events
|
||||
|
||||
export type AnyEvent = MouseEvent | PointerEvent | TouchEvent;
|
||||
|
||||
export function isTouch(e: AnyEvent): e is TouchEvent {
|
||||
return /^touch/i.test(e.type);
|
||||
}
|
||||
|
||||
export function getButton(e: AnyEvent): number {
|
||||
return ('button' in e) ? (e.button || 0) : 0;
|
||||
}
|
||||
|
||||
export function getX(e: AnyEvent): number {
|
||||
return ('touches' in e && e.touches.length > 0) ? e.touches[0].pageX : (e as any).pageX;
|
||||
}
|
||||
|
||||
export function getY(e: AnyEvent): number {
|
||||
return ('touches' in e && e.touches.length > 0) ? e.touches[0].pageY : (e as any).pageY;
|
||||
}
|
||||
|
||||
export function isKeyEventInvalid(e: KeyboardEvent) {
|
||||
return e.target && /^(input|textarea|select)$/i.test((<any>e.target).tagName);
|
||||
}
|
||||
@@ -0,0 +1,741 @@
|
||||
import {
|
||||
Entity, Point, TileType, Rect, MapInfo, Camera, Region, IMap, MapState, defaultMapState, Pony,
|
||||
MapType, EntityFlags, WorldMap, Weather, EntityState, canWalk, MapFlags,
|
||||
} from './interfaces';
|
||||
import { contains, removeItem, boundsIntersect, array, pushUniq, containsPoint, removeItemFast } from './utils';
|
||||
import { isBoundsVisible, } from './camera';
|
||||
import {
|
||||
getRegionTile, setRegionTile, setRegionTileDirty, getRegionElevation, setRegionElevation,
|
||||
getRegionTileIndex, worldToRegionX, worldToRegionY, generateRegionCollider, invalidateRegionsCollider
|
||||
} from './region';
|
||||
import { weatherRain, splash } from './entities';
|
||||
import { releaseEntity, isMoving, isHidden, isDrawable, isPonyFlying } from './entityUtils';
|
||||
import { updatePonyEntity, invalidatePalettesForPony, ensurePonyInfoDecoded, isPony, isPonyOnTheGround } from './pony';
|
||||
import { getTileHeight, updateTileIndices, isInWater } from '../client/tileUtils';
|
||||
import { toScreenX, toScreenY, toScreenYWithZ, rectToScreen, toWorldZ } from './positionUtils';
|
||||
import { hasDrawLight, hasLightSprite } from '../client/draw';
|
||||
import { PonyTownGame } from '../client/game';
|
||||
import { WATER_FPS, PONY_TYPE, REGION_SIZE } from './constants';
|
||||
import { updatePosition, canCollideWith } from './collision';
|
||||
import { PaletteManager } from '../graphics/paletteManager';
|
||||
import { timeEnd, timeStart } from '../client/timing';
|
||||
import { playEffect } from '../client/handlers';
|
||||
import { isFlyingDown } from '../client/ponyStates';
|
||||
|
||||
const defaultMapInfo: MapInfo = {
|
||||
type: MapType.None,
|
||||
flags: MapFlags.None,
|
||||
regionsX: 0,
|
||||
regionsY: 0,
|
||||
defaultTile: TileType.None,
|
||||
};
|
||||
|
||||
export function createWorldMap(info = defaultMapInfo, state: MapState = { ...defaultMapState }): WorldMap {
|
||||
const { type, flags, regionsX, regionsY, defaultTile, editableArea } = info;
|
||||
const map: WorldMap = {
|
||||
type,
|
||||
flags,
|
||||
tileTime: 0,
|
||||
entities: [],
|
||||
entitiesDrawable: [],
|
||||
entitiesWithNames: [],
|
||||
entitiesWithChat: [],
|
||||
entitiesMoving: [],
|
||||
entitiesTriggers: [],
|
||||
entitiesLight: [],
|
||||
entitiesLightSprite: [],
|
||||
entitiesById: new Map<number, Entity>(),
|
||||
poniesToDecode: [],
|
||||
regionsX,
|
||||
regionsY,
|
||||
regions: array(regionsX * regionsY, undefined),
|
||||
defaultTile,
|
||||
width: regionsX * REGION_SIZE,
|
||||
height: regionsY * REGION_SIZE,
|
||||
minRegionX: 0,
|
||||
minRegionY: 0,
|
||||
maxRegionX: 0,
|
||||
maxRegionY: 0,
|
||||
state,
|
||||
editableArea,
|
||||
};
|
||||
|
||||
updateMinMaxRegion(map);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function pickAny(entity: Entity, point: Point): boolean {
|
||||
const bounds = entity.interactBounds || entity.bounds;
|
||||
return !!bounds && contains(entity.x, entity.y, bounds, point);
|
||||
}
|
||||
|
||||
export function getAnyBounds(entity: Entity) {
|
||||
return [
|
||||
entity.interactBounds,
|
||||
entity.bounds,
|
||||
entity.lightBounds,
|
||||
entity.lightSpriteBounds,
|
||||
entity.collidersBounds,
|
||||
entity.triggerBounds && rectToScreen(entity.triggerBounds),
|
||||
].filter(x => x && x.w > 0 && x.h > 0)[0];
|
||||
}
|
||||
|
||||
function pickAnyEvenLights(entity: Entity, point: Point): boolean {
|
||||
const bounds = getAnyBounds(entity);
|
||||
return !!bounds && contains(entity.x, entity.y, bounds, point);
|
||||
}
|
||||
|
||||
function pick(entity: Entity, point: Point, pickHidden: boolean, pickEditable: boolean): boolean {
|
||||
const editableOrInteractive = pickEditable ?
|
||||
(entity.type !== PONY_TYPE && ((entity.state & EntityState.Editable) !== 0)) :
|
||||
((entity.flags & EntityFlags.Interactive) !== 0);
|
||||
|
||||
return editableOrInteractive && (!isHidden(entity) || pickHidden) && pickAny(entity, point);
|
||||
}
|
||||
|
||||
function pickEntity(
|
||||
entity: Entity, point: Point, ignorePonies: boolean, pickHidden: boolean, pickEditable: boolean
|
||||
): boolean {
|
||||
return (!ignorePonies || entity.type !== PONY_TYPE) && pick(entity, point, pickHidden, pickEditable);
|
||||
}
|
||||
|
||||
function pickByBounds(entity: Entity, rect: Rect, pickHidden: boolean): boolean {
|
||||
if ((entity.flags & EntityFlags.Interactive) === 0 || (isHidden(entity) && !pickHidden)) {
|
||||
return false;
|
||||
} else {
|
||||
const bounds = entity.interactBounds || entity.bounds;
|
||||
return !!bounds && boundsIntersect(entity.x, entity.y, bounds, 0, 0, rect);
|
||||
}
|
||||
}
|
||||
|
||||
function pickEntityByBounds(entity: Entity, rect: Rect, ignorePonies: boolean, pickHidden: boolean): boolean {
|
||||
return (!ignorePonies || entity.type !== PONY_TYPE) && pickByBounds(entity, rect, pickHidden);
|
||||
}
|
||||
|
||||
export function pickAnyEntities(map: WorldMap, point: Point) {
|
||||
return map.entities.filter(e => pickAnyEvenLights(e, point)).reverse();
|
||||
}
|
||||
|
||||
export function pickEntities(map: WorldMap, point: Point, ignorePonies: boolean, pickHidden: boolean, pickEditable = false) {
|
||||
return map.entities.filter(e => pickEntity(e, point, ignorePonies, pickHidden, pickEditable)).reverse();
|
||||
}
|
||||
|
||||
export function pickEntitiesByRect(map: WorldMap, rect: Rect, ignorePonies: boolean, pickHidden: boolean) {
|
||||
return map.entities.filter(e => pickEntityByBounds(e, rect, ignorePonies, pickHidden)).reverse();
|
||||
}
|
||||
|
||||
export function removeRegions(map: WorldMap, coords: number[]) {
|
||||
if (coords.length === 0)
|
||||
return;
|
||||
|
||||
const entitiesToRemove = new Set<Entity>();
|
||||
|
||||
for (let i = 0; i < coords.length; i += 2) {
|
||||
const x = coords[i];
|
||||
const y = coords[i + 1];
|
||||
const index = x + y * map.regionsX;
|
||||
const region = map.regions[index];
|
||||
|
||||
if (region) {
|
||||
for (const entity of region.entities) {
|
||||
entitiesToRemove.add(entity);
|
||||
releaseEntity(entity);
|
||||
map.entitiesById.delete(entity.id);
|
||||
}
|
||||
}
|
||||
|
||||
map.regions[index] = undefined;
|
||||
setTilesDirty(map, x * REGION_SIZE - 1, y * REGION_SIZE - 1, REGION_SIZE + 2, REGION_SIZE + 2);
|
||||
}
|
||||
|
||||
removeEntitiesFromEntities(map, entitiesToRemove);
|
||||
updateMinMaxRegion(map);
|
||||
}
|
||||
|
||||
export function setRegion(map: WorldMap, x: number, y: number, region: Region) {
|
||||
if (x >= 0 && y >= 0 && x < map.regionsX && y < map.regionsY) {
|
||||
const index = x + y * map.regionsX;
|
||||
const oldRegion = map.regions[index];
|
||||
|
||||
if (oldRegion) {
|
||||
DEVELOPMENT && !TESTS && console.error(`Region already set (${x}, ${y})`);
|
||||
|
||||
for (const e of oldRegion.entities.slice()) {
|
||||
releaseAndRemoveEntityFromMap(map, e);
|
||||
}
|
||||
}
|
||||
|
||||
map.regions[index] = undefined;
|
||||
setTilesDirty(map, x * REGION_SIZE - 1, y * REGION_SIZE - 1, REGION_SIZE + 2, REGION_SIZE + 2);
|
||||
map.regions[index] = region;
|
||||
updateMinMaxRegion(map);
|
||||
} else {
|
||||
DEVELOPMENT && !TESTS && console.error(`Invalid region coords (${x}, ${y})`);
|
||||
}
|
||||
}
|
||||
|
||||
export function findEntityById(map: WorldMap, id: number) {
|
||||
return map.entitiesById.get(id);
|
||||
}
|
||||
|
||||
export function addEntity(map: WorldMap, entity: Entity) {
|
||||
const region = getRegionGlobal(map, entity.x, entity.y);
|
||||
|
||||
if (!region) {
|
||||
throw new Error(`Missing region at ${entity.x} ${entity.y}`);
|
||||
} else {
|
||||
addEntityToMapRegion(map, region, entity);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeEntity(map: WorldMap, entity: Entity) {
|
||||
removeEntityFromMapRegion(map, entity);
|
||||
releaseAndRemoveEntityFromMap(map, entity);
|
||||
}
|
||||
|
||||
function releaseAndRemoveEntityFromMap(map: WorldMap, entity: Entity) {
|
||||
releaseEntity(entity);
|
||||
removeEntityFromEntities(map, entity);
|
||||
}
|
||||
|
||||
export function removeEntityDirectly(map: WorldMap, entity: Entity) {
|
||||
forEachRegion(map, region => {
|
||||
const removed = removeEntityFromRegion(region, entity, map);
|
||||
|
||||
if (removed) {
|
||||
releaseEntity(entity);
|
||||
removeEntityFromEntities(map, entity);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function setTile(map: WorldMap, worldX: number, worldY: number, type: TileType) {
|
||||
const region = getRegionGlobal(map, worldX, worldY);
|
||||
|
||||
if (!region)
|
||||
return;
|
||||
|
||||
const x = Math.floor(worldX - region.x * REGION_SIZE);
|
||||
const y = Math.floor(worldY - region.y * REGION_SIZE);
|
||||
|
||||
const old = getRegionTile(region, x, y);
|
||||
setRegionTile(region, x, y, type);
|
||||
|
||||
setTilesDirty(map, worldX - 1, worldY - 1, 3, 3);
|
||||
|
||||
if (canWalk(old) !== canWalk(type)) {
|
||||
setColliderDirty(map, region, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
export function setColliderDirty(map: IMap<Region | undefined>, region: Region, x: number, y: number) {
|
||||
region.colliderDirty = true;
|
||||
|
||||
if (x === 0) {
|
||||
const r = getRegionUnsafe(map, region.x - 1, region.y);
|
||||
r && (r.colliderDirty = true);
|
||||
} else if (x === (REGION_SIZE - 1)) {
|
||||
const r = getRegionUnsafe(map, region.x + 1, region.y);
|
||||
r && (r.colliderDirty = true);
|
||||
}
|
||||
|
||||
if (y === 0) {
|
||||
const r = getRegionUnsafe(map, region.x, region.y - 1);
|
||||
r && (r.colliderDirty = true);
|
||||
} else if (y === (REGION_SIZE - 1)) {
|
||||
const r = getRegionUnsafe(map, region.x, region.y + 1);
|
||||
r && (r.colliderDirty = true);
|
||||
}
|
||||
}
|
||||
|
||||
export function setTileAtRegion(map: WorldMap, regionX: number, regionY: number, x: number, y: number, type: TileType) {
|
||||
setTile(map, regionX * REGION_SIZE + x, regionY * REGION_SIZE + y, type);
|
||||
}
|
||||
|
||||
export function setTilesDirty(map: IMap<Region | undefined>, ox: number, oy: number, w: number, h: number) {
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
doRelativeToRegion(map, x + ox, y + oy, (region, x, y) => setRegionTileDirty(region, x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTileIndex(map: IMap<Region | undefined>, x: number, y: number) {
|
||||
const region = getRegionGlobal(map, x, y);
|
||||
return region ? getRegionTileIndex(region, x - region.x * REGION_SIZE, y - region.y * REGION_SIZE) : 0;
|
||||
}
|
||||
|
||||
export function getElevation(map: WorldMap, x: number, y: number) {
|
||||
const region = getRegionGlobal(map, x, y);
|
||||
return region ? getRegionElevation(region, x - region.x * REGION_SIZE, y - region.y * REGION_SIZE) : 0;
|
||||
}
|
||||
|
||||
export function setElevation(map: WorldMap, x: number, y: number, value: number) {
|
||||
doRelativeToRegion(map, x, y, (region, x, y) => setRegionElevation(region, x, y, value));
|
||||
}
|
||||
|
||||
export function forEachRegion(map: WorldMap, callback: (region: Region) => boolean | void) {
|
||||
for (let y = map.minRegionY; y <= map.maxRegionY; y++) {
|
||||
for (let x = map.minRegionX; x <= map.maxRegionX; x++) {
|
||||
const region = getRegion(map, x, y);
|
||||
|
||||
if (region && callback(region) === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateMinMaxRegion(map: WorldMap) {
|
||||
map.minRegionX = map.regionsX;
|
||||
map.minRegionY = map.regionsY;
|
||||
map.maxRegionX = 0;
|
||||
map.maxRegionY = 0;
|
||||
|
||||
for (let y = 0; y < map.regionsY; y++) {
|
||||
for (let x = 0; x < map.regionsX; x++) {
|
||||
if (getRegion(map, x, y)) {
|
||||
map.minRegionX = Math.min(x, map.minRegionX);
|
||||
map.minRegionY = Math.min(y, map.minRegionY);
|
||||
map.maxRegionX = Math.max(x, map.maxRegionX);
|
||||
map.maxRegionY = Math.max(y, map.maxRegionY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map.maxRegionX = Math.min(map.maxRegionX, map.regionsX - 1);
|
||||
map.maxRegionY = Math.min(map.maxRegionY, map.regionsY - 1);
|
||||
}
|
||||
|
||||
function doRelativeToRegion(
|
||||
map: IMap<Region | undefined>, x: number, y: number, action: (region: Region, x: number, y: number) => void
|
||||
) {
|
||||
const region = getRegionGlobal(map, x, y);
|
||||
|
||||
if (region) {
|
||||
const regionX = Math.floor(x - region.x * REGION_SIZE);
|
||||
const regionY = Math.floor(y - region.y * REGION_SIZE);
|
||||
action(region, regionX, regionY);
|
||||
}
|
||||
}
|
||||
|
||||
function addEntityToRegion(region: Region, entity: Entity, map: WorldMap) {
|
||||
region.entities.push(entity);
|
||||
|
||||
if (canCollideWith(entity)) {
|
||||
region.colliders.push(entity);
|
||||
invalidateRegionsCollider(region, map);
|
||||
}
|
||||
}
|
||||
|
||||
function removeEntityFromRegion(region: Region, entity: Entity, map: WorldMap) {
|
||||
const removed = removeItemFast(region.entities, entity);
|
||||
|
||||
if (removed && canCollideWith(entity)) {
|
||||
removeItemFast(region.colliders, entity);
|
||||
invalidateRegionsCollider(region, map);
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
export function addEntityToMapRegion(map: WorldMap, region: Region, entity: Entity) {
|
||||
if (entity.id !== 0) {
|
||||
const existing = map.entitiesById.get(entity.id);
|
||||
|
||||
if (existing) {
|
||||
DEVELOPMENT && !TESTS && console.error(`Adding duplicate entity ${entity.id} (` +
|
||||
`${worldToRegionX(existing.x, map)}, ${worldToRegionY(existing.y, map)} => ` +
|
||||
`${worldToRegionX(entity.x, map)}, ${worldToRegionY(entity.y, map)})`);
|
||||
|
||||
removeEntity(map, existing);
|
||||
}
|
||||
|
||||
map.entitiesById.set(entity.id, entity);
|
||||
}
|
||||
|
||||
if (isPony(entity) && entity.palettePonyInfo === undefined) {
|
||||
map.poniesToDecode.push(entity);
|
||||
}
|
||||
|
||||
addEntityToRegion(region, entity, map);
|
||||
|
||||
map.entities.push(entity);
|
||||
|
||||
if (isDrawable(entity)) {
|
||||
map.entitiesDrawable.push(entity);
|
||||
}
|
||||
|
||||
if (isMoving(entity)) {
|
||||
map.entitiesMoving.push(entity);
|
||||
}
|
||||
|
||||
if (hasDrawLight(entity)) {
|
||||
pushUniq(map.entitiesLight, entity);
|
||||
}
|
||||
|
||||
if (hasLightSprite(entity)) {
|
||||
pushUniq(map.entitiesLightSprite, entity);
|
||||
}
|
||||
|
||||
if (entity.triggerBounds !== undefined) {
|
||||
pushUniq(map.entitiesTriggers, entity);
|
||||
}
|
||||
}
|
||||
|
||||
function removeEntityFromEntities(map: WorldMap, entity: Entity) {
|
||||
map.entitiesById.delete(entity.id);
|
||||
removeItemFast(map.entities, entity);
|
||||
removeItem(map.entitiesWithChat, entity);
|
||||
removeItem(map.entitiesWithNames, entity);
|
||||
|
||||
if (isDrawable(entity)) {
|
||||
removeItem(map.entitiesDrawable, entity);
|
||||
}
|
||||
|
||||
if (isMoving(entity)) {
|
||||
removeItemFast(map.entitiesMoving, entity);
|
||||
}
|
||||
|
||||
if (isPony(entity)) {
|
||||
removeItemFast(map.poniesToDecode, entity);
|
||||
}
|
||||
|
||||
if (hasDrawLight(entity)) {
|
||||
removeItemFast(map.entitiesLight, entity);
|
||||
}
|
||||
|
||||
if (hasLightSprite(entity)) {
|
||||
removeItemFast(map.entitiesLightSprite, entity);
|
||||
}
|
||||
|
||||
if (entity.triggerBounds !== undefined) {
|
||||
removeItemFast(map.entitiesTriggers, entity);
|
||||
}
|
||||
}
|
||||
|
||||
function removeEntitiesFromEntities(map: WorldMap, set: Set<Entity>) {
|
||||
if (set.size > 0) {
|
||||
const filter = (entity: Entity) => !set.has(entity);
|
||||
map.entities = map.entities.filter(filter);
|
||||
map.entitiesDrawable = map.entitiesDrawable.filter(filter);
|
||||
map.entitiesWithChat = map.entitiesWithChat.filter(filter);
|
||||
map.entitiesWithNames = map.entitiesWithNames.filter(filter);
|
||||
map.entitiesMoving = map.entitiesMoving.filter(filter);
|
||||
map.poniesToDecode = map.poniesToDecode.filter(filter);
|
||||
map.entitiesLight = map.entitiesLight.filter(filter);
|
||||
map.entitiesLightSprite = map.entitiesLightSprite.filter(filter);
|
||||
map.entitiesTriggers = map.entitiesTriggers.filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
function removeEntityFromMapRegion(map: WorldMap, entity: Entity) {
|
||||
forEachRegion(map, region => !removeEntityFromRegion(region, entity, map));
|
||||
}
|
||||
|
||||
export function getTile<T>(map: IMap<T>, x: number, y: number): TileType {
|
||||
const region = getRegionGlobal(map, x, y) as any as Region;
|
||||
|
||||
if (region) {
|
||||
const regionX = Math.floor(x - region.x * REGION_SIZE);
|
||||
const regionY = Math.floor(y - region.y * REGION_SIZE);
|
||||
return getRegionTile(region, regionX, regionY);
|
||||
} else {
|
||||
return TileType.None;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRegionGlobal<T>(map: IMap<T>, x: number, y: number): T {
|
||||
const rx = worldToRegionX(x, map);
|
||||
const ry = worldToRegionY(y, map);
|
||||
return getRegion(map, rx, ry);
|
||||
}
|
||||
|
||||
export function getRegion<T>(map: IMap<T>, x: number, y: number): T {
|
||||
if (x < 0 || y < 0 || x >= map.regionsX || y >= map.regionsY) {
|
||||
throw new Error(`Invalid region coords (${x}, ${y})`);
|
||||
} else {
|
||||
return map.regions[((x | 0) + (y | 0) * map.regionsX) | 0];
|
||||
}
|
||||
}
|
||||
|
||||
export function getRegionUnsafe<T>(map: IMap<T>, x: number, y: number): T | undefined {
|
||||
if (x < 0 || y < 0 || x >= map.regionsX || y >= map.regionsY) {
|
||||
return undefined;
|
||||
} else {
|
||||
return map.regions[((x | 0) + (y | 0) * map.regionsX) | 0];
|
||||
}
|
||||
}
|
||||
|
||||
export function addOrRemoveFromEntityList(list: Entity[], entity: Entity, had: boolean, has: boolean) {
|
||||
if (had !== has) {
|
||||
if (has) {
|
||||
pushUniq(list, entity);
|
||||
} else {
|
||||
removeItemFast(list, entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateEntitiesWithNames(map: WorldMap, hover: Point, player: Entity) {
|
||||
for (let i = map.entitiesWithNames.length - 1; i >= 0; i--) {
|
||||
const entity = map.entitiesWithNames[i];
|
||||
|
||||
if (!pickAny(entity, hover)) {
|
||||
map.entitiesWithNames.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const regionX = worldToRegionX(hover.x, map);
|
||||
const regionY = worldToRegionY(hover.y, map);
|
||||
|
||||
const minX = Math.max(0, regionX - 1) | 0;
|
||||
const minY = Math.max(0, regionY - 1) | 0;
|
||||
const maxX = Math.min(regionX + 1, map.regionsX - 1) | 0;
|
||||
const maxY = Math.min(regionY + 1, map.regionsY - 1) | 0;
|
||||
|
||||
for (let ry = minY; ry <= maxY; ry++) {
|
||||
for (let rx = minX; rx <= maxX; rx++) {
|
||||
const region = getRegion(map, rx, ry);
|
||||
|
||||
if (region !== undefined) {
|
||||
for (const e of region.entities) {
|
||||
if (e.name !== undefined && e !== player && pickAny(e, hover)) {
|
||||
pushUniq(map.entitiesWithNames, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateEntitiesCoverLifted(map: WorldMap, player: Entity, hideObjects: boolean, delta: number) {
|
||||
const playerX = toScreenX(player.x);
|
||||
const playerY = toScreenYWithZ(player.y, player.z);
|
||||
|
||||
for (const e of map.entitiesDrawable) {
|
||||
if (e.coverBounds !== undefined) {
|
||||
e.coverLifted = hideObjects || containsPoint(toScreenX(e.x), toScreenY(e.y), e.coverBounds, playerX, playerY);
|
||||
|
||||
const lifting = e.coverLifting || 0;
|
||||
|
||||
if (e.coverLifted && lifting < 1) {
|
||||
e.coverLifting = Math.min(lifting + delta * 2, 1);
|
||||
} else if (!e.coverLifted && lifting > 0) {
|
||||
e.coverLifting = Math.max(lifting - delta * 2, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateEntitiesTriggers(map: WorldMap, player: Pony, game: PonyTownGame) {
|
||||
for (const e of map.entitiesTriggers) {
|
||||
const on = (e.triggerTall || isPonyOnTheGround(player)) &&
|
||||
containsPoint(e.x, e.y, e.triggerBounds!, player.x, player.y);
|
||||
|
||||
if (e.triggerOn !== on) {
|
||||
if (on) {
|
||||
game.send(server => server.interact(e.id));
|
||||
}
|
||||
|
||||
e.triggerOn = on;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateMap(map: WorldMap, delta: number) {
|
||||
map.tileTime += delta * WATER_FPS;
|
||||
|
||||
forEachRegion(map, region => {
|
||||
if (region.tilesDirty) {
|
||||
updateTileIndices(region, map);
|
||||
}
|
||||
|
||||
if (region.colliderDirty) {
|
||||
generateRegionCollider(region, map);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getMapHeightAt(map: WorldMap, x: number, y: number, gameTime: number) {
|
||||
return getTileHeight(getTile(map, x, y), getTileIndex(map, x, y), x, y, gameTime, map.type);
|
||||
}
|
||||
|
||||
export function isInWaterAt(map: IMap<Region | undefined>, x: number, y: number) {
|
||||
return getTile(map, x, y) === TileType.Water && isInWater(getTileIndex(map, x, y), x, y);
|
||||
}
|
||||
|
||||
export function updateEntities(game: PonyTownGame, gameTime: number, delta: number, safe: boolean) {
|
||||
TIMING && timeStart('updateEntities');
|
||||
|
||||
const map = game.map;
|
||||
|
||||
for (const entity of map.entitiesMoving) {
|
||||
updatePosition(entity, delta, map);
|
||||
}
|
||||
|
||||
for (const entity of map.entities) {
|
||||
const flags = entity.flags;
|
||||
|
||||
if ((flags & EntityFlags.Bobbing) !== 0) {
|
||||
const bobs = entity.bobs!;
|
||||
const frame = (((gameTime / 1000) * entity.bobsFps!) | 0) % bobs.length;
|
||||
entity.z = toWorldZ(bobs[frame]);
|
||||
} else if ((flags & EntityFlags.StaticY) === 0) {
|
||||
entity.z = getMapHeightAt(map, entity.x, entity.y, gameTime);
|
||||
}
|
||||
|
||||
if (entity.type === PONY_TYPE) {
|
||||
const pony = entity as Pony;
|
||||
updatePonyEntity(pony, delta, gameTime, safe);
|
||||
|
||||
const wasSwimming = pony.swimming;
|
||||
pony.swimming = !isPonyFlying(pony) && isInWaterAt(map, pony.x, pony.y);
|
||||
|
||||
if (wasSwimming !== pony.swimming) {
|
||||
if (isFlyingDown(pony.animator.state)) {
|
||||
setTimeout(() => playEffect(game, pony, splash.type), 400);
|
||||
} else {
|
||||
playEffect(game, pony, splash.type);
|
||||
}
|
||||
}
|
||||
} else if (entity.update !== undefined) {
|
||||
entity.update(delta, gameTime);
|
||||
}
|
||||
|
||||
if ((flags & EntityFlags.OnOff) !== 0) {
|
||||
const on = (entity.state & EntityState.On) !== 0;
|
||||
|
||||
if (entity.lightOn !== undefined) {
|
||||
entity.lightOn = on;
|
||||
}
|
||||
|
||||
if (entity.lightSpriteOn !== undefined) {
|
||||
entity.lightSpriteOn = on;
|
||||
}
|
||||
}
|
||||
|
||||
if ((flags & EntityFlags.Light) !== 0) {
|
||||
if (entity.lightOn) {
|
||||
const move = delta * 0.2;
|
||||
|
||||
if (Math.abs(entity.lightScale! - entity.lightTarget!) < move) {
|
||||
entity.lightScale = entity.lightTarget;
|
||||
entity.lightTarget = 1 - Math.random() * 0.15;
|
||||
} else {
|
||||
entity.lightScale! += entity.lightScale! < entity.lightTarget! ? move : -move;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = map.entitiesWithChat.length - 1; i >= 0; i--) {
|
||||
const entity = map.entitiesWithChat[i];
|
||||
const says = entity.says!;
|
||||
|
||||
if (says.timer) {
|
||||
says.timer -= delta;
|
||||
|
||||
if (says.timer < 0) {
|
||||
says.timer = 0;
|
||||
entity.says = undefined;
|
||||
map.entitiesWithChat.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TIMING && timeEnd();
|
||||
}
|
||||
|
||||
export function invalidatePalettes(entities: Entity[]) {
|
||||
for (const entity of entities) {
|
||||
if (isPony(entity)) {
|
||||
invalidatePalettesForPony(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureAllVisiblePoniesAreDecoded(map: WorldMap, camera: Camera, paletteManager: PaletteManager) {
|
||||
const poniesToDecode = map.poniesToDecode;
|
||||
|
||||
if (!poniesToDecode.length)
|
||||
return;
|
||||
|
||||
const decode = new Set<number>();
|
||||
|
||||
for (let i = 0; i < poniesToDecode.length; i++) {
|
||||
const pony = poniesToDecode[i];
|
||||
|
||||
if (isBoundsVisible(camera, pony.bounds, pony.x, pony.y)) {
|
||||
decode.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (!decode.size)
|
||||
return;
|
||||
|
||||
if (decode.size > 100) {
|
||||
paletteManager.deduplicate = false;
|
||||
}
|
||||
|
||||
map.poniesToDecode = poniesToDecode.filter((pony, i) => {
|
||||
if (pony.palettePonyInfo !== undefined) {
|
||||
return false;
|
||||
} else if (decode.has(i)) {
|
||||
ensurePonyInfoDecoded(pony);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (decode.size > 100) {
|
||||
paletteManager.deduplicate = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function switchEntityRegion(map: WorldMap, entity: Entity, x: number, y: number) {
|
||||
removeEntityFromMapRegion(map, entity);
|
||||
|
||||
const region = getRegionGlobal(map, x, y);
|
||||
|
||||
if (region) {
|
||||
addEntityToRegion(region, entity, map);
|
||||
} else {
|
||||
releaseAndRemoveEntityFromMap(map, entity);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateMapState(map: WorldMap, prevState: MapState, newState: MapState) {
|
||||
if (prevState.weather !== newState.weather) {
|
||||
switch (newState.weather) {
|
||||
case Weather.None:
|
||||
removeWeatherEffects(map);
|
||||
break;
|
||||
case Weather.Rain:
|
||||
addRainEffects(map);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeWeatherEffects(map: WorldMap) {
|
||||
const effects = map.entities.filter(e => e.id === 0 && e.type === weatherRain.type);
|
||||
|
||||
for (const entity of effects) {
|
||||
removeEntityDirectly(map, entity);
|
||||
}
|
||||
}
|
||||
|
||||
function addRainEffects(map: WorldMap) {
|
||||
forEachRegion(map, region => {
|
||||
if (region.x === 3 && region.y === 4) { // TEMP: testing
|
||||
const entity = weatherRain((region.x + 0.5) * REGION_SIZE, (region.y + 0.5) * REGION_SIZE);
|
||||
addEntity(map, entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
div(*ngIf="account")
|
||||
.row
|
||||
.col-md-12
|
||||
h2
|
||||
| {{account.name}}
|
||||
small.text-muted.ml-1
|
||||
em {{account.name | translit}}
|
||||
|
||||
.row
|
||||
.col-md-4
|
||||
.mb-2
|
||||
account-info([account]="account" popoverPlacement="right")
|
||||
.mb-2
|
||||
code.text-muted {{account._id}}
|
||||
.mb-2
|
||||
div
|
||||
span.text-muted created:
|
||||
from-now.ml-1([time]="account.createdAt")
|
||||
span.text-muted.ml-1 ({{account.createdAt | date}})
|
||||
div
|
||||
span.text-muted last visit:
|
||||
from-now.ml-1([time]="account.lastVisit")
|
||||
span.text-muted.ml-1 ({{account.lastVisit | date}})
|
||||
div
|
||||
span.text-muted last browser:
|
||||
ua-info.ml-1([userAgent]="accountObject?.lastUserAgent")
|
||||
div
|
||||
span.text-muted age:
|
||||
span([class.text-strike]="account.birthyear")
|
||||
span.ml-1 {{age}}
|
||||
span.text-muted.ml-1 ({{account.birthdate | date:'yyyy-MM-dd'}})
|
||||
span(*ngIf="account.birthyear")
|
||||
span.ml-1 {{forceAge}}
|
||||
span.text-muted.ml-1 ({{account.birthyear}})
|
||||
.d-inline-block.dropdown(dropdown)
|
||||
a.icon-button.ml-1(dropdownToggle)
|
||||
fa-icon([icon]="cogIcon")
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
button.dropdown-item((click)="setAge(-1)")
|
||||
| unset
|
||||
button.dropdown-item(*ngFor="let age of ages" (click)="setAge(age)")
|
||||
| set to #[b {{age}}]yo
|
||||
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Counters
|
||||
div
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="refreshDetails()" title="Refresh account details")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
.mb-2
|
||||
div(*ngFor="let counter of counters")
|
||||
span.text-muted {{counter.key}}:
|
||||
span.ml-1 {{counter.value}}
|
||||
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Status
|
||||
div
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="clearSessions()" title="Sign out user" [disabled]="clearingSessions")
|
||||
fa-icon([icon]="clearingSessions ? spinnerIcon : signOutIcon" [fixedWidth]="true" [spin]="clearingSessions")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="showAccountData()" title="Fetch account data (in console)")
|
||||
fa-icon([icon]="consoleIcon" [fixedWidth]="true")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="accountStatus.refresh()" title="Refresh status")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.mb-2
|
||||
account-status(#accountStatus [account]="account" [verbose]="true")
|
||||
|
||||
h5 Roles
|
||||
.mb-2
|
||||
.btn-group.btn-group-sm
|
||||
button.btn(*ngFor="let r of roles" [btnHighlight]="hasRole(r)" (click)="toggleRole(r)" [disabled]="!canToggleRole(r)")
|
||||
| {{r}}
|
||||
button.btn.btn-sm.btn-danger.ml-1((click)="remove()" [disabled]="!canRemove" title="Delete account")
|
||||
fa-icon([icon]="trashIcon" [fixedWidth]="true")
|
||||
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5
|
||||
| Notes
|
||||
a.text-muted.ml-1([href]="translateUrl(account.note)" target="_blank" title="Translate note")
|
||||
fa-icon([icon]="langIcon")
|
||||
.text-muted
|
||||
from-now([time]="account.noteUpdated")
|
||||
.well.pre-line.text-truncate
|
||||
| {{account.note}}
|
||||
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Merges #[span.text-muted ({{merges?.length || 0}})]
|
||||
div
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="refreshDetails()" title="Refresh account details")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
table.table.table-sm(*ngIf="merges?.length")
|
||||
thead
|
||||
tr
|
||||
th id
|
||||
th name
|
||||
th date
|
||||
tbody
|
||||
tr(*ngFor="let m of merges" [class.text-slashed]="m.split")
|
||||
td
|
||||
ng-template(#mergeTooltip)
|
||||
div {{getMergeTooltip(m)}}
|
||||
div([tooltip]="mergeTooltip" placement="right" containerClass="tooltip-merge")
|
||||
| {{m.id}}
|
||||
td {{m.name}}
|
||||
td
|
||||
.d-flex
|
||||
.flex-grow-1
|
||||
time-field([time]="m.date")
|
||||
.dropdown.float-right(dropdown)
|
||||
button.btn.btn-xs.btn-default.dropdown-toggle(
|
||||
dropdownToggle [tooltip]="(m.reason || '[null reason]') + (m.split ? ' (split)' : '')")
|
||||
fa-icon([icon]="cogIcon" [fixedWidth]="true")
|
||||
.dropdown-menu.dropdown-menu-right(*dropdownMenu)
|
||||
button.dropdown-item((click)="printMerge(m)")
|
||||
| Print merge info
|
||||
button.dropdown-item((click)="printMerge2(m)")
|
||||
| Print usable merge info
|
||||
button.dropdown-item((click)="showMergeInNewTab(m)")
|
||||
| Show merge info
|
||||
button.dropdown-item((click)="showMergeInNewTab2(m)")
|
||||
| Show usable merge info
|
||||
.dropdown-divider
|
||||
button.dropdown-item((click)="unmerge(m._id, m.data.merge, m.data.account)")
|
||||
| Split off #[b merged]
|
||||
button.dropdown-item((click)="unmerge(m._id, m.data.account, m.data.merge)")
|
||||
| Split off #[b account]
|
||||
|
||||
h5 Support #[span.text-muted ({{support?.length || 0}})]
|
||||
table.table.table-sm(*ngIf="support?.length")
|
||||
thead
|
||||
tr
|
||||
th message
|
||||
th date
|
||||
tbody
|
||||
tr(*ngFor="let l of support")
|
||||
td
|
||||
fa-icon.mr-1([icon]="l.icon" [ngClass]="l.class")
|
||||
| {{l.message}}
|
||||
td
|
||||
time-field.mr-2([time]="l.date")
|
||||
span.text-muted ({{l.date | date:'MMM d'}})
|
||||
|
||||
h5 Ban log #[span.text-muted ({{banLog?.length || 0}})]
|
||||
table.table.table-sm(*ngIf="banLog?.length")
|
||||
thead
|
||||
tr
|
||||
th message
|
||||
th date
|
||||
tbody
|
||||
tr(*ngFor="let l of banLog")
|
||||
td {{l.message}}
|
||||
td: time-field([time]="l.date")
|
||||
|
||||
.col-md-5
|
||||
//- auths
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Auths #[span.text-muted ({{auths.length}})]
|
||||
.dropdown(dropdown)
|
||||
button.btn.btn-xs.btn-default.dropdown-toggle(dropdownToggle)
|
||||
fa-icon([icon]="cogIcon" [fixedWidth]="true")
|
||||
.dropdown-menu.dropdown-menu-right(*dropdownMenu)
|
||||
a.dropdown-item((click)="fetchAuths()")
|
||||
| Fetch auths data
|
||||
a.dropdown-item((click)="printAuthList()")
|
||||
| Print auths list
|
||||
a.dropdown-item((click)="printAuthData()")
|
||||
| Print auths data
|
||||
|
||||
.mb-2
|
||||
auth-info-edit(*ngFor="let a of auths | slice:0:authLimit" [authId]="a" [duplicates]="duplicates")
|
||||
|
||||
a.text-muted((click)="authLimit = 99999" *ngIf="authLimit < auths.length")
|
||||
| more ...
|
||||
|
||||
.well.mt-2.mb-1.py-2(*ngIf="authData" style="font-size: 12px;")
|
||||
button.close((click)="clearAuthData()" style="color: #ddd; margin-right: -5px; margin-top: -5px;") ×
|
||||
button.close((click)="copyAuthData()" style="color: #ddd; margin-right: 6px; margin-top: -5px;")
|
||||
fa-icon([icon]="copyIcon" size="xs")
|
||||
.text-muted Auths for {{authDataAccount}}
|
||||
.pre-line {{authData}}
|
||||
|
||||
//- origins
|
||||
.d-flex.justify-content-between
|
||||
h5 Origins #[span.text-muted ({{origins.length}})]
|
||||
div
|
||||
button.btn.btn-xs.btn-default((click)="clearOrigins(true, true)" title="Remove all origins older than 14 days used by only one account")
|
||||
fa-icon.mr-1([icon]="trashIcon")
|
||||
| old single
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="clearOrigins(true, false)" title="Remove all origins older than 14 days")
|
||||
fa-icon.mr-1([icon]="trashIcon")
|
||||
| old
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="clearOrigins(false, true)" title="Remove all origins used by only one account")
|
||||
fa-icon.mr-1([icon]="trashIcon")
|
||||
| single
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="clearOrigins(false, false)" title="Remove all origins")
|
||||
fa-icon.mr-1([icon]="trashIcon")
|
||||
| all
|
||||
.dropdown.d-inline-block.ml-1(dropdown)
|
||||
button.btn.btn-xs.btn-default.dropdown-toggle(dropdownToggle title="Remove all origins in region")
|
||||
fa-icon.mr-1([icon]="trashIcon")
|
||||
| region
|
||||
.dropdown-menu(*dropdownMenu)
|
||||
a.dropdown-item(*ngFor="let region of originRegions" (click)="clearOriginsInRegion(region)")
|
||||
| {{region}}
|
||||
.mb-2
|
||||
.d-flex.align-items-center.mb-1(*ngFor="let o of origins")
|
||||
origin-info-remote.flex-grow-1([originIP]="o.ip")
|
||||
small.text-muted
|
||||
from-now.mr-1([time]="o.last")
|
||||
button.btn.btn-xs.btn-default((click)="removeOrigin(o.ip)" title="Remove origin")
|
||||
fa-icon([icon]="trashIcon")
|
||||
|
||||
hr
|
||||
|
||||
//- duplicates
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5
|
||||
| Duplicates #[span.text-muted ({{duplicates?.length}})]
|
||||
fa-icon.text-muted.ml-1(*ngIf="loadingDuplicates" [icon]="spinnerIcon" [fixedWidth]="true" [spin]="true")
|
||||
div
|
||||
button.btn.btn-xs.btn-default((click)="clearOriginsFromDuplicates()" title="Remove all origins older than 14 days from all duplicates")
|
||||
fa-icon.mr-1([icon]="trashIcon")
|
||||
| old
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="refresh()" title="Refresh duplicates")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.mb-2
|
||||
.text-muted(*ngIf="!duplicates || !duplicates.length")
|
||||
| - no duplicates -
|
||||
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let d of duplicates | slice:0:duplicatesLimit")
|
||||
ng-template(#duplicatePonies)
|
||||
.text-left(*ngIf="d.ponies")
|
||||
div(*ngFor="let p of d.ponies")
|
||||
| {{p}}
|
||||
|
||||
span.badge.badge-none(title="Duplicate origins" [class.deleted]="d.origins === 0")
|
||||
| {{d.origins}}
|
||||
span.badge.badge-none.ml-1(
|
||||
title="Duplicate ponies" [class.deleted]="!d.ponies || !d.ponies.length"
|
||||
[tooltip]="duplicatePonies" [isDisabled]="!d.ponies || !d.ponies.length")
|
||||
| {{d.ponies ? d.ponies.length : '-'}}
|
||||
|
||||
fa-icon.ml-1.text-alert(
|
||||
*ngIf="d.note" [icon]="duplicateNoteIcon" title="Duplicate in note")
|
||||
fa-icon.ml-1.text-muted(
|
||||
*ngIf="d.browserId" [icon]="duplicateBrowserIdIcon" title="Duplicate browser ID")
|
||||
fa-icon.ml-1.text-muted(
|
||||
*ngIf="d.userAgent" [icon]="duplicateBrowserIcon" [tooltip]="d.userAgent" title="Duplicate browser")
|
||||
fa-icon.ml-1(
|
||||
*ngIf="d.emails" [icon]="duplicateEmailIcon" title="Duplicate emails: {{d.emails}}"
|
||||
[ngClass]="d.indenticalEmail ? 'text-alert' : 'text-muted'")
|
||||
fa-icon.ml-1.text-alert(*ngIf="d.birthdate" [icon]="duplicateBirthdateIcon" title="Duplicate birthdate")
|
||||
fa-icon.ml-1.text-muted(*ngIf="d.name" [icon]="duplicateNameIcon" title="Duplicate name")
|
||||
|
||||
account-info-remote.ml-1(#info [accountId]="d.account" [extendedAuths]="true")
|
||||
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="merge(d.account)" title="Merge account")
|
||||
fa-icon([icon]="mergeIcon" size="lg")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="chatLog.add(info.account)" title="Add to chat log")
|
||||
fa-icon([icon]="commentIcon" [fixedWidth]="true")
|
||||
|
||||
a.text-muted(*ngIf="duplicates && duplicates.length > duplicatesLimit" (click)="duplicatesLimit = 9999999")
|
||||
| more ...
|
||||
a.text-muted(*ngIf="duplicates && duplicatesLimit === 9999999" (click)="duplicatesLimit = 10")
|
||||
| less ...
|
||||
|
||||
hr
|
||||
|
||||
//- around
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Around #[span.text-muted ({{around ? around.length : '-'}})]
|
||||
button.btn.btn-xs.btn-default((click)="refreshAround()" title="Refresh around")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.mb-2
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let a of around")
|
||||
span.badge.badge-none(title="Distance in tiles from user") {{a.distance.toFixed(1)}}
|
||||
|
||||
fa-icon.ml-1.text-alert(*ngIf="a.party" [icon]="partyIcon" title="In party with user")
|
||||
|
||||
account-info-remote.ml-1(#info [accountId]="a.account" [extendedAuths]="true" [showDuplicates]="true")
|
||||
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="merge(a.account)" title="Merge account")
|
||||
fa-icon([icon]="mergeIcon" size="lg")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="chatLog.add(info.account)" title="Add to chat log")
|
||||
fa-icon([icon]="commentIcon" [fixedWidth]="true")
|
||||
|
||||
hr
|
||||
|
||||
//- friends
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Friends #[span.text-muted ({{friends ? friends.length : '?'}})]
|
||||
button.btn.btn-xs.btn-default((click)="fetchFriends()" title="Fetch friends list")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.mb-2
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let a of friends | slice:0:friendsLimit")
|
||||
account-info-remote([accountId]="a")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="removeFriend(a)" title="Remove friend")
|
||||
fa-icon([icon]="trashIcon")
|
||||
a.text-muted(*ngIf="friends && friends.length > friendsLimit" (click)="friendsLimit = 9999999")
|
||||
| more ...
|
||||
|
||||
//- supporter invites
|
||||
//- h5 Supporter invites #[span.text-muted ({{invites?.length || 0}})]
|
||||
//- div(*ngIf="invites?.length")
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let i of invites")
|
||||
.text-muted.mr-2 {{i.type}}
|
||||
fa-icon.mr-2([icon]="checkIcon" [ngClass]="i.active ? 'text-success' : 'text-danger'")
|
||||
account-info-remote.ml-1([accountId]="i.target" [extendedAuths]="true")
|
||||
small.text-muted.flex-grow-1.text-right
|
||||
time-field([time]="i.createdAt")
|
||||
|
||||
hr
|
||||
|
||||
//- force hides
|
||||
div(*ngIf="account.hides && account.hides.length")
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Force hidden #[span.text-muted ({{account.hides.length}})]
|
||||
|
||||
.mb-2
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let a of account.hides")
|
||||
account-info-remote([accountId]="a")
|
||||
|
||||
hr
|
||||
|
||||
//- hides
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Hidden by #[span.text-muted ({{hiddenBy ? hiddenBy.length : '?'}})]
|
||||
button.btn.btn-xs.btn-default((click)="fetchHidden()" title="Fetch hidden list")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.mb-2
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let a of hiddenBy | slice:0:hiddenByLimit")
|
||||
account-info-remote([accountId]="a")
|
||||
a.text-muted(*ngIf="hiddenBy && hiddenBy.length > hiddenByLimit" (click)="hiddenByLimit = 9999999")
|
||||
| more ...
|
||||
|
||||
h5 Hides users #[span.text-muted ({{hidden ? hidden.length : '?'}})]
|
||||
.mb-2
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let a of hidden | slice:0:hiddenLimit")
|
||||
account-info-remote([accountId]="a")
|
||||
a.text-muted(*ngIf="hidden && hidden.length > hiddenLimit" (click)="hiddenLimit = 9999999")
|
||||
| more ...
|
||||
|
||||
//- hides (perma)
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Perma hidden by #[span.text-muted ({{permaHiddenBy ? permaHiddenBy.length : '?'}})]
|
||||
|
||||
.mb-2
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let a of permaHiddenBy | slice:0:permaHiddenByLimit")
|
||||
account-info-remote([accountId]="a")
|
||||
a.text-muted(*ngIf="permaHiddenBy && permaHiddenBy.length > permaHiddenByLimit" (click)="permaHiddenByLimit = 9999999")
|
||||
| more ...
|
||||
|
||||
h5 Perma hides users #[span.text-muted ({{permaHidden ? permaHidden.length : '?'}})]
|
||||
.mb-2
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let a of permaHidden | slice:0:permaHiddenLimit")
|
||||
account-info-remote([accountId]="a")
|
||||
a.text-muted(*ngIf="permaHidden && permaHidden.length > permaHiddenLimit" (click)="permaHiddenLimit = 9999999")
|
||||
| more ...
|
||||
|
||||
hr
|
||||
|
||||
//- ignores
|
||||
.d-flex.justify-content-between.align-items-start
|
||||
h5 Ignored by #[span.text-muted ({{ignoredBy ? ignoredBy.length : account.ignoresCount}})]
|
||||
button.btn.btn-xs.btn-default((click)="fetchIgnores()" title="Fetch ignores list")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.mb-2
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let a of ignoredBy | slice:0:ignoredByLimit")
|
||||
account-info-remote([accountId]="a")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="removeIgnore(a._id, account._id)" title="Remove ignore")
|
||||
fa-icon([icon]="trashIcon")
|
||||
a.text-muted(*ngIf="ignoredBy && ignoredBy.length > ignoredByLimit" (click)="ignoredByLimit = 9999999")
|
||||
| more ...
|
||||
|
||||
h5 Ignores users #[span.text-muted ({{ignores ? ignores.length : '?'}})]
|
||||
.mb-2
|
||||
.mb-1.d-flex.align-items-center(*ngFor="let a of ignores | slice:0:ignoresLimit")
|
||||
account-info-remote([accountId]="a")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="removeIgnore(account._id, a._id)" title="Remove ignore")
|
||||
fa-icon([icon]="trashIcon")
|
||||
a.text-muted(*ngIf="ignores && ignores.length > ignoresLimit" (click)="ignoresLimit = 9999999")
|
||||
| more ...
|
||||
|
||||
.col-md-3
|
||||
//- emails
|
||||
h5.d-flex.justify-content-between
|
||||
div Emails #[span.text-muted ({{account.emails?.length || 0}})]
|
||||
.dropdown(dropdown)
|
||||
button.btn.btn-xs.btn-default.dropdown-toggle(dropdownToggle)
|
||||
fa-icon([icon]="cogIcon" [fixedWidth]="true")
|
||||
.dropdown-menu.dropdown-menu-right(*dropdownMenu)
|
||||
a.dropdown-item((click)="addEmail()")
|
||||
| Add email
|
||||
.mb-2
|
||||
.text-muted(*ngIf="!account.emails?.length")
|
||||
| - no emails -
|
||||
.d-flex.justify-content-between.items-align-center.mb-1(*ngFor="let e of account.emails | orderBy | slice:0:emailLimit")
|
||||
div {{e}}
|
||||
button.btn.btn-xs.btn-default((click)="removeEmail(e)" title="Remove email")
|
||||
fa-icon([icon]="trashIcon")
|
||||
|
||||
a.text-muted((click)="emailLimit = 99999" *ngIf="emailLimit < (account.emails?.length || 0)")
|
||||
| more ...
|
||||
|
||||
//- ponies
|
||||
h5.d-flex.justify-content-between
|
||||
div Ponies #[span.text-muted ({{account.characterCount}})]
|
||||
.dropdown(dropdown)
|
||||
button.btn.btn-xs.btn-default.dropdown-toggle(dropdownToggle)
|
||||
fa-icon([icon]="cogIcon" [fixedWidth]="true")
|
||||
.dropdown-menu.dropdown-menu-right(*dropdownMenu)
|
||||
a.dropdown-item((click)="getPoniesCreators()")
|
||||
| Fetch creator info
|
||||
a.dropdown-item((click)="restoringPonies = true")
|
||||
| Restore ponies
|
||||
a.dropdown-item((click)="removePoniesAboveLimit()")
|
||||
| Remove ponies above limit
|
||||
a.dropdown-item.text-danger((click)="removeAllPonies()")
|
||||
| Remove all ponies
|
||||
.mb-2(*ngIf="restoringPonies")
|
||||
textarea.form-control.mb-1([(ngModel)]="poniesToRestore" placeholder="paste removed pony logs here")
|
||||
textarea.form-control.mb-1([(ngModel)]="poniesToRestoreFilter" placeholder="paste pony ids to use here (optional)")
|
||||
.text-right
|
||||
button.btn.btn-xs.btn-default((click)="poniesToRestore = ''; restoringPonies = false") cancel
|
||||
button.btn.btn-xs.btn-success.ml-1((click)="restorePonies()" [disabled]="!poniesToRestore") restore
|
||||
hr
|
||||
.mb-2
|
||||
pony-list-remote(
|
||||
[accountId]="id" [expanded]="true" [deletable]="true" [highlight]="highlighCharacter" [duplicates]="duplicates")
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
h5 Events
|
||||
events-table([events]="events" (showChat)="chatLog.show(account)")
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
admin-chat-log(#chatLog [canClose]="false" [account]="account")
|
||||
button.btn.btn-sm.btn-default((click)="printJSON()") raw data
|
||||
|
||||
.well.pre(*ngIf="rawData")
|
||||
| {{rawData}}
|
||||
@@ -0,0 +1,17 @@
|
||||
@import 'partials/variables';
|
||||
|
||||
.spoiler-text {
|
||||
background: $body-color;
|
||||
border-radius: 3px;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.long-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { remove, uniq } from 'lodash';
|
||||
import {
|
||||
Account, Event, Auth, ROLES, Character, MergeInfo, MergeAccountData, SupporterInvite, accountFlags,
|
||||
OriginInfoBase, DuplicateResult, AroundEntry, LogEntry
|
||||
} from '../../../common/adminInterfaces';
|
||||
import { compareByName, createSupporterChanges, SupporterChange, getTranslationUrl, getAge } from '../../../common/adminUtils';
|
||||
import { hasRole } from '../../../common/accountUtils';
|
||||
import { AdminModel } from '../../services/adminModel';
|
||||
import {
|
||||
faCog, faSync, faLanguage, faTrash, faGlobe, faTerminal, faSpinner,
|
||||
faIdBadge, faEnvelope, faFont, faCompressArrowsAlt, faComment, faSignOutAlt, faUsers, faCheckCircle,
|
||||
faDatabase, faCopy, faCalendar, faExclamationCircle,
|
||||
} from '../../../client/icons';
|
||||
import { flagsToString, includes, flatten, removeItem } from '../../../common/utils';
|
||||
import { Subscription } from '../../../common/interfaces';
|
||||
import { showTextInNewTab } from '../../../client/htmlUtils';
|
||||
|
||||
const defaultLimit = 15;
|
||||
const defaultDuplicatesLimit = 10;
|
||||
const year = (new Date()).getFullYear();
|
||||
|
||||
@Component({
|
||||
selector: 'admin-account-details',
|
||||
templateUrl: 'admin-account-details.pug',
|
||||
styleUrls: ['admin-account-details.scss'],
|
||||
})
|
||||
export class AdminAccountDetails implements OnInit, OnDestroy {
|
||||
readonly cogIcon = faCog;
|
||||
readonly syncIcon = faSync;
|
||||
readonly langIcon = faLanguage;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly signOutIcon = faSignOutAlt;
|
||||
readonly duplicateNoteIcon = faExclamationCircle;
|
||||
readonly duplicateBrowserIcon = faGlobe;
|
||||
readonly duplicateBrowserIdIcon = faIdBadge;
|
||||
readonly duplicateEmailIcon = faEnvelope;
|
||||
readonly duplicateNameIcon = faFont;
|
||||
readonly duplicateBirthdateIcon = faCalendar;
|
||||
readonly mergeIcon = faCompressArrowsAlt;
|
||||
readonly commentIcon = faComment;
|
||||
readonly partyIcon = faUsers;
|
||||
readonly checkIcon = faCheckCircle;
|
||||
readonly consoleIcon = faTerminal;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly dataIcon = faDatabase;
|
||||
readonly copyIcon = faCopy;
|
||||
readonly roles = ROLES;
|
||||
ages = [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18];
|
||||
account?: Account;
|
||||
duplicates?: DuplicateResult[];
|
||||
ponyNames?: string[];
|
||||
ignores?: string[];
|
||||
ignoredBy?: string[];
|
||||
hidden?: string[];
|
||||
hiddenBy?: string[];
|
||||
permaHidden?: string[];
|
||||
permaHiddenBy?: string[];
|
||||
friends?: string[];
|
||||
events?: Event[];
|
||||
merges?: MergeInfo[];
|
||||
banLog?: LogEntry[];
|
||||
support?: SupporterChange[];
|
||||
rawData?: string;
|
||||
invites?: SupporterInvite[];
|
||||
auths: string[] = [];
|
||||
origins: OriginInfoBase[] = [];
|
||||
counters: { key: string; value: any; }[] = [];
|
||||
authLimit = defaultLimit;
|
||||
emailLimit = defaultLimit;
|
||||
loadingDuplicates = false;
|
||||
accountObject?: Account;
|
||||
clearingSessions = false;
|
||||
authData?: string;
|
||||
authDataAccount?: string;
|
||||
restoringPonies = false;
|
||||
poniesToRestore = '';
|
||||
poniesToRestoreFilter = '';
|
||||
friendsLimit = defaultLimit;
|
||||
hiddenByLimit = defaultLimit;
|
||||
hiddenLimit = defaultLimit;
|
||||
permaHiddenByLimit = defaultLimit;
|
||||
permaHiddenLimit = defaultLimit;
|
||||
private id?: string;
|
||||
private aroundMap = new Map<string, AroundEntry[]>();
|
||||
private duplicatesLimits = new Map<string, number>();
|
||||
private ignoresLimits = new Map<string, number>();
|
||||
private ignoredByLimits = new Map<string, number>();
|
||||
private authsSubscription?: Subscription;
|
||||
private accountSubscription?: Subscription;
|
||||
private originsSubscription?: Subscription;
|
||||
constructor(private route: ActivatedRoute, private model: AdminModel) {
|
||||
}
|
||||
get canRemove() {
|
||||
return hasRole(this.model.account, 'superadmin');
|
||||
}
|
||||
get around() {
|
||||
return this.aroundMap.get(this.id || '');
|
||||
}
|
||||
get duplicatesLimit() {
|
||||
return (this.id && this.duplicatesLimits.has(this.id)) ? this.duplicatesLimits.get(this.id)! : defaultDuplicatesLimit;
|
||||
}
|
||||
set duplicatesLimit(value) {
|
||||
if (this.id) {
|
||||
this.duplicatesLimits.set(this.id, value);
|
||||
}
|
||||
}
|
||||
get ignoresLimit() {
|
||||
return (this.id && this.ignoresLimits.has(this.id)) ? this.ignoresLimits.get(this.id)! : defaultLimit;
|
||||
}
|
||||
set ignoresLimit(value) {
|
||||
if (this.id) {
|
||||
this.ignoresLimits.set(this.id, value);
|
||||
}
|
||||
}
|
||||
get ignoredByLimit() {
|
||||
return (this.id && this.ignoredByLimits.has(this.id)) ? this.ignoredByLimits.get(this.id)! : defaultLimit;
|
||||
}
|
||||
set ignoredByLimit(value) {
|
||||
if (this.id) {
|
||||
this.ignoredByLimits.set(this.id, value);
|
||||
}
|
||||
}
|
||||
get originRegions() {
|
||||
return uniq(this.origins.map(o => o.country));
|
||||
}
|
||||
get age() {
|
||||
return (this.account && this.account.birthdate) ? getAge(this.account.birthdate) : '-';
|
||||
}
|
||||
get forceAge() {
|
||||
return (this.account && this.account.birthyear) ? (year - this.account.birthyear) : '-';
|
||||
}
|
||||
ngOnInit() {
|
||||
this.route.params.subscribe(({ id }) => {
|
||||
this.id = id;
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.model.updated = () => this.update();
|
||||
this.update();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.model.updated = () => { };
|
||||
this.authsSubscription && this.authsSubscription.unsubscribe();
|
||||
this.accountSubscription && this.accountSubscription.unsubscribe();
|
||||
this.originsSubscription && this.originsSubscription.unsubscribe();
|
||||
}
|
||||
refresh() {
|
||||
const account = this.account;
|
||||
|
||||
this.ponyNames = [];
|
||||
this.banLog = undefined;
|
||||
this.merges = undefined;
|
||||
this.support = undefined;
|
||||
this.invites = undefined;
|
||||
this.accountObject = undefined;
|
||||
this.loadingDuplicates = false;
|
||||
|
||||
this.authsSubscription && this.authsSubscription.unsubscribe();
|
||||
this.authsSubscription = undefined;
|
||||
|
||||
this.originsSubscription && this.originsSubscription.unsubscribe();
|
||||
this.originsSubscription = undefined;
|
||||
|
||||
this.auths = [];
|
||||
this.origins = [];
|
||||
this.counters = [];
|
||||
|
||||
this.friends = undefined;
|
||||
this.hidden = undefined;
|
||||
this.hiddenBy = undefined;
|
||||
this.permaHidden = undefined;
|
||||
this.permaHiddenBy = undefined;
|
||||
|
||||
if (account) {
|
||||
account.ignoredByLimit = account.ignoredByLimit || 10;
|
||||
account.ignoresLimit = account.ignoresLimit || 10;
|
||||
account.duplicatesLimit = account.duplicatesLimit || 10;
|
||||
|
||||
this.loadingDuplicates = true;
|
||||
this.model.getAllDuplicates(account._id)
|
||||
.then(duplicates => {
|
||||
if (duplicates) {
|
||||
this.ponyNames = uniq(flatten(duplicates.map(d => (d.ponies || []).map(x => x.toLowerCase()))));
|
||||
this.duplicates = duplicates;
|
||||
this.loadingDuplicates = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.model.getAccount(account._id)
|
||||
.then(account => this.accountObject = account);
|
||||
|
||||
this.refreshDetails();
|
||||
|
||||
this.authsSubscription = this.model.accountAuths
|
||||
.subscribe(account._id, auths => this.auths = auths || []);
|
||||
|
||||
this.originsSubscription = this.model.accountOrigins
|
||||
.subscribe(account._id, origins => this.origins = origins || []);
|
||||
} else {
|
||||
this.duplicates = [];
|
||||
this.ignores = [];
|
||||
this.ignoredBy = [];
|
||||
}
|
||||
}
|
||||
refreshAround() {
|
||||
const account = this.account;
|
||||
|
||||
if (account) {
|
||||
this.model.getAccountAround(account._id)
|
||||
.then(accounts => {
|
||||
if (accounts) {
|
||||
this.aroundMap.set(account._id, accounts);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
refreshDetails() {
|
||||
function createCounter(key: string, value: any) {
|
||||
if (key === 'toys') {
|
||||
value = (value >>> 0).toString(2).padStart(32, '0');
|
||||
}
|
||||
|
||||
return { key, value };
|
||||
}
|
||||
|
||||
if (this.account) {
|
||||
this.model.getDetailsForAccount(this.account)
|
||||
.then(details => {
|
||||
if (details) {
|
||||
this.banLog = details.banLog;
|
||||
this.merges = details.merges;
|
||||
this.support = createSupporterChanges(details.supporterLog);
|
||||
this.invites = [
|
||||
...details.invitesSent!.map(i => ({ ...i, type: 'sent' })),
|
||||
...details.invitesReceived!.map(i => ({ ...i, type: 'recv' })),
|
||||
];
|
||||
const state = details.state as any;
|
||||
this.counters = Object.keys(state).map(key => createCounter(key, state[key]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
fetchIgnores() {
|
||||
if (this.id) {
|
||||
this.model.getIgnoresAndIgnoredBy(this.id)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
this.ignores = result.ignores;
|
||||
this.ignoredBy = result.ignoredBy;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
fetchHidden() {
|
||||
if (this.id) {
|
||||
this.model.getAccountHidden(this.id)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
this.hidden = result.hidden;
|
||||
this.hiddenBy = result.hiddenBy;
|
||||
this.permaHidden = result.permaHidden;
|
||||
this.permaHiddenBy = result.permaHiddenBy;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
fetchFriends() {
|
||||
if (this.id) {
|
||||
this.model.getAccountFriends(this.id)
|
||||
.then(result => this.friends = result);
|
||||
}
|
||||
}
|
||||
removeFriend(friendId: string) {
|
||||
if (this.id && confirm('Are you sure ?')) {
|
||||
this.model.removeFriend(this.id, friendId)
|
||||
.then(() => this.friends && removeItem(this.friends, friendId));
|
||||
}
|
||||
}
|
||||
canToggleRole(role: string) {
|
||||
return role !== 'superadmin' && hasRole(this.model.account, 'superadmin');
|
||||
}
|
||||
hasRole(role: string) {
|
||||
return hasRole(this.account, role);
|
||||
}
|
||||
toggleRole(role: string) {
|
||||
if (this.id) {
|
||||
this.model.setRole(this.id, role, !this.hasRole(role));
|
||||
}
|
||||
}
|
||||
showAccountData() {
|
||||
if (this.id) {
|
||||
this.model.getAccount(this.id)
|
||||
.then(account => {
|
||||
(window as any).$data = account;
|
||||
console.log(account);
|
||||
console.log('accessible in $data');
|
||||
});
|
||||
}
|
||||
}
|
||||
printAuthList() {
|
||||
this.forAuths(auths => {
|
||||
const authList = auths.map((a, i) => `${i + 1}. [${a.provider}] ${a.name || '<no name>'}`).join('\n');
|
||||
this.authData = authList;
|
||||
this.authDataAccount = this.account ? `${this.account.name} [${this.account._id}]` : '';
|
||||
console.log(authList);
|
||||
});
|
||||
}
|
||||
printAuthData() {
|
||||
this.forAuths(auths => {
|
||||
const authData = auths.map((a, i) => `${i + 1}. [${a._id}] [${a.provider}] ${a.name || '<no name>'} (${a.emails})`).join('\n');
|
||||
this.authData = authData;
|
||||
this.authDataAccount = this.account ? `${this.account.name} [${this.account._id}]` : '';
|
||||
console.log(authData);
|
||||
});
|
||||
}
|
||||
copyAuthData() {
|
||||
if ('clipboard' in navigator) {
|
||||
(navigator as any).clipboard.writeText(this.authData);
|
||||
}
|
||||
}
|
||||
clearAuthData() {
|
||||
this.authData = undefined;
|
||||
this.authDataAccount = undefined;
|
||||
}
|
||||
fetchAuths() {
|
||||
this.forAuths(auths => {
|
||||
(window as any).$auths = auths;
|
||||
console.log(auths);
|
||||
});
|
||||
}
|
||||
private forAuths(callback: (auths: Auth[]) => void) {
|
||||
if (this.account) {
|
||||
this.model.getAuthsForAccount(this.account._id)
|
||||
.then(auths => callback(auths || []));
|
||||
}
|
||||
}
|
||||
printJSON() {
|
||||
if (this.id) {
|
||||
this.model.getAccount(this.id)
|
||||
.then(account => this.rawData = JSON.stringify(account, undefined, 2));
|
||||
}
|
||||
}
|
||||
clearOrigins(old: boolean, singles: boolean) {
|
||||
if (this.id) {
|
||||
this.model.clearOriginsForAccount(this.id, { old, singles })
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
}
|
||||
clearOriginsInRegion(country: string) {
|
||||
if (this.id) {
|
||||
this.model.clearOriginsForAccount(this.id, { country })
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
}
|
||||
clearOriginsFromDuplicates() {
|
||||
if (this.account) {
|
||||
const duplicates = (this.duplicates || []).slice(0, this.account.duplicatesLimit || 0);
|
||||
const accounts = duplicates.map(d => d.account);
|
||||
this.model.clearOriginsForAccounts(accounts, { old: true })
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
}
|
||||
removeOrigin(ip: string) {
|
||||
if (this.id) {
|
||||
this.model.removeOriginsForAccount(this.id, [ip])
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
}
|
||||
merge(accountId: string) {
|
||||
if (this.account && confirm('Are you sure?')) {
|
||||
this.model.mergeAccounts(this.account._id, accountId)
|
||||
.then(() => remove(this.duplicates || [], d => d.account === accountId))
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
}
|
||||
remove() {
|
||||
if (this.account && confirm('Are you sure?')) {
|
||||
this.model.removeAccount(this.account._id);
|
||||
}
|
||||
}
|
||||
translateUrl(text: string) {
|
||||
return getTranslationUrl(text);
|
||||
}
|
||||
getPoniesCreators() {
|
||||
if (this.account) {
|
||||
this.model.getPoniesCreators(this.account._id)
|
||||
.then(items => {
|
||||
if (items) {
|
||||
items.sort(compareByName);
|
||||
(window as any).$ponies = items;
|
||||
console.log(items.map(i => `[${i._id}] "${i.name}" ${i.creator}`).join('\n'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
removePoniesAboveLimit() {
|
||||
if (this.account && confirm('Are you sure?')) {
|
||||
this.model.removePoniesAboveLimit(this.account._id);
|
||||
}
|
||||
}
|
||||
removeAllPonies() {
|
||||
if (this.account && confirm('Are you sure?')) {
|
||||
this.model.removeAllPonies(this.account._id);
|
||||
}
|
||||
}
|
||||
restorePonies() {
|
||||
if (this.account && this.poniesToRestore) {
|
||||
let ids: string[] | undefined = undefined;
|
||||
|
||||
if (this.poniesToRestoreFilter) {
|
||||
const matches = Array.from(this.poniesToRestoreFilter.match(/\[[a-f0-9]{24}\]/g) || [])
|
||||
.map(id => id.substr(1, 24));
|
||||
|
||||
if (matches.length) {
|
||||
ids = matches;
|
||||
}
|
||||
}
|
||||
|
||||
this.model.restorePonies(this.account._id, this.poniesToRestore, ids);
|
||||
this.poniesToRestore = '';
|
||||
this.restoringPonies = false;
|
||||
}
|
||||
}
|
||||
removeEmail(email: string) {
|
||||
if (this.account && confirm('Are you sure?')) {
|
||||
this.model.removeEmail(this.account._id, email);
|
||||
}
|
||||
}
|
||||
removeIgnore(account: string, ignoredAccount: string) {
|
||||
this.model.removeIgnore(account, ignoredAccount)
|
||||
.then(() => this.fetchIgnores());
|
||||
}
|
||||
highlighCharacter = (char: Character | undefined): boolean => {
|
||||
return !!char && includes(this.ponyNames, char.name.toLowerCase());
|
||||
}
|
||||
clearSessions() {
|
||||
if (this.account) {
|
||||
this.clearingSessions = true;
|
||||
this.model.clearSessions(this.account._id)
|
||||
.finally(() => this.clearingSessions = false);
|
||||
}
|
||||
}
|
||||
getMergeTooltip(merge: MergeInfo) {
|
||||
function mergeInfo(title: string, account: MergeAccountData) {
|
||||
return [
|
||||
title,
|
||||
`\tname: ${account.name}`,
|
||||
`\tnote: ${account.note || ''}`,
|
||||
`\tflags: ${flagsToString(account.flags, accountFlags)}`,
|
||||
`\tage: ${account.birthdate ? getAge(account.birthdate) : '-'}`,
|
||||
`\tfriends: ${account.friends ? account.friends.length : 0}`,
|
||||
`\tcounters:`,
|
||||
...Object.keys(account.counters || {}).sort().map(key => `\t\t${key}: ${(account.counters as any)[key]}`),
|
||||
`\tstate:`,
|
||||
...(account.state ? JSON.stringify(account.state, null, 2).split(/\n/).map(x => `\t\t${x}`) : []),
|
||||
`\temails:`,
|
||||
...account.emails.map(e => `\t\t${e}`),
|
||||
`\tauths:`,
|
||||
...account.auths.map(a => `\t\t[${a.id}] ${a.name}`),
|
||||
`\tcharacters: ${account.characters.length}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
if (merge.data) {
|
||||
return `${mergeInfo('ACCOUNT', merge.data.account)}\n\n${mergeInfo('MERGED', merge.data.merge)}`;
|
||||
} else {
|
||||
return '<empty>';
|
||||
}
|
||||
}
|
||||
printMerge(merge: MergeInfo) {
|
||||
console.log(this.mergeInfo(merge));
|
||||
}
|
||||
printMerge2(merge: MergeInfo) {
|
||||
console.log(this.mergeInfo2(merge));
|
||||
}
|
||||
mergeInfo(merge: MergeInfo) {
|
||||
function mergeInfo(title: string, account: MergeAccountData) {
|
||||
return [
|
||||
title,
|
||||
`\tname: ${account.name}`,
|
||||
`\tnote: ${account.note || ''}`,
|
||||
`\tflags: ${flagsToString(account.flags, accountFlags)}`,
|
||||
`\tage: ${account.birthdate ? getAge(account.birthdate) : '-'}`,
|
||||
`\tcounters:`,
|
||||
...Object.keys(account.counters || {}).sort().map(key => `\t\t${key}: ${(account.counters as any)[key]}`),
|
||||
`\tstate:`,
|
||||
...(account.state ? JSON.stringify(account.state, null, 2).split(/\n/).map(x => `\t\t${x}`) : []),
|
||||
`\temails:`,
|
||||
...account.emails.map(e => `\t\t${e}`),
|
||||
`\tauths:`,
|
||||
...account.auths.map(a => `\t\t[${a.id}] ${a.name}`),
|
||||
`\tcharacters:`,
|
||||
...account.characters.map(a => `\t\t[${a.id}] ${a.name}`),
|
||||
`\tfriends:`,
|
||||
...(account.friends || []).map(i => `\t\t[${i}]`),
|
||||
`\tignores:`,
|
||||
...(account.ignores || []).map(i => `\t\t[${i}]`),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return merge.data && `${mergeInfo('ACCOUNT', merge.data.account)}\n\n${mergeInfo('MERGED', merge.data.merge)}`;
|
||||
}
|
||||
private mergeInfo2(merge: MergeInfo) {
|
||||
function mergeInfo(title: string, account: MergeAccountData) {
|
||||
return [
|
||||
title,
|
||||
`\tname: ${JSON.stringify(account.name)}`,
|
||||
`\tnote: ${JSON.stringify(account.note)}`,
|
||||
`\tflags: ${JSON.stringify(account.flags)}`,
|
||||
`\tcounters: ${JSON.stringify(account.counters)}`,
|
||||
`\tstate: ${JSON.stringify(account.state)}`,
|
||||
`\temails: ${JSON.stringify(account.emails)}`,
|
||||
`\tauths: ${JSON.stringify(account.auths)}`,
|
||||
`\tcharacters: ${JSON.stringify(account.characters)}`,
|
||||
`\tfriends: ${JSON.stringify(account.friends)}`,
|
||||
`\tignores: ${JSON.stringify(account.ignores)}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return merge.data && `${mergeInfo('ACCOUNT', merge.data.account)}\n\n${mergeInfo('MERGED', merge.data.merge)}`;
|
||||
}
|
||||
showMergeInNewTab(merge: MergeInfo) {
|
||||
showTextInNewTab(this.mergeInfo(merge) || '');
|
||||
}
|
||||
showMergeInNewTab2(merge: MergeInfo) {
|
||||
showTextInNewTab(this.mergeInfo2(merge) || '');
|
||||
}
|
||||
unmerge(mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData) {
|
||||
if (this.account) {
|
||||
this.model.unmergeAccounts(this.account._id, mergeId, split, keep)
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
}
|
||||
setAge(age: number) {
|
||||
if (this.account) {
|
||||
this.model.setAge(this.account._id, age);
|
||||
}
|
||||
}
|
||||
addEmail() {
|
||||
if (this.account) {
|
||||
const email = prompt('enter email');
|
||||
|
||||
if (email && /@/.test(email)) {
|
||||
this.model.addEmail(this.account._id, email);
|
||||
}
|
||||
}
|
||||
}
|
||||
private update() {
|
||||
if (this.id) {
|
||||
this.accountSubscription && this.accountSubscription.unsubscribe();
|
||||
this.accountSubscription = this.id ? this.model.accounts.subscribe(this.id, a => this.setAccount(a)) : undefined;
|
||||
|
||||
this.events = this.model.events
|
||||
.filter(e => e.account === this.id)
|
||||
.slice(0, 10);
|
||||
}
|
||||
}
|
||||
private setAccount(account: Account | undefined) {
|
||||
const theSame = this.account === account || (this.account && account && this.account._id === account._id);
|
||||
this.account = account;
|
||||
|
||||
if (!theSame) {
|
||||
this.duplicates = [];
|
||||
this.ponyNames = [];
|
||||
this.ignores = [];
|
||||
this.ignoredBy = [];
|
||||
this.rawData = undefined;
|
||||
this.authLimit = defaultLimit;
|
||||
this.emailLimit = defaultLimit;
|
||||
this.duplicatesLimit = defaultDuplicatesLimit;
|
||||
this.refresh();
|
||||
this.friendsLimit = defaultLimit;
|
||||
this.hiddenByLimit = defaultLimit;
|
||||
this.hiddenLimit = defaultLimit;
|
||||
this.permaHiddenByLimit = defaultLimit;
|
||||
this.permaHiddenLimit = defaultLimit;
|
||||
(window as any).$account = account;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
ng-template(#searchTooltip)
|
||||
.text-left.help-tooltip
|
||||
div: em search by:
|
||||
div name:<account_name> #[em - account name]
|
||||
div note:<note_text> #[em - note contents]
|
||||
div email:<email_fragment> #[em - email fragment]
|
||||
div role:<role_name> #[em - role]
|
||||
div exact:<name> #[em - exact account name]
|
||||
div ignores:<count> #[em - amount of ignores (at least)]
|
||||
div ponies:<count> #[em - amount of ponies (at least)]
|
||||
div auths:<count> #[em - amount of auths (at least)]
|
||||
div old:<number_of_days> #[em - days since last visit]
|
||||
div disabled! #[em - list accounts with disabled auths]
|
||||
div locked! #[em - list accounts with locked auths]
|
||||
div spam/swering/timeouts/limits:<number>
|
||||
|
||||
ng-template(#duplicatesPopover)
|
||||
a.text-secondary.d-block(*ngFor="let e of duplicateEntries" (click)="search = e")
|
||||
| {{e}}
|
||||
|
||||
.d-block.d-sm-flex
|
||||
input.form-control.form-control-sm.admin-search(type="search" placeholder="search" [(ngModel)]="search")
|
||||
.btn-group.btn-group-sm.ml-1
|
||||
button.btn.btn-default((click)="refresh(true)")
|
||||
fa-icon([icon]="syncIcon" size="lg")
|
||||
button.btn(btnCheckbox [(ngModel)]="autoRefresh" [btnHighlight]="autoRefresh")
|
||||
| auto
|
||||
.btn-group.btn-group-sm.ml-1
|
||||
button.btn.btn-sm.btn-danger(
|
||||
*ngIf="duplicateEntries?.length" [popover]="duplicatesPopover" [outsideClick]="true"
|
||||
placement="bottom" containerClass="popover-wide")
|
||||
| Duplicates ({{duplicateEntries.length}})
|
||||
button.btn.btn-sm.btn-danger((click)="refreshDuplicates()" title="Refresh duplicates")
|
||||
fa-icon([icon]="syncIcon" size="lg")
|
||||
.btn-group.btn-group-sm.ml-1(btnRadioGroup [(ngModel)]="showOnly")
|
||||
button.btn(*ngFor="let f of filters" [btnRadio]="f" [btnHighlight]="showOnly === f") {{f}}
|
||||
button.btn.btn-sm.ml-1((click)="not = !not" [btnHighlight]="not") not
|
||||
button.btn.btn-sm.btn-default.ml-2((click)="createAccount()") create account
|
||||
|
||||
.d-block.d-sm-flex.justify-content-between.align-items-start
|
||||
.pl-1([tooltip]="searchTooltip" placement="bottom" containerClass="tooltip-pre" style="width: 280px;")
|
||||
small.text-muted
|
||||
fa-icon.mr-1([icon]="filterIcon")
|
||||
| filters
|
||||
.d-block.d-sm-flex.mt-2
|
||||
fa-icon.text-muted.mr-3(*ngIf="loading" [icon]="spinnerIcon" [spin]="true" size="2x" style="margin-top: 3px")
|
||||
strong.text-muted.mr-3(style="margin-top: 6px")
|
||||
| {{totalItems === 99999999 ? '' : totalItems}}
|
||||
pagination(
|
||||
[totalItems]="totalItems" [itemsPerPage]="itemsPerPage" [(ngModel)]="currentPage"
|
||||
[maxSize]="15" [directionLinks]="false" [boundaryLinks]="true")
|
||||
|
||||
table.table.table-fixed.table-striped.table-hover.table-sm
|
||||
thead
|
||||
tr
|
||||
th.col-time created
|
||||
th.col-time last visit
|
||||
th.col-account name
|
||||
th.col-email emails
|
||||
th.col-auth auths
|
||||
th.col-origin origins
|
||||
th ponies
|
||||
th(style="width: 40px;")
|
||||
tbody
|
||||
tr(*ngFor="let a of itemsOnPageIds")
|
||||
td.col-time
|
||||
time-field([time]="info.account?.createdAt")
|
||||
td.col-time
|
||||
time-field([time]="info.account?.lastVisit")
|
||||
td.col-account
|
||||
account-info-remote(#info [accountId]="a")
|
||||
td.col-email
|
||||
email-list([emails]="info.account?.emails")
|
||||
td.col-auth
|
||||
auth-list-remote([accountId]="a")
|
||||
td.col-origin
|
||||
origin-list-remote([accountId]="a")
|
||||
td
|
||||
pony-list-remote([accountId]="a")
|
||||
td(style="width: 40px;")
|
||||
.btn-group.btn-group-xs
|
||||
button.btn.btn-xs.btn-default(
|
||||
(click)="$event.shiftKey ? chatLog.add(info.account) : chatLog.show(info.account)" title="Show chat")
|
||||
fa-icon([icon]="commentIcon" [fixedWidth]="true")
|
||||
|
||||
admin-chat-log(#chatLog)
|
||||
|
||||
.admin-bottom-padding
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { debounce } from 'lodash';
|
||||
import { Account } from '../../../common/adminInterfaces';
|
||||
import { AdminModel } from '../../services/adminModel';
|
||||
import { faSync, faComment, faFilter, faEraser, faSpinner } from '../../../client/icons';
|
||||
|
||||
let autoRefresh = false;
|
||||
let showOnly = 'all';
|
||||
let search = '';
|
||||
let currentPage = 0;
|
||||
let not = false;
|
||||
|
||||
@Component({
|
||||
selector: 'admin-accounts',
|
||||
templateUrl: 'admin-accounts.pug',
|
||||
})
|
||||
export class AdminAccounts implements OnInit {
|
||||
readonly syncIcon = faSync;
|
||||
readonly filterIcon = faFilter;
|
||||
readonly commentIcon = faComment;
|
||||
readonly eraserIcon = faEraser;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
readonly filters = [
|
||||
'all',
|
||||
'banned',
|
||||
'timed out',
|
||||
'with flags',
|
||||
'notes',
|
||||
'supporters',
|
||||
];
|
||||
totalItems = 99999999;
|
||||
itemsOnPageIds: string[] = [];
|
||||
itemsPerPage = 20;
|
||||
loading = false;
|
||||
private expanded = new Set<string>();
|
||||
constructor(public model: AdminModel, private router: Router) {
|
||||
}
|
||||
get showOnly() {
|
||||
return showOnly;
|
||||
}
|
||||
set showOnly(value) {
|
||||
if (showOnly !== value) {
|
||||
showOnly = value;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
get not() {
|
||||
return not;
|
||||
}
|
||||
set not(value) {
|
||||
if (not !== value) {
|
||||
not = value;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
get autoRefresh() {
|
||||
return autoRefresh;
|
||||
}
|
||||
set autoRefresh(value) {
|
||||
autoRefresh = value;
|
||||
}
|
||||
get currentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
set currentPage(value) {
|
||||
if (currentPage !== value) {
|
||||
currentPage = value;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
get search() {
|
||||
return search;
|
||||
}
|
||||
set search(value) {
|
||||
if (search !== value) {
|
||||
search = value;
|
||||
this.execSearch();
|
||||
}
|
||||
}
|
||||
private execSearch = debounce(() => this.refresh(), 500);
|
||||
get duplicateEntries() {
|
||||
return this.model.duplicateEntries;
|
||||
}
|
||||
refreshDuplicates() {
|
||||
this.model.checkDuplicateEntries(true);
|
||||
}
|
||||
limit(account: Account) {
|
||||
return this.expanded.has(account._id) ? 99999 : 2;
|
||||
}
|
||||
expand(account: Account) {
|
||||
this.expanded.add(account._id);
|
||||
}
|
||||
ngOnInit() {
|
||||
this.model.accountPromise
|
||||
.then(() => this.refresh());
|
||||
}
|
||||
refresh(force = false) {
|
||||
this.loading = true;
|
||||
this.model.findAccounts({
|
||||
search: this.search.trim(),
|
||||
not: this.not,
|
||||
showOnly: this.showOnly,
|
||||
page: this.currentPage - 1,
|
||||
itemsPerPage: this.itemsPerPage,
|
||||
force,
|
||||
}).then(result => {
|
||||
if (result && result.page === (this.currentPage - 1)) {
|
||||
this.totalItems = result.totalItems;
|
||||
this.itemsOnPageIds = result.accounts;
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
createAccount() {
|
||||
const name = prompt('enter new account name');
|
||||
|
||||
if (name) {
|
||||
this.model.createAccount(name)
|
||||
.then(id => this.router.navigate(['accounts', id]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
ng-template(#duplicatesPopover)
|
||||
div(*ngFor="let e of duplicateEntries")
|
||||
| {{e}}
|
||||
|
||||
.d-block.d-sm-flex.justify-content-between.align-items-center
|
||||
.form-inline
|
||||
input.form-control.form-control-sm.admin-search-inline.d-none.d-sm-inline-block(
|
||||
type="search" placeholder="search" [(ngModel)]="search" disabled)
|
||||
|
||||
button.btn.btn-sm.ml-1((click)="toggleNotifications()" [btnHighlight]="notifications" title="Toggle notifications")
|
||||
fa-icon([icon]="bellIcon" size="lg")
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="cleanupDeleted()" title="Cleanup deleted events")
|
||||
fa-icon.mr-1([icon]="syncIcon" size="lg")
|
||||
| clear
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="removeEvents(1000 * 60 * 10)" title="Delete all older than 10 minutes")
|
||||
fa-icon.mr-1([icon]="clockIcon" size="lg")
|
||||
| 10min
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="removeEvents(1000 * 3)" title="Delete all events")
|
||||
fa-icon.mr-1([icon]="trashIcon" size="lg")
|
||||
| all
|
||||
button.btn.btn-sm.btn-default.ml-1((click)="showChat()" title="Show chatlog")
|
||||
fa-icon.mr-1([icon]="commentsIcon" size="lg")
|
||||
|
||||
.btn-group.ml-1
|
||||
button.btn.btn-sm.btn-danger(
|
||||
*ngIf="duplicateEntries?.length" [popover]="duplicatesPopover" placement="bottom" containerClass="popover-wide")
|
||||
| duplicates ({{duplicateEntries.length}})
|
||||
button.btn.btn-sm.btn-danger((click)="refreshDuplicates()" title="Refresh duplicates")
|
||||
fa-icon([icon]="syncIcon" size="lg")
|
||||
|
||||
span.text-muted.d-none.d-sm-inline
|
||||
span.ml-3(title="Used disk space" [class.text-danger]="isLowDiskSpace")
|
||||
fa-icon.mr-2([icon]="hddIcon")
|
||||
| {{status.diskSpace}}
|
||||
span.ml-3(title="Memory usage" [class.text-danger]="isLowMemory")
|
||||
fa-icon.mr-2([icon]="ramIcon")
|
||||
| {{status.memoryUsage}}
|
||||
span.ml-3(title="Certificate expires in" [class.text-danger]="isOldCertificate")
|
||||
fa-icon.mr-2([icon]="certificateIcon")
|
||||
from-now([time]="status.certificateExpiration")
|
||||
span.ml-3(title="Last Patreon update" [class.text-danger]="isOldPatreon")
|
||||
fa-icon.mr-2([icon]="patreonIcon")
|
||||
from-now([time]="status.lastPatreonUpdate")
|
||||
|
||||
pagination(
|
||||
[totalItems]="filtered.length" [itemsPerPage]="itemsPerPage" [maxSize]="15" [(ngModel)]="currentPage"
|
||||
[directionLinks]="false")
|
||||
|
||||
events-table(
|
||||
[events]="filteredOnPage" (showChat)="showChat($event)" (addChat)="addChat($event)"
|
||||
(removedEvent)="removedEvent($event)")
|
||||
|
||||
admin-chat-log(#chatLog)
|
||||
|
||||
.admin-bottom-padding
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core';
|
||||
import { fromNow } from '../../../common/utils';
|
||||
import { Event, ChatEvent } from '../../../common/adminInterfaces';
|
||||
import { AdminModel } from '../../services/adminModel';
|
||||
import { BaseTable, BaseTableState } from '../base-table';
|
||||
import { AdminChatLog } from '../shared/admin-chat-log/admin-chat-log';
|
||||
import {
|
||||
faBell, faSync, faClock, faTrash, faComments, faHdd, faMicrochip, faCertificate, faClone, faPatreon
|
||||
} from '../../../client/icons';
|
||||
|
||||
let state: BaseTableState;
|
||||
|
||||
@Component({
|
||||
selector: 'admin-events',
|
||||
templateUrl: 'admin-events.pug',
|
||||
})
|
||||
export class AdminEvents extends BaseTable<Event> implements OnInit, OnDestroy {
|
||||
readonly bellIcon = faBell;
|
||||
readonly syncIcon = faSync;
|
||||
readonly clockIcon = faClock;
|
||||
readonly trashIcon = faTrash;
|
||||
readonly commentsIcon = faComments;
|
||||
readonly hddIcon = faHdd;
|
||||
readonly ramIcon = faMicrochip;
|
||||
readonly certificateIcon = faCertificate;
|
||||
readonly duplicateIcon = faClone;
|
||||
readonly patreonIcon = faPatreon;
|
||||
@ViewChild('chatLog', { static: true }) chatLog!: AdminChatLog;
|
||||
private chatEvent?: Event;
|
||||
constructor(private model: AdminModel) {
|
||||
super();
|
||||
}
|
||||
get status() {
|
||||
return this.model.state.status;
|
||||
}
|
||||
get isLowDiskSpace() {
|
||||
return this.model.isLowDiskSpace;
|
||||
}
|
||||
get isLowMemory() {
|
||||
return this.model.isLowMemory;
|
||||
}
|
||||
get isOldCertificate() {
|
||||
return this.model.isOldCertificate;
|
||||
}
|
||||
get isOldPatreon() {
|
||||
return this.model.isOldPatreon;
|
||||
}
|
||||
get items() {
|
||||
return this.model.events;
|
||||
}
|
||||
get duplicateEntries() {
|
||||
return this.model.duplicateEntries;
|
||||
}
|
||||
get notifications() {
|
||||
return this.model.notifications;
|
||||
}
|
||||
ngOnInit() {
|
||||
this.model.updated = () => this.updateItems();
|
||||
this.setState(state);
|
||||
this.updateItems();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.model.updated = () => { };
|
||||
}
|
||||
cleanupDeleted() {
|
||||
this.model.cleanupDeletedEvents();
|
||||
}
|
||||
refreshDuplicates() {
|
||||
this.model.checkDuplicateEntries(true);
|
||||
}
|
||||
removeEvents(olderThan: number) {
|
||||
const date = fromNow(-olderThan);
|
||||
const oldEvents = this.items.filter(e => e.updatedAt.getTime() < date.getTime());
|
||||
return Promise.all(oldEvents.map(e => this.model.removeEvent(e._id).then(() => this.removedEvent(e))));
|
||||
}
|
||||
showChat(e?: ChatEvent) {
|
||||
this.chatEvent = e && e.event;
|
||||
this.chatLog.show(e && e.account);
|
||||
}
|
||||
addChat(e: ChatEvent) {
|
||||
if (e.account) {
|
||||
this.chatLog.add(e.account);
|
||||
}
|
||||
}
|
||||
removedEvent(e: Event) {
|
||||
if (this.chatEvent === e) {
|
||||
this.chatLog.close();
|
||||
}
|
||||
}
|
||||
toggleNotifications() {
|
||||
this.model.toggleNotifications();
|
||||
}
|
||||
protected onChange() {
|
||||
state = this.getState();
|
||||
}
|
||||
protected updatePage() {
|
||||
super.updatePage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
.row(style="margin-top: 100px;")
|
||||
.col-md-6
|
||||
h2 {{ip}} [{{info.origin?.country}}]
|
||||
|
||||
div(style="margin-bottom: 10px;")
|
||||
origin-info-remote(#info [originIP]="ip")
|
||||
|
||||
div
|
||||
a([href]="whoisHref" target="_blank" rel="noopener noreferrer")
|
||||
| show whois info
|
||||
|
||||
div
|
||||
span.text-muted created:
|
||||
from-now.ml-1([time]="info.origin?.createdAt")
|
||||
|
||||
div
|
||||
span.text-muted updated:
|
||||
from-now.ml-1([time]="info.origin?.updatedAt")
|
||||
|
||||
.col-md-6
|
||||
h3 Accounts #[span.text-muted.ml-1 ({{accounts.length}})]
|
||||
|
||||
div(*ngFor="let a of accounts | slice:0:50")
|
||||
account-info-remote([accountId]="a")
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
h3 Events
|
||||
events-table([events]="events" (showChat)="chatLog.show($event.account)")
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
admin-chat-log(#chatLog)
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Event } from '../../../common/adminInterfaces';
|
||||
import { AdminModel } from '../../services/adminModel';
|
||||
|
||||
@Component({
|
||||
selector: 'admin-origin-details',
|
||||
templateUrl: 'admin-origin-details.pug',
|
||||
})
|
||||
export class AdminOriginDetails implements OnInit {
|
||||
events?: Event[];
|
||||
accounts: string[] = [];
|
||||
ip?: string;
|
||||
constructor(private route: ActivatedRoute, private model: AdminModel) {
|
||||
}
|
||||
get whoisHref() {
|
||||
return `http://whois.urih.com/record/${this.ip}/`;
|
||||
}
|
||||
ngOnInit() {
|
||||
this.route.params.forEach(p => {
|
||||
this.ip = p['ip'];
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
private update() {
|
||||
this.accounts = [];
|
||||
|
||||
if (this.model.connected && this.ip) {
|
||||
this.model.getAccountsByOrigin(this.ip)
|
||||
.then(accounts => this.accounts = accounts || []);
|
||||
this.events = this.model.events
|
||||
.filter(e => e.origin && e.origin.ip === this.ip)
|
||||
.slice(0, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
.text-center.pt-5(*ngIf="!stats")
|
||||
fa-icon.text-muted([icon]="spinnerIcon" [fixedWidth]="true" [spin]="true" size="3x")
|
||||
|
||||
.row(*ngIf="stats")
|
||||
.col-md-6
|
||||
h3
|
||||
| Origins statistics
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="update()")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.stat-field Total origin count: #[b {{stats.totalOrigins}}]
|
||||
.stat-field Total origin count (IP4 / IP6): #[b {{stats.totalOriginsIP4}} / {{stats.totalOriginsIP6}}]
|
||||
.stat-field.mt-3 Unique origins: #[b {{stats.uniqueOrigins}}]
|
||||
.stat-field Multiple account origins: #[b {{stats.duplicateOrigins}}]
|
||||
.stat-field Single account origins: #[b {{stats.singleOrigins}}]
|
||||
|
||||
h3.mt-3
|
||||
| Ignores statistics
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="update()")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.stat-field Total ignore count: #[b {{other?.totalIgnores}}]
|
||||
|
||||
h3.mt-3
|
||||
| Other statistics
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="update()")
|
||||
fa-icon([icon]="syncIcon" [fixedWidth]="true")
|
||||
|
||||
.stat-field Auths with empty account: #[b {{other?.authsWithEmptyAccount}}]
|
||||
.stat-field Auths with missing account: #[b {{other?.authsWithMissingAccount}}]
|
||||
|
||||
.col-md-6
|
||||
h3 Origins distribution
|
||||
|
||||
table.table.table-sm(style="width: auto;")
|
||||
thead
|
||||
tr
|
||||
th origins
|
||||
th accounts
|
||||
th
|
||||
tbody
|
||||
tr(*ngFor="let d of stats.distribution; let i = index")
|
||||
td {{i}}
|
||||
td {{d}}
|
||||
td
|
||||
button.btn.btn-xs.btn-default((click)="clear(i)" [disabled]="pending" title="Clear old singles in this row")
|
||||
fa-icon([icon]="eraserIcon" [fixedWidth]="true")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="clear(i, true)" [disabled]="pending" title="Clear old singles in or below this row")
|
||||
fa-icon([icon]="eraserIcon" [fixedWidth]="true")
|
||||
fa-icon([icon]="chevronDownIcon" [fixedWidth]="true")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="clearSingles(i)" [disabled]="pending" title="Clear singles in or below this row")
|
||||
fa-icon([icon]="userIcon" [fixedWidth]="true")
|
||||
fa-icon([icon]="chevronDownIcon" [fixedWidth]="true")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="clearOld(i)" [disabled]="pending" title="Clear very old (3 months) in or below this row")
|
||||
fa-icon([icon]="clockIcon" [fixedWidth]="true")
|
||||
fa-icon([icon]="chevronDownIcon" [fixedWidth]="true")
|
||||
button.btn.btn-xs.btn-default.ml-1((click)="clearTo10(i)" [disabled]="pending" title="Trim origins to 10 most recent")
|
||||
b 10
|
||||
fa-icon([icon]="chevronDownIcon" [fixedWidth]="true")
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RequestStats, OriginStats, OtherStats } from '../../../common/adminInterfaces';
|
||||
import { faSync, faEraser, faClock, faUser, faChevronDown, faSpinner } from '../../../client/icons';
|
||||
import { AdminModel } from '../../services/adminModel';
|
||||
|
||||
@Component({
|
||||
selector: 'admin-origins',
|
||||
templateUrl: 'admin-origins.pug',
|
||||
})
|
||||
export class AdminOrigins implements OnInit {
|
||||
readonly syncIcon = faSync;
|
||||
readonly eraserIcon = faEraser;
|
||||
readonly clockIcon = faClock;
|
||||
readonly userIcon = faUser;
|
||||
readonly chevronDownIcon = faChevronDown;
|
||||
readonly spinnerIcon = faSpinner;
|
||||
stats?: OriginStats;
|
||||
other?: OtherStats;
|
||||
requestStats: RequestStats[] = [];
|
||||
pending = false;
|
||||
constructor(public model: AdminModel) {
|
||||
}
|
||||
ngOnInit() {
|
||||
if (this.model.connected) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
update() {
|
||||
this.model.getOriginStats().then(stats => this.stats = stats);
|
||||
this.model.getOtherStats().then(stats => this.other = stats);
|
||||
}
|
||||
clear(count: number, andHigher = false) {
|
||||
this.clearAll(count, andHigher, true, true, false);
|
||||
}
|
||||
clearOld(count: number) {
|
||||
this.clearAll(count, true, true, false, false);
|
||||
}
|
||||
clearSingles(count: number) {
|
||||
this.clearAll(count, true, false, true, false);
|
||||
}
|
||||
clearTo10(count: number) {
|
||||
this.clearAll(count, true, false, true, true);
|
||||
}
|
||||
clearAll(count: number, andHigher: boolean, old: boolean, singles: boolean, trim: boolean) {
|
||||
this.pending = true;
|
||||
|
||||
return this.model.clearOrigins(count, andHigher, { old, singles, trim })
|
||||
.finally(() => {
|
||||
this.pending = false;
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
.row
|
||||
.col-6
|
||||
.form
|
||||
.form-group
|
||||
h3 Notifications
|
||||
|
||||
.form
|
||||
.form-group(*ngFor="let field of fields")
|
||||
label {{field.title}}
|
||||
textarea.form-control([(ngModel)]="field.value" rows="4" style="width: 400px;")
|
||||
.form-group
|
||||
button.btn.btn-primary(type="button" (click)="saveFields()") Save
|
||||
button.btn.btn-default.ml-1(type="button" (click)="resetFields()") Reset
|
||||
|
||||
.col-6
|
||||
.form
|
||||
.form-group
|
||||
h3 Patreon
|
||||
.form-group.d-flex
|
||||
button.btn.btn-default((click)="updatePatreon()")
|
||||
| Update Data
|
||||
.input-group.ml-1(style="width: 400px")
|
||||
input.form-control(#patreonToken placeholder="patreon token")
|
||||
.input-group-append
|
||||
button.btn.btn-warning((click)="updatePatreonToken(patreonToken.value); patreonToken.value = ''")
|
||||
| Update Token
|
||||
.form-group
|
||||
| To get new token, got to #[a(href="https://www.patreon.com/portal/registration/register-clients") My Clients],
|
||||
| refresh token and update patreon token field with "Creator's Access Token" from the site.
|
||||
.form-group
|
||||
button.btn.btn-default((click)="getLastPatreonData()")
|
||||
| Get last patreon data
|
||||
|
||||
.form
|
||||
.form-group
|
||||
h3 Other
|
||||
.form-group
|
||||
button.btn.btn-default((click)="updatePastSupporters()")
|
||||
| Update past supporters
|
||||
|
||||
.row
|
||||
.col-12
|
||||
.form
|
||||
.form-group
|
||||
label Suspicious pony info to report
|
||||
textarea.form-control([(ngModel)]="suspiciousPonies" rows="15")
|
||||
.form-group(*ngIf="suspiciousPoniesError")
|
||||
.alert.alert-danger
|
||||
| {{suspiciousPoniesError}}
|
||||
.form-group
|
||||
button.btn.btn-primary((click)="saveSuspiciousPonies()") Save
|
||||
button.btn.btn-default.ml-1((click)="resetSuspiciousPonies()") Reset
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { compact } from 'lodash';
|
||||
import { AdminModel } from '../../services/adminModel';
|
||||
import { Account, GeneralSettings } from '../../../common/adminInterfaces';
|
||||
import { Subscription } from '../../../common/interfaces';
|
||||
import { showTextInNewTab } from '../../../client/htmlUtils';
|
||||
|
||||
interface Field {
|
||||
key: keyof GeneralSettings;
|
||||
title: string;
|
||||
value: string | undefined;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'admin-other',
|
||||
templateUrl: 'admin-other.pug',
|
||||
})
|
||||
export class AdminOther implements OnInit, OnDestroy {
|
||||
fields: Field[] = [
|
||||
{ key: 'suspiciousNames', title: 'Suspicious pony names & emails to report', value: undefined },
|
||||
{ key: 'suspiciousAuths', title: 'Suspicious auths to report', value: undefined },
|
||||
{ key: 'suspiciousMessages', title: 'Suspicious messages to report (instant)', value: undefined },
|
||||
{ key: 'suspiciousSafeMessages', title: 'Suspicious messages to report (safe only) (5+)', value: undefined },
|
||||
{ key: 'suspiciousSafeWholeMessages', title: 'Suspicious messages to report (safe only) (5+) (whole words)', value: undefined },
|
||||
{ key: 'suspiciousSafeInstantMessages', title: 'Suspicious messages to report (safe only) (instant)', value: undefined },
|
||||
{
|
||||
key: 'suspiciousSafeInstantWholeMessages',
|
||||
title: 'Suspicious messages to report (safe only) (whole words) (instant)',
|
||||
value: undefined
|
||||
},
|
||||
];
|
||||
max = 100;
|
||||
value = 0;
|
||||
error?: string;
|
||||
succeeded = false;
|
||||
suspiciousPonies?: string;
|
||||
suspiciousPoniesError?: string;
|
||||
ignoreErrors?: string;
|
||||
account: Account | undefined;
|
||||
private subscription?: Subscription;
|
||||
constructor(public model: AdminModel) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.model.accountPromise.then(() => {
|
||||
this.resetFields();
|
||||
this.resetSuspiciousPonies();
|
||||
});
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
saveFields() {
|
||||
let settings: any = {};
|
||||
this.fields.forEach(field => settings[field.key] = field.value);
|
||||
this.model.updateSettings(settings);
|
||||
}
|
||||
resetFields() {
|
||||
this.fields.map(field => {
|
||||
field.value = this.model.state.loginServers[0][field.key] as string | undefined;
|
||||
});
|
||||
}
|
||||
saveSuspiciousPonies() {
|
||||
this.suspiciousPoniesError = undefined;
|
||||
|
||||
try {
|
||||
compact(this.suspiciousPonies!.split(/\n/g).map(x => x.trim())).map(x => JSON.parse(x));
|
||||
this.model.updateSettings({ suspiciousPonies: this.suspiciousPonies });
|
||||
} catch (e) {
|
||||
this.suspiciousPoniesError = e.message;
|
||||
}
|
||||
}
|
||||
resetSuspiciousPonies() {
|
||||
this.suspiciousPonies = this.model.state.loginServers[0].suspiciousPonies;
|
||||
}
|
||||
updatePatreon() {
|
||||
this.model.server.updatePatreon();
|
||||
}
|
||||
updatePatreonToken(patreonToken: string) {
|
||||
this.model.updateSettings({ patreonToken });
|
||||
}
|
||||
getLastPatreonData() {
|
||||
this.model.getLastPatreonData()
|
||||
.then(data => {
|
||||
showTextInNewTab(JSON.stringify(data, null, 2));
|
||||
});
|
||||
}
|
||||
updatePastSupporters() {
|
||||
this.model.updatePastSupporters();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user