Archive commit

This commit is contained in:
Erik McClure
2019-08-28 21:15:02 -07:00
commit 735845746e
1435 changed files with 140724 additions and 0 deletions
+5
View File
@@ -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 });
+17
View File
@@ -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();
}
+8
View File
@@ -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 });
}
+5
View File
@@ -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 });
+13
View File
@@ -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 });
}
+676
View File
@@ -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';
}
+101
View File
@@ -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;
}
}
+304
View File
@@ -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}()`));
}
+34
View File
@@ -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}"`);
}
}
}
}
+502
View File
@@ -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];
}
}
+75
View File
@@ -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'] },
],
},
];
+73
View File
@@ -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');
}
}
+297
View File
@@ -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);
}
}
+194
View File
@@ -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;
});
}
+39
View File
@@ -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
+89
View File
@@ -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;
}
+49
View File
@@ -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;
}
+802
View File
@@ -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(() => { });
}
+174
View File
@@ -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;
}
+206
View File
@@ -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,
};
+160
View File
@@ -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;
}
+144
View File
@@ -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;
}
+170
View File
@@ -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;
}
}
}
+96
View File
@@ -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();
}
}
+87
View File
@@ -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);
}
}
}
+203
View File
@@ -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,
};
}
}
+44
View File
@@ -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;
}
+229
View File
@@ -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));
}
+33
View File
@@ -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 { }
+547
View File
@@ -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];
+790
View File
@@ -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);
}
}
}
}
+56
View File
@@ -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,
};
}
+155
View File
@@ -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;
}
}
+176
View File
@@ -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})`);
}
}
+12
View File
@@ -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`)}`;
}
+44
View File
@@ -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=/`;
};
+31
View File
@@ -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 };
}
+81
View File
@@ -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));
+602
View File
@@ -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;
}
+107
View File
@@ -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 [];
}
+136
View File
@@ -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();
}
+80
View File
@@ -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;
}
}
}
+923
View File
@@ -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>;
}
+570
View File
@@ -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)}`;
}
+135
View File
@@ -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);
}
}
}
}
+183
View File
@@ -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;
}
+20
View File
@@ -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);
}
+114
View File
@@ -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;
};
}
+132
View File
@@ -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;
// }
+306
View File
@@ -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');
}
}
+495
View File
@@ -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)
};
}
+173
View File
@@ -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;
}
}
+157
View File
@@ -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;
}
+624
View File
@@ -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);
}
+190
View File
@@ -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`,
];
+251
View File
@@ -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`,
};
+49
View File
@@ -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);
}
+152
View File
@@ -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;
}
+167
View File
@@ -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
+225
View File
@@ -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;
}
+12
View File
@@ -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';
+352
View File
@@ -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);
}
+295
View File
@@ -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]],
];
+164
View File
@@ -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
+122
View File
@@ -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;
}
+33
View File
@@ -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;
}
+850
View File
@@ -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);
}
};
};
}
+123
View File
@@ -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));
}
+138
View File
@@ -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],
]);
+678
View File
@@ -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);
}
}
+587
View File
@@ -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);
}
}
}
}
+70
View File
@@ -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);
}
+50
View File
@@ -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,
};
}
+202
View File
@@ -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;
}
}
}
}
}
}
}
}
}
}
+102
View File
@@ -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);
}
+140
View File
@@ -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;
}
+882
View File
@@ -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),
};
}
+90
View File
@@ -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
+54
View File
@@ -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));
}
+127
View File
@@ -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;
}
+541
View File
@@ -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);
}
+741
View File
@@ -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;") &times;
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:&lt;account_name&gt; #[em - account name]
div note:&lt;note_text&gt; #[em - note contents]
div email:&lt;email_fragment&gt; #[em - email fragment]
div role:&lt;role_name&gt; #[em - role]
div exact:&lt;name&gt; #[em - exact account name]
div ignores:&lt;count&gt; #[em - amount of ignores (at least)]
div ponies:&lt;count&gt; #[em - amount of ponies (at least)]
div auths:&lt;count&gt; #[em - amount of auths (at least)]
div old:&lt;number_of_days&gt; #[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:&lt;number&gt;
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