diff --git a/.gitignore b/.gitignore index e668f3b..3ba2110 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ node_modules/ /build-copy/ /logs/ /settings/ +.idea/ -/config.json \ No newline at end of file +/config.json diff --git a/src/ts/bootstrap-common.ts b/src/ts/bootstrap-common.ts index 4688a80..f199517 100644 --- a/src/ts/bootstrap-common.ts +++ b/src/ts/bootstrap-common.ts @@ -9,9 +9,9 @@ import './client/polyfils'; import { enableProdMode } from '@angular/core'; if (document.body.getAttribute('data-debug') !== 'true' || localStorage.production) { - enableProdMode(); + enableProdMode(); } if (typeof module !== 'undefined' && module.hot) { - module.hot.accept(); + module.hot.accept(); } diff --git a/src/ts/bootstrap-es.ts b/src/ts/bootstrap-es.ts index 78f6a0d..d647f8d 100644 --- a/src/ts/bootstrap-es.ts +++ b/src/ts/bootstrap-es.ts @@ -4,5 +4,5 @@ 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 }); + platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); } diff --git a/src/ts/bootstrap.ts b/src/ts/bootstrap.ts index 2e055ab..ef097a4 100644 --- a/src/ts/bootstrap.ts +++ b/src/ts/bootstrap.ts @@ -4,10 +4,10 @@ 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 }, '*'); - } + 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 }); + platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); } diff --git a/src/ts/client/buttonActions.ts b/src/ts/client/buttonActions.ts index 1b619a5..a8a267a 100644 --- a/src/ts/client/buttonActions.ts +++ b/src/ts/client/buttonActions.ts @@ -1,8 +1,8 @@ import { compact } from 'lodash'; import { - Expression, ExpressionButtonAction, CommandButtonAction, ActionButtonAction, ItemButtonAction, - ColorShadow, Eye, Muzzle, Iris, ButtonActionSlot, ExpressionExtra, ButtonAction, ChatType, BodyAnimation, - Action, isPartyChat, EntityButtonAction, defaultDrawOptions + 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'; @@ -14,8 +14,8 @@ 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 + 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'; @@ -42,33 +42,33 @@ 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; + 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 (wings) { + info.wings!.type = 1; + } - if (horn) { - info.horn!.type = 1; - } + if (horn) { + info.horn!.type = 1; + } - syncLockedPonyInfo(info); - return toPalette(info, mockPaletteManager); + syncLockedPonyInfo(info); + return toPalette(info, mockPaletteManager); } function createState() { - const state = defaultPonyState(); - state.blushColor = blushColor(parseColor(ACTION_ACTION_COAT_COLOR)); - return state; + const state = defaultPonyState(); + state.blushColor = blushColor(parseColor(ACTION_ACTION_COAT_COLOR)); + return state; } function colorToGrayscale(value: string) { - return colorToHexRGB(toGrayscale(parseColor(value))); + return colorToHexRGB(toGrayscale(parseColor(value))); } const ACTION_ACTION_BG_DISABLED = toGrayscale(parseColor(ACTION_ACTION_BG)); @@ -84,276 +84,276 @@ 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' }; + return { type: 'expression', expression, title: expression ? '' : 'Reset expression' }; } export function commandButtonAction(command: string, icon: string): CommandButtonAction { - return { type: 'command', command, title: command, icon }; + return { type: 'command', command, title: command, icon }; } export function actionButtonAction(action: string, title: string, sendAction = Action.None): ActionButtonAction { - return { type: 'action', action, title, sendAction }; + return { type: 'action', action, title, sendAction }; } export function itemButtonAction(icon: ColorShadow, count?: number): ItemButtonAction { - return { type: 'item', icon, count }; + return { type: 'item', icon, count }; } export function entityButtonAction(entity: string): EntityButtonAction { - return { type: 'entity', entity, title: entity }; + 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'), + 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', '🥚'), + commandButtonAction('/roll', '🎲'), + commandButtonAction('/gifts', '🎁'), + commandButtonAction('/candies', '🍬'), + commandButtonAction('/clovers', '🍀'), + commandButtonAction('/toys', '🎅'), + commandButtonAction('/eggs', '🥚'), ]; const additionalActionsActions = [ - expressionButtonAction(undefined), + expressionButtonAction(undefined), ]; function getActionAction(action: string) { - return actionActions.find(a => a.action === action); + return actionActions.find(a => a.action === action); } function getCommandAction(command: string) { - return commandActions.find(a => a.command === command); + return commandActions.find(a => a.command === command); } export function createButtionActionActions() { - return [...actionActions, ...additionalActionsActions]; + return [...actionActions, ...additionalActionsActions]; } export function createButtonCommandActions() { - return [...commandActions]; + 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)) }, - ]; + 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); + const serialized = slots.slice(0, ACTIONS_LIMIT).map(serializeAction); - while (serialized.length && !serialized[serialized.length - 1]) { - serialized.pop(); - } + while (serialized.length && !serialized[serialized.length - 1]) { + serialized.pop(); + } - return JSON.stringify(serialized); + 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 []; - } + 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; - } + 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)}`); - } - } + 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 }; + 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 (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); - } - } + 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; - } + 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(); @@ -361,316 +361,316 @@ 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; + 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 (resizeCanvasWithRatio(canvas, CANVAS_SIZE, CANVAS_SIZE)) { + state.action = 0; + } - if (!spriteSheetsLoaded || !shouldRedrawAction(action, state, game)) - return; + if (!spriteSheetsLoaded || !shouldRedrawAction(action, state, game)) + return; - const context = canvas.getContext('2d'); + const context = canvas.getContext('2d'); - if (!context) - return; + if (!context) + return; - state.action = action; + state.action = action; - context.save(); - context.clearRect(0, 0, canvas.width, canvas.height); - disableImageSmoothing(context); + context.save(); + context.clearRect(0, 0, canvas.width, canvas.height); + disableImageSmoothing(context); - const scale = 2 * getPixelRatio(); - const bufferSize = ICON_SIZE; + 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) { + 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 (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.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.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); - } - }); + 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_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; + 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); + 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()); + 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}`); - } - }); - } + // 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_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)); + 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 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 }); - } - } - }); + 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.fillStyle = ENTITY_ITEM_BG; + context.fillRect(0, 0, canvas.width, canvas.height); + context.drawImage(buffer, 0, 0); + } + break; + } + } + } - context.restore(); + context.restore(); } function drawLie(batch: ContextSpriteBatch) { - const state = { ...createState(), animation: lie }; - drawPony(batch, actionPony, state, -6, 15, defaultDrawPonyOptions()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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}`); - } + 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; + 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'; - } - } + if (player) { + if (isPonyLying(player)) { + return 'sit'; + } else if (isPonySitting(player)) { + return 'stand'; + } else if (isPonyStanding(player) && canPonyFly(player)) { + return 'fly'; + } + } - return 'flyDisabled'; + return 'flyDisabled'; } function getDownDrawFunc(game: PonyTownGame) { - const player = game.player; + const player = game.player; - if (player) { - if (isPonySitting(player)) { - return 'lie'; - } else if (isPonyStanding(player)) { - return 'sit'; - } else if (isPonyFlying(player)) { - return 'stand'; - } - } + if (player) { + if (isPonySitting(player)) { + return 'lie'; + } else if (isPonyStanding(player)) { + return 'sit'; + } else if (isPonyFlying(player)) { + return 'stand'; + } + } - return 'lieDisabled'; + return 'lieDisabled'; } diff --git a/src/ts/client/canvasUtils.ts b/src/ts/client/canvasUtils.ts index d2ec4a0..02fbb05 100644 --- a/src/ts/client/canvasUtils.ts +++ b/src/ts/client/canvasUtils.ts @@ -2,100 +2,100 @@ 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; + const canvas = document.createElement('canvas'); + canvas.width = width | 0; + canvas.height = height | 0; + return canvas; }; /* istanbul ignore next */ export let loadImage = (src: string): Promise => { - return new Promise((resolve, reject) => { - const img = new Image(); - img.addEventListener('load', () => resolve(img)); - img.addEventListener('error', () => reject(new Error(`Error loading image (${src})`))); - img.src = src; - }); + return new Promise((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 + 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); + loadImage = src => fetch(src) + .then(response => response.blob()) + .then(createImageBitmap); } export function setup(methods: { - createCanvas(width: number, height: number): HTMLCanvasElement; - loadImage(src: string): Promise; + createCanvas(width: number, height: number): HTMLCanvasElement; + loadImage(src: string): Promise; }) { - createCanvas = methods.createCanvas; - loadImage = methods.loadImage; + 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; - } + 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; + 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 (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; - } + 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; + return resized; } /* istanbul ignore next */ export function canvasToSource(canvas: HTMLCanvasElement) { - return new Promise((resolve, reject) => { - canvas.toBlob(blob => { - if (blob) { - resolve(URL.createObjectURL(blob)); - } else { - reject(new Error('Failed to convert canvas')); - } - }); - }); + return new Promise((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)); + 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; - } + if ('imageSmoothingEnabled' in context) { + context.imageSmoothingEnabled = false; + } else { + (context as any).webkitImageSmoothingEnabled = false; + (context as any).mozImageSmoothingEnabled = false; + (context as any).msImageSmoothingEnabled = false; + } } diff --git a/src/ts/client/clientActions.ts b/src/ts/client/clientActions.ts index 4568caf..5f27c80 100644 --- a/src/ts/client/clientActions.ts +++ b/src/ts/client/clientActions.ts @@ -1,8 +1,8 @@ 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 + 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'; @@ -16,8 +16,8 @@ import { addNotification, removeNotification, markGameAsLoaded, resetGameFields, import { Model } from '../components/services/model'; import { decodeUpdate } from '../common/encoders/updateDecoder'; import { - updatePonyInfoWithPoof, subscribeRegion, handleUpdates, handleUpdateEntity, handleRemoveEntity, handleSays, - handleEntityInfo, handleUpdatePonies, filterEntityName, handleUpdateFriends + updatePonyInfoWithPoof, subscribeRegion, handleUpdates, handleUpdateEntity, handleRemoveEntity, handleSays, + handleEntityInfo, handleUpdatePonies, filterEntityName, handleUpdateFriends } from './handlers'; import { nameToHTML } from './emoji'; @@ -27,278 +27,278 @@ 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; + 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()); + 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); + 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; + 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; + 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); + const pony = findById(this.model.ponies, characterId); - if (pony) { - this.model.selectPony(pony); - } + 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(); - } + 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; + const entity = findEntityById(this.game.map, id) as Pony | undefined; - if (entity) { - entity.name = name; - updatePonyInfoWithPoof(this.game, entity, info, crc); - } + 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); + 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); - } + for (const subscribe of subscribes) { + subscribeRegion(this.game, subscribe); + } - if (subscribes.length) { - markGameAsLoaded(this.game); - } + if (subscribes.length) { + markGameAsLoaded(this.game); + } - if (updates) { - handleUpdates(this.game, updates); - } + if (updates) { + handleUpdates(this.game, updates); + } - for (const region of regions) { - const { x, y, updates, removes, tiles } = decodeUpdate(region); + for (const region of regions) { + const { x, y, updates, removes, tiles } = decodeUpdate(region); - for (const update of updates) { - handleUpdateEntity(this.game, update); - } + for (const update of updates) { + handleUpdateEntity(this.game, update); + } - for (const id of removes) { - handleRemoveEntity(this.game, id); - } + 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 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)})`); - } + 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; + const player = this.game.player; - if (player) { - player.x = x; - player.y = y; - savePlayerPosition(); - } + 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; + 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 || '')); + 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(([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), - })); + 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(([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 (members) { + const missing = members.filter(p => !p.pony).map(p => p.id); - if (missing.length) { - this.game.send(server => server.getPonies(missing)); - } - } + 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); + 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 }); - } - } + 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 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 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'); + 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}`); - } - } + 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}()`)); + getMethods(ClientActions) + .filter(m => !m.options.binary) + .forEach(m => console.error(`Missing binary encoding for ClientActions.${m.name}()`)); } diff --git a/src/ts/client/clientAdminActions.ts b/src/ts/client/clientAdminActions.ts index ada1ea8..e31059b 100644 --- a/src/ts/client/clientAdminActions.ts +++ b/src/ts/client/clientAdminActions.ts @@ -4,31 +4,31 @@ import { ModelTypes } from '../common/adminInterfaces'; import { ModelSubscriber } from '../components/services/modelSubscriber'; export interface ClientUpdate { - type: ModelTypes; - id: string; - update: any; + 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; + 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; - if (model) { - model.update(id, update); - } else { - console.error(`Invalid model type "${type}"`); - } - } - } + if (model) { + model.update(id, update); + } else { + console.error(`Invalid model type "${type}"`); + } + } + } } diff --git a/src/ts/client/clientUtils.ts b/src/ts/client/clientUtils.ts index db64d99..cf36db6 100644 --- a/src/ts/client/clientUtils.ts +++ b/src/ts/client/clientUtils.ts @@ -1,11 +1,11 @@ import { clamp } from 'lodash'; import { - SocialSite, SocialSiteInfo, Eye, Muzzle, Iris, ExpressionExtra, Expression, ServerInfo, ServerFeatureFlags, - AccountData, AccountDataFlags + 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 + 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'; @@ -18,85 +18,85 @@ export const matchCyrillic = /[\u0400-\u04FF]/g; export const containsCyrillic = matcher(matchCyrillic); const otherValid = [ - '♂♀⚲⚥⚧☿♁⚨⚩⚦⚢⚣⚤', // gender symbols - '™®♥♦♣♠❥♡♢♤♧ღஐ·´°•◦✿❀◆◇◈◉◊。¥€«»,:■□—', // other - '〈〉「」『』【】《》♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼№●○◌★☆✰✦✧▪▫・', // other 2 - '\u1160\u3000\u3164', // spaces (replaced later) + '♂♀⚲⚥⚧☿♁⚨⚩⚦⚢⚣⚤', // gender symbols + '™®♥♦♣♠❥♡♢♤♧ღஐ·´°•◦✿❀◆◇◈◉◊。¥€«»,:■□—', // other + '〈〉「」『』【】《》♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼№●○◌★☆✰✦✧▪▫・', // other 2 + '\u1160\u3000\u3164', // spaces (replaced later) ].join('').split('').reduce((set, c) => (set.add(c.charCodeAt(0)), set), new Set()); 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 - ; + 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) - ; + 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 - ; + return c === 0x1f595 // middle finger emoji + || c === 0x00ad // soft hyphen + ; } function isValidForName(c: number): boolean { - return isValid(c) && !isInvalid(c); + return isValid(c) && !isInvalid(c); } function isValidForMessage(c: number): boolean { - return (isValid(c) || isValid2(c)) && !isInvalid(c); + return (isValid(c) || isValid2(c)) && !isInvalid(c); } export const matchRomaji = /[\uff01-\uff5e]/g; @@ -104,333 +104,333 @@ 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); + 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(); + 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); + 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 || ''; + value = value || ''; - for (let i = 0; i < value.length; i++) { - let code = value.charCodeAt(i); - let size = 1; - let invalidSurrogate = false; + 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 (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 (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); - } - } + if (invalidSurrogate || !filter(code)) { + i -= size; + value = value.substr(0, i + 1) + value.substr(i + size + 1); + } + } - return value; + return value; } export function validatePonyName(name: string | undefined): boolean { - return !!name && !!name.length && name.length <= PLAYER_NAME_MAX_LENGTH && !/^[.,_-]+$/.test(name); + 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); + const oauth = oauthProviders.find(p => p.id === provider); - return { - id, - name, - url, - icon: oauth && oauth.id, - color: oauth && oauth.color, - }; + return { + id, + name, + url, + icon: oauth && oauth.id, + color: oauth && oauth.color, + }; } function isMultipleMatch(message: string, last: string): boolean { - const minMessageLength = 4; + const minMessageLength = 4; - if (message.length >= minMessageLength && last.length >= minMessageLength) { - let current = last; + if (message.length >= minMessageLength && last.length >= minMessageLength) { + let current = last; - while (current.length < message.length) { - current += last; - } + while (current.length < message.length) { + current += last; + } - return message === current.substr(0, SAY_MAX_LENGTH); - } else { - return false; - } + 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; + return message.indexOf(last) === 0 && (message.length - last.length) < 3; } function isTrailingMatch(message: string, last: string) { - const minMessageLength = 5; + 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; - } + 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; - } + 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); + 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 + right: Eye, left: Eye, muzzle: Muzzle, rightIris = Iris.Forward, leftIris = Iris.Forward, extra = ExpressionExtra.None ): Expression { - return { right, left, muzzle, rightIris, leftIris, extra }; + return { right, left, muzzle, rightIris, leftIris, extra }; } export const isAndroidBrowser = (() => { - const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent; + 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; - } + // 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; + return false; })(); /* istanbul ignore next */ export const isBrowserOutdated = (() => { - const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent; + 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); + // 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; - } + 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; - } + // 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; - } + if (!supportsLetAndConst()) { + return true; + } - return false; + return false; })(); export function getLocale() { - return (navigator.languages ? navigator.languages[0] : navigator.language) || 'en-US'; + 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); + 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 (a.flag === 'ru' && a.flag !== b.flag) { + return -1; + } - if (b.flag === 'ru' && a.flag !== b.flag) { - return 1; - } + if (b.flag === 'ru' && a.flag !== b.flag) { + return 1; + } - return a.id.localeCompare(b.id); + return a.id.localeCompare(b.id); } export function readFileAsText(file: File) { - return new Promise((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); - }); + return new Promise((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; - } + try { + return !!new Blob; + } catch { + return false; + } } export let isInIncognitoMode = false; export function setIsIncognitoMode(value: boolean) { - isInIncognitoMode = value; + isInIncognitoMode = value; } /* istanbul ignore next */ function checkIncognitoMode(wnd: any) { - if (!wnd || !wnd.chrome) - return; + if (!wnd || !wnd.chrome) + return; - const fs = wnd.RequestFileSystem || wnd.webkitRequestFileSystem; + const fs = wnd.RequestFileSystem || wnd.webkitRequestFileSystem; - if (!fs) - return; + if (!fs) + return; - fs(wnd.TEMPORARY, 100, () => { }, () => isInIncognitoMode = true); + fs(wnd.TEMPORARY, 100, () => { }, () => isInIncognitoMode = true); } let focused = true; /* istanbul ignore next */ export function isFocused() { - return focused; + return focused; } /* istanbul ignore next */ if (typeof window !== 'undefined') { - checkIncognitoMode(window); - window.addEventListener('focus', () => focused = true); - window.addEventListener('blur', () => focused = false); + 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 + 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; - } + 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; + try { + if ('serviceWorker' in navigator && typeof navigator.serviceWorker.register === 'function') { + let hadWorker = false; - navigator.serviceWorker.register(url) - .then(worker => { - hadWorker = !!worker.active; + navigator.serviceWorker.register(url) + .then(worker => { + hadWorker = !!worker.active; - worker.addEventListener('updatefound', () => { - if (hadWorker) { - onUpdate(); - } - }); - }); + worker.addEventListener('updatefound', () => { + if (hadWorker) { + onUpdate(); + } + }); + }); - navigator.serviceWorker.addEventListener('controllerchange', () => { - if (hadWorker) { - location.reload(); - } - }); - } - } catch (e) { - console.error(e); - } + 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(); - } + 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; - } + 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')!; + 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'; - } + 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); - } + 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; - } + return false; + } } let flags: ServerFeatureFlags = {}; @@ -438,17 +438,17 @@ let flags: ServerFeatureFlags = {}; export const featureFlagsChanged = new Subject(); export function initFeatureFlags(newFlags: ServerFeatureFlags) { - flags = newFlags; - featureFlagsChanged.next(newFlags); + flags = newFlags; + featureFlagsChanged.next(newFlags); } export function hasFeatureFlag(flag: keyof ServerFeatureFlags) { - return !!flags[flag]; + return !!flags[flag]; } export function hardReload() { - unregisterServiceWorker() - .then(() => location.reload(true)); + unregisterServiceWorker() + .then(() => location.reload(true)); } const LOGGING = false; @@ -456,47 +456,47 @@ const LOGGING = false; let logger = (_: string) => { }; export function initLogger(newLogger: (message: string) => void) { - if (LOGGING) { - logger = newLogger; - } + if (LOGGING) { + logger = newLogger; + } } export function log(message: string) { - if (LOGGING) { - logger(message); - } + if (LOGGING) { + logger(message); + } } export function isSupporterOrPastSupporter(account: AccountData | undefined) { - return !!account && (!!account.supporter || hasFlag(account.flags, AccountDataFlags.PastSupporter)); + 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 ''; - } + 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'; - } + 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]; - } + 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]; + } } diff --git a/src/ts/client/credits.ts b/src/ts/client/credits.ts index 8bb7bf7..dd86b46 100644 --- a/src/ts/client/credits.ts +++ b/src/ts/client/credits.ts @@ -1,99 +1,99 @@ export interface Credit { - name: string; - title: string; - avatarIndex: number; - links: string[]; + name: string; + title: string; + avatarIndex: number; + links: string[]; } export interface Contributor { - name: string; - links?: string[]; + name: string; + links?: string[]; } export interface Contributors { - group: string; - contributors: Contributor[]; + 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'], - // }, - { - name: 'Bytewave', - title: 'Programmer / Moderator', - avatarIndex: 0, - links: ['https://twitter.com/BytewaveMLP', 'https://github.com/BytewaveMLP'] - }, - { - name: 'Cloud Hop', - title: 'Programmer / Moderator', - avatarIndex: 1, - links: ['https://twitter.com/blackhole0173', 'https://github.com/blackhole12'] - }, - { - name: 'CyberPon3', - title: 'Programmer / Moderator', - avatarIndex: 2, - links: ['https://twitter.com/CyberPon3'] - }, - { - name: 'NotMyWing', - title: 'Programmer / Moderator', - avatarIndex: 3, - links: ['https://twitter.com/NotMyWing', 'https://github.com/Neeve01'] - }, - { - name: 'Stubenhocker', - title: 'Programmer', - avatarIndex: 4, - links: ['https://github.com/Stubenhocker1399'] - } + // 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'], + // }, + { + name: 'Bytewave', + title: 'Programmer / Moderator', + avatarIndex: 0, + links: ['https://twitter.com/BytewaveMLP', 'https://github.com/BytewaveMLP'] + }, + { + name: 'Cloud Hop', + title: 'Programmer / Moderator', + avatarIndex: 1, + links: ['https://twitter.com/blackhole0173', 'https://github.com/blackhole12'] + }, + { + name: 'CyberPon3', + title: 'Programmer / Moderator', + avatarIndex: 2, + links: ['https://twitter.com/CyberPon3'] + }, + { + name: 'NotMyWing', + title: 'Programmer / Moderator', + avatarIndex: 3, + links: ['https://twitter.com/NotMyWing', 'https://github.com/Neeve01'] + }, + { + name: 'Stubenhocker', + title: 'Programmer', + avatarIndex: 4, + links: ['https://github.com/Stubenhocker1399'] + } ]; 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: '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'] }, - ], - }, + { + 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: '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'] }, + ], + }, ]; diff --git a/src/ts/client/data.ts b/src/ts/client/data.ts index bbc750e..2963dc3 100644 --- a/src/ts/client/data.ts +++ b/src/ts/client/data.ts @@ -4,17 +4,17 @@ import { OAuthProvider } from '../common/interfaces'; /* istanbul ignore next */ function attr(name: string): string | undefined { - return typeof document !== 'undefined' ? (document.body.getAttribute(name) || undefined) : 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; + const element = typeof document !== 'undefined' ? document.getElementById(id) : undefined; + return element ? element.innerHTML : undefined; } function json(id: string, def: string): T { - return JSON.parse(data(id) || def); + return JSON.parse(data(id) || def); } export let isMobile = false; @@ -31,7 +31,7 @@ export const copyrightName = attr('data-copyright'); /* istanbul ignore next */ export const oauthProviders = json('oauth-providers', '[]') - .map(a => { ...a, url: `/auth/${a.id}` }); + .map(a => { ...a, url: `/auth/${a.id}` }); /* istanbul ignore next */ export const signUpProviders = oauthProviders.filter(i => !i.connectOnly); /* istanbul ignore next */ @@ -39,35 +39,35 @@ export const signInProviders = oauthProviders.filter(i => i.connectOnly); /* istanbul ignore next */ export function socketOptions(): ClientOptions { - const options = data('socket-options'); + 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'); - } + 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'); + 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 (!/windows/i.test(navigator.userAgent)) { + window.addEventListener('touchstart', setMobile); + } - if (/Trident/.test(navigator.userAgent)) { - document.body.classList.add('is-msie'); - } + if (/Trident/.test(navigator.userAgent)) { + document.body.classList.add('is-msie'); + } - if (/YaBrowser/.test(navigator.userAgent)) { - document.body.classList.add('is-yandex'); - } + if (/YaBrowser/.test(navigator.userAgent)) { + document.body.classList.add('is-yandex'); + } } diff --git a/src/ts/client/draw.ts b/src/ts/client/draw.ts index 76b17e5..20b0000 100644 --- a/src/ts/client/draw.ts +++ b/src/ts/client/draw.ts @@ -1,5 +1,5 @@ import { - Entity, DrawOptions, Camera, PaletteSpriteBatch, SpriteBatch, TileSets, Engine, Pony, WorldMap, EntityState + 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'; @@ -16,282 +16,282 @@ 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; + 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; + 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; - } - } - } - } + if (pony.batch !== undefined) { + batch.releaseBatch(pony.batch); + pony.batch = undefined; + } + } + } + } - return entitiesDrawn; + return entitiesDrawn; } export function drawEntityLights(batch: SpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) { - const drawHidden = options.drawHidden; + const drawHidden = options.drawHidden; - for (const entity of entities) { - if (DEVELOPMENT && (entity.type !== PONY_TYPE && !entity.drawLight)) { - console.error('Cannot draw entity light', entity); - } + 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); - } - } - } + 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; + 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); - } + 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); - } - } - } + 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; - } + 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; - } + 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[], + 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('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('sortEntities'); + sortEntities(map.entitiesDrawable); + TIMING && timeEnd(); - TIMING && timeStart('drawEntities'); - const entitiesDrawn = drawEntities(batch, map.entitiesDrawable, camera, options); - 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 || TOOLS) { + forEachRegion(map, region => drawTilesDebugInfo(batch, region, camera, options)); + } - if (BETA && options.debug.showHelpers) { - drawDebugHelpers(batch, map.entities, 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) { + 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.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.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); - } + if (BETA && options.showHeightmap) { + drawDebugInWater(batch, map, camera); + } - return entitiesDrawn; + return entitiesDrawn; } // debug function drawDebugHelpers(batch: PaletteSpriteBatch, entities: Entity[], options: DrawOptions) { - const textColor = 0x000000b2; - const show = options.debug; + 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); + 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; + if (show.collider) { + batch.globalAlpha = 0.5; - const x = Math.floor(e.x * tileWidth); - const y = Math.floor(e.y * tileHeight); + 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); - } - } - } + 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 + 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()); - } - } + 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); + 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; + 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; + 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; + 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 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; + for (let x = 0; x < w; x++) { + if ((sx + x + 1) < cameraLeft || (sx + x) > cameraRight) + continue; - const tx = x; + const tx = x; - while (isInWaterAt(map, toWorldX(sx + x + 0.5), toWorldY(sy + y + 0.5)) && x < w) { - 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); - } - } - } - }); + 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); + 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; + 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; + 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; + 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 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; + for (let x = 0; x < w; x++) { + if ((sx + x + 1) < cameraLeft || (sx + x) > cameraRight) + continue; - const tx = x; + const tx = x; - while (collider[x + y * w] !== 0 && x < w) { - x++; - } + while (collider[x + y * w] !== 0 && x < w) { + x++; + } - if (x > tx) { - batch.drawRect(color, sx + tx, sy + y, x - tx, 1); - } - } - } - }); + 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; + 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++) { + 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.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); - } + for (let i = 0; i <= map.regionsX; i++) { + batch.drawRect(GRAY, x + rw * i, y, 1, height); + } } diff --git a/src/ts/client/emoji.ts b/src/ts/client/emoji.ts index 3cc9c8f..7ab6d71 100644 --- a/src/ts/client/emoji.ts +++ b/src/ts/client/emoji.ts @@ -7,108 +7,108 @@ import { normalSpriteSheet } from '../generated/sprites'; import { includes } from '../common/utils'; export interface Emoji { - names: string[]; - symbol: string; + 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'], + // 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'], + // 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'], + // 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'], + // 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'], + // animals + ['🦋', 'butterfly'], + ['🦇', 'bat'], + ['🕷', 'spider'], + ['👻', 'ghost'], + ['🐈', 'cat2'], - // other - ['™', 'tm'], - ['♂', 'male'], - ['♀', 'female'], - ['⚧', 'trans', 'transgender'], + // other + ['™', 'tm'], + ['♂', 'male'], + ['♀', 'female'], + ['⚧', 'trans', 'transgender'], ].map(createEmoji); export const emojiMap = new Map(); @@ -116,79 +116,79 @@ 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)); + 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); + 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, ''))] }; + return { symbol, names: [...names, ...names.filter(n => /_/.test(n)).map(n => n.replace(/_/g, ''))] }; } const emojiImages = new Map(); const emojiImagePromises = new Map>(); export function getEmojiImageAsync(sprite: Sprite, callback: (str: string) => void) { - const src = emojiImages.get(sprite); + const src = emojiImages.get(sprite); - if (src) { - callback(src); - return; - } + if (src) { + callback(src); + return; + } - const promise = emojiImagePromises.get(sprite); + const promise = emojiImagePromises.get(sprite); - if (promise) { - promise.then(callback); - return; - } + 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); + 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); + newPromise + .then(src => { + emojiImages.set(sprite, src); + emojiImagePromises.delete(sprite); + return src; + }) + .then(callback); } const emojisRegex = new RegExp(`(${[ - ...emojis.map(e => e.symbol), - '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎', + ...emojis.map(e => e.symbol), + '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎', ].join('|')})`, 'g'); export function splitEmojis(text: string) { - return text.split(emojisRegex); + return text.split(emojisRegex); } export function hasEmojis(text: string) { - return emojisRegex.test(text); + return emojisRegex.test(text); } export function nameToHTML(name: string) { - return escape(name); + return escape(name); } export interface AutocompleteState { - lastEmoji?: string; + 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; - }); + 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; + }); } diff --git a/src/ts/client/fonts.ts b/src/ts/client/fonts.ts index 9317025..22209a4 100644 --- a/src/ts/client/fonts.ts +++ b/src/ts/client/fonts.ts @@ -9,31 +9,31 @@ export let fontMono: SpriteFont; export let fontMonoPal: SpriteFont; export function createFonts() { - font = createSpriteFont(sprites.font, sprites.emoji, 3); - font.lineSpacing = 3; - font.letterShiftY = -2; + 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; + 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; + 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; + 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; + 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; + fontMonoPal = createSpriteFont(sprites.fontMonoPal, [], 4); + fontMonoPal.lineSpacing = 4; + fontMonoPal.letterShiftY = -2; + fontMonoPal.letterHeightReal += 2; } diff --git a/src/ts/client/game.ts b/src/ts/client/game.ts index 06d8269..e4389c3 100644 --- a/src/ts/client/game.ts +++ b/src/ts/client/game.ts @@ -2,23 +2,23 @@ import { Injectable, NgZone } from '@angular/core'; import { Subject, BehaviorSubject } from 'rxjs'; import { debounce } from 'lodash'; import { - EntityState, Pony, Notification, TileType, Action, IServerActions, Season, WorldState, Holiday, - TileSets, ChatMessage, PartyInfo, Entity, DrawOptions, Engine, defaultDrawOptions, PonyStateFlags, - DoAction, ChatType, WorldStateFlags, DebugFlags, MessageType, AccountSettings, Matrix4, - FakeEntity, SelectFlags, WorldMap, MapType, MapFlags, EntityFlags, houseTiles, isValidTile + EntityState, Pony, Notification, TileType, Action, IServerActions, Season, WorldState, Holiday, + TileSets, ChatMessage, PartyInfo, Entity, DrawOptions, Engine, defaultDrawOptions, PonyStateFlags, + DoAction, ChatType, WorldStateFlags, DebugFlags, MessageType, AccountSettings, Matrix4, + FakeEntity, SelectFlags, WorldMap, MapType, MapFlags, EntityFlags, houseTiles, isValidTile } from '../common/interfaces'; import { - clamp, lengthOfXY, setFlag, hasFlag, boundsIntersect, point, toInt, lerpColor, distanceXY + clamp, lengthOfXY, setFlag, hasFlag, boundsIntersect, point, toInt, lerpColor, distanceXY } from '../common/utils'; import { - OFFLINE_PONY, MAX_SCALE, SUPPORTER_PONY, HOUR, MIN_SCALE, CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX, - CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX, SECOND, TILE_CHANGE_RANGE, MINUTE, PONY_TYPE, - REGION_SIZE, tileWidth, tileHeight + OFFLINE_PONY, MAX_SCALE, SUPPORTER_PONY, HOUR, MIN_SCALE, CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX, + CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX, SECOND, TILE_CHANGE_RANGE, MINUTE, PONY_TYPE, + REGION_SIZE, tileWidth, tileHeight } from '../common/constants'; import { - ensureAllVisiblePoniesAreDecoded, invalidatePalettes, updateMap, updateEntities, - getMapHeightAt, updateEntitiesWithNames, updateEntitiesCoverLifted, getTile, - pickEntities, updateEntitiesTriggers, getElevation, setElevation, createWorldMap + ensureAllVisiblePoniesAreDecoded, invalidatePalettes, updateMap, updateEntities, + getMapHeightAt, updateEntitiesWithNames, updateEntitiesCoverLifted, getTile, + pickEntities, updateEntitiesTriggers, getElevation, setElevation, createWorldMap } from '../common/worldMap'; import { updateCamera, centerCameraOn, screenToWorld, createCamera } from '../common/camera'; import { WHITE, BLACK, SHADOW_COLOR, getTileColor, RED, CAVE_LIGHT, CAVE_SHADOW } from '../common/colors'; @@ -42,8 +42,8 @@ import { roundPositionX, roundPositionY, toScreenX, toScreenY } from '../common/ import { StorageService } from '../components/services/storageService'; import { SettingsService } from '../components/services/settingsService'; import { - isPonyLying, isPonySitting, setPonyState, getInteractBounds, entityInRange, isFacingRight, - isHidden, updateEntityVelocity, releaseEntity, addChatBubble + isPonyLying, isPonySitting, setPonyState, getInteractBounds, entityInRange, isFacingRight, + isHidden, updateEntityVelocity, releaseEntity, addChatBubble } from '../common/entityUtils'; import { vectorToDir, dirToVector, flagsToSpeed, encodeMovement, isMovingRight } from '../common/movementUtils'; import { createDefaultButtonActions, useAction, deserializeActions } from './buttonActions'; @@ -53,8 +53,8 @@ import { restorePlayerPosition, savePlayerPosition } from './sec'; import { drawEntityLights, drawEntityLightSprites, drawMap, drawDebugRegions } from './draw'; import { updateTileSets, initializeTileHeightmaps } from './tileUtils'; import { - downAction, upAction, turnHeadAction, boopAction, interact, toggleWall, editorMoveEntities, - editorSelectEntities, editorDragEntities + downAction, upAction, turnHeadAction, boopAction, interact, toggleWall, editorMoveEntities, + editorSelectEntities, editorDragEntities } from './playerActions'; import { fontSmallPal, fontSmall, font, fontMono } from './fonts'; import { drawText, drawOutlinedText, measureText } from '../graphics/spriteFont'; @@ -66,7 +66,7 @@ import { bindFrameBuffer, unbindFrameBuffer, resizeFrameBuffer } from '../graphi import { WebGL, initWebGL, disposeWebGL, initWebGLResources } from './webgl'; import { bindTexture } from '../graphics/webgl/texture2d'; import { - normalSpriteSheet, paletteSpriteSheet, defaultPalette, wall_h_placeholder, wall_v_placeholder + normalSpriteSheet, paletteSpriteSheet, defaultPalette, wall_h_placeholder, wall_v_placeholder } from '../generated/sprites'; import { createMat4, ortho } from '../common/mat4'; import { Model } from '../components/services/model'; @@ -74,16 +74,16 @@ import { filterEntityName } from './handlers'; import { isOutsideMap } from '../common/collision'; interface Minimap { - width: number; - height: number; - data: Uint32Array; + width: number; + height: number; + data: Uint32Array; } interface IncompleteSay { - id: number; - message: string; - type: MessageType; - time: number; + id: number; + message: string; + type: MessageType; + time: number; } const LOG_POSITION = false; @@ -97,1680 +97,1680 @@ const placeEntitiesTool = hammer; const changeTileTool = shovel; const numpad = [ - Key.NUMPAD_0, - Key.NUMPAD_1, - Key.NUMPAD_2, - Key.NUMPAD_3, - Key.NUMPAD_4, - Key.NUMPAD_5, - Key.NUMPAD_6, - Key.NUMPAD_7, - Key.NUMPAD_8, - Key.NUMPAD_9, + Key.NUMPAD_0, + Key.NUMPAD_1, + Key.NUMPAD_2, + Key.NUMPAD_3, + Key.NUMPAD_4, + Key.NUMPAD_5, + Key.NUMPAD_6, + Key.NUMPAD_7, + Key.NUMPAD_8, + Key.NUMPAD_9, ]; export const engines = [ - { name: 'Default', engine: Engine.Default }, - { name: 'LayeredTiles', engine: Engine.LayeredTiles }, - { name: 'Whiteness', engine: Engine.Whiteness }, - // { name: 'NewLighting', engine: Engines.NewLighting }, + { name: 'Default', engine: Engine.Default }, + { name: 'LayeredTiles', engine: Engine.LayeredTiles }, + { name: 'Whiteness', engine: Engine.Whiteness }, + // { name: 'NewLighting', engine: Engines.NewLighting }, ]; let pixelRatioEnabled = true; let pixelRatioCache = 1; function pixelRatio() { - return pixelRatioEnabled ? pixelRatioCache : 1; + return pixelRatioEnabled ? pixelRatioCache : 1; } function integerPixelRatio() { - return Math.max(1, Math.floor(pixelRatio())); + return Math.max(1, Math.floor(pixelRatio())); } function getMovementFlag(x: number, y: number, walkKey: boolean) { - const len = lengthOfXY(x, y); - const walk = len < 0.5 || walkKey; - return (x || y) ? (walk ? EntityState.PonyWalking : EntityState.PonyTrotting) : EntityState.None; + const len = lengthOfXY(x, y); + const walk = len < 0.5 || walkKey; + return (x || y) ? (walk ? EntityState.PonyWalking : EntityState.PonyTrotting) : EntityState.None; } export const actionButtons: { dirty: boolean; draw(): void; }[] = []; export function redrawActionButtons(force: boolean) { - for (const button of actionButtons) { - if (force || button.dirty) { - button.draw(); - } - } + for (const button of actionButtons) { + if (force || button.dirty) { + button.draw(); + } + } } @Injectable({ providedIn: 'root' }) export class PonyTownGame implements Game { - fallbackPonies = new Map(); - positions: { x: number; y: number; moved: boolean; }[] = []; - lastChatMessageType = ChatType.Say; - nextFriendsCRC = 0; - editingActions = false; - placeInQueue = 0; - time = performance.now(); - lightData = createLightData(Season.Summer); - season = Season.Summer; - holiday = Holiday.None; - worldFlags = WorldStateFlags.None; - showMinimap = false; - minimap: Minimap | undefined = undefined; - editor = { - type: 'stoneWall', - brushSize: 1, - tile: -1, - elevation: '', - special: '', - draggingEntities: false, - draggingStart: point(0, 0), - selectingEntities: false, - selectedEntities: [] as Entity[], - customLight: false, - lightColor: 'ffffff', - }; - incompleteSays: IncompleteSay[] = []; - shadowColor = SHADOW_COLOR; - onChat = new Subject(); - onToggleChat = new Subject(); - onCommand = new Subject(); - onCancel = () => false; - onClock = new BehaviorSubject(''); - onJoined = new Subject(); - onLeft = new Subject(); - onFrame = new Subject(); - onMessage = new Subject(); - messageQueue: ChatMessage[] = []; - lastWhisperFrom: { entityId: number; accountId?: string; } | undefined = undefined; - onPonyAddOrUpdate = new Subject(); - onActionsUpdate = new Subject(); - onPartyUpdate = new Subject(); - announcements = new Subject(); - onEntityIdUpdate = new Subject<{ old: number; new: number; }>(); - loaded = false; - fullyLoaded = false; - fps = 0; - player: Pony | undefined = undefined; - playerId: number | undefined = undefined; - playerName: string | undefined = undefined; - playerInfo: string | undefined = undefined; - playerCRC: number | undefined = undefined; - selected: Pony | undefined = undefined; - party: PartyInfo | undefined = undefined; - notifications: Notification[] = []; - map = createWorldMap(); - camera = createCamera(); - paletteManager = new PaletteManager(); - tileSets?: TileSets; - offlinePony = createPony(0, 0, OFFLINE_PONY, mockPaletteManager.addArray(defaultPalette), mockPaletteManager); - supporterPony = createPony(0, 0, SUPPORTER_PONY, mockPaletteManager.addArray(defaultPalette), mockPaletteManager); - scale: number; - failedFBO = false; - rightOverride?: boolean; - headTurnedOverride?: boolean; - stateOverride?: EntityState; - actions = createDefaultButtonActions(); - mod = false; - webgl?: WebGL; - actionsChanged = true; - debug: DebugFlags = {}; - whisperTo: Entity | FakeEntity | undefined = undefined; - findEntityFromChatLog: (id: number) => FakeEntity | undefined = () => undefined; - findEntityFromChatLogByName: (name: string) => FakeEntity | undefined = () => undefined; - private drawOptions: DrawOptions = { - ...defaultDrawOptions, - }; - private input = new InputManager(); - socket?: ClientSocketService; - private canvas?: HTMLCanvasElement; - private statsText?: Text; - private timeSize = 0; - private lastStats = 0; - private sent = 0; - private recv = 0; - private hideText = false; - private hidePublicChat = false; - private hover = point(0, 0); - private viewMatrix = createMat4(); - private fboMatrix = createMat4(); - private initialized = false; - private changedScale = false; - private baseTime = 0; - private targetBaseTime = 0; - private connectedTime = 0; - private lastPixelRatio = pixelRatio(); - private resized = true; - private resizedCamera = true; - private bg = colorToFloatArray(BLACK); - private deltaMultiplier = 1; - private lastDraw = 0; - private entitiesDrawn = 0; - private lastFps = performance.now(); - private frames = 0; - private drawFps = 0; - private lastCanvasRatio = 0; - private extraStats = ''; - private timingsText = ''; - private statsTextValue = ''; - private windowWidth = 0; - private windowHeight = 0; - private debugShortcuts: string[] = []; - private cameraShiftOn = false; - private cameraShiftTarget = 0; - private lastIsKeyboardOpen = false; - private element?: HTMLElement; - private showWallPlaceholder = false; - private highlightEntity?: Entity; - placeEntity = 0; - placeTile = 0; - constructor( - public audio: Audio, - private storage: StorageService, - public settings: SettingsService, - public model: Model, - private errorReporter: ErrorReporter, - private zone: NgZone, - ) { - this.scale = this.getScale(); - this.audio.initTracks(this.season, this.holiday, this.map.type); - this.audio.setVolume(this.volume); - this.debug = storage.getJSON('debug', {}); - this.drawOptions.error = message => errorReporter.reportError(message); - this.onActionsUpdate.subscribe(() => this.actionsChanged = true); - - if (DEVELOPMENT) { - attachDebugMethod('setScale', (x: number) => this.setScale(x)); - attachDebugMethod('game', this); - } - } - get volume() { - return this.settings.browser.volume || 0; - } - get disableLighting() { - return !!this.settings.browser.lowGraphicsMode || this.failedFBO; - } - get frameDelay() { - return (this.settings.browser.powerSaving || this.editingActions) ? (1000 / 45) : 0; - } - get engine() { - return BETA ? (this.debug.engine || Engine.Default) : Engine.Default; - } - set engine(value: Engine) { - if (BETA) { - this.debug.engine = value; - this.saveDebug(); - } - } - private applied(func: () => void) { - return () => this.apply(func); - } - apply = (func: () => void) => { - return this.zone.run(func); - } - applyChanges = () => this.zone.run(() => { }); - private getScale() { - const defaultScale = pixelRatio() > 1 ? 3 : 2; - const scale = toInt(this.settings.browser.scale) || defaultScale; - return clamp(scale, MIN_SCALE, MAX_SCALE); - } - private setScale(scale: number) { - if (this.scale !== scale) { - this.scale = scale; - this.settings.browser.scale = this.scale; - this.settings.saveBrowserSettings(); - this.changedScale = true; - } - } - private toggleDisableLighting() { - if (!this.failedFBO) { - this.settings.browser.lowGraphicsMode = !this.settings.browser.lowGraphicsMode; - this.settings.saveBrowserSettings(); - } - } - send(action: (server: IServerActions) => T) { - if (this.socket && this.socket.isConnected) { - return action(this.socket.server); - } else { - return undefined; - } - } - changeScale() { - this.setScale((this.scale % MAX_SCALE) + 1); - this.changedScale = true; - } - zoomIn() { - this.setScale(Math.min(MAX_SCALE, this.scale + 1)); - } - zoomOut() { - this.setScale(Math.max(1, this.scale - 1)); - } - select(pony: Pony | undefined) { - if (this.selected === pony) - return; - - if (pony && isHidden(pony) && !this.mod) - return; - - this.zone.run(() => { - if (this.selected) { - this.selected.selected = false; - } - - this.selected = pony; - - if (pony && !pony.info && !pony.palettePonyInfo) { - this.send(server => server.select(pony.id, SelectFlags.FetchEx | SelectFlags.FetchInfo)); - } else { - this.sendSelected(); - } - - if (this.selected) { - this.selected.selected = true; - } - }); - } - private sendSelected = debounce(() => { - const pony = this.selected; - const id = pony ? pony.id : 0; - const fetchEx = !!pony && !hasExtendedInfo(pony); - this.send(server => server.select(id, fetchEx ? SelectFlags.FetchEx : SelectFlags.None)); - }, 300); - load() { - return loadAndInitSpriteSheets() - .then(initializeTileHeightmaps); - } - init() { - this.canvas = document.getElementById('canvas') as HTMLCanvasElement; - this.updateTileSets(); - this.input.initialize(this.canvas); - - if (!this.initialized) { - this.canvas.addEventListener('webglcontextlost', e => { - e.preventDefault(); - DEVELOPMENT && console.warn('Context lost'); - this.errorReporter.captureEvent({ name: 'Context lost' }); - }); - - this.canvas.addEventListener('webglcontextrestored', () => { - DEVELOPMENT && console.warn('Context restored'); - this.errorReporter.captureEvent({ name: 'Context restored' }); - - if (this.webgl) { - this.webgl = initWebGLResources(this.webgl.gl, this.paletteManager, this.camera); - } - }); - - this.initialized = true; - - const stats = document.getElementById('stats') as HTMLElement; - this.statsText = document.createTextNode(''); - stats.appendChild(this.statsText); - - this.input.onReleased(Key.KEY_O, () => this.zoomOut()); - this.input.onReleased(Key.KEY_P, () => this.zoomIn()); - this.input.onReleased(Key.GAMEPAD_BUTTON_Y, () => this.changeScale()); - - this.input.onPressed(Key.ENTER, () => this.onChat.next()); - this.input.onPressed(Key.ESCAPE, () => this.escape()); - this.input.onPressed(Key.GAMEPAD_BUTTON_X, () => this.onToggleChat.next()); - // this.input.onPressed(Key.BACKSPACE, () => this.backspace()); - this.input.onPressed(Key.KEY_H, () => turnHeadAction(this)); - this.input.onPressed(Key.FORWARD_SLASH, () => this.onCommand.next()); - this.input.onPressed([Key.KEY_B, Key.GAMEPAD_BUTTON_B, Key.TOUCH_SECOND_CLICK], () => boopAction(this)); - this.input.onPressed([Key.KEY_E, Key.GAMEPAD_BUTTON_A], () => { - interact(this, this.input.isPressed(Key.SHIFT)); - }); - this.input.onPressed([Key.KEY_X, Key.GAMEPAD_BUTTON_DOWN], () => downAction(this)); - this.input.onPressed([Key.KEY_C, Key.GAMEPAD_BUTTON_UP], () => upAction(this)); - this.input.onPressed(Key.F2, () => { - if (!this.settings.browser.disableFKeys) { - this.hideText = !this.hideText; - this.hidePublicChat = false; - } - }); - this.input.onPressed(Key.F3, () => { - if (!this.settings.browser.disableFKeys) { - this.hideText = false; - this.hidePublicChat = !this.hidePublicChat; - } - }); - this.input.onPressed(Key.F4, () => { - if (!this.settings.browser.disableFKeys) { - this.settings.account.seeThroughObjects = !this.settings.account.seeThroughObjects; - this.settings.saveAccountSettings(this.settings.account); - } - }); - - [ - Key.KEY_1, Key.KEY_2, Key.KEY_3, Key.KEY_4, Key.KEY_5, Key.KEY_6, - Key.KEY_7, Key.KEY_8, Key.KEY_9, Key.KEY_0, Key.DASH, Key.EQUALS, - ].forEach((key, index) => this.input.onPressed(key, () => { - if (this.actions[index]) { - this.zone.run(() => useAction(this, this.actions[index].action)); - } - })); - - const addDebugShortcut = (num: number, name: string, action: () => void) => { - this.input.onPressed(numpad[num], () => { - if (!this.input.isPressed(Key.SHIFT)) { - this.apply(action); - } - }); - this.debugShortcuts.push(`${num} - ${name}`); - this.debugShortcuts.sort(); - }; - - // const addDebugShortcutShift = (num: number, name: string, action: () => void) => { - // this.input.onPressed(numpad[num], () => { - // if (this.input.isPressed(Key.SHIFT)) { - // this.apply(action); - // } - // }); - // this.debugShortcuts.push(`${num} (shift) - ${name}`); - // this.debugShortcuts.sort(); - // }; - - if (BETA) { - // editor - this.input.onPressed(Key.BACKSPACE, () => { - if (this.mod) { - this.send(server => server.editorAction({ type: 'undo' })); - } - }); - this.input.onPressed(Key.DELETE, this.applied(() => { - const entities = this.editor.selectedEntities.map(e => e.id); - this.send(server => server.editorAction({ type: 'remove', entities })); - this.editor.selectedEntities.length = 0; - })); - - [ - { key: Key.LEFT, dx: -1 / tileWidth, dy: 0 }, - { key: Key.RIGHT, dx: 1 / tileWidth, dy: 0 }, - { key: Key.UP, dx: 0, dy: -1 / tileHeight }, - { key: Key.DOWN, dx: 0, dy: 1 / tileHeight }, - ].forEach(({ key, dx, dy }) => this.input.onPressed(key, () => { - this.editor.selectedEntities.forEach(({ id, x, y }) => { - this.send(server => server.editorAction({ - type: 'move', - entities: [{ id, x: x + dx, y: y + dy }], - })); - }); - })); - - // debug - this.input.onReleased(Key.KEY_M, () => this.showMinimap = !this.showMinimap); - this.input.onPressed(Key.KEY_G, () => { - if (this.input.isPressed(Key.SHIFT)) { - let faceDir = 0; - let dir = 1; - this.player!.doAction = DoAction.Swing; - const state = this.player!.ponyState; - - const interval = setInterval(() => { - faceDir += dir; - - if (faceDir < 0) { - clearInterval(interval); - return; - } - - state.headTurn = faceDir; - - if (faceDir === 3) { - turnHeadAction(this); - } - - if (faceDir >= 6) { - dir = -1; - } - }, 1000 / 24); - } else { - let faceDir = 0; - const state = this.player!.ponyState; - - const interval = setInterval(() => { - faceDir++; - state.headTurn = faceDir; - - if (faceDir === 3) { - turnHeadAction(this); - } - - if (faceDir >= 7) { - clearInterval(interval); - } - }, 1000 / 24); - } - }); - addDebugShortcut(1, 'show info at cursor', () => { - this.debug.showInfo = !this.debug.showInfo; - this.saveDebug(); - }); - addDebugShortcut(2, 'show water bounds', () => { - this.drawOptions.showHeightmap = !this.drawOptions.showHeightmap; - }); - addDebugShortcut(3, 'show collision map', () => { - this.drawOptions.showColliderMap = !this.drawOptions.showColliderMap; - }); - addDebugShortcut(4, 'show helpers', () => { - this.debug.showHelpers = !this.debug.showHelpers; - this.saveDebug(); - }); - addDebugShortcut(5, 'show tile indices', () => { - this.drawOptions.tileIndices = !this.drawOptions.tileIndices; - }); - addDebugShortcut(6, 'show tile grid', () => { - this.drawOptions.tileGrid = !this.drawOptions.tileGrid; - }); - addDebugShortcut(7, 'grayscale', () => { - document.documentElement.style.filter = document.documentElement.style.filter ? null : 'grayscale(100%)'; - }); - addDebugShortcut(8, 'show regions', () => { - this.debug.showRegions = !this.debug.showRegions; - this.saveDebug(); - }); - } - - if (DEVELOPMENT) { - let showingRange = false; - addDebugShortcut(9, 'show chatlog range', () => { - showingRange = !showingRange; - updateRangeIndicator(showingRange ? this.settings.account.chatlogRange : undefined, this); - }); - this.input.onPressed(Key.F6, () => { - this.cameraShiftOn = !this.cameraShiftOn; - this.cameraShiftTarget = 400; - }); - this.input.onPressed(Key.F7, () => { - this.debug.showPalette = !this.debug.showPalette; - this.saveDebug(); - }); - this.input.onPressed(Key.F8, () => { - }); - - let loseContext: WEBGL_lose_context | null = null; - - this.input.onPressed(Key.F9, () => { - if (loseContext) { - loseContext.restoreContext(); - loseContext = null; - } else { - loseContext = this.webgl!.gl.getExtension('WEBGL_lose_context')!; - loseContext.loseContext(); - } - }); - this.input.onPressed(Key.F10, () => { - this.settings.browser.brightNight = !this.settings.browser.brightNight; - }); - this.input.onPressed(Key.KEY_R, this.applied(() => { - if (!Date.now() && this.player) { - const bounds = getInteractBounds(this.player); - const entities = this.map.entities.filter(e => - e !== this.player && boundsIntersect(e.x, e.y, e.bounds, 0, 0, bounds)); - - if (entities.length) { - const entity = entities[0]; - const typeName = getEntityTypeName(entity.type); - this.announce(`${typeName}${entities.length > 1 ? ` (1 of ${entities.length})` : ''}`); - } else { - this.announce('nothing'); - } - } - - // if (this.player) this.player.swimming = !this.player.swimming; - // this.editorElevation = ''; - // this.editorSpecial = this.editorSpecial ? '' : 'ramp-e'; - })); - this.input.onPressed(Key.KEY_J, () => { - if (this.player) { - this.player.ponyState.headTilt = (this.player.ponyState.headTilt || 0) + 0.5; - } - }); - this.input.onPressed(Key.KEY_K, () => { - if (this.player) { - this.player.ponyState.headTilt = (this.player.ponyState.headTilt || 0) - 0.5; - } - }); - this.input.onPressed(Key.KEY_L, () => this.player && setHeadAnimation(this.player, nom)); - this.input.onReleased(Key.KEY_Q, () => this.send(server => server.leave())); - this.input.onReleased(Key.KEY_T, () => this.toggleDisableLighting()); - this.input.onReleased(Key.KEY_U, () => { - if (this.player) { - console.log( - `position: ${this.player.x.toFixed(2)}, ${this.player.y.toFixed(2)} ` + - `region: ${Math.floor(this.player.x / REGION_SIZE)}, ${Math.floor(this.player.y / REGION_SIZE)}`); - } - }); - this.input.onReleased(Key.KEY_I, () => { - const state = this.player!.ponyState; - state.flags = setFlag(state.flags, PonyStateFlags.CurlTail, !hasFlag(state.flags, PonyStateFlags.CurlTail)); - }); - // this.input.onReleased(Key.KEY_N, () => this.engine = (this.engine + 1) % Engine.Total); - this.input.onPressed(Key.KEY_N, () => this.audio.playRandomTrack()); - // this.input.onPressed(Key.KEY_G, () => this.wind = Math.max(0, this.wind - 1)); - // this.input.onReleased(Key.KEY_M, () => this.send(server => server.editorAction({ type: 'party' }))); - this.input.onPressed(Key.F8, () => toggleWalls()); - this.input.onPressed(Key.COMMA, () => this.deltaMultiplier = 0.5); - this.input.onPressed(Key.PERIOD, () => this.deltaMultiplier = 2); - } - - window.addEventListener('resize', () => { - this.resized = true; - DEVELOPMENT && log(`resized ${window.innerHeight} (${window.scrollY})`); - }); - - this.canvas.addEventListener('touchstart', () => this.audio.touch()); - } - - this.resized = true; - - if (!this.webgl) { - this.initWebGL(); - } - } - leave() { - if (this.socket) { - if (this.socket.isConnected) { - this.socket.server.leave(); - } else { - this.socket.disconnect(); - } - } - } - joined() { - this.connectedTime = Math.round(performance.now()); - this.onJoined.next(); - } - togglePixelRatio() { - pixelRatioEnabled = !pixelRatioEnabled; - } - private escape() { - if (this.socket && !this.onCancel()) { - this.select(undefined); - } - } - backspace() { - if (this.player && this.player.says !== undefined) { - this.send(server => server.say(0, '.', ChatType.Dismiss)); - } - } - private initWebGL() { - this.errorReporter.captureEvent({ name: 'game.initWebGL' }); - - if (!this.canvas) { - throw new Error('Missing canvas'); - } - - try { - this.resizeCamera(); - this.webgl = initWebGL(this.canvas, this.paletteManager, this.camera); - const { failedFBO, palettes, renderer } = this.webgl; - - if (renderer) { - this.errorReporter.configureData({ renderer }); - } - - if (failedFBO) { - this.errorReporter.captureEvent({ name: 'game.initWebGL failed FBO' }); - } - - this.offlinePony = createPony(0, 0, OFFLINE_PONY, palettes.defaultPalette, this.paletteManager); - this.supporterPony = createPony(0, 0, SUPPORTER_PONY, palettes.defaultPalette, this.paletteManager); - initializeToys(this.paletteManager); - } catch (e) { - this.errorReporter.captureEvent({ name: 'failed game.initWebGL', error: e.message, stack: e.stack }); - this.releaseWebGL(); - DEVELOPMENT && console.error(e); - throw new Error(`Failed to initialize graphics device (${e.message})`); - } - } - private releaseWebGL() { - this.errorReporter.captureEvent({ name: 'game.releaseWebGL' }); - - if (this.webgl) { - try { - this.paletteManager.dispose(this.webgl.gl); - disposeWebGL(this.webgl); - } catch (e) { - DEVELOPMENT && console.error(e); - } - - this.webgl = undefined; - } - } - private resizeCamera() { - if (this.canvas) { - const actualScale = this.scale * integerPixelRatio(); - const w = clamp(Math.ceil(this.canvas.width / actualScale), CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX); - const h = clamp(Math.ceil(this.canvas.height / actualScale), CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX); - - if (this.camera.w !== w || this.camera.h !== h) { - this.camera.w = w; - this.camera.h = h; - this.resizedCamera = true; - } - } - } - release() { - this.settings.saving(() => false); - this.loaded = false; - this.fullyLoaded = false; - this.player = undefined; - this.selected = undefined; - this.party = undefined; - this.rightOverride = undefined; - this.headTurnedOverride = undefined; - this.stateOverride = undefined; - this.notifications = []; - this.map = createWorldMap(); - this.camera = createCamera(); - - if (this.socket) { - this.socket.disconnect(); - this.socket = undefined; - } - - this.audio.stop(); - this.input.release(); - this.releaseWebGL(); - } - startup(socket: ClientSocketService, mod: boolean) { - if (this.settings.account.actions) { - this.actions = deserializeActions(this.settings.account.actions); - } else { - this.actions = createDefaultButtonActions(); - } - - const saveSettings = (settings: AccountSettings) => this.send(server => server.saveSettings(settings)); - const saveSettingsDebounced = debounce(saveSettings, 1500); - - this.element = document.getElementById('app-game')!; - this.settings.saving(settings => (saveSettingsDebounced(settings), true)); - this.lastChatMessageType = ChatType.Say; - this.selected = undefined; - this.party = undefined; - this.socket = socket; - this.audio.setVolume(this.volume); - // this.audio.play(); - this.mod = mod; - this.nextFriendsCRC = performance.now() + 5 * SECOND; - - if (DEVELOPMENT) { - initLogger(message => this.onMessage.next({ - id: 1, crc: 1, name: 'log', type: MessageType.System, - message: `[${((performance.now() | 0) % 10000)}] ${message}` - })); - } - - if (DEVELOPMENT && LOG_POSITION) { - this.positions.length = 0; - } - } - private getPixelScale() { - return this.scale * (integerPixelRatio() / pixelRatio()); - } - update(delta: number, now: number, last: number) { - TIMING && timeStart('update'); - delta *= this.deltaMultiplier; - - const shiftSpeed = delta * 10; - - if (this.cameraShiftOn && this.camera.shiftRatio !== 1) { - this.camera.shiftRatio = Math.min(1, this.camera.shiftRatio + shiftSpeed); - } else if (!this.cameraShiftOn && this.camera.shiftRatio !== 0) { - this.camera.shiftRatio = Math.max(0, this.camera.shiftRatio - shiftSpeed); - } - - updateMap(this.map, delta); - - if (!this.socket || !this.socket.isConnected || !this.element) - return; - - this.updateGameTime(delta); - - if (this.lastPixelRatio !== pixelRatio()) { - this.lastPixelRatio = pixelRatio(); - this.resized = true; - } - - if (this.resized) { - this.resizeCanvas(); - } - - const player = this.player; - const camera = this.camera; - const input = this.input; - const server = this.socket.server; - - restorePlayerPosition(); - - input.disabledGamepad = !!this.settings.browser.disableGamepad; - input.update(); - - this.resizeCamera(); - this.updateCameraShift(); - - const actualScale = this.scale * integerPixelRatio(); - this.camera.offset = this.cameraShiftTarget / actualScale; - - let moved = false; - - if (player && this.loaded) { - if (BETA && this.editor.selectedEntities.length) { - input.disableArrows = true; - } - - this.showWallPlaceholder = player.hold === toggleWallsTool.type && hasFlag(this.map.flags, MapFlags.EditableWalls); - - if (this.highlightEntity) { - if (this.highlightEntity.id === 0) { - releaseEntity(this.highlightEntity); - } - - this.highlightEntity = undefined; - } - - const shift = input.isPressed(Key.SHIFT); - const x = this.fullyLoaded ? input.axisX : 0; - const y = this.fullyLoaded ? input.axisY : 0; - const dir = vectorToDir(x, y); - const vec = (x || y) ? dirToVector(dir) : { x: 0, y: 0 }; - const walk = input.isMovementFromButtons ? (this.settings.browser.walkByDefault ? !shift : shift) : false; - const flags = getMovementFlag(x, y, walk); - const speed = flagsToSpeed(flags); - const vx = vec.x * speed; - const vy = vec.y * speed; - - if (BETA) { - input.disableArrows = false; - } - - if (player.vx !== vx || player.vy !== vy) { - if (vx === 0) { - player.x = roundPositionX(player.x) + 0.5 / tileWidth; - } - - if (vy === 0) { - player.y = roundPositionY(player.y) + 0.5 / tileHeight; - } - - const time = (last - this.connectedTime) >>> 0; - const [a, b, c, d, e] = encodeMovement(player.x, player.y, dir, flags, time, camera); - server.move(a, b, c, d, e); - moved = true; - this.resizedCamera = false; - } - - if ((vx || vy) && (isPonySitting(player) || isPonyLying(player))) { - player.state = setPonyState(player.state, EntityState.PonyStanding); - } - - updateEntityVelocity(this.map, player, vx, vy); - - const facingRight = isFacingRight(player); - const right = isMovingRight(vx, facingRight); - - if (facingRight !== right) { - player.state = setFlag(player.state, EntityState.FacingRight, right); - player.state = setFlag(player.state, EntityState.HeadTurned, false); - this.rightOverride = right; - } - - updateCamera(camera, player, this.map); - - const scale = this.getPixelScale(); - const hover = screenToWorld(camera, point(input.pointerX / scale, input.pointerY / scale)); - - if (input.usingTouch && !input.wasPressed(Key.TOUCH_CLICK) && !input.isPressed(Key.TOUCH)) { - hover.x = 0; - hover.y = 0; - } - - this.hover = hover; - - if (this.fullyLoaded) { - if (BETA && this.editor.draggingEntities) { - editorDragEntities(this, hover, input.isPressed(Key.MOUSE_BUTTON2)); - } - - if (hasFlag(this.map.flags, MapFlags.EditableEntities)) { - if (player.hold === removeEntitiesTool.type) { - this.highlightEntity = pickEntities(this.map, hover, true, false, true)[0]; - } else if (player.hold === placeEntitiesTool.type) { - if (!isOutsideMap(hover.x, hover.y, this.map)) { - const { type } = placeableEntities[this.placeEntity]; - let { x, y } = hover; - - if (this.map.editableArea) { - x = clamp(x, this.map.editableArea.x, this.map.editableArea.x + this.map.editableArea.w); - y = clamp(y, this.map.editableArea.y, this.map.editableArea.y + this.map.editableArea.h); - } - - this.highlightEntity = createAnEntity(type, 0, x, y, {}, this.paletteManager, this); - } - } - } - - if (input.wasPressed(Key.MOUSE_BUTTON1) || input.wasPressed(Key.TOUCH_CLICK)) { - const pickedEntities = pickEntities(this.map, hover, shift, this.mod); - const pickedEntity = pickedEntities[(pickedEntities.indexOf(this.selected!) + 1) % pickedEntities.length]; - const holdingRemoveTool = player.hold === removeEntitiesTool.type; - const editableMap = hasFlag(this.map.flags, MapFlags.EditableEntities); - - if (BETA && this.editor.selectingEntities) { - editorSelectEntities(this, hover, shift); - } else if (pickedEntity && (!holdingRemoveTool || !editableMap || hasFlag(pickedEntity.flags, EntityFlags.IgnoreTool))) { - if (pickedEntity.type === PONY_TYPE) { - this.select(pickedEntity as Pony); - } else if (entityInRange(pickedEntity, player)) { - server.interact(pickedEntity.id); - } - } else if (BETA && this.editor.tile !== -1) { - if (this.editor.brushSize > 1) { - const x = Math.floor((hover.x - (this.editor.brushSize / 2))); - const y = Math.floor((hover.y - (this.editor.brushSize / 2))); - server.editorAction({ type: 'tile', x, y, tile: this.editor.tile, size: this.editor.brushSize }); - } else { - const x = hover.x | 0; - const y = hover.y | 0; - const type = this.editor.tile === getTile(this.map, hover.x, hover.y) ? TileType.Dirt : this.editor.tile; - server.changeTile(x, y, type); - } - } else if (player.hold === changeTileTool.type && hasFlag(this.map.flags, MapFlags.EditableTiles)) { - const x = hover.x | 0; - const y = hover.y | 0; - server.changeTile(x, y, houseTiles[this.placeTile].type); - } else if (player.hold === toggleWallsTool.type && hasFlag(this.map.flags, MapFlags.EditableWalls)) { - toggleWall(this, hover); - } else if (holdingRemoveTool && this.highlightEntity && editableMap) { - const id = this.highlightEntity.id; - this.send(server => server.actionParam(Action.RemoveEntity, id)); - } else if (player.hold === placeEntitiesTool.type && this.highlightEntity && editableMap) { - const { x, y, type } = this.highlightEntity; - this.send(server => server.actionParam(Action.PlaceEntity, { x, y, type })); - } else if (this.selected) { - this.select(undefined); - } else if (hasFlag(this.map.flags, MapFlags.EdibleGrass)) { - const tile = getTile(this.map, hover.x, hover.y); - - if (isValidTile(tile) && distanceXY(player.x, player.y, hover.x, hover.y) < TILE_CHANGE_RANGE) { - const x = hover.x | 0; - const y = hover.y | 0; - let type = tile === TileType.Grass ? TileType.Dirt : TileType.Grass; - server.changeTile(x, y, type); - } - } else if (DEVELOPMENT && this.engine === Engine.LayeredTiles && this.editor.elevation) { - const value = getElevation(this.map, hover.x, hover.y); - setElevation(this.map, hover.x, hover.y, clamp(this.editor.elevation === 'up' ? value + 1 : value - 1, 0, 10)); - } - } - - if (BETA && input.wasPressed(Key.MOUSE_BUTTON2)) { - if (this.editor.selectingEntities) { - editorMoveEntities(this, hover); - } else if (this.mod) { - toggleWall(this, hover); - } - } - - if (BETA && input.wasPressed(Key.MOUSE_BUTTON3)) { - if (this.mod) { - server.editorAction({ type: 'place', entity: this.editor.type, x: hover.x, y: hover.y }); - console.log(`${this.editor.type}(${hover.x.toFixed(2)}, ${hover.y.toFixed(2)})`); - } - } - - if (this.player && input.wheelY) { - if (input.isPressed(Key.SHIFT)) { - if (hasFlag(this.map.flags, MapFlags.EditableEntities)) { - const action = input.wheelY < 0 ? Action.SwitchTool : Action.SwitchToolRev; - this.send(server => server.action(action)); - } - } else { - if (this.player.hold === placeEntitiesTool.type) { - this.changePlaceEntity(input.wheelY < 0); - } else if (this.player.hold === changeTileTool.type) { - this.changePlaceTile(input.wheelY < 0); - } - } - } - - const isHeadTurned = hasFlag(player.state, EntityState.HeadTurned); - const isHeadFacingRight = right ? !isHeadTurned : isHeadTurned; - - if (((input.axis2X < 0 && isHeadFacingRight) || (input.axis2X > 0 && !isHeadFacingRight))) { - if (server.action(Action.TurnHead) as any) { - player.state = (player.state) ^ EntityState.HeadTurned; - } - } - } - } - - if (player) { - const safe = hasFlag(this.worldFlags, WorldStateFlags.Safe); - updateEntities(this, this.time, delta, safe); - savePlayerPosition(); - } - - if (this.changedScale) { - this.changedScale = false; - this.resizedCamera = true; - - if (player) { - centerCameraOn(camera, player); - } - } - - if (player) { - updateCamera(camera, player, this.map); - } - - if (this.resizedCamera) { - server.updateCamera(camera.x, camera.y, camera.w, camera.h); - this.resizedCamera = false; - } - - if (player) { - updateEntitiesCoverLifted(this.map, player, !!this.settings.account.seeThroughObjects, delta); - updateEntitiesWithNames(this.map, this.hover, player); - updateEntitiesTriggers(this.map, player, this); - } - - input.end(); - - if (this.nextFriendsCRC < now) { - this.send(server => server.actionParam(Action.FriendsCRC, this.model.computeFriendsCRC())); - this.nextFriendsCRC = now + 15 * MINUTE; - } - - const threshold = Date.now() - 10 * SECOND; - - for (let i = this.incompleteSays.length - 1; i >= 0; i--) { - if (this.incompleteSays[i].time < threshold) { - this.incompleteSays.splice(i, 1); - } - } - - this.updateSocketStats(delta); - TIMING && timeEnd(); - - if (DEVELOPMENT && LOG_POSITION) { - if (this.player) { - this.positions.push({ x: this.player.x, y: this.player.y, moved }); - } - } - } - private updateGameTime(delta: number) { - if (this.baseTime !== this.targetBaseTime) { - const timeDelta = Math.floor(delta * 0.2 * HOUR); - const baseTime = this.baseTime + timeDelta * (this.baseTime > this.targetBaseTime ? -1 : 1); - - if (Math.abs(this.targetBaseTime - baseTime) < timeDelta) { - this.baseTime = this.targetBaseTime; - } else { - this.baseTime = baseTime; - } - } - - this.time = this.baseTime + performance.now(); - } - private updateSocketStats(delta: number) { - this.timeSize += delta; - - if (this.timeSize > 1) { - this.sent = 8 * this.socket!.sentSize / this.timeSize / 1024; - this.recv = 8 * this.socket!.receivedSize / this.timeSize / 1024; - this.socket!.sentSize = 0; - this.socket!.receivedSize = 0; - this.timeSize = 0; - } - } - setWorldState(state: WorldState, initial: boolean) { - this.season = state.season; - this.holiday = state.holiday; - this.worldFlags = state.flags; - this.lightData = createLightData(this.season); - initFeatureFlags(state.featureFlags); - - const baseTime = state.time - performance.now(); - - if (initial) { - this.baseTime = this.targetBaseTime = baseTime; - } else { - this.targetBaseTime = baseTime; - } - - this.updateTileSets(); - - if (this.model.friends) { - for (const friend of this.model.friends) { - friend.actualName = filterEntityName(this, friend.name, friend.nameBad) || ''; - } - } - } - setPlayer(player: Pony) { - this.player = player; - centerCameraOn(this.camera, player); - this.send(server => server.loaded()); - } - setupMap() { - this.bg = colorToFloatArray(getTileColor(this.map.defaultTile, this.season)); - this.audio.initTracks(this.season, this.holiday, this.map.type); - this.audio.playOrSwitchToRandomTrack(); - this.updateTileSets(); - } - private updateTileSets() { - this.tileSets = updateTileSets(this.paletteManager, this.tileSets, this.season, this.map.type); - } - draw() { - redrawActionButtons(this.actionsChanged); - this.actionsChanged = false; - - if (!this.webgl) - return; - - if (this.webgl.gl.isContextLost()) { - DEVELOPMENT && console.warn('Context is lost'); - return; - } - - // start frame - const now = performance.now(); - - if ((now - this.lastDraw) < this.frameDelay) { - return; - } - - this.frames++; - - if ((now - this.lastFps) > 1000) { - this.drawFps = this.frames * 1000 / (now - this.lastFps); - this.frames = 0; - this.lastFps = now; - } - - this.lastDraw = now; - - // draw - const { - gl, frameBuffer, frameBufferSheet, spriteShader, spriteBatch, lightShader, paletteBatch, paletteShader, - palettes, - } = this.webgl; - - TIMING && timeStart('draw'); - - TIMING && timeStart('draw init'); - let lightColor = WHITE; - let shadowColor = 0; - - if (this.map.type === MapType.Cave) { - lightColor = CAVE_LIGHT; - shadowColor = CAVE_SHADOW; - } else { - lightColor = getLightColor(this.lightData, this.time); - shadowColor = getShadowColor(this.lightData, this.time); - } - - if (BETA && this.editor.customLight) { - lightColor = parseColor(this.editor.lightColor); - shadowColor = this.shadowColor; - } - - const camera = this.camera; - const width = camera.w; - const height = camera.h; - const ratio = integerPixelRatio(); - const actualScale = this.scale * ratio; - const bg = this.bg; - - colorToExistingFloatArray(light, lightColor); - - const drawOptions = this.drawOptions; - drawOptions.gameTime = this.time; - drawOptions.lightColor = lightColor; - drawOptions.shadowColor = shadowColor; - drawOptions.drawHidden = this.mod; - drawOptions.season = this.season; - - if (DEVELOPMENT) { - drawOptions.debug = this.debug; - } - - if (DEVELOPMENT || BETA) { - drawOptions.engine = this.engine; - } - - ortho(this.fboMatrix, 0, gl.drawingBufferWidth / actualScale, gl.drawingBufferHeight / actualScale, 0, 0, 1000); - TIMING && timeEnd(); - - TIMING && timeStart('ensureAllVisiblePon...'); - ensureAllVisiblePoniesAreDecoded(this.map, camera, this.paletteManager); - TIMING && timeEnd(); - - TIMING && timeStart('commit+invalidatePalettes'); - if (this.paletteManager.commit(gl)) { - invalidatePalettes(this.map.entitiesDrawable); - } - TIMING && timeEnd(); - - if (this.settings.browser.brightNight) { - lerpColor(light, white, 0.3); - } - - if (this.engine === Engine.NewLighting) { - // ... - } else if (this.disableLighting) { - ortho(this.viewMatrix, camera.x, camera.x + camera.w, camera.actualY + camera.h, camera.actualY, 0, 1000); - lerpColor(light, white, 0.1); // adjust lighting for missing lights - - // color -> screen - gl.clearColor(bg[0], bg[1], bg[2], bg[3]); - gl.clear(gl.COLOR_BUFFER_BIT); - gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); - gl.disable(gl.DEPTH_TEST); - gl.enable(gl.BLEND); - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - - this.drawMap(this.webgl, this.map, this.viewMatrix, light, drawOptions); - } else { - ortho(this.viewMatrix, camera.x, camera.x + camera.w, camera.actualY, camera.actualY + camera.h, 0, 1000); - - TIMING && timeStart('initializeFrameBuffer'); - this.initializeFrameBuffer(this.webgl, width, height); - TIMING && timeEnd(); - - if (!frameBuffer) { - DEVELOPMENT && console.warn('No frame buffer'); - return; - } - - // color -> fbo - TIMING && timeStart('color -> fbo'); - // gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer.handle); - bindFrameBuffer(gl, frameBuffer); - gl.viewport(0, 0, frameBuffer.width, frameBuffer.height); - gl.clearColor(bg[0], bg[1], bg[2], bg[3]); - gl.clear(gl.COLOR_BUFFER_BIT); // | gl.DEPTH_BUFFER_BIT); - gl.viewport(0, 0, width, height); - gl.disable(gl.DEPTH_TEST); - //gl.depthFunc(gl.LEQUAL); - gl.enable(gl.BLEND); - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - this.drawMap(this.webgl, this.map, this.viewMatrix, white, drawOptions); - TIMING && timeEnd(); - - // color -> screen - TIMING && timeStart('color -> screen'); - // gl.bindFramebuffer(gl.FRAMEBUFFER, null); - unbindFrameBuffer(gl); - gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); - gl.clearColor(0, 0, 0, 1); - gl.clear(gl.COLOR_BUFFER_BIT); - // gl.disable(gl.DEPTH_TEST); - gl.disable(gl.BLEND); - - gl.useProgram(spriteShader.program); - gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); - gl.uniform4fv(spriteShader.uniforms.lighting, white); - gl.uniform1f(spriteShader.uniforms.textureSize, frameBufferSheet.texture!.width); - bindTexture(gl, 0, frameBufferSheet.texture); - spriteBatch.begin(); - spriteBatch.drawImage(WHITE, 0, 0, width, height, 0, 0, width, height); - spriteBatch.end(); - TIMING && timeEnd(); - - // light -> fbo - TIMING && timeStart('light -> fbo'); - // gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer.handle); - bindFrameBuffer(gl, frameBuffer); - gl.viewport(0, 0, frameBuffer.width, frameBuffer.height); - gl.clearColor(light[0], light[1], light[2], light[3]); - gl.clear(gl.COLOR_BUFFER_BIT); - gl.viewport(0, 0, width, height); - //gl.enable(gl.DEPTH_TEST); - gl.enable(gl.BLEND); - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.ONE, gl.ONE); - TIMING && timeEnd(); - - // shadows - //for (const e of map.entities) { - // if (e.drawShadow && camera.isBoundVisible(e.shadowBounds || e.bounds, e.x, e.y)) { - // this.spriteBatch.depth = camera.mapDepth(e.y); - // e.drawShadow(this.spriteBatch); - // } - //} - - // soft lights - TIMING && timeStart('drawEntityLights'); - gl.useProgram(lightShader.program); - gl.uniformMatrix4fv(lightShader.uniforms.transform, false, this.viewMatrix); - gl.uniform4fv(lightShader.uniforms.lighting, white); - spriteBatch.begin(); - drawEntityLights(spriteBatch, this.map.entitiesLight, this.camera, drawOptions); - spriteBatch.end(); - TIMING && timeEnd(); - - // light sprites - TIMING && timeStart('drawEntityLightSprites'); - gl.useProgram(spriteShader.program); - gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.viewMatrix); - gl.uniform4fv(spriteShader.uniforms.lighting, white); - gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); - bindTexture(gl, 0, normalSpriteSheet.texture); - spriteBatch.begin(); - drawEntityLightSprites(spriteBatch, this.map.entitiesLightSprite, this.camera, drawOptions); - spriteBatch.end(); - TIMING && timeEnd(); - - // light -> screen - TIMING && timeStart('light -> screen'); - // gl.bindFramebuffer(gl.FRAMEBUFFER, null); - unbindFrameBuffer(gl); - gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); - gl.enable(gl.BLEND); - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.DST_COLOR, gl.ZERO); - - gl.useProgram(spriteShader.program); - gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); - gl.uniform4fv(spriteShader.uniforms.lighting, white); - gl.uniform1f(spriteShader.uniforms.textureSize, frameBufferSheet.texture!.width); - bindTexture(gl, 0, frameBufferSheet.texture); - spriteBatch.begin(); - spriteBatch.drawImage(WHITE, 0, 0, width, height, 0, 0, width, height); - spriteBatch.end(); - TIMING && timeEnd(); - } - - // ui -> screen - gl.enable(gl.BLEND); - gl.blendEquation(gl.FUNC_ADD); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - - TIMING && timeStart('drawNames+drawChat'); - gl.useProgram(paletteShader.program); - gl.uniformMatrix4fv(paletteShader.uniforms.transform, false, this.fboMatrix); - gl.uniform4fv(paletteShader.uniforms.lighting, white); - gl.uniform1f(paletteShader.uniforms.pixelSize, this.paletteManager.pixelSize); - gl.uniform1f(paletteShader.uniforms.textureSize, paletteSpriteSheet.texture!.width); - bindTexture(gl, 0, paletteSpriteSheet.texture); - bindTexture(gl, 1, this.paletteManager.texture); - paletteBatch.begin(); - - if (!this.hideText) { - drawNames( - paletteBatch, this.map.entitiesWithNames, this.player, this.party, this.camera, this.hover, this.mod, palettes); - drawChat( - paletteBatch, this.map.entitiesWithChat, this.camera, this.mod, palettes, this.hidePublicChat); - } - - if (!this.socket || !this.socket.isConnected) { - this.drawMessage(this.webgl, 'Connecting...'); - } else if (!this.loaded) { - if (this.placeInQueue) { - this.drawMessage(this.webgl, `Waiting in queue (${this.placeInQueue})`); - } else { - this.drawMessage(this.webgl, 'Loading...'); - } - } else if ((performance.now() - this.socket.lastPacket) > CONNECTION_ISSUE_TIMEOUT) { - // this.drawMessage('Connection issues...'); - } - - if (BETA && this.debug.showInfo) { - try { - const scale = this.getPixelScale(); - const x = this.input.pointerX / scale + 5; - const y = this.input.pointerY / scale; - const height = getMapHeightAt(this.map, this.hover.x, this.hover.y, this.time); - drawText(paletteBatch, `${height.toFixed(2)}`, fontSmallPal, BLACK, x, y); - } catch (e) { - console.warn(e.message); - } - } - - if (this.showWallPlaceholder) { - const x = this.hover.x | 0; - const y = this.hover.y | 0; - const dx = this.hover.x - x; - const dy = this.hover.y - y; - const palette = this.webgl.palettes.defaultPalette; - const color = makeTransparent(WHITE, 0.6); - const screenX = toScreenX(x) - this.camera.x; - const screenY = toScreenY(y) - this.camera.actualY; - - if (x >= 0 && y >= 0 && x < this.map.width && y < this.map.height) { - if (dx > dy) { - if ((dx + dy) < 1) { - paletteBatch.drawSprite(wall_h_placeholder.color, color, palette, screenX, screenY - 15); - } else { - paletteBatch.drawSprite(wall_v_placeholder.color, color, palette, screenX + tileWidth - 4, screenY - 12); - } - } else { - if ((dx + dy) < 1) { - paletteBatch.drawSprite(wall_v_placeholder.color, color, palette, screenX - 4, screenY - 12); - } else { - paletteBatch.drawSprite(wall_h_placeholder.color, color, palette, screenX, screenY + tileHeight - 15); - } - } - } - } - - paletteBatch.end(); - TIMING && timeEnd(); - - gl.useProgram(spriteShader.program); - gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); - gl.uniform4fv(spriteShader.uniforms.lighting, white); - - if (BETA && this.showMinimap && this.minimap) { - gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); - bindTexture(gl, 0, normalSpriteSheet.texture); - spriteBatch.begin(); - spriteBatch.save(); - - const { width, height, data } = this.minimap; - const scale = 4 / this.scale; - - spriteBatch.translate(100, 100); - spriteBatch.scale(scale, scale); - - spriteBatch.drawRect(BLACK, -1, -1, width + 2, height + 2); - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - spriteBatch.drawRect(data[x + y * width], x, y, 1, 1); - } - } - - if (this.player) { - spriteBatch.drawRect(RED, Math.floor(this.player.x), Math.floor(this.player.y), 1, 1); - } - - spriteBatch.restore(); - spriteBatch.end(); - } - - if (BETA && this.debug.showRegions && this.player) { - gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); - bindTexture(gl, 0, normalSpriteSheet.texture); - spriteBatch.begin(); - drawDebugRegions(spriteBatch, this.map, this.player, this.camera); - spriteBatch.end(); - } - - const showFPS = !!this.settings.browser.showFps; - const showHelp = BETA && this.input.isPressed(Key.F1); - const showPalette = DEVELOPMENT && this.debug.showPalette; - - if (showFPS || showHelp || showPalette) { - // 1 to 1 pixel scale drawing - TIMING && timeStart('showFps'); - const scale = 2; - // const height = gl.drawingBufferHeight / (ratio * scale); - ortho(this.fboMatrix, 0, gl.drawingBufferWidth / ratio, gl.drawingBufferHeight / ratio, 0, 0, 1000); - gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); - gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); - bindTexture(gl, 0, normalSpriteSheet.texture); - spriteBatch.begin(); - spriteBatch.save(); - spriteBatch.scale(scale, scale); - - if (showFPS) { - drawText(spriteBatch, this.drawFps.toFixed(), fontSmall, BLACK, 2, 2); - - if (this.timingsText) { - const size = measureText(this.timingsText, fontMono); - spriteBatch.drawRect(0x000000aa, 2, 26, 220, size.h + 8); - drawText(spriteBatch, this.timingsText, fontMono, WHITE, 2, 30); - } - } - - if (BETA && showHelp) { - let y = 25; - - for (const shortcut of this.debugShortcuts) { - drawOutlinedText(spriteBatch, shortcut, font, WHITE, BLACK, 5, y); - y += 10; - } - } - - // if (DEVELOPMENT) { - // const width = gl.drawingBufferWidth / (ratio * scale); - // const { isCollidingCount, isCollidingObjectCount } = getCollisionStats(); - // const text = - // `${isCollidingCount.toString().padStart(7)} calls\n` + - // `${isCollidingObjectCount.toString().padStart(7)} total checks\n` + - // `${this.markedColliding.toString().padStart(7)} player checks`; - // const size = measureText(text, fontMono); - // const x = width - 160; - // const y = 26; - // spriteBatch.drawRect(0x000000aa, x, y, 150, size.h + 10); - // drawText(spriteBatch, text, fontMono, WHITE, x + 5, y + 5); - // } - - spriteBatch.restore(); - spriteBatch.end(); - - if (DEVELOPMENT && showPalette) { - const paletteTexture = this.paletteManager.texture!; - const { width, height } = paletteTexture; - - gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); - bindTexture(gl, 0, normalSpriteSheet.texture); - spriteBatch.begin(); - spriteBatch.drawRect(0x00000066, 20, 20, width, height); - spriteBatch.end(); - - gl.uniform1f(spriteShader.uniforms.textureSize, width); - bindTexture(gl, 0, paletteTexture); - spriteBatch.begin(); - spriteBatch.drawImage(WHITE, 0, 0, width, height, 20, 20, width, height); - spriteBatch.end(); - } - - TIMING && timeEnd(); - } - - bindTexture(gl, 0, undefined); - bindTexture(gl, 1, undefined); - gl.useProgram(null); - - TIMING && timeEnd(); - this.updateStatsText(); - - TIMING && timeStart('messageQueue'); - while (this.messageQueue.length) { - this.onMessage.next(this.messageQueue.shift()!); - } - TIMING && timeEnd(); - - TIMING && timeStart('onFrame'); - this.onFrame.next(); - TIMING && timeEnd(); - } - private drawMessage({ paletteBatch, palettes }: WebGL, message: string) { - drawFullScreenMessage(paletteBatch, this.camera, message, palettes.mainFont.white); - } - private drawMap(webgl: WebGL, map: WorldMap, viewMatrix: Matrix4, lighting: Float32Array, options: DrawOptions) { - const { gl, paletteBatch, paletteShader } = webgl; - - TIMING && timeStart('drawMap'); - if (this.tileSets && this.player) { - gl.useProgram(paletteShader.program); - gl.uniformMatrix4fv(paletteShader.uniforms.transform, false, viewMatrix); - gl.uniform4fv(paletteShader.uniforms.lighting, lighting); - gl.uniform1f(paletteShader.uniforms.pixelSize, this.paletteManager.pixelSize); - gl.uniform1f(paletteShader.uniforms.textureSize, paletteSpriteSheet.texture!.width); - bindTexture(gl, 0, paletteSpriteSheet.texture); - bindTexture(gl, 1, this.paletteManager.texture); - paletteBatch.begin(); - this.entitiesDrawn = drawMap( - paletteBatch, map, this.camera, this.player, options, this.tileSets, this.editor.selectedEntities); - paletteBatch.end(); - - if (this.highlightEntity && this.highlightEntity.draw) { - gl.uniform4fv(paletteShader.uniforms.lighting, highlightColor); - paletteBatch.begin(); - this.highlightEntity.draw(paletteBatch, this.drawOptions); - paletteBatch.end(); - } - } - TIMING && timeEnd(); - } - private updateCameraShift() { - if (isMobile) { - const isKeyboardOpen = !!document.activeElement && /input/i.test(document.activeElement.tagName); - - if (this.lastIsKeyboardOpen !== isKeyboardOpen) { - DEVELOPMENT && log(`keyboard open ${isKeyboardOpen}`); - this.lastIsKeyboardOpen = isKeyboardOpen; - } - - if (isKeyboardOpen) { - if (!this.cameraShiftOn && window.scrollY > 100) { - this.cameraShiftOn = true; - this.cameraShiftTarget = window.scrollY; - - if (DEVELOPMENT) { - log(`shift camera ${this.cameraShiftTarget} (${this.windowHeight} - ${window.innerHeight}, ${window.scrollY})`); - } - } - } else { - if (this.cameraShiftOn && window.scrollY < 100) { - this.cameraShiftOn = false; - DEVELOPMENT && log(`unshift camera`); - } - } - } - } - private resizeCanvas() { - pixelRatioCache = getPixelRatio(); - const canvas = this.canvas; - const ratio = pixelRatio(); - const rect = this.element!.getBoundingClientRect(); - this.windowWidth = rect.width; - this.windowHeight = rect.height; - let w = Math.ceil(this.windowWidth * ratio); - let h = Math.ceil(this.windowHeight * ratio); - - while ((w % 12) !== 0) { - w++; - } - - while ((h % 12) !== 0) { - h++; - } - - if (canvas && w && h && (canvas.width !== w || canvas.height !== h || this.lastCanvasRatio !== ratio)) { - canvas.width = w; - canvas.height = h; - canvas.style.width = `${w / ratio}px`; - canvas.style.height = `${h / ratio}px`; - this.lastCanvasRatio = ratio; - this.resized = false; - DEVELOPMENT && log(`scrollY: ${window.scrollY}`); - } - } - private initializeFrameBuffer({ gl, frameBuffer }: WebGL, width: number, height: number) { - const targetSize = getRenderTargetSize(width, height); - - if (frameBuffer && targetSize !== frameBuffer.width) { - const maxSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); - - if (maxSize != null && targetSize > maxSize) { - this.setScale(this.scale + 1); - } else { - resizeFrameBuffer(gl, frameBuffer, targetSize, targetSize); - } - } - } - private announce(announcement: string) { - this.announcements.next(announcement); - } - changePlaceEntity(reverse: boolean) { - if (reverse) { - this.placeEntity = this.placeEntity === 0 ? (placeableEntities.length - 1) : (this.placeEntity - 1); - } else { - this.placeEntity = (this.placeEntity + 1) % placeableEntities.length; - } - - const { name } = placeableEntities[this.placeEntity]; - const total = getSaysTime(name); - addChatBubble(this.map, this.player!, { message: name, type: MessageType.System, total, timer: total, created: Date.now() }); - } - changePlaceTile(reverse: boolean) { - if (reverse) { - this.placeTile = this.placeTile === 0 ? (houseTiles.length - 1) : (this.placeTile - 1); - } else { - this.placeTile = (this.placeTile + 1) % houseTiles.length; - } - - const { name } = houseTiles[this.placeTile]; - const total = getSaysTime(name); - addChatBubble(this.map, this.player!, { message: name, type: MessageType.System, total, timer: total, created: Date.now() }); - } - saveDebug() { - this.storage.setJSON('debug', this.debug); - } - private updateStatsText() { - const { gl, spriteBatch, paletteBatch } = this.webgl!; - - if ((performance.now() - this.lastStats) > SECOND) { - TIMING && timingCollate(); - - if (TIMING) { - const timings = timingCollate(); - this.timingsText = timings - .map(({ selfTime, selfPercent, totalPercent, count, name }) => - `${selfTime.toFixed(2).padStart(6)}ms` + - `${selfPercent.toFixed(2).padStart(6)}%` + - `${totalPercent.toFixed(2).padStart(6)}%` + - `${count.toString().padStart(6)} ${name}`) - .join('\n'); - } - - if (this.statsText) { - let value = ''; - - if (this.settings.browser.showStats) { - const tris = spriteBatch.tris + paletteBatch.tris; - const flush = paletteBatch.flushes; - const sent = this.sent.toFixed(); - const recv = this.recv.toFixed(); - const drawn = this.entitiesDrawn; - const total = this.map.entities.length; - const ponies = this.map.entities.reduce((sum, e) => sum + (e.type === PONY_TYPE ? 1 : 0), 0); - const extra = DEVELOPMENT ? `(${drawn}/${total}) ${tris} tris, ${flush} flush, ${this.audio.trackName}` : version; - const gl2 = isWebGL2(gl) ? ' WebGL2' : ''; - const engine = this.engine === Engine.Default ? '' : Engine[this.engine].toUpperCase(); - const fps = this.drawFps.toFixed(0); - const low = this.disableLighting ? ' LOW' : ''; - const extraStats = this.extraStats; - const palSize = ` pal ${this.paletteManager.textureSize}`; - value = `${extraStats}${engine} ${fps} fps ${sent}/${recv} kb/s ${ponies} ` + - `ponies ${extra}${gl2}${low}${palSize}`.trim(); - } - - if (value !== this.statsTextValue) { - this.statsText.nodeValue = value; - this.statsTextValue = value; - } - } - - this.lastStats = performance.now(); - this.onClock.next(formatHourMinutes(this.time)); - } - - TIMING && timeReset(); - - spriteBatch!.tris = 0; - spriteBatch!.flushes = 0; - paletteBatch!.tris = 0; - paletteBatch!.flushes = 0; - } + fallbackPonies = new Map(); + positions: { x: number; y: number; moved: boolean; }[] = []; + lastChatMessageType = ChatType.Say; + nextFriendsCRC = 0; + editingActions = false; + placeInQueue = 0; + time = performance.now(); + lightData = createLightData(Season.Summer); + season = Season.Summer; + holiday = Holiday.None; + worldFlags = WorldStateFlags.None; + showMinimap = false; + minimap: Minimap | undefined = undefined; + editor = { + type: 'stoneWall', + brushSize: 1, + tile: -1, + elevation: '', + special: '', + draggingEntities: false, + draggingStart: point(0, 0), + selectingEntities: false, + selectedEntities: [] as Entity[], + customLight: false, + lightColor: 'ffffff', + }; + incompleteSays: IncompleteSay[] = []; + shadowColor = SHADOW_COLOR; + onChat = new Subject(); + onToggleChat = new Subject(); + onCommand = new Subject(); + onCancel = () => false; + onClock = new BehaviorSubject(''); + onJoined = new Subject(); + onLeft = new Subject(); + onFrame = new Subject(); + onMessage = new Subject(); + messageQueue: ChatMessage[] = []; + lastWhisperFrom: { entityId: number; accountId?: string; } | undefined = undefined; + onPonyAddOrUpdate = new Subject(); + onActionsUpdate = new Subject(); + onPartyUpdate = new Subject(); + announcements = new Subject(); + onEntityIdUpdate = new Subject<{ old: number; new: number; }>(); + loaded = false; + fullyLoaded = false; + fps = 0; + player: Pony | undefined = undefined; + playerId: number | undefined = undefined; + playerName: string | undefined = undefined; + playerInfo: string | undefined = undefined; + playerCRC: number | undefined = undefined; + selected: Pony | undefined = undefined; + party: PartyInfo | undefined = undefined; + notifications: Notification[] = []; + map = createWorldMap(); + camera = createCamera(); + paletteManager = new PaletteManager(); + tileSets?: TileSets; + offlinePony = createPony(0, 0, OFFLINE_PONY, mockPaletteManager.addArray(defaultPalette), mockPaletteManager); + supporterPony = createPony(0, 0, SUPPORTER_PONY, mockPaletteManager.addArray(defaultPalette), mockPaletteManager); + scale: number; + failedFBO = false; + rightOverride?: boolean; + headTurnedOverride?: boolean; + stateOverride?: EntityState; + actions = createDefaultButtonActions(); + mod = false; + webgl?: WebGL; + actionsChanged = true; + debug: DebugFlags = {}; + whisperTo: Entity | FakeEntity | undefined = undefined; + findEntityFromChatLog: (id: number) => FakeEntity | undefined = () => undefined; + findEntityFromChatLogByName: (name: string) => FakeEntity | undefined = () => undefined; + private drawOptions: DrawOptions = { + ...defaultDrawOptions, + }; + private input = new InputManager(); + socket?: ClientSocketService; + private canvas?: HTMLCanvasElement; + private statsText?: Text; + private timeSize = 0; + private lastStats = 0; + private sent = 0; + private recv = 0; + private hideText = false; + private hidePublicChat = false; + private hover = point(0, 0); + private viewMatrix = createMat4(); + private fboMatrix = createMat4(); + private initialized = false; + private changedScale = false; + private baseTime = 0; + private targetBaseTime = 0; + private connectedTime = 0; + private lastPixelRatio = pixelRatio(); + private resized = true; + private resizedCamera = true; + private bg = colorToFloatArray(BLACK); + private deltaMultiplier = 1; + private lastDraw = 0; + private entitiesDrawn = 0; + private lastFps = performance.now(); + private frames = 0; + private drawFps = 0; + private lastCanvasRatio = 0; + private extraStats = ''; + private timingsText = ''; + private statsTextValue = ''; + private windowWidth = 0; + private windowHeight = 0; + private debugShortcuts: string[] = []; + private cameraShiftOn = false; + private cameraShiftTarget = 0; + private lastIsKeyboardOpen = false; + private element?: HTMLElement; + private showWallPlaceholder = false; + private highlightEntity?: Entity; + placeEntity = 0; + placeTile = 0; + constructor( + public audio: Audio, + private storage: StorageService, + public settings: SettingsService, + public model: Model, + private errorReporter: ErrorReporter, + private zone: NgZone, + ) { + this.scale = this.getScale(); + this.audio.initTracks(this.season, this.holiday, this.map.type); + this.audio.setVolume(this.volume); + this.debug = storage.getJSON('debug', {}); + this.drawOptions.error = message => errorReporter.reportError(message); + this.onActionsUpdate.subscribe(() => this.actionsChanged = true); + + if (DEVELOPMENT) { + attachDebugMethod('setScale', (x: number) => this.setScale(x)); + attachDebugMethod('game', this); + } + } + get volume() { + return this.settings.browser.volume || 0; + } + get disableLighting() { + return !!this.settings.browser.lowGraphicsMode || this.failedFBO; + } + get frameDelay() { + return (this.settings.browser.powerSaving || this.editingActions) ? (1000 / 45) : 0; + } + get engine() { + return BETA ? (this.debug.engine || Engine.Default) : Engine.Default; + } + set engine(value: Engine) { + if (BETA) { + this.debug.engine = value; + this.saveDebug(); + } + } + private applied(func: () => void) { + return () => this.apply(func); + } + apply = (func: () => void) => { + return this.zone.run(func); + } + applyChanges = () => this.zone.run(() => { }); + private getScale() { + const defaultScale = pixelRatio() > 1 ? 3 : 2; + const scale = toInt(this.settings.browser.scale) || defaultScale; + return clamp(scale, MIN_SCALE, MAX_SCALE); + } + private setScale(scale: number) { + if (this.scale !== scale) { + this.scale = scale; + this.settings.browser.scale = this.scale; + this.settings.saveBrowserSettings(); + this.changedScale = true; + } + } + private toggleDisableLighting() { + if (!this.failedFBO) { + this.settings.browser.lowGraphicsMode = !this.settings.browser.lowGraphicsMode; + this.settings.saveBrowserSettings(); + } + } + send(action: (server: IServerActions) => T) { + if (this.socket && this.socket.isConnected) { + return action(this.socket.server); + } else { + return undefined; + } + } + changeScale() { + this.setScale((this.scale % MAX_SCALE) + 1); + this.changedScale = true; + } + zoomIn() { + this.setScale(Math.min(MAX_SCALE, this.scale + 1)); + } + zoomOut() { + this.setScale(Math.max(1, this.scale - 1)); + } + select(pony: Pony | undefined) { + if (this.selected === pony) + return; + + if (pony && isHidden(pony) && !this.mod) + return; + + this.zone.run(() => { + if (this.selected) { + this.selected.selected = false; + } + + this.selected = pony; + + if (pony && !pony.info && !pony.palettePonyInfo) { + this.send(server => server.select(pony.id, SelectFlags.FetchEx | SelectFlags.FetchInfo)); + } else { + this.sendSelected(); + } + + if (this.selected) { + this.selected.selected = true; + } + }); + } + private sendSelected = debounce(() => { + const pony = this.selected; + const id = pony ? pony.id : 0; + const fetchEx = !!pony && !hasExtendedInfo(pony); + this.send(server => server.select(id, fetchEx ? SelectFlags.FetchEx : SelectFlags.None)); + }, 300); + load() { + return loadAndInitSpriteSheets() + .then(initializeTileHeightmaps); + } + init() { + this.canvas = document.getElementById('canvas') as HTMLCanvasElement; + this.updateTileSets(); + this.input.initialize(this.canvas); + + if (!this.initialized) { + this.canvas.addEventListener('webglcontextlost', e => { + e.preventDefault(); + DEVELOPMENT && console.warn('Context lost'); + this.errorReporter.captureEvent({ name: 'Context lost' }); + }); + + this.canvas.addEventListener('webglcontextrestored', () => { + DEVELOPMENT && console.warn('Context restored'); + this.errorReporter.captureEvent({ name: 'Context restored' }); + + if (this.webgl) { + this.webgl = initWebGLResources(this.webgl.gl, this.paletteManager, this.camera); + } + }); + + this.initialized = true; + + const stats = document.getElementById('stats') as HTMLElement; + this.statsText = document.createTextNode(''); + stats.appendChild(this.statsText); + + this.input.onReleased(Key.KEY_O, () => this.zoomOut()); + this.input.onReleased(Key.KEY_P, () => this.zoomIn()); + this.input.onReleased(Key.GAMEPAD_BUTTON_Y, () => this.changeScale()); + + this.input.onPressed(Key.ENTER, () => this.onChat.next()); + this.input.onPressed(Key.ESCAPE, () => this.escape()); + this.input.onPressed(Key.GAMEPAD_BUTTON_X, () => this.onToggleChat.next()); + // this.input.onPressed(Key.BACKSPACE, () => this.backspace()); + this.input.onPressed(Key.KEY_H, () => turnHeadAction(this)); + this.input.onPressed(Key.FORWARD_SLASH, () => this.onCommand.next()); + this.input.onPressed([Key.KEY_B, Key.GAMEPAD_BUTTON_B, Key.TOUCH_SECOND_CLICK], () => boopAction(this)); + this.input.onPressed([Key.KEY_E, Key.GAMEPAD_BUTTON_A], () => { + interact(this, this.input.isPressed(Key.SHIFT)); + }); + this.input.onPressed([Key.KEY_X, Key.GAMEPAD_BUTTON_DOWN], () => downAction(this)); + this.input.onPressed([Key.KEY_C, Key.GAMEPAD_BUTTON_UP], () => upAction(this)); + this.input.onPressed(Key.F2, () => { + if (!this.settings.browser.disableFKeys) { + this.hideText = !this.hideText; + this.hidePublicChat = false; + } + }); + this.input.onPressed(Key.F3, () => { + if (!this.settings.browser.disableFKeys) { + this.hideText = false; + this.hidePublicChat = !this.hidePublicChat; + } + }); + this.input.onPressed(Key.F4, () => { + if (!this.settings.browser.disableFKeys) { + this.settings.account.seeThroughObjects = !this.settings.account.seeThroughObjects; + this.settings.saveAccountSettings(this.settings.account); + } + }); + + [ + Key.KEY_1, Key.KEY_2, Key.KEY_3, Key.KEY_4, Key.KEY_5, Key.KEY_6, + Key.KEY_7, Key.KEY_8, Key.KEY_9, Key.KEY_0, Key.DASH, Key.EQUALS, + ].forEach((key, index) => this.input.onPressed(key, () => { + if (this.actions[index]) { + this.zone.run(() => useAction(this, this.actions[index].action)); + } + })); + + const addDebugShortcut = (num: number, name: string, action: () => void) => { + this.input.onPressed(numpad[num], () => { + if (!this.input.isPressed(Key.SHIFT)) { + this.apply(action); + } + }); + this.debugShortcuts.push(`${num} - ${name}`); + this.debugShortcuts.sort(); + }; + + // const addDebugShortcutShift = (num: number, name: string, action: () => void) => { + // this.input.onPressed(numpad[num], () => { + // if (this.input.isPressed(Key.SHIFT)) { + // this.apply(action); + // } + // }); + // this.debugShortcuts.push(`${num} (shift) - ${name}`); + // this.debugShortcuts.sort(); + // }; + + if (BETA) { + // editor + this.input.onPressed(Key.BACKSPACE, () => { + if (this.mod) { + this.send(server => server.editorAction({ type: 'undo' })); + } + }); + this.input.onPressed(Key.DELETE, this.applied(() => { + const entities = this.editor.selectedEntities.map(e => e.id); + this.send(server => server.editorAction({ type: 'remove', entities })); + this.editor.selectedEntities.length = 0; + })); + + [ + { key: Key.LEFT, dx: -1 / tileWidth, dy: 0 }, + { key: Key.RIGHT, dx: 1 / tileWidth, dy: 0 }, + { key: Key.UP, dx: 0, dy: -1 / tileHeight }, + { key: Key.DOWN, dx: 0, dy: 1 / tileHeight }, + ].forEach(({ key, dx, dy }) => this.input.onPressed(key, () => { + this.editor.selectedEntities.forEach(({ id, x, y }) => { + this.send(server => server.editorAction({ + type: 'move', + entities: [{ id, x: x + dx, y: y + dy }], + })); + }); + })); + + // debug + this.input.onReleased(Key.KEY_M, () => this.showMinimap = !this.showMinimap); + this.input.onPressed(Key.KEY_G, () => { + if (this.input.isPressed(Key.SHIFT)) { + let faceDir = 0; + let dir = 1; + this.player!.doAction = DoAction.Swing; + const state = this.player!.ponyState; + + const interval = setInterval(() => { + faceDir += dir; + + if (faceDir < 0) { + clearInterval(interval); + return; + } + + state.headTurn = faceDir; + + if (faceDir === 3) { + turnHeadAction(this); + } + + if (faceDir >= 6) { + dir = -1; + } + }, 1000 / 24); + } else { + let faceDir = 0; + const state = this.player!.ponyState; + + const interval = setInterval(() => { + faceDir++; + state.headTurn = faceDir; + + if (faceDir === 3) { + turnHeadAction(this); + } + + if (faceDir >= 7) { + clearInterval(interval); + } + }, 1000 / 24); + } + }); + addDebugShortcut(1, 'show info at cursor', () => { + this.debug.showInfo = !this.debug.showInfo; + this.saveDebug(); + }); + addDebugShortcut(2, 'show water bounds', () => { + this.drawOptions.showHeightmap = !this.drawOptions.showHeightmap; + }); + addDebugShortcut(3, 'show collision map', () => { + this.drawOptions.showColliderMap = !this.drawOptions.showColliderMap; + }); + addDebugShortcut(4, 'show helpers', () => { + this.debug.showHelpers = !this.debug.showHelpers; + this.saveDebug(); + }); + addDebugShortcut(5, 'show tile indices', () => { + this.drawOptions.tileIndices = !this.drawOptions.tileIndices; + }); + addDebugShortcut(6, 'show tile grid', () => { + this.drawOptions.tileGrid = !this.drawOptions.tileGrid; + }); + addDebugShortcut(7, 'grayscale', () => { + document.documentElement.style.filter = document.documentElement.style.filter ? null : 'grayscale(100%)'; + }); + addDebugShortcut(8, 'show regions', () => { + this.debug.showRegions = !this.debug.showRegions; + this.saveDebug(); + }); + } + + if (DEVELOPMENT) { + let showingRange = false; + addDebugShortcut(9, 'show chatlog range', () => { + showingRange = !showingRange; + updateRangeIndicator(showingRange ? this.settings.account.chatlogRange : undefined, this); + }); + this.input.onPressed(Key.F6, () => { + this.cameraShiftOn = !this.cameraShiftOn; + this.cameraShiftTarget = 400; + }); + this.input.onPressed(Key.F7, () => { + this.debug.showPalette = !this.debug.showPalette; + this.saveDebug(); + }); + this.input.onPressed(Key.F8, () => { + }); + + let loseContext: WEBGL_lose_context | null = null; + + this.input.onPressed(Key.F9, () => { + if (loseContext) { + loseContext.restoreContext(); + loseContext = null; + } else { + loseContext = this.webgl!.gl.getExtension('WEBGL_lose_context')!; + loseContext.loseContext(); + } + }); + this.input.onPressed(Key.F10, () => { + this.settings.browser.brightNight = !this.settings.browser.brightNight; + }); + this.input.onPressed(Key.KEY_R, this.applied(() => { + if (!Date.now() && this.player) { + const bounds = getInteractBounds(this.player); + const entities = this.map.entities.filter(e => + e !== this.player && boundsIntersect(e.x, e.y, e.bounds, 0, 0, bounds)); + + if (entities.length) { + const entity = entities[0]; + const typeName = getEntityTypeName(entity.type); + this.announce(`${typeName}${entities.length > 1 ? ` (1 of ${entities.length})` : ''}`); + } else { + this.announce('nothing'); + } + } + + // if (this.player) this.player.swimming = !this.player.swimming; + // this.editorElevation = ''; + // this.editorSpecial = this.editorSpecial ? '' : 'ramp-e'; + })); + this.input.onPressed(Key.KEY_J, () => { + if (this.player) { + this.player.ponyState.headTilt = (this.player.ponyState.headTilt || 0) + 0.5; + } + }); + this.input.onPressed(Key.KEY_K, () => { + if (this.player) { + this.player.ponyState.headTilt = (this.player.ponyState.headTilt || 0) - 0.5; + } + }); + this.input.onPressed(Key.KEY_L, () => this.player && setHeadAnimation(this.player, nom)); + this.input.onReleased(Key.KEY_Q, () => this.send(server => server.leave())); + this.input.onReleased(Key.KEY_T, () => this.toggleDisableLighting()); + this.input.onReleased(Key.KEY_U, () => { + if (this.player) { + console.log( + `position: ${this.player.x.toFixed(2)}, ${this.player.y.toFixed(2)} ` + + `region: ${Math.floor(this.player.x / REGION_SIZE)}, ${Math.floor(this.player.y / REGION_SIZE)}`); + } + }); + this.input.onReleased(Key.KEY_I, () => { + const state = this.player!.ponyState; + state.flags = setFlag(state.flags, PonyStateFlags.CurlTail, !hasFlag(state.flags, PonyStateFlags.CurlTail)); + }); + // this.input.onReleased(Key.KEY_N, () => this.engine = (this.engine + 1) % Engine.Total); + this.input.onPressed(Key.KEY_N, () => this.audio.playRandomTrack()); + // this.input.onPressed(Key.KEY_G, () => this.wind = Math.max(0, this.wind - 1)); + // this.input.onReleased(Key.KEY_M, () => this.send(server => server.editorAction({ type: 'party' }))); + this.input.onPressed(Key.F8, () => toggleWalls()); + this.input.onPressed(Key.COMMA, () => this.deltaMultiplier = 0.5); + this.input.onPressed(Key.PERIOD, () => this.deltaMultiplier = 2); + } + + window.addEventListener('resize', () => { + this.resized = true; + DEVELOPMENT && log(`resized ${window.innerHeight} (${window.scrollY})`); + }); + + this.canvas.addEventListener('touchstart', () => this.audio.touch()); + } + + this.resized = true; + + if (!this.webgl) { + this.initWebGL(); + } + } + leave() { + if (this.socket) { + if (this.socket.isConnected) { + this.socket.server.leave(); + } else { + this.socket.disconnect(); + } + } + } + joined() { + this.connectedTime = Math.round(performance.now()); + this.onJoined.next(); + } + togglePixelRatio() { + pixelRatioEnabled = !pixelRatioEnabled; + } + private escape() { + if (this.socket && !this.onCancel()) { + this.select(undefined); + } + } + backspace() { + if (this.player && this.player.says !== undefined) { + this.send(server => server.say(0, '.', ChatType.Dismiss)); + } + } + private initWebGL() { + this.errorReporter.captureEvent({ name: 'game.initWebGL' }); + + if (!this.canvas) { + throw new Error('Missing canvas'); + } + + try { + this.resizeCamera(); + this.webgl = initWebGL(this.canvas, this.paletteManager, this.camera); + const { failedFBO, palettes, renderer } = this.webgl; + + if (renderer) { + this.errorReporter.configureData({ renderer }); + } + + if (failedFBO) { + this.errorReporter.captureEvent({ name: 'game.initWebGL failed FBO' }); + } + + this.offlinePony = createPony(0, 0, OFFLINE_PONY, palettes.defaultPalette, this.paletteManager); + this.supporterPony = createPony(0, 0, SUPPORTER_PONY, palettes.defaultPalette, this.paletteManager); + initializeToys(this.paletteManager); + } catch (e) { + this.errorReporter.captureEvent({ name: 'failed game.initWebGL', error: e.message, stack: e.stack }); + this.releaseWebGL(); + DEVELOPMENT && console.error(e); + throw new Error(`Failed to initialize graphics device (${e.message})`); + } + } + private releaseWebGL() { + this.errorReporter.captureEvent({ name: 'game.releaseWebGL' }); + + if (this.webgl) { + try { + this.paletteManager.dispose(this.webgl.gl); + disposeWebGL(this.webgl); + } catch (e) { + DEVELOPMENT && console.error(e); + } + + this.webgl = undefined; + } + } + private resizeCamera() { + if (this.canvas) { + const actualScale = this.scale * integerPixelRatio(); + const w = clamp(Math.ceil(this.canvas.width / actualScale), CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX); + const h = clamp(Math.ceil(this.canvas.height / actualScale), CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX); + + if (this.camera.w !== w || this.camera.h !== h) { + this.camera.w = w; + this.camera.h = h; + this.resizedCamera = true; + } + } + } + release() { + this.settings.saving(() => false); + this.loaded = false; + this.fullyLoaded = false; + this.player = undefined; + this.selected = undefined; + this.party = undefined; + this.rightOverride = undefined; + this.headTurnedOverride = undefined; + this.stateOverride = undefined; + this.notifications = []; + this.map = createWorldMap(); + this.camera = createCamera(); + + if (this.socket) { + this.socket.disconnect(); + this.socket = undefined; + } + + this.audio.stop(); + this.input.release(); + this.releaseWebGL(); + } + startup(socket: ClientSocketService, mod: boolean) { + if (this.settings.account.actions) { + this.actions = deserializeActions(this.settings.account.actions); + } else { + this.actions = createDefaultButtonActions(); + } + + const saveSettings = (settings: AccountSettings) => this.send(server => server.saveSettings(settings)); + const saveSettingsDebounced = debounce(saveSettings, 1500); + + this.element = document.getElementById('app-game')!; + this.settings.saving(settings => (saveSettingsDebounced(settings), true)); + this.lastChatMessageType = ChatType.Say; + this.selected = undefined; + this.party = undefined; + this.socket = socket; + this.audio.setVolume(this.volume); + // this.audio.play(); + this.mod = mod; + this.nextFriendsCRC = performance.now() + 5 * SECOND; + + if (DEVELOPMENT) { + initLogger(message => this.onMessage.next({ + id: 1, crc: 1, name: 'log', type: MessageType.System, + message: `[${((performance.now() | 0) % 10000)}] ${message}` + })); + } + + if (DEVELOPMENT && LOG_POSITION) { + this.positions.length = 0; + } + } + private getPixelScale() { + return this.scale * (integerPixelRatio() / pixelRatio()); + } + update(delta: number, now: number, last: number) { + TIMING && timeStart('update'); + delta *= this.deltaMultiplier; + + const shiftSpeed = delta * 10; + + if (this.cameraShiftOn && this.camera.shiftRatio !== 1) { + this.camera.shiftRatio = Math.min(1, this.camera.shiftRatio + shiftSpeed); + } else if (!this.cameraShiftOn && this.camera.shiftRatio !== 0) { + this.camera.shiftRatio = Math.max(0, this.camera.shiftRatio - shiftSpeed); + } + + updateMap(this.map, delta); + + if (!this.socket || !this.socket.isConnected || !this.element) + return; + + this.updateGameTime(delta); + + if (this.lastPixelRatio !== pixelRatio()) { + this.lastPixelRatio = pixelRatio(); + this.resized = true; + } + + if (this.resized) { + this.resizeCanvas(); + } + + const player = this.player; + const camera = this.camera; + const input = this.input; + const server = this.socket.server; + + restorePlayerPosition(); + + input.disabledGamepad = !!this.settings.browser.disableGamepad; + input.update(); + + this.resizeCamera(); + this.updateCameraShift(); + + const actualScale = this.scale * integerPixelRatio(); + this.camera.offset = this.cameraShiftTarget / actualScale; + + let moved = false; + + if (player && this.loaded) { + if (BETA && this.editor.selectedEntities.length) { + input.disableArrows = true; + } + + this.showWallPlaceholder = player.hold === toggleWallsTool.type && hasFlag(this.map.flags, MapFlags.EditableWalls); + + if (this.highlightEntity) { + if (this.highlightEntity.id === 0) { + releaseEntity(this.highlightEntity); + } + + this.highlightEntity = undefined; + } + + const shift = input.isPressed(Key.SHIFT); + const x = this.fullyLoaded ? input.axisX : 0; + const y = this.fullyLoaded ? input.axisY : 0; + const dir = vectorToDir(x, y); + const vec = (x || y) ? dirToVector(dir) : { x: 0, y: 0 }; + const walk = input.isMovementFromButtons ? (this.settings.browser.walkByDefault ? !shift : shift) : false; + const flags = getMovementFlag(x, y, walk); + const speed = flagsToSpeed(flags); + const vx = vec.x * speed; + const vy = vec.y * speed; + + if (BETA) { + input.disableArrows = false; + } + + if (player.vx !== vx || player.vy !== vy) { + if (vx === 0) { + player.x = roundPositionX(player.x) + 0.5 / tileWidth; + } + + if (vy === 0) { + player.y = roundPositionY(player.y) + 0.5 / tileHeight; + } + + const time = (last - this.connectedTime) >>> 0; + const [a, b, c, d, e] = encodeMovement(player.x, player.y, dir, flags, time, camera); + server.move(a, b, c, d, e); + moved = true; + this.resizedCamera = false; + } + + if ((vx || vy) && (isPonySitting(player) || isPonyLying(player))) { + player.state = setPonyState(player.state, EntityState.PonyStanding); + } + + updateEntityVelocity(this.map, player, vx, vy); + + const facingRight = isFacingRight(player); + const right = isMovingRight(vx, facingRight); + + if (facingRight !== right) { + player.state = setFlag(player.state, EntityState.FacingRight, right); + player.state = setFlag(player.state, EntityState.HeadTurned, false); + this.rightOverride = right; + } + + updateCamera(camera, player, this.map); + + const scale = this.getPixelScale(); + const hover = screenToWorld(camera, point(input.pointerX / scale, input.pointerY / scale)); + + if (input.usingTouch && !input.wasPressed(Key.TOUCH_CLICK) && !input.isPressed(Key.TOUCH)) { + hover.x = 0; + hover.y = 0; + } + + this.hover = hover; + + if (this.fullyLoaded) { + if (BETA && this.editor.draggingEntities) { + editorDragEntities(this, hover, input.isPressed(Key.MOUSE_BUTTON2)); + } + + if (hasFlag(this.map.flags, MapFlags.EditableEntities)) { + if (player.hold === removeEntitiesTool.type) { + this.highlightEntity = pickEntities(this.map, hover, true, false, true)[0]; + } else if (player.hold === placeEntitiesTool.type) { + if (!isOutsideMap(hover.x, hover.y, this.map)) { + const { type } = placeableEntities[this.placeEntity]; + let { x, y } = hover; + + if (this.map.editableArea) { + x = clamp(x, this.map.editableArea.x, this.map.editableArea.x + this.map.editableArea.w); + y = clamp(y, this.map.editableArea.y, this.map.editableArea.y + this.map.editableArea.h); + } + + this.highlightEntity = createAnEntity(type, 0, x, y, {}, this.paletteManager, this); + } + } + } + + if (input.wasPressed(Key.MOUSE_BUTTON1) || input.wasPressed(Key.TOUCH_CLICK)) { + const pickedEntities = pickEntities(this.map, hover, shift, this.mod); + const pickedEntity = pickedEntities[(pickedEntities.indexOf(this.selected!) + 1) % pickedEntities.length]; + const holdingRemoveTool = player.hold === removeEntitiesTool.type; + const editableMap = hasFlag(this.map.flags, MapFlags.EditableEntities); + + if (BETA && this.editor.selectingEntities) { + editorSelectEntities(this, hover, shift); + } else if (pickedEntity && (!holdingRemoveTool || !editableMap || hasFlag(pickedEntity.flags, EntityFlags.IgnoreTool))) { + if (pickedEntity.type === PONY_TYPE) { + this.select(pickedEntity as Pony); + } else if (entityInRange(pickedEntity, player)) { + server.interact(pickedEntity.id); + } + } else if (BETA && this.editor.tile !== -1) { + if (this.editor.brushSize > 1) { + const x = Math.floor((hover.x - (this.editor.brushSize / 2))); + const y = Math.floor((hover.y - (this.editor.brushSize / 2))); + server.editorAction({ type: 'tile', x, y, tile: this.editor.tile, size: this.editor.brushSize }); + } else { + const x = hover.x | 0; + const y = hover.y | 0; + const type = this.editor.tile === getTile(this.map, hover.x, hover.y) ? TileType.Dirt : this.editor.tile; + server.changeTile(x, y, type); + } + } else if (player.hold === changeTileTool.type && hasFlag(this.map.flags, MapFlags.EditableTiles)) { + const x = hover.x | 0; + const y = hover.y | 0; + server.changeTile(x, y, houseTiles[this.placeTile].type); + } else if (player.hold === toggleWallsTool.type && hasFlag(this.map.flags, MapFlags.EditableWalls)) { + toggleWall(this, hover); + } else if (holdingRemoveTool && this.highlightEntity && editableMap) { + const id = this.highlightEntity.id; + this.send(server => server.actionParam(Action.RemoveEntity, id)); + } else if (player.hold === placeEntitiesTool.type && this.highlightEntity && editableMap) { + const { x, y, type } = this.highlightEntity; + this.send(server => server.actionParam(Action.PlaceEntity, { x, y, type })); + } else if (this.selected) { + this.select(undefined); + } else if (hasFlag(this.map.flags, MapFlags.EdibleGrass)) { + const tile = getTile(this.map, hover.x, hover.y); + + if (isValidTile(tile) && distanceXY(player.x, player.y, hover.x, hover.y) < TILE_CHANGE_RANGE) { + const x = hover.x | 0; + const y = hover.y | 0; + let type = tile === TileType.Grass ? TileType.Dirt : TileType.Grass; + server.changeTile(x, y, type); + } + } else if (DEVELOPMENT && this.engine === Engine.LayeredTiles && this.editor.elevation) { + const value = getElevation(this.map, hover.x, hover.y); + setElevation(this.map, hover.x, hover.y, clamp(this.editor.elevation === 'up' ? value + 1 : value - 1, 0, 10)); + } + } + + if (BETA && input.wasPressed(Key.MOUSE_BUTTON2)) { + if (this.editor.selectingEntities) { + editorMoveEntities(this, hover); + } else if (this.mod) { + toggleWall(this, hover); + } + } + + if (BETA && input.wasPressed(Key.MOUSE_BUTTON3)) { + if (this.mod) { + server.editorAction({ type: 'place', entity: this.editor.type, x: hover.x, y: hover.y }); + console.log(`${this.editor.type}(${hover.x.toFixed(2)}, ${hover.y.toFixed(2)})`); + } + } + + if (this.player && input.wheelY) { + if (input.isPressed(Key.SHIFT)) { + if (hasFlag(this.map.flags, MapFlags.EditableEntities)) { + const action = input.wheelY < 0 ? Action.SwitchTool : Action.SwitchToolRev; + this.send(server => server.action(action)); + } + } else { + if (this.player.hold === placeEntitiesTool.type) { + this.changePlaceEntity(input.wheelY < 0); + } else if (this.player.hold === changeTileTool.type) { + this.changePlaceTile(input.wheelY < 0); + } + } + } + + const isHeadTurned = hasFlag(player.state, EntityState.HeadTurned); + const isHeadFacingRight = right ? !isHeadTurned : isHeadTurned; + + if (((input.axis2X < 0 && isHeadFacingRight) || (input.axis2X > 0 && !isHeadFacingRight))) { + if (server.action(Action.TurnHead) as any) { + player.state = (player.state) ^ EntityState.HeadTurned; + } + } + } + } + + if (player) { + const safe = hasFlag(this.worldFlags, WorldStateFlags.Safe); + updateEntities(this, this.time, delta, safe); + savePlayerPosition(); + } + + if (this.changedScale) { + this.changedScale = false; + this.resizedCamera = true; + + if (player) { + centerCameraOn(camera, player); + } + } + + if (player) { + updateCamera(camera, player, this.map); + } + + if (this.resizedCamera) { + server.updateCamera(camera.x, camera.y, camera.w, camera.h); + this.resizedCamera = false; + } + + if (player) { + updateEntitiesCoverLifted(this.map, player, !!this.settings.account.seeThroughObjects, delta); + updateEntitiesWithNames(this.map, this.hover, player); + updateEntitiesTriggers(this.map, player, this); + } + + input.end(); + + if (this.nextFriendsCRC < now) { + this.send(server => server.actionParam(Action.FriendsCRC, this.model.computeFriendsCRC())); + this.nextFriendsCRC = now + 15 * MINUTE; + } + + const threshold = Date.now() - 10 * SECOND; + + for (let i = this.incompleteSays.length - 1; i >= 0; i--) { + if (this.incompleteSays[i].time < threshold) { + this.incompleteSays.splice(i, 1); + } + } + + this.updateSocketStats(delta); + TIMING && timeEnd(); + + if (DEVELOPMENT && LOG_POSITION) { + if (this.player) { + this.positions.push({ x: this.player.x, y: this.player.y, moved }); + } + } + } + private updateGameTime(delta: number) { + if (this.baseTime !== this.targetBaseTime) { + const timeDelta = Math.floor(delta * 0.2 * HOUR); + const baseTime = this.baseTime + timeDelta * (this.baseTime > this.targetBaseTime ? -1 : 1); + + if (Math.abs(this.targetBaseTime - baseTime) < timeDelta) { + this.baseTime = this.targetBaseTime; + } else { + this.baseTime = baseTime; + } + } + + this.time = this.baseTime + performance.now(); + } + private updateSocketStats(delta: number) { + this.timeSize += delta; + + if (this.timeSize > 1) { + this.sent = 8 * this.socket!.sentSize / this.timeSize / 1024; + this.recv = 8 * this.socket!.receivedSize / this.timeSize / 1024; + this.socket!.sentSize = 0; + this.socket!.receivedSize = 0; + this.timeSize = 0; + } + } + setWorldState(state: WorldState, initial: boolean) { + this.season = state.season; + this.holiday = state.holiday; + this.worldFlags = state.flags; + this.lightData = createLightData(this.season); + initFeatureFlags(state.featureFlags); + + const baseTime = state.time - performance.now(); + + if (initial) { + this.baseTime = this.targetBaseTime = baseTime; + } else { + this.targetBaseTime = baseTime; + } + + this.updateTileSets(); + + if (this.model.friends) { + for (const friend of this.model.friends) { + friend.actualName = filterEntityName(this, friend.name, friend.nameBad) || ''; + } + } + } + setPlayer(player: Pony) { + this.player = player; + centerCameraOn(this.camera, player); + this.send(server => server.loaded()); + } + setupMap() { + this.bg = colorToFloatArray(getTileColor(this.map.defaultTile, this.season)); + this.audio.initTracks(this.season, this.holiday, this.map.type); + this.audio.playOrSwitchToRandomTrack(); + this.updateTileSets(); + } + private updateTileSets() { + this.tileSets = updateTileSets(this.paletteManager, this.tileSets, this.season, this.map.type); + } + draw() { + redrawActionButtons(this.actionsChanged); + this.actionsChanged = false; + + if (!this.webgl) + return; + + if (this.webgl.gl.isContextLost()) { + DEVELOPMENT && console.warn('Context is lost'); + return; + } + + // start frame + const now = performance.now(); + + if ((now - this.lastDraw) < this.frameDelay) { + return; + } + + this.frames++; + + if ((now - this.lastFps) > 1000) { + this.drawFps = this.frames * 1000 / (now - this.lastFps); + this.frames = 0; + this.lastFps = now; + } + + this.lastDraw = now; + + // draw + const { + gl, frameBuffer, frameBufferSheet, spriteShader, spriteBatch, lightShader, paletteBatch, paletteShader, + palettes, + } = this.webgl; + + TIMING && timeStart('draw'); + + TIMING && timeStart('draw init'); + let lightColor = WHITE; + let shadowColor = 0; + + if (this.map.type === MapType.Cave) { + lightColor = CAVE_LIGHT; + shadowColor = CAVE_SHADOW; + } else { + lightColor = getLightColor(this.lightData, this.time); + shadowColor = getShadowColor(this.lightData, this.time); + } + + if (BETA && this.editor.customLight) { + lightColor = parseColor(this.editor.lightColor); + shadowColor = this.shadowColor; + } + + const camera = this.camera; + const width = camera.w; + const height = camera.h; + const ratio = integerPixelRatio(); + const actualScale = this.scale * ratio; + const bg = this.bg; + + colorToExistingFloatArray(light, lightColor); + + const drawOptions = this.drawOptions; + drawOptions.gameTime = this.time; + drawOptions.lightColor = lightColor; + drawOptions.shadowColor = shadowColor; + drawOptions.drawHidden = this.mod; + drawOptions.season = this.season; + + if (DEVELOPMENT) { + drawOptions.debug = this.debug; + } + + if (DEVELOPMENT || BETA) { + drawOptions.engine = this.engine; + } + + ortho(this.fboMatrix, 0, gl.drawingBufferWidth / actualScale, gl.drawingBufferHeight / actualScale, 0, 0, 1000); + TIMING && timeEnd(); + + TIMING && timeStart('ensureAllVisiblePon...'); + ensureAllVisiblePoniesAreDecoded(this.map, camera, this.paletteManager); + TIMING && timeEnd(); + + TIMING && timeStart('commit+invalidatePalettes'); + if (this.paletteManager.commit(gl)) { + invalidatePalettes(this.map.entitiesDrawable); + } + TIMING && timeEnd(); + + if (this.settings.browser.brightNight) { + lerpColor(light, white, 0.3); + } + + if (this.engine === Engine.NewLighting) { + // ... + } else if (this.disableLighting) { + ortho(this.viewMatrix, camera.x, camera.x + camera.w, camera.actualY + camera.h, camera.actualY, 0, 1000); + lerpColor(light, white, 0.1); // adjust lighting for missing lights + + // color -> screen + gl.clearColor(bg[0], bg[1], bg[2], bg[3]); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.disable(gl.DEPTH_TEST); + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + this.drawMap(this.webgl, this.map, this.viewMatrix, light, drawOptions); + } else { + ortho(this.viewMatrix, camera.x, camera.x + camera.w, camera.actualY, camera.actualY + camera.h, 0, 1000); + + TIMING && timeStart('initializeFrameBuffer'); + this.initializeFrameBuffer(this.webgl, width, height); + TIMING && timeEnd(); + + if (!frameBuffer) { + DEVELOPMENT && console.warn('No frame buffer'); + return; + } + + // color -> fbo + TIMING && timeStart('color -> fbo'); + // gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer.handle); + bindFrameBuffer(gl, frameBuffer); + gl.viewport(0, 0, frameBuffer.width, frameBuffer.height); + gl.clearColor(bg[0], bg[1], bg[2], bg[3]); + gl.clear(gl.COLOR_BUFFER_BIT); // | gl.DEPTH_BUFFER_BIT); + gl.viewport(0, 0, width, height); + gl.disable(gl.DEPTH_TEST); + //gl.depthFunc(gl.LEQUAL); + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + this.drawMap(this.webgl, this.map, this.viewMatrix, white, drawOptions); + TIMING && timeEnd(); + + // color -> screen + TIMING && timeStart('color -> screen'); + // gl.bindFramebuffer(gl.FRAMEBUFFER, null); + unbindFrameBuffer(gl); + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + // gl.disable(gl.DEPTH_TEST); + gl.disable(gl.BLEND); + + gl.useProgram(spriteShader.program); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform4fv(spriteShader.uniforms.lighting, white); + gl.uniform1f(spriteShader.uniforms.textureSize, frameBufferSheet.texture!.width); + bindTexture(gl, 0, frameBufferSheet.texture); + spriteBatch.begin(); + spriteBatch.drawImage(WHITE, 0, 0, width, height, 0, 0, width, height); + spriteBatch.end(); + TIMING && timeEnd(); + + // light -> fbo + TIMING && timeStart('light -> fbo'); + // gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer.handle); + bindFrameBuffer(gl, frameBuffer); + gl.viewport(0, 0, frameBuffer.width, frameBuffer.height); + gl.clearColor(light[0], light[1], light[2], light[3]); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.viewport(0, 0, width, height); + //gl.enable(gl.DEPTH_TEST); + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ONE); + TIMING && timeEnd(); + + // shadows + //for (const e of map.entities) { + // if (e.drawShadow && camera.isBoundVisible(e.shadowBounds || e.bounds, e.x, e.y)) { + // this.spriteBatch.depth = camera.mapDepth(e.y); + // e.drawShadow(this.spriteBatch); + // } + //} + + // soft lights + TIMING && timeStart('drawEntityLights'); + gl.useProgram(lightShader.program); + gl.uniformMatrix4fv(lightShader.uniforms.transform, false, this.viewMatrix); + gl.uniform4fv(lightShader.uniforms.lighting, white); + spriteBatch.begin(); + drawEntityLights(spriteBatch, this.map.entitiesLight, this.camera, drawOptions); + spriteBatch.end(); + TIMING && timeEnd(); + + // light sprites + TIMING && timeStart('drawEntityLightSprites'); + gl.useProgram(spriteShader.program); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.viewMatrix); + gl.uniform4fv(spriteShader.uniforms.lighting, white); + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + drawEntityLightSprites(spriteBatch, this.map.entitiesLightSprite, this.camera, drawOptions); + spriteBatch.end(); + TIMING && timeEnd(); + + // light -> screen + TIMING && timeStart('light -> screen'); + // gl.bindFramebuffer(gl.FRAMEBUFFER, null); + unbindFrameBuffer(gl); + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.DST_COLOR, gl.ZERO); + + gl.useProgram(spriteShader.program); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform4fv(spriteShader.uniforms.lighting, white); + gl.uniform1f(spriteShader.uniforms.textureSize, frameBufferSheet.texture!.width); + bindTexture(gl, 0, frameBufferSheet.texture); + spriteBatch.begin(); + spriteBatch.drawImage(WHITE, 0, 0, width, height, 0, 0, width, height); + spriteBatch.end(); + TIMING && timeEnd(); + } + + // ui -> screen + gl.enable(gl.BLEND); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + TIMING && timeStart('drawNames+drawChat'); + gl.useProgram(paletteShader.program); + gl.uniformMatrix4fv(paletteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform4fv(paletteShader.uniforms.lighting, white); + gl.uniform1f(paletteShader.uniforms.pixelSize, this.paletteManager.pixelSize); + gl.uniform1f(paletteShader.uniforms.textureSize, paletteSpriteSheet.texture!.width); + bindTexture(gl, 0, paletteSpriteSheet.texture); + bindTexture(gl, 1, this.paletteManager.texture); + paletteBatch.begin(); + + if (!this.hideText) { + drawNames( + paletteBatch, this.map.entitiesWithNames, this.player, this.party, this.camera, this.hover, this.mod, palettes); + drawChat( + paletteBatch, this.map.entitiesWithChat, this.camera, this.mod, palettes, this.hidePublicChat); + } + + if (!this.socket || !this.socket.isConnected) { + this.drawMessage(this.webgl, 'Connecting...'); + } else if (!this.loaded) { + if (this.placeInQueue) { + this.drawMessage(this.webgl, `Waiting in queue (${this.placeInQueue})`); + } else { + this.drawMessage(this.webgl, 'Loading...'); + } + } else if ((performance.now() - this.socket.lastPacket) > CONNECTION_ISSUE_TIMEOUT) { + // this.drawMessage('Connection issues...'); + } + + if (BETA && this.debug.showInfo) { + try { + const scale = this.getPixelScale(); + const x = this.input.pointerX / scale + 5; + const y = this.input.pointerY / scale; + const height = getMapHeightAt(this.map, this.hover.x, this.hover.y, this.time); + drawText(paletteBatch, `${height.toFixed(2)}`, fontSmallPal, BLACK, x, y); + } catch (e) { + console.warn(e.message); + } + } + + if (this.showWallPlaceholder) { + const x = this.hover.x | 0; + const y = this.hover.y | 0; + const dx = this.hover.x - x; + const dy = this.hover.y - y; + const palette = this.webgl.palettes.defaultPalette; + const color = makeTransparent(WHITE, 0.6); + const screenX = toScreenX(x) - this.camera.x; + const screenY = toScreenY(y) - this.camera.actualY; + + if (x >= 0 && y >= 0 && x < this.map.width && y < this.map.height) { + if (dx > dy) { + if ((dx + dy) < 1) { + paletteBatch.drawSprite(wall_h_placeholder.color, color, palette, screenX, screenY - 15); + } else { + paletteBatch.drawSprite(wall_v_placeholder.color, color, palette, screenX + tileWidth - 4, screenY - 12); + } + } else { + if ((dx + dy) < 1) { + paletteBatch.drawSprite(wall_v_placeholder.color, color, palette, screenX - 4, screenY - 12); + } else { + paletteBatch.drawSprite(wall_h_placeholder.color, color, palette, screenX, screenY + tileHeight - 15); + } + } + } + } + + paletteBatch.end(); + TIMING && timeEnd(); + + gl.useProgram(spriteShader.program); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform4fv(spriteShader.uniforms.lighting, white); + + if (BETA && this.showMinimap && this.minimap) { + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + spriteBatch.save(); + + const { width, height, data } = this.minimap; + const scale = 4 / this.scale; + + spriteBatch.translate(100, 100); + spriteBatch.scale(scale, scale); + + spriteBatch.drawRect(BLACK, -1, -1, width + 2, height + 2); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + spriteBatch.drawRect(data[x + y * width], x, y, 1, 1); + } + } + + if (this.player) { + spriteBatch.drawRect(RED, Math.floor(this.player.x), Math.floor(this.player.y), 1, 1); + } + + spriteBatch.restore(); + spriteBatch.end(); + } + + if (BETA && this.debug.showRegions && this.player) { + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + drawDebugRegions(spriteBatch, this.map, this.player, this.camera); + spriteBatch.end(); + } + + const showFPS = !!this.settings.browser.showFps; + const showHelp = BETA && this.input.isPressed(Key.F1); + const showPalette = DEVELOPMENT && this.debug.showPalette; + + if (showFPS || showHelp || showPalette) { + // 1 to 1 pixel scale drawing + TIMING && timeStart('showFps'); + const scale = 2; + // const height = gl.drawingBufferHeight / (ratio * scale); + ortho(this.fboMatrix, 0, gl.drawingBufferWidth / ratio, gl.drawingBufferHeight / ratio, 0, 0, 1000); + gl.uniformMatrix4fv(spriteShader.uniforms.transform, false, this.fboMatrix); + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + spriteBatch.save(); + spriteBatch.scale(scale, scale); + + if (showFPS) { + drawText(spriteBatch, this.drawFps.toFixed(), fontSmall, BLACK, 2, 2); + + if (this.timingsText) { + const size = measureText(this.timingsText, fontMono); + spriteBatch.drawRect(0x000000aa, 2, 26, 220, size.h + 8); + drawText(spriteBatch, this.timingsText, fontMono, WHITE, 2, 30); + } + } + + if (BETA && showHelp) { + let y = 25; + + for (const shortcut of this.debugShortcuts) { + drawOutlinedText(spriteBatch, shortcut, font, WHITE, BLACK, 5, y); + y += 10; + } + } + + // if (DEVELOPMENT) { + // const width = gl.drawingBufferWidth / (ratio * scale); + // const { isCollidingCount, isCollidingObjectCount } = getCollisionStats(); + // const text = + // `${isCollidingCount.toString().padStart(7)} calls\n` + + // `${isCollidingObjectCount.toString().padStart(7)} total checks\n` + + // `${this.markedColliding.toString().padStart(7)} player checks`; + // const size = measureText(text, fontMono); + // const x = width - 160; + // const y = 26; + // spriteBatch.drawRect(0x000000aa, x, y, 150, size.h + 10); + // drawText(spriteBatch, text, fontMono, WHITE, x + 5, y + 5); + // } + + spriteBatch.restore(); + spriteBatch.end(); + + if (DEVELOPMENT && showPalette) { + const paletteTexture = this.paletteManager.texture!; + const { width, height } = paletteTexture; + + gl.uniform1f(spriteShader.uniforms.textureSize, normalSpriteSheet.texture!.width); + bindTexture(gl, 0, normalSpriteSheet.texture); + spriteBatch.begin(); + spriteBatch.drawRect(0x00000066, 20, 20, width, height); + spriteBatch.end(); + + gl.uniform1f(spriteShader.uniforms.textureSize, width); + bindTexture(gl, 0, paletteTexture); + spriteBatch.begin(); + spriteBatch.drawImage(WHITE, 0, 0, width, height, 20, 20, width, height); + spriteBatch.end(); + } + + TIMING && timeEnd(); + } + + bindTexture(gl, 0, undefined); + bindTexture(gl, 1, undefined); + gl.useProgram(null); + + TIMING && timeEnd(); + this.updateStatsText(); + + TIMING && timeStart('messageQueue'); + while (this.messageQueue.length) { + this.onMessage.next(this.messageQueue.shift()!); + } + TIMING && timeEnd(); + + TIMING && timeStart('onFrame'); + this.onFrame.next(); + TIMING && timeEnd(); + } + private drawMessage({ paletteBatch, palettes }: WebGL, message: string) { + drawFullScreenMessage(paletteBatch, this.camera, message, palettes.mainFont.white); + } + private drawMap(webgl: WebGL, map: WorldMap, viewMatrix: Matrix4, lighting: Float32Array, options: DrawOptions) { + const { gl, paletteBatch, paletteShader } = webgl; + + TIMING && timeStart('drawMap'); + if (this.tileSets && this.player) { + gl.useProgram(paletteShader.program); + gl.uniformMatrix4fv(paletteShader.uniforms.transform, false, viewMatrix); + gl.uniform4fv(paletteShader.uniforms.lighting, lighting); + gl.uniform1f(paletteShader.uniforms.pixelSize, this.paletteManager.pixelSize); + gl.uniform1f(paletteShader.uniforms.textureSize, paletteSpriteSheet.texture!.width); + bindTexture(gl, 0, paletteSpriteSheet.texture); + bindTexture(gl, 1, this.paletteManager.texture); + paletteBatch.begin(); + this.entitiesDrawn = drawMap( + paletteBatch, map, this.camera, this.player, options, this.tileSets, this.editor.selectedEntities); + paletteBatch.end(); + + if (this.highlightEntity && this.highlightEntity.draw) { + gl.uniform4fv(paletteShader.uniforms.lighting, highlightColor); + paletteBatch.begin(); + this.highlightEntity.draw(paletteBatch, this.drawOptions); + paletteBatch.end(); + } + } + TIMING && timeEnd(); + } + private updateCameraShift() { + if (isMobile) { + const isKeyboardOpen = !!document.activeElement && /input/i.test(document.activeElement.tagName); + + if (this.lastIsKeyboardOpen !== isKeyboardOpen) { + DEVELOPMENT && log(`keyboard open ${isKeyboardOpen}`); + this.lastIsKeyboardOpen = isKeyboardOpen; + } + + if (isKeyboardOpen) { + if (!this.cameraShiftOn && window.scrollY > 100) { + this.cameraShiftOn = true; + this.cameraShiftTarget = window.scrollY; + + if (DEVELOPMENT) { + log(`shift camera ${this.cameraShiftTarget} (${this.windowHeight} - ${window.innerHeight}, ${window.scrollY})`); + } + } + } else { + if (this.cameraShiftOn && window.scrollY < 100) { + this.cameraShiftOn = false; + DEVELOPMENT && log(`unshift camera`); + } + } + } + } + private resizeCanvas() { + pixelRatioCache = getPixelRatio(); + const canvas = this.canvas; + const ratio = pixelRatio(); + const rect = this.element!.getBoundingClientRect(); + this.windowWidth = rect.width; + this.windowHeight = rect.height; + let w = Math.ceil(this.windowWidth * ratio); + let h = Math.ceil(this.windowHeight * ratio); + + while ((w % 12) !== 0) { + w++; + } + + while ((h % 12) !== 0) { + h++; + } + + if (canvas && w && h && (canvas.width !== w || canvas.height !== h || this.lastCanvasRatio !== ratio)) { + canvas.width = w; + canvas.height = h; + canvas.style.width = `${w / ratio}px`; + canvas.style.height = `${h / ratio}px`; + this.lastCanvasRatio = ratio; + this.resized = false; + DEVELOPMENT && log(`scrollY: ${window.scrollY}`); + } + } + private initializeFrameBuffer({ gl, frameBuffer }: WebGL, width: number, height: number) { + const targetSize = getRenderTargetSize(width, height); + + if (frameBuffer && targetSize !== frameBuffer.width) { + const maxSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); + + if (maxSize != null && targetSize > maxSize) { + this.setScale(this.scale + 1); + } else { + resizeFrameBuffer(gl, frameBuffer, targetSize, targetSize); + } + } + } + private announce(announcement: string) { + this.announcements.next(announcement); + } + changePlaceEntity(reverse: boolean) { + if (reverse) { + this.placeEntity = this.placeEntity === 0 ? (placeableEntities.length - 1) : (this.placeEntity - 1); + } else { + this.placeEntity = (this.placeEntity + 1) % placeableEntities.length; + } + + const { name } = placeableEntities[this.placeEntity]; + const total = getSaysTime(name); + addChatBubble(this.map, this.player!, { message: name, type: MessageType.System, total, timer: total, created: Date.now() }); + } + changePlaceTile(reverse: boolean) { + if (reverse) { + this.placeTile = this.placeTile === 0 ? (houseTiles.length - 1) : (this.placeTile - 1); + } else { + this.placeTile = (this.placeTile + 1) % houseTiles.length; + } + + const { name } = houseTiles[this.placeTile]; + const total = getSaysTime(name); + addChatBubble(this.map, this.player!, { message: name, type: MessageType.System, total, timer: total, created: Date.now() }); + } + saveDebug() { + this.storage.setJSON('debug', this.debug); + } + private updateStatsText() { + const { gl, spriteBatch, paletteBatch } = this.webgl!; + + if ((performance.now() - this.lastStats) > SECOND) { + TIMING && timingCollate(); + + if (TIMING) { + const timings = timingCollate(); + this.timingsText = timings + .map(({ selfTime, selfPercent, totalPercent, count, name }) => + `${selfTime.toFixed(2).padStart(6)}ms` + + `${selfPercent.toFixed(2).padStart(6)}%` + + `${totalPercent.toFixed(2).padStart(6)}%` + + `${count.toString().padStart(6)} ${name}`) + .join('\n'); + } + + if (this.statsText) { + let value = ''; + + if (this.settings.browser.showStats) { + const tris = spriteBatch.tris + paletteBatch.tris; + const flush = paletteBatch.flushes; + const sent = this.sent.toFixed(); + const recv = this.recv.toFixed(); + const drawn = this.entitiesDrawn; + const total = this.map.entities.length; + const ponies = this.map.entities.reduce((sum, e) => sum + (e.type === PONY_TYPE ? 1 : 0), 0); + const extra = DEVELOPMENT ? `(${drawn}/${total}) ${tris} tris, ${flush} flush, ${this.audio.trackName}` : version; + const gl2 = isWebGL2(gl) ? ' WebGL2' : ''; + const engine = this.engine === Engine.Default ? '' : Engine[this.engine].toUpperCase(); + const fps = this.drawFps.toFixed(0); + const low = this.disableLighting ? ' LOW' : ''; + const extraStats = this.extraStats; + const palSize = ` pal ${this.paletteManager.textureSize}`; + value = `${extraStats}${engine} ${fps} fps ${sent}/${recv} kb/s ${ponies} ` + + `ponies ${extra}${gl2}${low}${palSize}`.trim(); + } + + if (value !== this.statsTextValue) { + this.statsText.nodeValue = value; + this.statsTextValue = value; + } + } + + this.lastStats = performance.now(); + this.onClock.next(formatHourMinutes(this.time)); + } + + TIMING && timeReset(); + + spriteBatch!.tris = 0; + spriteBatch!.flushes = 0; + paletteBatch!.tris = 0; + paletteBatch!.flushes = 0; + } } diff --git a/src/ts/client/gameLoop.ts b/src/ts/client/gameLoop.ts index 78e66eb..66db7f8 100644 --- a/src/ts/client/gameLoop.ts +++ b/src/ts/client/gameLoop.ts @@ -1,89 +1,89 @@ export interface Game { - fps: number; - load(): any; - init(): void; - update(delta: number, now: number, last: number): void; - draw(): void; + fps: number; + load(): any; + init(): void; + update(delta: number, now: number, last: number): void; + draw(): void; } export interface GameLoop { - started: Promise; - cancel(): void; + started: Promise; + 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 handle: any; + let backup: any; + let cancelled = false; - let last = Math.round(performance.now()); - let lastFps = last; - let frames = 0; - let fps = 0; + 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); - } + function step(now: number, draw: boolean) { + if (draw) { + handle = requestAnimationFrame(onFrame); + } - frames++; + frames++; - if ((now - lastFps) > 1000) { - fps = frames * 1000 / (now - lastFps); - frames = 0; - lastFps = now; - } + if ((now - lastFps) > 1000) { + fps = frames * 1000 / (now - lastFps); + frames = 0; + lastFps = now; + } - try { - game.fps = fps; - game.update((now - last) / 1000, now, last); + try { + game.fps = fps; + game.update((now - last) / 1000, now, last); - if (draw) { - game.draw(); - } - } catch (e) { - onError(e); - } + if (draw) { + game.draw(); + } + } catch (e) { + onError(e); + } - last = now; - } + last = now; + } - function onTimer() { - step(Math.round(performance.now()), false); - backup = setTimeout(onTimer, 1000 / 10); - } + 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 onFrame() { + clearTimeout(backup); + step(Math.round(performance.now()), true); + backup = setTimeout(onTimer, 1000 / 10); + } - function cancel() { - cancelAnimationFrame(handle); - clearTimeout(backup); - cancelled = true; - } + function cancel() { + cancelAnimationFrame(handle); + clearTimeout(backup); + cancelled = true; + } - if (gameLoop) { - gameLoop.cancel(); - } + 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); - } - }); + 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 }; + gameLoop = { started, cancel }; - return gameLoop; + return gameLoop; } diff --git a/src/ts/client/gameUtils.ts b/src/ts/client/gameUtils.ts index b7db00e..933da57 100644 --- a/src/ts/client/gameUtils.ts +++ b/src/ts/client/gameUtils.ts @@ -3,47 +3,47 @@ import { PonyTownGame } from './game'; import { removeById } from '../common/utils'; export function addNotification({ notifications }: PonyTownGame, notification: Notification) { - const open = notifications.length === 0; + const open = notifications.length === 0; - notifications.push(notification); + notifications.push(notification); - setTimeout(() => { - notification.open = open; - notification.fresh = false; - }, 500); + setTimeout(() => { + notification.open = open; + notification.fresh = false; + }, 500); } export function removeNotification({ notifications }: PonyTownGame, id: number) { - const notification = removeById(notifications, id); + const notification = removeById(notifications, id); - if (notification && notification.open && notifications.length) { - notifications[0].open = true; - } + 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(); + 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); - } + 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; + return game.selected && game.selected.id === id; } diff --git a/src/ts/client/handlers.ts b/src/ts/client/handlers.ts index 3899d07..830680b 100644 --- a/src/ts/client/handlers.ts +++ b/src/ts/client/handlers.ts @@ -2,9 +2,9 @@ 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, + 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'; @@ -12,9 +12,9 @@ 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 + isPony, createPony, setPonyExpression, updatePonyInfo, updatePonyHold, doPonyAction, hasHeadAnimation, + setHeadAnimation, + doBoopPonyAction } from '../common/pony'; import { PonyTownGame } from './game'; import { setupPlayer, savePlayerPosition } from './sec'; @@ -27,8 +27,8 @@ 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, + findEntityById, getRegionGlobal, setTile, removeEntity, addEntity, removeEntityDirectly, setRegion, + addEntityToMapRegion, switchEntityRegion, getRegionUnsafe, addOrRemoveFromEntityList, } from '../common/worldMap'; import { isSelected } from './gameUtils'; import { compareFriends } from '../components/services/model'; @@ -36,767 +36,767 @@ import { canCollideWith } from '../common/collision'; import { hasDrawLight, hasLightSprite } from './draw'; function log(message: string) { - if (DEVELOPMENT && !TESTS) { - console.error(message); - } + 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 { + 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); + 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; + 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); + addEntityToMapRegion(game.map, region, entity); - if (isPony(entity)) { - if (id === game.playerId) { - game.apply(() => setupPlayer(game, entity)); - } + if (isPony(entity)) { + if (id === game.playerId) { + game.apply(() => setupPlayer(game, entity)); + } - if (isSelected(game, id)) { - game.select(entity); - } + if (isSelected(game, id)) { + game.select(entity); + } - if (game.whisperTo && game.whisperTo.id === id) { - game.whisperTo = entity; - } + if (game.whisperTo && game.whisperTo.id === id) { + game.whisperTo = entity; + } - if (!initial) { - game.onPonyAddOrUpdate.next(entity); - } - } + if (!initial) { + game.onPonyAddOrUpdate.next(entity); + } + } - if (action !== undefined) { - handleAction(game, id, action); - } + 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 { + 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); + const filteredName = filterEntityName(game, name, filterName); + const entity = findEntityByIdInGame(game, id); - if (entity) { - const isPlayer = id === game.playerId; + 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`); - } + 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); - } + 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)})`); - // } + 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); + 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); + 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); + 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 (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 (state !== undefined) { + updateEntityStateInternal(game, entity, state); + } - if (playerState !== undefined) { - updateEntityPlayerStateInternal(game, entity, playerState); - } + if (playerState !== undefined) { + updateEntityPlayerStateInternal(game, entity, playerState); + } - if (expression !== undefined && isPony(entity)) { - setPonyExpression(entity, expression); - } + if (expression !== undefined && isPony(entity)) { + setPonyExpression(entity, expression); + } - if (options != null) { - updateEntityOptionsInternal(entity, options, game); - } + if (options != null) { + updateEntityOptionsInternal(entity, options, game); + } - if (filteredName !== undefined && !isPlayer) { - entity.name = filteredName; - } + if (filteredName !== undefined && !isPlayer) { + entity.name = filteredName; + } - if (info !== undefined && !isPlayer) { - const ponyInfo = bitmask(info, PONY_INFO_KEY); + 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 (entity.fake) { + (entity as Pony).palettePonyInfo = decodePonyInfo(ponyInfo, mockPaletteManager); + } else { + updatePonyInfoWithPoof(game, entity, ponyInfo, crc); + } + } - if (action !== undefined) { - handleAction(game, id, action); - } + if (action !== undefined) { + handleAction(game, id, action); + } - applyIfSelected(game, id); - } else { - log(`handleUpdateEntity: missing entity: ${id}`); - } + 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); - } + 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); + 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))); - } + 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 + game: PonyTownGame, id: number, options: PonyOptions, name: string | undefined, info: string | Uint8Array, + state: EntityState ) { - if (!game.webgl) { - throw new Error('WebGL not initialized'); - } + if (!game.webgl) { + throw new Error('WebGL not initialized'); + } - const pony = createPony(id, state, info, game.webgl.palettes.defaultPalette, game.paletteManager); + const pony = createPony(id, state, info, game.webgl.palettes.defaultPalette, game.paletteManager); - if (name) { - pony.name = name; - } + if (name) { + pony.name = name; + } - updateEntityOptionsInternal(pony, options, game); + updateEntityOptionsInternal(pony, options, game); - // bypass name/info filtering for player pony - if (id === game.playerId) { - if (game.playerName) { - pony.name = game.playerName; - } + // 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); - } - } + if (game.playerInfo) { + pony.crc = game.playerCRC; + updatePonyInfo(pony, game.playerInfo, game.applyChanges); + } + } - return pony; + 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 (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 (right !== undefined) { + state = setFlag(state, EntityState.FacingRight, right); + game.rightOverride = undefined; + } - if (headTurned !== undefined) { - state = setFlag(state, EntityState.HeadTurned, headTurned); - game.headTurnedOverride = undefined; - } + if (headTurned !== undefined) { + state = setFlag(state, EntityState.HeadTurned, headTurned); + game.headTurnedOverride = undefined; + } - if (stateOverride !== undefined) { - if (stateOverride !== getPonyState(state)) { - state = setPonyState(state, stateOverride); - } + if (stateOverride !== undefined) { + if (stateOverride !== getPonyState(state)) { + state = setPonyState(state, stateOverride); + } - game.stateOverride = undefined; - } + game.stateOverride = undefined; + } - game.onActionsUpdate.next(); - } + game.onActionsUpdate.next(); + } - const wasPonyFlying = isPonyFlying(entity); - const hadLight = hasDrawLight(entity); - const hadLightSprite = hasLightSprite(entity); + const wasPonyFlying = isPonyFlying(entity); + const hadLight = hasDrawLight(entity); + const hadLightSprite = hasLightSprite(entity); - entity.state = state; + entity.state = state; - if (!wasPonyFlying && isPonyFlying(entity) && isPony(entity)) { - entity.inTheAirDelay = FLY_DELAY; - } + if (!wasPonyFlying && isPonyFlying(entity) && isPony(entity)) { + entity.inTheAirDelay = FLY_DELAY; + } - const hasLight = hasDrawLight(entity); - const hasLightSprite1 = hasLightSprite(entity); + const hasLight = hasDrawLight(entity); + const hasLightSprite1 = hasLightSprite(entity); - addOrRemoveFromEntityList(game.map.entitiesLight, entity, hadLight, hasLight); - addOrRemoveFromEntityList(game.map.entitiesLightSprite, entity, hadLightSprite, hasLightSprite1); + 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 (!entity.fake && !isHidden(entity) && hasFlag(playerState, EntityPlayerState.Hidden)) { + playEffect(game, entity, poof.type); - if (isSelected(game, entity.id)) { - game.select(undefined); - } - } + if (isSelected(game, entity.id)) { + game.select(undefined); + } + } - entity.playerState = playerState; + entity.playerState = playerState; } function findEntityByIdInGame(game: PonyTownGame, id: number) { - let entity = findEntityById(game.map, id); + let entity = findEntityById(game.map, id); - if (!entity && isSelected(game, id)) { - entity = game.selected; - } + if (!entity && isSelected(game, id)) { + entity = game.selected; + } - return entity; + return entity; } function applyIfSelected(game: PonyTownGame, id: number) { - if (isSelected(game, id)) { - game.applyChanges(); - } + if (isSelected(game, id)) { + game.applyChanges(); + } } export function handleUpdates(game: PonyTownGame, updates: Uint8Array) { - const reader = createBinaryReader(updates); + const reader = createBinaryReader(updates); - while (reader.offset < reader.view.byteLength) { - const type = readUint8(reader) as UpdateType; + 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); + 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); - } - } + 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); - }; + 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); - } - } + 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); + const entity = findEntityById(game.map, id); - if (entity) { - removeEntity(game.map, entity); - } else { - log(`handleRemoveEntity: Missing entity: ${id}`); - } + if (entity) { + removeEntity(game.map, entity); + } else { + log(`handleRemoveEntity: Missing entity: ${id}`); + } - if (id === game.playerId) { - log(`handleRemoveEntity: Removing player`); - } + if (id === game.playerId) { + log(`handleRemoveEntity: Removing player`); + } - if (entity && entity.type === PONY_TYPE) { - playEffect(game, entity, poof.type); - } + 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); - } + 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; + 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); + 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}`); - } + 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; + 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); - } + 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; - } + if (!id) { + return undefined; + } - let entity: Entity | FakeEntity | undefined = findEntityById(game.map, id); + 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 && 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 (!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 (friend) { + entity = { fake: true, type: PONY_TYPE, id: friend.entityId, name: friend.actualName, crc: friend.crc }; + } + } - if (!entity) { - entity = game.findEntityFromChatLog(id); - } + if (!entity) { + entity = game.findEntityFromChatLog(id); + } - return entity; + return entity; } export function findBestEntityByName(game: PonyTownGame, name: string): Entity | FakeEntity | undefined { - const regex = new RegExp(`^${escapeRegExp(name)}$`, 'i'); + 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 }; - } - } - } + 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; + 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 (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); - } + if (!result) { + result = game.findEntityFromChatLogByName(name); + } - return result; + return result; } export function findMatchingEntityNames(game: PonyTownGame, match: string): string[] { - const result: string[] = []; - const ids = new Set(); - const regex = new RegExp(`^${escapeRegExp(match)}`, 'i'); + const result: string[] = []; + const ids = new Set(); + 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); - } - } - } + 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); - } - } + 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; + 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; - } + 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; - } + cachedFilter = filter; + } - return cachedRegex && cachedRegex.test(message); + return cachedRegex && cachedRegex.test(message); } export function handleSays(game: PonyTownGame, id: number, message: string, type: MessageType) { - const entity = findEntityOrMockByAnyMeans(game, id); + 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)); - } + 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; - } - } - } + if (game.model.friends) { + for (const friend of game.model.friends) { + if (friend.entityId === id) { + return true; + } + } + } - return false; + return false; } function shouldShowChatMessage(game: PonyTownGame, entity: Entity | FakeEntity, message: string, type: MessageType): boolean { - if (entity === game.player) - return true; + if (entity === game.player) + return true; - if (isWhisperTo(type)) - return true; + if (isWhisperTo(type)) + return true; - if (isWhisper(type) && isFriendEntityId(game, entity.id)) - return true; + if (isWhisper(type) && isFriendEntityId(game, entity.id)) + return true; - if (isPublicMessage(type) && !entity.fake && !isChatVisible(game.camera, entity)) - return false; + if (isPublicMessage(type) && !entity.fake && !isChatVisible(game.camera, entity)) + return false; - if (isNonIgnorableMessage(type)) - return true; + if (isNonIgnorableMessage(type)) + return true; - if (game.settings.account.filterCyrillic && containsCyrillic(message)) - return false; + if (game.settings.account.filterCyrillic && containsCyrillic(message)) + return false; - if (game.settings.account.ignorePublicChat && isPublicMessage(type)) - return false; + if (game.settings.account.ignorePublicChat && isPublicMessage(type)) + return false; - if (isWhisper(type) && game.settings.account.ignoreNonFriendWhispers) - return false; + if (isWhisper(type) && game.settings.account.ignoreNonFriendWhispers) + return false; - if (containsFilteredWords(message, game.settings.account.filterWords)) - return false; + if (containsFilteredWords(message, game.settings.account.filterWords)) + return false; - return true; + return true; } function isChatInRange(entity: Entity, player: Entity | undefined, range: number | undefined) { - return player === undefined || isChatlogRangeUnlimited(range) || distance(entity, player) < range!; + 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.type !== PONY_TYPE) + return false; - if (entity.fake) - return true; + if (entity.fake) + return true; - if (!isPublicMessage(type)) - return true; + if (!isPublicMessage(type)) + return true; - if (!isChatInRange(entity, game.player, game.settings.account.chatlogRange)) - return false; + if (!isChatInRange(entity, game.player, game.settings.account.chatlogRange)) + return false; - return true; + return true; } export function handleSay(game: PonyTownGame, entity: Entity | FakeEntity, message: string, type: MessageType) { - if (!shouldShowChatMessage(game, entity, message, type)) - return; + 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 (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 (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 (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 }); - } - } + 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)!; + name = filterEntityName(game, name, nameBad)!; - for (let i = 0; i < game.incompleteSays.length;) { - const say = game.incompleteSays[i]; + 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++; - } - } + 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); + 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); - } + 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; - } + 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 + 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 (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 (member) { + game.apply(() => member.pony = entity); + } - if (isSelected(game, id)) { - game.select(entity); - } + if (isSelected(game, id)) { + game.select(entity); + } - return entity; - } else { - const entity = createAnEntity(type, id, x, y, options, game.paletteManager, game); + return entity; + } else { + const entity = createAnEntity(type, id, x, y, options, game.paletteManager, game); - entity.state = state; + entity.state = state; - if (name) { - entity.name = name; - } + if (name) { + entity.name = name; + } - return entity; - } + return entity; + } } function updateEntityOptionsInternal(entity: Entity, options: Partial, game: PonyTownGame) { - Object.assign(entity, options); + Object.assign(entity, options); - if (isPony(entity) && 'hold' in options) { - updatePonyHold(entity, game); - } + if (isPony(entity) && 'hold' in options) { + updatePonyHold(entity, game); + } } export function handleUpdateFriends(game: PonyTownGame, friends: FriendStatusData[], removeMissing: boolean) { - if (!game.model.friends) - return; + 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); + 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: '', - }; + 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); - } + game.model.friends.push(friend); + } - friend.online = hasFlag(status, FriendStatusFlags.Online); + friend.online = hasFlag(status, FriendStatusFlags.Online); - if (accountName !== undefined) { - friend.accountName = accountName; - } + if (accountName !== undefined) { + friend.accountName = accountName; + } - if (entityId !== undefined) { - if (game.lastWhisperFrom && game.lastWhisperFrom.accountId === friend.accountId) { - game.lastWhisperFrom.entityId = entityId; - } + if (entityId !== undefined) { + if (game.lastWhisperFrom && game.lastWhisperFrom.accountId === friend.accountId) { + game.lastWhisperFrom.entityId = entityId; + } - game.onEntityIdUpdate.next({ old: friend.entityId, new: entityId }); + game.onEntityIdUpdate.next({ old: friend.entityId, new: entityId }); - friend.entityId = entityId; - } + friend.entityId = entityId; + } - if (name !== undefined) { - friend.name = name; - friend.nameBad = nameBad; - friend.actualName = filterEntityName(game, name, nameBad) || ''; - } + if (name !== undefined) { + friend.name = name; + friend.nameBad = nameBad; + friend.actualName = filterEntityName(game, name, nameBad) || ''; + } - if (crc !== undefined) { - friend.crc = crc; - } + if (crc !== undefined) { + friend.crc = crc; + } - if (info !== undefined) { - friend.pony = info; - friend.ponyInfo = decodePonyInfo(info, mockPaletteManager); - } + 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 (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); - } - } + 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'); - } + DEVELOPMENT && console.log('Refreshing friend list'); + } - game.model.friends.sort(compareFriends); - game.apply(() => { }); + game.model.friends.sort(compareFriends); + game.apply(() => { }); } diff --git a/src/ts/client/htmlUtils.ts b/src/ts/client/htmlUtils.ts index fe9ff58..0a01e59 100644 --- a/src/ts/client/htmlUtils.ts +++ b/src/ts/client/htmlUtils.ts @@ -4,171 +4,171 @@ 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); + 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 (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]); - } + if (emote) { + img.setAttribute('aria-label', emote.names[0]); + } - getEmojiImageAsync(sprite, src => { - img.alt = x; - img.src = src; - img.style.visibility = 'visible'; - }); + getEmojiImageAsync(sprite, src => { + img.alt = x; + img.src = src; + img.style.visibility = 'visible'; + }); - return img; - } else { - return document.createTextNode(x); - } - }) : []; + return img; + } else { + return document.createTextNode(x); + } + }) : []; } export function textNode(text: string) { - return document.createTextNode(text); + return document.createTextNode(text); } export function element( - tag: string, className?: string, nodes?: (Node | undefined)[], attrs?: Dict, events?: Dict<() => any> + tag: string, className?: string, nodes?: (Node | undefined)[], attrs?: Dict, events?: Dict<() => any> ) { - const element = document.createElement(tag); + const element = document.createElement(tag); - if (className) { - element.className = className; - } + if (className) { + element.className = className; + } - if (nodes !== undefined) { - appendAllNodes(element, nodes); - } + if (nodes !== undefined) { + appendAllNodes(element, nodes); + } - if (attrs !== undefined) { - Object.keys(attrs).forEach(key => element.setAttribute(key, attrs[key])); - } + 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])); - } + if (events !== undefined) { + Object.keys(events).forEach(key => element.addEventListener(key, events[key])); + } - return element; + return element; } export function appendAllNodes(element: Element, nodes: (Node | undefined)[]) { - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; - if (node !== undefined) { - element.appendChild(node); - } - } + if (node !== undefined) { + element.appendChild(node); + } + } } export function removeAllNodes(element: Element) { - let child: Node | null; + let child: Node | null; - while (child = element.lastChild) { - element.removeChild(child); - } + while (child = element.lastChild) { + element.removeChild(child); + } } export function removeFirstChild(element: HTMLElement) { - let child: Node | null; + let child: Node | null; - if (child = element.firstChild) { - element.removeChild(child); - } + if (child = element.firstChild) { + element.removeChild(child); + } } export function removeElement(element: HTMLElement) { - element.parentElement && element.parentElement.removeChild(element); + element.parentElement && element.parentElement.removeChild(element); } export function replaceNodes(element: HTMLElement, text: string) { - while (element.lastChild && element.lastChild !== element.firstChild) { - element.removeChild(element.lastChild); - } + while (element.lastChild && element.lastChild !== element.firstChild) { + element.removeChild(element.lastChild); + } - let firstChild = element.firstChild; + let firstChild = element.firstChild; - if (!firstChild) { - element.appendChild(firstChild = textNode('')); - } + if (!firstChild) { + element.appendChild(firstChild = textNode('')); + } - if (hasEmojis(text)) { - firstChild.nodeValue = ''; - appendAllNodes(element, createHtmlNodes(text, 2)); - } else { - firstChild.nodeValue = text; - } + 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; + const elements = Array.from(document.querySelectorAll(selector)); + let current = element.parentElement; - while (current && elements.indexOf(current) === -1) { - current = current.parentElement; - } + while (current && elements.indexOf(current) === -1) { + current = current.parentElement; + } - return current; + 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[]; + 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); + const elements = findFocusableElements(root); - if (elements.length) { - elements[0].focus(); - return elements[0]; - } + if (elements.length) { + elements[0].focus(); + return elements[0]; + } - return undefined; + return undefined; } export function focusElement(root: HTMLElement, selector: string) { - const target = root.querySelector(selector) as HTMLElement | null; + const target = root.querySelector(selector) as HTMLElement | null; - if (target) { - target.focus(); - } + if (target) { + target.focus(); + } } export function focusElementAfterTimeout(root: HTMLElement, selector: string) { - setTimeout(() => focusElement(root, selector), 10); + 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; - } - } + for (let current = child.parentElement; current; current = current.parentElement) { + if (current === parent) { + return true; + } + } - return false; + 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); + 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; + const styleElement = document.createElement('style'); + styleElement.appendChild(document.createTextNode(style)); + document.head.appendChild(styleElement); + return styleElement; } diff --git a/src/ts/client/icons.ts b/src/ts/client/icons.ts index 762c805..cf76de1 100644 --- a/src/ts/client/icons.ts +++ b/src/ts/client/icons.ts @@ -1,206 +1,206 @@ import { - faCrown, - faPlug, - faGamepad, - faMobile, - faTablet, - faTv, + 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, + 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, + 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, + 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', ''], + 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, + 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, + // 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, }; diff --git a/src/ts/client/input/gamepad.ts b/src/ts/client/input/gamepad.ts index 8cb0148..90d8abd 100644 --- a/src/ts/client/input/gamepad.ts +++ b/src/ts/client/input/gamepad.ts @@ -4,157 +4,157 @@ import { InputManager } from './inputManager'; import { isFocused } from '../clientUtils'; interface GamepadInstance { - gamepad: Gamepad; - mapping: GamepadMapping; + gamepad: Gamepad; + mapping: GamepadMapping; } const JOYSTICK_THRESHHOLD = 0.2; function createGamepad(gamepad: Gamepad): GamepadInstance { - const mapping = detectMapping(gamepad.id, navigator.userAgent); - return { gamepad, mapping }; + 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]; + 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; - } - } + if (id.indexOf(supported.id) !== -1 && browser.indexOf(supported.os) !== -1 && browser.indexOf(browser) !== -1) { + return true; + } + } - return false; + 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]; - } - } + for (let i = 0; i < GAMEPAD_MAPPINGS.length; i++) { + if (isCompatible(GAMEPAD_MAPPINGS[i], id, browser)) { + return GAMEPAD_MAPPINGS[i]; + } + } - return GAMEPAD_MAPPINGS[0]; + 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; + 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; + const button = mapping.buttons[name] as any; - if (!button) { - return false; - } + if (!button) { + return false; + } - if (button.index !== undefined) { - return gamepad.buttons[button.index] && gamepad.buttons[button.index].pressed; - } + 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; - } - } + 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; + 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; + 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]; + const gamepads = navigator.getGamepads(); + const gamepad = gamepads[this.gamepadIndex]; - if (!gamepad) { - this.scanGamepads(); - return; - } + if (!gamepad) { + this.scanGamepads(); + return; + } - const pad = createGamepad(gamepad); + 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.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_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(); + 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]; + // 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; - } - } + 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(); - } - } + 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); + 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; - } + 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; + return zeroed; } diff --git a/src/ts/client/input/input.ts b/src/ts/client/input/input.ts index e0a0df2..8ea0b71 100644 --- a/src/ts/client/input/input.ts +++ b/src/ts/client/input/input.ts @@ -1,144 +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, + // 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; + initialize(element: HTMLElement): void; + release(): void; + update(): void; + clear(): void; } diff --git a/src/ts/client/input/inputManager.ts b/src/ts/client/input/inputManager.ts index a359a1c..45d6aeb 100644 --- a/src/ts/client/input/inputManager.ts +++ b/src/ts/client/input/inputManager.ts @@ -11,160 +11,160 @@ 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), - ]; + 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.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; - } + 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 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; - } - } + 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; + 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); - } + if (this.actions[input] && this.actions[input].length) { + for (const action of this.actions[input]) { + action(input, value); + } - return true; - } - } + 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; - } - } + return false; + } + addValue(input: Key, value: number) { + if (input < 0 || input >= KEYS) { + console.warn(`Input out of range: ${input}`); + } else { + this.state[input] += value; + } + } } diff --git a/src/ts/client/input/keyboard.ts b/src/ts/client/input/keyboard.ts index 4962d1b..871ac67 100644 --- a/src/ts/client/input/keyboard.ts +++ b/src/ts/client/input/keyboard.ts @@ -5,92 +5,92 @@ 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); + 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; + 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; - } + if (firefox) { + if (key === 173) return Key.DASH; + if (key === 61) return Key.EQUALS; + } - return key; + return key; } const iosKeyToKeyCode: { [key: string]: number | undefined; } = { - UIKeyInputEscape: Key.ESCAPE, - UIKeyInputUpArrow: Key.UP, - UIKeyInputLeftArrow: Key.LEFT, - UIKeyInputRightArrow: Key.RIGHT, - UIKeyInputDownArrow: Key.DOWN, + 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); + 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 (!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); + 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; + // fix keyCode on iOS bluetooth keyboard + if (code === 0) { + code = iosKeyToKeyCode[e.key] || 0; - if (code === 0) { - code = this.stack.pop() || 0; - } - } + if (code === 0) { + code = this.stack.pop() || 0; + } + } - if (this.manager.setValue(code, 0)) { - e.preventDefault(); - e.stopPropagation(); - } + if (this.manager.setValue(code, 0)) { + e.preventDefault(); + e.stopPropagation(); + } - removeItem(this.stack, code); - } - private blur = () => { - this.manager.clear(); - } + removeItem(this.stack, code); + } + private blur = () => { + this.manager.clear(); + } } diff --git a/src/ts/client/input/mouse.ts b/src/ts/client/input/mouse.ts index f249eb2..7386dde 100644 --- a/src/ts/client/input/mouse.ts +++ b/src/ts/client/input/mouse.ts @@ -5,83 +5,83 @@ 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; + 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; - } + 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(); + 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; + this.manager.usingTouch = false; - const button = MOUSE_BUTTONS[e.button]; + 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, 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); - } - } + 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); + } + } } diff --git a/src/ts/client/input/touch.ts b/src/ts/client/input/touch.ts index e8123e3..eb8bf44 100644 --- a/src/ts/client/input/touch.ts +++ b/src/ts/client/input/touch.ts @@ -4,200 +4,200 @@ 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 (id !== -1) { + for (let i = 0; i < e.changedTouches.length; ++i) { + const touch = e.changedTouches.item(i); - if (touch && touch.identifier === id) { - return touch; - } - } - } + if (touch && touch.identifier === id) { + return touch; + } + } + } - return undefined; + 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')!; + 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; + 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; - } + 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; + 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.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 (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 (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 (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 (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 (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(); + 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; + this.manager.usingTouch = true; - if (this.touchId === -1) { - const touch = e.changedTouches.item(0); + 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 = 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(); + if (touch) { + this.tapInvalidated = true; + this.touch2Id = touch.identifier; + } + } + } + private touchmove = (e: any) => { + e.preventDefault(); + e.stopPropagation(); - const touch = getTouch(e, this.touchId); + 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(); + 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); + 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); - } + 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(); - } + this.resetTouch(); + } - const touch2 = getTouch(e, this.touch2Id); + 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(); + 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, - }; - } + return { + x: touch.clientX - left, + y: touch.clientY - top, + }; + } } diff --git a/src/ts/client/partyUtils.ts b/src/ts/client/partyUtils.ts index af81fa5..21800b7 100644 --- a/src/ts/client/partyUtils.ts +++ b/src/ts/client/partyUtils.ts @@ -3,42 +3,42 @@ 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: [], - }; + if (!info || !info.length) { + return undefined; + } else { + const party = current || { + leaderId: 0, + members: [], + }; - remove(party.members, p => !info.some(m => p.id === m.id)); + remove(party.members, p => !info.some(m => p.id === m.id)); - info.forEach(m => { - const existing = party.members.find(x => m.id === x.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 (existing) { + Object.assign(existing, m); + } else { + party.members.push(m); + } - if (m.leader) { - party.leaderId = m.id; - } - }); + if (m.leader) { + party.leaderId = m.id; + } + }); - return party; - } + return party; + } } export function isPonyInParty(party: PartyInfo | undefined, pony: Pony, pending: boolean) { - return !!party && party.members.some(m => m.pony === pony && (pending || !m.pending)); + 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; + 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; + return game.party !== undefined && game.party.members.length > 0; } diff --git a/src/ts/client/playerActions.ts b/src/ts/client/playerActions.ts index edb6b86..3a5448e 100644 --- a/src/ts/client/playerActions.ts +++ b/src/ts/client/playerActions.ts @@ -2,8 +2,8 @@ import { isCommand, processCommand, hasFlag, includes, point } from '../common/u import { canPonyLie, canPonyFlyUp, canPonyStand, canPonySit, doBoopPonyAction } from '../common/pony'; import { PonyTownGame } from './game'; import { - setPonyState, canBoop, isPonyLying, isPonyFlying, isPonyStanding, isPonySitting, getInteractBounds, - isFacingRight, closestEntity, entityInRange + 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'; @@ -14,216 +14,216 @@ import { pointToWorld, roundPositionX, roundPositionY } from '../common/position 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 (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; - } - } + 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; - } - } + 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; + return false; } export function upAction(game: PonyTownGame) { - const player = game.player; + 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); - } - } + 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; + 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); - } - } + 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(); - } + 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(); - } + 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(); - } + 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(); - } + 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); - } + 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(); - } + 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; + 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 (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()); - } - } + 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; + 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)); - } - } + 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); + 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] : []; - } - }); + 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; + 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 })), - })); - } + 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)); + game.editor.draggingEntities = true; + game.editor.draggingStart = hover; + game.editor.selectedEntities.forEach(e => e.draggingStart = point(e.x, e.y)); } diff --git a/src/ts/client/polyfils.ts b/src/ts/client/polyfils.ts index 3981639..e33fbbe 100644 --- a/src/ts/client/polyfils.ts +++ b/src/ts/client/polyfils.ts @@ -2,32 +2,32 @@ // Safari <= 8.4, Android try { - if (!('performance' in window && 'now' in performance)) { - (window as any).performance = Date; - } + if (!('performance' in window && 'now' in performance)) { + (window as any).performance = Date; + } } catch { } try { - if (!('getGamepads' in navigator)) { - (window.navigator as any).getGamepads = () => []; - } + 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; - } + 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; - } + if (!('cancelAnimationFrame' in window)) { + (window as any).cancelAnimationFrame = clearTimeout; + } } catch { } // IE <= 10 try { - if (!('devicePixelRatio' in window)) { - (window as any).devicePixelRatio = 1; - } + if (!('devicePixelRatio' in window)) { + (window as any).devicePixelRatio = 1; + } } catch { } diff --git a/src/ts/client/ponyAnimations.ts b/src/ts/client/ponyAnimations.ts index b80d71d..1dd7d73 100644 --- a/src/ts/client/ponyAnimations.ts +++ b/src/ts/client/ponyAnimations.ts @@ -4,459 +4,459 @@ 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 + 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 { - return { - body, head, wing, tail, - frontLeg, frontFarLeg, backLeg, backFarLeg, - bodyX, bodyY, headX, headY, - frontLegX, frontLegY, frontFarLegX, frontFarLegY, - backLegX, backLegY, backFarLegX, backFarLegY - }; + 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[][] + name: string, fps: number, loop: boolean, frames: number[][], shadowOffsets?: number[][] ): Readonly { - if (shadowOffsets && shadowOffsets.length !== frames.length) { - throw new Error(`Incorrect frame count for shadowOffsets for ${name}`); - } + if (shadowOffsets && shadowOffsets.length !== frames.length) { + throw new Error(`Incorrect frame count for shadowOffsets for ${name}`); + } - const shadow = shadowOffsets && shadowOffsets.map(([frame, offset]) => ({ frame, offset })); + const shadow = shadowOffsets && shadowOffsets.map(([frame, offset]) => ({ frame, offset })); - return { name, loop, fps, frames: frames.map(createBodyFrame), shadow }; + return { name, loop, fps, frames: frames.map(createBodyFrame), shadow }; } export const stand = createBodyAnimation('stand', 24, true, [ - [1, 1, 0, 0, 1, 1, 1, 1], + [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] + [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] + [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] + [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] + [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] + [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] + [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] + [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], + [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], + [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], + [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], + [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] + [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], + [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], + [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], + [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], + [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], + [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]), + [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], + ...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], + [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], + [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] + [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] + [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], + [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] + [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] + [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, 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] + // [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, 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] + // [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] + [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] + [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] + [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] + [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]), + ...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]; @@ -464,78 +464,78 @@ 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, + 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 })))), - }; + 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 }; + 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) }; + return { name, fps, loop, frames: frames.map(createHeadFrame) }; } export const smile = createHeadAnimation('smile', 24, true, [ - [0, 0, 1, 1, 0], + [0, 0, 1, 1, 0], ]); export const nom = createHeadAnimation('nom', 12, true, [ - [0, 0, 1, 1, 0], - [0, 0, 1, 1, 25], + [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]), + ...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], + [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]), + [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]), + [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]), + [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]), + [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, + smile, nom, laugh, yawn, surprise, surpriseSad, sneeze, excite, ]; // default animations diff --git a/src/ts/client/ponyDraw.ts b/src/ts/client/ponyDraw.ts index fdd4ddc..29018d4 100644 --- a/src/ts/client/ponyDraw.ts +++ b/src/ts/client/ponyDraw.ts @@ -1,7 +1,7 @@ import { - PonyEye, PonyState, PalettePonyInfo, PaletteSpriteSet, Palette, HeadAnimationFrame, - Eye, Iris, ColorExtraSets, ExpressionExtra, BodyAnimationFrame, DrawPonyOptions, Muzzle, BodyShadow, DrawOptions, - NoDraw, PaletteSpriteBatch, defaultDrawOptions, PonyStateFlags, PaletteManager, isEyeSleeping, Matrix2D, + 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'; @@ -10,10 +10,10 @@ 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 + 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'; @@ -25,18 +25,18 @@ type State = Readonly; type Options = Readonly; const holdingDrawOptions: DrawOptions = { - ...defaultDrawOptions, - shadowColor: TRANSPARENT, + ...defaultDrawOptions, + shadowColor: TRANSPARENT, }; function checker(parts: number[]) { - const set = new Set(parts); - return (part: number) => set.has(part); + 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 check = checker(parts); + return (set: PaletteSpriteSet | undefined) => set !== undefined && !!set.type && check(set.type); } const SHADOW_OX = 20; @@ -60,92 +60,92 @@ const headFlipOffsetY = 42; const headTransform = createMat2D(); function clamp(value: number, min: number, max: number): number { - return value > min ? (value < max ? value : max) : min; + return value > min ? (value < max ? value : max) : min; } function at(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)}`); - // } - // } + // 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)]; + return items[clamp(index | 0, 0, items.length - 1)]; } function att(items: T[] | undefined, index: any): T | undefined { - return items && items[clamp(index | 0, 0, items.length - 1)]; + return items && items[clamp(index | 0, 0, items.length - 1)]; } function atDef(items: T[] | undefined, index: number, def: T): T { - return (items && items.length > 0 && index >= 0 && index < items.length) ? items[index | 0] : def; + return (items && items.length > 0 && index >= 0 && index < items.length) ? items[index | 0] : def; } export function getPonyAnimationFrame({ frames }: { frames: T[] }, frame: number, defaultFrame: T): T { - return frames.length > 0 ? frames[Math.max(0, frame) % frames.length] : defaultFrame; + 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 }; + 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 }; + 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 + originalTransform: Matrix2D | undefined, headX: number, headY: number, { headTilt, headTurned }: State ) { - if (originalTransform !== undefined) { - copyMat2D(headTransform, originalTransform); - } else { - identityMat2D(headTransform); - } + if (originalTransform !== undefined) { + copyMat2D(headTransform, originalTransform); + } else { + identityMat2D(headTransform); + } - translateMat2D(headTransform, headTransform, headX + headFlipOffsetX, headY + headFlipOffsetY); + translateMat2D(headTransform, headTransform, headX + headFlipOffsetX, headY + headFlipOffsetY); - if (headTilt) { - rotateMat2D(headTransform, headTransform, headTilt * 0.1); - } + if (headTilt) { + rotateMat2D(headTransform, headTransform, headTilt * 0.1); + } - scaleMat2D(headTransform, headTransform, headTurned ? -1 : 1, 1); - translateMat2D(headTransform, headTransform, -headFlipOffsetX, -headFlipOffsetY); - return headTransform; + 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 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), + 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; - } + if (TOOLS) { + return !hasFlag(options.no, flag); + } else { + return true; + } } const headOffsetsX = [0, 1, 1, 1, 1, 1, 0]; @@ -154,637 +154,637 @@ 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))])) - ]; + 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) }; - } + 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]), - ]; + 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); - } + 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 }, + { 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]; + 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 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 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 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 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; + 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); - } + 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); - } + // 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); - } + // 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); + // 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 (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); + 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); + batch.save(); + batch.multiplyTransform(headTransform); - if (swimming) { - // batch.drawRect(0xffff0066, 0, headCropY, cropW, cropH); - batch.crop(0, headCropY, cropW, cropH); - } + 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 (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.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.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 (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); - } + if (!noMane) { + drawSet(batch, sprites.headAccessoriesBehind, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE); + } - drawSet(batch, sprites.backBehindManes, info.backMane, 0, maneBehindOffsetY, WHITE); + drawSet(batch, sprites.backBehindManes, info.backMane, 0, maneBehindOffsetY, WHITE); - if (!state.headTurned) { - drawSet(batch, sprites.behindManes, info.mane, 0, maneBehindOffsetY, WHITE); - } + if (!state.headTurned) { + drawSet(batch, sprites.behindManes, info.mane, 0, maneBehindOffsetY, WHITE); + } - batch.restore(); + batch.restore(); - // chest accessory behind - drawSet(batch, chestBehind[body], info.chestAccessory, chestX, chestY, WHITE); - } + // 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); + // 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; + 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 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 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 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); - } + // 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; + // 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); + 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); - } + // 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); - } + // 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); + // neck accessory + const frontNeckAccessory = true; // hasPart(info.neckAccessory, FRONT_NECK_ACCESSORIES); - // if (!frontNeckAccessory) { - // drawSpriteSet(context, neckAccessories[frame.body], info.neckAccessory, x, y); - // } + // 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; + 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); - } + // 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); - } + // 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); - } + // 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); + const isChestAccessoryInFront = chestAccessoryInFront(info.chestAccessory); - // chest accessory - if (!isChestAccessoryInFront && draw(options, NoDraw.Front)) { - drawSet(batch, chest[body], info.chestAccessory, chestX, chestY, WHITE); - } + // 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 + 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); - } + // 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); - } + // 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 (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); - } + // 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); - } + // 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); + // 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); + // 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]; + // 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(); - } + if (swimming) { + batch.clearCrop(); + } - batch.save(); - batch.multiplyTransform(headTransform); + batch.save(); + batch.multiplyTransform(headTransform); - if (swimming) { - batch.crop(0, headCropY, cropW, cropH); - } + if (swimming) { + batch.crop(0, headCropY, cropW, cropH); + } - if (headTurned) { - drawSet(batch, sprites.behindManes, info.mane, 0, maneBehindOffsetY, WHITE); - } + 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); + 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(); + batch.restore(); - if (swimming) { - batch.drawSprite(wake.front.frames[wakeFrame], WHITE, info.waterPalette, wakeX, wakeY); - } + 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, + 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); + 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 (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); - } + 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; + 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); - } + 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; + 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; - } + // 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; - } - } - } + 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; + 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.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 (draw(options, NoDraw.Front)) { + drawSet(batch, sprites.facialHairBehind, info.facialHair, x, y, WHITE); + } - if (drawFaceExtra !== undefined) { - drawFaceExtra(batch); - } + if (drawFaceExtra !== undefined) { + drawFaceExtra(batch); + } - const faceAccessory = info.faceAccessory; - let faceAccessoryType = 0; - let faceAccessoryPattern = 0; + 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 (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 (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 (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; + 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]; + 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); + 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); - } + 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); + drawSet(batch, noses, info.nose, x, y, WHITE); - if (info.fangs && nose.fangs) { - batch.drawSprite(nose.fangs, WHITE, info.defaultPalette, x, y); - } - } + 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); - } + if (draw(options, NoDraw.Front2)) { + drawSet(batch, sprites.facialHair, info.facialHair, x, y, WHITE); + } - const skipTopAndFrontMane = info.headAccessory !== undefined && info.headAccessory.type === 20; + 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.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 (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 (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.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 (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 (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); - // } - } + // 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; + 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 (!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 (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); - } + 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 + 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]; + const hoofInFront = hoofSet !== undefined && !!hoovesInFront[hoofSet.type]; - drawSet(batch, at(leg, frame), legSet, x, y, color); + drawSet(batch, at(leg, frame), legSet, x, y, color); - if (!hoofInFront) { - drawSet(batch, at(hoof, frame), hoofSet, 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); - } + // 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); + 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); - } + 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; + if (anim !== -1) + return anim; - const frame = expression === -1 ? base : expression; - const blink = blinkFrames[frame]; + const frame = expression === -1 ? base : expression; + const blink = blinkFrames[frame]; - if (blinkFrame > 1 && blink) { - const frameOffset = 6 - blinkFrame; + if (blinkFrame > 1 && blink) { + const frameOffset = 6 - blinkFrame; - if (frameOffset < blink.length) { - return blink[blink.length - frameOffset - 1]; - } - } + if (frameOffset < blink.length) { + return blink[blink.length - frameOffset - 1]; + } + } - return frame; + return frame; } function drawEye( - batch: Batch, eye: PonyEye | undefined, iris: Iris, info: Info, palette: Palette | undefined, eyePalette: Palette, - x: number, y: number + 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); - } + 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); - } + 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 + batch: Batch, sprites: ColorExtraSets, set: PaletteSpriteSet | undefined, x: number, y: number, tint: number ) { - if (set !== undefined) { - const patterns = att(sprites, set.type); + if (set !== undefined) { + const patterns = att(sprites, set.type); - if (patterns !== undefined) { - const patternSprite = at(patterns, set.pattern); + if (patterns !== undefined) { + const patternSprite = at(patterns, set.pattern); - if (patternSprite !== undefined) { - batch.drawSprite(patternSprite.color, tint, set.palette, x, y); - } - } - } + 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 + batch: Batch, sprites: ColorExtraSets, type: number, pattern: number, palette: Palette, extraPalette: Palette | undefined, + x: number, y: number, tint: number ) { - const patterns = att(sprites, type); + const patterns = att(sprites, type); - if (patterns !== undefined) { - const patternSprite = at(patterns, pattern); + if (patterns !== undefined) { + const patternSprite = at(patterns, pattern); - if (patternSprite !== undefined) { - batch.drawSprite(patternSprite.color, tint, palette, x, y); + 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); - } - } - } + if (patternSprite.extra !== undefined && extraPalette !== undefined) { + batch.drawSprite(patternSprite.extra, WHITE, extraPalette, x, y); + } + } + } } diff --git a/src/ts/client/ponyHelpers.ts b/src/ts/client/ponyHelpers.ts index 2030a68..f0057a2 100644 --- a/src/ts/client/ponyHelpers.ts +++ b/src/ts/client/ponyHelpers.ts @@ -5,52 +5,52 @@ 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, - }; + 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; + 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, - }; + 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, + }; } diff --git a/src/ts/client/ponyStates.ts b/src/ts/client/ponyStates.ts index 7117dbd..1381fad 100644 --- a/src/ts/client/ponyStates.ts +++ b/src/ts/client/ponyStates.ts @@ -1,13 +1,13 @@ 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 + 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 : ''; + return (DEVELOPMENT || SERVER) ? value : ''; } export const standing = state(n('standing'), stand); @@ -46,12 +46,12 @@ export const flyingToTrotting = state(n('flying-to-trotting'), flyToTrot, { bug: 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, + 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 }); @@ -120,36 +120,36 @@ transition(standing, swinging, { exitAfter: 0 }); transition(swinging, standing); export function isFlyingUp(state: AnimatorState | undefined) { - return state === flyingUp || state === trottingToFlying || state === swimmingToFlying; + return state === flyingUp || state === trottingToFlying || state === swimmingToFlying; } export function isFlyingDown(state: AnimatorState | undefined) { - return state === flyingDown || state === flyingToTrotting || state === flyingToSwimming; + return state === flyingDown || state === flyingToTrotting || state === flyingToSwimming; } export function isSwimmingState(state: AnimatorState | undefined) { - return state === swimming || state === trottingToSwimming || state === swimmingToTrotting || - state === flyingToSwimming || state === swimmingToFlying || state === boopingSwimming; + return state === swimming || state === trottingToSwimming || state === swimmingToTrotting || + state === flyingToSwimming || state === swimmingToFlying || state === boopingSwimming; } export function isFlyingUpOrDown(state: AnimatorState | undefined) { - return isFlyingUp(state) || isFlyingDown(state); + return isFlyingUp(state) || isFlyingDown(state); } export function isSittingDown(state: AnimatorState | undefined) { - return state === sittingDown; + return state === sittingDown; } export function isSittingUp(state: AnimatorState | undefined) { - return state === sittingUp; + return state === sittingUp; } export function toBoopState(state: AnimatorState) { - 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; - } + 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; + } } diff --git a/src/ts/client/ponyUtils.ts b/src/ts/client/ponyUtils.ts index 2a0f7b4..95af8ab 100644 --- a/src/ts/client/ponyUtils.ts +++ b/src/ts/client/ponyUtils.ts @@ -2,7 +2,7 @@ import { range, dropRight, compact, max, zip } from 'lodash'; import { - Eye, Iris, Muzzle, ExpressionExtra, Sprite, ColorExtraSets, PonyInfoBase, SpriteSetBase, ColorExtra, ColorExtraSet + 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'; @@ -20,14 +20,14 @@ export type Sprites = (Sprite | undefined)[]; export type Sets = ColorExtraSets[]; // [frame][type][pattern] export const headCenter = [ - undefined, - [[0].map(i => sprites.head[2]![0]![i])], + 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]); + .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)]); + .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]; @@ -44,77 +44,77 @@ export const neckAccessories = createCompleteSets(sprites.neckAccessories, bodyF export const waistAccessories = createCompleteSets(sprites.waistAccessories, bodyFrames + 1); function frameType(sets: Sets, frame: number, type: number) { - const set = sets[frame]; - return set && set[type]; + 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 = []; + 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))); - } + for (let frame = 0; frame < frameCount; frame++) { + result.push(typeRange.map(type => frameType(sets, frame, type) || frameType(result, frame - 1, type))); + } - return result; + return result; } export function canFly(info: PonyInfoBase) { - const type = info.wings && info.wings.type || 0; - return type > 0; + const type = info.wings && info.wings.type || 0; + return type > 0; } export function canMagic(info: PonyInfoBase) { - const type = info.horn && info.horn.type || 0; - return type === 1 || type === 2 || type === 3 || type === 14; + 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; - } + 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 === 6) return 7; + if (type === 7) return 6; - if (type === 9) return 10; - if (type === 10) return 9; + if (type === 9) return 10; + if (type === 10) return 9; - return type; + 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; - } + 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; + return pattern; } export const defaultExpression = { - left: Eye.Neutral, - leftIris: Iris.Forward, - right: Eye.Neutral, - rightIris: Iris.Forward, - muzzle: Muzzle.Neutral, - extra: ExpressionExtra.None, + 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)); + 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]); @@ -125,25 +125,25 @@ setupBlinkFrames([Eye.Angry, Eye.Angry2, Eye.Neutral4, Eye.Neutral5, Eye.Closed] // sets function mergeColorExtras(sprites: (ColorExtra | undefined)[]): ColorExtra | undefined { - const filtered = compact(sprites); + 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, - }; + 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); + return zip(...sets).map(mergeColorExtras); } function mergeSpriteSets(...sets: ColorExtraSets[]): ColorExtraSets { - return zip(...sets).map(mergeSprites); + return zip(...sets).map(mergeSprites); } export const backLegSleeves: Sets = sprites.backLegSleeves - .map(sets => sets && [undefined, undefined, undefined, undefined, undefined, ...sets]); + .map(sets => sets && [undefined, undefined, undefined, undefined, undefined, ...sets]); // TEMP: remove summer hat sprites.headAccessoriesBehind.pop(); @@ -157,20 +157,20 @@ export const mergedHeadAccessories = mergeSpriteSets(sprites.headAccessoriesBehi 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]!])!; + backAccessories[1], [undefined, undefined, undefined, undefined, undefined, ...sprites.backLegSleeves[1]!])!; export const mergedExtraAccessories = mergeSpriteSets(sprites.extraAccessoriesBehind, sprites.extraAccessories)! - .slice(0, DEVELOPMENT ? 100 : 2); + .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]!); + 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})`); - } + if (a.length !== b.length) { + throw new Error(`Invalid ${name} length (${a.length} !== ${b.length})`); + } } diff --git a/src/ts/client/rev.ts b/src/ts/client/rev.ts index 2ecee8f..d673eb5 100644 --- a/src/ts/client/rev.ts +++ b/src/ts/client/rev.ts @@ -2,11 +2,11 @@ import { REV } from '../generated/rev'; /* istanbul ignore next */ export function getUrl(name: string): string { - if (DEVELOPMENT) - return `/assets/${name}`; + if (DEVELOPMENT) + return `/assets/${name}`; - if (!REV[name]) - throw new Error(`Cannot find file url (${name})`); + if (!REV[name]) + throw new Error(`Cannot find file url (${name})`); - return `/assets/${name.replace(/(\.\S+)$/, `-${REV[name]}$1`)}`; + return `/assets/${name.replace(/(\.\S+)$/, `-${REV[name]}$1`)}`; } diff --git a/src/ts/client/sec.ts b/src/ts/client/sec.ts index 22adcc6..fb1cdfd 100644 --- a/src/ts/client/sec.ts +++ b/src/ts/client/sec.ts @@ -9,36 +9,36 @@ let setX = 0; let setY = 0; export function setupPlayer(game: PonyTownGame, player: Pony) { - const pony = player; - pony.flags = setFlag(pony.flags, EntityFlags.Interactive, false); + const pony = player; + pony.flags = setFlag(pony.flags, EntityFlags.Interactive, false); - if (isStaticCollision(player, game.map, false)) { - fixCollision(player, game.map); - } + if (isStaticCollision(player, game.map, false)) { + fixCollision(player, game.map); + } - game.setPlayer(pony); - currentPlayer = player; - savePlayerPosition(); + game.setPlayer(pony); + currentPlayer = player; + savePlayerPosition(); } export function savePlayerPosition() { - if (currentPlayer) { - setX = currentPlayer.x; - setY = currentPlayer.y; - } + 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'); - } - } + 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=/`; + document.cookie = `acl=${acl}; expires=${fromNow(WEEK).toUTCString()}; path=/`; }; diff --git a/src/ts/client/spriteAnimations.ts b/src/ts/client/spriteAnimations.ts index cf28cd6..418aec7 100644 --- a/src/ts/client/spriteAnimations.ts +++ b/src/ts/client/spriteAnimations.ts @@ -4,9 +4,9 @@ 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); + 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); + 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); @@ -24,8 +24,8 @@ export const holdPoofAnimation = createSpriteAnimation(sprites.hold_poof, 12, 0, 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[], + { 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 }; + return { start, middle, end, fps, palette, frames, loop, flipFrames }; } diff --git a/src/ts/client/spriteUtils.ts b/src/ts/client/spriteUtils.ts index 57c1d9c..33886ca 100644 --- a/src/ts/client/spriteUtils.ts +++ b/src/ts/client/spriteUtils.ts @@ -6,76 +6,76 @@ 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 }; + 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] })); + 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; + 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] }; + 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; + return sprite && sprite.colors ? Math.floor((sprite.colors - 1) / 2) : 0; } export function createSpriteUtils() { - createFonts(); + createFonts(); } type LoadImage = (src: string) => Promise; 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); + 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); + 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; + 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]; - } - } - }); + 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); + 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); + 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)); + return loadImage(getUrl(url)); } export const loadAndInitSpriteSheets = once(() => loadAndInitSheets(spriteSheets, loadImageFromUrl)); diff --git a/src/ts/client/tileUtils.ts b/src/ts/client/tileUtils.ts index 16d483f..ddc1e6e 100644 --- a/src/ts/client/tileUtils.ts +++ b/src/ts/client/tileUtils.ts @@ -1,6 +1,6 @@ import { - PaletteManager, Season, TileSets, Region, TileType, Camera, PaletteSpriteBatch, DrawOptions, WorldMap, IMap, - Sprite, MapType + 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'; @@ -18,18 +18,18 @@ 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); - } + while (TILE_COUNT_MAP.length < (tile + 1)) { + TILE_COUNT_MAP.push(1); + } - TILE_COUNT_MAP[tile] = count; + 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]; + TILE_MAP_MAP.push(tileIndex); + tileIndex += TILE_COUNT_MAP[i]; } // 1 | 2 | 4 @@ -38,565 +38,565 @@ for (let i = 0; i <= 47; i++) { // ----+----+---- // 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 + 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, + 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 + TileTypeNumber.Water, TileTypeNumber.Water2, TileTypeNumber.Water3, TileTypeNumber.Water4 ]; export function updateTileSets( - paletteManager: PaletteManager, tileSets: TileSets | undefined, season: Season, mapType: MapType + paletteManager: PaletteManager, tileSets: TileSets | undefined, season: Season, mapType: MapType ) { - if (tileSets) { - tileSets.forEach(t => releasePalette(t.palette)); - } + if (tileSets) { + tileSets.forEach(t => releasePalette(t.palette)); + } - return createTileSets(paletteManager, season, mapType); + 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 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]; + 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]), - }, - ]; + // 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 + 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; + 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); + 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)]; + 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; - } + 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]; + 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; - } + 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; + const rx = (x + regionX) * tileWidth; + const ry = (y + regionY) * tileHeight; - if (DEVELOPMENT && !tileSet.sprites[tileSpriteIndex]) { - console.error('Missing sprite', tileSetIndex, tileSpriteIndex); - } + if (DEVELOPMENT && !tileSet.sprites[tileSpriteIndex]) { + console.error('Missing sprite', tileSetIndex, tileSpriteIndex); + } - batch.drawSprite(tileSet.sprites[tileSpriteIndex], WHITE, tileSet.palette, rx, ry); - } - } - } + 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; + 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); + 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)]; + for (let y = minY; y < maxY; y++) { + for (let x = minX; x < maxX; x++) { + const tileIndex = tileIndices[x | (y << 3)]; - if (tileIndex === -1) { - continue; - } + if (tileIndex === -1) { + continue; + } - const tileTypeNumber = tileIndex >>> 8; - const tileSpriteIndex = tileIndex & 0xff; - const rx = (x + regionX) * tileWidth; - const ry = (y + regionY) * tileHeight; + 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.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); - } - } - } - } + 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 + 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; + 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); + 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; + 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); + batch.drawRect(TILE_COLOR, tx, ty, tileWidth, tileHeight); - if (elevation) { - batch.drawRect(TILE_FRONT_COLOR, tx, ty + tileHeight, tileWidth, elevDiff * tileElevation); + 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 (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 (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 (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 (cliffTop) { + batch.drawRect(OUTLINE_2_COLOR, tx, ty, tileWidth, 1); + } - if (cliffLeft) { - batch.drawRect(OUTLINE_2_COLOR, tx, ty, 1, tileHeight); - } + if (cliffLeft) { + batch.drawRect(OUTLINE_2_COLOR, tx, ty, 1, tileHeight); + } - if (cliffRight) { - batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, 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 (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)); - } + 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 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; + 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); - } - } - } - } + 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) { - 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); - } - } - } + 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(); + 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); - } + 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; - } + 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; - } + 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, base: TileType + tiles: Uint8Array, baseX: number, baseY: number, x: number, y: number, map: IMap, 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 (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; - } - } + 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): number { - const tiles = region.tiles; - const type = tiles[x | (y << 3)] as TileType; - const tileType = tileTypeNumber(type); - let baseTileIndex = 0; + 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 (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); - } + 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); + 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]; - } + 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 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, + 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(); let tileHeightMapsInitialized = false; function valueToHeight(value: number, bottom: number, top: number) { - return bottom + ((value / 255) * (top - bottom)); + return bottom + ((value / 255) * (top - bottom)); } export function initializeTileHeightmaps() { - if (tileHeightMapsInitialized) - return; + if (tileHeightMapsInitialized) + return; - function createTileHeightMaps(sprite: Sprite, tileType: TileTypeNumber, bottom: number, top: number) { - const sheetData = sprites.normalSpriteSheet.data!; - const tiles: number[][] = []; + 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 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)); - } - } + 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); - } - } + tiles.push(tile); + } + } - tileHeightMapsInitialized = true; + tileHeightMapsInitialized = true; - const counts = new Uint8Array(100); + const counts = new Uint8Array(100); - for (let i = 0; i < tileIndices.length; i++) { - const index = tileIndices[i]; + 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]++; - } - } - } + 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); + 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; + const tileType = (tileIndex & 0xff00) >> 8; - if (tileType === TileTypeNumber.Water) { - const heightMaps = tileHeightMaps.get(tileIndex); + 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; - } - } + 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; + return false; } export function getTileHeight( - tileType: TileType, tileIndex: number, x: number, y: number, gameTime: number, mapType: MapType + tileType: TileType, tileIndex: number, x: number, y: number, gameTime: number, mapType: MapType ) { - const typeNumber = (tileIndex & 0xff00) >> 8; + 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 ( + 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]; - } + 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; + return 0; } diff --git a/src/ts/client/timing.ts b/src/ts/client/timing.ts index 9d45e65..2f92b1f 100644 --- a/src/ts/client/timing.ts +++ b/src/ts/client/timing.ts @@ -1,15 +1,15 @@ interface TimingEntry { - time: number; - name?: string; + time: number; + name?: string; } interface TimingResult { - name: string; - count: number; - selfTime: number; - totalTime: number; - selfPercent: number; - totalPercent: number; + name: string; + count: number; + selfTime: number; + totalTime: number; + selfPercent: number; + totalPercent: number; } const ENABLED = false; @@ -19,89 +19,89 @@ const entries: TimingEntry[] = []; let entriesCount = 0; if (TIMING && ENABLED) { - for (let i = 0; i < ENTRIES_LIMIT; i++) { - entries.push({ time: 0, name: undefined }); - } + 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`); - } - } + 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`); - } - } + 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; - } + if (TIMING && ENABLED) { + entriesCount = 0; + } } export function timingCollate(): TimingResult[] { - if (TIMING && ENABLED && entriesCount > 0) { - interface Entry extends TimingEntry { - excludedTime: number; - } + if (TIMING && ENABLED && entriesCount > 0) { + interface Entry extends TimingEntry { + excludedTime: number; + } - const listings: TimingResult[] = []; - const startStack: Entry[] = []; + const listings: TimingResult[] = []; + const startStack: Entry[] = []; - for (let i = 0; i < entriesCount; i++) { - const entry = entries[i]; + 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 (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); - } + 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; + listing.count++; + listing.selfTime += (time - start.excludedTime); + listing.totalTime += time; - if (startStack.length) { - startStack[startStack.length - 1].excludedTime += 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; + 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; - } + 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 listings.sort((a, b) => b.selfTime - a.selfTime); + } - return []; + return []; } diff --git a/src/ts/client/webgl.ts b/src/ts/client/webgl.ts index b6300b1..be20e95 100644 --- a/src/ts/client/webgl.ts +++ b/src/ts/client/webgl.ts @@ -11,17 +11,17 @@ 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; + 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; @@ -29,108 +29,108 @@ const paletteShaderSource = paletteLayersShader; const lightShaderSource = lightShader; function createIndices(capacity: number) { - const numIndices = (capacity * 6) | 0; - const indices = new Uint16Array(numIndices); + 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; - } + 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; + return indices; } export function initWebGL(canvas: HTMLCanvasElement, paletteManager: PaletteManager, camera: Camera): WebGL { - const gl = getWebGLContext(canvas); - return initWebGLResources(gl, paletteManager, camera); + 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 }; + 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; - } + 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); + createTexturesForSpriteSheets(gl, sprites.spriteSheets); + const palettes = createCommonPalettes(paletteManager); - const paletteShader = createShader(gl, paletteShaderSource); - const spriteShader = createShader(gl, spriteShaderSource); - const lightShader = createShader(gl, lightShaderSource); + 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(); + 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`); - } + if (!vertexBuffer) { + throw new Error(`Failed to allocate vertex buffer`); + } - const indexBuffer = gl.createBuffer(); + const indexBuffer = gl.createBuffer(); - if (!indexBuffer) { - throw new Error(`Failed to allocate index buffer`); - } + 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); + 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(); + const vertexBuffer2 = gl.createBuffer(); - if (!vertexBuffer2) { - throw new Error(`Failed to allocate vertex buffer (2)`); - } + 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); + 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; + 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); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); - paletteManager.init(gl); + paletteManager.init(gl); - const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); + const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); - if (debugInfo) { - renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); - } + if (debugInfo) { + renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); + } - return { - gl, paletteShader, spriteShader, lightShader, spriteBatch, paletteBatch, - frameBuffer, frameBufferSheet, palettes, failedFBO, renderer, - }; + return { + gl, paletteShader, spriteShader, lightShader, spriteBatch, paletteBatch, + frameBuffer, frameBufferSheet, palettes, failedFBO, renderer, + }; } export function disposeWebGL(webgl: WebGL) { - const { gl } = 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(); + 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(); } diff --git a/src/ts/common/accountUtils.ts b/src/ts/common/accountUtils.ts index da318da..8db6923 100644 --- a/src/ts/common/accountUtils.ts +++ b/src/ts/common/accountUtils.ts @@ -1,80 +1,80 @@ import { - BASE_CHARACTER_LIMIT, ADDITIONAL_CHARACTERS_SUPPORTER1, ADDITIONAL_CHARACTERS_SUPPORTER2, - ADDITIONAL_CHARACTERS_SUPPORTER3, ADDITIONAL_CHARACTERS_PAST_SUPPORTER + 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; + roles?: string[] | undefined; } export interface AccountSupporter extends AccountRoles { - supporter?: number | undefined; - supporterInvited?: boolean; - flags?: AccountDataFlags; + 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); + return !!(account && account.roles && account.roles.indexOf(role) !== -1); } export function isAdmin(account: AccountRoles): boolean { - return hasRole(account, 'admin') || hasRole(account, 'superadmin'); + return hasRole(account, 'admin') || hasRole(account, 'superadmin'); } export function isMod(account: AccountRoles): boolean { - return hasRole(account, 'mod') || isAdmin(account); + return hasRole(account, 'mod') || isAdmin(account); } export function isDev(account: AccountRoles): boolean { - return hasRole(account, 'dev'); + return hasRole(account, 'dev'); } export function meetsRequirement(account: AccountSupporter, require: string | undefined): boolean { - return !require || hasRole(account, require) || meetsSupporterRequirement(account, require); + 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); + 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; - } + 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; - } - } + 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; - } - } + 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; + } + } } diff --git a/src/ts/common/adminInterfaces.ts b/src/ts/common/adminInterfaces.ts index ce12b7e..bd631a6 100644 --- a/src/ts/common/adminInterfaces.ts +++ b/src/ts/common/adminInterfaces.ts @@ -1,5 +1,5 @@ import { - PonyInfo, AccountSettings, AccountData, PonyObject, AccountCounters, ServerFeatureFlags, Dict, Subscription + PonyInfo, AccountSettings, AccountData, PonyObject, AccountCounters, ServerFeatureFlags, Dict, Subscription } from './interfaces'; export const ITEM_LIMIT = 1000; @@ -7,480 +7,480 @@ 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', + '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, + No, + Yes, + Very, } export const enum CharacterFlags { - None = 0, - BadCM = 1, - HideSupport = 4, - RespawnAtSpawn = 8, - ForbiddenName = 16, + None = 0, + BadCM = 1, + HideSupport = 4, + RespawnAtSpawn = 8, + ForbiddenName = 16, } // NOTE: also update createLoginServerStatus() (internal-login.ts) export interface GeneralSettings { - isPageOffline?: boolean; + isPageOffline?: boolean; - canCreateAccounts?: boolean; - blockWebView?: boolean; - reportPotentialDuplicates?: boolean; - autoMergeDuplicates?: 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; + 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' }, + { 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; + 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; + 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; + servers: Dict; } 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' }, + { 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; + reloadSettings(): Promise; } export interface InternalApi extends InternalCommonApi { - state(): Promise; - stats(): Promise; - statsTable(stats: Stats): Promise; - action(action: string, accountId: string): Promise; - join(accountId: string, ponyId: string): Promise; - kick(accountId: string | undefined, characterId: string | undefined): Promise; - kickAll(): Promise; - accountChanged(accountId: string): Promise; - accountMerged(accountId: string, mergeId: string): Promise; - accountStatus(accountId: string): Promise; - accountAround(accountId: string): Promise; - accountHidden(accountId: string): Promise; - notifyUpdate(): Promise; - cancelUpdate(): Promise; - shutdownServer(value: boolean): Promise; - getTimings(): Promise; - teleportTo(adminAccountId: string, targetAccountId: string): Promise; + state(): Promise; + stats(): Promise; + statsTable(stats: Stats): Promise; + action(action: string, accountId: string): Promise; + join(accountId: string, ponyId: string): Promise; + kick(accountId: string | undefined, characterId: string | undefined): Promise; + kickAll(): Promise; + accountChanged(accountId: string): Promise; + accountMerged(accountId: string, mergeId: string): Promise; + accountStatus(accountId: string): Promise; + accountAround(accountId: string): Promise; + accountHidden(accountId: string): Promise; + notifyUpdate(): Promise; + cancelUpdate(): Promise; + shutdownServer(value: boolean): Promise; + getTimings(): Promise; + teleportTo(adminAccountId: string, targetAccountId: string): Promise; } export interface InternalLoginApi extends InternalCommonApi { - state(): Promise; - loginServerStats(): Promise; - updateLiveSettings(update: Partial): Promise; - mergeAccounts(id: string, withId: string, reason: string, allowAdmin: boolean, creatingDuplicates: boolean): Promise; + state(): Promise; + loginServerStats(): Promise; + updateLiveSettings(update: Partial): Promise; + mergeAccounts(id: string, withId: string, reason: string, allowAdmin: boolean, creatingDuplicates: boolean): Promise; } 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; + 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; + 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; + dead: boolean; + maps: number; + online: number; + onMain: number; + queued: number; + shutdown: boolean; + settings: GameServerSettings; } export interface InternalServerState { - id: string; - api: InternalCommonApi; + id: string; + api: InternalCommonApi; } export interface InternalLoginServerState extends InternalServerState { - api: InternalLoginApi; - state: LoginServerStatus; + api: InternalLoginApi; + state: LoginServerStatus; } export interface InternalGameServerState extends InternalServerState { - api: InternalApi; - state: GameServerState; + api: InternalApi; + state: GameServerState; } export interface ServerStatus { - diskSpace: string; - memoryUsage: string; - certificateExpiration: string; - lastPatreonUpdate: string; + diskSpace: string; + memoryUsage: string; + certificateExpiration: string; + lastPatreonUpdate: string; } export interface LoginServerStatus extends GeneralSettings { - updating: boolean; - dead: boolean; + updating: boolean; + dead: boolean; } export interface AdminState { - status: ServerStatus; - loginServers: LoginServerStatus[]; - gameServers: GameServerState[]; + status: ServerStatus; + loginServers: LoginServerStatus[]; + gameServers: GameServerState[]; } export interface MemoryStatus { - total: number; - used: number; - free: number; + total: number; + used: number; + free: number; } export interface OriginInfoBase { - ip: string; - country: string; - last?: Date; + ip: string; + country: string; + last?: Date; } export interface MergeItemData { - id: string; - name: string; + id: string; + name: string; } export interface MergeHideData { - id: string; - name: string; - date: string; + 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[]; + 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; + account: MergeAccountData; + merge: MergeAccountData; } export interface MergeInfo { - _id?: string; - id: string; - name: string; - //code: number; - date: Date; - reason?: string; - data?: MergeData; - split?: boolean; + _id?: string; + id: string; + name: string; + //code: number; + date: Date; + reason?: string; + data?: MergeData; + split?: boolean; } export interface LogEntry { - message: string; - date: Date; + message: string; + date: Date; } export interface AccountDetails { - merges: MergeInfo[]; - supporterLog: LogEntry[]; - banLog: LogEntry[]; - invitesReceived: SupporterInvite[]; - invitesSent: SupporterInvite[]; - state: AccountState; + merges: MergeInfo[]; + supporterLog: LogEntry[]; + banLog: LogEntry[]; + invitesReceived: SupporterInvite[]; + invitesSent: SupporterInvite[]; + state: AccountState; } export interface AuthDetails { - id: string; - lastUsed: string | undefined; + id: string; + lastUsed: string | undefined; } export interface BannedMuted { - mute?: number; - shadow?: number; - ban?: number; + mute?: number; + shadow?: number; + ban?: number; } export interface Document extends Timestamps { - _id: string; - deleted?: boolean; + _id: string; + deleted?: boolean; } // bases export interface TimestampsBase { - createdAt?: Date; - updatedAt: Date; + createdAt?: Date; + updatedAt: Date; } export interface ChatMessageBase { - createdAt: Date; - message: string; + 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' }, + { 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, + 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' }, + { 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, + 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, + 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' }, + { 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; + gifts?: number; + candies?: number; + clovers?: number; + toys?: number; + eggs?: number; } export interface AccountAlert { - expires: Date; - message: string; + expires: Date; + message: string; } export interface AccountBase 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; + 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 extends TimestampsBase { - account?: ID; - openId?: string; - provider: string; - name: string; - url: string; - emails?: string[]; - disabled?: boolean; - banned?: boolean; - pledged?: number; - lastUsed?: Date; + 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, + None = 0, + Right = 1, + Extra = 2, } export interface CharacterState { - x: number; - y: number; - map?: string; - toy?: number; - flags?: CharacterStateFlags; - hold?: string; + x: number; + y: number; + map?: string; + toy?: number; + flags?: CharacterStateFlags; + hold?: string; } export interface CharacterBase 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; }; + 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 extends TimestampsBase { - account?: ID; - pony?: ID; - type: string; - server: string; - message: string; - desc: string; - origin?: OriginInfoBase; - count: number; + account?: ID; + pony?: ID; + type: string; + server: string; + message: string; + desc: string; + origin?: OriginInfoBase; + count: number; } export interface SupporterInviteBase extends TimestampsBase { - source: ID; - target: ID; - name: string; - info: string; - active: boolean; + source: ID; + target: ID; + name: string; + info: string; + active: boolean; } export interface FriendRequestBase { - source: ID; - target: ID; + source: ID; + target: ID; } export interface HideRequestBase { - source: ID; - target: ID; - name: string; - date: Date; + source: ID; + target: ID; + name: string; + date: Date; } export const eventFields: (keyof Event)[] = [ - '_id', 'updatedAt', 'createdAt', 'type', 'server', 'message', 'desc', 'count', 'origin', 'account', 'pony' + '_id', 'updatedAt', 'createdAt', 'type', 'server', 'message', 'desc', 'count', 'origin', 'account', 'pony' ]; // models @@ -492,89 +492,89 @@ 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; + 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; + count: number; + name: boolean; + emails: boolean; + browserId: boolean; + generatedAt: number; + perma: boolean; } export type ListListener = (items: T[]) => void; export interface IObservableList { - 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): Subscription; + 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): Subscription; } export interface PonyIdDateName { - id: string; - date: number; - name: string; + id: string; + date: number; + name: string; } export interface Account extends AccountBase, Document { - nameLower?: string; - auths?: Auth[]; - originsRefs?: OriginRef[]; - ignoredByLimit?: number; - ignoresLimit?: number; - ignoresCount?: number; - duplicatesLimit?: number; - totalPledged?: number; + nameLower?: string; + auths?: Auth[]; + originsRefs?: OriginRef[]; + ignoredByLimit?: number; + ignoresLimit?: number; + ignoresCount?: number; + duplicatesLimit?: number; + totalPledged?: number; - ponies?: Character[]; - invitesReceived?: SupporterInvite[]; - invitesSent?: SupporterInvite[]; + ponies?: Character[]; + invitesReceived?: SupporterInvite[]; + invitesSent?: SupporterInvite[]; - authsList?: IObservableList; - poniesList?: IObservableList; - originsList?: IObservableList; + authsList?: IObservableList; + poniesList?: IObservableList; + originsList?: IObservableList; } export interface Auth extends AuthBase, Document { } export interface Character extends CharacterBase, Document { - ponyInfo?: PonyInfo; - deleted?: boolean; + ponyInfo?: PonyInfo; + deleted?: boolean; } export interface Origin extends OriginBase, Document { - accounts?: Account[]; - accountsCount?: number; + accounts?: Account[]; + accountsCount?: number; } export interface OriginRef { - origin: Origin; - last: Date; + origin: Origin; + last: Date; } export interface Event extends EventBase, Document { - deleted?: boolean; - descHTML?: any; + deleted?: boolean; + descHTML?: any; } export interface ChatEvent { - event: Event; - account: Account | undefined; + event: Event; + account: Account | undefined; } export interface SupporterInvite extends SupporterInviteBase, Document { @@ -589,335 +589,335 @@ export interface UpdateOrigin extends OriginInfo, BannedMuted { } export interface AccountUpdate extends BannedMuted { - age?: number; - name?: string; - note?: string; - flags?: number; - supporter?: number; + age?: number; + name?: string; + note?: string; + flags?: number; + supporter?: number; } export interface BaseValues { - updatedAt?: string; - createdAt?: string; - lastVisit?: string; + updatedAt?: string; + createdAt?: string; + lastVisit?: string; } export interface LiveResponse { - updates: any[][]; - deletes: string[]; - base: BaseValues; - more: boolean; + updates: any[][]; + deletes: string[]; + base: BaseValues; + more: boolean; } export interface RequestStats { - path: string; - count: number; - average: string; - total: string; - order: string; - totalCount: number; + path: string; + count: number; + average: string; + total: string; + order: string; + totalCount: number; } export interface UserCountStats { - count: number; - date: string; + count: number; + date: string; } export interface LoginStats { - requests: RequestStats[]; - userCounts: UserCountStats[]; + requests: RequestStats[]; + userCounts: UserCountStats[]; } export interface ItemCounts { - accounts: number; - characters: number; - auths: number; - origins: number; + accounts: number; + characters: number; + auths: number; + origins: number; } export interface FindPonyQuery { - search?: string; - orderBy?: string; + search?: string; + orderBy?: string; } export interface AuthUpdate { - disabled?: boolean; - banned?: boolean; - pledged?: number; + disabled?: boolean; + banned?: boolean; + pledged?: number; } export interface AccountPonies { - account: string; - count: number; - ponies: any[][]; + account: string; + count: number; + ponies: any[][]; } export interface AccountPoniesResponse { - base: BaseValues; - accounts: AccountPonies[]; + base: BaseValues; + accounts: AccountPonies[]; } export interface PoniesResponse { - base: BaseValues; - ponies: any[][]; + base: BaseValues; + ponies: any[][]; } export interface PonyCreator { - _id: string; - name: string; - creator: string; + _id: string; + name: string; + creator: string; } export interface AccountOrigins { - accountId: string; - ips: string[]; + accountId: string; + ips: string[]; } export interface ServerStats { - actions: { - id: number; - name: string; - type: string; - countBin: number; - countStr: number; - average: string; - total: string; - }[]; + actions: { + id: number; + name: string; + type: string; + countBin: number; + countStr: number; + average: string; + total: string; + }[]; } export interface DuplicateInfoEntry { - account: string; - userAgent: string; - ponies: string[]; + account: string; + userAgent: string; + ponies: string[]; } export interface AroundEntry { - account: string; - distance: number; - party: boolean; + account: string; + distance: number; + party: boolean; } export const enum Stats { - Country, - Support, - Maps, + Country, + Support, + Maps, } export type StatsTable = string[][]; export interface OriginStats { - uniqueOrigins: number; - duplicateOrigins: number; - singleOrigins: number; - totalOrigins: number; - totalOriginsIP4: number; - totalOriginsIP6: number; - distribution: number[]; + uniqueOrigins: number; + duplicateOrigins: number; + singleOrigins: number; + totalOrigins: number; + totalOriginsIP4: number; + totalOriginsIP6: number; + distribution: number[]; } export interface OtherStats { - totalIgnores: number; - authsWithEmptyAccount: number; - authsWithMissingAccount: number; + totalIgnores: number; + authsWithEmptyAccount: number; + authsWithMissingAccount: number; } export interface Around { - account: Account; - distance: number; - party: boolean; + 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; + 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; + account: string; } export interface Duplicate extends DuplicateBase { - account: Account; + account: Account; } export interface FindAccountQuery { - search: string; - showOnly: string; - not: boolean; - page: number; - itemsPerPage: number; - force?: boolean; + search: string; + showOnly: string; + not: boolean; + page: number; + itemsPerPage: number; + force?: boolean; } export interface FindAccountResult { - accounts: string[]; - page: number; - totalItems: number; + accounts: string[]; + page: number; + totalItems: number; } export interface AdminCacheEntry { - query: string; - result: T; - timestamp: Date; + query: string; + result: T; + timestamp: Date; } export interface AdminCache { - findAccounts?: AdminCacheEntry; + findAccounts?: AdminCacheEntry; } export interface ClearOrignsOptions { - old?: boolean; - singles?: boolean; - trim?: boolean; - veryOld?: boolean; - country?: string; + old?: boolean; + singles?: boolean; + trim?: boolean; + veryOld?: boolean; + country?: string; } export interface PatreonReward { - id: string; - title: string; - description: string; + id: string; + title: string; + description: string; } export interface PatreonPledge { - user: string; - reward: string; - total: number; - declinedSince?: string; - account?: string; + user: string; + reward: string; + total: number; + declinedSince?: string; + account?: string; } export interface PatreonData { - rewards: PatreonReward[]; - pledges: PatreonPledge[]; + rewards: PatreonReward[]; + pledges: PatreonPledge[]; } export interface HidingStats { - account: string; - hidden: string[]; - hiddenBy: string[]; - permaHidden: string[]; - permaHiddenBy: string[]; + account: string; + hidden: string[]; + hiddenBy: string[]; + permaHidden: string[]; + permaHiddenBy: string[]; } export const enum TimingEntryType { - Start, - End, + Start, + End, } export interface TimingEntry { - type: TimingEntryType; - time: number; - name?: string; + type: TimingEntryType; + time: number; + name?: string; } export type ModelTypes = - 'accounts' | 'auths' | 'origins' | 'ponies' | 'accountAuths' | 'accountOrigins' | 'accountPonies'; + '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; - getCounts(): Promise; - getState(): Promise; - updateSettings(update: Partial): Promise; - updateGameServerSettings(serverId: string, update: Partial): Promise; - fetchServerStats(serverId: string): Promise; - fetchServerStatsTable(serverId: string, stats: Stats): Promise; - notifyUpdate(serverId: string): Promise; - shutdownServers(serverId: string): Promise; - resetUpdating(serverId: string): Promise; - report(accountId: string): Promise; - action(action: string, accountId: string): Promise; - kick(accountId: string): Promise; - kickAll(serverId: string): Promise; - getChat(search: string, date: string, caseInsensitive: boolean): Promise; - getChatForAccounts(accountIds: string[], date: string): Promise; - getRequestStats(): Promise; - updatePatreon(): Promise; - resetSupporter(accountId: string): Promise; - getLastPatreonData(): Promise; - updatePastSupporters(): Promise; - // live - get(endPoint: 'events', id: string): Promise; - getAll(endPoint: 'events', timestamp?: string): Promise; - assignAccount(endPoint: 'events', id: string, account: string): Promise; - removeItem(endPoint: 'events', id: string): Promise; - // events - removeEvent(id: string): Promise; - // origins - updateOrigin(origin: UpdateOrigin): Promise; - getOriginStats(): Promise; - getOtherStats(): Promise; - clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions): Promise; - clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions): Promise; - // characters - getPony(id: string): Promise; - getPonyInfo(id: string): Promise; - getPoniesCreators(accountId: string): Promise; - getPoniesForAccount(accountId: string): Promise; - getDetailsForAccount(accountId: string): Promise; - findPonies(query: FindPonyQuery, page: number, skipTotalCount: boolean): Promise<{ items: string[]; totalCount: number; }>; - createPony(account: string, name: string, info: string): Promise; - assignPony(ponyId: string, accountId: string): Promise; - removePony(id: string): Promise; - removePoniesAboveLimit(accountId: string): Promise; - removeAllPonies(accountId: string): Promise; - // auths - getAuth(id: string): Promise; - getAuthsForAccount(accountId: string): Promise; - fetchAuthDetails(auths: string[]): Promise; - updateAuth(id: string, update: AuthUpdate): Promise; - assignAuth(authId: string, accountId: string): Promise; - removeAuth(id: string): Promise; - // accounts - getAccount(id: string): Promise; - findAccounts(query: FindAccountQuery): Promise; - createAccount(name: string): Promise; - getAccountsByEmails(emails: string[]): Promise>; - getAccountsByOrigin(ip: string): Promise; - setName(accountId: string, name: string): Promise; - setAge(accountId: string, age: number): Promise; - setRole(accountId: string, role: string, set: boolean): Promise; - updateAccount(accountId: string, update: AccountUpdate, message?: string): Promise; - timeoutAccount(accountId: string, timeout: number): Promise; - updateAccountCounter(accountId: string, name: keyof AccountCounters, value: number): Promise; - removeAllOrigins(accountId: string): Promise; - removeOriginsForAccount(accountId: string, ips: string[]): Promise; - removeOriginsForAccounts(origins: AccountOrigins[]): Promise; - addOriginToAccount(accountId: string, origin: OriginInfo): Promise; - mergeAccounts(accountId: string, withId: string): Promise; - unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData): Promise; - addEmail(accountId: string, email: string): Promise; - removeEmail(accountId: string, email: string): Promise; - removeIgnore(accountId: string, ignore: string): Promise; - addIgnores(accountId: string, ignores: string[]): Promise; - removeFriend(accountId: string, friendId: string): Promise; - addFriend(accountId: string, friendId: string): Promise; - setAccountState(accountId: string, state: AccountState): Promise; - getAccountStatus(accountId: string): Promise; - getAccountAround(accountId: string): Promise; - getAccountHidden(accountId: string): Promise; - getAccountFriends(accountId: string): Promise; - removeAccount(accountId: string): Promise; - setAlert(accountId: string, message: string, expiresIn: number): Promise; - getIgnoresAndIgnoredBy(accountId: string): Promise<{ ignores: string[]; ignoredBy: string[]; }>; - getAllDuplicatesQuickInfo(accountId: string): Promise; - getAllDuplicates(accountId: string): Promise; - getDuplicateEntries(force: boolean): Promise; - clearSessions(accountId: string): Promise; - // other - getTimings(serverId: string): Promise; - teleportTo(accountId: string): Promise; + // subscribing + subscribe(model: ModelTypes, id: string): void; + unsubscribe(model: ModelTypes, id: string): void; + // other + getSignedAccount(): Promise; + getCounts(): Promise; + getState(): Promise; + updateSettings(update: Partial): Promise; + updateGameServerSettings(serverId: string, update: Partial): Promise; + fetchServerStats(serverId: string): Promise; + fetchServerStatsTable(serverId: string, stats: Stats): Promise; + notifyUpdate(serverId: string): Promise; + shutdownServers(serverId: string): Promise; + resetUpdating(serverId: string): Promise; + report(accountId: string): Promise; + action(action: string, accountId: string): Promise; + kick(accountId: string): Promise; + kickAll(serverId: string): Promise; + getChat(search: string, date: string, caseInsensitive: boolean): Promise; + getChatForAccounts(accountIds: string[], date: string): Promise; + getRequestStats(): Promise; + updatePatreon(): Promise; + resetSupporter(accountId: string): Promise; + getLastPatreonData(): Promise; + updatePastSupporters(): Promise; + // live + get(endPoint: 'events', id: string): Promise; + getAll(endPoint: 'events', timestamp?: string): Promise; + assignAccount(endPoint: 'events', id: string, account: string): Promise; + removeItem(endPoint: 'events', id: string): Promise; + // events + removeEvent(id: string): Promise; + // origins + updateOrigin(origin: UpdateOrigin): Promise; + getOriginStats(): Promise; + getOtherStats(): Promise; + clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions): Promise; + clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions): Promise; + // characters + getPony(id: string): Promise; + getPonyInfo(id: string): Promise; + getPoniesCreators(accountId: string): Promise; + getPoniesForAccount(accountId: string): Promise; + getDetailsForAccount(accountId: string): Promise; + findPonies(query: FindPonyQuery, page: number, skipTotalCount: boolean): Promise<{ items: string[]; totalCount: number; }>; + createPony(account: string, name: string, info: string): Promise; + assignPony(ponyId: string, accountId: string): Promise; + removePony(id: string): Promise; + removePoniesAboveLimit(accountId: string): Promise; + removeAllPonies(accountId: string): Promise; + // auths + getAuth(id: string): Promise; + getAuthsForAccount(accountId: string): Promise; + fetchAuthDetails(auths: string[]): Promise; + updateAuth(id: string, update: AuthUpdate): Promise; + assignAuth(authId: string, accountId: string): Promise; + removeAuth(id: string): Promise; + // accounts + getAccount(id: string): Promise; + findAccounts(query: FindAccountQuery): Promise; + createAccount(name: string): Promise; + getAccountsByEmails(emails: string[]): Promise>; + getAccountsByOrigin(ip: string): Promise; + setName(accountId: string, name: string): Promise; + setAge(accountId: string, age: number): Promise; + setRole(accountId: string, role: string, set: boolean): Promise; + updateAccount(accountId: string, update: AccountUpdate, message?: string): Promise; + timeoutAccount(accountId: string, timeout: number): Promise; + updateAccountCounter(accountId: string, name: keyof AccountCounters, value: number): Promise; + removeAllOrigins(accountId: string): Promise; + removeOriginsForAccount(accountId: string, ips: string[]): Promise; + removeOriginsForAccounts(origins: AccountOrigins[]): Promise; + addOriginToAccount(accountId: string, origin: OriginInfo): Promise; + mergeAccounts(accountId: string, withId: string): Promise; + unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData): Promise; + addEmail(accountId: string, email: string): Promise; + removeEmail(accountId: string, email: string): Promise; + removeIgnore(accountId: string, ignore: string): Promise; + addIgnores(accountId: string, ignores: string[]): Promise; + removeFriend(accountId: string, friendId: string): Promise; + addFriend(accountId: string, friendId: string): Promise; + setAccountState(accountId: string, state: AccountState): Promise; + getAccountStatus(accountId: string): Promise; + getAccountAround(accountId: string): Promise; + getAccountHidden(accountId: string): Promise; + getAccountFriends(accountId: string): Promise; + removeAccount(accountId: string): Promise; + setAlert(accountId: string, message: string, expiresIn: number): Promise; + getIgnoresAndIgnoredBy(accountId: string): Promise<{ ignores: string[]; ignoredBy: string[]; }>; + getAllDuplicatesQuickInfo(accountId: string): Promise; + getAllDuplicates(accountId: string): Promise; + getDuplicateEntries(force: boolean): Promise; + clearSessions(accountId: string): Promise; + // other + getTimings(serverId: string): Promise; + teleportTo(accountId: string): Promise; } diff --git a/src/ts/common/adminUtils.ts b/src/ts/common/adminUtils.ts index be8c3e4..170f032 100644 --- a/src/ts/common/adminUtils.ts +++ b/src/ts/common/adminUtils.ts @@ -3,8 +3,8 @@ 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 + Account, OriginInfo, Document, OriginRef, SupporterFlags, BannedMuted, AccountBase, LogEntry, + DuplicateResult, DuplicateBase, Duplicate, Auth, MergeInfo } from './adminInterfaces'; import { hasRole } from './accountUtils'; import { filterBadWordsPartial } from './swears'; @@ -12,559 +12,559 @@ import { faPlusCircle, faClock, faMinusCircle, faCaretSquareUp, faCaretSquareDow import { element, textNode } from '../client/htmlUtils'; interface UpdatedAt { - updatedAt: Date; + 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); + compareDates(b.last, a.last) || compareOrigins(a.origin, b.origin); export const compareByName = (a: T, b: T) => (a.name || '').localeCompare(b.name || ''); export const getId = (item: Document) => item._id; export const tagBad = (s: string) => `${s}`; export function compareAccounts(a: Account, b: Account) { - return compareDates(a.createdAt, b.createdAt); + 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; + 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); - } + 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; + text = text || ''; + text = filterBadWordsPartial(text, tagBad); + return text; } export function getAge(birthdate: Date) { - return moment().diff(birthdate, 'years'); + return moment().diff(birthdate, 'years'); } // chat & events function enc(text?: string): string { - return escape(text || ''); + return escape(text || ''); } function encWithHighlight(text?: string): string { - return highlightWords(enc(text || '')); + return highlightWords(enc(text || '')); } export function formatEventDesc(text: string): string { - return encWithHighlight(text).replace(/\[([a-z0-f]{24})\]/g, `[$1]`); + return encWithHighlight(text).replace(/\[([a-z0-f]{24})\]/g, `[$1]`); } 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'; - } + 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; + const text = element.textContent; - if (text) { - const replaced = encWithHighlight(text); + if (text) { + const replaced = encWithHighlight(text); - if (text !== replaced) { - element.innerHTML = replaced; - } - } + 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 + // 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); + /* 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' : ''; + 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))]); - } + 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; + 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; - } + 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'))}`); + 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 })); - }; + (window as any).goToAccount = (accountId: string) => { + window.dispatchEvent(new CustomEvent('go-to-account', { detail: accountId })); + }; } export function formatChat(chat: string): HTMLElement[] { - return (chat || '') - .trim() - .split(/\r?\n/g) - .reverse() - .map(formatChatLine); + return (chat || '') + .trim() + .split(/\r?\n/g) + .reverse() + .map(formatChatLine); } export interface ChatDate { - value: string; - label: string; + value: string; + label: string; } export function createChatDate(date: moment.Moment): ChatDate { - return { - value: date.toISOString(), - label: date.format('MMMM Do YYYY'), - }; + 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); + 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)); - } + if (search) { + items = items.filter(createFilter(search)); + } - const filter = createFilter2(showOnly); + const filter = createFilter2(showOnly); - if (filter) { - if (not) { - items = items.filter(i => !filter(i)); - } else { - items = items.filter(filter); - } - } + if (filter) { + if (not) { + items = items.filter(i => !filter(i)); + } else { + items = items.filter(filter); + } + } - return items; + return items; } export function createFilter(search: string): (account: Account) => boolean { - const regex = new RegExp(escapeRegExp(search), 'i'); + const regex = new RegExp(escapeRegExp(search), 'i'); - function test(value: string): boolean { - return !!value && regex.test(value); - } + 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 testAuth(auth: Auth) { + return test(auth.name) || auth.provider === search || auth.url === search; + } - function testMerge(merge: MergeInfo) { - return merge.id === 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; + 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; - } + return false; + } - function prefixWith(prefix: string, action: (phrase: string) => (account: Account) => boolean) { - return startsWith(search, prefix) ? action(search.substr(prefix.length)) : undefined; - } + 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 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)); - } + 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; + 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; + 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); + return isBanned(account) || isMuted(account) || isShadowed(account); } export function createPotentialDuplicatesFilter(getAccountsByBrowserId: (id: string) => Account[] | undefined): (account: Account) => boolean { - return i => { - const name = i.nameLower; + return i => { + const name = i.nameLower; - if (name === 'anonymous' || !i.lastBrowserId) - return false; + if (name === 'anonymous' || !i.lastBrowserId) + return false; - const accounts = getAccountsByBrowserId(i.lastBrowserId); + const accounts = getAccountsByBrowserId(i.lastBrowserId); - if (accounts !== undefined && accounts.length > 1) { - for (const a of accounts) { - if (a !== i && a.nameLower === name) { - return true; - } - } - } + if (accounts !== undefined && accounts.length > 1) { + for (const a of accounts) { + if (a !== i && a.nameLower === name) { + return true; + } + } + } - return false; - }; + return false; + }; } export function createFilter2(showOnly: string): ((account: Account) => boolean) | undefined { - const now = Date.now(); + 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; - } + 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; + 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 []; - } + 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(); + 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(); + 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); - } + 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 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 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 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 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 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()); + 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), - }; + 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 }; + return { ...createDuplicate(account, base), account: account._id }; } export function pushOrdered(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; - } - } + for (let i = 0; i < items.length; i++) { + if (compare(items[i], item) >= 0) { + items.splice(i, 0, item); + return; + } + } - items.push(item); + items.push(item); } export function duplicatesCollector(duplicates: string[]) { - const set = new Set(); + const set = new Set(); - return (item: string) => { - if (set.has(item)) { - duplicates.push(item); - } else { - set.add(item); - } - }; + return (item: string) => { + if (set.has(item)) { + duplicates.push(item); + } else { + set.add(item); + } + }; } export function patreonSupporterLevel(account: AccountBase) { - return account.patreon! & 0xf; + return account.patreon! & 0xf; } export function supporterLevel(account: AccountBase) { - 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); + 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) { - const flags = account.supporter!; - return (hasFlag(flags, SupporterFlags.PastSupporter) || hasFlag(flags, SupporterFlags.ForcePastSupporter)) && - !hasFlag(flags, SupporterFlags.IgnorePastSupporter); + 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', + mute: 'Muted', + shadow: 'Shadowed', + ban: 'Banned', }; export function banMessage(field: string, value: number) { - const action = fieldToAction[field] || 'Did'; + 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()})`; - } + 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()); + return !!value && (value === -1 || value > Date.now()); } export function isPerma(value: number | undefined): boolean { - return value === -1; + return value === -1; } export function isTemporarilyActive(value: number | undefined): boolean { - return !!value && value > Date.now(); + return !!value && value > Date.now(); } export function isMuted(account: BannedMuted): boolean { - return isActive(account.mute); + return isActive(account.mute); } export function isShadowed(account: BannedMuted): boolean { - return isActive(account.shadow); + return isActive(account.shadow); } export function isBanned(account: BannedMuted): boolean { - return isActive(account.ban); + return isActive(account.ban); } export function isPermaShadowed(account: BannedMuted): boolean { - return isPerma(account.shadow); + return isPerma(account.shadow); } export function isPermaBanned(account: BannedMuted): boolean { - return isPerma(account.ban); + return isPerma(account.ban); } export function isTemporarilyBanned(account: BannedMuted): boolean { - return isTemporarilyActive(account.ban); + return isTemporarilyActive(account.ban); } export interface SupporterChange { - message: string; - date: Date; - icon: any; - class: string; + 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'), - })); + 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]; + 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.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'; - } - } - } + 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; + return changes; } export function getIdsFromNote(note: string | undefined) { - return note ? uniq(note.match(/[0-9a-f]{24}/g)) : []; + return note ? uniq(note.match(/[0-9a-f]{24}/g)) : []; } export function addToMap(map: Map, key: string, item: T) { - const items = map.get(key); + const items = map.get(key); - if (items) { - items.push(item); - } else { - map.set(key, [item]); - } + if (items) { + items.push(item); + } else { + map.set(key, [item]); + } } export function removeFromMap(map: Map, key: string, item: T) { - const items = map.get(key); + const items = map.get(key); - if (items) { - removeItem(items, item); + if (items) { + removeItem(items, item); - if (items.length === 0) { - map.delete(key); - } - } + 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)); + 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(); + const idsMap = new Map(); - return (id: string) => { - const result = idsMap.get(id); + return (id: string) => { + const result = idsMap.get(id); - if (result) { - return result; - } else { - idsMap.set(id, id); - return 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)}`; + 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)}`; } diff --git a/src/ts/common/animationPlayer.ts b/src/ts/common/animationPlayer.ts index a87c7c1..f55c367 100644 --- a/src/ts/common/animationPlayer.ts +++ b/src/ts/common/animationPlayer.ts @@ -5,131 +5,131 @@ import { includes } from './utils'; import { WHITE } from './colors'; const enum AnimationPhase { - Starting, - Playing, - Ending, + Starting, + Playing, + Ending, } export interface SpriteAnimation { - loop: boolean; - start: number; - middle: number; - end: number; - fps: number; - palette: Uint32Array; - frames: Sprite[]; - flipFrames?: Sprite[]; + 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; + 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, - }; + return { + nextAnimation: undefined, + currentAnimation: undefined, + time: 0, + frame: 0, + phase: AnimationPhase.Starting, + dirty: true, + palette, + }; } export function isAnimationPlaying(player: AnimationPlayer) { - return player.currentAnimation !== undefined; + return player.currentAnimation !== undefined; } export function playOneOfAnimations(player: AnimationPlayer, animations: SpriteAnimation[]) { - if (player.phase === AnimationPhase.Ending || !includes(animations, player.currentAnimation)) { - playAnimation(player, sample(animations)); - } + 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; - } + 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; + if (player.currentAnimation !== undefined) { + player.time += delta; + const { start, middle, end, fps, loop } = player.currentAnimation; - let extraFrame = Math.floor(player.time * fps); + 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.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.Playing) { + extraFrame = start + ((extraFrame - start) % middle); + } - if (player.phase === AnimationPhase.Ending && extraFrame > (start + middle + end)) { - player.currentAnimation = undefined; - player.dirty = true; + 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.nextAnimation !== undefined) { + const nextAnimation = player.nextAnimation; + player.nextAnimation = undefined; + playAnimation(player, nextAnimation); + } + } - if (player.frame !== extraFrame) { - player.frame = extraFrame; - player.dirty = true; - } - } + 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 + batch: PaletteSpriteBatch, player: AnimationPlayer, x: number, y: number, color = WHITE, flip = false, maxY = 0 ) { - const animation = player.currentAnimation; + const animation = player.currentAnimation; - if (animation !== undefined) { - const frames = (flip && animation.flipFrames) ? animation.flipFrames : animation.frames; + if (animation !== undefined) { + const frames = (flip && animation.flipFrames) ? animation.flipFrames : animation.frames; - if (player.frame < frames.length) { - const frame = frames[player.frame]; + if (player.frame < frames.length) { + const frame = frames[player.frame]; - if (DEVELOPMENT && !frame) { - throw new Error('Undefined frame in sprite animation'); - } + if (DEVELOPMENT && !frame) { + throw new Error('Undefined frame in sprite animation'); + } - if (!frame) // TEMP - return; + 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); - } - } - } + if (maxY === 0) { + batch.drawSprite(frame, color, player.palette, x, y); + } else { + drawSpriteCropped(batch, frame, color, player.palette, x, y, maxY); + } + } + } } diff --git a/src/ts/common/animator.ts b/src/ts/common/animator.ts index c5580f7..13f778d 100644 --- a/src/ts/common/animator.ts +++ b/src/ts/common/animator.ts @@ -1,183 +1,183 @@ export interface Animation { - fps: number; - loop: boolean; - frames: any[]; + fps: number; + loop: boolean; + frames: any[]; } export interface AnimatorTransition { - state: AnimatorState; - exitAfter?: number; - enterTime?: number; - keepTime?: boolean; - onlyDirectTo?: AnimatorState; + state: AnimatorState; + exitAfter?: number; + enterTime?: number; + keepTime?: boolean; + onlyDirectTo?: AnimatorState; } export interface AnimatorState { - name: string; - animation: T; - variants: { [key: string]: T; }; - from: AnimatorTransition[]; + name: string; + animation: T; + variants: { [key: string]: T; }; + from: AnimatorTransition[]; } export function animatorState( - name: string, animation: T, variants: { [key: string]: T; } = {} + name: string, animation: T, variants: { [key: string]: T; } = {} ): AnimatorState { - return { name, animation, variants, from: [] }; + return { name, animation, variants, from: [] }; } export function animatorTransition( - from: AnimatorState, to: AnimatorState, options: Partial> = {} + from: AnimatorState, to: AnimatorState, options: Partial> = {} ) { - to.from.push({ state: from, ...options }); + to.from.push({ state: from, ...options }); } export const anyState = animatorState('any', { fps: 1, loop: false, frames: [] }); export interface Animator { - state: AnimatorState | undefined; - target: AnimatorState | undefined; - next: AnimatorTransition | undefined; - time: number; - variant: string; + state: AnimatorState | undefined; + target: AnimatorState | undefined; + next: AnimatorTransition | undefined; + time: number; + variant: string; } export function createAnimator(): Animator { - return { - time: 0, - variant: '', - state: undefined, - target: undefined, - next: undefined, - }; + return { + time: 0, + variant: '', + state: undefined, + target: undefined, + next: undefined, + }; } export function getAnimation(animator: Animator) { - return animator.state && getAnimationForState(animator.state, animator.variant); + return animator.state && getAnimationForState(animator.state, animator.variant); } export function getAnimationFrame(animator: Animator) { - const animation = getAnimation(animator); - return animation ? Math.floor(animator.time * animation.fps) % animation.frames.length : 0; + const animation = getAnimation(animator); + return animation ? Math.floor(animator.time * animation.fps) % animation.frames.length : 0; } export function resetAnimatorState(animator: Animator) { - animator.state = undefined; - animator.target = undefined; - animator.next = undefined; + animator.state = undefined; + animator.target = undefined; + animator.next = undefined; } export function setAnimatorState(animator: Animator, state: AnimatorState) { - if (animator.target !== state) { - if (animator.state !== state) { - if (animator.state === undefined) { - animator.state = state; - } else { - animator.target = state; - } - } else { - animator.target = undefined; - } + 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; - } + animator.next = undefined; + } } export function updateAnimator(animator: Animator, delta: number) { - const time = animator.time; - animator.time += delta; + 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; + 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; + let animationEnded = frameBefore !== frameAfter; + let switched = false; - do { - switched = false; - const transition = animator.next = animator.next || findTransition(animator.state, animator.target); + 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 (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; - } + if (frameTimeAfter >= exitAfter || animationEnded) { + if (!transition.keepTime) { + animator.time = transition.enterTime || 0; + } else { + animator.time = animator.time % animationLength; + } - setCurrentState(animator, transition.state); + setCurrentState(animator, transition.state); - switched = true; - animationEnded = false; - } - } - } while (switched && animator.target); - } + switched = true; + animationEnded = false; + } + } + } while (switched && animator.target); + } } function setCurrentState(animator: Animator, state: AnimatorState) { - animator.next = undefined; - animator.state = state; + animator.next = undefined; + animator.state = state; - if (state === animator.target) { - animator.target = undefined; - } + if (state === animator.target) { + animator.target = undefined; + } } function getAnimationForState(state: AnimatorState, variant: string) { - return state.variants[variant] || state.animation; + return state.variants[variant] || state.animation; } function findTransition( - current: AnimatorState, target: AnimatorState + current: AnimatorState, target: AnimatorState ): AnimatorTransition | undefined { - return findTransMinMax(current, target, 0, 1) - || findTrans(anyState, target, target, 0, 0, []) - || findTransMinMax(current, target, 2, 10); + return findTransMinMax(current, target, 0, 1) + || findTrans(anyState, target, target, 0, 0, []) + || findTransMinMax(current, target, 2, 10); } function findTransMinMax( - current: AnimatorState, target: AnimatorState, min: number, max: number + current: AnimatorState, target: AnimatorState, min: number, max: number ): AnimatorTransition | undefined { - for (let i = min; i <= max; i++) { - const trans = findTrans(current, target, target, 0, i, [current]); + for (let i = min; i <= max; i++) { + const trans = findTrans(current, target, target, 0, i, [current]); - if (trans !== undefined) { - return trans; - } - } + if (trans !== undefined) { + return trans; + } + } - return undefined; + return undefined; } function findTrans( - current: AnimatorState, target: AnimatorState, finalTarget: AnimatorState, - depth: number, maxDepth: number, done: AnimatorState[] + current: AnimatorState, target: AnimatorState, finalTarget: AnimatorState, + depth: number, maxDepth: number, done: AnimatorState[] ): AnimatorTransition | undefined { - if (done.indexOf(target) === -1) { - done.push(target); + 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 }; - } - } + 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 (depth < maxDepth) { + for (const from of target.from) { + const trans = findTrans(current, from.state, finalTarget, depth + 1, maxDepth, done); - if (trans !== undefined) { - return trans; - } - } - } - } + if (trans !== undefined) { + return trans; + } + } + } + } - return undefined; + return undefined; } diff --git a/src/ts/common/binaryUtils.ts b/src/ts/common/binaryUtils.ts index ef5ff94..4513986 100644 --- a/src/ts/common/binaryUtils.ts +++ b/src/ts/common/binaryUtils.ts @@ -1,20 +1,20 @@ import { BinaryWriter, getWriterBuffer, createBinaryWriter, resizeWriter } from 'ag-sockets/dist/browser'; export function writeBinary(write: (writer: BinaryWriter) => void): Uint8Array { - const writer = createBinaryWriter(); + 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); + 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); + return getWriterBuffer(writer); } diff --git a/src/ts/common/bitUtils.ts b/src/ts/common/bitUtils.ts index c810e45..f8086e5 100644 --- a/src/ts/common/bitUtils.ts +++ b/src/ts/common/bitUtils.ts @@ -2,113 +2,113 @@ export type WriteBits = (value: number, bits: number) => void; export type ReadBits = (bits: number) => number; export function numberToBitCount(value: number) { - value = value >>> 0; + value = value >>> 0; - for (let mask = 0xffffffff >>> 0, bits = 0; mask; mask = (mask << 1) >>> 0, bits++) { - if ((value & mask) === 0) { - return bits; - } - } + for (let mask = 0xffffffff >>> 0, bits = 0; mask; mask = (mask << 1) >>> 0, bits++) { + if ((value & mask) === 0) { + return bits; + } + } - return 32; + return 32; } export function countBits(value: number) { - value = value >>> 0; + value = value >>> 0; - let bits = 0; + let bits = 0; - while (value) { - bits += value & 1; - value = value >>> 1; - } + while (value) { + bits += value & 1; + value = value >>> 1; + } - return bits; + return bits; } export function bitWriter(writes: (writer: WriteBits) => void): Uint8Array { - let buffer = new Uint8Array(16); - let length = 0; - let byte = 0; - let byteBits = 0; + 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; - } + function writeByte(value: number) { + if (buffer.length <= length) { + const newBuffer = new Uint8Array(buffer.length * 2); + newBuffer.set(buffer); + buffer = newBuffer; + } - buffer[length] = value; - length++; - } + buffer[length] = value; + length++; + } - writes((value, bits) => { - if (bits < 0 || bits > 32) { - throw new Error('Invalid bit count'); - } + 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; + 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 === 8) { + writeByte(byte); + byte = 0; + byteBits = 0; + } + } + }); - if (byteBits) { - writeByte(byte); - byteBits = 0; - byte = 0; - } + if (byteBits) { + writeByte(byte); + byteBits = 0; + byte = 0; + } - return buffer.subarray(0, length); + return buffer.subarray(0, length); } export function bitReader(buffer: Uint8Array): ReadBits { - let offset = 0; + let offset = 0; - return bitReaderCustom(() => { - if (buffer.length <= offset) { - throw new Error('Reading past end'); - } + return bitReaderCustom(() => { + if (buffer.length <= offset) { + throw new Error('Reading past end'); + } - return buffer[offset++]; - }); + return buffer[offset++]; + }); } export function bitReaderCustom(readByte: () => number): ReadBits { - let byte = 0; - let byteBits = 0; + let byte = 0; + let byteBits = 0; - return bits => { - if (bits < 0 || bits > 32) { - throw new Error('Invalid bit count'); - } + return bits => { + if (bits < 0 || bits > 32) { + throw new Error('Invalid bit count'); + } - let result = 0; + let result = 0; - while (bits) { - if (!byteBits) { - byte = readByte(); - byteBits = 8; - } + 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; - } + 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; - }; + return result >>> 0; + }; } diff --git a/src/ts/common/camera.ts b/src/ts/common/camera.ts index a238548..235f5da 100644 --- a/src/ts/common/camera.ts +++ b/src/ts/common/camera.ts @@ -8,123 +8,123 @@ 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, - }; + 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); + 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 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 playerX = toScreenX(player.x); + const playerY = toScreenY(player.y); - const mapWidth = toScreenX(map.width); - const mapHeight = toScreenY(map.height); + 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 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 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 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); + 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); + 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); + 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)); + 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); + 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); + 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); + 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); + 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); + 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); + return isBoundsVisible(camera, entity.bounds, entity.x, entity.y); } function isChatBaloonAboveScreenTop(camera: Camera, entity: Entity) { - return getChatBallonXY(entity, camera).y <= -5; + 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); + 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), - }; + 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), - }; + 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 { diff --git a/src/ts/common/collision.ts b/src/ts/common/collision.ts index 02cea72..67c0a3d 100644 --- a/src/ts/common/collision.ts +++ b/src/ts/common/collision.ts @@ -9,298 +9,298 @@ let isCollidingCount = 0; let isCollidingObjectCount = 0; export function getCollisionStats() { - const stats = { isCollidingCount, isCollidingObjectCount }; - isCollidingCount = 0; - isCollidingObjectCount = 0; - return stats; + const stats = { isCollidingCount, isCollidingObjectCount }; + isCollidingCount = 0; + isCollidingObjectCount = 0; + return stats; } export function isOutsideMap(x: number, y: number, map: IMap): boolean { - return x < 0 || y < 0 || x >= map.width || y >= map.height; + return x < 0 || y < 0 || x >= map.width || y >= map.height; } export function canCollideWith(entity: Entity): boolean { - return (entity.flags & EntityFlags.CanCollideWith) !== 0; + return (entity.flags & EntityFlags.CanCollideWith) !== 0; } export function isStaticCollision(entity: Entity, map: IMap, forceOnGround = false) { - if (DEVELOPMENT && entity.type !== PONY_TYPE) { - console.error(`isStaticCollision: non-pony entity`); - } + 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); + const flying = !forceOnGround && isInTheAir(entity); + return isPonyColliding(entity.x, entity.y, map as any, flying); } export function fixCollision(entity: Entity, map: IMap) { - if (DEVELOPMENT && entity.type !== PONY_TYPE) { - console.error(`fixCollision: non-pony entity`); - } + if (DEVELOPMENT && entity.type !== PONY_TYPE) { + console.error(`fixCollision: non-pony entity`); + } - const flying = isInTheAir(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; + 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; - } - } - } + if (!isPonyColliding(tx, ty, map as any, flying)) { + entity.x += x; + entity.y += y; + return true; + } + } + } - return false; + return false; } function isPonyColliding(x: number, y: number, map: IMap, flying: boolean): boolean { - if (isOutsideMap(x, y, map)) { - return true; - } + if (isOutsideMap(x, y, map)) { + return true; + } - const region = getRegionGlobal(map, x, y); + const region = getRegionGlobal(map, x, y); - if (region === undefined) { - return true; - } + 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; + 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; + return (pixel & mask) !== 0; } function isColliding(x: number, y: number, mask: number, map: IMap) { - 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 (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; - } - } + 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) { - 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; + 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 ((entity.flags & EntityFlags.CanCollide) === 0) { + entity.x = destX; + entity.y = destY; + return; + } - if (DEVELOPMENT && entity.type !== PONY_TYPE) { - console.error(`updatePosition: non-pony entity`); - } + if (DEVELOPMENT && entity.type !== PONY_TYPE) { + console.error(`updatePosition: non-pony entity`); + } - const flying = isInTheAir(entity); - const mask = flying ? 2 : 1; + 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 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 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; + 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 x = x0 | 0; + let y = y0 | 0; - let actualX = x | 0; - let actualY = y | 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; - } + if (isColliding(actualX, actualY, mask, map)) { + if (!isOutsideMap(destX, destY, map)) { + entity.x = destX; + entity.y = destY; + } - return; - } + return; + } - const a = (dstY - srcY) / (dstX - srcX); - const b = srcY - a * srcX; - const useGt = srcY < dstY; + 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; + 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; + 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; - } - } + 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; + let steps = 1000; - for (; steps; steps--) { - const fx = a * (x + ox) + b; - const fy = y + oy; + for (; steps; steps--) { + const fx = a * (x + ox) + b; + const fy = y + oy; - let tx = 0 | 0; - let ty = 0 | 0; + 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; - } + 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; + x = (x + tx) | 0; + y = (y + ty) | 0; - if (x < minX || x > maxX || y < minY || y > maxY) { - break; - } + 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; + 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 (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; - } + 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; + 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; - } + 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; - } - } + canMove = canShiftLeft || canShiftRight; + } + } - if (!collides) { - actualX = actualNX; - actualY = actualNY; - } else if (!canMove || horizontalOrVertical) { - break; - } - } + 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; + 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)); + entity.x = toWorldX(clamp(dstX, left, right)); + entity.y = toWorldY(clamp(dstY, top, bottom)); - if (DEVELOPMENT && steps <= 0) { - console.error('Overflow collision steps'); - } + if (DEVELOPMENT && steps <= 0) { + console.error('Overflow collision steps'); + } } diff --git a/src/ts/common/color.ts b/src/ts/common/color.ts index f6b7040..5e9b071 100644 --- a/src/ts/common/color.ts +++ b/src/ts/common/color.ts @@ -2,239 +2,239 @@ 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' + 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; + h: number; + s: number; + v: number; + a: number; } export interface RGB { - r: number; - g: number; - b: number; + r: number; + g: number; + b: number; } export interface RGBA extends RGB { - a: number; + a: number; } export function getR(color: number) { - return (color >> 24) & 0xff; + return (color >> 24) & 0xff; } export function getG(color: number) { - return (color >> 16) & 0xff; + return (color >> 16) & 0xff; } export function getB(color: number) { - return (color >> 8) & 0xff; + return (color >> 8) & 0xff; } export function getAlpha(color: number) { - return color & 0xff; + return color & 0xff; } export function withAlpha(color: number, alpha: number) { - return (color & 0xffffff00) | (alpha & 0xff); + return (color & 0xffffff00) | (alpha & 0xff); } export function withAlphaFloat(color: number, alpha: number) { - return (color & 0xffffff00) | ((alpha * 255) & 0xff); + 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), - }; + 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); + return rgb2hsv(getR(color), getG(color), getB(color), getAlpha(color) / 255, h); } export function colorToCSS(color: number): string { - const alpha = getAlpha(color); + const alpha = getAlpha(color); - if (alpha === 0xff) { - return `#${colorToHexRGB(color)}`; - } else { - return `rgba(${getR(color)},${getG(color)},${getB(color)},${alpha / 255})`; - } + 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'); + return value.toString(16).padStart(length, '0'); } export function colorToHexRGB(color: number) { - return toHex(color >>> 8, 6); + return toHex(color >>> 8, 6); } export function colorToFloatArray(color: number): Float32Array { - const result = new Float32Array(4); - colorToExistingFloatArray(result, color); - return result; + 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; + 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); @@ -242,254 +242,254 @@ 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]; + 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]; + 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; + 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); + 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); + return colorFromHSVA(h, s, v, a); } // parse export function parseColorFast(str: string): number { - if (!isString(str)) - return TRANSPARENT; + if (!isString(str)) + return TRANSPARENT; - const int = parseInt(str, 16); + const int = parseInt(str, 16); - if (str.length !== 6 || isNaN(int) || int < 0) { - return parseColorWithAlpha(str, 1); - } else { - return (((int << 8) | 0xff) >>> 0); - } + 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; + if (!isString(str)) + return TRANSPARENT; - str = str.trim().toLowerCase(); + str = str.trim().toLowerCase(); - if (str === '' || str === 'none' || str === 'transparent') - return TRANSPARENT; + if (str === '' || str === 'none' || str === 'transparent') + return TRANSPARENT; - str = colorNames[str] || str; + str = colorNames[str] || str; - const m = /(\d+)[ ,]+(\d+)[ ,]+(\d+)(?:[ ,]+(\d*\.?\d+))?/.exec(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); - } + 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); + const n = /[0-9a-f]+/i.exec(str); - if (n) { - const s = n[0]; + 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); - } - } + 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; + return BLACK; } export function parseColorWithAlpha(str: string, alpha: number /* 0-1 */): number { - return ((parseColor(str) & 0xffffff00) | ((alpha * 255) & 0xff)) >>> 0; + 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); + 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; + 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) - ); + 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; + 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 - ); + 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; + 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; + 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; - } + 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 }; + 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)); + 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; + let r = v; + let g = v; + let b = v; - if (s !== 0) { - h /= 60; + 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)); + 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; - } - } + 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), - }; + 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)); + 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; - } + 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) - }; + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; } diff --git a/src/ts/common/colors.ts b/src/ts/common/colors.ts index 1e2087d..80fe35f 100644 --- a/src/ts/common/colors.ts +++ b/src/ts/common/colors.ts @@ -65,109 +65,109 @@ export const ENTITY_ITEM_BG = '#dc76bc'; export const MAGIC_ALPHA = 150; export function updateActionColor(color: string) { - if (DEVELOPMENT) { - ACTION_EXPRESSION_BG = color; - } + 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); - } + 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; + return color ? colorToHexRGB(fillToOutlineColor(parseColorFast(color))) : undefined; } export function fillToOutlineWithDarken(color: string | undefined): string | undefined { - return color ? colorToHexRGB(darkenForOutline(fillToOutlineColor(parseColorFast(color)))) : 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 { 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); + 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; - } + 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; - } + 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; + } } diff --git a/src/ts/common/compress.ts b/src/ts/common/compress.ts index df344fe..f371ce4 100644 --- a/src/ts/common/compress.ts +++ b/src/ts/common/compress.ts @@ -3,155 +3,155 @@ import { bitWriter, bitReader } from './bitUtils'; import { REGION_SIZE } from './constants'; function getBitsForNumber(value: number) { - let bits = 0; - let max = value - 1; + let bits = 0; + let max = value - 1; - while (max > 0) { - bits++; - max >>= 1; - } + while (max > 0) { + bits++; + max >>= 1; + } - return bits; + return bits; } export function compressTiles(tiles: Uint8Array): Uint8Array { - const types: number[] = []; + const types: number[] = []; - for (let i = 0; i < tiles.length; i++) { - const tile = tiles[i]; + for (let i = 0; i < tiles.length; i++) { + const tile = tiles[i]; - if (types.indexOf(tile) === -1) { - types.push(tile); - } - } + if (types.indexOf(tile) === -1) { + types.push(tile); + } + } - const bitsPerTile = getBitsForNumber(types.length); - const bitsPerRun = 4; + const bitsPerTile = getBitsForNumber(types.length); + const bitsPerRun = 4; - return bitWriter(write => { - write(types.length, 8); + return bitWriter(write => { + write(types.length, 8); - for (const type of types) { - write(type, 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 (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 (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++; - } + if (value === tiles[i]) { + while (i < tiles.length && count < 0b111 && tiles[i] === value) { + i++; + count++; + } - i--; + i--; - write(count, bitsPerRun); - write(types.indexOf(value), bitsPerTile); - } else { - let last = tiles[i]; - let last2 = last; - let pushLast = true; - const values = [value]; - count++; + 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]; + 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; - } - } + 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); + write(count | 0b1000, bitsPerRun); - for (const v of values) { - write(types.indexOf(v), bitsPerTile); - } + for (const v of values) { + write(types.indexOf(v), bitsPerTile); + } - if (pushLast) { - write(types.indexOf(last), 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[] = []; + 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)); - } + 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; + 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); + for (let i = 0; i < size;) { + const value = read(bitsPerRun); - if ((value & 0b1000) === 0) { - const count = value; - const entry = read(bitsPerTile); + 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[entry]; + i++; + } + } else { + const count = value & 0b0111; - for (let j = 0; j < count; j++) { - result[i] = types[read(bitsPerTile)]; - i++; - } - } - } - } + for (let j = 0; j < count; j++) { + result[i] = types[read(bitsPerTile)]; + i++; + } + } + } + } - return result; + return result; } export function deserializeTiles(tiles: string) { - const decodedTiles = toByteArray(tiles); - const result: number[] = []; + 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]; + 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--; - } - } + while (count > 0) { + result.push(tile); + count--; + } + } - return result; + return result; } diff --git a/src/ts/common/compressPony.ts b/src/ts/common/compressPony.ts index 0c5d007..f797f65 100644 --- a/src/ts/common/compressPony.ts +++ b/src/ts/common/compressPony.ts @@ -9,176 +9,176 @@ 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 + SLEEVED_ACCESSORIES, frontHooves, mergedFacialHair, mergedBackAccessories, mergedManes, + mergedBackManes, mergedExtraAccessories, mergedHeadAccessories } from '../client/ponyUtils'; import { CM_SIZE } from './constants'; export const VERSION = 3; interface FieldDefinition { - name: keyof PonyInfo; - default?: T; - omit?: (info: PonyInfoBase>) => boolean; - dontSave?: boolean; + name: keyof PonyInfo; + default?: T; + omit?: (info: PonyInfoBase>) => boolean; + dontSave?: boolean; } interface SetDefinition extends FieldDefinition { - preserveOnZero?: boolean; - sets: ColorExtraSet[]; - minColors?: number; - // defaultLockFills?: boolean[]; - // defaultLockOutlines?: boolean[]; + 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[]; + 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[]; + version: number; + colors: number[]; + setFields: (PrecompressedSet | undefined)[]; + colorFields: number[]; + numberFields: number[]; + booleanFields: boolean[]; + cm: number[]; } const identity = (x: T) => x; const not = (x: T) => !x; function emptyOrUnlocked(set: SpriteSet | undefined): boolean { - return !set || !set.type || !set.lockFills || set.lockFills.every(x => !x); + return !set || !set.type || !set.lockFills || set.lockFills.every(x => !x); } function emptyOrZeroLocked(set: SpriteSet | 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))); + 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(set: SpriteSet | undefined): boolean { - return !set || !set.type; + return !set || !set.type; } function omitMane(info: PonyInfoNumber) { - return empty(info.mane) && emptyOrUnlocked(info.backMane) - && emptyOrUnlocked(info.tail) && emptyOrUnlocked(info.facialHair); + return empty(info.mane) && emptyOrUnlocked(info.backMane) + && emptyOrUnlocked(info.tail) && emptyOrUnlocked(info.facialHair); } function omitHead(info: PonyInfoNumber): boolean { - return emptyOrZeroLocked(info.head, !!info.customOutlines); + return emptyOrZeroLocked(info.head, !!info.customOutlines); } function omitSleeves(info: PonyInfoNumber) { - return !info.chestAccessory || !includes(SLEEVED_ACCESSORIES, toInt(info.chestAccessory.type)); + return !info.chestAccessory || !includes(SLEEVED_ACCESSORIES, toInt(info.chestAccessory.type)); } function omitFrontHooves(info: PonyInfoNumber) { - return empty(info.frontHooves) && emptyOrUnlocked(info.backHooves); + return empty(info.frontHooves) && emptyOrUnlocked(info.backHooves); } function readTimes(read: ReadBits, count: number, bitsPerItem: number): number[] { - const result: number[] = []; + const result: number[] = []; - for (let i = 0; i < count; i++) { - result[i] = read(bitsPerItem); - } + for (let i = 0; i < count; i++) { + result[i] = read(bitsPerItem); + } - return result; + 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, - }, + { 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[] = [ - { 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 }, + { 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[] = [ - { name: 'eyelashes' }, - { name: 'eyeOpennessRight' }, - { name: 'eyeOpennessLeft', omit: info => !!info.lockEyes }, - { name: 'fangs' }, - { name: 'muzzle' }, - { name: 'freckles', dontSave: true }, // TODO: remove + { name: 'eyelashes' }, + { name: 'eyeOpennessRight' }, + { name: 'eyeOpennessLeft', omit: info => !!info.lockEyes }, + { name: 'fangs' }, + { name: 'muzzle' }, + { name: 'freckles', dontSave: true }, // TODO: remove ]; const colorFields: FieldDefinition[] = [ - { 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 }, + { 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[] = [ - ...setFields, - ...booleanFields, - ...numberFields, - ...colorFields, + ...setFields, + ...booleanFields, + ...numberFields, + ...colorFields, ].filter(f => !!f.omit); const VERSION_BITS = 6; // max 63 @@ -192,106 +192,106 @@ const NUMBERS_BITS = 6; // max 63 /* istanbul ignore next */ if (DEVELOPMENT) { - (function () { - function verifyFields(obj: any, lengthBits: number, defs: FieldDefinition[], verify: (field: any) => boolean) { - const missing = Object.keys(obj) - .filter(key => verify(obj[key])) - .filter(key => defs.every(d => d.name !== key)); + (function () { + function verifyFields(obj: any, lengthBits: number, defs: FieldDefinition[], 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])); + const unnecessary = defs + .filter(({ name }) => !verify(obj[name])); - if (missing.length || unnecessary.length) { - throw new Error(`Incorrect fields (${missing} / ${unnecessary})`); - } + 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)})`); - } - } + 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); + 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})`); - } - })(); + if (setFields.some(f => !f.sets)) { + throw new Error(`Undefined set in set field (${setFields.find(f => !f.sets)!.name})`); + } + })(); } function trimRight(items: T[]) { - const index = findLastIndex(items, x => !!x); - return (index !== (items.length - 1)) ? items.slice(0, index + 1) : items; + const index = findLastIndex(items, x => !!x); + return (index !== (items.length - 1)) ? items.slice(0, index + 1) : items; } export function precompressCM(cm: (T | undefined)[] | undefined, addColor: (color: T | undefined) => number): number[] { - const result: number[] = []; + const result: number[] = []; - if (cm) { - let length = CM_SIZE * CM_SIZE; + if (cm) { + let length = CM_SIZE * CM_SIZE; - while (length > 0 && !cm[length - 1]) { - length--; - } + while (length > 0 && !cm[length - 1]) { + length--; + } - for (let i = 0; i < length; i++) { - result.push(addColor(cm[i])); - } - } + for (let i = 0; i < length; i++) { + result.push(addColor(cm[i])); + } + } - return result; + 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); + 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[] = []; + const result: boolean[] = []; - for (let i = 0; i < MAX_COLORS; i++) { - result[i] = i < count ? !!(set & (1 << i)) : defaultValues[i]; - } + for (let i = 0; i < MAX_COLORS; i++) { + result[i] = i < count ? !!(set & (1 << i)) : defaultValues[i]; + } - return result; + return result; } // colors export function precompressColorSet( - set: (T | undefined)[] | undefined, count: number, locks: number, defaultColor: T, addColor: (color: T) => number + set: (T | undefined)[] | undefined, count: number, locks: number, defaultColor: T, addColor: (color: T) => number ): number[] { - const result: 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)); - } - } - } + 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; + return result; } export function postdecompressColorSet( - colors: number[], count: number, locks: number, colorList: number[], parseColor: (color: number) => T + colors: number[], count: number, locks: number, colorList: number[], parseColor: (color: number) => T ): T[] { - const result: 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)); - } + 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; + return result; } // set @@ -301,70 +301,70 @@ const ALL_UNLOCKED = array(MAX_COLORS, false); const ALL_LOCKED = array(MAX_COLORS, true); export function precompressSet( - set: SpriteSet | undefined, def: SetDefinition, customOutlines: boolean, defaultColor: T, addColor: (color: T) => number + set: SpriteSet | undefined, def: SetDefinition, customOutlines: boolean, defaultColor: T, addColor: (color: T) => number ): PrecompressedSet | undefined { - if (!set) - return undefined; + if (!set) + return undefined; - const type = clamp(toInt(set.type), 0, def.sets.length - 1); + const type = clamp(toInt(set.type), 0, def.sets.length - 1); - if (type === 0 && !def.preserveOnZero) - return undefined; + 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); + 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; + /* 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) : []; + 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 }; + return { type, pattern, colors, fillLocks, fills, outlineLocks, outlines }; } export function postdecompressSet( - set: PrecompressedSet, _def: SetDefinition, customOutlines: boolean, colorList: number[], parseColor: (color: number) => T + set: PrecompressedSet, _def: SetDefinition, customOutlines: boolean, colorList: number[], parseColor: (color: number) => T ): SpriteSet | 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) : [], - }; + 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, TValue, TResult>( - data: any, defs: TDef[], defaultValue: TResult, encode: (value: TValue | undefined, def: TDef) => 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); - } - })); + return trimRight(defs.map(def => { + if (def.dontSave || (def.omit && def.omit(data))) { + return defaultValue; + } else { + return encode(data[def.name], def); + } + })); } function postdecompressFields, TValue, TResult>( - result: any, defs: TDef[], values: (TValue | undefined)[], defaultValue: TValue, decode: (value: TValue, def: TDef) => 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); - } + 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 @@ -372,105 +372,105 @@ function postdecompressFields, TValue, TRes type Info = PonyInfoBase>; export function precompressPony(info: Info, 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); - }; + 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 | undefined, def: SetDefinition) => precompressSet(x, def, customOutlines, defaultColor, addColor)), - cm: precompressCM(info.cm, addColor), - }; + 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 | 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(result: Info, 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], - }; + 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; - }); - } + 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')); + 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); + postdecompressSet, setFields, omittableFields, fixVersion); export function postdecompressPony(data: Precompressed, parseColor: (color: number) => T): Info { - // NOTE: when updating also update createPostDecompressPony() + // NOTE: when updating also update createPostDecompressPony() - const result: Info = {} 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)); + const result: Info = {} 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; + 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; - } - }); + omittableFields.forEach(def => { + if (def.omit && def.omit(result)) { + result[def.name] = undefined; + } + }); - fixVersion(result, data, parseColor); + fixVersion(result, data, parseColor); - return result; + return result; } // set @@ -480,69 +480,69 @@ 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); + 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 (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)); - } - } + 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); + 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; - } + 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(write: WriteBits, lengthBits: number, fields: T[], writeField: (value: T) => void) { - write(fields.length, lengthBits); - fields.forEach(writeField); + write(fields.length, lengthBits); + fields.forEach(writeField); } function readFields(read: ReadBits, lengthBits: number, readField: (read: ReadBits) => T): T[] { - const length = read(lengthBits); - const result: T[] = []; + const length = read(lengthBits); + const result: T[] = []; - for (let i = 0; i < length; i++) { - result.push(readField(read)); - } + for (let i = 0; i < length; i++) { + result.push(readField(read)); + } - return result; + 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 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; @@ -551,74 +551,74 @@ 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 }; + 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))); + return fromByteArray(bitWriter(write => writePony(write, data))); } function readPonyFromBuffer(info: Uint8Array): Precompressed { - return readPony(bitReader(info)); + return readPony(bitReader(info)); } function readPonyFromString(info: string): Precompressed { - return info ? readPonyFromBuffer(toByteArray(info)) : { - version: VERSION, - colors: [], - booleanFields: [], - numberFields: [], - colorFields: [], - setFields: [], - cm: [], - }; + 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)); + 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); + 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; + return color ? parseColorFast(color) : TRANSPARENT; } function colorToString(color: number): string { - return color ? colorToHexRGB(color) : ''; + return color ? colorToHexRGB(color) : ''; } export function compressPonyString(info: PonyInfo): string { - return writePonyToString(precompressPony(info, '000000', parseColorFastSafe)); + 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); + 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); + return toPaletteNumber(decompressPony(info), paletteManager); } diff --git a/src/ts/common/constants.ts b/src/ts/common/constants.ts index 351edb0..7fe73a6 100644 --- a/src/ts/common/constants.ts +++ b/src/ts/common/constants.ts @@ -42,7 +42,7 @@ 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; + return !range || range < MIN_CHATLOG_RANGE || range >= MAX_CHATLOG_RANGE; } export const WATER_FPS = 6; @@ -108,30 +108,30 @@ 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' }, + { 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', + 'January', + 'February ', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', ]; export const OFFLINE_PONY = 'DAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA=='; @@ -143,48 +143,48 @@ export const rewardLevel2 = '2411886'; export const rewardLevel3 = '2411888'; const SUPPORTER_REWARDS_COMMON = [ - `In-game supporter tag`, - `Supporter chat color`, + `In-game supporter tag`, + `Supporter chat color`, ]; const SUPPORTER_REWARDS_MORE = [ - `Access to patreon posts`, - `Early access to new and experimental features`, + `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`, - ], + [], + [ + ...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`, + ...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`, + `${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`, + `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`, ]; diff --git a/src/ts/common/countries.ts b/src/ts/common/countries.ts index b4cdb5c..895930c 100644 --- a/src/ts/common/countries.ts +++ b/src/ts/common/countries.ts @@ -1,251 +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`, + 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`, }; diff --git a/src/ts/common/debugData.ts b/src/ts/common/debugData.ts index 1e88c47..11eebad 100644 --- a/src/ts/common/debugData.ts +++ b/src/ts/common/debugData.ts @@ -3,47 +3,47 @@ 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 🍎 mesaaasage', 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: '♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎' }, - ); + 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 🍎 mesaaasage', 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: '♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎' }, + ); } diff --git a/src/ts/common/encoders/expressionEncoder.ts b/src/ts/common/encoders/expressionEncoder.ts index 7084ac5..08207a4 100644 --- a/src/ts/common/encoders/expressionEncoder.ts +++ b/src/ts/common/encoders/expressionEncoder.ts @@ -4,31 +4,31 @@ import { hasFlag } from '../utils'; export const EMPTY_EXPRESSION = 0x1fffffff; export function encodeExpression(expression: Expression | undefined): number { - if (!expression) - return EMPTY_EXPRESSION; + if (!expression) + return EMPTY_EXPRESSION; - const { extra, rightIris, leftIris, right, left, muzzle } = 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; + // 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; + value = value >>> 0; - if (value === EMPTY_EXPRESSION) - return undefined; + 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; + 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 }; + return { muzzle, left, right, leftIris, rightIris, extra }; } export function isCancellableExpression(expression: Expression) { - return hasFlag(expression.extra, ExpressionExtra.Zzz); + return hasFlag(expression.extra, ExpressionExtra.Zzz); } diff --git a/src/ts/common/encoders/updateDecoder.ts b/src/ts/common/encoders/updateDecoder.ts index 961ddef..0cc7476 100644 --- a/src/ts/common/encoders/updateDecoder.ts +++ b/src/ts/common/encoders/updateDecoder.ts @@ -1,152 +1,152 @@ import { - BinaryWriter, BinaryReader, writeInt16, readInt16, createBinaryReader, readUint16, readLength, - readUint32, readUint8, readObject, readUint8Array + 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})`); - } + if (value >= MAX_VELOCITY || value <= -MAX_VELOCITY) { + throw new Error(`Exceeded max velocity (${value})`); + } - writeInt16(writer, (value * 0x8000) / MAX_VELOCITY); + writeInt16(writer, (value * 0x8000) / MAX_VELOCITY); } export function readVelocity(reader: BinaryReader) { - return (readInt16(reader) * MAX_VELOCITY) / 0x8000; + return (readInt16(reader) * MAX_VELOCITY) / 0x8000; } export function writeCoordX(writer: BinaryWriter, value: number) { - writeInt16(writer, (value * tileWidth) | 0); + writeInt16(writer, (value * tileWidth) | 0); } export function writeCoordY(writer: BinaryWriter, value: number) { - writeInt16(writer, (value * tileHeight) | 0); + writeInt16(writer, (value * tileHeight) | 0); } export function readCoordX(reader: BinaryReader) { - return readInt16(reader) / tileWidth; + return readInt16(reader) / tileWidth; } export function readCoordY(reader: BinaryReader) { - return readInt16(reader) / tileHeight; + 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, - }; + 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; + 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); - } + while (update = readOneUpdate(reader)) { + updates.push(update); + } - const removesLength = readLength(reader); - const removes: number[] = []; + const removesLength = readLength(reader); + const removes: number[] = []; - for (let i = 0; i < removesLength; i++) { - removes.push(readUint32(reader)); - } + for (let i = 0; i < removesLength; i++) { + removes.push(readUint32(reader)); + } - const tilesLength = readLength(reader); - const tiles: TileUpdate[] = []; + 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), - }); - } + for (let i = 0; i < tilesLength; i++) { + tiles.push({ + x: readUint8(reader), + y: readUint8(reader), + type: readUint8(reader), + }); + } - const tileData = readUint8Array(reader); + const tileData = readUint8Array(reader); - return { x, y, updates, removes, tiles, tileData }; + return { x, y, updates, removes, tiles, tileData }; } export function readOneUpdate(reader: BinaryReader): DecodedUpdate | undefined { - if (reader.offset >= reader.view.byteLength) - return undefined; + if (reader.offset >= reader.view.byteLength) + return undefined; - const flags = readUint16(reader); + const flags = readUint16(reader); - if (flags === 0) { - return undefined; - } + if (flags === 0) { + return undefined; + } - const id = readUint32(reader); - const update = emptyUpdate(id); + const id = readUint32(reader); + const update = emptyUpdate(id); - update.switchRegion = (flags & UpdateFlags.SwitchRegion) !== 0; + update.switchRegion = (flags & UpdateFlags.SwitchRegion) !== 0; - if ((flags & UpdateFlags.Position) !== 0) { - update.x = readCoordX(reader); - update.y = readCoordY(reader); - } + 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.Velocity) !== 0) { + update.vx = readVelocity(reader); + update.vy = readVelocity(reader); + } - if ((flags & UpdateFlags.State) !== 0) { - update.state = readUint8(reader); - } + if ((flags & UpdateFlags.State) !== 0) { + update.state = readUint8(reader); + } - if ((flags & UpdateFlags.Expression) !== 0) { - update.expression = readUint32(reader); - } + if ((flags & UpdateFlags.Expression) !== 0) { + update.expression = readUint32(reader); + } - if ((flags & UpdateFlags.Type) !== 0) { - update.type = readUint16(reader); - } + if ((flags & UpdateFlags.Type) !== 0) { + update.type = readUint16(reader); + } - if ((flags & UpdateFlags.Options) !== 0) { - update.options = readObject(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.Info) !== 0) { + update.crc = readUint16(reader); + update.info = readUint8Array(reader)!; + } - if ((flags & UpdateFlags.Action) !== 0) { - update.action = readUint8(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.Name) !== 0) { + update.name = decodeString(readUint8Array(reader)) || undefined; + update.filterName = (flags & UpdateFlags.NameBad) !== 0; + } - if ((flags & UpdateFlags.PlayerState) !== 0) { - update.playerState = readUint8(reader); - } + if ((flags & UpdateFlags.PlayerState) !== 0) { + update.playerState = readUint8(reader); + } - return update; + return update; } diff --git a/src/ts/common/encoders/updateEncoder.ts b/src/ts/common/encoders/updateEncoder.ts index 511f8fe..948a782 100644 --- a/src/ts/common/encoders/updateEncoder.ts +++ b/src/ts/common/encoders/updateEncoder.ts @@ -1,5 +1,5 @@ import { - BinaryWriter, writeUint32, writeUint16, writeUint8, writeObject, writeUint8Array, writeLength + BinaryWriter, writeUint32, writeUint16, writeUint8, writeObject, writeUint8Array, writeLength } from 'ag-sockets/dist/browser'; import { UpdateFlags, EntityPlayerState, Action } from '../interfaces'; import { writeBinary } from '../binaryUtils'; @@ -11,157 +11,157 @@ 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; + 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 + 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 (DEVELOPMENT && flags === 0) { + logger.error(`Writing empty update`); + } - if ((flags & UpdateFlags.Position) !== 0) { - flags |= UpdateFlags.State; + if ((flags & UpdateFlags.Position) !== 0) { + flags |= UpdateFlags.State; - if (vx || vy) { - flags |= UpdateFlags.Velocity; - } - } + if (vx || vy) { + flags |= UpdateFlags.Velocity; + } + } - if ((flags & UpdateFlags.Name) !== 0 && entity.nameBad === true) { - flags |= UpdateFlags.NameBad; - } + if ((flags & UpdateFlags.Name) !== 0 && entity.nameBad === true) { + flags |= UpdateFlags.NameBad; + } - writeUint16(writer, flags); - writeUint32(writer, entity.id); + writeUint16(writer, flags); + writeUint32(writer, entity.id); - if ((flags & UpdateFlags.Position) !== 0) { - writeCoordX(writer, x); - writeCoordY(writer, y); - } + 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.Velocity) !== 0) { + writeVelocity(writer, vx); + writeVelocity(writer, vy); + } - if ((flags & UpdateFlags.State) !== 0) { - writeUint8(writer, entity.state); - } + if ((flags & UpdateFlags.State) !== 0) { + writeUint8(writer, entity.state); + } - if ((flags & UpdateFlags.Expression) !== 0) { - writeUint32(writer, entity.options!.expr!); - } + if ((flags & UpdateFlags.Expression) !== 0) { + writeUint32(writer, entity.options!.expr!); + } - if ((flags & UpdateFlags.Type) !== 0) { - writeUint16(writer, entity.type); - } + if ((flags & UpdateFlags.Type) !== 0) { + writeUint16(writer, entity.type); + } - if ((flags & UpdateFlags.Options) !== 0) { - writeObject(writer, options); - } + if ((flags & UpdateFlags.Options) !== 0) { + writeObject(writer, options); + } - if ((flags & UpdateFlags.Info) !== 0) { - writeUint16(writer, entity.crc!); - writeUint8Array(writer, entity.encryptedInfoSafe!); - } + if ((flags & UpdateFlags.Info) !== 0) { + writeUint16(writer, entity.crc!); + writeUint8Array(writer, entity.encryptedInfoSafe!); + } - if ((flags & UpdateFlags.Action) !== 0) { - writeUint8(writer, action!); - } + if ((flags & UpdateFlags.Action) !== 0) { + writeUint8(writer, action!); + } - if ((flags & UpdateFlags.Name) !== 0) { - writeUint8Array(writer, entity.encodedName!); - } + if ((flags & UpdateFlags.Name) !== 0) { + writeUint8Array(writer, entity.encodedName!); + } - if ((flags & UpdateFlags.PlayerState) !== 0) { - writeUint8(writer, playerState!); - } + 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); + 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; + let flags = UpdateFlags.Position | UpdateFlags.State | UpdateFlags.Type; - if (entity.encryptedInfoSafe !== undefined) { - flags |= UpdateFlags.Info; - } + if (entity.encryptedInfoSafe !== undefined) { + flags |= UpdateFlags.Info; + } - if (entity.encodedName !== undefined) { - flags |= UpdateFlags.Name; - } + if (entity.encodedName !== undefined) { + flags |= UpdateFlags.Name; + } - if (playerState !== 0) { - flags |= UpdateFlags.PlayerState; - } + if (playerState !== 0) { + flags |= UpdateFlags.PlayerState; + } - if (options !== undefined) { - flags |= UpdateFlags.Options; - } + if (options !== undefined) { + flags |= UpdateFlags.Options; + } - writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, Action.None, playerState); + 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; + const { x, y, entityUpdates, entityRemoves, tileUpdates } = region; - writeUint16(writer, x); - writeUint16(writer, y); + 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); - } + 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 + writeUint16(writer, 0); // end marker - writeLength(writer, entityRemoves.length); + writeLength(writer, entityRemoves.length); - for (const remove of entityRemoves) { - writeUint32(writer, remove); - } + for (const remove of entityRemoves) { + writeUint32(writer, remove); + } - writeLength(writer, tileUpdates.length); + writeLength(writer, tileUpdates.length); - for (const { x, y, type: tile } of tileUpdates) { - writeUint8(writer, x); - writeUint8(writer, y); - writeUint8(writer, tile); - } + for (const { x, y, type: tile } of tileUpdates) { + writeUint8(writer, x); + writeUint8(writer, y); + writeUint8(writer, tile); + } - writeUint8Array(writer, null); // tile data + writeUint8Array(writer, null); // tile data } export function writeRegion(writer: BinaryWriter, region: ServerRegion, client: IClient) { - const { x, y, entities } = region; + const { x, y, entities } = region; - writeUint16(writer, x); - writeUint16(writer, y); + writeUint16(writer, x); + writeUint16(writer, y); - for (const entity of entities) { - if (!isEntityShadowed(entity) || entity === client.pony) { - writeOneEntity(writer, entity, client); - } - } + for (const entity of entities) { + if (!isEntityShadowed(entity) || entity === client.pony) { + writeOneEntity(writer, entity, client); + } + } - writeUint16(writer, 0); // end marker + writeUint16(writer, 0); // end marker - writeLength(writer, 0); // removes - writeLength(writer, 0); // tile updates - writeUint8Array(writer, getRegionTiles(region)); // tile data + 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)); + return writeBinary(writer => writeUpdate(writer, region)); } // For testing export function encodeRegionSimple(region: ServerRegion, client: IClient) { - return writeBinary(writer => writeRegion(writer, region, client)); + return writeBinary(writer => writeRegion(writer, region, client)); } diff --git a/src/ts/common/entities.ts b/src/ts/common/entities.ts index 93c1d9d..7923dad 100644 --- a/src/ts/common/entities.ts +++ b/src/ts/common/entities.ts @@ -1,18 +1,18 @@ import { compact } from 'lodash'; import * as sprites from '../generated/sprites'; import { - Entity, PaletteManager, Rect, ServerFlags, PaletteRenderable, ColorShadow, EntityDescriptor, EntityOptions, - CreateEntityMethod, CreateEntity, EntityFlags, MixinEntity, EntityWorldState, defaultWorldState, ColorExtra, InteractAction + Entity, PaletteManager, Rect, ServerFlags, PaletteRenderable, ColorShadow, EntityDescriptor, EntityOptions, + CreateEntityMethod, CreateEntity, EntityFlags, MixinEntity, EntityWorldState, defaultWorldState, ColorExtra, InteractAction } from './interfaces'; import { tileWidth, tileHeight, ENTITY_TYPE_LIMIT, WATER_FPS, PONY_TYPE, WATER_HEIGHT } from './constants'; import { CLOUD_SHADOW_COLOR, WHITE } from './colors'; import { - mixDrawSpider, setPaletteManager, AnimatedRenderable, - AnimatedRenderable1, collider, taperColliderSW, taperColliderNE, taperColliderSE, - taperColliderNW, skewColliderNE, ponyColliders, ponyCollidersBounds, mixTrigger, mixDraw, mixInteract, - mixPickable, mixMinimap, mixColliderRect, mixColliderRounded, mixLight, mixLightSprite, mixColliders, - mixDrawShadow, mixAnimation, mixBobbing, mixDrawWall, mixDrawRain, mixDrawSeasonal, mixDrawWindow, - mixDrawDirectionSign, skewColliderNW, triangleColliderNE, triangleColliderNW, mixInteractAt + mixDrawSpider, setPaletteManager, AnimatedRenderable, + AnimatedRenderable1, collider, taperColliderSW, taperColliderNE, taperColliderSE, + taperColliderNW, skewColliderNE, ponyColliders, ponyCollidersBounds, mixTrigger, mixDraw, mixInteract, + mixPickable, mixMinimap, mixColliderRect, mixColliderRounded, mixLight, mixLightSprite, mixColliders, + mixDrawShadow, mixAnimation, mixBobbing, mixDrawWall, mixDrawRain, mixDrawSeasonal, mixDrawWindow, + mixDrawDirectionSign, skewColliderNW, triangleColliderNE, triangleColliderNW, mixInteractAt } from './mixins'; import { times, repeat, flatten, hasFlag } from './utils'; import { withAlphaFloat } from './color'; @@ -22,166 +22,166 @@ import { mockPaletteManager } from './ponyInfo'; const entities: EntityDescriptor[] = []; export function createBaseEntity(type: number, id: number, x: number, y: number): Entity { - return { id, type, x, y, z: 0, vx: 0, vy: 0, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0 }; + return { id, type, x, y, z: 0, vx: 0, vy: 0, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0 }; } function createEntity( - type: number, id: number, x: number, y: number, options: EntityOptions, worldState: EntityWorldState + type: number, id: number, x: number, y: number, options: EntityOptions, worldState: EntityWorldState ): Entity { - const descriptor = entities[type]; + const descriptor = entities[type]; - if (!descriptor) { - throw new Error(`Invalid entity type ${type}`); - } + if (!descriptor) { + throw new Error(`Invalid entity type ${type}`); + } - return descriptor.create(createBaseEntity(type, id, x, y), options, worldState); + return descriptor.create(createBaseEntity(type, id, x, y), options, worldState); } function register(typeName: string, create: CreateEntity): CreateEntityMethod { - if (DEVELOPMENT && entities.length >= ENTITY_TYPE_LIMIT) { - throw new Error(`Exceeded entity limit of ${ENTITY_TYPE_LIMIT} with (${typeName})`); - } + if (DEVELOPMENT && entities.length >= ENTITY_TYPE_LIMIT) { + throw new Error(`Exceeded entity limit of ${ENTITY_TYPE_LIMIT} with (${typeName})`); + } - if (DEVELOPMENT && entities.some(e => e.typeName === typeName)) { - throw new Error(`Entity name already registered (${typeName})`); - } + if (DEVELOPMENT && entities.some(e => e.typeName === typeName)) { + throw new Error(`Entity name already registered (${typeName})`); + } - const type = entities.length; - entities.push({ type, typeName, create }); - const method: any = (x: number, y: number, options: EntityOptions = {}, worldState: EntityWorldState = defaultWorldState) => - createEntity(type, 0, x, y, options, worldState); - method.type = type; - method.typeName = typeName; + const type = entities.length; + entities.push({ type, typeName, create }); + const method: any = (x: number, y: number, options: EntityOptions = {}, worldState: EntityWorldState = defaultWorldState) => + createEntity(type, 0, x, y, options, worldState); + method.type = type; + method.typeName = typeName; - return method; + return method; } function registerMix(typeName: string, ...mixins: (MixinEntity | undefined)[]) { - const mixinsCompacted = compact(mixins); + const mixinsCompacted = compact(mixins); - return register(typeName, (base, options, worldState) => { - for (const mixin of mixinsCompacted) { - mixin(base, options, worldState); - } + return register(typeName, (base, options, worldState) => { + for (const mixin of mixinsCompacted) { + mixin(base, options, worldState); + } - return base; - }); + return base; + }); } export function getEntityTypeName(type: number): string { - return entities[type].typeName; + return entities[type].typeName; } export function getEntityType(typeName: string): number { - for (let i = 1; i < entities.length; i++) { - if (entities[i].typeName === typeName) { - return i; - } - } + for (let i = 1; i < entities.length; i++) { + if (entities[i].typeName === typeName) { + return i; + } + } - return 0; + return 0; } export function getEntityTypesAndNames() { - return entities.map(({ type, typeName }) => ({ type, name: typeName })); + return entities.map(({ type, typeName }) => ({ type, name: typeName })); } function checkEntity(entity: Entity) { - if (entity.draw && !entity.bounds) { - console.error('missing bounds for', getEntityTypeName(entity.type), entity); - } + if (entity.draw && !entity.bounds) { + console.error('missing bounds for', getEntityTypeName(entity.type), entity); + } - if (entity.drawLight && !entity.lightBounds) { - console.error('missing lightBounds for', getEntityTypeName(entity.type), entity); - } + if (entity.drawLight && !entity.lightBounds) { + console.error('missing lightBounds for', getEntityTypeName(entity.type), entity); + } - if (entity.drawLightSprite && !entity.lightSpriteBounds) { - console.error('missing lightSpriteBounds for', getEntityTypeName(entity.type), entity); - } + if (entity.drawLightSprite && !entity.lightSpriteBounds) { + console.error('missing lightSpriteBounds for', getEntityTypeName(entity.type), entity); + } } export function createAnEntity( - type: number, id: number, x: number, y: number, options: any, paletteManager: PaletteManager, - worldState: EntityWorldState + type: number, id: number, x: number, y: number, options: any, paletteManager: PaletteManager, + worldState: EntityWorldState ): Entity { - setPaletteManager(paletteManager); + setPaletteManager(paletteManager); - const entity = createEntity(type, id, x, y, options, worldState); + const entity = createEntity(type, id, x, y, options, worldState); - if (DEVELOPMENT) { - checkEntity(entity); - } + if (DEVELOPMENT) { + checkEntity(entity); + } - return entity; + return entity; } // helpers // strips names in release build function n(value: string) { - return (DEVELOPMENT || SERVER) ? value : ''; + return (DEVELOPMENT || SERVER) ? value : ''; } function mixCover(x: number, y: number, w: number, h: number): MixinEntity { - const bounds = rect(x, y, w, h); - return base => base.coverBounds = bounds; + const bounds = rect(x, y, w, h); + return base => base.coverBounds = bounds; } function mixFlags(flags: EntityFlags): MixinEntity { - return base => base.flags |= flags; + return base => base.flags |= flags; } function mixInteractAction(action: InteractAction): MixinEntity { - return base => base.interactAction = action; + return base => base.interactAction = action; } function mixBounds(x: number, y: number, w: number, h: number): MixinEntity { - const bounds = rect(x, y, w, h); - return base => base.bounds = bounds; + const bounds = rect(x, y, w, h); + return base => base.bounds = bounds; } function mixServerFlags(flags: ServerFlags): MixinEntity { - if (SERVER) { - return base => base.serverFlags! |= flags; - } else { - return () => { }; - } + if (SERVER) { + return base => base.serverFlags! |= flags; + } else { + return () => { }; + } } function mixOrder(order: number): MixinEntity { - return base => base.order = order; + return base => base.order = order; } const collectableInteractive = mixInteract(-8, -12, 16, 16, 1.5); function collectable(name: string, sprite: PaletteRenderable, paletteIndex = 0, ...other: MixinEntity[]) { - return doodad(name, sprite, Math.floor(sprite.color!.w / 2), sprite.color!.h - 1, paletteIndex, - collectableInteractive, - ...other); + return doodad(name, sprite, Math.floor(sprite.color!.w / 2), sprite.color!.h - 1, paletteIndex, + collectableInteractive, + ...other); } function decal(name: string, sprite: PaletteRenderable, palette = 0, ...other: MixinEntity[]) { - return registerMix(name, - mixDraw(sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, palette), - mixFlags(EntityFlags.Decal), - ...other); + return registerMix(name, + mixDraw(sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, palette), + mixFlags(EntityFlags.Decal), + ...other); } function decalOffset(name: string, sprite: PaletteRenderable, dx: number, dy: number, palette = 0, ...other: MixinEntity[]) { - return registerMix(name, - mixDraw(sprite, dx, dy, palette), - mixFlags(EntityFlags.Decal), - ...other); + return registerMix(name, + mixDraw(sprite, dx, dy, palette), + mixFlags(EntityFlags.Decal), + ...other); } function doodad( - name: string, sprite: PaletteRenderable, ox: number, oy: number, palatte = 0, ...other: (MixinEntity | undefined)[] + name: string, sprite: PaletteRenderable, ox: number, oy: number, palatte = 0, ...other: (MixinEntity | undefined)[] ) { - return registerMix(name, mixDraw(sprite, ox, oy, palatte), ...compact(other)); + return registerMix(name, mixDraw(sprite, ox, oy, palatte), ...compact(other)); } function doodadSet(name: string, sprite: PaletteRenderable, ox: number, oy: number, ...other: MixinEntity[]) { - return times(sprite.palettes!.length, i => registerMix(`${name}-${i}`, mixDraw(sprite, ox, oy, i), ...other)); + return times(sprite.palettes!.length, i => registerMix(`${name}-${i}`, mixDraw(sprite, ox, oy, i), ...other)); } // placeholder entity @@ -191,42 +191,42 @@ registerMix(n('null'), () => { throw new Error('Invalid type (0)'); }); // entities export const pony = registerMix(n('pony'), - base => { - base.flags = EntityFlags.Movable | EntityFlags.CanCollide; - base.colliders = ponyColliders; - base.collidersBounds = ponyCollidersBounds; - }, - mixServerFlags(ServerFlags.DoNotSave)); + base => { + base.flags = EntityFlags.Movable | EntityFlags.CanCollide; + base.colliders = ponyColliders; + base.collidersBounds = ponyCollidersBounds; + }, + mixServerFlags(ServerFlags.DoNotSave)); // triggers export const triggerDoor = registerMix(n('trigger-door'), - mixTrigger(-32, -6, 64, 12, true)); + mixTrigger(-32, -6, 64, 12, true)); export const triggerHouseDoor = registerMix(n('trigger-house-door'), - mixTrigger(-32, -6, 64, 12, false)); + mixTrigger(-32, -6, 64, 12, false)); export const triggerBoat = registerMix(n('trigger-boat'), - mixTrigger(-50, -12, 100, 24, false)); + mixTrigger(-50, -12, 100, 24, false)); export const trigger3x1 = registerMix(n('trigger-3x1'), - mixTrigger(-48, 0, 96, 24, true)); + mixTrigger(-48, 0, 96, 24, true)); // house export const house = doodad(n('house'), sprites.house, 79, 186, 0, - mixColliders( - collider(-70, -92, 137, 90, true), - collider(-70, -2, 32, 2, true), - collider(-70, 0, 30, 3, true), - collider(0, -2, 67, 2, true), - collider(2, 0, 65, 3, true), - ), - mixInteract(-35, -49, 32, 49, 3)); + mixColliders( + collider(-70, -92, 137, 90, true), + collider(-70, -2, 32, 2, true), + collider(-70, 0, 30, 3, true), + collider(0, -2, 67, 2, true), + collider(2, 0, 65, 3, true), + ), + mixInteract(-35, -49, 32, 49, 3)); export const window1 = registerMix(n('window-1'), - mixDrawWindow(sprites.window_1, 21, 53, 0, 3, 0, 3, 1), - mixOrder(1)); + mixDrawWindow(sprites.window_1, 21, 53, 0, 3, 0, 3, 1), + mixOrder(1)); export const picture1 = doodad(n('picture-1'), sprites.picture_1, 15, 54, 0); export const picture2 = doodad(n('picture-2'), sprites.picture_1, 15, 54, 1); @@ -237,7 +237,7 @@ export const cushion2 = decal(n('cushion-2'), sprites.cushion_1, 1, cushionPicka export const cushion3 = decal(n('cushion-3'), sprites.cushion_1, 2, cushionPickable); export const bookshelf = doodad(n('bookshelf'), sprites.bookshelf, 28, 81, 0, - mixColliderRect(-32, -14, 66, 15)); + mixColliderRect(-32, -14, 66, 15)); // boat @@ -246,59 +246,59 @@ const boatSailCollider = mixColliderRect(-5, -3, 11, 6); const waterBobbing = mixBobbing(WATER_FPS, WATER_HEIGHT); export const boat = doodad(n('boat'), sprites.boat, 95, 4, 0, - boatMinimap, - mixOrder(-1)); + boatMinimap, + mixOrder(-1)); export const boatBob = doodad(n('boat-bob'), sprites.boat, 95, 18, 0, - boatMinimap, - waterBobbing, - mixOrder(-1), - mixFlags(EntityFlags.StaticY)); + boatMinimap, + waterBobbing, + mixOrder(-1), + mixFlags(EntityFlags.StaticY)); export const boatFrontBob = doodad(n('boat-front-bob'), sprites.boat_front, 71, 16, 0, - boatMinimap, - waterBobbing, - mixFlags(EntityFlags.StaticY)); + boatMinimap, + waterBobbing, + mixFlags(EntityFlags.StaticY)); export const boatSail = doodad(n('boat-sail'), sprites.boat_sail, 77, 173, 0, - mixCover(-8, -130, 70, 116), - boatSailCollider); + mixCover(-8, -130, 70, 116), + boatSailCollider); export const rope = doodad(n('rope'), sprites.boat_rope, 5, 19, 0, - mixPickable(31, 58)); + mixPickable(31, 58)); export const ropeRack = doodad(n('rope-rack'), sprites.rope_rack, 11, 34, 0, - mixInteract(-10, -31, 23, 31, 5), - mixFlags(EntityFlags.StaticY)); + mixInteract(-10, -31, 23, 31, 5), + mixFlags(EntityFlags.StaticY)); export const boatRopeBob = doodad(n('boat-rope-bob'), sprites.boat_rope, 5, 19, 0, - waterBobbing, - mixFlags(EntityFlags.StaticY)); + waterBobbing, + mixFlags(EntityFlags.StaticY)); export const boatWake = registerMix(n('boat-wake'), - mixAnimation(sprites.boat_wake, WATER_FPS, 93, 0, { useGameTime: true }), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.boat_wake, WATER_FPS, 93, 0, { useGameTime: true }), + mixFlags(EntityFlags.StaticY)); export function fullBoat(x: number, y: number, sail = true) { - const sailEntities = sail ? [ - boatSail(x - (12 / tileWidth), y + (16 / tileHeight)), - boatRopeBob(x - (91 / tileWidth), y + (8 / tileHeight)), - ] : []; + const sailEntities = sail ? [ + boatSail(x - (12 / tileWidth), y + (16 / tileHeight)), + boatRopeBob(x - (91 / tileWidth), y + (8 / tileHeight)), + ] : []; - return [ - boatBob(x, y), - boatFrontBob(x, y + (29 / tileHeight)), - ...sailEntities, - boatWake(x, y + (5 / tileHeight)), - ]; + return [ + boatBob(x, y), + boatFrontBob(x, y + (29 / tileHeight)), + ...sailEntities, + boatWake(x, y + (5 / tileHeight)), + ]; } // pier export const pierLeg = registerMix(n('pier-leg'), - mixAnimation(sprites.pier_leg, WATER_FPS, 10, -14), - mixOrder(-2), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.pier_leg, WATER_FPS, 10, -14), + mixOrder(-2), + mixFlags(EntityFlags.StaticY)); // planks @@ -315,28 +315,28 @@ export const plank4 = decalOffset(n('plank-4'), sprites.plank_4, 39, -2, plankPa export const planks = [plank1, plank2, plank3, plank4]; export const plankShort1 = decalOffset( - n('plank-short-1'), sprites.plank_short_1, 21, -2, plankPal, plankShortMinimap, plankFlags); + n('plank-short-1'), sprites.plank_short_1, 21, -2, plankPal, plankShortMinimap, plankFlags); export const plankShort2 = decalOffset( - n('plank-short-2'), sprites.plank_short_2, 21, -2, plankPal, plankShortMinimap, plankFlags); + n('plank-short-2'), sprites.plank_short_2, 21, -2, plankPal, plankShortMinimap, plankFlags); export const plankShort3 = decalOffset( - n('plank-short-3'), sprites.plank_short_3, 21, -2, plankPal, plankShortMinimap, plankFlags); + n('plank-short-3'), sprites.plank_short_3, 21, -2, plankPal, plankShortMinimap, plankFlags); export const planksShort = [plankShort1, plankShort2, plankShort3]; export const plankShadow = registerMix(n('plank-shadow'), - mixDrawShadow(sprites.plank_shadow, 39, -12), - mixFlags(EntityFlags.Decal | EntityFlags.StaticY), - mixOrder(-1)); + mixDrawShadow(sprites.plank_shadow, 39, -12), + mixFlags(EntityFlags.Decal | EntityFlags.StaticY), + mixOrder(-1)); export const plankShadow2 = registerMix(n('plank-shadow-2'), - mixDrawShadow(sprites.plank_shadow2, 39, -12), - mixFlags(EntityFlags.Decal | EntityFlags.StaticY), - mixOrder(-1)); + mixDrawShadow(sprites.plank_shadow2, 39, -12), + mixFlags(EntityFlags.Decal | EntityFlags.StaticY), + mixOrder(-1)); export const plankShadowShort = registerMix(n('plank-shadow-short'), - mixDrawShadow(sprites.plank_shadow_short, 21, -12), - mixFlags(EntityFlags.Decal | EntityFlags.StaticY), - mixOrder(-1)); + mixDrawShadow(sprites.plank_shadow_short, 21, -12), + mixFlags(EntityFlags.Decal | EntityFlags.StaticY), + mixOrder(-1)); // pickables @@ -373,27 +373,27 @@ export const grapePurple = collectable(n('grape-purple'), sprites.grapes_one, 0, export const grapeGreen = collectable(n('grape-green'), sprites.grapes_one, 1, mixPickable(29, 43)); function grapes(name: string, sprite: ColorShadow, palette: number) { - return doodad(name, sprite, 5, 15, palette, collectableInteractive, grapesPickable); + return doodad(name, sprite, 5, 15, palette, collectableInteractive, grapesPickable); } export const grapesPurple = [ - grapes(n('grapes-purple-1'), sprites.grapes_1, 0), - grapes(n('grapes-purple-2'), sprites.grapes_2, 0), - grapes(n('grapes-purple-3'), sprites.grapes_3, 0), - grapes(n('grapes-purple-4'), sprites.grapes_4, 0), - grapes(n('grapes-purple-5'), sprites.grapes_5, 0), - grapes(n('grapes-purple-6'), sprites.grapes_6, 0), - grapes(n('grapes-purple-7'), sprites.grapes_7, 0), + grapes(n('grapes-purple-1'), sprites.grapes_1, 0), + grapes(n('grapes-purple-2'), sprites.grapes_2, 0), + grapes(n('grapes-purple-3'), sprites.grapes_3, 0), + grapes(n('grapes-purple-4'), sprites.grapes_4, 0), + grapes(n('grapes-purple-5'), sprites.grapes_5, 0), + grapes(n('grapes-purple-6'), sprites.grapes_6, 0), + grapes(n('grapes-purple-7'), sprites.grapes_7, 0), ]; export const grapesGreen = [ - grapes(n('grapes-green-1'), sprites.grapes_1, 1), - grapes(n('grapes-green-2'), sprites.grapes_2, 1), - grapes(n('grapes-green-3'), sprites.grapes_3, 1), - grapes(n('grapes-green-4'), sprites.grapes_4, 1), - grapes(n('grapes-green-5'), sprites.grapes_5, 1), - grapes(n('grapes-green-6'), sprites.grapes_6, 1), - grapes(n('grapes-green-7'), sprites.grapes_7, 1), + grapes(n('grapes-green-1'), sprites.grapes_1, 1), + grapes(n('grapes-green-2'), sprites.grapes_2, 1), + grapes(n('grapes-green-3'), sprites.grapes_3, 1), + grapes(n('grapes-green-4'), sprites.grapes_4, 1), + grapes(n('grapes-green-5'), sprites.grapes_5, 1), + grapes(n('grapes-green-6'), sprites.grapes_6, 1), + grapes(n('grapes-green-7'), sprites.grapes_7, 1), ]; export const mango = collectable(n('mango'), sprites.mango, 0, mixPickable(31, 46)); @@ -404,44 +404,44 @@ export const cookie = collectable(n('cookie'), sprites.cookie, 0, mixPickable(31 export const cookiePony = collectable(n('cookie-pony'), sprites.cookie_pony, 0, mixPickable(30, 45)); export const cookieTable = doodad(n('cookie-table'), sprites.cookie_table_1, 13, 28, 0, - mixFlags(EntityFlags.Interactive), - base => base.interactRange = 5, - mixInteractAction(InteractAction.GiveCookie1), - mixColliderRect(-13, -12, 26, 14)); + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 5, + mixInteractAction(InteractAction.GiveCookie1), + mixColliderRect(-13, -12, 26, 14)); export const cookieTable2 = doodad(n('cookie-table-2'), sprites.cookie_table_2, 13, 28, 0, - mixFlags(EntityFlags.Interactive), - base => base.interactRange = 5, - mixInteractAction(InteractAction.GiveCookie2), - mixColliderRect(-13, -12, 26, 14)); + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 5, + mixInteractAction(InteractAction.GiveCookie2), + mixColliderRect(-13, -12, 26, 14)); export const letter = doodad(n('letter'), sprites.letter, 4, 10, 0, - mixPickable(30, 50)); + mixPickable(30, 50)); export const rose = doodad(n('rose'), sprites.rose, 8, 1, 0, - mixPickable(30, 41)); + mixPickable(30, 41)); // tools export const hammer = doodad(n('hammer'), sprites.hammer, 8, 10, 0, - mixPickable(25, 46), - mixFlags(EntityFlags.Usable)); + mixPickable(25, 46), + mixFlags(EntityFlags.Usable)); export const shovel = doodad(n('shovel'), sprites.shovel, 16, 6, 0, - mixPickable(29, 42), - mixFlags(EntityFlags.Usable)); + mixPickable(29, 42), + mixFlags(EntityFlags.Usable)); export const rake = doodad(n('rake'), sprites.rake, 16, 6, 0, - mixPickable(30, 42)); + mixPickable(30, 42)); export const pickaxe = doodad(n('pickaxe'), sprites.pickaxe, 10, 8, 0, - mixPickable(28, 42)); + mixPickable(28, 42)); export const broom = doodad(n('broom'), sprites.broom, 16, 6, 0, - mixPickable(27, 42)); + mixPickable(27, 42)); export const saw = doodad(n('saw'), sprites.saw, 10, 7, 0, - mixPickable(26, 47)); + mixPickable(26, 47)); // jacko lanterns @@ -450,88 +450,88 @@ const jackoLanternPickable = mixPickable(31, 52); const jackLanternCollider = mixColliderRect(-5, -5, 10, 10, false); export const jackoLanternOff = collectable( - n('jacko-lantern-off'), sprites.jacko_lantern_off, 0, jackoLanternPickable, jackLanternCollider); + n('jacko-lantern-off'), sprites.jacko_lantern_off, 0, jackoLanternPickable, jackLanternCollider); export const jackoLanternOn = collectable( - n('jacko-lantern-on'), sprites.jacko_lantern_on, 0, jackoLanternPickable, jackLanternCollider, - mixLight(jackoLightColor, 0, 0, 192, 144), - mixLightSprite(sprites.jacko_lantern_light, WHITE, 6, 9)); + n('jacko-lantern-on'), sprites.jacko_lantern_on, 0, jackoLanternPickable, jackLanternCollider, + mixLight(jackoLightColor, 0, 0, 192, 144), + mixLightSprite(sprites.jacko_lantern_light, WHITE, 6, 9)); export const jackoLantern = collectable( - n('jacko-lantern'), sprites.jacko_lantern_on, 0, jackoLanternPickable, jackLanternCollider, - mixLight(jackoLightColor, 0, 0, 192, 144), - mixLightSprite(sprites.jacko_lantern_light, WHITE, 6, 9), - mixFlags(EntityFlags.OnOff)); + n('jacko-lantern'), sprites.jacko_lantern_on, 0, jackoLanternPickable, jackLanternCollider, + mixLight(jackoLightColor, 0, 0, 192, 144), + mixLightSprite(sprites.jacko_lantern_light, WHITE, 6, 9), + mixFlags(EntityFlags.OnOff)); // lanterns const lanternLightSprite: AnimatedRenderable1 = { - frames: sprites.lantern_light.frames, + frames: sprites.lantern_light.frames, }; const lanternPickable = mixPickable(31, 53); const lanternCollider = mixColliderRounded(-5, -5, 10, 10, 2, false); export const lanternOn = registerMix(n('lantern-on'), - mixAnimation(sprites.lantern, 12, 4, 13, { lightSprite: lanternLightSprite }), - mixLight(0x916a32ff, 0, 0, 384, 288), - lanternPickable, - lanternCollider); + mixAnimation(sprites.lantern, 12, 4, 13, { lightSprite: lanternLightSprite }), + mixLight(0x916a32ff, 0, 0, 384, 288), + lanternPickable, + lanternCollider); export const lanternOnWall = registerMix(n('lantern-on-wall'), - mixAnimation(sprites.lantern, 12, 4, 13 + 24, { lightSprite: lanternLightSprite }), - mixLight(0x916a32ff, 0, 0, 384, 288)); + mixAnimation(sprites.lantern, 12, 4, 13 + 24, { lightSprite: lanternLightSprite }), + mixLight(0x916a32ff, 0, 0, 384, 288)); export const lanternOnTable = registerMix(n('lantern-on-table'), - mixAnimation(sprites.lantern, 12, 4, 13 + 14, { lightSprite: lanternLightSprite }), - mixLight(0x916a32ff, 0, 0, 384, 288)); + mixAnimation(sprites.lantern, 12, 4, 13 + 14, { lightSprite: lanternLightSprite }), + mixLight(0x916a32ff, 0, 0, 384, 288)); export const candy = doodad(n('candy'), sprites.candy, 4, 2, 0, - mixInteract(-6, -6, 13, 13, 1.5)); + mixInteract(-6, -6, 13, 13, 1.5)); const eggInteractive = mixInteract(-6, -6, 13, 13, 1.5); export const eggs = [ - // upright - collectable(n('egg-1-0'), sprites.egg_1, 0, eggInteractive), - collectable(n('egg-1-1'), sprites.egg_1, 1, eggInteractive), - collectable(n('egg-1-2'), sprites.egg_1, 2, eggInteractive), - collectable(n('egg-2-0'), sprites.egg_2, 0, eggInteractive), - collectable(n('egg-3-0'), sprites.egg_3, 0, eggInteractive), - collectable(n('egg-3-1'), sprites.egg_3, 3, eggInteractive), - collectable(n('egg-3-2'), sprites.egg_3, 4, eggInteractive), - collectable(n('egg-4-0'), sprites.egg_4, 0, eggInteractive), - collectable(n('egg-5-0'), sprites.egg_5, 0, eggInteractive), - collectable(n('egg-5-1'), sprites.egg_5, 8, eggInteractive), - collectable(n('egg-6-0'), sprites.egg_6, 0, eggInteractive), - collectable(n('egg-6-1'), sprites.egg_6, 5, eggInteractive), - collectable(n('egg-7-0'), sprites.egg_7, 0, eggInteractive), - collectable(n('egg-7-1'), sprites.egg_7, 6, eggInteractive), - collectable(n('egg-8-0'), sprites.egg_8, 0, eggInteractive), - collectable(n('egg-8-1'), sprites.egg_8, 7, eggInteractive), - collectable(n('egg-9-0'), sprites.egg_9, 0, eggInteractive), - collectable(n('egg-10-0'), sprites.egg_10, 0, eggInteractive), - collectable(n('egg-11-0'), sprites.egg_11, 0, eggInteractive), - collectable(n('egg-12-0'), sprites.egg_12, 0, eggInteractive), - collectable(n('egg-13-0'), sprites.egg_13, 0, eggInteractive), - collectable(n('egg-14-0'), sprites.egg_14, 0, eggInteractive), - // tilted - collectable(n('egg-14-1'), sprites.egg_14, 1, eggInteractive), - collectable(n('egg-14-2'), sprites.egg_14, 2, eggInteractive), - collectable(n('egg-15-0'), sprites.egg_15, 0, eggInteractive), - collectable(n('egg-15-1'), sprites.egg_15, 3, eggInteractive), - collectable(n('egg-15-2'), sprites.egg_15, 4, eggInteractive), - collectable(n('egg-16-0'), sprites.egg_16, 0, eggInteractive), - collectable(n('egg-16-1'), sprites.egg_16, 8, eggInteractive), - collectable(n('egg-17-0'), sprites.egg_17, 0, eggInteractive), - collectable(n('egg-17-1'), sprites.egg_17, 5, eggInteractive), - collectable(n('egg-18-0'), sprites.egg_18, 0, eggInteractive), - collectable(n('egg-18-1'), sprites.egg_18, 7, eggInteractive), - collectable(n('egg-19-0'), sprites.egg_19, 0, eggInteractive), - collectable(n('egg-20-0'), sprites.egg_20, 0, eggInteractive), - collectable(n('egg-21-0'), sprites.egg_21, 0, eggInteractive), - collectable(n('egg-22-0'), sprites.egg_22, 0, eggInteractive), - collectable(n('egg-23-0'), sprites.egg_23, 0, eggInteractive), + // upright + collectable(n('egg-1-0'), sprites.egg_1, 0, eggInteractive), + collectable(n('egg-1-1'), sprites.egg_1, 1, eggInteractive), + collectable(n('egg-1-2'), sprites.egg_1, 2, eggInteractive), + collectable(n('egg-2-0'), sprites.egg_2, 0, eggInteractive), + collectable(n('egg-3-0'), sprites.egg_3, 0, eggInteractive), + collectable(n('egg-3-1'), sprites.egg_3, 3, eggInteractive), + collectable(n('egg-3-2'), sprites.egg_3, 4, eggInteractive), + collectable(n('egg-4-0'), sprites.egg_4, 0, eggInteractive), + collectable(n('egg-5-0'), sprites.egg_5, 0, eggInteractive), + collectable(n('egg-5-1'), sprites.egg_5, 8, eggInteractive), + collectable(n('egg-6-0'), sprites.egg_6, 0, eggInteractive), + collectable(n('egg-6-1'), sprites.egg_6, 5, eggInteractive), + collectable(n('egg-7-0'), sprites.egg_7, 0, eggInteractive), + collectable(n('egg-7-1'), sprites.egg_7, 6, eggInteractive), + collectable(n('egg-8-0'), sprites.egg_8, 0, eggInteractive), + collectable(n('egg-8-1'), sprites.egg_8, 7, eggInteractive), + collectable(n('egg-9-0'), sprites.egg_9, 0, eggInteractive), + collectable(n('egg-10-0'), sprites.egg_10, 0, eggInteractive), + collectable(n('egg-11-0'), sprites.egg_11, 0, eggInteractive), + collectable(n('egg-12-0'), sprites.egg_12, 0, eggInteractive), + collectable(n('egg-13-0'), sprites.egg_13, 0, eggInteractive), + collectable(n('egg-14-0'), sprites.egg_14, 0, eggInteractive), + // tilted + collectable(n('egg-14-1'), sprites.egg_14, 1, eggInteractive), + collectable(n('egg-14-2'), sprites.egg_14, 2, eggInteractive), + collectable(n('egg-15-0'), sprites.egg_15, 0, eggInteractive), + collectable(n('egg-15-1'), sprites.egg_15, 3, eggInteractive), + collectable(n('egg-15-2'), sprites.egg_15, 4, eggInteractive), + collectable(n('egg-16-0'), sprites.egg_16, 0, eggInteractive), + collectable(n('egg-16-1'), sprites.egg_16, 8, eggInteractive), + collectable(n('egg-17-0'), sprites.egg_17, 0, eggInteractive), + collectable(n('egg-17-1'), sprites.egg_17, 5, eggInteractive), + collectable(n('egg-18-0'), sprites.egg_18, 0, eggInteractive), + collectable(n('egg-18-1'), sprites.egg_18, 7, eggInteractive), + collectable(n('egg-19-0'), sprites.egg_19, 0, eggInteractive), + collectable(n('egg-20-0'), sprites.egg_20, 0, eggInteractive), + collectable(n('egg-21-0'), sprites.egg_21, 0, eggInteractive), + collectable(n('egg-22-0'), sprites.egg_22, 0, eggInteractive), + collectable(n('egg-23-0'), sprites.egg_23, 0, eggInteractive), ]; const eggBasketPickable = mixPickable(31, 53); @@ -545,9 +545,9 @@ export const eggBasket4 = doodad(n('egg-basket-4'), sprites.egg_basket_4, 6, 13, export const eggBaskets = [basket, eggBasket2, eggBasket3, eggBasket4]; export const basketBin = doodad(n('basket-bin'), sprites.basket_bin, 21, 31, 0, - mixColliderRect(-20, -8, 46, 26), - mixFlags(EntityFlags.Interactive), - base => base.interactRange = 3); + mixColliderRect(-20, -8, 46, 26), + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 3); const signCollider = mixColliderRect(-6, -1, 16, 2); const signInteractive = mixFlags(EntityFlags.Interactive); @@ -556,63 +556,63 @@ const signPickable = mixPickable(32, 62); const signParts = [signCollider, signInteractive, signInteractRange, signPickable]; export const sign = registerMix(n('sign'), - mixDrawSeasonal({ - summer: { sprite: sprites.sign_1, dx: 12, dy: 24, palette: 0 }, - winter: { sprite: sprites.sign_winter, dx: 12, dy: 24, palette: 0 }, - }), - ...signParts); + mixDrawSeasonal({ + summer: { sprite: sprites.sign_1, dx: 12, dy: 24, palette: 0 }, + winter: { sprite: sprites.sign_winter, dx: 12, dy: 24, palette: 0 }, + }), + ...signParts); export const signQuest = doodad(n('sign-quest'), sprites.sign_2, 12, 24, 0, ...signParts); export const signQuestion = doodad(n('sign-question'), sprites.sign_4, 12, 24, 0, ...signParts); export const signDonate = doodad(n('sign-donate'), sprites.sign_3, 12, 24, 0, ...signParts); export const signDebug = doodad(n('sign-debug'), sprites.sign_4, 12, 24, 0, - ...signParts, - mixFlags(EntityFlags.Debug)); + ...signParts, + mixFlags(EntityFlags.Debug)); export const tile = decal(n('tile'), sprites.tile); // direction signs export const enum SignIcon { - Spawn, - Pumpkins, - TownCenter, - PineForest, - Boat, - Mountains, - GiftPile, - Forest, - Lake, - Bridge, - Mines, - Barrels, - Fields, - Carrots, + Spawn, + Pumpkins, + TownCenter, + PineForest, + Boat, + Mountains, + GiftPile, + Forest, + Lake, + Bridge, + Mines, + Barrels, + Fields, + Carrots, } export const directionSign = registerMix(n('direction-sign'), - mixDrawDirectionSign(), - mixColliderRect(-10, -8, 20, 16), - mixFlags(EntityFlags.Interactive), - base => base.interactRange = 10); + mixDrawDirectionSign(), + mixColliderRect(-10, -8, 20, 16), + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 10); export const directionSignLefts = times(5, i => registerMix(n(`direction-sign-left-${i}`), - mixBounds(-10, -59 + i * 11, 14, 10))); + mixBounds(-10, -59 + i * 11, 14, 10))); export const directionSignRights = times(5, i => registerMix(n(`direction-sign-right-${i}`), - mixBounds(-4, -59 + i * 11, 14, 10))); + mixBounds(-4, -59 + i * 11, 14, 10))); export const directionSignUpsLeft = times(5, i => registerMix(n(`direction-sign-up-left-${i}`), - mixBounds(-6, -70 + i * 12, 6, 11))); + mixBounds(-6, -70 + i * 12, 6, 11))); export const directionSignUpsRight = times(5, i => registerMix(n(`direction-sign-up-right-${i}`), - mixBounds(1, -70 + i * 12, 5, 11))); + mixBounds(1, -70 + i * 12, 5, 11))); export const directionSignDownsLeft = times(5, i => registerMix(n(`direction-sign-down-left-${i}`), - mixBounds(-6, -57 + i * 13, 7, 12))); + mixBounds(-6, -57 + i * 13, 7, 12))); export const directionSignDownsRight = times(5, i => registerMix(n(`direction-sign-down-right-${i}`), - mixBounds(0, -57 + i * 13, 5, 12))); + mixBounds(0, -57 + i * 13, 5, 12))); // box @@ -623,42 +623,42 @@ const boxPickable = mixPickable(32, 72); const boxParts = [boxCollider, boxInteractive, boxPickable]; export const box = doodad(n('box'), sprites.box_empty, 16, 32, 0, - ...boxParts); + ...boxParts); export const boxLanterns = doodad(n('box-lanterns'), sprites.box_lanterns, 16, 32, 0, - ...boxParts, - mixInteractAction(InteractAction.GiveLantern)); + ...boxParts, + mixInteractAction(InteractAction.GiveLantern)); export const boxBaskets = doodad(n('box-baskets'), sprites.box_baskets, 16, 32, 0, - ...boxParts); + ...boxParts); export const boxFruits = doodad(n('box-fruits'), sprites.box_fruits, 16, 32, 0, - ...boxParts, - mixInteractAction(InteractAction.GiveFruits)); + ...boxParts, + mixInteractAction(InteractAction.GiveFruits)); export const boxGifts = doodad(n('box-gifts'), sprites.box_gifts, 16, 32, 0, - boxCollider, - boxInteractiveClose, - boxPickable); + boxCollider, + boxInteractiveClose, + boxPickable); const toolboxParts = [ - mixColliderRect(-15, -10, 30, 10), - mixInteractAt(5), - mixPickable(31, 60), + mixColliderRect(-15, -10, 30, 10), + mixInteractAt(5), + mixPickable(31, 60), ]; export const toolboxEmpty = doodad(n('toolbox-empty'), sprites.toolbox_empty, 16, 22, 0, - ...toolboxParts); + ...toolboxParts); export const toolboxFull = doodad(n('toolbox-full'), sprites.toolbox_full, 16, 22, 0, - ...toolboxParts, - base => base.interactRange = 20, - mixInteractAction(InteractAction.Toolbox), - mixFlags(EntityFlags.IgnoreTool)); + ...toolboxParts, + base => base.interactRange = 20, + mixInteractAction(InteractAction.Toolbox), + mixFlags(EntityFlags.IgnoreTool)); export const barrel = doodad(n('barrel'), sprites.barrel, 13, 27, 0, - mixColliderRounded(-12, -8, 24, 13, 5), - mixFlags(EntityFlags.Interactive), - base => base.interactRange = 3); + mixColliderRounded(-12, -8, 24, 13, 5), + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 3); export const bench1 = doodad(n('bench-1'), sprites.bench_1, 37, 20, 0, mixColliderRect(-37, -3, 75, 8)); export const benchSeat = doodad(n('bench-seat'), sprites.bench_seat, 37, 0); @@ -687,95 +687,95 @@ export const crate3B = doodad(n('crate-3b'), sprites.crate_3, 15, 23, 1, mixColl export type Walls = ReturnType; function createWalls(baseName: string, spriteFull: ColorExtra[], spritesHalf: ColorExtra[]) { - function wall(name: string, index: number, ox: number, oy: number, oy2: number, ...other: MixinEntity[]) { - return registerMix(name, - mixDrawWall(spriteFull[index], spritesHalf[index], ox, oy, oy2), - mixMinimap(0x503d45ff, rect(0, 0, 1, 1)), - mixFlags(EntityFlags.StaticY), - mixServerFlags(ServerFlags.DoNotSave), - ...other); - } + function wall(name: string, index: number, ox: number, oy: number, oy2: number, ...other: MixinEntity[]) { + return registerMix(name, + mixDrawWall(spriteFull[index], spritesHalf[index], ox, oy, oy2), + mixMinimap(0x503d45ff, rect(0, 0, 1, 1)), + mixFlags(EntityFlags.StaticY), + mixServerFlags(ServerFlags.DoNotSave), + ...other); + } - function wallShort(name: string, index: number, ox: number, _oy: number, oy2: number, ...other: MixinEntity[]) { - return doodad(name, spritesHalf[index], ox, oy2, 0, - mixMinimap(0x503d45ff, rect(0, 0, 1, 1)), - mixFlags(EntityFlags.StaticY), - mixServerFlags(ServerFlags.DoNotSave), - ...other); - } + function wallShort(name: string, index: number, ox: number, _oy: number, oy2: number, ...other: MixinEntity[]) { + return doodad(name, spritesHalf[index], ox, oy2, 0, + mixMinimap(0x503d45ff, rect(0, 0, 1, 1)), + mixFlags(EntityFlags.StaticY), + mixServerFlags(ServerFlags.DoNotSave), + ...other); + } - const wallThickness = 8; - const wallOffsetX = wallThickness / 2; - const wallOffsetY = 18; - const wallOffsetFullY = 81; - const wallHCollider = mixColliderRect(-16, -6, 32, 6); - const wallVCollider = mixColliderRect(-10, -15, 20, 30); + const wallThickness = 8; + const wallOffsetX = wallThickness / 2; + const wallOffsetY = 18; + const wallOffsetFullY = 81; + const wallHCollider = mixColliderRect(-16, -6, 32, 6); + const wallVCollider = mixColliderRect(-10, -15, 20, 30); - const wallH = wall( - n(`${baseName}-h`), 16, (32 - wallThickness) / 2, wallOffsetFullY, wallOffsetY, wallHCollider); + const wallH = wall( + n(`${baseName}-h`), 16, (32 - wallThickness) / 2, wallOffsetFullY, wallOffsetY, wallHCollider); - const wallHShort = wallShort( - n(`${baseName}-h-short`), 16, (32 - wallThickness) / 2, wallOffsetFullY, wallOffsetY, wallHCollider); + const wallHShort = wallShort( + n(`${baseName}-h-short`), 16, (32 - wallThickness) / 2, wallOffsetFullY, wallOffsetY, wallHCollider); - const wallV = wall( - n(`${baseName}-v`), 17, wallOffsetX, wallOffsetFullY + 3, wallOffsetY + 3, wallVCollider); + const wallV = wall( + n(`${baseName}-v`), 17, wallOffsetX, wallOffsetFullY + 3, wallOffsetY + 3, wallVCollider); - const wallVShort = wallShort( - n(`${baseName}-v-short`), 17, wallOffsetX, wallOffsetFullY + 3, wallOffsetY + 3, wallVCollider); + const wallVShort = wallShort( + n(`${baseName}-v-short`), 17, wallOffsetX, wallOffsetFullY + 3, wallOffsetY + 3, wallVCollider); - const wallCutL = doodad( - n(`${baseName}-cut-l`), spriteFull[18], (32 - wallThickness) / 2, wallOffsetFullY, 0, - wallHCollider, - mixFlags(EntityFlags.StaticY), - mixServerFlags(ServerFlags.DoNotSave)); + const wallCutL = doodad( + n(`${baseName}-cut-l`), spriteFull[18], (32 - wallThickness) / 2, wallOffsetFullY, 0, + wallHCollider, + mixFlags(EntityFlags.StaticY), + mixServerFlags(ServerFlags.DoNotSave)); - const wallCutR = doodad( - n(`${baseName}-cut-r`), spriteFull[19], (32 - wallThickness) / 2, wallOffsetFullY, 0, - wallHCollider, - mixFlags(EntityFlags.StaticY), - mixServerFlags(ServerFlags.DoNotSave)); + const wallCutR = doodad( + n(`${baseName}-cut-r`), spriteFull[19], (32 - wallThickness) / 2, wallOffsetFullY, 0, + wallHCollider, + mixFlags(EntityFlags.StaticY), + mixServerFlags(ServerFlags.DoNotSave)); - const wallCorners = [ - // top right bottom left - wall(n(`${baseName}-00`), 0, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 0 - wall(n(`${baseName}-01`), 1, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 1 - wall(n(`${baseName}-02`), 2, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 0 - wall(n(`${baseName}-03`), 3, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 1 - wall(n(`${baseName}-04`), 4, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 0 - wall(n(`${baseName}-05`), 5, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 1 - wall(n(`${baseName}-06`), 6, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 0 - wall(n(`${baseName}-07`), 7, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 1 - wall(n(`${baseName}-08`), 8, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 0 - wall(n(`${baseName}-09`), 9, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 1 - wall(n(`${baseName}-10`), 10, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 0 - wall(n(`${baseName}-11`), 11, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 1 - wall(n(`${baseName}-12`), 12, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 0 - wall(n(`${baseName}-13`), 13, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 1 - wall(n(`${baseName}-14`), 14, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 0 - wall(n(`${baseName}-15`), 15, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 1 - ]; + const wallCorners = [ + // top right bottom left + wall(n(`${baseName}-00`), 0, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 0 + wall(n(`${baseName}-01`), 1, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 1 + wall(n(`${baseName}-02`), 2, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 0 + wall(n(`${baseName}-03`), 3, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 1 + wall(n(`${baseName}-04`), 4, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 0 + wall(n(`${baseName}-05`), 5, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 1 + wall(n(`${baseName}-06`), 6, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 0 + wall(n(`${baseName}-07`), 7, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 1 + wall(n(`${baseName}-08`), 8, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 0 + wall(n(`${baseName}-09`), 9, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 1 + wall(n(`${baseName}-10`), 10, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 0 + wall(n(`${baseName}-11`), 11, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 1 + wall(n(`${baseName}-12`), 12, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 0 + wall(n(`${baseName}-13`), 13, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 1 + wall(n(`${baseName}-14`), 14, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 0 + wall(n(`${baseName}-15`), 15, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 1 + ]; - const wallCornersShort = [ - // top right bottom left - wallShort(n(`${baseName}-00-short`), 0, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 0 - wallShort(n(`${baseName}-01-short`), 1, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 1 - wallShort(n(`${baseName}-02-short`), 2, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 0 - wallShort(n(`${baseName}-03-short`), 3, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 1 - wallShort(n(`${baseName}-04-short`), 4, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 0 - wallShort(n(`${baseName}-05-short`), 5, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 1 - wallShort(n(`${baseName}-06-short`), 6, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 0 - wallShort(n(`${baseName}-07-short`), 7, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 1 - wallShort(n(`${baseName}-08-short`), 8, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 0 - wallShort(n(`${baseName}-09-short`), 9, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 1 - wallShort(n(`${baseName}-10-short`), 10, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 0 - wallShort(n(`${baseName}-11-short`), 11, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 1 - wallShort(n(`${baseName}-12-short`), 12, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 0 - wallShort(n(`${baseName}-13-short`), 13, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 1 - wallShort(n(`${baseName}-14-short`), 14, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 0 - wallShort(n(`${baseName}-15-short`), 15, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 1 - ]; + const wallCornersShort = [ + // top right bottom left + wallShort(n(`${baseName}-00-short`), 0, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 0 + wallShort(n(`${baseName}-01-short`), 1, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 0 1 + wallShort(n(`${baseName}-02-short`), 2, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 0 + wallShort(n(`${baseName}-03-short`), 3, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 0 1 1 + wallShort(n(`${baseName}-04-short`), 4, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 0 + wallShort(n(`${baseName}-05-short`), 5, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 0 1 + wallShort(n(`${baseName}-06-short`), 6, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 0 + wallShort(n(`${baseName}-07-short`), 7, wallOffsetX, wallOffsetFullY, wallOffsetY), // 0 1 1 1 + wallShort(n(`${baseName}-08-short`), 8, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 0 + wallShort(n(`${baseName}-09-short`), 9, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 0 1 + wallShort(n(`${baseName}-10-short`), 10, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 0 + wallShort(n(`${baseName}-11-short`), 11, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 0 1 1 + wallShort(n(`${baseName}-12-short`), 12, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 0 + wallShort(n(`${baseName}-13-short`), 13, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 0 1 + wallShort(n(`${baseName}-14-short`), 14, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 0 + wallShort(n(`${baseName}-15-short`), 15, wallOffsetX, wallOffsetFullY, wallOffsetY), // 1 1 1 1 + ]; - return { wallH, wallHShort, wallV, wallVShort, wallCutL, wallCutR, wallCorners, wallCornersShort }; + return { wallH, wallHShort, wallV, wallVShort, wallCutL, wallCutR, wallCorners, wallCornersShort }; } export const woodenWalls = createWalls('wall', sprites.wall_wood_full, sprites.wall_wood_half); @@ -784,127 +784,127 @@ export const stoneWalls = createWalls('wall-stone', sprites.wall_stone_full, spr const rockMinimap = mixMinimap(0x78716aff, rect(0, 0, 1, 1)); export const rock = doodad(n('rock'), sprites.rock_1, 15, 20, 0, - mixColliderRounded(-16, -12, 32, 12, 3, false), - rockMinimap); + mixColliderRounded(-16, -12, 32, 12, 3, false), + rockMinimap); export const rock2 = doodad(n('rock-2'), sprites.rock_2, 11, 11, 0, - mixColliderRounded(-10, -4, 17, 5, 2, false), - rockMinimap); + mixColliderRounded(-10, -4, 17, 5, 2, false), + rockMinimap); export const rock3 = doodad(n('rock-3'), sprites.rock_3, 10, 11, 0, - mixColliderRounded(-10, -4, 18, 5, 2, false), - rockMinimap); + mixColliderRounded(-10, -4, 18, 5, 2, false), + rockMinimap); export const rockB = doodad(n('rockb'), sprites.rock_1, 15, 20, 1, - mixColliderRounded(-16, -12, 32, 12, 3, false), - rockMinimap); + mixColliderRounded(-16, -12, 32, 12, 3, false), + rockMinimap); export const rock2B = doodad(n('rock-2b'), sprites.rock_2, 11, 11, 1, - mixColliderRounded(-10, -4, 17, 5, 2, false), - rockMinimap); + mixColliderRounded(-10, -4, 17, 5, 2, false), + rockMinimap); export const rock3B = doodad(n('rock-3b'), sprites.rock_3, 10, 11, 1, - mixColliderRounded(-10, -4, 18, 5, 2, false), - rockMinimap); + mixColliderRounded(-10, -4, 18, 5, 2, false), + rockMinimap); // other export const well = doodad(n('well'), sprites.well, 30, 67, 0, - mixColliderRect(-26, -20, 54, 30)); + mixColliderRect(-26, -20, 54, 30)); // water rocks const waterRockFPS = WATER_FPS; export const waterRock1 = registerMix(n('water-rock-1'), - mixAnimation(sprites.water_rock_1, waterRockFPS, 10, 12), - mixColliderRounded(-12, -6, 25, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_1, waterRockFPS, 10, 12), + mixColliderRounded(-12, -6, 25, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock2 = registerMix(n('water-rock-2'), - mixAnimation(sprites.water_rock_2, waterRockFPS, 11, 8), - mixColliderRounded(-12, -5, 22, 8, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_2, waterRockFPS, 11, 8), + mixColliderRounded(-12, -5, 22, 8, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock3 = registerMix(n('water-rock-3'), - mixAnimation(sprites.water_rock_3, waterRockFPS, 12, 9), - mixColliderRounded(-12, -4, 22, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_3, waterRockFPS, 12, 9), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock4 = registerMix(n('water-rock-4'), - mixAnimation(sprites.water_rock_4, waterRockFPS, 11, 12), - mixColliderRounded(-10, -4, 18, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_4, waterRockFPS, 11, 12), + mixColliderRounded(-10, -4, 18, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock5 = registerMix(n('water-rock-5'), - mixAnimation(sprites.water_rock_5, waterRockFPS, 11, 11), - mixColliderRounded(-12, -4, 22, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_5, waterRockFPS, 11, 11), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock6 = registerMix(n('water-rock-6'), - mixAnimation(sprites.water_rock_6, waterRockFPS, 13, 11), - mixColliderRounded(-12, -4, 22, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_6, waterRockFPS, 13, 11), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock7 = registerMix(n('water-rock-7'), - mixAnimation(sprites.water_rock_7, waterRockFPS, 10, 10), - mixColliderRounded(-12, -4, 22, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_7, waterRockFPS, 10, 10), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock8 = registerMix(n('water-rock-8'), - mixAnimation(sprites.water_rock_8, waterRockFPS, 11, 9), - mixColliderRounded(-12, -4, 22, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_8, waterRockFPS, 11, 9), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock9 = registerMix(n('water-rock-9'), - mixAnimation(sprites.water_rock_9, waterRockFPS, 10, 15), - mixColliderRounded(-12, -4, 22, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_9, waterRockFPS, 10, 15), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock10 = registerMix(n('water-rock-10'), - mixAnimation(sprites.water_rock_10, waterRockFPS, 10, 12), - mixColliderRounded(-12, -4, 22, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_10, waterRockFPS, 10, 12), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterRock11 = registerMix(n('water-rock-11'), - mixAnimation(sprites.water_rock_11, waterRockFPS, 10, 13), - mixColliderRounded(-12, -4, 22, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_rock_11, waterRockFPS, 10, 13), + mixColliderRounded(-12, -4, 22, 7, 2, false), + mixFlags(EntityFlags.StaticY)); // stone wall (old) export const stoneWallFull = doodad(n('stone-wall-full'), sprites.stone_wall_full, 38, 22, 0, - mixColliderRect(-38, -4, 76, 6)); + mixColliderRect(-38, -4, 76, 6)); // stone wall const stoneWallMinimapColor = 0x9b9977ff; export const stoneWallPole1 = registerMix(n('stone-wall-pole-1'), - mixDrawSeasonal({ - summer: { sprite: sprites.stone_wall_pole1, dx: 7, dy: 20, palette: 0 }, - winter: { sprite: sprites.stone_wall_winter_pole1, dx: 8, dy: 21, palette: 0 }, - }), - mixColliderRect(-7, -6, 14, 12, false), - mixMinimap(stoneWallMinimapColor, rect(0, 0, 1, 1)), - mixOrder(1)); + mixDrawSeasonal({ + summer: { sprite: sprites.stone_wall_pole1, dx: 7, dy: 20, palette: 0 }, + winter: { sprite: sprites.stone_wall_winter_pole1, dx: 8, dy: 21, palette: 0 }, + }), + mixColliderRect(-7, -6, 14, 12, false), + mixMinimap(stoneWallMinimapColor, rect(0, 0, 1, 1)), + mixOrder(1)); export const stoneWallBeamH1 = registerMix(n('stone-wall-beam-h-1'), - mixDrawSeasonal({ - summer: { sprite: sprites.stone_wall_horizontal1, dx: 25, dy: 17, palette: 0 }, - winter: { sprite: sprites.stone_wall_winter_horizontal1, dx: 25, dy: 20, palette: 0 }, - }), - mixColliderRect(-25, -6, 50, 12, false), - mixMinimap(stoneWallMinimapColor, rect(0, 0, 2, 1))); + mixDrawSeasonal({ + summer: { sprite: sprites.stone_wall_horizontal1, dx: 25, dy: 17, palette: 0 }, + winter: { sprite: sprites.stone_wall_winter_horizontal1, dx: 25, dy: 20, palette: 0 }, + }), + mixColliderRect(-25, -6, 50, 12, false), + mixMinimap(stoneWallMinimapColor, rect(0, 0, 2, 1))); export const stoneWallBeamV1 = registerMix(n('stone-wall-beam-v-1'), - mixDrawSeasonal({ - summer: { sprite: sprites.stone_wall_vertical1, dx: 5, dy: 8, palette: 0 }, - winter: { sprite: sprites.stone_wall_winter_vertical1, dx: 5, dy: 8, palette: 0 }, - }), - mixColliderRect(-7, 0, 14, 48, false), - mixMinimap(stoneWallMinimapColor, rect(0, 0, 1, 2)), - mixOrder(2)); + mixDrawSeasonal({ + summer: { sprite: sprites.stone_wall_vertical1, dx: 5, dy: 8, palette: 0 }, + winter: { sprite: sprites.stone_wall_winter_vertical1, dx: 5, dy: 8, palette: 0 }, + }), + mixColliderRect(-7, 0, 14, 48, false), + mixMinimap(stoneWallMinimapColor, rect(0, 0, 1, 2)), + mixOrder(2)); // wooden fence (modular) @@ -913,107 +913,107 @@ const woodenFenceMinimapColor = 0xac7146ff; const woodenFenceMinimap = mixMinimap(woodenFenceMinimapColor, rect(0, 0, 1, 1)); function woodenFencePole(name: string, sprite: PaletteRenderable, spriteWinter: PaletteRenderable) { - return registerMix(name, - mixDrawSeasonal({ - summer: { sprite: sprite, dx: 4, dy: 25, palette: 0 }, - winter: { sprite: spriteWinter, dx: 4, dy: 26, palette: 0 }, - }), - mixColliderRect(-4, -3, 8, 6, woodenFenceTall), - woodenFenceMinimap, - mixOrder(1)); + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite: sprite, dx: 4, dy: 25, palette: 0 }, + winter: { sprite: spriteWinter, dx: 4, dy: 26, palette: 0 }, + }), + mixColliderRect(-4, -3, 8, 6, woodenFenceTall), + woodenFenceMinimap, + mixOrder(1)); } function woodenFenceBeamH(name: string, sprite: PaletteRenderable, spriteWinter: PaletteRenderable) { - return registerMix(name, - mixDrawSeasonal({ - summer: { sprite: sprite, dx: 12, dy: 21, palette: 0 }, - winter: { sprite: spriteWinter, dx: 12, dy: 23, palette: 0 }, - }), - mixColliderRect(-12, -3, 24, 6, woodenFenceTall), - woodenFenceMinimap); + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite: sprite, dx: 12, dy: 21, palette: 0 }, + winter: { sprite: spriteWinter, dx: 12, dy: 23, palette: 0 }, + }), + mixColliderRect(-12, -3, 24, 6, woodenFenceTall), + woodenFenceMinimap); } function woodenFenceBeamV(name: string, sprite: PaletteRenderable, spriteWinter: PaletteRenderable) { - return registerMix(name, - mixDrawSeasonal({ - summer: { sprite: sprite, dx: 2, dy: 18, palette: 0 }, - winter: { sprite: spriteWinter, dx: 2, dy: 18, palette: 0 }, - }), - mixColliderRect(-4, 0, 8, 24, woodenFenceTall), - woodenFenceMinimap, - mixOrder(2)); + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite: sprite, dx: 2, dy: 18, palette: 0 }, + winter: { sprite: spriteWinter, dx: 2, dy: 18, palette: 0 }, + }), + mixColliderRect(-4, 0, 8, 24, woodenFenceTall), + woodenFenceMinimap, + mixOrder(2)); } export const spawnPole = doodad(n('spawn-pole'), sprites.wooden_fence_pole1, 4, 25, 1, mixFlags(EntityFlags.Debug)); export const routePole = doodad(n('route-pole'), sprites.route_pole, 2, 14, 1, mixFlags(EntityFlags.Debug)); export const woodenFencePole1 = woodenFencePole(n('wooden-fence-pole-1'), - sprites.wooden_fence_pole1, sprites.wooden_fence_winter_pole1); + sprites.wooden_fence_pole1, sprites.wooden_fence_winter_pole1); export const woodenFencePole2 = woodenFencePole(n('wooden-fence-pole-2'), - sprites.wooden_fence_pole2, sprites.wooden_fence_winter_pole2); + sprites.wooden_fence_pole2, sprites.wooden_fence_winter_pole2); export const woodenFencePole3 = woodenFencePole(n('wooden-fence-pole-3'), - sprites.wooden_fence_pole3, sprites.wooden_fence_winter_pole3); + sprites.wooden_fence_pole3, sprites.wooden_fence_winter_pole3); export const woodenFencePole4 = woodenFencePole(n('wooden-fence-pole-4'), - sprites.wooden_fence_pole4, sprites.wooden_fence_winter_pole4); + sprites.wooden_fence_pole4, sprites.wooden_fence_winter_pole4); export const woodenFencePole5 = woodenFencePole(n('wooden-fence-pole-5'), - sprites.wooden_fence_pole5, sprites.wooden_fence_winter_pole5); + sprites.wooden_fence_pole5, sprites.wooden_fence_winter_pole5); export const woodenFenceBeamH1 = woodenFenceBeamH(n('wooden-fence-beam-h-1'), - sprites.wooden_fence_horizontal1, sprites.wooden_fence_winter_horizontal1); + sprites.wooden_fence_horizontal1, sprites.wooden_fence_winter_horizontal1); export const woodenFenceBeamH2 = woodenFenceBeamH(n('wooden-fence-beam-h-2'), - sprites.wooden_fence_horizontal2, sprites.wooden_fence_winter_horizontal2); + sprites.wooden_fence_horizontal2, sprites.wooden_fence_winter_horizontal2); export const woodenFenceBeamH3 = woodenFenceBeamH(n('wooden-fence-beam-h-3'), - sprites.wooden_fence_horizontal3, sprites.wooden_fence_winter_horizontal3); + sprites.wooden_fence_horizontal3, sprites.wooden_fence_winter_horizontal3); export const woodenFenceBeamH4 = woodenFenceBeamH(n('wooden-fence-beam-h-4'), - sprites.wooden_fence_horizontal4, sprites.wooden_fence_winter_horizontal4); + sprites.wooden_fence_horizontal4, sprites.wooden_fence_winter_horizontal4); export const woodenFenceBeamH5 = woodenFenceBeamH(n('wooden-fence-beam-h-5'), - sprites.wooden_fence_horizontal5, sprites.wooden_fence_winter_horizontal5); + sprites.wooden_fence_horizontal5, sprites.wooden_fence_winter_horizontal5); export const woodenFenceBeamH6 = woodenFenceBeamH(n('wooden-fence-beam-h-6'), - sprites.wooden_fence_horizontal6, sprites.wooden_fence_winter_horizontal6); + sprites.wooden_fence_horizontal6, sprites.wooden_fence_winter_horizontal6); export const woodenFenceBeamV1 = woodenFenceBeamV(n('wooden-fence-beam-v-1'), - sprites.wooden_fence_vertical1, sprites.wooden_fence_winter_vertical1); + sprites.wooden_fence_vertical1, sprites.wooden_fence_winter_vertical1); export const woodenFenceBeamV2 = woodenFenceBeamV(n('wooden-fence-beam-v-2'), - sprites.wooden_fence_vertical2, sprites.wooden_fence_winter_vertical2); + sprites.wooden_fence_vertical2, sprites.wooden_fence_winter_vertical2); export const woodenFenceBeamV3 = woodenFenceBeamV(n('wooden-fence-beam-v-3'), - sprites.wooden_fence_vertical3, sprites.wooden_fence_winter_vertical3); + sprites.wooden_fence_vertical3, sprites.wooden_fence_winter_vertical3); // fence export const fence1 = registerMix(n('fence-1'), - mixDrawSeasonal({ - summer: { sprite: sprites.fence_1, dx: 40, dy: 25, palette: 0 }, - winter: { sprite: sprites.fence_winter_1, dx: 40, dy: 25, palette: 0 }, - }), - mixColliderRect(-38, -2, 83, 4, false), - mixMinimap(woodenFenceMinimapColor, rect(0, 0, 1, 1)), - mixPickable(30, 62)); + mixDrawSeasonal({ + summer: { sprite: sprites.fence_1, dx: 40, dy: 25, palette: 0 }, + winter: { sprite: sprites.fence_winter_1, dx: 40, dy: 25, palette: 0 }, + }), + mixColliderRect(-38, -2, 83, 4, false), + mixMinimap(woodenFenceMinimapColor, rect(0, 0, 1, 1)), + mixPickable(30, 62)); export const fence2 = registerMix(n('fence-2'), - mixDrawSeasonal({ - summer: { sprite: sprites.fence_2, dx: 72, dy: 25, palette: 0 }, - winter: { sprite: sprites.fence_winter_2, dx: 72, dy: 25, palette: 0 }, - }), - mixColliderRect(-70, -2, 148, 4, false), - mixMinimap(woodenFenceMinimapColor, rect(0, 0, 2, 1))); + mixDrawSeasonal({ + summer: { sprite: sprites.fence_2, dx: 72, dy: 25, palette: 0 }, + winter: { sprite: sprites.fence_winter_2, dx: 72, dy: 25, palette: 0 }, + }), + mixColliderRect(-70, -2, 148, 4, false), + mixMinimap(woodenFenceMinimapColor, rect(0, 0, 2, 1))); export const fence3 = registerMix(n('fence-3'), - mixDrawSeasonal({ - summer: { sprite: sprites.fence_3, dx: 104, dy: 25, palette: 0 }, - winter: { sprite: sprites.fence_winter_3, dx: 104, dy: 25, palette: 0 }, - }), - mixColliderRect(-102, -2, 204, 4, false), - mixMinimap(woodenFenceMinimapColor, rect(0, 0, 3, 1))); + mixDrawSeasonal({ + summer: { sprite: sprites.fence_3, dx: 104, dy: 25, palette: 0 }, + winter: { sprite: sprites.fence_winter_3, dx: 104, dy: 25, palette: 0 }, + }), + mixColliderRect(-102, -2, 204, 4, false), + mixMinimap(woodenFenceMinimapColor, rect(0, 0, 3, 1))); // rain const rainColor = 0xffffff77; // 48 export const rain = registerMix(n('rain'), - mixAnimation(sprites.rain, 12, 16, 512, { color: rainColor })); + mixAnimation(sprites.rain, 12, 16, 512, { color: rainColor })); export const raindrop = registerMix(n('raindrop'), - mixAnimation(sprites.raindrop, 12, 4, 0, { color: rainColor })); + mixAnimation(sprites.raindrop, 12, 4, 0, { color: rainColor })); export const weatherRain = registerMix(n('weather-rain'), mixDrawRain()); @@ -1032,10 +1032,10 @@ export const flowerPatch6 = decal(n('flowers-6'), sprites.flower_patch6); export const flowerPatch7 = decal(n('flowers-7'), sprites.flower_patch7); export const flower3Pickable = decal(n('flower-3-pickable'), sprites.flower_3, 0, - mixInteract(-7, -3, 15, 15, 1.5)); + mixInteract(-7, -3, 15, 15, 1.5)); export const flowerPick = decal(n('flower-pick'), sprites.flower_pick, 0, - mixPickable(31, 39)); + mixPickable(31, 39)); // clovers @@ -1045,7 +1045,7 @@ export const clover3 = decal(n('clover-3'), sprites.clover_3); export const clover4 = decal(n('clover-4'), sprites.clover_5); export const fourLeafClover = decal(n('four-leaf-clover'), sprites.clover_4, 0, - mixInteract(-7, -3, 15, 15, 1.5)); + mixInteract(-7, -3, 15, 15, 1.5)); export const cloverPatch3 = decal(n('clovers-3'), sprites.clover_patch3); export const cloverPatch4 = decal(n('clovers-4'), sprites.clover_patch4); @@ -1054,10 +1054,10 @@ export const cloverPatch6 = decal(n('clovers-6'), sprites.clover_patch6); export const cloverPatch7 = decal(n('clovers-7'), sprites.clover_patch7); export const cloverPick = doodad(n('clover-pick'), sprites.clover_mouth, 5, 0, 0, - mixPickable(29, 39)); + mixPickable(29, 39)); export const cloverPick2 = doodad(n('clover-pick-2'), sprites.clover_pick, 5, 0, 0, - mixPickable(31, 39)); + mixPickable(31, 39)); // autumn @@ -1072,22 +1072,22 @@ const mediumLeafPileCollider = mixColliderRect(-16, -8, 34, 15); const bigLeafPileCollider = mixColliderRect(-30, -13, 60, 24); export const [leafpileSmallYellow, leafpileSmallOrange, leafpileSmallRed] - = doodadSet(n('leafpile-small'), sprites.leafpile_small, 18, 16, smallLeafPileCollider); + = doodadSet(n('leafpile-small'), sprites.leafpile_small, 18, 16, smallLeafPileCollider); export const [leafpileStickYellow, leafpileStickOrange, leafpileStickRed] - = doodadSet(n('leafpile-stick'), sprites.leafpile_stick, 18, 16, smallLeafPileCollider); + = doodadSet(n('leafpile-stick'), sprites.leafpile_stick, 18, 16, smallLeafPileCollider); export const [leafpileMediumYellow, leafpileMediumOrange, leafpileMediumRed] - = doodadSet(n('leafpile-medium'), sprites.leafpile_medium, 35, 23, mediumLeafPileCollider); + = doodadSet(n('leafpile-medium'), sprites.leafpile_medium, 35, 23, mediumLeafPileCollider); export const [leafpileMediumAltYellow, leafpileMediumAltOrange, leafpileMediumAltRed] - = doodadSet(n('leafpile-mediumalt'), sprites.leafpile_mediumalt, 35, 23, mediumLeafPileCollider); + = doodadSet(n('leafpile-mediumalt'), sprites.leafpile_mediumalt, 35, 23, mediumLeafPileCollider); export const [leafpileBigYellow, leafpileBigOrange, leafpileBigRed] - = doodadSet(n('leafpile-big'), sprites.leafpile_big, 43, 34, bigLeafPileCollider); + = doodadSet(n('leafpile-big'), sprites.leafpile_big, 43, 34, bigLeafPileCollider); export const [leafpileBigstickYellow, leafpileBigstickOrange, leafpileBigstickRed] - = doodadSet(n('leafpile-bigstick'), sprites.leafpile_bigstick, 43, 34, bigLeafPileCollider); + = doodadSet(n('leafpile-bigstick'), sprites.leafpile_bigstick, 43, 34, bigLeafPileCollider); // gifts @@ -1097,27 +1097,27 @@ const giftOffsetX = 7; const giftOffsetY = 15; export const gift1 = doodad(n('gift-1'), sprites.gift_1, giftOffsetX, giftOffsetY, 0, - giftInteractive, - giftPickable, - mixFlags(EntityFlags.Usable)); + giftInteractive, + giftPickable, + mixFlags(EntityFlags.Usable)); export const gift2 = doodad(n('gift-2'), sprites.gift_2, giftOffsetX, giftOffsetY, 0, - giftInteractive, - giftPickable, - mixFlags(EntityFlags.Usable)); + giftInteractive, + giftPickable, + mixFlags(EntityFlags.Usable)); export const gift3 = doodad(n('gift-3'), sprites.gift_2, giftOffsetX, giftOffsetY, 1, - giftInteractive, - giftPickable); + giftInteractive, + giftPickable); // gift piles export const giftPileSign = doodad(n('giftpile-sign'), sprites.giftpile_sign, 47, 39, 0, - mixColliderRounded(-44, -21, 89, 55, 7)); + mixColliderRounded(-44, -21, 89, 55, 7)); export const giftPileTree = doodad(n('giftpile-tree'), sprites.giftpile_tree, 42, 21, 0, - mixColliderRounded(-41, -12, 83, 28, 7)); + mixColliderRounded(-41, -12, 83, 28, 7)); export const giftPilePine = doodad(n('giftpile-pine'), sprites.giftpile_pine, 56, 24, 0, - mixColliderRounded(-51, -19, 102, 40, 7)); + mixColliderRounded(-51, -19, 102, 40, 7)); export const giftPile1 = doodad(n('giftpile-1'), sprites.giftpile_1, 28, 26, 0, mixColliderRect(-28, -12, 57, 33)); export const giftPile2 = doodad(n('giftpile-2'), sprites.giftpile_2, 30, 27, 0, mixColliderRect(-28, -12, 57, 31)); @@ -1127,9 +1127,9 @@ export const giftPile5 = doodad(n('giftpile-5'), sprites.giftpile_5, 20, 20, 0, export const giftPile6 = doodad(n('giftpile-6'), sprites.giftpile_6, 19, 23, 0, mixColliderRect(-19, -12, 38, 28)); export const giftPileInteractive = doodad(n('giftpile-5-interactive'), sprites.giftpile_5, 20, 20, 0, - mixColliderRect(-19, -7, 42, 19), - mixFlags(EntityFlags.Interactive), - base => base.interactRange = 5); + mixColliderRect(-19, -7, 42, 19), + mixFlags(EntityFlags.Interactive), + base => base.interactRange = 5); // winter @@ -1147,34 +1147,34 @@ export const mistletoe = doodad(n('mistletoe'), sprites.mistletoe, 5, 65); export const holly = doodad(n('holly'), sprites.holly, 5, 25, 0, mixOrder(1)); export const snowponies = [ - snowpony1, - snowpony2, - snowpony3, - snowpony4, - snowpony5, - snowpony6, - snowpony7, - snowpony8, - snowpony9, + snowpony1, + snowpony2, + snowpony3, + snowpony4, + snowpony5, + snowpony6, + snowpony7, + snowpony8, + snowpony9, ]; export const snowPileTinier = decal(n('snowpile-tinier'), sprites.snowpile_tinier); export const snowPileTiny = decal(n('snowpile-tiny'), sprites.snowpile_tiny); export const snowPileSmall = doodad(n('snowpile-small'), sprites.snowpile_small, 22, 15, 0, - mixColliderRounded(-15, -4, 31, 7, 2, false)); + mixColliderRounded(-15, -4, 31, 7, 2, false)); export const snowPileMedium = doodad(n('snowpile-medium'), sprites.snowpile_medium, 33, 20, 0, - mixColliderRounded(-23, -4, 46, 12, 4, false)); + mixColliderRounded(-23, -4, 46, 12, 4, false)); export const snowPileBig = doodad(n('snowpile-big'), sprites.snowpile_big, 43, 28, 0, - mixColliderRounded(-39, -2, 78, 16, 6, false)); + mixColliderRounded(-39, -2, 78, 16, 6, false)); export const sandPileTinier = decal(n('sandpile-tinier'), sprites.snowpile_tinier, 1); export const sandPileTiny = decal(n('sandpile-tiny'), sprites.snowpile_tiny, 1); export const sandPileSmall = doodad(n('sandpile-small'), sprites.snowpile_small, 22, 15, 1, - mixColliderRounded(-15, -4, 31, 7, 2, false)); + mixColliderRounded(-15, -4, 31, 7, 2, false)); export const sandPileMedium = doodad(n('sandpile-medium'), sprites.snowpile_medium, 33, 20, 1, - mixColliderRounded(-23, -4, 46, 12, 4, false)); + mixColliderRounded(-23, -4, 46, 12, 4, false)); export const sandPileBig = doodad(n('sandpile-big'), sprites.snowpile_big, 43, 28, 1, - mixColliderRounded(-39, -2, 78, 16, 6, false)); + mixColliderRounded(-39, -2, 78, 16, 6, false)); // pumpkins @@ -1185,29 +1185,29 @@ const pumpkinDX = 11; const pumpkinDY = 15; export const pumpkin = doodad(n('pumpkin'), sprites.pumpkin_default, pumpkinDX, pumpkinDY, 0, - ...pumpkinParts); + ...pumpkinParts); export const jackoOff = doodad(n('jacko-off'), sprites.pumpkin_off, pumpkinDX, pumpkinDY, 0, - ...pumpkinParts); + ...pumpkinParts); export const jackoOn = doodad(n('jacko-on'), sprites.pumpkin_on, pumpkinDX, pumpkinDY, 0, - ...pumpkinParts, - mixLight(jackoLightColor, 0, 0, 256, 192), - mixLightSprite(sprites.pumpkin_light, WHITE, pumpkinDX, pumpkinDY)); + ...pumpkinParts, + mixLight(jackoLightColor, 0, 0, 256, 192), + mixLightSprite(sprites.pumpkin_light, WHITE, pumpkinDX, pumpkinDY)); export const jacko = doodad(n('jacko'), sprites.pumpkin_on, pumpkinDX, pumpkinDY, 0, - ...pumpkinParts, - mixLight(jackoLightColor, 0, 0, 256, 192), - mixLightSprite(sprites.pumpkin_light, WHITE, pumpkinDX, pumpkinDY), - mixFlags(EntityFlags.OnOff)); + ...pumpkinParts, + mixLight(jackoLightColor, 0, 0, 256, 192), + mixLightSprite(sprites.pumpkin_light, WHITE, pumpkinDX, pumpkinDY), + mixFlags(EntityFlags.OnOff)); // tombstones export const tombstone1 = doodad(n('tombstone-1'), sprites.tombstone_1, 14, 18, 0, - mixColliderRect(-14, -4, 29, 9)); + mixColliderRect(-14, -4, 29, 9)); export const tombstone2 = doodad(n('tombstone-2'), sprites.tombstone_2, 11, 27, 0, - mixColliderRect(-12, -3, 26, 6)); + mixColliderRect(-12, -3, 26, 6)); // torch @@ -1219,63 +1219,63 @@ const torchAnimOff = [0]; const torchAnimOn = torchSprites.frames.map((_, i) => i).slice(1); const torchUnlitSprite: PaletteRenderable = { - color: torchSprites.frames[0], - shadow: torchSprites.shadow, - palettes: [torchSprites.palette], + color: torchSprites.frames[0], + shadow: torchSprites.shadow, + palettes: [torchSprites.palette], }; const torchSpriteOn: AnimatedRenderable = { - frames: torchSprites.frames.slice(1), - shadow: torchSprites.shadow, - palette: torchSprites.palette, + frames: torchSprites.frames.slice(1), + shadow: torchSprites.shadow, + palette: torchSprites.palette, }; const torchLightSpriteOn: AnimatedRenderable1 = { - frames: sprites.torch2_light.frames.slice(1), + frames: sprites.torch2_light.frames.slice(1), }; export const torchOff = doodad(n('torch-off'), torchUnlitSprite, torchDX, torchDY, 0, - torchCollider); + torchCollider); export const torchOn = registerMix(n('torch-on'), - mixAnimation(torchSpriteOn, 8, torchDX, torchDY, { lightSprite: torchLightSpriteOn }), - torchCollider, - mixLight(0x926923ff, 0, 0, 440, 332)); // 0x924d23ff 0x917b32ff + mixAnimation(torchSpriteOn, 8, torchDX, torchDY, { lightSprite: torchLightSpriteOn }), + torchCollider, + mixLight(0x926923ff, 0, 0, 440, 332)); // 0x924d23ff 0x917b32ff export const torch = registerMix(n('torch'), - mixAnimation(torchSprites, 8, torchDX, torchDY, { - lightSprite: sprites.torch2_light, - animations: [torchAnimOff, torchAnimOn], - }), - torchCollider, - mixLight(0x926923ff, 0, 0, 440, 332), - mixFlags(EntityFlags.OnOff)); + mixAnimation(torchSprites, 8, torchDX, torchDY, { + lightSprite: sprites.torch2_light, + animations: [torchAnimOff, torchAnimOn], + }), + torchCollider, + mixLight(0x926923ff, 0, 0, 440, 332), + mixFlags(EntityFlags.OnOff)); export const poof = registerMix(n('poof'), - mixAnimation({ - frames: [...sprites.poof.frames, sprites.emptySprite2], - palette: sprites.poof.palette - }, 12, 13, 30, { repeat: false }), - mixOrder(100)); + mixAnimation({ + frames: [...sprites.poof.frames, sprites.emptySprite2], + palette: sprites.poof.palette + }, 12, 13, 30, { repeat: false }), + mixOrder(100)); export const poof2 = registerMix(n('poof-2'), - mixAnimation({ - frames: [...sprites.poof2.frames, sprites.emptySprite2], - palette: sprites.poof2.palette - }, 12, 50, 120, { repeat: false }), - mixOrder(100)); + mixAnimation({ + frames: [...sprites.poof2.frames, sprites.emptySprite2], + palette: sprites.poof2.palette + }, 12, 50, 120, { repeat: false }), + mixOrder(100)); export const splash = registerMix(n('splash'), - mixAnimation({ - frames: [...sprites.splash.frames, sprites.emptySprite2], - palette: sprites.splash.palette - }, 20, 25, 22, { repeat: false }), - mixOrder(50)); + mixAnimation({ + frames: [...sprites.splash.frames, sprites.emptySprite2], + palette: sprites.splash.palette + }, 20, 25, 22, { repeat: false }), + mixOrder(50)); const boopSplashFrames = [ - ...repeat(3, sprites.emptySprite2), - ...sprites.splash_boop.frames, - sprites.emptySprite2, + ...repeat(3, sprites.emptySprite2), + ...sprites.splash_boop.frames, + sprites.emptySprite2, ]; const boopSlashFps = 20; @@ -1283,39 +1283,39 @@ const boopSlashDX = 11; const boopSlashDY = 55; export const boopSplashRight = registerMix(n('boop-splash-right'), - mixAnimation({ - frames: boopSplashFrames, - palette: sprites.splash_boop.palette - }, boopSlashFps, boopSlashDX, boopSlashDY, { repeat: false }), - mixOrder(51)); + mixAnimation({ + frames: boopSplashFrames, + palette: sprites.splash_boop.palette + }, boopSlashFps, boopSlashDX, boopSlashDY, { repeat: false }), + mixOrder(51)); export const boopSplashLeft = registerMix(n('boop-splash-left'), - mixAnimation({ - frames: boopSplashFrames, - palette: sprites.splash_boop.palette - }, boopSlashFps, boopSlashDX, boopSlashDY, { repeat: false, flipped: true }), - mixOrder(51)); + mixAnimation({ + frames: boopSplashFrames, + palette: sprites.splash_boop.palette + }, boopSlashFps, boopSlashDX, boopSlashDY, { repeat: false, flipped: true }), + mixOrder(51)); // critters export const butterfly = registerMix(n('butterfly'), - mixAnimation(sprites.butterfly, 8, 5, 50), - mixFlags(EntityFlags.Critter | EntityFlags.Movable | EntityFlags.StaticY)); + mixAnimation(sprites.butterfly, 8, 5, 50), + mixFlags(EntityFlags.Critter | EntityFlags.Movable | EntityFlags.StaticY)); export const firefly = registerMix(n('firefly'), - mixAnimation(sprites.firefly, 24, 4, 44), - mixLight(0x446a27ff, 0, 37, 128, 128), // 386a27, 83842a - mixLightSprite(sprites.firefly_light, WHITE, 2, 40), - mixFlags(EntityFlags.Critter | EntityFlags.Movable | EntityFlags.StaticY)); + mixAnimation(sprites.firefly, 24, 4, 44), + mixLight(0x446a27ff, 0, 37, 128, 128), // 386a27, 83842a + mixLightSprite(sprites.firefly_light, WHITE, 2, 40), + mixFlags(EntityFlags.Critter | EntityFlags.Movable | EntityFlags.StaticY)); export const bat = registerMix(n('bat'), - mixAnimation(sprites.bat, 8, 10, 65), - mixFlags(EntityFlags.Critter | EntityFlags.Movable | EntityFlags.StaticY)); + mixAnimation(sprites.bat, 8, 10, 65), + mixFlags(EntityFlags.Critter | EntityFlags.Movable | EntityFlags.StaticY)); export const spider = registerMix(n('spider'), - (base, options) => base.options = { height: 20, time: 0, ...options }, - mixDrawSpider(sprites.spider, 2, 2), - mixFlags(EntityFlags.Critter)); + (base, options) => base.options = { height: 20, time: 0, ...options }, + mixDrawSpider(sprites.spider, 2, 2), + mixFlags(EntityFlags.Critter)); // cat @@ -1323,11 +1323,11 @@ sprites.cat.frames.push(sprites.emptySprite2); sprites.cat_light.frames.push(sprites.emptySprite); export const enum CatAnimation { - Sit = 0, - Enter = 1, - Exit = 2, - Blink = 3, - Wag = 4, + Sit = 0, + Enter = 1, + Exit = 2, + Blink = 3, + Wag = 4, } const catSit = [9]; @@ -1337,21 +1337,21 @@ const catBlink = [9, 10, 10, 9]; const catWag = [9, 11, 11, 12, 12, 13, 13, 14, 14, 14, 15, 15, 16, 16, 9]; export const cat = registerMix(n('cat'), - mixAnimation(sprites.cat, 24, 17, 39, { - repeat: false, - animations: [catSit, catEnter, catExit, catBlink, catWag], - lightSprite: sprites.cat_light, - }), - base => base.chatY = -5); + mixAnimation(sprites.cat, 24, 17, 39, { + repeat: false, + animations: [catSit, catEnter, catExit, catBlink, catWag], + lightSprite: sprites.cat_light, + }), + base => base.chatY = -5); // bunny export const enum BunnyAnimation { - Sit = 0, - Walk = 1, - Blink = 2, - Clean = 3, - Look = 4, + Sit = 0, + Walk = 1, + Blink = 2, + Clean = 3, + Look = 4, } const bunnySit = [7]; @@ -1361,56 +1361,56 @@ const bunnyClean = [7, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 const bunnyLook = [7, 9, 25, 26, 27, 28, 29, 30, 31, 32, 33, 9, ...repeat(30, 7)]; export const bunny = registerMix(n('bunny'), - mixAnimation(sprites.bunny, 14, 12, 23, { - animations: [bunnySit, bunnyWalk, bunnyBlink, bunnyClean, bunnyLook], - }), - mixFlags(EntityFlags.Movable)); + mixAnimation(sprites.bunny, 14, 12, 23, { + animations: [bunnySit, bunnyWalk, bunnyBlink, bunnyClean, bunnyLook], + }), + mixFlags(EntityFlags.Movable)); // eyes const spritesEyes: AnimatedRenderable = { - frames: [sprites.emptySprite2], - palette: sprites.defaultPalette, + frames: [sprites.emptySprite2], + palette: sprites.defaultPalette, }; export const eyes = registerMix(n('eyes'), - mixAnimation(spritesEyes, 24, 16, 8, { - repeat: false, - animations: [[9], [10]], - lightSprite: sprites.cat_light, - })); + mixAnimation(spritesEyes, 24, 16, 8, { + repeat: false, + animations: [[9], [10]], + lightSprite: sprites.cat_light, + })); // ghosts const ghostSprite: AnimatedRenderable = { - frames: [ - sprites.emptySprite2, - ...sprites.ghost1.frames, - ], - palette: sprites.ghost1.palette, + frames: [ + sprites.emptySprite2, + ...sprites.ghost1.frames, + ], + palette: sprites.ghost1.palette, }; const ghostHoovesSprite: AnimatedRenderable = { - frames: [ - sprites.emptySprite2, - ...sprites.ghost1_hooves.frames, - ...repeat(17, sprites.emptySprite2), - ], - palette: sprites.ghost1_hooves.palette, + frames: [ + sprites.emptySprite2, + ...sprites.ghost1_hooves.frames, + ...repeat(17, sprites.emptySprite2), + ], + palette: sprites.ghost1_hooves.palette, }; const ghostLightSprite: AnimatedRenderable1 = { - frames: [ - sprites.emptySprite, - ...sprites.ghost1_light.frames, - ], + frames: [ + sprites.emptySprite, + ...sprites.ghost1_light.frames, + ], }; const ghostHoovesLightSprite: AnimatedRenderable1 = { - frames: [ - sprites.emptySprite, - ...sprites.ghost1_hooves_light.frames, - ], + frames: [ + sprites.emptySprite, + ...sprites.ghost1_hooves_light.frames, + ], }; const ghostFPS = 20; @@ -1421,124 +1421,124 @@ const ghostColor = withAlphaFloat(WHITE, 0.7); const ghostLightColor = 0x777777ff; const ghostNone = [0]; const ghostAnim1 = [ - 0, - 1, 1, - 2, 2, - 3, 3, - 4, 4, - 5, 5, - 6, 6, - 7, 7, - 8, 8, - ...repeat(10, 9), - 10, - ...repeat(3, 11), - 12, - ...repeat(14, 13), - 14, 14, - ...repeat(10, 15), - 16, - 17, - 18, - 19, - 20, - 0, + 0, + 1, 1, + 2, 2, + 3, 3, + 4, 4, + 5, 5, + 6, 6, + 7, 7, + 8, 8, + ...repeat(10, 9), + 10, + ...repeat(3, 11), + 12, + ...repeat(14, 13), + 14, 14, + ...repeat(10, 15), + 16, + 17, + 18, + 19, + 20, + 0, ]; const ghostAnim2 = [ - 0, - 21, 21, - 22, 22, - 23, 23, - 24, 24, - 25, 25, - ...repeat(10, 26), - 27, - 28, - ...repeat(4, 29), - 30, - 31, - 32, - ...repeat(10, 33), - 34, - 35, - 36, - 37, - 0, + 0, + 21, 21, + 22, 22, + 23, 23, + 24, 24, + 25, 25, + ...repeat(10, 26), + 27, + 28, + ...repeat(4, 29), + 30, + 31, + 32, + ...repeat(10, 33), + 34, + 35, + 36, + 37, + 0, ]; const ghostAnim3 = [ - 0, - 38, 38, - 39, 39, - 40, 40, - 41, 41, - 42, 42, - 43, 43, - 44, 44, - 45, 45, - 46, 46, - 47, 47, - 48, 48, - ...repeat(10, 49), - 50, - 51, 51, - 52, 52, - 53, 53, - 54, 54, - 55, 55, - 56, 56, - 57, 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - 0, + 0, + 38, 38, + 39, 39, + 40, 40, + 41, 41, + 42, 42, + 43, 43, + 44, 44, + 45, 45, + 46, 46, + 47, 47, + 48, 48, + ...repeat(10, 49), + 50, + 51, 51, + 52, 52, + 53, 53, + 54, 54, + 55, 55, + 56, 56, + 57, 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 0, ]; export const enum GhostAnimation { - None = 0, - Anim1 = 1, - Anim2 = 2, - Anim3 = 3, + None = 0, + Anim1 = 1, + Anim2 = 2, + Anim3 = 3, } const createGhost = (tomb: number) => { - const anim = mixAnimation(ghostSprite, ghostFPS, ghostDX, ghostDY[tomb], { - color: ghostColor, - repeat: false, - animations: [ghostNone, ghostAnim1, ghostAnim2, ghostAnim3], - lightSprite: ghostLightSprite, - }); + const anim = mixAnimation(ghostSprite, ghostFPS, ghostDX, ghostDY[tomb], { + color: ghostColor, + repeat: false, + animations: [ghostNone, ghostAnim1, ghostAnim2, ghostAnim3], + lightSprite: ghostLightSprite, + }); - return registerMix(n(`ghost-${tomb + 1}`), - anim, - base => { - base.order = -1; - base.lightSpriteColor = ghostLightColor; - }); + return registerMix(n(`ghost-${tomb + 1}`), + anim, + base => { + base.order = -1; + base.lightSpriteColor = ghostLightColor; + }); }; const createGhostHooves = (tomb: number) => { - const anim = mixAnimation(ghostHoovesSprite, ghostFPS, ghostDX, ghostDY[tomb], { - color: ghostColor, - repeat: false, - animations: [ghostNone, ghostAnim1, ghostAnim2, ghostAnim3], - lightSprite: ghostHoovesLightSprite, - }); + const anim = mixAnimation(ghostHoovesSprite, ghostFPS, ghostDX, ghostDY[tomb], { + color: ghostColor, + repeat: false, + animations: [ghostNone, ghostAnim1, ghostAnim2, ghostAnim3], + lightSprite: ghostHoovesLightSprite, + }); - return registerMix(n(`ghost-hooves-${tomb + 1}`), - anim, - base => { - base.order = -1; - base.lightSpriteColor = ghostLightColor; - }); + return registerMix(n(`ghost-hooves-${tomb + 1}`), + anim, + base => { + base.order = -1; + base.lightSpriteColor = ghostLightColor; + }); }; export const ghost1 = createGhost(0); @@ -1551,8 +1551,8 @@ export const ghostHooves2 = createGhostHooves(1); const cloudSprite = sprites.cloud.shadow; export const cloud = registerMix(n('cloud'), - mixDrawShadow(sprites.cloud, Math.floor(cloudSprite.w / 2), cloudSprite.h, CLOUD_SHADOW_COLOR), - mixFlags(EntityFlags.Decal | EntityFlags.Movable)); + mixDrawShadow(sprites.cloud, Math.floor(cloudSprite.w / 2), cloudSprite.h, CLOUD_SHADOW_COLOR), + mixFlags(EntityFlags.Decal | EntityFlags.Movable)); // vegetation @@ -1560,32 +1560,32 @@ const largeLeafedBushLarge = mixColliderRounded(-14, -6, 28, 12, 2, false); const largeLeafedBushSmall = mixColliderRounded(-8, -4, 16, 8, 2, false); export const largeLeafedBush1 = registerMix(n('large-leafed-bush-1'), - mixDrawSeasonal({ - summer: { sprite: sprites.large_leafed_bush_1, dx: 17, dy: 23, palette: 0 }, - winter: { palette: 1 }, - }), - largeLeafedBushLarge); + mixDrawSeasonal({ + summer: { sprite: sprites.large_leafed_bush_1, dx: 17, dy: 23, palette: 0 }, + winter: { palette: 1 }, + }), + largeLeafedBushLarge); export const largeLeafedBush2 = registerMix(n('large-leafed-bush-2'), - mixDrawSeasonal({ - summer: { sprite: sprites.large_leafed_bush_2, dx: 17, dy: 23, palette: 0 }, - winter: { palette: 1 }, - }), - largeLeafedBushLarge); + mixDrawSeasonal({ + summer: { sprite: sprites.large_leafed_bush_2, dx: 17, dy: 23, palette: 0 }, + winter: { palette: 1 }, + }), + largeLeafedBushLarge); export const largeLeafedBush3 = registerMix(n('large-leafed-bush-3'), - mixDrawSeasonal({ - summer: { sprite: sprites.large_leafed_bush_3, dx: 12, dy: 17, palette: 0 }, - winter: { palette: 1 }, - }), - largeLeafedBushSmall); + mixDrawSeasonal({ + summer: { sprite: sprites.large_leafed_bush_3, dx: 12, dy: 17, palette: 0 }, + winter: { palette: 1 }, + }), + largeLeafedBushSmall); export const largeLeafedBush4 = registerMix(n('large-leafed-bush-4'), - mixDrawSeasonal({ - summer: { sprite: sprites.large_leafed_bush_4, dx: 12, dy: 17, palette: 0 }, - winter: { palette: 1 }, - }), - largeLeafedBushSmall); + mixDrawSeasonal({ + summer: { sprite: sprites.large_leafed_bush_4, dx: 12, dy: 17, palette: 0 }, + winter: { palette: 1 }, + }), + largeLeafedBushSmall); // cliffs @@ -1601,76 +1601,76 @@ const cliffColor = 0x908d7cff; const cliffExtra = mixMinimap(cliffColor, rect(-1, 0, 1, 1)); function cliffOffset(name: string, sprite: PaletteRenderable, dx: number, dy: number, ...other: MixinEntity[]) { - return registerMix(name, - mixDrawSeasonal({ - summer: { sprite, dx, dy, palette: 0 }, - autumn: { palette: 1 }, - winter: { palette: 2 }, - }), - mixFlags(EntityFlags.StaticY), - ...other); + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite, dx, dy, palette: 0 }, + autumn: { palette: 1 }, + winter: { palette: 2 }, + }), + mixFlags(EntityFlags.StaticY), + ...other); } function cliff(name: string, sprite: PaletteRenderable, ...other: MixinEntity[]) { - return cliffOffset(name, sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, - mixFlags(EntityFlags.StaticY), - ...other); + return cliffOffset(name, sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, + mixFlags(EntityFlags.StaticY), + ...other); } function cliffDecal(name: string, sprite: PaletteRenderable, dx: number, dy: number, ...other: MixinEntity[]) { - return cliffOffset(name, sprite, dx, dy, mixFlags(EntityFlags.Decal), - ...other); + return cliffOffset(name, sprite, dx, dy, mixFlags(EntityFlags.Decal), + ...other); } export const cliffSW = cliff(n('cliff-sw'), sprites.cliffs_grass_sw, - mixColliders( - ...taperColliderSW(-16, -3, 32, 25, cliffTall), - collider(-16, 22, 35, 24 * 2 + 7, cliffTall), - ...taperColliderNE(-16, 22 + 24 * 2 + 7, 32, 24, cliffTall), - ), - mixMinimap(cliffColor, rect(-1, 0, 1, 4))); + mixColliders( + ...taperColliderSW(-16, -3, 32, 25, cliffTall), + collider(-16, 22, 35, 24 * 2 + 7, cliffTall), + ...taperColliderNE(-16, 22 + 24 * 2 + 7, 32, 24, cliffTall), + ), + mixMinimap(cliffColor, rect(-1, 0, 1, 4))); export const cliffSE = cliff(n('cliff-se'), sprites.cliffs_grass_se, - mixColliders( - ...taperColliderSE(-16, -3, 32, 25, cliffTall), - collider(-19, 22, 35, 24 * 2 + 7, cliffTall), - ...taperColliderNW(-16, 22 + 24 * 2 + 7, 32, 24, cliffTall), - ), - mixMinimap(cliffColor, rect(-1, 0, 1, 4))); + mixColliders( + ...taperColliderSE(-16, -3, 32, 25, cliffTall), + collider(-19, 22, 35, 24 * 2 + 7, cliffTall), + ...taperColliderNW(-16, 22 + 24 * 2 + 7, 32, 24, cliffTall), + ), + mixMinimap(cliffColor, rect(-1, 0, 1, 4))); const cliffSCollider = mixColliderRect(-16, -3, 32, 24 * 3 + 8, cliffTall); export const cliffS1 = cliff(n('cliff-s1'), sprites.cliffs_grass_s1, - cliffSCollider, - mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + cliffSCollider, + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); export const cliffS2 = cliff(n('cliff-s2'), sprites.cliffs_grass_s2, - cliffSCollider, - mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + cliffSCollider, + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); export const cliffS3 = cliff(n('cliff-s3'), sprites.cliffs_grass_s3, - cliffSCollider, - mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + cliffSCollider, + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); export const cliffSb = cliff(n('cliff-sb'), sprites.cliffs_grass_sb, - cliffSCollider, - mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + cliffSCollider, + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); export const cliffSbEntrance = cliff(n('cliff-sb-entrance'), sprites.cliffs_grass_sb, - mixColliderRect(-16, -3, 32, 24 * 3, cliffTall), - mixMinimap(cliffColor, rect(-1, 0, 1, 3))); + mixColliderRect(-16, -3, 32, 24 * 3, cliffTall), + mixMinimap(cliffColor, rect(-1, 0, 1, 3))); const cliffNWColliders = mixColliders(...skewColliderNW(0, 0, 21, 24, cliffTall)); const cliffNEColliders = mixColliders(...skewColliderNE(-22, 0, 21, 24, cliffTall)); const cliffColliderTrimLeftBot = mixColliders( - collider(-16, 0, 16, 17, cliffTall), - ...taperColliderNW(-16, 17, 16, 11, cliffTall), + collider(-16, 0, 16, 17, cliffTall), + ...taperColliderNW(-16, 17, 16, 11, cliffTall), ); const cliffColliderTrimRightBot = mixColliders( - collider(0, 0, 16, 17, cliffTall), - ...taperColliderNE(0, 17, 16, 11, cliffTall), + collider(0, 0, 16, 17, cliffTall), + ...taperColliderNE(0, 17, 16, 11, cliffTall), ); export const cliffTopNW = cliff(n('cliff-top-nw'), sprites.cliffs_grass_top_nw, cliffNWColliders, cliffExtra); @@ -1705,18 +1705,18 @@ export const cliffBotSW = cliff(n('cliff-bot-sw'), sprites.cliffs_grass_bot_sw, export const cliffBotSE = cliff(n('cliff-bot-se'), sprites.cliffs_grass_bot_se, cliffCollider, cliffExtra); export const cliffTopTrimLeft = cliffDecal( - n('cliff-top-trim-left'), sprites.cliffs_grass_top_trim_left, 16, 0, cliffColliderTrimRight, mixOrder(1)); + n('cliff-top-trim-left'), sprites.cliffs_grass_top_trim_left, 16, 0, cliffColliderTrimRight, mixOrder(1)); export const cliffMidTrimLeft = cliffDecal( - n('cliff-mid-trim-left'), sprites.cliffs_grass_mid_trim_left, 16, 0, cliffColliderTrimRight, mixOrder(1)); + n('cliff-mid-trim-left'), sprites.cliffs_grass_mid_trim_left, 16, 0, cliffColliderTrimRight, mixOrder(1)); export const cliffBotTrimLeft = cliffDecal( - n('cliff-bot-trim-left'), sprites.cliffs_grass_bot_trim_left, 16, 0, cliffColliderTrimRightBot, mixOrder(1)); + n('cliff-bot-trim-left'), sprites.cliffs_grass_bot_trim_left, 16, 0, cliffColliderTrimRightBot, mixOrder(1)); export const cliffTopTrimRight = cliffDecal( - n('cliff-top-trim-right'), sprites.cliffs_grass_top_trim_right, 16, 0, cliffColliderTrimLeft); + n('cliff-top-trim-right'), sprites.cliffs_grass_top_trim_right, 16, 0, cliffColliderTrimLeft); export const cliffMidTrimRight = cliffDecal( - n('cliff-mid-trim-right'), sprites.cliffs_grass_mid_trim_right, 16, 0, cliffColliderTrimLeft); + n('cliff-mid-trim-right'), sprites.cliffs_grass_mid_trim_right, 16, 0, cliffColliderTrimLeft); export const cliffBotTrimRight = cliffDecal( - n('cliff-bot-trim-right'), sprites.cliffs_grass_bot_trim_right, 16, 0, cliffColliderTrimLeftBot); + n('cliff-bot-trim-right'), sprites.cliffs_grass_bot_trim_right, 16, 0, cliffColliderTrimLeftBot); export const cliffDecal1 = cliffDecal(n('cliff-decal-1'), sprites.cliffs_grass_decal_1, 14, 1, mixOrder(2)); export const cliffDecal2 = cliffDecal(n('cliff-decal-2'), sprites.cliffs_grass_decal_2, 14, 1, mixOrder(2)); @@ -1738,69 +1738,69 @@ const caveColor = 0x6a6f73ff; const caveExtra = mixMinimap(caveColor, rect(-1, 0, 1, 1)); function caveOffset(name: string, sprite: PaletteRenderable, dx: number, dy: number, ...other: MixinEntity[]) { - return registerMix(name, - mixDrawSeasonal({ - summer: { sprite, dx, dy, palette: 0 }, - autumn: { palette: 1 }, - winter: { palette: 2 }, - }), - mixFlags(EntityFlags.StaticY), - ...other); + return registerMix(name, + mixDrawSeasonal({ + summer: { sprite, dx, dy, palette: 0 }, + autumn: { palette: 1 }, + winter: { palette: 2 }, + }), + mixFlags(EntityFlags.StaticY), + ...other); } function cave(name: string, sprite: PaletteRenderable, ...other: MixinEntity[]) { - return caveOffset(name, sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, - ...other); + return caveOffset(name, sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, + ...other); } function caveDecal(name: string, sprite: PaletteRenderable, dx: number, dy: number, ...other: MixinEntity[]) { - return caveOffset(name, sprite, dx, dy, mixFlags(EntityFlags.Decal), - ...other); + return caveOffset(name, sprite, dx, dy, mixFlags(EntityFlags.Decal), + ...other); } export const caveSW = cave(n('cave-sw'), sprites.cave_walls_sw, - mixColliders( - collider(-16, -3, 35, 24 * 2 + 7 + 25, caveTall), - ...taperColliderNE(-16, 22 + 24 * 2 + 7, 32, 24, caveTall), - ), - mixMinimap(caveColor, rect(-1, 0, 1, 4))); + mixColliders( + collider(-16, -3, 35, 24 * 2 + 7 + 25, caveTall), + ...taperColliderNE(-16, 22 + 24 * 2 + 7, 32, 24, caveTall), + ), + mixMinimap(caveColor, rect(-1, 0, 1, 4))); export const caveSE = cave(n('cave-se'), sprites.cave_walls_se, - mixColliders( - collider(-19, -3, 35, 24 * 2 + 7 + 25, caveTall), - ...taperColliderNW(-16, 22 + 24 * 2 + 7, 32, 24, caveTall), - ), - mixMinimap(caveColor, rect(-1, 0, 1, 4))); + mixColliders( + collider(-19, -3, 35, 24 * 2 + 7 + 25, caveTall), + ...taperColliderNW(-16, 22 + 24 * 2 + 7, 32, 24, caveTall), + ), + mixMinimap(caveColor, rect(-1, 0, 1, 4))); const caveSCollider = mixColliderRect(-16, -3, 32, 24 * 3 + 8, caveTall); export const caveS1 = cave(n('cave-s1'), sprites.cave_walls_s1, - caveSCollider, - mixMinimap(caveColor, rect(-1, 0, 1, 3))); + caveSCollider, + mixMinimap(caveColor, rect(-1, 0, 1, 3))); export const caveS2 = cave(n('cave-s2'), sprites.cave_walls_s2, - caveSCollider, - mixMinimap(caveColor, rect(-1, 0, 1, 3))); + caveSCollider, + mixMinimap(caveColor, rect(-1, 0, 1, 3))); export const caveS3 = cave(n('cave-s3'), sprites.cave_walls_s3, - caveSCollider, - mixMinimap(caveColor, rect(-1, 0, 1, 3))); + caveSCollider, + mixMinimap(caveColor, rect(-1, 0, 1, 3))); export const caveSb = cave(n('cave-sb'), sprites.cave_walls_sb, - caveSCollider, - mixMinimap(caveColor, rect(-1, 0, 1, 3))); + caveSCollider, + mixMinimap(caveColor, rect(-1, 0, 1, 3))); const caveNWColliders = mixColliders(...triangleColliderNW(0, 0, 21, 24, caveTall)); const caveNEColliders = mixColliders(...triangleColliderNE(-22, 0, 21, 24, caveTall)); const caveColliderTrimLeftBot = mixColliders( - collider(-16, 0, 16, 17, caveTall), - ...taperColliderNW(-16, 17, 16, 11, caveTall), + collider(-16, 0, 16, 17, caveTall), + ...taperColliderNW(-16, 17, 16, 11, caveTall), ); const caveColliderTrimRightBot = mixColliders( - collider(0, 0, 16, 17, caveTall), - ...taperColliderNE(0, 17, 16, 11, caveTall), + collider(0, 0, 16, 17, caveTall), + ...taperColliderNE(0, 17, 16, 11, caveTall), ); export const caveTopNW = cave(n('cave-top-nw'), sprites.cave_walls_top_nw, caveNWColliders, caveExtra); @@ -1835,18 +1835,18 @@ export const caveBotSW = cave(n('cave-bot-sw'), sprites.cave_walls_bot_sw, caveC export const caveBotSE = cave(n('cave-bot-se'), sprites.cave_walls_bot_se, caveCollider, caveExtra); export const caveTopTrimLeft = caveDecal( - n('cave-top-trim-left'), sprites.cave_walls_top_trim_left, 16, 0, caveColliderTrimRight, mixOrder(1)); + n('cave-top-trim-left'), sprites.cave_walls_top_trim_left, 16, 0, caveColliderTrimRight, mixOrder(1)); export const caveMidTrimLeft = caveDecal( - n('cave-mid-trim-left'), sprites.cave_walls_mid_trim_left, 16, 0, caveColliderTrimRight, mixOrder(1)); + n('cave-mid-trim-left'), sprites.cave_walls_mid_trim_left, 16, 0, caveColliderTrimRight, mixOrder(1)); export const caveBotTrimLeft = caveDecal( - n('cave-bot-trim-left'), sprites.cave_walls_bot_trim_left, 16, 0, caveColliderTrimRightBot, mixOrder(1)); + n('cave-bot-trim-left'), sprites.cave_walls_bot_trim_left, 16, 0, caveColliderTrimRightBot, mixOrder(1)); export const caveTopTrimRight = caveDecal( - n('cave-top-trim-right'), sprites.cave_walls_top_trim_right, 16, 0, caveColliderTrimLeft); + n('cave-top-trim-right'), sprites.cave_walls_top_trim_right, 16, 0, caveColliderTrimLeft); export const caveMidTrimRight = caveDecal( - n('cave-mid-trim-right'), sprites.cave_walls_mid_trim_right, 16, 0, caveColliderTrimLeft); + n('cave-mid-trim-right'), sprites.cave_walls_mid_trim_right, 16, 0, caveColliderTrimLeft); export const caveBotTrimRight = caveDecal( - n('cave-bot-trim-right'), sprites.cave_walls_bot_trim_right, 16, 0, caveColliderTrimLeftBot); + n('cave-bot-trim-right'), sprites.cave_walls_bot_trim_right, 16, 0, caveColliderTrimLeftBot); export const caveDecal1 = caveDecal(n('cave-decal-1'), sprites.cave_walls_decal_1, 14, 1, mixOrder(2)); export const caveDecal2 = caveDecal(n('cave-decal-2'), sprites.cave_walls_decal_2, 14, 1, mixOrder(2)); @@ -1855,27 +1855,27 @@ export const caveDecalL = caveDecal(n('cave-decal-l'), sprites.cave_walls_decal_ export const caveDecalR = caveDecal(n('cave-decal-r'), sprites.cave_walls_decal_r, 16, 1, mixOrder(2)); export const caveFill = registerMix(n('cave-fill'), - mixDraw(sprites.tile_none, 0, 0), - mixColliderRect(0, 0, 32, 24, true, true), - mixFlags(EntityFlags.StaticY)); + mixDraw(sprites.tile_none, 0, 0), + mixColliderRect(0, 0, 32, 24, true, true), + mixFlags(EntityFlags.StaticY)); export const caveCover = registerMix(n('cave-cover'), - mixDraw(sprites.tile_none, 0, 24), - mixFlags(EntityFlags.StaticY)); + mixDraw(sprites.tile_none, 0, 24), + mixFlags(EntityFlags.StaticY)); // stalactites export const stalactite1 = doodad(n('stalactite-1'), sprites.stalactite_1, 4, 15, 0, - mixColliderRounded(-4, -3, 8, 5, 2), - mixFlags(EntityFlags.StaticY)); + mixColliderRounded(-4, -3, 8, 5, 2), + mixFlags(EntityFlags.StaticY)); export const stalactite2 = doodad(n('stalactite-2'), sprites.stalactite_2, 5, 31, 0, - mixColliderRounded(-5, -4, 10, 5, 2), - mixFlags(EntityFlags.StaticY)); + mixColliderRounded(-5, -4, 10, 5, 2), + mixFlags(EntityFlags.StaticY)); export const stalactite3 = doodad(n('stalactite-3'), sprites.stalactite_3, 6, 51, 0, - mixColliderRounded(-6, -6, 12, 7, 3), - mixFlags(EntityFlags.StaticY)); + mixColliderRounded(-6, -6, 12, 7, 3), + mixFlags(EntityFlags.StaticY)); // crystals @@ -1884,136 +1884,136 @@ const mixCrystalLight = mixLight(crystalLight, 0, 0, 200, 200); const waterCrystalFPS = WATER_FPS; export const crystals1 = registerMix(n('crystals-1'), - mixDraw(sprites.crystals_1, 8, 16), - mixLightSprite(sprites.light_crystals_1, WHITE, 8, 16), - mixCrystalLight, - mixColliderRounded(-7, -4, 16, 4, 1)); + mixDraw(sprites.crystals_1, 8, 16), + mixLightSprite(sprites.light_crystals_1, WHITE, 8, 16), + mixCrystalLight, + mixColliderRounded(-7, -4, 16, 4, 1)); export const crystals2 = registerMix(n('crystals-2'), - mixDraw(sprites.crystals_2, 11, 19), - mixLightSprite(sprites.light_crystals_2, WHITE, 11, 19), - mixCrystalLight, - mixColliderRounded(-9, -4, 18, 4, 1)); + mixDraw(sprites.crystals_2, 11, 19), + mixLightSprite(sprites.light_crystals_2, WHITE, 11, 19), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); export const crystals3 = registerMix(n('crystals-3'), - mixDraw(sprites.crystals_3, 13, 18), - mixLightSprite(sprites.light_crystals_3, WHITE, 13, 18), - mixCrystalLight, - mixColliderRounded(-9, -4, 18, 4, 1)); + mixDraw(sprites.crystals_3, 13, 18), + mixLightSprite(sprites.light_crystals_3, WHITE, 13, 18), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); export const crystals4 = registerMix(n('crystals-4'), - mixDraw(sprites.crystals_4, 11, 15), - mixLightSprite(sprites.light_crystals_4, WHITE, 11, 15), - mixCrystalLight, - mixColliderRounded(-9, -4, 18, 4, 1)); + mixDraw(sprites.crystals_4, 11, 15), + mixLightSprite(sprites.light_crystals_4, WHITE, 11, 15), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); export const crystals5 = registerMix(n('crystals-5'), - mixDraw(sprites.crystals_5, 12, 18), - mixLightSprite(sprites.light_crystals_5, WHITE, 12, 18), - mixCrystalLight, - mixColliderRounded(-9, -4, 18, 4, 1)); + mixDraw(sprites.crystals_5, 12, 18), + mixLightSprite(sprites.light_crystals_5, WHITE, 12, 18), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); export const crystals6 = registerMix(n('crystals-6'), - mixDraw(sprites.crystals_6, 11, 13), - mixLightSprite(sprites.light_crystals_6, WHITE, 11, 13), - mixCrystalLight, - mixColliderRounded(-9, -4, 18, 4, 1)); + mixDraw(sprites.crystals_6, 11, 13), + mixLightSprite(sprites.light_crystals_6, WHITE, 11, 13), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); export const crystals7 = registerMix(n('crystals-7'), - mixDraw(sprites.crystals_7, 13, 16), - mixLightSprite(sprites.light_crystals_7, WHITE, 13, 16), - mixCrystalLight, - mixColliderRounded(-9, -4, 18, 4, 1)); + mixDraw(sprites.crystals_7, 13, 16), + mixLightSprite(sprites.light_crystals_7, WHITE, 13, 16), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); export const crystals8 = registerMix(n('crystals-8'), - mixDraw(sprites.crystals_8, 8, 17), - mixLightSprite(sprites.light_crystals_8, WHITE, 8, 17), - mixCrystalLight, - mixColliderRounded(-9, -4, 18, 4, 1)); + mixDraw(sprites.crystals_8, 8, 17), + mixLightSprite(sprites.light_crystals_8, WHITE, 8, 17), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); export const crystals9 = registerMix(n('crystals-9'), - mixDraw(sprites.crystals_9, 8, 11), - mixLightSprite(sprites.light_crystals_9, WHITE, 8, 11), - mixCrystalLight, - mixColliderRounded(-9, -4, 18, 4, 1)); + mixDraw(sprites.crystals_9, 8, 11), + mixLightSprite(sprites.light_crystals_9, WHITE, 8, 11), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); export const crystals10 = registerMix(n('crystals-10'), - mixDraw(sprites.crystals_10, 5, 12), - mixLightSprite(sprites.light_crystals_10, WHITE, 5, 12), - mixCrystalLight, - mixColliderRounded(-9, -4, 18, 4, 1)); + mixDraw(sprites.crystals_10, 5, 12), + mixLightSprite(sprites.light_crystals_10, WHITE, 5, 12), + mixCrystalLight, + mixColliderRounded(-9, -4, 18, 4, 1)); export const crystalsCartPile = registerMix(n('crystals-cart-pile'), - mixDraw(sprites.crystals_cart_pile, 21, 28), - mixLightSprite(sprites.light_crystals_cart_pile, WHITE, 21, 28), - mixCrystalLight, - mixInteract(-20, -28, 40, 40, 3), - mixFlags(EntityFlags.StaticY), - mixOrder(2)); + mixDraw(sprites.crystals_cart_pile, 21, 28), + mixLightSprite(sprites.light_crystals_cart_pile, WHITE, 21, 28), + mixCrystalLight, + mixInteract(-20, -28, 40, 40, 3), + mixFlags(EntityFlags.StaticY), + mixOrder(2)); export const crystalHeld = registerMix(n('crystal-held'), - mixDraw(sprites.crystals_held, 7, 4), - mixLightSprite(sprites.light_crystals_held, WHITE, 7, 4), - mixLight(crystalLight, 0, 0, 160, 160), - mixPickable(31, 44)); + mixDraw(sprites.crystals_held, 7, 4), + mixLightSprite(sprites.light_crystals_held, WHITE, 7, 4), + mixLight(crystalLight, 0, 0, 160, 160), + mixPickable(31, 44)); export const crystalLantern = registerMix(n('crystal-lantern'), - mixDraw(sprites.crystal_lantern, 4, 15), - mixLightSprite(sprites.light_crystal_lantern, WHITE, 4, 15), - mixLight(crystalLight, 0, 0, 192, 144), - mixPickable(31, 55)); + mixDraw(sprites.crystal_lantern, 4, 15), + mixLightSprite(sprites.light_crystal_lantern, WHITE, 4, 15), + mixLight(crystalLight, 0, 0, 192, 144), + mixPickable(31, 55)); export const waterCrystal1 = registerMix(n('water-crystal-1'), - mixAnimation(sprites.water_crystal_1, waterCrystalFPS, 4, 12, { - lightSprite: sprites.water_crystal_1_light, - }), - mixCrystalLight, - mixColliderRounded(-4, -4, 9, 8, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_crystal_1, waterCrystalFPS, 4, 12, { + lightSprite: sprites.water_crystal_1_light, + }), + mixCrystalLight, + mixColliderRounded(-4, -4, 9, 8, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterCrystal2 = registerMix(n('water-crystal-2'), - mixAnimation(sprites.water_crystal_2, waterCrystalFPS, 9, 11, { - lightSprite: sprites.water_crystal_2_light, - }), - mixCrystalLight, - mixColliderRounded(-9, -6, 16, 7, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_crystal_2, waterCrystalFPS, 9, 11, { + lightSprite: sprites.water_crystal_2_light, + }), + mixCrystalLight, + mixColliderRounded(-9, -6, 16, 7, 2, false), + mixFlags(EntityFlags.StaticY)); export const waterCrysta3 = registerMix(n('water-crystal-3'), - mixAnimation(sprites.water_crystal_3, waterCrystalFPS, 5, 10, { - lightSprite: sprites.water_crystal_3_light, - }), - mixCrystalLight, - mixColliderRounded(-4, -4, 7, 5, 2, false), - mixFlags(EntityFlags.StaticY)); + mixAnimation(sprites.water_crystal_3, waterCrystalFPS, 5, 10, { + lightSprite: sprites.water_crystal_3_light, + }), + mixCrystalLight, + mixColliderRounded(-4, -4, 7, 5, 2, false), + mixFlags(EntityFlags.StaticY)); // mine export const mineEntrance = registerMix(n('mine-entrance'), - mixDraw(sprites.mine_entrance, 49, 0), - mixInteract(-32, -48, 65, 46, 3), - mixOrder(10)); + mixDraw(sprites.mine_entrance, 49, 0), + mixInteract(-32, -48, 65, 46, 3), + mixOrder(10)); export const mineClosed = registerMix(n('mine-closed'), - mixDraw(sprites.mine_closed, 36, -24), - mixOrder(11)); + mixDraw(sprites.mine_closed, 36, -24), + mixOrder(11)); export const mineCart = doodad(n('mine-cart'), sprites.mine_cart, 26, 32, 0, - mixColliderRect(-27, 0, 54, 21), - mixFlags(EntityFlags.StaticY), - mixOrder(1)); + mixColliderRect(-27, 0, 54, 21), + mixFlags(EntityFlags.StaticY), + mixOrder(1)); export const mineCartFront = doodad(n('mine-cart-front'), sprites.mine_cart_front, 26, 52, 0, - mixColliders( - collider(-30, -25, 55, 4), - collider(-30, -19, 15, 20), - collider(-30, 2, 55, 4), - ), - mixFlags(EntityFlags.StaticY)); + mixColliders( + collider(-30, -25, 55, 4), + collider(-30, -19, 15, 20), + collider(-30, 2, 55, 4), + ), + mixFlags(EntityFlags.StaticY)); export const mineCartBack = doodad(n('mine-cart-back'), sprites.mine_cart_back, 26, 30, 0, - mixFlags(EntityFlags.StaticY)); + mixFlags(EntityFlags.StaticY)); const railsExtra = mixFlags(EntityFlags.StaticY); @@ -2030,19 +2030,19 @@ export const mineRailsNWE = decalOffset(n('mine-rails-nwe'), sprites.mine_rails_ export const mineRailsSWE = decalOffset(n('mine-rails-swe'), sprites.mine_rails_swe, 16, 0, 0, railsExtra); export const mineRailsEndLeft = doodad(n('mine-rails-end-left'), sprites.mine_rails_end_left, 17, 30, 0, - mixColliderRect(-20, -10, 38, 23), - railsExtra); + mixColliderRect(-20, -10, 38, 23), + railsExtra); export const mineRailsEndRight = doodad(n('mine-rails-end-right'), sprites.mine_rails_end_right, 16, 30, 0, - mixColliderRect(-16, -10, 38, 23), - railsExtra); + mixColliderRect(-16, -10, 38, 23), + railsExtra); export const mineRailsEndTop = doodad(n('mine-rails-end-top'), sprites.mine_rails_end_top, 16, 32, 0, - mixColliderRect(-16, -32, 32, 32), - railsExtra); + mixColliderRect(-16, -32, 32, 32), + railsExtra); export const mineRailsFadeUp = decal(n('mine-rails-fade-up'), sprites.mine_rail_fade_up, 0, - railsExtra); + railsExtra); // collider utils @@ -2058,7 +2058,7 @@ const stumpsTall = false; const treeAutumnPals = [2, 3, 4]; export const web = doodad(n('web'), sprites.web, -6, 39, 0, - mixCover(-50, -135, 110, 120)); + mixCover(-50, -135, 110, 120)); export const xmasLights = registerMix(n('xmas-lights'), mixLightSprite(sprites.light6, WHITE, 75, 180)); export const xmasLight = registerMix(n('xmas-light'), mixLight(0x926923ff, 0, 0, 50, 50)); @@ -2068,326 +2068,326 @@ const tree2Options = { sprite: sprites.tree_2, dx: 10, dy: 32 }; const tree3Options = { sprite: sprites.tree_3, dx: 21, dy: 59 }; export const trees1 = times(3, i => registerMix(n(`tree1-${i}`), - mixDrawSeasonal({ - summer: { ...tree1Options, palette: 0 }, - autumn: { ...tree1Options, palette: treeAutumnPals[i] }, - winter: { ...tree1Options, palette: 1 }, - }))); + mixDrawSeasonal({ + summer: { ...tree1Options, palette: 0 }, + autumn: { ...tree1Options, palette: treeAutumnPals[i] }, + winter: { ...tree1Options, palette: 1 }, + }))); export const trees2 = times(3, i => registerMix(n(`tree2-${i}`), - mixDrawSeasonal({ - summer: { ...tree2Options, palette: 0 }, - autumn: { ...tree2Options, palette: treeAutumnPals[i] }, - winter: { ...tree2Options, palette: 1 }, - }))); + mixDrawSeasonal({ + summer: { ...tree2Options, palette: 0 }, + autumn: { ...tree2Options, palette: treeAutumnPals[i] }, + winter: { ...tree2Options, palette: 1 }, + }))); export const trees3 = times(3, i => registerMix(n(`tree3-${i}`), - mixDrawSeasonal({ - summer: { ...tree3Options, palette: 0 }, - autumn: { ...tree3Options, palette: treeAutumnPals[i] }, - winter: { ...tree3Options, palette: 1 }, - }), - mixColliderRounded(-3, -2, 6, 4, 1))); + mixDrawSeasonal({ + summer: { ...tree3Options, palette: 0 }, + autumn: { ...tree3Options, palette: treeAutumnPals[i] }, + winter: { ...tree3Options, palette: 1 }, + }), + mixColliderRounded(-3, -2, 6, 4, 1))); export const tree1 = trees1[0]; export const tree2 = trees2[0]; export const tree3 = trees3[0]; export const [tree4] = createTree(n('tree4'), 31, 92, 12, { - stumpCollider: mixColliderRounded(-5, -1, 12, 6, 1, stumpsTall), - trunkCollider: mixColliderRounded(-5, -1, 12, 6, 1), - cover: rect(-20, -77, 42, 60), - variants: times(3, i => - ({ - stump: sprites.tree_4Stump0, - trunk: sprites.tree_4Trunk0, - crown: sprites.tree_4Crown0_0, - palette: 0, - paletteAutumn: treeAutumnPals[i], - paletteWinter: 1, - })), + stumpCollider: mixColliderRounded(-5, -1, 12, 6, 1, stumpsTall), + trunkCollider: mixColliderRounded(-5, -1, 12, 6, 1), + cover: rect(-20, -77, 42, 60), + variants: times(3, i => + ({ + stump: sprites.tree_4Stump0, + trunk: sprites.tree_4Trunk0, + crown: sprites.tree_4Crown0_0, + palette: 0, + paletteAutumn: treeAutumnPals[i], + paletteWinter: 1, + })), }); export const [tree5, [tree5Stump]] = createTree(n('tree5'), 43, 128, 24, { - stumpCollider: mixColliderRounded(-8, -2, 16, 8, 2, stumpsTall), - trunkCollider: mixColliderRounded(-8, -2, 16, 8, 2), - cover: rect(-30, -106, 64, 80), - variants: times(3, i => - ({ - stump: sprites.tree_5Stump0, - trunk: sprites.tree_5Trunk0, - crown: sprites.tree_5Crown0_0, - palette: 0, - paletteAutumn: treeAutumnPals[i], - paletteWinter: 1, - })), + stumpCollider: mixColliderRounded(-8, -2, 16, 8, 2, stumpsTall), + trunkCollider: mixColliderRounded(-8, -2, 16, 8, 2), + cover: rect(-30, -106, 64, 80), + variants: times(3, i => + ({ + stump: sprites.tree_5Stump0, + trunk: sprites.tree_5Trunk0, + crown: sprites.tree_5Crown0_0, + palette: 0, + paletteAutumn: treeAutumnPals[i], + paletteWinter: 1, + })), }); export const [tree, [treeStump1, treeStump2]] = createTree(n('tree'), 80, 162, 30, { - stumpCollider: mixColliderRounded(-16, -1, 32, 12, 4, stumpsTall), - trunkCollider: mixColliderRounded(-16, -1, 32, 12, 4), - cover: rect(-50, -135, 110, 120), - variants: flatten(times(3, i => [ - { - stump: sprites.tree_6Stump0, - stumpWinter: sprites.tree_6StumpWinter0, - trunk: sprites.tree_6Trunk0, - crown: sprites.tree_6Crown0_0, - webX: 0, webY: 0, spiderHeight: 19, - palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], - }, - { - stump: sprites.tree_6Stump1, - stumpWinter: sprites.tree_6StumpWinter1, - trunk: sprites.tree_6Trunk1, - crown: sprites.tree_6Crown0_1, - webX: -2, webY: 0, spiderHeight: 19, - palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], - }, - { - stump: sprites.tree_6Stump0, - stumpWinter: sprites.tree_6StumpWinter0, - trunk: sprites.tree_6Trunk0, - crown: sprites.tree_6Crown1_0, - webX: 0, webY: -4, spiderHeight: 27, - palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], - }, - { - stump: sprites.tree_6Stump1, - stumpWinter: sprites.tree_6StumpWinter1, - trunk: sprites.tree_6Trunk1, - crown: sprites.tree_6Crown1_1, - webX: -2, webY: -4, spiderHeight: 27, - palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], - }, - ])) + stumpCollider: mixColliderRounded(-16, -1, 32, 12, 4, stumpsTall), + trunkCollider: mixColliderRounded(-16, -1, 32, 12, 4), + cover: rect(-50, -135, 110, 120), + variants: flatten(times(3, i => [ + { + stump: sprites.tree_6Stump0, + stumpWinter: sprites.tree_6StumpWinter0, + trunk: sprites.tree_6Trunk0, + crown: sprites.tree_6Crown0_0, + webX: 0, webY: 0, spiderHeight: 19, + palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], + }, + { + stump: sprites.tree_6Stump1, + stumpWinter: sprites.tree_6StumpWinter1, + trunk: sprites.tree_6Trunk1, + crown: sprites.tree_6Crown0_1, + webX: -2, webY: 0, spiderHeight: 19, + palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], + }, + { + stump: sprites.tree_6Stump0, + stumpWinter: sprites.tree_6StumpWinter0, + trunk: sprites.tree_6Trunk0, + crown: sprites.tree_6Crown1_0, + webX: 0, webY: -4, spiderHeight: 27, + palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], + }, + { + stump: sprites.tree_6Stump1, + stumpWinter: sprites.tree_6StumpWinter1, + trunk: sprites.tree_6Trunk1, + crown: sprites.tree_6Crown1_1, + webX: -2, webY: -4, spiderHeight: 27, + palette: 0, paletteWinter: 1, paletteAutumn: treeAutumnPals[i], + }, + ])) }); const pine1Options = { sprite: sprites.pine_1, dx: 7, dy: 18 }; const pine2Options = { sprite: sprites.pine_2, dx: 10, dy: 35 }; export const pine1 = registerMix(n('pine1'), - mixDrawSeasonal({ - summer: { ...pine1Options, palette: 0 }, - autumn: { ...pine1Options, palette: 1 }, - winter: { ...pine1Options, palette: 2 }, - })); + mixDrawSeasonal({ + summer: { ...pine1Options, palette: 0 }, + autumn: { ...pine1Options, palette: 1 }, + winter: { ...pine1Options, palette: 2 }, + })); export const pine2 = registerMix(n('pine2'), - mixDrawSeasonal({ - summer: { ...pine2Options, palette: 0 }, - autumn: { ...pine2Options, palette: 1 }, - winter: { ...pine2Options, palette: 2 }, - }), - mixColliderRounded(-3, -2, 6, 4, 1)); + mixDrawSeasonal({ + summer: { ...pine2Options, palette: 0 }, + autumn: { ...pine2Options, palette: 1 }, + winter: { ...pine2Options, palette: 2 }, + }), + mixColliderRounded(-3, -2, 6, 4, 1)); export const [pine3] = createTree(n('pine3'), 25, 68, 2, { - stumpCollider: mixColliderRounded(-5, -1, 12, 6, 1, stumpsTall), - trunkCollider: mixColliderRounded(-5, -1, 12, 6, 1), - crownCollider: mixColliderRounded(-14, -6, 29, 12, 4), - cover: rect(-17, -41, 35, 40), - variants: [ - { stump: sprites.pine_3Stump0, crown: sprites.pine_3Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, - ] + stumpCollider: mixColliderRounded(-5, -1, 12, 6, 1, stumpsTall), + trunkCollider: mixColliderRounded(-5, -1, 12, 6, 1), + crownCollider: mixColliderRounded(-14, -6, 29, 12, 4), + cover: rect(-17, -41, 35, 40), + variants: [ + { stump: sprites.pine_3Stump0, crown: sprites.pine_3Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, + ] }); export const [pine4] = createTree(n('pine4'), 41, 95, 8, { - stumpCollider: mixColliderRounded(-5, 4, 11, 6, 1, stumpsTall), - trunkCollider: mixColliderRounded(-5, 4, 11, 6, 1), - crownCollider: mixColliderRounded(-23, -8, 46, 20, 6), - cover: rect(-23, -68, 46, 70), - variants: [ - { stump: sprites.pine_4Stump0, crown: sprites.pine_4Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, - ] + stumpCollider: mixColliderRounded(-5, 4, 11, 6, 1, stumpsTall), + trunkCollider: mixColliderRounded(-5, 4, 11, 6, 1), + crownCollider: mixColliderRounded(-23, -8, 46, 20, 6), + cover: rect(-23, -68, 46, 70), + variants: [ + { stump: sprites.pine_4Stump0, crown: sprites.pine_4Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, + ] }); export const [pine5] = createTree(n('pine5'), 53, 136, 5, { - stumpCollider: mixColliderRounded(-8, -3, 18, 10, 4, stumpsTall), - trunkCollider: mixColliderRounded(-8, -3, 18, 10, 4), - crownCollider: mixColliderRounded(-29, -12, 60, 25, 6), - cover: rect(-38, -95, 80, 100), - variants: [ - { stump: sprites.pine_5Stump0, crown: sprites.pine_5Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, - ] + stumpCollider: mixColliderRounded(-8, -3, 18, 10, 4, stumpsTall), + trunkCollider: mixColliderRounded(-8, -3, 18, 10, 4), + crownCollider: mixColliderRounded(-29, -12, 60, 25, 6), + cover: rect(-38, -95, 80, 100), + variants: [ + { stump: sprites.pine_5Stump0, crown: sprites.pine_5Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, + ] }); const xmasCrown: ColorShadow = { - color: sprites.christmastree.color, - shadow: sprites.pine_6Crown0_0.shadow, - palettes: sprites.christmastree.palettes, + color: sprites.christmastree.color, + shadow: sprites.pine_6Crown0_0.shadow, + palettes: sprites.christmastree.palettes, }; export const [pine] = createTree(n('pine'), 75, 180, 17, { - stumpCollider: mixColliderRounded(-16, -1, 32, 14, 4, stumpsTall), - trunkCollider: mixColliderRounded(-16, -1, 32, 14, 4), - crownCollider: mixColliderRounded(-38, -21, 76, 31, 7), - cover: rect(-55, -120, 110, 133), - variants: [ - { stump: sprites.pine_6Stump0, crown: sprites.pine_6Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, - { stump: sprites.pine_6Stump0, crown: xmasCrown, palette: 0 }, - ] + stumpCollider: mixColliderRounded(-16, -1, 32, 14, 4, stumpsTall), + trunkCollider: mixColliderRounded(-16, -1, 32, 14, 4), + crownCollider: mixColliderRounded(-38, -21, 76, 31, 7), + cover: rect(-55, -120, 110, 133), + variants: [ + { stump: sprites.pine_6Stump0, crown: sprites.pine_6Crown0_0, palette: 0, paletteAutumn: 1, paletteWinter: 2 }, + { stump: sprites.pine_6Stump0, crown: xmasCrown, palette: 0 }, + ] }); // tree helpers interface TreeVariant { - stump: PaletteRenderable; - stumpWinter?: PaletteRenderable; - trunk?: PaletteRenderable; - crown: PaletteRenderable; - webX?: number; - webY?: number; - spiderHeight?: number; - palette?: number; - paletteWinter?: number; - paletteAutumn?: number; + stump: PaletteRenderable; + stumpWinter?: PaletteRenderable; + trunk?: PaletteRenderable; + crown: PaletteRenderable; + webX?: number; + webY?: number; + spiderHeight?: number; + palette?: number; + paletteWinter?: number; + paletteAutumn?: number; } interface TreeParams { - variants: TreeVariant[]; - cover: Rect; - stumpCollider?: MixinEntity; - trunkCollider?: MixinEntity; - crownCollider?: MixinEntity; + variants: TreeVariant[]; + cover: Rect; + stumpCollider?: MixinEntity; + trunkCollider?: MixinEntity; + crownCollider?: MixinEntity; } type CreateTreeMethod = (x: number, y: number, v: number, hasWeb?: boolean, hasSpider?: boolean) => Entity[]; function createTree( - name: string, offsetX: number, offsetY: number, crownOffset: number, - { cover, stumpCollider, trunkCollider, crownCollider, variants }: TreeParams, + name: string, offsetX: number, offsetY: number, crownOffset: number, + { cover, stumpCollider, trunkCollider, crownCollider, variants }: TreeParams, ): [CreateTreeMethod, CreateEntityMethod[], (CreateEntityMethod | undefined)[], CreateEntityMethod[]] { - const trunkCover: MixinEntity = base => base.coverBounds = cover; - const crownCover = mixCover(cover.x, cover.y - crownOffset, cover.w, cover.h); - const crownFlags = mixServerFlags(ServerFlags.TreeCrown); - const crownMinimap = mixMinimap(0x386c4fff, rect(-1, -1, 3, 3), 2); + const trunkCover: MixinEntity = base => base.coverBounds = cover; + const crownCover = mixCover(cover.x, cover.y - crownOffset, cover.w, cover.h); + const crownFlags = mixServerFlags(ServerFlags.TreeCrown); + const crownMinimap = mixMinimap(0x386c4fff, rect(-1, -1, 3, 3), 2); - const stumps = variants.map((v, i) => v.stump && registerMix(n(`${name}-stump-${i}`), - mixDrawSeasonal({ - summer: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.palette || 0 }, - autumn: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.paletteAutumn || v.palette || 0 }, - winter: { sprite: v.stumpWinter || v.stump, dx: offsetX, dy: offsetY, palette: v.paletteWinter || v.palette || 0 }, - }), - stumpCollider, - mixOrder(1))); + const stumps = variants.map((v, i) => v.stump && registerMix(n(`${name}-stump-${i}`), + mixDrawSeasonal({ + summer: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.palette || 0 }, + autumn: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.paletteAutumn || v.palette || 0 }, + winter: { sprite: v.stumpWinter || v.stump, dx: offsetX, dy: offsetY, palette: v.paletteWinter || v.palette || 0 }, + }), + stumpCollider, + mixOrder(1))); - const stumpsTall = variants.map((v, i) => v.stump && registerMix(n(`${name}-stump-tall-${i}`), - mixDrawSeasonal({ - summer: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.palette || 0 }, - autumn: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.paletteAutumn || v.palette || 0 }, - winter: { sprite: v.stumpWinter || v.stump, dx: offsetX, dy: offsetY, palette: v.paletteWinter || v.palette || 0 }, - }), - trunkCollider, - mixOrder(1))); + const stumpsTall = variants.map((v, i) => v.stump && registerMix(n(`${name}-stump-tall-${i}`), + mixDrawSeasonal({ + summer: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.palette || 0 }, + autumn: { sprite: v.stump, dx: offsetX, dy: offsetY, palette: v.paletteAutumn || v.palette || 0 }, + winter: { sprite: v.stumpWinter || v.stump, dx: offsetX, dy: offsetY, palette: v.paletteWinter || v.palette || 0 }, + }), + trunkCollider, + mixOrder(1))); - const trunks = variants.map((v, i) => v.trunk && registerMix(n(`${name}-trunk-${i}`), - mixDrawSeasonal({ - summer: { sprite: v.trunk, dx: offsetX, dy: offsetY, palette: v.palette || 0 }, - autumn: { sprite: v.trunk, dx: offsetX, dy: offsetY, palette: v.paletteAutumn || v.palette || 0 }, - winter: { sprite: v.trunk, dx: offsetX, dy: offsetY, palette: v.paletteWinter || v.palette || 0 }, - }), - trunkCover, - mixOrder(2))); + const trunks = variants.map((v, i) => v.trunk && registerMix(n(`${name}-trunk-${i}`), + mixDrawSeasonal({ + summer: { sprite: v.trunk, dx: offsetX, dy: offsetY, palette: v.palette || 0 }, + autumn: { sprite: v.trunk, dx: offsetX, dy: offsetY, palette: v.paletteAutumn || v.palette || 0 }, + winter: { sprite: v.trunk, dx: offsetX, dy: offsetY, palette: v.paletteWinter || v.palette || 0 }, + }), + trunkCover, + mixOrder(2))); - const crowns = variants.map((v, i) => v.crown && registerMix(n(`${name}-crown-${i}`), - mixDrawSeasonal({ - summer: { sprite: v.crown, dx: offsetX, dy: offsetY + crownOffset, palette: v.palette || 0 }, - autumn: { sprite: v.crown, dx: offsetX, dy: offsetY + crownOffset, palette: v.paletteAutumn || v.palette || 0 }, - winter: { sprite: v.crown, dx: offsetX, dy: offsetY + crownOffset, palette: v.paletteWinter || v.palette || 0 }, - }), - crownCollider, crownCover, crownFlags, crownMinimap)); + const crowns = variants.map((v, i) => v.crown && registerMix(n(`${name}-crown-${i}`), + mixDrawSeasonal({ + summer: { sprite: v.crown, dx: offsetX, dy: offsetY + crownOffset, palette: v.palette || 0 }, + autumn: { sprite: v.crown, dx: offsetX, dy: offsetY + crownOffset, palette: v.paletteAutumn || v.palette || 0 }, + winter: { sprite: v.crown, dx: offsetX, dy: offsetY + crownOffset, palette: v.paletteWinter || v.palette || 0 }, + }), + crownCollider, crownCover, crownFlags, crownMinimap)); - const trees = variants.map((v, i) => ({ - stump: stumps[i], - stumpTall: stumpsTall[i], - trunk: trunks[i], - crown: crowns[i], - webX: v.webX, - webY: v.webY, - spiderHeight: v.spiderHeight, - })); + const trees = variants.map((v, i) => ({ + stump: stumps[i], + stumpTall: stumpsTall[i], + trunk: trunks[i], + crown: crowns[i], + webX: v.webX, + webY: v.webY, + spiderHeight: v.spiderHeight, + })); - function tree(x: number, y: number, v?: number, hasWeb?: boolean, hasSpider?: boolean): Entity[] { - const { stumpTall, trunk, crown, webX, webY, spiderHeight } = trees[v || 0]; + function tree(x: number, y: number, v?: number, hasWeb?: boolean, hasSpider?: boolean): Entity[] { + const { stumpTall, trunk, crown, webX, webY, spiderHeight } = trees[v || 0]; - return compact([ - stumpTall && stumpTall(x, y), - trunk && trunk(x, y), - crown && crown(x, y + (crownOffset / tileHeight)), - hasWeb ? web(x + (webX! / tileWidth), y + (webY! / tileHeight)) : undefined, - hasSpider ? spider(x - 1, y + 0.3, { height: spiderHeight!, time: Math.random() * 100 }) : undefined, - ]); - } + return compact([ + stumpTall && stumpTall(x, y), + trunk && trunk(x, y), + crown && crown(x, y + (crownOffset / tileHeight)), + hasWeb ? web(x + (webX! / tileWidth), y + (webY! / tileHeight)) : undefined, + hasSpider ? spider(x - 1, y + 0.3, { height: spiderHeight!, time: Math.random() * 100 }) : undefined, + ]); + } - return [tree, stumps, trunks, crowns]; + return [tree, stumps, trunks, crowns]; } export const stashEntities = [ - rose, cookie, cookiePony, letter, rope, + rose, cookie, cookiePony, letter, rope, ]; export const placeableEntities: { type: number; name: string; }[] = [ - { type: cushion1.type, name: 'Cushion' }, - { type: barrel.type, name: 'Barrel' }, - { type: box.type, name: 'Box' }, - { type: boxLanterns.type, name: 'Box of lanterns' }, - { type: boxFruits.type, name: 'Box of fruits' }, - { type: cookieTable2.type, name: 'Cookie table' }, - { type: table1.type, name: 'Small table' }, - { type: table2.type, name: 'Large table' }, - { type: table3.type, name: 'Long table' }, - { type: lanternOn.type, name: 'Lantern' }, - { type: pumpkin.type, name: 'Pumpkin' }, - { type: jackoOn.type, name: `Jack-o'-lantern (lit)` }, - { type: jackoOff.type, name: `Jack-o'-lantern (unlit)` }, - { type: crate1A.type, name: 'Large crate' }, - { type: crate3A.type, name: 'Small crate' }, - { type: crate2A.type, name: 'Lockbox' }, - { type: largeLeafedBush1.type, name: 'Large plant' }, - { type: largeLeafedBush3.type, name: 'Small plant' }, - { type: picture1.type, name: 'Picture (1)' }, - { type: picture2.type, name: 'Picture (2)' }, - { type: window1.type, name: 'Window' }, - { type: bookshelf.type, name: 'Bookshelf' }, - { type: rock.type, name: 'Rock' }, + { type: cushion1.type, name: 'Cushion' }, + { type: barrel.type, name: 'Barrel' }, + { type: box.type, name: 'Box' }, + { type: boxLanterns.type, name: 'Box of lanterns' }, + { type: boxFruits.type, name: 'Box of fruits' }, + { type: cookieTable2.type, name: 'Cookie table' }, + { type: table1.type, name: 'Small table' }, + { type: table2.type, name: 'Large table' }, + { type: table3.type, name: 'Long table' }, + { type: lanternOn.type, name: 'Lantern' }, + { type: pumpkin.type, name: 'Pumpkin' }, + { type: jackoOn.type, name: `Jack-o'-lantern (lit)` }, + { type: jackoOff.type, name: `Jack-o'-lantern (unlit)` }, + { type: crate1A.type, name: 'Large crate' }, + { type: crate3A.type, name: 'Small crate' }, + { type: crate2A.type, name: 'Lockbox' }, + { type: largeLeafedBush1.type, name: 'Large plant' }, + { type: largeLeafedBush3.type, name: 'Small plant' }, + { type: picture1.type, name: 'Picture (1)' }, + { type: picture2.type, name: 'Picture (2)' }, + { type: window1.type, name: 'Window' }, + { type: bookshelf.type, name: 'Bookshelf' }, + { type: rock.type, name: 'Rock' }, ]; export const fruits = [ - apple, apple2, appleGreen, appleGreen2, orange, pear, banana, - lemon, lime, carrotHeld, mango, grapesGreen[0], grapesPurple[0], + apple, apple2, appleGreen, appleGreen2, orange, pear, banana, + lemon, lime, carrotHeld, mango, grapesGreen[0], grapesPurple[0], ]; export const tools = [ - { type: saw.type, text: 'Saw: place & remove walls' }, - { type: broom.type, text: 'Broom: remove furniture' }, - { type: hammer.type, text: 'Hammer: place furniture\nuse [mouse wheel] to switch item' }, - { type: shovel.type, text: 'Shovel: change floor\nuse [mouse wheel] to switch floor type' }, + { type: saw.type, text: 'Saw: place & remove walls' }, + { type: broom.type, text: 'Broom: remove furniture' }, + { type: hammer.type, text: 'Hammer: place furniture\nuse [mouse wheel] to switch item' }, + { type: shovel.type, text: 'Shovel: change floor\nuse [mouse wheel] to switch floor type' }, ]; export const candies1Types = [candyCane1, candyCane2, cookie, cookiePony].map(e => e.type); export const candies2Types = [cookie, cookiePony].map(e => e.type); if (DEVELOPMENT) { - if (pony.type !== PONY_TYPE) { - throw new Error(`Invalid pony type ${pony.type} !== ${PONY_TYPE}`); - } + if (pony.type !== PONY_TYPE) { + throw new Error(`Invalid pony type ${pony.type} !== ${PONY_TYPE}`); + } - for (const { type } of entities) { - if (type === 0) - continue; + for (const { type } of entities) { + if (type === 0) + continue; - const entity = createAnEntity(type, 0, 0, 0, {}, mockPaletteManager, defaultWorldState); - const name = getEntityTypeName(type); + const entity = createAnEntity(type, 0, 0, 0, {}, mockPaletteManager, defaultWorldState); + const name = getEntityTypeName(type); - if (entity.colliders) { - const maxWidth = (hasFlag(entity.flags, EntityFlags.Movable) ? 1 : 4) * tileWidth; - const maxHeight = (hasFlag(entity.flags, EntityFlags.Movable) ? 1 : 5) * tileHeight; + if (entity.colliders) { + const maxWidth = (hasFlag(entity.flags, EntityFlags.Movable) ? 1 : 4) * tileWidth; + const maxHeight = (hasFlag(entity.flags, EntityFlags.Movable) ? 1 : 5) * tileHeight; - for (const { x, y, w, h } of entity.colliders) { - if ((x < -maxWidth || (x + w) > maxWidth || y < -maxHeight || (y + h) > maxHeight)) { - throw new Error(`Invalid entity "${name}": Collider too large ${JSON.stringify({ x, y, w, h })}`); - } - } - } - } + for (const { x, y, w, h } of entity.colliders) { + if ((x < -maxWidth || (x + w) > maxWidth || y < -maxHeight || (y + h) > maxHeight)) { + throw new Error(`Invalid entity "${name}": Collider too large ${JSON.stringify({ x, y, w, h })}`); + } + } + } + } } diff --git a/src/ts/common/entityUtils.ts b/src/ts/common/entityUtils.ts index 7d56fa7..c604d8f 100644 --- a/src/ts/common/entityUtils.ts +++ b/src/ts/common/entityUtils.ts @@ -1,6 +1,6 @@ import { sort } from 'timsort'; import { - Entity, EntityState, BodyAnimation, Point, EntityFlags, IMap, Pony, Says, EntityPlayerState, WorldMap + 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'; @@ -13,91 +13,91 @@ import { PONY_TYPE } from './constants'; import { isStaticCollision } from './collision'; export function releaseEntity(entity: Entity) { - if (isPony(entity)) { - releasePony(entity); - } + if (isPony(entity)) { + releasePony(entity); + } - if (entity.palettes !== undefined) { - for (const palette of entity.palettes) { - releasePalette(palette); - } - } + 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); + 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); + 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) - ); + 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); + 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]); + 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); + 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; + return entity.vx !== 0 || entity.vy !== 0; } export function isDrawable(entity: Entity) { - return entity.type === PONY_TYPE || entity.draw !== undefined; + return entity.type === PONY_TYPE || entity.draw !== undefined; } export function canLand(entity: Entity, map: IMap) { - return !isStaticCollision(entity, map, true); + return !isStaticCollision(entity, map, true); } export function canStand(entity: Entity, map: IMap) { - return !isPonyStanding(entity) && isPonyLandedOrCanLand(entity, map); + return !isPonyStanding(entity) && isPonyLandedOrCanLand(entity, map); } export function canSit(entity: Entity, map: IMap) { - return !isPonySitting(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity); + return !isPonySitting(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity); } export function canLie(entity: Entity, map: IMap) { - return !isPonyLying(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity); + return !isPonyLying(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity); } export function entityInRange(entity: Entity, player: Entity) { - return (!entity.interactRange || distance(player, entity) < entity.interactRange); + 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)); + 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)); + 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; @@ -105,121 +105,121 @@ 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)); + 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; + 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); + return !isMoving(pony) && isIdleAnimation(pony.ponyState.animation); } export function canBoop(pony: Pony) { - return isIdle(pony); + return isIdle(pony); } export function canBoop2(entity: Entity) { - return !isMoving(entity) && (isPonyStanding(entity) || isPonySitting(entity) || isPonyLying(entity) || isPonyFlying(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; + return (entity.playerState & EntityPlayerState.Hidden) !== 0; } export function isIgnored(entity: Entity) { - return (entity.playerState & EntityPlayerState.Ignored) !== 0; + return (entity.playerState & EntityPlayerState.Ignored) !== 0; } export function isFriend(entity: Entity) { - return (entity.playerState & EntityPlayerState.Friend) !== 0; + return (entity.playerState & EntityPlayerState.Friend) !== 0; } export function isInTheAir(entity: Entity) { - return isFlying(entity) && (entity.inTheAirDelay === undefined || entity.inTheAirDelay <= 0); + return isFlying(entity) && (entity.inTheAirDelay === undefined || entity.inTheAirDelay <= 0); } // entity state export function isFlying(entity: Entity) { - return (entity.state & EntityState.Flying) !== 0; + return (entity.state & EntityState.Flying) !== 0; } export function isFacingRight(entity: Entity) { - return (entity.state & EntityState.FacingRight) !== 0; + return (entity.state & EntityState.FacingRight) !== 0; } export function hasHeadTurned(entity: Entity) { - return (entity.state & EntityState.HeadTurned) !== 0; + return (entity.state & EntityState.HeadTurned) !== 0; } export function isHeadFacingRight(entity: Entity) { - const headTurned = hasHeadTurned(entity); - const facingRight = isFacingRight(entity); - return facingRight ? !headTurned : headTurned; + const headTurned = hasHeadTurned(entity); + const facingRight = isFacingRight(entity); + return facingRight ? !headTurned : headTurned; } export function getPonyState(state: EntityState): EntityState { - return state & EntityState.PonyStateMask; + 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; + 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; + return getPonyState(state) === EntityState.PonySitting; } export function isLyingState(state: EntityState) { - return getPonyState(state) === EntityState.PonyLying; + return getPonyState(state) === EntityState.PonyLying; } export function isPonyWalking(entity: Entity) { - return getPonyState(entity.state) === EntityState.PonyWalking; + return getPonyState(entity.state) === EntityState.PonyWalking; } export function isPonyTrotting(entity: Entity) { - return getPonyState(entity.state) === EntityState.PonyTrotting; + return getPonyState(entity.state) === EntityState.PonyTrotting; } export function isPonySitting(entity: Entity) { - return getPonyState(entity.state) === EntityState.PonySitting; + return getPonyState(entity.state) === EntityState.PonySitting; } export function isPonyStanding(entity: Entity) { - return getPonyState(entity.state) === EntityState.PonyStanding; + return getPonyState(entity.state) === EntityState.PonyStanding; } export function isPonyLying(entity: Entity) { - return getPonyState(entity.state) === EntityState.PonyLying; + return getPonyState(entity.state) === EntityState.PonyLying; } export function isPonyFlying(entity: Entity) { - return getPonyState(entity.state) === EntityState.PonyFlying; + return getPonyState(entity.state) === EntityState.PonyFlying; } export function isPonyLandedOrCanLand(entity: Entity, map: IMap) { - return !isPonyFlying(entity) || canLand(entity, map); + return !isPonyFlying(entity) || canLand(entity, map); } // entity flags export function isDecal(entity: Entity) { - return (entity.flags & EntityFlags.Decal) !== 0; + return (entity.flags & EntityFlags.Decal) !== 0; } export function isCritter(entity: Entity) { - return (entity.flags & EntityFlags.Critter) !== 0; + return (entity.flags & EntityFlags.Critter) !== 0; } diff --git a/src/ts/common/expressionUtils.ts b/src/ts/common/expressionUtils.ts index 4a8e3e1..58cc8f1 100644 --- a/src/ts/common/expressionUtils.ts +++ b/src/ts/common/expressionUtils.ts @@ -7,26 +7,26 @@ 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', + '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', + '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('|')})$`); @@ -39,120 +39,120 @@ 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)], + [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([ - ...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)], + ...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([ - ...muzzlesBoth, - [Muzzle.Smile, ...smilesLeft], - [Muzzle.Frown, ...smilesRight], - [Muzzle.ConcernedOpen2, 'D'], - [Muzzle.ConcernedOpen3, 'DD', 'DDD'], - [Muzzle.SmileTeeth, ...double(smilesLeft)], - [Muzzle.FrownTeeth, ...double(smilesRight)], + ...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, '|'], + [Eye.Neutral, ...neutralEyes], + [Eye.X, 'X', 'x'], + [Eye.Neutral3, 'B'], + [Eye.Lines, '|'], ]; export const verticalEyesRight = createMap([ - ...verticalEyesBoth, - [Eye.Angry, ...prefix(neutralEyes, '>')], - [Eye.Angry2, '>B'], - [Eye.Sad, ...prefix(neutralEyes, '<')], - [Eye.Sad2, '')], + [Eye.Angry2, '>B'], + [Eye.Sad, ...prefix(neutralEyes, '<')], + [Eye.Sad2, '([ - ...verticalEyesBoth, - [Eye.Angry, ...suffix(neutralEyes, '<')], - [Eye.Sad, ...suffix(neutralEyes, '>')], - [Eye.Frown, ...suffix(neutralEyes, '|')], + ...verticalEyesBoth, + [Eye.Angry, ...suffix(neutralEyes, '<')], + [Eye.Sad, ...suffix(neutralEyes, '>')], + [Eye.Frown, ...suffix(neutralEyes, '|')], ]); // horizontal -_- export const horizontalMuzzles = createMap([ - [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'], + [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'], + [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([ - ...horizontalEyes, - [Eye.Neutral2, '>'], - [Eye.X, '<'], - [Eye.Sad, 'ò', 'Ò'], - [Eye.Angry, 'ó', 'Ó'], + ...horizontalEyes, + [Eye.Neutral2, '>'], + [Eye.X, '<'], + [Eye.Sad, 'ò', 'Ò'], + [Eye.Angry, 'ó', 'Ó'], ]); export const horizontalEyesRight = createMap([ - ...horizontalEyes, - [Eye.Neutral2, '<'], - [Eye.X, '>'], - [Eye.Sad, 'ó', 'Ó'], - [Eye.Angry, 'ò', 'Ò'], + ...horizontalEyes, + [Eye.Neutral2, '<'], + [Eye.X, '>'], + [Eye.Sad, 'ó', 'Ó'], + [Eye.Angry, 'ò', 'Ò'], ]); const horizontalIrises = createMap([ - [Iris.Up, '9'], - [Iris.UpLeft, 'e'], - [Iris.UpRight, 'g'], - [Iris.Right, '<', 'd'], - [Iris.Left, '>', 'b'], + [Iris.Up, '9'], + [Iris.UpLeft, 'e'], + [Iris.UpRight, 'g'], + [Iris.Right, '<', 'd'], + [Iris.Left, '>', 'b'], ]); const muzzleToEye: Eye[] = []; @@ -166,7 +166,7 @@ neutralToSmile[Muzzle.ConcernedOpen] = Muzzle.SmileOpen2; neutralToSmile[Muzzle.ConcernedOpen2] = Muzzle.SmileOpen3; function any(obj: object) { - return `(${Object.keys(obj).map(escapeRegExp).join('|')})`; + return `(${Object.keys(obj).map(escapeRegExp).join('|')})`; } const bigEyes = /[O0ÒÓÔÕŌŎQ]/; @@ -178,175 +178,175 @@ const verticalLeftRegex = new RegExp(`^${any(muzzlesLeft)}-?${tears}${any(vertic const horizontalRegex = new RegExp(`^${any(horizontalEyesRight)}(//)?${any(horizontalMuzzles)}(//)?${any(horizontalEyesLeft)}$`); function matchVertical( - text: string, regex: RegExp, flip: boolean, muzzleMap: Dict, eyesMap: Dict + text: string, regex: RegExp, flip: boolean, muzzleMap: Dict, eyesMap: Dict ): Expression | undefined { - if (/^([|]{2,}|BS|8x|x8|x-?x|\d+)$/i.test(text)) - return undefined; + if (/^([|]{2,}|BS|8x|x8|x-?x|\d+)$/i.test(text)) + return undefined; - const match = regex.exec(text); + const match = regex.exec(text); - if (!match) - return undefined; + 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; + 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 }; + 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 (/\.\.|--|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 (/[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 (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 (/[a-z][a-z][.,*-]/i.test(text)) { + const clear = text.replace(/[^a-z]/ig, '').toLowerCase(); - if (clear.length === 2 && twoLetterWords.test(clear)) { - return undefined; - } - } + if (clear.length === 2 && twoLetterWords.test(clear)) { + return undefined; + } + } - const match = horizontalRegex.exec(text); + const match = horizontalRegex.exec(text); - if (!match) { - return undefined; - } + if (!match) { + return undefined; + } - const [, rightStr, rightBlush, muzzleStr, leftBlush, leftStr] = match; + const [, rightStr, rightBlush, muzzleStr, leftBlush, leftStr] = match; - if ((rightBlush || leftBlush) && rightBlush !== leftBlush) { - return undefined; - } + 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); + 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), - }; + 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 + right: Eye, left: Eye, muzzle: Muzzle, rightIris = Iris.Forward, leftIris = Iris.Forward, extra = ExpressionExtra.None ): Expression { - return { right, left, muzzle, rightIris, leftIris, extra }; + 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), + '^^': () => 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](); - } + 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; - } + if (/тот/ui.test(text)) { + return undefined; + } - text = replaceRussian(text) - .replace(/D{4,}/, 'DDD') - .replace(/\\/g, '/') - .replace(/\/{3,}/g, '//'); + 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); + 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; + const emoteMatch = /(?:^| )(\S+)\s*$/.exec(text); + const emote = emoteMatch && emoteMatch[1].trim(); + return emote ? matchExpression(emote) : undefined; } function createMap(values: any[][]): Dict { - return values.reduce((obj: Dict, [exp, ...values]) => (values.forEach(v => obj[v] = exp), obj), Object.create(null)); + return values.reduce((obj: Dict, [exp, ...values]) => (values.forEach(v => obj[v] = exp), obj), Object.create(null)); } const charMap = createPlainMap({ - 'З': '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', + 'З': '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]; + return charMap[x]; } function replaceRussian(text: string): string { - return text.replace(charRegex, mapChar); + return text.replace(charRegex, mapChar); } diff --git a/src/ts/common/expressions.ts b/src/ts/common/expressions.ts index ee3c5a5..289e9b7 100644 --- a/src/ts/common/expressions.ts +++ b/src/ts/common/expressions.ts @@ -2,294 +2,294 @@ 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]; + | [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]], - [';(', [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]], + // 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]], + [';(', [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]], ]; diff --git a/src/ts/common/filterUtils.ts b/src/ts/common/filterUtils.ts index 8288e64..d9b7fe0 100644 --- a/src/ts/common/filterUtils.ts +++ b/src/ts/common/filterUtils.ts @@ -8,157 +8,157 @@ 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', + '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; - } + 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 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', 'Ж'], + [`'`, 'Ъ ъ Ь ь'], + ['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); + 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')); + return latinize(name + .replace(/[ǫ]/ui, 'q') + .replace(/[с]/ui, 'c') + .replace(/[н]|\|-\|/ui, 'h') + .replace(/[лпий]/ui, 'n')); } diff --git a/src/ts/common/interfaces.ts b/src/ts/common/interfaces.ts index f14bdaa..32b6ff2 100644 --- a/src/ts/common/interfaces.ts +++ b/src/ts/common/interfaces.ts @@ -9,788 +9,788 @@ export type Matrix4 = Float32Array; export type Matrix2D = Float32Array; export interface Dict { - [key: string]: T; + [key: string]: T; } export const enum Season { - Summer = 1, - Autumn = 2, - Winter = 4, - Spring = 8, + Summer = 1, + Autumn = 2, + Winter = 4, + Spring = 8, } export const enum Holiday { - None, - Christmas, - Halloween, - StPatricks, - Easter, + None, + Christmas, + Halloween, + StPatricks, + Easter, } export const enum Weather { - None, - Rain, + None, + Rain, } export const enum MapType { - None, - Island, - House, - Cave, + None, + Island, + House, + Cave, } export const enum MapFlags { - None = 0, - EditableWalls = 1, - EditableEntities = 2, - EditableTiles = 4, - EdibleGrass = 8, + None = 0, + EditableWalls = 1, + EditableEntities = 2, + EditableTiles = 4, + EdibleGrass = 8, } export const enum NotificationFlags { - None = 0, - Ok = 1, - Yes = 2, - No = 4, - Accept = 8, - Reject = 16, - Supporter = 32, - Ignore = 64, - NameBad = 128, + None = 0, + Ok = 1, + Yes = 2, + No = 4, + Accept = 8, + Reject = 16, + Supporter = 32, + Ignore = 64, + NameBad = 128, } export interface Notification { - id: number; - message: string; - note?: string; - flags: NotificationFlags; - pony: Pony; - open: boolean; - fresh: boolean; + id: number; + message: string; + note?: string; + flags: NotificationFlags; + pony: Pony; + open: boolean; + fresh: boolean; } export interface Palette { - x: number; - y: number; - u: number; - v: number; - refs: number; - colors: Uint32Array; + x: number; + y: number; + u: number; + v: number; + refs: number; + colors: Uint32Array; } export interface JoinResponse { - token?: string; - alert?: string; + token?: string; + alert?: string; } export interface PaletteManager { - add(colors: number[]): Palette; - addArray(colors: Uint32Array): Palette; - init(gl: WebGLRenderingContext): void; + add(colors: number[]): Palette; + addArray(colors: Uint32Array): Palette; + init(gl: WebGLRenderingContext): void; } export type Batch = Float32Array; export interface SpriteBatchBase { - globalAlpha: number; - crop(x: number, y: number, w: number, h: number): void; - clearCrop(): void; - save(): void; - restore(): void; - translate(x: number, y: number): void; - scale(x: number, y: number): void; - rotate(angle: number): void; - multiplyTransform(mat: Matrix2D): void; - drawBatch(batch: Batch): void; - startBatch(): void; - finishBatch(): Batch | undefined; - releaseBatch(batch: Batch): void; + globalAlpha: number; + crop(x: number, y: number, w: number, h: number): void; + clearCrop(): void; + save(): void; + restore(): void; + translate(x: number, y: number): void; + scale(x: number, y: number): void; + rotate(angle: number): void; + multiplyTransform(mat: Matrix2D): void; + drawBatch(batch: Batch): void; + startBatch(): void; + finishBatch(): Batch | undefined; + releaseBatch(batch: Batch): void; } export interface SpriteBatchCommons extends SpriteBatchBase { - palette: boolean; - depth?: number; - drawRect(color: number, x: number, y: number, w: number, h: number): void; + palette: boolean; + depth?: number; + drawRect(color: number, x: number, y: number, w: number, h: number): void; } export interface SpriteBatch extends SpriteBatchCommons { - drawImage( - color: number, - sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number - ): void; - drawSprite(sprite: Sprite | undefined, color: number, x: number, y: number): void; + drawImage( + color: number, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + ): void; + drawSprite(sprite: Sprite | undefined, color: number, x: number, y: number): void; } export interface PaletteSpriteBatch extends SpriteBatchCommons { - drawImage( - type: number, color: number, palette: Palette | undefined, - sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number - ): void; - drawSprite(sprite: Sprite, color: number, palette: Palette | undefined, x: number, y: number): void; + drawImage( + type: number, color: number, palette: Palette | undefined, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + ): void; + drawSprite(sprite: Sprite, color: number, palette: Palette | undefined, x: number, y: number): void; } export function isPaletteSpriteBatch(batch: SpriteBatch | PaletteSpriteBatch): batch is PaletteSpriteBatch { - return batch.palette; + return batch.palette; } export interface ServerFeatureFlags { - test?: boolean; // test server - objects?: boolean; // editor - // more flags here ... + test?: boolean; // test server + objects?: boolean; // editor + // more flags here ... } export const enum ServerFlags { - None = 0, - TreeCrown = 1, - DoNotSave = 2, - Seasonal = 4, + None = 0, + TreeCrown = 1, + DoNotSave = 2, + Seasonal = 4, } export const enum EntityFlags { - None = 0, - Movable = 1, - Decal = 2, - Critter = 4, - Usable = 8, - Debug = 16, - StaticY = 32, // ignore ground Y - CanCollide = 64, // can this object collide with other objects - CanCollideWith = 128, // can other objects collide with this one - Interactive = 256, - Light = 512, - OnOff = 1024, - Bobbing = 2048, - IgnoreTool = 4096, + None = 0, + Movable = 1, + Decal = 2, + Critter = 4, + Usable = 8, + Debug = 16, + StaticY = 32, // ignore ground Y + CanCollide = 64, // can this object collide with other objects + CanCollideWith = 128, // can other objects collide with this one + Interactive = 256, + Light = 512, + OnOff = 1024, + Bobbing = 2048, + IgnoreTool = 4096, } export const enum EntityState { - None = 0, + None = 0, - // general - Flying = 1, - FacingRight = 2, + // general + Flying = 1, + FacingRight = 2, - // lights - On = 4, + // lights + On = 4, - // other - Editable = 8, + // other + Editable = 8, - // pony - HeadTurned = 4, - Magic = 8, - // CanFly ?, // Or in flags ? + // pony + HeadTurned = 4, + Magic = 8, + // CanFly ?, // Or in flags ? - // pony state - PonyStanding = 0 << 4, - PonyWalking = 1 << 4, - PonyTrotting = 2 << 4, - PonySitting = 3 << 4, - PonyLying = 4 << 4, - PonyFlying = 5 << 4, - PonyStateMask = 0xf << 4, + // pony state + PonyStanding = 0 << 4, + PonyWalking = 1 << 4, + PonyTrotting = 2 << 4, + PonySitting = 3 << 4, + PonyLying = 4 << 4, + PonyFlying = 5 << 4, + PonyStateMask = 0xf << 4, - // animated entities - AnimationMask = 0xf << 4, + // animated entities + AnimationMask = 0xf << 4, - // max 0xff + // max 0xff } export const enum EntityPlayerState { - None = 0, - Ignored = 1, - Hidden = 2, - Friend = 4, + None = 0, + Ignored = 1, + Hidden = 2, + Friend = 4, - // max 0xff + // max 0xff } export function getAnimationFromEntityState(state: EntityState) { - return (state & EntityState.AnimationMask) >> 4; + return (state & EntityState.AnimationMask) >> 4; } export function setAnimationToEntityState(state: EntityState, animation: number) { - return (state & ~EntityState.AnimationMask) | (animation << 4); + return (state & ~EntityState.AnimationMask) | (animation << 4); } export const enum MessageType { - Chat = 0, - System = 1, - Admin = 2, - Mod = 3, - Party = 4, - Thinking = 5, - PartyThinking = 6, - Announcement = 7, - PartyAnnouncement = 8, - Supporter1 = 9, - Supporter2 = 10, - Supporter3 = 11, - Dismiss = 12, - Whisper = 13, - WhisperTo = 14, - WhisperAnnouncement = 15, - WhisperToAnnouncement = 16, + Chat = 0, + System = 1, + Admin = 2, + Mod = 3, + Party = 4, + Thinking = 5, + PartyThinking = 6, + Announcement = 7, + PartyAnnouncement = 8, + Supporter1 = 9, + Supporter2 = 10, + Supporter3 = 11, + Dismiss = 12, + Whisper = 13, + WhisperTo = 14, + WhisperAnnouncement = 15, + WhisperToAnnouncement = 16, } export function toMessageType(type: MessageType) { - if (type === MessageType.WhisperAnnouncement) { - return MessageType.WhisperToAnnouncement; - } else { - return MessageType.WhisperTo; - } + if (type === MessageType.WhisperAnnouncement) { + return MessageType.WhisperToAnnouncement; + } else { + return MessageType.WhisperTo; + } } export function toAnnouncementMessageType(type: ChatType) { - switch (type) { - case ChatType.Party: return MessageType.PartyAnnouncement; - case ChatType.Whisper: return MessageType.WhisperAnnouncement; - default: return MessageType.Announcement; - } + switch (type) { + case ChatType.Party: return MessageType.PartyAnnouncement; + case ChatType.Whisper: return MessageType.WhisperAnnouncement; + default: return MessageType.Announcement; + } } export function isWhisper(type: MessageType) { - return type === MessageType.Whisper || - type === MessageType.WhisperAnnouncement; + return type === MessageType.Whisper || + type === MessageType.WhisperAnnouncement; } export function isWhisperTo(type: MessageType) { - return type === MessageType.WhisperTo || - type === MessageType.WhisperToAnnouncement; + return type === MessageType.WhisperTo || + type === MessageType.WhisperToAnnouncement; } export function isThinking(type: MessageType) { - return type === MessageType.Thinking || - type === MessageType.PartyThinking; + return type === MessageType.Thinking || + type === MessageType.PartyThinking; } export function isModOrAdminMessage(type: MessageType) { - return type === MessageType.Mod || - type === MessageType.Admin; + return type === MessageType.Mod || + type === MessageType.Admin; } export function isPartyMessage(type: MessageType) { - return type === MessageType.Party || - type === MessageType.PartyThinking || - type === MessageType.PartyAnnouncement; + return type === MessageType.Party || + type === MessageType.PartyThinking || + type === MessageType.PartyAnnouncement; } export function isPublicMessage(type: MessageType) { - return type === MessageType.Chat || - type === MessageType.Thinking || - type === MessageType.Announcement || - type === MessageType.Admin || - type === MessageType.Mod || - type === MessageType.Supporter1 || - type === MessageType.Supporter2 || - type === MessageType.Supporter3; + return type === MessageType.Chat || + type === MessageType.Thinking || + type === MessageType.Announcement || + type === MessageType.Admin || + type === MessageType.Mod || + type === MessageType.Supporter1 || + type === MessageType.Supporter2 || + type === MessageType.Supporter3; } export function isNonIgnorableMessage(type: MessageType) { - return isModOrAdminMessage(type) || - isPartyMessage(type) || - type === MessageType.System || - type === MessageType.Dismiss || - type === MessageType.Announcement; + return isModOrAdminMessage(type) || + isPartyMessage(type) || + type === MessageType.System || + type === MessageType.Dismiss || + type === MessageType.Announcement; } export const enum ChatType { - Say = 0, - Party = 1, - Think = 2, - PartyThink = 3, - Supporter = 4, - Supporter1 = 5, - Supporter2 = 6, - Supporter3 = 7, - Dismiss = 8, - Whisper = 9, + Say = 0, + Party = 1, + Think = 2, + PartyThink = 3, + Supporter = 4, + Supporter1 = 5, + Supporter2 = 6, + Supporter3 = 7, + Dismiss = 8, + Whisper = 9, } export function isPartyChat(type: ChatType | undefined) { - return type === ChatType.Party || type === ChatType.PartyThink; + return type === ChatType.Party || type === ChatType.PartyThink; } export function isPublicChat(type: ChatType) { - return type !== ChatType.Party && - type !== ChatType.PartyThink && - type !== ChatType.Dismiss && - type !== ChatType.Whisper; + return type !== ChatType.Party && + type !== ChatType.PartyThink && + type !== ChatType.Dismiss && + type !== ChatType.Whisper; } export interface Point { - x: number; - y: number; + x: number; + y: number; } export interface Size { - width: number; - height: number; + width: number; + height: number; } export interface Rect { - x: number; - y: number; - w: number; - h: number; + x: number; + y: number; + w: number; + h: number; } export interface Camera extends Rect { - offset: number; - shift: number; - shiftTarget: number; - shiftRatio: number; - actualY: number; + offset: number; + shift: number; + shiftTarget: number; + shiftRatio: number; + actualY: number; } export interface Sprite { - x: number; - y: number; - w: number; - h: number; - ox: number; - oy: number; - type: number; + x: number; + y: number; + w: number; + h: number; + ox: number; + oy: number; + type: number; } export interface SpriteSheet { - src?: string; - srcA?: string; - data?: ImageData; - texture: Texture2D | undefined; - sprites: (Sprite | undefined)[]; - palette: boolean; + src?: string; + srcA?: string; + data?: ImageData; + texture: Texture2D | undefined; + sprites: (Sprite | undefined)[]; + palette: boolean; } export interface SpriteBorder { - border: number; - topLeft: Sprite; - top: Sprite; - topRight: Sprite; - left: Sprite; - bg: Sprite; - right: Sprite; - bottomLeft: Sprite; - bottom: Sprite; - bottomRight: Sprite; - palette?: number[]; + border: number; + topLeft: Sprite; + top: Sprite; + topRight: Sprite; + left: Sprite; + bg: Sprite; + right: Sprite; + bottomLeft: Sprite; + bottom: Sprite; + bottomRight: Sprite; + palette?: number[]; } export interface SpritePalette { - sprite: Sprite; - palette: number[]; + sprite: Sprite; + palette: number[]; } export interface SpriteMap { - [key: string]: Sprite; + [key: string]: Sprite; } export interface PonyEye { - base?: Sprite; - irises: (Sprite | undefined)[]; - shadow?: Sprite; - shine?: Sprite; + base?: Sprite; + irises: (Sprite | undefined)[]; + shadow?: Sprite; + shine?: Sprite; } export interface PonyNose extends ColorExtra { - mouth?: Sprite; - fangs?: Sprite; + mouth?: Sprite; + fangs?: Sprite; } export interface FillOutline { - fill: Sprite; - outline?: Sprite; - extra?: Sprite; + fill: Sprite; + outline?: Sprite; + extra?: Sprite; } export interface Says { - created: number; - message: string; - type?: MessageType; - timer?: number; - total?: number; + created: number; + message: string; + type?: MessageType; + timer?: number; + total?: number; } export interface PaletteRenderable { - color?: Sprite; - shadow?: Sprite; - palettes?: Uint32Array[]; + color?: Sprite; + shadow?: Sprite; + palettes?: Uint32Array[]; } export interface Collider { - x: number; - y: number; - w: number; - h: number; - tall: boolean; - exact: boolean; + x: number; + y: number; + w: number; + h: number; + tall: boolean; + exact: boolean; } export const enum InteractAction { - None, - Toolbox, - GiveLantern, - GiveFruits, - GiveCookie1, - GiveCookie2, + None, + Toolbox, + GiveLantern, + GiveFruits, + GiveCookie1, + GiveCookie2, } export interface EntityPart { - // info - name?: string; - crc?: number; - tag?: string; - fake?: boolean; + // info + name?: string; + crc?: number; + tag?: string; + fake?: boolean; - // chat - says?: Says; - chatX?: number; - chatY?: number; - chatBounds?: Rect; + // chat + says?: Says; + chatX?: number; + chatY?: number; + chatBounds?: Rect; - // interaction - interactBounds?: Rect; - interactRange?: number; + // interaction + interactBounds?: Rect; + interactRange?: number; - // collisions - colliders?: Collider[]; - collidersBounds?: Rect; + // collisions + colliders?: Collider[]; + collidersBounds?: Rect; - // cover - coverBounds?: Rect; - coverLifted?: boolean; - coverLifting?: number; + // cover + coverBounds?: Rect; + coverLifted?: boolean; + coverLifting?: number; - // pickable - pickableX?: number; - pickableY?: number; + // pickable + pickableX?: number; + pickableY?: number; - // update - update?(delta: number, gameTime: number): void | boolean; + // update + update?(delta: number, gameTime: number): void | boolean; - // draw - ox?: number; - oy?: number; - oz?: number; - bounds?: Rect; - draw?(batch: PaletteSpriteBatch, options: DrawOptions): void; + // draw + ox?: number; + oy?: number; + oz?: number; + bounds?: Rect; + draw?(batch: PaletteSpriteBatch, options: DrawOptions): void; - // light - lightOn?: boolean; - lightScale?: number; - lightTarget?: number; - lightColor?: number; - lightScaleAdjust?: number; - lightBounds?: Rect; - drawLight?(batch: SpriteBatch, options: DrawOptions): void; + // light + lightOn?: boolean; + lightScale?: number; + lightTarget?: number; + lightColor?: number; + lightScaleAdjust?: number; + lightBounds?: Rect; + drawLight?(batch: SpriteBatch, options: DrawOptions): void; - // light sprite - lightSpriteOn?: boolean; - lightSpriteX?: number; - lightSpriteY?: number; - lightSpriteColor?: number; - lightSpriteBounds?: Rect; - drawLightSprite?(batch: SpriteBatch, options: DrawOptions): void; + // light sprite + lightSpriteOn?: boolean; + lightSpriteX?: number; + lightSpriteY?: number; + lightSpriteColor?: number; + lightSpriteBounds?: Rect; + drawLightSprite?(batch: SpriteBatch, options: DrawOptions): void; - // trigger - triggerBounds?: Rect; - triggerTall?: boolean; - triggerOn?: boolean; + // trigger + triggerBounds?: Rect; + triggerTall?: boolean; + triggerOn?: boolean; - // bobbing - bobsFps?: number; - bobs?: number[]; + // bobbing + bobsFps?: number; + bobs?: number[]; - // resources to release - palettes?: (Palette | undefined)[]; + // resources to release + palettes?: (Palette | undefined)[]; - // server - // expr?: number; - serverFlags?: ServerFlags; + // server + // expr?: number; + serverFlags?: ServerFlags; - // other - text?: string; - inTheAirDelay?: number; - options?: EntityOrPonyOptions; - extraOptions?: any; - selected?: boolean; + // other + text?: string; + inTheAirDelay?: number; + options?: EntityOrPonyOptions; + extraOptions?: any; + selected?: boolean; - // for setting entity fields - order?: number; - flags?: EntityFlags; - state?: EntityState; - vx?: number; - vy?: number; + // for setting entity fields + order?: number; + flags?: EntityFlags; + state?: EntityState; + vx?: number; + vy?: number; - // minimap - minimap?: { color: number; rect: Rect; order: number; }; + // minimap + minimap?: { color: number; rect: Rect; order: number; }; - // interact - interactAction?: InteractAction; + // interact + interactAction?: InteractAction; } export interface Entity extends EntityPart { - id: number; - type: number; - flags: EntityFlags; - state: EntityState; - playerState: EntityPlayerState; - order: number; - x: number; - y: number; - z: number; - vx: number; - vy: number; - // frame: number; // last update frame - timestamp: number; + id: number; + type: number; + flags: EntityFlags; + state: EntityState; + playerState: EntityPlayerState; + order: number; + x: number; + y: number; + z: number; + vx: number; + vy: number; + // frame: number; // last update frame + timestamp: number; - // editor / debug - movedOnFrame?: number; - draggingStart?: Point; + // editor / debug + movedOnFrame?: number; + draggingStart?: Point; } export interface FakeEntity { - fake: true; - id: number; - type: number; - name?: string; - crc?: number; + fake: true; + id: number; + type: number; + name?: string; + crc?: number; } export const enum DoAction { - None, - Boop, - Swing, - HoldPoof, + None, + Boop, + Swing, + HoldPoof, } export interface Pony extends Entity { - info: string | Uint8Array | undefined; - expr: number; - currentExpression: number; - extra: boolean; - toy: number; - swimming: boolean; - inTheAirDelay: number; - hold: number; - palettePonyInfo: PalettePonyInfo | undefined; - headAnimation: HeadAnimation | undefined; - batch: Batch | undefined; - discardBatch: boolean; - headTime: number; - blinkTime: number; - nextBlink: number; - ponyState: PonyState; - drawingOptions: DrawPonyOptions; - animator: Animator; - initialized: boolean; - doAction: DoAction; - paletteManager: PaletteManager; - magicColor: number; + info: string | Uint8Array | undefined; + expr: number; + currentExpression: number; + extra: boolean; + toy: number; + swimming: boolean; + inTheAirDelay: number; + hold: number; + palettePonyInfo: PalettePonyInfo | undefined; + headAnimation: HeadAnimation | undefined; + batch: Batch | undefined; + discardBatch: boolean; + headTime: number; + blinkTime: number; + nextBlink: number; + ponyState: PonyState; + drawingOptions: DrawPonyOptions; + animator: Animator; + initialized: boolean; + doAction: DoAction; + paletteManager: PaletteManager; + magicColor: number; - // player info - name: string | undefined; - tag: string | undefined; - site: SocialSite | undefined; - modInfo: ModInfo | undefined; - ex: boolean; // extended data indicator, sent in extended options + // player info + name: string | undefined; + tag: string | undefined; + site: SocialSite | undefined; + modInfo: ModInfo | undefined; + ex: boolean; // extended data indicator, sent in extended options - // effect animations - zzzEffect: AnimationPlayer; - cryEffect: AnimationPlayer; - sneezeEffect: AnimationPlayer; - holdPoofEffect: AnimationPlayer; - heartsEffect: AnimationPlayer; - magicEffect: AnimationPlayer; + // effect animations + zzzEffect: AnimationPlayer; + cryEffect: AnimationPlayer; + sneezeEffect: AnimationPlayer; + holdPoofEffect: AnimationPlayer; + heartsEffect: AnimationPlayer; + magicEffect: AnimationPlayer; - // last state - lastX: number; - lastY: number; - lastRight: boolean; - lastState: PonyState; + // last state + lastX: number; + lastY: number; + lastRight: boolean; + lastState: PonyState; - lastBoopSplash: number; + lastBoopSplash: number; } export interface Region { - x: number; // region number in X axis - y: number; // region number in Y axis - tiles: Uint8Array; - tileIndices: Int16Array; - tilesDirty: boolean; - lastTileUpdate?: number; - randoms: Uint8Array; - entities: Entity[]; - colliders: Entity[]; - collider: Uint8Array; - colliderDirty: boolean; - // elevation: Uint8Array; + x: number; // region number in X axis + y: number; // region number in Y axis + tiles: Uint8Array; + tileIndices: Int16Array; + tilesDirty: boolean; + lastTileUpdate?: number; + randoms: Uint8Array; + entities: Entity[]; + colliders: Entity[]; + collider: Uint8Array; + colliderDirty: boolean; + // elevation: Uint8Array; } export interface CharacterTag { - id: string; - name: string; - label: string; - className: string; - tagClass: string; - color: number; + id: string; + name: string; + label: string; + className: string; + tagClass: string; + color: number; } export interface SocialSite { - id: string; - name: string; - provider: string; - url: string; + id: string; + name: string; + provider: string; + url: string; } export interface SocialSiteInfo { - id: string; - name: string; - url: string; - icon?: string; - color?: string; + id: string; + name: string; + url: string; + icon?: string; + color?: string; } // NOTE: also modify fixAccountSettings export interface AccountSettings { - defaultServer?: string; - filterSwearWords?: boolean; - filterCyrillic?: boolean; - filterWords?: string; - ignorePartyInvites?: boolean; - ignoreFriendInvites?: boolean; - ignorePublicChat?: boolean; - ignoreNonFriendWhispers?: boolean; - chatlogOpacity?: number; - seeThroughObjects?: boolean; - chatlogRange?: number; - actions?: string; - hidden?: boolean; + defaultServer?: string; + filterSwearWords?: boolean; + filterCyrillic?: boolean; + filterWords?: string; + ignorePartyInvites?: boolean; + ignoreFriendInvites?: boolean; + ignorePublicChat?: boolean; + ignoreNonFriendWhispers?: boolean; + chatlogOpacity?: number; + seeThroughObjects?: boolean; + chatlogRange?: number; + actions?: string; + hidden?: boolean; } export interface BrowserSettings { - lowGraphicsMode?: boolean; - chatlogClosed?: boolean; - chatlogTab?: string; - chatlogWidth?: number; - chatlogHeight?: number; - volume?: number; - disableGamepad?: boolean; - disableFKeys?: boolean; - showStats?: boolean; - showFps?: boolean; - powerSaving?: boolean; - scale?: number; - walkByDefault?: boolean; - brightNight?: boolean; + lowGraphicsMode?: boolean; + chatlogClosed?: boolean; + chatlogTab?: string; + chatlogWidth?: number; + chatlogHeight?: number; + volume?: number; + disableGamepad?: boolean; + disableFKeys?: boolean; + showStats?: boolean; + showFps?: boolean; + powerSaving?: boolean; + scale?: number; + walkByDefault?: boolean; + brightNight?: boolean; } export interface EntityTypeName { - type: number; - name: string; + type: number; + name: string; } export interface EntityNameTypes { - name: string; - types: number[]; + name: string; + types: number[]; } export interface EntitiesEditorInfo { - names: string[]; - typeToName: EntityTypeName[]; - nameToTypes: EntityNameTypes[]; + names: string[]; + typeToName: EntityTypeName[]; + nameToTypes: EntityNameTypes[]; } export interface HiddenPlayer { - id: string; - name: string; - date: string; + id: string; + name: string; + date: string; } export interface AccountDataExtra { - sites?: SocialSite[]; - ponies?: PonyObject[]; - supporterInvited?: boolean; - editor?: EntitiesEditorInfo; - alert?: string; + sites?: SocialSite[]; + ponies?: PonyObject[]; + supporterInvited?: boolean; + editor?: EntitiesEditorInfo; + alert?: string; } export const enum AccountDataFlags { - None = 0, - Duplicates = 1, - PastSupporter = 4, + None = 0, + Duplicates = 1, + PastSupporter = 4, } export interface FriendData { - accountId: string; - accountName: string; - name: string | undefined; - pony: string | undefined; - nameBad: boolean; + accountId: string; + accountName: string; + name: string | undefined; + pony: string | undefined; + nameBad: boolean; } export const enum FriendStatusFlags { - None = 0, - Online = 1, - Remove = 2, + None = 0, + Online = 1, + Remove = 2, } export interface FriendStatusData { - accountId: string; - accountName?: string; - status: FriendStatusFlags; - entityId?: number; - name?: string; - info?: string; - crc?: number; - nameBad?: boolean; + accountId: string; + accountName?: string; + status: FriendStatusFlags; + entityId?: number; + name?: string; + info?: string; + crc?: number; + nameBad?: boolean; } export interface AccountData extends AccountDataExtra { - id: string; - name: string; - birthdate: string; - birthyear?: number; - settings: AccountSettings; - characterCount: number; - supporter?: number; - roles?: string[] | undefined; - check?: any; - flags: AccountDataFlags; + id: string; + name: string; + birthdate: string; + birthyear?: number; + settings: AccountSettings; + characterCount: number; + supporter?: number; + roles?: string[] | undefined; + check?: any; + flags: AccountDataFlags; } export interface UpdateAccountData { - name: string; - birthdate: string; + name: string; + birthdate: string; } export interface AccountCounters { - spam?: number; - swears?: number; - timeouts?: number; - inviteLimit?: number; - friendLimit?: number; + spam?: number; + swears?: number; + timeouts?: number; + inviteLimit?: number; + friendLimit?: number; } export interface ModInfo { - age?: number; - note?: string; - mute?: string; - shadow?: string; - account?: string; - country?: string; - counters?: AccountCounters; + age?: number; + note?: string; + mute?: string; + shadow?: string; + account?: string; + country?: string; + counters?: AccountCounters; } export interface PonyOptions { - expr?: number; // TODO: move to dedicated field - tag?: string; // TODO: move to flags ? - extra?: boolean; // TODO: move to flags ? - hold?: number; - toy?: number; - // mod/extra info - site?: SocialSite; - modInfo?: ModInfo; + expr?: number; // TODO: move to dedicated field + tag?: string; // TODO: move to flags ? + extra?: boolean; // TODO: move to flags ? + hold?: number; + toy?: number; + // mod/extra info + site?: SocialSite; + modInfo?: ModInfo; } // id, options, name, info, playerState, badName @@ -798,499 +798,499 @@ export type PonyData = [number, PonyOptions | undefined, Uint8Array | undefined, // NOTE: also update in serverActions.ts export const enum PlayerAction { - None, - Ignore, - Unignore, - InviteToParty, - RemoveFromParty, - PromotePartyLeader, - HidePlayer, - InviteToSupporterServers, - AddFriend, - RemoveFriend, + None, + Ignore, + Unignore, + InviteToParty, + RemoveFromParty, + PromotePartyLeader, + HidePlayer, + InviteToSupporterServers, + AddFriend, + RemoveFriend, } export const enum ModAction { - None, - Report, - Mute, - Shadow, - Kick, - Ban, + None, + Report, + Mute, + Shadow, + Kick, + Ban, } export const enum LeaveReason { - None, - Swearing, + None, + Swearing, } export interface TileUpdate { - x: number; - y: number; - type: TileType; + x: number; + y: number; + type: TileType; } // [id, message, type] export type SayData = [number, string, MessageType]; export const enum UpdateType { - None = 0, - AddEntity = 1, - UpdateEntity = 2, - RemoveEntity = 3, - UpdateTile = 4, + None = 0, + AddEntity = 1, + UpdateEntity = 2, + RemoveEntity = 3, + UpdateTile = 4, } export interface DecodedUpdate { - id: number; - type: number | undefined; - state: EntityState | undefined; - x: number | undefined; - y: number | undefined; - vx: number; - vy: number; - expression: number | undefined; - options: EntityOrPonyOptions | undefined; - crc: number | undefined; - name: string | undefined; - filterName: boolean; - info: Uint8Array | undefined; - action: Action | undefined; - switchRegion: boolean; - playerState: EntityPlayerState | undefined; + id: number; + type: number | undefined; + state: EntityState | undefined; + x: number | undefined; + y: number | undefined; + vx: number; + vy: number; + expression: number | undefined; + options: EntityOrPonyOptions | undefined; + crc: number | undefined; + name: string | undefined; + filterName: boolean; + info: Uint8Array | undefined; + action: Action | undefined; + switchRegion: boolean; + playerState: EntityPlayerState | undefined; } export interface DecodedRegionUpdate { - x: number; - y: number; - updates: DecodedUpdate[]; - removes: number[]; - tiles: TileUpdate[]; - tileData: Uint8Array | null; + x: number; + y: number; + updates: DecodedUpdate[]; + removes: number[]; + tiles: TileUpdate[]; + tileData: Uint8Array | null; } export const enum WorldStateFlags { - None = 0, - Safe = 1, + None = 0, + Safe = 1, } export interface WorldState { - time: number; - season: Season; - holiday: Holiday; - flags: WorldStateFlags; - featureFlags: ServerFeatureFlags; + time: number; + season: Season; + holiday: Holiday; + flags: WorldStateFlags; + featureFlags: ServerFeatureFlags; } export interface MapState { - weather: Weather; + weather: Weather; } export const defaultMapState: MapState = { - weather: Weather.None, + weather: Weather.None, }; export const enum WallType { - None = 0, - Wood = 1, + None = 0, + Wood = 1, } export const enum TileType { - None = 0, - Dirt = 1, - Grass = 2, - Water = 3, - Wood = 4, - Ice = 5, - SnowOnIce = 6, - WalkableWater = 7, - Boat = 8, - WalkableIce = 9, - Stone = 10, - Stone2 = 11, - ElevatedDirt = 12, + None = 0, + Dirt = 1, + Grass = 2, + Water = 3, + Wood = 4, + Ice = 5, + SnowOnIce = 6, + WalkableWater = 7, + Boat = 8, + WalkableIce = 9, + Stone = 10, + Stone2 = 11, + ElevatedDirt = 12, - // special - WallH = 100, - WallV = 101, + // special + WallH = 100, + WallV = 101, } export const tileTypeNames = [ - 'none', 'dirt', 'grass', 'water', 'wood', 'ice', 'snow-on-ice', 'walkable-water', 'boat', 'walkable-ice', - 'stone', 'stone-2', 'elevated-dirt', + 'none', 'dirt', 'grass', 'water', 'wood', 'ice', 'snow-on-ice', 'walkable-water', 'boat', 'walkable-ice', + 'stone', 'stone-2', 'elevated-dirt', ]; export const houseTiles = [ - { type: TileType.Dirt, name: 'Dirt' }, - { type: TileType.Wood, name: 'Wood' }, - { type: TileType.Grass, name: 'Grass' }, - { type: TileType.Water, name: 'Water' }, - { type: TileType.Ice, name: 'Ice' }, - { type: TileType.Stone, name: 'Stone' }, - { type: TileType.Stone2, name: 'Brick' }, + { type: TileType.Dirt, name: 'Dirt' }, + { type: TileType.Wood, name: 'Wood' }, + { type: TileType.Grass, name: 'Grass' }, + { type: TileType.Water, name: 'Water' }, + { type: TileType.Ice, name: 'Ice' }, + { type: TileType.Stone, name: 'Stone' }, + { type: TileType.Stone2, name: 'Brick' }, ]; export function canWalk(tile: TileType): boolean { - return tile !== TileType.None; + return tile !== TileType.None; } export function isValidTile(tile: TileType): boolean { - return tile === TileType.Dirt || tile === TileType.Grass; + return tile === TileType.Dirt || tile === TileType.Grass; } export function isValidModTile(tile: TileType): boolean { - return tile >= TileType.None && tile < TileType.WallH; + return tile >= TileType.None && tile < TileType.WallH; } export interface MapInfo { - type: MapType; - flags: MapFlags; - regionsX: number; - regionsY: number; - defaultTile: TileType; - editableArea?: Rect; + type: MapType; + flags: MapFlags; + regionsX: number; + regionsY: number; + defaultTile: TileType; + editableArea?: Rect; } export interface WorldMap extends IMap { - type: MapType; - flags: MapFlags; - tileTime: number; - entities: Entity[]; - entitiesDrawable: Entity[]; - entitiesWithNames: Entity[]; - entitiesWithChat: Entity[]; - entitiesMoving: Entity[]; - entitiesTriggers: Entity[]; - entitiesLight: Entity[]; - entitiesLightSprite: Entity[]; - entitiesById: Map; - poniesToDecode: Pony[]; - width: number; - height: number; - regionsX: number; - regionsY: number; - defaultTile: TileType; - regions: (Region | undefined)[]; - minRegionX: number; - minRegionY: number; - maxRegionX: number; - maxRegionY: number; - state: MapState; - editableArea?: Rect; + type: MapType; + flags: MapFlags; + tileTime: number; + entities: Entity[]; + entitiesDrawable: Entity[]; + entitiesWithNames: Entity[]; + entitiesWithChat: Entity[]; + entitiesMoving: Entity[]; + entitiesTriggers: Entity[]; + entitiesLight: Entity[]; + entitiesLightSprite: Entity[]; + entitiesById: Map; + poniesToDecode: Pony[]; + width: number; + height: number; + regionsX: number; + regionsY: number; + defaultTile: TileType; + regions: (Region | undefined)[]; + minRegionX: number; + minRegionY: number; + maxRegionX: number; + maxRegionY: number; + state: MapState; + editableArea?: Rect; } export interface TileSet { - sprites: Sprite[]; - palette: Palette; + sprites: Sprite[]; + palette: Palette; } export type TileSets = TileSet[]; export const enum PartyFlags { - None = 0, - Leader = 1, - Pending = 2, - Offline = 4, + None = 0, + Leader = 1, + Pending = 2, + Offline = 4, } export interface PartyMember { - id: number; - pony: Pony | undefined; - self: boolean; - leader: boolean; - pending: boolean; - offline: boolean; + id: number; + pony: Pony | undefined; + self: boolean; + leader: boolean; + pending: boolean; + offline: boolean; } export interface PartyInfo { - leaderId: number; - members: PartyMember[]; + leaderId: number; + members: PartyMember[]; } export interface ChatMessage { - id: number; - crc: number | undefined; - name: string; - message: string; - type: MessageType; + id: number; + crc: number | undefined; + name: string; + message: string; + type: MessageType; } export interface CommonButtonAction { - title?: string; + title?: string; } export interface ExpressionButtonAction { - type: 'expression'; - expression: Expression | undefined; - title?: string; + type: 'expression'; + expression: Expression | undefined; + title?: string; } export interface CommandButtonAction { - type: 'command'; - title: string; - icon: string; - command: string; + type: 'command'; + title: string; + icon: string; + command: string; } export interface ActionButtonAction { - type: 'action'; - title: string; - action: string; - sendAction: Action; + type: 'action'; + title: string; + action: string; + sendAction: Action; } export interface ItemButtonAction { - type: 'item'; - icon: ColorExtra; - count?: number; + type: 'item'; + icon: ColorExtra; + count?: number; } export interface EntityButtonAction { - type: 'entity'; - entity: string; - title?: string; + type: 'entity'; + entity: string; + title?: string; } export type ButtonAction = CommonButtonAction & - (ExpressionButtonAction | CommandButtonAction | ActionButtonAction | ItemButtonAction | EntityButtonAction); + (ExpressionButtonAction | CommandButtonAction | ActionButtonAction | ItemButtonAction | EntityButtonAction); export interface ButtonActionSlot { - action: ButtonAction | undefined; + action: ButtonAction | undefined; } export const enum Action { - None = 0, - Boop, - TurnHead, - Yawn, - Laugh, - Sneeze, - Sit, - Lie, - Fly, - UnhideAllHiddenPlayers, - Stand, - SwapCharacter, - HoldPoof, - Sleep, - Drop, - DropToy, - Blush, - Cry, - Love, - CancelSupporterInvite, - Info, - KeepAlive, - RemoveFriend, - FriendsCRC, - RequestEntityInfo, - ACL, - Magic, - RemoveEntity, - PlaceEntity, - SwitchTool, - SwitchToolRev, - SwitchToPlaceTool, - SwitchToTileTool, + None = 0, + Boop, + TurnHead, + Yawn, + Laugh, + Sneeze, + Sit, + Lie, + Fly, + UnhideAllHiddenPlayers, + Stand, + SwapCharacter, + HoldPoof, + Sleep, + Drop, + DropToy, + Blush, + Cry, + Love, + CancelSupporterInvite, + Info, + KeepAlive, + RemoveFriend, + FriendsCRC, + RequestEntityInfo, + ACL, + Magic, + RemoveEntity, + PlaceEntity, + SwitchTool, + SwitchToolRev, + SwitchToPlaceTool, + SwitchToTileTool, } export const enum InfoFlags { - None = 0, - Incognito = 1, - SupportsWASM = 2, - SupportsLetAndConst = 4, + None = 0, + Incognito = 1, + SupportsWASM = 2, + SupportsLetAndConst = 4, } export function isExpressionAction(action: Action) { - return action === Action.Yawn || action === Action.Laugh || action === Action.Sneeze; + return action === Action.Yawn || action === Action.Laugh || action === Action.Sneeze; } export interface EditorPlaceAction { - type: 'place'; - entity: string; - x: number; - y: number; + type: 'place'; + entity: string; + x: number; + y: number; } export interface EditorMoveAction { - type: 'move'; - entities: { id: number; x: number; y: number; }[]; + type: 'move'; + entities: { id: number; x: number; y: number; }[]; } export interface EditorRemoveAction { - type: 'remove'; - entities: number[]; + type: 'remove'; + entities: number[]; } export interface EditorOtherAction { - type: 'undo' | 'clear' | 'list'; + type: 'undo' | 'clear' | 'list'; } export interface EditorTileAction { - type: 'tile'; - x: number; - y: number; - tile: TileType; - size: number; + type: 'tile'; + x: number; + y: number; + tile: TileType; + size: number; } export interface EditorPartyAction { - type: 'party'; + type: 'party'; } export type EditorAction = - EditorPlaceAction | - EditorMoveAction | - EditorRemoveAction | - EditorOtherAction | - EditorTileAction | - EditorPartyAction; + EditorPlaceAction | + EditorMoveAction | + EditorRemoveAction | + EditorOtherAction | + EditorTileAction | + EditorPartyAction; export const enum SelectFlags { - None = 0, - FetchEx = 1, - FetchInfo = 2, + None = 0, + FetchEx = 1, + FetchInfo = 2, } export interface IServerActions { - say(entityId: number, text: string, type: ChatType): void; - select(entityId: number, flags: SelectFlags): void; - interact(entityId: number): void; - use(): void; - action(action: Action): void; - actionParam(action: Action, param: any): void; - actionParam2(action: Action, param: any): void; - getInvites(): Promise; - expression(expression: number): void; - playerAction(entityId: number, action: PlayerAction, param: any): void; - leaveParty(): void; - otherAction(entityId: number, action: ModAction, param: number): Promise; - setNote(entityId: number, text: string): Promise; - saveSettings(settings: AccountSettings): void; - acceptNotification(id: number): void; - rejectNotification(id: number): void; - getPonies(ids: number[]): void; - loaded(): void; - fixedPosition(): void; - updateCamera(x: number, y: number, w: number, h: number): void; - move(a: number, b: number, c: number, d: number, e: number): void; - changeTile(x: number, y: number, type: TileType): void; - leave(): void; - editorAction(action: EditorAction): void; + say(entityId: number, text: string, type: ChatType): void; + select(entityId: number, flags: SelectFlags): void; + interact(entityId: number): void; + use(): void; + action(action: Action): void; + actionParam(action: Action, param: any): void; + actionParam2(action: Action, param: any): void; + getInvites(): Promise; + expression(expression: number): void; + playerAction(entityId: number, action: PlayerAction, param: any): void; + leaveParty(): void; + otherAction(entityId: number, action: ModAction, param: number): Promise; + setNote(entityId: number, text: string): Promise; + saveSettings(settings: AccountSettings): void; + acceptNotification(id: number): void; + rejectNotification(id: number): void; + getPonies(ids: number[]): void; + loaded(): void; + fixedPosition(): void; + updateCamera(x: number, y: number, w: number, h: number): void; + move(a: number, b: number, c: number, d: number, e: number): void; + changeTile(x: number, y: number, type: TileType): void; + leave(): void; + editorAction(action: EditorAction): void; } // Pony export interface PonyObject { - id: string; - name: string; - info: string; - tag?: string; - site?: string; - desc?: string; - lastUsed?: string; - creator?: string; - ponyInfo?: PonyInfo; - hideSupport?: boolean; - respawnAtSpawn?: boolean; + id: string; + name: string; + info: string; + tag?: string; + site?: string; + desc?: string; + lastUsed?: string; + creator?: string; + ponyInfo?: PonyInfo; + hideSupport?: boolean; + respawnAtSpawn?: boolean; } export interface SpriteSetBase { - type?: number; - pattern?: number; + type?: number; + pattern?: number; } export interface SpriteSet extends SpriteSetBase { - fills?: (T | undefined)[]; - outlines?: (T | undefined)[]; - lockFills?: boolean[]; - lockOutlines?: boolean[]; + fills?: (T | undefined)[]; + outlines?: (T | undefined)[]; + lockFills?: boolean[]; + lockOutlines?: boolean[]; } export interface PonyInfoBase { - head: SET | undefined; - nose: SET | undefined; - ears: SET | undefined; - horn: SET | undefined; - wings: SET | undefined; - frontHooves: SET | undefined; - backHooves: SET | undefined; + head: SET | undefined; + nose: SET | undefined; + ears: SET | undefined; + horn: SET | undefined; + wings: SET | undefined; + frontHooves: SET | undefined; + backHooves: SET | undefined; - mane: SET | undefined; - backMane: SET | undefined; - tail: SET | undefined; - facialHair: SET | undefined; + mane: SET | undefined; + backMane: SET | undefined; + tail: SET | undefined; + facialHair: SET | undefined; - headAccessory: SET | undefined; - earAccessory: SET | undefined; - faceAccessory: SET | undefined; - neckAccessory: SET | undefined; - frontLegAccessory: SET | undefined; - backLegAccessory: SET | undefined; - frontLegAccessoryRight: SET | undefined; - backLegAccessoryRight: SET | undefined; - lockBackLegAccessory: boolean | undefined; - unlockFrontLegAccessory: boolean | undefined; - unlockBackLegAccessory: boolean | undefined; - backAccessory: SET | undefined; - waistAccessory: SET | undefined; - chestAccessory: SET | undefined; - sleeveAccessory: SET | undefined; - extraAccessory: SET | undefined; + headAccessory: SET | undefined; + earAccessory: SET | undefined; + faceAccessory: SET | undefined; + neckAccessory: SET | undefined; + frontLegAccessory: SET | undefined; + backLegAccessory: SET | undefined; + frontLegAccessoryRight: SET | undefined; + backLegAccessoryRight: SET | undefined; + lockBackLegAccessory: boolean | undefined; + unlockFrontLegAccessory: boolean | undefined; + unlockBackLegAccessory: boolean | undefined; + backAccessory: SET | undefined; + waistAccessory: SET | undefined; + chestAccessory: SET | undefined; + sleeveAccessory: SET | undefined; + extraAccessory: SET | undefined; - coatFill: T | undefined; - coatOutline: T | undefined; - lockCoatOutline: boolean | undefined; + coatFill: T | undefined; + coatOutline: T | undefined; + lockCoatOutline: boolean | undefined; - eyelashes: number | undefined; - eyeColorLeft: T | undefined; - eyeColorRight: T | undefined; - eyeWhitesLeft: T | undefined; - eyeWhites: T | undefined; - eyeOpennessLeft: number | undefined; - eyeOpennessRight: number | undefined; - eyeshadow: boolean | undefined; - eyeshadowColor: T | undefined; - lockEyes: boolean | undefined; - lockEyeColor: boolean | undefined; - unlockEyeWhites: boolean | undefined; - unlockEyelashColor: boolean | undefined; - eyelashColor: T | undefined; - eyelashColorLeft: T | undefined; + eyelashes: number | undefined; + eyeColorLeft: T | undefined; + eyeColorRight: T | undefined; + eyeWhitesLeft: T | undefined; + eyeWhites: T | undefined; + eyeOpennessLeft: number | undefined; + eyeOpennessRight: number | undefined; + eyeshadow: boolean | undefined; + eyeshadowColor: T | undefined; + lockEyes: boolean | undefined; + lockEyeColor: boolean | undefined; + unlockEyeWhites: boolean | undefined; + unlockEyelashColor: boolean | undefined; + eyelashColor: T | undefined; + eyelashColorLeft: T | undefined; - fangs: number | undefined; - muzzle: number | undefined; - freckles: number | undefined; // TODO: remove - frecklesColor: T | undefined; // TODO: remove - magicColor: T | undefined; + fangs: number | undefined; + muzzle: number | undefined; + freckles: number | undefined; // TODO: remove + frecklesColor: T | undefined; // TODO: remove + magicColor: T | undefined; - cm: T[] | undefined; - cmFlip: boolean | undefined; + cm: T[] | undefined; + cmFlip: boolean | undefined; - customOutlines: boolean | undefined; - freeOutlines: boolean | undefined; - darkenLockedOutlines: boolean | undefined; + customOutlines: boolean | undefined; + freeOutlines: boolean | undefined; + darkenLockedOutlines: boolean | undefined; } export interface PaletteSpriteSet extends SpriteSetBase { - type: number; - pattern: number; - palette: Palette; - extraPalette?: Palette; + type: number; + pattern: number; + palette: Palette; + extraPalette?: Palette; } export interface PalettePonyInfo extends PonyInfoBase { - coatPalette: Palette; - eyePaletteLeft: Palette; - eyePalette: Palette; - cmPalette?: Palette; - defaultPalette: Palette; - waterPalette: Palette; - // faceAccessoryExtraPalette: Palette | undefined; - body: PaletteSpriteSet | undefined; - backLegs: PaletteSpriteSet | undefined; - frontLegs: PaletteSpriteSet | undefined; - magicColorValue: number; + coatPalette: Palette; + eyePaletteLeft: Palette; + eyePalette: Palette; + cmPalette?: Palette; + defaultPalette: Palette; + waterPalette: Palette; + // faceAccessoryExtraPalette: Palette | undefined; + body: PaletteSpriteSet | undefined; + backLegs: PaletteSpriteSet | undefined; + frontLegs: PaletteSpriteSet | undefined; + magicColorValue: number; } export interface PonyInfo extends PonyInfoBase> { @@ -1300,466 +1300,466 @@ export interface PonyInfoNumber extends PonyInfoBase> } export interface ColorExtra { - color: Sprite; - colors?: number; - extra?: Sprite; - palettes?: Uint32Array[]; - title?: string; - label?: string; - timestamp?: any; - colorMany?: Sprite[]; + color: Sprite; + colors?: number; + extra?: Sprite; + palettes?: Uint32Array[]; + title?: string; + label?: string; + timestamp?: any; + colorMany?: Sprite[]; } export interface TileSprites { - sprites: Sprite[]; - palettes: Uint32Array[]; + sprites: Sprite[]; + palettes: Uint32Array[]; } export interface ColorShadow { - color: Sprite; - shadow: Sprite; - palettes?: Uint32Array[]; + color: Sprite; + shadow: Sprite; + palettes?: Uint32Array[]; } export interface Shadow { - shadow: Sprite; + shadow: Sprite; } export type ColorExtraSet = (ColorExtra | undefined)[] | undefined; export type ColorExtraSets = ColorExtraSet[] | undefined; export interface BodyAnimationFrame { - body: number; - head: number; - wing: number; - tail: number; + body: number; + head: number; + wing: number; + tail: number; - frontLeg: number; - frontFarLeg: number; - backLeg: number; - backFarLeg: number; + frontLeg: number; + frontFarLeg: number; + backLeg: number; + backFarLeg: number; - bodyX: number; - bodyY: number; - headX: number; - headY: number; - frontLegX: number; - frontLegY: number; - frontFarLegX: number; - frontFarLegY: number; - backLegX: number; - backLegY: number; - backFarLegX: number; - backFarLegY: number; + bodyX: number; + bodyY: number; + headX: number; + headY: number; + frontLegX: number; + frontLegY: number; + frontFarLegX: number; + frontFarLegY: number; + backLegX: number; + backLegY: number; + backFarLegX: number; + backFarLegY: number; } export interface BodyShadow { - frame: number; - offset: number; + frame: number; + offset: number; } export interface BodyAnimation { - name: string; - loop: boolean; - fps: number; - frames: BodyAnimationFrame[]; - shadow?: BodyShadow[]; + name: string; + loop: boolean; + fps: number; + frames: BodyAnimationFrame[]; + shadow?: BodyShadow[]; } export interface HeadAnimationFrame { - headX: number; - headY: number; - left: Eye; - right: Eye; - mouth: Muzzle; + headX: number; + headY: number; + left: Eye; + right: Eye; + mouth: Muzzle; } export interface HeadAnimation { - name: string; - loop: boolean; - fps: number; - frames: HeadAnimationFrame[]; + name: string; + loop: boolean; + fps: number; + frames: HeadAnimationFrame[]; } export const enum PonyStateFlags { - None = 0, - CurlTail = 1, - FaceForward = 2, + None = 0, + CurlTail = 1, + FaceForward = 2, } export interface PonyState { - animation: BodyAnimation; - animationFrame: number; - headAnimation: HeadAnimation | undefined; - headAnimationFrame: number; - headTurned: boolean; - headTilt: number; - headTurn: number; - blinkFrame: number; - expression: Expression | undefined; - holding: Entity | undefined; - blushColor: number; - drawFaceExtra?: ((batch: PaletteSpriteBatch) => void) | undefined; - flags: PonyStateFlags; + animation: BodyAnimation; + animationFrame: number; + headAnimation: HeadAnimation | undefined; + headAnimationFrame: number; + headTurned: boolean; + headTilt: number; + headTurn: number; + blinkFrame: number; + expression: Expression | undefined; + holding: Entity | undefined; + blushColor: number; + drawFaceExtra?: ((batch: PaletteSpriteBatch) => void) | undefined; + flags: PonyStateFlags; } export const enum NoDraw { - None = 0x0, - Front = 0x1, - Front2 = 0x2, - BehindLeg = 0x4, - BehindBody = 0x8, - Behind = 0x10, - Body = 0x20, - BodyOnly = 0x40, - FarEar = 0x80, - CloseEar = 0x100, - FaceAccessory1 = 0x200, - FaceAccessory2 = 0x400, - TopMane = 0x800, - FrontMane = 0x1000, - Nose = 0x2000, - Head = 0x4000, - Eyes = 0x8000, - BackAccessory = 0x10000, - BackLeg = 0x20000, - BackFarLeg = 0x40000, - FrontLeg = 0x80000, - FrontFarLeg = 0x100000, - FarSleeves = 0x200000, - CloseSleeves = 0x400000, - FarEarShade = 0x800000, - Ears = NoDraw.FarEar | NoDraw.CloseEar, - WholeHead = NoDraw.Head | NoDraw.Nose | NoDraw.Ears, - FarLegs = NoDraw.BackFarLeg | NoDraw.FrontFarLeg, - CloseLegs = NoDraw.BackLeg | NoDraw.FrontLeg, - AllLegs = NoDraw.FarLegs | NoDraw.CloseLegs, - Sleeves = NoDraw.FarSleeves | NoDraw.CloseSleeves, + None = 0x0, + Front = 0x1, + Front2 = 0x2, + BehindLeg = 0x4, + BehindBody = 0x8, + Behind = 0x10, + Body = 0x20, + BodyOnly = 0x40, + FarEar = 0x80, + CloseEar = 0x100, + FaceAccessory1 = 0x200, + FaceAccessory2 = 0x400, + TopMane = 0x800, + FrontMane = 0x1000, + Nose = 0x2000, + Head = 0x4000, + Eyes = 0x8000, + BackAccessory = 0x10000, + BackLeg = 0x20000, + BackFarLeg = 0x40000, + FrontLeg = 0x80000, + FrontFarLeg = 0x100000, + FarSleeves = 0x200000, + CloseSleeves = 0x400000, + FarEarShade = 0x800000, + Ears = NoDraw.FarEar | NoDraw.CloseEar, + WholeHead = NoDraw.Head | NoDraw.Nose | NoDraw.Ears, + FarLegs = NoDraw.BackFarLeg | NoDraw.FrontFarLeg, + CloseLegs = NoDraw.BackLeg | NoDraw.FrontLeg, + AllLegs = NoDraw.FarLegs | NoDraw.CloseLegs, + Sleeves = NoDraw.FarSleeves | NoDraw.CloseSleeves, } export interface DrawPonyOptions { - flipped: boolean; - selected: boolean; - shadow: boolean; - extra: boolean; - toy: number; - swimming: boolean; - bounce: boolean; - shadowColor: number; - noEars: boolean; - gameTime: number; + flipped: boolean; + selected: boolean; + shadow: boolean; + extra: boolean; + toy: number; + swimming: boolean; + bounce: boolean; + shadowColor: number; + noEars: boolean; + gameTime: number; - // sheet generation switches - no: NoDraw; - useAllHooves: boolean; + // sheet generation switches + no: NoDraw; + useAllHooves: boolean; } // other export interface OAuthProvider { - id: string; - name: string; - color: string; - url?: string; - disabled?: boolean; - connectOnly?: boolean; - faIcon?: any; + id: string; + name: string; + color: string; + url?: string; + disabled?: boolean; + connectOnly?: boolean; + faIcon?: any; } export interface Profile { - id: string; - provider: string; - emails: string[]; - name: string | undefined; - username: string | undefined; - url: string | undefined; - createdAt: Date | undefined; - suspended: boolean | undefined; + id: string; + provider: string; + emails: string[]; + name: string | undefined; + username: string | undefined; + url: string | undefined; + createdAt: Date | undefined; + suspended: boolean | undefined; } export interface GameStatus { - version: string; - update?: boolean; - servers: (ServerInfo | ServerInfoShort)[]; + version: string; + update?: boolean; + servers: (ServerInfo | ServerInfoShort)[]; } export interface ServerInfoShort { - id: string; - offline: boolean; - online: number; + id: string; + offline: boolean; + online: number; } export interface ServerInfo extends ServerInfoShort { - name: string; - path: string; - desc: string; - host?: string; - flag?: string; - alert?: string; - dead: boolean; - filter: boolean; - require?: string; - flags?: ServerFeatureFlags; - countryFlags?: string[]; + name: string; + path: string; + desc: string; + host?: string; + flag?: string; + alert?: string; + dead: boolean; + filter: boolean; + require?: string; + flags?: ServerFeatureFlags; + countryFlags?: string[]; } // expression export const enum Muzzle { - Smile = 0, - Frown = 1, - Neutral = 2, - Scrunch = 3, - Blep = 4, - SmileOpen = 5, - Flat = 6, - Concerned = 7, - ConcernedOpen = 8, - SmileOpen2 = 9, - FrownOpen = 10, - NeutralOpen2 = 11, - ConcernedOpen2 = 12, - Kiss = 13, - SmileOpen3 = 14, - NeutralOpen3 = 15, - ConcernedOpen3 = 16, - Kiss2 = 17, - SmileTeeth = 18, - FrownTeeth = 19, - NeutralTeeth = 20, - ConcernedTeeth = 21, - SmilePant = 22, - NeutralPant = 23, - Oh = 24, - FlatBlep = 25, - // max: 31 + Smile = 0, + Frown = 1, + Neutral = 2, + Scrunch = 3, + Blep = 4, + SmileOpen = 5, + Flat = 6, + Concerned = 7, + ConcernedOpen = 8, + SmileOpen2 = 9, + FrownOpen = 10, + NeutralOpen2 = 11, + ConcernedOpen2 = 12, + Kiss = 13, + SmileOpen3 = 14, + NeutralOpen3 = 15, + ConcernedOpen3 = 16, + Kiss2 = 17, + SmileTeeth = 18, + FrownTeeth = 19, + NeutralTeeth = 20, + ConcernedTeeth = 21, + SmilePant = 22, + NeutralPant = 23, + Oh = 24, + FlatBlep = 25, + // max: 31 } export const CLOSED_MUZZLES = [ - Muzzle.Smile, Muzzle.Frown, Muzzle.Neutral, Muzzle.Scrunch, Muzzle.Flat, Muzzle.Concerned, - Muzzle.Kiss, Muzzle.Kiss2, + Muzzle.Smile, Muzzle.Frown, Muzzle.Neutral, Muzzle.Scrunch, Muzzle.Flat, Muzzle.Concerned, + Muzzle.Kiss, Muzzle.Kiss2, ]; export const enum Eye { - None = 0, - Neutral = 1, - Neutral2 = 2, - Neutral3 = 3, - Neutral4 = 4, - Neutral5 = 5, - Closed = 6, - Frown = 7, - Frown2 = 8, - Frown3 = 9, - Frown4 = 10, - Lines = 11, - ClosedHappy3 = 12, - ClosedHappy2 = 13, - ClosedHappy = 14, - Sad = 15, - Sad2 = 16, - Sad3 = 17, - Sad4 = 18, - Angry = 19, - Angry2 = 20, - Peaceful = 21, - Peaceful2 = 22, - X = 23, - X2 = 24, - // max: 31 + None = 0, + Neutral = 1, + Neutral2 = 2, + Neutral3 = 3, + Neutral4 = 4, + Neutral5 = 5, + Closed = 6, + Frown = 7, + Frown2 = 8, + Frown3 = 9, + Frown4 = 10, + Lines = 11, + ClosedHappy3 = 12, + ClosedHappy2 = 13, + ClosedHappy = 14, + Sad = 15, + Sad2 = 16, + Sad3 = 17, + Sad4 = 18, + Angry = 19, + Angry2 = 20, + Peaceful = 21, + Peaceful2 = 22, + X = 23, + X2 = 24, + // max: 31 } export function isEyeSleeping(eye: Eye) { - return eye === Eye.Closed || - (eye >= Eye.Lines && eye <= Eye.ClosedHappy) || - (eye >= Eye.Peaceful && eye <= Eye.X2); + return eye === Eye.Closed || + (eye >= Eye.Lines && eye <= Eye.ClosedHappy) || + (eye >= Eye.Peaceful && eye <= Eye.X2); } export const enum Iris { - Forward = 0, - Up = 1, - Left = 2, - Right = 3, - UpLeft = 4, - UpRight = 5, - Shocked = 6, - Down = 7, - // max: 15 - COUNT, + Forward = 0, + Up = 1, + Left = 2, + Right = 3, + UpLeft = 4, + UpRight = 5, + Shocked = 6, + Down = 7, + // max: 15 + COUNT, } export const enum ExpressionExtra { - None = 0, - Blush = 1, - Zzz = 2, - Cry = 4, // overrides tears - Tears = 8, - Hearts = 16, - // max: 31 + None = 0, + Blush = 1, + Zzz = 2, + Cry = 4, // overrides tears + Tears = 8, + Hearts = 16, + // max: 31 } export interface Expression { - left: Eye; - leftIris: Iris; - right: Eye; - rightIris: Iris; - muzzle: Muzzle; - extra: ExpressionExtra; + left: Eye; + leftIris: Iris; + right: Eye; + rightIris: Iris; + muzzle: Muzzle; + extra: ExpressionExtra; } export interface SupporterInvite { - id: string; - active: boolean; - name: string; - info: string; + id: string; + active: boolean; + name: string; + info: string; } export interface Subscription { - unsubscribe(): void; + unsubscribe(): void; } export enum Engine { - Default, - LayeredTiles, - Whiteness, - NewLighting, - Total, + Default, + LayeredTiles, + Whiteness, + NewLighting, + Total, } export interface DrawOptions { - gameTime: number; - lightColor: number; - shadowColor: number; - drawHidden: boolean; - showColliderMap: boolean; - showHeightmap: boolean; - debug: DebugFlags; - gridLines: boolean; - tileIndices: boolean; - tileGrid: boolean; - engine: Engine; - season: Season; - error: (message: string) => void; + gameTime: number; + lightColor: number; + shadowColor: number; + drawHidden: boolean; + showColliderMap: boolean; + showHeightmap: boolean; + debug: DebugFlags; + gridLines: boolean; + tileIndices: boolean; + tileGrid: boolean; + engine: Engine; + season: Season; + error: (message: string) => void; } export const defaultDrawOptions: DrawOptions = { - gameTime: 0, - lightColor: WHITE, - shadowColor: SHADOW_COLOR, - drawHidden: false, - showColliderMap: false, - showHeightmap: false, - debug: {}, - gridLines: false, - tileIndices: false, - tileGrid: false, - engine: Engine.Default, - season: Season.Summer, - error: () => { }, + gameTime: 0, + lightColor: WHITE, + shadowColor: SHADOW_COLOR, + drawHidden: false, + showColliderMap: false, + showHeightmap: false, + debug: {}, + gridLines: false, + tileIndices: false, + tileGrid: false, + engine: Engine.Default, + season: Season.Summer, + error: () => { }, }; export interface PonyEntityOptions { - hold?: number; - extra?: boolean; + hold?: number; + extra?: boolean; } export interface SpiderEntityOptions { - height: number; - time: number; + height: number; + time: number; } export interface SignEntityOptions { - sign: { - r?: number; - n?: number[]; - w?: number[]; - s?: number[]; - e?: number[]; - }; + sign: { + r?: number; + n?: number[]; + w?: number[]; + s?: number[]; + e?: number[]; + }; } export interface EntityDescriptor { - type: number; - typeName: string; - create: CreateEntity; + type: number; + typeName: string; + create: CreateEntity; } export type EntityOptions = PonyEntityOptions | SpiderEntityOptions | SignEntityOptions | {}; export type EntityOrPonyOptions = Partial & EntityOptions; export interface EntityWorldState { - season: Season; + season: Season; } export const defaultWorldState = { - season: Season.Summer, + season: Season.Summer, }; export type CreateEntity = (base: Entity, options: EntityOptions, worldState: EntityWorldState) => Entity; export type MixinEntity = (base: Entity, options: EntityOptions, worldState: EntityWorldState) => void; export interface CreateEntityMethod { - type: number; - typeName: string; - (x: number, y: number, options?: EntityOptions): Entity; + type: number; + typeName: string; + (x: number, y: number, options?: EntityOptions): Entity; } export interface IMap { - width: number; - height: number; - regionsX: number; - regionsY: number; - regions: TRegion[]; + width: number; + height: number; + regionsX: number; + regionsY: number; + regions: TRegion[]; } export interface FontPalettes { - emoji: Palette; - white: Palette; - supporter1: Palette; - supporter2: Palette; - supporter3: Palette; + emoji: Palette; + white: Palette; + supporter1: Palette; + supporter2: Palette; + supporter3: Palette; } export interface CommonPalettes { - defaultPalette: Palette; - mainFont: FontPalettes; - smallFont: FontPalettes; + defaultPalette: Palette; + mainFont: FontPalettes; + smallFont: FontPalettes; } export interface EngineInfo { - name: string; - engine: Engine; + name: string; + engine: Engine; } export interface DebugFlags { - showHelpers?: boolean; - showPalette?: boolean; - showRegions?: boolean; - showInfo?: boolean; - engine?: number; - debugCamera?: boolean; - // entity helpers - bounds?: boolean; - collider?: boolean; - id?: boolean; - cover?: boolean; - interact?: boolean; - trigger?: boolean; + showHelpers?: boolean; + showPalette?: boolean; + showRegions?: boolean; + showInfo?: boolean; + engine?: number; + debugCamera?: boolean; + // entity helpers + bounds?: boolean; + collider?: boolean; + id?: boolean; + cover?: boolean; + interact?: boolean; + trigger?: boolean; } export const enum UpdateFlags { - None = 0, - Position = 1, - Velocity = 2, - State = 4, - Expression = 8, - Type = 16, - Options = 32, - Info = 64, - Action = 128, - Name = 256, - NameBad = 512, - PlayerState = 1024, - SwitchRegion = 2048, - // max 32768 + None = 0, + Position = 1, + Velocity = 2, + State = 4, + Expression = 8, + Type = 16, + Options = 32, + Info = 64, + Action = 128, + Name = 256, + NameBad = 512, + PlayerState = 1024, + SwitchRegion = 2048, + // max 32768 } diff --git a/src/ts/common/mat2d.ts b/src/ts/common/mat2d.ts index 4f46fd1..6335f3e 100644 --- a/src/ts/common/mat2d.ts +++ b/src/ts/common/mat2d.ts @@ -1,122 +1,122 @@ import { Matrix2D } from './interfaces'; export function createMat2D(): Matrix2D { - const out = new Float32Array(6); - out[0] = 1; - out[3] = 1; - return out; + 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; + 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; + 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; + 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; + 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; + 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; + 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 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; + 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; + 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); + 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 (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); - } + if (base !== undefined) { + mulMat2D(tempMatrix, base, tempMatrix); + } - return 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; + 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; + return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1; } diff --git a/src/ts/common/mat4.ts b/src/ts/common/mat4.ts index 3e830aa..4036d07 100644 --- a/src/ts/common/mat4.ts +++ b/src/ts/common/mat4.ts @@ -1,33 +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; + 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; + 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; } diff --git a/src/ts/common/mixins.ts b/src/ts/common/mixins.ts index eb88a01..c6b0bad 100644 --- a/src/ts/common/mixins.ts +++ b/src/ts/common/mixins.ts @@ -1,9 +1,9 @@ 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, + 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'; @@ -14,557 +14,557 @@ import { mockPaletteManager } from './ponyInfo'; import { releasePalette } from '../graphics/paletteManager'; interface Renderable { - color?: Sprite; - shadow?: Sprite; + color?: Sprite; + shadow?: Sprite; } export interface AnimatedRenderable { - frames: Sprite[]; - shadow?: Sprite; - palette: Uint32Array; + frames: Sprite[]; + shadow?: Sprite; + palette: Uint32Array; } export interface AnimatedRenderable1 { - frames: (Sprite | undefined)[]; + 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], + [], + [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); + return palette && paletteManager && paletteManager.addArray(palette); } export function setPaletteManager(manager: PaletteManager | undefined) { - paletteManager = manager; + paletteManager = manager; } export function fakePaletteManager(action: () => T): T { - const tempPaletteManager = paletteManager; - paletteManager = mockPaletteManager; - const result = action(); - paletteManager = tempPaletteManager; - return result; + 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); + 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); - } + 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)); + 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 }; + return { pickableX, pickableY }; } export function mixPickable(pickableX: number, pickableY: number): MixinEntity { - return base => { - base.pickableX = pickableX; - base.pickableY = pickableY; - }; + 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); + 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; - }; + 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 }; + 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); + const bounds = rect(0, 0, 0, 0); - for (const collider of colliders) { - addRect(bounds, collider); - } + for (const collider of colliders) { + addRect(bounds, collider); + } - return bounds; + return bounds; } function roundedColliderList(x: number, y: number, w: number, h: number, stepsCount: number, tall = true) { - const list: Collider[] = []; - const steps = predefinedSteps[stepsCount]; + const list: Collider[] = []; + const steps = predefinedSteps[stepsCount]; - if (DEVELOPMENT && !steps) { - console.error('Invalid step count', steps); - } + 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)); - } + 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)); + 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)); - } + 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; + 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)); + 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)); + return mixColliders(...roundedColliderList(x, y, w, h, stepsCount, tall)); } export function mixColliders(...list: Collider[]): MixinEntity { - const bounds = getColliderBounds(list); + const bounds = getColliderBounds(list); - return base => { - base.flags |= EntityFlags.CanCollideWith; - base.colliders = list; - base.collidersBounds = bounds; - }; + 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[] = []; + 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)); - } + 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; + return colliders; } export function taperColliderSW(x: number, y: number, w: number, h: number, tall?: boolean) { - const colliders: Collider[] = []; + 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)); - } + 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; + return colliders; } export function taperColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) { - const colliders: Collider[] = []; + 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)); - } + 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; + return colliders; } export function taperColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) { - const colliders: Collider[] = []; + 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)); - } + 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; + return colliders; } export function skewColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) { - const colliders: Collider[] = []; + 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)); - } + 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; + return colliders; } export function skewColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) { - const colliders: Collider[] = []; + 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)); - } + 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; + return colliders; } export function triangleColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) { - const colliders: Collider[] = []; + 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)); - } + 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; + return colliders; } export function triangleColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) { - const colliders: Collider[] = []; + 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)); - } + 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; + return colliders; } export function mixInteract(x: number, y: number, w: number, h: number, interactRange?: number): MixinEntity { - const interactBounds = rect(x, y, w, h); + const interactBounds = rect(x, y, w, h); - return base => { - base.flags |= EntityFlags.Interactive; - base.interactBounds = interactBounds; - base.interactRange = interactRange; - }; + 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; - }; + 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; + 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; + 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 = {} + 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); + 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; - } + if (SERVER && !TESTS) { + return base => base.bounds = bounds; + } - return base => { - const defaultPalette = anim.shadow && createPalette(sprites.defaultPalette); - const palette = createPalette(anim.palette); + 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; + let time = repeat ? Math.random() * 5 : 0; + let animation = 0; + let lastFrame = 0; - const getFrame = (options: DrawOptions) => { - let frameNumber = Math.floor(time * fps); + const getFrame = (options: DrawOptions) => { + let frameNumber = Math.floor(time * fps); - if (useGameTime) { - frameNumber = Math.floor((options.gameTime / 1000) * fps); - } + if (useGameTime) { + frameNumber = Math.floor((options.gameTime / 1000) * fps); + } - if (animations) { - if (repeat) { - frameNumber = frameNumber % animations[animation].length; - } + 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); - } - }; + 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.bounds = bounds; + base.palettes = []; + defaultPalette && base.palettes.push(defaultPalette); + palette && base.palettes.push(palette); - base.update = function (delta: number) { - time += delta; + base.update = function (delta: number) { + time += delta; - const anim = getAnimationFromEntityState(this.state); + const anim = getAnimationFromEntityState(this.state); - if (animations && anim !== animation) { - animation = anim; - time = 0; - } + if (animations && anim !== animation) { + animation = anim; + time = 0; + } - const frameNumber = Math.floor(time * fps); + const frameNumber = Math.floor(time * fps); - if (lastFrame !== frameNumber) { - lastFrame = frameNumber; - return true; - } else { - return false; - } - }; + 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); + 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); + batch.save(); + batch.translate(x, y); - if (hasFlag(this.state, EntityState.FacingRight) || flipped) { - batch.scale(-1, 1); - } + 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(); - }; + 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); + 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); + batch.save(); + batch.translate(x, y); - if (hasFlag(this.state, EntityState.FacingRight) || flipped) { - batch.scale(-1, 1); - } + if (hasFlag(this.state, EntityState.FacingRight) || flipped) { + batch.scale(-1, 1); + } - batch.translate(-dx, -dy); - batch.drawSprite(frameSprite, this.lightSpriteColor!, 0, 0); - batch.restore(); - }; - } - }; + 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, + sprite: PaletteRenderable, dx: number, dy: number, paletteIndex: number, + padLeft: number, padTop: number, padRight: number, padBottom: number, ): MixinEntity { - const bounds = getRenderableBounds(sprite, dx, dy); + const bounds = getRenderableBounds(sprite, dx, dy); - return base => { - base.bounds = bounds; + 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); + 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; + 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.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); - } - }; - } - }; + 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); + const bounds = getRenderableBounds(sprite, dx, dy); - return base => { - base.bounds = bounds; + 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); + 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); + 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); - } + if (sprite.shadow !== undefined) { + batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); + } - batch.globalAlpha = opacity; + batch.globalAlpha = opacity; - if (sprite.color !== undefined) { - batch.drawSprite(sprite.color, WHITE, palette, x, y); - } + if (sprite.color !== undefined) { + batch.drawSprite(sprite.color, WHITE, palette, x, y); + } - batch.globalAlpha = 1; - }; - } - }; + batch.globalAlpha = 1; + }; + } + }; } export interface MixDraw { - sprite: PaletteRenderable; - dx: number; - dy: number; - palette: number; + sprite: PaletteRenderable; + dx: number; + dy: number; + palette: number; } export interface MixDrawSeasonal { - summer: MixDraw; - autumn?: Partial; - winter?: Partial; - spring?: Partial; + summer: MixDraw; + autumn?: Partial; + winter?: Partial; + spring?: Partial; } function addBounds(bounds: Rect, setup: MixDraw) { - addRect(bounds, getRenderableBounds(setup.sprite, setup.dx, setup.dy)); + 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 }; + 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); + addBounds(bounds, summer); + addBounds(bounds, autumn); + addBounds(bounds, winter); + addBounds(bounds, spring); - return (base, _, worldState) => { - base.bounds = bounds; + 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; + 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; + 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; - } + 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; + sprite = set.sprite; + dx = set.dx; + dy = set.dy; + paletteIndex = set.palette; - if (base.palettes) { - for (const palette of base.palettes) { - releasePalette(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); - }; + 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); + 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); + 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); - } + if (sprite.shadow !== undefined) { + batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); + } - batch.globalAlpha = opacity; + batch.globalAlpha = opacity; - if (sprite.color !== undefined) { - batch.drawSprite(sprite.color, WHITE, palette, x, y); - } + if (sprite.color !== undefined) { + batch.drawSprite(sprite.color, WHITE, palette, x, y); + } - batch.globalAlpha = 1; + batch.globalAlpha = 1; - if (season !== options.season) { - setupSeason(options.season); - } - }; - } - }; + if (season !== options.season) { + setupSeason(options.season); + } + }; + } + }; } function splitSprite(sprite: Sprite, x: number, w: number, h: number) { - const result: Sprite[] = []; + 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 }); - } + 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; + 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 }, + { 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; @@ -573,278 +573,278 @@ 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, - }, + { + 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; + 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; + 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; + base.bounds = rect(-20, -boundsH, 40, boundsH); + base.options = options; - if (SERVER && !TESTS) - return; + 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 { + 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); + 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); + 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); + 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); - } - } + 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); + 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 < 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 = 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); - } - } - }; - }; + 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; + 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!; + 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); - }; - } - }; + 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; + 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!; + const x = toScreenX(this.x) - this.lightSpriteX!; + const y = toScreenYWithZ(this.y, this.z) - this.lightSpriteY!; - batch.drawSprite(sprite, this.lightSpriteColor || BLACK, x, y); - }; - } - }; + 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); + const sprite = sprites.rainfall.color; // 110x477 + const bounds = rect(toScreenX(-4), -sprite.h, toScreenX(8), sprite.h); - return base => { - base.bounds = bounds; + return base => { + base.bounds = bounds; - if (SERVER && !TESTS) - return; + if (SERVER && !TESTS) + return; - let time = 0; - const palette = createPalette(sprites.defaultPalette); - base.palettes = [palette]; + let time = 0; + const palette = createPalette(sprites.defaultPalette); + base.palettes = [palette]; - // update(delta: number) { - // time += delta * 1000; + // update(delta: number) { + // time += delta * 1000; - // if (time > 200) { - // time -= 200; - // } - // }, + // 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); - }; - }; + 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); + const bounds = getRenderableBounds(sprite, dx, dy); - return base => { - base.bounds = bounds; + 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); - }; - } - }; + 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; - }; + return base => { + base.flags |= EntityFlags.Bobbing; + base.bobsFps = bobsFps; + base.bobs = bobs; + }; } let fullWalls = true; export function toggleWalls() { - fullWalls = !fullWalls; + fullWalls = !fullWalls; } export function mixDrawWall( - full: PaletteRenderable, half: PaletteRenderable, dx: number, dy: number, dy2: number + full: PaletteRenderable, half: PaletteRenderable, dx: number, dy: number, dy2: number ): MixinEntity { - const fullBounds = getRenderableBounds(full, dx, dy); - // const halfBounds = getRenderableBounds(half, dx, dy2); + const fullBounds = getRenderableBounds(full, dx, dy); + // const halfBounds = getRenderableBounds(half, dx, dy2); - return base => { - base.bounds = fullBounds; // fullWalls ? fullBounds : halfBounds + return base => { + base.bounds = fullBounds; // fullWalls ? fullBounds : halfBounds - if (SERVER && !TESTS) - return; + if (SERVER && !TESTS) + return; - const fullPalette = createPalette(att(full.palettes, 0)); - const halfPalette = createPalette(att(half.palettes, 0)); + 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.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); - }; - }; + 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 + sprite: PaletteRenderable, dx: number, dy: number ): MixinEntity { - const heightOffset = 30; - const spriteColor = sprite.color; - const baseBounds = getRenderableBounds(sprite, dx, dy); + const heightOffset = 30; + const spriteColor = sprite.color; + const baseBounds = getRenderableBounds(sprite, dx, dy); - if (!spriteColor) - throw new Error('Missing sprite'); + 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; + 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; + if (SERVER && !TESTS) + return; - const palette = createPalette(sprite.palettes && sprite.palettes[0]); - base.palettes = [palette]; + 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; + 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; + 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); - } - }; - }; + batch.drawRect(0x181818ff, x + 2, y - lineLength, 1, lineLength + 1); + batch.drawSprite(spriteColor, WHITE, palette, x, y); + } + }; + }; } diff --git a/src/ts/common/movementUtils.ts b/src/ts/common/movementUtils.ts index e31097d..32508a7 100644 --- a/src/ts/common/movementUtils.ts +++ b/src/ts/common/movementUtils.ts @@ -5,22 +5,22 @@ 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], + [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; @@ -32,92 +32,92 @@ const PI2 = Math.PI * 2; const DIRS_ANGLE = DIRS.length / PI2; export function flagsToSpeed(flags: EntityState): number { - const state = flags & EntityState.PonyStateMask; + 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; - } + 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 }; + 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; + 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; + 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 + 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 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; + 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, - ]; + 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; + 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 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 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; + 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) }; + 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); + return vx < 0 ? false : (vx > 0 ? true : right); } export function shouldBeFacingRight(entity: Entity): boolean { - return isMovingRight(entity.vx, hasFlag(entity.state, EntityState.FacingRight)); + return isMovingRight(entity.vx, hasFlag(entity.state, EntityState.FacingRight)); } diff --git a/src/ts/common/offsets.ts b/src/ts/common/offsets.ts index 2b0de49..f53676f 100644 --- a/src/ts/common/offsets.ts +++ b/src/ts/common/offsets.ts @@ -1,6 +1,6 @@ interface Point { - x: number; - y: number; + x: number; + y: number; } type Pt = [number, number]; @@ -20,19 +20,19 @@ 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 + _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)); + 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 @@ -59,80 +59,80 @@ offsets(15, [8, 11], [7, 11], [9, 14], [7, 11], [6, 9], [8, 11], /***/[7, 11], [ 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 + [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], + [0, 9], // 0 + [0, 0], + [0, 0], + [0, 1], + [0, 2], + [0, 2], // 5 + [0, 1], + [0, 1], + [0, 2], + [0, 3], + [0, 2], // 10 + [0, 2], + [0, 3], + [0, 1], + [0, 1], + [0, 1], // 15 + [0, 9], + [0, 3], + [0, 3], + [0, 3], + [0, 3], // 20 + [0, 3], + [0, 2], + [0, 3], + [0, 3], + [0, 3], // 25 + [0, 2], + [0, 3], + [-1, 3], + [0, 3], + [0, 3], // 30 + [0, 3], ]); export const HEAD_ACCESSORY_OFFSETS = createPoints([ - [0, 0], // 0 - [0, -5], - [0, -5], - [0, -4], - [0, -4], - [0, -4], // 5 - [0, -4], - [1, -4], - [0, -4], - [0, -3], - [0, -4], // 10 - [0, -4], - [0, -3], - [1, -5], - [0, -4], - [0, -4], // 15 - [0, 0], - [0, -4], - [0, -4], - [0, -4], - [0, -4], // 20 - [0, -4], - [0, -5], - [0, -5], - [0, -4], - [1, -3], // 25 - [0, -4], - [0, -4], - [0, -4], - [0, -4], - [0, -4], // 30 - [0, -3], + [0, 0], // 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], ]); diff --git a/src/ts/common/pony.ts b/src/ts/common/pony.ts index 7bfc6b8..1ba8077 100644 --- a/src/ts/common/pony.ts +++ b/src/ts/common/pony.ts @@ -1,37 +1,37 @@ 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, + 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 + 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 + isPonySitting, isPonyFlying, isPonyLying, isPonyStanding, isPonyLandedOrCanLand, isIdle, isIdleAnimation, + isFacingRight, releaseEntity } from './entityUtils'; import { - getAnimation, getAnimationFrame, setAnimatorState, updateAnimator, createAnimator, AnimatorState, - resetAnimatorState + 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, + 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 + sneezeAnimation, holdPoofAnimation, heartsAnimation, tearsAnimation, cryAnimation, zzzAnimations, magicAnimation } from '../client/spriteAnimations'; import { rect } from './rect'; import { addOrRemoveFromEntityList } from './worldMap'; @@ -55,624 +55,624 @@ const interactBoundsFly = rect(interactBounds.x, interactBounds.y - flyY, intera const defaultExpr = encodeExpression(undefined); export function createPony( - id: number, state: EntityState, info: string | Uint8Array | undefined, defaultPalette: Palette, - paletteManager: PaletteManager + 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(), - 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, - }; + 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(), + 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); + pony.ponyState.drawFaceExtra = batch => drawFaceExtra(batch, pony); - return pony; + return pony; } export function isPony(entity: Entity): entity is Pony { - return entity.type === PONY_TYPE; + return entity.type === PONY_TYPE; } export function isPonyOnTheGround(pony: Pony) { - return !isPonyFlying(pony) && !isFlyingUpOrDown(pony.animator.state); + return !isPonyFlying(pony) && !isFlyingUpOrDown(pony.animator.state); } export function getPaletteInfo(pony: Pony) { - return ensurePonyInfoDecoded(pony); + return ensurePonyInfoDecoded(pony); } export function releasePony(pony: Pony) { - if (pony.ponyState.holding) { - releaseEntity(pony.ponyState.holding); - } + if (pony.ponyState.holding) { + releaseEntity(pony.ponyState.holding); + } - releasePalettePonyInfo(pony); + releasePalettePonyInfo(pony); } export function canPonyFly(pony: Pony) { - return !!pony.palettePonyInfo && canFly(pony.palettePonyInfo); + return !!pony.palettePonyInfo && canFly(pony.palettePonyInfo); } export function canPonyLie(pony: Pony, map: IMap) { - return !isPonyLying(pony) && (isIdle(pony) || isSittingDown(pony.animator.state) || isFlyingDown(pony.animator.state)) && - isPonyLandedOrCanLand(pony, map); + return !isPonyLying(pony) && (isIdle(pony) || isSittingDown(pony.animator.state) || isFlyingDown(pony.animator.state)) && + isPonyLandedOrCanLand(pony, map); } export function canPonySit(pony: Pony, map: IMap) { - return !isPonySitting(pony) && (isIdle(pony) || isFlyingDown(pony.animator.state)) && - isPonyLandedOrCanLand(pony, map); + return !isPonySitting(pony) && (isIdle(pony) || isFlyingDown(pony.animator.state)) && + isPonyLandedOrCanLand(pony, map); } export function canPonyStand(pony: Pony, map: IMap) { - return !isPonyStanding(pony) && (isIdleAnimation(pony.ponyState.animation) || isSittingUp(pony.animator.state)) && - isPonyLandedOrCanLand(pony, map); + 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); + return !isPonyFlying(pony) && canPonyFly(pony) && !isFlyingUpOrDown(pony.animator.state); } export function getPonyChatHeight(pony: Pony) { - const baseHeight = 2; - const state = pony.ponyState; + 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); - } + 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; + pony.info = info; - if (pony.palettePonyInfo !== undefined) { - releasePalettePonyInfo(pony); - ensurePonyInfoDecoded(pony); - pony.discardBatch = true; + 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); - } + if (isPonyFlying(pony) && !canPonyFly(pony)) { + DEVELOPMENT && console.warn('Force land'); + pony.state = setFlag(pony.state, EntityState.PonyFlying, false); + resetAnimatorState(pony.animator); + } - apply(); - } + 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); - } + 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!; + return pony.palettePonyInfo!; } export function invalidatePalettesForPony(pony: Pony) { - pony.discardBatch = true; + pony.discardBatch = true; } export function doBoopPonyAction(game: PonyTownGame, pony: Pony) { - doPonyAction(pony, DoAction.Boop); + 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); - } + 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; - } + pony.lastBoopSplash = performance.now() + 800; + } } export function doPonyAction(pony: Pony, action: DoAction) { - pony.doAction = action; + pony.doAction = action; } export function setPonyExpression(pony: Pony, expr: number) { - pony.expr = expr; + pony.expr = expr; } export function hasExtendedInfo(pony: Pony) { - return pony.ex; + return pony.ex; } export function hasHeadAnimation(pony: Pony) { - return pony.headAnimation !== undefined; + return pony.headAnimation !== undefined; } export function setHeadAnimation(pony: Pony, headAnimation: HeadAnimation | undefined) { - if (pony.headAnimation !== headAnimation) { - pony.headTime = 0; - pony.headAnimation = headAnimation; - } + 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.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 (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; - } - } + 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); + const createBatch = pony.vx === 0 && pony.vy === 0 && !swimming; + const right = isFacingRight(pony); - if (createBatch) { - batch.startBatch(); - } + if (createBatch) { + batch.startBatch(); + } - batch.save(); - transformBatch(batch, pony); + 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; + 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; + options.shadowColor = drawOptions.shadowColor; - const ponyState = pony.ponyState; - drawPony(batch, pony.palettePonyInfo, ponyState, 0, 0, options); + 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 ( + 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); - } - } + 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(); + 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); - } - } + 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 + 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; + 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 (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 (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); - } + 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(); - } + 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; + 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 (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 (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); - // } + // if (drawMagic) { + // const frame = pony.magicEffect.frame; + // const sprite = sprites.magic2_light.frames[frame]; + // batch.drawSprite(sprite, WHITE, 0, 0); + // } - batch.restore(); - } + batch.restore(); + } } export function flagsToState(state: EntityState, moving: boolean, isSwimming: boolean): AnimatorState { - const ponyState = state & EntityState.PonyStateMask; + 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})`); - } - } + 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); + // 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.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}`); - } - } + 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); - } + pony.doAction = DoAction.None; + } else { + setAnimatorState(pony.animator, animationState); + } - // head - pony.headTime += delta; + // head + pony.headTime += delta; - if (pony.headAnimation !== undefined) { - const frame = Math.floor(pony.headTime * pony.headAnimation.fps); + 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 (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 (state.headAnimation !== pony.headAnimation) { + state.headAnimation = pony.headAnimation; - if (pony.headAnimation === sneeze) { - playAnimation(pony.sneezeEffect, sneezeAnimation); - } - } + if (pony.headAnimation === sneeze) { + playAnimation(pony.sneezeEffect, sneezeAnimation); + } + } - // effects / expressions - if (pony.currentExpression !== pony.expr) { - updatePonyExpression(pony, pony.expr, safe); - } + // 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); - } + 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); + 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); + // holding + const holdingUpdated = + state.holding !== undefined && + state.holding.update !== undefined && + state.holding.update(delta, gameTime); - // blink - pony.blinkTime += delta; + // blink + pony.blinkTime += delta; - if ((pony.blinkTime - pony.nextBlink) > 1) { - pony.nextBlink = pony.blinkTime + Math.random() * 2 + 3; - } + if ((pony.blinkTime - pony.nextBlink) > 1) { + pony.nextBlink = pony.blinkTime + Math.random() * 2 + 3; + } - // update animator - updateAnimator(pony.animator, delta); + // 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); + // 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); - } + // 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); + // 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; - } - } + 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; + // 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; + 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); + 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; - } + 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); + const hasLight = hasDrawLight(pony); + const hasLightSprite1 = hasLightSprite(pony); - addOrRemoveFromEntityList(game.map.entitiesLight, pony, hadLight, hasLight); - addOrRemoveFromEntityList(game.map.entitiesLightSprite, pony, hadLightSprite, hasLightSprite1); + 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); + 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 || + 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 ( + 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 (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; - } - } + 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; + const expression = decodeExpression(expr); + pony.currentExpression = pony.expr; + pony.ponyState.expression = expression; - if (expression && safe) { - filterExpression(expression); - } + if (expression && safe) { + filterExpression(expression); + } - const extra = (expression && expression.extra) || 0; + 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.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.Zzz)) { + playOneOfAnimations(pony.zzzEffect, zzzAnimations); + } else { + playAnimation(pony.zzzEffect, undefined); + } - if (hasFlag(extra, ExpressionExtra.Hearts)) { - playAnimation(pony.heartsEffect, heartsAnimation); - } else { - playAnimation(pony.heartsEffect, 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); + 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; - } + 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); + 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); - } + 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); + } } diff --git a/src/ts/common/ponyInfo.ts b/src/ts/common/ponyInfo.ts index 2fdc198..53cce77 100644 --- a/src/ts/common/ponyInfo.ts +++ b/src/ts/common/ponyInfo.ts @@ -1,16 +1,16 @@ import * as sprites from '../generated/sprites'; import { releasePalette, createPalette } from '../graphics/paletteManager'; import { - PonyInfo, SpriteSet, PalettePonyInfo, PaletteSpriteSet, PaletteManager, Palette, ColorExtraSets, PonyInfoBase, - PonyInfoNumber, ColorExtra + 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 + mergedManes, mergedBackManes, mergedFacialHair, mergedEarAccessories, mergedChestAccessories, + SLEEVED_ACCESSORIES, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories } from '../client/ponyUtils'; const MAX_COLORS = 6; @@ -26,109 +26,109 @@ type Arr = (T | undefined)[] | undefined; type PonyInfoGeneric = PonyInfoBase>; export const mockPaletteManager: PaletteManager = { - add(colors: number[]): Palette { - return this.addArray(new Uint32Array(colors)); - }, - addArray(colors: Uint32Array): Palette { - return createPalette(colors); - }, - init() { - } + 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 { - if (otherFills.length !== (MAX_COLORS - 1)) - throw new Error('Invalid fills count'); + if (otherFills.length !== (MAX_COLORS - 1)) + throw new Error('Invalid fills count'); - const fills = [fill, ...otherFills]; - const outlines = fills.map(fillToOutline); + 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), - }; + 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; + 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'), + 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), + 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), - }, + 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, + 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', + 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', + fangs: 0, + muzzle: 0, + freckles: 0, + frecklesColor: '8b0000', + magicColor: 'ffffff', - cm: [], - cmFlip: false, + cm: [], + cmFlip: false, - customOutlines: false, - freeOutlines: false, - darkenLockedOutlines: false, - }); + customOutlines: false, + freeOutlines: false, + darkenLockedOutlines: false, + }); } // sync @@ -136,321 +136,321 @@ export function createBasePony(): PonyInfo { type FillToOutline = (fill: T | undefined) => T | undefined; export function getBaseFill(set?: SpriteSet): T | undefined { - return set && set.fills && set.fills[0]; + return set && set.fills && set.fills[0]; } export function getBaseOutline(set?: SpriteSet): T | undefined { - return set && set.outlines && set.outlines[0]; + return set && set.outlines && set.outlines[0]; } export function syncLockedSpriteSet( - set: SpriteSet | undefined, customOutlines: boolean, fillToOutline: FillToOutline, baseFill?: T, - baseOutline?: T + set: SpriteSet | undefined, customOutlines: boolean, fillToOutline: FillToOutline, baseFill?: T, + baseOutline?: T ) { - if (set === undefined) - return; + if (set === undefined) + return; - const fills = set.fills; + const fills = set.fills; - if (!fills) - return; + if (!fills) + return; - const lockFills = set.lockFills; + const lockFills = set.lockFills; - if (lockFills) { - for (let i = 0; i < lockFills.length; i++) { - if (lockFills[i]) { - fills[i] = i === 0 ? baseFill : fills[0]; - } - } - } + 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; + 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 (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]); - } - } - } - } + if (lockOutlines[i]) { + if (i === 0 && baseOutline && lockFills && lockFills[i]) { + outlines[i] = baseOutline; + } else { + outlines[i] = fillToOutline(fills[i]); + } + } + } + } } function syncLockedSpritesSet2( - set: SpriteSet | undefined, fillToOutline: FillToOutline, baseFills: (T | undefined)[], - baseOutlines: (T | undefined)[] + set: SpriteSet | undefined, fillToOutline: FillToOutline, 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.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]); - } - } - }); - } + 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(set: SpriteSet | undefined, defaultColor: T): T | undefined { - return set && set.type && set.fills && set.fills[0] || defaultColor; + return set && set.type && set.fills && set.fills[0] || defaultColor; } function getOutlineOf2(set: SpriteSet | undefined, defaultColor: T): T | undefined { - return set && set.type && set.outlines && set.outlines[0] || defaultColor; + return set && set.type && set.outlines && set.outlines[0] || defaultColor; } function syncLockedBasePonyInfo( - info: PonyInfoGeneric, fillToOutline: FillToOutline, defaultColor: T + info: PonyInfoGeneric, fillToOutline: FillToOutline, defaultColor: T ): PonyInfoGeneric { - const customOutlines = !!info.customOutlines; + const customOutlines = !!info.customOutlines; - if (!customOutlines || info.lockCoatOutline) { - info.coatOutline = fillToOutline(info.coatFill); - } + if (!customOutlines || info.lockCoatOutline) { + info.coatOutline = fillToOutline(info.coatFill); + } - if (info.lockEyes) { - info.eyeOpennessLeft = info.eyeOpennessRight; - } + if (info.lockEyes) { + info.eyeOpennessLeft = info.eyeOpennessRight; + } - if (info.lockEyeColor) { - info.eyeColorLeft = info.eyeColorRight; - } + if (info.lockEyeColor) { + info.eyeColorLeft = info.eyeColorRight; + } - if (!info.unlockEyeWhites) { - info.eyeWhitesLeft = info.eyeWhites; - } + if (!info.unlockEyeWhites) { + info.eyeWhitesLeft = info.eyeWhites; + } - if (!info.unlockEyelashColor) { - info.eyelashColorLeft = info.eyelashColor; - } + if (!info.unlockEyelashColor) { + info.eyelashColorLeft = info.eyelashColor; + } - syncLockedSpriteSet(info.head, customOutlines, fillToOutline, info.coatFill, info.coatOutline); - syncLockedSpriteSet(info.nose, customOutlines, fillToOutline, info.coatFill, info.coatOutline); - syncLockedSpriteSet(info.ears, customOutlines, fillToOutline, info.coatFill, info.coatOutline); - syncLockedSpriteSet(info.horn, customOutlines, fillToOutline, info.coatFill, info.coatOutline); - syncLockedSpriteSet(info.wings, customOutlines, fillToOutline, info.coatFill, info.coatOutline); - syncLockedSpriteSet(info.frontHooves, customOutlines, fillToOutline, info.coatFill, info.coatOutline); - syncLockedSpriteSet( - info.backHooves, customOutlines, fillToOutline, getBaseFill(info.frontHooves), getBaseOutline(info.frontHooves)); + syncLockedSpriteSet(info.head, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.nose, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.ears, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.horn, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.wings, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet(info.frontHooves, customOutlines, fillToOutline, info.coatFill, info.coatOutline); + syncLockedSpriteSet( + info.backHooves, customOutlines, fillToOutline, getBaseFill(info.frontHooves), getBaseOutline(info.frontHooves)); - syncLockedSpriteSet(info.mane, customOutlines, fillToOutline); + syncLockedSpriteSet(info.mane, customOutlines, fillToOutline); - const baseManeFill = getBaseFill(info.mane); - const baseManeOutline = getBaseOutline(info.mane); + const baseManeFill = getBaseFill(info.mane); + const baseManeOutline = getBaseOutline(info.mane); - syncLockedSpriteSet(info.backMane, customOutlines, fillToOutline, baseManeFill, baseManeOutline); - syncLockedSpriteSet(info.tail, customOutlines, fillToOutline, baseManeFill, baseManeOutline); - syncLockedSpriteSet(info.facialHair, customOutlines, fillToOutline, baseManeFill, baseManeOutline); + syncLockedSpriteSet(info.backMane, customOutlines, fillToOutline, baseManeFill, baseManeOutline); + syncLockedSpriteSet(info.tail, customOutlines, fillToOutline, baseManeFill, baseManeOutline); + syncLockedSpriteSet(info.facialHair, customOutlines, fillToOutline, baseManeFill, baseManeOutline); - syncLockedSpriteSet(info.headAccessory, customOutlines, fillToOutline); - syncLockedSpriteSet(info.earAccessory, customOutlines, fillToOutline); - syncLockedSpriteSet(info.faceAccessory, customOutlines, fillToOutline); - syncLockedSpriteSet(info.neckAccessory, customOutlines, fillToOutline); - syncLockedSpriteSet(info.frontLegAccessory, customOutlines, fillToOutline); - syncLockedSpriteSet(info.backLegAccessory, customOutlines, fillToOutline); - syncLockedSpriteSet(info.frontLegAccessoryRight, customOutlines, fillToOutline); - syncLockedSpriteSet(info.backLegAccessoryRight, customOutlines, fillToOutline); - syncLockedSpriteSet(info.backAccessory, customOutlines, fillToOutline); - syncLockedSpriteSet(info.waistAccessory, customOutlines, fillToOutline); - syncLockedSpriteSet(info.chestAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.headAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.earAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.faceAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.neckAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.frontLegAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.backLegAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.frontLegAccessoryRight, customOutlines, fillToOutline); + syncLockedSpriteSet(info.backLegAccessoryRight, customOutlines, fillToOutline); + syncLockedSpriteSet(info.backAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(info.waistAccessory, customOutlines, fillToOutline); + syncLockedSpriteSet(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), - }; - } + 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( - info.sleeveAccessory, customOutlines, fillToOutline, getBaseFill(info.chestAccessory), getBaseOutline(info.chestAccessory)); + syncLockedSpriteSet( + info.sleeveAccessory, customOutlines, fillToOutline, getBaseFill(info.chestAccessory), getBaseOutline(info.chestAccessory)); - syncLockedSpritesSet2(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), - ]); + syncLockedSpritesSet2(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; + return info; } export function syncLockedPonyInfo(info: PonyInfo): PonyInfo { - const darkenLocked = !!info.freeOutlines && !!info.darkenLockedOutlines; - const fillToOutlineFunc = darkenLocked ? fillToOutlineWithDarken : fillToOutline; - return syncLockedBasePonyInfo(info, fillToOutlineFunc, '000000'); + const darkenLocked = !!info.freeOutlines && !!info.darkenLockedOutlines; + const fillToOutlineFunc = darkenLocked ? fillToOutlineWithDarken : fillToOutline; + return syncLockedBasePonyInfo(info, fillToOutlineFunc, '000000'); } function fillToOutlineSafe(color: number | undefined) { - return fillToOutlineColor((color === undefined || color === 0) ? BLACK : color); + return fillToOutlineColor((color === undefined || color === 0) ? BLACK : color); } function fillToOutlineSafeWithDarken(color: number | undefined) { - return darkenForOutline(fillToOutlineColor((color === undefined || color === 0) ? BLACK : color)); + 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(info, fillToOutlineFunc, BLACK); + const darkenLocked = !!info.freeOutlines && !!info.darkenLockedOutlines; + const fillToOutlineFunc = darkenLocked ? fillToOutlineSafeWithDarken : fillToOutlineSafe; + return syncLockedBasePonyInfo(info, fillToOutlineFunc, BLACK); } // PalettePonyInfo function parseFast(color: string | undefined): number { - return color ? parseColorFast(color) : BLACK; + return color ? parseColorFast(color) : BLACK; } function parseCMColor(color: string): number { - return color ? parseColorFast(color) : TRANSPARENT; + return color ? parseColorFast(color) : TRANSPARENT; } export function toColorList(colors: (string | undefined)[]): Uint32Array { - const result = new Uint32Array(colors.length + 1); + const result = new Uint32Array(colors.length + 1); - for (let i = 0; i < colors.length; i++) { - result[i + 1] = parseFast(colors[i]); - } + for (let i = 0; i < colors.length; i++) { + result[i + 1] = parseFast(colors[i]); + } - return result; + 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); + 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, outlineColors: Arr, defaultColor: string, length: number, darken: boolean + fillColors: Arr, outlineColors: Arr, defaultColor: string, length: number, darken: boolean ): string[] { - const fills = fillColors || []; - const outlines = outlineColors || []; - const colors = array(length * 2, defaultColor); + 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; + 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; - } - } + if (darken) { + colors[i * 2 + 1] = outlines[i] ? colorToHexRGB(darkenForOutline(parseColorFast(outlines[i]!))) : defaultColor; + } else { + colors[i * 2 + 1] = outlines[i] || defaultColor; + } + } - return colors; + return colors; } export function getColorsFromSet({ fills, outlines }: SpriteSet, defaultColor: string, darken: boolean): string[] { - const length = Math.max(fills ? fills.length : 0, outlines ? outlines.length : 0); - return getColorsGeneric(fills, outlines, defaultColor, length, darken); + 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); + const result = new Uint32Array(colors.length + 1); - for (let i = 0; i < colors.length; i++) { - result[i + 1] = colors[i] || BLACK; - } + for (let i = 0; i < colors.length; i++) { + result[i + 1] = colors[i] || BLACK; + } - return result; + return result; } export type GetColorsForSet = (set: SpriteSet, count: number, darken: boolean) => Uint32Array; export const getColorsForSet: GetColorsForSet = (set, count, darken) => { - const t = getColorsGeneric(set.fills, set.outlines, '000000', count, darken); - return toColorList(t); + const t = getColorsGeneric(set.fills, set.outlines, '000000', count, darken); + return toColorList(t); }; const emptyArray: number[] = []; export const getColorsForSetNumber: GetColorsForSet = (set, length, darken) => { - const fills = set.fills || emptyArray; - const outlines = set.outlines || emptyArray; - const result = new Uint32Array(length * 2 + 1); + 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; + 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; - } - } + 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; + 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)); + const extraPalette = pattern && pattern.palettes && pattern.palettes[0]; + return extraPalette && manager.addArray(new Uint32Array(extraPalette)); } export function toPaletteSet( - set: SpriteSet, sets: ColorExtraSets, manager: PaletteManager, getColorsForSet: GetColorsForSet, - hasExtra: boolean, darken: boolean + set: SpriteSet, sets: ColorExtraSets, manager: PaletteManager, getColorsForSet: GetColorsForSet, + 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); + 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, - }; + return { + type: toInt(set.type), + pattern: toInt(set.pattern), + palette: manager.addArray(colors), + extraPalette: hasExtra ? getExtraPalette(pattern, manager) : undefined, + }; } function createCMPalette( - cm: T[] | undefined, manager: PaletteManager, parseColor: (color: T) => number + cm: T[] | undefined, manager: PaletteManager, parseColor: (color: T) => number ): Palette | undefined { - const size = CM_SIZE * CM_SIZE; + const size = CM_SIZE * CM_SIZE; - if (cm === undefined || cm.length === 0 || cm.length > size) - return undefined; + if (cm === undefined || cm.length === 0 || cm.length > size) + return undefined; - const result = new Uint32Array(size); + const result = new Uint32Array(size); - for (let i = 0; i < cm.length; i++) { - result[i] = parseColor(cm[i]); - } + for (let i = 0; i < cm.length; i++) { + result[i] = parseColor(cm[i]); + } - return manager.addArray(result); + return manager.addArray(result); } export type ToSet = (set: SpriteSet | undefined, sets: ColorExtraSets, extra?: boolean) => PaletteSpriteSet | undefined; @@ -458,130 +458,130 @@ export type ToSet = (set: SpriteSet | undefined, sets: ColorExtraSets, ext const defaultPalette = new Uint32Array(sprites.defaultPalette); export const createToPaletteSet = - (manager: PaletteManager, getColorsForSet: GetColorsForSet, extra: boolean, darken: boolean): ToSet => - (set, sets) => set === undefined ? undefined : toPaletteSet(set, sets, manager, getColorsForSet, extra, darken); + (manager: PaletteManager, getColorsForSet: GetColorsForSet, extra: boolean, darken: boolean): ToSet => + (set, sets) => set === undefined ? undefined : toPaletteSet(set, sets, manager, getColorsForSet, extra, darken); export function toPaletteGeneric( - info: PonyInfoGeneric, manager: PaletteManager, toColorList: (color: (T | undefined)[]) => Uint32Array, - getColorsForSet: GetColorsForSet, blackColor: T, whiteColor: T, parseCMColor: (color: T) => number + info: PonyInfoGeneric, manager: PaletteManager, toColorList: (color: (T | undefined)[]) => Uint32Array, + getColorsForSet: GetColorsForSet, 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 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] }; + 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), + 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), + 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), + 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, + 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, + 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), + 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(info.cm, manager, parseCMColor), + cm: undefined, + cmFlip: !!info.cmFlip, + cmPalette: createCMPalette(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), - }; + 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); + return toPaletteGeneric(info, manager, toColorList, getColorsForSet, '000000', 'ffffff', parseCMColor); } export function toPaletteNumber(info: PonyInfoNumber, manager = mockPaletteManager): PalettePonyInfo { - return toPaletteGeneric(info, manager, toColorListNumber, getColorsForSetNumber, BLACK, WHITE, x => x); + return toPaletteGeneric(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; + 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); - } - } - } + 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); + } + } + } } diff --git a/src/ts/common/positionUtils.ts b/src/ts/common/positionUtils.ts index c8028d9..9ecd8fc 100644 --- a/src/ts/common/positionUtils.ts +++ b/src/ts/common/positionUtils.ts @@ -2,69 +2,69 @@ import { tileWidth, tileHeight, tileElevation } from './constants'; import { Point, Rect } from './interfaces'; export function toScreenX(x: number) { - return Math.floor(x * tileWidth) | 0; + return Math.floor(x * tileWidth) | 0; } export function toScreenY(y: number) { - return Math.floor(y * tileHeight) | 0; + return Math.floor(y * tileHeight) | 0; } export function toScreenYWithZ(y: number, z: number) { - return Math.floor(y * tileHeight - z * tileElevation) | 0; + return Math.floor(y * tileHeight - z * tileElevation) | 0; } export function toWorldX(x: number) { - return x / tileWidth; + return x / tileWidth; } export function toWorldY(y: number) { - return y / tileHeight; + return y / tileHeight; } export function toWorldZ(z: number) { - return z / tileElevation; + return z / tileElevation; } export function pointToScreen({ x, y }: Point): Point { - return { - x: toScreenX(x), - y: toScreenY(y), - }; + return { + x: toScreenX(x), + y: toScreenY(y), + }; } export function pointToWorld({ x, y }: Point): Point { - return { - x: toWorldX(x), - y: toWorldY(y), - }; + 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), - }; + return { + x: toScreenX(x), + y: toScreenY(y), + w: toScreenX(w), + h: toScreenY(h), + }; } export function roundPositionX(x: number) { - return Math.floor(x * tileWidth) / tileWidth; + return Math.floor(x * tileWidth) / tileWidth; } export function roundPositionY(y: number) { - return Math.floor(y * tileHeight) / tileHeight; + return Math.floor(y * tileHeight) / tileHeight; } export function roundPositionXMidPixel(x: number) { - return (Math.floor(x * tileWidth) + 0.5) / tileWidth; + return (Math.floor(x * tileWidth) + 0.5) / tileWidth; } export function roundPositionYMidPixel(y: number) { - return (Math.floor(y * tileHeight) + 0.5) / tileHeight; + return (Math.floor(y * tileHeight) + 0.5) / tileHeight; } export function roundPosition(point: Point) { - point.x = roundPositionX(point.x); - point.y = roundPositionY(point.y); + point.x = roundPositionX(point.x); + point.y = roundPositionY(point.y); } diff --git a/src/ts/common/rect.ts b/src/ts/common/rect.ts index c8c4363..a28632e 100644 --- a/src/ts/common/rect.ts +++ b/src/ts/common/rect.ts @@ -2,49 +2,49 @@ 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 }; + return { x, y, w, h }; } export function centerPoint(rect: Rect): Point { - return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 }; + 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; + 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); + 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); + 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); + 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); + 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; + 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); + 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, - }; + 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, + }; } diff --git a/src/ts/common/region.ts b/src/ts/common/region.ts index 6710e3f..0a7bef6 100644 --- a/src/ts/common/region.ts +++ b/src/ts/common/region.ts @@ -9,194 +9,194 @@ 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); + 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); - } + if (!tileData) { + tiles.fill(TileType.Dirt); + } - tileIndices.fill(-1); + tileIndices.fill(-1); - for (let i = 0; i < randoms.length; i++) { - randoms[i] = (Math.random() * 256) | 0; - } + 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, - }; + 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)]; + return region.tiles[x | (y << 3)]; } export function setRegionTile(region: Region, x: number, y: number, type: TileType) { - region.tiles[x | (y << 3)] = type; + region.tiles[x | (y << 3)] = type; } export function getRegionTileIndex(region: Region, x: number, y: number) { - return region.tileIndices[x | (y << 3)]; + return region.tileIndices[x | (y << 3)]; } export function setRegionTileDirty(region: Region, x: number, y: number) { - region.tileIndices[x | (y << 3)] = -1; - region.tilesDirty = true; + 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)]; + return 0; // region.elevation[x | (y << 3)]; } export function setRegionElevation(_region: Region, _x: number, _y: number, _value: number) { - // region.elevation[x | (y << 3)] = value; + // region.elevation[x | (y << 3)] = value; } export function worldToRegionX(x: number, map: IMap) { - return clamp(floor(x / REGION_SIZE), 0, map.regionsX - 1); + return clamp(floor(x / REGION_SIZE), 0, map.regionsX - 1); } export function worldToRegionY(y: number, map: IMap) { - return clamp(floor(y / REGION_SIZE), 0, map.regionsY - 1); + return clamp(floor(y / REGION_SIZE), 0, map.regionsY - 1); } export function invalidateRegionsCollider(region: Region, map: IMap) { - 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 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); + 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; - } - } - } + if (r) { + r.colliderDirty = true; + } + } + } } export function generateRegionCollider(region: Region, map: IMap) { - const regionCollider = region.collider; - const tileTypes = region.tiles; + const regionCollider = region.collider; + const tileTypes = region.tiles; - region.colliderDirty = false; - regionCollider.fill(0); + 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]; + 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; + 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; - } - } - } - } - } + 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 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 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; + 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); + for (let ry = minY; ry <= maxY; ry++) { + for (let rx = minX; rx <= maxX; rx++) { + const r = getRegion(map, rx, ry); - if (r === undefined) - continue; + if (r === undefined) + continue; - for (const entity of r.colliders) { - const entityX = toScreenX(entity.x - baseX) | 0; - const entityY = toScreenY(entity.y - baseY) | 0; + 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; + 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; - } + 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; + 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 (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; + 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; + 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; + 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; + 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; - } - } - } - } - } - } - } - } - } + for (let x = x0 | 0; x < x1; x = (x + 1) | 0) { + regionCollider[(x + oy) | 0] |= value; + } + } + } + } + } + } + } + } + } } diff --git a/src/ts/common/rollbar.ts b/src/ts/common/rollbar.ts index cf9c2c8..6101a0d 100644 --- a/src/ts/common/rollbar.ts +++ b/src/ts/common/rollbar.ts @@ -3,100 +3,100 @@ 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', + // 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 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', + // 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`, + // 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 + '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', + // 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', + // server + 'Range Not Satisfiable', 'Precondition Failed', ].map(escapeRegExp).join('|'), 'i'); export interface Person { - id: string; - username: string; - custom?: any; + 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() : ''; - } + 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); + return IGNORE.test(message); } export function isIgnoredError(error: Error) { - return isIgnoredMessage(error.message || `${error}` || '') || isIgnoredMessage(error.stack || ''); + 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); + return (Array.isArray(args) ? args : [args]) + .map(getLabel) + .some(isIgnoredMessage); } diff --git a/src/ts/common/security.ts b/src/ts/common/security.ts index 58708ae..c4a60a7 100644 --- a/src/ts/common/security.ts +++ b/src/ts/common/security.ts @@ -10,131 +10,131 @@ 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())); + const lines = list && compact(list.split(/\r?\n/).map(x => x.trim())); - if (lines && lines.length) { - const combined = lines.map(escapeRegExp).join('|'); + 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; - } + 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; + 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 (list: string | undefined, value: string) => { + if (cachedList !== list) { + cachedList = list; + cachedRegex = createRegExpFromList(list, wholeWords); + } - return cachedRegex ? cachedRegex.test(value) : false; - }; + 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); + 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; + 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 (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; - } - } + if (testSafe(general.suspiciousSafeMessages, text) || + testWhole(general.suspiciousSafeWholeMessages, text)) { + return Suspicious.Yes; + } + } - return Suspicious.No; - }; + return Suspicious.No; + }; }; export const createIsSuspiciousName = - (settings: GeneralSettings) => { - const test = createCachedTest(); - return (name: string) => test(settings.suspiciousNames, name); - }; + (settings: GeneralSettings) => { + const test = createCachedTest(); + return (name: string) => test(settings.suspiciousNames, name); + }; export const createIsSuspiciousAuth = - (settings: GeneralSettings) => { - const test = createCachedTest(); - return ({ name, emails = [] }: AuthBase) => - test(settings.suspiciousAuths, name) || - emails.some(email => test(settings.suspiciousAuths, email)); - }; + (settings: GeneralSettings) => { + const test = createCachedTest(); + return ({ name, emails = [] }: AuthBase) => + 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; - } + try { + return JSON.parse(value); + } catch { + return undefined; + } } function createMatchesFromList(list: string | undefined): Partial[] { - return compact((list || '').split(/\n/g).map(x => x.trim()).map(tryParseJSON)); + 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)); - }; + (settings: GeneralSettings) => + (info: PonyInfoNumber) => { + const matches = createMatchesFromList(settings.suspiciousPonies); + return matches.some(match => matchPony(info, match)); + }; function matchPony(info: PonyInfoNumber, match: Partial) { - return isMatchWith(info, match, comparePonyInfoFields); + 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; - } + 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: uncomment, to filter offensive messages + // if (/niggers$/.test(_message) || /faggots?/.test(_message)) return true; - // NOTE: add more filters here + // NOTE: add more filters here - return false; + 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 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: 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 + // NOTE: add more filters here - return false; + return false; } diff --git a/src/ts/common/sheets.ts b/src/ts/common/sheets.ts index 0d41a7e..f0106bc 100644 --- a/src/ts/common/sheets.ts +++ b/src/ts/common/sheets.ts @@ -9,874 +9,874 @@ 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; - 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, x: number, y: number, pattern: number) => void; - importMirrored?: { fieldName: string; offsetX: number }; + name: string; + set?: string; + setOverride?: string; + options?: Partial; + 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, x: number, y: number, pattern: number) => void; + importMirrored?: { fieldName: string; offsetX: number }; } export interface Spacer { - spacer: true; + spacer: true; } export interface Sheet { - name: string; - file?: string; - skipImport?: boolean; - alert?: string; - spacer?: boolean; + name: string; + file?: string; + skipImport?: boolean; + alert?: string; + spacer?: boolean; - rows?: number; + rows?: number; - width: number; - height: number; - offset: number; - offsetY?: number; + width: number; + height: number; + offset: number; + offsetY?: number; - padLeft?: number; - padTop?: number; + padLeft?: number; + padTop?: number; - offsets?: Point[]; - importOffsets?: Point[]; - fieldName?: keyof PonyInfo; + 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[]; + 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[]; + frame?: OnFrame; + layers: SheetLayer[]; - masks?: { - name: string, - layerName: string, - mask: string, - reverse?: boolean, - maskFile?: string; - }[]; + 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; + 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; + 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 }, + { 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 }, + { 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 }, + ...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 }, + { 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, + 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)), + 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)), + 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), + width: 60, + height: 60, + offset: 50, + state: stateFromFrames(bodyFrames), }; const chestSheet = { - width: 60, - height: 60, - offset: 60, - state: stateFromFrames(bodyFrames), + width: 60, + height: 60, + offset: 60, + state: stateFromFrames(bodyFrames), }; const waistSheet = { - ...chestSheet, - state: stateFromFrames(waistFrames), + ...chestSheet, + state: stateFromFrames(waistFrames), }; const singleFrameSheet = { - ...bodySheet, - state: stateFromFrames(headFrames), + ...bodySheet, + state: stateFromFrames(headFrames), }; const headSheet = { - width: 60, - height: 75, - offset: 60, - offsetY: 20, - state: stateFromFrames(headFrames), + width: 60, + height: 75, + offset: 60, + offsetY: 20, + state: stateFromFrames(headFrames), }; const bodyLayer: SheetLayer = { - name: '', body: true, head: true, frontLeg: true, backLeg: true, frontFarLeg: true, backFarLeg: true, + name: '', body: true, head: true, frontLeg: true, backLeg: true, frontFarLeg: true, backFarLeg: true, }; const muzzleLayer: SheetLayer = { name: '', setup: pony => pony.nose = defaultSet() }; const frontLegLayer: SheetLayer = { name: '', frontLeg: true, setup: pony => pony.coatFill = SPECIAL_COLOR }; const backLegLayer: SheetLayer = { name: '', 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; - } - }, - ], - }, + // 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; - }, - }, - ], - }, + // 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: '', 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: '', frontLeg: true }, - { name: '', backLeg: true }, - { name: '', 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: 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: '', options: { no: NoDraw.Behind }, frame: (pony, _state, _options, x) => { - pony.wings = x === 16 ? specialSet(1) : ignoreSet(); - }, - }, - ], - importOffsets: offsets.waistAccessoryOffsets, - }, + { + ...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: '', 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: '', frontLeg: true }, + { name: '', backLeg: true }, + { name: '', 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: 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: '', 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: '', 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 layer(s) - }, - { - ...headSheet, - name: 'head - horns', - file: 'horns', - fieldName: 'horn', - single: true, - wrap: 8, - layers: [ - { name: '', 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: '', 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: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } }, - { - name: '', 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: '', setup: pony => pony.horn = specialSet(1) }, - { name: '', 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: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.CloseEar } }, - { name: 'behind', set: 'headAccessoriesBehind' }, - { ...bodyLayer, options: { no: NoDraw.FarEar } }, - { - ...bodyLayer, name: '', 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: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } }, - { name: 'front-2', set: 'faceAccessories2', options: { no: NoDraw.FaceAccessory1 } }, - muzzleLayer, - { name: '', 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) - }, - ], - }, + // 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: '', 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 layer(s) + }, + { + ...headSheet, + name: 'head - horns', + file: 'horns', + fieldName: 'horn', + single: true, + wrap: 8, + layers: [ + { name: '', 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: '', 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: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } }, + { + name: '', 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: '', setup: pony => pony.horn = specialSet(1) }, + { name: '', 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: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.CloseEar } }, + { name: 'behind', set: 'headAccessoriesBehind' }, + { ...bodyLayer, options: { no: NoDraw.FarEar } }, + { + ...bodyLayer, name: '', 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: '', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } }, + { name: 'front-2', set: 'faceAccessories2', options: { no: NoDraw.FaceAccessory1 } }, + muzzleLayer, + { name: '', 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, - }, + { + 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: 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: '', 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: '', 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; - }, - }, - ], - }, + // 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: 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: '', 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: '', 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); + 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[] + 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); + 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], - })), - }; + 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; + return state; } export function ignoreSet(type = 0): SpriteSet { - return createSet(type, BLACK); + return createSet(type, BLACK); } function defaultSet(type = 0): SpriteSet { - return createSet(type, DEFAULT_COLOR); + return createSet(type, DEFAULT_COLOR); } function specialSet(type = 0): SpriteSet { - return createSet(type, SPECIAL_COLOR); + return createSet(type, SPECIAL_COLOR); } function whiteSet(type = 0): SpriteSet { - return createSet(type, WHITE); + return createSet(type, WHITE); } function createSet(type: number, color: number, count = 2): SpriteSet { - return { - type, - fills: times(count, () => color), - lockFills: times(count, () => false), - outlines: times(count, () => color), - lockOutlines: times(count, () => true), - }; + return { + type, + fills: times(count, () => color), + lockFills: times(count, () => false), + outlines: times(count, () => color), + lockOutlines: times(count, () => true), + }; } diff --git a/src/ts/common/stringUtils.ts b/src/ts/common/stringUtils.ts index 76239b7..e49cefc 100644 --- a/src/ts/common/stringUtils.ts +++ b/src/ts/common/stringUtils.ts @@ -3,88 +3,88 @@ const uppercaseCharacters = lowercaseCharacters + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const CARRIAGERETURN = '\r'.charCodeAt(0); export function randomString(length: number, useUpperCase = false): string { - const characters = useUpperCase ? uppercaseCharacters : lowercaseCharacters; - let result = ''; + const characters = useUpperCase ? uppercaseCharacters : lowercaseCharacters; + let result = ''; - for (let i = 0; i < length; i++) { - result += characters[(Math.random() * characters.length) | 0]; - } + for (let i = 0; i < length; i++) { + result += characters[(Math.random() * characters.length) | 0]; + } - return result; + return result; } export function isSurrogate(code: number): boolean { - return code >= 0xd800 && code <= 0xdbff; + return code >= 0xd800 && code <= 0xdbff; } export function isLowSurrogate(code: number): boolean { - return (code & 0xfc00) === 0xdc00; + return (code & 0xfc00) === 0xdc00; } export function fromSurrogate(high: number, low: number): number { - return (((high & 0x3ff) << 10) + (low & 0x3ff) + 0x10000) | 0; + return (((high & 0x3ff) << 10) + (low & 0x3ff) + 0x10000) | 0; } export function charsToCodes(text: string) { - const chars: number[] = []; + const chars: number[] = []; - for (let i = 0; i < text.length; i++) { - let code = text.charCodeAt(i); + 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 (isSurrogate(code) && (i + 1) < text.length) { + const extra = text.charCodeAt(i + 1); - if (isLowSurrogate(extra)) { - code = fromSurrogate(code, extra); - i++; - } - } + if (isLowSurrogate(extra)) { + code = fromSurrogate(code, extra); + i++; + } + } - chars.push(code); - } + chars.push(code); + } - return chars; + return chars; } export function stringToCodes(buffer: Uint32Array, text: string): number { - const textLength = text.length | 0; - let length = 0 | 0; + 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; + 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 (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 (isLowSurrogate(extra)) { + code = fromSurrogate(code, extra) | 0; + i = (i + 1) | 0; + } + } - if (isVisibleChar(code)) { - buffer[length] = code; - length = (length + 1) | 0; - } - } + if (isVisibleChar(code)) { + buffer[length] = code; + length = (length + 1) | 0; + } + } - return length; + return length; } export let codesBuffer = new Uint32Array(32); export function stringToCodesTemp(text: string) { - while (text.length > codesBuffer.length) { - codesBuffer = new Uint32Array(codesBuffer.length * 2); - } + while (text.length > codesBuffer.length) { + codesBuffer = new Uint32Array(codesBuffer.length * 2); + } - return stringToCodes(codesBuffer, text); + return stringToCodes(codesBuffer, text); } export function matcher(regex: RegExp) { - return (text: string): boolean => !!text && regex.test(text); + return (text: string): boolean => !!text && regex.test(text); } export function isVisibleChar(code: number) { - return code !== CARRIAGERETURN && !(code >= 0xfe00 && code <= 0xfe0f); + return code !== CARRIAGERETURN && !(code >= 0xfe00 && code <= 0xfe0f); } diff --git a/src/ts/common/swears.ts b/src/ts/common/swears.ts index 5162e1e..c0b95b9 100644 --- a/src/ts/common/swears.ts +++ b/src/ts/common/swears.ts @@ -7,1469 +7,1469 @@ const wordStartRU = `(?:^|${wordBreakRU})`; const wordEndRU = `(?=$|${wordBreakRU})`; function createBadWords(fast: boolean) { - const emoji = fast ? '*' : '😀-🙏☀-⛿✀-➿🚀-🛶⬀-⯯🌀-🗿'; - const separators1 = `:;!|\`"@#$%^&'*,._=+~\\-`; // \\(\\)\\{\\}\\]\\[ - const separators2 = `${separators1}\\(\\)/\\\\`; - const sep = `[ ${emoji}${separators2}]`; - const sep2 = `[${emoji}${separators2}]`; - const sep3 = `[${emoji}${separators1}]`; - const sep4 = `[${emoji}${separators1}\\(\\)\\{\\}\\]\\[]`; - const sep5 = `[ ${emoji}${separators1}\\(\\)\\{\\}\\]\\[]`; - const sep2OrNum = `[0-9${emoji}${separators2}]`; + const emoji = fast ? '*' : '😀-🙏☀-⛿✀-➿🚀-🛶⬀-⯯🌀-🗿'; + const separators1 = `:;!|\`"@#$%^&'*,._=+~\\-`; // \\(\\)\\{\\}\\]\\[ + const separators2 = `${separators1}\\(\\)/\\\\`; + const sep = `[ ${emoji}${separators2}]`; + const sep2 = `[${emoji}${separators2}]`; + const sep3 = `[${emoji}${separators1}]`; + const sep4 = `[${emoji}${separators1}\\(\\)\\{\\}\\]\\[]`; + const sep5 = `[ ${emoji}${separators1}\\(\\)\\{\\}\\]\\[]`; + const sep2OrNum = `[0-9${emoji}${separators2}]`; - const onlyLetterT = fast ? 't' : 'tţțťтᴛ'; + const onlyLetterT = fast ? 't' : 'tţțťтᴛ'; - const letter = { - a: '[aаáα@4åα]', - b: '[bв🅱]', - c: '[cсćčçḉĉɕċƈȼ¢ς©ᴄ<]', - e: '[eе3єεęėëê€é]', - f: '[fғƒꜰ]', - g: '[gɢ]', - h: '[hн]', - i: '[ıιi!1íĭǐîïḯịȉìỉȋīįᶖɨĩḭᴉᵢ¡İ|🕯ɪ]', - k: '[kĸкḱǩķⱪꝃḳƙḵᶄꝁꝅʞκᴋ]', - n: '[nηɴñń]', - o: '[o○о0σõø🍪🥚°]', - p: '[pр]', - s: '[sş5$š]', - ss: fast ? 'ss' : '(?:ss|ß)', - t: `[${onlyLetterT}^7]`, - u: '[υuúŭǔûṷüǘǚǜǖṳụűȕùủưứựừửữȗūṻųᶙůũṹṵᴜ]', - x: '[xх]', - ь: '[ьЪ]', - }; + const letter = { + a: '[aаáα@4åα]', + b: '[bв🅱]', + c: '[cсćčçḉĉɕċƈȼ¢ς©ᴄ<]', + e: '[eе3єεęėëê€é]', + f: '[fғƒꜰ]', + g: '[gɢ]', + h: '[hн]', + i: '[ıιi!1íĭǐîïḯịȉìỉȋīįᶖɨĩḭᴉᵢ¡İ|🕯ɪ]', + k: '[kĸкḱǩķⱪꝃḳƙḵᶄꝁꝅʞκᴋ]', + n: '[nηɴñń]', + o: '[o○о0σõø🍪🥚°]', + p: '[pр]', + s: '[sş5$š]', + ss: fast ? 'ss' : '(?:ss|ß)', + t: `[${onlyLetterT}^7]`, + u: '[υuúŭǔûṷüǘǚǜǖṳụűȕùủưứựừửữȗūṻųᶙůũṹṵᴜ]', + x: '[xх]', + ь: '[ьЪ]', + }; - if (fast) { - Object.keys(letter).forEach(key => { - (letter as any)[key] = (letter as any)[key].replace(/[^\u0020-\u007e]/g, ''); - }); - } + if (fast) { + Object.keys(letter).forEach(key => { + (letter as any)[key] = (letter as any)[key].replace(/[^\u0020-\u007e]/g, ''); + }); + } - const anyD = '(?:d|\\|\\))'; - const anyH = '(?:h|\\|-\\|)'; - const anyK = '(?:k|\\|<)'; - const anyL = '(?:l|\\|_)'; - const anyN = '(?:n|\\|\\\\\\|)'; - const anyO = '(?:o|0|\\(\\))'; - const anyU = '(?:u|\\|_\\||\\\\/|\\(_\\))'; + const anyD = '(?:d|\\|\\))'; + const anyH = '(?:h|\\|-\\|)'; + const anyK = '(?:k|\\|<)'; + const anyL = '(?:l|\\|_)'; + const anyN = '(?:n|\\|\\\\\\|)'; + const anyO = '(?:o|0|\\(\\))'; + const anyU = '(?:u|\\|_\\||\\\\/|\\(_\\))'; - const wordBreak = fast ? `[^a-zA-Z]` : wordBreakRU; - const wordStart = `(?:^|${wordBreak})`; - const wordEnd = `(?=$|${wordBreak})`; + const wordBreak = fast ? `[^a-zA-Z]` : wordBreakRU; + const wordStart = `(?:^|${wordBreak})`; + const wordEnd = `(?=$|${wordBreak})`; - // sp - const oó = fast ? 'o' : 'oó'; + // sp + const oó = fast ? 'o' : 'oó'; - // pr - const ã = fast ? 'a' : 'aã'; - const á = fast ? 'a' : 'aá'; + // pr + const ã = fast ? 'a' : 'aã'; + const á = fast ? 'a' : 'aá'; - // ro - const ă = fast ? 'a' : 'aă'; - const ț = fast ? 't' : 'tț'; + // ro + const ă = fast ? 'a' : 'aă'; + const ț = fast ? 't' : 'tț'; - // pl - const ą = fast ? 'a' : 'aą'; - const ę = fast ? 'e' : 'eę'; - const ć = fast ? 'c' : 'cć'; - const ń = fast ? 'n' : 'nń'; - const ł = fast ? 'l' : 'lł'; - const ó = fast ? 'u' : 'uó'; - const ś = fast ? 's' : 'sś'; + // pl + const ą = fast ? 'a' : 'aą'; + const ę = fast ? 'e' : 'eę'; + const ć = fast ? 'c' : 'cć'; + const ń = fast ? 'n' : 'nń'; + const ł = fast ? 'l' : 'lł'; + const ó = fast ? 'u' : 'uó'; + const ś = fast ? 's' : 'sś'; - function separeted(letters: string, separator = sep): string { - return letters.split('').map(x => `${(letter as any)[x] || x}+`).join(`${separator}*`); - } + function separeted(letters: string, separator = sep): string { + return letters.split('').map(x => `${(letter as any)[x] || x}+`).join(`${separator}*`); + } - function baseSeparate(letters: string, sep: string, optional?: string) { - return `_*${separeted(letters, sep)}${(optional ? `(?:${separeted(optional, sep)})?` : '')}_*`; - } + function baseSeparate(letters: string, sep: string, optional?: string) { + return `_*${separeted(letters, sep)}${(optional ? `(?:${separeted(optional, sep)})?` : '')}_*`; + } - function separate(letters: string, optional?: string, separator = sep): string { - return baseSeparate(letters, separator, optional); - } + function separate(letters: string, optional?: string, separator = sep): string { + return baseSeparate(letters, separator, optional); + } - function separate2(letters: string, optional?: string): string { - return baseSeparate(letters, sep2, optional); - } + function separate2(letters: string, optional?: string): string { + return baseSeparate(letters, sep2, optional); + } - function alts(letters: string) { - return letters.split('').map(x => `${(letter as any)[x] || x}+`).join(''); - } + function alts(letters: string) { + return letters.split('').map(x => `${(letter as any)[x] || x}+`).join(''); + } - let fuckWords: string[]; + let fuckWords: string[]; - const badWords = [ - // english - 'aborted ?fetus', - 'all?(?:uh|ah ?u?a?|uah) ?a+c?k(?:ba+r*)?', - '(?][-~]*', // penis ascii art - '8==[|]?[oD]', // penis ascii art - '8[ =]{2,}D', // penis ascii art - '(?:^| )[.][i|][.](?: |$)', // penis ascii art - 'c[=-]{3,}3', - '(?:^| )n [i!] g g [e3] s [t^](?: |$)', - `(?][-~]*', // penis ascii art + '8==[|]?[oD]', // penis ascii art + '8[ =]{2,}D', // penis ascii art + '(?:^| )[.][i|][.](?: |$)', // penis ascii art + 'c[=-]{3,}3', + '(?:^| )n [i!] g g [e3] s [t^](?: |$)', + `(? string; @@ -1518,78 +1518,78 @@ const defaultReplacer: Replacer = match => repeat('*', match.length); const createReplacerRU = (replacer = defaultReplacer): Replacer => match => match.replace(regexReplaceRUSingle, replacer); function canUseFast(text: string) { - return isAscii(text); + return isAscii(text); } function slowReplace(text: string, replacer: Replacer, replacerRU: Replacer) { - return text - .replace(regexReplace, replacer) - .replace(regexReplaceRU, replacerRU) - .replace(regexReplaceOther, replacer) - .replace(regexReplaceSpecific, replacer); + return text + .replace(regexReplace, replacer) + .replace(regexReplaceRU, replacerRU) + .replace(regexReplaceOther, replacer) + .replace(regexReplaceSpecific, replacer); } function fastReplace(text: string, replacer: Replacer, replacerRU: Replacer) { - return text - .replace(regexReplaceFast, replacer) - .replace(regexReplaceRUFast, replacerRU) - .replace(regexReplaceOtherFast, replacer) - .replace(regexReplaceSpecificFast, replacer); + return text + .replace(regexReplaceFast, replacer) + .replace(regexReplaceRUFast, replacerRU) + .replace(regexReplaceOtherFast, replacer) + .replace(regexReplaceSpecificFast, replacer); } export function createFilter(replacer = defaultReplacer) { - const replacerRU = createReplacerRU(replacer); + const replacerRU = createReplacerRU(replacer); - return (text: string) => canUseFast(text) ? - fastReplace(text, replacer, replacerRU) : - slowReplace(text, replacer, replacerRU); + return (text: string) => canUseFast(text) ? + fastReplace(text, replacer, replacerRU) : + slowReplace(text, replacer, replacerRU); } function slowTest(text: string) { - return regexTest.test(text) || - regexTestRU.test(text) || - regexTestOther.test(text) || - regexTestSpecific.test(text); + return regexTest.test(text) || + regexTestRU.test(text) || + regexTestOther.test(text) || + regexTestSpecific.test(text); } function fastTest(text: string) { - return regexTestFast.test(text) || - regexTestRUFast.test(text) || - regexTestOtherFast.test(text) || - regexTestSpecificFast.test(text); + return regexTestFast.test(text) || + regexTestRUFast.test(text) || + regexTestOtherFast.test(text) || + regexTestSpecificFast.test(text); } export function hasBadWords(text: string) { - return canUseFast(text) ? fastTest(text) : slowTest(text); + return canUseFast(text) ? fastTest(text) : slowTest(text); } export function hasFuck(text: string) { - return regexTestFuck.test(text); + return regexTestFuck.test(text); } export const filterBadWords = createFilter(); export function filterName(name: string) { - const filtered = filterBadWords(name); - return name === filtered ? name : repeat('*', name.length); + const filtered = filterBadWords(name); + return name === filtered ? name : repeat('*', name.length); } export function filterBadWordsPartial(text: string, replacer = defaultReplacer): string { - return text.replace(regexReplacePartial, replacer); + return text.replace(regexReplacePartial, replacer); } export function findMatch(text: string): string | undefined { - return unicode.all.find(x => (new RegExp(`\\b(?:${x})\\b`, 'ui')).test(text)) - || unicode.foreign.find(x => (new RegExp(`${wordStartRU}(?:${x})${wordEndRU}`, 'ui')).test(text)) - || unicode.other.find(x => (new RegExp(`${x}`, 'u')).test(text)) - || unicode.specific.find(x => (new RegExp(`${x}`, 'u')).test(text)); + return unicode.all.find(x => (new RegExp(`\\b(?:${x})\\b`, 'ui')).test(text)) + || unicode.foreign.find(x => (new RegExp(`${wordStartRU}(?:${x})${wordEndRU}`, 'ui')).test(text)) + || unicode.other.find(x => (new RegExp(`${x}`, 'u')).test(text)) + || unicode.specific.find(x => (new RegExp(`${x}`, 'u')).test(text)); } export function createMatchEntries() { - return [ - ...unicode.all.map(line => ({ line, regex: new RegExp(`\\b(?:${line})\\b`, 'ui') })), - ...unicode.foreign.map(line => ({ line, regex: new RegExp(`${wordStartRU}(?:${line})${wordEndRU}`, 'ui') })), - ...unicode.other.map(line => ({ line, regex: new RegExp(line, 'ui') })), - ...unicode.specific.map(line => ({ line, regex: new RegExp(line, 'u') })), - ]; + return [ + ...unicode.all.map(line => ({ line, regex: new RegExp(`\\b(?:${line})\\b`, 'ui') })), + ...unicode.foreign.map(line => ({ line, regex: new RegExp(`${wordStartRU}(?:${line})${wordEndRU}`, 'ui') })), + ...unicode.other.map(line => ({ line, regex: new RegExp(line, 'ui') })), + ...unicode.specific.map(line => ({ line, regex: new RegExp(line, 'u') })), + ]; } diff --git a/src/ts/common/tags.ts b/src/ts/common/tags.ts index 208627f..27bfbc7 100644 --- a/src/ts/common/tags.ts +++ b/src/ts/common/tags.ts @@ -4,51 +4,51 @@ import { MOD_COLOR, ADMIN_COLOR, PATREON_COLOR, ANNOUNCEMENT_COLOR, WHITE } from 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 }, + '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}`; + 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]); + return Object.keys(tags).map(key => tags[key]); } export function getTag(id: string | undefined): CharacterTag | undefined { - return id ? tags[id] : 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; - } + 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; - } + 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)); + return getAllTags().filter(tag => canUseTag(account, tag.id)); } diff --git a/src/ts/common/timeUtils.ts b/src/ts/common/timeUtils.ts index e93fa76..ae3ea11 100644 --- a/src/ts/common/timeUtils.ts +++ b/src/ts/common/timeUtils.ts @@ -16,22 +16,22 @@ 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; + 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 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)); + return test(getHour(time)); }; export const isDay = isHour(hour => hour > DAY_START && hour <= DAY_END); @@ -49,79 +49,79 @@ export const isNightTime = isHour(hour => hour < (DAY_START - SUN_HALF) || hour // light color export interface LightData { - lightColors: number[]; - shadowColors: number[]; - lightStops: number[]; + lightColors: number[]; + shadowColors: number[]; + lightStops: number[]; } export function createLightData(season: Season): LightData { - const lightDay = WHITE; - const lightNight = season === Season.Winter ? 0x253f76ff : 0x2b3374ff; + const lightDay = WHITE; + const lightNight = season === Season.Winter ? 0x253f76ff : 0x2b3374ff; - const sunrise1 = 0x853d7dff; - const sunrise2 = 0xc96161ff; - const sunrise3 = 0xeeb7a0ff; + const sunrise1 = 0x853d7dff; + const sunrise2 = 0xc96161ff; + const sunrise3 = 0xeeb7a0ff; - const sunset1 = sunrise3; - const sunset2 = sunrise2; - const sunset3 = sunrise1; + 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 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 }, + 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 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 }, + // 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 }, - ]; + // 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); + 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 }; + return { lightColors, shadowColors, lightStops }; } export function getLightColor(data: LightData, time: number): number { - return getColorForTime(time, data.lightStops, data.lightColors, WHITE); + 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); + 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); + 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)); - } - } + 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; + return defaultColor; } diff --git a/src/ts/common/utils.ts b/src/ts/common/utils.ts index 66f0891..723378f 100644 --- a/src/ts/common/utils.ts +++ b/src/ts/common/utils.ts @@ -7,420 +7,420 @@ import { ACCESS_ERROR, NOT_FOUND_ERROR, OFFLINE_ERROR, PROTECTION_ERROR } from ' // enum export function invalidEnum(value: never) { - if (DEVELOPMENT) { - throw new Error(`Invalid enum value: ${value}`); - } + if (DEVELOPMENT) { + throw new Error(`Invalid enum value: ${value}`); + } } export function invalidEnumReturn(value: never, ret: T): T { - if (DEVELOPMENT && !TESTS) { - throw new Error(`Invalid enum value: ${value}`); - } + if (DEVELOPMENT && !TESTS) { + throw new Error(`Invalid enum value: ${value}`); + } - return ret; + return ret; } // date export function fromDate(date: Date, duration: number): Date { - date.setTime(date.getTime() + duration); - return date; + date.setTime(date.getTime() + duration); + return date; } export function fromNow(duration: number): Date { - return fromDate(new Date(), duration); + return fromDate(new Date(), duration); } export function compareDates(a?: Date, b?: Date) { - return a ? (b ? a.getTime() - b.getTime() : 1) : (b ? -1 : 0); + 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; + 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; + 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); + 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`; - } + 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')}`; + 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; + 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); - } + if (match) { + year = parseInt(match[1], 10); + month = parseInt(match[2], 10); + day = parseInt(match[3], 10); + } - return { day, month, year }; + 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); + 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; - } + 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)); + 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; + return value > min ? (value < max ? value : max) : min; } export function lerp(a: number, b: number, t: number) { - return a + t * (b - a); + 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 }; + const d = Math.sqrt(x * x + y * y); + return { x: x / d, y: y / d }; } export function computeCRC(colors: Uint32Array): number { - let crc = 0; + let crc = 0; - for (let i = 0; i < colors.length; i++) { - crc ^= colors[i]; + 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); - } - } + for (let j = 0; j < 8; j++) { + crc = (crc & 1) ? ((crc >>> 1) ^ 0x82f63b78) : (crc >>> 1); + } + } - return crc >>> 0; + return crc >>> 0; } export function computeFriendsCRC(friends: string[]) { - if (!friends.length) { - return 0; - } + if (!friends.length) { + return 0; + } - friends.sort(); - const data = new Uint32Array(friends.length * 3); + 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); - } + 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); + 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]; + 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; + return value | 0; } export function dispose(obj: T | undefined): undefined { - obj && obj.dispose(); - return undefined; + obj && obj.dispose(); + return undefined; } export function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)); + return JSON.parse(JSON.stringify(obj)); } // enums export function hasFlag(value: number | undefined, flag: number): boolean { - return (value! & flag) === flag; + return (value! & flag) === flag; } export function setFlag(value: number | undefined, flag: number, on: boolean): number { - return (value! & ~flag) | (on ? flag : 0); + 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; + return flags + .filter(flag => hasFlag(value, flag.value)) + .map(flag => flag.name).join(' | ') || none; } // collections export function includes(array: T[] | undefined, item: T): boolean { - return array !== undefined && array.indexOf(item) !== -1; + return array !== undefined && array.indexOf(item) !== -1; } export function array(size: number, defaultValue: T) { - const result: T[] = []; + const result: T[] = []; - for (let i = 0; i < size; i++) { - result.push(defaultValue); - } + for (let i = 0; i < size; i++) { + result.push(defaultValue); + } - return result; + return result; } export function repeat(count: number, ...values: T[]): T[] { - const result: T[] = []; + const result: T[] = []; - for (let i = 0; i < count; i++) { - result.push(...values); - } + for (let i = 0; i < count; i++) { + result.push(...values); + } - return result; + return result; } export function times(count: number, action: (index: number) => T) { - const result: T[] = []; + const result: T[] = []; - for (let i = 0; i < count; i++) { - result.push(action(i)); - } + for (let i = 0; i < count; i++) { + result.push(action(i)); + } - return result; + return result; } export function last(array: T[]): T | undefined { - return array.length > 0 ? array[array.length - 1] : undefined; + return array.length > 0 ? array[array.length - 1] : undefined; } export function flatten(arrays: T[][]): T[] { - return ([] as T[]).concat(...arrays); + return ([] as T[]).concat(...arrays); } export function at(items: T[], index: any): T | undefined { - return items[clamp(index | 0, 0, items.length - 1)]; + return items[clamp(index | 0, 0, items.length - 1)]; } export function att(items: T[] | null | undefined, index: any): T | undefined { - return items ? items[clamp(index | 0, 0, items.length - 1)] : undefined; + return items ? items[clamp(index | 0, 0, items.length - 1)] : undefined; } export function findById(items: T[], id: U): T | undefined { - for (let i = 0; i < items.length; i++) { - if (items[i].id === id) { - return items[i]; - } - } + for (let i = 0; i < items.length; i++) { + if (items[i].id === id) { + return items[i]; + } + } - return undefined; + return undefined; } export function findIndexById(items: T[], id: U): number { - for (let i = 0; i < items.length; i++) { - if (items[i].id === id) { - return i; - } - } + for (let i = 0; i < items.length; i++) { + if (items[i].id === id) { + return i; + } + } - return -1; + return -1; } export function removeItem(items: T[], item: T): boolean { - const index = items.indexOf(item); + const index = items.indexOf(item); - if (index !== -1) { - items.splice(index, 1); - return true; - } else { - return false; - } + if (index !== -1) { + items.splice(index, 1); + return true; + } else { + return false; + } } export function removeItemFast(items: T[], item: T): boolean { - const index = items.indexOf(item); + const index = items.indexOf(item); - if (index !== -1) { - items[index] = items[items.length - 1]; - items.pop(); - return true; - } else { - return false; - } + if (index !== -1) { + items[index] = items[items.length - 1]; + items.pop(); + return true; + } else { + return false; + } } export function removeById(items: T[], id: U): T | undefined { - const index = findIndexById(items, id); + const index = findIndexById(items, id); - if (index !== -1) { - const item = items[index]; - items.splice(index, 1); - return item; - } else { - return undefined; - } + if (index !== -1) { + const item = items[index]; + items.splice(index, 1); + return item; + } else { + return undefined; + } } export function arraysEqual(a: T[], b: T[]): boolean { - if (a.length !== b.length) { - return false; - } + if (a.length !== b.length) { + return false; + } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } - return true; + return true; } export function pushUniq(array: T[], item: T) { - const index = array.indexOf(item); + const index = array.indexOf(item); - if (index === -1) { - array.push(item); - return array.length; - } else { - return index + 1; - } + if (index === -1) { + array.push(item); + return array.length; + } else { + return index + 1; + } } export function createPlainMap(values: Dict): Dict { - return Object.keys(values).reduce((obj: Dict, key: string) => (obj[key] = values[key], obj), Object.create(null)); + return Object.keys(values).reduce((obj: Dict, key: string) => (obj[key] = values[key], obj), Object.create(null)); } // rects / points export function point(x: number, y: number): Point { - return { x, y }; + 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; + 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); + 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); + 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; + 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; + 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(), - }; + 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); + 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); + 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; + 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); + 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; + const aBounds = a.bounds; + const bBounds = b.bounds; - if (!aBounds || !bBounds) { - return false; - } + 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; + 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); + 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 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; + 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; + 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 + 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)); + 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 + 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; + return ax <= (bx + bw) && (ax + aw) >= bx && ay <= (by + bh) && (ay + ah) >= by; } // requests @@ -428,92 +428,92 @@ export function intersect( 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); - } + 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(resolve => setTimeout(resolve, timeout)); + return new Promise(resolve => setTimeout(resolve, timeout)); } export function observableToPromise(observable: Observable) { - return observable.toPromise() - .catch(({ status, error }: HttpErrorResponse) => { - const text = error && error.text; + return observable.toPromise() + .catch(({ status, error }: HttpErrorResponse) => { + const text = error && error.text; - try { - error = JSON.parse(error); - } catch { } + try { + error = JSON.parse(error); + } catch { } - const e: RequestError = createError(status || 0, error); - e.status = status; - e.text = text; - throw e; - }); + 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; - } + if (element) { + element.style.transform = transform; + } } function setTransformSafari(element: HTMLElement | undefined, transform: string) { - if (element) { - (element.style as any).webkitTransform = transform; - } + if (element) { + (element.style as any).webkitTransform = transform; + } } export const setTransform = (typeof document !== 'undefined' && 'transform' in document.body.style) ? - setTransformDefault : setTransformSafari; + setTransformDefault : setTransformSafari; export class ObjectCache { - 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); - } - } + 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; - } - } + if (key) { + for (let i = 0; i < data.length; i++) { + data[i] = data[i] ^ key; + } + } - return data; + return data; } export function isCommand(text: string) { - return /^\//.test(text); + 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 }; + 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 @@ -521,21 +521,21 @@ export function processCommand(text: string) { export type AnyEvent = MouseEvent | PointerEvent | TouchEvent; export function isTouch(e: AnyEvent): e is TouchEvent { - return /^touch/i.test(e.type); + return /^touch/i.test(e.type); } export function getButton(e: AnyEvent): number { - return ('button' in e) ? (e.button || 0) : 0; + 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; + 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; + 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((e.target).tagName); + return e.target && /^(input|textarea|select)$/i.test((e.target).tagName); } diff --git a/src/ts/common/worldMap.ts b/src/ts/common/worldMap.ts index 8c70662..244c5f8 100644 --- a/src/ts/common/worldMap.ts +++ b/src/ts/common/worldMap.ts @@ -1,12 +1,12 @@ import { - Entity, Point, TileType, Rect, MapInfo, Camera, Region, IMap, MapState, defaultMapState, Pony, - MapType, EntityFlags, WorldMap, Weather, EntityState, canWalk, MapFlags, + 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 + 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'; @@ -23,719 +23,719 @@ 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, + 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(), - 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, - }; + 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(), + 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); + updateMinMaxRegion(map); - return 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); + 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]; + 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); + 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); + 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); + return editableOrInteractive && (!isHidden(entity) || pickHidden) && pickAny(entity, point); } function pickEntity( - entity: Entity, point: Point, ignorePonies: boolean, pickHidden: boolean, pickEditable: boolean + entity: Entity, point: Point, ignorePonies: boolean, pickHidden: boolean, pickEditable: boolean ): boolean { - return (!ignorePonies || entity.type !== PONY_TYPE) && pick(entity, point, pickHidden, pickEditable); + 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); - } + 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); + 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(); + 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(); + 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(); + return map.entities.filter(e => pickEntityByBounds(e, rect, ignorePonies, pickHidden)).reverse(); } export function removeRegions(map: WorldMap, coords: number[]) { - if (coords.length === 0) - return; + if (coords.length === 0) + return; - const entitiesToRemove = new Set(); + const entitiesToRemove = new Set(); - 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]; + 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); - } - } + 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); - } + 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); + 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 (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})`); + if (oldRegion) { + DEVELOPMENT && !TESTS && console.error(`Region already set (${x}, ${y})`); - for (const e of oldRegion.entities.slice()) { - releaseAndRemoveEntityFromMap(map, e); - } - } + 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})`); - } + 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); + return map.entitiesById.get(id); } export function addEntity(map: WorldMap, entity: Entity) { - const region = getRegionGlobal(map, entity.x, entity.y); + 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); - } + 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); + removeEntityFromMapRegion(map, entity); + releaseAndRemoveEntityFromMap(map, entity); } function releaseAndRemoveEntityFromMap(map: WorldMap, entity: Entity) { - releaseEntity(entity); - removeEntityFromEntities(map, entity); + releaseEntity(entity); + removeEntityFromEntities(map, entity); } export function removeEntityDirectly(map: WorldMap, entity: Entity) { - forEachRegion(map, region => { - const removed = removeEntityFromRegion(region, entity, map); + forEachRegion(map, region => { + const removed = removeEntityFromRegion(region, entity, map); - if (removed) { - releaseEntity(entity); - removeEntityFromEntities(map, entity); - return false; - } else { - return true; - } - }); + 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); + const region = getRegionGlobal(map, worldX, worldY); - if (!region) - return; + if (!region) + return; - const x = Math.floor(worldX - region.x * REGION_SIZE); - const y = Math.floor(worldY - region.y * REGION_SIZE); + 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); + const old = getRegionTile(region, x, y); + setRegionTile(region, x, y, type); - setTilesDirty(map, worldX - 1, worldY - 1, 3, 3); + setTilesDirty(map, worldX - 1, worldY - 1, 3, 3); - if (canWalk(old) !== canWalk(type)) { - setColliderDirty(map, region, x, y); - } + if (canWalk(old) !== canWalk(type)) { + setColliderDirty(map, region, x, y); + } } export function setColliderDirty(map: IMap, region: Region, x: number, y: number) { - region.colliderDirty = true; + 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 (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); - } + 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); + setTile(map, regionX * REGION_SIZE + x, regionY * REGION_SIZE + y, type); } export function setTilesDirty(map: IMap, 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)); - } - } + 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, 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; + 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; + 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)); + 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); + 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; - } - } - } + if (region && callback(region) === false) { + return; + } + } + } } function updateMinMaxRegion(map: WorldMap) { - map.minRegionX = map.regionsX; - map.minRegionY = map.regionsY; - map.maxRegionX = 0; - map.maxRegionY = 0; + 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); - } - } - } + 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); + map.maxRegionX = Math.min(map.maxRegionX, map.regionsX - 1); + map.maxRegionY = Math.min(map.maxRegionY, map.regionsY - 1); } function doRelativeToRegion( - map: IMap, x: number, y: number, action: (region: Region, x: number, y: number) => void + map: IMap, x: number, y: number, action: (region: Region, x: number, y: number) => void ) { - const region = getRegionGlobal(map, x, y); + 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); - } + 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); + region.entities.push(entity); - if (canCollideWith(entity)) { - region.colliders.push(entity); - invalidateRegionsCollider(region, map); - } + if (canCollideWith(entity)) { + region.colliders.push(entity); + invalidateRegionsCollider(region, map); + } } function removeEntityFromRegion(region: Region, entity: Entity, map: WorldMap) { - const removed = removeItemFast(region.entities, entity); + const removed = removeItemFast(region.entities, entity); - if (removed && canCollideWith(entity)) { - removeItemFast(region.colliders, entity); - invalidateRegionsCollider(region, map); - } + if (removed && canCollideWith(entity)) { + removeItemFast(region.colliders, entity); + invalidateRegionsCollider(region, map); + } - return removed; + return removed; } export function addEntityToMapRegion(map: WorldMap, region: Region, entity: Entity) { - if (entity.id !== 0) { - const existing = map.entitiesById.get(entity.id); + 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)})`); + 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); - } + removeEntity(map, existing); + } - map.entitiesById.set(entity.id, entity); - } + map.entitiesById.set(entity.id, entity); + } - if (isPony(entity) && entity.palettePonyInfo === undefined) { - map.poniesToDecode.push(entity); - } + if (isPony(entity) && entity.palettePonyInfo === undefined) { + map.poniesToDecode.push(entity); + } - addEntityToRegion(region, entity, map); + addEntityToRegion(region, entity, map); - map.entities.push(entity); + map.entities.push(entity); - if (isDrawable(entity)) { - map.entitiesDrawable.push(entity); - } + if (isDrawable(entity)) { + map.entitiesDrawable.push(entity); + } - if (isMoving(entity)) { - map.entitiesMoving.push(entity); - } + if (isMoving(entity)) { + map.entitiesMoving.push(entity); + } - if (hasDrawLight(entity)) { - pushUniq(map.entitiesLight, entity); - } + if (hasDrawLight(entity)) { + pushUniq(map.entitiesLight, entity); + } - if (hasLightSprite(entity)) { - pushUniq(map.entitiesLightSprite, entity); - } + if (hasLightSprite(entity)) { + pushUniq(map.entitiesLightSprite, entity); + } - if (entity.triggerBounds !== undefined) { - pushUniq(map.entitiesTriggers, 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); + 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 (isDrawable(entity)) { + removeItem(map.entitiesDrawable, entity); + } - if (isMoving(entity)) { - removeItemFast(map.entitiesMoving, entity); - } + if (isMoving(entity)) { + removeItemFast(map.entitiesMoving, entity); + } - if (isPony(entity)) { - removeItemFast(map.poniesToDecode, entity); - } + if (isPony(entity)) { + removeItemFast(map.poniesToDecode, entity); + } - if (hasDrawLight(entity)) { - removeItemFast(map.entitiesLight, entity); - } + if (hasDrawLight(entity)) { + removeItemFast(map.entitiesLight, entity); + } - if (hasLightSprite(entity)) { - removeItemFast(map.entitiesLightSprite, entity); - } + if (hasLightSprite(entity)) { + removeItemFast(map.entitiesLightSprite, entity); + } - if (entity.triggerBounds !== undefined) { - removeItemFast(map.entitiesTriggers, entity); - } + if (entity.triggerBounds !== undefined) { + removeItemFast(map.entitiesTriggers, entity); + } } function removeEntitiesFromEntities(map: WorldMap, set: Set) { - 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); - } + 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)); + forEachRegion(map, region => !removeEntityFromRegion(region, entity, map)); } export function getTile(map: IMap, x: number, y: number): TileType { - const region = getRegionGlobal(map, x, y) as any as Region; + 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; - } + 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(map: IMap, x: number, y: number): T { - const rx = worldToRegionX(x, map); - const ry = worldToRegionY(y, map); - return getRegion(map, rx, ry); + const rx = worldToRegionX(x, map); + const ry = worldToRegionY(y, map); + return getRegion(map, rx, ry); } export function getRegion(map: IMap, 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]; - } + 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(map: IMap, 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]; - } + 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); - } - } + 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]; + for (let i = map.entitiesWithNames.length - 1; i >= 0; i--) { + const entity = map.entitiesWithNames[i]; - if (!pickAny(entity, hover)) { - map.entitiesWithNames.splice(i, 1); - } - } + if (!pickAny(entity, hover)) { + map.entitiesWithNames.splice(i, 1); + } + } - const regionX = worldToRegionX(hover.x, map); - const regionY = worldToRegionY(hover.y, map); + 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; + 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); + 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); - } - } - } - } - } + 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); + 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); + 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; + 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); - } - } - } + 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); + 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)); - } + if (e.triggerOn !== on) { + if (on) { + game.send(server => server.interact(e.id)); + } - e.triggerOn = on; - } - } + e.triggerOn = on; + } + } } export function updateMap(map: WorldMap, delta: number) { - map.tileTime += delta * WATER_FPS; + map.tileTime += delta * WATER_FPS; - forEachRegion(map, region => { - if (region.tilesDirty) { - updateTileIndices(region, map); - } + forEachRegion(map, region => { + if (region.tilesDirty) { + updateTileIndices(region, map); + } - if (region.colliderDirty) { - generateRegionCollider(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); + return getTileHeight(getTile(map, x, y), getTileIndex(map, x, y), x, y, gameTime, map.type); } export function isInWaterAt(map: IMap, x: number, y: number) { - return getTile(map, x, y) === TileType.Water && isInWater(getTileIndex(map, x, y), x, y); + 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'); + TIMING && timeStart('updateEntities'); - const map = game.map; + const map = game.map; - for (const entity of map.entitiesMoving) { - updatePosition(entity, delta, map); - } + for (const entity of map.entitiesMoving) { + updatePosition(entity, delta, map); + } - for (const entity of map.entities) { - const flags = entity.flags; + 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 ((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); + 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); + 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 (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 ((flags & EntityFlags.OnOff) !== 0) { + const on = (entity.state & EntityState.On) !== 0; - if (entity.lightOn !== undefined) { - entity.lightOn = on; - } + if (entity.lightOn !== undefined) { + entity.lightOn = on; + } - if (entity.lightSpriteOn !== undefined) { - entity.lightSpriteOn = on; - } - } + if (entity.lightSpriteOn !== undefined) { + entity.lightSpriteOn = on; + } + } - if ((flags & EntityFlags.Light) !== 0) { - if (entity.lightOn) { - const move = delta * 0.2; + 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; - } - } - } - } + 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!; + 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) { + says.timer -= delta; - if (says.timer < 0) { - says.timer = 0; - entity.says = undefined; - map.entitiesWithChat.splice(i, 1); - } - } - } + if (says.timer < 0) { + says.timer = 0; + entity.says = undefined; + map.entitiesWithChat.splice(i, 1); + } + } + } - TIMING && timeEnd(); + TIMING && timeEnd(); } export function invalidatePalettes(entities: Entity[]) { - for (const entity of entities) { - if (isPony(entity)) { - invalidatePalettesForPony(entity); - } - } + for (const entity of entities) { + if (isPony(entity)) { + invalidatePalettesForPony(entity); + } + } } export function ensureAllVisiblePoniesAreDecoded(map: WorldMap, camera: Camera, paletteManager: PaletteManager) { - const poniesToDecode = map.poniesToDecode; + const poniesToDecode = map.poniesToDecode; - if (!poniesToDecode.length) - return; + if (!poniesToDecode.length) + return; - const decode = new Set(); + const decode = new Set(); - for (let i = 0; i < poniesToDecode.length; i++) { - const pony = poniesToDecode[i]; + 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 (isBoundsVisible(camera, pony.bounds, pony.x, pony.y)) { + decode.add(i); + } + } - if (!decode.size) - return; + if (!decode.size) + return; - if (decode.size > 100) { - paletteManager.deduplicate = false; - } + 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; - } - }); + 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; - } + if (decode.size > 100) { + paletteManager.deduplicate = true; + } } export function switchEntityRegion(map: WorldMap, entity: Entity, x: number, y: number) { - removeEntityFromMapRegion(map, entity); + removeEntityFromMapRegion(map, entity); - const region = getRegionGlobal(map, x, y); + const region = getRegionGlobal(map, x, y); - if (region) { - addEntityToRegion(region, entity, map); - } else { - releaseAndRemoveEntityFromMap(map, entity); - } + 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; - } - } + 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); + const effects = map.entities.filter(e => e.id === 0 && e.type === weatherRain.type); - for (const entity of effects) { - removeEntityDirectly(map, entity); - } + 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); - } - }); + 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); + } + }); } diff --git a/src/ts/components/admin/admin-account-details/admin-account-details.ts b/src/ts/components/admin/admin-account-details/admin-account-details.ts index 02ff835..13b523f 100644 --- a/src/ts/components/admin/admin-account-details/admin-account-details.ts +++ b/src/ts/components/admin/admin-account-details/admin-account-details.ts @@ -2,16 +2,16 @@ 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 + 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, + 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'; @@ -22,561 +22,561 @@ const defaultDuplicatesLimit = 10; const year = (new Date()).getFullYear(); @Component({ - selector: 'admin-account-details', - templateUrl: 'admin-account-details.pug', - styleUrls: ['admin-account-details.scss'], + 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(); - private duplicatesLimits = new Map(); - private ignoresLimits = new Map(); - private ignoredByLimits = new Map(); - 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(); - }); + 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(); + private duplicatesLimits = new Map(); + private ignoresLimits = new Map(); + private ignoredByLimits = new Map(); + 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.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.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.authsSubscription && this.authsSubscription.unsubscribe(); + this.authsSubscription = undefined; - this.originsSubscription && this.originsSubscription.unsubscribe(); - this.originsSubscription = undefined; + this.originsSubscription && this.originsSubscription.unsubscribe(); + this.originsSubscription = undefined; - this.auths = []; - this.origins = []; - this.counters = []; + this.auths = []; + this.origins = []; + this.counters = []; - this.friends = undefined; - this.hidden = undefined; - this.hiddenBy = undefined; - this.permaHidden = undefined; - this.permaHiddenBy = undefined; + 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; + 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.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.model.getAccount(account._id) + .then(account => this.accountObject = account); - this.refreshDetails(); + this.refreshDetails(); - this.authsSubscription = this.model.accountAuths - .subscribe(account._id, auths => this.auths = auths || []); + 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; + 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'); - } + 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 }; - } + 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 || ''}`).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 || ''} (${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.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 || ''}`).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 || ''} (${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 (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; - } - } + 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'); - } + 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 ''; - } - } - 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'); - } + if (merge.data) { + return `${mergeInfo('ACCOUNT', merge.data.account)}\n\n${mergeInfo('MERGED', merge.data.merge)}`; + } else { + return ''; + } + } + 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)}`; + } + 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'); + 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; + 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; + 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; - } - } + 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; + } + } } diff --git a/src/ts/components/admin/admin-accounts/admin-accounts.ts b/src/ts/components/admin/admin-accounts/admin-accounts.ts index 295f997..0a2d707 100644 --- a/src/ts/components/admin/admin-accounts/admin-accounts.ts +++ b/src/ts/components/admin/admin-accounts/admin-accounts.ts @@ -12,112 +12,112 @@ let currentPage = 0; let not = false; @Component({ - selector: 'admin-accounts', - templateUrl: 'admin-accounts.pug', + 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(); - 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'); + 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(); + 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])); - } - } + if (name) { + this.model.createAccount(name) + .then(id => this.router.navigate(['accounts', id])); + } + } } diff --git a/src/ts/components/admin/admin-events/admin-events.ts b/src/ts/components/admin/admin-events/admin-events.ts index 1aec824..2055c72 100644 --- a/src/ts/components/admin/admin-events/admin-events.ts +++ b/src/ts/components/admin/admin-events/admin-events.ts @@ -5,95 +5,95 @@ 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 + faBell, faSync, faClock, faTrash, faComments, faHdd, faMicrochip, faCertificate, faClone, faPatreon } from '../../../client/icons'; let state: BaseTableState; @Component({ - selector: 'admin-events', - templateUrl: 'admin-events.pug', + selector: 'admin-events', + templateUrl: 'admin-events.pug', }) export class AdminEvents extends BaseTable 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(); - } + 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(); + } } diff --git a/src/ts/components/admin/admin-origin-details/admin-origin-details.ts b/src/ts/components/admin/admin-origin-details/admin-origin-details.ts index adbd2c2..6cff228 100644 --- a/src/ts/components/admin/admin-origin-details/admin-origin-details.ts +++ b/src/ts/components/admin/admin-origin-details/admin-origin-details.ts @@ -4,33 +4,33 @@ import { Event } from '../../../common/adminInterfaces'; import { AdminModel } from '../../services/adminModel'; @Component({ - selector: 'admin-origin-details', - templateUrl: 'admin-origin-details.pug', + 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 = []; + 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); - } - } + 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); + } + } } diff --git a/src/ts/components/admin/admin-origins/admin-origins.ts b/src/ts/components/admin/admin-origins/admin-origins.ts index 504c844..73d2fd3 100644 --- a/src/ts/components/admin/admin-origins/admin-origins.ts +++ b/src/ts/components/admin/admin-origins/admin-origins.ts @@ -4,50 +4,50 @@ import { faSync, faEraser, faClock, faUser, faChevronDown, faSpinner } from '../ import { AdminModel } from '../../services/adminModel'; @Component({ - selector: 'admin-origins', - templateUrl: 'admin-origins.pug', + 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; + 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(); - }); - } + return this.model.clearOrigins(count, andHigher, { old, singles, trim }) + .finally(() => { + this.pending = false; + this.update(); + }); + } } diff --git a/src/ts/components/admin/admin-other/admin-other.ts b/src/ts/components/admin/admin-other/admin-other.ts index 6ce952c..5f3af4c 100644 --- a/src/ts/components/admin/admin-other/admin-other.ts +++ b/src/ts/components/admin/admin-other/admin-other.ts @@ -6,85 +6,85 @@ import { Subscription } from '../../../common/interfaces'; import { showTextInNewTab } from '../../../client/htmlUtils'; interface Field { - key: keyof GeneralSettings; - title: string; - value: string | undefined; + key: keyof GeneralSettings; + title: string; + value: string | undefined; } @Component({ - selector: 'admin-other', - templateUrl: 'admin-other.pug', + 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; + 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(); - } + 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(); + } } diff --git a/src/ts/components/admin/admin-ponies/admin-ponies.ts b/src/ts/components/admin/admin-ponies/admin-ponies.ts index fdcfd25..fbaa291 100644 --- a/src/ts/components/admin/admin-ponies/admin-ponies.ts +++ b/src/ts/components/admin/admin-ponies/admin-ponies.ts @@ -9,89 +9,89 @@ let currentPage = 1; let query: FindPonyQuery = {}; @Component({ - selector: 'admin-ponies', - templateUrl: 'admin-ponies.pug', - styleUrls: ['admin-ponies.scss'], + selector: 'admin-ponies', + templateUrl: 'admin-ponies.pug', + styleUrls: ['admin-ponies.scss'], }) export class AdminPonies implements OnInit { - readonly syncIcon = faSync; - readonly filterIcon = faFilter; - readonly spinnerIcon = faSpinner; - readonly trashIcon = faTrash; - readonly commentIcon = faComment; - items?: string[]; - itemsPerPage = 20; - query: FindPonyQuery = {}; - loading = false; - private lastQuery: FindPonyQuery = {}; - private totalCount?: number; - private execSearch = debounce(() => this.fetchPonies(false), 1000); - constructor(private model: AdminModel) { - } - get totalItems() { - return this.totalCount === undefined ? this.model.counts.characters : this.totalCount; - } - get search() { - return query.search; - } - set search(value: string | undefined) { - if (query.search !== value) { - query.search = value; - this.execSearch(); - } - } - get orderBy() { - return query.orderBy; - } - set orderBy(value: string | undefined) { - if (query.orderBy !== value) { - query.orderBy = value; - this.execSearch(); - } - } - get currentPage() { - return currentPage; - } - set currentPage(value) { - if (currentPage !== value) { - currentPage = value; - this.fetchPonies(isEqual(this.lastQuery, query)); - } - } - ngOnInit() { - if (this.model.connected) { - this.fetchPonies(false); - } - } - refresh() { - this.fetchPonies(false); - } - remove(pony: Character) { - if (confirm('Are you sure?')) { - this.model.removePony(pony._id) - .then(() => delay(500)) - .then(() => this.refresh()); - } - } - private fetchPonies(skipTotalCount: boolean) { - const thisQuery = cloneDeep(query); - this.lastQuery = cloneDeep(query); - this.loading = true; + readonly syncIcon = faSync; + readonly filterIcon = faFilter; + readonly spinnerIcon = faSpinner; + readonly trashIcon = faTrash; + readonly commentIcon = faComment; + items?: string[]; + itemsPerPage = 20; + query: FindPonyQuery = {}; + loading = false; + private lastQuery: FindPonyQuery = {}; + private totalCount?: number; + private execSearch = debounce(() => this.fetchPonies(false), 1000); + constructor(private model: AdminModel) { + } + get totalItems() { + return this.totalCount === undefined ? this.model.counts.characters : this.totalCount; + } + get search() { + return query.search; + } + set search(value: string | undefined) { + if (query.search !== value) { + query.search = value; + this.execSearch(); + } + } + get orderBy() { + return query.orderBy; + } + set orderBy(value: string | undefined) { + if (query.orderBy !== value) { + query.orderBy = value; + this.execSearch(); + } + } + get currentPage() { + return currentPage; + } + set currentPage(value) { + if (currentPage !== value) { + currentPage = value; + this.fetchPonies(isEqual(this.lastQuery, query)); + } + } + ngOnInit() { + if (this.model.connected) { + this.fetchPonies(false); + } + } + refresh() { + this.fetchPonies(false); + } + remove(pony: Character) { + if (confirm('Are you sure?')) { + this.model.removePony(pony._id) + .then(() => delay(500)) + .then(() => this.refresh()); + } + } + private fetchPonies(skipTotalCount: boolean) { + const thisQuery = cloneDeep(query); + this.lastQuery = cloneDeep(query); + this.loading = true; - thisQuery.search = thisQuery.search && thisQuery.search.trim(); + thisQuery.search = thisQuery.search && thisQuery.search.trim(); - this.model.findPonies(thisQuery, this.currentPage - 1, skipTotalCount) - .then(result => { - if (result) { - if (!skipTotalCount) { - this.totalCount = result.totalCount; - } + this.model.findPonies(thisQuery, this.currentPage - 1, skipTotalCount) + .then(result => { + if (result) { + if (!skipTotalCount) { + this.totalCount = result.totalCount; + } - this.items = result.items; - } else { - this.items = []; - } - }) - .finally(() => this.loading = false); - } + this.items = result.items; + } else { + this.items = []; + } + }) + .finally(() => this.loading = false); + } } diff --git a/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts index d06c842..e67eaa0 100644 --- a/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts +++ b/src/ts/components/admin/admin-reports/admin-reports-perf/admin-reports-perf.ts @@ -6,286 +6,286 @@ import { SERVER_FPS } from '../../../../common/constants'; import { AgDragEvent } from '../../../shared/directives/agDrag'; interface Tooltip { - x: number; - y: number; - w: number; - h: number; - text: string; + x: number; + y: number; + w: number; + h: number; + text: string; } interface ListingEntry { - name: string; - count: number; - selfTime: number; - totalTime: number; - selfPercent: number; - totalPercent: number; + name: string; + count: number; + selfTime: number; + totalTime: number; + selfPercent: number; + totalPercent: number; } const frameTime = 1000 / SERVER_FPS; const timePadding = 10; // ms @Component({ - selector: 'admin-reports-perf', - templateUrl: 'admin-reports-perf.pug', + selector: 'admin-reports-perf', + templateUrl: 'admin-reports-perf.pug', }) export class AdminReportsPerf { - @ViewChild('container', { static: true }) container!: ElementRef; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - @ViewChild('tooltip', { static: true }) tooltip!: ElementRef; - loaded = false; - server = ''; - startTime = 0; - endTime = 0; - listing: ListingEntry[] = []; - timings: TimingEntry[] = []; - private tooltips: Tooltip[] = []; - private startTimeFrom = 0; - private endTimeFrom = 0; - private frame: any = 0; - private lastZoom = 0; - constructor(private model: AdminModel) { - if (DEVELOPMENT) { - const interval = setInterval(() => { - if (findById(this.model.state.gameServers, 'dev')) { - clearInterval(interval); - this.load('dev'); - } - }, 100); - } - } - get servers() { - return this.model.state.gameServers.map(s => s.id); - } - async load(server: string) { - this.server = server; - this.timings = []; - this.loaded = false; - const result = await this.model.getTimings(server); + @ViewChild('container', { static: true }) container!: ElementRef; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + @ViewChild('tooltip', { static: true }) tooltip!: ElementRef; + loaded = false; + server = ''; + startTime = 0; + endTime = 0; + listing: ListingEntry[] = []; + timings: TimingEntry[] = []; + private tooltips: Tooltip[] = []; + private startTimeFrom = 0; + private endTimeFrom = 0; + private frame: any = 0; + private lastZoom = 0; + constructor(private model: AdminModel) { + if (DEVELOPMENT) { + const interval = setInterval(() => { + if (findById(this.model.state.gameServers, 'dev')) { + clearInterval(interval); + this.load('dev'); + } + }, 100); + } + } + get servers() { + return this.model.state.gameServers.map(s => s.id); + } + async load(server: string) { + this.server = server; + this.timings = []; + this.loaded = false; + const result = await this.model.getTimings(server); - if (result) { - this.loaded = true; - this.timings = result as TimingEntry[]; - this.setupZoom(); - this.recalcListing(); - } + if (result) { + this.loaded = true; + this.timings = result as TimingEntry[]; + this.setupZoom(); + this.recalcListing(); + } - this.redraw(); - } - mouseMove(e: MouseEvent) { - const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; - const x = e.pageX - rect.left; - const y = e.pageY - rect.top; + this.redraw(); + } + mouseMove(e: MouseEvent) { + const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; + const x = e.pageX - rect.left; + const y = e.pageY - rect.top; - const tooltip = this.tooltips.find(t => pointInRect(x, y, t)); - const element = this.tooltip.nativeElement as HTMLElement; + const tooltip = this.tooltips.find(t => pointInRect(x, y, t)); + const element = this.tooltip.nativeElement as HTMLElement; - if (tooltip) { - element.style.display = 'block'; - element.style.left = `${x + 10}px`; - element.style.top = `${y + 10}px`; - element.innerText = tooltip.text; - } else { - element.style.display = 'none'; - } - } - wheel(e: WheelEvent) { - e.preventDefault(); + if (tooltip) { + element.style.display = 'block'; + element.style.left = `${x + 10}px`; + element.style.top = `${y + 10}px`; + element.innerText = tooltip.text; + } else { + element.style.display = 'none'; + } + } + wheel(e: WheelEvent) { + e.preventDefault(); - const deltaY = clamp(e.deltaY, -1, 1); - const change = ((this.endTime - this.startTime) * 0.2 * deltaY); - const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; - const ratioFromLeft = (e.pageX - rect.left) / rect.width; + const deltaY = clamp(e.deltaY, -1, 1); + const change = ((this.endTime - this.startTime) * 0.2 * deltaY); + const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; + const ratioFromLeft = (e.pageX - rect.left) / rect.width; - this.startTime = this.startTime - change * ratioFromLeft; - this.endTime = this.endTime + change * (1 - ratioFromLeft); + this.startTime = this.startTime - change * ratioFromLeft; + this.endTime = this.endTime + change * (1 - ratioFromLeft); - this.redraw(); - } - setupZoom() { - switch (this.lastZoom) { - case 0: - default: - this.resetZoom(); - break; - case 1: - this.fitZoom(); - break; - case 2: - this.fullFrameZoom(); - break; - } - } - resetZoom() { - const firstTime = this.timings[0].time; - const lastTime = this.timings[this.timings.length - 1].time; - this.startTime = firstTime - timePadding; - this.endTime = lastTime + timePadding; - this.redraw(); - this.lastZoom = 0; - } - fitZoom() { - const firstTime = this.timings[0].time; - const lastTime = this.timings[this.timings.length - 1].time; - const totalTime = lastTime - firstTime; - this.startTime = firstTime - totalTime * 0.05; - this.endTime = lastTime + totalTime * 0.05; - this.redraw(); - this.lastZoom = 1; - } - fullFrameZoom() { - const firstTime = this.timings[0].time; - const lastTime = firstTime + frameTime; - this.startTime = firstTime - 2; - this.endTime = lastTime + 2; - this.redraw(); - this.lastZoom = 2; - } - drag(e: AgDragEvent) { - if (e.type === 'start') { - this.startTimeFrom = this.startTime; - this.endTimeFrom = this.endTime; - } + this.redraw(); + } + setupZoom() { + switch (this.lastZoom) { + case 0: + default: + this.resetZoom(); + break; + case 1: + this.fitZoom(); + break; + case 2: + this.fullFrameZoom(); + break; + } + } + resetZoom() { + const firstTime = this.timings[0].time; + const lastTime = this.timings[this.timings.length - 1].time; + this.startTime = firstTime - timePadding; + this.endTime = lastTime + timePadding; + this.redraw(); + this.lastZoom = 0; + } + fitZoom() { + const firstTime = this.timings[0].time; + const lastTime = this.timings[this.timings.length - 1].time; + const totalTime = lastTime - firstTime; + this.startTime = firstTime - totalTime * 0.05; + this.endTime = lastTime + totalTime * 0.05; + this.redraw(); + this.lastZoom = 1; + } + fullFrameZoom() { + const firstTime = this.timings[0].time; + const lastTime = firstTime + frameTime; + this.startTime = firstTime - 2; + this.endTime = lastTime + 2; + this.redraw(); + this.lastZoom = 2; + } + drag(e: AgDragEvent) { + if (e.type === 'start') { + this.startTimeFrom = this.startTime; + this.endTimeFrom = this.endTime; + } - const scale = (this.endTime - this.startTime) / this.canvas.nativeElement.width; - this.startTime = this.startTimeFrom - e.dx * scale; - this.endTime = this.endTimeFrom - e.dx * scale; - this.redraw(); - } - redraw() { - this.frame = this.frame || requestAnimationFrame(() => this.draw()); - } - draw() { - this.frame = 0; + const scale = (this.endTime - this.startTime) / this.canvas.nativeElement.width; + this.startTime = this.startTimeFrom - e.dx * scale; + this.endTime = this.endTimeFrom - e.dx * scale; + this.redraw(); + } + redraw() { + this.frame = this.frame || requestAnimationFrame(() => this.draw()); + } + draw() { + this.frame = 0; - const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; - const canvas = this.canvas.nativeElement as HTMLCanvasElement; - canvas.width = rect.width; - canvas.height = 400; + const rect = this.container.nativeElement.getBoundingClientRect() as DOMRect; + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + canvas.width = rect.width; + canvas.height = 400; - const context = canvas.getContext('2d')!; - context.fillStyle = '#222'; - context.fillRect(0, 0, canvas.width, canvas.height); + const context = canvas.getContext('2d')!; + context.fillStyle = '#222'; + context.fillRect(0, 0, canvas.width, canvas.height); - this.tooltips.length = 0; + this.tooltips.length = 0; - if (!this.timings.length) - return; + if (!this.timings.length) + return; - const firstTime = this.timings[0].time; - const startTime = this.startTime; - const endTime = this.endTime; - const totalTime = endTime - startTime; - const timeScale = canvas.width / totalTime; + const firstTime = this.timings[0].time; + const startTime = this.startTime; + const endTime = this.endTime; + const totalTime = endTime - startTime; + const timeScale = canvas.width / totalTime; - function timeToX(time: number) { - return (time - startTime) * timeScale; - } + function timeToX(time: number) { + return (time - startTime) * timeScale; + } - function timeToXAligned(time: number) { - return Math.floor(timeToX(time)) + 0.5; - } + function timeToXAligned(time: number) { + return Math.floor(timeToX(time)) + 0.5; + } - context.textBaseline = 'middle'; - context.font = 'Arial 14px normal'; + context.textBaseline = 'middle'; + context.font = 'Arial 14px normal'; - // scale - const scaleHeight = 20; + // scale + const scaleHeight = 20; - context.strokeStyle = '#666'; - context.beginPath(); + context.strokeStyle = '#666'; + context.beginPath(); - for (let time = firstTime; time < endTime; time += frameTime) { - context.moveTo(timeToXAligned(time), 0); - context.lineTo(timeToXAligned(time), canvas.height); - } + for (let time = firstTime; time < endTime; time += frameTime) { + context.moveTo(timeToXAligned(time), 0); + context.lineTo(timeToXAligned(time), canvas.height); + } - context.stroke(); + context.stroke(); - context.strokeStyle = '#ddd'; - context.beginPath(); - context.moveTo(0, scaleHeight + 0.5); - context.lineTo(canvas.width, scaleHeight + 0.5); - context.stroke(); + context.strokeStyle = '#ddd'; + context.beginPath(); + context.moveTo(0, scaleHeight + 0.5); + context.lineTo(canvas.width, scaleHeight + 0.5); + context.stroke(); - // entries - const rowHeight = 20; - const startStack: TimingEntry[] = []; + // entries + const rowHeight = 20; + const startStack: TimingEntry[] = []; - for (const entry of this.timings) { - if (entry.type === TimingEntryType.Start) { - startStack.push(entry); - } else { - const start = startStack.pop()!; - const name = start.name!; - const startX = timeToX(start.time); - const endX = timeToX(entry.time); - const y = scaleHeight + 2 + startStack.length * rowHeight; - const w = endX - startX; - let text = name; - const time = entry.time - start.time; + for (const entry of this.timings) { + if (entry.type === TimingEntryType.Start) { + startStack.push(entry); + } else { + const start = startStack.pop()!; + const name = start.name!; + const startX = timeToX(start.time); + const endX = timeToX(entry.time); + const y = scaleHeight + 2 + startStack.length * rowHeight; + const w = endX - startX; + let text = name; + const time = entry.time - start.time; - if (startStack.length === 0) { - context.fillStyle = '#efc457'; - text = `${text} (${time.toFixed(2)} ms) ${(100 * time / frameTime).toFixed(0)}%`; - } else if (/\(\)$/.test(name)) { - context.fillStyle = '#d4ecc6'; - } else { - context.fillStyle = '#c6dcec'; - } + if (startStack.length === 0) { + context.fillStyle = '#efc457'; + text = `${text} (${time.toFixed(2)} ms) ${(100 * time / frameTime).toFixed(0)}%`; + } else if (/\(\)$/.test(name)) { + context.fillStyle = '#d4ecc6'; + } else { + context.fillStyle = '#c6dcec'; + } - context.fillRect(startX, y, w, rowHeight - 1); - this.tooltips.push({ x: startX, y, w, h: rowHeight, text: `${name}\n${time.toFixed(2)} ms` }); + context.fillRect(startX, y, w, rowHeight - 1); + this.tooltips.push({ x: startX, y, w, h: rowHeight, text: `${name}\n${time.toFixed(2)} ms` }); - if (w > 4) { - context.fillStyle = '#222'; - context.fillText(text, startX + 4, y + rowHeight / 2); - } - } - } - } - recalcListing() { - interface Entry extends TimingEntry { - excludedTime: number; - } + if (w > 4) { + context.fillStyle = '#222'; + context.fillText(text, startX + 4, y + rowHeight / 2); + } + } + } + } + recalcListing() { + interface Entry extends TimingEntry { + excludedTime: number; + } - this.listing = []; - const startStack: Entry[] = []; + this.listing = []; + const startStack: Entry[] = []; - for (const entry of this.timings) { - if (entry.type === TimingEntryType.Start) { - startStack.push({ ...entry, excludedTime: 0 }); - } else { - const start = startStack.pop()!; - const name = start.name!; - const time = entry.time - start.time; + for (const entry of this.timings) { + if (entry.type === TimingEntryType.Start) { + startStack.push({ ...entry, excludedTime: 0 }); + } else { + const start = startStack.pop()!; + const name = start.name!; + const time = entry.time - start.time; - let listing = this.listing.find(l => l.name === name); + let listing = this.listing.find(l => l.name === name); - if (!listing) { - listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 }; - this.listing.push(listing); - } + if (!listing) { + listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 }; + this.listing.push(listing); + } - listing.count++; - listing.selfTime += (time - start.excludedTime); - listing.totalTime += time; + listing.count++; + listing.selfTime += (time - start.excludedTime); + listing.totalTime += time; - if (startStack.length) { - startStack[startStack.length - 1].excludedTime += time; - } - } - } + if (startStack.length) { + startStack[startStack.length - 1].excludedTime += time; + } + } + } - const firstTime = this.timings[0].time; - const lastTime = this.timings[this.timings.length - 1].time; - const totalTime = lastTime - firstTime; + const firstTime = this.timings[0].time; + const lastTime = this.timings[this.timings.length - 1].time; + const totalTime = lastTime - firstTime; - for (const listing of this.listing) { - listing.selfPercent = 100 * listing.selfTime / totalTime; - listing.totalPercent = 100 * listing.totalTime / totalTime; - } + for (const listing of this.listing) { + listing.selfPercent = 100 * listing.selfTime / totalTime; + listing.totalPercent = 100 * listing.totalTime / totalTime; + } - this.listing.sort((a, b) => b.selfTime - a.selfTime); - } + this.listing.sort((a, b) => b.selfTime - a.selfTime); + } } diff --git a/src/ts/components/admin/admin-reports/admin-reports.ts b/src/ts/components/admin/admin-reports/admin-reports.ts index 4b574db..89a02b9 100644 --- a/src/ts/components/admin/admin-reports/admin-reports.ts +++ b/src/ts/components/admin/admin-reports/admin-reports.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core'; @Component({ - selector: 'admin-reports', - templateUrl: 'admin-reports.pug', + selector: 'admin-reports', + templateUrl: 'admin-reports.pug', }) export class AdminReports { } diff --git a/src/ts/components/admin/admin-sign-in/admin-sign-in.ts b/src/ts/components/admin/admin-sign-in/admin-sign-in.ts index af10d93..b01878b 100644 --- a/src/ts/components/admin/admin-sign-in/admin-sign-in.ts +++ b/src/ts/components/admin/admin-sign-in/admin-sign-in.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core'; @Component({ - selector: 'admin-sign-in', - templateUrl: 'admin-sign-in.pug', + selector: 'admin-sign-in', + templateUrl: 'admin-sign-in.pug', }) export class AdminSignIn { } diff --git a/src/ts/components/admin/admin-state/admin-state.ts b/src/ts/components/admin/admin-state/admin-state.ts index f14b01e..63b4f9b 100644 --- a/src/ts/components/admin/admin-state/admin-state.ts +++ b/src/ts/components/admin/admin-state/admin-state.ts @@ -1,111 +1,111 @@ import { Component } from '@angular/core'; import { - GameServerState, SERVER_SETTINGS, LOGIN_SERVER_SETTINGS, ServerStats, RequestStats, - UserCountStats, Stats, StatsTable + GameServerState, SERVER_SETTINGS, LOGIN_SERVER_SETTINGS, ServerStats, RequestStats, + UserCountStats, Stats, StatsTable } from '../../../common/adminInterfaces'; import { hasRole } from '../../../common/accountUtils'; import { AdminModel } from '../../services/adminModel'; import { faCog, faSlidersH } from '../../../client/icons'; @Component({ - selector: 'admin-state', - templateUrl: 'admin-state.pug', - styleUrls: ['admin-state.scss'], + selector: 'admin-state', + templateUrl: 'admin-state.pug', + styleUrls: ['admin-state.scss'], }) export class AdminState { - readonly cogIcon = faCog; - readonly optionsIcon = faSlidersH; - readonly loginOptions = LOGIN_SERVER_SETTINGS; - readonly options = SERVER_SETTINGS; - private stats = new Map(); - private statsTables = new Map(); - requestStats?: RequestStats[]; - userCounts?: UserCountStats[]; - showSettings: any = {}; - constructor(private model: AdminModel) { - } - get state() { - return this.model.state; - } - get loginServers() { - return this.model.state.loginServers; - } - get servers() { - return this.model.state.gameServers; - } - get isSuperadmin() { - return hasRole(this.model.account, 'superadmin'); - } - kickAll(server: GameServerState) { - return this.model.kickAll(server.id); - } - updateLoginSetting(key: string, value: boolean) { - return this.model.updateSettings({ [key]: value }); - } - updateLoginSettings(state: any) { - return this.model.updateSettings(state); - } - updateServerSetting(server: GameServerState, key: string, value: boolean) { - return this.model.updateGameServerSettings(server.id, { [key]: value }); - } - updateServerSettings(server: GameServerState, state: any) { - return this.model.updateGameServerSettings(server.id, state); - } - // request stats - fetchRequestStats() { - this.model.getRequestStats() - .then(stats => { - this.requestStats = stats && stats.requests; - this.userCounts = stats && stats.userCounts; - }); - } - resetRequestStats() { - this.requestStats = undefined; - this.userCounts = undefined; - } - // socket stats - fetchStats(server: GameServerState) { - return this.model.fetchServerStats(server.id) - .then(stats => stats && this.stats.set(server.id, stats)); - } - resetStats(server: GameServerState) { - this.stats.delete(server.id); - } - getStats(server: GameServerState) { - return this.stats.get(server.id); - } - // stats tables - fetchCountryStats(server: GameServerState) { - return this.fetchStatsTable(server, Stats.Country); - } - fetchSupportStats(server: GameServerState) { - return this.fetchStatsTable(server, Stats.Support); - } - fetchMapStats(server: GameServerState) { - return this.fetchStatsTable(server, Stats.Maps); - } - resetStatsTable(server: GameServerState) { - this.statsTables.delete(server.id); - } - getStatsTable(server: GameServerState) { - return this.statsTables.get(server.id); - } - private fetchStatsTable(server: GameServerState, stats: Stats) { - return this.model.fetchServerStatsTable(server.id, stats) - .then(stats => stats && this.statsTables.set(server.id, stats)); - } - // updates - notifyOfUpdate(server = '*') { - if (confirm('Are you sure?')) { - this.model.notifyUpdate(server); - } - } - shutdownServers(server = '*') { - if (confirm('Are you sure?')) { - this.model.shutdownServers(server); - } - } - resetUpdating(server = '*') { - this.model.resetUpdating(server); - } + readonly cogIcon = faCog; + readonly optionsIcon = faSlidersH; + readonly loginOptions = LOGIN_SERVER_SETTINGS; + readonly options = SERVER_SETTINGS; + private stats = new Map(); + private statsTables = new Map(); + requestStats?: RequestStats[]; + userCounts?: UserCountStats[]; + showSettings: any = {}; + constructor(private model: AdminModel) { + } + get state() { + return this.model.state; + } + get loginServers() { + return this.model.state.loginServers; + } + get servers() { + return this.model.state.gameServers; + } + get isSuperadmin() { + return hasRole(this.model.account, 'superadmin'); + } + kickAll(server: GameServerState) { + return this.model.kickAll(server.id); + } + updateLoginSetting(key: string, value: boolean) { + return this.model.updateSettings({ [key]: value }); + } + updateLoginSettings(state: any) { + return this.model.updateSettings(state); + } + updateServerSetting(server: GameServerState, key: string, value: boolean) { + return this.model.updateGameServerSettings(server.id, { [key]: value }); + } + updateServerSettings(server: GameServerState, state: any) { + return this.model.updateGameServerSettings(server.id, state); + } + // request stats + fetchRequestStats() { + this.model.getRequestStats() + .then(stats => { + this.requestStats = stats && stats.requests; + this.userCounts = stats && stats.userCounts; + }); + } + resetRequestStats() { + this.requestStats = undefined; + this.userCounts = undefined; + } + // socket stats + fetchStats(server: GameServerState) { + return this.model.fetchServerStats(server.id) + .then(stats => stats && this.stats.set(server.id, stats)); + } + resetStats(server: GameServerState) { + this.stats.delete(server.id); + } + getStats(server: GameServerState) { + return this.stats.get(server.id); + } + // stats tables + fetchCountryStats(server: GameServerState) { + return this.fetchStatsTable(server, Stats.Country); + } + fetchSupportStats(server: GameServerState) { + return this.fetchStatsTable(server, Stats.Support); + } + fetchMapStats(server: GameServerState) { + return this.fetchStatsTable(server, Stats.Maps); + } + resetStatsTable(server: GameServerState) { + this.statsTables.delete(server.id); + } + getStatsTable(server: GameServerState) { + return this.statsTables.get(server.id); + } + private fetchStatsTable(server: GameServerState, stats: Stats) { + return this.model.fetchServerStatsTable(server.id, stats) + .then(stats => stats && this.statsTables.set(server.id, stats)); + } + // updates + notifyOfUpdate(server = '*') { + if (confirm('Are you sure?')) { + this.model.notifyUpdate(server); + } + } + shutdownServers(server = '*') { + if (confirm('Are you sure?')) { + this.model.shutdownServers(server); + } + } + resetUpdating(server = '*') { + this.model.resetUpdating(server); + } } diff --git a/src/ts/components/admin/admin.module.ts b/src/ts/components/admin/admin.module.ts index 074ecb2..59de1e8 100644 --- a/src/ts/components/admin/admin.module.ts +++ b/src/ts/components/admin/admin.module.ts @@ -56,84 +56,84 @@ import { AdminApp } from './admin'; import { ErrorReporter } from '../services/errorReporter'; export const routes: Routes = [ - { path: '', component: AdminState }, - { path: 'sign-in', component: AdminSignIn }, - { path: 'events', component: AdminEvents }, - { path: 'accounts', component: AdminAccounts }, - { path: 'accounts/:id', component: AdminAccountDetails }, - { path: 'ponies', component: AdminPonies }, - { path: 'origins', component: AdminOrigins }, - { path: 'origins/:ip/:country', component: AdminOriginDetails }, - { - path: 'reports', - component: AdminReports, - children: [ - { path: '', redirectTo: 'perf', pathMatch: 'full' }, - { path: 'perf', component: AdminReportsPerf }, - ], - }, - { path: 'other', component: AdminOther }, + { path: '', component: AdminState }, + { path: 'sign-in', component: AdminSignIn }, + { path: 'events', component: AdminEvents }, + { path: 'accounts', component: AdminAccounts }, + { path: 'accounts/:id', component: AdminAccountDetails }, + { path: 'ponies', component: AdminPonies }, + { path: 'origins', component: AdminOrigins }, + { path: 'origins/:ip/:country', component: AdminOriginDetails }, + { + path: 'reports', + component: AdminReports, + children: [ + { path: '', redirectTo: 'perf', pathMatch: 'full' }, + { path: 'perf', component: AdminReportsPerf }, + ], + }, + { path: 'other', component: AdminOther }, ]; @NgModule({ - imports: [ - BrowserModule, - RouterModule, - FormsModule, - HttpClientModule, - PopoverModule.forRoot(), - PaginationModule.forRoot(), - ButtonsModule.forRoot(), - ModalModule, - TooltipModule, - SharedModule, - RouterModule.forRoot(routes), - FontAwesomeModule, - ], - declarations: [ - KeysPipe, - OrderByPipe, - TranslitPipe, - EventsTable, - AccountInfo, - AccountInfoRemote, - AccountStatus, - AccountTooltip, - OriginInfo, - OriginInfoRemote, - OriginListRemote, - PonyInfo, - PonyInfoRemote, - PonyListRemote, - AuthInfo, - AuthInfoRemote, - AuthInfoEdit, - AuthList, - AuthListRemote, - OnOffSwitch, - AdminChatLog, - BanIcon, - EmailList, - FromNow, - TimeField, - UAInfo, - AdminState, - AdminEvents, - AdminOther, - AdminAccounts, - AdminAccountDetails, - AdminPonies, - AdminReports, - AdminReportsPerf, - AdminOrigins, - AdminOriginDetails, - AdminSignIn, - AdminApp, - ], - providers: [ - ErrorReporter, - ], - bootstrap: [AdminApp], + imports: [ + BrowserModule, + RouterModule, + FormsModule, + HttpClientModule, + PopoverModule.forRoot(), + PaginationModule.forRoot(), + ButtonsModule.forRoot(), + ModalModule, + TooltipModule, + SharedModule, + RouterModule.forRoot(routes), + FontAwesomeModule, + ], + declarations: [ + KeysPipe, + OrderByPipe, + TranslitPipe, + EventsTable, + AccountInfo, + AccountInfoRemote, + AccountStatus, + AccountTooltip, + OriginInfo, + OriginInfoRemote, + OriginListRemote, + PonyInfo, + PonyInfoRemote, + PonyListRemote, + AuthInfo, + AuthInfoRemote, + AuthInfoEdit, + AuthList, + AuthListRemote, + OnOffSwitch, + AdminChatLog, + BanIcon, + EmailList, + FromNow, + TimeField, + UAInfo, + AdminState, + AdminEvents, + AdminOther, + AdminAccounts, + AdminAccountDetails, + AdminPonies, + AdminReports, + AdminReportsPerf, + AdminOrigins, + AdminOriginDetails, + AdminSignIn, + AdminApp, + ], + providers: [ + ErrorReporter, + ], + bootstrap: [AdminApp], }) export class AdminAppModule { } diff --git a/src/ts/components/admin/admin.ts b/src/ts/components/admin/admin.ts index c7de660..821c623 100644 --- a/src/ts/components/admin/admin.ts +++ b/src/ts/components/admin/admin.ts @@ -5,71 +5,71 @@ import { PopoverConfig } from 'ngx-bootstrap/popover'; import { hasRole } from '../../common/accountUtils'; import { AdminModel } from '../services/adminModel'; import { - faSpinner, faSlidersH, faExclamationCircle, faUsers, faHorseHead, faMapMarkerAlt, faChartPie, faCog + faSpinner, faSlidersH, faExclamationCircle, faUsers, faHorseHead, faMapMarkerAlt, faChartPie, faCog } from '../../client/icons'; export function tooltipConfig() { - return Object.assign(new TooltipConfig(), { container: 'body' }); + return Object.assign(new TooltipConfig(), { container: 'body' }); } export function popoverConfig() { - return Object.assign(new PopoverConfig(), { container: 'body' }); + return Object.assign(new PopoverConfig(), { container: 'body' }); } @Component({ - selector: 'pony-town-app', - templateUrl: 'admin.pug', - styleUrls: ['admin.scss'], - providers: [ - { provide: TooltipConfig, useFactory: tooltipConfig }, - { provide: PopoverConfig, useFactory: popoverConfig }, - ] + selector: 'pony-town-app', + templateUrl: 'admin.pug', + styleUrls: ['admin.scss'], + providers: [ + { provide: TooltipConfig, useFactory: tooltipConfig }, + { provide: PopoverConfig, useFactory: popoverConfig }, + ] }) export class AdminApp { - readonly spinnerIcon = faSpinner; - readonly stateIcon = faSlidersH; - readonly eventsIcon = faExclamationCircle; - readonly accountsIcon = faUsers; - readonly poniesIcon = faHorseHead; - readonly originsIcon = faMapMarkerAlt; - readonly reportsIcon = faChartPie; - readonly otherIcon = faCog; - constructor(public model: AdminModel, private router: Router) { - } - get loading() { - if (!this.model.initialized) { - return 'Initializing'; - } else if (!this.model.connected) { - return 'Connecting'; - } else if (!this.model.loaded) { - return 'Loading'; - } else { - return ''; - } - } - get clients() { - return this.model.state.gameServers.reduce((sum, s) => sum + s.online, 0); - } - get events() { - return this.model.events.length; - } - get accounts() { - return this.model.counts.accounts; - } - get ponies() { - return this.model.counts.characters; - } - get origins() { - return this.model.counts.origins; - } - get isSuperadmin() { - return hasRole(this.model.account, 'superadmin'); - } - @HostListener('window:go-to-account', ['$event']) - goToAccount({ detail }: CustomEvent) { - this.router.navigate(['/accounts', detail]); - } - signOut() { - window.location.href = '/'; - } + readonly spinnerIcon = faSpinner; + readonly stateIcon = faSlidersH; + readonly eventsIcon = faExclamationCircle; + readonly accountsIcon = faUsers; + readonly poniesIcon = faHorseHead; + readonly originsIcon = faMapMarkerAlt; + readonly reportsIcon = faChartPie; + readonly otherIcon = faCog; + constructor(public model: AdminModel, private router: Router) { + } + get loading() { + if (!this.model.initialized) { + return 'Initializing'; + } else if (!this.model.connected) { + return 'Connecting'; + } else if (!this.model.loaded) { + return 'Loading'; + } else { + return ''; + } + } + get clients() { + return this.model.state.gameServers.reduce((sum, s) => sum + s.online, 0); + } + get events() { + return this.model.events.length; + } + get accounts() { + return this.model.counts.accounts; + } + get ponies() { + return this.model.counts.characters; + } + get origins() { + return this.model.counts.origins; + } + get isSuperadmin() { + return hasRole(this.model.account, 'superadmin'); + } + @HostListener('window:go-to-account', ['$event']) + goToAccount({ detail }: CustomEvent) { + this.router.navigate(['/accounts', detail]); + } + signOut() { + window.location.href = '/'; + } } diff --git a/src/ts/components/admin/base-table.ts b/src/ts/components/admin/base-table.ts index 9930238..dbdc87a 100644 --- a/src/ts/components/admin/base-table.ts +++ b/src/ts/components/admin/base-table.ts @@ -1,100 +1,100 @@ import { debounce } from 'lodash'; export interface BaseTableState { - search: string; - currentPage: number; - sortedBy: string; - sortedAsc: boolean; + search: string; + currentPage: number; + sortedBy: string; + sortedAsc: boolean; } export abstract class BaseTable { - itemsPerPage = 20; - sorted: T[] = []; - filtered: T[] = []; - filteredOnPage: T[] = []; - sortedBy = 'createdAt'; - sortedAsc = true; - private _search = ''; - private _currentPage = 1; - private execSearch = debounce(() => { - this.updateFiltered(); - this.onChange(); - }, 500); - get itemsFrom() { - return (this.currentPage - 1) * this.itemsPerPage; - } - get items(): T[] { - return []; - } - get search() { - return this._search; - } - set search(value: string) { - if (this._search !== value) { - this._search = value; - this.execSearch(); - } - } - get currentPage() { - return this._currentPage; - } - set currentPage(value: number) { - if (this._currentPage !== value) { - this._currentPage = value; - this.updatePage(); - this.onChange(); - } - } - sortBy(field: string) { - if (this.sortedBy === field) { - this.sortedAsc = !this.sortedAsc; - } else { - this.sortedBy = field; - this.sortedAsc = true; - } + itemsPerPage = 20; + sorted: T[] = []; + filtered: T[] = []; + filteredOnPage: T[] = []; + sortedBy = 'createdAt'; + sortedAsc = true; + private _search = ''; + private _currentPage = 1; + private execSearch = debounce(() => { + this.updateFiltered(); + this.onChange(); + }, 500); + get itemsFrom() { + return (this.currentPage - 1) * this.itemsPerPage; + } + get items(): T[] { + return []; + } + get search() { + return this._search; + } + set search(value: string) { + if (this._search !== value) { + this._search = value; + this.execSearch(); + } + } + get currentPage() { + return this._currentPage; + } + set currentPage(value: number) { + if (this._currentPage !== value) { + this._currentPage = value; + this.updatePage(); + this.onChange(); + } + } + sortBy(field: string) { + if (this.sortedBy === field) { + this.sortedAsc = !this.sortedAsc; + } else { + this.sortedBy = field; + this.sortedAsc = true; + } - this.updateSorted(); - this.onChange(); - } - sortedClass(field: string) { - return this.sortedBy === field ? (this.sortedAsc ? 'sorted-asc' : 'sorted-desc') : undefined; - } - protected updateItems() { - this.updateSorted(); - } - protected updateSorted() { - this.sorted = this.sortItems(this.items, this.sortedBy, this.sortedAsc); - this.updateFiltered(); - } - protected updateFiltered() { - this.filtered = this.filterItems(this.sorted, this.search); - this.updatePage(); - } - protected updatePage() { - this.filteredOnPage = this.filtered.slice(this.itemsFrom, this.itemsFrom + this.itemsPerPage); - } - protected sortItems(items: T[], _by: string, _asc: boolean) { - return items; - } - protected filterItems(items: T[], _search: string) { - return items; - } - protected onChange() { - } - protected getState(): BaseTableState { - return { - search: this.search, - currentPage: this.currentPage, - sortedBy: this.sortedBy, - sortedAsc: this.sortedAsc, - }; - } - protected setState(state: BaseTableState | undefined) { - if (state) { - this._search = state.search; - this._currentPage = state.currentPage || 1; - this.sortedBy = state.sortedBy; - this.sortedAsc = state.sortedAsc; - } - } + this.updateSorted(); + this.onChange(); + } + sortedClass(field: string) { + return this.sortedBy === field ? (this.sortedAsc ? 'sorted-asc' : 'sorted-desc') : undefined; + } + protected updateItems() { + this.updateSorted(); + } + protected updateSorted() { + this.sorted = this.sortItems(this.items, this.sortedBy, this.sortedAsc); + this.updateFiltered(); + } + protected updateFiltered() { + this.filtered = this.filterItems(this.sorted, this.search); + this.updatePage(); + } + protected updatePage() { + this.filteredOnPage = this.filtered.slice(this.itemsFrom, this.itemsFrom + this.itemsPerPage); + } + protected sortItems(items: T[], _by: string, _asc: boolean) { + return items; + } + protected filterItems(items: T[], _search: string) { + return items; + } + protected onChange() { + } + protected getState(): BaseTableState { + return { + search: this.search, + currentPage: this.currentPage, + sortedBy: this.sortedBy, + sortedAsc: this.sortedAsc, + }; + } + protected setState(state: BaseTableState | undefined) { + if (state) { + this._search = state.search; + this._currentPage = state.currentPage || 1; + this.sortedBy = state.sortedBy; + this.sortedAsc = state.sortedAsc; + } + } } diff --git a/src/ts/components/admin/pipes/keysPipe.ts b/src/ts/components/admin/pipes/keysPipe.ts index 44af0d1..80c758b 100644 --- a/src/ts/components/admin/pipes/keysPipe.ts +++ b/src/ts/components/admin/pipes/keysPipe.ts @@ -1,10 +1,10 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ - name: 'keys', + name: 'keys', }) export class KeysPipe implements PipeTransform { - transform(value: any) { - return value ? Object.keys(value) : value; - } + transform(value: any) { + return value ? Object.keys(value) : value; + } } diff --git a/src/ts/components/admin/pipes/orderByPipe.ts b/src/ts/components/admin/pipes/orderByPipe.ts index ad03bd9..66d7a4e 100644 --- a/src/ts/components/admin/pipes/orderByPipe.ts +++ b/src/ts/components/admin/pipes/orderByPipe.ts @@ -1,10 +1,10 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ - name: 'orderBy', + name: 'orderBy', }) export class OrderByPipe implements PipeTransform { - transform(value: any[] | undefined, compare?: any) { - return value && value.slice().sort(compare); - } + transform(value: any[] | undefined, compare?: any) { + return value && value.slice().sort(compare); + } } diff --git a/src/ts/components/admin/pipes/translitPipe.ts b/src/ts/components/admin/pipes/translitPipe.ts index dabbcaa..9da5f48 100644 --- a/src/ts/components/admin/pipes/translitPipe.ts +++ b/src/ts/components/admin/pipes/translitPipe.ts @@ -2,14 +2,14 @@ import { Pipe, PipeTransform } from '@angular/core'; import { transliterate } from 'transliteration'; @Pipe({ - name: 'translit', + name: 'translit', }) export class TranslitPipe implements PipeTransform { - transform(value: string) { - if (!value || /^[a-z0-9-_.,\[\]!@#$%^&*{}|\/\\ ]+$/i.test(value)) - return undefined; + transform(value: string) { + if (!value || /^[a-z0-9-_.,\[\]!@#$%^&*{}|\/\\ ]+$/i.test(value)) + return undefined; - const translit = transliterate(value); - return translit !== value ? translit : undefined; - } + const translit = transliterate(value); + return translit !== value ? translit : undefined; + } } diff --git a/src/ts/components/admin/shared/account-info-remote/account-info-remote.ts b/src/ts/components/admin/shared/account-info-remote/account-info-remote.ts index 2a5f4ee..1c42d37 100644 --- a/src/ts/components/admin/shared/account-info-remote/account-info-remote.ts +++ b/src/ts/components/admin/shared/account-info-remote/account-info-remote.ts @@ -4,41 +4,41 @@ import { Subscription } from '../../../../common/interfaces'; import { AdminModel } from '../../../services/adminModel'; @Component({ - selector: 'account-info-remote', - templateUrl: 'account-info-remote.pug', + selector: 'account-info-remote', + templateUrl: 'account-info-remote.pug', }) export class AccountInfoRemote implements OnDestroy { - @Input() extendedAuths = false; - @Input() popoverPlacement?: string; - @Input() showDuplicates = false; - @Input() basic = false; - account?: Account; - private _accountId?: string; - private subscription?: Subscription; - constructor(private model: AdminModel) { - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } - get accountId() { - return this._accountId; - } - @Input() set accountId(value) { - if (this._accountId !== value) { - this._accountId = value; - this.account = undefined; - this.updateSubscription(); - } - } - private updateSubscription() { - if (this.subscription) { - this.subscription.unsubscribe(); - this.subscription = undefined; - } + @Input() extendedAuths = false; + @Input() popoverPlacement?: string; + @Input() showDuplicates = false; + @Input() basic = false; + account?: Account; + private _accountId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + get accountId() { + return this._accountId; + } + @Input() set accountId(value) { + if (this._accountId !== value) { + this._accountId = value; + this.account = undefined; + this.updateSubscription(); + } + } + private updateSubscription() { + if (this.subscription) { + this.subscription.unsubscribe(); + this.subscription = undefined; + } - if (this.accountId) { - this.subscription = this.model.accounts - .subscribe(this.accountId, account => this.account = account); - } - } + if (this.accountId) { + this.subscription = this.model.accounts + .subscribe(this.accountId, account => this.account = account); + } + } } diff --git a/src/ts/components/admin/shared/account-info/account-info.ts b/src/ts/components/admin/shared/account-info/account-info.ts index deddb48..1644a29 100644 --- a/src/ts/components/admin/shared/account-info/account-info.ts +++ b/src/ts/components/admin/shared/account-info/account-info.ts @@ -4,14 +4,14 @@ import { compact } from 'lodash'; import { DAY, MINUTE, HOUR } from '../../../../common/constants'; import { hasFlag, fromNow, toInt, setFlag } from '../../../../common/utils'; import { - Account, AccountFlags, accountCounters, accountFlags, SupporterFlags, supporterFlags, BannedMuted, DuplicatesInfo + Account, AccountFlags, accountCounters, accountFlags, SupporterFlags, supporterFlags, BannedMuted, DuplicatesInfo } from '../../../../common/adminInterfaces'; import { supporterLevel, patreonSupporterLevel, getAge, isPastSupporter } from '../../../../common/adminUtils'; import { AdminModel } from '../../../services/adminModel'; import { AccountCounters } from '../../../../common/interfaces'; import { - faPatreon, faCog, faMinus, faPlus, faCheck, faFlag, faStickyNote, faCertificate, faIdBadge, faEnvelope, faFont, - faClock, faBan, faMapMarkerAlt + faPatreon, faCog, faMinus, faPlus, faCheck, faFlag, faStickyNote, faCertificate, faIdBadge, faEnvelope, faFont, + faClock, faBan, faMapMarkerAlt } from '../../../../client/icons'; const EMPTY_ROLES: string[] = []; @@ -22,271 +22,271 @@ const accountDuplicatesIntervalTime = 10 * MINUTE; const accountDuplicates = new Map(); const predefinedAlerts = [ - { - name: 'erp:flagged', - message: `Your account has been flagged for inappropriate bahavior on PG rated server. ` - + `Continuing that behavior may result in permanent ban.` - }, - { - name: 'erp:timeout', - message: `Your account has been timed out for inappropriate language and bahavior on PG rated server. ` - + `Continuing that behavior may result in permanent ban.` - }, - { - name: 'dups', - message: `Your account has been flagged for making multiple accounts. ` - + `Continuing that behavior may result in permanent ban.` - }, - { - name: 'under', - message: `Your account has been reported for being underage, please do NOT play on 18+ server. ` - + `Continuing that may result in permanent ban.` - }, + { + name: 'erp:flagged', + message: `Your account has been flagged for inappropriate bahavior on PG rated server. ` + + `Continuing that behavior may result in permanent ban.` + }, + { + name: 'erp:timeout', + message: `Your account has been timed out for inappropriate language and bahavior on PG rated server. ` + + `Continuing that behavior may result in permanent ban.` + }, + { + name: 'dups', + message: `Your account has been flagged for making multiple accounts. ` + + `Continuing that behavior may result in permanent ban.` + }, + { + name: 'under', + message: `Your account has been reported for being underage, please do NOT play on 18+ server. ` + + `Continuing that may result in permanent ban.` + }, ]; const alertExpires = [ - { name: '1h', length: HOUR }, - { name: '5h', length: 5 * HOUR }, - { name: '12h', length: 12 * HOUR }, - { name: '1d', length: DAY }, - { name: '2d', length: 2 * DAY }, - { name: '5d', length: 5 * DAY }, - { name: '7d', length: 7 * DAY }, - { name: '2w', length: 14 * DAY }, + { name: '1h', length: HOUR }, + { name: '5h', length: 5 * HOUR }, + { name: '12h', length: 12 * HOUR }, + { name: '1d', length: DAY }, + { name: '2d', length: 2 * DAY }, + { name: '5d', length: 5 * DAY }, + { name: '7d', length: 7 * DAY }, + { name: '2w', length: 14 * DAY }, ]; @Component({ - selector: 'account-info', - templateUrl: 'account-info.pug', - styleUrls: ['account-info.scss'], + selector: 'account-info', + templateUrl: 'account-info.pug', + styleUrls: ['account-info.scss'], }) export class AccountInfo implements OnInit, OnChanges { - readonly counters = accountCounters; - readonly flags = accountFlags; - readonly supporterFlags = supporterFlags; - readonly cogIcon = faCog; - readonly minusIcon = faMinus; - readonly plusIcon = faPlus; - readonly checkIcon = faCheck; - readonly flagIcon = faFlag; - readonly noteIcon = faStickyNote; - readonly newIcon = faCertificate; - readonly duplicateBrowserIdIcon = faIdBadge; - readonly duplicateEmailIcon = faEnvelope; - readonly duplicateNameIcon = faFont; - readonly duplicatePermaIcon = faBan; - readonly teleportIcon = faMapMarkerAlt; - predefinedAlerts = predefinedAlerts; - alertExpires = alertExpires; - alertExpire = alertExpires[3]; - alertMessage = ''; - @Input() account!: Account; - @Input() extendedAuths = false; - @Input() popoverPlacement?: string; - @Input() showDuplicates = false; - @ViewChild('alertModal', { static: true }) alertModal!: TemplateRef; - note?: string; - duplicates?: DuplicatesInfo; - private alertModalRef?: BsModalRef; - private _isNoteOpen = false; - constructor(private model: AdminModel, private modalService: BsModalService) { - } - ngOnInit() { - this.updateDuplicates(); - } - ngOnChanges(changes: SimpleChanges) { - if (changes.account) { - this.updateDuplicates(); - } - } - get age() { - return this.account.birthdate ? getAge(this.account.birthdate) : '-'; - } - get alert() { - const alert = this.account.alert; - return (alert && alert.expires.getTime() > Date.now()) ? alert : undefined; - } - get isNew() { - return this.account.createdAt && this.account.createdAt.getTime() > newTime; - } - get isNoteOpen() { - return this._isNoteOpen; - } - set isNoteOpen(value: boolean) { - if (this._isNoteOpen !== value) { - this._isNoteOpen = value; + readonly counters = accountCounters; + readonly flags = accountFlags; + readonly supporterFlags = supporterFlags; + readonly cogIcon = faCog; + readonly minusIcon = faMinus; + readonly plusIcon = faPlus; + readonly checkIcon = faCheck; + readonly flagIcon = faFlag; + readonly noteIcon = faStickyNote; + readonly newIcon = faCertificate; + readonly duplicateBrowserIdIcon = faIdBadge; + readonly duplicateEmailIcon = faEnvelope; + readonly duplicateNameIcon = faFont; + readonly duplicatePermaIcon = faBan; + readonly teleportIcon = faMapMarkerAlt; + predefinedAlerts = predefinedAlerts; + alertExpires = alertExpires; + alertExpire = alertExpires[3]; + alertMessage = ''; + @Input() account!: Account; + @Input() extendedAuths = false; + @Input() popoverPlacement?: string; + @Input() showDuplicates = false; + @ViewChild('alertModal', { static: true }) alertModal!: TemplateRef; + note?: string; + duplicates?: DuplicatesInfo; + private alertModalRef?: BsModalRef; + private _isNoteOpen = false; + constructor(private model: AdminModel, private modalService: BsModalService) { + } + ngOnInit() { + this.updateDuplicates(); + } + ngOnChanges(changes: SimpleChanges) { + if (changes.account) { + this.updateDuplicates(); + } + } + get age() { + return this.account.birthdate ? getAge(this.account.birthdate) : '-'; + } + get alert() { + const alert = this.account.alert; + return (alert && alert.expires.getTime() > Date.now()) ? alert : undefined; + } + get isNew() { + return this.account.createdAt && this.account.createdAt.getTime() > newTime; + } + get isNoteOpen() { + return this._isNoteOpen; + } + set isNoteOpen(value: boolean) { + if (this._isNoteOpen !== value) { + this._isNoteOpen = value; - if (value) { - this.note = this.account.note; - } - } - } - get isInactive() { - return this.account && this.account.lastVisit && this.account.lastVisit.getTime() < oldTime; - } - get roles() { - return this.account.roles ? this.account.roles.filter(r => r !== 'superadmin') : EMPTY_ROLES; - } - get flagClass() { - const counters = this.account.counters; + if (value) { + this.note = this.account.note; + } + } + } + get isInactive() { + return this.account && this.account.lastVisit && this.account.lastVisit.getTime() < oldTime; + } + get roles() { + return this.account.roles ? this.account.roles.filter(r => r !== 'superadmin') : EMPTY_ROLES; + } + get flagClass() { + const counters = this.account.counters; - if (this.account.flags) { - return 'text-banned'; - } else if (!counters && !this.account.supporter) { - return 'text-muted'; - } else if (counters && ((counters.spam || 0) > 100 || (counters.swears || 0) > 10)) { - return 'text-alert'; - } else { - return 'text-present'; - } - } - get hasDuplicateNote() { - return this.account.note && /duplicate/i.test(this.account.note); - } - hasFlag(value: AccountFlags) { - return hasFlag(this.account.flags, value); - } - toggleFlag(value: AccountFlags) { - this.model.setAccountFlags(this.account._id, this.account.flags ^ value); - } - toggleBan(field: keyof BannedMuted, value: number) { - this.model.setAccountBanField(this.account._id, field, value); - } - kick() { - this.model.kick(this.account._id); - } - report() { - this.model.report(this.account._id); - } - blur() { - this.model.setNote(this.account._id, this.note || ''); - this.isNoteOpen = false; - } - decrementCounter(name: keyof AccountCounters) { - if (this.getCounter(name) > 0) { - this.setCounter(name, this.getCounter(name) - 1); - } - } - incrementCounter(name: keyof AccountCounters) { - this.setCounter(name, this.getCounter(name) + 1); - } - getCounter(name: keyof AccountCounters) { - const counters = this.account.counters; - return toInt(counters && counters[name]); - } - setCounter(name: keyof AccountCounters, value: number) { - const counters = this.account.counters || (this.account.counters = {}); - counters[name] = value; - this.model.setAccountCounter(this.account._id, name, value); - } - removeAlert() { - this.model.setAlert(this.account._id, '', 0); - } - setAlert() { - this.alertMessage = this.alert ? this.alert.message : ''; - this.alertExpire = this.alertExpires[2]; - this.alertModalRef = this.modalService.show(this.alertModal, { ignoreBackdropClick: true }); - } - cancelAlert() { - this.alertModalRef && this.alertModalRef.hide(); - this.alertModalRef = undefined; - } - confirmAlert() { - this.model.setAlert(this.account._id, this.alertMessage, this.alertExpire.length); - this.cancelAlert(); - } - private updateDuplicates() { - const account = this.account; + if (this.account.flags) { + return 'text-banned'; + } else if (!counters && !this.account.supporter) { + return 'text-muted'; + } else if (counters && ((counters.spam || 0) > 100 || (counters.swears || 0) > 10)) { + return 'text-alert'; + } else { + return 'text-present'; + } + } + get hasDuplicateNote() { + return this.account.note && /duplicate/i.test(this.account.note); + } + hasFlag(value: AccountFlags) { + return hasFlag(this.account.flags, value); + } + toggleFlag(value: AccountFlags) { + this.model.setAccountFlags(this.account._id, this.account.flags ^ value); + } + toggleBan(field: keyof BannedMuted, value: number) { + this.model.setAccountBanField(this.account._id, field, value); + } + kick() { + this.model.kick(this.account._id); + } + report() { + this.model.report(this.account._id); + } + blur() { + this.model.setNote(this.account._id, this.note || ''); + this.isNoteOpen = false; + } + decrementCounter(name: keyof AccountCounters) { + if (this.getCounter(name) > 0) { + this.setCounter(name, this.getCounter(name) - 1); + } + } + incrementCounter(name: keyof AccountCounters) { + this.setCounter(name, this.getCounter(name) + 1); + } + getCounter(name: keyof AccountCounters) { + const counters = this.account.counters; + return toInt(counters && counters[name]); + } + setCounter(name: keyof AccountCounters, value: number) { + const counters = this.account.counters || (this.account.counters = {}); + counters[name] = value; + this.model.setAccountCounter(this.account._id, name, value); + } + removeAlert() { + this.model.setAlert(this.account._id, '', 0); + } + setAlert() { + this.alertMessage = this.alert ? this.alert.message : ''; + this.alertExpire = this.alertExpires[2]; + this.alertModalRef = this.modalService.show(this.alertModal, { ignoreBackdropClick: true }); + } + cancelAlert() { + this.alertModalRef && this.alertModalRef.hide(); + this.alertModalRef = undefined; + } + confirmAlert() { + this.model.setAlert(this.account._id, this.alertMessage, this.alertExpire.length); + this.cancelAlert(); + } + private updateDuplicates() { + const account = this.account; - if (this.showDuplicates && account) { - const cached = accountDuplicates.get(account._id); - const threshold = fromNow(-accountDuplicatesIntervalTime); + if (this.showDuplicates && account) { + const cached = accountDuplicates.get(account._id); + const threshold = fromNow(-accountDuplicatesIntervalTime); - if (cached && cached.generatedAt > threshold.getTime()) { - this.duplicates = cached; - } else { - this.model.getAllDuplicatesQuickInfo(account._id) - .then(duplicates => { - if (duplicates) { - accountDuplicates.set(account._id, duplicates); - this.duplicates = duplicates; - } - }); - } - } - } - teleportTo() { - this.model.teleportTo(this.account._id); - } - // supporters - get isPatreonOrSupporter() { - return !!(this.account.patreon || this.account.supporter || this.account.supporterDeclinedSince); - } - get supporterClass() { - return supporterLevel(this.account) ? 'badge-success' : 'badge-warning'; - } - get supporterTitle() { - const supporter = this.account.supporter!; - const flagSupporter = (supporter & SupporterFlags.SupporterMask) !== 0; - const patreonSupporter = patreonSupporterLevel(this.account); - const ignorePatreon = hasFlag(supporter, SupporterFlags.IgnorePatreon); - const pastSupporter = hasFlag(supporter, SupporterFlags.PastSupporter); + if (cached && cached.generatedAt > threshold.getTime()) { + this.duplicates = cached; + } else { + this.model.getAllDuplicatesQuickInfo(account._id) + .then(duplicates => { + if (duplicates) { + accountDuplicates.set(account._id, duplicates); + this.duplicates = duplicates; + } + }); + } + } + } + teleportTo() { + this.model.teleportTo(this.account._id); + } + // supporters + get isPatreonOrSupporter() { + return !!(this.account.patreon || this.account.supporter || this.account.supporterDeclinedSince); + } + get supporterClass() { + return supporterLevel(this.account) ? 'badge-success' : 'badge-warning'; + } + get supporterTitle() { + const supporter = this.account.supporter!; + const flagSupporter = (supporter & SupporterFlags.SupporterMask) !== 0; + const patreonSupporter = patreonSupporterLevel(this.account); + const ignorePatreon = hasFlag(supporter, SupporterFlags.IgnorePatreon); + const pastSupporter = hasFlag(supporter, SupporterFlags.PastSupporter); - return compact([ - flagSupporter && 'flags', - patreonSupporter && `patreon`, - ignorePatreon && 'ignore', - !patreonSupporter && this.account.supporterDeclinedSince && 'declined', - pastSupporter && 'past', - ]).join(', '); - } - get supporterIcon() { - const hasPatreon = patreonSupporterLevel(this.account); - const hasIgnoreFlag = hasFlag(this.account.supporter, SupporterFlags.IgnorePatreon); - const hasDeclined = !!this.account.supporterDeclinedSince; - return hasPatreon ? faPatreon : ((hasIgnoreFlag || !hasDeclined) ? faFlag : faClock); - } - get hasAnySupporter() { - return (this.account.supporter! & SupporterFlags.SupporterMask) !== 0; - } - get hasPastSupporter() { - return hasFlag(this.account.supporter, SupporterFlags.PastSupporter); - } - get supporterLevel() { - return supporterLevel(this.account); - } - get supporterLevelString() { - const level = supporterLevel(this.account); - return level ? level : (isPastSupporter(this.account) ? 'P' : ''); - } - isSupporter(level: number) { - return (this.account.supporter! & SupporterFlags.SupporterMask) === level; - } - setSupporter(level: number) { - const supporter = (this.account.supporter! & ~SupporterFlags.SupporterMask) | level; - this.model.setSupporterFlags(this.account._id, supporter); - } - hasSupporterFlag(value: SupporterFlags) { - return hasFlag(this.account.supporter, value); - } - toggleSupporterFlag(value: SupporterFlags) { - this.model.setSupporterFlags(this.account._id, this.account.supporter! ^ value); - } - // past supporter - get isForcePastSupporter() { - return hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter); - } - get isIgnorePastSupporter() { - return hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter); - } - toggleForcePastSupporter() { - const has = hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter); - const supporter = setFlag(this.account.supporter, SupporterFlags.ForcePastSupporter, !has); - this.model.setSupporterFlags(this.account._id, supporter); - } - toggleIgnorePastSupporter() { - const has = hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter); - const supporter = setFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter, !has); - this.model.setSupporterFlags(this.account._id, supporter); - } + return compact([ + flagSupporter && 'flags', + patreonSupporter && `patreon`, + ignorePatreon && 'ignore', + !patreonSupporter && this.account.supporterDeclinedSince && 'declined', + pastSupporter && 'past', + ]).join(', '); + } + get supporterIcon() { + const hasPatreon = patreonSupporterLevel(this.account); + const hasIgnoreFlag = hasFlag(this.account.supporter, SupporterFlags.IgnorePatreon); + const hasDeclined = !!this.account.supporterDeclinedSince; + return hasPatreon ? faPatreon : ((hasIgnoreFlag || !hasDeclined) ? faFlag : faClock); + } + get hasAnySupporter() { + return (this.account.supporter! & SupporterFlags.SupporterMask) !== 0; + } + get hasPastSupporter() { + return hasFlag(this.account.supporter, SupporterFlags.PastSupporter); + } + get supporterLevel() { + return supporterLevel(this.account); + } + get supporterLevelString() { + const level = supporterLevel(this.account); + return level ? level : (isPastSupporter(this.account) ? 'P' : ''); + } + isSupporter(level: number) { + return (this.account.supporter! & SupporterFlags.SupporterMask) === level; + } + setSupporter(level: number) { + const supporter = (this.account.supporter! & ~SupporterFlags.SupporterMask) | level; + this.model.setSupporterFlags(this.account._id, supporter); + } + hasSupporterFlag(value: SupporterFlags) { + return hasFlag(this.account.supporter, value); + } + toggleSupporterFlag(value: SupporterFlags) { + this.model.setSupporterFlags(this.account._id, this.account.supporter! ^ value); + } + // past supporter + get isForcePastSupporter() { + return hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter); + } + get isIgnorePastSupporter() { + return hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter); + } + toggleForcePastSupporter() { + const has = hasFlag(this.account.supporter, SupporterFlags.ForcePastSupporter); + const supporter = setFlag(this.account.supporter, SupporterFlags.ForcePastSupporter, !has); + this.model.setSupporterFlags(this.account._id, supporter); + } + toggleIgnorePastSupporter() { + const has = hasFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter); + const supporter = setFlag(this.account.supporter, SupporterFlags.IgnorePastSupporter, !has); + this.model.setSupporterFlags(this.account._id, supporter); + } } diff --git a/src/ts/components/admin/shared/account-status/account-status.ts b/src/ts/components/admin/shared/account-status/account-status.ts index 3a2b0a0..21bf95d 100644 --- a/src/ts/components/admin/shared/account-status/account-status.ts +++ b/src/ts/components/admin/shared/account-status/account-status.ts @@ -4,25 +4,25 @@ import { Account, AccountStatus as IAccountStatus } from '../../../../common/adm import { faUserSecret } from '../../../../client/icons'; @Component({ - selector: 'account-status', - templateUrl: 'account-status.pug', + selector: 'account-status', + templateUrl: 'account-status.pug', }) export class AccountStatus implements OnInit, OnChanges { - readonly incognitoIcon = faUserSecret; - @Input() account!: Account; - @Input() verbose = false; - status: IAccountStatus[] | undefined = undefined; - constructor(private model: AdminModel) { - } - ngOnInit() { - this.refresh(); - } - ngOnChanges() { - this.status = undefined; - this.refresh(); - } - refresh() { - this.model.getAccountStatus(this.account._id) - .then(status => this.status = status); - } + readonly incognitoIcon = faUserSecret; + @Input() account!: Account; + @Input() verbose = false; + status: IAccountStatus[] | undefined = undefined; + constructor(private model: AdminModel) { + } + ngOnInit() { + this.refresh(); + } + ngOnChanges() { + this.status = undefined; + this.refresh(); + } + refresh() { + this.model.getAccountStatus(this.account._id) + .then(status => this.status = status); + } } diff --git a/src/ts/components/admin/shared/account-tooltip/account-tooltip.ts b/src/ts/components/admin/shared/account-tooltip/account-tooltip.ts index 7fe8d40..f814e56 100644 --- a/src/ts/components/admin/shared/account-tooltip/account-tooltip.ts +++ b/src/ts/components/admin/shared/account-tooltip/account-tooltip.ts @@ -5,16 +5,16 @@ import { getAge } from '../../../../common/adminUtils'; const year = (new Date()).getFullYear(); @Component({ - selector: 'account-tooltip', - templateUrl: 'account-tooltip.pug', + selector: 'account-tooltip', + templateUrl: 'account-tooltip.pug', }) export class AccountTooltip { - @Input() account!: Account; - @Input() extendedAuths = false; - get age() { - return this.account.birthdate ? getAge(this.account.birthdate) : '-'; - } - get forceAge() { - return this.account.birthyear ? (year - this.account.birthyear) : ''; - } + @Input() account!: Account; + @Input() extendedAuths = false; + get age() { + return this.account.birthdate ? getAge(this.account.birthdate) : '-'; + } + get forceAge() { + return this.account.birthyear ? (year - this.account.birthyear) : ''; + } } diff --git a/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.ts b/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.ts index 5e179bd..35f1649 100644 --- a/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.ts +++ b/src/ts/components/admin/shared/admin-chat-log/admin-chat-log.ts @@ -8,175 +8,175 @@ import { removeAllNodes, appendAllNodes, showTextInNewTab } from '../../../../cl import { includes } from '../../../../common/utils'; @Component({ - selector: 'admin-chat-log', - templateUrl: 'admin-chat-log.pug', - styleUrls: ['admin-chat-log.scss'], + selector: 'admin-chat-log', + templateUrl: 'admin-chat-log.pug', + styleUrls: ['admin-chat-log.scss'], }) export class AdminChatLog implements OnDestroy { - readonly searchIcon = faSearch; - readonly spinnerIcon = faSpinner; - readonly syncIcon = faSync; - readonly fileIcon = faFileAlt; - readonly closeIcon = faTimes; - readonly chevronLeftIcon = faChevronLeft; - readonly chevronRightIcon = faChevronRight; - @Input() canClose = true; - accounts: Account[] = []; - search?: string; - open = false; - today: ChatDate = createChatDate(moment()); - dates: ChatDate[] = [/*{ value: 'all', label: 'All' },*/ ...createDateRange(new Date(), 14)]; - date?: ChatDate; - chatRaw?: string; - loading = false; - private _account?: Account; - private refreshInterval?: any; - constructor(private model: AdminModel, private element: ElementRef) { - } - get autoRefresh() { - return !!this.refreshInterval; - } - set autoRefresh(value: boolean) { - if (value) { - this.refreshInterval = this.refreshInterval || setInterval(() => this.refresh(), 10 * 1000); - } else { - this.stopInterval(); - } - } - get title() { - return this.search || (this.account && this.account.name) || 'Chat'; - } - get account() { - return this._account; - } - @Input() set account(value) { - const theSame = this._account === value || (value && this._account && value._id === this._account._id); - this._account = value; + readonly searchIcon = faSearch; + readonly spinnerIcon = faSpinner; + readonly syncIcon = faSync; + readonly fileIcon = faFileAlt; + readonly closeIcon = faTimes; + readonly chevronLeftIcon = faChevronLeft; + readonly chevronRightIcon = faChevronRight; + @Input() canClose = true; + accounts: Account[] = []; + search?: string; + open = false; + today: ChatDate = createChatDate(moment()); + dates: ChatDate[] = [/*{ value: 'all', label: 'All' },*/ ...createDateRange(new Date(), 14)]; + date?: ChatDate; + chatRaw?: string; + loading = false; + private _account?: Account; + private refreshInterval?: any; + constructor(private model: AdminModel, private element: ElementRef) { + } + get autoRefresh() { + return !!this.refreshInterval; + } + set autoRefresh(value: boolean) { + if (value) { + this.refreshInterval = this.refreshInterval || setInterval(() => this.refresh(), 10 * 1000); + } else { + this.stopInterval(); + } + } + get title() { + return this.search || (this.account && this.account.name) || 'Chat'; + } + get account() { + return this._account; + } + @Input() set account(value) { + const theSame = this._account === value || (value && this._account && value._id === this._account._id); + this._account = value; - if (!theSame) { - this.date = undefined; - this.setChatlogElements([]); - } - } - ngOnDestroy() { - this.close(); - } - show(account?: Account, date?: ChatDate) { - this.search = undefined; - this.account = account; - this.date = date || this.today; - this.open = true; - this.accounts = []; - this.refresh(); - } - add(account: Account) { - if (!this.account) { - this.show(account); - } else if (account !== this.account && !includes(this.accounts, account)) { - this.date = this.date || this.today; - this.accounts.push(account); - this.refresh(); - } - } - removeAccount(index: number) { - this.accounts.splice(index, 1); - this.refresh(); - } - showDate(date: ChatDate) { - this.date = date; - this.refresh(); - } - prev() { - this.switchDate(-1); - } - next() { - this.switchDate(1); - } - all() { - this.date = this.dates[0]; - this.refresh(); - } - close() { - this.account = undefined; - this.date = undefined; - this.open = false; - this.setChatlogElements([]); - this.stopInterval(); - } - searchChat(search: string | undefined) { - this.search = search; - this.refresh(); - } - refresh() { - const date = this.date && this.date.value; + if (!theSame) { + this.date = undefined; + this.setChatlogElements([]); + } + } + ngOnDestroy() { + this.close(); + } + show(account?: Account, date?: ChatDate) { + this.search = undefined; + this.account = account; + this.date = date || this.today; + this.open = true; + this.accounts = []; + this.refresh(); + } + add(account: Account) { + if (!this.account) { + this.show(account); + } else if (account !== this.account && !includes(this.accounts, account)) { + this.date = this.date || this.today; + this.accounts.push(account); + this.refresh(); + } + } + removeAccount(index: number) { + this.accounts.splice(index, 1); + this.refresh(); + } + showDate(date: ChatDate) { + this.date = date; + this.refresh(); + } + prev() { + this.switchDate(-1); + } + next() { + this.switchDate(1); + } + all() { + this.date = this.dates[0]; + this.refresh(); + } + close() { + this.account = undefined; + this.date = undefined; + this.open = false; + this.setChatlogElements([]); + this.stopInterval(); + } + searchChat(search: string | undefined) { + this.search = search; + this.refresh(); + } + refresh() { + const date = this.date && this.date.value; - if (this.account) { - const accounts = [this.account._id, ...this.accounts.map(a => a._id)]; - this.handleChat(this.model.accountsFormattedChat(accounts, date)); - } else if (this.search) { - this.handleChat(this.model.searchFormattedChat(this.search, date)); - } - } - openLog() { - showTextInNewTab(`${this.date ? this.date.label : 'none'}\n\n${(this.chatRaw || '').replace(/\t/g, ' ')}`); - } - private handleChat(promise: Promise<{ raw: string; html: HTMLElement[]; }>) { - this.loading = true; + if (this.account) { + const accounts = [this.account._id, ...this.accounts.map(a => a._id)]; + this.handleChat(this.model.accountsFormattedChat(accounts, date)); + } else if (this.search) { + this.handleChat(this.model.searchFormattedChat(this.search, date)); + } + } + openLog() { + showTextInNewTab(`${this.date ? this.date.label : 'none'}\n\n${(this.chatRaw || '').replace(/\t/g, ' ')}`); + } + private handleChat(promise: Promise<{ raw: string; html: HTMLElement[]; }>) { + this.loading = true; - promise - .then(({ raw, html }) => { - this.chatRaw = raw; - this.setChatlogElements(html); - }) - .finally(() => this.loading = false); - } - private switchDate(days: number) { - const validDate = this.date && this.date.value !== 'all'; - this.date = validDate ? createChatDate(moment(this.date!.value).add(days, 'days')) : this.today; - this.refresh(); - } - private stopInterval() { - clearInterval(this.refreshInterval); - this.refreshInterval = undefined; - } - private getChatlogElement() { - return (this.element.nativeElement as HTMLElement).querySelector('.chatlog'); - } - private setChatlogElements(elements: HTMLElement[]) { - const element = this.getChatlogElement(); + promise + .then(({ raw, html }) => { + this.chatRaw = raw; + this.setChatlogElements(html); + }) + .finally(() => this.loading = false); + } + private switchDate(days: number) { + const validDate = this.date && this.date.value !== 'all'; + this.date = validDate ? createChatDate(moment(this.date!.value).add(days, 'days')) : this.today; + this.refresh(); + } + private stopInterval() { + clearInterval(this.refreshInterval); + this.refreshInterval = undefined; + } + private getChatlogElement() { + return (this.element.nativeElement as HTMLElement).querySelector('.chatlog'); + } + private setChatlogElements(elements: HTMLElement[]) { + const element = this.getChatlogElement(); - if (element) { - removeAllNodes(element); - appendAllNodes(element, elements); - this.nodesToProcess = [ - ...Array.from(element.getElementsByClassName('name')), - ...Array.from(element.getElementsByClassName('message')), - ] as HTMLElement[]; - this.atNode = 0; - this.processNodes(); - } - } - private processNodes() { - const processStep = 50; - const nodes = this.nodesToProcess; + if (element) { + removeAllNodes(element); + appendAllNodes(element, elements); + this.nodesToProcess = [ + ...Array.from(element.getElementsByClassName('name')), + ...Array.from(element.getElementsByClassName('message')), + ] as HTMLElement[]; + this.atNode = 0; + this.processNodes(); + } + } + private processNodes() { + const processStep = 50; + const nodes = this.nodesToProcess; - cancelIdleCallback(this.processIdle); + cancelIdleCallback(this.processIdle); - if (nodes && this.atNode < nodes.length) { - let i = 0; + if (nodes && this.atNode < nodes.length) { + let i = 0; - while (i < processStep && (i + this.atNode) < nodes.length) { - replaceSwears(nodes[i + this.atNode]); - i++; - } + while (i < processStep && (i + this.atNode) < nodes.length) { + replaceSwears(nodes[i + this.atNode]); + i++; + } - this.atNode += i; - this.processIdle = requestIdleCallback(() => this.processNodes()); - } else { - this.nodesToProcess = undefined; - } - } - private nodesToProcess?: HTMLElement[]; - private atNode = 0; - private processIdle = 0; + this.atNode += i; + this.processIdle = requestIdleCallback(() => this.processNodes()); + } else { + this.nodesToProcess = undefined; + } + } + private nodesToProcess?: HTMLElement[]; + private atNode = 0; + private processIdle = 0; } diff --git a/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.ts b/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.ts index 152ccd1..f1a391a 100644 --- a/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.ts +++ b/src/ts/components/admin/shared/auth-info-edit/auth-info-edit.ts @@ -5,60 +5,60 @@ import { AdminModel } from '../../../services/adminModel'; import { faInfo, faLock, faTrash, faEyeSlash, faArrowRight } from '../../../../client/icons'; @Component({ - selector: 'auth-info-edit', - templateUrl: 'auth-info-edit.pug', + selector: 'auth-info-edit', + templateUrl: 'auth-info-edit.pug', }) export class AuthInfoEdit implements OnDestroy { - readonly assignIcon = faArrowRight; - readonly infoIcon = faInfo; - readonly lockIcon = faLock; - readonly trashIcon = faTrash; - readonly eyeSlashIcon = faEyeSlash; - @Input() duplicates?: DuplicateResult[]; - @Input() showName = false; - auth?: Auth; - private _authId?: string; - private subscription?: Subscription; - constructor(private model: AdminModel) { - } - get authId() { - return this._authId; - } - @Input() set authId(value: string | undefined) { - if (this.authId !== value) { - this._authId = value; - this.auth = undefined; - this.subscription && this.subscription.unsubscribe(); - this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined; - } - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } - removeAuth(auth: Auth | undefined) { - if (auth && confirm('Are you sure?')) { - this.model.removeAuth(auth._id); - } - } - showAuthData(auth: Auth | undefined) { - if (auth) { - this.model.getAuth(auth._id) - .then(x => console.log(x)); - } - } - toggleAuthDisabled(auth: Auth | undefined) { - if (auth) { - this.model.updateAuth(auth._id, { disabled: !auth.disabled }); - } - } - toggleAuthBanned(auth: Auth | undefined) { - if (auth) { - this.model.updateAuth(auth._id, { banned: !auth.banned }); - } - } - assignTo(accountId: string) { - if (this.authId) { - this.model.assignAuth(this.authId, accountId); - } - } + readonly assignIcon = faArrowRight; + readonly infoIcon = faInfo; + readonly lockIcon = faLock; + readonly trashIcon = faTrash; + readonly eyeSlashIcon = faEyeSlash; + @Input() duplicates?: DuplicateResult[]; + @Input() showName = false; + auth?: Auth; + private _authId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get authId() { + return this._authId; + } + @Input() set authId(value: string | undefined) { + if (this.authId !== value) { + this._authId = value; + this.auth = undefined; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + removeAuth(auth: Auth | undefined) { + if (auth && confirm('Are you sure?')) { + this.model.removeAuth(auth._id); + } + } + showAuthData(auth: Auth | undefined) { + if (auth) { + this.model.getAuth(auth._id) + .then(x => console.log(x)); + } + } + toggleAuthDisabled(auth: Auth | undefined) { + if (auth) { + this.model.updateAuth(auth._id, { disabled: !auth.disabled }); + } + } + toggleAuthBanned(auth: Auth | undefined) { + if (auth) { + this.model.updateAuth(auth._id, { banned: !auth.banned }); + } + } + assignTo(accountId: string) { + if (this.authId) { + this.model.assignAuth(this.authId, accountId); + } + } } diff --git a/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.ts b/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.ts index b01b6d6..5362c05 100644 --- a/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.ts +++ b/src/ts/components/admin/shared/auth-info-remote/auth-info-remote.ts @@ -4,28 +4,28 @@ import { AdminModel } from '../../../services/adminModel'; import { Auth } from '../../../../common/adminInterfaces'; @Component({ - selector: 'auth-info-remote', - templateUrl: 'auth-info-remote.pug', + selector: 'auth-info-remote', + templateUrl: 'auth-info-remote.pug', }) export class AuthInfoRemote implements OnDestroy { - @Input() showName = false; - auth?: Auth; - private _authId?: string; - private subscription?: Subscription; - constructor(private model: AdminModel) { - } - get authId() { - return this._authId; - } - @Input() set authId(value: string | undefined) { - if (this.authId !== value) { - this._authId = value; - this.auth = undefined; - this.subscription && this.subscription.unsubscribe(); - this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined; - } - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } + @Input() showName = false; + auth?: Auth; + private _authId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get authId() { + return this._authId; + } + @Input() set authId(value: string | undefined) { + if (this.authId !== value) { + this._authId = value; + this.auth = undefined; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.auths.subscribe(value, auth => this.auth = auth) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } } diff --git a/src/ts/components/admin/shared/auth-info/auth-info.ts b/src/ts/components/admin/shared/auth-info/auth-info.ts index 4f1b127..cc6329d 100644 --- a/src/ts/components/admin/shared/auth-info/auth-info.ts +++ b/src/ts/components/admin/shared/auth-info/auth-info.ts @@ -3,26 +3,26 @@ import { Auth } from '../../../../common/adminInterfaces'; import { oauthIcons, faGlobe } from '../../../../client/icons'; @Component({ - selector: 'auth-info', - templateUrl: 'auth-info.pug', - styleUrls: ['auth-info.scss'], - host: { - '[class.deleted]': 'deleted', - }, + selector: 'auth-info', + templateUrl: 'auth-info.pug', + styleUrls: ['auth-info.scss'], + host: { + '[class.deleted]': 'deleted', + }, }) export class AuthInfo { - @Input() auth?: Auth; - @Input() showName = false; - get deleted(): boolean { - return !!(this.auth && (this.auth.disabled || this.auth.banned)); - } - get name(): string { - return this.auth && this.auth.name || ''; - } - get icon() { - return oauthIcons[this.auth && this.auth.provider || ''] || faGlobe; - } - get pledged() { - return (this.auth && this.auth.pledged || 0) / 100; - } + @Input() auth?: Auth; + @Input() showName = false; + get deleted(): boolean { + return !!(this.auth && (this.auth.disabled || this.auth.banned)); + } + get name(): string { + return this.auth && this.auth.name || ''; + } + get icon() { + return oauthIcons[this.auth && this.auth.provider || ''] || faGlobe; + } + get pledged() { + return (this.auth && this.auth.pledged || 0) / 100; + } } diff --git a/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.ts b/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.ts index 05f9c87..cdd7d42 100644 --- a/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.ts +++ b/src/ts/components/admin/shared/auth-list-remote/auth-list-remote.ts @@ -3,35 +3,35 @@ import { Subscription } from '../../../../common/interfaces'; import { AdminModel } from '../../../services/adminModel'; @Component({ - selector: 'auth-list-remote', - templateUrl: 'auth-list-remote.pug', - styleUrls: ['auth-list-remote.scss'], + selector: 'auth-list-remote', + templateUrl: 'auth-list-remote.pug', + styleUrls: ['auth-list-remote.scss'], }) export class AuthListRemote implements OnDestroy { - @Input() limit = 6; - @Input() extended = false; - auths: string[] = []; - loading = false; - private _accountId?: string; - private subscription?: Subscription; - constructor(private model: AdminModel) { - } - get accountId() { - return this._accountId; - } - @Input() set accountId(value: string | undefined) { - if (this.accountId !== value) { - this._accountId = value; - this.auths = []; - this.loading = true; - this.subscription && this.subscription.unsubscribe(); - this.subscription = value ? this.model.accountAuths.subscribe(value, auths => { - this.auths = auths || []; - this.loading = false; - }) : undefined; - } - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } + @Input() limit = 6; + @Input() extended = false; + auths: string[] = []; + loading = false; + private _accountId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get accountId() { + return this._accountId; + } + @Input() set accountId(value: string | undefined) { + if (this.accountId !== value) { + this._accountId = value; + this.auths = []; + this.loading = true; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.accountAuths.subscribe(value, auths => { + this.auths = auths || []; + this.loading = false; + }) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } } diff --git a/src/ts/components/admin/shared/auth-list/auth-list.ts b/src/ts/components/admin/shared/auth-list/auth-list.ts index f7c0385..a609e5d 100644 --- a/src/ts/components/admin/shared/auth-list/auth-list.ts +++ b/src/ts/components/admin/shared/auth-list/auth-list.ts @@ -2,18 +2,18 @@ import { Component, Input } from '@angular/core'; import { Auth } from '../../../../common/adminInterfaces'; @Component({ - selector: 'auth-list', - templateUrl: 'auth-list.pug', - styleUrls: ['auth-list.scss'], - host: { - '[class.extended]': 'extended', - }, + selector: 'auth-list', + templateUrl: 'auth-list.pug', + styleUrls: ['auth-list.scss'], + host: { + '[class.extended]': 'extended', + }, }) export class AuthList { - @Input() limit = 6; - @Input() extended = false; - @Input() auths?: Auth[]; - get fixedAuths() { - return this.auths || []; - } + @Input() limit = 6; + @Input() extended = false; + @Input() auths?: Auth[]; + get fixedAuths() { + return this.auths || []; + } } diff --git a/src/ts/components/admin/shared/ban-icon/ban-icon.ts b/src/ts/components/admin/shared/ban-icon/ban-icon.ts index 6529851..069b5dd 100644 --- a/src/ts/components/admin/shared/ban-icon/ban-icon.ts +++ b/src/ts/components/admin/shared/ban-icon/ban-icon.ts @@ -1,5 +1,5 @@ import { - Component, Input, EventEmitter, Output, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, NgZone + Component, Input, EventEmitter, Output, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, NgZone } from '@angular/core'; import { TIMEOUTS } from '../../../../common/constants'; import { BannedMuted } from '../../../../common/adminInterfaces'; @@ -7,71 +7,71 @@ import { IntervalUpdateService } from '../../../services/intervalUpdateService'; import { faClock, faMicrophoneSlash, faEyeSlash, faBan } from '../../../../client/icons'; const ICONS = { - mute: faMicrophoneSlash, - shadow: faEyeSlash, - ban: faBan, + mute: faMicrophoneSlash, + shadow: faEyeSlash, + ban: faBan, }; @Component({ - selector: 'ban-icon', - templateUrl: 'ban-icon.pug', - styleUrls: ['ban-icon.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'ban-icon', + templateUrl: 'ban-icon.pug', + styleUrls: ['ban-icon.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class BanIcon implements OnInit, OnDestroy, OnChanges { - readonly timeouts = TIMEOUTS; - readonly clockIcon = faClock; - @Input() type: keyof BannedMuted = 'ban'; - @Input() value = 0; - @Output() toggle = new EventEmitter(); - get icon() { - return ICONS[this.type] || ICONS.ban; - } - get isPerma() { - return this.value === -1; - } - get isTimedOut() { - return this.value > Date.now(); - } - get className() { - if (this.isPerma) { - return 'text-banned'; - } else if (this.isTimedOut) { - return 'text-alert'; - } else { - return 'text-muted'; - } - } - private timedOut = false; - private toggleUpdate: (on: boolean) => void; - constructor(zone: NgZone, updateService: IntervalUpdateService) { - this.toggleUpdate = updateService.toggle(() => { - if (this.timedOut !== this.isTimedOut) { - zone.run(() => this.timedOut = this.isTimedOut); - this.toggleUpdate(this.isTimedOut); - } - }); - } - ngOnInit() { - this.toggleUpdate(this.isTimedOut); - } - ngOnChanges() { - this.toggleUpdate(this.isTimedOut); - } - ngOnDestroy() { - this.toggleUpdate(false); - } - clear() { - this.setValue(0); - } - perma() { - this.setValue(-1); - } - timeout(value: number) { - this.setValue(Date.now() + value); - } - private setValue(value: number) { - this.value = value; - this.toggle.emit(value); - } + readonly timeouts = TIMEOUTS; + readonly clockIcon = faClock; + @Input() type: keyof BannedMuted = 'ban'; + @Input() value = 0; + @Output() toggle = new EventEmitter(); + get icon() { + return ICONS[this.type] || ICONS.ban; + } + get isPerma() { + return this.value === -1; + } + get isTimedOut() { + return this.value > Date.now(); + } + get className() { + if (this.isPerma) { + return 'text-banned'; + } else if (this.isTimedOut) { + return 'text-alert'; + } else { + return 'text-muted'; + } + } + private timedOut = false; + private toggleUpdate: (on: boolean) => void; + constructor(zone: NgZone, updateService: IntervalUpdateService) { + this.toggleUpdate = updateService.toggle(() => { + if (this.timedOut !== this.isTimedOut) { + zone.run(() => this.timedOut = this.isTimedOut); + this.toggleUpdate(this.isTimedOut); + } + }); + } + ngOnInit() { + this.toggleUpdate(this.isTimedOut); + } + ngOnChanges() { + this.toggleUpdate(this.isTimedOut); + } + ngOnDestroy() { + this.toggleUpdate(false); + } + clear() { + this.setValue(0); + } + perma() { + this.setValue(-1); + } + timeout(value: number) { + this.setValue(Date.now() + value); + } + private setValue(value: number) { + this.value = value; + this.toggle.emit(value); + } } diff --git a/src/ts/components/admin/shared/email-list/email-list.ts b/src/ts/components/admin/shared/email-list/email-list.ts index 95c837d..af3eaf2 100644 --- a/src/ts/components/admin/shared/email-list/email-list.ts +++ b/src/ts/components/admin/shared/email-list/email-list.ts @@ -1,16 +1,16 @@ import { Component, Input } from '@angular/core'; @Component({ - selector: 'email-list', - templateUrl: 'email-list.pug', + selector: 'email-list', + templateUrl: 'email-list.pug', }) export class EmailList { - @Input() emails?: string[]; - limit = 3; - get hasMore() { - return this.emails && this.emails.length > this.limit; - } - showMore() { - this.limit = 9999; - } + @Input() emails?: string[]; + limit = 3; + get hasMore() { + return this.emails && this.emails.length > this.limit; + } + showMore() { + this.limit = 9999; + } } diff --git a/src/ts/components/admin/shared/events-table/events-table.ts b/src/ts/components/admin/shared/events-table/events-table.ts index 115632b..80ba281 100644 --- a/src/ts/components/admin/shared/events-table/events-table.ts +++ b/src/ts/components/admin/shared/events-table/events-table.ts @@ -5,52 +5,52 @@ import { faLanguage, faTrash, faComment, faClipboard } from '../../../../client/ import { getTranslationUrl } from '../../../../common/adminUtils'; @Component({ - selector: 'events-table', - templateUrl: 'events-table.pug', - styleUrls: ['events-table.scss'], + selector: 'events-table', + templateUrl: 'events-table.pug', + styleUrls: ['events-table.scss'], }) export class EventsTable { - readonly clipboardIcon = faClipboard; - readonly langIcon = faLanguage; - readonly trashIcon = faTrash; - readonly commentIcon = faComment; - @Input() events!: Event[]; - @Output() showChat = new EventEmitter(); - @Output() addChat = new EventEmitter(); - @Output() removedEvent = new EventEmitter(); - constructor(private model: AdminModel) { - } - serverLabel(e: Event) { - return SERVER_LABELS[e.server] || 'badge-none'; - } - remove(e: Event) { - this.model.removeEvent(e._id); - this.removedEvent.emit(e); - } - removeAll(e: Event) { - this.model.events - .filter(x => x.message === e.message) - .forEach(x => this.model.removeEvent(x._id)); - } - copyToNotes(e: Event, account: Account | undefined) { - if (account) { - const desc = e.desc ? `: ${e.desc}` : ''; - const count = e.count > 1 ? `[${e.count}] ` : ''; - const note = `${(account.note || '')}\r\n[${e.server}]${count}${e.message}${desc}`; - this.model.setNote(account._id, note.trim()); - } - } - translateUrl(e: Event) { - return getTranslationUrl(e.desc); - } - onShowChat(e: MouseEvent, event: Event, account: Account | undefined) { - if (e.shiftKey) { - this.addChat.emit({ event, account }); - } else { - this.showChat.emit({ event, account }); - } - } - onAddChat(event: Event, account: Account | undefined) { - this.addChat.emit({ event, account }); - } + readonly clipboardIcon = faClipboard; + readonly langIcon = faLanguage; + readonly trashIcon = faTrash; + readonly commentIcon = faComment; + @Input() events!: Event[]; + @Output() showChat = new EventEmitter(); + @Output() addChat = new EventEmitter(); + @Output() removedEvent = new EventEmitter(); + constructor(private model: AdminModel) { + } + serverLabel(e: Event) { + return SERVER_LABELS[e.server] || 'badge-none'; + } + remove(e: Event) { + this.model.removeEvent(e._id); + this.removedEvent.emit(e); + } + removeAll(e: Event) { + this.model.events + .filter(x => x.message === e.message) + .forEach(x => this.model.removeEvent(x._id)); + } + copyToNotes(e: Event, account: Account | undefined) { + if (account) { + const desc = e.desc ? `: ${e.desc}` : ''; + const count = e.count > 1 ? `[${e.count}] ` : ''; + const note = `${(account.note || '')}\r\n[${e.server}]${count}${e.message}${desc}`; + this.model.setNote(account._id, note.trim()); + } + } + translateUrl(e: Event) { + return getTranslationUrl(e.desc); + } + onShowChat(e: MouseEvent, event: Event, account: Account | undefined) { + if (e.shiftKey) { + this.addChat.emit({ event, account }); + } else { + this.showChat.emit({ event, account }); + } + } + onAddChat(event: Event, account: Account | undefined) { + this.addChat.emit({ event, account }); + } } diff --git a/src/ts/components/admin/shared/from-now.ts b/src/ts/components/admin/shared/from-now.ts index 2fcf0e2..4529750 100644 --- a/src/ts/components/admin/shared/from-now.ts +++ b/src/ts/components/admin/shared/from-now.ts @@ -3,34 +3,34 @@ import * as moment from 'moment'; import { IntervalUpdateService } from '../../services/intervalUpdateService'; @Component({ - selector: 'from-now', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'from-now', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FromNow implements OnInit, OnDestroy, OnChanges { - @Input() time?: any; - private moment?: moment.Moment; - private text?: string; - private unsubscribe?: () => void; - constructor(private element: ElementRef, private updateService: IntervalUpdateService) { - } - ngOnChanges() { - this.moment = this.time ? moment(this.time) : undefined; - this.update(); - } - ngOnInit() { - this.unsubscribe = this.updateService.subscribe(() => this.update()); - this.update(); - } - ngOnDestroy() { - this.unsubscribe && this.unsubscribe(); - } - private update() { - const text = this.moment ? this.moment.fromNow(true).replace('seconds', 'secs') : ''; + @Input() time?: any; + private moment?: moment.Moment; + private text?: string; + private unsubscribe?: () => void; + constructor(private element: ElementRef, private updateService: IntervalUpdateService) { + } + ngOnChanges() { + this.moment = this.time ? moment(this.time) : undefined; + this.update(); + } + ngOnInit() { + this.unsubscribe = this.updateService.subscribe(() => this.update()); + this.update(); + } + ngOnDestroy() { + this.unsubscribe && this.unsubscribe(); + } + private update() { + const text = this.moment ? this.moment.fromNow(true).replace('seconds', 'secs') : ''; - if (this.text !== text) { - this.text = text; - (this.element.nativeElement as HTMLElement).children[0].textContent = text; - } - } + if (this.text !== text) { + this.text = text; + (this.element.nativeElement as HTMLElement).children[0].textContent = text; + } + } } diff --git a/src/ts/components/admin/shared/on-off-switch/on-off-switch.ts b/src/ts/components/admin/shared/on-off-switch/on-off-switch.ts index 4c51fcf..5692cd0 100644 --- a/src/ts/components/admin/shared/on-off-switch/on-off-switch.ts +++ b/src/ts/components/admin/shared/on-off-switch/on-off-switch.ts @@ -1,20 +1,20 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; @Component({ - selector: 'on-off-switch', - templateUrl: 'on-off-switch.pug', - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'on-off-switch', + templateUrl: 'on-off-switch.pug', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class OnOffSwitch { - @Input() on = false; - @Input() disabled = false; - @Input() onText = 'ON'; - @Input() offText = 'OFF'; - @Input() label = ''; - @Output() toggle = new EventEmitter(); - onToggle(value: boolean) { - if (value !== this.on) { - this.toggle.emit(value); - } - } + @Input() on = false; + @Input() disabled = false; + @Input() onText = 'ON'; + @Input() offText = 'OFF'; + @Input() label = ''; + @Output() toggle = new EventEmitter(); + onToggle(value: boolean) { + if (value !== this.on) { + this.toggle.emit(value); + } + } } diff --git a/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.ts b/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.ts index 55f5df8..8392136 100644 --- a/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.ts +++ b/src/ts/components/admin/shared/origin-info-remote/origin-info-remote.ts @@ -4,28 +4,28 @@ import { AdminModel } from '../../../services/adminModel'; import { Origin } from '../../../../common/adminInterfaces'; @Component({ - selector: 'origin-info-remote', - templateUrl: 'origin-info-remote.pug', + selector: 'origin-info-remote', + templateUrl: 'origin-info-remote.pug', }) export class OriginInfoRemote implements OnDestroy { - @Input() showName = false; - origin?: Origin; - private _originIP?: string; - private subscription?: Subscription; - constructor(private model: AdminModel) { - } - get originIP() { - return this._originIP; - } - @Input() set originIP(value: string | undefined) { - if (this.originIP !== value) { - this._originIP = value; - this.origin = undefined; - this.subscription && this.subscription.unsubscribe(); - this.subscription = value ? this.model.origins.subscribe(value, origin => this.origin = origin) : undefined; - } - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } + @Input() showName = false; + origin?: Origin; + private _originIP?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get originIP() { + return this._originIP; + } + @Input() set originIP(value: string | undefined) { + if (this.originIP !== value) { + this._originIP = value; + this.origin = undefined; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.origins.subscribe(value, origin => this.origin = origin) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } } diff --git a/src/ts/components/admin/shared/origin-info/origin-info.ts b/src/ts/components/admin/shared/origin-info/origin-info.ts index 71681fb..bbdfcbd 100644 --- a/src/ts/components/admin/shared/origin-info/origin-info.ts +++ b/src/ts/components/admin/shared/origin-info/origin-info.ts @@ -4,20 +4,20 @@ import { AdminModel } from '../../../services/adminModel'; import { countryCodeToName } from '../../../../common/countries'; @Component({ - selector: 'origin-info', - templateUrl: 'origin-info.pug', - styleUrls: ['origin-info.scss'], + selector: 'origin-info', + templateUrl: 'origin-info.pug', + styleUrls: ['origin-info.scss'], }) export class OriginInfo { - @Input() origin?: Origin; - constructor(private model: AdminModel) { - } - get countryName() { - return countryCodeToName[this.origin && this.origin.country || '??'] || 'Unknown'; - } - toggleBan(field: keyof BannedMuted, value: number) { - if (this.origin) { - this.model.updateOrigin({ ip: this.origin.ip, country: this.origin.country, [field]: value }); - } - } + @Input() origin?: Origin; + constructor(private model: AdminModel) { + } + get countryName() { + return countryCodeToName[this.origin && this.origin.country || '??'] || 'Unknown'; + } + toggleBan(field: keyof BannedMuted, value: number) { + if (this.origin) { + this.model.updateOrigin({ ip: this.origin.ip, country: this.origin.country, [field]: value }); + } + } } diff --git a/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.ts b/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.ts index 612c0b0..7c9a82f 100644 --- a/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.ts +++ b/src/ts/components/admin/shared/origin-list-remote/origin-list-remote.ts @@ -4,29 +4,29 @@ import { AdminModel } from '../../../services/adminModel'; import { OriginInfoBase } from '../../../../common/adminInterfaces'; @Component({ - selector: 'origin-list-remote', - templateUrl: 'origin-list-remote.pug', + selector: 'origin-list-remote', + templateUrl: 'origin-list-remote.pug', }) export class OriginListRemote implements OnDestroy { - @Input() limit = 2; - @Input() extended = false; - origins: OriginInfoBase[] = []; - private _accountId?: string; - private subscription?: Subscription; - constructor(private model: AdminModel) { - } - get accountId() { - return this._accountId; - } - @Input() set accountId(value: string | undefined) { - if (this.accountId !== value) { - this._accountId = value; - this.origins = []; - this.subscription && this.subscription.unsubscribe(); - this.subscription = value ? this.model.accountOrigins.subscribe(value, x => this.origins = x || []) : undefined; - } - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } + @Input() limit = 2; + @Input() extended = false; + origins: OriginInfoBase[] = []; + private _accountId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get accountId() { + return this._accountId; + } + @Input() set accountId(value: string | undefined) { + if (this.accountId !== value) { + this._accountId = value; + this.origins = []; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.accountOrigins.subscribe(value, x => this.origins = x || []) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } } diff --git a/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.ts b/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.ts index 1b64bb4..7b24061 100644 --- a/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.ts +++ b/src/ts/components/admin/shared/pony-info-remote/pony-info-remote.ts @@ -4,29 +4,29 @@ import { AdminModel } from '../../../services/adminModel'; import { Character } from '../../../../common/adminInterfaces'; @Component({ - selector: 'pony-info-remote', - templateUrl: 'pony-info-remote.pug', + selector: 'pony-info-remote', + templateUrl: 'pony-info-remote.pug', }) export class PonyInfoRemote implements OnDestroy { - @Input() highlight = false; - @Input() showName = false; - pony?: Character; - private _ponyId?: string; - private subscription?: Subscription; - constructor(private model: AdminModel) { - } - get ponyId() { - return this._ponyId; - } - @Input() set ponyId(value: string | undefined) { - if (this.ponyId !== value) { - this._ponyId = value; - this.pony = undefined; - this.subscription && this.subscription.unsubscribe(); - this.subscription = value ? this.model.ponies.subscribe(value, pony => this.pony = pony) : undefined; - } - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } + @Input() highlight = false; + @Input() showName = false; + pony?: Character; + private _ponyId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get ponyId() { + return this._ponyId; + } + @Input() set ponyId(value: string | undefined) { + if (this.ponyId !== value) { + this._ponyId = value; + this.pony = undefined; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.ponies.subscribe(value, pony => this.pony = pony) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } } diff --git a/src/ts/components/admin/shared/pony-info/pony-info.ts b/src/ts/components/admin/shared/pony-info/pony-info.ts index 9e1c9b2..3f11c1e 100644 --- a/src/ts/components/admin/shared/pony-info/pony-info.ts +++ b/src/ts/components/admin/shared/pony-info/pony-info.ts @@ -5,38 +5,38 @@ import { isForbiddenName } from '../../../../common/security'; import { AdminModel } from '../../../services/adminModel'; @Component({ - selector: 'pony-info', - templateUrl: 'pony-info.pug', - styleUrls: ['pony-info.scss'], + selector: 'pony-info', + templateUrl: 'pony-info.pug', + styleUrls: ['pony-info.scss'], }) export class PonyInfo implements OnChanges { - @Input() pony?: Character; - @Input() highlight = false; - labelClass = 'badge-none'; - private promise?: Promise; - constructor(private model: AdminModel) { - } - get isBadCM() { - return !!this.pony && hasFlag(this.pony.flags, CharacterFlags.BadCM); - } - ngOnChanges() { - if (this.pony) { - if (isForbiddenName(this.pony.name)) { - this.labelClass = 'badge-forbidden'; - } else if (hasFlag(this.pony.flags, CharacterFlags.BadCM)) { - this.labelClass = 'badge-danger'; - } else { - this.labelClass = 'badge-none'; - } - } - } - onShown() { - if (this.pony && !this.pony.ponyInfo && !this.promise) { - this.promise = this.model.getPonyInfo(this.pony) - .finally(() => this.promise = undefined); - } - } - click() { - console.log(this.pony); - } + @Input() pony?: Character; + @Input() highlight = false; + labelClass = 'badge-none'; + private promise?: Promise; + constructor(private model: AdminModel) { + } + get isBadCM() { + return !!this.pony && hasFlag(this.pony.flags, CharacterFlags.BadCM); + } + ngOnChanges() { + if (this.pony) { + if (isForbiddenName(this.pony.name)) { + this.labelClass = 'badge-forbidden'; + } else if (hasFlag(this.pony.flags, CharacterFlags.BadCM)) { + this.labelClass = 'badge-danger'; + } else { + this.labelClass = 'badge-none'; + } + } + } + onShown() { + if (this.pony && !this.pony.ponyInfo && !this.promise) { + this.promise = this.model.getPonyInfo(this.pony) + .finally(() => this.promise = undefined); + } + } + click() { + console.log(this.pony); + } } diff --git a/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.ts b/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.ts index 3cc48e9..f3f9a9f 100644 --- a/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.ts +++ b/src/ts/components/admin/shared/pony-list-remote/pony-list-remote.ts @@ -5,71 +5,71 @@ import { faTrash, faArrowRight } from '../../../../client/icons'; import { Character, PonyIdDateName, DuplicateResult } from '../../../../common/adminInterfaces'; @Component({ - selector: 'pony-list-remote', - templateUrl: 'pony-list-remote.pug', - styleUrls: ['pony-list-remote.scss'], + selector: 'pony-list-remote', + templateUrl: 'pony-list-remote.pug', + styleUrls: ['pony-list-remote.scss'], }) export class PonyListRemote implements OnDestroy { - readonly trashIcon = faTrash; - readonly assignIcon = faArrowRight; - @Input() limit = 10; - @Input() expanded = false; - @Input() deletable = false; - @Input() highlight: (pony: Character) => boolean = () => false; - @Input() duplicates?: DuplicateResult[]; - full = false; - ponies: string[] = []; - loading = false; - private ponyInfos: PonyIdDateName[] = []; - private _accountId?: string; - private subscription?: Subscription; - constructor(private model: AdminModel) { - } - get limitTo() { - return this.full ? 999999 : this.limit; - } - get accountId() { - return this._accountId; - } - @Input() set accountId(value: string | undefined) { - if (this.accountId !== value) { - this._accountId = value; - this.ponies = []; - this.ponyInfos = []; - this.loading = true; - this.subscription && this.subscription.unsubscribe(); - this.subscription = value ? this.model.accountPonies.subscribe(value, (x = []) => { - this.ponyInfos = x; - this.updatePonies(); - this.loading = false; - }) : undefined; - } - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } - remove(characterId: string) { - if (confirm('Are you sure?')) { - this.model.removePony(characterId); - } - } - toggleFull() { - this.full = !this.full; - this.updatePonies(); - } - assignTo(pony: string, account: string) { - this.model.assignPony(pony, account); - } - private updatePonies() { - const compare = this.full ? compareNames : compareDates; - this.ponies = this.ponyInfos.sort(compare).map(p => p.id); - } + readonly trashIcon = faTrash; + readonly assignIcon = faArrowRight; + @Input() limit = 10; + @Input() expanded = false; + @Input() deletable = false; + @Input() highlight: (pony: Character) => boolean = () => false; + @Input() duplicates?: DuplicateResult[]; + full = false; + ponies: string[] = []; + loading = false; + private ponyInfos: PonyIdDateName[] = []; + private _accountId?: string; + private subscription?: Subscription; + constructor(private model: AdminModel) { + } + get limitTo() { + return this.full ? 999999 : this.limit; + } + get accountId() { + return this._accountId; + } + @Input() set accountId(value: string | undefined) { + if (this.accountId !== value) { + this._accountId = value; + this.ponies = []; + this.ponyInfos = []; + this.loading = true; + this.subscription && this.subscription.unsubscribe(); + this.subscription = value ? this.model.accountPonies.subscribe(value, (x = []) => { + this.ponyInfos = x; + this.updatePonies(); + this.loading = false; + }) : undefined; + } + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + remove(characterId: string) { + if (confirm('Are you sure?')) { + this.model.removePony(characterId); + } + } + toggleFull() { + this.full = !this.full; + this.updatePonies(); + } + assignTo(pony: string, account: string) { + this.model.assignPony(pony, account); + } + private updatePonies() { + const compare = this.full ? compareNames : compareDates; + this.ponies = this.ponyInfos.sort(compare).map(p => p.id); + } } export function compareNames(a: { name: string }, b: { name: string }) { - return a.name.localeCompare(b.name); + return a.name.localeCompare(b.name); } export function compareDates(a: { date: number }, b: { date: number }) { - return b.date - a.date; + return b.date - a.date; } diff --git a/src/ts/components/admin/shared/time-field/time-field.ts b/src/ts/components/admin/shared/time-field/time-field.ts index 4ddae25..e8dd379 100644 --- a/src/ts/components/admin/shared/time-field/time-field.ts +++ b/src/ts/components/admin/shared/time-field/time-field.ts @@ -1,10 +1,10 @@ import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; @Component({ - selector: 'time-field', - templateUrl: 'time-field.pug', - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'time-field', + templateUrl: 'time-field.pug', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TimeField { - @Input() time: any; + @Input() time: any; } diff --git a/src/ts/components/admin/shared/ua-info/ua-info.ts b/src/ts/components/admin/shared/ua-info/ua-info.ts index 84c2794..f5c5c86 100644 --- a/src/ts/components/admin/shared/ua-info/ua-info.ts +++ b/src/ts/components/admin/shared/ua-info/ua-info.ts @@ -3,43 +3,43 @@ import { UAParser } from 'ua-parser-js'; import { uaIcons, faQuestionCircle, faGlobe, faDesktop } from '../../../../client/icons'; function icon(value: string | undefined, defaultValue: any): any { - return value && uaIcons[value] || defaultValue; + return value && uaIcons[value] || defaultValue; } const extensions = { - browser: [ - [/(Amigo|YaBrowser)\/([\w\.]+)/i], [UAParser.BROWSER.NAME, UAParser.BROWSER.VERSION] - ], + browser: [ + [/(Amigo|YaBrowser)\/([\w\.]+)/i], [UAParser.BROWSER.NAME, UAParser.BROWSER.VERSION] + ], }; @Component({ - selector: 'ua-info', - templateUrl: 'ua-info.pug', - styles: [`:host { display: inline-block; }`], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'ua-info', + templateUrl: 'ua-info.pug', + styles: [`:host { display: inline-block; }`], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class UAInfo { - osClass?: string; - osVersion?: string; - browserClass?: string; - browserVersion?: string; - deviceClass?: string; - private _userAgent?: string; - @Input() set userAgent(value: string | undefined) { - if (this._userAgent !== value) { - this._userAgent = value; + osClass?: string; + osVersion?: string; + browserClass?: string; + browserVersion?: string; + deviceClass?: string; + private _userAgent?: string; + @Input() set userAgent(value: string | undefined) { + if (this._userAgent !== value) { + this._userAgent = value; - const parser = new UAParser(value, extensions); - const { os, browser, device } = parser.getResult(); + const parser = new UAParser(value, extensions); + const { os, browser, device } = parser.getResult(); - this.osVersion = os.version; - this.browserVersion = (browser.version || '').replace(/\..*$/, ''); - this.osClass = icon(os.name, faQuestionCircle); - this.browserClass = icon(browser.name, faGlobe); - this.deviceClass = icon(device.type, faDesktop); - } - } - get userAgent() { - return this._userAgent; - } + this.osVersion = os.version; + this.browserVersion = (browser.version || '').replace(/\..*$/, ''); + this.osClass = icon(os.name, faQuestionCircle); + this.browserClass = icon(browser.name, faGlobe); + this.deviceClass = icon(device.type, faDesktop); + } + } + get userAgent() { + return this._userAgent; + } } diff --git a/src/ts/components/app/about/about.ts b/src/ts/components/app/about/about.ts index e5470f8..ece1844 100644 --- a/src/ts/components/app/about/about.ts +++ b/src/ts/components/app/about/about.ts @@ -7,25 +7,25 @@ import { SUPPORTER_REWARDS_LIST } from '../../../common/constants'; import { supporterLink, contactEmail } from '../../../client/data'; function toCredit(credit: Credit) { - return { - ...credit, - background: `url(${getUrl('images/avatars.jpg')})`, - position: `${(credit.avatarIndex % 4) * -82}px ${Math.floor(credit.avatarIndex / 4) * -82}px`, - }; + return { + ...credit, + background: `url(${getUrl('images/avatars.jpg')})`, + position: `${(credit.avatarIndex % 4) * -82}px ${Math.floor(credit.avatarIndex / 4) * -82}px`, + }; } @Component({ - selector: 'about', - templateUrl: 'about.pug', - styleUrls: ['about.scss'], + selector: 'about', + templateUrl: 'about.pug', + styleUrls: ['about.scss'], }) export class About { - readonly title = document.title; - readonly emotes = emojis; - readonly credits = CREDITS.map(toCredit); - readonly contributors = CONTRIBUTORS; - readonly changelog = CHANGELOG; - readonly rewards = SUPPORTER_REWARDS_LIST; - readonly patreonLink = supporterLink; - readonly contactEmail = contactEmail; + readonly title = document.title; + readonly emotes = emojis; + readonly credits = CREDITS.map(toCredit); + readonly contributors = CONTRIBUTORS; + readonly changelog = CHANGELOG; + readonly rewards = SUPPORTER_REWARDS_LIST; + readonly patreonLink = supporterLink; + readonly contactEmail = contactEmail; } diff --git a/src/ts/components/app/account/account.ts b/src/ts/components/app/account/account.ts index a6a7183..40f5d8e 100644 --- a/src/ts/components/app/account/account.ts +++ b/src/ts/components/app/account/account.ts @@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { ACCOUNT_NAME_MAX_LENGTH, ACCOUNT_NAME_MIN_LENGTH, HIDES_PER_PAGE } from '../../../common/constants'; import { UpdateAccountData, SocialSiteInfo, OAuthProvider, HiddenPlayer } from '../../../common/interfaces'; import { - toSocialSiteInfo, cleanName, supporterTitle, supporterClass, isSupporterOrPastSupporter, supporterRewards + toSocialSiteInfo, cleanName, supporterTitle, supporterClass, isSupporterOrPastSupporter, supporterRewards } from '../../../client/clientUtils'; import { oauthProviders } from '../../../client/data'; import { Model } from '../../services/model'; @@ -10,121 +10,121 @@ import { getProviderIcon } from '../../shared/sign-in-box/sign-in-box'; import { faStar, faExclamationCircle, faSync } from '../../../client/icons'; @Component({ - selector: 'account', - templateUrl: 'account.pug', - styleUrls: ['account.scss'], + selector: 'account', + templateUrl: 'account.pug', + styleUrls: ['account.scss'], }) export class Account implements OnInit, OnDestroy { - readonly refreshIcon = faSync; - readonly starIcon = faStar; - readonly alertIcon = faExclamationCircle; - readonly providers = oauthProviders.filter(p => !p.disabled); - readonly nameMinLength = ACCOUNT_NAME_MIN_LENGTH; - readonly nameMaxLength = ACCOUNT_NAME_MAX_LENGTH; - readonly hidesPerPage = HIDES_PER_PAGE; - data: UpdateAccountData = { - name: '', - birthdate: '', - }; - sites?: SocialSiteInfo[]; - password?: string; - removingSite?: boolean; - mergeError?: string; - removedAccount?: boolean; - accountError?: string; - accountSaved = false; - hides: HiddenPlayer[] | undefined = undefined; - page = 0; - constructor(private model: Model) { - } - ngOnInit() { - const account = this.account!; - this.sites = account.sites && account.sites.map(toSocialSiteInfo); - this.data = { - name: account.name, - birthdate: account.birthdate, - }; + readonly refreshIcon = faSync; + readonly starIcon = faStar; + readonly alertIcon = faExclamationCircle; + readonly providers = oauthProviders.filter(p => !p.disabled); + readonly nameMinLength = ACCOUNT_NAME_MIN_LENGTH; + readonly nameMaxLength = ACCOUNT_NAME_MAX_LENGTH; + readonly hidesPerPage = HIDES_PER_PAGE; + data: UpdateAccountData = { + name: '', + birthdate: '', + }; + sites?: SocialSiteInfo[]; + password?: string; + removingSite?: boolean; + mergeError?: string; + removedAccount?: boolean; + accountError?: string; + accountSaved = false; + hides: HiddenPlayer[] | undefined = undefined; + page = 0; + constructor(private model: Model) { + } + ngOnInit() { + const account = this.account!; + this.sites = account.sites && account.sites.map(toSocialSiteInfo); + this.data = { + name: account.name, + birthdate: account.birthdate, + }; - this.pageChanged(); - } - ngOnDestroy() { - this.model.mergedAccount = false; - } - pageChanged() { - this.model.getHides(this.page) - .then(result => this.hides = result); - } - get authError() { - return this.model.authError; - } - get mergedAccount() { - return this.model.mergedAccount; - } - get account() { - return this.model.account; - } - get supporter() { - return this.model.supporter; - } - get showSupporter() { - return isSupporterOrPastSupporter(this.account); - } - get canSubmit() { - return this.account && this.data.name && !!cleanName(this.data.name).length; - } - get supporterTitle() { - return supporterTitle(this.account); - } - get supporterClass() { - return supporterClass(this.account); - } - get supporterRewards() { - return supporterRewards(this.account); - } - get showSupporterInfo() { - const account = this.account; - return !!(!this.supporter && account && account.sites && account.sites.some(s => s.provider === 'patreon')); - } - get showAccountAlert() { - return this.model.missingBirthdate; - } - icon(id: string) { - return getProviderIcon(id); - } - submit() { - if (this.canSubmit) { - this.resetAllMessages(); - this.data.name = cleanName(this.data.name).substr(0, ACCOUNT_NAME_MAX_LENGTH); - this.model.updateAccount(this.data) - .catch((e: Error) => this.accountError = e.message) - .then(() => this.accountSaved = true); - } - } - removeSite(site: SocialSiteInfo) { - if (confirm('Are you sure you want to remove this social account ?')) { - this.removingSite = true; - this.resetAllMessages(); - this.model.removeSite(site.id) - .then(() => this.sites = this.account!.sites!.map(toSocialSiteInfo)) - .then(() => this.removedAccount = true) - .catch((e: Error) => this.mergeError = e.message) - .then(() => this.removingSite = false); - } - } - connectSite(provider: OAuthProvider) { - this.model.connectSite(provider); - } - private resetAllMessages() { - this.accountSaved = false; - this.mergeError = undefined; - this.accountError = undefined; - this.removedAccount = false; - this.model.authError = undefined; - this.model.mergedAccount = false; - } - unhidePlayer(player: HiddenPlayer) { - this.model.unhidePlayer(player.id) - .then(() => this.pageChanged()) - .catch((e: Error) => console.error(e)); - } + this.pageChanged(); + } + ngOnDestroy() { + this.model.mergedAccount = false; + } + pageChanged() { + this.model.getHides(this.page) + .then(result => this.hides = result); + } + get authError() { + return this.model.authError; + } + get mergedAccount() { + return this.model.mergedAccount; + } + get account() { + return this.model.account; + } + get supporter() { + return this.model.supporter; + } + get showSupporter() { + return isSupporterOrPastSupporter(this.account); + } + get canSubmit() { + return this.account && this.data.name && !!cleanName(this.data.name).length; + } + get supporterTitle() { + return supporterTitle(this.account); + } + get supporterClass() { + return supporterClass(this.account); + } + get supporterRewards() { + return supporterRewards(this.account); + } + get showSupporterInfo() { + const account = this.account; + return !!(!this.supporter && account && account.sites && account.sites.some(s => s.provider === 'patreon')); + } + get showAccountAlert() { + return this.model.missingBirthdate; + } + icon(id: string) { + return getProviderIcon(id); + } + submit() { + if (this.canSubmit) { + this.resetAllMessages(); + this.data.name = cleanName(this.data.name).substr(0, ACCOUNT_NAME_MAX_LENGTH); + this.model.updateAccount(this.data) + .catch((e: Error) => this.accountError = e.message) + .then(() => this.accountSaved = true); + } + } + removeSite(site: SocialSiteInfo) { + if (confirm('Are you sure you want to remove this social account ?')) { + this.removingSite = true; + this.resetAllMessages(); + this.model.removeSite(site.id) + .then(() => this.sites = this.account!.sites!.map(toSocialSiteInfo)) + .then(() => this.removedAccount = true) + .catch((e: Error) => this.mergeError = e.message) + .then(() => this.removingSite = false); + } + } + connectSite(provider: OAuthProvider) { + this.model.connectSite(provider); + } + private resetAllMessages() { + this.accountSaved = false; + this.mergeError = undefined; + this.accountError = undefined; + this.removedAccount = false; + this.model.authError = undefined; + this.model.mergedAccount = false; + } + unhidePlayer(player: HiddenPlayer) { + this.model.unhidePlayer(player.id) + .then(() => this.pageChanged()) + .catch((e: Error) => console.error(e)); + } } diff --git a/src/ts/components/app/app.module.ts b/src/ts/components/app/app.module.ts index 13f49d7..b3bf7d0 100644 --- a/src/ts/components/app/app.module.ts +++ b/src/ts/components/app/app.module.ts @@ -24,43 +24,43 @@ import { ErrorReporter } from '../services/errorReporter'; import { RollbarErrorReporter } from '../services/rollbarErrorReporter'; export const routes: Routes = [ - { path: '', component: Home }, - { path: 'help', component: Help }, - { path: 'about', component: About }, - { path: 'account', component: Account, canActivate: [AuthGuard] }, - { path: 'character', component: Character, canActivate: [AuthGuard] }, - { path: '**', redirectTo: '/', pathMatch: 'full' }, + { path: '', component: Home }, + { path: 'help', component: Help }, + { path: 'about', component: About }, + { path: 'account', component: Account, canActivate: [AuthGuard] }, + { path: 'character', component: Character, canActivate: [AuthGuard] }, + { path: '**', redirectTo: '/', pathMatch: 'full' }, ]; @NgModule({ - imports: [ - BrowserModule, - RouterModule, - FormsModule, - HttpClientModule, - PopoverModule.forRoot(), - ButtonsModule.forRoot(), - TooltipModule.forRoot(), - // TypeaheadModule.forRoot(), - SharedModule, - RouterModule.forRoot(routes), - FontAwesomeModule, - ], - declarations: [ - App, - Home, - Help, - About, - Account, - Character, - EditorBox, - ], - providers: [ - { provide: RollbarService, useFactory: rollbarFactory }, - { provide: ErrorHandler, useClass: RollbarErrorHandler }, - { provide: ErrorReporter, useClass: RollbarErrorReporter }, - ], - bootstrap: [App], + imports: [ + BrowserModule, + RouterModule, + FormsModule, + HttpClientModule, + PopoverModule.forRoot(), + ButtonsModule.forRoot(), + TooltipModule.forRoot(), + // TypeaheadModule.forRoot(), + SharedModule, + RouterModule.forRoot(routes), + FontAwesomeModule, + ], + declarations: [ + App, + Home, + Help, + About, + Account, + Character, + EditorBox, + ], + providers: [ + { provide: RollbarService, useFactory: rollbarFactory }, + { provide: ErrorHandler, useClass: RollbarErrorHandler }, + { provide: ErrorReporter, useClass: RollbarErrorReporter }, + ], + bootstrap: [App], }) export class AppModule { } diff --git a/src/ts/components/app/app.ts b/src/ts/components/app/app.ts index 8071de1..22b6329 100644 --- a/src/ts/components/app/app.ts +++ b/src/ts/components/app/app.ts @@ -21,190 +21,190 @@ import { findEntityById } from '../../common/worldMap'; import { isSelected } from '../../client/gameUtils'; export function tooltipConfig() { - return Object.assign(new TooltipConfig(), { container: 'body' }); + return Object.assign(new TooltipConfig(), { container: 'body' }); } export function popoverConfig() { - return Object.assign(new PopoverConfig(), { container: 'body' }); + return Object.assign(new PopoverConfig(), { container: 'body' }); } @Component({ - selector: 'pony-town-app', - templateUrl: 'app.pug', - styleUrls: ['app.scss'], - providers: [ - { provide: TooltipConfig, useFactory: tooltipConfig }, - { provide: PopoverConfig, useFactory: popoverConfig }, - ] + selector: 'pony-town-app', + templateUrl: 'app.pug', + styleUrls: ['app.scss'], + providers: [ + { provide: TooltipConfig, useFactory: tooltipConfig }, + { provide: PopoverConfig, useFactory: popoverConfig }, + ] }) export class App implements OnInit, OnDestroy { - @ViewChild('announcer', { static: true }) announcer!: ElementRef; - @ViewChild('announcerText', { static: true }) announcerText!: ElementRef; - @ViewChild('reloadModal', { static: true }) reloadModal!: TemplateRef; - @ViewChild('signInModal', { static: true }) signInModal!: TemplateRef; - readonly version = version; - readonly date = new Date(); - readonly emailIcon = faEnvelope; - readonly twitterIcon = faTwitter; - readonly patreonIcon = faPatreon; - readonly cogIcon = faCog; - readonly homeIcon = faHome; - readonly helpIcon = faGamepad; - readonly aboutIcon = faInfoCircle; - readonly charactersIcon = faHorseHead; - readonly contactEmail = contactEmail; - readonly patreonLink = supporterLink; - readonly twitterLink = twitterLink; - readonly copyright = copyrightName; - private url = location.pathname; - private reloadModalRef?: BsModalRef; - private reloadInterval?: any; - private subscriptions: Subscription[] = []; - constructor( - private modalService: BsModalService, - private gameService: GameService, - private model: Model, - private game: PonyTownGame, - private router: Router, - private activatedRoute: ActivatedRoute, - private installService: InstallService, - private errorReporter: ErrorReporter, - ) { - } - get canInstall() { - return this.installService.canInstall; - } - get loading() { - return this.model.loading; - } - get account() { - return this.model.account; - } - get isMod() { - return this.model.isMod; - } - get notifications() { - return this.game.notifications; - } - get selected() { - return this.gameService.selected; - } - get playing() { - return this.gameService.playing; - } - get showActionBar() { - return this.playing; - } - get editingActions() { - return this.game.editingActions; - } - ngOnInit() { - if (typeof ga !== 'undefined') { - this.subscriptions.push(this.router.events.subscribe(event => { - if (event instanceof NavigationEnd && this.url !== event.url) { - ga('set', 'page', this.url = event.url); - ga('send', 'pageview'); - } - })); - } + @ViewChild('announcer', { static: true }) announcer!: ElementRef; + @ViewChild('announcerText', { static: true }) announcerText!: ElementRef; + @ViewChild('reloadModal', { static: true }) reloadModal!: TemplateRef; + @ViewChild('signInModal', { static: true }) signInModal!: TemplateRef; + readonly version = version; + readonly date = new Date(); + readonly emailIcon = faEnvelope; + readonly twitterIcon = faTwitter; + readonly patreonIcon = faPatreon; + readonly cogIcon = faCog; + readonly homeIcon = faHome; + readonly helpIcon = faGamepad; + readonly aboutIcon = faInfoCircle; + readonly charactersIcon = faHorseHead; + readonly contactEmail = contactEmail; + readonly patreonLink = supporterLink; + readonly twitterLink = twitterLink; + readonly copyright = copyrightName; + private url = location.pathname; + private reloadModalRef?: BsModalRef; + private reloadInterval?: any; + private subscriptions: Subscription[] = []; + constructor( + private modalService: BsModalService, + private gameService: GameService, + private model: Model, + private game: PonyTownGame, + private router: Router, + private activatedRoute: ActivatedRoute, + private installService: InstallService, + private errorReporter: ErrorReporter, + ) { + } + get canInstall() { + return this.installService.canInstall; + } + get loading() { + return this.model.loading; + } + get account() { + return this.model.account; + } + get isMod() { + return this.model.isMod; + } + get notifications() { + return this.game.notifications; + } + get selected() { + return this.gameService.selected; + } + get playing() { + return this.gameService.playing; + } + get showActionBar() { + return this.playing; + } + get editingActions() { + return this.game.editingActions; + } + ngOnInit() { + if (typeof ga !== 'undefined') { + this.subscriptions.push(this.router.events.subscribe(event => { + if (event instanceof NavigationEnd && this.url !== event.url) { + ga('set', 'page', this.url = event.url); + ga('send', 'pageview'); + } + })); + } - if (isBrowserOutdated) { - this.errorReporter.disable(); - } + if (isBrowserOutdated) { + this.errorReporter.disable(); + } - if (!DEVELOPMENT) { - registerServiceWorker(`${host}sw.js`, () => { - this.model.updating = true; - setTimeout(() => { - this.model.updatingTakesLongTime = true; - }, 20 * SECOND); - }); - } + if (!DEVELOPMENT) { + registerServiceWorker(`${host}sw.js`, () => { + this.model.updating = true; + setTimeout(() => { + this.model.updatingTakesLongTime = true; + }, 20 * SECOND); + }); + } - if (DEVELOPMENT) { - this.subscriptions.push(this.game.announcements.subscribe(message => { - (this.announcer.nativeElement as HTMLElement).style.display = 'flex'; - const announcerText = this.announcerText.nativeElement as HTMLElement; - announcerText.textContent = ''; - setTimeout(() => announcerText.textContent = message, 100); - })); - } + if (DEVELOPMENT) { + this.subscriptions.push(this.game.announcements.subscribe(message => { + (this.announcer.nativeElement as HTMLElement).style.display = 'flex'; + const announcerText = this.announcerText.nativeElement as HTMLElement; + announcerText.textContent = ''; + setTimeout(() => announcerText.textContent = message, 100); + })); + } - this.activatedRoute.queryParams.subscribe(({ error, merged, alert }) => { - this.model.authError = error; - this.model.accountAlert = alert; - this.model.mergedAccount = !!merged; - }); + this.activatedRoute.queryParams.subscribe(({ error, merged, alert }) => { + this.model.authError = error; + this.model.accountAlert = alert; + this.model.mergedAccount = !!merged; + }); - this.subscriptions.push(this.model.protectionErrors.subscribe(() => { - this.openReloadModal(); - })); - } - ngOnDestroy() { - this.subscriptions.forEach(s => s.unsubscribe()); - } - @HostListener('window:focus') - focus() { - this.model.verifyAccount(); - } - signIn(provider: OAuthProvider) { - this.model.signIn(provider); - } - signOut() { - this.model.signOut(); - } - openReloadModal() { - if (!this.reloadModalRef) { - this.reloadModalRef = this.modalService.show( - this.reloadModal, { class: 'modal-lg', ignoreBackdropClick: true, keyboard: false }); + this.subscriptions.push(this.model.protectionErrors.subscribe(() => { + this.openReloadModal(); + })); + } + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + @HostListener('window:focus') + focus() { + this.model.verifyAccount(); + } + signIn(provider: OAuthProvider) { + this.model.signIn(provider); + } + signOut() { + this.model.signOut(); + } + openReloadModal() { + if (!this.reloadModalRef) { + this.reloadModalRef = this.modalService.show( + this.reloadModal, { class: 'modal-lg', ignoreBackdropClick: true, keyboard: false }); - this.reloadInterval = setInterval(() => { - if (checkIframeKey('reload-frame', 'gep84r9jshge4g')) { - this.cancelReloadModal(); - } - }, 500); - } - } - cancelReloadModal() { - if (this.reloadModalRef) { - this.reloadModalRef.hide(); - this.reloadModalRef = undefined; - } + this.reloadInterval = setInterval(() => { + if (checkIframeKey('reload-frame', 'gep84r9jshge4g')) { + this.cancelReloadModal(); + } + }, 500); + } + } + cancelReloadModal() { + if (this.reloadModalRef) { + this.reloadModalRef.hide(); + this.reloadModalRef = undefined; + } - clearInterval(this.reloadInterval); - } - chatLogNameClick(chatBox: ChatBox, message: ChatLogMessage) { - if (!message.entityId) { - return; - } + clearInterval(this.reloadInterval); + } + chatLogNameClick(chatBox: ChatBox, message: ChatLogMessage) { + if (!message.entityId) { + return; + } - let entity = findEntityById(this.game.map, message.entityId); + let entity = findEntityById(this.game.map, message.entityId); - if (entity && (!isPony(entity) || entity === this.game.player)) { - return; - } + if (entity && (!isPony(entity) || entity === this.game.player)) { + return; + } - if (!entity) { - entity = { fake: true, type: PONY_TYPE, id: message.entityId, name: message.name } as FakeEntity as any; - } + if (!entity) { + entity = { fake: true, type: PONY_TYPE, id: message.entityId, name: message.name } as FakeEntity as any; + } - if (isSelected(this.game, message.entityId)) { - this.game.whisperTo = entity; - chatBox.setChatType('whisper'); - } else { - this.game.select(entity as Pony); - } - } - messageToFriend(chatBox: ChatBox, friend: Friend) { - if (friend.entityId) { - const entity: any = { id: friend.entityId, name: friend.actualName || 'unknown' }; - this.messageToPony(chatBox, entity); - } - } - messageToPony(chatBox: ChatBox, pony: Entity) { - setTimeout(() => { - this.game.whisperTo = pony; - chatBox.setChatType('whisper'); - }); - } + if (isSelected(this.game, message.entityId)) { + this.game.whisperTo = entity; + chatBox.setChatType('whisper'); + } else { + this.game.select(entity as Pony); + } + } + messageToFriend(chatBox: ChatBox, friend: Friend) { + if (friend.entityId) { + const entity: any = { id: friend.entityId, name: friend.actualName || 'unknown' }; + this.messageToPony(chatBox, entity); + } + } + messageToPony(chatBox: ChatBox, pony: Entity) { + setTimeout(() => { + this.game.whisperTo = pony; + chatBox.setChatType('whisper'); + }); + } } diff --git a/src/ts/components/app/character/character.ts b/src/ts/components/app/character/character.ts index 2b88abd..3a49f1d 100644 --- a/src/ts/components/app/character/character.ts +++ b/src/ts/components/app/character/character.ts @@ -2,13 +2,13 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { clamp } from 'lodash'; import { PLAYER_NAME_MAX_LENGTH, PLAYER_DESC_MAX_LENGTH } from '../../../common/constants'; import { - PonyInfo, PonyObject, PonyState, SocialSiteInfo, ColorExtraSet, ColorExtra, CharacterTag, PonyEye, Eye, Muzzle, - Iris, ExpressionExtra + PonyInfo, PonyObject, PonyState, SocialSiteInfo, ColorExtraSet, ColorExtra, CharacterTag, PonyEye, Eye, Muzzle, + Iris, ExpressionExtra } from '../../../common/interfaces'; import { findById, toInt, cloneDeep, delay } from '../../../common/utils'; import { - SLEEVED_ACCESSORIES, frontHooves, mergedBackManes, mergedManes, mergedFacialHair, mergedEarAccessories, - mergedChestAccessories, mergedFaceAccessories, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories + SLEEVED_ACCESSORIES, frontHooves, mergedBackManes, mergedManes, mergedFacialHair, mergedEarAccessories, + mergedChestAccessories, mergedFaceAccessories, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories } from '../../../client/ponyUtils'; import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers'; import { toPalette, getBaseFill, syncLockedPonyInfo } from '../../../common/ponyInfo'; @@ -33,17 +33,17 @@ const frontHoofTitles = ['', 'Fetlocks', 'Paws', 'Claws', '']; const backHoofTitles = ['', 'Fetlocks', 'Paws', '', '']; const horns = addLabels(sprites.horns, [ - 'None', 'Unicorn horn', 'Short unicorn horn', 'Curved unicorn horn', 'Tiny deer antlers', - 'Short deer antlers', 'Medium deer antlers', 'Large deer antlers', 'Raindeer antlers', 'Goat horns', - 'Ram horns', 'Buffalo horns', 'Moose horns', 'Bug antenna', 'Long unicorn horn', + 'None', 'Unicorn horn', 'Short unicorn horn', 'Curved unicorn horn', 'Tiny deer antlers', + 'Short deer antlers', 'Medium deer antlers', 'Large deer antlers', 'Raindeer antlers', 'Goat horns', + 'Ram horns', 'Buffalo horns', 'Moose horns', 'Bug antenna', 'Long unicorn horn', ]); const wings = addLabels(sprites.wings[0]!, [ - 'None', 'Pegasus wings', 'Bat wings', 'Gryphon wings', 'Bug wings' + 'None', 'Pegasus wings', 'Bat wings', 'Gryphon wings', 'Bug wings' ]); const ears = addLabels(sprites.ears, [ - 'Regular ears', 'Fluffy ears', 'Long feathered ears', 'Bug ears', 'Short feathered ears', 'Deer ears', + 'Regular ears', 'Fluffy ears', 'Long feathered ears', 'Bug ears', 'Short feathered ears', 'Deer ears', ]); const noses = addTitles(sprites.noses[0], ['Pony muzzle', 'Gryphon beak', 'Deer nose']); @@ -51,451 +51,451 @@ const noses = addTitles(sprites.noses[0], ['Pony muzzle', 'Gryphon beak', 'Deer const flyAnimations = [{ ...stand, name: 'fly' }, fly, fly, fly, { ...flyBug, name: 'fly' }]; function eyeSprite(e: PonyEye | undefined) { - return createEyeSprite(e, 0, sprites.defaultPalette); + return createEyeSprite(e, 0, sprites.defaultPalette); } @Component({ - selector: 'character', - templateUrl: 'character.pug', - styleUrls: ['character.scss'], + selector: 'character', + templateUrl: 'character.pug', + styleUrls: ['character.scss'], }) export class Character implements OnInit, OnDestroy { - readonly debug = DEVELOPMENT || BETA; - readonly playIcon = faPlay; - readonly lockIcon = faLock; - readonly saveIcon = faSave; - readonly codeIcon = faCode; - readonly infoIcon = faInfoCircle; - readonly maxNameLength = PLAYER_NAME_MAX_LENGTH; - readonly maxDescLength = PLAYER_DESC_MAX_LENGTH; - readonly horns = horns; - readonly manes = mergedManes; - readonly backManes = mergedBackManes; - readonly tails = sprites.tails[0]; - readonly wings = wings; - readonly ears = ears; - readonly facialHair = mergedFacialHair; - readonly headAccessories = mergedHeadAccessories; - readonly earAccessories = mergedEarAccessories; - readonly faceAccessories = mergedFaceAccessories; - readonly neckAccessories = sprites.neckAccessories[1]; - readonly frontLegAccessories = sprites.frontLegAccessories[1]; - readonly backLegAccessories = sprites.backLegAccessories[1]; - readonly backAccessories = mergedBackAccessories; - readonly chestAccessories = mergedChestAccessories; - readonly sleeveAccessories = sprites.frontLegSleeves[1]; - readonly waistAccessories = sprites.waistAccessories[1]; - readonly extraAccessories = mergedExtraAccessories; - readonly frontHooves = addTitles(frontHooves[1], frontHoofTitles); - readonly backHooves = addTitles(sprites.backLegHooves[1], backHoofTitles); - readonly animations = [ - () => stand, - () => trot, - () => boop, - () => sitDownUp, - () => lieDownUp, - () => flyAnimations[this.previewInfo!.wings!.type || 0], - ]; - readonly eyelashes: ColorExtraSet = sprites.eyeLeft[1]!.map(eyeSprite); - readonly eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite); - readonly eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite); - readonly noses = noses; - readonly heads = sprites.head1[1]; - readonly buttMarkState: ButtMarkEditorState = { - brushType: 'brush', - brush: 'orange', - }; - muzzles: ColorExtraSet; - fangs: ColorExtraSet; - tags: CharacterTag[] = [ - emptyTag, - ]; - state = defaultPonyState(); - saved: PonyInfo[] = []; - activeAnimation = 0; - loaded = false; - playAnimation = true; - deleting = false; - fixed = false; - previewExtra = false; - previewPony: PonyObject | undefined = undefined; - sites: SocialSiteInfo[] = []; - error?: string; - canSaveFiles = isFileSaverSupported(); - private savingLocked = false; - private interval?: any; - private syncTimeout?: any; - private animationTime = 0; - constructor(private gameService: GameService, private model: Model) { - this.createMuzzles(); - this.updateMuzzles(); - } - private getMuzzleType() { - return clamp(toInt(this.info && this.info.nose && this.info.nose.type), 0, sprites.noses[0].length); - } - createMuzzles() { - const type = this.getMuzzleType(); - const happy = sprites.noses[0][type][0]; + readonly debug = DEVELOPMENT || BETA; + readonly playIcon = faPlay; + readonly lockIcon = faLock; + readonly saveIcon = faSave; + readonly codeIcon = faCode; + readonly infoIcon = faInfoCircle; + readonly maxNameLength = PLAYER_NAME_MAX_LENGTH; + readonly maxDescLength = PLAYER_DESC_MAX_LENGTH; + readonly horns = horns; + readonly manes = mergedManes; + readonly backManes = mergedBackManes; + readonly tails = sprites.tails[0]; + readonly wings = wings; + readonly ears = ears; + readonly facialHair = mergedFacialHair; + readonly headAccessories = mergedHeadAccessories; + readonly earAccessories = mergedEarAccessories; + readonly faceAccessories = mergedFaceAccessories; + readonly neckAccessories = sprites.neckAccessories[1]; + readonly frontLegAccessories = sprites.frontLegAccessories[1]; + readonly backLegAccessories = sprites.backLegAccessories[1]; + readonly backAccessories = mergedBackAccessories; + readonly chestAccessories = mergedChestAccessories; + readonly sleeveAccessories = sprites.frontLegSleeves[1]; + readonly waistAccessories = sprites.waistAccessories[1]; + readonly extraAccessories = mergedExtraAccessories; + readonly frontHooves = addTitles(frontHooves[1], frontHoofTitles); + readonly backHooves = addTitles(sprites.backLegHooves[1], backHoofTitles); + readonly animations = [ + () => stand, + () => trot, + () => boop, + () => sitDownUp, + () => lieDownUp, + () => flyAnimations[this.previewInfo!.wings!.type || 0], + ]; + readonly eyelashes: ColorExtraSet = sprites.eyeLeft[1]!.map(eyeSprite); + readonly eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite); + readonly eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite); + readonly noses = noses; + readonly heads = sprites.head1[1]; + readonly buttMarkState: ButtMarkEditorState = { + brushType: 'brush', + brush: 'orange', + }; + muzzles: ColorExtraSet; + fangs: ColorExtraSet; + tags: CharacterTag[] = [ + emptyTag, + ]; + state = defaultPonyState(); + saved: PonyInfo[] = []; + activeAnimation = 0; + loaded = false; + playAnimation = true; + deleting = false; + fixed = false; + previewExtra = false; + previewPony: PonyObject | undefined = undefined; + sites: SocialSiteInfo[] = []; + error?: string; + canSaveFiles = isFileSaverSupported(); + private savingLocked = false; + private interval?: any; + private syncTimeout?: any; + private animationTime = 0; + constructor(private gameService: GameService, private model: Model) { + this.createMuzzles(); + this.updateMuzzles(); + } + private getMuzzleType() { + return clamp(toInt(this.info && this.info.nose && this.info.nose.type), 0, sprites.noses[0].length); + } + createMuzzles() { + const type = this.getMuzzleType(); + const happy = sprites.noses[0][type][0]; - this.muzzles = sprites.noses - .slice() - .map(n => n[type][0]) - .map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palettes: [sprites.defaultPalette] } as ColorExtra)); + this.muzzles = sprites.noses + .slice() + .map(n => n[type][0]) + .map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palettes: [sprites.defaultPalette] } as ColorExtra)); - this.fangs = [undefined, { color: happy.color, colors: 3, extra: happy.fangs, palettes: [sprites.defaultPalette] }]; - } - updateMuzzles() { - const type = this.getMuzzleType(); - const happy = sprites.noses[0][type][0]; + this.fangs = [undefined, { color: happy.color, colors: 3, extra: happy.fangs, palettes: [sprites.defaultPalette] }]; + } + updateMuzzles() { + const type = this.getMuzzleType(); + const happy = sprites.noses[0][type][0]; - this.muzzles!.forEach((m, i) => { - if (m) { - const { color, colors, mouth } = sprites.noses[i][type][0]; - m.color = color; - m.colors = colors; - m.extra = mouth; - m.timestamp = Date.now(); - } - }); + this.muzzles!.forEach((m, i) => { + if (m) { + const { color, colors, mouth } = sprites.noses[i][type][0]; + m.color = color; + m.colors = colors; + m.extra = mouth; + m.timestamp = Date.now(); + } + }); - const fangs = this.fangs![1]!; - fangs.color = happy.color; - fangs.extra = happy.fangs; - fangs.timestamp = Date.now(); - } - get account() { - return this.model.account; - } - get loading() { - return this.model.loading || this.model.updating; - } - get updateWarning() { - return this.gameService.updateWarning; - } - get playing() { - return this.gameService.playing; - } - get previewInfo() { - return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo; - } - get previewName() { - return this.previewPony ? this.previewPony.name : this.pony.name; - } - get previewTag() { - return getPonyTag(this.previewPony || this.pony, this.account); - } - get customOutlines() { - return this.info.customOutlines; - } - get ponies() { - return this.model.ponies; - } - get pony() { - return this.model.pony; - } - set pony(value: PonyObject) { - this.model.selectPony(value); - } - get info() { - return this.pony.ponyInfo!; - } - get maneFill() { - return getBaseFill(this.info.mane); - } - get coatFill() { - return this.info.coatFill; - } - get hoovesFill() { - return getBaseFill(this.info.frontHooves); - } - get canExport() { - return DEVELOPMENT; - } - get site() { - return findById(this.sites, this.pony.site) || this.sites[0]; - } - set site(value: SocialSiteInfo) { - this.pony.site = value.id; - } - get tag() { - return findById(this.tags, this.pony.tag) || this.tags[0]; - } - set tag(value: CharacterTag) { - this.pony.tag = value.id; - } - get lockEyeWhites() { - return !this.info.unlockEyeWhites; - } - set lockEyeWhites(value) { - this.info.unlockEyeWhites = !value; - } - get darken() { - return !this.info.freeOutlines; - } - get lockFrontLegAccessory() { - return !this.info.unlockFrontLegAccessory; - } - set lockFrontLegAccessory(value) { - this.info.unlockFrontLegAccessory = !value; - } - get lockBackLegAccessory() { - return !this.info.unlockBackLegAccessory; - } - set lockBackLegAccessory(value) { - this.info.unlockBackLegAccessory = !value; - } - get lockEyelashColor() { - return !this.info.unlockEyelashColor; - } - set lockEyelashColor(value) { - this.info.unlockEyelashColor = !value; - } - icon(id: string) { - return getProviderIcon(id); - } - hasSleeves(type: number) { - return SLEEVED_ACCESSORIES.indexOf(type) !== -1; - } - ngOnInit() { - if (this.model.account) { - this.tags.push(...getAvailableTags(this.model.account)); - } + const fangs = this.fangs![1]!; + fangs.color = happy.color; + fangs.extra = happy.fangs; + fangs.timestamp = Date.now(); + } + get account() { + return this.model.account; + } + get loading() { + return this.model.loading || this.model.updating; + } + get updateWarning() { + return this.gameService.updateWarning; + } + get playing() { + return this.gameService.playing; + } + get previewInfo() { + return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo; + } + get previewName() { + return this.previewPony ? this.previewPony.name : this.pony.name; + } + get previewTag() { + return getPonyTag(this.previewPony || this.pony, this.account); + } + get customOutlines() { + return this.info.customOutlines; + } + get ponies() { + return this.model.ponies; + } + get pony() { + return this.model.pony; + } + set pony(value: PonyObject) { + this.model.selectPony(value); + } + get info() { + return this.pony.ponyInfo!; + } + get maneFill() { + return getBaseFill(this.info.mane); + } + get coatFill() { + return this.info.coatFill; + } + get hoovesFill() { + return getBaseFill(this.info.frontHooves); + } + get canExport() { + return DEVELOPMENT; + } + get site() { + return findById(this.sites, this.pony.site) || this.sites[0]; + } + set site(value: SocialSiteInfo) { + this.pony.site = value.id; + } + get tag() { + return findById(this.tags, this.pony.tag) || this.tags[0]; + } + set tag(value: CharacterTag) { + this.pony.tag = value.id; + } + get lockEyeWhites() { + return !this.info.unlockEyeWhites; + } + set lockEyeWhites(value) { + this.info.unlockEyeWhites = !value; + } + get darken() { + return !this.info.freeOutlines; + } + get lockFrontLegAccessory() { + return !this.info.unlockFrontLegAccessory; + } + set lockFrontLegAccessory(value) { + this.info.unlockFrontLegAccessory = !value; + } + get lockBackLegAccessory() { + return !this.info.unlockBackLegAccessory; + } + set lockBackLegAccessory(value) { + this.info.unlockBackLegAccessory = !value; + } + get lockEyelashColor() { + return !this.info.unlockEyelashColor; + } + set lockEyelashColor(value) { + this.info.unlockEyelashColor = !value; + } + icon(id: string) { + return getProviderIcon(id); + } + hasSleeves(type: number) { + return SLEEVED_ACCESSORIES.indexOf(type) !== -1; + } + ngOnInit() { + if (this.model.account) { + this.tags.push(...getAvailableTags(this.model.account)); + } - this.sites = this.model.sites.filter(s => !!s.name); - this.updateMuzzles(); + this.sites = this.model.sites.filter(s => !!s.name); + this.updateMuzzles(); - let last = Date.now(); + let last = Date.now(); - return loadAndInitSpriteSheets().then(() => { - this.loaded = true; - this.interval = setInterval(() => { - const now = Date.now(); - this.update((now - last) / 1000); - last = now; - }, 1000 / 24); - }); - } - ngOnDestroy() { - clearInterval(this.interval); - } - changed() { - if (!this.syncTimeout) { - this.syncTimeout = requestAnimationFrame(() => { - this.syncTimeout = undefined; - syncLockedPonyInfo(this.info); - }); - } + return loadAndInitSpriteSheets().then(() => { + this.loaded = true; + this.interval = setInterval(() => { + const now = Date.now(); + this.update((now - last) / 1000); + last = now; + }, 1000 / 24); + }); + } + ngOnDestroy() { + clearInterval(this.interval); + } + changed() { + if (!this.syncTimeout) { + this.syncTimeout = requestAnimationFrame(() => { + this.syncTimeout = undefined; + syncLockedPonyInfo(this.info); + }); + } - if (DEVELOPMENT || BETA) { - this.state.blushColor = blushColor(parseColorWithAlpha(this.coatFill || '', 1)); - } - } - update(delta: number) { - this.animationTime += delta; + if (DEVELOPMENT || BETA) { + this.state.blushColor = blushColor(parseColorWithAlpha(this.coatFill || '', 1)); + } + } + update(delta: number) { + this.animationTime += delta; - const animation = this.animations[this.activeAnimation](); - this.state.animation = animation; + const animation = this.animations[this.activeAnimation](); + this.state.animation = animation; - if (this.playAnimation) { - this.state.animationFrame = Math.floor(this.animationTime * animation.fps) % animation.frames.length; - } - } - copyCoatColorToTail() { - if (this.info.tail && this.info.tail.fills) { - this.info.tail.fills[0] = this.info.coatFill; - this.changed(); - } - } - eyeColorLockChanged(locked: boolean) { - if (locked) { - this.info.eyeColorLeft = this.info.eyeColorRight; - } - } - eyeWhiteLockChanged(locked: boolean) { - if (locked) { - this.info.eyeWhitesLeft = this.info.eyeWhites; - } - } - eyeOpennessChanged(locked: boolean) { - if (locked) { - this.info.eyeOpennessLeft = this.info.eyeOpennessRight; - } - } - eyelashLockChanged(locked: boolean) { - if (locked) { - this.info.eyelashColorLeft = this.info.eyelashColor; - } - } - select(pony: PonyObject | undefined) { - if (pony) { - this.deleting = false; - this.pony = pony; - } - } - setActiveAnimation(index: number) { - this.activeAnimation = index; - this.animationTime = 0; - } - freeOutlinesChanged(_free: boolean) { - this.changed(); - } - darkenLockedOutlinesChanged(_darken: boolean) { - this.changed(); - } - get canSave() { - return !this.model.pending && !!this.pony && !!this.pony.name && !this.savingLocked; - } - save() { - if (this.canSave) { - this.error = undefined; - this.deleting = false; - this.savingLocked = true; + if (this.playAnimation) { + this.state.animationFrame = Math.floor(this.animationTime * animation.fps) % animation.frames.length; + } + } + copyCoatColorToTail() { + if (this.info.tail && this.info.tail.fills) { + this.info.tail.fills[0] = this.info.coatFill; + this.changed(); + } + } + eyeColorLockChanged(locked: boolean) { + if (locked) { + this.info.eyeColorLeft = this.info.eyeColorRight; + } + } + eyeWhiteLockChanged(locked: boolean) { + if (locked) { + this.info.eyeWhitesLeft = this.info.eyeWhites; + } + } + eyeOpennessChanged(locked: boolean) { + if (locked) { + this.info.eyeOpennessLeft = this.info.eyeOpennessRight; + } + } + eyelashLockChanged(locked: boolean) { + if (locked) { + this.info.eyelashColorLeft = this.info.eyelashColor; + } + } + select(pony: PonyObject | undefined) { + if (pony) { + this.deleting = false; + this.pony = pony; + } + } + setActiveAnimation(index: number) { + this.activeAnimation = index; + this.animationTime = 0; + } + freeOutlinesChanged(_free: boolean) { + this.changed(); + } + darkenLockedOutlinesChanged(_darken: boolean) { + this.changed(); + } + get canSave() { + return !this.model.pending && !!this.pony && !!this.pony.name && !this.savingLocked; + } + save() { + if (this.canSave) { + this.error = undefined; + this.deleting = false; + this.savingLocked = true; - this.model.savePony(this.pony) - .catch((e: Error) => this.error = e.message) - .then(() => delay(2000)) - .then(() => this.savingLocked = false); - } - } - get canRevert() { - return !!findById(this.ponies, this.pony.id); - } - revert() { - if (this.canRevert) { - this.select(findById(this.ponies, this.pony.id)); - } - } - get canDuplicate() { - return this.ponies.length < this.model.characterLimit; - } - duplicate() { - if (this.canDuplicate) { - this.deleting = false; - this.pony = cloneDeep(this.pony); - this.pony.name = ''; - this.pony.id = ''; - } - } - export(index?: number) { - const frameWidth = 80; - const frameHeight = 90; - const animations = index === undefined ? this.animations.map(a => a()) : [this.animations[index]()]; - const frames = animations.reduce((sum, a) => sum + a.frames.length, 0); - const info = toPalette(this.info); - const options = defaultDrawPonyOptions(); + this.model.savePony(this.pony) + .catch((e: Error) => this.error = e.message) + .then(() => delay(2000)) + .then(() => this.savingLocked = false); + } + } + get canRevert() { + return !!findById(this.ponies, this.pony.id); + } + revert() { + if (this.canRevert) { + this.select(findById(this.ponies, this.pony.id)); + } + } + get canDuplicate() { + return this.ponies.length < this.model.characterLimit; + } + duplicate() { + if (this.canDuplicate) { + this.deleting = false; + this.pony = cloneDeep(this.pony); + this.pony.name = ''; + this.pony.id = ''; + } + } + export(index?: number) { + const frameWidth = 80; + const frameHeight = 90; + const animations = index === undefined ? this.animations.map(a => a()) : [this.animations[index]()]; + const frames = animations.reduce((sum, a) => sum + a.frames.length, 0); + const info = toPalette(this.info); + const options = defaultDrawPonyOptions(); - const canvas = drawCanvas(frameWidth * frames, frameHeight, sprites.paletteSpriteSheet, TRANSPARENT, batch => { - let i = 0; + const canvas = drawCanvas(frameWidth * frames, frameHeight, sprites.paletteSpriteSheet, TRANSPARENT, batch => { + let i = 0; - animations.forEach(a => { - for (let f = 0; f < a.frames.length; f++ , i++) { - const state: PonyState = { - ...defaultPonyState(), - animation: a, - animationFrame: f, - blinkFrame: 1, - }; + animations.forEach(a => { + for (let f = 0; f < a.frames.length; f++ , i++) { + const state: PonyState = { + ...defaultPonyState(), + animation: a, + animationFrame: f, + blinkFrame: 1, + }; - drawPony(batch, info, state, i * frameWidth + frameWidth / 2, frameHeight - 10, options); - } - }); - }); + drawPony(batch, info, state, i * frameWidth + frameWidth / 2, frameHeight - 10, options); + } + }); + }); - const name = animations.length === 1 ? animations[0].name : 'all'; - saveCanvas(canvas, `${this.pony.name}-${name}.png`); - } - import() { - if (DEVELOPMENT) { - const data = prompt('enter data'); + const name = animations.length === 1 ? animations[0].name : 'all'; + saveCanvas(canvas, `${this.pony.name}-${name}.png`); + } + import() { + if (DEVELOPMENT) { + const data = prompt('enter data'); - if (data) { - this.importPony(data); - } - } - } - private importPony(data: string) { - if (DEVELOPMENT) { - this.pony.ponyInfo = decompressPonyString(data, true); - const t = decompressPonyString(data, false); - console.log(JSON.stringify(t, undefined, 2)); - } - } - addBlush() { - if (DEVELOPMENT || BETA) { - this.state = { - ...this.state, - expression: createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush), - }; - this.changed(); - } - } - testSize() { - function stringifyValues(values: any[]): string { - return values.map(x => JSON.stringify(x)).join(typeof values[0] === 'object' ? ',\n\t' : ', '); - } + if (data) { + this.importPony(data); + } + } + } + private importPony(data: string) { + if (DEVELOPMENT) { + this.pony.ponyInfo = decompressPonyString(data, true); + const t = decompressPonyString(data, false); + console.log(JSON.stringify(t, undefined, 2)); + } + } + addBlush() { + if (DEVELOPMENT || BETA) { + this.state = { + ...this.state, + expression: createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush), + }; + this.changed(); + } + } + testSize() { + function stringifyValues(values: any[]): string { + return values.map(x => JSON.stringify(x)).join(typeof values[0] === 'object' ? ',\n\t' : ', '); + } - if (DEVELOPMENT) { - const compressed = compressPonyString(this.info); - const regularSize = JSON.stringify(this.info).length; - const ponyInfoNumber = decompressPony(compressed); - const precomp = precompressPony(ponyInfoNumber, BLACK, x => x) as any; - const details = Object.keys(precomp) - .filter(key => key !== 'version') - .map(key => ({ key, values: precomp[key] || [] as any[] })) - .map(({ key, values }) => `${key}: [\n\t${stringifyValues(values)}\n]`) - .join(',\n'); - const serialized = compressPonyString(this.info); + if (DEVELOPMENT) { + const compressed = compressPonyString(this.info); + const regularSize = JSON.stringify(this.info).length; + const ponyInfoNumber = decompressPony(compressed); + const precomp = precompressPony(ponyInfoNumber, BLACK, x => x) as any; + const details = Object.keys(precomp) + .filter(key => key !== 'version') + .map(key => ({ key, values: precomp[key] || [] as any[] })) + .map(({ key, values }) => `${key}: [\n\t${stringifyValues(values)}\n]`) + .join(',\n'); + const serialized = compressPonyString(this.info); - console.log(serialized); - console.log(details); - console.log(`${serialized.length} / ${regularSize}`); - } - } - testJSON() { - if (DEVELOPMENT) { - console.log(JSON.stringify(this.info, undefined, 2)); - } - } - exportPony() { - const data = ponyToExport(this.pony) + '\r\n'; - saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), `${this.pony.name}.txt`); - } - exportPonies() { - const data = this.ponies.map(ponyToExport).join('\r\n') + '\r\n'; - saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), 'ponies.txt'); - } - async importPonies(file: File | undefined) { - if (file) { - const text = await readFileAsText(file); - const lines = text.split(/\r?\n/g); - let imported = 0; + console.log(serialized); + console.log(details); + console.log(`${serialized.length} / ${regularSize}`); + } + } + testJSON() { + if (DEVELOPMENT) { + console.log(JSON.stringify(this.info, undefined, 2)); + } + } + exportPony() { + const data = ponyToExport(this.pony) + '\r\n'; + saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), `${this.pony.name}.txt`); + } + exportPonies() { + const data = this.ponies.map(ponyToExport).join('\r\n') + '\r\n'; + saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), 'ponies.txt'); + } + async importPonies(file: File | undefined) { + if (file) { + const text = await readFileAsText(file); + const lines = text.split(/\r?\n/g); + let imported = 0; - for (const line of lines) { - try { - const [name, info, desc = ''] = line.split(/\t/g); + for (const line of lines) { + try { + const [name, info, desc = ''] = line.split(/\t/g); - if (name && info) { - const pony: PonyObject = { - name, - id: '', - info, - desc, - ponyInfo: decompressPonyString(info, true), - }; + if (name && info) { + const pony: PonyObject = { + name, + id: '', + info, + desc, + ponyInfo: decompressPonyString(info, true), + }; - await this.model.savePony(pony, true); - imported++; - } - } catch (e) { - DEVELOPMENT && console.error(e); - } - } + await this.model.savePony(pony, true); + imported++; + } + } catch (e) { + DEVELOPMENT && console.error(e); + } + } - alert(`Imported ${imported} ponies`); - } - } + alert(`Imported ${imported} ponies`); + } + } } function ponyToExport(pony: PonyObject) { - return `${pony.name}\t${pony.info}\t${pony.desc || ''}`.trim(); + return `${pony.name}\t${pony.info}\t${pony.desc || ''}`.trim(); } diff --git a/src/ts/components/app/editor-box/editor-box.ts b/src/ts/components/app/editor-box/editor-box.ts index 0374952..899d9e9 100644 --- a/src/ts/components/app/editor-box/editor-box.ts +++ b/src/ts/components/app/editor-box/editor-box.ts @@ -9,197 +9,197 @@ import { BLACK } from '../../../common/colors'; import { Entity, Engine, EngineInfo, DebugFlags, tileTypeNames } from '../../../common/interfaces'; @Component({ - selector: 'editor-box', - templateUrl: 'editor-box.pug', + selector: 'editor-box', + templateUrl: 'editor-box.pug', }) export class EditorBox { - readonly dev = DEVELOPMENT; - readonly cogIcon = faCog; - readonly editIcon = faEdit; - readonly selectIcon = faDrawPolygon; - readonly deleteIcon = faTrash; - readonly checkIcon = faCheck; - readonly emptyIcon = emptyIcon; - readonly tiles = ['---', ...tileTypeNames]; - readonly engines = engines; - readonly editorEntities: string[]; - readonly showFields: (keyof DebugFlags)[] = ['id', 'bounds', 'collider', 'cover', 'interact', 'trigger']; - private showEditor = false; - constructor( - public model: Model, - private game: PonyTownGame, - private storage: StorageService, - private zone: NgZone, - ) { - this.game.editor.type = this.storage.getItem('editor-entity') || 'rock'; - this.showEditor = this.storage.getBoolean('show-editor'); - this.editorEntities = getEntityNames().slice().sort(); - } - get editor() { - return this.game.editor; - } - get hasElevation() { - return this.game.engine === Engine.LayeredTiles; - } - get editorElevation() { - return this.game.editor.elevation; - } - get editorSpecial() { - return this.game.editor.special; - } - get editorEntity() { - return this.game.editor.type; - } - set editorEntity(value: string) { - this.game.editor.type = value; - this.storage.setItem('editor-entity', value); - } - get editorTile() { - return this.game.editor.tile; - } - set editorTile(value: number) { - this.game.editor.tile = value; - } - get hasEditor() { - return this.model.isMod && this.showEditor; - } - get oneEntity() { - return this.editor.selectedEntities[0]; - } - get singleEntity() { - return this.editor.selectedEntities.length === 1; - } - get hasSelectedEntities() { - return this.editor.selectedEntities.length > 0; - } - get isLightEntity() { - return this.editor.selectedEntities.some(e => !!e.drawLight); - } - get isLightSpriteEntity() { - return this.editor.selectedEntities.some(e => !!e.drawLightSprite); - } - get selectingEntities() { - return this.game.editor.selectingEntities; - } - set selectingEntities(value) { - this.game.editor.selectingEntities = value; - } - get shadowOpacity() { - return getAlpha(this.game.shadowColor); - } - set shadowOpacity(value) { - this.game.shadowColor = withAlpha(this.game.shadowColor, value); - } - private getEntityName(type: number) { - return getEntityNameFromType(type); - } - private getEntityValue(map: (entity: Entity) => T) { - const entity = this.editor.selectedEntities[0] as any; - return map(entity); - } - get entityName() { - const entities = this.editor.selectedEntities; - const types = entities.map(e => e.type); - const names = uniq(types).map(type => this.getEntityName(type)).join(', '); - return types.length === 1 ? `${names} [${entities[0].id}]` : names; - } - get entityLightColor() { - return colorToHexRGB(this.getEntityValue(e => e && e.lightColor || BLACK)); - } - set entityLightColor(value) { - this.editor.selectedEntities.forEach(e => e.lightColor = parseColor(value)); - } - get entityLightSpriteColor() { - const entity = this.editor.selectedEntities[0]; - return colorToHexRGB(entity && entity.lightSpriteColor || BLACK); - } - set entityLightSpriteColor(value) { - this.editor.selectedEntities.forEach(e => e.lightSpriteColor = parseColor(value)); - } - get entityLightSpriteX() { - const entity = this.editor.selectedEntities[0]; - return entity && entity.lightSpriteX || 0; - } - set entityLightSpriteX(value) { - console.log('set x', value, this.editor.selectedEntities); - this.editor.selectedEntities.forEach(e => e.lightSpriteX = value); - } - get entityLightSpriteY() { - const entity = this.editor.selectedEntities[0]; - return entity && entity.lightSpriteY || 0; - } - set entityLightSpriteY(value) { - this.editor.selectedEntities.forEach(e => e.lightSpriteY = value); - } - get entityLightScale() { - return this.editor.selectedEntities.length ? this.editor.selectedEntities[0].lightScaleAdjust : 1; - } - set entityLightScale(value) { - this.editor.selectedEntities.forEach(e => e.lightScaleAdjust = value); - } - get entityX() { - return this.oneEntity.x; - } - set entityX(value) { - this.oneEntity.x = value; - const { id, x, y } = this.oneEntity; - this.game.send(server => server.editorAction({ - type: 'move', - entities: [{ id, x, y }], - })); - } - get entityY() { - return this.oneEntity.y; - } - set entityY(value) { - this.oneEntity.y = value; - const { id, x, y } = this.oneEntity; - this.game.send(server => server.editorAction({ - type: 'move', - entities: [{ id, x, y }], - })); - } - editorClear() { - this.game.send(server => server.editorAction({ type: 'clear' })); - } - clearLocalStorage() { - this.storage.clear(); - } - setEngine(engine: EngineInfo) { - this.game.engine = engine.engine; - } - isActiveEngine(engine: EngineInfo) { - return this.game.engine === engine.engine; - } - toggleEditor() { - this.zone.run(() => { - this.showEditor = !this.showEditor; - this.storage.setBoolean('show-editor', this.showEditor); - }); - } - toggleSelecting() { - this.selectingEntities = !this.selectingEntities; + readonly dev = DEVELOPMENT; + readonly cogIcon = faCog; + readonly editIcon = faEdit; + readonly selectIcon = faDrawPolygon; + readonly deleteIcon = faTrash; + readonly checkIcon = faCheck; + readonly emptyIcon = emptyIcon; + readonly tiles = ['---', ...tileTypeNames]; + readonly engines = engines; + readonly editorEntities: string[]; + readonly showFields: (keyof DebugFlags)[] = ['id', 'bounds', 'collider', 'cover', 'interact', 'trigger']; + private showEditor = false; + constructor( + public model: Model, + private game: PonyTownGame, + private storage: StorageService, + private zone: NgZone, + ) { + this.game.editor.type = this.storage.getItem('editor-entity') || 'rock'; + this.showEditor = this.storage.getBoolean('show-editor'); + this.editorEntities = getEntityNames().slice().sort(); + } + get editor() { + return this.game.editor; + } + get hasElevation() { + return this.game.engine === Engine.LayeredTiles; + } + get editorElevation() { + return this.game.editor.elevation; + } + get editorSpecial() { + return this.game.editor.special; + } + get editorEntity() { + return this.game.editor.type; + } + set editorEntity(value: string) { + this.game.editor.type = value; + this.storage.setItem('editor-entity', value); + } + get editorTile() { + return this.game.editor.tile; + } + set editorTile(value: number) { + this.game.editor.tile = value; + } + get hasEditor() { + return this.model.isMod && this.showEditor; + } + get oneEntity() { + return this.editor.selectedEntities[0]; + } + get singleEntity() { + return this.editor.selectedEntities.length === 1; + } + get hasSelectedEntities() { + return this.editor.selectedEntities.length > 0; + } + get isLightEntity() { + return this.editor.selectedEntities.some(e => !!e.drawLight); + } + get isLightSpriteEntity() { + return this.editor.selectedEntities.some(e => !!e.drawLightSprite); + } + get selectingEntities() { + return this.game.editor.selectingEntities; + } + set selectingEntities(value) { + this.game.editor.selectingEntities = value; + } + get shadowOpacity() { + return getAlpha(this.game.shadowColor); + } + set shadowOpacity(value) { + this.game.shadowColor = withAlpha(this.game.shadowColor, value); + } + private getEntityName(type: number) { + return getEntityNameFromType(type); + } + private getEntityValue(map: (entity: Entity) => T) { + const entity = this.editor.selectedEntities[0] as any; + return map(entity); + } + get entityName() { + const entities = this.editor.selectedEntities; + const types = entities.map(e => e.type); + const names = uniq(types).map(type => this.getEntityName(type)).join(', '); + return types.length === 1 ? `${names} [${entities[0].id}]` : names; + } + get entityLightColor() { + return colorToHexRGB(this.getEntityValue(e => e && e.lightColor || BLACK)); + } + set entityLightColor(value) { + this.editor.selectedEntities.forEach(e => e.lightColor = parseColor(value)); + } + get entityLightSpriteColor() { + const entity = this.editor.selectedEntities[0]; + return colorToHexRGB(entity && entity.lightSpriteColor || BLACK); + } + set entityLightSpriteColor(value) { + this.editor.selectedEntities.forEach(e => e.lightSpriteColor = parseColor(value)); + } + get entityLightSpriteX() { + const entity = this.editor.selectedEntities[0]; + return entity && entity.lightSpriteX || 0; + } + set entityLightSpriteX(value) { + console.log('set x', value, this.editor.selectedEntities); + this.editor.selectedEntities.forEach(e => e.lightSpriteX = value); + } + get entityLightSpriteY() { + const entity = this.editor.selectedEntities[0]; + return entity && entity.lightSpriteY || 0; + } + set entityLightSpriteY(value) { + this.editor.selectedEntities.forEach(e => e.lightSpriteY = value); + } + get entityLightScale() { + return this.editor.selectedEntities.length ? this.editor.selectedEntities[0].lightScaleAdjust : 1; + } + set entityLightScale(value) { + this.editor.selectedEntities.forEach(e => e.lightScaleAdjust = value); + } + get entityX() { + return this.oneEntity.x; + } + set entityX(value) { + this.oneEntity.x = value; + const { id, x, y } = this.oneEntity; + this.game.send(server => server.editorAction({ + type: 'move', + entities: [{ id, x, y }], + })); + } + get entityY() { + return this.oneEntity.y; + } + set entityY(value) { + this.oneEntity.y = value; + const { id, x, y } = this.oneEntity; + this.game.send(server => server.editorAction({ + type: 'move', + entities: [{ id, x, y }], + })); + } + editorClear() { + this.game.send(server => server.editorAction({ type: 'clear' })); + } + clearLocalStorage() { + this.storage.clear(); + } + setEngine(engine: EngineInfo) { + this.game.engine = engine.engine; + } + isActiveEngine(engine: EngineInfo) { + return this.game.engine === engine.engine; + } + toggleEditor() { + this.zone.run(() => { + this.showEditor = !this.showEditor; + this.storage.setBoolean('show-editor', this.showEditor); + }); + } + toggleSelecting() { + this.selectingEntities = !this.selectingEntities; - if (!this.selectingEntities) { - this.editor.selectedEntities.length = 0; - } - } - listEntities() { - this.game.send(server => server.editorAction({ type: 'list' })); - } - deleteEntities() { - const entities = this.editor.selectedEntities.map(e => e.id); - this.game.send(server => server.editorAction({ type: 'remove', entities })); - this.editor.selectedEntities.length = 0; - } - showEntitiesInfo() { - console.log(this.editor.selectedEntities); - } - toggleShow(field: keyof DebugFlags) { - (this.game.debug as any)[field] = !this.isShow(field); - this.game.saveDebug(); - } - isShow(field: keyof DebugFlags) { - return !!this.game.debug[field]; - } + if (!this.selectingEntities) { + this.editor.selectedEntities.length = 0; + } + } + listEntities() { + this.game.send(server => server.editorAction({ type: 'list' })); + } + deleteEntities() { + const entities = this.editor.selectedEntities.map(e => e.id); + this.game.send(server => server.editorAction({ type: 'remove', entities })); + this.editor.selectedEntities.length = 0; + } + showEntitiesInfo() { + console.log(this.editor.selectedEntities); + } + toggleShow(field: keyof DebugFlags) { + (this.game.debug as any)[field] = !this.isShow(field); + this.game.saveDebug(); + } + isShow(field: keyof DebugFlags) { + return !!this.game.debug[field]; + } } diff --git a/src/ts/components/app/help/help.ts b/src/ts/components/app/help/help.ts index d9afe00..6deec90 100644 --- a/src/ts/components/app/help/help.ts +++ b/src/ts/components/app/help/help.ts @@ -4,16 +4,16 @@ import { faArrowLeft, faArrowRight, faArrowUp, faArrowDown } from '../../../clie import { contactEmail } from '../../../client/data'; @Component({ - selector: 'help', - templateUrl: 'help.pug', - styleUrls: ['help.scss'], + selector: 'help', + templateUrl: 'help.pug', + styleUrls: ['help.scss'], }) export class Help { - readonly leftIcon = faArrowLeft; - readonly rightIcon = faArrowRight; - readonly upIcon = faArrowUp; - readonly downIcon = faArrowDown; - readonly emotes = emojis.map(e => e.names[0]); - readonly mac = /Macintosh/.test(navigator.userAgent); - readonly contactEmail = contactEmail; + readonly leftIcon = faArrowLeft; + readonly rightIcon = faArrowRight; + readonly upIcon = faArrowUp; + readonly downIcon = faArrowDown; + readonly emotes = emojis.map(e => e.names[0]); + readonly mac = /Macintosh/.test(navigator.userAgent); + readonly contactEmail = contactEmail; } diff --git a/src/ts/components/app/home/home.ts b/src/ts/components/app/home/home.ts index faa6478..7c7e82f 100644 --- a/src/ts/components/app/home/home.ts +++ b/src/ts/components/app/home/home.ts @@ -6,51 +6,51 @@ import { InstallService } from '../../services/installService'; import { OAuthProvider, PonyObject } from '../../../common/interfaces'; @Component({ - selector: 'home', - templateUrl: 'home.pug', - styleUrls: ['home.scss'], + selector: 'home', + templateUrl: 'home.pug', + styleUrls: ['home.scss'], }) export class Home { - state = defaultPonyState(); - previewPony: PonyObject | undefined = undefined; - error?: string; - constructor( - private gameService: GameService, - private model: Model, - private installService: InstallService, - ) { - } - get authError() { - return this.model.authError; - } - get accountAlert() { - return this.model.accountAlert; - } - get canInstall() { - return this.installService.canInstall; - } - get playing() { - return this.gameService.playing; - } - get loading() { - return this.model.loading || this.model.updating; - } - get account() { - return this.model.account; - } - get pony() { - return this.model.pony; - } - get previewInfo() { - return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo; - } - get previewName() { - return this.previewPony ? this.previewPony.name : this.pony.name; - } - get previewTag() { - return getPonyTag(this.previewPony || this.pony, this.account); - } - signIn(provider: OAuthProvider) { - this.model.signIn(provider); - } + state = defaultPonyState(); + previewPony: PonyObject | undefined = undefined; + error?: string; + constructor( + private gameService: GameService, + private model: Model, + private installService: InstallService, + ) { + } + get authError() { + return this.model.authError; + } + get accountAlert() { + return this.model.accountAlert; + } + get canInstall() { + return this.installService.canInstall; + } + get playing() { + return this.gameService.playing; + } + get loading() { + return this.model.loading || this.model.updating; + } + get account() { + return this.model.account; + } + get pony() { + return this.model.pony; + } + get previewInfo() { + return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo; + } + get previewName() { + return this.previewPony ? this.previewPony.name : this.pony.name; + } + get previewTag() { + return getPonyTag(this.previewPony || this.pony, this.account); + } + signIn(provider: OAuthProvider) { + this.model.signIn(provider); + } } diff --git a/src/ts/components/services/adminModel.ts b/src/ts/components/services/adminModel.ts index 48b01c9..ffa7572 100644 --- a/src/ts/components/services/adminModel.ts +++ b/src/ts/components/services/adminModel.ts @@ -6,612 +6,612 @@ import { fromNow } from '../../common/utils'; import { DAY, MINUTE } from '../../common/constants'; import { AccountData, AccountCounters } from '../../common/interfaces'; import { - Account, Auth, Character, Origin, OriginInfo, Event, AdminState, UpdateOrigin, AccountFlags, IAdminServerActions, - FindPonyQuery, AuthUpdate, ItemCounts, SupporterFlags, GameServerSettings, Settings, - AccountState, MergeAccountData, FindAccountQuery, ClearOrignsOptions, OriginInfoBase, PonyIdDateName, Stats, BaseValues, + Account, Auth, Character, Origin, OriginInfo, Event, AdminState, UpdateOrigin, AccountFlags, IAdminServerActions, + FindPonyQuery, AuthUpdate, ItemCounts, SupporterFlags, GameServerSettings, Settings, + AccountState, MergeAccountData, FindAccountQuery, ClearOrignsOptions, OriginInfoBase, PonyIdDateName, Stats, BaseValues, } from '../../common/adminInterfaces'; import { ClientAdminActions } from '../../client/clientAdminActions'; import { LiveCollection } from './liveCollection'; import { socketOptions, token } from '../../client/data'; import { getUrl } from '../../client/rev'; import { - formatChat, formatEventDesc, getId, banMessage, parsePonies + formatChat, formatEventDesc, getId, banMessage, parsePonies } from '../../common/adminUtils'; import { StorageService } from './storageService'; import { decompressPonyString } from '../../common/compressPony'; import { ModelSubscriber } from './modelSubscriber'; interface FindPoniesResult { - items: string[]; - totalCount: number; + items: string[]; + totalCount: number; } const notification = window.Notification; function shouldNotify(e: Event) { - return !/^(Spam|Suspicious message|Invalid account|Invite limit reached|Timed out for (swearing|spamming))$/i - .test(e.message); + return !/^(Spam|Suspicious message|Invalid account|Invite limit reached|Timed out for (swearing|spamming))$/i + .test(e.message); } @Injectable({ providedIn: 'root' }) export class AdminModel { - account?: AccountData; - counts: ItemCounts = { - accounts: 0, - characters: 0, - auths: 0, - origins: 0, - }; - initialized = false; - state: AdminState = { - status: { - diskSpace: '', - memoryUsage: '', - certificateExpiration: '', - lastPatreonUpdate: '', - }, - loginServers: [ - { - updating: false, - dead: false, - }, - ], - gameServers: [], - }; - updated?: (list: string, added: boolean) => void; // TODO: remove this - duplicateEntries?: string[]; - error?: string; - log = (..._: any[]) => { }; - get loading(): boolean { - return !this.account; - } - accountPromise!: Promise; - accounts: ModelSubscriber; - origins: ModelSubscriber; - auths: ModelSubscriber; - ponies: ModelSubscriber; - accountAuths: ModelSubscriber; - accountPonies: ModelSubscriber; - accountOrigins: ModelSubscriber; - private liveEvents: LiveCollection; - private handleError = (error: Error) => { - console.error(error); - this.error = error.message; - return undefined; - } - private checkError = (promise: Promise) => promise.catch(this.handleError) as Promise; - private running = true; - private initializedLive = false; - private socket: SocketService; - private resolveAccount!: (account: AccountData) => void; - private initAccountPromise() { - this.accountPromise = new Promise(resolve => { - this.resolveAccount = resolve; - }); - } - constructor(private sanitizer: DomSanitizer, private storage: StorageService, zone: NgZone) { - this.initAccountPromise(); - this.socket = createClientSocket( - { ...socketOptions() }, token, undefined, zone.run.bind(zone)); + account?: AccountData; + counts: ItemCounts = { + accounts: 0, + characters: 0, + auths: 0, + origins: 0, + }; + initialized = false; + state: AdminState = { + status: { + diskSpace: '', + memoryUsage: '', + certificateExpiration: '', + lastPatreonUpdate: '', + }, + loginServers: [ + { + updating: false, + dead: false, + }, + ], + gameServers: [], + }; + updated?: (list: string, added: boolean) => void; // TODO: remove this + duplicateEntries?: string[]; + error?: string; + log = (..._: any[]) => { }; + get loading(): boolean { + return !this.account; + } + accountPromise!: Promise; + accounts: ModelSubscriber; + origins: ModelSubscriber; + auths: ModelSubscriber; + ponies: ModelSubscriber; + accountAuths: ModelSubscriber; + accountPonies: ModelSubscriber; + accountOrigins: ModelSubscriber; + private liveEvents: LiveCollection; + private handleError = (error: Error) => { + console.error(error); + this.error = error.message; + return undefined; + } + private checkError = (promise: Promise) => promise.catch(this.handleError) as Promise; + private running = true; + private initializedLive = false; + private socket: SocketService; + private resolveAccount!: (account: AccountData) => void; + private initAccountPromise() { + this.accountPromise = new Promise(resolve => { + this.resolveAccount = resolve; + }); + } + constructor(private sanitizer: DomSanitizer, private storage: StorageService, zone: NgZone) { + this.initAccountPromise(); + this.socket = createClientSocket( + { ...socketOptions() }, token, undefined, zone.run.bind(zone)); - (window as any).model = this; + (window as any).model = this; - if (this.socket) { - this.socket.client = new ClientAdminActions(this); - this.socket.connect(); - } + if (this.socket) { + this.socket.client = new ClientAdminActions(this); + this.socket.connect(); + } - this.accounts = new ModelSubscriber('accounts', this.socket, { - fix: account => { - account.createdAt = new Date(account.createdAt!); - account.updatedAt = new Date(account.updatedAt!); - account.lastVisit = account.lastVisit && new Date(account.lastVisit); + this.accounts = new ModelSubscriber('accounts', this.socket, { + fix: account => { + account.createdAt = new Date(account.createdAt!); + account.updatedAt = new Date(account.updatedAt!); + account.lastVisit = account.lastVisit && new Date(account.lastVisit); - if (account.alert) { - account.alert.expires = new Date(account.alert.expires); - } - }, - }); + if (account.alert) { + account.alert.expires = new Date(account.alert.expires); + } + }, + }); - this.auths = new ModelSubscriber('auths', this.socket, { - fix: account => { - account.updatedAt = new Date(account.updatedAt!); - account.lastUsed = account.lastUsed && new Date(account.lastUsed); - }, - }); + this.auths = new ModelSubscriber('auths', this.socket, { + fix: account => { + account.updatedAt = new Date(account.updatedAt!); + account.lastUsed = account.lastUsed && new Date(account.lastUsed); + }, + }); - this.ponies = new ModelSubscriber('ponies', this.socket, { - fix: character => { - character.createdAt = new Date(character.createdAt!); - character.updatedAt = new Date(character.updatedAt!); - character.lastUsed = character.lastUsed && new Date(character.lastUsed); - }, - }); + this.ponies = new ModelSubscriber('ponies', this.socket, { + fix: character => { + character.createdAt = new Date(character.createdAt!); + character.updatedAt = new Date(character.updatedAt!); + character.lastUsed = character.lastUsed && new Date(character.lastUsed); + }, + }); - this.origins = new ModelSubscriber('origins', this.socket, {}); + this.origins = new ModelSubscriber('origins', this.socket, {}); - this.accountAuths = new ModelSubscriber('accountAuths', this.socket, {}, []); - this.accountPonies = new ModelSubscriber('accountPonies', this.socket, {}, []); - this.accountOrigins = new ModelSubscriber('accountOrigins', this.socket, {}, []); + this.accountAuths = new ModelSubscriber('accountAuths', this.socket, {}, []); + this.accountPonies = new ModelSubscriber('accountPonies', this.socket, {}, []); + this.accountOrigins = new ModelSubscriber('accountOrigins', this.socket, {}, []); - this.liveEvents = new LiveCollection('events', 1000, getId, { - decode: decodeEvent, - onUpdated: (added, all) => { - if (all.length) { - this.log(`events ${all.length}`); - } + this.liveEvents = new LiveCollection('events', 1000, getId, { + decode: decodeEvent, + onUpdated: (added, all) => { + if (all.length) { + this.log(`events ${all.length}`); + } - all.forEach(e => { - e.descHTML = this.sanitizer.bypassSecurityTrustHtml(formatEventDesc(e.desc)); - }); + all.forEach(e => { + e.descHTML = this.sanitizer.bypassSecurityTrustHtml(formatEventDesc(e.desc)); + }); - this.callUpdated('events', !!added); - this.updateTitle(); + this.callUpdated('events', !!added); + this.updateTitle(); - if (this.notifications) { - added.filter(shouldNotify).forEach(e => this.notify(e.message, e.desc)); - all.filter(e => e.count === 10).forEach(e => this.notify(e.message, e.desc)); - } - }, - onDelete: () => this.updateTitle(), - }, this.socket); + if (this.notifications) { + added.filter(shouldNotify).forEach(e => this.notify(e.message, e.desc)); + all.filter(e => e.count === 10).forEach(e => this.notify(e.message, e.desc)); + } + }, + onDelete: () => this.updateTitle(), + }, this.socket); - if (!this.socket) { - this.initialize(true); - } - } - get server() { - return this.socket.server; - } - get notifications() { - return this.storage.getItem('admin-notifications') === 'true'; - } - get connected() { - return this.socket.isConnected; - } - get events() { - return this.liveEvents.items; - } - get loaded() { - return this.liveEvents.finished; - } - initialize(live: boolean) { - if (this.initializedLive) - return; + if (!this.socket) { + this.initialize(true); + } + } + get server() { + return this.socket.server; + } + get notifications() { + return this.storage.getItem('admin-notifications') === 'true'; + } + get connected() { + return this.socket.isConnected; + } + get events() { + return this.liveEvents.items; + } + get loaded() { + return this.liveEvents.finished; + } + initialize(live: boolean) { + if (this.initializedLive) + return; - notification.requestPermission(); + notification.requestPermission(); - this.initializedLive = true; - this.server.getSignedAccount() - .then(account => { - this.account = account; - this.updateState(); - this.resolveAccount(account); + this.initializedLive = true; + this.server.getSignedAccount() + .then(account => { + this.account = account; + this.updateState(); + this.resolveAccount(account); - if (live) { - setTimeout(() => this.liveEvents.live(), 100); - setInterval(() => this.checkDuplicateEntries(), 60 * MINUTE); - setInterval(() => { - if (this.connected) { - this.getCounts().then(counts => this.counts = counts || this.counts); - } - }, 5 * 1000); - } - }); - } - connectedToSocket() { - this.accounts.connected(); - this.origins.connected(); - this.auths.connected(); - this.ponies.connected(); - this.accountAuths.connected(); - this.accountPonies.connected(); - this.accountOrigins.connected(); - this.updateTitle(); - } - checkDuplicateEntries(force = false) { - return this.server.getDuplicateEntries(force) - .then(entries => this.duplicateEntries = entries || []) - .catch(noop); - } - stop() { - this.running = false; - this.liveEvents.stop(); - } - toggleNotifications() { - this.storage.setItem('admin-notifications', this.notifications ? 'false' : 'true'); - } - // other - getCounts() { - return this.checkError(this.server.getCounts()); - } - getRequestStats() { - return this.checkError(this.server.getRequestStats()); - } - getOtherStats() { - return this.checkError(this.server.getOtherStats()); - } - // auths - getAuth(id: string) { - return this.checkError(this.server.getAuth(id)); - } - getAuthsForAccount(accountId: string) { - return this.checkError(this.server.getAuthsForAccount(accountId)); - } - removeAuth(id: string) { - return this.checkError(this.server.removeAuth(id)); - } - assignAuth(authId: string, accountId: string) { - return this.checkError(this.server.assignAuth(authId, accountId)); - } - updateAuth(id: string, update: AuthUpdate) { - return this.checkError(this.server.updateAuth(id, update)); - } - setAuthPledged(id: string, pledged: number) { - return this.checkError(this.server.updateAuth(id, { pledged })); - } - // ponies - getPoniesCreators(accountId: string) { - return this.checkError(this.server.getPoniesCreators(accountId)); - } - getPoniesForAccount(accountId: string) { - return this.checkError(this.server.getPoniesForAccount(accountId)); - } - getPonyInfo(pony: Character) { - return this.server.getPonyInfo(pony._id) - .then(data => { - if (data) { - pony.info = data.info; - pony.ponyInfo = decompressPonyString(data.info, false); - pony.lastUsed = data.lastUsed ? new Date(data.lastUsed) : undefined; - pony.creator = data.creator; - } - }); - } - removePony(id: string) { - return this.checkError(this.server.removePony(id)); - } - assignPony(ponyId: string, accountId: string) { - return this.checkError(this.server.assignPony(ponyId, accountId)); - } - findPonies(query: FindPonyQuery, page: number, skipTotalCount: boolean): Promise { - return this.checkError(this.server.findPonies(query, page, skipTotalCount)); - } - removePoniesAboveLimit(account: string) { - return this.checkError(this.server.removePoniesAboveLimit(account)); - } - removeAllPonies(account: string) { - return this.checkError(this.server.removeAllPonies(account)); - } - createPony(accountId: string, name: string, info: string) { - return this.checkError(this.server.createPony(accountId, name, info)); - } - restorePonies(accountId: string, chatlog: string, onlyIds?: string[]) { - const ponies = parsePonies(chatlog, onlyIds); - return Promise.all(ponies.map(({ name, info }) => this.createPony(accountId, name, info))); - } - // origins - updateOrigin(origin: UpdateOrigin) { - return this.checkError(this.server.updateOrigin(origin)); - } - removeOriginsForAccount(accountId: string, ips: string[]) { - return this.checkError(this.server.removeOriginsForAccount(accountId, ips)); - } - clearOriginsForAccount(accountId: string, options: ClearOrignsOptions) { - return this.clearOriginsForAccounts([accountId], options); - } - clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions) { - return this.checkError(this.server.clearOriginsForAccounts(accounts, options)); - } - clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions) { - return this.checkError(this.server.clearOrigins(count, andHigher, options)); - } - addOriginToAccount(accountId: string, origin: OriginInfo) { - return this.checkError(this.server.addOriginToAccount(accountId, origin)); - } - getOriginStats() { - return this.checkError(this.server.getOriginStats()); - } - // accounts - getAccount(id: string) { - return this.checkError(this.server.getAccount(id)); - } - getDetailsForAccount(account: Account) { - return this.checkError(this.server.getDetailsForAccount(account._id)); - } - findAccounts(query: FindAccountQuery) { - return this.checkError(this.server.findAccounts(query)); - } - createAccount(name = '') { - return this.checkError(this.server.createAccount(name) - .then(id => (console.log(`created account: [${id}]`), id))); - } - getAccountStatus(accountId: string) { - return this.checkError(this.server.getAccountStatus(accountId)); - } - getAccountAround(accountId: string) { - return this.checkError(this.server.getAccountAround(accountId)); - } - getAccountHidden(accountId: string) { - return this.checkError(this.server.getAccountHidden(accountId)); - } - getAccountFriends(accountId: string) { - return this.checkError(this.server.getAccountFriends(accountId)); - } - getAllDuplicatesQuickInfo(accountId: string) { - return this.checkError(this.server.getAllDuplicatesQuickInfo(accountId)); - } - getAllDuplicates(accountId: string) { - return this.checkError(this.server.getAllDuplicates(accountId)); - } - getAccountsByEmails(emails: string[]) { - return this.checkError(this.server.getAccountsByEmails(emails)); - } - getAccountsByOrigin(ip: string) { - return this.checkError(this.server.getAccountsByOrigin(ip)); - } - removeAccount(accountId: string) { - return this.checkError(this.server.removeAccount(accountId)); - } - setAlert(accountId: string, message: string, expiresIn: number) { - return this.checkError(this.server.setAlert(accountId, message, expiresIn)); - } - setName(accountId: string, name: string) { - return this.checkError(this.server.setName(accountId, name)); - } - setAge(accountId: string, age: number) { - return this.checkError(this.server.setAge(accountId, age)); - } - setRole(accountId: string, role: string, set: boolean) { - return this.checkError(this.server.setRole(accountId, role, set)); - } - setNote(accountId: string, note: string) { - const account = this.accounts.get(accountId); + if (live) { + setTimeout(() => this.liveEvents.live(), 100); + setInterval(() => this.checkDuplicateEntries(), 60 * MINUTE); + setInterval(() => { + if (this.connected) { + this.getCounts().then(counts => this.counts = counts || this.counts); + } + }, 5 * 1000); + } + }); + } + connectedToSocket() { + this.accounts.connected(); + this.origins.connected(); + this.auths.connected(); + this.ponies.connected(); + this.accountAuths.connected(); + this.accountPonies.connected(); + this.accountOrigins.connected(); + this.updateTitle(); + } + checkDuplicateEntries(force = false) { + return this.server.getDuplicateEntries(force) + .then(entries => this.duplicateEntries = entries || []) + .catch(noop); + } + stop() { + this.running = false; + this.liveEvents.stop(); + } + toggleNotifications() { + this.storage.setItem('admin-notifications', this.notifications ? 'false' : 'true'); + } + // other + getCounts() { + return this.checkError(this.server.getCounts()); + } + getRequestStats() { + return this.checkError(this.server.getRequestStats()); + } + getOtherStats() { + return this.checkError(this.server.getOtherStats()); + } + // auths + getAuth(id: string) { + return this.checkError(this.server.getAuth(id)); + } + getAuthsForAccount(accountId: string) { + return this.checkError(this.server.getAuthsForAccount(accountId)); + } + removeAuth(id: string) { + return this.checkError(this.server.removeAuth(id)); + } + assignAuth(authId: string, accountId: string) { + return this.checkError(this.server.assignAuth(authId, accountId)); + } + updateAuth(id: string, update: AuthUpdate) { + return this.checkError(this.server.updateAuth(id, update)); + } + setAuthPledged(id: string, pledged: number) { + return this.checkError(this.server.updateAuth(id, { pledged })); + } + // ponies + getPoniesCreators(accountId: string) { + return this.checkError(this.server.getPoniesCreators(accountId)); + } + getPoniesForAccount(accountId: string) { + return this.checkError(this.server.getPoniesForAccount(accountId)); + } + getPonyInfo(pony: Character) { + return this.server.getPonyInfo(pony._id) + .then(data => { + if (data) { + pony.info = data.info; + pony.ponyInfo = decompressPonyString(data.info, false); + pony.lastUsed = data.lastUsed ? new Date(data.lastUsed) : undefined; + pony.creator = data.creator; + } + }); + } + removePony(id: string) { + return this.checkError(this.server.removePony(id)); + } + assignPony(ponyId: string, accountId: string) { + return this.checkError(this.server.assignPony(ponyId, accountId)); + } + findPonies(query: FindPonyQuery, page: number, skipTotalCount: boolean): Promise { + return this.checkError(this.server.findPonies(query, page, skipTotalCount)); + } + removePoniesAboveLimit(account: string) { + return this.checkError(this.server.removePoniesAboveLimit(account)); + } + removeAllPonies(account: string) { + return this.checkError(this.server.removeAllPonies(account)); + } + createPony(accountId: string, name: string, info: string) { + return this.checkError(this.server.createPony(accountId, name, info)); + } + restorePonies(accountId: string, chatlog: string, onlyIds?: string[]) { + const ponies = parsePonies(chatlog, onlyIds); + return Promise.all(ponies.map(({ name, info }) => this.createPony(accountId, name, info))); + } + // origins + updateOrigin(origin: UpdateOrigin) { + return this.checkError(this.server.updateOrigin(origin)); + } + removeOriginsForAccount(accountId: string, ips: string[]) { + return this.checkError(this.server.removeOriginsForAccount(accountId, ips)); + } + clearOriginsForAccount(accountId: string, options: ClearOrignsOptions) { + return this.clearOriginsForAccounts([accountId], options); + } + clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions) { + return this.checkError(this.server.clearOriginsForAccounts(accounts, options)); + } + clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions) { + return this.checkError(this.server.clearOrigins(count, andHigher, options)); + } + addOriginToAccount(accountId: string, origin: OriginInfo) { + return this.checkError(this.server.addOriginToAccount(accountId, origin)); + } + getOriginStats() { + return this.checkError(this.server.getOriginStats()); + } + // accounts + getAccount(id: string) { + return this.checkError(this.server.getAccount(id)); + } + getDetailsForAccount(account: Account) { + return this.checkError(this.server.getDetailsForAccount(account._id)); + } + findAccounts(query: FindAccountQuery) { + return this.checkError(this.server.findAccounts(query)); + } + createAccount(name = '') { + return this.checkError(this.server.createAccount(name) + .then(id => (console.log(`created account: [${id}]`), id))); + } + getAccountStatus(accountId: string) { + return this.checkError(this.server.getAccountStatus(accountId)); + } + getAccountAround(accountId: string) { + return this.checkError(this.server.getAccountAround(accountId)); + } + getAccountHidden(accountId: string) { + return this.checkError(this.server.getAccountHidden(accountId)); + } + getAccountFriends(accountId: string) { + return this.checkError(this.server.getAccountFriends(accountId)); + } + getAllDuplicatesQuickInfo(accountId: string) { + return this.checkError(this.server.getAllDuplicatesQuickInfo(accountId)); + } + getAllDuplicates(accountId: string) { + return this.checkError(this.server.getAllDuplicates(accountId)); + } + getAccountsByEmails(emails: string[]) { + return this.checkError(this.server.getAccountsByEmails(emails)); + } + getAccountsByOrigin(ip: string) { + return this.checkError(this.server.getAccountsByOrigin(ip)); + } + removeAccount(accountId: string) { + return this.checkError(this.server.removeAccount(accountId)); + } + setAlert(accountId: string, message: string, expiresIn: number) { + return this.checkError(this.server.setAlert(accountId, message, expiresIn)); + } + setName(accountId: string, name: string) { + return this.checkError(this.server.setName(accountId, name)); + } + setAge(accountId: string, age: number) { + return this.checkError(this.server.setAge(accountId, age)); + } + setRole(accountId: string, role: string, set: boolean) { + return this.checkError(this.server.setRole(accountId, role, set)); + } + setNote(accountId: string, note: string) { + const account = this.accounts.get(accountId); - if (account && account.note !== note) { - account.note = note; - account.noteUpdated = new Date(); - } + if (account && account.note !== note) { + account.note = note; + account.noteUpdated = new Date(); + } - return this.checkError(this.server.updateAccount(accountId, { note })); - } - setAccountFlags(accountId: string, flags: AccountFlags) { - return this.checkError(this.server.updateAccount(accountId, { flags })); - } - setSupporterFlags(accountId: string, supporter: SupporterFlags) { - return this.checkError(this.server.updateAccount(accountId, { supporter })); - } - setAccountBanField(accountId: string, field: string, value: number) { - return this.checkError(this.server.updateAccount(accountId, { [field]: value }, banMessage(field, value))); - } - setAccountTimeout(accountId: string, timeout: number) { - return this.checkError(this.server.timeoutAccount(accountId, timeout)); - } - setAccountCounter(accountId: string, name: keyof AccountCounters, value: number) { - return this.checkError(this.server.updateAccountCounter(accountId, name, value)); - } - updateAccount(accountId: string, update: Partial) { - return this.checkError(this.server.updateAccount(accountId, update)); - } - mergeAccounts(accountId: string, withId: string) { - return this.checkError(this.server.mergeAccounts(accountId, withId)); - } - unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData) { - return this.checkError(this.server.unmergeAccounts(accountId, mergeId, split, keep)); - } - addEmail(accountId: string, email: string) { - return this.checkError(this.server.addEmail(accountId, email)); - } - removeEmail(accountId: string, email: string) { - return this.checkError(this.server.removeEmail(accountId, email)); - } - removeIgnore(accountId: string, ignore: string) { - return this.checkError(this.server.removeIgnore(accountId, ignore)); - } - addIgnores(accountId: string, ignores: string[]) { - return this.checkError(this.server.addIgnores(accountId, ignores)); - } - removeFriend(accountId: string, friendId: string) { - return this.checkError(this.server.removeFriend(accountId, friendId)); - } - addFriend(accountId: string, friendId: string) { - return this.checkError(this.server.addFriend(accountId, friendId)); - } - setAccountState(accountId: string, state: AccountState) { - return this.checkError(this.server.setAccountState(accountId, state)); - } - getIgnoresAndIgnoredBy(accountId: string) { - return this.checkError(this.server.getIgnoresAndIgnoredBy(accountId)); - } - clearSessions(accountId: string) { - return this.checkError(this.server.clearSessions(accountId)); - } - // events - removeEvent(eventId: string) { - // return this.checkError(this.server.removeEvent(eventId)) - return this.liveEvents.remove(eventId) - .then(() => this.callUpdated('events', false)) - .then(() => this.updateTitle()) - .catch(this.handleError); - } - cleanupDeletedEvents() { - if (remove(this.events, e => e.deleted).length) { - this.callUpdated('events', false); - this.updateTitle(); - } - } - // state - updateSettings(settings: Partial) { - return this.server.updateSettings(settings) - .then(() => this.updateState()); - } - updateGameServerSettings(serverId: string, settings: Partial) { - return this.server.updateGameServerSettings(serverId, settings) - .then(() => this.updateState()); - } - report(accountId: string) { - return this.checkError(this.server.report(accountId)); - } - action(action: string, accountId: string) { - return this.checkError(this.server.action(action, accountId)); - } - kick(accountId: string) { - return this.checkError(this.server.kick(accountId)); - } - kickAll(serverId: string) { - return this.server.kickAll(serverId) - .then(() => this.updateState()); - } - getChat(search: string, date?: string, caseInsensitive = false) { - date = date || (new Date()).toISOString(); - return search ? this.server.getChat(search, date, caseInsensitive) : Promise.resolve(''); - } - getChatForAccounts(accountIds: string[], date?: string) { - date = date || (new Date()).toISOString(); - return accountIds.length ? this.server.getChatForAccounts(accountIds, date) : Promise.resolve(''); - } - searchFormattedChat(search: string, date?: string) { - return this.formatChat(this.getChat(search, date, true)); - } - accountsFormattedChat(accountIds: string[], date?: string) { - return this.formatChat(this.getChatForAccounts(accountIds, date)); - } - private formatChat(promise: Promise) { - return this.checkError(promise) - .then(chat => chat === undefined ? 'ERROR' : chat) - .then(raw => ({ raw, html: formatChat(raw) })); - } - fetchServerStats(serverId: string) { - return this.checkError(this.server.fetchServerStats(serverId)); - } - fetchServerStatsTable(serverId: string, stats: Stats) { - return this.checkError(this.server.fetchServerStatsTable(serverId, stats)); - } - notifyUpdate(server: string) { - return this.server.notifyUpdate(server) - .then(() => this.updateState()) - .catch(this.handleError); - } - shutdownServers(server: string) { - return this.server.shutdownServers(server) - .then(() => this.updateState()) - .catch(this.handleError); - } - resetUpdating(server: string) { - return this.server.resetUpdating(server) - .then(() => this.updateState()) - .catch(this.handleError); - } - resetSupporter(accountId: string) { - return this.server.resetSupporter(accountId) - .catch(this.handleError); - } - getLastPatreonData() { - return this.server.getLastPatreonData() - .catch(this.handleError); - } - updatePastSupporters() { - return this.server.updatePastSupporters() - .catch(this.handleError); - } - // other - getTimings(server: string) { - return this.server.getTimings(server) - .catch(this.handleError); - } - teleportTo(accountId: string) { - return this.server.teleportTo(accountId) - .catch(this.handleError); - } - // helpers - get isLowDiskSpace() { - return parseInt(this.state.status.diskSpace || '0', 10) > 95; - } - get isLowMemory() { - return parseInt(this.state.status.memoryUsage || '0', 10) > 90; - } - get isOldCertificate() { - const date = this.state.status.certificateExpiration; - return date && (new Date(date)).getTime() < fromNow(7 * DAY).getTime(); - } - get isOldPatreon() { - const date = this.state.status.lastPatreonUpdate; - return date && (new Date(date)).getTime() < fromNow(-21 * MINUTE).getTime(); - } - private requestState() { - return this.socket.isConnected ? this.server.getState().then(s => this.readState(s)) : Promise.resolve(); - } - private readState(state: AdminState) { - merge(this.state, state); - this.initialized = true; - this.updateTitle(); - } - private updateStateTimeout: any; - private updateState(): void { - if (!this.running) - return; + return this.checkError(this.server.updateAccount(accountId, { note })); + } + setAccountFlags(accountId: string, flags: AccountFlags) { + return this.checkError(this.server.updateAccount(accountId, { flags })); + } + setSupporterFlags(accountId: string, supporter: SupporterFlags) { + return this.checkError(this.server.updateAccount(accountId, { supporter })); + } + setAccountBanField(accountId: string, field: string, value: number) { + return this.checkError(this.server.updateAccount(accountId, { [field]: value }, banMessage(field, value))); + } + setAccountTimeout(accountId: string, timeout: number) { + return this.checkError(this.server.timeoutAccount(accountId, timeout)); + } + setAccountCounter(accountId: string, name: keyof AccountCounters, value: number) { + return this.checkError(this.server.updateAccountCounter(accountId, name, value)); + } + updateAccount(accountId: string, update: Partial) { + return this.checkError(this.server.updateAccount(accountId, update)); + } + mergeAccounts(accountId: string, withId: string) { + return this.checkError(this.server.mergeAccounts(accountId, withId)); + } + unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData) { + return this.checkError(this.server.unmergeAccounts(accountId, mergeId, split, keep)); + } + addEmail(accountId: string, email: string) { + return this.checkError(this.server.addEmail(accountId, email)); + } + removeEmail(accountId: string, email: string) { + return this.checkError(this.server.removeEmail(accountId, email)); + } + removeIgnore(accountId: string, ignore: string) { + return this.checkError(this.server.removeIgnore(accountId, ignore)); + } + addIgnores(accountId: string, ignores: string[]) { + return this.checkError(this.server.addIgnores(accountId, ignores)); + } + removeFriend(accountId: string, friendId: string) { + return this.checkError(this.server.removeFriend(accountId, friendId)); + } + addFriend(accountId: string, friendId: string) { + return this.checkError(this.server.addFriend(accountId, friendId)); + } + setAccountState(accountId: string, state: AccountState) { + return this.checkError(this.server.setAccountState(accountId, state)); + } + getIgnoresAndIgnoredBy(accountId: string) { + return this.checkError(this.server.getIgnoresAndIgnoredBy(accountId)); + } + clearSessions(accountId: string) { + return this.checkError(this.server.clearSessions(accountId)); + } + // events + removeEvent(eventId: string) { + // return this.checkError(this.server.removeEvent(eventId)) + return this.liveEvents.remove(eventId) + .then(() => this.callUpdated('events', false)) + .then(() => this.updateTitle()) + .catch(this.handleError); + } + cleanupDeletedEvents() { + if (remove(this.events, e => e.deleted).length) { + this.callUpdated('events', false); + this.updateTitle(); + } + } + // state + updateSettings(settings: Partial) { + return this.server.updateSettings(settings) + .then(() => this.updateState()); + } + updateGameServerSettings(serverId: string, settings: Partial) { + return this.server.updateGameServerSettings(serverId, settings) + .then(() => this.updateState()); + } + report(accountId: string) { + return this.checkError(this.server.report(accountId)); + } + action(action: string, accountId: string) { + return this.checkError(this.server.action(action, accountId)); + } + kick(accountId: string) { + return this.checkError(this.server.kick(accountId)); + } + kickAll(serverId: string) { + return this.server.kickAll(serverId) + .then(() => this.updateState()); + } + getChat(search: string, date?: string, caseInsensitive = false) { + date = date || (new Date()).toISOString(); + return search ? this.server.getChat(search, date, caseInsensitive) : Promise.resolve(''); + } + getChatForAccounts(accountIds: string[], date?: string) { + date = date || (new Date()).toISOString(); + return accountIds.length ? this.server.getChatForAccounts(accountIds, date) : Promise.resolve(''); + } + searchFormattedChat(search: string, date?: string) { + return this.formatChat(this.getChat(search, date, true)); + } + accountsFormattedChat(accountIds: string[], date?: string) { + return this.formatChat(this.getChatForAccounts(accountIds, date)); + } + private formatChat(promise: Promise) { + return this.checkError(promise) + .then(chat => chat === undefined ? 'ERROR' : chat) + .then(raw => ({ raw, html: formatChat(raw) })); + } + fetchServerStats(serverId: string) { + return this.checkError(this.server.fetchServerStats(serverId)); + } + fetchServerStatsTable(serverId: string, stats: Stats) { + return this.checkError(this.server.fetchServerStatsTable(serverId, stats)); + } + notifyUpdate(server: string) { + return this.server.notifyUpdate(server) + .then(() => this.updateState()) + .catch(this.handleError); + } + shutdownServers(server: string) { + return this.server.shutdownServers(server) + .then(() => this.updateState()) + .catch(this.handleError); + } + resetUpdating(server: string) { + return this.server.resetUpdating(server) + .then(() => this.updateState()) + .catch(this.handleError); + } + resetSupporter(accountId: string) { + return this.server.resetSupporter(accountId) + .catch(this.handleError); + } + getLastPatreonData() { + return this.server.getLastPatreonData() + .catch(this.handleError); + } + updatePastSupporters() { + return this.server.updatePastSupporters() + .catch(this.handleError); + } + // other + getTimings(server: string) { + return this.server.getTimings(server) + .catch(this.handleError); + } + teleportTo(accountId: string) { + return this.server.teleportTo(accountId) + .catch(this.handleError); + } + // helpers + get isLowDiskSpace() { + return parseInt(this.state.status.diskSpace || '0', 10) > 95; + } + get isLowMemory() { + return parseInt(this.state.status.memoryUsage || '0', 10) > 90; + } + get isOldCertificate() { + const date = this.state.status.certificateExpiration; + return date && (new Date(date)).getTime() < fromNow(7 * DAY).getTime(); + } + get isOldPatreon() { + const date = this.state.status.lastPatreonUpdate; + return date && (new Date(date)).getTime() < fromNow(-21 * MINUTE).getTime(); + } + private requestState() { + return this.socket.isConnected ? this.server.getState().then(s => this.readState(s)) : Promise.resolve(); + } + private readState(state: AdminState) { + merge(this.state, state); + this.initialized = true; + this.updateTitle(); + } + private updateStateTimeout: any; + private updateState(): void { + if (!this.running) + return; - clearTimeout(this.updateStateTimeout); + clearTimeout(this.updateStateTimeout); - this.requestState() - .catch((e: Error) => console.error(e.stack)) - .then(() => { - this.updateStateTimeout = setTimeout(() => this.updateState(), 1000); - }); - } - private callUpdated(list: string, added: boolean) { - if (this.updated) { - this.updated(list, added); - } - } - updateTitle() { - const ponies = this.state.gameServers.reduce((sum, s) => sum + s.online, 0); - const count = this.events.reduce((sum, e) => sum + (e.deleted ? 0 : 1), 0); - const inred = this.events.reduce((sum, e) => sum + ((!e.deleted && e.count > 9) ? 1 : 0), 0); - const flag = this.isLowDiskSpace || this.isLowMemory || this.isOldCertificate || this.isOldPatreon; - document.title = `${ponies} | ${count}${repeat('!', inred)}${flag ? ' 🚩' : ''}${!this.connected ? ' ⚠' : ''} | Pony Town`; - } - private notify(title: string, body: string) { - if (this.notifications && notification.permission === 'granted') { - const n = new notification(title, { - body: body || '', - icon: getUrl('images/logo-120.png'), - }); + this.requestState() + .catch((e: Error) => console.error(e.stack)) + .then(() => { + this.updateStateTimeout = setTimeout(() => this.updateState(), 1000); + }); + } + private callUpdated(list: string, added: boolean) { + if (this.updated) { + this.updated(list, added); + } + } + updateTitle() { + const ponies = this.state.gameServers.reduce((sum, s) => sum + s.online, 0); + const count = this.events.reduce((sum, e) => sum + (e.deleted ? 0 : 1), 0); + const inred = this.events.reduce((sum, e) => sum + ((!e.deleted && e.count > 9) ? 1 : 0), 0); + const flag = this.isLowDiskSpace || this.isLowMemory || this.isOldCertificate || this.isOldPatreon; + document.title = `${ponies} | ${count}${repeat('!', inred)}${flag ? ' 🚩' : ''}${!this.connected ? ' ⚠' : ''} | Pony Town`; + } + private notify(title: string, body: string) { + if (this.notifications && notification.permission === 'granted') { + const n = new notification(title, { + body: body || '', + icon: getUrl('images/logo-120.png'), + }); - n.onclick = () => { - window.focus(); - n.close(); - }; + n.onclick = () => { + window.focus(); + n.close(); + }; - n.onshow = () => { - setTimeout(() => n.close(), 4000); - }; - } - } + n.onshow = () => { + setTimeout(() => n.close(), 4000); + }; + } + } } function decodeDate(value: number | undefined, base: string | undefined): Date { - if (value == null || base == null) { - return new Date(0); - } else { - const d = new Date(base); - d.setTime(d.getTime() + value); - return d; - } + if (value == null || base == null) { + return new Date(0); + } else { + const d = new Date(base); + d.setTime(d.getTime() + value); + return d; + } } export function decodeEvent(values: any[], base: BaseValues): Event { - return { - _id: values[0], - updatedAt: decodeDate(values[1], base.updatedAt!), - createdAt: decodeDate(values[2], base.createdAt!), - type: values[3], - server: values[4], - message: values[5], - desc: values[6], - count: values[7] | 0, - origin: values[8], - account: values[9], - pony: values[10], - }; + return { + _id: values[0], + updatedAt: decodeDate(values[1], base.updatedAt!), + createdAt: decodeDate(values[2], base.createdAt!), + type: values[3], + server: values[4], + message: values[5], + desc: values[6], + count: values[7] | 0, + origin: values[8], + account: values[9], + pony: values[10], + }; } diff --git a/src/ts/components/services/audio.ts b/src/ts/components/services/audio.ts index 6d5737d..d0190dc 100644 --- a/src/ts/components/services/audio.ts +++ b/src/ts/components/services/audio.ts @@ -5,247 +5,247 @@ import { Season, Holiday, MapType } from '../../common/interfaces'; import { getUrl } from '../../client/rev'; interface Track { - name: string; - src: string[]; - howl?: Howl; + name: string; + src: string[]; + howl?: Howl; } function getTracks(season: Season, holiday: Holiday, map: MapType) { - switch (map) { - case MapType.Island: - return [ - 'island', - 'sunny-island', - ]; - case MapType.House: - return [ - 'happy-house', - 'sweet-home', - ]; - case MapType.Cave: - return [ - 'cave-crystals', - 'cave-secrets', - ]; - default: - return [ - //'largo', - //'musicbox', - //'unrest', - 'bossanova', - 'clop', - 'fivefour', - 'hypnosis', - 'scherzo', - 'trills', - 'waltzalt', - ...(season === Season.Winter ? [ - 'trees-winter', - 'reindeer-winter', - ] : [ - 'trees', - 'reindeer', - ]), - 'season', - 'ambient', - 'building', - 'school', - 'falling', - 'tio', - 'orchid', - ...(season === Season.Winter ? [ - 'xmas-air', - 'xmas-horns', - 'xmas-presents', - ] : []), - ...(holiday === Holiday.Halloween ? [ - 'ghost', - 'pumpkin', - ] : []), - ]; - } + switch (map) { + case MapType.Island: + return [ + 'island', + 'sunny-island', + ]; + case MapType.House: + return [ + 'happy-house', + 'sweet-home', + ]; + case MapType.Cave: + return [ + 'cave-crystals', + 'cave-secrets', + ]; + default: + return [ + //'largo', + //'musicbox', + //'unrest', + 'bossanova', + 'clop', + 'fivefour', + 'hypnosis', + 'scherzo', + 'trills', + 'waltzalt', + ...(season === Season.Winter ? [ + 'trees-winter', + 'reindeer-winter', + ] : [ + 'trees', + 'reindeer', + ]), + 'season', + 'ambient', + 'building', + 'school', + 'falling', + 'tio', + 'orchid', + ...(season === Season.Winter ? [ + 'xmas-air', + 'xmas-horns', + 'xmas-presents', + ] : []), + ...(holiday === Holiday.Halloween ? [ + 'ghost', + 'pumpkin', + ] : []), + ]; + } } const FADE_TRACKS = true; function fadeOut(track: Track, id: number, volume: number) { - const howl = track && track.howl; + const howl = track && track.howl; - if (howl) { - if (FADE_TRACKS) { - howl - .fade(volume, 0, 1000, id) - .once('fade', () => howl.pause(id).stop(id), id); - } else { - howl - .volume(0, id) - .pause(id) - .stop(id); - } - } + if (howl) { + if (FADE_TRACKS) { + howl + .fade(volume, 0, 1000, id) + .once('fade', () => howl.pause(id).stop(id), id); + } else { + howl + .volume(0, id) + .pause(id) + .stop(id); + } + } } function fadeIn(track: Track, id: number, volume: number) { - if (track && track.howl) { - track.howl.fade(0, volume, 1000, id); - } + if (track && track.howl) { + track.howl.fade(0, volume, 1000, id); + } } interface Instance { - id: number; - track: Track; + id: number; + track: Track; } @Injectable({ providedIn: 'root' }) export class Audio { - private tracks: Track[] = []; - private volume = 0; - private loops = 0; - private playing = false; - private stopped: Instance[] = []; - private instance?: Instance; - get trackName() { - return this.instance && this.volume ? this.instance.track.name : ''; - } - initTracks(season: Season, holiday: Holiday, map: MapType) { - const tracks = getTracks(season, holiday, map); + private tracks: Track[] = []; + private volume = 0; + private loops = 0; + private playing = false; + private stopped: Instance[] = []; + private instance?: Instance; + get trackName() { + return this.instance && this.volume ? this.instance.track.name : ''; + } + initTracks(season: Season, holiday: Holiday, map: MapType) { + const tracks = getTracks(season, holiday, map); - // Make new tracks more frequent - // const duplicateTracks = tracks.filter(t => t === 'ghost' || t === 'pumpkin'); - // tracks.push(...duplicateTracks); - // tracks.push(...duplicateTracks); + // Make new tracks more frequent + // const duplicateTracks = tracks.filter(t => t === 'ghost' || t === 'pumpkin'); + // tracks.push(...duplicateTracks); + // tracks.push(...duplicateTracks); - this.tracks = tracks.map(name => ({ name, src: [getUrl(`music/${name}.webm`), getUrl(`music/${name}.mp3`)] })); - this.loops = 0; - } - setVolume(volume: number) { - this.volume = volume / 100; + this.tracks = tracks.map(name => ({ name, src: [getUrl(`music/${name}.webm`), getUrl(`music/${name}.mp3`)] })); + this.loops = 0; + } + setVolume(volume: number) { + this.volume = volume / 100; - if (this.playing) { - if (this.instance) { - this.setInstanceVolume(this.instance, this.volume); - } else if (this.volume) { - this.playRandomTrack(); - } - } - } - play() { - try { - if (!this.playing) { - this.playing = true; + if (this.playing) { + if (this.instance) { + this.setInstanceVolume(this.instance, this.volume); + } else if (this.volume) { + this.playRandomTrack(); + } + } + } + play() { + try { + if (!this.playing) { + this.playing = true; - if (this.volume) { - if (this.instance) { - this.resumeInstance(this.instance); - } else { - this.playRandomTrack(); - } - } - } - } catch (e) { - console.error(e); - } - } - playOrSwitchToRandomTrack() { - if (FADE_TRACKS) { - if (this.playing && this.volume) { - this.playRandomTrack(); - } else { - this.play(); - } - } else { - this.play(); - } - } - stop() { - if (this.playing) { - this.playing = false; - this.stopInstance(this.instance); - } - } - forcePlay() { - if (!this.instance || !this.instance.track.howl!.playing(this.instance.id)) { - this.playRandomTrack(); - } - } - touch() { - this.stopInstances(); - this.setInstanceVolume(this.instance, this.volume); - } - private switchToTrack(track: Track) { - if (this.instance && this.instance.track === track) { - return false; - } else { - this.stopInstance(this.instance); - this.instance = this.playTrack(track); - return true; - } - } - playRandomTrack() { - while (!this.switchToTrack(sample(this.tracks)!)) - ; + if (this.volume) { + if (this.instance) { + this.resumeInstance(this.instance); + } else { + this.playRandomTrack(); + } + } + } + } catch (e) { + console.error(e); + } + } + playOrSwitchToRandomTrack() { + if (FADE_TRACKS) { + if (this.playing && this.volume) { + this.playRandomTrack(); + } else { + this.play(); + } + } else { + this.play(); + } + } + stop() { + if (this.playing) { + this.playing = false; + this.stopInstance(this.instance); + } + } + forcePlay() { + if (!this.instance || !this.instance.track.howl!.playing(this.instance.id)) { + this.playRandomTrack(); + } + } + touch() { + this.stopInstances(); + this.setInstanceVolume(this.instance, this.volume); + } + private switchToTrack(track: Track) { + if (this.instance && this.instance.track === track) { + return false; + } else { + this.stopInstance(this.instance); + this.instance = this.playTrack(track); + return true; + } + } + playRandomTrack() { + while (!this.switchToTrack(sample(this.tracks)!)) + ; - this.loops = random(4, 7); - } - private playTrack(track: Track): Instance { - this.prepareTrack(track); - const id = track.howl!.play(); - fadeIn(track, id, this.volume); - return { id, track }; - } - private resumeInstance({ track, id }: Instance) { - track.howl!.play(id); - fadeIn(track, id, this.volume); - } - private stopInstance(instance: Instance | undefined) { - if (instance) { - this.stopped.push(instance); - } + this.loops = random(4, 7); + } + private playTrack(track: Track): Instance { + this.prepareTrack(track); + const id = track.howl!.play(); + fadeIn(track, id, this.volume); + return { id, track }; + } + private resumeInstance({ track, id }: Instance) { + track.howl!.play(id); + fadeIn(track, id, this.volume); + } + private stopInstance(instance: Instance | undefined) { + if (instance) { + this.stopped.push(instance); + } - this.stopInstances(); - } - private stopInstances() { - this.stopped.forEach(({ track, id }) => fadeOut(track, id, this.volume)); - this.stopped = this.stopped.filter(({ track, id }) => track.howl!.playing(id)); - } - private setInstanceVolume(instance: Instance | undefined, volume: number) { - if (instance) { - const howl = instance.track.howl!; - howl.volume(volume, instance.id); + this.stopInstances(); + } + private stopInstances() { + this.stopped.forEach(({ track, id }) => fadeOut(track, id, this.volume)); + this.stopped = this.stopped.filter(({ track, id }) => track.howl!.playing(id)); + } + private setInstanceVolume(instance: Instance | undefined, volume: number) { + if (instance) { + const howl = instance.track.howl!; + howl.volume(volume, instance.id); - if (volume && !howl.playing(instance.id)) { - howl.play(instance.id); - } else if (!volume && howl.playing(instance.id)) { - howl.pause(instance.id); - } - } - } - private prepareTrack(track: Track) { - if (!track.howl) { - track.howl = new Howl({ - src: track.src, - loop: true, - html5: true, - }); + if (volume && !howl.playing(instance.id)) { + howl.play(instance.id); + } else if (!volume && howl.playing(instance.id)) { + howl.pause(instance.id); + } + } + } + private prepareTrack(track: Track) { + if (!track.howl) { + track.howl = new Howl({ + src: track.src, + loop: true, + html5: true, + }); - track.howl.on('end', id => this.onEnd(id)); - } - } - private handlingOnEnd = 0; - private handlingOnEndAt = 0; - private onEnd(id: number) { - if ( - this.instance && this.instance.id === id && --this.loops < 0 && - (this.handlingOnEnd !== id || this.handlingOnEndAt < performance.now()) - ) { - this.handlingOnEnd = id; - this.handlingOnEndAt = performance.now() + 500; + track.howl.on('end', id => this.onEnd(id)); + } + } + private handlingOnEnd = 0; + private handlingOnEndAt = 0; + private onEnd(id: number) { + if ( + this.instance && this.instance.id === id && --this.loops < 0 && + (this.handlingOnEnd !== id || this.handlingOnEndAt < performance.now()) + ) { + this.handlingOnEnd = id; + this.handlingOnEndAt = performance.now() + 500; - if (this.volume && this.playing) { - this.playRandomTrack(); - } else { - this.stopInstance(this.instance); - } - } - } + if (this.volume && this.playing) { + this.playRandomTrack(); + } else { + this.stopInstance(this.instance); + } + } + } } diff --git a/src/ts/components/services/authGuard.ts b/src/ts/components/services/authGuard.ts index b185f5f..f8f58ef 100644 --- a/src/ts/components/services/authGuard.ts +++ b/src/ts/components/services/authGuard.ts @@ -3,20 +3,20 @@ import { Router, CanActivate } from '@angular/router'; import { Model } from './model'; @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class AuthGuard implements CanActivate { - constructor(private router: Router, private model: Model) { - } - canActivate() { - return this.model.accountPromise - .then(account => { - if (account) { - return true; - } else { - this.router.navigate(['/']); - return false; - } - }) as any; - } + constructor(private router: Router, private model: Model) { + } + canActivate() { + return this.model.accountPromise + .then(account => { + if (account) { + return true; + } else { + this.router.navigate(['/']); + return false; + } + }) as any; + } } diff --git a/src/ts/components/services/errorReporter.ts b/src/ts/components/services/errorReporter.ts index 8c07615..e6f595e 100644 --- a/src/ts/components/services/errorReporter.ts +++ b/src/ts/components/services/errorReporter.ts @@ -4,44 +4,44 @@ import { Person } from '../../common/rollbar'; @Injectable() export class ErrorReporter { - disable() { - } - configureUser(_person: Person) { - } - configureData(_data: any) { - } - captureEvent(_data: any) { - } - reportError(error: any, data?: any) { - console.error(error, data); - } - createClientErrorHandler(socketOptions: ClientOptions): ClientErrorHandler { - const handleRecvError = (error: Error, data: string | Uint8Array) => { - if (error.message) { - let method: string | undefined; + disable() { + } + configureUser(_person: Person) { + } + configureData(_data: any) { + } + captureEvent(_data: any) { + } + reportError(error: any, data?: any) { + console.error(error, data); + } + createClientErrorHandler(socketOptions: ClientOptions): ClientErrorHandler { + const handleRecvError = (error: Error, data: string | Uint8Array) => { + if (error.message) { + let method: string | undefined; - if (data instanceof Uint8Array) { - const bytes: number[] = []; - const length = Math.min(data.length, 200); + if (data instanceof Uint8Array) { + const bytes: number[] = []; + const length = Math.min(data.length, 200); - for (let i = 0; i < length; i++) { - bytes.push(data[i]); - } + for (let i = 0; i < length; i++) { + bytes.push(data[i]); + } - const trail = length < data.length ? '...' : ''; + const trail = length < data.length ? '...' : ''; - if (data.length > 0) { - const item = socketOptions.client[data[0]] as string | [string, any]; - method = typeof item === 'string' ? item : item[0]; - } + if (data.length > 0) { + const item = socketOptions.client[data[0]] as string | [string, any]; + method = typeof item === 'string' ? item : item[0]; + } - data = `<${bytes.toString()}${trail}>`; - } + data = `<${bytes.toString()}${trail}>`; + } - this.reportError(error, { data, method }); - } - }; + this.reportError(error, { data, method }); + } + }; - return { handleRecvError }; - } + return { handleRecvError }; + } } diff --git a/src/ts/components/services/frameService.ts b/src/ts/components/services/frameService.ts index 02ee716..95d05be 100644 --- a/src/ts/components/services/frameService.ts +++ b/src/ts/components/services/frameService.ts @@ -1,38 +1,38 @@ import { Injectable, NgZone } from '@angular/core'; export interface FrameLoop { - init(): void; - destroy(): void; + init(): void; + destroy(): void; } @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class FrameService { - constructor(private zone: NgZone) { - } - create(frame: (delta: number) => void) { - const zone = this.zone; - let ref = 0; - let last = 0; + constructor(private zone: NgZone) { + } + create(frame: (delta: number) => void) { + const zone = this.zone; + let ref = 0; + let last = 0; - function tick(now: number) { - ref = requestAnimationFrame(tick); - frame((now - last) / 1000); - last = now; - } + function tick(now: number) { + ref = requestAnimationFrame(tick); + frame((now - last) / 1000); + last = now; + } - return { - init() { - if (!ref) { - last = performance.now(); - zone.runOutsideAngular(() => ref = requestAnimationFrame(tick)); - } - }, - destroy() { - cancelAnimationFrame(ref); - ref = 0; - } - }; - } + return { + init() { + if (!ref) { + last = performance.now(); + zone.runOutsideAngular(() => ref = requestAnimationFrame(tick)); + } + }, + destroy() { + cancelAnimationFrame(ref); + ref = 0; + } + }; + } } diff --git a/src/ts/components/services/gameService.ts b/src/ts/components/services/gameService.ts index 7376eac..03797cc 100644 --- a/src/ts/components/services/gameService.ts +++ b/src/ts/components/services/gameService.ts @@ -17,296 +17,296 @@ import { StorageService } from './storageService'; export interface ClientSocketService extends SocketService { } function createSocket( - gameService: GameService, game: PonyTownGame, model: Model, zone: NgZone, options: ClientOptions, - token: string, errorHandler: ClientErrorHandler + gameService: GameService, game: PonyTownGame, model: Model, zone: NgZone, options: ClientOptions, + token: string, errorHandler: ClientErrorHandler ): ClientSocketService { - const socket = createClientSocket(options, token, errorHandler); - socket.client = new ClientActions(gameService, game, model, zone); + const socket = createClientSocket(options, token, errorHandler); + socket.client = new ClientActions(gameService, game, model, zone); - if (!socket.supportsBinary) { - throw new Error(BROWSER_NOT_SUPPORTED_ERROR); - } + if (!socket.supportsBinary) { + throw new Error(BROWSER_NOT_SUPPORTED_ERROR); + } - return socket; + return socket; } @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class GameService { - playing = false; - joining = false; - offline = false; - protectionError = false; - rateLimitError = false; - versionError = false; - version?: string; - server?: ServerInfo; - servers: ServerInfo[] = []; - error?: string; - leftMessage?: string; - private safelyLeft = false; - private gameLoop?: GameLoop; - private disconnectedTimeout?: any; - private initialized = false; - private update?: boolean; - private locked = false; - constructor( - private model: Model, - private game: PonyTownGame, - private zone: NgZone, - private errorHandler: ErrorHandler, - private errorReporter: ErrorReporter, - private storage: StorageService, - ) { - this.pollStatus(); - } - get selected() { - return this.game.selected; - } - get account() { - return this.model.account; - } - get canPlay(): boolean { - return !!this.model.pony && - !!this.model.pony.name && - !this.model.pending && - !this.joining && - !!this.server && - !this.server.offline && - !this.rateLimitError && - !this.versionError && - !this.locked; - } - get updateWarning(): boolean { - return !!this.update; - } - get filterSwearWords(): boolean { - return !!(this.server && this.server.filter) - || !!(this.account && this.account.settings && this.account.settings.filterSwearWords); - } - get wasPlaying() { - return this.storage.getBoolean('playing'); - } - join(ponyId: string) { - this.errorReporter.captureEvent({ name: 'Join' }); - const server = this.server; + playing = false; + joining = false; + offline = false; + protectionError = false; + rateLimitError = false; + versionError = false; + version?: string; + server?: ServerInfo; + servers: ServerInfo[] = []; + error?: string; + leftMessage?: string; + private safelyLeft = false; + private gameLoop?: GameLoop; + private disconnectedTimeout?: any; + private initialized = false; + private update?: boolean; + private locked = false; + constructor( + private model: Model, + private game: PonyTownGame, + private zone: NgZone, + private errorHandler: ErrorHandler, + private errorReporter: ErrorReporter, + private storage: StorageService, + ) { + this.pollStatus(); + } + get selected() { + return this.game.selected; + } + get account() { + return this.model.account; + } + get canPlay(): boolean { + return !!this.model.pony && + !!this.model.pony.name && + !this.model.pending && + !this.joining && + !!this.server && + !this.server.offline && + !this.rateLimitError && + !this.versionError && + !this.locked; + } + get updateWarning(): boolean { + return !!this.update; + } + get filterSwearWords(): boolean { + return !!(this.server && this.server.filter) + || !!(this.account && this.account.settings && this.account.settings.filterSwearWords); + } + get wasPlaying() { + return this.storage.getBoolean('playing'); + } + join(ponyId: string) { + this.errorReporter.captureEvent({ name: 'Join' }); + const server = this.server; - if (this.playing || this.joining || !server) { - return Promise.resolve(); - } + if (this.playing || this.joining || !server) { + return Promise.resolve(); + } - if (typeof WebSocket === 'undefined' || typeof Float32Array === 'undefined') { - return Promise.reject(new Error(BROWSER_NOT_SUPPORTED_ERROR)); - } + if (typeof WebSocket === 'undefined' || typeof Float32Array === 'undefined') { + return Promise.reject(new Error(BROWSER_NOT_SUPPORTED_ERROR)); + } - this.joining = true; - this.leftMessage = undefined; - this.safelyLeft = false; + this.joining = true; + this.leftMessage = undefined; + this.safelyLeft = false; - return this.model.join(server.id, ponyId) - .then(({ token, alert }) => { - if (!this.joining) { - return false; - } + return this.model.join(server.id, ponyId) + .then(({ token, alert }) => { + if (!this.joining) { + return false; + } - if (!token) { - this.model.accountAlert = alert; - this.joining = false; - return false; - } + if (!token) { + this.model.accountAlert = alert; + this.joining = false; + return false; + } - return this.zone.runOutsideAngular(() => { - const options = { ...socketOptions(), path: server.path, host: server.host }; - const errorHandler = this.errorReporter.createClientErrorHandler(options); - const socket = createSocket(this, this.game, this.model, this.zone, options, token, errorHandler); + return this.zone.runOutsideAngular(() => { + const options = { ...socketOptions(), path: server.path, host: server.host }; + const errorHandler = this.errorReporter.createClientErrorHandler(options); + const socket = createSocket(this, this.game, this.model, this.zone, options, token, errorHandler); - if (this.gameLoop) { - this.gameLoop.cancel(); - } + if (this.gameLoop) { + this.gameLoop.cancel(); + } - this.game.startup(socket, this.model.isMod); - this.gameLoop = startGameLoop(this.game, e => this.handleGameError(e)); + this.game.startup(socket, this.model.isMod); + this.gameLoop = startGameLoop(this.game, e => this.handleGameError(e)); - return this.gameLoop.started - .then(() => { - this.errorReporter.captureEvent({ name: 'gameLoop.started' }); - const socketConnected = this.pollUntilConnected(socket); - socket.connect(); - return socketConnected; - }) - .then(() => { - this.errorReporter.captureEvent({ name: 'socketConnected' }); - return true; - }) - .catch(e => { - this.errorReporter.captureEvent({ name: 'socket.disconnect()', error: e.message }); - socket.disconnect(); - throw e; - }); - }); - }) - .then(joined => { - this.errorReporter.captureEvent({ name: joined ? 'Joined game' : 'Not joined game' }); - }) - .catch((e: RequestError) => { - this.errorReporter.captureEvent({ name: 'Failed to join game', error: e.message }); + return this.gameLoop.started + .then(() => { + this.errorReporter.captureEvent({ name: 'gameLoop.started' }); + const socketConnected = this.pollUntilConnected(socket); + socket.connect(); + return socketConnected; + }) + .then(() => { + this.errorReporter.captureEvent({ name: 'socketConnected' }); + return true; + }) + .catch(e => { + this.errorReporter.captureEvent({ name: 'socket.disconnect()', error: e.message }); + socket.disconnect(); + throw e; + }); + }); + }) + .then(joined => { + this.errorReporter.captureEvent({ name: joined ? 'Joined game' : 'Not joined game' }); + }) + .catch((e: RequestError) => { + this.errorReporter.captureEvent({ name: 'Failed to join game', error: e.message }); - // if (e.status && e.status > 500 && e.status < 500) { - // this.rateLimitError = true; - // setTimeout(() => this.rateLimitError = false, 5000); - // } + // if (e.status && e.status > 500 && e.status < 500) { + // this.rateLimitError = true; + // setTimeout(() => this.rateLimitError = false, 5000); + // } - this.zone.run(() => this.left('join.catch')); - throw e; - }); - } - leave(reason: string) { - this.errorReporter.captureEvent({ name: 'Leave', reason }); - this.game.leave(); - this.left('leave'); - } - joined() { - this.errorReporter.captureEvent({ name: 'Joined' }); - this.storage.setBoolean('playing', true); - clearTimeout(this.disconnectedTimeout); - setTimeout(() => { - this.joining = false; - this.playing = true; - }); - } - left(from: string, reason = LeaveReason.None) { - this.errorReporter.captureEvent({ name: 'Left', from, reason }); - this.storage.setBoolean('playing', false); - this.safelyLeft = true; + this.zone.run(() => this.left('join.catch')); + throw e; + }); + } + leave(reason: string) { + this.errorReporter.captureEvent({ name: 'Leave', reason }); + this.game.leave(); + this.left('leave'); + } + joined() { + this.errorReporter.captureEvent({ name: 'Joined' }); + this.storage.setBoolean('playing', true); + clearTimeout(this.disconnectedTimeout); + setTimeout(() => { + this.joining = false; + this.playing = true; + }); + } + left(from: string, reason = LeaveReason.None) { + this.errorReporter.captureEvent({ name: 'Left', from, reason }); + this.storage.setBoolean('playing', false); + this.safelyLeft = true; - if (reason === LeaveReason.Swearing) { - this.leftMessage = 'Kicked for swearing or inappropriate language'; - this.locked = true; - } else { - this.leftMessage = undefined; - } + if (reason === LeaveReason.Swearing) { + this.leftMessage = 'Kicked for swearing or inappropriate language'; + this.locked = true; + } else { + this.leftMessage = undefined; + } - if (this.gameLoop) { - this.errorReporter.captureEvent({ name: 'gameLoop.cancel()' }); - this.gameLoop.cancel(); - this.gameLoop = undefined; - } + if (this.gameLoop) { + this.errorReporter.captureEvent({ name: 'gameLoop.cancel()' }); + this.gameLoop.cancel(); + this.gameLoop = undefined; + } - clearTimeout(this.disconnectedTimeout); + clearTimeout(this.disconnectedTimeout); - setTimeout(() => { - this.joining = false; - this.playing = false; - }); + setTimeout(() => { + this.joining = false; + this.playing = false; + }); - if (this.locked) { - setTimeout(() => { - this.locked = false; - }, 7000); - } + if (this.locked) { + setTimeout(() => { + this.locked = false; + }, 7000); + } - if (this.model.friends) { - for (const friend of this.model.friends) { - friend.online = false; - friend.entityId = 0; - } - } + if (this.model.friends) { + for (const friend of this.model.friends) { + friend.online = false; + friend.entityId = 0; + } + } - this.game.release(); - this.game.onLeft.next(); - } - disconnected() { - this.errorReporter.captureEvent({ name: 'Disconnected' }); - clearTimeout(this.disconnectedTimeout); + this.game.release(); + this.game.onLeft.next(); + } + disconnected() { + this.errorReporter.captureEvent({ name: 'Disconnected' }); + clearTimeout(this.disconnectedTimeout); - if (!this.safelyLeft) { - this.disconnectedTimeout = setTimeout(() => this.left('disconnected.timeout'), 10000); - } - } - private pollStatus() { - return this.getAndUpdateStatus(this.account) - .finally(() => { - setTimeout(() => this.pollStatus(), this.initialized ? 10000 : 500); - }); - } - private getAndUpdateStatus(account: AccountData | undefined) { - if (this.joining || this.playing || !account || !isFocused()) { - return Promise.resolve(); - } else { - return this.model.status(this.initialized) - .then(status => this.updateStatus(account, status)) - .catch((e: RequestError) => { - DEVELOPMENT && console.error(e); - this.offline = e.message === OFFLINE_ERROR; - this.versionError = e.message === VERSION_ERROR; - this.protectionError = e.message === PROTECTION_ERROR; - }); - } - } - private updateStatus(account: AccountData, status: GameStatus) { - this.initialized = true; - this.offline = false; - this.version = status.version; - this.update = status.update; + if (!this.safelyLeft) { + this.disconnectedTimeout = setTimeout(() => this.left('disconnected.timeout'), 10000); + } + } + private pollStatus() { + return this.getAndUpdateStatus(this.account) + .finally(() => { + setTimeout(() => this.pollStatus(), this.initialized ? 10000 : 500); + }); + } + private getAndUpdateStatus(account: AccountData | undefined) { + if (this.joining || this.playing || !account || !isFocused()) { + return Promise.resolve(); + } else { + return this.model.status(this.initialized) + .then(status => this.updateStatus(account, status)) + .catch((e: RequestError) => { + DEVELOPMENT && console.error(e); + this.offline = e.message === OFFLINE_ERROR; + this.versionError = e.message === VERSION_ERROR; + this.protectionError = e.message === PROTECTION_ERROR; + }); + } + } + private updateStatus(account: AccountData, status: GameStatus) { + this.initialized = true; + this.offline = false; + this.version = status.version; + this.update = status.update; - for (const server of status.servers) { - const existing = findById(this.servers, server.id); + for (const server of status.servers) { + const existing = findById(this.servers, server.id); - if (existing) { - merge(existing, server); - } else if ('name' in server) { - const info = server as ServerInfo; - info.countryFlags = info.flag && /^[a-z]{2}( [a-z]{2})*$/.test(info.flag) ? info.flag.split(/ /g) : []; + if (existing) { + merge(existing, server); + } else if ('name' in server) { + const info = server as ServerInfo; + info.countryFlags = info.flag && /^[a-z]{2}( [a-z]{2})*$/.test(info.flag) ? info.flag.split(/ /g) : []; - if (info.name && account && meetsRequirement(account, info.require)) { - this.servers.push(info); - } - } else { - // got new server on the list - this.initialized = false; - } - } + if (info.name && account && meetsRequirement(account, info.require)) { + this.servers.push(info); + } + } else { + // got new server on the list + this.initialized = false; + } + } - for (let i = this.servers.length - 1; i >= 0; i--) { - if (!findById(status.servers, this.servers[i].id)) { - this.servers.splice(i, 1); - } - } + for (let i = this.servers.length - 1; i >= 0; i--) { + if (!findById(status.servers, this.servers[i].id)) { + this.servers.splice(i, 1); + } + } - if (isLanguage('ru')) { - this.servers.sort(sortServersForRussian); - } + if (isLanguage('ru')) { + this.servers.sort(sortServersForRussian); + } - if (!this.server && account.settings.defaultServer) { - this.server = findById(this.servers, account.settings.defaultServer); + if (!this.server && account.settings.defaultServer) { + this.server = findById(this.servers, account.settings.defaultServer); - if (DEVELOPMENT && /join/.test(this.model.pony.name)) { - setTimeout(() => this.join(this.model.pony.id)); - } - } + if (DEVELOPMENT && /join/.test(this.model.pony.name)) { + setTimeout(() => this.join(this.model.pony.id)); + } + } - if (!includes(this.servers, this.server)) { - this.server = undefined; - } - } - private handleGameError(error: Error) { - this.errorReporter.captureEvent({ name: 'handleGameError', error: error.message }); - this.error = error.message; - this.errorHandler.handleError(error); - this.leave('handleGameError'); - } - private pollUntilConnected(socket: ClientSocketService) { - return new Promise((resolve, reject) => { - const interval = setInterval(() => { - if (socket.isConnected) { - clearInterval(interval); - this.zone.run(resolve); - } else if (!this.joining) { - clearInterval(interval); - this.zone.run(() => reject(new Error('Cancelled (poll)'))); - } - }, 10); - }); - } + if (!includes(this.servers, this.server)) { + this.server = undefined; + } + } + private handleGameError(error: Error) { + this.errorReporter.captureEvent({ name: 'handleGameError', error: error.message }); + this.error = error.message; + this.errorHandler.handleError(error); + this.leave('handleGameError'); + } + private pollUntilConnected(socket: ClientSocketService) { + return new Promise((resolve, reject) => { + const interval = setInterval(() => { + if (socket.isConnected) { + clearInterval(interval); + this.zone.run(resolve); + } else if (!this.joining) { + clearInterval(interval); + this.zone.run(() => reject(new Error('Cancelled (poll)'))); + } + }, 10); + }); + } } diff --git a/src/ts/components/services/installService.ts b/src/ts/components/services/installService.ts index b53e496..0c2be16 100644 --- a/src/ts/components/services/installService.ts +++ b/src/ts/components/services/installService.ts @@ -2,40 +2,40 @@ import { Injectable } from '@angular/core'; import { StorageService } from './storageService'; interface InstallEvent extends Event { - prompt(): void; - userChoice: Promise<'accepted' | 'dismissed'>; + prompt(): void; + userChoice: Promise<'accepted' | 'dismissed'>; } @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class InstallService { - private installEvent?: InstallEvent; - constructor(private storage: StorageService) { - if (!this.storage.getBoolean('install-dismissed')) { - window.addEventListener('beforeinstallprompt', event => { - event.preventDefault(); - this.installEvent = event as any; - }); - } - } - get canInstall() { - return !!this.installEvent || (DEVELOPMENT && localStorage.getItem('install')); - } - install() { - if (!this.installEvent) { - return Promise.reject(new Error('Cannot install')); - } + private installEvent?: InstallEvent; + constructor(private storage: StorageService) { + if (!this.storage.getBoolean('install-dismissed')) { + window.addEventListener('beforeinstallprompt', event => { + event.preventDefault(); + this.installEvent = event as any; + }); + } + } + get canInstall() { + return !!this.installEvent || (DEVELOPMENT && localStorage.getItem('install')); + } + install() { + if (!this.installEvent) { + return Promise.reject(new Error('Cannot install')); + } - this.installEvent.prompt(); + this.installEvent.prompt(); - return this.installEvent.userChoice - .finally(() => { - this.installEvent = undefined; - }); - } - dismiss() { - this.installEvent = undefined; - this.storage.setBoolean('install-dismissed', true); - } + return this.installEvent.userChoice + .finally(() => { + this.installEvent = undefined; + }); + } + dismiss() { + this.installEvent = undefined; + this.storage.setBoolean('install-dismissed', true); + } } diff --git a/src/ts/components/services/intervalUpdateService.ts b/src/ts/components/services/intervalUpdateService.ts index 1f04415..9248c9b 100644 --- a/src/ts/components/services/intervalUpdateService.ts +++ b/src/ts/components/services/intervalUpdateService.ts @@ -2,43 +2,43 @@ import { Injectable, NgZone } from '@angular/core'; import { removeItem } from '../../common/utils'; @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class IntervalUpdateService { - private interval: any; - private actions: (() => void)[] = []; - constructor(private zone: NgZone) { - } - subscribe(action: () => void) { - this.actions.push(action); + private interval: any; + private actions: (() => void)[] = []; + constructor(private zone: NgZone) { + } + subscribe(action: () => void) { + this.actions.push(action); - if (!this.interval) { - this.zone.runOutsideAngular(() => { - this.interval = setInterval(() => { - this.actions.forEach(a => a()); - }, 1000 * 10); - }); - } + if (!this.interval) { + this.zone.runOutsideAngular(() => { + this.interval = setInterval(() => { + this.actions.forEach(a => a()); + }, 1000 * 10); + }); + } - return () => { - removeItem(this.actions, action); + return () => { + removeItem(this.actions, action); - if (this.actions.length === 0) { - clearInterval(this.interval); - this.interval = undefined; - } - }; - } - toggle(action: () => void) { - let unsubscribe: (() => void) | undefined; + if (this.actions.length === 0) { + clearInterval(this.interval); + this.interval = undefined; + } + }; + } + toggle(action: () => void) { + let unsubscribe: (() => void) | undefined; - return (on: boolean) => { - if (on && !unsubscribe) { - unsubscribe = this.subscribe(action); - } else if (!on && unsubscribe) { - unsubscribe(); - unsubscribe = undefined; - } - }; - } + return (on: boolean) => { + if (on && !unsubscribe) { + unsubscribe = this.subscribe(action); + } else if (!on && unsubscribe) { + unsubscribe(); + unsubscribe = undefined; + } + }; + } } diff --git a/src/ts/components/services/liveCollection.ts b/src/ts/components/services/liveCollection.ts index 4c69456..3deb46a 100644 --- a/src/ts/components/services/liveCollection.ts +++ b/src/ts/components/services/liveCollection.ts @@ -4,142 +4,142 @@ import { Document, LiveResponse, IAdminServerActions, BaseValues } from '../../c import { ClientAdminActions } from '../../client/clientAdminActions'; export interface Options { - // collection - beforeUpdate?: (updates: T[]) => void; - onUpdated?: (added: T[], all: T[]) => void; - onFinished?: () => void; - // item - decode: (fields: any[], base: BaseValues) => T; - onUpdate?: (oldItem: T, newItem: T) => void; - onDelete?: (item: T) => void; - deleteItems?: boolean; - ignore?: (item: T) => boolean; + // collection + beforeUpdate?: (updates: T[]) => void; + onUpdated?: (added: T[], all: T[]) => void; + onFinished?: () => void; + // item + decode: (fields: any[], base: BaseValues) => T; + onUpdate?: (oldItem: T, newItem: T) => void; + onDelete?: (item: T) => void; + deleteItems?: boolean; + ignore?: (item: T) => boolean; } export class LiveCollection { - items: T[] = []; - finished = false; - private running = true; - private itemsMap = new Map(); - private liveTimeout: any; - constructor( - private name: 'events', - private rate: number, - private getKey: (item: T) => string, - private options: Options, - private socket: SocketService, - private timestamp = (new Date(0)).toISOString(), - private logError = (e: Error) => console.error(e.stack), - ) { - } - get(key: string) { - return this.itemsMap.get(key); - } - push(item: T) { - this.items.push(item); - this.itemsMap.set(this.getKey(item), item); - return item; - } - remove(key: string) { - return this.server.removeItem(this.name, key) - .then(() => this.removeItem(key, true, true)); - } - removeItem(key: string, deleted = false, removeFromList = false) { - const item = this.itemsMap.get(key); + items: T[] = []; + finished = false; + private running = true; + private itemsMap = new Map(); + private liveTimeout: any; + constructor( + private name: 'events', + private rate: number, + private getKey: (item: T) => string, + private options: Options, + private socket: SocketService, + private timestamp = (new Date(0)).toISOString(), + private logError = (e: Error) => console.error(e.stack), + ) { + } + get(key: string) { + return this.itemsMap.get(key); + } + push(item: T) { + this.items.push(item); + this.itemsMap.set(this.getKey(item), item); + return item; + } + remove(key: string) { + return this.server.removeItem(this.name, key) + .then(() => this.removeItem(key, true, true)); + } + removeItem(key: string, deleted = false, removeFromList = false) { + const item = this.itemsMap.get(key); - if (item) { - if (removeFromList || this.options.deleteItems) { - removeItem(this.items, item); - this.itemsMap.delete(key); - } else if (deleted) { - item.deleted = true; - } + if (item) { + if (removeFromList || this.options.deleteItems) { + removeItem(this.items, item); + this.itemsMap.delete(key); + } else if (deleted) { + item.deleted = true; + } - if (deleted && this.options.onDelete) { - this.options.onDelete(item); - } - } - } - assignAccount(id: string, account: string) { - return this.server.assignAccount(this.name, id, account); - } - live(): Promise { - if (!this.running) - return Promise.resolve(); + if (deleted && this.options.onDelete) { + this.options.onDelete(item); + } + } + } + assignAccount(id: string, account: string) { + return this.server.assignAccount(this.name, id, account); + } + live(): Promise { + if (!this.running) + return Promise.resolve(); - clearTimeout(this.liveTimeout); + clearTimeout(this.liveTimeout); - return this.update() - .catch(this.logError) - .then(more => { - this.liveTimeout = setTimeout(() => this.live(), more ? 100 : this.rate); - }); - } - stop() { - this.running = false; - } - read({ updates, deletes, base, more }: LiveResponse, liveFetch = true) { - const items = updates.map(i => this.options.decode(i, base)); + return this.update() + .catch(this.logError) + .then(more => { + this.liveTimeout = setTimeout(() => this.live(), more ? 100 : this.rate); + }); + } + stop() { + this.running = false; + } + read({ updates, deletes, base, more }: LiveResponse, liveFetch = true) { + const items = updates.map(i => this.options.decode(i, base)); - if (liveFetch) { - const timestamp = items - .reduce((max, i) => max.getTime() < i.updatedAt.getTime() ? i.updatedAt : max, new Date(this.timestamp)); - this.timestamp = timestamp.toISOString(); - } + if (liveFetch) { + const timestamp = items + .reduce((max, i) => max.getTime() < i.updatedAt.getTime() ? i.updatedAt : max, new Date(this.timestamp)); + this.timestamp = timestamp.toISOString(); + } - if (this.options.beforeUpdate) { - this.options.beforeUpdate(items); - } + if (this.options.beforeUpdate) { + this.options.beforeUpdate(items); + } - const { added, all } = this.applyUpdates(items, liveFetch); + const { added, all } = this.applyUpdates(items, liveFetch); - if (this.options.onUpdated && items.length) { - this.options.onUpdated(added, all); - } + if (this.options.onUpdated && items.length) { + this.options.onUpdated(added, all); + } - deletes.forEach(key => this.removeItem(key, true)); + deletes.forEach(key => this.removeItem(key, true)); - if (liveFetch) { - const finished = this.finished || !more; + if (liveFetch) { + const finished = this.finished || !more; - if (!this.finished && finished) { - this.finished = true; + if (!this.finished && finished) { + this.finished = true; - if (this.options.onFinished) { - this.options.onFinished(); - } - } - } + if (this.options.onFinished) { + this.options.onFinished(); + } + } + } - return more; - } - private get server() { - return this.socket.server; - } - private update() { - return this.socket.isConnected ? this.server.getAll(this.name, this.timestamp).then(r => this.read(r)) : Promise.resolve(false); - } - private applyUpdates(updates: T[], liveFetch: boolean) { - const added: T[] = []; - const all: T[] = []; + return more; + } + private get server() { + return this.socket.server; + } + private update() { + return this.socket.isConnected ? this.server.getAll(this.name, this.timestamp).then(r => this.read(r)) : Promise.resolve(false); + } + private applyUpdates(updates: T[], liveFetch: boolean) { + const added: T[] = []; + const all: T[] = []; - updates.forEach(update => { - const doc = this.get(this.getKey(update)); + updates.forEach(update => { + const doc = this.get(this.getKey(update)); - if (doc) { - if (this.options.onUpdate) { - this.options.onUpdate(doc, update); - } else { - Object.assign(doc, update); - } - all.push(doc); - } else if (!liveFetch || !this.options.ignore || !this.options.ignore(update)) { - this.push(update); - added.push(update); - all.push(update); - } - }); + if (doc) { + if (this.options.onUpdate) { + this.options.onUpdate(doc, update); + } else { + Object.assign(doc, update); + } + all.push(doc); + } else if (!liveFetch || !this.options.ignore || !this.options.ignore(update)) { + this.push(update); + added.push(update); + all.push(update); + } + }); - return { added, all }; - } + return { added, all }; + } } diff --git a/src/ts/components/services/model.ts b/src/ts/components/services/model.ts index 149eb36..0078a7f 100644 --- a/src/ts/components/services/model.ts +++ b/src/ts/components/services/model.ts @@ -5,18 +5,18 @@ import { merge } from 'lodash'; import { Subject } from 'rxjs'; import { HASH } from '../../generated/hash'; import { - AccountData, UpdateAccountData, AccountSettings, GameStatus, SocialSiteInfo, PonyObject, JoinResponse, - OAuthProvider, EntitiesEditorInfo, FriendData, PalettePonyInfo, HiddenPlayer + AccountData, UpdateAccountData, AccountSettings, GameStatus, SocialSiteInfo, PonyObject, JoinResponse, + OAuthProvider, EntitiesEditorInfo, FriendData, PalettePonyInfo, HiddenPlayer } from '../../common/interfaces'; import { createDefaultPony, syncLockedPonyInfo, mockPaletteManager } from '../../common/ponyInfo'; import { removeById, observableToPromise, delay, computeFriendsCRC } from '../../common/utils'; import { isMod, getSupporterInviteLimit, getCharacterLimit } from '../../common/accountUtils'; import { - NAME_ERROR, ACCESS_ERROR, CHARACTER_SAVING_ERROR, NOT_AUTHENTICATED_ERROR, OFFLINE_ERROR, PROTECTION_ERROR + NAME_ERROR, ACCESS_ERROR, CHARACTER_SAVING_ERROR, NOT_AUTHENTICATED_ERROR, OFFLINE_ERROR, PROTECTION_ERROR } from '../../common/errors'; import { version, host } from '../../client/data'; import { - toSocialSiteInfo, cleanName, validatePonyName, isStandalone, attachDebugMethod + toSocialSiteInfo, cleanName, validatePonyName, isStandalone, attachDebugMethod } from '../../client/clientUtils'; import { ErrorReporter } from './errorReporter'; import { randomString } from '../../common/stringUtils'; @@ -26,463 +26,463 @@ import { SECOND, PLAYER_DESC_MAX_LENGTH } from '../../common/constants'; import { canUseTag } from '../../common/tags'; export interface Friend extends FriendData { - entityId: number; - crc: number; - online: boolean; - ponyInfo: PalettePonyInfo | undefined; - actualName: string; + entityId: number; + crc: number; + online: boolean; + ponyInfo: PalettePonyInfo | undefined; + actualName: string; } const LIMIT_ERROR = 'Request limit reached, please wait'; const noneSite: SocialSiteInfo = { id: '', name: 'none', url: '', icon: '', color: '#222' }; const modStatus = { - mod: false, - check: {} as any, - editor: { - names: [], - typeToName: [], - nameToTypes: [], - } as EntitiesEditorInfo, + mod: false, + check: {} as any, + editor: { + names: [], + typeToName: [], + nameToTypes: [], + } as EntitiesEditorInfo, }; function compareStrings(a: string | undefined, b: string | undefined) { - return (a || '').localeCompare(b || ''); + return (a || '').localeCompare(b || ''); } function comparePonies(a: PonyObject, b: PonyObject) { - return compareStrings(a.name, b.name) || compareStrings(a.id, b.id); + return compareStrings(a.name, b.name) || compareStrings(a.id, b.id); } function getDefaultPony(ponies: PonyObject[]) { - let result = ponies[0]; + let result = ponies[0]; - for (let i = 1; i < ponies.length; i++) { - if (compareStrings(result.lastUsed, ponies[i].lastUsed) < 0) { - result = ponies[i]; - } - } + for (let i = 1; i < ponies.length; i++) { + if (compareStrings(result.lastUsed, ponies[i].lastUsed) < 0) { + result = ponies[i]; + } + } - return result || createDefaultPonyObject(); + return result || createDefaultPonyObject(); } export function createDefaultPonyObject(): PonyObject { - return { - id: '', - name: '', - info: '', - ponyInfo: createDefaultPony(), - }; + return { + id: '', + name: '', + info: '', + ponyInfo: createDefaultPony(), + }; } export function getPonyTag(pony: PonyObject, account: AccountData | undefined) { - if (account) { - const tag = canUseTag(account, pony.tag || '') ? pony.tag : undefined; - return (!tag && account.supporter && !pony.hideSupport) ? `sup${account.supporter}` : tag; - } else { - return undefined; - } + if (account) { + const tag = canUseTag(account, pony.tag || '') ? pony.tag : undefined; + return (!tag && account.supporter && !pony.hideSupport) ? `sup${account.supporter}` : tag; + } else { + return undefined; + } } const entityTypeToName = new Map(); const entityNameToTypes = new Map(); export function getEntityNames() { - return modStatus.editor.names; + return modStatus.editor.names; } export function getEntityTypesFromName(name: string) { - return entityNameToTypes.get(name); + return entityNameToTypes.get(name); } export function getEntityNameFromType(type: number) { - return entityTypeToName.get(type); + return entityTypeToName.get(type); } export function compareFriends(a: Friend, b: Friend) { - return a.online !== b.online ? (a.online ? -1 : 1) : a.accountName.localeCompare(b.accountName); + return a.online !== b.online ? (a.online ? -1 : 1) : a.accountName.localeCompare(b.accountName); } @Injectable({ providedIn: 'root' }) export class Model { - loading = true; - loadingError?: string; - account?: AccountData; - ponies: PonyObject[] = []; - pending = false; - sites: SocialSiteInfo[] = [noneSite]; - accountPromise!: Promise; - accountChanged = new Subject(); - protectionErrors = new Subject(); - authError?: string; - accountAlert?: string; - mergedAccount = false; - updating = false; - updatingTakesLongTime = false; - suffix = ''; - friends: Friend[] | undefined = undefined; - private _pony: PonyObject = createDefaultPonyObject(); - constructor( - private http: HttpClient, - private router: Router, - private storage: StorageService, - private errorReporter: ErrorReporter, - ) { - this.initialize(); + loading = true; + loadingError?: string; + account?: AccountData; + ponies: PonyObject[] = []; + pending = false; + sites: SocialSiteInfo[] = [noneSite]; + accountPromise!: Promise; + accountChanged = new Subject(); + protectionErrors = new Subject(); + authError?: string; + accountAlert?: string; + mergedAccount = false; + updating = false; + updatingTakesLongTime = false; + suffix = ''; + friends: Friend[] | undefined = undefined; + private _pony: PonyObject = createDefaultPonyObject(); + constructor( + private http: HttpClient, + private router: Router, + private storage: StorageService, + private errorReporter: ErrorReporter, + ) { + this.initialize(); - // handle completed sign-in - if (typeof window !== 'undefined') { - window.addEventListener('message', event => { - if (event.data && event.data.type === 'loaded-page') { - const path = event.data.path; + // handle completed sign-in + if (typeof window !== 'undefined') { + window.addEventListener('message', event => { + if (event.data && event.data.type === 'loaded-page') { + const path = event.data.path; - if (event.source && 'close' in event.source) { - event.source.close(); - } + if (event.source && 'close' in event.source) { + event.source.close(); + } - this.initialize(); - this.accountPromise.then(() => router.navigateByUrl(path)); - } - }); - } + this.initialize(); + this.accountPromise.then(() => router.navigateByUrl(path)); + } + }); + } - if (DEVELOPMENT) { - attachDebugMethod('ddos', () => this.protectionErrors.next()); - attachDebugMethod('userModel', this); - } - } - private initialize() { - this.loading = true; - this.account = undefined; - this.loadingError = undefined; - this.accountAlert = undefined; - this.ponies = []; - this.friends = undefined; - this.sites = [noneSite]; - this._pony = createDefaultPonyObject(); - this.storage.setItem('bid', this.storage.getItem('bid') || randomString(20)); - this.accountPromise = this.initializeAccount(); - } - private initializeAccount(): Promise { - return this.getAccount() - .then(account => { - if (!account) { - throw new Error(ACCESS_ERROR); - } + if (DEVELOPMENT) { + attachDebugMethod('ddos', () => this.protectionErrors.next()); + attachDebugMethod('userModel', this); + } + } + private initialize() { + this.loading = true; + this.account = undefined; + this.loadingError = undefined; + this.accountAlert = undefined; + this.ponies = []; + this.friends = undefined; + this.sites = [noneSite]; + this._pony = createDefaultPonyObject(); + this.storage.setItem('bid', this.storage.getItem('bid') || randomString(20)); + this.accountPromise = this.initializeAccount(); + } + private initializeAccount(): Promise { + return this.getAccount() + .then(account => { + if (!account) { + throw new Error(ACCESS_ERROR); + } - if ('limit' in account) { - throw new Error(LIMIT_ERROR); - } + if ('limit' in account) { + throw new Error(LIMIT_ERROR); + } - this.errorReporter.configureUser({ id: account.id, username: account.name }); + this.errorReporter.configureUser({ id: account.id, username: account.name }); - try { - modStatus.mod = isMod(account); - modStatus.check = account.check; - modStatus.editor = account.editor || modStatus.editor; - } catch { } + try { + modStatus.mod = isMod(account); + modStatus.check = account.check; + modStatus.editor = account.editor || modStatus.editor; + } catch { } - if (modStatus.editor) { - modStatus.editor.typeToName.forEach(({ type, name }) => entityTypeToName.set(type, name)); - modStatus.editor.nameToTypes.forEach(({ types, name }) => entityNameToTypes.set(name, types)); - } + if (modStatus.editor) { + modStatus.editor.typeToName.forEach(({ type, name }) => entityTypeToName.set(type, name)); + modStatus.editor.nameToTypes.forEach(({ types, name }) => entityNameToTypes.set(name, types)); + } - this.account = account; - this.sites = [noneSite, ...(account.sites || []).map(toSocialSiteInfo)]; - this.ponies = account.ponies ? account.ponies.sort(comparePonies) : []; - this.friends = undefined; + this.account = account; + this.sites = [noneSite, ...(account.sites || []).map(toSocialSiteInfo)]; + this.ponies = account.ponies ? account.ponies.sort(comparePonies) : []; + this.friends = undefined; - this.selectPony(getDefaultPony(this.ponies)); - this.storage.setItem('vid', account.id); - this.loading = false; - this.accountAlert = account.alert; - this.accountChanged.next(); - this.fetchFriends(); + this.selectPony(getDefaultPony(this.ponies)); + this.storage.setItem('vid', account.id); + this.loading = false; + this.accountAlert = account.alert; + this.accountChanged.next(); + this.fetchFriends(); - return account; - }) - .catch((e: Error) => { - if (e.message === ACCESS_ERROR) { - this.loading = false; - this.storage.setItem('vid', '---'); - } else if (e.message === LIMIT_ERROR) { - this.loadingError = 'request-limit'; - return delay(5000).then(() => this.initializeAccount()); - } else if (e.message === OFFLINE_ERROR) { - this.loadingError = 'cannot-connect'; - return delay(5000).then(() => this.initializeAccount()); - } else if (e.message === PROTECTION_ERROR) { - this.loadingError = 'cloudflare-error'; - this.protectionErrors.next(); - // } else if (e.message === VERSION_ERROR) { - // this.updating = true; - } else { - setTimeout(() => this.loadingError = 'unexpected-error', 5 * SECOND); - console.error(e); - } + return account; + }) + .catch((e: Error) => { + if (e.message === ACCESS_ERROR) { + this.loading = false; + this.storage.setItem('vid', '---'); + } else if (e.message === LIMIT_ERROR) { + this.loadingError = 'request-limit'; + return delay(5000).then(() => this.initializeAccount()); + } else if (e.message === OFFLINE_ERROR) { + this.loadingError = 'cannot-connect'; + return delay(5000).then(() => this.initializeAccount()); + } else if (e.message === PROTECTION_ERROR) { + this.loadingError = 'cloudflare-error'; + this.protectionErrors.next(); + // } else if (e.message === VERSION_ERROR) { + // this.updating = true; + } else { + setTimeout(() => this.loadingError = 'unexpected-error', 5 * SECOND); + console.error(e); + } - return undefined; - }); - } - private fetchFriends() { - this.getFriends() - .then(friends => { - this.friends = friends.map(f => ({ - ...f, - online: false, - entityId: 0, - crc: 0, - ponyInfo: f.pony && decodePonyInfo(f.pony, mockPaletteManager) || undefined, - actualName: '', - })).sort(compareFriends); - }) - .catch(e => { - DEVELOPMENT && console.error(e); - setTimeout(() => this.fetchFriends(), 5000); - }); - } - get characterLimit() { - return this.account ? getCharacterLimit(this.account) : 0; - } - get supporterInviteLimit() { - return this.account ? getSupporterInviteLimit(this.account) : 0; - } - get isMod() { - return modStatus.mod; - } - get modCheck() { - return modStatus.check; - } - get editorInfo() { - return modStatus.editor; - } - get pony() { - return this._pony; - } - get supporter() { - return this.account && this.account.supporter || 0; - } - get missingBirthdate() { - return !!this.account && !this.account.birthdate; - } - computeFriendsCRC() { - return this.friends ? computeFriendsCRC(this.friends.map(f => f.accountId)) : 0; - } - parsePonyObject(pony: PonyObject): PonyObject { - try { - const ponyInfo = decompressPonyString(pony.info, true); - return { ponyInfo, ...pony }; - } catch (e) { - this.errorReporter.reportError(e, { ponyInfo: pony.info }); - this.errorReporter.reportError('Pony info reading error', { originalError: e.message, ponyInfo: pony.info }); - throw new Error('Error while reading pony info'); - } - } - selectPony(pony: PonyObject) { - const copy = this.parsePonyObject(pony); - copy.ponyInfo && syncLockedPonyInfo(copy.ponyInfo); - this._pony = copy; - } - // account - signIn(provider: OAuthProvider) { - this.authError = undefined; - this.openAuth(provider.url!); - } - connectSite(provider: OAuthProvider) { - this.authError = undefined; - this.openAuth(`${provider.url}/merge`); - } - signOut() { - this.authError = undefined; + return undefined; + }); + } + private fetchFriends() { + this.getFriends() + .then(friends => { + this.friends = friends.map(f => ({ + ...f, + online: false, + entityId: 0, + crc: 0, + ponyInfo: f.pony && decodePonyInfo(f.pony, mockPaletteManager) || undefined, + actualName: '', + })).sort(compareFriends); + }) + .catch(e => { + DEVELOPMENT && console.error(e); + setTimeout(() => this.fetchFriends(), 5000); + }); + } + get characterLimit() { + return this.account ? getCharacterLimit(this.account) : 0; + } + get supporterInviteLimit() { + return this.account ? getSupporterInviteLimit(this.account) : 0; + } + get isMod() { + return modStatus.mod; + } + get modCheck() { + return modStatus.check; + } + get editorInfo() { + return modStatus.editor; + } + get pony() { + return this._pony; + } + get supporter() { + return this.account && this.account.supporter || 0; + } + get missingBirthdate() { + return !!this.account && !this.account.birthdate; + } + computeFriendsCRC() { + return this.friends ? computeFriendsCRC(this.friends.map(f => f.accountId)) : 0; + } + parsePonyObject(pony: PonyObject): PonyObject { + try { + const ponyInfo = decompressPonyString(pony.info, true); + return { ponyInfo, ...pony }; + } catch (e) { + this.errorReporter.reportError(e, { ponyInfo: pony.info }); + this.errorReporter.reportError('Pony info reading error', { originalError: e.message, ponyInfo: pony.info }); + throw new Error('Error while reading pony info'); + } + } + selectPony(pony: PonyObject) { + const copy = this.parsePonyObject(pony); + copy.ponyInfo && syncLockedPonyInfo(copy.ponyInfo); + this._pony = copy; + } + // account + signIn(provider: OAuthProvider) { + this.authError = undefined; + this.openAuth(provider.url!); + } + connectSite(provider: OAuthProvider) { + this.authError = undefined; + this.openAuth(`${provider.url}/merge`); + } + signOut() { + this.authError = undefined; - return this.post('/auth/sign-out', {}, false) - .catch(e => console.error(e)) - .then(() => this.initialize()) - .then(() => this.router.navigate(['/'])); - } - private openAuth(url: string) { - url = `${host.replace(/\/$/, '')}${url}`; + return this.post('/auth/sign-out', {}, false) + .catch(e => console.error(e)) + .then(() => this.initialize()) + .then(() => this.router.navigate(['/'])); + } + private openAuth(url: string) { + url = `${host.replace(/\/$/, '')}${url}`; - if (isStandalone()) { - window.open(url); - } else { - location.href = url; - } - } - getAccount() { - return this.post('/api1/account', {}, false); - } - getAccountCharacters() { - return this.post('/api/account-characters', {}); - } - updateAccount(account: Partial) { - return this.post('/api/account-update', { account }) - .then(a => merge(this.account, a)); - } - saveSettings(settings: AccountSettings) { - return this.post('/api/account-settings', { settings }) - .then(a => merge(this.account, a)); - } - removeSite(siteId: string) { - return this.post('/api/remove-site', { siteId }) - .then(() => { - if (this.account && this.account.sites) { - removeById(this.account.sites, siteId); - } - }); - } - unhidePlayer(hideId: string) { - return this.post('/api/remove-hide', { hideId }); - } - verifyAccount() { - const verificationId = this.storage.getItem('vid'); - const accountId = this.account && this.account.id || '---'; + if (isStandalone()) { + window.open(url); + } else { + location.href = url; + } + } + getAccount() { + return this.post('/api1/account', {}, false); + } + getAccountCharacters() { + return this.post('/api/account-characters', {}); + } + updateAccount(account: Partial) { + return this.post('/api/account-update', { account }) + .then(a => merge(this.account, a)); + } + saveSettings(settings: AccountSettings) { + return this.post('/api/account-settings', { settings }) + .then(a => merge(this.account, a)); + } + removeSite(siteId: string) { + return this.post('/api/remove-site', { siteId }) + .then(() => { + if (this.account && this.account.sites) { + removeById(this.account.sites, siteId); + } + }); + } + unhidePlayer(hideId: string) { + return this.post('/api/remove-hide', { hideId }); + } + verifyAccount() { + const verificationId = this.storage.getItem('vid'); + const accountId = this.account && this.account.id || '---'; - if (!this.loading && verificationId && accountId !== verificationId) { - this.initialize(); - } - } - getHides(page: number) { - return this.post('/api/get-hides', { page }); - } - getFriends() { - return this.post('/api/get-friends', {}); - } - // ponies - savePony(pony: PonyObject, fast = false) { - return Promise.resolve() - .then(() => { - if (this.pending) { - throw new Error('Saving in progress'); - } + if (!this.loading && verificationId && accountId !== verificationId) { + this.initialize(); + } + } + getHides(page: number) { + return this.post('/api/get-hides', { page }); + } + getFriends() { + return this.post('/api/get-friends', {}); + } + // ponies + savePony(pony: PonyObject, fast = false) { + return Promise.resolve() + .then(() => { + if (this.pending) { + throw new Error('Saving in progress'); + } - pony.name = cleanName(pony.name); - pony.desc = pony.desc && pony.desc.substr(0, PLAYER_DESC_MAX_LENGTH) || ''; + pony.name = cleanName(pony.name); + pony.desc = pony.desc && pony.desc.substr(0, PLAYER_DESC_MAX_LENGTH) || ''; - if (!validatePonyName(pony.name)) { - throw new Error(NAME_ERROR); - } + if (!validatePonyName(pony.name)) { + throw new Error(NAME_ERROR); + } - if (pony.ponyInfo) { - pony.info = compressPonyString(pony.ponyInfo); - } + if (pony.ponyInfo) { + pony.info = compressPonyString(pony.ponyInfo); + } - const { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn } = pony; + const { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn } = pony; - if (!fast) { - this.pending = true; - } + if (!fast) { + this.pending = true; + } - return this.post('/api/pony/save', { - pony: { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn } - }); - }) - .catch((e: Error) => { - if (e.message === CHARACTER_SAVING_ERROR) { - this.errorReporter.reportError(e, { pony }); - } + return this.post('/api/pony/save', { + pony: { id, name, desc, site, tag, info, hideSupport, respawnAtSpawn } + }); + }) + .catch((e: Error) => { + if (e.message === CHARACTER_SAVING_ERROR) { + this.errorReporter.reportError(e, { pony }); + } - throw e; - }) - .then(newPony => { - if (!newPony) { - throw new Error('Failed to save pony'); - } + throw e; + }) + .then(newPony => { + if (!newPony) { + throw new Error('Failed to save pony'); + } - if (pony.id) { - removeById(this.ponies, pony.id); - } else { - this.account!.characterCount++; - } + if (pony.id) { + removeById(this.ponies, pony.id); + } else { + this.account!.characterCount++; + } - this.ponies.push(newPony); - this.ponies.sort(comparePonies); + this.ponies.push(newPony); + this.ponies.sort(comparePonies); - if (this.pony === pony) { - this.selectPony(newPony); - } + if (this.pony === pony) { + this.selectPony(newPony); + } - return newPony; - }) - .finally(() => this.pending = false); - } - removePony(pony: PonyObject) { - return this.post('/api/pony/remove', { id: pony.id }) - .then(() => { - removeById(this.ponies, pony.id); - this.account!.characterCount--; + return newPony; + }) + .finally(() => this.pending = false); + } + removePony(pony: PonyObject) { + return this.post('/api/pony/remove', { id: pony.id }) + .then(() => { + removeById(this.ponies, pony.id); + this.account!.characterCount--; - if (this.pony === pony) { - this.selectPony(getDefaultPony(this.ponies)); - } - }); - } - loadPonies() { - return this.getAccountCharacters() - .then(ponies => { - if (this.account) { - this.account.ponies = ponies || []; - this.ponies = this.account.ponies.sort(comparePonies); - } - }); - } - sortPonies() { - this.ponies.sort(comparePonies); - } - // game - status(short: boolean): Promise { - let age = 6; + if (this.pony === pony) { + this.selectPony(getDefaultPony(this.ponies)); + } + }); + } + loadPonies() { + return this.getAccountCharacters() + .then(ponies => { + if (this.account) { + this.account.ponies = ponies || []; + this.ponies = this.account.ponies.sort(comparePonies); + } + }); + } + sortPonies() { + this.ponies.sort(comparePonies); + } + // game + status(short: boolean): Promise { + let age = 6; - if (this.account) { - const now = new Date(); - const currentYear = now.getFullYear(); - const currentMonth = now.getMonth() + 1; + if (this.account) { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; - if (this.account.birthyear) { - age = currentYear - this.account.birthyear; - } else if (this.account.birthdate) { - const [year, month] = this.account.birthdate.split('-'); - const before = parseInt(month, 10) > currentMonth; - age = Math.max(0, currentYear - parseInt(year, 10) - (before ? 1 : 0)); - } - } + if (this.account.birthyear) { + age = currentYear - this.account.birthyear; + } else if (this.account.birthdate) { + const [year, month] = this.account.birthdate.split('-'); + const before = parseInt(month, 10) > currentMonth; + age = Math.max(0, currentYear - parseInt(year, 10) - (before ? 1 : 0)); + } + } - const params = new HttpParams() - .set('short', short.toString()) - .set('d', age.toString()) - .set('t', (Date.now() % 0x10000).toString(16)); + const params = new HttpParams() + .set('short', short.toString()) + .set('d', age.toString()) + .set('t', (Date.now() % 0x10000).toString(16)); - return observableToPromise(this.http.get('/api2/game/status', { params })); - } - join(serverId: string, ponyId: string): Promise { - if (this.pending) - return Promise.reject(new Error('Joining in progress')); - if (!serverId) - return Promise.reject(new Error('Invalid server ID')); - if (!ponyId) - return Promise.reject(new Error('Invalid pony ID')); + return observableToPromise(this.http.get('/api2/game/status', { params })); + } + join(serverId: string, ponyId: string): Promise { + if (this.pending) + return Promise.reject(new Error('Joining in progress')); + if (!serverId) + return Promise.reject(new Error('Invalid server ID')); + if (!ponyId) + return Promise.reject(new Error('Invalid pony ID')); - this.pending = true; + this.pending = true; - const alert = !!this.accountAlert ? 'y' : ''; + const alert = !!this.accountAlert ? 'y' : ''; - return this.post('/api/game/join', { version, ponyId, serverId, alert, url: location.href }) - .finally(() => this.pending = false); - } - private post(url: string, data: any, authenticate = true): Promise { - if (authenticate) { - if (!this.account) { - return Promise.reject(new Error(NOT_AUTHENTICATED_ERROR)); - } + return this.post('/api/game/join', { version, ponyId, serverId, alert, url: location.href }) + .finally(() => this.pending = false); + } + private post(url: string, data: any, authenticate = true): Promise { + if (authenticate) { + if (!this.account) { + return Promise.reject(new Error(NOT_AUTHENTICATED_ERROR)); + } - const accountId = this.account.id + this.suffix; - const accountName = this.account.name + this.suffix; - data = { accountId, accountName, ...data }; - } + const accountId = this.account.id + this.suffix; + const accountName = this.account.name + this.suffix; + data = { accountId, accountName, ...data }; + } - const params = new HttpParams() - .set('t', (Date.now() % 0x10000).toString(16)); - const headers = new HttpHeaders({ 'api-version': HASH, 'api-bid': this.storage.getItem('bid') || '-' }); + const params = new HttpParams() + .set('t', (Date.now() % 0x10000).toString(16)); + const headers = new HttpHeaders({ 'api-version': HASH, 'api-bid': this.storage.getItem('bid') || '-' }); - return observableToPromise(this.http.post(url, data, { params, headers })); - } + return observableToPromise(this.http.post(url, data, { params, headers })); + } } diff --git a/src/ts/components/services/modelSubscriber.ts b/src/ts/components/services/modelSubscriber.ts index e047237..a227152 100644 --- a/src/ts/components/services/modelSubscriber.ts +++ b/src/ts/components/services/modelSubscriber.ts @@ -9,99 +9,99 @@ import { MINUTE } from '../../common/constants'; type OnModel = (item: T | undefined) => void; interface ModelSubscriberConfig { - fix?: (item: T) => void; + fix?: (item: T) => void; } interface ModelSubscription { - value: T | undefined; - timeout: any; - callbacks: OnModel[]; + value: T | undefined; + timeout: any; + callbacks: OnModel[]; } const unsubscribeTimeout = 1 * MINUTE; export class ModelSubscriber { - private subscriptions = new Map>(); - // private observables = new Map>(); - constructor( - private type: ModelTypes, - private socket: SocketService, - private config: ModelSubscriberConfig = {}, - private defaultValue: T | undefined = undefined, - ) { - } - // for(id: string) { - // return this.createObservable(id); - // } - // private createObservable(id: string) { - // return new Observable(observer => { - // this.socket.server.subscribe(this.model, id); + private subscriptions = new Map>(); + // private observables = new Map>(); + constructor( + private type: ModelTypes, + private socket: SocketService, + private config: ModelSubscriberConfig = {}, + private defaultValue: T | undefined = undefined, + ) { + } + // for(id: string) { + // return this.createObservable(id); + // } + // private createObservable(id: string) { + // return new Observable(observer => { + // this.socket.server.subscribe(this.model, id); - // return () => { - // this.socket.server.unsubscribe(this.model, id); - // }; - // }); - // } - get(id: string) { - const subscription = this.subscriptions.get(id); - return subscription && subscription.value; - } - subscribe(id: string, callback: OnModel): Subscription { - const subscription = this.subscriptions.get(id); + // return () => { + // this.socket.server.unsubscribe(this.model, id); + // }; + // }); + // } + get(id: string) { + const subscription = this.subscriptions.get(id); + return subscription && subscription.value; + } + subscribe(id: string, callback: OnModel): Subscription { + const subscription = this.subscriptions.get(id); - if (subscription) { - if (subscription.timeout) { - clearTimeout(subscription.timeout); - subscription.timeout = 0; - } + if (subscription) { + if (subscription.timeout) { + clearTimeout(subscription.timeout); + subscription.timeout = 0; + } - subscription.callbacks.push(callback); + subscription.callbacks.push(callback); - if (subscription.value !== undefined) { - callback(subscription.value); - } - } else { - this.socket.server.subscribe(this.type, id); - this.subscriptions.set(id, { - value: this.defaultValue, - timeout: 0, - callbacks: [callback], - }); - } + if (subscription.value !== undefined) { + callback(subscription.value); + } + } else { + this.socket.server.subscribe(this.type, id); + this.subscriptions.set(id, { + value: this.defaultValue, + timeout: 0, + callbacks: [callback], + }); + } - return { - unsubscribe: () => this.unsubscribe(id, callback), - }; - } - unsubscribe(id: string, callback: OnModel) { - const subscription = this.subscriptions.get(id); + return { + unsubscribe: () => this.unsubscribe(id, callback), + }; + } + unsubscribe(id: string, callback: OnModel) { + const subscription = this.subscriptions.get(id); - if (subscription) { - removeItem(subscription.callbacks, callback); + if (subscription) { + removeItem(subscription.callbacks, callback); - if (subscription.callbacks.length === 0) { - subscription.timeout = setTimeout(() => { - this.socket.server.unsubscribe(this.type, id); - this.subscriptions.delete(id); - }, unsubscribeTimeout); - } - } - } - update(id: string, update: T) { - const subscription = this.subscriptions.get(id); + if (subscription.callbacks.length === 0) { + subscription.timeout = setTimeout(() => { + this.socket.server.unsubscribe(this.type, id); + this.subscriptions.delete(id); + }, unsubscribeTimeout); + } + } + } + update(id: string, update: T) { + const subscription = this.subscriptions.get(id); - if (update !== undefined && this.config.fix) { - this.config.fix(update); - } + if (update !== undefined && this.config.fix) { + this.config.fix(update); + } - if (subscription) { - subscription.value = update; - subscription.callbacks.forEach(c => c(update)); - } - } - connected() { - this.subscriptions.forEach((_, id) => { - this.socket.server.subscribe(this.type, id); - }); - } + if (subscription) { + subscription.value = update; + subscription.callbacks.forEach(c => c(update)); + } + } + connected() { + this.subscriptions.forEach((_, id) => { + this.socket.server.subscribe(this.type, id); + }); + } } diff --git a/src/ts/components/services/rollbarErrorHandler.ts b/src/ts/components/services/rollbarErrorHandler.ts index 20b9d2d..a62a89c 100644 --- a/src/ts/components/services/rollbarErrorHandler.ts +++ b/src/ts/components/services/rollbarErrorHandler.ts @@ -8,54 +8,54 @@ import { rollbarCheckIgnore, isIgnoredError } from '../../common/rollbar'; const host = typeof location === 'undefined' ? '' : location.host; const rollbarConfig = { - environment: ROLLBAR_ENV, - accessToken: ROLLBAR_TOKEN, - ignoredMessages: ['disconnected'], - hostWhiteList: [host], - captureUncaught: true, - captureUnhandleRejections: true, - // checkIgnore, - enabled: true, - payload: { - environment: ROLLBAR_ENV, - version: version, // NOTE: workaround for compilation issue - client: { - javascript: { - source_map_enabled: true, - guess_uncaught_frames: true, - code_version: HASH, - }, - }, - }, + environment: ROLLBAR_ENV, + accessToken: ROLLBAR_TOKEN, + ignoredMessages: ['disconnected'], + hostWhiteList: [host], + captureUncaught: true, + captureUnhandleRejections: true, + // checkIgnore, + enabled: true, + payload: { + environment: ROLLBAR_ENV, + version: version, // NOTE: workaround for compilation issue + client: { + javascript: { + source_map_enabled: true, + guess_uncaught_frames: true, + code_version: HASH, + }, + }, + }, }; export const RollbarService = new InjectionToken('rollbar'); export function rollbarFactory() { - if (DEVELOPMENT) { - return undefined; - } else { - const rollbar = Rollbar.init(rollbarConfig); - rollbar.configure({ checkIgnore: rollbarCheckIgnore }); - return rollbar; - } + if (DEVELOPMENT) { + return undefined; + } else { + const rollbar = Rollbar.init(rollbarConfig); + rollbar.configure({ checkIgnore: rollbarCheckIgnore }); + return rollbar; + } } @Injectable() export class RollbarErrorHandler extends ErrorHandler { - constructor(private injector: Injector) { - super(); - } - handleError(error: any) { - super.handleError(error); + constructor(private injector: Injector) { + super(); + } + handleError(error: any) { + super.handleError(error); - if (!DEVELOPMENT && rollbarConfig.accessToken) { - const rollbar = this.injector.get(RollbarService); - const err = error.originalError || error || {}; + if (!DEVELOPMENT && rollbarConfig.accessToken) { + const rollbar = this.injector.get(RollbarService); + const err = error.originalError || error || {}; - if (!isIgnoredError(err)) { - rollbar.error(err); - } - } - } + if (!isIgnoredError(err)) { + rollbar.error(err); + } + } + } } diff --git a/src/ts/components/services/rollbarErrorReporter.ts b/src/ts/components/services/rollbarErrorReporter.ts index 823675e..b255a0d 100644 --- a/src/ts/components/services/rollbarErrorReporter.ts +++ b/src/ts/components/services/rollbarErrorReporter.ts @@ -6,34 +6,34 @@ import { ErrorReporter } from './errorReporter'; @Injectable() export class RollbarErrorReporter extends ErrorReporter { - constructor(@Inject(RollbarService) private rollbar?: Rollbar) { - super(); - } - configureUser(person: Person) { - if (this.rollbar) { - this.rollbar.configure({ payload: { person }, checkIgnore: rollbarCheckIgnore }); - } - } - configureData(data: any) { - if (this.rollbar) { - this.rollbar.configure({ payload: data, checkIgnore: rollbarCheckIgnore }); - } - } - captureEvent(data: any) { - if (this.rollbar) { - this.rollbar.captureEvent(data, 'info'); - } - } - reportError(error: any, data?: any) { - DEVELOPMENT && console.error(error, data); + constructor(@Inject(RollbarService) private rollbar?: Rollbar) { + super(); + } + configureUser(person: Person) { + if (this.rollbar) { + this.rollbar.configure({ payload: { person }, checkIgnore: rollbarCheckIgnore }); + } + } + configureData(data: any) { + if (this.rollbar) { + this.rollbar.configure({ payload: data, checkIgnore: rollbarCheckIgnore }); + } + } + captureEvent(data: any) { + if (this.rollbar) { + this.rollbar.captureEvent(data, 'info'); + } + } + reportError(error: any, data?: any) { + DEVELOPMENT && console.error(error, data); - if (this.rollbar && !isIgnoredError(error)) { - this.rollbar.error(error, data); - } - } - disable() { - if (this.rollbar) { - this.rollbar.configure({ enabled: false }); - } - } + if (this.rollbar && !isIgnoredError(error)) { + this.rollbar.error(error, data); + } + } + disable() { + if (this.rollbar) { + this.rollbar.configure({ enabled: false }); + } + } } diff --git a/src/ts/components/services/settingsService.ts b/src/ts/components/services/settingsService.ts index d472f9f..0dd3fa9 100644 --- a/src/ts/components/services/settingsService.ts +++ b/src/ts/components/services/settingsService.ts @@ -5,39 +5,39 @@ import { Model } from './model'; @Injectable({ providedIn: 'root' }) export class SettingsService { - browser: BrowserSettings; - private save: (settings: AccountSettings) => boolean = () => false; - constructor(private storage: StorageService, private model: Model) { - this.browser = this.storage.getJSON('browser-settings', {}); - } - get account(): AccountSettings { - return this.model.account ? this.model.account.settings : {}; - } - set account(value) { - if (this.model.account) { - this.model.account.settings = value; - } - } - saving(save: (settings: AccountSettings) => boolean) { - this.save = save; - } - saveAccountSettings(settings: AccountSettings) { - if (this.model.account) { - this.model.account.settings = settings; - } + browser: BrowserSettings; + private save: (settings: AccountSettings) => boolean = () => false; + constructor(private storage: StorageService, private model: Model) { + this.browser = this.storage.getJSON('browser-settings', {}); + } + get account(): AccountSettings { + return this.model.account ? this.model.account.settings : {}; + } + set account(value) { + if (this.model.account) { + this.model.account.settings = value; + } + } + saving(save: (settings: AccountSettings) => boolean) { + this.save = save; + } + saveAccountSettings(settings: AccountSettings) { + if (this.model.account) { + this.model.account.settings = settings; + } - if (settings.filterWords) { - settings.filterWords = settings.filterWords.trim(); - } + if (settings.filterWords) { + settings.filterWords = settings.filterWords.trim(); + } - if (this.save(settings)) { - return Promise.resolve(); - } else { - return this.model.saveSettings(settings); - } - } - saveBrowserSettings(settings?: BrowserSettings) { - this.browser = settings || this.browser; - this.storage.setJSON('browser-settings', this.browser); - } + if (this.save(settings)) { + return Promise.resolve(); + } else { + return this.model.saveSettings(settings); + } + } + saveBrowserSettings(settings?: BrowserSettings) { + this.browser = settings || this.browser; + this.storage.setJSON('browser-settings', this.browser); + } } diff --git a/src/ts/components/services/storageService.ts b/src/ts/components/services/storageService.ts index 54e40f2..0aefe69 100644 --- a/src/ts/components/services/storageService.ts +++ b/src/ts/components/services/storageService.ts @@ -3,82 +3,82 @@ import { Injectable } from '@angular/core'; /* istanbul ignore next */ @Injectable({ providedIn: 'root' }) export class StorageService { - private data?: Map = undefined; - constructor() { - try { - if (typeof localStorage === 'undefined') { - this.data = new Map(); - } - } catch { - this.data = new Map(); - } - } - getItem(key: string) { - if (this.data) { - return this.data.get(key); - } else { - try { - const value = localStorage.getItem(key); - return value == null ? undefined : value; - } catch { - return undefined; - } - } - } - setItem(key: string, data: string) { - try { - localStorage.setItem(key, data); - this.data = undefined; - } catch { - if (!this.data) { - this.data = new Map(); - } + private data?: Map = undefined; + constructor() { + try { + if (typeof localStorage === 'undefined') { + this.data = new Map(); + } + } catch { + this.data = new Map(); + } + } + getItem(key: string) { + if (this.data) { + return this.data.get(key); + } else { + try { + const value = localStorage.getItem(key); + return value == null ? undefined : value; + } catch { + return undefined; + } + } + } + setItem(key: string, data: string) { + try { + localStorage.setItem(key, data); + this.data = undefined; + } catch { + if (!this.data) { + this.data = new Map(); + } - this.data.set(key, data); - } - } - removeItem(key: string) { - if (this.data) { - this.data.delete(key); - } else { - try { - localStorage.removeItem(key); - } catch { } - } - } - clear() { - if (this.data) { - this.data.clear(); - } else { - try { - localStorage.clear(); - } catch { } - } - } - getJSON(key: string, defaultValue: T): T { - try { - return JSON.parse(this.getItem(key) || ''); - } catch { - return defaultValue; - } - } - setJSON(key: string, value: any) { - this.setItem(key, JSON.stringify(value)); - } - getInt(key: string) { - return parseInt(this.getItem(key) || '0', 10) | 0; - } - setInt(key: string, value: number) { - this.setItem(key, value.toString(10)); - } - getBoolean(key: string) { - return this.getItem(key) === 'true'; - } - setBoolean(key: string, value: boolean) { - if (value) { - this.setItem(key, 'true'); - } else { - this.removeItem(key); - } - } + this.data.set(key, data); + } + } + removeItem(key: string) { + if (this.data) { + this.data.delete(key); + } else { + try { + localStorage.removeItem(key); + } catch { } + } + } + clear() { + if (this.data) { + this.data.clear(); + } else { + try { + localStorage.clear(); + } catch { } + } + } + getJSON(key: string, defaultValue: T): T { + try { + return JSON.parse(this.getItem(key) || ''); + } catch { + return defaultValue; + } + } + setJSON(key: string, value: any) { + this.setItem(key, JSON.stringify(value)); + } + getInt(key: string) { + return parseInt(this.getItem(key) || '0', 10) | 0; + } + setInt(key: string, value: number) { + this.setItem(key, value.toString(10)); + } + getBoolean(key: string) { + return this.getItem(key) === 'true'; + } + setBoolean(key: string, value: boolean) { + if (value) { + this.setItem(key, 'true'); + } else { + this.removeItem(key); + } + } } diff --git a/src/ts/components/shared/action-bar/action-bar.ts b/src/ts/components/shared/action-bar/action-bar.ts index 04e2d98..c370fa5 100644 --- a/src/ts/components/shared/action-bar/action-bar.ts +++ b/src/ts/components/shared/action-bar/action-bar.ts @@ -8,77 +8,77 @@ import { ACTIONS_LIMIT } from '../../../common/constants'; import { last } from '../../../common/utils'; @Component({ - selector: 'action-bar', - templateUrl: 'action-bar.pug', - styleUrls: ['action-bar.scss'], + selector: 'action-bar', + templateUrl: 'action-bar.pug', + styleUrls: ['action-bar.scss'], }) export class ActionBar { - @ViewChild('scroller', { static: true }) scroller!: ElementRef; - @Input() blurred = false; - activeAction: ButtonAction | undefined = undefined; - shortcuts = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=']; - private _editable = false; - constructor(private game: PonyTownGame, private settings: SettingsService) { - } - @Input() get editable() { - return this._editable; - } - set editable(value) { - if (this._editable !== value) { - this._editable = value; - this.updateFreeSlots(); + @ViewChild('scroller', { static: true }) scroller!: ElementRef; + @Input() blurred = false; + activeAction: ButtonAction | undefined = undefined; + shortcuts = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=']; + private _editable = false; + constructor(private game: PonyTownGame, private settings: SettingsService) { + } + @Input() get editable() { + return this._editable; + } + set editable(value) { + if (this._editable !== value) { + this._editable = value; + this.updateFreeSlots(); - if (!value) { - this.save(); - } - } - } - get actions() { - return this.game.actions; - } - get mobile() { - return isMobile; - } - get hasScroller() { - return this.editable && isMobile; - } - get blurCount() { - const boxWidth = isMobile ? 50 : 40; - const width = 450 + this.scroller.nativeElement.scrollLeft; - return Math.floor(width / boxWidth); - } - use(action: ButtonAction | undefined) { - useAction(this.game, action); - } - drag(index: number) { - this.actions[index].action = undefined; - this.updateFreeSlots(); - } - drop(action: ButtonAction | undefined, index: number) { - this.actions[index].action = action; - this.updateFreeSlots(); - } - save() { - const settings = { ...this.settings.account, actions: serializeActions(this.actions) }; - this.settings.saveAccountSettings(settings); - } - scroll(e: MouseWheelEvent) { - if (e.deltaY) { - const delta = e.deltaY > 0 ? 1 : -1; - this.scroller.nativeElement.scrollLeft += delta * 20; - } - } - private updateFreeSlots() { - const actions = this.actions; + if (!value) { + this.save(); + } + } + } + get actions() { + return this.game.actions; + } + get mobile() { + return isMobile; + } + get hasScroller() { + return this.editable && isMobile; + } + get blurCount() { + const boxWidth = isMobile ? 50 : 40; + const width = 450 + this.scroller.nativeElement.scrollLeft; + return Math.floor(width / boxWidth); + } + use(action: ButtonAction | undefined) { + useAction(this.game, action); + } + drag(index: number) { + this.actions[index].action = undefined; + this.updateFreeSlots(); + } + drop(action: ButtonAction | undefined, index: number) { + this.actions[index].action = action; + this.updateFreeSlots(); + } + save() { + const settings = { ...this.settings.account, actions: serializeActions(this.actions) }; + this.settings.saveAccountSettings(settings); + } + scroll(e: MouseWheelEvent) { + if (e.deltaY) { + const delta = e.deltaY > 0 ? 1 : -1; + this.scroller.nativeElement.scrollLeft += delta * 20; + } + } + private updateFreeSlots() { + const actions = this.actions; - if (this.editable) { - while (actions.length < 5 || (last(actions)!.action !== undefined && actions.length < ACTIONS_LIMIT)) { - actions.push({ action: undefined }); - } - } else { - while (actions.length > 0 && last(actions)!.action === undefined) { - actions.pop(); - } - } - } + if (this.editable) { + while (actions.length < 5 || (last(actions)!.action !== undefined && actions.length < ACTIONS_LIMIT)) { + actions.push({ action: undefined }); + } + } else { + while (actions.length > 0 && last(actions)!.action === undefined) { + actions.pop(); + } + } + } } diff --git a/src/ts/components/shared/action-button/action-button.ts b/src/ts/components/shared/action-button/action-button.ts index 843ac2a..6c5939a 100644 --- a/src/ts/components/shared/action-button/action-button.ts +++ b/src/ts/components/shared/action-button/action-button.ts @@ -5,42 +5,42 @@ import { drawAction } from '../../../client/buttonActions'; import { removeItem } from '../../../common/utils'; @Component({ - selector: 'action-button', - templateUrl: 'action-button.pug', - styleUrls: ['action-button.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { - '[class.empty]': '!editable && !action', - }, + selector: 'action-button', + templateUrl: 'action-button.pug', + styleUrls: ['action-button.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.empty]': '!editable && !action', + }, }) export class ActionButton { - @Input() action?: ButtonAction; - @Input() editable = false; - @Input() active = false; - @Input() shadow = true; - @Input() shortcut = ''; - @Output() use = new EventEmitter(); - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - dirty = true; - private state: any = {}; - constructor(private game: PonyTownGame) { - } - ngOnInit() { - actionButtons.push(this); - } - ngOnDestroy() { - removeItem(actionButtons, this); - } - ngOnChanges() { - this.dirty = true; - } - click() { - if (this.action) { - this.use.emit(this.action); - } - } - draw() { - drawAction(this.canvas.nativeElement, this.action, this.state, this.game); - this.dirty = false; - } + @Input() action?: ButtonAction; + @Input() editable = false; + @Input() active = false; + @Input() shadow = true; + @Input() shortcut = ''; + @Output() use = new EventEmitter(); + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + dirty = true; + private state: any = {}; + constructor(private game: PonyTownGame) { + } + ngOnInit() { + actionButtons.push(this); + } + ngOnDestroy() { + removeItem(actionButtons, this); + } + ngOnChanges() { + this.dirty = true; + } + click() { + if (this.action) { + this.use.emit(this.action); + } + } + draw() { + drawAction(this.canvas.nativeElement, this.action, this.state, this.game); + this.dirty = false; + } } diff --git a/src/ts/components/shared/actions-modal/actions-modal.ts b/src/ts/components/shared/actions-modal/actions-modal.ts index 944b3cd..b187f9d 100644 --- a/src/ts/components/shared/actions-modal/actions-modal.ts +++ b/src/ts/components/shared/actions-modal/actions-modal.ts @@ -1,13 +1,13 @@ import { Component, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; import { - createButtionActionActions, expressionButtonAction, createButtonCommandActions, - createDefaultButtonActions, actionExpressionDefaultPalette, entityButtonAction + createButtionActionActions, expressionButtonAction, createButtonCommandActions, + createDefaultButtonActions, actionExpressionDefaultPalette, entityButtonAction } from '../../../client/buttonActions'; import * as sprites from '../../../generated/sprites'; import { - Eye, Muzzle, ColorExtraSet, ColorExtra, Iris, ExpressionExtra, PonyEye, ButtonAction, Action, - ButtonActionSlot, EntityButtonAction + Eye, Muzzle, ColorExtraSet, ColorExtra, Iris, ExpressionExtra, PonyEye, ButtonAction, Action, + ButtonActionSlot, EntityButtonAction } from '../../../common/interfaces'; import { createExpression } from '../../../client/clientUtils'; import { ACTION_EXPRESSION_BG, ACTION_EXPRESSION_EYE_COLOR, fillToOutline } from '../../../common/colors'; @@ -18,137 +18,137 @@ import { PonyTownGame } from '../../../client/game'; import { getEntityNames } from '../../services/model'; function eyeSprite(e: PonyEye | undefined) { - return createEyeSprite(e, 0, sprites.defaultPalette); + return createEyeSprite(e, 0, sprites.defaultPalette); } @Component({ - selector: 'actions-modal', - templateUrl: 'actions-modal.pug', - styleUrls: ['actions-modal.scss'], + selector: 'actions-modal', + templateUrl: 'actions-modal.pug', + styleUrls: ['actions-modal.scss'], }) export class ActionsModal implements OnInit, OnDestroy { - readonly lockIcon = faLock; - readonly actionsIcon = faApple; - readonly expressionsIcon = faLaughBeam; - readonly chatIcon = faComment; - readonly optionsIcon = faCog; - readonly devIcon = faCogs; - readonly dev = BETA; - @Output() close = new EventEmitter(); - actions = createButtionActionActions(); - commands = createButtonCommandActions(); - emoteAction = expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile)); - entityAction = entityButtonAction('apple'); - entityActions: EntityButtonAction[] = []; - entityName = 'apple'; - lockEyes = true; - lockIrises = true; - eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite); - eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite); - irisesLeft: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeLeft[1]![0]!, i, sprites.defaultPalette)); - irisesRight: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeRight[1]![0]!, i, sprites.defaultPalette)); - muzzles: ColorExtraSet = sprites.noses - .map(n => n[0][0]) - .map(({ color, colors, mouth }) => ({ - color, colors, extra: mouth, palettes: [actionExpressionDefaultPalette.colors] - } as ColorExtra)); - noseFills = [ACTION_EXPRESSION_BG]; - noseOutlines = [fillToOutline(ACTION_EXPRESSION_BG)]; - coatFill = ACTION_EXPRESSION_BG; - eyeColor = ACTION_EXPRESSION_EYE_COLOR; - muzzle: Muzzle = 0; - eyeLeft: Eye = 1; - eyeRight: Eye = 1; - irisLeft: Iris = 0; - irisRight: Iris = 0; - tabIndex = 0; - blush = false; - sleeping = false; - tears = false; - crying = false; - hearts = false; - activeTab = 'right-eye'; - private interval: any = 0; - private subscription?: Subscription; - private actionsToUndo: ButtonActionSlot[][] = []; - constructor(private game: PonyTownGame) { - this.updateEmoteAction(); - } - ngOnInit() { - document.body.classList.add('actions-modal-opened'); - this.game.editingActions = true; - this.interval = setInterval(() => this.game.send(server => server.action(Action.KeepAlive)), 10000); - this.subscription = this.game.onLeft.subscribe(() => this.ok()); + readonly lockIcon = faLock; + readonly actionsIcon = faApple; + readonly expressionsIcon = faLaughBeam; + readonly chatIcon = faComment; + readonly optionsIcon = faCog; + readonly devIcon = faCogs; + readonly dev = BETA; + @Output() close = new EventEmitter(); + actions = createButtionActionActions(); + commands = createButtonCommandActions(); + emoteAction = expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile)); + entityAction = entityButtonAction('apple'); + entityActions: EntityButtonAction[] = []; + entityName = 'apple'; + lockEyes = true; + lockIrises = true; + eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite); + eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite); + irisesLeft: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeLeft[1]![0]!, i, sprites.defaultPalette)); + irisesRight: ColorExtraSet = times(Iris.COUNT, i => createEyeSprite(sprites.eyeRight[1]![0]!, i, sprites.defaultPalette)); + muzzles: ColorExtraSet = sprites.noses + .map(n => n[0][0]) + .map(({ color, colors, mouth }) => ({ + color, colors, extra: mouth, palettes: [actionExpressionDefaultPalette.colors] + } as ColorExtra)); + noseFills = [ACTION_EXPRESSION_BG]; + noseOutlines = [fillToOutline(ACTION_EXPRESSION_BG)]; + coatFill = ACTION_EXPRESSION_BG; + eyeColor = ACTION_EXPRESSION_EYE_COLOR; + muzzle: Muzzle = 0; + eyeLeft: Eye = 1; + eyeRight: Eye = 1; + irisLeft: Iris = 0; + irisRight: Iris = 0; + tabIndex = 0; + blush = false; + sleeping = false; + tears = false; + crying = false; + hearts = false; + activeTab = 'right-eye'; + private interval: any = 0; + private subscription?: Subscription; + private actionsToUndo: ButtonActionSlot[][] = []; + constructor(private game: PonyTownGame) { + this.updateEmoteAction(); + } + ngOnInit() { + document.body.classList.add('actions-modal-opened'); + this.game.editingActions = true; + this.interval = setInterval(() => this.game.send(server => server.action(Action.KeepAlive)), 10000); + this.subscription = this.game.onLeft.subscribe(() => this.ok()); - if (BETA) { - this.entityActions = getEntityNames().map(name => entityButtonAction(name)); - } - } - ngOnDestroy() { - document.body.classList.remove('actions-modal-opened'); - this.game.editingActions = false; - clearInterval(this.interval); - this.subscription && this.subscription.unsubscribe(); - } - ok() { - this.close.emit(); - } - changed(locked: boolean) { - if (locked) { - this.eyeLeft = this.eyeRight; - } + if (BETA) { + this.entityActions = getEntityNames().map(name => entityButtonAction(name)); + } + } + ngOnDestroy() { + document.body.classList.remove('actions-modal-opened'); + this.game.editingActions = false; + clearInterval(this.interval); + this.subscription && this.subscription.unsubscribe(); + } + ok() { + this.close.emit(); + } + changed(locked: boolean) { + if (locked) { + this.eyeLeft = this.eyeRight; + } - if (this.lockIrises) { - this.irisLeft = this.irisRight; - } + if (this.lockIrises) { + this.irisLeft = this.irisRight; + } - this.updateEmoteAction(); - } - drop(action: ButtonAction) { - if (action.type === 'expression' && action.expression) { - const e = action.expression; - this.lockEyes = e.right === e.left; - this.lockIrises = e.rightIris === e.leftIris; - this.eyeRight = e.right; - this.eyeLeft = e.left; - this.muzzle = e.muzzle; - this.irisRight = e.rightIris; - this.irisLeft = e.leftIris; - this.blush = hasFlag(e.extra, ExpressionExtra.Blush); - this.sleeping = hasFlag(e.extra, ExpressionExtra.Zzz); - this.tears = hasFlag(e.extra, ExpressionExtra.Tears); - this.crying = hasFlag(e.extra, ExpressionExtra.Cry); - this.hearts = hasFlag(e.extra, ExpressionExtra.Hearts); - this.changed(this.lockEyes); - } - } - updateEmoteAction() { - const extra = - (this.blush ? ExpressionExtra.Blush : 0) | - (this.sleeping ? ExpressionExtra.Zzz : 0) | - (this.tears ? ExpressionExtra.Tears : 0) | - (this.crying ? ExpressionExtra.Cry : 0) | - (this.hearts ? ExpressionExtra.Hearts : 0); + this.updateEmoteAction(); + } + drop(action: ButtonAction) { + if (action.type === 'expression' && action.expression) { + const e = action.expression; + this.lockEyes = e.right === e.left; + this.lockIrises = e.rightIris === e.leftIris; + this.eyeRight = e.right; + this.eyeLeft = e.left; + this.muzzle = e.muzzle; + this.irisRight = e.rightIris; + this.irisLeft = e.leftIris; + this.blush = hasFlag(e.extra, ExpressionExtra.Blush); + this.sleeping = hasFlag(e.extra, ExpressionExtra.Zzz); + this.tears = hasFlag(e.extra, ExpressionExtra.Tears); + this.crying = hasFlag(e.extra, ExpressionExtra.Cry); + this.hearts = hasFlag(e.extra, ExpressionExtra.Hearts); + this.changed(this.lockEyes); + } + } + updateEmoteAction() { + const extra = + (this.blush ? ExpressionExtra.Blush : 0) | + (this.sleeping ? ExpressionExtra.Zzz : 0) | + (this.tears ? ExpressionExtra.Tears : 0) | + (this.crying ? ExpressionExtra.Cry : 0) | + (this.hearts ? ExpressionExtra.Hearts : 0); - const expression = createExpression(this.eyeRight, this.eyeLeft, this.muzzle, this.irisRight, this.irisLeft, extra); - this.emoteAction = expressionButtonAction(expression); - } - resetToDefault() { - this.actionsToUndo.push(this.game.actions); - this.game.actions = [...createDefaultButtonActions(), { action: undefined }]; - } - clearActionBar() { - this.actionsToUndo.push(this.game.actions); - this.game.actions = this.game.actions.map(() => ({ action: undefined })); - } - undo() { - if (this.actionsToUndo.length) { - this.game.actions = this.actionsToUndo.pop()!; - } - } - updateEntity() { - if (BETA) { - this.entityAction = entityButtonAction(this.entityName); - } - } + const expression = createExpression(this.eyeRight, this.eyeLeft, this.muzzle, this.irisRight, this.irisLeft, extra); + this.emoteAction = expressionButtonAction(expression); + } + resetToDefault() { + this.actionsToUndo.push(this.game.actions); + this.game.actions = [...createDefaultButtonActions(), { action: undefined }]; + } + clearActionBar() { + this.actionsToUndo.push(this.game.actions); + this.game.actions = this.game.actions.map(() => ({ action: undefined })); + } + undo() { + if (this.actionsToUndo.length) { + this.game.actions = this.actionsToUndo.pop()!; + } + } + updateEntity() { + if (BETA) { + this.entityAction = entityButtonAction(this.entityName); + } + } } diff --git a/src/ts/components/shared/bitmap-box/bitmap-box.ts b/src/ts/components/shared/bitmap-box/bitmap-box.ts index c015ef1..d8aa5f1 100644 --- a/src/ts/components/shared/bitmap-box/bitmap-box.ts +++ b/src/ts/components/shared/bitmap-box/bitmap-box.ts @@ -2,44 +2,44 @@ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from import { parseColor, colorToCSS } from '../../../common/color'; @Component({ - selector: 'bitmap-box', - templateUrl: 'bitmap-box.pug', - styleUrls: ['bitmap-box.scss'], + selector: 'bitmap-box', + templateUrl: 'bitmap-box.pug', + styleUrls: ['bitmap-box.scss'], }) export class BitmapBox implements OnChanges { - @Input() width = 5; - @Input() height = 5; - @Input() bitmap?: string[]; - @Input() tool?: string; - @Input() color = 'red'; - @Output() colorChange = new EventEmitter(); - rows?: number[][]; - ngOnChanges(changes: SimpleChanges) { - if (changes.width || changes.height) { - this.rows = []; + @Input() width = 5; + @Input() height = 5; + @Input() bitmap?: string[]; + @Input() tool?: string; + @Input() color = 'red'; + @Output() colorChange = new EventEmitter(); + rows?: number[][]; + ngOnChanges(changes: SimpleChanges) { + if (changes.width || changes.height) { + this.rows = []; - for (let y = 0; y < this.height; y++) { - this.rows[y] = []; + for (let y = 0; y < this.height; y++) { + this.rows[y] = []; - for (let x = 0; x < this.width; x++) { - this.rows[y][x] = x + this.width * y; - } - } - } - } - draw(index: number) { - if (this.bitmap) { - if (this.tool === 'eraser') { - this.bitmap[index] = ''; - } else if (this.tool === 'brush') { - this.bitmap[index] = parseColor(this.bitmap[index]) === parseColor(this.color) ? '' : this.color; - } else if (this.tool === 'eyedropper') { - this.color = this.bitmap[index]; - this.colorChange.emit(this.color); - } - } - } - colorAt(index: number) { - return this.bitmap && this.bitmap[index] ? colorToCSS(parseColor(this.bitmap[index])) : ''; - } + for (let x = 0; x < this.width; x++) { + this.rows[y][x] = x + this.width * y; + } + } + } + } + draw(index: number) { + if (this.bitmap) { + if (this.tool === 'eraser') { + this.bitmap[index] = ''; + } else if (this.tool === 'brush') { + this.bitmap[index] = parseColor(this.bitmap[index]) === parseColor(this.color) ? '' : this.color; + } else if (this.tool === 'eyedropper') { + this.color = this.bitmap[index]; + this.colorChange.emit(this.color); + } + } + } + colorAt(index: number) { + return this.bitmap && this.bitmap[index] ? colorToCSS(parseColor(this.bitmap[index])) : ''; + } } diff --git a/src/ts/components/shared/butt-mark-editor/butt-mark-editor.ts b/src/ts/components/shared/butt-mark-editor/butt-mark-editor.ts index 1c421ac..3f440d6 100644 --- a/src/ts/components/shared/butt-mark-editor/butt-mark-editor.ts +++ b/src/ts/components/shared/butt-mark-editor/butt-mark-editor.ts @@ -5,26 +5,26 @@ import { CM_SIZE } from '../../../common/constants'; import { faTrash, faEraser, faPaintBrush, faEyeDropper } from '../../../client/icons'; export interface ButtMarkEditorState { - brushType: string; - brush: string; + brushType: string; + brush: string; } @Component({ - selector: 'butt-mark-editor', - templateUrl: 'butt-mark-editor.pug', + selector: 'butt-mark-editor', + templateUrl: 'butt-mark-editor.pug', }) export class ButtMarkEditor { - readonly trashIcon = faTrash; - readonly eraserIcon = faEraser; - readonly eyeDropperIcon = faEyeDropper; - readonly paintBrushIcon = faPaintBrush; - readonly cmSize = CM_SIZE; - @Input() info!: PonyInfo; - @Input() state = { - brushType: 'brush', - brush: 'orange', - }; - clearCM() { - fill(this.info.cm!, ''); - } + readonly trashIcon = faTrash; + readonly eraserIcon = faEraser; + readonly eyeDropperIcon = faEyeDropper; + readonly paintBrushIcon = faPaintBrush; + readonly cmSize = CM_SIZE; + @Input() info!: PonyInfo; + @Input() state = { + brushType: 'brush', + brush: 'orange', + }; + clearCM() { + fill(this.info.cm!, ''); + } } diff --git a/src/ts/components/shared/character-list/character-list.ts b/src/ts/components/shared/character-list/character-list.ts index 3a961b1..9ec8eba 100644 --- a/src/ts/components/shared/character-list/character-list.ts +++ b/src/ts/components/shared/character-list/character-list.ts @@ -9,164 +9,164 @@ import { LATEST_CHARACTER_LIMIT } from '../../../common/constants'; import { faHashtag } from '../../../client/icons'; function getSortTag(pony: PonyObject) { - const match = pony.desc && /(?:^| )@(top|end|\d+)(?:$| )/.exec(pony.desc); - return match && match[1]; + const match = pony.desc && /(?:^| )@(top|end|\d+)(?:$| )/.exec(pony.desc); + return match && match[1]; } function sortTagToNumber(tag: string) { - if (tag === 'top') { - return -1; - } else if (tag === 'end') { - return 999999999; - } else { - return +tag; - } + if (tag === 'top') { + return -1; + } else if (tag === 'end') { + return 999999999; + } else { + return +tag; + } } function fallbackComparePonies(a: PonyObject, b: PonyObject) { - return a.name.localeCompare(b.name) || (a.desc || '').localeCompare(b.desc || ''); + return a.name.localeCompare(b.name) || (a.desc || '').localeCompare(b.desc || ''); } function comparePonies(a: PonyObject, b: PonyObject) { - const aTag = getSortTag(a); - const bTag = getSortTag(b); + const aTag = getSortTag(a); + const bTag = getSortTag(b); - if (aTag && bTag) { - return (sortTagToNumber(aTag) - sortTagToNumber(bTag)) || fallbackComparePonies(a, b); - } else if (aTag) { - return aTag === 'end' ? 1 : -1; - } else if (bTag) { - return bTag === 'end' ? -1 : 1; - } else { - return fallbackComparePonies(a, b); - } + if (aTag && bTag) { + return (sortTagToNumber(aTag) - sortTagToNumber(bTag)) || fallbackComparePonies(a, b); + } else if (aTag) { + return aTag === 'end' ? 1 : -1; + } else if (bTag) { + return bTag === 'end' ? -1 : 1; + } else { + return fallbackComparePonies(a, b); + } } @Component({ - selector: 'character-list', - templateUrl: 'character-list.pug', - styleUrls: ['character-list.scss'], + selector: 'character-list', + templateUrl: 'character-list.pug', + styleUrls: ['character-list.scss'], }) export class CharacterList implements OnInit { - readonly hashIcon = faHashtag; - @Input() inGame = false; - @Input() canNew = false; - @Output() close = new EventEmitter(); - @Output() newCharacter = new EventEmitter(); - @Output() selectCharacter = new EventEmitter(); - @Output() previewCharacter = new EventEmitter(); - @ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef; - @ViewChild('searchInput', { static: true }) searchInput!: ElementRef; - search?: string; - selectedIndex = -1; - ponies: PonyObject[] = []; - tags: string[] = []; - private previewPony: PonyObject | undefined = undefined; - constructor(private model: Model, private zone: NgZone) { - } - get selectedPony() { - return this.model.pony; - } - get searchable() { - return this.model.ponies.length > LATEST_CHARACTER_LIMIT; - } - get placeholder() { - return `search (${this.model.ponies.length} / ${this.model.characterLimit} ponies)`; - } - ngOnInit() { - this.updatePonies(); + readonly hashIcon = faHashtag; + @Input() inGame = false; + @Input() canNew = false; + @Output() close = new EventEmitter(); + @Output() newCharacter = new EventEmitter(); + @Output() selectCharacter = new EventEmitter(); + @Output() previewCharacter = new EventEmitter(); + @ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef; + @ViewChild('searchInput', { static: true }) searchInput!: ElementRef; + search?: string; + selectedIndex = -1; + ponies: PonyObject[] = []; + tags: string[] = []; + private previewPony: PonyObject | undefined = undefined; + constructor(private model: Model, private zone: NgZone) { + } + get selectedPony() { + return this.model.pony; + } + get searchable() { + return this.model.ponies.length > LATEST_CHARACTER_LIMIT; + } + get placeholder() { + return `search (${this.model.ponies.length} / ${this.model.characterLimit} ponies)`; + } + ngOnInit() { + this.updatePonies(); - this.tags = uniq(flatten(this.ponies.map(p => (p.desc || '').split(/ /g).map(x => x.trim()))) - .filter(x => /^#/.test(x))) - .sort(); + this.tags = uniq(flatten(this.ponies.map(p => (p.desc || '').split(/ /g).map(x => x.trim()))) + .filter(x => /^#/.test(x))) + .sort(); - if (!isMobile) { - setTimeout(() => this.searchInput.nativeElement.focus()); - } - } - keydown(e: KeyboardEvent) { - if (e.keyCode === Key.ESCAPE) { - if (this.search) { - e.preventDefault(); - e.stopPropagation(); - this.search = ''; - this.updatePonies(); - } else { - this.closed(); - } - } else if (e.keyCode === Key.ENTER) { - const pony = this.ponies[this.selectedIndex]; + if (!isMobile) { + setTimeout(() => this.searchInput.nativeElement.focus()); + } + } + keydown(e: KeyboardEvent) { + if (e.keyCode === Key.ESCAPE) { + if (this.search) { + e.preventDefault(); + e.stopPropagation(); + this.search = ''; + this.updatePonies(); + } else { + this.closed(); + } + } else if (e.keyCode === Key.ENTER) { + const pony = this.ponies[this.selectedIndex]; - if (pony) { - this.select(pony); - } else { - this.closed(); - } - } else if (e.keyCode === Key.UP) { - this.setSelectedIndex(this.selectedIndex <= 0 ? (this.ponies.length - 1) : (this.selectedIndex - 1)); - } else if (e.keyCode === Key.DOWN) { - this.setSelectedIndex(this.selectedIndex === (this.ponies.length - 1) ? 0 : (this.selectedIndex + 1)); - } - } - setPreview(pony: PonyObject) { - this.previewPony = pony; - this.previewCharacter.emit(this.model.parsePonyObject(pony)); - } - unsetPreview(pony: PonyObject) { - if (this.previewPony && pony && this.previewPony.id === pony.id) { - this.previewPony = undefined; - this.previewCharacter.emit(undefined); - } - } - updatePonies() { - this.zone.run(() => { - const query = this.search && this.search.toLowerCase().trim(); + if (pony) { + this.select(pony); + } else { + this.closed(); + } + } else if (e.keyCode === Key.UP) { + this.setSelectedIndex(this.selectedIndex <= 0 ? (this.ponies.length - 1) : (this.selectedIndex - 1)); + } else if (e.keyCode === Key.DOWN) { + this.setSelectedIndex(this.selectedIndex === (this.ponies.length - 1) ? 0 : (this.selectedIndex + 1)); + } + } + setPreview(pony: PonyObject) { + this.previewPony = pony; + this.previewCharacter.emit(this.model.parsePonyObject(pony)); + } + unsetPreview(pony: PonyObject) { + if (this.previewPony && pony && this.previewPony.id === pony.id) { + this.previewPony = undefined; + this.previewCharacter.emit(undefined); + } + } + updatePonies() { + this.zone.run(() => { + const query = this.search && this.search.toLowerCase().trim(); - function matchesWords(text: string, words: string[]) { - for (const word of words) { - if (text.indexOf(word) === -1) { - return false; - } - } + function matchesWords(text: string, words: string[]) { + for (const word of words) { + if (text.indexOf(word) === -1) { + return false; + } + } - return true; - } + return true; + } - if (query) { - const words = query.split(/ /g).map(x => x.trim()); + if (query) { + const words = query.split(/ /g).map(x => x.trim()); - this.ponies = this.model.ponies.filter(pony => { - const text = `${pony.name} ${pony.desc || ''}`.toLowerCase(); - return matchesWords(text, words); - }).sort(comparePonies); - } else { - this.ponies = this.model.ponies.slice().sort(comparePonies); - } + this.ponies = this.model.ponies.filter(pony => { + const text = `${pony.name} ${pony.desc || ''}`.toLowerCase(); + return matchesWords(text, words); + }).sort(comparePonies); + } else { + this.ponies = this.model.ponies.slice().sort(comparePonies); + } - this.setSelectedIndex(this.selectedIndex); - this.previewCharacter.emit(undefined); - }); - } - select(pony: PonyObject) { - this.selectCharacter.emit(pony); - } - createNew() { - this.newCharacter.emit(); - } - private closed() { - this.zone.run(() => this.close.emit()); - } - private setSelectedIndex(index: number) { - this.zone.run(() => { - this.selectedIndex = clamp(index, -1, this.ponies.length - 1); - const pony = this.ponies[index]; - this.ariaAnnounce.nativeElement.textContent = pony ? pony.name : ''; + this.setSelectedIndex(this.selectedIndex); + this.previewCharacter.emit(undefined); + }); + } + select(pony: PonyObject) { + this.selectCharacter.emit(pony); + } + createNew() { + this.newCharacter.emit(); + } + private closed() { + this.zone.run(() => this.close.emit()); + } + private setSelectedIndex(index: number) { + this.zone.run(() => { + this.selectedIndex = clamp(index, -1, this.ponies.length - 1); + const pony = this.ponies[index]; + this.ariaAnnounce.nativeElement.textContent = pony ? pony.name : ''; - if (pony) { - this.setPreview(pony); - } else if (this.previewPony) { - this.unsetPreview(this.previewPony); - } - }); - } + if (pony) { + this.setPreview(pony); + } else if (this.previewPony) { + this.unsetPreview(this.previewPony); + } + }); + } } diff --git a/src/ts/components/shared/character-preview/character-preview.ts b/src/ts/components/shared/character-preview/character-preview.ts index 42f8e6d..c57ffca 100644 --- a/src/ts/components/shared/character-preview/character-preview.ts +++ b/src/ts/components/shared/character-preview/character-preview.ts @@ -1,11 +1,11 @@ import { - Component, Input, ElementRef, AfterViewInit, OnDestroy, NgZone, ViewChild, OnChanges, HostListener + Component, Input, ElementRef, AfterViewInit, OnDestroy, NgZone, ViewChild, OnChanges, HostListener } from '@angular/core'; import { PonyInfo, PonyState } from '../../../common/interfaces'; import { toPalette } from '../../../common/ponyInfo'; import { GRASS_COLOR, TRANSPARENT } from '../../../common/colors'; import { - createCanvas, disableImageSmoothing, getPixelRatio, resizeCanvas, resizeCanvasWithRatio + createCanvas, disableImageSmoothing, getPixelRatio, resizeCanvas, resizeCanvasWithRatio } from '../../../client/canvasUtils'; import { BLINK_FRAMES } from '../../../client/ponyUtils'; import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers'; @@ -21,172 +21,172 @@ const DEFAULT_STATE = defaultPonyState(); const DEFAULT_OPTIONS = defaultDrawPonyOptions(); @Component({ - selector: 'character-preview', - template: '', - styles: [`:host { display: block; } canvas { width: 100%; height: 100%; }`], + selector: 'character-preview', + template: '', + styles: [`:host { display: block; } canvas { width: 100%; height: 100%; }`], }) export class CharacterPreview implements OnDestroy, OnChanges, AfterViewInit { - @Input() scale = 3; - @Input() name?: string; - @Input() tag?: string; - @Input() pony?: PonyInfo; - @Input() state?: PonyState = defaultPonyState(); - @Input() noBackground = false; - @Input() noOutline = false; - @Input() noShadow = false; - @Input() extra = false; - @Input() passive = false; - @Input() blinks = true; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - private batch?: ContextSpriteBatch; - private nameBatch?: ContextSpriteBatch; - private frame = 0; - private lastFrame = 0; - private initialized = false; - private nextBlink = performance.now() + 2000; - private blinkFrame = -1; - constructor(private zone: NgZone) { - } - ngAfterViewInit() { - return loadAndInitSpriteSheets() - .then(() => this.initialized = true) - .then(() => this.ngOnChanges()); - } - ngOnDestroy() { - cancelAnimationFrame(this.frame); - } - ngOnChanges() { - if (!this.frame) { - this.zone.runOutsideAngular(() => this.frame = requestAnimationFrame(this.onFrame)); - } - } - @HostListener('window:resize') - redraw() { - this.tryDraw(); - } - blink() { - this.nextBlink = performance.now(); - } - private onFrame = () => { - if (this.passive && this.initialized) { - this.frame = 0; - this.tryDraw(); - return; - } + @Input() scale = 3; + @Input() name?: string; + @Input() tag?: string; + @Input() pony?: PonyInfo; + @Input() state?: PonyState = defaultPonyState(); + @Input() noBackground = false; + @Input() noOutline = false; + @Input() noShadow = false; + @Input() extra = false; + @Input() passive = false; + @Input() blinks = true; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + private batch?: ContextSpriteBatch; + private nameBatch?: ContextSpriteBatch; + private frame = 0; + private lastFrame = 0; + private initialized = false; + private nextBlink = performance.now() + 2000; + private blinkFrame = -1; + constructor(private zone: NgZone) { + } + ngAfterViewInit() { + return loadAndInitSpriteSheets() + .then(() => this.initialized = true) + .then(() => this.ngOnChanges()); + } + ngOnDestroy() { + cancelAnimationFrame(this.frame); + } + ngOnChanges() { + if (!this.frame) { + this.zone.runOutsideAngular(() => this.frame = requestAnimationFrame(this.onFrame)); + } + } + @HostListener('window:resize') + redraw() { + this.tryDraw(); + } + blink() { + this.nextBlink = performance.now(); + } + private onFrame = () => { + if (this.passive && this.initialized) { + this.frame = 0; + this.tryDraw(); + return; + } - this.frame = requestAnimationFrame(this.onFrame); + this.frame = requestAnimationFrame(this.onFrame); - const now = performance.now(); + const now = performance.now(); - if ((now - this.lastFrame) > (1000 / 24)) { - if (this.blinks) { - if (this.blinkFrame === -1) { - if (this.nextBlink < now) { - this.blinkFrame = 0; - } - } else { - this.blinkFrame++; + if ((now - this.lastFrame) > (1000 / 24)) { + if (this.blinks) { + if (this.blinkFrame === -1) { + if (this.nextBlink < now) { + this.blinkFrame = 0; + } + } else { + this.blinkFrame++; - if (this.blinkFrame >= BLINK_FRAMES.length) { - this.nextBlink = now + Math.random() * 2000 + 3000; - this.blinkFrame = -1; - } - } + if (this.blinkFrame >= BLINK_FRAMES.length) { + this.nextBlink = now + Math.random() * 2000 + 3000; + this.blinkFrame = -1; + } + } - if (this.state) { - this.state.blinkFrame = this.blinkFrame === -1 ? 1 : BLINK_FRAMES[this.blinkFrame]; - } - } + if (this.state) { + this.state.blinkFrame = this.blinkFrame === -1 ? 1 : BLINK_FRAMES[this.blinkFrame]; + } + } - this.lastFrame = now; - this.tryDraw(); - } - } - private tryDraw() { - try { - this.draw(); - } catch { } - } - private draw() { - if (!this.initialized) - return; + this.lastFrame = now; + this.tryDraw(); + } + } + private tryDraw() { + try { + this.draw(); + } catch { } + } + private draw() { + if (!this.initialized) + return; - const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const canvas = this.canvas.nativeElement as HTMLCanvasElement; - const { width, height } = canvas.getBoundingClientRect(); - resizeCanvasWithRatio(canvas, width, height, false); + const { width, height } = canvas.getBoundingClientRect(); + resizeCanvasWithRatio(canvas, width, height, false); - const scale = this.scale * getPixelRatio(); - const bufferWidth = Math.round(canvas.width / scale); - const bufferHeight = Math.round(canvas.height / scale); + const scale = this.scale * getPixelRatio(); + const bufferWidth = Math.round(canvas.width / scale); + const bufferHeight = Math.round(canvas.height / scale); - if (!bufferWidth || !bufferHeight) - return; + if (!bufferWidth || !bufferHeight) + return; - this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferWidth, bufferHeight)); - resizeCanvas(this.batch.canvas, bufferWidth, bufferHeight); + this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferWidth, bufferHeight)); + resizeCanvas(this.batch.canvas, bufferWidth, bufferHeight); - const x = Math.round(bufferWidth / 2); - const y = Math.round(bufferHeight / 2 + 28); + const x = Math.round(bufferWidth / 2); + const y = Math.round(bufferHeight / 2 + 28); - if (this.pony) { - this.batch.start(paletteSpriteSheet, this.noBackground ? TRANSPARENT : GRASS_COLOR); + if (this.pony) { + this.batch.start(paletteSpriteSheet, this.noBackground ? TRANSPARENT : GRASS_COLOR); - try { - const options = { ...DEFAULT_OPTIONS, shadow: !this.noShadow, extra: !!this.extra }; - drawPony(this.batch, toPalette(this.pony), this.state || DEFAULT_STATE, x, y, options); - } catch (e) { - console.error(e); - } + try { + const options = { ...DEFAULT_OPTIONS, shadow: !this.noShadow, extra: !!this.extra }; + drawPony(this.batch, toPalette(this.pony), this.state || DEFAULT_STATE, x, y, options); + } catch (e) { + console.error(e); + } - this.batch.end(); - } + this.batch.end(); + } - const viewContext = canvas.getContext('2d'); + const viewContext = canvas.getContext('2d'); - if (!viewContext) - return; + if (!viewContext) + return; - disableImageSmoothing(viewContext); + disableImageSmoothing(viewContext); - if (this.noBackground) { - viewContext.clearRect(0, 0, canvas.width, canvas.height); - } + if (this.noBackground) { + viewContext.clearRect(0, 0, canvas.width, canvas.height); + } - viewContext.save(); - viewContext.scale(scale, scale); + viewContext.save(); + viewContext.scale(scale, scale); - // draw outline - if (this.pony && this.noShadow && this.noBackground && !this.noOutline) { - for (let x = -1; x <= 1; x++) { - for (let y = -1; y <= 1; y++) { - viewContext.drawImage(this.batch.canvas, x, y); - } - } + // draw outline + if (this.pony && this.noShadow && this.noBackground && !this.noOutline) { + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + viewContext.drawImage(this.batch.canvas, x, y); + } + } - viewContext.globalCompositeOperation = 'source-in'; - viewContext.fillStyle = colorToCSS(GRASS_COLOR); - viewContext.fillRect(0, 0, viewContext.canvas.width, viewContext.canvas.height); - viewContext.globalCompositeOperation = 'source-over'; - } + viewContext.globalCompositeOperation = 'source-in'; + viewContext.fillStyle = colorToCSS(GRASS_COLOR); + viewContext.fillRect(0, 0, viewContext.canvas.width, viewContext.canvas.height); + viewContext.globalCompositeOperation = 'source-over'; + } - viewContext.drawImage(this.batch.canvas, 0, 0); - viewContext.restore(); + viewContext.drawImage(this.batch.canvas, 0, 0); + viewContext.restore(); - // draw name plate - if (!this.noShadow && this.name) { - const name = replaceEmojis(this.name); - const scale = 2 * getPixelRatio(); - const nameBufferWidth = Math.round(canvas.width / scale); - this.nameBatch = this.nameBatch || new ContextSpriteBatch(createCanvas(nameBufferWidth, 25)); - resizeCanvas(this.nameBatch.canvas, nameBufferWidth, 25); - this.nameBatch.start(paletteSpriteSheet, TRANSPARENT); - drawNamePlate(this.nameBatch, name, nameBufferWidth / 2, 11, DrawNameFlags.None, commonPalettes, this.tag); - this.nameBatch.end(); - viewContext.save(); - viewContext.scale(scale, scale); - viewContext.drawImage(this.nameBatch.canvas, 0, 10); - viewContext.restore(); - } - } + // draw name plate + if (!this.noShadow && this.name) { + const name = replaceEmojis(this.name); + const scale = 2 * getPixelRatio(); + const nameBufferWidth = Math.round(canvas.width / scale); + this.nameBatch = this.nameBatch || new ContextSpriteBatch(createCanvas(nameBufferWidth, 25)); + resizeCanvas(this.nameBatch.canvas, nameBufferWidth, 25); + this.nameBatch.start(paletteSpriteSheet, TRANSPARENT); + drawNamePlate(this.nameBatch, name, nameBufferWidth / 2, 11, DrawNameFlags.None, commonPalettes, this.tag); + this.nameBatch.end(); + viewContext.save(); + viewContext.scale(scale, scale); + viewContext.drawImage(this.nameBatch.canvas, 0, 10); + viewContext.restore(); + } + } } diff --git a/src/ts/components/shared/character-select/character-select.ts b/src/ts/components/shared/character-select/character-select.ts index 33f5688..81d11e0 100644 --- a/src/ts/components/shared/character-select/character-select.ts +++ b/src/ts/components/shared/character-select/character-select.ts @@ -12,117 +12,117 @@ import { delay } from '../../../common/utils'; import { isMobile } from '../../../client/data'; @Component({ - selector: 'character-select', - templateUrl: 'character-select.pug', - styleUrls: ['character-select.scss'], + selector: 'character-select', + templateUrl: 'character-select.pug', + styleUrls: ['character-select.scss'], }) export class CharacterSelect { - readonly maxNameLength = PLAYER_NAME_MAX_LENGTH; - readonly spinnerIcon = faSpinner; - readonly deleteIcon = faTrash; - readonly removeIcon = faTimes; - readonly confirmIcon = faCheck; - @Input() newButton = false; - @Input() editButton = false; - @Input() removeButton = false; - @Input() error?: string; - @Output() errorChange = new EventEmitter(); - @Output() change = new EventEmitter(); - @Output() preview = new EventEmitter(); - @ViewChild('nameInput', { static: true }) nameInput!: ElementRef; - @ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef; - @ViewChild('dropdown', { static: true }) dropdown!: Dropdown; - removing = false; - private locked = false; // TEMP: move to model - constructor( - private element: ElementRef, - private router: Router, - private model: Model, - private gameService: GameService, - ) { - } - get joining() { - return this.gameService.joining; - } - get pony() { - return this.model.pony; - } - get canNew() { - return !this.joining && this.model.account && this.model.account.characterCount < this.model.characterLimit; - } - get canEdit() { - return !this.joining; - } - get canRemove() { - return !this.joining && !this.locked && !this.model.pending && !!this.pony - && !!this.pony.id && this.error !== VERSION_ERROR; - } - get hasPonies() { - return !!this.model.ponies.length; - } - select(pony: PonyObject) { - if (pony) { - this.removing = false; - this.model.selectPony(pony); - this.change.emit(pony); - this.preview.emit(undefined); - } + readonly maxNameLength = PLAYER_NAME_MAX_LENGTH; + readonly spinnerIcon = faSpinner; + readonly deleteIcon = faTrash; + readonly removeIcon = faTimes; + readonly confirmIcon = faCheck; + @Input() newButton = false; + @Input() editButton = false; + @Input() removeButton = false; + @Input() error?: string; + @Output() errorChange = new EventEmitter(); + @Output() change = new EventEmitter(); + @Output() preview = new EventEmitter(); + @ViewChild('nameInput', { static: true }) nameInput!: ElementRef; + @ViewChild('ariaAnnounce', { static: true }) ariaAnnounce!: ElementRef; + @ViewChild('dropdown', { static: true }) dropdown!: Dropdown; + removing = false; + private locked = false; // TEMP: move to model + constructor( + private element: ElementRef, + private router: Router, + private model: Model, + private gameService: GameService, + ) { + } + get joining() { + return this.gameService.joining; + } + get pony() { + return this.model.pony; + } + get canNew() { + return !this.joining && this.model.account && this.model.account.characterCount < this.model.characterLimit; + } + get canEdit() { + return !this.joining; + } + get canRemove() { + return !this.joining && !this.locked && !this.model.pending && !!this.pony + && !!this.pony.id && this.error !== VERSION_ERROR; + } + get hasPonies() { + return !!this.model.ponies.length; + } + select(pony: PonyObject) { + if (pony) { + this.removing = false; + this.model.selectPony(pony); + this.change.emit(pony); + this.preview.emit(undefined); + } - this.dropdown.close(); - this.focusName(); - } - createNew() { - if (this.canNew) { - this.removing = false; - this.model.selectPony(createDefaultPonyObject()); - this.change.emit(this.pony); - this.router.navigate(['/character']); - this.focusName(); - } - } - edit() { - if (this.canEdit) { - this.removing = false; - this.router.navigate(['/character']); - } - } - remove() { - if (this.canRemove) { - this.removing = true; - focusElementAfterTimeout(this.element.nativeElement, '.cancel-remove-button'); - } - } - cancelRemove() { - this.removing = false; - focusElementAfterTimeout(this.element.nativeElement, '.remove-button'); - } - confirmRemove() { - if (this.canRemove) { - this.setError(undefined); - this.removing = false; - this.locked = true; + this.dropdown.close(); + this.focusName(); + } + createNew() { + if (this.canNew) { + this.removing = false; + this.model.selectPony(createDefaultPonyObject()); + this.change.emit(this.pony); + this.router.navigate(['/character']); + this.focusName(); + } + } + edit() { + if (this.canEdit) { + this.removing = false; + this.router.navigate(['/character']); + } + } + remove() { + if (this.canRemove) { + this.removing = true; + focusElementAfterTimeout(this.element.nativeElement, '.cancel-remove-button'); + } + } + cancelRemove() { + this.removing = false; + focusElementAfterTimeout(this.element.nativeElement, '.remove-button'); + } + confirmRemove() { + if (this.canRemove) { + this.setError(undefined); + this.removing = false; + this.locked = true; - this.model.removePony(this.pony) - .then(() => this.change.emit(this.pony)) - .catch((e: Error) => this.setError(e.message)) - .then(() => this.ariaAnnounce.nativeElement.textContent = 'Character removed') - .then(() => delay(2000)) - .then(() => this.locked = false) - .then(() => this.focusName()); - } - } - onToggle(show: boolean) { - if (!show) { - this.preview.emit(undefined); - } - } - private focusName() { - if (!isMobile) { - this.nameInput.nativeElement.focus(); - } - } - private setError(error: string | undefined) { - this.error = error; - this.errorChange.emit(error); - } + this.model.removePony(this.pony) + .then(() => this.change.emit(this.pony)) + .catch((e: Error) => this.setError(e.message)) + .then(() => this.ariaAnnounce.nativeElement.textContent = 'Character removed') + .then(() => delay(2000)) + .then(() => this.locked = false) + .then(() => this.focusName()); + } + } + onToggle(show: boolean) { + if (!show) { + this.preview.emit(undefined); + } + } + private focusName() { + if (!isMobile) { + this.nameInput.nativeElement.focus(); + } + } + private setError(error: string | undefined) { + this.error = error; + this.errorChange.emit(error); + } } diff --git a/src/ts/components/shared/chat-box/chat-box.ts b/src/ts/components/shared/chat-box/chat-box.ts index 909afc7..850c550 100644 --- a/src/ts/components/shared/chat-box/chat-box.ts +++ b/src/ts/components/shared/chat-box/chat-box.ts @@ -18,8 +18,8 @@ const chatTypeNames: string[] = []; const chatTypeClasses: string[] = []; function setupChatType(type: ChatType, name: string) { - chatTypeNames[type] = name; - chatTypeClasses[type] = `chat-${name.replace(/ /, '-')}`; + chatTypeNames[type] = name; + chatTypeClasses[type] = `chat-${name.replace(/ /, '-')}`; } setupChatType(ChatType.Say, 'say'); @@ -33,356 +33,356 @@ setupChatType(ChatType.Think, 'think'); setupChatType(ChatType.PartyThink, 'party think'); function isActionCommand(message: string) { - return /^\/(yawn|sneeze|achoo|laugh|lol|haha|хаха|jaja)/i.test(message); + return /^\/(yawn|sneeze|achoo|laugh|lol|haha|хаха|jaja)/i.test(message); } @Component({ - selector: 'chat-box', - templateUrl: 'chat-box.pug', - styleUrls: ['chat-box.scss'], + selector: 'chat-box', + templateUrl: 'chat-box.pug', + styleUrls: ['chat-box.scss'], }) export class ChatBox implements AfterViewInit, OnDestroy { - readonly maxSayLength = SAY_MAX_LENGTH; - readonly commentIcon = faComment; - readonly sendIcon = faAngleDoubleRight; - @ViewChild('inputElement', { static: true }) inputElement!: ElementRef; - @ViewChild('typeBox', { static: true }) typeBox!: ElementRef; - @ViewChild('typePrefix', { static: true }) typePrefix!: ElementRef; - @ViewChild('typeName', { static: true }) typeName!: ElementRef; - @ViewChild('chatBox', { static: true }) chatBox!: ElementRef; - @ViewChild('chatBoxInput', { static: true }) chatBoxInput!: ElementRef; - isOpen = false; - message: string | undefined = ''; - chatType = ChatType.Say; - private pasted = false; - private lastMessages: string[] = []; - private state: AutocompleteState = {}; - private subscriptions: Subscription[]; - private _disabled = false; - constructor(private game: PonyTownGame, zone: NgZone) { - this.subscriptions = [ - this.game.onChat.subscribe(() => zone.run(() => this.chat(undefined))), - this.game.onToggleChat.subscribe(() => zone.run(() => this.toggle())), - this.game.onCommand.subscribe(() => zone.run(() => this.command())), - this.game.onLeft.subscribe(() => { - this.chatType = ChatType.Say; - this.close(); - }), - ]; + readonly maxSayLength = SAY_MAX_LENGTH; + readonly commentIcon = faComment; + readonly sendIcon = faAngleDoubleRight; + @ViewChild('inputElement', { static: true }) inputElement!: ElementRef; + @ViewChild('typeBox', { static: true }) typeBox!: ElementRef; + @ViewChild('typePrefix', { static: true }) typePrefix!: ElementRef; + @ViewChild('typeName', { static: true }) typeName!: ElementRef; + @ViewChild('chatBox', { static: true }) chatBox!: ElementRef; + @ViewChild('chatBoxInput', { static: true }) chatBoxInput!: ElementRef; + isOpen = false; + message: string | undefined = ''; + chatType = ChatType.Say; + private pasted = false; + private lastMessages: string[] = []; + private state: AutocompleteState = {}; + private subscriptions: Subscription[]; + private _disabled = false; + constructor(private game: PonyTownGame, zone: NgZone) { + this.subscriptions = [ + this.game.onChat.subscribe(() => zone.run(() => this.chat(undefined))), + this.game.onToggleChat.subscribe(() => zone.run(() => this.toggle())), + this.game.onCommand.subscribe(() => zone.run(() => this.command())), + this.game.onLeft.subscribe(() => { + this.chatType = ChatType.Say; + this.close(); + }), + ]; - this.game.onCancel = () => this.isOpen ? (zone.run(() => this.close()), true) : false; - } - @Input() get disabled() { - return this._disabled; - } - set disabled(value) { - this._disabled = value; + this.game.onCancel = () => this.isOpen ? (zone.run(() => this.close()), true) : false; + } + @Input() get disabled() { + return this._disabled; + } + set disabled(value) { + this._disabled = value; - if (value) { - this.close(); - } - } - get input() { - return this.inputElement.nativeElement as HTMLInputElement; - } - ngAfterViewInit() { - this.chatBox.nativeElement.hidden = true; - this.input.addEventListener('paste', () => this.pasted = true); - } - ngOnDestroy() { - this.subscriptions.forEach(s => s.unsubscribe()); - } - send(_event: Event | undefined) { - let chatType = this.chatType; - let message = replaceEmojis(cleanMessage(this.message || '')).substr(0, SAY_MAX_LENGTH); - const handled = handleActionCommand(message, this.game); - const spam = this.pasted && chatType !== ChatType.Party && isSpamMessage(message, this.lastMessages); - const empty = !this.game.player || !message; - const ignoreAction = isActionCommand(message) && this.game.player && hasHeadAnimation(this.game.player); - const whisperTo = this.game.whisperTo; - let entityId = whisperTo && whisperTo.id || 0; + if (value) { + this.close(); + } + } + get input() { + return this.inputElement.nativeElement as HTMLInputElement; + } + ngAfterViewInit() { + this.chatBox.nativeElement.hidden = true; + this.input.addEventListener('paste', () => this.pasted = true); + } + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + send(_event: Event | undefined) { + let chatType = this.chatType; + let message = replaceEmojis(cleanMessage(this.message || '')).substr(0, SAY_MAX_LENGTH); + const handled = handleActionCommand(message, this.game); + const spam = this.pasted && chatType !== ChatType.Party && isSpamMessage(message, this.lastMessages); + const empty = !this.game.player || !message; + const ignoreAction = isActionCommand(message) && this.game.player && hasHeadAnimation(this.game.player); + const whisperTo = this.game.whisperTo; + let entityId = whisperTo && whisperTo.id || 0; - if (/^\/(w|whisper) .+$/i.test(message)) { - chatType = ChatType.Whisper; - message = message.substr(/^\/w /i.test(message) ? 3 : 9); + if (/^\/(w|whisper) .+$/i.test(message)) { + chatType = ChatType.Whisper; + message = message.substr(/^\/w /i.test(message) ? 3 : 9); - let offset = 0; - let entity: Entity | FakeEntity | undefined = undefined; + let offset = 0; + let entity: Entity | FakeEntity | undefined = undefined; - do { - offset = message.indexOf(' ', offset); + do { + offset = message.indexOf(' ', offset); - if (offset === -1) - break; + if (offset === -1) + break; - const name = message.substr(0, offset); - entity = findBestEntityByName(this.game, name); - offset++; - } while (!entity); + const name = message.substr(0, offset); + entity = findBestEntityByName(this.game, name); + offset++; + } while (!entity); - if (entity) { - message = message.substr(offset); - entityId = entity.id; - } else { - entityId = 0; - } - } + if (entity) { + message = message.substr(offset); + entityId = entity.id; + } else { + entityId = 0; + } + } - if (handled || spam || empty || ignoreAction || this.say(message, chatType, entityId)) { - if (message) { - this.lastMessages.push(message); + if (handled || spam || empty || ignoreAction || this.say(message, chatType, entityId)) { + if (message) { + this.lastMessages.push(message); - while (this.lastMessages.length > 5) { - this.lastMessages.shift(); - } - } + while (this.lastMessages.length > 5) { + this.lastMessages.shift(); + } + } - this.close(); - } - } - keydown(e: KeyboardEvent) { - if (e.keyCode !== Key.TAB && e.keyCode !== Key.SHIFT) { - this.state.lastEmoji = undefined; - } + this.close(); + } + } + keydown(e: KeyboardEvent) { + if (e.keyCode !== Key.TAB && e.keyCode !== Key.SHIFT) { + this.state.lastEmoji = undefined; + } - if (e.keyCode === Key.TAB) { - if (this.message) { - if (/^\/(w|whisper) .+$/i.test(this.message)) { - const space = this.message.indexOf(' '); - const names = findMatchingEntityNames(this.game, this.message.substr(space + 1)); + if (e.keyCode === Key.TAB) { + if (this.message) { + if (/^\/(w|whisper) .+$/i.test(this.message)) { + const space = this.message.indexOf(' '); + const names = findMatchingEntityNames(this.game, this.message.substr(space + 1)); - if (names.length === 1) { - this.message = `${this.message.substring(0, space)} ${names[0]}`; - } - } else { - this.message = autocompleteMesssage(this.message, e.shiftKey, this.state); - } - } + if (names.length === 1) { + this.message = `${this.message.substring(0, space)} ${names[0]}`; + } + } else { + this.message = autocompleteMesssage(this.message, e.shiftKey, this.state); + } + } - e.preventDefault(); - } else if (e.keyCode === Key.ENTER && this.isOpen) { - this.send(e); - } else if (e.keyCode === Key.ESCAPE) { - this.close(); - e.preventDefault(); - } else if (e.keyCode === Key.SPACE) { - if (!this.message) - return; + e.preventDefault(); + } else if (e.keyCode === Key.ENTER && this.isOpen) { + this.send(e); + } else if (e.keyCode === Key.ESCAPE) { + this.close(); + e.preventDefault(); + } else if (e.keyCode === Key.SPACE) { + if (!this.message) + return; - const isParty = /^\/(p|party)$/i.test(this.message); - const isSay = /^\/(s|say)$/i.test(this.message); - const isSup = /^\/(ss)$/i.test(this.message); - const isSup1 = /^\/(s1)$/i.test(this.message); - const isSup2 = /^\/(s2)$/i.test(this.message); - const isSup3 = /^\/(s3)$/i.test(this.message); + const isParty = /^\/(p|party)$/i.test(this.message); + const isSay = /^\/(s|say)$/i.test(this.message); + const isSup = /^\/(ss)$/i.test(this.message); + const isSup1 = /^\/(s1)$/i.test(this.message); + const isSup2 = /^\/(s2)$/i.test(this.message); + const isSup3 = /^\/(s3)$/i.test(this.message); - const supporter = this.game.model.supporter; - const isSayOrInvalid = isSay - || (isParty && !isInParty(this.game)) - || (isSup && supporter === 0) - || (isSup1 && supporter < 1) - || (isSup2 && supporter < 2) - || (isSup3 && supporter < 3); + const supporter = this.game.model.supporter; + const isSayOrInvalid = isSay + || (isParty && !isInParty(this.game)) + || (isSup && supporter === 0) + || (isSup1 && supporter < 1) + || (isSup2 && supporter < 2) + || (isSup3 && supporter < 3); - if (isSayOrInvalid) { - this.changeChatType(e, ChatType.Say); - } else if (isParty) { - this.changeChatType(e, ChatType.Party); - } else if (isSup) { - this.changeChatType(e, ChatType.Supporter); - } else if (isSup1) { - this.changeChatType(e, ChatType.Supporter1); - } else if (isSup2) { - this.changeChatType(e, ChatType.Supporter2); - } else if (isSup3) { - this.changeChatType(e, ChatType.Supporter3); - } else if (/^\/(t|think)$/i.test(this.message)) { - if (isPartyChat(this.chatType)) { - this.changeChatType(e, ChatType.PartyThink); - } else { - this.changeChatType(e, ChatType.Think); - } - } else if (/^\/(r|reply)$/i.test(this.message)) { - const lastWhisperFrom = this.game.lastWhisperFrom; - const entity = lastWhisperFrom && findEntityOrMockByAnyMeans(this.game, lastWhisperFrom.entityId); + if (isSayOrInvalid) { + this.changeChatType(e, ChatType.Say); + } else if (isParty) { + this.changeChatType(e, ChatType.Party); + } else if (isSup) { + this.changeChatType(e, ChatType.Supporter); + } else if (isSup1) { + this.changeChatType(e, ChatType.Supporter1); + } else if (isSup2) { + this.changeChatType(e, ChatType.Supporter2); + } else if (isSup3) { + this.changeChatType(e, ChatType.Supporter3); + } else if (/^\/(t|think)$/i.test(this.message)) { + if (isPartyChat(this.chatType)) { + this.changeChatType(e, ChatType.PartyThink); + } else { + this.changeChatType(e, ChatType.Think); + } + } else if (/^\/(r|reply)$/i.test(this.message)) { + const lastWhisperFrom = this.game.lastWhisperFrom; + const entity = lastWhisperFrom && findEntityOrMockByAnyMeans(this.game, lastWhisperFrom.entityId); - if (entity) { - this.game.whisperTo = entity; - this.changeChatType(e, ChatType.Whisper); - } else { - this.changeChatType(e, ChatType.Say); - } - } else if (/^\/(w|whisper) .+$/i.test(this.message) && !e.shiftKey) { - const name = this.message.substr(/^\/w /i.test(this.message) ? 3 : 9); - const entity = findBestEntityByName(this.game, name); + if (entity) { + this.game.whisperTo = entity; + this.changeChatType(e, ChatType.Whisper); + } else { + this.changeChatType(e, ChatType.Say); + } + } else if (/^\/(w|whisper) .+$/i.test(this.message) && !e.shiftKey) { + const name = this.message.substr(/^\/w /i.test(this.message) ? 3 : 9); + const entity = findBestEntityByName(this.game, name); - if (entity) { - this.game.whisperTo = entity; - this.changeChatType(e, ChatType.Whisper); - } - } - } - } - private say(message: string, chatType: ChatType, entityId: number): boolean { - this.game.lastChatMessageType = chatType; - return !!this.game.send(server => server.say(entityId, message, chatType)); - } - private changeChatType(e: KeyboardEvent, chatType: ChatType) { - this.chatType = chatType; - this.message = ''; - this.updateChatType(); - e.preventDefault(); - } - private chat(event: Event | undefined) { - if (this.isOpen) { - this.send(event); - } else { - this.open(); - } - } - private command() { - if (!this.isOpen) { - this.chat(undefined); - this.message = '/'; - this.input.selectionStart = this.input.selectionEnd = 10000; - } - } - private open() { - if (!this.isOpen) { - this.isOpen = true; - this.chatBox.nativeElement.hidden = false; - } + if (entity) { + this.game.whisperTo = entity; + this.changeChatType(e, ChatType.Whisper); + } + } + } + } + private say(message: string, chatType: ChatType, entityId: number): boolean { + this.game.lastChatMessageType = chatType; + return !!this.game.send(server => server.say(entityId, message, chatType)); + } + private changeChatType(e: KeyboardEvent, chatType: ChatType) { + this.chatType = chatType; + this.message = ''; + this.updateChatType(); + e.preventDefault(); + } + private chat(event: Event | undefined) { + if (this.isOpen) { + this.send(event); + } else { + this.open(); + } + } + private command() { + if (!this.isOpen) { + this.chat(undefined); + this.message = '/'; + this.input.selectionStart = this.input.selectionEnd = 10000; + } + } + private open() { + if (!this.isOpen) { + this.isOpen = true; + this.chatBox.nativeElement.hidden = false; + } - this.chatType = isValidChatType(this.chatType, this.game) ? this.chatType : ChatType.Say; - this.updateChatType(); - this.input.focus(); - } - private close() { - if (this.isOpen) { - this.input.blur(); - this.isOpen = false; - this.chatBox.nativeElement.hidden = true; - this.message = ''; - this.pasted = false; - } - } - toggle() { - if (this.isOpen) { - this.close(); - } else { - this.open(); - } - } - toggleChatType() { - const chatTypes = getChatTypes(this.game); - this.chatType = chatTypes[(chatTypes.indexOf(this.chatType) + 1) % chatTypes.length]; - this.updateChatType(); - this.input.focus(); - } - setChatType(type: 'say' | 'party' | 'whisper') { - if (type === 'say') { - this.chatType = ChatType.Say; - this.open(); - } else if (type === 'party' && isInParty(this.game)) { - this.chatType = ChatType.Party; - this.open(); - } else if (type === 'whisper') { - this.chatType = ChatType.Whisper; - this.open(); - } - } - private currentTypeClass = ''; - private currentTypePrefix = ''; - private currentTypeName = ''; - private updateChatType() { - let typeName: string; - let typePrefix: string; - let changed = false; + this.chatType = isValidChatType(this.chatType, this.game) ? this.chatType : ChatType.Say; + this.updateChatType(); + this.input.focus(); + } + private close() { + if (this.isOpen) { + this.input.blur(); + this.isOpen = false; + this.chatBox.nativeElement.hidden = true; + this.message = ''; + this.pasted = false; + } + } + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + toggleChatType() { + const chatTypes = getChatTypes(this.game); + this.chatType = chatTypes[(chatTypes.indexOf(this.chatType) + 1) % chatTypes.length]; + this.updateChatType(); + this.input.focus(); + } + setChatType(type: 'say' | 'party' | 'whisper') { + if (type === 'say') { + this.chatType = ChatType.Say; + this.open(); + } else if (type === 'party' && isInParty(this.game)) { + this.chatType = ChatType.Party; + this.open(); + } else if (type === 'whisper') { + this.chatType = ChatType.Whisper; + this.open(); + } + } + private currentTypeClass = ''; + private currentTypePrefix = ''; + private currentTypeName = ''; + private updateChatType() { + let typeName: string; + let typePrefix: string; + let changed = false; - const typeClass = chatTypeClass(this.chatType, this.game.model.supporter); + const typeClass = chatTypeClass(this.chatType, this.game.model.supporter); - if (this.currentTypeClass !== typeClass) { - this.currentTypeClass = typeClass; - (this.chatBoxInput.nativeElement as HTMLElement).className = typeClass; - } + if (this.currentTypeClass !== typeClass) { + this.currentTypeClass = typeClass; + (this.chatBoxInput.nativeElement as HTMLElement).className = typeClass; + } - if (this.chatType === ChatType.Whisper) { - typePrefix = 'To '; - typeName = this.game.whisperTo && this.game.whisperTo.name || 'unknown'; - } else { - typePrefix = ''; - typeName = chatTypeNames[this.chatType]; - } + if (this.chatType === ChatType.Whisper) { + typePrefix = 'To '; + typeName = this.game.whisperTo && this.game.whisperTo.name || 'unknown'; + } else { + typePrefix = ''; + typeName = chatTypeNames[this.chatType]; + } - if (this.currentTypePrefix !== typePrefix) { - changed = true; - this.currentTypePrefix = typePrefix; - (this.typePrefix.nativeElement as HTMLElement).textContent = typePrefix; - } + if (this.currentTypePrefix !== typePrefix) { + changed = true; + this.currentTypePrefix = typePrefix; + (this.typePrefix.nativeElement as HTMLElement).textContent = typePrefix; + } - if (this.currentTypeName !== typeName) { - changed = true; - this.currentTypeName = typeName; - replaceNodes(this.typeName.nativeElement, typeName); - } + if (this.currentTypeName !== typeName) { + changed = true; + this.currentTypeName = typeName; + replaceNodes(this.typeName.nativeElement, typeName); + } - if (changed) { - const { width } = (this.typeBox.nativeElement as HTMLElement).getBoundingClientRect(); - const padding = 35 + 13 + Math.ceil(width); - (this.inputElement.nativeElement as HTMLElement).style.paddingLeft = `${padding}px`; - } - } + if (changed) { + const { width } = (this.typeBox.nativeElement as HTMLElement).getBoundingClientRect(); + const padding = 35 + 13 + Math.ceil(width); + (this.inputElement.nativeElement as HTMLElement).style.paddingLeft = `${padding}px`; + } + } } function chatTypeClass(chatType: ChatType, supporter: number) { - if (chatType === ChatType.Supporter) { - switch (supporter) { - case 1: return 'chat-sup chat-sup1'; - case 2: return 'chat-sup chat-sup2'; - case 3: return 'chat-sup chat-sup3'; - } - } + if (chatType === ChatType.Supporter) { + switch (supporter) { + case 1: return 'chat-sup chat-sup1'; + case 2: return 'chat-sup chat-sup2'; + case 3: return 'chat-sup chat-sup3'; + } + } - return chatTypeClasses[chatType]; + return chatTypeClasses[chatType]; } function isValidChatType(type: ChatType, game: PonyTownGame) { - const supporter = game.model.supporter; + const supporter = game.model.supporter; - switch (type) { - case ChatType.Say: - case ChatType.Think: - case ChatType.Whisper: - return true; - case ChatType.Party: - case ChatType.PartyThink: - return isInParty(game); - case ChatType.Supporter: - return supporter > 0; - case ChatType.Supporter1: - return supporter >= 1; - case ChatType.Supporter2: - return supporter >= 2; - case ChatType.Supporter3: - return supporter >= 3; - case ChatType.Dismiss: - return false; - default: - return invalidEnumReturn(type, false); - } + switch (type) { + case ChatType.Say: + case ChatType.Think: + case ChatType.Whisper: + return true; + case ChatType.Party: + case ChatType.PartyThink: + return isInParty(game); + case ChatType.Supporter: + return supporter > 0; + case ChatType.Supporter1: + return supporter >= 1; + case ChatType.Supporter2: + return supporter >= 2; + case ChatType.Supporter3: + return supporter >= 3; + case ChatType.Dismiss: + return false; + default: + return invalidEnumReturn(type, false); + } } function getChatTypes(game: PonyTownGame) { - const chatTypes = [ChatType.Say]; - const supporter = game.model.supporter; + const chatTypes = [ChatType.Say]; + const supporter = game.model.supporter; - if (isInParty(game)) { - chatTypes.push(ChatType.Party); - } + if (isInParty(game)) { + chatTypes.push(ChatType.Party); + } - if (supporter) { - chatTypes.push(ChatType.Supporter); - } + if (supporter) { + chatTypes.push(ChatType.Supporter); + } - return chatTypes; + return chatTypes; } diff --git a/src/ts/components/shared/chat-log/chat-log.ts b/src/ts/components/shared/chat-log/chat-log.ts index 1ac6223..bd95afe 100644 --- a/src/ts/components/shared/chat-log/chat-log.ts +++ b/src/ts/components/shared/chat-log/chat-log.ts @@ -1,5 +1,5 @@ import { - Component, ViewChild, ElementRef, NgZone, AfterViewInit, OnDestroy, HostListener, Output, EventEmitter, DoCheck + Component, ViewChild, ElementRef, NgZone, AfterViewInit, OnDestroy, HostListener, Output, EventEmitter, DoCheck } from '@angular/core'; import { Subscription } from 'rxjs'; import { clamp, escapeRegExp } from 'lodash'; @@ -13,40 +13,40 @@ import { faCaretUp, faArrowDown } from '../../../client/icons'; import { sampleMessages } from '../../../common/debugData'; interface IndexEntryUser { - id: number; - crc: number | undefined; + id: number; + crc: number | undefined; } interface IndexEntry { - users: IndexEntryUser[]; - counter: number; + users: IndexEntryUser[]; + counter: number; } interface ChatLogLineDOM { - entry: ChatLogMessage; - root: HTMLElement; - label: HTMLElement; - labelText: Text; - name: HTMLElement; - nameContent: HTMLElement; - index: HTMLElement; - indexText: Text; - prefixText: Text; - suffixText: Text; - message: HTMLElement; + entry: ChatLogMessage; + root: HTMLElement; + label: HTMLElement; + labelText: Text; + name: HTMLElement; + nameContent: HTMLElement; + index: HTMLElement; + indexText: Text; + prefixText: Text; + suffixText: Text; + message: HTMLElement; } export interface ChatLogMessage { - message: string; - name?: string; - crc?: number; - prefix?: string; - suffix?: string; - label?: string; - index: number; - classes?: string; - entityId?: number; - dom?: ChatLogLineDOM; + message: string; + name?: string; + crc?: number; + prefix?: string; + suffix?: string; + label?: string; + index: number; + classes?: string; + entityId?: number; + dom?: ChatLogLineDOM; } type Tab = 'local' | 'party' | 'whisper'; @@ -94,527 +94,527 @@ CLASSES[MessageType.WhisperAnnouncement] = 'chat-line-whisper-announcement'; CLASSES[MessageType.WhisperToAnnouncement] = 'chat-line-whisper-announcement'; export function createChatLogLineDOM(clickLabel: ClickHandler, clickName: ClickHandler): ChatLogLineDOM { - const line: ChatLogLineDOM = {} as any; + const line: ChatLogLineDOM = {} as any; - line.root = element('div', 'chat-line', [ - element('span', 'chat-line-lead'), - line.label = element( - 'span', 'chat-line-label mr-1', [line.labelText = textNode('')], undefined, { click: () => clickLabel(line.entry) }), - line.prefixText = textNode(''), - line.name = element('span', 'chat-line-name', [ - textNode('['), - line.nameContent = element( - 'span', 'chat-line-name-content', [textNode('')], undefined, { click: () => clickName(line.entry) }), - line.index = element('span', 'chat-line-name-index', [line.indexText = textNode('')], { title: 'duplicate name' }), - textNode(']'), - ]), - line.suffixText = textNode(''), - line.message = element('span', 'chat-line-message', [textNode('')]), - ]); + line.root = element('div', 'chat-line', [ + element('span', 'chat-line-lead'), + line.label = element( + 'span', 'chat-line-label mr-1', [line.labelText = textNode('')], undefined, { click: () => clickLabel(line.entry) }), + line.prefixText = textNode(''), + line.name = element('span', 'chat-line-name', [ + textNode('['), + line.nameContent = element( + 'span', 'chat-line-name-content', [textNode('')], undefined, { click: () => clickName(line.entry) }), + line.index = element('span', 'chat-line-name-index', [line.indexText = textNode('')], { title: 'duplicate name' }), + textNode(']'), + ]), + line.suffixText = textNode(''), + line.message = element('span', 'chat-line-message', [textNode('')]), + ]); - return line; + return line; } export function updateChatLogLine(line: ChatLogLineDOM, entry: ChatLogMessage) { - const { classes, label, message, prefix, suffix } = entry; - const hasSpace = message.indexOf(' ') !== -1; + const { classes, label, message, prefix, suffix } = entry; + const hasSpace = message.indexOf(' ') !== -1; - line.entry = entry; - line.root.className = `chat-line ${hasSpace ? '' : 'chat-line-break '}${classes}`.trim(); - line.label.style.display = label ? 'inline' : 'none'; - line.labelText.nodeValue = label ? `[${label}]` : ''; + line.entry = entry; + line.root.className = `chat-line ${hasSpace ? '' : 'chat-line-break '}${classes}`.trim(); + line.label.style.display = label ? 'inline' : 'none'; + line.labelText.nodeValue = label ? `[${label}]` : ''; - updateChatLogName(line, entry); + updateChatLogName(line, entry); - line.prefixText.nodeValue = prefix || ''; - line.suffixText.nodeValue = suffix ? ` ${suffix}: ` : ': '; - replaceNodes(line.message, message); + line.prefixText.nodeValue = prefix || ''; + line.suffixText.nodeValue = suffix ? ` ${suffix}: ` : ': '; + replaceNodes(line.message, message); } function updateChatLogName(line: ChatLogLineDOM, { name, index }: ChatLogMessage) { - if (name) { - line.name.style.display = 'inline'; - replaceNodes(line.nameContent, name); - line.index.style.display = (index > 0) ? 'inline' : 'none'; - line.indexText.nodeValue = (index > 0) ? ` #${index + 1}` : ''; - } else { - line.name.style.display = 'none'; - } + if (name) { + line.name.style.display = 'inline'; + replaceNodes(line.nameContent, name); + line.index.style.display = (index > 0) ? 'inline' : 'none'; + line.indexText.nodeValue = (index > 0) ? ` #${index + 1}` : ''; + } else { + line.name.style.display = 'none'; + } } function isMatch(e: ChatLogMessage, id: number, name: string, crc: number | undefined) { - return e.name === name && (e.entityId === id || (e.crc === crc && crc !== undefined)); + return e.name === name && (e.entityId === id || (e.crc === crc && crc !== undefined)); } function addOrUpdatePony(pony: Pony, list: ChatLogMessage[]) { - for (const e of list) { - if (isMatch(e, pony.id, pony.name!, pony.crc)) { - e.entityId = pony.id; - } - } + for (const e of list) { + if (isMatch(e, pony.id, pony.name!, pony.crc)) { + e.entityId = pony.id; + } + } } function updateEntityId(list: ChatLogMessage[], oldId: number, newId: number) { - for (const e of list) { - if (e.entityId === oldId) { - e.entityId = newId; - } - } + for (const e of list) { + if (e.entityId === oldId) { + e.entityId = newId; + } + } } function findUserIndex(users: IndexEntryUser[], id: number, crc: number | undefined) { - for (let i = 0; i < users.length; i++) { - const user = users[i]; + for (let i = 0; i < users.length; i++) { + const user = users[i]; - if (user.id === id || (crc !== undefined && user.crc === crc)) { - user.id = id; - return i; - } - } + if (user.id === id || (crc !== undefined && user.crc === crc)) { + user.id = id; + return i; + } + } - return -1; + return -1; } @Component({ - selector: 'chat-log', - templateUrl: 'chat-log.pug', - styleUrls: ['chat-log.scss'], + selector: 'chat-log', + templateUrl: 'chat-log.pug', + styleUrls: ['chat-log.scss'], }) export class ChatLog implements AfterViewInit, OnDestroy, DoCheck { - readonly toBottomIcon = faArrowDown; - readonly resizeIcon = faCaretUp; - @ViewChild('chatLog', { static: true }) chatLog!: ElementRef; - @ViewChild('scroll', { static: true }) scroll!: ElementRef; - @ViewChild('lines', { static: true }) lines!: ElementRef; - @ViewChild('localTab', { static: true }) localTab!: ElementRef; - @ViewChild('partyTab', { static: true }) partyTab!: ElementRef; - @ViewChild('whisperTab', { static: true }) whisperTab!: ElementRef; - @ViewChild('toggleButton', { static: true }) toggleButton!: ElementRef; - @ViewChild('count', { static: true }) countElement!: ElementRef; - @ViewChild('content', { static: true }) contentElement!: ElementRef; - @Output() toggleType = new EventEmitter(); - @Output() nameClick = new EventEmitter(); - innerWidth = 0; - // TODO: move to game ? - local: ChatLogMessage[] = []; - party: ChatLogMessage[] = []; - whisper: ChatLogMessage[] = []; - unread = 0; - private subscriptions: Subscription[] = []; - private startX = 0; - private startY = 0; - private shouldScrollToEnd = false; - private scrolledToEnd = true; - private scrollingToEnd = false; - private scrollToEndAtFrame = false; - private indexes = new Map(); - private messageCounter = 0; - private lastOpacity = 0; - constructor( - private game: PonyTownGame, - private settingsService: SettingsService, - private element: ElementRef, - private zone: NgZone, - ) { - // TODO: just put reference to chatlog on game ??? - this.subscriptions.push( - game.onFrame.subscribe(() => { - if (this.scrollToEndAtFrame) { - this.scrollToEndAtFrame = false; - this.scrollHandler(); - } - }), - game.onMessage.subscribe(message => { - this.addMessage(message); - }), - // TODO: move to game ? - game.onPonyAddOrUpdate.subscribe(pony => { - addOrUpdatePony(pony, this.local); - addOrUpdatePony(pony, this.party); - addOrUpdatePony(pony, this.whisper); - }), - game.onJoined.subscribe(() => { - if (!DEVELOPMENT) { - // TODO: move to game ? - this.local = []; - this.party = []; - this.whisper = []; - this.messageCounter = 0; - this.indexes = new Map(); - this.clearList(); - } - }), - game.onEntityIdUpdate.subscribe(update => { - updateEntityId(this.local, update.old, update.new); - updateEntityId(this.party, update.old, update.new); - updateEntityId(this.whisper, update.old, update.new); - }), - ); - } - get linesElement() { - return this.lines.nativeElement as HTMLElement; - } - private updateOpen() { - this.updateChatlog(); + readonly toBottomIcon = faArrowDown; + readonly resizeIcon = faCaretUp; + @ViewChild('chatLog', { static: true }) chatLog!: ElementRef; + @ViewChild('scroll', { static: true }) scroll!: ElementRef; + @ViewChild('lines', { static: true }) lines!: ElementRef; + @ViewChild('localTab', { static: true }) localTab!: ElementRef; + @ViewChild('partyTab', { static: true }) partyTab!: ElementRef; + @ViewChild('whisperTab', { static: true }) whisperTab!: ElementRef; + @ViewChild('toggleButton', { static: true }) toggleButton!: ElementRef; + @ViewChild('count', { static: true }) countElement!: ElementRef; + @ViewChild('content', { static: true }) contentElement!: ElementRef; + @Output() toggleType = new EventEmitter(); + @Output() nameClick = new EventEmitter(); + innerWidth = 0; + // TODO: move to game ? + local: ChatLogMessage[] = []; + party: ChatLogMessage[] = []; + whisper: ChatLogMessage[] = []; + unread = 0; + private subscriptions: Subscription[] = []; + private startX = 0; + private startY = 0; + private shouldScrollToEnd = false; + private scrolledToEnd = true; + private scrollingToEnd = false; + private scrollToEndAtFrame = false; + private indexes = new Map(); + private messageCounter = 0; + private lastOpacity = 0; + constructor( + private game: PonyTownGame, + private settingsService: SettingsService, + private element: ElementRef, + private zone: NgZone, + ) { + // TODO: just put reference to chatlog on game ??? + this.subscriptions.push( + game.onFrame.subscribe(() => { + if (this.scrollToEndAtFrame) { + this.scrollToEndAtFrame = false; + this.scrollHandler(); + } + }), + game.onMessage.subscribe(message => { + this.addMessage(message); + }), + // TODO: move to game ? + game.onPonyAddOrUpdate.subscribe(pony => { + addOrUpdatePony(pony, this.local); + addOrUpdatePony(pony, this.party); + addOrUpdatePony(pony, this.whisper); + }), + game.onJoined.subscribe(() => { + if (!DEVELOPMENT) { + // TODO: move to game ? + this.local = []; + this.party = []; + this.whisper = []; + this.messageCounter = 0; + this.indexes = new Map(); + this.clearList(); + } + }), + game.onEntityIdUpdate.subscribe(update => { + updateEntityId(this.local, update.old, update.new); + updateEntityId(this.party, update.old, update.new); + updateEntityId(this.whisper, update.old, update.new); + }), + ); + } + get linesElement() { + return this.lines.nativeElement as HTMLElement; + } + private updateOpen() { + this.updateChatlog(); - if (this.open) { - this.setUnread(0); - this.regenerateList(); - this.scrollToEnd(); - } else { - this.clearList(); - } + if (this.open) { + this.setUnread(0); + this.regenerateList(); + this.scrollToEnd(); + } else { + this.clearList(); + } - this.updateInnerWidth(); - } - private updateChatlog() { - const element = this.chatLog.nativeElement as HTMLElement; - element.style.display = this.open ? 'flex' : 'none'; + this.updateInnerWidth(); + } + private updateChatlog() { + const element = this.chatLog.nativeElement as HTMLElement; + element.style.display = this.open ? 'flex' : 'none'; - if (this.open) { - element.style.width = `${this.width}px`; - element.style.height = `${this.height}px`; - } - } - ngAfterViewInit() { - this.game.findEntityFromChatLog = this.findEntityFromMessages; - this.game.findEntityFromChatLogByName = this.findEntityFromMessagesByName; + if (this.open) { + element.style.width = `${this.width}px`; + element.style.height = `${this.height}px`; + } + } + ngAfterViewInit() { + this.game.findEntityFromChatLog = this.findEntityFromMessages; + this.game.findEntityFromChatLogByName = this.findEntityFromMessagesByName; - this.updateTabs(); - this.updateOpen(); + this.updateTabs(); + this.updateOpen(); - this.zone.runOutsideAngular(() => { - const scroll = this.scroll.nativeElement as HTMLElement; + this.zone.runOutsideAngular(() => { + const scroll = this.scroll.nativeElement as HTMLElement; - scroll.addEventListener('scroll', () => { - if (this.scrollingToEnd) { - this.scrolledToEnd = true; - this.scrollingToEnd = false; - } else { - const clientHeight = scroll.getBoundingClientRect().height; - this.scrolledToEnd = scroll.scrollTop >= (scroll.scrollHeight - clientHeight - SCROLL_END_THRESHOLD); - } - }); - }); + scroll.addEventListener('scroll', () => { + if (this.scrollingToEnd) { + this.scrolledToEnd = true; + this.scrollingToEnd = false; + } else { + const clientHeight = scroll.getBoundingClientRect().height; + this.scrolledToEnd = scroll.scrollTop >= (scroll.scrollHeight - clientHeight - SCROLL_END_THRESHOLD); + } + }); + }); - setTimeout(() => { - this.scrollToEnd(); - this.updateInnerWidth(); - }); + setTimeout(() => { + this.scrollToEnd(); + this.updateInnerWidth(); + }); - if (DEVELOPMENT) { - sampleMessages.forEach(({ name, id, message, type }) => - this.addMessage({ id: id || 999999, crc: undefined, name, message, type: type || MessageType.Chat })); - } - } - ngOnDestroy() { - if (this.game.findEntityFromChatLog === this.findEntityFromMessages) { - this.game.findEntityFromChatLog = () => undefined; - } + if (DEVELOPMENT) { + sampleMessages.forEach(({ name, id, message, type }) => + this.addMessage({ id: id || 999999, crc: undefined, name, message, type: type || MessageType.Chat })); + } + } + ngOnDestroy() { + if (this.game.findEntityFromChatLog === this.findEntityFromMessages) { + this.game.findEntityFromChatLog = () => undefined; + } - if (this.game.findEntityFromChatLogByName === this.findEntityFromMessagesByName) { - this.game.findEntityFromChatLogByName = () => undefined; - } + if (this.game.findEntityFromChatLogByName === this.findEntityFromMessagesByName) { + this.game.findEntityFromChatLogByName = () => undefined; + } - this.subscriptions.forEach(s => s.unsubscribe()); - this.subscriptions = []; - } - ngDoCheck() { - if (this.lastOpacity !== this.opacity) { - this.lastOpacity = this.opacity; - this.contentElement.nativeElement.style.backgroundColor = this.bg; - this.updateTabs(); - } - } - @HostListener('window:resize') - updateInnerWidth() { - const maxWidth = (this.element.nativeElement as HTMLElement).getBoundingClientRect().width; - const innerWidth = Math.min(maxWidth || this.width, this.width) - 40; + this.subscriptions.forEach(s => s.unsubscribe()); + this.subscriptions = []; + } + ngDoCheck() { + if (this.lastOpacity !== this.opacity) { + this.lastOpacity = this.opacity; + this.contentElement.nativeElement.style.backgroundColor = this.bg; + this.updateTabs(); + } + } + @HostListener('window:resize') + updateInnerWidth() { + const maxWidth = (this.element.nativeElement as HTMLElement).getBoundingClientRect().width; + const innerWidth = Math.min(maxWidth || this.width, this.width) - 40; - if (this.innerWidth !== innerWidth) { - this.innerWidth = innerWidth; - this.linesElement.style.width = `${innerWidth}px`; - } + if (this.innerWidth !== innerWidth) { + this.innerWidth = innerWidth; + this.linesElement.style.width = `${innerWidth}px`; + } - if (!maxWidth) { - setTimeout(() => this.updateInnerWidth(), 10); - } - } - get messages() { - return this[this.activeTab]; - } - get settings() { - return this.settingsService.browser; - } - get settings2() { - return this.settingsService.account; - } - get activeTab(): Tab { - const tab = this.settings.chatlogTab; - return (tab === 'local' || tab === 'party' || tab === 'whisper') ? tab : 'local'; - } - get open() { - return !this.settings.chatlogClosed; - } - get width() { - return this.settings.chatlogWidth || 500; - } - get height() { - return this.settings.chatlogHeight || 310; - } - get opacity() { - return this.settings2.chatlogOpacity === undefined ? DEFAULT_CHATLOG_OPACITY : this.settings2.chatlogOpacity; - } - get bg() { - return `rgba(0, 0, 0, ${this.opacity / 100})`; - } - get inactiveBg() { - return `rgba(0, 0, 0, ${(this.opacity / 200) * 0.5})`; - } - private createEntry({ id, crc, name, message, type }: ChatMessage): ChatLogMessage { - const system = type === MessageType.System; - const entry: ChatLogMessage = { - entityId: system ? 0 : id, - name: system ? '' : name, - index: 0, - crc, - message, - label: LABELS[type] || '', - prefix: PREFIXES[type] || '', - suffix: SUFFIXES[type] || '', - classes: CLASSES[type] || '', - }; + if (!maxWidth) { + setTimeout(() => this.updateInnerWidth(), 10); + } + } + get messages() { + return this[this.activeTab]; + } + get settings() { + return this.settingsService.browser; + } + get settings2() { + return this.settingsService.account; + } + get activeTab(): Tab { + const tab = this.settings.chatlogTab; + return (tab === 'local' || tab === 'party' || tab === 'whisper') ? tab : 'local'; + } + get open() { + return !this.settings.chatlogClosed; + } + get width() { + return this.settings.chatlogWidth || 500; + } + get height() { + return this.settings.chatlogHeight || 310; + } + get opacity() { + return this.settings2.chatlogOpacity === undefined ? DEFAULT_CHATLOG_OPACITY : this.settings2.chatlogOpacity; + } + get bg() { + return `rgba(0, 0, 0, ${this.opacity / 100})`; + } + get inactiveBg() { + return `rgba(0, 0, 0, ${(this.opacity / 200) * 0.5})`; + } + private createEntry({ id, crc, name, message, type }: ChatMessage): ChatLogMessage { + const system = type === MessageType.System; + const entry: ChatLogMessage = { + entityId: system ? 0 : id, + name: system ? '' : name, + index: 0, + crc, + message, + label: LABELS[type] || '', + prefix: PREFIXES[type] || '', + suffix: SUFFIXES[type] || '', + classes: CLASSES[type] || '', + }; - if (!system) { - entry.index = this.findOrCreateIndex(name, id, crc); - } + if (!system) { + entry.index = this.findOrCreateIndex(name, id, crc); + } - return entry; - } - private findOrCreateIndex(name: string, id: number, crc: number | undefined) { - let found = this.indexes.get(name); + return entry; + } + private findOrCreateIndex(name: string, id: number, crc: number | undefined) { + let found = this.indexes.get(name); - if (!found || (this.messageCounter - found.counter) > FORGET_INDEX_AFTER) { - found = { - users: [{ id, crc }], - counter: 0, - }; + if (!found || (this.messageCounter - found.counter) > FORGET_INDEX_AFTER) { + found = { + users: [{ id, crc }], + counter: 0, + }; - this.indexes.set(name, found); - } + this.indexes.set(name, found); + } - found.counter = this.messageCounter; + found.counter = this.messageCounter; - let index = findUserIndex(found.users, id, crc); + let index = findUserIndex(found.users, id, crc); - if (index === -1) { - index = found.users.length; - found.users.push({ id, crc }); - } + if (index === -1) { + index = found.users.length; + found.users.push({ id, crc }); + } - return index; - } - addMessage(message: ChatMessage) { - if (message.name && message.message) { - const entry = this.createEntry(message); - const party = isPartyMessage(message.type); - const whisper = isWhisper(message.type) || isWhisperTo(message.type); - const open = this.open; - const scrolledToEnd = open ? this.scrolledToEnd : false; - const tab = this.activeTab; + return index; + } + addMessage(message: ChatMessage) { + if (message.name && message.message) { + const entry = this.createEntry(message); + const party = isPartyMessage(message.type); + const whisper = isWhisper(message.type) || isWhisperTo(message.type); + const open = this.open; + const scrolledToEnd = open ? this.scrolledToEnd : false; + const tab = this.activeTab; - this.addEntryToList(this.local, GENERAL_CHAT_LIMIT, open && tab === 'local', entry); + this.addEntryToList(this.local, GENERAL_CHAT_LIMIT, open && tab === 'local', entry); - if (party || whisper) { - const partyEntry = { ...entry }; - partyEntry.dom = undefined; - partyEntry.label = whisper ? partyEntry.label : undefined; - this.addEntryToList(this.party, PARTY_CHAT_LIMIT, open && tab === 'party', partyEntry); - } + if (party || whisper) { + const partyEntry = { ...entry }; + partyEntry.dom = undefined; + partyEntry.label = whisper ? partyEntry.label : undefined; + this.addEntryToList(this.party, PARTY_CHAT_LIMIT, open && tab === 'party', partyEntry); + } - if (whisper) { - const whisperEntry = { ...entry }; - whisperEntry.dom = undefined; - whisperEntry.label = undefined; - this.addEntryToList(this.whisper, WHISPER_CHAT_LIMIT, open && tab === 'whisper', whisperEntry); - } + if (whisper) { + const whisperEntry = { ...entry }; + whisperEntry.dom = undefined; + whisperEntry.label = undefined; + this.addEntryToList(this.whisper, WHISPER_CHAT_LIMIT, open && tab === 'whisper', whisperEntry); + } - if (message.type === MessageType.Whisper && !this.open) { - this.setUnread(this.unread + 1); - } + if (message.type === MessageType.Whisper && !this.open) { + this.setUnread(this.unread + 1); + } - if (scrolledToEnd) { - this.scrollToEnd(); - } + if (scrolledToEnd) { + this.scrollToEnd(); + } - this.messageCounter++; - } - } - private addEntryToList(list: ChatLogMessage[], limit: number, isOpen: boolean, entry: ChatLogMessage) { - let removedDom: ChatLogLineDOM | undefined; + this.messageCounter++; + } + } + private addEntryToList(list: ChatLogMessage[], limit: number, isOpen: boolean, entry: ChatLogMessage) { + let removedDom: ChatLogLineDOM | undefined; - while (list.length >= limit) { - const removed = list.shift(); + while (list.length >= limit) { + const removed = list.shift(); - if (isOpen && removed && removed.dom) { - if (removed.dom.root.parentElement) { - removed.dom.root.parentElement.removeChild(removed.dom.root); - } + if (isOpen && removed && removed.dom) { + if (removed.dom.root.parentElement) { + removed.dom.root.parentElement.removeChild(removed.dom.root); + } - removedDom = removed.dom; - removed.dom = undefined; - } - } + removedDom = removed.dom; + removed.dom = undefined; + } + } - list.push(entry); + list.push(entry); - if (isOpen) { - entry.dom = removedDom || createChatLogLineDOM(this.clickLabel, this.clickNameHandler); - updateChatLogLine(entry.dom, entry); - this.linesElement.appendChild(entry.dom.root); - } - } - toggle() { - this.settings.chatlogClosed = !this.settings.chatlogClosed; - this.settingsService.saveBrowserSettings(); - this.updateOpen(); - } - switchTab(tab: Tab) { - if (this.activeTab !== tab) { - this.settings.chatlogTab = tab; - this.settingsService.saveBrowserSettings(); - this.regenerateList(); - this.scrollToEnd(); - this.updateTabs(); - } - } - private updateTabs() { - this.setActiveTab(this.localTab.nativeElement, this.activeTab === 'local'); - this.setActiveTab(this.partyTab.nativeElement, this.activeTab === 'party'); - this.setActiveTab(this.whisperTab.nativeElement, this.activeTab === 'whisper'); - } - private setActiveTab(tab: HTMLElement, active: boolean) { - if (active) { - tab.classList.add('active'); - tab.style.backgroundColor = this.bg; - } else { - tab.classList.remove('active'); - tab.style.backgroundColor = this.inactiveBg; - } - } - scrollToEnd() { - // requestAnimationFrame(this.scrollHandler); - this.scrollToEndAtFrame = true; - } - scrollHandler = () => { - this.scrollingToEnd = true; - this.scroll.nativeElement.scrollTop = 99999; - } - clickNameHandler = (message: ChatLogMessage) => { - this.zone.run(() => this.nameClick.emit(message)); - } - clickLabel = (message: ChatLogMessage) => { - this.zone.run(() => { - if (message.label) { - this.toggleType.emit(message.label); - } - }); - } - private clearList() { - removeAllNodes(this.linesElement); - } - private regenerateList() { - this.clearList(); + if (isOpen) { + entry.dom = removedDom || createChatLogLineDOM(this.clickLabel, this.clickNameHandler); + updateChatLogLine(entry.dom, entry); + this.linesElement.appendChild(entry.dom.root); + } + } + toggle() { + this.settings.chatlogClosed = !this.settings.chatlogClosed; + this.settingsService.saveBrowserSettings(); + this.updateOpen(); + } + switchTab(tab: Tab) { + if (this.activeTab !== tab) { + this.settings.chatlogTab = tab; + this.settingsService.saveBrowserSettings(); + this.regenerateList(); + this.scrollToEnd(); + this.updateTabs(); + } + } + private updateTabs() { + this.setActiveTab(this.localTab.nativeElement, this.activeTab === 'local'); + this.setActiveTab(this.partyTab.nativeElement, this.activeTab === 'party'); + this.setActiveTab(this.whisperTab.nativeElement, this.activeTab === 'whisper'); + } + private setActiveTab(tab: HTMLElement, active: boolean) { + if (active) { + tab.classList.add('active'); + tab.style.backgroundColor = this.bg; + } else { + tab.classList.remove('active'); + tab.style.backgroundColor = this.inactiveBg; + } + } + scrollToEnd() { + // requestAnimationFrame(this.scrollHandler); + this.scrollToEndAtFrame = true; + } + scrollHandler = () => { + this.scrollingToEnd = true; + this.scroll.nativeElement.scrollTop = 99999; + } + clickNameHandler = (message: ChatLogMessage) => { + this.zone.run(() => this.nameClick.emit(message)); + } + clickLabel = (message: ChatLogMessage) => { + this.zone.run(() => { + if (message.label) { + this.toggleType.emit(message.label); + } + }); + } + private clearList() { + removeAllNodes(this.linesElement); + } + private regenerateList() { + this.clearList(); - const lines = this.linesElement; + const lines = this.linesElement; - this.messages.forEach(entry => { - if (!entry.dom) { - entry.dom = createChatLogLineDOM(this.clickLabel, this.clickNameHandler); - updateChatLogLine(entry.dom, entry); - } + this.messages.forEach(entry => { + if (!entry.dom) { + entry.dom = createChatLogLineDOM(this.clickLabel, this.clickNameHandler); + updateChatLogLine(entry.dom, entry); + } - lines.appendChild(entry.dom.root); - }); - } - drag({ x, y, type, event }: AgDragEvent, resizeY: boolean, resizeX: boolean) { - event.preventDefault(); + lines.appendChild(entry.dom.root); + }); + } + drag({ x, y, type, event }: AgDragEvent, resizeY: boolean, resizeX: boolean) { + event.preventDefault(); - if (type === 'start') { - const { left, top } = (this.element.nativeElement as HTMLElement).getBoundingClientRect(); - this.startX = left; - this.startY = top; - this.shouldScrollToEnd = this.scrolledToEnd; - } + if (type === 'start') { + const { left, top } = (this.element.nativeElement as HTMLElement).getBoundingClientRect(); + this.startX = left; + this.startY = top; + this.shouldScrollToEnd = this.scrolledToEnd; + } - if (resizeX) { - this.settings.chatlogWidth = clamp(x - this.startX, 200, 2000); - } + if (resizeX) { + this.settings.chatlogWidth = clamp(x - this.startX, 200, 2000); + } - if (resizeY) { - this.settings.chatlogHeight = clamp(this.startY - y, 120, 2000); - } + if (resizeY) { + this.settings.chatlogHeight = clamp(this.startY - y, 120, 2000); + } - this.updateChatlog(); - this.updateInnerWidth(); + this.updateChatlog(); + this.updateInnerWidth(); - if (type === 'end') { - this.settingsService.saveBrowserSettings(); - } + if (type === 'end') { + this.settingsService.saveBrowserSettings(); + } - if (this.shouldScrollToEnd) { - this.scrollToEnd(); - } - } - private setUnread(value: number) { - if (this.unread !== value) { - this.unread = value; - const count = this.countElement.nativeElement as HTMLElement; - const toggle = this.toggleButton.nativeElement as HTMLElement; + if (this.shouldScrollToEnd) { + this.scrollToEnd(); + } + } + private setUnread(value: number) { + if (this.unread !== value) { + this.unread = value; + const count = this.countElement.nativeElement as HTMLElement; + const toggle = this.toggleButton.nativeElement as HTMLElement; - if (value) { - count.textContent = value > 99 ? '99+' : `${value}`; - toggle.classList.add('has-unread'); - } else { - count.textContent = ''; - toggle.classList.remove('has-unread'); - } - } - } - private findEntityFromMessages = (id: number): FakeEntity | undefined => { - return findEntityFromMessages(id, this.whisper) || - findEntityFromMessages(id, this.party) || - findEntityFromMessages(id, this.local); - } - private findEntityFromMessagesByName = (name: string): FakeEntity | undefined => { - return findEntityFromMessagesByName(name, this.game.playerId, this.whisper) || - findEntityFromMessagesByName(name, this.game.playerId, this.party) || - findEntityFromMessagesByName(name, this.game.playerId, this.local); - } + if (value) { + count.textContent = value > 99 ? '99+' : `${value}`; + toggle.classList.add('has-unread'); + } else { + count.textContent = ''; + toggle.classList.remove('has-unread'); + } + } + } + private findEntityFromMessages = (id: number): FakeEntity | undefined => { + return findEntityFromMessages(id, this.whisper) || + findEntityFromMessages(id, this.party) || + findEntityFromMessages(id, this.local); + } + private findEntityFromMessagesByName = (name: string): FakeEntity | undefined => { + return findEntityFromMessagesByName(name, this.game.playerId, this.whisper) || + findEntityFromMessagesByName(name, this.game.playerId, this.party) || + findEntityFromMessagesByName(name, this.game.playerId, this.local); + } } function findEntityFromMessages(id: number, messages: ChatLogMessage[]): FakeEntity | undefined { - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].entityId === id) { - return { fake: true, id, type: PONY_TYPE, name: messages[i].name, crc: messages[i].crc }; - } - } + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].entityId === id) { + return { fake: true, id, type: PONY_TYPE, name: messages[i].name, crc: messages[i].crc }; + } + } - return undefined; + return undefined; } function findEntityFromMessagesByName( - name: string, playerId: number | undefined, messages: ChatLogMessage[] + name: string, playerId: number | undefined, messages: ChatLogMessage[] ): FakeEntity | undefined { - const regex = new RegExp(`^${escapeRegExp(name)}$`, 'i'); + const regex = new RegExp(`^${escapeRegExp(name)}$`, 'i'); - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; - if (message.name && message.entityId && message.entityId !== playerId && regex.test(message.name)) { - return { fake: true, id: message.entityId, type: PONY_TYPE, name: message.name, crc: message.crc }; - } - } + if (message.name && message.entityId && message.entityId !== playerId && regex.test(message.name)) { + return { fake: true, id: message.entityId, type: PONY_TYPE, name: message.name, crc: message.crc }; + } + } - return undefined; + return undefined; } diff --git a/src/ts/components/shared/check-box/check-box.ts b/src/ts/components/shared/check-box/check-box.ts index 30ae192..040384b 100644 --- a/src/ts/components/shared/check-box/check-box.ts +++ b/src/ts/components/shared/check-box/check-box.ts @@ -2,21 +2,21 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from import { faCheck } from '../../../client/icons'; @Component({ - selector: 'check-box', - templateUrl: 'check-box.pug', - styleUrls: ['check-box.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'check-box', + templateUrl: 'check-box.pug', + styleUrls: ['check-box.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CheckBox { - @Input() icon = faCheck; - @Input() label?: string; - @Input() disabled = false; - @Input() checked = false; - @Output() checkedChange = new EventEmitter(); - toggle() { - if (!this.disabled) { - this.checked = !this.checked; - this.checkedChange.emit(this.checked); - } - } + @Input() icon = faCheck; + @Input() label?: string; + @Input() disabled = false; + @Input() checked = false; + @Output() checkedChange = new EventEmitter(); + toggle() { + if (!this.disabled) { + this.checked = !this.checked; + this.checkedChange.emit(this.checked); + } + } } diff --git a/src/ts/components/shared/color-picker/color-picker.ts b/src/ts/components/shared/color-picker/color-picker.ts index b31d005..1c247a6 100644 --- a/src/ts/components/shared/color-picker/color-picker.ts +++ b/src/ts/components/shared/color-picker/color-picker.ts @@ -7,119 +7,119 @@ import { faChevronDown } from '../../../client/icons'; const SIZE = 175; @Component({ - selector: 'color-picker', - templateUrl: 'color-picker.pug', - styleUrls: ['color-picker.scss'], + selector: 'color-picker', + templateUrl: 'color-picker.pug', + styleUrls: ['color-picker.scss'], }) export class ColorPicker { - readonly chevronIcon = faChevronDown; - @Input() isOpen = false; - @Input() isDisabled = false; - @Input() disabledColor = ''; - @Input() color = ''; - @Input() indicatorColor = ''; - @Input() label?: string = undefined; - @Input() labelledBy?: string = undefined; - @Output() colorChange = new EventEmitter(); - s = 0; - v = 0; - h = 0; - private lastColor = ''; - private closeHandler = () => this.close(); - get inputColor() { - return this.isDisabled && this.disabledColor ? this.disabledColor : this.color; - } - set inputColor(value) { - if (!this.isDisabled) { - this.color = value; - } - } - get bg() { - return colorToCSS(parseColorFast(this.inputColor)); - } - get svLeft() { - this.updateHsv(); - return this.s * 100; - } - get svTop() { - this.updateHsv(); - return (1 - this.v) * 100; - } - get hueTop() { - this.updateHsv(); - return this.h * 100 / 360; - } - get hue() { - this.updateHsv(); - return colorToCSS(colorFromHSVA(this.h, 1, 1, 1)); - } - focus(e: Event) { - this.isOpen = true; - (e.target as HTMLInputElement).select(); - } - dragSV({ event, x, y }: AgDragEvent) { - event.preventDefault(); + readonly chevronIcon = faChevronDown; + @Input() isOpen = false; + @Input() isDisabled = false; + @Input() disabledColor = ''; + @Input() color = ''; + @Input() indicatorColor = ''; + @Input() label?: string = undefined; + @Input() labelledBy?: string = undefined; + @Output() colorChange = new EventEmitter(); + s = 0; + v = 0; + h = 0; + private lastColor = ''; + private closeHandler = () => this.close(); + get inputColor() { + return this.isDisabled && this.disabledColor ? this.disabledColor : this.color; + } + set inputColor(value) { + if (!this.isDisabled) { + this.color = value; + } + } + get bg() { + return colorToCSS(parseColorFast(this.inputColor)); + } + get svLeft() { + this.updateHsv(); + return this.s * 100; + } + get svTop() { + this.updateHsv(); + return (1 - this.v) * 100; + } + get hueTop() { + this.updateHsv(); + return this.h * 100 / 360; + } + get hue() { + this.updateHsv(); + return colorToCSS(colorFromHSVA(this.h, 1, 1, 1)); + } + focus(e: Event) { + this.isOpen = true; + (e.target as HTMLInputElement).select(); + } + dragSV({ event, x, y }: AgDragEvent) { + event.preventDefault(); - this.updateHsv(); - this.s = clamp(x / SIZE, 0, 1); - this.v = 1 - clamp(y / SIZE, 0, 1); - this.updateColor(); - } - dragHue({ event, y }: AgDragEvent) { - event.preventDefault(); + this.updateHsv(); + this.s = clamp(x / SIZE, 0, 1); + this.v = 1 - clamp(y / SIZE, 0, 1); + this.updateColor(); + } + dragHue({ event, y }: AgDragEvent) { + event.preventDefault(); - this.updateHsv(); - this.h = clamp(360 * y / SIZE, 0, 360); - this.updateColor(); - } - updateHsv() { - if (this.lastColor !== this.color) { - const { h, s, v } = colorToHSVA(parseColorFast(this.color), this.h); - this.h = h; - this.s = s; - this.v = v; - this.lastColor = this.color; - } - } - updateColor() { - const color = colorToHexRGB(colorFromHSVA(this.h, this.s, this.v, 1)); - const changed = this.color !== color; - this.lastColor = this.color = color; + this.updateHsv(); + this.h = clamp(360 * y / SIZE, 0, 360); + this.updateColor(); + } + updateHsv() { + if (this.lastColor !== this.color) { + const { h, s, v } = colorToHSVA(parseColorFast(this.color), this.h); + this.h = h; + this.s = s; + this.v = v; + this.lastColor = this.color; + } + } + updateColor() { + const color = colorToHexRGB(colorFromHSVA(this.h, this.s, this.v, 1)); + const changed = this.color !== color; + this.lastColor = this.color = color; - if (changed) { - this.colorChange.emit(color); - } - } - inputChanged(value: string) { - this.color = value; - this.colorChange.emit(this.color); - } - stopEvent(e: Event) { - e.stopPropagation(); - e.preventDefault(); - } - open() { - if (!this.isOpen) { - this.isOpen = true; + if (changed) { + this.colorChange.emit(color); + } + } + inputChanged(value: string) { + this.color = value; + this.colorChange.emit(this.color); + } + stopEvent(e: Event) { + e.stopPropagation(); + e.preventDefault(); + } + open() { + if (!this.isOpen) { + this.isOpen = true; - setTimeout(() => { - document.addEventListener('mousedown', this.closeHandler); - document.addEventListener('touchstart', this.closeHandler); - }); - } - } - close() { - this.isOpen = false; - document.removeEventListener('mousedown', this.closeHandler); - document.removeEventListener('touchstart', this.closeHandler); - } - toggleOpen() { - if (!this.isDisabled) { - if (this.isOpen) { - this.close(); - } else { - this.open(); - } - } - } + setTimeout(() => { + document.addEventListener('mousedown', this.closeHandler); + document.addEventListener('touchstart', this.closeHandler); + }); + } + } + close() { + this.isOpen = false; + document.removeEventListener('mousedown', this.closeHandler); + document.removeEventListener('touchstart', this.closeHandler); + } + toggleOpen() { + if (!this.isDisabled) { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + } } diff --git a/src/ts/components/shared/custom-checkbox/custom-checkbox.ts b/src/ts/components/shared/custom-checkbox/custom-checkbox.ts index 0417484..2b14cd9 100644 --- a/src/ts/components/shared/custom-checkbox/custom-checkbox.ts +++ b/src/ts/components/shared/custom-checkbox/custom-checkbox.ts @@ -2,15 +2,15 @@ import { Component, ChangeDetectionStrategy, Output, Input, EventEmitter } from import { uniqueId } from 'lodash'; @Component({ - selector: 'custom-checkbox', - templateUrl: 'custom-checkbox.pug', - styleUrls: ['custom-checkbox.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'custom-checkbox', + templateUrl: 'custom-checkbox.pug', + styleUrls: ['custom-checkbox.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CustomCheckbox { - @Input() disabled = false; - @Input() help = ''; - @Input() checked = false; - @Output() checkedChange = new EventEmitter(); - helpId = uniqueId('custom-checkbox-help-'); + @Input() disabled = false; + @Input() help = ''; + @Input() checked = false; + @Output() checkedChange = new EventEmitter(); + helpId = uniqueId('custom-checkbox-help-'); } diff --git a/src/ts/components/shared/date-picker/date-picker.ts b/src/ts/components/shared/date-picker/date-picker.ts index aa78d58..4362677 100644 --- a/src/ts/components/shared/date-picker/date-picker.ts +++ b/src/ts/components/shared/date-picker/date-picker.ts @@ -4,52 +4,52 @@ import { MONTH_NAMES_EN } from '../../../common/constants'; import { getLocale } from '../../../client/clientUtils'; @Component({ - selector: 'date-picker', - templateUrl: 'date-picker.pug', + selector: 'date-picker', + templateUrl: 'date-picker.pug', }) export class DatePicker { - readonly days = times(31, i => i + 1); - readonly years: number[] = []; - readonly months = getMonthNames(); - day = 0; - month = 0; - year = 0; - @Output() dateChange = new EventEmitter(); - constructor() { - const minYear = 1914; - const maxYear = (new Date()).getFullYear() - 6; + readonly days = times(31, i => i + 1); + readonly years: number[] = []; + readonly months = getMonthNames(); + day = 0; + month = 0; + year = 0; + @Output() dateChange = new EventEmitter(); + constructor() { + const minYear = 1914; + const maxYear = (new Date()).getFullYear() - 6; - for (let year = maxYear; year >= minYear; year--) { - this.years.push(year); - } - } - @Input() get date() { - const date = createValidBirthDate(this.day, this.month, this.year); - return date && formatISODate(date); - } - set date(value) { - if (value) { - const { day, month, year } = parseISODate(value); - this.day = day; - this.month = month; - this.year = year; - } - } - change() { - this.dateChange.emit(this.date); - } + for (let year = maxYear; year >= minYear; year--) { + this.years.push(year); + } + } + @Input() get date() { + const date = createValidBirthDate(this.day, this.month, this.year); + return date && formatISODate(date); + } + set date(value) { + if (value) { + const { day, month, year } = parseISODate(value); + this.day = day; + this.month = month; + this.year = year; + } + } + change() { + this.dateChange.emit(this.date); + } } function getMonthNames() { - try { - const format = new Intl.DateTimeFormat(getLocale(), { month: 'long' }); + try { + const format = new Intl.DateTimeFormat(getLocale(), { month: 'long' }); - return times(12, i => { - const date = new Date(523456789); - date.setMonth(i); - return format.format(date); - }); - } catch { - return MONTH_NAMES_EN; - } + return times(12, i => { + const date = new Date(523456789); + date.setMonth(i); + return format.format(date); + }); + } catch { + return MONTH_NAMES_EN; + } } diff --git a/src/ts/components/shared/directives/agAutoFocus.ts b/src/ts/components/shared/directives/agAutoFocus.ts index e27dfab..53ec289 100644 --- a/src/ts/components/shared/directives/agAutoFocus.ts +++ b/src/ts/components/shared/directives/agAutoFocus.ts @@ -1,12 +1,12 @@ import { Directive, AfterViewInit, ElementRef } from '@angular/core'; @Directive({ - selector: '[agAutoFocus]' + selector: '[agAutoFocus]' }) export class AgAutoFocus implements AfterViewInit { - constructor(private element: ElementRef) { - } - ngAfterViewInit() { - setTimeout(() => this.element.nativeElement.focus(), 100); - } + constructor(private element: ElementRef) { + } + ngAfterViewInit() { + setTimeout(() => this.element.nativeElement.focus(), 100); + } } diff --git a/src/ts/components/shared/directives/agDrag.ts b/src/ts/components/shared/directives/agDrag.ts index f567143..445c61c 100644 --- a/src/ts/components/shared/directives/agDrag.ts +++ b/src/ts/components/shared/directives/agDrag.ts @@ -3,138 +3,138 @@ import { noop } from 'lodash'; import { getButton, getX, getY, AnyEvent } from '../../../common/utils'; export interface AgDragEvent { - event: AnyEvent; - type: 'start' | 'drag' | 'end'; - x: number; - y: number; - dx: number; - dy: number; + event: AnyEvent; + type: 'start' | 'drag' | 'end'; + x: number; + y: number; + dx: number; + dy: number; } export interface AgDragOptions { - relative?: 'self' | 'parent'; - prevent?: boolean; + relative?: 'self' | 'parent'; + prevent?: boolean; } export function handleDrag(element: HTMLElement, emit: (event: AgDragEvent) => void, options: AgDragOptions = {}) { - // typeof PointerEvent !== 'undefined' - const eventSets = window.navigator.pointerEnabled ? [ - { down: 'pointerdown', move: 'pointermove', up: 'pointerup' }, // , up2: 'pointercancel' }, - ] : [ - { down: 'mousedown', move: 'mousemove', up: 'mouseup' }, - { down: 'touchstart', move: 'touchmove', up: 'touchend', up2: 'touchcancel' }, - ]; - const emptyRect = { left: 0, top: 0 }; - let rect = emptyRect; - let scrollLeft = 0; - let scrollTop = 0; - let startX = 0; - let startY = 0; - let button = 0; - let dragging = false; - let lastEvent: any; + // typeof PointerEvent !== 'undefined' + const eventSets = window.navigator.pointerEnabled ? [ + { down: 'pointerdown', move: 'pointermove', up: 'pointerup' }, // , up2: 'pointercancel' }, + ] : [ + { down: 'mousedown', move: 'mousemove', up: 'mouseup' }, + { down: 'touchstart', move: 'touchmove', up: 'touchend', up2: 'touchcancel' }, + ]; + const emptyRect = { left: 0, top: 0 }; + let rect = emptyRect; + let scrollLeft = 0; + let scrollTop = 0; + let startX = 0; + let startY = 0; + let button = 0; + let dragging = false; + let lastEvent: any; - function setupScrollAndRect() { - // TODO: fix issue with scroll - switch (options.relative) { - case 'self': - rect = element.getBoundingClientRect(); - scrollLeft = -(window.scrollX || window.pageXOffset || 0); - scrollTop = -(window.scrollY || window.pageYOffset || 0); - break; - case 'parent': - rect = element.parentElement!.getBoundingClientRect(); - scrollLeft = element.parentElement!.scrollLeft; - scrollTop = element.parentElement!.scrollTop; - break; - default: - rect = emptyRect; - scrollLeft = 0; - scrollTop = 0; - } - } + function setupScrollAndRect() { + // TODO: fix issue with scroll + switch (options.relative) { + case 'self': + rect = element.getBoundingClientRect(); + scrollLeft = -(window.scrollX || window.pageXOffset || 0); + scrollTop = -(window.scrollY || window.pageYOffset || 0); + break; + case 'parent': + rect = element.parentElement!.getBoundingClientRect(); + scrollLeft = element.parentElement!.scrollLeft; + scrollTop = element.parentElement!.scrollTop; + break; + default: + rect = emptyRect; + scrollLeft = 0; + scrollTop = 0; + } + } - function send(event: AnyEvent, type: 'start' | 'drag' | 'end') { - const x = getX(event); - const y = getY(event); + function send(event: AnyEvent, type: 'start' | 'drag' | 'end') { + const x = getX(event); + const y = getY(event); - emit({ - event, - type, - x: x - rect.left + scrollLeft, - y: y - rect.top + scrollTop, - dx: x - startX, - dy: y - startY, - }); - } + emit({ + event, + type, + x: x - rect.left + scrollLeft, + y: y - rect.top + scrollTop, + dx: x - startX, + dy: y - startY, + }); + } - const handlers = eventSets.map(events => { - function move(e: any) { - lastEvent = e; - e.preventDefault(); - send(e, 'drag'); - } + const handlers = eventSets.map(events => { + function move(e: any) { + lastEvent = e; + e.preventDefault(); + send(e, 'drag'); + } - function up(e: any) { - if (getButton(e) === button) { - // touchend event does not have x, y coordinates, use last touchmove event instead - if (e.type !== 'touchend' && e.type !== 'touchcancel') { - lastEvent = e; - } - end(); - } - } + function up(e: any) { + if (getButton(e) === button) { + // touchend event does not have x, y coordinates, use last touchmove event instead + if (e.type !== 'touchend' && e.type !== 'touchcancel') { + lastEvent = e; + } + end(); + } + } - function end() { - send(lastEvent, 'end'); - window.removeEventListener(events.move, move); - window.removeEventListener(events.up, up); - events.up2 && window.removeEventListener(events.up2, up); - window.removeEventListener('blur', end); - dragging = false; - } + function end() { + send(lastEvent, 'end'); + window.removeEventListener(events.move, move); + window.removeEventListener(events.up, up); + events.up2 && window.removeEventListener(events.up2, up); + window.removeEventListener('blur', end); + dragging = false; + } - function handler(e: any) { - if (!dragging) { - setupScrollAndRect(); - dragging = true; - button = getButton(e); - startX = getX(e); - startY = getY(e); - send(e, 'start'); - lastEvent = e; + function handler(e: any) { + if (!dragging) { + setupScrollAndRect(); + dragging = true; + button = getButton(e); + startX = getX(e); + startY = getY(e); + send(e, 'start'); + lastEvent = e; - window.addEventListener(events.move, move); - window.addEventListener(events.up, up); - events.up2 && window.addEventListener(events.up2, up); - window.addEventListener('blur', end); - e.stopPropagation(); + window.addEventListener(events.move, move); + window.addEventListener(events.up, up); + events.up2 && window.addEventListener(events.up2, up); + window.addEventListener('blur', end); + e.stopPropagation(); - if (options.prevent) { - e.preventDefault(); - } - } - } + if (options.prevent) { + e.preventDefault(); + } + } + } - element.addEventListener(events.down, handler); - return () => element.removeEventListener(events.down, handler); - }); + element.addEventListener(events.down, handler); + return () => element.removeEventListener(events.down, handler); + }); - return () => handlers.forEach(f => f()); + return () => handlers.forEach(f => f()); } @Directive({ selector: '[agDrag]' }) export class AgDrag implements OnInit, OnDestroy { - @Input('agDragRelative') relative: 'self' | 'parent' | undefined = undefined; - @Input('agDragPrevent') prevent = false; - @Output('agDrag') drag = new EventEmitter(); - private unsubscribe = noop; - constructor(private element: ElementRef) { - } - ngOnInit() { - this.unsubscribe = handleDrag(this.element.nativeElement, e => this.drag.emit(e), this); - } - ngOnDestroy() { - this.unsubscribe(); - } + @Input('agDragRelative') relative: 'self' | 'parent' | undefined = undefined; + @Input('agDragPrevent') prevent = false; + @Output('agDrag') drag = new EventEmitter(); + private unsubscribe = noop; + constructor(private element: ElementRef) { + } + ngOnInit() { + this.unsubscribe = handleDrag(this.element.nativeElement, e => this.drag.emit(e), this); + } + ngOnDestroy() { + this.unsubscribe(); + } } diff --git a/src/ts/components/shared/directives/anchor.ts b/src/ts/components/shared/directives/anchor.ts index 0cfbb1b..3ee1406 100644 --- a/src/ts/components/shared/directives/anchor.ts +++ b/src/ts/components/shared/directives/anchor.ts @@ -1,17 +1,17 @@ import { Directive, OnInit, ElementRef } from '@angular/core'; @Directive({ - selector: 'a[href]' + selector: 'a[href]' }) export class Anchor implements OnInit { - constructor(private element: ElementRef) { - } - ngOnInit() { - const a = this.element.nativeElement as HTMLAnchorElement; + constructor(private element: ElementRef) { + } + ngOnInit() { + const a = this.element.nativeElement as HTMLAnchorElement; - if (/^(https?|mailto):/.test(a.href) && !a.target) { - a.setAttribute('target', '_blank'); - a.setAttribute('rel', 'noopener noreferrer'); - } - } + if (/^(https?|mailto):/.test(a.href) && !a.target) { + a.setAttribute('target', '_blank'); + a.setAttribute('rel', 'noopener noreferrer'); + } + } } diff --git a/src/ts/components/shared/directives/btnHighlight.ts b/src/ts/components/shared/directives/btnHighlight.ts index 31ebfcf..9ddf860 100644 --- a/src/ts/components/shared/directives/btnHighlight.ts +++ b/src/ts/components/shared/directives/btnHighlight.ts @@ -2,29 +2,29 @@ import { Directive, Input, Optional } from '@angular/core'; import { NgModel } from '@angular/forms'; @Directive({ - selector: '[btnHighlight]', - host: { - '[class.btn-default]': '!on', - '[class.btn-primary]': 'on', - }, + selector: '[btnHighlight]', + host: { + '[class.btn-default]': '!on', + '[class.btn-primary]': 'on', + }, }) export class BtnHighlight { - @Input() btnHighlight?: boolean = undefined; - constructor(@Optional() private model?: NgModel) { - } - get on() { - const value = this.btnHighlight; - return (value === true || value === false || !this.model) ? value : !!this.model.value; - } + @Input() btnHighlight?: boolean = undefined; + constructor(@Optional() private model?: NgModel) { + } + get on() { + const value = this.btnHighlight; + return (value === true || value === false || !this.model) ? value : !!this.model.value; + } } @Directive({ - selector: '[btnHighlightDanger]', - host: { - '[class.btn-default]': '!btnHighlightDanger', - '[class.btn-danger]': 'btnHighlightDanger', - }, + selector: '[btnHighlightDanger]', + host: { + '[class.btn-default]': '!btnHighlightDanger', + '[class.btn-danger]': 'btnHighlightDanger', + }, }) export class BtnHighlightDanger { - @Input() btnHighlightDanger = false; + @Input() btnHighlightDanger = false; } diff --git a/src/ts/components/shared/directives/draggable.ts b/src/ts/components/shared/directives/draggable.ts index 88525d4..f925104 100644 --- a/src/ts/components/shared/directives/draggable.ts +++ b/src/ts/components/shared/directives/draggable.ts @@ -6,202 +6,202 @@ import { rect } from '../../../common/rect'; @Injectable({ providedIn: 'root' }) export class DraggableService { - root?: ElementRef; - draggedItem?: any; - activeDropZone?: DraggableDrop; - dropZones: DraggableDrop[] = []; - get rootElement(): HTMLElement { - return this.root ? this.root.nativeElement : document.body; - } - setActiveDropZone(dropZone: DraggableDrop | undefined) { - if (this.activeDropZone !== dropZone) { - if (this.activeDropZone) { - this.activeDropZone.setActive(false); - } + root?: ElementRef; + draggedItem?: any; + activeDropZone?: DraggableDrop; + dropZones: DraggableDrop[] = []; + get rootElement(): HTMLElement { + return this.root ? this.root.nativeElement : document.body; + } + setActiveDropZone(dropZone: DraggableDrop | undefined) { + if (this.activeDropZone !== dropZone) { + if (this.activeDropZone) { + this.activeDropZone.setActive(false); + } - this.activeDropZone = dropZone; + this.activeDropZone = dropZone; - if (this.activeDropZone) { - this.activeDropZone.setActive(true); - } - } - } - startMove(element: HTMLElement, item: any) { - this.setActiveDropZone(undefined); - this.rootElement.appendChild(element); - this.draggedItem = item; - this.initRects(); - } - endMove() { - if (this.activeDropZone) { - this.activeDropZone.drop.emit(this.draggedItem); - this.setActiveDropZone(undefined); - } + if (this.activeDropZone) { + this.activeDropZone.setActive(true); + } + } + } + startMove(element: HTMLElement, item: any) { + this.setActiveDropZone(undefined); + this.rootElement.appendChild(element); + this.draggedItem = item; + this.initRects(); + } + endMove() { + if (this.activeDropZone) { + this.activeDropZone.drop.emit(this.draggedItem); + this.setActiveDropZone(undefined); + } - this.draggedItem = undefined; - } - addDropZone(dropZone: DraggableDrop) { - this.dropZones.push(dropZone); + this.draggedItem = undefined; + } + addDropZone(dropZone: DraggableDrop) { + this.dropZones.push(dropZone); - if (this.draggedItem) { - this.initRects(); - } - } - removeDropZone(dropZone: DraggableDrop) { - removeItem(this.dropZones, dropZone); + if (this.draggedItem) { + this.initRects(); + } + } + removeDropZone(dropZone: DraggableDrop) { + removeItem(this.dropZones, dropZone); - if (this.draggedItem) { - this.initRects(); - } + if (this.draggedItem) { + this.initRects(); + } - if (this.activeDropZone === dropZone) { - this.setActiveDropZone(undefined); - } - } - updateHover(x: number, y: number) { - if (this.draggedItem) { - for (const zone of this.dropZones) { - if (pointInRect(x, y, zone.rect)) { - this.setActiveDropZone(zone); - return; - } - } + if (this.activeDropZone === dropZone) { + this.setActiveDropZone(undefined); + } + } + updateHover(x: number, y: number) { + if (this.draggedItem) { + for (const zone of this.dropZones) { + if (pointInRect(x, y, zone.rect)) { + this.setActiveDropZone(zone); + return; + } + } - this.setActiveDropZone(undefined); - } - } - private initRects() { - this.dropZones.forEach(i => i.initRect()); - } + this.setActiveDropZone(undefined); + } + } + private initRects() { + this.dropZones.forEach(i => i.initRect()); + } } @Component({ - selector: 'draggable-outlet', - template: `
`, - styles: [`:host { position: fixed; top: 0; left: 0; z-index: 10000; }`], + selector: 'draggable-outlet', + template: `
`, + styles: [`:host { position: fixed; top: 0; left: 0; z-index: 10000; }`], }) export class DraggableOutlet { - constructor(element: ElementRef, service: DraggableService) { - service.root = element; - } + constructor(element: ElementRef, service: DraggableService) { + service.root = element; + } } @Directive({ selector: '[draggableDrop]' }) export class DraggableDrop implements OnInit, OnDestroy { - @Input('draggablePad') pad = 0; - @Output('draggableDrop') drop = new EventEmitter(); - rect = rect(0, 0, 0, 0); - constructor(private element: ElementRef, private service: DraggableService) { - } - ngOnInit() { - this.service.addDropZone(this); - } - ngOnDestroy() { - this.service.removeDropZone(this); - } - setActive(active: boolean) { - const element = this.element.nativeElement as HTMLElement; + @Input('draggablePad') pad = 0; + @Output('draggableDrop') drop = new EventEmitter(); + rect = rect(0, 0, 0, 0); + constructor(private element: ElementRef, private service: DraggableService) { + } + ngOnInit() { + this.service.addDropZone(this); + } + ngOnDestroy() { + this.service.removeDropZone(this); + } + setActive(active: boolean) { + const element = this.element.nativeElement as HTMLElement; - if (active) { - element.classList.add('draggable-hover'); - } else { - element.classList.remove('draggable-hover'); - } - } - initRect() { - const element = this.element.nativeElement as HTMLElement; - const clientBounds = element.getBoundingClientRect(); - this.rect.x = clientBounds.left - this.pad; - this.rect.y = clientBounds.top - this.pad; - this.rect.w = clientBounds.width + 2 * this.pad; - this.rect.h = clientBounds.height + 2 * this.pad; - } + if (active) { + element.classList.add('draggable-hover'); + } else { + element.classList.remove('draggable-hover'); + } + } + initRect() { + const element = this.element.nativeElement as HTMLElement; + const clientBounds = element.getBoundingClientRect(); + this.rect.x = clientBounds.left - this.pad; + this.rect.y = clientBounds.top - this.pad; + this.rect.w = clientBounds.width + 2 * this.pad; + this.rect.h = clientBounds.height + 2 * this.pad; + } } @Directive({ - selector: '[draggableItem]', - host: { - '[style.touch-action]': `touchAction`, - } + selector: '[draggableItem]', + host: { + '[style.touch-action]': `touchAction`, + } }) export class DraggableItem implements OnInit, OnDestroy { - @Input('draggableItem') item: T | undefined; - @Output('draggableDrag') dragStarted = new EventEmitter(); - private startX = 0; - private startY = 0; - private draggable?: HTMLElement; - private width = 0; - private height = 0; - private unsubscribeDrag = noop; - private _disabled = false; - constructor(private element: ElementRef, private service: DraggableService) { - } - ngOnInit() { - this.setupDragEvents(); - } - ngOnDestroy() { - this.unsubscribeDrag(); - } - get touchAction() { - return this.disabled ? 'inherit' : 'none'; - } - @Input('draggableDisabled') get disabled() { - return this._disabled; - } - set disabled(value) { - if (this._disabled !== value) { - this._disabled = value; - this.setupDragEvents(); - } - } - private setupDragEvents() { - this.unsubscribeDrag(); - this.unsubscribeDrag = noop; + @Input('draggableItem') item: T | undefined; + @Output('draggableDrag') dragStarted = new EventEmitter(); + private startX = 0; + private startY = 0; + private draggable?: HTMLElement; + private width = 0; + private height = 0; + private unsubscribeDrag = noop; + private _disabled = false; + constructor(private element: ElementRef, private service: DraggableService) { + } + ngOnInit() { + this.setupDragEvents(); + } + ngOnDestroy() { + this.unsubscribeDrag(); + } + get touchAction() { + return this.disabled ? 'inherit' : 'none'; + } + @Input('draggableDisabled') get disabled() { + return this._disabled; + } + set disabled(value) { + if (this._disabled !== value) { + this._disabled = value; + this.setupDragEvents(); + } + } + private setupDragEvents() { + this.unsubscribeDrag(); + this.unsubscribeDrag = noop; - if (!this.disabled) { - this.unsubscribeDrag = handleDrag(this.element.nativeElement, e => this.drag(e), { prevent: true }); - } - } - drag(e: AgDragEvent) { - if (this.item && !this.disabled && !this.draggable && (Math.abs(e.dx) > 5 || Math.abs(e.dy) > 5)) { - const element = this.element.nativeElement as HTMLElement; - const rect = element.getBoundingClientRect(); - this.startX = rect.left; - this.startY = rect.top; - this.draggable = element.cloneNode(true) as HTMLElement; - this.draggable.style.position = 'absolute'; - this.draggable.style.width = `${rect.width}px`; - this.draggable.style.height = `${rect.height}px`; - this.draggable.style.margin = '0'; - this.draggable.classList.add('draggable-dragging'); - this.width = rect.width; - this.height = rect.height; + if (!this.disabled) { + this.unsubscribeDrag = handleDrag(this.element.nativeElement, e => this.drag(e), { prevent: true }); + } + } + drag(e: AgDragEvent) { + if (this.item && !this.disabled && !this.draggable && (Math.abs(e.dx) > 5 || Math.abs(e.dy) > 5)) { + const element = this.element.nativeElement as HTMLElement; + const rect = element.getBoundingClientRect(); + this.startX = rect.left; + this.startY = rect.top; + this.draggable = element.cloneNode(true) as HTMLElement; + this.draggable.style.position = 'absolute'; + this.draggable.style.width = `${rect.width}px`; + this.draggable.style.height = `${rect.height}px`; + this.draggable.style.margin = '0'; + this.draggable.classList.add('draggable-dragging'); + this.width = rect.width; + this.height = rect.height; - const src = element.querySelectorAll('canvas') as NodeListOf; - const dst = this.draggable.querySelectorAll('canvas') as NodeListOf; + const src = element.querySelectorAll('canvas') as NodeListOf; + const dst = this.draggable.querySelectorAll('canvas') as NodeListOf; - for (let i = 0; i < src.length; i++) { - const context = dst.item(i).getContext('2d'); - context && context.drawImage(src.item(i), 0, 0); - } + for (let i = 0; i < src.length; i++) { + const context = dst.item(i).getContext('2d'); + context && context.drawImage(src.item(i), 0, 0); + } - this.service.startMove(this.draggable, this.item!); - this.dragStarted.emit(); - } + this.service.startMove(this.draggable, this.item!); + this.dragStarted.emit(); + } - if (this.draggable) { - if (e.type === 'end') { - this.draggable!.parentNode!.removeChild(this.draggable!); - this.draggable = undefined; - this.service.endMove(); - } else { - const x = clamp(this.startX + e.dx, 0, window.innerWidth - this.width); - const y = clamp(this.startY + e.dy, 0, window.innerHeight - this.height); - setTransform(this.draggable, `translate3d(${x}px, ${y}px, 0px)`); - this.service.updateHover(e.x, e.y); - } - } - } + if (this.draggable) { + if (e.type === 'end') { + this.draggable!.parentNode!.removeChild(this.draggable!); + this.draggable = undefined; + this.service.endMove(); + } else { + const x = clamp(this.startX + e.dx, 0, window.innerWidth - this.width); + const y = clamp(this.startY + e.dy, 0, window.innerHeight - this.height); + setTransform(this.draggable, `translate3d(${x}px, ${y}px, 0px)`); + this.service.updateHover(e.x, e.y); + } + } + } } export const draggableComponents = [DraggableOutlet, DraggableItem, DraggableDrop]; diff --git a/src/ts/components/shared/directives/dropdown.ts b/src/ts/components/shared/directives/dropdown.ts index 74fb33d..782365b 100644 --- a/src/ts/components/shared/directives/dropdown.ts +++ b/src/ts/components/shared/directives/dropdown.ts @@ -1,227 +1,227 @@ import { - Directive, HostListener, Input, Output, EventEmitter, TemplateRef, ViewContainerRef, ContentChild, - Renderer2, ElementRef, EmbeddedViewRef, Component, Injectable + Directive, HostListener, Input, Output, EventEmitter, TemplateRef, ViewContainerRef, ContentChild, + Renderer2, ElementRef, EmbeddedViewRef, Component, Injectable } from '@angular/core'; import { uniqueId } from 'lodash'; import { focusFirstElement } from '../../../client/htmlUtils'; @Injectable({ providedIn: 'root' }) export class DropdownOutletService { - viewContainer?: ViewContainerRef; - rootElement?: HTMLElement; + viewContainer?: ViewContainerRef; + rootElement?: HTMLElement; } @Component({ - selector: 'dropdown-outlet', - template: ``, + selector: 'dropdown-outlet', + template: ``, }) export class DropdownOutlet { - constructor(service: DropdownOutletService, viewContainer: ViewContainerRef, element: ElementRef) { - service.viewContainer = viewContainer; - service.rootElement = element.nativeElement.parentElement; - } + constructor(service: DropdownOutletService, viewContainer: ViewContainerRef, element: ElementRef) { + service.viewContainer = viewContainer; + service.rootElement = element.nativeElement.parentElement; + } } @Directive({ - selector: '[dropdownMenu]', + selector: '[dropdownMenu]', }) export class DropdownMenu { - ref?: EmbeddedViewRef; - id = uniqueId('dropdown-menu-'); - private onClose?: () => void; - constructor( - private templateRef: TemplateRef, - private viewContainer: ViewContainerRef, - private renderer: Renderer2, - private service: DropdownOutletService, - ) { - } - private get root(): HTMLElement { - return this.ref && this.ref.rootNodes[0]; - } - open(useOutlet: boolean, rootElement: HTMLElement) { - if (!this.ref) { - if (useOutlet) { - this.ref = this.service.viewContainer!.createEmbeddedView(this.templateRef); - } else { - this.ref = this.viewContainer.createEmbeddedView(this.templateRef); - } + ref?: EmbeddedViewRef; + id = uniqueId('dropdown-menu-'); + private onClose?: () => void; + constructor( + private templateRef: TemplateRef, + private viewContainer: ViewContainerRef, + private renderer: Renderer2, + private service: DropdownOutletService, + ) { + } + private get root(): HTMLElement { + return this.ref && this.ref.rootNodes[0]; + } + open(useOutlet: boolean, rootElement: HTMLElement) { + if (!this.ref) { + if (useOutlet) { + this.ref = this.service.viewContainer!.createEmbeddedView(this.templateRef); + } else { + this.ref = this.viewContainer.createEmbeddedView(this.templateRef); + } - const { renderer, root } = this; + const { renderer, root } = this; - renderer.addClass(root, 'show'); - renderer.setAttribute(root, 'id', this.id); + renderer.addClass(root, 'show'); + renderer.setAttribute(root, 'id', this.id); - if (useOutlet) { - const positionMenu = () => { - const rect = rootElement.getBoundingClientRect(); - const menuRect = root.getBoundingClientRect(); - let transform: string; + if (useOutlet) { + const positionMenu = () => { + const rect = rootElement.getBoundingClientRect(); + const menuRect = root.getBoundingClientRect(); + let transform: string; - if ((rect.bottom + menuRect.height) > window.innerHeight) { - transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top - menuRect.height)}px, 0)`; - renderer.addClass(root, 'dropdown-menu-up'); - } else { - transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.bottom)}px, 0)`; - renderer.removeClass(root, 'dropdown-menu-up'); - } + if ((rect.bottom + menuRect.height) > window.innerHeight) { + transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top - menuRect.height)}px, 0)`; + renderer.addClass(root, 'dropdown-menu-up'); + } else { + transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.bottom)}px, 0)`; + renderer.removeClass(root, 'dropdown-menu-up'); + } - renderer.setStyle(root, 'transform', transform); - }; + renderer.setStyle(root, 'transform', transform); + }; - renderer.addClass(root, 'dropdown-in-outlet'); - positionMenu(); + renderer.addClass(root, 'dropdown-in-outlet'); + positionMenu(); - const closeDropdown = () => { - this.close(); - }; + const closeDropdown = () => { + this.close(); + }; - document.addEventListener('scroll', closeDropdown, true); - window.addEventListener('resize', closeDropdown, true); + document.addEventListener('scroll', closeDropdown, true); + window.addEventListener('resize', closeDropdown, true); - this.onClose = () => { - document.removeEventListener('scroll', closeDropdown, true); - window.removeEventListener('resize', closeDropdown, true); - }; - } - } - } - close() { - if (this.ref) { - this.ref.destroy(); - this.ref = undefined; - } + this.onClose = () => { + document.removeEventListener('scroll', closeDropdown, true); + window.removeEventListener('resize', closeDropdown, true); + }; + } + } + } + close() { + if (this.ref) { + this.ref.destroy(); + this.ref = undefined; + } - if (this.onClose) { - this.onClose(); - this.onClose = undefined; - } - } - checkTarget(e: Event) { - return this.root && this.root.contains(e.target as any); - } - focusFirstElement() { - if (this.root) { - focusFirstElement(this.root); - } - } + if (this.onClose) { + this.onClose(); + this.onClose = undefined; + } + } + checkTarget(e: Event) { + return this.root && this.root.contains(e.target as any); + } + focusFirstElement() { + if (this.root) { + focusFirstElement(this.root); + } + } } @Directive({ - selector: '[dropdown]', - exportAs: 'ag-dropdown', - host: { - '[class.show]': 'isOpen', - }, + selector: '[dropdown]', + exportAs: 'ag-dropdown', + host: { + '[class.show]': 'isOpen', + }, }) export class Dropdown { - dropdownToggle?: DropdownToggle; - @ContentChild(DropdownMenu, { static: false }) menu!: DropdownMenu; - @Input() autoClose: boolean | 'outsideClick' = true; - @Input() preventAutoCloseOnOutlet = false; - @Input() hookToCanvas = false; - @Input() focusOnOpen = true; - @Input() focusOnClose = true; - @Input() useOutlet = false; - @Input() isOpen = false; - @Output() isOpenChange = new EventEmitter(); - get menuId() { - return this.isOpen ? this.menu.id : ''; - } - constructor(private element: ElementRef, private service: DropdownOutletService) { - } - open() { - if (!this.isOpen) { - this.isOpen = true; - this.isOpenChange.emit(true); - this.menu.open(this.useOutlet, this.element.nativeElement); + dropdownToggle?: DropdownToggle; + @ContentChild(DropdownMenu, { static: false }) menu!: DropdownMenu; + @Input() autoClose: boolean | 'outsideClick' = true; + @Input() preventAutoCloseOnOutlet = false; + @Input() hookToCanvas = false; + @Input() focusOnOpen = true; + @Input() focusOnClose = true; + @Input() useOutlet = false; + @Input() isOpen = false; + @Output() isOpenChange = new EventEmitter(); + get menuId() { + return this.isOpen ? this.menu.id : ''; + } + constructor(private element: ElementRef, private service: DropdownOutletService) { + } + open() { + if (!this.isOpen) { + this.isOpen = true; + this.isOpenChange.emit(true); + this.menu.open(this.useOutlet, this.element.nativeElement); - setTimeout(() => { - document.addEventListener('click', this.closeHandler); - document.addEventListener('keydown', this.closeHandler); + setTimeout(() => { + document.addEventListener('click', this.closeHandler); + document.addEventListener('keydown', this.closeHandler); - if (this.focusOnOpen) { - this.menu.focusFirstElement(); - } + if (this.focusOnOpen) { + this.menu.focusFirstElement(); + } - if (this.hookToCanvas) { - const canvas = document.getElementById('canvas'); + if (this.hookToCanvas) { + const canvas = document.getElementById('canvas'); - if (canvas) { - canvas.addEventListener('touchstart', this.canvasCloseHandler); - canvas.addEventListener('mousedown', this.canvasCloseHandler); - } - } - }); - } - } - close() { - if (this.isOpen) { - this.isOpen = false; - this.isOpenChange.emit(false); - this.menu.close(); + if (canvas) { + canvas.addEventListener('touchstart', this.canvasCloseHandler); + canvas.addEventListener('mousedown', this.canvasCloseHandler); + } + } + }); + } + } + close() { + if (this.isOpen) { + this.isOpen = false; + this.isOpenChange.emit(false); + this.menu.close(); - if (this.focusOnClose && this.dropdownToggle) { - this.dropdownToggle.focus(); - } + if (this.focusOnClose && this.dropdownToggle) { + this.dropdownToggle.focus(); + } - document.removeEventListener('click', this.closeHandler); - document.removeEventListener('keydown', this.closeHandler); + document.removeEventListener('click', this.closeHandler); + document.removeEventListener('keydown', this.closeHandler); - if (this.hookToCanvas) { - const canvas = document.getElementById('canvas'); + if (this.hookToCanvas) { + const canvas = document.getElementById('canvas'); - if (canvas) { - canvas.removeEventListener('touchstart', this.canvasCloseHandler); - canvas.removeEventListener('mousedown', this.canvasCloseHandler); - } - } - } - } - toggle() { - if (this.isOpen) { - this.close(); - } else { - this.open(); - } - } - private closeHandler: any = (e: KeyboardEvent) => { - if ( - !e.keyCode - && (this.autoClose || (this.dropdownToggle && this.dropdownToggle.checkTarget(e))) - && !(this.preventAutoCloseOnOutlet && this.service.rootElement && this.service.rootElement.contains(e.target as any)) - && !(this.autoClose === 'outsideClick' && this.menu.checkTarget(e)) - ) { - this.close(); - } else if (this.autoClose && e.keyCode === 27) { // esc - this.close(); - } - } - private canvasCloseHandler: any = () => this.close(); + if (canvas) { + canvas.removeEventListener('touchstart', this.canvasCloseHandler); + canvas.removeEventListener('mousedown', this.canvasCloseHandler); + } + } + } + } + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + private closeHandler: any = (e: KeyboardEvent) => { + if ( + !e.keyCode + && (this.autoClose || (this.dropdownToggle && this.dropdownToggle.checkTarget(e))) + && !(this.preventAutoCloseOnOutlet && this.service.rootElement && this.service.rootElement.contains(e.target as any)) + && !(this.autoClose === 'outsideClick' && this.menu.checkTarget(e)) + ) { + this.close(); + } else if (this.autoClose && e.keyCode === 27) { // esc + this.close(); + } + } + private canvasCloseHandler: any = () => this.close(); } @Directive({ - selector: '[dropdownToggle]', - host: { - 'aria-haspopup': 'true', - '[attr.aria-expanded]': 'dropdown.isOpen', - '[attr.aria-controls]': 'dropdown.isOpen ? dropdown.menuId : undefined', - }, + selector: '[dropdownToggle]', + host: { + 'aria-haspopup': 'true', + '[attr.aria-expanded]': 'dropdown.isOpen', + '[attr.aria-controls]': 'dropdown.isOpen ? dropdown.menuId : undefined', + }, }) export class DropdownToggle { - constructor(private element: ElementRef, public dropdown: Dropdown) { - dropdown.dropdownToggle = this; - } - @HostListener('click') - click() { - this.dropdown.toggle(); - } - checkTarget(e: Event) { - return this.element.nativeElement.contains(e.target); - } - focus() { - this.element.nativeElement.focus(); - } + constructor(private element: ElementRef, public dropdown: Dropdown) { + dropdown.dropdownToggle = this; + } + @HostListener('click') + click() { + this.dropdown.toggle(); + } + checkTarget(e: Event) { + return this.element.nativeElement.contains(e.target); + } + focus() { + this.element.nativeElement.focus(); + } } export const dropdownDirectives = [Dropdown, DropdownToggle, DropdownMenu, DropdownOutlet]; diff --git a/src/ts/components/shared/directives/fixToTop.ts b/src/ts/components/shared/directives/fixToTop.ts index 9798c19..2f43c4e 100644 --- a/src/ts/components/shared/directives/fixToTop.ts +++ b/src/ts/components/shared/directives/fixToTop.ts @@ -1,22 +1,22 @@ import { Directive, ElementRef, Input, HostListener, HostBinding, Output, EventEmitter } from '@angular/core'; @Directive({ - selector: '[fixToTop]', + selector: '[fixToTop]', }) export class FixToTop { - @Input() fixToTopOffset = 0; - @Output() fixToTop = new EventEmitter(); - @HostBinding('class.fixed-to-top') fixed = false; - constructor(private element: ElementRef) { - } - @HostListener('window:scroll') - scroll() { - const element = this.element.nativeElement as HTMLElement; - const { top } = element.getBoundingClientRect(); + @Input() fixToTopOffset = 0; + @Output() fixToTop = new EventEmitter(); + @HostBinding('class.fixed-to-top') fixed = false; + constructor(private element: ElementRef) { + } + @HostListener('window:scroll') + scroll() { + const element = this.element.nativeElement as HTMLElement; + const { top } = element.getBoundingClientRect(); - if (this.fixed !== top < this.fixToTopOffset) { - this.fixed = top < this.fixToTopOffset; - this.fixToTop.emit(this.fixed); - } - } + if (this.fixed !== top < this.fixToTopOffset) { + this.fixed = top < this.fixToTopOffset; + this.fixToTop.emit(this.fixed); + } + } } diff --git a/src/ts/components/shared/directives/focusTitle.ts b/src/ts/components/shared/directives/focusTitle.ts index fd49244..747ac14 100644 --- a/src/ts/components/shared/directives/focusTitle.ts +++ b/src/ts/components/shared/directives/focusTitle.ts @@ -1,15 +1,15 @@ import { Directive, AfterViewInit, ElementRef } from '@angular/core'; @Directive({ - selector: '[focusTitle]', - host: { - 'tabindex': '-1', - }, + selector: '[focusTitle]', + host: { + 'tabindex': '-1', + }, }) export class FocusTitle implements AfterViewInit { - constructor(private element: ElementRef) { - } - ngAfterViewInit() { - setTimeout(() => this.element.nativeElement.focus()); - } + constructor(private element: ElementRef) { + } + ngAfterViewInit() { + setTimeout(() => this.element.nativeElement.focus()); + } } diff --git a/src/ts/components/shared/directives/focusTrap.ts b/src/ts/components/shared/directives/focusTrap.ts index 7f2be05..abc4dd8 100644 --- a/src/ts/components/shared/directives/focusTrap.ts +++ b/src/ts/components/shared/directives/focusTrap.ts @@ -3,55 +3,55 @@ import { isParentOf, focusFirstElement, findFocusableElements } from '../../../c import { isMobile } from '../../../client/data'; @Directive({ - selector: '[focusTrap]', + selector: '[focusTrap]', }) export class FocusTrap implements OnInit, OnDestroy { - private on = true; - private lastActiveElement?: HTMLElement; - @Input() set focusTrap(value: boolean) { - if (this.on !== value) { - this.on = value; - this.update(); - } - } - constructor(private element: ElementRef) { - } - ngOnInit() { - this.update(); - } - ngOnDestroy() { - this.focusTrap = false; - } - private update() { - if (!isMobile) { - if (this.on) { - this.lastActiveElement = document.activeElement as HTMLElement; - document.addEventListener('focusin', this.focus); + private on = true; + private lastActiveElement?: HTMLElement; + @Input() set focusTrap(value: boolean) { + if (this.on !== value) { + this.on = value; + this.update(); + } + } + constructor(private element: ElementRef) { + } + ngOnInit() { + this.update(); + } + ngOnDestroy() { + this.focusTrap = false; + } + private update() { + if (!isMobile) { + if (this.on) { + this.lastActiveElement = document.activeElement as HTMLElement; + document.addEventListener('focusin', this.focus); - if (!isParentOf(this.element.nativeElement, this.lastActiveElement)) { - setTimeout(() => this.lastActiveElement = focusFirstElement(this.element.nativeElement)); - } - } else { - this.lastActiveElement = undefined; - document.removeEventListener('focusin', this.focus); - } - } - } - private focus = (e: Event) => { - if (isParentOf(this.element.nativeElement, e.target as any)) { - this.lastActiveElement = e.target as any; - } else { - const focusable = findFocusableElements(this.element.nativeElement); + if (!isParentOf(this.element.nativeElement, this.lastActiveElement)) { + setTimeout(() => this.lastActiveElement = focusFirstElement(this.element.nativeElement)); + } + } else { + this.lastActiveElement = undefined; + document.removeEventListener('focusin', this.focus); + } + } + } + private focus = (e: Event) => { + if (isParentOf(this.element.nativeElement, e.target as any)) { + this.lastActiveElement = e.target as any; + } else { + const focusable = findFocusableElements(this.element.nativeElement); - if (focusable.length) { - if (this.lastActiveElement === focusable[0]) { - this.lastActiveElement = focusable[focusable.length - 1]; - } else { - this.lastActiveElement = focusable[0]; - } + if (focusable.length) { + if (this.lastActiveElement === focusable[0]) { + this.lastActiveElement = focusable[focusable.length - 1]; + } else { + this.lastActiveElement = focusable[0]; + } - this.lastActiveElement.focus(); - } - } - } + this.lastActiveElement.focus(); + } + } + } } diff --git a/src/ts/components/shared/directives/hasFeature.ts b/src/ts/components/shared/directives/hasFeature.ts index cce9388..062c835 100644 --- a/src/ts/components/shared/directives/hasFeature.ts +++ b/src/ts/components/shared/directives/hasFeature.ts @@ -4,57 +4,57 @@ import { hasFeatureFlag, featureFlagsChanged } from '../../../client/clientUtils import { Model } from '../../services/model'; @Directive({ - selector: '[hasFeature]', + selector: '[hasFeature]', }) export class HasFeature implements AfterViewInit, OnDestroy { - private subscriptions: Subscription[] = []; - private showing = false; - private _flag: string | undefined = undefined; - private _orMod = false; - private _alsoIf = true; - private ref?: EmbeddedViewRef; - constructor(private templateRef: TemplateRef, private viewContainer: ViewContainerRef, private model: Model) { - } - ngAfterViewInit() { - this.subscriptions.push(featureFlagsChanged.subscribe(() => this.update())); - this.subscriptions.push(this.model.accountChanged.subscribe(() => this.update())); - } - ngOnDestroy() { - this.subscriptions.forEach(s => s.unsubscribe()); - } - @Input() - set hasFeature(value: string | undefined) { - if (this._flag !== value) { - this._flag = value; - this.update(); - } - } - @Input() - set hasFeatureOrMod(value: boolean) { - if (this._orMod !== value) { - this._orMod = value; - this.update(); - } - } - @Input() - set hasFeatureAlso(value: boolean) { - if (this._alsoIf !== value) { - this._alsoIf = value; - this.update(); - } - } - private update() { - const show = this._alsoIf && (hasFeatureFlag(this._flag as any) || (this._orMod && this.model.isMod)); + private subscriptions: Subscription[] = []; + private showing = false; + private _flag: string | undefined = undefined; + private _orMod = false; + private _alsoIf = true; + private ref?: EmbeddedViewRef; + constructor(private templateRef: TemplateRef, private viewContainer: ViewContainerRef, private model: Model) { + } + ngAfterViewInit() { + this.subscriptions.push(featureFlagsChanged.subscribe(() => this.update())); + this.subscriptions.push(this.model.accountChanged.subscribe(() => this.update())); + } + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + @Input() + set hasFeature(value: string | undefined) { + if (this._flag !== value) { + this._flag = value; + this.update(); + } + } + @Input() + set hasFeatureOrMod(value: boolean) { + if (this._orMod !== value) { + this._orMod = value; + this.update(); + } + } + @Input() + set hasFeatureAlso(value: boolean) { + if (this._alsoIf !== value) { + this._alsoIf = value; + this.update(); + } + } + private update() { + const show = this._alsoIf && (hasFeatureFlag(this._flag as any) || (this._orMod && this.model.isMod)); - if (this.showing !== show) { - this.showing = show; + if (this.showing !== show) { + this.showing = show; - if (show) { - this.ref = this.ref || this.viewContainer.createEmbeddedView(this.templateRef); - } else { - this.viewContainer.clear(); - this.ref = undefined; - } - } - } + if (show) { + this.ref = this.ref || this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); + this.ref = undefined; + } + } + } } diff --git a/src/ts/components/shared/directives/labelledBy.ts b/src/ts/components/shared/directives/labelledBy.ts index e9a4a62..fec7631 100644 --- a/src/ts/components/shared/directives/labelledBy.ts +++ b/src/ts/components/shared/directives/labelledBy.ts @@ -3,19 +3,19 @@ import { uniqueId } from 'lodash'; import { findParentElement } from '../../../client/htmlUtils'; @Directive({ - selector: '[labelledBy]', + selector: '[labelledBy]', }) export class LabelledBy implements OnInit { - @Input('labelledBy') selector!: string; - constructor(private element: ElementRef) { - } - ngOnInit() { - const element = this.element.nativeElement as HTMLElement; - const target = findParentElement(element, this.selector); - const id = element.id = element.id || uniqueId('labelled-by-'); + @Input('labelledBy') selector!: string; + constructor(private element: ElementRef) { + } + ngOnInit() { + const element = this.element.nativeElement as HTMLElement; + const target = findParentElement(element, this.selector); + const id = element.id = element.id || uniqueId('labelled-by-'); - if (target) { - target.setAttribute('aria-labelledby', id); - } - } + if (target) { + target.setAttribute('aria-labelledby', id); + } + } } diff --git a/src/ts/components/shared/directives/linkCurrent.ts b/src/ts/components/shared/directives/linkCurrent.ts index 09bb882..eea7abe 100644 --- a/src/ts/components/shared/directives/linkCurrent.ts +++ b/src/ts/components/shared/directives/linkCurrent.ts @@ -2,13 +2,13 @@ import { Directive, HostBinding } from '@angular/core'; import { RouterLinkActive } from '@angular/router'; @Directive({ - selector: '[linkCurrent]', + selector: '[linkCurrent]', }) export class LinkCurrent { - constructor(private routerLinkActive: RouterLinkActive) { - } - @HostBinding('attr.aria-current') - get current() { - return this.routerLinkActive.isActive ? 'true' : undefined; - } + constructor(private routerLinkActive: RouterLinkActive) { + } + @HostBinding('attr.aria-current') + get current() { + return this.routerLinkActive.isActive ? 'true' : undefined; + } } diff --git a/src/ts/components/shared/directives/revSrc.ts b/src/ts/components/shared/directives/revSrc.ts index fb2e487..905f441 100644 --- a/src/ts/components/shared/directives/revSrc.ts +++ b/src/ts/components/shared/directives/revSrc.ts @@ -2,11 +2,11 @@ import { Directive, Input, HostBinding } from '@angular/core'; import { getUrl } from '../../../client/rev'; @Directive({ - selector: '[revSrc]', + selector: '[revSrc]', }) export class RevSrc { - @HostBinding() get src() { - return this.revSrc && getUrl(this.revSrc); - } - @Input() revSrc?: string; + @HostBinding() get src() { + return this.revSrc && getUrl(this.revSrc); + } + @Input() revSrc?: string; } diff --git a/src/ts/components/shared/directives/saveActiveTab.ts b/src/ts/components/shared/directives/saveActiveTab.ts index 11c6a72..b0376dc 100644 --- a/src/ts/components/shared/directives/saveActiveTab.ts +++ b/src/ts/components/shared/directives/saveActiveTab.ts @@ -4,23 +4,23 @@ import { Tabset } from '../tabset/tabset'; import { StorageService } from '../../services/storageService'; @Directive({ - selector: '[saveActiveTab]', + selector: '[saveActiveTab]', }) export class SaveActiveTab implements OnInit, OnDestroy { - @Input('saveActiveTab') key!: string; - private subscription?: Subscription; - constructor(@Host() private tabset: Tabset, private storage: StorageService) { - } - ngOnInit() { - // this.tabset.activeIndex = parseInt(this.storage.getItem(this.key) || '0', 10); - this.tabset.select(parseInt(this.storage.getItem(this.key) || '0', 10)); - this.subscription = this.tabset.activeIndexChange.subscribe((i: number) => { - this.storage.setItem(this.key, i.toString()); - }); - } - ngOnDestroy() { - if (this.subscription) { - this.subscription.unsubscribe(); - } - } + @Input('saveActiveTab') key!: string; + private subscription?: Subscription; + constructor(@Host() private tabset: Tabset, private storage: StorageService) { + } + ngOnInit() { + // this.tabset.activeIndex = parseInt(this.storage.getItem(this.key) || '0', 10); + this.tabset.select(parseInt(this.storage.getItem(this.key) || '0', 10)); + this.subscription = this.tabset.activeIndexChange.subscribe((i: number) => { + this.storage.setItem(this.key, i.toString()); + }); + } + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } } diff --git a/src/ts/components/shared/emote-box/emote-box.ts b/src/ts/components/shared/emote-box/emote-box.ts index cf9cbe9..5eee434 100644 --- a/src/ts/components/shared/emote-box/emote-box.ts +++ b/src/ts/components/shared/emote-box/emote-box.ts @@ -5,76 +5,76 @@ import { font } from '../../../client/fonts'; import { getCharacterSprite } from '../../../graphics/spriteFont'; @Component({ - selector: 'emote-box', - template: '', - styles: ['.emote-box { pointer-events: none; }'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'emote-box', + template: '', + styles: ['.emote-box { pointer-events: none; }'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class EmoteBox implements AfterViewInit { - @ViewChild('image', { static: true }) image!: ElementRef; - private emoteValue = ''; - private scaleValue = 2; - private initialized = false; - constructor(private zone: NgZone) { - } - ngAfterViewInit() { - loadAndInitSpriteSheets() - .then(() => { - this.initialized = true; - this.zone.runOutsideAngular(() => this.redraw()); - }); - } - get emote() { - return this.emoteValue; - } - @Input() - set emote(value: string) { - if (this.emoteValue !== value) { - this.emoteValue = value; - this.zone.runOutsideAngular(() => this.redraw()); - } - } - get scale() { - return this.scaleValue; - } - @Input() - set scale(value: number) { - if (this.scaleValue !== value) { - this.scaleValue = value; - this.zone.runOutsideAngular(() => this.redraw()); - } - } - redraw() { - if (this.initialized) { - const emote = findEmoji(this.emote); - const sprite = font && emote && getCharacterSprite(emote.symbol, font); - const image = this.image.nativeElement as HTMLImageElement; + @ViewChild('image', { static: true }) image!: ElementRef; + private emoteValue = ''; + private scaleValue = 2; + private initialized = false; + constructor(private zone: NgZone) { + } + ngAfterViewInit() { + loadAndInitSpriteSheets() + .then(() => { + this.initialized = true; + this.zone.runOutsideAngular(() => this.redraw()); + }); + } + get emote() { + return this.emoteValue; + } + @Input() + set emote(value: string) { + if (this.emoteValue !== value) { + this.emoteValue = value; + this.zone.runOutsideAngular(() => this.redraw()); + } + } + get scale() { + return this.scaleValue; + } + @Input() + set scale(value: number) { + if (this.scaleValue !== value) { + this.scaleValue = value; + this.zone.runOutsideAngular(() => this.redraw()); + } + } + redraw() { + if (this.initialized) { + const emote = findEmoji(this.emote); + const sprite = font && emote && getCharacterSprite(emote.symbol, font); + const image = this.image.nativeElement as HTMLImageElement; - if (sprite) { - const width = sprite.w + sprite.ox; - const height = 10; // sprite.h + sprite.oy; + if (sprite) { + const width = sprite.w + sprite.ox; + const height = 10; // sprite.h + sprite.oy; - image.style.width = `${width * this.scale}px`; - image.style.height = `${height * this.scale}px`; - image.style.marginTop = `${-this.scale}px`; - image.style.display = 'inline-block'; - image.style.visibility = 'hidden'; + image.style.width = `${width * this.scale}px`; + image.style.height = `${height * this.scale}px`; + image.style.marginTop = `${-this.scale}px`; + image.style.display = 'inline-block'; + image.style.visibility = 'hidden'; - if (emote) { - image.setAttribute('aria-label', emote.names[0]); - } + if (emote) { + image.setAttribute('aria-label', emote.names[0]); + } - getEmojiImageAsync(sprite, src => { - image.src = src; - image.alt = emote ? emote.symbol : ''; - image.style.visibility = 'visible'; - }); - } else { - image.style.width = `0px`; - image.style.height = `0px`; - image.src = ''; - image.alt = ''; - } - } - } + getEmojiImageAsync(sprite, src => { + image.src = src; + image.alt = emote ? emote.symbol : ''; + image.style.visibility = 'visible'; + }); + } else { + image.style.width = `0px`; + image.style.height = `0px`; + image.src = ''; + image.alt = ''; + } + } + } } diff --git a/src/ts/components/shared/fill-outline/fill-outline.ts b/src/ts/components/shared/fill-outline/fill-outline.ts index aad4190..341bcc3 100644 --- a/src/ts/components/shared/fill-outline/fill-outline.ts +++ b/src/ts/components/shared/fill-outline/fill-outline.ts @@ -2,46 +2,46 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; import { faLock } from '../../../client/icons'; @Component({ - selector: 'fill-outline', - templateUrl: 'fill-outline.pug', - styleUrls: ['fill-outline.scss'], + selector: 'fill-outline', + templateUrl: 'fill-outline.pug', + styleUrls: ['fill-outline.scss'], }) export class FillOutline { - readonly lockIcon = faLock; - @Input() label = 'Color'; - @Input() indicatorColor = ''; - @Input() base?: string; - @Input() fill?: string; - @Output() fillChange = new EventEmitter(); - @Input() outline?: string; - @Output() outlineChange = new EventEmitter(); - @Input() locked?: boolean; - @Output() lockedChange = new EventEmitter(); - @Input() nonLockable = false; - @Input() outlineLocked = false; - @Output() outlineLockedChange = new EventEmitter(); - @Input() outlineHidden = false; - @Output() change = new EventEmitter(); - get hasLock() { - return this.locked !== undefined; - } - onChange() { - this.change.emit(); - } - onFillChange(value: string) { - this.fillChange.emit(value); - this.onChange(); - } - onOutlineChange(value: string) { - this.outlineChange.emit(value); - this.onChange(); - } - onLockedChange(value: boolean) { - this.lockedChange.emit(value); - this.onChange(); - } - onOutlineLockedChange(value: boolean) { - this.outlineLockedChange.emit(value); - this.onChange(); - } + readonly lockIcon = faLock; + @Input() label = 'Color'; + @Input() indicatorColor = ''; + @Input() base?: string; + @Input() fill?: string; + @Output() fillChange = new EventEmitter(); + @Input() outline?: string; + @Output() outlineChange = new EventEmitter(); + @Input() locked?: boolean; + @Output() lockedChange = new EventEmitter(); + @Input() nonLockable = false; + @Input() outlineLocked = false; + @Output() outlineLockedChange = new EventEmitter(); + @Input() outlineHidden = false; + @Output() change = new EventEmitter(); + get hasLock() { + return this.locked !== undefined; + } + onChange() { + this.change.emit(); + } + onFillChange(value: string) { + this.fillChange.emit(value); + this.onChange(); + } + onOutlineChange(value: string) { + this.outlineChange.emit(value); + this.onChange(); + } + onLockedChange(value: boolean) { + this.lockedChange.emit(value); + this.onChange(); + } + onOutlineLockedChange(value: boolean) { + this.outlineLockedChange.emit(value); + this.onChange(); + } } diff --git a/src/ts/components/shared/friends-box/friends-box.ts b/src/ts/components/shared/friends-box/friends-box.ts index bbfa640..9693421 100644 --- a/src/ts/components/shared/friends-box/friends-box.ts +++ b/src/ts/components/shared/friends-box/friends-box.ts @@ -7,55 +7,55 @@ import { removeItem } from '../../../common/utils'; import { SettingsService } from '../../services/settingsService'; @Component({ - selector: 'friends-box', - templateUrl: 'friends-box.pug', - styleUrls: ['friends-box.scss'], + selector: 'friends-box', + templateUrl: 'friends-box.pug', + styleUrls: ['friends-box.scss'], }) export class FriendsBox { - readonly friendsIcon = faUserFriends; - readonly cogIcon = faCog; - readonly addToPartyIcon = faUserPlus; - readonly userOptionsIcon = faUserCog; - readonly statusIcon = faCircle; - @Output() sendMessage = new EventEmitter(); - removing?: Friend; - constructor(private settings: SettingsService, private model: Model, private game: PonyTownGame) { - } - get friends() { - return this.model.friends; - } - get hidden() { - return !!this.settings.account.hidden; - } - toggleHidden() { - this.settings.account.hidden = !this.settings.account.hidden; - this.settings.saveAccountSettings(this.settings.account); - } - toggle() { - this.removing = undefined; - } - sendMessageTo(friend: Friend) { - this.sendMessage.emit(friend); - } - inviteToParty(friend: Friend) { - this.game.send(server => server.playerAction(friend.entityId, PlayerAction.InviteToParty, undefined)); - } - remove(friend: Friend) { - this.removing = friend; - } - cancelRemove() { - this.removing = undefined; - } - confirmRemove() { - if (this.removing && this.model.friends) { - const { accountId } = this.removing; - this.game.send(server => server.actionParam(Action.RemoveFriend, accountId)); - removeItem(this.model.friends, this.removing); - this.removing = undefined; - } - } - setStatus(status: string) { - this.settings.account.hidden = status === 'invisible'; - this.settings.saveAccountSettings(this.settings.account); - } + readonly friendsIcon = faUserFriends; + readonly cogIcon = faCog; + readonly addToPartyIcon = faUserPlus; + readonly userOptionsIcon = faUserCog; + readonly statusIcon = faCircle; + @Output() sendMessage = new EventEmitter(); + removing?: Friend; + constructor(private settings: SettingsService, private model: Model, private game: PonyTownGame) { + } + get friends() { + return this.model.friends; + } + get hidden() { + return !!this.settings.account.hidden; + } + toggleHidden() { + this.settings.account.hidden = !this.settings.account.hidden; + this.settings.saveAccountSettings(this.settings.account); + } + toggle() { + this.removing = undefined; + } + sendMessageTo(friend: Friend) { + this.sendMessage.emit(friend); + } + inviteToParty(friend: Friend) { + this.game.send(server => server.playerAction(friend.entityId, PlayerAction.InviteToParty, undefined)); + } + remove(friend: Friend) { + this.removing = friend; + } + cancelRemove() { + this.removing = undefined; + } + confirmRemove() { + if (this.removing && this.model.friends) { + const { accountId } = this.removing; + this.game.send(server => server.actionParam(Action.RemoveFriend, accountId)); + removeItem(this.model.friends, this.removing); + this.removing = undefined; + } + } + setStatus(status: string) { + this.settings.account.hidden = status === 'invisible'; + this.settings.saveAccountSettings(this.settings.account); + } } diff --git a/src/ts/components/shared/install-button/install-button.ts b/src/ts/components/shared/install-button/install-button.ts index 03f1340..2f569a7 100644 --- a/src/ts/components/shared/install-button/install-button.ts +++ b/src/ts/components/shared/install-button/install-button.ts @@ -4,24 +4,24 @@ import { InstallService } from '../../services/installService'; import { isMobile } from '../../../client/data'; @Component({ - selector: 'install-button', - templateUrl: 'install-button.pug', - styleUrls: ['install-button.scss'], + selector: 'install-button', + templateUrl: 'install-button.pug', + styleUrls: ['install-button.scss'], }) export class InstallButton { - readonly closeIcon = faTimes; - constructor(private installService: InstallService) { - } - get canInstall() { - return this.installService.canInstall; - } - get isMobile() { - return isMobile; - } - install() { - this.installService.install(); - } - dismiss() { - this.installService.dismiss(); - } + readonly closeIcon = faTimes; + constructor(private installService: InstallService) { + } + get canInstall() { + return this.installService.canInstall; + } + get isMobile() { + return isMobile; + } + install() { + this.installService.install(); + } + dismiss() { + this.installService.dismiss(); + } } diff --git a/src/ts/components/shared/invites-modal/invites-modal.ts b/src/ts/components/shared/invites-modal/invites-modal.ts index 0ba5236..5330c70 100644 --- a/src/ts/components/shared/invites-modal/invites-modal.ts +++ b/src/ts/components/shared/invites-modal/invites-modal.ts @@ -7,26 +7,26 @@ import { removeItem } from '../../../common/utils'; import { Model } from '../../services/model'; @Component({ - selector: 'invites-modal', - templateUrl: 'invites-modal.pug', + selector: 'invites-modal', + templateUrl: 'invites-modal.pug', }) export class InvitesModal implements OnInit { - @Output() close = new EventEmitter(); - invites: (SupporterInvite & { pony: PalettePonyInfo; })[] = []; - error?: string; - constructor(private model: Model, private game: PonyTownGame) { - } - get inviteLimit() { - return this.model.supporterInviteLimit; - } - ngOnInit() { - this.game.send(server => server.getInvites())! - .then(invites => invites.map(i => ({ ...i, pony: toPalette(decompressPonyString(i.info)) }))) - .then(invites => this.invites = invites); - } - remove(invite: SupporterInvite) { - this.error = undefined; - this.game.send(server => server.actionParam(Action.CancelSupporterInvite, invite.id)); - removeItem(this.invites, invite); - } + @Output() close = new EventEmitter(); + invites: (SupporterInvite & { pony: PalettePonyInfo; })[] = []; + error?: string; + constructor(private model: Model, private game: PonyTownGame) { + } + get inviteLimit() { + return this.model.supporterInviteLimit; + } + ngOnInit() { + this.game.send(server => server.getInvites())! + .then(invites => invites.map(i => ({ ...i, pony: toPalette(decompressPonyString(i.info)) }))) + .then(invites => this.invites = invites); + } + remove(invite: SupporterInvite) { + this.error = undefined; + this.game.send(server => server.actionParam(Action.CancelSupporterInvite, invite.id)); + removeItem(this.invites, invite); + } } diff --git a/src/ts/components/shared/kbd-key/kbd-key.ts b/src/ts/components/shared/kbd-key/kbd-key.ts index 773ac0f..c9a5577 100644 --- a/src/ts/components/shared/kbd-key/kbd-key.ts +++ b/src/ts/components/shared/kbd-key/kbd-key.ts @@ -1,9 +1,9 @@ import { Component, Input } from '@angular/core'; @Component({ - selector: 'kbd-key', - templateUrl: 'kbd-key.pug', + selector: 'kbd-key', + templateUrl: 'kbd-key.pug', }) export class KbdKey { - @Input() title?: string; + @Input() title?: string; } diff --git a/src/ts/components/shared/menu-bar/menu-bar.ts b/src/ts/components/shared/menu-bar/menu-bar.ts index d7d978a..b0dde72 100644 --- a/src/ts/components/shared/menu-bar/menu-bar.ts +++ b/src/ts/components/shared/menu-bar/menu-bar.ts @@ -9,53 +9,53 @@ import { SettingsService } from '../../services/settingsService'; import { REQUEST_DATE_OF_BIRTH } from '../../../common/constants'; @Component({ - selector: 'menu-bar', - templateUrl: 'menu-bar.pug', - styleUrls: ['menu-bar.scss'], + selector: 'menu-bar', + templateUrl: 'menu-bar.pug', + styleUrls: ['menu-bar.scss'], }) export class MenuBar { - readonly signUpProviders = signUpProviders; - readonly signInProviders = signInProviders; - readonly starIcon = faStar; - readonly spinnerIcon = faSpinner; - readonly userIcon = faUser; - readonly alertIcon = faExclamationCircle; - readonly cogIcon = faCog; - readonly statusIcon = faCircle; - @Input() logo = false; - @Input() loading = false; - @Input() loadingError = false; - @Input() account?: AccountData; - @Output() signOut = new EventEmitter(); - @Output() signIn = new EventEmitter(); - constructor(private model: Model, private settings: SettingsService) { - } - get hasSupporterIcon() { - return isSupporterOrPastSupporter(this.account); - } - get supporterTitle() { - return supporterTitle(this.account); - } - get supporterClass() { - return supporterClass(this.account); - } - get showAccountAlert() { - return this.model.missingBirthdate && REQUEST_DATE_OF_BIRTH; - } - get hidden() { - return !!this.settings.account.hidden; - } - icon(id: string) { - return getProviderIcon(id); - } - signInTo(provider: OAuthProvider) { - this.signIn.emit(provider); - } - @HostListener('window:resize') - resize() { - } - setStatus(status: string) { - this.settings.account.hidden = status === 'invisible'; - this.settings.saveAccountSettings(this.settings.account); - } + readonly signUpProviders = signUpProviders; + readonly signInProviders = signInProviders; + readonly starIcon = faStar; + readonly spinnerIcon = faSpinner; + readonly userIcon = faUser; + readonly alertIcon = faExclamationCircle; + readonly cogIcon = faCog; + readonly statusIcon = faCircle; + @Input() logo = false; + @Input() loading = false; + @Input() loadingError = false; + @Input() account?: AccountData; + @Output() signOut = new EventEmitter(); + @Output() signIn = new EventEmitter(); + constructor(private model: Model, private settings: SettingsService) { + } + get hasSupporterIcon() { + return isSupporterOrPastSupporter(this.account); + } + get supporterTitle() { + return supporterTitle(this.account); + } + get supporterClass() { + return supporterClass(this.account); + } + get showAccountAlert() { + return this.model.missingBirthdate && REQUEST_DATE_OF_BIRTH; + } + get hidden() { + return !!this.settings.account.hidden; + } + icon(id: string) { + return getProviderIcon(id); + } + signInTo(provider: OAuthProvider) { + this.signIn.emit(provider); + } + @HostListener('window:resize') + resize() { + } + setStatus(status: string) { + this.settings.account.hidden = status === 'invisible'; + this.settings.saveAccountSettings(this.settings.account); + } } diff --git a/src/ts/components/shared/menu-item/menu-item.ts b/src/ts/components/shared/menu-item/menu-item.ts index b422246..f59b7e6 100644 --- a/src/ts/components/shared/menu-item/menu-item.ts +++ b/src/ts/components/shared/menu-item/menu-item.ts @@ -2,12 +2,12 @@ import { Component, Input } from '@angular/core'; import { emptyIcon } from '../../../client/icons'; @Component({ - selector: 'menu-item', - templateUrl: 'menu-item.pug', - styleUrls: ['menu-item.scss'], + selector: 'menu-item', + templateUrl: 'menu-item.pug', + styleUrls: ['menu-item.scss'], }) export class MenuItem { - @Input() route: any; - @Input() name?: string; - @Input() icon = emptyIcon; + @Input() route: any; + @Input() name?: string; + @Input() icon = emptyIcon; } diff --git a/src/ts/components/shared/mod-box/mod-box.ts b/src/ts/components/shared/mod-box/mod-box.ts index 9308213..8a52665 100644 --- a/src/ts/components/shared/mod-box/mod-box.ts +++ b/src/ts/components/shared/mod-box/mod-box.ts @@ -9,89 +9,89 @@ const ageLabels = ['', 'M', 'A', '', '', '[M]', '[A]']; const ageTitles = ['Not set', 'Minor', 'Adult', '', '', 'Minor (locked)', 'Adult (locked)']; @Component({ - selector: 'mod-box', - templateUrl: 'mod-box.pug', - styleUrls: ['mod-box.scss'], + selector: 'mod-box', + templateUrl: 'mod-box.pug', + styleUrls: ['mod-box.scss'], }) export class ModBox implements OnDestroy { - readonly flagIcon = faFlag; - readonly noteIcon = faStickyNote; - readonly muteIcon = faMicrophoneSlash; - readonly hideIcon = faEyeSlash; - readonly moreIcon = faUserCog; - readonly dangerIcon = faExclamationCircle; - readonly timeouts = TIMEOUTS; - @Input() pony!: Pony; - isNoteOpen = false; - constructor(private model: Model, private game: PonyTownGame) { - } - get ageLabel() { - return ageLabels[this.modInfo && this.modInfo.age || 0]; - } - get ageTitle() { - return ageTitles[this.modInfo && this.modInfo.age || 0]; - } - get modInfo() { - return this.pony.modInfo; - } - get account() { - return this.modInfo && this.modInfo.account; - } - get country() { - return this.modInfo && this.modInfo.country; - } - get mute() { - return this.modInfo && this.modInfo.mute; - } - get muteTooltip() { - return this.mute ? (this.mute === 'perma' ? 'Permanently Muted' : `Muted for ${this.mute}`) : 'Mute'; - } - get shadow() { - return this.modInfo && this.modInfo.shadow; - } - get shadowTooltip() { - return this.shadow ? (this.shadow === 'perma' ? 'Permanently Shadowed' : `Shadowed for ${this.shadow}`) : 'Shadow'; - } - get counters() { - return this.modInfo && this.modInfo.counters; - } - get hasCounters() { - const counters = this.counters; - return counters && (counters.spam || counters.swears || counters.timeouts); - } - get check() { - return this.model.modCheck; - } - get note() { - return this.modInfo && this.modInfo.note; - } - set note(value) { - if (this.modInfo) { - this.modInfo.note = value; - } - } - ngOnDestroy() { - if (this.isNoteOpen) { - this.blur(); - } - } - className(value: string) { - return value ? (value === 'perma' ? 'btn-danger' : 'btn-warning') : 'btn-default'; - } - report() { - this.modAction(ModAction.Report); - } - setMute(value: number) { - this.modAction(ModAction.Mute, value); - } - setShadow(value: number) { - this.modAction(ModAction.Shadow, value); - } - blur() { - this.game.send(server => server.setNote(this.pony.id, this.modInfo && this.modInfo.note || '')); - this.isNoteOpen = false; - } - modAction(type: ModAction, param = 0) { - return this.game.send(server => server.otherAction(this.pony.id, type, param)); - } + readonly flagIcon = faFlag; + readonly noteIcon = faStickyNote; + readonly muteIcon = faMicrophoneSlash; + readonly hideIcon = faEyeSlash; + readonly moreIcon = faUserCog; + readonly dangerIcon = faExclamationCircle; + readonly timeouts = TIMEOUTS; + @Input() pony!: Pony; + isNoteOpen = false; + constructor(private model: Model, private game: PonyTownGame) { + } + get ageLabel() { + return ageLabels[this.modInfo && this.modInfo.age || 0]; + } + get ageTitle() { + return ageTitles[this.modInfo && this.modInfo.age || 0]; + } + get modInfo() { + return this.pony.modInfo; + } + get account() { + return this.modInfo && this.modInfo.account; + } + get country() { + return this.modInfo && this.modInfo.country; + } + get mute() { + return this.modInfo && this.modInfo.mute; + } + get muteTooltip() { + return this.mute ? (this.mute === 'perma' ? 'Permanently Muted' : `Muted for ${this.mute}`) : 'Mute'; + } + get shadow() { + return this.modInfo && this.modInfo.shadow; + } + get shadowTooltip() { + return this.shadow ? (this.shadow === 'perma' ? 'Permanently Shadowed' : `Shadowed for ${this.shadow}`) : 'Shadow'; + } + get counters() { + return this.modInfo && this.modInfo.counters; + } + get hasCounters() { + const counters = this.counters; + return counters && (counters.spam || counters.swears || counters.timeouts); + } + get check() { + return this.model.modCheck; + } + get note() { + return this.modInfo && this.modInfo.note; + } + set note(value) { + if (this.modInfo) { + this.modInfo.note = value; + } + } + ngOnDestroy() { + if (this.isNoteOpen) { + this.blur(); + } + } + className(value: string) { + return value ? (value === 'perma' ? 'btn-danger' : 'btn-warning') : 'btn-default'; + } + report() { + this.modAction(ModAction.Report); + } + setMute(value: number) { + this.modAction(ModAction.Mute, value); + } + setShadow(value: number) { + this.modAction(ModAction.Shadow, value); + } + blur() { + this.game.send(server => server.setNote(this.pony.id, this.modInfo && this.modInfo.note || '')); + this.isNoteOpen = false; + } + modAction(type: ModAction, param = 0) { + return this.game.send(server => server.otherAction(this.pony.id, type, param)); + } } diff --git a/src/ts/components/shared/notification/notification-item.ts b/src/ts/components/shared/notification/notification-item.ts index af9cf95..3ecee89 100644 --- a/src/ts/components/shared/notification/notification-item.ts +++ b/src/ts/components/shared/notification/notification-item.ts @@ -6,63 +6,63 @@ import { faBan } from '../../../client/icons'; import { getPaletteInfo } from '../../../common/pony'; @Component({ - selector: 'notification-item', - templateUrl: 'notification-item.pug', - styleUrls: ['notification-item.scss'], + selector: 'notification-item', + templateUrl: 'notification-item.pug', + styleUrls: ['notification-item.scss'], }) export class NotificationItem implements OnDestroy { - readonly banIcon = faBan; - @Input() notification!: Notification; - constructor(private game: PonyTownGame) { - } - get isOpen() { - return this.notification.open; - } - set isOpen(value: boolean) { - if (value) { - this.game.notifications.forEach(n => n.open = false); - } + readonly banIcon = faBan; + @Input() notification!: Notification; + constructor(private game: PonyTownGame) { + } + get isOpen() { + return this.notification.open; + } + set isOpen(value: boolean) { + if (value) { + this.game.notifications.forEach(n => n.open = false); + } - this.notification.open = value; - } - get okButton() { - return hasFlag(this.notification.flags, NotificationFlags.Ok); - } - get yesButton() { - return hasFlag(this.notification.flags, NotificationFlags.Yes); - } - get acceptButton() { - return hasFlag(this.notification.flags, NotificationFlags.Accept); - } - get noButton() { - return hasFlag(this.notification.flags, NotificationFlags.No); - } - get rejectButton() { - return hasFlag(this.notification.flags, NotificationFlags.Reject); - } - get ignoreButton() { - return hasFlag(this.notification.flags, NotificationFlags.Ignore); - } - get paletteInfo() { - return getPaletteInfo(this.notification.pony); - } - ngOnDestroy() { - this.isOpen = false; - } - accept() { - this.game.send(server => server.acceptNotification(this.notification.id)); - } - reject() { - this.game.send(server => server.rejectNotification(this.notification.id)); - } - ignore() { - this.reject(); + this.notification.open = value; + } + get okButton() { + return hasFlag(this.notification.flags, NotificationFlags.Ok); + } + get yesButton() { + return hasFlag(this.notification.flags, NotificationFlags.Yes); + } + get acceptButton() { + return hasFlag(this.notification.flags, NotificationFlags.Accept); + } + get noButton() { + return hasFlag(this.notification.flags, NotificationFlags.No); + } + get rejectButton() { + return hasFlag(this.notification.flags, NotificationFlags.Reject); + } + get ignoreButton() { + return hasFlag(this.notification.flags, NotificationFlags.Ignore); + } + get paletteInfo() { + return getPaletteInfo(this.notification.pony); + } + ngOnDestroy() { + this.isOpen = false; + } + accept() { + this.game.send(server => server.acceptNotification(this.notification.id)); + } + reject() { + this.game.send(server => server.rejectNotification(this.notification.id)); + } + ignore() { + this.reject(); - const pony = this.notification.pony; + const pony = this.notification.pony; - if (pony !== this.game.player) { - this.game.send(server => server.playerAction(pony.id, PlayerAction.Ignore, undefined)); - pony.playerState = setFlag(pony.playerState, EntityPlayerState.Ignored, true); - } - } + if (pony !== this.game.player) { + this.game.send(server => server.playerAction(pony.id, PlayerAction.Ignore, undefined)); + pony.playerState = setFlag(pony.playerState, EntityPlayerState.Ignored, true); + } + } } diff --git a/src/ts/components/shared/notification/notification-list.ts b/src/ts/components/shared/notification/notification-list.ts index b43d3b1..35ff640 100644 --- a/src/ts/components/shared/notification/notification-list.ts +++ b/src/ts/components/shared/notification/notification-list.ts @@ -5,29 +5,29 @@ import { faEllipsisV } from '../../../client/icons'; const LIMIT = 8; @Component({ - selector: 'notification-list', - templateUrl: 'notification-list.pug', - styleUrls: ['notification-list.scss'], + selector: 'notification-list', + templateUrl: 'notification-list.pug', + styleUrls: ['notification-list.scss'], }) export class NotificationList { - readonly ellipsisIcon = faEllipsisV; - @Input() notifications!: Notification[]; - @Input() set notificationsLength(value: number) { - while (this.start > value) { - this.prev(); - } - } - start = 0; - get limit() { - return this.start + LIMIT; - } - get hasMore() { - return this.notifications.length > (this.start + this.limit); - } - next() { - this.start += this.limit; - } - prev() { - this.start -= this.start <= LIMIT ? LIMIT : LIMIT - 1; - } + readonly ellipsisIcon = faEllipsisV; + @Input() notifications!: Notification[]; + @Input() set notificationsLength(value: number) { + while (this.start > value) { + this.prev(); + } + } + start = 0; + get limit() { + return this.start + LIMIT; + } + get hasMore() { + return this.notifications.length > (this.start + this.limit); + } + next() { + this.start += this.limit; + } + prev() { + this.start -= this.start <= LIMIT ? LIMIT : LIMIT - 1; + } } diff --git a/src/ts/components/shared/page-loader/page-loader.ts b/src/ts/components/shared/page-loader/page-loader.ts index 52c2330..22808f2 100644 --- a/src/ts/components/shared/page-loader/page-loader.ts +++ b/src/ts/components/shared/page-loader/page-loader.ts @@ -4,27 +4,27 @@ import { Model } from '../../services/model'; import { hardReload } from '../../../client/clientUtils'; @Component({ - selector: 'page-loader', - templateUrl: 'page-loader.pug', - styleUrls: ['page-loader.scss'], + selector: 'page-loader', + templateUrl: 'page-loader.pug', + styleUrls: ['page-loader.scss'], }) export class PageLoader { - readonly spinnerIcon = faSpinner; - constructor(private model: Model) { - } - get loading() { - return this.model.loading; - } - get updating() { - return this.model.updating; - } - get updatingTakesLongTime() { - return this.model.updatingTakesLongTime; - } - get loadingError() { - return this.model.loadingError; - } - reload() { - hardReload(); - } + readonly spinnerIcon = faSpinner; + constructor(private model: Model) { + } + get loading() { + return this.model.loading; + } + get updating() { + return this.model.updating; + } + get updatingTakesLongTime() { + return this.model.updatingTakesLongTime; + } + get loadingError() { + return this.model.loadingError; + } + reload() { + hardReload(); + } } diff --git a/src/ts/components/shared/party-box/party-box.ts b/src/ts/components/shared/party-box/party-box.ts index eb73ea6..1d304d5 100644 --- a/src/ts/components/shared/party-box/party-box.ts +++ b/src/ts/components/shared/party-box/party-box.ts @@ -5,20 +5,20 @@ import { partyLeaderIcon, offlineIcon } from '../../../client/icons'; import { getPaletteInfo } from '../../../common/pony'; @Component({ - selector: 'party-box', - templateUrl: 'party-box.pug', - styleUrls: ['party-box.scss'], + selector: 'party-box', + templateUrl: 'party-box.pug', + styleUrls: ['party-box.scss'], }) export class PartyBox { - readonly leaderIcon = partyLeaderIcon; - readonly offlineIcon = offlineIcon; - @Input() member!: PartyMember; - constructor(private game: PonyTownGame) { - } - get paletteInfo() { - return this.member.pony && getPaletteInfo(this.member.pony); - } - click() { - this.game.select(this.member.pony); - } + readonly leaderIcon = partyLeaderIcon; + readonly offlineIcon = offlineIcon; + @Input() member!: PartyMember; + constructor(private game: PonyTownGame) { + } + get paletteInfo() { + return this.member.pony && getPaletteInfo(this.member.pony); + } + click() { + this.game.select(this.member.pony); + } } diff --git a/src/ts/components/shared/party-list/party-list.ts b/src/ts/components/shared/party-list/party-list.ts index e2b7078..41a4d84 100644 --- a/src/ts/components/shared/party-list/party-list.ts +++ b/src/ts/components/shared/party-list/party-list.ts @@ -8,86 +8,86 @@ import { clamp } from '../../../common/utils'; import { isPartyLeader } from '../../../client/partyUtils'; function visibleMembers(members: PartyMember[], max: number, start: number) { - return members.length > max ? Math.max(max - (start > 0 ? 2 : 1), 1) : max; + return members.length > max ? Math.max(max - (start > 0 ? 2 : 1), 1) : max; } @Component({ - selector: 'party-list', - templateUrl: 'party-list.pug', - styleUrls: ['party-list.scss'], + selector: 'party-list', + templateUrl: 'party-list.pug', + styleUrls: ['party-list.scss'], }) export class PartyList implements OnInit, OnDestroy { - readonly ellipsisIcon = faEllipsisV; - readonly leaderIcon = partyLeaderIcon; - readonly cogIcon = faCog; - hidden = false; - start = 0; - maxMembers = PARTY_LIMIT - 1; - members: PartyMember[] = []; - private subscription?: Subscription; - constructor(private game: PonyTownGame) { - } - get hasParty() { - return this.game.party !== undefined; - } - get isLeader() { - return isPartyLeader(this.game); - } - get hasMore() { - return this.members.length > (this.start + this.visible); - } - get visible() { - return visibleMembers(this.members, this.maxMembers, this.start); - } - get limit() { - return this.start + this.visible; - } - ngOnInit() { - this.subscription = this.game.onPartyUpdate.subscribe(() => this.update()); - this.resized(); - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } - isMe(member: PartyMember) { - return this.game.player && this.game.player.id === member.id; - } - leave() { - this.game.send(server => server.leaveParty()); - } - update() { - if (this.hasParty) { - this.members = this.game.party ? this.game.party.members.filter(m => !m.self) : []; + readonly ellipsisIcon = faEllipsisV; + readonly leaderIcon = partyLeaderIcon; + readonly cogIcon = faCog; + hidden = false; + start = 0; + maxMembers = PARTY_LIMIT - 1; + members: PartyMember[] = []; + private subscription?: Subscription; + constructor(private game: PonyTownGame) { + } + get hasParty() { + return this.game.party !== undefined; + } + get isLeader() { + return isPartyLeader(this.game); + } + get hasMore() { + return this.members.length > (this.start + this.visible); + } + get visible() { + return visibleMembers(this.members, this.maxMembers, this.start); + } + get limit() { + return this.start + this.visible; + } + ngOnInit() { + this.subscription = this.game.onPartyUpdate.subscribe(() => this.update()); + this.resized(); + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + isMe(member: PartyMember) { + return this.game.player && this.game.player.id === member.id; + } + leave() { + this.game.send(server => server.leaveParty()); + } + update() { + if (this.hasParty) { + this.members = this.game.party ? this.game.party.members.filter(m => !m.self) : []; - while (this.start > 0 && this.members.length <= this.start) { - this.start = 0; - } - } else { - this.members = []; - this.start = 0; - } - } - @HostListener('window:resize') - resized() { - const padding = 140 + 110; - const max = clamp(Math.floor((window.innerHeight - padding) / 43), 0, PARTY_LIMIT - 1); + while (this.start > 0 && this.members.length <= this.start) { + this.start = 0; + } + } else { + this.members = []; + this.start = 0; + } + } + @HostListener('window:resize') + resized() { + const padding = 140 + 110; + const max = clamp(Math.floor((window.innerHeight - padding) / 43), 0, PARTY_LIMIT - 1); - if (this.maxMembers !== max) { - this.start = 0; - this.maxMembers = max; - } - } - next() { - this.start += this.visible; - } - prev() { - const max = this.members.length - 1; - let start = 0; + if (this.maxMembers !== max) { + this.start = 0; + this.maxMembers = max; + } + } + next() { + this.start += this.visible; + } + prev() { + const max = this.members.length - 1; + let start = 0; - while (start < max && (start + visibleMembers(this.members, this.maxMembers, start)) !== this.start) { - start++; - } + while (start < max && (start + visibleMembers(this.members, this.maxMembers, start)) !== this.start) { + start++; + } - this.start = start; - } + this.start = start; + } } diff --git a/src/ts/components/shared/pipes/siteName.ts b/src/ts/components/shared/pipes/siteName.ts index 8566dd1..2fae2b1 100644 --- a/src/ts/components/shared/pipes/siteName.ts +++ b/src/ts/components/shared/pipes/siteName.ts @@ -1,11 +1,11 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ - name: 'siteName', + name: 'siteName', }) export class SiteNamePipe implements PipeTransform { - transform(value: string | undefined) { - const match = String(value || '').match(/(\w+)\.com/); - return match && match[1]; - } + transform(value: string | undefined) { + const match = String(value || '').match(/(\w+)\.com/); + return match && match[1]; + } } diff --git a/src/ts/components/shared/play-box/play-box.ts b/src/ts/components/shared/play-box/play-box.ts index 87796a3..dbdc689 100644 --- a/src/ts/components/shared/play-box/play-box.ts +++ b/src/ts/components/shared/play-box/play-box.ts @@ -2,8 +2,8 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { ServerInfo, AccountDataFlags } from '../../../common/interfaces'; import { RequestError, delay, includes, hasFlag } from '../../../common/utils'; import { - WEBGL_CREATION_ERROR, ACCESS_ERROR, ACCOUNT_ERROR, BROWSER_NOT_SUPPORTED_ERROR, NAME_ERROR, VERSION_ERROR, - OFFLINE_ERROR, PROTECTION_ERROR, NOT_AUTHENTICATED_ERROR, CHARACTER_LIMIT_ERROR + WEBGL_CREATION_ERROR, ACCESS_ERROR, ACCOUNT_ERROR, BROWSER_NOT_SUPPORTED_ERROR, NAME_ERROR, VERSION_ERROR, + OFFLINE_ERROR, PROTECTION_ERROR, NOT_AUTHENTICATED_ERROR, CHARACTER_LIMIT_ERROR } from '../../../common/errors'; import { version } from '../../../client/data'; import { GameService } from '../../services/gameService'; @@ -16,168 +16,168 @@ import { ErrorReporter } from '../../services/errorReporter'; import { REQUEST_DATE_OF_BIRTH } from '../../../common/constants'; const ignoredErrors = [ - WEBGL_CREATION_ERROR, - BROWSER_NOT_SUPPORTED_ERROR, - NAME_ERROR, - OFFLINE_ERROR, - VERSION_ERROR, - ACCESS_ERROR, - PROTECTION_ERROR, - NOT_AUTHENTICATED_ERROR, - CHARACTER_LIMIT_ERROR, - 'Saving in progress', + WEBGL_CREATION_ERROR, + BROWSER_NOT_SUPPORTED_ERROR, + NAME_ERROR, + OFFLINE_ERROR, + VERSION_ERROR, + ACCESS_ERROR, + PROTECTION_ERROR, + NOT_AUTHENTICATED_ERROR, + CHARACTER_LIMIT_ERROR, + 'Saving in progress', ]; @Component({ - selector: 'play-box', - templateUrl: 'play-box.pug', - styleUrls: ['play-box.scss'], + selector: 'play-box', + templateUrl: 'play-box.pug', + styleUrls: ['play-box.scss'], }) export class PlayBox implements OnInit { - readonly spinnerIcon = faSpinner; - readonly warningIcon = faExclamationCircle; - readonly infoIcon = faInfoCircle; - readonly requestBirthdate = REQUEST_DATE_OF_BIRTH; - @Output() errorChange = new EventEmitter(); - @Input() label?: string; - joining = false; - failedToLoadImages = false; - birthdate = ''; - birthdateSet = false; - private locked = false; - constructor( - public gameService: GameService, - public model: Model, - private storage: StorageService, - private errorReporter: ErrorReporter, - ) { - } - @Input() - get error() { - return this.gameService.error; - } - set error(value: string | undefined) { - if (this.gameService) { - this.gameService.error = value; - this.errorChange.emit(value); - } - } - get server() { - return this.gameService.server; - } - set server(value: ServerInfo | undefined) { - this.gameService.server = value; - } - get servers() { - return this.gameService.servers; - } - get offline(): boolean { - return this.gameService.offline; - } - get updateWarning() { - return this.gameService.updateWarning; - } - get invalidVersion(): boolean { - return !!(this.gameService.versionError || this.error === VERSION_ERROR - || (this.gameService.version && this.gameService.version !== version)); - } - get protectionError(): boolean { - return this.gameService.protectionError || this.error === PROTECTION_ERROR; - } - get canPlay(): boolean { - return !!this.server && this.gameService.canPlay && !this.locked && !this.invalidVersion && !this.failedToLoadImages; - } - get isAccessError(): boolean { - return this.error === ACCESS_ERROR || this.error === ACCOUNT_ERROR; - } - get isWebGLError(): boolean { - return this.error === WEBGL_CREATION_ERROR; - } - get isBrowserError(): boolean { - return this.error === BROWSER_NOT_SUPPORTED_ERROR; - } - get isOtherError(): boolean { - return !!this.error && !this.invalidVersion && !this.isAccessError && !this.isWebGLError && !this.isBrowserError; - } - get ponyLimit() { - return this.model.characterLimit; - } - get hasTooManyPonies(): boolean { - return this.model.ponies.length > this.ponyLimit; - } - get isMarkedForMultiples(): boolean { - const account = this.model.account; - return !!account && hasFlag(account.flags, AccountDataFlags.Duplicates); - } - get isAndroidBrowser() { - return isAndroidBrowser; - } - get isBrowserOutdated() { - return !isAndroidBrowser && isBrowserOutdated && !this.storage.getBoolean('dismiss-outdated-browser'); - } - get leftMessage() { - return this.gameService.leftMessage; - } - get accountAlert() { - return this.model.accountAlert; - } - ngOnInit() { - loadAndInitSpriteSheets() - .then(loaded => this.failedToLoadImages = !loaded); - } - play() { - if (this.canPlay) { - this.joining = true; - this.locked = true; - this.error = undefined; + readonly spinnerIcon = faSpinner; + readonly warningIcon = faExclamationCircle; + readonly infoIcon = faInfoCircle; + readonly requestBirthdate = REQUEST_DATE_OF_BIRTH; + @Output() errorChange = new EventEmitter(); + @Input() label?: string; + joining = false; + failedToLoadImages = false; + birthdate = ''; + birthdateSet = false; + private locked = false; + constructor( + public gameService: GameService, + public model: Model, + private storage: StorageService, + private errorReporter: ErrorReporter, + ) { + } + @Input() + get error() { + return this.gameService.error; + } + set error(value: string | undefined) { + if (this.gameService) { + this.gameService.error = value; + this.errorChange.emit(value); + } + } + get server() { + return this.gameService.server; + } + set server(value: ServerInfo | undefined) { + this.gameService.server = value; + } + get servers() { + return this.gameService.servers; + } + get offline(): boolean { + return this.gameService.offline; + } + get updateWarning() { + return this.gameService.updateWarning; + } + get invalidVersion(): boolean { + return !!(this.gameService.versionError || this.error === VERSION_ERROR + || (this.gameService.version && this.gameService.version !== version)); + } + get protectionError(): boolean { + return this.gameService.protectionError || this.error === PROTECTION_ERROR; + } + get canPlay(): boolean { + return !!this.server && this.gameService.canPlay && !this.locked && !this.invalidVersion && !this.failedToLoadImages; + } + get isAccessError(): boolean { + return this.error === ACCESS_ERROR || this.error === ACCOUNT_ERROR; + } + get isWebGLError(): boolean { + return this.error === WEBGL_CREATION_ERROR; + } + get isBrowserError(): boolean { + return this.error === BROWSER_NOT_SUPPORTED_ERROR; + } + get isOtherError(): boolean { + return !!this.error && !this.invalidVersion && !this.isAccessError && !this.isWebGLError && !this.isBrowserError; + } + get ponyLimit() { + return this.model.characterLimit; + } + get hasTooManyPonies(): boolean { + return this.model.ponies.length > this.ponyLimit; + } + get isMarkedForMultiples(): boolean { + const account = this.model.account; + return !!account && hasFlag(account.flags, AccountDataFlags.Duplicates); + } + get isAndroidBrowser() { + return isAndroidBrowser; + } + get isBrowserOutdated() { + return !isAndroidBrowser && isBrowserOutdated && !this.storage.getBoolean('dismiss-outdated-browser'); + } + get leftMessage() { + return this.gameService.leftMessage; + } + get accountAlert() { + return this.model.accountAlert; + } + ngOnInit() { + loadAndInitSpriteSheets() + .then(loaded => this.failedToLoadImages = !loaded); + } + play() { + if (this.canPlay) { + this.joining = true; + this.locked = true; + this.error = undefined; - const delayTime = (!DEVELOPMENT && this.gameService.wasPlaying) ? 1500 : 10; + const delayTime = (!DEVELOPMENT && this.gameService.wasPlaying) ? 1500 : 10; - delay(delayTime) // delay joing if user reloaded the game instead of leaving cleanly - .then(() => this.model.savePony(this.model.pony)) - .then(pony => this.joining ? this.gameService.join(pony.id) : Promise.resolve()) - .catch((e: RequestError) => { - if (!/^Cancelled/.test(e.message)) { - this.error = e.message; + delay(delayTime) // delay joing if user reloaded the game instead of leaving cleanly + .then(() => this.model.savePony(this.model.pony)) + .then(pony => this.joining ? this.gameService.join(pony.id) : Promise.resolve()) + .catch((e: RequestError) => { + if (!/^Cancelled/.test(e.message)) { + this.error = e.message; - if (!includes(ignoredErrors, e.message) && !/shader/.test(e.message)) { - this.errorReporter.reportError(e, { status: e.status, text: e.text }); - } + if (!includes(ignoredErrors, e.message) && !/shader/.test(e.message)) { + this.errorReporter.reportError(e, { status: e.status, text: e.text }); + } - DEVELOPMENT && console.error(e); - } - }) - .finally(() => this.joining = false) - .then(() => delay(1500)) - .finally(() => this.locked = false); - } - } - cancel() { - this.gameService.leave('Cancelled joining'); - } - reload() { - location.reload(true); - } - hardReload() { - hardReload(); - } - hasFlag(server: ServerInfo) { - return server.countryFlags && server.countryFlags.length; - } - getIcon(server: ServerInfo) { - switch (server.flag) { - case 'star': return faStar; - case 'test': return faWrench; - default: return faGlobe; - } - } - dismissOutdatedBrowser() { - this.storage.setBoolean('dismiss-outdated-browser', true); - } - saveBirthdate() { - if (this.birthdate) { - this.model.updateAccount({ birthdate: this.birthdate }); - this.birthdateSet = true; - } - } + DEVELOPMENT && console.error(e); + } + }) + .finally(() => this.joining = false) + .then(() => delay(1500)) + .finally(() => this.locked = false); + } + } + cancel() { + this.gameService.leave('Cancelled joining'); + } + reload() { + location.reload(true); + } + hardReload() { + hardReload(); + } + hasFlag(server: ServerInfo) { + return server.countryFlags && server.countryFlags.length; + } + getIcon(server: ServerInfo) { + switch (server.flag) { + case 'star': return faStar; + case 'test': return faWrench; + default: return faGlobe; + } + } + dismissOutdatedBrowser() { + this.storage.setBoolean('dismiss-outdated-browser', true); + } + saveBirthdate() { + if (this.birthdate) { + this.model.updateAccount({ birthdate: this.birthdate }); + this.birthdateSet = true; + } + } } diff --git a/src/ts/components/shared/play-notice/play-notice.ts b/src/ts/components/shared/play-notice/play-notice.ts index 77776cb..589d6dc 100644 --- a/src/ts/components/shared/play-notice/play-notice.ts +++ b/src/ts/components/shared/play-notice/play-notice.ts @@ -3,10 +3,10 @@ import { supporterLink } from '../../../client/data'; import { GENERAL_RULES } from '../../../common/constants'; @Component({ - selector: 'play-notice', - templateUrl: 'play-notice.pug', + selector: 'play-notice', + templateUrl: 'play-notice.pug', }) export class PlayNotice { - readonly patreonLink = supporterLink; - readonly rules = GENERAL_RULES; + readonly patreonLink = supporterLink; + readonly rules = GENERAL_RULES; } diff --git a/src/ts/components/shared/pony-box/pony-box.ts b/src/ts/components/shared/pony-box/pony-box.ts index 87a17f6..0929da2 100644 --- a/src/ts/components/shared/pony-box/pony-box.ts +++ b/src/ts/components/shared/pony-box/pony-box.ts @@ -4,8 +4,8 @@ import { getPaletteInfo } from '../../../common/pony'; import { Model } from '../../services/model'; import { PonyTownGame } from '../../../client/game'; import { - partyLeaderIcon, faUserPlus, faUserTimes, faCheck, faMicrophoneSlash, faEyeSlash, faStar, faUserMinus, - faUserCog, faComment + partyLeaderIcon, faUserPlus, faUserTimes, faCheck, faMicrophoneSlash, faEyeSlash, faStar, faUserMinus, + faUserCog, faComment } from '../../../client/icons'; import { DAY } from '../../../common/constants'; import { isPonyInParty, isPartyLeader } from '../../../client/partyUtils'; @@ -14,100 +14,100 @@ import { isIgnored, isHidden, isFriend } from '../../../common/entityUtils'; import { setFlag } from '../../../common/utils'; @Component({ - selector: 'pony-box', - templateUrl: 'pony-box.pug', - styleUrls: ['pony-box.scss'], + selector: 'pony-box', + templateUrl: 'pony-box.pug', + styleUrls: ['pony-box.scss'], }) export class PonyBox { - readonly leaderIcon = partyLeaderIcon; - readonly inviteIcon = faUserPlus; - readonly removeIcon = faUserTimes; - readonly cogIcon = faUserCog; - readonly checkIcon = faCheck; - readonly ignoreIcon = faMicrophoneSlash; - readonly hideIcon = faEyeSlash; - readonly starIcon = faStar; - readonly addFriendIcon = faUserPlus; - readonly removeFriendIcon = faUserMinus; - readonly messageIcon = faComment; - isIgnored = isIgnored; - isFriend = isFriend; - removingFriend = false; - @Input() pony?: Pony; - @Output() sendMessage = new EventEmitter(); - constructor(private model: Model, private game: PonyTownGame) { - } - get ignoredOrHidden() { - return this.pony && (isIgnored(this.pony) || isHidden(this.pony)); - } - get isMod() { - return this.model.isMod; - } - get canInviteToParty() { - return this.pony && (!this.game.party || (isPartyLeader(this.game) && !isPonyInParty(this.game.party, this.pony, true))); - } - get canRemoveFromParty() { - return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, true); - } - get canPromoteToLeader() { - return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, false); - } - get special() { - const tag = getTag(this.pony && this.pony.tag); - return tag && tag.name; - } - get specialClass() { - const tag = getTag(this.pony && this.pony.tag); - return tag && tag.tagClass; - } - get paletteInfo() { - return this.pony && getPaletteInfo(this.pony); - } - inviteToParty() { - this.playerAction(PlayerAction.InviteToParty); - } - removeFromParty() { - this.playerAction(PlayerAction.RemoveFromParty); - } - promoteToLeader() { - this.playerAction(PlayerAction.PromotePartyLeader); - } - toggleIgnore() { - if (this.pony) { - const ignored = isIgnored(this.pony); - this.playerAction(ignored ? PlayerAction.Unignore : PlayerAction.Ignore); - this.pony.playerState = setFlag(this.pony.playerState, EntityPlayerState.Ignored, !ignored); - } - } - hidePlayer(days: number) { - this.playerAction(PlayerAction.HidePlayer, days * DAY); - } - addFriend() { - this.playerAction(PlayerAction.AddFriend); - } - removeFriend() { - this.playerAction(PlayerAction.RemoveFriend); - } - private playerAction(type: PlayerAction, param: any = undefined) { - const ponyId = this.pony && this.pony.id; + readonly leaderIcon = partyLeaderIcon; + readonly inviteIcon = faUserPlus; + readonly removeIcon = faUserTimes; + readonly cogIcon = faUserCog; + readonly checkIcon = faCheck; + readonly ignoreIcon = faMicrophoneSlash; + readonly hideIcon = faEyeSlash; + readonly starIcon = faStar; + readonly addFriendIcon = faUserPlus; + readonly removeFriendIcon = faUserMinus; + readonly messageIcon = faComment; + isIgnored = isIgnored; + isFriend = isFriend; + removingFriend = false; + @Input() pony?: Pony; + @Output() sendMessage = new EventEmitter(); + constructor(private model: Model, private game: PonyTownGame) { + } + get ignoredOrHidden() { + return this.pony && (isIgnored(this.pony) || isHidden(this.pony)); + } + get isMod() { + return this.model.isMod; + } + get canInviteToParty() { + return this.pony && (!this.game.party || (isPartyLeader(this.game) && !isPonyInParty(this.game.party, this.pony, true))); + } + get canRemoveFromParty() { + return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, true); + } + get canPromoteToLeader() { + return this.pony && isPartyLeader(this.game) && isPonyInParty(this.game.party, this.pony, false); + } + get special() { + const tag = getTag(this.pony && this.pony.tag); + return tag && tag.name; + } + get specialClass() { + const tag = getTag(this.pony && this.pony.tag); + return tag && tag.tagClass; + } + get paletteInfo() { + return this.pony && getPaletteInfo(this.pony); + } + inviteToParty() { + this.playerAction(PlayerAction.InviteToParty); + } + removeFromParty() { + this.playerAction(PlayerAction.RemoveFromParty); + } + promoteToLeader() { + this.playerAction(PlayerAction.PromotePartyLeader); + } + toggleIgnore() { + if (this.pony) { + const ignored = isIgnored(this.pony); + this.playerAction(ignored ? PlayerAction.Unignore : PlayerAction.Ignore); + this.pony.playerState = setFlag(this.pony.playerState, EntityPlayerState.Ignored, !ignored); + } + } + hidePlayer(days: number) { + this.playerAction(PlayerAction.HidePlayer, days * DAY); + } + addFriend() { + this.playerAction(PlayerAction.AddFriend); + } + removeFriend() { + this.playerAction(PlayerAction.RemoveFriend); + } + private playerAction(type: PlayerAction, param: any = undefined) { + const ponyId = this.pony && this.pony.id; - if (ponyId) { - this.game.send(server => server.playerAction(ponyId, type, param)); - } - } - sendMessageTo() { - if (this.pony) { - this.sendMessage.emit(this.pony); - } - } - // supporter servers - get canInviteToSupporterServers() { - return false; // DEVELOPMENT; // TODO: check if ignored or hidden - } - get isInvitedToSupporterServers() { - return false; - } - inviteToSupporterServers() { - this.playerAction(PlayerAction.InviteToSupporterServers); - } + if (ponyId) { + this.game.send(server => server.playerAction(ponyId, type, param)); + } + } + sendMessageTo() { + if (this.pony) { + this.sendMessage.emit(this.pony); + } + } + // supporter servers + get canInviteToSupporterServers() { + return false; // DEVELOPMENT; // TODO: check if ignored or hidden + } + get isInvitedToSupporterServers() { + return false; + } + inviteToSupporterServers() { + this.playerAction(PlayerAction.InviteToSupporterServers); + } } diff --git a/src/ts/components/shared/portrait-box/portrait-box.ts b/src/ts/components/shared/portrait-box/portrait-box.ts index 327ba81..9b0afa8 100644 --- a/src/ts/components/shared/portrait-box/portrait-box.ts +++ b/src/ts/components/shared/portrait-box/portrait-box.ts @@ -1,5 +1,5 @@ import { - Component, Input, AfterViewInit, OnChanges, ElementRef, ChangeDetectionStrategy, ViewChild, NgZone + Component, Input, AfterViewInit, OnChanges, ElementRef, ChangeDetectionStrategy, ViewChild, NgZone } from '@angular/core'; import { PalettePonyInfo } from '../../../common/interfaces'; import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch'; @@ -10,15 +10,15 @@ import { drawPony } from '../../../client/ponyDraw'; import { paletteSpriteSheet } from '../../../generated/sprites'; const scales: { [key: string]: number } = { - large: 3, - medium: 2, - small: 1, + large: 3, + medium: 2, + small: 1, }; const sizes: { [key: string]: number } = { - large: 100, - medium: 66, - small: 33, + large: 100, + medium: 66, + small: 33, }; const BUFFER_SIZE = 34; @@ -26,61 +26,61 @@ const options = defaultDrawPonyOptions(); const state = defaultPonyState(); @Component({ - selector: 'portrait-box', - templateUrl: 'portrait-box.pug', - styleUrls: ['portrait-box.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'portrait-box', + templateUrl: 'portrait-box.pug', + styleUrls: ['portrait-box.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class PortraitBox implements AfterViewInit, OnChanges { - @Input() noBorder = false; - @Input() flip = false; - @Input() size = 'large'; - @Input() pony: PalettePonyInfo | undefined = undefined; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - private frame = 0; - private batch?: ContextSpriteBatch; - constructor(private zone: NgZone) { - } - ngAfterViewInit() { - loadAndInitSpriteSheets() - .then(() => this.redraw()); - } - ngOnChanges() { - this.redraw(); - } - private redraw() { - this.frame = this.frame || this.zone.runOutsideAngular(() => requestAnimationFrame(() => { - this.frame = 0; - this.draw(); - })); - } - private draw() { - const canvas = this.canvas.nativeElement as HTMLCanvasElement; - const size = sizes[this.size]; - resizeCanvasWithRatio(canvas, size, size); + @Input() noBorder = false; + @Input() flip = false; + @Input() size = 'large'; + @Input() pony: PalettePonyInfo | undefined = undefined; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + private frame = 0; + private batch?: ContextSpriteBatch; + constructor(private zone: NgZone) { + } + ngAfterViewInit() { + loadAndInitSpriteSheets() + .then(() => this.redraw()); + } + ngOnChanges() { + this.redraw(); + } + private redraw() { + this.frame = this.frame || this.zone.runOutsideAngular(() => requestAnimationFrame(() => { + this.frame = 0; + this.draw(); + })); + } + private draw() { + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const size = sizes[this.size]; + resizeCanvasWithRatio(canvas, size, size); - const context = canvas.getContext('2d'); + const context = canvas.getContext('2d'); - if (context) { - context.save(); - context.fillStyle = '#444'; - context.fillRect(0, 0, canvas.width, canvas.height); + if (context) { + context.save(); + context.fillStyle = '#444'; + context.fillRect(0, 0, canvas.width, canvas.height); - if (this.pony) { - const scale = scales[this.size] * getPixelRatio(); - this.batch = this.batch || new ContextSpriteBatch(createCanvas(BUFFER_SIZE, BUFFER_SIZE)); - options.flipped = !this.flip; + if (this.pony) { + const scale = scales[this.size] * getPixelRatio(); + this.batch = this.batch || new ContextSpriteBatch(createCanvas(BUFFER_SIZE, BUFFER_SIZE)); + options.flipped = !this.flip; - this.batch.start(paletteSpriteSheet, 0); - drawPony(this.batch, this.pony, state, 25, 54, options); - this.batch.end(); + this.batch.start(paletteSpriteSheet, 0); + drawPony(this.batch, this.pony, state, 25, 54, options); + this.batch.end(); - disableImageSmoothing(context); - context.scale(this.flip ? scale : -scale, scale); - context.drawImage(this.batch.canvas, this.flip ? 0 : -BUFFER_SIZE, 0); - } + disableImageSmoothing(context); + context.scale(this.flip ? scale : -scale, scale); + context.drawImage(this.batch.canvas, this.flip ? 0 : -BUFFER_SIZE, 0); + } - context.restore(); - } - } + context.restore(); + } + } } diff --git a/src/ts/components/shared/scale-picker/scale-picker.ts b/src/ts/components/shared/scale-picker/scale-picker.ts index 8256373..e37dffc 100644 --- a/src/ts/components/shared/scale-picker/scale-picker.ts +++ b/src/ts/components/shared/scale-picker/scale-picker.ts @@ -2,20 +2,20 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; import { times } from 'lodash'; @Component({ - selector: 'scale-picker', - templateUrl: 'scale-picker.pug', + selector: 'scale-picker', + templateUrl: 'scale-picker.pug', }) export class ScalePicker { - @Input() scale = 1; - @Output() scaleChange = new EventEmitter(); - scales = [1, 2, 3, 4]; - @Input() set maxScale(value: number) { - this.scales = times(value, i => i + 1); - } - setScale(value: number) { - if (value !== this.scale) { - this.scale = value; - this.scaleChange.emit(value); - } - } + @Input() scale = 1; + @Output() scaleChange = new EventEmitter(); + scales = [1, 2, 3, 4]; + @Input() set maxScale(value: number) { + this.scales = times(value, i => i + 1); + } + setScale(value: number) { + if (value !== this.scale) { + this.scale = value; + this.scaleChange.emit(value); + } + } } diff --git a/src/ts/components/shared/set-selection/set-selection.ts b/src/ts/components/shared/set-selection/set-selection.ts index 1906a0e..4c9308e 100644 --- a/src/ts/components/shared/set-selection/set-selection.ts +++ b/src/ts/components/shared/set-selection/set-selection.ts @@ -7,67 +7,67 @@ const FILLS = ['Orange', 'DodgerBlue', 'LimeGreen', 'Orchid', 'crimson', 'Aquama const OUTLINES = ['Chocolate', 'SteelBlue', 'ForestGreen', 'DarkOrchid', 'darkred', 'DarkTurquoise']; @Directive({ - selector: '[setOutlineHidden]', + selector: '[setOutlineHidden]', }) export class SetOutlineHidden { - @Input() setOutlineHidden = false; + @Input() setOutlineHidden = false; } @Component({ - selector: 'set-selection', - templateUrl: 'set-selection.pug', - styleUrls: ['set-selection.scss'], + selector: 'set-selection', + templateUrl: 'set-selection.pug', + styleUrls: ['set-selection.scss'], }) export class SetSelection implements OnChanges { - readonly exampleFills = FILLS; - readonly exampleOutlines = OUTLINES; - @Input() label?: string; - @Input() base?: string; - @Input() set?: SpriteSet; - @Input() sets?: ColorExtraSets; - @Input() sprites?: ColorExtraSet; - @Input() circle?: string; - @Input() outlineHidden = false; - @Input() nonLockable = false; - @Input() compact = false; - @Input() onlyPatterns = false; - @Input() darken = true; - @Output() change = new EventEmitter(); - constructor(@Optional() private hidden: SetOutlineHidden) { - } - get isOutlineHidden() { - return this.hidden ? this.hidden.setOutlineHidden : this.outlineHidden; - } - get patternColors() { - const set = this.getSet(); - const pat = this.set && set && set[this.set.pattern || 0]; + readonly exampleFills = FILLS; + readonly exampleOutlines = OUTLINES; + @Input() label?: string; + @Input() base?: string; + @Input() set?: SpriteSet; + @Input() sets?: ColorExtraSets; + @Input() sprites?: ColorExtraSet; + @Input() circle?: string; + @Input() outlineHidden = false; + @Input() nonLockable = false; + @Input() compact = false; + @Input() onlyPatterns = false; + @Input() darken = true; + @Output() change = new EventEmitter(); + constructor(@Optional() private hidden: SetOutlineHidden) { + } + get isOutlineHidden() { + return this.hidden ? this.hidden.setOutlineHidden : this.outlineHidden; + } + get patternColors() { + const set = this.getSet(); + const pat = this.set && set && set[this.set.pattern || 0]; - if (pat && !pat.colors) { - return 0; - } else if (pat) { - return getColorCount(pat); - } else { - return this.nonLockable ? 1 : 0; - } - } - get showColorPatterns(): boolean { - const type = this.set && this.set.type || 0; - const set = this.sets && this.sets[type]; - return !!set && set.length > 1; - } - ngOnChanges() { - this.sprites = this.sets ? this.sets.map(s => s ? s[0] : undefined) : undefined; - } - onChange() { - const set = this.getSet(); + if (pat && !pat.colors) { + return 0; + } else if (pat) { + return getColorCount(pat); + } else { + return this.nonLockable ? 1 : 0; + } + } + get showColorPatterns(): boolean { + const type = this.set && this.set.type || 0; + const set = this.sets && this.sets[type]; + return !!set && set.length > 1; + } + ngOnChanges() { + this.sprites = this.sets ? this.sets.map(s => s ? s[0] : undefined) : undefined; + } + onChange() { + const set = this.getSet(); - if (this.set && set) { - this.set.pattern = clamp(this.set.pattern || 0, 0, set.length - 1); - } + if (this.set && set) { + this.set.pattern = clamp(this.set.pattern || 0, 0, set.length - 1); + } - this.change.emit(); - } - private getSet(): ColorExtraSet | undefined { - return this.set && this.sets && this.sets[this.set.type || 0]; - } + this.change.emit(); + } + private getSet(): ColorExtraSet | undefined { + return this.set && this.sets && this.sets[this.set.type || 0]; + } } diff --git a/src/ts/components/shared/settings-box/settings-box.ts b/src/ts/components/shared/settings-box/settings-box.ts index d8ca7c3..e837614 100644 --- a/src/ts/components/shared/settings-box/settings-box.ts +++ b/src/ts/components/shared/settings-box/settings-box.ts @@ -8,124 +8,124 @@ import { Model } from '../../services/model'; import { PonyTownGame } from '../../../client/game'; import { Dropdown } from '../directives/dropdown'; import { - emptyIcon, faCog, faSearch, faSignOutAlt, faStepForward, faVolumeOff, faVolumeUp, faVolumeDown, faPlus, faMinus + emptyIcon, faCog, faSearch, faSignOutAlt, faStepForward, faVolumeOff, faVolumeUp, faVolumeDown, faPlus, faMinus } from '../../../client/icons'; import { SettingsService } from '../../services/settingsService'; import { Audio } from '../../services/audio'; @Component({ - selector: 'settings-box', - templateUrl: 'settings-box.pug', - styleUrls: ['settings-box.scss'], + selector: 'settings-box', + templateUrl: 'settings-box.pug', + styleUrls: ['settings-box.scss'], }) export class SettingsBox implements OnInit, OnDestroy { - readonly cogIcon = faCog; - readonly searchIcon = faSearch; - readonly signOutIcon = faSignOutAlt; - readonly forwardIcon = faStepForward; - readonly emptyIcon = emptyIcon; - readonly plusIcon = faPlus; - readonly minusIcon = faMinus; - modalRef?: BsModalRef; - time?: string; - @ViewChild('dropdown', { static: true }) dropdown!: Dropdown; - @ViewChild('actionsModal', { static: true }) actionsModal!: TemplateRef; - @ViewChild('settingsModal', { static: true }) settingsModal!: TemplateRef; - @ViewChild('invitesModal', { static: true }) invitesModal!: TemplateRef; - private subscription?: Subscription; - constructor( - private model: Model, - private modalService: BsModalService, - private settingsService: SettingsService, - private gameService: GameService, - private game: PonyTownGame, - private audio: Audio, - private zone: NgZone, - ) { - } - get scale() { - return this.game.scale; - } - get volume() { - return this.game.volume; - } - set volume(value: number) { - this.settingsService.browser.volume = value; - this.settingsService.saveBrowserSettings(); - this.audio.setVolume(value); - } - get server() { - return this.gameService.server && this.gameService.server.name || ''; - } - get settings() { - return this.model.account && this.model.account.settings || {}; - } - get track() { - return this.game.audio.trackName; - } - get volumeIcon() { - return this.volume === 0 ? faVolumeOff : (this.volume < 50 ? faVolumeDown : faVolumeUp); - } - get isMod() { - return this.model.isMod; - } - get hasInvites() { - return this.isMod; // TEMP - } - ngOnInit() { - this.game.onClock - .pipe( - distinctUntilChanged(), - ) - .subscribe(text => { - if (this.dropdown.isOpen) { - this.zone.run(() => this.time = text); - } else { - this.time = text; - } - }); - } - ngOnDestroy() { - this.subscription && this.subscription.unsubscribe(); - } - toggleVolume() { - this.volume = this.volume === 0 ? 50 : 0; - } - volumeStarted() { - this.game.audio.forcePlay(); - } - nextTrack() { - this.game.audio.playRandomTrack(); - } - leave() { - this.gameService.leave('From settings dropdown'); - this.dropdown.close(); - } - zoomOut() { - this.game.zoomOut(); - } - zoomIn() { - this.game.zoomIn(); - } - unhideAllHiddenPlayers() { - this.game.send(server => server.action(Action.UnhideAllHiddenPlayers)); - this.dropdown.close(); - } - openModal(template: TemplateRef) { - this.modalRef = this.modalService.show(template, { ignoreBackdropClick: true }); - } - openSettings() { - this.openModal(this.settingsModal); - this.dropdown.close(); - } - openActions() { - this.openModal(this.actionsModal); - this.dropdown.close(); - } - openInvites() { - if (BETA) { - this.openModal(this.invitesModal); - this.dropdown.close(); - } - } + readonly cogIcon = faCog; + readonly searchIcon = faSearch; + readonly signOutIcon = faSignOutAlt; + readonly forwardIcon = faStepForward; + readonly emptyIcon = emptyIcon; + readonly plusIcon = faPlus; + readonly minusIcon = faMinus; + modalRef?: BsModalRef; + time?: string; + @ViewChild('dropdown', { static: true }) dropdown!: Dropdown; + @ViewChild('actionsModal', { static: true }) actionsModal!: TemplateRef; + @ViewChild('settingsModal', { static: true }) settingsModal!: TemplateRef; + @ViewChild('invitesModal', { static: true }) invitesModal!: TemplateRef; + private subscription?: Subscription; + constructor( + private model: Model, + private modalService: BsModalService, + private settingsService: SettingsService, + private gameService: GameService, + private game: PonyTownGame, + private audio: Audio, + private zone: NgZone, + ) { + } + get scale() { + return this.game.scale; + } + get volume() { + return this.game.volume; + } + set volume(value: number) { + this.settingsService.browser.volume = value; + this.settingsService.saveBrowserSettings(); + this.audio.setVolume(value); + } + get server() { + return this.gameService.server && this.gameService.server.name || ''; + } + get settings() { + return this.model.account && this.model.account.settings || {}; + } + get track() { + return this.game.audio.trackName; + } + get volumeIcon() { + return this.volume === 0 ? faVolumeOff : (this.volume < 50 ? faVolumeDown : faVolumeUp); + } + get isMod() { + return this.model.isMod; + } + get hasInvites() { + return this.isMod; // TEMP + } + ngOnInit() { + this.game.onClock + .pipe( + distinctUntilChanged(), + ) + .subscribe(text => { + if (this.dropdown.isOpen) { + this.zone.run(() => this.time = text); + } else { + this.time = text; + } + }); + } + ngOnDestroy() { + this.subscription && this.subscription.unsubscribe(); + } + toggleVolume() { + this.volume = this.volume === 0 ? 50 : 0; + } + volumeStarted() { + this.game.audio.forcePlay(); + } + nextTrack() { + this.game.audio.playRandomTrack(); + } + leave() { + this.gameService.leave('From settings dropdown'); + this.dropdown.close(); + } + zoomOut() { + this.game.zoomOut(); + } + zoomIn() { + this.game.zoomIn(); + } + unhideAllHiddenPlayers() { + this.game.send(server => server.action(Action.UnhideAllHiddenPlayers)); + this.dropdown.close(); + } + openModal(template: TemplateRef) { + this.modalRef = this.modalService.show(template, { ignoreBackdropClick: true }); + } + openSettings() { + this.openModal(this.settingsModal); + this.dropdown.close(); + } + openActions() { + this.openModal(this.actionsModal); + this.dropdown.close(); + } + openInvites() { + if (BETA) { + this.openModal(this.invitesModal); + this.dropdown.close(); + } + } } diff --git a/src/ts/components/shared/settings-modal/settings-modal.ts b/src/ts/components/shared/settings-modal/settings-modal.ts index 48ecf7f..df970a2 100644 --- a/src/ts/components/shared/settings-modal/settings-modal.ts +++ b/src/ts/components/shared/settings-modal/settings-modal.ts @@ -3,7 +3,7 @@ import { Subscription } from 'rxjs'; import { AccountSettings, BrowserSettings } from '../../../common/interfaces'; import { SettingsService } from '../../services/settingsService'; import { - DEFAULT_CHATLOG_OPACITY, MAX_CHATLOG_RANGE, MIN_CHATLOG_RANGE, isChatlogRangeUnlimited, MAX_FILTER_WORDS_LENGTH + DEFAULT_CHATLOG_OPACITY, MAX_CHATLOG_RANGE, MIN_CHATLOG_RANGE, isChatlogRangeUnlimited, MAX_FILTER_WORDS_LENGTH } from '../../../common/constants'; import { StorageService } from '../../services/storageService'; import { cloneDeep } from '../../../common/utils'; @@ -12,111 +12,111 @@ import { updateRangeIndicator } from '../../../client/clientUtils'; import { faSlidersH, faCommentSlash, faGamepad, faImage } from '../../../client/icons'; @Component({ - selector: 'settings-modal', - templateUrl: 'settings-modal.pug', - styleUrls: ['settings-modal.scss'], + selector: 'settings-modal', + templateUrl: 'settings-modal.pug', + styleUrls: ['settings-modal.scss'], }) export class SettingsModal implements OnInit, OnDestroy { - readonly maxChatlogRange = MAX_CHATLOG_RANGE; - readonly minChatlogRange = MIN_CHATLOG_RANGE; - readonly gameIcon = faSlidersH; - readonly chatIcon = faCommentSlash; - readonly filtersIcon = faCommentSlash; - readonly controlsIcon = faGamepad; - readonly graphicsIcon = faImage; - @Output() close = new EventEmitter(); - account: AccountSettings = {}; - browser: BrowserSettings = {}; - accountBackup: AccountSettings = {}; - browserBackup: BrowserSettings = {}; - private done = false; - private subscription?: Subscription; - constructor( - private settingsService: SettingsService, - private storage: StorageService, - private game: PonyTownGame, - ) { - } - get pane() { - return this.storage.getItem('settings-modal-pane') || 'game'; - } - set pane(value: string) { - this.storage.setItem('settings-modal-pane', value); - } - get lockLowGraphicsMode() { - return this.game.failedFBO; - } - get chatlogRangeText() { - const range = this.account.chatlogRange; - return isChatlogRangeUnlimited(range) ? 'entire screen' : `${range} tiles`; - } - ngOnInit() { - this.accountBackup = cloneDeep(this.settingsService.account); - this.browserBackup = cloneDeep(this.settingsService.browser); - this.account = this.settingsService.account; - this.browser = this.settingsService.browser; - this.setupDefaults(); - this.subscription = this.game.onLeft.subscribe(() => this.cancel()); - } - ngOnDestroy() { - this.finishChatlogRange(); + readonly maxChatlogRange = MAX_CHATLOG_RANGE; + readonly minChatlogRange = MIN_CHATLOG_RANGE; + readonly gameIcon = faSlidersH; + readonly chatIcon = faCommentSlash; + readonly filtersIcon = faCommentSlash; + readonly controlsIcon = faGamepad; + readonly graphicsIcon = faImage; + @Output() close = new EventEmitter(); + account: AccountSettings = {}; + browser: BrowserSettings = {}; + accountBackup: AccountSettings = {}; + browserBackup: BrowserSettings = {}; + private done = false; + private subscription?: Subscription; + constructor( + private settingsService: SettingsService, + private storage: StorageService, + private game: PonyTownGame, + ) { + } + get pane() { + return this.storage.getItem('settings-modal-pane') || 'game'; + } + set pane(value: string) { + this.storage.setItem('settings-modal-pane', value); + } + get lockLowGraphicsMode() { + return this.game.failedFBO; + } + get chatlogRangeText() { + const range = this.account.chatlogRange; + return isChatlogRangeUnlimited(range) ? 'entire screen' : `${range} tiles`; + } + ngOnInit() { + this.accountBackup = cloneDeep(this.settingsService.account); + this.browserBackup = cloneDeep(this.settingsService.browser); + this.account = this.settingsService.account; + this.browser = this.settingsService.browser; + this.setupDefaults(); + this.subscription = this.game.onLeft.subscribe(() => this.cancel()); + } + ngOnDestroy() { + this.finishChatlogRange(); - if (!this.done) { - this.cancel(); - } + if (!this.done) { + this.cancel(); + } - this.subscription && this.subscription.unsubscribe(); - } - reset() { - this.account = this.settingsService.account = {}; - this.browser = this.settingsService.browser = {}; - this.setupDefaults(); - } - cancel() { - this.done = true; - this.settingsService.account = this.accountBackup; - this.settingsService.browser = this.browserBackup; - this.close.emit(); - } - ok() { - if (this.account.filterWords) { - let filter = this.account.filterWords; + this.subscription && this.subscription.unsubscribe(); + } + reset() { + this.account = this.settingsService.account = {}; + this.browser = this.settingsService.browser = {}; + this.setupDefaults(); + } + cancel() { + this.done = true; + this.settingsService.account = this.accountBackup; + this.settingsService.browser = this.browserBackup; + this.close.emit(); + } + ok() { + if (this.account.filterWords) { + let filter = this.account.filterWords; - while (filter.length > MAX_FILTER_WORDS_LENGTH && /\s/.test(filter)) { - filter = filter.trim().replace(/\s+\S+$/, ''); - } + while (filter.length > MAX_FILTER_WORDS_LENGTH && /\s/.test(filter)) { + filter = filter.trim().replace(/\s+\S+$/, ''); + } - if (filter.length > MAX_FILTER_WORDS_LENGTH) { - this.account.filterWords = ''; - } else { - this.account.filterWords = filter; - } - } + if (filter.length > MAX_FILTER_WORDS_LENGTH) { + this.account.filterWords = ''; + } else { + this.account.filterWords = filter; + } + } - this.done = true; - this.settingsService.saveAccountSettings(this.account); - this.settingsService.saveBrowserSettings(this.browser); - this.close.emit(); - } - updateChatlogRange(range: number | undefined) { - document.body.classList.add('translucent-modals'); - updateRangeIndicator(range, this.game); - } - finishChatlogRange() { - document.body.classList.remove('translucent-modals'); - updateRangeIndicator(undefined, this.game); - } - private setupDefaults() { - if (this.account.chatlogOpacity === undefined) { - this.account.chatlogOpacity = DEFAULT_CHATLOG_OPACITY; - } + this.done = true; + this.settingsService.saveAccountSettings(this.account); + this.settingsService.saveBrowserSettings(this.browser); + this.close.emit(); + } + updateChatlogRange(range: number | undefined) { + document.body.classList.add('translucent-modals'); + updateRangeIndicator(range, this.game); + } + finishChatlogRange() { + document.body.classList.remove('translucent-modals'); + updateRangeIndicator(undefined, this.game); + } + private setupDefaults() { + if (this.account.chatlogOpacity === undefined) { + this.account.chatlogOpacity = DEFAULT_CHATLOG_OPACITY; + } - if (this.account.chatlogRange === undefined) { - this.account.chatlogRange = MAX_CHATLOG_RANGE; - } + if (this.account.chatlogRange === undefined) { + this.account.chatlogRange = MAX_CHATLOG_RANGE; + } - if (this.account.filterWords === undefined) { - this.account.filterWords = ''; - } - } + if (this.account.filterWords === undefined) { + this.account.filterWords = ''; + } + } } diff --git a/src/ts/components/shared/shared.module.ts b/src/ts/components/shared/shared.module.ts index 9bc1180..dea45b8 100644 --- a/src/ts/components/shared/shared.module.ts +++ b/src/ts/components/shared/shared.module.ts @@ -75,87 +75,87 @@ import { SaveActiveTab } from './directives/saveActiveTab'; import { SiteNamePipe } from './pipes/siteName'; const declarations = [ - ActionBar, - ActionButton, - ActionsModal, - BitmapBox, - ButtMarkEditor, - MenuBar, - MenuItem, - CharacterList, - CharacterPreview, - CharacterSelect, - EmoteBox, - SliderBar, - SpriteBox, - SpriteSelection, - SupportButton, - SupporterPony, - SetSelection, - SetOutlineHidden, - CheckBox, - PortraitBox, - ScalePicker, - SwapBox, - ColorPicker, - CustomCheckbox, - DatePicker, - SignInBox, - PonyBox, - ModBox, - PartyBox, - PartyList, - SiteInfo, - SettingsBox, - SettingsModal, - FillOutline, - FriendsBox, - InstallButton, - InvitesModal, - KbdKey, - PlayBox, - PlayNotice, - PageLoader, - ChatBox, - ChatLog, - SiteLinks, - NotificationItem, - NotificationList, - ...dropdownDirectives, - ...tabsetComponents, - ...draggableComponents, - ...virtualListDirectives, - VirtualList, - Anchor, - BtnHighlight, - BtnHighlightDanger, - AgDrag, - AgAutoFocus, - LinkCurrent, - LabelledBy, - RevSrc, - FixToTop, - FocusTitle, - FocusTrap, - HasFeature, - SaveActiveTab, - SiteNamePipe, + ActionBar, + ActionButton, + ActionsModal, + BitmapBox, + ButtMarkEditor, + MenuBar, + MenuItem, + CharacterList, + CharacterPreview, + CharacterSelect, + EmoteBox, + SliderBar, + SpriteBox, + SpriteSelection, + SupportButton, + SupporterPony, + SetSelection, + SetOutlineHidden, + CheckBox, + PortraitBox, + ScalePicker, + SwapBox, + ColorPicker, + CustomCheckbox, + DatePicker, + SignInBox, + PonyBox, + ModBox, + PartyBox, + PartyList, + SiteInfo, + SettingsBox, + SettingsModal, + FillOutline, + FriendsBox, + InstallButton, + InvitesModal, + KbdKey, + PlayBox, + PlayNotice, + PageLoader, + ChatBox, + ChatLog, + SiteLinks, + NotificationItem, + NotificationList, + ...dropdownDirectives, + ...tabsetComponents, + ...draggableComponents, + ...virtualListDirectives, + VirtualList, + Anchor, + BtnHighlight, + BtnHighlightDanger, + AgDrag, + AgAutoFocus, + LinkCurrent, + LabelledBy, + RevSrc, + FixToTop, + FocusTitle, + FocusTrap, + HasFeature, + SaveActiveTab, + SiteNamePipe, ]; @NgModule({ - imports: [ - BrowserModule, - RouterModule, - FormsModule, - TooltipModule.forRoot(), - PopoverModule, - ButtonsModule, - ModalModule.forRoot(), - FontAwesomeModule, - // ScrollingModule, - ], - declarations: declarations, - exports: declarations, + imports: [ + BrowserModule, + RouterModule, + FormsModule, + TooltipModule.forRoot(), + PopoverModule, + ButtonsModule, + ModalModule.forRoot(), + FontAwesomeModule, + // ScrollingModule, + ], + declarations: declarations, + exports: declarations, }) export class SharedModule { } diff --git a/src/ts/components/shared/sign-in-box/sign-in-box.ts b/src/ts/components/shared/sign-in-box/sign-in-box.ts index e08e79e..59bf6be 100644 --- a/src/ts/components/shared/sign-in-box/sign-in-box.ts +++ b/src/ts/components/shared/sign-in-box/sign-in-box.ts @@ -4,23 +4,23 @@ import { emptyIcon, oauthIcons } from '../../../client/icons'; import { OAuthProvider } from '../../../common/interfaces'; export function getProviderIcon(id: string) { - return oauthIcons[id] || emptyIcon; + return oauthIcons[id] || emptyIcon; } @Component({ - selector: 'sign-in-box', - templateUrl: 'sign-in-box.pug', - styleUrls: ['sign-in-box.scss'], + selector: 'sign-in-box', + templateUrl: 'sign-in-box.pug', + styleUrls: ['sign-in-box.scss'], }) export class SignInBox { - readonly signUpProviders = signUpProviders; - readonly signInProviders = signInProviders; - readonly local = local || DEVELOPMENT; - @Output() signIn = new EventEmitter(); - icon(id: string) { - return getProviderIcon(id); - } - signInTo(provider: OAuthProvider) { - this.signIn.emit(provider); - } + readonly signUpProviders = signUpProviders; + readonly signInProviders = signInProviders; + readonly local = local || DEVELOPMENT; + @Output() signIn = new EventEmitter(); + icon(id: string) { + return getProviderIcon(id); + } + signInTo(provider: OAuthProvider) { + this.signIn.emit(provider); + } } diff --git a/src/ts/components/shared/site-info/site-info.ts b/src/ts/components/shared/site-info/site-info.ts index a1043a3..10d21f1 100644 --- a/src/ts/components/shared/site-info/site-info.ts +++ b/src/ts/components/shared/site-info/site-info.ts @@ -4,16 +4,16 @@ import { toSocialSiteInfo } from '../../../client/clientUtils'; import { getProviderIcon } from '../sign-in-box/sign-in-box'; @Component({ - selector: 'site-info', - templateUrl: 'site-info.pug', - styleUrls: ['site-info.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'site-info', + templateUrl: 'site-info.pug', + styleUrls: ['site-info.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SiteInfo { - info?: SocialSiteInfo; - icon: any; - @Input() set site(value: SocialSite | undefined) { - this.info = value && toSocialSiteInfo(value); - this.icon = getProviderIcon(this.info && this.info.icon || ''); - } + info?: SocialSiteInfo; + icon: any; + @Input() set site(value: SocialSite | undefined) { + this.info = value && toSocialSiteInfo(value); + this.icon = getProviderIcon(this.info && this.info.icon || ''); + } } diff --git a/src/ts/components/shared/site-links/site-links.ts b/src/ts/components/shared/site-links/site-links.ts index ce87c67..34ece70 100644 --- a/src/ts/components/shared/site-links/site-links.ts +++ b/src/ts/components/shared/site-links/site-links.ts @@ -1,10 +1,10 @@ import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; @Component({ - selector: 'site-links', - templateUrl: 'site-links.pug', - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'site-links', + templateUrl: 'site-links.pug', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SiteLinks { - @Input() links: string[] = []; + @Input() links: string[] = []; } diff --git a/src/ts/components/shared/slider-bar/slider-bar.ts b/src/ts/components/shared/slider-bar/slider-bar.ts index aab2461..ea1e599 100644 --- a/src/ts/components/shared/slider-bar/slider-bar.ts +++ b/src/ts/components/shared/slider-bar/slider-bar.ts @@ -1,89 +1,89 @@ import { - Component, Input, Output, EventEmitter, ElementRef, ChangeDetectionStrategy, HostListener, ViewChild + Component, Input, Output, EventEmitter, ElementRef, ChangeDetectionStrategy, HostListener, ViewChild } from '@angular/core'; import { clamp } from 'lodash'; import { AgDragEvent } from '../directives/agDrag'; import { Key } from '../../../client/input/input'; @Component({ - selector: 'slider-bar', - templateUrl: 'slider-bar.pug', - styleUrls: ['slider-bar.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { - 'role': 'slider', - '[tabindex]': 'disabled ? -1 : 0', - '[attr.aria-valuemin]': 'min', - '[attr.aria-valuemax]': 'max', - '[attr.aria-valuenow]': 'value', - '[attr.aria-disabled]': 'disabled', - }, + selector: 'slider-bar', + templateUrl: 'slider-bar.pug', + styleUrls: ['slider-bar.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'role': 'slider', + '[tabindex]': 'disabled ? -1 : 0', + '[attr.aria-valuemin]': 'min', + '[attr.aria-valuemax]': 'max', + '[attr.aria-valuenow]': 'value', + '[attr.aria-disabled]': 'disabled', + }, }) export class SliderBar { - @Input() min = 0; - @Input() max = 100; - @Input() step = 0; - @Input() largeStep = 10; - @Input() disabled = false; - @Input() value = 0; - @Output() valueChange = new EventEmitter(); - @Output() changed = new EventEmitter(); - @ViewChild('bar', { static: true }) bar!: ElementRef; - private currentWidth = 0; - get width() { - return clamp(((this.value - this.min) / (this.max - this.min)) * 100, 0, 100); - } - drag({ type, x, event }: AgDragEvent) { - if (this.disabled) - return; + @Input() min = 0; + @Input() max = 100; + @Input() step = 0; + @Input() largeStep = 10; + @Input() disabled = false; + @Input() value = 0; + @Output() valueChange = new EventEmitter(); + @Output() changed = new EventEmitter(); + @ViewChild('bar', { static: true }) bar!: ElementRef; + private currentWidth = 0; + get width() { + return clamp(((this.value - this.min) / (this.max - this.min)) * 100, 0, 100); + } + drag({ type, x, event }: AgDragEvent) { + if (this.disabled) + return; - event.preventDefault(); + event.preventDefault(); - if (type === 'start') { - this.currentWidth = this.bar.nativeElement.getBoundingClientRect().width; - } + if (type === 'start') { + this.currentWidth = this.bar.nativeElement.getBoundingClientRect().width; + } - let val = this.min + clamp(x / this.currentWidth, 0, 1) * (this.max - this.min); + let val = this.min + clamp(x / this.currentWidth, 0, 1) * (this.max - this.min); - if (this.step) { - val = Math.round(val / this.step) * this.step; - } + if (this.step) { + val = Math.round(val / this.step) * this.step; + } - this.setValue(val, false); + this.setValue(val, false); - if (type === 'end') { - this.changed.emit(val); - } - } - @HostListener('keydown', ['$event']) - keydown(e: KeyboardEvent) { - if (this.disabled) - return; + if (type === 'end') { + this.changed.emit(val); + } + } + @HostListener('keydown', ['$event']) + keydown(e: KeyboardEvent) { + if (this.disabled) + return; - const step = this.step || 1; + const step = this.step || 1; - if (e.keyCode === Key.LEFT || e.keyCode === Key.DOWN || e.keyCode === Key.PAGE_DOWN) { - e.preventDefault(); - this.setValue(this.value - step * (e.keyCode === Key.PAGE_DOWN ? this.largeStep : 1), true); - } else if (e.keyCode === Key.RIGHT || e.keyCode === Key.UP || e.keyCode === Key.PAGE_UP) { - e.preventDefault(); - this.setValue(this.value + step * (e.keyCode === Key.PAGE_UP ? this.largeStep : 1), true); - } else if (e.keyCode === Key.HOME) { - e.preventDefault(); - this.setValue(this.min, true); - } else if (e.keyCode === Key.END) { - e.preventDefault(); - this.setValue(this.max, true); - } - } - private setValue(value: number, emit: boolean) { - if (this.value !== value) { - this.value = clamp(value, this.min, this.max); - this.valueChange.emit(this.value); + if (e.keyCode === Key.LEFT || e.keyCode === Key.DOWN || e.keyCode === Key.PAGE_DOWN) { + e.preventDefault(); + this.setValue(this.value - step * (e.keyCode === Key.PAGE_DOWN ? this.largeStep : 1), true); + } else if (e.keyCode === Key.RIGHT || e.keyCode === Key.UP || e.keyCode === Key.PAGE_UP) { + e.preventDefault(); + this.setValue(this.value + step * (e.keyCode === Key.PAGE_UP ? this.largeStep : 1), true); + } else if (e.keyCode === Key.HOME) { + e.preventDefault(); + this.setValue(this.min, true); + } else if (e.keyCode === Key.END) { + e.preventDefault(); + this.setValue(this.max, true); + } + } + private setValue(value: number, emit: boolean) { + if (this.value !== value) { + this.value = clamp(value, this.min, this.max); + this.valueChange.emit(this.value); - if (emit) { - this.changed.emit(value); - } - } - } + if (emit) { + this.changed.emit(value); + } + } + } } diff --git a/src/ts/components/shared/sprite-box/sprite-box.ts b/src/ts/components/shared/sprite-box/sprite-box.ts index 124c188..e61197d 100644 --- a/src/ts/components/shared/sprite-box/sprite-box.ts +++ b/src/ts/components/shared/sprite-box/sprite-box.ts @@ -1,5 +1,5 @@ import { - Component, Input, AfterViewInit, ViewChild, ElementRef, NgZone, DoCheck, IterableDiffers, IterableDiffer, OnChanges + Component, Input, AfterViewInit, ViewChild, ElementRef, NgZone, DoCheck, IterableDiffers, IterableDiffer, OnChanges } from '@angular/core'; import { Rect, Sprite, ColorExtra, Palette } from '../../../common/interfaces'; import { parseColor, colorToCSS } from '../../../common/color'; @@ -16,171 +16,171 @@ let redrawFrame = 0; const forRedraw: SpriteBox[] = []; function drawAll() { - redrawFrame = 0; - forRedraw.forEach(box => box.draw()); - forRedraw.length = 0; + redrawFrame = 0; + forRedraw.forEach(box => box.draw()); + forRedraw.length = 0; } @Component({ - selector: 'sprite-box', - templateUrl: 'sprite-box.pug', - styleUrls: ['sprite-box.scss'], + selector: 'sprite-box', + templateUrl: 'sprite-box.pug', + styleUrls: ['sprite-box.scss'], }) export class SpriteBox implements AfterViewInit, OnChanges, DoCheck { - readonly debug = DEVELOPMENT; - readonly noneIcon = faTimes; - @Input() size = 52; - @Input() scale = 2; - @Input() x = 0; - @Input() y = 0; - @Input() center = true; - @Input() index?: number; - @Input() sprite?: ColorExtra; - @Input() palette?: Palette; - @Input() fill?: string[] | string; - @Input() outline?: string[] | string; - @Input() reverseExtra?: boolean; - @Input() timestamp: any; - @Input() invisible = false; - @Input() darken = true; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - private _circle?: string; - private fillDiffer: IterableDiffer; - private outlineDiffer: IterableDiffer; - private batch?: ContextSpriteBatch; - constructor(private zone: NgZone, iterableDiffers: IterableDiffers) { - this.fillDiffer = iterableDiffers.find([]).create(); - this.outlineDiffer = iterableDiffers.find([]).create(); - } - @Input() get circle() { - return this._circle; - } - set circle(value) { - this._circle = colorToCSS(parseColor(value || '')); - } - ngAfterViewInit() { - loadAndInitSpriteSheets().then(() => this.redraw()); - } - ngDoCheck() { - const fillChanges = this.fill && Array.isArray(this.fill) && this.fillDiffer.diff(this.fill); - const outlineChanges = this.outline && Array.isArray(this.outline) && this.outlineDiffer.diff(this.outline); + readonly debug = DEVELOPMENT; + readonly noneIcon = faTimes; + @Input() size = 52; + @Input() scale = 2; + @Input() x = 0; + @Input() y = 0; + @Input() center = true; + @Input() index?: number; + @Input() sprite?: ColorExtra; + @Input() palette?: Palette; + @Input() fill?: string[] | string; + @Input() outline?: string[] | string; + @Input() reverseExtra?: boolean; + @Input() timestamp: any; + @Input() invisible = false; + @Input() darken = true; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + private _circle?: string; + private fillDiffer: IterableDiffer; + private outlineDiffer: IterableDiffer; + private batch?: ContextSpriteBatch; + constructor(private zone: NgZone, iterableDiffers: IterableDiffers) { + this.fillDiffer = iterableDiffers.find([]).create(); + this.outlineDiffer = iterableDiffers.find([]).create(); + } + @Input() get circle() { + return this._circle; + } + set circle(value) { + this._circle = colorToCSS(parseColor(value || '')); + } + ngAfterViewInit() { + loadAndInitSpriteSheets().then(() => this.redraw()); + } + ngDoCheck() { + const fillChanges = this.fill && Array.isArray(this.fill) && this.fillDiffer.diff(this.fill); + const outlineChanges = this.outline && Array.isArray(this.outline) && this.outlineDiffer.diff(this.outline); - if (fillChanges || outlineChanges) { - this.redraw(); - } - } - ngOnChanges() { - this.redraw(); - } - private redraw() { - if (!redrawFrame) { - this.zone.runOutsideAngular(() => redrawFrame = requestAnimationFrame(drawAll)); - } + if (fillChanges || outlineChanges) { + this.redraw(); + } + } + ngOnChanges() { + this.redraw(); + } + private redraw() { + if (!redrawFrame) { + this.zone.runOutsideAngular(() => redrawFrame = requestAnimationFrame(drawAll)); + } - if (forRedraw.indexOf(this) === -1) { - forRedraw.push(this); - } - } - draw() { - const size = this.size; - const scale = this.scale; - const canvas = this.canvas.nativeElement as HTMLCanvasElement; + if (forRedraw.indexOf(this) === -1) { + forRedraw.push(this); + } + } + draw() { + const size = this.size; + const scale = this.scale; + const canvas = this.canvas.nativeElement as HTMLCanvasElement; - if (!size || this.invisible) - return; + if (!size || this.invisible) + return; - if (canvas.width !== size || canvas.height !== size) { - canvas.width = size; - canvas.height = size; - } + if (canvas.width !== size || canvas.height !== size) { + canvas.width = size; + canvas.height = size; + } - const context = canvas.getContext('2d'); + const context = canvas.getContext('2d'); - if (!context) - return; + if (!context) + return; - context.save(); - context.clearRect(0, 0, canvas.width, canvas.height); + context.save(); + context.clearRect(0, 0, canvas.width, canvas.height); - const sprite = this.sprite; + const sprite = this.sprite; - if (sprite) { - if (this.circle) { - context.fillStyle = this.circle; - context.beginPath(); - context.arc(canvas.width / 2, canvas.height / 2, canvas.width / 3, 0, Math.PI * 2); - context.fill(); - } + if (sprite) { + if (this.circle) { + context.fillStyle = this.circle; + context.beginPath(); + context.arc(canvas.width / 2, canvas.height / 2, canvas.width / 3, 0, Math.PI * 2); + context.fill(); + } - const bufferSize = size / scale; - const batch = this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferSize, bufferSize)); - resizeCanvas(batch.canvas, bufferSize, bufferSize); + const bufferSize = size / scale; + const batch = this.batch = this.batch || new ContextSpriteBatch(createCanvas(bufferSize, bufferSize)); + resizeCanvas(batch.canvas, bufferSize, bufferSize); - const fills = Array.isArray(this.fill) ? this.fill : [this.fill]; - const outlines = Array.isArray(this.outline) ? this.outline : [this.outline]; - const paletteColors = toColorList(getColorsFromSet({ fills, outlines }, '000000', this.darken)); - const palette = mockPaletteManager.addArray(paletteColors); - const extraPalette = sprite.palettes && mockPaletteManager.addArray(sprite.palettes[0]); + const fills = Array.isArray(this.fill) ? this.fill : [this.fill]; + const outlines = Array.isArray(this.outline) ? this.outline : [this.outline]; + const paletteColors = toColorList(getColorsFromSet({ fills, outlines }, '000000', this.darken)); + const palette = mockPaletteManager.addArray(paletteColors); + const extraPalette = sprite.palettes && mockPaletteManager.addArray(sprite.palettes[0]); - let x = this.x; - let y = this.y; + let x = this.x; + let y = this.y; - if (this.center) { - const bounds = rect(0, 0, 0, 0); - addRect(bounds, sprite.color); - addRect(bounds, sprite.extra); + if (this.center) { + const bounds = rect(0, 0, 0, 0); + addRect(bounds, sprite.color); + addRect(bounds, sprite.extra); - if (sprite.colorMany) { - sprite.colorMany.forEach(c => addRect(bounds, c)); - } + if (sprite.colorMany) { + sprite.colorMany.forEach(c => addRect(bounds, c)); + } - x = Math.round((bufferSize - bounds.w) / 2 - bounds.x); - y = Math.round((bufferSize - bounds.h) / 2 - bounds.y); - } + x = Math.round((bufferSize - bounds.w) / 2 - bounds.x); + y = Math.round((bufferSize - bounds.h) / 2 - bounds.y); + } - batch.start(paletteSpriteSheet, 0); + batch.start(paletteSpriteSheet, 0); - if (this.reverseExtra) { - batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y); - } + if (this.reverseExtra) { + batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y); + } - if (sprite.colorMany) { - for (const color of sprite.colorMany) { - batch.drawSprite(color, WHITE, palette, x, y); - } - } else { - batch.drawSprite(sprite.color, WHITE, palette, x, y); - } + if (sprite.colorMany) { + for (const color of sprite.colorMany) { + batch.drawSprite(color, WHITE, palette, x, y); + } + } else { + batch.drawSprite(sprite.color, WHITE, palette, x, y); + } - if (!this.reverseExtra) { - batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y); - } + if (!this.reverseExtra) { + batch.drawSprite(sprite.extra, WHITE, extraPalette, x, y); + } - batch.end(); + batch.end(); - disableImageSmoothing(context); - context.scale(scale, scale); - context.drawImage(batch.canvas, 0, 0); - } + disableImageSmoothing(context); + context.scale(scale, scale); + context.drawImage(batch.canvas, 0, 0); + } - context.restore(); - } + context.restore(); + } } function addRect(rect: Rect, sprite: Sprite | undefined) { - if (sprite && sprite.w && sprite.h) { - if (rect.w === 0 || rect.h === 0) { - rect.x = sprite.ox; - rect.y = sprite.oy; - rect.w = sprite.w; - rect.h = sprite.h; - } else { - const x = Math.min(rect.x, sprite.ox); - const y = Math.min(rect.y, sprite.oy); - rect.w = Math.max(rect.x + rect.w, sprite.ox + sprite.w) - x; - rect.h = Math.max(rect.y + rect.h, sprite.oy + sprite.h) - y; - rect.x = x; - rect.y = y; - } - } + if (sprite && sprite.w && sprite.h) { + if (rect.w === 0 || rect.h === 0) { + rect.x = sprite.ox; + rect.y = sprite.oy; + rect.w = sprite.w; + rect.h = sprite.h; + } else { + const x = Math.min(rect.x, sprite.ox); + const y = Math.min(rect.y, sprite.oy); + rect.w = Math.max(rect.x + rect.w, sprite.ox + sprite.w) - x; + rect.h = Math.max(rect.y + rect.h, sprite.oy + sprite.h) - y; + rect.x = x; + rect.y = y; + } + } } diff --git a/src/ts/components/shared/sprite-selection/sprite-selection.ts b/src/ts/components/shared/sprite-selection/sprite-selection.ts index 5384502..01e32b6 100644 --- a/src/ts/components/shared/sprite-selection/sprite-selection.ts +++ b/src/ts/components/shared/sprite-selection/sprite-selection.ts @@ -7,91 +7,91 @@ import { focusElementAfterTimeout } from '../../../client/htmlUtils'; const MAX = 999999; @Component({ - selector: 'sprite-selection', - templateUrl: 'sprite-selection.pug', - styleUrls: ['sprite-selection.scss'], - host: { - 'role': 'radiogroup', - 'tabindex': '0', - '(keydown)': 'keydown($event)', - '[attr.aria-activedescendant]': 'activeDescendant', - }, + selector: 'sprite-selection', + templateUrl: 'sprite-selection.pug', + styleUrls: ['sprite-selection.scss'], + host: { + 'role': 'radiogroup', + 'tabindex': '0', + '(keydown)': 'keydown($event)', + '[attr.aria-activedescendant]': 'activeDescendant', + }, }) export class SpriteSelection { - @Input() selected = 0; - @Output() selectedChange = new EventEmitter(); - @Input() sprites?: ColorExtra[]; - @Input() fill?: string | string[]; - @Input() outline?: string | string[]; - @Input() circle?: string; - @Input() reverseExtra = false; - @Input() limit = MAX; - @Input() skip = 0; - @Input() disabled = false; - @Input() emptyLabel?: string; - @Input() invisible = false; - @Input() darken = true; - id = uniqueId('sprite-selection-'); - constructor(private element: ElementRef) { - } - get hasMore() { - return this.sprites && this.sprites.length > this.limit; - } - get end() { - return this.skip + this.limit; - } - get activeDescendant() { - return `${this.id}-${this.selected - this.skip}`; - } - isSelected(index: number) { - return this.selected === (index + this.skip); - } - select(index: number, focus = false) { - if (!this.disabled && this.selected !== index) { - this.selected = index; - this.selectedChange.emit(index); + @Input() selected = 0; + @Output() selectedChange = new EventEmitter(); + @Input() sprites?: ColorExtra[]; + @Input() fill?: string | string[]; + @Input() outline?: string | string[]; + @Input() circle?: string; + @Input() reverseExtra = false; + @Input() limit = MAX; + @Input() skip = 0; + @Input() disabled = false; + @Input() emptyLabel?: string; + @Input() invisible = false; + @Input() darken = true; + id = uniqueId('sprite-selection-'); + constructor(private element: ElementRef) { + } + get hasMore() { + return this.sprites && this.sprites.length > this.limit; + } + get end() { + return this.skip + this.limit; + } + get activeDescendant() { + return `${this.id}-${this.selected - this.skip}`; + } + isSelected(index: number) { + return this.selected === (index + this.skip); + } + select(index: number, focus = false) { + if (!this.disabled && this.selected !== index) { + this.selected = index; + this.selectedChange.emit(index); - if (this.hasMore && index >= this.end) { - this.showMore(); - } + if (this.hasMore && index >= this.end) { + this.showMore(); + } - if (focus) { - focusElementAfterTimeout(this.element.nativeElement, '.active'); - } - } - } - showMore() { - this.limit = MAX; - } - keydown(e: KeyboardEvent) { - const select = this.handleKey(e.keyCode); + if (focus) { + focusElementAfterTimeout(this.element.nativeElement, '.active'); + } + } + } + showMore() { + this.limit = MAX; + } + keydown(e: KeyboardEvent) { + const select = this.handleKey(e.keyCode); - if (select !== undefined) { - e.preventDefault(); - this.select(select, true); - } - } - private handleKey(keyCode: number): number | undefined { - if (this.sprites) { - if (keyCode === Key.RIGHT || keyCode === Key.DOWN) { - if (this.selected >= (this.sprites.length - 1)) { - return this.skip; - } else { - return this.selected + 1; - } - } else if (keyCode === Key.LEFT || keyCode === Key.UP) { - if (this.selected <= this.skip) { - return this.sprites.length - 1; - } else { - return this.selected - 1; - } - } else if (keyCode === Key.HOME) { - return this.skip; - } else if (keyCode === Key.END) { - return this.sprites.length - 1; - } - } + if (select !== undefined) { + e.preventDefault(); + this.select(select, true); + } + } + private handleKey(keyCode: number): number | undefined { + if (this.sprites) { + if (keyCode === Key.RIGHT || keyCode === Key.DOWN) { + if (this.selected >= (this.sprites.length - 1)) { + return this.skip; + } else { + return this.selected + 1; + } + } else if (keyCode === Key.LEFT || keyCode === Key.UP) { + if (this.selected <= this.skip) { + return this.sprites.length - 1; + } else { + return this.selected - 1; + } + } else if (keyCode === Key.HOME) { + return this.skip; + } else if (keyCode === Key.END) { + return this.sprites.length - 1; + } + } - return undefined; - } + return undefined; + } } diff --git a/src/ts/components/shared/support-button/support-button.ts b/src/ts/components/shared/support-button/support-button.ts index 4e8700f..79452e9 100644 --- a/src/ts/components/shared/support-button/support-button.ts +++ b/src/ts/components/shared/support-button/support-button.ts @@ -3,15 +3,15 @@ import { Model } from '../../services/model'; import { supporterLink } from '../../../client/data'; @Component({ - selector: 'support-button', - templateUrl: 'support-button.pug', - styleUrls: ['support-button.scss'], + selector: 'support-button', + templateUrl: 'support-button.pug', + styleUrls: ['support-button.scss'], }) export class SupportButton { - readonly patreonLink = supporterLink; - constructor(private model: Model) { - } - get supporter() { - return this.model.supporter; - } + readonly patreonLink = supporterLink; + constructor(private model: Model) { + } + get supporter() { + return this.model.supporter; + } } diff --git a/src/ts/components/shared/supporter-pony/supporter-pony.ts b/src/ts/components/shared/supporter-pony/supporter-pony.ts index 69e742c..d6c9eff 100644 --- a/src/ts/components/shared/supporter-pony/supporter-pony.ts +++ b/src/ts/components/shared/supporter-pony/supporter-pony.ts @@ -9,80 +9,80 @@ import { CharacterPreview } from '../character-preview/character-preview'; import { decompressPonyString } from '../../../common/compressPony'; const BLEP: Expression = { - ...defaultExpression, - muzzle: Muzzle.Blep, + ...defaultExpression, + muzzle: Muzzle.Blep, }; const EXCITED: Expression = { - ...defaultExpression, - muzzle: Muzzle.SmileOpen, + ...defaultExpression, + muzzle: Muzzle.SmileOpen, }; const DERP: Expression = { - ...defaultExpression, - muzzle: Muzzle.SmileOpen, - leftIris: Iris.Up, + ...defaultExpression, + muzzle: Muzzle.SmileOpen, + leftIris: Iris.Up, }; @Component({ - selector: 'supporter-pony', - templateUrl: 'supporter-pony.pug', + selector: 'supporter-pony', + templateUrl: 'supporter-pony.pug', }) export class SupporterPony implements OnInit, OnDestroy { - @ViewChild('characterPreview', { static: true }) characterPreview!: CharacterPreview; - @Input() scale = 3; - pony = decompressPonyString(SUPPORTER_PONY); - state = defaultPonyState(); - private expression?: Expression; - private headAnimation?: HeadAnimation; - private headTime = 0; - private loop: FrameLoop; - constructor(frameService: FrameService) { - this.loop = frameService.create(delta => this.tick(delta)); - } - ngOnInit() { - this.loop.init(); - } - ngOnDestroy() { - this.loop.destroy(); - } - excite() { - this.headTime = 0; - this.headAnimation = excite; - this.expression = Math.random() < 0.2 ? DERP : EXCITED; - } - reset() { - this.expression = undefined; - } - private tick(delta: number) { - this.headTime += delta; + @ViewChild('characterPreview', { static: true }) characterPreview!: CharacterPreview; + @Input() scale = 3; + pony = decompressPonyString(SUPPORTER_PONY); + state = defaultPonyState(); + private expression?: Expression; + private headAnimation?: HeadAnimation; + private headTime = 0; + private loop: FrameLoop; + constructor(frameService: FrameService) { + this.loop = frameService.create(delta => this.tick(delta)); + } + ngOnInit() { + this.loop.init(); + } + ngOnDestroy() { + this.loop.destroy(); + } + excite() { + this.headTime = 0; + this.headAnimation = excite; + this.expression = Math.random() < 0.2 ? DERP : EXCITED; + } + reset() { + this.expression = undefined; + } + private tick(delta: number) { + this.headTime += delta; - if (this.headAnimation) { - const frame = Math.floor(this.headTime * this.headAnimation.fps); + if (this.headAnimation) { + const frame = Math.floor(this.headTime * this.headAnimation.fps); - if (frame >= this.headAnimation.frames.length && !this.headAnimation.loop) { - this.headAnimation = undefined; - this.state.headAnimation = undefined; - this.state.headAnimationFrame = 0; - this.characterPreview.blink(); - } else { - this.state.headAnimation = this.headAnimation; - this.state.headAnimationFrame = frame % this.headAnimation.frames.length; - } - } else { - this.state.headAnimation = undefined; + if (frame >= this.headAnimation.frames.length && !this.headAnimation.loop) { + this.headAnimation = undefined; + this.state.headAnimation = undefined; + this.state.headAnimationFrame = 0; + this.characterPreview.blink(); + } else { + this.state.headAnimation = this.headAnimation; + this.state.headAnimationFrame = frame % this.headAnimation.frames.length; + } + } else { + this.state.headAnimation = undefined; - if (this.expression) { - if (Math.random() < 0.01) { - this.expression = undefined; - } - } else { - if (Math.random() < 0.005) { - this.expression = BLEP; - } - } - } + if (this.expression) { + if (Math.random() < 0.01) { + this.expression = undefined; + } + } else { + if (Math.random() < 0.005) { + this.expression = BLEP; + } + } + } - this.state.expression = this.expression; - } + this.state.expression = this.expression; + } } diff --git a/src/ts/components/shared/swap-box/swap-box.ts b/src/ts/components/shared/swap-box/swap-box.ts index cc7a964..8533c4c 100644 --- a/src/ts/components/shared/swap-box/swap-box.ts +++ b/src/ts/components/shared/swap-box/swap-box.ts @@ -8,36 +8,36 @@ import { Model } from '../../services/model'; // import { SWAP_TIMEOUT, SECOND } from '../../../common/constants'; @Component({ - selector: 'swap-box', - templateUrl: 'swap-box.pug', - styleUrls: ['swap-box.scss'], + selector: 'swap-box', + templateUrl: 'swap-box.pug', + styleUrls: ['swap-box.scss'], }) export class SwapBox { - readonly swapIcon = faExchangeAlt; - readonly timerIcon = faClock; - previewInfo: any; - @ViewChild('dropdown', { static: true }) dropdown!: Dropdown; - timeout = false; - constructor(private game: PonyTownGame, private zone: NgZone, private model: Model) { - } - toggleSwapDropdown() { - this.zone.run(() => setTimeout(() => { }, 10)); - } - swapPony(pony: PonyObject) { - this.game.send(server => server.actionParam(Action.SwapCharacter, pony.id)); - setTimeout(() => { - this.dropdown && this.dropdown.close(); - pony.lastUsed = (new Date()).toISOString(); - this.model.sortPonies(); - }); + readonly swapIcon = faExchangeAlt; + readonly timerIcon = faClock; + previewInfo: any; + @ViewChild('dropdown', { static: true }) dropdown!: Dropdown; + timeout = false; + constructor(private game: PonyTownGame, private zone: NgZone, private model: Model) { + } + toggleSwapDropdown() { + this.zone.run(() => setTimeout(() => { }, 10)); + } + swapPony(pony: PonyObject) { + this.game.send(server => server.actionParam(Action.SwapCharacter, pony.id)); + setTimeout(() => { + this.dropdown && this.dropdown.close(); + pony.lastUsed = (new Date()).toISOString(); + this.model.sortPonies(); + }); - // if (!this.timeout) { - // this.timeout = true; - // setTimeout(() => this.timeout = false, SWAP_TIMEOUT + SECOND); - // } - } - preview(pony: PonyObject | undefined) { - const info = pony && pony.ponyInfo; - this.previewInfo = info && toPalette(info, mockPaletteManager); - } + // if (!this.timeout) { + // this.timeout = true; + // setTimeout(() => this.timeout = false, SWAP_TIMEOUT + SECOND); + // } + } + preview(pony: PonyObject | undefined) { + const info = pony && pony.ponyInfo; + this.previewInfo = info && toPalette(info, mockPaletteManager); + } } diff --git a/src/ts/components/shared/tabset/tabset.ts b/src/ts/components/shared/tabset/tabset.ts index 7c7a43b..6386076 100644 --- a/src/ts/components/shared/tabset/tabset.ts +++ b/src/ts/components/shared/tabset/tabset.ts @@ -1,93 +1,93 @@ import { - Component, Directive, TemplateRef, Input, ContentChild, ContentChildren, QueryList, Output, EventEmitter + Component, Directive, TemplateRef, Input, ContentChild, ContentChildren, QueryList, Output, EventEmitter } from '@angular/core'; import { uniqueId } from 'lodash'; import { Key } from '../../../client/input/input'; @Directive({ - selector: '[tabTitle]' + selector: '[tabTitle]' }) export class TabTitle { - constructor(public templateRef: TemplateRef) { - } + constructor(public templateRef: TemplateRef) { + } } @Directive({ - selector: '[tabContent]', + selector: '[tabContent]', }) export class TabContent { - constructor(public templateRef: TemplateRef) { - } + constructor(public templateRef: TemplateRef) { + } } @Directive({ - selector: 'tab', + selector: 'tab', }) export class Tab { - @Input() id = uniqueId(`tabset-tab`); - @Input() title?: string; - @Input() icon?: any; - @Input() disabled = false; - @ContentChild(TabContent, { static: false }) contentTpl?: TabContent; - @ContentChild(TabTitle, { static: false }) titleTpl?: TabTitle; + @Input() id = uniqueId(`tabset-tab`); + @Input() title?: string; + @Input() icon?: any; + @Input() disabled = false; + @ContentChild(TabContent, { static: false }) contentTpl?: TabContent; + @ContentChild(TabTitle, { static: false }) titleTpl?: TabTitle; } @Component({ - selector: 'tabset', - templateUrl: 'tabset.pug', + selector: 'tabset', + templateUrl: 'tabset.pug', }) export class Tabset { - justifyClass?: string; - @ContentChildren(Tab) tabs!: QueryList; - @Input() label = ''; - @Input() destroyOnHide = true; - @Input() - set justify(className: 'start' | 'center' | 'end' | 'fill' | 'justified') { - if (className === 'fill' || className === 'justified') { - this.justifyClass = `nav-${className}`; - } else { - this.justifyClass = `justify-content-${className}`; - } - } - @Input() orientation: 'horizontal' | 'vertical' = 'horizontal'; - @Input() type: 'tabs' | 'pills' = 'tabs'; - @Input() activeIndex = 0; - @Output() activeIndexChange = new EventEmitter(); - constructor() { - this.justify = 'start'; - } - get navClass() { - return `nav-${this.type}${this.orientation === 'horizontal' ? ` ${this.justifyClass}` : ' flex-column'}`; - } - select(index: number) { - if (this.activeIndex !== index) { - this.activeIndex = index; - this.activeIndexChange.emit(index); - } - } - keydown(e: KeyboardEvent) { - const index = this.handleKey(e.keyCode); + justifyClass?: string; + @ContentChildren(Tab) tabs!: QueryList; + @Input() label = ''; + @Input() destroyOnHide = true; + @Input() + set justify(className: 'start' | 'center' | 'end' | 'fill' | 'justified') { + if (className === 'fill' || className === 'justified') { + this.justifyClass = `nav-${className}`; + } else { + this.justifyClass = `justify-content-${className}`; + } + } + @Input() orientation: 'horizontal' | 'vertical' = 'horizontal'; + @Input() type: 'tabs' | 'pills' = 'tabs'; + @Input() activeIndex = 0; + @Output() activeIndexChange = new EventEmitter(); + constructor() { + this.justify = 'start'; + } + get navClass() { + return `nav-${this.type}${this.orientation === 'horizontal' ? ` ${this.justifyClass}` : ' flex-column'}`; + } + select(index: number) { + if (this.activeIndex !== index) { + this.activeIndex = index; + this.activeIndexChange.emit(index); + } + } + keydown(e: KeyboardEvent) { + const index = this.handleKey(e.keyCode); - if (index !== undefined) { - e.preventDefault(); - const element = document.getElementById(this.tabs.toArray()[index].id); - element && element.focus(); - this.select(index); - } - } - private handleKey(keyCode: number) { - if (keyCode === Key.LEFT) { - return this.activeIndex === 0 ? this.tabs.length - 1 : this.activeIndex - 1; - } else if (keyCode === Key.RIGHT) { - return this.activeIndex === this.tabs.length - 1 ? 0 : this.activeIndex + 1; - } else if (keyCode === Key.HOME) { - return 0; - } else if (keyCode === Key.END) { - return this.tabs.length - 1; - } else { - return undefined; - } - } + if (index !== undefined) { + e.preventDefault(); + const element = document.getElementById(this.tabs.toArray()[index].id); + element && element.focus(); + this.select(index); + } + } + private handleKey(keyCode: number) { + if (keyCode === Key.LEFT) { + return this.activeIndex === 0 ? this.tabs.length - 1 : this.activeIndex - 1; + } else if (keyCode === Key.RIGHT) { + return this.activeIndex === this.tabs.length - 1 ? 0 : this.activeIndex + 1; + } else if (keyCode === Key.HOME) { + return 0; + } else if (keyCode === Key.END) { + return this.tabs.length - 1; + } else { + return undefined; + } + } } export const tabsetComponents = [TabContent, TabTitle, Tabset, Tab]; diff --git a/src/ts/components/shared/virtual-list/virtual-list.ts b/src/ts/components/shared/virtual-list/virtual-list.ts index b83361a..de7702f 100644 --- a/src/ts/components/shared/virtual-list/virtual-list.ts +++ b/src/ts/components/shared/virtual-list/virtual-list.ts @@ -1,167 +1,167 @@ import { - Directive, DoCheck, Input, ViewContainerRef, TemplateRef, IterableDiffers, IterableDiffer, - EmbeddedViewRef, Component, ElementRef, ViewChild, NgZone, OnDestroy, ChangeDetectorRef, AfterViewInit + Directive, DoCheck, Input, ViewContainerRef, TemplateRef, IterableDiffers, IterableDiffer, + EmbeddedViewRef, Component, ElementRef, ViewChild, NgZone, OnDestroy, ChangeDetectorRef, AfterViewInit } from '@angular/core'; interface Context { - $implicit: T; - index: number; - count: number; - _currentIndex: number; + $implicit: T; + index: number; + count: number; + _currentIndex: number; } @Component({ - selector: 'virtual-list', - template: '
', - styleUrls: ['virtual-list.scss'], - host: { - 'tabindex': '0', - }, + selector: 'virtual-list', + template: '
', + styleUrls: ['virtual-list.scss'], + host: { + 'tabindex': '0', + }, }) export class VirtualList { - @Input() itemSize = 50; - @ViewChild('padStart', { static: true }) padStart!: ElementRef; - @ViewChild('padEnd', { static: true }) padEnd!: ElementRef; - constructor(public element: ElementRef) { - } + @Input() itemSize = 50; + @ViewChild('padStart', { static: true }) padStart!: ElementRef; + @ViewChild('padEnd', { static: true }) padEnd!: ElementRef; + constructor(public element: ElementRef) { + } } @Directive({ - selector: '[virtualFor][virtualForOf]', + selector: '[virtualFor][virtualForOf]', }) export class VirtualFor implements DoCheck, OnDestroy, AfterViewInit { - @Input() - set virtualForOf(forOf: T[]) { - this.forOf = forOf; - this.forOfDirty = true; - } - private forOf!: T[]; - private forOfDirty: boolean = true; - private differ: IterableDiffer | null = null; - private first = 0; - private last = 0; - constructor( - private viewContainer: ViewContainerRef, - private template: TemplateRef>, - private differs: IterableDiffers, - private list: VirtualList, - private changeDetector: ChangeDetectorRef, - zone: NgZone, - ) { - zone.runOutsideAngular(() => { - list.element.nativeElement.addEventListener('scroll', this.detect); - window.addEventListener('resize', this.detect); - }); - } - private detect = () => this.changeDetector.detectChanges(); - @Input() - set virtualForTemplate(value: TemplateRef>) { - if (value) { - this.template = value; - } - } - ngOnDestroy() { - this.list.element.nativeElement.removeEventListener('scroll', this.detect); - window.removeEventListener('resize', this.detect); - } - ngAfterViewInit() { - setTimeout(this.detect, 0); - } - ngDoCheck(): void { - if (this.forOfDirty) { - this.forOfDirty = false; - const value = this.forOf; + @Input() + set virtualForOf(forOf: T[]) { + this.forOf = forOf; + this.forOfDirty = true; + } + private forOf!: T[]; + private forOfDirty: boolean = true; + private differ: IterableDiffer | null = null; + private first = 0; + private last = 0; + constructor( + private viewContainer: ViewContainerRef, + private template: TemplateRef>, + private differs: IterableDiffers, + private list: VirtualList, + private changeDetector: ChangeDetectorRef, + zone: NgZone, + ) { + zone.runOutsideAngular(() => { + list.element.nativeElement.addEventListener('scroll', this.detect); + window.addEventListener('resize', this.detect); + }); + } + private detect = () => this.changeDetector.detectChanges(); + @Input() + set virtualForTemplate(value: TemplateRef>) { + if (value) { + this.template = value; + } + } + ngOnDestroy() { + this.list.element.nativeElement.removeEventListener('scroll', this.detect); + window.removeEventListener('resize', this.detect); + } + ngAfterViewInit() { + setTimeout(this.detect, 0); + } + ngDoCheck(): void { + if (this.forOfDirty) { + this.forOfDirty = false; + const value = this.forOf; - if (!this.differ && value) { - try { - this.differ = this.differs.find(value).create(); - } catch { - throw new Error(`Cannot find a differ`); - } - } - } + if (!this.differ && value) { + try { + this.differ = this.differs.find(value).create(); + } catch { + throw new Error(`Cannot find a differ`); + } + } + } - const changes = this.differ && this.differ.diff(this.forOf); - const element = this.list.element.nativeElement as HTMLElement; - const itemSize = this.list.itemSize; - const { height } = element.getBoundingClientRect(); - const scroll = element.scrollTop; - const first = Math.floor(scroll / itemSize); - const last = first + Math.ceil(height / itemSize); - let scrollChanged = false; + const changes = this.differ && this.differ.diff(this.forOf); + const element = this.list.element.nativeElement as HTMLElement; + const itemSize = this.list.itemSize; + const { height } = element.getBoundingClientRect(); + const scroll = element.scrollTop; + const first = Math.floor(scroll / itemSize); + const last = first + Math.ceil(height / itemSize); + let scrollChanged = false; - if (this.first !== first || this.last !== last) { - this.first = first; - this.last = last; - scrollChanged = true; - } + if (this.first !== first || this.last !== last) { + this.first = first; + this.last = last; + scrollChanged = true; + } - if (changes || scrollChanged) { - this.applyChanges(); - } - } - private applyChanges() { - const viewContainer = this.viewContainer; - const first = this.first; - const last = this.last; - const forOf = this.forOf; - const actualLast = Math.min(last, forOf.length - 1); + if (changes || scrollChanged) { + this.applyChanges(); + } + } + private applyChanges() { + const viewContainer = this.viewContainer; + const first = this.first; + const last = this.last; + const forOf = this.forOf; + const actualLast = Math.min(last, forOf.length - 1); - type Ref = EmbeddedViewRef>; - const insertTuples: { item: T; view: Ref; }[] = []; - const views: Ref[] = []; + type Ref = EmbeddedViewRef>; + const insertTuples: { item: T; view: Ref; }[] = []; + const views: Ref[] = []; - for (let i = viewContainer.length - 1; i >= 0; i--) { - const ref = viewContainer.get(i) as Ref; + for (let i = viewContainer.length - 1; i >= 0; i--) { + const ref = viewContainer.get(i) as Ref; - if (ref.context._currentIndex < first || ref.context._currentIndex > actualLast) { - viewContainer.detach(i); - views.push(ref); - } - } + if (ref.context._currentIndex < first || ref.context._currentIndex > actualLast) { + viewContainer.detach(i); + views.push(ref); + } + } - for (let index = first, i = 0; index <= actualLast; index++ , i++) { - if (viewContainer.length <= i || (viewContainer.get(i) as Ref).context._currentIndex !== index) { - let view = views.pop(); + for (let index = first, i = 0; index <= actualLast; index++ , i++) { + if (viewContainer.length <= i || (viewContainer.get(i) as Ref).context._currentIndex !== index) { + let view = views.pop(); - if (view) { - view.context.$implicit = null!; - view.context._currentIndex = index; - viewContainer.insert(view, i); - } else { - const context: Context = { $implicit: null!, index: -1, count: -1, _currentIndex: index }; - view = viewContainer.createEmbeddedView(this.template, context, i); - } + if (view) { + view.context.$implicit = null!; + view.context._currentIndex = index; + viewContainer.insert(view, i); + } else { + const context: Context = { $implicit: null!, index: -1, count: -1, _currentIndex: index }; + view = viewContainer.createEmbeddedView(this.template, context, i); + } - insertTuples.push({ item: forOf[index], view }); - } - } + insertTuples.push({ item: forOf[index], view }); + } + } - if (DEVELOPMENT && viewContainer.length !== (actualLast - first + 1)) { - console.error('virtual-list: Invalid length', viewContainer.length, first, actualLast); - } + if (DEVELOPMENT && viewContainer.length !== (actualLast - first + 1)) { + console.error('virtual-list: Invalid length', viewContainer.length, first, actualLast); + } - for (const view of views) { - view.destroy(); - } + for (const view of views) { + view.destroy(); + } - for (let i = 0; i < insertTuples.length; i++) { - insertTuples[i].view.context.$implicit = insertTuples[i].item; - } + for (let i = 0; i < insertTuples.length; i++) { + insertTuples[i].view.context.$implicit = insertTuples[i].item; + } - const count = forOf.length; + const count = forOf.length; - for (let i = 0, ilen = viewContainer.length; i < ilen; i++) { - const viewRef = viewContainer.get(i) as Ref; - viewRef.context.$implicit = forOf[first + i]; - viewRef.context.index = first + i; - viewRef.context.count = count; - } + for (let i = 0, ilen = viewContainer.length; i < ilen; i++) { + const viewRef = viewContainer.get(i) as Ref; + viewRef.context.$implicit = forOf[first + i]; + viewRef.context.index = first + i; + viewRef.context.count = count; + } - const itemSize = this.list.itemSize; - this.list.padStart.nativeElement.style.height = `${first * itemSize}px`; - this.list.padEnd.nativeElement.style.height = `${(forOf.length - actualLast - 1) * itemSize}px`; - } + const itemSize = this.list.itemSize; + this.list.padStart.nativeElement.style.height = `${first * itemSize}px`; + this.list.padEnd.nativeElement.style.height = `${(forOf.length - actualLast - 1) * itemSize}px`; + } } export const virtualListDirectives = [VirtualFor]; diff --git a/src/ts/components/tools/shared/tools-frame/tools-frame.ts b/src/ts/components/tools/shared/tools-frame/tools-frame.ts index ca64435..928889b 100644 --- a/src/ts/components/tools/shared/tools-frame/tools-frame.ts +++ b/src/ts/components/tools/shared/tools-frame/tools-frame.ts @@ -4,73 +4,73 @@ import { Sprite, PonyInfo } from '../../../../common/interfaces'; let openedPopover: ToolsFrame; @Component({ - selector: 'tools-frame', - templateUrl: 'tools-frame.pug', - styleUrls: ['tools-frame.scss'], + selector: 'tools-frame', + templateUrl: 'tools-frame.pug', + styleUrls: ['tools-frame.scss'], }) export class ToolsFrame { - @Input() x = 0; - @Input() y = 0; - @Input() sprites!: Sprite[]; - @Input() frame!: number; - @Input() pony!: PonyInfo; - @Input() reverseExtra = false; - @Input() circle?: string; - @Output() frameChange = new EventEmitter(); - popoverIsOpen = false; - placement = 'right'; - private savedFrame = 0; - private selected = false; - constructor(private element: ElementRef) { - } - get sprite() { - return this.sprites[this.frame]; - } - closePopover = () => { - if (this.popoverIsOpen) { - this.togglePopover(); - } - } - togglePopover() { - const rect = (this.element.nativeElement as HTMLElement).getBoundingClientRect(); - this.placement = (rect.left < (window.innerWidth / 2)) ? 'right' : 'left'; + @Input() x = 0; + @Input() y = 0; + @Input() sprites!: Sprite[]; + @Input() frame!: number; + @Input() pony!: PonyInfo; + @Input() reverseExtra = false; + @Input() circle?: string; + @Output() frameChange = new EventEmitter(); + popoverIsOpen = false; + placement = 'right'; + private savedFrame = 0; + private selected = false; + constructor(private element: ElementRef) { + } + get sprite() { + return this.sprites[this.frame]; + } + closePopover = () => { + if (this.popoverIsOpen) { + this.togglePopover(); + } + } + togglePopover() { + const rect = (this.element.nativeElement as HTMLElement).getBoundingClientRect(); + this.placement = (rect.left < (window.innerWidth / 2)) ? 'right' : 'left'; - if (!this.popoverIsOpen) { - if (openedPopover) { - openedPopover.popoverIsOpen = false; - } + if (!this.popoverIsOpen) { + if (openedPopover) { + openedPopover.popoverIsOpen = false; + } - openedPopover = this; - } + openedPopover = this; + } - this.popoverIsOpen = !this.popoverIsOpen; + this.popoverIsOpen = !this.popoverIsOpen; - if (this.popoverIsOpen) { - this.selected = false; - window.addEventListener('mousedown', this.closePopover); - } else { - window.removeEventListener('mousedown', this.closePopover); - } + if (this.popoverIsOpen) { + this.selected = false; + window.addEventListener('mousedown', this.closePopover); + } else { + window.removeEventListener('mousedown', this.closePopover); + } - setTimeout(() => { }, 10); - } - select(index: number) { - this.selected = true; - this.frame = index; - this.togglePopover(); - this.frameChange.emit(this.frame); - } - enter(index: number) { - if (!this.selected) { - this.savedFrame = this.frame; - this.frame = index; - this.frameChange.emit(this.frame); - } - } - leave() { - if (!this.selected) { - this.frame = this.savedFrame; - this.frameChange.emit(this.frame); - } - } + setTimeout(() => { }, 10); + } + select(index: number) { + this.selected = true; + this.frame = index; + this.togglePopover(); + this.frameChange.emit(this.frame); + } + enter(index: number) { + if (!this.selected) { + this.savedFrame = this.frame; + this.frame = index; + this.frameChange.emit(this.frame); + } + } + leave() { + if (!this.selected) { + this.frame = this.savedFrame; + this.frameChange.emit(this.frame); + } + } } diff --git a/src/ts/components/tools/shared/tools-offset/tools-offset.ts b/src/ts/components/tools/shared/tools-offset/tools-offset.ts index f48e04e..2d9e909 100644 --- a/src/ts/components/tools/shared/tools-offset/tools-offset.ts +++ b/src/ts/components/tools/shared/tools-offset/tools-offset.ts @@ -3,27 +3,27 @@ import { Point } from '../../../../common/interfaces'; import { faChevronRight, faChevronLeft, faChevronDown, faChevronUp } from '../../../../client/icons'; @Component({ - selector: 'tools-offset', - templateUrl: 'tools-offset.pug', - styleUrls: ['tools-offset.scss'], + selector: 'tools-offset', + templateUrl: 'tools-offset.pug', + styleUrls: ['tools-offset.scss'], }) export class ToolsOffset { - readonly rightIcon = faChevronRight; - readonly leftIcon = faChevronLeft; - readonly upIcon = faChevronUp; - readonly downIcon = faChevronDown; - @Input() offset?: Point; - @Output() change = new EventEmitter(); - moveX(value: number) { - if (this.offset) { - this.offset.x += value; - this.change.emit(); - } - } - moveY(value: number) { - if (this.offset) { - this.offset.y += value; - this.change.emit(); - } - } + readonly rightIcon = faChevronRight; + readonly leftIcon = faChevronLeft; + readonly upIcon = faChevronUp; + readonly downIcon = faChevronDown; + @Input() offset?: Point; + @Output() change = new EventEmitter(); + moveX(value: number) { + if (this.offset) { + this.offset.x += value; + this.change.emit(); + } + } + moveY(value: number) { + if (this.offset) { + this.offset.y += value; + this.change.emit(); + } + } } diff --git a/src/ts/components/tools/shared/tools-range/tools-range.ts b/src/ts/components/tools/shared/tools-range/tools-range.ts index bebcd08..be0bea0 100644 --- a/src/ts/components/tools/shared/tools-range/tools-range.ts +++ b/src/ts/components/tools/shared/tools-range/tools-range.ts @@ -3,53 +3,53 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { faChevronRight, faChevronLeft, faChevronUp, faChevronDown } from '../../../../client/icons'; @Component({ - selector: 'tools-range', - templateUrl: 'tools-range.pug', - styleUrls: ['tools-range.scss'], - providers: [ - { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToolsRange), multi: true }, - ], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'tools-range', + templateUrl: 'tools-range.pug', + styleUrls: ['tools-range.scss'], + providers: [ + { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToolsRange), multi: true }, + ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ToolsRange implements ControlValueAccessor { - readonly rightIcon = faChevronRight; - readonly leftIcon = faChevronLeft; - readonly upIcon = faChevronUp; - readonly downIcon = faChevronDown; - @Input() min = 0; - @Input() max = 100; - @Input() vertical = false; - @Input() small = false; - @Input() placeholder?: string; - @Output() change = new EventEmitter(); - private _value = 0; - private propagateChange: any = () => { }; - get value() { - return this._value; - } - set value(value: number) { - this._value = value; - this.propagateChange(value); - this.change.emit(); - } - decrement() { - if (this.value > this.min) { - this.value = this.value - 1; - } - } - increment() { - if (this.value < this.max) { - this.value = this.value + 1; - } - } - writeValue(value: number | undefined) { - if (value !== undefined) { - this.value = value; - } - } - registerOnChange(callback: any) { - this.propagateChange = callback; - } - registerOnTouched() { - } + readonly rightIcon = faChevronRight; + readonly leftIcon = faChevronLeft; + readonly upIcon = faChevronUp; + readonly downIcon = faChevronDown; + @Input() min = 0; + @Input() max = 100; + @Input() vertical = false; + @Input() small = false; + @Input() placeholder?: string; + @Output() change = new EventEmitter(); + private _value = 0; + private propagateChange: any = () => { }; + get value() { + return this._value; + } + set value(value: number) { + this._value = value; + this.propagateChange(value); + this.change.emit(); + } + decrement() { + if (this.value > this.min) { + this.value = this.value - 1; + } + } + increment() { + if (this.value < this.max) { + this.value = this.value + 1; + } + } + writeValue(value: number | undefined) { + if (value !== undefined) { + this.value = value; + } + } + registerOnChange(callback: any) { + this.propagateChange = callback; + } + registerOnTouched() { + } } diff --git a/src/ts/components/tools/shared/tools-xy/tools-xy.ts b/src/ts/components/tools/shared/tools-xy/tools-xy.ts index 0d3863f..7d29051 100644 --- a/src/ts/components/tools/shared/tools-xy/tools-xy.ts +++ b/src/ts/components/tools/shared/tools-xy/tools-xy.ts @@ -2,31 +2,31 @@ import { Component, Input, ChangeDetectionStrategy, EventEmitter, Output } from import { faChevronRight, faChevronLeft, faChevronUp, faChevronDown } from '../../../../client/icons'; @Component({ - selector: 'tools-xy', - templateUrl: 'tools-xy.pug', - styleUrls: ['tools-xy.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'tools-xy', + templateUrl: 'tools-xy.pug', + styleUrls: ['tools-xy.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ToolsXY { - readonly rightIcon = faChevronRight; - readonly leftIcon = faChevronLeft; - readonly upIcon = faChevronUp; - readonly downIcon = faChevronDown; - @Input() min = 0; - @Input() max = 100; - @Input() x = 0; - @Input() y = 0; - @Output() xChange = new EventEmitter(); - @Output() yChange = new EventEmitter(); - @Output() change = new EventEmitter(); - changeX(value: number) { - this.x = value; - this.xChange.emit(value); - this.change.emit(); - } - changeY(value: number) { - this.y = value; - this.yChange.emit(value); - this.change.emit(); - } + readonly rightIcon = faChevronRight; + readonly leftIcon = faChevronLeft; + readonly upIcon = faChevronUp; + readonly downIcon = faChevronDown; + @Input() min = 0; + @Input() max = 100; + @Input() x = 0; + @Input() y = 0; + @Output() xChange = new EventEmitter(); + @Output() yChange = new EventEmitter(); + @Output() change = new EventEmitter(); + changeX(value: number) { + this.x = value; + this.xChange.emit(value); + this.change.emit(); + } + changeY(value: number) { + this.y = value; + this.yChange.emit(value); + this.change.emit(); + } } diff --git a/src/ts/components/tools/sheetExport.ts b/src/ts/components/tools/sheetExport.ts index 563a5ed..dde7510 100644 --- a/src/ts/components/tools/sheetExport.ts +++ b/src/ts/components/tools/sheetExport.ts @@ -23,336 +23,336 @@ const patternColors = [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, BLACK]; const whiteColors = [WHITE, WHITE, WHITE, WHITE, WHITE, WHITE, WHITE]; function maxPatterns(sprites: Sets): number { - return max(sprites.map(s => s && s.length ? max(s.map(x => x ? x.length : 0)) : 0))!; + return max(sprites.map(s => s && s.length ? max(s.map(x => x ? x.length : 0)) : 0))!; } const backupSprites: any = { - head: [ - undefined, - [ - [ - {} - ], - ], - ], + head: [ + undefined, + [ + [ + {} + ], + ], + ], }; function getSets(sheet: Sheet, key: string, override?: string): Sets | undefined { - const setsKey = override || key || ''; - const sets = backupSprites[setsKey] || (sprites as any)[setsKey]; + const setsKey = override || key || ''; + const sets = backupSprites[setsKey] || (sprites as any)[setsKey]; - if (sheet.duplicateFirstFrame !== undefined) { - return times(sheet.duplicateFirstFrame, () => sets[0]); - } else { - return sheet.single ? [sets] : sets; - } + if (sheet.duplicateFirstFrame !== undefined) { + return times(sheet.duplicateFirstFrame, () => sets[0]); + } else { + return sheet.single ? [sets] : sets; + } } function getSetsForFirstKey(sheet: Sheet) { - const layer = sheet.layers.find(l => !!l.set); - return layer && getSets(sheet, layer.set!); + const layer = sheet.layers.find(l => !!l.set); + return layer && getSets(sheet, layer.set!); } export function getCols(sheet: Sheet) { - return sheet.state!.animation.frames.length; + return sheet.state!.animation.frames.length; } export function getRows(sheet: Sheet) { - if (sheet.rows !== undefined) { - return sheet.rows; - } else { - const sets = getSetsForFirstKey(sheet); - const maxFrames = sets && max(sets.map(f => f ? f.length : 0)); - return (maxFrames || 0) + 1; - } + if (sheet.rows !== undefined) { + return sheet.rows; + } else { + const sets = getSetsForFirstKey(sheet); + const maxFrames = sets && max(sets.map(f => f ? f.length : 0)); + return (maxFrames || 0) + 1; + } } export function savePsd(psd: Psd, name: string) { - saveAs(new Blob([writePsd(psd, { generateThumbnail: true })], { type: 'application/octet-stream' }), name); + saveAs(new Blob([writePsd(psd, { generateThumbnail: true })], { type: 'application/octet-stream' }), name); } export function createPsd(sheet: Sheet, rows: number, cols: number): Psd { - const width = canvasWidth(sheet, rows, cols); - const height = canvasHeight(sheet, rows, cols); + const width = canvasWidth(sheet, rows, cols); + const height = canvasHeight(sheet, rows, cols); - return { - width, - height, - children: compact([ - { name: '', canvas: createBackground(rows, cols, width, height, sheet), transparencyProtected: true }, - ...sheet.layers!.map(layer => createPsdLayer(sheet, rows, cols, layer)), - { name: '', canvas: createRefsCanvas(width, height, sheet.paletteOffsetY) }, - ]), - }; + return { + width, + height, + children: compact([ + { name: '', canvas: createBackground(rows, cols, width, height, sheet), transparencyProtected: true }, + ...sheet.layers!.map(layer => createPsdLayer(sheet, rows, cols, layer)), + { name: '', canvas: createRefsCanvas(width, height, sheet.paletteOffsetY) }, + ]), + }; } function canvasWidth(sheet: Sheet, _rows: number, cols: number) { - if (sheet.wrap) { - cols = sheet.wrap; - } + if (sheet.wrap) { + cols = sheet.wrap; + } - return (sheet.offset * (cols - 1)) + sheet.width; + return (sheet.offset * (cols - 1)) + sheet.width; } function canvasHeight(sheet: Sheet, rows: number, _cols: number) { - if (sheet.wrap) { - rows = Math.ceil(rows / sheet.wrap); - } + if (sheet.wrap) { + rows = Math.ceil(rows / sheet.wrap); + } - return sheet.height * rows; + return sheet.height * rows; } function drawPsdLayer( - sheet: Sheet, rows: number, cols: number, layer: SheetLayer, pattern = -1, extra = false + sheet: Sheet, rows: number, cols: number, layer: SheetLayer, pattern = -1, extra = false ): HTMLCanvasElement { - const { width, height, offset, offsetY = 0, wrap } = sheet; - const pony = createPony(); - const baseState = { ...defaultPonyState(), ...sheet.state, blushColor: BLACK }; - const options = { ...defaultDrawPonyOptions(), ...layer.options }; - const ignoreColor = (layer.drawBlack === undefined ? !!layer.head : layer.drawBlack) ? TRANSPARENT : BLACK; - const fieldName = layer.fieldName || sheet.fieldName; + const { width, height, offset, offsetY = 0, wrap } = sheet; + const pony = createPony(); + const baseState = { ...defaultPonyState(), ...sheet.state, blushColor: BLACK }; + const options = { ...defaultDrawPonyOptions(), ...layer.options }; + const ignoreColor = (layer.drawBlack === undefined ? !!layer.head : layer.drawBlack) ? TRANSPARENT : BLACK; + const fieldName = layer.fieldName || sheet.fieldName; - if (!layer.head) { - baseState.headAnimation = createHeadAnimation('', 1, false, [[]]); - pony.head = ignoreSet(); - pony.nose = ignoreSet(); - pony.ears = ignoreSet(); - } else if (layer.noFace) { - baseState.headAnimation = createHeadAnimation('', 1, false, [[]]); - pony.head = ignoreSet(); - pony.nose = ignoreSet(); - } + if (!layer.head) { + baseState.headAnimation = createHeadAnimation('', 1, false, [[]]); + pony.head = ignoreSet(); + pony.nose = ignoreSet(); + pony.ears = ignoreSet(); + } else if (layer.noFace) { + baseState.headAnimation = createHeadAnimation('', 1, false, [[]]); + pony.head = ignoreSet(); + pony.nose = ignoreSet(); + } - if (!layer.body) { - options.no = setFlag(options.no, NoDraw.BodyOnly, true); - } + if (!layer.body) { + options.no = setFlag(options.no, NoDraw.BodyOnly, true); + } - if (!layer.frontLeg) { - options.no = setFlag(options.no, NoDraw.FrontLeg, true); - } + if (!layer.frontLeg) { + options.no = setFlag(options.no, NoDraw.FrontLeg, true); + } - if (!layer.backLeg) { - options.no = setFlag(options.no, NoDraw.BackLeg, true); - } + if (!layer.backLeg) { + options.no = setFlag(options.no, NoDraw.BackLeg, true); + } - if (!layer.frontFarLeg) { - options.no = setFlag(options.no, NoDraw.FrontFarLeg, true); - } + if (!layer.frontFarLeg) { + options.no = setFlag(options.no, NoDraw.FrontFarLeg, true); + } - if (!layer.backFarLeg) { - options.no = setFlag(options.no, NoDraw.BackFarLeg, true); - } + if (!layer.backFarLeg) { + options.no = setFlag(options.no, NoDraw.BackFarLeg, true); + } - layer.setup && layer.setup(pony, baseState); + layer.setup && layer.setup(pony, baseState); - syncLockedPonyInfoNumber(pony); + syncLockedPonyInfoNumber(pony); - const actualRows = wrap ? Math.ceil(rows / wrap) : rows; - const actualCols = wrap ? wrap : cols; - const empties = sheet.empties && includes(sheet.setsWithEmpties, layer.set) ? sheet.empties : []; + const actualRows = wrap ? Math.ceil(rows / wrap) : rows; + const actualCols = wrap ? wrap : cols; + const empties = sheet.empties && includes(sheet.setsWithEmpties, layer.set) ? sheet.empties : []; - return drawFrames(actualRows, actualCols, width, height, offset, (batch, x, y) => { - const xIndexBase = (wrap ? actualCols * y + x : x); - const xIndexOffset = empties.filter(i => i <= xIndexBase).length; - const xIndex = includes(empties, xIndexBase) ? 0 : (xIndexBase - xIndexOffset); - const yIndex = wrap ? 0 : y; + return drawFrames(actualRows, actualCols, width, height, offset, (batch, x, y) => { + const xIndexBase = (wrap ? actualCols * y + x : x); + const xIndexOffset = empties.filter(i => i <= xIndexBase).length; + const xIndex = includes(empties, xIndexBase) ? 0 : (xIndexBase - xIndexOffset); + const yIndex = wrap ? 0 : y; - const state = cloneDeep(baseState); + const state = cloneDeep(baseState); - sheet.frame && sheet.frame(pony, state, options, xIndex, yIndex, pattern); - layer.frame && layer.frame(pony, state, options, xIndex, yIndex, pattern); + sheet.frame && sheet.frame(pony, state, options, xIndex, yIndex, pattern); + layer.frame && layer.frame(pony, state, options, xIndex, yIndex, pattern); - state.animationFrame = xIndex; + state.animationFrame = xIndex; - if (layer.set && fieldName) { - const sets = getSets(sheet, layer.set, layer.setOverride); + if (layer.set && fieldName) { + const sets = getSets(sheet, layer.set, layer.setOverride); - if (!sets) { - throw new Error(`Missing sets for (${layer.set})`); - } + if (!sets) { + throw new Error(`Missing sets for (${layer.set})`); + } - const frameIndex = sheet.single ? 0 : xIndex; - const typeIndex = sheet.single ? xIndex : yIndex; - const aframe = sets[frameIndex]; - const type = (aframe && typeIndex < aframe.length) ? typeIndex : -1; - const set: SpriteSet = { type }; + const frameIndex = sheet.single ? 0 : xIndex; + const typeIndex = sheet.single ? xIndex : yIndex; + const aframe = sets[frameIndex]; + const type = (aframe && typeIndex < aframe.length) ? typeIndex : -1; + const set: SpriteSet = { type }; - if (pattern !== -1) { - set.fills = patternColors; - set.outlines = patternColors; + if (pattern !== -1) { + set.fills = patternColors; + set.outlines = patternColors; - if (aframe && typeIndex < aframe.length && aframe[typeIndex] && pattern < aframe[typeIndex]!.length) { - set.pattern = pattern; - } else { - set.type = -1; - } - } else { - set.fills = whiteColors; - set.outlines = whiteColors; + if (aframe && typeIndex < aframe.length && aframe[typeIndex] && pattern < aframe[typeIndex]!.length) { + set.pattern = pattern; + } else { + set.type = -1; + } + } else { + set.fills = whiteColors; + set.outlines = whiteColors; - if (!(aframe && typeIndex < aframe.length && aframe[typeIndex])) { - set.type = -1; - } - } + if (!(aframe && typeIndex < aframe.length && aframe[typeIndex])) { + set.type = -1; + } + } - layer.frameSet && layer.frameSet(set, xIndex, yIndex, pattern); + layer.frameSet && layer.frameSet(set, xIndex, yIndex, pattern); - (pony as any)[fieldName] = set.type === -1 ? ignoreSet() : set; - } + (pony as any)[fieldName] = set.type === -1 ? ignoreSet() : set; + } - batch.disableShading = pattern !== -1; - batch.ignoreColor = ignoreColor; + batch.disableShading = pattern !== -1; + batch.ignoreColor = ignoreColor; - const pal = toPaletteNumber(pony); + const pal = toPaletteNumber(pony); - if (layer.extra !== undefined) { - const set = pal[layer.extra] as PaletteSpriteSet; + if (layer.extra !== undefined) { + const set = pal[layer.extra] as PaletteSpriteSet; - if (extra) { - set.palette = mockPaletteManager.add([0, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK]); - } else { - set.extraPalette = undefined; - } - } + if (extra) { + set.palette = mockPaletteManager.add([0, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK]); + } else { + set.extraPalette = undefined; + } + } - drawPony(batch, pal, state, PONY_X, PONY_Y + offsetY + toInt(layer.shiftY), options); - }); + drawPony(batch, pal, state, PONY_X, PONY_Y + offsetY + toInt(layer.shiftY), options); + }); } function createPsdPatternLayers(sheet: Sheet, rows: number, cols: number, layer: SheetLayer): Layer[] { - const sets = getSets(sheet, layer.set!, layer.setOverride); - const patterns = layer.patterns || (sets ? maxPatterns(sets) : 0) || 6; + const sets = getSets(sheet, layer.set!, layer.setOverride); + const patterns = layer.patterns || (sets ? maxPatterns(sets) : 0) || 6; - return compact([ - layer.extra && { - name: 'extra', - canvas: drawPsdLayer(sheet, rows, cols, layer, -1, true), - }, - { - name: 'color', - canvas: drawPsdLayer(sheet, rows, cols, layer), - }, - ...times(patterns, i => ({ - name: `pattern ${i}`, - canvas: drawPsdLayer(sheet, rows, cols, layer, i), - hidden: true, - clipping: true, - blendMode: 'multiply', - })), - ]); + return compact([ + layer.extra && { + name: 'extra', + canvas: drawPsdLayer(sheet, rows, cols, layer, -1, true), + }, + { + name: 'color', + canvas: drawPsdLayer(sheet, rows, cols, layer), + }, + ...times(patterns, i => ({ + name: `pattern ${i}`, + canvas: drawPsdLayer(sheet, rows, cols, layer, i), + hidden: true, + clipping: true, + blendMode: 'multiply', + })), + ]); } function createPsdLayer(sheet: Sheet, rows: number, cols: number, layer: SheetLayer): Layer { - const name = layer.name; + const name = layer.name; - if (layer.set) { - return { name, children: createPsdPatternLayers(sheet, rows, cols, layer) }; - } else { - return { name, canvas: drawPsdLayer(sheet, rows, cols, layer) }; - } + if (layer.set) { + return { name, children: createPsdPatternLayers(sheet, rows, cols, layer) }; + } else { + return { name, canvas: drawPsdLayer(sheet, rows, cols, layer) }; + } } function drawFrames( - rows: number, cols: number, w: number, h: number, offset: number, - draw: (batch: ContextSpriteBatch, x: number, y: number) => void + rows: number, cols: number, w: number, h: number, offset: number, + draw: (batch: ContextSpriteBatch, x: number, y: number) => void ): HTMLCanvasElement { - const canvas = createCanvas((offset * (cols - 1)) + w, h * rows); - const buffer = createCanvas(w, h); - const batch = new ContextSpriteBatch(buffer); - const viewContext = canvas.getContext('2d')!; - viewContext.save(); - disableImageSmoothing(viewContext); + const canvas = createCanvas((offset * (cols - 1)) + w, h * rows); + const buffer = createCanvas(w, h); + const batch = new ContextSpriteBatch(buffer); + const viewContext = canvas.getContext('2d')!; + viewContext.save(); + disableImageSmoothing(viewContext); - for (let y = 0; y < rows; y++) { - for (let x = 0; x < cols; x++) { - batch.start(sprites.paletteSpriteSheet, 0); - draw(batch, x, y); - batch.end(); - viewContext.drawImage(buffer, x * offset, y * h); - } - } + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + batch.start(sprites.paletteSpriteSheet, 0); + draw(batch, x, y); + batch.end(); + viewContext.drawImage(buffer, x * offset, y * h); + } + } - viewContext.restore(); - return canvas; + viewContext.restore(); + return canvas; } function createBackground(rows: number, cols: number, width: number, height: number, sheet: Sheet) { - const canvas = createCanvas(width, height); - const context = canvas.getContext('2d')!; + const canvas = createCanvas(width, height); + const context = canvas.getContext('2d')!; - if (sheet.wrap) { - cols = sheet.wrap; - rows = Math.ceil(rows / sheet.wrap); - } + if (sheet.wrap) { + cols = sheet.wrap; + rows = Math.ceil(rows / sheet.wrap); + } - fillRect(context, 'lightgreen', 0, 0, canvas.width, canvas.height); + fillRect(context, 'lightgreen', 0, 0, canvas.width, canvas.height); - context.globalAlpha = 0.1; + context.globalAlpha = 0.1; - for (let y = 0; y < rows; y++) { - for (let x = 0; x < cols; x++) { - const color = (x + (y % 2)) % 2 ? 'green' : 'blue'; - fillRect(context, color, x * sheet.offset, y * sheet.height, sheet.width, sheet.height); - } - } + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const color = (x + (y % 2)) % 2 ? 'green' : 'blue'; + fillRect(context, color, x * sheet.offset, y * sheet.height, sheet.width, sheet.height); + } + } - context.globalAlpha = 1; + context.globalAlpha = 1; - for (let y = 0; y < rows; y++) { - for (let x = 0; x < cols; x++) { - const gap = sheet.width - sheet.offset; - const index = sheet.wrap ? (cols * y + x) : x; - drawPixelTextOnCanvas(context, x * sheet.offset + gap + 2, y * sheet.height + 2, 0x76c189ff, index.toString()); - } - } + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const gap = sheet.width - sheet.offset; + const index = sheet.wrap ? (cols * y + x) : x; + drawPixelTextOnCanvas(context, x * sheet.offset + gap + 2, y * sheet.height + 2, 0x76c189ff, index.toString()); + } + } - return canvas; + return canvas; } function createRefsCanvas(width: number, height: number, offsetY = 0) { - const canvas = createCanvas(width, height); - const context = canvas.getContext('2d')!; - const h = 2; + const canvas = createCanvas(width, height); + const context = canvas.getContext('2d')!; + const h = 2; - patternColors.forEach((c, i) => fillRect(context, colorToCSS(c), 5, 5 + h * i + offsetY, 10, h)); + patternColors.forEach((c, i) => fillRect(context, colorToCSS(c), 5, 5 + h * i + offsetY, 10, h)); - fillRect(context, '#888888', 25, 10 + offsetY, 8, 10); - fillRect(context, '#d9d9d9', 27, 12 + offsetY, 4, 4); - fillRect(context, '#afafaf', 27, 16 + offsetY, 4, 2); - fillRect(context, '#9f9f9f', 20, 5 + offsetY, 8, 10); - fillRect(context, '#ffffff', 22, 7 + offsetY, 4, 4); - fillRect(context, '#cdcdcd', 22, 11 + offsetY, 4, 2); + fillRect(context, '#888888', 25, 10 + offsetY, 8, 10); + fillRect(context, '#d9d9d9', 27, 12 + offsetY, 4, 4); + fillRect(context, '#afafaf', 27, 16 + offsetY, 4, 2); + fillRect(context, '#9f9f9f', 20, 5 + offsetY, 8, 10); + fillRect(context, '#ffffff', 22, 7 + offsetY, 4, 4); + fillRect(context, '#cdcdcd', 22, 11 + offsetY, 4, 2); - return canvas; + return canvas; } function createPony(): PonyInfoNumber { - const pony = decompressPony(compressPonyString(createDefaultPony())); - pony.mane!.type = 0; - pony.backMane!.type = 0; - pony.tail!.type = 0; - pony.coatFill = DEFAULT_COLOR; - pony.lockCoatOutline = true; - pony.lockBackLegAccessory = false; - return syncLockedPonyInfoNumber(pony); + const pony = decompressPony(compressPonyString(createDefaultPony())); + pony.mane!.type = 0; + pony.backMane!.type = 0; + pony.tail!.type = 0; + pony.coatFill = DEFAULT_COLOR; + pony.lockCoatOutline = true; + pony.lockBackLegAccessory = false; + return syncLockedPonyInfoNumber(pony); } export function drawPsd(psd: Psd, scale: number, canvas?: HTMLCanvasElement): HTMLCanvasElement { - const buffer = canvas || createCanvas(100, 100); - buffer.width = psd.width * scale; - buffer.height = psd.height * scale; - const context = buffer.getContext('2d')!; - context.save(); - context.scale(scale, scale); - disableImageSmoothing(context); - drawLayer(psd, context); - context.restore(); - return buffer; + const buffer = canvas || createCanvas(100, 100); + buffer.width = psd.width * scale; + buffer.height = psd.height * scale; + const context = buffer.getContext('2d')!; + context.save(); + context.scale(scale, scale); + disableImageSmoothing(context); + drawLayer(psd, context); + context.restore(); + return buffer; } function drawLayer(layer: Layer, context: CanvasRenderingContext2D) { - if (!layer.hidden) { - layer.canvas && context.drawImage(layer.canvas, 0, 0); - layer.children && layer.children.forEach(c => drawLayer(c, context)); - } + if (!layer.hidden) { + layer.canvas && context.drawImage(layer.canvas, 0, 0); + layer.children && layer.children.forEach(c => drawLayer(c, context)); + } } diff --git a/src/ts/components/tools/tools-animation/tools-animation.ts b/src/ts/components/tools/tools-animation/tools-animation.ts index 0679a10..dd8c315 100644 --- a/src/ts/components/tools/tools-animation/tools-animation.ts +++ b/src/ts/components/tools/tools-animation/tools-animation.ts @@ -3,19 +3,19 @@ import { ActivatedRoute } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { flatMap, dropRightWhile, compact } from 'lodash'; import { - BodyAnimation as IBodyAnimation, - BodyAnimationFrame as IBodyAnimationFrame, - HeadAnimation as IHeadAnimation, - HeadAnimationFrame as IHeadAnimationFrame, - ColorExtraSet, PonyInfo, PonyObject, BodyShadow, PonyEye + BodyAnimation as IBodyAnimation, + BodyAnimationFrame as IBodyAnimationFrame, + HeadAnimation as IHeadAnimation, + HeadAnimationFrame as IHeadAnimationFrame, + ColorExtraSet, PonyInfo, PonyObject, BodyShadow, PonyEye } from '../../../common/interfaces'; import { removeItem, repeat, isKeyEventInvalid, cloneDeep, array } from '../../../common/utils'; import { toPalette, createDefaultPony, syncLockedPonyInfo } from '../../../common/ponyInfo'; import { Key } from '../../../client/input/input'; import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers'; import { - headAnimations, animations, createBodyFrame, createHeadFrame, stand, sit, mergeAnimations, - sitDown, lieDown, lie, sitUp, standUp + headAnimations, animations, createBodyFrame, createHeadFrame, stand, sit, mergeAnimations, + sitDown, lieDown, lie, sitUp, standUp } from '../../../client/ponyAnimations'; import { ContextSpriteBatch } from '../../../graphics/contextSpriteBatch'; import * as sprites from '../../../generated/sprites'; @@ -23,9 +23,9 @@ import { createCanvas, disableImageSmoothing, saveCanvas } from '../../../client import { loadAndInitSpriteSheets, createEyeSprite } from '../../../client/spriteUtils'; import { drawPony } from '../../../client/ponyDraw'; import { - faLock, faHome, faArrowRight, faArrowLeft, faPause, faPlay, faChevronRight, faChevronLeft, faRetweet, - faClone, faPlus, faAngleDoubleDown, faAngleDoubleUp, faAngleDoubleRight, faAngleDoubleLeft, - faCode, faShare, faTrash, faCopy, faFile, faStop, faRedo, faSync + faLock, faHome, faArrowRight, faArrowLeft, faPause, faPlay, faChevronRight, faChevronLeft, faRetweet, + faClone, faPlus, faAngleDoubleDown, faAngleDoubleUp, faAngleDoubleRight, faAngleDoubleLeft, + faCode, faShare, faTrash, faCopy, faFile, faStop, faRedo, faSync } from '../../../client/icons'; import { FrameService, FrameLoop } from '../../services/frameService'; import { StorageService } from '../../services/storageService'; @@ -37,768 +37,768 @@ const ponyHeight = 80; type AnimationMode = 'body' | 'head'; interface BaseAnimationFrame { - duration: number; + duration: number; } interface BodyAnimationFrame extends IBodyAnimationFrame, BaseAnimationFrame { - shadowOffset: number; - shadowFrame: number; + shadowOffset: number; + shadowFrame: number; } interface HeadAnimationFrame extends IHeadAnimationFrame, BaseAnimationFrame { } interface BaseAnimation { - name: string; - fps: number; - loop: boolean; - builtin?: boolean; + name: string; + fps: number; + loop: boolean; + builtin?: boolean; } interface BodyAnimation extends BaseAnimation { - lockFrontLegs?: boolean; - lockBackLegs?: boolean; - frames: BodyAnimationFrame[]; + lockFrontLegs?: boolean; + lockBackLegs?: boolean; + frames: BodyAnimationFrame[]; } interface HeadAnimation extends BaseAnimation { - lockEyes?: boolean; - frames: HeadAnimationFrame[]; + lockEyes?: boolean; + frames: HeadAnimationFrame[]; } interface AnimationsData { - active?: string; - animations?: BodyAnimation[]; - headActive?: string; - headAnimations?: HeadAnimation[]; + active?: string; + animations?: BodyAnimation[]; + headActive?: string; + headAnimations?: HeadAnimation[]; } interface PonyItem { - name: string; - info: PonyInfo; + name: string; + info: PonyInfo; } const testPony = { name: 'test pony', info: createDefaultPony() }; function eyeSprite(e: PonyEye | undefined) { - return createEyeSprite(e, 0, sprites.defaultPalette); + return createEyeSprite(e, 0, sprites.defaultPalette); } @Component({ - selector: 'tools-animation', - templateUrl: 'tools-animation.pug', - styleUrls: ['tools-animation.scss'], + selector: 'tools-animation', + templateUrl: 'tools-animation.pug', + styleUrls: ['tools-animation.scss'], }) export class ToolsAnimation implements OnInit, OnDestroy { - readonly lockIcon = faLock; - readonly homeIcon = faHome; - readonly rightIcon = faArrowRight; - readonly leftIcon = faArrowLeft; - readonly stopIcon = faStop; - readonly pauseIcon = faPause; - readonly playIcon = faPlay; - readonly replayIcon = faRedo; - readonly prevIcon = faChevronLeft; - readonly nextIcon = faChevronRight; - readonly switchIcon = faRetweet; - readonly fileIcon = faFile; - readonly copyIcon = faCopy; - readonly trashIcon = faTrash; - readonly shareIcon = faShare; - readonly codeIcon = faCode; - readonly doubleLeftIcon = faAngleDoubleLeft; - readonly doubleRightIcon = faAngleDoubleRight; - readonly doubleUpIcon = faAngleDoubleUp; - readonly doubleDownIcon = faAngleDoubleDown; - readonly plusIcon = faPlus; - readonly cloneIcon = faClone; - readonly syncIcon = faSync; - loaded = false; - pony: PonyItem; - ponies: PonyItem[] = [testPony]; - scale = 3; - shareLink?: string; - shareLinkOpen = false; - state = defaultPonyState(); - bodyAnimations: BodyAnimation[] = animations.map(fromBodyAnimation); - bodyAnimation: BodyAnimation; - headAnimations: HeadAnimation[] = headAnimations.map(fromHeadAnimation); - headAnimation: HeadAnimation; - body = sprites.body.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 })); - wing = sprites.wings.map(types => types![3]![0]!); - tail = sprites.tails.map(types => types![17]![0]!); - frontLegs: ColorExtraSet = sprites.frontLegs.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 })); - backLegs: ColorExtraSet = sprites.backLegs.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 })); - leftEyes: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite); - rightEyes: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite); - mouths: ColorExtraSet = sprites.noses - .map(m => m[0][0]) - .map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palette: sprites.defaultPalette })); - flip = false; - switch = false; - mode: AnimationMode; - beforeAnimation: BodyAnimation | undefined; - afterAnimation: BodyAnimation | undefined; - private _playing = false; - private _frame = 0; - private loop: FrameLoop; - constructor( - private http: HttpClient, - private route: ActivatedRoute, - private storage: StorageService, - frameService: FrameService - ) { - this.mode = storage.getItem('tools-animation-mode') as AnimationMode || 'body'; - this.loop = frameService.create(delta => this.tick(delta)); + readonly lockIcon = faLock; + readonly homeIcon = faHome; + readonly rightIcon = faArrowRight; + readonly leftIcon = faArrowLeft; + readonly stopIcon = faStop; + readonly pauseIcon = faPause; + readonly playIcon = faPlay; + readonly replayIcon = faRedo; + readonly prevIcon = faChevronLeft; + readonly nextIcon = faChevronRight; + readonly switchIcon = faRetweet; + readonly fileIcon = faFile; + readonly copyIcon = faCopy; + readonly trashIcon = faTrash; + readonly shareIcon = faShare; + readonly codeIcon = faCode; + readonly doubleLeftIcon = faAngleDoubleLeft; + readonly doubleRightIcon = faAngleDoubleRight; + readonly doubleUpIcon = faAngleDoubleUp; + readonly doubleDownIcon = faAngleDoubleDown; + readonly plusIcon = faPlus; + readonly cloneIcon = faClone; + readonly syncIcon = faSync; + loaded = false; + pony: PonyItem; + ponies: PonyItem[] = [testPony]; + scale = 3; + shareLink?: string; + shareLinkOpen = false; + state = defaultPonyState(); + bodyAnimations: BodyAnimation[] = animations.map(fromBodyAnimation); + bodyAnimation: BodyAnimation; + headAnimations: HeadAnimation[] = headAnimations.map(fromHeadAnimation); + headAnimation: HeadAnimation; + body = sprites.body.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 })); + wing = sprites.wings.map(types => types![3]![0]!); + tail = sprites.tails.map(types => types![17]![0]!); + frontLegs: ColorExtraSet = sprites.frontLegs.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 })); + backLegs: ColorExtraSet = sprites.backLegs.map(x => x && x[0] && x[0]![0].color).map(color => ({ color: color!, colors: 2 })); + leftEyes: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite); + rightEyes: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite); + mouths: ColorExtraSet = sprites.noses + .map(m => m[0][0]) + .map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palette: sprites.defaultPalette })); + flip = false; + switch = false; + mode: AnimationMode; + beforeAnimation: BodyAnimation | undefined; + afterAnimation: BodyAnimation | undefined; + private _playing = false; + private _frame = 0; + private loop: FrameLoop; + constructor( + private http: HttpClient, + private route: ActivatedRoute, + private storage: StorageService, + frameService: FrameService + ) { + this.mode = storage.getItem('tools-animation-mode') as AnimationMode || 'body'; + this.loop = frameService.create(delta => this.tick(delta)); - const data = this.loadAnimations(); + const data = this.loadAnimations(); - const extraAnimations: IBodyAnimation[] = [ - mergeAnimations('sit-lie-sit', 24, false, [...repeat(12, sit), lieDown, ...repeat(12, lie), sitUp, sit]), - mergeAnimations('stand-sit-stand', 24, false, [...repeat(12, stand), sitDown, ...repeat(12, sit), standUp, stand]), - mergeAnimations('stand-to-sit', 24, false, [stand, sitDown, sit]), - mergeAnimations('sit-to-lie', 24, false, [sit, lieDown, lie]), - { ...stand, loop: false, name: 'standing (1s)', frames: array(stand.fps, stand.frames[0]) }, - { ...sit, loop: false, name: 'sitting (1s)', frames: array(sit.fps, sit.frames[0]) }, - { ...lie, loop: false, name: 'lying (1s)', frames: array(lie.fps, lie.frames[0]) }, - ]; + const extraAnimations: IBodyAnimation[] = [ + mergeAnimations('sit-lie-sit', 24, false, [...repeat(12, sit), lieDown, ...repeat(12, lie), sitUp, sit]), + mergeAnimations('stand-sit-stand', 24, false, [...repeat(12, stand), sitDown, ...repeat(12, sit), standUp, stand]), + mergeAnimations('stand-to-sit', 24, false, [stand, sitDown, sit]), + mergeAnimations('sit-to-lie', 24, false, [sit, lieDown, lie]), + { ...stand, loop: false, name: 'standing (1s)', frames: array(stand.fps, stand.frames[0]) }, + { ...sit, loop: false, name: 'sitting (1s)', frames: array(sit.fps, sit.frames[0]) }, + { ...lie, loop: false, name: 'lying (1s)', frames: array(lie.fps, lie.frames[0]) }, + ]; - this.bodyAnimations.push(...extraAnimations.map((a, i) => fromBodyAnimation(a, 90 + i))); - this.bodyAnimations.push(...(data.animations || []).map(fixBodyAnimation)); - this.headAnimations.push(...(data.headAnimations || []).map(fixHeadAnimation)); - this.bodyAnimation = this.bodyAnimations[0]; - this.headAnimation = this.headAnimations[0]; + this.bodyAnimations.push(...extraAnimations.map((a, i) => fromBodyAnimation(a, 90 + i))); + this.bodyAnimations.push(...(data.animations || []).map(fixBodyAnimation)); + this.headAnimations.push(...(data.headAnimations || []).map(fixHeadAnimation)); + this.bodyAnimation = this.bodyAnimations[0]; + this.headAnimation = this.headAnimations[0]; - this.sortAnimations(); - this.selectBodyAnimation(this.bodyAnimations[parseInt(data.active || '0', 10) | 0] || this.bodyAnimation); - this.selectHeadAnimation(this.headAnimations[parseInt(data.headActive || '0', 10) | 0] || this.headAnimation); + this.sortAnimations(); + this.selectBodyAnimation(this.bodyAnimations[parseInt(data.active || '0', 10) | 0] || this.bodyAnimation); + this.selectHeadAnimation(this.headAnimations[parseInt(data.headActive || '0', 10) | 0] || this.headAnimation); - this.pony = this.ponies[0]; - this.pony.info.coatFill = '#9f7e7e'; - this.pony.info.mane!.type = 9; - this.pony.info.mane!.fills![0] = '#e2cf67'; - this.pony.info.lockEyes = false; - this.pony.info.cm = [ - 'orange', 'orange', 'orange', 'orange', 'orange', - 'orange', '', '', '', 'orange', - 'orange', '', '', '', 'orange', - 'orange', '', '', '', 'orange', - 'orange', 'orange', 'orange', 'orange', 'orange', - ]; + this.pony = this.ponies[0]; + this.pony.info.coatFill = '#9f7e7e'; + this.pony.info.mane!.type = 9; + this.pony.info.mane!.fills![0] = '#e2cf67'; + this.pony.info.lockEyes = false; + this.pony.info.cm = [ + 'orange', 'orange', 'orange', 'orange', 'orange', + 'orange', '', '', '', 'orange', + 'orange', '', '', '', 'orange', + 'orange', '', '', '', 'orange', + 'orange', 'orange', 'orange', 'orange', 'orange', + ]; - syncLockedPonyInfo(this.pony.info); - this.reloadPonies(); - } - get info() { - return this.pony.info; - } - get frame() { - return this._frame; - } - set frame(value: number) { - if (this._frame !== value) { - this._frame = value % this.frames.length; + syncLockedPonyInfo(this.pony.info); + this.reloadPonies(); + } + get info() { + return this.pony.info; + } + get frame() { + return this._frame; + } + set frame(value: number) { + if (this._frame !== value) { + this._frame = value % this.frames.length; - if (!this.playing) { - if (this.mode === 'body') { - this.state.animationFrame = this.frame; - } else { - this.state.headAnimationFrame = this.frame; - } - } - } - } - get activeFrame(): BaseAnimationFrame { - return this.frames[this.frame] || ({} as any); - } - get totalFrames() { - return this.frames.length; - } - get playing() { - return this._playing; - } - set playing(value: boolean) { - if (this._playing !== value) { - this._playing = value; - this.time = 0; - this.update(); + if (!this.playing) { + if (this.mode === 'body') { + this.state.animationFrame = this.frame; + } else { + this.state.headAnimationFrame = this.frame; + } + } + } + } + get activeFrame(): BaseAnimationFrame { + return this.frames[this.frame] || ({} as any); + } + get totalFrames() { + return this.frames.length; + } + get playing() { + return this._playing; + } + set playing(value: boolean) { + if (this._playing !== value) { + this._playing = value; + this.time = 0; + this.update(); - if (!value) { - this.frame = this.mode === 'body' ? this.state.animationFrame : this.state.headAnimationFrame; - } - } - } - get bodyFrames() { - return this.bodyAnimation.frames; - } - get headFrames() { - return this.headAnimation.frames; - } - ngOnInit() { - this.route.params.subscribe(({ id }) => id && this.fetchAnimation(id)); + if (!value) { + this.frame = this.mode === 'body' ? this.state.animationFrame : this.state.headAnimationFrame; + } + } + } + get bodyFrames() { + return this.bodyAnimation.frames; + } + get headFrames() { + return this.headAnimation.frames; + } + ngOnInit() { + this.route.params.subscribe(({ id }) => id && this.fetchAnimation(id)); - return loadAndInitSpriteSheets().then(() => { - this.loaded = true; - this.update(); - this.loop.init(); - }); - } - ngOnDestroy() { - this.loop.destroy(); - } - reloadPonies() { - this.http.get('/api-tools/ponies') - .subscribe(data => { - this.ponies = [ - testPony, - ...data - .map(p => ({ name: p.name, info: decompressPonyString(p.info) })) - .sort((a, b) => a.name.localeCompare(b.name)), - ]; + return loadAndInitSpriteSheets().then(() => { + this.loaded = true; + this.update(); + this.loop.init(); + }); + } + ngOnDestroy() { + this.loop.destroy(); + } + reloadPonies() { + this.http.get('/api-tools/ponies') + .subscribe(data => { + this.ponies = [ + testPony, + ...data + .map(p => ({ name: p.name, info: decompressPonyString(p.info) })) + .sort((a, b) => a.name.localeCompare(b.name)), + ]; - const ponyName = this.storage.getItem('tools-animation-pony'); + const ponyName = this.storage.getItem('tools-animation-pony'); - if (ponyName) { - this.pony = this.ponies.find(p => p.name === ponyName) || this.pony; - } - }); - } - setPony(pony: PonyItem) { - this.pony = pony; - this.update(); - this.storage.setItem('tools-animation-pony', pony.name); - } - selectBodyAnimation(animation: BodyAnimation) { - this.bodyAnimation = animation; - this.update(); - } - selectHeadAnimation(animation: HeadAnimation) { - this.headAnimation = animation; - this.update(); - } - setMode(mode: AnimationMode) { - this.mode = mode; - this.storage.setItem('tools-animation-mode', mode); - } - replay() { - this.bodyAnimationPlaying = 0; - this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying]; - this.time = 0; - } - private fetchAnimation(id: string) { - return this.http.get<{ type: string, animation: any; }>(`/api-tools/animation/${id}`) - .subscribe(({ type, animation }) => { - if (type === 'body') { - this.bodyAnimations.push(animation); - } else { - this.headAnimations.push(animation); - } + if (ponyName) { + this.pony = this.ponies.find(p => p.name === ponyName) || this.pony; + } + }); + } + setPony(pony: PonyItem) { + this.pony = pony; + this.update(); + this.storage.setItem('tools-animation-pony', pony.name); + } + selectBodyAnimation(animation: BodyAnimation) { + this.bodyAnimation = animation; + this.update(); + } + selectHeadAnimation(animation: HeadAnimation) { + this.headAnimation = animation; + this.update(); + } + setMode(mode: AnimationMode) { + this.mode = mode; + this.storage.setItem('tools-animation-mode', mode); + } + replay() { + this.bodyAnimationPlaying = 0; + this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying]; + this.time = 0; + } + private fetchAnimation(id: string) { + return this.http.get<{ type: string, animation: any; }>(`/api-tools/animation/${id}`) + .subscribe(({ type, animation }) => { + if (type === 'body') { + this.bodyAnimations.push(animation); + } else { + this.headAnimations.push(animation); + } - this.sortAnimations(); - this.selectAnimation(animation); - }); - } - private createAnimation(): BodyAnimation | HeadAnimation { - if (this.mode === 'body') { - return { name: 'new animation', loop: true, fps: 24, frames: [createDefaultBodyFrame()] }; - } else { - return { name: 'new animation', loop: true, fps: 24, frames: [createDefaultHeadFrame()] }; - } - } - newAnimation() { - const animation = this.createAnimation(); - const animations = this.animations as any[]; - animations.push(animation); - this.selectAnimation(animation); - } - duplicateAnimation() { - const animation = cloneDeep(this.animation); - const animations = this.animations as any[]; - animation.name = animation.name.replace(/# builtin \d+ #/, '').trim() + ' (clone)'; - delete animation.builtin; - animations.push(animation); - this.selectAnimation(animation); - } - removeAnimation() { - const animations = this.animations; + this.sortAnimations(); + this.selectAnimation(animation); + }); + } + private createAnimation(): BodyAnimation | HeadAnimation { + if (this.mode === 'body') { + return { name: 'new animation', loop: true, fps: 24, frames: [createDefaultBodyFrame()] }; + } else { + return { name: 'new animation', loop: true, fps: 24, frames: [createDefaultHeadFrame()] }; + } + } + newAnimation() { + const animation = this.createAnimation(); + const animations = this.animations as any[]; + animations.push(animation); + this.selectAnimation(animation); + } + duplicateAnimation() { + const animation = cloneDeep(this.animation); + const animations = this.animations as any[]; + animation.name = animation.name.replace(/# builtin \d+ #/, '').trim() + ' (clone)'; + delete animation.builtin; + animations.push(animation); + this.selectAnimation(animation); + } + removeAnimation() { + const animations = this.animations; - if (animations.length && confirm('are you sure ?')) { - removeItem(animations, this.animation); - this.selectAnimation(animations[0]); - } - } - selectAnimation(animation: BodyAnimation | HeadAnimation) { - if (this.mode === 'body') { - this.selectBodyAnimation(animation as BodyAnimation); - } else { - this.selectHeadAnimation(animation as HeadAnimation); - } - } - selectBeforeAnimation(animation: BodyAnimation | undefined) { - this.beforeAnimation = animation; - this.update(); - } - selectAfterAnimation(animation: BodyAnimation | undefined) { - this.afterAnimation = animation; - this.update(); - } - selectFrame(index: number) { - this.frame = index; - } - prevFrame() { - this.frame = this.frame === 0 ? (this.frames.length - 1) : (this.frame - 1); - } - nextFrame() { - this.frame = this.frame + 1; - } - addFrame() { - const frames = this.frames as any[]; - const frame = this.mode === 'body' ? createDefaultBodyFrame() : createDefaultHeadFrame(); - frames.splice(this.frame + 1, 0, frame); - this.update(); - this.frame++; - } - duplicateFrame() { - const frames = this.frames as any[]; - frames.splice(this.frame + 1, 0, cloneDeep(frames[this.frame])); - this.update(); - this.frame++; - } - removeFrame() { - const frames = this.frames; + if (animations.length && confirm('are you sure ?')) { + removeItem(animations, this.animation); + this.selectAnimation(animations[0]); + } + } + selectAnimation(animation: BodyAnimation | HeadAnimation) { + if (this.mode === 'body') { + this.selectBodyAnimation(animation as BodyAnimation); + } else { + this.selectHeadAnimation(animation as HeadAnimation); + } + } + selectBeforeAnimation(animation: BodyAnimation | undefined) { + this.beforeAnimation = animation; + this.update(); + } + selectAfterAnimation(animation: BodyAnimation | undefined) { + this.afterAnimation = animation; + this.update(); + } + selectFrame(index: number) { + this.frame = index; + } + prevFrame() { + this.frame = this.frame === 0 ? (this.frames.length - 1) : (this.frame - 1); + } + nextFrame() { + this.frame = this.frame + 1; + } + addFrame() { + const frames = this.frames as any[]; + const frame = this.mode === 'body' ? createDefaultBodyFrame() : createDefaultHeadFrame(); + frames.splice(this.frame + 1, 0, frame); + this.update(); + this.frame++; + } + duplicateFrame() { + const frames = this.frames as any[]; + frames.splice(this.frame + 1, 0, cloneDeep(frames[this.frame])); + this.update(); + this.frame++; + } + removeFrame() { + const frames = this.frames; - if (frames.length && confirm('are you sure ?')) { - frames.splice(this.frame, 1); - this.update(); - this.frame = Math.min(this.frame, frames.length - 1); - } - } - moveFrameLeft() { - if (this.frame > 0) { - swap(this.frames, this.frame, this.frame - 1); - this.update(); - this.frame--; - } - } - moveFrameRight() { - const frames = this.frames; + if (frames.length && confirm('are you sure ?')) { + frames.splice(this.frame, 1); + this.update(); + this.frame = Math.min(this.frame, frames.length - 1); + } + } + moveFrameLeft() { + if (this.frame > 0) { + swap(this.frames, this.frame, this.frame - 1); + this.update(); + this.frame--; + } + } + moveFrameRight() { + const frames = this.frames; - if (this.frame < (frames.length - 1)) { - swap(frames, this.frame, this.frame + 1); - this.update(); - this.frame++; - } - } - isActive(index: number) { - return this.frame === index; - } - get animations() { - return this.mode === 'body' ? this.bodyAnimations : this.headAnimations; - } - get animation() { - return this.mode === 'body' ? this.bodyAnimation : this.headAnimation; - } - get frames() { - return this.animation.frames; - } - @HostListener('window:keydown', ['$event']) - keydown(e: KeyboardEvent) { - if (!isKeyEventInvalid(e) && this.handleKey(e.keyCode)) { - e.preventDefault(); - } - } - moveAllHead(x: number, y: number) { - (this.frames as HeadAnimationFrame[]).forEach(f => { - f.headX += x; - f.headY += y; - }); - this.update(); - } - moveAllBody(x: number, y: number) { - (this.frames as BodyAnimationFrame[]).forEach(f => { - f.bodyX += x; - f.bodyY += y; - }); - this.update(); - } - handleKey(keyCode: number) { - if (keyCode === Key.OPEN_BRACKET || keyCode === Key.LEFT || keyCode === Key.COMMA) { - this.prevFrame(); - } else if (keyCode === Key.CLOSE_BRACKET || keyCode === Key.RIGHT || keyCode === Key.PERIOD) { - this.nextFrame(); - } else if (keyCode === Key.ENTER) { - this.playing = !this.playing; - } else { - return false; - } + if (this.frame < (frames.length - 1)) { + swap(frames, this.frame, this.frame + 1); + this.update(); + this.frame++; + } + } + isActive(index: number) { + return this.frame === index; + } + get animations() { + return this.mode === 'body' ? this.bodyAnimations : this.headAnimations; + } + get animation() { + return this.mode === 'body' ? this.bodyAnimation : this.headAnimation; + } + get frames() { + return this.animation.frames; + } + @HostListener('window:keydown', ['$event']) + keydown(e: KeyboardEvent) { + if (!isKeyEventInvalid(e) && this.handleKey(e.keyCode)) { + e.preventDefault(); + } + } + moveAllHead(x: number, y: number) { + (this.frames as HeadAnimationFrame[]).forEach(f => { + f.headX += x; + f.headY += y; + }); + this.update(); + } + moveAllBody(x: number, y: number) { + (this.frames as BodyAnimationFrame[]).forEach(f => { + f.bodyX += x; + f.bodyY += y; + }); + this.update(); + } + handleKey(keyCode: number) { + if (keyCode === Key.OPEN_BRACKET || keyCode === Key.LEFT || keyCode === Key.COMMA) { + this.prevFrame(); + } else if (keyCode === Key.CLOSE_BRACKET || keyCode === Key.RIGHT || keyCode === Key.PERIOD) { + this.nextFrame(); + } else if (keyCode === Key.ENTER) { + this.playing = !this.playing; + } else { + return false; + } - return true; - } - share() { - const wasOpened = this.shareLinkOpen; - this.shareLink = undefined; - this.shareLinkOpen = false; + return true; + } + share() { + const wasOpened = this.shareLinkOpen; + this.shareLink = undefined; + this.shareLinkOpen = false; - if (!wasOpened) { - const animation = { type: this.mode, animation: this.animation }; - this.http.post<{ name: string; }>('/api-tools/animation', { animation }) - .subscribe(({ name }) => { - this.shareLink = `${location.protocol}//${location.host}/tools/animation/${name}`; - this.shareLinkOpen = true; - }); - } - } - export() { - if (this.mode === 'body') { - const frames = this.bodyAnimation.frames - .map(f => [f.duration, '[' + compressBodyFrame(f).join(', ') + ']']) - .map(([repeat, frame]) => repeat > 1 ? `...repeat(${repeat}, ${frame})` : frame); - console.log(`frames: [\n${frames.map(x => `\t${x}`).join(',\n')}\n]`); + if (!wasOpened) { + const animation = { type: this.mode, animation: this.animation }; + this.http.post<{ name: string; }>('/api-tools/animation', { animation }) + .subscribe(({ name }) => { + this.shareLink = `${location.protocol}//${location.host}/tools/animation/${name}`; + this.shareLinkOpen = true; + }); + } + } + export() { + if (this.mode === 'body') { + const frames = this.bodyAnimation.frames + .map(f => [f.duration, '[' + compressBodyFrame(f).join(', ') + ']']) + .map(([repeat, frame]) => repeat > 1 ? `...repeat(${repeat}, ${frame})` : frame); + console.log(`frames: [\n${frames.map(x => `\t${x}`).join(',\n')}\n]`); - if (this.bodyAnimation.frames.some(f => !!f.shadowFrame || !!f.shadowOffset)) { - const shadow = this.bodyAnimation.frames.map(f => [f.shadowFrame, f.shadowOffset]); - console.log(`shadow: [${shadow.map(x => `[${x.join(', ')}]`).join(', ')}]`); - } - } else { - const animation = toHeadAnimation(this.headAnimation, true); - const compressed = animation.frames.map(compressHeadFrame); - console.log(JSON.stringify(compressed)); - } - } - png(scale = 1) { - const { canvas } = this.createAnimationSprites(scale); - saveCanvas(canvas, `${this.animation.name}.png`); - } - gif(scale = 1) { - const { canvas, empty } = this.createAnimationSprites(scale); - const wnd = window.open('')!; - const width = scale * ponyWidth; - const height = scale * ponyHeight; - const fps = this.animation.fps || 24; - const image = canvas.toDataURL(); + if (this.bodyAnimation.frames.some(f => !!f.shadowFrame || !!f.shadowOffset)) { + const shadow = this.bodyAnimation.frames.map(f => [f.shadowFrame, f.shadowOffset]); + console.log(`shadow: [${shadow.map(x => `[${x.join(', ')}]`).join(', ')}]`); + } + } else { + const animation = toHeadAnimation(this.headAnimation, true); + const compressed = animation.frames.map(compressHeadFrame); + console.log(JSON.stringify(compressed)); + } + } + png(scale = 1) { + const { canvas } = this.createAnimationSprites(scale); + saveCanvas(canvas, `${this.animation.name}.png`); + } + gif(scale = 1) { + const { canvas, empty } = this.createAnimationSprites(scale); + const wnd = window.open('')!; + const width = scale * ponyWidth; + const height = scale * ponyHeight; + const fps = this.animation.fps || 24; + const image = canvas.toDataURL(); - this.http.post<{ name: string; }>('/api-tools/animation-gif', { image, width, height, fps, remove: empty }) - .subscribe(({ name }) => wnd.location.href = `/api-tools/animation/${name}.gif`); - } - sortAnimations() { - this.bodyAnimations.sort(compareAnimations); - this.headAnimations.sort(compareAnimations); - } - private update() { - if (this.headAnimation && this.headAnimation.lockEyes) { - this.headAnimation.frames.forEach(f => f.left = f.right); - } + this.http.post<{ name: string; }>('/api-tools/animation-gif', { image, width, height, fps, remove: empty }) + .subscribe(({ name }) => wnd.location.href = `/api-tools/animation/${name}.gif`); + } + sortAnimations() { + this.bodyAnimations.sort(compareAnimations); + this.headAnimations.sort(compareAnimations); + } + private update() { + if (this.headAnimation && this.headAnimation.lockEyes) { + this.headAnimation.frames.forEach(f => f.left = f.right); + } - if (this.bodyAnimation) { - if (this.bodyAnimation.lockFrontLegs) { - this.bodyAnimation.frames.forEach(f => { - f.frontFarLeg = f.frontLeg; - f.frontFarLegX = f.frontLegX; - f.frontFarLegY = f.frontLegY; - }); - } + if (this.bodyAnimation) { + if (this.bodyAnimation.lockFrontLegs) { + this.bodyAnimation.frames.forEach(f => { + f.frontFarLeg = f.frontLeg; + f.frontFarLegX = f.frontLegX; + f.frontFarLegY = f.frontLegY; + }); + } - if (this.bodyAnimation.lockBackLegs) { - this.bodyAnimation.frames.forEach(f => { - f.backFarLeg = f.backLeg; - f.backFarLegX = f.backLegX; - f.backFarLegY = f.backLegY; - }); - } - } + if (this.bodyAnimation.lockBackLegs) { + this.bodyAnimation.frames.forEach(f => { + f.backFarLeg = f.backLeg; + f.backFarLegX = f.backLegX; + f.backFarLegY = f.backLegY; + }); + } + } - this.bodyAnimationsToPlay = compact([ - this.playing && this.beforeAnimation && { ...toBodyAnimation(this.beforeAnimation, true, false), loop: false }, - toBodyAnimation(this.bodyAnimation, this.playing, this.switch), - this.playing && this.afterAnimation && { ...toBodyAnimation(this.afterAnimation, true, false), loop: true }, - ]); + this.bodyAnimationsToPlay = compact([ + this.playing && this.beforeAnimation && { ...toBodyAnimation(this.beforeAnimation, true, false), loop: false }, + toBodyAnimation(this.bodyAnimation, this.playing, this.switch), + this.playing && this.afterAnimation && { ...toBodyAnimation(this.afterAnimation, true, false), loop: true }, + ]); - this.bodyAnimationPlaying = 0; + this.bodyAnimationPlaying = 0; - this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying]; - this.state.headAnimation = toHeadAnimation(this.headAnimation, this.playing); + this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying]; + this.state.headAnimation = toHeadAnimation(this.headAnimation, this.playing); - if (this.playing) { - this.state.animationFrame = 0; - this.state.headAnimationFrame = 0; - } + if (this.playing) { + this.state.animationFrame = 0; + this.state.headAnimationFrame = 0; + } - this.saveAnimations(); - } - private bodyAnimationsToPlay: IBodyAnimation[] = []; - private bodyAnimationPlaying = 0; - private time = 0; - private tick(delta: number) { - if (this.playing) { - this.time += delta; + this.saveAnimations(); + } + private bodyAnimationsToPlay: IBodyAnimation[] = []; + private bodyAnimationPlaying = 0; + private time = 0; + private tick(delta: number) { + if (this.playing) { + this.time += delta; - if (this.mode === 'body') { - if (this.state.animation) { - const frame = this.time * this.state.animation.fps; + if (this.mode === 'body') { + if (this.state.animation) { + const frame = this.time * this.state.animation.fps; - if (frame > this.state.animation.frames.length && !this.state.animation.loop) { - this.bodyAnimationPlaying = (this.bodyAnimationPlaying + 1) % this.bodyAnimationsToPlay.length; - this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying]; - this.state.animationFrame = 0; - this.time = 0; - } else { - this.state.animationFrame = Math.floor(frame) % this.state.animation.frames.length; - } - } - } else { - if (this.state.headAnimation) { - const frame = Math.floor(this.time * this.state.headAnimation.fps); - this.state.headAnimationFrame = frame % this.state.headAnimation.frames.length; - } - } - } - } - private saveAnimations() { - this.storage.setJSON('tools-animations', { - active: this.bodyAnimations.indexOf(this.bodyAnimation).toString(), - animations: this.bodyAnimations.filter(a => !a.builtin), - headActive: this.headAnimations.indexOf(this.headAnimation).toString(), - headAnimations: this.headAnimations.filter(a => !a.builtin), - }); - } - private loadAnimations(): AnimationsData { - return this.storage.getJSON('tools-animations', {}); - } - private createAnimationSprites(scale: number) { - const animation = toBodyAnimation(this.bodyAnimation, true, this.switch); - const headAnimation = toHeadAnimation(this.headAnimation, true); - const frames = this.mode === 'body' ? animation.frames.length : headAnimation.frames.length; - const buffer = createCanvas(ponyWidth, ponyHeight); - const batch = new ContextSpriteBatch(buffer); - const info = toPalette(this.pony.info); - const cols = Math.ceil(Math.sqrt(frames)); - const canvas = createCanvas(ponyWidth * cols * scale, ponyHeight * Math.ceil(frames / cols) * scale); - const context = canvas.getContext('2d')!; - const empty = (cols * Math.ceil(frames / cols)) - frames; - const options = defaultDrawPonyOptions(); - disableImageSmoothing(context); - context.scale(scale, scale); + if (frame > this.state.animation.frames.length && !this.state.animation.loop) { + this.bodyAnimationPlaying = (this.bodyAnimationPlaying + 1) % this.bodyAnimationsToPlay.length; + this.state.animation = this.bodyAnimationsToPlay[this.bodyAnimationPlaying]; + this.state.animationFrame = 0; + this.time = 0; + } else { + this.state.animationFrame = Math.floor(frame) % this.state.animation.frames.length; + } + } + } else { + if (this.state.headAnimation) { + const frame = Math.floor(this.time * this.state.headAnimation.fps); + this.state.headAnimationFrame = frame % this.state.headAnimation.frames.length; + } + } + } + } + private saveAnimations() { + this.storage.setJSON('tools-animations', { + active: this.bodyAnimations.indexOf(this.bodyAnimation).toString(), + animations: this.bodyAnimations.filter(a => !a.builtin), + headActive: this.headAnimations.indexOf(this.headAnimation).toString(), + headAnimations: this.headAnimations.filter(a => !a.builtin), + }); + } + private loadAnimations(): AnimationsData { + return this.storage.getJSON('tools-animations', {}); + } + private createAnimationSprites(scale: number) { + const animation = toBodyAnimation(this.bodyAnimation, true, this.switch); + const headAnimation = toHeadAnimation(this.headAnimation, true); + const frames = this.mode === 'body' ? animation.frames.length : headAnimation.frames.length; + const buffer = createCanvas(ponyWidth, ponyHeight); + const batch = new ContextSpriteBatch(buffer); + const info = toPalette(this.pony.info); + const cols = Math.ceil(Math.sqrt(frames)); + const canvas = createCanvas(ponyWidth * cols * scale, ponyHeight * Math.ceil(frames / cols) * scale); + const context = canvas.getContext('2d')!; + const empty = (cols * Math.ceil(frames / cols)) - frames; + const options = defaultDrawPonyOptions(); + disableImageSmoothing(context); + context.scale(scale, scale); - for (let i = 0; i < frames; i++) { - const x = i % cols; - const y = Math.floor(i / cols); + for (let i = 0; i < frames; i++) { + const x = i % cols; + const y = Math.floor(i / cols); - batch.start(sprites.paletteSpriteSheet, 0); + batch.start(sprites.paletteSpriteSheet, 0); - drawPony(batch, info, { - ...defaultPonyState(), - animation, - animationFrame: this.mode === 'body' ? i : 0, - headAnimation: this.mode === 'head' ? headAnimation : undefined, - headAnimationFrame: this.mode === 'head' ? i : 0, - blinkFrame: 1, - }, ponyWidth / 2, ponyHeight - 10, options); + drawPony(batch, info, { + ...defaultPonyState(), + animation, + animationFrame: this.mode === 'body' ? i : 0, + headAnimation: this.mode === 'head' ? headAnimation : undefined, + headAnimationFrame: this.mode === 'head' ? i : 0, + blinkFrame: 1, + }, ponyWidth / 2, ponyHeight - 10, options); - batch.end(); - context.drawImage(buffer, x * ponyWidth, y * ponyHeight); - } + batch.end(); + context.drawImage(buffer, x * ponyWidth, y * ponyHeight); + } - return { canvas, empty }; - } + return { canvas, empty }; + } } // helper methods function compareAnimations(a: T, b: T): number { - return a.name.localeCompare(b.name); + return a.name.localeCompare(b.name); } function swap(array: any[], a: number, b: number) { - const temp = array[a]; - array[a] = array[b]; - array[b] = temp; + const temp = array[a]; + array[a] = array[b]; + array[b] = temp; } function fromBodyAnimation({ name, frames, fps, loop, shadow }: IBodyAnimation, index: number): BodyAnimation { - const fs: BodyAnimationFrame[] = []; + const fs: BodyAnimationFrame[] = []; - frames.forEach((f, i) => { - const l = fs[fs.length - 1]; - const s = shadow && shadow[i]; + frames.forEach((f, i) => { + const l = fs[fs.length - 1]; + const s = shadow && shadow[i]; - if ( - l && l.headX === f.headX && l.headY === f.headY && l.bodyX === f.bodyX && l.bodyY === f.bodyY - && l.body === f.body && l.frontLeg === f.frontLeg && l.backLeg === f.backLeg - && l.frontFarLeg === f.frontFarLeg && l.backFarLeg === f.backFarLeg - && l.frontLegX === f.frontLegX && l.frontLegY === f.frontLegY - && l.frontFarLegX === f.frontFarLegX && l.frontFarLegY === f.frontFarLegY - && l.backLegX === f.backLegX && l.backLegY === f.backLegY - && l.backFarLegX === f.backFarLegX && l.backFarLegY === f.backFarLegY - && l.wing === f.wing - ) { - l.duration++; - } else { - fs.push({ - duration: 1, - ...f, - shadowOffset: s && s.offset || 0, - shadowFrame: s && s.frame || 0 - }); - } - }); + if ( + l && l.headX === f.headX && l.headY === f.headY && l.bodyX === f.bodyX && l.bodyY === f.bodyY + && l.body === f.body && l.frontLeg === f.frontLeg && l.backLeg === f.backLeg + && l.frontFarLeg === f.frontFarLeg && l.backFarLeg === f.backFarLeg + && l.frontLegX === f.frontLegX && l.frontLegY === f.frontLegY + && l.frontFarLegX === f.frontFarLegX && l.frontFarLegY === f.frontFarLegY + && l.backLegX === f.backLegX && l.backLegY === f.backLegY + && l.backFarLegX === f.backFarLegX && l.backFarLegY === f.backFarLegY + && l.wing === f.wing + ) { + l.duration++; + } else { + fs.push({ + duration: 1, + ...f, + shadowOffset: s && s.offset || 0, + shadowFrame: s && s.frame || 0 + }); + } + }); - return { - builtin: true, - loop, - fps, - name: `# ${index.toString().padStart(2, '0')}-${name}`, - frames: fs, - }; + return { + builtin: true, + loop, + fps, + name: `# ${index.toString().padStart(2, '0')}-${name}`, + frames: fs, + }; } function toBodyAnimation({ name, loop, fps, frames }: BodyAnimation, full: boolean, switchFarClose: boolean): IBodyAnimation { - let shadow: BodyShadow[] | undefined = undefined; + let shadow: BodyShadow[] | undefined = undefined; - if (frames.some(f => !!f.shadowFrame || !!f.shadowOffset)) { - shadow = flatMap(frames, f => repeat(full ? f.duration : 1, { frame: f.shadowFrame, offset: f.shadowOffset })); - } + if (frames.some(f => !!f.shadowFrame || !!f.shadowOffset)) { + shadow = flatMap(frames, f => repeat(full ? f.duration : 1, { frame: f.shadowFrame, offset: f.shadowOffset })); + } - return { - name, - loop, - fps, - shadow, - frames: flatMap(frames, f => repeat(full ? f.duration : 1, { - body: f.body, - head: f.head, - wing: f.wing, - tail: f.tail, - frontLeg: switchFarClose ? f.frontFarLeg : f.frontLeg, - frontFarLeg: switchFarClose ? f.frontLeg : f.frontFarLeg, - backLeg: switchFarClose ? f.backFarLeg : f.backLeg, - backFarLeg: switchFarClose ? f.backLeg : f.backFarLeg, - bodyX: f.bodyX, - bodyY: f.bodyY, - headX: f.headX, - headY: f.headY, - frontLegX: switchFarClose ? f.frontFarLegX : f.frontLegX, - frontLegY: switchFarClose ? f.frontFarLegY : f.frontLegY, - frontFarLegX: switchFarClose ? f.frontLegX : f.frontFarLegX, - frontFarLegY: switchFarClose ? f.frontLegY : f.frontFarLegY, - backLegX: switchFarClose ? f.backFarLegX : f.backLegX, - backLegY: switchFarClose ? f.backFarLegY : f.backLegY, - backFarLegX: switchFarClose ? f.backLegX : f.backFarLegX, - backFarLegY: switchFarClose ? f.backLegY : f.backFarLegY, - })), - }; + return { + name, + loop, + fps, + shadow, + frames: flatMap(frames, f => repeat(full ? f.duration : 1, { + body: f.body, + head: f.head, + wing: f.wing, + tail: f.tail, + frontLeg: switchFarClose ? f.frontFarLeg : f.frontLeg, + frontFarLeg: switchFarClose ? f.frontLeg : f.frontFarLeg, + backLeg: switchFarClose ? f.backFarLeg : f.backLeg, + backFarLeg: switchFarClose ? f.backLeg : f.backFarLeg, + bodyX: f.bodyX, + bodyY: f.bodyY, + headX: f.headX, + headY: f.headY, + frontLegX: switchFarClose ? f.frontFarLegX : f.frontLegX, + frontLegY: switchFarClose ? f.frontFarLegY : f.frontLegY, + frontFarLegX: switchFarClose ? f.frontLegX : f.frontFarLegX, + frontFarLegY: switchFarClose ? f.frontLegY : f.frontFarLegY, + backLegX: switchFarClose ? f.backFarLegX : f.backLegX, + backLegY: switchFarClose ? f.backFarLegY : f.backLegY, + backFarLegX: switchFarClose ? f.backLegX : f.backFarLegX, + backFarLegY: switchFarClose ? f.backLegY : f.backFarLegY, + })), + }; } function compressBodyFrame(f: BodyAnimationFrame): number[] { - return dropRightWhile([ - f.body, f.head, f.wing, f.tail, f.frontLeg, f.frontFarLeg, f.backLeg, f.backFarLeg, - f.bodyX, f.bodyY, f.headX, f.headY, - f.frontLegX, f.frontLegY, f.frontFarLegX, f.frontFarLegY, - f.backLegX, f.backLegY, f.backFarLegX, f.backFarLegY, - ], x => !x); + return dropRightWhile([ + f.body, f.head, f.wing, f.tail, f.frontLeg, f.frontFarLeg, f.backLeg, f.backFarLeg, + f.bodyX, f.bodyY, f.headX, f.headY, + f.frontLegX, f.frontLegY, f.frontFarLegX, f.frontFarLegY, + f.backLegX, f.backLegY, f.backFarLegX, f.backFarLegY, + ], x => !x); } function fromHeadAnimation({ name, fps, loop, frames }: IHeadAnimation, index: number): HeadAnimation { - const fs: HeadAnimationFrame[] = []; + const fs: HeadAnimationFrame[] = []; - frames.forEach(f => { - const l = fs[fs.length - 1]; + frames.forEach(f => { + const l = fs[fs.length - 1]; - if (l && l.headX === f.headX && l.headY === f.headY && l.left === f.left && l.right === f.right && l.mouth === f.mouth) { - l.duration++; - } else { - fs.push({ duration: 1, ...f }); - } - }); + if (l && l.headX === f.headX && l.headY === f.headY && l.left === f.left && l.right === f.right && l.mouth === f.mouth) { + l.duration++; + } else { + fs.push({ duration: 1, ...f }); + } + }); - return { - builtin: true, - fps, - loop, - name: `# builtin ${index.toString().padStart(2, '0')} # ${name}`, - frames: fs, - }; + return { + builtin: true, + fps, + loop, + name: `# builtin ${index.toString().padStart(2, '0')} # ${name}`, + frames: fs, + }; } function toHeadAnimation({ name, frames, fps, loop }: HeadAnimation, full: boolean): IHeadAnimation { - const fs = (full && !loop) ? repeat(fps, createDefaultHeadFrame()).concat(frames) : frames; + const fs = (full && !loop) ? repeat(fps, createDefaultHeadFrame()).concat(frames) : frames; - return { - name, - fps, - loop, - frames: flatMap(fs, f => repeat(full ? f.duration : 1, f)), - }; + return { + name, + fps, + loop, + frames: flatMap(fs, f => repeat(full ? f.duration : 1, f)), + }; } function compressHeadFrame({ headX, headY, left, right, mouth }: IHeadAnimationFrame) { - return [headX, headY, left, right, mouth]; + return [headX, headY, left, right, mouth]; } function createDefaultBodyFrame(): BodyAnimationFrame { - return { duration: 1, ...createBodyFrame([1, 1, 0, 0, 1, 1, 1, 1]), shadowFrame: 0, shadowOffset: 0 }; + return { duration: 1, ...createBodyFrame([1, 1, 0, 0, 1, 1, 1, 1]), shadowFrame: 0, shadowOffset: 0 }; } function createDefaultHeadFrame(): HeadAnimationFrame { - return { duration: 1, ...createHeadFrame([0, 0, 1, 1, 0]) }; + return { duration: 1, ...createHeadFrame([0, 0, 1, 1, 0]) }; } // fixing helpers function fixBodyAnimation(a: BodyAnimation): BodyAnimation { - return { - name: a.name || '', - fps: a.fps || 24, - loop: a.loop || false, - lockFrontLegs: a.lockFrontLegs || false, - lockBackLegs: a.lockBackLegs || false, - frames: (a.frames || []).map(fixBodyFrame), - }; + return { + name: a.name || '', + fps: a.fps || 24, + loop: a.loop || false, + lockFrontLegs: a.lockFrontLegs || false, + lockBackLegs: a.lockBackLegs || false, + frames: (a.frames || []).map(fixBodyFrame), + }; } function fixBodyFrame(f: BodyAnimationFrame): BodyAnimationFrame { - return { - duration: f.duration || 1, - body: f.body || 0, - head: f.head || 0, - wing: f.wing || 0, - tail: f.tail || 0, - frontLeg: f.frontLeg || 0, - frontFarLeg: f.frontFarLeg || 0, - backLeg: f.backLeg || 0, - backFarLeg: f.backFarLeg || 0, - bodyX: f.bodyX || 0, - bodyY: f.bodyY || 0, - headX: f.headX || 0, - headY: f.headY || 0, - frontLegX: f.frontLegX || 0, - frontLegY: f.frontLegY || 0, - frontFarLegX: f.frontFarLegX || 0, - frontFarLegY: f.frontFarLegY || 0, - backLegX: f.backLegX || 0, - backLegY: f.backLegY || 0, - backFarLegX: f.backFarLegX || 0, - backFarLegY: f.backFarLegY || 0, - shadowFrame: f.shadowFrame || 0, - shadowOffset: f.shadowOffset || 0, - }; + return { + duration: f.duration || 1, + body: f.body || 0, + head: f.head || 0, + wing: f.wing || 0, + tail: f.tail || 0, + frontLeg: f.frontLeg || 0, + frontFarLeg: f.frontFarLeg || 0, + backLeg: f.backLeg || 0, + backFarLeg: f.backFarLeg || 0, + bodyX: f.bodyX || 0, + bodyY: f.bodyY || 0, + headX: f.headX || 0, + headY: f.headY || 0, + frontLegX: f.frontLegX || 0, + frontLegY: f.frontLegY || 0, + frontFarLegX: f.frontFarLegX || 0, + frontFarLegY: f.frontFarLegY || 0, + backLegX: f.backLegX || 0, + backLegY: f.backLegY || 0, + backFarLegX: f.backFarLegX || 0, + backFarLegY: f.backFarLegY || 0, + shadowFrame: f.shadowFrame || 0, + shadowOffset: f.shadowOffset || 0, + }; } function fixHeadAnimation(a: HeadAnimation): HeadAnimation { - return { - name: a.name || '', - fps: a.fps || 24, - loop: a.loop || false, - lockEyes: a.lockEyes || false, - frames: (a.frames || []).map(fixHeadFrame), - }; + return { + name: a.name || '', + fps: a.fps || 24, + loop: a.loop || false, + lockEyes: a.lockEyes || false, + frames: (a.frames || []).map(fixHeadFrame), + }; } function fixHeadFrame(f: HeadAnimationFrame): HeadAnimationFrame { - return { - duration: f.duration || 1, - headX: f.headX || 0, - headY: f.headY || 0, - left: f.left || 0, - right: f.right || 0, - mouth: f.mouth || 0, - }; + return { + duration: f.duration || 1, + headX: f.headX || 0, + headY: f.headY || 0, + left: f.left || 0, + right: f.right || 0, + mouth: f.mouth || 0, + }; } diff --git a/src/ts/components/tools/tools-chat/tools-chat.ts b/src/ts/components/tools/tools-chat/tools-chat.ts index 3929340..ae4ef66 100644 --- a/src/ts/components/tools/tools-chat/tools-chat.ts +++ b/src/ts/components/tools/tools-chat/tools-chat.ts @@ -1,11 +1,11 @@ import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { - drawSpeechBaloon, drawNamePlate, createCommonPalettes, drawBaloon, DrawNameFlags + drawSpeechBaloon, drawNamePlate, createCommonPalettes, drawBaloon, DrawNameFlags } from '../../../graphics/graphicsUtils'; import { drawCanvas } from '../../../graphics/contextSpriteBatch'; import { - GRASS_COLOR, getMessageColor, OUTLINE_COLOR, MOD_COLOR, ADMIN_COLOR, PATREON_COLOR, ANNOUNCEMENT_COLOR, - WHITE, PARTY_COLOR, RED, ORANGE, PURPLE, GREEN, YELLOW, BLUE, BLACK, CYAN, TRANSPARENT, WHISPER_COLOR + GRASS_COLOR, getMessageColor, OUTLINE_COLOR, MOD_COLOR, ADMIN_COLOR, PATREON_COLOR, ANNOUNCEMENT_COLOR, + WHITE, PARTY_COLOR, RED, ORANGE, PURPLE, GREEN, YELLOW, BLUE, BLACK, CYAN, TRANSPARENT, WHISPER_COLOR } from '../../../common/colors'; import { loadAndInitSpriteSheets } from '../../../client/spriteUtils'; import { MessageType, FontPalettes, Palette } from '../../../common/interfaces'; @@ -19,173 +19,173 @@ import { rect } from '../../../common/rect'; import { colorToCSS } from '../../../common/color'; interface Message { - label: string; - color: number; - palette?: (palettes: FontPalettes) => Palette | undefined; + label: string; + color: number; + palette?: (palettes: FontPalettes) => Palette | undefined; } @Component({ - selector: 'tools-chat', - templateUrl: 'tools-chat.pug', + selector: 'tools-chat', + templateUrl: 'tools-chat.pug', }) export class ToolsChat implements AfterViewInit { - readonly homeIcon = faHome; - readonly starIcon = faStar; - @ViewChild('canvas', { static: true }) element!: ElementRef; - messages: Message[] = [ - { label: 'Chat message', color: getMessageColor(MessageType.Chat) }, - { label: 'System message', color: getMessageColor(MessageType.System) }, - { label: 'Admin message', color: getMessageColor(MessageType.Admin) }, - { label: 'Mod message', color: getMessageColor(MessageType.Mod) }, - { label: 'Announcement message', color: getMessageColor(MessageType.Announcement) }, - { label: 'Party message', color: getMessageColor(MessageType.Party) }, - { label: 'Thinking message', color: getMessageColor(MessageType.Thinking) }, - { label: 'PartyThinking message', color: getMessageColor(MessageType.PartyThinking) }, - { label: 'PartyAnnouncement message', color: getMessageColor(MessageType.PartyAnnouncement) }, - // { - // label: 'PartyAnnouncement msg ⚧', color: WHITE, - // palette: () => mockPaletteManager.add([ - // TRANSPARENT, - // ANNOUNCEMENT_COLOR, - // PARTY_COLOR, ANNOUNCEMENT_COLOR, - // PARTY_COLOR, ANNOUNCEMENT_COLOR, - // PARTY_COLOR, ANNOUNCEMENT_COLOR, - // PARTY_COLOR, ANNOUNCEMENT_COLOR, - // PARTY_COLOR, ANNOUNCEMENT_COLOR, - // ]), - // }, - // { - // label: 'PartyAnnouncement msg ⚧', color: WHITE, - // palette: () => mockPaletteManager.add([ - // TRANSPARENT, - // ANNOUNCEMENT_COLOR, - // ANNOUNCEMENT_COLOR, ANNOUNCEMENT_COLOR, - // ANNOUNCEMENT_COLOR, ANNOUNCEMENT_COLOR, - // ANNOUNCEMENT_COLOR, PARTY_COLOR, - // PARTY_COLOR, PARTY_COLOR, - // PARTY_COLOR, PARTY_COLOR, - // ]), - // }, - // { label: 'Red message', color: getMessageColor(MessageType.Red) }, - // { label: 'Green message', color: getMessageColor(MessageType.Green) }, - // { label: 'Blue message', color: getMessageColor(MessageType.Blue) }, - { label: 'Supporter message 1', color: getMessageColor(MessageType.Supporter1) }, - { label: 'Supporter message 2', color: WHITE, palette: p => p.supporter2 }, - { label: 'Supporter message 3', color: WHITE, palette: p => p.supporter3 }, - { label: 'Whisper message', color: WHISPER_COLOR }, - // { label: 'Supporter message 2', color: 0xffd45aff }, - ]; - names = [ - { label: 'Regular name', color: WHITE, font: () => fontPal }, - { label: 'Party name', color: PARTY_COLOR, font: () => fontPal }, - { label: 'MODERATOR tag', color: MOD_COLOR, font: () => fontSmallPal }, - { label: 'DEVELOPER tag', color: ADMIN_COLOR, font: () => fontSmallPal }, - { label: 'SUPPORTER tag', color: PATREON_COLOR, font: () => fontSmallPal }, - { label: 'HIDDEN tag', color: ANNOUNCEMENT_COLOR, font: () => fontSmallPal }, - ]; - private bg = GRASS_COLOR; - private initialized = false; - ngAfterViewInit() { - loadAndInitSpriteSheets() - .then(() => this.initialized = true) - .then(() => this.redraw()); - } - toggleBg() { - this.bg = this.bg === GRASS_COLOR ? 0x172e14ff : GRASS_COLOR; - this.redraw(); - } - redraw() { - if (!this.initialized) - return; + readonly homeIcon = faHome; + readonly starIcon = faStar; + @ViewChild('canvas', { static: true }) element!: ElementRef; + messages: Message[] = [ + { label: 'Chat message', color: getMessageColor(MessageType.Chat) }, + { label: 'System message', color: getMessageColor(MessageType.System) }, + { label: 'Admin message', color: getMessageColor(MessageType.Admin) }, + { label: 'Mod message', color: getMessageColor(MessageType.Mod) }, + { label: 'Announcement message', color: getMessageColor(MessageType.Announcement) }, + { label: 'Party message', color: getMessageColor(MessageType.Party) }, + { label: 'Thinking message', color: getMessageColor(MessageType.Thinking) }, + { label: 'PartyThinking message', color: getMessageColor(MessageType.PartyThinking) }, + { label: 'PartyAnnouncement message', color: getMessageColor(MessageType.PartyAnnouncement) }, + // { + // label: 'PartyAnnouncement msg ⚧', color: WHITE, + // palette: () => mockPaletteManager.add([ + // TRANSPARENT, + // ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // PARTY_COLOR, ANNOUNCEMENT_COLOR, + // ]), + // }, + // { + // label: 'PartyAnnouncement msg ⚧', color: WHITE, + // palette: () => mockPaletteManager.add([ + // TRANSPARENT, + // ANNOUNCEMENT_COLOR, + // ANNOUNCEMENT_COLOR, ANNOUNCEMENT_COLOR, + // ANNOUNCEMENT_COLOR, ANNOUNCEMENT_COLOR, + // ANNOUNCEMENT_COLOR, PARTY_COLOR, + // PARTY_COLOR, PARTY_COLOR, + // PARTY_COLOR, PARTY_COLOR, + // ]), + // }, + // { label: 'Red message', color: getMessageColor(MessageType.Red) }, + // { label: 'Green message', color: getMessageColor(MessageType.Green) }, + // { label: 'Blue message', color: getMessageColor(MessageType.Blue) }, + { label: 'Supporter message 1', color: getMessageColor(MessageType.Supporter1) }, + { label: 'Supporter message 2', color: WHITE, palette: p => p.supporter2 }, + { label: 'Supporter message 3', color: WHITE, palette: p => p.supporter3 }, + { label: 'Whisper message', color: WHISPER_COLOR }, + // { label: 'Supporter message 2', color: 0xffd45aff }, + ]; + names = [ + { label: 'Regular name', color: WHITE, font: () => fontPal }, + { label: 'Party name', color: PARTY_COLOR, font: () => fontPal }, + { label: 'MODERATOR tag', color: MOD_COLOR, font: () => fontSmallPal }, + { label: 'DEVELOPER tag', color: ADMIN_COLOR, font: () => fontSmallPal }, + { label: 'SUPPORTER tag', color: PATREON_COLOR, font: () => fontSmallPal }, + { label: 'HIDDEN tag', color: ANNOUNCEMENT_COLOR, font: () => fontSmallPal }, + ]; + private bg = GRASS_COLOR; + private initialized = false; + ngAfterViewInit() { + loadAndInitSpriteSheets() + .then(() => this.initialized = true) + .then(() => this.redraw()); + } + toggleBg() { + this.bg = this.bg === GRASS_COLOR ? 0x172e14ff : GRASS_COLOR; + this.redraw(); + } + redraw() { + if (!this.initialized) + return; - const canvas1 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => { - const palettes = createCommonPalettes(mockPaletteManager); + const canvas1 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => { + const palettes = createCommonPalettes(mockPaletteManager); - this.messages.forEach(({ label, color, palette }, index) => { - const size = measureText(label, fontPal); - const options = { palette: palette ? palette(palettes.mainFont) : palettes.mainFont.white }; - drawSpeechBaloon(batch, label, color, options, 10 + size.w / 2, 20 + 20 * index, size.w, size.h, 1, 5); - }); + this.messages.forEach(({ label, color, palette }, index) => { + const size = measureText(label, fontPal); + const options = { palette: palette ? palette(palettes.mainFont) : palettes.mainFont.white }; + drawSpeechBaloon(batch, label, color, options, 10 + size.w / 2, 20 + 20 * index, size.w, size.h, 1, 5); + }); - this.names.forEach(({ label, color, font }, index) => { - const palette = font() === fontSmallPal ? palettes.smallFont.white : palettes.mainFont.white; - drawOutlinedText(batch, label, font(), color, OUTLINE_COLOR, 190, 10 + 15 * index, { palette }); - }); + this.names.forEach(({ label, color, font }, index) => { + const palette = font() === fontSmallPal ? palettes.smallFont.white : palettes.mainFont.white; + drawOutlinedText(batch, label, font(), color, OUTLINE_COLOR, 190, 10 + 15 * index, { palette }); + }); - // --- + // --- - drawOutlinedText( - batch, '', fontSmallPal, PATREON_COLOR, OUTLINE_COLOR, 190, 120, { palette: palettes.smallFont.white }); + drawOutlinedText( + batch, '', fontSmallPal, PATREON_COLOR, OUTLINE_COLOR, 190, 120, { palette: palettes.smallFont.white }); - // names + // names - drawNamePlate(batch, 'Some name 1', 220, 150, DrawNameFlags.None, palettes, 'sup1'); - drawNamePlate(batch, 'Some name 2', 220, 175, DrawNameFlags.None, palettes, 'sup2'); - drawNamePlate(batch, 'Some name 3', 220, 200, DrawNameFlags.None, palettes, 'sup3'); + drawNamePlate(batch, 'Some name 1', 220, 150, DrawNameFlags.None, palettes, 'sup1'); + drawNamePlate(batch, 'Some name 2', 220, 175, DrawNameFlags.None, palettes, 'sup2'); + drawNamePlate(batch, 'Some name 3', 220, 200, DrawNameFlags.None, palettes, 'sup3'); - // speech baloons + // speech baloons - drawBaloon( - batch, { message: 'regular baloon', type: MessageType.Chat, created: 0 }, - 50, 300, rect(0, 0, 1000, 1000), palettes); - drawBaloon( - batch, { message: 'thinking baloon', type: MessageType.Thinking, created: 0 }, - 50, 330, rect(0, 0, 1000, 1000), palettes); - drawBaloon( - batch, { message: 'whisper baloon', type: MessageType.Whisper, created: 0 }, - 50, 360, rect(0, 0, 1000, 1000), palettes); - }); + drawBaloon( + batch, { message: 'regular baloon', type: MessageType.Chat, created: 0 }, + 50, 300, rect(0, 0, 1000, 1000), palettes); + drawBaloon( + batch, { message: 'thinking baloon', type: MessageType.Thinking, created: 0 }, + 50, 330, rect(0, 0, 1000, 1000), palettes); + drawBaloon( + batch, { message: 'whisper baloon', type: MessageType.Whisper, created: 0 }, + 50, 360, rect(0, 0, 1000, 1000), palettes); + }); - const canvas2 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => { - const emojiPalette = mockPaletteManager.addArray(sprites.emojiPalette); + const canvas2 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => { + const emojiPalette = mockPaletteManager.addArray(sprites.emojiPalette); - for (let i = 0; i < sprites.emojiPal.length; i++) { - const x = i % 10; - const y = Math.floor(i / 10); - batch.drawSprite(sprites.emojiPal[i].sprite, WHITE, emojiPalette, 10 + x * 12, 10 + y * 12); - } + for (let i = 0; i < sprites.emojiPal.length; i++) { + const x = i % 10; + const y = Math.floor(i / 10); + batch.drawSprite(sprites.emojiPal[i].sprite, WHITE, emojiPalette, 10 + x * 12, 10 + y * 12); + } - const palette = mockPaletteManager.addArray(sprites.fontSupporter2Palette); - drawText(batch, 'Lorem 🍎 ipsum', fontPal, WHITE, 10, 150, { palette, emojiPalette }); + const palette = mockPaletteManager.addArray(sprites.fontSupporter2Palette); + drawText(batch, 'Lorem 🍎 ipsum', fontPal, WHITE, 10, 150, { palette, emojiPalette }); - const palette1 = mockPaletteManager.addArray(sprites.fontSupporter1Palette); - drawText(batch, '', fontSmallPal, WHITE, 20, 180, { palette: palette1 }); + const palette1 = mockPaletteManager.addArray(sprites.fontSupporter1Palette); + drawText(batch, '', fontSmallPal, WHITE, 20, 180, { palette: palette1 }); - const palette2 = mockPaletteManager.addArray(sprites.fontSupporter2Palette); - drawText(batch, '', fontSmallPal, WHITE, 20, 195, { palette: palette2 }); + const palette2 = mockPaletteManager.addArray(sprites.fontSupporter2Palette); + drawText(batch, '', fontSmallPal, WHITE, 20, 195, { palette: palette2 }); - let palette3 = mockPaletteManager.add([TRANSPARENT, RED, BLUE, ORANGE, WHITE, PURPLE, BLACK, GREEN, CYAN, YELLOW]); - palette3 = mockPaletteManager.addArray(sprites.fontSupporter3Palette); - drawText(batch, '', fontSmallPal, WHITE, 20, 210, { palette: palette3 }); - }); + let palette3 = mockPaletteManager.add([TRANSPARENT, RED, BLUE, ORANGE, WHITE, PURPLE, BLACK, GREEN, CYAN, YELLOW]); + palette3 = mockPaletteManager.addArray(sprites.fontSupporter3Palette); + drawText(batch, '', fontSmallPal, WHITE, 20, 210, { palette: palette3 }); + }); - const canvas3 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => { - /* tslint:disable */ - const loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce scelerisque interdum scelerisque. Suspendisse malesuada, enim in viverra ornare, dui ex laoreet ipsum, at mollis orci felis vitae ipsum. In faucibus venenatis augue, ac ornare libero. Etiam vitae aliquet neque.'; - const text = lineBreak(loremIpsum, fontPal, 200); + const canvas3 = drawCanvas(500, 400, sprites.paletteSpriteSheet, undefined, batch => { + /* tslint:disable */ + const loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce scelerisque interdum scelerisque. Suspendisse malesuada, enim in viverra ornare, dui ex laoreet ipsum, at mollis orci felis vitae ipsum. In faucibus venenatis augue, ac ornare libero. Etiam vitae aliquet neque.'; + const text = lineBreak(loremIpsum, fontPal, 200); - batch.drawRect(WHITE, 10, 10, 200, 100); - drawText(batch, text, fontPal, BLACK, 10, 10); + batch.drawRect(WHITE, 10, 10, 200, 100); + drawText(batch, text, fontPal, BLACK, 10, 10); - const bounds = rect(10 + 200 + 20, 10, 200, 100); - batch.drawRect(WHITE, bounds.x, bounds.y, bounds.w, bounds.h); - drawTextAligned(batch, text, fontPal, BLACK, bounds, HAlign.Right); + const bounds = rect(10 + 200 + 20, 10, 200, 100); + batch.drawRect(WHITE, bounds.x, bounds.y, bounds.w, bounds.h); + drawTextAligned(batch, text, fontPal, BLACK, bounds, HAlign.Right); - const bounds2 = rect(10 + 200 + 20, 10 + 120, 200, 20); - batch.drawRect(WHITE, bounds2.x, bounds2.y, bounds2.w, bounds2.h); - drawTextAligned(batch, 'test text', fontPal, BLACK, bounds2, HAlign.Right); - }); + const bounds2 = rect(10 + 200 + 20, 10 + 120, 200, 20); + batch.drawRect(WHITE, bounds2.x, bounds2.y, bounds2.w, bounds2.h); + drawTextAligned(batch, 'test text', fontPal, BLACK, bounds2, HAlign.Right); + }); - const canvas = this.element.nativeElement as HTMLCanvasElement; - const context = canvas.getContext('2d')!; - context.fillStyle = colorToCSS(this.bg); - context.fillRect(0, 0, canvas.width, canvas.height); - disableImageSmoothing(context); - context.save(); - context.scale(2, 2); - context.drawImage(canvas1, 0, 0); - context.drawImage(canvas2, 300, 0); - context.drawImage(canvas3, 0, 400); - context.restore(); - } + const canvas = this.element.nativeElement as HTMLCanvasElement; + const context = canvas.getContext('2d')!; + context.fillStyle = colorToCSS(this.bg); + context.fillRect(0, 0, canvas.width, canvas.height); + disableImageSmoothing(context); + context.save(); + context.scale(2, 2); + context.drawImage(canvas1, 0, 0); + context.drawImage(canvas2, 300, 0); + context.drawImage(canvas3, 0, 400); + context.restore(); + } } diff --git a/src/ts/components/tools/tools-collisions/tools-collisions.ts b/src/ts/components/tools/tools-collisions/tools-collisions.ts index 8abcd64..66389ce 100644 --- a/src/ts/components/tools/tools-collisions/tools-collisions.ts +++ b/src/ts/components/tools/tools-collisions/tools-collisions.ts @@ -10,645 +10,645 @@ const tileWidth = 32 * pixelSize; const tileHeight = 24 * pixelSize; interface ExPoint extends Point { - type?: string; + type?: string; } function toWorldX(x: number) { - return x / tileWidth; + return x / tileWidth; } function toWorldY(y: number) { - return y / tileHeight; + return y / tileHeight; } function toWorld(pt: Point) { - return { x: toWorldX(pt.x), y: toWorldY(pt.y) }; + return { x: toWorldX(pt.x), y: toWorldY(pt.y) }; } function toScreenX(x: number) { - return x * tileWidth; + return x * tileWidth; } function toScreenY(y: number) { - return y * tileHeight; + return y * tileHeight; } function toScreen(pt: Point) { - return { x: toScreenX(pt.x), y: toScreenY(pt.y) }; + return { x: toScreenX(pt.x), y: toScreenY(pt.y) }; } @Component({ - selector: 'tools-collisions', - templateUrl: 'tools-collisions.pug', + selector: 'tools-collisions', + templateUrl: 'tools-collisions.pug', }) export class ToolsCollisions implements OnInit { - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - start: Point = { x: 0, y: 0 }; - target: Point = { x: 0, y: 0 }; - steps: Point[] = []; - deflection: Point = { x: 0, y: 0 }; - collided = false; - rects: Rect[] = [ - { x: 1, y: 1, w: 1, h: 1 }, - { x: 3, y: 1.5, w: 0.5, h: 1 }, - { x: 3.5, y: 1.5, w: 0.5, h: 1.5 }, - { x: 2, y: 4, w: 2, h: 1 }, - { x: 3.5, y: 3.5, w: 1, h: 1 }, - ]; - collider = new Uint8Array(6 * 32 * 6 * 24); - colliderCanvas!: HTMLCanvasElement; - ngOnInit() { - const line = 6 * 32; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + start: Point = { x: 0, y: 0 }; + target: Point = { x: 0, y: 0 }; + steps: Point[] = []; + deflection: Point = { x: 0, y: 0 }; + collided = false; + rects: Rect[] = [ + { x: 1, y: 1, w: 1, h: 1 }, + { x: 3, y: 1.5, w: 0.5, h: 1 }, + { x: 3.5, y: 1.5, w: 0.5, h: 1.5 }, + { x: 2, y: 4, w: 2, h: 1 }, + { x: 3.5, y: 3.5, w: 1, h: 1 }, + ]; + collider = new Uint8Array(6 * 32 * 6 * 24); + colliderCanvas!: HTMLCanvasElement; + ngOnInit() { + const line = 6 * 32; - for (const r of this.rects) { - const x0 = Math.floor(r.x * 32); - const y0 = Math.floor(r.y * 24); - const x1 = x0 + Math.floor(r.w * 32); - const y1 = y0 + Math.floor(r.h * 24); + for (const r of this.rects) { + const x0 = Math.floor(r.x * 32); + const y0 = Math.floor(r.y * 24); + const x1 = x0 + Math.floor(r.w * 32); + const y1 = y0 + Math.floor(r.h * 24); - for (let y = y0; y < y1; y++) { - for (let x = x0; x < x1; x++) { - this.collider[x + y * line] = 1; - } - } - } + for (let y = y0; y < y1; y++) { + for (let x = x0; x < x1; x++) { + this.collider[x + y * line] = 1; + } + } + } - for (let y = 60, x0 = 40; y < 70; y++ , x0++) { - for (let x = x0; x < 60; x++) { - this.collider[x + y * line] = 1; - } - } + for (let y = 60, x0 = 40; y < 70; y++ , x0++) { + for (let x = x0; x < 60; x++) { + this.collider[x + y * line] = 1; + } + } - for (let y = 70, x0 = 80; y < 80; y++ , x0--) { - for (let x = x0; x < (x0 + 20); x++) { - this.collider[x + y * line] = 1; - } - } + for (let y = 70, x0 = 80; y < 80; y++ , x0--) { + for (let x = x0; x < (x0 + 20); x++) { + this.collider[x + y * line] = 1; + } + } - for (let y = 10, x0 = 80; y < 20; y++ , x0 -= 2) { - for (let x = x0; x < (x0 + 20); x++) { - this.collider[x + y * line] = 1; - } - } + for (let y = 10, x0 = 80; y < 20; y++ , x0 -= 2) { + for (let x = x0; x < (x0 + 20); x++) { + this.collider[x + y * line] = 1; + } + } - this.colliderCanvas = createCanvas(6 * 32, 6 * 24); - const context = this.colliderCanvas.getContext('2d')!; - const data = context.getImageData(0, 0, this.colliderCanvas.width, this.colliderCanvas.height); + this.colliderCanvas = createCanvas(6 * 32, 6 * 24); + const context = this.colliderCanvas.getContext('2d')!; + const data = context.getImageData(0, 0, this.colliderCanvas.width, this.colliderCanvas.height); - for (let i = 0; i < this.collider.length; i++) { - if (this.collider[i] !== 0) { - data.data[i * 4 + 0] = 255; - data.data[i * 4 + 1] = 255; - data.data[i * 4 + 2] = 0; - data.data[i * 4 + 3] = 255; - } - } + for (let i = 0; i < this.collider.length; i++) { + if (this.collider[i] !== 0) { + data.data[i * 4 + 0] = 255; + data.data[i * 4 + 1] = 255; + data.data[i * 4 + 2] = 0; + data.data[i * 4 + 3] = 255; + } + } - context.putImageData(data, 0, 0); + context.putImageData(data, 0, 0); - this.draw(); - } - drag({ type, x, y, dx, dy, event: { shiftKey } }: AgDragEvent) { - if (type === 'start') { - this.start = toWorld({ x, y }); - } + this.draw(); + } + drag({ type, x, y, dx, dy, event: { shiftKey } }: AgDragEvent) { + if (type === 'start') { + this.start = toWorld({ x, y }); + } - if (shiftKey) { - if (Math.abs(dx) > Math.abs(dy)) { - this.target = toWorld({ x, y: toScreenY(this.start.y) }); - } else { - this.target = toWorld({ x: toScreenX(this.start.x), y }); - } - } else { - this.target = toWorld({ x, y }); - } + if (shiftKey) { + if (Math.abs(dx) > Math.abs(dy)) { + this.target = toWorld({ x, y: toScreenY(this.start.y) }); + } else { + this.target = toWorld({ x: toScreenX(this.start.x), y }); + } + } else { + this.target = toWorld({ x, y }); + } - this.draw(); - } - draw() { - if (true) { - // roundPosition(this.start); - // roundPosition(this.target); + this.draw(); + } + draw() { + if (true) { + // roundPosition(this.start); + // roundPosition(this.target); - this.steps = []; - this.collided = false; + this.steps = []; + this.collided = false; - let steps = 100; - const current = point(this.start.x, this.start.y); + let steps = 100; + const current = point(this.start.x, this.start.y); - while (--steps > 0) { - const collision = point(0, 0); + while (--steps > 0) { + const collision = point(0, 0); - if (isColliding(current.x, current.y, this.target.x, this.target.y, this.rects, collision)) { - this.collided = true; - this.steps.push(collision); - current.x = collision.x; - current.y = collision.y; - } else { - this.steps.push(point(this.target.x, this.target.y)); - } + if (isColliding(current.x, current.y, this.target.x, this.target.y, this.rects, collision)) { + this.collided = true; + this.steps.push(collision); + current.x = collision.x; + current.y = collision.y; + } else { + this.steps.push(point(this.target.x, this.target.y)); + } - break; - } + break; + } - if (steps <= 0) { - console.error('Failed'); - } - } else { - const collision = getClosestCollisionOld(this.start, this.target, this.rects); + if (steps <= 0) { + console.error('Failed'); + } + } else { + const collision = getClosestCollisionOld(this.start, this.target, this.rects); - if (equal(this.target, collision)) { - this.deflection = { ...this.target }; - } else { - const coll = getClosestCollisionOld(collision, this.target, this.rects); + if (equal(this.target, collision)) { + this.deflection = { ...this.target }; + } else { + const coll = getClosestCollisionOld(collision, this.target, this.rects); - if (equal(collision, coll)) { - const horizontal = getClosestCollisionOld(collision, { x: this.target.x, y: collision.y }, this.rects); - const vertical = getClosestCollisionOld(collision, { x: collision.x, y: this.target.y }, this.rects); - this.deflection = equal(collision, horizontal) ? vertical : horizontal; - } else { - console.log('not', collision, coll); - } - } - } + if (equal(collision, coll)) { + const horizontal = getClosestCollisionOld(collision, { x: this.target.x, y: collision.y }, this.rects); + const vertical = getClosestCollisionOld(collision, { x: collision.x, y: this.target.y }, this.rects); + this.deflection = equal(collision, horizontal) ? vertical : horizontal; + } else { + console.log('not', collision, coll); + } + } + } - const checkedTiles = new Set(); + const checkedTiles = new Set(); - // plot + // plot - { - let srcX = this.start.x; - let srcY = this.start.y; - let dstX = this.target.x; - let dstY = this.target.y; + { + let srcX = this.start.x; + let srcY = this.start.y; + let dstX = this.target.x; + let dstY = this.target.y; - if (srcX > dstX) { - const tx = srcX; - const ty = srcY; - srcX = dstX; - srcY = dstY; - dstX = tx; - dstY = ty; - } + if (srcX > dstX) { + const tx = srcX; + const ty = srcY; + srcX = dstX; + srcY = dstY; + dstX = tx; + dstY = ty; + } - const x0 = Math.floor(srcX); - const y0 = Math.floor(srcY); - const x1 = Math.floor(dstX); - const y1 = Math.floor(dstY); + const x0 = Math.floor(srcX); + const y0 = Math.floor(srcY); + const x1 = Math.floor(dstX); + const y1 = Math.floor(dstY); - let steps = 100; - let x = x0; - let y = y0; + let steps = 100; + let x = x0; + let y = y0; - const DYbyDX = (dstY - srcY) / (dstX - srcX); + const DYbyDX = (dstY - srcY) / (dstX - srcX); - checkedTiles.add(`${x}-${y}`); + checkedTiles.add(`${x}-${y}`); - if (srcY < dstY) { - while (--steps && (x !== x1 || y !== y1)) { - const dx = (x + 1) - srcX; - const dy = dx * DYbyDX; - const ay = srcY + dy; + if (srcY < dstY) { + while (--steps && (x !== x1 || y !== y1)) { + const dx = (x + 1) - srcX; + const dy = dx * DYbyDX; + const ay = srcY + dy; - if (ay < (y + 1)) { - x++; - } else { - y++; - } + if (ay < (y + 1)) { + x++; + } else { + y++; + } - checkedTiles.add(`${x}-${y}`); - } - } else { - while (--steps && (x !== x1 || y !== y1)) { - const dx = (x + 1) - srcX; - const dy = dx * DYbyDX; - const ay = srcY + dy; + checkedTiles.add(`${x}-${y}`); + } + } else { + while (--steps && (x !== x1 || y !== y1)) { + const dx = (x + 1) - srcX; + const dy = dx * DYbyDX; + const ay = srcY + dy; - if (ay >= y) { - x++; - } else { - y--; - } + if (ay >= y) { + x++; + } else { + y--; + } - checkedTiles.add(`${x}-${y}`); - } - } - } + checkedTiles.add(`${x}-${y}`); + } + } + } - // end + // end - const result = checkInLine( - this.start.x * 32, this.start.y * 24, - this.target.x * 32, this.target.y * 24, - this.collider); + const result = checkInLine( + this.start.x * 32, this.start.y * 24, + this.target.x * 32, this.target.y * 24, + this.collider); - const canvas = this.canvas.nativeElement as HTMLCanvasElement; - const context = canvas.getContext('2d')!; + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const context = canvas.getContext('2d')!; - context.fillStyle = '#444'; - context.fillRect(0, 0, canvas.width, canvas.height); + context.fillStyle = '#444'; + context.fillRect(0, 0, canvas.width, canvas.height); - const xs = Math.ceil(canvas.width / tileWidth); - const ys = Math.ceil(canvas.height / tileHeight); + const xs = Math.ceil(canvas.width / tileWidth); + const ys = Math.ceil(canvas.height / tileHeight); - context.save(); - context.fillStyle = '#533'; - context.globalAlpha = 0.5; + context.save(); + context.fillStyle = '#533'; + context.globalAlpha = 0.5; - for (let y = 0; y < ys; y++) { - for (let x = 0; x < xs; x++) { - if (checkedTiles.has(`${x}-${y}`)) { - context.fillRect(x * tileWidth, y * tileHeight, tileWidth - 1, tileHeight - 1); - } - } - } + for (let y = 0; y < ys; y++) { + for (let x = 0; x < xs; x++) { + if (checkedTiles.has(`${x}-${y}`)) { + context.fillRect(x * tileWidth, y * tileHeight, tileWidth - 1, tileHeight - 1); + } + } + } - context.restore(); + context.restore(); - context.save(); - context.scale(pixelSize, pixelSize); - context.globalAlpha = 0.2; - disableImageSmoothing(context); - context.drawImage(this.colliderCanvas, 0, 0); - context.restore(); + context.save(); + context.scale(pixelSize, pixelSize); + context.globalAlpha = 0.2; + disableImageSmoothing(context); + context.drawImage(this.colliderCanvas, 0, 0); + context.restore(); - context.save(); - context.strokeStyle = 'white'; - context.globalAlpha = 0.05; - context.beginPath(); + context.save(); + context.strokeStyle = 'white'; + context.globalAlpha = 0.05; + context.beginPath(); - for (let y = 0; y < canvas.height; y += pixelSize) { - context.moveTo(0, round5(y)); - context.lineTo(canvas.width, round5(y)); - } + for (let y = 0; y < canvas.height; y += pixelSize) { + context.moveTo(0, round5(y)); + context.lineTo(canvas.width, round5(y)); + } - for (let x = 0; x < canvas.width; x += pixelSize) { - context.moveTo(round5(x), 0); - context.lineTo(round5(x), canvas.height); - } + for (let x = 0; x < canvas.width; x += pixelSize) { + context.moveTo(round5(x), 0); + context.lineTo(round5(x), canvas.height); + } - context.stroke(); - context.restore(); + context.stroke(); + context.restore(); - context.save(); - context.strokeStyle = 'white'; - context.globalAlpha = 0.15; - context.beginPath(); + context.save(); + context.strokeStyle = 'white'; + context.globalAlpha = 0.15; + context.beginPath(); - for (let y = 0; y < canvas.height; y += tileHeight) { - context.moveTo(0, round5(y)); - context.lineTo(canvas.width, round5(y)); - } + for (let y = 0; y < canvas.height; y += tileHeight) { + context.moveTo(0, round5(y)); + context.lineTo(canvas.width, round5(y)); + } - for (let x = 0; x < canvas.width; x += tileWidth) { - context.moveTo(round5(x), 0); - context.lineTo(round5(x), canvas.height); - } + for (let x = 0; x < canvas.width; x += tileWidth) { + context.moveTo(round5(x), 0); + context.lineTo(round5(x), canvas.height); + } - context.stroke(); - context.restore(); + context.stroke(); + context.restore(); - context.save(); - context.fillStyle = 'lime'; - context.globalAlpha = 0.2; + context.save(); + context.fillStyle = 'lime'; + context.globalAlpha = 0.2; - let collided = false; + let collided = false; - for (const pt of result.checks) { - const colliding = isCollidingWithRect(pt.x, pt.y, this.rects); - context.fillStyle = pt.type === 'break' ? 'blue' : 'lime'; // colliding ? 'red' : (collided ? 'orange' : 'lime'); - collided = collided || colliding; - context.fillRect(pt.x * pixelSize, pt.y * pixelSize, pixelSize, pixelSize); - } + for (const pt of result.checks) { + const colliding = isCollidingWithRect(pt.x, pt.y, this.rects); + context.fillStyle = pt.type === 'break' ? 'blue' : 'lime'; // colliding ? 'red' : (collided ? 'orange' : 'lime'); + collided = collided || colliding; + context.fillRect(pt.x * pixelSize, pt.y * pixelSize, pixelSize, pixelSize); + } - context.restore(); + context.restore(); - // context.save(); - // context.strokeStyle = 'orange'; - // context.setLineDash([3, 3]); + // context.save(); + // context.strokeStyle = 'orange'; + // context.setLineDash([3, 3]); - // for (const rect of this.rects) { - // context.strokeRect( - // round5(toScreenX(rect.x)), round5(toScreenY(rect.y)), - // Math.round(toScreenX(rect.w)), Math.round(toScreenY(rect.h))); - // } + // for (const rect of this.rects) { + // context.strokeRect( + // round5(toScreenX(rect.x)), round5(toScreenY(rect.y)), + // Math.round(toScreenX(rect.w)), Math.round(toScreenY(rect.h))); + // } - // context.restore(); + // context.restore(); - drawLine(context, toScreen(this.start), toScreen(this.target), 'gray', true); + drawLine(context, toScreen(this.start), toScreen(this.target), 'gray', true); - let last = this.start; + let last = this.start; - this.steps = [point(result.result.x / 32, result.result.y / 24)]; + this.steps = [point(result.result.x / 32, result.result.y / 24)]; - for (const c of this.steps) { - drawLine(context, toScreen(last), toScreen(c), 'lime', true); - last = c; - } + for (const c of this.steps) { + drawLine(context, toScreen(last), toScreen(c), 'lime', true); + last = c; + } - drawPoint(context, toScreen(this.start), 'greenyellow'); - drawPoint(context, toScreen(this.target), 'red'); + drawPoint(context, toScreen(this.start), 'greenyellow'); + drawPoint(context, toScreen(this.target), 'red'); - for (const c of this.steps) { - const colliding = isColliding(c.x, c.y, c.x, c.y, this.rects, point(0, 0)); - drawPoint(context, toScreen(c), colliding ? 'red' : 'yellow'); - } - } + for (const c of this.steps) { + const colliding = isColliding(c.x, c.y, c.x, c.y, this.rects, point(0, 0)); + drawPoint(context, toScreen(c), colliding ? 'red' : 'yellow'); + } + } } function drawLine(context: CanvasRenderingContext2D, a: Point, b: Point, color: string, arrow = false) { - context.save(); - context.strokeStyle = color; - // context.lineWidth = 2; - context.beginPath(); - context.moveTo(a.x, a.y); - context.lineTo(b.x, b.y); - context.stroke(); - context.restore(); + context.save(); + context.strokeStyle = color; + // context.lineWidth = 2; + context.beginPath(); + context.moveTo(a.x, a.y); + context.lineTo(b.x, b.y); + context.stroke(); + context.restore(); - if (arrow && (a.x !== b.x || a.y !== b.y)) { - const scale = 0.5; - context.save(); - context.fillStyle = color; - context.translate(b.x, b.y); - context.rotate(Math.atan2(b.y - a.y, b.x - a.x)); - context.beginPath(); - context.moveTo(0, 0); - context.lineTo(-12 * scale, -6 * scale); - context.lineTo(-12 * scale, 6 * scale); - context.closePath(); - context.fill(); - context.restore(); - } + if (arrow && (a.x !== b.x || a.y !== b.y)) { + const scale = 0.5; + context.save(); + context.fillStyle = color; + context.translate(b.x, b.y); + context.rotate(Math.atan2(b.y - a.y, b.x - a.x)); + context.beginPath(); + context.moveTo(0, 0); + context.lineTo(-12 * scale, -6 * scale); + context.lineTo(-12 * scale, 6 * scale); + context.closePath(); + context.fill(); + context.restore(); + } } function drawPoint(context: CanvasRenderingContext2D, { x, y }: Point, color: string) { - context.save(); - context.shadowColor = 'black'; - context.shadowBlur = 3; - context.fillStyle = color; - context.globalAlpha = 0.5; - context.beginPath(); - context.arc(x, y, 2, 0, Math.PI * 2); - context.fill(); - context.restore(); + context.save(); + context.shadowColor = 'black'; + context.shadowBlur = 3; + context.fillStyle = color; + context.globalAlpha = 0.5; + context.beginPath(); + context.arc(x, y, 2, 0, Math.PI * 2); + context.fill(); + context.restore(); } function round5(x: number) { - return Math.ceil(x) - 0.5; + return Math.ceil(x) - 0.5; } function equal(a: Point, b: Point) { - return a.x === b.x && a.y === b.y; + return a.x === b.x && a.y === b.y; } function isColliding(srcX: number, srcY: number, dstX: number, dstY: number, rects: Rect[], collision: Point): boolean { - const temp = point(0, 0); - let collided = false; - collision.x = dstX; - collision.y = dstY; + const temp = point(0, 0); + let collided = false; + collision.x = dstX; + collision.y = dstY; - for (const r of rects) { - if (getCollision(srcX, srcY, dstX, dstY, r.x, r.y, r.x + r.w, r.y + r.h, temp)) { - if (!collided || (distanceSquaredXY(srcX, srcY, temp.x, temp.y) < distanceSquaredXY(srcX, srcY, collision.x, collision.y))) { - collision.x = temp.x; - collision.y = temp.y; - collided = true; - } - } - } + for (const r of rects) { + if (getCollision(srcX, srcY, dstX, dstY, r.x, r.y, r.x + r.w, r.y + r.h, temp)) { + if (!collided || (distanceSquaredXY(srcX, srcY, temp.x, temp.y) < distanceSquaredXY(srcX, srcY, collision.x, collision.y))) { + collision.x = temp.x; + collision.y = temp.y; + collided = true; + } + } + } - roundPosition(collision); - return collided; + roundPosition(collision); + return collided; } function getClosestCollisionOld(a: Point, b: Point, rects: Rect[]) { - return rects.reduce((pt, r) => getCollisionTest(a, pt, r) || pt, { ...b }); + return rects.reduce((pt, r) => getCollisionTest(a, pt, r) || pt, { ...b }); } function getCollisionTest({ x, y }: Point, b: Point, r: Rect): Point | undefined { - const vx = b.x - x; - const vy = b.y - y; - const p = [-vx, vx, -vy, vy]; - const q = [x - r.x, r.x + r.w - x, y - r.y, r.y + r.h - y]; - let u1 = -999999; - let u2 = 999999; + const vx = b.x - x; + const vy = b.y - y; + const p = [-vx, vx, -vy, vy]; + const q = [x - r.x, r.x + r.w - x, y - r.y, r.y + r.h - y]; + let u1 = -999999; + let u2 = 999999; - for (let i = 0; i < 4; i++) { - if (p[i] === 0) { - if (q[i] < 0) { - return undefined; - } - } else { - const t = q[i] / p[i]; + for (let i = 0; i < 4; i++) { + if (p[i] === 0) { + if (q[i] < 0) { + return undefined; + } + } else { + const t = q[i] / p[i]; - if (p[i] < 0 && u1 < t) { - u1 = t; - } else if (p[i] > 0 && u2 > t) { - u2 = t; - } - } - } + if (p[i] < 0 && u1 < t) { + u1 = t; + } else if (p[i] > 0 && u2 > t) { + u2 = t; + } + } + } - if (u1 > u2 || u1 > 1 || u1 < 0) { - return undefined; - } + if (u1 > u2 || u1 > 1 || u1 < 0) { + return undefined; + } - return { - x: x + u1 * vx, - y: y + u1 * vy, - }; + return { + x: x + u1 * vx, + y: y + u1 * vy, + }; } function isCollidingWithRect(x: number, y: number, rects: Rect[]) { - for (const r of rects) { - const x0 = Math.floor(r.x * 32) | 0; - const y0 = Math.floor(r.y * 24) | 0; - const x1 = Math.ceil((r.x + r.w) * 32) | 0; - const y1 = Math.ceil((r.y + r.h) * 24) | 0; + for (const r of rects) { + const x0 = Math.floor(r.x * 32) | 0; + const y0 = Math.floor(r.y * 24) | 0; + const x1 = Math.ceil((r.x + r.w) * 32) | 0; + const y1 = Math.ceil((r.y + r.h) * 24) | 0; - if (x >= x0 && x < x1 && y >= y0 && y < y1) { - return true; - } - } + if (x >= x0 && x < x1 && y >= y0 && y < y1) { + return true; + } + } - return false; + return false; } function checkInLine(srcX: number, srcY: number, dstX: number, dstY: number, collider: Uint8Array) { - function isColliding(x: number, y: number) { - return x < 0 || y < 0 || x >= (6 * 32) || y >= (6 * 32) || collider[x + y * (6 * 32)] !== 0; - } + function isColliding(x: number, y: number) { + return x < 0 || y < 0 || x >= (6 * 32) || y >= (6 * 32) || collider[x + y * (6 * 32)] !== 0; + } - const checks: ExPoint[] = []; - const result = point(srcX, srcY); + const checks: ExPoint[] = []; + const result = point(srcX, srcY); - 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 x0 = Math.floor(srcX) | 0; + const y0 = Math.floor(srcY) | 0; + const x1 = Math.floor(dstX) | 0; + const y1 = Math.floor(dstY) | 0; - let minX = Math.min(x0, x1) | 0; - let maxX = Math.max(x0, x1) | 0; - let minY = Math.min(y0, y1) | 0; - let maxY = Math.max(y0, y1) | 0; + let minX = Math.min(x0, x1) | 0; + let maxX = Math.max(x0, x1) | 0; + let minY = Math.min(y0, y1) | 0; + let maxY = Math.max(y0, y1) | 0; - let x = x0 | 0; - let y = y0 | 0; + let x = x0 | 0; + let y = y0 | 0; - checks.push({ x, y }); + checks.push({ x, y }); - let actualX = x | 0; - let actualY = y | 0; + let actualX = x | 0; + let actualY = y | 0; - const a = (dstY - srcY) / (dstX - srcX); - const b = srcY - a * srcX; - const useGt = srcY < dstY; + 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; + 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; + 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; - } - } + 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; + } + } - for (let steps = 1000; steps; steps--) { - const fx = a * (x + ox) + b; - const fy = y + oy; + for (let steps = 1000; steps; steps--) { + const fx = a * (x + ox) + b; + const fy = y + oy; - let tx = 0 | 0; - let ty = 0 | 0; + 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; - } + 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; + x = (x + tx) | 0; + y = (y + ty) | 0; - if (x < minX || x > maxX || y < minY || y > maxY) { - break; - } + if (x < minX || x > maxX || y < minY || y > maxY) { + break; + } - let actualNX = (actualX + tx) | 0; - let actualNY = (actualY + ty) | 0; + let actualNX = (actualX + tx) | 0; + let actualNY = (actualY + ty) | 0; - let collides = isColliding(actualNX, actualNY); - let canMove = false; + let collides = isColliding(actualNX, actualNY); + let canMove = false; - if (collides) { - if (tx !== 0) { - let canShiftUp = false; - let canShiftDown = false; + if (collides) { + if (tx !== 0) { + let canShiftUp = false; + let canShiftDown = false; - if (shiftUp && (canShiftUp = !isColliding(actualX, actualY - 1)) && !isColliding(actualNX, actualY - 1)) { - actualNX = actualX; - actualNY -= 1; - dstY -= 1; - collides = false; - } else if (shiftDown && (canShiftDown = !isColliding(actualX, actualY + 1)) && !isColliding(actualNX, actualY + 1)) { - actualNX = actualX; - actualNY += 1; - dstY += 1; - collides = false; - } else if (shiftUp && canShiftUp && !isColliding(actualNX, actualY - 2)) { - actualNX = actualX; - actualNY -= 1; - dstY -= 1; - collides = false; - } else if (shiftDown && canShiftDown && !isColliding(actualNX, actualY + 2)) { - actualNX = actualX; - actualNY += 1; - dstY += 1; - collides = false; - } + if (shiftUp && (canShiftUp = !isColliding(actualX, actualY - 1)) && !isColliding(actualNX, actualY - 1)) { + actualNX = actualX; + actualNY -= 1; + dstY -= 1; + collides = false; + } else if (shiftDown && (canShiftDown = !isColliding(actualX, actualY + 1)) && !isColliding(actualNX, actualY + 1)) { + actualNX = actualX; + actualNY += 1; + dstY += 1; + collides = false; + } else if (shiftUp && canShiftUp && !isColliding(actualNX, actualY - 2)) { + actualNX = actualX; + actualNY -= 1; + dstY -= 1; + collides = false; + } else if (shiftDown && canShiftDown && !isColliding(actualNX, actualY + 2)) { + actualNX = actualX; + actualNY += 1; + dstY += 1; + collides = false; + } - canMove = canShiftUp || canShiftDown; - } else { - let canShiftLeft = false; - let canShiftRight = false; + canMove = canShiftUp || canShiftDown; + } else { + let canShiftLeft = false; + let canShiftRight = false; - if (shiftLeft && (canShiftLeft = !isColliding(actualX - 1, actualY)) && !isColliding(actualX - 1, actualNY)) { - actualNX -= 1; - actualNY = actualY; - dstX -= 1; - collides = false; - } else if (shiftRight && (canShiftRight = !isColliding(actualX + 1, actualY)) && !isColliding(actualX + 1, actualNY)) { - actualNX += 1; - actualNY = actualY; - dstX += 1; - collides = false; - } else if (shiftLeft && canShiftLeft && !isColliding(actualX - 2, actualNY)) { - actualNX -= 1; - actualNY = actualY; - dstX -= 1; - collides = false; - } else if (shiftRight && canShiftRight && !isColliding(actualX + 2, actualNY)) { - actualNX += 1; - actualNY = actualY; - dstX += 1; - collides = false; - } + if (shiftLeft && (canShiftLeft = !isColliding(actualX - 1, actualY)) && !isColliding(actualX - 1, actualNY)) { + actualNX -= 1; + actualNY = actualY; + dstX -= 1; + collides = false; + } else if (shiftRight && (canShiftRight = !isColliding(actualX + 1, actualY)) && !isColliding(actualX + 1, actualNY)) { + actualNX += 1; + actualNY = actualY; + dstX += 1; + collides = false; + } else if (shiftLeft && canShiftLeft && !isColliding(actualX - 2, actualNY)) { + actualNX -= 1; + actualNY = actualY; + dstX -= 1; + collides = false; + } else if (shiftRight && canShiftRight && !isColliding(actualX + 2, actualNY)) { + actualNX += 1; + actualNY = actualY; + dstX += 1; + collides = false; + } - canMove = canShiftLeft || canShiftRight; - } - } + canMove = canShiftLeft || canShiftRight; + } + } - if (!collides) { - actualX = actualNX; - actualY = actualNY; - checks.push({ x: actualX, y: actualY }); - } else if (!canMove || horizontalOrVertical) { - checks.push({ x: actualX, y: actualY, type: 'break' }); - break; - } - } + if (!collides) { + actualX = actualNX; + actualY = actualNY; + checks.push({ x: actualX, y: actualY }); + } else if (!canMove || horizontalOrVertical) { + checks.push({ x: actualX, y: actualY, type: 'break' }); + 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; + 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; - result.x = clamp(dstX, left, right); - result.y = clamp(dstY, top, bottom); + result.x = clamp(dstX, left, right); + result.y = clamp(dstY, top, bottom); - return { checks, result }; + return { checks, result }; } // if (srcX < dstX) { @@ -688,94 +688,94 @@ function checkInLine(srcX: number, srcY: number, dstX: number, dstY: number, col // } function getCollision( - srcX: number, srcY: number, dstX: number, dstY: number, x0: number, y0: number, x1: number, y1: number, out: Point + srcX: number, srcY: number, dstX: number, dstY: number, x0: number, y0: number, x1: number, y1: number, out: Point ): boolean { - const vx = dstX - srcX; - const vy = dstY - srcY; - let u1 = -999999; - let u2 = 999999; + const vx = dstX - srcX; + const vy = dstY - srcY; + let u1 = -999999; + let u2 = 999999; - { - const p = -vx; - const q = srcX - x0; + { + const p = -vx; + const q = srcX - x0; - if (p === 0) { - if (q < 0) { - return false; - } - } else { - const t = q / p; + if (p === 0) { + if (q < 0) { + return false; + } + } else { + const t = q / p; - if (p < 0 && u1 < t) { - u1 = t; - } else if (p > 0 && u2 > t) { - u2 = t; - } - } - } + if (p < 0 && u1 < t) { + u1 = t; + } else if (p > 0 && u2 > t) { + u2 = t; + } + } + } - { - const p = vx; - const q = x1 - srcX; + { + const p = vx; + const q = x1 - srcX; - if (p === 0) { - if (q < 0) { - return false; - } - } else { - const t = q / p; + if (p === 0) { + if (q < 0) { + return false; + } + } else { + const t = q / p; - if (p < 0 && u1 < t) { - u1 = t; - } else if (p > 0 && u2 > t) { - u2 = t; - } - } - } + if (p < 0 && u1 < t) { + u1 = t; + } else if (p > 0 && u2 > t) { + u2 = t; + } + } + } - { - const p = -vy; - const q = srcY - y0; + { + const p = -vy; + const q = srcY - y0; - if (p === 0) { - if (q < 0) { - return false; - } - } else { - const t = q / p; + if (p === 0) { + if (q < 0) { + return false; + } + } else { + const t = q / p; - if (p < 0 && u1 < t) { - u1 = t; - } else if (p > 0 && u2 > t) { - u2 = t; - } - } - } + if (p < 0 && u1 < t) { + u1 = t; + } else if (p > 0 && u2 > t) { + u2 = t; + } + } + } - { - const p = vy; - const q = y1 - srcY; + { + const p = vy; + const q = y1 - srcY; - if (p === 0) { - if (q < 0) { - return false; - } - } else { - const t = q / p; + if (p === 0) { + if (q < 0) { + return false; + } + } else { + const t = q / p; - if (p < 0 && u1 < t) { - u1 = t; - } else if (p > 0 && u2 > t) { - u2 = t; - } - } - } + if (p < 0 && u1 < t) { + u1 = t; + } else if (p > 0 && u2 > t) { + u2 = t; + } + } + } - if (u1 > u2 || u1 > 1 || u1 < 0) { - return false; - } + if (u1 > u2 || u1 > 1 || u1 < 0) { + return false; + } - out.x = srcX + u1 * vx; - out.y = srcY + u1 * vy; - return true; + out.x = srcX + u1 * vx; + out.y = srcY + u1 * vy; + return true; } diff --git a/src/ts/components/tools/tools-entity/tools-entity.ts b/src/ts/components/tools/tools-entity/tools-entity.ts index 8cd6708..6b3a759 100644 --- a/src/ts/components/tools/tools-entity/tools-entity.ts +++ b/src/ts/components/tools/tools-entity/tools-entity.ts @@ -36,388 +36,388 @@ const X = 128; const Y = 190; const colors: Dict = { - cover: COVER, - collider: COLLIDER, - pickable: PICKABLE, + cover: COVER, + collider: COLLIDER, + pickable: PICKABLE, }; interface BasePart { - x: number; - y: number; + x: number; + y: number; } interface BoundsPart extends BasePart { - w: number; - h: number; + w: number; + h: number; } interface SpritePart extends BasePart { - type: 'sprite'; - sprite: string; + type: 'sprite'; + sprite: string; } interface CoverPart extends BoundsPart { - type: 'cover'; + type: 'cover'; } interface ColliderPart extends BoundsPart { - type: 'collider'; + type: 'collider'; } interface PickablePart extends BasePart { - type: 'pickable'; + type: 'pickable'; } type Part = SpritePart | CoverPart | ColliderPart | PickablePart; interface PartEntity { - name: string; - parts: Part[]; + name: string; + parts: Part[]; } interface EntityData { - parts?: Part[]; - entities?: PartEntity[]; + parts?: Part[]; + entities?: PartEntity[]; } @Component({ - selector: 'tools-entity', - templateUrl: 'tools-entity.pug', + selector: 'tools-entity', + templateUrl: 'tools-entity.pug', }) export class ToolsEntity implements OnInit { - readonly homeIcon = faHome; - readonly saveIcon = faSave; - readonly eraserIcon = faEraser; - readonly trashIcon = faTrash; - readonly crosshairsIcon = faCrosshairs; - readonly plusIcon = faPlus; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - scale = 2; - name = ''; - drawCenter = true; - drawSelection = true; - drawHold = false; - sprites = Object.keys(sprites).filter(key => { - const s = (sprites as any)[key] as any; - return !!(s && s.color); - }); - selectedPart = -1; - entities: PartEntity[] = []; - parts: Part[] = []; - pony = toPalette(decompressPonyString(OFFLINE_PONY), mockPaletteManager); - private startX = 0; - private startY = 0; - constructor(private storage: StorageService) { - } - ngOnInit() { - setPaletteManager(paletteManager); - loadAndInitSpriteSheets().then(() => this.changed()); + readonly homeIcon = faHome; + readonly saveIcon = faSave; + readonly eraserIcon = faEraser; + readonly trashIcon = faTrash; + readonly crosshairsIcon = faCrosshairs; + readonly plusIcon = faPlus; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + scale = 2; + name = ''; + drawCenter = true; + drawSelection = true; + drawHold = false; + sprites = Object.keys(sprites).filter(key => { + const s = (sprites as any)[key] as any; + return !!(s && s.color); + }); + selectedPart = -1; + entities: PartEntity[] = []; + parts: Part[] = []; + pony = toPalette(decompressPonyString(OFFLINE_PONY), mockPaletteManager); + private startX = 0; + private startY = 0; + constructor(private storage: StorageService) { + } + ngOnInit() { + setPaletteManager(paletteManager); + loadAndInitSpriteSheets().then(() => this.changed()); - const data = this.load(); - this.parts = compact(data.parts || [this.createSpritePart('apple')]); - this.entities = compact(data.entities || []); - } - @HostListener('window:keydown', ['$event']) - keydown(e: KeyboardEvent) { - if (!isKeyEventInvalid(e) && this.handleKey(e.keyCode)) { - e.preventDefault(); - } - } - handleKey(keyCode: number) { - if (keyCode === Key.UP) { - this.movePart(0, -1); - } else if (keyCode === Key.DOWN) { - this.movePart(0, 1); - } else if (keyCode === Key.LEFT) { - this.movePart(-1, 0); - } else if (keyCode === Key.RIGHT) { - this.movePart(1, 0); - } else { - return false; - } + const data = this.load(); + this.parts = compact(data.parts || [this.createSpritePart('apple')]); + this.entities = compact(data.entities || []); + } + @HostListener('window:keydown', ['$event']) + keydown(e: KeyboardEvent) { + if (!isKeyEventInvalid(e) && this.handleKey(e.keyCode)) { + e.preventDefault(); + } + } + handleKey(keyCode: number) { + if (keyCode === Key.UP) { + this.movePart(0, -1); + } else if (keyCode === Key.DOWN) { + this.movePart(0, 1); + } else if (keyCode === Key.LEFT) { + this.movePart(-1, 0); + } else if (keyCode === Key.RIGHT) { + this.movePart(1, 0); + } else { + return false; + } - return true; - } - mousedown(e: MouseEvent) { - const canvas = this.canvas.nativeElement as HTMLCanvasElement; - const { left, top } = canvas.getBoundingClientRect(); - const x = (e.pageX - left) / this.scale - X; - const y = (e.pageY - top) / this.scale - Y; + return true; + } + mousedown(e: MouseEvent) { + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const { left, top } = canvas.getBoundingClientRect(); + const x = (e.pageX - left) / this.scale - X; + const y = (e.pageY - top) / this.scale - Y; - this.selectedPart = findLastIndex(this.parts, p => { - if (this.drawHold) { - return p.type === 'pickable'; - } else { - const bounds = getBounds(p); - return !!bounds && containsPoint(0, 0, bounds, x, y); - } - }); + this.selectedPart = findLastIndex(this.parts, p => { + if (this.drawHold) { + return p.type === 'pickable'; + } else { + const bounds = getBounds(p); + return !!bounds && containsPoint(0, 0, bounds, x, y); + } + }); - this.changed(); - } - drag({ dx, dy, type }: AgDragEvent) { - const part = this.parts[this.selectedPart]; + this.changed(); + } + drag({ dx, dy, type }: AgDragEvent) { + const part = this.parts[this.selectedPart]; - if (part) { - if (type === 'start') { - this.startX = part.x; - this.startY = part.y; - } + if (part) { + if (type === 'start') { + this.startX = part.x; + this.startY = part.y; + } - part.x = Math.round(this.startX + dx / this.scale); - part.y = Math.round(this.startY + dy / this.scale); - } + part.x = Math.round(this.startX + dx / this.scale); + part.y = Math.round(this.startY + dy / this.scale); + } - this.changed(); - } - setEntity(entity: PartEntity | null) { - if (entity) { - this.name = entity.name; - this.parts = entity.parts; - } else { - this.name = ''; - this.parts = []; - } + this.changed(); + } + setEntity(entity: PartEntity | null) { + if (entity) { + this.name = entity.name; + this.parts = entity.parts; + } else { + this.name = ''; + this.parts = []; + } - this.changed(); - } - saveEntity() { - if (this.name) { - const existing = this.entities.find(e => e.name === this.name); + this.changed(); + } + saveEntity() { + if (this.name) { + const existing = this.entities.find(e => e.name === this.name); - if (existing) { - existing.parts = cloneDeep(this.parts); - } else { - this.entities.push({ - name: this.name, - parts: cloneDeep(this.parts), - }); - } + if (existing) { + existing.parts = cloneDeep(this.parts); + } else { + this.entities.push({ + name: this.name, + parts: cloneDeep(this.parts), + }); + } - this.changed(); - } - } - removeEntity() { - removeItem(this.entities, this.entities.find(e => e.name === this.name)); - this.changed(); - } - movePart(dx: number, dy: number) { - const part = this.parts[this.selectedPart]; + this.changed(); + } + } + removeEntity() { + removeItem(this.entities, this.entities.find(e => e.name === this.name)); + this.changed(); + } + movePart(dx: number, dy: number) { + const part = this.parts[this.selectedPart]; - if (part) { - part.x += dx; - part.y += dy; - } + if (part) { + part.x += dx; + part.y += dy; + } - this.changed(); - } - changed() { - requestAnimationFrame(() => this.redraw()); - } - redraw() { - const canvas = this.canvas.nativeElement as HTMLCanvasElement; - const scale = this.scale; - const width = Math.ceil(canvas.width / scale); - const height = Math.ceil(canvas.height / scale); + this.changed(); + } + changed() { + requestAnimationFrame(() => this.redraw()); + } + redraw() { + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const scale = this.scale; + const width = Math.ceil(canvas.width / scale); + const height = Math.ceil(canvas.height / scale); - const draw = (batch: ContextSpriteBatch) => { - if (this.drawCenter) { - batch.drawRect(LINES, 0, Y, width, 1); - batch.drawRect(LINES, X, 0, 1, height); - } + const draw = (batch: ContextSpriteBatch) => { + if (this.drawCenter) { + batch.drawRect(LINES, 0, Y, width, 1); + batch.drawRect(LINES, X, 0, 1, height); + } - this.parts.forEach(p => drawPart(batch, p, X, Y)); + this.parts.forEach(p => drawPart(batch, p, X, Y)); - const part = this.parts[this.selectedPart] as Part | undefined; + const part = this.parts[this.selectedPart] as Part | undefined; - if (part) { - const bounds = getBounds(part); + if (part) { + const bounds = getBounds(part); - if (this.drawSelection && bounds) { - const sx = X + bounds.x; - const sy = Y + bounds.y; - drawOutline(batch, SELECTION, sx - 1, sy - 1, bounds.w + 2, bounds.h + 2); - } - } + if (this.drawSelection && bounds) { + const sx = X + bounds.x; + const sy = Y + bounds.y; + drawOutline(batch, SELECTION, sx - 1, sy - 1, bounds.w + 2, bounds.h + 2); + } + } - if (this.drawCenter) { - batch.drawRect(RED, X, Y, 1, 1); - } - }; + if (this.drawCenter) { + batch.drawRect(RED, X, Y, 1, 1); + } + }; - const buffer = drawCanvas(width, height, sprites.paletteSpriteSheet, BG, batch => { - if (this.drawHold) { - const spritePart = this.parts.find(p => p.type === 'sprite') as SpritePart | undefined; - const pickablePart = this.parts.find(p => p.type === 'pickable') as PickablePart | undefined; + const buffer = drawCanvas(width, height, sprites.paletteSpriteSheet, BG, batch => { + if (this.drawHold) { + const spritePart = this.parts.find(p => p.type === 'sprite') as SpritePart | undefined; + const pickablePart = this.parts.find(p => p.type === 'pickable') as PickablePart | undefined; - const holding: Entity | undefined = spritePart && pickablePart && getSprite(spritePart.sprite) ? { - ...createBaseEntity(0, 0, 0, 0), - ...drawMixin(getSprite(spritePart.sprite), -spritePart.x, -spritePart.y), - ...pickable(pickablePart.x, pickablePart.y), - } : undefined; + const holding: Entity | undefined = spritePart && pickablePart && getSprite(spritePart.sprite) ? { + ...createBaseEntity(0, 0, 0, 0), + ...drawMixin(getSprite(spritePart.sprite), -spritePart.x, -spritePart.y), + ...pickable(pickablePart.x, pickablePart.y), + } : undefined; - const state = { ...defaultPonyState(), holding }; - drawPony(batch, this.pony, state, X, Y, defaultDrawPonyOptions()); - } else { - draw(batch); - } - }); + const state = { ...defaultPonyState(), holding }; + drawPony(batch, this.pony, state, X, Y, defaultDrawPonyOptions()); + } else { + draw(batch); + } + }); - drawBufferScaled(canvas, buffer, scale); + drawBufferScaled(canvas, buffer, scale); - this.save(); - } - createSpritePart(sprite: string): SpritePart { - return { - type: 'sprite', - sprite, - x: 0, - y: 0, - }; - } - createBoundsPart(type: any): CoverPart { - return { - type, - x: 0, - y: 0, - w: 10, - h: 10, - }; - } - createPart(type: string): Part { - switch (type) { - case 'sprite': - return this.createSpritePart('apple'); - case 'cover': - case 'collider': - return this.createBoundsPart(type); - case 'pickable': - return { type, x: 0, y: 0 }; - default: - throw new Error(`Invalid type (${type})`); - } - } - addPart(type: string) { - this.parts.push(this.createPart(type)); - this.changed(); - } - removePart(part: Part) { - removeItem(this.parts, part); - this.changed(); - } - centerPart(part: Part) { - if (part.type === 'sprite') { - const sprite = getSprite(part.sprite); - const color = sprite && sprite.color; + this.save(); + } + createSpritePart(sprite: string): SpritePart { + return { + type: 'sprite', + sprite, + x: 0, + y: 0, + }; + } + createBoundsPart(type: any): CoverPart { + return { + type, + x: 0, + y: 0, + w: 10, + h: 10, + }; + } + createPart(type: string): Part { + switch (type) { + case 'sprite': + return this.createSpritePart('apple'); + case 'cover': + case 'collider': + return this.createBoundsPart(type); + case 'pickable': + return { type, x: 0, y: 0 }; + default: + throw new Error(`Invalid type (${type})`); + } + } + addPart(type: string) { + this.parts.push(this.createPart(type)); + this.changed(); + } + removePart(part: Part) { + removeItem(this.parts, part); + this.changed(); + } + centerPart(part: Part) { + if (part.type === 'sprite') { + const sprite = getSprite(part.sprite); + const color = sprite && sprite.color; - if (color) { - part.x = Math.round(-color.ox - color.w / 2); - part.y = Math.round(-color.oy - color.h / 2); - } - } + if (color) { + part.x = Math.round(-color.ox - color.w / 2); + part.y = Math.round(-color.oy - color.h / 2); + } + } - this.changed(); - } - private save() { - this.storage.setJSON('tools-entity', { - parts: this.parts, - entities: this.entities, - }); - } - private load() { - return this.storage.getJSON('tools-entity', {}); - } + this.changed(); + } + private save() { + this.storage.setJSON('tools-entity', { + parts: this.parts, + entities: this.entities, + }); + } + private load() { + return this.storage.getJSON('tools-entity', {}); + } } function getBounds(part: Part): Rect | undefined { - if (part.type === 'sprite') { - const sprite = getSprite(part.sprite); - const color = sprite && sprite.color; + if (part.type === 'sprite') { + const sprite = getSprite(part.sprite); + const color = sprite && sprite.color; - if (color) { - return { - x: part.x + color.ox, - y: part.y + color.oy, - w: color.w, - h: color.h, - }; - } - } else if (part.type === 'cover' || part.type === 'collider') { - return part; - } + if (color) { + return { + x: part.x + color.ox, + y: part.y + color.oy, + w: color.w, + h: color.h, + }; + } + } else if (part.type === 'cover' || part.type === 'collider') { + return part; + } - return undefined; + return undefined; } function getSprite(name: string): PaletteRenderable { - return (sprites as any)[name]; + return (sprites as any)[name]; } function drawSpritePart(batch: PaletteSpriteBatch, part: SpritePart, px: number, py: number) { - const sprite = getSprite(part.sprite); + const sprite = getSprite(part.sprite); - if (!sprite) - return; + if (!sprite) + return; - const x = px + part.x; - const y = py + part.y; - const palette = paletteManager.addArray(sprite.palettes![0]); + const x = px + part.x; + const y = py + part.y; + const palette = paletteManager.addArray(sprite.palettes![0]); - sprite.shadow && batch.drawSprite(sprite.shadow, SHADOW_COLOR, defaultPalette, x, y); - sprite.color && batch.drawSprite(sprite.color, WHITE, palette, x, y); + sprite.shadow && batch.drawSprite(sprite.shadow, SHADOW_COLOR, defaultPalette, x, y); + sprite.color && batch.drawSprite(sprite.color, WHITE, palette, x, y); - releasePalette(palette); + releasePalette(palette); } function drawPart(batch: PaletteSpriteBatch, part: Part, x: number, y: number) { - if (part.type === 'sprite') { - return drawSpritePart(batch, part, x, y); - } else if (part.type === 'cover' || part.type === 'collider') { - return drawOutline(batch, colors[part.type], part.x + x, part.y + y, part.w, part.h); - } else if (part.type === 'pickable') { - return drawOutline(batch, colors[part.type], part.x + x, part.y + y, 1, 1); - } else { - throw new Error(`Invalid part type (${(part as any).type})`); - } + if (part.type === 'sprite') { + return drawSpritePart(batch, part, x, y); + } else if (part.type === 'cover' || part.type === 'collider') { + return drawOutline(batch, colors[part.type], part.x + x, part.y + y, part.w, part.h); + } else if (part.type === 'pickable') { + return drawOutline(batch, colors[part.type], part.x + x, part.y + y, 1, 1); + } else { + throw new Error(`Invalid part type (${(part as any).type})`); + } } function drawBufferScaled(canvas: HTMLCanvasElement, buffer: HTMLCanvasElement, scale: number) { - const context = canvas.getContext('2d')!; - context.save(); - disableImageSmoothing(context); - context.scale(scale, scale); - context.drawImage(buffer, 0, 0); - context.restore(); + const context = canvas.getContext('2d')!; + context.save(); + disableImageSmoothing(context); + context.scale(scale, scale); + context.drawImage(buffer, 0, 0); + context.restore(); } function drawMixin(sprite: PaletteRenderable, dx: number, dy: number, paletteIndex = 0): EntityPart { - const bounds = getRenderableBounds(sprite, dx, dy); + const bounds = getRenderableBounds(sprite, dx, dy); - if (SERVER && !TESTS) - return { bounds }; + if (SERVER && !TESTS) + return { bounds }; - const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette); - const palette = createPalette(att(sprite.palettes, paletteIndex)); + const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette); + const palette = createPalette(att(sprite.palettes, paletteIndex)); - return { - bounds, - draw(this: Entity, 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); + return { + bounds, + draw(this: Entity, 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); - } + if (sprite.shadow !== undefined) { + batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); + } - batch.globalAlpha = opacity; + batch.globalAlpha = opacity; - if (sprite.color !== undefined) { - batch.drawSprite(sprite.color, WHITE, palette, x, y); - } + if (sprite.color !== undefined) { + batch.drawSprite(sprite.color, WHITE, palette, x, y); + } - batch.globalAlpha = 1; - }, - palettes: compact([defaultPalette, palette]), - }; + batch.globalAlpha = 1; + }, + palettes: compact([defaultPalette, palette]), + }; } diff --git a/src/ts/components/tools/tools-expressions/tools-expressions.ts b/src/ts/components/tools/tools-expressions/tools-expressions.ts index abd6a7e..a869274 100644 --- a/src/ts/components/tools/tools-expressions/tools-expressions.ts +++ b/src/ts/components/tools/tools-expressions/tools-expressions.ts @@ -13,95 +13,95 @@ import { faHome } from '../../../client/icons'; import { paletteSpriteSheet } from '../../../generated/sprites'; @Component({ - selector: 'tools-expressions', - templateUrl: 'tools-expressions.pug', + selector: 'tools-expressions', + templateUrl: 'tools-expressions.pug', }) export class ToolsExpressions implements OnInit { - readonly homeIcon = faHome; - scale = 2; - columns = 12; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - constructor() { - } - ngOnInit() { - loadAndInitSpriteSheets() - .then(() => this.redraw()); - } - redraw() { - this.draw(); - } - png() { - this.draw(); - saveCanvas(this.canvas.nativeElement, 'expressions.png'); - } - private draw() { - drawSheet(this.canvas.nativeElement, this.scale, this.columns); - } + readonly homeIcon = faHome; + scale = 2; + columns = 12; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + constructor() { + } + ngOnInit() { + loadAndInitSpriteSheets() + .then(() => this.redraw()); + } + redraw() { + this.draw(); + } + png() { + this.draw(); + saveCanvas(this.canvas.nativeElement, 'expressions.png'); + } + private draw() { + drawSheet(this.canvas.nativeElement, this.scale, this.columns); + } } function drawSheet(canvas: HTMLCanvasElement, scale: number, columns: number, bg = 'lightgreen'): HTMLCanvasElement { - const frameWidth = 55; - const frameOffset = 50; - const frameHeight = 30; - const buffer = createCanvas(frameWidth, frameHeight); - const batch = new ContextSpriteBatch(buffer); - const pony = createPony(); - const state = createState(); - const info = toPalette(pony); - const filteredExpressions = expressions.filter(([, expr]) => !!expr).slice(2); - const rows = Math.ceil(filteredExpressions.length / columns); - const options = defaultDrawPonyOptions(); + const frameWidth = 55; + const frameOffset = 50; + const frameHeight = 30; + const buffer = createCanvas(frameWidth, frameHeight); + const batch = new ContextSpriteBatch(buffer); + const pony = createPony(); + const state = createState(); + const info = toPalette(pony); + const filteredExpressions = expressions.filter(([, expr]) => !!expr).slice(2); + const rows = Math.ceil(filteredExpressions.length / columns); + const options = defaultDrawPonyOptions(); - canvas.width = ((frameOffset * (columns - 1)) + frameWidth) * scale; - canvas.height = (frameHeight * rows) * scale; + canvas.width = ((frameOffset * (columns - 1)) + frameWidth) * scale; + canvas.height = (frameHeight * rows) * scale; - const viewContext = canvas.getContext('2d')!; - viewContext.save(); - disableImageSmoothing(viewContext); - viewContext.scale(scale, scale); + const viewContext = canvas.getContext('2d')!; + viewContext.save(); + disableImageSmoothing(viewContext); + viewContext.scale(scale, scale); - if (bg) { - viewContext.fillStyle = bg; - viewContext.fillRect(0, 0, canvas.width, canvas.height); - } + if (bg) { + viewContext.fillStyle = bg; + viewContext.fillRect(0, 0, canvas.width, canvas.height); + } - viewContext.font = 'normal 6px monospace'; - viewContext.textAlign = 'right'; - viewContext.fillStyle = 'black'; + viewContext.font = 'normal 6px monospace'; + viewContext.textAlign = 'right'; + viewContext.fillStyle = 'black'; - filteredExpressions.forEach(([name, [right, left, muzzle, rightIris = 0, leftIris = 0, extra = 0]]: any, i) => { - state.expression = { right, left, muzzle, rightIris, leftIris, extra }; + filteredExpressions.forEach(([name, [right, left, muzzle, rightIris = 0, leftIris = 0, extra = 0]]: any, i) => { + state.expression = { right, left, muzzle, rightIris, leftIris, extra }; - batch.start(paletteSpriteSheet, 0); - drawPony(batch, info, state, 35, 50, options); - batch.end(); + batch.start(paletteSpriteSheet, 0); + drawPony(batch, info, state, 35, 50, options); + batch.end(); - const x = (i % columns) * frameOffset; - const y = Math.floor(i / columns) * frameHeight; + const x = (i % columns) * frameOffset; + const y = Math.floor(i / columns) * frameHeight; - viewContext.drawImage(buffer, x, y); - viewContext.fillText(name, x + 18, y + 20); - }); + viewContext.drawImage(buffer, x, y); + viewContext.fillText(name, x + 18, y + 20); + }); - viewContext.restore(); - return canvas; + viewContext.restore(); + return canvas; } function createState(): PonyState { - const state = defaultPonyState(); - state.blushColor = RED; - state.animation = createBodyAnimation('', 24, false, [[0, 1]]); - return state; + const state = defaultPonyState(); + state.blushColor = RED; + state.animation = createBodyAnimation('', 24, false, [[0, 1]]); + return state; } function createPony(): PonyInfo { - const pony = createDefaultPony(); - pony.mane!.type = 0; - pony.backMane!.type = 0; - pony.tail!.type = 0; - pony.coatFill = 'dec078'; - pony.lockCoatOutline = true; - pony.lockBackLegAccessory = false; - pony.eyeColorRight = 'cornflowerblue'; - return syncLockedPonyInfo(pony); + const pony = createDefaultPony(); + pony.mane!.type = 0; + pony.backMane!.type = 0; + pony.tail!.type = 0; + pony.coatFill = 'dec078'; + pony.lockCoatOutline = true; + pony.lockBackLegAccessory = false; + pony.eyeColorRight = 'cornflowerblue'; + return syncLockedPonyInfo(pony); } diff --git a/src/ts/components/tools/tools-index/tools-index.ts b/src/ts/components/tools/tools-index/tools-index.ts index 6a80bd7..f28a33a 100644 --- a/src/ts/components/tools/tools-index/tools-index.ts +++ b/src/ts/components/tools/tools-index/tools-index.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core'; @Component({ - selector: 'tools-index', - templateUrl: 'tools-index.pug', + selector: 'tools-index', + templateUrl: 'tools-index.pug', }) export class ToolsIndex { } diff --git a/src/ts/components/tools/tools-map/tools-map.ts b/src/ts/components/tools/tools-map/tools-map.ts index 75dda51..5d60846 100644 --- a/src/ts/components/tools/tools-map/tools-map.ts +++ b/src/ts/components/tools/tools-map/tools-map.ts @@ -6,7 +6,7 @@ import { tileHeight, tileWidth, REGION_SIZE } from '../../../common/constants'; import { faHome } from '../../../client/icons'; import { updateMap, getTile, createWorldMap, setRegion, setTile } from '../../../common/worldMap'; import { - Season, DrawOptions, defaultDrawOptions, EntityFlags, Entity, WorldMap, defaultWorldState, MapType, MapFlags + Season, DrawOptions, defaultDrawOptions, EntityFlags, Entity, WorldMap, defaultWorldState, MapType, MapFlags } from '../../../common/interfaces'; import { drawCanvas } from '../../../graphics/contextSpriteBatch'; import { paletteSpriteSheet } from '../../../generated/sprites'; @@ -17,7 +17,7 @@ import { createCamera } from '../../../common/camera'; import { mockPaletteManager } from '../../../common/ponyInfo'; import { isCritter } from '../../../common/entityUtils'; import { - createAnEntity, cloud, pony, apple, apple2, orange, orange2, candy, gift1, gift2, appleGreen, appleGreen2 + createAnEntity, cloud, pony, apple, apple2, orange, orange2, candy, gift1, gift2, appleGreen, appleGreen2 } from '../../../common/entities'; import { drawMap } from '../../../client/draw'; import { includes, observableToPromise, hasFlag } from '../../../common/utils'; @@ -27,184 +27,184 @@ import { getTileColor } from '../../../common/colors'; import { colorToCSS } from '../../../common/color'; export interface ToolsMapOtherInfo { - season: Season; - entities: { type: number; x: number; y: number; order: number; id: number; }[]; + season: Season; + entities: { type: number; x: number; y: number; order: number; id: number; }[]; } export interface ToolsMapInfo { - width: number; - height: number; - defaultTile: number; - tiles?: string; - type: MapType; - info: ToolsMapOtherInfo; + width: number; + height: number; + defaultTile: number; + tiles?: string; + type: MapType; + info: ToolsMapOtherInfo; } @Component({ - selector: 'tools-map', - templateUrl: 'tools-map.pug', + selector: 'tools-map', + templateUrl: 'tools-map.pug', }) export class ToolsMap implements OnInit { - readonly homeIcon = faHome; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - maps: string[] = []; - selectedMap = ''; - grid = false; - private map?: WorldMap; - private info?: ToolsMapOtherInfo; - constructor(private http: HttpClient, private storage: StorageService) { - } - get scale() { - return this.storage.getInt('tools-map-scale') || 1; - } - set scale(value) { - this.storage.setInt('tools-map-scale', value); - } - get type() { - return this.storage.getItem('tools-map-type') || 'regular'; - } - set type(value) { - this.storage.setItem('tools-map-type', value); - } - async ngOnInit() { - await loadAndInitSpriteSheets(); - await this.fetchList(); - await this.fetch(); - } - setType(type: string) { - this.type = type; - this.redraw(); - } - async fetchList() { - this.maps = await observableToPromise(this.http.get('/api-tools/maps')); - } - fetch() { - this.http.get('/api-tools/map', { params: { map: this.selectedMap } }).subscribe(map => { - this.info = map.info; + readonly homeIcon = faHome; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + maps: string[] = []; + selectedMap = ''; + grid = false; + private map?: WorldMap; + private info?: ToolsMapOtherInfo; + constructor(private http: HttpClient, private storage: StorageService) { + } + get scale() { + return this.storage.getInt('tools-map-scale') || 1; + } + set scale(value) { + this.storage.setInt('tools-map-scale', value); + } + get type() { + return this.storage.getItem('tools-map-type') || 'regular'; + } + set type(value) { + this.storage.setItem('tools-map-type', value); + } + async ngOnInit() { + await loadAndInitSpriteSheets(); + await this.fetchList(); + await this.fetch(); + } + setType(type: string) { + this.type = type; + this.redraw(); + } + async fetchList() { + this.maps = await observableToPromise(this.http.get('/api-tools/maps')); + } + fetch() { + this.http.get('/api-tools/map', { params: { map: this.selectedMap } }).subscribe(map => { + this.info = map.info; - const regionsX = map.width / REGION_SIZE; - const regionsY = map.height / REGION_SIZE; - const { type, defaultTile } = map; + const regionsX = map.width / REGION_SIZE; + const regionsY = map.height / REGION_SIZE; + const { type, defaultTile } = map; - this.map = createWorldMap({ type, flags: MapFlags.None, defaultTile, regionsX, regionsY }); - const tiles = deserializeTiles(map.tiles!); + this.map = createWorldMap({ type, flags: MapFlags.None, defaultTile, regionsX, regionsY }); + const tiles = deserializeTiles(map.tiles!); - for (let y = 0, i = 0; y < regionsX; y++) { - for (let x = 0; x < regionsY; x++ , i++) { - setRegion(this.map, x, y, createRegion(x, y)); - } - } + for (let y = 0, i = 0; y < regionsX; y++) { + for (let x = 0; x < regionsY; x++ , i++) { + setRegion(this.map, x, y, createRegion(x, y)); + } + } - for (let y = 0, i = 0; y < map.height; y++) { - for (let x = 0; x < map.width; x++ , i++) { - setTile(this.map, x, y, tiles[i]); - } - } + for (let y = 0, i = 0; y < map.height; y++) { + for (let x = 0; x < map.width; x++ , i++) { + setTile(this.map, x, y, tiles[i]); + } + } - this.redraw(); - }); - } - selectMap(map: string) { - this.selectedMap = map; - this.fetch(); - } - redraw() { - this.draw(); - } - png() { - saveCanvas(this.canvas.nativeElement, 'map.png'); - } - private draw() { - if (this.map && this.info) { - if (this.type === 'regular') { - drawTheMap(this.canvas.nativeElement, this.map, this.info, this.scale, this.grid); - } else if (this.type === 'minimap') { - drawMinimap(this.canvas.nativeElement, this.map, this.info, this.scale); - } - } - } + this.redraw(); + }); + } + selectMap(map: string) { + this.selectedMap = map; + this.fetch(); + } + redraw() { + this.draw(); + } + png() { + saveCanvas(this.canvas.nativeElement, 'map.png'); + } + private draw() { + if (this.map && this.info) { + if (this.type === 'regular') { + drawTheMap(this.canvas.nativeElement, this.map, this.info, this.scale, this.grid); + } else if (this.type === 'minimap') { + drawMinimap(this.canvas.nativeElement, this.map, this.info, this.scale); + } + } + } } function drawTheMap(canvas: HTMLCanvasElement, map: WorldMap, info: ToolsMapOtherInfo, scale: number, grid: boolean) { - const mapCanvas = drawCanvas(map.width * tileWidth, map.height * tileHeight, paletteSpriteSheet, 0x222222ff, batch => { - const camera = createCamera(); - camera.w = map.width * tileWidth; - camera.h = map.height * tileHeight; + const mapCanvas = drawCanvas(map.width * tileWidth, map.height * tileHeight, paletteSpriteSheet, 0x222222ff, batch => { + const camera = createCamera(); + camera.w = map.width * tileWidth; + camera.h = map.height * tileHeight; - const tileSets = createTileSets(mockPaletteManager, info.season, map.type); - const lightData = createLightData(info.season); + const tileSets = createTileSets(mockPaletteManager, info.season, map.type); + const lightData = createLightData(info.season); - const drawOptions: DrawOptions = { - ...defaultDrawOptions, - tileGrid: grid, - shadowColor: getShadowColor(lightData, HOUR_LENGTH * 12), - }; + const drawOptions: DrawOptions = { + ...defaultDrawOptions, + tileGrid: grid, + shadowColor: getShadowColor(lightData, HOUR_LENGTH * 12), + }; - const ignoreTypes = [ - cloud, pony, apple, apple2, appleGreen, appleGreen2, orange, orange2, candy, gift1, gift2 - ].map(e => e.type); + const ignoreTypes = [ + cloud, pony, apple, apple2, appleGreen, appleGreen2, orange, orange2, candy, gift1, gift2 + ].map(e => e.type); - const shouldDraw = (e: Entity) => { - return !hasFlag(e.flags, EntityFlags.Debug) && !isCritter(e) && !includes(ignoreTypes, e.type); - }; + const shouldDraw = (e: Entity) => { + return !hasFlag(e.flags, EntityFlags.Debug) && !isCritter(e) && !includes(ignoreTypes, e.type); + }; - map.entitiesDrawable = info.entities - .map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager, defaultWorldState)) - .filter(shouldDraw); + map.entitiesDrawable = info.entities + .map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager, defaultWorldState)) + .filter(shouldDraw); - updateMap(map, 0); - drawMap(batch, map, camera, {} as any, drawOptions, tileSets, []); - }); + updateMap(map, 0); + drawMap(batch, map, camera, {} as any, drawOptions, tileSets, []); + }); - canvas.width = Math.floor(mapCanvas.width / scale); - canvas.height = Math.floor(mapCanvas.height / scale); - const context = canvas.getContext('2d')!; - // disableImageSmoothing(context); - context.scale(1 / scale, 1 / scale); - context.drawImage(mapCanvas, 0, 0); + canvas.width = Math.floor(mapCanvas.width / scale); + canvas.height = Math.floor(mapCanvas.height / scale); + const context = canvas.getContext('2d')!; + // disableImageSmoothing(context); + context.scale(1 / scale, 1 / scale); + context.drawImage(mapCanvas, 0, 0); } function drawMinimap(canvas: HTMLCanvasElement, map: WorldMap, info: ToolsMapOtherInfo, scale: number) { - const tileWidth = 1; - const tileHeight = 1; + const tileWidth = 1; + const tileHeight = 1; - const mapCanvas = createCanvas(map.width * tileWidth, map.height * tileHeight); - const mapContext = mapCanvas.getContext('2d')!; + const mapCanvas = createCanvas(map.width * tileWidth, map.height * tileHeight); + const mapContext = mapCanvas.getContext('2d')!; - updateMap(map, 0); + updateMap(map, 0); - for (let x = 0; x < map.width; x++) { - for (let y = 0; y < map.height; y++) { - const tile = getTile(map, x, y); - const color = getTileColor(tile, info.season); - mapContext.fillStyle = colorToCSS(color); - mapContext.fillRect(x, y, 1, 1); - } - } + for (let x = 0; x < map.width; x++) { + for (let y = 0; y < map.height; y++) { + const tile = getTile(map, x, y); + const color = getTileColor(tile, info.season); + mapContext.fillStyle = colorToCSS(color); + mapContext.fillRect(x, y, 1, 1); + } + } - map.entities = info.entities - .map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager, defaultWorldState)); + map.entities = info.entities + .map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager, defaultWorldState)); - for (let i = 1; i <= 2; i++) { - for (const e of map.entities) { - if (e.minimap && e.minimap.order === i) { - const { color, rect } = e.minimap; - mapContext.fillStyle = colorToCSS(color); - mapContext.fillRect(Math.round(e.x + rect.x), Math.round(e.y + rect.y), rect.w, rect.h); - } - } - } + for (let i = 1; i <= 2; i++) { + for (const e of map.entities) { + if (e.minimap && e.minimap.order === i) { + const { color, rect } = e.minimap; + mapContext.fillStyle = colorToCSS(color); + mapContext.fillRect(Math.round(e.x + rect.x), Math.round(e.y + rect.y), rect.w, rect.h); + } + } + } - canvas.width = mapCanvas.width * scale; - canvas.height = mapCanvas.height * scale; - const context = canvas.getContext('2d')!; - context.save(); + canvas.width = mapCanvas.width * scale; + canvas.height = mapCanvas.height * scale; + const context = canvas.getContext('2d')!; + context.save(); - if (scale >= 1) { - disableImageSmoothing(context); - } + if (scale >= 1) { + disableImageSmoothing(context); + } - context.scale(scale, scale); - context.drawImage(mapCanvas, 0, 0); - context.restore(); + context.scale(scale, scale); + context.drawImage(mapCanvas, 0, 0); + context.restore(); } diff --git a/src/ts/components/tools/tools-palette/tools-palette.ts b/src/ts/components/tools/tools-palette/tools-palette.ts index 26ea2c7..1c8f893 100644 --- a/src/ts/components/tools/tools-palette/tools-palette.ts +++ b/src/ts/components/tools/tools-palette/tools-palette.ts @@ -16,63 +16,63 @@ const paletteManager = new PaletteManager(); const defaultPalette = paletteManager.add(DEFAULT_PALETTE); @Component({ - selector: 'tools-palette', - templateUrl: 'tools-palette.pug', + selector: 'tools-palette', + templateUrl: 'tools-palette.pug', }) export class ToolsPalette implements OnInit { - readonly homeIcon = faHome; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - scale = 3; - sprites = Object.keys(sprites).filter(key => { - const s = (sprites as any)[key] as any; - return !!(s && s.color); - }); - spriteName = ''; - palette = ['red', 'blue', 'orange', 'violet'].map(x => ({ original: x, current: x })); - ngOnInit() { - setPaletteManager(paletteManager); - loadAndInitSpriteSheets().then(() => this.redraw()); - } - spriteChanged() { - this.redraw(); - } - loadPalette() { - const sprite = (sprites as any)[this.spriteName] as PaletteRenderable; + readonly homeIcon = faHome; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + scale = 3; + sprites = Object.keys(sprites).filter(key => { + const s = (sprites as any)[key] as any; + return !!(s && s.color); + }); + spriteName = ''; + palette = ['red', 'blue', 'orange', 'violet'].map(x => ({ original: x, current: x })); + ngOnInit() { + setPaletteManager(paletteManager); + loadAndInitSpriteSheets().then(() => this.redraw()); + } + spriteChanged() { + this.redraw(); + } + loadPalette() { + const sprite = (sprites as any)[this.spriteName] as PaletteRenderable; - if (sprite) { - this.palette = Array.from(sprite.palettes![0]).map(colorToCSS).map(c => ({ original: c, current: c })); - } + if (sprite) { + this.palette = Array.from(sprite.palettes![0]).map(colorToCSS).map(c => ({ original: c, current: c })); + } - this.redraw(); - } - redraw() { - const canvas = this.canvas.nativeElement as HTMLCanvasElement; - const width = Math.ceil(canvas.width / this.scale); - const height = Math.ceil(canvas.height / this.scale); + this.redraw(); + } + redraw() { + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + const width = Math.ceil(canvas.width / this.scale); + const height = Math.ceil(canvas.height / this.scale); - const buffer = drawCanvas(width, height, sprites.paletteSpriteSheet, BG, batch => { - const sprite = (sprites as any)[this.spriteName] as PaletteRenderable; + const buffer = drawCanvas(width, height, sprites.paletteSpriteSheet, BG, batch => { + const sprite = (sprites as any)[this.spriteName] as PaletteRenderable; - if (sprite) { - const palette = paletteManager.add(this.palette.map(x => parseColor(x.current))); + if (sprite) { + const palette = paletteManager.add(this.palette.map(x => parseColor(x.current))); - const x = (width - (sprite.color!.w + sprite.color!.ox)) / 2; - const y = (height - (sprite.color!.h + sprite.color!.oy)) / 2; + const x = (width - (sprite.color!.w + sprite.color!.ox)) / 2; + const y = (height - (sprite.color!.h + sprite.color!.oy)) / 2; - console.log(x, y, width, sprite.color!.w, sprite.color!.ox); + console.log(x, y, width, sprite.color!.w, sprite.color!.ox); - batch.drawSprite(sprite.shadow, SHADOW_COLOR, defaultPalette, x, y); - batch.drawSprite(sprite.color, WHITE, palette, x, y); + batch.drawSprite(sprite.shadow, SHADOW_COLOR, defaultPalette, x, y); + batch.drawSprite(sprite.color, WHITE, palette, x, y); - releasePalette(palette); - } - }); + releasePalette(palette); + } + }); - const viewContext = canvas.getContext('2d')!; - viewContext.save(); - disableImageSmoothing(viewContext); - viewContext.scale(this.scale, this.scale); - viewContext.drawImage(buffer, 0, 0); - viewContext.restore(); - } + const viewContext = canvas.getContext('2d')!; + viewContext.save(); + disableImageSmoothing(viewContext); + viewContext.scale(this.scale, this.scale); + viewContext.drawImage(buffer, 0, 0); + viewContext.restore(); + } } diff --git a/src/ts/components/tools/tools-perf/methods.ts b/src/ts/components/tools/tools-perf/methods.ts index cff8896..0d8cac0 100644 --- a/src/ts/components/tools/tools-perf/methods.ts +++ b/src/ts/components/tools/tools-perf/methods.ts @@ -1,156 +1,156 @@ // import { TextEncoder } from 'util'; function forEachCharacter(value: string, callback: (code: number) => void) { - for (let i = 0; i < value.length; i++) { - const code = value.charCodeAt(i); + for (let i = 0; i < value.length; i++) { + const code = value.charCodeAt(i); - // high surrogate - if (code >= 0xd800 && code <= 0xdbff) { - if ((i + 1) < value.length) { - const extra = value.charCodeAt(i + 1); + // high surrogate + if (code >= 0xd800 && code <= 0xdbff) { + if ((i + 1) < value.length) { + const extra = value.charCodeAt(i + 1); - // low surrogate - if ((extra & 0xfc00) === 0xdc00) { - i++; - callback(((code & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000); - } - } - } else { - callback(code); - } - } + // low surrogate + if ((extra & 0xfc00) === 0xdc00) { + i++; + callback(((code & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000); + } + } + } else { + callback(code); + } + } } function charLengthInBytes(code: number): number { - if ((code & 0xffffff80) === 0) { - return 1; - } else if ((code & 0xfffff800) === 0) { - return 2; - } else if ((code & 0xffff0000) === 0) { - return 3; - } else { - return 4; - } + if ((code & 0xffffff80) === 0) { + return 1; + } else if ((code & 0xfffff800) === 0) { + return 2; + } else if ((code & 0xffff0000) === 0) { + return 3; + } else { + return 4; + } } function stringLengthInBytes(value: string): number { - let result = 0; - forEachCharacter(value, code => result += charLengthInBytes(code)); - return result; + let result = 0; + forEachCharacter(value, code => result += charLengthInBytes(code)); + return result; } function encodeStringTo(buffer: Uint8Array | Buffer, offset: number, value: string): number { - forEachCharacter(value, code => { - const length = charLengthInBytes(code); + forEachCharacter(value, code => { + const length = charLengthInBytes(code); - if (length === 1) { - buffer[offset++] = code; - } else { - if (length === 2) { - buffer[offset++] = ((code >> 6) & 0x1f) | 0xc0; - } else if (length === 3) { - buffer[offset++] = ((code >> 12) & 0x0f) | 0xe0; - buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; - } else { - buffer[offset++] = ((code >> 18) & 0x07) | 0xf0; - buffer[offset++] = ((code >> 12) & 0x3f) | 0x80; - buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; - } + if (length === 1) { + buffer[offset++] = code; + } else { + if (length === 2) { + buffer[offset++] = ((code >> 6) & 0x1f) | 0xc0; + } else if (length === 3) { + buffer[offset++] = ((code >> 12) & 0x0f) | 0xe0; + buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; + } else { + buffer[offset++] = ((code >> 18) & 0x07) | 0xf0; + buffer[offset++] = ((code >> 12) & 0x3f) | 0x80; + buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; + } - buffer[offset++] = (code & 0x3f) | 0x80; - } - }); + buffer[offset++] = (code & 0x3f) | 0x80; + } + }); - return offset; + return offset; } export function encodeString(value: string | null): Uint8Array | null { - if (value == null) - return null; + if (value == null) + return null; - const buffer = new Uint8Array(stringLengthInBytes(value)); - encodeStringTo(buffer, 0, value); - return buffer; + const buffer = new Uint8Array(stringLengthInBytes(value)); + encodeStringTo(buffer, 0, value); + return buffer; } export function encodeStringNew(value: string | null): Uint8Array | null { - if (value == null) - return null; + if (value == null) + return null; - const buffer = new Uint8Array(stringLengthInBytes2(value)); - encodeStringTo2(buffer, 0, value); - return buffer; + const buffer = new Uint8Array(stringLengthInBytes2(value)); + encodeStringTo2(buffer, 0, value); + return buffer; } // new methods function charLengthInBytes2(code: number): number { - if ((code & 0xffffff80) === 0) { - return 1; - } else if ((code & 0xfffff800) === 0) { - return 2; - } else if ((code & 0xffff0000) === 0) { - return 3; - } else { - return 4; - } + if ((code & 0xffffff80) === 0) { + return 1; + } else if ((code & 0xfffff800) === 0) { + return 2; + } else if ((code & 0xffff0000) === 0) { + return 3; + } else { + return 4; + } } export function stringLengthInBytes2(value: string): number { - let result = 0; - forEachCharacter2(value, code => result = (result + charLengthInBytes2(code)) | 0); - return result; + let result = 0; + forEachCharacter2(value, code => result = (result + charLengthInBytes2(code)) | 0); + return result; } export function encodeStringTo2(buffer: Uint8Array, offset: number, value: string): number { - forEachCharacter2(value, code => { - const length = charLengthInBytes2(code) | 0; + forEachCharacter2(value, code => { + const length = charLengthInBytes2(code) | 0; - if (length === 1) { - buffer[offset++] = code; - } else { - if (length === 2) { - buffer[offset++] = ((code >> 6) & 0x1f) | 0xc0; - } else if (length === 3) { - buffer[offset++] = ((code >> 12) & 0x0f) | 0xe0; - buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; - } else { - buffer[offset++] = ((code >> 18) & 0x07) | 0xf0; - buffer[offset++] = ((code >> 12) & 0x3f) | 0x80; - buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; - } + if (length === 1) { + buffer[offset++] = code; + } else { + if (length === 2) { + buffer[offset++] = ((code >> 6) & 0x1f) | 0xc0; + } else if (length === 3) { + buffer[offset++] = ((code >> 12) & 0x0f) | 0xe0; + buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; + } else { + buffer[offset++] = ((code >> 18) & 0x07) | 0xf0; + buffer[offset++] = ((code >> 12) & 0x3f) | 0x80; + buffer[offset++] = ((code >> 6) & 0x3f) | 0x80; + } - buffer[offset++] = (code & 0x3f) | 0x80; - } - }); + buffer[offset++] = (code & 0x3f) | 0x80; + } + }); - return offset; + return offset; } function forEachCharacter2(value: string, callback: (code: number) => void) { - const length = value.length | 0; - const lengthMinusOne = Math.max(0, length - 1) | 0; + const length = value.length | 0; + const lengthMinusOne = Math.max(0, length - 1) | 0; - for (let i = 0; i < length; i = (i + 1) | 0) { - let code = value.charCodeAt(i) | 0; + for (let i = 0; i < length; i = (i + 1) | 0) { + let code = value.charCodeAt(i) | 0; - // high surrogate - if (code >= 0xd800 && code <= 0xdbff) { - if (i < lengthMinusOne) { - const extra = value.charCodeAt(i + 1) | 0; + // high surrogate + if (code >= 0xd800 && code <= 0xdbff) { + if (i < lengthMinusOne) { + const extra = value.charCodeAt(i + 1) | 0; - // low surrogate - if ((extra & 0xfc00) === 0xdc00) { - i = (i + 1) | 0; - code = (((((code & 0x3ff) << 10) + (extra & 0x3ff)) | 0) + 0x10000) | 0; - } else { - continue; - } - } else { - continue; - } - } + // low surrogate + if ((extra & 0xfc00) === 0xdc00) { + i = (i + 1) | 0; + code = (((((code & 0x3ff) << 10) + (extra & 0x3ff)) | 0) + 0x10000) | 0; + } else { + continue; + } + } else { + continue; + } + } - callback(code); - } + callback(code); + } } diff --git a/src/ts/components/tools/tools-perf/tools-perf.ts b/src/ts/components/tools/tools-perf/tools-perf.ts index 50e3f01..e198198 100644 --- a/src/ts/components/tools/tools-perf/tools-perf.ts +++ b/src/ts/components/tools/tools-perf/tools-perf.ts @@ -13,392 +13,392 @@ import { includes as utilsIncludes } from '../../../common/utils'; import { encodeString, encodeStringNew } from './methods'; @Component({ - selector: 'tools-perf', - templateUrl: 'tools-perf.pug', + selector: 'tools-perf', + templateUrl: 'tools-perf.pug', }) export class ToolsPerf { - readonly homeIcon = faHome; - output = ''; - ponies: string[] = []; - messages: string[] = []; - tests: { name: string; func: () => void; }[] = []; - constructor(http: HttpClient, private zone: NgZone) { - http.get('/tests/ponies.json').subscribe(data => this.ponies = data); - http.get('/tests/messages.json').subscribe(data => this.messages = data); - this.output = createPostDecompressPony().toString(); - this.tests.push({ name: 'arrays', func: () => this.runTest(arrayTest) }); - this.tests.push({ name: 'compress colors', func: () => this.runTest(compressColorsTest) }); - this.tests.push({ name: 'parse color', func: () => this.runTest(parseColorTest) }); - this.tests.push({ name: 'compare arrays', func: () => this.runTest(compareArrays) }); - this.tests.push({ name: 'utf', func: () => this.runTest(() => utfTest(this.messages)) }); - this.tests.push({ name: 'includes', func: () => this.runTest(includeTest) }); - } - run() { - this.runTest(() => utfTest(this.messages)); - } - stats() { - swearEntryTest(this.messages, x => this.output = x); - } - private runTest(test: () => void) { - this.zone.runOutsideAngular(() => setTimeout(test, 20)); - } + readonly homeIcon = faHome; + output = ''; + ponies: string[] = []; + messages: string[] = []; + tests: { name: string; func: () => void; }[] = []; + constructor(http: HttpClient, private zone: NgZone) { + http.get('/tests/ponies.json').subscribe(data => this.ponies = data); + http.get('/tests/messages.json').subscribe(data => this.messages = data); + this.output = createPostDecompressPony().toString(); + this.tests.push({ name: 'arrays', func: () => this.runTest(arrayTest) }); + this.tests.push({ name: 'compress colors', func: () => this.runTest(compressColorsTest) }); + this.tests.push({ name: 'parse color', func: () => this.runTest(parseColorTest) }); + this.tests.push({ name: 'compare arrays', func: () => this.runTest(compareArrays) }); + this.tests.push({ name: 'utf', func: () => this.runTest(() => utfTest(this.messages)) }); + this.tests.push({ name: 'includes', func: () => this.runTest(includeTest) }); + } + run() { + this.runTest(() => utfTest(this.messages)); + } + stats() { + swearEntryTest(this.messages, x => this.output = x); + } + private runTest(test: () => void) { + this.zone.runOutsideAngular(() => setTimeout(test, 20)); + } } function measure(name: string, iterations: number, func: (i: number) => void) { - if (!iterations) - return; + if (!iterations) + return; - const start = performance.now(); - let v: any; + const start = performance.now(); + let v: any; - for (let i = 0; i < iterations; i++) { - v = func(i); - } + for (let i = 0; i < iterations; i++) { + v = func(i); + } - const end = performance.now(); - const diff = end - start; + const end = performance.now(); + const diff = end - start; - console.log(`${name}: ${diff.toFixed(0)}ms, ${(diff / iterations).toFixed(3)}ms per iteration // ${!!v}`); + console.log(`${name}: ${diff.toFixed(0)}ms, ${(diff / iterations).toFixed(3)}ms per iteration // ${!!v}`); } export function compressColorsTest() { - const colors = [ - 3553475327, 2592097791, 3662487807, 545184511, 2744818431, 1658462463, 16764927, 7864319, - 4278253055, 2492366335, 10033407, 11763711, 6730751, 3003165951, 2997456127, 1447512063, - 1936281087, 874586623, 3713423103, 2099652351, 4293220607, 512819199, 852308735, 3664828159, - 3692313855, 2147472639, 2861699071, 5944319, 42992383, 2570622463, 2583699455, 1051954175, - 3013286911, 2762969343, 1400052223, 1991223551, 1049483775, 611346431, 272724223, 1392443647, - 1589395455, 3201321215, 2679322623, 3233857791, 1280068863, 2560137471, 437918463, 3908210943, - 3602601215, 4001558271, 2806294527, 2508550143, 1732985855, 1330597887, 1381126911, 1750746879, - 1211049983, 926365695, 960051711, 2659530751, 3597364223, 777334527, 2201321727, 2120247039, - 2441106175, 2354205439, 1844342271, 3613774847, 1967148031, 4289003775, 3600494335, 3164100095, - 2845275903, 3098517247, 1463486719, 1699355647, 1834628607, 1936084991, 2307095039, 857933311, - 740297471, 807801599, 740629247, 656811007, 3587560959, 2998055679, 2425393407, 1869574143, - 4294967295, 4294238719, 4293575679, 4292051711, 4291190527 - ].map(x => x >>> 0); + const colors = [ + 3553475327, 2592097791, 3662487807, 545184511, 2744818431, 1658462463, 16764927, 7864319, + 4278253055, 2492366335, 10033407, 11763711, 6730751, 3003165951, 2997456127, 1447512063, + 1936281087, 874586623, 3713423103, 2099652351, 4293220607, 512819199, 852308735, 3664828159, + 3692313855, 2147472639, 2861699071, 5944319, 42992383, 2570622463, 2583699455, 1051954175, + 3013286911, 2762969343, 1400052223, 1991223551, 1049483775, 611346431, 272724223, 1392443647, + 1589395455, 3201321215, 2679322623, 3233857791, 1280068863, 2560137471, 437918463, 3908210943, + 3602601215, 4001558271, 2806294527, 2508550143, 1732985855, 1330597887, 1381126911, 1750746879, + 1211049983, 926365695, 960051711, 2659530751, 3597364223, 777334527, 2201321727, 2120247039, + 2441106175, 2354205439, 1844342271, 3613774847, 1967148031, 4289003775, 3600494335, 3164100095, + 2845275903, 3098517247, 1463486719, 1699355647, 1834628607, 1936084991, 2307095039, 857933311, + 740297471, 807801599, 740629247, 656811007, 3587560959, 2998055679, 2425393407, 1869574143, + 4294967295, 4294238719, 4293575679, 4292051711, 4291190527 + ].map(x => x >>> 0); - const oldMethod = bitWriter(write => colors.forEach(x => write(x >> 8, 24))); + const oldMethod = bitWriter(write => colors.forEach(x => write(x >> 8, 24))); - const truncated = colors.map(c => (c >>> 8) & 0xffffff); - truncated.sort((a, b) => a > b ? 1 : (a < b ? -1 : 0)); + const truncated = colors.map(c => (c >>> 8) & 0xffffff); + truncated.sort((a, b) => a > b ? 1 : (a < b ? -1 : 0)); - console.log(truncated); - console.log(truncated.slice(1).map((c, i) => (c - truncated[i]).toString(16))); + console.log(truncated); + console.log(truncated.slice(1).map((c, i) => (c - truncated[i]).toString(16))); - const newMethod = bitWriter(write => truncated.forEach(x => write(x, 24))); + const newMethod = bitWriter(write => truncated.forEach(x => write(x, 24))); - console.log('old', oldMethod.byteLength); - console.log('new', newMethod.byteLength); + console.log('old', oldMethod.byteLength); + console.log('new', newMethod.byteLength); } export const results: any[] = []; export function parseColorTest() { - const iterations = 100000; + const iterations = 100000; - function parseColorExperimental(value: string) { - return (parseInt(value, 16) << 8) | 0xff; - } + function parseColorExperimental(value: string) { + return (parseInt(value, 16) << 8) | 0xff; + } - measure('COLOR parseColorWithAlpha', iterations, () => { - return parseColorWithAlpha('ff4354', 1); - }); + measure('COLOR parseColorWithAlpha', iterations, () => { + return parseColorWithAlpha('ff4354', 1); + }); - measure('COLOR parseColorFast', iterations, () => { - return parseColorFast('ff4354'); - }); + measure('COLOR parseColorFast', iterations, () => { + return parseColorFast('ff4354'); + }); - measure('COLOR parseColorExperimental', iterations, () => { - return parseColorExperimental('ff4354'); - }); + measure('COLOR parseColorExperimental', iterations, () => { + return parseColorExperimental('ff4354'); + }); } export function compareArrays() { - const iterations = 100000; - const a = [2423, 534534, 546124, 23412, 54364, 67756, 234234]; - const b = [564, 867867, 65645, 567567, 34534, 32453, 867867]; - const c = [2423, 534534, 546124, 23412, 54364, 67756, 234234]; - const d = [2423, 534534, 546124, 23412, 54364, 67756]; - let t: any; + const iterations = 100000; + const a = [2423, 534534, 546124, 23412, 54364, 67756, 234234]; + const b = [564, 867867, 65645, 567567, 34534, 32453, 867867]; + const c = [2423, 534534, 546124, 23412, 54364, 67756, 234234]; + const d = [2423, 534534, 546124, 23412, 54364, 67756]; + let t: any; - function compareArrays(a: number[], b: number[]) { - if (a.length !== b.length) - return false; + function compareArrays(a: number[], b: number[]) { + if (a.length !== b.length) + return false; - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) - return false; - } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) + return false; + } - return true; - } + return true; + } - measure('ARRAY _.isEqual', iterations, () => { - t = isEqual(a, b); - t = isEqual(a, c) || t; - t = isEqual(a, d) || t; - results.push(t); - }); + measure('ARRAY _.isEqual', iterations, () => { + t = isEqual(a, b); + t = isEqual(a, c) || t; + t = isEqual(a, d) || t; + results.push(t); + }); - measure('ARRAY compareArrays', iterations, () => { - t = compareArrays(a, b); - t = compareArrays(a, c) || t; - t = compareArrays(a, d) || t; - results.push(t); - }); + measure('ARRAY compareArrays', iterations, () => { + t = compareArrays(a, b); + t = compareArrays(a, c) || t; + t = compareArrays(a, d) || t; + results.push(t); + }); } export function decode(data: string[]) { - let manager = new PaletteManager(); - // manager = ({ addArray: (x: any) => x } as any); + let manager = new PaletteManager(); + // manager = ({ addArray: (x: any) => x } as any); - measure('DECODE 1', 10000, i => { - results.push(decodePonyInfo(data[i % data.length], manager)); - }); + measure('DECODE 1', 10000, i => { + results.push(decodePonyInfo(data[i % data.length], manager)); + }); - // console.log(((manager as any).palettes as any[]).map(x => x.length).join(', ')); + // console.log(((manager as any).palettes as any[]).map(x => x.length).join(', ')); } export function utfTest(messages: string[]) { - const iterations = 300000; + const iterations = 300000; - measure('old', iterations, i => { - results.push(encodeString(messages[i % messages.length])); - }); + measure('old', iterations, i => { + results.push(encodeString(messages[i % messages.length])); + }); - measure('new', iterations, i => { - results.push(encodeStringNew(messages[i % messages.length])); - }); + measure('new', iterations, i => { + results.push(encodeStringNew(messages[i % messages.length])); + }); - const encoder = new (window as any).TextEncoder('utf8'); + const encoder = new (window as any).TextEncoder('utf8'); - measure('native', iterations, i => { - results.push(encoder.encode(messages[i % messages.length])); - }); + measure('native', iterations, i => { + results.push(encoder.encode(messages[i % messages.length])); + }); } export function arrayTest() { - const iterations = 10000; - const length = 100; - const size = 24; - const typed = new Int32Array(length * size); - const typed2 = new Uint16Array(length * size); + const iterations = 10000; + const length = 100; + const size = 24; + const typed = new Int32Array(length * size); + const typed2 = new Uint16Array(length * size); - for (let i = 0; i < typed.length; i++) { - typed2[i] = typed[i] = Math.random() * 0xffff; - } + for (let i = 0; i < typed.length; i++) { + typed2[i] = typed[i] = Math.random() * 0xffff; + } - const objects = times(length, i => ({ - a: typed[i * size + 0], - b: typed[i * size + 1], - c: typed[i * size + 2], - d: typed[i * size + 3], - e: typed[i * size + 4], - f: typed[i * size + 5], - g: typed[i * size + 6], - h: typed[i * size + 7], - i: typed[i * size + 8], - j: typed[i * size + 9], - k: typed[i * size + 10], - l: typed[i * size + 11], - m: typed[i * size + 12], - n: typed[i * size + 13], - o: typed[i * size + 14], - p: typed[i * size + 15], - q: typed[i * size + 16], - r: typed[i * size + 17], - s: typed[i * size + 18], - t: typed[i * size + 19], - u: typed[i * size + 20], - v: typed[i * size + 21], - w: typed[i * size + 22], - x: typed[i * size + 23], - })); + const objects = times(length, i => ({ + a: typed[i * size + 0], + b: typed[i * size + 1], + c: typed[i * size + 2], + d: typed[i * size + 3], + e: typed[i * size + 4], + f: typed[i * size + 5], + g: typed[i * size + 6], + h: typed[i * size + 7], + i: typed[i * size + 8], + j: typed[i * size + 9], + k: typed[i * size + 10], + l: typed[i * size + 11], + m: typed[i * size + 12], + n: typed[i * size + 13], + o: typed[i * size + 14], + p: typed[i * size + 15], + q: typed[i * size + 16], + r: typed[i * size + 17], + s: typed[i * size + 18], + t: typed[i * size + 19], + u: typed[i * size + 20], + v: typed[i * size + 21], + w: typed[i * size + 22], + x: typed[i * size + 23], + })); - const indexes = times(length, () => (Math.random() * length) | 0); + const indexes = times(length, () => (Math.random() * length) | 0); - measure('typed', iterations, index => { - let sum = 0; - for (let i = 0; i < length; i++) { - const offset = (indexes[(i + index) % length]) * 24; + measure('typed', iterations, index => { + let sum = 0; + for (let i = 0; i < length; i++) { + const offset = (indexes[(i + index) % length]) * 24; - for (let j = 0; j < size; j++) { - sum += typed[offset + j]; - } - } - results.push(sum); - }); + for (let j = 0; j < size; j++) { + sum += typed[offset + j]; + } + } + results.push(sum); + }); - measure('typed2', iterations, index => { - let sum = 0; - for (let i = 0; i < length; i++) { - const offset = (indexes[(i + index) % length]) * 24; + measure('typed2', iterations, index => { + let sum = 0; + for (let i = 0; i < length; i++) { + const offset = (indexes[(i + index) % length]) * 24; - for (let j = 0; j < size; j++) { - sum += typed2[offset + j]; - } - } - results.push(sum); - }); + for (let j = 0; j < size; j++) { + sum += typed2[offset + j]; + } + } + results.push(sum); + }); - measure('objects', iterations, index => { - let sum = 0; - for (let i = 0; i < length; i++) { - const offset = indexes[(i + index) % length]; - const o = objects[offset]; - sum += o.a + o.b + o.c + o.d + o.e + o.f + o.g + o.h + o.i + o.j + o.k + o.l + - o.m + o.n + o.o + o.p + o.q + o.r + o.s + o.t + o.u + o.v + o.w + o.x; - } - results.push(sum); - }); + measure('objects', iterations, index => { + let sum = 0; + for (let i = 0; i < length; i++) { + const offset = indexes[(i + index) % length]; + const o = objects[offset]; + sum += o.a + o.b + o.c + o.d + o.e + o.f + o.g + o.h + o.i + o.j + o.k + o.l + + o.m + o.n + o.o + o.p + o.q + o.r + o.s + o.t + o.u + o.v + o.w + o.x; + } + results.push(sum); + }); } export function fillToOutlineTest() { - const iterations = 100000; + const iterations = 100000; - function fillToOutlineFast(color: string) { - return colorToHexRGB(parseColorFast(color)); - } + function fillToOutlineFast(color: string) { + return colorToHexRGB(parseColorFast(color)); + } - measure('FILL-TO-OUTLINE fillToOutline', iterations, () => { - fillToOutline('32cd32'); - }); + measure('FILL-TO-OUTLINE fillToOutline', iterations, () => { + fillToOutline('32cd32'); + }); - measure('FILL-TO-OUTLINE fillToOutlineFast', iterations, () => { - fillToOutlineFast('32cd32'); - }); + measure('FILL-TO-OUTLINE fillToOutlineFast', iterations, () => { + fillToOutlineFast('32cd32'); + }); } export function includeTest() { - const iterations = 100000; - const array = range(1000).map(() => random(0, 1000)); - let t = 0; + const iterations = 100000; + const array = range(1000).map(() => random(0, 1000)); + let t = 0; - // measure('INCLUDE _.includes', iterations, () => { - // t += includes(array, array[random(0, 1000)]) as any | 0; - // t += includes(array, array[random(0, 1000)]) as any | 0; - // t += includes(array, array[random(0, 1000)]) as any | 0; - // }); + // measure('INCLUDE _.includes', iterations, () => { + // t += includes(array, array[random(0, 1000)]) as any | 0; + // t += includes(array, array[random(0, 1000)]) as any | 0; + // t += includes(array, array[random(0, 1000)]) as any | 0; + // }); - measure('INCLUDE includes', iterations, () => { - t += utilsIncludes(array, array[random(0, 1000)]) as any | 0; - t += utilsIncludes(array, array[random(0, 1000)]) as any | 0; - t += utilsIncludes(array, array[random(0, 1000)]) as any | 0; - }); + measure('INCLUDE includes', iterations, () => { + t += utilsIncludes(array, array[random(0, 1000)]) as any | 0; + t += utilsIncludes(array, array[random(0, 1000)]) as any | 0; + t += utilsIncludes(array, array[random(0, 1000)]) as any | 0; + }); - measure('INCLUDE indexOf !== -1', iterations, () => { - t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0; - t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0; - t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0; - }); + measure('INCLUDE indexOf !== -1', iterations, () => { + t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0; + t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0; + t += (array.indexOf(array[random(0, 1000)]) !== -1) as any | 0; + }); - results.push(t); + results.push(t); } export function toColorListTest() { - function toColorList2Old(colors: number[]) { - return [0, ...colors.map(c => c || 0xff)]; - } + function toColorList2Old(colors: number[]) { + return [0, ...colors.map(c => c || 0xff)]; + } - const iterations = 100000; - let t: any[] = []; + const iterations = 100000; + let t: any[] = []; - measure('TOCOLORLIST2 old', iterations, () => { - t.push(toColorList2Old([1, 2, Date.now(), Date.now(), 5, 6])); - }); + measure('TOCOLORLIST2 old', iterations, () => { + t.push(toColorList2Old([1, 2, Date.now(), Date.now(), 5, 6])); + }); - measure('TOCOLORLIST2 new', iterations, () => { - t.push(toColorListNumber([1, 2, Date.now(), Date.now(), 5, 6])); - }); + measure('TOCOLORLIST2 new', iterations, () => { + t.push(toColorListNumber([1, 2, Date.now(), Date.now(), 5, 6])); + }); - results.push(t); + results.push(t); } export function copyTest() { - const iterations = 10000; - let t: any[] = []; - const src = new Uint32Array(1000); - const dst = new Uint32Array(10000); + const iterations = 10000; + let t: any[] = []; + const src = new Uint32Array(1000); + const dst = new Uint32Array(10000); - for (let i = 0; i < src.length; i++) { - src[i] = Math.random() * 10000; - } + for (let i = 0; i < src.length; i++) { + src[i] = Math.random() * 10000; + } - for (let i = 0; i < dst.length; i++) { - dst[i] = Math.random() * 10000; - } + for (let i = 0; i < dst.length; i++) { + dst[i] = Math.random() * 10000; + } - measure('ONE_BY_ONE', iterations, iteration => { - for (let i = 0; i < 10; i++) { - for (let j = 0; j < src.length; j++) { - dst[1000 * i + j] = src[j]; - } - } + measure('ONE_BY_ONE', iterations, iteration => { + for (let i = 0; i < 10; i++) { + for (let j = 0; j < src.length; j++) { + dst[1000 * i + j] = src[j]; + } + } - t.push(dst, iteration); - }); + t.push(dst, iteration); + }); - measure('COPY_BUFFER', iterations, iteration => { - for (let i = 0; i < 10; i++) { - dst.set(src, 1000 * i); - } + measure('COPY_BUFFER', iterations, iteration => { + for (let i = 0; i < 10; i++) { + dst.set(src, 1000 * i); + } - t.push(dst, iteration); - }); + t.push(dst, iteration); + }); - results.push(t); + results.push(t); } export function regexTest(testStrings: string[]) { - const iterations = 1000000; - const t: any[] = []; + const iterations = 1000000; + const t: any[] = []; - measure('WITH_GROUPS', iterations, iteration => { - const test = testStrings[iteration % testStrings.length]; - t.push(test.replace(/(a|b)(.)(.)(.)(.)(.)/, 'X')); - }); + measure('WITH_GROUPS', iterations, iteration => { + const test = testStrings[iteration % testStrings.length]; + t.push(test.replace(/(a|b)(.)(.)(.)(.)(.)/, 'X')); + }); - measure('WITHOUT_GROUPS', iterations, iteration => { - const test = testStrings[iteration % testStrings.length]; - t.push(test.replace(/(?:a|b)(?:.)(?:.)(?:.)(?:.)(?:.)/, 'X')); - }); + measure('WITHOUT_GROUPS', iterations, iteration => { + const test = testStrings[iteration % testStrings.length]; + t.push(test.replace(/(?:a|b)(?:.)(?:.)(?:.)(?:.)(?:.)/, 'X')); + }); - results.push(t); + results.push(t); } export function swearTest(testStrings: string[]) { - const iterations = 10000; - const t: any[] = []; + const iterations = 10000; + const t: any[] = []; - measure('TEST', iterations, iteration => { - const test = testStrings[iteration % testStrings.length]; - t.push(filterBadWords(test)); - }); + measure('TEST', iterations, iteration => { + const test = testStrings[iteration % testStrings.length]; + t.push(filterBadWords(test)); + }); - results.push(t); + results.push(t); } export function swearEntryTest(testStrings: string[], onResult: (output: string) => void) { - const iterations = 2000; - const output: any[] = []; - const entries = createMatchEntries(); + const iterations = 2000; + const output: any[] = []; + const entries = createMatchEntries(); - for (const e of entries) { - const start = performance.now(); + for (const e of entries) { + const start = performance.now(); - for (let i = 0; i < iterations; i++) { - const test = testStrings[i % testStrings.length]; - results.push(test.replace(e.regex, '*****')); - } + for (let i = 0; i < iterations; i++) { + const test = testStrings[i % testStrings.length]; + results.push(test.replace(e.regex, '*****')); + } - const diff = performance.now() - start; - output.push({ e, diff }); - } + const diff = performance.now() - start; + output.push({ e, diff }); + } - onResult(output - .sort((a, b) => b.diff - a.diff) - .map(x => `${x.diff.toFixed(2).padStart(6)} "${x.e.line}"`) - .join('\n')); + onResult(output + .sort((a, b) => b.diff - a.diff) + .map(x => `${x.diff.toFixed(2).padStart(6)} "${x.e.line}"`) + .join('\n')); } (window as any).__results = results; diff --git a/src/ts/components/tools/tools-regions/tools-regions.ts b/src/ts/components/tools/tools-regions/tools-regions.ts index 71a935d..b095f0c 100644 --- a/src/ts/components/tools/tools-regions/tools-regions.ts +++ b/src/ts/components/tools/tools-regions/tools-regions.ts @@ -9,164 +9,164 @@ import { Rect, Point } from '../../../common/interfaces'; import { toWorldX, toWorldY } from '../../../common/positionUtils'; export function getRegionsBounds(client: any, region: any) { - const screenSize = client.screenSize; - const width = Math.ceil(((1.3 * screenSize.width) / region.size) / 2) * 2 + 2; - const height = Math.ceil(((1.3 * screenSize.height) / region.size) / 2) * 2 + 2; - return rect(region.x - Math.ceil(width / 2), region.y - Math.ceil(height / 2), width, height); + const screenSize = client.screenSize; + const width = Math.ceil(((1.3 * screenSize.width) / region.size) / 2) * 2 + 2; + const height = Math.ceil(((1.3 * screenSize.height) / region.size) / 2) * 2 + 2; + return rect(region.x - Math.ceil(width / 2), region.y - Math.ceil(height / 2), width, height); } export function getRegionsBoundsCameraBased(_entity: Point, camera: Rect, regionSize: number) { - const left = Math.floor(camera.x / regionSize - 0.5); - const top = Math.floor(camera.y / regionSize - 0.5); - const right = Math.floor((camera.x + camera.w) / regionSize + 0.5); - const bottom = Math.floor((camera.y + camera.h) / regionSize + 0.5); - return rect(left, top, right - left, bottom - top); + const left = Math.floor(camera.x / regionSize - 0.5); + const top = Math.floor(camera.y / regionSize - 0.5); + const right = Math.floor((camera.x + camera.w) / regionSize + 0.5); + const bottom = Math.floor((camera.y + camera.h) / regionSize + 0.5); + return rect(left, top, right - left, bottom - top); } @Component({ - selector: 'tools-regions', - templateUrl: 'tools-regions.pug', + selector: 'tools-regions', + templateUrl: 'tools-regions.pug', }) export class ToolsRegions implements OnInit, OnDestroy { - currentMapSize = 80; - tileWidth = tileWidth; - tileHeight = tileHeight; - screen = { width: 390, height: 580 }; - // screen = { width: 1920, height: 1080 }; - regionsX = 18; - regionsY = 16; - regionSize = 8; - scale = 0.25; - zoom = 2; - regions: string[][]; - camera = createCamera(); - approxCamera = createCamera(); - player = { x: 0, y: 0 }; - frame = 0; - lastFrame = 0; - constructor() { - this.regions = times(this.regionsY, () => times(this.regionsX, () => '')); - } - ngOnInit() { - this.update(); - this.frame = requestAnimationFrame(this.tick); - } - ngOnDestroy() { - cancelAnimationFrame(this.frame); - } - update() { - const regionWidth = this.regionSize * tileWidth; - const regionHeight = this.regionSize * tileHeight; - const map = { width: this.regionsX * this.regionSize, height: this.regionsY * this.regionSize } as any; + currentMapSize = 80; + tileWidth = tileWidth; + tileHeight = tileHeight; + screen = { width: 390, height: 580 }; + // screen = { width: 1920, height: 1080 }; + regionsX = 18; + regionsY = 16; + regionSize = 8; + scale = 0.25; + zoom = 2; + regions: string[][]; + camera = createCamera(); + approxCamera = createCamera(); + player = { x: 0, y: 0 }; + frame = 0; + lastFrame = 0; + constructor() { + this.regions = times(this.regionsY, () => times(this.regionsX, () => '')); + } + ngOnInit() { + this.update(); + this.frame = requestAnimationFrame(this.tick); + } + ngOnDestroy() { + cancelAnimationFrame(this.frame); + } + update() { + const regionWidth = this.regionSize * tileWidth; + const regionHeight = this.regionSize * tileHeight; + const map = { width: this.regionsX * this.regionSize, height: this.regionsY * this.regionSize } as any; - this.camera.w = Math.ceil(this.screen.width / this.zoom); - this.camera.h = Math.ceil(this.screen.height / this.zoom); - updateCamera(this.camera, this.player, map); + this.camera.w = Math.ceil(this.screen.width / this.zoom); + this.camera.h = Math.ceil(this.screen.height / this.zoom); + updateCamera(this.camera, this.player, map); - this.approxCamera.w = this.camera.w * 1.3; - this.approxCamera.h = this.camera.h * 1.3; - centerCameraOn(this.approxCamera, this.player); - updateCamera(this.approxCamera, this.player, map); + this.approxCamera.w = this.camera.w * 1.3; + this.approxCamera.h = this.camera.h * 1.3; + centerCameraOn(this.approxCamera, this.player); + updateCamera(this.approxCamera, this.player, map); - this.regions.forEach(x => fill(x, '')); + this.regions.forEach(x => fill(x, '')); - const rx = clamp(Math.floor(this.player.x / this.regionSize), 0, this.regionsX - 1); - const ry = clamp(Math.floor(this.player.y / this.regionSize), 0, this.regionsY - 1); + const rx = clamp(Math.floor(this.player.x / this.regionSize), 0, this.regionsX - 1); + const ry = clamp(Math.floor(this.player.y / this.regionSize), 0, this.regionsY - 1); - const bounds1 = getRegionsBounds( - { - screenSize: { - width: Math.ceil(this.camera.w / tileWidth), - height: Math.ceil(this.camera.h / tileHeight) - } - }, - { size: this.regionSize, x: rx, y: ry }); + const bounds1 = getRegionsBounds( + { + screenSize: { + width: Math.ceil(this.camera.w / tileWidth), + height: Math.ceil(this.camera.h / tileHeight) + } + }, + { size: this.regionSize, x: rx, y: ry }); - const bounds2 = getRegionsBoundsCameraBased( - this.player, - rect(toWorldX(this.camera.x), toWorldY(this.camera.y), toWorldX(this.camera.w), toWorldY(this.camera.h)), - this.regionSize); + const bounds2 = getRegionsBoundsCameraBased( + this.player, + rect(toWorldX(this.camera.x), toWorldY(this.camera.y), toWorldX(this.camera.w), toWorldY(this.camera.h)), + this.regionSize); - const bounds = [bounds1, bounds2][1]; + const bounds = [bounds1, bounds2][1]; - for (let ix = 0; ix <= bounds.w; ix++) { - for (let iy = 0; iy <= bounds.h; iy++) { - const yy = bounds.y + iy; - const xx = bounds.x + ix; + for (let ix = 0; ix <= bounds.w; ix++) { + for (let iy = 0; iy <= bounds.h; iy++) { + const yy = bounds.y + iy; + const xx = bounds.x + ix; - if (xx >= 0 && xx < this.regionsX && yy >= 0 && yy < this.regionsY) { - this.regions[yy][xx] = 'Sienna'; - } - } - } + if (xx >= 0 && xx < this.regionsX && yy >= 0 && yy < this.regionsY) { + this.regions[yy][xx] = 'Sienna'; + } + } + } - for (let x = 0; x < this.regionsX; x++) { - for (let y = 0; y < this.regionsY; y++) { - if (isAreaVisible(this.camera, x * regionWidth, y * regionHeight, regionWidth, regionHeight)) { - if (this.regions[y][x] === 'Sienna') { - this.regions[y][x] = 'SeaGreen'; - } else { - this.regions[y][x] = 'Crimson'; - } - } - } - } + for (let x = 0; x < this.regionsX; x++) { + for (let y = 0; y < this.regionsY; y++) { + if (isAreaVisible(this.camera, x * regionWidth, y * regionHeight, regionWidth, regionHeight)) { + if (this.regions[y][x] === 'Sienna') { + this.regions[y][x] = 'SeaGreen'; + } else { + this.regions[y][x] = 'Crimson'; + } + } + } + } - this.regions[ry][rx] = 'MediumSeaGreen'; - } - dragRegion({ x, y }: AgDragEvent) { - this.player.x = x / (this.tileWidth * this.scale); - this.player.y = y / (this.tileHeight * this.scale); - this.update(); - } - private right = false; - private left = false; - private up = false; - private down = false; - @HostListener('window:keydown', ['$event']) - keydown(e: KeyboardEvent) { - if (e.keyCode === Key.KEY_P) { - this.zoom = this.zoom === 4 ? 1 : (this.zoom + 1); - this.update(); - } else if (e.keyCode === Key.RIGHT) { - this.right = true; - } else if (e.keyCode === Key.LEFT) { - this.left = true; - } else if (e.keyCode === Key.UP) { - this.up = true; - } else if (e.keyCode === Key.DOWN) { - this.down = true; - } - } - @HostListener('window:keyup', ['$event']) - keyup(e: KeyboardEvent) { - if (e.keyCode === Key.RIGHT) { - this.right = false; - } else if (e.keyCode === Key.LEFT) { - this.left = false; - } else if (e.keyCode === Key.UP) { - this.up = false; - } else if (e.keyCode === Key.DOWN) { - this.down = false; - } - } - tick = (now: number) => { - this.frame = requestAnimationFrame(this.tick); - const delta = (now - this.lastFrame) / 1000; - this.lastFrame = now; + this.regions[ry][rx] = 'MediumSeaGreen'; + } + dragRegion({ x, y }: AgDragEvent) { + this.player.x = x / (this.tileWidth * this.scale); + this.player.y = y / (this.tileHeight * this.scale); + this.update(); + } + private right = false; + private left = false; + private up = false; + private down = false; + @HostListener('window:keydown', ['$event']) + keydown(e: KeyboardEvent) { + if (e.keyCode === Key.KEY_P) { + this.zoom = this.zoom === 4 ? 1 : (this.zoom + 1); + this.update(); + } else if (e.keyCode === Key.RIGHT) { + this.right = true; + } else if (e.keyCode === Key.LEFT) { + this.left = true; + } else if (e.keyCode === Key.UP) { + this.up = true; + } else if (e.keyCode === Key.DOWN) { + this.down = true; + } + } + @HostListener('window:keyup', ['$event']) + keyup(e: KeyboardEvent) { + if (e.keyCode === Key.RIGHT) { + this.right = false; + } else if (e.keyCode === Key.LEFT) { + this.left = false; + } else if (e.keyCode === Key.UP) { + this.up = false; + } else if (e.keyCode === Key.DOWN) { + this.down = false; + } + } + tick = (now: number) => { + this.frame = requestAnimationFrame(this.tick); + const delta = (now - this.lastFrame) / 1000; + this.lastFrame = now; - let dx = 0; - let dy = 0; + let dx = 0; + let dy = 0; - if (this.right) dx += 1; - if (this.left) dx -= 1; - if (this.up) dy -= 1; - if (this.down) dy += 1; + if (this.right) dx += 1; + if (this.left) dx -= 1; + if (this.up) dy -= 1; + if (this.down) dy += 1; - if (dx || dy) { - this.player.x += dx * delta * PONY_SPEED_TROT; - this.player.y += dy * delta * PONY_SPEED_TROT; - this.update(); - } - } + if (dx || dy) { + this.player.x += dx * delta * PONY_SPEED_TROT; + this.player.y += dy * delta * PONY_SPEED_TROT; + this.update(); + } + } } diff --git a/src/ts/components/tools/tools-sheet/tools-sheet.ts b/src/ts/components/tools/tools-sheet/tools-sheet.ts index 5414b4e..04b2417 100644 --- a/src/ts/components/tools/tools-sheet/tools-sheet.ts +++ b/src/ts/components/tools/tools-sheet/tools-sheet.ts @@ -9,68 +9,68 @@ import { at } from '../../../common/utils'; import { sheets, Sheet } from '../../../common/sheets'; @Component({ - selector: 'tools-sheet', - templateUrl: 'tools-sheet.pug', - styleUrls: ['tools-sheet.scss'], + selector: 'tools-sheet', + templateUrl: 'tools-sheet.pug', + styleUrls: ['tools-sheet.scss'], }) export class ToolsSheet implements OnInit { - readonly homeIcon = faHome; - readonly syncIcon = faSync; - readonly imageIcon = faFileImage; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - sheets = sheets; - sheet = sheets[0] as Sheet; - scale = 2; - rows = 1; - cols = 1; - pattern = -1; - constructor(private storage: StorageService) { - const sheet = at(this.sheets, storage.getInt('tools-sheet-sheet'))!; + readonly homeIcon = faHome; + readonly syncIcon = faSync; + readonly imageIcon = faFileImage; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + sheets = sheets; + sheet = sheets[0] as Sheet; + scale = 2; + rows = 1; + cols = 1; + pattern = -1; + constructor(private storage: StorageService) { + const sheet = at(this.sheets, storage.getInt('tools-sheet-sheet'))!; - if ('name' in sheet) { - this.setSheet(sheet); - } - } - ngOnInit() { - loadAndInitSpriteSheets() - .then(() => this.redraw()); - } - setSheet(sheet: Sheet) { - sheet = sheet.spacer ? this.sheets[0] as Sheet : sheet; - this.sheet = sheet; - this.cols = getCols(sheet); - this.rows = getRows(sheet); - this.redraw(); - this.storage.setInt('tools-sheet-sheet', this.sheets.indexOf(sheet)); - } - png() { - this.redraw(); - saveCanvas(this.canvas.nativeElement, 'sheet.png'); - } - psd() { - const psd = createPsd(this.sheet, this.rows, this.cols); - psd.canvas = drawPsd(psd, 1); - savePsd(psd, `${this.sheet.file}.psd`); - } - allPSDs() { - this.sheets - .filter(x => 'name' in x && !!x.file) - .map(x => x as Sheet) - .forEach(sheet => { - const rows = getRows(sheet); - const cols = getCols(sheet); - const psd = createPsd(sheet, rows, cols); - psd.canvas = drawPsd(psd, 1); - savePsd(psd, `${sheet.file}.psd`); - }); - } - redraw() { - if (this.canvas) { - const psd = createPsd(this.sheet, this.rows, this.cols); - const layers = compact(psd.children!.map(c => c.children)); - const patterns = compact(layers.map(xs => xs.find(x => x.name === `pattern ${this.pattern}`))); - patterns.forEach(x => x.hidden = false); - drawPsd(psd, this.scale, this.canvas.nativeElement); - } - } + if ('name' in sheet) { + this.setSheet(sheet); + } + } + ngOnInit() { + loadAndInitSpriteSheets() + .then(() => this.redraw()); + } + setSheet(sheet: Sheet) { + sheet = sheet.spacer ? this.sheets[0] as Sheet : sheet; + this.sheet = sheet; + this.cols = getCols(sheet); + this.rows = getRows(sheet); + this.redraw(); + this.storage.setInt('tools-sheet-sheet', this.sheets.indexOf(sheet)); + } + png() { + this.redraw(); + saveCanvas(this.canvas.nativeElement, 'sheet.png'); + } + psd() { + const psd = createPsd(this.sheet, this.rows, this.cols); + psd.canvas = drawPsd(psd, 1); + savePsd(psd, `${this.sheet.file}.psd`); + } + allPSDs() { + this.sheets + .filter(x => 'name' in x && !!x.file) + .map(x => x as Sheet) + .forEach(sheet => { + const rows = getRows(sheet); + const cols = getCols(sheet); + const psd = createPsd(sheet, rows, cols); + psd.canvas = drawPsd(psd, 1); + savePsd(psd, `${sheet.file}.psd`); + }); + } + redraw() { + if (this.canvas) { + const psd = createPsd(this.sheet, this.rows, this.cols); + const layers = compact(psd.children!.map(c => c.children)); + const patterns = compact(layers.map(xs => xs.find(x => x.name === `pattern ${this.pattern}`))); + patterns.forEach(x => x.hidden = false); + drawPsd(psd, this.scale, this.canvas.nativeElement); + } + } } diff --git a/src/ts/components/tools/tools-states/tools-states.ts b/src/ts/components/tools/tools-states/tools-states.ts index c801e14..1cb864e 100644 --- a/src/ts/components/tools/tools-states/tools-states.ts +++ b/src/ts/components/tools/tools-states/tools-states.ts @@ -7,133 +7,133 @@ import { AnimatorState } from '../../../common/animator'; import { distance, flatten } from '../../../common/utils'; function getPos(key: string, type: 'x' | 'y', defaultValue = 100) { - const item = localStorage.getItem(`tools-stats-${key}-${type}`); - return item ? parseInt(item, 10) : defaultValue; + const item = localStorage.getItem(`tools-stats-${key}-${type}`); + return item ? parseInt(item, 10) : defaultValue; } function setPos(key: string, type: 'x' | 'y', value: number) { - localStorage.setItem(`tools-stats-${key}-${type}`, value.toString()); + localStorage.setItem(`tools-stats-${key}-${type}`, value.toString()); } const defaultPositions: any = { - 'any': { 'x': 917, 'y': 43 }, - 'standing': { 'x': 317, 'y': 300 }, - 'trotting': { 'x': 660, 'y': 304 }, - 'swimming': { 'x': 988, 'y': 435 }, - 'swimming-to-trotting': { 'x': 747, 'y': 548 }, - 'trotting-to-swimming': { 'x': 805, 'y': 413 }, - 'booping': { 'x': 106, 'y': 301 }, - 'booping-sitting': { 'x': 111, 'y': 569 }, - 'booping-lying': { 'x': 127, 'y': 821 }, - 'booping-flying': { 'x': 110, 'y': 64 }, - 'sitting': { 'x': 312, 'y': 569 }, - 'sitting-down': { 'x': 186, 'y': 437 }, - 'standing-up': { 'x': 313, 'y': 436 }, - 'sitting-to-trotting': { 'x': 517, 'y': 500 }, - 'lying': { 'x': 326, 'y': 818 }, - 'lying-down': { 'x': 254, 'y': 692 }, - 'sitting-up': { 'x': 385, 'y': 682 }, - 'lying-to-trotting': { 'x': 541, 'y': 731 }, - 'hovering': { 'x': 334, 'y': 21 }, - 'flying': { 'x': 641, 'y': 29 }, - 'flying-up': { 'x': 378, 'y': 182 }, - 'flying-down': { 'x': 247, 'y': 177 }, - 'trotting-to-flying': { 'x': 601, 'y': 188 }, - 'flying-to-trotting': { 'x': 805, 'y': 270 }, - 'swinging': { 'x': 484, 'y': 238 }, - 'swimming-to-flying': { 'x': 1063, 'y': 182 }, - 'flying-to-swimming': { 'x': 938, 'y': 232 }, - 'booping-swimming': { 'x': 1144, 'y': 432 } + 'any': { 'x': 917, 'y': 43 }, + 'standing': { 'x': 317, 'y': 300 }, + 'trotting': { 'x': 660, 'y': 304 }, + 'swimming': { 'x': 988, 'y': 435 }, + 'swimming-to-trotting': { 'x': 747, 'y': 548 }, + 'trotting-to-swimming': { 'x': 805, 'y': 413 }, + 'booping': { 'x': 106, 'y': 301 }, + 'booping-sitting': { 'x': 111, 'y': 569 }, + 'booping-lying': { 'x': 127, 'y': 821 }, + 'booping-flying': { 'x': 110, 'y': 64 }, + 'sitting': { 'x': 312, 'y': 569 }, + 'sitting-down': { 'x': 186, 'y': 437 }, + 'standing-up': { 'x': 313, 'y': 436 }, + 'sitting-to-trotting': { 'x': 517, 'y': 500 }, + 'lying': { 'x': 326, 'y': 818 }, + 'lying-down': { 'x': 254, 'y': 692 }, + 'sitting-up': { 'x': 385, 'y': 682 }, + 'lying-to-trotting': { 'x': 541, 'y': 731 }, + 'hovering': { 'x': 334, 'y': 21 }, + 'flying': { 'x': 641, 'y': 29 }, + 'flying-up': { 'x': 378, 'y': 182 }, + 'flying-down': { 'x': 247, 'y': 177 }, + 'trotting-to-flying': { 'x': 601, 'y': 188 }, + 'flying-to-trotting': { 'x': 805, 'y': 270 }, + 'swinging': { 'x': 484, 'y': 238 }, + 'swimming-to-flying': { 'x': 1063, 'y': 182 }, + 'flying-to-swimming': { 'x': 938, 'y': 232 }, + 'booping-swimming': { 'x': 1144, 'y': 432 } }; @Component({ - selector: 'tools-states', - templateUrl: 'tools-states.pug', - styleUrls: ['tools-states.scss'], + selector: 'tools-states', + templateUrl: 'tools-states.pug', + styleUrls: ['tools-states.scss'], }) export class ToolsStates { - readonly homeIcon = faHome; - private startX = 0; - private startY = 0; - arrowColors = ['orange', 'red', 'lime']; - states = ponyStates.map(state => { - const def = defaultPositions[state.name] || { x: 0, y: 0 }; + readonly homeIcon = faHome; + private startX = 0; + private startY = 0; + arrowColors = ['orange', 'red', 'lime']; + states = ponyStates.map(state => { + const def = defaultPositions[state.name] || { x: 0, y: 0 }; - return { - color: state.name === 'any' ? 'orange' : (state.animation.loop ? 'LightSeaGreen' : 'cornflowerblue'), - name: state.name, - variants: Object.keys(state.variants || {}).join(', '), - state, - x: getPos(state.name, 'x', def.x), - y: getPos(state.name, 'y', def.y), - }; - }); - arrows: { path: string; color: string; }[] = []; - times: { x: number; y: number; color: string; text: string; title?: string; }[] = []; - constructor() { - this.updateArrows(); - } - drag(state: any, { dx, dy, type }: AgDragEvent) { - if (type === 'start') { - this.startX = state.x; - this.startY = state.y; - } + return { + color: state.name === 'any' ? 'orange' : (state.animation.loop ? 'LightSeaGreen' : 'cornflowerblue'), + name: state.name, + variants: Object.keys(state.variants || {}).join(', '), + state, + x: getPos(state.name, 'x', def.x), + y: getPos(state.name, 'y', def.y), + }; + }); + arrows: { path: string; color: string; }[] = []; + times: { x: number; y: number; color: string; text: string; title?: string; }[] = []; + constructor() { + this.updateArrows(); + } + drag(state: any, { dx, dy, type }: AgDragEvent) { + if (type === 'start') { + this.startX = state.x; + this.startY = state.y; + } - setPos(state.name, 'x', state.x = this.startX + dx); - setPos(state.name, 'y', state.y = this.startY + dy); - this.updateArrows(); - } - logPositions() { - const positions = fromPairs(this.states.map(({ name, x, y }) => [name, { x, y }])); - console.log(JSON.stringify(positions).replace(/"/g, `'`).replace(/},/g, '},\n')); - } - private updateArrows() { - this.times = []; - this.arrows = flatten(this.states.map(s => s.state.from.map(f => ({ - to: s, - from: this.findState(f.state), - color: f.exitAfter === 0 ? (f.keepTime ? 'orange' : 'red') : 'lime', - exitAfter: f.exitAfter, - enterTime: f.enterTime, - onlyDirectTo: f.onlyDirectTo, - })))) - .filter(({ from, to }) => from && to) - .map(({ from, to, color, exitAfter, enterTime, onlyDirectTo }) => { - const length = distance(from, to) || 1; - const r1 = 50; - const nx1 = ((to.x - from.x) / length) * r1; - const ny1 = ((to.y - from.y) / length) * r1; - const r2 = 60; - const nx2 = ((to.x - from.x) / length) * r2; - const ny2 = ((to.y - from.y) / length) * r2; - const r3 = 75; - const nx3 = ((to.x - from.x) / length) * r3; - const ny3 = ((to.y - from.y) / length) * r3; - const r4 = 80; - const nx4 = ((to.x - from.x) / length) * r4; - const ny4 = ((to.y - from.y) / length) * r4; + setPos(state.name, 'x', state.x = this.startX + dx); + setPos(state.name, 'y', state.y = this.startY + dy); + this.updateArrows(); + } + logPositions() { + const positions = fromPairs(this.states.map(({ name, x, y }) => [name, { x, y }])); + console.log(JSON.stringify(positions).replace(/"/g, `'`).replace(/},/g, '},\n')); + } + private updateArrows() { + this.times = []; + this.arrows = flatten(this.states.map(s => s.state.from.map(f => ({ + to: s, + from: this.findState(f.state), + color: f.exitAfter === 0 ? (f.keepTime ? 'orange' : 'red') : 'lime', + exitAfter: f.exitAfter, + enterTime: f.enterTime, + onlyDirectTo: f.onlyDirectTo, + })))) + .filter(({ from, to }) => from && to) + .map(({ from, to, color, exitAfter, enterTime, onlyDirectTo }) => { + const length = distance(from, to) || 1; + const r1 = 50; + const nx1 = ((to.x - from.x) / length) * r1; + const ny1 = ((to.y - from.y) / length) * r1; + const r2 = 60; + const nx2 = ((to.x - from.x) / length) * r2; + const ny2 = ((to.y - from.y) / length) * r2; + const r3 = 75; + const nx3 = ((to.x - from.x) / length) * r3; + const ny3 = ((to.y - from.y) / length) * r3; + const r4 = 80; + const nx4 = ((to.x - from.x) / length) * r4; + const ny4 = ((to.y - from.y) / length) * r4; - const fromX = from.x + nx1; - const fromY = from.y + ny1; + const fromX = from.x + nx1; + const fromY = from.y + ny1; - if (exitAfter) { - this.times.push({ x: fromX, y: fromY, color, text: exitAfter.toFixed(1) }); - } + if (exitAfter) { + this.times.push({ x: fromX, y: fromY, color, text: exitAfter.toFixed(1) }); + } - if (enterTime) { - this.times.push({ x: to.x - nx3, y: to.y - ny3, color, text: enterTime.toFixed(1) }); - } + if (enterTime) { + this.times.push({ x: to.x - nx3, y: to.y - ny3, color, text: enterTime.toFixed(1) }); + } - if (onlyDirectTo) { - this.times.push({ - x: from.x + nx4, y: from.y + ny4, color, text: '?', title: `only directly to: ${onlyDirectTo.name}` - }); - } + if (onlyDirectTo) { + this.times.push({ + x: from.x + nx4, y: from.y + ny4, color, text: '?', title: `only directly to: ${onlyDirectTo.name}` + }); + } - return { path: `M ${fromX} ${fromY} L ${to.x - nx2} ${to.y - ny2}`, color }; - }); - } - private findState(state: AnimatorState) { - return this.states.find(s => s.state === state)!; - } + return { path: `M ${fromX} ${fromY} L ${to.x - nx2} ${to.y - ny2}`, color }; + }); + } + private findState(state: AnimatorState) { + return this.states.find(s => s.state === state)!; + } } diff --git a/src/ts/components/tools/tools-ui/tools-ui.ts b/src/ts/components/tools/tools-ui/tools-ui.ts index 3035a1a..121e9f1 100644 --- a/src/ts/components/tools/tools-ui/tools-ui.ts +++ b/src/ts/components/tools/tools-ui/tools-ui.ts @@ -43,201 +43,201 @@ tails.forEach((t, i) => t ? t[0].label = labels[i] : undefined); const colors = Object.values(colorNames); @Component({ - selector: 'tools-ui', - templateUrl: 'tools-ui.pug', + selector: 'tools-ui', + templateUrl: 'tools-ui.pug', }) export class ToolsUI implements OnInit, OnDestroy { - readonly homeIcon = faHome; - readonly starIcon = faStar; - readonly heartIcon = faHeart; - readonly lockIcon = faLock; - isHidden = isHidden; - isIgnored = isIgnored; - focusTrap = true; - tails = tails; - cmSize = CM_SIZE; - pony = offlinePonyInfo; - customOutlines = false; - pal = offlinePonyPal; - color = 'cornflowerblue'; - checked = true; - radio = 'a'; - slider = 50; - sprite = sprites.tails[0]![1]![0]; - fills = ['ff0000', '00ff00']; - outlines = ['990000', '009900']; - spriteActive = false; - selected = offlinePony; - timeout = fromNow(1000 * 3600 * 10).toISOString(); - autoCloseDropdown: any = true; - spamChatInterval: any; - initialized = false; - customChecked = false; - actionBarEditable = true; - tags = ['', ...getAllTags().map(t => t.id)]; - animationFrame: any; - virtualItems = times(1000, i => ({ value: i, name: `This is item ${i}`, color: colors[i % colors.length] })); - virtualItems2 = [{ name: 'An item 0' }]; - constructor( - private game: PonyTownGame, - private zone: NgZone, - public settings: SettingsService, - private modalService: BsModalService, - private model: Model, - ) { - this.selected.name = 'Offline Pony'; - this.selected.site = { - id: '', - name: 'Offline Pony (official)', - provider: 'twitter', - url: 'https://twitter.com/offlinepony', - }; - this.selected.tag = 'dev'; - this.selected.modInfo = { - account: 'offline-pony [abc]', - country: 'PL', - counters: { swears: 5 }, - age: 12, - }; + readonly homeIcon = faHome; + readonly starIcon = faStar; + readonly heartIcon = faHeart; + readonly lockIcon = faLock; + isHidden = isHidden; + isIgnored = isIgnored; + focusTrap = true; + tails = tails; + cmSize = CM_SIZE; + pony = offlinePonyInfo; + customOutlines = false; + pal = offlinePonyPal; + color = 'cornflowerblue'; + checked = true; + radio = 'a'; + slider = 50; + sprite = sprites.tails[0]![1]![0]; + fills = ['ff0000', '00ff00']; + outlines = ['990000', '009900']; + spriteActive = false; + selected = offlinePony; + timeout = fromNow(1000 * 3600 * 10).toISOString(); + autoCloseDropdown: any = true; + spamChatInterval: any; + initialized = false; + customChecked = false; + actionBarEditable = true; + tags = ['', ...getAllTags().map(t => t.id)]; + animationFrame: any; + virtualItems = times(1000, i => ({ value: i, name: `This is item ${i}`, color: colors[i % colors.length] })); + virtualItems2 = [{ name: 'An item 0' }]; + constructor( + private game: PonyTownGame, + private zone: NgZone, + public settings: SettingsService, + private modalService: BsModalService, + private model: Model, + ) { + this.selected.name = 'Offline Pony'; + this.selected.site = { + id: '', + name: 'Offline Pony (official)', + provider: 'twitter', + url: 'https://twitter.com/offlinepony', + }; + this.selected.tag = 'dev'; + this.selected.modInfo = { + account: 'offline-pony [abc]', + country: 'PL', + counters: { swears: 5 }, + age: 12, + }; - game.player = { - id: 123, - name: 'Player pony', - } as any; - game.party = { - leaderId: 0, - members: [ - { id: 1, leader: true, offline: false, pending: false, pony: offlinePony, self: false }, - { id: 2, leader: false, offline: true, pending: false, pony: supporterPony, self: false }, - { id: 3, leader: false, offline: false, pending: true, pony: pendingPony, self: false }, - ], - }; - game.onClock.next('00:00'); - game.failedFBO = true; - game.send = (action: (server: any) => T) => action({ - action() { }, - select() { }, - say() { }, - expression() { }, - getInvites: () => Promise.resolve([ - { id: 'a', info: OFFLINE_PONY, name: 'Offline Pony', active: true }, - { id: 'b', info: OFFLINE_PONY, name: 'Fuzzy', active: true }, - { id: 'c', info: OFFLINE_PONY, name: 'Meno', active: true }, - { id: 'd', info: OFFLINE_PONY, name: 'Offline Pony', active: true }, - { id: 'e', info: OFFLINE_PONY, name: 'Fuzzy', active: true }, - { id: 'f', info: OFFLINE_PONY, name: 'Meno', active: false }, - { id: 'g', info: OFFLINE_PONY, name: 'Offline Pony', active: false }, - { id: 'h', info: OFFLINE_PONY, name: 'Fuzzy', active: false }, - { id: 'i', info: OFFLINE_PONY, name: 'Meno', active: false }, - { id: 'j', info: OFFLINE_PONY, name: 'Meno', active: false }, - ]), - } as any); - } - ngOnInit() { - initFeatureFlags({}); + game.player = { + id: 123, + name: 'Player pony', + } as any; + game.party = { + leaderId: 0, + members: [ + { id: 1, leader: true, offline: false, pending: false, pony: offlinePony, self: false }, + { id: 2, leader: false, offline: true, pending: false, pony: supporterPony, self: false }, + { id: 3, leader: false, offline: false, pending: true, pony: pendingPony, self: false }, + ], + }; + game.onClock.next('00:00'); + game.failedFBO = true; + game.send = (action: (server: any) => T) => action({ + action() { }, + select() { }, + say() { }, + expression() { }, + getInvites: () => Promise.resolve([ + { id: 'a', info: OFFLINE_PONY, name: 'Offline Pony', active: true }, + { id: 'b', info: OFFLINE_PONY, name: 'Fuzzy', active: true }, + { id: 'c', info: OFFLINE_PONY, name: 'Meno', active: true }, + { id: 'd', info: OFFLINE_PONY, name: 'Offline Pony', active: true }, + { id: 'e', info: OFFLINE_PONY, name: 'Fuzzy', active: true }, + { id: 'f', info: OFFLINE_PONY, name: 'Meno', active: false }, + { id: 'g', info: OFFLINE_PONY, name: 'Offline Pony', active: false }, + { id: 'h', info: OFFLINE_PONY, name: 'Fuzzy', active: false }, + { id: 'i', info: OFFLINE_PONY, name: 'Meno', active: false }, + { id: 'j', info: OFFLINE_PONY, name: 'Meno', active: false }, + ]), + } as any); + } + ngOnInit() { + initFeatureFlags({}); - return loadAndInitSpriteSheets() - .then(() => { - initializeToys(mockPaletteManager); - this.initialized = true; - this.model.loading = true; - this.zone.runOutsideAngular(() => this.update()); - }); - } - ngOnDestroy() { - cancelAnimationFrame(this.animationFrame); - } - get baseHairColor() { - return getBaseFill(this.pony.mane); - } - get isFriend() { - return isFriend(this.selected); - } - set isFriend(value) { - this.selected.playerState = setFlag(this.selected.playerState, EntityPlayerState.Friend, value); - } - update() { - this.animationFrame = requestAnimationFrame(() => this.update()); - redrawActionButtons(this.game.actionsChanged); - this.game.actionsChanged = false; - this.game.onFrame.next(); - } - changed() { - syncLockedPonyInfo(this.pony); - } - toggleIgnored(entity: Entity) { - entity.playerState = setFlag(entity.playerState, EntityPlayerState.Ignored, !isIgnored(entity)); - } - toggleHidden(entity: Entity) { - entity.playerState = setFlag(entity.playerState, EntityPlayerState.Hidden, !isHidden(entity)); - } - spamChat(chatlog: ChatLog) { - if (this.spamChatInterval) { - clearInterval(this.spamChatInterval); - this.spamChatInterval = 0; - } else { - this.spamChatInterval = 1; - this.zone.runOutsideAngular(() => this.spamChatInterval = setInterval(() => { - chatlog.addMessage({ - id: 0, - crc: undefined, - name: randomString(random(1, 20)), - message: randomString(random(1, 40)), - type: MessageType.Chat - }); - }, 50)); - } - } - get isPartyLeader() { - return isPartyLeader(this.game); - } - set isPartyLeader(value: boolean) { - if (value) { - this.game.party!.leaderId = this.game.player!.id; - } else { - this.game.party!.leaderId = 1; - } - } - get chatlogOpacity() { - return this.settings.account.chatlogOpacity || DEFAULT_CHATLOG_OPACITY; - } - set chatlogOpacity(value: number) { - this.settings.account.chatlogOpacity = value; - } - addMessage(chatlog: ChatLog, message: string) { - chatlog.addMessage({ name: 'test name', id: 123, crc: undefined, message, type: MessageType.Chat }); - } - addWhisper(chatlog: ChatLog, message: string) { - chatlog.addMessage({ name: 'test name', id: 123, crc: undefined, message, type: MessageType.Whisper }); - } - angle = 45; - get angleInRad() { - return (this.angle / 180) * Math.PI; - } - get horizontalTileHeight() { - return 32 * Math.sin(this.angleInRad); - } - get verticalTileHeight() { - return 32 * Math.cos(this.angleInRad); - } - // angle = Math.asin(expectedHorizontalTileHeight / 32) // 0.848062078981481 - modalRef?: BsModalRef; - showModal(template: TemplateRef) { - this.modalRef = this.modalService.show(template, {}); - } - saveActions() { - if (DEVELOPMENT) { - const serialized = serializeActions(this.game.actions); - this.game.actions = deserializeActions(serialized); - console.log(serialized); - } - } - // actions - get expressionActionsColor() { - return ACTION_EXPRESSION_BG; - } - set expressionActionsColor(value) { - updateActionColor(colorToCSS(parseColor(value))); - this.game.actionsChanged = true; - } + return loadAndInitSpriteSheets() + .then(() => { + initializeToys(mockPaletteManager); + this.initialized = true; + this.model.loading = true; + this.zone.runOutsideAngular(() => this.update()); + }); + } + ngOnDestroy() { + cancelAnimationFrame(this.animationFrame); + } + get baseHairColor() { + return getBaseFill(this.pony.mane); + } + get isFriend() { + return isFriend(this.selected); + } + set isFriend(value) { + this.selected.playerState = setFlag(this.selected.playerState, EntityPlayerState.Friend, value); + } + update() { + this.animationFrame = requestAnimationFrame(() => this.update()); + redrawActionButtons(this.game.actionsChanged); + this.game.actionsChanged = false; + this.game.onFrame.next(); + } + changed() { + syncLockedPonyInfo(this.pony); + } + toggleIgnored(entity: Entity) { + entity.playerState = setFlag(entity.playerState, EntityPlayerState.Ignored, !isIgnored(entity)); + } + toggleHidden(entity: Entity) { + entity.playerState = setFlag(entity.playerState, EntityPlayerState.Hidden, !isHidden(entity)); + } + spamChat(chatlog: ChatLog) { + if (this.spamChatInterval) { + clearInterval(this.spamChatInterval); + this.spamChatInterval = 0; + } else { + this.spamChatInterval = 1; + this.zone.runOutsideAngular(() => this.spamChatInterval = setInterval(() => { + chatlog.addMessage({ + id: 0, + crc: undefined, + name: randomString(random(1, 20)), + message: randomString(random(1, 40)), + type: MessageType.Chat + }); + }, 50)); + } + } + get isPartyLeader() { + return isPartyLeader(this.game); + } + set isPartyLeader(value: boolean) { + if (value) { + this.game.party!.leaderId = this.game.player!.id; + } else { + this.game.party!.leaderId = 1; + } + } + get chatlogOpacity() { + return this.settings.account.chatlogOpacity || DEFAULT_CHATLOG_OPACITY; + } + set chatlogOpacity(value: number) { + this.settings.account.chatlogOpacity = value; + } + addMessage(chatlog: ChatLog, message: string) { + chatlog.addMessage({ name: 'test name', id: 123, crc: undefined, message, type: MessageType.Chat }); + } + addWhisper(chatlog: ChatLog, message: string) { + chatlog.addMessage({ name: 'test name', id: 123, crc: undefined, message, type: MessageType.Whisper }); + } + angle = 45; + get angleInRad() { + return (this.angle / 180) * Math.PI; + } + get horizontalTileHeight() { + return 32 * Math.sin(this.angleInRad); + } + get verticalTileHeight() { + return 32 * Math.cos(this.angleInRad); + } + // angle = Math.asin(expectedHorizontalTileHeight / 32) // 0.848062078981481 + modalRef?: BsModalRef; + showModal(template: TemplateRef) { + this.modalRef = this.modalService.show(template, {}); + } + saveActions() { + if (DEVELOPMENT) { + const serialized = serializeActions(this.game.actions); + this.game.actions = deserializeActions(serialized); + console.log(serialized); + } + } + // actions + get expressionActionsColor() { + return ACTION_EXPRESSION_BG; + } + set expressionActionsColor(value) { + updateActionColor(colorToCSS(parseColor(value))); + this.game.actionsChanged = true; + } } diff --git a/src/ts/components/tools/tools-variants/tools-variants.ts b/src/ts/components/tools/tools-variants/tools-variants.ts index d554d0b..716d93f 100644 --- a/src/ts/components/tools/tools-variants/tools-variants.ts +++ b/src/ts/components/tools/tools-variants/tools-variants.ts @@ -11,87 +11,87 @@ import { faHome } from '../../../client/icons'; import { paletteSpriteSheet } from '../../../generated/sprites'; @Component({ - selector: 'tools-variants', - templateUrl: 'tools-variants.pug', - styleUrls: ['tools-variants.scss'], + selector: 'tools-variants', + templateUrl: 'tools-variants.pug', + styleUrls: ['tools-variants.scss'], }) export class ToolsVariants implements OnInit { - readonly homeIcon = faHome; - @ViewChild('canvas', { static: true }) canvas!: ElementRef; - fields: string[]; - vertical: keyof PonyInfo = 'backMane'; - horizontal: keyof PonyInfo = 'mane'; - coat = 'red'; - hair = 'gold'; - justHead = false; - scale = 2; - private pony: PonyInfo = createDefaultPony(); - private state: PonyState = defaultPonyState(); - constructor() { - this.fields = Object.keys(this.pony) - .filter(key => { - const value = (this.pony as any)[key]; - return value && value.type !== undefined; - }); - } - ngOnInit() { - loadAndInitSpriteSheets() - .then(() => this.redraw()); - } - redraw() { - this.draw(); - } - private draw() { - this.pony.coatFill = this.coat; - this.pony.mane!.fills![0] = this.hair; - syncLockedPonyInfo(this.pony); + readonly homeIcon = faHome; + @ViewChild('canvas', { static: true }) canvas!: ElementRef; + fields: string[]; + vertical: keyof PonyInfo = 'backMane'; + horizontal: keyof PonyInfo = 'mane'; + coat = 'red'; + hair = 'gold'; + justHead = false; + scale = 2; + private pony: PonyInfo = createDefaultPony(); + private state: PonyState = defaultPonyState(); + constructor() { + this.fields = Object.keys(this.pony) + .filter(key => { + const value = (this.pony as any)[key]; + return value && value.type !== undefined; + }); + } + ngOnInit() { + loadAndInitSpriteSheets() + .then(() => this.redraw()); + } + redraw() { + this.draw(); + } + private draw() { + this.pony.coatFill = this.coat; + this.pony.mane!.fills![0] = this.hair; + syncLockedPonyInfo(this.pony); - this.fields.forEach(f => (this.pony as any)[f].type = 0); - (this.pony as any)[this.vertical].type = 999; - (this.pony as any)[this.horizontal].type = 999; + this.fields.forEach(f => (this.pony as any)[f].type = 0); + (this.pony as any)[this.vertical].type = 999; + (this.pony as any)[this.horizontal].type = 999; - const fixed: any = decompressPony(compressPonyString(this.pony)); - const maxX = fixed[this.horizontal].type; - const maxY = fixed[this.vertical].type; + const fixed: any = decompressPony(compressPonyString(this.pony)); + const maxX = fixed[this.horizontal].type; + const maxY = fixed[this.vertical].type; - const scale = this.scale; - const info = toPalette(this.pony); - const buffer = createCanvas(80, 80); - const batch = new ContextSpriteBatch(buffer); - const options = defaultDrawPonyOptions(); + const scale = this.scale; + const info = toPalette(this.pony); + const buffer = createCanvas(80, 80); + const batch = new ContextSpriteBatch(buffer); + const options = defaultDrawPonyOptions(); - const canvas = this.canvas.nativeElement as HTMLCanvasElement; - canvas.width = ((maxX + 1) * (this.justHead ? 45 : 60) + 10) * scale; - canvas.height = ((maxY + 1) * (this.justHead ? 45 : 60) + 10) * scale; + const canvas = this.canvas.nativeElement as HTMLCanvasElement; + canvas.width = ((maxX + 1) * (this.justHead ? 45 : 60) + 10) * scale; + canvas.height = ((maxY + 1) * (this.justHead ? 45 : 60) + 10) * scale; - const viewContext = canvas.getContext('2d')!; - viewContext.save(); - disableImageSmoothing(viewContext); - viewContext.scale(scale, scale); + const viewContext = canvas.getContext('2d')!; + viewContext.save(); + disableImageSmoothing(viewContext); + viewContext.scale(scale, scale); - viewContext.fillStyle = 'LightGreen'; - viewContext.fillRect(0, 0, canvas.width, canvas.height); + viewContext.fillStyle = 'LightGreen'; + viewContext.fillRect(0, 0, canvas.width, canvas.height); - for (let y = 0; y <= maxY; y++) { - (info as any)[this.vertical].type = y; + for (let y = 0; y <= maxY; y++) { + (info as any)[this.vertical].type = y; - for (let x = 0; x <= maxX; x++) { - batch.start(paletteSpriteSheet, 0); + for (let x = 0; x <= maxX; x++) { + batch.start(paletteSpriteSheet, 0); - (info as any)[this.horizontal].type = x; + (info as any)[this.horizontal].type = x; - drawPony(batch, info, this.state, 40, 60, options); + drawPony(batch, info, this.state, 40, 60, options); - batch.end(); + batch.end(); - if (this.justHead) { - viewContext.drawImage(buffer, 0, 0, 55, 45, x * 45 - 10, y * 45, 55, 45); - } else { - viewContext.drawImage(buffer, x * 60 - 10, y * 60); - } - } - } + if (this.justHead) { + viewContext.drawImage(buffer, 0, 0, 55, 45, x * 45 - 10, y * 45, 55, 45); + } else { + viewContext.drawImage(buffer, x * 60 - 10, y * 60); + } + } + } - viewContext.restore(); - } + viewContext.restore(); + } } diff --git a/src/ts/components/tools/tools-webgl/tools-webgl.ts b/src/ts/components/tools/tools-webgl/tools-webgl.ts index 98814c1..915fb09 100644 --- a/src/ts/components/tools/tools-webgl/tools-webgl.ts +++ b/src/ts/components/tools/tools-webgl/tools-webgl.ts @@ -26,15 +26,15 @@ import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; // } from '../../../generated/shaders'; @Component({ - selector: 'tools-webgl', - templateUrl: 'tools-webgl.pug', + selector: 'tools-webgl', + templateUrl: 'tools-webgl.pug', }) export class ToolsWebgl implements OnInit { - @ViewChild('canvas', { static: true }) canvasElement!: ElementRef; - @ViewChild('canvas2', { static: true }) canvasElement2!: ElementRef; - ngOnInit() { - // testSpriteBatch(this.canvasElement.nativeElement); - } + @ViewChild('canvas', { static: true }) canvasElement!: ElementRef; + @ViewChild('canvas2', { static: true }) canvasElement2!: ElementRef; + ngOnInit() { + // testSpriteBatch(this.canvasElement.nativeElement); + } } // export function testLightsShader(canvas: HTMLCanvasElement) { diff --git a/src/ts/components/tools/tools.module.ts b/src/ts/components/tools/tools.module.ts index 7e6cc89..c3aecdf 100644 --- a/src/ts/components/tools/tools.module.ts +++ b/src/ts/components/tools/tools.module.ts @@ -36,64 +36,64 @@ import { ToolsIndex } from './tools-index/tools-index'; import { ToolsApp } from './tools'; export const routes: Routes = [ - { path: '', component: ToolsIndex }, - { path: 'sheet', component: ToolsSheet }, - { path: 'states', component: ToolsStates }, - { path: 'variants', component: ToolsVariants }, - { path: 'webgl', component: ToolsWebgl }, - { path: 'animation/:id', component: ToolsAnimation }, - { path: 'animation', component: ToolsAnimation }, - { path: 'chat', component: ToolsChat }, - { path: 'expressions', component: ToolsExpressions }, - { path: 'entity', component: ToolsEntity }, - { path: 'palette', component: ToolsPalette }, - { path: 'perf', component: ToolsPerf }, - { path: 'regions', component: ToolsRegions }, - { path: 'ui', component: ToolsUI }, - { path: 'collisions', component: ToolsCollisions }, - { path: 'map', component: ToolsMap }, + { path: '', component: ToolsIndex }, + { path: 'sheet', component: ToolsSheet }, + { path: 'states', component: ToolsStates }, + { path: 'variants', component: ToolsVariants }, + { path: 'webgl', component: ToolsWebgl }, + { path: 'animation/:id', component: ToolsAnimation }, + { path: 'animation', component: ToolsAnimation }, + { path: 'chat', component: ToolsChat }, + { path: 'expressions', component: ToolsExpressions }, + { path: 'entity', component: ToolsEntity }, + { path: 'palette', component: ToolsPalette }, + { path: 'perf', component: ToolsPerf }, + { path: 'regions', component: ToolsRegions }, + { path: 'ui', component: ToolsUI }, + { path: 'collisions', component: ToolsCollisions }, + { path: 'map', component: ToolsMap }, ]; @NgModule({ - imports: [ - BrowserModule, - RouterModule, - FormsModule, - HttpClientModule, - SharedModule, - PopoverModule.forRoot(), - TypeaheadModule.forRoot(), - ButtonsModule.forRoot(), - RouterModule.forRoot(routes), - FontAwesomeModule, - NoopAnimationsModule, - ], - declarations: [ - ToolsRange, - ToolsFrame, - ToolsOffset, - ToolsXY, - ToolsExpressions, - ToolsAnimation, - ToolsChat, - ToolsVariants, - ToolsWebgl, - ToolsPalette, - ToolsPerf, - ToolsRegions, - ToolsEntity, - ToolsSheet, - ToolsStates, - ToolsCollisions, - ToolsMap, - ToolsUI, - ToolsIndex, - ToolsApp, - ], - providers: [ - ErrorReporter, - ], - bootstrap: [ToolsApp], + imports: [ + BrowserModule, + RouterModule, + FormsModule, + HttpClientModule, + SharedModule, + PopoverModule.forRoot(), + TypeaheadModule.forRoot(), + ButtonsModule.forRoot(), + RouterModule.forRoot(routes), + FontAwesomeModule, + NoopAnimationsModule, + ], + declarations: [ + ToolsRange, + ToolsFrame, + ToolsOffset, + ToolsXY, + ToolsExpressions, + ToolsAnimation, + ToolsChat, + ToolsVariants, + ToolsWebgl, + ToolsPalette, + ToolsPerf, + ToolsRegions, + ToolsEntity, + ToolsSheet, + ToolsStates, + ToolsCollisions, + ToolsMap, + ToolsUI, + ToolsIndex, + ToolsApp, + ], + providers: [ + ErrorReporter, + ], + bootstrap: [ToolsApp], }) export class ToolsAppModule { } diff --git a/src/ts/components/tools/tools.ts b/src/ts/components/tools/tools.ts index 3201cab..b5d4f71 100644 --- a/src/ts/components/tools/tools.ts +++ b/src/ts/components/tools/tools.ts @@ -3,20 +3,20 @@ import { TooltipConfig } from 'ngx-bootstrap/tooltip'; import { PopoverConfig } from 'ngx-bootstrap/popover'; export function tooltipConfig() { - return Object.assign(new TooltipConfig(), { container: 'body' }); + return Object.assign(new TooltipConfig(), { container: 'body' }); } export function popoverConfig() { - return Object.assign(new PopoverConfig(), { container: 'body' }); + return Object.assign(new PopoverConfig(), { container: 'body' }); } @Component({ - selector: 'pony-town-app', - templateUrl: 'tools.pug', - providers: [ - { provide: TooltipConfig, useFactory: tooltipConfig }, - { provide: PopoverConfig, useFactory: popoverConfig }, - ] + selector: 'pony-town-app', + templateUrl: 'tools.pug', + providers: [ + { provide: TooltipConfig, useFactory: tooltipConfig }, + { provide: PopoverConfig, useFactory: popoverConfig }, + ] }) export class ToolsApp { } diff --git a/src/ts/generated/gamepad-mappings.ts b/src/ts/generated/gamepad-mappings.ts index b4ed477..2cb24db 100644 --- a/src/ts/generated/gamepad-mappings.ts +++ b/src/ts/generated/gamepad-mappings.ts @@ -1,274 +1,274 @@ /* tslint:disable:max-line-length */ export interface GamepadMapping { - axes: AxesTable; - buttons: ButtonsTable; - name: string; - supported: any[]; + axes: AxesTable; + buttons: ButtonsTable; + name: string; + supported: any[]; } interface Index { - index: number; + index: number; } interface PositiveNegative { - buttonPositive: number; - buttonNegative: number; + buttonPositive: number; + buttonNegative: number; } interface Positive { - buttonPositive: number; + buttonPositive: number; } interface AxisDirection { - axis: number; - direction: number; + axis: number; + direction: number; } export const enum GamepadAxes { - LeftStickX, - LeftStickY, - RightStickX, - RightStickY, - DpadX, - DpadY, - LeftTrigger, - RightTrigger, + LeftStickX, + LeftStickY, + RightStickX, + RightStickY, + DpadX, + DpadY, + LeftTrigger, + RightTrigger, } export interface AxesTable { - [key: number]: Index | Positive | PositiveNegative | undefined; + [key: number]: Index | Positive | PositiveNegative | undefined; } export const enum GamepadButtons { - A, - B, - X, - Y, - Back, - Start, - DpadDown, - DpadLeft, - DpadRight, - DpadUp, - LeftShoulder, - LeftStick, - LeftStickDown, - LeftStickLeft, - LeftStickRight, - LeftStickUp, - LeftTrigger, - RightShoulder, - RightStick, - RightStickDown, - RightStickLeft, - RightStickRight, - RightStickUp, - RightTrigger, - Home, + A, + B, + X, + Y, + Back, + Start, + DpadDown, + DpadLeft, + DpadRight, + DpadUp, + LeftShoulder, + LeftStick, + LeftStickDown, + LeftStickLeft, + LeftStickRight, + LeftStickUp, + LeftTrigger, + RightShoulder, + RightStick, + RightStickDown, + RightStickLeft, + RightStickRight, + RightStickUp, + RightTrigger, + Home, } export interface GamepadBrowser { - browser: string; - id: string; - os: string; + browser: string; + id: string; + os: string; } export interface ButtonsTable { - [key: number]: Index | AxisDirection | undefined; + [key: number]: Index | AxisDirection | undefined; } function gamepad(name: string, supported: GamepadBrowser[], axes: AxesTable, buttons: ButtonsTable): GamepadMapping { - return { name, supported, axes, buttons }; + return { name, supported, axes, buttons }; } function browser(browser: string, id: string, os: string): GamepadBrowser { - return { browser, id, os }; + return { browser, id, os }; } function index(index: number): Index { - return { index }; + return { index }; } function positive(buttonPositive: number): Positive { - return { buttonPositive }; + return { buttonPositive }; } function positiveNegative(buttonPositive: number, buttonNegative: number): PositiveNegative { - return { buttonPositive, buttonNegative }; + return { buttonPositive, buttonNegative }; } function axisDirection(axis: number, direction: number): AxisDirection { - return { axis, direction }; + return { axis, direction }; } function axes( - lx: number, ly: number, rx: number, ry: number, dpadX?: Index | PositiveNegative, dpadY?: Index | PositiveNegative, - lt?: Index | Positive, rt?: Index | Positive + lx: number, ly: number, rx: number, ry: number, dpadX?: Index | PositiveNegative, dpadY?: Index | PositiveNegative, + lt?: Index | Positive, rt?: Index | Positive ): AxesTable { - const result: AxesTable = []; - result[GamepadAxes.LeftStickX] = index(lx); - result[GamepadAxes.LeftStickY] = index(ly); - result[GamepadAxes.RightStickX] = index(rx); - result[GamepadAxes.RightStickY] = index(ry); - result[GamepadAxes.DpadX] = dpadX; - result[GamepadAxes.DpadY] = dpadY; - result[GamepadAxes.LeftTrigger] = lt; - result[GamepadAxes.RightTrigger] = rt; - return result; + const result: AxesTable = []; + result[GamepadAxes.LeftStickX] = index(lx); + result[GamepadAxes.LeftStickY] = index(ly); + result[GamepadAxes.RightStickX] = index(rx); + result[GamepadAxes.RightStickY] = index(ry); + result[GamepadAxes.DpadX] = dpadX; + result[GamepadAxes.DpadY] = dpadY; + result[GamepadAxes.LeftTrigger] = lt; + result[GamepadAxes.RightTrigger] = rt; + return result; } function buttons( - a: number, - b: number, - x: number, - y: number, - back: number, - start: number, - dpad_down: Index | AxisDirection, - dpad_left: Index | AxisDirection, - dpad_right: Index | AxisDirection, - dpad_up: Index | AxisDirection, - left_shoulder: number, - left_stick: number, - left_stick_down: AxisDirection, - left_stick_left: AxisDirection, - left_stick_right: AxisDirection, - left_stick_up: AxisDirection, - left_trigger: Index | AxisDirection, - right_shoulder: number, - right_stick: number, - right_stick_down: AxisDirection, - right_stick_left: AxisDirection, - right_stick_right: AxisDirection | undefined, - right_stick_up: AxisDirection, - right_trigger: Index | AxisDirection, - home?: number, + a: number, + b: number, + x: number, + y: number, + back: number, + start: number, + dpad_down: Index | AxisDirection, + dpad_left: Index | AxisDirection, + dpad_right: Index | AxisDirection, + dpad_up: Index | AxisDirection, + left_shoulder: number, + left_stick: number, + left_stick_down: AxisDirection, + left_stick_left: AxisDirection, + left_stick_right: AxisDirection, + left_stick_up: AxisDirection, + left_trigger: Index | AxisDirection, + right_shoulder: number, + right_stick: number, + right_stick_down: AxisDirection, + right_stick_left: AxisDirection, + right_stick_right: AxisDirection | undefined, + right_stick_up: AxisDirection, + right_trigger: Index | AxisDirection, + home?: number, ): ButtonsTable { - const result: ButtonsTable = []; - result[GamepadButtons.A] = index(a); - result[GamepadButtons.B] = index(b); - result[GamepadButtons.X] = index(x); - result[GamepadButtons.Y] = index(y); - result[GamepadButtons.Back] = index(back); - result[GamepadButtons.Start] = index(start); - result[GamepadButtons.DpadDown] = dpad_down; - result[GamepadButtons.DpadLeft] = dpad_left; - result[GamepadButtons.DpadRight] = dpad_right; - result[GamepadButtons.DpadUp] = dpad_up; - result[GamepadButtons.LeftShoulder] = index(left_shoulder); - result[GamepadButtons.LeftStick] = index(left_stick); - result[GamepadButtons.LeftStickDown] = left_stick_down; - result[GamepadButtons.LeftStickLeft] = left_stick_left; - result[GamepadButtons.LeftStickRight] = left_stick_right; - result[GamepadButtons.LeftStickUp] = left_stick_up; - result[GamepadButtons.LeftTrigger] = left_trigger; - result[GamepadButtons.RightShoulder] = index(right_shoulder); - result[GamepadButtons.RightStick] = index(right_stick); - result[GamepadButtons.RightStickDown] = right_stick_down; - result[GamepadButtons.RightStickLeft] = right_stick_left; - result[GamepadButtons.RightStickRight] = right_stick_right; - result[GamepadButtons.RightStickUp] = right_stick_up; - result[GamepadButtons.RightTrigger] = right_trigger; - result[GamepadButtons.Home] = home ? index(home) : undefined; - return result; + const result: ButtonsTable = []; + result[GamepadButtons.A] = index(a); + result[GamepadButtons.B] = index(b); + result[GamepadButtons.X] = index(x); + result[GamepadButtons.Y] = index(y); + result[GamepadButtons.Back] = index(back); + result[GamepadButtons.Start] = index(start); + result[GamepadButtons.DpadDown] = dpad_down; + result[GamepadButtons.DpadLeft] = dpad_left; + result[GamepadButtons.DpadRight] = dpad_right; + result[GamepadButtons.DpadUp] = dpad_up; + result[GamepadButtons.LeftShoulder] = index(left_shoulder); + result[GamepadButtons.LeftStick] = index(left_stick); + result[GamepadButtons.LeftStickDown] = left_stick_down; + result[GamepadButtons.LeftStickLeft] = left_stick_left; + result[GamepadButtons.LeftStickRight] = left_stick_right; + result[GamepadButtons.LeftStickUp] = left_stick_up; + result[GamepadButtons.LeftTrigger] = left_trigger; + result[GamepadButtons.RightShoulder] = index(right_shoulder); + result[GamepadButtons.RightStick] = index(right_stick); + result[GamepadButtons.RightStickDown] = right_stick_down; + result[GamepadButtons.RightStickLeft] = right_stick_left; + result[GamepadButtons.RightStickRight] = right_stick_right; + result[GamepadButtons.RightStickUp] = right_stick_up; + result[GamepadButtons.RightTrigger] = right_trigger; + result[GamepadButtons.Home] = home ? index(home) : undefined; + return result; } export const GAMEPAD_MAPPINGS: GamepadMapping[] = [ - gamepad('Logitech F310 (DirectInput) Chrome/Firefox Linux', [ - browser('Chrome', 'Logitech Logitech Dual Action (Vendor: 046d Product: c216)', 'Linux'), - browser('Firefox', '046d-c216-Logitech Logitech Dual Action', 'Linux'), - ], - axes(0, 1, 2, 3, index(4), index(5)), - buttons(1, 2, 0, 3, 8, 9, axisDirection(5, 1), axisDirection(4, -1), axisDirection(4, 1), axisDirection(5, -1), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), - gamepad('Logitech F310 (DirectInput) Chrome Windows/OSX', [ - browser('Chrome', 'Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)', 'Mac OS X'), - browser('Chrome', 'Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)', 'Windows NT'), - ], - axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), - buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), - gamepad('Logitech F310 (DirectInput) Firefox OSX', [ - browser('Firefox', '46d-c216-Logitech Dual Action', 'Mac OS X'), - ], - axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), - buttons(1, 2, 0, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(2, 1), axisDirection(1, -1), axisDirection(1, 1), axisDirection(2, -1), index(6), 5, 11, axisDirection(4, 1), axisDirection(3, -1), undefined, axisDirection(4, -1), index(7), undefined)), - gamepad('Logitech F310 (DirectInput) Firefox Windows', [ - browser('Firefox', '046d-c216-Logitech Dual Action', 'Windows NT'), - ], - axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), - buttons(1, 2, 0, 3, 8, 9, index(14), index(15), index(16), index(13), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), - gamepad('Logitech F310 (XInput) Chrome Linux', [ - browser('Chrome', 'Logitech Gamepad F310 (STANDARD GAMEPAD Vendor: 046d Product: c21d)', 'Linux'), - ], - axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), - buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), - gamepad('Logitech F310 (XInput) Firefox Linux', [ - browser('Firefox', '046d-c21d-Logitech Gamepad F310', 'Linux'), - ], - axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), - buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), - gamepad('PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268) Chrome OSX Linux', [ - browser('Chrome', 'PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)', 'Mac OS X'), - browser('Chrome', 'Sony PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)', 'Linux'), - ], - axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), - buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), - gamepad('054c-0268-Sony PLAYSTATION(R)3 Controller Firefox Linux', [ - browser('Firefox', '054c-0268-Sony PLAYSTATION(R)3 Controller', 'Linux'), - ], - axes(0, 1, 2, 3, index(6), index(7), index(12), index(13)), - buttons(14, 13, 15, 12, 0, 3, index(6), index(7), index(5), index(4), 10, 1, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(8), 11, 2, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(9), 16)), - gamepad('PS4 Chrome Linux', [ - browser('Chrome', 'Sony Computer Entertainment Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)', 'Linux'), - ], - axes(0, 1, 2, 3, positiveNegative(15, 14), positiveNegative(13, 12), positive(6), positive(7)), - buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), - gamepad('PS4 Chrome Windows/OSX', [ - browser('Chrome', 'Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)', 'Windows NT'), - browser('Chrome', 'Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)', 'Mac OS X'), - ], - axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), - buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), - gamepad('PS4 Firefox Linux', [ - browser('Firefox', '054c-05c4-Sony Computer Entertainment Wireless Controller', 'Linux'), - ], - axes(0, 1, 2, 5, index(6), index(7), index(3), index(4)), - buttons(1, 2, 0, 3, 8, 9, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(5, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(5, -1), index(7), 12)), - gamepad('PS4 Firefox OSX', [ - browser('Firefox', '54c-5c4-Wireless Controller', 'Mac OS X'), - ], - axes(0, 1, 2, 5, index(6), index(7)), - buttons(0, 1, 2, 3, 8, 9, index(15), index(16), index(17), index(14), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(5, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(5, -1), index(7), 12)), - gamepad('XBone Chrome Linux', [ - browser('Chrome', 'Microsoft Controller (Vendor: 045e Product: 02d1)', 'Linux'), - ], - axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), - buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), - gamepad('Xbox One Chrome OSX Linux', [ - browser('Chrome', '©Microsoft Corporation Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)', 'Linux'), - browser('Chrome', 'Xbox One Controller (STANDARD GAMEPAD Vendor: 02d1 Product: 045e)', 'Mac OS X'), - ], - axes(0, 1, 2, 3), - buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), - gamepad('Xbone Firefox Linux', [ - browser('Firefox', '045e-02d1-Microsoft X-Box One pad', 'Linux'), - ], - axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), - buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), - gamepad('Xbox 360 Chrome Windows/OSX', [ - browser('Chrome', 'Xbox 360 Controller (STANDARD GAMEPAD Vendor: 028e Product: 045e)', 'Mac OS X'), - browser('Chrome', 'Xbox 360 Controller (XInput STANDARD GAMEPAD)', 'Windows NT'), - ], - axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), - buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), - gamepad('Xbox 360 Firefox Linux', [ - browser('Firefox', '045e-028e-Microsoft X-Box 360 pad', 'Linux'), - ], - axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), - buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), - gamepad('Xbox 360 FF Windows', [ - browser('Firefox', 'xinput', 'Windows NT'), - ], - axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), - buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), + gamepad('Logitech F310 (DirectInput) Chrome/Firefox Linux', [ + browser('Chrome', 'Logitech Logitech Dual Action (Vendor: 046d Product: c216)', 'Linux'), + browser('Firefox', '046d-c216-Logitech Logitech Dual Action', 'Linux'), + ], + axes(0, 1, 2, 3, index(4), index(5)), + buttons(1, 2, 0, 3, 8, 9, axisDirection(5, 1), axisDirection(4, -1), axisDirection(4, 1), axisDirection(5, -1), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), + gamepad('Logitech F310 (DirectInput) Chrome Windows/OSX', [ + browser('Chrome', 'Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)', 'Mac OS X'), + browser('Chrome', 'Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)', 'Windows NT'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), + gamepad('Logitech F310 (DirectInput) Firefox OSX', [ + browser('Firefox', '46d-c216-Logitech Dual Action', 'Mac OS X'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(1, 2, 0, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(2, 1), axisDirection(1, -1), axisDirection(1, 1), axisDirection(2, -1), index(6), 5, 11, axisDirection(4, 1), axisDirection(3, -1), undefined, axisDirection(4, -1), index(7), undefined)), + gamepad('Logitech F310 (DirectInput) Firefox Windows', [ + browser('Firefox', '046d-c216-Logitech Dual Action', 'Windows NT'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(1, 2, 0, 3, 8, 9, index(14), index(15), index(16), index(13), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), + gamepad('Logitech F310 (XInput) Chrome Linux', [ + browser('Chrome', 'Logitech Gamepad F310 (STANDARD GAMEPAD Vendor: 046d Product: c21d)', 'Linux'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('Logitech F310 (XInput) Firefox Linux', [ + browser('Firefox', '046d-c21d-Logitech Gamepad F310', 'Linux'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), + gamepad('PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268) Chrome OSX Linux', [ + browser('Chrome', 'PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)', 'Mac OS X'), + browser('Chrome', 'Sony PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)', 'Linux'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('054c-0268-Sony PLAYSTATION(R)3 Controller Firefox Linux', [ + browser('Firefox', '054c-0268-Sony PLAYSTATION(R)3 Controller', 'Linux'), + ], + axes(0, 1, 2, 3, index(6), index(7), index(12), index(13)), + buttons(14, 13, 15, 12, 0, 3, index(6), index(7), index(5), index(4), 10, 1, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(8), 11, 2, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(9), 16)), + gamepad('PS4 Chrome Linux', [ + browser('Chrome', 'Sony Computer Entertainment Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)', 'Linux'), + ], + axes(0, 1, 2, 3, positiveNegative(15, 14), positiveNegative(13, 12), positive(6), positive(7)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('PS4 Chrome Windows/OSX', [ + browser('Chrome', 'Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)', 'Windows NT'), + browser('Chrome', 'Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)', 'Mac OS X'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('PS4 Firefox Linux', [ + browser('Firefox', '054c-05c4-Sony Computer Entertainment Wireless Controller', 'Linux'), + ], + axes(0, 1, 2, 5, index(6), index(7), index(3), index(4)), + buttons(1, 2, 0, 3, 8, 9, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(5, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(5, -1), index(7), 12)), + gamepad('PS4 Firefox OSX', [ + browser('Firefox', '54c-5c4-Wireless Controller', 'Mac OS X'), + ], + axes(0, 1, 2, 5, index(6), index(7)), + buttons(0, 1, 2, 3, 8, 9, index(15), index(16), index(17), index(14), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(5, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(5, -1), index(7), 12)), + gamepad('XBone Chrome Linux', [ + browser('Chrome', 'Microsoft Controller (Vendor: 045e Product: 02d1)', 'Linux'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), + gamepad('Xbox One Chrome OSX Linux', [ + browser('Chrome', '©Microsoft Corporation Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)', 'Linux'), + browser('Chrome', 'Xbox One Controller (STANDARD GAMEPAD Vendor: 02d1 Product: 045e)', 'Mac OS X'), + ], + axes(0, 1, 2, 3), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('Xbone Firefox Linux', [ + browser('Firefox', '045e-02d1-Microsoft X-Box One pad', 'Linux'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), + gamepad('Xbox 360 Chrome Windows/OSX', [ + browser('Chrome', 'Xbox 360 Controller (STANDARD GAMEPAD Vendor: 028e Product: 045e)', 'Mac OS X'), + browser('Chrome', 'Xbox 360 Controller (XInput STANDARD GAMEPAD)', 'Windows NT'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), 16)), + gamepad('Xbox 360 Firefox Linux', [ + browser('Firefox', '045e-028e-Microsoft X-Box 360 pad', 'Linux'), + ], + axes(0, 1, 3, 4, index(6), index(7), index(2), index(5)), + buttons(0, 1, 2, 3, 6, 7, axisDirection(7, 1), axisDirection(6, -1), axisDirection(6, 1), axisDirection(7, -1), 4, 9, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), axisDirection(2, 1), 5, 10, axisDirection(4, 1), axisDirection(3, -1), axisDirection(3, 1), axisDirection(4, -1), axisDirection(5, 1), 8)), + gamepad('Xbox 360 FF Windows', [ + browser('Firefox', 'xinput', 'Windows NT'), + ], + axes(0, 1, 2, 3, index(6), index(7), undefined, index(5)), + buttons(0, 1, 2, 3, 8, 9, index(13), index(14), index(15), index(12), 4, 10, axisDirection(1, 1), axisDirection(0, -1), axisDirection(0, 1), axisDirection(1, -1), index(6), 5, 11, axisDirection(3, 1), axisDirection(2, -1), axisDirection(2, 1), axisDirection(3, -1), index(7), undefined)), ]; diff --git a/src/ts/generated/gamepad-template.ts b/src/ts/generated/gamepad-template.ts index 7a2cadb..94c1ddb 100644 --- a/src/ts/generated/gamepad-template.ts +++ b/src/ts/generated/gamepad-template.ts @@ -1,177 +1,177 @@ export interface GamepadMapping { - axes: AxesTable; - buttons: ButtonsTable; - name: string; - supported: any[]; + axes: AxesTable; + buttons: ButtonsTable; + name: string; + supported: any[]; } interface Index { - index: number; + index: number; } interface PositiveNegative { - buttonPositive: number; - buttonNegative: number; + buttonPositive: number; + buttonNegative: number; } interface Positive { - buttonPositive: number; + buttonPositive: number; } interface AxisDirection { - axis: number; - direction: number; + axis: number; + direction: number; } export const enum GamepadAxes { - LeftStickX, - LeftStickY, - RightStickX, - RightStickY, - DpadX, - DpadY, - LeftTrigger, - RightTrigger, + LeftStickX, + LeftStickY, + RightStickX, + RightStickY, + DpadX, + DpadY, + LeftTrigger, + RightTrigger, } export interface AxesTable { - [key: number]: Index | Positive | PositiveNegative | undefined; + [key: number]: Index | Positive | PositiveNegative | undefined; } export const enum GamepadButtons { - A, - B, - X, - Y, - Back, - Start, - DpadDown, - DpadLeft, - DpadRight, - DpadUp, - LeftShoulder, - LeftStick, - LeftStickDown, - LeftStickLeft, - LeftStickRight, - LeftStickUp, - LeftTrigger, - RightShoulder, - RightStick, - RightStickDown, - RightStickLeft, - RightStickRight, - RightStickUp, - RightTrigger, - Home, + A, + B, + X, + Y, + Back, + Start, + DpadDown, + DpadLeft, + DpadRight, + DpadUp, + LeftShoulder, + LeftStick, + LeftStickDown, + LeftStickLeft, + LeftStickRight, + LeftStickUp, + LeftTrigger, + RightShoulder, + RightStick, + RightStickDown, + RightStickLeft, + RightStickRight, + RightStickUp, + RightTrigger, + Home, } export interface GamepadBrowser { - browser: string; - id: string; - os: string; + browser: string; + id: string; + os: string; } export interface ButtonsTable { - [key: number]: Index | AxisDirection | undefined; + [key: number]: Index | AxisDirection | undefined; } export function gamepad(name: string, supported: GamepadBrowser[], axes: AxesTable, buttons: ButtonsTable): GamepadMapping { - return { name, supported, axes, buttons }; + return { name, supported, axes, buttons }; } export function browser(browser: string, id: string, os: string): GamepadBrowser { - return { browser, id, os }; + return { browser, id, os }; } export function index(index: number): Index { - return { index }; + return { index }; } export function positive(buttonPositive: number): Positive { - return { buttonPositive }; + return { buttonPositive }; } export function positiveNegative(buttonPositive: number, buttonNegative: number): PositiveNegative { - return { buttonPositive, buttonNegative }; + return { buttonPositive, buttonNegative }; } export function axisDirection(axis: number, direction: number): AxisDirection { - return { axis, direction }; + return { axis, direction }; } export function axes( - lx: number, ly: number, rx: number, ry: number, dpadX?: Index | PositiveNegative, dpadY?: Index | PositiveNegative, - lt?: Index | Positive, rt?: Index | Positive + lx: number, ly: number, rx: number, ry: number, dpadX?: Index | PositiveNegative, dpadY?: Index | PositiveNegative, + lt?: Index | Positive, rt?: Index | Positive ): AxesTable { - const result: AxesTable = []; - result[GamepadAxes.LeftStickX] = index(lx); - result[GamepadAxes.LeftStickY] = index(ly); - result[GamepadAxes.RightStickX] = index(rx); - result[GamepadAxes.RightStickY] = index(ry); - result[GamepadAxes.DpadX] = dpadX; - result[GamepadAxes.DpadY] = dpadY; - result[GamepadAxes.LeftTrigger] = lt; - result[GamepadAxes.RightTrigger] = rt; - return result; + const result: AxesTable = []; + result[GamepadAxes.LeftStickX] = index(lx); + result[GamepadAxes.LeftStickY] = index(ly); + result[GamepadAxes.RightStickX] = index(rx); + result[GamepadAxes.RightStickY] = index(ry); + result[GamepadAxes.DpadX] = dpadX; + result[GamepadAxes.DpadY] = dpadY; + result[GamepadAxes.LeftTrigger] = lt; + result[GamepadAxes.RightTrigger] = rt; + return result; } export function buttons( - a: number, - b: number, - x: number, - y: number, - back: number, - start: number, - dpad_down: Index | AxisDirection, - dpad_left: Index | AxisDirection, - dpad_right: Index | AxisDirection, - dpad_up: Index | AxisDirection, - left_shoulder: number, - left_stick: number, - left_stick_down: AxisDirection, - left_stick_left: AxisDirection, - left_stick_right: AxisDirection, - left_stick_up: AxisDirection, - left_trigger: Index | AxisDirection, - right_shoulder: number, - right_stick: number, - right_stick_down: AxisDirection, - right_stick_left: AxisDirection, - right_stick_right: AxisDirection | undefined, - right_stick_up: AxisDirection, - right_trigger: Index | AxisDirection, - home?: number, + a: number, + b: number, + x: number, + y: number, + back: number, + start: number, + dpad_down: Index | AxisDirection, + dpad_left: Index | AxisDirection, + dpad_right: Index | AxisDirection, + dpad_up: Index | AxisDirection, + left_shoulder: number, + left_stick: number, + left_stick_down: AxisDirection, + left_stick_left: AxisDirection, + left_stick_right: AxisDirection, + left_stick_up: AxisDirection, + left_trigger: Index | AxisDirection, + right_shoulder: number, + right_stick: number, + right_stick_down: AxisDirection, + right_stick_left: AxisDirection, + right_stick_right: AxisDirection | undefined, + right_stick_up: AxisDirection, + right_trigger: Index | AxisDirection, + home?: number, ): ButtonsTable { - const result: ButtonsTable = []; - result[GamepadButtons.A] = index(a); - result[GamepadButtons.B] = index(b); - result[GamepadButtons.X] = index(x); - result[GamepadButtons.Y] = index(y); - result[GamepadButtons.Back] = index(back); - result[GamepadButtons.Start] = index(start); - result[GamepadButtons.DpadDown] = dpad_down; - result[GamepadButtons.DpadLeft] = dpad_left; - result[GamepadButtons.DpadRight] = dpad_right; - result[GamepadButtons.DpadUp] = dpad_up; - result[GamepadButtons.LeftShoulder] = index(left_shoulder); - result[GamepadButtons.LeftStick] = index(left_stick); - result[GamepadButtons.LeftStickDown] = left_stick_down; - result[GamepadButtons.LeftStickLeft] = left_stick_left; - result[GamepadButtons.LeftStickRight] = left_stick_right; - result[GamepadButtons.LeftStickUp] = left_stick_up; - result[GamepadButtons.LeftTrigger] = left_trigger; - result[GamepadButtons.RightShoulder] = index(right_shoulder); - result[GamepadButtons.RightStick] = index(right_stick); - result[GamepadButtons.RightStickDown] = right_stick_down; - result[GamepadButtons.RightStickLeft] = right_stick_left; - result[GamepadButtons.RightStickRight] = right_stick_right; - result[GamepadButtons.RightStickUp] = right_stick_up; - result[GamepadButtons.RightTrigger] = right_trigger; - result[GamepadButtons.Home] = home ? index(home) : undefined; - return result; + const result: ButtonsTable = []; + result[GamepadButtons.A] = index(a); + result[GamepadButtons.B] = index(b); + result[GamepadButtons.X] = index(x); + result[GamepadButtons.Y] = index(y); + result[GamepadButtons.Back] = index(back); + result[GamepadButtons.Start] = index(start); + result[GamepadButtons.DpadDown] = dpad_down; + result[GamepadButtons.DpadLeft] = dpad_left; + result[GamepadButtons.DpadRight] = dpad_right; + result[GamepadButtons.DpadUp] = dpad_up; + result[GamepadButtons.LeftShoulder] = index(left_shoulder); + result[GamepadButtons.LeftStick] = index(left_stick); + result[GamepadButtons.LeftStickDown] = left_stick_down; + result[GamepadButtons.LeftStickLeft] = left_stick_left; + result[GamepadButtons.LeftStickRight] = left_stick_right; + result[GamepadButtons.LeftStickUp] = left_stick_up; + result[GamepadButtons.LeftTrigger] = left_trigger; + result[GamepadButtons.RightShoulder] = index(right_shoulder); + result[GamepadButtons.RightStick] = index(right_stick); + result[GamepadButtons.RightStickDown] = right_stick_down; + result[GamepadButtons.RightStickLeft] = right_stick_left; + result[GamepadButtons.RightStickRight] = right_stick_right; + result[GamepadButtons.RightStickUp] = right_stick_up; + result[GamepadButtons.RightTrigger] = right_trigger; + result[GamepadButtons.Home] = home ? index(home) : undefined; + return result; } export const GAMEPAD_MAPPINGS: GamepadMapping[] = [ - /*MAPPINGS*/ + /*MAPPINGS*/ ]; diff --git a/src/ts/generated/rev.ts b/src/ts/generated/rev.ts index cf9642c..c17339c 100644 --- a/src/ts/generated/rev.ts +++ b/src/ts/generated/rev.ts @@ -1,76 +1,76 @@ export const REV: { [key: string]: string; } = { - 'images/avatars.jpg': '0f8cf8266f', - 'images/logo-120.png': 'b051cf1915', - 'images/logo-64.png': '64a63307c9', - 'images/logo-gray.png': '7d4473f069', - 'images/logo-large.png': 'ef5e56ccc9', - 'images/logo-small.png': '770b141d9d', - 'images/logo.png': 'fedfb401e4', - 'images/pony.png': '981a26108b', - 'images/pony2.png': 'c030edb607', - 'images/pony2a.png': 'a6292b4e18', - 'music/ambient.mp3': 'd37b620d70', - 'music/ambient.webm': '4dbe61d738', - 'music/bossanova.mp3': 'd43e480500', - 'music/bossanova.webm': '50804c773f', - 'music/building.mp3': '33849b8cdf', - 'music/building.webm': '191ce0fe72', - 'music/cave-crystals.mp3': 'cdbbe63437', - 'music/cave-crystals.webm': '7a8a3f78d9', - 'music/cave-secrets.mp3': 'e0ad1b7375', - 'music/cave-secrets.webm': '427ee41405', - 'music/clop.mp3': '9f22e7790c', - 'music/clop.webm': 'f64c342651', - 'music/falling.mp3': '0af7264a52', - 'music/falling.webm': '60853db70e', - 'music/fivefour.mp3': '22bd477d78', - 'music/fivefour.webm': 'bc820e69db', - 'music/ghost.mp3': '428b1aa64f', - 'music/ghost.webm': 'aad1010bf1', - 'music/happy-house.mp3': '33124c1ada', - 'music/happy-house.webm': '2a3dbb08ba', - 'music/hypnosis.mp3': '6fdea241cf', - 'music/hypnosis.webm': '1564ab67ab', - 'music/island.mp3': '37cd6a1366', - 'music/island.webm': 'bccb8ecf08', - 'music/largo.mp3': '610c3069cc', - 'music/largo.webm': 'feebe2225a', - 'music/musicbox.mp3': '611cbd3a5b', - 'music/musicbox.webm': '16d28e09e9', - 'music/orchid.mp3': 'b55f22b71b', - 'music/orchid.webm': '49619b426a', - 'music/pumpkin.mp3': '87d399ff35', - 'music/pumpkin.webm': '24483ce5bc', - 'music/reindeer-winter.mp3': 'b694f34cd3', - 'music/reindeer-winter.webm': '723bf90a42', - 'music/reindeer.mp3': '36beea329a', - 'music/reindeer.webm': '15cb6fd996', - 'music/scherzo.mp3': '40368b2226', - 'music/scherzo.webm': '46ca4a9319', - 'music/school.mp3': '123fe87d99', - 'music/school.webm': '1382c07e55', - 'music/season.mp3': 'e344fa20b9', - 'music/season.webm': '1ae126e63e', - 'music/sunny-island.mp3': '5642308479', - 'music/sunny-island.webm': 'cf21301c77', - 'music/sweet-home.mp3': 'fec2f51460', - 'music/sweet-home.webm': 'cd615b0263', - 'music/tio.mp3': '9fc08033d2', - 'music/tio.webm': '0e04d47690', - 'music/trees-winter.mp3': '8beaf61d14', - 'music/trees-winter.webm': '61b71cb819', - 'music/trees.mp3': '26329a7c08', - 'music/trees.webm': 'e9a53a89e5', - 'music/trills.mp3': 'a9ba881602', - 'music/trills.webm': '77bf29797f', - 'music/unrest.mp3': 'b6271109eb', - 'music/unrest.webm': '60d492fe63', - 'music/waltzalt.mp3': '21c33fb0ee', - 'music/waltzalt.webm': '1456aa1916', - 'music/xmas-air.mp3': '4d1d720321', - 'music/xmas-air.webm': '4bfa7f0e7d', - 'music/xmas-horns.mp3': 'aa242e4e90', - 'music/xmas-horns.webm': '152b457225', - 'music/xmas-presents.mp3': '5efe475f4f', - 'music/xmas-presents.webm': '9eea1ab5c0' + 'images/avatars.jpg': '0f8cf8266f', + 'images/logo-120.png': 'b051cf1915', + 'images/logo-64.png': '64a63307c9', + 'images/logo-gray.png': '7d4473f069', + 'images/logo-large.png': 'ef5e56ccc9', + 'images/logo-small.png': '770b141d9d', + 'images/logo.png': 'fedfb401e4', + 'images/pony.png': '981a26108b', + 'images/pony2.png': 'c030edb607', + 'images/pony2a.png': 'a6292b4e18', + 'music/ambient.mp3': 'd37b620d70', + 'music/ambient.webm': '4dbe61d738', + 'music/bossanova.mp3': 'd43e480500', + 'music/bossanova.webm': '50804c773f', + 'music/building.mp3': '33849b8cdf', + 'music/building.webm': '191ce0fe72', + 'music/cave-crystals.mp3': 'cdbbe63437', + 'music/cave-crystals.webm': '7a8a3f78d9', + 'music/cave-secrets.mp3': 'e0ad1b7375', + 'music/cave-secrets.webm': '427ee41405', + 'music/clop.mp3': '9f22e7790c', + 'music/clop.webm': 'f64c342651', + 'music/falling.mp3': '0af7264a52', + 'music/falling.webm': '60853db70e', + 'music/fivefour.mp3': '22bd477d78', + 'music/fivefour.webm': 'bc820e69db', + 'music/ghost.mp3': '428b1aa64f', + 'music/ghost.webm': 'aad1010bf1', + 'music/happy-house.mp3': '33124c1ada', + 'music/happy-house.webm': '2a3dbb08ba', + 'music/hypnosis.mp3': '6fdea241cf', + 'music/hypnosis.webm': '1564ab67ab', + 'music/island.mp3': '37cd6a1366', + 'music/island.webm': 'bccb8ecf08', + 'music/largo.mp3': '610c3069cc', + 'music/largo.webm': 'feebe2225a', + 'music/musicbox.mp3': '611cbd3a5b', + 'music/musicbox.webm': '16d28e09e9', + 'music/orchid.mp3': 'b55f22b71b', + 'music/orchid.webm': '49619b426a', + 'music/pumpkin.mp3': '87d399ff35', + 'music/pumpkin.webm': '24483ce5bc', + 'music/reindeer-winter.mp3': 'b694f34cd3', + 'music/reindeer-winter.webm': '723bf90a42', + 'music/reindeer.mp3': '36beea329a', + 'music/reindeer.webm': '15cb6fd996', + 'music/scherzo.mp3': '40368b2226', + 'music/scherzo.webm': '46ca4a9319', + 'music/school.mp3': '123fe87d99', + 'music/school.webm': '1382c07e55', + 'music/season.mp3': 'e344fa20b9', + 'music/season.webm': '1ae126e63e', + 'music/sunny-island.mp3': '5642308479', + 'music/sunny-island.webm': 'cf21301c77', + 'music/sweet-home.mp3': 'fec2f51460', + 'music/sweet-home.webm': 'cd615b0263', + 'music/tio.mp3': '9fc08033d2', + 'music/tio.webm': '0e04d47690', + 'music/trees-winter.mp3': '8beaf61d14', + 'music/trees-winter.webm': '61b71cb819', + 'music/trees.mp3': '26329a7c08', + 'music/trees.webm': 'e9a53a89e5', + 'music/trills.mp3': 'a9ba881602', + 'music/trills.webm': '77bf29797f', + 'music/unrest.mp3': 'b6271109eb', + 'music/unrest.webm': '60d492fe63', + 'music/waltzalt.mp3': '21c33fb0ee', + 'music/waltzalt.webm': '1456aa1916', + 'music/xmas-air.mp3': '4d1d720321', + 'music/xmas-air.webm': '4bfa7f0e7d', + 'music/xmas-horns.mp3': 'aa242e4e90', + 'music/xmas-horns.webm': '152b457225', + 'music/xmas-presents.mp3': '5efe475f4f', + 'music/xmas-presents.webm': '9eea1ab5c0' }; diff --git a/src/ts/generated/shaders.ts b/src/ts/generated/shaders.ts index ba25a21..2ac9727 100644 --- a/src/ts/generated/shaders.ts +++ b/src/ts/generated/shaders.ts @@ -1,17 +1 @@ -/* tslint:disable */ - -export const basicShader = `// VERTEX attribute vec2 position; attribute vec2 texcoords; uniform mat4 transform; varying vec2 textureCoord; void main() { textureCoord = texcoords; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; varying vec2 textureCoord; void main() { gl_FragColor = texture2D(sampler1, textureCoord); }`; - -export const lightShader = `// VERTEX attribute vec2 position; attribute vec2 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec2 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; varying vec2 textureCoord; varying vec4 vColor; void main() { float d = clamp((1.0 - length(textureCoord)) + 0.1, 0.0, 1.0); float m = d * d * d; gl_FragColor = vec4(m, m, m, 1) * vColor; }`; - -export const paletteDepthShader = `// VERTEX attribute vec3 position; attribute vec4 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec4 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; varying vec4 textureCoord; varying vec4 vColor; void main() { vec4 sprite = texture2D(sampler1, vec2(textureCoord.x, textureCoord.y)); vec4 palette = texture2D(sampler2, vec2(textureCoord.z + sprite.x * pixelSize, textureCoord.w)); vec4 color = vec4(palette.xyz * sprite.y, palette.w) * vColor; gl_FragColor = color; if (color.a < 0.01) discard; }`; - -export const paletteLayersInstancedShader = `// VERTEX attribute vec2 position0; attribute vec4 position1; attribute vec4 texcoord0; attribute vec2 texcoord1; attribute vec4 vertexColor; attribute vec4 vertexColor1; uniform mat4 transform; uniform vec4 lighting; varying vec2 textureCoord0; varying vec2 textureCoord1; varying vec4 vColor; varying vec4 vColor1; void main() { textureCoord0 = vec2(texcoord0.x + position0.x * texcoord0.z, texcoord0.y + position0.y * texcoord0.w); textureCoord1 = texcoord1; vColor = vertexColor * lighting; vColor1 = vertexColor1; gl_Position = transform * vec4( position1.x + position0.x * position1.z, position1.y + position0.y * position1.w, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; varying vec2 textureCoord0; varying vec2 textureCoord1; varying vec4 vColor; varying vec4 vColor1; void main() { vec4 sprite = texture2D(sampler1, textureCoord0.xy); float shade = clamp(sprite.g + vColor1.a, 0.0, 1.0); vec4 mask = vec4(vColor1.rgb, 1.0 - (vColor1.r + vColor1.g + vColor1.b)); float paletteIndex = dot(mask, sprite); vec4 palette = texture2D(sampler2, vec2(textureCoord1.x + paletteIndex * pixelSize, textureCoord1.y)); gl_FragColor = vec4(palette.xyz * shade, palette.w) * vColor; }`; - -export const paletteLayersShader = `// VERTEX attribute vec2 position; attribute vec4 texcoords; attribute vec4 vertexColor; attribute vec4 vertexColor1; uniform mat4 transform; uniform vec4 lighting; varying vec4 textureCoord; varying vec4 vColor; varying vec4 vColor1; void main() { textureCoord = texcoords; // float f = texcoords.z; // float fr = fract(texcoords.z); // textureCoord.z = fr; // textureCoord.w = (f - fr) / 1024.0; vColor = vertexColor * lighting; vColor1 = vertexColor1; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; uniform float textureSize; varying vec4 textureCoord; varying vec4 vColor; varying vec4 vColor1; void main() { vec4 sprite = texture2D(sampler1, textureCoord.xy / textureSize); float shade = clamp(sprite.g + vColor1.a, 0.0, 1.0); vec4 mask = vec4(vColor1.rgb, 1.0 - (vColor1.r + vColor1.g + vColor1.b)); float paletteIndex = dot(mask, sprite); vec2 paletteCoord = textureCoord.zw; vec4 palette = texture2D(sampler2, vec2(paletteCoord.x + paletteIndex * pixelSize, paletteCoord.y)); gl_FragColor = vec4(palette.xyz * shade, palette.w) * vColor; }`; - -export const paletteShader = `// VERTEX attribute vec2 position; attribute vec4 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec4 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; varying vec4 textureCoord; varying vec4 vColor; void main() { vec4 sprite = texture2D(sampler1, vec2(textureCoord.x, textureCoord.y)); vec4 palette = texture2D(sampler2, vec2(textureCoord.z + sprite.x * pixelSize, textureCoord.w)); gl_FragColor = vec4(palette.xyz * sprite.y, palette.w) * vColor; }`; - -export const spriteShader = `// VERTEX attribute vec2 position; attribute vec2 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec2 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; uniform float textureSize; varying vec2 textureCoord; varying vec4 vColor; void main() { gl_FragColor = texture2D(sampler1, textureCoord / textureSize) * vColor; }`; - -export const sprite2Shader = `// VERTEX attribute vec3 position; attribute vec2 texcoords; attribute vec4 vertexColor; uniform mat4 transform; varying vec2 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor; gl_Position = transform * vec4(position, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; varying vec2 textureCoord; varying vec4 vColor; void main() { gl_FragColor = texture2D(sampler1, textureCoord) * vColor; }`; +/* tslint:disable */ export const basicShader = `// VERTEX attribute vec2 position; attribute vec2 texcoords; uniform mat4 transform; varying vec2 textureCoord; void main() { textureCoord = texcoords; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; varying vec2 textureCoord; void main() { gl_FragColor = texture2D(sampler1, textureCoord); }`; export const lightShader = `// VERTEX attribute vec2 position; attribute vec2 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec2 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; varying vec2 textureCoord; varying vec4 vColor; void main() { float d = clamp((1.0 - length(textureCoord)) + 0.1, 0.0, 1.0); float m = d * d * d; gl_FragColor = vec4(m, m, m, 1) * vColor; }`; export const paletteDepthShader = `// VERTEX attribute vec3 position; attribute vec4 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec4 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; varying vec4 textureCoord; varying vec4 vColor; void main() { vec4 sprite = texture2D(sampler1, vec2(textureCoord.x, textureCoord.y)); vec4 palette = texture2D(sampler2, vec2(textureCoord.z + sprite.x * pixelSize, textureCoord.w)); vec4 color = vec4(palette.xyz * sprite.y, palette.w) * vColor; gl_FragColor = color; if (color.a < 0.01) discard; }`; export const paletteLayersInstancedShader = `// VERTEX attribute vec2 position0; attribute vec4 position1; attribute vec4 texcoord0; attribute vec2 texcoord1; attribute vec4 vertexColor; attribute vec4 vertexColor1; uniform mat4 transform; uniform vec4 lighting; varying vec2 textureCoord0; varying vec2 textureCoord1; varying vec4 vColor; varying vec4 vColor1; void main() { textureCoord0 = vec2(texcoord0.x + position0.x * texcoord0.z, texcoord0.y + position0.y * texcoord0.w); textureCoord1 = texcoord1; vColor = vertexColor * lighting; vColor1 = vertexColor1; gl_Position = transform * vec4( position1.x + position0.x * position1.z, position1.y + position0.y * position1.w, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; varying vec2 textureCoord0; varying vec2 textureCoord1; varying vec4 vColor; varying vec4 vColor1; void main() { vec4 sprite = texture2D(sampler1, textureCoord0.xy); float shade = clamp(sprite.g + vColor1.a, 0.0, 1.0); vec4 mask = vec4(vColor1.rgb, 1.0 - (vColor1.r + vColor1.g + vColor1.b)); float paletteIndex = dot(mask, sprite); vec4 palette = texture2D(sampler2, vec2(textureCoord1.x + paletteIndex * pixelSize, textureCoord1.y)); gl_FragColor = vec4(palette.xyz * shade, palette.w) * vColor; }`; export const paletteLayersShader = `// VERTEX attribute vec2 position; attribute vec4 texcoords; attribute vec4 vertexColor; attribute vec4 vertexColor1; uniform mat4 transform; uniform vec4 lighting; varying vec4 textureCoord; varying vec4 vColor; varying vec4 vColor1; void main() { textureCoord = texcoords; // float f = texcoords.z; // float fr = fract(texcoords.z); // textureCoord.z = fr; // textureCoord.w = (f - fr) / 1024.0; vColor = vertexColor * lighting; vColor1 = vertexColor1; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; uniform float textureSize; varying vec4 textureCoord; varying vec4 vColor; varying vec4 vColor1; void main() { vec4 sprite = texture2D(sampler1, textureCoord.xy / textureSize); float shade = clamp(sprite.g + vColor1.a, 0.0, 1.0); vec4 mask = vec4(vColor1.rgb, 1.0 - (vColor1.r + vColor1.g + vColor1.b)); float paletteIndex = dot(mask, sprite); vec2 paletteCoord = textureCoord.zw; vec4 palette = texture2D(sampler2, vec2(paletteCoord.x + paletteIndex * pixelSize, paletteCoord.y)); gl_FragColor = vec4(palette.xyz * shade, palette.w) * vColor; }`; export const paletteShader = `// VERTEX attribute vec2 position; attribute vec4 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec4 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; // sprite uniform sampler2D sampler2; // palette uniform float pixelSize; varying vec4 textureCoord; varying vec4 vColor; void main() { vec4 sprite = texture2D(sampler1, vec2(textureCoord.x, textureCoord.y)); vec4 palette = texture2D(sampler2, vec2(textureCoord.z + sprite.x * pixelSize, textureCoord.w)); gl_FragColor = vec4(palette.xyz * sprite.y, palette.w) * vColor; }`; export const spriteShader = `// VERTEX attribute vec2 position; attribute vec2 texcoords; attribute vec4 vertexColor; uniform mat4 transform; uniform vec4 lighting; varying vec2 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor * lighting; gl_Position = transform * vec4(position, 0, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; uniform float textureSize; varying vec2 textureCoord; varying vec4 vColor; void main() { gl_FragColor = texture2D(sampler1, textureCoord / textureSize) * vColor; }`; export const sprite2Shader = `// VERTEX attribute vec3 position; attribute vec2 texcoords; attribute vec4 vertexColor; uniform mat4 transform; varying vec2 textureCoord; varying vec4 vColor; void main() { textureCoord = texcoords; vColor = vertexColor; gl_Position = transform * vec4(position, 1); } // FRAGMENT precision mediump float; uniform sampler2D sampler1; varying vec2 textureCoord; varying vec4 vColor; void main() { gl_FragColor = texture2D(sampler1, textureCoord) * vColor; }`; \ No newline at end of file diff --git a/src/ts/generated/sprites.ts b/src/ts/generated/sprites.ts index d3208fd..1762fa2 100644 --- a/src/ts/generated/sprites.ts +++ b/src/ts/generated/sprites.ts @@ -2,7 +2,7 @@ /* tslint:disable */ import { - Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, Shadow, ColorShadow, TileSprites, SpriteSheet + Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, Shadow, ColorShadow, TileSprites, SpriteSheet } from '../common/interfaces'; import { parseSpriteColor } from '../common/utils'; import { bitReaderCustom } from '../common/bitUtils'; @@ -1386,188 +1386,188 @@ export const fontMonoPal = createFont(sprites2, [ ]); const palettes: Uint32Array[] = createPalettes('0 0000004b 0000004d 00000059 00000081 000000 003f59 005763 00673f 006f19 0075a3 007a25 009152 0092cc 0097ff 00ab30 00ff9b 020202 03246f 036f28 040303 044200 050505 060606 070a0a 073b15 082647 097900 097a2b 09cc37 0a0a0a 0aff44 0b0a0a 0b0b0b 0c0202 0c9800 0d0b16 0d0d0c 0d0d0d 0f0a05 0f0f0f 0f1012 100f0f 101420 10718d 11090c 110a0d 110f0f 111721 111817 11211e 11720a 121212 131212 131313 131414 14182e 142325 151515 154422 161210 161515 1616164f 161616 161c1e 171111 171118 171616 171716 171717 17181a 176300 180909 181108 181211 18130e 181314 181619 181818 191718 191917 191f16 199bcf 19cf50 1a1515 1a181c 1a1a1d 1a2631 1b1514 1b181c 1b1f21 1b2447 1b2631 1b2731 1b4527 1c1b1c 1c2547 1c312d 1d1121 1d1a1a 1d1b1b 1d1c17 1d592e 1e1446 1e1616 1e1e1e 1e1f1e 1f150b 1f1916 1f1919 1f1d1d 1f436c 1f50cc 1fcc5e 20150e 201919 201d1e 201e20 202020 20241c 20671c 211211 211304 21190f 211a1a 211d11 211e23 213ea6 21a65a 222020 222222 222620 224100 224b17 23110f 231818 231e1e 23221a 232222 232323 232627 232728 233c5f 234873 23572e 235f55 236435 24121b 242330 242f26 244c7a 251810 252629 252e30 25494b 26160e 261c10 262122 262626 264284 26576f 271515 271c1d 271f1b 272421 272624 272727 275048 275049 2789cc 2789cd 27a4b7 282225 282425 282526 282626 282727 282827 28443f 28456d 286d62 2889cd 28dc00 291311 291d2b 291e11 291e1e 292929 294f2a 2a201f 2a2025 2a2626 2a274b 2a3932 2a3933 2a4c4b 2a5c2e 2ab45d 2b2116 2b2119 2b2128 2b2228 2b2318 2b2b32 2b2b45 2b2c2a 2b3234 2b473b 2b4bff 2b4e94 2b4e95 2bdfff 2c0605 2c1d18 2c1e1d 2c1f19 2c2c2c 2c354d 2d0706 2d0807 2d1d18 2d1e19 2d1e1f 2d2a2a 2d302b 2d3031 2d3235 2d590b 2d5e2f 2d5f9b 2e0806 2e0807 2e2011 2e2017 2e2221 2e2314 2e2d2e 2e2e2e 2e3130 2e4e49 2e6c42 2f2423 2f2711 2f633e 301414 301a0a 30211c 302528 30302f 303a3d 304708 306639 30930e 311e1c 31261f 31271f 312720 313130 313947 31803e 31cee5 322211 322425 32281f 322820 322e2d 322f35 323130 323230 323231 32332e 326e46 32b8ec 32ed6a 33261f 332624 33272d 333333 334d22 3363cc 33cc66 342624 342a19 343131 343434 346ba6 351f09 352626 35271e 352927 353233 353535 35363a 35383b 361f1f 362716 362826 362a22 362a29 362d1c 362e0e 362f30 363636 372b26 372f19 373f30 37474f 37843f 382e28 382e31 38332f 383637 385cb7 386c4f 386c51 390000 392125 392b2a 392d2a 393536 393636 393b33 394336 3a2b19 3a8246 3b1b25 3b2514 3b2b2a 3b3635 3b3b3b 3b6554 3b6958 3b6f17 3b7bc7 3b8751 3c2915 3c3c3c 3c3e45 3c4e42 3c6655 3d1220 3d2936 3d3523 3d3662 3d3e3c 3d6e41 3e2080 3e2844 3e2d2e 3e322a 3e551f 3e6543 3e9255 3f1d0f 3f1e0f 3f2f2e 3f332c 3f3721 3f3f44 3f4345 3f8c7f 402222 404973 407cff 40ff80 412719 412e2b 41342f 413625 41443e 416609 41660a 417e49 423b3a 423c3b 424138 42473f 427013 42bee7 42bfe8 432210 434141 434e0d 441717 442625 442b28 442e27 442f2d 443026 443433 444240 444444 452a1b 453524 453627 453730 455a64 457d09 45a435 46006b 462a16 462a17 462e2c 46362c 46372c 463e3f 464548 464646 46b727 470809 472e4e 47372c 47372d 474131 474445 47494b 475b5f 47ce12 482410 483621 483622 483a31 48436b 484444 484d52 485c60 488f49 48a105 48a293 493626 493726 49455a 494a48 4a176f 4a3522 4a3925 4a4f53 4a5055 4a79da 4b303b 4b3735 4b4b4b 4b5156 4c4645 4c4b4b 4c4d49 4c4f45 4c8958 4c895a 4e312e 4e3220 4e3e3a 4e534a 4e9d5d 4f2333 4f4036 4f4f4f 4f5246 4f6c1f 4fa5ef 503628 503d45 503e36 506452 51271d 512d08 514042 514e4b 515200 51565b 52235f 52333f 52472f 525252 52585e 529454 53380c 533c35 533c36 534228 540e2b 544a4b 544c4b 54534e 54705e 551928 553b38 55463f 554f4e 555f67 55687d 558a69 560a04 563c3a 56432f 564632 56480f 56b467 57031b 572524 57432d 575c51 57666e 582d77 58352f 583925 583d3a 584331 58463d 585355 585852 585858 586874 588a60 590b13 591202 591a08 592f20 593d3c 594f40 59513a 595652 595852 595e61 5989e7 59e531 5a0104 5a0c13 5a0c14 5a4a32 5a4b32 5a6167 5a7a24 5b4639 5b482c 5b4b42 5b4c32 5b6063 5c0000 5c1818 5c3b27 5c3e0e 5c503f 5c5134 5c5d5e 5c9519 5ca1d5 5ca751 5d2f17 5d5e5c 5da2d5 5e3d2a 5e5647 5e5e5d 5e5e5e 5e912a 5f2823 5f286d 5f3e13 5f5323 5f636a 5f8771 5f8a55 604238 604536 604b37 605856 606359 60635d 607d8b 60c539 613321 6190aa 6244a8 625d41 626b98 62ab64 634d1e 635d52 63635c 644a30 645144 64595a 645d52 64aacc 64ae69 654e44 656200 65827b 663931 666f9c 66896d 66ad51 66c7ed 66ed8f 673149 673e27 674531 676564 676767 67bb77 683f36 684c44 684e36 684f36 684f37 686f73 686f99 68a27b 68cb58 694e43 694f36 696548 696d5f 6a1832 6a39a4 6a570d 6a5800 6a615f 6a6b60 6a6f73 6b0000 6b0028 6b2233 6b4100 6b4834 6b5800 6b6800 6b6e67 6b788e 6bb46d 6c1f1f 6c3d24 6c7160 6c7275 6d2e28 6d3332 6d381a 6d6028 6dbfee 6e006f 6e2715 6e4a13 6e4b13 6e4c49 6e4e37 6e6e6e 6e706c 6e736a 6e7a80 6ea5c5 6ebfef 6f0000 6f0330 6f1681 6f1818 6f1d58 6f2400 6f3000 6f5d42 6f5e42 6f5e43 6f6803 6f6e67 6f6e74 6f6f54 6f6f6f 6f716d 6f8094 701f1c 704426 705b3d 705d43 707a80 70ae6e 70b57e 715547 715a53 71736e 71787b 719b2d 721946 722f2d 723b2e 724313 724b2c 725848 725944 72ae6e 736046 736967 736a51 737570 737869 737f84 73efe8 744070 74452c 745c3f 746442 746f67 747371 747671 74c51d 756139 756b48 757f82 758083 758184 758285 758386 758487 758588 758689 758699 75c075 765b4a 767173 76746c 76756c 76a6ff 76ed5c 770000 772542 774c3e 777971 777a73 778587 78716a 787462 787575 787879 793312 793e29 79443f 796337 797979 797c74 7a2424 7a2700 7a3fa1 7a4b2d 7a6909 7a8180 7a9f1c 7ab045 7affeb 7b1010 7b1438 7b7c74 7b858a 7c3831 7c4d34 7c5f41 7c6148 7c7654 7c9c50 7cb173 7cca92 7d0300 7d7071 7d7b5c 7d7d7d 7d8d95 7e583c 7e5c52 7ed4ff 7f5233 7fc484 80006c 80447e 805f4d 806a47 808080 808878 809034 815947 816a47 818181 822424 823320 82a692 838b8f 83cc4f 83e86e 842a38 843734 84673c 847a67 847e87 84805a 848378 848381 848c90 85234e 855f39 856f58 857243 858b95 858d91 8591a2 85d69e 867059 867a56 868e92 8691a2 8715c4 87564c 875c44 877c6d 877f6a 878c78 878f93 87969c 8797a9 87999f 88725a 888c83 889077 889094 8897bb 889aa0 89452e 895d3f 89624d 899195 899ba1 8a0e0e 8a2247 8a6245 8a7111 8a735c 8a8c86 8a9195 8a9296 8b1e40 8b574a 8b8161 8b8b8b 8c0a07 8c453f 8c4986 8c5421 8c5b3e 8c6c03 8c755d 8c8a90 8c9dbf 8d2b00 8ddcf2 8e0f24 8e3042 8e453f 8e6944 8e8b7c 8ec488 8f0807 8f0d24 8f4d57 8f563b 8f5738 90320b 90563c 907853 908d7c 91310b 91320c 917e53 918665 919783 921308 921509 922427 92508f 927a55 92968a 92c73b 92d097 92e09e 92e6ae 931309 931313 931409 937a55 93d5f5 94594b 94684c 946a26 94725d 948d8e 949800 94d6f6 950000 9553c1 955817 958e6b 95daf0 96563b 9671c1 969693 969eaa 97a0a5 97aac9 983471 984fcc 985a39 9876ff 98a4ab 997621 99775e 99a19f 99a2a7 9a653e 9a9583 9aa3a8 9af437 9b2e2d 9b9977 9b9c88 9ba4a9 9bafcc 9bc464 9c6141 9c8256 9c8a41 9c9c9c 9ca5aa 9d9d9e 9da6ab 9e1e1e 9e6c5c 9e7431 9e8c73 9e967d 9e9784 9e9999 9ea19a 9ebfd8 9f725f 9f855f a09577 a0a0a0 a0a499 a0a691 a0b442 a1051f a1061f a10620 a11616 a13600 a1371b a17d68 a25048 a261ce a2a2a2 a38964 a38a4e a39693 a3a08c a3a29a a3a7c2 a43700 a4ff63 a54e24 a58361 a58a6d a5b4ba a5cfe0 a63447 a83434 a84f97 a8b6d3 a8b8bf a91bf6 a94b41 a9b2ad a9b6d3 a9b7d4 aa3a4d aa9a9b aaa08b aaa696 ab643f ab6e4d ab7666 ab7b60 ababab ac19cf ac7146 acaa89 ad1416 ada6a5 ada896 aeaeae aeb6c3 af784f afb3a7 afbed9 b05911 b0ab8c b0b2ad b19248 b1ac9a b2360f b24717 b29562 b2b9bc b30e0e b38e68 b3b194 b49c61 b4b4b4 b4bcc0 b4ffda b52a2a b5d2e4 b67aff b6b300 b7346c b7b29a b7c410 b81414 b8a880 b9705e ba81b7 ba987f baa27f bbbbbb bc652b bc7625 bc7a62 bcc8c0 bd6a62 bd6c4d bd934d bdcde5 bdea85 be63ff be946e bebc9c becee6 befa82 bf1da7 bf8f7d bfa17a bfbfbf bfc1ba c05151 c08665 c0a165 c0ccc4 c1dcec c1eef0 c28959 c2a910 c2c2ba c3c3c3 c3ff82 c41042 c41254 c41515 c47e15 c4a624 c4a940 c4c012 c54a3a c54b3a c58347 c5d1e6 c63920 c64035 c64059 c68556 c6d2e7 c72424 c73c3b c8381f c93a3a c966ff c9871d c99560 c9b497 c9ecfd ca32ed ca6a2a ca861c ca861d caa175 cab06d caf0ff caf1fd cb871c cbb767 cbbfa8 cc02c9 cc3374 cc4400 cc5151 cc58ab cc5c08 ccb595 ccb89a ccc133 cccbc0 cccccc ccffc4 ccfff7 cd621a cd835c cd8452 cdb16e cde8f0 ce621a ce631a ce631b ce8452 ce9742 cee3ef cf1717 cf1919 cf2626 cf641d cf9742 cf9866 cfb319 d06fca d09742 d14600 d16549 d19f21 d1b683 d366ed d38755 d39741 d39b4a d3b3dc d3c5a7 d3ceab d3d3ba d3dff0 d43737 d4b070 d4c44c d4cdba d56a69 d60a0a d69f74 d6a260 d6bc74 d6caa5 d77099 d7936c d85543 d8ad7e d8dbd6 d8dc00 d91717 d97532 d97e09 d9a066 d9c759 da468c daa056 daeaf2 db5d2b dbc4a3 dc2d2d dcdcdc dd5c2b ddb75f ddcf71 ddd02a dddddd dea1ff deb028 deb02a df807f df917d dfaf28 dfc588 dfd7be e0a455 e0b028 e0b769 e0fffe e13955 e15151 e1c693 e1f8ff e344cb e38926 e3edfa e3f163 e4b065 e4cf7a e4eefb e586df e5b66e e5eefb e5f615 e6337b e6aa86 e6b66e e6b86f e6e000 e6e5d8 e6eefb e6f1f6 e7559b e78149 e7b280 e7b86f e84c31 e8c28e e9bc88 e9d19b eaed58 ebcd90 ec3232 ecb380 ecd132 ed3232 ed6666 ed943b edb480 edd470 ede1c7 ee8138 ee97e9 eec88f eed39f ef8237 f08a7d f095e9 f0bf4c f0e687 f0efe2 f19b37 f1b339 f1d934 f1d935 f281ed f28b1d f2b339 f2d26a f2da34 f38336 f38f41 f39f4b f3ad52 f3ed3a f495bf f5b332 f5d451 f5dae6 f61553 f6176a f61b1b f65a5a f69e1b f6ca05 f6f117 f7db9d f85592 f86754 f8f455 f8f8f8 f9c0ec f9c957 fa9381 fbffad fc20ff fc82b5 fdbb0b fefefe70 fefefe71 feff74 ff0000 ff00cc ff3c80 ff4090 ff5500 ff6666 ff6741 ff6c22 ff6ed6 ff740b ff7af9 ff9f3b ffae70 ffbaba ffc018 ffcd99 ffdb87 ffdd7a ffdfc1 ffe7b4 ffe98f ffea3b ffeb9e fff07f fff240 fff2cc fffd46 fffda4 fffef1 ffff47 ffffff', [ - [0, 1307, 5, 681, 1248, 1172, 1052, 141, 1306, 1216, 1155, 1283, 1277, 1287, 1278, 1016, 906, 720, 14, 16, 721, 182, 721], - [0, 28, 30, 43, 44, 54, 59, 81, 82, 83, 103, 149, 203, 208, 211, 212, 213, 216, 220, 240, 259, 272, 273, 322, 350, 352, 492, 497, 499, 554, 558, 573, 593, 594, 596, 599, 602, 654, 657, 669, 729, 742, 743, 746, 756, 759, 779, 828, 842, 848, 849, 878, 896, 898, 903, 910, 913, 921, 968, 970, 986, 987, 1002, 1007, 1027, 1050, 1070, 1071, 1077, 1078, 1083, 1090, 1093, 1096, 1106, 1111, 1119, 1121, 1124, 1128, 1129, 1131, 1132, 1134, 1150, 1152, 1156, 1157, 1160, 1162, 1173, 1181, 1188, 1194, 1200, 1209, 1211, 1212, 1220, 1221, 1222, 1223, 1232, 1233, 1236, 1247, 1251, 1254, 1262, 1268, 1279, 1281, 1290, 1294, 1296, 1302, 1307], - [0, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307], - [0, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264], - [0, 1295, 1295, 1295, 1292, 1292, 1288, 1288, 1288, 1158, 1158], - [0, 1307, 1307, 1307, 1304, 1304, 1298, 1298, 1298, 1273, 1273], - [0, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307], - [0, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264], - [0, 1295, 1295, 1292, 1292, 1288, 1288, 1158, 1158, 1158], - [0, 1307, 1307, 1304, 1304, 1298, 1298, 1273, 1273, 1273], - [0, 763], - [0, 1275], - [0, 1274, 1275], - [0, 1274], - [0, 1, 2, 62, 172, 195, 206, 256, 331, 403, 404, 491, 495, 603, 604, 672, 777, 879, 939, 1013, 1049, 1148], - [0, 1, 2, 62, 190, 207, 238, 256, 351, 403, 404, 562, 495, 603, 604, 672, 777, 1235, 939, 1270, 1049, 1148], - [0, 1, 2, 62, 343, 847, 575, 256, 997, 402, 404, 1195, 495, 601, 604, 672, 938, 1035, 938, 1198, 1048, 1146], - [0, 1, 2, 62, 167, 168, 193, 194, 247, 254, 255, 256, 313, 314, 403, 404, 429, 430, 447, 448, 529, 530, 603, 604, 608, 659, 660, 661, 674, 688, 777, 879, 939, 1013, 1049, 1148], - [0, 1, 2, 62, 820, 820, 590, 590, 343, 254, 255, 256, 977, 977, 402, 404, 429, 430, 1073, 1073, 529, 530, 601, 604, 902, 659, 660, 661, 1195, 1195, 776, 1035, 938, 1198, 1048, 1146], - [0, 1, 2, 62, 998, 998, 865, 865, 515, 254, 255, 256, 1091, 1091, 403, 404, 429, 430, 1182, 1182, 529, 530, 603, 604, 1092, 659, 660, 661, 1245, 1245, 777, 1235, 939, 1270, 1049, 1148], - [0, 1, 2, 62, 1003, 1003, 880, 880, 526, 254, 255, 256, 1112, 1112, 403, 404, 429, 430, 1190, 1190, 529, 530, 603, 604, 1092, 659, 660, 661, 1252, 1252, 777, 1235, 939, 1270, 1049, 1148], - [0, 1, 2, 62, 990, 990, 857, 857, 498, 254, 255, 256, 1076, 1076, 403, 404, 429, 430, 1168, 1168, 529, 530, 603, 604, 1092, 659, 660, 661, 1246, 1246, 777, 1235, 939, 1270, 1049, 1148], - [0, 402, 601, 776, 902, 938, 1035, 1040, 1048, 1146, 1198], - [0, 5, 24, 57, 64, 95, 110, 117, 153, 183, 236, 249, 290, 311, 414, 489, 509, 589, 717, 859, 1159], - [0, 122, 286, 465, 684], - [0, 275, 488, 504, 724, 731, 886, 927, 1038, 1055], - [0, 105, 201, 309, 318, 466, 507, 676, 698, 863, 939, 1110], - [0, 54, 73, 78, 123, 151, 156, 185, 233, 235, 281, 295, 323, 419, 434, 435, 534, 671, 923, 1010], - [0, 151, 233, 434, 534, 671, 923, 1010], - [0, 242, 304, 357, 934, 940, 1143], - [0, 54, 60, 73, 164, 202, 310, 357, 413, 434, 534, 940, 956, 1143, 1149, 1180], - [0, 34, 48, 49, 50, 66, 74, 97, 98, 121, 125, 142, 145, 161, 178, 179, 180, 239, 253, 347, 360, 385, 406, 410, 418, 428, 449, 470, 556, 557, 559, 635, 637, 638, 682, 741, 786, 840, 846, 952, 983], - [0, 69, 163, 213, 220, 221, 302, 477, 478, 542, 602, 795, 932, 1100, 1133], - [0, 896, 970, 1090, 1233, 1267], - [0, 8, 11, 12, 15, 36, 89, 172, 192, 195, 206, 252, 331, 343, 417, 454, 480, 491, 560, 575, 641, 694, 704, 723, 732, 748, 798, 838, 847, 950, 969, 975, 994, 997, 1001, 1004, 1056, 1195, 1225, 1293, 1307], - [0, 353, 642, 1122, 1126, 1202, 1211, 1229], - [0, 643, 1126, 1203], - [0, 80, 96, 170, 210, 376, 379, 567, 695, 791, 1184], - [675, 758, 879, 1013], - [0, 69, 158, 213, 216, 220, 221, 302, 473, 477, 478, 599, 602, 986], - [0, 94, 243, 324, 340, 614, 785, 968], - [0, 108, 384, 490, 518, 520, 540, 543, 551, 597, 625, 668, 705, 714, 761, 771, 814, 837, 887, 899, 917, 931, 933, 951, 1009, 1034, 1138, 1169, 1244], - [0, 700, 1185, 1205], - [0, 133, 492, 499, 558, 592, 595, 779, 851, 855, 872, 974, 1030, 1070, 1128, 1160, 1162, 1296], - [0, 5, 24, 57, 64, 95, 110, 117, 153, 183, 222, 236, 249, 290, 311, 414, 489, 509, 589, 717, 859, 1159], - [0, 5, 95, 110, 183, 222, 290, 311, 414, 589, 859, 1159], - [0, 300, 468, 496, 616, 890, 1063, 1253], - [0, 136, 262, 506, 716, 747, 827], - [0, 5, 40, 106, 248, 348, 432, 549, 644, 718, 719, 864, 888, 1044], - [0, 5, 17, 20, 22, 23, 32, 33, 42, 47, 53, 61, 75, 77, 79, 85, 99, 100, 116, 126, 129, 173, 174, 175, 191, 199, 223, 234, 265, 283, 296, 303, 320, 327, 328, 349, 355, 356, 374, 395, 421, 423, 440, 443, 455, 482, 508, 581, 618], - [0, 94, 251, 324, 325, 337, 484, 835, 1050], - [0, 219, 350, 371, 744, 756, 921, 1036], - [0, 69, 213, 216, 220, 221, 302, 473, 477, 599, 602, 986], - [0, 1307], - [0, 69, 131, 187, 215, 302, 338, 462, 463, 561, 586], - [0, 242, 304, 357, 361, 373, 690, 734, 836, 930, 934, 940, 957, 1043, 1143], - [0, 244, 278, 383, 539, 929, 1120], - [0, 1, 948, 1140, 1186], - [0, 291, 316, 391, 636, 730, 852, 991], - [0, 69, 157, 187, 215, 301, 302, 405, 462, 481, 582, 586, 760, 979], - [0, 130, 805, 822, 900, 902, 982, 1035, 1115, 1198, 1224], - [0, 78], - [197, 1014], - [5], - [0, 69, 216, 220, 291, 302, 316, 391, 473, 478, 599, 602, 636, 730, 852, 986, 991], - [0, 138, 275, 504, 552, 724, 788, 886, 980, 1136, 1145, 1226], - [0, 52, 118, 232, 261, 276, 282, 299, 335, 336, 368, 420, 479, 494, 511, 512, 577, 675, 736, 758, 787, 954, 993, 1013], - [0, 139, 275, 504, 724, 808, 886], - [0, 218, 370, 514, 744, 945, 1065, 1269], - [0, 218, 370, 227, 744, 427, 570, 1041], - [0, 40, 56, 90, 184, 204, 217, 341, 362, 471, 607, 858, 960, 1032, 1289], - [0, 80, 200, 201, 218, 219, 224, 227, 300, 318, 342, 350, 370, 371, 376, 411, 427, 468, 472, 496, 507, 514, 567, 570, 603, 616, 669, 698, 699, 744, 756, 777, 791, 839, 844, 863, 867, 890, 921, 939, 945, 1005, 1021, 1028, 1036, 1041, 1054, 1063, 1065, 1110, 1118, 1156, 1220, 1237, 1253, 1269, 1299], - [0, 778, 856, 1218, 1307], - [0, 382, 657, 775, 944, 961, 1096, 1284], - [0, 377, 739, 555, 745, 949, 1127, 1284], - [0, 2, 144, 188, 271, 334, 372, 453, 475, 600, 630], - [0, 245, 353, 354, 400, 401, 450, 476, 505, 541, 642, 643, 670, 767, 778, 856, 912, 967, 1074, 1089, 1116, 1122, 1126, 1167, 1197, 1203, 1211, 1218, 1307], - [0, 58, 65, 104, 135, 158, 162, 186, 214, 262, 287, 297, 329, 366, 387, 412, 486, 493, 506, 533, 610, 644, 686, 765, 888], - [0, 58, 65, 104, 135, 158, 76, 84, 88, 109, 124, 297, 329, 366, 189, 412, 289, 493, 298, 308, 367, 389, 535, 451, 535], - [0, 91, 96, 169, 170, 181, 209, 210, 378, 379, 606, 695, 825, 833, 834, 926, 928, 1184], - [0, 409, 631, 738, 916, 1075], - [0, 26, 111, 150, 229, 333], - [0, 250, 458, 532, 680, 876], - [0, 403, 683, 823, 897, 982, 1109, 1219], - [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1102, 1104, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1307], - [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 662, 332, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1102, 783, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 962], - [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 662, 769, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1102, 1042, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1189], - [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 624, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1061, 1064, 1098, 1099, 1102, 1104, 1199, 1204, 1259, 1261, 1265, 1285, 1286, 1301, 1307], - [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 399, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 806, 1064, 1098, 1099, 1102, 1104, 1199, 1204, 973, 1261, 1265, 1285, 1286, 1301, 1307], - [0, 6, 7, 10, 13, 19, 112, 113, 127, 160, 171, 260, 280, 332, 364, 364, 433, 584, 587, 621, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1058, 1037, 1060, 1064, 1098, 1099, 1102, 1104, 1255, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1307], - [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 9, 584, 587, 621, 627, 656, 658, 662, 640, 766, 783, 29, 962, 1017, 1020, 31, 1060, 1064, 1098, 1099, 1102, 1094, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1271], - [0, 6, 7, 10, 13, 19, 113, 113, 128, 160, 171, 260, 279, 332, 364, 364, 433, 584, 587, 621, 622, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1059, 1098, 1099, 1102, 1104, 1199, 1204, 1257, 1256, 1263, 1285, 1286, 1301, 1307], - [0, 6, 71, 10, 13, 18, 112, 113, 127, 160, 408, 525, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 653, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1095, 1104, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1280, 1307], - [0, 102, 143, 146, 201, 285, 307, 318, 459, 485, 507, 547, 623, 698, 863, 939, 978, 1110, 1258], - [0, 2, 277, 822, 982, 1115, 1224], - [0, 277, 629, 800, 820, 822, 977, 982, 1068, 1115, 1198, 1206, 1224], - [0, 2, 3, 743, 995, 1031, 1147, 1193, 1307], - [0, 839, 844, 1021, 1054, 1118, 1156, 1220], - [0, 797, 794, 1088, 1018, 1179, 1215, 1208], - [0, 159, 196, 228, 312, 345, 426, 438, 503, 524, 576, 585, 715, 740, 751, 768, 794, 797, 841, 877, 893, 953, 1018, 1047, 1088, 1125, 1144, 1161, 1176, 1179, 1196, 1208, 1215, 1228], - [0, 503, 740, 841, 893, 953, 1125, 1196, 1228], - [0, 794, 1018, 797, 1161, 1208, 1088, 1179, 1215], - [0, 159, 312, 345, 438, 524, 585, 768, 877], - [0, 196, 426, 751, 576, 715, 1047, 1144, 1176], - [0, 43, 81, 103, 149, 322, 346, 422, 573, 615, 898, 1135], - [0, 25, 147, 120, 294, 439, 398, 51, 609, 784, 784, 1105], - [0, 467, 516, 780, 810, 963, 992, 1084, 1142, 1247, 1249, 1300], - [0, 87, 92, 93, 154, 330, 339, 513, 757], - [0, 87, 148, 93, 258, 416, 425, 588, 781], - [0, 865, 1091, 1182, 1245], - [0, 870, 1112, 1190, 1243], - [0, 857, 1076, 1168, 1231], - [0, 256, 753, 865, 880, 883, 998, 1091, 1182, 1245], - [0, 256, 753, 870, 880, 883, 1003, 1112, 1190, 1243], - [0, 256, 753, 857, 880, 883, 990, 1076, 1168, 1246], - [0, 857, 865, 880, 1076, 1091, 1112, 1168, 1182, 1190, 1231, 1243, 1245], - [0, 830, 1063, 1253, 1297], - [0, 397, 703, 915, 1057], - [0, 41, 70, 86, 115, 126, 152, 173, 174, 199, 266, 292, 319, 355, 358, 423, 440, 508, 566, 581, 618], - [0, 218, 370, 669, 744, 1028, 1237, 1299], - [0, 39, 107, 198, 393, 500, 696, 754, 873, 1024], - [0, 39, 107, 198, 393, 500, 306, 754, 396, 569], - [0, 380, 632, 752, 860, 922], - [0, 326, 632, 645, 764, 829], - [0, 467, 516, 780, 810, 963, 992, 1084, 1247, 1249, 1300], - [0, 578, 583, 728, 854, 981, 1093], - [0, 415, 415, 531, 713, 826, 966], - [0, 1, 5, 130, 822, 982, 1115, 1224], - [0, 590, 820, 971, 976, 1035, 1068, 1073, 1139, 1195], - [0, 502, 649, 907, 782, 907, 907, 907, 928, 972], - [0, 21, 27, 35, 63, 67, 68, 69, 132, 166, 167, 176, 177, 187, 216, 230, 231, 257, 263, 264, 267, 268, 269, 277, 284, 313, 315, 411, 441, 444, 447, 456, 527, 528, 536, 538, 545, 553, 554, 611, 617, 626, 652, 655, 659, 666, 688, 722, 753, 777, 850, 861, 866, 871, 874, 882, 892, 894, 902, 946, 947, 1011, 1035, 1066, 1069, 1079, 1085, 1086, 1097, 1107, 1112, 1113, 1114, 1164, 1166, 1174, 1175, 1178, 1190, 1195, 1206, 1227, 1238, 1239, 1240, 1242, 1243, 1260, 1269, 1277, 1282, 1307], - [0, 293, 415, 537, 634, 750], - [0, 519, 574, 612, 691, 755, 790, 803, 837, 868, 895, 917, 989, 999, 1039, 1093, 1137], - [0, 519, 691, 820, 837, 917, 976, 977, 999, 1068, 1093, 1191, 1206], - [0, 517, 548, 571, 697, 733, 807, 862, 905, 984, 1033, 1067, 1108, 1151, 1201], - [0, 483, 619, 725, 817, 936, 1046, 1154], - [0, 69, 155, 213, 216, 220, 221, 291, 316, 391, 392, 464, 473, 477, 599, 602, 636, 685, 730, 843, 852, 885, 986, 991, 1023, 1072, 1177], - [0, 114, 288, 605, 772, 939], - [0, 683, 897, 988, 1053, 1123, 1210], - [0, 908, 1062, 1171, 1250, 1303, 1276], - [0, 683, 820, 897, 977, 988, 1053, 1068, 1123, 1206, 1210], - [0, 770, 1241], - [0, 664, 762, 935, 1045], - [0, 237, 407], - [0, 510, 522, 663, 881, 959, 1103, 1272, 1305], - [0, 101, 137, 375, 1291], - [0, 119, 305, 735, 1029, 1192], - [0, 274, 487, 687, 964, 1153, 1209, 1234, 1236, 1268], - [0, 546, 550, 891, 1082], - [0, 80, 342, 376, 472, 567, 699, 791, 867, 1209, 1236, 1268], - [0, 546, 639, 651, 884, 891], - [0, 550, 639, 891, 1082], - [0, 664, 762, 889, 1045], - [0, 134, 365, 460, 546, 651, 891], - [0, 762, 1266], - [0, 5, 521, 646, 762, 789, 925, 941, 1045, 1104, 1266, 1307], - [0, 546, 639, 891], - [0, 91, 96, 170, 210, 379, 550, 639, 651, 695, 891, 1184], - [0, 205, 344, 369, 452, 501, 549, 550, 639, 648, 651, 774, 891, 918], - [], - [0, 1, 4, 45, 46, 241, 246, 317, 386, 388, 394, 461, 563, 564, 565, 580, 677, 689, 796, 802, 816, 824, 831, 845, 853, 955, 965, 985, 1080, 1081, 1213], - [0, 1, 4, 37, 38, 165, 269, 390, 445, 568, 579, 628, 647, 667, 678, 692, 702, 726, 737, 749, 832, 875, 942, 996, 1000], - [675, 758, 774, 801, 879, 1013, 1022, 1183, 1217, 1262], - [1092, 1170, 809, 1170, 1235, 1270, 1022, 1141, 1187, 1230], - [909, 1025, 1026, 1035, 1101, 1139, 1165, 1195, 1198], - [687, 1008, 1087, 1153, 1214], - [55, 72, 225, 359, 523, 679, 834, 928, 1012], - [270, 321, 381, 446, 457, 598, 613, 633, 665, 693, 701, 792, 811, 818, 869, 943], - [549, 550, 572, 639, 648, 650, 773, 774, 891, 918, 1130, 1183, 1217, 1262], - [549, 550, 572, 639, 648, 650, 773, 774, 891, 918, 1130, 1141, 1187, 1230], - [549, 550, 572, 639, 648, 650, 773, 774, 891, 918, 1026, 1026, 1101, 1165], - [544, 550, 572, 639, 620, 650, 773, 727, 891, 813, 673, 673, 834, 928], - [549, 572, 648, 650, 773, 774, 918, 937, 1015, 1051, 1117, 1130, 1163, 1183, 1207, 1217, 1262], - [549, 572, 648, 650, 773, 774, 918, 937, 1015, 1051, 1117, 1141, 1163, 1141, 1207, 1187, 1230], - [549, 572, 648, 650, 773, 774, 918, 937, 1015, 1051, 1117, 1026, 1163, 1026, 1207, 1101, 1165], - [0, 2, 675, 758, 774, 801, 879, 1013], - [977, 1015, 1035, 1051, 1117, 1139, 1163, 1195, 1198], - [277, 424, 436, 437, 442, 469, 474, 531, 606, 706, 707, 708, 709, 710, 711, 712, 713, 782, 793, 799, 804, 812, 815, 819, 821, 825, 826, 834, 901, 911, 914, 919, 926, 928, 1006], - [0, 431, 549, 591, 675, 718, 758, 864, 879, 958, 1013, 1019], - [0, 431, 549, 924, 1092, 718, 1170, 864, 1235, 958, 1270, 1019], - [0, 431, 549, 920, 1035, 718, 1195, 864, 1139, 958, 1307, 1019], - [0, 5, 140, 226, 415, 531, 537, 606, 713, 826, 966] + [0, 1307, 5, 681, 1248, 1172, 1052, 141, 1306, 1216, 1155, 1283, 1277, 1287, 1278, 1016, 906, 720, 14, 16, 721, 182, 721], + [0, 28, 30, 43, 44, 54, 59, 81, 82, 83, 103, 149, 203, 208, 211, 212, 213, 216, 220, 240, 259, 272, 273, 322, 350, 352, 492, 497, 499, 554, 558, 573, 593, 594, 596, 599, 602, 654, 657, 669, 729, 742, 743, 746, 756, 759, 779, 828, 842, 848, 849, 878, 896, 898, 903, 910, 913, 921, 968, 970, 986, 987, 1002, 1007, 1027, 1050, 1070, 1071, 1077, 1078, 1083, 1090, 1093, 1096, 1106, 1111, 1119, 1121, 1124, 1128, 1129, 1131, 1132, 1134, 1150, 1152, 1156, 1157, 1160, 1162, 1173, 1181, 1188, 1194, 1200, 1209, 1211, 1212, 1220, 1221, 1222, 1223, 1232, 1233, 1236, 1247, 1251, 1254, 1262, 1268, 1279, 1281, 1290, 1294, 1296, 1302, 1307], + [0, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307], + [0, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264], + [0, 1295, 1295, 1295, 1292, 1292, 1288, 1288, 1288, 1158, 1158], + [0, 1307, 1307, 1307, 1304, 1304, 1298, 1298, 1298, 1273, 1273], + [0, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307, 1307], + [0, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264, 1264], + [0, 1295, 1295, 1292, 1292, 1288, 1288, 1158, 1158, 1158], + [0, 1307, 1307, 1304, 1304, 1298, 1298, 1273, 1273, 1273], + [0, 763], + [0, 1275], + [0, 1274, 1275], + [0, 1274], + [0, 1, 2, 62, 172, 195, 206, 256, 331, 403, 404, 491, 495, 603, 604, 672, 777, 879, 939, 1013, 1049, 1148], + [0, 1, 2, 62, 190, 207, 238, 256, 351, 403, 404, 562, 495, 603, 604, 672, 777, 1235, 939, 1270, 1049, 1148], + [0, 1, 2, 62, 343, 847, 575, 256, 997, 402, 404, 1195, 495, 601, 604, 672, 938, 1035, 938, 1198, 1048, 1146], + [0, 1, 2, 62, 167, 168, 193, 194, 247, 254, 255, 256, 313, 314, 403, 404, 429, 430, 447, 448, 529, 530, 603, 604, 608, 659, 660, 661, 674, 688, 777, 879, 939, 1013, 1049, 1148], + [0, 1, 2, 62, 820, 820, 590, 590, 343, 254, 255, 256, 977, 977, 402, 404, 429, 430, 1073, 1073, 529, 530, 601, 604, 902, 659, 660, 661, 1195, 1195, 776, 1035, 938, 1198, 1048, 1146], + [0, 1, 2, 62, 998, 998, 865, 865, 515, 254, 255, 256, 1091, 1091, 403, 404, 429, 430, 1182, 1182, 529, 530, 603, 604, 1092, 659, 660, 661, 1245, 1245, 777, 1235, 939, 1270, 1049, 1148], + [0, 1, 2, 62, 1003, 1003, 880, 880, 526, 254, 255, 256, 1112, 1112, 403, 404, 429, 430, 1190, 1190, 529, 530, 603, 604, 1092, 659, 660, 661, 1252, 1252, 777, 1235, 939, 1270, 1049, 1148], + [0, 1, 2, 62, 990, 990, 857, 857, 498, 254, 255, 256, 1076, 1076, 403, 404, 429, 430, 1168, 1168, 529, 530, 603, 604, 1092, 659, 660, 661, 1246, 1246, 777, 1235, 939, 1270, 1049, 1148], + [0, 402, 601, 776, 902, 938, 1035, 1040, 1048, 1146, 1198], + [0, 5, 24, 57, 64, 95, 110, 117, 153, 183, 236, 249, 290, 311, 414, 489, 509, 589, 717, 859, 1159], + [0, 122, 286, 465, 684], + [0, 275, 488, 504, 724, 731, 886, 927, 1038, 1055], + [0, 105, 201, 309, 318, 466, 507, 676, 698, 863, 939, 1110], + [0, 54, 73, 78, 123, 151, 156, 185, 233, 235, 281, 295, 323, 419, 434, 435, 534, 671, 923, 1010], + [0, 151, 233, 434, 534, 671, 923, 1010], + [0, 242, 304, 357, 934, 940, 1143], + [0, 54, 60, 73, 164, 202, 310, 357, 413, 434, 534, 940, 956, 1143, 1149, 1180], + [0, 34, 48, 49, 50, 66, 74, 97, 98, 121, 125, 142, 145, 161, 178, 179, 180, 239, 253, 347, 360, 385, 406, 410, 418, 428, 449, 470, 556, 557, 559, 635, 637, 638, 682, 741, 786, 840, 846, 952, 983], + [0, 69, 163, 213, 220, 221, 302, 477, 478, 542, 602, 795, 932, 1100, 1133], + [0, 896, 970, 1090, 1233, 1267], + [0, 8, 11, 12, 15, 36, 89, 172, 192, 195, 206, 252, 331, 343, 417, 454, 480, 491, 560, 575, 641, 694, 704, 723, 732, 748, 798, 838, 847, 950, 969, 975, 994, 997, 1001, 1004, 1056, 1195, 1225, 1293, 1307], + [0, 353, 642, 1122, 1126, 1202, 1211, 1229], + [0, 643, 1126, 1203], + [0, 80, 96, 170, 210, 376, 379, 567, 695, 791, 1184], + [675, 758, 879, 1013], + [0, 69, 158, 213, 216, 220, 221, 302, 473, 477, 478, 599, 602, 986], + [0, 94, 243, 324, 340, 614, 785, 968], + [0, 108, 384, 490, 518, 520, 540, 543, 551, 597, 625, 668, 705, 714, 761, 771, 814, 837, 887, 899, 917, 931, 933, 951, 1009, 1034, 1138, 1169, 1244], + [0, 700, 1185, 1205], + [0, 133, 492, 499, 558, 592, 595, 779, 851, 855, 872, 974, 1030, 1070, 1128, 1160, 1162, 1296], + [0, 5, 24, 57, 64, 95, 110, 117, 153, 183, 222, 236, 249, 290, 311, 414, 489, 509, 589, 717, 859, 1159], + [0, 5, 95, 110, 183, 222, 290, 311, 414, 589, 859, 1159], + [0, 300, 468, 496, 616, 890, 1063, 1253], + [0, 136, 262, 506, 716, 747, 827], + [0, 5, 40, 106, 248, 348, 432, 549, 644, 718, 719, 864, 888, 1044], + [0, 5, 17, 20, 22, 23, 32, 33, 42, 47, 53, 61, 75, 77, 79, 85, 99, 100, 116, 126, 129, 173, 174, 175, 191, 199, 223, 234, 265, 283, 296, 303, 320, 327, 328, 349, 355, 356, 374, 395, 421, 423, 440, 443, 455, 482, 508, 581, 618], + [0, 94, 251, 324, 325, 337, 484, 835, 1050], + [0, 219, 350, 371, 744, 756, 921, 1036], + [0, 69, 213, 216, 220, 221, 302, 473, 477, 599, 602, 986], + [0, 1307], + [0, 69, 131, 187, 215, 302, 338, 462, 463, 561, 586], + [0, 242, 304, 357, 361, 373, 690, 734, 836, 930, 934, 940, 957, 1043, 1143], + [0, 244, 278, 383, 539, 929, 1120], + [0, 1, 948, 1140, 1186], + [0, 291, 316, 391, 636, 730, 852, 991], + [0, 69, 157, 187, 215, 301, 302, 405, 462, 481, 582, 586, 760, 979], + [0, 130, 805, 822, 900, 902, 982, 1035, 1115, 1198, 1224], + [0, 78], + [197, 1014], + [5], + [0, 69, 216, 220, 291, 302, 316, 391, 473, 478, 599, 602, 636, 730, 852, 986, 991], + [0, 138, 275, 504, 552, 724, 788, 886, 980, 1136, 1145, 1226], + [0, 52, 118, 232, 261, 276, 282, 299, 335, 336, 368, 420, 479, 494, 511, 512, 577, 675, 736, 758, 787, 954, 993, 1013], + [0, 139, 275, 504, 724, 808, 886], + [0, 218, 370, 514, 744, 945, 1065, 1269], + [0, 218, 370, 227, 744, 427, 570, 1041], + [0, 40, 56, 90, 184, 204, 217, 341, 362, 471, 607, 858, 960, 1032, 1289], + [0, 80, 200, 201, 218, 219, 224, 227, 300, 318, 342, 350, 370, 371, 376, 411, 427, 468, 472, 496, 507, 514, 567, 570, 603, 616, 669, 698, 699, 744, 756, 777, 791, 839, 844, 863, 867, 890, 921, 939, 945, 1005, 1021, 1028, 1036, 1041, 1054, 1063, 1065, 1110, 1118, 1156, 1220, 1237, 1253, 1269, 1299], + [0, 778, 856, 1218, 1307], + [0, 382, 657, 775, 944, 961, 1096, 1284], + [0, 377, 739, 555, 745, 949, 1127, 1284], + [0, 2, 144, 188, 271, 334, 372, 453, 475, 600, 630], + [0, 245, 353, 354, 400, 401, 450, 476, 505, 541, 642, 643, 670, 767, 778, 856, 912, 967, 1074, 1089, 1116, 1122, 1126, 1167, 1197, 1203, 1211, 1218, 1307], + [0, 58, 65, 104, 135, 158, 162, 186, 214, 262, 287, 297, 329, 366, 387, 412, 486, 493, 506, 533, 610, 644, 686, 765, 888], + [0, 58, 65, 104, 135, 158, 76, 84, 88, 109, 124, 297, 329, 366, 189, 412, 289, 493, 298, 308, 367, 389, 535, 451, 535], + [0, 91, 96, 169, 170, 181, 209, 210, 378, 379, 606, 695, 825, 833, 834, 926, 928, 1184], + [0, 409, 631, 738, 916, 1075], + [0, 26, 111, 150, 229, 333], + [0, 250, 458, 532, 680, 876], + [0, 403, 683, 823, 897, 982, 1109, 1219], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1102, 1104, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1307], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 662, 332, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1102, 783, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 962], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 662, 769, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1102, 1042, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1189], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 624, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1061, 1064, 1098, 1099, 1102, 1104, 1199, 1204, 1259, 1261, 1265, 1285, 1286, 1301, 1307], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 433, 584, 587, 399, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 806, 1064, 1098, 1099, 1102, 1104, 1199, 1204, 973, 1261, 1265, 1285, 1286, 1301, 1307], + [0, 6, 7, 10, 13, 19, 112, 113, 127, 160, 171, 260, 280, 332, 364, 364, 433, 584, 587, 621, 627, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1058, 1037, 1060, 1064, 1098, 1099, 1102, 1104, 1255, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1307], + [0, 6, 7, 10, 13, 18, 112, 113, 127, 160, 171, 260, 279, 332, 363, 364, 9, 584, 587, 621, 627, 656, 658, 662, 640, 766, 783, 29, 962, 1017, 1020, 31, 1060, 1064, 1098, 1099, 1102, 1094, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1301, 1271], + [0, 6, 7, 10, 13, 19, 113, 113, 128, 160, 171, 260, 279, 332, 364, 364, 433, 584, 587, 621, 622, 656, 658, 662, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1059, 1098, 1099, 1102, 1104, 1199, 1204, 1257, 1256, 1263, 1285, 1286, 1301, 1307], + [0, 6, 71, 10, 13, 18, 112, 113, 127, 160, 408, 525, 279, 332, 363, 364, 433, 584, 587, 621, 627, 656, 658, 653, 666, 766, 783, 904, 962, 1017, 1020, 1037, 1060, 1064, 1098, 1099, 1095, 1104, 1199, 1204, 1257, 1261, 1265, 1285, 1286, 1280, 1307], + [0, 102, 143, 146, 201, 285, 307, 318, 459, 485, 507, 547, 623, 698, 863, 939, 978, 1110, 1258], + [0, 2, 277, 822, 982, 1115, 1224], + [0, 277, 629, 800, 820, 822, 977, 982, 1068, 1115, 1198, 1206, 1224], + [0, 2, 3, 743, 995, 1031, 1147, 1193, 1307], + [0, 839, 844, 1021, 1054, 1118, 1156, 1220], + [0, 797, 794, 1088, 1018, 1179, 1215, 1208], + [0, 159, 196, 228, 312, 345, 426, 438, 503, 524, 576, 585, 715, 740, 751, 768, 794, 797, 841, 877, 893, 953, 1018, 1047, 1088, 1125, 1144, 1161, 1176, 1179, 1196, 1208, 1215, 1228], + [0, 503, 740, 841, 893, 953, 1125, 1196, 1228], + [0, 794, 1018, 797, 1161, 1208, 1088, 1179, 1215], + [0, 159, 312, 345, 438, 524, 585, 768, 877], + [0, 196, 426, 751, 576, 715, 1047, 1144, 1176], + [0, 43, 81, 103, 149, 322, 346, 422, 573, 615, 898, 1135], + [0, 25, 147, 120, 294, 439, 398, 51, 609, 784, 784, 1105], + [0, 467, 516, 780, 810, 963, 992, 1084, 1142, 1247, 1249, 1300], + [0, 87, 92, 93, 154, 330, 339, 513, 757], + [0, 87, 148, 93, 258, 416, 425, 588, 781], + [0, 865, 1091, 1182, 1245], + [0, 870, 1112, 1190, 1243], + [0, 857, 1076, 1168, 1231], + [0, 256, 753, 865, 880, 883, 998, 1091, 1182, 1245], + [0, 256, 753, 870, 880, 883, 1003, 1112, 1190, 1243], + [0, 256, 753, 857, 880, 883, 990, 1076, 1168, 1246], + [0, 857, 865, 880, 1076, 1091, 1112, 1168, 1182, 1190, 1231, 1243, 1245], + [0, 830, 1063, 1253, 1297], + [0, 397, 703, 915, 1057], + [0, 41, 70, 86, 115, 126, 152, 173, 174, 199, 266, 292, 319, 355, 358, 423, 440, 508, 566, 581, 618], + [0, 218, 370, 669, 744, 1028, 1237, 1299], + [0, 39, 107, 198, 393, 500, 696, 754, 873, 1024], + [0, 39, 107, 198, 393, 500, 306, 754, 396, 569], + [0, 380, 632, 752, 860, 922], + [0, 326, 632, 645, 764, 829], + [0, 467, 516, 780, 810, 963, 992, 1084, 1247, 1249, 1300], + [0, 578, 583, 728, 854, 981, 1093], + [0, 415, 415, 531, 713, 826, 966], + [0, 1, 5, 130, 822, 982, 1115, 1224], + [0, 590, 820, 971, 976, 1035, 1068, 1073, 1139, 1195], + [0, 502, 649, 907, 782, 907, 907, 907, 928, 972], + [0, 21, 27, 35, 63, 67, 68, 69, 132, 166, 167, 176, 177, 187, 216, 230, 231, 257, 263, 264, 267, 268, 269, 277, 284, 313, 315, 411, 441, 444, 447, 456, 527, 528, 536, 538, 545, 553, 554, 611, 617, 626, 652, 655, 659, 666, 688, 722, 753, 777, 850, 861, 866, 871, 874, 882, 892, 894, 902, 946, 947, 1011, 1035, 1066, 1069, 1079, 1085, 1086, 1097, 1107, 1112, 1113, 1114, 1164, 1166, 1174, 1175, 1178, 1190, 1195, 1206, 1227, 1238, 1239, 1240, 1242, 1243, 1260, 1269, 1277, 1282, 1307], + [0, 293, 415, 537, 634, 750], + [0, 519, 574, 612, 691, 755, 790, 803, 837, 868, 895, 917, 989, 999, 1039, 1093, 1137], + [0, 519, 691, 820, 837, 917, 976, 977, 999, 1068, 1093, 1191, 1206], + [0, 517, 548, 571, 697, 733, 807, 862, 905, 984, 1033, 1067, 1108, 1151, 1201], + [0, 483, 619, 725, 817, 936, 1046, 1154], + [0, 69, 155, 213, 216, 220, 221, 291, 316, 391, 392, 464, 473, 477, 599, 602, 636, 685, 730, 843, 852, 885, 986, 991, 1023, 1072, 1177], + [0, 114, 288, 605, 772, 939], + [0, 683, 897, 988, 1053, 1123, 1210], + [0, 908, 1062, 1171, 1250, 1303, 1276], + [0, 683, 820, 897, 977, 988, 1053, 1068, 1123, 1206, 1210], + [0, 770, 1241], + [0, 664, 762, 935, 1045], + [0, 237, 407], + [0, 510, 522, 663, 881, 959, 1103, 1272, 1305], + [0, 101, 137, 375, 1291], + [0, 119, 305, 735, 1029, 1192], + [0, 274, 487, 687, 964, 1153, 1209, 1234, 1236, 1268], + [0, 546, 550, 891, 1082], + [0, 80, 342, 376, 472, 567, 699, 791, 867, 1209, 1236, 1268], + [0, 546, 639, 651, 884, 891], + [0, 550, 639, 891, 1082], + [0, 664, 762, 889, 1045], + [0, 134, 365, 460, 546, 651, 891], + [0, 762, 1266], + [0, 5, 521, 646, 762, 789, 925, 941, 1045, 1104, 1266, 1307], + [0, 546, 639, 891], + [0, 91, 96, 170, 210, 379, 550, 639, 651, 695, 891, 1184], + [0, 205, 344, 369, 452, 501, 549, 550, 639, 648, 651, 774, 891, 918], + [], + [0, 1, 4, 45, 46, 241, 246, 317, 386, 388, 394, 461, 563, 564, 565, 580, 677, 689, 796, 802, 816, 824, 831, 845, 853, 955, 965, 985, 1080, 1081, 1213], + [0, 1, 4, 37, 38, 165, 269, 390, 445, 568, 579, 628, 647, 667, 678, 692, 702, 726, 737, 749, 832, 875, 942, 996, 1000], + [675, 758, 774, 801, 879, 1013, 1022, 1183, 1217, 1262], + [1092, 1170, 809, 1170, 1235, 1270, 1022, 1141, 1187, 1230], + [909, 1025, 1026, 1035, 1101, 1139, 1165, 1195, 1198], + [687, 1008, 1087, 1153, 1214], + [55, 72, 225, 359, 523, 679, 834, 928, 1012], + [270, 321, 381, 446, 457, 598, 613, 633, 665, 693, 701, 792, 811, 818, 869, 943], + [549, 550, 572, 639, 648, 650, 773, 774, 891, 918, 1130, 1183, 1217, 1262], + [549, 550, 572, 639, 648, 650, 773, 774, 891, 918, 1130, 1141, 1187, 1230], + [549, 550, 572, 639, 648, 650, 773, 774, 891, 918, 1026, 1026, 1101, 1165], + [544, 550, 572, 639, 620, 650, 773, 727, 891, 813, 673, 673, 834, 928], + [549, 572, 648, 650, 773, 774, 918, 937, 1015, 1051, 1117, 1130, 1163, 1183, 1207, 1217, 1262], + [549, 572, 648, 650, 773, 774, 918, 937, 1015, 1051, 1117, 1141, 1163, 1141, 1207, 1187, 1230], + [549, 572, 648, 650, 773, 774, 918, 937, 1015, 1051, 1117, 1026, 1163, 1026, 1207, 1101, 1165], + [0, 2, 675, 758, 774, 801, 879, 1013], + [977, 1015, 1035, 1051, 1117, 1139, 1163, 1195, 1198], + [277, 424, 436, 437, 442, 469, 474, 531, 606, 706, 707, 708, 709, 710, 711, 712, 713, 782, 793, 799, 804, 812, 815, 819, 821, 825, 826, 834, 901, 911, 914, 919, 926, 928, 1006], + [0, 431, 549, 591, 675, 718, 758, 864, 879, 958, 1013, 1019], + [0, 431, 549, 924, 1092, 718, 1170, 864, 1235, 958, 1270, 1019], + [0, 431, 549, 920, 1035, 718, 1195, 864, 1139, 958, 1307, 1019], + [0, 5, 140, 226, 415, 531, 537, 606, 713, 826, 966] ]); export const emptySprite = sprites[0]; @@ -1585,106 +1585,106 @@ export const fontSmallSupporter2Palette = palettes[8]; export const fontSmallSupporter3Palette = palettes[9]; export const spriteSheets: SpriteSheet[] = [ - { - src: 'images/pony.png', - data: undefined, texture: undefined, sprites: sprites, palette: false - }, - { - src: 'images/pony2.png', srcA: 'images/pony2a.png', - data: undefined, texture: undefined, sprites: sprites2, palette: true - }, + { + src: 'images/pony.png', + data: undefined, texture: undefined, sprites: sprites, palette: false + }, + { + src: 'images/pony2.png', srcA: 'images/pony2a.png', + data: undefined, texture: undefined, sprites: sprites2, palette: true + }, ]; export const normalSpriteSheet = spriteSheets[0]; export const paletteSpriteSheet = spriteSheets[1]; export function createSprites(data: string): Sprite[] { - const sprites: Sprite[] = [ - { x: 0, y: 0, w: 0, h: 0, ox: 0, oy: 0, type: 0 }, - ]; + const sprites: Sprite[] = [ + { x: 0, y: 0, w: 0, h: 0, ox: 0, oy: 0, type: 0 }, + ]; - let offset = 0; - const read = bitReaderCustom(() => { - const value = parseInt(data.substr(offset, 2), 16); - offset += 2; - return value; - }); + let offset = 0; + const read = bitReaderCustom(() => { + const value = parseInt(data.substr(offset, 2), 16); + offset += 2; + return value; + }); - while (offset < data.length) { - sprites.push({ - x: read(12), - y: read(12), - w: read(9), - h: read(9), - ox: read(8), - oy: read(8), - type: read(6), - }); - } + while (offset < data.length) { + sprites.push({ + x: read(12), + y: read(12), + w: read(9), + h: read(9), + ox: read(8), + oy: read(8), + type: read(6), + }); + } - return sprites; + return sprites; } export function createFont(sprites: Sprite[], groups: [number, number[]][]) { - const chars: { code: number; sprite: Sprite; }[] = []; + const chars: { code: number; sprite: Sprite; }[] = []; - for (const [start, codes] of groups) { - for (let i = 0; i < codes.length; i++) { - if (codes[i]) { - chars.push({ code: start + i, sprite: sprites[codes[i]] }); - } - } - } + for (const [start, codes] of groups) { + for (let i = 0; i < codes.length; i++) { + if (codes[i]) { + chars.push({ code: start + i, sprite: sprites[codes[i]] }); + } + } + } - return chars; + return chars; } export function createButton( - border: number, topLeft: number, top: number, topRight: number, left: number, bg: number, right: number, - bottomLeft: number, bottom: number, bottomRight: number + border: number, topLeft: number, top: number, topRight: number, left: number, bg: number, right: number, + bottomLeft: number, bottom: number, bottomRight: number ): SpriteBorder { - return { - border, - topLeft: sprites[topLeft], - top: sprites[top], - topRight: sprites[topRight], - left: sprites[left], - bg: sprites[bg], - right: sprites[right], - bottomLeft: sprites[bottomLeft], - bottom: sprites[bottom], - bottomRight: sprites[bottomRight] - }; + return { + border, + topLeft: sprites[topLeft], + top: sprites[top], + topRight: sprites[topRight], + left: sprites[left], + bg: sprites[bg], + right: sprites[right], + bottomLeft: sprites[bottomLeft], + bottom: sprites[bottom], + bottomRight: sprites[bottomRight] + }; } export function mapSprites(frames: number[]) { - return frames.map(i => sprites[i]); + return frames.map(i => sprites[i]); } export function mapSprites2(frames: number[]) { - return frames.map(i => sprites2[i]); + return frames.map(i => sprites2[i]); } export function createPalettes(colorsString: string, palettes: number[][]): Uint32Array[] { - const colors = colorsString.split(/ /g).map(parseSpriteColor); + const colors = colorsString.split(/ /g).map(parseSpriteColor); - return palettes.map(palette => { - const result = new Uint32Array(palette.length); + return palettes.map(palette => { + const result = new Uint32Array(palette.length); - for (let i = 0; i < palette.length; i++) { - result[i] = colors[palette[i]] >>> 0; - } + for (let i = 0; i < palette.length; i++) { + result[i] = colors[palette[i]] >>> 0; + } - return result; - }); + return result; + }); } export function createColorPal(color: number, colors: number): ColorExtra { - return { color: sprites2[color], colors }; + return { color: sprites2[color], colors }; } export function colorPal(colors: number) { - return (color: number) => createColorPal(color, colors); + return (color: number) => createColorPal(color, colors); } const colorPal3 = colorPal(3); @@ -1696,58 +1696,58 @@ const colorPal13 = colorPal(13); const colorPal17 = colorPal(17); export function getPalette(index: number) { - return palettes[index]; + return palettes[index]; } const emptyPalette = new Uint32Array(0); export function emptyColorPalette(): ColorExtra { - return { color: sprites2[0], palettes: [emptyPalette] }; + return { color: sprites2[0], palettes: [emptyPalette] }; } export function createSpritesPalette(sprites: number[], paletteIndexes: number[]): TileSprites { - return { sprites: sprites.map(i => sprites2[i]), palettes: paletteIndexes.map(getPalette) }; + return { sprites: sprites.map(i => sprites2[i]), palettes: paletteIndexes.map(getPalette) }; } export function createColorPalette(color: number, paletteIndexes: number[]): ColorExtra { - return { color: sprites2[color], palettes: paletteIndexes.map(getPalette) }; + return { color: sprites2[color], palettes: paletteIndexes.map(getPalette) }; } export function createColorExtraPal(color: number, colors: number, extra: number, paletteIndexes: number[]): ColorExtra { - return { color: sprites2[color], colors, extra: sprites2[extra], palettes: paletteIndexes.map(getPalette) }; + return { color: sprites2[color], colors, extra: sprites2[extra], palettes: paletteIndexes.map(getPalette) }; } export function createShadow(shadow: number): Shadow { - return { shadow: sprites2[shadow] }; + return { shadow: sprites2[shadow] }; } export function createColorShadowPalette(color: number, shadow: number, paletteIndexes: number[]): ColorShadow { - return { color: sprites2[color], shadow: sprites2[shadow], palettes: paletteIndexes.map(getPalette) }; + return { color: sprites2[color], shadow: sprites2[shadow], palettes: paletteIndexes.map(getPalette) }; } export function createNose(color: number, colors: number, mouth: number, fangs: number) { - return { color: sprites2[color], colors, mouth: sprites2[mouth], fangs: sprites2[fangs] }; + return { color: sprites2[color], colors, mouth: sprites2[mouth], fangs: sprites2[fangs] }; } export function createEye(base: number, irises: number[], shadow?: number, shine?: number): PonyEye { - return { base: sprites2[base], irises: mapSprites2(irises), shadow: sprites2[shadow || 0], shine: sprites2[shine || 0] }; + return { base: sprites2[base], irises: mapSprites2(irises), shadow: sprites2[shadow || 0], shine: sprites2[shine || 0] }; } export function createAnimation(frames: number[]) { - return { frames: mapSprites(frames) }; + return { frames: mapSprites(frames) }; } export function createAnimationPalette(frames: number[], palette: number) { - return { frames: mapSprites2(frames), palette: getPalette(palette) }; + return { frames: mapSprites2(frames), palette: getPalette(palette) }; } export function createAnimationShadow(frames: number[], shadow: number, palette: number) { - return { frames: mapSprites2(frames), shadow: sprites2[shadow], palette: getPalette(palette) }; + return { frames: mapSprites2(frames), shadow: sprites2[shadow], palette: getPalette(palette) }; } export { - Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, - colorPal3, colorPal5, colorPal7, colorPal9, colorPal11, colorPal13, colorPal17 + Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, + colorPal3, colorPal5, colorPal7, colorPal9, colorPal11, colorPal13, colorPal17 }; export const bubble = createButton(2, 1, 2, 3, 4, 5, 6, 7, 8, 9); export const bubble2 = createButton(2, 10, 11, 12, 13, 14, 15, 16, 17, 18); @@ -1790,1101 +1790,1101 @@ export const light_crystals_held = sprites[2054]; export const light_crystal_lantern = sprites[2055]; export const pixel = sprites[2056]; export const eyeRight: PonyEyes = [ - undefined, - [createEye(2, [3, 4, 5, 6, 7, 8, 9, 10], 1, 11), createEye(13, [14, 15, 16, 17, 18, 19, 20, 21], 12, 22), createEye(24, [25, 26, 27, 28, 29, 30, 31, 32], 23, 33), createEye(35, [36, 37, 38, 39, 40, 41, 42, 43], 34, 44)], - [createEye(45, [46, 47, 48, 49, 50, 51, 52, 53], 54, 55), createEye(56, [57, 58, 59, 60, 61, 62, 63, 64], 65, 66), createEye(67, [68, 69, 70, 71, 72, 73, 74, 75], 76, 77), createEye(78, [79, 80, 81, 82, 83, 84, 85, 86], 87, 88)], - [createEye(89, [90, 91, 92, 93, 94, 95, 96, 97], 98, 99), createEye(100, [101, 102, 103, 104, 105, 106, 107, 108], 109, 110), createEye(111, [112, 113, 114, 115, 116, 117, 118, 119], 120, 121), createEye(122, [123, 124, 125, 126, 127, 128, 129, 130], 131, 132)], - [createEye(133, [134, 0, 135, 136, 0, 0, 0, 137], 138, 139), createEye(140, [141, 0, 142, 143, 0, 0, 0, 144], 145, 146), createEye(147, [148, 0, 149, 150, 0, 0, 0, 151], 152, 153), createEye(154, [155, 0, 156, 157, 0, 0, 0, 158], 159, 160)], - [createEye(161, [0, 0, 0, 0, 0, 0, 0, 0], 162, 163), createEye(164, [0, 0, 0, 0, 0, 0, 0, 0], 165, 166), createEye(167, [0, 0, 0, 0, 0, 0, 0, 0], 168, 169), createEye(170, [0, 0, 0, 0, 0, 0, 0, 0], 171, 172)], - [createEye(173, [0, 0, 0, 0, 0, 0, 0, 0], 174, 175), createEye(176, [0, 0, 0, 0, 0, 0, 0, 0], 177, 178), createEye(179, [0, 0, 0, 0, 0, 0, 0, 0], 180, 181), createEye(182, [0, 0, 0, 0, 0, 0, 0, 0], 183, 184)], - [createEye(185, [186, 187, 188, 189, 190, 191, 192, 193], 194, 195), createEye(196, [197, 198, 199, 200, 201, 202, 203, 204], 205, 206), createEye(207, [208, 209, 210, 211, 212, 213, 214, 215], 216, 217), createEye(218, [219, 220, 221, 222, 223, 224, 225, 226], 227, 228)], - [createEye(229, [230, 231, 232, 233, 234, 235, 236, 237], 238, 239), createEye(240, [241, 242, 243, 244, 245, 246, 247, 248], 249, 250), createEye(251, [252, 253, 254, 255, 256, 257, 258, 259], 260, 261), createEye(262, [263, 264, 265, 266, 267, 268, 269, 270], 271, 272)], - [createEye(273, [274, 0, 275, 276, 0, 0, 0, 277], 278, 279), createEye(280, [281, 0, 282, 283, 0, 0, 0, 284], 285, 286), createEye(287, [288, 0, 289, 290, 0, 0, 0, 291], 292, 293), createEye(294, [295, 0, 296, 297, 0, 0, 0, 298], 299, 300)], - [createEye(301, [0, 0, 0, 0, 0, 0, 0, 0], 302, 303), createEye(304, [0, 0, 0, 0, 0, 0, 0, 0], 305, 306), createEye(307, [0, 0, 0, 0, 0, 0, 0, 0], 308, 309), createEye(310, [0, 0, 0, 0, 0, 0, 0, 0], 311, 312)], - [createEye(314, [0, 0, 0, 0, 0, 0, 0, 0], 313, 315), createEye(317, [0, 0, 0, 0, 0, 0, 0, 0], 316, 318), createEye(320, [0, 0, 0, 0, 0, 0, 0, 0], 319, 321), createEye(323, [0, 0, 0, 0, 0, 0, 0, 0], 322, 324)], - [createEye(326, [0, 0, 0, 0, 0, 0, 0, 0], 325, 327), createEye(329, [0, 0, 0, 0, 0, 0, 0, 0], 328, 330), createEye(332, [0, 0, 0, 0, 0, 0, 0, 0], 331, 333), createEye(335, [0, 0, 0, 0, 0, 0, 0, 0], 334, 336)], - [createEye(338, [0, 0, 0, 0, 0, 0, 0, 0], 337, 339), createEye(341, [0, 0, 0, 0, 0, 0, 0, 0], 340, 342), createEye(344, [0, 0, 0, 0, 0, 0, 0, 0], 343, 345), createEye(347, [0, 0, 0, 0, 0, 0, 0, 0], 346, 348)], - [createEye(350, [0, 0, 0, 0, 0, 0, 0, 0], 349, 351), createEye(353, [0, 0, 0, 0, 0, 0, 0, 0], 352, 354), createEye(356, [0, 0, 0, 0, 0, 0, 0, 0], 355, 357), createEye(359, [0, 0, 0, 0, 0, 0, 0, 0], 358, 360)], - [createEye(361, [362, 363, 364, 365, 366, 367, 368, 369], 370, 371), createEye(372, [373, 374, 375, 376, 377, 378, 379, 380], 381, 382), createEye(383, [384, 385, 386, 387, 388, 389, 390, 391], 392, 393), createEye(394, [395, 396, 397, 398, 399, 400, 401, 402], 403, 404)], - [createEye(405, [406, 407, 408, 409, 410, 411, 412, 413], 414, 415), createEye(416, [417, 418, 419, 420, 421, 422, 423, 424], 425, 426), createEye(427, [428, 429, 430, 431, 432, 433, 434, 435], 436, 437), createEye(438, [439, 440, 441, 442, 443, 444, 445, 446], 447, 448)], - [createEye(449, [450, 451, 452, 453, 454, 0, 455, 456], 457, 458), createEye(459, [460, 461, 462, 463, 464, 0, 465, 466], 467, 468), createEye(469, [470, 471, 472, 473, 474, 0, 475, 476], 477, 478), createEye(479, [480, 481, 482, 483, 484, 0, 485, 486], 487, 488)], - [createEye(489, [490, 0, 491, 492, 0, 0, 0, 493], 494, 495), createEye(496, [497, 0, 498, 499, 0, 0, 0, 500], 501, 502), createEye(503, [504, 0, 505, 506, 0, 0, 0, 507], 508, 509), createEye(510, [511, 0, 512, 513, 0, 0, 0, 514], 515, 516)], - [createEye(517, [518, 519, 520, 521, 522, 523, 524, 525], 526, 527), createEye(528, [529, 530, 531, 532, 533, 534, 535, 536], 537, 538), createEye(539, [540, 541, 542, 543, 544, 545, 546, 547], 548, 549), createEye(550, [551, 552, 553, 554, 555, 556, 557, 558], 559, 560)], - [createEye(561, [562, 0, 563, 564, 0, 565, 0, 566], 567, 568), createEye(569, [570, 0, 571, 572, 0, 573, 0, 574], 575, 576), createEye(577, [578, 0, 579, 580, 0, 581, 0, 582], 583, 584), createEye(585, [586, 0, 587, 588, 0, 589, 0, 590], 591, 592)], - [createEye(594, [0, 0, 0, 0, 0, 0, 0, 0], 593, 595), createEye(597, [0, 0, 0, 0, 0, 0, 0, 0], 596, 598), createEye(600, [0, 0, 0, 0, 0, 0, 0, 0], 599, 601), createEye(603, [0, 0, 0, 0, 0, 0, 0, 0], 602, 604)], - [createEye(606, [0, 0, 0, 0, 0, 0, 0, 0], 605, 0), createEye(608, [0, 0, 0, 0, 0, 0, 0, 0], 607, 0), createEye(610, [0, 0, 0, 0, 0, 0, 0, 0], 609, 0), createEye(612, [0, 0, 0, 0, 0, 0, 0, 0], 611, 0)], - [createEye(614, [0, 0, 0, 0, 0, 0, 0, 0], 613, 0), createEye(616, [0, 0, 0, 0, 0, 0, 0, 0], 615, 0), createEye(618, [0, 0, 0, 0, 0, 0, 0, 0], 617, 0), createEye(620, [0, 0, 0, 0, 0, 0, 0, 0], 619, 0)], - [createEye(622, [0, 0, 0, 0, 0, 0, 0, 0], 621, 0), createEye(624, [0, 0, 0, 0, 0, 0, 0, 0], 623, 0), createEye(626, [0, 0, 0, 0, 0, 0, 0, 0], 625, 0), createEye(628, [0, 0, 0, 0, 0, 0, 0, 0], 627, 0)] + undefined, + [createEye(2, [3, 4, 5, 6, 7, 8, 9, 10], 1, 11), createEye(13, [14, 15, 16, 17, 18, 19, 20, 21], 12, 22), createEye(24, [25, 26, 27, 28, 29, 30, 31, 32], 23, 33), createEye(35, [36, 37, 38, 39, 40, 41, 42, 43], 34, 44)], + [createEye(45, [46, 47, 48, 49, 50, 51, 52, 53], 54, 55), createEye(56, [57, 58, 59, 60, 61, 62, 63, 64], 65, 66), createEye(67, [68, 69, 70, 71, 72, 73, 74, 75], 76, 77), createEye(78, [79, 80, 81, 82, 83, 84, 85, 86], 87, 88)], + [createEye(89, [90, 91, 92, 93, 94, 95, 96, 97], 98, 99), createEye(100, [101, 102, 103, 104, 105, 106, 107, 108], 109, 110), createEye(111, [112, 113, 114, 115, 116, 117, 118, 119], 120, 121), createEye(122, [123, 124, 125, 126, 127, 128, 129, 130], 131, 132)], + [createEye(133, [134, 0, 135, 136, 0, 0, 0, 137], 138, 139), createEye(140, [141, 0, 142, 143, 0, 0, 0, 144], 145, 146), createEye(147, [148, 0, 149, 150, 0, 0, 0, 151], 152, 153), createEye(154, [155, 0, 156, 157, 0, 0, 0, 158], 159, 160)], + [createEye(161, [0, 0, 0, 0, 0, 0, 0, 0], 162, 163), createEye(164, [0, 0, 0, 0, 0, 0, 0, 0], 165, 166), createEye(167, [0, 0, 0, 0, 0, 0, 0, 0], 168, 169), createEye(170, [0, 0, 0, 0, 0, 0, 0, 0], 171, 172)], + [createEye(173, [0, 0, 0, 0, 0, 0, 0, 0], 174, 175), createEye(176, [0, 0, 0, 0, 0, 0, 0, 0], 177, 178), createEye(179, [0, 0, 0, 0, 0, 0, 0, 0], 180, 181), createEye(182, [0, 0, 0, 0, 0, 0, 0, 0], 183, 184)], + [createEye(185, [186, 187, 188, 189, 190, 191, 192, 193], 194, 195), createEye(196, [197, 198, 199, 200, 201, 202, 203, 204], 205, 206), createEye(207, [208, 209, 210, 211, 212, 213, 214, 215], 216, 217), createEye(218, [219, 220, 221, 222, 223, 224, 225, 226], 227, 228)], + [createEye(229, [230, 231, 232, 233, 234, 235, 236, 237], 238, 239), createEye(240, [241, 242, 243, 244, 245, 246, 247, 248], 249, 250), createEye(251, [252, 253, 254, 255, 256, 257, 258, 259], 260, 261), createEye(262, [263, 264, 265, 266, 267, 268, 269, 270], 271, 272)], + [createEye(273, [274, 0, 275, 276, 0, 0, 0, 277], 278, 279), createEye(280, [281, 0, 282, 283, 0, 0, 0, 284], 285, 286), createEye(287, [288, 0, 289, 290, 0, 0, 0, 291], 292, 293), createEye(294, [295, 0, 296, 297, 0, 0, 0, 298], 299, 300)], + [createEye(301, [0, 0, 0, 0, 0, 0, 0, 0], 302, 303), createEye(304, [0, 0, 0, 0, 0, 0, 0, 0], 305, 306), createEye(307, [0, 0, 0, 0, 0, 0, 0, 0], 308, 309), createEye(310, [0, 0, 0, 0, 0, 0, 0, 0], 311, 312)], + [createEye(314, [0, 0, 0, 0, 0, 0, 0, 0], 313, 315), createEye(317, [0, 0, 0, 0, 0, 0, 0, 0], 316, 318), createEye(320, [0, 0, 0, 0, 0, 0, 0, 0], 319, 321), createEye(323, [0, 0, 0, 0, 0, 0, 0, 0], 322, 324)], + [createEye(326, [0, 0, 0, 0, 0, 0, 0, 0], 325, 327), createEye(329, [0, 0, 0, 0, 0, 0, 0, 0], 328, 330), createEye(332, [0, 0, 0, 0, 0, 0, 0, 0], 331, 333), createEye(335, [0, 0, 0, 0, 0, 0, 0, 0], 334, 336)], + [createEye(338, [0, 0, 0, 0, 0, 0, 0, 0], 337, 339), createEye(341, [0, 0, 0, 0, 0, 0, 0, 0], 340, 342), createEye(344, [0, 0, 0, 0, 0, 0, 0, 0], 343, 345), createEye(347, [0, 0, 0, 0, 0, 0, 0, 0], 346, 348)], + [createEye(350, [0, 0, 0, 0, 0, 0, 0, 0], 349, 351), createEye(353, [0, 0, 0, 0, 0, 0, 0, 0], 352, 354), createEye(356, [0, 0, 0, 0, 0, 0, 0, 0], 355, 357), createEye(359, [0, 0, 0, 0, 0, 0, 0, 0], 358, 360)], + [createEye(361, [362, 363, 364, 365, 366, 367, 368, 369], 370, 371), createEye(372, [373, 374, 375, 376, 377, 378, 379, 380], 381, 382), createEye(383, [384, 385, 386, 387, 388, 389, 390, 391], 392, 393), createEye(394, [395, 396, 397, 398, 399, 400, 401, 402], 403, 404)], + [createEye(405, [406, 407, 408, 409, 410, 411, 412, 413], 414, 415), createEye(416, [417, 418, 419, 420, 421, 422, 423, 424], 425, 426), createEye(427, [428, 429, 430, 431, 432, 433, 434, 435], 436, 437), createEye(438, [439, 440, 441, 442, 443, 444, 445, 446], 447, 448)], + [createEye(449, [450, 451, 452, 453, 454, 0, 455, 456], 457, 458), createEye(459, [460, 461, 462, 463, 464, 0, 465, 466], 467, 468), createEye(469, [470, 471, 472, 473, 474, 0, 475, 476], 477, 478), createEye(479, [480, 481, 482, 483, 484, 0, 485, 486], 487, 488)], + [createEye(489, [490, 0, 491, 492, 0, 0, 0, 493], 494, 495), createEye(496, [497, 0, 498, 499, 0, 0, 0, 500], 501, 502), createEye(503, [504, 0, 505, 506, 0, 0, 0, 507], 508, 509), createEye(510, [511, 0, 512, 513, 0, 0, 0, 514], 515, 516)], + [createEye(517, [518, 519, 520, 521, 522, 523, 524, 525], 526, 527), createEye(528, [529, 530, 531, 532, 533, 534, 535, 536], 537, 538), createEye(539, [540, 541, 542, 543, 544, 545, 546, 547], 548, 549), createEye(550, [551, 552, 553, 554, 555, 556, 557, 558], 559, 560)], + [createEye(561, [562, 0, 563, 564, 0, 565, 0, 566], 567, 568), createEye(569, [570, 0, 571, 572, 0, 573, 0, 574], 575, 576), createEye(577, [578, 0, 579, 580, 0, 581, 0, 582], 583, 584), createEye(585, [586, 0, 587, 588, 0, 589, 0, 590], 591, 592)], + [createEye(594, [0, 0, 0, 0, 0, 0, 0, 0], 593, 595), createEye(597, [0, 0, 0, 0, 0, 0, 0, 0], 596, 598), createEye(600, [0, 0, 0, 0, 0, 0, 0, 0], 599, 601), createEye(603, [0, 0, 0, 0, 0, 0, 0, 0], 602, 604)], + [createEye(606, [0, 0, 0, 0, 0, 0, 0, 0], 605, 0), createEye(608, [0, 0, 0, 0, 0, 0, 0, 0], 607, 0), createEye(610, [0, 0, 0, 0, 0, 0, 0, 0], 609, 0), createEye(612, [0, 0, 0, 0, 0, 0, 0, 0], 611, 0)], + [createEye(614, [0, 0, 0, 0, 0, 0, 0, 0], 613, 0), createEye(616, [0, 0, 0, 0, 0, 0, 0, 0], 615, 0), createEye(618, [0, 0, 0, 0, 0, 0, 0, 0], 617, 0), createEye(620, [0, 0, 0, 0, 0, 0, 0, 0], 619, 0)], + [createEye(622, [0, 0, 0, 0, 0, 0, 0, 0], 621, 0), createEye(624, [0, 0, 0, 0, 0, 0, 0, 0], 623, 0), createEye(626, [0, 0, 0, 0, 0, 0, 0, 0], 625, 0), createEye(628, [0, 0, 0, 0, 0, 0, 0, 0], 627, 0)] ]; export const eyeLeft: PonyEyes = [ - undefined, - [createEye(630, [631, 632, 633, 634, 635, 636, 637, 638], 629, 639), createEye(641, [642, 643, 644, 645, 646, 647, 648, 649], 640, 650), createEye(652, [653, 654, 655, 656, 657, 658, 659, 660], 651, 661), createEye(663, [664, 665, 666, 667, 668, 669, 670, 671], 662, 672)], - [createEye(673, [674, 675, 676, 677, 678, 679, 680, 681], 682, 683), createEye(684, [685, 686, 687, 688, 689, 690, 691, 692], 693, 694), createEye(695, [696, 697, 698, 699, 700, 701, 702, 703], 704, 705), createEye(706, [707, 708, 709, 710, 711, 712, 713, 714], 715, 716)], - [createEye(717, [718, 719, 720, 721, 722, 723, 724, 725], 726, 727), createEye(728, [729, 730, 731, 732, 733, 734, 735, 736], 737, 738), createEye(739, [740, 741, 742, 743, 744, 745, 746, 747], 748, 749), createEye(750, [751, 752, 753, 754, 755, 756, 757, 758], 759, 760)], - [createEye(761, [762, 0, 763, 764, 0, 0, 0, 765], 766, 767), createEye(768, [769, 0, 770, 771, 0, 0, 0, 772], 773, 774), createEye(775, [776, 0, 777, 778, 0, 0, 0, 779], 780, 781), createEye(782, [783, 0, 784, 785, 0, 0, 0, 786], 787, 788)], - [createEye(789, [0, 0, 0, 0, 0, 0, 0, 0], 790, 791), createEye(792, [0, 0, 0, 0, 0, 0, 0, 0], 793, 794), createEye(795, [0, 0, 0, 0, 0, 0, 0, 0], 796, 797), createEye(798, [0, 0, 0, 0, 0, 0, 0, 0], 799, 800)], - [createEye(801, [0, 0, 0, 0, 0, 0, 0, 0], 802, 803), createEye(804, [0, 0, 0, 0, 0, 0, 0, 0], 805, 806), createEye(807, [0, 0, 0, 0, 0, 0, 0, 0], 808, 809), createEye(810, [0, 0, 0, 0, 0, 0, 0, 0], 811, 812)], - [createEye(813, [814, 815, 816, 817, 818, 819, 820, 821], 822, 823), createEye(824, [825, 826, 827, 828, 829, 830, 831, 832], 833, 834), createEye(835, [836, 837, 838, 839, 840, 841, 842, 843], 844, 845), createEye(846, [847, 848, 849, 850, 851, 852, 853, 854], 855, 856)], - [createEye(857, [858, 859, 860, 861, 862, 863, 864, 865], 866, 867), createEye(868, [869, 870, 871, 872, 873, 874, 875, 876], 877, 878), createEye(879, [880, 881, 882, 883, 884, 885, 886, 887], 888, 889), createEye(890, [891, 892, 893, 894, 895, 896, 897, 898], 899, 900)], - [createEye(901, [902, 0, 903, 904, 0, 0, 0, 905], 906, 907), createEye(908, [909, 0, 910, 911, 0, 0, 0, 912], 913, 914), createEye(915, [916, 0, 917, 918, 0, 0, 0, 919], 920, 921), createEye(922, [923, 0, 924, 925, 0, 0, 0, 926], 927, 928)], - [createEye(929, [0, 0, 0, 0, 0, 0, 0, 0], 930, 931), createEye(932, [0, 0, 0, 0, 0, 0, 0, 0], 933, 934), createEye(935, [0, 0, 0, 0, 0, 0, 0, 0], 936, 937), createEye(938, [0, 0, 0, 0, 0, 0, 0, 0], 939, 940)], - [createEye(942, [0, 0, 0, 0, 0, 0, 0, 0], 941, 943), createEye(945, [0, 0, 0, 0, 0, 0, 0, 0], 944, 946), createEye(948, [0, 0, 0, 0, 0, 0, 0, 0], 947, 949), createEye(951, [0, 0, 0, 0, 0, 0, 0, 0], 950, 952)], - [createEye(954, [0, 0, 0, 0, 0, 0, 0, 0], 953, 955), createEye(957, [0, 0, 0, 0, 0, 0, 0, 0], 956, 958), createEye(960, [0, 0, 0, 0, 0, 0, 0, 0], 959, 961), createEye(963, [0, 0, 0, 0, 0, 0, 0, 0], 962, 964)], - [createEye(966, [0, 0, 0, 0, 0, 0, 0, 0], 965, 967), createEye(969, [0, 0, 0, 0, 0, 0, 0, 0], 968, 970), createEye(972, [0, 0, 0, 0, 0, 0, 0, 0], 971, 973), createEye(975, [0, 0, 0, 0, 0, 0, 0, 0], 974, 976)], - [createEye(978, [0, 0, 0, 0, 0, 0, 0, 0], 977, 979), createEye(981, [0, 0, 0, 0, 0, 0, 0, 0], 980, 982), createEye(984, [0, 0, 0, 0, 0, 0, 0, 0], 983, 985), createEye(987, [0, 0, 0, 0, 0, 0, 0, 0], 986, 988)], - [createEye(989, [990, 991, 992, 993, 994, 995, 996, 997], 998, 999), createEye(1000, [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008], 1009, 1010), createEye(1011, [1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019], 1020, 1021), createEye(1022, [1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030], 1031, 1032)], - [createEye(1033, [1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041], 1042, 1043), createEye(1044, [1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052], 1053, 1054), createEye(1055, [1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063], 1064, 1065), createEye(1066, [1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074], 1075, 1076)], - [createEye(1077, [1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085], 1086, 1087), createEye(1088, [1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096], 1097, 1098), createEye(1099, [1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107], 1108, 1109), createEye(1110, [1111, 1112, 1113, 1114, 1115, 1116, 1117, 1118], 1119, 1120)], - [createEye(1121, [1122, 0, 1123, 1124, 0, 0, 0, 1125], 1126, 1127), createEye(1128, [1129, 0, 1130, 1131, 0, 0, 0, 1132], 1133, 1134), createEye(1135, [1136, 0, 1137, 1138, 0, 0, 0, 1139], 1140, 1141), createEye(1142, [1143, 0, 1144, 1145, 0, 0, 0, 1146], 1147, 1148)], - [createEye(1149, [1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157], 1158, 1159), createEye(1160, [1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168], 1169, 1170), createEye(1171, [1172, 1173, 1174, 1175, 1176, 1177, 1178, 1179], 1180, 1181), createEye(1182, [1183, 1184, 1185, 1186, 1187, 1188, 1189, 1190], 1191, 1192)], - [createEye(1193, [1194, 1195, 1196, 1197, 1198, 1199, 0, 1200], 1201, 1202), createEye(1203, [1204, 1205, 1206, 1207, 1208, 1209, 0, 1210], 1211, 1212), createEye(1213, [1214, 1215, 1216, 1217, 1218, 1219, 0, 1220], 1221, 1222), createEye(1223, [1224, 1225, 1226, 1227, 1228, 1229, 0, 1230], 1231, 1232)], - [createEye(1234, [0, 0, 0, 0, 0, 0, 0, 0], 1233, 1235), createEye(1237, [0, 0, 0, 0, 0, 0, 0, 0], 1236, 1238), createEye(1240, [0, 0, 0, 0, 0, 0, 0, 0], 1239, 1241), createEye(1243, [0, 0, 0, 0, 0, 0, 0, 0], 1242, 1244)], - [createEye(1246, [0, 0, 0, 0, 0, 0, 0, 0], 1245, 0), createEye(1248, [0, 0, 0, 0, 0, 0, 0, 0], 1247, 0), createEye(1250, [0, 0, 0, 0, 0, 0, 0, 0], 1249, 0), createEye(1252, [0, 0, 0, 0, 0, 0, 0, 0], 1251, 0)], - [createEye(1254, [0, 0, 0, 0, 0, 0, 0, 0], 1253, 1255), createEye(1257, [0, 0, 0, 0, 0, 0, 0, 0], 1256, 1258), createEye(1260, [0, 0, 0, 0, 0, 0, 0, 0], 1259, 1261), createEye(1263, [0, 0, 0, 0, 0, 0, 0, 0], 1262, 1264)], - [createEye(1266, [0, 0, 0, 0, 0, 0, 0, 0], 1265, 0), createEye(1268, [0, 0, 0, 0, 0, 0, 0, 0], 1267, 0), createEye(1270, [0, 0, 0, 0, 0, 0, 0, 0], 1269, 0), createEye(1272, [0, 0, 0, 0, 0, 0, 0, 0], 1271, 0)] + undefined, + [createEye(630, [631, 632, 633, 634, 635, 636, 637, 638], 629, 639), createEye(641, [642, 643, 644, 645, 646, 647, 648, 649], 640, 650), createEye(652, [653, 654, 655, 656, 657, 658, 659, 660], 651, 661), createEye(663, [664, 665, 666, 667, 668, 669, 670, 671], 662, 672)], + [createEye(673, [674, 675, 676, 677, 678, 679, 680, 681], 682, 683), createEye(684, [685, 686, 687, 688, 689, 690, 691, 692], 693, 694), createEye(695, [696, 697, 698, 699, 700, 701, 702, 703], 704, 705), createEye(706, [707, 708, 709, 710, 711, 712, 713, 714], 715, 716)], + [createEye(717, [718, 719, 720, 721, 722, 723, 724, 725], 726, 727), createEye(728, [729, 730, 731, 732, 733, 734, 735, 736], 737, 738), createEye(739, [740, 741, 742, 743, 744, 745, 746, 747], 748, 749), createEye(750, [751, 752, 753, 754, 755, 756, 757, 758], 759, 760)], + [createEye(761, [762, 0, 763, 764, 0, 0, 0, 765], 766, 767), createEye(768, [769, 0, 770, 771, 0, 0, 0, 772], 773, 774), createEye(775, [776, 0, 777, 778, 0, 0, 0, 779], 780, 781), createEye(782, [783, 0, 784, 785, 0, 0, 0, 786], 787, 788)], + [createEye(789, [0, 0, 0, 0, 0, 0, 0, 0], 790, 791), createEye(792, [0, 0, 0, 0, 0, 0, 0, 0], 793, 794), createEye(795, [0, 0, 0, 0, 0, 0, 0, 0], 796, 797), createEye(798, [0, 0, 0, 0, 0, 0, 0, 0], 799, 800)], + [createEye(801, [0, 0, 0, 0, 0, 0, 0, 0], 802, 803), createEye(804, [0, 0, 0, 0, 0, 0, 0, 0], 805, 806), createEye(807, [0, 0, 0, 0, 0, 0, 0, 0], 808, 809), createEye(810, [0, 0, 0, 0, 0, 0, 0, 0], 811, 812)], + [createEye(813, [814, 815, 816, 817, 818, 819, 820, 821], 822, 823), createEye(824, [825, 826, 827, 828, 829, 830, 831, 832], 833, 834), createEye(835, [836, 837, 838, 839, 840, 841, 842, 843], 844, 845), createEye(846, [847, 848, 849, 850, 851, 852, 853, 854], 855, 856)], + [createEye(857, [858, 859, 860, 861, 862, 863, 864, 865], 866, 867), createEye(868, [869, 870, 871, 872, 873, 874, 875, 876], 877, 878), createEye(879, [880, 881, 882, 883, 884, 885, 886, 887], 888, 889), createEye(890, [891, 892, 893, 894, 895, 896, 897, 898], 899, 900)], + [createEye(901, [902, 0, 903, 904, 0, 0, 0, 905], 906, 907), createEye(908, [909, 0, 910, 911, 0, 0, 0, 912], 913, 914), createEye(915, [916, 0, 917, 918, 0, 0, 0, 919], 920, 921), createEye(922, [923, 0, 924, 925, 0, 0, 0, 926], 927, 928)], + [createEye(929, [0, 0, 0, 0, 0, 0, 0, 0], 930, 931), createEye(932, [0, 0, 0, 0, 0, 0, 0, 0], 933, 934), createEye(935, [0, 0, 0, 0, 0, 0, 0, 0], 936, 937), createEye(938, [0, 0, 0, 0, 0, 0, 0, 0], 939, 940)], + [createEye(942, [0, 0, 0, 0, 0, 0, 0, 0], 941, 943), createEye(945, [0, 0, 0, 0, 0, 0, 0, 0], 944, 946), createEye(948, [0, 0, 0, 0, 0, 0, 0, 0], 947, 949), createEye(951, [0, 0, 0, 0, 0, 0, 0, 0], 950, 952)], + [createEye(954, [0, 0, 0, 0, 0, 0, 0, 0], 953, 955), createEye(957, [0, 0, 0, 0, 0, 0, 0, 0], 956, 958), createEye(960, [0, 0, 0, 0, 0, 0, 0, 0], 959, 961), createEye(963, [0, 0, 0, 0, 0, 0, 0, 0], 962, 964)], + [createEye(966, [0, 0, 0, 0, 0, 0, 0, 0], 965, 967), createEye(969, [0, 0, 0, 0, 0, 0, 0, 0], 968, 970), createEye(972, [0, 0, 0, 0, 0, 0, 0, 0], 971, 973), createEye(975, [0, 0, 0, 0, 0, 0, 0, 0], 974, 976)], + [createEye(978, [0, 0, 0, 0, 0, 0, 0, 0], 977, 979), createEye(981, [0, 0, 0, 0, 0, 0, 0, 0], 980, 982), createEye(984, [0, 0, 0, 0, 0, 0, 0, 0], 983, 985), createEye(987, [0, 0, 0, 0, 0, 0, 0, 0], 986, 988)], + [createEye(989, [990, 991, 992, 993, 994, 995, 996, 997], 998, 999), createEye(1000, [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008], 1009, 1010), createEye(1011, [1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019], 1020, 1021), createEye(1022, [1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030], 1031, 1032)], + [createEye(1033, [1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041], 1042, 1043), createEye(1044, [1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052], 1053, 1054), createEye(1055, [1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063], 1064, 1065), createEye(1066, [1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074], 1075, 1076)], + [createEye(1077, [1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085], 1086, 1087), createEye(1088, [1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096], 1097, 1098), createEye(1099, [1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107], 1108, 1109), createEye(1110, [1111, 1112, 1113, 1114, 1115, 1116, 1117, 1118], 1119, 1120)], + [createEye(1121, [1122, 0, 1123, 1124, 0, 0, 0, 1125], 1126, 1127), createEye(1128, [1129, 0, 1130, 1131, 0, 0, 0, 1132], 1133, 1134), createEye(1135, [1136, 0, 1137, 1138, 0, 0, 0, 1139], 1140, 1141), createEye(1142, [1143, 0, 1144, 1145, 0, 0, 0, 1146], 1147, 1148)], + [createEye(1149, [1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157], 1158, 1159), createEye(1160, [1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168], 1169, 1170), createEye(1171, [1172, 1173, 1174, 1175, 1176, 1177, 1178, 1179], 1180, 1181), createEye(1182, [1183, 1184, 1185, 1186, 1187, 1188, 1189, 1190], 1191, 1192)], + [createEye(1193, [1194, 1195, 1196, 1197, 1198, 1199, 0, 1200], 1201, 1202), createEye(1203, [1204, 1205, 1206, 1207, 1208, 1209, 0, 1210], 1211, 1212), createEye(1213, [1214, 1215, 1216, 1217, 1218, 1219, 0, 1220], 1221, 1222), createEye(1223, [1224, 1225, 1226, 1227, 1228, 1229, 0, 1230], 1231, 1232)], + [createEye(1234, [0, 0, 0, 0, 0, 0, 0, 0], 1233, 1235), createEye(1237, [0, 0, 0, 0, 0, 0, 0, 0], 1236, 1238), createEye(1240, [0, 0, 0, 0, 0, 0, 0, 0], 1239, 1241), createEye(1243, [0, 0, 0, 0, 0, 0, 0, 0], 1242, 1244)], + [createEye(1246, [0, 0, 0, 0, 0, 0, 0, 0], 1245, 0), createEye(1248, [0, 0, 0, 0, 0, 0, 0, 0], 1247, 0), createEye(1250, [0, 0, 0, 0, 0, 0, 0, 0], 1249, 0), createEye(1252, [0, 0, 0, 0, 0, 0, 0, 0], 1251, 0)], + [createEye(1254, [0, 0, 0, 0, 0, 0, 0, 0], 1253, 1255), createEye(1257, [0, 0, 0, 0, 0, 0, 0, 0], 1256, 1258), createEye(1260, [0, 0, 0, 0, 0, 0, 0, 0], 1259, 1261), createEye(1263, [0, 0, 0, 0, 0, 0, 0, 0], 1262, 1264)], + [createEye(1266, [0, 0, 0, 0, 0, 0, 0, 0], 1265, 0), createEye(1268, [0, 0, 0, 0, 0, 0, 0, 0], 1267, 0), createEye(1270, [0, 0, 0, 0, 0, 0, 0, 0], 1269, 0), createEye(1272, [0, 0, 0, 0, 0, 0, 0, 0], 1271, 0)] ]; export const noses: PonyNose[][][] = [ - [[createNose(1274, 3, 0, 1273)], [createNose(1276, 3, 0, 1275)], [createNose(1278, 5, 0, 1277)]], - [[createNose(1280, 3, 0, 1279)], [createNose(1282, 3, 0, 1281)], [createNose(1284, 5, 0, 1283)]], - [[createNose(1286, 3, 0, 1285)], [createNose(1288, 3, 0, 1287)], [createNose(1290, 5, 0, 1289)]], - [[createNose(1292, 3, 0, 1291)], [createNose(1294, 3, 0, 1293)], [createNose(1296, 5, 0, 1295)]], - [[createNose(1299, 3, 1298, 1297)], [createNose(1302, 3, 1301, 1300)], [createNose(1305, 5, 1304, 1303)]], - [[createNose(1308, 3, 1307, 1306)], [createNose(1311, 3, 1310, 1309)], [createNose(1314, 5, 1313, 1312)]], - [[createNose(1316, 3, 0, 1315)], [createNose(1318, 3, 0, 1317)], [createNose(1320, 5, 0, 1319)]], - [[createNose(1322, 3, 0, 1321)], [createNose(1324, 3, 0, 1323)], [createNose(1326, 5, 0, 1325)]], - [[createNose(1329, 3, 1328, 1327)], [createNose(1332, 3, 1331, 1330)], [createNose(1335, 5, 1334, 1333)]], - [[createNose(1338, 3, 1337, 1336)], [createNose(1341, 3, 1340, 1339)], [createNose(1344, 5, 1343, 1342)]], - [[createNose(1347, 3, 1346, 1345)], [createNose(1350, 3, 1349, 1348)], [createNose(1353, 5, 1352, 1351)]], - [[createNose(1356, 3, 1355, 1354)], [createNose(1359, 3, 1358, 1357)], [createNose(1362, 5, 1361, 1360)]], - [[createNose(1365, 3, 1364, 1363)], [createNose(1368, 3, 1367, 1366)], [createNose(1371, 5, 1370, 1369)]], - [[createNose(1372, 3, 0, 0)], [createNose(1373, 3, 0, 0)], [createNose(1374, 5, 0, 0)]], - [[createNose(1377, 3, 1376, 1375)], [createNose(1380, 3, 1379, 1378)], [createNose(1383, 5, 1382, 1381)]], - [[createNose(1386, 3, 1385, 1384)], [createNose(1389, 3, 1388, 1387)], [createNose(1392, 5, 1391, 1390)]], - [[createNose(1395, 3, 1394, 1393)], [createNose(1398, 3, 1397, 1396)], [createNose(1401, 5, 1400, 1399)]], - [[createNose(1402, 3, 0, 0)], [createNose(1403, 3, 0, 0)], [createNose(1404, 5, 0, 0)]], - [[createNose(1407, 3, 1406, 1405)], [createNose(1410, 3, 1409, 1408)], [createNose(1413, 5, 1412, 1411)]], - [[createNose(1416, 3, 1415, 1414)], [createNose(1419, 3, 1418, 1417)], [createNose(1422, 5, 1421, 1420)]], - [[createNose(1425, 3, 1424, 1423)], [createNose(1428, 3, 1427, 1426)], [createNose(1431, 5, 1430, 1429)]], - [[createNose(1434, 3, 1433, 1432)], [createNose(1437, 3, 1436, 1435)], [createNose(1440, 5, 1439, 1438)]], - [[createNose(1443, 3, 1442, 1441)], [createNose(1446, 3, 1445, 1444)], [createNose(1449, 5, 1448, 1447)]], - [[createNose(1452, 3, 1451, 1450)], [createNose(1455, 3, 1454, 1453)], [createNose(1458, 5, 1457, 1456)]], - [[createNose(1461, 3, 1460, 1459)], [createNose(1464, 3, 1463, 1462)], [createNose(1467, 5, 1466, 1465)]], - [[createNose(1470, 3, 1469, 1468)], [createNose(1473, 3, 1472, 1471)], [createNose(1476, 5, 1475, 1474)]] + [[createNose(1274, 3, 0, 1273)], [createNose(1276, 3, 0, 1275)], [createNose(1278, 5, 0, 1277)]], + [[createNose(1280, 3, 0, 1279)], [createNose(1282, 3, 0, 1281)], [createNose(1284, 5, 0, 1283)]], + [[createNose(1286, 3, 0, 1285)], [createNose(1288, 3, 0, 1287)], [createNose(1290, 5, 0, 1289)]], + [[createNose(1292, 3, 0, 1291)], [createNose(1294, 3, 0, 1293)], [createNose(1296, 5, 0, 1295)]], + [[createNose(1299, 3, 1298, 1297)], [createNose(1302, 3, 1301, 1300)], [createNose(1305, 5, 1304, 1303)]], + [[createNose(1308, 3, 1307, 1306)], [createNose(1311, 3, 1310, 1309)], [createNose(1314, 5, 1313, 1312)]], + [[createNose(1316, 3, 0, 1315)], [createNose(1318, 3, 0, 1317)], [createNose(1320, 5, 0, 1319)]], + [[createNose(1322, 3, 0, 1321)], [createNose(1324, 3, 0, 1323)], [createNose(1326, 5, 0, 1325)]], + [[createNose(1329, 3, 1328, 1327)], [createNose(1332, 3, 1331, 1330)], [createNose(1335, 5, 1334, 1333)]], + [[createNose(1338, 3, 1337, 1336)], [createNose(1341, 3, 1340, 1339)], [createNose(1344, 5, 1343, 1342)]], + [[createNose(1347, 3, 1346, 1345)], [createNose(1350, 3, 1349, 1348)], [createNose(1353, 5, 1352, 1351)]], + [[createNose(1356, 3, 1355, 1354)], [createNose(1359, 3, 1358, 1357)], [createNose(1362, 5, 1361, 1360)]], + [[createNose(1365, 3, 1364, 1363)], [createNose(1368, 3, 1367, 1366)], [createNose(1371, 5, 1370, 1369)]], + [[createNose(1372, 3, 0, 0)], [createNose(1373, 3, 0, 0)], [createNose(1374, 5, 0, 0)]], + [[createNose(1377, 3, 1376, 1375)], [createNose(1380, 3, 1379, 1378)], [createNose(1383, 5, 1382, 1381)]], + [[createNose(1386, 3, 1385, 1384)], [createNose(1389, 3, 1388, 1387)], [createNose(1392, 5, 1391, 1390)]], + [[createNose(1395, 3, 1394, 1393)], [createNose(1398, 3, 1397, 1396)], [createNose(1401, 5, 1400, 1399)]], + [[createNose(1402, 3, 0, 0)], [createNose(1403, 3, 0, 0)], [createNose(1404, 5, 0, 0)]], + [[createNose(1407, 3, 1406, 1405)], [createNose(1410, 3, 1409, 1408)], [createNose(1413, 5, 1412, 1411)]], + [[createNose(1416, 3, 1415, 1414)], [createNose(1419, 3, 1418, 1417)], [createNose(1422, 5, 1421, 1420)]], + [[createNose(1425, 3, 1424, 1423)], [createNose(1428, 3, 1427, 1426)], [createNose(1431, 5, 1430, 1429)]], + [[createNose(1434, 3, 1433, 1432)], [createNose(1437, 3, 1436, 1435)], [createNose(1440, 5, 1439, 1438)]], + [[createNose(1443, 3, 1442, 1441)], [createNose(1446, 3, 1445, 1444)], [createNose(1449, 5, 1448, 1447)]], + [[createNose(1452, 3, 1451, 1450)], [createNose(1455, 3, 1454, 1453)], [createNose(1458, 5, 1457, 1456)]], + [[createNose(1461, 3, 1460, 1459)], [createNose(1464, 3, 1463, 1462)], [createNose(1467, 5, 1466, 1465)]], + [[createNose(1470, 3, 1469, 1468)], [createNose(1473, 3, 1472, 1471)], [createNose(1476, 5, 1475, 1474)]] ]; export const blush = sprites2[1477]; export const ponyShadows = [ - sprites2[1478], - sprites2[1479], - sprites2[1480], - sprites2[1481], - sprites2[1482] + sprites2[1478], + sprites2[1479], + sprites2[1480], + sprites2[1481], + sprites2[1482] ]; export const ponySelections = [ - sprites2[1483], - sprites2[1484], - sprites2[1485], - sprites2[1486], - sprites2[1487] + sprites2[1483], + sprites2[1484], + sprites2[1485], + sprites2[1486], + sprites2[1487] ]; export const cms = sprites2[1488]; export const cmsFlip = sprites2[1489]; export const frontLegs: AnimatedSprites = [ - undefined, - [[colorPal3(1490)]], - [[colorPal3(1491)]], - [[colorPal3(1492)]], - [[colorPal3(1493)]], - [[colorPal3(1494)]], - [[colorPal3(1495)]], - [[colorPal3(1496)]], - [[colorPal3(1497)]], - [[colorPal3(1498)]], - [[colorPal3(1499)]], - [[colorPal3(1500)]], - [[colorPal3(1501)]], - [[colorPal3(1502)]], - [[colorPal3(1503)]], - [[colorPal3(1504)]], - [[colorPal3(1505)]], - [[colorPal3(1506)]], - [[colorPal3(1507)]], - [[colorPal3(1508)]], - [[colorPal3(1509)]], - [[colorPal3(1510)]], - [[colorPal3(1511)]], - [[colorPal3(1512)]], - [[colorPal3(1513)]], - [[colorPal3(1514)]], - [[colorPal3(1515)]], - [[colorPal3(1516)]], - [[colorPal3(1517)]], - [[colorPal3(1518)]], - [[colorPal3(1519)]], - [[colorPal3(1520)]], - [[colorPal3(1521)]], - [[colorPal3(1522)]], - [[colorPal3(1523)]], - [[colorPal3(1524)]], - [[colorPal3(1525)]], - [[colorPal3(1526)]], - [[colorPal3(1527)]] + undefined, + [[colorPal3(1490)]], + [[colorPal3(1491)]], + [[colorPal3(1492)]], + [[colorPal3(1493)]], + [[colorPal3(1494)]], + [[colorPal3(1495)]], + [[colorPal3(1496)]], + [[colorPal3(1497)]], + [[colorPal3(1498)]], + [[colorPal3(1499)]], + [[colorPal3(1500)]], + [[colorPal3(1501)]], + [[colorPal3(1502)]], + [[colorPal3(1503)]], + [[colorPal3(1504)]], + [[colorPal3(1505)]], + [[colorPal3(1506)]], + [[colorPal3(1507)]], + [[colorPal3(1508)]], + [[colorPal3(1509)]], + [[colorPal3(1510)]], + [[colorPal3(1511)]], + [[colorPal3(1512)]], + [[colorPal3(1513)]], + [[colorPal3(1514)]], + [[colorPal3(1515)]], + [[colorPal3(1516)]], + [[colorPal3(1517)]], + [[colorPal3(1518)]], + [[colorPal3(1519)]], + [[colorPal3(1520)]], + [[colorPal3(1521)]], + [[colorPal3(1522)]], + [[colorPal3(1523)]], + [[colorPal3(1524)]], + [[colorPal3(1525)]], + [[colorPal3(1526)]], + [[colorPal3(1527)]] ]; export const frontLegHooves: AnimatedSprites = [ - undefined, - [undefined, [colorPal3(1528)], [colorPal3(1529)], [colorPal5(1530)], [colorPal5(1531)], [colorPal5(1532)], [colorPal3(1533)]], - [undefined, [colorPal3(1534)], [colorPal3(1535)], [colorPal5(1536)], [colorPal5(1537)], [colorPal5(1538)], [colorPal3(1539)]], - [undefined, [colorPal3(1540)], [colorPal3(1541)], [colorPal5(1542)], [colorPal5(1543)], [colorPal5(1544)], [colorPal3(1545)]], - [undefined, [colorPal3(1546)], [colorPal3(1547)], [colorPal5(1548)], [colorPal5(1549)], [colorPal5(1550)], [colorPal3(1551)]], - [undefined, [colorPal3(1552)], [colorPal3(1553)], [colorPal5(1554)], [colorPal5(1555)], [colorPal5(1556)], [colorPal3(1557)]], - [undefined, [colorPal3(1558)], [colorPal3(1559)], [colorPal5(1560)], [colorPal5(1561)], [colorPal5(1562)], [colorPal3(1563)]], - [undefined, [colorPal3(1564)], [colorPal3(1565)], [colorPal5(1566)], [colorPal5(1567)], [colorPal5(1568)], [colorPal3(1569)]], - [undefined, [colorPal3(1570)], [colorPal3(1571)], [colorPal5(1572)], [colorPal5(1573)], [colorPal5(1574)], [colorPal3(1575)]], - [undefined, [colorPal3(1576)], [colorPal3(1577)], [colorPal5(1578)], [colorPal5(1579)], [colorPal5(1580)], [colorPal3(1581)]], - [undefined, [colorPal3(1582)], [colorPal3(1583)], [colorPal5(1584)], [colorPal5(1585)], [colorPal5(1586)], [colorPal3(1587)]], - [undefined, [colorPal3(1588)], [colorPal3(1589)], [colorPal5(1590)], [colorPal5(1591)], [colorPal5(1592)], [colorPal3(1593)]], - [undefined, [colorPal3(1594)], [colorPal3(1595)], [colorPal5(1596)], [colorPal5(1597)], [colorPal5(1598)], [colorPal3(1599)]], - [undefined, [colorPal3(1600)], [colorPal3(1601)], [colorPal5(1602)], [colorPal5(1603)], [colorPal5(1604)], [colorPal3(1605)]], - [undefined, [colorPal3(1606)], [colorPal3(1607)], [colorPal5(1608)], [colorPal5(1609)], [colorPal5(1610)], [colorPal3(1611)]], - [undefined, [colorPal3(1612)], [colorPal3(1613)], [colorPal5(1614)], [colorPal5(1615)], [colorPal5(1616)], [colorPal3(1617)]], - [undefined, [colorPal3(1618)], [colorPal3(1619)], [colorPal5(1620)], [colorPal5(1621)], [colorPal5(1622)], [colorPal3(1623)]], - [undefined, [colorPal3(1624)], [colorPal3(1625)], [colorPal5(1626)], [colorPal5(1627)], [colorPal5(1628)], [colorPal3(1629)]], - [undefined, [colorPal3(1630)], [colorPal3(1631)], [colorPal5(1632)], [colorPal5(1633)], [colorPal5(1634)], [colorPal3(1635)]], - [undefined, [colorPal3(1636)], [colorPal3(1637)], [colorPal5(1638)], [colorPal5(1639)], [colorPal5(1640)], [colorPal3(1641)]], - [undefined, [colorPal3(1642)], [colorPal3(1643)], [colorPal5(1644)], [colorPal5(1645)], [colorPal5(1646)], [colorPal3(1647)]], - [undefined, [colorPal3(1648)], [colorPal3(1649)], [colorPal5(1650)], [colorPal5(1651)], [colorPal5(1652)], [colorPal3(1653)]], - [undefined, [colorPal3(1654)], [colorPal3(1655)], [colorPal5(1656)], [colorPal5(1657)], [colorPal5(1658)], [colorPal3(1659)]], - [undefined, [colorPal3(1660)], [colorPal3(1661)], [colorPal5(1662)], [colorPal5(1663)], [colorPal5(1664)], [colorPal3(1665)]], - [undefined, [colorPal3(1666)], [colorPal3(1667)], [colorPal5(1668)], [colorPal5(1669)], [colorPal5(1670)], [colorPal3(1671)]], - [undefined, [colorPal3(1672)], [colorPal3(1673)], [colorPal5(1674)], [colorPal5(1675)], [colorPal5(1676)], [colorPal3(1677)]], - [undefined, [colorPal3(1678)], [colorPal3(1679)], [colorPal5(1680)], [colorPal5(1681)], [colorPal5(1682)], [colorPal3(1683)]], - [undefined, [colorPal3(1684)], [colorPal3(1685)], [colorPal5(1686)], [colorPal5(1687)], [colorPal5(1688)], [colorPal3(1689)]], - [undefined, [colorPal3(1690)], [colorPal3(1691)], [colorPal5(1692)], [colorPal5(1693)], [colorPal5(1694)], [colorPal3(1695)]], - [undefined, [colorPal3(1696)], [colorPal3(1697)], [colorPal5(1698)], [colorPal5(1699)], [colorPal5(1700)], [colorPal3(1701)]], - [undefined, [colorPal3(1702)], [colorPal3(1703)], [colorPal5(1704)], [colorPal5(1705)], [colorPal5(1706)], [colorPal3(1707)]], - [undefined, [colorPal3(1708)], [colorPal3(1709)], [colorPal5(1710)], [colorPal5(1711)], [colorPal5(1712)], [colorPal3(1713)]], - [undefined, [colorPal3(1714)], [colorPal3(1715)], [colorPal5(1716)], [colorPal5(1717)], [colorPal5(1718)], [colorPal3(1719)]], - [undefined, [colorPal3(1720)], [colorPal3(1721)], [colorPal5(1722)], [colorPal5(1723)], [colorPal5(1724)], [colorPal3(1725)]], - [undefined, [colorPal3(1726)], [colorPal3(1727)], [colorPal5(1728)], [colorPal5(1729)], [colorPal5(1730)], [colorPal3(1731)]], - [undefined, [colorPal3(1732)], [colorPal3(1733)], [colorPal5(1734)], [colorPal5(1735)], [colorPal5(1736)], [colorPal3(1737)]], - [undefined, [colorPal3(1738)], [colorPal3(1739)], [colorPal5(1740)], [colorPal5(1741)], [colorPal5(1742)], [colorPal3(1743)]], - [undefined, [colorPal3(1744)], [colorPal3(1745)], [colorPal5(1746)], [colorPal5(1747)], [colorPal5(1748)], [colorPal3(1749)]], - [undefined, [colorPal3(1750)], [colorPal3(1751)], [colorPal5(1752)], [colorPal5(1753)], [colorPal5(1754)], [colorPal3(1755)]] + undefined, + [undefined, [colorPal3(1528)], [colorPal3(1529)], [colorPal5(1530)], [colorPal5(1531)], [colorPal5(1532)], [colorPal3(1533)]], + [undefined, [colorPal3(1534)], [colorPal3(1535)], [colorPal5(1536)], [colorPal5(1537)], [colorPal5(1538)], [colorPal3(1539)]], + [undefined, [colorPal3(1540)], [colorPal3(1541)], [colorPal5(1542)], [colorPal5(1543)], [colorPal5(1544)], [colorPal3(1545)]], + [undefined, [colorPal3(1546)], [colorPal3(1547)], [colorPal5(1548)], [colorPal5(1549)], [colorPal5(1550)], [colorPal3(1551)]], + [undefined, [colorPal3(1552)], [colorPal3(1553)], [colorPal5(1554)], [colorPal5(1555)], [colorPal5(1556)], [colorPal3(1557)]], + [undefined, [colorPal3(1558)], [colorPal3(1559)], [colorPal5(1560)], [colorPal5(1561)], [colorPal5(1562)], [colorPal3(1563)]], + [undefined, [colorPal3(1564)], [colorPal3(1565)], [colorPal5(1566)], [colorPal5(1567)], [colorPal5(1568)], [colorPal3(1569)]], + [undefined, [colorPal3(1570)], [colorPal3(1571)], [colorPal5(1572)], [colorPal5(1573)], [colorPal5(1574)], [colorPal3(1575)]], + [undefined, [colorPal3(1576)], [colorPal3(1577)], [colorPal5(1578)], [colorPal5(1579)], [colorPal5(1580)], [colorPal3(1581)]], + [undefined, [colorPal3(1582)], [colorPal3(1583)], [colorPal5(1584)], [colorPal5(1585)], [colorPal5(1586)], [colorPal3(1587)]], + [undefined, [colorPal3(1588)], [colorPal3(1589)], [colorPal5(1590)], [colorPal5(1591)], [colorPal5(1592)], [colorPal3(1593)]], + [undefined, [colorPal3(1594)], [colorPal3(1595)], [colorPal5(1596)], [colorPal5(1597)], [colorPal5(1598)], [colorPal3(1599)]], + [undefined, [colorPal3(1600)], [colorPal3(1601)], [colorPal5(1602)], [colorPal5(1603)], [colorPal5(1604)], [colorPal3(1605)]], + [undefined, [colorPal3(1606)], [colorPal3(1607)], [colorPal5(1608)], [colorPal5(1609)], [colorPal5(1610)], [colorPal3(1611)]], + [undefined, [colorPal3(1612)], [colorPal3(1613)], [colorPal5(1614)], [colorPal5(1615)], [colorPal5(1616)], [colorPal3(1617)]], + [undefined, [colorPal3(1618)], [colorPal3(1619)], [colorPal5(1620)], [colorPal5(1621)], [colorPal5(1622)], [colorPal3(1623)]], + [undefined, [colorPal3(1624)], [colorPal3(1625)], [colorPal5(1626)], [colorPal5(1627)], [colorPal5(1628)], [colorPal3(1629)]], + [undefined, [colorPal3(1630)], [colorPal3(1631)], [colorPal5(1632)], [colorPal5(1633)], [colorPal5(1634)], [colorPal3(1635)]], + [undefined, [colorPal3(1636)], [colorPal3(1637)], [colorPal5(1638)], [colorPal5(1639)], [colorPal5(1640)], [colorPal3(1641)]], + [undefined, [colorPal3(1642)], [colorPal3(1643)], [colorPal5(1644)], [colorPal5(1645)], [colorPal5(1646)], [colorPal3(1647)]], + [undefined, [colorPal3(1648)], [colorPal3(1649)], [colorPal5(1650)], [colorPal5(1651)], [colorPal5(1652)], [colorPal3(1653)]], + [undefined, [colorPal3(1654)], [colorPal3(1655)], [colorPal5(1656)], [colorPal5(1657)], [colorPal5(1658)], [colorPal3(1659)]], + [undefined, [colorPal3(1660)], [colorPal3(1661)], [colorPal5(1662)], [colorPal5(1663)], [colorPal5(1664)], [colorPal3(1665)]], + [undefined, [colorPal3(1666)], [colorPal3(1667)], [colorPal5(1668)], [colorPal5(1669)], [colorPal5(1670)], [colorPal3(1671)]], + [undefined, [colorPal3(1672)], [colorPal3(1673)], [colorPal5(1674)], [colorPal5(1675)], [colorPal5(1676)], [colorPal3(1677)]], + [undefined, [colorPal3(1678)], [colorPal3(1679)], [colorPal5(1680)], [colorPal5(1681)], [colorPal5(1682)], [colorPal3(1683)]], + [undefined, [colorPal3(1684)], [colorPal3(1685)], [colorPal5(1686)], [colorPal5(1687)], [colorPal5(1688)], [colorPal3(1689)]], + [undefined, [colorPal3(1690)], [colorPal3(1691)], [colorPal5(1692)], [colorPal5(1693)], [colorPal5(1694)], [colorPal3(1695)]], + [undefined, [colorPal3(1696)], [colorPal3(1697)], [colorPal5(1698)], [colorPal5(1699)], [colorPal5(1700)], [colorPal3(1701)]], + [undefined, [colorPal3(1702)], [colorPal3(1703)], [colorPal5(1704)], [colorPal5(1705)], [colorPal5(1706)], [colorPal3(1707)]], + [undefined, [colorPal3(1708)], [colorPal3(1709)], [colorPal5(1710)], [colorPal5(1711)], [colorPal5(1712)], [colorPal3(1713)]], + [undefined, [colorPal3(1714)], [colorPal3(1715)], [colorPal5(1716)], [colorPal5(1717)], [colorPal5(1718)], [colorPal3(1719)]], + [undefined, [colorPal3(1720)], [colorPal3(1721)], [colorPal5(1722)], [colorPal5(1723)], [colorPal5(1724)], [colorPal3(1725)]], + [undefined, [colorPal3(1726)], [colorPal3(1727)], [colorPal5(1728)], [colorPal5(1729)], [colorPal5(1730)], [colorPal3(1731)]], + [undefined, [colorPal3(1732)], [colorPal3(1733)], [colorPal5(1734)], [colorPal5(1735)], [colorPal5(1736)], [colorPal3(1737)]], + [undefined, [colorPal3(1738)], [colorPal3(1739)], [colorPal5(1740)], [colorPal5(1741)], [colorPal5(1742)], [colorPal3(1743)]], + [undefined, [colorPal3(1744)], [colorPal3(1745)], [colorPal5(1746)], [colorPal5(1747)], [colorPal5(1748)], [colorPal3(1749)]], + [undefined, [colorPal3(1750)], [colorPal3(1751)], [colorPal5(1752)], [colorPal5(1753)], [colorPal5(1754)], [colorPal3(1755)]] ]; export const frontLegAccessories: AnimatedSprites = [ - undefined, - [undefined, [colorPal3(1756), colorPal13(1757), colorPal13(1758), colorPal13(1759), colorPal13(1760)]], - [undefined, [colorPal3(1761), colorPal13(1762), colorPal13(1763), colorPal13(1764), colorPal13(1765)]], - [undefined, [colorPal3(1766), colorPal13(1767), colorPal13(1768), colorPal13(1769), colorPal13(1770)]], - [undefined, [colorPal3(1771), colorPal13(1772), colorPal13(1773), colorPal13(1774), colorPal13(1775)]], - [undefined, [colorPal3(1776), colorPal13(1777), colorPal13(1778), colorPal13(1779), colorPal13(1780)]], - [undefined, [colorPal3(1781), colorPal13(1782), colorPal13(1783), colorPal13(1784), colorPal13(1785)]], - [undefined, [colorPal3(1786), colorPal13(1787), colorPal13(1788), colorPal13(1789), colorPal13(1790)]], - [undefined, [colorPal3(1791), colorPal13(1792), colorPal13(1793), colorPal13(1794), colorPal13(1795)]], - [undefined, [colorPal3(1796), colorPal13(1797), colorPal13(1798), colorPal13(1799), colorPal13(1800)]], - [undefined, [colorPal3(1801), colorPal13(1802), colorPal13(1803), colorPal13(1804), colorPal13(1805)]], - [undefined, [colorPal3(1806), colorPal13(1807), colorPal13(1808), colorPal13(1809), colorPal13(1810)]], - [undefined, [colorPal3(1811), colorPal13(1812), colorPal13(1813), colorPal13(1814), colorPal13(1815)]], - [undefined, [colorPal3(1816), colorPal13(1817), colorPal13(1818), colorPal13(1819), colorPal13(1820)]], - [undefined, [colorPal3(1821), colorPal13(1822), colorPal13(1823), colorPal13(1824), colorPal13(1825)]], - [undefined, [colorPal3(1826), colorPal13(1827), colorPal13(1828), colorPal13(1829), colorPal13(1830)]], - [undefined, [colorPal3(1831), colorPal13(1832), colorPal13(1833), colorPal13(1834), colorPal13(1835)]], - [undefined, [colorPal3(1836), colorPal13(1837), colorPal13(1838), colorPal13(1839), colorPal13(1840)]], - [undefined, [colorPal3(1841), colorPal13(1842), colorPal13(1843), colorPal13(1844), colorPal13(1845)]], - [undefined, [colorPal3(1846), colorPal13(1847), colorPal13(1848), colorPal13(1849), colorPal13(1850)]], - [undefined, [colorPal3(1851), colorPal13(1852), colorPal13(1853), colorPal13(1854), colorPal13(1855)]], - [undefined, [colorPal3(1856), colorPal13(1857), colorPal13(1858), colorPal13(1859), colorPal13(1860)]], - [undefined, [colorPal3(1861), colorPal13(1862), colorPal13(1863), colorPal13(1864), colorPal13(1865)]], - [undefined, [colorPal3(1866), colorPal13(1867), colorPal13(1868), colorPal13(1869), colorPal13(1870)]], - [undefined, [colorPal3(1871), colorPal13(1872), colorPal13(1873), colorPal13(1874), colorPal13(1875)]], - [undefined, [colorPal3(1876), colorPal13(1877), colorPal13(1878), colorPal13(1879), colorPal13(1880)]], - [undefined, [colorPal3(1881), colorPal13(1882), colorPal13(1883), colorPal13(1884), colorPal13(1885)]], - [undefined, [colorPal3(1886), colorPal13(1887), colorPal13(1888), colorPal13(1889), colorPal13(1890)]], - [undefined, [colorPal3(1891), colorPal13(1892), colorPal13(1893), colorPal13(1894), colorPal13(1895)]], - [undefined, [colorPal3(1896), colorPal13(1897), colorPal13(1898), colorPal13(1899), colorPal13(1900)]], - [undefined, [colorPal3(1901), colorPal13(1902), colorPal13(1903), colorPal13(1904), colorPal13(1905)]], - [undefined, [colorPal3(1906), colorPal13(1907), colorPal13(1908), colorPal13(1909), colorPal13(1910)]], - [undefined, [colorPal3(1911), colorPal13(1912), colorPal13(1913), colorPal13(1914), colorPal13(1915)]], - [undefined, [colorPal3(1916), colorPal13(1917), colorPal13(1918), colorPal13(1919), colorPal13(1920)]], - [undefined, [colorPal3(1921), colorPal13(1922), colorPal13(1923), colorPal13(1924), colorPal13(1925)]], - [undefined, [colorPal3(1926), colorPal13(1927), colorPal13(1928), colorPal13(1929), colorPal13(1930)]], - [undefined, [colorPal3(1931), colorPal13(1932), colorPal13(1933), colorPal13(1934), colorPal13(1935)]], - [undefined, [colorPal3(1936), colorPal13(1937), colorPal13(1938), colorPal13(1939), colorPal13(1940)]], - [undefined, [colorPal3(1941), colorPal13(1942), colorPal13(1943), colorPal13(1944), colorPal13(1945)]] + undefined, + [undefined, [colorPal3(1756), colorPal13(1757), colorPal13(1758), colorPal13(1759), colorPal13(1760)]], + [undefined, [colorPal3(1761), colorPal13(1762), colorPal13(1763), colorPal13(1764), colorPal13(1765)]], + [undefined, [colorPal3(1766), colorPal13(1767), colorPal13(1768), colorPal13(1769), colorPal13(1770)]], + [undefined, [colorPal3(1771), colorPal13(1772), colorPal13(1773), colorPal13(1774), colorPal13(1775)]], + [undefined, [colorPal3(1776), colorPal13(1777), colorPal13(1778), colorPal13(1779), colorPal13(1780)]], + [undefined, [colorPal3(1781), colorPal13(1782), colorPal13(1783), colorPal13(1784), colorPal13(1785)]], + [undefined, [colorPal3(1786), colorPal13(1787), colorPal13(1788), colorPal13(1789), colorPal13(1790)]], + [undefined, [colorPal3(1791), colorPal13(1792), colorPal13(1793), colorPal13(1794), colorPal13(1795)]], + [undefined, [colorPal3(1796), colorPal13(1797), colorPal13(1798), colorPal13(1799), colorPal13(1800)]], + [undefined, [colorPal3(1801), colorPal13(1802), colorPal13(1803), colorPal13(1804), colorPal13(1805)]], + [undefined, [colorPal3(1806), colorPal13(1807), colorPal13(1808), colorPal13(1809), colorPal13(1810)]], + [undefined, [colorPal3(1811), colorPal13(1812), colorPal13(1813), colorPal13(1814), colorPal13(1815)]], + [undefined, [colorPal3(1816), colorPal13(1817), colorPal13(1818), colorPal13(1819), colorPal13(1820)]], + [undefined, [colorPal3(1821), colorPal13(1822), colorPal13(1823), colorPal13(1824), colorPal13(1825)]], + [undefined, [colorPal3(1826), colorPal13(1827), colorPal13(1828), colorPal13(1829), colorPal13(1830)]], + [undefined, [colorPal3(1831), colorPal13(1832), colorPal13(1833), colorPal13(1834), colorPal13(1835)]], + [undefined, [colorPal3(1836), colorPal13(1837), colorPal13(1838), colorPal13(1839), colorPal13(1840)]], + [undefined, [colorPal3(1841), colorPal13(1842), colorPal13(1843), colorPal13(1844), colorPal13(1845)]], + [undefined, [colorPal3(1846), colorPal13(1847), colorPal13(1848), colorPal13(1849), colorPal13(1850)]], + [undefined, [colorPal3(1851), colorPal13(1852), colorPal13(1853), colorPal13(1854), colorPal13(1855)]], + [undefined, [colorPal3(1856), colorPal13(1857), colorPal13(1858), colorPal13(1859), colorPal13(1860)]], + [undefined, [colorPal3(1861), colorPal13(1862), colorPal13(1863), colorPal13(1864), colorPal13(1865)]], + [undefined, [colorPal3(1866), colorPal13(1867), colorPal13(1868), colorPal13(1869), colorPal13(1870)]], + [undefined, [colorPal3(1871), colorPal13(1872), colorPal13(1873), colorPal13(1874), colorPal13(1875)]], + [undefined, [colorPal3(1876), colorPal13(1877), colorPal13(1878), colorPal13(1879), colorPal13(1880)]], + [undefined, [colorPal3(1881), colorPal13(1882), colorPal13(1883), colorPal13(1884), colorPal13(1885)]], + [undefined, [colorPal3(1886), colorPal13(1887), colorPal13(1888), colorPal13(1889), colorPal13(1890)]], + [undefined, [colorPal3(1891), colorPal13(1892), colorPal13(1893), colorPal13(1894), colorPal13(1895)]], + [undefined, [colorPal3(1896), colorPal13(1897), colorPal13(1898), colorPal13(1899), colorPal13(1900)]], + [undefined, [colorPal3(1901), colorPal13(1902), colorPal13(1903), colorPal13(1904), colorPal13(1905)]], + [undefined, [colorPal3(1906), colorPal13(1907), colorPal13(1908), colorPal13(1909), colorPal13(1910)]], + [undefined, [colorPal3(1911), colorPal13(1912), colorPal13(1913), colorPal13(1914), colorPal13(1915)]], + [undefined, [colorPal3(1916), colorPal13(1917), colorPal13(1918), colorPal13(1919), colorPal13(1920)]], + [undefined, [colorPal3(1921), colorPal13(1922), colorPal13(1923), colorPal13(1924), colorPal13(1925)]], + [undefined, [colorPal3(1926), colorPal13(1927), colorPal13(1928), colorPal13(1929), colorPal13(1930)]], + [undefined, [colorPal3(1931), colorPal13(1932), colorPal13(1933), colorPal13(1934), colorPal13(1935)]], + [undefined, [colorPal3(1936), colorPal13(1937), colorPal13(1938), colorPal13(1939), colorPal13(1940)]], + [undefined, [colorPal3(1941), colorPal13(1942), colorPal13(1943), colorPal13(1944), colorPal13(1945)]] ]; export const frontLegSleeves: AnimatedSprites = [ - undefined, - [[colorPal3(1946), colorPal13(1947), colorPal5(1948), colorPal5(1949), colorPal5(1950), colorPal7(1951), colorPal11(1952)], [colorPal3(1953), colorPal9(1954), colorPal11(1955)], [colorPal3(1956), colorPal5(1957)], [colorPal3(1958), colorPal5(1959)]], - [[colorPal3(1960), colorPal13(1961), colorPal5(1962), colorPal5(1963), colorPal5(1964), colorPal7(1965), colorPal11(1966)], [colorPal3(1967), colorPal9(1968), colorPal11(1969)], [colorPal3(1970), colorPal5(1971)], [colorPal3(1972), colorPal5(1973)]], - [[colorPal3(1974), colorPal13(1975), colorPal5(1976), colorPal5(1977), colorPal5(1978), colorPal7(1979), colorPal11(1980)], [colorPal3(1981), colorPal9(1982), colorPal11(1983)], [colorPal3(1984), colorPal5(1985)], [colorPal3(1986), colorPal5(1987)]], - [[colorPal3(1988), colorPal13(1989), colorPal5(1990), colorPal5(1991), colorPal5(1992), colorPal7(1993), colorPal11(1994)], [colorPal3(1995), colorPal9(1996), colorPal11(1997)], [colorPal3(1998), colorPal5(1999)], [colorPal3(2000), colorPal5(2001)]], - [[colorPal3(2002), colorPal13(2003), colorPal5(2004), colorPal5(2005), colorPal5(2006), colorPal7(2007), colorPal11(2008)], [colorPal3(2009), colorPal9(2010), colorPal11(2011)], [colorPal3(2012), colorPal5(2013)], [colorPal3(2014), colorPal5(2015)]], - [[colorPal3(2016), colorPal13(2017), colorPal5(2018), colorPal5(2019), colorPal5(2020), colorPal7(2021), colorPal11(2022)], [colorPal3(2023), colorPal9(2024), colorPal11(2025)], [colorPal3(2026), colorPal5(2027)], [colorPal3(2028), colorPal5(2029)]], - [[colorPal3(2030), colorPal13(2031), colorPal5(2032), colorPal5(2033), colorPal5(2034), colorPal7(2035), colorPal11(2036)], [colorPal3(2037), colorPal9(2038), colorPal11(2039)], [colorPal3(2040), colorPal5(2041)], [colorPal3(2042), colorPal5(2043)]], - [[colorPal3(2044), colorPal13(2045), colorPal5(2046), colorPal5(2047), colorPal5(2048), colorPal7(2049), colorPal11(2050)], [colorPal3(2051), colorPal9(2052), colorPal11(2053)], [colorPal3(2054), colorPal5(2055)], [colorPal3(2056), colorPal5(2057)]], - [[colorPal3(2058), colorPal13(2059), colorPal5(2060), colorPal5(2061), colorPal5(2062), colorPal7(2063), colorPal11(2064)], [colorPal3(2065), colorPal9(2066), colorPal11(2067)], [colorPal3(2068), colorPal5(2069)], [colorPal3(2070), colorPal5(2071)]], - [[colorPal3(2072), colorPal13(2073), colorPal5(2074), colorPal5(2075), colorPal5(2076), colorPal7(2077), colorPal11(2078)], [colorPal3(2079), colorPal9(2080), colorPal11(2081)], [colorPal3(2082), colorPal5(2083)], [colorPal3(2084), colorPal5(2085)]], - [[colorPal3(2086), colorPal13(2087), colorPal5(2088), colorPal5(2089), colorPal5(2090), colorPal7(2091), colorPal11(2092)], [colorPal3(2093), colorPal9(2094), colorPal11(2095)], [colorPal3(2096), colorPal5(2097)], [colorPal3(2098), colorPal5(2099)]], - [[colorPal3(2100), colorPal13(2101), colorPal5(2102), colorPal5(2103), colorPal5(2104), colorPal7(2105), colorPal11(2106)], [colorPal3(2107), colorPal9(2108), colorPal11(2109)], [colorPal3(2110), colorPal5(2111)], [colorPal3(2112), colorPal5(2113)]], - [[colorPal3(2114), colorPal13(2115), colorPal5(2116), colorPal5(2117), colorPal5(2118), colorPal7(2119), colorPal11(2120)], [colorPal3(2121), colorPal9(2122), colorPal11(2123)], [colorPal3(2124), colorPal5(2125)], [colorPal3(2126), colorPal5(2127)]], - [[colorPal3(2128), colorPal13(2129), colorPal5(2130), colorPal5(2131), colorPal5(2132), colorPal7(2133), colorPal11(2134)], [colorPal3(2135), colorPal9(2136), colorPal11(2137)], [colorPal3(2138), colorPal5(2139)], [colorPal3(2140), colorPal5(2141)]], - [[colorPal3(2142), colorPal13(2143), colorPal5(2144), colorPal5(2145), colorPal5(2146), colorPal7(2147), colorPal11(2148)], [colorPal3(2149), colorPal9(2150), colorPal11(2151)], [colorPal3(2152), colorPal5(2153)], [colorPal3(2154), colorPal5(2155)]], - [[colorPal3(2156), colorPal13(2157), colorPal5(2158), colorPal5(2159), colorPal5(2160), colorPal7(2161), colorPal11(2162)], [colorPal3(2163), colorPal9(2164), colorPal11(2165)], [colorPal3(2166), colorPal5(2167)], [colorPal3(2168), colorPal5(2169)]], - [[colorPal3(2170), colorPal13(2171), colorPal5(2172), colorPal5(2173), colorPal5(2174), colorPal7(2175), colorPal11(2176)], [colorPal3(2177), colorPal9(2178), colorPal11(2179)], [colorPal3(2180), colorPal5(2181)], [colorPal3(2182), colorPal5(2183)]], - [[colorPal3(2184), colorPal13(2185), colorPal5(2186), colorPal5(2187), colorPal5(2188), colorPal7(2189), colorPal11(2190)], [colorPal3(2191), colorPal9(2192), colorPal11(2193)], [colorPal3(2194), colorPal5(2195)], [colorPal3(2196), colorPal5(2197)]], - [[colorPal3(2198), colorPal13(2199), colorPal5(2200), colorPal5(2201), colorPal5(2202), colorPal7(2203), colorPal11(2204)], [colorPal3(2205), colorPal9(2206), colorPal11(2207)], [colorPal3(2208), colorPal5(2209)], [colorPal3(2210), colorPal5(2211)]], - [[colorPal3(2212), colorPal13(2213), colorPal5(2214), colorPal5(2215), colorPal5(2216), colorPal7(2217), colorPal11(2218)], [colorPal3(2219), colorPal9(2220), colorPal11(2221)], [colorPal3(2222), colorPal5(2223)], [colorPal3(2224), colorPal5(2225)]], - [[colorPal3(2226), colorPal13(2227), colorPal5(2228), colorPal5(2229), colorPal5(2230), colorPal7(2231), colorPal11(2232)], [colorPal3(2233), colorPal9(2234), colorPal11(2235)], [colorPal3(2236), colorPal5(2237)], [colorPal3(2238), colorPal5(2239)]], - [[colorPal3(2240), colorPal13(2241), colorPal5(2242), colorPal5(2243), colorPal5(2244), colorPal7(2245), colorPal11(2246)], [colorPal3(2247), colorPal9(2248), colorPal11(2249)], [colorPal3(2250), colorPal5(2251)], [colorPal3(2252), colorPal5(2253)]], - [[colorPal3(2254), colorPal13(2255), colorPal5(2256), colorPal5(2257), colorPal5(2258), colorPal7(2259), colorPal11(2260)], [colorPal3(2261), colorPal9(2262), colorPal11(2263)], [colorPal3(2264), colorPal5(2265)], [colorPal3(2266), colorPal5(2267)]], - [[colorPal3(2268), colorPal13(2269), colorPal5(2270), colorPal5(2271), colorPal5(2272), colorPal7(2273), colorPal11(2274)], [colorPal3(2275), colorPal9(2276), colorPal11(2277)], [colorPal3(2278), colorPal5(2279)], [colorPal3(2280), colorPal5(2281)]], - [[colorPal3(2282), colorPal13(2283), colorPal5(2284), colorPal5(2285), colorPal5(2286), colorPal7(2287), colorPal11(2288)], [colorPal3(2289), colorPal9(2290), colorPal11(2291)], [colorPal3(2292), colorPal5(2293)], [colorPal3(2294), colorPal5(2295)]], - [[colorPal3(2296), colorPal13(2297), colorPal5(2298), colorPal5(2299), colorPal5(2300), colorPal7(2301), colorPal11(2302)], [colorPal3(2303), colorPal9(2304), colorPal11(2305)], [colorPal3(2306), colorPal5(2307)], [colorPal3(2308), colorPal5(2309)]], - [[colorPal3(2310), colorPal13(2311), colorPal5(2312), colorPal5(2313), colorPal5(2314), colorPal7(2315), colorPal11(2316)], [colorPal3(2317), colorPal9(2318), colorPal11(2319)], [colorPal3(2320), colorPal5(2321)], [colorPal3(2322), colorPal5(2323)]], - [[colorPal3(2324), colorPal13(2325), colorPal5(2326), colorPal5(2327), colorPal5(2328), colorPal7(2329), colorPal11(2330)], [colorPal3(2331), colorPal9(2332), colorPal11(2333)], [colorPal3(2334), colorPal5(2335)], [colorPal3(2336), colorPal5(2337)]], - [[colorPal3(2338), colorPal13(2339), colorPal5(2340), colorPal5(2341), colorPal5(2342), colorPal7(2343), colorPal11(2344)], [colorPal3(2345), colorPal9(2346), colorPal11(2347)], [colorPal3(2348), colorPal5(2349)], [colorPal3(2350), colorPal5(2351)]], - [[colorPal3(2352), colorPal13(2353), colorPal5(2354), colorPal5(2355), colorPal5(2356), colorPal7(2357), colorPal11(2358)], [colorPal3(2359), colorPal9(2360), colorPal11(2361)], [colorPal3(2362), colorPal5(2363)], [colorPal3(2364), colorPal5(2365)]], - [[colorPal3(2366), colorPal13(2367), colorPal5(2368), colorPal5(2369), colorPal5(2370), colorPal7(2371), colorPal11(2372)], [colorPal3(2373), colorPal9(2374), colorPal11(2375)], [colorPal3(2376), colorPal5(2377)], [colorPal3(2378), colorPal5(2379)]], - [[colorPal3(2380), colorPal13(2381), colorPal5(2382), colorPal5(2383), colorPal5(2384), colorPal7(2385), colorPal11(2386)], [colorPal3(2387), colorPal9(2388), colorPal11(2389)], [colorPal3(2390), colorPal5(2391)], [colorPal3(2392), colorPal5(2393)]], - [[colorPal3(2394), colorPal13(2395), colorPal5(2396), colorPal5(2397), colorPal5(2398), colorPal7(2399), colorPal11(2400)], [colorPal3(2401), colorPal9(2402), colorPal11(2403)], [colorPal3(2404), colorPal5(2405)], [colorPal3(2406), colorPal5(2407)]], - [[colorPal3(2408), colorPal13(2409), colorPal5(2410), colorPal5(2411), colorPal5(2412), colorPal7(2413), colorPal11(2414)], [colorPal3(2415), colorPal9(2416), colorPal11(2417)], [colorPal3(2418), colorPal5(2419)], [colorPal3(2420), colorPal5(2421)]], - [[colorPal3(2422), colorPal13(2423), colorPal5(2424), colorPal5(2425), colorPal5(2426), colorPal7(2427), colorPal11(2428)], [colorPal3(2429), colorPal9(2430), colorPal11(2431)], [colorPal3(2432), colorPal5(2433)], [colorPal3(2434), colorPal5(2435)]], - [[colorPal3(2436), colorPal13(2437), colorPal5(2438), colorPal5(2439), colorPal5(2440), colorPal7(2441), colorPal11(2442)], [colorPal3(2443), colorPal9(2444), colorPal11(2445)], [colorPal3(2446), colorPal5(2447)], [colorPal3(2448), colorPal5(2449)]], - [[colorPal3(2450), colorPal13(2451), colorPal5(2452), colorPal5(2453), colorPal5(2454), colorPal7(2455), colorPal11(2456)], [colorPal3(2457), colorPal9(2458), colorPal11(2459)], [colorPal3(2460), colorPal5(2461)], [colorPal3(2462), colorPal5(2463)]], - [[colorPal3(2464), colorPal13(2465), colorPal5(2466), colorPal5(2467), colorPal5(2468), colorPal7(2469), colorPal11(2470)], [colorPal3(2471), colorPal9(2472), colorPal11(2473)], [colorPal3(2474), colorPal5(2475)], [colorPal3(2476), colorPal5(2477)]] + undefined, + [[colorPal3(1946), colorPal13(1947), colorPal5(1948), colorPal5(1949), colorPal5(1950), colorPal7(1951), colorPal11(1952)], [colorPal3(1953), colorPal9(1954), colorPal11(1955)], [colorPal3(1956), colorPal5(1957)], [colorPal3(1958), colorPal5(1959)]], + [[colorPal3(1960), colorPal13(1961), colorPal5(1962), colorPal5(1963), colorPal5(1964), colorPal7(1965), colorPal11(1966)], [colorPal3(1967), colorPal9(1968), colorPal11(1969)], [colorPal3(1970), colorPal5(1971)], [colorPal3(1972), colorPal5(1973)]], + [[colorPal3(1974), colorPal13(1975), colorPal5(1976), colorPal5(1977), colorPal5(1978), colorPal7(1979), colorPal11(1980)], [colorPal3(1981), colorPal9(1982), colorPal11(1983)], [colorPal3(1984), colorPal5(1985)], [colorPal3(1986), colorPal5(1987)]], + [[colorPal3(1988), colorPal13(1989), colorPal5(1990), colorPal5(1991), colorPal5(1992), colorPal7(1993), colorPal11(1994)], [colorPal3(1995), colorPal9(1996), colorPal11(1997)], [colorPal3(1998), colorPal5(1999)], [colorPal3(2000), colorPal5(2001)]], + [[colorPal3(2002), colorPal13(2003), colorPal5(2004), colorPal5(2005), colorPal5(2006), colorPal7(2007), colorPal11(2008)], [colorPal3(2009), colorPal9(2010), colorPal11(2011)], [colorPal3(2012), colorPal5(2013)], [colorPal3(2014), colorPal5(2015)]], + [[colorPal3(2016), colorPal13(2017), colorPal5(2018), colorPal5(2019), colorPal5(2020), colorPal7(2021), colorPal11(2022)], [colorPal3(2023), colorPal9(2024), colorPal11(2025)], [colorPal3(2026), colorPal5(2027)], [colorPal3(2028), colorPal5(2029)]], + [[colorPal3(2030), colorPal13(2031), colorPal5(2032), colorPal5(2033), colorPal5(2034), colorPal7(2035), colorPal11(2036)], [colorPal3(2037), colorPal9(2038), colorPal11(2039)], [colorPal3(2040), colorPal5(2041)], [colorPal3(2042), colorPal5(2043)]], + [[colorPal3(2044), colorPal13(2045), colorPal5(2046), colorPal5(2047), colorPal5(2048), colorPal7(2049), colorPal11(2050)], [colorPal3(2051), colorPal9(2052), colorPal11(2053)], [colorPal3(2054), colorPal5(2055)], [colorPal3(2056), colorPal5(2057)]], + [[colorPal3(2058), colorPal13(2059), colorPal5(2060), colorPal5(2061), colorPal5(2062), colorPal7(2063), colorPal11(2064)], [colorPal3(2065), colorPal9(2066), colorPal11(2067)], [colorPal3(2068), colorPal5(2069)], [colorPal3(2070), colorPal5(2071)]], + [[colorPal3(2072), colorPal13(2073), colorPal5(2074), colorPal5(2075), colorPal5(2076), colorPal7(2077), colorPal11(2078)], [colorPal3(2079), colorPal9(2080), colorPal11(2081)], [colorPal3(2082), colorPal5(2083)], [colorPal3(2084), colorPal5(2085)]], + [[colorPal3(2086), colorPal13(2087), colorPal5(2088), colorPal5(2089), colorPal5(2090), colorPal7(2091), colorPal11(2092)], [colorPal3(2093), colorPal9(2094), colorPal11(2095)], [colorPal3(2096), colorPal5(2097)], [colorPal3(2098), colorPal5(2099)]], + [[colorPal3(2100), colorPal13(2101), colorPal5(2102), colorPal5(2103), colorPal5(2104), colorPal7(2105), colorPal11(2106)], [colorPal3(2107), colorPal9(2108), colorPal11(2109)], [colorPal3(2110), colorPal5(2111)], [colorPal3(2112), colorPal5(2113)]], + [[colorPal3(2114), colorPal13(2115), colorPal5(2116), colorPal5(2117), colorPal5(2118), colorPal7(2119), colorPal11(2120)], [colorPal3(2121), colorPal9(2122), colorPal11(2123)], [colorPal3(2124), colorPal5(2125)], [colorPal3(2126), colorPal5(2127)]], + [[colorPal3(2128), colorPal13(2129), colorPal5(2130), colorPal5(2131), colorPal5(2132), colorPal7(2133), colorPal11(2134)], [colorPal3(2135), colorPal9(2136), colorPal11(2137)], [colorPal3(2138), colorPal5(2139)], [colorPal3(2140), colorPal5(2141)]], + [[colorPal3(2142), colorPal13(2143), colorPal5(2144), colorPal5(2145), colorPal5(2146), colorPal7(2147), colorPal11(2148)], [colorPal3(2149), colorPal9(2150), colorPal11(2151)], [colorPal3(2152), colorPal5(2153)], [colorPal3(2154), colorPal5(2155)]], + [[colorPal3(2156), colorPal13(2157), colorPal5(2158), colorPal5(2159), colorPal5(2160), colorPal7(2161), colorPal11(2162)], [colorPal3(2163), colorPal9(2164), colorPal11(2165)], [colorPal3(2166), colorPal5(2167)], [colorPal3(2168), colorPal5(2169)]], + [[colorPal3(2170), colorPal13(2171), colorPal5(2172), colorPal5(2173), colorPal5(2174), colorPal7(2175), colorPal11(2176)], [colorPal3(2177), colorPal9(2178), colorPal11(2179)], [colorPal3(2180), colorPal5(2181)], [colorPal3(2182), colorPal5(2183)]], + [[colorPal3(2184), colorPal13(2185), colorPal5(2186), colorPal5(2187), colorPal5(2188), colorPal7(2189), colorPal11(2190)], [colorPal3(2191), colorPal9(2192), colorPal11(2193)], [colorPal3(2194), colorPal5(2195)], [colorPal3(2196), colorPal5(2197)]], + [[colorPal3(2198), colorPal13(2199), colorPal5(2200), colorPal5(2201), colorPal5(2202), colorPal7(2203), colorPal11(2204)], [colorPal3(2205), colorPal9(2206), colorPal11(2207)], [colorPal3(2208), colorPal5(2209)], [colorPal3(2210), colorPal5(2211)]], + [[colorPal3(2212), colorPal13(2213), colorPal5(2214), colorPal5(2215), colorPal5(2216), colorPal7(2217), colorPal11(2218)], [colorPal3(2219), colorPal9(2220), colorPal11(2221)], [colorPal3(2222), colorPal5(2223)], [colorPal3(2224), colorPal5(2225)]], + [[colorPal3(2226), colorPal13(2227), colorPal5(2228), colorPal5(2229), colorPal5(2230), colorPal7(2231), colorPal11(2232)], [colorPal3(2233), colorPal9(2234), colorPal11(2235)], [colorPal3(2236), colorPal5(2237)], [colorPal3(2238), colorPal5(2239)]], + [[colorPal3(2240), colorPal13(2241), colorPal5(2242), colorPal5(2243), colorPal5(2244), colorPal7(2245), colorPal11(2246)], [colorPal3(2247), colorPal9(2248), colorPal11(2249)], [colorPal3(2250), colorPal5(2251)], [colorPal3(2252), colorPal5(2253)]], + [[colorPal3(2254), colorPal13(2255), colorPal5(2256), colorPal5(2257), colorPal5(2258), colorPal7(2259), colorPal11(2260)], [colorPal3(2261), colorPal9(2262), colorPal11(2263)], [colorPal3(2264), colorPal5(2265)], [colorPal3(2266), colorPal5(2267)]], + [[colorPal3(2268), colorPal13(2269), colorPal5(2270), colorPal5(2271), colorPal5(2272), colorPal7(2273), colorPal11(2274)], [colorPal3(2275), colorPal9(2276), colorPal11(2277)], [colorPal3(2278), colorPal5(2279)], [colorPal3(2280), colorPal5(2281)]], + [[colorPal3(2282), colorPal13(2283), colorPal5(2284), colorPal5(2285), colorPal5(2286), colorPal7(2287), colorPal11(2288)], [colorPal3(2289), colorPal9(2290), colorPal11(2291)], [colorPal3(2292), colorPal5(2293)], [colorPal3(2294), colorPal5(2295)]], + [[colorPal3(2296), colorPal13(2297), colorPal5(2298), colorPal5(2299), colorPal5(2300), colorPal7(2301), colorPal11(2302)], [colorPal3(2303), colorPal9(2304), colorPal11(2305)], [colorPal3(2306), colorPal5(2307)], [colorPal3(2308), colorPal5(2309)]], + [[colorPal3(2310), colorPal13(2311), colorPal5(2312), colorPal5(2313), colorPal5(2314), colorPal7(2315), colorPal11(2316)], [colorPal3(2317), colorPal9(2318), colorPal11(2319)], [colorPal3(2320), colorPal5(2321)], [colorPal3(2322), colorPal5(2323)]], + [[colorPal3(2324), colorPal13(2325), colorPal5(2326), colorPal5(2327), colorPal5(2328), colorPal7(2329), colorPal11(2330)], [colorPal3(2331), colorPal9(2332), colorPal11(2333)], [colorPal3(2334), colorPal5(2335)], [colorPal3(2336), colorPal5(2337)]], + [[colorPal3(2338), colorPal13(2339), colorPal5(2340), colorPal5(2341), colorPal5(2342), colorPal7(2343), colorPal11(2344)], [colorPal3(2345), colorPal9(2346), colorPal11(2347)], [colorPal3(2348), colorPal5(2349)], [colorPal3(2350), colorPal5(2351)]], + [[colorPal3(2352), colorPal13(2353), colorPal5(2354), colorPal5(2355), colorPal5(2356), colorPal7(2357), colorPal11(2358)], [colorPal3(2359), colorPal9(2360), colorPal11(2361)], [colorPal3(2362), colorPal5(2363)], [colorPal3(2364), colorPal5(2365)]], + [[colorPal3(2366), colorPal13(2367), colorPal5(2368), colorPal5(2369), colorPal5(2370), colorPal7(2371), colorPal11(2372)], [colorPal3(2373), colorPal9(2374), colorPal11(2375)], [colorPal3(2376), colorPal5(2377)], [colorPal3(2378), colorPal5(2379)]], + [[colorPal3(2380), colorPal13(2381), colorPal5(2382), colorPal5(2383), colorPal5(2384), colorPal7(2385), colorPal11(2386)], [colorPal3(2387), colorPal9(2388), colorPal11(2389)], [colorPal3(2390), colorPal5(2391)], [colorPal3(2392), colorPal5(2393)]], + [[colorPal3(2394), colorPal13(2395), colorPal5(2396), colorPal5(2397), colorPal5(2398), colorPal7(2399), colorPal11(2400)], [colorPal3(2401), colorPal9(2402), colorPal11(2403)], [colorPal3(2404), colorPal5(2405)], [colorPal3(2406), colorPal5(2407)]], + [[colorPal3(2408), colorPal13(2409), colorPal5(2410), colorPal5(2411), colorPal5(2412), colorPal7(2413), colorPal11(2414)], [colorPal3(2415), colorPal9(2416), colorPal11(2417)], [colorPal3(2418), colorPal5(2419)], [colorPal3(2420), colorPal5(2421)]], + [[colorPal3(2422), colorPal13(2423), colorPal5(2424), colorPal5(2425), colorPal5(2426), colorPal7(2427), colorPal11(2428)], [colorPal3(2429), colorPal9(2430), colorPal11(2431)], [colorPal3(2432), colorPal5(2433)], [colorPal3(2434), colorPal5(2435)]], + [[colorPal3(2436), colorPal13(2437), colorPal5(2438), colorPal5(2439), colorPal5(2440), colorPal7(2441), colorPal11(2442)], [colorPal3(2443), colorPal9(2444), colorPal11(2445)], [colorPal3(2446), colorPal5(2447)], [colorPal3(2448), colorPal5(2449)]], + [[colorPal3(2450), colorPal13(2451), colorPal5(2452), colorPal5(2453), colorPal5(2454), colorPal7(2455), colorPal11(2456)], [colorPal3(2457), colorPal9(2458), colorPal11(2459)], [colorPal3(2460), colorPal5(2461)], [colorPal3(2462), colorPal5(2463)]], + [[colorPal3(2464), colorPal13(2465), colorPal5(2466), colorPal5(2467), colorPal5(2468), colorPal7(2469), colorPal11(2470)], [colorPal3(2471), colorPal9(2472), colorPal11(2473)], [colorPal3(2474), colorPal5(2475)], [colorPal3(2476), colorPal5(2477)]] ]; export const backLegs: AnimatedSprites = [ - undefined, - [[colorPal3(2478)]], - [[colorPal3(2479)]], - [[colorPal3(2480)]], - [[colorPal3(2481)]], - [[colorPal3(2482)]], - [[colorPal3(2483)]], - [[colorPal3(2484)]], - [[colorPal3(2485)]], - [[colorPal3(2486)]], - [[colorPal3(2487)]], - [[colorPal3(2488)]], - [[colorPal3(2489)]], - [[colorPal3(2490)]], - [[colorPal3(2491)]], - [[colorPal3(2492)]], - [[colorPal3(2493)]], - [[colorPal3(2494)]], - [[colorPal3(2495)]], - [[colorPal3(2496)]], - [[colorPal3(2497)]], - [[colorPal3(2498)]], - [[colorPal3(2499)]], - [[colorPal3(2500)]], - [[colorPal3(2501)]], - [[colorPal3(2502)]], - [[colorPal3(2503)]] + undefined, + [[colorPal3(2478)]], + [[colorPal3(2479)]], + [[colorPal3(2480)]], + [[colorPal3(2481)]], + [[colorPal3(2482)]], + [[colorPal3(2483)]], + [[colorPal3(2484)]], + [[colorPal3(2485)]], + [[colorPal3(2486)]], + [[colorPal3(2487)]], + [[colorPal3(2488)]], + [[colorPal3(2489)]], + [[colorPal3(2490)]], + [[colorPal3(2491)]], + [[colorPal3(2492)]], + [[colorPal3(2493)]], + [[colorPal3(2494)]], + [[colorPal3(2495)]], + [[colorPal3(2496)]], + [[colorPal3(2497)]], + [[colorPal3(2498)]], + [[colorPal3(2499)]], + [[colorPal3(2500)]], + [[colorPal3(2501)]], + [[colorPal3(2502)]], + [[colorPal3(2503)]] ]; export const backLegs2: AnimatedSprites = [ - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - [[colorPal3(2504)]], - [[colorPal3(2505)]], - [[colorPal3(2506)]], - [[colorPal3(2507)]], - [[colorPal3(2508)]] + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [[colorPal3(2504)]], + [[colorPal3(2505)]], + [[colorPal3(2506)]], + [[colorPal3(2507)]], + [[colorPal3(2508)]] ]; export const backLegHooves: AnimatedSprites = [ - undefined, - [undefined, [colorPal3(2509)], [colorPal3(2510)], [colorPal5(2511)], [colorPal3(2512)]], - [undefined, [colorPal3(2513)], [colorPal3(2514)], [colorPal5(2515)], [colorPal3(2516)]], - [undefined, [colorPal3(2517)], [colorPal3(2518)], [colorPal5(2519)], [colorPal3(2520)]], - [undefined, [colorPal3(2521)], [colorPal3(2522)], [colorPal5(2523)], [colorPal3(2524)]], - [undefined, [colorPal3(2525)], [colorPal3(2526)], [colorPal5(2527)], [colorPal3(2528)]], - [undefined, [colorPal3(2529)], [colorPal3(2530)], [colorPal5(2531)], [colorPal3(2532)]], - [undefined, [colorPal3(2533)], [colorPal3(2534)], [colorPal5(2535)], [colorPal3(2536)]], - [undefined, [colorPal3(2537)], [colorPal3(2538)], [colorPal5(2539)], [colorPal3(2540)]], - [undefined, [colorPal3(2541)], [colorPal3(2542)], [colorPal5(2543)], [colorPal3(2544)]], - [undefined, [colorPal3(2545)], [colorPal3(2546)], [colorPal5(2547)], [colorPal3(2548)]], - [undefined, [colorPal3(2549)], [colorPal3(2550)], [colorPal5(2551)], [colorPal3(2552)]], - [undefined, [colorPal3(2553)], [colorPal3(2554)], [colorPal5(2555)], [colorPal3(2556)]], - [undefined, [colorPal3(2557)], [colorPal3(2558)], [colorPal5(2559)], [colorPal3(2560)]], - [undefined, [colorPal3(2561)], [colorPal3(2562)], [colorPal5(2563)], [colorPal3(2564)]], - [undefined, [colorPal3(2565)], [colorPal3(2566)], [colorPal5(2567)], [colorPal3(2568)]], - [undefined, [colorPal3(2569)], [colorPal3(2570)], [colorPal5(2571)], [colorPal3(2572)]], - [undefined, [colorPal3(2573)], [colorPal3(2574)], [colorPal5(2575)], [colorPal3(2576)]], - [undefined, [colorPal3(2577)], [colorPal3(2578)], [colorPal5(2579)], [colorPal17(2580)]], - [undefined, [colorPal3(2581)], [colorPal3(2582)], [colorPal5(2583)], [colorPal3(2584)]], - [undefined, [colorPal3(2585)], [colorPal3(2586)], [colorPal5(2587)], [colorPal3(2588)]], - [undefined, [colorPal3(2589)], [colorPal3(2590)], [colorPal5(2591)], [colorPal3(2592)]], - [undefined, [colorPal3(2593)], [colorPal3(2594)], [colorPal5(2595)], [colorPal3(2596)]], - [undefined, [colorPal3(2597)], [colorPal3(2598)], [colorPal5(2599)], [colorPal3(2600)]], - [undefined, [colorPal3(2601)], [colorPal3(2602)], [colorPal5(2603)], [colorPal3(2604)]], - [undefined, [colorPal3(2605)], [colorPal3(2606)], [colorPal5(2607)], [colorPal3(2608)]], - [undefined, [colorPal3(2609)], [colorPal3(2610)], [colorPal5(2611)], [colorPal3(2612)]] + undefined, + [undefined, [colorPal3(2509)], [colorPal3(2510)], [colorPal5(2511)], [colorPal3(2512)]], + [undefined, [colorPal3(2513)], [colorPal3(2514)], [colorPal5(2515)], [colorPal3(2516)]], + [undefined, [colorPal3(2517)], [colorPal3(2518)], [colorPal5(2519)], [colorPal3(2520)]], + [undefined, [colorPal3(2521)], [colorPal3(2522)], [colorPal5(2523)], [colorPal3(2524)]], + [undefined, [colorPal3(2525)], [colorPal3(2526)], [colorPal5(2527)], [colorPal3(2528)]], + [undefined, [colorPal3(2529)], [colorPal3(2530)], [colorPal5(2531)], [colorPal3(2532)]], + [undefined, [colorPal3(2533)], [colorPal3(2534)], [colorPal5(2535)], [colorPal3(2536)]], + [undefined, [colorPal3(2537)], [colorPal3(2538)], [colorPal5(2539)], [colorPal3(2540)]], + [undefined, [colorPal3(2541)], [colorPal3(2542)], [colorPal5(2543)], [colorPal3(2544)]], + [undefined, [colorPal3(2545)], [colorPal3(2546)], [colorPal5(2547)], [colorPal3(2548)]], + [undefined, [colorPal3(2549)], [colorPal3(2550)], [colorPal5(2551)], [colorPal3(2552)]], + [undefined, [colorPal3(2553)], [colorPal3(2554)], [colorPal5(2555)], [colorPal3(2556)]], + [undefined, [colorPal3(2557)], [colorPal3(2558)], [colorPal5(2559)], [colorPal3(2560)]], + [undefined, [colorPal3(2561)], [colorPal3(2562)], [colorPal5(2563)], [colorPal3(2564)]], + [undefined, [colorPal3(2565)], [colorPal3(2566)], [colorPal5(2567)], [colorPal3(2568)]], + [undefined, [colorPal3(2569)], [colorPal3(2570)], [colorPal5(2571)], [colorPal3(2572)]], + [undefined, [colorPal3(2573)], [colorPal3(2574)], [colorPal5(2575)], [colorPal3(2576)]], + [undefined, [colorPal3(2577)], [colorPal3(2578)], [colorPal5(2579)], [colorPal17(2580)]], + [undefined, [colorPal3(2581)], [colorPal3(2582)], [colorPal5(2583)], [colorPal3(2584)]], + [undefined, [colorPal3(2585)], [colorPal3(2586)], [colorPal5(2587)], [colorPal3(2588)]], + [undefined, [colorPal3(2589)], [colorPal3(2590)], [colorPal5(2591)], [colorPal3(2592)]], + [undefined, [colorPal3(2593)], [colorPal3(2594)], [colorPal5(2595)], [colorPal3(2596)]], + [undefined, [colorPal3(2597)], [colorPal3(2598)], [colorPal5(2599)], [colorPal3(2600)]], + [undefined, [colorPal3(2601)], [colorPal3(2602)], [colorPal5(2603)], [colorPal3(2604)]], + [undefined, [colorPal3(2605)], [colorPal3(2606)], [colorPal5(2607)], [colorPal3(2608)]], + [undefined, [colorPal3(2609)], [colorPal3(2610)], [colorPal5(2611)], [colorPal3(2612)]] ]; export const backLegHooves2: AnimatedSprites = [ - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - [undefined, [colorPal3(2613)], [colorPal3(2614)], [colorPal5(2615)], [colorPal3(2616)]], - [undefined, [colorPal3(2617)], [colorPal3(2618)], [colorPal5(2619)], [colorPal3(2620)]], - [undefined, [colorPal3(2621)], [colorPal3(2622)], [colorPal5(2623)], [colorPal3(2624)]], - [undefined, [colorPal3(2625)], [colorPal3(2626)], [colorPal5(2627)], [colorPal3(2628)]], - [undefined, [colorPal3(2629)], [colorPal3(2630)], [colorPal5(2631)], [colorPal3(2632)]] + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [undefined, [colorPal3(2613)], [colorPal3(2614)], [colorPal5(2615)], [colorPal3(2616)]], + [undefined, [colorPal3(2617)], [colorPal3(2618)], [colorPal5(2619)], [colorPal3(2620)]], + [undefined, [colorPal3(2621)], [colorPal3(2622)], [colorPal5(2623)], [colorPal3(2624)]], + [undefined, [colorPal3(2625)], [colorPal3(2626)], [colorPal5(2627)], [colorPal3(2628)]], + [undefined, [colorPal3(2629)], [colorPal3(2630)], [colorPal5(2631)], [colorPal3(2632)]] ]; export const backLegAccessories: AnimatedSprites = [ - undefined, - [undefined, [colorPal3(2633), colorPal13(2634), colorPal13(2635), colorPal13(2636), colorPal13(2637)]], - [undefined, [colorPal3(2638), colorPal13(2639), colorPal13(2640), colorPal13(2641), colorPal13(2642)]], - [undefined, [colorPal3(2643), colorPal13(2644), colorPal13(2645), colorPal13(2646), colorPal13(2647)]], - [undefined, [colorPal3(2648), colorPal13(2649), colorPal13(2650), colorPal13(2651), colorPal13(2652)]], - [undefined, [colorPal3(2653), colorPal13(2654), colorPal13(2655), colorPal13(2656), colorPal13(2657)]], - [undefined, [colorPal3(2658), colorPal13(2659), colorPal13(2660), colorPal13(2661), colorPal13(2662)]], - [undefined, [colorPal3(2663), colorPal13(2664), colorPal13(2665), colorPal13(2666), colorPal13(2667)]], - [undefined, [colorPal3(2668), colorPal13(2669), colorPal13(2670), colorPal13(2671), colorPal13(2672)]], - [undefined, [colorPal3(2673), colorPal13(2674), colorPal13(2675), colorPal13(2676), colorPal13(2677)]], - [undefined, [colorPal3(2678), colorPal13(2679), colorPal13(2680), colorPal13(2681), colorPal13(2682)]], - [undefined, [colorPal3(2683), colorPal13(2684), colorPal13(2685), colorPal13(2686), colorPal13(2687)]], - [undefined, [colorPal3(2688), colorPal13(2689), colorPal13(2690), colorPal13(2691), colorPal13(2692)]], - [undefined, [colorPal3(2693), colorPal13(2694), colorPal13(2695), colorPal13(2696), colorPal13(2697)]], - [undefined, [colorPal3(2698), colorPal13(2699), colorPal13(2700), colorPal13(2701), colorPal13(2702)]], - [undefined, [colorPal3(2703), colorPal13(2704), colorPal13(2705), colorPal13(2706), colorPal13(2707)]], - [undefined, [colorPal3(2708), colorPal13(2709), colorPal13(2710), colorPal13(2711), colorPal13(2712)]], - [undefined, [colorPal3(2713), colorPal13(2714), colorPal13(2715), colorPal13(2716), colorPal13(2717)]], - [undefined, [colorPal3(2718), colorPal13(2719), colorPal13(2720), colorPal13(2721), colorPal13(2722)]], - [undefined, [colorPal3(2723), colorPal13(2724), colorPal13(2725), colorPal13(2726), colorPal13(2727)]], - [undefined, [colorPal3(2728), colorPal13(2729), colorPal13(2730), colorPal13(2731), colorPal13(2732)]], - [undefined, [colorPal3(2733), colorPal13(2734), colorPal13(2735), colorPal13(2736), colorPal13(2737)]], - [undefined, [colorPal3(2738), colorPal13(2739), colorPal13(2740), colorPal13(2741), colorPal13(2742)]], - [undefined, [colorPal3(2743), colorPal13(2744), colorPal13(2745), colorPal13(2746), colorPal13(2747)]], - [undefined, [colorPal3(2748), colorPal13(2749), colorPal13(2750), colorPal13(2751), colorPal13(2752)]], - [undefined, [colorPal3(2753), colorPal13(2754), colorPal13(2755), colorPal13(2756), colorPal13(2757)]], - [undefined, [colorPal3(2758), colorPal13(2759), colorPal13(2760), colorPal13(2761), colorPal13(2762)]] + undefined, + [undefined, [colorPal3(2633), colorPal13(2634), colorPal13(2635), colorPal13(2636), colorPal13(2637)]], + [undefined, [colorPal3(2638), colorPal13(2639), colorPal13(2640), colorPal13(2641), colorPal13(2642)]], + [undefined, [colorPal3(2643), colorPal13(2644), colorPal13(2645), colorPal13(2646), colorPal13(2647)]], + [undefined, [colorPal3(2648), colorPal13(2649), colorPal13(2650), colorPal13(2651), colorPal13(2652)]], + [undefined, [colorPal3(2653), colorPal13(2654), colorPal13(2655), colorPal13(2656), colorPal13(2657)]], + [undefined, [colorPal3(2658), colorPal13(2659), colorPal13(2660), colorPal13(2661), colorPal13(2662)]], + [undefined, [colorPal3(2663), colorPal13(2664), colorPal13(2665), colorPal13(2666), colorPal13(2667)]], + [undefined, [colorPal3(2668), colorPal13(2669), colorPal13(2670), colorPal13(2671), colorPal13(2672)]], + [undefined, [colorPal3(2673), colorPal13(2674), colorPal13(2675), colorPal13(2676), colorPal13(2677)]], + [undefined, [colorPal3(2678), colorPal13(2679), colorPal13(2680), colorPal13(2681), colorPal13(2682)]], + [undefined, [colorPal3(2683), colorPal13(2684), colorPal13(2685), colorPal13(2686), colorPal13(2687)]], + [undefined, [colorPal3(2688), colorPal13(2689), colorPal13(2690), colorPal13(2691), colorPal13(2692)]], + [undefined, [colorPal3(2693), colorPal13(2694), colorPal13(2695), colorPal13(2696), colorPal13(2697)]], + [undefined, [colorPal3(2698), colorPal13(2699), colorPal13(2700), colorPal13(2701), colorPal13(2702)]], + [undefined, [colorPal3(2703), colorPal13(2704), colorPal13(2705), colorPal13(2706), colorPal13(2707)]], + [undefined, [colorPal3(2708), colorPal13(2709), colorPal13(2710), colorPal13(2711), colorPal13(2712)]], + [undefined, [colorPal3(2713), colorPal13(2714), colorPal13(2715), colorPal13(2716), colorPal13(2717)]], + [undefined, [colorPal3(2718), colorPal13(2719), colorPal13(2720), colorPal13(2721), colorPal13(2722)]], + [undefined, [colorPal3(2723), colorPal13(2724), colorPal13(2725), colorPal13(2726), colorPal13(2727)]], + [undefined, [colorPal3(2728), colorPal13(2729), colorPal13(2730), colorPal13(2731), colorPal13(2732)]], + [undefined, [colorPal3(2733), colorPal13(2734), colorPal13(2735), colorPal13(2736), colorPal13(2737)]], + [undefined, [colorPal3(2738), colorPal13(2739), colorPal13(2740), colorPal13(2741), colorPal13(2742)]], + [undefined, [colorPal3(2743), colorPal13(2744), colorPal13(2745), colorPal13(2746), colorPal13(2747)]], + [undefined, [colorPal3(2748), colorPal13(2749), colorPal13(2750), colorPal13(2751), colorPal13(2752)]], + [undefined, [colorPal3(2753), colorPal13(2754), colorPal13(2755), colorPal13(2756), colorPal13(2757)]], + [undefined, [colorPal3(2758), colorPal13(2759), colorPal13(2760), colorPal13(2761), colorPal13(2762)]] ]; export const backLegAccessories2: AnimatedSprites = [ - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - [undefined, [colorPal3(2763), colorPal13(2764), colorPal13(2765), colorPal13(2766), colorPal13(2767)]], - [undefined, [colorPal3(2768), colorPal13(2769), colorPal13(2770), colorPal13(2771), colorPal13(2772)]], - [undefined, [colorPal3(2773), colorPal13(2774), colorPal13(2775), colorPal13(2776), colorPal13(2777)]], - [undefined, [colorPal3(2778), colorPal13(2779), colorPal13(2780), colorPal13(2781), colorPal13(2782)]], - [undefined, [colorPal3(2783), colorPal13(2784), colorPal13(2785), colorPal13(2786), colorPal13(2787)]] + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [undefined, [colorPal3(2763), colorPal13(2764), colorPal13(2765), colorPal13(2766), colorPal13(2767)]], + [undefined, [colorPal3(2768), colorPal13(2769), colorPal13(2770), colorPal13(2771), colorPal13(2772)]], + [undefined, [colorPal3(2773), colorPal13(2774), colorPal13(2775), colorPal13(2776), colorPal13(2777)]], + [undefined, [colorPal3(2778), colorPal13(2779), colorPal13(2780), colorPal13(2781), colorPal13(2782)]], + [undefined, [colorPal3(2783), colorPal13(2784), colorPal13(2785), colorPal13(2786), colorPal13(2787)]] ]; export const backLegSleeves: AnimatedSprites = [ - undefined, - [[colorPal3(2788), colorPal13(2789)]], - [[colorPal3(2790), colorPal13(2791)]], - [[colorPal3(2792), colorPal13(2793)]], - [[colorPal3(2794), colorPal13(2795)]], - [[colorPal3(2796), colorPal13(2797)]], - [[colorPal3(2798), colorPal13(2799)]], - [[colorPal3(2800), colorPal13(2801)]], - [[colorPal3(2802), colorPal13(2803)]], - [[colorPal3(2804), colorPal13(2805)]], - [[colorPal3(2806), colorPal13(2807)]], - [[colorPal3(2808), colorPal13(2809)]], - [[colorPal3(2810), colorPal13(2811)]], - [[colorPal3(2812), colorPal13(2813)]], - [[colorPal3(2814), colorPal13(2815)]], - [[colorPal3(2816), colorPal13(2817)]], - [[colorPal3(2818), colorPal13(2819)]], - [[colorPal3(2820), colorPal13(2821)]], - [[colorPal3(2822), colorPal13(2823)]], - [[colorPal3(2824), colorPal13(2825)]], - [[colorPal3(2826), colorPal13(2827)]], - [[colorPal3(2828), colorPal13(2829)]], - [[colorPal3(2830), colorPal13(2831)]], - [[colorPal3(2832), colorPal13(2833)]], - [[colorPal3(2834), colorPal13(2835)]], - [[colorPal3(2836), colorPal13(2837)]], - [[colorPal3(2838), colorPal13(2839)]] + undefined, + [[colorPal3(2788), colorPal13(2789)]], + [[colorPal3(2790), colorPal13(2791)]], + [[colorPal3(2792), colorPal13(2793)]], + [[colorPal3(2794), colorPal13(2795)]], + [[colorPal3(2796), colorPal13(2797)]], + [[colorPal3(2798), colorPal13(2799)]], + [[colorPal3(2800), colorPal13(2801)]], + [[colorPal3(2802), colorPal13(2803)]], + [[colorPal3(2804), colorPal13(2805)]], + [[colorPal3(2806), colorPal13(2807)]], + [[colorPal3(2808), colorPal13(2809)]], + [[colorPal3(2810), colorPal13(2811)]], + [[colorPal3(2812), colorPal13(2813)]], + [[colorPal3(2814), colorPal13(2815)]], + [[colorPal3(2816), colorPal13(2817)]], + [[colorPal3(2818), colorPal13(2819)]], + [[colorPal3(2820), colorPal13(2821)]], + [[colorPal3(2822), colorPal13(2823)]], + [[colorPal3(2824), colorPal13(2825)]], + [[colorPal3(2826), colorPal13(2827)]], + [[colorPal3(2828), colorPal13(2829)]], + [[colorPal3(2830), colorPal13(2831)]], + [[colorPal3(2832), colorPal13(2833)]], + [[colorPal3(2834), colorPal13(2835)]], + [[colorPal3(2836), colorPal13(2837)]], + [[colorPal3(2838), colorPal13(2839)]] ]; export const backLegSleeves2: AnimatedSprites = [ - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - [[colorPal3(2840), colorPal13(2841)]], - [[colorPal3(2842), colorPal13(2843)]], - [[colorPal3(2844), colorPal13(2845)]], - [[colorPal3(2846), colorPal13(2847)]], - [[colorPal3(2848), colorPal13(2849)]] + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [[colorPal3(2840), colorPal13(2841)]], + [[colorPal3(2842), colorPal13(2843)]], + [[colorPal3(2844), colorPal13(2845)]], + [[colorPal3(2846), colorPal13(2847)]], + [[colorPal3(2848), colorPal13(2849)]] ]; export const body: AnimatedSprites = [ - undefined, - [[colorPal3(2850), colorPal5(2851)]], - [[colorPal3(2852), colorPal5(2853)]], - [[colorPal3(2854), colorPal5(2855)]], - [[colorPal3(2856), colorPal5(2857)]], - [[colorPal3(2858), colorPal5(2859)]], - [[colorPal3(2860), colorPal5(2861)]], - [[colorPal3(2862), colorPal5(2863)]], - [[colorPal3(2864), colorPal5(2865)]], - [[colorPal3(2866), colorPal5(2867)]], - [[colorPal3(2868), colorPal5(2869)]], - [[colorPal3(2870), colorPal5(2871)]], - [[colorPal3(2872), colorPal5(2873)]], - [[colorPal3(2874), colorPal5(2875)]], - [[colorPal3(2876), colorPal5(2877)]], - [[colorPal3(2878), colorPal5(2879)]] + undefined, + [[colorPal3(2850), colorPal5(2851)]], + [[colorPal3(2852), colorPal5(2853)]], + [[colorPal3(2854), colorPal5(2855)]], + [[colorPal3(2856), colorPal5(2857)]], + [[colorPal3(2858), colorPal5(2859)]], + [[colorPal3(2860), colorPal5(2861)]], + [[colorPal3(2862), colorPal5(2863)]], + [[colorPal3(2864), colorPal5(2865)]], + [[colorPal3(2866), colorPal5(2867)]], + [[colorPal3(2868), colorPal5(2869)]], + [[colorPal3(2870), colorPal5(2871)]], + [[colorPal3(2872), colorPal5(2873)]], + [[colorPal3(2874), colorPal5(2875)]], + [[colorPal3(2876), colorPal5(2877)]], + [[colorPal3(2878), colorPal5(2879)]] ]; export const wings: AnimatedSprites = [ - [undefined, [colorPal3(2880), colorPal5(2881), colorPal9(2882)], [colorPal5(2883)], [colorPal3(2884), colorPal5(2885), colorPal9(2886)], [colorPal3(2887)]], - [undefined, [colorPal3(2888), colorPal5(2889), colorPal9(2890)], [colorPal5(2891)], [colorPal3(2892), colorPal5(2893), colorPal9(2894)], [colorPal3(2895)]], - [undefined, [colorPal3(2896), colorPal5(2897), colorPal9(2898)], [colorPal5(2899)], [colorPal3(2900), colorPal5(2901), colorPal9(2902)], [colorPal3(2903)]], - [undefined, [colorPal3(2904), colorPal5(2905), colorPal9(2906)], [colorPal5(2907)], [colorPal3(2908), colorPal5(2909), colorPal9(2910)], [colorPal3(2911)]], - [undefined, [colorPal3(2912), colorPal5(2913), colorPal9(2914)], [colorPal5(2915)], [colorPal3(2916), colorPal5(2917), colorPal9(2918)], [colorPal3(2919)]], - [undefined, [colorPal3(2920), colorPal5(2921), colorPal9(2922)], [colorPal5(2923)], [colorPal3(2924), colorPal5(2925), colorPal9(2926)], [colorPal3(2927)]], - [undefined, [colorPal3(2928), colorPal5(2929), colorPal9(2930)], [colorPal5(2931)], [colorPal3(2932), colorPal5(2933), colorPal9(2934)]], - [undefined, [colorPal3(2935), colorPal5(2936), colorPal9(2937)], [colorPal5(2938)], [colorPal3(2939), colorPal5(2940), colorPal9(2941)]], - [undefined, [colorPal3(2942), colorPal5(2943), colorPal9(2944)], [colorPal5(2945)], [colorPal3(2946), colorPal5(2947), colorPal9(2948)]], - [undefined, [colorPal3(2949), colorPal5(2950), colorPal9(2951)], [colorPal5(2952)], [colorPal3(2953), colorPal5(2954), colorPal9(2955)]], - [undefined, [colorPal3(2956), colorPal5(2957), colorPal9(2958)], [colorPal5(2959)], [colorPal3(2960), colorPal5(2961), colorPal9(2962)]], - [undefined, [colorPal3(2963), colorPal5(2964), colorPal9(2965)], [colorPal5(2966)], [colorPal3(2967), colorPal5(2968), colorPal9(2969)]], - [undefined, [colorPal3(2970), colorPal5(2971), colorPal9(2972)], [colorPal5(2973)], [colorPal3(2974), colorPal5(2975), colorPal9(2976)]] + [undefined, [colorPal3(2880), colorPal5(2881), colorPal9(2882)], [colorPal5(2883)], [colorPal3(2884), colorPal5(2885), colorPal9(2886)], [colorPal3(2887)]], + [undefined, [colorPal3(2888), colorPal5(2889), colorPal9(2890)], [colorPal5(2891)], [colorPal3(2892), colorPal5(2893), colorPal9(2894)], [colorPal3(2895)]], + [undefined, [colorPal3(2896), colorPal5(2897), colorPal9(2898)], [colorPal5(2899)], [colorPal3(2900), colorPal5(2901), colorPal9(2902)], [colorPal3(2903)]], + [undefined, [colorPal3(2904), colorPal5(2905), colorPal9(2906)], [colorPal5(2907)], [colorPal3(2908), colorPal5(2909), colorPal9(2910)], [colorPal3(2911)]], + [undefined, [colorPal3(2912), colorPal5(2913), colorPal9(2914)], [colorPal5(2915)], [colorPal3(2916), colorPal5(2917), colorPal9(2918)], [colorPal3(2919)]], + [undefined, [colorPal3(2920), colorPal5(2921), colorPal9(2922)], [colorPal5(2923)], [colorPal3(2924), colorPal5(2925), colorPal9(2926)], [colorPal3(2927)]], + [undefined, [colorPal3(2928), colorPal5(2929), colorPal9(2930)], [colorPal5(2931)], [colorPal3(2932), colorPal5(2933), colorPal9(2934)]], + [undefined, [colorPal3(2935), colorPal5(2936), colorPal9(2937)], [colorPal5(2938)], [colorPal3(2939), colorPal5(2940), colorPal9(2941)]], + [undefined, [colorPal3(2942), colorPal5(2943), colorPal9(2944)], [colorPal5(2945)], [colorPal3(2946), colorPal5(2947), colorPal9(2948)]], + [undefined, [colorPal3(2949), colorPal5(2950), colorPal9(2951)], [colorPal5(2952)], [colorPal3(2953), colorPal5(2954), colorPal9(2955)]], + [undefined, [colorPal3(2956), colorPal5(2957), colorPal9(2958)], [colorPal5(2959)], [colorPal3(2960), colorPal5(2961), colorPal9(2962)]], + [undefined, [colorPal3(2963), colorPal5(2964), colorPal9(2965)], [colorPal5(2966)], [colorPal3(2967), colorPal5(2968), colorPal9(2969)]], + [undefined, [colorPal3(2970), colorPal5(2971), colorPal9(2972)], [colorPal5(2973)], [colorPal3(2974), colorPal5(2975), colorPal9(2976)]] ]; export const tails: AnimatedSprites = [ - [undefined, [colorPal3(2977), colorPal11(2978), colorPal13(2979), colorPal7(2980), colorPal5(2981), colorPal5(2982)], [colorPal3(2983), colorPal13(2984), colorPal9(2985), colorPal7(2986), colorPal5(2987), colorPal5(2988)], [colorPal3(2989), colorPal11(2990), colorPal9(2991), colorPal7(2992), colorPal5(2993), colorPal5(2994)], [colorPal3(2995), colorPal13(2996), colorPal11(2997), colorPal7(2998), colorPal5(2999), colorPal5(3000)], [colorPal3(3001), colorPal9(3002), colorPal13(3003), colorPal7(3004), colorPal5(3005), colorPal5(3006)], [colorPal3(3007), colorPal11(3008), colorPal13(3009), colorPal7(3010), colorPal5(3011), colorPal5(3012), colorPal5(3013)], [colorPal5(3014), colorPal13(3015), colorPal11(3016), colorPal7(3017), colorPal7(3018)], [colorPal3(3019), colorPal11(3020), colorPal13(3021), colorPal5(3022), colorPal7(3023), colorPal5(3024)], [colorPal3(3025), colorPal11(3026), colorPal11(3027), colorPal7(3028), colorPal5(3029), colorPal5(3030), colorPal11(3031)], [colorPal3(3032), colorPal11(3033), colorPal13(3034), colorPal5(3035)], [colorPal3(3036), colorPal11(3037), colorPal11(3038), colorPal7(3039), colorPal5(3040), colorPal5(3041)], [colorPal3(3042), colorPal9(3043), colorPal9(3044), colorPal7(3045), colorPal5(3046)], [colorPal3(3047), colorPal5(3048), colorPal9(3049), colorPal5(3050)], [colorPal5(3051), colorPal11(3052), colorPal9(3053), colorPal7(3054), colorPal7(3055)], [colorPal5(3056), colorPal7(3057)], [colorPal3(3058), colorPal5(3059), colorPal7(3060)], [colorPal5(3061), colorPal11(3062), colorPal9(3063), colorPal7(3064), colorPal7(3065)], [colorPal7(3066), colorPal13(3067), colorPal11(3068), colorPal9(3069), colorPal9(3070)], [colorPal5(3071), colorPal7(3072)], [colorPal3(3073), colorPal11(3074), colorPal13(3075), colorPal7(3076), colorPal5(3077), colorPal5(3078)], [colorPal3(3079), colorPal9(3080), colorPal11(3081), colorPal7(3082), colorPal5(3083), colorPal5(3084)], [colorPal3(3085), colorPal11(3086), colorPal13(3087), colorPal7(3088), colorPal5(3089), colorPal5(3090)], [colorPal3(3091), colorPal9(3092), colorPal11(3093), colorPal7(3094), colorPal5(3095), colorPal5(3096)], [colorPal3(3097), colorPal13(3098), colorPal7(3099)], [colorPal3(3100), colorPal5(3101), colorPal13(3102), colorPal13(3103), colorPal11(3104), colorPal11(3105)], [colorPal3(3106), colorPal9(3107), colorPal13(3108), colorPal13(3109), colorPal13(3110), colorPal11(3111)], [colorPal3(3112), colorPal7(3113), colorPal5(3114), colorPal13(3115), colorPal5(3116)], [colorPal5(3117), colorPal13(3118), colorPal9(3119), colorPal9(3120), colorPal7(3121), colorPal7(3122)], [colorPal5(3123), colorPal13(3124), colorPal9(3125), colorPal9(3126), colorPal7(3127), colorPal7(3128)], [colorPal3(3129), colorPal5(3130), colorPal7(3131)]], - [undefined, undefined, undefined, undefined, undefined, undefined, undefined, [colorPal5(3132), colorPal13(3133), colorPal11(3134), colorPal7(3135), colorPal7(3136)], undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, [colorPal3(3137), colorPal5(3138), colorPal7(3139)], [colorPal5(3140), colorPal9(3141), colorPal9(3142), colorPal7(3143), colorPal7(3144)], [colorPal7(3145), colorPal9(3146), colorPal11(3147), colorPal9(3148), colorPal17(3149)], [colorPal5(3150), colorPal7(3151)], undefined, undefined, undefined, undefined, undefined, [colorPal3(3152), colorPal5(3153), colorPal11(3154), colorPal13(3155), colorPal11(3156), colorPal11(3157)], [colorPal3(3158), colorPal9(3159), colorPal13(3160), colorPal13(3161), colorPal13(3162), colorPal11(3163)], undefined, undefined, [colorPal5(3164), colorPal13(3165), colorPal9(3166), colorPal9(3167), colorPal7(3168), colorPal7(3169)], [colorPal3(3170), colorPal5(3171), colorPal7(3172)]], - [undefined, [colorPal3(3173), colorPal11(3174), colorPal13(3175), colorPal7(3176), colorPal5(3177), colorPal5(3178)], undefined, undefined, [colorPal3(3179), colorPal13(3180), colorPal11(3181), colorPal7(3182), colorPal5(3183), colorPal5(3184)], [colorPal3(3185), colorPal9(3186), colorPal13(3187), colorPal7(3188), colorPal5(3189), colorPal5(3190)], [colorPal3(3191), colorPal11(3192), colorPal13(3193), colorPal7(3194), colorPal5(3195), colorPal5(3196), colorPal5(3197)], [colorPal5(3198), colorPal13(3199), colorPal11(3200), colorPal7(3201), colorPal7(3202)], [colorPal3(3203), colorPal11(3204), colorPal13(3205), colorPal5(3206), colorPal7(3207), colorPal5(3208)], [colorPal3(3209), colorPal11(3210), colorPal11(3211), colorPal7(3212), colorPal5(3213), colorPal5(3214), colorPal11(3215)], [colorPal3(3216), colorPal11(3217), colorPal13(3218), colorPal5(3219)], [colorPal3(3220), colorPal11(3221), colorPal11(3222), colorPal7(3223), colorPal5(3224), colorPal5(3225)], [colorPal3(3226), colorPal9(3227), colorPal9(3228), colorPal7(3229), colorPal5(3230)], [colorPal3(3231), colorPal5(3232), colorPal9(3233), colorPal5(3234)], undefined, [colorPal5(3235), colorPal7(3236)], [colorPal3(3237), colorPal5(3238), colorPal7(3239)], [colorPal3(3240), colorPal3(3241), colorPal3(3242), colorPal3(3243), colorPal3(3244)], [colorPal5(3245), colorPal5(3246), colorPal5(3247), colorPal5(3248), colorPal5(3249)], [colorPal5(3250), colorPal7(3251)], [colorPal3(3252), colorPal11(3253), colorPal13(3254), colorPal7(3255), colorPal5(3256), colorPal5(3257)], undefined, [colorPal3(3258), colorPal11(3259), colorPal13(3260), colorPal7(3261), colorPal5(3262), colorPal5(3263)], [colorPal3(3264), colorPal9(3265), colorPal11(3266), colorPal7(3267), colorPal5(3268), colorPal5(3269)], [colorPal3(3270), colorPal13(3271), colorPal7(3272)], [colorPal3(3273), colorPal3(3274), colorPal3(3275), colorPal13(3276), colorPal11(3277), colorPal11(3278)], [colorPal3(3279), colorPal9(3280), colorPal13(3281), colorPal13(3282), colorPal13(3283), colorPal11(3284)], [colorPal3(3285), colorPal7(3286), colorPal5(3287), colorPal13(3288), colorPal5(3289)], [colorPal5(3290), colorPal13(3291), colorPal9(3292), colorPal9(3293), colorPal7(3294), colorPal7(3295)], [colorPal5(3296), colorPal13(3297), colorPal9(3298), colorPal9(3299), colorPal7(3300), colorPal7(3301)], [colorPal3(3302), colorPal5(3303), colorPal7(3304)]] + [undefined, [colorPal3(2977), colorPal11(2978), colorPal13(2979), colorPal7(2980), colorPal5(2981), colorPal5(2982)], [colorPal3(2983), colorPal13(2984), colorPal9(2985), colorPal7(2986), colorPal5(2987), colorPal5(2988)], [colorPal3(2989), colorPal11(2990), colorPal9(2991), colorPal7(2992), colorPal5(2993), colorPal5(2994)], [colorPal3(2995), colorPal13(2996), colorPal11(2997), colorPal7(2998), colorPal5(2999), colorPal5(3000)], [colorPal3(3001), colorPal9(3002), colorPal13(3003), colorPal7(3004), colorPal5(3005), colorPal5(3006)], [colorPal3(3007), colorPal11(3008), colorPal13(3009), colorPal7(3010), colorPal5(3011), colorPal5(3012), colorPal5(3013)], [colorPal5(3014), colorPal13(3015), colorPal11(3016), colorPal7(3017), colorPal7(3018)], [colorPal3(3019), colorPal11(3020), colorPal13(3021), colorPal5(3022), colorPal7(3023), colorPal5(3024)], [colorPal3(3025), colorPal11(3026), colorPal11(3027), colorPal7(3028), colorPal5(3029), colorPal5(3030), colorPal11(3031)], [colorPal3(3032), colorPal11(3033), colorPal13(3034), colorPal5(3035)], [colorPal3(3036), colorPal11(3037), colorPal11(3038), colorPal7(3039), colorPal5(3040), colorPal5(3041)], [colorPal3(3042), colorPal9(3043), colorPal9(3044), colorPal7(3045), colorPal5(3046)], [colorPal3(3047), colorPal5(3048), colorPal9(3049), colorPal5(3050)], [colorPal5(3051), colorPal11(3052), colorPal9(3053), colorPal7(3054), colorPal7(3055)], [colorPal5(3056), colorPal7(3057)], [colorPal3(3058), colorPal5(3059), colorPal7(3060)], [colorPal5(3061), colorPal11(3062), colorPal9(3063), colorPal7(3064), colorPal7(3065)], [colorPal7(3066), colorPal13(3067), colorPal11(3068), colorPal9(3069), colorPal9(3070)], [colorPal5(3071), colorPal7(3072)], [colorPal3(3073), colorPal11(3074), colorPal13(3075), colorPal7(3076), colorPal5(3077), colorPal5(3078)], [colorPal3(3079), colorPal9(3080), colorPal11(3081), colorPal7(3082), colorPal5(3083), colorPal5(3084)], [colorPal3(3085), colorPal11(3086), colorPal13(3087), colorPal7(3088), colorPal5(3089), colorPal5(3090)], [colorPal3(3091), colorPal9(3092), colorPal11(3093), colorPal7(3094), colorPal5(3095), colorPal5(3096)], [colorPal3(3097), colorPal13(3098), colorPal7(3099)], [colorPal3(3100), colorPal5(3101), colorPal13(3102), colorPal13(3103), colorPal11(3104), colorPal11(3105)], [colorPal3(3106), colorPal9(3107), colorPal13(3108), colorPal13(3109), colorPal13(3110), colorPal11(3111)], [colorPal3(3112), colorPal7(3113), colorPal5(3114), colorPal13(3115), colorPal5(3116)], [colorPal5(3117), colorPal13(3118), colorPal9(3119), colorPal9(3120), colorPal7(3121), colorPal7(3122)], [colorPal5(3123), colorPal13(3124), colorPal9(3125), colorPal9(3126), colorPal7(3127), colorPal7(3128)], [colorPal3(3129), colorPal5(3130), colorPal7(3131)]], + [undefined, undefined, undefined, undefined, undefined, undefined, undefined, [colorPal5(3132), colorPal13(3133), colorPal11(3134), colorPal7(3135), colorPal7(3136)], undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, [colorPal3(3137), colorPal5(3138), colorPal7(3139)], [colorPal5(3140), colorPal9(3141), colorPal9(3142), colorPal7(3143), colorPal7(3144)], [colorPal7(3145), colorPal9(3146), colorPal11(3147), colorPal9(3148), colorPal17(3149)], [colorPal5(3150), colorPal7(3151)], undefined, undefined, undefined, undefined, undefined, [colorPal3(3152), colorPal5(3153), colorPal11(3154), colorPal13(3155), colorPal11(3156), colorPal11(3157)], [colorPal3(3158), colorPal9(3159), colorPal13(3160), colorPal13(3161), colorPal13(3162), colorPal11(3163)], undefined, undefined, [colorPal5(3164), colorPal13(3165), colorPal9(3166), colorPal9(3167), colorPal7(3168), colorPal7(3169)], [colorPal3(3170), colorPal5(3171), colorPal7(3172)]], + [undefined, [colorPal3(3173), colorPal11(3174), colorPal13(3175), colorPal7(3176), colorPal5(3177), colorPal5(3178)], undefined, undefined, [colorPal3(3179), colorPal13(3180), colorPal11(3181), colorPal7(3182), colorPal5(3183), colorPal5(3184)], [colorPal3(3185), colorPal9(3186), colorPal13(3187), colorPal7(3188), colorPal5(3189), colorPal5(3190)], [colorPal3(3191), colorPal11(3192), colorPal13(3193), colorPal7(3194), colorPal5(3195), colorPal5(3196), colorPal5(3197)], [colorPal5(3198), colorPal13(3199), colorPal11(3200), colorPal7(3201), colorPal7(3202)], [colorPal3(3203), colorPal11(3204), colorPal13(3205), colorPal5(3206), colorPal7(3207), colorPal5(3208)], [colorPal3(3209), colorPal11(3210), colorPal11(3211), colorPal7(3212), colorPal5(3213), colorPal5(3214), colorPal11(3215)], [colorPal3(3216), colorPal11(3217), colorPal13(3218), colorPal5(3219)], [colorPal3(3220), colorPal11(3221), colorPal11(3222), colorPal7(3223), colorPal5(3224), colorPal5(3225)], [colorPal3(3226), colorPal9(3227), colorPal9(3228), colorPal7(3229), colorPal5(3230)], [colorPal3(3231), colorPal5(3232), colorPal9(3233), colorPal5(3234)], undefined, [colorPal5(3235), colorPal7(3236)], [colorPal3(3237), colorPal5(3238), colorPal7(3239)], [colorPal3(3240), colorPal3(3241), colorPal3(3242), colorPal3(3243), colorPal3(3244)], [colorPal5(3245), colorPal5(3246), colorPal5(3247), colorPal5(3248), colorPal5(3249)], [colorPal5(3250), colorPal7(3251)], [colorPal3(3252), colorPal11(3253), colorPal13(3254), colorPal7(3255), colorPal5(3256), colorPal5(3257)], undefined, [colorPal3(3258), colorPal11(3259), colorPal13(3260), colorPal7(3261), colorPal5(3262), colorPal5(3263)], [colorPal3(3264), colorPal9(3265), colorPal11(3266), colorPal7(3267), colorPal5(3268), colorPal5(3269)], [colorPal3(3270), colorPal13(3271), colorPal7(3272)], [colorPal3(3273), colorPal3(3274), colorPal3(3275), colorPal13(3276), colorPal11(3277), colorPal11(3278)], [colorPal3(3279), colorPal9(3280), colorPal13(3281), colorPal13(3282), colorPal13(3283), colorPal11(3284)], [colorPal3(3285), colorPal7(3286), colorPal5(3287), colorPal13(3288), colorPal5(3289)], [colorPal5(3290), colorPal13(3291), colorPal9(3292), colorPal9(3293), colorPal7(3294), colorPal7(3295)], [colorPal5(3296), colorPal13(3297), colorPal9(3298), colorPal9(3299), colorPal7(3300), colorPal7(3301)], [colorPal3(3302), colorPal5(3303), colorPal7(3304)]] ]; export const neckAccessories: AnimatedSprites = [ - undefined, - [undefined, [colorPal3(3305), colorPal5(3306)], [colorPal3(3307), colorPal5(3308), colorPal9(3309), colorPal5(3310)], [colorPal3(3311), colorPal5(3312), colorPal7(3313), colorPal13(3314), colorPal7(3315)], [colorPal3(3316), colorPal5(3317)], [colorPal3(3318), colorPal7(3319), colorPal7(3320), colorPal5(3321), colorPal5(3322)], [colorPal3(3323), colorPal5(3324), colorPal7(3325)], [colorPal3(3326), colorPal5(3327)], [colorPal3(3328)], [colorPal3(3329)], [colorPal3(3330), colorPal5(3331), colorPal9(3332), colorPal5(3333)], [colorPal3(3334), colorPal5(3335), colorPal9(3336)], [colorPal3(3337), colorPal9(3338)], [colorPal3(3339)], [colorPal3(3340)], [colorPal3(3341), colorPal9(3342)]], - [undefined, [colorPal3(3343), colorPal5(3344)], [colorPal3(3345), colorPal5(3346), colorPal9(3347), colorPal5(3348)], [colorPal3(3349), colorPal5(3350), colorPal7(3351), colorPal13(3352), colorPal7(3353)], [colorPal3(3354), colorPal5(3355)], [colorPal3(3356), colorPal7(3357), colorPal7(3358), colorPal5(3359), colorPal5(3360)], [colorPal3(3361), colorPal5(3362), colorPal7(3363)], [colorPal3(3364), colorPal5(3365)], [colorPal3(3366)], [colorPal3(3367)], [colorPal3(3368), colorPal5(3369), colorPal9(3370), colorPal5(3371)], [colorPal3(3372), colorPal5(3373), colorPal9(3374)], [colorPal3(3375), colorPal9(3376)], [colorPal3(3377)], [colorPal3(3378)], [colorPal3(3379), colorPal9(3380)]], - [undefined, [colorPal3(3381), colorPal5(3382)], [colorPal3(3383), colorPal5(3384), colorPal9(3385), colorPal5(3386)], [colorPal3(3387), colorPal5(3388), colorPal7(3389), colorPal13(3390), colorPal7(3391)], [colorPal3(3392), colorPal5(3393)], [colorPal3(3394), colorPal7(3395), colorPal7(3396), colorPal5(3397), colorPal5(3398)], [colorPal3(3399), colorPal5(3400), colorPal7(3401)], [colorPal3(3402), colorPal5(3403)], [colorPal3(3404)], [colorPal3(3405)], [colorPal3(3406), colorPal5(3407), colorPal9(3408), colorPal5(3409)], [colorPal3(3410), colorPal5(3411), colorPal9(3412)], [colorPal3(3413), colorPal9(3414)], [colorPal3(3415)], [colorPal3(3416)], [colorPal3(3417), colorPal9(3418)]], - [undefined, [colorPal3(3419), colorPal5(3420)], [colorPal3(3421), colorPal5(3422), colorPal9(3423), colorPal5(3424)], [colorPal3(3425), colorPal5(3426), colorPal7(3427), colorPal13(3428), colorPal7(3429)], [colorPal3(3430), colorPal5(3431)], [colorPal3(3432), colorPal7(3433), colorPal7(3434), colorPal5(3435), colorPal5(3436)], [colorPal3(3437), colorPal5(3438), colorPal7(3439)], [colorPal3(3440), colorPal5(3441)], [colorPal3(3442)], [colorPal3(3443)], [colorPal3(3444), colorPal5(3445), colorPal9(3446), colorPal5(3447)], [colorPal3(3448), colorPal5(3449), colorPal9(3450)], [colorPal3(3451), colorPal9(3452)], [colorPal3(3453)], [colorPal3(3454)], [colorPal3(3455), colorPal9(3456)]], - [undefined, [colorPal3(3457), colorPal5(3458)], [colorPal3(3459), colorPal5(3460), colorPal9(3461), colorPal5(3462)], [colorPal3(3463), colorPal5(3464), colorPal7(3465), colorPal13(3466), colorPal7(3467)], [colorPal3(3468), colorPal5(3469)], [colorPal3(3470), colorPal7(3471), colorPal7(3472), colorPal5(3473), colorPal5(3474)], [colorPal3(3475), colorPal5(3476), colorPal7(3477)], [colorPal3(3478), colorPal5(3479)], [colorPal3(3480)], [colorPal3(3481)], [colorPal3(3482), colorPal5(3483), colorPal9(3484), colorPal5(3485)], [colorPal3(3486), colorPal5(3487), colorPal9(3488)], [colorPal3(3489), colorPal9(3490)], [colorPal3(3491)], [colorPal3(3492)], [colorPal3(3493), colorPal9(3494)]], - [undefined, [colorPal3(3495), colorPal5(3496)], [colorPal3(3497), colorPal5(3498), colorPal9(3499), colorPal5(3500)], [colorPal3(3501), colorPal5(3502), colorPal7(3503), colorPal13(3504), colorPal7(3505)], [colorPal3(3506), colorPal5(3507)], [colorPal3(3508), colorPal7(3509), colorPal7(3510), colorPal5(3511), colorPal5(3512)], [colorPal3(3513), colorPal5(3514), colorPal7(3515)], [colorPal3(3516), colorPal5(3517)], [colorPal3(3518)], [colorPal3(3519)], [colorPal3(3520), colorPal5(3521), colorPal9(3522), colorPal5(3523)], [colorPal3(3524), colorPal5(3525), colorPal9(3526)], [colorPal3(3527), colorPal9(3528)], [colorPal3(3529)], [colorPal3(3530)], [colorPal3(3531), colorPal9(3532)]], - [undefined, [colorPal3(3533), colorPal5(3534)], [colorPal3(3535), colorPal5(3536), colorPal9(3537), colorPal5(3538)], [colorPal3(3539), colorPal5(3540), colorPal7(3541), colorPal13(3542), colorPal7(3543)], [colorPal3(3544), colorPal5(3545)], [colorPal3(3546), colorPal7(3547), colorPal7(3548), colorPal5(3549), colorPal5(3550)], [colorPal3(3551), colorPal5(3552), colorPal7(3553)], [colorPal3(3554), colorPal5(3555)], [colorPal3(3556)], [colorPal3(3557)], [colorPal3(3558), colorPal5(3559), colorPal9(3560), colorPal5(3561)], [colorPal3(3562), colorPal5(3563), colorPal9(3564)], [colorPal3(3565), colorPal9(3566)], [colorPal3(3567)], [colorPal3(3568)], [colorPal3(3569), colorPal9(3570)]], - [undefined, [colorPal3(3571), colorPal5(3572)], [colorPal3(3573), colorPal5(3574), colorPal9(3575), colorPal5(3576)], [colorPal3(3577), colorPal5(3578), colorPal7(3579), colorPal13(3580), colorPal7(3581)], [colorPal3(3582), colorPal5(3583)], [colorPal3(3584), colorPal7(3585), colorPal7(3586), colorPal5(3587), colorPal5(3588)], [colorPal3(3589), colorPal5(3590), colorPal7(3591)], [colorPal3(3592), colorPal5(3593)], [colorPal3(3594)], [colorPal3(3595)], [colorPal3(3596), colorPal5(3597), colorPal9(3598), colorPal5(3599)], [colorPal3(3600), colorPal5(3601), colorPal9(3602)], [colorPal3(3603), colorPal9(3604)], [colorPal3(3605)], [colorPal3(3606)], [colorPal3(3607), colorPal9(3608)]], - [undefined, [colorPal3(3609), colorPal5(3610)], [colorPal3(3611), colorPal5(3612), colorPal9(3613), colorPal5(3614)], [colorPal3(3615), colorPal5(3616), colorPal7(3617), colorPal13(3618), colorPal7(3619)], [colorPal3(3620), colorPal5(3621)], [colorPal3(3622), colorPal7(3623), colorPal7(3624), colorPal5(3625), colorPal5(3626)], [colorPal3(3627), colorPal5(3628), colorPal7(3629)], [colorPal3(3630), colorPal5(3631)], [colorPal3(3632)], [colorPal3(3633)], [colorPal3(3634), colorPal5(3635), colorPal9(3636), colorPal5(3637)], [colorPal3(3638), colorPal5(3639), colorPal9(3640)], [colorPal3(3641), colorPal9(3642)], [colorPal3(3643)], [colorPal3(3644)], [colorPal3(3645), colorPal9(3646)]], - [undefined, [colorPal3(3647), colorPal5(3648)], [colorPal3(3649), colorPal5(3650), colorPal9(3651), colorPal5(3652)], [colorPal3(3653), colorPal5(3654), colorPal7(3655), colorPal13(3656), colorPal7(3657)], [colorPal3(3658), colorPal5(3659)], [colorPal3(3660), colorPal7(3661), colorPal7(3662), colorPal5(3663), colorPal5(3664)], [colorPal3(3665), colorPal5(3666), colorPal7(3667)], [colorPal3(3668), colorPal5(3669)], [colorPal3(3670)], [colorPal3(3671)], [colorPal3(3672), colorPal5(3673), colorPal9(3674), colorPal5(3675)], [colorPal3(3676), colorPal5(3677), colorPal9(3678)], [colorPal3(3679), colorPal9(3680)], [colorPal3(3681)], [colorPal3(3682)], [colorPal3(3683), colorPal9(3684)]], - [undefined, [colorPal3(3685), colorPal5(3686)], [colorPal3(3687), colorPal5(3688), colorPal9(3689), colorPal5(3690)], [colorPal3(3691), colorPal5(3692), colorPal7(3693), colorPal13(3694), colorPal7(3695)], [colorPal3(3696), colorPal5(3697)], [colorPal3(3698), colorPal7(3699), colorPal7(3700), colorPal5(3701), colorPal5(3702)], [colorPal3(3703), colorPal5(3704), colorPal7(3705)], [colorPal3(3706), colorPal5(3707)], [colorPal3(3708)], [colorPal3(3709)], [colorPal3(3710), colorPal5(3711), colorPal9(3712), colorPal5(3713)], [colorPal3(3714), colorPal5(3715), colorPal9(3716)], [colorPal3(3717), colorPal9(3718)], [colorPal3(3719)], [colorPal3(3720)], [colorPal3(3721), colorPal9(3722)]], - [undefined, [colorPal3(3723), colorPal5(3724)], [colorPal3(3725), colorPal5(3726), colorPal9(3727), colorPal5(3728)], [colorPal3(3729), colorPal5(3730), colorPal7(3731), colorPal13(3732), colorPal7(3733)], [colorPal3(3734), colorPal5(3735)], [colorPal3(3736), colorPal7(3737), colorPal7(3738), colorPal5(3739), colorPal5(3740)], [colorPal3(3741), colorPal5(3742), colorPal7(3743)], [colorPal3(3744), colorPal5(3745)], [colorPal3(3746)], [colorPal3(3747)], [colorPal3(3748), colorPal5(3749), colorPal9(3750), colorPal5(3751)], [colorPal3(3752), colorPal5(3753), colorPal9(3754)], [colorPal3(3755), colorPal9(3756)], [colorPal3(3757)], [colorPal3(3758)], [colorPal3(3759), colorPal9(3760)]], - [undefined, [colorPal3(3761), colorPal5(3762)], [colorPal3(3763), colorPal5(3764), colorPal9(3765), colorPal5(3766)], [colorPal3(3767), colorPal5(3768), colorPal7(3769), colorPal13(3770), colorPal7(3771)], [colorPal3(3772), colorPal5(3773)], [colorPal3(3774), colorPal7(3775), colorPal7(3776), colorPal5(3777), colorPal5(3778)], [colorPal3(3779), colorPal5(3780), colorPal7(3781)], [colorPal3(3782), colorPal5(3783)], [colorPal3(3784)], [colorPal3(3785)], [colorPal3(3786), colorPal5(3787), colorPal9(3788), colorPal5(3789)], [colorPal3(3790), colorPal5(3791), colorPal9(3792)], [colorPal3(3793), colorPal9(3794)], [colorPal3(3795)], [colorPal3(3796)], [colorPal3(3797), colorPal9(3798)]], - [undefined, [colorPal3(3799), colorPal5(3800)], [colorPal3(3801), colorPal5(3802), colorPal9(3803), colorPal5(3804)], [colorPal3(3805), colorPal5(3806), colorPal7(3807), colorPal13(3808), colorPal7(3809)], [colorPal3(3810), colorPal5(3811)], [colorPal3(3812), colorPal7(3813), colorPal7(3814), colorPal5(3815), colorPal5(3816)], [colorPal3(3817), colorPal5(3818), colorPal7(3819)], [colorPal3(3820), colorPal5(3821)], [colorPal3(3822)], [colorPal3(3823)], [colorPal3(3824), colorPal5(3825), colorPal9(3826), colorPal5(3827)], [colorPal3(3828), colorPal5(3829), colorPal9(3830)], [colorPal3(3831), colorPal9(3832)], [colorPal3(3833)], [colorPal3(3834)], [colorPal3(3835), colorPal9(3836)]], - [undefined, [colorPal3(3837), colorPal5(3838)], [colorPal3(3839), colorPal5(3840), colorPal9(3841), colorPal5(3842)], [colorPal3(3843), colorPal5(3844), colorPal7(3845), colorPal13(3846), colorPal7(3847)], [colorPal3(3848), colorPal5(3849)], [colorPal3(3850), colorPal7(3851), colorPal7(3852), colorPal5(3853), colorPal5(3854)], [colorPal3(3855), colorPal5(3856), colorPal7(3857)], [colorPal3(3858), colorPal5(3859)], [colorPal3(3860)], [colorPal3(3861)], [colorPal3(3862), colorPal5(3863), colorPal9(3864), colorPal5(3865)], [colorPal3(3866), colorPal5(3867), colorPal9(3868)], [colorPal3(3869), colorPal9(3870)], [colorPal3(3871)], [colorPal3(3872)], [colorPal3(3873), colorPal9(3874)]] + undefined, + [undefined, [colorPal3(3305), colorPal5(3306)], [colorPal3(3307), colorPal5(3308), colorPal9(3309), colorPal5(3310)], [colorPal3(3311), colorPal5(3312), colorPal7(3313), colorPal13(3314), colorPal7(3315)], [colorPal3(3316), colorPal5(3317)], [colorPal3(3318), colorPal7(3319), colorPal7(3320), colorPal5(3321), colorPal5(3322)], [colorPal3(3323), colorPal5(3324), colorPal7(3325)], [colorPal3(3326), colorPal5(3327)], [colorPal3(3328)], [colorPal3(3329)], [colorPal3(3330), colorPal5(3331), colorPal9(3332), colorPal5(3333)], [colorPal3(3334), colorPal5(3335), colorPal9(3336)], [colorPal3(3337), colorPal9(3338)], [colorPal3(3339)], [colorPal3(3340)], [colorPal3(3341), colorPal9(3342)]], + [undefined, [colorPal3(3343), colorPal5(3344)], [colorPal3(3345), colorPal5(3346), colorPal9(3347), colorPal5(3348)], [colorPal3(3349), colorPal5(3350), colorPal7(3351), colorPal13(3352), colorPal7(3353)], [colorPal3(3354), colorPal5(3355)], [colorPal3(3356), colorPal7(3357), colorPal7(3358), colorPal5(3359), colorPal5(3360)], [colorPal3(3361), colorPal5(3362), colorPal7(3363)], [colorPal3(3364), colorPal5(3365)], [colorPal3(3366)], [colorPal3(3367)], [colorPal3(3368), colorPal5(3369), colorPal9(3370), colorPal5(3371)], [colorPal3(3372), colorPal5(3373), colorPal9(3374)], [colorPal3(3375), colorPal9(3376)], [colorPal3(3377)], [colorPal3(3378)], [colorPal3(3379), colorPal9(3380)]], + [undefined, [colorPal3(3381), colorPal5(3382)], [colorPal3(3383), colorPal5(3384), colorPal9(3385), colorPal5(3386)], [colorPal3(3387), colorPal5(3388), colorPal7(3389), colorPal13(3390), colorPal7(3391)], [colorPal3(3392), colorPal5(3393)], [colorPal3(3394), colorPal7(3395), colorPal7(3396), colorPal5(3397), colorPal5(3398)], [colorPal3(3399), colorPal5(3400), colorPal7(3401)], [colorPal3(3402), colorPal5(3403)], [colorPal3(3404)], [colorPal3(3405)], [colorPal3(3406), colorPal5(3407), colorPal9(3408), colorPal5(3409)], [colorPal3(3410), colorPal5(3411), colorPal9(3412)], [colorPal3(3413), colorPal9(3414)], [colorPal3(3415)], [colorPal3(3416)], [colorPal3(3417), colorPal9(3418)]], + [undefined, [colorPal3(3419), colorPal5(3420)], [colorPal3(3421), colorPal5(3422), colorPal9(3423), colorPal5(3424)], [colorPal3(3425), colorPal5(3426), colorPal7(3427), colorPal13(3428), colorPal7(3429)], [colorPal3(3430), colorPal5(3431)], [colorPal3(3432), colorPal7(3433), colorPal7(3434), colorPal5(3435), colorPal5(3436)], [colorPal3(3437), colorPal5(3438), colorPal7(3439)], [colorPal3(3440), colorPal5(3441)], [colorPal3(3442)], [colorPal3(3443)], [colorPal3(3444), colorPal5(3445), colorPal9(3446), colorPal5(3447)], [colorPal3(3448), colorPal5(3449), colorPal9(3450)], [colorPal3(3451), colorPal9(3452)], [colorPal3(3453)], [colorPal3(3454)], [colorPal3(3455), colorPal9(3456)]], + [undefined, [colorPal3(3457), colorPal5(3458)], [colorPal3(3459), colorPal5(3460), colorPal9(3461), colorPal5(3462)], [colorPal3(3463), colorPal5(3464), colorPal7(3465), colorPal13(3466), colorPal7(3467)], [colorPal3(3468), colorPal5(3469)], [colorPal3(3470), colorPal7(3471), colorPal7(3472), colorPal5(3473), colorPal5(3474)], [colorPal3(3475), colorPal5(3476), colorPal7(3477)], [colorPal3(3478), colorPal5(3479)], [colorPal3(3480)], [colorPal3(3481)], [colorPal3(3482), colorPal5(3483), colorPal9(3484), colorPal5(3485)], [colorPal3(3486), colorPal5(3487), colorPal9(3488)], [colorPal3(3489), colorPal9(3490)], [colorPal3(3491)], [colorPal3(3492)], [colorPal3(3493), colorPal9(3494)]], + [undefined, [colorPal3(3495), colorPal5(3496)], [colorPal3(3497), colorPal5(3498), colorPal9(3499), colorPal5(3500)], [colorPal3(3501), colorPal5(3502), colorPal7(3503), colorPal13(3504), colorPal7(3505)], [colorPal3(3506), colorPal5(3507)], [colorPal3(3508), colorPal7(3509), colorPal7(3510), colorPal5(3511), colorPal5(3512)], [colorPal3(3513), colorPal5(3514), colorPal7(3515)], [colorPal3(3516), colorPal5(3517)], [colorPal3(3518)], [colorPal3(3519)], [colorPal3(3520), colorPal5(3521), colorPal9(3522), colorPal5(3523)], [colorPal3(3524), colorPal5(3525), colorPal9(3526)], [colorPal3(3527), colorPal9(3528)], [colorPal3(3529)], [colorPal3(3530)], [colorPal3(3531), colorPal9(3532)]], + [undefined, [colorPal3(3533), colorPal5(3534)], [colorPal3(3535), colorPal5(3536), colorPal9(3537), colorPal5(3538)], [colorPal3(3539), colorPal5(3540), colorPal7(3541), colorPal13(3542), colorPal7(3543)], [colorPal3(3544), colorPal5(3545)], [colorPal3(3546), colorPal7(3547), colorPal7(3548), colorPal5(3549), colorPal5(3550)], [colorPal3(3551), colorPal5(3552), colorPal7(3553)], [colorPal3(3554), colorPal5(3555)], [colorPal3(3556)], [colorPal3(3557)], [colorPal3(3558), colorPal5(3559), colorPal9(3560), colorPal5(3561)], [colorPal3(3562), colorPal5(3563), colorPal9(3564)], [colorPal3(3565), colorPal9(3566)], [colorPal3(3567)], [colorPal3(3568)], [colorPal3(3569), colorPal9(3570)]], + [undefined, [colorPal3(3571), colorPal5(3572)], [colorPal3(3573), colorPal5(3574), colorPal9(3575), colorPal5(3576)], [colorPal3(3577), colorPal5(3578), colorPal7(3579), colorPal13(3580), colorPal7(3581)], [colorPal3(3582), colorPal5(3583)], [colorPal3(3584), colorPal7(3585), colorPal7(3586), colorPal5(3587), colorPal5(3588)], [colorPal3(3589), colorPal5(3590), colorPal7(3591)], [colorPal3(3592), colorPal5(3593)], [colorPal3(3594)], [colorPal3(3595)], [colorPal3(3596), colorPal5(3597), colorPal9(3598), colorPal5(3599)], [colorPal3(3600), colorPal5(3601), colorPal9(3602)], [colorPal3(3603), colorPal9(3604)], [colorPal3(3605)], [colorPal3(3606)], [colorPal3(3607), colorPal9(3608)]], + [undefined, [colorPal3(3609), colorPal5(3610)], [colorPal3(3611), colorPal5(3612), colorPal9(3613), colorPal5(3614)], [colorPal3(3615), colorPal5(3616), colorPal7(3617), colorPal13(3618), colorPal7(3619)], [colorPal3(3620), colorPal5(3621)], [colorPal3(3622), colorPal7(3623), colorPal7(3624), colorPal5(3625), colorPal5(3626)], [colorPal3(3627), colorPal5(3628), colorPal7(3629)], [colorPal3(3630), colorPal5(3631)], [colorPal3(3632)], [colorPal3(3633)], [colorPal3(3634), colorPal5(3635), colorPal9(3636), colorPal5(3637)], [colorPal3(3638), colorPal5(3639), colorPal9(3640)], [colorPal3(3641), colorPal9(3642)], [colorPal3(3643)], [colorPal3(3644)], [colorPal3(3645), colorPal9(3646)]], + [undefined, [colorPal3(3647), colorPal5(3648)], [colorPal3(3649), colorPal5(3650), colorPal9(3651), colorPal5(3652)], [colorPal3(3653), colorPal5(3654), colorPal7(3655), colorPal13(3656), colorPal7(3657)], [colorPal3(3658), colorPal5(3659)], [colorPal3(3660), colorPal7(3661), colorPal7(3662), colorPal5(3663), colorPal5(3664)], [colorPal3(3665), colorPal5(3666), colorPal7(3667)], [colorPal3(3668), colorPal5(3669)], [colorPal3(3670)], [colorPal3(3671)], [colorPal3(3672), colorPal5(3673), colorPal9(3674), colorPal5(3675)], [colorPal3(3676), colorPal5(3677), colorPal9(3678)], [colorPal3(3679), colorPal9(3680)], [colorPal3(3681)], [colorPal3(3682)], [colorPal3(3683), colorPal9(3684)]], + [undefined, [colorPal3(3685), colorPal5(3686)], [colorPal3(3687), colorPal5(3688), colorPal9(3689), colorPal5(3690)], [colorPal3(3691), colorPal5(3692), colorPal7(3693), colorPal13(3694), colorPal7(3695)], [colorPal3(3696), colorPal5(3697)], [colorPal3(3698), colorPal7(3699), colorPal7(3700), colorPal5(3701), colorPal5(3702)], [colorPal3(3703), colorPal5(3704), colorPal7(3705)], [colorPal3(3706), colorPal5(3707)], [colorPal3(3708)], [colorPal3(3709)], [colorPal3(3710), colorPal5(3711), colorPal9(3712), colorPal5(3713)], [colorPal3(3714), colorPal5(3715), colorPal9(3716)], [colorPal3(3717), colorPal9(3718)], [colorPal3(3719)], [colorPal3(3720)], [colorPal3(3721), colorPal9(3722)]], + [undefined, [colorPal3(3723), colorPal5(3724)], [colorPal3(3725), colorPal5(3726), colorPal9(3727), colorPal5(3728)], [colorPal3(3729), colorPal5(3730), colorPal7(3731), colorPal13(3732), colorPal7(3733)], [colorPal3(3734), colorPal5(3735)], [colorPal3(3736), colorPal7(3737), colorPal7(3738), colorPal5(3739), colorPal5(3740)], [colorPal3(3741), colorPal5(3742), colorPal7(3743)], [colorPal3(3744), colorPal5(3745)], [colorPal3(3746)], [colorPal3(3747)], [colorPal3(3748), colorPal5(3749), colorPal9(3750), colorPal5(3751)], [colorPal3(3752), colorPal5(3753), colorPal9(3754)], [colorPal3(3755), colorPal9(3756)], [colorPal3(3757)], [colorPal3(3758)], [colorPal3(3759), colorPal9(3760)]], + [undefined, [colorPal3(3761), colorPal5(3762)], [colorPal3(3763), colorPal5(3764), colorPal9(3765), colorPal5(3766)], [colorPal3(3767), colorPal5(3768), colorPal7(3769), colorPal13(3770), colorPal7(3771)], [colorPal3(3772), colorPal5(3773)], [colorPal3(3774), colorPal7(3775), colorPal7(3776), colorPal5(3777), colorPal5(3778)], [colorPal3(3779), colorPal5(3780), colorPal7(3781)], [colorPal3(3782), colorPal5(3783)], [colorPal3(3784)], [colorPal3(3785)], [colorPal3(3786), colorPal5(3787), colorPal9(3788), colorPal5(3789)], [colorPal3(3790), colorPal5(3791), colorPal9(3792)], [colorPal3(3793), colorPal9(3794)], [colorPal3(3795)], [colorPal3(3796)], [colorPal3(3797), colorPal9(3798)]], + [undefined, [colorPal3(3799), colorPal5(3800)], [colorPal3(3801), colorPal5(3802), colorPal9(3803), colorPal5(3804)], [colorPal3(3805), colorPal5(3806), colorPal7(3807), colorPal13(3808), colorPal7(3809)], [colorPal3(3810), colorPal5(3811)], [colorPal3(3812), colorPal7(3813), colorPal7(3814), colorPal5(3815), colorPal5(3816)], [colorPal3(3817), colorPal5(3818), colorPal7(3819)], [colorPal3(3820), colorPal5(3821)], [colorPal3(3822)], [colorPal3(3823)], [colorPal3(3824), colorPal5(3825), colorPal9(3826), colorPal5(3827)], [colorPal3(3828), colorPal5(3829), colorPal9(3830)], [colorPal3(3831), colorPal9(3832)], [colorPal3(3833)], [colorPal3(3834)], [colorPal3(3835), colorPal9(3836)]], + [undefined, [colorPal3(3837), colorPal5(3838)], [colorPal3(3839), colorPal5(3840), colorPal9(3841), colorPal5(3842)], [colorPal3(3843), colorPal5(3844), colorPal7(3845), colorPal13(3846), colorPal7(3847)], [colorPal3(3848), colorPal5(3849)], [colorPal3(3850), colorPal7(3851), colorPal7(3852), colorPal5(3853), colorPal5(3854)], [colorPal3(3855), colorPal5(3856), colorPal7(3857)], [colorPal3(3858), colorPal5(3859)], [colorPal3(3860)], [colorPal3(3861)], [colorPal3(3862), colorPal5(3863), colorPal9(3864), colorPal5(3865)], [colorPal3(3866), colorPal5(3867), colorPal9(3868)], [colorPal3(3869), colorPal9(3870)], [colorPal3(3871)], [colorPal3(3872)], [colorPal3(3873), colorPal9(3874)]] ]; export const chestAccessoriesBehind: AnimatedSprites = [ - undefined, - [undefined, [colorPal3(3875), colorPal3(3876), colorPal7(3877)], [], [], []], - [undefined, [colorPal3(3878), colorPal3(3879), colorPal7(3880)], [], [], []], - [undefined, [colorPal3(3881), colorPal3(3882), colorPal7(3883)], [], [], []], - [undefined, [colorPal3(3884), colorPal3(3885), colorPal7(3886)], [], [], []], - [undefined, [colorPal3(3887), colorPal3(3888), colorPal7(3889)], [], [], []], - [undefined, [colorPal3(3890), colorPal3(3891), colorPal7(3892)], [], [], []], - [undefined, [colorPal3(3893), colorPal3(3894), colorPal7(3895)], [], [], []], - [undefined, [colorPal3(3896), colorPal3(3897), colorPal7(3898)], [], [], []], - [undefined, [colorPal3(3899), colorPal3(3900), colorPal7(3901)], [], [], []], - [undefined, [colorPal3(3902), colorPal3(3903), colorPal7(3904)], [], [], []], - [undefined, [colorPal3(3905), colorPal3(3906), colorPal7(3907)], [], [], []], - [undefined, [colorPal3(3908), colorPal3(3909), colorPal7(3910)], [], [], []], - [undefined, [colorPal17(3911), colorPal3(3912), colorPal7(3913)], [], [], []], - [undefined, [colorPal3(3914), colorPal3(3915), colorPal7(3916)], [], [], []], - [undefined, [colorPal3(3917), colorPal3(3918), colorPal7(3919)], [], [], []] + undefined, + [undefined, [colorPal3(3875), colorPal3(3876), colorPal7(3877)], [], [], []], + [undefined, [colorPal3(3878), colorPal3(3879), colorPal7(3880)], [], [], []], + [undefined, [colorPal3(3881), colorPal3(3882), colorPal7(3883)], [], [], []], + [undefined, [colorPal3(3884), colorPal3(3885), colorPal7(3886)], [], [], []], + [undefined, [colorPal3(3887), colorPal3(3888), colorPal7(3889)], [], [], []], + [undefined, [colorPal3(3890), colorPal3(3891), colorPal7(3892)], [], [], []], + [undefined, [colorPal3(3893), colorPal3(3894), colorPal7(3895)], [], [], []], + [undefined, [colorPal3(3896), colorPal3(3897), colorPal7(3898)], [], [], []], + [undefined, [colorPal3(3899), colorPal3(3900), colorPal7(3901)], [], [], []], + [undefined, [colorPal3(3902), colorPal3(3903), colorPal7(3904)], [], [], []], + [undefined, [colorPal3(3905), colorPal3(3906), colorPal7(3907)], [], [], []], + [undefined, [colorPal3(3908), colorPal3(3909), colorPal7(3910)], [], [], []], + [undefined, [colorPal17(3911), colorPal3(3912), colorPal7(3913)], [], [], []], + [undefined, [colorPal3(3914), colorPal3(3915), colorPal7(3916)], [], [], []], + [undefined, [colorPal3(3917), colorPal3(3918), colorPal7(3919)], [], [], []] ]; export const chestAccessories: AnimatedSprites = [ - undefined, - [undefined, [colorPal3(3920), colorPal5(3921), colorPal9(3922)], [colorPal3(3923), colorPal7(3924), colorPal5(3925), colorPal5(3926), colorPal7(3927), colorPal5(3928), colorPal7(3929), colorPal5(3930), colorPal7(3931), colorPal11(3932), colorPal9(3933), colorPal5(3934), colorPal11(3935)], [colorPal5(3936), colorPal7(3937), colorPal11(3938)], [colorPal5(3939), colorPal7(3940)]], - [undefined, [colorPal3(3941), colorPal5(3942), colorPal9(3943)], [colorPal3(3944), colorPal7(3945), colorPal5(3946), colorPal5(3947), colorPal7(3948), colorPal5(3949), colorPal7(3950), colorPal5(3951), colorPal7(3952), colorPal11(3953), colorPal9(3954), colorPal5(3955), colorPal11(3956)], [colorPal5(3957), colorPal7(3958), colorPal11(3959)], [colorPal5(3960), colorPal7(3961)]], - [undefined, [colorPal3(3962), colorPal5(3963), colorPal9(3964)], [colorPal3(3965), colorPal7(3966), colorPal5(3967), colorPal5(3968), colorPal7(3969), colorPal5(3970), colorPal7(3971), colorPal5(3972), colorPal7(3973), colorPal11(3974), colorPal9(3975), colorPal5(3976), colorPal11(3977)], [colorPal5(3978), colorPal7(3979), colorPal11(3980)], [colorPal5(3981), colorPal7(3982)]], - [undefined, [colorPal3(3983), colorPal5(3984), colorPal9(3985)], [colorPal3(3986), colorPal7(3987), colorPal5(3988), colorPal5(3989), colorPal7(3990), colorPal5(3991), colorPal7(3992), colorPal5(3993), colorPal7(3994), colorPal11(3995), colorPal9(3996), colorPal5(3997), colorPal11(3998)], [colorPal5(3999), colorPal7(4000), colorPal11(4001)], [colorPal5(4002), colorPal7(4003)]], - [undefined, [colorPal3(4004), colorPal5(4005), colorPal9(4006)], [colorPal3(4007), colorPal7(4008), colorPal5(4009), colorPal5(4010), colorPal7(4011), colorPal5(4012), colorPal7(4013), colorPal5(4014), colorPal7(4015), colorPal11(4016), colorPal9(4017), colorPal5(4018), colorPal11(4019)], [colorPal5(4020), colorPal7(4021), colorPal11(4022)], [colorPal5(4023), colorPal7(4024)]], - [undefined, [colorPal3(4025), colorPal5(4026), colorPal9(4027)], [colorPal3(4028), colorPal7(4029), colorPal5(4030), colorPal5(4031), colorPal7(4032), colorPal5(4033), colorPal7(4034), colorPal5(4035), colorPal7(4036), colorPal11(4037), colorPal9(4038), colorPal5(4039), colorPal11(4040)], [colorPal5(4041), colorPal7(4042), colorPal11(4043)], [colorPal5(4044), colorPal7(4045)]], - [undefined, [colorPal3(4046), colorPal5(4047), colorPal9(4048)], [colorPal3(4049), colorPal7(4050), colorPal5(4051), colorPal5(4052), colorPal7(4053), colorPal5(4054), colorPal7(4055), colorPal5(4056), colorPal7(4057), colorPal11(4058), colorPal9(4059), colorPal5(4060), colorPal11(4061)], [colorPal5(4062), colorPal7(4063), colorPal11(4064)], [colorPal5(4065), colorPal7(4066)]], - [undefined, [colorPal3(4067), colorPal5(4068), colorPal9(4069)], [colorPal3(4070), colorPal7(4071), colorPal5(4072), colorPal5(4073), colorPal7(4074), colorPal5(4075), colorPal7(4076), colorPal5(4077), colorPal7(4078), colorPal11(4079), colorPal9(4080), colorPal5(4081), colorPal11(4082)], [colorPal5(4083), colorPal7(4084), colorPal11(4085)], [colorPal5(4086), colorPal7(4087)]], - [undefined, [colorPal3(4088), colorPal5(4089), colorPal9(4090)], [colorPal3(4091), colorPal7(4092), colorPal5(4093), colorPal5(4094), colorPal7(4095), colorPal5(4096), colorPal7(4097), colorPal5(4098), colorPal7(4099), colorPal11(4100), colorPal9(4101), colorPal5(4102), colorPal11(4103)], [colorPal5(4104), colorPal7(4105), colorPal11(4106)], [colorPal5(4107), colorPal7(4108)]], - [undefined, [colorPal3(4109), colorPal5(4110), colorPal9(4111)], [colorPal3(4112), colorPal7(4113), colorPal5(4114), colorPal5(4115), colorPal7(4116), colorPal5(4117), colorPal7(4118), colorPal5(4119), colorPal7(4120), colorPal11(4121), colorPal9(4122), colorPal5(4123), colorPal11(4124)], [colorPal5(4125), colorPal7(4126), colorPal11(4127)], [colorPal5(4128), colorPal7(4129)]], - [undefined, [colorPal3(4130), colorPal5(4131), colorPal9(4132)], [colorPal3(4133), colorPal7(4134), colorPal5(4135), colorPal5(4136), colorPal7(4137), colorPal5(4138), colorPal7(4139), colorPal5(4140), colorPal7(4141), colorPal11(4142), colorPal9(4143), colorPal5(4144), colorPal11(4145)], [colorPal5(4146), colorPal7(4147), colorPal11(4148)], [colorPal5(4149), colorPal7(4150)]], - [undefined, [colorPal3(4151), colorPal5(4152), colorPal9(4153)], [colorPal3(4154), colorPal7(4155), colorPal5(4156), colorPal5(4157), colorPal7(4158), colorPal5(4159), colorPal7(4160), colorPal5(4161), colorPal7(4162), colorPal11(4163), colorPal9(4164), colorPal5(4165), colorPal11(4166)], [colorPal5(4167), colorPal7(4168), colorPal11(4169)], [colorPal5(4170), colorPal7(4171)]], - [undefined, [colorPal3(4172), colorPal5(4173), colorPal9(4174)], [colorPal3(4175), colorPal7(4176), colorPal5(4177), colorPal5(4178), colorPal7(4179), colorPal5(4180), colorPal7(4181), colorPal5(4182), colorPal7(4183), colorPal11(4184), colorPal9(4185), colorPal5(4186), colorPal11(4187)], [colorPal5(4188), colorPal7(4189), colorPal11(4190)], [colorPal5(4191), colorPal7(4192)]], - [undefined, [colorPal3(4193), colorPal5(4194), colorPal9(4195)], [colorPal3(4196), colorPal7(4197), colorPal5(4198), colorPal5(4199), colorPal7(4200), colorPal5(4201), colorPal7(4202), colorPal5(4203), colorPal7(4204), colorPal11(4205), colorPal9(4206), colorPal5(4207), colorPal11(4208)], [colorPal5(4209), colorPal7(4210), colorPal11(4211)], [colorPal5(4212), colorPal7(4213)]], - [undefined, [colorPal3(4214), colorPal5(4215), colorPal9(4216)], [colorPal3(4217), colorPal7(4218), colorPal5(4219), colorPal5(4220), colorPal7(4221), colorPal5(4222), colorPal7(4223), colorPal5(4224), colorPal7(4225), colorPal11(4226), colorPal9(4227), colorPal5(4228), colorPal11(4229)], [colorPal5(4230), colorPal7(4231), colorPal11(4232)], [colorPal5(4233), colorPal7(4234)]] + undefined, + [undefined, [colorPal3(3920), colorPal5(3921), colorPal9(3922)], [colorPal3(3923), colorPal7(3924), colorPal5(3925), colorPal5(3926), colorPal7(3927), colorPal5(3928), colorPal7(3929), colorPal5(3930), colorPal7(3931), colorPal11(3932), colorPal9(3933), colorPal5(3934), colorPal11(3935)], [colorPal5(3936), colorPal7(3937), colorPal11(3938)], [colorPal5(3939), colorPal7(3940)]], + [undefined, [colorPal3(3941), colorPal5(3942), colorPal9(3943)], [colorPal3(3944), colorPal7(3945), colorPal5(3946), colorPal5(3947), colorPal7(3948), colorPal5(3949), colorPal7(3950), colorPal5(3951), colorPal7(3952), colorPal11(3953), colorPal9(3954), colorPal5(3955), colorPal11(3956)], [colorPal5(3957), colorPal7(3958), colorPal11(3959)], [colorPal5(3960), colorPal7(3961)]], + [undefined, [colorPal3(3962), colorPal5(3963), colorPal9(3964)], [colorPal3(3965), colorPal7(3966), colorPal5(3967), colorPal5(3968), colorPal7(3969), colorPal5(3970), colorPal7(3971), colorPal5(3972), colorPal7(3973), colorPal11(3974), colorPal9(3975), colorPal5(3976), colorPal11(3977)], [colorPal5(3978), colorPal7(3979), colorPal11(3980)], [colorPal5(3981), colorPal7(3982)]], + [undefined, [colorPal3(3983), colorPal5(3984), colorPal9(3985)], [colorPal3(3986), colorPal7(3987), colorPal5(3988), colorPal5(3989), colorPal7(3990), colorPal5(3991), colorPal7(3992), colorPal5(3993), colorPal7(3994), colorPal11(3995), colorPal9(3996), colorPal5(3997), colorPal11(3998)], [colorPal5(3999), colorPal7(4000), colorPal11(4001)], [colorPal5(4002), colorPal7(4003)]], + [undefined, [colorPal3(4004), colorPal5(4005), colorPal9(4006)], [colorPal3(4007), colorPal7(4008), colorPal5(4009), colorPal5(4010), colorPal7(4011), colorPal5(4012), colorPal7(4013), colorPal5(4014), colorPal7(4015), colorPal11(4016), colorPal9(4017), colorPal5(4018), colorPal11(4019)], [colorPal5(4020), colorPal7(4021), colorPal11(4022)], [colorPal5(4023), colorPal7(4024)]], + [undefined, [colorPal3(4025), colorPal5(4026), colorPal9(4027)], [colorPal3(4028), colorPal7(4029), colorPal5(4030), colorPal5(4031), colorPal7(4032), colorPal5(4033), colorPal7(4034), colorPal5(4035), colorPal7(4036), colorPal11(4037), colorPal9(4038), colorPal5(4039), colorPal11(4040)], [colorPal5(4041), colorPal7(4042), colorPal11(4043)], [colorPal5(4044), colorPal7(4045)]], + [undefined, [colorPal3(4046), colorPal5(4047), colorPal9(4048)], [colorPal3(4049), colorPal7(4050), colorPal5(4051), colorPal5(4052), colorPal7(4053), colorPal5(4054), colorPal7(4055), colorPal5(4056), colorPal7(4057), colorPal11(4058), colorPal9(4059), colorPal5(4060), colorPal11(4061)], [colorPal5(4062), colorPal7(4063), colorPal11(4064)], [colorPal5(4065), colorPal7(4066)]], + [undefined, [colorPal3(4067), colorPal5(4068), colorPal9(4069)], [colorPal3(4070), colorPal7(4071), colorPal5(4072), colorPal5(4073), colorPal7(4074), colorPal5(4075), colorPal7(4076), colorPal5(4077), colorPal7(4078), colorPal11(4079), colorPal9(4080), colorPal5(4081), colorPal11(4082)], [colorPal5(4083), colorPal7(4084), colorPal11(4085)], [colorPal5(4086), colorPal7(4087)]], + [undefined, [colorPal3(4088), colorPal5(4089), colorPal9(4090)], [colorPal3(4091), colorPal7(4092), colorPal5(4093), colorPal5(4094), colorPal7(4095), colorPal5(4096), colorPal7(4097), colorPal5(4098), colorPal7(4099), colorPal11(4100), colorPal9(4101), colorPal5(4102), colorPal11(4103)], [colorPal5(4104), colorPal7(4105), colorPal11(4106)], [colorPal5(4107), colorPal7(4108)]], + [undefined, [colorPal3(4109), colorPal5(4110), colorPal9(4111)], [colorPal3(4112), colorPal7(4113), colorPal5(4114), colorPal5(4115), colorPal7(4116), colorPal5(4117), colorPal7(4118), colorPal5(4119), colorPal7(4120), colorPal11(4121), colorPal9(4122), colorPal5(4123), colorPal11(4124)], [colorPal5(4125), colorPal7(4126), colorPal11(4127)], [colorPal5(4128), colorPal7(4129)]], + [undefined, [colorPal3(4130), colorPal5(4131), colorPal9(4132)], [colorPal3(4133), colorPal7(4134), colorPal5(4135), colorPal5(4136), colorPal7(4137), colorPal5(4138), colorPal7(4139), colorPal5(4140), colorPal7(4141), colorPal11(4142), colorPal9(4143), colorPal5(4144), colorPal11(4145)], [colorPal5(4146), colorPal7(4147), colorPal11(4148)], [colorPal5(4149), colorPal7(4150)]], + [undefined, [colorPal3(4151), colorPal5(4152), colorPal9(4153)], [colorPal3(4154), colorPal7(4155), colorPal5(4156), colorPal5(4157), colorPal7(4158), colorPal5(4159), colorPal7(4160), colorPal5(4161), colorPal7(4162), colorPal11(4163), colorPal9(4164), colorPal5(4165), colorPal11(4166)], [colorPal5(4167), colorPal7(4168), colorPal11(4169)], [colorPal5(4170), colorPal7(4171)]], + [undefined, [colorPal3(4172), colorPal5(4173), colorPal9(4174)], [colorPal3(4175), colorPal7(4176), colorPal5(4177), colorPal5(4178), colorPal7(4179), colorPal5(4180), colorPal7(4181), colorPal5(4182), colorPal7(4183), colorPal11(4184), colorPal9(4185), colorPal5(4186), colorPal11(4187)], [colorPal5(4188), colorPal7(4189), colorPal11(4190)], [colorPal5(4191), colorPal7(4192)]], + [undefined, [colorPal3(4193), colorPal5(4194), colorPal9(4195)], [colorPal3(4196), colorPal7(4197), colorPal5(4198), colorPal5(4199), colorPal7(4200), colorPal5(4201), colorPal7(4202), colorPal5(4203), colorPal7(4204), colorPal11(4205), colorPal9(4206), colorPal5(4207), colorPal11(4208)], [colorPal5(4209), colorPal7(4210), colorPal11(4211)], [colorPal5(4212), colorPal7(4213)]], + [undefined, [colorPal3(4214), colorPal5(4215), colorPal9(4216)], [colorPal3(4217), colorPal7(4218), colorPal5(4219), colorPal5(4220), colorPal7(4221), colorPal5(4222), colorPal7(4223), colorPal5(4224), colorPal7(4225), colorPal11(4226), colorPal9(4227), colorPal5(4228), colorPal11(4229)], [colorPal5(4230), colorPal7(4231), colorPal11(4232)], [colorPal5(4233), colorPal7(4234)]] ]; export const backAccessories: AnimatedSprites = [ - undefined, - [undefined, [colorPal3(4235)], [colorPal3(4236), colorPal7(4237), colorPal13(4238), colorPal5(4239)], [colorPal3(4240), colorPal7(4241), colorPal13(4242), colorPal5(4243)], [colorPal3(4244), colorPal9(4245), colorPal13(4246), colorPal5(4247)], [colorPal3(4248), colorPal3(4249)]], - [undefined, [colorPal3(4250)], [colorPal3(4251), colorPal7(4252), colorPal13(4253), colorPal5(4254)], [colorPal3(4255), colorPal7(4256), colorPal13(4257), colorPal5(4258)], [colorPal3(4259), colorPal9(4260), colorPal13(4261), colorPal5(4262)], [colorPal3(4263), colorPal3(4264)]], - [undefined, [colorPal3(4265)], [colorPal3(4266), colorPal7(4267), colorPal13(4268), colorPal5(4269)], [colorPal3(4270), colorPal7(4271), colorPal13(4272), colorPal5(4273)], [colorPal3(4274), colorPal9(4275), colorPal13(4276), colorPal5(4277)], [colorPal3(4278), colorPal3(4279)]], - [undefined, [colorPal3(4280)], [colorPal3(4281), colorPal7(4282), colorPal13(4283), colorPal5(4284)], [colorPal3(4285), colorPal7(4286), colorPal13(4287), colorPal5(4288)], [colorPal3(4289), colorPal9(4290), colorPal13(4291), colorPal5(4292)], [colorPal3(4293), colorPal3(4294)]], - [undefined, [colorPal3(4295)], [colorPal3(4296), colorPal7(4297), colorPal13(4298), colorPal5(4299)], [colorPal3(4300), colorPal7(4301), colorPal13(4302), colorPal5(4303)], [colorPal3(4304), colorPal9(4305), colorPal13(4306), colorPal5(4307)], [colorPal3(4308), colorPal3(4309)]], - [undefined, [colorPal3(4310)], [colorPal3(4311), colorPal7(4312), colorPal13(4313), colorPal5(4314)], [colorPal3(4315), colorPal7(4316), colorPal13(4317), colorPal5(4318)], [colorPal3(4319), colorPal9(4320), colorPal13(4321), colorPal5(4322)], [colorPal3(4323), colorPal3(4324)]], - [undefined, [colorPal3(4325)], [colorPal3(4326), colorPal7(4327), colorPal13(4328), colorPal5(4329)], [colorPal3(4330), colorPal7(4331), colorPal13(4332), colorPal5(4333)], [colorPal3(4334), colorPal9(4335), colorPal13(4336), colorPal5(4337)], [colorPal3(4338), colorPal3(4339)]], - [undefined, [colorPal3(4340)], [colorPal3(4341), colorPal7(4342), colorPal13(4343), colorPal5(4344)], [colorPal3(4345), colorPal7(4346), colorPal13(4347), colorPal5(4348)], [colorPal3(4349), colorPal9(4350), colorPal13(4351), colorPal5(4352)], [colorPal3(4353), colorPal3(4354)]], - [undefined, [colorPal3(4355)], [colorPal3(4356), colorPal7(4357), colorPal13(4358), colorPal5(4359)], [colorPal3(4360), colorPal7(4361), colorPal13(4362), colorPal5(4363)], [colorPal3(4364), colorPal9(4365), colorPal13(4366), colorPal5(4367)], [colorPal3(4368), colorPal3(4369)]], - [undefined, [colorPal3(4370)], [colorPal3(4371), colorPal7(4372), colorPal13(4373), colorPal5(4374)], [colorPal3(4375), colorPal7(4376), colorPal13(4377), colorPal5(4378)], [colorPal3(4379), colorPal9(4380), colorPal13(4381), colorPal5(4382)], [colorPal3(4383), colorPal3(4384)]], - [undefined, [colorPal3(4385)], [colorPal3(4386), colorPal7(4387), colorPal13(4388), colorPal5(4389)], [colorPal3(4390), colorPal7(4391), colorPal13(4392), colorPal5(4393)], [colorPal3(4394), colorPal9(4395), colorPal13(4396), colorPal5(4397)], [colorPal3(4398), colorPal3(4399)]], - [undefined, [colorPal3(4400)], [colorPal3(4401), colorPal7(4402), colorPal13(4403), colorPal5(4404)], [colorPal3(4405), colorPal7(4406), colorPal13(4407), colorPal5(4408)], [colorPal3(4409), colorPal9(4410), colorPal13(4411), colorPal5(4412)], [colorPal3(4413), colorPal3(4414)]], - [undefined, [colorPal3(4415)], [colorPal3(4416), colorPal7(4417), colorPal13(4418), colorPal5(4419)], [colorPal3(4420), colorPal7(4421), colorPal13(4422), colorPal5(4423)], [colorPal3(4424), colorPal9(4425), colorPal13(4426), colorPal5(4427)], [colorPal3(4428), colorPal3(4429)]], - [undefined, [colorPal3(4430)], [colorPal3(4431), colorPal7(4432), colorPal13(4433), colorPal5(4434)], [colorPal3(4435), colorPal7(4436), colorPal13(4437), colorPal5(4438)], [colorPal3(4439), colorPal9(4440), colorPal13(4441), colorPal5(4442)], [colorPal3(4443), colorPal3(4444)]], - [undefined, [colorPal3(4445)], [colorPal3(4446), colorPal7(4447), colorPal13(4448), colorPal5(4449)], [colorPal3(4450), colorPal7(4451), colorPal13(4452), colorPal5(4453)], [colorPal3(4454), colorPal9(4455), colorPal13(4456), colorPal5(4457)], [colorPal3(4458), colorPal3(4459)]] + undefined, + [undefined, [colorPal3(4235)], [colorPal3(4236), colorPal7(4237), colorPal13(4238), colorPal5(4239)], [colorPal3(4240), colorPal7(4241), colorPal13(4242), colorPal5(4243)], [colorPal3(4244), colorPal9(4245), colorPal13(4246), colorPal5(4247)], [colorPal3(4248), colorPal3(4249)]], + [undefined, [colorPal3(4250)], [colorPal3(4251), colorPal7(4252), colorPal13(4253), colorPal5(4254)], [colorPal3(4255), colorPal7(4256), colorPal13(4257), colorPal5(4258)], [colorPal3(4259), colorPal9(4260), colorPal13(4261), colorPal5(4262)], [colorPal3(4263), colorPal3(4264)]], + [undefined, [colorPal3(4265)], [colorPal3(4266), colorPal7(4267), colorPal13(4268), colorPal5(4269)], [colorPal3(4270), colorPal7(4271), colorPal13(4272), colorPal5(4273)], [colorPal3(4274), colorPal9(4275), colorPal13(4276), colorPal5(4277)], [colorPal3(4278), colorPal3(4279)]], + [undefined, [colorPal3(4280)], [colorPal3(4281), colorPal7(4282), colorPal13(4283), colorPal5(4284)], [colorPal3(4285), colorPal7(4286), colorPal13(4287), colorPal5(4288)], [colorPal3(4289), colorPal9(4290), colorPal13(4291), colorPal5(4292)], [colorPal3(4293), colorPal3(4294)]], + [undefined, [colorPal3(4295)], [colorPal3(4296), colorPal7(4297), colorPal13(4298), colorPal5(4299)], [colorPal3(4300), colorPal7(4301), colorPal13(4302), colorPal5(4303)], [colorPal3(4304), colorPal9(4305), colorPal13(4306), colorPal5(4307)], [colorPal3(4308), colorPal3(4309)]], + [undefined, [colorPal3(4310)], [colorPal3(4311), colorPal7(4312), colorPal13(4313), colorPal5(4314)], [colorPal3(4315), colorPal7(4316), colorPal13(4317), colorPal5(4318)], [colorPal3(4319), colorPal9(4320), colorPal13(4321), colorPal5(4322)], [colorPal3(4323), colorPal3(4324)]], + [undefined, [colorPal3(4325)], [colorPal3(4326), colorPal7(4327), colorPal13(4328), colorPal5(4329)], [colorPal3(4330), colorPal7(4331), colorPal13(4332), colorPal5(4333)], [colorPal3(4334), colorPal9(4335), colorPal13(4336), colorPal5(4337)], [colorPal3(4338), colorPal3(4339)]], + [undefined, [colorPal3(4340)], [colorPal3(4341), colorPal7(4342), colorPal13(4343), colorPal5(4344)], [colorPal3(4345), colorPal7(4346), colorPal13(4347), colorPal5(4348)], [colorPal3(4349), colorPal9(4350), colorPal13(4351), colorPal5(4352)], [colorPal3(4353), colorPal3(4354)]], + [undefined, [colorPal3(4355)], [colorPal3(4356), colorPal7(4357), colorPal13(4358), colorPal5(4359)], [colorPal3(4360), colorPal7(4361), colorPal13(4362), colorPal5(4363)], [colorPal3(4364), colorPal9(4365), colorPal13(4366), colorPal5(4367)], [colorPal3(4368), colorPal3(4369)]], + [undefined, [colorPal3(4370)], [colorPal3(4371), colorPal7(4372), colorPal13(4373), colorPal5(4374)], [colorPal3(4375), colorPal7(4376), colorPal13(4377), colorPal5(4378)], [colorPal3(4379), colorPal9(4380), colorPal13(4381), colorPal5(4382)], [colorPal3(4383), colorPal3(4384)]], + [undefined, [colorPal3(4385)], [colorPal3(4386), colorPal7(4387), colorPal13(4388), colorPal5(4389)], [colorPal3(4390), colorPal7(4391), colorPal13(4392), colorPal5(4393)], [colorPal3(4394), colorPal9(4395), colorPal13(4396), colorPal5(4397)], [colorPal3(4398), colorPal3(4399)]], + [undefined, [colorPal3(4400)], [colorPal3(4401), colorPal7(4402), colorPal13(4403), colorPal5(4404)], [colorPal3(4405), colorPal7(4406), colorPal13(4407), colorPal5(4408)], [colorPal3(4409), colorPal9(4410), colorPal13(4411), colorPal5(4412)], [colorPal3(4413), colorPal3(4414)]], + [undefined, [colorPal3(4415)], [colorPal3(4416), colorPal7(4417), colorPal13(4418), colorPal5(4419)], [colorPal3(4420), colorPal7(4421), colorPal13(4422), colorPal5(4423)], [colorPal3(4424), colorPal9(4425), colorPal13(4426), colorPal5(4427)], [colorPal3(4428), colorPal3(4429)]], + [undefined, [colorPal3(4430)], [colorPal3(4431), colorPal7(4432), colorPal13(4433), colorPal5(4434)], [colorPal3(4435), colorPal7(4436), colorPal13(4437), colorPal5(4438)], [colorPal3(4439), colorPal9(4440), colorPal13(4441), colorPal5(4442)], [colorPal3(4443), colorPal3(4444)]], + [undefined, [colorPal3(4445)], [colorPal3(4446), colorPal7(4447), colorPal13(4448), colorPal5(4449)], [colorPal3(4450), colorPal7(4451), colorPal13(4452), colorPal5(4453)], [colorPal3(4454), colorPal9(4455), colorPal13(4456), colorPal5(4457)], [colorPal3(4458), colorPal3(4459)]] ]; export const waistAccessories: AnimatedSprites = [ - undefined, - [undefined, [colorPal9(4460)], [colorPal9(4461)], [colorPal9(4462)]], - [undefined, [colorPal9(4463)], [colorPal9(4464)], [colorPal9(4465)]], - [undefined, [colorPal9(4466)], [colorPal9(4467)], [colorPal9(4468)]], - [undefined, [colorPal9(4469)], [colorPal9(4470)], [colorPal9(4471)]], - [undefined, [colorPal9(4472)], [colorPal9(4473)], [colorPal9(4474)]], - [undefined, [colorPal9(4475)], [colorPal9(4476)], [colorPal9(4477)]], - [undefined, [colorPal9(4478)], [colorPal9(4479)], [colorPal9(4480)]], - [undefined, [colorPal9(4481)], [colorPal9(4482)], [colorPal9(4483)]], - [undefined, [colorPal9(4484)], [colorPal9(4485)], [colorPal9(4486)]], - [undefined, [colorPal9(4487)], [colorPal9(4488)], [colorPal9(4489)]], - [undefined, [colorPal9(4490)], [colorPal9(4491)], [colorPal9(4492)]], - [undefined, [colorPal9(4493)], [colorPal9(4494)], [colorPal9(4495)]], - [undefined, [colorPal9(4496)], [colorPal9(4497)], [colorPal9(4498)]], - [undefined, [colorPal9(4499)], [colorPal9(4500)], [colorPal9(4501)]], - [undefined, [colorPal9(4502)], [colorPal9(4503)], [colorPal9(4504)]], - [undefined, [colorPal9(4505)], [colorPal9(4506)], [colorPal9(4507)]] + undefined, + [undefined, [colorPal9(4460)], [colorPal9(4461)], [colorPal9(4462)]], + [undefined, [colorPal9(4463)], [colorPal9(4464)], [colorPal9(4465)]], + [undefined, [colorPal9(4466)], [colorPal9(4467)], [colorPal9(4468)]], + [undefined, [colorPal9(4469)], [colorPal9(4470)], [colorPal9(4471)]], + [undefined, [colorPal9(4472)], [colorPal9(4473)], [colorPal9(4474)]], + [undefined, [colorPal9(4475)], [colorPal9(4476)], [colorPal9(4477)]], + [undefined, [colorPal9(4478)], [colorPal9(4479)], [colorPal9(4480)]], + [undefined, [colorPal9(4481)], [colorPal9(4482)], [colorPal9(4483)]], + [undefined, [colorPal9(4484)], [colorPal9(4485)], [colorPal9(4486)]], + [undefined, [colorPal9(4487)], [colorPal9(4488)], [colorPal9(4489)]], + [undefined, [colorPal9(4490)], [colorPal9(4491)], [colorPal9(4492)]], + [undefined, [colorPal9(4493)], [colorPal9(4494)], [colorPal9(4495)]], + [undefined, [colorPal9(4496)], [colorPal9(4497)], [colorPal9(4498)]], + [undefined, [colorPal9(4499)], [colorPal9(4500)], [colorPal9(4501)]], + [undefined, [colorPal9(4502)], [colorPal9(4503)], [colorPal9(4504)]], + [undefined, [colorPal9(4505)], [colorPal9(4506)], [colorPal9(4507)]] ]; export const head: AnimatedSprites = [ - undefined, - [[colorPal3(4508), colorPal7(4509), colorPal7(4510), colorPal7(4511), colorPal7(4512), colorPal7(4513), colorPal11(4514), colorPal11(4515), colorPal9(4516), colorPal9(4517), colorPal5(4518), colorPal5(4519), colorPal9(4520), colorPal5(4521), colorPal7(4522), colorPal7(4523), colorPal7(4524), colorPal7(4525), colorPal5(4526), colorPal11(4527)]], - [[colorPal3(4528)]] + undefined, + [[colorPal3(4508), colorPal7(4509), colorPal7(4510), colorPal7(4511), colorPal7(4512), colorPal7(4513), colorPal11(4514), colorPal11(4515), colorPal9(4516), colorPal9(4517), colorPal5(4518), colorPal5(4519), colorPal9(4520), colorPal5(4521), colorPal7(4522), colorPal7(4523), colorPal7(4524), colorPal7(4525), colorPal5(4526), colorPal11(4527)]], + [[colorPal3(4528)]] ]; export const earsFar: StaticSprites = [ - [colorPal3(4529), colorPal7(4530)], - [colorPal3(4531), colorPal9(4532)], - [colorPal3(4533), colorPal9(4534)], - [colorPal3(4535), colorPal5(4536)], - [colorPal3(4537), colorPal7(4538), colorPal7(4539)], - [colorPal3(4540), colorPal7(4541), colorPal9(4542)] + [colorPal3(4529), colorPal7(4530)], + [colorPal3(4531), colorPal9(4532)], + [colorPal3(4533), colorPal9(4534)], + [colorPal3(4535), colorPal5(4536)], + [colorPal3(4537), colorPal7(4538), colorPal7(4539)], + [colorPal3(4540), colorPal7(4541), colorPal9(4542)] ]; ; export const ears: StaticSprites = [ - [colorPal3(4543), colorPal7(4544)], - [colorPal3(4545), colorPal9(4546)], - [colorPal3(4547), colorPal9(4548)], - [colorPal3(4549), colorPal5(4550)], - [colorPal3(4551), colorPal7(4552), colorPal7(4553)], - [colorPal3(4554), colorPal7(4555), colorPal9(4556)] + [colorPal3(4543), colorPal7(4544)], + [colorPal3(4545), colorPal9(4546)], + [colorPal3(4547), colorPal9(4548)], + [colorPal3(4549), colorPal5(4550)], + [colorPal3(4551), colorPal7(4552), colorPal7(4553)], + [colorPal3(4554), colorPal7(4555), colorPal9(4556)] ]; ; export const hornsBehind: StaticSprites = [ - undefined, - undefined, - undefined, - undefined, - [colorPal3(4557), colorPal5(4558)], - [colorPal3(4559), colorPal7(4560)], - [colorPal3(4561), colorPal9(4562), colorPal9(4563)], - [colorPal3(4564), colorPal9(4565), colorPal13(4566)], - [colorPal3(4567), colorPal9(4568), colorPal13(4569)], - [colorPal3(4570), colorPal7(4571)], - [colorPal3(4572), colorPal11(4573), colorPal5(4574), colorPal5(4575)], - [colorPal3(4576), colorPal9(4577), colorPal5(4578), colorPal5(4579)], - [colorPal3(4580), colorPal9(4581), colorPal5(4582)], - [colorPal3(4583), colorPal7(4584)], - [] + undefined, + undefined, + undefined, + undefined, + [colorPal3(4557), colorPal5(4558)], + [colorPal3(4559), colorPal7(4560)], + [colorPal3(4561), colorPal9(4562), colorPal9(4563)], + [colorPal3(4564), colorPal9(4565), colorPal13(4566)], + [colorPal3(4567), colorPal9(4568), colorPal13(4569)], + [colorPal3(4570), colorPal7(4571)], + [colorPal3(4572), colorPal11(4573), colorPal5(4574), colorPal5(4575)], + [colorPal3(4576), colorPal9(4577), colorPal5(4578), colorPal5(4579)], + [colorPal3(4580), colorPal9(4581), colorPal5(4582)], + [colorPal3(4583), colorPal7(4584)], + [] ]; ; export const horns: StaticSprites = [ - undefined, - [colorPal3(4585), colorPal7(4586)], - [colorPal3(4587), colorPal5(4588)], - [colorPal3(4589), colorPal7(4590)], - [colorPal3(4591), colorPal5(4592)], - [colorPal3(4593), colorPal7(4594)], - [colorPal3(4595), colorPal9(4596), colorPal9(4597)], - [colorPal3(4598), colorPal9(4599), colorPal13(4600)], - [colorPal3(4601), colorPal9(4602), colorPal13(4603)], - [colorPal3(4604), colorPal7(4605)], - [colorPal3(4606), colorPal11(4607), colorPal5(4608), colorPal5(4609)], - [colorPal3(4610), colorPal9(4611), colorPal5(4612), colorPal5(4613)], - [colorPal3(4614), colorPal9(4615), colorPal5(4616)], - [colorPal3(4617), colorPal7(4618)], - [colorPal3(4619), colorPal9(4620)] + undefined, + [colorPal3(4585), colorPal7(4586)], + [colorPal3(4587), colorPal5(4588)], + [colorPal3(4589), colorPal7(4590)], + [colorPal3(4591), colorPal5(4592)], + [colorPal3(4593), colorPal7(4594)], + [colorPal3(4595), colorPal9(4596), colorPal9(4597)], + [colorPal3(4598), colorPal9(4599), colorPal13(4600)], + [colorPal3(4601), colorPal9(4602), colorPal13(4603)], + [colorPal3(4604), colorPal7(4605)], + [colorPal3(4606), colorPal11(4607), colorPal5(4608), colorPal5(4609)], + [colorPal3(4610), colorPal9(4611), colorPal5(4612), colorPal5(4613)], + [colorPal3(4614), colorPal9(4615), colorPal5(4616)], + [colorPal3(4617), colorPal7(4618)], + [colorPal3(4619), colorPal9(4620)] ]; ; export const behindManes: StaticSprites = [ - undefined, - [colorPal3(4621), colorPal13(4622), colorPal13(4623), colorPal7(4624), colorPal5(4625), colorPal5(4626)], - undefined, - [colorPal3(4627), colorPal13(4628), colorPal13(4629), colorPal5(4630), colorPal5(4631), colorPal7(4632)], - undefined, - undefined, - undefined, - undefined, - undefined, - [colorPal3(4633), colorPal13(4634), colorPal13(4635), colorPal7(4636), colorPal5(4637)], - [colorPal3(4638), colorPal13(4639), colorPal13(4640), colorPal7(4641), colorPal5(4642), colorPal5(4643), colorPal13(4644)], - [colorPal3(4645), colorPal9(4646), colorPal11(4647), colorPal5(4648), colorPal5(4649)], - [colorPal3(4650), colorPal9(4651), colorPal9(4652), colorPal7(4653), colorPal5(4654), colorPal5(4655)], - undefined, - undefined, - undefined, - undefined, - undefined, - [colorPal3(4656), colorPal13(4657), colorPal13(4658), colorPal7(4659), colorPal5(4660), colorPal5(4661)], - undefined, - [colorPal3(4662), colorPal11(4663), colorPal11(4664), colorPal7(4665), colorPal5(4666), colorPal5(4667)], - [colorPal3(4668), colorPal13(4669), colorPal13(4670), colorPal7(4671), colorPal5(4672), colorPal5(4673)], - undefined, - undefined, - undefined, - [colorPal3(4674), colorPal7(4675), colorPal5(4676), colorPal13(4677), colorPal5(4678)], - undefined, - [colorPal3(4679), colorPal9(4680), colorPal13(4681), colorPal7(4682), colorPal5(4683), colorPal5(4684)], - [colorPal3(4685), colorPal9(4686), colorPal13(4687), colorPal7(4688), colorPal5(4689), colorPal5(4690)], - undefined, - [colorPal3(4691), colorPal13(4692), colorPal5(4693), colorPal11(4694), colorPal5(4695)], - [] + undefined, + [colorPal3(4621), colorPal13(4622), colorPal13(4623), colorPal7(4624), colorPal5(4625), colorPal5(4626)], + undefined, + [colorPal3(4627), colorPal13(4628), colorPal13(4629), colorPal5(4630), colorPal5(4631), colorPal7(4632)], + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal3(4633), colorPal13(4634), colorPal13(4635), colorPal7(4636), colorPal5(4637)], + [colorPal3(4638), colorPal13(4639), colorPal13(4640), colorPal7(4641), colorPal5(4642), colorPal5(4643), colorPal13(4644)], + [colorPal3(4645), colorPal9(4646), colorPal11(4647), colorPal5(4648), colorPal5(4649)], + [colorPal3(4650), colorPal9(4651), colorPal9(4652), colorPal7(4653), colorPal5(4654), colorPal5(4655)], + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal3(4656), colorPal13(4657), colorPal13(4658), colorPal7(4659), colorPal5(4660), colorPal5(4661)], + undefined, + [colorPal3(4662), colorPal11(4663), colorPal11(4664), colorPal7(4665), colorPal5(4666), colorPal5(4667)], + [colorPal3(4668), colorPal13(4669), colorPal13(4670), colorPal7(4671), colorPal5(4672), colorPal5(4673)], + undefined, + undefined, + undefined, + [colorPal3(4674), colorPal7(4675), colorPal5(4676), colorPal13(4677), colorPal5(4678)], + undefined, + [colorPal3(4679), colorPal9(4680), colorPal13(4681), colorPal7(4682), colorPal5(4683), colorPal5(4684)], + [colorPal3(4685), colorPal9(4686), colorPal13(4687), colorPal7(4688), colorPal5(4689), colorPal5(4690)], + undefined, + [colorPal3(4691), colorPal13(4692), colorPal5(4693), colorPal11(4694), colorPal5(4695)], + [] ]; ; export const backBehindManes: StaticSprites = [ - undefined, - [colorPal3(4696), colorPal7(4697), colorPal7(4698), colorPal5(4699), colorPal5(4700), colorPal5(4701)], - [colorPal5(4702), colorPal13(4703), colorPal11(4704), colorPal9(4705), colorPal7(4706), colorPal7(4707)], - undefined, - [colorPal5(4708), colorPal11(4709), colorPal11(4710), colorPal7(4711), colorPal7(4712), colorPal7(4713)], - [colorPal3(4714), colorPal7(4715), colorPal7(4716), colorPal5(4717), colorPal5(4718)], - [colorPal3(4719), colorPal9(4720), colorPal11(4721), colorPal7(4722), colorPal5(4723), colorPal5(4724)], - [colorPal3(4725), colorPal9(4726), colorPal5(4727), colorPal5(4728), colorPal5(4729)], - undefined, - [colorPal3(4730), colorPal5(4731), colorPal11(4732), colorPal5(4733)], - undefined, - undefined, - [colorPal3(4734), colorPal5(4735), colorPal5(4736), colorPal5(4737)], - [colorPal3(4738), colorPal11(4739), colorPal7(4740), colorPal5(4741), colorPal5(4742)], - undefined, - undefined, - [colorPal3(4743), colorPal7(4744), colorPal7(4745), colorPal7(4746), colorPal5(4747), colorPal5(4748)], - [colorPal5(4749), colorPal13(4750), colorPal13(4751), colorPal9(4752), colorPal7(4753), colorPal7(4754)], - [colorPal3(4755), colorPal7(4756), colorPal5(4757), colorPal7(4758), colorPal5(4759), colorPal5(4760)], - [colorPal3(4761), colorPal7(4762), colorPal13(4763), colorPal7(4764), colorPal5(4765), colorPal5(4766)], - [colorPal5(4767), colorPal9(4768), colorPal13(4769), colorPal7(4770)], - [colorPal3(4771), colorPal7(4772), colorPal13(4773), colorPal7(4774), colorPal5(4775), colorPal5(4776)], - [colorPal3(4777), colorPal9(4778), colorPal5(4779), colorPal9(4780), colorPal5(4781)], - [colorPal3(4782), colorPal7(4783), colorPal7(4784), colorPal5(4785)], - [colorPal3(4786), colorPal11(4787), colorPal7(4788), colorPal5(4789), colorPal5(4790)], - [colorPal3(4791), colorPal11(4792), colorPal7(4793), colorPal7(4794), colorPal5(4795)], - [colorPal3(4796), colorPal11(4797), colorPal7(4798), colorPal7(4799), colorPal5(4800), colorPal5(4801), colorPal5(4802)], - [colorPal3(4803)], - [colorPal3(4804), colorPal9(4805), colorPal7(4806), colorPal5(4807), colorPal5(4808)] + undefined, + [colorPal3(4696), colorPal7(4697), colorPal7(4698), colorPal5(4699), colorPal5(4700), colorPal5(4701)], + [colorPal5(4702), colorPal13(4703), colorPal11(4704), colorPal9(4705), colorPal7(4706), colorPal7(4707)], + undefined, + [colorPal5(4708), colorPal11(4709), colorPal11(4710), colorPal7(4711), colorPal7(4712), colorPal7(4713)], + [colorPal3(4714), colorPal7(4715), colorPal7(4716), colorPal5(4717), colorPal5(4718)], + [colorPal3(4719), colorPal9(4720), colorPal11(4721), colorPal7(4722), colorPal5(4723), colorPal5(4724)], + [colorPal3(4725), colorPal9(4726), colorPal5(4727), colorPal5(4728), colorPal5(4729)], + undefined, + [colorPal3(4730), colorPal5(4731), colorPal11(4732), colorPal5(4733)], + undefined, + undefined, + [colorPal3(4734), colorPal5(4735), colorPal5(4736), colorPal5(4737)], + [colorPal3(4738), colorPal11(4739), colorPal7(4740), colorPal5(4741), colorPal5(4742)], + undefined, + undefined, + [colorPal3(4743), colorPal7(4744), colorPal7(4745), colorPal7(4746), colorPal5(4747), colorPal5(4748)], + [colorPal5(4749), colorPal13(4750), colorPal13(4751), colorPal9(4752), colorPal7(4753), colorPal7(4754)], + [colorPal3(4755), colorPal7(4756), colorPal5(4757), colorPal7(4758), colorPal5(4759), colorPal5(4760)], + [colorPal3(4761), colorPal7(4762), colorPal13(4763), colorPal7(4764), colorPal5(4765), colorPal5(4766)], + [colorPal5(4767), colorPal9(4768), colorPal13(4769), colorPal7(4770)], + [colorPal3(4771), colorPal7(4772), colorPal13(4773), colorPal7(4774), colorPal5(4775), colorPal5(4776)], + [colorPal3(4777), colorPal9(4778), colorPal5(4779), colorPal9(4780), colorPal5(4781)], + [colorPal3(4782), colorPal7(4783), colorPal7(4784), colorPal5(4785)], + [colorPal3(4786), colorPal11(4787), colorPal7(4788), colorPal5(4789), colorPal5(4790)], + [colorPal3(4791), colorPal11(4792), colorPal7(4793), colorPal7(4794), colorPal5(4795)], + [colorPal3(4796), colorPal11(4797), colorPal7(4798), colorPal7(4799), colorPal5(4800), colorPal5(4801), colorPal5(4802)], + [colorPal3(4803)], + [colorPal3(4804), colorPal9(4805), colorPal7(4806), colorPal5(4807), colorPal5(4808)] ]; ; export const backFrontManes: StaticSprites = [ - undefined, - [colorPal3(4809), colorPal7(4810), colorPal7(4811), colorPal5(4812), colorPal5(4813), colorPal5(4814)], - undefined, - [colorPal3(4815), colorPal9(4816), colorPal9(4817), colorPal7(4818), colorPal5(4819), colorPal5(4820)], - undefined, - undefined, - undefined, - undefined, - [colorPal3(4821), colorPal11(4822), colorPal11(4823), colorPal5(4824)], - undefined, - [colorPal3(4825), colorPal7(4826), colorPal7(4827), colorPal5(4828), colorPal5(4829)], - [colorPal3(4830), colorPal9(4831), colorPal9(4832), colorPal7(4833), colorPal5(4834)], - undefined, - undefined, - [colorPal3(4835), colorPal7(4836), colorPal9(4837), colorPal5(4838), colorPal5(4839), colorPal5(4840)], - [colorPal3(4841), colorPal7(4842), colorPal13(4843), colorPal5(4844), colorPal5(4845), colorPal5(4846)], - [colorPal3(4847), colorPal7(4848), colorPal7(4849), colorPal7(4850), colorPal5(4851), colorPal5(4852)], - [colorPal5(4853), colorPal13(4854), colorPal13(4855), colorPal9(4856), colorPal7(4857), colorPal7(4858)], - [colorPal3(4859), colorPal7(4860), colorPal5(4861), colorPal7(4862), colorPal5(4863), colorPal5(4864)], - undefined, - [colorPal5(4865), colorPal9(4866), colorPal13(4867), colorPal7(4868)], - [colorPal3(4869), colorPal7(4870), colorPal13(4871), colorPal7(4872), colorPal5(4873), colorPal5(4874)], - [colorPal3(4875), colorPal9(4876), colorPal5(4877), colorPal9(4878), colorPal5(4879)], - [], - [], - [], - [], - [], - [] + undefined, + [colorPal3(4809), colorPal7(4810), colorPal7(4811), colorPal5(4812), colorPal5(4813), colorPal5(4814)], + undefined, + [colorPal3(4815), colorPal9(4816), colorPal9(4817), colorPal7(4818), colorPal5(4819), colorPal5(4820)], + undefined, + undefined, + undefined, + undefined, + [colorPal3(4821), colorPal11(4822), colorPal11(4823), colorPal5(4824)], + undefined, + [colorPal3(4825), colorPal7(4826), colorPal7(4827), colorPal5(4828), colorPal5(4829)], + [colorPal3(4830), colorPal9(4831), colorPal9(4832), colorPal7(4833), colorPal5(4834)], + undefined, + undefined, + [colorPal3(4835), colorPal7(4836), colorPal9(4837), colorPal5(4838), colorPal5(4839), colorPal5(4840)], + [colorPal3(4841), colorPal7(4842), colorPal13(4843), colorPal5(4844), colorPal5(4845), colorPal5(4846)], + [colorPal3(4847), colorPal7(4848), colorPal7(4849), colorPal7(4850), colorPal5(4851), colorPal5(4852)], + [colorPal5(4853), colorPal13(4854), colorPal13(4855), colorPal9(4856), colorPal7(4857), colorPal7(4858)], + [colorPal3(4859), colorPal7(4860), colorPal5(4861), colorPal7(4862), colorPal5(4863), colorPal5(4864)], + undefined, + [colorPal5(4865), colorPal9(4866), colorPal13(4867), colorPal7(4868)], + [colorPal3(4869), colorPal7(4870), colorPal13(4871), colorPal7(4872), colorPal5(4873), colorPal5(4874)], + [colorPal3(4875), colorPal9(4876), colorPal5(4877), colorPal9(4878), colorPal5(4879)], + [], + [], + [], + [], + [], + [] ]; ; export const topManes: StaticSprites = [ - undefined, - [colorPal3(4880), colorPal13(4881), colorPal13(4882), colorPal7(4883), colorPal5(4884), colorPal5(4885)], - [colorPal3(4886), colorPal11(4887), colorPal11(4888), colorPal7(4889), colorPal5(4890), colorPal5(4891)], - [colorPal3(4892), colorPal13(4893), colorPal13(4894), colorPal5(4895), colorPal5(4896), colorPal7(4897)], - [colorPal3(4898), colorPal11(4899), colorPal11(4900), colorPal7(4901), colorPal5(4902), colorPal5(4903)], - [colorPal3(4904), colorPal9(4905), colorPal9(4906), colorPal7(4907), colorPal5(4908)], - [colorPal3(4909), colorPal11(4910), colorPal9(4911), colorPal7(4912), colorPal5(4913), colorPal5(4914)], - [colorPal3(4915), colorPal9(4916), colorPal9(4917), colorPal7(4918), colorPal5(4919), colorPal5(4920)], - [colorPal3(4921), colorPal11(4922), colorPal11(4923), colorPal7(4924), colorPal5(4925), colorPal5(4926)], - [colorPal3(4927), colorPal13(4928), colorPal13(4929), colorPal7(4930), colorPal5(4931)], - [colorPal3(4932), colorPal13(4933), colorPal13(4934), colorPal7(4935), colorPal5(4936), colorPal5(4937), colorPal13(4938)], - [colorPal3(4939), colorPal9(4940), colorPal11(4941), colorPal5(4942), colorPal5(4943)], - [colorPal3(4944), colorPal9(4945), colorPal9(4946), colorPal7(4947), colorPal5(4948), colorPal5(4949)], - [colorPal3(4950), colorPal9(4951), colorPal7(4952), colorPal5(4953), colorPal5(4954)], - [colorPal3(4955), colorPal11(4956), colorPal9(4957), colorPal7(4958), colorPal5(4959)], - [colorPal3(4960), colorPal11(4961), colorPal7(4962), colorPal5(4963), colorPal5(4964)], - [colorPal3(4965), colorPal7(4966)], - [colorPal3(4967), colorPal11(4968), colorPal9(4969), colorPal7(4970), colorPal5(4971), colorPal5(4972)], - [colorPal3(4973), colorPal13(4974), colorPal13(4975), colorPal7(4976), colorPal5(4977), colorPal5(4978)], - [colorPal3(4979), colorPal9(4980), colorPal13(4981), colorPal7(4982), colorPal5(4983), colorPal5(4984)], - [colorPal3(4985), colorPal11(4986), colorPal11(4987), colorPal7(4988), colorPal5(4989), colorPal5(4990)], - [colorPal3(4991), colorPal13(4992), colorPal13(4993), colorPal7(4994), colorPal5(4995), colorPal5(4996)], - [colorPal3(4997), colorPal13(4998), colorPal13(4999), colorPal7(5000), colorPal5(5001), colorPal5(5002), colorPal9(5003)], - [colorPal3(5004), colorPal11(5005), colorPal13(5006), colorPal7(5007), colorPal5(5008), colorPal5(5009)], - [colorPal3(5010), colorPal13(5011), colorPal11(5012), colorPal7(5013), colorPal5(5014), colorPal5(5015)], - [colorPal3(5016), colorPal7(5017), colorPal5(5018), colorPal13(5019), colorPal5(5020)], - [colorPal3(5021), colorPal11(5022), colorPal11(5023), colorPal7(5024), colorPal5(5025), colorPal5(5026)], - [colorPal3(5027), colorPal9(5028), colorPal13(5029), colorPal7(5030), colorPal5(5031), colorPal5(5032)], - [colorPal3(5033), colorPal9(5034), colorPal13(5035), colorPal7(5036), colorPal5(5037), colorPal5(5038)], - [colorPal3(5039), colorPal11(5040), colorPal13(5041), colorPal7(5042), colorPal5(5043), colorPal5(5044)], - [colorPal3(5045), colorPal13(5046), colorPal5(5047), colorPal11(5048), colorPal5(5049)], - [colorPal3(5050), colorPal7(5051), colorPal5(5052), colorPal7(5053), colorPal5(5054), colorPal5(5055), colorPal7(5056)] + undefined, + [colorPal3(4880), colorPal13(4881), colorPal13(4882), colorPal7(4883), colorPal5(4884), colorPal5(4885)], + [colorPal3(4886), colorPal11(4887), colorPal11(4888), colorPal7(4889), colorPal5(4890), colorPal5(4891)], + [colorPal3(4892), colorPal13(4893), colorPal13(4894), colorPal5(4895), colorPal5(4896), colorPal7(4897)], + [colorPal3(4898), colorPal11(4899), colorPal11(4900), colorPal7(4901), colorPal5(4902), colorPal5(4903)], + [colorPal3(4904), colorPal9(4905), colorPal9(4906), colorPal7(4907), colorPal5(4908)], + [colorPal3(4909), colorPal11(4910), colorPal9(4911), colorPal7(4912), colorPal5(4913), colorPal5(4914)], + [colorPal3(4915), colorPal9(4916), colorPal9(4917), colorPal7(4918), colorPal5(4919), colorPal5(4920)], + [colorPal3(4921), colorPal11(4922), colorPal11(4923), colorPal7(4924), colorPal5(4925), colorPal5(4926)], + [colorPal3(4927), colorPal13(4928), colorPal13(4929), colorPal7(4930), colorPal5(4931)], + [colorPal3(4932), colorPal13(4933), colorPal13(4934), colorPal7(4935), colorPal5(4936), colorPal5(4937), colorPal13(4938)], + [colorPal3(4939), colorPal9(4940), colorPal11(4941), colorPal5(4942), colorPal5(4943)], + [colorPal3(4944), colorPal9(4945), colorPal9(4946), colorPal7(4947), colorPal5(4948), colorPal5(4949)], + [colorPal3(4950), colorPal9(4951), colorPal7(4952), colorPal5(4953), colorPal5(4954)], + [colorPal3(4955), colorPal11(4956), colorPal9(4957), colorPal7(4958), colorPal5(4959)], + [colorPal3(4960), colorPal11(4961), colorPal7(4962), colorPal5(4963), colorPal5(4964)], + [colorPal3(4965), colorPal7(4966)], + [colorPal3(4967), colorPal11(4968), colorPal9(4969), colorPal7(4970), colorPal5(4971), colorPal5(4972)], + [colorPal3(4973), colorPal13(4974), colorPal13(4975), colorPal7(4976), colorPal5(4977), colorPal5(4978)], + [colorPal3(4979), colorPal9(4980), colorPal13(4981), colorPal7(4982), colorPal5(4983), colorPal5(4984)], + [colorPal3(4985), colorPal11(4986), colorPal11(4987), colorPal7(4988), colorPal5(4989), colorPal5(4990)], + [colorPal3(4991), colorPal13(4992), colorPal13(4993), colorPal7(4994), colorPal5(4995), colorPal5(4996)], + [colorPal3(4997), colorPal13(4998), colorPal13(4999), colorPal7(5000), colorPal5(5001), colorPal5(5002), colorPal9(5003)], + [colorPal3(5004), colorPal11(5005), colorPal13(5006), colorPal7(5007), colorPal5(5008), colorPal5(5009)], + [colorPal3(5010), colorPal13(5011), colorPal11(5012), colorPal7(5013), colorPal5(5014), colorPal5(5015)], + [colorPal3(5016), colorPal7(5017), colorPal5(5018), colorPal13(5019), colorPal5(5020)], + [colorPal3(5021), colorPal11(5022), colorPal11(5023), colorPal7(5024), colorPal5(5025), colorPal5(5026)], + [colorPal3(5027), colorPal9(5028), colorPal13(5029), colorPal7(5030), colorPal5(5031), colorPal5(5032)], + [colorPal3(5033), colorPal9(5034), colorPal13(5035), colorPal7(5036), colorPal5(5037), colorPal5(5038)], + [colorPal3(5039), colorPal11(5040), colorPal13(5041), colorPal7(5042), colorPal5(5043), colorPal5(5044)], + [colorPal3(5045), colorPal13(5046), colorPal5(5047), colorPal11(5048), colorPal5(5049)], + [colorPal3(5050), colorPal7(5051), colorPal5(5052), colorPal7(5053), colorPal5(5054), colorPal5(5055), colorPal7(5056)] ]; ; export const frontManes: StaticSprites = [ - undefined, - [colorPal3(5057), colorPal13(5058), colorPal13(5059), colorPal7(5060), colorPal5(5061), colorPal5(5062)], - [colorPal3(5063), colorPal11(5064), colorPal11(5065), colorPal7(5066), colorPal5(5067), colorPal5(5068)], - [colorPal3(5069), colorPal13(5070), colorPal13(5071), colorPal5(5072), colorPal5(5073), colorPal7(5074)], - [colorPal3(5075), colorPal11(5076), colorPal11(5077), colorPal7(5078), colorPal5(5079), colorPal5(5080)], - [colorPal3(5081), colorPal9(5082), colorPal9(5083), colorPal7(5084), colorPal5(5085)], - [colorPal3(5086), colorPal11(5087), colorPal9(5088), colorPal7(5089), colorPal5(5090), colorPal5(5091)], - undefined, - [colorPal3(5092), colorPal11(5093), colorPal11(5094), colorPal7(5095), colorPal5(5096), colorPal5(5097)], - undefined, - [colorPal3(5098), colorPal13(5099), colorPal13(5100), colorPal7(5101), colorPal5(5102), colorPal5(5103), colorPal13(5104)], - [colorPal3(5105), colorPal9(5106), colorPal11(5107), colorPal5(5108), colorPal5(5109)], - [colorPal3(5110), colorPal9(5111), colorPal9(5112), colorPal7(5113), colorPal5(5114), colorPal5(5115)], - undefined, - [colorPal3(5116), colorPal11(5117), colorPal9(5118), colorPal7(5119), colorPal5(5120)], - undefined, - undefined, - [colorPal3(5121), colorPal11(5122), colorPal9(5123), colorPal7(5124), colorPal5(5125), colorPal5(5126)], - [colorPal3(5127), colorPal13(5128), colorPal13(5129), colorPal7(5130), colorPal5(5131), colorPal5(5132)], - undefined, - [colorPal3(5133), colorPal11(5134), colorPal11(5135), colorPal7(5136), colorPal5(5137), colorPal5(5138)], - [colorPal3(5139), colorPal13(5140), colorPal13(5141), colorPal7(5142), colorPal5(5143), colorPal5(5144)], - [colorPal3(5145), colorPal13(5146), colorPal13(5147), colorPal7(5148), colorPal5(5149), colorPal5(5150), colorPal9(5151)], - undefined, - [colorPal3(5152), colorPal13(5153), colorPal11(5154), colorPal7(5155), colorPal5(5156), colorPal5(5157)], - [colorPal3(5158), colorPal7(5159), colorPal5(5160), colorPal13(5161), colorPal5(5162)], - [colorPal3(5163), colorPal11(5164), colorPal11(5165), colorPal7(5166), colorPal5(5167), colorPal5(5168)], - undefined, - undefined, - [colorPal3(5169), colorPal11(5170), colorPal13(5171), colorPal7(5172), colorPal5(5173), colorPal5(5174)], - undefined, - [colorPal3(5175), colorPal7(5176), colorPal5(5177), colorPal7(5178), colorPal5(5179), colorPal5(5180), colorPal7(5181)] + undefined, + [colorPal3(5057), colorPal13(5058), colorPal13(5059), colorPal7(5060), colorPal5(5061), colorPal5(5062)], + [colorPal3(5063), colorPal11(5064), colorPal11(5065), colorPal7(5066), colorPal5(5067), colorPal5(5068)], + [colorPal3(5069), colorPal13(5070), colorPal13(5071), colorPal5(5072), colorPal5(5073), colorPal7(5074)], + [colorPal3(5075), colorPal11(5076), colorPal11(5077), colorPal7(5078), colorPal5(5079), colorPal5(5080)], + [colorPal3(5081), colorPal9(5082), colorPal9(5083), colorPal7(5084), colorPal5(5085)], + [colorPal3(5086), colorPal11(5087), colorPal9(5088), colorPal7(5089), colorPal5(5090), colorPal5(5091)], + undefined, + [colorPal3(5092), colorPal11(5093), colorPal11(5094), colorPal7(5095), colorPal5(5096), colorPal5(5097)], + undefined, + [colorPal3(5098), colorPal13(5099), colorPal13(5100), colorPal7(5101), colorPal5(5102), colorPal5(5103), colorPal13(5104)], + [colorPal3(5105), colorPal9(5106), colorPal11(5107), colorPal5(5108), colorPal5(5109)], + [colorPal3(5110), colorPal9(5111), colorPal9(5112), colorPal7(5113), colorPal5(5114), colorPal5(5115)], + undefined, + [colorPal3(5116), colorPal11(5117), colorPal9(5118), colorPal7(5119), colorPal5(5120)], + undefined, + undefined, + [colorPal3(5121), colorPal11(5122), colorPal9(5123), colorPal7(5124), colorPal5(5125), colorPal5(5126)], + [colorPal3(5127), colorPal13(5128), colorPal13(5129), colorPal7(5130), colorPal5(5131), colorPal5(5132)], + undefined, + [colorPal3(5133), colorPal11(5134), colorPal11(5135), colorPal7(5136), colorPal5(5137), colorPal5(5138)], + [colorPal3(5139), colorPal13(5140), colorPal13(5141), colorPal7(5142), colorPal5(5143), colorPal5(5144)], + [colorPal3(5145), colorPal13(5146), colorPal13(5147), colorPal7(5148), colorPal5(5149), colorPal5(5150), colorPal9(5151)], + undefined, + [colorPal3(5152), colorPal13(5153), colorPal11(5154), colorPal7(5155), colorPal5(5156), colorPal5(5157)], + [colorPal3(5158), colorPal7(5159), colorPal5(5160), colorPal13(5161), colorPal5(5162)], + [colorPal3(5163), colorPal11(5164), colorPal11(5165), colorPal7(5166), colorPal5(5167), colorPal5(5168)], + undefined, + undefined, + [colorPal3(5169), colorPal11(5170), colorPal13(5171), colorPal7(5172), colorPal5(5173), colorPal5(5174)], + undefined, + [colorPal3(5175), colorPal7(5176), colorPal5(5177), colorPal7(5178), colorPal5(5179), colorPal5(5180), colorPal7(5181)] ]; ; export const facialHairBehind: StaticSprites = [ - undefined, - [colorPal3(5182)], - [colorPal3(5183), colorPal7(5184)], - undefined, - [colorPal3(5185), colorPal7(5186), colorPal9(5187)], - [colorPal3(5188), colorPal5(5189)], - [colorPal3(5190), colorPal7(5191), colorPal9(5192)], - [colorPal3(5193), colorPal7(5194), colorPal17(5195)], - [colorPal3(5196), colorPal17(5197), colorPal7(5198)], - [colorPal3(5199), colorPal13(5200), colorPal7(5201), colorPal7(5202)], - [colorPal3(5203), colorPal13(5204), colorPal7(5205), colorPal9(5206)], - [colorPal3(5207), colorPal13(5208), colorPal7(5209), colorPal9(5210)], - [colorPal3(5211), colorPal13(5212), colorPal7(5213), colorPal9(5214)], - [colorPal5(5215), colorPal13(5216), colorPal9(5217), colorPal13(5218)], - [colorPal5(5219), colorPal13(5220), colorPal9(5221), colorPal13(5222)], - [colorPal3(5223), colorPal17(5224), colorPal7(5225), colorPal11(5226)] + undefined, + [colorPal3(5182)], + [colorPal3(5183), colorPal7(5184)], + undefined, + [colorPal3(5185), colorPal7(5186), colorPal9(5187)], + [colorPal3(5188), colorPal5(5189)], + [colorPal3(5190), colorPal7(5191), colorPal9(5192)], + [colorPal3(5193), colorPal7(5194), colorPal17(5195)], + [colorPal3(5196), colorPal17(5197), colorPal7(5198)], + [colorPal3(5199), colorPal13(5200), colorPal7(5201), colorPal7(5202)], + [colorPal3(5203), colorPal13(5204), colorPal7(5205), colorPal9(5206)], + [colorPal3(5207), colorPal13(5208), colorPal7(5209), colorPal9(5210)], + [colorPal3(5211), colorPal13(5212), colorPal7(5213), colorPal9(5214)], + [colorPal5(5215), colorPal13(5216), colorPal9(5217), colorPal13(5218)], + [colorPal5(5219), colorPal13(5220), colorPal9(5221), colorPal13(5222)], + [colorPal3(5223), colorPal17(5224), colorPal7(5225), colorPal11(5226)] ]; ; export const facialHair: StaticSprites = [ - undefined, - undefined, - undefined, - [colorPal3(5227)], - [colorPal3(5228), colorPal7(5229), colorPal9(5230)], - [colorPal3(5231), colorPal5(5232)], - undefined, - [colorPal3(5233), colorPal7(5234), colorPal17(5235)], - [colorPal3(5236), colorPal17(5237), colorPal7(5238)], - [colorPal3(5239), colorPal13(5240), colorPal7(5241), colorPal7(5242)], - [colorPal3(5243), colorPal13(5244), colorPal7(5245), colorPal9(5246)], - [colorPal3(5247), colorPal13(5248), colorPal7(5249), colorPal9(5250)], - [colorPal3(5251), colorPal13(5252), colorPal7(5253), colorPal9(5254)], - [colorPal5(5255), colorPal13(5256), colorPal9(5257), colorPal13(5258)], - [colorPal5(5259), colorPal13(5260), colorPal9(5261), colorPal13(5262)], - [colorPal3(5263), colorPal17(5264), colorPal7(5265), colorPal11(5266)] + undefined, + undefined, + undefined, + [colorPal3(5227)], + [colorPal3(5228), colorPal7(5229), colorPal9(5230)], + [colorPal3(5231), colorPal5(5232)], + undefined, + [colorPal3(5233), colorPal7(5234), colorPal17(5235)], + [colorPal3(5236), colorPal17(5237), colorPal7(5238)], + [colorPal3(5239), colorPal13(5240), colorPal7(5241), colorPal7(5242)], + [colorPal3(5243), colorPal13(5244), colorPal7(5245), colorPal9(5246)], + [colorPal3(5247), colorPal13(5248), colorPal7(5249), colorPal9(5250)], + [colorPal3(5251), colorPal13(5252), colorPal7(5253), colorPal9(5254)], + [colorPal5(5255), colorPal13(5256), colorPal9(5257), colorPal13(5258)], + [colorPal5(5259), colorPal13(5260), colorPal9(5261), colorPal13(5262)], + [colorPal3(5263), colorPal17(5264), colorPal7(5265), colorPal11(5266)] ]; ; export const earAccessoriesBehind: StaticSprites = [ - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - [colorPal3(5267)], - undefined, - [colorPal3(5268), colorPal7(5269), colorPal7(5270), colorPal7(5271)], - [colorPal3(5272), colorPal11(5273), colorPal11(5274), colorPal11(5275)], - [] + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal3(5267)], + undefined, + [colorPal3(5268), colorPal7(5269), colorPal7(5270), colorPal7(5271)], + [colorPal3(5272), colorPal11(5273), colorPal11(5274), colorPal11(5275)], + [] ]; ; export const earAccessories: StaticSprites = [ - undefined, - [colorPal3(5276)], - [colorPal3(5277)], - [colorPal3(5278)], - [colorPal3(5279)], - [colorPal5(5280)], - [colorPal3(5281), colorPal11(5282)], - [colorPal3(5283)], - [colorPal3(5284)], - [colorPal3(5285)], - [colorPal3(5286), colorPal7(5287), colorPal7(5288), colorPal7(5289)], - [colorPal3(5290), colorPal11(5291), colorPal11(5292), colorPal11(5293)], - [colorPal3(5294)] + undefined, + [colorPal3(5276)], + [colorPal3(5277)], + [colorPal3(5278)], + [colorPal3(5279)], + [colorPal5(5280)], + [colorPal3(5281), colorPal11(5282)], + [colorPal3(5283)], + [colorPal3(5284)], + [colorPal3(5285)], + [colorPal3(5286), colorPal7(5287), colorPal7(5288), colorPal7(5289)], + [colorPal3(5290), colorPal11(5291), colorPal11(5292), colorPal11(5293)], + [colorPal3(5294)] ]; ; export const headAccessoriesBehind: StaticSprites = [ - undefined, - [colorPal3(5295), colorPal5(5296)], - [colorPal3(5297), colorPal9(5298), colorPal9(5299), colorPal9(5300)], - [colorPal3(5301), colorPal5(5302), colorPal7(5303), colorPal9(5304), colorPal7(5305)], - [colorPal3(5306), colorPal5(5307)], - [colorPal3(5308), colorPal7(5309)], - [colorPal3(5310), colorPal7(5311), colorPal13(5312)], - [colorPal3(5313), colorPal11(5314), colorPal11(5315)], - [colorPal3(5316), colorPal11(5317), colorPal13(5318)], - [colorPal3(5319), colorPal11(5320), colorPal7(5321), colorPal9(5322), colorPal9(5323)], - [colorPal3(5324), colorPal9(5325), colorPal13(5326)], - [colorPal5(5327), colorPal11(5328), colorPal13(5329)], - [colorPal3(5330), colorPal7(5331)], - [colorPal3(5332), colorPal9(5333), colorPal13(5334)], - [colorPal3(5335), colorPal9(5336)], - [colorPal3(5337), colorPal9(5338)], - [colorPal3(5339), colorPal7(5340)], - [colorPal3(5341), colorPal7(5342)], - [colorPal3(5343), colorPal7(5344), colorPal5(5345), colorPal5(5346)], - [colorPal3(5347), colorPal7(5348), colorPal11(5349)], - [colorPal5(5350)] + undefined, + [colorPal3(5295), colorPal5(5296)], + [colorPal3(5297), colorPal9(5298), colorPal9(5299), colorPal9(5300)], + [colorPal3(5301), colorPal5(5302), colorPal7(5303), colorPal9(5304), colorPal7(5305)], + [colorPal3(5306), colorPal5(5307)], + [colorPal3(5308), colorPal7(5309)], + [colorPal3(5310), colorPal7(5311), colorPal13(5312)], + [colorPal3(5313), colorPal11(5314), colorPal11(5315)], + [colorPal3(5316), colorPal11(5317), colorPal13(5318)], + [colorPal3(5319), colorPal11(5320), colorPal7(5321), colorPal9(5322), colorPal9(5323)], + [colorPal3(5324), colorPal9(5325), colorPal13(5326)], + [colorPal5(5327), colorPal11(5328), colorPal13(5329)], + [colorPal3(5330), colorPal7(5331)], + [colorPal3(5332), colorPal9(5333), colorPal13(5334)], + [colorPal3(5335), colorPal9(5336)], + [colorPal3(5337), colorPal9(5338)], + [colorPal3(5339), colorPal7(5340)], + [colorPal3(5341), colorPal7(5342)], + [colorPal3(5343), colorPal7(5344), colorPal5(5345), colorPal5(5346)], + [colorPal3(5347), colorPal7(5348), colorPal11(5349)], + [colorPal5(5350)] ]; ; export const headAccessories: StaticSprites = [ - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - [colorPal5(5351)] + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal5(5351)] ]; ; export const faceAccessories: StaticSpritesExtra = [ - undefined, - [createColorExtraPal(5353, 3, 5352, [10])], - [colorPal3(5354), colorPal7(5355), colorPal7(5356)], - [createColorExtraPal(5358, 3, 5357, [11])], - [createColorExtraPal(5360, 3, 5359, [12])], - [createColorExtraPal(5362, 5, 5361, [11])], - [createColorExtraPal(5364, 3, 5363, [11])], - [createColorExtraPal(5366, 3, 5365, [11])], - [colorPal3(5367)], - [colorPal5(5368)], - [colorPal5(5369)], - [colorPal3(5370), colorPal7(5371), colorPal7(5372)], - [createColorExtraPal(5374, 3, 5373, [13])], - [createColorExtraPal(5376, 5, 5375, [13])], - [colorPal3(5377), colorPal5(5378)] + undefined, + [createColorExtraPal(5353, 3, 5352, [10])], + [colorPal3(5354), colorPal7(5355), colorPal7(5356)], + [createColorExtraPal(5358, 3, 5357, [11])], + [createColorExtraPal(5360, 3, 5359, [12])], + [createColorExtraPal(5362, 5, 5361, [11])], + [createColorExtraPal(5364, 3, 5363, [11])], + [createColorExtraPal(5366, 3, 5365, [11])], + [colorPal3(5367)], + [colorPal5(5368)], + [colorPal5(5369)], + [colorPal3(5370), colorPal7(5371), colorPal7(5372)], + [createColorExtraPal(5374, 3, 5373, [13])], + [createColorExtraPal(5376, 5, 5375, [13])], + [colorPal3(5377), colorPal5(5378)] ]; export const faceAccessoriesExtra: StaticSprites = [ - undefined, - [createColorPalette(5352, [10])], - [emptyColorPalette(), emptyColorPalette(), emptyColorPalette()], - [createColorPalette(5357, [11])], - [createColorPalette(5359, [12])], - [createColorPalette(5361, [11])], - [createColorPalette(5363, [11])], - [createColorPalette(5365, [11])], - [emptyColorPalette()], - [emptyColorPalette()], - [emptyColorPalette()], - [emptyColorPalette(), emptyColorPalette(), emptyColorPalette()], - [createColorPalette(5373, [13])], - [createColorPalette(5375, [13])], - [emptyColorPalette(), emptyColorPalette()], + undefined, + [createColorPalette(5352, [10])], + [emptyColorPalette(), emptyColorPalette(), emptyColorPalette()], + [createColorPalette(5357, [11])], + [createColorPalette(5359, [12])], + [createColorPalette(5361, [11])], + [createColorPalette(5363, [11])], + [createColorPalette(5365, [11])], + [emptyColorPalette()], + [emptyColorPalette()], + [emptyColorPalette()], + [emptyColorPalette(), emptyColorPalette(), emptyColorPalette()], + [createColorPalette(5373, [13])], + [createColorPalette(5375, [13])], + [emptyColorPalette(), emptyColorPalette()], ]; ; export const faceAccessories2: StaticSpritesExtra = [ - undefined, - undefined, - undefined, - undefined, - [colorPal3(5379)], - [colorPal5(5380)], - undefined, - undefined, - undefined, - [colorPal5(5381)], - undefined, - [colorPal3(5382), colorPal7(5383), colorPal7(5384)], - undefined, - [colorPal5(5385)], - [colorPal3(5386), colorPal5(5387)] + undefined, + undefined, + undefined, + undefined, + [colorPal3(5379)], + [colorPal5(5380)], + undefined, + undefined, + undefined, + [colorPal5(5381)], + undefined, + [colorPal3(5382), colorPal7(5383), colorPal7(5384)], + undefined, + [colorPal5(5385)], + [colorPal3(5386), colorPal5(5387)] ]; export const faceAccessories2Extra: StaticSprites = [ - undefined, - undefined, - undefined, - undefined, - [emptyColorPalette()], - [emptyColorPalette()], - undefined, - undefined, - undefined, - [emptyColorPalette()], - undefined, - [emptyColorPalette(), emptyColorPalette(), emptyColorPalette()], - undefined, - [emptyColorPalette()], - [emptyColorPalette(), emptyColorPalette()], + undefined, + undefined, + undefined, + undefined, + [emptyColorPalette()], + [emptyColorPalette()], + undefined, + undefined, + undefined, + [emptyColorPalette()], + undefined, + [emptyColorPalette(), emptyColorPalette(), emptyColorPalette()], + undefined, + [emptyColorPalette()], + [emptyColorPalette(), emptyColorPalette()], ]; ; export const extraAccessoriesBehind: StaticSprites = [ - [colorPal11(5388)], - [colorPal5(5389)], - undefined, - [colorPal7(5390)], - [colorPal9(5391)], - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - [colorPal7(5392)], - [colorPal9(5393)], - [colorPal5(5394)], - [colorPal5(5395)], - [colorPal13(5396)], - [] + [colorPal11(5388)], + [colorPal5(5389)], + undefined, + [colorPal7(5390)], + [colorPal9(5391)], + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [colorPal7(5392)], + [colorPal9(5393)], + [colorPal5(5394)], + [colorPal5(5395)], + [colorPal13(5396)], + [] ]; ; export const extraAccessories: StaticSprites = [ - [colorPal11(5397)], - [colorPal5(5398)], - [colorPal5(5399)], - [colorPal7(5400)], - [colorPal9(5401)], - [colorPal13(5402)], - [colorPal5(5403)], - [colorPal11(5404)], - [colorPal9(5405)], - [colorPal11(5406), colorPal11(5407)], - [colorPal9(5408)], - [colorPal9(5409), colorPal9(5410)], - [colorPal7(5411)], - [colorPal9(5412)], - undefined, - undefined, - [colorPal13(5413)], - [colorPal13(5414)] + [colorPal11(5397)], + [colorPal5(5398)], + [colorPal5(5399)], + [colorPal7(5400)], + [colorPal9(5401)], + [colorPal13(5402)], + [colorPal5(5403)], + [colorPal11(5404)], + [colorPal9(5405)], + [colorPal11(5406), colorPal11(5407)], + [colorPal9(5408)], + [colorPal9(5409), colorPal9(5410)], + [colorPal7(5411)], + [colorPal9(5412)], + undefined, + undefined, + [colorPal13(5413)], + [colorPal13(5414)] ]; ; @@ -3283,88 +3283,88 @@ export const emote_cry2 = createAnimationPalette([6484, 6485, 6486, 6487, 6488, export const emote_sleep2 = createAnimationPalette([6497, 6498, 6499, 6500, 6501, 6502, 6503, 6504, 6505, 6506, 6507, 6508, 6509, 6510, 6511, 6512, 6513, 6514, 6515, 6516, 6517, 6518, 6519, 6520, 6521, 6522, 6523, 6524, 6525, 6526, 6527, 6528, 6529, 6530, 6531, 6532, 6533, 6534, 6535, 6536], 159); export const emote_sleep2_flip = createAnimationPalette([6537, 6538, 6539, 6540, 6541, 6542, 6543, 6544, 6545, 6546, 6547, 6548, 6549, 6550, 6551, 6552, 6553, 6554, 6555, 6556, 6557, 6558, 6559, 6560, 6561, 6562, 6563, 6564, 6565, 6566, 6567, 6568, 6569, 6570, 6571, 6572, 6573, 6574, 6575, 6576], 159); export const wall_wood_full = [ - /* no palettes */ createColorPalette(6577, [160]), - /* no palettes */ createColorPalette(6578, [160]), - /* no palettes */ createColorPalette(6579, [160]), - /* no palettes */ createColorPalette(6580, [160]), - /* no palettes */ createColorPalette(6581, [160]), - /* no palettes */ createColorPalette(6582, [160]), - /* no palettes */ createColorPalette(6583, [160]), - /* no palettes */ createColorPalette(6584, [160]), - /* no palettes */ createColorPalette(6585, [160]), - /* no palettes */ createColorPalette(6586, [160]), - /* no palettes */ createColorPalette(6587, [160]), - /* no palettes */ createColorPalette(6588, [160]), - /* no palettes */ createColorPalette(6589, [160]), - /* no palettes */ createColorPalette(6590, [160]), - /* no palettes */ createColorPalette(6591, [160]), - /* no palettes */ createColorPalette(6592, [160]), - /* no palettes */ createColorPalette(6593, [160]), - /* no palettes */ createColorPalette(6594, [160]), - /* no palettes */ createColorPalette(6595, [160]), - /* no palettes */ createColorPalette(6596, [160]) + /* no palettes */ createColorPalette(6577, [160]), + /* no palettes */ createColorPalette(6578, [160]), + /* no palettes */ createColorPalette(6579, [160]), + /* no palettes */ createColorPalette(6580, [160]), + /* no palettes */ createColorPalette(6581, [160]), + /* no palettes */ createColorPalette(6582, [160]), + /* no palettes */ createColorPalette(6583, [160]), + /* no palettes */ createColorPalette(6584, [160]), + /* no palettes */ createColorPalette(6585, [160]), + /* no palettes */ createColorPalette(6586, [160]), + /* no palettes */ createColorPalette(6587, [160]), + /* no palettes */ createColorPalette(6588, [160]), + /* no palettes */ createColorPalette(6589, [160]), + /* no palettes */ createColorPalette(6590, [160]), + /* no palettes */ createColorPalette(6591, [160]), + /* no palettes */ createColorPalette(6592, [160]), + /* no palettes */ createColorPalette(6593, [160]), + /* no palettes */ createColorPalette(6594, [160]), + /* no palettes */ createColorPalette(6595, [160]), + /* no palettes */ createColorPalette(6596, [160]) ]; export const wall_wood_half = [ - /* no palettes */ createColorPalette(6597, [160]), - /* no palettes */ createColorPalette(6598, [160]), - /* no palettes */ createColorPalette(6599, [160]), - /* no palettes */ createColorPalette(6600, [160]), - /* no palettes */ createColorPalette(6601, [160]), - /* no palettes */ createColorPalette(6602, [160]), - /* no palettes */ createColorPalette(6603, [160]), - /* no palettes */ createColorPalette(6604, [160]), - /* no palettes */ createColorPalette(6605, [160]), - /* no palettes */ createColorPalette(6606, [160]), - /* no palettes */ createColorPalette(6607, [160]), - /* no palettes */ createColorPalette(6608, [160]), - /* no palettes */ createColorPalette(6609, [160]), - /* no palettes */ createColorPalette(6610, [160]), - /* no palettes */ createColorPalette(6611, [160]), - /* no palettes */ createColorPalette(6612, [160]), - /* no palettes */ createColorPalette(6613, [160]), - /* no palettes */ createColorPalette(6614, [160]) + /* no palettes */ createColorPalette(6597, [160]), + /* no palettes */ createColorPalette(6598, [160]), + /* no palettes */ createColorPalette(6599, [160]), + /* no palettes */ createColorPalette(6600, [160]), + /* no palettes */ createColorPalette(6601, [160]), + /* no palettes */ createColorPalette(6602, [160]), + /* no palettes */ createColorPalette(6603, [160]), + /* no palettes */ createColorPalette(6604, [160]), + /* no palettes */ createColorPalette(6605, [160]), + /* no palettes */ createColorPalette(6606, [160]), + /* no palettes */ createColorPalette(6607, [160]), + /* no palettes */ createColorPalette(6608, [160]), + /* no palettes */ createColorPalette(6609, [160]), + /* no palettes */ createColorPalette(6610, [160]), + /* no palettes */ createColorPalette(6611, [160]), + /* no palettes */ createColorPalette(6612, [160]), + /* no palettes */ createColorPalette(6613, [160]), + /* no palettes */ createColorPalette(6614, [160]) ]; export const wall_stone_full = [ - /* no palettes */ createColorPalette(6615, [161]), - /* no palettes */ createColorPalette(6616, [161]), - /* no palettes */ createColorPalette(6617, [161]), - /* no palettes */ createColorPalette(6618, [161]), - /* no palettes */ createColorPalette(6619, [161]), - /* no palettes */ createColorPalette(6620, [161]), - /* no palettes */ createColorPalette(6621, [161]), - /* no palettes */ createColorPalette(6622, [161]), - /* no palettes */ createColorPalette(6623, [161]), - /* no palettes */ createColorPalette(6624, [161]), - /* no palettes */ createColorPalette(6625, [161]), - /* no palettes */ createColorPalette(6626, [161]), - /* no palettes */ createColorPalette(6627, [161]), - /* no palettes */ createColorPalette(6628, [161]), - /* no palettes */ createColorPalette(6629, [161]), - /* no palettes */ createColorPalette(6630, [161]), - /* no palettes */ createColorPalette(6631, [161]), - /* no palettes */ createColorPalette(6632, [161]), - /* no palettes */ createColorPalette(6633, [161]), - /* no palettes */ createColorPalette(6634, [161]) + /* no palettes */ createColorPalette(6615, [161]), + /* no palettes */ createColorPalette(6616, [161]), + /* no palettes */ createColorPalette(6617, [161]), + /* no palettes */ createColorPalette(6618, [161]), + /* no palettes */ createColorPalette(6619, [161]), + /* no palettes */ createColorPalette(6620, [161]), + /* no palettes */ createColorPalette(6621, [161]), + /* no palettes */ createColorPalette(6622, [161]), + /* no palettes */ createColorPalette(6623, [161]), + /* no palettes */ createColorPalette(6624, [161]), + /* no palettes */ createColorPalette(6625, [161]), + /* no palettes */ createColorPalette(6626, [161]), + /* no palettes */ createColorPalette(6627, [161]), + /* no palettes */ createColorPalette(6628, [161]), + /* no palettes */ createColorPalette(6629, [161]), + /* no palettes */ createColorPalette(6630, [161]), + /* no palettes */ createColorPalette(6631, [161]), + /* no palettes */ createColorPalette(6632, [161]), + /* no palettes */ createColorPalette(6633, [161]), + /* no palettes */ createColorPalette(6634, [161]) ]; export const wall_stone_half = [ - /* no palettes */ createColorPalette(6635, [161]), - /* no palettes */ createColorPalette(6636, [161]), - /* no palettes */ createColorPalette(6637, [161]), - /* no palettes */ createColorPalette(6638, [161]), - /* no palettes */ createColorPalette(6639, [161]), - /* no palettes */ createColorPalette(6640, [161]), - /* no palettes */ createColorPalette(6641, [161]), - /* no palettes */ createColorPalette(6642, [161]), - /* no palettes */ createColorPalette(6643, [161]), - /* no palettes */ createColorPalette(6644, [161]), - /* no palettes */ createColorPalette(6645, [161]), - /* no palettes */ createColorPalette(6646, [161]), - /* no palettes */ createColorPalette(6647, [161]), - /* no palettes */ createColorPalette(6648, [161]), - /* no palettes */ createColorPalette(6649, [161]), - /* no palettes */ createColorPalette(6650, [161]), - /* no palettes */ createColorPalette(6651, [161]), - /* no palettes */ createColorPalette(6652, [161]) + /* no palettes */ createColorPalette(6635, [161]), + /* no palettes */ createColorPalette(6636, [161]), + /* no palettes */ createColorPalette(6637, [161]), + /* no palettes */ createColorPalette(6638, [161]), + /* no palettes */ createColorPalette(6639, [161]), + /* no palettes */ createColorPalette(6640, [161]), + /* no palettes */ createColorPalette(6641, [161]), + /* no palettes */ createColorPalette(6642, [161]), + /* no palettes */ createColorPalette(6643, [161]), + /* no palettes */ createColorPalette(6644, [161]), + /* no palettes */ createColorPalette(6645, [161]), + /* no palettes */ createColorPalette(6646, [161]), + /* no palettes */ createColorPalette(6647, [161]), + /* no palettes */ createColorPalette(6648, [161]), + /* no palettes */ createColorPalette(6649, [161]), + /* no palettes */ createColorPalette(6650, [161]), + /* no palettes */ createColorPalette(6651, [161]), + /* no palettes */ createColorPalette(6652, [161]) ]; export const pixelRect2 = sprites2[6653]; export const grassTiles = createSpritesPalette([6654, 6655, 6656, 6657, 6658, 6659, 6660, 6661, 6662, 6663, 6664, 6665, 6666, 6667, 6668, 6669, 6670, 6671, 6672, 6673, 6674, 6675, 6676, 6677, 6678, 6679, 6680, 6681, 6682, 6683, 6684, 6685, 6686, 6687, 6688, 6689, 6690, 6691, 6692, 6693, 6694, 6695, 6696, 6697, 6698, 6699, 6700, 6701, 6702, 6703, 6704, 6705, 6706, 6707, 6708, 6709, 6710, 6711, 6712, 6713, 6714, 6715, 6716, 6717, 6718, 6719], [162, 163]); @@ -3467,12 +3467,11 @@ export const cave_walls_se = createColorPalette(7595, [181]); export const pixel2 = sprites2[14653]; export const head0 = [ - undefined, - [[0, 1, 2, 4, 7, 8, 9, 10, 12, 13, 14, 16, 18, 19].map(i => head[1]![0]![i])], + undefined, + [[0, 1, 2, 4, 7, 8, 9, 10, 12, 13, 14, 16, 18, 19].map(i => head[1]![0]![i])], ]; export const head1 = [ - undefined, - [[0, 1, 3, 5, 6, 8, 9, 11, 12, 13, 15, 17, 18, 19].map(i => head[1]![0]![i])], + undefined, + [[0, 1, 3, 5, 6, 8, 9, 11, 12, 13, 15, 17, 18, 19].map(i => head[1]![0]![i])], ]; - \ No newline at end of file diff --git a/src/ts/graphics/baseSpriteBatch.ts b/src/ts/graphics/baseSpriteBatch.ts index 4054de8..d9220ee 100644 --- a/src/ts/graphics/baseSpriteBatch.ts +++ b/src/ts/graphics/baseSpriteBatch.ts @@ -34,169 +34,169 @@ import { isIdentity } from '../common/mat2d'; const WHITE_FLOAT = colorToFloat(WHITE); export function getColorFloat(color: number, alpha: number) { - return (color === WHITE && alpha === 1) ? WHITE_FLOAT : colorToFloatAlpha(color, alpha); + return (color === WHITE && alpha === 1) ? WHITE_FLOAT : colorToFloatAlpha(color, alpha); } export abstract class BaseSpriteBatch extends BaseStateBatch implements SpriteBatchBase { - tris = 0; - flushes = 0; - index = 0; - spritesCount = 0; - vertices!: Float32Array; - verticesUint32!: Uint32Array; - vao: VAO | undefined = undefined; - rectSprite: Sprite | undefined = undefined; - vertexBuffer: WebGLBuffer | undefined = undefined; - indexBuffer: WebGLBuffer | undefined = undefined; - spriteSheet: SpriteSheet | undefined = undefined; - floatsPerSprite: number; - batching = false; - private startBatchIndex = 0; - private startBatchSprites = 0; - constructor( - public gl: WebGLRenderingContext, - public capacity: number, - buffer: ArrayBuffer, - vertexBuffer: WebGLBuffer, - indexBuffer: WebGLBuffer, - public attributes: VAOAttributeDefinition[], - ) { - super(); - this.floatsPerSprite = getVAOAttributesSize(gl, attributes); - this.vertices = new Float32Array(buffer, 0, capacity * this.floatsPerSprite); - this.verticesUint32 = new Uint32Array(buffer, 0, capacity * this.floatsPerSprite); - this.vertexBuffer = vertexBuffer; - this.indexBuffer = indexBuffer; - this.vao = createVAO(gl, createVAOAttributes(gl, attributes, vertexBuffer), indexBuffer); - } - dispose() { - disposeBuffers(this.gl, this); - } - begin() { - if (!this.vao) { - throw new Error('Disposed'); - } + tris = 0; + flushes = 0; + index = 0; + spritesCount = 0; + vertices!: Float32Array; + verticesUint32!: Uint32Array; + vao: VAO | undefined = undefined; + rectSprite: Sprite | undefined = undefined; + vertexBuffer: WebGLBuffer | undefined = undefined; + indexBuffer: WebGLBuffer | undefined = undefined; + spriteSheet: SpriteSheet | undefined = undefined; + floatsPerSprite: number; + batching = false; + private startBatchIndex = 0; + private startBatchSprites = 0; + constructor( + public gl: WebGLRenderingContext, + public capacity: number, + buffer: ArrayBuffer, + vertexBuffer: WebGLBuffer, + indexBuffer: WebGLBuffer, + public attributes: VAOAttributeDefinition[], + ) { + super(); + this.floatsPerSprite = getVAOAttributesSize(gl, attributes); + this.vertices = new Float32Array(buffer, 0, capacity * this.floatsPerSprite); + this.verticesUint32 = new Uint32Array(buffer, 0, capacity * this.floatsPerSprite); + this.vertexBuffer = vertexBuffer; + this.indexBuffer = indexBuffer; + this.vao = createVAO(gl, createVAOAttributes(gl, attributes, vertexBuffer), indexBuffer); + } + dispose() { + disposeBuffers(this.gl, this); + } + begin() { + if (!this.vao) { + throw new Error('Disposed'); + } - this.batching = false; - this.vao.bind(); - } - end() { - if (!this.vao) { - throw new Error('Disposed'); - } + this.batching = false; + this.vao.bind(); + } + end() { + if (!this.vao) { + throw new Error('Disposed'); + } - this.flush(); - this.vao.unbind(); - } - drawBatch(batch: Batch) { - if (DEVELOPMENT && !isIdentity(this.transform)) { - throw new Error('Cannot transform batch'); - } + this.flush(); + this.vao.unbind(); + } + drawBatch(batch: Batch) { + if (DEVELOPMENT && !isIdentity(this.transform)) { + throw new Error('Cannot transform batch'); + } - const batchSpriteCount = (batch.length / this.floatsPerSprite) | 0; + const batchSpriteCount = (batch.length / this.floatsPerSprite) | 0; - if (this.capacity < (this.spritesCount + batchSpriteCount)) { - this.flush(); - } + if (this.capacity < (this.spritesCount + batchSpriteCount)) { + this.flush(); + } - this.vertices.set(batch, this.index); - this.index += batch.length; - this.spritesCount += batchSpriteCount; - this.tris += batchSpriteCount * 2; - } - startBatch() { - if (this.batching) { - throw new Error('Cannot start new batch'); - } + this.vertices.set(batch, this.index); + this.index += batch.length; + this.spritesCount += batchSpriteCount; + this.tris += batchSpriteCount * 2; + } + startBatch() { + if (this.batching) { + throw new Error('Cannot start new batch'); + } - this.startBatchIndex = this.index; - this.startBatchSprites = this.spritesCount; - this.batching = true; - } - finishBatch(): Batch | undefined { - if (!this.batching) { - throw new Error('Cannot finish batch'); - } + this.startBatchIndex = this.index; + this.startBatchSprites = this.spritesCount; + this.batching = true; + } + finishBatch(): Batch | undefined { + if (!this.batching) { + throw new Error('Cannot finish batch'); + } - this.batching = false; + this.batching = false; - try { - // const batch = aquireBuffer(this.index - this.startBatchIndex); + try { + // const batch = aquireBuffer(this.index - this.startBatchIndex); - // if (batch) { - // batch.set(this.vertices.subarray(this.startBatchIndex, this.index)); - // } + // if (batch) { + // batch.set(this.vertices.subarray(this.startBatchIndex, this.index)); + // } - // return batch; - return this.vertices.slice(this.startBatchIndex, this.index); - } catch { - return undefined; - } - } - releaseBatch(_batch: Batch) { - // releaseBuffer(batch); - } - flush() { - if (this.index === 0) - return; + // return batch; + return this.vertices.slice(this.startBatchIndex, this.index); + } catch { + return undefined; + } + } + releaseBatch(_batch: Batch) { + // releaseBuffer(batch); + } + flush() { + if (this.index === 0) + return; - if (!this.vao || !this.vertexBuffer) { - throw new Error('Disposed'); - } + if (!this.vao || !this.vertexBuffer) { + throw new Error('Disposed'); + } - const gl = this.gl; + const gl = this.gl; - if (this.batching) { - TIMING && timeStart('bufferSubData'); - gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); - gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices.subarray(0, this.startBatchIndex)); - TIMING && timeEnd(); + if (this.batching) { + TIMING && timeStart('bufferSubData'); + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices.subarray(0, this.startBatchIndex)); + TIMING && timeEnd(); - TIMING && timeStart('vao.draw'); - this.vao.draw(this.gl.TRIANGLES, this.startBatchSprites * 6, 0); - TIMING && timeEnd(); + TIMING && timeStart('vao.draw'); + this.vao.draw(this.gl.TRIANGLES, this.startBatchSprites * 6, 0); + TIMING && timeEnd(); - this.spritesCount -= this.startBatchSprites; - this.index -= this.startBatchIndex; - this.vertices.copyWithin(0, this.startBatchIndex, this.startBatchIndex + this.index); - this.startBatchIndex = 0; - this.startBatchSprites = 0; - } else { - TIMING && timeStart('bufferSubData'); - gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); - gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices.subarray(0, this.index)); - TIMING && timeEnd(); + this.spritesCount -= this.startBatchSprites; + this.index -= this.startBatchIndex; + this.vertices.copyWithin(0, this.startBatchIndex, this.startBatchIndex + this.index); + this.startBatchIndex = 0; + this.startBatchSprites = 0; + } else { + TIMING && timeStart('bufferSubData'); + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices.subarray(0, this.index)); + TIMING && timeEnd(); - TIMING && timeStart('vao.draw'); - this.vao.draw(this.gl.TRIANGLES, this.spritesCount * 6, 0); - TIMING && timeEnd(); + TIMING && timeStart('vao.draw'); + this.vao.draw(this.gl.TRIANGLES, this.spritesCount * 6, 0); + TIMING && timeEnd(); - this.spritesCount = 0; - this.index = 0; - } + this.spritesCount = 0; + this.index = 0; + } - this.flushes++; - } + this.flushes++; + } } function disposeBuffers(gl: WebGLRenderingContext, batch: BaseSpriteBatch) { - try { - if (batch.vao) { - batch.vao.dispose(); - } + try { + if (batch.vao) { + batch.vao.dispose(); + } - if (batch.vertexBuffer) { - gl.deleteBuffer(batch.vertexBuffer); - } + if (batch.vertexBuffer) { + gl.deleteBuffer(batch.vertexBuffer); + } - if (batch.indexBuffer) { - gl.deleteBuffer(batch.indexBuffer); - } - } catch (e) { - DEVELOPMENT && console.error(e); - } + if (batch.indexBuffer) { + gl.deleteBuffer(batch.indexBuffer); + } + } catch (e) { + DEVELOPMENT && console.error(e); + } - batch.vao = undefined; - batch.vertexBuffer = undefined; - batch.indexBuffer = undefined; + batch.vao = undefined; + batch.vertexBuffer = undefined; + batch.indexBuffer = undefined; } diff --git a/src/ts/graphics/baseStateBatch.ts b/src/ts/graphics/baseStateBatch.ts index 219f31a..6f54e33 100644 --- a/src/ts/graphics/baseStateBatch.ts +++ b/src/ts/graphics/baseStateBatch.ts @@ -4,73 +4,73 @@ import { createMat2D, copyMat2D, mulMat2D, rotateMat2D, scaleMat2D, translateMat import { rect, copyRect } from '../common/rect'; interface SavedState { - globalAlpha: number; - transform: Matrix2D; - hasCrop: boolean; - cropRect: Rect; + globalAlpha: number; + transform: Matrix2D; + hasCrop: boolean; + cropRect: Rect; } function createEmptyState(): SavedState { - return { - transform: createMat2D(), - globalAlpha: 1, - hasCrop: false, - cropRect: rect(0, 0, 0, 0), - }; + return { + transform: createMat2D(), + globalAlpha: 1, + hasCrop: false, + cropRect: rect(0, 0, 0, 0), + }; } const stateCache = new ObjectCache(10, createEmptyState); export abstract class BaseStateBatch { - globalAlpha = 1; - transform = createMat2D(); - private savedStates: SavedState[] = []; - protected hasCrop = false; - protected cropRect = rect(0, 0, 0, 0); - crop(x: number, y: number, w: number, h: number) { - // console.error('Crop not supported'); - this.hasCrop = true; - this.cropRect.x = x; - this.cropRect.y = y; - this.cropRect.w = w; - this.cropRect.h = h; - } - clearCrop() { - this.hasCrop = false; - } - save() { - const state = stateCache.get(); - state.globalAlpha = this.globalAlpha; - state.hasCrop = this.hasCrop; - copyMat2D(state.transform, this.transform); - copyRect(state.cropRect, this.cropRect); - this.savedStates.push(state); + globalAlpha = 1; + transform = createMat2D(); + private savedStates: SavedState[] = []; + protected hasCrop = false; + protected cropRect = rect(0, 0, 0, 0); + crop(x: number, y: number, w: number, h: number) { + // console.error('Crop not supported'); + this.hasCrop = true; + this.cropRect.x = x; + this.cropRect.y = y; + this.cropRect.w = w; + this.cropRect.h = h; + } + clearCrop() { + this.hasCrop = false; + } + save() { + const state = stateCache.get(); + state.globalAlpha = this.globalAlpha; + state.hasCrop = this.hasCrop; + copyMat2D(state.transform, this.transform); + copyRect(state.cropRect, this.cropRect); + this.savedStates.push(state); - if (DEVELOPMENT && this.savedStates.length > 100) { - console.error('More than 100 save states'); - } - } - restore() { - const state = this.savedStates.pop(); + if (DEVELOPMENT && this.savedStates.length > 100) { + console.error('More than 100 save states'); + } + } + restore() { + const state = this.savedStates.pop(); - if (state !== undefined) { - this.globalAlpha = state.globalAlpha; - this.hasCrop = state.hasCrop; - copyMat2D(this.transform, state.transform); - copyRect(this.cropRect, state.cropRect); - stateCache.put(state); - } - } - translate(x: number, y: number) { - translateMat2D(this.transform, this.transform, x, y); - } - scale(x: number, y: number) { - scaleMat2D(this.transform, this.transform, x, y); - } - rotate(angle: number) { - rotateMat2D(this.transform, this.transform, angle); - } - multiplyTransform(mat: Matrix2D) { - mulMat2D(this.transform, this.transform, mat); - } + if (state !== undefined) { + this.globalAlpha = state.globalAlpha; + this.hasCrop = state.hasCrop; + copyMat2D(this.transform, state.transform); + copyRect(this.cropRect, state.cropRect); + stateCache.put(state); + } + } + translate(x: number, y: number) { + translateMat2D(this.transform, this.transform, x, y); + } + scale(x: number, y: number) { + scaleMat2D(this.transform, this.transform, x, y); + } + rotate(angle: number) { + rotateMat2D(this.transform, this.transform, angle); + } + multiplyTransform(mat: Matrix2D) { + mulMat2D(this.transform, this.transform, mat); + } } diff --git a/src/ts/graphics/contextSpriteBatch.ts b/src/ts/graphics/contextSpriteBatch.ts index 61227fb..13bf999 100644 --- a/src/ts/graphics/contextSpriteBatch.ts +++ b/src/ts/graphics/contextSpriteBatch.ts @@ -7,340 +7,340 @@ import { isTranslation } from '../common/mat2d'; import { TRANSPARENT } from '../common/colors'; export function drawBatch( - canvas: HTMLCanvasElement, sheet: SpriteSheet, bg: number | undefined, action: (batch: ContextSpriteBatch) => void + canvas: HTMLCanvasElement, sheet: SpriteSheet, bg: number | undefined, action: (batch: ContextSpriteBatch) => void ) { - const batch = new ContextSpriteBatch(canvas); - batch.start(sheet, bg || TRANSPARENT); - action(batch); - batch.end(); - return canvas; + const batch = new ContextSpriteBatch(canvas); + batch.start(sheet, bg || TRANSPARENT); + action(batch); + batch.end(); + return canvas; } export function drawCanvas( - width: number, height: number, sheet: SpriteSheet, bg: number | undefined, action: (batch: ContextSpriteBatch) => void + width: number, height: number, sheet: SpriteSheet, bg: number | undefined, action: (batch: ContextSpriteBatch) => void ) { - return drawBatch(createCanvas(width, height), sheet, bg, action); + return drawBatch(createCanvas(width, height), sheet, bg, action); } export class ContextSpriteBatch extends BaseStateBatch implements PaletteSpriteBatch, SpriteBatch { - pixelSize = 1; - disableShading = false; - ignoreColor = 0; - palette = false; - private started = false; - private data: ImageData | undefined = undefined; - private sheet: SpriteSheet | undefined = undefined; - private sheetData: ImageData | undefined = undefined; - constructor(public canvas: HTMLCanvasElement) { - super(); - } - start(sheet: SpriteSheet, clearColor: number) { - if (!this.data || this.data.width !== this.canvas.width || this.data.height !== this.canvas.height) { - if (this.canvas && this.canvas.width && this.canvas.height) { - this.data = this.canvas.getContext('2d')!.getImageData(0, 0, this.canvas.width, this.canvas.height); - } - } + pixelSize = 1; + disableShading = false; + ignoreColor = 0; + palette = false; + private started = false; + private data: ImageData | undefined = undefined; + private sheet: SpriteSheet | undefined = undefined; + private sheetData: ImageData | undefined = undefined; + constructor(public canvas: HTMLCanvasElement) { + super(); + } + start(sheet: SpriteSheet, clearColor: number) { + if (!this.data || this.data.width !== this.canvas.width || this.data.height !== this.canvas.height) { + if (this.canvas && this.canvas.width && this.canvas.height) { + this.data = this.canvas.getContext('2d')!.getImageData(0, 0, this.canvas.width, this.canvas.height); + } + } - if (this.data) { - const color = clearColor || 0; - const r = getR(color); - const g = getG(color); - const b = getB(color); - const a = getAlpha(color); - const data = this.data.data; + if (this.data) { + const color = clearColor || 0; + const r = getR(color); + const g = getG(color); + const b = getB(color); + const a = getAlpha(color); + const data = this.data.data; - for (let i = 0; i < data.length; i += 4) { - data[i] = r; - data[i + 1] = g; - data[i + 2] = b; - data[i + 3] = a; - } - } + for (let i = 0; i < data.length; i += 4) { + data[i] = r; + data[i + 1] = g; + data[i + 2] = b; + data[i + 3] = a; + } + } - this.sheet = sheet; - this.sheetData = sheet.data; - this.palette = this.sheet.palette; - this.started = true; - } - // clear(color?: number) { - // this.end(); + this.sheet = sheet; + this.sheetData = sheet.data; + this.palette = this.sheet.palette; + this.started = true; + } + // clear(color?: number) { + // this.end(); - // const context = this.canvas.getContext('2d')!; + // const context = this.canvas.getContext('2d')!; - // if (color !== undefined) { - // context.fillStyle = colorToCSS(color); - // context.fillRect(0, 0, this.canvas.width, this.canvas.height); - // } else { - // context.clearRect(0, 0, this.canvas.width, this.canvas.height); - // } - // } - end() { - if (this.started) { - if (this.data) { - this.canvas.getContext('2d')!.putImageData(this.data, 0, 0); - } + // if (color !== undefined) { + // context.fillStyle = colorToCSS(color); + // context.fillRect(0, 0, this.canvas.width, this.canvas.height); + // } else { + // context.clearRect(0, 0, this.canvas.width, this.canvas.height); + // } + // } + end() { + if (this.started) { + if (this.data) { + this.canvas.getContext('2d')!.putImageData(this.data, 0, 0); + } - this.started = false; - } - } - drawSprite(s: Sprite | undefined, color: number, palette: Palette | undefined | number, x: number, y?: number) { - if (s !== undefined) { - if (y === undefined) { - y = x; - x = palette as number; - drawImageNormal( - this.sheetData, this.data, this.transform, this.globalAlpha, - color, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h); - } else { - drawImagePalette( - this.sheetData, this.data, this.transform, this.globalAlpha, - this.ignoreColor, this.disableShading, - s.type, color, palette as Palette, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h); - } - } - } - drawImage( // SpriteBatch - color: number, - sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number - ): void; - drawImage( // PaletteSpriteBatch3 - type: number, color: number, palette: Palette | undefined, - sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void; - drawImage( - colorOrType: number, sxOrColor: number, syOrPalette: Palette | undefined | number, - swOrSx: number, shOrSy: number, dxOrSw: number, dyOrSh: number, - dwOrDx: number, dhOrDy: number, _OrDw?: number, _OrDh?: number, - ) { - if (_OrDh === undefined) { - drawImageNormal( - this.sheetData, this.data, this.transform, this.globalAlpha, - colorOrType, sxOrColor, syOrPalette as number, - swOrSx, shOrSy, dxOrSw, dyOrSh, dwOrDx, dhOrDy); - } else { - drawImagePalette( - this.sheetData, this.data, this.transform, this.globalAlpha, - this.ignoreColor, this.disableShading, - colorOrType, sxOrColor, syOrPalette as Palette | undefined, - swOrSx, shOrSy, dxOrSw, dyOrSh, dwOrDx, dhOrDy, _OrDw!, _OrDh!); - } - } - drawRect(color: number, x: number, y: number, w: number, h: number) { - drawRect(this.data, this.transform, this.globalAlpha, color, x, y, w, h); - } - drawBatch() { - throw new Error('drawBatch not supported'); - } - startBatch() { - } - finishBatch() { - return undefined; - } - releaseBatch() { - } + this.started = false; + } + } + drawSprite(s: Sprite | undefined, color: number, palette: Palette | undefined | number, x: number, y?: number) { + if (s !== undefined) { + if (y === undefined) { + y = x; + x = palette as number; + drawImageNormal( + this.sheetData, this.data, this.transform, this.globalAlpha, + color, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h); + } else { + drawImagePalette( + this.sheetData, this.data, this.transform, this.globalAlpha, + this.ignoreColor, this.disableShading, + s.type, color, palette as Palette, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h); + } + } + } + drawImage( // SpriteBatch + color: number, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + ): void; + drawImage( // PaletteSpriteBatch3 + type: number, color: number, palette: Palette | undefined, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void; + drawImage( + colorOrType: number, sxOrColor: number, syOrPalette: Palette | undefined | number, + swOrSx: number, shOrSy: number, dxOrSw: number, dyOrSh: number, + dwOrDx: number, dhOrDy: number, _OrDw?: number, _OrDh?: number, + ) { + if (_OrDh === undefined) { + drawImageNormal( + this.sheetData, this.data, this.transform, this.globalAlpha, + colorOrType, sxOrColor, syOrPalette as number, + swOrSx, shOrSy, dxOrSw, dyOrSh, dwOrDx, dhOrDy); + } else { + drawImagePalette( + this.sheetData, this.data, this.transform, this.globalAlpha, + this.ignoreColor, this.disableShading, + colorOrType, sxOrColor, syOrPalette as Palette | undefined, + swOrSx, shOrSy, dxOrSw, dyOrSh, dwOrDx, dhOrDy, _OrDw!, _OrDh!); + } + } + drawRect(color: number, x: number, y: number, w: number, h: number) { + drawRect(this.data, this.transform, this.globalAlpha, color, x, y, w, h); + } + drawBatch() { + throw new Error('drawBatch not supported'); + } + startBatch() { + } + finishBatch() { + return undefined; + } + releaseBatch() { + } } const min = Math.min; const typeOffsets = [0, 2, 3, 0, 1, 2, 3]; function drawRect( - dst: ImageData | undefined, transform: Matrix2D, globalAlpha: number, - color: number, x: number, y: number, w: number, h: number + dst: ImageData | undefined, transform: Matrix2D, globalAlpha: number, + color: number, x: number, y: number, w: number, h: number ) { - if (DEVELOPMENT && !isTranslation(transform)) { - console.error('Transform not supported'); - } + if (DEVELOPMENT && !isTranslation(transform)) { + console.error('Transform not supported'); + } - if (!dst) - return; + if (!dst) + return; - x = Math.round(x + transform[4]); - y = Math.round(y + transform[5]); + x = Math.round(x + transform[4]); + y = Math.round(y + transform[5]); - const xx = min(0, x, x); - w += xx; - x -= xx; + const xx = min(0, x, x); + w += xx; + x -= xx; - const yy = min(0, y, y); - h += yy; - y -= yy; + const yy = min(0, y, y); + h += yy; + y -= yy; - w += min(0, dst.width - (x + w)); - h += min(0, dst.height - (y + h)); + w += min(0, dst.width - (x + w)); + h += min(0, dst.height - (y + h)); - if (w <= 0 && h <= 0) - return; + if (w <= 0 && h <= 0) + return; - const { r, g, b, a } = colorToRGBA(color); - const alpha = (globalAlpha * a) | 0; + const { r, g, b, a } = colorToRGBA(color); + const alpha = (globalAlpha * a) | 0; - if (alpha === 0) - return; + if (alpha === 0) + return; - const dstData = dst.data; - const dstWidth = dst.width | 0; + const dstData = dst.data; + const dstWidth = dst.width | 0; - for (let iy = 0; iy < h; iy++) { - for (let ix = 0; ix < w; ix++) { - const dst0 = ((ix + x) + (iy + y) * dstWidth) << 2; - blendPrecise(dstData, dst0, r, g, b, alpha); - } - } + for (let iy = 0; iy < h; iy++) { + for (let ix = 0; ix < w; ix++) { + const dst0 = ((ix + x) + (iy + y) * dstWidth) << 2; + blendPrecise(dstData, dst0, r, g, b, alpha); + } + } } function drawImageNormal( - src: ImageData | undefined, dst: ImageData | undefined, transform: Matrix2D, globalAlpha: number, - tint: number, sx: number, sy: number, sw: number, sh: number, - dx: number, dy: number, dw: number, dh: number + src: ImageData | undefined, dst: ImageData | undefined, transform: Matrix2D, globalAlpha: number, + tint: number, sx: number, sy: number, sw: number, sh: number, + dx: number, dy: number, dw: number, dh: number ) { - if (sw !== dw || sh !== dh) - throw new Error('Different dimentions not supported'); + if (sw !== dw || sh !== dh) + throw new Error('Different dimentions not supported'); - if (DEVELOPMENT && !isTranslation(transform)) { - console.error('Transform not supported'); - } + if (DEVELOPMENT && !isTranslation(transform)) { + console.error('Transform not supported'); + } - if (!src || !dst) - return; + if (!src || !dst) + return; - dx = Math.round(dx + transform[4]); - dy = Math.round(dy + transform[5]); + dx = Math.round(dx + transform[4]); + dy = Math.round(dy + transform[5]); - let w = sw; - let h = sh; + let w = sw; + let h = sh; - const xx = min(0, sx, dx); - w += xx; - dx -= xx; - sx -= xx; + const xx = min(0, sx, dx); + w += xx; + dx -= xx; + sx -= xx; - const yy = min(0, sy, dy); - h += yy; - dy -= yy; - sy -= yy; + const yy = min(0, sy, dy); + h += yy; + dy -= yy; + sy -= yy; - w += min(0, src.width - (sx + w), dst.width - (dx + w)); - h += min(0, src.height - (sy + h), dst.height - (dy + h)); + w += min(0, src.width - (sx + w), dst.width - (dx + w)); + h += min(0, src.height - (sy + h), dst.height - (dy + h)); - if (w <= 0 && h <= 0) - return; + if (w <= 0 && h <= 0) + return; - const { r, g, b, a } = colorToRGBA(tint); - const alpha = (globalAlpha * a) | 0; - const dstData = dst.data; - const srcData = src.data; - const dstWidth = dst.width | 0; - const srcWidth = src.width | 0; + const { r, g, b, a } = colorToRGBA(tint); + const alpha = (globalAlpha * a) | 0; + const dstData = dst.data; + const srcData = src.data; + const dstWidth = dst.width | 0; + const srcWidth = src.width | 0; - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const srcO = ((sx + x) + (sy + y) * srcWidth) << 2; - const sr = srcData[srcO]; - const sg = srcData[srcO + 1]; - const sb = srcData[srcO + 2]; - const sa = srcData[srcO + 3]; - const srcAlpha = blendColor(alpha, sa, 255); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const srcO = ((sx + x) + (sy + y) * srcWidth) << 2; + const sr = srcData[srcO]; + const sg = srcData[srcO + 1]; + const sb = srcData[srcO + 2]; + const sa = srcData[srcO + 3]; + const srcAlpha = blendColor(alpha, sa, 255); - if (srcAlpha !== 0) { - const rr = blendColor(r, sr, 255); - const gg = blendColor(g, sg, 255); - const bb = blendColor(b, sb, 255); - const dst0 = ((dx + x) + (dy + y) * dstWidth) << 2; - blendPrecise(dstData, dst0, rr, gg, bb, srcAlpha); - } - } - } + if (srcAlpha !== 0) { + const rr = blendColor(r, sr, 255); + const gg = blendColor(g, sg, 255); + const bb = blendColor(b, sb, 255); + const dst0 = ((dx + x) + (dy + y) * dstWidth) << 2; + blendPrecise(dstData, dst0, rr, gg, bb, srcAlpha); + } + } + } } function drawImagePalette( - src: ImageData | undefined, dst: ImageData | undefined, transform: Matrix2D, globalAlpha: number, - ignoreColorOption: number, disableShadingOption: boolean, - type: number, tint: number, palette: Palette | undefined, - sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + src: ImageData | undefined, dst: ImageData | undefined, transform: Matrix2D, globalAlpha: number, + ignoreColorOption: number, disableShadingOption: boolean, + type: number, tint: number, palette: Palette | undefined, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number ) { - if (sw !== dw || sh !== dh) - throw new Error('Different dimentions not supported'); + if (sw !== dw || sh !== dh) + throw new Error('Different dimentions not supported'); - if (DEVELOPMENT && !isTranslation(transform)) { - console.error('Transform not supported'); - } + if (DEVELOPMENT && !isTranslation(transform)) { + console.error('Transform not supported'); + } - if (palette === undefined) { - palette = commonPalettes.defaultPalette; - } + if (palette === undefined) { + palette = commonPalettes.defaultPalette; + } - if (!src || !dst) - return; + if (!src || !dst) + return; - dx = Math.round(dx + transform[4]); - dy = Math.round(dy + transform[5]); + dx = Math.round(dx + transform[4]); + dy = Math.round(dy + transform[5]); - let w = sw; - let h = sh; + let w = sw; + let h = sh; - const xx = min(0, sx, dx); - w += xx; - dx -= xx; - sx -= xx; + const xx = min(0, sx, dx); + w += xx; + dx -= xx; + sx -= xx; - const yy = min(0, sy, dy); - h += yy; - dy -= yy; - sy -= yy; + const yy = min(0, sy, dy); + h += yy; + dy -= yy; + sy -= yy; - w += min(0, src.width - (sx + w), dst.width - (dx + w)); - h += min(0, src.height - (sy + h), dst.height - (dy + h)); + w += min(0, src.width - (sx + w), dst.width - (dx + w)); + h += min(0, src.height - (sy + h), dst.height - (dy + h)); - if (w <= 0 && h <= 0) - return; + if (w <= 0 && h <= 0) + return; - const { r, g, b, a } = colorToRGBA(tint); - const alpha = (globalAlpha * a) | 0; - const colors = palette.colors; - const dstData = dst.data; - const srcData = src.data; - const dstWidth = dst.width | 0; - const srcWidth = src.width | 0; - const ignoreColor = ignoreColorOption >>> 0; - const disableShading = disableShadingOption || type > 2; - const offset = typeOffsets[type]; + const { r, g, b, a } = colorToRGBA(tint); + const alpha = (globalAlpha * a) | 0; + const colors = palette.colors; + const dstData = dst.data; + const srcData = src.data; + const dstWidth = dst.width | 0; + const srcWidth = src.width | 0; + const ignoreColor = ignoreColorOption >>> 0; + const disableShading = disableShadingOption || type > 2; + const offset = typeOffsets[type]; - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const srcO = ((sx + x) + (sy + y) * srcWidth) << 2; - const index = srcData[srcO + offset]; - const color = colors[index]; - const srcAlpha = ignoreColor === color ? 0 : blendColor(getAlpha(color), alpha, 255); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const srcO = ((sx + x) + (sy + y) * srcWidth) << 2; + const index = srcData[srcO + offset]; + const color = colors[index]; + const srcAlpha = ignoreColor === color ? 0 : blendColor(getAlpha(color), alpha, 255); - if (srcAlpha !== 0) { - const shade = disableShading ? 255 : srcData[srcO + 1]; - const rr = blendColor(getR(color), r, shade); - const gg = blendColor(getG(color), g, shade); - const bb = blendColor(getB(color), b, shade); - const dst0 = ((dx + x) + (dy + y) * dstWidth) << 2; - blendPrecise(dstData, dst0, rr, gg, bb, srcAlpha); - } - } - } + if (srcAlpha !== 0) { + const shade = disableShading ? 255 : srcData[srcO + 1]; + const rr = blendColor(getR(color), r, shade); + const gg = blendColor(getG(color), g, shade); + const bb = blendColor(getB(color), b, shade); + const dst0 = ((dx + x) + (dy + y) * dstWidth) << 2; + blendPrecise(dstData, dst0, rr, gg, bb, srcAlpha); + } + } + } } function blendColor(base: number, tint: number, shade: number) { - return (((((base * tint) | 0) * shade) | 0) / 65025) | 0; + return (((((base * tint) | 0) * shade) | 0) / 65025) | 0; } function blendPrecise(dstData: Uint8ClampedArray, dst0: number, r: number, g: number, b: number, alpha: number) { - if (alpha === 0xff || dstData[dst0 + 3] === 0) { - dstData[dst0] = r; - dstData[dst0 + 1] = g; - dstData[dst0 + 2] = b; - dstData[dst0 + 3] = alpha; - } else { - const dstAlpha = (0xff - alpha) | 0; - dstData[dst0] = ((((r * alpha) | 0) / 255) | 0) + ((((dstData[dst0] * dstAlpha) | 0) / 255) | 0); - dstData[dst0 + 1] = ((((g * alpha) | 0) / 255) | 0) + ((((dstData[dst0 + 1] * dstAlpha) | 0) / 255) | 0); - dstData[dst0 + 2] = ((((b * alpha) | 0) / 255) | 0) + ((((dstData[dst0 + 2] * dstAlpha) | 0) / 255) | 0); - const a = (alpha + ((((dstData[dst0 + 3] * dstAlpha) | 0) / 255) | 0)) | 0; - dstData[dst0 + 3] = a > 0xff ? 0xff : a; - } + if (alpha === 0xff || dstData[dst0 + 3] === 0) { + dstData[dst0] = r; + dstData[dst0 + 1] = g; + dstData[dst0 + 2] = b; + dstData[dst0 + 3] = alpha; + } else { + const dstAlpha = (0xff - alpha) | 0; + dstData[dst0] = ((((r * alpha) | 0) / 255) | 0) + ((((dstData[dst0] * dstAlpha) | 0) / 255) | 0); + dstData[dst0 + 1] = ((((g * alpha) | 0) / 255) | 0) + ((((dstData[dst0 + 1] * dstAlpha) | 0) / 255) | 0); + dstData[dst0 + 2] = ((((b * alpha) | 0) / 255) | 0) + ((((dstData[dst0 + 2] * dstAlpha) | 0) / 255) | 0); + const a = (alpha + ((((dstData[dst0 + 3] * dstAlpha) | 0) / 255) | 0)) | 0; + dstData[dst0 + 3] = a > 0xff ? 0xff : a; + } } // function blendFast(dstData: Uint8ClampedArray, dst0: number, r: number, g: number, b: number, alpha: number) { diff --git a/src/ts/graphics/graphicsUtils.ts b/src/ts/graphics/graphicsUtils.ts index cb7f014..3752e62 100644 --- a/src/ts/graphics/graphicsUtils.ts +++ b/src/ts/graphics/graphicsUtils.ts @@ -1,13 +1,13 @@ import { - Says, Rect, SpriteBatch, Sprite, SpriteBorder, MessageType, Entity, Point, Pony, SpriteBatchCommons, - PaletteSpriteBatch, Palette, isPartyMessage, PartyInfo, Camera, CommonPalettes, FontPalettes, PaletteManager, - isWhisperTo, isWhisper, isThinking, isPublicMessage + Says, Rect, SpriteBatch, Sprite, SpriteBorder, MessageType, Entity, Point, Pony, SpriteBatchCommons, + PaletteSpriteBatch, Palette, isPartyMessage, PartyInfo, Camera, CommonPalettes, FontPalettes, PaletteManager, + isWhisperTo, isWhisper, isThinking, isPublicMessage } from '../common/interfaces'; import { clamp, contains, intersect, toInt, hasFlag } from '../common/utils'; import { BLACK, WHITE, OUTLINE_COLOR, getMessageColor, PARTY_COLOR, MESSAGE_COLOR, FRIENDS_COLOR } from '../common/colors'; import { tileWidth, tileHeight, tileElevation, PONY_TYPE } from '../common/constants'; import { - HAlign, VAlign, TextOptions, lineBreak, drawTextAligned, measureText, drawText, drawOutlinedText + HAlign, VAlign, TextOptions, lineBreak, drawTextAligned, measureText, drawText, drawOutlinedText } from '../graphics/spriteFont'; import * as sprites from '../generated/sprites'; import { fontPal, fontSmallPal } from '../client/fonts'; @@ -20,15 +20,15 @@ import { mockPaletteManager } from '../common/ponyInfo'; import { sortEntities, isHidden, isFriend } from '../common/entityUtils'; const baloonTaper = [ - { w: 1, y: 2 }, - { w: 1, y: 1 }, + { w: 1, y: 2 }, + { w: 1, y: 1 }, ]; const roundTaper = [ - { w: 1, y: 5 }, - { w: 1, y: 3 }, - { w: 2, y: 2 }, - { w: 4, y: 1 }, + { w: 1, y: 5 }, + { w: 1, y: 3 }, + { w: 2, y: 2 }, + { w: 4, y: 1 }, ]; export const commonPalettes = createCommonPalettes(mockPaletteManager); @@ -36,551 +36,551 @@ export const commonPalettes = createCommonPalettes(mockPaletteManager); type AnyBatch = SpriteBatch | PaletteSpriteBatch; export function drawTaperedRect( - batch: AnyBatch, color: number, x: number, y: number, w: number, h: number, taper: { w: number; y: number; }[] + batch: AnyBatch, color: number, x: number, y: number, w: number, h: number, taper: { w: number; y: number; }[] ) { - x = Math.round(x) | 0; - y = Math.round(y) | 0; - w = Math.round(w) | 0; - h = Math.round(h) | 0; + x = Math.round(x) | 0; + y = Math.round(y) | 0; + w = Math.round(w) | 0; + h = Math.round(h) | 0; - let gap = 0; + let gap = 0; - for (let i = 0; i < taper.length; i++) { - const t = taper[i]; - const th = h - t.y * 2; - batch.drawRect(color, x + gap, y + t.y, t.w, th); - batch.drawRect(color, x + w - gap - t.w, y + t.y, t.w, th); - gap += t.w; - } + for (let i = 0; i < taper.length; i++) { + const t = taper[i]; + const th = h - t.y * 2; + batch.drawRect(color, x + gap, y + t.y, t.w, th); + batch.drawRect(color, x + w - gap - t.w, y + t.y, t.w, th); + gap += t.w; + } - batch.drawRect(color, x + gap, y, Math.max(0, w - gap * 2), h); + batch.drawRect(color, x + gap, y, Math.max(0, w - gap * 2), h); } export function drawRectBaloon(batch: AnyBatch, color: number, x: number, y: number, w: number, h: number) { - drawTaperedRect(batch, color, x, y, w, h, baloonTaper); + drawTaperedRect(batch, color, x, y, w, h, baloonTaper); } export function drawRoundBaloon(batch: AnyBatch, color: number, x: number, y: number, w: number, h: number) { - drawTaperedRect(batch, color, x, y, w, h, roundTaper); + drawTaperedRect(batch, color, x, y, w, h, roundTaper); } function getMessagePalette(type: MessageType, palettes: FontPalettes) { - if (type === MessageType.Supporter2) { - return palettes.supporter2; - } else if (type === MessageType.Supporter3) { - return palettes.supporter3; - } else { - return undefined; - } + if (type === MessageType.Supporter2) { + return palettes.supporter2; + } else if (type === MessageType.Supporter3) { + return palettes.supporter3; + } else { + return undefined; + } } export function drawBaloon( - batch: PaletteSpriteBatch, { message, type = MessageType.Chat, timer = 1, total = 10 }: Says, - x: number, y: number, bounds: Rect, palettes: CommonPalettes + batch: PaletteSpriteBatch, { message, type = MessageType.Chat, timer = 1, total = 10 }: Says, + x: number, y: number, bounds: Rect, palettes: CommonPalettes ) { - if (!fontPal) - return; + if (!fontPal) + return; - let { w, h } = measureText(message, fontPal); + let { w, h } = measureText(message, fontPal); - w = Math.max(w, 4); + w = Math.max(w, 4); - const screenPad = 8; - const availableWidth = bounds.w - screenPad * 2; + const screenPad = 8; + const availableWidth = bounds.w - screenPad * 2; - if (w > availableWidth) { - message = lineBreak(message, fontPal, availableWidth); - const size = measureText(message, fontPal); - w = size.w; - h = size.h; - } + if (w > availableWidth) { + message = lineBreak(message, fontPal, availableWidth); + const size = measureText(message, fontPal); + w = size.w; + h = size.h; + } - const { dy, alpha } = calcAnimation(timer, total); + const { dy, alpha } = calcAnimation(timer, total); - y += dy; + y += dy; - const nippleX = x; - const toTheLeft = Math.max(0, screenPad - x); - const toTheRight = Math.max(0, x - bounds.w + screenPad); + const nippleX = x; + const toTheLeft = Math.max(0, screenPad - x); + const toTheRight = Math.max(0, x - bounds.w + screenPad); - x = clamp(x, screenPad + w / 2 - toTheLeft, bounds.w - screenPad - w / 2 + toTheRight); + x = clamp(x, screenPad + w / 2 - toTheLeft, bounds.w - screenPad - w / 2 + toTheRight); - if (intersect(0, 0, bounds.w, bounds.h, x - w / 2, y - h / 2, w, h)) { - const palette = getMessagePalette(type, palettes.mainFont); - const color = palette ? WHITE : getMessageColor(type); - const options: TextOptions = { - palette: palette || palettes.mainFont.white, - emojiPalette: palettes.mainFont.emoji, - }; + if (intersect(0, 0, bounds.w, bounds.h, x - w / 2, y - h / 2, w, h)) { + const palette = getMessagePalette(type, palettes.mainFont); + const color = palette ? WHITE : getMessageColor(type); + const options: TextOptions = { + palette: palette || palettes.mainFont.white, + emojiPalette: palettes.mainFont.emoji, + }; - if (isThinking(type)) { - drawThinkingBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX); - } else if (isWhisper(type) || isWhisperTo(type)) { - drawWhisperBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX); - } else { - drawSpeechBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX); - } - } + if (isThinking(type)) { + drawThinkingBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX); + } else if (isWhisper(type) || isWhisperTo(type)) { + drawWhisperBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX); + } else { + drawSpeechBaloon(batch, message, color, options, x, y, w, h, alpha, nippleX); + } + } } export function drawSpeechBaloon( - batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number, - alpha: number, nippleX: number, + batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number, + alpha: number, nippleX: number, ) { - const pad = 4; - const xx = x - Math.round(w / 2); - const yy = y - h; - const nipple = sprites.nipple_2.color; + const pad = 4; + const xx = x - Math.round(w / 2); + const yy = y - h; + const nipple = sprites.nipple_2.color; - nippleX = clamp(nippleX, xx + pad, xx + w - pad); + nippleX = clamp(nippleX, xx + pad, xx + w - pad); - batch.globalAlpha = 0.6 * alpha; - drawRectBaloon(batch, BLACK, xx - pad, yy - pad, w + pad * 2, h + pad * 2); - batch.drawSprite(nipple, BLACK, undefined, nippleX - Math.round(nipple.w / 2), y + pad); + batch.globalAlpha = 0.6 * alpha; + drawRectBaloon(batch, BLACK, xx - pad, yy - pad, w + pad * 2, h + pad * 2); + batch.drawSprite(nipple, BLACK, undefined, nippleX - Math.round(nipple.w / 2), y + pad); - batch.globalAlpha = alpha; - drawText(batch, text, fontPal, color, xx, yy, options); - batch.globalAlpha = 1; + batch.globalAlpha = alpha; + drawText(batch, text, fontPal, color, xx, yy, options); + batch.globalAlpha = 1; } export function drawWhisperBaloon( - batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number, - alpha: number, nippleX: number, + batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number, + alpha: number, nippleX: number, ) { - const pad = 4; - const xx = x - Math.round(w / 2); - const yy = y - h; - const nipple = sprites.nipple_alt_2.color; + const pad = 4; + const xx = x - Math.round(w / 2); + const yy = y - h; + const nipple = sprites.nipple_alt_2.color; - nippleX = clamp(nippleX, xx + pad, xx + w - pad); + nippleX = clamp(nippleX, xx + pad, xx + w - pad); - batch.globalAlpha = 0.6 * alpha; + batch.globalAlpha = 0.6 * alpha; - const left = xx - pad; - const top = yy - pad; - const width = w + pad * 2; - const height = h + pad * 2; + const left = xx - pad; + const top = yy - pad; + const width = w + pad * 2; + const height = h + pad * 2; - batch.drawRect(BLACK, left + 2, top, width - 4, 1); - batch.drawRect(BLACK, left + 1, top + 1, width - 2, 1); - batch.drawRect(BLACK, left, top + 2, width, height - 4); - batch.drawRect(BLACK, left + 1, top + height - 2, width - 2, 1); - batch.drawRect(BLACK, left + 2, top + height - 1, width - 4, 1); + batch.drawRect(BLACK, left + 2, top, width - 4, 1); + batch.drawRect(BLACK, left + 1, top + 1, width - 2, 1); + batch.drawRect(BLACK, left, top + 2, width, height - 4); + batch.drawRect(BLACK, left + 1, top + height - 2, width - 2, 1); + batch.drawRect(BLACK, left + 2, top + height - 1, width - 4, 1); - const yyy = top + height + 1; - const right = left + width - 1; + const yyy = top + height + 1; + const right = left + width - 1; - for (let tx = nippleX - 7; (tx + 5) > (left + 1); tx -= 5) { - const shorten = Math.max(0, (left + 1) - tx); - batch.drawRect(BLACK, tx + shorten, yyy, 4 - shorten, 1); - } + for (let tx = nippleX - 7; (tx + 5) > (left + 1); tx -= 5) { + const shorten = Math.max(0, (left + 1) - tx); + batch.drawRect(BLACK, tx + shorten, yyy, 4 - shorten, 1); + } - for (let tx = nippleX + 2; tx < right; tx += 5) { - const shorten = Math.max(0, (tx + 5) - right); - batch.drawRect(BLACK, tx, yyy, Math.min(4, 5 - shorten), 1); - } + for (let tx = nippleX + 2; tx < right; tx += 5) { + const shorten = Math.max(0, (tx + 5) - right); + batch.drawRect(BLACK, tx, yyy, Math.min(4, 5 - shorten), 1); + } - batch.drawSprite(nipple, BLACK, undefined, nippleX - 3, y + pad + 2); + batch.drawSprite(nipple, BLACK, undefined, nippleX - 3, y + pad + 2); - batch.globalAlpha = alpha; - drawText(batch, text, fontPal, color, xx, yy, options); - batch.globalAlpha = 1; + batch.globalAlpha = alpha; + drawText(batch, text, fontPal, color, xx, yy, options); + batch.globalAlpha = 1; } export function drawThinkingBaloon( - batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number, - alpha: number, nippleX: number, + batch: PaletteSpriteBatch, text: string, color: number, options: TextOptions, x: number, y: number, w: number, h: number, + alpha: number, nippleX: number, ) { - const padX = 6; - const padY = 4; - const xx = x - Math.round(w / 2); - const yy = y - h; - const ox = clamp(nippleX, xx, xx + w) - 1; - const oy = y + 12; + const padX = 6; + const padY = 4; + const xx = x - Math.round(w / 2); + const yy = y - h; + const ox = clamp(nippleX, xx, xx + w) - 1; + const oy = y + 12; - batch.globalAlpha = 0.6 * alpha; - drawRoundBaloon(batch, BLACK, xx - padX, yy - padY, w + padX * 2, h + padY * 2); - batch.drawRect(BLACK, ox, oy, 1, 1); - batch.drawRect(BLACK, ox - 1, oy - 3, 2, 2); - batch.drawRect(BLACK, ox, oy - 7, 3, 3); + batch.globalAlpha = 0.6 * alpha; + drawRoundBaloon(batch, BLACK, xx - padX, yy - padY, w + padX * 2, h + padY * 2); + batch.drawRect(BLACK, ox, oy, 1, 1); + batch.drawRect(BLACK, ox - 1, oy - 3, 2, 2); + batch.drawRect(BLACK, ox, oy - 7, 3, 3); - batch.globalAlpha = alpha; - drawText(batch, text, fontPal, color, xx, yy, options); - batch.globalAlpha = 1; + batch.globalAlpha = alpha; + drawText(batch, text, fontPal, color, xx, yy, options); + batch.globalAlpha = 1; } export enum DrawNameFlags { - None = 0, - Party = 1, - Friend = 2, + None = 0, + Party = 1, + Friend = 2, } function getNameColor(flags: DrawNameFlags) { - if (hasFlag(flags, DrawNameFlags.Party)) { - return PARTY_COLOR; - } else if (hasFlag(flags, DrawNameFlags.Friend)) { - return FRIENDS_COLOR; - } else { - return WHITE; - } + if (hasFlag(flags, DrawNameFlags.Party)) { + return PARTY_COLOR; + } else if (hasFlag(flags, DrawNameFlags.Friend)) { + return FRIENDS_COLOR; + } else { + return WHITE; + } } export function drawNamePlate( - batch: PaletteSpriteBatch, text: string, x: number, y: number, flags: DrawNameFlags, - palettes: CommonPalettes, tagId?: string, + batch: PaletteSpriteBatch, text: string, x: number, y: number, flags: DrawNameFlags, + palettes: CommonPalettes, tagId?: string, ) { - const tag = getTag(tagId); - const size = measureText(text, fontPal); - const xx = x - Math.round(size.w / 2); - const yy = y - size.h + 6 - (tag ? 3 : 0); - const color = getNameColor(flags); - const options = { palette: palettes.mainFont.white, emojiPalette: palettes.mainFont.emoji }; - drawOutlinedText(batch, text, fontPal, color, OUTLINE_COLOR, xx, yy, options); + const tag = getTag(tagId); + const size = measureText(text, fontPal); + const xx = x - Math.round(size.w / 2); + const yy = y - size.h + 6 - (tag ? 3 : 0); + const color = getNameColor(flags); + const options = { palette: palettes.mainFont.white, emojiPalette: palettes.mainFont.emoji }; + drawOutlinedText(batch, text, fontPal, color, OUTLINE_COLOR, xx, yy, options); - if (tag) { - const tagSize = measureText(tag.label, fontSmallPal); - const textX = x - Math.round(tagSize.w / 2); - const palette = getTagPalette(tag, palettes.smallFont); - drawOutlinedText(batch, tag.label, fontSmallPal, tag.color, OUTLINE_COLOR, textX, yy + 11, { palette }); - } + if (tag) { + const tagSize = measureText(tag.label, fontSmallPal); + const textX = x - Math.round(tagSize.w / 2); + const palette = getTagPalette(tag, palettes.smallFont); + drawOutlinedText(batch, tag.label, fontSmallPal, tag.color, OUTLINE_COLOR, textX, yy + 11, { palette }); + } } export function drawBounds(batch: PaletteSpriteBatch, e: Entity, r: Rect | undefined, color: number) { - if (r) { - batch.drawRect( - color, - Math.round(e.x * tileWidth + r.x), Math.round(e.y * tileHeight + r.y), - Math.round(r.w), Math.round(r.h)); - } + if (r) { + batch.drawRect( + color, + Math.round(e.x * tileWidth + r.x), Math.round(e.y * tileHeight + r.y), + Math.round(r.w), Math.round(r.h)); + } } export function drawWorldBounds(batch: PaletteSpriteBatch, e: Entity, r: Rect | undefined, color: number) { - if (r) { - batch.drawRect( - color, - Math.round((e.x + r.x) * tileWidth), Math.round((e.y + r.y) * tileHeight), - Math.round(r.w * tileWidth), Math.round(r.h * tileHeight)); - } + if (r) { + batch.drawRect( + color, + Math.round((e.x + r.x) * tileWidth), Math.round((e.y + r.y) * tileHeight), + Math.round(r.w * tileWidth), Math.round(r.h * tileHeight)); + } } export function drawBoundsOutline(batch: PaletteSpriteBatch, e: Entity, r: Rect | undefined, color: number, thickness = 1) { - if (r) { - drawOutline( - batch, color, - Math.round(e.x * tileWidth + r.x), Math.round(e.y * tileHeight + r.y), - Math.round(r.w), Math.round(r.h), - thickness); - } + if (r) { + drawOutline( + batch, color, + Math.round(e.x * tileWidth + r.x), Math.round(e.y * tileHeight + r.y), + Math.round(r.w), Math.round(r.h), + thickness); + } } export function drawOutlineRect(batch: SpriteBatchCommons, color: number, { x, y, w, h }: Rect, thickness = 1) { - drawOutline(batch, color, x, y, w, h, thickness); + drawOutline(batch, color, x, y, w, h, thickness); } export function drawOutline(batch: SpriteBatchCommons, color: number, x: number, y: number, w: number, h: number, thickness = 1) { - batch.drawRect(color, x - thickness, y - thickness, w + thickness * 2, thickness); // top - batch.drawRect(color, x - thickness, y + h, w + thickness * 2, thickness); // bottom - batch.drawRect(color, x - thickness, y, thickness, h); // left - batch.drawRect(color, x + w, y, thickness, h); // right + batch.drawRect(color, x - thickness, y - thickness, w + thickness * 2, thickness); // top + batch.drawRect(color, x - thickness, y + h, w + thickness * 2, thickness); // bottom + batch.drawRect(color, x - thickness, y, thickness, h); // left + batch.drawRect(color, x + w, y, thickness, h); // right } export type DrawRect = (color: number, x: number, y: number, w: number, h: number) => void; export function drawCharacter(drawRect: DrawRect, x: number, y: number, color: number, char: string) { - switch (char) { - case '0': - drawRect(color, x, y, 1, 5); - drawRect(color, x + 1, y, 1, 1); - drawRect(color, x + 1, y + 4, 1, 1); - drawRect(color, x + 2, y, 1, 5); - return 3; - case '1': - drawRect(color, x, y + 1, 1, 1); - drawRect(color, x + 1, y, 1, 4); - drawRect(color, x, y + 4, 3, 1); - return 3; - case '2': - drawRect(color, x, y, 3, 1); - drawRect(color, x, y + 2, 3, 1); - drawRect(color, x, y + 4, 3, 1); - drawRect(color, x + 2, y + 1, 1, 1); - drawRect(color, x, y + 3, 1, 1); - return 3; - case '3': - drawRect(color, x, y, 2, 1); - drawRect(color, x, y + 2, 2, 1); - drawRect(color, x, y + 4, 2, 1); - drawRect(color, x + 2, y, 1, 5); - return 3; - case '4': - drawRect(color, x, y, 1, 3); - drawRect(color, x, y + 2, 3, 1); - drawRect(color, x + 2, y, 1, 5); - return 3; - case '5': - drawRect(color, x, y, 3, 1); - drawRect(color, x, y + 2, 3, 1); - drawRect(color, x, y + 4, 3, 1); - drawRect(color, x, y + 1, 1, 1); - drawRect(color, x + 2, y + 3, 1, 1); - return 3; - case '6': - drawRect(color, x, y, 3, 1); - drawRect(color, x, y + 2, 3, 1); - drawRect(color, x, y + 4, 3, 1); - drawRect(color, x, y, 1, 5); - drawRect(color, x + 2, y + 3, 1, 1); - return 3; - case '7': - drawRect(color, x, y, 3, 1); - drawRect(color, x + 2, y + 1, 1, 4); - return 3; - case '8': - drawRect(color, x, y, 1, 5); - drawRect(color, x + 1, y, 1, 1); - drawRect(color, x + 1, y + 2, 1, 1); - drawRect(color, x + 1, y + 4, 1, 1); - drawRect(color, x + 2, y, 1, 5); - return 3; - case '9': - drawRect(color, x, y, 1, 3); - drawRect(color, x + 1, y, 1, 1); - drawRect(color, x + 1, y + 2, 1, 1); - drawRect(color, x, y + 4, 2, 1); - drawRect(color, x + 2, y, 1, 5); - return 3; - case ':': - drawRect(color, x, y + 1, 1, 1); - drawRect(color, x, y + 3, 1, 1); - return 1; - case '.': - drawRect(color, x, y + 4, 1, 1); - return 1; - case '-': - drawRect(color, x, y + 2, 3, 1); - return 3; - case ' ': - return 2; - default: - drawRect(color, x, y, 3, 5); - return 3; - } + switch (char) { + case '0': + drawRect(color, x, y, 1, 5); + drawRect(color, x + 1, y, 1, 1); + drawRect(color, x + 1, y + 4, 1, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case '1': + drawRect(color, x, y + 1, 1, 1); + drawRect(color, x + 1, y, 1, 4); + drawRect(color, x, y + 4, 3, 1); + return 3; + case '2': + drawRect(color, x, y, 3, 1); + drawRect(color, x, y + 2, 3, 1); + drawRect(color, x, y + 4, 3, 1); + drawRect(color, x + 2, y + 1, 1, 1); + drawRect(color, x, y + 3, 1, 1); + return 3; + case '3': + drawRect(color, x, y, 2, 1); + drawRect(color, x, y + 2, 2, 1); + drawRect(color, x, y + 4, 2, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case '4': + drawRect(color, x, y, 1, 3); + drawRect(color, x, y + 2, 3, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case '5': + drawRect(color, x, y, 3, 1); + drawRect(color, x, y + 2, 3, 1); + drawRect(color, x, y + 4, 3, 1); + drawRect(color, x, y + 1, 1, 1); + drawRect(color, x + 2, y + 3, 1, 1); + return 3; + case '6': + drawRect(color, x, y, 3, 1); + drawRect(color, x, y + 2, 3, 1); + drawRect(color, x, y + 4, 3, 1); + drawRect(color, x, y, 1, 5); + drawRect(color, x + 2, y + 3, 1, 1); + return 3; + case '7': + drawRect(color, x, y, 3, 1); + drawRect(color, x + 2, y + 1, 1, 4); + return 3; + case '8': + drawRect(color, x, y, 1, 5); + drawRect(color, x + 1, y, 1, 1); + drawRect(color, x + 1, y + 2, 1, 1); + drawRect(color, x + 1, y + 4, 1, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case '9': + drawRect(color, x, y, 1, 3); + drawRect(color, x + 1, y, 1, 1); + drawRect(color, x + 1, y + 2, 1, 1); + drawRect(color, x, y + 4, 2, 1); + drawRect(color, x + 2, y, 1, 5); + return 3; + case ':': + drawRect(color, x, y + 1, 1, 1); + drawRect(color, x, y + 3, 1, 1); + return 1; + case '.': + drawRect(color, x, y + 4, 1, 1); + return 1; + case '-': + drawRect(color, x, y + 2, 3, 1); + return 3; + case ' ': + return 2; + default: + drawRect(color, x, y, 3, 5); + return 3; + } } export function drawPixelTextBase(drawRect: DrawRect, x: number, y: number, color: number, text: string) { - for (let i = 0; i < text.length; i++) { - x += drawCharacter(drawRect, x, y, color, text.charAt(i)) + 1; - } + for (let i = 0; i < text.length; i++) { + x += drawCharacter(drawRect, x, y, color, text.charAt(i)) + 1; + } } export function drawPixelText(batch: PaletteSpriteBatch, x: number, y: number, color: number, text: string) { - drawPixelTextBase(batch.drawRect.bind(batch), x, y, color, text); + drawPixelTextBase(batch.drawRect.bind(batch), x, y, color, text); } export function fillRect(context: CanvasRenderingContext2D, color: string, x: number, y: number, w: number, h: number) { - context.fillStyle = color; - context.fillRect(x, y, w, h); + context.fillStyle = color; + context.fillRect(x, y, w, h); } export function drawPixelTextOnCanvas(context: CanvasRenderingContext2D, x: number, y: number, color: number, text: string) { - drawPixelTextBase((color, x, y, w, h) => fillRect(context, colorToCSS(color), x, y, w, h), x, y, color, text); + drawPixelTextBase((color, x, y, w, h) => fillRect(context, colorToCSS(color), x, y, w, h), x, y, color, text); } interface Say { - message: Says; - x: number; - y: number; + message: Says; + x: number; + y: number; } export function compareSays(a: Say, b: Say) { - return a.message.created - b.message.created; + return a.message.created - b.message.created; } function isPartyMember(entity: Entity, party: PartyInfo | undefined) { - return entity.type === PONY_TYPE && party !== undefined && party.members.some(p => p.id === entity.id && !p.pending); + return entity.type === PONY_TYPE && party !== undefined && party.members.some(p => p.id === entity.id && !p.pending); } export function drawNames( - batch: PaletteSpriteBatch, entities: Entity[], player: Pony | undefined, party: PartyInfo | undefined, - camera: Camera, hover: Point, drawHidden: boolean, palettes: CommonPalettes + batch: PaletteSpriteBatch, entities: Entity[], player: Pony | undefined, party: PartyInfo | undefined, + camera: Camera, hover: Point, drawHidden: boolean, palettes: CommonPalettes ) { - sortEntities(entities); + sortEntities(entities); - for (const e of entities) { - if ((!isHidden(e) || drawHidden) && e.name && e !== player) { - const nameOffsetBase = 12; - const bounds = e.interactBounds || e.bounds; - const chatBounds = e.chatBounds || bounds; + for (const e of entities) { + if ((!isHidden(e) || drawHidden) && e.name && e !== player) { + const nameOffsetBase = 12; + const bounds = e.interactBounds || e.bounds; + const chatBounds = e.chatBounds || bounds; - if (chatBounds !== undefined && bounds !== undefined && contains(e.x, e.y, bounds, hover)) { - const { x, y } = worldToScreen(camera, e); - const nameOffset = nameOffsetBase - getChatHeight(e); - const tag = (isHidden(e) && drawHidden) ? 'hidden' : e.tag; - const flags = DrawNameFlags.None | - (isPartyMember(e, party) ? DrawNameFlags.Party : 0) | - (isFriend(e) ? DrawNameFlags.Friend : 0); - drawNamePlate(batch, e.name, x, y + chatBounds.y - nameOffset, flags, palettes, tag); - } - } - } + if (chatBounds !== undefined && bounds !== undefined && contains(e.x, e.y, bounds, hover)) { + const { x, y } = worldToScreen(camera, e); + const nameOffset = nameOffsetBase - getChatHeight(e); + const tag = (isHidden(e) && drawHidden) ? 'hidden' : e.tag; + const flags = DrawNameFlags.None | + (isPartyMember(e, party) ? DrawNameFlags.Party : 0) | + (isFriend(e) ? DrawNameFlags.Friend : 0); + drawNamePlate(batch, e.name, x, y + chatBounds.y - nameOffset, flags, palettes, tag); + } + } + } } export function getChatBallonXY(e: Entity, camera: Camera): Point { - const nameOffsetBase = 12; - const bounds = e.interactBounds || e.bounds; - const chatBounds = e.chatBounds || bounds; - const screen = worldToScreen(camera, e); - const nameOffset = nameOffsetBase - getChatHeight(e); - const offset = (nameOffset + 6) + (e.tag ? 5 : 0); - const yy = screen.y + (chatBounds ? chatBounds.y : 0) - offset; - const x = screen.x + toInt(e.chatX); - const y = yy + toInt(e.chatY); - return { x, y }; + const nameOffsetBase = 12; + const bounds = e.interactBounds || e.bounds; + const chatBounds = e.chatBounds || bounds; + const screen = worldToScreen(camera, e); + const nameOffset = nameOffsetBase - getChatHeight(e); + const offset = (nameOffset + 6) + (e.tag ? 5 : 0); + const yy = screen.y + (chatBounds ? chatBounds.y : 0) - offset; + const x = screen.x + toInt(e.chatX); + const y = yy + toInt(e.chatY); + return { x, y }; } function drawChatBaloon(batch: PaletteSpriteBatch, entity: Entity, camera: Camera, palettes: CommonPalettes) { - const { x, y } = getChatBallonXY(entity, camera); - drawBaloon(batch, entity.says!, x, y, camera, palettes); + const { x, y } = getChatBallonXY(entity, camera); + drawBaloon(batch, entity.says!, x, y, camera, palettes); } export function drawChat( - batch: PaletteSpriteBatch, entities: Entity[], camera: Camera, drawHidden: boolean, palettes: CommonPalettes, - hidePublic: boolean + batch: PaletteSpriteBatch, entities: Entity[], camera: Camera, drawHidden: boolean, palettes: CommonPalettes, + hidePublic: boolean ) { - sortEntities(entities); + sortEntities(entities); - for (const entity of entities) { - if ((!isHidden(entity) || drawHidden) && !isPartyMessage(entity.says!.type || MessageType.Chat)) { - if (!hidePublic || !isPublicMessage(entity.says!.type || MessageType.Chat)) { - drawChatBaloon(batch, entity, camera, palettes); - } - } - } + for (const entity of entities) { + if ((!isHidden(entity) || drawHidden) && !isPartyMessage(entity.says!.type || MessageType.Chat)) { + if (!hidePublic || !isPublicMessage(entity.says!.type || MessageType.Chat)) { + drawChatBaloon(batch, entity, camera, palettes); + } + } + } - for (const entity of entities) { - if ((!isHidden(entity) || drawHidden) && isPartyMessage(entity.says!.type || MessageType.Chat)) { - drawChatBaloon(batch, entity, camera, palettes); - } - } + for (const entity of entities) { + if ((!isHidden(entity) || drawHidden) && isPartyMessage(entity.says!.type || MessageType.Chat)) { + drawChatBaloon(batch, entity, camera, palettes); + } + } } function getChatHeight(entity: Entity): number { - return isPony(entity) ? getPonyChatHeight(entity) : 0; + return isPony(entity) ? getPonyChatHeight(entity) : 0; } export const chatAnimationDuration = 0.2; export function dismissSays(says: Says) { - if (says.timer !== undefined) { - says.timer = Math.min(says.timer, chatAnimationDuration); - } + if (says.timer !== undefined) { + says.timer = Math.min(says.timer, chatAnimationDuration); + } } function calcAnimation(timer: number, total: number) { - const start = (total - timer) / chatAnimationDuration; - const end = timer / chatAnimationDuration; + const start = (total - timer) / chatAnimationDuration; + const end = timer / chatAnimationDuration; - const dys = [3, 2, 1, 0, -1, 0]; - const dys2 = [-4, -3, -2, -1]; - const dyd = start * dys.length; - const dyd2 = end * dys2.length; - const dyi = clamp(Math.round(dyd), 0, dys.length - 1); - const dyi2 = clamp(Math.round(dyd2), 0, dys2.length); - const dy = dyi2 < dys2.length ? dys2[dyi2] : dys[dyi]; - const alpha = Math.min(start, end, 1); + const dys = [3, 2, 1, 0, -1, 0]; + const dys2 = [-4, -3, -2, -1]; + const dyd = start * dys.length; + const dyd2 = end * dys2.length; + const dyi = clamp(Math.round(dyd), 0, dys.length - 1); + const dyi2 = clamp(Math.round(dyd2), 0, dys2.length); + const dy = dyi2 < dys2.length ? dys2[dyi2] : dys[dyi]; + const alpha = Math.min(start, end, 1); - return { alpha, dy }; + return { alpha, dy }; } export function drawBox( - batch: SpriteBatchCommons, color: number, shadowColor: number, x: number, y: number, z: number, - w: number, l: number, h: number + batch: SpriteBatchCommons, color: number, shadowColor: number, x: number, y: number, z: number, + w: number, l: number, h: number ) { - const darker = multiplyColor(color, 0.8); - const left = (x - w / 2) * tileWidth; - const bottom = y * tileHeight; - const elevation = z * tileElevation; - const width = w * tileWidth; - const frontHeight = h * tileElevation; - const topHeight = l * tileHeight; + const darker = multiplyColor(color, 0.8); + const left = (x - w / 2) * tileWidth; + const bottom = y * tileHeight; + const elevation = z * tileElevation; + const width = w * tileWidth; + const frontHeight = h * tileElevation; + const topHeight = l * tileHeight; - batch.drawRect(shadowColor, left, bottom - topHeight, width, topHeight); // shadow - batch.drawRect(darker, left, bottom - elevation - frontHeight, width, frontHeight); // front - batch.drawRect(color, left, bottom - elevation - frontHeight - topHeight, width, topHeight); // top + batch.drawRect(shadowColor, left, bottom - topHeight, width, topHeight); // shadow + batch.drawRect(darker, left, bottom - elevation - frontHeight, width, frontHeight); // front + batch.drawRect(color, left, bottom - elevation - frontHeight - topHeight, width, topHeight); // top } export function drawSpriteBorder( - batch: SpriteBatch, border: SpriteBorder, color: number, x: number, y: number, w: number, h: number + batch: SpriteBatch, border: SpriteBorder, color: number, x: number, y: number, w: number, h: number ) { - x = Math.round(x) | 0; - y = Math.round(y) | 0; - w = Math.round(w) | 0; - h = Math.round(h) | 0; + x = Math.round(x) | 0; + y = Math.round(y) | 0; + w = Math.round(w) | 0; + h = Math.round(h) | 0; - const size = border.border; - const right = x + w - size; - const bottom = y + h - size; - const bgWidth = w - size * 2; - const bgHeight = h - size * 2; + const size = border.border; + const right = x + w - size; + const bottom = y + h - size; + const bgWidth = w - size * 2; + const bgHeight = h - size * 2; - batch.drawSprite(border.topLeft, color, x, y); - batch.drawSprite(border.topRight, color, right, y); - batch.drawSprite(border.bottomLeft, color, x, bottom); - batch.drawSprite(border.bottomRight, color, right, bottom); + batch.drawSprite(border.topLeft, color, x, y); + batch.drawSprite(border.topRight, color, right, y); + batch.drawSprite(border.bottomLeft, color, x, bottom); + batch.drawSprite(border.bottomRight, color, right, bottom); - drawStretched(batch, border.top, color, x + size, y, bgWidth, size); - drawStretched(batch, border.left, color, x, y + size, size, bgHeight); - //drawStretched(batch, border.bg, color, x + size, y + size, bgWidth, bgHeight); - batch.drawRect(color, x + size, y + size, bgWidth, bgHeight); - drawStretched(batch, border.right, color, x + w - size, y + size, size, bgHeight); - drawStretched(batch, border.bottom, color, x + size, y + h - size, bgWidth, size); + drawStretched(batch, border.top, color, x + size, y, bgWidth, size); + drawStretched(batch, border.left, color, x, y + size, size, bgHeight); + //drawStretched(batch, border.bg, color, x + size, y + size, bgWidth, bgHeight); + batch.drawRect(color, x + size, y + size, bgWidth, bgHeight); + drawStretched(batch, border.right, color, x + w - size, y + size, size, bgHeight); + drawStretched(batch, border.bottom, color, x + size, y + h - size, bgWidth, size); } function drawStretched(batch: SpriteBatch, sprite: Sprite, color: number, x: number, y: number, w: number, h: number) { - if (sprite.h && sprite.w) { - const sw = Math.min(sprite.w, w); - const sh = Math.min(sprite.h, h); - batch.drawImage(color, sprite.x, sprite.y, sw, sh, x, y, w, h); - } + if (sprite.h && sprite.w) { + const sw = Math.min(sprite.w, w); + const sh = Math.min(sprite.h, h); + batch.drawImage(color, sprite.x, sprite.y, sw, sh, x, y, w, h); + } } export function drawSpriteCropped( - batch: PaletteSpriteBatch, s: Sprite, color: number, palette: Palette | undefined, x: number, y: number, - maxY: number + batch: PaletteSpriteBatch, s: Sprite, color: number, palette: Palette | undefined, x: number, y: number, + maxY: number ) { - const top = y + s.oy; - const bottom = Math.min(top + s.h, maxY); + const top = y + s.oy; + const bottom = Math.min(top + s.h, maxY); - if (bottom > top) { - batch.drawImage(s.type, color, palette, s.x, s.y, s.w, s.h, x + s.ox, top, s.w, bottom - top); - } + if (bottom > top) { + batch.drawImage(s.type, color, palette, s.x, s.y, s.w, s.h, x + s.ox, top, s.w, bottom - top); + } } export function drawFullScreenMessage(batch: PaletteSpriteBatch, camera: Camera, text: string, palette: Palette) { - const messageHeight = 100; - const messageY = Math.round((camera.h - messageHeight) / 2); - batch.drawRect(MESSAGE_COLOR, 0, messageY, camera.w, messageHeight); - const textRect = rect(0, messageY, camera.w, messageHeight); - drawTextAligned(batch, text, fontPal, WHITE, textRect, HAlign.Center, VAlign.Middle, { palette }); + const messageHeight = 100; + const messageY = Math.round((camera.h - messageHeight) / 2); + batch.drawRect(MESSAGE_COLOR, 0, messageY, camera.w, messageHeight); + const textRect = rect(0, messageY, camera.w, messageHeight); + drawTextAligned(batch, text, fontPal, WHITE, textRect, HAlign.Center, VAlign.Middle, { palette }); } export function createCommonPalettes(paletteManager: PaletteManager): CommonPalettes { - return { - defaultPalette: paletteManager.addArray(sprites.defaultPalette), - mainFont: { - emoji: paletteManager.addArray(sprites.emojiPalette), - white: paletteManager.addArray(sprites.fontPalette), - supporter1: paletteManager.addArray(sprites.fontSupporter1Palette), - supporter2: paletteManager.addArray(sprites.fontSupporter2Palette), - supporter3: paletteManager.addArray(sprites.fontSupporter3Palette), - }, - smallFont: { - emoji: paletteManager.addArray(sprites.emojiPalette), - white: paletteManager.addArray(sprites.fontSmallPalette), - supporter1: paletteManager.addArray(sprites.fontSmallSupporter1Palette), - supporter2: paletteManager.addArray(sprites.fontSmallSupporter2Palette), - supporter3: paletteManager.addArray(sprites.fontSmallSupporter3Palette), - }, - }; + return { + defaultPalette: paletteManager.addArray(sprites.defaultPalette), + mainFont: { + emoji: paletteManager.addArray(sprites.emojiPalette), + white: paletteManager.addArray(sprites.fontPalette), + supporter1: paletteManager.addArray(sprites.fontSupporter1Palette), + supporter2: paletteManager.addArray(sprites.fontSupporter2Palette), + supporter3: paletteManager.addArray(sprites.fontSupporter3Palette), + }, + smallFont: { + emoji: paletteManager.addArray(sprites.emojiPalette), + white: paletteManager.addArray(sprites.fontSmallPalette), + supporter1: paletteManager.addArray(sprites.fontSmallSupporter1Palette), + supporter2: paletteManager.addArray(sprites.fontSmallSupporter2Palette), + supporter3: paletteManager.addArray(sprites.fontSmallSupporter3Palette), + }, + }; } diff --git a/src/ts/graphics/paletteManager.ts b/src/ts/graphics/paletteManager.ts index b9e1e4c..ba3e0da 100644 --- a/src/ts/graphics/paletteManager.ts +++ b/src/ts/graphics/paletteManager.ts @@ -6,242 +6,242 @@ const INITIAL_SIZE = 512; const MAX_SIZE = 2048; export function createPalette(colors: Uint32Array): Palette { - return { x: 0, y: 0, u: 0, v: 0, refs: 1, colors }; + return { x: 0, y: 0, u: 0, v: 0, refs: 1, colors }; } export function releasePalette(palette: Palette | undefined) { - if (palette && palette.refs) { - palette.refs--; - } + if (palette && palette.refs) { + palette.refs--; + } } export function colorsEqual(a: Uint32Array, b: Uint32Array): boolean { - if (a === b) { - return true; - } + if (a === b) { + return true; + } - if (a.length !== b.length) { - return false; - } + if (a.length !== b.length) { + return false; + } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; + } + } - return true; + return true; } function isInUse(palette: Palette) { - return palette.refs > 0; + return palette.refs > 0; } export class PaletteManager implements IPaletteManager { - private paletteTexture?: Texture2D; - private palettes = times(512, () => []); - private dirty: Palette[] = []; - private dirtyMinY = 0; - private dirtyMaxY = -1; - private lastX = 0; - private lastY = 0; - private initialized = true; - deduplicate = true; - constructor(private size = INITIAL_SIZE) { - } - get texture() { - return this.paletteTexture; - } - get textureSize() { - return this.size; - } - get pixelSize() { - return 256 / this.size; - } - activePalettes() { - return this.palettes.reduce((sum, p) => sum + p.reduce((sum, p) => sum + (p.refs > 0 ? 1 : 0), 0), 0); - } - add(colorValues: number[]): Palette { - const colors = new Uint32Array(colorValues.length); + private paletteTexture?: Texture2D; + private palettes = times(512, () => []); + private dirty: Palette[] = []; + private dirtyMinY = 0; + private dirtyMaxY = -1; + private lastX = 0; + private lastY = 0; + private initialized = true; + deduplicate = true; + constructor(private size = INITIAL_SIZE) { + } + get texture() { + return this.paletteTexture; + } + get textureSize() { + return this.size; + } + get pixelSize() { + return 256 / this.size; + } + activePalettes() { + return this.palettes.reduce((sum, p) => sum + p.reduce((sum, p) => sum + (p.refs > 0 ? 1 : 0), 0), 0); + } + add(colorValues: number[]): Palette { + const colors = new Uint32Array(colorValues.length); - for (let i = 0; i < colorValues.length; i++) { - colors[i] = colorValues[i] >>> 0; - } + for (let i = 0; i < colorValues.length; i++) { + colors[i] = colorValues[i] >>> 0; + } - return this.addArray(colors); - } - addArray(colors: Uint32Array): Palette { - const hash = computeCRC(colors) & 0x1ff; - const palettes = this.palettes[hash]; + return this.addArray(colors); + } + addArray(colors: Uint32Array): Palette { + const hash = computeCRC(colors) & 0x1ff; + const palettes = this.palettes[hash]; - if (this.deduplicate) { - for (let i = 0; i < palettes.length; i++) { - const existing = palettes[i]; + if (this.deduplicate) { + for (let i = 0; i < palettes.length; i++) { + const existing = palettes[i]; - if (colorsEqual(existing.colors, colors)) { - existing.refs = (existing.refs + 1) | 0; - return existing; - } - } - } + if (colorsEqual(existing.colors, colors)) { + existing.refs = (existing.refs + 1) | 0; + return existing; + } + } + } - const palette = createPalette(colors); - palettes.push(palette); - this.dirty.push(palette); - return palette; - } - commit(gl: WebGLRenderingContext): boolean { - let changed = this.initialized; + const palette = createPalette(colors); + palettes.push(palette); + this.dirty.push(palette); + return palette; + } + commit(gl: WebGLRenderingContext): boolean { + let changed = this.initialized; - if (!this.paletteTexture) { - this.initializeTexture(gl, this.size); - changed = true; - } + if (!this.paletteTexture) { + this.initializeTexture(gl, this.size); + changed = true; + } - if (this.dirty.length) { - if (!this.arrange(this.dirty)) { - this.cleanupPalettes(); - changed = true; + if (this.dirty.length) { + if (!this.arrange(this.dirty)) { + this.cleanupPalettes(); + changed = true; - while (!this.arrange(this.dirty)) { - if (this.size < MAX_SIZE) { - this.initializeTexture(gl, this.size * 2); - } else { - throw new Error('Exceeded maximum palettes limit'); - } - } - } + while (!this.arrange(this.dirty)) { + if (this.size < MAX_SIZE) { + this.initializeTexture(gl, this.size * 2); + } else { + throw new Error('Exceeded maximum palettes limit'); + } + } + } - this.updateTexture(gl); - this.dirty = []; - } + this.updateTexture(gl); + this.dirty = []; + } - this.initialized = false; - return changed; - } - init(gl: WebGLRenderingContext) { - this.initialized = true; - this.paletteTexture = undefined; - this.initializeTexture(gl, this.size); - } - dispose(gl: WebGLRenderingContext | undefined) { - this.paletteTexture = disposeTexture(gl, this.paletteTexture); + this.initialized = false; + return changed; + } + init(gl: WebGLRenderingContext) { + this.initialized = true; + this.paletteTexture = undefined; + this.initializeTexture(gl, this.size); + } + dispose(gl: WebGLRenderingContext | undefined) { + this.paletteTexture = disposeTexture(gl, this.paletteTexture); - for (let i = 0; i < this.palettes.length; i++) { - if (this.palettes[i].length > 0) { - this.palettes[i] = []; - } - } + for (let i = 0; i < this.palettes.length; i++) { + if (this.palettes[i].length > 0) { + this.palettes[i] = []; + } + } - this.size = INITIAL_SIZE; - this.resetPalettes(); - } - cleanup() { - this.cleanupPalettes(); - } - private resetPalettes() { - this.dirty = flatten(this.palettes); - this.lastX = 0; - this.lastY = 0; - } - private cleanupPalettes() { - const palettes = this.palettes; + this.size = INITIAL_SIZE; + this.resetPalettes(); + } + cleanup() { + this.cleanupPalettes(); + } + private resetPalettes() { + this.dirty = flatten(this.palettes); + this.lastX = 0; + this.lastY = 0; + } + private cleanupPalettes() { + const palettes = this.palettes; - for (let i = 0; i < palettes.length; i++) { - if (palettes[i].length > 0) { - palettes[i] = palettes[i].filter(isInUse); - } - } + for (let i = 0; i < palettes.length; i++) { + if (palettes[i].length > 0) { + palettes[i] = palettes[i].filter(isInUse); + } + } - this.resetPalettes(); - } - private initializeTexture(gl: WebGLRenderingContext, size: number) { - try { - if (!this.paletteTexture) { - this.paletteTexture = createEmptyTexture(gl, size, size, gl.RGBA, gl.UNSIGNED_BYTE); - } else if (this.paletteTexture.width !== size) { - resizeTexture(gl, this.paletteTexture, size, size); - } - } catch (e) { - throw new Error(`Failed to create/resize texture (${size}) ${e.stack}`); - } + this.resetPalettes(); + } + private initializeTexture(gl: WebGLRenderingContext, size: number) { + try { + if (!this.paletteTexture) { + this.paletteTexture = createEmptyTexture(gl, size, size, gl.RGBA, gl.UNSIGNED_BYTE); + } else if (this.paletteTexture.width !== size) { + resizeTexture(gl, this.paletteTexture, size, size); + } + } catch (e) { + throw new Error(`Failed to create/resize texture (${size}) ${e.stack}`); + } - this.size = size; - this.resetPalettes(); - } - private arrange(palettes: Palette[]) { - if (!palettes.length) { - return true; - } + this.size = size; + this.resetPalettes(); + } + private arrange(palettes: Palette[]) { + if (!palettes.length) { + return true; + } - const size = this.size | 0; - let x = this.lastX | 0; - let y = this.lastY | 0; - let minY = -1; - let maxY = -1; + const size = this.size | 0; + let x = this.lastX | 0; + let y = this.lastY | 0; + let minY = -1; + let maxY = -1; - for (let i = 0; i < palettes.length; i++) { - const p = palettes[i]; - const colorCount = p.colors.length | 0; + for (let i = 0; i < palettes.length; i++) { + const p = palettes[i]; + const colorCount = p.colors.length | 0; - if ((size - x) < colorCount) { - x = 0; - y++; + if ((size - x) < colorCount) { + x = 0; + y++; - if (y >= size) { - return false; - } - } + if (y >= size) { + return false; + } + } - p.x = x; - p.y = y; - p.u = (x + 0.5) / size; - p.v = (y + 0.5) / size; - x = (x + colorCount) | 0; + p.x = x; + p.y = y; + p.u = (x + 0.5) / size; + p.v = (y + 0.5) / size; + x = (x + colorCount) | 0; - minY = minY === -1 ? y : minY; - maxY = Math.max(maxY, y); - } + minY = minY === -1 ? y : minY; + maxY = Math.max(maxY, y); + } - this.lastX = x; - this.lastY = y; - this.dirtyMinY = minY; - this.dirtyMaxY = maxY; - return true; - } - private updateTexture(gl: WebGLRenderingContext) { - if (!this.paletteTexture || this.dirtyMinY > this.dirtyMaxY) - return; + this.lastX = x; + this.lastY = y; + this.dirtyMinY = minY; + this.dirtyMaxY = maxY; + return true; + } + private updateTexture(gl: WebGLRenderingContext) { + if (!this.paletteTexture || this.dirtyMinY > this.dirtyMaxY) + return; - const width = this.size; - const height = (this.dirtyMaxY - this.dirtyMinY) + 1; - const data = new Uint8Array(width * height * 4); + const width = this.size; + const height = (this.dirtyMaxY - this.dirtyMinY) + 1; + const data = new Uint8Array(width * height * 4); - for (let k = 0; k < this.palettes.length; k++) { - const palettes = this.palettes[k]; + for (let k = 0; k < this.palettes.length; k++) { + const palettes = this.palettes[k]; - for (let i = 0; i < palettes.length; i++) { - const { x, y, colors } = palettes[i]; + for (let i = 0; i < palettes.length; i++) { + const { x, y, colors } = palettes[i]; - if (y < this.dirtyMinY || y > this.dirtyMaxY) - continue; + if (y < this.dirtyMinY || y > this.dirtyMaxY) + continue; - let offset = (x + (y - this.dirtyMinY) * width) << 2; + let offset = (x + (y - this.dirtyMinY) * width) << 2; - for (let j = 0; j < colors.length; j++) { - const c = colors[j]; - data[offset++] = (c >>> 24) & 0xff; - data[offset++] = (c >>> 16) & 0xff; - data[offset++] = (c >>> 8) & 0xff; - data[offset++] = (c >>> 0) & 0xff; - } - } - } + for (let j = 0; j < colors.length; j++) { + const c = colors[j]; + data[offset++] = (c >>> 24) & 0xff; + data[offset++] = (c >>> 16) & 0xff; + data[offset++] = (c >>> 8) & 0xff; + data[offset++] = (c >>> 0) & 0xff; + } + } + } - gl.bindTexture(gl.TEXTURE_2D, this.paletteTexture.handle); - gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, this.dirtyMinY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); + gl.bindTexture(gl.TEXTURE_2D, this.paletteTexture.handle); + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, this.dirtyMinY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); - this.dirtyMinY = 0; - this.dirtyMaxY = -1; - } + this.dirtyMinY = 0; + this.dirtyMaxY = -1; + } } diff --git a/src/ts/graphics/paletteSpriteBatch.ts b/src/ts/graphics/paletteSpriteBatch.ts index 445667e..e0cdb07 100644 --- a/src/ts/graphics/paletteSpriteBatch.ts +++ b/src/ts/graphics/paletteSpriteBatch.ts @@ -7,123 +7,123 @@ import { createPalette } from './paletteManager'; const defaultRectSprite = createSprite(0, 0, 1, 1, 0, 0, 3); const types = new Float32Array([ - colorToFloat(colorFromRGBA(255, 0, 0, 0)), // type 0 shade - colorToFloat(colorFromRGBA(0, 0, 255, 0)), // type 2 shade - colorToFloat(colorFromRGBA(0, 0, 0, 0)), // type 3 shade - colorToFloat(colorFromRGBA(255, 0, 0, 255)), // type 0 - colorToFloat(colorFromRGBA(0, 255, 0, 255)), // type 1 - colorToFloat(colorFromRGBA(0, 0, 255, 255)), // type 2 - colorToFloat(colorFromRGBA(0, 0, 0, 255)), // type 3 + colorToFloat(colorFromRGBA(255, 0, 0, 0)), // type 0 shade + colorToFloat(colorFromRGBA(0, 0, 255, 0)), // type 2 shade + colorToFloat(colorFromRGBA(0, 0, 0, 0)), // type 3 shade + colorToFloat(colorFromRGBA(255, 0, 0, 255)), // type 0 + colorToFloat(colorFromRGBA(0, 255, 0, 255)), // type 1 + colorToFloat(colorFromRGBA(0, 0, 255, 255)), // type 2 + colorToFloat(colorFromRGBA(0, 0, 0, 255)), // type 3 ]); /* function pushVertex( - vertices: Float32Array, _verticesUint32: Uint32Array, index: number, - x: number, y: number, u: number, v: number, pu: number, pv: number, c: number, c1: number, transform: Matrix2D + vertices: Float32Array, _verticesUint32: Uint32Array, index: number, + x: number, y: number, u: number, v: number, pu: number, pv: number, c: number, c1: number, transform: Matrix2D ) { - vertices[index++] = transform[0] * x + transform[2] * y + transform[4]; - vertices[index++] = transform[1] * x + transform[3] * y + transform[5]; - vertices[index++] = u; - vertices[index++] = v; - vertices[index++] = pu; - vertices[index++] = pv; - vertices[index++] = c; - vertices[index++] = c1; + vertices[index++] = transform[0] * x + transform[2] * y + transform[4]; + vertices[index++] = transform[1] * x + transform[3] * y + transform[5]; + vertices[index++] = u; + vertices[index++] = v; + vertices[index++] = pu; + vertices[index++] = pv; + vertices[index++] = c; + vertices[index++] = c1; } function pushQuad( - vertices: Float32Array, verticesUint32: Uint32Array, - transform: mat2d, index: number, type: number, color: number, palette: Palette, - sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + vertices: Float32Array, verticesUint32: Uint32Array, + transform: mat2d, index: number, type: number, color: number, palette: Palette, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number ) { - const c1 = types[type]; + const c1 = types[type]; - const y2 = dy + dh; - const x2 = dx + dw; + const y2 = dy + dh; + const x2 = dx + dw; - const u1 = sx + 0.1; - const v1 = sy + 0.1; - const u2 = sx + sw; - const v2 = sy + sh; + const u1 = sx + 0.1; + const v1 = sy + 0.1; + const u2 = sx + sw; + const v2 = sy + sh; - const pu = palette.u; - const pv = palette.v; + const pu = palette.u; + const pv = palette.v; - pushVertex(vertices, verticesUint32, index, dx, dy, u1, v1, pu, pv, color, c1, transform); - pushVertex(vertices, verticesUint32, index + 8, x2, dy, u2, v1, pu, pv, color, c1, transform); - pushVertex(vertices, verticesUint32, index + 16, x2, y2, u2, v2, pu, pv, color, c1, transform); - pushVertex(vertices, verticesUint32, index + 24, dx, y2, u1, v2, pu, pv, color, c1, transform); + pushVertex(vertices, verticesUint32, index, dx, dy, u1, v1, pu, pv, color, c1, transform); + pushVertex(vertices, verticesUint32, index + 8, x2, dy, u2, v1, pu, pv, color, c1, transform); + pushVertex(vertices, verticesUint32, index + 16, x2, y2, u2, v2, pu, pv, color, c1, transform); + pushVertex(vertices, verticesUint32, index + 24, dx, y2, u1, v2, pu, pv, color, c1, transform); - return index + 32; + return index + 32; } */ function pushQuad( - vertices: Float32Array, //_verticesUint32: Uint32Array, - transform: Matrix2D, index: number, type: number, color: number, palette: Palette, - sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number, + vertices: Float32Array, //_verticesUint32: Uint32Array, + transform: Matrix2D, index: number, type: number, color: number, palette: Palette, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number, ) { - const c1 = types[type]; + const c1 = types[type]; - const y2 = dy + dh; - const x2 = dx + dw; + const y2 = dy + dh; + const x2 = dx + dw; - const u1 = sx + 0.1; - const v1 = sy + 0.1; - const u2 = sx + sw; - const v2 = sy + sh; + const u1 = sx + 0.1; + const v1 = sy + 0.1; + const u2 = sx + sw; + const v2 = sy + sh; - const pu = palette.u; // + palette.v * 1024; - const pv = palette.v; + const pu = palette.u; // + palette.v * 1024; + const pv = palette.v; - const t0 = transform[0]; - const t1 = transform[1]; - const t2 = transform[2]; - const t3 = transform[3]; - const t4 = transform[4]; - const t5 = transform[5]; + const t0 = transform[0]; + const t1 = transform[1]; + const t2 = transform[2]; + const t3 = transform[3]; + const t4 = transform[4]; + const t5 = transform[5]; - // pushVertex(vertices, index, dx, dy, u1, v1, pu, pv, color, c1, transform); - vertices[(index + 0) | 0] = t0 * dx + t2 * dy + t4; - vertices[(index + 1) | 0] = t1 * dx + t3 * dy + t5; - vertices[(index + 2) | 0] = u1; - vertices[(index + 3) | 0] = v1; - vertices[(index + 4) | 0] = pu; - vertices[(index + 5) | 0] = pv; - vertices[(index + 6) | 0] = color; - vertices[(index + 7) | 0] = c1; + // pushVertex(vertices, index, dx, dy, u1, v1, pu, pv, color, c1, transform); + vertices[(index + 0) | 0] = t0 * dx + t2 * dy + t4; + vertices[(index + 1) | 0] = t1 * dx + t3 * dy + t5; + vertices[(index + 2) | 0] = u1; + vertices[(index + 3) | 0] = v1; + vertices[(index + 4) | 0] = pu; + vertices[(index + 5) | 0] = pv; + vertices[(index + 6) | 0] = color; + vertices[(index + 7) | 0] = c1; - // pushVertex(vertices, index + 8, x2, dy, u2, v1, pu, pv, color, c1, transform); - vertices[(index + 8) | 0] = t0 * x2 + t2 * dy + t4; - vertices[(index + 9) | 0] = t1 * x2 + t3 * dy + t5; - vertices[(index + 10) | 0] = u2; - vertices[(index + 11) | 0] = v1; - vertices[(index + 12) | 0] = pu; - vertices[(index + 13) | 0] = pv; - vertices[(index + 14) | 0] = color; - vertices[(index + 15) | 0] = c1; + // pushVertex(vertices, index + 8, x2, dy, u2, v1, pu, pv, color, c1, transform); + vertices[(index + 8) | 0] = t0 * x2 + t2 * dy + t4; + vertices[(index + 9) | 0] = t1 * x2 + t3 * dy + t5; + vertices[(index + 10) | 0] = u2; + vertices[(index + 11) | 0] = v1; + vertices[(index + 12) | 0] = pu; + vertices[(index + 13) | 0] = pv; + vertices[(index + 14) | 0] = color; + vertices[(index + 15) | 0] = c1; - // pushVertex(vertices, index + 16, x2, y2, u2, v2, pu, pv, color, c1, transform); - vertices[(index + 16) | 0] = t0 * x2 + t2 * y2 + t4; - vertices[(index + 17) | 0] = t1 * x2 + t3 * y2 + t5; - vertices[(index + 18) | 0] = u2; - vertices[(index + 19) | 0] = v2; - vertices[(index + 20) | 0] = pu; - vertices[(index + 21) | 0] = pv; - vertices[(index + 22) | 0] = color; - vertices[(index + 23) | 0] = c1; + // pushVertex(vertices, index + 16, x2, y2, u2, v2, pu, pv, color, c1, transform); + vertices[(index + 16) | 0] = t0 * x2 + t2 * y2 + t4; + vertices[(index + 17) | 0] = t1 * x2 + t3 * y2 + t5; + vertices[(index + 18) | 0] = u2; + vertices[(index + 19) | 0] = v2; + vertices[(index + 20) | 0] = pu; + vertices[(index + 21) | 0] = pv; + vertices[(index + 22) | 0] = color; + vertices[(index + 23) | 0] = c1; - // pushVertex(vertices, index + 24, dx, y2, u1, v2, pu, pv, color, c1, transform); - vertices[(index + 24) | 0] = t0 * dx + t2 * y2 + t4; - vertices[(index + 25) | 0] = t1 * dx + t3 * y2 + t5; - vertices[(index + 26) | 0] = u1; - vertices[(index + 27) | 0] = v2; - vertices[(index + 28) | 0] = pu; - vertices[(index + 29) | 0] = pv; - vertices[(index + 30) | 0] = color; - vertices[(index + 31) | 0] = c1; + // pushVertex(vertices, index + 24, dx, y2, u1, v2, pu, pv, color, c1, transform); + vertices[(index + 24) | 0] = t0 * dx + t2 * y2 + t4; + vertices[(index + 25) | 0] = t1 * dx + t3 * y2 + t5; + vertices[(index + 26) | 0] = u1; + vertices[(index + 27) | 0] = v2; + vertices[(index + 28) | 0] = pu; + vertices[(index + 29) | 0] = pv; + vertices[(index + 30) | 0] = color; + vertices[(index + 31) | 0] = c1; - return index + 32; + return index + 32; } // function colorWithAlpha(color: number, alpha: number) { @@ -133,111 +133,111 @@ function pushQuad( export const PALETTE_BATCH_BYTES_PER_VERTEX = 2 * 4 + 4 * 4 + 4 + 4; export class PaletteSpriteBatch extends BaseSpriteBatch implements IPaletteSpriteBatch { - palette = true; - defaultPalette: Palette = createPalette(new Uint32Array(0)); - constructor( - gl: WebGLRenderingContext, capacity: number, buffer: ArrayBuffer, vertexBuffer: WebGLBuffer, indexBuffer: WebGLBuffer - ) { - super(gl, capacity, buffer, vertexBuffer, indexBuffer, [ - { name: 'position', size: 2 }, - { name: 'texcoord0', size: 4 }, //, type: gl.UNSIGNED_SHORT }, - { name: 'color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true }, - { name: 'color1', size: 4, type: gl.UNSIGNED_BYTE, normalized: true }, - ]); - } - drawImage( - type: number, color: number, palette: Palette | undefined, - sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number - ) { - if (this.capacity <= this.spritesCount) { - this.flush(); - } + palette = true; + defaultPalette: Palette = createPalette(new Uint32Array(0)); + constructor( + gl: WebGLRenderingContext, capacity: number, buffer: ArrayBuffer, vertexBuffer: WebGLBuffer, indexBuffer: WebGLBuffer + ) { + super(gl, capacity, buffer, vertexBuffer, indexBuffer, [ + { name: 'position', size: 2 }, + { name: 'texcoord0', size: 4 }, //, type: gl.UNSIGNED_SHORT }, + { name: 'color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true }, + { name: 'color1', size: 4, type: gl.UNSIGNED_BYTE, normalized: true }, + ]); + } + drawImage( + type: number, color: number, palette: Palette | undefined, + sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number + ) { + if (this.capacity <= this.spritesCount) { + this.flush(); + } - this.index = pushQuad( - this.vertices, //this.verticesUint32, - this.transform, this.index, type, getColorFloat(color, this.globalAlpha), - palette || this.defaultPalette, sx, sy, sw, sh, dx, dy, dw, dh, - ); - this.spritesCount++; - this.tris += 2; - } - drawRect(color: number, x: number, y: number, w: number, h: number) { - if (w !== 0 && h !== 0) { - if (this.capacity <= this.spritesCount) { - this.flush(); - } + this.index = pushQuad( + this.vertices, //this.verticesUint32, + this.transform, this.index, type, getColorFloat(color, this.globalAlpha), + palette || this.defaultPalette, sx, sy, sw, sh, dx, dy, dw, dh, + ); + this.spritesCount++; + this.tris += 2; + } + drawRect(color: number, x: number, y: number, w: number, h: number) { + if (w !== 0 && h !== 0) { + if (this.capacity <= this.spritesCount) { + this.flush(); + } - const s = this.rectSprite || defaultRectSprite; - this.index = pushQuad( - this.vertices, //this.verticesUint32, - this.transform, this.index, s.type, getColorFloat(color, this.globalAlpha), - this.defaultPalette, s.x, s.y, s.w, s.h, x, y, w, h, - ); - this.spritesCount++; - this.tris += 2; - } - } - drawSprite(s: Sprite, color: number, palette: Palette | undefined, x: number, y: number) { - if (s.w !== 0 && s.h !== 0) { - if (this.capacity <= this.spritesCount) { - this.flush(); - } + const s = this.rectSprite || defaultRectSprite; + this.index = pushQuad( + this.vertices, //this.verticesUint32, + this.transform, this.index, s.type, getColorFloat(color, this.globalAlpha), + this.defaultPalette, s.x, s.y, s.w, s.h, x, y, w, h, + ); + this.spritesCount++; + this.tris += 2; + } + } + drawSprite(s: Sprite, color: number, palette: Palette | undefined, x: number, y: number) { + if (s.w !== 0 && s.h !== 0) { + if (this.capacity <= this.spritesCount) { + this.flush(); + } - if (this.hasCrop) { - const crop = this.cropRect; + if (this.hasCrop) { + const crop = this.cropRect; - let sx = s.x; - let sy = s.y; - let w = s.w; - let h = s.h; - let dx = x + s.ox; - let dy = y + s.oy; + let sx = s.x; + let sy = s.y; + let w = s.w; + let h = s.h; + let dx = x + s.ox; + let dy = y + s.oy; - const cropX = crop.x; // - this.transform[4]; - const cropY = crop.y; // - this.transform[5]; - const shiftLeft = cropX - dx; - const shiftTop = cropY - dy; - const shiftRight = (dx + w) - (cropX + crop.w); - const shiftBottom = (dy + h) - (cropY + crop.h); + const cropX = crop.x; // - this.transform[4]; + const cropY = crop.y; // - this.transform[5]; + const shiftLeft = cropX - dx; + const shiftTop = cropY - dy; + const shiftRight = (dx + w) - (cropX + crop.w); + const shiftBottom = (dy + h) - (cropY + crop.h); - if (shiftLeft > 0) { - sx += shiftLeft; - dx += shiftLeft; - w -= shiftLeft; - } + if (shiftLeft > 0) { + sx += shiftLeft; + dx += shiftLeft; + w -= shiftLeft; + } - if (shiftRight > 0) { - w -= shiftRight; - } + if (shiftRight > 0) { + w -= shiftRight; + } - if (shiftTop > 0) { - sy += shiftTop; - dy += shiftTop; - h -= shiftTop; - } + if (shiftTop > 0) { + sy += shiftTop; + dy += shiftTop; + h -= shiftTop; + } - if (shiftBottom > 0) { - h -= shiftBottom; - } + if (shiftBottom > 0) { + h -= shiftBottom; + } - if (w > 0 && h > 0) { - this.index = pushQuad( - this.vertices, //this.verticesUint32, - this.transform, this.index, s.type, getColorFloat(color, this.globalAlpha), - palette || this.defaultPalette, sx, sy, w, h, dx, dy, w, h, - ); - this.spritesCount++; - this.tris += 2; - } - } else { - this.index = pushQuad( - this.vertices, //this.verticesUint32, - this.transform, this.index, s.type, getColorFloat(color, this.globalAlpha), - palette || this.defaultPalette, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h, - ); - this.spritesCount++; - this.tris += 2; - } - } - } + if (w > 0 && h > 0) { + this.index = pushQuad( + this.vertices, //this.verticesUint32, + this.transform, this.index, s.type, getColorFloat(color, this.globalAlpha), + palette || this.defaultPalette, sx, sy, w, h, dx, dy, w, h, + ); + this.spritesCount++; + this.tris += 2; + } + } else { + this.index = pushQuad( + this.vertices, //this.verticesUint32, + this.transform, this.index, s.type, getColorFloat(color, this.globalAlpha), + palette || this.defaultPalette, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h, + ); + this.spritesCount++; + this.tris += 2; + } + } + } } diff --git a/src/ts/graphics/spriteBatch.ts b/src/ts/graphics/spriteBatch.ts index f308d84..e173346 100644 --- a/src/ts/graphics/spriteBatch.ts +++ b/src/ts/graphics/spriteBatch.ts @@ -2,14 +2,14 @@ import { Sprite, SpriteBatch as ISpriteBatch, Matrix2D } from '../common/interfa import { BaseSpriteBatch, getColorFloat } from './baseSpriteBatch'; function vertex( - vertices: Float32Array, _verticesUint32: Uint32Array, index: number, - x: number, y: number, u: number, v: number, c: number, transform: Matrix2D + vertices: Float32Array, _verticesUint32: Uint32Array, index: number, + x: number, y: number, u: number, v: number, c: number, transform: Matrix2D ) { - vertices[index++] = transform[0] * x + transform[2] * y + transform[4]; - vertices[index++] = transform[1] * x + transform[3] * y + transform[5]; - vertices[index++] = u; - vertices[index++] = v; - vertices[index++] = c; + vertices[index++] = transform[0] * x + transform[2] * y + transform[4]; + vertices[index++] = transform[1] * x + transform[3] * y + transform[5]; + vertices[index++] = u; + vertices[index++] = v; + vertices[index++] = c; } // function colorWithAlpha(color: number, alpha: number) { @@ -17,68 +17,68 @@ function vertex( // } export class SpriteBatch extends BaseSpriteBatch implements ISpriteBatch { - palette = false; - depth = 0; - constructor( - gl: WebGLRenderingContext, - capacity: number, - buffer: ArrayBuffer, - vertexBuffer: WebGLBuffer, - indexBuffer: WebGLBuffer - ) { - super(gl, capacity, buffer, vertexBuffer, indexBuffer, [ - { name: 'position', size: 2 }, - { name: 'texcoord0', size: 2 }, // , type: gl.UNSIGNED_SHORT }, - { name: 'color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true }, - ]); - } - drawImage( - color: number, sx: number, sy: number, sw: number, sh: number, - dx: number, dy: number, dw: number, dh: number - ) { - if (this.capacity <= this.spritesCount) { - this.flush(); - } + palette = false; + depth = 0; + constructor( + gl: WebGLRenderingContext, + capacity: number, + buffer: ArrayBuffer, + vertexBuffer: WebGLBuffer, + indexBuffer: WebGLBuffer + ) { + super(gl, capacity, buffer, vertexBuffer, indexBuffer, [ + { name: 'position', size: 2 }, + { name: 'texcoord0', size: 2 }, // , type: gl.UNSIGNED_SHORT }, + { name: 'color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true }, + ]); + } + drawImage( + color: number, sx: number, sy: number, sw: number, sh: number, + dx: number, dy: number, dw: number, dh: number + ) { + if (this.capacity <= this.spritesCount) { + this.flush(); + } - const c = getColorFloat(color, this.globalAlpha); + const c = getColorFloat(color, this.globalAlpha); - const x2 = dx + dw; - const y2 = dy + dh; + const x2 = dx + dw; + const y2 = dy + dh; - const u1 = sx; - const v1 = sy; - const u2 = sx + sw; - const v2 = sy + sh; + const u1 = sx; + const v1 = sy; + const u2 = sx + sw; + const v2 = sy + sh; - const vertices = this.vertices; - const verticesUint32 = this.verticesUint32; - const transform = this.transform; + const vertices = this.vertices; + const verticesUint32 = this.verticesUint32; + const transform = this.transform; - const index = this.index; + const index = this.index; - vertex(vertices, verticesUint32, index, dx, dy, u1, v1, c, transform); - vertex(vertices, verticesUint32, index + 5, x2, dy, u2, v1, c, transform); - vertex(vertices, verticesUint32, index + 10, x2, y2, u2, v2, c, transform); - vertex(vertices, verticesUint32, index + 15, dx, y2, u1, v2, c, transform); + vertex(vertices, verticesUint32, index, dx, dy, u1, v1, c, transform); + vertex(vertices, verticesUint32, index + 5, x2, dy, u2, v1, c, transform); + vertex(vertices, verticesUint32, index + 10, x2, y2, u2, v2, c, transform); + vertex(vertices, verticesUint32, index + 15, dx, y2, u1, v2, c, transform); - this.index += 20; - this.spritesCount++; - this.tris += 2; - } - drawRect(color: number, x: number, y: number, w: number, h: number) { - if (w && h) { - const rect = this.rectSprite; + this.index += 20; + this.spritesCount++; + this.tris += 2; + } + drawRect(color: number, x: number, y: number, w: number, h: number) { + if (w && h) { + const rect = this.rectSprite; - if (rect) { - this.drawImage(color, rect.x, rect.y, rect.w, rect.h, x, y, w, h); - } else { - this.drawImage(color, 0, 0, 1, 1, x, y, w, h); - } - } - } - drawSprite(s: Sprite, color: number, x: number, y: number) { - if (s && s.w && s.h) { - this.drawImage(color, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h); - } - } + if (rect) { + this.drawImage(color, rect.x, rect.y, rect.w, rect.h, x, y, w, h); + } else { + this.drawImage(color, 0, 0, 1, 1, x, y, w, h); + } + } + } + drawSprite(s: Sprite, color: number, x: number, y: number) { + if (s && s.w && s.h) { + this.drawImage(color, s.x, s.y, s.w, s.h, x + s.ox, y + s.oy, s.w, s.h); + } + } } diff --git a/src/ts/graphics/spriteFont.ts b/src/ts/graphics/spriteFont.ts index 673be5e..e87737a 100644 --- a/src/ts/graphics/spriteFont.ts +++ b/src/ts/graphics/spriteFont.ts @@ -4,24 +4,24 @@ import { stringToCodesTemp, codesBuffer } from '../common/stringUtils'; import { createSprite } from '../client/spriteUtils'; export const enum HAlign { - Left, - Right, - Center, + Left, + Right, + Center, } export const enum VAlign { - Top, - Bottom, - Middle, + Top, + Bottom, + Middle, } export interface TextOptions { - skipEmotes?: boolean; - colorEmotes?: boolean; - palette?: Palette; - emojiPalette?: Palette; - lineSpacing?: number; - monospace?: boolean; + skipEmotes?: boolean; + colorEmotes?: boolean; + palette?: Palette; + emojiPalette?: Palette; + lineSpacing?: number; + monospace?: boolean; } type Charset = { code: number; sprite: Sprite; }[]; @@ -34,243 +34,243 @@ const LINEFEED = '\n'.charCodeAt(0); const defaultOptions: TextOptions = {}; export interface SpriteFont { - lineSpacing: number; - letterSpacing: number; - letterShiftX: number; - letterShiftY: number; - letterShiftWidth: number; - letterShiftHeight: number; - letterHeight: number; - letterHeightReal: number; - letterWidth: number; - chars: Map; - emoji: Map; - defaultChar: Sprite; + lineSpacing: number; + letterSpacing: number; + letterShiftX: number; + letterShiftY: number; + letterShiftWidth: number; + letterShiftHeight: number; + letterHeight: number; + letterHeightReal: number; + letterWidth: number; + chars: Map; + emoji: Map; + defaultChar: Sprite; } export function createSpriteFont(charset: Charset, emojiCharset: Charset, spaceWidth: number): SpriteFont { - const lineSpacing = 2; - const letterSpacing = 1; - const letterShiftX = 0; - const letterShiftY = 0; - const letterShiftWidth = 0; - const letterShiftHeight = 0; + const lineSpacing = 2; + const letterSpacing = 1; + const letterShiftX = 0; + const letterShiftY = 0; + const letterShiftWidth = 0; + const letterShiftHeight = 0; - const chars = new Map(); - const emoji = new Map(); + const chars = new Map(); + const emoji = new Map(); - charset.filter(c => !!c).forEach(c => chars.set(c.code, c.sprite)); - emojiCharset.filter(e => !!e && !!e.code).forEach(e => emoji.set(e.code, e.sprite)); + charset.filter(c => !!c).forEach(c => chars.set(c.code, c.sprite)); + emojiCharset.filter(e => !!e && !!e.code).forEach(e => emoji.set(e.code, e.sprite)); - const char0 = chars.get(0)!; - const letterHeight = char0.h; - const letterWidth = char0.w; - const letterHeightReal = char0.h + char0.oy; - chars.set(SPACE, createSprite(0, 0, 0, 0, spaceWidth, char0.h, 0)); - chars.set(TAB, createSprite(0, 0, 0, 0, spaceWidth * 4, char0.h, 0)); - const defaultChar = chars.get(DEFAULT)!; + const char0 = chars.get(0)!; + const letterHeight = char0.h; + const letterWidth = char0.w; + const letterHeightReal = char0.h + char0.oy; + chars.set(SPACE, createSprite(0, 0, 0, 0, spaceWidth, char0.h, 0)); + chars.set(TAB, createSprite(0, 0, 0, 0, spaceWidth * 4, char0.h, 0)); + const defaultChar = chars.get(DEFAULT)!; - return { - lineSpacing, letterSpacing, letterShiftX, letterShiftY, letterShiftWidth, letterShiftHeight, - letterHeight, letterHeightReal, letterWidth, chars, emoji, defaultChar, - }; + return { + lineSpacing, letterSpacing, letterShiftX, letterShiftY, letterShiftWidth, letterShiftHeight, + letterHeight, letterHeightReal, letterWidth, chars, emoji, defaultChar, + }; } function drawChars( - batch: Batch, chars: Uint32Array, length: number, font: SpriteFont, color: number, x: number, y: number, - options: TextOptions + batch: Batch, chars: Uint32Array, length: number, font: SpriteFont, color: number, x: number, y: number, + options: TextOptions ) { - const { lineSpacing = font.lineSpacing, monospace = false } = options; - x = Math.round(x) | 0; - y = Math.round(y) | 0; - let currentX = x; + const { lineSpacing = font.lineSpacing, monospace = false } = options; + x = Math.round(x) | 0; + y = Math.round(y) | 0; + let currentX = x; - for (let i = 0; i < length; i++) { - const code = chars[i]; + for (let i = 0; i < length; i++) { + const code = chars[i]; - if (code === LINEFEED) { - currentX = x; - y += font.letterHeight + lineSpacing; - } else { - const charWidth = drawChar(batch, font, code, color, currentX, y, options); - currentX += (monospace ? font.letterWidth : charWidth) + font.letterSpacing; - } - } + if (code === LINEFEED) { + currentX = x; + y += font.letterHeight + lineSpacing; + } else { + const charWidth = drawChar(batch, font, code, color, currentX, y, options); + currentX += (monospace ? font.letterWidth : charWidth) + font.letterSpacing; + } + } } export function drawText( - batch: Batch, text: string, font: SpriteFont, color: number, x: number, y: number, options = defaultOptions + batch: Batch, text: string, font: SpriteFont, color: number, x: number, y: number, options = defaultOptions ) { - const length = stringToCodesTemp(text); - drawChars(batch, codesBuffer, length, font, color, x, y, options); + const length = stringToCodesTemp(text); + drawChars(batch, codesBuffer, length, font, color, x, y, options); } export function drawOutlinedText( - batch: Batch, text: string, font: SpriteFont, color: number, outlineColor: number, x: number, y: number, - options = defaultOptions + batch: Batch, text: string, font: SpriteFont, color: number, outlineColor: number, x: number, y: number, + options = defaultOptions ) { - const length = stringToCodesTemp(text); + const length = stringToCodesTemp(text); - options.skipEmotes = true; + options.skipEmotes = true; - drawChars(batch, codesBuffer, length, font, outlineColor, x - 1, y - 1, options); - drawChars(batch, codesBuffer, length, font, outlineColor, x + 1, y - 1, options); - drawChars(batch, codesBuffer, length, font, outlineColor, x - 1, y + 1, options); - drawChars(batch, codesBuffer, length, font, outlineColor, x + 1, y + 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x - 1, y - 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x + 1, y - 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x - 1, y + 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x + 1, y + 1, options); - drawChars(batch, codesBuffer, length, font, outlineColor, x - 1, y, options); - drawChars(batch, codesBuffer, length, font, outlineColor, x, y - 1, options); - drawChars(batch, codesBuffer, length, font, outlineColor, x + 1, y, options); - drawChars(batch, codesBuffer, length, font, outlineColor, x, y + 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x - 1, y, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x, y - 1, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x + 1, y, options); + drawChars(batch, codesBuffer, length, font, outlineColor, x, y + 1, options); - options.skipEmotes = false; + options.skipEmotes = false; - drawChars(batch, codesBuffer, length, font, color, x, y, options); + drawChars(batch, codesBuffer, length, font, color, x, y, options); } export function drawTextAligned( - spriteBatch: Batch, text: string, font: SpriteFont, color: number, rect: Rect, halign = HAlign.Left, valign = VAlign.Top, - options: TextOptions = defaultOptions + spriteBatch: Batch, text: string, font: SpriteFont, color: number, rect: Rect, halign = HAlign.Left, valign = VAlign.Top, + options: TextOptions = defaultOptions ) { - const length = stringToCodesTemp(text); - const { x, y } = alignChars(font, codesBuffer, length, rect, halign, valign); - drawChars(spriteBatch, codesBuffer, length, font, color, x, y, options); + const length = stringToCodesTemp(text); + const { x, y } = alignChars(font, codesBuffer, length, rect, halign, valign); + drawChars(spriteBatch, codesBuffer, length, font, color, x, y, options); } export function lineBreak(text: string, font: SpriteFont, width: number) { - const lines: string[][] = []; - const spaceWidth = measureChar(font, SPACE) + font.letterSpacing * 2; + const lines: string[][] = []; + const spaceWidth = measureChar(font, SPACE) + font.letterSpacing * 2; - let line: string[] = []; - let lineWidth = 0; + let line: string[] = []; + let lineWidth = 0; - for (const word of text.split(' ')) { - const { w } = measureText(word, font); + for (const word of text.split(' ')) { + const { w } = measureText(word, font); - if (lineWidth) { - lineWidth += spaceWidth; + if (lineWidth) { + lineWidth += spaceWidth; - if (lineWidth + w > width) { - lines.push(line); - line = []; - lineWidth = 0; - } - } + if (lineWidth + w > width) { + lines.push(line); + line = []; + lineWidth = 0; + } + } - line.push(word); - lineWidth += w; - } + line.push(word); + lineWidth += w; + } - if (line.length) { - lines.push(line); - } + if (line.length) { + lines.push(line); + } - return lines.map(x => x.join(' ')).join('\n'); + return lines.map(x => x.join(' ')).join('\n'); } function measureChars(chars: Uint32Array, length: number, font: SpriteFont): { w: number; h: number; } { - let maxW = 0; - let lines = 1; - let w = 0; + let maxW = 0; + let lines = 1; + let w = 0; - for (let i = 0; i < length; i++) { - const code = chars[i]; + for (let i = 0; i < length; i++) { + const code = chars[i]; - if (code === LINEFEED) { - maxW = Math.max(maxW, w); - w = 0; - lines++; - } else { - if (w) { - w += font.letterSpacing; - } - w += measureChar(font, code); - } - } + if (code === LINEFEED) { + maxW = Math.max(maxW, w); + w = 0; + lines++; + } else { + if (w) { + w += font.letterSpacing; + } + w += measureChar(font, code); + } + } - return { - w: Math.max(maxW, w), - h: lines * font.letterHeight + (lines - 1) * font.lineSpacing - }; + return { + w: Math.max(maxW, w), + h: lines * font.letterHeight + (lines - 1) * font.lineSpacing + }; } export function measureText(text: string, font: SpriteFont) { - const length = stringToCodesTemp(text); - return measureChars(codesBuffer, length, font); + const length = stringToCodesTemp(text); + return measureChars(codesBuffer, length, font); } export function getCharacterSprite(char: string, font: SpriteFont): Sprite | undefined { - let sprite: Sprite | undefined; - let unset = false; - const length = stringToCodesTemp(char); + let sprite: Sprite | undefined; + let unset = false; + const length = stringToCodesTemp(char); - for (let i = 0; i < length; i++) { - const code = codesBuffer[i]; - unset = !!sprite; - sprite = font.emoji.get(code) || getChar(font, code); - } + for (let i = 0; i < length; i++) { + const code = codesBuffer[i]; + unset = !!sprite; + sprite = font.emoji.get(code) || getChar(font, code); + } - return unset ? undefined : sprite; + return unset ? undefined : sprite; } function measureChar(font: SpriteFont, code: number): number { - const sprite = font.emoji.get(code) || getChar(font, code); - return sprite.w + sprite.ox; + const sprite = font.emoji.get(code) || getChar(font, code); + return sprite.w + sprite.ox; } function drawChar( - batch: Batch, font: SpriteFont, code: number, color: number, x: number, y: number, options: TextOptions + batch: Batch, font: SpriteFont, code: number, color: number, x: number, y: number, options: TextOptions ): number { - const emote = font.emoji.get(code); - const px = x + font.letterShiftX; - const py = y + font.letterShiftY; + const emote = font.emoji.get(code); + const px = x + font.letterShiftX; + const py = y + font.letterShiftY; - if (emote) { - const skipEmote = !!options.skipEmotes; - const colorEmote = !!options.colorEmotes; - const emoteColor = colorEmote ? color : WHITE; + if (emote) { + const skipEmote = !!options.skipEmotes; + const colorEmote = !!options.colorEmotes; + const emoteColor = colorEmote ? color : WHITE; - if (!skipEmote) { - if (isPaletteSpriteBatch(batch)) { - batch.drawSprite(emote, emoteColor, options.emojiPalette, px, py); - } else { - batch.drawSprite(emote, emoteColor, px, py); - } - } + if (!skipEmote) { + if (isPaletteSpriteBatch(batch)) { + batch.drawSprite(emote, emoteColor, options.emojiPalette, px, py); + } else { + batch.drawSprite(emote, emoteColor, px, py); + } + } - return emote.w + emote.ox; - } else { - const c = getChar(font, code); + return emote.w + emote.ox; + } else { + const c = getChar(font, code); - if (isPaletteSpriteBatch(batch)) { - batch.drawSprite(c, color, options.palette, px, py); - } else { - batch.drawSprite(c, color, px, py); - } + if (isPaletteSpriteBatch(batch)) { + batch.drawSprite(c, color, options.palette, px, py); + } else { + batch.drawSprite(c, color, px, py); + } - return c.w + c.ox + font.letterShiftWidth; - } + return c.w + c.ox + font.letterShiftWidth; + } } function alignChars(font: SpriteFont, chars: Uint32Array, length: number, rect: Rect, halign: HAlign, valign: VAlign) { - let x = rect.x; - let y = rect.y; + let x = rect.x; + let y = rect.y; - if (halign !== HAlign.Left || valign !== VAlign.Top) { - const size = measureChars(chars, length, font); + if (halign !== HAlign.Left || valign !== VAlign.Top) { + const size = measureChars(chars, length, font); - if (halign !== HAlign.Left) { - x += halign === HAlign.Center ? (rect.w - size.w) / 2 : (rect.w - size.w); - } + if (halign !== HAlign.Left) { + x += halign === HAlign.Center ? (rect.w - size.w) / 2 : (rect.w - size.w); + } - if (valign !== VAlign.Top) { - y += valign === VAlign.Middle ? (rect.h - size.h) / 2 : (rect.h - size.h); - } - } + if (valign !== VAlign.Top) { + y += valign === VAlign.Middle ? (rect.h - size.h) / 2 : (rect.h - size.h); + } + } - return { x, y }; + return { x, y }; } function getChar(font: SpriteFont, code: number): Sprite { - return font.chars.get(code) || font.defaultChar; + return font.chars.get(code) || font.defaultChar; } diff --git a/src/ts/graphics/spriteSheetUtils.ts b/src/ts/graphics/spriteSheetUtils.ts index ced07f7..40f95a7 100644 --- a/src/ts/graphics/spriteSheetUtils.ts +++ b/src/ts/graphics/spriteSheetUtils.ts @@ -2,15 +2,15 @@ import { SpriteSheet } from '../common/interfaces'; import { createTexture, disposeTexture } from './webgl/texture2d'; export function createTexturesForSpriteSheets(gl: WebGLRenderingContext, sheets: SpriteSheet[], texture = createTexture) { - sheets.forEach(sheet => { - if (sheet.data) { - sheet.texture = texture(gl, sheet.data); - } - }); + sheets.forEach(sheet => { + if (sheet.data) { + sheet.texture = texture(gl, sheet.data); + } + }); } export function disposeTexturesForSpriteSheets(gl: WebGLRenderingContext, sheets: SpriteSheet[]) { - sheets.forEach(sheet => { - sheet.texture = disposeTexture(gl, sheet.texture); - }); + sheets.forEach(sheet => { + sheet.texture = disposeTexture(gl, sheet.texture); + }); } diff --git a/src/ts/graphics/webgl/frameBuffer.ts b/src/ts/graphics/webgl/frameBuffer.ts index 9708c25..8036659 100644 --- a/src/ts/graphics/webgl/frameBuffer.ts +++ b/src/ts/graphics/webgl/frameBuffer.ts @@ -1,48 +1,48 @@ import { Texture2D, createEmptyTexture, resizeTexture } from './texture2d'; export interface FrameBuffer { - handle: WebGLFramebuffer; - texture: Texture2D; - width: number; - height: number; + handle: WebGLFramebuffer; + texture: Texture2D; + width: number; + height: number; } export function createFrameBuffer(gl: WebGLRenderingContext, width: number, height: number): FrameBuffer { - const handle = gl.createFramebuffer(); + const handle = gl.createFramebuffer(); - if (!handle) { - throw new Error('Failed to create frame buffer'); - } + if (!handle) { + throw new Error('Failed to create frame buffer'); + } - const texture = createEmptyTexture(gl, width, height, gl.RGB); - return { handle, texture, width, height }; + const texture = createEmptyTexture(gl, width, height, gl.RGB); + return { handle, texture, width, height }; } export function disposeFrameBuffer(gl: WebGLRenderingContext | undefined, buffer: FrameBuffer | undefined) { - try { - if (gl && buffer) { - gl.deleteFramebuffer(buffer.handle); - gl.deleteTexture(buffer.texture.handle); - } - } catch (e) { - DEVELOPMENT && console.error(e); - } + try { + if (gl && buffer) { + gl.deleteFramebuffer(buffer.handle); + gl.deleteTexture(buffer.texture.handle); + } + } catch (e) { + DEVELOPMENT && console.error(e); + } - return undefined; + return undefined; } export function resizeFrameBuffer(gl: WebGLRenderingContext, frameBuffer: FrameBuffer, width: number, height: number) { - resizeTexture(gl, frameBuffer.texture, width, height); - frameBuffer.width = width; - frameBuffer.height = height; + resizeTexture(gl, frameBuffer.texture, width, height); + frameBuffer.width = width; + frameBuffer.height = height; } export function bindFrameBuffer(gl: WebGLRenderingContext, { handle, texture }: FrameBuffer) { - gl.bindFramebuffer(gl.FRAMEBUFFER, handle); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture.handle, 0); + gl.bindFramebuffer(gl.FRAMEBUFFER, handle); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture.handle, 0); } export function unbindFrameBuffer(gl: WebGLRenderingContext) { - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0); - gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); } diff --git a/src/ts/graphics/webgl/glFbo.ts b/src/ts/graphics/webgl/glFbo.ts index c8a4bfe..7f4b74c 100644 --- a/src/ts/graphics/webgl/glFbo.ts +++ b/src/ts/graphics/webgl/glFbo.ts @@ -2,16 +2,16 @@ import { Texture2D, resizeTexture, disposeTexture, createEmptyTexture } from './ import { array } from '../../common/utils'; export interface FBOOptions { - /* Upgrade to floating point if available, otherwise fallback to 8bit. (default false) */ - preferFloat?: boolean; - /* Use floating point textures (default false) */ - float?: boolean; - /* The number of color buffers to create (default 1) */ - color?: number; - /* If fbo has a depth buffer (default: true) */ - depth?: boolean; - /* If fbo has a stencil buffer (default: false) */ - stencil?: boolean; + /* Upgrade to floating point if available, otherwise fallback to 8bit. (default false) */ + preferFloat?: boolean; + /* Use floating point textures (default false) */ + float?: boolean; + /* The number of color buffers to create (default 1) */ + color?: number; + /* If fbo has a depth buffer (default: true) */ + depth?: boolean; + /* If fbo has a stencil buffer (default: false) */ + stencil?: boolean; } type WebGL = WebGLRenderingContext; @@ -20,289 +20,289 @@ type FBOState = [WebGLFramebuffer | null, WebGLRenderbuffer | null, WebGLTexture let colorAttachmentArrays: number[][] | null = null; export interface FrameBuffer { - color: (Texture2D | undefined)[]; - depth: Texture2D | undefined; - handle: WebGLFramebuffer | null; - colorRenderBuffer: WebGLRenderbuffer | null; - depthRenderBuffer: WebGLRenderbuffer | null; - width: number; - height: number; - gl: WebGLRenderingContext; - colorType: number; - useDepth: boolean; - useStencil: boolean; - ext: WEBGL_draw_buffers | null; + color: (Texture2D | undefined)[]; + depth: Texture2D | undefined; + handle: WebGLFramebuffer | null; + colorRenderBuffer: WebGLRenderbuffer | null; + depthRenderBuffer: WebGLRenderbuffer | null; + width: number; + height: number; + gl: WebGLRenderingContext; + colorType: number; + useDepth: boolean; + useStencil: boolean; + ext: WEBGL_draw_buffers | null; } export function createFrameBuffer(gl: WebGL, width: number, height: number, options: FBOOptions = {}): FrameBuffer { - const { color = 1, depth = true, stencil = false, float = false, preferFloat = false } = options; - const ext = gl.getExtension('WEBGL_draw_buffers'); + const { color = 1, depth = true, stencil = false, float = false, preferFloat = false } = options; + const ext = gl.getExtension('WEBGL_draw_buffers'); - width = width | 0; - height = height | 0; + width = width | 0; + height = height | 0; - if (!colorAttachmentArrays && ext) { - lazyInitColorAttachments(gl, ext); - } + if (!colorAttachmentArrays && ext) { + lazyInitColorAttachments(gl, ext); + } - const maxFBOSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); + const maxFBOSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); - if (maxFBOSize != null && (width < 0 || width > maxFBOSize || height < 0 || height > maxFBOSize)) { - throw new Error('Parameters are too large for FBO'); - } + if (maxFBOSize != null && (width < 0 || width > maxFBOSize || height < 0 || height > maxFBOSize)) { + throw new Error('Parameters are too large for FBO'); + } - const numColors = Math.max(color, 0); + const numColors = Math.max(color, 0); - if (numColors < 0) { - throw new Error('Must specify a nonnegative number of colors'); - } else if (numColors > 1) { - if (!ext) { - throw new Error('Multiple draw buffer extension not supported'); - } else if (numColors > gl.getParameter(ext.MAX_COLOR_ATTACHMENTS_WEBGL)) { - throw new Error(`Context does not support ${numColors} draw buffers`); - } - } + if (numColors < 0) { + throw new Error('Must specify a nonnegative number of colors'); + } else if (numColors > 1) { + if (!ext) { + throw new Error('Multiple draw buffer extension not supported'); + } else if (numColors > gl.getParameter(ext.MAX_COLOR_ATTACHMENTS_WEBGL)) { + throw new Error(`Context does not support ${numColors} draw buffers`); + } + } - let colorType = gl.UNSIGNED_BYTE; - const OES_texture_float = gl.getExtension('OES_texture_float'); + let colorType = gl.UNSIGNED_BYTE; + const OES_texture_float = gl.getExtension('OES_texture_float'); - if (float && numColors > 0) { - if (!OES_texture_float) { - throw new Error('Context does not support floating point textures'); - } + if (float && numColors > 0) { + if (!OES_texture_float) { + throw new Error('Context does not support floating point textures'); + } - colorType = gl.FLOAT; - } else if (preferFloat && numColors > 0) { - if (OES_texture_float) { - colorType = gl.FLOAT; - } - } + colorType = gl.FLOAT; + } else if (preferFloat && numColors > 0) { + if (OES_texture_float) { + colorType = gl.FLOAT; + } + } - const fbo: FrameBuffer = { - gl, width, height, colorType, color: array(numColors, undefined), useDepth: depth, useStencil: stencil, ext, - colorRenderBuffer: null, depth: undefined, depthRenderBuffer: null, handle: null, - }; + const fbo: FrameBuffer = { + gl, width, height, colorType, color: array(numColors, undefined), useDepth: depth, useStencil: stencil, ext, + colorRenderBuffer: null, depth: undefined, depthRenderBuffer: null, handle: null, + }; - rebuild(fbo); - return fbo; + rebuild(fbo); + return fbo; } export function resizeFrameBuffer(fbo: FrameBuffer, w: number, h: number) { - if (fbo.width === w && fbo.height === h) { - return; - } + if (fbo.width === w && fbo.height === h) { + return; + } - const gl = fbo.gl; - const maxFBOSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); + const gl = fbo.gl; + const maxFBOSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); - if (maxFBOSize != null && (w < 0 || w > maxFBOSize || h < 0 || h > maxFBOSize)) { - throw new Error(`Can't resize FBO, invalid dimensions`); - } + if (maxFBOSize != null && (w < 0 || w > maxFBOSize || h < 0 || h > maxFBOSize)) { + throw new Error(`Can't resize FBO, invalid dimensions`); + } - fbo.width = w; - fbo.height = h; + fbo.width = w; + fbo.height = h; - const state = saveFBOState(gl); + const state = saveFBOState(gl); - for (const color of fbo.color) { - if (color) { - resizeTexture(gl, color, w, h); - } - } + for (const color of fbo.color) { + if (color) { + resizeTexture(gl, color, w, h); + } + } - if (fbo.colorRenderBuffer) { - gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.colorRenderBuffer); - gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA4, w, h); - } + if (fbo.colorRenderBuffer) { + gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.colorRenderBuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA4, w, h); + } - if (fbo.depth) { - resizeTexture(gl, fbo.depth, w, h); - } + if (fbo.depth) { + resizeTexture(gl, fbo.depth, w, h); + } - if (fbo.depthRenderBuffer) { - gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.depthRenderBuffer); + if (fbo.depthRenderBuffer) { + gl.bindRenderbuffer(gl.RENDERBUFFER, fbo.depthRenderBuffer); - if (fbo.useDepth && fbo.useStencil) { - gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, w, h); - } else if (fbo.useDepth) { - gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h); - } else if (fbo.useStencil) { - gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, w, h); - } - } + if (fbo.useDepth && fbo.useStencil) { + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, w, h); + } else if (fbo.useDepth) { + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h); + } else if (fbo.useStencil) { + gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, w, h); + } + } - gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.handle); - const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo.handle); + const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); - if (status !== gl.FRAMEBUFFER_COMPLETE) { - disposeFrameBuffer(fbo); - restoreFBOState(gl, state); - throwFBOError(gl, status, `reshape(${w}, ${h})`); - } + if (status !== gl.FRAMEBUFFER_COMPLETE) { + disposeFrameBuffer(fbo); + restoreFBOState(gl, state); + throwFBOError(gl, status, `reshape(${w}, ${h})`); + } - restoreFBOState(gl, state); + restoreFBOState(gl, state); } export function bindFrameBuffer(fbo: FrameBuffer) { - fbo.gl.bindFramebuffer(fbo.gl.FRAMEBUFFER, fbo.handle); - fbo.gl.viewport(0, 0, fbo.width, fbo.height); + fbo.gl.bindFramebuffer(fbo.gl.FRAMEBUFFER, fbo.handle); + fbo.gl.viewport(0, 0, fbo.width, fbo.height); } export function disposeFrameBuffer(fbo: FrameBuffer | undefined): undefined { - if (fbo) { - fbo.gl.deleteFramebuffer(fbo.handle); - fbo.handle = null; - fbo.depth = disposeTexture(fbo.gl, fbo.depth); + if (fbo) { + fbo.gl.deleteFramebuffer(fbo.handle); + fbo.handle = null; + fbo.depth = disposeTexture(fbo.gl, fbo.depth); - if (fbo.depthRenderBuffer) { - fbo.gl.deleteRenderbuffer(fbo.depthRenderBuffer); - fbo.depthRenderBuffer = null; - } + if (fbo.depthRenderBuffer) { + fbo.gl.deleteRenderbuffer(fbo.depthRenderBuffer); + fbo.depthRenderBuffer = null; + } - for (let i = 0; i < fbo.color.length; i++) { - fbo.color[i] = disposeTexture(fbo.gl, fbo.color[i]); - } + for (let i = 0; i < fbo.color.length; i++) { + fbo.color[i] = disposeTexture(fbo.gl, fbo.color[i]); + } - if (fbo.colorRenderBuffer) { - fbo.gl.deleteRenderbuffer(fbo.colorRenderBuffer); - fbo.colorRenderBuffer = null; - } - } + if (fbo.colorRenderBuffer) { + fbo.gl.deleteRenderbuffer(fbo.colorRenderBuffer); + fbo.colorRenderBuffer = null; + } + } - return undefined; + return undefined; } function rebuild(fbo: FrameBuffer) { - const state = saveFBOState(fbo.gl); - const gl = fbo.gl; - const handle = fbo.handle = gl.createFramebuffer(); - const numColors = fbo.color.length; - const { width, height, ext, useStencil, useDepth, colorType } = fbo; + const state = saveFBOState(fbo.gl); + const gl = fbo.gl; + const handle = fbo.handle = gl.createFramebuffer(); + const numColors = fbo.color.length; + const { width, height, ext, useStencil, useDepth, colorType } = fbo; - gl.bindFramebuffer(gl.FRAMEBUFFER, handle); + gl.bindFramebuffer(gl.FRAMEBUFFER, handle); - for (let i = 0; i < numColors; ++i) { - fbo.color[i] = initTexture(gl, width, height, colorType, gl.RGBA, gl.COLOR_ATTACHMENT0 + i); - } + for (let i = 0; i < numColors; ++i) { + fbo.color[i] = initTexture(gl, width, height, colorType, gl.RGBA, gl.COLOR_ATTACHMENT0 + i); + } - if (numColors === 0) { - fbo.colorRenderBuffer = initRenderBuffer(gl, width, height, gl.RGBA4, gl.COLOR_ATTACHMENT0); + if (numColors === 0) { + fbo.colorRenderBuffer = initRenderBuffer(gl, width, height, gl.RGBA4, gl.COLOR_ATTACHMENT0); - if (ext) { - ext.drawBuffersWEBGL(colorAttachmentArrays![0]); - } - } else if (numColors > 1) { - if (ext) { - ext.drawBuffersWEBGL(colorAttachmentArrays![numColors]); - } - } + if (ext) { + ext.drawBuffersWEBGL(colorAttachmentArrays![0]); + } + } else if (numColors > 1) { + if (ext) { + ext.drawBuffersWEBGL(colorAttachmentArrays![numColors]); + } + } - const WEBGL_depth_texture = gl.getExtension('WEBGL_depth_texture'); + const WEBGL_depth_texture = gl.getExtension('WEBGL_depth_texture'); - if (WEBGL_depth_texture) { - if (useStencil) { - fbo.depth = initTexture( - gl, width, height, WEBGL_depth_texture.UNSIGNED_INT_24_8_WEBGL, gl.DEPTH_STENCIL, gl.DEPTH_STENCIL_ATTACHMENT); - } else if (useDepth) { - fbo.depth = initTexture(gl, width, height, gl.UNSIGNED_SHORT, gl.DEPTH_COMPONENT, gl.DEPTH_ATTACHMENT); - } - } else { - if (useDepth && useStencil) { - fbo.depthRenderBuffer = initRenderBuffer(gl, width, height, gl.DEPTH_STENCIL, gl.DEPTH_STENCIL_ATTACHMENT); - } else if (useDepth) { - fbo.depthRenderBuffer = initRenderBuffer(gl, width, height, gl.DEPTH_COMPONENT16, gl.DEPTH_ATTACHMENT); - } else if (useStencil) { - fbo.depthRenderBuffer = initRenderBuffer(gl, width, height, gl.STENCIL_INDEX8, gl.STENCIL_ATTACHMENT); - } - } + if (WEBGL_depth_texture) { + if (useStencil) { + fbo.depth = initTexture( + gl, width, height, WEBGL_depth_texture.UNSIGNED_INT_24_8_WEBGL, gl.DEPTH_STENCIL, gl.DEPTH_STENCIL_ATTACHMENT); + } else if (useDepth) { + fbo.depth = initTexture(gl, width, height, gl.UNSIGNED_SHORT, gl.DEPTH_COMPONENT, gl.DEPTH_ATTACHMENT); + } + } else { + if (useDepth && useStencil) { + fbo.depthRenderBuffer = initRenderBuffer(gl, width, height, gl.DEPTH_STENCIL, gl.DEPTH_STENCIL_ATTACHMENT); + } else if (useDepth) { + fbo.depthRenderBuffer = initRenderBuffer(gl, width, height, gl.DEPTH_COMPONENT16, gl.DEPTH_ATTACHMENT); + } else if (useStencil) { + fbo.depthRenderBuffer = initRenderBuffer(gl, width, height, gl.STENCIL_INDEX8, gl.STENCIL_ATTACHMENT); + } + } - const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); - if (status !== gl.FRAMEBUFFER_COMPLETE) { - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - gl.deleteFramebuffer(fbo.handle); - fbo.handle = null; - fbo.depth = disposeTexture(gl, fbo.depth); + if (status !== gl.FRAMEBUFFER_COMPLETE) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.deleteFramebuffer(fbo.handle); + fbo.handle = null; + fbo.depth = disposeTexture(gl, fbo.depth); - if (fbo.depthRenderBuffer) { - gl.deleteRenderbuffer(fbo.depthRenderBuffer); - fbo.depthRenderBuffer = null; - } + if (fbo.depthRenderBuffer) { + gl.deleteRenderbuffer(fbo.depthRenderBuffer); + fbo.depthRenderBuffer = null; + } - for (let i = 0; i < fbo.color.length; i++) { - fbo.color[i] = disposeTexture(gl, fbo.color[i]); - } + for (let i = 0; i < fbo.color.length; i++) { + fbo.color[i] = disposeTexture(gl, fbo.color[i]); + } - if (fbo.colorRenderBuffer) { - gl.deleteRenderbuffer(fbo.colorRenderBuffer); - fbo.colorRenderBuffer = null; - } + if (fbo.colorRenderBuffer) { + gl.deleteRenderbuffer(fbo.colorRenderBuffer); + fbo.colorRenderBuffer = null; + } - restoreFBOState(gl, state); - throwFBOError(gl, status); - } + restoreFBOState(gl, state); + throwFBOError(gl, status); + } - restoreFBOState(gl, state); + restoreFBOState(gl, state); } function saveFBOState(gl: WebGL): FBOState { - const fbo = gl.getParameter(gl.FRAMEBUFFER_BINDING); - const rbo = gl.getParameter(gl.RENDERBUFFER_BINDING); - const tex = gl.getParameter(gl.TEXTURE_BINDING_2D); - return [fbo, rbo, tex]; + const fbo = gl.getParameter(gl.FRAMEBUFFER_BINDING); + const rbo = gl.getParameter(gl.RENDERBUFFER_BINDING); + const tex = gl.getParameter(gl.TEXTURE_BINDING_2D); + return [fbo, rbo, tex]; } function restoreFBOState(gl: WebGL, [fbo, rbo, tex]: FBOState) { - gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); - gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); - gl.bindTexture(gl.TEXTURE_2D, tex); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.bindTexture(gl.TEXTURE_2D, tex); } function lazyInitColorAttachments(gl: WebGL, ext: WEBGL_draw_buffers) { - const maxColorAttachments = gl.getParameter(ext.MAX_COLOR_ATTACHMENTS_WEBGL); - colorAttachmentArrays = []; + const maxColorAttachments = gl.getParameter(ext.MAX_COLOR_ATTACHMENTS_WEBGL); + colorAttachmentArrays = []; - for (let i = 0; i <= maxColorAttachments; ++i) { - const x: number[] = []; + for (let i = 0; i <= maxColorAttachments; ++i) { + const x: number[] = []; - for (let j = 0; j < i; ++j) { - x.push(gl.COLOR_ATTACHMENT0 + j); - } + for (let j = 0; j < i; ++j) { + x.push(gl.COLOR_ATTACHMENT0 + j); + } - for (let j = i; j < maxColorAttachments; ++j) { - x.push(gl.NONE); - } + for (let j = i; j < maxColorAttachments; ++j) { + x.push(gl.NONE); + } - colorAttachmentArrays.push(x); - } + colorAttachmentArrays.push(x); + } } function throwFBOError(gl: WebGL, status: number, message = '') { - switch (status) { - case gl.FRAMEBUFFER_UNSUPPORTED: - throw new Error('Framebuffer unsupported'); - case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: - throw new Error('Framebuffer incomplete attachment'); - case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: - throw new Error('Framebuffer incomplete dimensions'); - case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: - throw new Error('Framebuffer incomplete missing attachment'); - default: - throw new Error(`Framebuffer failed for unspecified reason [${status}] (${message})`); - } + switch (status) { + case gl.FRAMEBUFFER_UNSUPPORTED: + throw new Error('Framebuffer unsupported'); + case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + throw new Error('Framebuffer incomplete attachment'); + case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: + throw new Error('Framebuffer incomplete dimensions'); + case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + throw new Error('Framebuffer incomplete missing attachment'); + default: + throw new Error(`Framebuffer failed for unspecified reason [${status}] (${message})`); + } } function initTexture(gl: WebGL, width: number, height: number, type: number, format: number, attachment: number) { - const texture = createEmptyTexture(gl, width, height, format, type); - gl.bindTexture(gl.TEXTURE_2D, texture.handle); - gl.framebufferTexture2D(gl.FRAMEBUFFER, attachment, gl.TEXTURE_2D, texture.handle, 0); - return texture; + const texture = createEmptyTexture(gl, width, height, format, type); + gl.bindTexture(gl.TEXTURE_2D, texture.handle); + gl.framebufferTexture2D(gl.FRAMEBUFFER, attachment, gl.TEXTURE_2D, texture.handle, 0); + return texture; } function initRenderBuffer(gl: WebGL, width: number, height: number, component: number, attachment: number) { - const result = gl.createRenderbuffer(); - gl.bindRenderbuffer(gl.RENDERBUFFER, result); - gl.renderbufferStorage(gl.RENDERBUFFER, component, width, height); - gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, result); - return result; + const result = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, result); + gl.renderbufferStorage(gl.RENDERBUFFER, component, width, height); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachment, gl.RENDERBUFFER, result); + return result; } diff --git a/src/ts/graphics/webgl/glVao.ts b/src/ts/graphics/webgl/glVao.ts index 0a92240..1c77948 100644 --- a/src/ts/graphics/webgl/glVao.ts +++ b/src/ts/graphics/webgl/glVao.ts @@ -1,155 +1,155 @@ import { timeStart, timeEnd } from '../../client/timing'; export interface VAOAttributes { - name: string; - buffer: WebGLBuffer; - size?: number; - type?: number; - normalized?: boolean; - stride?: number; - offset?: number; - divisor?: number; + name: string; + buffer: WebGLBuffer; + size?: number; + type?: number; + normalized?: boolean; + stride?: number; + offset?: number; + divisor?: number; } export interface VAO { - gl: WebGLRenderingContext; - bind(): void; - unbind(): void; - dispose(): void; - update(attributes: VAOAttributes[], elements: WebGLBuffer, elementsType?: number): void; - draw(mode: number, count: number, offset?: number): void; + gl: WebGLRenderingContext; + bind(): void; + unbind(): void; + dispose(): void; + update(attributes: VAOAttributes[], elements: WebGLBuffer, elementsType?: number): void; + draw(mode: number, count: number, offset?: number): void; } interface WebGL extends WebGLRenderingContext { - bindVertexArray?: any; - createVertexArray?: any; - deleteVertexArray?: any; + bindVertexArray?: any; + createVertexArray?: any; + deleteVertexArray?: any; } function extensionShim(gl: WebGL): OES_vertex_array_object { - return { - bindVertexArrayOES: gl.bindVertexArray.bind(gl), - createVertexArrayOES: gl.createVertexArray.bind(gl), - deleteVertexArrayOES: gl.deleteVertexArray.bind(gl), - } as any; + return { + bindVertexArrayOES: gl.bindVertexArray.bind(gl), + createVertexArrayOES: gl.createVertexArray.bind(gl), + deleteVertexArrayOES: gl.deleteVertexArray.bind(gl), + } as any; } export function createVAO(gl: WebGL, attributes: VAOAttributes[], elements: WebGLBuffer, elementsType?: number): VAO { - const ext = gl.createVertexArray ? extensionShim(gl) : gl.getExtension('OES_vertex_array_object'); - const handle = ext && ext.createVertexArrayOES(); - const vao = (ext && handle) ? new VAONative(gl, ext, handle) : new VAOEmulated(gl); - vao.update(attributes, elements, elementsType); - return vao; + const ext = gl.createVertexArray ? extensionShim(gl) : gl.getExtension('OES_vertex_array_object'); + const handle = ext && ext.createVertexArrayOES(); + const vao = (ext && handle) ? new VAONative(gl, ext, handle) : new VAOEmulated(gl); + vao.update(attributes, elements, elementsType); + return vao; } class VAONative implements VAO { - private useElements = false; - private elementsType: number; - private maxAttribs: number; - constructor( - public gl: WebGLRenderingContext, - private ext: OES_vertex_array_object, - public handle: WebGLVertexArrayObjectOES, - ) { - this.elementsType = gl.UNSIGNED_SHORT; - this.maxAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); - } - bind() { - this.ext.bindVertexArrayOES(this.handle); - } - unbind() { - this.ext.bindVertexArrayOES(null); - } - dispose() { - this.ext.deleteVertexArrayOES(this.handle); - } - update(attributes: VAOAttributes[], elements: WebGLBuffer | null, elementsType?: number) { - this.bind(); - bindAttribs(this.gl, elements, attributes, this.maxAttribs); - this.unbind(); - this.useElements = !!elements; - this.elementsType = elementsType || this.gl.UNSIGNED_SHORT; - } - draw(mode: number, count: number, offset = 0) { - TIMING && timeStart('VAONative.draw'); - if (this.useElements) { - this.gl.drawElements(mode, count, this.elementsType, offset); - } else { - this.gl.drawArrays(mode, offset, count); - } - TIMING && timeEnd(); - } + private useElements = false; + private elementsType: number; + private maxAttribs: number; + constructor( + public gl: WebGLRenderingContext, + private ext: OES_vertex_array_object, + public handle: WebGLVertexArrayObjectOES, + ) { + this.elementsType = gl.UNSIGNED_SHORT; + this.maxAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); + } + bind() { + this.ext.bindVertexArrayOES(this.handle); + } + unbind() { + this.ext.bindVertexArrayOES(null); + } + dispose() { + this.ext.deleteVertexArrayOES(this.handle); + } + update(attributes: VAOAttributes[], elements: WebGLBuffer | null, elementsType?: number) { + this.bind(); + bindAttribs(this.gl, elements, attributes, this.maxAttribs); + this.unbind(); + this.useElements = !!elements; + this.elementsType = elementsType || this.gl.UNSIGNED_SHORT; + } + draw(mode: number, count: number, offset = 0) { + TIMING && timeStart('VAONative.draw'); + if (this.useElements) { + this.gl.drawElements(mode, count, this.elementsType, offset); + } else { + this.gl.drawArrays(mode, offset, count); + } + TIMING && timeEnd(); + } } class VAOEmulated implements VAO { - private elements: WebGLBuffer | null = null; - private attributes: VAOAttributes[] | null = null; - private elementsType: number; - private maxAttribs: number; - constructor(public gl: WebGLRenderingContext) { - this.elementsType = gl.UNSIGNED_SHORT; - this.maxAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); - } - bind() { - bindAttribs(this.gl, this.elements, this.attributes, this.maxAttribs); - } - update(attributes: VAOAttributes[], elements: WebGLBuffer | null, elementsType?: number) { - this.elements = elements; - this.attributes = attributes; - this.elementsType = elementsType || this.gl.UNSIGNED_SHORT; - } - dispose() { - } - unbind() { - } - draw(mode: number, count: number, offset = 0) { - TIMING && timeStart('VAOEmulated.draw'); - if (this.elements) { - this.gl.drawElements(mode, count, this.elementsType, offset); - } else { - this.gl.drawArrays(mode, offset, count); - } - TIMING && timeEnd(); - } + private elements: WebGLBuffer | null = null; + private attributes: VAOAttributes[] | null = null; + private elementsType: number; + private maxAttribs: number; + constructor(public gl: WebGLRenderingContext) { + this.elementsType = gl.UNSIGNED_SHORT; + this.maxAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); + } + bind() { + bindAttribs(this.gl, this.elements, this.attributes, this.maxAttribs); + } + update(attributes: VAOAttributes[], elements: WebGLBuffer | null, elementsType?: number) { + this.elements = elements; + this.attributes = attributes; + this.elementsType = elementsType || this.gl.UNSIGNED_SHORT; + } + dispose() { + } + unbind() { + } + draw(mode: number, count: number, offset = 0) { + TIMING && timeStart('VAOEmulated.draw'); + if (this.elements) { + this.gl.drawElements(mode, count, this.elementsType, offset); + } else { + this.gl.drawArrays(mode, offset, count); + } + TIMING && timeEnd(); + } } function bindAttribs( - gl: WebGLRenderingContext, elements: WebGLBuffer | null, attributes: VAOAttributes[] | null, maxAttribs: number + gl: WebGLRenderingContext, elements: WebGLBuffer | null, attributes: VAOAttributes[] | null, maxAttribs: number ) { - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elements); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elements); - if (attributes) { - if (maxAttribs != null && attributes.length > maxAttribs) { - throw new Error(`Too many vertex attributes ${attributes.length}/${maxAttribs}`); - } + if (attributes) { + if (maxAttribs != null && attributes.length > maxAttribs) { + throw new Error(`Too many vertex attributes ${attributes.length}/${maxAttribs}`); + } - let i = 0; + let i = 0; - for (; i < attributes.length; ++i) { - const attrib = attributes[i]; - const buffer = attrib.buffer; - const size = attrib.size || 4; - const type = attrib.type || gl.FLOAT; - const normalized = !!attrib.normalized; - const stride = attrib.stride || 0; - const offset = attrib.offset || 0; - gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - gl.enableVertexAttribArray(i); - gl.vertexAttribPointer(i, size, type, normalized, stride, offset); + for (; i < attributes.length; ++i) { + const attrib = attributes[i]; + const buffer = attrib.buffer; + const size = attrib.size || 4; + const type = attrib.type || gl.FLOAT; + const normalized = !!attrib.normalized; + const stride = attrib.stride || 0; + const offset = attrib.offset || 0; + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(i); + gl.vertexAttribPointer(i, size, type, normalized, stride, offset); - if (attrib.divisor !== undefined) { - (gl as any).vertexAttribDivisor(i, attrib.divisor); - } - } + if (attrib.divisor !== undefined) { + (gl as any).vertexAttribDivisor(i, attrib.divisor); + } + } - for (; i < maxAttribs; ++i) { - gl.disableVertexAttribArray(i); - } - } else { - gl.bindBuffer(gl.ARRAY_BUFFER, null); + for (; i < maxAttribs; ++i) { + gl.disableVertexAttribArray(i); + } + } else { + gl.bindBuffer(gl.ARRAY_BUFFER, null); - for (let i = 0; i < maxAttribs; ++i) { - gl.disableVertexAttribArray(i); - } - } + for (let i = 0; i < maxAttribs; ++i) { + gl.disableVertexAttribArray(i); + } + } } diff --git a/src/ts/graphics/webgl/shader.ts b/src/ts/graphics/webgl/shader.ts index 09a29fe..6ea0f99 100644 --- a/src/ts/graphics/webgl/shader.ts +++ b/src/ts/graphics/webgl/shader.ts @@ -1,100 +1,100 @@ export interface Shader { - program: WebGLProgram; - vertexShader: WebGLShader; - fragmentShader: WebGLShader; - uniforms: { [key: string]: WebGLUniformLocation; }; + program: WebGLProgram; + vertexShader: WebGLShader; + fragmentShader: WebGLShader; + uniforms: { [key: string]: WebGLUniformLocation; }; } export function createShader(gl: WebGLRenderingContext, source: string | { vertex: string; fragment: string; }): Shader { - if (typeof source === 'string') { - const index = source.indexOf('// FRAGMENT'); + if (typeof source === 'string') { + const index = source.indexOf('// FRAGMENT'); - if (index === -1) { - throw new Error(`Missing fragment shader separator`); - } + if (index === -1) { + throw new Error(`Missing fragment shader separator`); + } - source = { - vertex: source.substring(0, index), - fragment: source.substring(index), - }; - } + source = { + vertex: source.substring(0, index), + fragment: source.substring(index), + }; + } - const vertexShader = createWebGLShader(gl, gl.VERTEX_SHADER, source.vertex); - const fragmentShader = createWebGLShader(gl, gl.FRAGMENT_SHADER, source.fragment); - const program = gl.createProgram(); + const vertexShader = createWebGLShader(gl, gl.VERTEX_SHADER, source.vertex); + const fragmentShader = createWebGLShader(gl, gl.FRAGMENT_SHADER, source.fragment); + const program = gl.createProgram(); - if (!program) { - throw new Error('Failed to create shader program'); - } + if (!program) { + throw new Error('Failed to create shader program'); + } - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); - const attribs = source.vertex.match(/^attribute [a-zA-Z0-9_]+ ([a-zA-Z0-9_]+)/mg)!; + const attribs = source.vertex.match(/^attribute [a-zA-Z0-9_]+ ([a-zA-Z0-9_]+)/mg)!; - for (var i = 0; i < attribs.length; ++i) { - const [, name] = /attribute [a-zA-Z0-9_]+ ([a-zA-Z0-9_]+)/.exec(attribs[i])!; - gl.bindAttribLocation(program, i, name); - } + for (var i = 0; i < attribs.length; ++i) { + const [, name] = /attribute [a-zA-Z0-9_]+ ([a-zA-Z0-9_]+)/.exec(attribs[i])!; + gl.bindAttribLocation(program, i, name); + } - gl.linkProgram(program); + gl.linkProgram(program); - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - throw new Error('Failed to link shader program'); - } + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error('Failed to link shader program'); + } - gl.useProgram(program); + gl.useProgram(program); - const uniforms: any = {}; - const samplers: string[] = []; + const uniforms: any = {}; + const samplers: string[] = []; - for (let i = 0; i < gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); i++) { - const info = gl.getActiveUniform(program, i)!; - uniforms[info.name] = gl.getUniformLocation(program, info.name); + for (let i = 0; i < gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); i++) { + const info = gl.getActiveUniform(program, i)!; + uniforms[info.name] = gl.getUniformLocation(program, info.name); - if (!uniforms[info.name]) { - throw new Error(`Failed to get uniform location (${info.name})`); - } + if (!uniforms[info.name]) { + throw new Error(`Failed to get uniform location (${info.name})`); + } - if (info.type === gl.SAMPLER_2D) { - samplers.push(info.name); - } - } + if (info.type === gl.SAMPLER_2D) { + samplers.push(info.name); + } + } - samplers.sort().forEach((name, i) => gl.uniform1i(uniforms[name], i)); + samplers.sort().forEach((name, i) => gl.uniform1i(uniforms[name], i)); - gl.useProgram(null); + gl.useProgram(null); - return { program, vertexShader, fragmentShader, uniforms }; + return { program, vertexShader, fragmentShader, uniforms }; } export function disposeShader(gl: WebGLRenderingContext | undefined, shader: Shader | undefined) { - try { - if (gl && shader) { - gl.deleteProgram(shader.program); - gl.deleteShader(shader.vertexShader); - gl.deleteShader(shader.fragmentShader); - } - } catch (e) { - DEVELOPMENT && console.error(e); - } + try { + if (gl && shader) { + gl.deleteProgram(shader.program); + gl.deleteShader(shader.vertexShader); + gl.deleteShader(shader.fragmentShader); + } + } catch (e) { + DEVELOPMENT && console.error(e); + } - return undefined; + return undefined; } function createWebGLShader(gl: WebGLRenderingContext, type: number, source: string) { - const shader = gl.createShader(type); + const shader = gl.createShader(type); - if (!shader) { - throw new Error('Failed to create shader'); - } + if (!shader) { + throw new Error('Failed to create shader'); + } - gl.shaderSource(shader, source); - gl.compileShader(shader); + gl.shaderSource(shader, source); + gl.compileShader(shader); - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - throw new Error(gl.getShaderInfoLog(shader) || 'Shader error'); - } + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + throw new Error(gl.getShaderInfoLog(shader) || 'Shader error'); + } - return shader; + return shader; } diff --git a/src/ts/graphics/webgl/texture2d.ts b/src/ts/graphics/webgl/texture2d.ts index 681e022..f8c3c56 100644 --- a/src/ts/graphics/webgl/texture2d.ts +++ b/src/ts/graphics/webgl/texture2d.ts @@ -2,97 +2,97 @@ type WebGL = WebGLRenderingContext; type Pixels = ImageBitmap | ImageData | HTMLVideoElement | HTMLImageElement | HTMLCanvasElement; export interface Texture2D { - handle: WebGLTexture; - width: number; - height: number; - format: number; - type: number; + handle: WebGLTexture; + width: number; + height: number; + format: number; + type: number; } export function createEmptyTexture(gl: WebGL, width: number, height: number, format?: number, type?: number): Texture2D { - if (format === undefined) { - format = gl.RGBA; - } + if (format === undefined) { + format = gl.RGBA; + } - if (type === undefined) { - type = gl.UNSIGNED_BYTE; - } + if (type === undefined) { + type = gl.UNSIGNED_BYTE; + } - const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null; + const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null; - if (maxTextureSize != null && (width < 0 || width > maxTextureSize || height < 0 || height > maxTextureSize)) { - throw new Error('Invalid texture shape'); - } + if (maxTextureSize != null && (width < 0 || width > maxTextureSize || height < 0 || height > maxTextureSize)) { + throw new Error('Invalid texture shape'); + } - if (type === gl.FLOAT && !gl.getExtension('OES_texture_float')) { - throw new Error('Floating point textures not supported on this platform'); - } + if (type === gl.FLOAT && !gl.getExtension('OES_texture_float')) { + throw new Error('Floating point textures not supported on this platform'); + } - const handle = createTextureHandle(gl); - gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, type, null); - return { handle, width, height, format, type }; + const handle = createTextureHandle(gl); + gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, type, null); + return { handle, width, height, format, type }; } export function createTexture(gl: WebGL, data: Pixels, format?: number, type?: number): Texture2D { - if (format === undefined) { - format = gl.RGBA; - } + if (format === undefined) { + format = gl.RGBA; + } - if (type === undefined) { - type = gl.UNSIGNED_BYTE; - } + if (type === undefined) { + type = gl.UNSIGNED_BYTE; + } - const handle = createTextureHandle(gl); - gl.texImage2D(gl.TEXTURE_2D, 0, format, format, type, data); - return { handle, width: data.width, height: data.height, format, type }; + const handle = createTextureHandle(gl); + gl.texImage2D(gl.TEXTURE_2D, 0, format, format, type, data); + return { handle, width: data.width, height: data.height, format, type }; } export function disposeTexture(gl: WebGL | undefined, texture: Texture2D | undefined): undefined { - try { - if (gl && texture) { - gl.deleteTexture(texture.handle); - } - } catch (e) { - DEVELOPMENT && console.error(e); - } + try { + if (gl && texture) { + gl.deleteTexture(texture.handle); + } + } catch (e) { + DEVELOPMENT && console.error(e); + } - return undefined; + return undefined; } export function bindTexture(gl: WebGL, unit: number, texture: Texture2D | undefined) { - gl.activeTexture(gl.TEXTURE0 + unit); - gl.bindTexture(gl.TEXTURE_2D, texture ? texture.handle : null); + gl.activeTexture(gl.TEXTURE0 + unit); + gl.bindTexture(gl.TEXTURE_2D, texture ? texture.handle : null); } export function resizeTexture(gl: WebGL, texture: Texture2D, width: number, height: number) { - width = width | 0; - height = height | 0; + width = width | 0; + height = height | 0; - const { format, type } = texture; - const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null; + const { format, type } = texture; + const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null; - if (maxSize != null && (width < 0 || width > maxSize || height < 0 || height > maxSize)) { - throw new Error('Invalid texture size'); - } + if (maxSize != null && (width < 0 || width > maxSize || height < 0 || height > maxSize)) { + throw new Error('Invalid texture size'); + } - texture.width = width; - texture.height = height; - gl.bindTexture(gl.TEXTURE_2D, texture.handle); - gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, type, null); + texture.width = width; + texture.height = height; + gl.bindTexture(gl.TEXTURE_2D, texture.handle); + gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, type, null); } function createTextureHandle(gl: WebGL) { - const texture = gl.createTexture(); + const texture = gl.createTexture(); - if (!texture) { - throw new Error('Failed to create texture'); - } + if (!texture) { + throw new Error('Failed to create texture'); + } - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - return texture; + return texture; } diff --git a/src/ts/graphics/webgl/vaoAttributes.ts b/src/ts/graphics/webgl/vaoAttributes.ts index c5d0feb..bb55cf4 100644 --- a/src/ts/graphics/webgl/vaoAttributes.ts +++ b/src/ts/graphics/webgl/vaoAttributes.ts @@ -1,44 +1,44 @@ import { VAOAttributes } from './glVao'; export interface VAOAttributeDefinition { - name: string; - size: number; - type?: number; - normalized?: boolean; - divisor?: number; + name: string; + size: number; + type?: number; + normalized?: boolean; + divisor?: number; } export function getVAOAttributesSize(gl: WebGLRenderingContext, attributes: VAOAttributeDefinition[]) { - return attributes.reduce((sum, a) => sum + a.size * sizeOfType(gl, a.type), 0); + return attributes.reduce((sum, a) => sum + a.size * sizeOfType(gl, a.type), 0); } export function createVAOAttributes( - gl: WebGLRenderingContext, attributes: VAOAttributeDefinition[], buffer: WebGLBuffer + gl: WebGLRenderingContext, attributes: VAOAttributeDefinition[], buffer: WebGLBuffer ): VAOAttributes[] { - const result: VAOAttributes[] = []; - const stride = getVAOAttributesSize(gl, attributes); - let offset = 0; + const result: VAOAttributes[] = []; + const stride = getVAOAttributesSize(gl, attributes); + let offset = 0; - for (const a of attributes) { - result.push({ ...a, stride, buffer, offset }); - offset += a.size * sizeOfType(gl, a.type); - } + for (const a of attributes) { + result.push({ ...a, stride, buffer, offset }); + offset += a.size * sizeOfType(gl, a.type); + } - return result; + return result; } function sizeOfType(gl: WebGLRenderingContext, type: number | undefined) { - switch (type) { - case gl.BYTE: - case gl.UNSIGNED_BYTE: - return 1; - case gl.SHORT: - case gl.UNSIGNED_SHORT: - return 2; - case gl.FLOAT: - case undefined: - return 4; - default: - throw new Error(`Invalid attribute type (${type})`); - } + switch (type) { + case gl.BYTE: + case gl.UNSIGNED_BYTE: + return 1; + case gl.SHORT: + case gl.UNSIGNED_SHORT: + return 2; + case gl.FLOAT: + case undefined: + return 4; + default: + throw new Error(`Invalid attribute type (${type})`); + } } diff --git a/src/ts/graphics/webgl/webglUtils.ts b/src/ts/graphics/webgl/webglUtils.ts index 6dbd7a7..3c14313 100644 --- a/src/ts/graphics/webgl/webglUtils.ts +++ b/src/ts/graphics/webgl/webglUtils.ts @@ -1,67 +1,67 @@ import { WEBGL_CREATION_ERROR } from '../../common/errors'; export function getRenderTargetSize(width: number, height: number) { - const max = Math.max(width, height); - let pow = 256; + const max = Math.max(width, height); + let pow = 256; - while (pow < max) { - pow *= 2; - } + while (pow < max) { + pow *= 2; + } - return pow; + return pow; } export function getWebGLContext(canvas: HTMLCanvasElement): WebGLRenderingContext { - const options: WebGLContextAttributes = { - alpha: false, - premultipliedAlpha: false, - antialias: false, - }; + const options: WebGLContextAttributes = { + alpha: false, + premultipliedAlpha: false, + antialias: false, + }; - const gl = canvas.getContext('webgl2', options) - || canvas.getContext('webgl', options) - || canvas.getContext('experimental-webgl', options); + const gl = canvas.getContext('webgl2', options) + || canvas.getContext('webgl', options) + || canvas.getContext('experimental-webgl', options); - if (!gl) { - throw new Error(WEBGL_CREATION_ERROR); - } + if (!gl) { + throw new Error(WEBGL_CREATION_ERROR); + } - return gl; + return gl; } export function isWebGL2(gl: WebGLRenderingContext | undefined) { - return !!(gl && gl.MAX_ELEMENT_INDEX); + return !!(gl && gl.MAX_ELEMENT_INDEX); } export function getWebGLError(gl: WebGLRenderingContext) { - const error = gl.getError(); + const error = gl.getError(); - switch (error) { - case gl.NO_ERROR: return 'NO_ERROR'; - case gl.INVALID_ENUM: return 'INVALID_ENUM'; - case gl.INVALID_VALUE: return 'INVALID_VALUE'; - case gl.INVALID_OPERATION: return 'INVALID_OPERATION'; - case gl.INVALID_FRAMEBUFFER_OPERATION: return 'INVALID_FRAMEBUFFER_OPERATION'; - case gl.OUT_OF_MEMORY: return 'OUT_OF_MEMORY'; - case gl.CONTEXT_LOST_WEBGL: return 'CONTEXT_LOST_WEBGL'; - default: return `${error}`; - } + switch (error) { + case gl.NO_ERROR: return 'NO_ERROR'; + case gl.INVALID_ENUM: return 'INVALID_ENUM'; + case gl.INVALID_VALUE: return 'INVALID_VALUE'; + case gl.INVALID_OPERATION: return 'INVALID_OPERATION'; + case gl.INVALID_FRAMEBUFFER_OPERATION: return 'INVALID_FRAMEBUFFER_OPERATION'; + case gl.OUT_OF_MEMORY: return 'OUT_OF_MEMORY'; + case gl.CONTEXT_LOST_WEBGL: return 'CONTEXT_LOST_WEBGL'; + default: return `${error}`; + } } export function unbindAllTexturesAndBuffers(gl: WebGLRenderingContext) { - try { - const numTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) | 0; + try { + const numTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) | 0; - for (let i = 0; i < numTextureUnits; i++) { - gl.activeTexture(gl.TEXTURE0 + i); - gl.bindTexture(gl.TEXTURE_2D, null); - } + for (let i = 0; i < numTextureUnits; i++) { + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, null); + } - gl.bindBuffer(gl.ARRAY_BUFFER, null); - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); - gl.bindRenderbuffer(gl.RENDERBUFFER, null); - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - } catch (e) { - DEVELOPMENT && console.error(e); - } + gl.bindBuffer(gl.ARRAY_BUFFER, null); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + } catch (e) { + DEVELOPMENT && console.error(e); + } } diff --git a/src/ts/lodash.ts b/src/ts/lodash.ts index 1efe199..43e207d 100644 --- a/src/ts/lodash.ts +++ b/src/ts/lodash.ts @@ -1,12 +1,12 @@ type List = ArrayLike; type PartialDeep = { - [P in keyof T]?: PartialDeep; + [P in keyof T]?: PartialDeep; }; interface Dictionary { - [index: string]: T; + [index: string]: T; } interface NumericDictionary { - [index: number]: T; + [index: number]: T; } type NotVoid = {} | null | undefined; type ListIteratee = ListIterator | string | [string, any] | PartialDeep; @@ -16,33 +16,33 @@ type ObjectIterator = (value: TObject[keyof TObject], key: str type Many = T | T[]; interface FlatMap { - ( - collection: List> | Dictionary> | NumericDictionary> | null | undefined - ): T[]; + ( + collection: List> | Dictionary> | NumericDictionary> | null | undefined + ): T[]; - ( - collection: object | null | undefined - ): any[]; + ( + collection: object | null | undefined + ): any[]; - ( - collection: List | null | undefined, - iteratee: ListIterator> - ): TResult[]; + ( + collection: List | null | undefined, + iteratee: ListIterator> + ): TResult[]; - ( - collection: T | null | undefined, - iteratee: ObjectIterator> - ): TResult[]; + ( + collection: T | null | undefined, + iteratee: ObjectIterator> + ): TResult[]; - ( - collection: object | null | undefined, - iteratee: string - ): any[]; + ( + collection: object | null | undefined, + iteratee: string + ): any[]; - ( - collection: object | null | undefined, - iteratee: object - ): boolean[]; + ( + collection: object | null | undefined, + iteratee: object + ): boolean[]; } type PropertyName = string | number | symbol; @@ -55,8 +55,8 @@ export const escapeRegExp: (string?: string) => string = require('lodash/escapeR export const escape: (string?: string) => string = require('lodash/escape'); export const sample = require('lodash/sample'); export const findLastIndex: - (array: List | null | undefined, predicate?: ListIterateeCustom, fromIndex?: number) => number = - require('lodash/findLastIndex'); + (array: List | null | undefined, predicate?: ListIterateeCustom, fromIndex?: number) => number = + require('lodash/findLastIndex'); export const times: (n: number, iteratee: (num: number) => TResult) => TResult[] = require('lodash/times'); export const isNumber: (value?: any) => value is number = require('lodash/isNumber'); export const isString: (value?: any) => value is string = require('lodash/isString'); @@ -65,7 +65,7 @@ export const isFunction: (value?: any) => value is (...args: any[]) => any = req export const clamp: (number: number, lower: number, upper: number) => number = require('lodash/clamp'); export const merge: (object: TObject, source: TSource) => TObject & TSource = require('lodash/merge'); export const mapValues: (obj: T | null | undefined, callback: ObjectIterator) => - { [P in keyof T]: TResult } = require('lodash/mapValues'); + { [P in keyof T]: TResult } = require('lodash/mapValues'); export const zip: (...arrays: (List | null | undefined)[]) => (T | undefined)[][] = require('lodash/zip'); export const assignWith = require('lodash/assignWith'); export const isMatchWith = require('lodash/isMatchWith'); @@ -88,10 +88,10 @@ export const uniq: (array: List | null | undefined) => T[] = require('loda export const flatMap: FlatMap = require('lodash/flatMap'); export const without: (array: List | null | undefined, ...values: T[]) => T[] = require('lodash/without'); export const compact: (array: List | null | undefined) => T[] = - require('lodash/compact'); + require('lodash/compact'); export const isEqual = require('lodash/isEqual'); export const dropRightWhile: (array: List | null | undefined, predicate?: ListIteratee) => T[] = - require('lodash/dropRightWhile'); + require('lodash/dropRightWhile'); export const fromPairs = require('lodash/fromPairs'); export const camelCase = require('lodash/camelCase'); export const truncate = require('lodash/truncate'); @@ -99,4 +99,4 @@ export const findIndex = require('lodash/findIndex'); export const last = require('lodash/last'); export const toPairs: (object?: Dictionary | NumericDictionary) => [string, T][] = require('lodash/toPairs'); export const groupBy: (collection: List | null | undefined, iteratee?: ValueIteratee) => Dictionary = - require('lodash/groupBy'); + require('lodash/groupBy'); diff --git a/src/ts/server/accountUtils.ts b/src/ts/server/accountUtils.ts index fbf83e1..75e4303 100644 --- a/src/ts/server/accountUtils.ts +++ b/src/ts/server/accountUtils.ts @@ -6,12 +6,12 @@ import { Profile, ModInfo, AccountDataFlags } from '../common/interfaces'; import { ACCOUNT_NAME_MAX_LENGTH, DAY } from '../common/constants'; import { fromNow, includes, hasFlag } from '../common/utils'; import { - isAdmin, getCharacterLimit as getCharacterLimitInternal, getSupporterInviteLimit as getSupporterInviteLimitInternal + isAdmin, getCharacterLimit as getCharacterLimitInternal, getSupporterInviteLimit as getSupporterInviteLimitInternal } from '../common/accountUtils'; import { cleanName } from '../client/clientUtils'; import { - IAccount, IAuth, Account, ID, characterCount as getCharacterCount, findAccount, queryAccount, updateAccount, - FriendRequest, IFriendRequest + IAccount, IAuth, Account, ID, characterCount as getCharacterCount, findAccount, queryAccount, updateAccount, + FriendRequest, IFriendRequest } from './db'; import { assignAuth } from './authUtils'; import { UserError } from './userError'; @@ -22,228 +22,228 @@ import { providers } from './oauth'; import { taskQueue } from './utils/taskQueue'; export interface SuspiciousCheckers { - isSuspiciousName(name: string): boolean; - isSuspiciousAuth(auth: AuthBase): boolean; + isSuspiciousName(name: string): boolean; + isSuspiciousAuth(auth: AuthBase): boolean; } export interface CreateAccountOptions extends SuspiciousCheckers { - userAgent: string | undefined; - browserId: string | undefined; - connectOnly: boolean; - creationLocked: boolean; - canCreateAccounts: boolean; - reportPotentialDuplicates: boolean; - ip: string; - warn: (accountId: string | Types.ObjectId, message: string, desc?: string) => void; + userAgent: string | undefined; + browserId: string | undefined; + connectOnly: boolean; + creationLocked: boolean; + canCreateAccounts: boolean; + reportPotentialDuplicates: boolean; + ip: string; + warn: (accountId: string | Types.ObjectId, message: string, desc?: string) => void; } function getBanInfo(value: number | undefined): string | undefined { - return isActive(value) ? (value === -1 ? 'perma' : moment(value).fromNow(true)) : undefined; + return isActive(value) ? (value === -1 ? 'perma' : moment(value).fromNow(true)) : undefined; } export function getModInfo({ accountId, account, country }: IClient): ModInfo { - return { - shadow: getBanInfo(account.shadow), - mute: getBanInfo(account.mute), - note: account.note, - counters: account.counters || {}, - country, - account: `${account.name} [${accountId.substr(-3)}]`, - }; + return { + shadow: getBanInfo(account.shadow), + mute: getBanInfo(account.mute), + note: account.note, + counters: account.counters || {}, + country, + account: `${account.name} [${accountId.substr(-3)}]`, + }; } function findAccountByEmail(emails?: string[]) { - return emails && emails.length ? queryAccount({ emails: { $in: emails } }) : Promise.resolve(undefined); + return emails && emails.length ? queryAccount({ emails: { $in: emails } }) : Promise.resolve(undefined); } const availableProviders = providers.filter(a => !a.connectOnly).map(a => a.name).join(', '); export const connectOnlySocialError = - `Cannot create new account using this social site, new accounts can only be created using: ${availableProviders}`; + `Cannot create new account using this social site, new accounts can only be created using: ${availableProviders}`; function createNewAccount(profile: Profile, options: CreateAccountOptions) { - if (!options.canCreateAccounts) { - throw new UserError( - 'Creating accounts is temporarily disabled, try again later'); - } else if (options.connectOnly) { - throw new UserError(connectOnlySocialError); - } else if (options.creationLocked) { - throw new UserError( - 'Could not create account, try again later', { log: `account creation blocked by ACL (${options.ip})` }); - } else if (profile.suspended) { - throw new UserError( - 'Cannot create new account using suspended social site account', { log: 'account creation blocked by suspended' }); - } else { - return new Account(); - } + if (!options.canCreateAccounts) { + throw new UserError( + 'Creating accounts is temporarily disabled, try again later'); + } else if (options.connectOnly) { + throw new UserError(connectOnlySocialError); + } else if (options.creationLocked) { + throw new UserError( + 'Could not create account, try again later', { log: `account creation blocked by ACL (${options.ip})` }); + } else if (profile.suspended) { + throw new UserError( + 'Cannot create new account using suspended social site account', { log: 'account creation blocked by suspended' }); + } else { + return new Account(); + } } async function hasDuplicatesAtOrigin(account: IAccount, ip: string) { - const now = Date.now(); - const query = { origins: { $elemMatch: { ip } } }; - const duplicates: IAccount[] = await Account.find(query, '_id ban mute shadow flags name').lean().exec(); + const now = Date.now(); + const query = { origins: { $elemMatch: { ip } } }; + const duplicates: IAccount[] = await Account.find(query, '_id ban mute shadow flags name').lean().exec(); - return duplicates.some(({ _id, ban = 0, mute = 0, shadow = 0, flags = 0, name }) => { - if (_id.toString() === account._id.toString()) - return false; + return duplicates.some(({ _id, ban = 0, mute = 0, shadow = 0, flags = 0, name }) => { + if (_id.toString() === account._id.toString()) + return false; - if (ban === -1 || ban > now || mute === -1 || mute > now || shadow === -1 || shadow > now) - return true; + if (ban === -1 || ban > now || mute === -1 || mute > now || shadow === -1 || shadow > now) + return true; - if (hasFlag(flags, AccountFlags.CreatingDuplicates)) - return true; + if (hasFlag(flags, AccountFlags.CreatingDuplicates)) + return true; - if (name === account.name) - return true; + if (name === account.name) + return true; - return false; - }); + return false; + }); } const newAccountCheckQueue = taskQueue(); async function checkNewAccount(account: IAccount, options: CreateAccountOptions) { - newAccountCheckQueue.push(async () => { - try { - if (options.reportPotentialDuplicates) { - const duplicate = await hasDuplicatesAtOrigin(account, options.ip); + newAccountCheckQueue.push(async () => { + try { + if (options.reportPotentialDuplicates) { + const duplicate = await hasDuplicatesAtOrigin(account, options.ip); - if (duplicate) { - options.warn(account._id, `Potential duplicate`); - } - } - } catch (e) { - options.warn(account._id, `Error when checking new account`, e.message); - } - }); + if (duplicate) { + options.warn(account._id, `Potential duplicate`); + } + } + } catch (e) { + options.warn(account._id, `Error when checking new account`, e.message); + } + }); } export async function findOrCreateAccount(auth: IAuth, profile: Profile, options: CreateAccountOptions): Promise { - let account: IAccount | undefined = undefined; - let isNew = false; + let account: IAccount | undefined = undefined; + let isNew = false; - if (auth.account) { - account = await findAccount(auth.account); - } + if (auth.account) { + account = await findAccount(auth.account); + } - if (!account) { - account = await findAccountByEmail(profile.emails); - } + if (!account) { + account = await findAccountByEmail(profile.emails); + } - if (!account) { - account = createNewAccount(profile, options); - isNew = true; - } + if (!account) { + account = createNewAccount(profile, options); + isNew = true; + } - const assigned = await assignAuth(auth, account); + const assigned = await assignAuth(auth, account); - if (assigned && options.isSuspiciousAuth(auth)) { - options.warn(account._id, 'Suspicious auth'); - } + if (assigned && options.isSuspiciousAuth(auth)) { + options.warn(account._id, 'Suspicious auth'); + } - // fix accounts fields + // fix accounts fields - account.name = account.name || truncate(cleanName(profile.name) || 'Anonymous', { length: ACCOUNT_NAME_MAX_LENGTH }); - account.emails = account.emails || []; + account.name = account.name || truncate(cleanName(profile.name) || 'Anonymous', { length: ACCOUNT_NAME_MAX_LENGTH }); + account.emails = account.emails || []; - if (profile.emails.some(e => !includes(account!.emails, e))) { - const suspiciousEmails = profile.emails.filter(options.isSuspiciousName); + if (profile.emails.some(e => !includes(account!.emails, e))) { + const suspiciousEmails = profile.emails.filter(options.isSuspiciousName); - if (suspiciousEmails.length) { - options.warn(account._id, 'Suspicious email', suspiciousEmails.join(', ')); - } + if (suspiciousEmails.length) { + options.warn(account._id, 'Suspicious email', suspiciousEmails.join(', ')); + } - account.emails = uniq([...account.emails, ...profile.emails]); - } + account.emails = uniq([...account.emails, ...profile.emails]); + } - account.lastVisit = new Date(); - account.lastUserAgent = options.userAgent || account.lastUserAgent; - account.lastBrowserId = options.browserId || account.lastBrowserId; + account.lastVisit = new Date(); + account.lastUserAgent = options.userAgent || account.lastUserAgent; + account.lastBrowserId = options.browserId || account.lastBrowserId; - // save account + // save account - if (isNew) { - await account.save(); - system(account._id, `created account "${account.name}"`); - checkNewAccount(account, options); - } else { - const { name, emails, lastVisit, lastUserAgent, lastBrowserId } = account; - await Account.updateOne({ _id: account._id }, { name, emails, lastVisit, lastUserAgent, lastBrowserId }).exec(); - } + if (isNew) { + await account.save(); + system(account._id, `created account "${account.name}"`); + checkNewAccount(account, options); + } else { + const { name, emails, lastVisit, lastUserAgent, lastBrowserId } = account; + await Account.updateOne({ _id: account._id }, { name, emails, lastVisit, lastUserAgent, lastBrowserId }).exec(); + } - return account; + return account; } export function isNew(account: IAccount): boolean { - return !account.createdAt || account.createdAt.getTime() > fromNow(-DAY).getTime(); + return !account.createdAt || account.createdAt.getTime() > fromNow(-DAY).getTime(); } export function checkIfNotAdmin(account: IAccount, message: string) { - if (isAdmin(account)) { - logger.warn(`Cannot perform this action on admin user (${message})`); - throw new Error('Cannot perform this action on admin user'); - } else { - return account; - } + if (isAdmin(account)) { + logger.warn(`Cannot perform this action on admin user (${message})`); + throw new Error('Cannot perform this action on admin user'); + } else { + return account; + } } export async function updateCharacterCount(account: ID) { - const characterCount = await getCharacterCount(account); - await updateAccount(account, { characterCount }); + const characterCount = await getCharacterCount(account); + await updateAccount(account, { characterCount }); } export function updateAccountState(account: IAccount, update: (state: AccountState) => void) { - const state = account.state || {}; - update(state); - account.state = state; - updateAccount(account._id, { state: account.state }) - .catch(e => logger.error(e)); + const state = account.state || {}; + update(state); + account.state = state; + updateAccount(account._id, { state: account.state }) + .catch(e => logger.error(e)); } export function getAccountAlertMessage(account: IAccount) { - return (account.alert && account.alert.expires.getTime() > Date.now()) ? account.alert.message : undefined; + return (account.alert && account.alert.expires.getTime() > Date.now()) ? account.alert.message : undefined; } async function findFriendRequest(accountId: string, friendId: string): Promise { - const requests = await FriendRequest.find({ - $or: [ - { source: accountId, target: friendId }, - { source: friendId, target: accountId }, - ] - }).exec(); + const requests = await FriendRequest.find({ + $or: [ + { source: accountId, target: friendId }, + { source: friendId, target: accountId }, + ] + }).exec(); - return requests[0]; + return requests[0]; } export async function addFriend(accountId: string, friendId: string) { - const existing = await findFriendRequest(accountId, friendId); + const existing = await findFriendRequest(accountId, friendId); - if (existing) { - throw new Error(`Friend request already exists`); - } + if (existing) { + throw new Error(`Friend request already exists`); + } - await FriendRequest.create({ source: accountId, target: friendId }); + await FriendRequest.create({ source: accountId, target: friendId }); } export async function removeFriend(accountId: string, friendId: string) { - const existing = await findFriendRequest(accountId, friendId); + const existing = await findFriendRequest(accountId, friendId); - if (existing) { - existing.remove(); - } + if (existing) { + existing.remove(); + } } export function getCharacterLimit(account: IAccount) { - return getCharacterLimitInternal({ - flags: isPastSupporter(account) ? AccountDataFlags.PastSupporter : 0, - supporter: supporterLevel(account), - }); + return getCharacterLimitInternal({ + flags: isPastSupporter(account) ? AccountDataFlags.PastSupporter : 0, + supporter: supporterLevel(account), + }); } export function getSupporterInviteLimit(account: IAccount) { - return getSupporterInviteLimitInternal({ - roles: account.roles, - flags: isPastSupporter(account) ? AccountDataFlags.PastSupporter : 0, - supporter: supporterLevel(account), - }); + return getSupporterInviteLimitInternal({ + roles: account.roles, + flags: isPastSupporter(account) ? AccountDataFlags.PastSupporter : 0, + supporter: supporterLevel(account), + }); } diff --git a/src/ts/server/adminEncoders.ts b/src/ts/server/adminEncoders.ts index 381281d..e11ace4 100644 --- a/src/ts/server/adminEncoders.ts +++ b/src/ts/server/adminEncoders.ts @@ -3,43 +3,43 @@ import { BaseValues } from '../common/adminInterfaces'; import { IEvent } from './db'; export interface BaseTimes { - updatedAt: number; - createdAt: number; - lastVisit: number; + updatedAt: number; + createdAt: number; + lastVisit: number; } export function getBaseDate(items: T[], get: (item: T) => Date): string { - return items.reduce((min, i) => { - const date = get(i); - return date && min.getTime() > date.getTime() ? date : min; - }, new Date(0)).toISOString(); + return items.reduce((min, i) => { + const date = get(i); + return date && min.getTime() > date.getTime() ? date : min; + }, new Date(0)).toISOString(); } export function getBaseTimes(base: BaseValues): BaseTimes { - return mapValues(base, (x: string) => (new Date(x)).getTime()) as any; + return mapValues(base, (x: string) => (new Date(x)).getTime()) as any; } function trimValues(values: any[]): any[] { - return dropRightWhile(values, x => !x || (Array.isArray(x) && x.length === 0)); + return dropRightWhile(values, x => !x || (Array.isArray(x) && x.length === 0)); } function encodeDate(date: Date | undefined, baseValue: number): number { - return date ? (date.getTime() - baseValue) : 0; + return date ? (date.getTime() - baseValue) : 0; } // NOTE: update eventFields export function encodeEvent(event: IEvent, base: BaseTimes): any[] { - return trimValues([ - event._id, - encodeDate(event.updatedAt, base.updatedAt), - encodeDate(event.createdAt, base.createdAt), - event.type, - event.server, - event.message, - event.desc, - event.count, - event.origin ? { ip: event.origin.ip, country: event.origin.country } : null, - event.account, - event.pony && event.pony.toString(), - ]); + return trimValues([ + event._id, + encodeDate(event.updatedAt, base.updatedAt), + encodeDate(event.createdAt, base.createdAt), + event.type, + event.server, + event.message, + event.desc, + event.count, + event.origin ? { ip: event.origin.ip, country: event.origin.country } : null, + event.account, + event.pony && event.pony.toString(), + ]); } diff --git a/src/ts/server/adminServerActions.ts b/src/ts/server/adminServerActions.ts index a2c9c74..a178df4 100644 --- a/src/ts/server/adminServerActions.ts +++ b/src/ts/server/adminServerActions.ts @@ -5,29 +5,29 @@ import { HOUR } from '../common/constants'; import { fromNow, removeItem, formatDuration } from '../common/utils'; import { hasRole } from '../common/accountUtils'; import { - Settings, UpdateOrigin, AccountUpdate, OriginInfo, AccountOrigins, IAdminServerActions, FindPonyQuery, - AuthUpdate, PonyCreator, ServerConfig, GameServerSettings, AccountState, AuthDetails, MergeAccountData, - FindAccountQuery, AdminCache, ClearOrignsOptions, ModelTypes, Stats + Settings, UpdateOrigin, AccountUpdate, OriginInfo, AccountOrigins, IAdminServerActions, FindPonyQuery, + AuthUpdate, PonyCreator, ServerConfig, GameServerSettings, AccountState, AuthDetails, MergeAccountData, + FindAccountQuery, AdminCache, ClearOrignsOptions, ModelTypes, Stats } from '../common/adminInterfaces'; import { ClientAdminActions, ClientUpdate } from '../client/clientAdminActions'; import { TokenData } from './serverInterfaces'; import { toAccountData, toPonyObjectAdmin } from './serverUtils'; import { - IAccount, Account, ICharacter, Character, Auth, checkIfAdmin, ID, findCharacterById, updateAuth, queryAuths, - nullToUndefined, findAccount, findFriendIds + IAccount, Account, ICharacter, Character, Auth, checkIfAdmin, ID, findCharacterById, updateAuth, queryAuths, + nullToUndefined, findAccount, findFriendIds } from './db'; import { - updateAccountSafe, setRole, addEmail, removeEmail, removeIgnore, updateAccountCounter, timeoutAccount, - addIgnores, setAccountState, findAccounts, getAccountsByEmails, getAccountsByOrigin, - removeAccount, setAccountAlert + updateAccountSafe, setRole, addEmail, removeEmail, removeIgnore, updateAccountCounter, timeoutAccount, + addIgnores, setAccountState, findAccounts, getAccountsByEmails, getAccountsByOrigin, + removeAccount, setAccountAlert } from './api/admin-accounts'; import { - getAdminState, getChat, kickFromAllServers, notifyUpdate, clearSessions, actionForAllServers, updateOrigin, - getChatForAccounts, shutdownServers, resetUpdating, getUserCounts, getAccountDetails, - EndPoints, getOtherStats, updateGameServerSettings, updateServerSettings, forAllGameServers, + getAdminState, getChat, kickFromAllServers, notifyUpdate, clearSessions, actionForAllServers, updateOrigin, + getChatForAccounts, shutdownServers, resetUpdating, getUserCounts, getAccountDetails, + EndPoints, getOtherStats, updateGameServerSettings, updateServerSettings, forAllGameServers, } from './api/admin'; import { - findPonies, removeCharactersAboveLimit, createCharacter, removeCharacter, assignCharacter, removeAllCharacters + findPonies, removeCharactersAboveLimit, createCharacter, removeCharacter, assignCharacter, removeAllCharacters } from './api/ponies'; import { accountStatus, accountAround, getServer, getLoginServer, RemovedDocument, accountHidden } from './internal'; import { create } from './reporter'; @@ -42,557 +42,557 @@ import { getLastPatreonData } from './patreon'; import { removeFriend, addFriend } from './accountUtils'; @Socket({ - id: 'admin', - path: '/ws-admin', - connectionTokens: true, - tokenLifetime: 12 * HOUR, - perMessageDeflate: false, + id: 'admin', + path: '/ws-admin', + connectionTokens: true, + tokenLifetime: 12 * HOUR, + perMessageDeflate: false, }) export class AdminServerActions implements IAdminServerActions, SocketServer { - private account: IAccount; - private cache: AdminCache = {}; - private subscriptions = new Map(); - constructor( - private client: ClientAdminActions & ClientExtensions, - private server: ServerConfig, - private settings: Settings, - private adminService: AdminService, - private endPoints: EndPoints, - private removedDocument: RemovedDocument, - ) { - this.account = (client.tokenData as TokenData).account; - this.subscriptions.set('account:deleted', this.adminService.accountDeleted.subscribe(account => { - if (this.cache.findAccounts) { - removeItem(this.cache.findAccounts.result, account); - } - })); - } - disconnected() { - this.subscriptions.forEach(subscription => subscription.unsubscribe()); - clearTimeout(this.updatesTimeout); - } - // other - @Method({ promise: true }) - async getSignedAccount() { - return toAccountData(this.account); - } - @Method({ promise: true }) - async getCounts() { - const characters = await Promise.resolve(Character.estimatedDocumentCount() as any); + private account: IAccount; + private cache: AdminCache = {}; + private subscriptions = new Map(); + constructor( + private client: ClientAdminActions & ClientExtensions, + private server: ServerConfig, + private settings: Settings, + private adminService: AdminService, + private endPoints: EndPoints, + private removedDocument: RemovedDocument, + ) { + this.account = (client.tokenData as TokenData).account; + this.subscriptions.set('account:deleted', this.adminService.accountDeleted.subscribe(account => { + if (this.cache.findAccounts) { + removeItem(this.cache.findAccounts.result, account); + } + })); + } + disconnected() { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + clearTimeout(this.updatesTimeout); + } + // other + @Method({ promise: true }) + async getSignedAccount() { + return toAccountData(this.account); + } + @Method({ promise: true }) + async getCounts() { + const characters = await Promise.resolve(Character.estimatedDocumentCount() as any); - return { - characters, - accounts: this.adminService.accounts.items.length, - auths: this.adminService.auths.items.length, - origins: this.adminService.origins.items.length, - }; - } - @Method({ promise: true }) - async getOtherStats() { - return await getOtherStats(this.adminService); - } - // subscribing - private updates: ClientUpdate[] = []; - private updatesTimeout: any; - private pushUpdate(type: ModelTypes, id: string, update: any) { - const index = this.updates.findIndex(u => u.type === type && u.id === id); + return { + characters, + accounts: this.adminService.accounts.items.length, + auths: this.adminService.auths.items.length, + origins: this.adminService.origins.items.length, + }; + } + @Method({ promise: true }) + async getOtherStats() { + return await getOtherStats(this.adminService); + } + // subscribing + private updates: ClientUpdate[] = []; + private updatesTimeout: any; + private pushUpdate(type: ModelTypes, id: string, update: any) { + const index = this.updates.findIndex(u => u.type === type && u.id === id); - if (index !== -1) { - this.updates[index].update = update; - } else { - this.updates.push({ type, id, update }); - } + if (index !== -1) { + this.updates[index].update = update; + } else { + this.updates.push({ type, id, update }); + } - if (!this.updatesTimeout) { - this.updatesTimeout = setTimeout(() => { - this.client.updates(this.updates); - this.updates = []; - this.updatesTimeout = 0; - }, 50); - } - } - @Method() - subscribe(type: ModelTypes, id: string) { - const key = `${type}:${id}`; + if (!this.updatesTimeout) { + this.updatesTimeout = setTimeout(() => { + this.client.updates(this.updates); + this.updates = []; + this.updatesTimeout = 0; + }, 50); + } + } + @Method() + subscribe(type: ModelTypes, id: string) { + const key = `${type}:${id}`; - if (this.subscriptions.has(key)) - return; + if (this.subscriptions.has(key)) + return; - if (type === 'ponies') { - if (!this.adminService.ponies.get(id)) { - this.adminService.ponies.fetch({ _id: id }); - } - } + if (type === 'ponies') { + if (!this.adminService.ponies.get(id)) { + this.adminService.ponies.fetch({ _id: id }); + } + } - let subscription: Subscription | undefined; + let subscription: Subscription | undefined; - if (type === 'accountAuths') { - subscription = this.adminService.subscribeToAccountAuths(id, update => this.pushUpdate(type, id, update)); - } else if (type === 'accountOrigins') { - subscription = this.adminService.subscribeToAccountOrigins(id, update => this.pushUpdate(type, id, update)); - } else if (type === 'accountPonies') { - subscription = this.adminService.subscribeToAccountPonies(id, update => this.pushUpdate(type, id, update)); - } else if (type in this.adminService) { - subscription = this.adminService[type].subscribe(id, (id, update) => this.pushUpdate(type, id, update)); - } else { - throw new Error(`Invalid model type (${type})`); - } + if (type === 'accountAuths') { + subscription = this.adminService.subscribeToAccountAuths(id, update => this.pushUpdate(type, id, update)); + } else if (type === 'accountOrigins') { + subscription = this.adminService.subscribeToAccountOrigins(id, update => this.pushUpdate(type, id, update)); + } else if (type === 'accountPonies') { + subscription = this.adminService.subscribeToAccountPonies(id, update => this.pushUpdate(type, id, update)); + } else if (type in this.adminService) { + subscription = this.adminService[type].subscribe(id, (id, update) => this.pushUpdate(type, id, update)); + } else { + throw new Error(`Invalid model type (${type})`); + } - if (subscription) { - this.subscriptions.set(key, subscription); - } - } - @Method() - unsubscribe(type: ModelTypes, id: string) { - const key = `${type}:${id}`; - const subscription = this.subscriptions.get(key); + if (subscription) { + this.subscriptions.set(key, subscription); + } + } + @Method() + unsubscribe(type: ModelTypes, id: string) { + const key = `${type}:${id}`; + const subscription = this.subscriptions.get(key); - if (subscription) { - subscription.unsubscribe(); - this.subscriptions.delete(key); + if (subscription) { + subscription.unsubscribe(); + this.subscriptions.delete(key); - if (type === 'ponies') { - this.adminService.cleanupPony(id); - } else if (type === 'accountPonies') { - this.adminService.cleanupPoniesList(id); - } - } - } - // state - @Method({ promise: true }) - async clearSessions(accountId: string) { - await clearSessions(accountId); - } - @Method({ promise: true }) - async getState() { - return getAdminState(); - } - @Method({ promise: true }) - async updateSettings(update: Partial) { - await updateServerSettings(this.settings, update); - } - @Method({ promise: true }) - async updateGameServerSettings(serverId: string, update: Partial) { - await updateGameServerSettings(this.settings, serverId, update); - } - @Method({ promise: true }) - async fetchServerStats(serverId: string) { - const server = getServer(serverId); - return await server.api.stats(); - } - @Method({ promise: true }) - async fetchServerStatsTable(serverId: string, stats: Stats) { - const server = getServer(serverId); - return await server.api.statsTable(stats); - } - @Method({ promise: true }) - async report(accountId: string) { - create(this.server, accountId).info(`Reported by ${this.account.name}`); - } - @Method({ promise: true }) - async notifyUpdate(server: string) { - await notifyUpdate(server); - } - @Method({ promise: true }) - async shutdownServers(server: string) { - await shutdownServers(server, true); - } - @Method({ promise: true }) - async resetUpdating(server: string) { - await resetUpdating(server); - } - @Method({ promise: true }) - async action(action: string, accountId: string) { - await actionForAllServers(action, accountId); - } - @Method({ promise: true }) - async kick(accountId: string) { - await kickFromAllServers(accountId); - } - @Method({ promise: true }) - async kickAll(serverId: string) { - const server = getServer(serverId); - await server.api.kickAll(); - } - @Method({ promise: true }) - async getChat(search: string, date: string, caseInsensitive: boolean) { - return await getChat(search, date, caseInsensitive); - } - @Method({ promise: true }) - async getChatForAccounts(accountIds: string[], date: string) { - return await getChatForAccounts(accountIds, date); - } - @Method({ promise: true }) - async getRequestStats() { - const loginServer = getLoginServer('login'); - const requests = await loginServer.api.loginServerStats(); - const userCounts = await getUserCounts(); - return { requests, userCounts }; - } - // live (remove) - @Method({ promise: true }) - async get(endPoint: keyof EndPoints, id: string) { - // console.log('get', endPoint); - // return this.adminService[endPoint].get(id); - return await this.endPoints[endPoint].get(id) as any; - } - @Method({ promise: true }) - async getAll(endPoint: keyof EndPoints, timestamp?: string) { - return await this.endPoints[endPoint].getAll(timestamp) as any; - } - @Method({ promise: true }) - async assignAccount(endPoint: keyof EndPoints, id: string, account: string) { - return await this.endPoints[endPoint].assignAccount(id, account) as any; - } - @Method({ promise: true }) - async removeItem(endPoint: keyof EndPoints, id: string) { - return await this.endPoints[endPoint].removeItem(id) as any; - } - // events - @Method({ promise: true }) - async removeEvent(id: string) { - await this.adminService.events.remove(id); - await this.endPoints.events.removedItem(id); - } - // origins - @Method({ promise: true }) - async updateOrigin(origin: UpdateOrigin) { - await updateOrigin(origin); - } - @Method({ promise: true }) - async getOriginStats() { - return await getOriginStats(this.adminService.accounts.items); - } - @Method({ promise: true }) - async clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions) { - if (!this.adminService.loaded) { - throw new Error('Not loaded yet'); - } else { - await clearOrigins(this.adminService, count, andHigher, options); - } - } - @Method({ promise: true }) - async clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions) { - if (!this.adminService.loaded) { - throw new Error('Not loaded yet'); - } else { - await clearOriginsForAccounts(this.adminService, accounts, options); - } - } - // ponies - @Method({ promise: true }) - async getPony(id: string) { - return await Character.findById(id).exec().then(nullToUndefined) as any; - } - @Method({ promise: true }) - async getPonyInfo(id: string) { - const character = await findCharacterById(id); - return toPonyObjectAdmin(character); - } - @Method({ promise: true }) - async getPoniesCreators(account: string) { - const items: ICharacter[] = await Character.find({ account }, '_id name creator').lean().exec(); - return items.map(({ _id, name, creator }) => { _id, name, creator }); - } - @Method({ promise: true }) - async getPoniesForAccount(account: string) { - return await Character.find({ account }).lean().exec(); - } - @Method({ promise: true }) - async getDetailsForAccount(accountId: string) { - return await getAccountDetails(accountId); - } - @Method({ promise: true }) - async findPonies(query: FindPonyQuery, page: number, _skipTotalCount: boolean) { - return await findPonies(query, page); - } - @Method({ promise: true }) - async createPony(account: string, name: string, info: string) { - await createCharacter(account, name, info); - system(account, `Created character (${name}) ${this.by()}`); - } - @Method({ promise: true }) - async assignPony(ponyId: string, accountId: string) { - await assignCharacter(ponyId, accountId); - } - @Method({ promise: true }) - async removePony(id: string) { - await removeCharacter(this.adminService, id); - } - @Method({ promise: true }) - async removePoniesAboveLimit(account: string) { - await removeCharactersAboveLimit(this.removedDocument, account); - system(account, `Removed ponies above limit ${this.by()}`); - } - @Method({ promise: true }) - async removeAllPonies(account: string) { - await removeAllCharacters(this.removedDocument, account); - system(account, `Removed all ponies ${this.by()}`); - } - // auths - @Method({ promise: true }) - async getAuth(id: string) { - return await Auth.findById(id).exec().then(nullToUndefined) as any; - } - @Method({ promise: true }) - async getAuthsForAccount(accountId: string) { - return await Auth.find({ account: accountId }).exec() as any; - } - @Method({ promise: true }) - async fetchAuthDetails(auths: string[]): Promise { - const items = await queryAuths({ _id: { $in: auths } }, '_id lastUsed'); + if (type === 'ponies') { + this.adminService.cleanupPony(id); + } else if (type === 'accountPonies') { + this.adminService.cleanupPoniesList(id); + } + } + } + // state + @Method({ promise: true }) + async clearSessions(accountId: string) { + await clearSessions(accountId); + } + @Method({ promise: true }) + async getState() { + return getAdminState(); + } + @Method({ promise: true }) + async updateSettings(update: Partial) { + await updateServerSettings(this.settings, update); + } + @Method({ promise: true }) + async updateGameServerSettings(serverId: string, update: Partial) { + await updateGameServerSettings(this.settings, serverId, update); + } + @Method({ promise: true }) + async fetchServerStats(serverId: string) { + const server = getServer(serverId); + return await server.api.stats(); + } + @Method({ promise: true }) + async fetchServerStatsTable(serverId: string, stats: Stats) { + const server = getServer(serverId); + return await server.api.statsTable(stats); + } + @Method({ promise: true }) + async report(accountId: string) { + create(this.server, accountId).info(`Reported by ${this.account.name}`); + } + @Method({ promise: true }) + async notifyUpdate(server: string) { + await notifyUpdate(server); + } + @Method({ promise: true }) + async shutdownServers(server: string) { + await shutdownServers(server, true); + } + @Method({ promise: true }) + async resetUpdating(server: string) { + await resetUpdating(server); + } + @Method({ promise: true }) + async action(action: string, accountId: string) { + await actionForAllServers(action, accountId); + } + @Method({ promise: true }) + async kick(accountId: string) { + await kickFromAllServers(accountId); + } + @Method({ promise: true }) + async kickAll(serverId: string) { + const server = getServer(serverId); + await server.api.kickAll(); + } + @Method({ promise: true }) + async getChat(search: string, date: string, caseInsensitive: boolean) { + return await getChat(search, date, caseInsensitive); + } + @Method({ promise: true }) + async getChatForAccounts(accountIds: string[], date: string) { + return await getChatForAccounts(accountIds, date); + } + @Method({ promise: true }) + async getRequestStats() { + const loginServer = getLoginServer('login'); + const requests = await loginServer.api.loginServerStats(); + const userCounts = await getUserCounts(); + return { requests, userCounts }; + } + // live (remove) + @Method({ promise: true }) + async get(endPoint: keyof EndPoints, id: string) { + // console.log('get', endPoint); + // return this.adminService[endPoint].get(id); + return await this.endPoints[endPoint].get(id) as any; + } + @Method({ promise: true }) + async getAll(endPoint: keyof EndPoints, timestamp?: string) { + return await this.endPoints[endPoint].getAll(timestamp) as any; + } + @Method({ promise: true }) + async assignAccount(endPoint: keyof EndPoints, id: string, account: string) { + return await this.endPoints[endPoint].assignAccount(id, account) as any; + } + @Method({ promise: true }) + async removeItem(endPoint: keyof EndPoints, id: string) { + return await this.endPoints[endPoint].removeItem(id) as any; + } + // events + @Method({ promise: true }) + async removeEvent(id: string) { + await this.adminService.events.remove(id); + await this.endPoints.events.removedItem(id); + } + // origins + @Method({ promise: true }) + async updateOrigin(origin: UpdateOrigin) { + await updateOrigin(origin); + } + @Method({ promise: true }) + async getOriginStats() { + return await getOriginStats(this.adminService.accounts.items); + } + @Method({ promise: true }) + async clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions) { + if (!this.adminService.loaded) { + throw new Error('Not loaded yet'); + } else { + await clearOrigins(this.adminService, count, andHigher, options); + } + } + @Method({ promise: true }) + async clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions) { + if (!this.adminService.loaded) { + throw new Error('Not loaded yet'); + } else { + await clearOriginsForAccounts(this.adminService, accounts, options); + } + } + // ponies + @Method({ promise: true }) + async getPony(id: string) { + return await Character.findById(id).exec().then(nullToUndefined) as any; + } + @Method({ promise: true }) + async getPonyInfo(id: string) { + const character = await findCharacterById(id); + return toPonyObjectAdmin(character); + } + @Method({ promise: true }) + async getPoniesCreators(account: string) { + const items: ICharacter[] = await Character.find({ account }, '_id name creator').lean().exec(); + return items.map(({ _id, name, creator }) => { _id, name, creator }); + } + @Method({ promise: true }) + async getPoniesForAccount(account: string) { + return await Character.find({ account }).lean().exec(); + } + @Method({ promise: true }) + async getDetailsForAccount(accountId: string) { + return await getAccountDetails(accountId); + } + @Method({ promise: true }) + async findPonies(query: FindPonyQuery, page: number, _skipTotalCount: boolean) { + return await findPonies(query, page); + } + @Method({ promise: true }) + async createPony(account: string, name: string, info: string) { + await createCharacter(account, name, info); + system(account, `Created character (${name}) ${this.by()}`); + } + @Method({ promise: true }) + async assignPony(ponyId: string, accountId: string) { + await assignCharacter(ponyId, accountId); + } + @Method({ promise: true }) + async removePony(id: string) { + await removeCharacter(this.adminService, id); + } + @Method({ promise: true }) + async removePoniesAboveLimit(account: string) { + await removeCharactersAboveLimit(this.removedDocument, account); + system(account, `Removed ponies above limit ${this.by()}`); + } + @Method({ promise: true }) + async removeAllPonies(account: string) { + await removeAllCharacters(this.removedDocument, account); + system(account, `Removed all ponies ${this.by()}`); + } + // auths + @Method({ promise: true }) + async getAuth(id: string) { + return await Auth.findById(id).exec().then(nullToUndefined) as any; + } + @Method({ promise: true }) + async getAuthsForAccount(accountId: string) { + return await Auth.find({ account: accountId }).exec() as any; + } + @Method({ promise: true }) + async fetchAuthDetails(auths: string[]): Promise { + const items = await queryAuths({ _id: { $in: auths } }, '_id lastUsed'); - return items.map(a => ({ - id: a._id.toString(), - lastUsed: a.lastUsed && a.lastUsed.toISOString(), - })); - } - @Method({ promise: true }) - async updateAuth(id: string, update: AuthUpdate) { - const auth = await Auth.findById(id).exec(); - await throwOnAdmin(auth && auth.account); - await updateAuth(id, update); - } - @Method({ promise: true }) - async assignAuth(authId: string, accountId: string) { - await assignAuth(authId, accountId); - } - @Method({ promise: true }) - async removeAuth(id: string) { - await removeAuth(this.adminService, id); - } - // accounts - @Method({ promise: true }) - async getAccount(id: string) { - return await findAccount(id) as any; - } - @Method({ promise: true }) - async findAccounts(query: FindAccountQuery) { - return await findAccounts(this.cache, this.adminService, query); - } - @Method({ promise: true }) - async createAccount(name: string): Promise { - const account = await Account.create({ name }); - system(account._id.toString(), `Created account ${this.by()}`); - return account._id.toString(); - } - @Method({ promise: true }) - async getAccountsByEmails(emails: string[]) { - return getAccountsByEmails(this.adminService, emails); - } - @Method({ promise: true }) - async getAccountsByOrigin(ip: string) { - return getAccountsByOrigin(this.adminService, ip); - } - @Method({ promise: true }) - async setName(accountId: string, name: string) { - await updateAccountSafe(accountId, { name }); - system(accountId, `Updated name (${name}) ${this.by()}`); - } - @Method({ promise: true }) - async setAge(accountId: string, age: number) { - if (age === -1) { - await Account.updateOne({ _id: accountId }, { $unset: { birthyear: 1 } }).exec(); - } else { - const birthyear = (new Date()).getFullYear() - age; - await Account.updateOne({ _id: accountId }, { birthyear }).exec(); - } + return items.map(a => ({ + id: a._id.toString(), + lastUsed: a.lastUsed && a.lastUsed.toISOString(), + })); + } + @Method({ promise: true }) + async updateAuth(id: string, update: AuthUpdate) { + const auth = await Auth.findById(id).exec(); + await throwOnAdmin(auth && auth.account); + await updateAuth(id, update); + } + @Method({ promise: true }) + async assignAuth(authId: string, accountId: string) { + await assignAuth(authId, accountId); + } + @Method({ promise: true }) + async removeAuth(id: string) { + await removeAuth(this.adminService, id); + } + // accounts + @Method({ promise: true }) + async getAccount(id: string) { + return await findAccount(id) as any; + } + @Method({ promise: true }) + async findAccounts(query: FindAccountQuery) { + return await findAccounts(this.cache, this.adminService, query); + } + @Method({ promise: true }) + async createAccount(name: string): Promise { + const account = await Account.create({ name }); + system(account._id.toString(), `Created account ${this.by()}`); + return account._id.toString(); + } + @Method({ promise: true }) + async getAccountsByEmails(emails: string[]) { + return getAccountsByEmails(this.adminService, emails); + } + @Method({ promise: true }) + async getAccountsByOrigin(ip: string) { + return getAccountsByOrigin(this.adminService, ip); + } + @Method({ promise: true }) + async setName(accountId: string, name: string) { + await updateAccountSafe(accountId, { name }); + system(accountId, `Updated name (${name}) ${this.by()}`); + } + @Method({ promise: true }) + async setAge(accountId: string, age: number) { + if (age === -1) { + await Account.updateOne({ _id: accountId }, { $unset: { birthyear: 1 } }).exec(); + } else { + const birthyear = (new Date()).getFullYear() - age; + await Account.updateOne({ _id: accountId }, { birthyear }).exec(); + } - system(accountId, `Updated birth year (${age}) ${this.by()}`); - } - @Method({ promise: true }) - async setRole(accountId: string, role: string, set: boolean) { - await setRole(accountId, role, set, hasRole(this.account, 'superadmin')); - system(accountId, `${set ? 'Added' : 'Removed'} role (${role}) ${this.by()}`); - } - @Method({ promise: true }) - async updateAccount(accountId: string, update: AccountUpdate, message?: string) { - await updateAccountSafe(accountId, update); + system(accountId, `Updated birth year (${age}) ${this.by()}`); + } + @Method({ promise: true }) + async setRole(accountId: string, role: string, set: boolean) { + await setRole(accountId, role, set, hasRole(this.account, 'superadmin')); + system(accountId, `${set ? 'Added' : 'Removed'} role (${role}) ${this.by()}`); + } + @Method({ promise: true }) + async updateAccount(accountId: string, update: AccountUpdate, message?: string) { + await updateAccountSafe(accountId, update); - if (message) { - system(accountId, `${message} ${this.by()}`); - } - } - @Method({ promise: true }) - async timeoutAccount(accountId: string, timeout: number) { - const message = timeout ? `Timed out ${moment.duration(timeout).humanize()}` : 'Unmuted'; - system(accountId, `${message} ${this.by()}`); - await timeoutAccount(accountId, fromNow(timeout | 0)); - } - @Method({ promise: true }) - async updateAccountCounter(accountId: string, name: keyof AccountCounters, value: number) { - await updateAccountCounter(accountId, name, value); - } - @Method({ promise: true }) - async mergeAccounts(accountId: string, withId: string) { - const server = getLoginServer('login'); - await server.api.mergeAccounts(accountId, withId, this.by(), hasRole(this.account, 'superadmin'), true); - } - @Method({ promise: true }) - async unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData) { - await splitAccounts(accountId, mergeId, split, keep, this.by()); - } - @Method({ promise: true }) - async getAccountStatus(accountId: string) { - return await accountStatus(accountId); - } - @Method({ promise: true }) - async getAccountAround(accountId: string) { - return await accountAround(accountId); - } - @Method({ promise: true }) - async getAccountHidden(accountId: string) { - return await accountHidden(accountId); - } - @Method({ promise: true }) - async getAccountFriends(accountId: string) { - return findFriendIds(accountId); - } - @Method({ promise: true }) - async removeAccount(accountId: string) { - await removeAccount(this.adminService, accountId); - } - @Method({ promise: true }) - async setAlert(accountId: string, message: string, expiresIn: number) { - await setAccountAlert(accountId, message, fromNow(expiresIn)); - system(accountId, `${expiresIn ? 'Set' : 'Unset'} alert for ${formatDuration(expiresIn)} "${message}" ${this.by()}`); - } - // accounts - origins - @Method({ promise: true }) - async removeAllOrigins(accountId: string) { - await removeAllOrigins(this.adminService, accountId); - } - @Method({ promise: true }) - async removeOriginsForAccount(accountId: string, ips: string[]) { - await removeOrigins(this.adminService, accountId, ips); - } - @Method({ promise: true }) - async removeOriginsForAccounts(origins: AccountOrigins[]) { - await Promise.all(origins.map(o => removeOrigins(this.adminService, o.accountId, o.ips))); - } - @Method({ promise: true }) - async addOriginToAccount(accountId: string, origin: OriginInfo) { - if (origin && origin.ip && origin.country) { - await addOrigin(accountId, origin); - system(accountId, `Added origin (${JSON.stringify(origin)}) ${this.by()}`); - } else { - throw new Error('Invalid origin'); - } - } - // accounts - emails - @Method({ promise: true }) - async addEmail(accountId: string, email: string) { - await addEmail(accountId, email); - system(accountId, `Added email (${email}) ${this.by()}`); - } - @Method({ promise: true }) - async removeEmail(accountId: string, email: string) { - await removeEmail(accountId, email); - system(accountId, `Removed email (${email}) ${this.by()}`); - } - // accounts - ignores - @Method({ promise: true }) - async removeIgnore(accountId: string, ignore: string) { - await removeIgnore(accountId, ignore); - } - @Method({ promise: true }) - async addIgnores(accountId: string, ignores: string[]) { - await addIgnores(accountId, ignores); - } - @Method({ promise: true }) - async setAccountState(accountId: string, state: AccountState) { - await setAccountState(accountId, state); - } - @Method({ promise: true }) - async getIgnoresAndIgnoredBy(accountId: string): Promise<{ ignores: string[]; ignoredBy: string[]; }> { - const [ignores, ignoredBy] = await Promise.all([ - Account - .find({ ignores: { $in: [accountId] } }, '_id') - .lean() - .exec() - .then((accounts: IAccount[]) => accounts.map(a => a._id.toString())), - Account - .findOne({ _id: accountId }, 'ignores') - .lean() - .exec() - .then((account: IAccount | null) => account && account.ignores || []), - ]); + if (message) { + system(accountId, `${message} ${this.by()}`); + } + } + @Method({ promise: true }) + async timeoutAccount(accountId: string, timeout: number) { + const message = timeout ? `Timed out ${moment.duration(timeout).humanize()}` : 'Unmuted'; + system(accountId, `${message} ${this.by()}`); + await timeoutAccount(accountId, fromNow(timeout | 0)); + } + @Method({ promise: true }) + async updateAccountCounter(accountId: string, name: keyof AccountCounters, value: number) { + await updateAccountCounter(accountId, name, value); + } + @Method({ promise: true }) + async mergeAccounts(accountId: string, withId: string) { + const server = getLoginServer('login'); + await server.api.mergeAccounts(accountId, withId, this.by(), hasRole(this.account, 'superadmin'), true); + } + @Method({ promise: true }) + async unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData) { + await splitAccounts(accountId, mergeId, split, keep, this.by()); + } + @Method({ promise: true }) + async getAccountStatus(accountId: string) { + return await accountStatus(accountId); + } + @Method({ promise: true }) + async getAccountAround(accountId: string) { + return await accountAround(accountId); + } + @Method({ promise: true }) + async getAccountHidden(accountId: string) { + return await accountHidden(accountId); + } + @Method({ promise: true }) + async getAccountFriends(accountId: string) { + return findFriendIds(accountId); + } + @Method({ promise: true }) + async removeAccount(accountId: string) { + await removeAccount(this.adminService, accountId); + } + @Method({ promise: true }) + async setAlert(accountId: string, message: string, expiresIn: number) { + await setAccountAlert(accountId, message, fromNow(expiresIn)); + system(accountId, `${expiresIn ? 'Set' : 'Unset'} alert for ${formatDuration(expiresIn)} "${message}" ${this.by()}`); + } + // accounts - origins + @Method({ promise: true }) + async removeAllOrigins(accountId: string) { + await removeAllOrigins(this.adminService, accountId); + } + @Method({ promise: true }) + async removeOriginsForAccount(accountId: string, ips: string[]) { + await removeOrigins(this.adminService, accountId, ips); + } + @Method({ promise: true }) + async removeOriginsForAccounts(origins: AccountOrigins[]) { + await Promise.all(origins.map(o => removeOrigins(this.adminService, o.accountId, o.ips))); + } + @Method({ promise: true }) + async addOriginToAccount(accountId: string, origin: OriginInfo) { + if (origin && origin.ip && origin.country) { + await addOrigin(accountId, origin); + system(accountId, `Added origin (${JSON.stringify(origin)}) ${this.by()}`); + } else { + throw new Error('Invalid origin'); + } + } + // accounts - emails + @Method({ promise: true }) + async addEmail(accountId: string, email: string) { + await addEmail(accountId, email); + system(accountId, `Added email (${email}) ${this.by()}`); + } + @Method({ promise: true }) + async removeEmail(accountId: string, email: string) { + await removeEmail(accountId, email); + system(accountId, `Removed email (${email}) ${this.by()}`); + } + // accounts - ignores + @Method({ promise: true }) + async removeIgnore(accountId: string, ignore: string) { + await removeIgnore(accountId, ignore); + } + @Method({ promise: true }) + async addIgnores(accountId: string, ignores: string[]) { + await addIgnores(accountId, ignores); + } + @Method({ promise: true }) + async setAccountState(accountId: string, state: AccountState) { + await setAccountState(accountId, state); + } + @Method({ promise: true }) + async getIgnoresAndIgnoredBy(accountId: string): Promise<{ ignores: string[]; ignoredBy: string[]; }> { + const [ignores, ignoredBy] = await Promise.all([ + Account + .find({ ignores: { $in: [accountId] } }, '_id') + .lean() + .exec() + .then((accounts: IAccount[]) => accounts.map(a => a._id.toString())), + Account + .findOne({ _id: accountId }, 'ignores') + .lean() + .exec() + .then((account: IAccount | null) => account && account.ignores || []), + ]); - return { ignores, ignoredBy }; - } - // accounts - friends - @Method({ promise: true }) - async removeFriend(accountId: string, friendId: string) { - await removeFriend(accountId, friendId); - } - @Method({ promise: true }) - async addFriend(accountId: string, friendId: string) { - await addFriend(accountId, friendId); - } - // accounts - duplicates - @Method({ promise: true }) - async getAllDuplicatesQuickInfo(accountId: string) { - return await getAllDuplicatesQuickInfo(this.adminService, accountId); - } - @Method({ promise: true }) - async getAllDuplicates(accountId: string) { - return await getAllDuplicatesWithInfo(this.adminService, accountId); - } - @Method({ promise: true }) - async getDuplicateEntries(force: boolean) { - return await getDuplicateEntries(this.adminService.accounts.items, force); - } - // patreon - @Method({ promise: true }) - async updatePatreon() { - await updatePatreonData(this.server, this.settings); - } - @Method({ promise: true }) - async resetSupporter(accountId: string) { - await Account.updateOne( - { _id: accountId }, - { $unset: { supporter: 1, patreon: 1, supporterDeclinedSince: 1 } }).exec(); - } - @Method({ promise: true }) - async getLastPatreonData() { - const data = await getLastPatreonData(); + return { ignores, ignoredBy }; + } + // accounts - friends + @Method({ promise: true }) + async removeFriend(accountId: string, friendId: string) { + await removeFriend(accountId, friendId); + } + @Method({ promise: true }) + async addFriend(accountId: string, friendId: string) { + await addFriend(accountId, friendId); + } + // accounts - duplicates + @Method({ promise: true }) + async getAllDuplicatesQuickInfo(accountId: string) { + return await getAllDuplicatesQuickInfo(this.adminService, accountId); + } + @Method({ promise: true }) + async getAllDuplicates(accountId: string) { + return await getAllDuplicatesWithInfo(this.adminService, accountId); + } + @Method({ promise: true }) + async getDuplicateEntries(force: boolean) { + return await getDuplicateEntries(this.adminService.accounts.items, force); + } + // patreon + @Method({ promise: true }) + async updatePatreon() { + await updatePatreonData(this.server, this.settings); + } + @Method({ promise: true }) + async resetSupporter(accountId: string) { + await Account.updateOne( + { _id: accountId }, + { $unset: { supporter: 1, patreon: 1, supporterDeclinedSince: 1 } }).exec(); + } + @Method({ promise: true }) + async getLastPatreonData() { + const data = await getLastPatreonData(); - // if (data) { - // data.pledges.forEach(pledge => { - // const auth = this.adminService.auths.items.find(a => a.openId === pledge.user); - // pledge.account = auth && auth.account; - // }); - // } + // if (data) { + // data.pledges.forEach(pledge => { + // const auth = this.adminService.auths.items.find(a => a.openId === pledge.user); + // pledge.account = auth && auth.account; + // }); + // } - return data; - } - @Method({ promise: true }) - async updatePastSupporters() { - await updatePastSupporters(); - } - // other - @Method({ promise: true }) - async getTimings(serverId: string) { - const server = getServer(serverId); - return server.api.getTimings(); - } - @Method({ promise: true }) - async teleportTo(accountId: string) { - const adminAccountId = this.account._id.toString(); - await forAllGameServers(server => server.api.teleportTo(adminAccountId, accountId)); - } - // utils - private by() { - return `by ${this.account.name} [${this.account._id}]`; - } + return data; + } + @Method({ promise: true }) + async updatePastSupporters() { + await updatePastSupporters(); + } + // other + @Method({ promise: true }) + async getTimings(serverId: string) { + const server = getServer(serverId); + return server.api.getTimings(); + } + @Method({ promise: true }) + async teleportTo(accountId: string) { + const adminAccountId = this.account._id.toString(); + await forAllGameServers(server => server.api.teleportTo(adminAccountId, accountId)); + } + // utils + private by() { + return `by ${this.account.name} [${this.account._id}]`; + } } async function throwOnAdmin(account: ID | null | undefined) { - if (account) { - const isAdmin = await checkIfAdmin(account); + if (account) { + const isAdmin = await checkIfAdmin(account); - if (isAdmin) { - throw new Error('Cannot change for admin user'); - } - } + if (isAdmin) { + throw new Error('Cannot change for admin user'); + } + } } diff --git a/src/ts/server/api/account.ts b/src/ts/server/api/account.ts index 62bf50e..e5fa0fa 100644 --- a/src/ts/server/api/account.ts +++ b/src/ts/server/api/account.ts @@ -1,14 +1,14 @@ import * as moment from 'moment'; import { ACCOUNT_NAME_MAX_LENGTH, ACCOUNT_NAME_MIN_LENGTH, MIN_CHATLOG_RANGE, MAX_CHATLOG_RANGE, HIDES_PER_PAGE } from '../../common/constants'; import { - UpdateAccountData, AccountSettings, AccountData, ModAction, EntitiesEditorInfo, EntityNameTypes + UpdateAccountData, AccountSettings, AccountData, ModAction, EntitiesEditorInfo, EntityNameTypes } from '../../common/interfaces'; import { isMod } from '../../common/accountUtils'; import { cleanName } from '../../client/clientUtils'; import { toAccountData, toPonyObject, toSocialSite, toPonyObjectFields, toSocialSiteFields } from '../serverUtils'; import { - IAccount, FindAccountSafe, FindAuth, FindAuths, FindCharacters, CountAuths, Auth, - ID, findFriends, Account, HideRequest + IAccount, FindAccountSafe, FindAuth, FindAuths, FindCharacters, CountAuths, Auth, + ID, findFriends, Account, HideRequest } from '../db'; import { UserError } from '../userError'; import * as entities from '../../common/entities'; @@ -25,234 +25,234 @@ export type GetAccountData = ReturnType; type LogAccount = (accountId: ID, message: string) => void; const exclude = [ - 'getEntityType', 'getEntityTypeName', 'createAnEntity', 'createEntity', 'pony', - 'createBaseEntity', 'getEntityTypesAndNames', + 'getEntityType', 'getEntityTypeName', 'createAnEntity', 'createEntity', 'pony', + 'createBaseEntity', 'getEntityTypesAndNames', ]; export const allEntities = Object.keys(entities) - .filter(key => typeof (entities as any)[key] === 'function') - .filter(key => !includes(exclude, key)); + .filter(key => typeof (entities as any)[key] === 'function') + .filter(key => !includes(exclude, key)); function getEntityNamesToTypes() { - const result: EntityNameTypes[] = []; + const result: EntityNameTypes[] = []; - for (const name of allEntities) { - const created = (entities as any)[name](0, 0); - const array = Array.isArray(created) ? created : [created]; - const types = array.map(e => e.type); - result.push({ name, types }); - } + for (const name of allEntities) { + const created = (entities as any)[name](0, 0); + const array = Array.isArray(created) ? created : [created]; + const types = array.map(e => e.type); + result.push({ name, types }); + } - return result; + return result; } const entitiesInfo: EntitiesEditorInfo = { - typeToName: entities.getEntityTypesAndNames(), - nameToTypes: getEntityNamesToTypes(), - names: allEntities, + typeToName: entities.getEntityTypesAndNames(), + nameToTypes: getEntityNamesToTypes(), + names: allEntities, }; const actions = [ - { name: 'kick', action: ModAction.Kick }, - { name: 'ban', action: ModAction.Ban }, + { name: 'kick', action: ModAction.Kick }, + { name: 'ban', action: ModAction.Ban }, ]; export const modCheck = { xcz: { vdw: { qwe: { mnb: {} } } }, actions }; function fixUpdateAccountData(update: UpdateAccountData | undefined) { - const fixed: UpdateAccountData = {} as any; + const fixed: UpdateAccountData = {} as any; - if (update) { - if (update.name && typeof update.name === 'string') { - const name = cleanName(update.name); + if (update) { + if (update.name && typeof update.name === 'string') { + const name = cleanName(update.name); - if (name.length >= ACCOUNT_NAME_MIN_LENGTH && name.length <= ACCOUNT_NAME_MAX_LENGTH) { - fixed.name = name; - } - } + if (name.length >= ACCOUNT_NAME_MIN_LENGTH && name.length <= ACCOUNT_NAME_MAX_LENGTH) { + fixed.name = name; + } + } - if (update.birthdate && typeof update.birthdate === 'string') { - fixed.birthdate = update.birthdate; - } - } + if (update.birthdate && typeof update.birthdate === 'string') { + fixed.birthdate = update.birthdate; + } + } - return fixed; + return fixed; } function fixAccountSettings(settings: AccountSettings | undefined) { - const fixed: Partial = {}; + const fixed: Partial = {}; - if (settings) { - if (settings.defaultServer !== undefined) { - fixed.defaultServer = `${settings.defaultServer}`; - } + if (settings) { + if (settings.defaultServer !== undefined) { + fixed.defaultServer = `${settings.defaultServer}`; + } - if (settings.filterCyrillic !== undefined) { - fixed.filterCyrillic = !!settings.filterCyrillic; - } + if (settings.filterCyrillic !== undefined) { + fixed.filterCyrillic = !!settings.filterCyrillic; + } - if (settings.filterSwearWords !== undefined) { - fixed.filterSwearWords = !!settings.filterSwearWords; - } + if (settings.filterSwearWords !== undefined) { + fixed.filterSwearWords = !!settings.filterSwearWords; + } - if (settings.ignorePartyInvites !== undefined) { - fixed.ignorePartyInvites = !!settings.ignorePartyInvites; - } + if (settings.ignorePartyInvites !== undefined) { + fixed.ignorePartyInvites = !!settings.ignorePartyInvites; + } - if (settings.ignoreFriendInvites !== undefined) { - fixed.ignoreFriendInvites = !!settings.ignoreFriendInvites; - } + if (settings.ignoreFriendInvites !== undefined) { + fixed.ignoreFriendInvites = !!settings.ignoreFriendInvites; + } - if (settings.ignorePublicChat !== undefined) { - fixed.ignorePublicChat = !!settings.ignorePublicChat; - } + if (settings.ignorePublicChat !== undefined) { + fixed.ignorePublicChat = !!settings.ignorePublicChat; + } - if (settings.ignoreNonFriendWhispers !== undefined) { - fixed.ignoreNonFriendWhispers = !!settings.ignoreNonFriendWhispers; - } + if (settings.ignoreNonFriendWhispers !== undefined) { + fixed.ignoreNonFriendWhispers = !!settings.ignoreNonFriendWhispers; + } - if (settings.chatlogOpacity !== undefined) { - fixed.chatlogOpacity = clamp(settings.chatlogOpacity | 0, 0, 100); - } + if (settings.chatlogOpacity !== undefined) { + fixed.chatlogOpacity = clamp(settings.chatlogOpacity | 0, 0, 100); + } - if (settings.chatlogRange !== undefined) { - fixed.chatlogRange = clamp(settings.chatlogRange | 0, MIN_CHATLOG_RANGE, MAX_CHATLOG_RANGE); - } + if (settings.chatlogRange !== undefined) { + fixed.chatlogRange = clamp(settings.chatlogRange | 0, MIN_CHATLOG_RANGE, MAX_CHATLOG_RANGE); + } - if (settings.seeThroughObjects !== undefined) { - fixed.seeThroughObjects = !!settings.seeThroughObjects; - } + if (settings.seeThroughObjects !== undefined) { + fixed.seeThroughObjects = !!settings.seeThroughObjects; + } - if (settings.filterWords !== undefined) { - fixed.filterWords = `${settings.filterWords}`; - } + if (settings.filterWords !== undefined) { + fixed.filterWords = `${settings.filterWords}`; + } - if (settings.actions !== undefined) { - fixed.actions = `${settings.actions}`; - } + if (settings.actions !== undefined) { + fixed.actions = `${settings.actions}`; + } - if (settings.hidden !== undefined) { - fixed.hidden = !!settings.hidden; - } - } + if (settings.hidden !== undefined) { + fixed.hidden = !!settings.hidden; + } + } - return fixed; + return fixed; } export const createGetAccountData = - (findCharacters: FindCharacters, findAuths: FindAuths) => - async (account: IAccount): Promise => { - const [ponies, auths] = await Promise.all([ - findCharacters(account._id, toPonyObjectFields), - findAuths(account._id, toSocialSiteFields), - ]); + (findCharacters: FindCharacters, findAuths: FindAuths) => + async (account: IAccount): Promise => { + const [ponies, auths] = await Promise.all([ + findCharacters(account._id, toPonyObjectFields), + findAuths(account._id, toSocialSiteFields), + ]); - const data = toAccountData(account); - data.ponies = ponies.map(toPonyObject) as any; - data.sites = auths.map(toSocialSite); - data.alert = getAccountAlertMessage(account); + const data = toAccountData(account); + data.ponies = ponies.map(toPonyObject) as any; + data.sites = auths.map(toSocialSite); + data.alert = getAccountAlertMessage(account); - if (isMod(account)) { - data.check = modCheck; - } + if (isMod(account)) { + data.check = modCheck; + } - if (BETA && isMod(account)) { - data.editor = entitiesInfo; - } + if (BETA && isMod(account)) { + data.editor = entitiesInfo; + } - return data; - }; + return data; + }; export async function getFriends(account: IAccount) { - return findFriends(account._id, true); + return findFriends(account._id, true); } export async function getHides(account: IAccount, page: number) { - const hideRequests = await HideRequest - .find({ source: account._id }, '_id name date') - .sort({ date: -1 }) - .skip(page * HIDES_PER_PAGE) - .limit(HIDES_PER_PAGE) - .lean() - .exec(); + const hideRequests = await HideRequest + .find({ source: account._id }, '_id name date') + .sort({ date: -1 }) + .skip(page * HIDES_PER_PAGE) + .limit(HIDES_PER_PAGE) + .lean() + .exec(); - return hideRequests.map((f: any) => ({ - id: f._id.toString(), - name: f.name, - date: moment(f.date).fromNow(), - })); + return hideRequests.map((f: any) => ({ + id: f._id.toString(), + name: f.name, + date: moment(f.date).fromNow(), + })); } export const createGetAccountCharacters = - (findCharacters: FindCharacters) => - async (account: IAccount) => { - const ponies = await findCharacters(account._id); - return ponies.map(toPonyObject); - }; + (findCharacters: FindCharacters) => + async (account: IAccount) => { + const ponies = await findCharacters(account._id); + return ponies.map(toPonyObject); + }; export const createUpdateAccount = - (findAccount: FindAccountSafe, log: LogAccount) => - async (account: IAccount, update: UpdateAccountData | undefined) => { - const a = await findAccount(account._id); + (findAccount: FindAccountSafe, log: LogAccount) => + async (account: IAccount, update: UpdateAccountData | undefined) => { + const a = await findAccount(account._id); - if (update) { - const fixed = fixUpdateAccountData(update); - const up: Partial = {}; + if (update) { + const fixed = fixUpdateAccountData(update); + const up: Partial = {}; - if (fixed.name && fixed.name !== a.name) { - up.name = fixed.name; - log(a._id, `Renamed "${a.name}" => "${fixed.name}"`); - } + if (fixed.name && fixed.name !== a.name) { + up.name = fixed.name; + log(a._id, `Renamed "${a.name}" => "${fixed.name}"`); + } - if (fixed.birthdate) { - const { day, month, year } = parseISODate(fixed.birthdate); - const date = createValidBirthDate(day, month, year); + if (fixed.birthdate) { + const { day, month, year } = parseISODate(fixed.birthdate); + const date = createValidBirthDate(day, month, year); - if ((date && a.birthdate && date.getTime() !== a.birthdate.getTime()) || !a.birthdate) { - up.birthdate = date; + if ((date && a.birthdate && date.getTime() !== a.birthdate.getTime()) || !a.birthdate) { + up.birthdate = date; - const from = a.birthdate ? `${formatISODate(a.birthdate)} (${getAge(a.birthdate)}yo)` : `undefined`; - const to = up.birthdate ? `${formatISODate(up.birthdate)} (${getAge(up.birthdate)}yo)` : `undefined`; - log(a._id, `Changed birthdate ${from} => ${to}`); - } - } + const from = a.birthdate ? `${formatISODate(a.birthdate)} (${getAge(a.birthdate)}yo)` : `undefined`; + const to = up.birthdate ? `${formatISODate(up.birthdate)} (${getAge(up.birthdate)}yo)` : `undefined`; + log(a._id, `Changed birthdate ${from} => ${to}`); + } + } - Object.assign(a, up); - await Account.updateOne({ _id: a._id }, up).exec(); - } + Object.assign(a, up); + await Account.updateOne({ _id: a._id }, up).exec(); + } - return toAccountData(a); - }; + return toAccountData(a); + }; export const createUpdateSettings = - (findAccount: FindAccountSafe) => - async (account: IAccount, settings: AccountSettings | undefined) => { - const a = await findAccount(account._id); - account.settings = a.settings = { ...a.settings, ...fixAccountSettings(settings) }; - await Account.updateOne({ _id: account._id }, { settings: account.settings }).exec(); - return toAccountData(a); - }; + (findAccount: FindAccountSafe) => + async (account: IAccount, settings: AccountSettings | undefined) => { + const a = await findAccount(account._id); + account.settings = a.settings = { ...a.settings, ...fixAccountSettings(settings) }; + await Account.updateOne({ _id: account._id }, { settings: account.settings }).exec(); + return toAccountData(a); + }; export const createRemoveSite = - (findAuth: FindAuth, countAllVisibleAuths: CountAuths, log: LogAccount) => - async (account: IAccount, siteId: unknown) => { - const [auth, auths] = await Promise.all([ - siteId && typeof siteId === 'string' ? findAuth(siteId, account._id) : Promise.resolve(undefined), - countAllVisibleAuths(account._id), - ]); + (findAuth: FindAuth, countAllVisibleAuths: CountAuths, log: LogAccount) => + async (account: IAccount, siteId: unknown) => { + const [auth, auths] = await Promise.all([ + siteId && typeof siteId === 'string' ? findAuth(siteId, account._id) : Promise.resolve(undefined), + countAllVisibleAuths(account._id), + ]); - if (!auth || auth.disabled) { - throw new UserError('Social account not found'); - } else if (auths === 1) { - throw new UserError('Cannot remove your only one social account'); - } else { - log(account._id, `removed auth: ${auth.name} [${auth._id}]`); - await Auth.updateOne({ _id: auth._id }, { disabled: true }).exec(); - } + if (!auth || auth.disabled) { + throw new UserError('Social account not found'); + } else if (auths === 1) { + throw new UserError('Cannot remove your only one social account'); + } else { + log(account._id, `removed auth: ${auth.name} [${auth._id}]`); + await Auth.updateOne({ _id: auth._id }, { disabled: true }).exec(); + } - return {}; - }; + return {}; + }; export async function removeHide(account: IAccount, hideId: string) { - await HideRequest.deleteOne({ source: account._id, _id: hideId }).exec(); + await HideRequest.deleteOne({ source: account._id, _id: hideId }).exec(); } diff --git a/src/ts/server/api/admin-accounts.ts b/src/ts/server/api/admin-accounts.ts index d4133f6..4edd0c8 100644 --- a/src/ts/server/api/admin-accounts.ts +++ b/src/ts/server/api/admin-accounts.ts @@ -1,8 +1,8 @@ import { noop, uniq, fromPairs } from 'lodash'; import { AccountCounters, Dict } from '../../common/interfaces'; import { - AccountUpdate, AccountState, FindAccountQuery, FindAccountResult, AdminCache, AdminCacheEntry, - Account as AccountInterface + AccountUpdate, AccountState, FindAccountQuery, FindAccountResult, AdminCache, AdminCacheEntry, + Account as AccountInterface } from '../../common/adminInterfaces'; import { checkIfNotAdmin } from '../accountUtils'; import { updateAccount, findAccountSafe, IAccount, MongoUpdate, findAccount, Account } from '../db'; @@ -15,181 +15,181 @@ import { AdminService } from '../services/adminService'; const banLogLimit = 10; async function updateAccountAndNotify(accountId: string, update: MongoUpdate) { - await updateAccount(accountId, update); - await accountChanged(accountId); + await updateAccount(accountId, update); + await accountChanged(accountId); } export async function timeoutAccount(accountId: string, timeout: Date, message?: string) { - const account = await findAccountSafe(accountId, 'roles mute shadow'); + const account = await findAccountSafe(accountId, 'roles mute shadow'); - checkIfNotAdmin(account, `timeout account: ${accountId}`); + checkIfNotAdmin(account, `timeout account: ${accountId}`); - const update: MongoUpdate = { mute: timeout.getTime() }; + const update: MongoUpdate = { mute: timeout.getTime() }; - if (!isMuted(account) && !isShadowed(account)) { - update.$inc = { 'counters.timeouts': 1 }; + if (!isMuted(account) && !isShadowed(account)) { + update.$inc = { 'counters.timeouts': 1 }; - if (message) { - update.$push = { - banLog: { - $each: [{ message, date: new Date() }], - $slice: -banLogLimit, - }, - }; - } - } + if (message) { + update.$push = { + banLog: { + $each: [{ message, date: new Date() }], + $slice: -banLogLimit, + }, + }; + } + } - await updateAccountAndNotify(accountId, update); + await updateAccountAndNotify(accountId, update); } function incrementAccountCounter(accountId: string, counter: keyof AccountCounters) { - return updateAccountAndNotify(accountId, { $inc: { [`counters.${counter}`]: 1 } }); + return updateAccountAndNotify(accountId, { $inc: { [`counters.${counter}`]: 1 } }); } export function updateAccountCounter(accountId: string, counter: keyof AccountCounters, value: number) { - return updateAccountAndNotify(accountId, { [`counters.${counter}`]: value }); + return updateAccountAndNotify(accountId, { [`counters.${counter}`]: value }); } let logSwearing: () => void = noop; let logSpamming: () => void = noop; export function initLogSwearingAndSpamming(swearing: typeof logSwearing, spamming: typeof logSpamming) { - logSwearing = swearing; - logSpamming = spamming; + logSwearing = swearing; + logSpamming = spamming; } export function reportSwearingAccount(accountId: string) { - logSwearing(); - return incrementAccountCounter(accountId, 'swears'); + logSwearing(); + return incrementAccountCounter(accountId, 'swears'); } export function reportSpammingAccount(accountId: string) { - logSpamming(); - return incrementAccountCounter(accountId, 'spam'); + logSpamming(); + return incrementAccountCounter(accountId, 'spam'); } export async function reportInviteLimitAccount(accountId: string) { - await incrementAccountCounter(accountId, 'inviteLimit'); - const account = await findAccountSafe(accountId, 'counters'); - return account.counters && account.counters.inviteLimit || 0; + await incrementAccountCounter(accountId, 'inviteLimit'); + const account = await findAccountSafe(accountId, 'counters'); + return account.counters && account.counters.inviteLimit || 0; } export async function reportFriendLimitAccount(accountId: string) { - await incrementAccountCounter(accountId, 'friendLimit'); - const account = await findAccountSafe(accountId, 'counters'); - return account.counters && account.counters.friendLimit || 0; + await incrementAccountCounter(accountId, 'friendLimit'); + const account = await findAccountSafe(accountId, 'counters'); + return account.counters && account.counters.friendLimit || 0; } export async function updateAccountSafe(accountId: string, update: AccountUpdate) { - const keys = Object.keys(update); - const allowAdmin = arraysEqual(keys, ['note']) || arraysEqual(keys, ['supporter']); - const account = await findAccountSafe(accountId); + const keys = Object.keys(update); + const allowAdmin = arraysEqual(keys, ['note']) || arraysEqual(keys, ['supporter']); + const account = await findAccountSafe(accountId); - if (!allowAdmin) { - checkIfNotAdmin(account, `update account: ${accountId}`); - } + if (!allowAdmin) { + checkIfNotAdmin(account, `update account: ${accountId}`); + } - const isNoteUpdate = 'note' in update && update.note !== account.note; - const accountUpdate = isNoteUpdate ? { ...update, noteUpdated: new Date() } : update; + const isNoteUpdate = 'note' in update && update.note !== account.note; + const accountUpdate = isNoteUpdate ? { ...update, noteUpdated: new Date() } : update; - await updateAccountAndNotify(accountId, accountUpdate); + await updateAccountAndNotify(accountId, accountUpdate); } export async function setRole(accountId: string, role: string, set: boolean, isSuperadmin: boolean) { - if (role === 'superadmin' || !isSuperadmin) { - throw new Error('Not allowed'); - } else { - await updateAccountAndNotify(accountId, set ? { $addToSet: { roles: [role] } } : { $pull: { roles: role } }); - } + if (role === 'superadmin' || !isSuperadmin) { + throw new Error('Not allowed'); + } else { + await updateAccountAndNotify(accountId, set ? { $addToSet: { roles: [role] } } : { $pull: { roles: role } }); + } } export function addEmail(accountId: string, email: string) { - return updateAccount(accountId, { $addToSet: { emails: [email.trim().toLowerCase()] } }); + return updateAccount(accountId, { $addToSet: { emails: [email.trim().toLowerCase()] } }); } export function removeEmail(accountId: string, email: string) { - return updateAccount(accountId, { $pull: { emails: email } }); + return updateAccount(accountId, { $pull: { emails: email } }); } export function removeIgnore(accountId: string, ignoredAccount: string) { - return updateAccountAndNotify(ignoredAccount, { $pull: { ignores: accountId } }); + return updateAccountAndNotify(ignoredAccount, { $pull: { ignores: accountId } }); } export function addIgnores(accountId: string, ignores: string[]) { - return updateAccountAndNotify(accountId, { $addToSet: { ignores } }); + return updateAccountAndNotify(accountId, { $addToSet: { ignores } }); } export function setAccountState(accountId: string, state: AccountState) { - return updateAccountAndNotify(accountId, { state }); + return updateAccountAndNotify(accountId, { state }); } function isValidCache(entry: AdminCacheEntry, query: string, duration: number): boolean { - return entry.query === query && entry.timestamp.getTime() > fromNow(-duration).getTime(); + return entry.query === query && entry.timestamp.getTime() > fromNow(-duration).getTime(); } export async function findAccounts( - cache: AdminCache, service: AdminService, { search, showOnly, not, page, itemsPerPage, force }: FindAccountQuery + cache: AdminCache, service: AdminService, { search, showOnly, not, page, itemsPerPage, force }: FindAccountQuery ): Promise { - const query = JSON.stringify({ search, showOnly, not }); - let found: AccountInterface[]; + const query = JSON.stringify({ search, showOnly, not }); + let found: AccountInterface[]; - if (force) { - cache.findAccounts = undefined; - } + if (force) { + cache.findAccounts = undefined; + } - if (cache.findAccounts && isValidCache(cache.findAccounts, query, 5 * MINUTE)) { - found = cache.findAccounts.result; - } else { - found = filterAccounts(service.accounts.items, search, showOnly, not); - cache.findAccounts = { - query, - result: found, - timestamp: new Date(), - }; - } + if (cache.findAccounts && isValidCache(cache.findAccounts, query, 5 * MINUTE)) { + found = cache.findAccounts.result; + } else { + found = filterAccounts(service.accounts.items, search, showOnly, not); + cache.findAccounts = { + query, + result: found, + timestamp: new Date(), + }; + } - const start = page * itemsPerPage; + const start = page * itemsPerPage; - return { - accounts: found.slice(start, start + itemsPerPage).map(a => a._id), - page, - totalItems: found.length, - }; + return { + accounts: found.slice(start, start + itemsPerPage).map(a => a._id), + page, + totalItems: found.length, + }; } export function getAccountsByEmail(service: AdminService, email: string) { - email = email.toLowerCase(); - const name = emailName(email); - const accounts = service.getAccountsByEmailName(name) || []; - return accounts.filter(a => includes(a.emails, email)).map(a => a._id); + email = email.toLowerCase(); + const name = emailName(email); + const accounts = service.getAccountsByEmailName(name) || []; + return accounts.filter(a => includes(a.emails, email)).map(a => a._id); } export function getAccountsByEmails(service: AdminService, emails: string[]): Dict { - const pairs = uniq(emails) - .map(email => [email, getAccountsByEmail(service, email)] as [string, string[]]) - .filter(([_, accounts]) => accounts.length > 0); + const pairs = uniq(emails) + .map(email => [email, getAccountsByEmail(service, email)] as [string, string[]]) + .filter(([_, accounts]) => accounts.length > 0); - return fromPairs(pairs); + return fromPairs(pairs); } export function getAccountsByOrigin(service: AdminService, ip: string): string[] { - const origin = service.origins.get(ip); - return origin && origin.accounts && origin.accounts.map(a => a._id) || []; + const origin = service.origins.get(ip); + return origin && origin.accounts && origin.accounts.map(a => a._id) || []; } export async function removeAccount(service: AdminService, accountId: string) { - const account = await findAccount(accountId); + const account = await findAccount(accountId); - if (account) { - checkIfNotAdmin(account, `remove account: ${accountId}`); + if (account) { + checkIfNotAdmin(account, `remove account: ${accountId}`); - await account.remove(); + await account.remove(); - service.removedItem('accounts', accountId); - } + service.removedItem('accounts', accountId); + } } export async function setAccountAlert(accountId: string, message: string, expires: Date) { - const update = message ? { alert: { message, expires } } : { $unset: { alert: 1 } }; - await Account.updateOne({ _id: accountId }, update).exec(); + const update = message ? { alert: { message, expires } } : { $unset: { alert: 1 } }; + await Account.updateOne({ _id: accountId }, update).exec(); } diff --git a/src/ts/server/api/admin-auths.ts b/src/ts/server/api/admin-auths.ts index 592efcd..5884c08 100644 --- a/src/ts/server/api/admin-auths.ts +++ b/src/ts/server/api/admin-auths.ts @@ -3,34 +3,34 @@ import { Auth, Account } from '../db'; import { checkIfNotAdmin } from '../accountUtils'; export async function assignAuth(authId: string, accountId: string) { - const auth = await Auth.findById(authId).exec(); + const auth = await Auth.findById(authId).exec(); - if (!auth) - return; + if (!auth) + return; - const [src, dest] = await Promise.all([ - Account.findById(auth.account).exec(), - Account.findById(accountId).exec(), - ]); + const [src, dest] = await Promise.all([ + Account.findById(auth.account).exec(), + Account.findById(accountId).exec(), + ]); - src && checkIfNotAdmin(src, `assign auth from ${src._id}`); - dest && checkIfNotAdmin(dest, `assign auth to ${dest._id}`); + src && checkIfNotAdmin(src, `assign auth from ${src._id}`); + dest && checkIfNotAdmin(dest, `assign auth to ${dest._id}`); - await Auth.updateOne({ _id: authId }, { account: accountId }).exec(); + await Auth.updateOne({ _id: authId }, { account: accountId }).exec(); } export async function removeAuth(service: AdminService, authId: string) { - const auth = await Auth.findById(authId).exec(); + const auth = await Auth.findById(authId).exec(); - if (!auth) - return; + if (!auth) + return; - if (auth.account) { - const account = await Account.findById(auth.account).exec(); - account && checkIfNotAdmin(account, `remove auth from ${account._id}`); - } + if (auth.account) { + const account = await Account.findById(auth.account).exec(); + account && checkIfNotAdmin(account, `remove auth from ${account._id}`); + } - await Auth.deleteOne({ _id: authId }).exec(); + await Auth.deleteOne({ _id: authId }).exec(); - service.auths.removed(authId); + service.auths.removed(authId); } diff --git a/src/ts/server/api/admin.ts b/src/ts/server/api/admin.ts index 088926d..2abed39 100644 --- a/src/ts/server/api/admin.ts +++ b/src/ts/server/api/admin.ts @@ -1,13 +1,13 @@ import * as fs from 'fs'; import * as moment from 'moment'; import { - AdminState, eventFields, BaseValues, UpdateOrigin, UserCountStats, AccountDetails, SupporterInvite, - InternalGameServerState, InternalLoginServerState, OtherStats, Settings, GameServerSettings + AdminState, eventFields, BaseValues, UpdateOrigin, UserCountStats, AccountDetails, SupporterInvite, + InternalGameServerState, InternalLoginServerState, OtherStats, Settings, GameServerSettings } from '../../common/adminInterfaces'; import { execAsync } from '../serverUtils'; import { - IAccount, Account, Origin, Event, iterate, ISession, Session, SupporterInvite as DBSupporterInvite, - findAccount, ISupporterInvite, ID + IAccount, Account, Origin, Event, iterate, ISession, Session, SupporterInvite as DBSupporterInvite, + findAccount, ISupporterInvite, ID } from '../db'; import { servers, serverStatus, loginServers } from '../internal'; import { encodeEvent, BaseTimes, getBaseDate, getBaseTimes } from '../adminEncoders'; @@ -19,280 +19,280 @@ import { loadSettings, saveSettings } from '../settings'; import { flatten } from '../../common/utils'; function encodeItems(items: T[], base: BaseValues, encode: (items: T, base: BaseTimes) => any[]): any[][] { - const baseValues = getBaseTimes(base); - return items.map(i => encode(i, baseValues)); + const baseValues = getBaseTimes(base); + return items.map(i => encode(i, baseValues)); } const events = createLiveEndPoint({ - model: Event, - fields: eventFields, - encode(items, base) { - base.createdAt = getBaseDate(items, i => i.createdAt!); - base.updatedAt = getBaseDate(items, i => i.updatedAt); - return encodeItems(items, base, encodeEvent); - }, + model: Event, + fields: eventFields, + encode(items, base) { + base.createdAt = getBaseDate(items, i => i.createdAt!); + base.updatedAt = getBaseDate(items, i => i.updatedAt); + return encodeItems(items, base, encodeEvent); + }, }); export interface EndPoints { - events: LiveEndPoint; + events: LiveEndPoint; } export function createEndPoints(): EndPoints { - return { events }; + return { events }; } export function getAdminState(): AdminState { - return { - status: serverStatus, - loginServers: loginServers.map(s => s.state), - gameServers: servers.map(s => s.state), - }; + return { + status: serverStatus, + loginServers: loginServers.map(s => s.state), + gameServers: servers.map(s => s.state), + }; } async function forAllLoginServers( - action: (server: InternalLoginServerState) => any, filter = (_: InternalLoginServerState) => true + action: (server: InternalLoginServerState) => any, filter = (_: InternalLoginServerState) => true ) { - await Promise.all(loginServers.filter(filter).map(action)); + await Promise.all(loginServers.filter(filter).map(action)); } export async function forAllGameServers( - action: (server: InternalGameServerState) => any, filter = (_: InternalGameServerState) => true + action: (server: InternalGameServerState) => any, filter = (_: InternalGameServerState) => true ) { - const liveServers = servers.filter(s => !s.state.dead); - await Promise.all(liveServers.filter(filter).map(action)); + const liveServers = servers.filter(s => !s.state.dead); + await Promise.all(liveServers.filter(filter).map(action)); } export function actionForAllServers(action: string, accountId: string) { - return forAllGameServers(s => s.api.action(action, accountId)); + return forAllGameServers(s => s.api.action(action, accountId)); } export function kickFromAllServers(accountId: string) { - return forAllGameServers(s => s.api.kick(accountId, undefined)); + return forAllGameServers(s => s.api.kick(accountId, undefined)); } export function kickFromAllServersByCharacter(characterId: string) { - return forAllGameServers(s => s.api.kick(undefined, characterId)); + return forAllGameServers(s => s.api.kick(undefined, characterId)); } function createFilter(id: string) { - return (server: { id: string; }) => id === '*' || server.id === id; + return (server: { id: string; }) => id === '*' || server.id === id; } export async function notifyUpdate(server: string) { - await Promise.all([ - forAllLoginServers(s => s.api.updateLiveSettings({ updating: true }), createFilter(server)), - forAllGameServers(s => s.api.notifyUpdate(), createFilter(server)), - ]); + await Promise.all([ + forAllLoginServers(s => s.api.updateLiveSettings({ updating: true }), createFilter(server)), + forAllGameServers(s => s.api.notifyUpdate(), createFilter(server)), + ]); } export function shutdownServers(server: string, value: boolean) { - return forAllGameServers(s => s.api.shutdownServer(value), createFilter(server)); + return forAllGameServers(s => s.api.shutdownServer(value), createFilter(server)); } export async function resetUpdating(server: string) { - await Promise.all([ - forAllLoginServers(s => s.api.updateLiveSettings({ updating: false }), createFilter(server)), - forAllGameServers(s => s.api.cancelUpdate(), createFilter(server)), - shutdownServers(server, false), - ]); + await Promise.all([ + forAllLoginServers(s => s.api.updateLiveSettings({ updating: false }), createFilter(server)), + forAllGameServers(s => s.api.cancelUpdate(), createFilter(server)), + shutdownServers(server, false), + ]); } export async function reloadSettingsOnAllServers() { - await Promise.all([ - forAllLoginServers(s => s.api.reloadSettings()), - forAllGameServers(s => s.api.reloadSettings()), - ]); + await Promise.all([ + forAllLoginServers(s => s.api.reloadSettings()), + forAllGameServers(s => s.api.reloadSettings()), + ]); } export async function getChat(search: string, date: string, caseInsensitive: boolean) { - const query = search - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\./g, '\\.') - .replace(/\*/g, '\\*') - .replace(/\$/g, '\\*') - .replace(/\^/g, '\\*'); - const flags = caseInsensitive ? '-i -E ' : '-E '; - const options = { maxBuffer: 1 * 1024 * 1024 }; // 1MB + const query = search + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\./g, '\\.') + .replace(/\*/g, '\\*') + .replace(/\$/g, '\\*') + .replace(/\^/g, '\\*'); + const flags = caseInsensitive ? '-i -E ' : '-E '; + const options = { maxBuffer: 1 * 1024 * 1024 }; // 1MB - async function fetchChatlog(lines: number) { - const logFile = paths.pathTo('logs', `info.${moment(date).format('YYYYMMDD')}.log`); - const { stdout } = await execAsync(`grep ${flags}"${query}" "${logFile}" | tail -n ${lines}`, options); - return stdout; - } + async function fetchChatlog(lines: number) { + const logFile = paths.pathTo('logs', `info.${moment(date).format('YYYYMMDD')}.log`); + const { stdout } = await execAsync(`grep ${flags}"${query}" "${logFile}" | tail -n ${lines}`, options); + return stdout; + } - try { - if (!search) { - return ''; - } else if (date === 'all') { - const { stdout } = await execAsync(`for f in ${paths.pathTo('logs')}/*.log; do ` - + `echo "$f" | grep -o '[0-9]*';` - + `cat "$f" | grep ${flags}"${query}";` - + `done`, options); - return stdout; - } else { - let lines = 8192; - let more = ''; + try { + if (!search) { + return ''; + } else if (date === 'all') { + const { stdout } = await execAsync(`for f in ${paths.pathTo('logs')}/*.log; do ` + + `echo "$f" | grep -o '[0-9]*';` + + `cat "$f" | grep ${flags}"${query}";` + + `done`, options); + return stdout; + } else { + let lines = 8192; + let more = ''; - do { - try { - const log = await fetchChatlog(lines); - return more + log; - } catch (e) { - if (e.message !== 'stdout maxBuffer exceeded') { - throw e; - } - } + do { + try { + const log = await fetchChatlog(lines); + return more + log; + } catch (e) { + if (e.message !== 'stdout maxBuffer exceeded') { + throw e; + } + } - lines /= 2; - more = '... more lines ...\n'; - } while (lines > 1); + lines /= 2; + more = '... more lines ...\n'; + } while (lines > 1); - return ''; - } - } catch (e) { - console.error('Failed to fetch chatlog: ', e); - return ''; - } + return ''; + } + } catch (e) { + console.error('Failed to fetch chatlog: ', e); + return ''; + } } export async function getChatForAccounts(accountIds: string[], date: string) { - const accounts: Partial[] = await Account.find({ _id: { $in: accountIds } }, '_id merges').lean().exec(); - const map = new Map(); - const ids = flatten(accounts.map(a => [a._id.toString(), ...(a.merges || []).map(a => a.id)])); + const accounts: Partial[] = await Account.find({ _id: { $in: accountIds } }, '_id merges').lean().exec(); + const map = new Map(); + const ids = flatten(accounts.map(a => [a._id.toString(), ...(a.merges || []).map(a => a.id)])); - for (const a of accounts) { - const index = accountIds.indexOf(a._id.toString()); - map.set(a._id.toString(), index ? `[${index}]` : ``); + for (const a of accounts) { + const index = accountIds.indexOf(a._id.toString()); + map.set(a._id.toString(), index ? `[${index}]` : ``); - (a.merges || []).forEach(({ id }) => { - map.set(id, index ? `[${index}:merged]` : `[merged]`); - }); - } + (a.merges || []).forEach(({ id }) => { + map.set(id, index ? `[${index}:merged]` : `[merged]`); + }); + } - const chat = await getChat(ids.join('|'), date, false); - const fixed = chat.replace(/^([0-9:]+) \[([a-f0-9]{24})\]/gmu, (_, date, id) => - `${date} ${map.has(id) ? map.get(id) : `[${id}]`}`); + const chat = await getChat(ids.join('|'), date, false); + const fixed = chat.replace(/^([0-9:]+) \[([a-f0-9]{24})\]/gmu, (_, date, id) => + `${date} ${map.has(id) ? map.get(id) : `[${id}]`}`); - return fixed; + return fixed; } export async function clearSessions(accountId: string) { - const clearIds: string[] = []; + const clearIds: string[] = []; - await iterate(Session.find({ session: { $exists: true } }).lean() as any, session => { - try { - if (session.session) { - const data = JSON.parse(session.session); - const user = data && data.passport && data.passport.user; + await iterate(Session.find({ session: { $exists: true } }).lean() as any, session => { + try { + if (session.session) { + const data = JSON.parse(session.session); + const user = data && data.passport && data.passport.user; - if (user === accountId) { - clearIds.push(session._id); - } - } - } catch (e) { - logger.error('Error when claring session', e, session._id, session.session); - } - }); + if (user === accountId) { + clearIds.push(session._id); + } + } + } catch (e) { + logger.error('Error when claring session', e, session._id, session.session); + } + }); - await Session.deleteOne({ _id: { $in: clearIds } }).exec(); + await Session.deleteOne({ _id: { $in: clearIds } }).exec(); } export async function updateOrigin(update: UpdateOrigin) { - await Origin.updateOne({ ip: update.ip }, update, { upsert: true }).exec(); + await Origin.updateOne({ ip: update.ip }, update, { upsert: true }).exec(); } export async function getUserCounts(): Promise { - const statsFile = paths.pathTo('settings', `user-counts.log`); + const statsFile = paths.pathTo('settings', `user-counts.log`); - try { - const content = await fs.readFileAsync(statsFile, 'utf8'); - const lines = content.trim().split(/\n/); - return lines.map(line => JSON.parse(line)); - } catch { - return []; - } + try { + const content = await fs.readFileAsync(statsFile, 'utf8'); + const lines = content.trim().split(/\n/); + return lines.map(line => JSON.parse(line)); + } catch { + return []; + } } function convertInvite(invite: ISupporterInvite): SupporterInvite { - return { - _id: invite._id.toString(), - name: invite.name, - info: invite.info, - source: invite.source.toHexString(), - target: invite.target.toHexString(), - active: invite.active, - updatedAt: invite.updatedAt, - createdAt: invite.createdAt, - }; + return { + _id: invite._id.toString(), + name: invite.name, + info: invite.info, + source: invite.source.toHexString(), + target: invite.target.toHexString(), + active: invite.active, + updatedAt: invite.updatedAt, + createdAt: invite.createdAt, + }; } export async function getAccountDetails(accountId: ID): Promise { - const [account, invitesReceived, invitesSent] = await Promise.all([ - findAccount(accountId, 'merges supporterLog banLog state'), - DBSupporterInvite.find({ target: accountId }).exec(), - DBSupporterInvite.find({ source: accountId }).exec(), - ]); + const [account, invitesReceived, invitesSent] = await Promise.all([ + findAccount(accountId, 'merges supporterLog banLog state'), + DBSupporterInvite.find({ target: accountId }).exec(), + DBSupporterInvite.find({ source: accountId }).exec(), + ]); - return account ? { - merges: account.merges || [], - banLog: account.banLog || [], - supporterLog: account.supporterLog || [], - invitesReceived: invitesReceived.map(convertInvite), - invitesSent: invitesSent.map(convertInvite), - state: account.state || {}, - } : { - merges: [], - banLog: [], - supporterLog: [], - invitesReceived: [], - invitesSent: [], - state: {}, - }; + return account ? { + merges: account.merges || [], + banLog: account.banLog || [], + supporterLog: account.supporterLog || [], + invitesReceived: invitesReceived.map(convertInvite), + invitesSent: invitesSent.map(convertInvite), + state: account.state || {}, + } : { + merges: [], + banLog: [], + supporterLog: [], + invitesReceived: [], + invitesSent: [], + state: {}, + }; } export async function getOtherStats(service: AdminService): Promise { - let totalIgnores = 0; - let authsWithEmptyAccount = 0; - let authsWithMissingAccount = 0; + let totalIgnores = 0; + let authsWithEmptyAccount = 0; + let authsWithMissingAccount = 0; - for (const account of service.accounts.items) { - totalIgnores += account.ignoresCount!; - } + for (const account of service.accounts.items) { + totalIgnores += account.ignoresCount!; + } - for (const auth of service.auths.items) { - if (!auth.account) { - authsWithEmptyAccount++; - } else if (!service.accounts.get(auth.account)) { - authsWithMissingAccount++; - } - } + for (const auth of service.auths.items) { + if (!auth.account) { + authsWithEmptyAccount++; + } else if (!service.accounts.get(auth.account)) { + authsWithMissingAccount++; + } + } - return { - totalIgnores, - authsWithEmptyAccount, - authsWithMissingAccount, - }; + return { + totalIgnores, + authsWithEmptyAccount, + authsWithMissingAccount, + }; } export async function updateServerSettings(currentSettings: Settings, update: Partial) { - const settings = await Promise.resolve(loadSettings()); - Object.assign(currentSettings, settings, update); + const settings = await Promise.resolve(loadSettings()); + Object.assign(currentSettings, settings, update); - await saveSettings(currentSettings); - await reloadSettingsOnAllServers(); + await saveSettings(currentSettings); + await reloadSettingsOnAllServers(); } export async function updateGameServerSettings( - currentSettings: Settings, serverId: string, update: Partial + currentSettings: Settings, serverId: string, update: Partial ) { - const settings = await Promise.resolve(loadSettings()); - Object.assign(currentSettings, settings); + const settings = await Promise.resolve(loadSettings()); + Object.assign(currentSettings, settings); - const serverSettings = currentSettings.servers[serverId] = currentSettings.servers[serverId] || {}; - Object.assign(serverSettings, update); + const serverSettings = currentSettings.servers[serverId] = currentSettings.servers[serverId] || {}; + Object.assign(serverSettings, update); - await saveSettings(currentSettings); - await reloadSettingsOnAllServers(); + await saveSettings(currentSettings); + await reloadSettingsOnAllServers(); } diff --git a/src/ts/server/api/duplicates.ts b/src/ts/server/api/duplicates.ts index 9608f99..2ab6dc8 100644 --- a/src/ts/server/api/duplicates.ts +++ b/src/ts/server/api/duplicates.ts @@ -1,6 +1,6 @@ import { groupBy, uniq, compact } from 'lodash'; import { - duplicatesCollector, emailName, compareDuplicates, getIdsFromNote, createDuplicateResult + duplicatesCollector, emailName, compareDuplicates, getIdsFromNote, createDuplicateResult } from '../../common/adminUtils'; import { Account, Origin, DuplicatesInfo, DuplicateResult } from '../../common/adminInterfaces'; import { HOUR } from '../../common/constants'; @@ -17,171 +17,171 @@ let duplicateEntries: string[] | undefined = undefined; let duplicateTimestamp = 0; export async function getDuplicateEntries(accounts: Account[], force: boolean) { - if (!duplicateEntries || force || (Date.now() - duplicateTimestamp) > DUPLICATE_TIMEOUT) { - duplicateTimestamp = Date.now(); - duplicateEntries = [ - ...getDuplicateEmails(accounts), - // ...getDuplicateAuths(accounts), - ]; - } + if (!duplicateEntries || force || (Date.now() - duplicateTimestamp) > DUPLICATE_TIMEOUT) { + duplicateTimestamp = Date.now(); + duplicateEntries = [ + ...getDuplicateEmails(accounts), + // ...getDuplicateAuths(accounts), + ]; + } - return duplicateEntries; + return duplicateEntries; } export function getDuplicateEmails(accounts: Account[]) { - const duplicates: string[] = []; - const collect = duplicatesCollector(duplicates); - accounts.forEach(a => a.emails !== undefined && a.emails.forEach(collect)); - return duplicates; + const duplicates: string[] = []; + const collect = duplicatesCollector(duplicates); + accounts.forEach(a => a.emails !== undefined && a.emails.forEach(collect)); + return duplicates; } export function getDuplicateAuths(accounts: Account[]) { - const duplicates: string[] = []; - const collect = duplicatesCollector(duplicates); - accounts.forEach(a => a.auths !== undefined && a.auths.forEach(a => a.url && collect(a.url))); - return duplicates; + const duplicates: string[] = []; + const collect = duplicatesCollector(duplicates); + accounts.forEach(a => a.auths !== undefined && a.auths.forEach(a => a.url && collect(a.url))); + return duplicates; } // get duplicate info export async function getDuplicateInfo(accountId: string, otherAccounts: string[]): Promise { - const ids = [accountId, ...otherAccounts]; - const [chars, accounts] = await Promise.all([ - Character.find({ account: ids }, 'account name').lean().exec() as Promise, - DBAccount.find({ _id: ids }, '_id lastUserAgent').lean().exec() as Promise, - ]); + const ids = [accountId, ...otherAccounts]; + const [chars, accounts] = await Promise.all([ + Character.find({ account: ids }, 'account name').lean().exec() as Promise, + DBAccount.find({ _id: ids }, '_id lastUserAgent').lean().exec() as Promise, + ]); - chars.forEach(c => c.name = c.name.toLowerCase()); - const groups = groupBy(chars, c => c.account); - const account = accounts.find(a => a._id.toString() === accountId); - const userAgent = account && account.lastUserAgent || ''; + chars.forEach(c => c.name = c.name.toLowerCase()); + const groups = groupBy(chars, c => c.account); + const account = accounts.find(a => a._id.toString() === accountId); + const userAgent = account && account.lastUserAgent || ''; - return otherAccounts.map(id => { - const account = accounts.find(a => a._id.toString() === id); + return otherAccounts.map(id => { + const account = accounts.find(a => a._id.toString() === id); - return { - account: id, - userAgent: (account && userAgent && account.lastUserAgent === userAgent) ? userAgent : '', - ponies: getDuplicateNames(groups[accountId], groups[id]), - }; - }); + return { + account: id, + userAgent: (account && userAgent && account.lastUserAgent === userAgent) ? userAgent : '', + ponies: getDuplicateNames(groups[accountId], groups[id]), + }; + }); } function getDuplicateNames(mine: ICharacter[] = [], others: ICharacter[] = []): string[] { - return uniq(mine.filter(a => others.some(b => a.name === b.name)).map(c => c.name)); + return uniq(mine.filter(a => others.some(b => a.name === b.name)).map(c => c.name)); } // get all duplicates export async function getAllDuplicatesQuickInfo(service: AdminService, accountId: string): Promise { - const duplicates = await getAllDuplicates(service, accountId); + const duplicates = await getAllDuplicates(service, accountId); - return { - generatedAt: Date.now(), - count: duplicates.length, - name: duplicates.some(d => !!d.name), - emails: duplicates.some(d => !!d.emails), - browserId: duplicates.some(d => !!d.browserId), - perma: duplicates.some(d => !!d.perma), - }; + return { + generatedAt: Date.now(), + count: duplicates.length, + name: duplicates.some(d => !!d.name), + emails: duplicates.some(d => !!d.emails), + browserId: duplicates.some(d => !!d.browserId), + perma: duplicates.some(d => !!d.perma), + }; } export async function getAllDuplicatesWithInfo(service: AdminService, accountId: string) { - const duplicates = await getAllDuplicates(service, accountId); - const accountIds = duplicates.map(x => x.account); - const duplicatesInfo = await getDuplicateInfo(accountId, accountIds); + const duplicates = await getAllDuplicates(service, accountId); + const accountIds = duplicates.map(x => x.account); + const duplicatesInfo = await getDuplicateInfo(accountId, accountIds); - duplicatesInfo.forEach(({ account, ponies, userAgent }) => { - const duplicate = duplicates.find(d => d.account === account); + duplicatesInfo.forEach(({ account, ponies, userAgent }) => { + const duplicate = duplicates.find(d => d.account === account); - if (duplicate) { - duplicate.ponies = ponies; - duplicate.userAgent = userAgent; - } - }); + if (duplicate) { + duplicate.ponies = ponies; + duplicate.userAgent = userAgent; + } + }); - duplicates.forEach(d => d.ponies = d.ponies || []); + duplicates.forEach(d => d.ponies = d.ponies || []); - return duplicates; + return duplicates; } async function getAllDuplicates(service: AdminService, accountId: string): Promise { - const account = service.accounts.get(accountId); + const account = service.accounts.get(accountId); - if (!account) { - return []; - } else { - return uniq([ - ...getDuplicatesByNote(service, account), - ...getDuplicatesByEmail(service, account), - ...getDuplicatesByBrowserId(service, account), - ...getDuplicates(account), - ]) - .filter(a => a !== account) - .map(a => createDuplicateResult(a, account)) - .sort(compareDuplicates) - .slice(0, 50); - } + if (!account) { + return []; + } else { + return uniq([ + ...getDuplicatesByNote(service, account), + ...getDuplicatesByEmail(service, account), + ...getDuplicatesByBrowserId(service, account), + ...getDuplicates(account), + ]) + .filter(a => a !== account) + .map(a => createDuplicateResult(a, account)) + .sort(compareDuplicates) + .slice(0, 50); + } } function getDuplicates(account: Account) { - const accounts: Account[] = []; - const origins: Origin[] = []; - removeItem(accounts, account); - collectDuplicates(accounts, origins, account, 3); - return accounts; + const accounts: Account[] = []; + const origins: Origin[] = []; + removeItem(accounts, account); + collectDuplicates(accounts, origins, account, 3); + return accounts; } function getDuplicatesByNote(service: AdminService, account: Account) { - const linkedTo = compact(getIdsFromNote(account.note).map(id => service.accounts.get(id))); - const linkedFrom = service.getAccountsByNoteRef(account._id); - return uniqueOtherAccounts([...linkedTo, ...linkedFrom], account); + const linkedTo = compact(getIdsFromNote(account.note).map(id => service.accounts.get(id))); + const linkedFrom = service.getAccountsByNoteRef(account._id); + return uniqueOtherAccounts([...linkedTo, ...linkedFrom], account); } function getDuplicatesByEmail(service: AdminService, account: Account) { - const accounts = (account.emails || []) - .map(emailName) - .map(name => service.getAccountsByEmailName(name)); - return uniqueOtherAccounts(flatten(accounts), account); + const accounts = (account.emails || []) + .map(emailName) + .map(name => service.getAccountsByEmailName(name)); + return uniqueOtherAccounts(flatten(accounts), account); } function getDuplicatesByBrowserId(service: AdminService, account: Account) { - const browserId = account.lastBrowserId; - const accounts = browserId && service.getAccountsByBrowserId(browserId) || []; - return uniqueOtherAccounts(accounts, account); + const browserId = account.lastBrowserId; + const accounts = browserId && service.getAccountsByBrowserId(browserId) || []; + return uniqueOtherAccounts(accounts, account); } function uniqueOtherAccounts(accounts: Account[], exclude: Account) { - return uniq(accounts.filter(a => a !== exclude)); + return uniq(accounts.filter(a => a !== exclude)); } function collectDuplicates(accounts: Account[], origins: Origin[], account: Account, level: number) { - if (level > 0 && !includes(accounts, account)) { - accounts.push(account); - account.originsRefs!.forEach(o => { - if (!includes(origins, o.origin)) { - origins.push(o.origin); + if (level > 0 && !includes(accounts, account)) { + accounts.push(account); + account.originsRefs!.forEach(o => { + if (!includes(origins, o.origin)) { + origins.push(o.origin); - if (o.origin.accounts) { - o.origin.accounts.forEach(a => collectDuplicates(accounts, origins, a, level - 1)); - } - } - }); - } + if (o.origin.accounts) { + o.origin.accounts.forEach(a => collectDuplicates(accounts, origins, a, level - 1)); + } + } + }); + } } // unused export function getDuplicateEmailNames(accounts: Account[]) { - const set = new Set(); + const set = new Set(); - return uniq(accounts.reduce((duplicates, a) => { - if (a.emails !== undefined && a.emails.length > 0) { - const names = a.emails.map(e => e.replace(/@.+$/, '')); - duplicates.push(...names.filter(name => set.has(name))); - names.forEach(name => set.add(name)); - } + return uniq(accounts.reduce((duplicates, a) => { + if (a.emails !== undefined && a.emails.length > 0) { + const names = a.emails.map(e => e.replace(/@.+$/, '')); + duplicates.push(...names.filter(name => set.has(name))); + names.forEach(name => set.add(name)); + } - return duplicates; - }, [])); + return duplicates; + }, [])); } diff --git a/src/ts/server/api/game.ts b/src/ts/server/api/game.ts index e2f4b9c..67daa35 100644 --- a/src/ts/server/api/game.ts +++ b/src/ts/server/api/game.ts @@ -10,85 +10,85 @@ import { isServerOffline } from '../serverUtils'; import { getAccountAlertMessage } from '../accountUtils'; export interface Config { - version: string; - host: string; - debug: boolean; - local: boolean; + version: string; + host: string; + debug: boolean; + local: boolean; } export type FindServer = (id: string) => InternalGameServerState | undefined; export type Join = (server: InternalGameServerState, account: IAccount, pony: ICharacter) => Promise; export type AddOrigin = (account: IAccount, origin: IOriginInfo) => Promise; export type JoinGame = ( - account: IAccount, characterId: string, serverId: string, clientVersion: string, url: string, alert: unknown, - origin: IOriginInfo + account: IAccount, characterId: string, serverId: string, clientVersion: string, url: string, alert: unknown, + origin: IOriginInfo ) => Promise; export const createJoinGame = - ( - findServer: FindServer, { version, host, debug, local }: Config, findCharacter: FindCharacter, join: Join, - addOrigin: AddOrigin, hasInvites: HasActiveSupporterInvites - ): JoinGame => { - const waiting = new Map(); + ( + findServer: FindServer, { version, host, debug, local }: Config, findCharacter: FindCharacter, join: Join, + addOrigin: AddOrigin, hasInvites: HasActiveSupporterInvites + ): JoinGame => { + const waiting = new Map(); - return async (account, characterId, serverId, clientVersion, url, hasAlert, origin) => { - const accountId = account._id.toString(); + return async (account, characterId, serverId, clientVersion, url, hasAlert, origin) => { + const accountId = account._id.toString(); - try { - const [server, supporterInvited] = await Promise.all([ - findServer(serverId), - hasInvites(account._id), - ]); + try { + const [server, supporterInvited] = await Promise.all([ + findServer(serverId), + hasInvites(account._id), + ]); - if (clientVersion !== version) - throw new UserError(VERSION_ERROR); + if (clientVersion !== version) + throw new UserError(VERSION_ERROR); - if (parse(url).host !== parse(host).host && !debug && !local) - throw new UserError('Invalid data', { message: 'Invalid host', desc: url }); + if (parse(url).host !== parse(host).host && !debug && !local) + throw new UserError('Invalid data', { message: 'Invalid host', desc: url }); - if (!server) - throw new UserError('Invalid data'); + if (!server) + throw new UserError('Invalid data'); - if (isServerOffline(server)) - throw new UserError('Server is offline'); + if (isServerOffline(server)) + throw new UserError('Server is offline'); - if (server.state.settings.blockJoining) - throw new UserError('Cannot join to the server'); + if (server.state.settings.blockJoining) + throw new UserError('Cannot join to the server'); - if (!meetsRequirement({ roles: account.roles, supporter: supporterLevel(account), supporterInvited }, server.state.require)) - throw new UserError('Server is restricted'); + if (!meetsRequirement({ roles: account.roles, supporter: supporterLevel(account), supporterInvited }, server.state.require)) + throw new UserError('Server is restricted'); - if (!characterId || typeof characterId !== 'string') - throw new UserError('Invalid data', { message: 'Invalid pony ID', desc: `"${characterId}"` }); + if (!characterId || typeof characterId !== 'string') + throw new UserError('Invalid data', { message: 'Invalid pony ID', desc: `"${characterId}"` }); - const req = waiting.get(accountId); - const time = new Date(); + const req = waiting.get(accountId); + const time = new Date(); - if (req) { - throw new UserError('Already waiting for join request'); - } + if (req) { + throw new UserError('Already waiting for join request'); + } - const alert = getAccountAlertMessage(account); + const alert = getAccountAlertMessage(account); - if (alert && !hasAlert) { - return { alert }; - } + if (alert && !hasAlert) { + return { alert }; + } - waiting.set(accountId, { characterId, time }); + waiting.set(accountId, { characterId, time }); - const character = await findCharacter(characterId, account._id); + const character = await findCharacter(characterId, account._id); - if (!character) { - throw new UserError('Character does not exist', { - desc: `(join) (account: ${accountId} pony: ${characterId})` - }); - } + if (!character) { + throw new UserError('Character does not exist', { + desc: `(join) (account: ${accountId} pony: ${characterId})` + }); + } - await addOrigin(account, origin); - const token = await join(server, account, character!); - return { token }; - } finally { - waiting.delete(accountId); - } - }; - }; + await addOrigin(account, origin); + const token = await join(server, account, character!); + return { token }; + } finally { + waiting.delete(accountId); + } + }; + }; diff --git a/src/ts/server/api/internal-admin.ts b/src/ts/server/api/internal-admin.ts index 064d541..e5b5593 100644 --- a/src/ts/server/api/internal-admin.ts +++ b/src/ts/server/api/internal-admin.ts @@ -2,13 +2,13 @@ import { EndPoints } from './admin'; import { AdminService } from '../services/adminService'; export class InternalAdminApi { - constructor(private adminService: AdminService, private endPoints: EndPoints) { - } - removedDocument(model: 'events' | 'ponies' | 'accounts' | 'auths' | 'origins', id: string) { - if (model in this.endPoints) { - (this.endPoints as any)[model].removedItem(id); - } - this.adminService.removedItem(model, id); - return Promise.resolve(); - } + constructor(private adminService: AdminService, private endPoints: EndPoints) { + } + removedDocument(model: 'events' | 'ponies' | 'accounts' | 'auths' | 'origins', id: string) { + if (model in this.endPoints) { + (this.endPoints as any)[model].removedItem(id); + } + this.adminService.removedItem(model, id); + return Promise.resolve(); + } } diff --git a/src/ts/server/api/internal-common.ts b/src/ts/server/api/internal-common.ts index 9c357d9..d4a62f1 100644 --- a/src/ts/server/api/internal-common.ts +++ b/src/ts/server/api/internal-common.ts @@ -1,4 +1,4 @@ export const createReloadSettings = - (reloadSettings: () => Promise) => - async () => - await reloadSettings(); + (reloadSettings: () => Promise) => + async () => + await reloadSettings(); diff --git a/src/ts/server/api/internal-login.ts b/src/ts/server/api/internal-login.ts index 02bcdc4..4b40440 100644 --- a/src/ts/server/api/internal-login.ts +++ b/src/ts/server/api/internal-login.ts @@ -5,58 +5,58 @@ import { createReloadSettings } from './internal-common'; import { mergeAccounts } from './merge'; export function createLoginServerStatus(settings: Settings, live: ServerLiveSettings): LoginServerStatus { - return { - canCreateAccounts: !!settings.canCreateAccounts, - isPageOffline: !!settings.isPageOffline, - blockWebView: !!settings.blockWebView, - reportPotentialDuplicates: !!settings.reportPotentialDuplicates, - autoMergeDuplicates: !!settings.autoMergeDuplicates, + return { + canCreateAccounts: !!settings.canCreateAccounts, + isPageOffline: !!settings.isPageOffline, + blockWebView: !!settings.blockWebView, + reportPotentialDuplicates: !!settings.reportPotentialDuplicates, + autoMergeDuplicates: !!settings.autoMergeDuplicates, - suspiciousNames: settings.suspiciousNames || '', - suspiciousAuths: settings.suspiciousAuths || '', - suspiciousPonies: settings.suspiciousPonies || '', - suspiciousMessages: settings.suspiciousMessages || '', - suspiciousSafeMessages: settings.suspiciousSafeMessages || '', - suspiciousSafeWholeMessages: settings.suspiciousSafeWholeMessages || '', - suspiciousSafeInstantMessages: settings.suspiciousSafeInstantMessages || '', - suspiciousSafeInstantWholeMessages: settings.suspiciousSafeInstantWholeMessages || '', + suspiciousNames: settings.suspiciousNames || '', + suspiciousAuths: settings.suspiciousAuths || '', + suspiciousPonies: settings.suspiciousPonies || '', + suspiciousMessages: settings.suspiciousMessages || '', + suspiciousSafeMessages: settings.suspiciousSafeMessages || '', + suspiciousSafeWholeMessages: settings.suspiciousSafeWholeMessages || '', + suspiciousSafeInstantMessages: settings.suspiciousSafeInstantMessages || '', + suspiciousSafeInstantWholeMessages: settings.suspiciousSafeInstantWholeMessages || '', - updating: live.updating, - dead: false, - }; + updating: live.updating, + dead: false, + }; } export const createLoginServerState = - (settings: Settings, live: ServerLiveSettings) => - async () => - createLoginServerStatus(settings, live); + (settings: Settings, live: ServerLiveSettings) => + async () => + createLoginServerStatus(settings, live); export const createLoginServerStats = - (statsTracker: StatsTracker) => - async () => - statsTracker.getStats(); + (statsTracker: StatsTracker) => + async () => + statsTracker.getStats(); export const createUpdateLiveSettings = - (liveSettings: ServerLiveSettings) => - async (update: Partial) => { - Object.assign(liveSettings, update); - }; + (liveSettings: ServerLiveSettings) => + async (update: Partial) => { + Object.assign(liveSettings, update); + }; export const createInternalLoginApi = - ( - settings: Settings, live: ServerLiveSettings, statsTracker: StatsTracker, - reloadSettings: () => Promise, removedDocument: RemovedDocument, - ): InternalLoginApi => - ({ - reloadSettings: createReloadSettings(reloadSettings), - state: createLoginServerState(settings, live), - loginServerStats: createLoginServerStats(statsTracker), - updateLiveSettings: createUpdateLiveSettings(live), - mergeAccounts: async (id, withId, reason, allowAdmin, creatingDuplicates) => { - if (live.shutdown) { - throw new Error(`Cannot merge while server is shutdown`); - } + ( + settings: Settings, live: ServerLiveSettings, statsTracker: StatsTracker, + reloadSettings: () => Promise, removedDocument: RemovedDocument, + ): InternalLoginApi => + ({ + reloadSettings: createReloadSettings(reloadSettings), + state: createLoginServerState(settings, live), + loginServerStats: createLoginServerStats(statsTracker), + updateLiveSettings: createUpdateLiveSettings(live), + mergeAccounts: async (id, withId, reason, allowAdmin, creatingDuplicates) => { + if (live.shutdown) { + throw new Error(`Cannot merge while server is shutdown`); + } - await mergeAccounts(id, withId, reason, removedDocument, allowAdmin, creatingDuplicates); - }, - }); + await mergeAccounts(id, withId, reason, removedDocument, allowAdmin, creatingDuplicates); + }, + }); diff --git a/src/ts/server/api/internal.ts b/src/ts/server/api/internal.ts index 150c3a3..dca34ee 100644 --- a/src/ts/server/api/internal.ts +++ b/src/ts/server/api/internal.ts @@ -1,11 +1,11 @@ import { - GameServerState, AccountStatus, ServerConfig, InternalApi, ServerLiveSettings, Stats, StatsTable + GameServerState, AccountStatus, ServerConfig, InternalApi, ServerLiveSettings, Stats, StatsTable } from '../../common/adminInterfaces'; import { isBanned, supporterLevel } from '../../common/adminUtils'; import { IClient, TokenService, GetSettings } from '../serverInterfaces'; import { - ICharacter, IAccount, FindAccountSafe, FindAuth, FindCharacterSafe, findAccountSafe, findCharacterSafe, - findAuth, HasActiveSupporterInvites, hasActiveSupporterInvites + ICharacter, IAccount, FindAccountSafe, FindAuth, FindCharacterSafe, findAccountSafe, findCharacterSafe, + findAuth, HasActiveSupporterInvites, hasActiveSupporterInvites } from '../db'; import { World, findClientsAroundAccountId, findClientByAccountId } from '../world'; import { HidingService, saveHidingData } from '../services/hiding'; @@ -21,313 +21,313 @@ import { getSizeOfMap } from '../serverMap'; import { teleportTo } from '../playerUtils'; export const createAccountChanged = - (world: World, tokens: TokenService, findAccount: FindAccountSafe) => - async (accountId: string) => { - const account = await findAccount(accountId); - world.accountUpdated(account); + (world: World, tokens: TokenService, findAccount: FindAccountSafe) => + async (accountId: string) => { + const account = await findAccount(accountId); + world.accountUpdated(account); - if (isBanned(account)) { - tokens.clearTokensForAccount(accountId); - } - }; + if (isBanned(account)) { + tokens.clearTokensForAccount(accountId); + } + }; export const createAccountMerged = - (hiding: HidingService) => - async (accountId: string, mergedId: string) => - await hiding.merged(accountId, mergedId); + (hiding: HidingService) => + async (accountId: string, mergedId: string) => + await hiding.merged(accountId, mergedId); function toAccountStatus(client: IClient | undefined, server: ServerConfig): AccountStatus { - return client ? { - online: true, - character: client.characterName, - server: server.id, - map: client.map.id || '-', - x: Math.round(client.pony.x), - y: Math.round(client.pony.y), - userAgent: client.userAgent, - incognito: client.incognito, - duration: formatDuration(Date.now() - client.connectedTime), - } : { online: false }; + return client ? { + online: true, + character: client.characterName, + server: server.id, + map: client.map.id || '-', + x: Math.round(client.pony.x), + y: Math.round(client.pony.y), + userAgent: client.userAgent, + incognito: client.incognito, + duration: formatDuration(Date.now() - client.connectedTime), + } : { online: false }; } export const createAccountStatus = - (world: World, server: ServerConfig) => - async (accountId: string) => - toAccountStatus(findClientByAccountId(world, accountId), server); + (world: World, server: ServerConfig) => + async (accountId: string) => + toAccountStatus(findClientByAccountId(world, accountId), server); export const createAccountAround = - (world: World) => - async (accountId: string) => - findClientsAroundAccountId(world, accountId); + (world: World) => + async (accountId: string) => + findClientsAroundAccountId(world, accountId); export const createHiddenStats = - (hiding: HidingService) => - async (accountId: string) => - hiding.getStatsFor(accountId); + (hiding: HidingService) => + async (accountId: string) => + hiding.getStatsFor(accountId); export const createTeleportTo = - (world: World) => - async (adminAccountId: string, targetAccountId: string) => { - const admin = findClientByAccountId(world, adminAccountId); - const target = findClientByAccountId(world, targetAccountId); + (world: World) => + async (adminAccountId: string, targetAccountId: string) => { + const admin = findClientByAccountId(world, adminAccountId); + const target = findClientByAccountId(world, targetAccountId); - if (admin && target && admin.map === target.map) { - teleportTo(admin, target.pony.x, target.pony.y); - } - }; + if (admin && target && admin.map === target.map) { + teleportTo(admin, target.pony.x, target.pony.y); + } + }; async function setupPonyAuth(character: ICharacter, account: IAccount, findAuth: FindAuth) { - if (character.site) { - const auth = await findAuth(character.site, account._id); + if (character.site) { + const auth = await findAuth(character.site, account._id); - if (auth && !auth.disabled && !auth.banned) { - character.auth = auth; - } - } + if (auth && !auth.disabled && !auth.banned) { + character.auth = auth; + } + } } export const createJoin = - ( - world: World, getSettings: GetSettings, server: ServerConfig, - { clearTokensForAccount, createToken }: TokenService, findAccount: FindAccountSafe, - findCharacter: FindCharacterSafe, findAuth: FindAuth, live: ServerLiveSettings, - hasInvite: HasActiveSupporterInvites - ) => - async (accountId: string, characterId: string) => { - if (getSettings().isServerOffline || live.shutdown) { - throw new UserError('Server is offline'); - } + ( + world: World, getSettings: GetSettings, server: ServerConfig, + { clearTokensForAccount, createToken }: TokenService, findAccount: FindAccountSafe, + findCharacter: FindCharacterSafe, findAuth: FindAuth, live: ServerLiveSettings, + hasInvite: HasActiveSupporterInvites + ) => + async (accountId: string, characterId: string) => { + if (getSettings().isServerOffline || live.shutdown) { + throw new UserError('Server is offline'); + } - const [account, character, supporterInvited] = await Promise.all([ - findAccount(accountId), - findCharacter(characterId, accountId), - hasInvite(accountId), - ]); + const [account, character, supporterInvited] = await Promise.all([ + findAccount(accountId), + findCharacter(characterId, accountId), + hasInvite(accountId), + ]); - if (!meetsRequirement({ roles: account.roles, supporter: supporterLevel(account), supporterInvited }, server.require)) { - throw new UserError('Server is restricted'); - } + if (!meetsRequirement({ roles: account.roles, supporter: supporterLevel(account), supporterInvited }, server.require)) { + throw new UserError('Server is restricted'); + } - await setupPonyAuth(character, account, findAuth); + await setupPonyAuth(character, account, findAuth); - character.lastUsed = new Date(); - account.settings = { ...account.settings, defaultServer: server.id }; - account.lastVisit = new Date(); + character.lastUsed = new Date(); + account.settings = { ...account.settings, defaultServer: server.id }; + account.lastVisit = new Date(); - if (!account.settings.hidden) { - account.lastOnline = new Date(); - account.lastCharacter = character._id; - } + if (!account.settings.hidden) { + account.lastOnline = new Date(); + account.lastCharacter = character._id; + } - await Promise.all([character.save(), account.save()]); + await Promise.all([character.save(), account.save()]); - world.kickByAccount(accountId); - clearTokensForAccount(accountId); + world.kickByAccount(accountId); + clearTokensForAccount(accountId); - return createToken({ accountId, account, character }); - }; + return createToken({ accountId, account, character }); + }; function getClientCountOnMainMap(world: World) { - let count = 0; - const map = world.getMainMap(); + let count = 0; + const map = world.getMainMap(); - for (const client of world.clients) { - if (client.map === map) { - count++; - } - } + for (const client of world.clients) { + if (client.map === map) { + count++; + } + } - return count; + return count; } export const createGetServerState = - (server: ServerConfig, getSettings: GetSettings, world: World, live: ServerLiveSettings) => - async (): Promise => - ({ - id: server.id, - name: server.name, - path: server.path, - desc: server.desc, - flag: server.flag, - host: server.host, - alert: server.alert, - flags: server.flags, - require: server.require, - dead: false, - shutdown: live.shutdown, - maps: world.maps.length, - online: world.clients.length, - onMain: getClientCountOnMainMap(world), - queued: world.joinQueue.length, - settings: getSettings(), - }); + (server: ServerConfig, getSettings: GetSettings, world: World, live: ServerLiveSettings) => + async (): Promise => + ({ + id: server.id, + name: server.name, + path: server.path, + desc: server.desc, + flag: server.flag, + host: server.host, + alert: server.alert, + flags: server.flags, + require: server.require, + dead: false, + shutdown: live.shutdown, + maps: world.maps.length, + online: world.clients.length, + onMain: getClientCountOnMainMap(world), + queued: world.joinQueue.length, + settings: getSettings(), + }); export const createGetServerStats = - (statsTracker: StatsTracker) => - async () => - statsTracker.getSocketStats(); + (statsTracker: StatsTracker) => + async () => + statsTracker.getSocketStats(); export const createGetStatsTable = - (world: World) => - async (stats: Stats) => { - switch (stats) { - case Stats.Country: - return getCountryStats(world); - case Stats.Support: - return getSupportStats(world); - case Stats.Maps: - return getMapStats(world); - default: - invalidEnum(stats); - return []; - } - }; + (world: World) => + async (stats: Stats) => { + switch (stats) { + case Stats.Country: + return getCountryStats(world); + case Stats.Support: + return getSupportStats(world); + case Stats.Maps: + return getMapStats(world); + default: + invalidEnum(stats); + return []; + } + }; export const createAction = - (world: World) => - async (action: string, accountId: string) => { - switch (action) { - case 'unstuck': - const client = findClientByAccountId(world, accountId); + (world: World) => + async (action: string, accountId: string) => { + switch (action) { + case 'unstuck': + const client = findClientByAccountId(world, accountId); - if (client) { - world.resetToSpawn(client); - world.kick(client, 'unstuck'); - } - break; - default: - throw new Error(`Invalid action (${action})`); - } - }; + if (client) { + world.resetToSpawn(client); + world.kick(client, 'unstuck'); + } + break; + default: + throw new Error(`Invalid action (${action})`); + } + }; export const createKick = - (world: World, { clearTokensForAccount }: TokenService) => - async (accountId: string | undefined, characterId: string | undefined) => { - if (accountId) { - clearTokensForAccount(accountId); - return world.kickByAccount(accountId); - } else if (characterId) { - return world.kickByCharacter(characterId); - } else { - return false; - } - }; + (world: World, { clearTokensForAccount }: TokenService) => + async (accountId: string | undefined, characterId: string | undefined) => { + if (accountId) { + clearTokensForAccount(accountId); + return world.kickByAccount(accountId); + } else if (characterId) { + return world.kickByCharacter(characterId); + } else { + return false; + } + }; export const createKickAll = - (world: World, { clearTokensAll }: TokenService) => - async () => { - world.kickAll(); - clearTokensAll(); - }; + (world: World, { clearTokensAll }: TokenService) => + async () => { + world.kickAll(); + clearTokensAll(); + }; export const createNotifyUpdate = - (world: World, live: ServerLiveSettings) => - async () => { - live.updating = true; - world.notifyUpdate(); - world.saveClientStates(); - }; + (world: World, live: ServerLiveSettings) => + async () => { + live.updating = true; + world.notifyUpdate(); + world.saveClientStates(); + }; export const createCancelUpdate = - (live: ServerLiveSettings) => - async () => { - live.updating = false; - }; + (live: ServerLiveSettings) => + async () => { + live.updating = false; + }; export const createShutdownServer = - (world: World, live: ServerLiveSettings) => - async (value: boolean) => { - live.shutdown = value; + (world: World, live: ServerLiveSettings) => + async (value: boolean) => { + live.shutdown = value; - if (live.shutdown) { - world.kickAll(); - saveHidingData(world.hidingService, world.server.id); - } - }; + if (live.shutdown) { + world.kickAll(); + saveHidingData(world.hidingService, world.server.id); + } + }; /* istanbul ignore next */ export function createInternalApi( - world: World, server: ServerConfig, reloadSettings: () => Promise, getSettings: GetSettings, - tokens: TokenService, hiding: HidingService, statsTracker: StatsTracker, live: ServerLiveSettings, + world: World, server: ServerConfig, reloadSettings: () => Promise, getSettings: GetSettings, + tokens: TokenService, hiding: HidingService, statsTracker: StatsTracker, live: ServerLiveSettings, ): InternalApi { - return { - reloadSettings: createReloadSettings(reloadSettings), - state: createGetServerState(server, getSettings, world, live), - stats: createGetServerStats(statsTracker), - statsTable: createGetStatsTable(world), - action: createAction(world), - join: createJoin( - world, getSettings, server, tokens, findAccountSafe, findCharacterSafe, findAuth, live, hasActiveSupporterInvites), - kick: createKick(world, tokens), - kickAll: createKickAll(world, tokens), - accountChanged: createAccountChanged(world, tokens, findAccountSafe), - accountMerged: createAccountMerged(hiding), - accountStatus: createAccountStatus(world, server), - accountAround: createAccountAround(world), - notifyUpdate: createNotifyUpdate(world, liveSettings), - cancelUpdate: createCancelUpdate(liveSettings), - shutdownServer: createShutdownServer(world, live), - accountHidden: createHiddenStats(hiding), - getTimings: async () => timingEntries(), - teleportTo: createTeleportTo(world), - }; + return { + reloadSettings: createReloadSettings(reloadSettings), + state: createGetServerState(server, getSettings, world, live), + stats: createGetServerStats(statsTracker), + statsTable: createGetStatsTable(world), + action: createAction(world), + join: createJoin( + world, getSettings, server, tokens, findAccountSafe, findCharacterSafe, findAuth, live, hasActiveSupporterInvites), + kick: createKick(world, tokens), + kickAll: createKickAll(world, tokens), + accountChanged: createAccountChanged(world, tokens, findAccountSafe), + accountMerged: createAccountMerged(hiding), + accountStatus: createAccountStatus(world, server), + accountAround: createAccountAround(world), + notifyUpdate: createNotifyUpdate(world, liveSettings), + cancelUpdate: createCancelUpdate(liveSettings), + shutdownServer: createShutdownServer(world, live), + accountHidden: createHiddenStats(hiding), + getTimings: async () => timingEntries(), + teleportTo: createTeleportTo(world), + }; } function getCountryStats(world: World): StatsTable { - return [ - ['country', 'users'], - ...toPairs(groupBy(world.clients, c => c.country)) - .map(([key, value]) => ({ key, count: value.length })) - .sort((a, b) => b.count - a.count) - .map(({ key, count }) => [key, count.toString()]), - ]; + return [ + ['country', 'users'], + ...toPairs(groupBy(world.clients, c => c.country)) + .map(([key, value]) => ({ key, count: value.length })) + .sort((a, b) => b.count - a.count) + .map(({ key, count }) => [key, count.toString()]), + ]; } function getSupportStats(world: World): StatsTable { - let wasmYes = 0; - let wasmNo = 0; - let letAndConstYes = 0; - let letAndConstNo = 0; + let wasmYes = 0; + let wasmNo = 0; + let letAndConstYes = 0; + let letAndConstNo = 0; - for (const client of world.clients) { - if (client.supportsWasm) { - wasmYes++; - } else { - wasmNo++; - } + for (const client of world.clients) { + if (client.supportsWasm) { + wasmYes++; + } else { + wasmNo++; + } - if (client.supportsLetAndConst) { - letAndConstYes++; - } else { - letAndConstNo++; - } - } + if (client.supportsLetAndConst) { + letAndConstYes++; + } else { + letAndConstNo++; + } + } - function percent(yes: number, no: number) { - return (yes * 100 / ((yes + no) || 1)).toFixed(0) + '%'; - } + function percent(yes: number, no: number) { + return (yes * 100 / ((yes + no) || 1)).toFixed(0) + '%'; + } - return [ - ['supports', 'yes', 'no', ''], - ['wasm', wasmYes.toString(), wasmNo.toString(), percent(wasmYes, wasmNo)], - ['let & const', letAndConstYes.toString(), letAndConstNo.toString(), percent(letAndConstYes, letAndConstNo)], - ]; + return [ + ['supports', 'yes', 'no', ''], + ['wasm', wasmYes.toString(), wasmNo.toString(), percent(wasmYes, wasmNo)], + ['let & const', letAndConstYes.toString(), letAndConstNo.toString(), percent(letAndConstYes, letAndConstNo)], + ]; } function getMapStats(world: World): StatsTable { - return [ - ['id', 'instance', 'entities', 'players', 'memory'], - ...world.maps.map(map => { - const { entities, memory } = getSizeOfMap(map); + return [ + ['id', 'instance', 'entities', 'players', 'memory'], + ...world.maps.map(map => { + const { entities, memory } = getSizeOfMap(map); - return [ - map.id || 'main', - map.instance || '', - entities.toString(), - world.clients.reduce((sum, c) => sum + (c.map === map ? 1 : 0), 0).toString(), - `${(memory / 1024).toFixed()} kb`, - ]; - }), - ]; + return [ + map.id || 'main', + map.instance || '', + entities.toString(), + world.clients.reduce((sum, c) => sum + (c.map === map ? 1 : 0), 0).toString(), + `${(memory / 1024).toFixed()} kb`, + ]; + }), + ]; } diff --git a/src/ts/server/api/merge.ts b/src/ts/server/api/merge.ts index 8e1c619..5368b99 100644 --- a/src/ts/server/api/merge.ts +++ b/src/ts/server/api/merge.ts @@ -2,270 +2,270 @@ import { assignWith, uniq, uniqBy, clone, mapValues, difference } from 'lodash'; import { toInt, maxDate, minDate, compareDates } from '../../common/utils'; import { updateCharacterCount, checkIfNotAdmin } from '../accountUtils'; import { - Account, Auth, Character, Event, IAccount, SupporterInvite, findAccountSafe, MongoUpdate, ID, FriendRequest, - findFriendIds, findHidesForMerge, HideRequest + Account, Auth, Character, Event, IAccount, SupporterInvite, findAccountSafe, MongoUpdate, ID, FriendRequest, + findFriendIds, findHidesForMerge, HideRequest } from '../db'; import { accountChanged, accountMerged, RemovedDocument } from '../internal'; import { system } from '../logger'; import { makeQueued } from '../utils/taskQueue'; import { - AccountBase, MergeData, MergeAccountData, AccountState, AccountFlags, MergeHideData + AccountBase, MergeData, MergeAccountData, AccountState, AccountFlags, MergeHideData } from '../../common/adminInterfaces'; import { kickFromAllServers } from './admin'; function mergeBan(a: number | undefined, b: number | undefined): number { - return (a === -1 || b === -1) ? -1 : Math.max(a || 0, b || 0); + return (a === -1 || b === -1) ? -1 : Math.max(a || 0, b || 0); } function mergeLists(a: T[] | undefined, b: T[] | undefined, limit: number) { - return [...(a || []), ...(b || [])].sort((a, b) => compareDates(a.date, b.date)).slice(-limit); + return [...(a || []), ...(b || [])].sort((a, b) => compareDates(a.date, b.date)).slice(-limit); } async function findAccounts(id: ID, withId: ID, allowAdmin = false) { - const accounts = await Account.find({ _id: { $in: [id, withId] } }) - .populate('auths', 'name') - .populate('characters', 'name') - .exec(); + const accounts = await Account.find({ _id: { $in: [id, withId] } }) + .populate('auths', 'name') + .populate('characters', 'name') + .exec(); - if (!allowAdmin) { - accounts.forEach(a => checkIfNotAdmin(a, `merge: ${a._id}`)); - } + if (!allowAdmin) { + accounts.forEach(a => checkIfNotAdmin(a, `merge: ${a._id}`)); + } - const account = accounts.find(a => a._id.toString() === id); - const merge = accounts.find(a => a._id.toString() === withId); + const account = accounts.find(a => a._id.toString() === id); + const merge = accounts.find(a => a._id.toString() === withId); - if (accounts.length !== 2 || !account || !merge) { - throw new Error('Account does not exist'); - } + if (accounts.length !== 2 || !account || !merge) { + throw new Error('Account does not exist'); + } - return { account, merge }; + return { account, merge }; } function dumpData(account: IAccount, friends: string[], hides: MergeHideData[]): MergeAccountData { - const { - name, note, flags, counters = {}, auths = [], characters = [], ignores = [], emails = [], state = {}, - birthdate, - } = account; + const { + name, note, flags, counters = {}, auths = [], characters = [], ignores = [], emails = [], state = {}, + birthdate, + } = account; - return { - name, - note, - flags, - state, - birthdate, - emails: emails.slice(), - ignores: ignores.slice(), - counters: clone(counters), - auths: auths.map(({ _id, name }) => ({ id: _id.toString(), name })), - characters: characters.map(({ _id, name }) => ({ id: _id.toString(), name })), - settings: account.settings, - friends: friends.slice(), - hides: hides.slice(), - }; + return { + name, + note, + flags, + state, + birthdate, + emails: emails.slice(), + ignores: ignores.slice(), + counters: clone(counters), + auths: auths.map(({ _id, name }) => ({ id: _id.toString(), name })), + characters: characters.map(({ _id, name }) => ({ id: _id.toString(), name })), + settings: account.settings, + friends: friends.slice(), + hides: hides.slice(), + }; } function mergeStates(a: AccountState | undefined, b: AccountState | undefined) { - if (a && b) { - return { - ...b, - ...a, - gifts: toInt(a.gifts) + toInt(b.gifts), - candies: toInt(a.candies) + toInt(b.candies), - clovers: toInt(a.clovers) + toInt(b.clovers), - toys: toInt(a.toys) | toInt(b.toys), - }; - } else { - return a || b; - } + if (a && b) { + return { + ...b, + ...a, + gifts: toInt(a.gifts) + toInt(b.gifts), + candies: toInt(a.candies) + toInt(b.candies), + clovers: toInt(a.clovers) + toInt(b.clovers), + toys: toInt(a.toys) | toInt(b.toys), + }; + } else { + return a || b; + } } async function merge( - id: string, withId: string, reason: string, removedDocument: RemovedDocument, allowAdmin = false, - creatingDuplicates = false + id: string, withId: string, reason: string, removedDocument: RemovedDocument, allowAdmin = false, + creatingDuplicates = false ) { - const start = Date.now(); + const start = Date.now(); - const [{ account, merge }, accountFriends, mergeFriends, accountHides, mergeHides] = await Promise.all([ - findAccounts(id, withId, allowAdmin), - findFriendIds(id), - findFriendIds(withId), - findHidesForMerge(id), - findHidesForMerge(withId), - ]); + const [{ account, merge }, accountFriends, mergeFriends, accountHides, mergeHides] = await Promise.all([ + findAccounts(id, withId, allowAdmin), + findFriendIds(id), + findFriendIds(withId), + findHidesForMerge(id), + findHidesForMerge(withId), + ]); - const data: MergeData = { - account: dumpData(account, accountFriends, accountHides), - merge: dumpData(merge, mergeFriends, mergeHides), - }; + const data: MergeData = { + account: dumpData(account, accountFriends, accountHides), + merge: dumpData(merge, mergeFriends, mergeHides), + }; - const origins = uniqBy([...(account.origins || []), ...(merge.origins || [])], x => x.ip); - const ignores = uniq([...(account.ignores || []), ...(merge.ignores || [])]); - const emails = uniq([...(account.emails || []), ...(merge.emails || [])]); - const note = `${account.note || ''}\n${merge.note || ''}`.trim(); - const createdAt = minDate(account.createdAt, merge.createdAt); - const lastVisit = maxDate(account.lastVisit, merge.lastVisit); - const ban = mergeBan(account.ban, merge.ban); - const shadow = mergeBan(account.shadow, merge.shadow); - const mute = mergeBan(account.mute, merge.mute); - const patreon = Math.max(toInt(account.patreon), toInt(merge.patreon)); - const counters = assignWith(account.counters || {}, merge.counters || {}, (a, b) => (a | 0) + (b | 0)); - const creatingDuplicatesFlag = creatingDuplicates ? AccountFlags.CreatingDuplicates : 0; - const flags = account.flags | merge.flags | creatingDuplicatesFlag; - const supporter = toInt(account.supporter) | toInt(merge.supporter); - const birthdate = account.birthdate || merge.birthdate; - const supporterLog = mergeLists(account.supporterLog, merge.supporterLog, 10); - const supporterTotal = toInt(account.supporterTotal) + toInt(merge.supporterTotal); - const banLog = mergeLists(account.banLog, merge.banLog, 10); - const merges = mergeLists(account.merges, merge.merges, 20); - const state = mergeStates(account.state, merge.state); - const alert = account.alert || merge.alert; - merges.push({ id: withId, name: merge.name, date: new Date(), reason, data }); + const origins = uniqBy([...(account.origins || []), ...(merge.origins || [])], x => x.ip); + const ignores = uniq([...(account.ignores || []), ...(merge.ignores || [])]); + const emails = uniq([...(account.emails || []), ...(merge.emails || [])]); + const note = `${account.note || ''}\n${merge.note || ''}`.trim(); + const createdAt = minDate(account.createdAt, merge.createdAt); + const lastVisit = maxDate(account.lastVisit, merge.lastVisit); + const ban = mergeBan(account.ban, merge.ban); + const shadow = mergeBan(account.shadow, merge.shadow); + const mute = mergeBan(account.mute, merge.mute); + const patreon = Math.max(toInt(account.patreon), toInt(merge.patreon)); + const counters = assignWith(account.counters || {}, merge.counters || {}, (a, b) => (a | 0) + (b | 0)); + const creatingDuplicatesFlag = creatingDuplicates ? AccountFlags.CreatingDuplicates : 0; + const flags = account.flags | merge.flags | creatingDuplicatesFlag; + const supporter = toInt(account.supporter) | toInt(merge.supporter); + const birthdate = account.birthdate || merge.birthdate; + const supporterLog = mergeLists(account.supporterLog, merge.supporterLog, 10); + const supporterTotal = toInt(account.supporterTotal) + toInt(merge.supporterTotal); + const banLog = mergeLists(account.banLog, merge.banLog, 10); + const merges = mergeLists(account.merges, merge.merges, 20); + const state = mergeStates(account.state, merge.state); + const alert = account.alert || merge.alert; + merges.push({ id: withId, name: merge.name, date: new Date(), reason, data }); - const update: Partial> = { - origins, ignores, emails, note, lastVisit, ban, shadow, mute, flags, counters, patreon, supporter, merges, - createdAt, supporterLog, supporterTotal, banLog, state, alert, birthdate, - }; + const update: Partial> = { + origins, ignores, emails, note, lastVisit, ban, shadow, mute, flags, counters, patreon, supporter, merges, + createdAt, supporterLog, supporterTotal, banLog, state, alert, birthdate, + }; - await Promise.all([ - Account.updateOne({ _id: account._id }, update).exec(), - Account.updateMany({ ignores: { $exists: true, $ne: [], $in: [withId] } }, { $addToSet: { ignores: id } }).exec() - .then(() => Account.updateMany({ ignores: { $exists: true, $ne: [], $in: [withId] } }, { $pull: { ignores: withId } }).exec()), - Auth.updateMany({ account: merge._id }, { account: account._id }).exec(), - Event.updateMany({ account: merge._id }, { account: account._id }).exec(), - Character.updateMany({ account: merge._id }, { account: account._id }).exec(), - Promise.all([ - SupporterInvite.updateMany({ source: merge._id }, { source: account._id }).exec(), - SupporterInvite.updateMany({ target: merge._id }, { target: account._id }).exec(), - ]).then(() => SupporterInvite.remove({ target: account._id, source: account._id }).exec()), - Promise.all([ - FriendRequest.updateMany({ source: merge._id }, { source: account._id }).exec(), - FriendRequest.updateMany({ target: merge._id }, { target: account._id }).exec(), - ]).then(() => FriendRequest.remove({ target: account._id, source: account._id }).exec()), - Promise.all([ - HideRequest.updateMany({ source: merge._id }, { source: account._id }).exec(), - HideRequest.updateMany({ target: merge._id }, { target: account._id }).exec(), - ]).then(() => HideRequest.remove({ target: account._id, source: account._id }).exec()), - ]); + await Promise.all([ + Account.updateOne({ _id: account._id }, update).exec(), + Account.updateMany({ ignores: { $exists: true, $ne: [], $in: [withId] } }, { $addToSet: { ignores: id } }).exec() + .then(() => Account.updateMany({ ignores: { $exists: true, $ne: [], $in: [withId] } }, { $pull: { ignores: withId } }).exec()), + Auth.updateMany({ account: merge._id }, { account: account._id }).exec(), + Event.updateMany({ account: merge._id }, { account: account._id }).exec(), + Character.updateMany({ account: merge._id }, { account: account._id }).exec(), + Promise.all([ + SupporterInvite.updateMany({ source: merge._id }, { source: account._id }).exec(), + SupporterInvite.updateMany({ target: merge._id }, { target: account._id }).exec(), + ]).then(() => SupporterInvite.remove({ target: account._id, source: account._id }).exec()), + Promise.all([ + FriendRequest.updateMany({ source: merge._id }, { source: account._id }).exec(), + FriendRequest.updateMany({ target: merge._id }, { target: account._id }).exec(), + ]).then(() => FriendRequest.remove({ target: account._id, source: account._id }).exec()), + Promise.all([ + HideRequest.updateMany({ source: merge._id }, { source: account._id }).exec(), + HideRequest.updateMany({ target: merge._id }, { target: account._id }).exec(), + ]).then(() => HideRequest.remove({ target: account._id, source: account._id }).exec()), + ]); - await removeDuplicateFriendRequests(id); - await merge.remove(); - await kickFromAllServers(withId); - await removedDocument('accounts', withId); - await updateCharacterCount(id); - await accountMerged(id, withId); - await accountChanged(id); + await removeDuplicateFriendRequests(id); + await merge.remove(); + await kickFromAllServers(withId); + await removedDocument('accounts', withId); + await updateCharacterCount(id); + await accountMerged(id, withId); + await accountChanged(id); - system(account._id, `Merged ${account.name} with ${merge.name} [${merge._id}] (${reason}) (${Date.now() - start}ms)`); + system(account._id, `Merged ${account.name} with ${merge.name} [${merge._id}] (${reason}) (${Date.now() - start}ms)`); } async function removeDuplicateFriendRequests(id: string) { - const friendRequests = await FriendRequest.find({ $or: [{ source: id }, { target: id }] }).exec(); - const checked = new Set(); - const removeRequests: ID[] = []; + const friendRequests = await FriendRequest.find({ $or: [{ source: id }, { target: id }] }).exec(); + const checked = new Set(); + const removeRequests: ID[] = []; - for (const request of friendRequests) { - const friendId = request.source.toString() === id ? request.target.toString() : request.source.toString(); + for (const request of friendRequests) { + const friendId = request.source.toString() === id ? request.target.toString() : request.source.toString(); - if (checked.has(friendId)) { - removeRequests.push(request._id); - } else { - checked.add(friendId); - } - } + if (checked.has(friendId)) { + removeRequests.push(request._id); + } else { + checked.add(friendId); + } + } - if (removeRequests.length) { - await FriendRequest.remove({ _id: { $in: removeRequests } }).exec(); - } + if (removeRequests.length) { + await FriendRequest.remove({ _id: { $in: removeRequests } }).exec(); + } } export async function split( - accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData, reason: string + accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData, reason: string ) { - const start = Date.now(); - const account = await findAccountSafe(accountId); - const unmerge = await Account.create({ - name: split.name, - note: split.note, - flags: split.flags || 0, - emails: split.emails, - state: split.state, - ignores: split.ignores, - counters: split.counters, - birthdate: split.birthdate, - settings: split.settings, - }); + const start = Date.now(); + const account = await findAccountSafe(accountId); + const unmerge = await Account.create({ + name: split.name, + note: split.note, + flags: split.flags || 0, + emails: split.emails, + state: split.state, + ignores: split.ignores, + counters: split.counters, + birthdate: split.birthdate, + settings: split.settings, + }); - const accountUpdate: MongoUpdate = { - note: `${account.note}\nsplit: [${unmerge._id}]`.trim(), - state: keep.state, - }; + const accountUpdate: MongoUpdate = { + note: `${account.note}\nsplit: [${unmerge._id}]`.trim(), + state: keep.state, + }; - const removeIgnores = difference(split.ignores, account.ignores || []); + const removeIgnores = difference(split.ignores, account.ignores || []); - if (removeIgnores.length) { - accountUpdate.$pull = { ignores: removeIgnores }; - } + if (removeIgnores.length) { + accountUpdate.$pull = { ignores: removeIgnores }; + } - const newCounters = split.counters || {}; + const newCounters = split.counters || {}; - if (Object.keys(newCounters).length > 0) { - const oldCounters = account.counters || {} as any; - const counters = mapValues(newCounters, (value, key) => Math.max(0, toInt(oldCounters[key]) - toInt(value))); - accountUpdate.counters = counters; - } + if (Object.keys(newCounters).length > 0) { + const oldCounters = account.counters || {} as any; + const counters = mapValues(newCounters, (value, key) => Math.max(0, toInt(oldCounters[key]) - toInt(value))); + accountUpdate.counters = counters; + } - const authIds = split.auths.map(x => x.id); + const authIds = split.auths.map(x => x.id); - await Promise.all([ - Auth.updateMany({ _id: { $in: authIds } }, { account: unmerge._id, disabled: false }).exec(), - Character.updateMany({ _id: { $in: split.characters.map(x => x.id) } }, { account: unmerge._id }).exec(), - Account.updateOne({ _id: account._id }, accountUpdate).exec(), - ]); + await Promise.all([ + Auth.updateMany({ _id: { $in: authIds } }, { account: unmerge._id, disabled: false }).exec(), + Character.updateMany({ _id: { $in: split.characters.map(x => x.id) } }, { account: unmerge._id }).exec(), + Account.updateOne({ _id: account._id }, accountUpdate).exec(), + ]); - // friends + // friends - const friendsToRemove = [...(keep.friends || []), ...(split.friends || [])]; + const friendsToRemove = [...(keep.friends || []), ...(split.friends || [])]; - await FriendRequest.deleteMany({ - $or: [ - { target: account._id, source: { $in: friendsToRemove } }, - { source: account._id, target: { $in: friendsToRemove } }, - ], - }).exec(); + await FriendRequest.deleteMany({ + $or: [ + { target: account._id, source: { $in: friendsToRemove } }, + { source: account._id, target: { $in: friendsToRemove } }, + ], + }).exec(); - await FriendRequest.create([ - ...(keep.friends || []).map(id => ({ source: account._id, target: id })), - ...(split.friends || []).map(id => ({ source: unmerge._id, target: id })) - ]); + await FriendRequest.create([ + ...(keep.friends || []).map(id => ({ source: account._id, target: id })), + ...(split.friends || []).map(id => ({ source: unmerge._id, target: id })) + ]); - // hides + // hides - const hidesToRemove = [...(keep.hides || []), ...(split.hides || [])].map(hide => hide.id); + const hidesToRemove = [...(keep.hides || []), ...(split.hides || [])].map(hide => hide.id); - await HideRequest.deleteMany({ - $or: [ - { source: account._id, target: { $in: hidesToRemove } }, - ], - }).exec(); + await HideRequest.deleteMany({ + $or: [ + { source: account._id, target: { $in: hidesToRemove } }, + ], + }).exec(); - await HideRequest.create([ - ...(keep.hides || []).map(hide => ({ source: account._id, target: hide.id, name: hide.name, date: new Date(hide.date) })), - ...(split.hides || []).map(hide => ({ source: unmerge._id, target: hide.id, name: hide.name, date: new Date(hide.date) })) - ]); + await HideRequest.create([ + ...(keep.hides || []).map(hide => ({ source: account._id, target: hide.id, name: hide.name, date: new Date(hide.date) })), + ...(split.hides || []).map(hide => ({ source: unmerge._id, target: hide.id, name: hide.name, date: new Date(hide.date) })) + ]); - // other + // other - if (mergeId) { - await Account.updateOne({ _id: account._id, 'merges._id': mergeId }, { 'merges.$.split': true }).exec(); - } + if (mergeId) { + await Account.updateOne({ _id: account._id, 'merges._id': mergeId }, { 'merges.$.split': true }).exec(); + } - await Promise.all([ - updateCharacterCount(accountId), - updateCharacterCount(unmerge._id), - accountChanged(accountId), - ]); + await Promise.all([ + updateCharacterCount(accountId), + updateCharacterCount(unmerge._id), + accountChanged(accountId), + ]); - system(account._id, `Split off ${unmerge.name} [${unmerge._id}] (${reason}) (${Date.now() - start}ms)`); + system(account._id, `Split off ${unmerge.name} [${unmerge._id}] (${reason}) (${Date.now() - start}ms)`); } export const mergeAccounts = makeQueued(merge); diff --git a/src/ts/server/api/origins.ts b/src/ts/server/api/origins.ts index 2a6e44e..f4aed59 100644 --- a/src/ts/server/api/origins.ts +++ b/src/ts/server/api/origins.ts @@ -7,107 +7,107 @@ import { updateAccount } from '../db'; import { AdminService } from '../services/adminService'; export async function getOriginStats(accounts: Account[]): Promise { - let totalOrigins = 0; - let totalOriginsIP4 = 0; - let totalOriginsIP6 = 0; + let totalOrigins = 0; + let totalOriginsIP4 = 0; + let totalOriginsIP6 = 0; - const distribution: number[] = []; - const uniques = new Set(); - const duplicates = new Set(); + const distribution: number[] = []; + const uniques = new Set(); + const duplicates = new Set(); - for (const account of accounts) { - if (account.origins) { - for (const origin of account.origins) { - totalOrigins++; + for (const account of accounts) { + if (account.origins) { + for (const origin of account.origins) { + totalOrigins++; - if (uniques.has(origin.ip)) { - duplicates.add(origin.ip); - } else { - uniques.add(origin.ip); - } + if (uniques.has(origin.ip)) { + duplicates.add(origin.ip); + } else { + uniques.add(origin.ip); + } - if (origin.ip.indexOf(':') !== -1) { - totalOriginsIP6++; - } else { - totalOriginsIP4++; - } - } - } + if (origin.ip.indexOf(':') !== -1) { + totalOriginsIP6++; + } else { + totalOriginsIP4++; + } + } + } - const count = account.origins ? account.origins.length : 0; + const count = account.origins ? account.origins.length : 0; - while (distribution.length <= count) { - distribution.push(0); - } + while (distribution.length <= count) { + distribution.push(0); + } - distribution[count]++; - } + distribution[count]++; + } - const uniqueOrigins = uniques.size; - const duplicateOrigins = duplicates.size; - const singleOrigins = uniqueOrigins - duplicateOrigins; + const uniqueOrigins = uniques.size; + const duplicateOrigins = duplicates.size; + const singleOrigins = uniqueOrigins - duplicateOrigins; - return { - uniqueOrigins, duplicateOrigins, singleOrigins, totalOrigins, totalOriginsIP4, totalOriginsIP6, distribution - }; + return { + uniqueOrigins, duplicateOrigins, singleOrigins, totalOrigins, totalOriginsIP4, totalOriginsIP6, distribution + }; } export function removeAllOrigins(service: AdminService, accountId: string) { - service.removeOriginsFromAccount(accountId); - return updateAccount(accountId, { origins: [] }); + service.removeOriginsFromAccount(accountId); + return updateAccount(accountId, { origins: [] }); } export function removeOrigins(service: AdminService, accountId: string, ips: string[]) { - service.removeOriginsFromAccount(accountId, ips); - return updateAccount(accountId, { $pull: { origins: { ip: { $in: ips } } } }); + service.removeOriginsFromAccount(accountId, ips); + return updateAccount(accountId, { $pull: { origins: { ip: { $in: ips } } } }); } export function addOrigin(accountId: string, { ip, country }: OriginInfo) { - return updateAccount(accountId, { $push: { origins: { ip, country, last: new Date() } } }); + return updateAccount(accountId, { $push: { origins: { ip, country, last: new Date() } } }); } export async function clearOriginsForAccount(service: AdminService, accountId: string, options: ClearOrignsOptions) { - const account = service.accounts.get(accountId); + const account = service.accounts.get(accountId); - if (account) { - const { ips } = getOriginsToRemove(account, options); - await removeOrigins(service, accountId, ips); - } + if (account) { + const { ips } = getOriginsToRemove(account, options); + await removeOrigins(service, accountId, ips); + } } export async function clearOriginsForAccounts(service: AdminService, accounts: string[], options: ClearOrignsOptions) { - await Bluebird.map(accounts, id => clearOriginsForAccount(service, id, options), { concurrency: 4 }); + await Bluebird.map(accounts, id => clearOriginsForAccount(service, id, options), { concurrency: 4 }); } export async function clearOrigins( - service: AdminService, count: number, andHigher: boolean, options: ClearOrignsOptions + service: AdminService, count: number, andHigher: boolean, options: ClearOrignsOptions ) { - const origins = service.accounts.items - .filter(a => a.originsRefs && (andHigher ? a.originsRefs.length >= count : a.originsRefs.length === count)) - .map(a => getOriginsToRemove(a, options)) - .filter(({ ips }) => !!ips.length); + const origins = service.accounts.items + .filter(a => a.originsRefs && (andHigher ? a.originsRefs.length >= count : a.originsRefs.length === count)) + .map(a => getOriginsToRemove(a, options)) + .filter(({ ips }) => !!ips.length); - await Bluebird.map(origins, o => removeOrigins(service, o.accountId, o.ips), { concurrency: 4 }); + await Bluebird.map(origins, o => removeOrigins(service, o.accountId, o.ips), { concurrency: 4 }); } const isBanned = (origin: Origin) => origin.ban || origin.mute || origin.shadow; function getOriginsToRemove(account: Account, { old, singles, trim, veryOld, country }: ClearOrignsOptions): AccountOrigins { - const date = fromNow((veryOld ? -90 : -14) * DAY).getTime(); - const originsRefs = account.originsRefs || []; - const filtered = country ? - originsRefs.filter(({ origin }) => origin.country === country) : - originsRefs.filter(({ last, origin }) => { - return (!old || (!last || last.getTime() < date)) - && (!singles || origin.accounts!.length === 1) - && !isBanned(origin); - }); + const date = fromNow((veryOld ? -90 : -14) * DAY).getTime(); + const originsRefs = account.originsRefs || []; + const filtered = country ? + originsRefs.filter(({ origin }) => origin.country === country) : + originsRefs.filter(({ last, origin }) => { + return (!old || (!last || last.getTime() < date)) + && (!singles || origin.accounts!.length === 1) + && !isBanned(origin); + }); - const ips = filtered.map(({ origin }) => origin.ip); + const ips = filtered.map(({ origin }) => origin.ip); - if (trim) { - ips.push(...difference(originsRefs.map(({ origin }) => origin.ip), ips).slice(10)); - } + if (trim) { + ips.push(...difference(originsRefs.map(({ origin }) => origin.ip), ips).slice(10)); + } - return { accountId: account._id, ips }; + return { accountId: account._id, ips }; } diff --git a/src/ts/server/api/ponies.ts b/src/ts/server/api/ponies.ts index b3bbf15..e4d4840 100644 --- a/src/ts/server/api/ponies.ts +++ b/src/ts/server/api/ponies.ts @@ -15,98 +15,98 @@ const ITEMS_LIMIT = 1000; const CACHE_TIMEOUT = 10 * MINUTE; function createQuery({ search }: FindPonyQuery) { - const and: any[] = []; + const and: any[] = []; - if (search) { - if (search === 'orphan') { - and.push({ account: { $exists: false } }); - } else if (/^exact:/.test(search)) { - and.push({ name: new RegExp(`^${escapeRegExp(search.substr(6))}$`, 'i') }); - } else { - and.push({ name: new RegExp(escapeRegExp(search), 'i') }); - } - } + if (search) { + if (search === 'orphan') { + and.push({ account: { $exists: false } }); + } else if (/^exact:/.test(search)) { + and.push({ name: new RegExp(`^${escapeRegExp(search.substr(6))}$`, 'i') }); + } else { + and.push({ name: new RegExp(escapeRegExp(search), 'i') }); + } + } - return and.length === 0 ? {} : (and.length === 1 ? and[0] : { $and: and }); + return and.length === 0 ? {} : (and.length === 1 ? and[0] : { $and: and }); } async function getPonyIds(query: FindPonyQuery) { - const items: ICharacter[] = await Character - .find(createQuery(query), '_id') - .sort(query.orderBy || 'createdAt') - .limit(ITEMS_LIMIT) - .lean() - .exec(); + const items: ICharacter[] = await Character + .find(createQuery(query), '_id') + .sort(query.orderBy || 'createdAt') + .limit(ITEMS_LIMIT) + .lean() + .exec(); - return items.map(i => i._id.toString()); + return items.map(i => i._id.toString()); } const cachedGetPonyIds = cached(getPonyIds, CACHE_TIMEOUT); export async function findPonies(query: FindPonyQuery, page: number) { - const from = page * ITEMS_PER_PAGE; - const ids = await cachedGetPonyIds(query); - const idsOnPage = ids.slice(from, from + ITEMS_PER_PAGE); + const from = page * ITEMS_PER_PAGE; + const ids = await cachedGetPonyIds(query); + const idsOnPage = ids.slice(from, from + ITEMS_PER_PAGE); - return { - items: idsOnPage, - totalCount: ids.length - }; + return { + items: idsOnPage, + totalCount: ids.length + }; } export async function assignCharacter(characterId: string, accountId: string) { - const character = await Character.findById(characterId).exec(); + const character = await Character.findById(characterId).exec(); - if (!character) - return; + if (!character) + return; - await kickFromAllServersByCharacter(characterId); - await Character.updateOne({ _id: characterId }, { account: accountId }).exec(); - await Promise.all([ - updateCharacterCount(character.account), - updateCharacterCount(accountId), - ]); + await kickFromAllServersByCharacter(characterId); + await Character.updateOne({ _id: characterId }, { account: accountId }).exec(); + await Promise.all([ + updateCharacterCount(character.account), + updateCharacterCount(accountId), + ]); } export async function removeCharacter(service: AdminService, characterId: string) { - const character = await Character.findById(characterId).exec(); + const character = await Character.findById(characterId).exec(); - if (!character) - return; + if (!character) + return; - await kickFromAllServersByCharacter(characterId); - await character.remove(); - await updateCharacterCount(character.account); - logRemovedCharacter(character); - service.ponies.removed(characterId); + await kickFromAllServersByCharacter(characterId); + await character.remove(); + await updateCharacterCount(character.account); + logRemovedCharacter(character); + service.ponies.removed(characterId); } async function removeCharacters(character: ICharacter[], accountId: string, removedDocument: RemovedDocument) { - await Bluebird.map(character, async c => { - await c.remove(); - await removedDocument('ponies', c._id.toString()); - logRemovedCharacter(c); - }, { concurrency: 4 }); + await Bluebird.map(character, async c => { + await c.remove(); + await removedDocument('ponies', c._id.toString()); + logRemovedCharacter(c); + }, { concurrency: 4 }); - await updateCharacterCount(accountId); + await updateCharacterCount(accountId); } export async function removeCharactersAboveLimit(removedDocument: RemovedDocument, accountId: string) { - const [account, items] = await Promise.all([ - findAccountSafe(accountId), - Character.find({ account: accountId }).sort({ lastUsed: -1 }).exec(), - ]); + const [account, items] = await Promise.all([ + findAccountSafe(accountId), + Character.find({ account: accountId }).sort({ lastUsed: -1 }).exec(), + ]); - const limited = items.slice(getCharacterLimit(account)); - await removeCharacters(limited, accountId, removedDocument); + const limited = items.slice(getCharacterLimit(account)); + await removeCharacters(limited, accountId, removedDocument); } export async function removeAllCharacters(removedDocument: RemovedDocument, accountId: string) { - const items = await Character.find({ account: accountId }).sort({ lastUsed: -1 }).exec(); - await removeCharacters(items, accountId, removedDocument); + const items = await Character.find({ account: accountId }).sort({ lastUsed: -1 }).exec(); + await removeCharacters(items, accountId, removedDocument); } export async function createCharacter(account: string, name: string, info: string) { - await Character.create({ account, name, info }); - await updateCharacterCount(account); + await Character.create({ account, name, info }); + await updateCharacterCount(account); } diff --git a/src/ts/server/api/pony.ts b/src/ts/server/api/pony.ts index 1f9b3cb..dec7944 100644 --- a/src/ts/server/api/pony.ts +++ b/src/ts/server/api/pony.ts @@ -14,7 +14,7 @@ import { getCharacterLimit } from '../accountUtils'; import { PLAYER_DESC_MAX_LENGTH } from '../../common/constants'; function colorToText(c: number): string { - return c ? colorToHexRGB(c) : ''; + return c ? colorToHexRGB(c) : ''; } export type UpdateCharacterCount = (accountId: ID) => Promise; @@ -23,126 +23,126 @@ export type SavePony = ReturnType; export type RemovePony = ReturnType; export const createSavePony = - ( - findCharacter: FindCharacter, findAuth: FindAuth, characterCount: CharacterCount, - updateCharacterCount: UpdateCharacterCount, createCharacter: CreateCharacter, log: LogAccountMessage, - isSuspiciousName: (name: string) => boolean, isSuspiciousPony: (info: PonyInfoNumber) => boolean, - ) => - async (account: IAccount, data: Partial | undefined, reporter: Reporter): Promise => { - if (!data || !data.info || typeof data.name !== 'string') { - throw new UserError('Invalid data', { data }); - } + ( + findCharacter: FindCharacter, findAuth: FindAuth, characterCount: CharacterCount, + updateCharacterCount: UpdateCharacterCount, createCharacter: CreateCharacter, log: LogAccountMessage, + isSuspiciousName: (name: string) => boolean, isSuspiciousPony: (info: PonyInfoNumber) => boolean, + ) => + async (account: IAccount, data: Partial | undefined, reporter: Reporter): Promise => { + if (!data || !data.info || typeof data.name !== 'string') { + throw new UserError('Invalid data', { data }); + } - const originalName = data.name; - data.name = cleanName(data.name); + const originalName = data.name; + data.name = cleanName(data.name); - if (!validatePonyName(data.name)) { - throw new UserError('Invalid name', { desc: JSON.stringify(originalName), data }); - } + if (!validatePonyName(data.name)) { + throw new UserError('Invalid name', { desc: JSON.stringify(originalName), data }); + } - let [character, auth] = await Promise.all([ - data.id ? findCharacter(data.id, account._id) : undefined, - data.site ? findAuth(data.site, account._id, '_id') : undefined, - ]).catch(error => { - throw new UserError('Invalid data', { error, data }); - }); + let [character, auth] = await Promise.all([ + data.id ? findCharacter(data.id, account._id) : undefined, + data.site ? findAuth(data.site, account._id, '_id') : undefined, + ]).catch(error => { + throw new UserError('Invalid data', { error, data }); + }); - let suspicious: string[] = []; - let created = false; - let nameChanged = false; - let oldName: string | undefined; + let suspicious: string[] = []; + let created = false; + let nameChanged = false; + let oldName: string | undefined; - try { - if (!character) { - character = createCharacter(account); - created = true; - } + try { + if (!character) { + character = createCharacter(account); + created = true; + } - const deco = decompressPony(data.info); - const info = compressPony(deco); + const deco = decompressPony(data.info); + const info = compressPony(deco); - // if (data.info !== info) { - // reporter.danger(`Pony info does not match after re-compression`, `original: ${data.info}\nre-compressed: ${info}`); - // } + // if (data.info !== info) { + // reporter.danger(`Pony info does not match after re-compression`, `original: ${data.info}\nre-compressed: ${info}`); + // } - const badCM = isBadCM(deco.cm && deco.cm.map(colorToText) || [], colorToHexRGB(deco.coatFill!)); - const forbiddenName = isForbiddenName(data.name); - const flags = - (badCM ? CharacterFlags.BadCM : 0) | - (data.hideSupport ? CharacterFlags.HideSupport : 0) | - (data.respawnAtSpawn ? CharacterFlags.RespawnAtSpawn : 0) | - (forbiddenName ? CharacterFlags.ForbiddenName : 0); + const badCM = isBadCM(deco.cm && deco.cm.map(colorToText) || [], colorToHexRGB(deco.coatFill!)); + const forbiddenName = isForbiddenName(data.name); + const flags = + (badCM ? CharacterFlags.BadCM : 0) | + (data.hideSupport ? CharacterFlags.HideSupport : 0) | + (data.respawnAtSpawn ? CharacterFlags.RespawnAtSpawn : 0) | + (forbiddenName ? CharacterFlags.ForbiddenName : 0); - nameChanged = character.name !== data.name; - oldName = character.name; + nameChanged = character.name !== data.name; + oldName = character.name; - if (nameChanged && isSuspiciousName(data.name)) { - suspicious.push('name'); - } + if (nameChanged && isSuspiciousName(data.name)) { + suspicious.push('name'); + } - if (character.info !== data.info && isSuspiciousPony(deco)) { - suspicious.push('look'); - } + if (character.info !== data.info && isSuspiciousPony(deco)) { + suspicious.push('look'); + } - character.desc = typeof data.desc === 'string' ? data.desc.substr(0, PLAYER_DESC_MAX_LENGTH) : ''; - character.name = data.name; - character.tag = data.tag; - character.site = auth ? auth._id : null; - character.info = info; - character.flags = flags; - character.lastUsed = new Date(); - } catch (error) { - const message = DEVELOPMENT ? `${CHARACTER_SAVING_ERROR} (${error})` : CHARACTER_SAVING_ERROR; - throw new UserError(message, { error, data: { pony: data }, desc: `info: "${data.info}"` }); - } + character.desc = typeof data.desc === 'string' ? data.desc.substr(0, PLAYER_DESC_MAX_LENGTH) : ''; + character.name = data.name; + character.tag = data.tag; + character.site = auth ? auth._id : null; + character.info = info; + character.flags = flags; + character.lastUsed = new Date(); + } catch (error) { + const message = DEVELOPMENT ? `${CHARACTER_SAVING_ERROR} (${error})` : CHARACTER_SAVING_ERROR; + throw new UserError(message, { error, data: { pony: data }, desc: `info: "${data.info}"` }); + } - const count = created ? await characterCount(account._id) : 0; + const count = created ? await characterCount(account._id) : 0; - if (count >= getCharacterLimit(account)) { - throw new UserError(CHARACTER_LIMIT_ERROR); - } + if (count >= getCharacterLimit(account)) { + throw new UserError(CHARACTER_LIMIT_ERROR); + } - await character.save(); + await character.save(); - if (created) { - await updateCharacterCount(account._id); - } + if (created) { + await updateCharacterCount(account._id); + } - if (suspicious.length) { - reporter.setPony(character._id.toString()); - reporter.warn('Suspicious pony created', `"${character.name}" (${suspicious.join(', ')})`); - } + if (suspicious.length) { + reporter.setPony(character._id.toString()); + reporter.warn('Suspicious pony created', `"${character.name}" (${suspicious.join(', ')})`); + } - if (created) { - log(account._id, `created pony "${character.name}"`); - } else if (nameChanged) { - log(account._id, `renamed pony "${oldName}" => "${character.name}"`); - } + if (created) { + log(account._id, `created pony "${character.name}"`); + } else if (nameChanged) { + log(account._id, `renamed pony "${oldName}" => "${character.name}"`); + } - return toPonyObject(character); - }; + return toPonyObject(character); + }; export const createRemovePony = - ( - kickFromAllServersByCharacter: (ponyId: string) => void, - removeCharacter: RemoveCharacter, - updateCharacterCount: UpdateCharacterCount, - removedCharacter: (ponyId: string) => void, - logRemovedCharacter: (character: ICharacter) => void, - ) => - async (ponyId: unknown, accountId: string) => { - if (!ponyId || typeof ponyId !== 'string') { - throw new Error(`Invalid ponyId (${ponyId})`); - } + ( + kickFromAllServersByCharacter: (ponyId: string) => void, + removeCharacter: RemoveCharacter, + updateCharacterCount: UpdateCharacterCount, + removedCharacter: (ponyId: string) => void, + logRemovedCharacter: (character: ICharacter) => void, + ) => + async (ponyId: unknown, accountId: string) => { + if (!ponyId || typeof ponyId !== 'string') { + throw new Error(`Invalid ponyId (${ponyId})`); + } - await kickFromAllServersByCharacter(ponyId); - const character = await removeCharacter(ponyId, accountId); - await updateCharacterCount(accountId); + await kickFromAllServersByCharacter(ponyId); + const character = await removeCharacter(ponyId, accountId); + await updateCharacterCount(accountId); - if (character) { - logRemovedCharacter(character); - removedCharacter(ponyId); - } + if (character) { + logRemovedCharacter(character); + removedCharacter(ponyId); + } - return {}; - }; + return {}; + }; diff --git a/src/ts/server/authUtils.ts b/src/ts/server/authUtils.ts index 1d3b6b1..d547dfb 100644 --- a/src/ts/server/authUtils.ts +++ b/src/ts/server/authUtils.ts @@ -8,99 +8,99 @@ import { UserError } from './userError'; import { CreateAccountOptions, connectOnlySocialError } from './accountUtils'; export async function assignAuth(auth: IAuth, account: IAccount) { - if (!auth.account || !auth.account.equals(account._id)) { - system(account._id, `connected auth ${auth.name} [${auth._id}]`); - await updateAuth(auth._id, { account: account._id }); - return true; - } else { - return false; - } + if (!auth.account || !auth.account.equals(account._id)) { + system(account._id, `connected auth ${auth.name} [${auth._id}]`); + await updateAuth(auth._id, { account: account._id }); + return true; + } else { + return false; + } } export async function findOrCreateAuth(profile: Profile, accountId: string | undefined, options: CreateAccountOptions) { - let auth = await findAuthByOpenId(profile.id, profile.provider); + let auth = await findAuthByOpenId(profile.id, profile.provider); - if (auth) { - await updateAuthInfo(updateAuth, auth, profile, accountId); - } else { - if (options.connectOnly && !accountId) { - if (profile.emails.length) { - const account = await Account.findOne({ emails: { $in: profile.emails } }).exec(); + if (auth) { + await updateAuthInfo(updateAuth, auth, profile, accountId); + } else { + if (options.connectOnly && !accountId) { + if (profile.emails.length) { + const account = await Account.findOne({ emails: { $in: profile.emails } }).exec(); - if (!account) { - throw new UserError(connectOnlySocialError); - } - } else { - throw new UserError(connectOnlySocialError); - } - } + if (!account) { + throw new UserError(connectOnlySocialError); + } + } else { + throw new UserError(connectOnlySocialError); + } + } - auth = await createAuth(profile, accountId); - } + auth = await createAuth(profile, accountId); + } - await verifyOrRestoreAuth(auth, accountId); - return auth; + await verifyOrRestoreAuth(auth, accountId); + return auth; } export async function updateAuthInfo( - updateAuth: UpdateAuth, auth: IAuth | undefined, profile: Profile, accountId: string | undefined + updateAuth: UpdateAuth, auth: IAuth | undefined, profile: Profile, accountId: string | undefined ) { - if (!auth) - return; + if (!auth) + return; - const changes: Partial = {}; + const changes: Partial = {}; - if (profile.url && auth.url !== profile.url) { - changes.url = profile.url; - } + if (profile.url && auth.url !== profile.url) { + changes.url = profile.url; + } - if (profile.username && auth.name !== profile.username) { - changes.name = profile.username; - } + if (profile.username && auth.name !== profile.username) { + changes.name = profile.username; + } - if (profile.emails && profile.emails.length) { - if (!auth.emails || !arraysEqual(auth.emails.sort(), profile.emails.sort())) { - changes.emails = uniq([...(auth.emails || []), ...profile.emails]); - } - } + if (profile.emails && profile.emails.length) { + if (!auth.emails || !arraysEqual(auth.emails.sort(), profile.emails.sort())) { + changes.emails = uniq([...(auth.emails || []), ...profile.emails]); + } + } - if (!auth.account && accountId) { - changes.account = Types.ObjectId(accountId); - } + if (!auth.account && accountId) { + changes.account = Types.ObjectId(accountId); + } - if (Object.keys(changes).length > 0) { - Object.assign(auth, changes); - await updateAuth(auth._id, changes); - } + if (Object.keys(changes).length > 0) { + Object.assign(auth, changes); + await updateAuth(auth._id, changes); + } } async function createAuth(profile: Profile, account: string | undefined) { - if (!profile.id) { - throw new Error('Missing profile ID'); - } + if (!profile.id) { + throw new Error('Missing profile ID'); + } - return await Auth.create({ - account, - openId: profile.id, - provider: profile.provider, - name: profile.username, - url: profile.url, - emails: profile.emails || [], - lastUsed: new Date(), - }); + return await Auth.create({ + account, + openId: profile.id, + provider: profile.provider, + name: profile.username, + url: profile.url, + emails: profile.emails || [], + lastUsed: new Date(), + }); } async function verifyOrRestoreAuth(auth: IAuth, mergeAccount: string | undefined) { - const changes: Partial = { lastUsed: new Date() }; + const changes: Partial = { lastUsed: new Date() }; - if (auth.disabled || auth.banned) { - if (!auth.banned && auth.account && !!mergeAccount) { - changes.disabled = false; - } else { - throw new UserError('Cannot sign-in using this social account'); - } - } + if (auth.disabled || auth.banned) { + if (!auth.banned && auth.account && !!mergeAccount) { + changes.disabled = false; + } else { + throw new UserError('Cannot sign-in using this social account'); + } + } - Object.assign(auth, changes); - await updateAuth(auth._id, changes); + Object.assign(auth, changes); + await updateAuth(auth._id, changes); } diff --git a/src/ts/server/canvasUtilsNode.ts b/src/ts/server/canvasUtilsNode.ts index a9d840a..95afa4c 100644 --- a/src/ts/server/canvasUtilsNode.ts +++ b/src/ts/server/canvasUtilsNode.ts @@ -7,16 +7,16 @@ import { setup } from '../client/canvasUtils'; export const createCanvas = createNodeCanvas; export async function loadImage(src: string) { - const buffer = await readFileAsync(src); - const image = new Image(); - image.src = buffer; - return image; + const buffer = await readFileAsync(src); + const image = new Image(); + image.src = buffer; + return image; } export function loadImageSync(src: string) { - const image = new Image(); - image.src = readFileSync(src); - return image; + const image = new Image(); + image.src = readFileSync(src); + return image; } setup({ createCanvas: createNodeCanvas, loadImage }); diff --git a/src/ts/server/characterUtils.ts b/src/ts/server/characterUtils.ts index 757919f..04cf164 100644 --- a/src/ts/server/characterUtils.ts +++ b/src/ts/server/characterUtils.ts @@ -26,195 +26,195 @@ import { encodeExpression } from '../common/encoders/expressionEncoder'; export const defaultCharacterState: CharacterState = { x: 0, y: 0 }; export function encryptInfo(info: string) { - return bitmask(toByteArray(info), PONY_INFO_KEY); + return bitmask(toByteArray(info), PONY_INFO_KEY); } export function createPony(account: IAccount, character: ICharacter, state: CharacterState) { - const pony = ponyEntity(state.x, state.y) as ServerEntity; - pony.state = hasFlag(state.flags, CharacterStateFlags.Right) ? EntityState.FacingRight : 0; - updatePony(pony, account, character); - updatePonyFromState(pony, state); - cleanupPonyOptions(pony); - return pony; + const pony = ponyEntity(state.x, state.y) as ServerEntity; + pony.state = hasFlag(state.flags, CharacterStateFlags.Right) ? EntityState.FacingRight : 0; + updatePony(pony, account, character); + updatePonyFromState(pony, state); + cleanupPonyOptions(pony); + return pony; } function createDefaultCharacterState(map: ServerMap): CharacterState { - return { - ...defaultCharacterState, - ...randomPoint(map.spawnArea), - map: map.id, - }; + return { + ...defaultCharacterState, + ...randomPoint(map.spawnArea), + map: map.id, + }; } export function getCharacterState(character: ICharacter, serverId: string, map: ServerMap): CharacterState { - return character.state && character.state[serverId] || createDefaultCharacterState(map); + return character.state && character.state[serverId] || createDefaultCharacterState(map); } export async function updateCharacterState(characterId: string, serverId: string, state: CharacterState) { - await Character.updateOne({ _id: characterId }, { [`state.${serverId}`]: state }).exec(); + await Character.updateOne({ _id: characterId }, { [`state.${serverId}`]: state }).exec(); } export function getAndFixCharacterState( - server: ServerConfig, character: ICharacter, world: World, states: CounterService + server: ServerConfig, character: ICharacter, world: World, states: CounterService ): CharacterState { - const map = world.getMainMap(); - const savedState = last(states.get(character._id.toString()).items) || getCharacterState(character, server.id, map); - const state = { ...defaultCharacterState, ...savedState }; + const map = world.getMainMap(); + const savedState = last(states.get(character._id.toString()).items) || getCharacterState(character, server.id, map); + const state = { ...defaultCharacterState, ...savedState }; - if (hasFlag(character.flags, CharacterFlags.RespawnAtSpawn)) { - Object.assign(state, { map: map.id, ...randomPoint(map.spawnArea) }); - } + if (hasFlag(character.flags, CharacterFlags.RespawnAtSpawn)) { + Object.assign(state, { map: map.id, ...randomPoint(map.spawnArea) }); + } - return state; + return state; } export function updatePonyFromState(pony: ServerEntity, state: CharacterState) { - if (!pony.options) { - pony.options = {}; - } + if (!pony.options) { + pony.options = {}; + } - if (state.hold) { - const type = getEntityType(state.hold); + if (state.hold) { + const type = getEntityType(state.hold); - if (type) { - pony.options.hold = type; - } - } else if (pony.options.hold) { - pony.options.hold = 0; - } + if (type) { + pony.options.hold = type; + } + } else if (pony.options.hold) { + pony.options.hold = 0; + } - if (state.toy) { - pony.options.toy = state.toy; - } else if (pony.options.toy) { - pony.options.toy = 0; - } + if (state.toy) { + pony.options.toy = state.toy; + } else if (pony.options.toy) { + pony.options.toy = 0; + } - pony.options.extra = hasFlag(state.flags, CharacterStateFlags.Extra); + pony.options.extra = hasFlag(state.flags, CharacterStateFlags.Extra); } export function cleanupPonyOptions({ options }: ServerEntity) { - if (options) { - if (!options.hold) { - delete options.hold; - } + if (options) { + if (!options.hold) { + delete options.hold; + } - if (!options.extra) { - delete options.extra; - } - } + if (!options.extra) { + delete options.extra; + } + } } export function filterForbidden(name: string) { - const isForbidden = isForbiddenName(name); - return isForbidden ? repeat('?', name.length) : name; + const isForbidden = isForbiddenName(name); + return isForbidden ? repeat('?', name.length) : name; } export function updatePony(pony: ServerEntity, account: IAccount, character: ICharacter) { - const info = character.info || ''; - const ponyInfo = decompressPony(info); - const originalName = replaceEmojis(character.name); - const allowedName = filterForbidden(originalName); - const options: PonyOptions = {}; - const level = supporterLevel(account); + const info = character.info || ''; + const ponyInfo = decompressPony(info); + const originalName = replaceEmojis(character.name); + const allowedName = filterForbidden(originalName); + const options: PonyOptions = {}; + const level = supporterLevel(account); - if (character.tag && canUseTag(account, character.tag)) { - options.tag = character.tag; - } else if (level && !hasFlag(character.flags, CharacterFlags.HideSupport)) { - options.tag = `sup${level}`; - } + if (character.tag && canUseTag(account, character.tag)) { + options.tag = character.tag; + } else if (level && !hasFlag(character.flags, CharacterFlags.HideSupport)) { + options.tag = `sup${level}`; + } - pony.options = options; - pony.extraOptions = createExtraOptions(character); - pony.canFly = canFly(ponyInfo); - pony.canMagic = canMagic(ponyInfo); + pony.options = options; + pony.extraOptions = createExtraOptions(character); + pony.canFly = canFly(ponyInfo); + pony.canMagic = canMagic(ponyInfo); - // name - setEntityName(pony, allowedName); + // name + setEntityName(pony, allowedName); - // info - pony.info = info; + // info + pony.info = info; - if (hasFlag(character.flags, CharacterFlags.BadCM) && ponyInfo.cm) { - ponyInfo.cm = undefined; - pony.infoSafe = compressPony(ponyInfo); - pony.encryptedInfoSafe = encryptInfo(pony.infoSafe); - } else { - pony.infoSafe = pony.info; - pony.encryptedInfoSafe = encryptInfo(info); - } + if (hasFlag(character.flags, CharacterFlags.BadCM) && ponyInfo.cm) { + ponyInfo.cm = undefined; + pony.infoSafe = compressPony(ponyInfo); + pony.encryptedInfoSafe = encryptInfo(pony.infoSafe); + } else { + pony.infoSafe = pony.info; + pony.encryptedInfoSafe = encryptInfo(info); + } - // crc - pony.crc = createCharacterCRC(account._id.toString(), originalName); + // crc + pony.crc = createCharacterCRC(account._id.toString(), originalName); } function createCharacterCRC(accountId: string, characterName: string) { - const characterNameBuffer = encodeString(characterName)!; - const accountIdBuffer = encodeString(accountId)!; - const buffer = new Uint32Array(Math.ceil((characterNameBuffer.byteLength + accountIdBuffer.byteLength) / 4)); - const bufferUint8 = new Uint8Array(buffer.buffer); - bufferUint8.set(characterNameBuffer); - bufferUint8.set(accountIdBuffer, characterNameBuffer.byteLength); - return computeCRC(buffer) & 0xffff; + const characterNameBuffer = encodeString(characterName)!; + const accountIdBuffer = encodeString(accountId)!; + const buffer = new Uint32Array(Math.ceil((characterNameBuffer.byteLength + accountIdBuffer.byteLength) / 4)); + const bufferUint8 = new Uint8Array(buffer.buffer); + bufferUint8.set(characterNameBuffer); + bufferUint8.set(accountIdBuffer, characterNameBuffer.byteLength); + return computeCRC(buffer) & 0xffff; } export function createExtraOptions(character: ICharacter) { - const options: any = { - ex: true, - }; + const options: any = { + ex: true, + }; - if (character.auth && !isForbiddenName(character.auth.name)) { - options.site = { - provider: character.auth.provider, - name: character.auth.name, - url: character.auth.url, - }; - } + if (character.auth && !isForbiddenName(character.auth.name)) { + options.site = { + provider: character.auth.provider, + name: character.auth.name, + url: character.auth.url, + }; + } - return options; + return options; } export function logRemovedCharacter({ _id, account, name, info }: ICharacter) { - log(systemMessage(`${account}`, `removed pony [${_id}] "${name}" ${info}`)); + log(systemMessage(`${account}`, `removed pony [${_id}] "${name}" ${info}`)); } export async function swapCharacter(client: IClient, { server }: World, query: MongoQuery) { - if (client.isSwitchingMap) - return; + if (client.isSwitchingMap) + return; - if ((Date.now() - client.lastSwap) < SWAP_TIMEOUT) { - return; - } + if ((Date.now() - client.lastSwap) < SWAP_TIMEOUT) { + return; + } - const character = await queryCharacter(query); + const character = await queryCharacter(query); - if (!character) { - return saySystem(client, `Can't find character`); - } + if (!character) { + return saySystem(client, `Can't find character`); + } - if (isPonyFlying(client.pony) && !canFly(decompressPony(character.info || ''))) { - return saySystem(client, `Can't swap to that character in-flight`); - } + if (isPonyFlying(client.pony) && !canFly(decompressPony(character.info || ''))) { + return saySystem(client, `Can't swap to that character in-flight`); + } - const state = createCharacterState(client.pony, client.map); - updateCharacterState(client.characterId, server.id, state) - .catch(logger.error); + const state = createCharacterState(client.pony, client.map); + updateCharacterState(client.characterId, server.id, state) + .catch(logger.error); - Character.updateOne({ _id: character._id }, { lastUsed: new Date() }).exec() - .catch(logger.error); + Character.updateOne({ _id: character._id }, { lastUsed: new Date() }).exec() + .catch(logger.error); - updateClientCharacter(client, character); - updatePony(client.pony, client.account, client.character); - updatePonyFromState(client.pony, getCharacterState(character, server.id, client.map)); - const options = client.pony.options as PonyOptions; - options.expr = encodeExpression(undefined); - client.pony.state &= ~EntityState.Magic; - pushUpdateEntity({ - entity: client.pony, options: { hold: 0, toy: 0, ...options }, - flags: UpdateFlags.Info | UpdateFlags.Name | UpdateFlags.Options | UpdateFlags.State, - }); + updateClientCharacter(client, character); + updatePony(client.pony, client.account, client.character); + updatePonyFromState(client.pony, getCharacterState(character, server.id, client.map)); + const options = client.pony.options as PonyOptions; + options.expr = encodeExpression(undefined); + client.pony.state &= ~EntityState.Magic; + pushUpdateEntity({ + entity: client.pony, options: { hold: 0, toy: 0, ...options }, + flags: UpdateFlags.Info | UpdateFlags.Name | UpdateFlags.Options | UpdateFlags.State, + }); - cleanupPonyOptions(client.pony); - client.myEntity(client.pony.id, client.characterName, client.character.info!, client.characterId, client.pony.crc || 0); - client.reporter.systemLog(`Swapped to "${client.characterName}"`); - client.lastSwap = Date.now(); + cleanupPonyOptions(client.pony); + client.myEntity(client.pony.id, client.characterName, client.character.info!, client.characterId, client.pony.crc || 0); + client.reporter.systemLog(`Swapped to "${client.characterName}"`); + client.lastSwap = Date.now(); } diff --git a/src/ts/server/chat.ts b/src/ts/server/chat.ts index db2c3b3..4b820fd 100644 --- a/src/ts/server/chat.ts +++ b/src/ts/server/chat.ts @@ -1,8 +1,8 @@ import { repeat } from 'lodash'; import { isForbiddenMessage, createIsSuspiciousMessage } from '../common/security'; import { - ChatType, MessageType, Action, LeaveReason, isPublicChat, isPartyChat, isPublicMessage, isPartyMessage, - toMessageType, isWhisper, isWhisperTo + ChatType, MessageType, Action, LeaveReason, isPublicChat, isPartyChat, isPublicMessage, isPartyMessage, + toMessageType, isWhisper, isWhisperTo } from '../common/interfaces'; import { trimRepeatedLetters, urlRegexTexts, ipRegexText, urlExceptionRegex } from '../common/filterUtils'; import { parseExpression } from '../common/expressionUtils'; @@ -19,64 +19,64 @@ import { isWorldPointWithPaddingVisible } from '../common/camera'; import { tileWidth } from '../common/constants'; function isLaugh(message: string): boolean { - return /(^| )(ha(ha)+|he(he)+|ja(ja)+|ха(ха)+|lol|rofl)$/i.test(message); + return /(^| )(ha(ha)+|he(he)+|ja(ja)+|ха(ха)+|lol|rofl)$/i.test(message); } function isIP(match: string): boolean { - const parts = match.split(/\./g); - return !parts.some(p => /^0\d+$/.test(p)) && parts.map(x => parseInt(x, 10)).every(x => x >= 0 && x <= 255); + const parts = match.split(/\./g); + return !parts.some(p => /^0\d+$/.test(p)) && parts.map(x => parseInt(x, 10)).every(x => x >= 0 && x <= 255); } function replaceIP(match: string) { - return isIP(match) ? '[LINK]' : match; + return isIP(match) ? '[LINK]' : match; } const urlRegexes = urlRegexTexts.map(text => new RegExp(text, 'uig')); const ipRegex = new RegExp(ipRegexText, 'uig'); function replaceLink(value: string) { - return urlExceptionRegex.test(value) ? value : '[LINK]'; + return urlExceptionRegex.test(value) ? value : '[LINK]'; } export function filterUrls(message: string): string { - message = message.replace(ipRegex, replaceIP); + message = message.replace(ipRegex, replaceIP); - for (const regex of urlRegexes) { - message = message.replace(regex, replaceLink); - } + for (const regex of urlRegexes) { + message = message.replace(regex, replaceLink); + } - return message; + return message; } // non-party messages function getMessageType(client: IClient, type: ChatType) { - switch (type) { - case ChatType.Say: - case ChatType.Party: - return MessageType.Chat; - case ChatType.Supporter: - switch (client.supporterLevel) { - case 1: return MessageType.Supporter1; - case 2: return MessageType.Supporter2; - case 3: return MessageType.Supporter3; - default: return MessageType.Chat; - } - case ChatType.Supporter1: - return client.supporterLevel >= 1 ? MessageType.Supporter1 : MessageType.Chat; - case ChatType.Supporter2: - return client.supporterLevel >= 2 ? MessageType.Supporter2 : MessageType.Chat; - case ChatType.Supporter3: - return client.supporterLevel >= 3 ? MessageType.Supporter3 : MessageType.Chat; - case ChatType.Think: - case ChatType.PartyThink: - return MessageType.Thinking; - case ChatType.Dismiss: - return MessageType.Dismiss; - case ChatType.Whisper: - return MessageType.Whisper; - default: - return invalidEnumReturn(type, MessageType.Chat); - } + switch (type) { + case ChatType.Say: + case ChatType.Party: + return MessageType.Chat; + case ChatType.Supporter: + switch (client.supporterLevel) { + case 1: return MessageType.Supporter1; + case 2: return MessageType.Supporter2; + case 3: return MessageType.Supporter3; + default: return MessageType.Chat; + } + case ChatType.Supporter1: + return client.supporterLevel >= 1 ? MessageType.Supporter1 : MessageType.Chat; + case ChatType.Supporter2: + return client.supporterLevel >= 2 ? MessageType.Supporter2 : MessageType.Chat; + case ChatType.Supporter3: + return client.supporterLevel >= 3 ? MessageType.Supporter3 : MessageType.Chat; + case ChatType.Think: + case ChatType.PartyThink: + return MessageType.Thinking; + case ChatType.Dismiss: + return MessageType.Dismiss; + case ChatType.Whisper: + return MessageType.Whisper; + default: + return invalidEnumReturn(type, MessageType.Chat); + } } export type LogChat = (client: IClient, text: string, type: ChatType, ignored: boolean, target: IClient | undefined) => void; @@ -86,218 +86,218 @@ export type IsSuspiciousMessage = ReturnType; export type Say = ReturnType; export const createSay = - ( - world: World, runCommand: RunCommand, log: LogChat, checkSpam: OnMessageSettings, reportSwears: OnMessageSettings, - reportForbidden: OnMessageSettings, reportSuspicious: OnSuspiciousMessage, spamCommands: string[], - random: () => number, isSuspiciousMessage: IsSuspiciousMessage, - ) => - (client: IClient, text: string, chatType: ChatType, target: IClient | undefined, settings: GameServerSettings) => { - text = cleanMessage(text); + ( + world: World, runCommand: RunCommand, log: LogChat, checkSpam: OnMessageSettings, reportSwears: OnMessageSettings, + reportForbidden: OnMessageSettings, reportSuspicious: OnSuspiciousMessage, spamCommands: string[], + random: () => number, isSuspiciousMessage: IsSuspiciousMessage, + ) => + (client: IClient, text: string, chatType: ChatType, target: IClient | undefined, settings: GameServerSettings) => { + text = cleanMessage(text); - const { command, args, type } = parseCommand(text, chatType); - const whisper = type === ChatType.Whisper; + const { command, args, type } = parseCommand(text, chatType); + const whisper = type === ChatType.Whisper; - if (!command && !args) - return; + if (!command && !args) + return; - if (whisper && client === target) - return; + if (whisper && client === target) + return; - const forbidden = command == null && isPublicChat(type) && isForbiddenMessage(args); + const forbidden = command == null && isPublicChat(type) && isForbiddenMessage(args); - log(client, text, type, forbidden, target); + log(client, text, type, forbidden, target); - const suspicious = isSuspiciousMessage(args, settings); + const suspicious = isSuspiciousMessage(args, settings); - if (suspicious !== Suspicious.No) { - reportSuspicious(client, `${getChatPrefix(type)}${text}`, suspicious); - } + if (suspicious !== Suspicious.No) { + reportSuspicious(client, `${getChatPrefix(type)}${text}`, suspicious); + } - if (command != null) { - if (runCommand(client, command, args, type, target, settings)) { - if (type !== ChatType.Party && spamCommands.indexOf(command) !== -1) { - if (!client.map.instance) { - checkSpam(client, text, settings); - } - } - } else { - const expression = parseExpression(text.substr(1)); + if (command != null) { + if (runCommand(client, command, args, type, target, settings)) { + if (type !== ChatType.Party && spamCommands.indexOf(command) !== -1) { + if (!client.map.instance) { + checkSpam(client, text, settings); + } + } + } else { + const expression = parseExpression(text.substr(1)); - if (expression) { - setEntityExpression(client.pony, expression); - } else { - saySystem(client, 'Invalid command'); - } - } - } else { - const message = args; - const think = type === ChatType.Think || type === ChatType.PartyThink; - const expression = (think || whisper) ? undefined : parseExpression(message); + if (expression) { + setEntityExpression(client.pony, expression); + } else { + saySystem(client, 'Invalid command'); + } + } + } else { + const message = args; + const think = type === ChatType.Think || type === ChatType.PartyThink; + const expression = (think || whisper) ? undefined : parseExpression(message); - if (expression) { - setEntityExpression(client.pony, expression); - } else if (!whisper && isLaugh(message)) { - execAction(client, Action.Laugh, settings); - } + if (expression) { + setEntityExpression(client.pony, expression); + } else if (!whisper && isLaugh(message)) { + execAction(client, Action.Laugh, settings); + } - if (isPartyChat(type)) { - sayToParty(client, message, think ? MessageType.PartyThinking : MessageType.Party); - } else { - const friendWhisper = whisper && target !== undefined && isFriend(client, target); - const messageNoLinks = filterUrls(message); - const messageCensored = forbidden ? repeat('*', messageNoLinks.length) : filterBadWords(messageNoLinks); - const trimmedMessage = trimRepeatedLetters(messageNoLinks); - const trimmedCensored = trimRepeatedLetters(messageCensored); - const messageType = getMessageType(client, type); - const swearing = messageNoLinks !== messageCensored; + if (isPartyChat(type)) { + sayToParty(client, message, think ? MessageType.PartyThinking : MessageType.Party); + } else { + const friendWhisper = whisper && target !== undefined && isFriend(client, target); + const messageNoLinks = filterUrls(message); + const messageCensored = forbidden ? repeat('*', messageNoLinks.length) : filterBadWords(messageNoLinks); + const trimmedMessage = trimRepeatedLetters(messageNoLinks); + const trimmedCensored = trimRepeatedLetters(messageCensored); + const messageType = getMessageType(client, type); + const swearing = messageNoLinks !== messageCensored; - if (!friendWhisper) { - if (!client.map.instance) { - checkSpam(client, message, settings); - } + if (!friendWhisper) { + if (!client.map.instance) { + checkSpam(client, message, settings); + } - if (settings.filterSwears && swearing) { - reportSwears(client, message, settings); - } + if (settings.filterSwears && swearing) { + reportSwears(client, message, settings); + } - if (forbidden) { - reportForbidden(client, message, settings); - } - } + if (forbidden) { + reportForbidden(client, message, settings); + } + } - if (!friendWhisper && swearing && settings.kickSwearing && random() < 0.75) { - if (settings.kickSwearingToSpawn) { - world.resetToSpawn(client); - } + if (!friendWhisper && swearing && settings.kickSwearing && random() < 0.75) { + if (settings.kickSwearingToSpawn) { + world.resetToSpawn(client); + } - world.kick(client, 'swearing', LeaveReason.Swearing); - } else if (!friendWhisper && forbidden) { - sayTo(client, client.pony, trimmedMessage, messageType); - } else if (whisper) { - sayWhisper(client, trimmedMessage, trimmedCensored, messageType, target, settings); - } else { - sayToEveryone(client, trimmedMessage, trimmedCensored, messageType, settings); - } - } - } - }; + world.kick(client, 'swearing', LeaveReason.Swearing); + } else if (!friendWhisper && forbidden) { + sayTo(client, client.pony, trimmedMessage, messageType); + } else if (whisper) { + sayWhisper(client, trimmedMessage, trimmedCensored, messageType, target, settings); + } else { + sayToEveryone(client, trimmedMessage, trimmedCensored, messageType, settings); + } + } + } + }; export function sayTo(client: IClient, { id }: ServerEntity, message: string, type: MessageType) { - client.saysQueue.push([id, message, type]); + client.saysQueue.push([id, message, type]); } export function saySystem(client: IClient, message: string) { - sayTo(client, client.pony, message, MessageType.System); + sayTo(client, client.pony, message, MessageType.System); } function sayToClient( - client: IClient, entity: ServerEntity, message: string, censoredMessage: string, type: MessageType, - settings: GameServerSettings + client: IClient, entity: ServerEntity, message: string, censoredMessage: string, type: MessageType, + settings: GameServerSettings ): boolean { - if (client.pony !== entity && !isWhisperTo(type)) { - const swear = !!settings.hideSwearing && message !== censoredMessage; + if (client.pony !== entity && !isWhisperTo(type)) { + const swear = !!settings.hideSwearing && message !== censoredMessage; - if (isPublicMessage(type)) { - if (swear) { - return false; - } + if (isPublicMessage(type)) { + if (swear) { + return false; + } - if (!isWorldPointWithPaddingVisible(client.camera, entity, tileWidth * 2)) { - return false; - } - } + if (!isWorldPointWithPaddingVisible(client.camera, entity, tileWidth * 2)) { + return false; + } + } - if (entity.client) { - if (isIgnored(client, entity.client)) { - return false; - } + if (entity.client) { + if (isIgnored(client, entity.client)) { + return false; + } - if (!client.isMod && isHiddenBy(client, entity.client)) { - return false; - } + if (!client.isMod && isHiddenBy(client, entity.client)) { + return false; + } - if (swear && !isFriend(client, entity.client)) { - return false; - } - } + if (swear && !isFriend(client, entity.client)) { + return false; + } + } - if (client.accountSettings.filterSwearWords || settings.filterSwears) { - message = censoredMessage; - } - } + if (client.accountSettings.filterSwearWords || settings.filterSwears) { + message = censoredMessage; + } + } - sayTo(client, entity, message, type); - return true; + sayTo(client, entity, message, type); + return true; } function sayWhisper( - client: IClient, message: string, censoredMessage: string, type: MessageType, - target: IClient | undefined, settings: GameServerSettings + client: IClient, message: string, censoredMessage: string, type: MessageType, + target: IClient | undefined, settings: GameServerSettings ) { - if (target === undefined || target.shadowed || isHiddenBy(client, target)) { - saySystem(client, `Couldn't find this player`); - } else { - const friend = isFriend(client, target); + if (target === undefined || target.shadowed || isHiddenBy(client, target)) { + saySystem(client, `Couldn't find this player`); + } else { + const friend = isFriend(client, target); - if (!friend && client.accountSettings.ignoreNonFriendWhispers) { - saySystem(client, `You can only whisper to friends`); - } else if (!friend && target.accountSettings.ignoreNonFriendWhispers) { - saySystem(client, `Can't whisper to this player`); - } else { - sayTo(client, target.pony, message, toMessageType(type)); + if (!friend && client.accountSettings.ignoreNonFriendWhispers) { + saySystem(client, `You can only whisper to friends`); + } else if (!friend && target.accountSettings.ignoreNonFriendWhispers) { + saySystem(client, `Can't whisper to this player`); + } else { + sayTo(client, target.pony, message, toMessageType(type)); - if (!isMutedOrShadowed(client)) { - sayToClient(target, client.pony, message, censoredMessage, type, settings); - } - } - } + if (!isMutedOrShadowed(client)) { + sayToClient(target, client.pony, message, censoredMessage, type, settings); + } + } + } } function sayToParty(client: IClient, message: string, type: MessageType) { - if (!client.party) { - saySystem(client, `you're not in a party`); - } else if (isMutedOrShadowed(client)) { - sayTo(client, client.pony, message, type); - } else { - for (const c of client.party.clients) { - sayTo(c, client.pony, message, type); - } - } + if (!client.party) { + saySystem(client, `you're not in a party`); + } else if (isMutedOrShadowed(client)) { + sayTo(client, client.pony, message, type); + } else { + for (const c of client.party.clients) { + sayTo(c, client.pony, message, type); + } + } } export function sayToAll( - entity: ServerEntity, message: string, censoredMessage: string, type: MessageType, settings: GameServerSettings + entity: ServerEntity, message: string, censoredMessage: string, type: MessageType, settings: GameServerSettings ) { - if (entity.region) { - for (const client of entity.region.clients) { - sayToClient(client, entity, message, censoredMessage, type, settings); - } - } + if (entity.region) { + for (const client of entity.region.clients) { + sayToClient(client, entity, message, censoredMessage, type, settings); + } + } } export function sayToEveryone( - client: IClient, message: string, censoredMessage: string, type: MessageType, settings: GameServerSettings + client: IClient, message: string, censoredMessage: string, type: MessageType, settings: GameServerSettings ) { - if ( - isMutedOrShadowed(client) || - client.accountSettings.ignorePublicChat - ) { - sayTo(client, client.pony, message, type); - } else { - sayToAll(client.pony, message, censoredMessage, type, settings); - } + if ( + isMutedOrShadowed(client) || + client.accountSettings.ignorePublicChat + ) { + sayTo(client, client.pony, message, type); + } else { + sayToAll(client.pony, message, censoredMessage, type, settings); + } } export function sayToOthers( - client: IClient, message: string, type: MessageType, target: IClient | undefined, settings: GameServerSettings + client: IClient, message: string, type: MessageType, target: IClient | undefined, settings: GameServerSettings ) { - if (isWhisper(type)) { - sayWhisper(client, message, message, type, target, settings); - } else if (isPartyMessage(type)) { - sayToParty(client, message, type); - } else { - sayToEveryone(client, message, message, type, settings); - } + if (isWhisper(type)) { + sayWhisper(client, message, message, type, target, settings); + } else if (isPartyMessage(type)) { + sayToParty(client, message, type); + } else { + sayToEveryone(client, message, message, type, settings); + } } export const sayToClientTest = sayToClient; diff --git a/src/ts/server/cmUtils.ts b/src/ts/server/cmUtils.ts index e6e821c..512af29 100644 --- a/src/ts/server/cmUtils.ts +++ b/src/ts/server/cmUtils.ts @@ -5,420 +5,420 @@ import { repeat } from '../common/utils'; import { CM_SIZE } from '../common/constants'; const patterns = [ - [ // 0 - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ // 1 - 1, 1, 1, 0, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 0, 1, 1, 1, - ], - [ // 2 - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 0, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ // 3 - 1, 1, 1, 0, 1, - 0, 0, 1, 0, 1, - 1, 1, 0, 1, 1, - 1, 0, 1, 0, 0, - 1, 0, 1, 1, 1, - ], - [ // 4 - 0, 0, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 0, 0, - ], - [ // 5 - 0, 0, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 1, 0, 1, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 0, 0, - ], - [ // 6 - 0, 1, 1, 0, 0, - 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 1, 0, 0, - 0, 0, 1, 1, 0, - ], - [ // 7 - 1, 1, 1, 0, 0, - 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 1, 0, 0, - 0, 0, 1, 1, 1, - ], - [ // 8 - 0, 1, 1, 0, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 0, 1, 1, 0, - ], - [ // 9 - 0, 1, 1, 0, 0, - 0, 0, 1, 0, 1, - 1, 1, 0, 1, 1, - 1, 0, 1, 0, 0, - 0, 0, 1, 1, 0, - ], - [ // 10 - 0, 1, 1, 0, 0, - 0, 0, 1, 0, 1, - 1, 1, 0, 1, 1, - 1, 0, 1, 0, 0, - 0, 0, 1, 1, 0, - ], - [ // 11 - 0, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 0, - ], - [ // 12 - 1, 0, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 0, 1, - ], - // short arms - [ // 13 - 1, 0, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ // 14 - 0, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ // 15 - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 0, 1, - ], - [ // 16 - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 0, - ], - // short arms inverted - [ - 1, 1, 1, 0, 0, - 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 0, 1, 1, 1, - ], - [ - 1, 1, 1, 0, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 0, 1, 1, 0, - ], - [ - 1, 1, 1, 0, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 1, 0, 0, - 0, 0, 1, 1, 1, - ], - [ // 20 - 0, 1, 1, 0, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 0, 1, 1, 1, - ], - // long arms - [ - 0, 0, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 0, 1, - ], - [ - 0, 0, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 0, - ], - [ - 1, 0, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 0, 0, - ], - [ - 0, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 0, 0, - ], - // no corners - [ // 25 - 1, 0, 0, 1, 1, - 1, 0, 1, 0, 0, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 1, - 1, 1, 0, 0, 1, - ], - [ - 1, 1, 0, 0, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 0, 0, 1, 1, - ], - // small - [ - 0, 1, 1, 0, 1, - 0, 0, 1, 1, 1, - 0, 1, 1, 1, 0, - 0, 1, 0, 1, 1, - 0, 0, 0, 0, 0, - ], - [ - 1, 1, 0, 1, 0, - 0, 1, 1, 1, 0, - 1, 1, 1, 0, 0, - 1, 0, 1, 1, 0, - 0, 0, 0, 0, 0, - ], - [ - 0, 0, 0, 0, 0, - 0, 1, 1, 0, 1, - 0, 0, 1, 1, 1, - 0, 1, 1, 1, 0, - 0, 1, 0, 1, 1, - ], - [ // 30 - 0, 0, 0, 0, 0, - 1, 1, 0, 1, 0, - 0, 1, 1, 1, 0, - 1, 1, 1, 0, 0, - 1, 0, 1, 1, 0, - ], - [ - 0, 1, 0, 1, 1, - 0, 1, 1, 1, 0, - 0, 0, 1, 1, 1, - 0, 1, 1, 0, 1, - 0, 0, 0, 0, 0, - ], - [ - 1, 0, 1, 1, 0, - 1, 1, 1, 0, 0, - 0, 1, 1, 1, 0, - 1, 1, 0, 1, 0, - 0, 0, 0, 0, 0, - ], - [ - 0, 0, 0, 0, 0, - 0, 1, 0, 1, 1, - 0, 1, 1, 1, 0, - 0, 0, 1, 1, 1, - 0, 1, 1, 0, 1, - ], - [ - 0, 0, 0, 0, 0, - 1, 0, 1, 1, 0, - 1, 1, 1, 0, 0, - 0, 1, 1, 1, 0, - 1, 1, 0, 1, 0, - ], - // weird shapes - [ // 35 - 0, 1, 1, 0, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 1, 1, - 0, 1, 0, 1, 0, - 0, 1, 0, 1, 1, - ], - [ - 1, 1, 0, 1, 0, - 0, 1, 0, 1, 0, - 1, 1, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 0, 1, 1, 0, - ], - [ - 1, 0, 1, 1, 0, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 1, 0, 0, 1, - 1, 1, 0, 0, 1, - ], - [ - 1, 0, 0, 1, 1, - 1, 0, 0, 1, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 0, 1, 1, 0, 1, - ], - [ - 0, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 1, - 1, 1, 0, 0, 1, - ], - [ // 40 - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 1, - 1, 1, 0, 0, 0, - ], - [ - 1, 0, 0, 1, 0, - 1, 0, 1, 0, 0, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 1, - 1, 1, 0, 0, 1, - ], - [ - 1, 0, 0, 1, 1, - 1, 0, 1, 0, 0, - 0, 1, 1, 1, 0, - 0, 0, 1, 0, 1, - 0, 1, 0, 0, 1, - ], - // additional pixels - [ - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 1, 1, - 1, 1, 1, 0, 1, - ], - [ - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 1, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ // 45 - 1, 0, 1, 1, 1, - 1, 1, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ - 1, 0, 1, 1, 1, - 1, 0, 1, 1, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - // very wonky - [ - 1, 1, 1, 0, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 1, 0, - 1, 0, 1, 0, 0, - 0, 0, 1, 1, 1, - ], - [ - 1, 1, 1, 0, 0, - 0, 0, 1, 0, 1, - 0, 1, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 0, 1, 1, 1, - ], - // missing one corner - [ - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 0, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ // 50 - 1, 0, 0, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 0, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 0, 0, 1, - ], - // missing arms - [ - 0, 0, 1, 1, 1, - 0, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ - 1, 0, 1, 0, 0, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 1, 1, 1, 0, 1, - ], - [ // 55 - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 0, - 1, 1, 1, 0, 0, - ], - [ - 1, 0, 1, 1, 1, - 1, 0, 1, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 1, 0, 1, - 0, 0, 1, 0, 1, - ], + [ // 0 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 1 + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + [ // 2 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 0, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 3 + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + [ // 4 + 0, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 0, + ], + [ // 5 + 0, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 0, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 0, + ], + [ // 6 + 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, + ], + [ // 7 + 1, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 1, + ], + [ // 8 + 0, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 0, + ], + [ // 9 + 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, + ], + [ // 10 + 0, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 0, + ], + [ // 11 + 0, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 0, + ], + [ // 12 + 1, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 1, + ], + // short arms + [ // 13 + 1, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 14 + 0, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 15 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 1, + ], + [ // 16 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 0, + ], + // short arms inverted + [ + 1, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + [ + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 0, + ], + [ + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 1, + ], + [ // 20 + 0, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + // long arms + [ + 0, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 1, + ], + [ + 0, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 0, + ], + [ + 1, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 0, + ], + [ + 0, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 0, + ], + // no corners + [ // 25 + 1, 0, 0, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 1, + ], + [ + 1, 1, 0, 0, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 0, 0, 1, 1, + ], + // small + [ + 0, 1, 1, 0, 1, + 0, 0, 1, 1, 1, + 0, 1, 1, 1, 0, + 0, 1, 0, 1, 1, + 0, 0, 0, 0, 0, + ], + [ + 1, 1, 0, 1, 0, + 0, 1, 1, 1, 0, + 1, 1, 1, 0, 0, + 1, 0, 1, 1, 0, + 0, 0, 0, 0, 0, + ], + [ + 0, 0, 0, 0, 0, + 0, 1, 1, 0, 1, + 0, 0, 1, 1, 1, + 0, 1, 1, 1, 0, + 0, 1, 0, 1, 1, + ], + [ // 30 + 0, 0, 0, 0, 0, + 1, 1, 0, 1, 0, + 0, 1, 1, 1, 0, + 1, 1, 1, 0, 0, + 1, 0, 1, 1, 0, + ], + [ + 0, 1, 0, 1, 1, + 0, 1, 1, 1, 0, + 0, 0, 1, 1, 1, + 0, 1, 1, 0, 1, + 0, 0, 0, 0, 0, + ], + [ + 1, 0, 1, 1, 0, + 1, 1, 1, 0, 0, + 0, 1, 1, 1, 0, + 1, 1, 0, 1, 0, + 0, 0, 0, 0, 0, + ], + [ + 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, + 0, 1, 1, 1, 0, + 0, 0, 1, 1, 1, + 0, 1, 1, 0, 1, + ], + [ + 0, 0, 0, 0, 0, + 1, 0, 1, 1, 0, + 1, 1, 1, 0, 0, + 0, 1, 1, 1, 0, + 1, 1, 0, 1, 0, + ], + // weird shapes + [ // 35 + 0, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 1, 1, + 0, 1, 0, 1, 0, + 0, 1, 0, 1, 1, + ], + [ + 1, 1, 0, 1, 0, + 0, 1, 0, 1, 0, + 1, 1, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 0, + ], + [ + 1, 0, 1, 1, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 1, 0, 0, 1, + 1, 1, 0, 0, 1, + ], + [ + 1, 0, 0, 1, 1, + 1, 0, 0, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 1, 1, 0, 1, + ], + [ + 0, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 1, + ], + [ // 40 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 0, + ], + [ + 1, 0, 0, 1, 0, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 1, + ], + [ + 1, 0, 0, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 0, 1, 0, 0, 1, + ], + // additional pixels + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 1, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 1, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 45 + 1, 0, 1, 1, 1, + 1, 1, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + // very wonky + [ + 1, 1, 1, 0, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 1, 0, + 1, 0, 1, 0, 0, + 0, 0, 1, 1, 1, + ], + [ + 1, 1, 1, 0, 0, + 0, 0, 1, 0, 1, + 0, 1, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 0, 1, 1, 1, + ], + // missing one corner + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 0, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 50 + 1, 0, 0, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 0, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 0, 0, 1, + ], + // missing arms + [ + 0, 0, 1, 1, 1, + 0, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ + 1, 0, 1, 0, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 1, 1, 1, 0, 1, + ], + [ // 55 + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 1, 1, 1, 0, 0, + ], + [ + 1, 0, 1, 1, 1, + 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 1, + 0, 0, 1, 0, 1, + ], ]; export function hexToLab(c: string): LAB { - const [L, A, B] = hex.lab(c); - return { L, A, B }; + const [L, A, B] = hex.lab(c); + return { L, A, B }; } // export function colorToGrayscale(c: string) { @@ -428,80 +428,80 @@ export function hexToLab(c: string): LAB { // } export function theSameColor(a: LAB, b: LAB, delta = 27): boolean { // 27 - return getDeltaE00(a, b) < delta; + return getDeltaE00(a, b) < delta; } export function isBadCM(cmString: string[], coatColor: string | undefined): string | undefined { - if (!cmString || !cmString.length) - return undefined; + if (!cmString || !cmString.length) + return undefined; - const pad = CM_SIZE * CM_SIZE - cmString.length; - const coat = hexToLab(coatColor || '000000'); - const padded = [...cmString, ...repeat(pad, '')]; - const cmAlpha = padded.map(c => (!c || (coatColor && theSameColor(hexToLab(c), coat, 1))) ? 0 : 1); - const cmAlpha2 = padded.map(c => c ? 1 : 0); + const pad = CM_SIZE * CM_SIZE - cmString.length; + const coat = hexToLab(coatColor || '000000'); + const padded = [...cmString, ...repeat(pad, '')]; + const cmAlpha = padded.map(c => (!c || (coatColor && theSameColor(hexToLab(c), coat, 1))) ? 0 : 1); + const cmAlpha2 = padded.map(c => c ? 1 : 0); - const hasAlpha = cmString.some(c => !c); - const cm = [...cmString.map(c => c ? hexToLab(c) : coat), ...repeat(pad, coat)]; - const colorsString = compact(uniq([coatColor, ...cmString])); - const colors = colorsString.map(hexToLab); + const hasAlpha = cmString.some(c => !c); + const cm = [...cmString.map(c => c ? hexToLab(c) : coat), ...repeat(pad, coat)]; + const colorsString = compact(uniq([coatColor, ...cmString])); + const colors = colorsString.map(hexToLab); - // const cmGrayscale = padded.map(colorToGrayscale); - // const grays = compact(uniq(cmGrayscale)); + // const cmGrayscale = padded.map(colorToGrayscale); + // const grays = compact(uniq(cmGrayscale)); - let patternIndex = 0; + let patternIndex = 0; - for (const pattern of patterns) { - if (matchesAlpha(pattern, cmAlpha)) { - return `alpha(pattern:${patternIndex})`; - } + for (const pattern of patterns) { + if (matchesAlpha(pattern, cmAlpha)) { + return `alpha(pattern:${patternIndex})`; + } - if (matchesAlpha(pattern, cmAlpha2)) { - return `alpha2(pattern:${patternIndex})`; - } + if (matchesAlpha(pattern, cmAlpha2)) { + return `alpha2(pattern:${patternIndex})`; + } - for (const color of colors) { - if (matchesColor(pattern, hasAlpha, color, cm)) { - return `color(pattern:${patternIndex}, color:${colorsString[colors.indexOf(color)]})`; - } - } + for (const color of colors) { + if (matchesColor(pattern, hasAlpha, color, cm)) { + return `color(pattern:${patternIndex}, color:${colorsString[colors.indexOf(color)]})`; + } + } - // for (const gray of grays) { - // if (matchesGrayscale(pattern, gray, cmGrayscale, patternIndex > 1 ? 50 : 120)) { - // return `grayscale(pattern:${patternIndex}, gray:${gray})`; - // } - // } + // for (const gray of grays) { + // if (matchesGrayscale(pattern, gray, cmGrayscale, patternIndex > 1 ? 50 : 120)) { + // return `grayscale(pattern:${patternIndex}, gray:${gray})`; + // } + // } - patternIndex++; - } + patternIndex++; + } - return undefined; + return undefined; } function matchesAlpha(pattern: number[], cm: number[]): boolean { - const length = Math.max(pattern.length, cm.length); + const length = Math.max(pattern.length, cm.length); - for (let i = 0; i < length; i++) { - if (pattern[i] !== cm[i]) { - return false; - } - } + for (let i = 0; i < length; i++) { + if (pattern[i] !== cm[i]) { + return false; + } + } - return true; + return true; } function matchesColor(pattern: number[], hasAlpha: boolean, color: LAB, cm: LAB[]): boolean { - for (let i = 0; i < pattern.length; i++) { - const delta = hasAlpha ? 27 : 30; - const on = pattern[i] === 1; - const same = theSameColor(cm[i], color, delta); + for (let i = 0; i < pattern.length; i++) { + const delta = hasAlpha ? 27 : 30; + const on = pattern[i] === 1; + const same = theSameColor(cm[i], color, delta); - if (on !== same) { - return false; - } - } + if (on !== same) { + return false; + } + } - return true; + return true; } // function matchesGrayscale(pattern: number[], color: number, cm: number[], delta: number): boolean { diff --git a/src/ts/server/commands.ts b/src/ts/server/commands.ts index e302e5a..adaa0bf 100644 --- a/src/ts/server/commands.ts +++ b/src/ts/server/commands.ts @@ -1,6 +1,6 @@ import { range, compact, escapeRegExp } from 'lodash'; import { - MessageType, ChatType, Expression, Eye, Muzzle, Action, Season, Holiday, Weather, toAnnouncementMessageType, + MessageType, ChatType, Expression, Eye, Muzzle, Action, Season, Holiday, Weather, toAnnouncementMessageType, } from '../common/interfaces'; import { hasRole } from '../common/accountUtils'; import { butterfly, bat, firefly, cloud, getEntityType, getEntityTypeName } from '../common/entities'; @@ -13,8 +13,8 @@ import { parseExpression, expression } from '../common/expressionUtils'; import { filterBadWords } from '../common/swears'; import { randomString } from '../common/stringUtils'; import { - getCounter, holdToy, getCollectedToysCount, holdItem, playerSleep, playerBlush, playerLove, playerCry, - setEntityExpression, execAction, teleportTo + getCounter, holdToy, getCollectedToysCount, holdItem, playerSleep, playerBlush, playerLove, playerCry, + setEntityExpression, execAction, teleportTo } from './playerUtils'; import { ServerLiveSettings, GameServerSettings } from '../common/adminInterfaces'; import { isCommand, processCommand, clamp, flatten, includes, randomPoint } from '../common/utils'; @@ -24,8 +24,8 @@ import { pathTo } from './paths'; import { sayTo, sayToEveryone, sayToOthers, sayToAll, saySystem } from './chat'; import { resetTiles } from './serverRegion'; import { - findEntities, updateMapState, loadMapFromFile, saveMapToFile, saveEntitiesToFile, getSizeOfMap, - saveMapToFileBinaryAlt, saveRegionCollider, saveMap, loadMap + findEntities, updateMapState, loadMapFromFile, saveMapToFile, saveEntitiesToFile, getSizeOfMap, + saveMapToFileBinaryAlt, saveRegionCollider, saveMap, loadMap } from './serverMap'; import { PARTY_LIMIT, tileWidth, tileHeight, MAP_LOAD_SAVE_TIMEOUT } from '../common/constants'; import { PartyService } from './services/party'; @@ -36,560 +36,560 @@ import { Account } from './db'; import { defaultHouseSave, removeToolbox, restoreToolbox } from './maps/houseMap'; export interface CommandContext { - world: World; - notifications: NotificationService; - liveSettings: ServerLiveSettings; - party: PartyService; - random: (min: number, max: number, floating?: boolean) => number; + world: World; + notifications: NotificationService; + liveSettings: ServerLiveSettings; + party: PartyService; + random: (min: number, max: number, floating?: boolean) => number; } export type CommandHandler = ( - context: CommandContext, client: IClient, message: string, type: ChatType, target: IClient | undefined, - settings: GameServerSettings + context: CommandContext, client: IClient, message: string, type: ChatType, target: IClient | undefined, + settings: GameServerSettings ) => any; export interface Command { - names: string[]; - help: string; - role: string; - spam?: boolean; - handler: CommandHandler; + names: string[]; + help: string; + role: string; + spam?: boolean; + handler: CommandHandler; } function hasRoleNull(client: IClient, role: string) { - if (!role || hasRole(client.account, role)) - return true; + if (!role || hasRole(client.account, role)) + return true; - return (role === 'sup1' && (client.supporterLevel >= 1 || client.isMod)) || - (role === 'sup2' && (client.supporterLevel >= 2 || client.isMod)) || - (role === 'sup3' && (client.supporterLevel >= 3 || client.isMod)); + return (role === 'sup1' && (client.supporterLevel >= 1 || client.isMod)) || + (role === 'sup2' && (client.supporterLevel >= 2 || client.isMod)) || + (role === 'sup3' && (client.supporterLevel >= 3 || client.isMod)); } function command(names: string[], help: string, role: string, handler: CommandHandler, spam = false): Command { - return { names, help, role, handler, spam }; + return { names, help, role, handler, spam }; } function emote(names: string[], expr: Expression, timeout?: number, cancellable?: boolean) { - return command(names, '', '', ({ }, { pony }) => setEntityExpression(pony, expr, timeout, cancellable)); + return command(names, '', '', ({ }, { pony }) => setEntityExpression(pony, expr, timeout, cancellable)); } function action(names: string[], action: Action) { - return command(names, '', '', ({ }, client, _, __, ___, settings) => execAction(client, action, settings)); + return command(names, '', '', ({ }, client, _, __, ___, settings) => execAction(client, action, settings)); } function adminModChat(names: string[], help: string, role: string, type: MessageType) { - return command(names, help, role, ({ }, client, message, _, __, settings) => { - sayToEveryone(client, message, filterBadWords(message), type, settings); - }); + return command(names, help, role, ({ }, client, message, _, __, settings) => { + sayToEveryone(client, message, filterBadWords(message), type, settings); + }); } function parseSeason(value: string): Season | undefined { - switch (value.toLowerCase()) { - case 'spring': return Season.Spring; - case 'summer': return Season.Summer; - case 'autumn': return Season.Autumn; - case 'winter': return Season.Winter; - default: return undefined; - } + switch (value.toLowerCase()) { + case 'spring': return Season.Spring; + case 'summer': return Season.Summer; + case 'autumn': return Season.Autumn; + case 'winter': return Season.Winter; + default: return undefined; + } } function parseHoliday(value: string): Holiday | undefined { - switch (value.toLowerCase()) { - case 'none': return Holiday.None; - case 'halloween': return Holiday.Halloween; - case 'christmas': return Holiday.Christmas; - default: return undefined; - } + switch (value.toLowerCase()) { + case 'none': return Holiday.None; + case 'halloween': return Holiday.Halloween; + case 'christmas': return Holiday.Christmas; + default: return undefined; + } } function parseWeather(value: string): Weather | undefined { - switch (value.toLowerCase()) { - case 'none': return Weather.None; - case 'rain': return Weather.Rain; - default: return undefined; - } + switch (value.toLowerCase()) { + case 'none': return Weather.None; + case 'rain': return Weather.Rain; + default: return undefined; + } } function getSpawnTarget(map: ServerMap, message: string) { - if (message === 'spawn') { - return randomPoint(map.spawnArea); - } + if (message === 'spawn') { + return randomPoint(map.spawnArea); + } - const spawn = map.spawns.get(message); + const spawn = map.spawns.get(message); - if (spawn) { - return randomPoint(spawn); - } + if (spawn) { + return randomPoint(spawn); + } - const match = /^(\d+) (\d+)$/.exec(message.trim()); + const match = /^(\d+) (\d+)$/.exec(message.trim()); - if (!match) { - throw new UserError('invalid parameters'); - } + if (!match) { + throw new UserError('invalid parameters'); + } - const [, tx, ty] = match; - const x = clamp(+tx, 0, map.width - 0.5 / tileWidth); - const y = clamp(+ty, 0, map.height - 0.5 / tileHeight); - return { x, y }; + const [, tx, ty] = match; + const x = clamp(+tx, 0, map.width - 0.5 / tileWidth); + const y = clamp(+ty, 0, map.height - 0.5 / tileHeight); + return { x, y }; } function execWithFileName(client: IClient, message: string, action: (fileName: string) => Promise) { - const fileName = message.replace(/[^a-zA-Z0-9_-]/g, ''); + const fileName = message.replace(/[^a-zA-Z0-9_-]/g, ''); - if (!fileName) { - throw new UserError('invalid file name'); - } + if (!fileName) { + throw new UserError('invalid file name'); + } - action(fileName) - .catch(e => (logger.error(e), e.message)) - .then(error => saySystem(client, error || 'saved')); + action(fileName) + .catch(e => (logger.error(e), e.message)) + .then(error => saySystem(client, error || 'saved')); } function shouldNotBeCalled() { - throw new Error('Should not be called'); + throw new Error('Should not be called'); } function isValidMapForEditing(map: ServerMap, client: IClient, checkTimeout: boolean, onlyLeader: boolean) { - if (map.id !== 'house') { - saySystem(client, 'Can only be done inside the house'); - return false; - } + if (map.id !== 'house') { + saySystem(client, 'Can only be done inside the house'); + return false; + } - if (checkTimeout && ((Date.now() - client.lastMapLoadOrSave) < MAP_LOAD_SAVE_TIMEOUT)) { - saySystem(client, `You need to wait ${Math.floor(MAP_LOAD_SAVE_TIMEOUT / 1000)} seconds before loading or saving again`); - return false; - } + if (checkTimeout && ((Date.now() - client.lastMapLoadOrSave) < MAP_LOAD_SAVE_TIMEOUT)) { + saySystem(client, `You need to wait ${Math.floor(MAP_LOAD_SAVE_TIMEOUT / 1000)} seconds before loading or saving again`); + return false; + } - if (onlyLeader && client.party && client.party.leader !== client) { - saySystem(client, 'Only party leader can do this'); - return false; - } + if (onlyLeader && client.party && client.party.leader !== client) { + saySystem(client, 'Only party leader can do this'); + return false; + } - return true; + return true; } let interval: any; export function createCommands(world: World): Command[] { - const commands = compact([ - // chat - command(['help', 'h', '?'], '/help - show help', '', ({ }, client) => { - const help = commands - .filter(c => c.help && hasRoleNull(client, c.role)) - .map(c => c.help) - .join('\n'); + const commands = compact([ + // chat + command(['help', 'h', '?'], '/help - show help', '', ({ }, client) => { + const help = commands + .filter(c => c.help && hasRoleNull(client, c.role)) + .map(c => c.help) + .join('\n'); - saySystem(client, help); - }), - command(['roll', 'rand', 'random'], '/roll [[min-]max] - randomize a number', '', - ({ random }, client, args, type, target, settings) => { - const ROLL_MAX = 1000000; - const [, min, max] = /^(?:(\d+)-)?(\d+)$/.exec(args) || ['', '', '']; - const minValue = clamp((min ? parseInt(min, 10) : 1) | 0, 0, ROLL_MAX); - const maxValue = clamp((max ? parseInt(max, 10) : 100) | 0, minValue, ROLL_MAX); - const result = args === '🍎' ? args : random(minValue, maxValue); - const message = `🎲 rolled ${result} of ${minValue !== 1 ? `${minValue}-` : ''}${maxValue}`; - sayToOthers(client, message, toAnnouncementMessageType(type), target, settings); - }, true), - command(['s', 'say'], '/s - say', '', shouldNotBeCalled), - command(['p', 'party'], '/p - party chat', '', shouldNotBeCalled), - command(['t', 'think'], '/t - thinking balloon', '', shouldNotBeCalled), - command(['w', 'whisper'], '/w - whisper to player', '', shouldNotBeCalled), - command(['r', 'reply'], '/r - reply to whisper', '', shouldNotBeCalled), - command(['e'], '/e - set permanent expression', '', ({ }, { pony }, message) => { - pony.exprPermanent = parseExpression(message); - setEntityExpression(pony, undefined, 0); - }), + saySystem(client, help); + }), + command(['roll', 'rand', 'random'], '/roll [[min-]max] - randomize a number', '', + ({ random }, client, args, type, target, settings) => { + const ROLL_MAX = 1000000; + const [, min, max] = /^(?:(\d+)-)?(\d+)$/.exec(args) || ['', '', '']; + const minValue = clamp((min ? parseInt(min, 10) : 1) | 0, 0, ROLL_MAX); + const maxValue = clamp((max ? parseInt(max, 10) : 100) | 0, minValue, ROLL_MAX); + const result = args === '🍎' ? args : random(minValue, maxValue); + const message = `🎲 rolled ${result} of ${minValue !== 1 ? `${minValue}-` : ''}${maxValue}`; + sayToOthers(client, message, toAnnouncementMessageType(type), target, settings); + }, true), + command(['s', 'say'], '/s - say', '', shouldNotBeCalled), + command(['p', 'party'], '/p - party chat', '', shouldNotBeCalled), + command(['t', 'think'], '/t - thinking balloon', '', shouldNotBeCalled), + command(['w', 'whisper'], '/w - whisper to player', '', shouldNotBeCalled), + command(['r', 'reply'], '/r - reply to whisper', '', shouldNotBeCalled), + command(['e'], '/e - set permanent expression', '', ({ }, { pony }, message) => { + pony.exprPermanent = parseExpression(message); + setEntityExpression(pony, undefined, 0); + }), - // actions - command(['turn'], '/turn - turn head', '', ({ }, client, _, __, ___, settings) => { - execAction(client, Action.TurnHead, settings); - }), - command(['boop', ')'], '/boop or /) - a boop', '', ({ }, client, message, _, __, settings) => { - const expression = parseExpression(message); + // actions + command(['turn'], '/turn - turn head', '', ({ }, client, _, __, ___, settings) => { + execAction(client, Action.TurnHead, settings); + }), + command(['boop', ')'], '/boop or /) - a boop', '', ({ }, client, message, _, __, settings) => { + const expression = parseExpression(message); - if (expression) { - setEntityExpression(client.pony, expression, 800); - } + if (expression) { + setEntityExpression(client.pony, expression, 800); + } - execAction(client, Action.Boop, settings); - }), - command(['drop'], '/drop - drop held item', '', ({ }, client, _, __, ___, settings) => { - execAction(client, Action.Drop, settings); - }), - command(['droptoy'], '/droptoy - drop held toy', '', ({ }, client, _, __, ___, settings) => { - execAction(client, Action.DropToy, settings); - }), - // command(['open'], '/open - open gift', '', ({ }, client) => { - // openGift(client); - // }), + execAction(client, Action.Boop, settings); + }), + command(['drop'], '/drop - drop held item', '', ({ }, client, _, __, ___, settings) => { + execAction(client, Action.Drop, settings); + }), + command(['droptoy'], '/droptoy - drop held toy', '', ({ }, client, _, __, ___, settings) => { + execAction(client, Action.DropToy, settings); + }), + // command(['open'], '/open - open gift', '', ({ }, client) => { + // openGift(client); + // }), - // counters - command(['gifts'], '/gifts - show gift score', '', ({ }, client, _, type, target, settings) => { - sayToOthers(client, `collected ${getCounter(client, 'gifts')} 🎁`, toAnnouncementMessageType(type), target, settings); - }, true), - command(['candies', 'candy'], '/candies - show candy score', '', ({ }, client, _, type, target, settings) => { - sayToOthers(client, `collected ${getCounter(client, 'candies')} 🍬`, toAnnouncementMessageType(type), target, settings); - }, true), - command(['eggs'], '/eggs - show egg score', '', ({ }, client, _, type, target, settings) => { - sayToOthers(client, `collected ${getCounter(client, 'eggs')} 🥚`, toAnnouncementMessageType(type), target, settings); - }, true), - command(['clovers', 'clover'], '/clovers - show clover score', '', ({ }, client, _, type, target, settings) => { - sayToOthers(client, `collected ${getCounter(client, 'clovers')} 🍀`, toAnnouncementMessageType(type), target, settings); - }, true), - command(['toys'], '/toys - show number of collected toys', '', ({ }, client, _, type, target, settings) => { - const { collected, total } = getCollectedToysCount(client); - sayToOthers(client, `collected ${collected}/${total} toys`, toAnnouncementMessageType(type), target, settings); - }), + // counters + command(['gifts'], '/gifts - show gift score', '', ({ }, client, _, type, target, settings) => { + sayToOthers(client, `collected ${getCounter(client, 'gifts')} 🎁`, toAnnouncementMessageType(type), target, settings); + }, true), + command(['candies', 'candy'], '/candies - show candy score', '', ({ }, client, _, type, target, settings) => { + sayToOthers(client, `collected ${getCounter(client, 'candies')} 🍬`, toAnnouncementMessageType(type), target, settings); + }, true), + command(['eggs'], '/eggs - show egg score', '', ({ }, client, _, type, target, settings) => { + sayToOthers(client, `collected ${getCounter(client, 'eggs')} 🥚`, toAnnouncementMessageType(type), target, settings); + }, true), + command(['clovers', 'clover'], '/clovers - show clover score', '', ({ }, client, _, type, target, settings) => { + sayToOthers(client, `collected ${getCounter(client, 'clovers')} 🍀`, toAnnouncementMessageType(type), target, settings); + }, true), + command(['toys'], '/toys - show number of collected toys', '', ({ }, client, _, type, target, settings) => { + const { collected, total } = getCollectedToysCount(client); + sayToOthers(client, `collected ${collected}/${total} toys`, toAnnouncementMessageType(type), target, settings); + }), - // other - command(['unstuck'], '/unstuck - respawn at spawn point', '', ({ world }, client) => { - world.resetToSpawn(client); - world.kick(client, '/unstuck'); - }), - command(['leave'], '/leave - leave the game', '', ({ world }, client) => { - world.kick(client, '/leave'); - }), + // other + command(['unstuck'], '/unstuck - respawn at spawn point', '', ({ world }, client) => { + world.resetToSpawn(client); + world.kick(client, '/unstuck'); + }), + command(['leave'], '/leave - leave the game', '', ({ world }, client) => { + world.kick(client, '/leave'); + }), - // pony states - command(['sit'], '/sit - sit down or stand up', '', shouldNotBeCalled), - command(['lie', 'lay'], '/lie - lie down or sit up', '', shouldNotBeCalled), - command(['fly'], '/fly - fly up or fly down', '', shouldNotBeCalled), - command(['stand'], '/stand - stand up', '', shouldNotBeCalled), + // pony states + command(['sit'], '/sit - sit down or stand up', '', shouldNotBeCalled), + command(['lie', 'lay'], '/lie - lie down or sit up', '', shouldNotBeCalled), + command(['fly'], '/fly - fly up or fly down', '', shouldNotBeCalled), + command(['stand'], '/stand - stand up', '', shouldNotBeCalled), - // emotes - command(['blush'], '', '', ({ }, { pony }, message) => playerBlush(pony, message)), - command(['love', '<3'], '', '', ({ }, { pony }, message) => playerLove(pony, message)), - command(['sleep', 'zzz'], '', '', ({ }, { pony }, message) => playerSleep(pony, message)), - command(['cry'], '', '', ({ }, { pony }, message) => playerCry(pony, message)), + // emotes + command(['blush'], '', '', ({ }, { pony }, message) => playerBlush(pony, message)), + command(['love', '<3'], '', '', ({ }, { pony }, message) => playerLove(pony, message)), + command(['sleep', 'zzz'], '', '', ({ }, { pony }, message) => playerSleep(pony, message)), + command(['cry'], '', '', ({ }, { pony }, message) => playerCry(pony, message)), - // expressions - emote(['smile', 'happy'], expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile)), - emote(['frown'], expression(Eye.Neutral, Eye.Neutral, Muzzle.Frown)), - emote(['angry'], expression(Eye.Angry, Eye.Angry, Muzzle.Frown)), - emote(['sad'], expression(Eye.Sad, Eye.Sad, Muzzle.Frown)), - emote(['thinking'], expression(Eye.Neutral, Eye.Frown2, Muzzle.Concerned)), + // expressions + emote(['smile', 'happy'], expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile)), + emote(['frown'], expression(Eye.Neutral, Eye.Neutral, Muzzle.Frown)), + emote(['angry'], expression(Eye.Angry, Eye.Angry, Muzzle.Frown)), + emote(['sad'], expression(Eye.Sad, Eye.Sad, Muzzle.Frown)), + emote(['thinking'], expression(Eye.Neutral, Eye.Frown2, Muzzle.Concerned)), - // actions - action(['yawn'], Action.Yawn), - action(['laugh', 'lol', 'haha', 'хаха', 'jaja'], Action.Laugh), - action(['sneeze', 'achoo'], Action.Sneeze), - action(['magic'], Action.Magic), + // actions + action(['yawn'], Action.Yawn), + action(['laugh', 'lol', 'haha', 'хаха', 'jaja'], Action.Laugh), + action(['sneeze', 'achoo'], Action.Sneeze), + action(['magic'], Action.Magic), - // house - command(['savehouse'], '/savehouse - saves current house setup', '', async ({ }, client) => { - if (!isValidMapForEditing(client.map, client, true, false)) - return; + // house + command(['savehouse'], '/savehouse - saves current house setup', '', async ({ }, client) => { + if (!isValidMapForEditing(client.map, client, true, false)) + return; - client.lastMapLoadOrSave = Date.now(); + client.lastMapLoadOrSave = Date.now(); - const savedMap = JSON.stringify(saveMap(client.map, - { saveTiles: true, saveEntities: true, saveWalls: true, saveOnlyEditableEntities: true })); + const savedMap = JSON.stringify(saveMap(client.map, + { saveTiles: true, saveEntities: true, saveWalls: true, saveOnlyEditableEntities: true })); - DEVELOPMENT && console.log(savedMap); + DEVELOPMENT && console.log(savedMap); - client.account.savedMap = savedMap; - await Account.updateOne({ _id: client.accountId }, { savedMap }).exec(); + client.account.savedMap = savedMap; + await Account.updateOne({ _id: client.accountId }, { savedMap }).exec(); - saySystem(client, 'Saved'); - client.reporter.systemLog(`Saved house`); - }), - command(['loadhouse'], '/loadhouse - loads saved house setup', '', ({ world }, client) => { - if (!isValidMapForEditing(client.map, client, true, true)) - return; + saySystem(client, 'Saved'); + client.reporter.systemLog(`Saved house`); + }), + command(['loadhouse'], '/loadhouse - loads saved house setup', '', ({ world }, client) => { + if (!isValidMapForEditing(client.map, client, true, true)) + return; - if (!client.account.savedMap) - return saySystem(client, 'No saved map state'); + if (!client.account.savedMap) + return saySystem(client, 'No saved map state'); - client.lastMapLoadOrSave = Date.now(); + client.lastMapLoadOrSave = Date.now(); - loadMap(world, client.map, JSON.parse(client.account.savedMap), - { loadEntities: true, loadWalls: true, loadEntitiesAsEditable: true }); + loadMap(world, client.map, JSON.parse(client.account.savedMap), + { loadEntities: true, loadWalls: true, loadEntitiesAsEditable: true }); - saySystem(client, 'Loaded'); - client.reporter.systemLog(`Loaded house`); - }), - command(['resethouse'], '/resethouse - resets house setup to original state', '', ({ }, client) => { - if (!isValidMapForEditing(client.map, client, true, true)) - return; + saySystem(client, 'Loaded'); + client.reporter.systemLog(`Loaded house`); + }), + command(['resethouse'], '/resethouse - resets house setup to original state', '', ({ }, client) => { + if (!isValidMapForEditing(client.map, client, true, true)) + return; - client.lastMapLoadOrSave = Date.now(); + client.lastMapLoadOrSave = Date.now(); - if (defaultHouseSave) { - loadMap(world, client.map, defaultHouseSave, - { loadEntities: true, loadWalls: true, loadEntitiesAsEditable: true }); - } + if (defaultHouseSave) { + loadMap(world, client.map, defaultHouseSave, + { loadEntities: true, loadWalls: true, loadEntitiesAsEditable: true }); + } - saySystem(client, 'Reset'); - client.reporter.systemLog(`Reset house`); - }), - command(['lockhouse'], '/lockhouse - prevents other people from changing the house', '', ({ }, client) => { - if (!isValidMapForEditing(client.map, client, false, true)) - return; + saySystem(client, 'Reset'); + client.reporter.systemLog(`Reset house`); + }), + command(['lockhouse'], '/lockhouse - prevents other people from changing the house', '', ({ }, client) => { + if (!isValidMapForEditing(client.map, client, false, true)) + return; - client.map.editingLocked = true; + client.map.editingLocked = true; - saySystem(client, 'House locked'); - client.reporter.systemLog(`House locked`); - }), - command(['unlockhouse'], '/unlockhouse - enables editing by other people', '', ({ }, client) => { - if (!isValidMapForEditing(client.map, client, false, true)) - return; + saySystem(client, 'House locked'); + client.reporter.systemLog(`House locked`); + }), + command(['unlockhouse'], '/unlockhouse - enables editing by other people', '', ({ }, client) => { + if (!isValidMapForEditing(client.map, client, false, true)) + return; - client.map.editingLocked = false; + client.map.editingLocked = false; - saySystem(client, 'House unlocked'); - client.reporter.systemLog(`House unlocked`); - }), - command(['removetoolbox'], '/removetoolbox - removes toolbox from the house', '', ({ world }, client) => { - if (!isValidMapForEditing(client.map, client, false, true)) - return; + saySystem(client, 'House unlocked'); + client.reporter.systemLog(`House unlocked`); + }), + command(['removetoolbox'], '/removetoolbox - removes toolbox from the house', '', ({ world }, client) => { + if (!isValidMapForEditing(client.map, client, false, true)) + return; - removeToolbox(world, client.map); + removeToolbox(world, client.map); - saySystem(client, 'Toolbox removed'); - client.reporter.systemLog(`Toolbox removed`); - }), - command(['restoretoolbox'], '/restoretoolbox - restores toolbox to the house', '', ({ }, client) => { - if (!isValidMapForEditing(client.map, client, false, true)) - return; + saySystem(client, 'Toolbox removed'); + client.reporter.systemLog(`Toolbox removed`); + }), + command(['restoretoolbox'], '/restoretoolbox - restores toolbox to the house', '', ({ }, client) => { + if (!isValidMapForEditing(client.map, client, false, true)) + return; - restoreToolbox(world, client.map); + restoreToolbox(world, client.map); - saySystem(client, 'Toolbox restored'); - client.reporter.systemLog(`Toolbox restored`); - }), + saySystem(client, 'Toolbox restored'); + client.reporter.systemLog(`Toolbox restored`); + }), - // supporters - command(['swap'], '/swap - swap character', '', async ({ world }, client, message) => { - if (!message) { - return saySystem(client, `You need to provide name of the character`); - } + // supporters + command(['swap'], '/swap - swap character', '', async ({ world }, client, message) => { + if (!message) { + return saySystem(client, `You need to provide name of the character`); + } - const regex = new RegExp(`^${escapeRegExp(message)}$`, 'i'); - const query = { account: client.account._id, name: { $regex: regex } }; - await swapCharacter(client, world, query); - }), - command(['s1'], '', 'sup1', shouldNotBeCalled), - command(['s2'], '', 'sup2', shouldNotBeCalled), - command(['s3'], '', 'sup3', shouldNotBeCalled), - command(['ss'], '/ss - supporter text', 'sup1', shouldNotBeCalled), + const regex = new RegExp(`^${escapeRegExp(message)}$`, 'i'); + const query = { account: client.account._id, name: { $regex: regex } }; + await swapCharacter(client, world, query); + }), + command(['s1'], '', 'sup1', shouldNotBeCalled), + command(['s2'], '', 'sup2', shouldNotBeCalled), + command(['s3'], '', 'sup3', shouldNotBeCalled), + command(['ss'], '/ss - supporter text', 'sup1', shouldNotBeCalled), - // mod - adminModChat(['m'], '/m - mod text', 'mod', MessageType.Mod), - command(['emotetest'], '/emotetest - print all emotes', 'mod', (_context, client) => { - let text = ''; + // mod + adminModChat(['m'], '/m - mod text', 'mod', MessageType.Mod), + command(['emotetest'], '/emotetest - print all emotes', 'mod', (_context, client) => { + let text = ''; - for (let i = 0; i < emojis.length;) { - if (text) { - text += '\n'; - } + for (let i = 0; i < emojis.length;) { + if (text) { + text += '\n'; + } - for (let j = 0; i < emojis.length && j < 20; j++ , i++) { - text += emojis[i].symbol; - } - } + for (let j = 0; i < emojis.length && j < 20; j++ , i++) { + text += emojis[i].symbol; + } + } - sayTo(client, client.pony, text, MessageType.Chat); - }), - command(['goto'], '/goto []', 'mod', ({ world }, client, message) => { - const [id = '', instance] = message.split(' '); - const map = world.maps.find(map => map.id === id && map.instance === instance); + sayTo(client, client.pony, text, MessageType.Chat); + }), + command(['goto'], '/goto []', 'mod', ({ world }, client, message) => { + const [id = '', instance] = message.split(' '); + const map = world.maps.find(map => map.id === id && map.instance === instance); - if (map) { - const { x, y } = randomPoint(map.spawnArea); - world.switchToMap(client, map, x, y); - } - }), - command(['tp'], '/tp | - teleport to location', 'mod', (_context, client, message) => { - const { x, y } = getSpawnTarget(client.map, message); - teleportTo(client, x, y); - }), + if (map) { + const { x, y } = randomPoint(map.spawnArea); + world.switchToMap(client, map, x, y); + } + }), + command(['tp'], '/tp | - teleport to location', 'mod', (_context, client, message) => { + const { x, y } = getSpawnTarget(client.map, message); + teleportTo(client, x, y); + }), - // admin - adminModChat(['a'], '/a - admin text', 'admin', MessageType.Admin), - command(['announce'], '/announce - global announcement', 'admin', ({ }, client, message, _, __, settings) => { - findEntities(client.map, e => e.type === butterfly.type || e.type === bat.type || e.type === firefly.type) - .forEach(e => sayToAll(e, message, filterBadWords(message), MessageType.Admin, settings)); - }), - command(['time'], '/time - change server time', DEVELOPMENT ? '' : 'admin', ({ world }, _client, message) => { - if (!/^\d+$/.test(message)) { - throw new UserError('invalid parameter'); - } + // admin + adminModChat(['a'], '/a - admin text', 'admin', MessageType.Admin), + command(['announce'], '/announce - global announcement', 'admin', ({ }, client, message, _, __, settings) => { + findEntities(client.map, e => e.type === butterfly.type || e.type === bat.type || e.type === firefly.type) + .forEach(e => sayToAll(e, message, filterBadWords(message), MessageType.Admin, settings)); + }), + command(['time'], '/time - change server time', DEVELOPMENT ? '' : 'admin', ({ world }, _client, message) => { + if (!/^\d+$/.test(message)) { + throw new UserError('invalid parameter'); + } - world.setTime(parseInt(message, 10) % 24); - }), - command(['togglerestore'], '/togglerestore - toggle terrain restoration', 'admin', ({ world: { options } }, client) => { - options.restoreTerrain = !options.restoreTerrain; - saySystem(client, `restoration is ${options.restoreTerrain ? 'on' : 'off'}`); - }), - command(['resettiles'], '/resettiles - reset tiles to original state', 'admin', ({ }, client) => { - for (const region of client.map.regions) { - resetTiles(client.map, region); - } - }), - BETA && command(['season'], '/season []', 'admin', ({ world }, _client, message) => { - const [s = '', h = ''] = message.split(' '); - const season = parseSeason(s); - const holiday = parseHoliday(h); + world.setTime(parseInt(message, 10) % 24); + }), + command(['togglerestore'], '/togglerestore - toggle terrain restoration', 'admin', ({ world: { options } }, client) => { + options.restoreTerrain = !options.restoreTerrain; + saySystem(client, `restoration is ${options.restoreTerrain ? 'on' : 'off'}`); + }), + command(['resettiles'], '/resettiles - reset tiles to original state', 'admin', ({ }, client) => { + for (const region of client.map.regions) { + resetTiles(client.map, region); + } + }), + BETA && command(['season'], '/season []', 'admin', ({ world }, _client, message) => { + const [s = '', h = ''] = message.split(' '); + const season = parseSeason(s); + const holiday = parseHoliday(h); - if (season === undefined) { - throw new UserError('invalid season'); - } else { - world.setSeason(season, holiday === undefined ? world.holiday : holiday); - } - }), - BETA && command(['weather'], '/weather ', 'admin', ({ }, client, message) => { - const weather = parseWeather(message); + if (season === undefined) { + throw new UserError('invalid season'); + } else { + world.setSeason(season, holiday === undefined ? world.holiday : holiday); + } + }), + BETA && command(['weather'], '/weather ', 'admin', ({ }, client, message) => { + const weather = parseWeather(message); - if (weather === undefined) { - throw new UserError('invalid weather'); - } else { - updateMapState(client.map, { weather }); - } - }), + if (weather === undefined) { + throw new UserError('invalid weather'); + } else { + updateMapState(client.map, { weather }); + } + }), - // superadmin - command(['update'], '/update - prepare server for update', 'superadmin', ({ world, liveSettings }) => { - createNotifyUpdate(world, liveSettings)(); - }), - command(['shutdown'], '/shutdown - shutdown server for update', 'superadmin', ({ world, liveSettings }) => { - createShutdownServer(world, liveSettings)(true); - }), + // superadmin + command(['update'], '/update - prepare server for update', 'superadmin', ({ world, liveSettings }) => { + createNotifyUpdate(world, liveSettings)(); + }), + command(['shutdown'], '/shutdown - shutdown server for update', 'superadmin', ({ world, liveSettings }) => { + createShutdownServer(world, liveSettings)(true); + }), - // debug - DEVELOPMENT && command(['map'], '/map - show map info', '', ({ world }, client) => { - const map = client.map; - const { memory, entities } = getSizeOfMap(map); - const message = `[${map.id}:${map.instance || '-'}] ${world.maps.indexOf(map)}/${world.maps.length} ` + - `${(memory / 1024).toFixed(2)} kb ${entities} entities`; - saySystem(client, message); - }), - command(['loadmap'], '/loadmap - load map from file', 'superadmin', ({ world }, client, message) => { - execWithFileName(client, message, fileName => - loadMapFromFile(world, client.map, pathTo('store', `${fileName}.json`), { loadOnlyTiles: true })); - }), - command(['savemap'], '/savemap - save map to file', 'superadmin', (_, client, message) => { - execWithFileName(client, message, async fileName => { - await saveMapToFile(client.map, pathTo('store', `${fileName}.json`), { saveTiles: true }); - // await saveMapToFileBinary(client.map, pathTo('store', `${fileName}.bin`)); - }); - }), - command(['savemapbin'], '/savemapbin - save map to file', 'superadmin', (_, client, message) => { - execWithFileName(client, message, fileName => saveMapToFileBinaryAlt(client.map, pathTo('store', `${fileName}.json`))); - }), - command(['saveentities'], '/saveentities - save entities to file', 'superadmin', (_, client, message) => { - execWithFileName(client, message, fileName => saveEntitiesToFile(client.map, pathTo('store', `${fileName}.txt`))); - }), - command(['savehides'], '/savehides - save hides to file', 'superadmin', async ({ world }, client) => { - const json = world.hidingService.serialize(); - await writeFileAsync(pathTo('store', 'hides.json'), json, 'utf8'); - saySystem(client, 'saved'); - }), - command(['throwerror'], '/throwerror - throw test error', 'superadmin', (_, _client, message) => { - throw new Error(message || 'test'); - }), - BETA && command(['test'], '', 'superadmin', ({ }, client) => { - client.map.regions.forEach(region => { - console.log(region.x, region.y, region.colliders.length); - }); - }), - BETA && command(['spamchat'], '/spamchat - spam chat messages', 'superadmin', - ({ world, random }, client, _, __, ___, settings) => { - if (interval) { - clearInterval(interval); - interval = undefined; - } else { - interval = setInterval(() => { - if (includes(world.clients, client)) { - const message = range(random(1, 10)).map(() => randomString(random(1, 10))).join(' '); - sayToEveryone(client, message, message, MessageType.Chat, settings); - } else { - clearInterval(interval); - } - }, 100); - } - }), - BETA && command(['noclouds'], '/noclouds - remove clouds', 'superadmin', ({ world }, client) => { - findEntities(client.map, e => e.type === cloud.type).forEach(e => world.removeEntity(e, client.map)); - }), - BETA && command(['msg'], '/msg - say random stuff', 'superadmin', ({ }, client, _, __, ___, settings) => { - findEntities(client.map, e => !!e.options && e.name === 'debug 2') - .forEach(e => sayToAll(e, 'Hello there!', 'Hello there!', MessageType.Chat, settings)); - }), - BETA && command(['hold'], '/hold - hold item', 'superadmin', ({ }, client, message) => { - holdItem(client.pony, getEntityType(message)); - }), - BETA && command(['toy'], '/toy - hold toy', 'superadmin', ({ }, client, message) => { - holdToy(client.pony, parseInt(message, 10) | 0); - }), - BETA && command(['dc'], '/dc', 'superadmin', ({ }, client) => { - client.disconnect(true, false); - }), - BETA && command(['disconnect'], '/disconnect', 'superadmin', ({ }, client) => { - client.disconnect(true, true); - }), - BETA && command(['info'], '/info ', 'superadmin', ({ world }, client, message) => { - const id = parseInt(message, 10) | 0; - const entity = world.getEntityById(id); + // debug + DEVELOPMENT && command(['map'], '/map - show map info', '', ({ world }, client) => { + const map = client.map; + const { memory, entities } = getSizeOfMap(map); + const message = `[${map.id}:${map.instance || '-'}] ${world.maps.indexOf(map)}/${world.maps.length} ` + + `${(memory / 1024).toFixed(2)} kb ${entities} entities`; + saySystem(client, message); + }), + command(['loadmap'], '/loadmap - load map from file', 'superadmin', ({ world }, client, message) => { + execWithFileName(client, message, fileName => + loadMapFromFile(world, client.map, pathTo('store', `${fileName}.json`), { loadOnlyTiles: true })); + }), + command(['savemap'], '/savemap - save map to file', 'superadmin', (_, client, message) => { + execWithFileName(client, message, async fileName => { + await saveMapToFile(client.map, pathTo('store', `${fileName}.json`), { saveTiles: true }); + // await saveMapToFileBinary(client.map, pathTo('store', `${fileName}.bin`)); + }); + }), + command(['savemapbin'], '/savemapbin - save map to file', 'superadmin', (_, client, message) => { + execWithFileName(client, message, fileName => saveMapToFileBinaryAlt(client.map, pathTo('store', `${fileName}.json`))); + }), + command(['saveentities'], '/saveentities - save entities to file', 'superadmin', (_, client, message) => { + execWithFileName(client, message, fileName => saveEntitiesToFile(client.map, pathTo('store', `${fileName}.txt`))); + }), + command(['savehides'], '/savehides - save hides to file', 'superadmin', async ({ world }, client) => { + const json = world.hidingService.serialize(); + await writeFileAsync(pathTo('store', 'hides.json'), json, 'utf8'); + saySystem(client, 'saved'); + }), + command(['throwerror'], '/throwerror - throw test error', 'superadmin', (_, _client, message) => { + throw new Error(message || 'test'); + }), + BETA && command(['test'], '', 'superadmin', ({ }, client) => { + client.map.regions.forEach(region => { + console.log(region.x, region.y, region.colliders.length); + }); + }), + BETA && command(['spamchat'], '/spamchat - spam chat messages', 'superadmin', + ({ world, random }, client, _, __, ___, settings) => { + if (interval) { + clearInterval(interval); + interval = undefined; + } else { + interval = setInterval(() => { + if (includes(world.clients, client)) { + const message = range(random(1, 10)).map(() => randomString(random(1, 10))).join(' '); + sayToEveryone(client, message, message, MessageType.Chat, settings); + } else { + clearInterval(interval); + } + }, 100); + } + }), + BETA && command(['noclouds'], '/noclouds - remove clouds', 'superadmin', ({ world }, client) => { + findEntities(client.map, e => e.type === cloud.type).forEach(e => world.removeEntity(e, client.map)); + }), + BETA && command(['msg'], '/msg - say random stuff', 'superadmin', ({ }, client, _, __, ___, settings) => { + findEntities(client.map, e => !!e.options && e.name === 'debug 2') + .forEach(e => sayToAll(e, 'Hello there!', 'Hello there!', MessageType.Chat, settings)); + }), + BETA && command(['hold'], '/hold - hold item', 'superadmin', ({ }, client, message) => { + holdItem(client.pony, getEntityType(message)); + }), + BETA && command(['toy'], '/toy - hold toy', 'superadmin', ({ }, client, message) => { + holdToy(client.pony, parseInt(message, 10) | 0); + }), + BETA && command(['dc'], '/dc', 'superadmin', ({ }, client) => { + client.disconnect(true, false); + }), + BETA && command(['disconnect'], '/disconnect', 'superadmin', ({ }, client) => { + client.disconnect(true, true); + }), + BETA && command(['info'], '/info ', 'superadmin', ({ world }, client, message) => { + const id = parseInt(message, 10) | 0; + const entity = world.getEntityById(id); - if (entity) { - const { id, type, x, y, options } = entity; - const info = { id, type: getEntityTypeName(type), x, y, options }; - saySystem(client, JSON.stringify(info, null, 2)); - } else { - saySystem(client, 'undefined'); - } - }), - BETA && command(['collider'], '/collider', 'superadmin', ({ }, client) => { - const region = getRegionGlobal(client.map, client.pony.x, client.pony.y); + if (entity) { + const { id, type, x, y, options } = entity; + const info = { id, type: getEntityTypeName(type), x, y, options }; + saySystem(client, JSON.stringify(info, null, 2)); + } else { + saySystem(client, 'undefined'); + } + }), + BETA && command(['collider'], '/collider', 'superadmin', ({ }, client) => { + const region = getRegionGlobal(client.map, client.pony.x, client.pony.y); - if (region) { - saveRegionCollider(region); - saySystem(client, 'saved'); - // console.log(region.tileIndices); - } - }), - DEVELOPMENT && command(['testparty'], '', 'superadmin', ({ party }, client) => { - const entities = findEntities(client.map, e => !!e.client && /^debug/.test(e.name || '')); + if (region) { + saveRegionCollider(region); + saySystem(client, 'saved'); + // console.log(region.tileIndices); + } + }), + DEVELOPMENT && command(['testparty'], '', 'superadmin', ({ party }, client) => { + const entities = findEntities(client.map, e => !!e.client && /^debug/.test(e.name || '')); - for (const e of entities.slice(0, PARTY_LIMIT - 1)) { - party.invite(client, e.client!); - } - }), - ]); + for (const e of entities.slice(0, PARTY_LIMIT - 1)) { + party.invite(client, e.client!); + } + }), + ]); - return commands; + return commands; } export function getSpamCommandNames(commands: Command[]): string[] { - return flatten(commands.filter(c => c.spam).map(c => c.names)); + return flatten(commands.filter(c => c.spam).map(c => c.names)); } export type RunCommand = ReturnType; export const createRunCommand = - (context: CommandContext, commands: Command[]) => - (client: IClient, command: string, args: string, type: ChatType, target: IClient | undefined, settings: GameServerSettings) => { - command = command.toLowerCase().trim(); - const func = commands.find(c => c.names.indexOf(command) !== -1); + (context: CommandContext, commands: Command[]) => + (client: IClient, command: string, args: string, type: ChatType, target: IClient | undefined, settings: GameServerSettings) => { + command = command.toLowerCase().trim(); + const func = commands.find(c => c.names.indexOf(command) !== -1); - try { - if (func && hasRoleNull(client, func.role)) { - func.handler(context, client, args, type, target, settings); - } else { - return false; - } - } catch (e) { - if (isUserError(e)) { - saySystem(client, e.message); - } else { - throw e; - } - } + try { + if (func && hasRoleNull(client, func.role)) { + func.handler(context, client, args, type, target, settings); + } else { + return false; + } + } catch (e) { + if (isUserError(e)) { + saySystem(client, e.message); + } else { + throw e; + } + } - return true; - }; + return true; + }; const chatTypes = new Map(); chatTypes.set('p', ChatType.Party); @@ -608,41 +608,41 @@ chatTypes.set('w', ChatType.Whisper); chatTypes.set('whisper', ChatType.Whisper); export function parseCommand(text: string, type: ChatType): { command?: string; args: string; type: ChatType; } { - if (!isCommand(text)) { - return { args: text, type }; - } + if (!isCommand(text)) { + return { args: text, type }; + } - const { command, args } = processCommand(text); + const { command, args } = processCommand(text); - if (command) { - const chatType = chatTypes.get(command.toLowerCase()); + if (command) { + const chatType = chatTypes.get(command.toLowerCase()); - if (chatType !== undefined) { - if (chatType === ChatType.Think) { - type = type === ChatType.Party ? ChatType.PartyThink : ChatType.Think; - } else { - type = chatType; - } + if (chatType !== undefined) { + if (chatType === ChatType.Think) { + type = type === ChatType.Party ? ChatType.PartyThink : ChatType.Think; + } else { + type = chatType; + } - return { args, type }; - } - } + return { args, type }; + } + } - return { command, args, type }; + return { command, args, type }; } export function getChatPrefix(type: ChatType) { - switch (type) { - case ChatType.Party: - case ChatType.PartyThink: - return '/p '; - case ChatType.Supporter: - return '/ss '; - case ChatType.Dismiss: - return '/dismiss '; - case ChatType.Whisper: - return '/w '; - default: - return ''; - } + switch (type) { + case ChatType.Party: + case ChatType.PartyThink: + return '/p '; + case ChatType.Supporter: + return '/ss '; + case ChatType.Dismiss: + return '/dismiss '; + case ChatType.Whisper: + return '/w '; + default: + return ''; + } } diff --git a/src/ts/server/config.ts b/src/ts/server/config.ts index 764c72b..49b3d36 100644 --- a/src/ts/server/config.ts +++ b/src/ts/server/config.ts @@ -2,55 +2,55 @@ import { argv } from 'yargs'; import { ServerConfig } from '../common/adminInterfaces'; export interface AppConfig { - title: string; - twitterLink?: string; - supporterLink?: string; - contactEmail?: string; - port: number; - adminPort?: number; - host: string; - proxy?: number; - noindex?: boolean; - secret: string; - token: string; - local: string; - adminLocal?: string; - sw?: boolean; - db: string; - pg: any; - rollbar?: { - environment: string; - clientToken: string; - serverToken: string; - gulpToken: string; - }; - analytics?: { - trackingID: string; - }; - assetsPath?: string; - oauth: { [key: string]: any }; - servers: ServerConfig[]; - facebookAppId?: string; + title: string; + twitterLink?: string; + supporterLink?: string; + contactEmail?: string; + port: number; + adminPort?: number; + host: string; + proxy?: number; + noindex?: boolean; + secret: string; + token: string; + local: string; + adminLocal?: string; + sw?: boolean; + db: string; + pg: any; + rollbar?: { + environment: string; + clientToken: string; + serverToken: string; + gulpToken: string; + }; + analytics?: { + trackingID: string; + }; + assetsPath?: string; + oauth: { [key: string]: any }; + servers: ServerConfig[]; + facebookAppId?: string; } export interface AppPackage { - name: string; - version: string; - description: string; + name: string; + version: string; + description: string; } export interface AppArgs { - port?: string; - login?: boolean; - admin?: boolean; - standaloneadmin?: boolean; - game?: string; - superadmin?: string; - users?: boolean; - tools?: boolean; - webpack?: boolean; - local?: boolean; - nocleanup?: boolean; + port?: string; + login?: boolean; + admin?: boolean; + standaloneadmin?: boolean; + game?: string; + superadmin?: string; + users?: boolean; + tools?: boolean; + webpack?: boolean; + local?: boolean; + nocleanup?: boolean; } export const args = argv as AppArgs; diff --git a/src/ts/server/controllerUtils.ts b/src/ts/server/controllerUtils.ts index 05d0030..1b7983f 100644 --- a/src/ts/server/controllerUtils.ts +++ b/src/ts/server/controllerUtils.ts @@ -9,131 +9,131 @@ import { setEntityName, updateEntityState } from './entityUtils'; import { hasFlag, repeat } from '../common/utils'; export function give(type: number, message?: string) { - return (e: ServerEntity, client: IClient) => { - if (client.pony.options && client.pony.options.hold === type) { - unholdItem(client.pony); - } else { - if (message) { - sayTo(client, e, message, MessageType.Announcement); - } + return (e: ServerEntity, client: IClient) => { + if (client.pony.options && client.pony.options.hold === type) { + unholdItem(client.pony); + } else { + if (message) { + sayTo(client, e, message, MessageType.Announcement); + } - holdItem(client.pony, type); - } - }; + holdItem(client.pony, type); + } + }; } export function createBoxOfLanterns(x: number, y: number) { - const boxOfLanterns = entities.boxLanterns(x, y) as ServerEntity; - boxOfLanterns.interact = give(entities.lanternOn.type); - setEntityName(boxOfLanterns, 'Box of lanterns'); - return boxOfLanterns; + const boxOfLanterns = entities.boxLanterns(x, y) as ServerEntity; + boxOfLanterns.interact = give(entities.lanternOn.type); + setEntityName(boxOfLanterns, 'Box of lanterns'); + return boxOfLanterns; } export function createSign(x: number, y: number, name: string, interact: Interact, create = entities.sign) { - const entity = create(x, y) as ServerEntity; - setEntityName(entity, name); - entity.interact = interact; - return entity; + const entity = create(x, y) as ServerEntity; + setEntityName(entity, name); + entity.interact = interact; + return entity; } export function createSignWithText(x: number, y: number, name: string, text: string, create = entities.sign) { - return createSign(x, y, name, (entity, client) => sayTo(client, entity, text, MessageType.System), create); + return createSign(x, y, name, (entity, client) => sayTo(client, entity, text, MessageType.System), create); } export function boopLight(this: ServerEntity) { - setTimeout(() => { - if (hasFlag(this.state, EntityState.On)) { - turnOff(this); - this.lightDelay = Date.now() + 3000; - } - }, 300); + setTimeout(() => { + if (hasFlag(this.state, EntityState.On)) { + turnOff(this); + this.lightDelay = Date.now() + 3000; + } + }, 300); } export function createAddLight(world: World, map: ServerMap, createEntity: CreateEntityMethod) { - return (x: number, y: number) => { - const entity = world.addEntity(createEntity(x, y), map); - entity.boop = boopLight; - return entity; - }; + return (x: number, y: number) => { + const entity = world.addEntity(createEntity(x, y), map); + entity.boop = boopLight; + return entity; + }; } export function turnOff(entity: ServerEntity) { - updateEntityState(entity, EntityState.None); + updateEntityState(entity, EntityState.None); } export function turnOn(entity: ServerEntity) { - updateEntityState(entity, setAnimationToEntityState(EntityState.On, 1)); + updateEntityState(entity, setAnimationToEntityState(EntityState.On, 1)); } export function updateLights(entities: ServerEntity[], on: boolean) { - for (const entity of entities) { - if (hasFlag(entity.state, EntityState.On) !== on && Math.random() < 0.2) { - if (entity.lightDelay === undefined || entity.lightDelay < Date.now()) { - if (on) { - turnOn(entity); - } else { - turnOff(entity); - } - } - } - } + for (const entity of entities) { + if (hasFlag(entity.state, EntityState.On) !== on && Math.random() < 0.2) { + if (entity.lightDelay === undefined || entity.lightDelay < Date.now()) { + if (on) { + turnOn(entity); + } else { + turnOff(entity); + } + } + } + } } export function createFenceMaker( - world: World, map: ServerMap, - size: number, poles: CreateEntityMethod[], beamsH: CreateEntityMethod[], beamsV: CreateEntityMethod[] + world: World, map: ServerMap, + size: number, poles: CreateEntityMethod[], beamsH: CreateEntityMethod[], beamsV: CreateEntityMethod[] ) { - const add = (entity: ServerEntity) => world.addEntity(entity, map); + const add = (entity: ServerEntity) => world.addEntity(entity, map); - return (x: number, y: number, length: number, horizontal = true, skipStart = false, skipEnd = false) => { - const dx = horizontal ? size : 0; - const dy = horizontal ? 0 : size; + return (x: number, y: number, length: number, horizontal = true, skipStart = false, skipEnd = false) => { + const dx = horizontal ? size : 0; + const dy = horizontal ? 0 : size; - for (let i = 0; i < length; i++) { - if (i || !skipStart) { - add(sample(poles)!(x + dx * i, y + dy * i)); - } + for (let i = 0; i < length; i++) { + if (i || !skipStart) { + add(sample(poles)!(x + dx * i, y + dy * i)); + } - if (horizontal) { - add(sample(beamsH)!(x + dx * i + (size / 2), y)); - } else { - add(sample(beamsV)!(x, y + dy * i)); - } - } + if (horizontal) { + add(sample(beamsH)!(x + dx * i + (size / 2), y)); + } else { + add(sample(beamsV)!(x, y + dy * i)); + } + } - if (!skipEnd) { - add(sample(poles)!(x + dx * length, y + dy * length)); - } - }; + if (!skipEnd) { + add(sample(poles)!(x + dx * length, y + dy * length)); + } + }; } export function createWoodenFenceMaker(world: World, map: ServerMap) { - return createFenceMaker(world, map, 1, [ - ...repeat(2, entities.woodenFencePole1), - ...repeat(2, entities.woodenFencePole2), - ...repeat(2, entities.woodenFencePole3), - ...repeat(2, entities.woodenFencePole4), - entities.woodenFencePole5, - ], [ - ...repeat(5, entities.woodenFenceBeamH1), - ...repeat(5, entities.woodenFenceBeamH2), - ...repeat(5, entities.woodenFenceBeamH3), - entities.woodenFenceBeamH4, - entities.woodenFenceBeamH5, - entities.woodenFenceBeamH6, - ], [ - entities.woodenFenceBeamV1, - entities.woodenFenceBeamV2, - entities.woodenFenceBeamV3, - ]); + return createFenceMaker(world, map, 1, [ + ...repeat(2, entities.woodenFencePole1), + ...repeat(2, entities.woodenFencePole2), + ...repeat(2, entities.woodenFencePole3), + ...repeat(2, entities.woodenFencePole4), + entities.woodenFencePole5, + ], [ + ...repeat(5, entities.woodenFenceBeamH1), + ...repeat(5, entities.woodenFenceBeamH2), + ...repeat(5, entities.woodenFenceBeamH3), + entities.woodenFenceBeamH4, + entities.woodenFenceBeamH5, + entities.woodenFenceBeamH6, + ], [ + entities.woodenFenceBeamV1, + entities.woodenFenceBeamV2, + entities.woodenFenceBeamV3, + ]); } export function createStoneWallFenceMaker(world: World, map: ServerMap) { - return createFenceMaker(world, map, 2, [ - entities.stoneWallPole1, - ], [ - entities.stoneWallBeamH1, - ], [ - entities.stoneWallBeamV1, - ]); + return createFenceMaker(world, map, 2, [ + entities.stoneWallPole1, + ], [ + entities.stoneWallBeamH1, + ], [ + entities.stoneWallBeamV1, + ]); } diff --git a/src/ts/server/controllers/cloudController.ts b/src/ts/server/controllers/cloudController.ts index 6c69d50..4c5ddfb 100644 --- a/src/ts/server/controllers/cloudController.ts +++ b/src/ts/server/controllers/cloudController.ts @@ -12,44 +12,44 @@ const spriteWidth = sprites.cloud.shadow!.w / tileWidth; const cloudVX = -0.5; export class CloudController implements Controller { - private clouds: Entity[] = []; - private initialized = false; - constructor(private world: World, private map: ServerMap, private cloudCount: number) { - } - initialize() { - if (this.initialized) - return; + private clouds: Entity[] = []; + private initialized = false; + constructor(private world: World, private map: ServerMap, private cloudCount: number) { + } + initialize() { + if (this.initialized) + return; - for (let i = 0; i < this.cloudCount; i++) { - this.addCloud(false, this.world.now / 1000); - } + for (let i = 0; i < this.cloudCount; i++) { + this.addCloud(false, this.world.now / 1000); + } - this.initialized = true; - } - update(_: number, now: number) { - timingStart('CloudController.update()'); - for (let i = this.clouds.length - 1; i >= 0; i--) { - const cloud = this.clouds[i]; + this.initialized = true; + } + update(_: number, now: number) { + timingStart('CloudController.update()'); + for (let i = this.clouds.length - 1; i >= 0; i--) { + const cloud = this.clouds[i]; - if (cloud.x < -spriteWidth) { - this.clouds.splice(i, 1); - this.world.removeEntity(cloud, this.map); - } - } + if (cloud.x < -spriteWidth) { + this.clouds.splice(i, 1); + this.world.removeEntity(cloud, this.map); + } + } - if (this.clouds.length < this.cloudCount) { - this.addCloud(true, now); - } - timingEnd(); - } - private addCloud(end: boolean, timestamp: number) { - const x = end ? this.map.width + spriteWidth : this.map.width * Math.random(); - const y = this.map.height * Math.random(); - const entity = cloud(x, y) as ServerEntity; + if (this.clouds.length < this.cloudCount) { + this.addCloud(true, now); + } + timingEnd(); + } + private addCloud(end: boolean, timestamp: number) { + const x = end ? this.map.width + spriteWidth : this.map.width * Math.random(); + const y = this.map.height * Math.random(); + const entity = cloud(x, y) as ServerEntity; - if (!this.clouds.some(c => entitiesIntersect(c, entity))) { - this.clouds.push(this.world.addEntity(entity, this.map)); - updateEntityVelocity(entity, cloudVX, 0, timestamp); - } - } + if (!this.clouds.some(c => entitiesIntersect(c, entity))) { + this.clouds.push(this.world.addEntity(entity, this.map)); + updateEntityVelocity(entity, cloudVX, 0, timestamp); + } + } } diff --git a/src/ts/server/controllers/collectableController.ts b/src/ts/server/controllers/collectableController.ts index 0b43133..075c7ac 100644 --- a/src/ts/server/controllers/collectableController.ts +++ b/src/ts/server/controllers/collectableController.ts @@ -6,67 +6,67 @@ import { timingEnd, timingStart } from '../timing'; import { canPlaceItem, canBePickedByPlayer, pushRemoveEntityToClient } from '../entityUtils'; export function randomPosition(map: ServerMap) { - const x = Math.random() * map.width; - const y = Math.random() * map.height; - return { x, y }; + const x = Math.random() * map.width; + const y = Math.random() * map.height; + return { x, y }; } export class CollectableController implements Controller { - private items: Entity[] = []; - constructor( - private world: World, - private map: ServerMap, - private ctors: CreateEntityMethod[], - public limit: number, - private pick: (client: IClient, entity: ServerEntity) => void, - private check: (client: IClient) => boolean = () => true, - private tries = 1, - private position = randomPosition, - private active = () => true - ) { - } - initialize() { - } - update() { - timingStart('CollectableController.update()'); + private items: Entity[] = []; + constructor( + private world: World, + private map: ServerMap, + private ctors: CreateEntityMethod[], + public limit: number, + private pick: (client: IClient, entity: ServerEntity) => void, + private check: (client: IClient) => boolean = () => true, + private tries = 1, + private position = randomPosition, + private active = () => true + ) { + } + initialize() { + } + update() { + timingStart('CollectableController.update()'); - if (this.active()) { - for (let i = 0; i < this.tries; i++) { - if (this.items.length < this.limit) { - this.generateItem(); - } - } - } + if (this.active()) { + for (let i = 0; i < this.tries; i++) { + if (this.items.length < this.limit) { + this.generateItem(); + } + } + } - timingEnd(); - } - private generateItem() { - const { world, map } = this; - const { x, y } = this.position(map); - const ctor = sample(this.ctors)!; - const entity = ctor(x, y) as ServerEntity; + timingEnd(); + } + private generateItem() { + const { world, map } = this; + const { x, y } = this.position(map); + const ctor = sample(this.ctors)!; + const entity = ctor(x, y) as ServerEntity; - if (!entity.interactRange) { - entity.interactRange = 1.5; - } + if (!entity.interactRange) { + entity.interactRange = 1.5; + } - if ( - x > 0 && y > 0 && x < map.width && y < map.height && canPlaceItem(map, entity) && !canBePickedByPlayer(map, entity) - ) { - entity.interact = this.interact; - this.items.push(world.addEntity(entity, map)); - } - } - private interact = (entity: Entity, client: IClient) => { - if (this.check(client)) { - if (client.shadowed) { - pushRemoveEntityToClient(client, entity); - } else { - remove(this.items, e => e === entity); - this.world.removeEntity(entity, this.map); - this.generateItem(); - this.pick(client, entity); - } - } - } + if ( + x > 0 && y > 0 && x < map.width && y < map.height && canPlaceItem(map, entity) && !canBePickedByPlayer(map, entity) + ) { + entity.interact = this.interact; + this.items.push(world.addEntity(entity, map)); + } + } + private interact = (entity: Entity, client: IClient) => { + if (this.check(client)) { + if (client.shadowed) { + pushRemoveEntityToClient(client, entity); + } else { + remove(this.items, e => e === entity); + this.world.removeEntity(entity, this.map); + this.generateItem(); + this.pick(client, entity); + } + } + } } diff --git a/src/ts/server/controllers/fakeClientController.ts b/src/ts/server/controllers/fakeClientController.ts index 70e094c..a1e99d1 100644 --- a/src/ts/server/controllers/fakeClientController.ts +++ b/src/ts/server/controllers/fakeClientController.ts @@ -1,7 +1,7 @@ import { sample } from 'lodash'; import { - createBinaryWriter, getWriterBuffer, resetWriter, resizeWriter, writeArrayHeader, writeUint8Array, - writeUint8 + createBinaryWriter, getWriterBuffer, resetWriter, resizeWriter, writeArrayHeader, writeUint8Array, + writeUint8 } from 'ag-sockets'; import { Controller, IClient } from '../serverInterfaces'; import { World } from '../world'; @@ -13,138 +13,138 @@ import { removeItem, times } from '../../common/utils'; import { timingStart, timingEnd } from '../timing'; interface Options { - count: number; + count: number; } const mockCharacterStates = new CounterService(0); export class FakeClientsController implements Controller { - private clients: IClient[] = []; - private tokens: any[] = []; - private initialized = false; - constructor(private world: World, private server: ServerConfig, private options: Options) { - } - initialize() { - if (this.initialized) - return; + private clients: IClient[] = []; + private tokens: any[] = []; + private initialized = false; + constructor(private world: World, private server: ServerConfig, private options: Options) { + } + initialize() { + if (this.initialized) + return; - times(1000, async i => { - try { - const name = `perf-${i}`; - const account = await Account.findOne({ name }).exec(); + times(1000, async i => { + try { + const name = `perf-${i}`; + const account = await Account.findOne({ name }).exec(); - if (!account) - throw new Error(`Missing debug account (${name})`); + if (!account) + throw new Error(`Missing debug account (${name})`); - const character = await Character.findOne({ account: account._id }).exec(); + const character = await Character.findOne({ account: account._id }).exec(); - if (!character) - throw new Error(`Missing debug character (${name})`); + if (!character) + throw new Error(`Missing debug character (${name})`); - this.tokens.push({ id: name, account, character }); - } catch (e) { - console.error(e); - } - }); + this.tokens.push({ id: name, account, character }); + } catch (e) { + console.error(e); + } + }); - this.initialized = true; - } - update() { - } - sparseUpdate() { - timingStart('FakeClientController.sparseUpdate()'); + this.initialized = true; + } + update() { + } + sparseUpdate() { + timingStart('FakeClientController.sparseUpdate()'); - if (this.tokens.length) { - for (let i = this.clients.length - 1; i >= 0; i--) { - if (Math.random() < (10 / this.options.count)) { - this.leave(this.clients[i]); - } - } + if (this.tokens.length) { + for (let i = this.clients.length - 1; i >= 0; i--) { + if (Math.random() < (10 / this.options.count)) { + this.leave(this.clients[i]); + } + } - if (this.clients.length < this.options.count) { - for (let i = 0; i < 10; i++) { - this.join(); - } - } - } + if (this.clients.length < this.options.count) { + for (let i = 0; i < 10; i++) { + this.join(); + } + } + } - timingEnd(); - } - async join() { - try { - const token = sample(this.tokens)!; + timingEnd(); + } + async join() { + try { + const token = sample(this.tokens)!; - if (!this.clients.some(c => c.tokenId === token.id)) { - const client = await joinFakeClient(token, this.server, this.world); - this.clients.push(client); - } - } catch (e) { - console.error(e); - } - } - async leave(client: IClient) { - this.world.leaveClient(client); - removeItem(this.clients, client); - } + if (!this.clients.some(c => c.tokenId === token.id)) { + const client = await joinFakeClient(token, this.server, this.world); + this.clients.push(client); + } + } catch (e) { + console.error(e); + } + } + async leave(client: IClient) { + this.world.leaveClient(client); + removeItem(this.clients, client); + } } const packetWriter = createBinaryWriter(); export let lastPacket: Uint8Array | undefined; async function joinFakeClient(token: any, server: ServerConfig, world: World): Promise { - const client: Partial = { - tokenId: token.id, - tokenData: token, - disconnect() { - world.leaveClient(client as IClient); - }, - queue() { }, - left() { }, - worldState() { }, - mapState() { }, - myEntity() { }, - mapTest() { }, - updateFriends() { }, - actionParam() { }, - update(_, subscribes, adds, datas) { - do { - try { - resetWriter(packetWriter); - writeUint8(packetWriter, 123); + const client: Partial = { + tokenId: token.id, + tokenData: token, + disconnect() { + world.leaveClient(client as IClient); + }, + queue() { }, + left() { }, + worldState() { }, + mapState() { }, + myEntity() { }, + mapTest() { }, + updateFriends() { }, + actionParam() { }, + update(_, subscribes, adds, datas) { + do { + try { + resetWriter(packetWriter); + writeUint8(packetWriter, 123); - if (writeArrayHeader(packetWriter, subscribes)) { - for (let i = 0; i < subscribes.length; i++) { - writeUint8Array(packetWriter, subscribes[i]); - } - } + if (writeArrayHeader(packetWriter, subscribes)) { + for (let i = 0; i < subscribes.length; i++) { + writeUint8Array(packetWriter, subscribes[i]); + } + } - writeUint8Array(packetWriter, adds); + writeUint8Array(packetWriter, adds); - if (writeArrayHeader(packetWriter, datas)) { - for (let i = 0; i < datas.length; i++) { - writeUint8Array(packetWriter, datas[i]); - } - } + if (writeArrayHeader(packetWriter, datas)) { + for (let i = 0; i < datas.length; i++) { + writeUint8Array(packetWriter, datas[i]); + } + } - break; - } catch (e) { - if (e instanceof RangeError || /DataView/.test(e.message)) { - resizeWriter(packetWriter); - } else { - throw e; - } - } - } while (true); + break; + } catch (e) { + if (e instanceof RangeError || /DataView/.test(e.message)) { + resizeWriter(packetWriter); + } else { + throw e; + } + } + } while (true); - lastPacket = getWriterBuffer(packetWriter); - }, - addNotification() { }, - removeNotification() { }, - }; + lastPacket = getWriterBuffer(packetWriter); + }, + addNotification() { }, + removeNotification() { }, + }; - createClientAndPony(client as IClient, [], [], server, world, mockCharacterStates); + createClientAndPony(client as IClient, [], [], server, world, mockCharacterStates); - world.joinClientToQueue(client as IClient); + world.joinClientToQueue(client as IClient); - return client as IClient; + return client as IClient; } diff --git a/src/ts/server/controllers/flyingCritterController.ts b/src/ts/server/controllers/flyingCritterController.ts index c705fe7..b1db3fb 100644 --- a/src/ts/server/controllers/flyingCritterController.ts +++ b/src/ts/server/controllers/flyingCritterController.ts @@ -9,81 +9,81 @@ import { moveRandomly, findClosest, moveTowards } from '../entityUtils'; import { randomPosition } from './collectableController'; export class FlyingCritterController implements Controller { - private entities: Entity[] = []; - constructor( - private world: World, private map: ServerMap, private critter: CreateEntityMethod, private speed: number, - private limit: number, private isActive: () => boolean, private spawnOnStart = false - ) { - } - initialize() { - if (this.spawnOnStart) { - for (let i = 0; i < this.limit; i++) { - const { x, y } = randomPosition(this.map); - this.entities.push(this.world.addEntity(this.critter(x, y), this.map)); - } - } - } - update(_: number, now: number) { - timingStart('FlyingCritterController.update()'); - updateTreehidingEntities( - this.entities, this.world, this.map, this.limit, this.speed, now, this.critter, this.isActive); - timingEnd(); - } + private entities: Entity[] = []; + constructor( + private world: World, private map: ServerMap, private critter: CreateEntityMethod, private speed: number, + private limit: number, private isActive: () => boolean, private spawnOnStart = false + ) { + } + initialize() { + if (this.spawnOnStart) { + for (let i = 0; i < this.limit; i++) { + const { x, y } = randomPosition(this.map); + this.entities.push(this.world.addEntity(this.critter(x, y), this.map)); + } + } + } + update(_: number, now: number) { + timingStart('FlyingCritterController.update()'); + updateTreehidingEntities( + this.entities, this.world, this.map, this.limit, this.speed, now, this.critter, this.isActive); + timingEnd(); + } } function isTreeCrown(entity: ServerEntity) { - return hasFlag(entity.serverFlags || 0, ServerFlags.TreeCrown); + return hasFlag(entity.serverFlags || 0, ServerFlags.TreeCrown); } export function findClosestTree(map: ServerMap, x: number, y: number) { - return findClosestEntity(map, x, y, isTreeCrown); + return findClosestEntity(map, x, y, isTreeCrown); } export function findTrees(map: ServerMap) { - return findEntities(map, isTreeCrown); + return findEntities(map, isTreeCrown); } interface TargetTree extends Entity { - targetTree?: Entity; + targetTree?: Entity; } export function updateTreehidingEntities( - entities: TargetTree[], world: World, map: ServerMap, limit: number, speed: number, timestamp: number, - create: (x: number, y: number) => Entity, isActive: () => boolean + entities: TargetTree[], world: World, map: ServerMap, limit: number, speed: number, timestamp: number, + create: (x: number, y: number) => Entity, isActive: () => boolean ) { - const offsetY = -2; + const offsetY = -2; - if (isActive()) { - // release new critter - if (entities.length < limit && Math.random() < 0.1) { - const trees = findTrees(map); - const tree = sample(trees); + if (isActive()) { + // release new critter + if (entities.length < limit && Math.random() < 0.1) { + const trees = findTrees(map); + const tree = sample(trees); - if (tree) { - const entity = create(tree.x, tree.y + offsetY); - entities.push(world.addEntity(entity, map)); - moveRandomly(map, entity, speed, 1, timestamp); - } - } + if (tree) { + const entity = create(tree.x, tree.y + offsetY); + entities.push(world.addEntity(entity, map)); + moveRandomly(map, entity, speed, 1, timestamp); + } + } - for (const entity of entities) { - moveRandomly(map, entity, speed, 0.02, timestamp); - } - } else if (entities.length) { - // head to tree and disappear - const trees = findTrees(map); + for (const entity of entities) { + moveRandomly(map, entity, speed, 0.02, timestamp); + } + } else if (entities.length) { + // head to tree and disappear + const trees = findTrees(map); - for (let i = entities.length - 1; i >= 0; i--) { - const e = entities[i]; + for (let i = entities.length - 1; i >= 0; i--) { + const e = entities[i]; - e.targetTree = e.targetTree || findClosest(e.x, e.y, trees); + e.targetTree = e.targetTree || findClosest(e.x, e.y, trees); - if (distanceXY(e.x, e.y, e.targetTree.x, e.targetTree.y + offsetY) < 0.1) { - entities.splice(i, 1); - world.removeEntity(e, map); - } else { - moveTowards(e, e.targetTree.x, e.targetTree.y + offsetY, speed, timestamp); - } - } - } + if (distanceXY(e.x, e.y, e.targetTree.x, e.targetTree.y + offsetY) < 0.1) { + entities.splice(i, 1); + world.removeEntity(e, map); + } else { + moveTowards(e, e.targetTree.x, e.targetTree.y + offsetY, speed, timestamp); + } + } + } } diff --git a/src/ts/server/controllers/perfController.ts b/src/ts/server/controllers/perfController.ts index da3ba66..1751410 100644 --- a/src/ts/server/controllers/perfController.ts +++ b/src/ts/server/controllers/perfController.ts @@ -13,116 +13,116 @@ import { timingEnd, timingStart } from '../timing'; import { sayToAll } from '../chat'; interface Options { - count: number; - moving: number; - saying?: boolean; - unique?: boolean; - spread?: boolean; - x?: number; - y?: number; + count: number; + moving: number; + saying?: boolean; + unique?: boolean; + spread?: boolean; + x?: number; + y?: number; } export class PerfController implements Controller { - private entities: Entity[] = []; - private limitLeft = 11; - private limitWidth = 30; - private limitTop = 9; - private limitHeight = 25; - private initialized = false; - constructor(private world: World, private options: Options) { - if (options.spread) { - this.limitWidth = 60; - this.limitHeight = 60; - } + private entities: Entity[] = []; + private limitLeft = 11; + private limitWidth = 30; + private limitTop = 9; + private limitHeight = 25; + private initialized = false; + constructor(private world: World, private options: Options) { + if (options.spread) { + this.limitWidth = 60; + this.limitHeight = 60; + } - if (options.x !== undefined) { - this.limitLeft = options.x; - } + if (options.x !== undefined) { + this.limitLeft = options.x; + } - if (options.y !== undefined) { - this.limitTop = options.y; - } - } - initialize() { - if (this.initialized) - return; + if (options.y !== undefined) { + this.limitTop = options.y; + } + } + initialize() { + if (this.initialized) + return; - const world = this.world; - const map = world.getMainMap(); + const world = this.world; + const map = world.getMainMap(); - const names = [ - 'performance', - 'performance 2', - ]; + const names = [ + 'performance', + 'performance 2', + ]; - const query = this.options.unique ? - Promise.resolve(Character.find({ account: '57ae2336a67f4dc52e123ed1' }).limit(this.options.count).exec()) : - Promise.all(names.map(name => Character.findOne({ name }).exec())).then(compact); + const query = this.options.unique ? + Promise.resolve(Character.find({ account: '57ae2336a67f4dc52e123ed1' }).limit(this.options.count).exec()) : + Promise.all(names.map(name => Character.findOne({ name }).exec())).then(compact); - query - .then(characters => { - if (characters.length) { - this.entities = range(this.options.count).map(i => { - const character = characters[i % characters.length]!; - const name = character._id.toString(); - const x = this.limitLeft + this.limitWidth * Math.random(); - const y = this.limitTop + this.limitHeight * Math.random(); - const p = pony(x, y) as ServerEntity; - setEntityName(p, name); - p.flags |= EntityFlags.CanCollide; - p.encryptedInfoSafe = encryptInfo(character.info || ''); - p.client = { - pony: p, - accountId: 'foobar', - characterId: character._id.toString(), - ignores: new Set(), - hides: new Set(), - permaHides: new Set(), - account: {} as any, - regions: [], - camera: createCamera(), - updateRegion() { }, - addEntity() { }, - mapTest() { }, - } as Partial as any; - p.client!.camera.x = -10000; - p.vx = this.options.moving ? randomVelocity() : 0; - p.vy = this.options.moving ? randomVelocity() : 0; - p.state = shouldBeFacingRight(p) ? EntityState.FacingRight : EntityState.None; - return world.addEntity(p, map); - }); - } - }); + query + .then(characters => { + if (characters.length) { + this.entities = range(this.options.count).map(i => { + const character = characters[i % characters.length]!; + const name = character._id.toString(); + const x = this.limitLeft + this.limitWidth * Math.random(); + const y = this.limitTop + this.limitHeight * Math.random(); + const p = pony(x, y) as ServerEntity; + setEntityName(p, name); + p.flags |= EntityFlags.CanCollide; + p.encryptedInfoSafe = encryptInfo(character.info || ''); + p.client = { + pony: p, + accountId: 'foobar', + characterId: character._id.toString(), + ignores: new Set(), + hides: new Set(), + permaHides: new Set(), + account: {} as any, + regions: [], + camera: createCamera(), + updateRegion() { }, + addEntity() { }, + mapTest() { }, + } as Partial as any; + p.client!.camera.x = -10000; + p.vx = this.options.moving ? randomVelocity() : 0; + p.vy = this.options.moving ? randomVelocity() : 0; + p.state = shouldBeFacingRight(p) ? EntityState.FacingRight : EntityState.None; + return world.addEntity(p, map); + }); + } + }); - this.initialized = true; - } - update(_: number, now: number) { - timingStart('PerfController.update()'); + this.initialized = true; + } + update(_: number, now: number) { + timingStart('PerfController.update()'); - const limitBottom = this.limitTop + this.limitHeight; - const limitRight = this.limitTop + this.limitHeight; + const limitBottom = this.limitTop + this.limitHeight; + const limitRight = this.limitTop + this.limitHeight; - if (this.options.moving) { - for (const entity of this.entities) { - if ((entity.vy > 0 && entity.y > limitBottom) || (entity.vy < 0 && entity.y < this.limitTop)) { - updateEntityVelocity(entity, entity.vx, -entity.vy, now); - } else if ((entity.vx > 0 && entity.x > limitRight) || (entity.vx < 0 && entity.x < this.limitLeft)) { - updateEntityVelocity(entity, -entity.vx, entity.vy, now); - } else if (Math.random() < 0.1) { - updateEntityVelocity(entity, randomVelocity(), randomVelocity(), now); - } + if (this.options.moving) { + for (const entity of this.entities) { + if ((entity.vy > 0 && entity.y > limitBottom) || (entity.vy < 0 && entity.y < this.limitTop)) { + updateEntityVelocity(entity, entity.vx, -entity.vy, now); + } else if ((entity.vx > 0 && entity.x > limitRight) || (entity.vx < 0 && entity.x < this.limitLeft)) { + updateEntityVelocity(entity, -entity.vx, entity.vy, now); + } else if (Math.random() < 0.1) { + updateEntityVelocity(entity, randomVelocity(), randomVelocity(), now); + } - if (this.options.saying && Math.random() < 0.01) { - sayToAll(entity, 'Hello World', 'Hello World', MessageType.Chat, {}); - } - } - } + if (this.options.saying && Math.random() < 0.01) { + sayToAll(entity, 'Hello World', 'Hello World', MessageType.Chat, {}); + } + } + } - timingEnd(); - } + timingEnd(); + } } function randomVelocity() { - const rand = Math.random(); - return rand < 0.333 ? 0 : (rand < 0.666 ? -PONY_SPEED_TROT : +PONY_SPEED_TROT); + const rand = Math.random(); + return rand < 0.333 ? 0 : (rand < 0.666 ? -PONY_SPEED_TROT : +PONY_SPEED_TROT); } diff --git a/src/ts/server/controllers/plantController.ts b/src/ts/server/controllers/plantController.ts index 49bbd1b..79009e6 100644 --- a/src/ts/server/controllers/plantController.ts +++ b/src/ts/server/controllers/plantController.ts @@ -7,83 +7,83 @@ import { removeItem, randomPoint } from '../../common/utils'; import { getTile } from '../../common/worldMap'; interface Plant extends ServerEntity { - plantStage: number; - plantStageNext: number; + plantStage: number; + plantStageNext: number; } export interface PlantConfig { - area: Rect; - count: number; - stages: CreateEntityMethod[][]; - onPick?: Interact; - growOnlyOn?: TileType; - isActive?: () => boolean; + area: Rect; + count: number; + stages: CreateEntityMethod[][]; + onPick?: Interact; + growOnlyOn?: TileType; + isActive?: () => boolean; } export class PlantController implements Controller { - private plants: Plant[] = []; - private interact: Interact = (entity, client) => { - this.world.removeEntity(entity, this.map); - removeItem(this.plants, entity); - this.config.onPick && this.config.onPick(entity, client); - } - private nextSpawn = 0; - constructor(private world: World, private map: ServerMap, private config: PlantConfig) { - } - initialize() { - } - update() { - } - sparseUpdate() { - timingStart('PlantController.sparseUpdate()'); + private plants: Plant[] = []; + private interact: Interact = (entity, client) => { + this.world.removeEntity(entity, this.map); + removeItem(this.plants, entity); + this.config.onPick && this.config.onPick(entity, client); + } + private nextSpawn = 0; + constructor(private world: World, private map: ServerMap, private config: PlantConfig) { + } + initialize() { + } + update() { + } + sparseUpdate() { + timingStart('PlantController.sparseUpdate()'); - const now = Date.now(); - const maxStage = this.config.stages.length - 1; + const now = Date.now(); + const maxStage = this.config.stages.length - 1; - if ( - this.nextSpawn < now && - (this.config.isActive === undefined || this.config.isActive()) && - this.plants.length < this.config.count - ) { - const { x, y } = randomPoint(this.config.area); + if ( + this.nextSpawn < now && + (this.config.isActive === undefined || this.config.isActive()) && + this.plants.length < this.config.count + ) { + const { x, y } = randomPoint(this.config.area); - if (this.config.growOnlyOn === undefined || getTile(this.map, x, y) === this.config.growOnlyOn) { - this.addPlant(x, y, 0); - this.nextSpawn = now + random(10000, 20000); - } - } + if (this.config.growOnlyOn === undefined || getTile(this.map, x, y) === this.config.growOnlyOn) { + this.addPlant(x, y, 0); + this.nextSpawn = now + random(10000, 20000); + } + } - const plantsToRemove: Plant[] = []; + const plantsToRemove: Plant[] = []; - for (const plant of this.plants) { - if (plant.plantStage < maxStage && plant.plantStageNext < now) { - plantsToRemove.push(plant); - this.addPlant(plant.x, plant.y, plant.plantStage + 1); - } - } + for (const plant of this.plants) { + if (plant.plantStage < maxStage && plant.plantStageNext < now) { + plantsToRemove.push(plant); + this.addPlant(plant.x, plant.y, plant.plantStage + 1); + } + } - for (const plant of plantsToRemove) { - this.removePlant(plant); - } + for (const plant of plantsToRemove) { + this.removePlant(plant); + } - timingEnd(); - } - private removePlant(plant: Plant) { - removeItem(this.plants, plant); - this.world.removeEntity(plant, this.map); - } - private addPlant(x: number, y: number, stage: number) { - const create = sample(this.config.stages[stage])!; - const plant = create(x, y) as Plant; - plant.plantStage = stage; - plant.plantStageNext = Date.now() + random(15000, 40000); - plant.serverFlags = ServerFlags.DoNotSave; + timingEnd(); + } + private removePlant(plant: Plant) { + removeItem(this.plants, plant); + this.world.removeEntity(plant, this.map); + } + private addPlant(x: number, y: number, stage: number) { + const create = sample(this.config.stages[stage])!; + const plant = create(x, y) as Plant; + plant.plantStage = stage; + plant.plantStageNext = Date.now() + random(15000, 40000); + plant.serverFlags = ServerFlags.DoNotSave; - if (stage === (this.config.stages.length - 1)) { - plant.interact = this.interact; - } + if (stage === (this.config.stages.length - 1)) { + plant.interact = this.interact; + } - this.plants.push(plant); - this.world.addEntity(plant, this.map); - } + this.plants.push(plant); + this.world.addEntity(plant, this.map); + } } diff --git a/src/ts/server/controllers/testController.ts b/src/ts/server/controllers/testController.ts index 77aa1b8..88fb0de 100644 --- a/src/ts/server/controllers/testController.ts +++ b/src/ts/server/controllers/testController.ts @@ -11,77 +11,77 @@ import { timingStart, timingEnd } from '../timing'; import { createBinaryWriter } from 'ag-sockets'; export class TestController implements Controller { - private clients: IClient[] = []; - private initialized = false; - constructor(private world: World, private map: ServerMap) { - } - initialize() { - if (this.initialized) - return; + private clients: IClient[] = []; + private initialized = false; + constructor(private world: World, private map: ServerMap) { + } + initialize() { + if (this.initialized) + return; - const world = this.world; - const map = this.map; + const world = this.world; + const map = this.map; - if (DEVELOPMENT) { - Promise.all(times(10, i => `debug ${i + 1}`).map(name => Character.findOne({ name }).exec())) - .then(compact) - .then(items => items.forEach((item, i) => { - const name = item.name; - const tag = i === 0 ? 'mod' : (i === 2 ? 'sup2' : ''); - const extraOptions = i === 0 ? { - site: { - provider: 'github', - name: 'Test name', - url: 'https://github.com/Microsoft/TypeScript', - } - } : undefined; - const p = entities.pony(57 + 1 * i, 47 + 1 * i) as ServerEntity; - p.options = { tag }; - setEntityName(p, name); - p.encryptedInfoSafe = encryptInfo(item.info || ''); - p.client = { - map, - accountSettings: {}, - account: { id: 'foobar', name: 'Debug account' } as any, - country: 'XY', - regions: [], - saysQueue: { push() { }, length: 0 } as any, - notifications: [], - camera: createCamera(), - accountId: 'foobar', - characterId: '', - ignores: new Set(), - hides: new Set(), - permaHides: new Set(), - updateQueue: createBinaryWriter(1), - addEntity() { }, - addNotification() { }, - removeNotification() { }, - updateParty() { }, - mapUpdate() { }, - } as Partial as any; - p.client!.pony = p; - this.clients.push(p.client!); - p.extraOptions = extraOptions; - world.addEntity(p, map); - })); - } + if (DEVELOPMENT) { + Promise.all(times(10, i => `debug ${i + 1}`).map(name => Character.findOne({ name }).exec())) + .then(compact) + .then(items => items.forEach((item, i) => { + const name = item.name; + const tag = i === 0 ? 'mod' : (i === 2 ? 'sup2' : ''); + const extraOptions = i === 0 ? { + site: { + provider: 'github', + name: 'Test name', + url: 'https://github.com/Microsoft/TypeScript', + } + } : undefined; + const p = entities.pony(57 + 1 * i, 47 + 1 * i) as ServerEntity; + p.options = { tag }; + setEntityName(p, name); + p.encryptedInfoSafe = encryptInfo(item.info || ''); + p.client = { + map, + accountSettings: {}, + account: { id: 'foobar', name: 'Debug account' } as any, + country: 'XY', + regions: [], + saysQueue: { push() { }, length: 0 } as any, + notifications: [], + camera: createCamera(), + accountId: 'foobar', + characterId: '', + ignores: new Set(), + hides: new Set(), + permaHides: new Set(), + updateQueue: createBinaryWriter(1), + addEntity() { }, + addNotification() { }, + removeNotification() { }, + updateParty() { }, + mapUpdate() { }, + } as Partial as any; + p.client!.pony = p; + this.clients.push(p.client!); + p.extraOptions = extraOptions; + world.addEntity(p, map); + })); + } - this.initialized = true; - } - update() { - timingStart('TestController.update()'); - timingEnd(); - } - sparseUpdate() { - timingStart('TestController.sparseUpdate()'); + this.initialized = true; + } + update() { + timingStart('TestController.update()'); + timingEnd(); + } + sparseUpdate() { + timingStart('TestController.sparseUpdate()'); - for (const client of this.clients) { - for (const notification of client.notifications) { - notification.accept && notification.accept(); - } - } + for (const client of this.clients) { + for (const notification of client.notifications) { + notification.accept && notification.accept(); + } + } - timingEnd(); - } + timingEnd(); + } } diff --git a/src/ts/server/controllers/torchController.ts b/src/ts/server/controllers/torchController.ts index 60b196c..941b7d0 100644 --- a/src/ts/server/controllers/torchController.ts +++ b/src/ts/server/controllers/torchController.ts @@ -7,27 +7,27 @@ import { hasFlag } from '../../common/utils'; import { EntityFlags } from '../../common/interfaces'; export class TorchController implements Controller { - private lights: ServerEntity[] = []; - constructor(private world: World, private map: ServerMap) { - } - initialize() { - this.lights = []; + private lights: ServerEntity[] = []; + constructor(private world: World, private map: ServerMap) { + } + initialize() { + this.lights = []; - for (const region of this.map.regions) { - for (const entity of region.entities) { - if (hasFlag(entity.flags, EntityFlags.OnOff)) { - this.lights.push(entity); - } - } - } - } - update() { - timingStart('TorchController.update()'); - timingEnd(); - } - sparseUpdate() { - timingStart('TorchController.sparseUpdate()'); - updateLights(this.lights, isNight(this.world.time)); - timingEnd(); - } + for (const region of this.map.regions) { + for (const entity of region.entities) { + if (hasFlag(entity.flags, EntityFlags.OnOff)) { + this.lights.push(entity); + } + } + } + } + update() { + timingStart('TorchController.update()'); + timingEnd(); + } + sparseUpdate() { + timingStart('TorchController.sparseUpdate()'); + updateLights(this.lights, isNight(this.world.time)); + timingEnd(); + } } diff --git a/src/ts/server/controllers/updateController.ts b/src/ts/server/controllers/updateController.ts index de97bef..9407a94 100644 --- a/src/ts/server/controllers/updateController.ts +++ b/src/ts/server/controllers/updateController.ts @@ -2,27 +2,27 @@ import { Controller, ServerEntity, ServerMap } from '../serverInterfaces'; import { timingStart, timingEnd } from '../timing'; export class UpdateController implements Controller { - private updatables: ServerEntity[] = []; - constructor(private map: ServerMap) { - } - initialize() { - this.updatables = []; + private updatables: ServerEntity[] = []; + constructor(private map: ServerMap) { + } + initialize() { + this.updatables = []; - for (const region of this.map.regions) { - for (const entity of region.entities) { - if (entity.serverUpdate) { - this.updatables.push(entity); - } - } - } - } - update(delta: number, now: number) { - timingStart('TorchController.update()'); + for (const region of this.map.regions) { + for (const entity of region.entities) { + if (entity.serverUpdate) { + this.updatables.push(entity); + } + } + } + } + update(delta: number, now: number) { + timingStart('TorchController.update()'); - for (const entity of this.updatables) { - entity.serverUpdate!(delta, now); - } + for (const entity of this.updatables) { + entity.serverUpdate!(delta, now); + } - timingEnd(); - } + timingEnd(); + } } diff --git a/src/ts/server/controllers/wallController.ts b/src/ts/server/controllers/wallController.ts index 6542ffe..a5cf343 100644 --- a/src/ts/server/controllers/wallController.ts +++ b/src/ts/server/controllers/wallController.ts @@ -7,172 +7,172 @@ import { array } from '../../common/utils'; import { Walls } from '../../common/entities'; const createGetAt = (width: number, height: number) => (items: T[], x: number, y: number) => { - return (x < 0 || y < 0 || x >= width || y >= height) ? undefined : items[x + y * width]; + return (x < 0 || y < 0 || x >= width || y >= height) ? undefined : items[x + y * width]; }; const createSetAt = (width: number, height: number) => (items: T[], x: number, y: number, value: T) => { - if (x >= 0 && y >= 0 && x < width && y < height) { - items[x + y * width] = value; - } + if (x >= 0 && y >= 0 && x < width && y < height) { + items[x + y * width] = value; + } }; export class WallController implements Controller { - top = 0; - isTall = (_x: number, _y: number) => false; - lockOuterWalls = false; - private lockedTiles = new Set(); - private hWalls: (Entity | undefined)[]; - private vWalls: (Entity | undefined)[]; - constructor(world: World, map: ServerMap, walls: Walls) { - const width = map.width + 1; - const height = map.height + 1; + top = 0; + isTall = (_x: number, _y: number) => false; + lockOuterWalls = false; + private lockedTiles = new Set(); + private hWalls: (Entity | undefined)[]; + private vWalls: (Entity | undefined)[]; + constructor(world: World, map: ServerMap, walls: Walls) { + const width = map.width + 1; + const height = map.height + 1; - const getAt = createGetAt(width, height); - const setAt = createSetAt(width, height); + const getAt = createGetAt(width, height); + const setAt = createSetAt(width, height); - const hWalls = this.hWalls = array(width * height, undefined); - const vWalls = this.vWalls = array(width * height, undefined); - const cWalls = array(width * height, undefined); + const hWalls = this.hWalls = array(width * height, undefined); + const vWalls = this.vWalls = array(width * height, undefined); + const cWalls = array(width * height, undefined); - const yOffset = 3 / tileHeight; + const yOffset = 3 / tileHeight; - const { wallHShort, wallVShort, wallH, wallV, wallCorners, wallCornersShort, wallCutR, wallCutL } = walls; + const { wallHShort, wallVShort, wallH, wallV, wallCorners, wallCornersShort, wallCutR, wallCutL } = walls; - const calcCorner = (x: number, y: number) => { - // top right bottom left - return (getAt(vWalls, x, y - 1) ? 8 : 0) - + (getAt(hWalls, x, y) ? 4 : 0) - + (getAt(vWalls, x, y) ? 2 : 0) - + (getAt(hWalls, x - 1, y) ? 1 : 0); - }; + const calcCorner = (x: number, y: number) => { + // top right bottom left + return (getAt(vWalls, x, y - 1) ? 8 : 0) + + (getAt(hWalls, x, y) ? 4 : 0) + + (getAt(vWalls, x, y) ? 2 : 0) + + (getAt(hWalls, x - 1, y) ? 1 : 0); + }; - const updateCorner = (x: number, y: number) => { - if (x < 0 || y < 0 || x >= width || y >= height) - return; + const updateCorner = (x: number, y: number) => { + if (x < 0 || y < 0 || x >= width || y >= height) + return; - const top = this.top; - const isOutside = x === 0 || y <= top || x === map.width || this.isTall(x, y); - const corners = isOutside ? wallCorners : wallCornersShort; - const current = getAt(cWalls, x, y); - const calc = calcCorner(x, y); + const top = this.top; + const isOutside = x === 0 || y <= top || x === map.width || this.isTall(x, y); + const corners = isOutside ? wallCorners : wallCornersShort; + const current = getAt(cWalls, x, y); + const calc = calcCorner(x, y); - if (!current || current.type !== corners[calc].type) { - if (current) { - world.removeEntity(current, map); - } + if (!current || current.type !== corners[calc].type) { + if (current) { + world.removeEntity(current, map); + } - setAt(cWalls, x, y, calc ? world.addEntity(corners[calc](x, y + yOffset), map) : undefined); - } - }; + setAt(cWalls, x, y, calc ? world.addEntity(corners[calc](x, y + yOffset), map) : undefined); + } + }; - this.toggleWall = (x, y, type) => { - if (x < 0 || y < 0 || x >= width || y >= height) - return; + this.toggleWall = (x, y, type) => { + if (x < 0 || y < 0 || x >= width || y >= height) + return; - if (this.lockedTiles.has(`${x},${y}:${type}`)) - return; + if (this.lockedTiles.has(`${x},${y}:${type}`)) + return; - const walls = type === TileType.WallH ? hWalls : vWalls; - const entity = getAt(walls, x, y); - const top = this.top; + const walls = type === TileType.WallH ? hWalls : vWalls; + const entity = getAt(walls, x, y); + const top = this.top; - if (type === TileType.WallH && x === (width - 1)) - return; + if (type === TileType.WallH && x === (width - 1)) + return; - if (type === TileType.WallV && y === (height - 1)) - return; + if (type === TileType.WallV && y === (height - 1)) + return; - if (this.lockOuterWalls) { - if (type === TileType.WallH && (y <= top || y === (width - 1))) - return; - if (type === TileType.WallV && (x === 0 || x === (height - 1) || y < top)) - return; - } + if (this.lockOuterWalls) { + if (type === TileType.WallH && (y <= top || y === (width - 1))) + return; + if (type === TileType.WallV && (x === 0 || x === (height - 1) || y < top)) + return; + } - if (entity) { - world.removeEntity(entity, map); - setAt(walls, x, y, undefined); - } else { - if (type === TileType.WallH) { - const ctor = (y <= top || this.isTall(x, y)) ? - wallH : (x === 0 ? wallCutL : (x === (width - 2) ? wallCutR : wallHShort)); - setAt(walls, x, y, world.addEntity(ctor(x + 0.5, y + yOffset), map)); - } else { - const ctor = (x === 0 || x === (width - 1) || this.isTall(x, y)) ? wallV : wallVShort; - setAt(walls, x, y, world.addEntity(ctor(x, y + 0.5), map)); - } - } + if (entity) { + world.removeEntity(entity, map); + setAt(walls, x, y, undefined); + } else { + if (type === TileType.WallH) { + const ctor = (y <= top || this.isTall(x, y)) ? + wallH : (x === 0 ? wallCutL : (x === (width - 2) ? wallCutR : wallHShort)); + setAt(walls, x, y, world.addEntity(ctor(x + 0.5, y + yOffset), map)); + } else { + const ctor = (x === 0 || x === (width - 1) || this.isTall(x, y)) ? wallV : wallVShort; + setAt(walls, x, y, world.addEntity(ctor(x, y + 0.5), map)); + } + } - updateCorner(x, y); - updateCorner(x + 1, y); - updateCorner(x, y + 1); - }; - } - initialize() { - } - update() { - } - toggleWall?: (x: number, y: number, type: TileType) => void; - lockWall(x: number, y: number, type: TileType.WallH | TileType.WallV) { - this.lockedTiles.add(`${x},${y}:${type}`); - } - serialize() { - const data = new Uint8Array(Math.ceil(this.vWalls.length / 8) + Math.ceil(this.hWalls.length / 8)); - let offset = 0; + updateCorner(x, y); + updateCorner(x + 1, y); + updateCorner(x, y + 1); + }; + } + initialize() { + } + update() { + } + toggleWall?: (x: number, y: number, type: TileType) => void; + lockWall(x: number, y: number, type: TileType.WallH | TileType.WallV) { + this.lockedTiles.add(`${x},${y}:${type}`); + } + serialize() { + const data = new Uint8Array(Math.ceil(this.vWalls.length / 8) + Math.ceil(this.hWalls.length / 8)); + let offset = 0; - for (let i = 0; i < this.vWalls.length; i += 8, offset++) { - let value = 0; + for (let i = 0; i < this.vWalls.length; i += 8, offset++) { + let value = 0; - for (let j = 0; j < 8; j++) { - if (this.vWalls[i + j]) { - value |= (1 << j); - } - } + for (let j = 0; j < 8; j++) { + if (this.vWalls[i + j]) { + value |= (1 << j); + } + } - data[offset] = value; - } + data[offset] = value; + } - for (let i = 0; i < this.hWalls.length; i += 8, offset++) { - let value = 0; + for (let i = 0; i < this.hWalls.length; i += 8, offset++) { + let value = 0; - for (let j = 0; j < 8; j++) { - if (this.hWalls[i + j]) { - value |= (1 << j); - } - } + for (let j = 0; j < 8; j++) { + if (this.hWalls[i + j]) { + value |= (1 << j); + } + } - data[offset] = value; - } + data[offset] = value; + } - return fromByteArray(data); - } - deserialize(width: number, height: number, serialized: string) { - const data = toByteArray(serialized); - const size = (width + 1) * (height + 1); - let offset = 0; + return fromByteArray(data); + } + deserialize(width: number, height: number, serialized: string) { + const data = toByteArray(serialized); + const size = (width + 1) * (height + 1); + let offset = 0; - for (let i = 0; i < size; i += 8, offset++) { - let value = data[offset]; + for (let i = 0; i < size; i += 8, offset++) { + let value = data[offset]; - for (let j = 0; j < 8; j++) { - if ((!!this.vWalls[i + j]) !== ((value & (1 << j)) !== 0)) { - const x = (i + j) % (width + 1); - const y = Math.floor((i + j) / (width + 1)); - this.toggleWall!(x, y, TileType.WallV); - } - } - } + for (let j = 0; j < 8; j++) { + if ((!!this.vWalls[i + j]) !== ((value & (1 << j)) !== 0)) { + const x = (i + j) % (width + 1); + const y = Math.floor((i + j) / (width + 1)); + this.toggleWall!(x, y, TileType.WallV); + } + } + } - for (let i = 0; i < size; i += 8, offset++) { - let value = data[offset]; + for (let i = 0; i < size; i += 8, offset++) { + let value = data[offset]; - for (let j = 0; j < 8; j++) { - if ((!!this.hWalls[i + j]) !== ((value & (1 << j)) !== 0)) { - const x = (i + j) % (width + 1); - const y = Math.floor((i + j) / (width + 1)); - this.toggleWall!(x, y, TileType.WallH); - } - } - } - } + for (let j = 0; j < 8; j++) { + if ((!!this.hWalls[i + j]) !== ((value & (1 << j)) !== 0)) { + const x = (i + j) % (width + 1); + const y = Math.floor((i + j) / (width + 1)); + this.toggleWall!(x, y, TileType.WallH); + } + } + } + } } diff --git a/src/ts/server/db.ts b/src/ts/server/db.ts index 5a42fd4..0e3c989 100644 --- a/src/ts/server/db.ts +++ b/src/ts/server/db.ts @@ -1,7 +1,7 @@ import { model, Schema, Types, Document, Query } from 'mongoose'; import { - TimestampsBase, EventBase, CharacterBase, AccountBase, AuthBase, OriginBase, OriginInfoBase, CharacterState, - SupporterInviteBase, FriendRequestBase, HideRequestBase, MergeHideData + TimestampsBase, EventBase, CharacterBase, AccountBase, AuthBase, OriginBase, OriginInfoBase, CharacterState, + SupporterInviteBase, FriendRequestBase, HideRequestBase, MergeHideData } from '../common/adminInterfaces'; import { logger } from './logger'; import { isAdmin } from '../common/accountUtils'; @@ -13,7 +13,7 @@ import { filterName } from '../common/swears'; //set('debug', true); // debug mongoose export interface Doc extends Document { - updatedAt: Date; + updatedAt: Date; } export interface IOriginInfo extends OriginInfoBase { } @@ -26,175 +26,175 @@ export interface IFriendRequest extends FriendRequestBase, Doc { export interface IHideRequest extends HideRequestBase, Doc { } export interface ICharacter extends CharacterBase, Doc { - auth?: IAuth; + auth?: IAuth; } export interface IAccount extends AccountBase, Doc { - auths?: IAuth[]; - characters?: ICharacter[]; + auths?: IAuth[]; + characters?: ICharacter[]; } export interface ISession extends Doc { - session: string; + session: string; } // schemas const originInfo = { - ip: String, - country: String, - last: Date, + ip: String, + country: String, + last: Date, }; const mergeInfo = { - id: String, - name: String, - //code: Number, - date: Date, - reason: String, - data: Object, - split: Boolean, + id: String, + name: String, + //code: Number, + date: Date, + reason: String, + data: Object, + split: Boolean, }; const logEntry = { - message: String, - date: Date, + message: String, + date: Date, }; const authSchema = new Schema({ - account: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, - openId: String, - provider: String, - name: String, - url: String, - emails: [String], - disabled: Boolean, - banned: Boolean, - pledged: Number, - lastUsed: Date, + account: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + openId: String, + provider: String, + name: String, + url: String, + emails: [String], + disabled: Boolean, + banned: Boolean, + pledged: Number, + lastUsed: Date, }, { timestamps: true }); authSchema.index({ updatedAt: 1 }); authSchema.index({ openId: 1, provider: 1 }, { unique: true }); const bannedMuted = { - mute: Number, - shadow: Number, - ban: Number, + mute: Number, + shadow: Number, + ban: Number, }; const originSchema = new Schema({ - ip: { type: String, index: true }, - country: String, - ...bannedMuted, + ip: { type: String, index: true }, + country: String, + ...bannedMuted, }, { timestamps: true }); originSchema.index({ updatedAt: 1 }); const accountSchema = new Schema({ - name: String, - birthdate: Date, - birthyear: Number, - // code: Number, - emails: { type: [String], index: true }, - lastVisit: Date, - lastUserAgent: String, - lastBrowserId: String, - lastOnline: Date, - lastCharacter: Schema.Types.ObjectId, - roles: [String], - origins: [originInfo], - note: String, - noteUpdated: Date, - ignores: [String], - // friends: [{ type: Schema.Types.ObjectId, unique: true, ref: 'Account' }], - flags: Number, - characterCount: { type: Number, default: 0 }, - // NOTE: use account.markModified('settings') if changed nested field - settings: { type: Schema.Types.Mixed, default: () => ({}) }, - counters: { type: Schema.Types.Mixed, default: () => ({}) }, - patreon: Number, - supporter: Number, - supporterLog: [logEntry], - supporterTotal: Number, - supporterDeclinedSince: Date, - merges: [mergeInfo], - banLog: [logEntry], - mute: Number, - shadow: Number, - ban: Number, - // auths: [{ type: Schema.Types.ObjectId, ref: 'Auth' }], - state: Object, - alert: Object, - savedMap: String, + name: String, + birthdate: Date, + birthyear: Number, + // code: Number, + emails: { type: [String], index: true }, + lastVisit: Date, + lastUserAgent: String, + lastBrowserId: String, + lastOnline: Date, + lastCharacter: Schema.Types.ObjectId, + roles: [String], + origins: [originInfo], + note: String, + noteUpdated: Date, + ignores: [String], + // friends: [{ type: Schema.Types.ObjectId, unique: true, ref: 'Account' }], + flags: Number, + characterCount: { type: Number, default: 0 }, + // NOTE: use account.markModified('settings') if changed nested field + settings: { type: Schema.Types.Mixed, default: () => ({}) }, + counters: { type: Schema.Types.Mixed, default: () => ({}) }, + patreon: Number, + supporter: Number, + supporterLog: [logEntry], + supporterTotal: Number, + supporterDeclinedSince: Date, + merges: [mergeInfo], + banLog: [logEntry], + mute: Number, + shadow: Number, + ban: Number, + // auths: [{ type: Schema.Types.ObjectId, ref: 'Auth' }], + state: Object, + alert: Object, + savedMap: String, }, { timestamps: true }); accountSchema.virtual('auths', { - ref: 'Auth', - localField: '_id', - foreignField: 'account', + ref: 'Auth', + localField: '_id', + foreignField: 'account', }); accountSchema.virtual('characters', { - ref: 'Character', - localField: '_id', - foreignField: 'account', + ref: 'Character', + localField: '_id', + foreignField: 'account', }); accountSchema.index({ updatedAt: 1 }); const characterSchema = new Schema({ - account: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, - site: { type: Schema.Types.ObjectId, ref: 'Auth' }, - name: { type: String, index: true }, - desc: String, - tag: String, - info: String, - flags: { type: Number, default: 0 }, - lastUsed: { type: Date, index: true }, - creator: String, - state: Object, + account: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + site: { type: Schema.Types.ObjectId, ref: 'Auth' }, + name: { type: String, index: true }, + desc: String, + tag: String, + info: String, + flags: { type: Number, default: 0 }, + lastUsed: { type: Date, index: true }, + creator: String, + state: Object, }, { timestamps: true }); characterSchema.index({ updatedAt: 1 }); characterSchema.index({ createdAt: 1 }); const eventSchema = new Schema({ - account: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, - pony: Schema.Types.ObjectId, - type: String, - server: String, - message: String, - desc: String, - origin: originInfo, - count: { type: Number, default: 1 }, + account: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + pony: Schema.Types.ObjectId, + type: String, + server: String, + message: String, + desc: String, + origin: originInfo, + count: { type: Number, default: 1 }, }, { timestamps: true }); eventSchema.index({ updatedAt: 1 }); const supporterInviteSchema = new Schema({ - source: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, - target: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, - name: String, - info: String, - active: Boolean, + source: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + target: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + name: String, + info: String, + active: Boolean, }, { timestamps: true }); const friendRequestSchema = new Schema({ - source: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, - target: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + source: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + target: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, }); const hideRequestSchema = new Schema({ - source: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, - target: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, - name: String, - date: Date, + source: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + target: { type: Schema.Types.ObjectId, index: true, ref: 'Account' }, + name: String, + date: Date, }); const sessionSchema = new Schema({ - _id: String, - session: String, + _id: String, + session: String, }); // models @@ -206,13 +206,13 @@ export const Session = model('session', sessionSchema); export const Character = model('Character', characterSchema); accountSchema.post('remove', function (doc: Document) { - Promise.all([ - Character.deleteMany({ account: doc._id }).exec(), - Event.deleteMany({ account: doc._id }).exec(), - Auth.deleteMany({ account: doc._id }).exec(), - FriendRequest.deleteMany({ $or: [{ target: doc._id }, { source: doc._id }] }).exec(), - HideRequest.deleteMany({ $or: [{ target: doc._id }, { source: doc._id }] }).exec(), - ]).catch(logger.error); + Promise.all([ + Character.deleteMany({ account: doc._id }).exec(), + Event.deleteMany({ account: doc._id }).exec(), + Auth.deleteMany({ account: doc._id }).exec(), + FriendRequest.deleteMany({ $or: [{ target: doc._id }, { source: doc._id }] }).exec(), + HideRequest.deleteMany({ $or: [{ target: doc._id }, { source: doc._id }] }).exec(), + ]).catch(logger.error); }); export const Account = model('Account', accountSchema); @@ -225,58 +225,58 @@ export const HideRequest = model('HideRequest', hideRequestSchema) export type ID = Types.ObjectId | string; export interface MongoQueryExpr { - $exists?: boolean; - $ne?: T; - $in?: T | T[]; - $gt?: T; - $lt?: T; - $not?: MongoQueryExpr; - $size?: number; - $regex?: RegExp; + $exists?: boolean; + $ne?: T; + $in?: T | T[]; + $gt?: T; + $lt?: T; + $not?: MongoQueryExpr; + $size?: number; + $regex?: RegExp; } export interface MongoUpdateExprField { - $inc?: any; - $dec?: any; - $pull?: any; - $push?: any; - $unset?: { - [P in keyof T]?: any; - }; + $inc?: any; + $dec?: any; + $pull?: any; + $push?: any; + $unset?: { + [P in keyof T]?: any; + }; } export interface MongoUpdateExpr extends MongoUpdateExprField { - $addToSet?: any; + $addToSet?: any; } export type MongoQuery = { - [P in keyof T]?: T[P] | MongoQueryExpr; + [P in keyof T]?: T[P] | MongoQueryExpr; }; export type MongoUpdate = { - [P in keyof T]?: T[P] | MongoUpdateExprField; + [P in keyof T]?: T[P] | MongoUpdateExprField; } & MongoUpdateExpr; export function iterate(query: Query, onData: (doc: T) => void) { - return new Promise(resolve => { - query.cursor() - .on('data', onData) - .on('end', resolve); - }); + return new Promise(resolve => { + query.cursor() + .on('data', onData) + .on('end', resolve); + }); } function throwOnEmpty(message: string): (item: T | undefined) => T { - return item => { - if (item) { - return item; - } else { - throw new Error(message); - } - }; + return item => { + if (item) { + return item; + } else { + throw new Error(message); + } + }; } export function nullToUndefined(item: T | null): T | undefined { - return item === null ? undefined : item; + return item === null ? undefined : item; } export const checkCharacterExists = throwOnEmpty('Character does not exist'); @@ -293,45 +293,45 @@ export type UpdateCharacterState = (characterId: ID, serverName: string, state: export type QueryCharacter = (query: MongoQuery, fields?: string) => Promise; export function createCharacter(account: IAccount) { - return new Character({ account: account._id, creator: `${account.name} [${account._id}]` }); + return new Character({ account: account._id, creator: `${account.name} [${account._id}]` }); } export function characterCount(account: ID): Promise { - return Character.countDocuments({ account }).exec(); + return Character.countDocuments({ account }).exec(); } export function findCharacter(pony: ID, account: ID): Promise { - return Character.findOne({ _id: pony, account }).exec().then(nullToUndefined); + return Character.findOne({ _id: pony, account }).exec().then(nullToUndefined); } export function findCharacterSafe(pony: ID, accountId: ID): Promise { - return findCharacter(pony, accountId) - .then(checkCharacterExists); + return findCharacter(pony, accountId) + .then(checkCharacterExists); } export function findCharacterById(id: string): Promise { - return Character.findById(id).exec().then(nullToUndefined); + return Character.findById(id).exec().then(nullToUndefined); } export const findAllCharacters: FindCharacters = (account, fields) => - Character.find({ account }, fields).lean().exec(); + Character.find({ account }, fields).lean().exec(); export function findLatestCharacters(account: ID, count: number): Promise { - return Character.find({ account }) - .sort('-lastUsed') - .limit(count) - .exec(); + return Character.find({ account }) + .sort('-lastUsed') + .limit(count) + .exec(); } export function removeCharacter(id: ID, account: ID): Promise { - return Character.findOneAndRemove({ _id: id, account }).exec().then(nullToUndefined); + return Character.findOneAndRemove({ _id: id, account }).exec().then(nullToUndefined); } export const updateCharacterState: UpdateCharacterState = (characterId, serverName, state) => - Character.updateOne({ _id: characterId }, { [`state.${serverName}`]: state }).exec().then(nullToUndefined); + Character.updateOne({ _id: characterId }, { [`state.${serverName}`]: state }).exec().then(nullToUndefined); export const queryCharacter: QueryCharacter = (query, fields) => - Character.findOne(query, fields).exec() as any; + Character.findOne(query, fields).exec() as any; // auths @@ -342,28 +342,28 @@ export type QueryAuths = (query: MongoQuery, fields?: string) => Promise< export type UpdateAuth = (authId: ID, update: MongoUpdate) => Promise; export const findAuthByOpenId = (openId: string, provider: string): Promise => - Auth.findOne({ openId, provider }).exec().then(nullToUndefined); + Auth.findOne({ openId, provider }).exec().then(nullToUndefined); export const findAuthByEmail = (emails: string[]): Promise => - Auth.findOne({ emails: { $in: emails } }).exec().then(nullToUndefined); + Auth.findOne({ emails: { $in: emails } }).exec().then(nullToUndefined); export const findAuth: FindAuth = (auth, account, fields) => - Auth.findOne({ _id: auth, account }, fields).exec().then(nullToUndefined); + Auth.findOne({ _id: auth, account }, fields).exec().then(nullToUndefined); export const findAllAuths: FindAuths = (account, fields) => - Auth.find({ account, fields }).exec(); + Auth.find({ account, fields }).exec(); export const findAllVisibleAuths: FindAuths = (account, fields) => - Auth.find({ account, disabled: { $ne: true }, banned: { $ne: true } }, fields).lean().exec(); + Auth.find({ account, disabled: { $ne: true }, banned: { $ne: true } }, fields).lean().exec(); export const countAllVisibleAuths: CountAuths = (account) => - Auth.find({ account, disabled: { $ne: true }, banned: { $ne: true } }).countDocuments().exec(); + Auth.find({ account, disabled: { $ne: true }, banned: { $ne: true } }).countDocuments().exec(); export const queryAuths: QueryAuths = (query, fields) => - Auth.find(query, fields).lean().exec(); + Auth.find(query, fields).lean().exec(); export const updateAuth: UpdateAuth = (id, update) => - Auth.updateOne({ _id: id }, update).exec(); + Auth.updateOne({ _id: id }, update).exec(); // accounts @@ -374,112 +374,112 @@ export type QueryAccounts = (query: MongoQuery, fields?: string) => Pr export type QueryAccount = (query: MongoQuery, fields?: string) => Promise; export const findAccount = (account: ID, projection?: string): Promise => - Account.findById(account, projection).exec().then(nullToUndefined); + Account.findById(account, projection).exec().then(nullToUndefined); export function checkIfAdmin(account: ID): Promise { - return Account.findOne({ _id: account }, 'roles').lean().exec() - .then(a => a && isAdmin(a)); + return Account.findOne({ _id: account }, 'roles').lean().exec() + .then(a => a && isAdmin(a)); } export function findAccountSafe(account: ID, projection?: string): Promise { - return findAccount(account, projection) - .then(checkAccountExists); + return findAccount(account, projection) + .then(checkAccountExists); } export const updateAccount: UpdateAccount = (accountId, update) => - Account.updateOne({ _id: accountId }, update).exec(); + Account.updateOne({ _id: accountId }, update).exec(); export const updateAccounts: UpdateAccounts = (query, update) => - Account.updateMany(query, update).exec(); + Account.updateMany(query, update).exec(); export const queryAccounts: QueryAccounts = (query, fields) => - Account.find(query, fields).lean().exec(); + Account.find(query, fields).lean().exec(); export const queryAccount: QueryAccount = (query, fields) => - Account.findOne(query, fields).exec().then(nullToUndefined); + Account.findOne(query, fields).exec().then(nullToUndefined); // supporter invites export type HasActiveSupporterInvites = (accountId: ID) => Promise; export const hasActiveSupporterInvites: HasActiveSupporterInvites = (accountId) => - SupporterInvite.countDocuments({ target: accountId, active: true }).exec() - .then(count => count > 0); + SupporterInvite.countDocuments({ target: accountId, active: true }).exec() + .then(count => count > 0); // friend requests export async function findFriendIds(accountId: ID) { - const accountIdString = accountId.toString(); + const accountIdString = accountId.toString(); - const friendRequests = await FriendRequest - .find({ $or: [{ source: accountId }, { target: accountId }] }, 'source target') - .lean() - .exec(); + const friendRequests = await FriendRequest + .find({ $or: [{ source: accountId }, { target: accountId }] }, 'source target') + .lean() + .exec(); - const friendIds = friendRequests - .map((f: any) => f.source.toString() === accountIdString ? f.target.toString() : f.source.toString()); + const friendIds = friendRequests + .map((f: any) => f.source.toString() === accountIdString ? f.target.toString() : f.source.toString()); - return friendIds; + return friendIds; } export async function findFriends(accountId: ID, withCharacters: boolean): Promise { - const friendIds = await findFriendIds(accountId); - const accounts: IAccount[] = await Account.find({ _id: { $in: friendIds } }, '_id name lastOnline lastCharacter').lean().exec(); - let characters: ICharacter[] = []; + const friendIds = await findFriendIds(accountId); + const accounts: IAccount[] = await Account.find({ _id: { $in: friendIds } }, '_id name lastOnline lastCharacter').lean().exec(); + let characters: ICharacter[] = []; - if (withCharacters) { - const characterIds = accounts.map(a => a.lastCharacter).filter(id => id); - characters = await Character.find({ _id: { $in: characterIds } }, '_id name info').lean().exec(); - } + if (withCharacters) { + const characterIds = accounts.map(a => a.lastCharacter).filter(id => id); + characters = await Character.find({ _id: { $in: characterIds } }, '_id name info').lean().exec(); + } - return accounts.map(a => { - const characterId = a.lastCharacter && a.lastCharacter.toString(); - const character = characterId && characters.find(c => c._id.toString() === characterId); - const name = character && filterForbidden(replaceEmojis(character.name)); - const nameFiltered = name && filterName(name); + return accounts.map(a => { + const characterId = a.lastCharacter && a.lastCharacter.toString(); + const character = characterId && characters.find(c => c._id.toString() === characterId); + const name = character && filterForbidden(replaceEmojis(character.name)); + const nameFiltered = name && filterName(name); - return { - accountId: a._id.toString(), - accountName: a.name, - name, - pony: character && character.info, - nameBad: name !== nameFiltered, - }; - }); + return { + accountId: a._id.toString(), + accountName: a.name, + name, + pony: character && character.info, + nameBad: name !== nameFiltered, + }; + }); } // hide requests export async function findHideIds(accountId: ID) { - const hideRequests: IHideRequest[] = await HideRequest.find({ source: accountId }, 'target').lean().exec(); - return hideRequests.map(f => f.target.toString()); + const hideRequests: IHideRequest[] = await HideRequest.find({ source: accountId }, 'target').lean().exec(); + return hideRequests.map(f => f.target.toString()); } export async function findHideIdsRev(accountId: ID) { - const hideRequests: IHideRequest[] = await HideRequest.find({ target: accountId }, 'source').lean().exec(); - return hideRequests.map(f => f.source.toString()); + const hideRequests: IHideRequest[] = await HideRequest.find({ target: accountId }, 'source').lean().exec(); + return hideRequests.map(f => f.source.toString()); } export async function findHidesForMerge(accountId: ID): Promise { - const hideRequests: IHideRequest[] = await HideRequest - .find({ source: accountId }, '_id name date') - .lean() - .exec(); + const hideRequests: IHideRequest[] = await HideRequest + .find({ source: accountId }, '_id name date') + .lean() + .exec(); - return hideRequests.map(f => ({ - id: f._id.toString(), - name: f.name, - date: f.date.toString(), - })); + return hideRequests.map(f => ({ + id: f._id.toString(), + name: f.name, + date: f.date.toString(), + })); } export async function addHide(source: ID, target: ID, name: string) { - if (source.toString() === target.toString()) - return; + if (source.toString() === target.toString()) + return; - const existing = await HideRequest.findOne({ source, target }, '_id').lean().exec(); + const existing = await HideRequest.findOne({ source, target }, '_id').lean().exec(); - if (!existing) { - await HideRequest.create({ source, target, name, date: new Date() }); - } + if (!existing) { + await HideRequest.create({ source, target, name, date: new Date() }); + } } diff --git a/src/ts/server/entityUtils.ts b/src/ts/server/entityUtils.ts index a62fd7b..fdbd1c3 100644 --- a/src/ts/server/entityUtils.ts +++ b/src/ts/server/entityUtils.ts @@ -1,12 +1,12 @@ import { resizeWriter, writeUint8, BinaryWriter, writeUint32, writeUint16 } from 'ag-sockets'; import { encodeString } from 'ag-sockets/dist/utf8'; import { - Entity, Rect, EntityState, UpdateFlags, Action, EntityOrPonyOptions, UpdateType, TileType, canWalk, setAnimationToEntityState + Entity, Rect, EntityState, UpdateFlags, Action, EntityOrPonyOptions, UpdateType, TileType, canWalk, setAnimationToEntityState } from '../common/interfaces'; import { normalize, containsPoint, boundsIntersect, clamp, pointInXYWH, hasFlag, setFlag } from '../common/utils'; import { ServerEntity, ServerEntityWithClient, ServerMap, EntityUpdateBase, IClient } from './serverInterfaces'; import { - isCritter, isDecal, entityInRange, SIT_ON_BOUNDS_WIDTH, SIT_ON_BOUNDS_HEIGHT, SIT_ON_BOUNDS_OFFSET + isCritter, isDecal, entityInRange, SIT_ON_BOUNDS_WIDTH, SIT_ON_BOUNDS_HEIGHT, SIT_ON_BOUNDS_OFFSET } from '../common/entityUtils'; import { pushUpdateEntityToRegion } from './serverRegion'; import { getRegion, getRegionGlobal, getTile } from '../common/worldMap'; @@ -17,330 +17,330 @@ import { PONY_TYPE } from '../common/constants'; import { grapesPurple, grapesGreen } from '../common/entities'; export function isEntityShadowed(entity: ServerEntity): entity is ServerEntityWithClient { - return entity.client !== undefined && entity.client.shadowed; + return entity.client !== undefined && entity.client.shadowed; } export function setEntityName(entity: ServerEntity, name: string) { - entity.name = name; - entity.nameBad = name !== filterName(name); - entity.encodedName = encodeString(name)!; + entity.name = name; + entity.nameBad = name !== filterName(name); + entity.encodedName = encodeString(name)!; } export function getEntityName(entity: ServerEntity, client: IClient) { - if (entity.name && entity.nameBad && client.accountSettings.filterSwearWords) { - return filterName(entity.name); - } else { - return entity.name; - } + if (entity.name && entity.nameBad && client.accountSettings.filterSwearWords) { + return filterName(entity.name); + } else { + return entity.name; + } } const grapeTypes = [...grapesPurple.map(x => x.type), ...grapesGreen.map(x => x.type)]; export function isHoldingGrapes(e: ServerEntity) { - const hold = e.options!.hold || 0; - return hold !== 0 && grapeTypes.indexOf(hold) !== -1; + const hold = e.options!.hold || 0; + return hold !== 0 && grapeTypes.indexOf(hold) !== -1; } export function canBoopEntity(e: ServerEntity, boopRect: Rect) { - if (e.type === PONY_TYPE) { - return isHoldingGrapes(e); - } else { - return e.boop !== undefined && containsPoint(0, 0, boopRect, e.x + (e.boopX || 0), e.y + (e.boopY || 0)); - } + if (e.type === PONY_TYPE) { + return isHoldingGrapes(e); + } else { + return e.boop !== undefined && containsPoint(0, 0, boopRect, e.x + (e.boopX || 0), e.y + (e.boopY || 0)); + } } function distSq(ax: number, ay: number, bx: number, by: number) { - const dx = ax - bx; - const dy = ay - by; - return dx * dx + dy * dy; + const dx = ax - bx; + const dy = ay - by; + return dx * dx + dy * dy; } export function findClosest(x: number, y: number, entities: Entity[]) { - let closest = entities[0]; - let distance = closest ? distSq(x, y, closest.x, closest.y) : 0; + let closest = entities[0]; + let distance = closest ? distSq(x, y, closest.x, closest.y) : 0; - for (let i = 1; i < entities.length; i++) { - const entity = entities[i]; - const dist = distSq(x, y, entity.x, entity.y); + for (let i = 1; i < entities.length; i++) { + const entity = entities[i]; + const dist = distSq(x, y, entity.x, entity.y); - if (dist < distance) { - closest = entity; - distance = dist; - } - } + if (dist < distance) { + closest = entity; + distance = dist; + } + } - return closest; + return closest; } export function moveRandomly( - map: ServerMap, e: ServerEntity, speed: number, randomness: number, timestamp: number + map: ServerMap, e: ServerEntity, speed: number, randomness: number, timestamp: number ) { - if (Math.random() < randomness) { - let vx = 0; - let vy = 0; + if (Math.random() < randomness) { + let vx = 0; + let vy = 0; - if (e.x < 0) { - vx = 1; - } else if (e.x > map.width) { - vx = -1; - } else if (e.y < 0) { - vy = 1; - } else if (e.y > map.height) { - vy = -1; - } else { - vx = Math.random() - 0.5; - vy = Math.random() - 0.5; - } + if (e.x < 0) { + vx = 1; + } else if (e.x > map.width) { + vx = -1; + } else if (e.y < 0) { + vy = 1; + } else if (e.y > map.height) { + vy = -1; + } else { + vx = Math.random() - 0.5; + vy = Math.random() - 0.5; + } - updateEntityVelocity(e, vx * speed, vy * speed, timestamp); - } + updateEntityVelocity(e, vx * speed, vy * speed, timestamp); + } } export function moveTowards(e: ServerEntity, x: number, y: number, speed: number, timestamp: number) { - const v = normalize(x - e.x, y - e.y); - updateEntityVelocity(e, v.x * speed, v.y * speed, timestamp); + const v = normalize(x - e.x, y - e.y); + updateEntityVelocity(e, v.x * speed, v.y * speed, timestamp); } // update entity functions export function setEntityAnimation(entity: ServerEntity, animation: number, faceRight?: boolean) { - let state = entity.state; + let state = entity.state; - if (faceRight !== undefined) { - state = setFlag(state, EntityState.FacingRight, faceRight); - } + if (faceRight !== undefined) { + state = setFlag(state, EntityState.FacingRight, faceRight); + } - state = setAnimationToEntityState(state, animation); - updateEntityState(entity, state); + state = setAnimationToEntityState(state, animation); + updateEntityState(entity, state); } export function updateEntityVelocity(entity: ServerEntity, vx: number, vy: number, timestamp: number) { - if (vx !== entity.vx || vy !== entity.vy) { - entity.vx = vx; - entity.vy = vy; - entity.timestamp = timestamp; - entity.state = setFlag(entity.state, EntityState.FacingRight, shouldBeFacingRight(entity)); - updateEntity(entity, false); - } + if (vx !== entity.vx || vy !== entity.vy) { + entity.vx = vx; + entity.vy = vy; + entity.timestamp = timestamp; + entity.state = setFlag(entity.state, EntityState.FacingRight, shouldBeFacingRight(entity)); + updateEntity(entity, false); + } } export function updateEntity(entity: ServerEntity, switchRegion: boolean) { - const flags = UpdateFlags.Position | UpdateFlags.State | (switchRegion ? UpdateFlags.SwitchRegion : 0); - const { x, y, vx, vy } = entity; - pushUpdateEntity({ entity, flags, x, y, vx, vy }); + const flags = UpdateFlags.Position | UpdateFlags.State | (switchRegion ? UpdateFlags.SwitchRegion : 0); + const { x, y, vx, vy } = entity; + pushUpdateEntity({ entity, flags, x, y, vx, vy }); } export function updateEntityState(entity: ServerEntity, state: EntityState) { - entity.state = state; - pushUpdateEntity({ entity, flags: UpdateFlags.State }); + entity.state = state; + pushUpdateEntity({ entity, flags: UpdateFlags.State }); } export function updateEntityOptions(entity: ServerEntity, options: Partial) { - entity.options = Object.assign(entity.options || {}, options) as any; - pushUpdateEntity({ entity, flags: UpdateFlags.Options, options }); + entity.options = Object.assign(entity.options || {}, options) as any; + pushUpdateEntity({ entity, flags: UpdateFlags.Options, options }); } export function updateEntityNameInfo(entity: ServerEntity) { - pushUpdateEntity({ entity, flags: UpdateFlags.Name | UpdateFlags.Info }); + pushUpdateEntity({ entity, flags: UpdateFlags.Name | UpdateFlags.Info }); } export function updateEntityExpression(entity: ServerEntity) { - pushUpdateEntity({ entity, flags: UpdateFlags.Expression }); + pushUpdateEntity({ entity, flags: UpdateFlags.Expression }); } export function sendAction(entity: ServerEntity, action: Action) { - pushUpdateEntity({ entity, flags: UpdateFlags.Action, action }); + pushUpdateEntity({ entity, flags: UpdateFlags.Action, action }); } export function pushUpdateEntity(update: EntityUpdateBase) { - const entity = update.entity; + const entity = update.entity; - if (isEntityShadowed(entity)) { - pushUpdateEntityToClient(entity.client, update); - } else if (entity.region) { - pushUpdateEntityToRegion(entity.region, update); - } + if (isEntityShadowed(entity)) { + pushUpdateEntityToClient(entity.client, update); + } else if (entity.region) { + pushUpdateEntityToRegion(entity.region, update); + } } export function isOverflowError(e: Error) { - return e instanceof RangeError || /DataView/.test(e.message); + return e instanceof RangeError || /DataView/.test(e.message); } function resizePreserveWriter(error: Error, writer: BinaryWriter, offset: number) { - if (isOverflowError(error)) { - const bytes = writer.bytes; - resizeWriter(writer); - writer.bytes.set(bytes); - writer.offset = offset; - // DEVELOPMENT && logger.debug(`resize writer to ${writer.bytes.byteLength} (${error.message})`); - } else { - throw error; - } + if (isOverflowError(error)) { + const bytes = writer.bytes; + resizeWriter(writer); + writer.bytes.set(bytes); + writer.offset = offset; + // DEVELOPMENT && logger.debug(`resize writer to ${writer.bytes.byteLength} (${error.message})`); + } else { + throw error; + } } export function pushAddEntityToClient(client: IClient, entity: ServerEntity) { - const writer = client.updateQueue; - const offset = writer.offset; + const writer = client.updateQueue; + const offset = writer.offset; - while (true) { - try { - writeUint8(writer, UpdateType.AddEntity); - writeOneEntity(writer, entity, client); - break; - } catch (e) { - resizePreserveWriter(e, writer, offset); - } - } + while (true) { + try { + writeUint8(writer, UpdateType.AddEntity); + writeOneEntity(writer, entity, client); + break; + } catch (e) { + resizePreserveWriter(e, writer, offset); + } + } } export function pushUpdateEntityToClient(client: IClient, update: EntityUpdateBase) { - const writer = client.updateQueue; - const offset = writer.offset; - const { entity, flags, x = 0, y = 0, vx = 0, vy = 0, options, action = 0, playerState = 0 } = update; + const writer = client.updateQueue; + const offset = writer.offset; + const { entity, flags, x = 0, y = 0, vx = 0, vy = 0, options, action = 0, playerState = 0 } = update; - while (true) { - try { - writeUint8(writer, UpdateType.UpdateEntity); - writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, action, playerState); - break; - } catch (e) { - resizePreserveWriter(e, writer, offset); - } - } + while (true) { + try { + writeUint8(writer, UpdateType.UpdateEntity); + writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, action, playerState); + break; + } catch (e) { + resizePreserveWriter(e, writer, offset); + } + } } export function pushRemoveEntityToClient(client: IClient, entity: ServerEntity) { - const writer = client.updateQueue; - const offset = writer.offset; + const writer = client.updateQueue; + const offset = writer.offset; - while (true) { - try { - writeUint8(writer, UpdateType.RemoveEntity); - writeUint32(writer, entity.id); - break; - } catch (e) { - resizePreserveWriter(e, writer, offset); - } - } + while (true) { + try { + writeUint8(writer, UpdateType.RemoveEntity); + writeUint32(writer, entity.id); + break; + } catch (e) { + resizePreserveWriter(e, writer, offset); + } + } } export function pushUpdateTileToClient(client: IClient, x: number, y: number, type: TileType) { - const writer = client.updateQueue; - const offset = writer.offset; + const writer = client.updateQueue; + const offset = writer.offset; - while (true) { - try { - writeUint8(writer, UpdateType.UpdateTile); - writeUint16(writer, x); - writeUint16(writer, y); - writeUint8(writer, type); - break; - } catch (e) { - resizePreserveWriter(e, writer, offset); - } - } + while (true) { + try { + writeUint8(writer, UpdateType.UpdateTile); + writeUint16(writer, x); + writeUint16(writer, y); + writeUint8(writer, type); + break; + } catch (e) { + resizePreserveWriter(e, writer, offset); + } + } } // other helpers export function findIntersectingEntityByBounds(map: ServerMap, entity: ServerEntity) { - const { x, y } = getRegionGlobal(map, entity.x, entity.y); - const minX = Math.max(x - 1, 0); - const minY = Math.max(y - 1, 0); - const maxX = Math.min(x + 1, map.regionsX - 1); - const maxY = Math.min(y + 1, map.regionsY - 1); + const { x, y } = getRegionGlobal(map, entity.x, entity.y); + const minX = Math.max(x - 1, 0); + const minY = Math.max(y - 1, 0); + const maxX = Math.min(x + 1, map.regionsX - 1); + const maxY = Math.min(y + 1, map.regionsY - 1); - for (let iy = minY; iy <= maxY; iy++) { - for (let ix = minX; ix <= maxX; ix++) { - const region = getRegion(map, ix, iy); + for (let iy = minY; iy <= maxY; iy++) { + for (let ix = minX; ix <= maxX; ix++) { + const region = getRegion(map, ix, iy); - for (const e of region.entities) { - if (e !== entity && !isDecal(e) && !isCritter(e) && boundsIntersect(entity.x, entity.y, entity.bounds, e.x, e.y, e.bounds)) { - return e; - } - } - } - } + for (const e of region.entities) { + if (e !== entity && !isDecal(e) && !isCritter(e) && boundsIntersect(entity.x, entity.y, entity.bounds, e.x, e.y, e.bounds)) { + return e; + } + } + } + } - return undefined; + return undefined; } export function findPlayerThatCanPickEntity(map: ServerMap, entity: ServerEntity) { - const { x, y } = getRegionGlobal(map, entity.x, entity.y); - const minX = Math.max(x - 1, 0); - const minY = Math.max(y - 1, 0); - const maxX = Math.min(x + 1, map.regionsX - 1); - const maxY = Math.min(y + 1, map.regionsY - 1); + const { x, y } = getRegionGlobal(map, entity.x, entity.y); + const minX = Math.max(x - 1, 0); + const minY = Math.max(y - 1, 0); + const maxX = Math.min(x + 1, map.regionsX - 1); + const maxY = Math.min(y + 1, map.regionsY - 1); - for (let iy = minY; iy <= maxY; iy++) { - for (let ix = minX; ix <= maxX; ix++) { - const region = getRegion(map, ix, iy); + for (let iy = minY; iy <= maxY; iy++) { + for (let ix = minX; ix <= maxX; ix++) { + const region = getRegion(map, ix, iy); - for (const e of region.entities) { - if (e.client !== undefined && entityInRange(entity, e)) { - return e; - } - } - } - } + for (const e of region.entities) { + if (e.client !== undefined && entityInRange(entity, e)) { + return e; + } + } + } + } - return undefined; + return undefined; } export function findPlayersThetCanBeSitOn(map: ServerMap, entity: ServerEntity) { - const { x, y } = getRegionGlobal(map, entity.x, entity.y); - const minX = Math.max(x - 1, 0); - const minY = Math.max(y - 1, 0); - const maxX = Math.min(x + 1, map.regionsX - 1); - const maxY = Math.min(y + 1, map.regionsY - 1); + const { x, y } = getRegionGlobal(map, entity.x, entity.y); + const minX = Math.max(x - 1, 0); + const minY = Math.max(y - 1, 0); + const maxX = Math.min(x + 1, map.regionsX - 1); + const maxY = Math.min(y + 1, map.regionsY - 1); - for (let iy = minY; iy <= maxY; iy++) { - for (let ix = minX; ix <= maxX; ix++) { - const region = getRegion(map, ix, iy); + for (let iy = minY; iy <= maxY; iy++) { + for (let ix = minX; ix <= maxX; ix++) { + const region = getRegion(map, ix, iy); - for (const e of region.entities) { - if (e !== entity && e.client !== undefined && canBeSitOn(e, entity)) { - return e; - } - } - } - } + for (const e of region.entities) { + if (e !== entity && e.client !== undefined && canBeSitOn(e, entity)) { + return e; + } + } + } + } - return undefined; + return undefined; } function canBeSitOn(entity: ServerEntity, by: ServerEntity) { - const right = hasFlag(by.state, EntityState.FacingRight); - const entityRight = hasFlag(entity.state, EntityState.FacingRight); + const right = hasFlag(by.state, EntityState.FacingRight); + const entityRight = hasFlag(entity.state, EntityState.FacingRight); - if (right !== entityRight) { - return false; - } + if (right !== entityRight) { + return false; + } - const x = by.x + (right ? -SIT_ON_BOUNDS_OFFSET : (SIT_ON_BOUNDS_OFFSET - SIT_ON_BOUNDS_WIDTH)); - const y = by.y - SIT_ON_BOUNDS_HEIGHT / 2; - const w = SIT_ON_BOUNDS_WIDTH; - const h = SIT_ON_BOUNDS_HEIGHT; - return pointInXYWH(entity.x, entity.y, x, y, w, h); + const x = by.x + (right ? -SIT_ON_BOUNDS_OFFSET : (SIT_ON_BOUNDS_OFFSET - SIT_ON_BOUNDS_WIDTH)); + const y = by.y - SIT_ON_BOUNDS_HEIGHT / 2; + const w = SIT_ON_BOUNDS_WIDTH; + const h = SIT_ON_BOUNDS_HEIGHT; + return pointInXYWH(entity.x, entity.y, x, y, w, h); } export function canPlaceItem(map: ServerMap, entity: ServerEntity) { - const tile = getTile(map, entity.x, entity.y); - return canWalk(tile) && tile !== TileType.Water && tile !== TileType.Boat && - !findIntersectingEntityByBounds(map, entity); + const tile = getTile(map, entity.x, entity.y); + return canWalk(tile) && tile !== TileType.Water && tile !== TileType.Boat && + !findIntersectingEntityByBounds(map, entity); } export function canBePickedByPlayer(map: ServerMap, entity: ServerEntity) { - return !!findPlayerThatCanPickEntity(map, entity); + return !!findPlayerThatCanPickEntity(map, entity); } export function fixPosition(entity: ServerEntity, map: ServerMap, x: number, y: number, safe: boolean) { - entity.x = clamp(x, 0, map.width); - entity.y = clamp(y, 0, map.height); - updateEntity(entity, false); + entity.x = clamp(x, 0, map.width); + entity.y = clamp(y, 0, map.height); + updateEntity(entity, false); - if (entity.client) { - entity.client.fixPosition(entity.x, entity.y, safe); - entity.client.fixingPosition = true; - } + if (entity.client) { + entity.client.fixPosition(entity.x, entity.y, safe); + entity.client.fixingPosition = true; + } } diff --git a/src/ts/server/internal.ts b/src/ts/server/internal.ts index c98977a..3741271 100644 --- a/src/ts/server/internal.ts +++ b/src/ts/server/internal.ts @@ -1,7 +1,7 @@ import * as request from 'request-promise'; import { noop, flatMap, uniq } from 'lodash'; import { - InternalGameServerState, ServerStatus, InternalLoginApi, InternalLoginServerState, InternalApi, HidingStats + InternalGameServerState, ServerStatus, InternalLoginApi, InternalLoginServerState, InternalApi, HidingStats } from '../common/adminInterfaces'; import { isMod } from '../common/accountUtils'; import { findById, flatten, delay } from '../common/utils'; @@ -18,165 +18,165 @@ import { AdminService } from './services/adminService'; // import { taskQueue } from './utils/taskQueue'; export const serverStatus: ServerStatus = { - diskSpace: '', - memoryUsage: '', - certificateExpiration: '', - lastPatreonUpdate: '', + diskSpace: '', + memoryUsage: '', + certificateExpiration: '', + lastPatreonUpdate: '', }; export const loginServers: InternalLoginServerState[] = [ - { - id: 'login', - state: { - updating: false, - dead: true, - }, - api: createApi(config.local, 'api-internal-login', config.token), - }, + { + id: 'login', + state: { + updating: false, + dead: true, + }, + api: createApi(config.local, 'api-internal-login', config.token), + }, ]; export const adminServer = config.adminLocal && !args.admin ? { - id: 'admin', - api: createApi(config.adminLocal, 'api-internal-admin', config.token), + id: 'admin', + api: createApi(config.adminLocal, 'api-internal-admin', config.token), } : undefined; export const servers: InternalGameServerState[] = []; if (args.login || args.admin) { - servers.push(...gameServers.map(s => ({ - id: s.id, - state: { - ...s, - offline: true, - dead: true, - maps: 0, - online: 0, - onMain: 0, - queued: 0, - shutdown: false, - filter: false, - settings: {}, - }, - api: createApi(s.local, 'api-internal', config.token), - }))); + servers.push(...gameServers.map(s => ({ + id: s.id, + state: { + ...s, + offline: true, + dead: true, + maps: 0, + online: 0, + onMain: 0, + queued: 0, + shutdown: false, + filter: false, + settings: {}, + }, + api: createApi(s.local, 'api-internal', config.token), + }))); } export function findServer(id: string) { - return findById(servers, id); + return findById(servers, id); } export function getLoginServer(_id: string) { - return loginServers[0]; + return loginServers[0]; } export function getServer(id: string) { - const server = findServer(id); + const server = findServer(id); - if (!server) { - throw new Error(`Invalid server ID (${id})`); - } + if (!server) { + throw new Error(`Invalid server ID (${id})`); + } - return server; + return server; } export function createApi(host: string, url: string, apiToken: string): T { - return new Proxy({} as any, { - get: (_, key) => - (...args: any[]) => - Promise.resolve(request(`http://${host}/${url}/api`, { - json: true, - headers: { 'api-token': apiToken }, - method: 'post', - body: { method: key, args }, - })), - }); + return new Proxy({} as any, { + get: (_, key) => + (...args: any[]) => + Promise.resolve(request(`http://${host}/${url}/api`, { + json: true, + headers: { 'api-token': apiToken }, + method: 'post', + body: { method: key, args }, + })), + }); } function mapGameServers(action: (server: InternalGameServerState) => Promise | T) { - return Promise.all(servers.filter(s => !s.state.dead).map(action)); + return Promise.all(servers.filter(s => !s.state.dead).map(action)); } export function createJoin(): typeof join { - return join; + return join; } async function join(joinServer: InternalGameServerState, account: IAccount, character: ICharacter): Promise { - try { - const kicked = await mapGameServers(s => { - if (isMod(account) && s !== joinServer) { - return false; - } else { - return s.api.kick(account._id.toString(), undefined).catch(e => (logger.error(e), false)); - } - }); + try { + const kicked = await mapGameServers(s => { + if (isMod(account) && s !== joinServer) { + return false; + } else { + return s.api.kick(account._id.toString(), undefined).catch(e => (logger.error(e), false)); + } + }); - if (kicked.some(x => x)) { - await delay(2000); - } + if (kicked.some(x => x)) { + await delay(2000); + } - return await joinServer.api.join(account._id.toString(), character._id.toString()); - } catch (error) { - if (error.error && error.error.userError) { - throw new UserError(error.error.error); - } else { - logger.error(error); - throw new Error('Internal error'); - } - } + return await joinServer.api.join(account._id.toString(), character._id.toString()); + } catch (error) { + if (error.error && error.error.userError) { + throw new UserError(error.error.error); + } else { + logger.error(error); + throw new Error('Internal error'); + } + } } let accountChangedHandler = (_accountId: string) => Promise.resolve(); export function init(world: World, tokens: TokenService) { - accountChangedHandler = createAccountChanged(world, tokens, findAccountSafe); + accountChangedHandler = createAccountChanged(world, tokens, findAccountSafe); } export async function accountChanged(accountId: string) { - if (args.login || args.admin) { - await mapGameServers(s => { - s.api.accountChanged(accountId).catch(noop); - }); - } else { - await accountChangedHandler(accountId); - } + if (args.login || args.admin) { + await mapGameServers(s => { + s.api.accountChanged(accountId).catch(noop); + }); + } else { + await accountChangedHandler(accountId); + } } export async function accountMerged(accountId: string, mergedId: string) { - await mapGameServers(s => { s.api.accountMerged(accountId, mergedId).catch(noop); }); + await mapGameServers(s => { s.api.accountMerged(accountId, mergedId).catch(noop); }); } export async function accountStatus(accountId: string) { - const statuses = await mapGameServers(s => s.api.accountStatus(accountId).catch(() => ({ online: false }))); - return statuses.filter(s => !!s.online); + const statuses = await mapGameServers(s => s.api.accountStatus(accountId).catch(() => ({ online: false }))); + return statuses.filter(s => !!s.online); } export async function accountAround(accountId: string) { - const users = await mapGameServers(s => s.api.accountAround(accountId).catch(() => [])); - return flatten(users).sort((a, b) => a.distance - b.distance).slice(0, 10); + const users = await mapGameServers(s => s.api.accountAround(accountId).catch(() => [])); + return flatten(users).sort((a, b) => a.distance - b.distance).slice(0, 10); } export async function accountHidden(accountId: string): Promise { - const [users, permaHidden, permaHiddenBy] = await Promise.all([ - mapGameServers(s => s.api.accountHidden(accountId).catch(() => ({ account: '', hidden: [], hiddenBy: [] }))), - findHideIds(accountId), - findHideIdsRev(accountId), - ]); + const [users, permaHidden, permaHiddenBy] = await Promise.all([ + mapGameServers(s => s.api.accountHidden(accountId).catch(() => ({ account: '', hidden: [], hiddenBy: [] }))), + findHideIds(accountId), + findHideIdsRev(accountId), + ]); - return { - account: accountId, - hidden: uniq(flatMap(users, u => u.hidden)), - hiddenBy: uniq(flatMap(users, u => u.hiddenBy)), - permaHidden, - permaHiddenBy, - }; + return { + account: accountId, + hidden: uniq(flatMap(users, u => u.hidden)), + hiddenBy: uniq(flatMap(users, u => u.hiddenBy)), + permaHidden, + permaHiddenBy, + }; } export type RemovedDocument = ReturnType; export const createRemovedDocument = - (endPoints: EndPoints | undefined, adminService: AdminService | undefined) => - (model: 'events' | 'ponies' | 'accounts' | 'auths' | 'origins', id: string) => { - endPoints && model in endPoints && (endPoints as any)[model].removedItem(id); - adminService && adminService.removedItem(model, id); - return adminServer ? adminServer.api.removedDocument(model, id).catch(noop) : Promise.resolve(); - }; + (endPoints: EndPoints | undefined, adminService: AdminService | undefined) => + (model: 'events' | 'ponies' | 'accounts' | 'auths' | 'origins', id: string) => { + endPoints && model in endPoints && (endPoints as any)[model].removedItem(id); + adminService && adminService.removedItem(model, id); + return adminServer ? adminServer.api.removedDocument(model, id).catch(noop) : Promise.resolve(); + }; diff --git a/src/ts/server/ipc.ts b/src/ts/server/ipc.ts index defa8ed..60da76f 100644 --- a/src/ts/server/ipc.ts +++ b/src/ts/server/ipc.ts @@ -1,98 +1,98 @@ import * as ipc from 'node-ipc'; export interface LoginServer { - hello(message: string): Promise; + hello(message: string): Promise; } export interface GameServer { - something(): Promise; + something(): Promise; } interface SocketState { - server: TServer; - client: TClient; + server: TServer; + client: TClient; } export function startIPCServer( - id: string, createServer: (client: TClient) => TServer + id: string, createServer: (client: TClient) => TServer ) { - ipc.config.id = id; - ipc.config.retry = 500; - ipc.config.silent = true; - ipc.serve(() => { - const sockets = new Map>(); + ipc.config.id = id; + ipc.config.retry = 500; + ipc.config.silent = true; + ipc.serve(() => { + const sockets = new Map>(); - ipc.server.on('connect', (socket) => { - console.log('server:connect'); - const client: TClient = new Proxy({} as any, { - get: (_, key) => (...args: any[]) => { - ipc.server.emit(socket, 'message', [key, args]); - }, - }); - const server = createServer(client); - sockets.set(socket, { server, client }); - }); + ipc.server.on('connect', (socket) => { + console.log('server:connect'); + const client: TClient = new Proxy({} as any, { + get: (_, key) => (...args: any[]) => { + ipc.server.emit(socket, 'message', [key, args]); + }, + }); + const server = createServer(client); + sockets.set(socket, { server, client }); + }); - ipc.server.on('message', (data, socket) => { - console.log('server:message', data); - const socketState = sockets.get(socket); + ipc.server.on('message', (data, socket) => { + console.log('server:message', data); + const socketState = sockets.get(socket); - if (socketState) { - (socketState.server as any)[data[0]](...data[1]); - } else { - console.error('missing server for socket'); - } - }); + if (socketState) { + (socketState.server as any)[data[0]](...data[1]); + } else { + console.error('missing server for socket'); + } + }); - ipc.server.on('error', (error) => { - console.log('server:error', error); - }); + ipc.server.on('error', (error) => { + console.log('server:error', error); + }); - ipc.server.on('disconnect', (socket) => { - console.log('server:disconnect'); - sockets.delete(socket); - }); + ipc.server.on('disconnect', (socket) => { + console.log('server:disconnect'); + sockets.delete(socket); + }); - ipc.server.on('socket.disconnected', (socket, _destroyedSocketID) => { - console.log('server:socket.disconnected'); - sockets.delete(socket); - }); - }); + ipc.server.on('socket.disconnected', (socket, _destroyedSocketID) => { + console.log('server:socket.disconnected'); + sockets.delete(socket); + }); + }); - ipc.server.start(); + ipc.server.start(); } export function startIPCClient( - serverId: string, clientId: string, createClient: (server: TServer) => TClient + serverId: string, clientId: string, createClient: (server: TServer) => TClient ) { - ipc.config.id = clientId; - ipc.config.retry = 500; - ipc.config.silent = true; - ipc.connectTo(serverId, () => { - let connected = false; - const socket = ipc.of[serverId]; - const server: TServer = new Proxy({} as any, { - get: (_, key) => (...args: any[]) => { - socket.emit('message', [key, args]); - }, - }); - const client = createClient(server); + ipc.config.id = clientId; + ipc.config.retry = 500; + ipc.config.silent = true; + ipc.connectTo(serverId, () => { + let connected = false; + const socket = ipc.of[serverId]; + const server: TServer = new Proxy({} as any, { + get: (_, key) => (...args: any[]) => { + socket.emit('message', [key, args]); + }, + }); + const client = createClient(server); - socket.on('connect', () => { - connected = true; - console.log('client:connect'); - }); + socket.on('connect', () => { + connected = true; + console.log('client:connect'); + }); - socket.on('disconnect', () => { - if (connected) { - connected = false; - console.log('client:disconnect'); - } - }); + socket.on('disconnect', () => { + if (connected) { + connected = false; + console.log('client:disconnect'); + } + }); - socket.on('message', (data: any) => { - console.log('client:message', data); - (client as any)[data[0]](...data[1]); - }); - }); + socket.on('message', (data: any) => { + console.log('client:message', data); + (client as any)[data[0]](...data[1]); + }); + }); } diff --git a/src/ts/server/liveEndPoint.ts b/src/ts/server/liveEndPoint.ts index a605eb7..53d4a59 100644 --- a/src/ts/server/liveEndPoint.ts +++ b/src/ts/server/liveEndPoint.ts @@ -8,163 +8,163 @@ import { Doc } from './db'; import { logger } from './logger'; export interface LiveEndPoint { - get(id: string): Promise; - getAll(timestamp?: string): Promise; - assignAccount(id: string, account: string): Promise; - removeItem(id: string): Promise; - removedItem(id: string): void; - encodeItems(items: any[], timestamp: Date, more: boolean): LiveResponse; - destroy(): void; + get(id: string): Promise; + getAll(timestamp?: string): Promise; + assignAccount(id: string, account: string): Promise; + removeItem(id: string): Promise; + removedItem(id: string): void; + encodeItems(items: any[], timestamp: Date, more: boolean): LiveResponse; + destroy(): void; } interface DeletedId { - updatedAt: Date; - id: string; + updatedAt: Date; + id: string; } interface LiveEndPointConfig { - model: Model; - fields: string[]; - fix?: boolean; - encode: (items: T[], base: BaseValues) => any[][]; - beforeDelete?: (item: T) => any; - afterDelete?: (item: T) => any; - beforeAssign?: (item: T, accountId: string) => any; - afterAssign?: (from: string, to: string) => any; + model: Model; + fields: string[]; + fix?: boolean; + encode: (items: T[], base: BaseValues) => any[][]; + beforeDelete?: (item: T) => any; + afterDelete?: (item: T) => any; + beforeAssign?: (item: T, accountId: string) => any; + afterAssign?: (from: string, to: string) => any; } export function createLiveEndPoint( - { model, fields, encode, beforeDelete, afterDelete, beforeAssign, afterAssign, fix = false }: LiveEndPointConfig + { model, fields, encode, beforeDelete, afterDelete, beforeAssign, afterAssign, fix = false }: LiveEndPointConfig ): LiveEndPoint { - const removedItems: DeletedId[] = []; - let fixing = false; + const removedItems: DeletedId[] = []; + let fixing = false; - function removedItem(id: string) { - removedItems.push({ id, updatedAt: new Date() }); - } + function removedItem(id: string) { + removedItems.push({ id, updatedAt: new Date() }); + } - function removeItem(id: string) { - return Promise.resolve(model.findById(id).exec()) - .tap(item => item && beforeDelete && beforeDelete(item)) - .tap(item => { - if (item) { - removedItem(item._id.toString()); - return item.remove() as any; - } - }) - .tap(item => item && afterDelete && afterDelete(item)) - .then(noop); - } + function removeItem(id: string) { + return Promise.resolve(model.findById(id).exec()) + .tap(item => item && beforeDelete && beforeDelete(item)) + .tap(item => { + if (item) { + removedItem(item._id.toString()); + return item.remove() as any; + } + }) + .tap(item => item && afterDelete && afterDelete(item)) + .then(noop); + } - function assignAccount(id: string, account: string) { - return Promise.resolve() - .then(() => model.findById(id, 'account').lean().exec()) - .tap((item: any) => item && beforeAssign && beforeAssign(item, account)) - .tap(() => model.findByIdAndUpdate(id, { account }).exec()) - .tap((item: any) => item && afterAssign && afterAssign(item.account, account)) - .then(noop); - } + function assignAccount(id: string, account: string) { + return Promise.resolve() + .then(() => model.findById(id, 'account').lean().exec()) + .tap((item: any) => item && beforeAssign && beforeAssign(item, account)) + .tap(() => model.findByIdAndUpdate(id, { account }).exec()) + .tap((item: any) => item && afterAssign && afterAssign(item.account, account)) + .then(noop); + } - function encodeItems(items: T[], timestamp: Date, more: boolean): LiveResponse { - const base: BaseValues = {}; - const updates = encode(items, base); - const deletes = removedItems - .filter(x => x.updatedAt.getTime() > timestamp.getTime()) - .map(x => x.id); + function encodeItems(items: T[], timestamp: Date, more: boolean): LiveResponse { + const base: BaseValues = {}; + const updates = encode(items, base); + const deletes = removedItems + .filter(x => x.updatedAt.getTime() > timestamp.getTime()) + .map(x => x.id); - return { updates, deletes, base, more }; - } + return { updates, deletes, base, more }; + } - function findItems(from: Date): Promise { - return Promise.resolve(model.find({ updatedAt: { $gt: from } }, fields.join(' ')) - // .sort([['updatedAt', 1], ['id', 1]]) - .sort({ updatedAt: 1 }) - .limit(ITEM_LIMIT + 1) - .lean() - .exec()); - } + function findItems(from: Date): Promise { + return Promise.resolve(model.find({ updatedAt: { $gt: from } }, fields.join(' ')) + // .sort([['updatedAt', 1], ['id', 1]]) + .sort({ updatedAt: 1 }) + .limit(ITEM_LIMIT + 1) + .lean() + .exec()); + } - function findItemsExact(date: Date): Promise { - return Promise.resolve(model.find({ updatedAt: date }, fields.join(' ')) - .lean() - .exec()); - } + function findItemsExact(date: Date): Promise { + return Promise.resolve(model.find({ updatedAt: date }, fields.join(' ')) + .lean() + .exec()); + } - function hasItem(items: T[], id: string) { - return items.some(i => i._id === id); - } + function hasItem(items: T[], id: string) { + return items.some(i => i._id === id); + } - function addTailItems(items: T[]): Promise<{ items: T[]; more: boolean; }> { - if (items.length <= ITEM_LIMIT) { - return Promise.resolve({ items, more: false }); - } + function addTailItems(items: T[]): Promise<{ items: T[]; more: boolean; }> { + if (items.length <= ITEM_LIMIT) { + return Promise.resolve({ items, more: false }); + } - const a = items[items.length - 1]; - const b = items[items.length - 2]; + const a = items[items.length - 1]; + const b = items[items.length - 2]; - if (a.updatedAt.getTime() !== b.updatedAt.getTime()) { - items.pop(); - return Promise.resolve({ items, more: true }); - } + if (a.updatedAt.getTime() !== b.updatedAt.getTime()) { + items.pop(); + return Promise.resolve({ items, more: true }); + } - return findItemsExact(items[items.length - 1].updatedAt) - .then(other => other.filter(i => !hasItem(items, i._id))) - .then(other => [...items, ...other]) - .then(items => ({ items, more: true })); - } + return findItemsExact(items[items.length - 1].updatedAt) + .then(other => other.filter(i => !hasItem(items, i._id))) + .then(other => [...items, ...other]) + .then(items => ({ items, more: true })); + } - function getAll(timestamp?: string): Promise { - const from = timestamp ? new Date(timestamp) : new Date(0); + function getAll(timestamp?: string): Promise { + const from = timestamp ? new Date(timestamp) : new Date(0); - return findItems(from) - .then(addTailItems) - .tap(({ items }) => { - try { - if (items.length > ITEM_LIMIT * 2) { - fixItems(items); - logger.warn(`Fetching ${items.length} ${model.modelName}s [${items[ITEM_LIMIT + 1].updatedAt.toISOString()}]`); - } - } catch (e) { - logger.error(e); - } - }) - .then(({ items, more }) => encodeItems(items, from, more)); - } + return findItems(from) + .then(addTailItems) + .tap(({ items }) => { + try { + if (items.length > ITEM_LIMIT * 2) { + fixItems(items); + logger.warn(`Fetching ${items.length} ${model.modelName}s [${items[ITEM_LIMIT + 1].updatedAt.toISOString()}]`); + } + } catch (e) { + logger.error(e); + } + }) + .then(({ items, more }) => encodeItems(items, from, more)); + } - function fixItems(items: T[]) { - if (fixing || !fix) - return; + function fixItems(items: T[]) { + if (fixing || !fix) + return; - fixing = true; - logger.info(`Fixing ${model.modelName}s`); + fixing = true; + logger.info(`Fixing ${model.modelName}s`); - Promise.map(items, item => model.updateOne({ _id: item._id }, { unused: Date.now() % 1000 }).exec(), { concurrency: 1 }) - .then(() => logger.info(`Fixed ${model.modelName}s`)) - .catch(e => logger.error(e)) - .finally(() => fixing = false) - .done(); - } + Promise.map(items, item => model.updateOne({ _id: item._id }, { unused: Date.now() % 1000 }).exec(), { concurrency: 1 }) + .then(() => logger.info(`Fixed ${model.modelName}s`)) + .catch(e => logger.error(e)) + .finally(() => fixing = false) + .done(); + } - function get(id: string) { - return Promise.resolve(model.findById(id).lean().exec()); - } + function get(id: string) { + return Promise.resolve(model.findById(id).lean().exec()); + } - const interval = setInterval(() => { - const date = fromNow(-10 * MINUTE); - remove(removedItems, x => x.updatedAt.getTime() < date.getTime()); - }, 1 * MINUTE); + const interval = setInterval(() => { + const date = fromNow(-10 * MINUTE); + remove(removedItems, x => x.updatedAt.getTime() < date.getTime()); + }, 1 * MINUTE); - function destroy() { - clearInterval(interval); - } + function destroy() { + clearInterval(interval); + } - return { - get, - getAll, - assignAccount, - removeItem, - removedItem, - encodeItems, - destroy, - }; + return { + get, + getAll, + assignAccount, + removeItem, + removedItem, + encodeItems, + destroy, + }; } diff --git a/src/ts/server/liveSettings.ts b/src/ts/server/liveSettings.ts index 93e4e08..732a133 100644 --- a/src/ts/server/liveSettings.ts +++ b/src/ts/server/liveSettings.ts @@ -1,6 +1,6 @@ import { ServerLiveSettings } from '../common/adminInterfaces'; export const liveSettings: ServerLiveSettings = { - updating: false, - shutdown: false, + updating: false, + shutdown: false, }; diff --git a/src/ts/server/logger.ts b/src/ts/server/logger.ts index 2182d3c..3d3b187 100644 --- a/src/ts/server/logger.ts +++ b/src/ts/server/logger.ts @@ -11,95 +11,95 @@ import { ID } from './db'; const { reset, gray, magenta, cyan, green, yellow, red } = chalk; function format(color: (text: string) => string) { - //'[{{timestamp}}] [{{title}}] {{message}} ({{file}}:{{line}})', - return [ - reset('['), - gray('{{timestamp}}'), - reset('] ['), - color('{{title}}'), - reset('] {{message}} '), - gray('({{file}}:{{line}})'), - ].join(''); + //'[{{timestamp}}] [{{title}}] {{message}} ({{file}}:{{line}})', + return [ + reset('['), + gray('{{timestamp}}'), + reset('] ['), + color('{{title}}'), + reset('] {{message}} '), + gray('({{file}}:{{line}})'), + ].join(''); } export const logger = console({ - level: 0, - dateformat: 'mmm dd HH:MM:ss', - format: [ - format(reset), - { - trace: format(cyan), - debug: format(magenta), - info: format(green), - warn: format(yellow), - error: format(red), - } - ], + level: 0, + dateformat: 'mmm dd HH:MM:ss', + format: [ + format(reset), + { + trace: format(cyan), + debug: format(magenta), + info: format(green), + warn: format(yellow), + error: format(red), + } + ], } as any); const daily = dailyfile({ - root: pathTo('logs'), - maxLogFiles: 14, - dateformat: 'HH:MM:ss', - format: '{{timestamp}} {{message}}', // ({{file}}:{{line}}) + root: pathTo('logs'), + maxLogFiles: 14, + dateformat: 'HH:MM:ss', + format: '{{timestamp}} {{message}}', // ({{file}}:{{line}}) } as any); export function log(message: string) { - daily.info(message); + daily.info(message); } export function formatMessage(accountId: ID, type: string, message: string) { - return `[${accountId}]${type}\t${message}`; + return `[${accountId}]${type}\t${message}`; } export function systemMessage(accountId: ID, message: string) { - return formatMessage(accountId, '[system]', message); + return formatMessage(accountId, '[system]', message); } function adminMessage(accountId: ID, message: string) { - return formatMessage(accountId, '[admin]', message); + return formatMessage(accountId, '[admin]', message); } export function system(accountId: ID, message: string) { - log(systemMessage(accountId, message)); + log(systemMessage(accountId, message)); } export function admin(accountId: ID, message: string) { - log(adminMessage(accountId, message)); + log(adminMessage(accountId, message)); } export function logPatreon(message: string) { - log(formatMessage('patreon', '', message)); + log(formatMessage('patreon', '', message)); } export function logServer(message: string) { - log(formatMessage('server', '', message)); + log(formatMessage('server', '', message)); } export function logPerformance(message: string) { - log(formatMessage('performance', '', message)); + log(formatMessage('performance', '', message)); } export function chat( - server: ServerConfig, client: IClient, text: string, type: ChatType, ignored: boolean, target: IClient | undefined + server: ServerConfig, client: IClient, text: string, type: ChatType, ignored: boolean, target: IClient | undefined ) { - let prefix = getChatPrefix(type); - let mod = ''; + let prefix = getChatPrefix(type); + let mod = ''; - if (ignored) { - mod = '[ignored]'; - } else if (isMutedOrShadowed(client)) { - mod = '[muted]'; - } else if (client.accountSettings.ignorePublicChat && isPublicChat(type)) { - mod = '[ignorepub]'; - } + if (ignored) { + mod = '[ignored]'; + } else if (isMutedOrShadowed(client)) { + mod = '[muted]'; + } else if (client.accountSettings.ignorePublicChat && isPublicChat(type)) { + mod = '[ignorepub]'; + } - if (type === ChatType.Whisper) { - prefix += `[${target ? `${target.accountId}${target.shadowed ? '][shadowed' : ''}` : 'undefined'}] `; - } + if (type === ChatType.Whisper) { + prefix += `[${target ? `${target.accountId}${target.shadowed ? '][shadowed' : ''}` : 'undefined'}] `; + } - const message = formatMessage( - client.accountId, `[${server.id}][${client.map.id || 'main'}][${client.characterName}]${mod}`, `${prefix}${text}`); + const message = formatMessage( + client.accountId, `[${server.id}][${client.map.id || 'main'}][${client.characterName}]${mod}`, `${prefix}${text}`); - log(message); + log(message); } diff --git a/src/ts/server/mapUtils.ts b/src/ts/server/mapUtils.ts index 63383c9..642870e 100644 --- a/src/ts/server/mapUtils.ts +++ b/src/ts/server/mapUtils.ts @@ -19,276 +19,276 @@ import { randomPosition } from './controllers/collectableController'; import { BunnyAnimation } from '../common/entities'; export const worldForTemplates: any = { - featureFlags: {}, - addEntity(entity: ServerEntity, map: ServerMap) { - roundPosition(entity); - const region = getRegionGlobal(map, entity.x, entity.y); - entity.region = region; - addEntityToRegion(region, entity, map); - return entity; - }, - removeEntity(entity: ServerEntity, map: ServerMap) { - let removed = false; + featureFlags: {}, + addEntity(entity: ServerEntity, map: ServerMap) { + roundPosition(entity); + const region = getRegionGlobal(map, entity.x, entity.y); + entity.region = region; + addEntityToRegion(region, entity, map); + return entity; + }, + removeEntity(entity: ServerEntity, map: ServerMap) { + let removed = false; - if (entity.region) { - removed = removeEntityFromRegion(entity.region, entity, map); - } + if (entity.region) { + removed = removeEntityFromRegion(entity.region, entity, map); + } - return removed; - }, + return removed; + }, }; export function addSpawnPointIndicators(world: World, map: ServerMap) { - const addSpawn = ({ x, y, w, h }: Rect) => { - world.addEntity(entities.spawnPole(x, y), map); + const addSpawn = ({ x, y, w, h }: Rect) => { + world.addEntity(entities.spawnPole(x, y), map); - if (w && h) { - world.addEntity(entities.spawnPole(x + w, y), map); - world.addEntity(entities.spawnPole(x, y + h), map); - world.addEntity(entities.spawnPole(x + w, y + h), map); - } - }; + if (w && h) { + world.addEntity(entities.spawnPole(x + w, y), map); + world.addEntity(entities.spawnPole(x, y + h), map); + world.addEntity(entities.spawnPole(x + w, y + h), map); + } + }; - addSpawn(map.spawnArea); + addSpawn(map.spawnArea); - for (const spawn of Array.from(map.spawns.values())) { - addSpawn(spawn); - } + for (const spawn of Array.from(map.spawns.values())) { + addSpawn(spawn); + } } export function generateTileIndicesAndColliders(map: ServerMap) { - for (const region of map.regions) { - getRegionTiles(region); // initialize encodedTiles + for (const region of map.regions) { + getRegionTiles(region); // initialize encodedTiles - if (region.tilesDirty) { - updateTileIndices(region, map); - } - } + if (region.tilesDirty) { + updateTileIndices(region, map); + } + } - for (const region of map.regions) { - if (region.colliderDirty) { - generateRegionCollider(region, map); - } - } + for (const region of map.regions) { + if (region.colliderDirty) { + generateRegionCollider(region, map); + } + } } export function removePonies(entities: ServerEntity[]) { - for (let i = entities.length - 1; i >= 0; i--) { - if (entities[i].type === PONY_TYPE) { - entities.splice(i, 1); - } - } + for (let i = entities.length - 1; i >= 0; i--) { + if (entities[i].type === PONY_TYPE) { + entities.splice(i, 1); + } + } } export interface SignDirection { - icon: number; - name: string; + icon: number; + name: string; } export interface SignConfig { - r?: number; - w?: (SignDirection | undefined)[]; - e?: (SignDirection | undefined)[]; - n?: (SignDirection | undefined)[]; - s?: (SignDirection | undefined)[]; + r?: number; + w?: (SignDirection | undefined)[]; + e?: (SignDirection | undefined)[]; + n?: (SignDirection | undefined)[]; + s?: (SignDirection | undefined)[]; } export function createDirectionSign(x: number, y: number, config: SignConfig) { - const result: ServerEntity[] = []; - const options: SignEntityOptions = { sign: {} }; - const lines: string[] = []; - const { w = [], e = [], s = [], n = [] } = config; - const max = clamp(Math.max(w.length, e.length, s.length, n.length), 3, 5); - const skip = 5 - max; + const result: ServerEntity[] = []; + const options: SignEntityOptions = { sign: {} }; + const lines: string[] = []; + const { w = [], e = [], s = [], n = [] } = config; + const max = clamp(Math.max(w.length, e.length, s.length, n.length), 3, 5); + const skip = 5 - max; - function parse(entries: (SignDirection | undefined)[], arrow: string, plates: CreateEntityMethod[], ox: number) { - for (let i = 0; i < entries.length; i++) { - const e = entries[i]; + function parse(entries: (SignDirection | undefined)[], arrow: string, plates: CreateEntityMethod[], ox: number) { + for (let i = 0; i < entries.length; i++) { + const e = entries[i]; - if (e) { - lines.push(`${arrow} ${e.name}`); - const nameplate = plates[i](x + ox / tileWidth, y); - setEntityName(nameplate, e.name); - result.push(nameplate); - } - } - } + if (e) { + lines.push(`${arrow} ${e.name}`); + const nameplate = plates[i](x + ox / tileWidth, y); + setEntityName(nameplate, e.name); + result.push(nameplate); + } + } + } - if (config.r) { - options.sign.r = config.r; - } + if (config.r) { + options.sign.r = config.r; + } - const ups = config.r ? entities.directionSignUpsRight : entities.directionSignUpsLeft; - const downs = config.r ? entities.directionSignDownsLeft : entities.directionSignDownsRight; + const ups = config.r ? entities.directionSignUpsRight : entities.directionSignUpsLeft; + const downs = config.r ? entities.directionSignDownsLeft : entities.directionSignDownsRight; - if (config.n) { - options.sign.n = config.n.map(x => x ? x.icon : -1); - parse(config.n, '↑', ups.slice(skip), 0); - } + if (config.n) { + options.sign.n = config.n.map(x => x ? x.icon : -1); + parse(config.n, '↑', ups.slice(skip), 0); + } - if (config.w) { - options.sign.w = config.w.map(x => x ? x.icon : -1); - parse(config.w, '←', entities.directionSignLefts.slice(skip), -10); - } + if (config.w) { + options.sign.w = config.w.map(x => x ? x.icon : -1); + parse(config.w, '←', entities.directionSignLefts.slice(skip), -10); + } - if (config.e) { - options.sign.e = config.e.map(x => x ? x.icon : -1); - parse(config.e, '→', entities.directionSignRights.slice(skip), 10); - } + if (config.e) { + options.sign.e = config.e.map(x => x ? x.icon : -1); + parse(config.e, '→', entities.directionSignRights.slice(skip), 10); + } - if (config.s) { - options.sign.s = config.s.map(x => x ? x.icon : -1); - parse(config.s, '↓', downs.slice(skip), 0); - } + if (config.s) { + options.sign.s = config.s.map(x => x ? x.icon : -1); + parse(config.s, '↓', downs.slice(skip), 0); + } - const text = lines.join('\n'); - const entity = entities.directionSign(x, y, options) as ServerEntity; - entity.interact = (entity, client) => sayTo(client, entity, text, MessageType.System); - result.push(entity); - return result; + const text = lines.join('\n'); + const entity = entities.directionSign(x, y, options) as ServerEntity; + entity.interact = (entity, client) => sayTo(client, entity, text, MessageType.System); + result.push(entity); + return result; } const patchTypes = [ - entities.cloverPatch3, entities.cloverPatch4, entities.cloverPatch5, entities.cloverPatch6, entities.cloverPatch7 + entities.cloverPatch3, entities.cloverPatch4, entities.cloverPatch5, entities.cloverPatch6, entities.cloverPatch7 ].map(x => x.type); const eggBasketTypes = entities.eggBaskets.map(b => b.type); export function pickCandy(client: IClient) { - let count = 0; - updateAccountState(client.account, state => state.candies = count = toInt(state.candies) + 1); - saySystem(client, `${count} 🍬`); + let count = 0; + updateAccountState(client.account, state => state.candies = count = toInt(state.candies) + 1); + saySystem(client, `${count} 🍬`); } export function pickGift(client: IClient) { - let count = 0; - updateAccountState(client.account, state => state.gifts = count = toInt(state.gifts) + 1); - saySystem(client, `${count} 🎁`); - holdItem(client.pony, entities.gift2.type); + let count = 0; + updateAccountState(client.account, state => state.gifts = count = toInt(state.gifts) + 1); + saySystem(client, `${count} 🎁`); + holdItem(client.pony, entities.gift2.type); } export function pickClover(client: IClient) { - let count = 0; - updateAccountState(client.account, state => state.clovers = count = toInt(state.clovers) + 1); - saySystem(client, `${count} 🍀`); - holdItem(client.pony, entities.cloverPick.type); + let count = 0; + updateAccountState(client.account, state => state.clovers = count = toInt(state.clovers) + 1); + saySystem(client, `${count} 🍀`); + holdItem(client.pony, entities.cloverPick.type); } export function pickEgg(client: IClient) { - let count = 0; - updateAccountState(client.account, state => state.eggs = count = toInt(state.eggs) + 1); - saySystem(client, `${count} 🥚`); + let count = 0; + updateAccountState(client.account, state => state.eggs = count = toInt(state.eggs) + 1); + saySystem(client, `${count} 🥚`); - if (Math.random() < 0.05) { - const options = client.pony.options as PonyOptions; - const basketIndex = eggBasketTypes.indexOf(options.hold || 0); + if (Math.random() < 0.05) { + const options = client.pony.options as PonyOptions; + const basketIndex = eggBasketTypes.indexOf(options.hold || 0); - if (basketIndex >= 0 && basketIndex < (eggBasketTypes.length - 1)) { - holdItem(client.pony, eggBasketTypes[basketIndex + 1]); - } - } + if (basketIndex >= 0 && basketIndex < (eggBasketTypes.length - 1)) { + holdItem(client.pony, eggBasketTypes[basketIndex + 1]); + } + } } export function pickEntity(client: IClient, entity: ServerEntity) { - holdItem(client.pony, entity.type); + holdItem(client.pony, entity.type); } export function checkLantern(client: IClient) { - const options = client.pony.options as PonyOptions; - const canPick = options.hold === entities.jackoLanternOn.type || options.hold === entities.jackoLanternOff.type; + const options = client.pony.options as PonyOptions; + const canPick = options.hold === entities.jackoLanternOn.type || options.hold === entities.jackoLanternOff.type; - if (!canPick) { - saySystem(client, 'Get a lantern to collect candies'); - } + if (!canPick) { + saySystem(client, 'Get a lantern to collect candies'); + } - return canPick; + return canPick; } export function checkBasket(client: IClient) { - const options = client.pony.options as PonyOptions; - const canPick = includes(eggBasketTypes, options.hold); + const options = client.pony.options as PonyOptions; + const canPick = includes(eggBasketTypes, options.hold); - if (!canPick) { - saySystem(client, 'Get a basket to collect eggs'); - } + if (!canPick) { + saySystem(client, 'Get a basket to collect eggs'); + } - return canPick; + return canPick; } export function checkNotCollecting(client: IClient) { - const options = client.pony.options as PonyOptions; - const canPick = includes(eggBasketTypes, options.hold) || - options.hold === entities.jackoLanternOn.type || - options.hold === entities.jackoLanternOff.type; - return !canPick; + const options = client.pony.options as PonyOptions; + const canPick = includes(eggBasketTypes, options.hold) || + options.hold === entities.jackoLanternOn.type || + options.hold === entities.jackoLanternOff.type; + return !canPick; } export function positionClover(map: ServerMap) { - const patch = sample(findEntities(map, e => includes(patchTypes, e.type))); + const patch = sample(findEntities(map, e => includes(patchTypes, e.type))); - if (patch && patch.bounds) { - const bounds = patch.bounds; - const position = { - x: patch.x + bounds.x / tileWidth + random(0, bounds.w / tileWidth, true), - y: patch.y + bounds.y / tileHeight + random(0, bounds.h / tileHeight, true), - }; - return position; - } else { - return randomPosition(map); - } + if (patch && patch.bounds) { + const bounds = patch.bounds; + const position = { + x: patch.x + bounds.x / tileWidth + random(0, bounds.w / tileWidth, true), + y: patch.y + bounds.y / tileHeight + random(0, bounds.h / tileHeight, true), + }; + return position; + } else { + return randomPosition(map); + } } export function createBunny(waypoints: Point[]) { - const { x, y } = waypoints[0]; - const entity = entities.bunny(x, y) as ServerEntity; - const bunnySpeed = 2; + const { x, y } = waypoints[0]; + const entity = entities.bunny(x, y) as ServerEntity; + const bunnySpeed = 2; - let waypoint = 0; - let sleepUntil = 0; + let waypoint = 0; + let sleepUntil = 0; - entity.serverUpdate = (_delta, now) => { - if (sleepUntil > now) - return; + entity.serverUpdate = (_delta, now) => { + if (sleepUntil > now) + return; - const { x, y } = waypoints[waypoint]; - const reachedX = Math.abs(entity.x - x) < 0.2; - const reachedY = Math.abs(entity.y - y) < 0.2; + const { x, y } = waypoints[waypoint]; + const reachedX = Math.abs(entity.x - x) < 0.2; + const reachedY = Math.abs(entity.y - y) < 0.2; - if (reachedX && reachedY) { - const rand = Math.random(); - updateEntityVelocity(entity, 0, 0, now); + if (reachedX && reachedY) { + const rand = Math.random(); + updateEntityVelocity(entity, 0, 0, now); - if (rand < 0.1) { - setEntityAnimation(entity, BunnyAnimation.Clean); - sleepUntil = now + 2; - } else if (rand < 0.2) { - setEntityAnimation(entity, BunnyAnimation.Look); - sleepUntil = now + 2; - } else if (rand < 0.3) { - setEntityAnimation(entity, BunnyAnimation.Blink); - sleepUntil = now + 2; - } else if (rand < 0.6) { - setEntityAnimation(entity, BunnyAnimation.Sit); - sleepUntil = now + 2; - } else { - waypoint = (waypoint + 1) % waypoints.length; - setEntityAnimation(entity, BunnyAnimation.Sit); - sleepUntil = now + random(0.2, 2, true); - } - } else { - const vx = reachedX ? 0 : (x < entity.x ? -bunnySpeed : bunnySpeed); - const vy = reachedY ? 0 : (y < entity.y ? -bunnySpeed : bunnySpeed); + if (rand < 0.1) { + setEntityAnimation(entity, BunnyAnimation.Clean); + sleepUntil = now + 2; + } else if (rand < 0.2) { + setEntityAnimation(entity, BunnyAnimation.Look); + sleepUntil = now + 2; + } else if (rand < 0.3) { + setEntityAnimation(entity, BunnyAnimation.Blink); + sleepUntil = now + 2; + } else if (rand < 0.6) { + setEntityAnimation(entity, BunnyAnimation.Sit); + sleepUntil = now + 2; + } else { + waypoint = (waypoint + 1) % waypoints.length; + setEntityAnimation(entity, BunnyAnimation.Sit); + sleepUntil = now + random(0.2, 2, true); + } + } else { + const vx = reachedX ? 0 : (x < entity.x ? -bunnySpeed : bunnySpeed); + const vy = reachedY ? 0 : (y < entity.y ? -bunnySpeed : bunnySpeed); - if (entity.vx !== vx || entity.vy !== vy) { - updateEntityVelocity(entity, vx, vy, now); - setEntityAnimation(entity, BunnyAnimation.Walk, vx === 0 ? undefined : vx > 0); - } - } - }; + if (entity.vx !== vx || entity.vy !== vy) { + updateEntityVelocity(entity, vx, vy, now); + setEntityAnimation(entity, BunnyAnimation.Walk, vx === 0 ? undefined : vx > 0); + } + } + }; - if (DEVELOPMENT && false) { - return [entity, ...waypoints.map(({ x, y }) => entities.routePole(x, y))]; - } else { - return [entity]; - } + if (DEVELOPMENT && false) { + return [entity, ...waypoints.map(({ x, y }) => entities.routePole(x, y))]; + } else { + return [entity]; + } } diff --git a/src/ts/server/maps/caveMap.ts b/src/ts/server/maps/caveMap.ts index 9114d97..76fa98f 100644 --- a/src/ts/server/maps/caveMap.ts +++ b/src/ts/server/maps/caveMap.ts @@ -15,969 +15,969 @@ import { holdItem } from '../playerUtils'; const mapData = JSON.parse(fs.readFileSync(pathTo('src', 'maps', 'cave.json'), 'utf8')); export function createCaveMap(world: World): ServerMap { - const map = createServerMap('cave', MapType.Cave, 7, 7, TileType.None, MapUsage.Public); - - map.spawnArea = rect(27, 52, 1, 2); - map.tilesLocked = true; - - deserializeMap(map, mapData); - - // for (let y = 0; y < map.height; y++) { - // for (let x = 0; x < map.width; x++) { - // const tile = getTile(map, x, y); - // if (tile === TileType.Dirt) { - // setTile(map, x, y, TileType.None); - // } else if (tile === TileType.Grass) { - // setTile(map, x, y, TileType.Dirt); - // } - // } - // } - - const add = (entity: ServerEntity) => world.addEntity(entity, map); - - const caveDecals = [entities.caveDecal1, entities.caveDecal3, entities.caveDecal2]; - - function cracksS(x: number, y: number) { - const code = (Math.random() * 1000) % 64; - const index1 = code & 0b11; - const index2 = (code >> 2) & 0b11; - const index3 = (code >> 4) & 0b11; - index1 && index1 !== 3 && add(caveDecals[index1 - 1](x + 0.5, y - 1)); // no decal 2 here - index2 && add(caveDecals[index2 - 1](x + 0.5, y)); - index3 && add(caveDecals[index3 - 1](x + 0.5, y + 1)); - } - - function cracksSLeft(x: number, y: number) { - const code = (Math.random() * 1000) % 4; - (code & 0b01) && add(entities.caveDecalL(x + 0.5, y - 1)); - (code & 0b10) && add(entities.caveDecalL(x + 0.5, y)); - } - - function cracksSRight(x: number, y: number) { - const code = (Math.random() * 1000) % 4; - (code & 0b01) && add(entities.caveDecalR(x + 0.5, y - 1)); - (code & 0b10) && add(entities.caveDecalR(x + 0.5, y)); - } - - function caveSW(x: number, y: number) { - add(entities.caveSW(x + 0.5, y - 2)); - cracksSLeft(x, y); - } - - function caveSE(x: number, y: number) { - add(entities.caveSE(x + 0.5, y - 2)); - cracksSRight(x, y); - } - - function caveS(x: number, y: number) { - add(entities.caveS2(x + 0.5, y - 1)); - cracksS(x, y); - } - - function caveSStart(x: number, y: number) { - add(entities.caveS1(x + 0.5, y - 1)); - cracksS(x, y); - } - - function caveSEnd(x: number, y: number) { - add(entities.caveS3(x + 0.5, y - 1)); - cracksS(x, y); - } - - function caveS1(x: number, y: number) { - add(entities.caveSb(x + 0.5, y - 1)); - cracksS(x, y); - } - - function caveN(x: number, y: number) { - add(entities.caveTopN(x + 0.5, y)); - } - - function caveNE(x: number, y: number) { - add(entities.caveTopNE(x + 0.5, y)); - } - - function caveNW(x: number, y: number) { - add(entities.caveTopNW(x + 0.5, y)); - } - - function caveRightWithTrimNoEdge(x: number, y: number, h: number) { - caveRight(x, y, h); - caveTrimRight(x + 1, y, h, false); - } - - function caveLeftWithTrimNoEdge(x: number, y: number, h: number) { - caveLeft(x, y, h); - caveTrimLeft(x, y, h, false); - } - - function caveRightWithTrim(x: number, y: number, h: number) { - caveRight(x, y - 3, h - 3); - caveTrimRight(x + 1, y, h); - } - - function caveLeftWithTrim(x: number, y: number, h: number) { - caveLeft(x, y - 3, h - 3); - caveTrimLeft(x, y, h); - } - - function caveLeft(x: number, y: number, h: number) { - for (let i = 0; i < h; i++) { - add(entities.caveTopW(x + 0.5, y - i)); - } - } - - function caveRight(x: number, y: number, h: number) { - for (let i = 0; i < h; i++) { - add(entities.caveTopE(x + 0.5, y - i)); - } - } - - function caveTrimLeft(x: number, y: number, h: number, botTrim = true) { - if (botTrim) { - add(entities.caveBotTrimLeft(x - 0.5, y)); - } else { - add(entities.caveMidTrimLeft(x - 0.5, y)); - } - - for (let i = 0; i < (h - 2); i++) { - add(entities.caveMidTrimLeft(x - 0.5, y - 1 - i)); - } - - if (h > 1) { - add(entities.caveTopTrimLeft(x - 0.5, y - h + 1)); - } - } - - function caveTrimRight(x: number, y: number, h: number, botTrim = true) { - if (botTrim) { - add(entities.caveBotTrimRight(x + 0.5, y)); - } else { - add(entities.caveMidTrimRight(x + 0.5, y)); - } - - for (let i = 0; i < (h - 2); i++) { - add(entities.caveMidTrimRight(x + 0.5, y - 1 - i)); - } - - if (h > 1) { - add(entities.caveTopTrimRight(x + 0.5, y - h + 1)); - } - } - - function caveSSection(x: number, y: number, w: number) { - caveSStart(x, y); - - for (let i = 1; i < (w - 1); i++) { - caveS(x + i, y); - } - - caveSEnd(x + w - 1, y); - } - - function caveSESection(x: number, y: number, w: number) { - for (let i = 0; i < w; i++) { - caveSE(x + i, y - i); - } - } - - function caveSWSection(x: number, y: number, w: number) { - for (let i = 0; i < w; i++) { - caveSW(x + i, y + i); - } - } - - // large crypt - caveS1(6, 1); - caveRightWithTrim(5, 3, 4); - caveSESection(4, 4, 2); - caveRightWithTrimNoEdge(3, 8, 6); - caveNE(4, 9); - caveNE(5, 10); - caveNE(6, 11); - caveN(7, 12); - caveNE(8, 12); - caveNE(9, 13); - caveRightWithTrim(9, 19, 6); - caveSE(9, 19); - caveSSection(7, 19, 2); - caveSE(6, 20); - caveRightWithTrim(5, 22, 4); - caveSESection(4, 23, 2); - caveRightWithTrimNoEdge(3, 27, 6); - caveNE(4, 28); - caveRightWithTrimNoEdge(4, 31, 3); - caveNE(5, 32); - caveN(6, 33); - caveNW(7, 32); - caveN(8, 32); - caveN(9, 32); - caveN(10, 32); - caveNW(11, 31); - caveNW(12, 30); - caveLeftWithTrimNoEdge(13, 29, 2); - caveNW(13, 27); - caveNW(14, 26); - caveLeftWithTrimNoEdge(15, 25, 1); - caveNW(15, 24); - caveN(16, 24); - caveN(17, 24); - caveNE(18, 24); - caveNE(19, 25); - caveN(20, 26); - caveNE(21, 26); - caveN(22, 27); - caveNE(23, 27); - caveNE(24, 28); - caveNE(25, 29); - caveNE(26, 30); - caveRightWithTrimNoEdge(26, 31, 1); - caveNE(27, 32); - caveNE(28, 33); - caveRightWithTrim(28, 38, 5); - caveSE(28, 38); - caveS1(27, 38); - caveSW(26, 38); - caveLeftWithTrim(26, 38, 4); - caveSW(25, 36); - caveSSection(23, 35, 2); - caveSE(22, 36); - caveRightWithTrimNoEdge(21, 37, 3); - caveNE(22, 38); - caveRightWithTrim(22, 41, 3); - caveSE(22, 41); - caveSW(21, 41); - caveLeftWithTrim(21, 41, 4); - caveSWSection(18, 37, 3); - caveS1(17, 36); - caveSE(16, 37); - caveRightWithTrim(15, 39, 4); - caveS1(15, 38); - caveSWSection(13, 37, 2); - caveS1(12, 36); - caveSESection(10, 38, 2); - caveRightWithTrimNoEdge(9, 39, 3); - caveNE(10, 40); - caveRightWithTrim(10, 44, 4); - caveSE(10, 44); - caveSSection(5, 44, 2); - caveSE(7, 44); - caveSW(8, 44); - caveS1(9, 44); - caveSE(4, 45); - caveRightWithTrimNoEdge(3, 46, 3); - caveNE(4, 47); - caveN(5, 48); - caveN(6, 48); - caveN(7, 48); - caveN(8, 48); - caveN(9, 48); - caveN(10, 48); - caveNE(11, 48); - caveN(12, 49); - caveNE(13, 49); - caveRightWithTrimNoEdge(13, 50, 1); - caveNE(14, 51); - caveN(15, 52); - caveNW(16, 51); - caveN(17, 51); - caveNW(18, 50); - caveNW(19, 49); - caveNW(20, 48); - caveLeftWithTrimNoEdge(21, 47, 2); - caveNW(21, 45); - caveN(22, 45); - caveNE(23, 45); - caveRightWithTrimNoEdge(23, 47, 2); - caveNE(24, 48); - caveRightWithTrimNoEdge(24, 49, 1); - caveNE(25, 50); - caveRightWithTrimNoEdge(25, 55, 5); - // entrance to caves - caveLeftWithTrimNoEdge(29, 55, 4); - caveNW(29, 51); - caveNW(30, 50); - caveN(31, 50); - caveN(32, 50); - caveNW(33, 49); - caveLeftWithTrimNoEdge(34, 48, 1); - caveNW(34, 47); - caveLeftWithTrimNoEdge(35, 46, 3); - caveNW(35, 43); - caveN(36, 43); - caveN(37, 43); - caveNW(38, 42); - caveN(39, 42); - caveN(40, 42); - caveN(41, 42); - caveN(42, 42); - caveNE(43, 42); - caveN(44, 43); - caveNE(45, 43); - caveN(46, 44); - caveNW(47, 43); - caveN(48, 43); - caveNW(49, 42); - caveN(50, 42); - caveNW(51, 41); - caveNW(52, 40); - caveLeftWithTrimNoEdge(53, 39, 1); - caveNW(53, 38); - caveLeftWithTrimNoEdge(54, 37, 4); - caveSW(53, 35); - caveLeftWithTrim(53, 35, 4); - caveSW(52, 33); - caveLeftWithTrim(52, 33, 5); - caveSWSection(50, 29, 2); - caveS1(49, 28); - caveSW(48, 28); - caveS1(47, 27); - caveSESection(45, 29, 2); - caveS1(44, 29); - caveRightWithTrim(43, 31, 4); - caveSESection(42, 32, 2); - caveRightWithTrim(41, 35, 5); - caveSE(41, 35); - caveRightWithTrim(40, 37, 4); - caveSE(40, 37); - caveSSection(37, 37, 3); - caveSE(36, 38); - caveSSection(33, 38, 3); - caveSW(32, 38); - caveLeftWithTrim(32, 38, 4); - caveNW(32, 34); - // small crypt - - // large crypt - caveLeftWithTrim(13, 18, 4); - caveSW(13, 18); - caveLeftWithTrim(14, 20, 4); - caveSW(14, 20); - caveSESection(15, 20, 2); - caveS1(17, 18); - caveSW(18, 19); - caveS1(19, 19); - caveSW(20, 20); - caveSSection(21, 20, 2); - caveSESection(23, 20, 3); - caveRightWithTrim(25, 18, 4); - caveSE(26, 16); - caveRightWithTrim(26, 16, 4); - caveNE(26, 12); - caveRightWithTrimNoEdge(25, 11, 2); - caveNE(25, 9); - caveRightWithTrimNoEdge(24, 8, 4); - caveSE(25, 6); - caveRightWithTrim(25, 6, 4); - caveSESection(26, 4, 2); - caveS1(28, 2); - caveSE(29, 2); - caveSSection(30, 1, 4); - caveSW(34, 2); - caveS1(35, 2); - caveSW(36, 3); - caveLeftWithTrim(37, 5, 4); - caveSW(37, 5); - caveLeftWithTrim(38, 7, 4); - caveSWSection(38, 7, 3); - caveS1(41, 9); - caveSESection(42, 9, 2); - caveS1(44, 7); - caveSE(45, 7); - caveSSection(46, 6, 2); - caveSW(48, 7); - caveS1(49, 7); - caveSW(50, 8); - caveS1(51, 8); - caveSW(52, 9); - caveLeftWithTrim(53, 11, 4); - caveSW(53, 11); - caveLeftWithTrimNoEdge(54, 15, 6); - caveNW(53, 16); - caveLeftWithTrimNoEdge(53, 18, 2); - caveNW(52, 19); - caveNW(51, 20); - caveNW(50, 21); - caveN(49, 22); - caveNW(48, 22); - caveN(47, 23); - caveNE(46, 22); - caveN(45, 22); - caveNE(44, 21); - caveNE(43, 20); - caveRightWithTrimNoEdge(42, 19, 1); - caveNE(42, 18); - caveN(41, 18); - caveN(40, 18); - caveNE(39, 17); - caveRightWithTrimNoEdge(38, 16, 1); - caveNE(38, 15); - caveN(37, 15); - caveNE(36, 14); - caveN(35, 14); - caveNW(34, 14); - caveNW(33, 15); - caveN(32, 16); - caveNW(31, 16); - caveN(30, 17); - caveNW(29, 17); - caveLeftWithTrimNoEdge(29, 19, 2); - caveNW(28, 20); - caveLeftWithTrimNoEdge(28, 21, 1); - caveNW(27, 22); - caveLeftWithTrim(27, 26, 4); - caveSW(27, 26); - caveS1(28, 26); - caveLeftWithTrim(29, 28, 4); - caveSWSection(29, 28, 2); - caveSSection(31, 29, 2); - // small crypt - - add(entities.caveFill(3, 9)); - add(entities.caveFill(4, 10)); - add(entities.caveFill(5, 11)); - add(entities.caveFill(6, 12)); - add(entities.caveFill(8, 13)); - add(entities.caveFill(3, 28)); - add(entities.caveFill(4, 32)); - add(entities.caveFill(5, 33)); - add(entities.caveFill(7, 33)); - add(entities.caveFill(11, 32)); - add(entities.caveFill(12, 31)); - add(entities.caveFill(13, 30)); - add(entities.caveFill(14, 27)); - add(entities.caveFill(15, 26)); - add(entities.caveFill(18, 25)); - add(entities.caveFill(19, 26)); - add(entities.caveFill(23, 28)); - add(entities.caveFill(24, 29)); - add(entities.caveFill(25, 30)); - add(entities.caveFill(26, 32)); - add(entities.caveFill(27, 33)); - add(entities.caveFill(9, 40)); - add(entities.caveFill(11, 49)); - add(entities.caveFill(13, 51)); - add(entities.caveFill(14, 52)); - add(entities.caveFill(16, 52)); - add(entities.caveFill(18, 51)); - add(entities.caveFill(19, 50)); - add(entities.caveFill(20, 49)); - add(entities.caveFill(21, 48)); - add(entities.caveFill(23, 48)); - add(entities.caveFill(24, 50)); - add(entities.caveFill(30, 51)); - add(entities.caveFill(33, 50)); - add(entities.caveFill(34, 49)); - add(entities.caveFill(35, 47)); - add(entities.caveFill(38, 43)); - add(entities.caveFill(43, 43)); - add(entities.caveFill(45, 44)); - add(entities.caveFill(47, 44)); - add(entities.caveFill(49, 43)); - add(entities.caveFill(51, 42)); - add(entities.caveFill(52, 41)); - add(entities.caveFill(53, 40)); - add(entities.caveFill(54, 38)); - add(entities.caveFill(33, 34)); - add(entities.caveFill(28, 22)); - add(entities.caveFill(31, 17)); - add(entities.caveFill(33, 16)); - add(entities.caveFill(36, 15)); - add(entities.caveFill(38, 17)); - add(entities.caveFill(39, 18)); - add(entities.caveFill(43, 21)); - add(entities.caveFill(44, 22)); - add(entities.caveFill(46, 23)); - add(entities.caveFill(48, 23)); - add(entities.caveFill(50, 22)); - add(entities.caveFill(51, 21)); - add(entities.caveFill(52, 20)); - add(entities.caveFill(53, 19)); - add(entities.caveFill(54, 16)); - add(entities.caveFill(24, 9)); - add(entities.caveFill(25, 12)); - add(entities.caveFill(21, 27)); - - add(entities.trigger3x1(27.5, 55)).trigger = (_, client) => goToMap(world, client, '', 'cave'); - - add(entities.lanternOn(10.38, 18.38)); - add(entities.lanternOn(12.66, 18.38)); - add(entities.lanternOn(18.41, 12.00)); - add(entities.lanternOn(21.41, 12.13)); - add(entities.lanternOn(18.47, 14.21)); - add(entities.lanternOn(21.44, 14.33)); - add(entities.lanternOn(10.41, 11.54)); - add(entities.lanternOn(12.66, 11.50)); - add(entities.lanternOn(13.94, 4.67)); - add(entities.lanternOn(9.44, 4.67)); - add(entities.lanternOn(27.44, 31.54)); - add(entities.lanternOn(21.97, 26.50)); - add(entities.lanternOn(19.31, 21.79)); - add(entities.lanternOn(15.53, 24.17)); - add(entities.lanternOn(9.28, 25.63)); - add(entities.lanternOn(29.34, 17.29)); - add(entities.lanternOn(15.81, 40.42)); - add(entities.lanternOn(11.28, 43.50)); - add(entities.lanternOn(14.66, 48.75)); - add(entities.lanternOn(18.44, 48.33)); - add(entities.lanternOn(16.18, 45.04)); - add(entities.lanternOn(39.53, 41.91)); - add(entities.lanternOn(41.37, 37.41)); - add(entities.lanternOn(45.84, 34.13)); - add(entities.lanternOn(50.50, 38.17)); - add(entities.lanternOn(44.78, 41.12)); - add(entities.lanternOn(37.46, 27.12)); - add(entities.lanternOn(26.22, 51.79)); - add(entities.lanternOn(28.75, 51.75)); - add(entities.lanternOn(23.34, 42.08)); - add(entities.lanternOn(23.88, 45.33)); - add(entities.lanternOn(33.66, 40.20)); - add(entities.lanternOn(34.44, 45.17)); - add(entities.lanternOn(28.81, 44.25)); - add(entities.lanternOn(25.31, 20.50)); - add(entities.lanternOn(28.63, 28.38)); - add(entities.lanternOn(29.28, 36.92)); - add(entities.lanternOn(32.38, 31.38)); - add(entities.lanternOn(33.22, 22.83)); - add(entities.lanternOn(31.84, 49.83)); - add(entities.lanternOn(30.28, 48.91)); - add(entities.lanternOn(14.09, 8.63)); - add(entities.lanternOn(9.41, 8.54)); - - add(entities.waterRock1(8.63, 27.92)); - add(entities.waterRock3(8.75, 28.08)); - add(entities.waterRock6(29.59, 8.79)); - add(entities.waterRock8(29.66, 9.00)); - add(entities.waterRock5(31.81, 6.83)); - add(entities.waterRock4(31.41, 6.58)); - add(entities.waterRock1(32.34, 9.79)); - add(entities.waterRock1(46.56, 13.50)); - add(entities.waterRock10(46.25, 13.71)); - add(entities.waterRock11(49.63, 16.63)); - add(entities.waterRock9(46.50, 11.46)); - add(entities.waterRock4(46.38, 11.63)); - - add(entities.box(32.03, 49.13)); - - add(createBoxOfLanterns(30.97, 49.63)).interact = (_, client) => { - if (client.pony.options!.hold === entities.crystalHeld.type) { - holdItem(client.pony, entities.crystalLantern.type); - } else { - holdItem(client.pony, entities.lanternOn.type); - } - }; - - // top rooms - add(createBoxOfLanterns(9.34, 11.46)); - add(entities.crate1A(17.78, 11.21)); - add(entities.crate1A(18.84, 11.29)); - add(entities.crate1A(7.69, 4.13)); - add(entities.crate1A(8.72, 4.13)); - add(entities.crate1A(10.00, 4.17)); - add(entities.crate1A(8.06, 5.33)); - add(entities.crate1BHigh(8.31, 4.17)); - add(entities.crate1BHigh(18.25, 11.30)); - add(entities.barrel(15.31, 3.79)); - add(entities.barrel(14.66, 4.54)); - add(entities.barrel(15.28, 5.08)); - add(entities.barrel(14.50, 3.75)); - add(entities.barrel(15.34, 6.08)); - add(entities.barrel(19.78, 10.75)); - add(entities.barrel(20.56, 11.25)); - - function railsH(x: number, y: number, length: number) { - for (let i = 0; i < length; i++) { - add(entities.mineRailsH(x + i + 0.5, y)); - } - } - - function railsV(x: number, y: number, length: number) { - for (let i = 0; i < length; i++) { - add(entities.mineRailsV(x + 0.5, y - i)); - } - } - - railsV(30, 40, 8); - add(entities.mineRailsSE(30.5, 32)); - railsH(31, 32, 5); - add(entities.mineRailsEndRight(36.5, 32.5)); - - railsH(5, 46, 13); - add(entities.mineRailsNW(18.5, 46)); - railsV(18, 45, 1); - add(entities.mineRailsNSE(18.5, 44)); - railsH(19, 44, 7); - railsV(18, 43, 1); - add(entities.mineRailsSW(18.5, 42)); - - add(entities.mineCart(49, 40)); - add(entities.crystalsCartPile(49, 40)).interact = give(entities.crystalHeld.type); - - add(entities.mineCart(31.5, 46)); - add(entities.crystalsCartPile(31.5, 46)).interact = give(entities.crystalHeld.type); - - add(entities.mineRailsEndRight(50.5, 40.5)); - railsH(38, 40, 9); - add(entities.mineRailsNWE(47.5, 40)); - railsH(48, 40, 2); - add(entities.mineRailsSE(37.5, 40)); - add(entities.mineRailsNW(37.5, 41)); - railsH(27, 41, 3); - add(entities.mineRailsNWE(30.5, 41)); - railsH(31, 41, 6); - add(entities.mineRailsSE(26.5, 41)); - railsV(26, 43, 2); - add(entities.mineRailsNSW(26.5, 44)); - railsV(26, 45, 1); - add(entities.mineRailsNE(26.5, 46)); - railsH(28, 46, 5); - add(entities.mineRailsEndRight(33.5, 46.5)); - railsH(14, 42, 4); - add(entities.mineRailsEndLeft(13.5, 42.5)); - - railsV(47, 39, 8); - add(entities.mineRailsEndTop(47.5, 32)); - - add(entities.mineRailsSWE(27.5, 46)); - railsV(27, 55, 9); - - add(entities.wallMap(23.97, 36.96)); - add(entities.table3(24.00, 37.2916)); - add(entities.lanternOnTable(24.375, 37.30)); - add(entities.lanternOn(22.50, 38.33)); - add(entities.sandPileSmall(25.50, 40.33)); - add(entities.table3(37.81, 39.41)); - - add(entities.sandPileMedium(31.44, 42.67)); - add(entities.sandPileSmall(32.03, 43.54)); - add(entities.sandPileTiny(28.75, 45.33)); - add(entities.sandPileSmall(32.63, 47.50)); - add(entities.sandPileTiny(33.03, 47.96)); - add(entities.sandPileSmall(27.94, 45.25)); - add(entities.sandPileSmall(29.81, 48.00)); - add(entities.sandPileTiny(29.13, 48.38)); - add(entities.sandPileSmall(35.72, 42.46)); - add(entities.sandPileTiny(32.53, 42.42)); - add(entities.sandPileTinier(36.47, 42.71)); - add(entities.sandPileBig(50.00, 34.96)); - add(entities.sandPileMedium(49.16, 36.00)); - add(entities.sandPileSmall(44.13, 34.42)); - add(entities.sandPileMedium(17.88, 40.58)); - add(entities.sandPileSmall(18.69, 41.29)); - add(entities.sandPileSmall(15.63, 49.88)); - add(entities.sandPileTiny(16.31, 50.33)); - add(entities.sandPileTinier(17.63, 41.58)); - add(entities.sandPileSmall(7.19, 24.29)); - add(entities.sandPileTiny(6.69, 24.79)); - - add(entities.sandPileMedium(6.19, 46.92)); - add(entities.sandPileTiny(5.03, 47.25)); - add(entities.rockB(5.13, 46.46)); - add(entities.rockB(7.31, 47.33)); - add(entities.rock2B(5.00, 47.13)); - add(entities.rock3B(5.72, 46.54)); - add(entities.rock3B(7.91, 47.04)); - add(entities.rock2B(8.03, 45.75)); - add(entities.rock2B(6.72, 47.54)); - add(entities.rock2B(32.03, 49.38)); - add(entities.rock2B(50.63, 39.71)); - add(entities.rock3B(41.59, 37.13)); - add(entities.rock3B(32.13, 31.25)); - add(entities.rock3B(22.34, 22.08)); - add(entities.rock2B(22.91, 22.29)); - add(entities.rock3B(35.63, 4.17)); - add(entities.rock2B(44.66, 9.29)); - add(entities.rockB(7.47, 21.33)); - add(entities.rock2B(7.81, 21.71)); - - add(entities.caveCover(13.125, 11.625)); - add(entities.caveCover(14.125, 11.625)); - add(entities.caveCover(15.125, 11.625)); - add(entities.caveCover(13.125, 15.625)); - add(entities.caveCover(14.125, 15.625)); - add(entities.caveCover(15.125, 15.625)); - add(entities.caveCover(15.875, 15.625)); - add(entities.caveCover(16.875, 16.625)); - add(entities.caveCover(17.875, 16.625)); - add(entities.caveCover(18.875, 16.625)); - add(entities.caveCover(19.875, 16.625)); - add(entities.caveCover(20.875, 16.625)); - add(entities.caveCover(21.875, 16.625)); - add(entities.caveCover(22.875, 16.625)); - add(entities.caveCover(30.88, 27.625)); - add(entities.caveCover(31.88, 27.625)); - add(entities.caveCover(32.88, 27.625)); - add(entities.caveCover(38.125, 30.625)); - add(entities.caveCover(33.125, 34.625)); - add(entities.caveCover(34.03, 34.625)); - add(entities.caveCover(35.03, 34.625)); - add(entities.caveCover(36.03, 34.625)); - add(entities.caveCover(37.03, 34.625)); - add(entities.caveCover(38.03, 34.625)); - add(entities.caveCover(32.88, 35.17)); - - add(entities.crystals1(48.50, 10.42)); - add(entities.crystals1(42.59, 17.17)); - add(entities.crystals2(47.44, 14.42)); - add(entities.crystals3(52.06, 16.25)); - add(entities.crystals4(48.69, 18.96)); - add(entities.crystals5(45.56, 10.21)); - add(entities.crystals6(51.53, 11.58)); - add(entities.crystals7(45.53, 16.50)); - add(entities.crystals9(51.56, 18.71)); - add(entities.crystals10(45.59, 20.67)); - add(entities.crystals10(43.59, 13.33)); - add(entities.crystals3(40.63, 13.46)); - add(entities.crystals9(38.80, 10.04)); - add(entities.crystals8(37.34, 13.58)); - add(entities.crystals8(49.91, 15.21)); - add(entities.crystals1(33.41, 10.46)); - add(entities.crystals1(32.66, 4.33)); - add(entities.crystals2(34.38, 8.71)); - add(entities.crystals3(33.44, 14.67)); - add(entities.crystals3(27.47, 6.38)); - add(entities.crystals4(28.63, 5.79)); - add(entities.crystals5(28.69, 9.46)); - add(entities.crystals5(37.38, 9.79)); - add(entities.crystals6(28.22, 10.96)); - add(entities.crystals7(31.56, 11.75)); - add(entities.crystals8(34.81, 6.21)); - add(entities.crystals9(26.22, 8.04)); - add(entities.crystals10(36.91, 8.21)); - add(entities.crystals2(28.68, 14.54)); - add(entities.crystals8(28.63, 18.79)); - add(entities.crystals7(26.34, 23.71)); - add(entities.crystals6(23.22, 23.29)); - add(entities.crystals5(16.43, 22.29)); - add(entities.crystals3(8.59, 30.33)); - add(entities.crystals5(6.59, 27.58)); - add(entities.crystals2(11.625, 28.42)); - add(entities.crystals8(7.31, 22.54)); - add(entities.crystals4(8.44, 23.17)); - add(entities.crystals6(7.31, 29.83)); - add(entities.crystals1(5.44, 25.38)); - add(entities.crystals3(5.31, 6.50)); - add(entities.crystals7(6.41, 10.42)); - - add(entities.crystals5(49.56, 31.46)); - add(entities.crystals8(50.63, 33.33)); - add(entities.crystals1(47.19, 30.21)); - add(entities.crystals9(46.06, 32.21)); - add(entities.crystals8(17.28, 39.38)); - add(entities.crystals3(12.41, 39.33)); - add(entities.crystals6(11.31, 40.75)); - - add(entities.waterCrystal1(31.34, 7.13)); - add(entities.waterCrystal2(31.03, 6.92)); - add(entities.waterCrysta3(31.00, 7.29)); - add(entities.waterCrysta3(32.34, 10.00)); - add(entities.waterCrystal2(46.06, 13.29)); - add(entities.waterCrysta3(49.44, 16.29)); - add(entities.waterCrysta3(46.72, 11.67)); - add(entities.waterCrysta3(41.75, 12.54)); - add(entities.waterCrystal1(8.47, 27.46)); - add(entities.waterCrysta3(10.47, 27.54)); - - add(entities.stalactite3(17.06, 20.79)); - add(entities.stalactite3(22.25, 22.83)); - add(entities.stalactite2(21.81, 22.46)); - add(entities.stalactite1(17.41, 21.08)); - add(entities.stalactite1(28.31, 28.25)); - add(entities.stalactite2(13.69, 20.63)); - add(entities.stalactite3(6.56, 22.04)); - add(entities.stalactite3(7.38, 28.46)); - add(entities.stalactite1(7.66, 28.54)); - add(entities.stalactite2(6.28, 22.42)); - add(entities.stalactite2(9.69, 20.79)); - add(entities.stalactite1(10.06, 20.58)); - add(entities.stalactite2(12.25, 25.79)); - add(entities.stalactite1(11.91, 26.08)); - add(entities.stalactite3(11.88, 25.71)); - add(entities.stalactite2(31.25, 31.25)); - add(entities.stalactite3(22.72, 42.88)); - add(entities.stalactite1(23.06, 42.71)); - add(entities.stalactite3(10.59, 40.21)); - add(entities.stalactite2(12.19, 38.42)); - add(entities.stalactite1(12.63, 38.42)); - add(entities.stalactite3(47.25, 29.46)); - add(entities.stalactite2(44.34, 31.33)); - add(entities.stalactite1(47.69, 29.58)); - add(entities.stalactite1(51.75, 33.50)); - add(entities.stalactite2(26.31, 18.21)); - add(entities.stalactite3(28.22, 4.33)); - add(entities.stalactite3(37.69, 7.29)); - add(entities.stalactite2(39.44, 10.00)); - add(entities.stalactite2(28.75, 4.13)); - add(entities.stalactite1(27.47, 10.88)); - add(entities.stalactite1(37.28, 7.04)); - add(entities.stalactite3(49.88, 9.42)); - add(entities.stalactite3(50.63, 13.92)); - add(entities.stalactite3(42.84, 14.63)); - add(entities.stalactite3(50.78, 20.96)); - add(entities.stalactite3(44.09, 9.46)); - add(entities.stalactite2(43.16, 15.04)); - add(entities.stalactite2(49.44, 9.21)); - add(entities.stalactite2(46.00, 8.42)); - add(entities.stalactite2(51.47, 14.17)); - add(entities.stalactite1(51.06, 14.29)); - add(entities.stalactite1(42.59, 15.13)); - add(entities.stalactite1(44.31, 9.67)); - add(entities.stalactite1(51.78, 10.33)); - add(entities.stalactite1(50.44, 21.29)); - add(entities.stalactite1(47.41, 17.71)); - add(entities.stalactite2(47.06, 17.71)); - add(entities.stalactite1(28.66, 19.67)); - add(entities.stalactite3(4.34, 6.33)); - add(entities.stalactite2(4.56, 7.00)); - add(entities.stalactite1(5.56, 10.29)); - add(entities.stalactite3(35.38, 11.83)); - add(entities.stalactite2(35.28, 12.21)); - add(entities.stalactite1(34.94, 11.79)); - add(entities.stalactite3(15.03, 22.29)); - add(entities.stalactite1(14.63, 22.17)); - - // storage room - add(entities.table3(37.03, 23.71)); - add(entities.lanternOnTable(37.25, 23.75)); - add(entities.barrel(31.69, 24.71)); - add(entities.barrel(32.03, 25.50)); - add(entities.barrel(31.63, 26.17)); - add(entities.barrel(38.34, 25.75)); - add(entities.barrel(38.41, 27.04)); - add(entities.barrel(37.78, 26.38)); - add(entities.barrel(31.94, 26.96)); - add(entities.toolboxFull(34.28, 22.96)).interact = give(entities.pickaxe.type); - add(entities.box(38.22, 29.96)); - add(entities.ropeRack(34.31, 22.20)).interact = give(entities.rope.type); - add(entities.boxLanterns(35.63, 24.04)).interact = give(entities.lanternOn.type); - add(entities.crate1A(38.19, 28.79)); - add(entities.crate1A(34.81, 28.58)); - add(entities.crate1A(34.72, 29.88)); - add(entities.crate2A(34.81, 30.67)); - - add(entities.ropeRack(34.31, 39.91)).interact = give(entities.rope.type); - add(entities.crate1A(34.22, 43.38)); - add(entities.crate1A(33.91, 44.50)); - - function placeMineCart(x: number, y: number) { - add(entities.mineCartBack(x + 0.22, y - 0.04)); - add(entities.mineCartFront(x + 0.22, y + 0.83)); - } - - placeMineCart(41, 40); - placeMineCart(45, 40); - placeMineCart(16, 42); - placeMineCart(8, 46); - placeMineCart(12, 46); - placeMineCart(15, 46); - placeMineCart(28, 41); - - map.controllers.push(new TorchController(world, map)); - map.controllers.push(new FlyingCritterController(world, map, entities.bat, 2, 10, () => true, true)); - - const wallController = new WallController(world, map, entities.stoneWalls); - map.controllers.push(wallController); - wallController.top = 3; - wallController.isTall = (x, y) => { - if (y === 10 && x >= 17 && x <= 23) - return true; - - if (x >= 31 && x <= 39 && y >= 22 && y <= 25) - return true; - - if (y === 31 && x >= 33 && x <= 34) - return true; - - return false; - }; - - if (wallController.toggleWall) { - // large crypt - for (let x = 7; x <= 15; x++) { - wallController.toggleWall(x, 3, TileType.WallH); - } - - for (let y = 3; y <= 10; y++) { - wallController.toggleWall(16, y, TileType.WallV); - } - - for (let x = 13; x <= 15; x++) { - wallController.toggleWall(x, 11, TileType.WallH); - } - - wallController.toggleWall(13, 11, TileType.WallV); - - for (let x = 13; x <= 16; x++) { - wallController.toggleWall(x, 12, TileType.WallH); - } - - wallController.toggleWall(17, 10, TileType.WallV); - wallController.toggleWall(17, 11, TileType.WallV); - - for (let x = 17; x <= 22; x++) { - wallController.toggleWall(x, 10, TileType.WallH); - } - - for (let y = 10; y <= 15; y++) { - wallController.toggleWall(23, y, TileType.WallV); - } - - for (let x = 17; x <= 22; x++) { - wallController.toggleWall(x, 16, TileType.WallH); - } - - wallController.toggleWall(17, 15, TileType.WallV); - - for (let x = 13; x <= 16; x++) { - wallController.toggleWall(x, 15, TileType.WallH); - } - - // small crypt - wallController.toggleWall(33, 31, TileType.WallH); - wallController.toggleWall(34, 30, TileType.WallV); - wallController.toggleWall(34, 29, TileType.WallV); - wallController.toggleWall(34, 28, TileType.WallV); - wallController.toggleWall(34, 27, TileType.WallV); - wallController.toggleWall(33, 27, TileType.WallH); - wallController.toggleWall(32, 27, TileType.WallH); - wallController.toggleWall(31, 27, TileType.WallH); - wallController.toggleWall(31, 26, TileType.WallV); - wallController.toggleWall(31, 25, TileType.WallV); - wallController.toggleWall(31, 24, TileType.WallV); - wallController.toggleWall(31, 24, TileType.WallH); - wallController.toggleWall(32, 23, TileType.WallV); - wallController.toggleWall(32, 22, TileType.WallV); - wallController.toggleWall(32, 22, TileType.WallH); - wallController.toggleWall(33, 22, TileType.WallH); - wallController.toggleWall(34, 22, TileType.WallH); - wallController.toggleWall(35, 22, TileType.WallV); - wallController.toggleWall(35, 23, TileType.WallH); - wallController.toggleWall(36, 23, TileType.WallH); - wallController.toggleWall(37, 23, TileType.WallH); - wallController.toggleWall(38, 23, TileType.WallV); - wallController.toggleWall(38, 24, TileType.WallV); - wallController.toggleWall(38, 25, TileType.WallH); - wallController.toggleWall(39, 25, TileType.WallV); - wallController.toggleWall(39, 26, TileType.WallV); - wallController.toggleWall(39, 27, TileType.WallV); - wallController.toggleWall(39, 28, TileType.WallV); - wallController.toggleWall(39, 29, TileType.WallV); - wallController.toggleWall(38, 30, TileType.WallH); - wallController.toggleWall(38, 30, TileType.WallV); - wallController.toggleWall(38, 31, TileType.WallV); - wallController.toggleWall(38, 32, TileType.WallV); - wallController.toggleWall(38, 33, TileType.WallV); - wallController.toggleWall(37, 34, TileType.WallH); - wallController.toggleWall(36, 34, TileType.WallH); - wallController.toggleWall(35, 34, TileType.WallH); - wallController.toggleWall(34, 34, TileType.WallH); - wallController.toggleWall(33, 34, TileType.WallH); - wallController.toggleWall(33, 34, TileType.WallV); - } - - if (DEVELOPMENT) { - addSpawnPointIndicators(world, map); - } - - return map; + const map = createServerMap('cave', MapType.Cave, 7, 7, TileType.None, MapUsage.Public); + + map.spawnArea = rect(27, 52, 1, 2); + map.tilesLocked = true; + + deserializeMap(map, mapData); + + // for (let y = 0; y < map.height; y++) { + // for (let x = 0; x < map.width; x++) { + // const tile = getTile(map, x, y); + // if (tile === TileType.Dirt) { + // setTile(map, x, y, TileType.None); + // } else if (tile === TileType.Grass) { + // setTile(map, x, y, TileType.Dirt); + // } + // } + // } + + const add = (entity: ServerEntity) => world.addEntity(entity, map); + + const caveDecals = [entities.caveDecal1, entities.caveDecal3, entities.caveDecal2]; + + function cracksS(x: number, y: number) { + const code = (Math.random() * 1000) % 64; + const index1 = code & 0b11; + const index2 = (code >> 2) & 0b11; + const index3 = (code >> 4) & 0b11; + index1 && index1 !== 3 && add(caveDecals[index1 - 1](x + 0.5, y - 1)); // no decal 2 here + index2 && add(caveDecals[index2 - 1](x + 0.5, y)); + index3 && add(caveDecals[index3 - 1](x + 0.5, y + 1)); + } + + function cracksSLeft(x: number, y: number) { + const code = (Math.random() * 1000) % 4; + (code & 0b01) && add(entities.caveDecalL(x + 0.5, y - 1)); + (code & 0b10) && add(entities.caveDecalL(x + 0.5, y)); + } + + function cracksSRight(x: number, y: number) { + const code = (Math.random() * 1000) % 4; + (code & 0b01) && add(entities.caveDecalR(x + 0.5, y - 1)); + (code & 0b10) && add(entities.caveDecalR(x + 0.5, y)); + } + + function caveSW(x: number, y: number) { + add(entities.caveSW(x + 0.5, y - 2)); + cracksSLeft(x, y); + } + + function caveSE(x: number, y: number) { + add(entities.caveSE(x + 0.5, y - 2)); + cracksSRight(x, y); + } + + function caveS(x: number, y: number) { + add(entities.caveS2(x + 0.5, y - 1)); + cracksS(x, y); + } + + function caveSStart(x: number, y: number) { + add(entities.caveS1(x + 0.5, y - 1)); + cracksS(x, y); + } + + function caveSEnd(x: number, y: number) { + add(entities.caveS3(x + 0.5, y - 1)); + cracksS(x, y); + } + + function caveS1(x: number, y: number) { + add(entities.caveSb(x + 0.5, y - 1)); + cracksS(x, y); + } + + function caveN(x: number, y: number) { + add(entities.caveTopN(x + 0.5, y)); + } + + function caveNE(x: number, y: number) { + add(entities.caveTopNE(x + 0.5, y)); + } + + function caveNW(x: number, y: number) { + add(entities.caveTopNW(x + 0.5, y)); + } + + function caveRightWithTrimNoEdge(x: number, y: number, h: number) { + caveRight(x, y, h); + caveTrimRight(x + 1, y, h, false); + } + + function caveLeftWithTrimNoEdge(x: number, y: number, h: number) { + caveLeft(x, y, h); + caveTrimLeft(x, y, h, false); + } + + function caveRightWithTrim(x: number, y: number, h: number) { + caveRight(x, y - 3, h - 3); + caveTrimRight(x + 1, y, h); + } + + function caveLeftWithTrim(x: number, y: number, h: number) { + caveLeft(x, y - 3, h - 3); + caveTrimLeft(x, y, h); + } + + function caveLeft(x: number, y: number, h: number) { + for (let i = 0; i < h; i++) { + add(entities.caveTopW(x + 0.5, y - i)); + } + } + + function caveRight(x: number, y: number, h: number) { + for (let i = 0; i < h; i++) { + add(entities.caveTopE(x + 0.5, y - i)); + } + } + + function caveTrimLeft(x: number, y: number, h: number, botTrim = true) { + if (botTrim) { + add(entities.caveBotTrimLeft(x - 0.5, y)); + } else { + add(entities.caveMidTrimLeft(x - 0.5, y)); + } + + for (let i = 0; i < (h - 2); i++) { + add(entities.caveMidTrimLeft(x - 0.5, y - 1 - i)); + } + + if (h > 1) { + add(entities.caveTopTrimLeft(x - 0.5, y - h + 1)); + } + } + + function caveTrimRight(x: number, y: number, h: number, botTrim = true) { + if (botTrim) { + add(entities.caveBotTrimRight(x + 0.5, y)); + } else { + add(entities.caveMidTrimRight(x + 0.5, y)); + } + + for (let i = 0; i < (h - 2); i++) { + add(entities.caveMidTrimRight(x + 0.5, y - 1 - i)); + } + + if (h > 1) { + add(entities.caveTopTrimRight(x + 0.5, y - h + 1)); + } + } + + function caveSSection(x: number, y: number, w: number) { + caveSStart(x, y); + + for (let i = 1; i < (w - 1); i++) { + caveS(x + i, y); + } + + caveSEnd(x + w - 1, y); + } + + function caveSESection(x: number, y: number, w: number) { + for (let i = 0; i < w; i++) { + caveSE(x + i, y - i); + } + } + + function caveSWSection(x: number, y: number, w: number) { + for (let i = 0; i < w; i++) { + caveSW(x + i, y + i); + } + } + + // large crypt + caveS1(6, 1); + caveRightWithTrim(5, 3, 4); + caveSESection(4, 4, 2); + caveRightWithTrimNoEdge(3, 8, 6); + caveNE(4, 9); + caveNE(5, 10); + caveNE(6, 11); + caveN(7, 12); + caveNE(8, 12); + caveNE(9, 13); + caveRightWithTrim(9, 19, 6); + caveSE(9, 19); + caveSSection(7, 19, 2); + caveSE(6, 20); + caveRightWithTrim(5, 22, 4); + caveSESection(4, 23, 2); + caveRightWithTrimNoEdge(3, 27, 6); + caveNE(4, 28); + caveRightWithTrimNoEdge(4, 31, 3); + caveNE(5, 32); + caveN(6, 33); + caveNW(7, 32); + caveN(8, 32); + caveN(9, 32); + caveN(10, 32); + caveNW(11, 31); + caveNW(12, 30); + caveLeftWithTrimNoEdge(13, 29, 2); + caveNW(13, 27); + caveNW(14, 26); + caveLeftWithTrimNoEdge(15, 25, 1); + caveNW(15, 24); + caveN(16, 24); + caveN(17, 24); + caveNE(18, 24); + caveNE(19, 25); + caveN(20, 26); + caveNE(21, 26); + caveN(22, 27); + caveNE(23, 27); + caveNE(24, 28); + caveNE(25, 29); + caveNE(26, 30); + caveRightWithTrimNoEdge(26, 31, 1); + caveNE(27, 32); + caveNE(28, 33); + caveRightWithTrim(28, 38, 5); + caveSE(28, 38); + caveS1(27, 38); + caveSW(26, 38); + caveLeftWithTrim(26, 38, 4); + caveSW(25, 36); + caveSSection(23, 35, 2); + caveSE(22, 36); + caveRightWithTrimNoEdge(21, 37, 3); + caveNE(22, 38); + caveRightWithTrim(22, 41, 3); + caveSE(22, 41); + caveSW(21, 41); + caveLeftWithTrim(21, 41, 4); + caveSWSection(18, 37, 3); + caveS1(17, 36); + caveSE(16, 37); + caveRightWithTrim(15, 39, 4); + caveS1(15, 38); + caveSWSection(13, 37, 2); + caveS1(12, 36); + caveSESection(10, 38, 2); + caveRightWithTrimNoEdge(9, 39, 3); + caveNE(10, 40); + caveRightWithTrim(10, 44, 4); + caveSE(10, 44); + caveSSection(5, 44, 2); + caveSE(7, 44); + caveSW(8, 44); + caveS1(9, 44); + caveSE(4, 45); + caveRightWithTrimNoEdge(3, 46, 3); + caveNE(4, 47); + caveN(5, 48); + caveN(6, 48); + caveN(7, 48); + caveN(8, 48); + caveN(9, 48); + caveN(10, 48); + caveNE(11, 48); + caveN(12, 49); + caveNE(13, 49); + caveRightWithTrimNoEdge(13, 50, 1); + caveNE(14, 51); + caveN(15, 52); + caveNW(16, 51); + caveN(17, 51); + caveNW(18, 50); + caveNW(19, 49); + caveNW(20, 48); + caveLeftWithTrimNoEdge(21, 47, 2); + caveNW(21, 45); + caveN(22, 45); + caveNE(23, 45); + caveRightWithTrimNoEdge(23, 47, 2); + caveNE(24, 48); + caveRightWithTrimNoEdge(24, 49, 1); + caveNE(25, 50); + caveRightWithTrimNoEdge(25, 55, 5); + // entrance to caves + caveLeftWithTrimNoEdge(29, 55, 4); + caveNW(29, 51); + caveNW(30, 50); + caveN(31, 50); + caveN(32, 50); + caveNW(33, 49); + caveLeftWithTrimNoEdge(34, 48, 1); + caveNW(34, 47); + caveLeftWithTrimNoEdge(35, 46, 3); + caveNW(35, 43); + caveN(36, 43); + caveN(37, 43); + caveNW(38, 42); + caveN(39, 42); + caveN(40, 42); + caveN(41, 42); + caveN(42, 42); + caveNE(43, 42); + caveN(44, 43); + caveNE(45, 43); + caveN(46, 44); + caveNW(47, 43); + caveN(48, 43); + caveNW(49, 42); + caveN(50, 42); + caveNW(51, 41); + caveNW(52, 40); + caveLeftWithTrimNoEdge(53, 39, 1); + caveNW(53, 38); + caveLeftWithTrimNoEdge(54, 37, 4); + caveSW(53, 35); + caveLeftWithTrim(53, 35, 4); + caveSW(52, 33); + caveLeftWithTrim(52, 33, 5); + caveSWSection(50, 29, 2); + caveS1(49, 28); + caveSW(48, 28); + caveS1(47, 27); + caveSESection(45, 29, 2); + caveS1(44, 29); + caveRightWithTrim(43, 31, 4); + caveSESection(42, 32, 2); + caveRightWithTrim(41, 35, 5); + caveSE(41, 35); + caveRightWithTrim(40, 37, 4); + caveSE(40, 37); + caveSSection(37, 37, 3); + caveSE(36, 38); + caveSSection(33, 38, 3); + caveSW(32, 38); + caveLeftWithTrim(32, 38, 4); + caveNW(32, 34); + // small crypt + + // large crypt + caveLeftWithTrim(13, 18, 4); + caveSW(13, 18); + caveLeftWithTrim(14, 20, 4); + caveSW(14, 20); + caveSESection(15, 20, 2); + caveS1(17, 18); + caveSW(18, 19); + caveS1(19, 19); + caveSW(20, 20); + caveSSection(21, 20, 2); + caveSESection(23, 20, 3); + caveRightWithTrim(25, 18, 4); + caveSE(26, 16); + caveRightWithTrim(26, 16, 4); + caveNE(26, 12); + caveRightWithTrimNoEdge(25, 11, 2); + caveNE(25, 9); + caveRightWithTrimNoEdge(24, 8, 4); + caveSE(25, 6); + caveRightWithTrim(25, 6, 4); + caveSESection(26, 4, 2); + caveS1(28, 2); + caveSE(29, 2); + caveSSection(30, 1, 4); + caveSW(34, 2); + caveS1(35, 2); + caveSW(36, 3); + caveLeftWithTrim(37, 5, 4); + caveSW(37, 5); + caveLeftWithTrim(38, 7, 4); + caveSWSection(38, 7, 3); + caveS1(41, 9); + caveSESection(42, 9, 2); + caveS1(44, 7); + caveSE(45, 7); + caveSSection(46, 6, 2); + caveSW(48, 7); + caveS1(49, 7); + caveSW(50, 8); + caveS1(51, 8); + caveSW(52, 9); + caveLeftWithTrim(53, 11, 4); + caveSW(53, 11); + caveLeftWithTrimNoEdge(54, 15, 6); + caveNW(53, 16); + caveLeftWithTrimNoEdge(53, 18, 2); + caveNW(52, 19); + caveNW(51, 20); + caveNW(50, 21); + caveN(49, 22); + caveNW(48, 22); + caveN(47, 23); + caveNE(46, 22); + caveN(45, 22); + caveNE(44, 21); + caveNE(43, 20); + caveRightWithTrimNoEdge(42, 19, 1); + caveNE(42, 18); + caveN(41, 18); + caveN(40, 18); + caveNE(39, 17); + caveRightWithTrimNoEdge(38, 16, 1); + caveNE(38, 15); + caveN(37, 15); + caveNE(36, 14); + caveN(35, 14); + caveNW(34, 14); + caveNW(33, 15); + caveN(32, 16); + caveNW(31, 16); + caveN(30, 17); + caveNW(29, 17); + caveLeftWithTrimNoEdge(29, 19, 2); + caveNW(28, 20); + caveLeftWithTrimNoEdge(28, 21, 1); + caveNW(27, 22); + caveLeftWithTrim(27, 26, 4); + caveSW(27, 26); + caveS1(28, 26); + caveLeftWithTrim(29, 28, 4); + caveSWSection(29, 28, 2); + caveSSection(31, 29, 2); + // small crypt + + add(entities.caveFill(3, 9)); + add(entities.caveFill(4, 10)); + add(entities.caveFill(5, 11)); + add(entities.caveFill(6, 12)); + add(entities.caveFill(8, 13)); + add(entities.caveFill(3, 28)); + add(entities.caveFill(4, 32)); + add(entities.caveFill(5, 33)); + add(entities.caveFill(7, 33)); + add(entities.caveFill(11, 32)); + add(entities.caveFill(12, 31)); + add(entities.caveFill(13, 30)); + add(entities.caveFill(14, 27)); + add(entities.caveFill(15, 26)); + add(entities.caveFill(18, 25)); + add(entities.caveFill(19, 26)); + add(entities.caveFill(23, 28)); + add(entities.caveFill(24, 29)); + add(entities.caveFill(25, 30)); + add(entities.caveFill(26, 32)); + add(entities.caveFill(27, 33)); + add(entities.caveFill(9, 40)); + add(entities.caveFill(11, 49)); + add(entities.caveFill(13, 51)); + add(entities.caveFill(14, 52)); + add(entities.caveFill(16, 52)); + add(entities.caveFill(18, 51)); + add(entities.caveFill(19, 50)); + add(entities.caveFill(20, 49)); + add(entities.caveFill(21, 48)); + add(entities.caveFill(23, 48)); + add(entities.caveFill(24, 50)); + add(entities.caveFill(30, 51)); + add(entities.caveFill(33, 50)); + add(entities.caveFill(34, 49)); + add(entities.caveFill(35, 47)); + add(entities.caveFill(38, 43)); + add(entities.caveFill(43, 43)); + add(entities.caveFill(45, 44)); + add(entities.caveFill(47, 44)); + add(entities.caveFill(49, 43)); + add(entities.caveFill(51, 42)); + add(entities.caveFill(52, 41)); + add(entities.caveFill(53, 40)); + add(entities.caveFill(54, 38)); + add(entities.caveFill(33, 34)); + add(entities.caveFill(28, 22)); + add(entities.caveFill(31, 17)); + add(entities.caveFill(33, 16)); + add(entities.caveFill(36, 15)); + add(entities.caveFill(38, 17)); + add(entities.caveFill(39, 18)); + add(entities.caveFill(43, 21)); + add(entities.caveFill(44, 22)); + add(entities.caveFill(46, 23)); + add(entities.caveFill(48, 23)); + add(entities.caveFill(50, 22)); + add(entities.caveFill(51, 21)); + add(entities.caveFill(52, 20)); + add(entities.caveFill(53, 19)); + add(entities.caveFill(54, 16)); + add(entities.caveFill(24, 9)); + add(entities.caveFill(25, 12)); + add(entities.caveFill(21, 27)); + + add(entities.trigger3x1(27.5, 55)).trigger = (_, client) => goToMap(world, client, '', 'cave'); + + add(entities.lanternOn(10.38, 18.38)); + add(entities.lanternOn(12.66, 18.38)); + add(entities.lanternOn(18.41, 12.00)); + add(entities.lanternOn(21.41, 12.13)); + add(entities.lanternOn(18.47, 14.21)); + add(entities.lanternOn(21.44, 14.33)); + add(entities.lanternOn(10.41, 11.54)); + add(entities.lanternOn(12.66, 11.50)); + add(entities.lanternOn(13.94, 4.67)); + add(entities.lanternOn(9.44, 4.67)); + add(entities.lanternOn(27.44, 31.54)); + add(entities.lanternOn(21.97, 26.50)); + add(entities.lanternOn(19.31, 21.79)); + add(entities.lanternOn(15.53, 24.17)); + add(entities.lanternOn(9.28, 25.63)); + add(entities.lanternOn(29.34, 17.29)); + add(entities.lanternOn(15.81, 40.42)); + add(entities.lanternOn(11.28, 43.50)); + add(entities.lanternOn(14.66, 48.75)); + add(entities.lanternOn(18.44, 48.33)); + add(entities.lanternOn(16.18, 45.04)); + add(entities.lanternOn(39.53, 41.91)); + add(entities.lanternOn(41.37, 37.41)); + add(entities.lanternOn(45.84, 34.13)); + add(entities.lanternOn(50.50, 38.17)); + add(entities.lanternOn(44.78, 41.12)); + add(entities.lanternOn(37.46, 27.12)); + add(entities.lanternOn(26.22, 51.79)); + add(entities.lanternOn(28.75, 51.75)); + add(entities.lanternOn(23.34, 42.08)); + add(entities.lanternOn(23.88, 45.33)); + add(entities.lanternOn(33.66, 40.20)); + add(entities.lanternOn(34.44, 45.17)); + add(entities.lanternOn(28.81, 44.25)); + add(entities.lanternOn(25.31, 20.50)); + add(entities.lanternOn(28.63, 28.38)); + add(entities.lanternOn(29.28, 36.92)); + add(entities.lanternOn(32.38, 31.38)); + add(entities.lanternOn(33.22, 22.83)); + add(entities.lanternOn(31.84, 49.83)); + add(entities.lanternOn(30.28, 48.91)); + add(entities.lanternOn(14.09, 8.63)); + add(entities.lanternOn(9.41, 8.54)); + + add(entities.waterRock1(8.63, 27.92)); + add(entities.waterRock3(8.75, 28.08)); + add(entities.waterRock6(29.59, 8.79)); + add(entities.waterRock8(29.66, 9.00)); + add(entities.waterRock5(31.81, 6.83)); + add(entities.waterRock4(31.41, 6.58)); + add(entities.waterRock1(32.34, 9.79)); + add(entities.waterRock1(46.56, 13.50)); + add(entities.waterRock10(46.25, 13.71)); + add(entities.waterRock11(49.63, 16.63)); + add(entities.waterRock9(46.50, 11.46)); + add(entities.waterRock4(46.38, 11.63)); + + add(entities.box(32.03, 49.13)); + + add(createBoxOfLanterns(30.97, 49.63)).interact = (_, client) => { + if (client.pony.options!.hold === entities.crystalHeld.type) { + holdItem(client.pony, entities.crystalLantern.type); + } else { + holdItem(client.pony, entities.lanternOn.type); + } + }; + + // top rooms + add(createBoxOfLanterns(9.34, 11.46)); + add(entities.crate1A(17.78, 11.21)); + add(entities.crate1A(18.84, 11.29)); + add(entities.crate1A(7.69, 4.13)); + add(entities.crate1A(8.72, 4.13)); + add(entities.crate1A(10.00, 4.17)); + add(entities.crate1A(8.06, 5.33)); + add(entities.crate1BHigh(8.31, 4.17)); + add(entities.crate1BHigh(18.25, 11.30)); + add(entities.barrel(15.31, 3.79)); + add(entities.barrel(14.66, 4.54)); + add(entities.barrel(15.28, 5.08)); + add(entities.barrel(14.50, 3.75)); + add(entities.barrel(15.34, 6.08)); + add(entities.barrel(19.78, 10.75)); + add(entities.barrel(20.56, 11.25)); + + function railsH(x: number, y: number, length: number) { + for (let i = 0; i < length; i++) { + add(entities.mineRailsH(x + i + 0.5, y)); + } + } + + function railsV(x: number, y: number, length: number) { + for (let i = 0; i < length; i++) { + add(entities.mineRailsV(x + 0.5, y - i)); + } + } + + railsV(30, 40, 8); + add(entities.mineRailsSE(30.5, 32)); + railsH(31, 32, 5); + add(entities.mineRailsEndRight(36.5, 32.5)); + + railsH(5, 46, 13); + add(entities.mineRailsNW(18.5, 46)); + railsV(18, 45, 1); + add(entities.mineRailsNSE(18.5, 44)); + railsH(19, 44, 7); + railsV(18, 43, 1); + add(entities.mineRailsSW(18.5, 42)); + + add(entities.mineCart(49, 40)); + add(entities.crystalsCartPile(49, 40)).interact = give(entities.crystalHeld.type); + + add(entities.mineCart(31.5, 46)); + add(entities.crystalsCartPile(31.5, 46)).interact = give(entities.crystalHeld.type); + + add(entities.mineRailsEndRight(50.5, 40.5)); + railsH(38, 40, 9); + add(entities.mineRailsNWE(47.5, 40)); + railsH(48, 40, 2); + add(entities.mineRailsSE(37.5, 40)); + add(entities.mineRailsNW(37.5, 41)); + railsH(27, 41, 3); + add(entities.mineRailsNWE(30.5, 41)); + railsH(31, 41, 6); + add(entities.mineRailsSE(26.5, 41)); + railsV(26, 43, 2); + add(entities.mineRailsNSW(26.5, 44)); + railsV(26, 45, 1); + add(entities.mineRailsNE(26.5, 46)); + railsH(28, 46, 5); + add(entities.mineRailsEndRight(33.5, 46.5)); + railsH(14, 42, 4); + add(entities.mineRailsEndLeft(13.5, 42.5)); + + railsV(47, 39, 8); + add(entities.mineRailsEndTop(47.5, 32)); + + add(entities.mineRailsSWE(27.5, 46)); + railsV(27, 55, 9); + + add(entities.wallMap(23.97, 36.96)); + add(entities.table3(24.00, 37.2916)); + add(entities.lanternOnTable(24.375, 37.30)); + add(entities.lanternOn(22.50, 38.33)); + add(entities.sandPileSmall(25.50, 40.33)); + add(entities.table3(37.81, 39.41)); + + add(entities.sandPileMedium(31.44, 42.67)); + add(entities.sandPileSmall(32.03, 43.54)); + add(entities.sandPileTiny(28.75, 45.33)); + add(entities.sandPileSmall(32.63, 47.50)); + add(entities.sandPileTiny(33.03, 47.96)); + add(entities.sandPileSmall(27.94, 45.25)); + add(entities.sandPileSmall(29.81, 48.00)); + add(entities.sandPileTiny(29.13, 48.38)); + add(entities.sandPileSmall(35.72, 42.46)); + add(entities.sandPileTiny(32.53, 42.42)); + add(entities.sandPileTinier(36.47, 42.71)); + add(entities.sandPileBig(50.00, 34.96)); + add(entities.sandPileMedium(49.16, 36.00)); + add(entities.sandPileSmall(44.13, 34.42)); + add(entities.sandPileMedium(17.88, 40.58)); + add(entities.sandPileSmall(18.69, 41.29)); + add(entities.sandPileSmall(15.63, 49.88)); + add(entities.sandPileTiny(16.31, 50.33)); + add(entities.sandPileTinier(17.63, 41.58)); + add(entities.sandPileSmall(7.19, 24.29)); + add(entities.sandPileTiny(6.69, 24.79)); + + add(entities.sandPileMedium(6.19, 46.92)); + add(entities.sandPileTiny(5.03, 47.25)); + add(entities.rockB(5.13, 46.46)); + add(entities.rockB(7.31, 47.33)); + add(entities.rock2B(5.00, 47.13)); + add(entities.rock3B(5.72, 46.54)); + add(entities.rock3B(7.91, 47.04)); + add(entities.rock2B(8.03, 45.75)); + add(entities.rock2B(6.72, 47.54)); + add(entities.rock2B(32.03, 49.38)); + add(entities.rock2B(50.63, 39.71)); + add(entities.rock3B(41.59, 37.13)); + add(entities.rock3B(32.13, 31.25)); + add(entities.rock3B(22.34, 22.08)); + add(entities.rock2B(22.91, 22.29)); + add(entities.rock3B(35.63, 4.17)); + add(entities.rock2B(44.66, 9.29)); + add(entities.rockB(7.47, 21.33)); + add(entities.rock2B(7.81, 21.71)); + + add(entities.caveCover(13.125, 11.625)); + add(entities.caveCover(14.125, 11.625)); + add(entities.caveCover(15.125, 11.625)); + add(entities.caveCover(13.125, 15.625)); + add(entities.caveCover(14.125, 15.625)); + add(entities.caveCover(15.125, 15.625)); + add(entities.caveCover(15.875, 15.625)); + add(entities.caveCover(16.875, 16.625)); + add(entities.caveCover(17.875, 16.625)); + add(entities.caveCover(18.875, 16.625)); + add(entities.caveCover(19.875, 16.625)); + add(entities.caveCover(20.875, 16.625)); + add(entities.caveCover(21.875, 16.625)); + add(entities.caveCover(22.875, 16.625)); + add(entities.caveCover(30.88, 27.625)); + add(entities.caveCover(31.88, 27.625)); + add(entities.caveCover(32.88, 27.625)); + add(entities.caveCover(38.125, 30.625)); + add(entities.caveCover(33.125, 34.625)); + add(entities.caveCover(34.03, 34.625)); + add(entities.caveCover(35.03, 34.625)); + add(entities.caveCover(36.03, 34.625)); + add(entities.caveCover(37.03, 34.625)); + add(entities.caveCover(38.03, 34.625)); + add(entities.caveCover(32.88, 35.17)); + + add(entities.crystals1(48.50, 10.42)); + add(entities.crystals1(42.59, 17.17)); + add(entities.crystals2(47.44, 14.42)); + add(entities.crystals3(52.06, 16.25)); + add(entities.crystals4(48.69, 18.96)); + add(entities.crystals5(45.56, 10.21)); + add(entities.crystals6(51.53, 11.58)); + add(entities.crystals7(45.53, 16.50)); + add(entities.crystals9(51.56, 18.71)); + add(entities.crystals10(45.59, 20.67)); + add(entities.crystals10(43.59, 13.33)); + add(entities.crystals3(40.63, 13.46)); + add(entities.crystals9(38.80, 10.04)); + add(entities.crystals8(37.34, 13.58)); + add(entities.crystals8(49.91, 15.21)); + add(entities.crystals1(33.41, 10.46)); + add(entities.crystals1(32.66, 4.33)); + add(entities.crystals2(34.38, 8.71)); + add(entities.crystals3(33.44, 14.67)); + add(entities.crystals3(27.47, 6.38)); + add(entities.crystals4(28.63, 5.79)); + add(entities.crystals5(28.69, 9.46)); + add(entities.crystals5(37.38, 9.79)); + add(entities.crystals6(28.22, 10.96)); + add(entities.crystals7(31.56, 11.75)); + add(entities.crystals8(34.81, 6.21)); + add(entities.crystals9(26.22, 8.04)); + add(entities.crystals10(36.91, 8.21)); + add(entities.crystals2(28.68, 14.54)); + add(entities.crystals8(28.63, 18.79)); + add(entities.crystals7(26.34, 23.71)); + add(entities.crystals6(23.22, 23.29)); + add(entities.crystals5(16.43, 22.29)); + add(entities.crystals3(8.59, 30.33)); + add(entities.crystals5(6.59, 27.58)); + add(entities.crystals2(11.625, 28.42)); + add(entities.crystals8(7.31, 22.54)); + add(entities.crystals4(8.44, 23.17)); + add(entities.crystals6(7.31, 29.83)); + add(entities.crystals1(5.44, 25.38)); + add(entities.crystals3(5.31, 6.50)); + add(entities.crystals7(6.41, 10.42)); + + add(entities.crystals5(49.56, 31.46)); + add(entities.crystals8(50.63, 33.33)); + add(entities.crystals1(47.19, 30.21)); + add(entities.crystals9(46.06, 32.21)); + add(entities.crystals8(17.28, 39.38)); + add(entities.crystals3(12.41, 39.33)); + add(entities.crystals6(11.31, 40.75)); + + add(entities.waterCrystal1(31.34, 7.13)); + add(entities.waterCrystal2(31.03, 6.92)); + add(entities.waterCrysta3(31.00, 7.29)); + add(entities.waterCrysta3(32.34, 10.00)); + add(entities.waterCrystal2(46.06, 13.29)); + add(entities.waterCrysta3(49.44, 16.29)); + add(entities.waterCrysta3(46.72, 11.67)); + add(entities.waterCrysta3(41.75, 12.54)); + add(entities.waterCrystal1(8.47, 27.46)); + add(entities.waterCrysta3(10.47, 27.54)); + + add(entities.stalactite3(17.06, 20.79)); + add(entities.stalactite3(22.25, 22.83)); + add(entities.stalactite2(21.81, 22.46)); + add(entities.stalactite1(17.41, 21.08)); + add(entities.stalactite1(28.31, 28.25)); + add(entities.stalactite2(13.69, 20.63)); + add(entities.stalactite3(6.56, 22.04)); + add(entities.stalactite3(7.38, 28.46)); + add(entities.stalactite1(7.66, 28.54)); + add(entities.stalactite2(6.28, 22.42)); + add(entities.stalactite2(9.69, 20.79)); + add(entities.stalactite1(10.06, 20.58)); + add(entities.stalactite2(12.25, 25.79)); + add(entities.stalactite1(11.91, 26.08)); + add(entities.stalactite3(11.88, 25.71)); + add(entities.stalactite2(31.25, 31.25)); + add(entities.stalactite3(22.72, 42.88)); + add(entities.stalactite1(23.06, 42.71)); + add(entities.stalactite3(10.59, 40.21)); + add(entities.stalactite2(12.19, 38.42)); + add(entities.stalactite1(12.63, 38.42)); + add(entities.stalactite3(47.25, 29.46)); + add(entities.stalactite2(44.34, 31.33)); + add(entities.stalactite1(47.69, 29.58)); + add(entities.stalactite1(51.75, 33.50)); + add(entities.stalactite2(26.31, 18.21)); + add(entities.stalactite3(28.22, 4.33)); + add(entities.stalactite3(37.69, 7.29)); + add(entities.stalactite2(39.44, 10.00)); + add(entities.stalactite2(28.75, 4.13)); + add(entities.stalactite1(27.47, 10.88)); + add(entities.stalactite1(37.28, 7.04)); + add(entities.stalactite3(49.88, 9.42)); + add(entities.stalactite3(50.63, 13.92)); + add(entities.stalactite3(42.84, 14.63)); + add(entities.stalactite3(50.78, 20.96)); + add(entities.stalactite3(44.09, 9.46)); + add(entities.stalactite2(43.16, 15.04)); + add(entities.stalactite2(49.44, 9.21)); + add(entities.stalactite2(46.00, 8.42)); + add(entities.stalactite2(51.47, 14.17)); + add(entities.stalactite1(51.06, 14.29)); + add(entities.stalactite1(42.59, 15.13)); + add(entities.stalactite1(44.31, 9.67)); + add(entities.stalactite1(51.78, 10.33)); + add(entities.stalactite1(50.44, 21.29)); + add(entities.stalactite1(47.41, 17.71)); + add(entities.stalactite2(47.06, 17.71)); + add(entities.stalactite1(28.66, 19.67)); + add(entities.stalactite3(4.34, 6.33)); + add(entities.stalactite2(4.56, 7.00)); + add(entities.stalactite1(5.56, 10.29)); + add(entities.stalactite3(35.38, 11.83)); + add(entities.stalactite2(35.28, 12.21)); + add(entities.stalactite1(34.94, 11.79)); + add(entities.stalactite3(15.03, 22.29)); + add(entities.stalactite1(14.63, 22.17)); + + // storage room + add(entities.table3(37.03, 23.71)); + add(entities.lanternOnTable(37.25, 23.75)); + add(entities.barrel(31.69, 24.71)); + add(entities.barrel(32.03, 25.50)); + add(entities.barrel(31.63, 26.17)); + add(entities.barrel(38.34, 25.75)); + add(entities.barrel(38.41, 27.04)); + add(entities.barrel(37.78, 26.38)); + add(entities.barrel(31.94, 26.96)); + add(entities.toolboxFull(34.28, 22.96)).interact = give(entities.pickaxe.type); + add(entities.box(38.22, 29.96)); + add(entities.ropeRack(34.31, 22.20)).interact = give(entities.rope.type); + add(entities.boxLanterns(35.63, 24.04)).interact = give(entities.lanternOn.type); + add(entities.crate1A(38.19, 28.79)); + add(entities.crate1A(34.81, 28.58)); + add(entities.crate1A(34.72, 29.88)); + add(entities.crate2A(34.81, 30.67)); + + add(entities.ropeRack(34.31, 39.91)).interact = give(entities.rope.type); + add(entities.crate1A(34.22, 43.38)); + add(entities.crate1A(33.91, 44.50)); + + function placeMineCart(x: number, y: number) { + add(entities.mineCartBack(x + 0.22, y - 0.04)); + add(entities.mineCartFront(x + 0.22, y + 0.83)); + } + + placeMineCart(41, 40); + placeMineCart(45, 40); + placeMineCart(16, 42); + placeMineCart(8, 46); + placeMineCart(12, 46); + placeMineCart(15, 46); + placeMineCart(28, 41); + + map.controllers.push(new TorchController(world, map)); + map.controllers.push(new FlyingCritterController(world, map, entities.bat, 2, 10, () => true, true)); + + const wallController = new WallController(world, map, entities.stoneWalls); + map.controllers.push(wallController); + wallController.top = 3; + wallController.isTall = (x, y) => { + if (y === 10 && x >= 17 && x <= 23) + return true; + + if (x >= 31 && x <= 39 && y >= 22 && y <= 25) + return true; + + if (y === 31 && x >= 33 && x <= 34) + return true; + + return false; + }; + + if (wallController.toggleWall) { + // large crypt + for (let x = 7; x <= 15; x++) { + wallController.toggleWall(x, 3, TileType.WallH); + } + + for (let y = 3; y <= 10; y++) { + wallController.toggleWall(16, y, TileType.WallV); + } + + for (let x = 13; x <= 15; x++) { + wallController.toggleWall(x, 11, TileType.WallH); + } + + wallController.toggleWall(13, 11, TileType.WallV); + + for (let x = 13; x <= 16; x++) { + wallController.toggleWall(x, 12, TileType.WallH); + } + + wallController.toggleWall(17, 10, TileType.WallV); + wallController.toggleWall(17, 11, TileType.WallV); + + for (let x = 17; x <= 22; x++) { + wallController.toggleWall(x, 10, TileType.WallH); + } + + for (let y = 10; y <= 15; y++) { + wallController.toggleWall(23, y, TileType.WallV); + } + + for (let x = 17; x <= 22; x++) { + wallController.toggleWall(x, 16, TileType.WallH); + } + + wallController.toggleWall(17, 15, TileType.WallV); + + for (let x = 13; x <= 16; x++) { + wallController.toggleWall(x, 15, TileType.WallH); + } + + // small crypt + wallController.toggleWall(33, 31, TileType.WallH); + wallController.toggleWall(34, 30, TileType.WallV); + wallController.toggleWall(34, 29, TileType.WallV); + wallController.toggleWall(34, 28, TileType.WallV); + wallController.toggleWall(34, 27, TileType.WallV); + wallController.toggleWall(33, 27, TileType.WallH); + wallController.toggleWall(32, 27, TileType.WallH); + wallController.toggleWall(31, 27, TileType.WallH); + wallController.toggleWall(31, 26, TileType.WallV); + wallController.toggleWall(31, 25, TileType.WallV); + wallController.toggleWall(31, 24, TileType.WallV); + wallController.toggleWall(31, 24, TileType.WallH); + wallController.toggleWall(32, 23, TileType.WallV); + wallController.toggleWall(32, 22, TileType.WallV); + wallController.toggleWall(32, 22, TileType.WallH); + wallController.toggleWall(33, 22, TileType.WallH); + wallController.toggleWall(34, 22, TileType.WallH); + wallController.toggleWall(35, 22, TileType.WallV); + wallController.toggleWall(35, 23, TileType.WallH); + wallController.toggleWall(36, 23, TileType.WallH); + wallController.toggleWall(37, 23, TileType.WallH); + wallController.toggleWall(38, 23, TileType.WallV); + wallController.toggleWall(38, 24, TileType.WallV); + wallController.toggleWall(38, 25, TileType.WallH); + wallController.toggleWall(39, 25, TileType.WallV); + wallController.toggleWall(39, 26, TileType.WallV); + wallController.toggleWall(39, 27, TileType.WallV); + wallController.toggleWall(39, 28, TileType.WallV); + wallController.toggleWall(39, 29, TileType.WallV); + wallController.toggleWall(38, 30, TileType.WallH); + wallController.toggleWall(38, 30, TileType.WallV); + wallController.toggleWall(38, 31, TileType.WallV); + wallController.toggleWall(38, 32, TileType.WallV); + wallController.toggleWall(38, 33, TileType.WallV); + wallController.toggleWall(37, 34, TileType.WallH); + wallController.toggleWall(36, 34, TileType.WallH); + wallController.toggleWall(35, 34, TileType.WallH); + wallController.toggleWall(34, 34, TileType.WallH); + wallController.toggleWall(33, 34, TileType.WallH); + wallController.toggleWall(33, 34, TileType.WallV); + } + + if (DEVELOPMENT) { + addSpawnPointIndicators(world, map); + } + + return map; } diff --git a/src/ts/server/maps/customMap.ts b/src/ts/server/maps/customMap.ts index 9b9d050..60bf131 100644 --- a/src/ts/server/maps/customMap.ts +++ b/src/ts/server/maps/customMap.ts @@ -15,28 +15,28 @@ import { pathTo } from '../paths'; const mapData = JSON.parse(fs.readFileSync(pathTo('src', 'maps', 'custom.json'), 'utf8')); export function createCustomMap(world: World) { - // size: 4 by 4 regions -> 32 by 32 tiles - // default tiles: grass - const map = createServerMap('custom', MapType.None, 4, 4, TileType.Grass); + // size: 4 by 4 regions -> 32 by 32 tiles + // default tiles: grass + const map = createServerMap('custom', MapType.None, 4, 4, TileType.Grass); - // initialize tiles - deserializeMap(map, mapData); + // initialize tiles + deserializeMap(map, mapData); - // place default spawn point at the center of the map - map.spawnArea = rect(map.width / 2, map.height / 2, 0, 0); + // place default spawn point at the center of the map + map.spawnArea = rect(map.width / 2, map.height / 2, 0, 0); - // shorthand for adding entities - function add(entity: ServerEntity) { - world.addEntity(entity, map); - } + // shorthand for adding entities + function add(entity: ServerEntity) { + world.addEntity(entity, map); + } - // place return sign 2 tiles north of center of the map - add(createSign(map.width / 2, map.height / 2 - 2, 'Go back', (_, client) => goToMap(world, client, '', 'center'))); + // place return sign 2 tiles north of center of the map + add(createSign(map.width / 2, map.height / 2 - 2, 'Go back', (_, client) => goToMap(world, client, '', 'center'))); - // place barrel at 5, 5 location - add(entities.barrel(5, 5)); + // place barrel at 5, 5 location + add(entities.barrel(5, 5)); - // place more entities here ... + // place more entities here ... - return map; + return map; } diff --git a/src/ts/server/maps/houseMap.ts b/src/ts/server/maps/houseMap.ts index 142ab79..a6fb3e7 100644 --- a/src/ts/server/maps/houseMap.ts +++ b/src/ts/server/maps/houseMap.ts @@ -16,174 +16,174 @@ const toolboxX = 2.125; const toolboxY = 15.41; export function createHouseMap(world: World, instanced: boolean, _template = false): ServerMap { - const map = createServerMap('house', MapType.House, 2, 2, TileType.Wood, instanced ? MapUsage.Party : MapUsage.Public); + const map = createServerMap('house', MapType.House, 2, 2, TileType.Wood, instanced ? MapUsage.Party : MapUsage.Public); - if (getTile(map, 0, 0) !== TileType.None) { - for (let x = 0; x < map.width; x++) { - setTile(map, x, 0, TileType.None); - setTile(map, x, 1, TileType.None); - setTile(map, x, 2, TileType.None); - } - } + if (getTile(map, 0, 0) !== TileType.None) { + for (let x = 0; x < map.width; x++) { + setTile(map, x, 0, TileType.None); + setTile(map, x, 1, TileType.None); + setTile(map, x, 2, TileType.None); + } + } - setTile(map, 4, map.height - 1, TileType.Stone); - setTile(map, 5, map.height - 1, TileType.Stone); + setTile(map, 4, map.height - 1, TileType.Stone); + setTile(map, 5, map.height - 1, TileType.Stone); - map.usage = instanced ? MapUsage.Party : MapUsage.Public; - map.spawnArea = rect(4, 8 + 6, 2, 1); - map.defaultTile = TileType.None; - map.flags |= MapFlags.EditableWalls | MapFlags.EditableEntities | MapFlags.EditableTiles; - map.editableEntityLimit = HOUSE_ENTITY_LIMIT; - map.editableArea = rect(0, 76 / tileHeight, map.width, map.height); + map.usage = instanced ? MapUsage.Party : MapUsage.Public; + map.spawnArea = rect(4, 8 + 6, 2, 1); + map.defaultTile = TileType.None; + map.flags |= MapFlags.EditableWalls | MapFlags.EditableEntities | MapFlags.EditableTiles; + map.editableEntityLimit = HOUSE_ENTITY_LIMIT; + map.editableArea = rect(0, 76 / tileHeight, map.width, map.height); - const topWall = 3; - const windowY = 76 / tileHeight; + const topWall = 3; + const windowY = 76 / tileHeight; - const add = (entity: ServerEntity) => world.addEntity(entity, map); - const addEditable = (entity: ServerEntity) => (entity.state |= EntityState.Editable, add(entity)); + const add = (entity: ServerEntity) => world.addEntity(entity, map); + const addEditable = (entity: ServerEntity) => (entity.state |= EntityState.Editable, add(entity)); - add(entities.triggerDoor(5, map.height)) - .trigger = (_, client) => goToMap(world, client, instanced ? 'island' : 'public-island', 'house'); + add(entities.triggerDoor(5, map.height)) + .trigger = (_, client) => goToMap(world, client, instanced ? 'island' : 'public-island', 'house'); - addEditable(entities.window1(2, windowY)); - addEditable(entities.window1(5, windowY)); - addEditable(entities.window1(8, windowY)); - addEditable(entities.window1(12, windowY)); - addEditable(entities.window1(14, windowY)); + addEditable(entities.window1(2, windowY)); + addEditable(entities.window1(5, windowY)); + addEditable(entities.window1(8, windowY)); + addEditable(entities.window1(12, windowY)); + addEditable(entities.window1(14, windowY)); - addEditable(entities.picture1(3.53, windowY)); - addEditable(entities.picture2(10.09, windowY)); + addEditable(entities.picture1(3.53, windowY)); + addEditable(entities.picture2(10.09, windowY)); - addEditable(entities.table1(13.50, 13.20)); - addEditable(entities.table1(2.69, 5.63)); - addEditable(entities.table2(8.69, 11.60)); + addEditable(entities.table1(13.50, 13.20)); + addEditable(entities.table1(2.69, 5.63)); + addEditable(entities.table2(8.69, 11.60)); - addEditable(entities.lanternOn(3.53, 15.13)); - addEditable(entities.lanternOn(6.56, 15.13)); - addEditable(entities.lanternOn(1.43, 14.04)); - addEditable(entities.lanternOn(9.97, 6.67)); - addEditable(entities.lanternOn(13.50, 6.54)); - addEditable(entities.lanternOn(10.09, 15.13)); - addEditable(entities.lanternOn(14.09, 15.04)); - addEditable(entities.lanternOn(9.44, 4.67)); - addEditable(entities.lanternOn(14.43, 8.54)); + addEditable(entities.lanternOn(3.53, 15.13)); + addEditable(entities.lanternOn(6.56, 15.13)); + addEditable(entities.lanternOn(1.43, 14.04)); + addEditable(entities.lanternOn(9.97, 6.67)); + addEditable(entities.lanternOn(13.50, 6.54)); + addEditable(entities.lanternOn(10.09, 15.13)); + addEditable(entities.lanternOn(14.09, 15.04)); + addEditable(entities.lanternOn(9.44, 4.67)); + addEditable(entities.lanternOn(14.43, 8.54)); - addEditable(entities.lanternOnWall(8.375, 12.70)); - addEditable(entities.lanternOnWall(9.09, 12.08)); - addEditable(entities.lanternOnWall(13.50, 13.33)); - addEditable(entities.lanternOnWall(2.69, 5.71)); + addEditable(entities.lanternOnWall(8.375, 12.70)); + addEditable(entities.lanternOnWall(9.09, 12.08)); + addEditable(entities.lanternOnWall(13.50, 13.33)); + addEditable(entities.lanternOnWall(2.69, 5.71)); - addEditable(entities.cushion1(3.94, 4.83)); - addEditable(entities.cushion1(1.56, 4.63)); - addEditable(entities.cushion1(6.91, 10.54)); - addEditable(entities.cushion1(10.38, 11.96)); - addEditable(entities.cushion1(10.56, 10.54)); - addEditable(entities.cushion1(6.91, 12.17)); - addEditable(entities.cushion1(8.88, 13.08)); - addEditable(entities.cushion1(8.84, 9.67)); - addEditable(entities.cushion1(13.25, 4.75)); - addEditable(entities.cushion1(14.50, 3.67)); - addEditable(entities.cushion1(15.38, 5.00)); - addEditable(entities.cushion1(14.13, 6.17)); - addEditable(entities.cushion1(14.69, 12.25)); - addEditable(entities.cushion1(12.31, 12.29)); - addEditable(entities.cushion1(9.94, 5.00)); - addEditable(entities.cushion1(8.03, 5.00)); + addEditable(entities.cushion1(3.94, 4.83)); + addEditable(entities.cushion1(1.56, 4.63)); + addEditable(entities.cushion1(6.91, 10.54)); + addEditable(entities.cushion1(10.38, 11.96)); + addEditable(entities.cushion1(10.56, 10.54)); + addEditable(entities.cushion1(6.91, 12.17)); + addEditable(entities.cushion1(8.88, 13.08)); + addEditable(entities.cushion1(8.84, 9.67)); + addEditable(entities.cushion1(13.25, 4.75)); + addEditable(entities.cushion1(14.50, 3.67)); + addEditable(entities.cushion1(15.38, 5.00)); + addEditable(entities.cushion1(14.13, 6.17)); + addEditable(entities.cushion1(14.69, 12.25)); + addEditable(entities.cushion1(12.31, 12.29)); + addEditable(entities.cushion1(9.94, 5.00)); + addEditable(entities.cushion1(8.03, 5.00)); - addEditable(entities.boxLanterns(0.72, 15.58)); - addEditable(entities.toolboxFull(toolboxX, toolboxY)); + addEditable(entities.boxLanterns(0.72, 15.58)); + addEditable(entities.toolboxFull(toolboxX, toolboxY)); - const wallController = new WallController(world, map, entities.woodenWalls); - map.controllers.push(wallController); - wallController.top = 3; + const wallController = new WallController(world, map, entities.woodenWalls); + map.controllers.push(wallController); + wallController.top = 3; - if (wallController.toggleWall) { - for (let x = 0; x < map.width; x++) { - wallController.toggleWall(x, topWall, TileType.WallH); + if (wallController.toggleWall) { + for (let x = 0; x < map.width; x++) { + wallController.toggleWall(x, topWall, TileType.WallH); - if (x !== 4 && x !== 5) { - wallController.toggleWall(x, map.height, TileType.WallH); - } + if (x !== 4 && x !== 5) { + wallController.toggleWall(x, map.height, TileType.WallH); + } - if (x !== 5 && x !== 8 && x !== 12) { - wallController.toggleWall(x, 8, TileType.WallH); - } - } + if (x !== 5 && x !== 8 && x !== 12) { + wallController.toggleWall(x, 8, TileType.WallH); + } + } - for (let x = 0; x < 3; x++) { - wallController.toggleWall(x, 13, TileType.WallH); - } + for (let x = 0; x < 3; x++) { + wallController.toggleWall(x, 13, TileType.WallH); + } - for (let y = topWall; y < 8; y++) { - wallController.toggleWall(7, y, TileType.WallV); - wallController.toggleWall(11, y, TileType.WallV); - } + for (let y = topWall; y < 8; y++) { + wallController.toggleWall(7, y, TileType.WallV); + wallController.toggleWall(11, y, TileType.WallV); + } - for (let y = 8; y < map.height; y++) { - if (y !== 11 && y !== 14) { - wallController.toggleWall(3, y, TileType.WallV); - } - } + for (let y = 8; y < map.height; y++) { + if (y !== 11 && y !== 14) { + wallController.toggleWall(3, y, TileType.WallV); + } + } - for (let y = topWall; y < map.height; y++) { - wallController.toggleWall(0, y, TileType.WallV); - wallController.toggleWall(map.width, y, TileType.WallV); - } - } + for (let y = topWall; y < map.height; y++) { + wallController.toggleWall(0, y, TileType.WallV); + wallController.toggleWall(map.width, y, TileType.WallV); + } + } - wallController.lockOuterWalls = true; + wallController.lockOuterWalls = true; - if (DEVELOPMENT) { - addSpawnPointIndicators(world, map); - } + if (DEVELOPMENT) { + addSpawnPointIndicators(world, map); + } - for (const region of map.regions) { - resetRegionUpdates(region); - } + for (const region of map.regions) { + resetRegionUpdates(region); + } - if (!defaultHouseSave) { - defaultHouseSave = saveMap(map, { - saveTiles: true, saveEntities: true, saveOnlyEditableEntities: true, saveWalls: true - }); - } + if (!defaultHouseSave) { + defaultHouseSave = saveMap(map, { + saveTiles: true, saveEntities: true, saveOnlyEditableEntities: true, saveWalls: true + }); + } - return map; + return map; } export function resetHouseMap(map: ServerMap) { - for (const { tiles } of map.regions) { - for (let i = 0; i < tiles.length; i++) { - tiles[i] = TileType.Wood; - } - } + for (const { tiles } of map.regions) { + for (let i = 0; i < tiles.length; i++) { + tiles[i] = TileType.Wood; + } + } } function findEntityByType(map: ServerMap, type: number) { - for (const region of map.regions) { - for (const entity of region.entities) { - if (entity.type === type) { - return entity; - } - } - } + for (const region of map.regions) { + for (const entity of region.entities) { + if (entity.type === type) { + return entity; + } + } + } - return undefined; + return undefined; } export function removeToolbox(world: World, map: ServerMap) { - const toolbox = findEntityByType(map, entities.toolboxFull.type); + const toolbox = findEntityByType(map, entities.toolboxFull.type); - if (toolbox) { - world.removeEntity(toolbox, map); - } + if (toolbox) { + world.removeEntity(toolbox, map); + } } export function restoreToolbox(world: World, map: ServerMap) { - const toolbox = findEntityByType(map, entities.toolboxFull.type); + const toolbox = findEntityByType(map, entities.toolboxFull.type); - if (!toolbox) { - const entity = entities.toolboxFull(toolboxX, toolboxY); - entity.state |= EntityState.Editable; - world.addEntity(entity, map); - } + if (!toolbox) { + const entity = entities.toolboxFull(toolboxX, toolboxY); + entity.state |= EntityState.Editable; + world.addEntity(entity, map); + } } diff --git a/src/ts/server/maps/islandMap.ts b/src/ts/server/maps/islandMap.ts index 2b17869..de3ba28 100644 --- a/src/ts/server/maps/islandMap.ts +++ b/src/ts/server/maps/islandMap.ts @@ -5,7 +5,7 @@ import { pathTo } from '../paths'; import { ServerMap, MapUsage, ServerEntity } from '../serverInterfaces'; import { World, goToMap } from '../world'; import { - addSpawnPointIndicators, generateTileIndicesAndColliders, removePonies, worldForTemplates, createBunny + addSpawnPointIndicators, generateTileIndicesAndColliders, removePonies, worldForTemplates, createBunny } from '../mapUtils'; import { serverMapInstanceFromTemplate, createServerMap, copyMapTiles, deserializeMap } from '../serverMap'; import { TileType, MapType, Season } from '../../common/interfaces'; @@ -23,364 +23,364 @@ const islandMapData = JSON.parse(fs.readFileSync(pathTo('src', 'maps', 'island.j let islandMapTemplate: ServerMap | undefined; export function createIslandMap(world: World, instanced: boolean, template = false): ServerMap { - if (!template && !islandMapTemplate) { - islandMapTemplate = createIslandMap(worldForTemplates, false, true); - } + if (!template && !islandMapTemplate) { + islandMapTemplate = createIslandMap(worldForTemplates, false, true); + } - const map = (instanced && islandMapTemplate) ? - serverMapInstanceFromTemplate(islandMapTemplate) : - createServerMap('island', MapType.Island, 7, 7, TileType.Water, instanced ? MapUsage.Party : MapUsage.Public); + const map = (instanced && islandMapTemplate) ? + serverMapInstanceFromTemplate(islandMapTemplate) : + createServerMap('island', MapType.Island, 7, 7, TileType.Water, instanced ? MapUsage.Party : MapUsage.Public); - map.usage = instanced ? MapUsage.Party : MapUsage.Public; - map.spawnArea = rect(43.4, 20, 3.3, 2.5); - map.spawns.set('house', rect(27, 24, 2, 2)); + map.usage = instanced ? MapUsage.Party : MapUsage.Public; + map.spawnArea = rect(43.4, 20, 3.3, 2.5); + map.spawns.set('house', rect(27, 24, 2, 2)); - if (islandMapTemplate) { - copyMapTiles(map, islandMapTemplate); - } else { - deserializeMap(map, islandMapData); - } + if (islandMapTemplate) { + copyMapTiles(map, islandMapTemplate); + } else { + deserializeMap(map, islandMapData); + } - const goto = instanced ? 'house' : 'public-house'; - const add = (entity: ServerEntity) => world.addEntity(entity, map); - const addEntities = (entities: ServerEntity[]) => entities.map(add); + const goto = instanced ? 'house' : 'public-house'; + const add = (entity: ServerEntity) => world.addEntity(entity, map); + const addEntities = (entities: ServerEntity[]) => entities.map(add); - add(entities.house(28, 23)).interact = (_, client) => goToMap(world, client, goto); - add(entities.triggerHouseDoor(27.40, 22.87)).trigger = (_, client) => goToMap(world, client, goto); + add(entities.house(28, 23)).interact = (_, client) => goToMap(world, client, goto); + add(entities.triggerHouseDoor(27.40, 22.87)).trigger = (_, client) => goToMap(world, client, goto); - add(createBoxOfLanterns(25.5, 25.5)); + add(createBoxOfLanterns(25.5, 25.5)); - const boxOfFruits = add(entities.boxFruits(20.72, 20.88)); - setEntityName(boxOfFruits, 'Box of fruits'); + const boxOfFruits = add(entities.boxFruits(20.72, 20.88)); + setEntityName(boxOfFruits, 'Box of fruits'); - const giftPile = add(entities.giftPileInteractive(37.66, 18.21)); - giftPile.interact = (_, client) => updateEntityOptions(client.pony, getNextToyOrExtra(client)); - setEntityName(giftPile, 'Toy stash'); + const giftPile = add(entities.giftPileInteractive(37.66, 18.21)); + giftPile.interact = (_, client) => updateEntityOptions(client.pony, getNextToyOrExtra(client)); + setEntityName(giftPile, 'Toy stash'); - const types = entities.stashEntities.map(e => e.type); + const types = entities.stashEntities.map(e => e.type); - const itemSign = add(entities.signQuest(24.41, 25.00)); - itemSign.interact = (_, client) => { - const index = types.indexOf(client.pony.options!.hold || 0); - holdItem(client.pony, types[(index + 1) % types.length]); - }; - setEntityName(itemSign, 'Item stash'); + const itemSign = add(entities.signQuest(24.41, 25.00)); + itemSign.interact = (_, client) => { + const index = types.indexOf(client.pony.options!.hold || 0); + holdItem(client.pony, types[(index + 1) % types.length]); + }; + setEntityName(itemSign, 'Item stash'); - const addTorch = createAddLight(world, map, entities.torch); + const addTorch = createAddLight(world, map, entities.torch); - addTorch(25.00, 24.00); - addTorch(39.69, 18.38); - addTorch(39.66, 21.67); - addTorch(23.34, 38.46); - addTorch(28.16, 31.79); - addTorch(29.13, 38.17); - addTorch(30.84, 29.42); - addTorch(29.69, 25.00); - addTorch(20.00, 27.88); - addTorch(22.63, 30.54); - addTorch(19.22, 32.08); - addTorch(25.50, 26.79); - addTorch(15.94, 26.42); - addTorch(15.88, 29.75); + addTorch(25.00, 24.00); + addTorch(39.69, 18.38); + addTorch(39.66, 21.67); + addTorch(23.34, 38.46); + addTorch(28.16, 31.79); + addTorch(29.13, 38.17); + addTorch(30.84, 29.42); + addTorch(29.69, 25.00); + addTorch(20.00, 27.88); + addTorch(22.63, 30.54); + addTorch(19.22, 32.08); + addTorch(25.50, 26.79); + addTorch(15.94, 26.42); + addTorch(15.88, 29.75); - // pier + // pier - const px = 1 / tileWidth; - const plankWidth = 78 / tileWidth; - const plankHeight = 12 / tileHeight; - const plankOffsets = [0, -1, 0, -2, -1, -1, 0, -2, -1, 0].map(x => x / tileWidth); + const px = 1 / tileWidth; + const plankWidth = 78 / tileWidth; + const plankHeight = 12 / tileHeight; + const plankOffsets = [0, -1, 0, -2, -1, -1, 0, -2, -1, 0].map(x => x / tileWidth); - addEntities(entities.fullBoat(45.06, 24)); - add(entities.pierLeg(41.31, 20.54)); - add(entities.pierLeg(43.00, 22.58)); - add(entities.pierLeg(44.84, 22.58)); - add(entities.pierLeg(46.65, 22.58)); - add(entities.barrel(46.83, 23.35)); - add(entities.lanternOn(46.19, 23.38)); - add(entities.lanternOn(42.63, 21.42)); - add(entities.lanternOn(47.00, 18.96)); - add(entities.triggerBoat(45.5, 24.8)).interact = (_, client) => goToMap(world, client, '', 'harbor'); - add(createSignWithText(43, 23.5, 'Return to land', `Hop on the boat to return to the mainland`)); + addEntities(entities.fullBoat(45.06, 24)); + add(entities.pierLeg(41.31, 20.54)); + add(entities.pierLeg(43.00, 22.58)); + add(entities.pierLeg(44.84, 22.58)); + add(entities.pierLeg(46.65, 22.58)); + add(entities.barrel(46.83, 23.35)); + add(entities.lanternOn(46.19, 23.38)); + add(entities.lanternOn(42.63, 21.42)); + add(entities.lanternOn(47.00, 18.96)); + add(entities.triggerBoat(45.5, 24.8)).interact = (_, client) => goToMap(world, client, '', 'harbor'); + add(createSignWithText(43, 23.5, 'Return to land', `Hop on the boat to return to the mainland`)); - for (let y = 0; y < 10; y++) { - const minX = y < 5 ? 0 : 1; - const maxX = (y % 2) ? 4 : 3; - const baseX = 40 + ((y % 2) ? 0 : (plankWidth / 2)) + plankOffsets[y]; - const baseY = 19 - (9 / tileHeight); + for (let y = 0; y < 10; y++) { + const minX = y < 5 ? 0 : 1; + const maxX = (y % 2) ? 4 : 3; + const baseX = 40 + ((y % 2) ? 0 : (plankWidth / 2)) + plankOffsets[y]; + const baseY = 19 - (9 / tileHeight); - for (let x = minX; x < maxX; x++) { - if ((x === minX && (y % 2)) || (x === (maxX - 1) && (y % 2))) { - const ox = x === minX ? (18 / tileWidth) : (-18 / tileWidth); - const plank = sample(entities.planksShort)!; - add(plank(baseX + ox + x * plankWidth, baseY + y * plankHeight)); - } else { - const plank = sample(entities.planks)!; - add(plank(baseX + x * plankWidth, baseY + y * plankHeight)); - } - } - } + for (let x = minX; x < maxX; x++) { + if ((x === minX && (y % 2)) || (x === (maxX - 1) && (y % 2))) { + const ox = x === minX ? (18 / tileWidth) : (-18 / tileWidth); + const plank = sample(entities.planksShort)!; + add(plank(baseX + ox + x * plankWidth, baseY + y * plankHeight)); + } else { + const plank = sample(entities.planks)!; + add(plank(baseX + x * plankWidth, baseY + y * plankHeight)); + } + } + } - add(entities.plankShadow(41.31, 20.58)); - add(entities.plankShadowShort(43.03, 21.08)); - add(entities.plankShadowShort(43, 21.08 + plankHeight * 2)); + add(entities.plankShadow(41.31, 20.58)); + add(entities.plankShadowShort(43.03, 21.08)); + add(entities.plankShadowShort(43, 21.08 + plankHeight * 2)); - const baseY = 18.58; - const baseX = 46.71; - add(entities.plankShadowShort(baseX, baseY)); - add(entities.plankShadowShort(baseX + 2 * px, baseY + plankHeight)); - add(entities.plankShadowShort(baseX, baseY + plankHeight * 2)); - add(entities.plankShadowShort(baseX + 1 * px, baseY + plankHeight * 3)); - add(entities.plankShadowShort(baseX - 1 * px, baseY + plankHeight * 4)); - add(entities.plankShadowShort(baseX + 2 * px, baseY + plankHeight * 5)); - add(entities.plankShadowShort(baseX, baseY + plankHeight * 6)); - add(entities.plankShadowShort(baseX + 1 * px, baseY + plankHeight * 7)); - add(entities.plankShadowShort(baseX - 1 * px, baseY + plankHeight * 8)); - add(entities.plankShadowShort(baseX + 3 * px, baseY + plankHeight * 9)); + const baseY = 18.58; + const baseX = 46.71; + add(entities.plankShadowShort(baseX, baseY)); + add(entities.plankShadowShort(baseX + 2 * px, baseY + plankHeight)); + add(entities.plankShadowShort(baseX, baseY + plankHeight * 2)); + add(entities.plankShadowShort(baseX + 1 * px, baseY + plankHeight * 3)); + add(entities.plankShadowShort(baseX - 1 * px, baseY + plankHeight * 4)); + add(entities.plankShadowShort(baseX + 2 * px, baseY + plankHeight * 5)); + add(entities.plankShadowShort(baseX, baseY + plankHeight * 6)); + add(entities.plankShadowShort(baseX + 1 * px, baseY + plankHeight * 7)); + add(entities.plankShadowShort(baseX - 1 * px, baseY + plankHeight * 8)); + add(entities.plankShadowShort(baseX + 3 * px, baseY + plankHeight * 9)); - add(entities.collider3x1(40, 18)); - add(entities.collider3x1(43, 18)); - add(entities.collider2x1(46, 18)); - add(entities.collider1x3(47, 19)); - add(entities.collider1x3(47, 22)); + add(entities.collider3x1(40, 18)); + add(entities.collider3x1(43, 18)); + add(entities.collider2x1(46, 18)); + add(entities.collider1x3(47, 19)); + add(entities.collider1x3(47, 22)); - add(entities.collider3x1(40, 21)); - add(entities.collider1x3(42, 22)); - add(entities.collider1x1(43, 24)); - add(entities.collider3x1(42, 25)); - add(entities.collider3x1(45, 25)); - add(entities.collider1x3(47.6, 23)); - add(entities.collider1x3(41.5, 22)); + add(entities.collider3x1(40, 21)); + add(entities.collider1x3(42, 22)); + add(entities.collider1x1(43, 24)); + add(entities.collider3x1(42, 25)); + add(entities.collider3x1(45, 25)); + add(entities.collider1x3(47.6, 23)); + add(entities.collider1x3(41.5, 22)); - // lone boat + // lone boat - addEntities(entities.fullBoat(27.12, 42, false)); - add(entities.plankShort3(27.43, 39.67)); - add(entities.plankShort3(27.46, 40.21)); - add(entities.plankShort3(27.43, 40.75)); - add(entities.plankShort3(27.46, 41.21)); - add(entities.pierLeg(27.96, 40.88)); - add(entities.pierLeg(27.02, 40.79)); - add(entities.plankShadowShort(27.5, 39.67)); - add(entities.plankShadowShort(27.5 + 1 * px, 39.67 + plankHeight)); - add(entities.plankShadowShort(27.5, 39.67 + plankHeight * 2)); - add(entities.plankShadowShort(27.5 + 1 * px, 39.67 + plankHeight * 3)); - add(entities.lanternOn(28.77, 42.27)); + addEntities(entities.fullBoat(27.12, 42, false)); + add(entities.plankShort3(27.43, 39.67)); + add(entities.plankShort3(27.46, 40.21)); + add(entities.plankShort3(27.43, 40.75)); + add(entities.plankShort3(27.46, 41.21)); + add(entities.pierLeg(27.96, 40.88)); + add(entities.pierLeg(27.02, 40.79)); + add(entities.plankShadowShort(27.5, 39.67)); + add(entities.plankShadowShort(27.5 + 1 * px, 39.67 + plankHeight)); + add(entities.plankShadowShort(27.5, 39.67 + plankHeight * 2)); + add(entities.plankShadowShort(27.5 + 1 * px, 39.67 + plankHeight * 3)); + add(entities.lanternOn(28.77, 42.27)); - add(entities.collider3x1(24, 41)); - add(entities.collider2x1(24, 42)); - add(entities.collider1x2(26, 40)); - add(entities.collider1x2(28, 40)); - add(entities.collider2x1(28, 41)); - add(entities.collider3x1(24, 43)); - add(entities.collider3x1(27, 43)); - add(entities.collider1x1(29, 42)); - add(entities.collider1x3(29.7, 41)); - add(entities.collider1x3(23.5, 41)); + add(entities.collider3x1(24, 41)); + add(entities.collider2x1(24, 42)); + add(entities.collider1x2(26, 40)); + add(entities.collider1x2(28, 40)); + add(entities.collider2x1(28, 41)); + add(entities.collider3x1(24, 43)); + add(entities.collider3x1(27, 43)); + add(entities.collider1x1(29, 42)); + add(entities.collider1x3(29.7, 41)); + add(entities.collider1x3(23.5, 41)); - const addWoodenFence = createWoodenFenceMaker(world, map); + const addWoodenFence = createWoodenFenceMaker(world, map); - addWoodenFence(20, 20, 4); - addWoodenFence(20, 20, 4, false, true); - addWoodenFence(24, 20, 4, false, true); - addWoodenFence(20, 24, 1, true, true); - addWoodenFence(23, 24, 1, true, false, true); + addWoodenFence(20, 20, 4); + addWoodenFence(20, 20, 4, false, true); + addWoodenFence(24, 20, 4, false, true); + addWoodenFence(20, 24, 1, true, true); + addWoodenFence(23, 24, 1, true, false, true); - addEntities(entities.tree(22.41, 18.21, 0)); - addEntities(entities.tree(30.44, 23.46, 1 + 4)); - addEntities(entities.tree5(34.53, 27.04, 0)); - addEntities(entities.tree5(19.28, 21.75, 1)); - add(entities.pumpkin(23.22, 20.58)); - add(entities.pumpkin(20.53, 22.29)); - add(entities.pumpkin(20.91, 23.08)); - add(entities.largeLeafedBush2(35.03, 26.71)); - add(entities.largeLeafedBush4(34.13, 27.54)); - add(entities.largeLeafedBush2(20.34, 24.42)); - add(entities.largeLeafedBush3(19.72, 24.04)); - add(entities.largeLeafedBush3(23.03, 38.67)); - add(entities.largeLeafedBush4(23.72, 38.04)); - add(entities.barrel(39.53, 18.96)); - add(entities.barrel(38.78, 18.38)); - add(entities.barrel(38.47, 19.21)); - add(entities.barrel(24.63, 23.50)); - add(entities.treeStump1(20.25, 36.46)); - add(entities.lanternOn(20.75, 37.00)); - add(entities.lanternOn(36.06, 34.25)); - add(entities.lanternOn(37.63, 36.83)); - add(entities.lanternOn(37.84, 26.94)); + addEntities(entities.tree(22.41, 18.21, 0)); + addEntities(entities.tree(30.44, 23.46, 1 + 4)); + addEntities(entities.tree5(34.53, 27.04, 0)); + addEntities(entities.tree5(19.28, 21.75, 1)); + add(entities.pumpkin(23.22, 20.58)); + add(entities.pumpkin(20.53, 22.29)); + add(entities.pumpkin(20.91, 23.08)); + add(entities.largeLeafedBush2(35.03, 26.71)); + add(entities.largeLeafedBush4(34.13, 27.54)); + add(entities.largeLeafedBush2(20.34, 24.42)); + add(entities.largeLeafedBush3(19.72, 24.04)); + add(entities.largeLeafedBush3(23.03, 38.67)); + add(entities.largeLeafedBush4(23.72, 38.04)); + add(entities.barrel(39.53, 18.96)); + add(entities.barrel(38.78, 18.38)); + add(entities.barrel(38.47, 19.21)); + add(entities.barrel(24.63, 23.50)); + add(entities.treeStump1(20.25, 36.46)); + add(entities.lanternOn(20.75, 37.00)); + add(entities.lanternOn(36.06, 34.25)); + add(entities.lanternOn(37.63, 36.83)); + add(entities.lanternOn(37.84, 26.94)); - add(entities.waterRock1(22.63, 41.83)); - add(entities.waterRock1(28.56, 13.75)); - add(entities.waterRock1(16.25, 20.88)); - add(entities.waterRock1(40.62, 29.62)); - add(entities.waterRock1(40.63, 17.96)); - add(entities.waterRock2(40.22, 17.50)); - add(entities.waterRock2(20.34, 15.75)); - add(entities.waterRock2(15.41, 34.13)); - add(entities.waterRock2(36.50, 40.63)); - add(entities.waterRock3(36.13, 40.96)); - add(entities.waterRock3(41.63, 34.38)); - add(entities.waterRock3(15.16, 34.67)); - add(entities.waterRock3(20.88, 15.50)); - add(entities.waterRock4(36.56, 41.13)); - add(entities.waterRock4(40.72, 17.29)); - add(entities.waterRock4(28.22, 13.25)); - add(entities.waterRock5(28.66, 13.17)); - add(entities.waterRock5(16.84, 20.42)); - add(entities.waterRock5(14.44, 30.50)); - add(entities.waterRock5(23.13, 41.88)); - add(entities.waterRock5(30.44, 39.71)); - add(entities.waterRock5(41.84, 34.83)); - add(entities.waterRock5(36.63, 14.75)); - add(entities.waterRock6(36.25, 14.54)); - add(entities.waterRock6(14.84, 34.29)); - add(entities.waterRock6(40.44, 38.71)); - add(entities.waterRock7(42.13, 34.42)); - add(entities.waterRock7(20.34, 15.25)); - add(entities.waterRock7(28.78, 40.79)); - add(entities.waterRock7(22.66, 42.08)); - add(entities.waterRock8(22.25, 41.92)); - add(entities.waterRock8(14.13, 30.79)); - add(entities.waterRock8(16.28, 20.21)); - add(entities.waterRock8(42.19, 34.79)); - add(entities.waterRock9(36.78, 14.29)); - add(entities.waterRock9(13.97, 30.33)); - add(entities.waterRock10(14.50, 25.83)); - add(entities.waterRock10(18.88, 40.75)); - add(entities.waterRock11(25.37, 13.75)); - add(entities.waterRock11(18.44, 40.58)); - add(entities.waterRock11(15.41, 24.33)); - add(entities.waterRock4(15.13, 23.96)); - add(entities.waterRock11(35.38, 31.08)); - add(entities.waterRock8(35.09, 30.71)); - add(entities.waterRock3(34.03, 29.79)); - add(entities.waterRock4(34.31, 29.67)); - add(entities.waterRock4(24.50, 35.92)); - add(entities.waterRock3(33.44, 38.21)); - add(entities.waterRock4(33.16, 37.92)); - add(entities.waterRock1(25.50, 17.79)); - add(entities.waterRock9(25.66, 18.13)); - add(entities.waterRock4(25.97, 17.79)); + add(entities.waterRock1(22.63, 41.83)); + add(entities.waterRock1(28.56, 13.75)); + add(entities.waterRock1(16.25, 20.88)); + add(entities.waterRock1(40.62, 29.62)); + add(entities.waterRock1(40.63, 17.96)); + add(entities.waterRock2(40.22, 17.50)); + add(entities.waterRock2(20.34, 15.75)); + add(entities.waterRock2(15.41, 34.13)); + add(entities.waterRock2(36.50, 40.63)); + add(entities.waterRock3(36.13, 40.96)); + add(entities.waterRock3(41.63, 34.38)); + add(entities.waterRock3(15.16, 34.67)); + add(entities.waterRock3(20.88, 15.50)); + add(entities.waterRock4(36.56, 41.13)); + add(entities.waterRock4(40.72, 17.29)); + add(entities.waterRock4(28.22, 13.25)); + add(entities.waterRock5(28.66, 13.17)); + add(entities.waterRock5(16.84, 20.42)); + add(entities.waterRock5(14.44, 30.50)); + add(entities.waterRock5(23.13, 41.88)); + add(entities.waterRock5(30.44, 39.71)); + add(entities.waterRock5(41.84, 34.83)); + add(entities.waterRock5(36.63, 14.75)); + add(entities.waterRock6(36.25, 14.54)); + add(entities.waterRock6(14.84, 34.29)); + add(entities.waterRock6(40.44, 38.71)); + add(entities.waterRock7(42.13, 34.42)); + add(entities.waterRock7(20.34, 15.25)); + add(entities.waterRock7(28.78, 40.79)); + add(entities.waterRock7(22.66, 42.08)); + add(entities.waterRock8(22.25, 41.92)); + add(entities.waterRock8(14.13, 30.79)); + add(entities.waterRock8(16.28, 20.21)); + add(entities.waterRock8(42.19, 34.79)); + add(entities.waterRock9(36.78, 14.29)); + add(entities.waterRock9(13.97, 30.33)); + add(entities.waterRock10(14.50, 25.83)); + add(entities.waterRock10(18.88, 40.75)); + add(entities.waterRock11(25.37, 13.75)); + add(entities.waterRock11(18.44, 40.58)); + add(entities.waterRock11(15.41, 24.33)); + add(entities.waterRock4(15.13, 23.96)); + add(entities.waterRock11(35.38, 31.08)); + add(entities.waterRock8(35.09, 30.71)); + add(entities.waterRock3(34.03, 29.79)); + add(entities.waterRock4(34.31, 29.67)); + add(entities.waterRock4(24.50, 35.92)); + add(entities.waterRock3(33.44, 38.21)); + add(entities.waterRock4(33.16, 37.92)); + add(entities.waterRock1(25.50, 17.79)); + add(entities.waterRock9(25.66, 18.13)); + add(entities.waterRock4(25.97, 17.79)); - add(entities.flower3Pickable(30.25, 24.92)).interact = (_, { pony }) => holdItem(pony, entities.flowerPick.type); + add(entities.flower3Pickable(30.25, 24.92)).interact = (_, { pony }) => holdItem(pony, entities.flowerPick.type); - add(entities.bench1(37.78, 24.96)); - add(entities.benchSeat(37.75, 28.29)); - add(entities.benchBack(37.75, 29.17)); + add(entities.bench1(37.78, 24.96)); + add(entities.benchSeat(37.75, 28.29)); + add(entities.benchBack(37.75, 29.17)); - // small island bit - addEntities(entities.tree(9.16, 16.50, 2 + 8)); - add(entities.waterRock1(5.34, 23.71)); - add(entities.waterRock1(13.28, 14.33)); - add(entities.waterRock1(11.19, 24.83)); - add(entities.waterRock3(10.94, 25.21)); - add(entities.waterRock3(6.31, 18.92)); - add(entities.waterRock3(13.59, 14.71)); - add(entities.waterRock5(13.84, 14.17)); - add(entities.waterRock4(11.38, 25.13)); - add(entities.waterRock6(6.41, 15.92)); - add(entities.waterRock8(5.16, 23.13)); - add(entities.waterRock10(6.06, 16.33)); - add(entities.waterRock11(14.66, 20.75)); - add(entities.torch(9.31, 17.83)); - add(entities.torch(9.44, 21.67)); - add(entities.torch(12.75, 18.29)); + // small island bit + addEntities(entities.tree(9.16, 16.50, 2 + 8)); + add(entities.waterRock1(5.34, 23.71)); + add(entities.waterRock1(13.28, 14.33)); + add(entities.waterRock1(11.19, 24.83)); + add(entities.waterRock3(10.94, 25.21)); + add(entities.waterRock3(6.31, 18.92)); + add(entities.waterRock3(13.59, 14.71)); + add(entities.waterRock5(13.84, 14.17)); + add(entities.waterRock4(11.38, 25.13)); + add(entities.waterRock6(6.41, 15.92)); + add(entities.waterRock8(5.16, 23.13)); + add(entities.waterRock10(6.06, 16.33)); + add(entities.waterRock11(14.66, 20.75)); + add(entities.torch(9.31, 17.83)); + add(entities.torch(9.44, 21.67)); + add(entities.torch(12.75, 18.29)); - if (world.season === Season.Summer || world.season === Season.Spring) { - add(entities.flowerPatch1(21.16, 26.04)); - add(entities.flowerPatch3(35.81, 34.75)); - add(entities.flowerPatch3(23.09, 32.29)); - add(entities.flowerPatch5(19.63, 33.04)); - add(entities.flowerPatch5(37.38, 26.42)); - add(entities.flowerPatch5(18.75, 26.38)); - add(entities.flowerPatch5(35.38, 33.88)); - add(entities.flowerPatch6(25.78, 31.04)); - add(entities.flowerPatch6(33.00, 33.79)); - add(entities.flowerPatch6(38.72, 26.00)); - add(entities.flowerPatch3(11.34, 19.33)); - add(entities.flowerPatch6(11.09, 17.79)); - add(entities.flowerPatch7(8.63, 21.67)); - } + if (world.season === Season.Summer || world.season === Season.Spring) { + add(entities.flowerPatch1(21.16, 26.04)); + add(entities.flowerPatch3(35.81, 34.75)); + add(entities.flowerPatch3(23.09, 32.29)); + add(entities.flowerPatch5(19.63, 33.04)); + add(entities.flowerPatch5(37.38, 26.42)); + add(entities.flowerPatch5(18.75, 26.38)); + add(entities.flowerPatch5(35.38, 33.88)); + add(entities.flowerPatch6(25.78, 31.04)); + add(entities.flowerPatch6(33.00, 33.79)); + add(entities.flowerPatch6(38.72, 26.00)); + add(entities.flowerPatch3(11.34, 19.33)); + add(entities.flowerPatch6(11.09, 17.79)); + add(entities.flowerPatch7(8.63, 21.67)); + } - if (world.season === Season.Autumn) { - add(entities.leafpileStickRed(31.00, 22.75)); - add(entities.leaves5(18.41, 21.46)); - add(entities.leaves2(21.59, 18.17)); - add(entities.leaves2(23.56, 18.00)); - add(entities.leaves1(22.00, 17.21)); - add(entities.leaves1(22.47, 20.00)); - add(entities.leaves2(33.69, 25.42)); - add(entities.leaves1(33.47, 26.75)); - add(entities.leaves1(35.66, 27.08)); - add(entities.leaves3(30.31, 24.04)); - add(entities.leaves1(31.56, 23.00)); - add(entities.leaves1(29.47, 23.21)); - add(entities.leaves3(8.84, 17.04)); - add(entities.leaves2(8.91, 15.25)); - add(entities.leaves1(10.44, 16.46)); - } + if (world.season === Season.Autumn) { + add(entities.leafpileStickRed(31.00, 22.75)); + add(entities.leaves5(18.41, 21.46)); + add(entities.leaves2(21.59, 18.17)); + add(entities.leaves2(23.56, 18.00)); + add(entities.leaves1(22.00, 17.21)); + add(entities.leaves1(22.47, 20.00)); + add(entities.leaves2(33.69, 25.42)); + add(entities.leaves1(33.47, 26.75)); + add(entities.leaves1(35.66, 27.08)); + add(entities.leaves3(30.31, 24.04)); + add(entities.leaves1(31.56, 23.00)); + add(entities.leaves1(29.47, 23.21)); + add(entities.leaves3(8.84, 17.04)); + add(entities.leaves2(8.91, 15.25)); + add(entities.leaves1(10.44, 16.46)); + } - addEntities(createBunny([ - point(22.19, 27.25), - point(19.75, 26.13), - point(18.44, 29.04), - point(20.41, 31.42), - point(19.94, 33.04), - point(22.88, 33.67), - point(24.91, 31.25), - point(26.09, 32.50), - point(26.34, 34.83), - point(32.50, 35.46), - point(34.03, 34.67), - point(36.06, 36.50), - point(36.47, 35.33), - point(36.22, 34.63), - point(33.63, 34.83), - point(31.88, 33.25), - point(32.00, 28.46), - point(33.22, 25.75), - point(35.09, 24.75), - point(36.13, 25.67), - point(36.72, 27.38), - point(38.31, 27.42), - point(38.63, 26.54), - point(37.31, 26.63), - point(36.06, 26.42), - point(35.34, 24.29), - point(33.13, 25.83), - point(30.06, 25.13), - point(26.50, 26.38), - point(24.66, 28.25), - point(22.25, 28.50), - point(20.97, 27.00), - point(19.13, 27.13), - point(20.69, 29.25), - point(22.34, 29.42), - point(21.53, 31.46), - point(23.50, 31.54), - point(23.81, 29.71), - ])); + addEntities(createBunny([ + point(22.19, 27.25), + point(19.75, 26.13), + point(18.44, 29.04), + point(20.41, 31.42), + point(19.94, 33.04), + point(22.88, 33.67), + point(24.91, 31.25), + point(26.09, 32.50), + point(26.34, 34.83), + point(32.50, 35.46), + point(34.03, 34.67), + point(36.06, 36.50), + point(36.47, 35.33), + point(36.22, 34.63), + point(33.63, 34.83), + point(31.88, 33.25), + point(32.00, 28.46), + point(33.22, 25.75), + point(35.09, 24.75), + point(36.13, 25.67), + point(36.72, 27.38), + point(38.31, 27.42), + point(38.63, 26.54), + point(37.31, 26.63), + point(36.06, 26.42), + point(35.34, 24.29), + point(33.13, 25.83), + point(30.06, 25.13), + point(26.50, 26.38), + point(24.66, 28.25), + point(22.25, 28.50), + point(20.97, 27.00), + point(19.13, 27.13), + point(20.69, 29.25), + point(22.34, 29.42), + point(21.53, 31.46), + point(23.50, 31.54), + point(23.81, 29.71), + ])); - map.controllers.push(new TorchController(world, map)); - map.controllers.push(new UpdateController(map)); + map.controllers.push(new TorchController(world, map)); + map.controllers.push(new UpdateController(map)); - if (DEVELOPMENT) { - addSpawnPointIndicators(world, map); - } + if (DEVELOPMENT) { + addSpawnPointIndicators(world, map); + } - if (!islandMapTemplate) { - generateTileIndicesAndColliders(map); - } + if (!islandMapTemplate) { + generateTileIndicesAndColliders(map); + } - return map; + return map; } export function resetIslandMap(map: ServerMap) { - copyMapTiles(map, islandMapTemplate!); + copyMapTiles(map, islandMapTemplate!); - for (const region of map.regions) { - region.clients = []; - removePonies(region.entities); - removePonies(region.movables); - resetRegionUpdates(region); - } + for (const region of map.regions) { + region.clients = []; + removePonies(region.entities); + removePonies(region.movables); + resetRegionUpdates(region); + } } diff --git a/src/ts/server/maps/mainMap.ts b/src/ts/server/maps/mainMap.ts index 60fecd1..0390b7c 100644 --- a/src/ts/server/maps/mainMap.ts +++ b/src/ts/server/maps/mainMap.ts @@ -7,12 +7,12 @@ import { TileType, Season, MapType, Holiday, MessageType, ServerFlags, MapFlags import { rect } from '../../common/rect'; import * as entities from '../../common/entities'; import { - addSpawnPointIndicators, createDirectionSign, pickEntity, checkNotCollecting, pickGift, checkLantern, - pickCandy, checkBasket, pickEgg, pickClover, positionClover, createBunny + addSpawnPointIndicators, createDirectionSign, pickEntity, checkNotCollecting, pickGift, checkLantern, + pickCandy, checkBasket, pickEgg, pickClover, positionClover, createBunny } from '../mapUtils'; import { - give, createWoodenFenceMaker, createStoneWallFenceMaker, createSign, createSignWithText, createAddLight, - boopLight, createBoxOfLanterns + give, createWoodenFenceMaker, createStoneWallFenceMaker, createSign, createSignWithText, createAddLight, + boopLight, createBoxOfLanterns } from '../controllerUtils'; import { ServerEntity, IClient, ServerMap } from '../serverInterfaces'; import { logger } from '../logger'; @@ -32,4615 +32,4615 @@ const mainMapData = JSON.parse(fs.readFileSync(pathTo('src', 'maps', 'main.json' const mainMapTiles = deserializeTiles(mainMapData.tiles); function createCookieTable(x: number, y: number) { - return entities.cookieTable(x, y + 0.5) as ServerEntity; + return entities.cookieTable(x, y + 0.5) as ServerEntity; } function createCookieTable2(x: number, y: number) { - return entities.cookieTable2(x, y + 0.5) as ServerEntity; + return entities.cookieTable2(x, y + 0.5) as ServerEntity; } function createToyStash(x: number, y: number) { - return [ - entities.giftPileSign(x, y - 0.1), - createSign(x, y, 'Toy Stash', (_, client) => updateEntityOptions(client.pony, getNextToyOrExtra(client)), entities.sign), - ]; + return [ + entities.giftPileSign(x, y - 0.1), + createSign(x, y, 'Toy Stash', (_, client) => updateEntityOptions(client.pony, getNextToyOrExtra(client)), entities.sign), + ]; } function donateGift(_: any, client: IClient) { - if (client.account.state && client.account.state.gifts) { - let count = 0; - updateAccountState(client.account, state => state.gifts = count = Math.max(0, toInt(state.gifts) - 1)); - saySystem(client, `${count} 🎁`); - } + if (client.account.state && client.account.state.gifts) { + let count = 0; + updateAccountState(client.account, state => state.gifts = count = Math.max(0, toInt(state.gifts) - 1)); + saySystem(client, `${count} 🎁`); + } - if (isGift(client.pony.options && client.pony.options.hold)) { - unholdItem(client.pony); - } + if (isGift(client.pony.options && client.pony.options.hold)) { + unholdItem(client.pony); + } } function donateCandy(_: any, client: IClient) { - if (client.account.state && client.account.state.candies) { - let count = 0; - updateAccountState(client.account, state => state.candies = count = Math.max(0, toInt(state.candies) - 1)); - saySystem(client, `${count} 🍬`); - } + if (client.account.state && client.account.state.candies) { + let count = 0; + updateAccountState(client.account, state => state.candies = count = Math.max(0, toInt(state.candies) - 1)); + saySystem(client, `${count} 🍬`); + } } function donateEgg(_: any, client: IClient) { - if (client.account.state && client.account.state.eggs) { - let count = 0; - updateAccountState(client.account, state => state.eggs = count = Math.max(0, toInt(state.eggs) - 1)); - saySystem(client, `${count} 🥚`); - } + if (client.account.state && client.account.state.eggs) { + let count = 0; + updateAccountState(client.account, state => state.eggs = count = Math.max(0, toInt(state.eggs) - 1)); + saySystem(client, `${count} 🥚`); + } } function removeSeasonalObjects(world: World, map: ServerMap) { - const remove: ServerEntity[] = []; + const remove: ServerEntity[] = []; - for (const region of map.regions) { - for (const entity of region.entities) { - if (hasFlag(entity.serverFlags, ServerFlags.Seasonal)) { - remove.push(entity); - } - } - } + for (const region of map.regions) { + for (const entity of region.entities) { + if (hasFlag(entity.serverFlags, ServerFlags.Seasonal)) { + remove.push(entity); + } + } + } - for (const entity of remove) { - world.removeEntity(entity, map); - } + for (const entity of remove) { + world.removeEntity(entity, map); + } } function addSeasonalObjects(world: World, map: ServerMap, season: Season, holiday: Holiday) { - const isWinter = season === Season.Winter; - const isAutumn = season === Season.Autumn; - const isSpring = season === Season.Spring; - const isSummer = season === Season.Summer; - const isHalloween = holiday === Holiday.Halloween; - const isChristmas = holiday === Holiday.Christmas; - const isEaster = holiday === Holiday.Easter; - - function add(entity: ServerEntity) { - entity.serverFlags! |= ServerFlags.Seasonal; - return world.addEntity(entity, map); - } - - function addEntities(entities: ServerEntity[]) { - return entities.map(add); - } - - function addHolly(x: number, y: number) { - add(entities.holly(x, y + (1 / tileHeight))); - } - - function addHollyDecoration(x: number, y: number, a = true, b = true, c = true, d = true) { - if (isChristmas) { - a && addHolly(x - 2.8, y); - b && addHolly(x - 1, y); - c && addHolly(x + 1, y); - d && addHolly(x + 2.8, y); - } - } - - addHollyDecoration(65.00, 71.00, true, false, true, false); - addHollyDecoration(64.00, 76.00); - addHollyDecoration(55.00, 76.50, true, false); - - if (isSpring || isSummer) { - add(entities.flowerPatch1(70.00, 55.00)); - add(entities.flowerPatch2(46.50, 78.00)); - add(entities.flowerPatch2(80.00, 49.50)); - add(entities.flowerPatch2(100.60, 75.50)); - add(entities.flowerPatch2(78.00, 92.00)); - add(entities.flowerPatch3(69.50, 69.00)); - add(entities.flowerPatch3(48.80, 82.00)); - add(entities.flowerPatch3(86.50, 53.50)); - add(entities.flowerPatch4(66.00, 61.00)); - add(entities.flowerPatch4(87.50, 63.50)); - add(entities.flowerPatch4(56.00, 89.70)); - add(entities.flowerPatch5(53.50, 68.00)); - add(entities.flowerPatch5(77.70, 96.00)); - add(entities.flowerPatch5(77.00, 71.30)); - add(entities.flowerPatch5(87.50, 69.00)); - add(entities.flowerPatch7(93.40, 65.70)); - add(entities.flowerPatch1(112.09, 64.63)); - add(entities.flowerPatch2(107.70, 53.77)); - add(entities.flowerPatch2(69.16, 107.00)); - add(entities.flowerPatch3(106.73, 94.79)); - add(entities.flowerPatch3(113.59, 61.96)); - add(entities.flowerPatch3(70.50, 104.90)); - add(entities.flowerPatch4(58.03, 112.92)); - add(entities.flowerPatch4(53.31, 99.88)); - add(entities.flowerPatch6(106.72, 93.46)); - add(entities.flowerPatch5(116.31, 50.94)); - - add(entities.flowerPatch1(125.31, 148.25)); - add(entities.flowerPatch1(153.06, 133.58)); - add(entities.flowerPatch1(143.68, 87.20)); - add(entities.flowerPatch1(85.25, 150.75)); - add(entities.flowerPatch1(66.71, 139.75)); - - add(entities.flowerPatch2(79.13, 148.92)); - add(entities.flowerPatch2(65.56, 126.83)); - add(entities.flowerPatch2(149.75, 147.42)); - add(entities.flowerPatch2(131.78, 87.12)); - - add(entities.flowerPatch3(143.13, 151.50)); - add(entities.flowerPatch3(148.19, 131.58)); - add(entities.flowerPatch3(152.81, 95.42)); - add(entities.flowerPatch3(123.94, 83.00)); - add(entities.flowerPatch3(92.38, 156.08)); - add(entities.flowerPatch3(114.88, 134.08)); - add(entities.flowerPatch3(119.13, 154.67)); - add(entities.flowerPatch3(69.41, 133.00)); - add(entities.flowerPatch3(75.69, 154.33)); - - add(entities.flowerPatch4(62.44, 131.25)); - add(entities.flowerPatch4(86.63, 146.58)); - add(entities.flowerPatch4(58.69, 150.83)); - add(entities.flowerPatch4(123.78, 155.08)); - add(entities.flowerPatch4(146.72, 153.25)); - add(entities.flowerPatch4(143.81, 95.75)); - add(entities.flowerPatch4(126.75, 85.92)); - - add(entities.flowerPatch5(144.03, 148.25)); - add(entities.flowerPatch5(150.09, 134.08)); - add(entities.flowerPatch5(133.28, 91.08)); - add(entities.flowerPatch5(151.09, 97.83)); - add(entities.flowerPatch5(124.47, 85.33)); - add(entities.flowerPatch5(85.41, 157.08)); - add(entities.flowerPatch5(100.66, 147.75)); - add(entities.flowerPatch5(114.91, 132.50)); - add(entities.flowerPatch5(90.47, 132.08)); - add(entities.flowerPatch5(84.16, 148.67)); - add(entities.flowerPatch5(66.50, 133.25)); - add(entities.flowerPatch5(67.94, 138.67)); - add(entities.flowerPatch5(59.44, 153.25)); - add(entities.flowerPatch5(74.44, 156.33)); - - add(entities.flowerPatch2(41.28, 96.54)); - add(entities.flowerPatch3(25.19, 88.21)); - add(entities.flowerPatch3(21.66, 109.17)); - add(entities.flowerPatch4(8.53, 102.63)); - add(entities.flowerPatch5(23.28, 112.21)); - add(entities.flowerPatch1(36.00, 126.46)); - add(entities.flowerPatch3(38.50, 123.92)); - add(entities.flowerPatch4(31.50, 126.58)); - add(entities.flowerPatch4(27.81, 122.46)); - add(entities.flowerPatch3(14.91, 140.08)); - add(entities.flowerPatch5(14.75, 137.88)); - add(entities.flowerPatch3(11.66, 152.75)); - add(entities.flowerPatch4(9.69, 150.92)); - add(entities.flowerPatch4(2.66, 117.96)); - add(entities.flowerPatch2(4.44, 113.21)); - add(entities.flowerPatch2(37.97, 64.63)); - add(entities.flowerPatch4(38.94, 62.25)); - add(entities.flowerPatch2(24.28, 61.46)); - add(entities.flowerPatch5(24.53, 60.42)); - add(entities.flowerPatch6(25.84, 66.04)); - add(entities.flowerPatch4(26.25, 45.54)); - add(entities.flowerPatch6(26.56, 43.75)); - add(entities.flowerPatch2(22.63, 24.88)); - add(entities.flowerPatch4(23.47, 21.96)); - add(entities.flowerPatch4(30.69, 14.63)); - add(entities.flowerPatch3(32.81, 16.42)); - add(entities.flowerPatch3(46.13, 4.79)); - add(entities.flowerPatch3(98.97, 10.46)); - add(entities.flowerPatch4(100.94, 44.96)); - add(entities.flowerPatch4(124.34, 43.25)); - add(entities.flowerPatch3(134.41, 45.88)); - add(entities.flowerPatch4(135.47, 27.33)); - add(entities.flowerPatch6(136.63, 29.92)); - add(entities.flowerPatch1(131.78, 4.50)); - add(entities.flowerPatch4(127.97, 4.67)); - add(entities.flowerPatch4(122.09, 28.42)); - add(entities.flowerPatch6(121.53, 30.96)); - add(entities.flowerPatch6(122.31, 41.63)); - add(entities.flowerPatch6(101.91, 47.17)); - add(entities.flowerPatch3(50.66, 39.42)); - add(entities.flowerPatch4(48.47, 38.00)); - add(entities.flowerPatch6(48.78, 41.46)); - add(entities.flowerPatch6(32.63, 46.13)); - add(entities.flowerPatch6(14.94, 55.08)); - add(entities.flowerPatch7(14.25, 56.79)); - - // hill above orchard - add(entities.flowerPatch1(81.56, 17.71)); - add(entities.flowerPatch1(72.44, 12.46)); - add(entities.flowerPatch2(80.38, 13.25)); - add(entities.flowerPatch3(74.09, 21.00)); - add(entities.flowerPatch3(75.25, 5.83)); - add(entities.flowerPatch3(85.31, 10.25)); - add(entities.flowerPatch4(80.25, 4.96)); - add(entities.flowerPatch4(77.38, 18.17)); - add(entities.flowerPatch4(64.22, 5.63)); - add(entities.flowerPatch5(73.69, 19.29)); - add(entities.flowerPatch5(90.22, 11.29)); - add(entities.flowerPatch5(77.34, 3.67)); - add(entities.flowerPatch5(64.88, 12.25)); - add(entities.flowerPatch6(70.34, 12.92)); - add(entities.flowerPatch6(85.75, 9.04)); - add(entities.flowerPatch6(82.91, 3.79)); - add(entities.flowerPatch6(61.38, 5.71)); - add(entities.flowerPatch7(73.63, 3.96)); - add(entities.flowerPatch7(74.53, 18.38)); - add(entities.flowerPatch7(67.19, 11.17)); - - add(entities.cloverPatch4(43.94, 116.83)); - add(entities.cloverPatch4(51.00, 116.29)); - add(entities.cloverPatch4(60.47, 117.92)); - add(entities.cloverPatch4(58.16, 109.04)); - add(entities.cloverPatch3(60.66, 116.17)); - add(entities.cloverPatch3(57.13, 117.50)); - add(entities.cloverPatch3(58.06, 110.46)); - add(entities.cloverPatch5(48.88, 113.58)); - add(entities.cloverPatch5(64.38, 113.75)); - add(entities.cloverPatch5(41.97, 105.54)); - add(entities.cloverPatch4(52.97, 103.71)); - add(entities.cloverPatch3(79.19, 115.25)); - add(entities.cloverPatch3(89.06, 117.08)); - add(entities.cloverPatch4(83.25, 110.67)); - add(entities.cloverPatch5(82.50, 113.54)); - add(entities.cloverPatch6(85.84, 117.92)); - add(entities.cloverPatch3(103.38, 108.17)); - add(entities.cloverPatch3(110.06, 117.38)); - add(entities.cloverPatch5(113.78, 117.92)); - add(entities.cloverPatch5(105.91, 107.00)); - add(entities.cloverPatch5(104.47, 109.75)); - add(entities.cloverPatch4(117.47, 97.17)); - add(entities.cloverPatch6(89.69, 83.46)); - add(entities.cloverPatch6(108.50, 73.88)); - add(entities.cloverPatch3(99.00, 64.80)); - add(entities.cloverPatch5(97.78, 66.63)); - add(entities.cloverPatch5(90.38, 69.29)); - add(entities.cloverPatch6(88.81, 65.21)); - add(entities.cloverPatch6(80.59, 53.42)); - add(entities.cloverPatch6(115.03, 45.58)); - add(entities.cloverPatch7(84.66, 55.29)); - add(entities.cloverPatch7(116.16, 45.79)); - add(entities.cloverPatch5(81.72, 54.29)); - add(entities.cloverPatch5(43.53, 75.83)); - add(entities.cloverPatch7(46.38, 76.63)); - add(entities.cloverPatch3(69.98, 48.58)); - add(entities.cloverPatch3(73.11, 43.50)); - add(entities.cloverPatch5(67.27, 48.31)); - add(entities.cloverPatch5(74.25, 45.19)); - add(entities.cloverPatch6(69.94, 47.40)); - add(entities.cloverPatch7(89.36, 63.65)); - add(entities.clover1(72.00, 70.00)); - add(entities.cloverPatch4(55.38, 148.38)); - add(entities.cloverPatch4(42.91, 141.83)); - add(entities.cloverPatch5(64.66, 145.75)); - add(entities.cloverPatch5(48.78, 131.00)); - add(entities.cloverPatch6(46.34, 131.96)); - add(entities.cloverPatch6(91.16, 146.83)); - add(entities.cloverPatch4(103.41, 137.33)); - add(entities.cloverPatch6(134.22, 149.08)); - add(entities.cloverPatch6(133.78, 135.79)); - add(entities.cloverPatch3(151.78, 128.38)); - add(entities.cloverPatch4(156.84, 109.63)); - add(entities.cloverPatch4(123.19, 108.54)); - add(entities.cloverPatch3(152.09, 91.63)); - add(entities.cloverPatch3(126.94, 97.04)); - add(entities.cloverPatch7(149.22, 81.13)); - add(entities.cloverPatch6(157.34, 74.00)); - } - - if (isAutumn) { - add(entities.leafpileStickRed(151.19, 98.60)); - add(entities.leaves5(150.11, 99.15)); - add(entities.leaves5(143.66, 105.88)); - add(entities.leaves4(149.91, 106.00)); - add(entities.leaves2(143.14, 100.50)); - add(entities.leaves1(67.00, 48.00)); - add(entities.leaves2(68.00, 45.00)); - add(entities.leaves3(61.20, 71.50)); - add(entities.leaves3(46.00, 46.00)); - add(entities.leaves3(86.00, 47.00)); - add(entities.leaves4(83.50, 64.00)); - add(entities.leaves4(88.50, 58.00)); - add(entities.leaves4(71.50, 47.00)); - add(entities.leaves4(45.00, 76.20)); - add(entities.leaves4(84.00, 55.50)); - add(entities.leaves5(82.00, 54.00)); - add(entities.leaves5(69.00, 53.00)); - add(entities.leaves5(42.50, 48.70)); - add(entities.leaves3(70.50, 70.50)); - add(entities.leaves5(53.00, 87.00)); - add(entities.leaves4(57.00, 89.00)); - add(entities.leaves4(117.28, 46.00)); - add(entities.leaves2(61.00, 86.00)); - add(entities.leaves3(66.00, 87.00)); - add(entities.leaves1(69.00, 87.50)); - add(entities.leaves2(72.00, 85.50)); - add(entities.leafpileStickOrange(71.00, 69.50)); - add(entities.leafpileSmallYellow(44.00, 45.50)); - add(entities.leafpileSmallOrange(64.00, 72.00)); - add(entities.leafpileStickOrange(67.50, 47.30)); - add(entities.leafpileStickYellow(84.50, 46.30)); - add(entities.leafpileMediumYellow(47.30, 75.50)); - add(entities.leafpileMediumRed(51.00, 89.00)); - add(entities.leafpileBigYellow(79.00, 54.50)); - add(entities.leafpileBigstickRed(45.00, 52.20)); - add(entities.leafpileBigstickRed(50.50, 88.00)); - add(entities.leafpileSmallOrange(66.40, 58.00)); - add(entities.leafpileBigstickOrange(66.50, 69.80)); - add(entities.leafpileStickOrange(67.50, 71.60)); - add(entities.leafpileSmallOrange(67.00, 70.50)); - add(entities.leafpileSmallYellow(91.00, 95.70)); - add(entities.leafpileBigYellow(84.00, 86.30)); - add(entities.leafpileBigRed(94.00, 91.40)); - add(entities.leafpileBigYellow(76.00, 96.00)); - add(entities.leafpileMediumYellow(77.50, 97.50)); - add(entities.leafpileBigstickRed(85.80, 74.00)); - add(entities.leafpileMediumOrange(87.00, 75.50)); - add(entities.leafpileMediumAltOrange(89.00, 83.50)); - add(entities.leafpileMediumRed(80.00, 92.50)); - add(entities.leafpileSmallOrange(80.00, 84.80)); - add(entities.leaves1(85.00, 90.00)); - add(entities.leaves2(89.00, 95.00)); - add(entities.leaves3(92.00, 93.00)); - add(entities.leaves4(95.00, 88.00)); - add(entities.leaves5(90.00, 85.00)); - add(entities.leaves1(93.00, 77.00)); - add(entities.leaves2(86.00, 83.00)); - add(entities.leaves3(77.00, 82.00)); - add(entities.leaves4(75.00, 84.00)); - add(entities.leaves5(77.00, 91.00)); - add(entities.leaves1(84.00, 92.00)); - add(entities.leaves2(83.00, 97.00)); - add(entities.leaves3(89.00, 95.00)); - add(entities.leaves4(95.00, 94.00)); - add(entities.leaves5(97.00, 88.00)); - add(entities.leaves1(75.00, 81.00)); - add(entities.leaves2(79.00, 95.00)); - add(entities.leaves1(79.00, 77.00)); - add(entities.leaves2(83.00, 73.00)); - add(entities.leaves3(87.00, 71.00)); - add(entities.leaves4(98.00, 73.00)); - add(entities.leafpileBigYellow(96.25, 103.29)); - add(entities.leafpileBigstickYellow(127.69, 95.46)); - add(entities.leafpileBigOrange(118.63, 116.04)); - add(entities.leafpileBigYellow(133.50, 135.38)); - add(entities.leafpileBigRed(118.06, 136.38)); - add(entities.leafpileBigOrange(143.25, 84.21)); - add(entities.leafpileBigstickRed(89.69, 114.71)); - add(entities.leafpileMediumRed(83.19, 112.88)); - add(entities.leafpileMediumOrange(110.81, 126.13)); - add(entities.leafpileMediumYellow(121.81, 137.21)); - add(entities.leafpileMediumRed(137.50, 131.71)); - add(entities.leafpileMediumOrange(123.69, 97.96)); - add(entities.leafpileMediumYellow(109.75, 81.29)); - add(entities.leaves5(101.16, 112.71)); - add(entities.leaves5(152.19, 91.21)); - add(entities.leaves5(100.06, 83.79)); - add(entities.leaves5(151.31, 125.46)); - add(entities.leaves5(147.19, 116.13)); - add(entities.leaves5(141.63, 135.04)); - add(entities.leaves4(115.38, 127.29)); - add(entities.leaves4(143.69, 133.38)); - add(entities.leaves4(148.56, 118.88)); - add(entities.leaves4(150.44, 93.88)); - add(entities.leaves4(107.25, 86.04)); - add(entities.leaves4(122.88, 108.38)); - add(entities.leaves3(153.25, 94.96)); - add(entities.leaves3(156.44, 108.96)); - add(entities.leaves3(142.63, 134.71)); - add(entities.leaves3(151.44, 131.46)); - add(entities.leaves3(101.50, 114.71)); - add(entities.leaves3(102.50, 111.46)); - add(entities.leaves3(116.50, 128.79)); - add(entities.leaves3(107.06, 84.96)); - add(entities.leaves3(122.63, 110.46)); - add(entities.leaves3(132.94, 105.29)); - add(entities.leaves2(148.88, 82.13)); - add(entities.leaves2(109.38, 85.04)); - add(entities.leaves2(103.81, 113.29)); - add(entities.leaves2(130.06, 94.63)); - add(entities.leaves2(120.56, 106.79)); - add(entities.leaves2(148.19, 120.54)); - add(entities.leaves2(142.00, 137.13)); - add(entities.leaves2(152.81, 132.38)); - add(entities.leaves1(157.69, 88.71)); - add(entities.leaves1(149.69, 80.13)); - add(entities.leaves1(105.50, 84.21)); - add(entities.leaves1(108.63, 113.38)); - add(entities.leaves1(116.81, 116.79)); - add(entities.leaves1(117.50, 128.46)); - - add(entities.leaves5(65.09, 24.92)); - add(entities.leaves5(42.56, 22.00)); - add(entities.leaves5(45.13, 28.96)); - add(entities.leaves5(71.13, 32.71)); - add(entities.leaves4(62.31, 32.54)); - add(entities.leaves3(41.78, 28.50)); - add(entities.leaves3(63.97, 24.17)); - add(entities.leaves2(66.75, 29.21)); - add(entities.leaves2(68.91, 31.42)); - add(entities.leaves2(59.72, 43.71)); - add(entities.leaves1(61.00, 42.75)); - add(entities.leaves1(47.53, 28.92)); - add(entities.leaves1(41.00, 25.04)); - add(entities.leaves3(66.09, 22.00)); - add(entities.leaves3(72.25, 21.00)); - add(entities.leaves4(69.91, 20.50)); - add(entities.leaves2(46.09, 43.88)); - add(entities.leafpileMediumRed(43.31, 29.25)); - add(entities.leafpileStickYellow(66.63, 27.75)); - add(entities.leafpileMediumYellow(62.44, 16.54)); - add(entities.leaves3(56.81, 17.17)); - add(entities.leaves1(50.16, 31.04)); - add(entities.leafpileBigYellow(32.69, 69.21)); - add(entities.leafpileMediumAltYellow(12.31, 63.63)); - add(entities.leafpileSmallOrange(24.16, 78.67)); - add(entities.leafpileStickYellow(11.88, 64.33)); - add(entities.leaves5(30.59, 66.50)); - add(entities.leaves5(32.41, 54.00)); - add(entities.leaves5(12.34, 61.67)); - add(entities.leaves4(25.91, 77.17)); - add(entities.leaves4(22.81, 66.79)); - add(entities.leaves4(9.94, 57.79)); - add(entities.leaves3(29.69, 54.54)); - add(entities.leaves3(28.97, 64.92)); - add(entities.leaves3(24.41, 79.54)); - add(entities.leaves3(10.97, 61.04)); - add(entities.leaves2(24.94, 67.25)); - add(entities.leaves2(34.81, 68.46)); - add(entities.leaves2(32.13, 56.08)); - add(entities.leaves2(10.97, 63.83)); - add(entities.leaves2(29.03, 77.38)); - add(entities.leaves2(16.09, 48.46)); - add(entities.leaves4(10.75, 46.75)); - add(entities.leaves1(8.53, 59.79)); - add(entities.leaves1(28.75, 66.67)); - add(entities.leaves1(29.97, 51.79)); - add(entities.leaves1(14.47, 65.58)); - add(entities.leaves1(31.38, 78.63)); - add(entities.leafpileBigOrange(23.44, 36.88)); - add(entities.leafpileMediumAltRed(13.09, 45.21)); - add(entities.leafpileMediumYellow(31.22, 25.71)); - add(entities.leafpileStickYellow(26.00, 26.13)); - add(entities.leafpileSmallOrange(20.75, 19.58)); - add(entities.leaves5(24.03, 38.38)); - add(entities.leaves5(29.88, 38.88)); - add(entities.leaves5(28.16, 22.50)); - add(entities.leaves5(24.94, 16.75)); - add(entities.leaves4(12.78, 42.71)); - add(entities.leaves4(26.53, 36.63)); - add(entities.leaves4(21.34, 20.21)); - add(entities.leaves4(26.84, 26.83)); - add(entities.leaves3(28.19, 43.67)); - add(entities.leaves3(28.94, 42.04)); - add(entities.leaves3(8.81, 47.42)); - add(entities.leaves3(24.50, 20.00)); - add(entities.leaves2(32.19, 35.04)); - add(entities.leaves2(30.81, 23.54)); - add(entities.leaves2(24.63, 20.63)); - add(entities.leaves2(39.19, 15.42)); - add(entities.leaves2(15.47, 46.88)); - add(entities.leafpileSmallYellow(30.19, 40.88)); - add(entities.leafpileBigRed(11.22, 9.63)); - add(entities.leafpileMediumAltYellow(38.22, 7.71)); - add(entities.leafpileStickRed(44.44, 6.25)); - add(entities.leafpileSmallOrange(25.31, 8.46)); - add(entities.leaves5(20.84, 6.17)); - add(entities.leaves5(43.78, 6.04)); - add(entities.leaves4(9.06, 7.88)); - add(entities.leaves4(38.94, 5.75)); - add(entities.leaves4(40.34, 13.96)); - add(entities.leaves3(42.78, 15.04)); - add(entities.leaves3(22.81, 8.50)); - add(entities.leaves3(13.06, 9.71)); - add(entities.leaves2(25.25, 6.83)); - add(entities.leaves2(8.44, 5.50)); - add(entities.leaves1(9.16, 5.17)); - add(entities.leaves1(20.44, 5.21)); - add(entities.leaves1(33.75, 22.92)); - add(entities.leaves1(29.09, 21.21)); - add(entities.leaves1(42.03, 5.17)); - add(entities.leafpileMediumAltOrange(59.41, 5.83)); - add(entities.leafpileMediumAltYellow(90.59, 3.33)); - add(entities.leafpileStickRed(75.69, 5.67)); - add(entities.leafpileSmallOrange(70.53, 23.00)); - add(entities.leaves5(84.19, 21.67)); - add(entities.leaves5(77.75, 5.25)); - add(entities.leaves4(75.16, 3.92)); - add(entities.leaves4(76.31, 15.38)); - add(entities.leaves3(87.00, 20.79)); - add(entities.leaves3(76.91, 14.33)); - add(entities.leaves3(89.41, 4.63)); - add(entities.leaves3(71.38, 6.08)); - add(entities.leaves2(71.50, 7.08)); - add(entities.leaves2(83.16, 23.71)); - add(entities.leaves2(88.84, 2.71)); - add(entities.leaves1(84.00, 23.88)); - add(entities.leaves1(75.03, 16.58)); - add(entities.leaves1(87.81, 4.71)); - add(entities.leaves1(91.88, 2.29)); - add(entities.leaves1(73.13, 8.08)); - add(entities.leaves1(72.03, 23.00)); - add(entities.leafpileStickRed(84.03, 36.21)); - add(entities.leafpileSmallYellow(82.78, 30.21)); - add(entities.leaves5(81.00, 32.67)); - add(entities.leaves5(87.53, 34.88)); - add(entities.leaves4(87.78, 37.00)); - add(entities.leaves4(79.47, 35.29)); - add(entities.leaves4(87.69, 29.83)); - add(entities.leaves3(78.34, 36.75)); - add(entities.leaves3(73.16, 35.21)); - add(entities.leaves3(85.66, 33.13)); - add(entities.leaves2(82.81, 36.13)); - add(entities.leaves2(83.94, 28.75)); - add(entities.leaves2(81.94, 29.71)); - add(entities.leaves2(87.00, 29.50)); - add(entities.leaves1(83.81, 30.38)); - add(entities.leaves1(85.38, 37.54)); - add(entities.leaves1(84.44, 37.00)); - add(entities.leaves1(82.34, 20.67)); - add(entities.leaves5(62.47, 5.17)); - add(entities.leaves4(58.72, 6.75)); - add(entities.leaves2(61.34, 3.29)); - add(entities.leaves1(63.25, 5.17)); - add(entities.leaves2(64.00, 2.79)); - add(entities.leafpileMediumYellow(107.56, 35.00)); - add(entities.leafpileStickYellow(115.25, 22.21)); - add(entities.leafpileBigYellow(119.63, 6.58)); - add(entities.leafpileSmallYellow(105.16, 6.21)); - add(entities.leafpileStickYellow(118.34, 33.83)); - add(entities.leaves5(108.16, 32.17)); - add(entities.leaves5(106.63, 21.54)); - add(entities.leaves5(123.50, 9.67)); - add(entities.leaves5(127.22, 34.83)); - add(entities.leaves4(119.25, 7.67)); - add(entities.leaves4(117.25, 8.50)); - add(entities.leaves4(99.06, 37.79)); - add(entities.leaves4(103.13, 19.50)); - add(entities.leaves4(102.75, 4.29)); - add(entities.leaves3(109.03, 24.79)); - add(entities.leaves3(100.38, 21.04)); - add(entities.leaves3(97.16, 36.79)); - add(entities.leaves3(107.97, 9.58)); - add(entities.leaves3(103.78, 7.21)); - add(entities.leaves3(125.94, 12.38)); - add(entities.leaves3(123.09, 12.08)); - add(entities.leaves3(117.41, 34.42)); - add(entities.leaves3(127.53, 38.17)); - add(entities.leaves2(115.56, 34.75)); - add(entities.leaves2(109.88, 33.42)); - add(entities.leaves2(108.41, 25.54)); - add(entities.leaves2(113.69, 21.25)); - add(entities.leaves2(116.34, 7.29)); - add(entities.leaves2(109.38, 7.79)); - add(entities.leaves2(99.69, 5.67)); - add(entities.leaves2(100.88, 19.29)); - add(entities.leaves2(103.63, 35.42)); - add(entities.leaves2(123.16, 36.71)); - add(entities.leaves1(115.16, 20.00)); - add(entities.leaves1(114.69, 22.75)); - add(entities.leaves1(106.69, 24.67)); - add(entities.leaves1(107.91, 8.17)); - add(entities.leaves1(98.56, 5.67)); - add(entities.leaves1(121.91, 6.42)); - add(entities.leaves1(121.84, 9.00)); - add(entities.leaves1(124.28, 13.38)); - add(entities.leaves1(129.13, 37.21)); - add(entities.leaves1(100.31, 36.67)); - add(entities.leaves1(103.75, 23.79)); - add(entities.leaves3(104.19, 15.67)); - add(entities.leaves2(104.00, 17.63)); - add(entities.leafpileBigOrange(137.00, 24.29)); - add(entities.leafpileMediumAltYellow(150.91, 9.96)); - add(entities.leafpileStickYellow(138.81, 9.75)); - add(entities.leaves5(153.50, 23.21)); - add(entities.leaves5(151.94, 5.50)); - add(entities.leaves5(136.25, 3.79)); - add(entities.leaves4(136.75, 8.54)); - add(entities.leaves4(150.34, 12.29)); - add(entities.leaves3(141.63, 8.67)); - add(entities.leaves3(139.16, 23.92)); - add(entities.leaves2(141.03, 22.71)); - add(entities.leaves2(137.41, 25.92)); - add(entities.leaves2(155.00, 26.13)); - add(entities.leaves2(148.31, 11.00)); - add(entities.leaves3(139.63, 2.50)); - add(entities.leaves1(137.13, 26.92)); - add(entities.leaves1(156.41, 26.67)); - add(entities.leaves1(148.88, 12.33)); - add(entities.leaves1(152.66, 7.13)); - add(entities.leaves4(155.44, 6.29)); - add(entities.leaves4(138.94, 5.00)); - add(entities.leaves3(139.19, 10.50)); - add(entities.leaves3(141.06, 25.46)); - add(entities.leaves1(140.13, 27.13)); - add(entities.leaves1(153.84, 22.63)); - add(entities.leafpileStickRed(141.97, 24.58)); - add(entities.leaves1(115.78, 44.00)); - add(entities.leaves2(117.47, 43.38)); - add(entities.leaves3(103.44, 44.88)); - add(entities.leaves2(102.97, 46.21)); - add(entities.leaves1(104.03, 46.50)); - add(entities.leaves4(97.44, 64.04)); - add(entities.leaves2(96.56, 62.67)); - add(entities.leaves3(118.56, 69.04)); - add(entities.leaves2(116.56, 69.13)); - add(entities.leaves1(117.59, 68.46)); - add(entities.leaves1(119.34, 70.96)); - add(entities.leaves2(87.13, 44.96)); - add(entities.leaves2(83.31, 53.04)); - add(entities.leaves5(155.84, 73.50)); - add(entities.leaves4(156.66, 75.67)); - add(entities.leaves1(154.56, 75.88)); - add(entities.leaves4(148.13, 80.63)); - add(entities.leaves2(147.16, 79.13)); - add(entities.leaves2(139.81, 83.63)); - add(entities.leaves3(141.75, 85.67)); - add(entities.leaves1(141.19, 84.75)); - add(entities.leaves1(133.78, 67.58)); - add(entities.leaves2(134.66, 67.67)); - add(entities.leaves3(136.22, 66.25)); - add(entities.leaves1(135.81, 67.83)); - add(entities.leaves1(142.66, 86.50)); - add(entities.leaves1(157.56, 77.25)); - add(entities.leaves4(151.13, 87.50)); - add(entities.leaves2(150.66, 86.21)); - add(entities.leaves2(158.03, 85.54)); - add(entities.leaves4(144.44, 94.21)); - add(entities.leaves4(127.34, 92.50)); - add(entities.leaves2(126.97, 91.79)); - add(entities.leaves2(131.22, 95.79)); - add(entities.leaves1(125.72, 99.46)); - add(entities.leaves1(125.13, 108.04)); - add(entities.leaves1(155.22, 106.42)); - add(entities.leaves2(152.72, 99.21)); - add(entities.leaves2(147.91, 95.67)); - add(entities.leaves2(132.94, 107.50)); - add(entities.leaves3(131.41, 149.21)); - add(entities.leaves2(131.09, 148.13)); - add(entities.leaves1(131.72, 147.54)); - add(entities.leaves4(134.91, 148.83)); - add(entities.leaves4(137.22, 137.63)); - add(entities.leaves4(110.41, 130.33)); - add(entities.leaves4(118.78, 138.54)); - add(entities.leaves2(116.84, 137.79)); - add(entities.leaves2(112.13, 130.17)); - add(entities.leaves3(25.44, 84.75)); - add(entities.leaves1(26.38, 83.79)); - add(entities.leaves4(86.25, 114.58)); - add(entities.leaves5(89.63, 117.63)); - add(entities.leaves2(86.25, 117.00)); - add(entities.leaves1(84.25, 117.21)); - add(entities.leaves5(41.75, 82.58)); - add(entities.leaves2(38.91, 80.96)); - add(entities.leaves1(40.78, 84.29)); - add(entities.leaves3(40.75, 81.38)); - } - - function addSnowpony(x: number, y: number, type: number) { - const snowpony = entities.snowponies[type - 1]; - const entity = add(snowpony(x, y + 0.5)); - lockTile(map, entity.x - 0.5, entity.y); - lockTile(map, entity.x + 0.5, entity.y); - } - - function addSnowPile(entity: ServerEntity) { - add(entity); - lockTile(map, entity.x - 0.5, entity.y); - lockTile(map, entity.x + 0.5, entity.y); - - if ( - entity.type === entities.snowPileSmall.type || - entity.type === entities.snowPileMedium.type || - entity.type === entities.snowPileBig.type - ) { - lockTile(map, entity.x - 0.5, entity.y - 1); - lockTile(map, entity.x + 0.5, entity.y - 1); - } - - if ( - entity.type === entities.snowPileMedium.type || - entity.type === entities.snowPileBig.type - ) { - lockTile(map, entity.x - 1, entity.y); - lockTile(map, entity.x - 0.5, entity.y + 1); - lockTile(map, entity.x + 0.5, entity.y + 1); - } - - if (entity.type === entities.snowPileBig.type) { - lockTile(map, entity.x + 1, entity.y); - lockTile(map, entity.x - 1.5, entity.y); - lockTile(map, entity.x - 1, entity.y + 1); - lockTile(map, entity.x + 1, entity.y + 1); - } - } - - if (isWinter) { - addSnowpony(44.00, 57.00, 1); - addSnowpony(65.00, 64.00, 1); - addSnowpony(68.00, 65.00, 2); - addSnowpony(84.00, 56.00, 2); - addSnowpony(67.00, 82.00, 1); - addSnowpony(59.00, 87.00, 2); - addSnowpony(85.19, 152.67, 3); - addSnowpony(86.16, 153.04, 6); - addSnowpony(86.09, 122.83, 4); - addSnowpony(115.09, 109.13, 5); - addSnowpony(108.31, 128.29, 3); - addSnowpony(88.44, 104.71, 7); - addSnowpony(106.50, 86.13, 9); - addSnowpony(107.31, 86.67, 8); - addSnowpony(87.88, 92.33, 5); - addSnowpony(107.50, 58.83, 1); - addSnowpony(85.09, 56.92, 1); - addSnowpony(83.38, 57.25, 6); - addSnowpony(66.03, 83.13, 7); - addSnowpony(51.69, 102.38, 1); - addSnowpony(69.41, 112.50, 1); - addSnowpony(67.84, 111.17, 4); - addSnowpony(53.69, 133.33, 9); - addSnowpony(59.75, 119.79, 6); - addSnowpony(65.03, 156.46, 2); - addSnowpony(49.13, 148.38, 3); - addSnowpony(119.19, 149.96, 3); - addSnowpony(136.06, 156.00, 7); - addSnowpony(141.22, 130.92, 5); - addSnowpony(116.06, 136.58, 2); - addSnowpony(147.50, 101.04, 4); - addSnowpony(138.91, 97.92, 3); - addSnowpony(125.50, 88.75, 2); - addSnowpony(137.13, 67.50, 7); - addSnowpony(115.13, 65.88, 2); - addSnowpony(135.53, 44.33, 2); - addSnowpony(153.34, 51.00, 5); - addSnowpony(156.78, 60.08, 3); - addSnowpony(116.88, 46.79, 8); - addSnowpony(123.56, 53.67, 3); - addSnowpony(100.56, 44.75, 4); - addSnowpony(156.31, 124.88, 3); - addSnowpony(149.50, 150.00, 6); - addSnowpony(54.41, 132.83, 8); - - addSnowpony(29.69, 67.63, 1); - addSnowpony(26.72, 44.42, 2); - addSnowpony(23.22, 26.04, 3); - addSnowpony(20.97, 27.21, 4); - addSnowpony(12.97, 14.71, 5); - addSnowpony(37.81, 28.21, 6); - addSnowpony(50.38, 4.00, 7); - addSnowpony(81.78, 18.00, 8); - addSnowpony(80.00, 19.50, 1); - addSnowpony(84.25, 3.08, 2); - addSnowpony(97.19, 21.25, 3); - addSnowpony(86.47, 38.42, 4); - addSnowpony(122.47, 27.42, 5); - addSnowpony(133.44, 7.54, 6); - addSnowpony(130.31, 9.04, 7); - addSnowpony(143.41, 19.42, 8); - addSnowpony(20.63, 78.13, 1); - addSnowpony(25.25, 90.63, 2); - addSnowpony(27.56, 102.08, 3); - addSnowpony(22.19, 109.83, 4); - addSnowpony(5.72, 113.42, 5); - addSnowpony(2.19, 116.75, 6); - addSnowpony(13.75, 138.71, 7); - addSnowpony(3.50, 150.63, 8); - addSnowpony(35.09, 151.63, 1); - addSnowpony(37.75, 123.75, 2); - - addSnowPile(entities.snowPileBig(67.06, 78.46)); - addSnowPile(entities.snowPileMedium(65.25, 77.21)); - addSnowPile(entities.snowPileSmall(61.13, 70.08)); - addSnowPile(entities.snowPileSmall(68.44, 71.25)); - addSnowPile(entities.snowPileBig(74.84, 94.58)); - addSnowPile(entities.snowPileSmall(76.56, 95.50)); - addSnowPile(entities.snowPileBig(52.97, 60.04)); - addSnowPile(entities.snowPileBig(80.63, 54.29)); - addSnowPile(entities.snowPileSmall(82.94, 65.04)); - addSnowPile(entities.snowPileBig(117.19, 80.38)); - addSnowPile(entities.snowPileSmall(122.19, 83.38)); - addSnowPile(entities.snowPileSmall(135.81, 66.58)); - addSnowPile(entities.snowPileBig(150.25, 99.42)); - addSnowPile(entities.snowPileMedium(143.50, 106.67)); - addSnowPile(entities.snowPileSmall(148.28, 98.88)); - addSnowPile(entities.snowPileMedium(115.75, 98.04)); - addSnowPile(entities.snowPileMedium(139.81, 85.25)); - addSnowPile(entities.snowPileMedium(137.53, 98.79)); - addSnowPile(entities.snowPileMedium(115.50, 45.79)); - addSnowPile(entities.snowPileBig(151.88, 138.79)); - addSnowPile(entities.snowPileSmall(150.09, 138.00)); - addSnowPile(entities.snowPileSmall(140.28, 144.50)); - addSnowPile(entities.snowPileSmall(102.56, 147.13)); - addSnowPile(entities.snowPileBig(92.75, 139.75)); - addSnowPile(entities.snowPileBig(74.34, 157.38)); - addSnowPile(entities.snowPileSmall(73.19, 155.96)); - addSnowPile(entities.snowPileSmall(87.09, 147.63)); - addSnowPile(entities.snowPileBig(69.59, 134.54)); - addSnowPile(entities.snowPileMedium(84.66, 130.25)); - addSnowPile(entities.snowPileMedium(77.41, 121.38)); - addSnowPile(entities.snowPileSmall(79.13, 120.75)); - addSnowPile(entities.snowPileSmall(77.34, 133.83)); - addSnowPile(entities.snowPileBig(72.84, 106.33)); - addSnowPile(entities.snowPileMedium(63.41, 121.63)); - addSnowPile(entities.snowPileSmall(64.91, 122.29)); - addSnowPile(entities.snowPileTiny(71.06, 70.04)); - addSnowPile(entities.snowPileTiny(64.25, 77.88)); - addSnowPile(entities.snowPileTiny(44.41, 76.17)); - addSnowPile(entities.snowPileTiny(51.28, 69.00)); - addSnowPile(entities.snowPileTiny(74.56, 83.79)); - addSnowPile(entities.snowPileTinier(64.72, 78.29)); - addSnowPile(entities.snowPileTinier(67.31, 71.04)); - addSnowPile(entities.snowPileTinier(60.56, 69.21)); - addSnowPile(entities.snowPileTinier(51.44, 59.58)); - addSnowPile(entities.snowPileTinier(74.19, 84.25)); - addSnowPile(entities.snowPileTiny(49.75, 87.50)); - addSnowPile(entities.snowPileTiny(77.31, 100.83)); - addSnowPile(entities.snowPileTiny(75.81, 118.29)); - addSnowPile(entities.snowPileTiny(74.25, 106.83)); - addSnowPile(entities.snowPileTinier(76.22, 118.58)); - addSnowPile(entities.snowPileTinier(73.75, 107.00)); - addSnowPile(entities.snowPileTinier(75.63, 95.92)); - addSnowPile(entities.snowPileTiny(62.28, 121.96)); - addSnowPile(entities.snowPileTinier(62.91, 122.42)); - addSnowPile(entities.snowPileTinier(86.19, 147.25)); - addSnowPile(entities.snowPileTinier(78.50, 121.54)); - addSnowPile(entities.snowPileTiny(85.69, 130.50)); - addSnowPile(entities.snowPileTiny(72.44, 156.25)); - addSnowPile(entities.snowPileTiny(101.75, 146.29)); - addSnowPile(entities.snowPileTiny(112.34, 152.42)); - addSnowPile(entities.snowPileTiny(93.28, 110.67)); - addSnowPile(entities.snowPileTiny(103.28, 122.71)); - addSnowPile(entities.snowPileTinier(103.75, 122.96)); - addSnowPile(entities.snowPileTinier(93.72, 111.00)); - addSnowPile(entities.snowPileTiny(116.81, 97.13)); - addSnowPile(entities.snowPileTiny(110.28, 91.79)); - addSnowPile(entities.snowPileTiny(115.78, 80.96)); - addSnowPile(entities.snowPileTinier(116.16, 81.33)); - addSnowPile(entities.snowPileTinier(123.00, 83.42)); - addSnowPile(entities.snowPileTinier(116.66, 45.96)); - addSnowPile(entities.snowPileTinier(140.84, 85.33)); - addSnowPile(entities.snowPileTiny(138.75, 98.96)); - addSnowPile(entities.snowPileTinier(139.09, 98.79)); - addSnowPile(entities.snowPileTiny(148.47, 99.33)); - addSnowPile(entities.snowPileTinier(147.34, 98.58)); - addSnowPile(entities.snowPileTinier(144.66, 106.79)); - addSnowPile(entities.snowPileTiny(149.13, 138.50)); - addSnowPile(entities.snowPileTiny(141.09, 144.63)); - addSnowPile(entities.snowPileTinier(149.63, 138.88)); - addSnowPile(entities.snowPileTinier(140.63, 145.00)); - addSnowPile(entities.snowPileTiny(85.88, 147.67)); - addSnowPile(entities.snowPileTiny(94.34, 139.96)); - addSnowPile(entities.snowPileTinier(82.88, 56.88)); - addSnowPile(entities.snowPileTiny(85.84, 56.54)); - addSnowPile(entities.snowPileTinier(85.31, 57.13)); - addSnowPile(entities.snowPileTiny(66.91, 82.79)); - addSnowPile(entities.snowPileTinier(66.47, 83.21)); - addSnowPile(entities.snowPileTinier(65.38, 82.92)); - addSnowPile(entities.snowPileTinier(68.75, 112.17)); - addSnowPile(entities.snowPileTinier(54.13, 133.38)); - addSnowPile(entities.snowPileTinier(84.44, 152.42)); - addSnowPile(entities.snowPileTiny(85.41, 152.96)); - addSnowPile(entities.snowPileTiny(86.56, 152.04)); - addSnowPile(entities.snowPileTinier(86.97, 152.38)); - addSnowPile(entities.snowPileTinier(105.84, 85.96)); - addSnowPile(entities.snowPileTiny(106.53, 86.29)); - addSnowPile(entities.snowPileTinier(107.91, 86.42)); - addSnowPile(entities.snowPileTinier(155.66, 61.17)); - addSnowPile(entities.snowPileTiny(157.47, 60.42)); - addSnowPile(entities.snowPileTinier(157.72, 60.21)); - addSnowPile(entities.snowPileTinier(116.22, 46.42)); - addSnowPile(entities.snowPileBig(94.25, 68.29)); - addSnowPile(entities.snowPileMedium(73.56, 43.42)); - addSnowPile(entities.snowPileMedium(52.75, 38.46)); - addSnowPile(entities.snowPileSmall(52.03, 39.88)); - addSnowPile(entities.snowPileBig(43.72, 30.13)); - addSnowPile(entities.snowPileSmall(54.22, 17.13)); - addSnowPile(entities.snowPileTiny(48.69, 30.38)); - addSnowPile(entities.snowPileTiny(53.22, 39.46)); - addSnowPile(entities.snowPileTiny(60.19, 34.79)); - addSnowPile(entities.snowPileSmall(79.28, 37.21)); - addSnowPile(entities.snowPileSmall(84.19, 22.21)); - addSnowPile(entities.snowPileBig(99.34, 21.25)); - addSnowPile(entities.snowPileTiny(97.88, 22.29)); - addSnowPile(entities.snowPileSmall(106.94, 42.13)); - addSnowPile(entities.snowPileMedium(120.38, 26.79)); - addSnowPile(entities.snowPileSmall(121.00, 28.00)); - addSnowPile(entities.snowPileMedium(145.56, 20.17)); - addSnowPile(entities.snowPileBig(144.41, 21.29)); - addSnowPile(entities.snowPileBig(134.91, 4.46)); - addSnowPile(entities.snowPileSmall(120.56, 7.88)); - addSnowPile(entities.snowPileSmall(155.88, 55.21)); - addSnowPile(entities.snowPileSmall(148.28, 114.38)); - addSnowPile(entities.snowPileSmall(138.84, 154.50)); - addSnowPile(entities.snowPileMedium(139.88, 153.13)); - addSnowPile(entities.snowPileMedium(60.38, 150.25)); - addSnowPile(entities.snowPileBig(36.41, 149.33)); - addSnowPile(entities.snowPileMedium(37.31, 150.63)); - addSnowPile(entities.snowPileMedium(16.25, 138.88)); - addSnowPile(entities.snowPileSmall(15.31, 139.83)); - addSnowPile(entities.snowPileSmall(2.63, 149.38)); - addSnowPile(entities.snowPileMedium(4.19, 113.96)); - addSnowPile(entities.snowPileSmall(4.97, 115.08)); - addSnowPile(entities.snowPileSmall(29.34, 102.83)); - addSnowPile(entities.snowPileMedium(25.66, 89.00)); - addSnowPile(entities.snowPileSmall(26.91, 89.88)); - addSnowPile(entities.snowPileSmall(32.91, 69.38)); - addSnowPile(entities.snowPileSmall(21.13, 76.46)); - addSnowPile(entities.snowPileMedium(15.81, 53.46)); - addSnowPile(entities.snowPileMedium(25.94, 27.25)); - addSnowPile(entities.snowPileSmall(24.22, 28.50)); - addSnowPile(entities.snowPileBig(13.53, 17.13)); - addSnowPile(entities.snowPileSmall(12.34, 16.38)); - addSnowPile(entities.snowPileSmall(22.59, 5.46)); - addSnowPile(entities.snowPileMedium(52.38, 3.96)); - addSnowPile(entities.snowPileTiny(51.13, 4.71)); - addSnowPile(entities.snowPileTinier(50.94, 13.33)); - addSnowPile(entities.snowPileSmall(89.22, 2.71)); - } - - function addXmasTree(x: number, y: number) { - x -= 33; - y -= 24; - addEntities(entities.pine(x + 33, y + 24, 1)); - add(entities.xmasLights(x + 33, y + 24)); - add(entities.xmasLight(x + 33.22, y + 24.88)); - add(entities.xmasLight(x + 33.59, y + 24.04)); - add(entities.xmasLight(x + 32.28, y + 24.08)); - add(entities.xmasLight(x + 31.84, y + 24.42)); - add(entities.xmasLight(x + 30.97, y + 24.00)); - add(entities.xmasLight(x + 32.31, y + 22.63)); - add(entities.xmasLight(x + 31.63, y + 21.75)); - add(entities.xmasLight(x + 32.75, y + 20.92)); - add(entities.xmasLight(x + 32.19, y + 19.50)); - add(entities.xmasLight(x + 32.75, y + 18.17)); - add(entities.xmasLight(x + 33.38, y + 18.42)); - add(entities.xmasLight(x + 33.38, y + 19.79)); - add(entities.xmasLight(x + 33.94, y + 20.42)); - add(entities.xmasLight(x + 33.63, y + 22.33)); - add(entities.xmasLight(x + 34.00, y + 22.46)); - add(entities.xmasLight(x + 34.38, y + 21.83)); - add(entities.xmasLight(x + 35.00, y + 23.63)); - add(entities.xmasLight(x + 34.56, y + 24.17)); - } - - function addGraveWithGhost(x: number, y: number, tombType: number) { - const tombs = [entities.tombstone1, entities.tombstone2]; - const tomb = add(tombs[tombType](x, y)); - const createGhost = tombType === 0 ? entities.ghost1 : entities.ghost2; - const createGhostHooves = tombType === 0 ? entities.ghostHooves1 : entities.ghostHooves2; - const ghost = add(createGhost(x + toWorldX(1), y)); - const hooves = add(createGhostHooves(x + toWorldX(1), y)); - return { tomb, ghost, hooves }; - } - - const addGhost = (x: number, y: number, tombType: number, anims?: number[]) => { - const { ghost, hooves, tomb } = addGraveWithGhost(x, y, tombType); - const randomDelay = () => random(1 * 60, 5 * 60, true); - let delay = randomDelay(); - let resetDelay = 0; - let reset = true; - - ghost.serverUpdate = delta => { - delay -= delta; - resetDelay -= delta; - - if (delay < 0) { - const flip = Math.random() > 0.5; - const anim = sample(anims || (tomb.type === entities.tombstone1.type ? [1, 3] : [1, 2, 3]))!; - setEntityAnimation(ghost, anim, flip); - setEntityAnimation(hooves, anim, flip); - delay = randomDelay(); - reset = false; - resetDelay = 5; - } else if (!reset && resetDelay < 0) { - setEntityAnimation(ghost, GhostAnimation.None); - setEntityAnimation(hooves, GhostAnimation.None); - reset = true; - } - }; - }; - - const createEyes = (x: number, y: number) => { - const entity = add(entities.eyes(x, y)); - let delay = 5; - let open = true; - - entity.serverUpdate = delta => { - delay -= delta; - - if (delay < 0) { - if (open) { - setEntityAnimation(entity, 1); - delay = 0.2; - open = false; - } else { - setEntityAnimation(entity, 0); - delay = random(5, 10, true); - open = true; - } - } - }; - }; - - const addJacko = (x: number, y: number) => { - add(entities.jacko(x, y)).boop = boopLight; - }; - - const addJackoLanternSpot = (x: number, y: number) => { - const giveLantern = give(entities.jackoLanternOn.type, 'Now go collect some candies!'); - - add(createSign(x, y, 'Jack-o-Lanterns', giveLantern, entities.signQuest)); - - addJacko(x + 0.5, y - 0.3); - addJacko(x - 0.3, y + 0.3); - - add(entities.jackoLanternOff(x + 0.2, 17.7 + y - 18.5)); - add(entities.jackoLanternOff(x + 0.7, 17.8 + y - 18.5)); - add(entities.jackoLanternOff(x + 0.3, y + 0.2)); - add(entities.jackoLanternOff(x + 0.7, y + 0.5)); - add(entities.jackoLanternOn(x, 19.2 + y - 18.5)); - add(entities.jackoLanternOn(x + 0.5, 19.4 + y - 18.5)); - add(entities.jackoLanternOn(x - 0.9, 19.2 + y - 18.5)); - }; - - if (isHalloween) { - const donateX = 64, donateY = 79; - add(createSign(donateX, donateY, 'Donate candies', donateCandy, entities.signDonate)); - add(entities.box(donateX + 0.1, donateY + 1.2)).interact = donateCandy; - - add(entities.jackoOn(132.69, 108.79)); - add(entities.jackoOn(131.31, 134.79)); - add(entities.jackoOn(134.55, 139.38)); - add(entities.jackoOn(122.94, 105.38)); - add(entities.jackoOn(126.63, 107.38)); - add(entities.jackoOn(127.75, 110.88)); - add(entities.jackoOn(126.13, 124.38)); - add(entities.jackoOn(126.44, 133.71)); - add(entities.jackoOn(126.88, 137.71)); - add(entities.jackoOn(132.38, 136.88)); - - addGhost(149.53, 135.58, 1); - addGhost(144.38, 100.21, 1); - addGhost(149.84, 100.58, 1); - add(entities.tombstone2(144.56, 105.25)); - addGhost(146.34, 105.29, 1); - add(entities.tombstone1(146.28, 100.50)); - addGhost(148.25, 100.50, 0); - add(entities.tombstone1(148.34, 105.38)); - add(entities.tombstone1(150.03, 105.21)); - addGhost(102.28, 93.29, 1); - addGhost(66.59, 57.46, 1); - addGhost(72.19, 69.21, 1); - - createEyes(155.52, 102.73); - createEyes(156.81, 104.02); - createEyes(155.66, 104.71); - createEyes(158.05, 106.65); - createEyes(147.25, 111.96); - createEyes(146.59, 109.33); - createEyes(148.19, 110.27); - createEyes(141.31, 111.52); - createEyes(145.23, 115.35); - createEyes(144.16, 114.44); - createEyes(142.39, 107.33); - createEyes(141.19, 108.63); - createEyes(142.39, 111.02); - createEyes(149.59, 113.52); - createEyes(150.22, 113.06); - createEyes(140.02, 107.44); - createEyes(142.83, 115.50); - createEyes(138.78, 111.50); - createEyes(137.22, 112.27); - - createEyes(144.59, 93.00); - createEyes(145.43, 93.95); - createEyes(147.40, 91.25); - createEyes(146.90, 93.04); - createEyes(148.68, 92.37); - createEyes(153.15, 94.29); - createEyes(154.46, 94.95); - createEyes(152.40, 96.08); - createEyes(154.87, 97.20); - createEyes(155.53, 97.79); - - add(entities.jackoLanternOn(131.97, 149.77)); - add(entities.jackoLanternOn(132.02, 83.46)); - add(entities.jackoLanternOn(131.98, 149.85)); - add(entities.jackoLanternOn(106.45, 150.31)); - add(entities.jackoLanternOn(107.14, 149.79)); - add(entities.jackoLanternOn(80.33, 93.19)); - - addGhost(45.34, 152.83, 0); - addGhost(45.75, 106.08, 0); - addGhost(59.78, 96.83, 0); - addGhost(81.00, 93.00, 0); - addGhost(83.62, 132.875, 0); - addGhost(115.96, 45.79, 0); - addGhost(149.75, 81.58, 0); - addGhost(52.40, 138.70, 1); - addGhost(52.12, 88.29, 1); - addGhost(49.06, 64.83, 1); - addGhost(48.43, 50.83, 1); - addGhost(80.72, 115.83, 1); - addGhost(86.50, 46.83, 1); - addGhost(113.812, 107.37, 1); - addGhost(118.062, 125.12, 1); - addGhost(106.59, 149.83, 1); - addGhost(132.43, 83.12, 1); - addGhost(135.46, 67.66, 1); - addGhost(131.53, 149.50, 1); - - add(entities.jackoLanternOn(45.84, 153.20)); - add(entities.jackoLanternOn(46.15, 152.75)); - add(entities.jackoLanternOn(53.00, 138.70)); - add(entities.jackoLanternOn(48.56, 64.58)); - add(entities.jackoLanternOn(48.93, 51.04)); - add(entities.jackoLanternOn(72.53, 69.58)); - add(entities.jackoLanternOn(80.31, 116.16)); - add(entities.jackoLanternOn(83.06, 133.29)); - add(entities.jackoLanternOn(86.15, 47.12)); - add(entities.jackoLanternOn(115.50, 46.13)); - add(entities.jackoLanternOn(114.25, 107.83)); - add(entities.jackoLanternOn(114.53, 107.41)); - add(entities.jackoLanternOn(132.03, 83.41)); - add(entities.jackoLanternOn(132.93, 83.08)); - add(entities.jackoLanternOn(115.50, 46.12)); - add(entities.jackoLanternOn(149.15, 81.37)); - add(entities.jackoLanternOn(144.15, 105.58)); - add(entities.jackoLanternOn(146.75, 100.83)); - add(entities.jackoLanternOn(150.63, 105.58)); - - add(entities.jackoLanternOn(52.68, 139.04)); - add(entities.jackoLanternOn(46.28, 106.41)); - add(entities.jackoLanternOn(52.53, 88.62)); - add(entities.jackoLanternOn(135.84, 67.91)); - - add(entities.jackoLanternOff(52.62, 88.12)); - add(entities.jackoLanternOff(106.09, 150.04)); - add(entities.jackoLanternOff(51.93, 138.58)); - - addJacko(71.90, 70.00); - addJacko(90.20, 66.40); - addJacko(80.00, 53.80); - addJacko(67.30, 88.60); - addJacko(86.20, 97.30); - addJacko(99.70, 92.00); - addJacko(91.00, 86.10); - addJacko(85.70, 75.10); - addJacko(84.50, 91.40); - addJacko(99.00, 87.00); - addJacko(91.60, 55.90); - addJacko(95.60, 47.50); - addJacko(98.40, 44.20); - addJacko(100.10, 49.00); - addJacko(87.20, 46.50); - addJacko(69.70, 47.20); - addJacko(49.10, 89.50); - addJacko(59.10, 97.60); - addJacko(46.70, 98.10); - addJacko(52.20, 77.40); - addJacko(62.50, 72.20); - addJacko(66.70, 71.40); - - addJacko(97.50, 100.79); - addJacko(110.34, 92.79); - addJacko(117.34, 99.38); - addJacko(108.94, 101.50); - addJacko(109.63, 102.21); - addJacko(108.00, 85.00); - addJacko(93.50, 106.17); - addJacko(113.22, 108.17); - addJacko(117.56, 117.38); - addJacko(118.81, 125.33); - addJacko(117.25, 127.71); - addJacko(118.03, 129.50); - addJacko(93.22, 135.46); - addJacko(78.94, 147.54); - addJacko(82.00, 147.67); - addJacko(102.06, 146.83); - addJacko(96.59, 146.58); - addJacko(108.75, 141.33); - addJacko(110.03, 146.71); - addJacko(147.88, 148.04); - addJacko(145.41, 150.08); - addJacko(147.28, 153.67); - addJacko(150.78, 153.00); - addJacko(150.78, 149.08); - addJacko(152.88, 131.38); - addJacko(143.25, 137.17); - addJacko(141.22, 116.13); - addJacko(138.16, 132.46); - addJacko(141.34, 102.04); - addJacko(141.31, 103.88); - addJacko(134.56, 86.71); - addJacko(139.34, 85.29); - addJacko(148.50, 81.71); - addJacko(150.09, 82.54); - addJacko(129.78, 98.67); - addJacko(136.75, 66.38); - addJacko(117.63, 70.33); - addJacko(131.81, 52.63); - addJacko(116.72, 46.21); - addJacko(137.22, 55.25); - addJacko(131.91, 49.21); - addJacko(124.63, 47.08); - addJacko(122.69, 51.58); - - addJacko(125.97, 145.21); - addJacko(115.91, 139.92); - addJacko(119.59, 145.54); - - addJacko(53.59, 133.58); - addJacko(48.78, 136.08); - addJacko(48.22, 142.75); - addJacko(55.22, 143.50); - addJacko(58.34, 135.21); - addJacko(58.94, 140.63); - - addJacko(76.63, 125.00); - addJacko(76.59, 128.88); - addJacko(77.06, 120.88); - addJacko(86.91, 121.08); - addJacko(86.84, 129.33); - addJacko(76.91, 134.63); - addJacko(86.47, 135.50); - addJacko(137.40, 97.60); - - addJacko(44.53, 152.91); - addJacko(49.56, 65.16); - addJacko(81.50, 92.41); - addJacko(81.18, 116.25); - addJacko(116.71, 46.20); - addJacko(118.81, 125.33); - addJacko(148.50, 81.70); - addJacko(150.09, 82.54); - addJacko(151.46, 99.08); - - addJacko(45.09, 105.75); - - add(entities.jackoLanternOff(66.60, 72.10)); - add(entities.jackoLanternOff(67.20, 71.90)); - - addJackoLanternSpot(101.00, 100.00); - addJackoLanternSpot(147.00, 135.00); - addJackoLanternSpot(190.00, 176.00); - addJackoLanternSpot(107.00, 173.00); - addJackoLanternSpot(165.00, 95.00); - } - - if (isChristmas) { - addEntities(createToyStash(103.87, 86.12)); - - const donateX = 64, donateY = 79; - add(createSign(donateX, donateY, 'Donate gifts', donateGift, entities.signDonate)); - add(entities.boxGifts(donateX + 0.1, donateY + 1.2)).interact = donateGift; - - const xmasTreeY = 49.5, xmasTreeX = 40.25; - addXmasTree(33 + xmasTreeX, 24 + xmasTreeY); - add(entities.giftPilePine(33 + xmasTreeX, 24.5 + xmasTreeY)); - add(entities.giftPile6(31.13 + xmasTreeX, 25.13 + xmasTreeY)); - add(entities.giftPile1(34.75 + xmasTreeX, 25.71 + xmasTreeY)); - add(createCookieTable(32.7 + xmasTreeX, 26.5 + xmasTreeY)); - - add(entities.mistletoe(86.40, 83.00)); - add(entities.mistletoe(92.00, 93.00)); - add(entities.giftPile4(43.50, 53.50)); - add(entities.giftPileTree(81.10, 92.50)); - add(entities.giftPile1(76.00, 90.00)); - add(entities.giftPile3(79.00, 53.00)); - add(entities.giftPile6(85.00, 54.50)); - - add(entities.giftPileTree(97.28, 64.38)); - add(entities.giftPilePine(63.03, 98.38)); - add(entities.giftPile5(65.25, 98.83)); - add(entities.giftPile6(61.03, 99.29)); - add(entities.giftPile1(65.09, 112.96)); - add(entities.giftPile3(63.06, 113.67)); - add(entities.giftPile4(64.59, 114.71)); - add(entities.giftPile2(57.50, 146.04)); - add(entities.giftPile6(58.25, 147.67)); - add(entities.giftPileTree(132.38, 149.08)); - add(entities.giftPile5(134.03, 149.33)); - add(entities.giftPile2(147.47, 126.58)); - add(entities.giftPileTree(137.84, 115.21)); - add(entities.giftPile5(134.06, 121.79)); - add(entities.giftPilePine(147.59, 101.86)); - add(entities.giftPile1(143.16, 98.99)); - add(entities.giftPile4(134.41, 67.58)); - add(entities.giftPilePine(154.81, 55.63)); - add(entities.giftPile1(144.13, 45.54)); - add(entities.giftPile3(132.66, 47.25)); - add(entities.giftPileTree(118.78, 48.25)); - - add(entities.giftPile1(51.31, 31.33)); - add(entities.giftPile2(58.66, 31.75)); - add(entities.giftPile4(72.28, 37.50)); - add(entities.giftPile6(91.06, 38.13)); - add(entities.giftPile3(100.00, 38.17)); - add(entities.giftPileTree(143.56, 14.21)); - add(entities.giftPile1(126.03, 12.08)); - add(entities.giftPileInteractive(148.91, 34.50)); - add(entities.giftPile6(96.97, 11.79)); - add(entities.giftPile2(77.22, 6.46)); - add(entities.giftPileTree(62.66, 6.13)); - add(entities.giftPile3(20.78, 6.71)); - add(entities.giftPile1(8.41, 15.75)); - add(entities.giftPile6(15.13, 32.08)); - add(entities.giftPileInteractive(14.22, 33.04)); - add(entities.giftPileTree(25.66, 59.29)); - add(entities.giftPile6(16.03, 77.17)); - add(entities.giftPile1(22.09, 105.88)); - add(entities.giftPileInteractive(9.34, 96.17)); - add(entities.giftPile3(24.25, 144.25)); - add(entities.giftPile6(14.88, 155.13)); - add(entities.giftPile1(33.19, 133.13)); - add(entities.giftPile6(41.56, 122.17)); - - add(entities.holly(121.00, 145.17)); - add(entities.holly(122.03, 145.17)); - add(entities.holly(123.00, 145.13)); - add(entities.holly(125.00, 145.13)); - add(entities.holly(118.00, 140.17)); - add(entities.holly(121.03, 140.13)); - add(entities.holly(119.03, 140.21)); - add(entities.holly(135.00, 144.17)); - add(entities.holly(137.03, 144.17)); - add(entities.holly(138.03, 144.17)); - add(entities.holly(137.00, 140.13)); - add(entities.holly(139.00, 140.13)); - add(entities.holly(140.00, 140.04)); - add(entities.holly(142.00, 140.13)); - add(entities.holly(143.00, 144.58)); - add(entities.holly(144.97, 144.63)); - add(entities.holly(121.00, 100.13)); - add(entities.holly(124.00, 100.13)); - add(entities.holly(119.03, 100.13)); - add(entities.holly(117.97, 100.13)); - add(entities.holly(118.03, 105.21)); - add(entities.holly(120.00, 105.17)); - add(entities.holly(79.00, 147.21)); - add(entities.holly(82.03, 147.17)); - add(entities.holly(76.97, 147.21)); - add(entities.holly(84.00, 147.17)); - add(entities.holly(85.03, 147.17)); - add(entities.holly(86.00, 147.08)); - add(entities.holly(77.03, 136.17)); - add(entities.holly(80.03, 136.13)); - add(entities.holly(81.00, 136.17)); - add(entities.holly(83.00, 137.17)); - add(entities.holly(86.00, 137.17)); - add(entities.holly(87.00, 137.08)); - add(entities.holly(88.00, 137.17)); - add(entities.holly(73.03, 100.88)); - add(entities.holly(76.81, 100.88)); - add(entities.holly(81.03, 97.04)); - add(entities.holly(83.63, 100.67)); - add(entities.holly(87.44, 100.71)); - add(entities.holly(92.22, 96.08)); - add(entities.holly(96.72, 100.58)); - add(entities.holly(45.50, 53.13)); - add(entities.holly(49.31, 53.13)); - - add(entities.giftPile3(120.69, 145.46)); - add(entities.mistletoe(130.81, 149.46)); - add(entities.mistletoe(125.56, 139.38)); - add(entities.mistletoe(140.09, 131.50)); - add(entities.mistletoe(154.81, 107.88)); - add(entities.mistletoe(132.06, 123.25)); - add(entities.mistletoe(148.16, 114.54)); - add(entities.giftPile6(137.34, 131.17)); - add(entities.giftPile3(47.94, 142.42)); - add(entities.giftPile2(76.00, 147.75)); - - add(createCookieTable(29.43, 70.20)); - add(createCookieTable(104.75, 58.92)); - add(createCookieTable(79.31, 6.70)); - add(createCookieTable(110.06, 93.00)); - add(createCookieTable(146.47, 145.21)); - } - - if (isEaster) { - const giveBasket = give(entities.basket.type); - - // spot 1 - add(entities.basketBin(73.00, 74.00)).interact = giveBasket; - add(entities.eggBasket2(73.53, 74.88)); - add(entities.eggBasket3(74.41, 73.92)); - add(entities.eggBasket4(74.16, 74.17)); - add(createSign(74.00, 73.80, 'Egg baskets', giveBasket, entities.signQuest)); - - // spot 2 - add(entities.basketBin(33 + 70, 34 + 53)).interact = giveBasket; - add(entities.eggBasket2(33.53 + 70, 34.88 + 53)); - add(entities.eggBasket3(34.41 + 70, 33.92 + 53)); - add(entities.eggBasket4(34.16 + 70, 34.17 + 53)); - add(createSign(34 + 70, 33.8 + 53, 'Egg baskets', giveBasket, entities.signQuest)); - - // donation spot - add(createSign(62.00, 78.00, 'Donate eggs', donateEgg, entities.signDonate)); - add(entities.barrel(62.15, 78.87)).interact = donateEgg; - } + const isWinter = season === Season.Winter; + const isAutumn = season === Season.Autumn; + const isSpring = season === Season.Spring; + const isSummer = season === Season.Summer; + const isHalloween = holiday === Holiday.Halloween; + const isChristmas = holiday === Holiday.Christmas; + const isEaster = holiday === Holiday.Easter; + + function add(entity: ServerEntity) { + entity.serverFlags! |= ServerFlags.Seasonal; + return world.addEntity(entity, map); + } + + function addEntities(entities: ServerEntity[]) { + return entities.map(add); + } + + function addHolly(x: number, y: number) { + add(entities.holly(x, y + (1 / tileHeight))); + } + + function addHollyDecoration(x: number, y: number, a = true, b = true, c = true, d = true) { + if (isChristmas) { + a && addHolly(x - 2.8, y); + b && addHolly(x - 1, y); + c && addHolly(x + 1, y); + d && addHolly(x + 2.8, y); + } + } + + addHollyDecoration(65.00, 71.00, true, false, true, false); + addHollyDecoration(64.00, 76.00); + addHollyDecoration(55.00, 76.50, true, false); + + if (isSpring || isSummer) { + add(entities.flowerPatch1(70.00, 55.00)); + add(entities.flowerPatch2(46.50, 78.00)); + add(entities.flowerPatch2(80.00, 49.50)); + add(entities.flowerPatch2(100.60, 75.50)); + add(entities.flowerPatch2(78.00, 92.00)); + add(entities.flowerPatch3(69.50, 69.00)); + add(entities.flowerPatch3(48.80, 82.00)); + add(entities.flowerPatch3(86.50, 53.50)); + add(entities.flowerPatch4(66.00, 61.00)); + add(entities.flowerPatch4(87.50, 63.50)); + add(entities.flowerPatch4(56.00, 89.70)); + add(entities.flowerPatch5(53.50, 68.00)); + add(entities.flowerPatch5(77.70, 96.00)); + add(entities.flowerPatch5(77.00, 71.30)); + add(entities.flowerPatch5(87.50, 69.00)); + add(entities.flowerPatch7(93.40, 65.70)); + add(entities.flowerPatch1(112.09, 64.63)); + add(entities.flowerPatch2(107.70, 53.77)); + add(entities.flowerPatch2(69.16, 107.00)); + add(entities.flowerPatch3(106.73, 94.79)); + add(entities.flowerPatch3(113.59, 61.96)); + add(entities.flowerPatch3(70.50, 104.90)); + add(entities.flowerPatch4(58.03, 112.92)); + add(entities.flowerPatch4(53.31, 99.88)); + add(entities.flowerPatch6(106.72, 93.46)); + add(entities.flowerPatch5(116.31, 50.94)); + + add(entities.flowerPatch1(125.31, 148.25)); + add(entities.flowerPatch1(153.06, 133.58)); + add(entities.flowerPatch1(143.68, 87.20)); + add(entities.flowerPatch1(85.25, 150.75)); + add(entities.flowerPatch1(66.71, 139.75)); + + add(entities.flowerPatch2(79.13, 148.92)); + add(entities.flowerPatch2(65.56, 126.83)); + add(entities.flowerPatch2(149.75, 147.42)); + add(entities.flowerPatch2(131.78, 87.12)); + + add(entities.flowerPatch3(143.13, 151.50)); + add(entities.flowerPatch3(148.19, 131.58)); + add(entities.flowerPatch3(152.81, 95.42)); + add(entities.flowerPatch3(123.94, 83.00)); + add(entities.flowerPatch3(92.38, 156.08)); + add(entities.flowerPatch3(114.88, 134.08)); + add(entities.flowerPatch3(119.13, 154.67)); + add(entities.flowerPatch3(69.41, 133.00)); + add(entities.flowerPatch3(75.69, 154.33)); + + add(entities.flowerPatch4(62.44, 131.25)); + add(entities.flowerPatch4(86.63, 146.58)); + add(entities.flowerPatch4(58.69, 150.83)); + add(entities.flowerPatch4(123.78, 155.08)); + add(entities.flowerPatch4(146.72, 153.25)); + add(entities.flowerPatch4(143.81, 95.75)); + add(entities.flowerPatch4(126.75, 85.92)); + + add(entities.flowerPatch5(144.03, 148.25)); + add(entities.flowerPatch5(150.09, 134.08)); + add(entities.flowerPatch5(133.28, 91.08)); + add(entities.flowerPatch5(151.09, 97.83)); + add(entities.flowerPatch5(124.47, 85.33)); + add(entities.flowerPatch5(85.41, 157.08)); + add(entities.flowerPatch5(100.66, 147.75)); + add(entities.flowerPatch5(114.91, 132.50)); + add(entities.flowerPatch5(90.47, 132.08)); + add(entities.flowerPatch5(84.16, 148.67)); + add(entities.flowerPatch5(66.50, 133.25)); + add(entities.flowerPatch5(67.94, 138.67)); + add(entities.flowerPatch5(59.44, 153.25)); + add(entities.flowerPatch5(74.44, 156.33)); + + add(entities.flowerPatch2(41.28, 96.54)); + add(entities.flowerPatch3(25.19, 88.21)); + add(entities.flowerPatch3(21.66, 109.17)); + add(entities.flowerPatch4(8.53, 102.63)); + add(entities.flowerPatch5(23.28, 112.21)); + add(entities.flowerPatch1(36.00, 126.46)); + add(entities.flowerPatch3(38.50, 123.92)); + add(entities.flowerPatch4(31.50, 126.58)); + add(entities.flowerPatch4(27.81, 122.46)); + add(entities.flowerPatch3(14.91, 140.08)); + add(entities.flowerPatch5(14.75, 137.88)); + add(entities.flowerPatch3(11.66, 152.75)); + add(entities.flowerPatch4(9.69, 150.92)); + add(entities.flowerPatch4(2.66, 117.96)); + add(entities.flowerPatch2(4.44, 113.21)); + add(entities.flowerPatch2(37.97, 64.63)); + add(entities.flowerPatch4(38.94, 62.25)); + add(entities.flowerPatch2(24.28, 61.46)); + add(entities.flowerPatch5(24.53, 60.42)); + add(entities.flowerPatch6(25.84, 66.04)); + add(entities.flowerPatch4(26.25, 45.54)); + add(entities.flowerPatch6(26.56, 43.75)); + add(entities.flowerPatch2(22.63, 24.88)); + add(entities.flowerPatch4(23.47, 21.96)); + add(entities.flowerPatch4(30.69, 14.63)); + add(entities.flowerPatch3(32.81, 16.42)); + add(entities.flowerPatch3(46.13, 4.79)); + add(entities.flowerPatch3(98.97, 10.46)); + add(entities.flowerPatch4(100.94, 44.96)); + add(entities.flowerPatch4(124.34, 43.25)); + add(entities.flowerPatch3(134.41, 45.88)); + add(entities.flowerPatch4(135.47, 27.33)); + add(entities.flowerPatch6(136.63, 29.92)); + add(entities.flowerPatch1(131.78, 4.50)); + add(entities.flowerPatch4(127.97, 4.67)); + add(entities.flowerPatch4(122.09, 28.42)); + add(entities.flowerPatch6(121.53, 30.96)); + add(entities.flowerPatch6(122.31, 41.63)); + add(entities.flowerPatch6(101.91, 47.17)); + add(entities.flowerPatch3(50.66, 39.42)); + add(entities.flowerPatch4(48.47, 38.00)); + add(entities.flowerPatch6(48.78, 41.46)); + add(entities.flowerPatch6(32.63, 46.13)); + add(entities.flowerPatch6(14.94, 55.08)); + add(entities.flowerPatch7(14.25, 56.79)); + + // hill above orchard + add(entities.flowerPatch1(81.56, 17.71)); + add(entities.flowerPatch1(72.44, 12.46)); + add(entities.flowerPatch2(80.38, 13.25)); + add(entities.flowerPatch3(74.09, 21.00)); + add(entities.flowerPatch3(75.25, 5.83)); + add(entities.flowerPatch3(85.31, 10.25)); + add(entities.flowerPatch4(80.25, 4.96)); + add(entities.flowerPatch4(77.38, 18.17)); + add(entities.flowerPatch4(64.22, 5.63)); + add(entities.flowerPatch5(73.69, 19.29)); + add(entities.flowerPatch5(90.22, 11.29)); + add(entities.flowerPatch5(77.34, 3.67)); + add(entities.flowerPatch5(64.88, 12.25)); + add(entities.flowerPatch6(70.34, 12.92)); + add(entities.flowerPatch6(85.75, 9.04)); + add(entities.flowerPatch6(82.91, 3.79)); + add(entities.flowerPatch6(61.38, 5.71)); + add(entities.flowerPatch7(73.63, 3.96)); + add(entities.flowerPatch7(74.53, 18.38)); + add(entities.flowerPatch7(67.19, 11.17)); + + add(entities.cloverPatch4(43.94, 116.83)); + add(entities.cloverPatch4(51.00, 116.29)); + add(entities.cloverPatch4(60.47, 117.92)); + add(entities.cloverPatch4(58.16, 109.04)); + add(entities.cloverPatch3(60.66, 116.17)); + add(entities.cloverPatch3(57.13, 117.50)); + add(entities.cloverPatch3(58.06, 110.46)); + add(entities.cloverPatch5(48.88, 113.58)); + add(entities.cloverPatch5(64.38, 113.75)); + add(entities.cloverPatch5(41.97, 105.54)); + add(entities.cloverPatch4(52.97, 103.71)); + add(entities.cloverPatch3(79.19, 115.25)); + add(entities.cloverPatch3(89.06, 117.08)); + add(entities.cloverPatch4(83.25, 110.67)); + add(entities.cloverPatch5(82.50, 113.54)); + add(entities.cloverPatch6(85.84, 117.92)); + add(entities.cloverPatch3(103.38, 108.17)); + add(entities.cloverPatch3(110.06, 117.38)); + add(entities.cloverPatch5(113.78, 117.92)); + add(entities.cloverPatch5(105.91, 107.00)); + add(entities.cloverPatch5(104.47, 109.75)); + add(entities.cloverPatch4(117.47, 97.17)); + add(entities.cloverPatch6(89.69, 83.46)); + add(entities.cloverPatch6(108.50, 73.88)); + add(entities.cloverPatch3(99.00, 64.80)); + add(entities.cloverPatch5(97.78, 66.63)); + add(entities.cloverPatch5(90.38, 69.29)); + add(entities.cloverPatch6(88.81, 65.21)); + add(entities.cloverPatch6(80.59, 53.42)); + add(entities.cloverPatch6(115.03, 45.58)); + add(entities.cloverPatch7(84.66, 55.29)); + add(entities.cloverPatch7(116.16, 45.79)); + add(entities.cloverPatch5(81.72, 54.29)); + add(entities.cloverPatch5(43.53, 75.83)); + add(entities.cloverPatch7(46.38, 76.63)); + add(entities.cloverPatch3(69.98, 48.58)); + add(entities.cloverPatch3(73.11, 43.50)); + add(entities.cloverPatch5(67.27, 48.31)); + add(entities.cloverPatch5(74.25, 45.19)); + add(entities.cloverPatch6(69.94, 47.40)); + add(entities.cloverPatch7(89.36, 63.65)); + add(entities.clover1(72.00, 70.00)); + add(entities.cloverPatch4(55.38, 148.38)); + add(entities.cloverPatch4(42.91, 141.83)); + add(entities.cloverPatch5(64.66, 145.75)); + add(entities.cloverPatch5(48.78, 131.00)); + add(entities.cloverPatch6(46.34, 131.96)); + add(entities.cloverPatch6(91.16, 146.83)); + add(entities.cloverPatch4(103.41, 137.33)); + add(entities.cloverPatch6(134.22, 149.08)); + add(entities.cloverPatch6(133.78, 135.79)); + add(entities.cloverPatch3(151.78, 128.38)); + add(entities.cloverPatch4(156.84, 109.63)); + add(entities.cloverPatch4(123.19, 108.54)); + add(entities.cloverPatch3(152.09, 91.63)); + add(entities.cloverPatch3(126.94, 97.04)); + add(entities.cloverPatch7(149.22, 81.13)); + add(entities.cloverPatch6(157.34, 74.00)); + } + + if (isAutumn) { + add(entities.leafpileStickRed(151.19, 98.60)); + add(entities.leaves5(150.11, 99.15)); + add(entities.leaves5(143.66, 105.88)); + add(entities.leaves4(149.91, 106.00)); + add(entities.leaves2(143.14, 100.50)); + add(entities.leaves1(67.00, 48.00)); + add(entities.leaves2(68.00, 45.00)); + add(entities.leaves3(61.20, 71.50)); + add(entities.leaves3(46.00, 46.00)); + add(entities.leaves3(86.00, 47.00)); + add(entities.leaves4(83.50, 64.00)); + add(entities.leaves4(88.50, 58.00)); + add(entities.leaves4(71.50, 47.00)); + add(entities.leaves4(45.00, 76.20)); + add(entities.leaves4(84.00, 55.50)); + add(entities.leaves5(82.00, 54.00)); + add(entities.leaves5(69.00, 53.00)); + add(entities.leaves5(42.50, 48.70)); + add(entities.leaves3(70.50, 70.50)); + add(entities.leaves5(53.00, 87.00)); + add(entities.leaves4(57.00, 89.00)); + add(entities.leaves4(117.28, 46.00)); + add(entities.leaves2(61.00, 86.00)); + add(entities.leaves3(66.00, 87.00)); + add(entities.leaves1(69.00, 87.50)); + add(entities.leaves2(72.00, 85.50)); + add(entities.leafpileStickOrange(71.00, 69.50)); + add(entities.leafpileSmallYellow(44.00, 45.50)); + add(entities.leafpileSmallOrange(64.00, 72.00)); + add(entities.leafpileStickOrange(67.50, 47.30)); + add(entities.leafpileStickYellow(84.50, 46.30)); + add(entities.leafpileMediumYellow(47.30, 75.50)); + add(entities.leafpileMediumRed(51.00, 89.00)); + add(entities.leafpileBigYellow(79.00, 54.50)); + add(entities.leafpileBigstickRed(45.00, 52.20)); + add(entities.leafpileBigstickRed(50.50, 88.00)); + add(entities.leafpileSmallOrange(66.40, 58.00)); + add(entities.leafpileBigstickOrange(66.50, 69.80)); + add(entities.leafpileStickOrange(67.50, 71.60)); + add(entities.leafpileSmallOrange(67.00, 70.50)); + add(entities.leafpileSmallYellow(91.00, 95.70)); + add(entities.leafpileBigYellow(84.00, 86.30)); + add(entities.leafpileBigRed(94.00, 91.40)); + add(entities.leafpileBigYellow(76.00, 96.00)); + add(entities.leafpileMediumYellow(77.50, 97.50)); + add(entities.leafpileBigstickRed(85.80, 74.00)); + add(entities.leafpileMediumOrange(87.00, 75.50)); + add(entities.leafpileMediumAltOrange(89.00, 83.50)); + add(entities.leafpileMediumRed(80.00, 92.50)); + add(entities.leafpileSmallOrange(80.00, 84.80)); + add(entities.leaves1(85.00, 90.00)); + add(entities.leaves2(89.00, 95.00)); + add(entities.leaves3(92.00, 93.00)); + add(entities.leaves4(95.00, 88.00)); + add(entities.leaves5(90.00, 85.00)); + add(entities.leaves1(93.00, 77.00)); + add(entities.leaves2(86.00, 83.00)); + add(entities.leaves3(77.00, 82.00)); + add(entities.leaves4(75.00, 84.00)); + add(entities.leaves5(77.00, 91.00)); + add(entities.leaves1(84.00, 92.00)); + add(entities.leaves2(83.00, 97.00)); + add(entities.leaves3(89.00, 95.00)); + add(entities.leaves4(95.00, 94.00)); + add(entities.leaves5(97.00, 88.00)); + add(entities.leaves1(75.00, 81.00)); + add(entities.leaves2(79.00, 95.00)); + add(entities.leaves1(79.00, 77.00)); + add(entities.leaves2(83.00, 73.00)); + add(entities.leaves3(87.00, 71.00)); + add(entities.leaves4(98.00, 73.00)); + add(entities.leafpileBigYellow(96.25, 103.29)); + add(entities.leafpileBigstickYellow(127.69, 95.46)); + add(entities.leafpileBigOrange(118.63, 116.04)); + add(entities.leafpileBigYellow(133.50, 135.38)); + add(entities.leafpileBigRed(118.06, 136.38)); + add(entities.leafpileBigOrange(143.25, 84.21)); + add(entities.leafpileBigstickRed(89.69, 114.71)); + add(entities.leafpileMediumRed(83.19, 112.88)); + add(entities.leafpileMediumOrange(110.81, 126.13)); + add(entities.leafpileMediumYellow(121.81, 137.21)); + add(entities.leafpileMediumRed(137.50, 131.71)); + add(entities.leafpileMediumOrange(123.69, 97.96)); + add(entities.leafpileMediumYellow(109.75, 81.29)); + add(entities.leaves5(101.16, 112.71)); + add(entities.leaves5(152.19, 91.21)); + add(entities.leaves5(100.06, 83.79)); + add(entities.leaves5(151.31, 125.46)); + add(entities.leaves5(147.19, 116.13)); + add(entities.leaves5(141.63, 135.04)); + add(entities.leaves4(115.38, 127.29)); + add(entities.leaves4(143.69, 133.38)); + add(entities.leaves4(148.56, 118.88)); + add(entities.leaves4(150.44, 93.88)); + add(entities.leaves4(107.25, 86.04)); + add(entities.leaves4(122.88, 108.38)); + add(entities.leaves3(153.25, 94.96)); + add(entities.leaves3(156.44, 108.96)); + add(entities.leaves3(142.63, 134.71)); + add(entities.leaves3(151.44, 131.46)); + add(entities.leaves3(101.50, 114.71)); + add(entities.leaves3(102.50, 111.46)); + add(entities.leaves3(116.50, 128.79)); + add(entities.leaves3(107.06, 84.96)); + add(entities.leaves3(122.63, 110.46)); + add(entities.leaves3(132.94, 105.29)); + add(entities.leaves2(148.88, 82.13)); + add(entities.leaves2(109.38, 85.04)); + add(entities.leaves2(103.81, 113.29)); + add(entities.leaves2(130.06, 94.63)); + add(entities.leaves2(120.56, 106.79)); + add(entities.leaves2(148.19, 120.54)); + add(entities.leaves2(142.00, 137.13)); + add(entities.leaves2(152.81, 132.38)); + add(entities.leaves1(157.69, 88.71)); + add(entities.leaves1(149.69, 80.13)); + add(entities.leaves1(105.50, 84.21)); + add(entities.leaves1(108.63, 113.38)); + add(entities.leaves1(116.81, 116.79)); + add(entities.leaves1(117.50, 128.46)); + + add(entities.leaves5(65.09, 24.92)); + add(entities.leaves5(42.56, 22.00)); + add(entities.leaves5(45.13, 28.96)); + add(entities.leaves5(71.13, 32.71)); + add(entities.leaves4(62.31, 32.54)); + add(entities.leaves3(41.78, 28.50)); + add(entities.leaves3(63.97, 24.17)); + add(entities.leaves2(66.75, 29.21)); + add(entities.leaves2(68.91, 31.42)); + add(entities.leaves2(59.72, 43.71)); + add(entities.leaves1(61.00, 42.75)); + add(entities.leaves1(47.53, 28.92)); + add(entities.leaves1(41.00, 25.04)); + add(entities.leaves3(66.09, 22.00)); + add(entities.leaves3(72.25, 21.00)); + add(entities.leaves4(69.91, 20.50)); + add(entities.leaves2(46.09, 43.88)); + add(entities.leafpileMediumRed(43.31, 29.25)); + add(entities.leafpileStickYellow(66.63, 27.75)); + add(entities.leafpileMediumYellow(62.44, 16.54)); + add(entities.leaves3(56.81, 17.17)); + add(entities.leaves1(50.16, 31.04)); + add(entities.leafpileBigYellow(32.69, 69.21)); + add(entities.leafpileMediumAltYellow(12.31, 63.63)); + add(entities.leafpileSmallOrange(24.16, 78.67)); + add(entities.leafpileStickYellow(11.88, 64.33)); + add(entities.leaves5(30.59, 66.50)); + add(entities.leaves5(32.41, 54.00)); + add(entities.leaves5(12.34, 61.67)); + add(entities.leaves4(25.91, 77.17)); + add(entities.leaves4(22.81, 66.79)); + add(entities.leaves4(9.94, 57.79)); + add(entities.leaves3(29.69, 54.54)); + add(entities.leaves3(28.97, 64.92)); + add(entities.leaves3(24.41, 79.54)); + add(entities.leaves3(10.97, 61.04)); + add(entities.leaves2(24.94, 67.25)); + add(entities.leaves2(34.81, 68.46)); + add(entities.leaves2(32.13, 56.08)); + add(entities.leaves2(10.97, 63.83)); + add(entities.leaves2(29.03, 77.38)); + add(entities.leaves2(16.09, 48.46)); + add(entities.leaves4(10.75, 46.75)); + add(entities.leaves1(8.53, 59.79)); + add(entities.leaves1(28.75, 66.67)); + add(entities.leaves1(29.97, 51.79)); + add(entities.leaves1(14.47, 65.58)); + add(entities.leaves1(31.38, 78.63)); + add(entities.leafpileBigOrange(23.44, 36.88)); + add(entities.leafpileMediumAltRed(13.09, 45.21)); + add(entities.leafpileMediumYellow(31.22, 25.71)); + add(entities.leafpileStickYellow(26.00, 26.13)); + add(entities.leafpileSmallOrange(20.75, 19.58)); + add(entities.leaves5(24.03, 38.38)); + add(entities.leaves5(29.88, 38.88)); + add(entities.leaves5(28.16, 22.50)); + add(entities.leaves5(24.94, 16.75)); + add(entities.leaves4(12.78, 42.71)); + add(entities.leaves4(26.53, 36.63)); + add(entities.leaves4(21.34, 20.21)); + add(entities.leaves4(26.84, 26.83)); + add(entities.leaves3(28.19, 43.67)); + add(entities.leaves3(28.94, 42.04)); + add(entities.leaves3(8.81, 47.42)); + add(entities.leaves3(24.50, 20.00)); + add(entities.leaves2(32.19, 35.04)); + add(entities.leaves2(30.81, 23.54)); + add(entities.leaves2(24.63, 20.63)); + add(entities.leaves2(39.19, 15.42)); + add(entities.leaves2(15.47, 46.88)); + add(entities.leafpileSmallYellow(30.19, 40.88)); + add(entities.leafpileBigRed(11.22, 9.63)); + add(entities.leafpileMediumAltYellow(38.22, 7.71)); + add(entities.leafpileStickRed(44.44, 6.25)); + add(entities.leafpileSmallOrange(25.31, 8.46)); + add(entities.leaves5(20.84, 6.17)); + add(entities.leaves5(43.78, 6.04)); + add(entities.leaves4(9.06, 7.88)); + add(entities.leaves4(38.94, 5.75)); + add(entities.leaves4(40.34, 13.96)); + add(entities.leaves3(42.78, 15.04)); + add(entities.leaves3(22.81, 8.50)); + add(entities.leaves3(13.06, 9.71)); + add(entities.leaves2(25.25, 6.83)); + add(entities.leaves2(8.44, 5.50)); + add(entities.leaves1(9.16, 5.17)); + add(entities.leaves1(20.44, 5.21)); + add(entities.leaves1(33.75, 22.92)); + add(entities.leaves1(29.09, 21.21)); + add(entities.leaves1(42.03, 5.17)); + add(entities.leafpileMediumAltOrange(59.41, 5.83)); + add(entities.leafpileMediumAltYellow(90.59, 3.33)); + add(entities.leafpileStickRed(75.69, 5.67)); + add(entities.leafpileSmallOrange(70.53, 23.00)); + add(entities.leaves5(84.19, 21.67)); + add(entities.leaves5(77.75, 5.25)); + add(entities.leaves4(75.16, 3.92)); + add(entities.leaves4(76.31, 15.38)); + add(entities.leaves3(87.00, 20.79)); + add(entities.leaves3(76.91, 14.33)); + add(entities.leaves3(89.41, 4.63)); + add(entities.leaves3(71.38, 6.08)); + add(entities.leaves2(71.50, 7.08)); + add(entities.leaves2(83.16, 23.71)); + add(entities.leaves2(88.84, 2.71)); + add(entities.leaves1(84.00, 23.88)); + add(entities.leaves1(75.03, 16.58)); + add(entities.leaves1(87.81, 4.71)); + add(entities.leaves1(91.88, 2.29)); + add(entities.leaves1(73.13, 8.08)); + add(entities.leaves1(72.03, 23.00)); + add(entities.leafpileStickRed(84.03, 36.21)); + add(entities.leafpileSmallYellow(82.78, 30.21)); + add(entities.leaves5(81.00, 32.67)); + add(entities.leaves5(87.53, 34.88)); + add(entities.leaves4(87.78, 37.00)); + add(entities.leaves4(79.47, 35.29)); + add(entities.leaves4(87.69, 29.83)); + add(entities.leaves3(78.34, 36.75)); + add(entities.leaves3(73.16, 35.21)); + add(entities.leaves3(85.66, 33.13)); + add(entities.leaves2(82.81, 36.13)); + add(entities.leaves2(83.94, 28.75)); + add(entities.leaves2(81.94, 29.71)); + add(entities.leaves2(87.00, 29.50)); + add(entities.leaves1(83.81, 30.38)); + add(entities.leaves1(85.38, 37.54)); + add(entities.leaves1(84.44, 37.00)); + add(entities.leaves1(82.34, 20.67)); + add(entities.leaves5(62.47, 5.17)); + add(entities.leaves4(58.72, 6.75)); + add(entities.leaves2(61.34, 3.29)); + add(entities.leaves1(63.25, 5.17)); + add(entities.leaves2(64.00, 2.79)); + add(entities.leafpileMediumYellow(107.56, 35.00)); + add(entities.leafpileStickYellow(115.25, 22.21)); + add(entities.leafpileBigYellow(119.63, 6.58)); + add(entities.leafpileSmallYellow(105.16, 6.21)); + add(entities.leafpileStickYellow(118.34, 33.83)); + add(entities.leaves5(108.16, 32.17)); + add(entities.leaves5(106.63, 21.54)); + add(entities.leaves5(123.50, 9.67)); + add(entities.leaves5(127.22, 34.83)); + add(entities.leaves4(119.25, 7.67)); + add(entities.leaves4(117.25, 8.50)); + add(entities.leaves4(99.06, 37.79)); + add(entities.leaves4(103.13, 19.50)); + add(entities.leaves4(102.75, 4.29)); + add(entities.leaves3(109.03, 24.79)); + add(entities.leaves3(100.38, 21.04)); + add(entities.leaves3(97.16, 36.79)); + add(entities.leaves3(107.97, 9.58)); + add(entities.leaves3(103.78, 7.21)); + add(entities.leaves3(125.94, 12.38)); + add(entities.leaves3(123.09, 12.08)); + add(entities.leaves3(117.41, 34.42)); + add(entities.leaves3(127.53, 38.17)); + add(entities.leaves2(115.56, 34.75)); + add(entities.leaves2(109.88, 33.42)); + add(entities.leaves2(108.41, 25.54)); + add(entities.leaves2(113.69, 21.25)); + add(entities.leaves2(116.34, 7.29)); + add(entities.leaves2(109.38, 7.79)); + add(entities.leaves2(99.69, 5.67)); + add(entities.leaves2(100.88, 19.29)); + add(entities.leaves2(103.63, 35.42)); + add(entities.leaves2(123.16, 36.71)); + add(entities.leaves1(115.16, 20.00)); + add(entities.leaves1(114.69, 22.75)); + add(entities.leaves1(106.69, 24.67)); + add(entities.leaves1(107.91, 8.17)); + add(entities.leaves1(98.56, 5.67)); + add(entities.leaves1(121.91, 6.42)); + add(entities.leaves1(121.84, 9.00)); + add(entities.leaves1(124.28, 13.38)); + add(entities.leaves1(129.13, 37.21)); + add(entities.leaves1(100.31, 36.67)); + add(entities.leaves1(103.75, 23.79)); + add(entities.leaves3(104.19, 15.67)); + add(entities.leaves2(104.00, 17.63)); + add(entities.leafpileBigOrange(137.00, 24.29)); + add(entities.leafpileMediumAltYellow(150.91, 9.96)); + add(entities.leafpileStickYellow(138.81, 9.75)); + add(entities.leaves5(153.50, 23.21)); + add(entities.leaves5(151.94, 5.50)); + add(entities.leaves5(136.25, 3.79)); + add(entities.leaves4(136.75, 8.54)); + add(entities.leaves4(150.34, 12.29)); + add(entities.leaves3(141.63, 8.67)); + add(entities.leaves3(139.16, 23.92)); + add(entities.leaves2(141.03, 22.71)); + add(entities.leaves2(137.41, 25.92)); + add(entities.leaves2(155.00, 26.13)); + add(entities.leaves2(148.31, 11.00)); + add(entities.leaves3(139.63, 2.50)); + add(entities.leaves1(137.13, 26.92)); + add(entities.leaves1(156.41, 26.67)); + add(entities.leaves1(148.88, 12.33)); + add(entities.leaves1(152.66, 7.13)); + add(entities.leaves4(155.44, 6.29)); + add(entities.leaves4(138.94, 5.00)); + add(entities.leaves3(139.19, 10.50)); + add(entities.leaves3(141.06, 25.46)); + add(entities.leaves1(140.13, 27.13)); + add(entities.leaves1(153.84, 22.63)); + add(entities.leafpileStickRed(141.97, 24.58)); + add(entities.leaves1(115.78, 44.00)); + add(entities.leaves2(117.47, 43.38)); + add(entities.leaves3(103.44, 44.88)); + add(entities.leaves2(102.97, 46.21)); + add(entities.leaves1(104.03, 46.50)); + add(entities.leaves4(97.44, 64.04)); + add(entities.leaves2(96.56, 62.67)); + add(entities.leaves3(118.56, 69.04)); + add(entities.leaves2(116.56, 69.13)); + add(entities.leaves1(117.59, 68.46)); + add(entities.leaves1(119.34, 70.96)); + add(entities.leaves2(87.13, 44.96)); + add(entities.leaves2(83.31, 53.04)); + add(entities.leaves5(155.84, 73.50)); + add(entities.leaves4(156.66, 75.67)); + add(entities.leaves1(154.56, 75.88)); + add(entities.leaves4(148.13, 80.63)); + add(entities.leaves2(147.16, 79.13)); + add(entities.leaves2(139.81, 83.63)); + add(entities.leaves3(141.75, 85.67)); + add(entities.leaves1(141.19, 84.75)); + add(entities.leaves1(133.78, 67.58)); + add(entities.leaves2(134.66, 67.67)); + add(entities.leaves3(136.22, 66.25)); + add(entities.leaves1(135.81, 67.83)); + add(entities.leaves1(142.66, 86.50)); + add(entities.leaves1(157.56, 77.25)); + add(entities.leaves4(151.13, 87.50)); + add(entities.leaves2(150.66, 86.21)); + add(entities.leaves2(158.03, 85.54)); + add(entities.leaves4(144.44, 94.21)); + add(entities.leaves4(127.34, 92.50)); + add(entities.leaves2(126.97, 91.79)); + add(entities.leaves2(131.22, 95.79)); + add(entities.leaves1(125.72, 99.46)); + add(entities.leaves1(125.13, 108.04)); + add(entities.leaves1(155.22, 106.42)); + add(entities.leaves2(152.72, 99.21)); + add(entities.leaves2(147.91, 95.67)); + add(entities.leaves2(132.94, 107.50)); + add(entities.leaves3(131.41, 149.21)); + add(entities.leaves2(131.09, 148.13)); + add(entities.leaves1(131.72, 147.54)); + add(entities.leaves4(134.91, 148.83)); + add(entities.leaves4(137.22, 137.63)); + add(entities.leaves4(110.41, 130.33)); + add(entities.leaves4(118.78, 138.54)); + add(entities.leaves2(116.84, 137.79)); + add(entities.leaves2(112.13, 130.17)); + add(entities.leaves3(25.44, 84.75)); + add(entities.leaves1(26.38, 83.79)); + add(entities.leaves4(86.25, 114.58)); + add(entities.leaves5(89.63, 117.63)); + add(entities.leaves2(86.25, 117.00)); + add(entities.leaves1(84.25, 117.21)); + add(entities.leaves5(41.75, 82.58)); + add(entities.leaves2(38.91, 80.96)); + add(entities.leaves1(40.78, 84.29)); + add(entities.leaves3(40.75, 81.38)); + } + + function addSnowpony(x: number, y: number, type: number) { + const snowpony = entities.snowponies[type - 1]; + const entity = add(snowpony(x, y + 0.5)); + lockTile(map, entity.x - 0.5, entity.y); + lockTile(map, entity.x + 0.5, entity.y); + } + + function addSnowPile(entity: ServerEntity) { + add(entity); + lockTile(map, entity.x - 0.5, entity.y); + lockTile(map, entity.x + 0.5, entity.y); + + if ( + entity.type === entities.snowPileSmall.type || + entity.type === entities.snowPileMedium.type || + entity.type === entities.snowPileBig.type + ) { + lockTile(map, entity.x - 0.5, entity.y - 1); + lockTile(map, entity.x + 0.5, entity.y - 1); + } + + if ( + entity.type === entities.snowPileMedium.type || + entity.type === entities.snowPileBig.type + ) { + lockTile(map, entity.x - 1, entity.y); + lockTile(map, entity.x - 0.5, entity.y + 1); + lockTile(map, entity.x + 0.5, entity.y + 1); + } + + if (entity.type === entities.snowPileBig.type) { + lockTile(map, entity.x + 1, entity.y); + lockTile(map, entity.x - 1.5, entity.y); + lockTile(map, entity.x - 1, entity.y + 1); + lockTile(map, entity.x + 1, entity.y + 1); + } + } + + if (isWinter) { + addSnowpony(44.00, 57.00, 1); + addSnowpony(65.00, 64.00, 1); + addSnowpony(68.00, 65.00, 2); + addSnowpony(84.00, 56.00, 2); + addSnowpony(67.00, 82.00, 1); + addSnowpony(59.00, 87.00, 2); + addSnowpony(85.19, 152.67, 3); + addSnowpony(86.16, 153.04, 6); + addSnowpony(86.09, 122.83, 4); + addSnowpony(115.09, 109.13, 5); + addSnowpony(108.31, 128.29, 3); + addSnowpony(88.44, 104.71, 7); + addSnowpony(106.50, 86.13, 9); + addSnowpony(107.31, 86.67, 8); + addSnowpony(87.88, 92.33, 5); + addSnowpony(107.50, 58.83, 1); + addSnowpony(85.09, 56.92, 1); + addSnowpony(83.38, 57.25, 6); + addSnowpony(66.03, 83.13, 7); + addSnowpony(51.69, 102.38, 1); + addSnowpony(69.41, 112.50, 1); + addSnowpony(67.84, 111.17, 4); + addSnowpony(53.69, 133.33, 9); + addSnowpony(59.75, 119.79, 6); + addSnowpony(65.03, 156.46, 2); + addSnowpony(49.13, 148.38, 3); + addSnowpony(119.19, 149.96, 3); + addSnowpony(136.06, 156.00, 7); + addSnowpony(141.22, 130.92, 5); + addSnowpony(116.06, 136.58, 2); + addSnowpony(147.50, 101.04, 4); + addSnowpony(138.91, 97.92, 3); + addSnowpony(125.50, 88.75, 2); + addSnowpony(137.13, 67.50, 7); + addSnowpony(115.13, 65.88, 2); + addSnowpony(135.53, 44.33, 2); + addSnowpony(153.34, 51.00, 5); + addSnowpony(156.78, 60.08, 3); + addSnowpony(116.88, 46.79, 8); + addSnowpony(123.56, 53.67, 3); + addSnowpony(100.56, 44.75, 4); + addSnowpony(156.31, 124.88, 3); + addSnowpony(149.50, 150.00, 6); + addSnowpony(54.41, 132.83, 8); + + addSnowpony(29.69, 67.63, 1); + addSnowpony(26.72, 44.42, 2); + addSnowpony(23.22, 26.04, 3); + addSnowpony(20.97, 27.21, 4); + addSnowpony(12.97, 14.71, 5); + addSnowpony(37.81, 28.21, 6); + addSnowpony(50.38, 4.00, 7); + addSnowpony(81.78, 18.00, 8); + addSnowpony(80.00, 19.50, 1); + addSnowpony(84.25, 3.08, 2); + addSnowpony(97.19, 21.25, 3); + addSnowpony(86.47, 38.42, 4); + addSnowpony(122.47, 27.42, 5); + addSnowpony(133.44, 7.54, 6); + addSnowpony(130.31, 9.04, 7); + addSnowpony(143.41, 19.42, 8); + addSnowpony(20.63, 78.13, 1); + addSnowpony(25.25, 90.63, 2); + addSnowpony(27.56, 102.08, 3); + addSnowpony(22.19, 109.83, 4); + addSnowpony(5.72, 113.42, 5); + addSnowpony(2.19, 116.75, 6); + addSnowpony(13.75, 138.71, 7); + addSnowpony(3.50, 150.63, 8); + addSnowpony(35.09, 151.63, 1); + addSnowpony(37.75, 123.75, 2); + + addSnowPile(entities.snowPileBig(67.06, 78.46)); + addSnowPile(entities.snowPileMedium(65.25, 77.21)); + addSnowPile(entities.snowPileSmall(61.13, 70.08)); + addSnowPile(entities.snowPileSmall(68.44, 71.25)); + addSnowPile(entities.snowPileBig(74.84, 94.58)); + addSnowPile(entities.snowPileSmall(76.56, 95.50)); + addSnowPile(entities.snowPileBig(52.97, 60.04)); + addSnowPile(entities.snowPileBig(80.63, 54.29)); + addSnowPile(entities.snowPileSmall(82.94, 65.04)); + addSnowPile(entities.snowPileBig(117.19, 80.38)); + addSnowPile(entities.snowPileSmall(122.19, 83.38)); + addSnowPile(entities.snowPileSmall(135.81, 66.58)); + addSnowPile(entities.snowPileBig(150.25, 99.42)); + addSnowPile(entities.snowPileMedium(143.50, 106.67)); + addSnowPile(entities.snowPileSmall(148.28, 98.88)); + addSnowPile(entities.snowPileMedium(115.75, 98.04)); + addSnowPile(entities.snowPileMedium(139.81, 85.25)); + addSnowPile(entities.snowPileMedium(137.53, 98.79)); + addSnowPile(entities.snowPileMedium(115.50, 45.79)); + addSnowPile(entities.snowPileBig(151.88, 138.79)); + addSnowPile(entities.snowPileSmall(150.09, 138.00)); + addSnowPile(entities.snowPileSmall(140.28, 144.50)); + addSnowPile(entities.snowPileSmall(102.56, 147.13)); + addSnowPile(entities.snowPileBig(92.75, 139.75)); + addSnowPile(entities.snowPileBig(74.34, 157.38)); + addSnowPile(entities.snowPileSmall(73.19, 155.96)); + addSnowPile(entities.snowPileSmall(87.09, 147.63)); + addSnowPile(entities.snowPileBig(69.59, 134.54)); + addSnowPile(entities.snowPileMedium(84.66, 130.25)); + addSnowPile(entities.snowPileMedium(77.41, 121.38)); + addSnowPile(entities.snowPileSmall(79.13, 120.75)); + addSnowPile(entities.snowPileSmall(77.34, 133.83)); + addSnowPile(entities.snowPileBig(72.84, 106.33)); + addSnowPile(entities.snowPileMedium(63.41, 121.63)); + addSnowPile(entities.snowPileSmall(64.91, 122.29)); + addSnowPile(entities.snowPileTiny(71.06, 70.04)); + addSnowPile(entities.snowPileTiny(64.25, 77.88)); + addSnowPile(entities.snowPileTiny(44.41, 76.17)); + addSnowPile(entities.snowPileTiny(51.28, 69.00)); + addSnowPile(entities.snowPileTiny(74.56, 83.79)); + addSnowPile(entities.snowPileTinier(64.72, 78.29)); + addSnowPile(entities.snowPileTinier(67.31, 71.04)); + addSnowPile(entities.snowPileTinier(60.56, 69.21)); + addSnowPile(entities.snowPileTinier(51.44, 59.58)); + addSnowPile(entities.snowPileTinier(74.19, 84.25)); + addSnowPile(entities.snowPileTiny(49.75, 87.50)); + addSnowPile(entities.snowPileTiny(77.31, 100.83)); + addSnowPile(entities.snowPileTiny(75.81, 118.29)); + addSnowPile(entities.snowPileTiny(74.25, 106.83)); + addSnowPile(entities.snowPileTinier(76.22, 118.58)); + addSnowPile(entities.snowPileTinier(73.75, 107.00)); + addSnowPile(entities.snowPileTinier(75.63, 95.92)); + addSnowPile(entities.snowPileTiny(62.28, 121.96)); + addSnowPile(entities.snowPileTinier(62.91, 122.42)); + addSnowPile(entities.snowPileTinier(86.19, 147.25)); + addSnowPile(entities.snowPileTinier(78.50, 121.54)); + addSnowPile(entities.snowPileTiny(85.69, 130.50)); + addSnowPile(entities.snowPileTiny(72.44, 156.25)); + addSnowPile(entities.snowPileTiny(101.75, 146.29)); + addSnowPile(entities.snowPileTiny(112.34, 152.42)); + addSnowPile(entities.snowPileTiny(93.28, 110.67)); + addSnowPile(entities.snowPileTiny(103.28, 122.71)); + addSnowPile(entities.snowPileTinier(103.75, 122.96)); + addSnowPile(entities.snowPileTinier(93.72, 111.00)); + addSnowPile(entities.snowPileTiny(116.81, 97.13)); + addSnowPile(entities.snowPileTiny(110.28, 91.79)); + addSnowPile(entities.snowPileTiny(115.78, 80.96)); + addSnowPile(entities.snowPileTinier(116.16, 81.33)); + addSnowPile(entities.snowPileTinier(123.00, 83.42)); + addSnowPile(entities.snowPileTinier(116.66, 45.96)); + addSnowPile(entities.snowPileTinier(140.84, 85.33)); + addSnowPile(entities.snowPileTiny(138.75, 98.96)); + addSnowPile(entities.snowPileTinier(139.09, 98.79)); + addSnowPile(entities.snowPileTiny(148.47, 99.33)); + addSnowPile(entities.snowPileTinier(147.34, 98.58)); + addSnowPile(entities.snowPileTinier(144.66, 106.79)); + addSnowPile(entities.snowPileTiny(149.13, 138.50)); + addSnowPile(entities.snowPileTiny(141.09, 144.63)); + addSnowPile(entities.snowPileTinier(149.63, 138.88)); + addSnowPile(entities.snowPileTinier(140.63, 145.00)); + addSnowPile(entities.snowPileTiny(85.88, 147.67)); + addSnowPile(entities.snowPileTiny(94.34, 139.96)); + addSnowPile(entities.snowPileTinier(82.88, 56.88)); + addSnowPile(entities.snowPileTiny(85.84, 56.54)); + addSnowPile(entities.snowPileTinier(85.31, 57.13)); + addSnowPile(entities.snowPileTiny(66.91, 82.79)); + addSnowPile(entities.snowPileTinier(66.47, 83.21)); + addSnowPile(entities.snowPileTinier(65.38, 82.92)); + addSnowPile(entities.snowPileTinier(68.75, 112.17)); + addSnowPile(entities.snowPileTinier(54.13, 133.38)); + addSnowPile(entities.snowPileTinier(84.44, 152.42)); + addSnowPile(entities.snowPileTiny(85.41, 152.96)); + addSnowPile(entities.snowPileTiny(86.56, 152.04)); + addSnowPile(entities.snowPileTinier(86.97, 152.38)); + addSnowPile(entities.snowPileTinier(105.84, 85.96)); + addSnowPile(entities.snowPileTiny(106.53, 86.29)); + addSnowPile(entities.snowPileTinier(107.91, 86.42)); + addSnowPile(entities.snowPileTinier(155.66, 61.17)); + addSnowPile(entities.snowPileTiny(157.47, 60.42)); + addSnowPile(entities.snowPileTinier(157.72, 60.21)); + addSnowPile(entities.snowPileTinier(116.22, 46.42)); + addSnowPile(entities.snowPileBig(94.25, 68.29)); + addSnowPile(entities.snowPileMedium(73.56, 43.42)); + addSnowPile(entities.snowPileMedium(52.75, 38.46)); + addSnowPile(entities.snowPileSmall(52.03, 39.88)); + addSnowPile(entities.snowPileBig(43.72, 30.13)); + addSnowPile(entities.snowPileSmall(54.22, 17.13)); + addSnowPile(entities.snowPileTiny(48.69, 30.38)); + addSnowPile(entities.snowPileTiny(53.22, 39.46)); + addSnowPile(entities.snowPileTiny(60.19, 34.79)); + addSnowPile(entities.snowPileSmall(79.28, 37.21)); + addSnowPile(entities.snowPileSmall(84.19, 22.21)); + addSnowPile(entities.snowPileBig(99.34, 21.25)); + addSnowPile(entities.snowPileTiny(97.88, 22.29)); + addSnowPile(entities.snowPileSmall(106.94, 42.13)); + addSnowPile(entities.snowPileMedium(120.38, 26.79)); + addSnowPile(entities.snowPileSmall(121.00, 28.00)); + addSnowPile(entities.snowPileMedium(145.56, 20.17)); + addSnowPile(entities.snowPileBig(144.41, 21.29)); + addSnowPile(entities.snowPileBig(134.91, 4.46)); + addSnowPile(entities.snowPileSmall(120.56, 7.88)); + addSnowPile(entities.snowPileSmall(155.88, 55.21)); + addSnowPile(entities.snowPileSmall(148.28, 114.38)); + addSnowPile(entities.snowPileSmall(138.84, 154.50)); + addSnowPile(entities.snowPileMedium(139.88, 153.13)); + addSnowPile(entities.snowPileMedium(60.38, 150.25)); + addSnowPile(entities.snowPileBig(36.41, 149.33)); + addSnowPile(entities.snowPileMedium(37.31, 150.63)); + addSnowPile(entities.snowPileMedium(16.25, 138.88)); + addSnowPile(entities.snowPileSmall(15.31, 139.83)); + addSnowPile(entities.snowPileSmall(2.63, 149.38)); + addSnowPile(entities.snowPileMedium(4.19, 113.96)); + addSnowPile(entities.snowPileSmall(4.97, 115.08)); + addSnowPile(entities.snowPileSmall(29.34, 102.83)); + addSnowPile(entities.snowPileMedium(25.66, 89.00)); + addSnowPile(entities.snowPileSmall(26.91, 89.88)); + addSnowPile(entities.snowPileSmall(32.91, 69.38)); + addSnowPile(entities.snowPileSmall(21.13, 76.46)); + addSnowPile(entities.snowPileMedium(15.81, 53.46)); + addSnowPile(entities.snowPileMedium(25.94, 27.25)); + addSnowPile(entities.snowPileSmall(24.22, 28.50)); + addSnowPile(entities.snowPileBig(13.53, 17.13)); + addSnowPile(entities.snowPileSmall(12.34, 16.38)); + addSnowPile(entities.snowPileSmall(22.59, 5.46)); + addSnowPile(entities.snowPileMedium(52.38, 3.96)); + addSnowPile(entities.snowPileTiny(51.13, 4.71)); + addSnowPile(entities.snowPileTinier(50.94, 13.33)); + addSnowPile(entities.snowPileSmall(89.22, 2.71)); + } + + function addXmasTree(x: number, y: number) { + x -= 33; + y -= 24; + addEntities(entities.pine(x + 33, y + 24, 1)); + add(entities.xmasLights(x + 33, y + 24)); + add(entities.xmasLight(x + 33.22, y + 24.88)); + add(entities.xmasLight(x + 33.59, y + 24.04)); + add(entities.xmasLight(x + 32.28, y + 24.08)); + add(entities.xmasLight(x + 31.84, y + 24.42)); + add(entities.xmasLight(x + 30.97, y + 24.00)); + add(entities.xmasLight(x + 32.31, y + 22.63)); + add(entities.xmasLight(x + 31.63, y + 21.75)); + add(entities.xmasLight(x + 32.75, y + 20.92)); + add(entities.xmasLight(x + 32.19, y + 19.50)); + add(entities.xmasLight(x + 32.75, y + 18.17)); + add(entities.xmasLight(x + 33.38, y + 18.42)); + add(entities.xmasLight(x + 33.38, y + 19.79)); + add(entities.xmasLight(x + 33.94, y + 20.42)); + add(entities.xmasLight(x + 33.63, y + 22.33)); + add(entities.xmasLight(x + 34.00, y + 22.46)); + add(entities.xmasLight(x + 34.38, y + 21.83)); + add(entities.xmasLight(x + 35.00, y + 23.63)); + add(entities.xmasLight(x + 34.56, y + 24.17)); + } + + function addGraveWithGhost(x: number, y: number, tombType: number) { + const tombs = [entities.tombstone1, entities.tombstone2]; + const tomb = add(tombs[tombType](x, y)); + const createGhost = tombType === 0 ? entities.ghost1 : entities.ghost2; + const createGhostHooves = tombType === 0 ? entities.ghostHooves1 : entities.ghostHooves2; + const ghost = add(createGhost(x + toWorldX(1), y)); + const hooves = add(createGhostHooves(x + toWorldX(1), y)); + return { tomb, ghost, hooves }; + } + + const addGhost = (x: number, y: number, tombType: number, anims?: number[]) => { + const { ghost, hooves, tomb } = addGraveWithGhost(x, y, tombType); + const randomDelay = () => random(1 * 60, 5 * 60, true); + let delay = randomDelay(); + let resetDelay = 0; + let reset = true; + + ghost.serverUpdate = delta => { + delay -= delta; + resetDelay -= delta; + + if (delay < 0) { + const flip = Math.random() > 0.5; + const anim = sample(anims || (tomb.type === entities.tombstone1.type ? [1, 3] : [1, 2, 3]))!; + setEntityAnimation(ghost, anim, flip); + setEntityAnimation(hooves, anim, flip); + delay = randomDelay(); + reset = false; + resetDelay = 5; + } else if (!reset && resetDelay < 0) { + setEntityAnimation(ghost, GhostAnimation.None); + setEntityAnimation(hooves, GhostAnimation.None); + reset = true; + } + }; + }; + + const createEyes = (x: number, y: number) => { + const entity = add(entities.eyes(x, y)); + let delay = 5; + let open = true; + + entity.serverUpdate = delta => { + delay -= delta; + + if (delay < 0) { + if (open) { + setEntityAnimation(entity, 1); + delay = 0.2; + open = false; + } else { + setEntityAnimation(entity, 0); + delay = random(5, 10, true); + open = true; + } + } + }; + }; + + const addJacko = (x: number, y: number) => { + add(entities.jacko(x, y)).boop = boopLight; + }; + + const addJackoLanternSpot = (x: number, y: number) => { + const giveLantern = give(entities.jackoLanternOn.type, 'Now go collect some candies!'); + + add(createSign(x, y, 'Jack-o-Lanterns', giveLantern, entities.signQuest)); + + addJacko(x + 0.5, y - 0.3); + addJacko(x - 0.3, y + 0.3); + + add(entities.jackoLanternOff(x + 0.2, 17.7 + y - 18.5)); + add(entities.jackoLanternOff(x + 0.7, 17.8 + y - 18.5)); + add(entities.jackoLanternOff(x + 0.3, y + 0.2)); + add(entities.jackoLanternOff(x + 0.7, y + 0.5)); + add(entities.jackoLanternOn(x, 19.2 + y - 18.5)); + add(entities.jackoLanternOn(x + 0.5, 19.4 + y - 18.5)); + add(entities.jackoLanternOn(x - 0.9, 19.2 + y - 18.5)); + }; + + if (isHalloween) { + const donateX = 64, donateY = 79; + add(createSign(donateX, donateY, 'Donate candies', donateCandy, entities.signDonate)); + add(entities.box(donateX + 0.1, donateY + 1.2)).interact = donateCandy; + + add(entities.jackoOn(132.69, 108.79)); + add(entities.jackoOn(131.31, 134.79)); + add(entities.jackoOn(134.55, 139.38)); + add(entities.jackoOn(122.94, 105.38)); + add(entities.jackoOn(126.63, 107.38)); + add(entities.jackoOn(127.75, 110.88)); + add(entities.jackoOn(126.13, 124.38)); + add(entities.jackoOn(126.44, 133.71)); + add(entities.jackoOn(126.88, 137.71)); + add(entities.jackoOn(132.38, 136.88)); + + addGhost(149.53, 135.58, 1); + addGhost(144.38, 100.21, 1); + addGhost(149.84, 100.58, 1); + add(entities.tombstone2(144.56, 105.25)); + addGhost(146.34, 105.29, 1); + add(entities.tombstone1(146.28, 100.50)); + addGhost(148.25, 100.50, 0); + add(entities.tombstone1(148.34, 105.38)); + add(entities.tombstone1(150.03, 105.21)); + addGhost(102.28, 93.29, 1); + addGhost(66.59, 57.46, 1); + addGhost(72.19, 69.21, 1); + + createEyes(155.52, 102.73); + createEyes(156.81, 104.02); + createEyes(155.66, 104.71); + createEyes(158.05, 106.65); + createEyes(147.25, 111.96); + createEyes(146.59, 109.33); + createEyes(148.19, 110.27); + createEyes(141.31, 111.52); + createEyes(145.23, 115.35); + createEyes(144.16, 114.44); + createEyes(142.39, 107.33); + createEyes(141.19, 108.63); + createEyes(142.39, 111.02); + createEyes(149.59, 113.52); + createEyes(150.22, 113.06); + createEyes(140.02, 107.44); + createEyes(142.83, 115.50); + createEyes(138.78, 111.50); + createEyes(137.22, 112.27); + + createEyes(144.59, 93.00); + createEyes(145.43, 93.95); + createEyes(147.40, 91.25); + createEyes(146.90, 93.04); + createEyes(148.68, 92.37); + createEyes(153.15, 94.29); + createEyes(154.46, 94.95); + createEyes(152.40, 96.08); + createEyes(154.87, 97.20); + createEyes(155.53, 97.79); + + add(entities.jackoLanternOn(131.97, 149.77)); + add(entities.jackoLanternOn(132.02, 83.46)); + add(entities.jackoLanternOn(131.98, 149.85)); + add(entities.jackoLanternOn(106.45, 150.31)); + add(entities.jackoLanternOn(107.14, 149.79)); + add(entities.jackoLanternOn(80.33, 93.19)); + + addGhost(45.34, 152.83, 0); + addGhost(45.75, 106.08, 0); + addGhost(59.78, 96.83, 0); + addGhost(81.00, 93.00, 0); + addGhost(83.62, 132.875, 0); + addGhost(115.96, 45.79, 0); + addGhost(149.75, 81.58, 0); + addGhost(52.40, 138.70, 1); + addGhost(52.12, 88.29, 1); + addGhost(49.06, 64.83, 1); + addGhost(48.43, 50.83, 1); + addGhost(80.72, 115.83, 1); + addGhost(86.50, 46.83, 1); + addGhost(113.812, 107.37, 1); + addGhost(118.062, 125.12, 1); + addGhost(106.59, 149.83, 1); + addGhost(132.43, 83.12, 1); + addGhost(135.46, 67.66, 1); + addGhost(131.53, 149.50, 1); + + add(entities.jackoLanternOn(45.84, 153.20)); + add(entities.jackoLanternOn(46.15, 152.75)); + add(entities.jackoLanternOn(53.00, 138.70)); + add(entities.jackoLanternOn(48.56, 64.58)); + add(entities.jackoLanternOn(48.93, 51.04)); + add(entities.jackoLanternOn(72.53, 69.58)); + add(entities.jackoLanternOn(80.31, 116.16)); + add(entities.jackoLanternOn(83.06, 133.29)); + add(entities.jackoLanternOn(86.15, 47.12)); + add(entities.jackoLanternOn(115.50, 46.13)); + add(entities.jackoLanternOn(114.25, 107.83)); + add(entities.jackoLanternOn(114.53, 107.41)); + add(entities.jackoLanternOn(132.03, 83.41)); + add(entities.jackoLanternOn(132.93, 83.08)); + add(entities.jackoLanternOn(115.50, 46.12)); + add(entities.jackoLanternOn(149.15, 81.37)); + add(entities.jackoLanternOn(144.15, 105.58)); + add(entities.jackoLanternOn(146.75, 100.83)); + add(entities.jackoLanternOn(150.63, 105.58)); + + add(entities.jackoLanternOn(52.68, 139.04)); + add(entities.jackoLanternOn(46.28, 106.41)); + add(entities.jackoLanternOn(52.53, 88.62)); + add(entities.jackoLanternOn(135.84, 67.91)); + + add(entities.jackoLanternOff(52.62, 88.12)); + add(entities.jackoLanternOff(106.09, 150.04)); + add(entities.jackoLanternOff(51.93, 138.58)); + + addJacko(71.90, 70.00); + addJacko(90.20, 66.40); + addJacko(80.00, 53.80); + addJacko(67.30, 88.60); + addJacko(86.20, 97.30); + addJacko(99.70, 92.00); + addJacko(91.00, 86.10); + addJacko(85.70, 75.10); + addJacko(84.50, 91.40); + addJacko(99.00, 87.00); + addJacko(91.60, 55.90); + addJacko(95.60, 47.50); + addJacko(98.40, 44.20); + addJacko(100.10, 49.00); + addJacko(87.20, 46.50); + addJacko(69.70, 47.20); + addJacko(49.10, 89.50); + addJacko(59.10, 97.60); + addJacko(46.70, 98.10); + addJacko(52.20, 77.40); + addJacko(62.50, 72.20); + addJacko(66.70, 71.40); + + addJacko(97.50, 100.79); + addJacko(110.34, 92.79); + addJacko(117.34, 99.38); + addJacko(108.94, 101.50); + addJacko(109.63, 102.21); + addJacko(108.00, 85.00); + addJacko(93.50, 106.17); + addJacko(113.22, 108.17); + addJacko(117.56, 117.38); + addJacko(118.81, 125.33); + addJacko(117.25, 127.71); + addJacko(118.03, 129.50); + addJacko(93.22, 135.46); + addJacko(78.94, 147.54); + addJacko(82.00, 147.67); + addJacko(102.06, 146.83); + addJacko(96.59, 146.58); + addJacko(108.75, 141.33); + addJacko(110.03, 146.71); + addJacko(147.88, 148.04); + addJacko(145.41, 150.08); + addJacko(147.28, 153.67); + addJacko(150.78, 153.00); + addJacko(150.78, 149.08); + addJacko(152.88, 131.38); + addJacko(143.25, 137.17); + addJacko(141.22, 116.13); + addJacko(138.16, 132.46); + addJacko(141.34, 102.04); + addJacko(141.31, 103.88); + addJacko(134.56, 86.71); + addJacko(139.34, 85.29); + addJacko(148.50, 81.71); + addJacko(150.09, 82.54); + addJacko(129.78, 98.67); + addJacko(136.75, 66.38); + addJacko(117.63, 70.33); + addJacko(131.81, 52.63); + addJacko(116.72, 46.21); + addJacko(137.22, 55.25); + addJacko(131.91, 49.21); + addJacko(124.63, 47.08); + addJacko(122.69, 51.58); + + addJacko(125.97, 145.21); + addJacko(115.91, 139.92); + addJacko(119.59, 145.54); + + addJacko(53.59, 133.58); + addJacko(48.78, 136.08); + addJacko(48.22, 142.75); + addJacko(55.22, 143.50); + addJacko(58.34, 135.21); + addJacko(58.94, 140.63); + + addJacko(76.63, 125.00); + addJacko(76.59, 128.88); + addJacko(77.06, 120.88); + addJacko(86.91, 121.08); + addJacko(86.84, 129.33); + addJacko(76.91, 134.63); + addJacko(86.47, 135.50); + addJacko(137.40, 97.60); + + addJacko(44.53, 152.91); + addJacko(49.56, 65.16); + addJacko(81.50, 92.41); + addJacko(81.18, 116.25); + addJacko(116.71, 46.20); + addJacko(118.81, 125.33); + addJacko(148.50, 81.70); + addJacko(150.09, 82.54); + addJacko(151.46, 99.08); + + addJacko(45.09, 105.75); + + add(entities.jackoLanternOff(66.60, 72.10)); + add(entities.jackoLanternOff(67.20, 71.90)); + + addJackoLanternSpot(101.00, 100.00); + addJackoLanternSpot(147.00, 135.00); + addJackoLanternSpot(190.00, 176.00); + addJackoLanternSpot(107.00, 173.00); + addJackoLanternSpot(165.00, 95.00); + } + + if (isChristmas) { + addEntities(createToyStash(103.87, 86.12)); + + const donateX = 64, donateY = 79; + add(createSign(donateX, donateY, 'Donate gifts', donateGift, entities.signDonate)); + add(entities.boxGifts(donateX + 0.1, donateY + 1.2)).interact = donateGift; + + const xmasTreeY = 49.5, xmasTreeX = 40.25; + addXmasTree(33 + xmasTreeX, 24 + xmasTreeY); + add(entities.giftPilePine(33 + xmasTreeX, 24.5 + xmasTreeY)); + add(entities.giftPile6(31.13 + xmasTreeX, 25.13 + xmasTreeY)); + add(entities.giftPile1(34.75 + xmasTreeX, 25.71 + xmasTreeY)); + add(createCookieTable(32.7 + xmasTreeX, 26.5 + xmasTreeY)); + + add(entities.mistletoe(86.40, 83.00)); + add(entities.mistletoe(92.00, 93.00)); + add(entities.giftPile4(43.50, 53.50)); + add(entities.giftPileTree(81.10, 92.50)); + add(entities.giftPile1(76.00, 90.00)); + add(entities.giftPile3(79.00, 53.00)); + add(entities.giftPile6(85.00, 54.50)); + + add(entities.giftPileTree(97.28, 64.38)); + add(entities.giftPilePine(63.03, 98.38)); + add(entities.giftPile5(65.25, 98.83)); + add(entities.giftPile6(61.03, 99.29)); + add(entities.giftPile1(65.09, 112.96)); + add(entities.giftPile3(63.06, 113.67)); + add(entities.giftPile4(64.59, 114.71)); + add(entities.giftPile2(57.50, 146.04)); + add(entities.giftPile6(58.25, 147.67)); + add(entities.giftPileTree(132.38, 149.08)); + add(entities.giftPile5(134.03, 149.33)); + add(entities.giftPile2(147.47, 126.58)); + add(entities.giftPileTree(137.84, 115.21)); + add(entities.giftPile5(134.06, 121.79)); + add(entities.giftPilePine(147.59, 101.86)); + add(entities.giftPile1(143.16, 98.99)); + add(entities.giftPile4(134.41, 67.58)); + add(entities.giftPilePine(154.81, 55.63)); + add(entities.giftPile1(144.13, 45.54)); + add(entities.giftPile3(132.66, 47.25)); + add(entities.giftPileTree(118.78, 48.25)); + + add(entities.giftPile1(51.31, 31.33)); + add(entities.giftPile2(58.66, 31.75)); + add(entities.giftPile4(72.28, 37.50)); + add(entities.giftPile6(91.06, 38.13)); + add(entities.giftPile3(100.00, 38.17)); + add(entities.giftPileTree(143.56, 14.21)); + add(entities.giftPile1(126.03, 12.08)); + add(entities.giftPileInteractive(148.91, 34.50)); + add(entities.giftPile6(96.97, 11.79)); + add(entities.giftPile2(77.22, 6.46)); + add(entities.giftPileTree(62.66, 6.13)); + add(entities.giftPile3(20.78, 6.71)); + add(entities.giftPile1(8.41, 15.75)); + add(entities.giftPile6(15.13, 32.08)); + add(entities.giftPileInteractive(14.22, 33.04)); + add(entities.giftPileTree(25.66, 59.29)); + add(entities.giftPile6(16.03, 77.17)); + add(entities.giftPile1(22.09, 105.88)); + add(entities.giftPileInteractive(9.34, 96.17)); + add(entities.giftPile3(24.25, 144.25)); + add(entities.giftPile6(14.88, 155.13)); + add(entities.giftPile1(33.19, 133.13)); + add(entities.giftPile6(41.56, 122.17)); + + add(entities.holly(121.00, 145.17)); + add(entities.holly(122.03, 145.17)); + add(entities.holly(123.00, 145.13)); + add(entities.holly(125.00, 145.13)); + add(entities.holly(118.00, 140.17)); + add(entities.holly(121.03, 140.13)); + add(entities.holly(119.03, 140.21)); + add(entities.holly(135.00, 144.17)); + add(entities.holly(137.03, 144.17)); + add(entities.holly(138.03, 144.17)); + add(entities.holly(137.00, 140.13)); + add(entities.holly(139.00, 140.13)); + add(entities.holly(140.00, 140.04)); + add(entities.holly(142.00, 140.13)); + add(entities.holly(143.00, 144.58)); + add(entities.holly(144.97, 144.63)); + add(entities.holly(121.00, 100.13)); + add(entities.holly(124.00, 100.13)); + add(entities.holly(119.03, 100.13)); + add(entities.holly(117.97, 100.13)); + add(entities.holly(118.03, 105.21)); + add(entities.holly(120.00, 105.17)); + add(entities.holly(79.00, 147.21)); + add(entities.holly(82.03, 147.17)); + add(entities.holly(76.97, 147.21)); + add(entities.holly(84.00, 147.17)); + add(entities.holly(85.03, 147.17)); + add(entities.holly(86.00, 147.08)); + add(entities.holly(77.03, 136.17)); + add(entities.holly(80.03, 136.13)); + add(entities.holly(81.00, 136.17)); + add(entities.holly(83.00, 137.17)); + add(entities.holly(86.00, 137.17)); + add(entities.holly(87.00, 137.08)); + add(entities.holly(88.00, 137.17)); + add(entities.holly(73.03, 100.88)); + add(entities.holly(76.81, 100.88)); + add(entities.holly(81.03, 97.04)); + add(entities.holly(83.63, 100.67)); + add(entities.holly(87.44, 100.71)); + add(entities.holly(92.22, 96.08)); + add(entities.holly(96.72, 100.58)); + add(entities.holly(45.50, 53.13)); + add(entities.holly(49.31, 53.13)); + + add(entities.giftPile3(120.69, 145.46)); + add(entities.mistletoe(130.81, 149.46)); + add(entities.mistletoe(125.56, 139.38)); + add(entities.mistletoe(140.09, 131.50)); + add(entities.mistletoe(154.81, 107.88)); + add(entities.mistletoe(132.06, 123.25)); + add(entities.mistletoe(148.16, 114.54)); + add(entities.giftPile6(137.34, 131.17)); + add(entities.giftPile3(47.94, 142.42)); + add(entities.giftPile2(76.00, 147.75)); + + add(createCookieTable(29.43, 70.20)); + add(createCookieTable(104.75, 58.92)); + add(createCookieTable(79.31, 6.70)); + add(createCookieTable(110.06, 93.00)); + add(createCookieTable(146.47, 145.21)); + } + + if (isEaster) { + const giveBasket = give(entities.basket.type); + + // spot 1 + add(entities.basketBin(73.00, 74.00)).interact = giveBasket; + add(entities.eggBasket2(73.53, 74.88)); + add(entities.eggBasket3(74.41, 73.92)); + add(entities.eggBasket4(74.16, 74.17)); + add(createSign(74.00, 73.80, 'Egg baskets', giveBasket, entities.signQuest)); + + // spot 2 + add(entities.basketBin(33 + 70, 34 + 53)).interact = giveBasket; + add(entities.eggBasket2(33.53 + 70, 34.88 + 53)); + add(entities.eggBasket3(34.41 + 70, 33.92 + 53)); + add(entities.eggBasket4(34.16 + 70, 34.17 + 53)); + add(createSign(34 + 70, 33.8 + 53, 'Egg baskets', giveBasket, entities.signQuest)); + + // donation spot + add(createSign(62.00, 78.00, 'Donate eggs', donateEgg, entities.signDonate)); + add(entities.barrel(62.15, 78.87)).interact = donateEgg; + } } export function updateMainMapSeason(world: World, map: ServerMap, season: Season, holiday: Holiday) { - removeSeasonalObjects(world, map); - addSeasonalObjects(world, map, season, holiday); + removeSeasonalObjects(world, map); + addSeasonalObjects(world, map, season, holiday); - const isWinter = season === Season.Winter; + const isWinter = season === Season.Winter; - for (let y = 0, i = 0; y < map.height; y++) { - for (let x = 0; x < map.width; x++ , i++) { - const tile = mainMapTiles[i]; + for (let y = 0, i = 0; y < map.height; y++) { + for (let x = 0; x < map.width; x++ , i++) { + const tile = mainMapTiles[i]; - if (isWinter) { - if (x > 18 && (tile === TileType.Water || tile === TileType.WalkableWater || tile === TileType.Boat)) { - setTile(map, x, y, tile === TileType.Water ? TileType.Ice : TileType.WalkableIce); - } else { - setTile(map, x, y, tile); - } - } else { - if (tile === TileType.Ice || tile === TileType.SnowOnIce) { - setTile(map, x, y, TileType.Water); - } else if (tile === TileType.WalkableIce) { - setTile(map, x, y, TileType.WalkableWater); - } else { - setTile(map, x, y, tile); - } - } - } - } + if (isWinter) { + if (x > 18 && (tile === TileType.Water || tile === TileType.WalkableWater || tile === TileType.Boat)) { + setTile(map, x, y, tile === TileType.Water ? TileType.Ice : TileType.WalkableIce); + } else { + setTile(map, x, y, tile); + } + } else { + if (tile === TileType.Ice || tile === TileType.SnowOnIce) { + setTile(map, x, y, TileType.Water); + } else if (tile === TileType.WalkableIce) { + setTile(map, x, y, TileType.WalkableWater); + } else { + setTile(map, x, y, tile); + } + } + } + } - snapshotTiles(map); + snapshotTiles(map); - for (const controller of map.controllers) { - controller.initialize(world.now / 1000); - } + for (const controller of map.controllers) { + controller.initialize(world.now / 1000); + } } export function createMainMap(world: World): ServerMap { - const mapSize = 20; - const map = createServerMap('', MapType.None, mapSize, mapSize, TileType.Grass); + const mapSize = 20; + const map = createServerMap('', MapType.None, mapSize, mapSize, TileType.Grass); - map.flags |= MapFlags.EdibleGrass; + map.flags |= MapFlags.EdibleGrass; - // spawns + // spawns - map.spawnArea = rect(51, 21, 8, 8); - - map.spawns.set('harbor', rect(5.2, 72.2, 3.4, 2.6)); - map.spawns.set('cave', rect(75.5, 27, 2, 2)); - - map.spawns.set('lake', rect(134, 68, 2, 1)); - map.spawns.set('bridge', rect(107, 37, 2, 2)); - map.spawns.set('forest', rect(105, 91, 3, 3)); - map.spawns.set('graveyard', rect(146, 101, 3, 3)); - map.spawns.set('pumpkins', rect(71, 125, 3, 3)); - - map.spawns.set('center', rect(74, 74, 2, 2)); - map.spawns.set('topleft', rect(17, 10, 3, 3)); - map.spawns.set('topright', rect(131, 17, 3, 3)); - map.spawns.set('bottomleft', rect(17, 149, 3, 3)); - map.spawns.set('bottomright', rect(154, 140, 3, 3)); - - // tiles - - deserializeMap(map, mainMapData); - - if (!DEVELOPMENT) { - snapshotTiles(map); - } - - if (DEVELOPMENT) { - addSpawnPointIndicators(world, map); - } - - const giveLantern = give(entities.lanternOn.type); - - const isWinter = world.season === Season.Winter; - const isHalloween = world.holiday === Holiday.Halloween; - - const addWoodenFence = createWoodenFenceMaker(world, map); - const addStoneWall = createStoneWallFenceMaker(world, map); - - function add(entity: ServerEntity) { - if (entity.x < 0 || entity.x > map.width || entity.y < 0 || entity.y > map.height) { - if (DEVELOPMENT) { - logger.warn(`skipped entity (${getEntityTypeName(entity.type)}) outside map (${entity.x} ${entity.y})`); - } - - return { x: entity.x, y: entity.y } as ServerEntity; - } - - return world.addEntity(entity, map); - } - - function addEntities(entities: ServerEntity[]) { - return entities.map(add); - } - - function addTree(x: number, y: number, variant: number, web = false, spider = false) { - addEntities(entities.tree(x, y, variant, web, spider && !isWinter)); - } - - function cliffNE(x: number, y: number) { - add(entities.cliffTopNE(x + 0.5, y)); - lockTiles(map, x - 1, y - 1, 3, 3); - } - - const cliffDecals = [entities.cliffDecal1, entities.cliffDecal3, entities.cliffDecal2]; - - function cracksS(x: number, y: number) { - const code = (Math.random() * 1000) % 64; - const index1 = code & 0b11; - const index2 = (code >> 2) & 0b11; - const index3 = (code >> 4) & 0b11; - index1 && index1 !== 3 && add(cliffDecals[index1 - 1](x + 0.5, y - 1)); // no decal 2 here - index2 && add(cliffDecals[index2 - 1](x + 0.5, y)); - index3 && add(cliffDecals[index3 - 1](x + 0.5, y + 1)); - } - - function cracksSLeft(x: number, y: number) { - const code = (Math.random() * 1000) % 4; - (code & 0b01) && add(entities.cliffDecalL(x + 0.5, y - 1)); - (code & 0b10) && add(entities.cliffDecalL(x + 0.5, y)); - } - - function cracksSRight(x: number, y: number) { - const code = (Math.random() * 1000) % 4; - (code & 0b01) && add(entities.cliffDecalR(x + 0.5, y - 1)); - (code & 0b10) && add(entities.cliffDecalR(x + 0.5, y)); - } - - function cliffSW(x: number, y: number) { - add(entities.cliffSW(x + 0.5, y - 2)); - lockTiles(map, x - 1, y - 4, 3, 7); - cracksSLeft(x, y); - } - - function cliffSE(x: number, y: number) { - add(entities.cliffSE(x + 0.5, y - 2)); - lockTiles(map, x - 1, y - 3, 3, 6); - cracksSRight(x, y); - } - - function cliffS(x: number, y: number) { - add(entities.cliffS2(x + 0.5, y - 1)); - lockTiles(map, x, y - 2, 1, 5); - cracksS(x, y); - } - - function cliffSStart(x: number, y: number) { - add(entities.cliffS1(x + 0.5, y - 1)); - lockTiles(map, x, y - 2, 1, 5); - cracksS(x, y); - } - - function cliffSEnd(x: number, y: number) { - add(entities.cliffS3(x + 0.5, y - 1)); - lockTiles(map, x, y - 2, 1, 5); - cracksS(x, y); - } - - function cliffS1(x: number, y: number) { - add(entities.cliffSb(x + 0.5, y - 1)); - lockTiles(map, x, y - 2, 1, 5); - cracksS(x, y); - } - - function cliffS1Entrance(x: number, y: number) { - add(entities.cliffSbEntrance(x + 0.5, y - 1)); - lockTiles(map, x, y - 2, 1, 5); - } - - function cliffRightWithTrimNoEdge(x: number, y: number, h: number) { - cliffRight(x, y, h); - cliffTrimRight(x + 1, y, h, false); - } - - function cliffRightWithTrim(x: number, y: number, h: number) { - cliffRight(x, y - 3, h - 3); - cliffTrimRight(x + 1, y, h); - } - - function cliffLeftWithTrim(x: number, y: number, h: number) { - cliffLeft(x, y - 3, h - 3); - cliffTrimLeft(x, y, h); - } - - function cliffLeft(x: number, y: number, h: number) { - for (let i = 0; i < h; i++) { - add(entities.cliffTopW(x + 0.5, y - i)); - lockTiles(map, x, y - i - 1, 2, 3); - } - } - - function cliffRight(x: number, y: number, h: number) { - for (let i = 0; i < h; i++) { - add(entities.cliffTopE(x + 0.5, y - i)); - lockTiles(map, x - 1, y - i - 1, 2, 3); - } - } - - function cliffTrimLeft(x: number, y: number, h: number) { - add(entities.cliffBotTrimLeft(x - 0.5, y)); - - for (let i = 0; i < (h - 2); i++) { - add(entities.cliffMidTrimLeft(x - 0.5, y - 1 - i)); - } - - add(entities.cliffTopTrimLeft(x - 0.5, y - h + 1)); - } - - function cliffTrimRight(x: number, y: number, h: number, botTrim = true) { - if (botTrim) { - add(entities.cliffBotTrimRight(x + 0.5, y)); - } else { - add(entities.cliffMidTrimRight(x + 0.5, y)); - } - - for (let i = 0; i < (h - 2); i++) { - add(entities.cliffMidTrimRight(x + 0.5, y - 1 - i)); - } - - if (h > 1) { - add(entities.cliffTopTrimRight(x + 0.5, y - h + 1)); - } - } - - function cliffSSection(x: number, y: number, w: number) { - cliffSStart(x, y); - - for (let i = 1; i < (w - 1); i++) { - cliffS(x + i, y); - } - - cliffSEnd(x + w - 1, y); - } - - function cliffSESection(x: number, y: number, w: number) { - for (let i = 0; i < w; i++) { - cliffSE(x + i, y - i); - } - } - - function cliffSWSection(x: number, y: number, w: number) { - for (let i = 0; i < w; i++) { - cliffSW(x + i, y + i); - } - } - - // actual cliffs - cliffS(0, 49); - cliffS(1, 49); - cliffSEnd(2, 49); - cliffSE(3, 49); - cliffSSection(4, 48, 2); - cliffSWSection(6, 49, 2); - cliffSSection(8, 50, 4); - cliffSW(12, 51); - cliffSSection(13, 51, 4); - cliffSE(17, 51); - cliffRightWithTrim(17, 51, 6); - // harbor ramp - cliffLeftWithTrim(23, 50, 6); - cliffSW(23, 50); - cliffSSection(24, 50, 5); - cliffSE(29, 50); - cliffRightWithTrim(29, 50, 7); - cliffSESection(30, 45, 2); - cliffRightWithTrim(31, 44, 5); - cliffSESection(32, 41, 1); - cliffRightWithTrim(32, 41, 4); - cliffSESection(33, 39, 2); - cliffRightWithTrim(34, 38, 5); - cliffNE(34, 33); - // no path spot - cliffRightWithTrimNoEdge(33, 32, 6); - cliffSE(34, 28); - cliffRightWithTrim(34, 28, 5); - cliffSESection(35, 25, 2); - cliffS1(37, 23); - cliffSE(38, 23); - cliffRightWithTrim(38, 23, 4); - cliffSESection(39, 21, 2); - cliffRightWithTrim(40, 20, 4); - cliffSESection(41, 18, 1); - cliffS1(42, 17); - cliffSE(43, 17); - // corner - cliffSSection(44, 16, 2); - cliffSE(46, 16); - cliffS1(47, 15); - cliffSW(48, 16); - cliffSSection(49, 16, 2); - cliffSESection(51, 16, 2); - // end corner - cliffSSection(53, 14, 3); - cliffSW(56, 15); - cliffSSection(57, 15, 2); - cliffSE(59, 15); - cliffSSection(60, 14, 3); - cliffSWSection(63, 15, 2); - cliffS1(65, 16); - cliffSW(66, 17); - cliffLeftWithTrim(67, 19, 4); - cliffSW(67, 19); - cliffLeftWithTrim(68, 23, 6); - cliffSW(68, 23); - cliffLeftWithTrim(69, 25, 4); - cliffSW(69, 25); - cliffSSection(70, 25, 4); - cliffSE(74, 25); - - // mine entrance - cliffS1Entrance(75, 24); - cliffS1Entrance(76, 24); - cliffS1Entrance(77, 24); - - cliffSW(78, 25); - cliffS1(79, 25); - cliffSW(80, 26); - cliffSSection(81, 26, 3); - cliffSE(84, 26); - cliffSSection(85, 25, 2); - cliffSE(87, 25); - cliffRightWithTrim(87, 25, 5); - cliffNE(87, 20); - cliffNE(86, 19); - cliffRightWithTrimNoEdge(85, 18, 1); - cliffNE(85, 17); - cliffRightWithTrimNoEdge(84, 16, 3); - cliffSESection(85, 15, 2); - cliffS1(87, 13); - cliffSW(88, 14); - cliffSSection(89, 14, 2); - cliffSE(91, 14); - cliffRightWithTrim(91, 14, 6); - // river ramp - cliffLeftWithTrim(96, 14, 6); - cliffSW(96, 14); - cliffSSection(97, 14, 3); - cliffSE(100, 14); - cliffSSection(101, 13, 3); - cliffSE(104, 13); - cliffRightWithTrim(104, 13, 5); - cliffSESection(105, 10, 1); - cliffRightWithTrim(105, 10, 5); - cliffSESection(106, 7, 1); - cliffS1(107, 6); - cliffSE(108, 6); - cliffRightWithTrim(108, 6, 4); - cliffSESection(109, 4, 2); - cliffRightWithTrim(110, 3, 4); - - const plankWidth = 78 / tileWidth; - const plankHeight = 12 / tileHeight; - - const plank = () => sample(entities.planks)!; - const shortPlank = () => sample(entities.planksShort)!; - - // barrel storage - addWoodenFence(100, 24, 6); - addWoodenFence(100, 24, 2, false, true, false); - addWoodenFence(100, 29, 3, false, false, true); - addWoodenFence(106, 24, 8, false, true, true); - addWoodenFence(100, 32, 6); - add(entities.barrel(104.56, 24.70)); - add(entities.barrel(105.34, 25.08)); - add(entities.barrel(104.90, 25.70)); - add(entities.barrel(100.84, 24.50)); - add(entities.barrel(101.75, 24.50)); - add(entities.barrel(100.72, 31.25)); - add(entities.barrel(101.44, 31.54)); - add(entities.barrel(101.75, 30.75)); - add(entities.barrel(105.25, 29.79)); - add(entities.barrel(105.50, 30.71)); - add(entities.barrel(105.25, 31.67)); - add(entities.barrel(103.91, 31.04)); - add(entities.barrel(106.94, 24.08)); - add(entities.barrel(107.75, 24.88)); - add(entities.barrel(105.59, 32.83)); - add(entities.barrel(99.34, 24.13)); - add(entities.barrel(99.41, 25.21)); - add(entities.barrel(98.31, 24.88)); - add(entities.lanternOn(100.50, 25.63)); - add(entities.lanternOn(100.50, 28.96)); - add(entities.lanternOn(105.53, 25.88)); - - // orchard / mine entrance - const mineEntrance = add(entities.mineEntrance(76.5, 23.08)); - // const mineClosed = add(entities.mineClosed(76.5, 23.08)); - // setEntityName(mineClosed, 'Mine Closed'); - - if (true) { - add(entities.collider1x1(74.50, 25.17)); - add(entities.collider1x1(77.50, 25.21)); - mineEntrance.interact = (_, client) => goToMap(world, client, 'cave'); - add(entities.triggerHouseDoor(76.50, 25.88)).trigger = (_, client) => goToMap(world, client, 'cave'); - add(entities.mineRailsFadeUp(76.5, 25)); - add(entities.mineRailsV(76.5, 26)); - add(entities.mineRailsV(76.5, 27)); - add(entities.mineRailsNW(76.5, 28)); - add(entities.mineRailsH(75.5, 28)); - add(entities.mineRailsH(74.5, 28)); - add(entities.mineRailsH(73.5, 28)); - add(entities.mineRailsEndLeft(72.5, 28.5)); - add(entities.mineCart(74, 28)); - } - - add(entities.rock(79.09, 27.33)); - add(entities.rock2(79.47, 27.71)); - add(entities.rock3(73.53, 27.21)); - add(entities.rock2(73.88, 27.42)); - add(entities.lanternOn(74.59, 29.20)); - add(entities.lanternOn(78.88, 27.67)); - add(createBoxOfLanterns(79.84, 29.00)); - - // addEntities(entities.tree5(74.25, 30.00, 0)); - // addEntities(entities.tree5(78.53, 30.04, 0)); - // addEntities(entities.tree5(76.34, 33.00, 1)); - add(entities.tree5Stump(74.25, 30.00)); - add(entities.tree5Stump(78.53, 30.04)); - - addEntities(entities.tree5(78.59, 36.63, 2)); - addEntities(entities.tree4(80.97, 33.29, 1)); - addEntities(entities.tree5(83.50, 36.54, 2)); - addEntities(entities.tree5(83.16, 29.63, 0)); - addEntities(entities.tree5(86.00, 33.00, 1)); - addEntities(entities.tree5(87.75, 36.54, 2)); - addEntities(entities.tree5(87.66, 29.54, 0)); - addEntities(entities.tree5(73.91, 36.17, 1)); - addEntities(entities.tree5(72.00, 32.83, 2)); - addEntities(entities.tree5(69.59, 36.04, 0)); - addWoodenFence(62, 38, 9); - addWoodenFence(71, 38, 1, false, true, true); - addWoodenFence(71, 39, 3); - addWoodenFence(78, 39, 3); - addWoodenFence(81, 39, 1, false, true, true); - addWoodenFence(81, 40, 9); - addWoodenFence(69, 43, 8); - addWoodenFence(90, 28, 12, false, false, true); - add(entities.lanternOn(77.97, 36.42)); - add(entities.lanternOn(73.81, 39.21)); - add(entities.box(81.91, 39.13)); - add(entities.box(83.03, 39.50)); - - // bridge - add(plank()(110.7, 36.60)); - add(shortPlank()(112.56, 36.60)); - add(shortPlank()(110.18, 37.08)); - add(plank()(112.06, 37.08)); - add(plank()(110.71, 37.58)); - add(shortPlank()(112.59, 37.58)); - add(shortPlank()(110.21, 38.08)); - add(plank()(112.09, 38.08)); - add(plank()(110.63, 38.58)); - add(shortPlank()(112.50, 38.58)); - add(entities.plankShadow(111.46, 38.50)); - add(entities.pierLeg(111.46, 38.50)); - - add(entities.collider3x1(110, 36)); - add(entities.collider3x1(110, 39)); - - // pier - const pierX = 0; - const pierY = 8 / tileHeight; - - add(createSignWithText(pierX + 8.5, pierY + 71.1, 'Party Island', - `Hop on the boat to travel to an island, that is unique to your party`)); - - add(entities.triggerBoat(7.5, 70)).trigger = (_, client) => goToMap(world, client, 'island'); - - addEntities(entities.fullBoat(7, 69.66)); - add(entities.pierLeg(pierX + 10, pierY + 72.6)); - add(entities.pierLeg(pierX + 11.9, pierY + 72.6)); - add(entities.pierLeg(pierX + 5, pierY + 74.6)); - add(entities.pierLeg(pierX + 6.9, pierY + 74.6)); - add(entities.pierLeg(pierX + 8.8, pierY + 74.6)); - const plankOffsets = [0, -1, 0, -2, -1, -1, 0, -2, -1].map(x => x / tileWidth); - - for (let y = 0; y < 9; y++) { - const maxX = y < 5 ? ((y % 2) ? 5 : 4) : ((y % 2) ? 3 : 2); - const baseX = pierX + 4.5 + ((y % 2) ? 0 : (plankWidth / 2)) + plankOffsets[y]; - const baseY = pierY + 71 - (9 / tileHeight); - - for (let x = 0; x < maxX; x++) { - if ((x === 0 && (y % 2)) || (x === (maxX - 1) && (y % 2))) { - const ox = x === 0 ? (18 / tileWidth) : (-18 / tileWidth); - add(shortPlank()(baseX + ox + x * plankWidth, baseY + y * plankHeight)); - } else { - add(plank()(baseX + x * plankWidth, baseY + y * plankHeight)); - } - } - } - - add(entities.collider1x3(3.5, 69)); - add(entities.collider1x3(3.5, 72)); - add(entities.collider1x1(3.5, 75)); - - add(entities.collider3x1(4, 69)); - add(entities.collider3x1(7, 69)); - add(entities.collider1x1(9, 70)); - add(entities.collider1x2(9.6, 69)); - add(entities.collider3x1(10, 70.3)); - add(entities.collider2x1(4, 70)); - add(entities.collider1x3(4, 71)); - add(entities.collider1x2(4, 73)); - add(entities.collider1x2(4, 73)); - add(entities.collider3x1(4, 75)); - add(entities.collider3x1(7, 75)); - add(entities.collider1x2(9, 73)); - add(entities.collider3x1(10, 73)); - - add(entities.plankShadowShort(pierX + 5.09, pierY + 74.08 - plankHeight * 6)); - add(entities.plankShadowShort(pierX + 5.06, pierY + 74.08 - plankHeight * 4)); - add(entities.plankShadowShort(pierX + 5.09, pierY + 74.08 - plankHeight * 2)); - add(entities.plankShadowShort(pierX + 5.06, pierY + 74.08)); - - add(entities.plankShadowShort(pierX + 8.84, pierY + 74.12 - plankHeight * 2)); - add(entities.plankShadowShort(pierX + 8.78, pierY + 74.12 - plankHeight)); - add(entities.plankShadowShort(pierX + 8.81, pierY + 74.12)); - - add(entities.plankShadow(pierX + 11.68, pierY + 72.62)); - add(entities.plankShadow2(pierX + 11.68 - plankWidth, pierY + 72.62)); - add(entities.plankShadowShort(pierX + 13.5, pierY + 72.45)); - - add(entities.plankShadow2(pierX + 5.75, pierY + 74.62)); - add(entities.plankShadow(pierX + 5.75 + plankWidth, pierY + 74.62)); - - add(entities.lanternOn(pierX + 13.25, pierY + 70.83)); - add(entities.lanternOn(pierX + 13.31, pierY + 73.04)); - add(entities.lanternOn(pierX + 9.31, pierY + 73.10)); - add(entities.lanternOn(pierX + 9.63, pierY + 70.88)); - add(entities.lanternOn(pierX + 4.72, pierY + 70.96)); - add(entities.lanternOn(pierX + 4.69, pierY + 74.96)); - - add(entities.barrel(pierX + 13.75, pierY + 73.71)); - add(entities.barrel(pierX + 13.56, pierY + 74.46)); - add(entities.barrel(pierX + 14.34, pierY + 74.17)); - add(entities.barrel(pierX + 16.56, pierY + 76.62)); - add(entities.barrel(pierX + 5.41, pierY + 71.21)); - add(entities.barrel(pierX + 13.96, pierY + 67.67)); - add(entities.barrel(pierX + 14.75, pierY + 67.96)); - - // plants - add(entities.largeLeafedBush1(13.59, 64.79)); - add(entities.largeLeafedBush2(22.09, 68.75)); - add(entities.largeLeafedBush1(31.72, 65.50)); - add(entities.largeLeafedBush2(30.25, 53.67)); - add(entities.largeLeafedBush1(31.46, 52.66)); - add(entities.largeLeafedBush2(42.50, 47.71)); - add(entities.largeLeafedBush1(43.44, 52.50)); - add(entities.largeLeafedBush2(37.72, 25.54)); - add(entities.largeLeafedBush1(38.53, 25.17)); - add(entities.largeLeafedBush2(43.94, 25.04)); - add(entities.largeLeafedBush1(53.13, 16.50)); - add(entities.largeLeafedBush2(53.03, 17.42)); - add(entities.largeLeafedBush1(63.50, 31.46)); - add(entities.largeLeafedBush2(62.19, 32.33)); - add(entities.largeLeafedBush1(68.53, 28.79)); - add(entities.largeLeafedBush2(67.38, 21.04)); - add(entities.largeLeafedBush1(68.97, 36.84)); - add(entities.largeLeafedBush2(70.69, 45.71)); - add(entities.largeLeafedBush1(76.91, 43.58)); - add(entities.largeLeafedBush2(77.75, 44.17)); - add(entities.largeLeafedBush1(63.59, 46.58)); - add(entities.largeLeafedBush2(76.71, 60.76)); - add(entities.largeLeafedBush1(77.59, 60.00)); - add(entities.largeLeafedBush2(80.88, 53.21)); - add(entities.largeLeafedBush1(89.38, 57.71)); - add(entities.largeLeafedBush2(104.41, 45.79)); - add(entities.largeLeafedBush1(104.88, 46.58)); - add(entities.largeLeafedBush2(105.81, 44.88)); - add(entities.largeLeafedBush1(104.47, 32.75)); - add(entities.largeLeafedBush2(103.72, 33.33)); - add(entities.largeLeafedBush1(88.03, 15.58)); - add(entities.largeLeafedBush2(88.84, 16.17)); - add(entities.largeLeafedBush1(88.09, 16.63)); - add(entities.largeLeafedBush2(105.91, 16.46)); - add(entities.largeLeafedBush1(106.65, 9.20)); - add(entities.largeLeafedBush2(103.69, 6.46)); - add(entities.largeLeafedBush1(100.94, 5.38)); - add(entities.largeLeafedBush2(73.22, 6.71)); - add(entities.largeLeafedBush1(57.66, 6.08)); - add(entities.largeLeafedBush2(56.97, 6.67)); - add(entities.largeLeafedBush1(32.53, 24.25)); - add(entities.largeLeafedBush2(33.47, 24.54)); - add(entities.largeLeafedBush1(25.19, 19.13)); - add(entities.largeLeafedBush2(13.94, 49.08)); - add(entities.largeLeafedBush1(13.50, 48.21)); - add(entities.largeLeafedBush2(11.09, 11.71)); - add(entities.largeLeafedBush1(11.81, 12.63)); - add(entities.largeLeafedBush2(23.72, 7.42)); - add(entities.largeLeafedBush1(23.22, 8.25)); - add(entities.largeLeafedBush2(25.84, 38.33)); - add(entities.largeLeafedBush1(9.03, 61.50)); - add(entities.largeLeafedBush2(125.03, 37.50)); - add(entities.largeLeafedBush1(119.56, 47.92)); - add(entities.largeLeafedBush2(124.69, 26.54)); - add(entities.largeLeafedBush1(125.56, 26.00)); - add(entities.largeLeafedBush2(139.97, 9.25)); - add(entities.largeLeafedBush1(150.41, 11.83)); - add(entities.largeLeafedBush2(154.31, 25.71)); - add(entities.largeLeafedBush1(153.46, 26.20)); - add(entities.largeLeafedBush2(139.00, 25.79)); - add(entities.largeLeafedBush1(121.69, 7.92)); - add(entities.largeLeafedBush2(157.03, 66.88)); - add(entities.largeLeafedBush1(152.81, 89.54)); - add(entities.largeLeafedBush2(151.81, 81.04)); - add(entities.largeLeafedBush1(152.06, 82.00)); - add(entities.largeLeafedBush2(146.66, 94.96)); - add(entities.largeLeafedBush1(156.56, 106.71)); - add(entities.largeLeafedBush2(156.63, 107.92)); - add(entities.largeLeafedBush1(152.81, 129.96)); - add(entities.largeLeafedBush2(143.50, 135.79)); - add(entities.largeLeafedBush1(143.19, 136.75)); - add(entities.largeLeafedBush2(131.53, 121.92)); - add(entities.largeLeafedBush1(132.44, 133.67)); - add(entities.largeLeafedBush2(132.84, 134.83)); - add(entities.largeLeafedBush1(139.00, 131.58)); - add(entities.largeLeafedBush2(125.00, 137.83)); - add(entities.largeLeafedBush1(123.41, 138.33)); - add(entities.largeLeafedBush2(119.81, 136.96)); - add(entities.largeLeafedBush1(122.38, 120.63)); - add(entities.largeLeafedBush2(111.19, 126.54)); - add(entities.largeLeafedBush1(113.66, 130.13)); - add(entities.largeLeafedBush2(112.50, 118.17)); - add(entities.largeLeafedBush1(107.94, 113.83)); - add(entities.largeLeafedBush2(108.06, 114.79)); - add(entities.largeLeafedBush1(105.28, 105.88)); - add(entities.largeLeafedBush2(100.16, 111.25)); - add(entities.largeLeafedBush1(101.06, 111.88)); - add(entities.largeLeafedBush2(121.81, 107.25)); - add(entities.largeLeafedBush1(122.84, 107.63)); - add(entities.largeLeafedBush2(88.69, 115.50)); - add(entities.largeLeafedBush1(89.06, 116.42)); - add(entities.largeLeafedBush2(81.00, 112.33)); - add(entities.largeLeafedBush1(81.69, 113.21)); - add(entities.largeLeafedBush2(91.53, 151.42)); - add(entities.largeLeafedBush1(90.81, 150.75)); - add(entities.largeLeafedBush2(99.44, 153.25)); - add(entities.largeLeafedBush1(99.03, 153.96)); - add(entities.largeLeafedBush2(108.50, 147.17)); - add(entities.largeLeafedBush1(117.75, 153.38)); - add(entities.largeLeafedBush2(118.44, 152.58)); - add(entities.largeLeafedBush1(132.91, 148.54)); - add(entities.largeLeafedBush2(125.53, 144.92)); - add(entities.largeLeafedBush1(124.72, 145.50)); - add(entities.largeLeafedBush2(145.03, 144.92)); - add(entities.largeLeafedBush1(118.06, 124.92)); - add(entities.largeLeafedBush2(116.03, 94.42)); - add(entities.largeLeafedBush1(103.25, 81.96)); - add(entities.largeLeafedBush2(103.78, 82.88)); - add(entities.largeLeafedBush1(87.47, 89.54)); - add(entities.largeLeafedBush2(87.97, 90.50)); - add(entities.largeLeafedBush1(79.59, 84.50)); - add(entities.largeLeafedBush2(77.88, 85.25)); - add(entities.largeLeafedBush1(80.34, 91.42)); - add(entities.largeLeafedBush2(90.81, 78.63)); - add(entities.largeLeafedBush1(98.00, 63.71)); - add(entities.largeLeafedBush2(42.41, 84.79)); - add(entities.largeLeafedBush1(39.19, 82.92)); - add(entities.largeLeafedBush2(30.78, 77.13)); - add(entities.largeLeafedBush1(31.16, 78.08)); - add(entities.largeLeafedBush2(34.47, 70.63)); - add(entities.largeLeafedBush1(14.09, 90.08)); - add(entities.largeLeafedBush2(14.97, 89.17)); - add(entities.largeLeafedBush1(32.41, 111.13)); - add(entities.largeLeafedBush2(31.59, 111.58)); - add(entities.largeLeafedBush1(52.81, 110.38)); - add(entities.largeLeafedBush2(53.44, 110.04)); - add(entities.largeLeafedBush1(31.59, 109.83)); - add(entities.largeLeafedBush2(51.13, 95.04)); - add(entities.largeLeafedBush1(50.34, 95.54)); - add(entities.largeLeafedBush2(52.25, 96.21)); - add(entities.largeLeafedBush1(67.65, 81.45)); - - add(entities.largeLeafedBush3(63.50, 32.21)); - add(entities.largeLeafedBush4(69.28, 35.67)); - add(entities.largeLeafedBush3(66.56, 20.08)); - add(entities.largeLeafedBush4(64.25, 23.33)); - add(entities.largeLeafedBush3(67.81, 29.42)); - add(entities.largeLeafedBush4(72.44, 27.38)); - add(entities.largeLeafedBush3(52.22, 17.21)); - add(entities.largeLeafedBush4(43.59, 24.08)); - add(entities.largeLeafedBush3(38.47, 25.88)); - add(entities.largeLeafedBush4(46.38, 29.21)); - add(entities.largeLeafedBush3(41.69, 47.96)); - add(entities.largeLeafedBush4(44.16, 53.21)); - add(entities.largeLeafedBush3(31.16, 53.67)); - add(entities.largeLeafedBush4(31.00, 65.92)); - add(entities.largeLeafedBush3(20.72, 69.42)); - add(entities.largeLeafedBush4(12.88, 65.04)); - add(entities.largeLeafedBush3(9.47, 62.29)); - add(entities.largeLeafedBush4(13.03, 49.04)); - add(entities.largeLeafedBush3(25.13, 38.79)); - add(entities.largeLeafedBush4(15.38, 34.33)); - add(entities.largeLeafedBush3(33.34, 25.17)); - add(entities.largeLeafedBush4(25.31, 19.92)); - add(entities.largeLeafedBush3(24.06, 8.58)); - add(entities.largeLeafedBush4(10.91, 12.58)); - add(entities.largeLeafedBush3(9.72, 7.38)); - add(entities.largeLeafedBush4(40.19, 16.13)); - add(entities.largeLeafedBush3(57.59, 7.33)); - add(entities.largeLeafedBush4(73.03, 7.54)); - add(entities.largeLeafedBush3(71.41, 22.75)); - add(entities.largeLeafedBush4(87.32, 16.17)); - add(entities.largeLeafedBush3(85.25, 27.29)); - add(entities.largeLeafedBush4(100.81, 6.17)); - add(entities.largeLeafedBush3(106.44, 10.25)); - add(entities.largeLeafedBush4(105.72, 17.71)); - add(entities.largeLeafedBush3(105.28, 7.13)); - add(entities.largeLeafedBush4(104.56, 33.75)); - add(entities.largeLeafedBush3(105.63, 45.58)); - add(entities.largeLeafedBush4(117.41, 49.58)); - add(entities.largeLeafedBush3(73.91, 66.96)); - add(entities.largeLeafedBush4(76.68, 59.87)); - add(entities.largeLeafedBush3(84.06, 64.33)); - add(entities.largeLeafedBush4(81.50, 53.67)); - add(entities.largeLeafedBush3(76.72, 44.54)); - add(entities.largeLeafedBush4(62.81, 46.92)); - add(entities.largeLeafedBush3(70.41, 46.63)); - add(entities.largeLeafedBush4(34.47, 69.75)); - add(entities.largeLeafedBush3(30.34, 78.54)); - add(entities.largeLeafedBush4(39.84, 83.38)); - add(entities.largeLeafedBush3(42.06, 85.42)); - add(entities.largeLeafedBush4(50.94, 96.17)); - add(entities.largeLeafedBush3(30.97, 110.46)); - add(entities.largeLeafedBush4(13.75, 95.92)); - add(entities.largeLeafedBush3(14.41, 95.67)); - add(entities.largeLeafedBush4(14.88, 90.13)); - add(entities.largeLeafedBush3(28.66, 119.33)); - add(entities.largeLeafedBush4(31.53, 133.17)); - add(entities.largeLeafedBush3(53.47, 110.92)); - add(entities.largeLeafedBush4(90.69, 151.54)); - add(entities.largeLeafedBush3(99.72, 154.13)); - add(entities.largeLeafedBush4(95.78, 146.63)); - add(entities.largeLeafedBush3(108.44, 148.00)); - add(entities.largeLeafedBush4(93.25, 131.96)); - add(entities.largeLeafedBush3(125.56, 145.83)); - add(entities.largeLeafedBush4(123.97, 139.04)); - add(entities.largeLeafedBush3(120.59, 137.38)); - add(entities.largeLeafedBush4(118.44, 153.63)); - add(entities.largeLeafedBush3(111.16, 152.63)); - add(entities.largeLeafedBush4(133.06, 149.46)); - add(entities.largeLeafedBush3(144.16, 144.96)); - add(entities.largeLeafedBush4(143.72, 137.46)); - add(entities.largeLeafedBush3(132.09, 135.33)); - add(entities.largeLeafedBush4(138.16, 131.88)); - add(entities.largeLeafedBush3(152.63, 131.21)); - add(entities.largeLeafedBush4(145.72, 120.67)); - add(entities.largeLeafedBush3(131.56, 122.79)); - add(entities.largeLeafedBush4(122.16, 121.50)); - add(entities.largeLeafedBush3(122.22, 107.92)); - add(entities.largeLeafedBush4(117.28, 125.38)); - add(entities.largeLeafedBush3(111.34, 127.42)); - add(entities.largeLeafedBush4(112.44, 119.13)); - add(entities.largeLeafedBush3(107.25, 115.04)); - add(entities.largeLeafedBush4(114.28, 130.79)); - add(entities.largeLeafedBush3(105.34, 106.79)); - add(entities.largeLeafedBush4(100.31, 112.17)); - add(entities.largeLeafedBush3(87.47, 91.08)); - add(entities.largeLeafedBush4(79.84, 85.33)); - add(entities.largeLeafedBush3(103.19, 83.25)); - add(entities.largeLeafedBush4(98.38, 85.96)); - add(entities.largeLeafedBush3(114.09, 92.21)); - add(entities.largeLeafedBush4(115.81, 95.21)); - add(entities.largeLeafedBush3(117.38, 70.04)); - add(entities.largeLeafedBush4(147.13, 95.75)); - add(entities.largeLeafedBush3(142.53, 98.54)); - add(entities.largeLeafedBush4(157.44, 107.33)); - add(entities.largeLeafedBush3(152.19, 90.08)); - add(entities.largeLeafedBush4(158.34, 113.71)); - add(entities.largeLeafedBush3(151.38, 82.42)); - add(entities.largeLeafedBush4(157.28, 67.79)); - add(entities.largeLeafedBush3(157.97, 57.04)); - add(entities.largeLeafedBush4(154.00, 26.54)); - add(entities.largeLeafedBush3(138.13, 26.17)); - add(entities.largeLeafedBush4(125.06, 27.17)); - add(entities.largeLeafedBush3(125.66, 38.25)); - add(entities.largeLeafedBush4(119.09, 48.54)); - add(entities.largeLeafedBush3(116.81, 35.83)); - add(entities.largeLeafedBush4(141.38, 10.13)); - add(entities.largeLeafedBush3(149.03, 11.33)); - add(entities.largeLeafedBush4(152.53, 5.75)); - add(entities.largeLeafedBush3(121.41, 8.63)); - add(entities.largeLeafedBush4(109.75, 26.25)); - add(entities.largeLeafedBush3(109.38, 35.46)); - - // north-west hill - addEntities(entities.pine(37.91, 88.33, 0)); - addEntities(entities.pine(39.91, 91.29, 0)); - addEntities(entities.pine(15.53, 83.54, 0)); - addEntities(entities.pine(17.91, 82.17, 0)); - addEntities(entities.pine(21.59, 86.71, 0)); - addEntities(entities.pine(27.56, 81.38, 0)); - addEntities(entities.pine(2.88, 36.17, 0)); - addEntities(entities.pine(4.34, 40.79, 0)); - addEntities(entities.pine(2.59, 56.46, 0)); - addEntities(entities.pine(0.72, 53.33, 0)); - addEntities(entities.pine(8.72, 26.92, 0)); - addEntities(entities.pine(10.19, 29.46, 0)); - addEntities(entities.pine(3.38, 20.33, 0)); - add(entities.lanternOn(7.06, 17.92)); - add(entities.lanternOn(7.78, 22.96)); - add(entities.lanternOn(11.72, 19.04)); - add(entities.boxLanterns(5.66, 21.88)).interact = giveLantern; - addStoneWall(6, 32, 3); - addStoneWall(6, 32, 4, false, true, true); - addStoneWall(12, 32, 1, false, true); - addStoneWall(12, 38, 1, false, false, true); - addStoneWall(6, 40, 3); - add(entities.barrel(11.34, 33.08)); - add(entities.barrel(10.31, 33.46)); - add(entities.barrel(11.22, 33.96)); - add(entities.barrel(6.63, 39.08)); - add(entities.barrel(7.75, 39.21)); - add(entities.lanternOnWall(7.97, 32.29)); - add(entities.lanternOnWall(10.00, 40.33)); - add(entities.lanternOnWall(6.00, 36.33)); - add(entities.lanternOnWall(12.00, 34.33)); - - // north hill - addWoodenFence(52, 12.5, 11); - addWoodenFence(104.25, 9.25, 2, false, false, true); - addWoodenFence(101.25, 11.25, 3); - - // bridge - addWoodenFence(104, 36, 5); - addWoodenFence(105, 41, 3); - - // north-east fields - addWoodenFence(116, 13, 9); - addWoodenFence(116, 13, 11, false, true, true); - addWoodenFence(116, 24, 2); - addWoodenFence(118, 24, 2, false, true, true); - addWoodenFence(118, 26, 7); - addWoodenFence(125, 13, 6, false, true); - addWoodenFence(125, 22, 4, false, false, true); - addWoodenFence(140, 25, 12); - addWoodenFence(140, 25, 9, false, true, true); - addWoodenFence(152, 25, 9, false, true, true); - addWoodenFence(140, 34, 5); - addWoodenFence(148, 34, 4); - - // harbor - addStoneWall(13.2, 67, 1); - addStoneWall(15.2, 61, 3, false, false, true); - addStoneWall(20, 64, 3, false, false, true); - addStoneWall(20, 70, 4); - addStoneWall(16, 76, 3); - - // north hills - addEntities(entities.tree(15.06, 48.38, 0)); - addEntities(entities.tree(12.25, 44.21, 1 + 8)); - addEntities(entities.tree(29.50, 41.29, 2)); - addEntities(entities.tree(24.88, 37.67, 0 + 4)); - addEntities(entities.tree(45.91, 28.38, 1)); - addEntities(entities.tree(42.94, 24.33, 2)); - addEntities(entities.tree(31.78, 24.71, 0)); - addEntities(entities.tree(29.53, 22.29, 1 + 4)); - addEntities(entities.tree(26.69, 26.50, 2)); - addEntities(entities.tree(41.69, 13.25, 0)); - addEntities(entities.tree(65.13, 23.38, 1 + 8)); - addEntities(entities.tree(69.81, 19.75, 2 + 8)); - addEntities(entities.tree(70.91, 22.00, 0 + 4)); - addEntities(entities.tree(67.63, 28.33, 1)); - addEntities(entities.tree(62.84, 31.67, 2 + 4)); - addEntities(entities.tree(108.62, 8.71, 0)); - addEntities(entities.tree(105.09, 16.96, 1)); - addEntities(entities.tree(103.03, 19.25, 2 + 8)); - addEntities(entities.tree(107.81, 23.50, 0 + 4)); - addEntities(entities.tree(97.81, 38.63, 1)); - addEntities(entities.tree(108.63, 35.13, 2)); - addEntities(entities.tree(61.13, 43.67, 0)); - addEntities(entities.tree(24.44, 7.92, 0 + 4)); - addEntities(entities.tree(23.06, 16.54, 1 + 8)); - addEntities(entities.tree(24.66, 19.46, 2)); - addEntities(entities.tree(38.97, 6.96, 0)); - addEntities(entities.tree(41.19, 4.54, 1)); - addEntities(entities.tree(62.44, 2.96, 2 + 4)); - addEntities(entities.tree(58.50, 6.38, 0 + 4)); - addEntities(entities.tree(75.19, 5.04, 1 + 8)); - addEntities(entities.tree(89.91, 4.00, 2)); - addEntities(entities.tree(104.47, 6.58, 0)); - addEntities(entities.tree(102.53, 3.42, 1)); - addEntities(entities.tree(21.72, 6.04, 2 + 4)); - addEntities(entities.tree(12.06, 10.75, 0 + 8)); - addEntities(entities.tree(9.66, 6.46, 1)); - addEntities(entities.tree5(13.50, 7.63, 0)); - addEntities(entities.tree5(21.22, 20.17, 1)); - addEntities(entities.tree5(43.88, 6.79, 2)); - addEntities(entities.tree5(62.53, 5.88, 0)); - addEntities(entities.tree5(72.44, 7.29, 1)); - addEntities(entities.tree5(100.38, 5.63, 2)); - addEntities(entities.pine(1.66, 6.29, 0)); - addEntities(entities.pine(3.91, 9.67, 0)); - addEntities(entities.pine(1.59, 24.67, 0)); - addEntities(entities.pine(26.75, 4.13, 0)); - - // north-east fields - add(entities.pumpkin(142.84, 27.29)); - add(entities.pumpkin(143.59, 27.92)); - add(entities.pumpkin(142.75, 28.88)); - add(entities.pumpkin(150.81, 29.33)); - add(entities.pumpkin(151.22, 30.00)); - add(entities.pumpkin(148.53, 26.54)); - add(entities.pumpkin(140.69, 32.58)); - add(entities.pumpkin(141.03, 32.96)); - add(entities.pumpkin(150.47, 33.21)); - add(entities.pumpkin(150.91, 32.54)); - add(entities.pumpkin(149.91, 32.29)); - add(entities.pumpkin(150.38, 31.54)); - add(entities.pumpkin(144.44, 30.29)); - addEntities(entities.tree(153.59, 24.96, 0)); - addEntities(entities.tree(153.28, 5.58, 1 + 4)); - addEntities(entities.tree(155.03, 8.08, 2 + 8)); - addEntities(entities.tree(149.78, 11.13, 0)); - addEntities(entities.tree(124.56, 11.67, 1)); - addEntities(entities.tree(121.00, 7.25, 2 + 4)); - addEntities(entities.tree(114.47, 21.38, 0 + 8)); - addEntities(entities.tree(136.50, 5.79, 1 + 4)); - addEntities(entities.tree(138.53, 4.04, 2)); - addEntities(entities.tree(140.72, 9.58, 0)); - addEntities(entities.tree(138.22, 25.13, 1 + 4)); - addEntities(entities.tree(140.09, 23.54, 2 + 8)); - addEntities(entities.tree(125.72, 36.83, 0)); - addEntities(entities.tree(116.44, 35.00, 1)); - addEntities(entities.tree(117.53, 33.17, 2 + 4)); - addEntities(entities.pine(141.28, 37.29, 0)); - addEntities(entities.pine(155.88, 34.67, 0)); - addEntities(entities.pine(135.19, 34.88, 0)); - addEntities(entities.pine(155.16, 11.54, 0)); - addEntities(entities.pine(149.75, 17.25, 0)); - addEntities(entities.pine(151.88, 19.79, 0)); - - // harbor road - addEntities(entities.tree(12.91, 64.13, 0)); - addEntities(entities.tree(30.03, 77.58, 1)); - addEntities(entities.tree(21.22, 69.17, 2)); - addEntities(entities.tree(30.69, 53.00, 0)); - addEntities(entities.tree(33.72, 69.96, 1)); - addEntities(entities.tree(30.94, 64.96, 2)); - addEntities(entities.tree(39.72, 82.25, 0)); - addEntities(entities.tree(10.81, 59.25, 1)); - addEntities(entities.pine(37.78, 58.79, 0)); - - // south-west forest - addEntities(entities.pine(28.22, 132.04, 0)); - addEntities(entities.pine(30.91, 130.67, 0)); - addEntities(entities.pine(32.03, 145.79, 0)); - addEntities(entities.pine(27.75, 152.04, 0)); - addEntities(entities.pine(28.59, 113.42, 0)); - addEntities(entities.pine(40.34, 119.54, 0)); - addEntities(entities.pine(18.75, 114.63, 0)); - addEntities(entities.pine(16.69, 117.79, 0)); - addEntities(entities.pine(20.91, 122.38, 0)); - addEntities(entities.pine(23.31, 121.71, 0)); - addEntities(entities.pine(23.69, 126.58, 0)); - addEntities(entities.pine(6.25, 141.58, 0)); - addEntities(entities.pine(4.66, 147.21, 0)); - addEntities(entities.pine(8.06, 144.71, 0)); - addEntities(entities.pine(3.47, 123.46, 0)); - addEntities(entities.pine(35.00, 105.04, 0)); - addEntities(entities.pine(32.00, 101.96, 0)); - addEntities(entities.pine(16.69, 96.46, 0)); - addEntities(entities.pine(17.75, 100.00, 0)); - addEntities(entities.pine(21.03, 95.13, 0)); - addEntities(entities.pine(36.94, 134.46, 0)); - addEntities(entities.pine(37.75, 136.83, 0)); - add(entities.rock(20.84, 98.25)); - add(entities.rock(29.09, 119.00)); - add(entities.rock(36.81, 141.83)); - add(entities.rock(10.00, 149.58)); - add(entities.rock(5.53, 121.88)); - add(entities.rock(3.28, 103.38)); - add(entities.rock(36.84, 122.42)); - add(entities.lanternOn(33.19, 135.83)); - add(entities.lanternOn(28.16, 135.38)); - add(entities.lanternOn(26.41, 137.92)); - add(entities.lanternOn(26.91, 143.96)); - add(entities.lanternOn(34.25, 140.50)); - add(entities.lanternOn(30.88, 143.71)); - add(entities.lanternOn(20.63, 146.96)); - add(entities.lanternOn(15.06, 147.46)); - add(entities.lanternOn(14.94, 152.50)); - add(entities.lanternOn(19.13, 154.92)); - add(entities.lanternOn(21.00, 152.71)); - add(entities.lanternOn(29.84, 105.04)); - add(entities.lanternOn(24.34, 108.92)); - add(entities.lanternOn(29.59, 109.92)); - add(entities.boxLanterns(15.66, 154.88)).interact = giveLantern; - add(entities.boxLanterns(31.72, 134.50)).interact = giveLantern; - add(entities.boxLanterns(138.97, 27.33)).interact = giveLantern; - add(entities.boxLanterns(139.47, 10.83)).interact = giveLantern; - add(entities.treeStump1(23.09, 145.67)); - add(entities.treeStump2(30.78, 132.92)); - add(entities.treeStump1(35.31, 143.04)); - add(entities.treeStump2(18.31, 124.13)); - add(entities.treeStump1(7.50, 147.38)); - add(entities.treeStump2(5.22, 128.71)); - add(entities.treeStump1(22.22, 147.25)); - add(entities.treeStump2(24.91, 149.33)); - add(entities.treeStump1(9.63, 139.50)); - add(entities.treeStump2(11.59, 141.79)); - add(entities.treeStump1(12.16, 139.67)); - add(entities.treeStump2(17.09, 125.88)); - addEntities(entities.pine(12.81, 133.33, 0)); - addEntities(entities.pine(9.38, 115.92, 0)); - addEntities(entities.pine(11.63, 112.17, 0)); - addEntities(entities.pine(20.56, 134.58, 0)); - addEntities(entities.pine(21.81, 156.33, 0)); - addEntities(entities.pine5(10.94, 136.04, 0)); - addEntities(entities.pine5(24.44, 158.46, 0)); - addEntities(entities.pine5(22.47, 133.29, 0)); - addEntities(entities.pine5(10.47, 110.38, 0)); - addEntities(entities.pine4(9.72, 133.50, 0)); - addEntities(entities.pine4(23.88, 136.75, 0)); - addEntities(entities.pine4(19.63, 158.33, 0)); - add(entities.treeStump1(13.66, 104.42)); - add(entities.treeStump1(29.03, 100.88)); - add(entities.treeStump1(3.19, 110.88)); - add(entities.treeStump1(35.56, 92.04)); - add(entities.treeStump1(13.13, 88.33)); - add(entities.treeStump1(34.81, 90.54)); - add(entities.treeStump1(38.59, 115.58)); - addWoodenFence(9, 95.5, 5); - addWoodenFence(13.5, 90, 1, false, false, true); - addWoodenFence(13.5, 91, 3); - add(entities.lanternOn(12.69, 90.08)); - add(entities.lanternOn(8.88, 94.42)); - add(entities.lanternOn(8.97, 90.75)); - add(entities.boxLanterns(12.38, 87.96)).interact = giveLantern; - addEntities(entities.pine(72.63, 153.08, 0)); - addEntities(entities.pine(70.41, 157.92, 0)); - - addEntities(entities.pine5(4.84, 125.75, 0)); - addEntities(entities.pine5(30.91, 154.46, 0)); - addEntities(entities.pine5(42.56, 155.46, 0)); - addEntities(entities.pine5(31.88, 106.21, 0)); - addEntities(entities.pine5(1.28, 128.04, 0)); - addEntities(entities.pine5(15.59, 87.58, 0)); - addEntities(entities.pine5(37.41, 94.33, 0)); - addEntities(entities.pine5(29.63, 84.88, 0)); - addEntities(entities.pine5(13.41, 101.63, 0)); - addEntities(entities.pine5(4.25, 108.21, 0)); - addEntities(entities.pine5(27.28, 115.88, 0)); - addEntities(entities.pine5(36.00, 139.08, 0)); - addEntities(entities.pine5(36.41, 61.08, 0)); - addEntities(entities.pine5(26.69, 57.04, 0)); - addEntities(entities.pine5(6.28, 54.46, 0)); - addEntities(entities.pine5(7.22, 43.50, 0)); - addEntities(entities.pine5(6.41, 30.54, 0)); - addEntities(entities.pine5(7.88, 14.13, 0)); - addEntities(entities.pine5(132.75, 38.08, 0)); - addEntities(entities.pine5(148.16, 21.42, 0)); - addEntities(entities.pine5(157.44, 14.13, 0)); - addEntities(entities.pine5(154.25, 32.38, 0)); - - addEntities(entities.tree5(41.69, 85.00, 0)); - addEntities(entities.tree5(24.91, 79.29, 1)); - addEntities(entities.tree5(24.06, 68.17, 2)); - addEntities(entities.tree5(10.00, 62.00, 0)); - addEntities(entities.tree5(8.94, 47.38, 1)); - addEntities(entities.tree5(33.06, 56.04, 2)); - addEntities(entities.tree5(32.94, 35.21, 0)); - addEntities(entities.tree5(39.88, 15.58, 1)); - addEntities(entities.tree5(42.38, 29.96, 2)); - addEntities(entities.tree5(69.53, 30.46, 0)); - addEntities(entities.tree5(85.94, 21.08, 1)); - addEntities(entities.tree5(84.38, 23.33, 2)); - addEntities(entities.tree5(103.91, 22.46, 0)); - addEntities(entities.tree5(110.50, 33.71, 1)); - addEntities(entities.tree5(103.63, 46.04, 2)); - addEntities(entities.tree5(62.81, 46.25, 0)); - - addEntities(entities.tree4(136.78, 8.79, 0)); - addEntities(entities.tree4(155.53, 26.58, 1)); - addEntities(entities.tree4(127.63, 38.21, 2)); - addEntities(entities.tree4(117.03, 8.42, 0)); - addEntities(entities.tree4(109.47, 25.83, 1)); - addEntities(entities.tree4(100.47, 20.96, 2)); - addEntities(entities.tree4(108.94, 1.83, 0)); - addEntities(entities.tree4(76.25, 15.42, 1)); - addEntities(entities.tree4(77.81, 5.88, 2)); - addEntities(entities.tree4(42.78, 15.25, 0)); - addEntities(entities.tree4(27.84, 28.21, 1)); - addEntities(entities.tree4(28.25, 43.75, 2)); - addEntities(entities.tree4(10.66, 46.75, 0)); - addEntities(entities.tree4(29.34, 66.25, 1)); - addEntities(entities.tree4(25.59, 84.79, 2)); - addEntities(entities.pine4(4.81, 59.46, 0)); - addEntities(entities.pine4(12.88, 85.58, 0)); - addEntities(entities.pine4(10.81, 99.63, 0)); - addEntities(entities.pine4(19.47, 127.13, 0)); - addEntities(entities.pine4(3.19, 130.63, 0)); - addEntities(entities.pine4(11.91, 148.50, 0)); - addEntities(entities.pine4(28.06, 155.58, 0)); - addEntities(entities.pine4(39.81, 147.38, 0)); - addEntities(entities.pine4(40.75, 132.83, 0)); - addEntities(entities.pine4(25.63, 113.83, 0)); - addEntities(entities.pine4(36.47, 102.96, 0)); - addEntities(entities.pine4(5.75, 109.96, 0)); - addEntities(entities.pine(5.66, 156.04, 0)); - addEntities(entities.pine(8.25, 158.83, 0)); - addEntities(entities.pine(37.38, 158.75, 0)); - addEntities(entities.pine(0.75, 139.38, 0)); - addEntities(entities.pine5(2.00, 137.63, 0)); - addEntities(entities.pine4(10.16, 156.88, 0)); - addEntities(entities.pine4(35.28, 157.71, 0)); - addEntities(entities.pine5(34.09, 148.13, 0)); - addEntities(entities.pine3(22.28, 143.50, 0)); - addEntities(entities.pine3(2.09, 141.83, 0)); - addEntities(entities.pine3(42.22, 121.67, 0)); - addEntities(entities.pine3(29.94, 121.00, 0)); - addEntities(entities.pine3(16.47, 120.25, 0)); - addEntities(entities.pine3(38.50, 105.08, 0)); - addEntities(entities.pine3(15.38, 103.58, 0)); - addEntities(entities.pine3(41.34, 94.21, 0)); - addEntities(entities.pine3(27.84, 87.21, 0)); - addEntities(entities.pine3(25.63, 58.58, 0)); - addEntities(entities.pine3(6.66, 15.96, 0)); - addEntities(entities.pine3(1.47, 39.46, 0)); - addEntities(entities.pine3(28.63, 6.08, 0)); - add(entities.rock(15.78, 34.00)); - add(entities.rock(32.63, 25.42)); - add(entities.rock(8.91, 7.29)); - add(entities.rock(51.63, 13.17)); - add(entities.rock(86.00, 22.29)); - add(entities.rock(104.91, 7.92)); - add(entities.rock(108.53, 24.21)); - add(entities.rock(98.63, 38.04)); - add(entities.rock(117.22, 24.63)); - - add(createSignWithText(70.5, 70.5, 'Pony Town', ' Pony Town\n[under construction]', entities.sign)); - - addEntities(createToyStash(47.00, 55.00)); - - addEntities(entities.pine3(72.78, 64.13, 0)); - - const addCat = (x: number, y: number) => { - const entity = add(entities.cat(x, y)); - let delay = 5; - let boopDelay = 5; - let hideDelay = 5; - let hidden = false; - - entity.boopY = -0.1; - entity.boop = () => { - if (!hidden && boopDelay < 0) { - setTimeout(() => sayToAll(entity, '😠', '😠', MessageType.Thinking, {}), 500); - boopDelay = random(5, 10, true); - } - }; - - entity.serverUpdate = delta => { - delay -= delta; - boopDelay -= delta; - hideDelay -= delta; - - if (hideDelay < 0 && delay < 0) { - if (hidden) { - hidden = false; - setEntityAnimation(entity, CatAnimation.Enter); - hideDelay = random(30, 60, true); - delay = random(2, 4, true); - } else { - hidden = true; - setEntityAnimation(entity, CatAnimation.Exit); - hideDelay = random(15, 30, true); - } - } else if (!hidden && delay < 0) { - const rand = Math.random(); - - if (rand < 0.1) { - sayToAll(entity, 'meow', 'meow', MessageType.System, {}); - delay = random(2, 4, true); - } else if (rand < 0.5) { - setEntityAnimation(entity, CatAnimation.Wag); - delay = random(2, 4, true); - } else { - setEntityAnimation(entity, CatAnimation.Blink); - delay = random(2, 4, true); - } - } - }; - }; - - add(createSign(70, 61.5, 'Letter Sign', give(entities.letter.type, `Here's your letter!`), entities.sign)); - - add(entities.mistletoe(43.00, 48.00)); - add(entities.mistletoe(78.00, 85.70)); - add(entities.fence3(46.50, 53.00)); - add(entities.fence3(65.00, 71.00)); - add(entities.fence3(64.00, 76.00)); - add(entities.fence3(55.00, 76.50)); - add(entities.fence2(92.00, 96.00)); - add(entities.fence2(85.00, 97.00)); - add(entities.fence1(48.00, 75.00)); - - // pine trees - add(entities.pine1(48.50, 65.50)); - add(entities.pine2(51.00, 65.00)); - - addEntities(entities.pine3(52.00, 63.00, 0)); - addEntities(entities.pine4(53.00, 67.00, 0)); - addEntities(entities.pine5(43.00, 62.00, 0)); - addEntities(entities.pine(48.50, 63.00, 0)); - addEntities(entities.pine(46.00, 67.00, 0)); - addEntities(entities.pine(42.00, 69.00, 0)); - addEntities(entities.pine3(113.00, 104.00, 0)); - add(createCookieTable2(74.00, 66.00)); - - addEntities(entities.pine(93.00, 53.00, 0)); - addEntities(entities.pine(97.00, 57.00, 0)); - addEntities(entities.pine(94.00, 62.00, 0)); - addEntities(entities.pine(64.00, 91.00, 0)); - addEntities(entities.pine(59.00, 95.00, 0)); - addEntities(entities.pine(63.00, 98.00, 0)); - - // small trees - add(entities.trees1[0](51.50, 69.00)); - add(entities.trees1[1](75.30, 72.20)); - add(entities.trees1[2](93.00, 67.00)); - add(entities.trees1[0](87.60, 47.00)); - add(entities.trees2[0](71.70, 78.70)); - add(entities.trees2[1](94.80, 94.60)); - add(entities.trees2[2](87.00, 52.70)); - add(entities.trees3[2](87.50, 45.50)); - add(entities.trees3[0](88.30, 82.50)); - add(entities.trees3[2](95.50, 94.00)); - add(entities.trees3[0](84.50, 97.50)); - add(entities.trees3[1](74.00, 83.80)); - add(entities.trees3[2](46.00, 77.50)); - - addEntities(entities.tree4(61.70, 70.50, 0)); - addEntities(entities.tree4(47.50, 74.50, 1)); - addEntities(entities.tree4(69.00, 53.50, 2)); - addEntities(entities.tree4(88.50, 57.70, 0)); - addEntities(entities.tree4(83.50, 64.50, 1)); - addEntities(entities.tree4(76.00, 82.00, 2)); - addEntities(entities.tree4(90.00, 82.70, 1)); - addEntities(entities.tree4(95.00, 80.00, 2)); - addEntities(entities.tree4(97.00, 88.00, 0)); - addEntities(entities.tree4(83.50, 96.50, 0)); - addEntities(entities.tree4(84.00, 91.00, 1)); - addEntities(entities.tree5(86.00, 46.50, 0)); - addEntities(entities.tree5(84.00, 55.00, 1)); - addEntities(entities.tree5(63.00, 71.60, 1)); - addEntities(entities.tree5(45.00, 76.00, 0)); - addEntities(entities.tree5(82.00, 80.50, 0)); - addEntities(entities.tree5(89.00, 81.50, 2)); - addEntities(entities.tree5(98.00, 85.50, 2)); - addEntities(entities.tree5(90.00, 95.00, 0)); - addEntities(entities.tree5(78.00, 90.50, 0)); - - // trees - top - addTree(45.00, 45.00, 1); - addTree(42.00, 47.00, 2 + 4, false, isHalloween); - addTree(48.00, 50.00, 0 + 8, true, true); - addTree(82.00, 53.00, 1); - - addEntities(entities.tree4(71.50, 47.00, 0)); - addEntities(entities.tree5(70.00, 46.00, 1)); - add(entities.trees2[2](68.50, 48.00)); - - // trees - bottom right - addTree(83.00, 87.00, 1); - addTree(85.00, 82.00, 0 + 8, isHalloween); - addTree(87.00, 90.00, 1 + 4); - addTree(92.00, 85.00, 2, true, true); - addTree(79.00, 85.00, 3 + 4); - addTree(81.00, 92.00, 0 + 8, isHalloween, isHalloween); - addTree(90.00, 79.00, 1 + 4); - addTree(94.00, 76.00, 2); - addTree(96.00, 87.00, 3 + 4); - addTree(93.00, 92.00, 0 + 8, isHalloween); - addTree(97.00, 82.00, 1 + 4, false, isHalloween); - - // tree stumps - right - add(entities.treeStump2(91.50, 68.50)); - add(entities.treeStump1(92.50, 66.50)); - add(entities.treeStump2(88.50, 64.50)); - - // trees - 80x80 - addTree(102.71, 82.41, 0); - addTree(102.09, 89.41, 1 + 4); - addTree(108.71, 82.37, 2 + 8); - addTree(111.37, 86.58, 0); - addTree(116.65, 95.12, 1 + 8); - addTree(118.43, 98.41, 2); - addTree(104.59, 106.54, 0 + 4); - addTree(101.12, 110.79, 1 + 8); - addTree(107.28, 114.20, 2, isHalloween); - addTree(119.62, 114.91, 0); - addTree(96.53, 94.95, 1 + 4); - addTree(94.75, 102.37, 2 + 4); - addTree(81.81, 112.12, 0 + 8, isHalloween); - addTree(88.03, 115.83, 1); - addTree(111.84, 118.62, 2 + 4); - addTree(118.78, 47.50, 2 + 8); - - // top right - addTree(135.00, 66.90, 1); - addTree(156.38, 75.17, 0); - addTree(149.38, 78.33, 1 + 4, isHalloween); - addTree(151.13, 81.33, 2); - addTree(156.88, 86.58, 0 + 4); - addTree(152.31, 88.83, 1 + 8, isHalloween, isHalloween); - addTree(155.50, 90.17, 2); - addTree(140.88, 83.92, 0); - addTree(147.56, 95.00, 1 + 8); - - addEntities(entities.pine(141.13, 45.67, 0)); - addEntities(entities.pine(138.31, 52.33, 0)); - addEntities(entities.pine(148.25, 49.00, 0)); - addEntities(entities.pine(155.06, 46.17, 0)); - addEntities(entities.pine(157.38, 49.17, 0)); - addEntities(entities.pine(150.56, 60.42, 0)); - addEntities(entities.tree5(147.88, 80.42, 2)); - addEntities(entities.tree5(154.31, 93.33, 0)); - addEntities(entities.pine5(155.72, 52.13, 0)); - addEntities(entities.pine5(139.66, 54.50, 0)); - addEntities(entities.pine4(149.72, 51.04, 0)); - addEntities(entities.pine4(142.09, 57.75, 0)); - addEntities(entities.pine4(153.34, 48.21, 0)); - addEntities(entities.pine3(147.63, 53.96, 0)); - addEntities(entities.pine3(143.47, 44.25, 0)); - addEntities(entities.pine3(136.06, 53.88, 0)); - add(entities.treeStump1(136.91, 45.67)); - add(entities.treeStump2(132.69, 51.63)); - add(entities.treeStump1(131.41, 48.08)); - add(entities.lanternOn(136.09, 67.29)); - add(entities.lanternOn(132.38, 68.38)); - add(entities.lanternOn(138.53, 67.75)); - add(entities.rock(151.16, 63.21)); - add(entities.rock(148.66, 79.38)); - add(entities.treeStump1(154.72, 54.83)); - add(entities.treeStump2(152.22, 63.29)); - add(entities.treeStump1(149.84, 67.58)); - add(entities.treeStump2(152.81, 70.54)); - add(entities.treeStump1(154.06, 64.63)); - add(entities.treeStump2(156.50, 67.38)); - add(entities.treeStump1(156.56, 61.29)); - add(entities.treeStump2(155.44, 60.38)); - add(entities.treeStump1(157.41, 56.67)); - - // forest - addTree(122.69, 106.50, 0); - addTree(131.69, 105.58, 1); - addTree(130.94, 122.33, 2 + 4, isHalloween); - addTree(132.69, 118.08, 0 + 8); - addTree(124.38, 138.25, 1 + 8); - addTree(134.94, 137.42, 2, isHalloween, isHalloween); - addTree(131.94, 134.25, 0); - addTree(138.25, 130.92, 1 + 8); - addTree(121.69, 121.00, 2 + 4); - addTree(111.94, 126.83, 0 + 4, isHalloween); - addTree(115.06, 125.25, 1 + 8); - addTree(114.44, 129.58, 2); - addTree(120.69, 136.25, 0); - addTree(125.00, 98.92, 1 + 8); - addTree(128.56, 93.92, 2); - addTree(140.88, 111.33, 0); - addTree(137.94, 114.83, 1); - addTree(147.19, 113.58, 2, isHalloween); - addTree(143.94, 118.17, 0 + 4); - addTree(149.94, 122.92, 1 + 8); - addTree(148.13, 125.67, 2, isHalloween, isHalloween); - addTree(144.19, 136.08, 0); - addTree(152.13, 130.33, 1 + 4); - addTree(155.88, 107.25, 2 + 8); - addTree(132.31, 148.75, 0, isHalloween); - addEntities(entities.tree5(125.81, 109.00, 0)); - addEntities(entities.tree5(119.31, 122.75, 1)); - addEntities(entities.tree5(141.88, 114.58, 2)); - addEntities(entities.tree5(139.13, 138.50, 0)); - addEntities(entities.tree5(150.25, 115.92, 1)); - addEntities(entities.tree5(118.69, 138.50, 2)); - addEntities(entities.tree5(133.56, 121.67, 0)); - addEntities(entities.tree4(130.50, 97.17, 0)); - addEntities(entities.tree4(116.00, 116.08, 1)); - addEntities(entities.tree4(133.44, 108.00, 2)); - addEntities(entities.tree4(110.50, 130.33, 0)); - addEntities(entities.tree4(145.31, 120.25, 1)); - addEntities(entities.tree4(149.40, 129.33, 2)); - addEntities(entities.tree4(141.81, 85.67, 0)); - addEntities(entities.tree4(158.19, 108.67, 1)); - add(entities.tree3(123.50, 122.50)); - add(entities.tree3(139.63, 116.25)); - add(entities.tree3(112.75, 130.83)); - add(entities.tree3(156.63, 92.42)); - add(entities.tree3(137.00, 138.08)); - add(entities.rock(133.81, 137.54)); - add(entities.rock(122.97, 139.54)); - add(entities.rock(130.22, 121.79)); - add(entities.rock(126.25, 109.54)); - add(entities.rock(151.00, 129.83)); - add(entities.treeStump1(141.53, 117.29)); - add(entities.treeStump2(139.31, 132.38)); - add(entities.treeStump1(117.78, 125.63)); - add(entities.treeStump2(125.66, 111.50)); - add(entities.treeStump1(123.06, 113.79)); - add(entities.treeStump2(124.13, 117.71)); - add(entities.treeStump1(129.25, 99.88)); - add(entities.lanternOn(52.97, 134.75)); - add(entities.lanternOn(49.19, 138.25)); - add(entities.lanternOn(49.25, 142.08)); - add(entities.lanternOn(54.97, 142.96)); - add(entities.lanternOn(45.53, 146.04)); - - // graveyard - addStoneWall(142, 98, 2, false, true); // left (1) - addStoneWall(142, 104, 2, false, false, true); // left (2) - addStoneWall(142, 98, 5); // top - addStoneWall(152, 98, 5, false, true, true); // right - addStoneWall(142, 108, 5); // bottom - - add(entities.lanternOnWall(148.00, 98.20)); - add(entities.lanternOnWall(144.00, 108.20)); - add(entities.lanternOnWall(152.00, 108.20)); - addCat(149, 108.19); - - // forest path - add(entities.boxLanterns(52.50, 133.96)).interact = giveLantern; - add(entities.boxLanterns(124.53, 112.67)).interact = giveLantern; - add(entities.boxLanterns(142.59, 118.54)).interact = giveLantern; - - addWoodenFence(132, 108, 6, false); - addWoodenFence(125, 121, 4, false); - addWoodenFence(131, 130, 3, false); - addWoodenFence(125, 132, 4, false); - addWoodenFence(118, 100, 6); - addWoodenFence(117, 105, 3); - - add(entities.lanternOn(123.59, 112.67)); - add(entities.lanternOn(124.03, 116.42)); - add(entities.lanternOn(141.50, 118.25)); - add(entities.lanternOn(138.25, 116.63)); - add(entities.lanternOn(145.63, 121.33)); - add(entities.lanternOn(135.78, 122.00)); - add(entities.lanternOn(137.78, 124.17)); - add(entities.lanternOn(138.53, 120.54)); - add(entities.lanternOn(143.44, 119.04)); - add(entities.lanternOn(94.41, 132.92)); - add(entities.lanternOn(95.53, 136.17)); - add(entities.lanternOn(91.31, 133.71)); - add(entities.lanternOn(90.56, 136.58)); - add(entities.lanternOn(93.34, 138.17)); - - // bottom left - addEntities(entities.pine(45.00, 126.33, 0)); - addEntities(entities.pine(52.69, 130.42, 0)); - addEntities(entities.pine(49.81, 133.58, 0)); - addEntities(entities.pine(46.75, 140.92, 0)); - addEntities(entities.pine(57.56, 143.92, 0)); - addEntities(entities.pine(52.38, 150.42, 0)); - addEntities(entities.pine(41.50, 144.83, 0)); - addEntities(entities.pine(44.06, 151.17, 0)); - addEntities(entities.pine(95.56, 122.83, 0)); - addEntities(entities.pine(105.81, 120.92, 0)); - addEntities(entities.pine(103.19, 131.00, 0)); - addEntities(entities.pine(100.81, 129.67, 0)); - addEntities(entities.pine(97.88, 133.33, 0)); - addEntities(entities.pine(108.13, 137.92, 0)); - addEntities(entities.pine(105.13, 139.50, 0)); - addEntities(entities.pine5(45.31, 143.75, 0)); - addEntities(entities.pine5(53.94, 153.50, 0)); - addEntities(entities.pine5(41.56, 130.83, 0)); - addEntities(entities.pine5(58.63, 132.58, 0)); - addEntities(entities.pine5(60.06, 147.17, 0)); - addEntities(entities.pine5(43.69, 133.25, 0)); - addEntities(entities.pine4(55.63, 133.92, 0)); - addEntities(entities.pine4(50.31, 152.83, 0)); - addEntities(entities.pine4(62.31, 143.92, 0)); - addEntities(entities.pine4(98.50, 139.08, 0)); - addEntities(entities.pine4(107.38, 123.75, 0)); - addEntities(entities.pine4(89.63, 125.33, 0)); - addEntities(entities.pine4(64.19, 149.00, 0)); - addEntities(entities.pine3(57.00, 127.75, 0)); - addEntities(entities.pine3(62.31, 151.17, 0)); - addEntities(entities.pine3(51.00, 155.58, 0)); - addEntities(entities.pine3(110.25, 140.25, 0)); - addEntities(entities.pine3(90.06, 130.33, 0)); - addEntities(entities.pine3(105.75, 124.75, 0)); - - // bottom center - addStoneWall(93, 141.5, 3); - addStoneWall(101, 141.5, 3); - addStoneWall(90, 146, 3); - addStoneWall(103, 146.5, 3); - - // bottom right - addWoodenFence(118, 140, 4); - addWoodenFence(121, 145, 4); - addWoodenFence(136, 140, 6); - addWoodenFence(134, 144, 6); - addWoodenFence(143, 144.5, 2); - - // pumpkin field - addEntities(entities.pine5(100.38, 120.58, 0)); - addEntities(entities.pine5(93.31, 128.58, 0)); - addEntities(entities.pine5(100.00, 137.75, 0)); - - addWoodenFence(76, 120, 12); // top - addWoodenFence(76, 120, 5, false, true); // left 1 - addWoodenFence(76, 129, 7, false, false); // left 2 - addWoodenFence(88, 120, 10, false, true); // right 1 - addWoodenFence(88, 130, 1, true, true, true); // right 2 - addWoodenFence(89, 130, 7, false, false, true); // right 3 - addWoodenFence(76, 136, 6); // bottom 1 - addWoodenFence(82, 136, 1, false, true, true); // bottom 2 - addWoodenFence(82, 137, 7); // bottom 3 - - addWoodenFence(82, 147, 6); // top - addWoodenFence(75, 147, 4); // top - addWoodenFence(88, 147, 10, false, true); // right - addWoodenFence(75, 157, 13, true, false, true); // bottom - addWoodenFence(75, 147, 10, false, true, true); // left - - if (true) { - add(entities.pumpkin(77.44, 124.67)); - add(entities.pumpkin(78.31, 125.75)); - add(entities.pumpkin(80.63, 124.00)); - add(entities.pumpkin(84.25, 123.83)); - add(entities.pumpkin(82.81, 126.67)); - add(entities.pumpkin(80.75, 129.17)); - add(entities.pumpkin(82.19, 128.42)); - add(entities.pumpkin(82.38, 129.42)); - add(entities.pumpkin(81.56, 129.67)); - add(entities.pumpkin(76.94, 131.25)); - add(entities.pumpkin(78.25, 134.67)); - add(entities.pumpkin(80.56, 134.00)); - add(entities.pumpkin(84.88, 124.17)); - add(entities.pumpkin(83.81, 124.83)); - add(entities.pumpkin(84.56, 132.50)); - add(entities.pumpkin(84.19, 133.83)); - add(entities.pumpkin(84.75, 135.83)); - add(entities.pumpkin(87.13, 132.50)); - add(entities.pumpkin(86.56, 134.00)); - add(entities.pumpkin(87.70, 134.50)); - add(entities.pumpkin(88.31, 135.67)); - add(entities.pumpkin(82.25, 122.17)); - } - - add(entities.treeStump1(91.25, 123.67)); - add(entities.treeStump1(94.94, 126.33)); - add(entities.treeStump1(92.56, 131.42)); - - // rock circle 2 - add(entities.rock(111.63, 152.33)); - add(entities.rock(107.63, 147.92)); - add(entities.rock(105.31, 152.33)); - add(entities.rock(108.50, 156.33)); - add(entities.rock(115.44, 155.67)); - add(entities.rock(115.38, 149.67)); - add(entities.rock(113.69, 147.33)); - add(entities.rock(105.63, 150.50)); - add(entities.rock(112.88, 157.25)); - add(entities.rock(117.38, 152.83)); - addEntities(entities.pine3(106.38, 148.58, 0)); - addEntities(entities.pine4(115.94, 148.25, 0)); - addEntities(entities.pine4(114.13, 157.92, 0)); - addEntities(entities.pine5(104.25, 150.50, 0)); - - addEntities(entities.tree5(85.28, 116.79, 1)); - addEntities(entities.tree5(91.03, 118.33, 2)); - addEntities(entities.tree5(104.46, 115.70, 0)); - addEntities(entities.tree5(112.96, 107.33, 1)); - addEntities(entities.tree5(100.84, 92.20, 2)); - addEntities(entities.tree5(107.62, 84.29, 0)); - addEntities(entities.tree5(97.43, 64.04, 1)); - addEntities(entities.tree5(116.28, 44.75, 2)); - addEntities(entities.tree5(118.68, 70.62, 0)); - addEntities(entities.tree4(89.81, 103.54, 0)); - addEntities(entities.tree4(110.78, 92.29, 1)); - addEntities(entities.tree4(117.09, 69.66, 2)); - - add(entities.trees3[0](76.37, 118.08)); - - addEntities(entities.pine(92.28, 104.58, 0)); - addEntities(entities.pine(116.15, 105.75, 0)); - addEntities(entities.pine(112.12, 103.45, 0)); - addEntities(entities.pine(118.93, 88.95, 0)); - addEntities(entities.pine(111.34, 77.33, 0)); - addEntities(entities.pine(114.62, 75.66, 0)); - addEntities(entities.pine(98.43, 117.79, 0)); - addEntities(entities.pine(42.59, 115.91, 0)); - addEntities(entities.pine(47.09, 117.70, 0)); - addEntities(entities.pine5(44.59, 104.50, 0)); - addEntities(entities.pine4(95.34, 106.58, 0)); - addEntities(entities.pine3(47.78, 104.75, 0)); - addEntities(entities.pine3(58.78, 110.04, 0)); - - add(entities.pine2(57.50, 109.00)); - add(entities.fence3(86.43, 100.58)); - add(entities.fence3(93.96, 100.50)); - - add(entities.treeStump1(105.31, 89.12)); - add(entities.treeStump2(108.25, 88.16)); - add(entities.treeStump1(113.37, 91.54)); - add(entities.treeStump2(114.12, 95.45)); - add(entities.treeStump1(109.53, 100.58)); - add(entities.treeStump2(107.46, 100.62)); - add(entities.treeStump1(99.34, 101.04)); - add(entities.treeStump2(101.12, 95.12)); - add(entities.treeStump1(102.43, 91.79)); - add(entities.treeStump2(80.18, 113.66)); - add(entities.treeStump1(81.81, 114.95)); - add(entities.treeStump2(77.62, 118.58)); - add(entities.treeStump1(46.93, 106.70)); - add(entities.treeStump2(51.81, 117.41)); - add(entities.treeStump1(53.50, 115.58)); - add(entities.treeStump2(117.03, 48.66)); - - add(entities.rock(104.81, 88.83)); - add(entities.rock(114.53, 95.25)); - add(entities.rock(103.96, 106.25)); - add(entities.rock(84.50, 116.21)); - add(entities.rock(54.25, 116.58)); - add(entities.rock(118.06, 49.20)); - add(entities.rock(104.56, 47.65)); - - add(entities.boxLanterns(113.45, 95.16)).interact = giveLantern; - add(entities.box(105.90, 88.81)); - add(entities.lanternOn(113.31, 95.48)); - add(entities.lanternOn(112.64, 94.42)); - add(entities.lanternOn(105.98, 89.17)); - add(entities.lanternOn(106.81, 100.67)); - add(entities.lanternOn(102.94, 92.25)); - - add(entities.fence1(80.03, 97.00)); - add(entities.fence2(74.81, 100.79)); - - // rocks - add(entities.rock(46.00, 50.00)); - add(entities.rock(48.50, 81.50)); - add(entities.rock(45.50, 84.50)); - add(entities.rock(44.50, 88.50)); - add(entities.rock(46.50, 93.50)); - add(entities.rock(50.50, 94.50)); - add(entities.rock(55.50, 93.50)); - add(entities.rock(56.50, 91.50)); - add(entities.rock(56.50, 86.50)); - add(entities.rock(55.50, 83.50)); - add(entities.rock(52.50, 81.50)); - add(entities.rock(50.50, 87.50)); - add(entities.rock(70.50, 62.30)); - add(entities.rock(74.50, 67.50)); - add(entities.rock(86.50, 52.50)); - add(entities.rock(91.50, 55.50)); - add(entities.rock(88.50, 58.50)); - add(entities.rock(52.00, 68.50)); - add(entities.rock(52.50, 69.50)); - add(entities.rock(61.50, 71.50)); - add(entities.rock(83.50, 91.50)); - add(entities.rock(90.50, 83.50)); - - // pumpkins - add(entities.pumpkin(74.50, 88.50)); - add(entities.pumpkin(74.00, 90.00)); - add(entities.pumpkin(68.00, 87.00)); - add(entities.pumpkin(69.00, 93.00)); - add(entities.pumpkin(68.00, 95.00)); - add(entities.pumpkin(69.70, 61.80)); - - addCat(67.06, 71); - - // lights - - const addJacko = createAddLight(world, map, entities.jacko); - const addTorch = createAddLight(world, map, entities.torch); - - // jack-o-lanterns - // top left - addJacko(45, 48); - - add(entities.lanternOn(66.75, 62)); - add(entities.lanternOn(67.7, 62.8)); - add(entities.lanternOn(69.5, 62.2)); - add(entities.boxLanterns(67.6, 62.6)).interact = giveLantern; - add(entities.boxLanterns(66.5, 63.3)).interact = giveLantern; - - add(entities.treeStump1(70, 62.5)); - - // top left - addTorch(50.00, 54.00); - addTorch(46.50, 55.00); - // top - addTorch(76.00, 53.00); - addTorch(63.00, 50.00); - addTorch(65.90, 64.00); - addTorch(68.00, 47.00); - addTorch(71.00, 50.00); - addTorch(70.00, 57.00); - addTorch(68.30, 62.00); - addTorch(70.70, 61.50); - // top right - addTorch(92.00, 59.00); - addTorch(88.00, 53.00); - addTorch(94.00, 45.00); - addTorch(89.00, 45.00); - addTorch(92.00, 49.00); - // left - addTorch(44.00, 65.00); - addTorch(49.00, 69.00); - addTorch(55.00, 65.00); - // center - addTorch(70.00, 69.00); - // right - addTorch(84.00, 65.00); - addTorch(80.00, 68.00); - addTorch(86.00, 71.00); - addTorch(98.00, 66.50); - // bot left - addTorch(47.00, 80.50); - addTorch(43.00, 87.00); - addTorch(49.00, 94.00); - addTorch(57.00, 91.00); - addTorch(56.00, 82.00); - addTorch(49.00, 87.00); - addTorch(60.00, 88.00); - // bot - addTorch(72.00, 79.00); - addTorch(68.00, 81.00); - addTorch(69.00, 88.80); - // bot right - addTorch(87.00, 94.00); - addTorch(101.50, 94.00); - addTorch(94.50, 93.80); - addTorch(100.00, 100.00); - addTorch(99.00, 95.00); - // 80x80 - addTorch(47.81, 106.50); - addTorch(52.00, 110.85); - addTorch(56.75, 116.96); - addTorch(52.03, 116.88); - addTorch(59.88, 100.50); - addTorch(63.81, 101.67); - addTorch(60.53, 104.63); - addTorch(62.00, 102.88); - addTorch(63.47, 106.33); - addTorch(66.97, 104.00); - addTorch(77.22, 116.17); - addTorch(73.13, 100.13); - addTorch(76.78, 100.42); - addTorch(85.53, 105.50); - addTorch(89.84, 106.33); - addTorch(86.78, 109.25); - addTorch(116.34, 110.83); - addTorch(112.00, 107.60); - addTorch(109.94, 111.46); - addTorch(113.78, 113.83); - addTorch(112.97, 110.83); - addTorch(96.09, 114.88); - addTorch(100.09, 50.75); - addTorch(112.59, 45.96); - addTorch(116.03, 47.92); - addTorch(116.94, 59.08); - addTorch(109.06, 64.29); - addTorch(111.94, 69.96); - addTorch(104.94, 71.21); - addTorch(102.91, 49.42); - addTorch(115.00, 79.79); - addTorch(116.63, 77.46); - addTorch(113.19, 78.17); - addTorch(64.36, 103.92); - // 15x15 - addTorch(117.09, 140.33); - addTorch(120.13, 144.79); - addTorch(123.34, 140.13); - addTorch(126.31, 144.79); - addTorch(134.00, 143.75); - addTorch(133.88, 139.00); - addTorch(131.66, 135.79); - addTorch(126.03, 137.58); - addTorch(125.94, 133.08); - addTorch(130.06, 129.50); - addTorch(125.81, 128.67); - addTorch(125.88, 123.29); - addTorch(130.09, 123.08); - addTorch(126.19, 118.75); - addTorch(127.22, 114.00); - addTorch(132.54, 111.75); - addTorch(127.13, 109.50); - addTorch(130.72, 106.33); - addTorch(121.47, 106.10); - addTorch(123.69, 100.71); - addTorch(126.69, 101.96); - addTorch(119.28, 100.58); - addTorch(117.89, 104.67); - addTorch(138.34, 139.08); - addTorch(139.69, 144.17); - addTorch(142.78, 139.75); - addTorch(145.16, 143.96); - addTorch(148.34, 138.88); - addTorch(148.53, 143.79); - addTorch(142.50, 144.42); - addTorch(138.34, 147.29); - addTorch(140.72, 147.71); - addTorch(143.81, 148.00); - addTorch(142.91, 151.58); - addTorch(139.47, 151.08); - addTorch(136.34, 150.79); - addTorch(136.41, 144.88); - addTorch(140.72, 155.04); - addTorch(152.69, 152.17); - addTorch(155.78, 152.42); - addTorch(153.84, 155.54); - addTorch(150.88, 131.13); - addTorch(153.38, 130.83); - addTorch(113.78, 146.13); - addTorch(110.94, 141.38); - addTorch(107.06, 141.83); - addTorch(107.13, 145.71); - addTorch(100.63, 141.79); - addTorch(101.97, 146.08); - addTorch(96.44, 145.92); - addTorch(98.41, 141.71); - addTorch(92.63, 141.79); - addTorch(90.28, 145.83); - addTorch(88.22, 140.79); - addTorch(88.03, 138.17); - addTorch(83.50, 138.33); - addTorch(79.59, 136.88); - addTorch(75.69, 136.75); - addTorch(75.19, 132.13); - addTorch(75.22, 121.33); - addTorch(78.63, 111.50); - addTorch(81.69, 105.92); - addTorch(82.78, 100.71); - addTorch(69.84, 115.58); - addTorch(68.94, 119.21); - addTorch(69.03, 122.75); - addTorch(69.00, 126.42); - addTorch(69.25, 131.79); - addTorch(69.59, 136.83); - addTorch(86.09, 145.33); - addTorch(82.09, 145.00); - addTorch(78.16, 144.79); - addTorch(73.78, 144.96); - addTorch(74.41, 149.21); - addTorch(66.16, 142.13); - addTorch(69.09, 140.71); - addTorch(69.59, 144.63); - addTorch(71.13, 147.83); - addTorch(58.22, 149.33); - addTorch(61.09, 150.92); - addTorch(63.88, 140.50); - addTorch(59.91, 138.63); - addTorch(56.72, 136.50); - addTorch(124.56, 46.29); - addTorch(127.19, 48.63); - addTorch(123.91, 50.33); - addTorch(126.97, 52.75); - addTorch(131.50, 46.63); - addTorch(130.56, 50.42); - addTorch(134.09, 49.17); - addTorch(132.59, 53.79); - addTorch(126.63, 56.58); - addTorch(129.38, 54.63); - addTorch(151.78, 62.96); - addTorch(155.75, 63.33); - addTorch(149.00, 66.21); - addTorch(148.84, 70.75); - addTorch(153.31, 71.71); - addTorch(157.34, 68.79); - addTorch(157.75, 65.54); - addTorch(151.16, 69.21); - addTorch(75.31, 124.79); - addTorch(75.47, 128.88); - // cookie stands lights - addTorch(33.19 + 0.25, 26.29 + 9.5); - addTorch(65.78, 114.08); - addTorch(89.78, 132.17); - addTorch(124.78, 147.25); - addTorch(132.97, 150.29); - addTorch(144.13, 98.58); - addTorch(120.34, 48.38); - addTorch(144.62, 125.20); - - // top left hill - addTorch(22.40, 45.12); - addTorch(18.19, 40.75); - addTorch(21.13, 35.96); - addTorch(20.91, 30.63); - addTorch(14.22, 30.54); - addTorch(27.78, 30.83); - addTorch(30.16, 34.46); - addTorch(18.13, 24.42); - addTorch(16.31, 17.83); - addTorch(17.69, 13.46); - addTorch(20.88, 10.00); - addTorch(14.22, 10.92); - addTorch(21.13, 16.75); - addTorch(14.09, 22.88); - addTorch(28.03, 12.96); - addTorch(31.19, 8.17); - addTorch(36.38, 10.83); - addTorch(44.56, 8.33); - addTorch(51.28, 11.88); - addTorch(60.13, 9.33); - addTorch(62.91, 11.88); - addTorch(71.00, 8.25); - addTorch(76.28, 12.54); - addTorch(82.66, 9.58); - // center - addTorch(53.09, 71.08); - addTorch(60.78, 70.88); - addTorch(67.19, 76.38); - addTorch(60.53, 76.63); - addTorch(52.38, 76.79); - addTorch(48.13, 75.29); - addTorch(58.97, 55.75); - addTorch(53.75, 51.00); - addTorch(58.81, 42.42); - addTorch(53.97, 40.00); - addTorch(59.59, 37.00); - addTorch(53.34, 30.46); - addTorch(58.09, 30.29); - addTorch(69.84, 38.92); - addTorch(68.09, 41.92); - addTorch(79.78, 39.79); - addTorch(78.19, 43.83); - addTorch(92.19, 39.00); - addTorch(99.25, 39.21); - addTorch(99.19, 45.21); - addTorch(103.19, 36.58); - addTorch(104.63, 41.71); - addTorch(100.00, 58.58); - addTorch(92.22, 30.46); - addTorch(96.06, 28.88); - addTorch(90.88, 24.08); - addTorch(95.34, 22.50); - addTorch(90.81, 17.21); - addTorch(95.88, 15.83); - addTorch(96.28, 10.33); - addTorch(91.25, 8.83); - addTorch(94.34, 2.38); - addTorch(97.16, 6.71); - addTorch(88.78, 4.54); - addTorch(50.69, 20.42); - addTorch(59.38, 20.29); - addTorch(50.28, 29.21); - addTorch(48.72, 32.92); - addTorch(42.66, 36.50); - addTorch(38.19, 31.17); - addTorch(37.53, 37.88); - // top right - addTorch(115.50, 36.17); - addTorch(113.94, 40.13); - addTorch(109.72, 35.71); - addTorch(123.44, 37.00); - addTorch(131.59, 39.75); - addTorch(138.38, 38.13); - addTorch(137.66, 43.00); - addTorch(144.16, 36.17); - addTorch(148.09, 36.71); - addTorch(146.03, 41.00); - addTorch(150.97, 41.00); - addTorch(155.88, 37.67); - addTorch(152.56, 34.83); - addTorch(127.00, 17.88); - addTorch(127.88, 25.21); - addTorch(131.88, 15.21); - addTorch(135.78, 21.92); - addTorch(132.41, 28.33); - addTorch(122.81, 32.38); - addTorch(127.66, 33.33); - addTorch(141.53, 13.67); - addTorch(145.75, 13.75); - addTorch(141.59, 17.25); - addTorch(145.69, 17.71); - addTorch(135.72, 9.29); - addTorch(131.59, 7.63); - addTorch(129.69, 11.71); - // harbor - addTorch(22.50, 75.92); - addTorch(20.34, 70.58); - addTorch(28.66, 70.29); - addTorch(36.72, 70.88); - addTorch(32.41, 77.00); - addTorch(15.03, 67.63); - addTorch(20.03, 63.00); - addTorch(15.59, 60.63); - addTorch(16.88, 53.79); - addTorch(21.75, 54.21); - addTorch(15.47, 76.13); - // south-west forest - addTorch(36.84, 84.25); - addTorch(30.28, 89.29); - addTorch(29.84, 96.92); - addTorch(23.34, 98.54); - addTorch(21.59, 105.33); - addTorch(15.28, 107.83); - addTorch(14.81, 115.33); - addTorch(9.41, 120.79); - addTorch(15.59, 125.63); - addTorch(14.16, 135.13); - addTorch(14.97, 94.67); - addTorch(17.16, 90.21); - addTorch(22.25, 92.71); - - add(createSign(77.06, 60.16, 'Rose sign', give(entities.rose.type, `Here's your rose!`), entities.sign)); - - if (world.featureFlags.test) { - add(createSign(14.5, 70, 'Public Island', (_, client) => goToMap(world, client, 'public-island'), entities.signDebug)); - add(createSignWithText(60.7, 60.2, 'Pickable items', `Click on the item to carry it around`, entities.signDebug)); - add(entities.flower3Pickable(60, 60)).interact = (_, { pony }) => holdItem(pony, entities.flowerPick.type); - add(entities.apple(61, 61)).interact = (_, { pony }) => holdItem(pony, entities.apple.type); - add(entities.appleGreen2(61.3, 61.4)).interact = (_, { pony }) => holdItem(pony, entities.appleGreen2.type); - add(entities.orange(60.3, 61.2)).interact = (_, { pony }) => holdItem(pony, entities.orange.type); - add(entities.grapesPurple[0](60.67, 61.63)).interact = (_, { pony }) => holdItem(pony, entities.grapesPurple[0].type); - } - - if (BETA) { - const objects = [ - entities.fence1, entities.box, entities.boxLanterns, entities.gift3, entities.pumpkin, entities.sign, - entities.rope, - ].map(e => e.type); - - add(createSign(62.7, 58.2, 'Jack-o-Lanterns', give(entities.jackoLanternOn.type, 'Have a lantern'), entities.signDebug)); - add(createSign(62.7, 60.2, 'Pickable objects', (_, client) => holdItem(client.pony, sample(objects)!), entities.signDebug)); - add(createSign(77.0, 69.0, 'Palette', (_, client) => goToMap(world, client, 'palette'), entities.signDebug)); - } - - // new added on Halloween - addEntities(entities.tree4(155.18, 100.04, 1)); - addEntities(entities.tree4(144.97, 96.25, 0)); - addEntities(entities.tree(153.60, 99.54, 2)); - addEntities(entities.pine(157.50, 96.79, 0)); - add(entities.treeStump1(158.03, 110.12)); - add(entities.treeStump1(158.75, 112.91)); - add(entities.treeStump2(157.00, 111.50)); - - // center lakes - add(entities.waterRock1(77.66, 61.67)); - add(entities.waterRock7(76.56, 65.58)); - add(entities.waterRock8(76.97, 65.79)); - add(entities.waterRock10(83.47, 67.63)); - - // sea - add(entities.waterRock1(3.78, 61.96)); - add(entities.waterRock1(12.53, 75.88)); - add(entities.waterRock1(9.38, 80.50)); - add(entities.waterRock1(5.34, 95.42)); - add(entities.waterRock3(4.91, 95.71)); - add(entities.waterRock3(8.44, 85.79)); - add(entities.waterRock3(8.50, 64.00)); - add(entities.waterRock2(3.19, 61.42)); - add(entities.waterRock2(8.97, 86.00)); - add(entities.waterRock2(3.31, 100.88)); - add(entities.waterRock4(4.75, 95.21)); - add(entities.waterRock4(12.44, 77.58)); - add(entities.waterRock4(8.44, 63.54)); - add(entities.waterRock5(3.38, 61.96)); - add(entities.waterRock5(7.22, 90.79)); - add(entities.waterRock6(3.66, 100.25)); - add(entities.waterRock6(12.22, 78.04)); - add(entities.waterRock7(7.97, 63.88)); - add(entities.waterRock7(8.25, 86.38)); - add(entities.waterRock8(9.03, 80.83)); - add(entities.waterRock8(1.31, 101.71)); - add(entities.waterRock9(11.34, 79.17)); - add(entities.waterRock10(9.97, 66.79)); - add(entities.waterRock10(4.25, 94.38)); - add(entities.waterRock11(9.47, 66.88)); - add(entities.waterRock11(12.28, 70.71)); - add(entities.waterRock11(7.00, 91.25)); - - // river - add(entities.waterRock1(114.06, 1.96)); - add(entities.waterRock1(112.13, 17.92)); - add(entities.waterRock1(112.84, 28.38)); - add(entities.waterRock1(111.66, 35.75)); - add(entities.waterRock2(109.15, 16.50)); - add(entities.waterRock2(114.66, 32.50)); - add(entities.waterRock3(112.91, 20.33)); - add(entities.waterRock3(110.38, 9.75)); - add(entities.waterRock4(109.59, 16.75)); - add(entities.waterRock4(113.41, 7.96)); - add(entities.waterRock4(111.63, 35.13)); - add(entities.waterRock5(114.34, 33.04)); - add(entities.waterRock5(110.63, 9.38)); - add(entities.waterRock6(109.63, 16.58)); - add(entities.waterRock6(113.28, 28.58)); - add(entities.waterRock7(114.50, 1.54)); - add(entities.waterRock7(111.97, 35.38)); - add(entities.waterRock8(113.28, 28.17)); - add(entities.waterRock8(111.44, 22.79)); - add(entities.waterRock8(110.72, 9.63)); - add(entities.waterRock9(107.94, 12.75)); - add(entities.waterRock9(113.97, 6.58)); - add(entities.waterRock10(108.38, 12.63)); - add(entities.waterRock10(114.44, 33.46)); - add(entities.waterRock11(112.31, 18.25)); - add(entities.waterRock11(111.34, 12.25)); - add(entities.waterRock11(110.59, 39.75)); - - // river+lake - add(entities.waterRock1(107.75, 45.83)); - add(entities.waterRock1(105.63, 50.92)); - add(entities.waterRock1(118.56, 58.83)); - add(entities.waterRock1(127.59, 59.13)); - add(entities.waterRock1(131.50, 67.58)); - add(entities.waterRock1(124.25, 68.54)); - add(entities.waterRock2(117.44, 53.67)); - add(entities.waterRock2(128.41, 59.17)); - add(entities.waterRock2(105.06, 50.30)); - add(entities.waterRock2(111.56, 43.54)); - add(entities.waterRock3(112.69, 56.08)); - add(entities.waterRock3(105.53, 50.29)); - add(entities.waterRock3(127.88, 59.46)); - add(entities.waterRock3(134.09, 57.88)); - add(entities.waterRock4(111.38, 43.21)); - add(entities.waterRock4(112.97, 55.83)); - add(entities.waterRock4(124.66, 68.88)); - add(entities.waterRock5(110.47, 52.58)); - add(entities.waterRock5(118.16, 58.54)); - add(entities.waterRock5(132.63, 70.75)); - add(entities.waterRock6(108.03, 46.17)); - add(entities.waterRock6(116.94, 54.04)); - add(entities.waterRock6(129.66, 55.92)); - add(entities.waterRock7(109.91, 52.46)); - add(entities.waterRock7(120.31, 53.54)); - add(entities.waterRock7(133.09, 71.08)); - add(entities.waterRock8(132.59, 71.13)); - add(entities.waterRock8(110.22, 52.04)); - add(entities.waterRock8(111.63, 43.00)); - add(entities.waterRock8(110.81, 39.88)); - add(entities.waterRock9(109.81, 48.83)); - add(entities.waterRock10(120.41, 54.08)); - add(entities.waterRock10(131.13, 67.96)); - add(entities.waterRock11(121.75, 64.25)); - add(entities.waterRock10(121.31, 63.60)); - add(entities.waterRock1(121.13, 75.83)); - add(entities.waterRock1(125.47, 78.67)); - add(entities.waterRock1(140.31, 78.96)); - add(entities.waterRock1(146.44, 71.42)); - add(entities.waterRock1(143.47, 50.71)); - add(entities.waterRock1(142.00, 63.75)); - add(entities.waterRock2(139.69, 67.21)); - add(entities.waterRock2(147.75, 61.83)); - add(entities.waterRock2(125.94, 78.92)); - add(entities.waterRock2(140.81, 78.46)); - add(entities.waterRock3(140.06, 67.67)); - add(entities.waterRock3(145.31, 74.92)); - add(entities.waterRock3(137.53, 58.75)); - add(entities.waterRock3(143.84, 50.88)); - add(entities.waterRock3(121.63, 75.42)); - add(entities.waterRock3(133.47, 82.50)); - add(entities.waterRock4(125.94, 78.46)); - add(entities.waterRock4(148.03, 62.17)); - add(entities.waterRock4(143.91, 50.42)); - add(entities.waterRock5(137.81, 59.13)); - add(entities.waterRock5(142.47, 64.04)); - add(entities.waterRock5(146.13, 71.96)); - add(entities.waterRock5(129.16, 80.54)); - add(entities.waterRock5(146.44, 52.88)); - add(entities.waterRock6(140.38, 78.25)); - add(entities.waterRock6(133.06, 82.21)); - add(entities.waterRock6(139.31, 62.96)); - add(entities.waterRock6(138.19, 69.79)); - add(entities.waterRock6(147.38, 65.83)); - add(entities.waterRock7(129.63, 80.79)); - add(entities.waterRock7(145.56, 74.54)); - add(entities.waterRock7(146.88, 52.67)); - add(entities.waterRock8(140.09, 67.21)); - add(entities.waterRock10(133.41, 82.04)); - add(entities.waterRock11(147.66, 62.33)); - add(entities.waterRock8(145.97, 71.42)); - add(entities.waterRock8(121.56, 75.92)); - add(entities.waterRock10(122.38, 73.54)); - add(entities.waterRock8(129.53, 81.46)); - add(entities.waterRock11(136.40, 80.92)); - add(entities.waterRock8(136.88, 80.54)); - add(entities.waterRock11(144.94, 58.71)); - add(entities.waterRock8(133.75, 57.75)); - add(entities.waterRock6(126.63, 80.29)); - add(entities.waterRock1(128.59, 74.63)); - add(entities.waterRock3(129.47, 74.33)); - add(entities.waterRock6(129.16, 74.83)); - add(entities.waterRock8(128.75, 75.00)); - add(entities.waterRock7(140.59, 73.54)); - add(entities.waterRock10(140.94, 73.67)); - add(entities.waterRock8(140.59, 74.08)); - - // pine forest lakes - add(entities.waterRock1(34.81, 118.79)); - add(entities.waterRock1(31.38, 115.79)); - add(entities.waterRock1(51.63, 111.79)); - add(entities.waterRock2(62.38, 111.75)); - add(entities.waterRock2(49.63, 112.83)); - add(entities.waterRock2(31.72, 115.25)); - add(entities.waterRock2(36.69, 109.75)); - add(entities.waterRock3(34.56, 119.04)); - add(entities.waterRock3(32.63, 122.50)); - add(entities.waterRock3(52.03, 112.08)); - add(entities.waterRock4(62.59, 111.38)); - add(entities.waterRock4(51.59, 112.17)); - add(entities.waterRock4(31.91, 115.54)); - add(entities.waterRock5(34.28, 118.46)); - add(entities.waterRock5(33.44, 112.67)); - add(entities.waterRock6(30.66, 118.25)); - add(entities.waterRock6(55.28, 114.13)); - add(entities.waterRock7(33.75, 112.21)); - add(entities.waterRock7(29.94, 116.67)); - add(entities.waterRock7(49.78, 113.29)); - add(entities.waterRock8(49.97, 112.88)); - add(entities.waterRock8(32.72, 122.96)); - add(entities.waterRock9(53.41, 111.63)); - add(entities.waterRock10(64.34, 110.96)); - add(entities.waterRock10(36.91, 110.13)); - add(entities.waterRock10(30.44, 116.50)); - add(entities.waterRock8(30.31, 116.79)); - add(entities.waterRock10(32.94, 122.67)); - - // bottom lake - add(entities.waterRock1(95.34, 148.83)); - add(entities.waterRock2(98.56, 150.54)); - add(entities.waterRock3(95.44, 151.63)); - add(entities.waterRock4(95.75, 151.58)); - add(entities.waterRock5(92.44, 150.67)); - add(entities.waterRock6(92.72, 150.92)); - add(entities.waterRock8(92.75, 150.50)); - add(entities.waterRock9(98.81, 150.96)); - add(entities.waterRock10(98.41, 150.92)); - add(entities.waterRock4(95.03, 149.00)); - - // forest puddle - add(entities.waterRock5(140.44, 122.71)); - add(entities.waterRock4(140.13, 122.67)); - - add(entities.bench1(18.69, 88.50)); - add(entities.bench1(27.94, 104.21)); - add(entities.bench1(22.56, 70.50)); - add(entities.bench1(25.28, 70.46)); - add(entities.bench1(44.16, 31.46)); - add(entities.bench1(46.84, 31.50)); - add(entities.bench1(30.03, 29.46)); - add(entities.bench1(87.00, 3.33)); - add(entities.bench1(101.91, 8.25)); - add(entities.bench1(142.13, 11.54)); - add(entities.bench1(144.84, 11.58)); - add(entities.benchSeat(142.00, 15.88)); - add(entities.benchSeat(144.78, 15.83)); - add(entities.benchBack(142.00, 16.7917)); - add(entities.benchBack(144.78, 16.71)); - add(entities.benchSeat(45.78, 36.42)); - add(entities.benchBack(45.78, 37.33)); - add(entities.bench1(93.59, 146.33)); - add(entities.bench1(144.13, 138.75)); - add(entities.bench1(146.66, 138.83)); - add(entities.bench1(133.22, 150.71)); - add(entities.benchSeat(133.13, 153.71)); - add(entities.benchBack(133.13, 154.63)); - add(entities.bench1(117.28, 62.13)); - add(entities.benchSeat(117.25, 65.13)); - add(entities.benchBack(117.25, 66.04)); - add(entities.bench1(123.81, 53.08)); - add(entities.bench1(127.09, 53.04)); - add(entities.lanternOn(117.09, 64.08)); - add(entities.lanternOn(20.34, 89.04)); - add(entities.benchSeatH(147.14, 13.02)); - add(entities.benchBackH2(147.66, 15.70)); - add(entities.benchSeatH(27.44, 117.21)); - add(entities.benchBackH(26.97, 119.92)); - add(entities.benchSeatH(136.63, 151.29)); - add(entities.benchSeatH(129.34, 151.25)); - add(entities.benchBackH(128.85, 153.96)); - add(entities.benchBackH2(137.16, 153.96)); - - updateMainMapSeason(world, map, world.season, world.holiday); - - addEntities(createBunny([ - point(72.34, 56.79), - point(70.69, 56.08), - point(69.56, 55.67), - point(68.03, 57.67), - point(70.28, 57.96), - point(71.28, 58.79), - point(70.00, 59.54), - point(68.72, 58.67), - point(67.22, 59.96), - point(65.38, 61.17), - point(65.31, 62.50), - point(64.25, 64.25), - point(65.69, 66.00), - point(68.47, 69.50), - point(69.59, 69.79), - point(69.56, 68.21), - point(71.06, 68.79), - point(72.94, 64.67), - point(74.31, 63.13), - point(74.41, 60.79), - point(75.50, 60.13), - point(76.03, 57.75), - point(78.19, 54.42), - point(77.47, 52.75), - point(78.38, 50.46), - point(79.03, 51.67), - point(80.50, 50.96), - point(81.69, 50.42), - point(82.75, 51.54), - point(83.75, 51.96), - point(83.75, 52.96), - point(86.13, 54.25), - point(87.28, 54.04), - point(87.59, 55.13), - point(86.31, 55.50), - point(84.22, 56.38), - point(82.06, 56.25), - point(81.47, 54.71), - point(80.84, 54.71), - point(81.41, 56.13), - point(82.19, 59.58), - point(84.03, 62.25), - point(86.44, 63.92), - point(88.00, 63.21), - point(87.16, 64.29), - point(85.78, 65.13), - point(82.50, 65.42), - point(81.47, 66.38), - point(80.25, 66.58), - point(79.47, 68.04), - point(77.84, 68.04), - point(75.91, 67.92), - point(75.16, 66.58), - point(74.72, 64.96), - point(73.16, 63.54), - point(71.97, 62.25), - point(72.56, 60.79), - point(71.16, 59.83), - point(72.19, 58.17), - ])); - - addEntities(createBunny([ - point(111.84, 63.92), - point(114.81, 64.79), - point(111.50, 67.54), - point(115.94, 69.58), - point(112.09, 75.42), - point(105.78, 79.75), - point(111.13, 80.96), - point(117.09, 80.29), - point(120.88, 87.38), - point(124.22, 91.50), - point(128.81, 88.71), - point(125.81, 85.21), - point(131.44, 91.17), - point(129.19, 95.38), - point(132.38, 96.21), - point(137.06, 96.50), - point(142.28, 95.75), - point(140.19, 89.46), - point(145.56, 87.33), - point(150.13, 89.83), - point(151.75, 84.92), - point(148.28, 82.29), - point(150.13, 80.04), - point(153.78, 79.17), - point(150.44, 70.71), - point(153.19, 67.96), - point(152.38, 65.08), - point(154.38, 62.63), - point(152.44, 58.17), - point(148.56, 57.00), - point(149.56, 53.58), - point(152.41, 51.21), - point(151.03, 47.79), - point(148.66, 45.25), - point(143.31, 46.46), - point(140.75, 49.58), - point(141.78, 54.04), - point(140.94, 56.08), - point(139.19, 57.33), - point(137.41, 55.13), - point(134.91, 55.88), - point(131.03, 52.08), - point(128.63, 48.67), - point(124.88, 49.25), - point(124.56, 44.46), - point(122.47, 45.83), - point(121.50, 42.83), - point(117.72, 45.63), - point(115.34, 46.13), - point(114.97, 43.25), - point(115.94, 40.38), - point(114.19, 37.83), - point(108.16, 37.75), - point(108.66, 40.25), - point(108.53, 41.63), - point(107.59, 42.67), - point(101.91, 44.00), - point(102.09, 46.67), - point(100.34, 46.08), - point(100.56, 48.25), - point(102.28, 51.83), - point(105.06, 52.92), - point(104.75, 55.75), - point(108.25, 55.67), - point(106.88, 57.92), - point(106.97, 62.38), - point(111.09, 60.42), - point(112.84, 62.63), - ])); - - addEntities(createBunny([ - point(85.00, 88.54), - point(81.91, 89.75), - point(77.06, 88.46), - point(75.75, 90.58), - point(78.06, 92.46), - point(76.88, 92.96), - point(78.59, 94.13), - point(76.72, 95.29), - point(79.94, 95.71), - point(82.34, 93.63), - point(85.81, 95.04), - point(88.88, 93.58), - point(92.66, 94.04), - point(95.06, 92.21), - point(98.00, 92.96), - point(98.25, 94.92), - point(98.13, 100.46), - point(96.94, 102.71), - point(96.09, 104.46), - point(98.00, 106.46), - point(100.41, 103.96), - point(102.25, 107.50), - point(103.97, 109.42), - point(105.66, 108.04), - point(107.19, 106.13), - point(107.69, 102.42), - point(108.78, 101.96), - point(109.88, 104.42), - point(108.53, 106.38), - point(111.22, 109.50), - point(114.34, 109.58), - point(114.75, 112.63), - point(114.28, 116.50), - point(117.38, 118.50), - point(117.88, 123.04), - point(119.38, 124.38), - point(119.25, 127.21), - point(117.72, 129.13), - point(114.41, 133.63), - point(115.28, 134.75), - point(115.03, 137.63), - point(117.81, 145.83), - point(122.03, 148.08), - point(120.56, 151.42), - point(119.28, 154.29), - point(121.28, 154.71), - point(123.44, 152.13), - point(123.34, 150.17), - point(126.38, 150.25), - point(125.91, 154.08), - point(124.91, 155.33), - point(131.81, 157.13), - point(139.19, 156.83), - point(144.88, 153.63), - point(147.00, 150.38), - point(145.63, 149.13), - point(150.34, 148.50), - point(150.78, 151.04), - point(149.28, 154.17), - point(145.00, 151.04), - point(150.50, 145.29), - point(150.56, 138.17), - point(153.09, 136.33), - point(156.03, 136.29), - point(154.78, 133.42), - point(151.63, 134.08), - point(148.56, 133.21), - point(146.16, 129.38), - point(146.34, 124.08), - point(148.03, 119.42), - point(145.09, 113.92), - point(142.81, 110.83), - point(140.09, 109.42), - point(135.94, 105.75), - point(138.91, 98.96), - point(141.97, 94.92), - point(143.66, 92.50), - point(141.00, 91.29), - point(144.03, 89.58), - point(141.38, 87.79), - point(138.75, 90.54), - point(134.84, 89.50), - point(131.41, 88.00), - point(131.38, 90.63), - point(128.03, 88.46), - point(125.25, 88.08), - point(125.50, 84.13), - point(122.72, 84.42), - point(119.22, 87.17), - point(113.50, 83.25), - point(110.56, 80.33), - point(105.22, 79.96), - point(102.34, 77.04), - point(100.69, 78.54), - point(98.09, 78.04), - point(99.97, 75.92), - point(95.53, 76.25), - point(94.31, 78.00), - point(91.00, 75.92), - point(86.34, 77.46), - point(87.31, 80.75), - point(86.19, 83.83), - point(89.47, 84.04), - point(86.28, 86.33), - ])); - - addEntities(createBunny([ - point(60.94, 119.21), - point(60.44, 116.92), - point(57.34, 117.92), - point(58.25, 114.08), - point(57.41, 111.25), - point(55.63, 109.79), - point(53.94, 104.42), - point(51.97, 105.04), - point(53.31, 101.04), - point(51.34, 96.96), - point(49.16, 96.54), - point(43.22, 97.79), - point(43.47, 99.42), - point(40.50, 99.17), - point(41.00, 96.54), - point(38.16, 98.25), - point(33.91, 98.88), - point(30.03, 101.04), - point(29.25, 102.17), - point(26.34, 102.71), - point(24.63, 102.96), - point(23.81, 106.83), - point(21.50, 108.38), - point(22.63, 110.17), - point(20.72, 111.79), - point(23.06, 113.33), - point(24.44, 117.58), - point(25.19, 120.79), - point(28.19, 121.92), - point(26.88, 123.79), - point(32.19, 126.29), - point(31.97, 128.25), - point(34.28, 130.54), - point(35.91, 130.00), - point(38.59, 129.88), - point(37.25, 125.92), - point(40.75, 123.96), - point(42.25, 128.00), - point(45.91, 129.33), - point(46.59, 132.08), - point(48.53, 129.29), - point(52.03, 126.50), - point(47.31, 122.00), - point(50.09, 118.42), - point(52.94, 119.54), - point(55.59, 119.38), - ])); - - addEntities(createBunny([ - point(65.28, 127.13), - point(62.63, 129.54), - point(67.25, 130.13), - point(64.53, 130.38), - point(62.81, 133.17), - point(67.31, 133.25), - point(69.09, 134.88), - point(64.97, 136.25), - point(67.03, 139.29), - point(64.94, 141.13), - point(64.66, 143.71), - point(66.75, 144.08), - point(67.56, 141.71), - point(69.50, 142.04), - point(68.16, 144.79), - point(66.25, 147.50), - point(64.25, 146.58), - point(62.41, 148.42), - point(59.22, 150.04), - point(59.88, 151.33), - point(58.94, 153.00), - point(57.66, 151.92), - point(55.47, 149.42), - point(55.91, 147.79), - point(53.06, 147.25), - point(50.38, 144.21), - point(47.00, 145.54), - point(44.06, 145.33), - point(42.84, 143.79), - point(43.78, 142.50), - point(42.38, 140.29), - point(44.84, 138.46), - point(45.84, 133.29), - point(48.72, 132.13), - point(50.16, 128.92), - point(52.63, 129.42), - point(54.78, 127.42), - point(56.31, 129.00), - point(56.22, 130.42), - point(59.56, 130.38), - ])); - - addEntities(createBunny([ - point(62.75, 128.29), - point(62.50, 130.54), - point(66.16, 128.46), - point(67.94, 129.42), - point(67.47, 133.38), - point(63.84, 132.25), - point(65.50, 130.67), - point(67.78, 136.50), - point(66.28, 139.96), - point(64.69, 141.96), - point(65.88, 146.08), - point(68.63, 143.75), - point(65.88, 143.08), - point(68.34, 141.58), - point(68.00, 140.38), - point(67.41, 144.13), - point(67.22, 139.92), - point(67.41, 134.38), - ])); - - addEntities(createBunny([ - point(66.56, 130.17), - point(67.41, 128.33), - point(66.25, 127.21), - point(64.41, 128.00), - point(62.97, 128.46), - point(63.53, 130.92), - point(62.16, 131.96), - point(64.66, 133.42), - point(67.34, 134.13), - point(68.25, 134.83), - point(64.47, 137.50), - point(65.09, 139.58), - point(64.59, 142.83), - point(66.56, 144.63), - point(67.75, 141.71), - point(69.63, 142.79), - point(68.34, 144.83), - point(63.63, 146.50), - point(62.03, 148.67), - point(58.63, 149.83), - point(58.78, 152.63), - point(60.06, 150.38), - point(62.19, 146.71), - point(65.66, 146.75), - point(69.34, 144.58), - point(71.59, 143.54), - point(72.19, 141.54), - point(68.09, 141.21), - point(66.75, 138.88), - point(67.09, 135.96), - point(63.63, 133.46), - ])); - - addEntities(createBunny([ - point(77.28, 149.21), - point(77.44, 150.83), - point(79.34, 151.38), - point(77.00, 153.88), - point(82.84, 153.00), - point(83.47, 155.21), - point(86.59, 154.17), - point(85.94, 151.04), - point(84.69, 149.54), - point(85.44, 147.92), - point(81.41, 149.38), - ])); - - addEntities(createBunny([ - point(77.59, 149.08), - point(78.03, 152.38), - point(76.63, 152.13), - point(76.81, 154.54), - point(79.34, 153.75), - point(81.22, 154.71), - point(83.19, 154.54), - point(84.56, 152.42), - point(81.09, 151.25), - point(81.94, 149.46), - point(80.09, 149.46), - point(84.00, 153.17), - point(85.91, 149.29), - point(86.34, 153.42), - ])); - - addEntities(createBunny([ - point(86.09, 148.13), - point(84.44, 149.13), - point(86.25, 152.13), - point(86.78, 150.29), - point(84.63, 151.46), - point(86.00, 153.79), - point(84.03, 154.88), - point(81.81, 152.25), - point(82.19, 150.88), - point(80.19, 149.42), - point(78.09, 149.42), - point(77.72, 152.25), - point(80.28, 152.54), - point(77.63, 154.33), - point(79.22, 155.21), - point(82.78, 153.58), - point(83.75, 150.38), - point(82.97, 149.04), - ])); - - addEntities(createBunny([ - point(86.09, 148.13), - point(84.44, 149.13), - point(86.25, 152.13), - point(86.78, 150.29), - point(84.63, 151.46), - point(86.00, 153.79), - point(84.03, 154.88), - point(81.81, 152.25), - point(82.19, 150.88), - point(80.19, 149.42), - point(78.09, 149.42), - point(77.72, 152.25), - point(80.28, 152.54), - point(77.63, 154.33), - point(79.22, 155.21), - point(82.78, 153.58), - point(83.75, 150.38), - point(82.97, 149.04), - point(9.09, 102.54), - point(6.84, 104.00), - point(8.75, 105.00), - point(6.94, 106.58), - point(8.59, 110.33), - point(7.56, 112.79), - point(6.00, 114.46), - point(4.63, 113.46), - point(2.34, 113.75), - point(1.19, 116.25), - point(2.50, 117.54), - point(5.00, 115.92), - point(6.16, 119.08), - point(7.44, 123.25), - point(6.53, 124.42), - point(7.13, 128.13), - point(6.00, 131.71), - point(7.56, 136.38), - point(10.88, 139.00), - point(10.91, 140.54), - point(12.69, 141.08), - point(14.06, 141.92), - point(16.09, 141.42), - point(14.59, 140.08), - point(14.78, 137.63), - point(12.34, 137.79), - point(10.38, 138.25), - point(7.97, 136.75), - point(6.69, 131.33), - point(8.56, 127.42), - point(7.25, 123.58), - point(7.00, 119.13), - point(4.34, 117.67), - point(3.53, 114.75), - point(4.75, 114.58), - point(6.34, 115.63), - point(7.19, 113.21), - point(7.81, 108.46), - point(9.22, 106.58), - point(11.47, 104.83), - point(11.63, 102.63), - point(10.34, 103.17), - ])); - - addEntities(createBunny([ - point(21.72, 63.50), - point(23.09, 65.46), - point(24.53, 63.54), - point(25.59, 65.13), - point(25.28, 66.71), - point(27.69, 67.92), - point(30.22, 67.58), - point(32.94, 67.79), - point(36.38, 68.92), - point(37.16, 67.83), - point(35.88, 65.79), - point(38.34, 65.25), - point(39.50, 67.38), - point(40.19, 64.96), - point(39.34, 62.88), - point(37.28, 64.04), - point(34.44, 62.67), - point(32.44, 61.17), - point(32.53, 59.63), - point(29.41, 58.88), - point(29.22, 61.79), - point(26.50, 61.83), - point(24.53, 61.58), - point(22.88, 61.50), - point(23.00, 63.38), - point(25.78, 62.88), - ])); - - addEntities(createBunny([ - point(21.38, 28.04), - point(19.81, 26.08), - point(21.22, 23.75), - point(23.19, 25.29), - point(25.09, 22.92), - point(25.81, 25.79), - point(26.56, 22.83), - point(28.31, 19.92), - point(29.41, 16.79), - point(31.00, 15.25), - point(32.41, 16.83), - point(32.72, 18.38), - point(34.63, 16.67), - point(33.53, 15.50), - point(38.81, 13.92), - point(41.16, 14.88), - point(43.09, 13.96), - point(46.19, 12.46), - point(45.22, 7.96), - point(46.31, 5.96), - point(45.34, 5.08), - point(47.00, 4.50), - point(47.41, 5.58), - point(55.22, 4.75), - point(56.09, 7.83), - point(57.91, 8.00), - point(60.94, 7.58), - point(62.13, 6.50), - point(63.09, 7.38), - point(64.34, 6.42), - point(65.09, 6.96), - point(67.06, 11.17), - point(65.78, 12.29), - point(67.66, 13.33), - point(70.09, 12.71), - point(71.13, 14.67), - point(70.22, 16.04), - point(73.09, 16.75), - point(74.78, 14.00), - point(74.47, 17.83), - point(73.19, 21.04), - point(74.38, 21.50), - point(77.09, 20.13), - point(77.72, 19.17), - point(79.22, 20.88), - point(80.19, 22.88), - point(81.84, 22.71), - point(82.47, 21.67), - point(82.50, 20.08), - point(81.47, 19.25), - point(81.00, 17.58), - point(82.47, 16.08), - point(80.63, 15.42), - point(78.59, 15.88), - point(78.50, 14.38), - point(79.78, 13.00), - point(81.81, 12.54), - point(83.19, 12.29), - point(84.22, 10.38), - point(87.00, 10.83), - point(87.91, 10.08), - point(89.41, 11.38), - point(90.38, 8.83), - point(91.91, 6.96), - point(97.44, 8.38), - point(99.56, 9.46), - point(98.50, 10.83), - point(97.28, 10.46), - point(97.72, 7.42), - point(95.28, 7.83), - point(95.06, 11.75), - point(94.84, 16.21), - point(96.50, 17.17), - point(98.66, 17.71), - point(99.00, 19.54), - point(97.06, 20.92), - point(95.50, 20.63), - point(90.22, 21.67), - point(89.28, 24.21), - point(89.22, 26.50), - point(88.66, 26.50), - point(88.38, 28.63), - point(85.84, 28.46), - point(85.31, 30.67), - point(83.59, 31.71), - point(81.63, 31.25), - point(79.53, 31.88), - point(73.91, 31.63), - point(71.19, 31.08), - point(69.97, 32.08), - point(66.94, 31.63), - point(65.47, 34.58), - point(62.72, 36.08), - point(60.00, 34.92), - point(57.09, 32.92), - point(52.00, 32.50), - point(52.59, 36.88), - point(50.75, 39.54), - point(48.63, 39.33), - point(48.72, 41.38), - point(47.03, 40.46), - point(43.28, 41.63), - point(40.88, 44.96), - point(35.88, 44.00), - point(32.72, 46.67), - point(32.13, 50.00), - point(33.47, 52.88), - point(32.91, 54.71), - point(30.38, 55.67), - point(28.06, 54.71), - point(25.66, 53.38), - point(24.03, 54.21), - point(22.78, 53.50), - point(21.19, 56.54), - point(18.25, 56.83), - point(15.22, 57.00), - point(13.69, 55.83), - point(14.88, 55.08), - point(16.47, 55.33), - point(18.31, 53.42), - point(19.25, 51.54), - point(19.38, 49.29), - point(21.84, 44.83), - point(22.34, 43.04), - point(24.00, 43.04), - point(25.31, 44.71), - point(25.03, 45.83), - point(26.38, 46.88), - point(27.28, 45.67), - point(26.94, 43.83), - point(26.88, 41.58), - point(28.31, 39.96), - point(27.81, 37.63), - point(25.72, 35.71), - point(24.16, 35.54), - point(22.50, 30.58), - point(23.81, 29.88), - point(24.13, 27.54), - ])); - - addEntities(createBunny([ - point(133.75, 6.38), - point(131.81, 5.29), - point(128.88, 5.42), - point(128.69, 7.00), - point(131.72, 8.96), - point(133.38, 8.13), - point(133.09, 10.63), - point(129.16, 9.33), - point(128.84, 8.25), - point(125.84, 9.96), - point(124.91, 8.96), - point(122.41, 10.83), - point(119.44, 10.46), - point(117.00, 10.25), - point(115.16, 10.67), - point(114.88, 11.58), - point(113.97, 13.88), - point(114.66, 15.33), - point(114.34, 16.33), - point(115.09, 17.75), - point(115.41, 19.83), - point(115.47, 24.79), - point(116.84, 25.92), - point(117.34, 28.17), - point(120.56, 29.50), - point(122.09, 30.50), - point(121.28, 32.08), - point(123.25, 29.92), - point(126.31, 29.46), - point(128.69, 33.00), - point(131.22, 32.67), - point(132.50, 31.25), - point(134.47, 32.13), - point(135.31, 29.88), - point(136.41, 29.75), - point(136.25, 28.25), - point(134.59, 27.75), - point(135.84, 25.38), - point(136.22, 23.29), - point(137.22, 23.25), - point(137.78, 20.25), - point(138.69, 19.29), - point(137.34, 17.00), - point(137.31, 14.21), - point(134.41, 13.25), - point(131.91, 11.83), - point(132.34, 9.88), - ])); - - if (true) { - const toLake = { icon: SignIcon.Lake, name: 'Lake' }; - const toHarbor = { icon: SignIcon.Boat, name: 'Harbor' }; - const toSpawn = { icon: SignIcon.Spawn, name: 'Spawn' }; - const toTownCenter = { icon: SignIcon.TownCenter, name: 'Town Center' }; - const toPineForest = { icon: SignIcon.PineForest, name: 'Pine Forest' }; - const toPartyIsland = { icon: SignIcon.Boat, name: 'Party Island' }; - const toGiftPile = { icon: SignIcon.GiftPile, name: 'Gift Pile' }; - const toMountains = { icon: SignIcon.Mountains, name: 'Mountains' }; - const toForest = { icon: SignIcon.Forest, name: 'Forest' }; - const toPumpkinFarm = { icon: SignIcon.Pumpkins, name: 'Pumpkin Farm' }; - const toFlowerField = { icon: SignIcon.Fields, name: 'Flower Field' }; - const toBarrelStorage = { icon: SignIcon.Barrels, name: 'Barrel Storage' }; - const toMines = { icon: SignIcon.Mines, name: 'Mines' }; - const toBridge = { icon: SignIcon.Bridge, name: 'Bridge' }; - const toCarrots = { icon: SignIcon.Carrots, name: 'Carrot farm' }; - - addEntities(createDirectionSign(77, 72, { - w: [toSpawn, toGiftPile, toHarbor, toPineForest, undefined], - e: [toLake, toCarrots, toMines, toBarrelStorage], - s: [toForest, toPumpkinFarm], - })); - - addEntities(createDirectionSign(54.33, 70.58, { - r: 1, - n: [toSpawn, toMines], - w: [toPineForest, toHarbor, toMountains], - e: [toTownCenter, toLake], - })); - - addEntities(createDirectionSign(36.00, 75.98, { - w: [toHarbor, toMountains], - e: [toSpawn, toTownCenter, toMines, toLake], - s: [toPineForest], - })); - - addEntities(createDirectionSign(19.34, 71.00, { - n: [toMountains], - w: [undefined, toPartyIsland], - e: [toSpawn, toTownCenter, toPineForest], - })); - - addEntities(createDirectionSign(24.86, 9.98, { - r: 1, - e: [toBridge, toMines, toLake], - s: [toHarbor, toPineForest], - })); - - addEntities(createDirectionSign(58.66, 54.88, { - w: [toGiftPile], - })); - - addEntities(createDirectionSign(54.38, 39.29, { - r: 1, - n: [toSpawn], - e: [toMines, toBridge, toLake], - s: [toTownCenter, toHarbor], - })); - - addEntities(createDirectionSign(99.00, 40.15, { - n: [toMountains, toBarrelStorage], - w: [toSpawn, toMines, toHarbor], - e: [toBridge, toCarrots], - s: [toLake, toTownCenter, toForest], - })); - - addEntities(createDirectionSign(122.75, 37.00, { - r: 1, - w: [toTownCenter, toSpawn, toMines], - n: [toCarrots], - })); - - addEntities(createDirectionSign(103.75, 70.10, { - r: 1, - n: [toBridge, toMountains, toCarrots], - w: [toTownCenter, toHarbor], - e: [toLake, toForest], - })); - - addEntities(createDirectionSign(128.16, 102.13, { - w: [toSpawn, toTownCenter, toHarbor], - e: [toLake], - s: [toFlowerField], - })); - - addEntities(createDirectionSign(129.53, 140.75, { - w: [toPumpkinFarm, toPineForest, toHarbor], - e: [toFlowerField], - n: [toForest, toLake, toTownCenter], - })); - - addEntities(createDirectionSign(70.98, 135.85, { - r: 1, - n: [toSpawn, toTownCenter, toHarbor, toMines], - w: [undefined, toPineForest], - e: [toForest, undefined, toFlowerField], - })); - - addEntities(createDirectionSign(54.91, 7.92, { - w: [toHarbor, toPineForest], - e: [toBridge, toMines, toLake], - })); - - addEntities(createDirectionSign(90.17, 5.35, { - w: [toHarbor, toPineForest], - s: [toBridge, toMines, toLake], - })); - - addEntities(createDirectionSign(78.41, 96.46, { - w: [toTownCenter, toHarbor], - e: [toForest, toLake], - s: [toPumpkinFarm, toPineForest], - })); - - addEntities(createDirectionSign(95.84, 25.33, { - e: [toBarrelStorage], - })); - - addEntities(createDirectionSign(77.15, 39.20, { - n: [toMines], - w: [undefined, toSpawn], - e: [toBridge, toCarrots, toLake], - })); - - addEntities(createDirectionSign(106.80, 95.46, { - r: 1, - w: [toTownCenter, toPumpkinFarm, toHarbor], - n: [toLake, toMines, toCarrots], - e: [undefined, toFlowerField], - })); - - addEntities(createDirectionSign(17.67, 138.90, { - n: [toHarbor, toMountains, toTownCenter], - })); - } - - const apples = [entities.apple, entities.apple2, entities.apple, entities.apple2, entities.appleGreen, entities.appleGreen2]; - const otherFruits = [entities.orange, entities.orange2, entities.pear, entities.banana]; - - const ctrls = map.controllers; - - ctrls.push(new ctrl.UpdateController(map)); - ctrls.push(new ctrl.TorchController(world, map)); - ctrls.push(new ctrl.CloudController(world, map, 5)); - ctrls.push(new ctrl.CollectableController(world, map, apples, 8, pickEntity, checkNotCollecting)); - ctrls.push(new ctrl.CollectableController(world, map, otherFruits, 3, pickEntity, checkNotCollecting)); - - ctrls.push(new ctrl.CollectableController( - world, map, [entities.gift1, entities.gift2], 50, pickGift, undefined, undefined, undefined, - () => world.holiday === Holiday.Christmas)); - - ctrls.push(new ctrl.CollectableController( - world, map, [entities.candy], 60, pickCandy, checkLantern, undefined, undefined, - () => world.holiday === Holiday.Halloween)); - - ctrls.push(new ctrl.CollectableController( - world, map, entities.eggs, 200, pickEgg, checkBasket, 5, undefined, - () => world.holiday === Holiday.Easter)); - - ctrls.push(new ctrl.CollectableController( - world, map, [entities.fourLeafClover], 2, pickClover, checkNotCollecting, 1, positionClover, - () => world.season === Season.Spring || world.season === Season.Summer)); - - ctrls.push(new ctrl.PlantController(world, map, { - area: rect(116.2, 14.2, 7.8, 9.6), - count: 100, - stages: [ - [entities.carrot4], - [entities.carrot3], - [entities.carrot2, entities.carrot2b], - [entities.carrot1, entities.carrot1b], - ], - growOnlyOn: TileType.Dirt, - onPick: (_, client) => holdItem(client.pony, entities.carrotHeld.type), - isActive: () => world.season !== Season.Winter, - })); - - if (!DEVELOPMENT) { - ctrls.push(new ctrl.FlyingCritterController( - world, map, entities.bat, 2, 20, () => isNightTime(world.time))); - ctrls.push(new ctrl.FlyingCritterController( - world, map, entities.firefly, 1, 40, () => world.season !== Season.Winter && isNightTime(world.time))); - ctrls.push(new ctrl.FlyingCritterController( - world, map, entities.butterfly, 1.5, 40, () => world.season !== Season.Winter && isDayTime(world.time))); - } - - if (BETA) { - ctrls.push(new ctrl.WallController(world, map, entities.woodenWalls)); - } - - return map; + map.spawnArea = rect(51, 21, 8, 8); + + map.spawns.set('harbor', rect(5.2, 72.2, 3.4, 2.6)); + map.spawns.set('cave', rect(75.5, 27, 2, 2)); + + map.spawns.set('lake', rect(134, 68, 2, 1)); + map.spawns.set('bridge', rect(107, 37, 2, 2)); + map.spawns.set('forest', rect(105, 91, 3, 3)); + map.spawns.set('graveyard', rect(146, 101, 3, 3)); + map.spawns.set('pumpkins', rect(71, 125, 3, 3)); + + map.spawns.set('center', rect(74, 74, 2, 2)); + map.spawns.set('topleft', rect(17, 10, 3, 3)); + map.spawns.set('topright', rect(131, 17, 3, 3)); + map.spawns.set('bottomleft', rect(17, 149, 3, 3)); + map.spawns.set('bottomright', rect(154, 140, 3, 3)); + + // tiles + + deserializeMap(map, mainMapData); + + if (!DEVELOPMENT) { + snapshotTiles(map); + } + + if (DEVELOPMENT) { + addSpawnPointIndicators(world, map); + } + + const giveLantern = give(entities.lanternOn.type); + + const isWinter = world.season === Season.Winter; + const isHalloween = world.holiday === Holiday.Halloween; + + const addWoodenFence = createWoodenFenceMaker(world, map); + const addStoneWall = createStoneWallFenceMaker(world, map); + + function add(entity: ServerEntity) { + if (entity.x < 0 || entity.x > map.width || entity.y < 0 || entity.y > map.height) { + if (DEVELOPMENT) { + logger.warn(`skipped entity (${getEntityTypeName(entity.type)}) outside map (${entity.x} ${entity.y})`); + } + + return { x: entity.x, y: entity.y } as ServerEntity; + } + + return world.addEntity(entity, map); + } + + function addEntities(entities: ServerEntity[]) { + return entities.map(add); + } + + function addTree(x: number, y: number, variant: number, web = false, spider = false) { + addEntities(entities.tree(x, y, variant, web, spider && !isWinter)); + } + + function cliffNE(x: number, y: number) { + add(entities.cliffTopNE(x + 0.5, y)); + lockTiles(map, x - 1, y - 1, 3, 3); + } + + const cliffDecals = [entities.cliffDecal1, entities.cliffDecal3, entities.cliffDecal2]; + + function cracksS(x: number, y: number) { + const code = (Math.random() * 1000) % 64; + const index1 = code & 0b11; + const index2 = (code >> 2) & 0b11; + const index3 = (code >> 4) & 0b11; + index1 && index1 !== 3 && add(cliffDecals[index1 - 1](x + 0.5, y - 1)); // no decal 2 here + index2 && add(cliffDecals[index2 - 1](x + 0.5, y)); + index3 && add(cliffDecals[index3 - 1](x + 0.5, y + 1)); + } + + function cracksSLeft(x: number, y: number) { + const code = (Math.random() * 1000) % 4; + (code & 0b01) && add(entities.cliffDecalL(x + 0.5, y - 1)); + (code & 0b10) && add(entities.cliffDecalL(x + 0.5, y)); + } + + function cracksSRight(x: number, y: number) { + const code = (Math.random() * 1000) % 4; + (code & 0b01) && add(entities.cliffDecalR(x + 0.5, y - 1)); + (code & 0b10) && add(entities.cliffDecalR(x + 0.5, y)); + } + + function cliffSW(x: number, y: number) { + add(entities.cliffSW(x + 0.5, y - 2)); + lockTiles(map, x - 1, y - 4, 3, 7); + cracksSLeft(x, y); + } + + function cliffSE(x: number, y: number) { + add(entities.cliffSE(x + 0.5, y - 2)); + lockTiles(map, x - 1, y - 3, 3, 6); + cracksSRight(x, y); + } + + function cliffS(x: number, y: number) { + add(entities.cliffS2(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + cracksS(x, y); + } + + function cliffSStart(x: number, y: number) { + add(entities.cliffS1(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + cracksS(x, y); + } + + function cliffSEnd(x: number, y: number) { + add(entities.cliffS3(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + cracksS(x, y); + } + + function cliffS1(x: number, y: number) { + add(entities.cliffSb(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + cracksS(x, y); + } + + function cliffS1Entrance(x: number, y: number) { + add(entities.cliffSbEntrance(x + 0.5, y - 1)); + lockTiles(map, x, y - 2, 1, 5); + } + + function cliffRightWithTrimNoEdge(x: number, y: number, h: number) { + cliffRight(x, y, h); + cliffTrimRight(x + 1, y, h, false); + } + + function cliffRightWithTrim(x: number, y: number, h: number) { + cliffRight(x, y - 3, h - 3); + cliffTrimRight(x + 1, y, h); + } + + function cliffLeftWithTrim(x: number, y: number, h: number) { + cliffLeft(x, y - 3, h - 3); + cliffTrimLeft(x, y, h); + } + + function cliffLeft(x: number, y: number, h: number) { + for (let i = 0; i < h; i++) { + add(entities.cliffTopW(x + 0.5, y - i)); + lockTiles(map, x, y - i - 1, 2, 3); + } + } + + function cliffRight(x: number, y: number, h: number) { + for (let i = 0; i < h; i++) { + add(entities.cliffTopE(x + 0.5, y - i)); + lockTiles(map, x - 1, y - i - 1, 2, 3); + } + } + + function cliffTrimLeft(x: number, y: number, h: number) { + add(entities.cliffBotTrimLeft(x - 0.5, y)); + + for (let i = 0; i < (h - 2); i++) { + add(entities.cliffMidTrimLeft(x - 0.5, y - 1 - i)); + } + + add(entities.cliffTopTrimLeft(x - 0.5, y - h + 1)); + } + + function cliffTrimRight(x: number, y: number, h: number, botTrim = true) { + if (botTrim) { + add(entities.cliffBotTrimRight(x + 0.5, y)); + } else { + add(entities.cliffMidTrimRight(x + 0.5, y)); + } + + for (let i = 0; i < (h - 2); i++) { + add(entities.cliffMidTrimRight(x + 0.5, y - 1 - i)); + } + + if (h > 1) { + add(entities.cliffTopTrimRight(x + 0.5, y - h + 1)); + } + } + + function cliffSSection(x: number, y: number, w: number) { + cliffSStart(x, y); + + for (let i = 1; i < (w - 1); i++) { + cliffS(x + i, y); + } + + cliffSEnd(x + w - 1, y); + } + + function cliffSESection(x: number, y: number, w: number) { + for (let i = 0; i < w; i++) { + cliffSE(x + i, y - i); + } + } + + function cliffSWSection(x: number, y: number, w: number) { + for (let i = 0; i < w; i++) { + cliffSW(x + i, y + i); + } + } + + // actual cliffs + cliffS(0, 49); + cliffS(1, 49); + cliffSEnd(2, 49); + cliffSE(3, 49); + cliffSSection(4, 48, 2); + cliffSWSection(6, 49, 2); + cliffSSection(8, 50, 4); + cliffSW(12, 51); + cliffSSection(13, 51, 4); + cliffSE(17, 51); + cliffRightWithTrim(17, 51, 6); + // harbor ramp + cliffLeftWithTrim(23, 50, 6); + cliffSW(23, 50); + cliffSSection(24, 50, 5); + cliffSE(29, 50); + cliffRightWithTrim(29, 50, 7); + cliffSESection(30, 45, 2); + cliffRightWithTrim(31, 44, 5); + cliffSESection(32, 41, 1); + cliffRightWithTrim(32, 41, 4); + cliffSESection(33, 39, 2); + cliffRightWithTrim(34, 38, 5); + cliffNE(34, 33); + // no path spot + cliffRightWithTrimNoEdge(33, 32, 6); + cliffSE(34, 28); + cliffRightWithTrim(34, 28, 5); + cliffSESection(35, 25, 2); + cliffS1(37, 23); + cliffSE(38, 23); + cliffRightWithTrim(38, 23, 4); + cliffSESection(39, 21, 2); + cliffRightWithTrim(40, 20, 4); + cliffSESection(41, 18, 1); + cliffS1(42, 17); + cliffSE(43, 17); + // corner + cliffSSection(44, 16, 2); + cliffSE(46, 16); + cliffS1(47, 15); + cliffSW(48, 16); + cliffSSection(49, 16, 2); + cliffSESection(51, 16, 2); + // end corner + cliffSSection(53, 14, 3); + cliffSW(56, 15); + cliffSSection(57, 15, 2); + cliffSE(59, 15); + cliffSSection(60, 14, 3); + cliffSWSection(63, 15, 2); + cliffS1(65, 16); + cliffSW(66, 17); + cliffLeftWithTrim(67, 19, 4); + cliffSW(67, 19); + cliffLeftWithTrim(68, 23, 6); + cliffSW(68, 23); + cliffLeftWithTrim(69, 25, 4); + cliffSW(69, 25); + cliffSSection(70, 25, 4); + cliffSE(74, 25); + + // mine entrance + cliffS1Entrance(75, 24); + cliffS1Entrance(76, 24); + cliffS1Entrance(77, 24); + + cliffSW(78, 25); + cliffS1(79, 25); + cliffSW(80, 26); + cliffSSection(81, 26, 3); + cliffSE(84, 26); + cliffSSection(85, 25, 2); + cliffSE(87, 25); + cliffRightWithTrim(87, 25, 5); + cliffNE(87, 20); + cliffNE(86, 19); + cliffRightWithTrimNoEdge(85, 18, 1); + cliffNE(85, 17); + cliffRightWithTrimNoEdge(84, 16, 3); + cliffSESection(85, 15, 2); + cliffS1(87, 13); + cliffSW(88, 14); + cliffSSection(89, 14, 2); + cliffSE(91, 14); + cliffRightWithTrim(91, 14, 6); + // river ramp + cliffLeftWithTrim(96, 14, 6); + cliffSW(96, 14); + cliffSSection(97, 14, 3); + cliffSE(100, 14); + cliffSSection(101, 13, 3); + cliffSE(104, 13); + cliffRightWithTrim(104, 13, 5); + cliffSESection(105, 10, 1); + cliffRightWithTrim(105, 10, 5); + cliffSESection(106, 7, 1); + cliffS1(107, 6); + cliffSE(108, 6); + cliffRightWithTrim(108, 6, 4); + cliffSESection(109, 4, 2); + cliffRightWithTrim(110, 3, 4); + + const plankWidth = 78 / tileWidth; + const plankHeight = 12 / tileHeight; + + const plank = () => sample(entities.planks)!; + const shortPlank = () => sample(entities.planksShort)!; + + // barrel storage + addWoodenFence(100, 24, 6); + addWoodenFence(100, 24, 2, false, true, false); + addWoodenFence(100, 29, 3, false, false, true); + addWoodenFence(106, 24, 8, false, true, true); + addWoodenFence(100, 32, 6); + add(entities.barrel(104.56, 24.70)); + add(entities.barrel(105.34, 25.08)); + add(entities.barrel(104.90, 25.70)); + add(entities.barrel(100.84, 24.50)); + add(entities.barrel(101.75, 24.50)); + add(entities.barrel(100.72, 31.25)); + add(entities.barrel(101.44, 31.54)); + add(entities.barrel(101.75, 30.75)); + add(entities.barrel(105.25, 29.79)); + add(entities.barrel(105.50, 30.71)); + add(entities.barrel(105.25, 31.67)); + add(entities.barrel(103.91, 31.04)); + add(entities.barrel(106.94, 24.08)); + add(entities.barrel(107.75, 24.88)); + add(entities.barrel(105.59, 32.83)); + add(entities.barrel(99.34, 24.13)); + add(entities.barrel(99.41, 25.21)); + add(entities.barrel(98.31, 24.88)); + add(entities.lanternOn(100.50, 25.63)); + add(entities.lanternOn(100.50, 28.96)); + add(entities.lanternOn(105.53, 25.88)); + + // orchard / mine entrance + const mineEntrance = add(entities.mineEntrance(76.5, 23.08)); + // const mineClosed = add(entities.mineClosed(76.5, 23.08)); + // setEntityName(mineClosed, 'Mine Closed'); + + if (true) { + add(entities.collider1x1(74.50, 25.17)); + add(entities.collider1x1(77.50, 25.21)); + mineEntrance.interact = (_, client) => goToMap(world, client, 'cave'); + add(entities.triggerHouseDoor(76.50, 25.88)).trigger = (_, client) => goToMap(world, client, 'cave'); + add(entities.mineRailsFadeUp(76.5, 25)); + add(entities.mineRailsV(76.5, 26)); + add(entities.mineRailsV(76.5, 27)); + add(entities.mineRailsNW(76.5, 28)); + add(entities.mineRailsH(75.5, 28)); + add(entities.mineRailsH(74.5, 28)); + add(entities.mineRailsH(73.5, 28)); + add(entities.mineRailsEndLeft(72.5, 28.5)); + add(entities.mineCart(74, 28)); + } + + add(entities.rock(79.09, 27.33)); + add(entities.rock2(79.47, 27.71)); + add(entities.rock3(73.53, 27.21)); + add(entities.rock2(73.88, 27.42)); + add(entities.lanternOn(74.59, 29.20)); + add(entities.lanternOn(78.88, 27.67)); + add(createBoxOfLanterns(79.84, 29.00)); + + // addEntities(entities.tree5(74.25, 30.00, 0)); + // addEntities(entities.tree5(78.53, 30.04, 0)); + // addEntities(entities.tree5(76.34, 33.00, 1)); + add(entities.tree5Stump(74.25, 30.00)); + add(entities.tree5Stump(78.53, 30.04)); + + addEntities(entities.tree5(78.59, 36.63, 2)); + addEntities(entities.tree4(80.97, 33.29, 1)); + addEntities(entities.tree5(83.50, 36.54, 2)); + addEntities(entities.tree5(83.16, 29.63, 0)); + addEntities(entities.tree5(86.00, 33.00, 1)); + addEntities(entities.tree5(87.75, 36.54, 2)); + addEntities(entities.tree5(87.66, 29.54, 0)); + addEntities(entities.tree5(73.91, 36.17, 1)); + addEntities(entities.tree5(72.00, 32.83, 2)); + addEntities(entities.tree5(69.59, 36.04, 0)); + addWoodenFence(62, 38, 9); + addWoodenFence(71, 38, 1, false, true, true); + addWoodenFence(71, 39, 3); + addWoodenFence(78, 39, 3); + addWoodenFence(81, 39, 1, false, true, true); + addWoodenFence(81, 40, 9); + addWoodenFence(69, 43, 8); + addWoodenFence(90, 28, 12, false, false, true); + add(entities.lanternOn(77.97, 36.42)); + add(entities.lanternOn(73.81, 39.21)); + add(entities.box(81.91, 39.13)); + add(entities.box(83.03, 39.50)); + + // bridge + add(plank()(110.7, 36.60)); + add(shortPlank()(112.56, 36.60)); + add(shortPlank()(110.18, 37.08)); + add(plank()(112.06, 37.08)); + add(plank()(110.71, 37.58)); + add(shortPlank()(112.59, 37.58)); + add(shortPlank()(110.21, 38.08)); + add(plank()(112.09, 38.08)); + add(plank()(110.63, 38.58)); + add(shortPlank()(112.50, 38.58)); + add(entities.plankShadow(111.46, 38.50)); + add(entities.pierLeg(111.46, 38.50)); + + add(entities.collider3x1(110, 36)); + add(entities.collider3x1(110, 39)); + + // pier + const pierX = 0; + const pierY = 8 / tileHeight; + + add(createSignWithText(pierX + 8.5, pierY + 71.1, 'Party Island', + `Hop on the boat to travel to an island, that is unique to your party`)); + + add(entities.triggerBoat(7.5, 70)).trigger = (_, client) => goToMap(world, client, 'island'); + + addEntities(entities.fullBoat(7, 69.66)); + add(entities.pierLeg(pierX + 10, pierY + 72.6)); + add(entities.pierLeg(pierX + 11.9, pierY + 72.6)); + add(entities.pierLeg(pierX + 5, pierY + 74.6)); + add(entities.pierLeg(pierX + 6.9, pierY + 74.6)); + add(entities.pierLeg(pierX + 8.8, pierY + 74.6)); + const plankOffsets = [0, -1, 0, -2, -1, -1, 0, -2, -1].map(x => x / tileWidth); + + for (let y = 0; y < 9; y++) { + const maxX = y < 5 ? ((y % 2) ? 5 : 4) : ((y % 2) ? 3 : 2); + const baseX = pierX + 4.5 + ((y % 2) ? 0 : (plankWidth / 2)) + plankOffsets[y]; + const baseY = pierY + 71 - (9 / tileHeight); + + for (let x = 0; x < maxX; x++) { + if ((x === 0 && (y % 2)) || (x === (maxX - 1) && (y % 2))) { + const ox = x === 0 ? (18 / tileWidth) : (-18 / tileWidth); + add(shortPlank()(baseX + ox + x * plankWidth, baseY + y * plankHeight)); + } else { + add(plank()(baseX + x * plankWidth, baseY + y * plankHeight)); + } + } + } + + add(entities.collider1x3(3.5, 69)); + add(entities.collider1x3(3.5, 72)); + add(entities.collider1x1(3.5, 75)); + + add(entities.collider3x1(4, 69)); + add(entities.collider3x1(7, 69)); + add(entities.collider1x1(9, 70)); + add(entities.collider1x2(9.6, 69)); + add(entities.collider3x1(10, 70.3)); + add(entities.collider2x1(4, 70)); + add(entities.collider1x3(4, 71)); + add(entities.collider1x2(4, 73)); + add(entities.collider1x2(4, 73)); + add(entities.collider3x1(4, 75)); + add(entities.collider3x1(7, 75)); + add(entities.collider1x2(9, 73)); + add(entities.collider3x1(10, 73)); + + add(entities.plankShadowShort(pierX + 5.09, pierY + 74.08 - plankHeight * 6)); + add(entities.plankShadowShort(pierX + 5.06, pierY + 74.08 - plankHeight * 4)); + add(entities.plankShadowShort(pierX + 5.09, pierY + 74.08 - plankHeight * 2)); + add(entities.plankShadowShort(pierX + 5.06, pierY + 74.08)); + + add(entities.plankShadowShort(pierX + 8.84, pierY + 74.12 - plankHeight * 2)); + add(entities.plankShadowShort(pierX + 8.78, pierY + 74.12 - plankHeight)); + add(entities.plankShadowShort(pierX + 8.81, pierY + 74.12)); + + add(entities.plankShadow(pierX + 11.68, pierY + 72.62)); + add(entities.plankShadow2(pierX + 11.68 - plankWidth, pierY + 72.62)); + add(entities.plankShadowShort(pierX + 13.5, pierY + 72.45)); + + add(entities.plankShadow2(pierX + 5.75, pierY + 74.62)); + add(entities.plankShadow(pierX + 5.75 + plankWidth, pierY + 74.62)); + + add(entities.lanternOn(pierX + 13.25, pierY + 70.83)); + add(entities.lanternOn(pierX + 13.31, pierY + 73.04)); + add(entities.lanternOn(pierX + 9.31, pierY + 73.10)); + add(entities.lanternOn(pierX + 9.63, pierY + 70.88)); + add(entities.lanternOn(pierX + 4.72, pierY + 70.96)); + add(entities.lanternOn(pierX + 4.69, pierY + 74.96)); + + add(entities.barrel(pierX + 13.75, pierY + 73.71)); + add(entities.barrel(pierX + 13.56, pierY + 74.46)); + add(entities.barrel(pierX + 14.34, pierY + 74.17)); + add(entities.barrel(pierX + 16.56, pierY + 76.62)); + add(entities.barrel(pierX + 5.41, pierY + 71.21)); + add(entities.barrel(pierX + 13.96, pierY + 67.67)); + add(entities.barrel(pierX + 14.75, pierY + 67.96)); + + // plants + add(entities.largeLeafedBush1(13.59, 64.79)); + add(entities.largeLeafedBush2(22.09, 68.75)); + add(entities.largeLeafedBush1(31.72, 65.50)); + add(entities.largeLeafedBush2(30.25, 53.67)); + add(entities.largeLeafedBush1(31.46, 52.66)); + add(entities.largeLeafedBush2(42.50, 47.71)); + add(entities.largeLeafedBush1(43.44, 52.50)); + add(entities.largeLeafedBush2(37.72, 25.54)); + add(entities.largeLeafedBush1(38.53, 25.17)); + add(entities.largeLeafedBush2(43.94, 25.04)); + add(entities.largeLeafedBush1(53.13, 16.50)); + add(entities.largeLeafedBush2(53.03, 17.42)); + add(entities.largeLeafedBush1(63.50, 31.46)); + add(entities.largeLeafedBush2(62.19, 32.33)); + add(entities.largeLeafedBush1(68.53, 28.79)); + add(entities.largeLeafedBush2(67.38, 21.04)); + add(entities.largeLeafedBush1(68.97, 36.84)); + add(entities.largeLeafedBush2(70.69, 45.71)); + add(entities.largeLeafedBush1(76.91, 43.58)); + add(entities.largeLeafedBush2(77.75, 44.17)); + add(entities.largeLeafedBush1(63.59, 46.58)); + add(entities.largeLeafedBush2(76.71, 60.76)); + add(entities.largeLeafedBush1(77.59, 60.00)); + add(entities.largeLeafedBush2(80.88, 53.21)); + add(entities.largeLeafedBush1(89.38, 57.71)); + add(entities.largeLeafedBush2(104.41, 45.79)); + add(entities.largeLeafedBush1(104.88, 46.58)); + add(entities.largeLeafedBush2(105.81, 44.88)); + add(entities.largeLeafedBush1(104.47, 32.75)); + add(entities.largeLeafedBush2(103.72, 33.33)); + add(entities.largeLeafedBush1(88.03, 15.58)); + add(entities.largeLeafedBush2(88.84, 16.17)); + add(entities.largeLeafedBush1(88.09, 16.63)); + add(entities.largeLeafedBush2(105.91, 16.46)); + add(entities.largeLeafedBush1(106.65, 9.20)); + add(entities.largeLeafedBush2(103.69, 6.46)); + add(entities.largeLeafedBush1(100.94, 5.38)); + add(entities.largeLeafedBush2(73.22, 6.71)); + add(entities.largeLeafedBush1(57.66, 6.08)); + add(entities.largeLeafedBush2(56.97, 6.67)); + add(entities.largeLeafedBush1(32.53, 24.25)); + add(entities.largeLeafedBush2(33.47, 24.54)); + add(entities.largeLeafedBush1(25.19, 19.13)); + add(entities.largeLeafedBush2(13.94, 49.08)); + add(entities.largeLeafedBush1(13.50, 48.21)); + add(entities.largeLeafedBush2(11.09, 11.71)); + add(entities.largeLeafedBush1(11.81, 12.63)); + add(entities.largeLeafedBush2(23.72, 7.42)); + add(entities.largeLeafedBush1(23.22, 8.25)); + add(entities.largeLeafedBush2(25.84, 38.33)); + add(entities.largeLeafedBush1(9.03, 61.50)); + add(entities.largeLeafedBush2(125.03, 37.50)); + add(entities.largeLeafedBush1(119.56, 47.92)); + add(entities.largeLeafedBush2(124.69, 26.54)); + add(entities.largeLeafedBush1(125.56, 26.00)); + add(entities.largeLeafedBush2(139.97, 9.25)); + add(entities.largeLeafedBush1(150.41, 11.83)); + add(entities.largeLeafedBush2(154.31, 25.71)); + add(entities.largeLeafedBush1(153.46, 26.20)); + add(entities.largeLeafedBush2(139.00, 25.79)); + add(entities.largeLeafedBush1(121.69, 7.92)); + add(entities.largeLeafedBush2(157.03, 66.88)); + add(entities.largeLeafedBush1(152.81, 89.54)); + add(entities.largeLeafedBush2(151.81, 81.04)); + add(entities.largeLeafedBush1(152.06, 82.00)); + add(entities.largeLeafedBush2(146.66, 94.96)); + add(entities.largeLeafedBush1(156.56, 106.71)); + add(entities.largeLeafedBush2(156.63, 107.92)); + add(entities.largeLeafedBush1(152.81, 129.96)); + add(entities.largeLeafedBush2(143.50, 135.79)); + add(entities.largeLeafedBush1(143.19, 136.75)); + add(entities.largeLeafedBush2(131.53, 121.92)); + add(entities.largeLeafedBush1(132.44, 133.67)); + add(entities.largeLeafedBush2(132.84, 134.83)); + add(entities.largeLeafedBush1(139.00, 131.58)); + add(entities.largeLeafedBush2(125.00, 137.83)); + add(entities.largeLeafedBush1(123.41, 138.33)); + add(entities.largeLeafedBush2(119.81, 136.96)); + add(entities.largeLeafedBush1(122.38, 120.63)); + add(entities.largeLeafedBush2(111.19, 126.54)); + add(entities.largeLeafedBush1(113.66, 130.13)); + add(entities.largeLeafedBush2(112.50, 118.17)); + add(entities.largeLeafedBush1(107.94, 113.83)); + add(entities.largeLeafedBush2(108.06, 114.79)); + add(entities.largeLeafedBush1(105.28, 105.88)); + add(entities.largeLeafedBush2(100.16, 111.25)); + add(entities.largeLeafedBush1(101.06, 111.88)); + add(entities.largeLeafedBush2(121.81, 107.25)); + add(entities.largeLeafedBush1(122.84, 107.63)); + add(entities.largeLeafedBush2(88.69, 115.50)); + add(entities.largeLeafedBush1(89.06, 116.42)); + add(entities.largeLeafedBush2(81.00, 112.33)); + add(entities.largeLeafedBush1(81.69, 113.21)); + add(entities.largeLeafedBush2(91.53, 151.42)); + add(entities.largeLeafedBush1(90.81, 150.75)); + add(entities.largeLeafedBush2(99.44, 153.25)); + add(entities.largeLeafedBush1(99.03, 153.96)); + add(entities.largeLeafedBush2(108.50, 147.17)); + add(entities.largeLeafedBush1(117.75, 153.38)); + add(entities.largeLeafedBush2(118.44, 152.58)); + add(entities.largeLeafedBush1(132.91, 148.54)); + add(entities.largeLeafedBush2(125.53, 144.92)); + add(entities.largeLeafedBush1(124.72, 145.50)); + add(entities.largeLeafedBush2(145.03, 144.92)); + add(entities.largeLeafedBush1(118.06, 124.92)); + add(entities.largeLeafedBush2(116.03, 94.42)); + add(entities.largeLeafedBush1(103.25, 81.96)); + add(entities.largeLeafedBush2(103.78, 82.88)); + add(entities.largeLeafedBush1(87.47, 89.54)); + add(entities.largeLeafedBush2(87.97, 90.50)); + add(entities.largeLeafedBush1(79.59, 84.50)); + add(entities.largeLeafedBush2(77.88, 85.25)); + add(entities.largeLeafedBush1(80.34, 91.42)); + add(entities.largeLeafedBush2(90.81, 78.63)); + add(entities.largeLeafedBush1(98.00, 63.71)); + add(entities.largeLeafedBush2(42.41, 84.79)); + add(entities.largeLeafedBush1(39.19, 82.92)); + add(entities.largeLeafedBush2(30.78, 77.13)); + add(entities.largeLeafedBush1(31.16, 78.08)); + add(entities.largeLeafedBush2(34.47, 70.63)); + add(entities.largeLeafedBush1(14.09, 90.08)); + add(entities.largeLeafedBush2(14.97, 89.17)); + add(entities.largeLeafedBush1(32.41, 111.13)); + add(entities.largeLeafedBush2(31.59, 111.58)); + add(entities.largeLeafedBush1(52.81, 110.38)); + add(entities.largeLeafedBush2(53.44, 110.04)); + add(entities.largeLeafedBush1(31.59, 109.83)); + add(entities.largeLeafedBush2(51.13, 95.04)); + add(entities.largeLeafedBush1(50.34, 95.54)); + add(entities.largeLeafedBush2(52.25, 96.21)); + add(entities.largeLeafedBush1(67.65, 81.45)); + + add(entities.largeLeafedBush3(63.50, 32.21)); + add(entities.largeLeafedBush4(69.28, 35.67)); + add(entities.largeLeafedBush3(66.56, 20.08)); + add(entities.largeLeafedBush4(64.25, 23.33)); + add(entities.largeLeafedBush3(67.81, 29.42)); + add(entities.largeLeafedBush4(72.44, 27.38)); + add(entities.largeLeafedBush3(52.22, 17.21)); + add(entities.largeLeafedBush4(43.59, 24.08)); + add(entities.largeLeafedBush3(38.47, 25.88)); + add(entities.largeLeafedBush4(46.38, 29.21)); + add(entities.largeLeafedBush3(41.69, 47.96)); + add(entities.largeLeafedBush4(44.16, 53.21)); + add(entities.largeLeafedBush3(31.16, 53.67)); + add(entities.largeLeafedBush4(31.00, 65.92)); + add(entities.largeLeafedBush3(20.72, 69.42)); + add(entities.largeLeafedBush4(12.88, 65.04)); + add(entities.largeLeafedBush3(9.47, 62.29)); + add(entities.largeLeafedBush4(13.03, 49.04)); + add(entities.largeLeafedBush3(25.13, 38.79)); + add(entities.largeLeafedBush4(15.38, 34.33)); + add(entities.largeLeafedBush3(33.34, 25.17)); + add(entities.largeLeafedBush4(25.31, 19.92)); + add(entities.largeLeafedBush3(24.06, 8.58)); + add(entities.largeLeafedBush4(10.91, 12.58)); + add(entities.largeLeafedBush3(9.72, 7.38)); + add(entities.largeLeafedBush4(40.19, 16.13)); + add(entities.largeLeafedBush3(57.59, 7.33)); + add(entities.largeLeafedBush4(73.03, 7.54)); + add(entities.largeLeafedBush3(71.41, 22.75)); + add(entities.largeLeafedBush4(87.32, 16.17)); + add(entities.largeLeafedBush3(85.25, 27.29)); + add(entities.largeLeafedBush4(100.81, 6.17)); + add(entities.largeLeafedBush3(106.44, 10.25)); + add(entities.largeLeafedBush4(105.72, 17.71)); + add(entities.largeLeafedBush3(105.28, 7.13)); + add(entities.largeLeafedBush4(104.56, 33.75)); + add(entities.largeLeafedBush3(105.63, 45.58)); + add(entities.largeLeafedBush4(117.41, 49.58)); + add(entities.largeLeafedBush3(73.91, 66.96)); + add(entities.largeLeafedBush4(76.68, 59.87)); + add(entities.largeLeafedBush3(84.06, 64.33)); + add(entities.largeLeafedBush4(81.50, 53.67)); + add(entities.largeLeafedBush3(76.72, 44.54)); + add(entities.largeLeafedBush4(62.81, 46.92)); + add(entities.largeLeafedBush3(70.41, 46.63)); + add(entities.largeLeafedBush4(34.47, 69.75)); + add(entities.largeLeafedBush3(30.34, 78.54)); + add(entities.largeLeafedBush4(39.84, 83.38)); + add(entities.largeLeafedBush3(42.06, 85.42)); + add(entities.largeLeafedBush4(50.94, 96.17)); + add(entities.largeLeafedBush3(30.97, 110.46)); + add(entities.largeLeafedBush4(13.75, 95.92)); + add(entities.largeLeafedBush3(14.41, 95.67)); + add(entities.largeLeafedBush4(14.88, 90.13)); + add(entities.largeLeafedBush3(28.66, 119.33)); + add(entities.largeLeafedBush4(31.53, 133.17)); + add(entities.largeLeafedBush3(53.47, 110.92)); + add(entities.largeLeafedBush4(90.69, 151.54)); + add(entities.largeLeafedBush3(99.72, 154.13)); + add(entities.largeLeafedBush4(95.78, 146.63)); + add(entities.largeLeafedBush3(108.44, 148.00)); + add(entities.largeLeafedBush4(93.25, 131.96)); + add(entities.largeLeafedBush3(125.56, 145.83)); + add(entities.largeLeafedBush4(123.97, 139.04)); + add(entities.largeLeafedBush3(120.59, 137.38)); + add(entities.largeLeafedBush4(118.44, 153.63)); + add(entities.largeLeafedBush3(111.16, 152.63)); + add(entities.largeLeafedBush4(133.06, 149.46)); + add(entities.largeLeafedBush3(144.16, 144.96)); + add(entities.largeLeafedBush4(143.72, 137.46)); + add(entities.largeLeafedBush3(132.09, 135.33)); + add(entities.largeLeafedBush4(138.16, 131.88)); + add(entities.largeLeafedBush3(152.63, 131.21)); + add(entities.largeLeafedBush4(145.72, 120.67)); + add(entities.largeLeafedBush3(131.56, 122.79)); + add(entities.largeLeafedBush4(122.16, 121.50)); + add(entities.largeLeafedBush3(122.22, 107.92)); + add(entities.largeLeafedBush4(117.28, 125.38)); + add(entities.largeLeafedBush3(111.34, 127.42)); + add(entities.largeLeafedBush4(112.44, 119.13)); + add(entities.largeLeafedBush3(107.25, 115.04)); + add(entities.largeLeafedBush4(114.28, 130.79)); + add(entities.largeLeafedBush3(105.34, 106.79)); + add(entities.largeLeafedBush4(100.31, 112.17)); + add(entities.largeLeafedBush3(87.47, 91.08)); + add(entities.largeLeafedBush4(79.84, 85.33)); + add(entities.largeLeafedBush3(103.19, 83.25)); + add(entities.largeLeafedBush4(98.38, 85.96)); + add(entities.largeLeafedBush3(114.09, 92.21)); + add(entities.largeLeafedBush4(115.81, 95.21)); + add(entities.largeLeafedBush3(117.38, 70.04)); + add(entities.largeLeafedBush4(147.13, 95.75)); + add(entities.largeLeafedBush3(142.53, 98.54)); + add(entities.largeLeafedBush4(157.44, 107.33)); + add(entities.largeLeafedBush3(152.19, 90.08)); + add(entities.largeLeafedBush4(158.34, 113.71)); + add(entities.largeLeafedBush3(151.38, 82.42)); + add(entities.largeLeafedBush4(157.28, 67.79)); + add(entities.largeLeafedBush3(157.97, 57.04)); + add(entities.largeLeafedBush4(154.00, 26.54)); + add(entities.largeLeafedBush3(138.13, 26.17)); + add(entities.largeLeafedBush4(125.06, 27.17)); + add(entities.largeLeafedBush3(125.66, 38.25)); + add(entities.largeLeafedBush4(119.09, 48.54)); + add(entities.largeLeafedBush3(116.81, 35.83)); + add(entities.largeLeafedBush4(141.38, 10.13)); + add(entities.largeLeafedBush3(149.03, 11.33)); + add(entities.largeLeafedBush4(152.53, 5.75)); + add(entities.largeLeafedBush3(121.41, 8.63)); + add(entities.largeLeafedBush4(109.75, 26.25)); + add(entities.largeLeafedBush3(109.38, 35.46)); + + // north-west hill + addEntities(entities.pine(37.91, 88.33, 0)); + addEntities(entities.pine(39.91, 91.29, 0)); + addEntities(entities.pine(15.53, 83.54, 0)); + addEntities(entities.pine(17.91, 82.17, 0)); + addEntities(entities.pine(21.59, 86.71, 0)); + addEntities(entities.pine(27.56, 81.38, 0)); + addEntities(entities.pine(2.88, 36.17, 0)); + addEntities(entities.pine(4.34, 40.79, 0)); + addEntities(entities.pine(2.59, 56.46, 0)); + addEntities(entities.pine(0.72, 53.33, 0)); + addEntities(entities.pine(8.72, 26.92, 0)); + addEntities(entities.pine(10.19, 29.46, 0)); + addEntities(entities.pine(3.38, 20.33, 0)); + add(entities.lanternOn(7.06, 17.92)); + add(entities.lanternOn(7.78, 22.96)); + add(entities.lanternOn(11.72, 19.04)); + add(entities.boxLanterns(5.66, 21.88)).interact = giveLantern; + addStoneWall(6, 32, 3); + addStoneWall(6, 32, 4, false, true, true); + addStoneWall(12, 32, 1, false, true); + addStoneWall(12, 38, 1, false, false, true); + addStoneWall(6, 40, 3); + add(entities.barrel(11.34, 33.08)); + add(entities.barrel(10.31, 33.46)); + add(entities.barrel(11.22, 33.96)); + add(entities.barrel(6.63, 39.08)); + add(entities.barrel(7.75, 39.21)); + add(entities.lanternOnWall(7.97, 32.29)); + add(entities.lanternOnWall(10.00, 40.33)); + add(entities.lanternOnWall(6.00, 36.33)); + add(entities.lanternOnWall(12.00, 34.33)); + + // north hill + addWoodenFence(52, 12.5, 11); + addWoodenFence(104.25, 9.25, 2, false, false, true); + addWoodenFence(101.25, 11.25, 3); + + // bridge + addWoodenFence(104, 36, 5); + addWoodenFence(105, 41, 3); + + // north-east fields + addWoodenFence(116, 13, 9); + addWoodenFence(116, 13, 11, false, true, true); + addWoodenFence(116, 24, 2); + addWoodenFence(118, 24, 2, false, true, true); + addWoodenFence(118, 26, 7); + addWoodenFence(125, 13, 6, false, true); + addWoodenFence(125, 22, 4, false, false, true); + addWoodenFence(140, 25, 12); + addWoodenFence(140, 25, 9, false, true, true); + addWoodenFence(152, 25, 9, false, true, true); + addWoodenFence(140, 34, 5); + addWoodenFence(148, 34, 4); + + // harbor + addStoneWall(13.2, 67, 1); + addStoneWall(15.2, 61, 3, false, false, true); + addStoneWall(20, 64, 3, false, false, true); + addStoneWall(20, 70, 4); + addStoneWall(16, 76, 3); + + // north hills + addEntities(entities.tree(15.06, 48.38, 0)); + addEntities(entities.tree(12.25, 44.21, 1 + 8)); + addEntities(entities.tree(29.50, 41.29, 2)); + addEntities(entities.tree(24.88, 37.67, 0 + 4)); + addEntities(entities.tree(45.91, 28.38, 1)); + addEntities(entities.tree(42.94, 24.33, 2)); + addEntities(entities.tree(31.78, 24.71, 0)); + addEntities(entities.tree(29.53, 22.29, 1 + 4)); + addEntities(entities.tree(26.69, 26.50, 2)); + addEntities(entities.tree(41.69, 13.25, 0)); + addEntities(entities.tree(65.13, 23.38, 1 + 8)); + addEntities(entities.tree(69.81, 19.75, 2 + 8)); + addEntities(entities.tree(70.91, 22.00, 0 + 4)); + addEntities(entities.tree(67.63, 28.33, 1)); + addEntities(entities.tree(62.84, 31.67, 2 + 4)); + addEntities(entities.tree(108.62, 8.71, 0)); + addEntities(entities.tree(105.09, 16.96, 1)); + addEntities(entities.tree(103.03, 19.25, 2 + 8)); + addEntities(entities.tree(107.81, 23.50, 0 + 4)); + addEntities(entities.tree(97.81, 38.63, 1)); + addEntities(entities.tree(108.63, 35.13, 2)); + addEntities(entities.tree(61.13, 43.67, 0)); + addEntities(entities.tree(24.44, 7.92, 0 + 4)); + addEntities(entities.tree(23.06, 16.54, 1 + 8)); + addEntities(entities.tree(24.66, 19.46, 2)); + addEntities(entities.tree(38.97, 6.96, 0)); + addEntities(entities.tree(41.19, 4.54, 1)); + addEntities(entities.tree(62.44, 2.96, 2 + 4)); + addEntities(entities.tree(58.50, 6.38, 0 + 4)); + addEntities(entities.tree(75.19, 5.04, 1 + 8)); + addEntities(entities.tree(89.91, 4.00, 2)); + addEntities(entities.tree(104.47, 6.58, 0)); + addEntities(entities.tree(102.53, 3.42, 1)); + addEntities(entities.tree(21.72, 6.04, 2 + 4)); + addEntities(entities.tree(12.06, 10.75, 0 + 8)); + addEntities(entities.tree(9.66, 6.46, 1)); + addEntities(entities.tree5(13.50, 7.63, 0)); + addEntities(entities.tree5(21.22, 20.17, 1)); + addEntities(entities.tree5(43.88, 6.79, 2)); + addEntities(entities.tree5(62.53, 5.88, 0)); + addEntities(entities.tree5(72.44, 7.29, 1)); + addEntities(entities.tree5(100.38, 5.63, 2)); + addEntities(entities.pine(1.66, 6.29, 0)); + addEntities(entities.pine(3.91, 9.67, 0)); + addEntities(entities.pine(1.59, 24.67, 0)); + addEntities(entities.pine(26.75, 4.13, 0)); + + // north-east fields + add(entities.pumpkin(142.84, 27.29)); + add(entities.pumpkin(143.59, 27.92)); + add(entities.pumpkin(142.75, 28.88)); + add(entities.pumpkin(150.81, 29.33)); + add(entities.pumpkin(151.22, 30.00)); + add(entities.pumpkin(148.53, 26.54)); + add(entities.pumpkin(140.69, 32.58)); + add(entities.pumpkin(141.03, 32.96)); + add(entities.pumpkin(150.47, 33.21)); + add(entities.pumpkin(150.91, 32.54)); + add(entities.pumpkin(149.91, 32.29)); + add(entities.pumpkin(150.38, 31.54)); + add(entities.pumpkin(144.44, 30.29)); + addEntities(entities.tree(153.59, 24.96, 0)); + addEntities(entities.tree(153.28, 5.58, 1 + 4)); + addEntities(entities.tree(155.03, 8.08, 2 + 8)); + addEntities(entities.tree(149.78, 11.13, 0)); + addEntities(entities.tree(124.56, 11.67, 1)); + addEntities(entities.tree(121.00, 7.25, 2 + 4)); + addEntities(entities.tree(114.47, 21.38, 0 + 8)); + addEntities(entities.tree(136.50, 5.79, 1 + 4)); + addEntities(entities.tree(138.53, 4.04, 2)); + addEntities(entities.tree(140.72, 9.58, 0)); + addEntities(entities.tree(138.22, 25.13, 1 + 4)); + addEntities(entities.tree(140.09, 23.54, 2 + 8)); + addEntities(entities.tree(125.72, 36.83, 0)); + addEntities(entities.tree(116.44, 35.00, 1)); + addEntities(entities.tree(117.53, 33.17, 2 + 4)); + addEntities(entities.pine(141.28, 37.29, 0)); + addEntities(entities.pine(155.88, 34.67, 0)); + addEntities(entities.pine(135.19, 34.88, 0)); + addEntities(entities.pine(155.16, 11.54, 0)); + addEntities(entities.pine(149.75, 17.25, 0)); + addEntities(entities.pine(151.88, 19.79, 0)); + + // harbor road + addEntities(entities.tree(12.91, 64.13, 0)); + addEntities(entities.tree(30.03, 77.58, 1)); + addEntities(entities.tree(21.22, 69.17, 2)); + addEntities(entities.tree(30.69, 53.00, 0)); + addEntities(entities.tree(33.72, 69.96, 1)); + addEntities(entities.tree(30.94, 64.96, 2)); + addEntities(entities.tree(39.72, 82.25, 0)); + addEntities(entities.tree(10.81, 59.25, 1)); + addEntities(entities.pine(37.78, 58.79, 0)); + + // south-west forest + addEntities(entities.pine(28.22, 132.04, 0)); + addEntities(entities.pine(30.91, 130.67, 0)); + addEntities(entities.pine(32.03, 145.79, 0)); + addEntities(entities.pine(27.75, 152.04, 0)); + addEntities(entities.pine(28.59, 113.42, 0)); + addEntities(entities.pine(40.34, 119.54, 0)); + addEntities(entities.pine(18.75, 114.63, 0)); + addEntities(entities.pine(16.69, 117.79, 0)); + addEntities(entities.pine(20.91, 122.38, 0)); + addEntities(entities.pine(23.31, 121.71, 0)); + addEntities(entities.pine(23.69, 126.58, 0)); + addEntities(entities.pine(6.25, 141.58, 0)); + addEntities(entities.pine(4.66, 147.21, 0)); + addEntities(entities.pine(8.06, 144.71, 0)); + addEntities(entities.pine(3.47, 123.46, 0)); + addEntities(entities.pine(35.00, 105.04, 0)); + addEntities(entities.pine(32.00, 101.96, 0)); + addEntities(entities.pine(16.69, 96.46, 0)); + addEntities(entities.pine(17.75, 100.00, 0)); + addEntities(entities.pine(21.03, 95.13, 0)); + addEntities(entities.pine(36.94, 134.46, 0)); + addEntities(entities.pine(37.75, 136.83, 0)); + add(entities.rock(20.84, 98.25)); + add(entities.rock(29.09, 119.00)); + add(entities.rock(36.81, 141.83)); + add(entities.rock(10.00, 149.58)); + add(entities.rock(5.53, 121.88)); + add(entities.rock(3.28, 103.38)); + add(entities.rock(36.84, 122.42)); + add(entities.lanternOn(33.19, 135.83)); + add(entities.lanternOn(28.16, 135.38)); + add(entities.lanternOn(26.41, 137.92)); + add(entities.lanternOn(26.91, 143.96)); + add(entities.lanternOn(34.25, 140.50)); + add(entities.lanternOn(30.88, 143.71)); + add(entities.lanternOn(20.63, 146.96)); + add(entities.lanternOn(15.06, 147.46)); + add(entities.lanternOn(14.94, 152.50)); + add(entities.lanternOn(19.13, 154.92)); + add(entities.lanternOn(21.00, 152.71)); + add(entities.lanternOn(29.84, 105.04)); + add(entities.lanternOn(24.34, 108.92)); + add(entities.lanternOn(29.59, 109.92)); + add(entities.boxLanterns(15.66, 154.88)).interact = giveLantern; + add(entities.boxLanterns(31.72, 134.50)).interact = giveLantern; + add(entities.boxLanterns(138.97, 27.33)).interact = giveLantern; + add(entities.boxLanterns(139.47, 10.83)).interact = giveLantern; + add(entities.treeStump1(23.09, 145.67)); + add(entities.treeStump2(30.78, 132.92)); + add(entities.treeStump1(35.31, 143.04)); + add(entities.treeStump2(18.31, 124.13)); + add(entities.treeStump1(7.50, 147.38)); + add(entities.treeStump2(5.22, 128.71)); + add(entities.treeStump1(22.22, 147.25)); + add(entities.treeStump2(24.91, 149.33)); + add(entities.treeStump1(9.63, 139.50)); + add(entities.treeStump2(11.59, 141.79)); + add(entities.treeStump1(12.16, 139.67)); + add(entities.treeStump2(17.09, 125.88)); + addEntities(entities.pine(12.81, 133.33, 0)); + addEntities(entities.pine(9.38, 115.92, 0)); + addEntities(entities.pine(11.63, 112.17, 0)); + addEntities(entities.pine(20.56, 134.58, 0)); + addEntities(entities.pine(21.81, 156.33, 0)); + addEntities(entities.pine5(10.94, 136.04, 0)); + addEntities(entities.pine5(24.44, 158.46, 0)); + addEntities(entities.pine5(22.47, 133.29, 0)); + addEntities(entities.pine5(10.47, 110.38, 0)); + addEntities(entities.pine4(9.72, 133.50, 0)); + addEntities(entities.pine4(23.88, 136.75, 0)); + addEntities(entities.pine4(19.63, 158.33, 0)); + add(entities.treeStump1(13.66, 104.42)); + add(entities.treeStump1(29.03, 100.88)); + add(entities.treeStump1(3.19, 110.88)); + add(entities.treeStump1(35.56, 92.04)); + add(entities.treeStump1(13.13, 88.33)); + add(entities.treeStump1(34.81, 90.54)); + add(entities.treeStump1(38.59, 115.58)); + addWoodenFence(9, 95.5, 5); + addWoodenFence(13.5, 90, 1, false, false, true); + addWoodenFence(13.5, 91, 3); + add(entities.lanternOn(12.69, 90.08)); + add(entities.lanternOn(8.88, 94.42)); + add(entities.lanternOn(8.97, 90.75)); + add(entities.boxLanterns(12.38, 87.96)).interact = giveLantern; + addEntities(entities.pine(72.63, 153.08, 0)); + addEntities(entities.pine(70.41, 157.92, 0)); + + addEntities(entities.pine5(4.84, 125.75, 0)); + addEntities(entities.pine5(30.91, 154.46, 0)); + addEntities(entities.pine5(42.56, 155.46, 0)); + addEntities(entities.pine5(31.88, 106.21, 0)); + addEntities(entities.pine5(1.28, 128.04, 0)); + addEntities(entities.pine5(15.59, 87.58, 0)); + addEntities(entities.pine5(37.41, 94.33, 0)); + addEntities(entities.pine5(29.63, 84.88, 0)); + addEntities(entities.pine5(13.41, 101.63, 0)); + addEntities(entities.pine5(4.25, 108.21, 0)); + addEntities(entities.pine5(27.28, 115.88, 0)); + addEntities(entities.pine5(36.00, 139.08, 0)); + addEntities(entities.pine5(36.41, 61.08, 0)); + addEntities(entities.pine5(26.69, 57.04, 0)); + addEntities(entities.pine5(6.28, 54.46, 0)); + addEntities(entities.pine5(7.22, 43.50, 0)); + addEntities(entities.pine5(6.41, 30.54, 0)); + addEntities(entities.pine5(7.88, 14.13, 0)); + addEntities(entities.pine5(132.75, 38.08, 0)); + addEntities(entities.pine5(148.16, 21.42, 0)); + addEntities(entities.pine5(157.44, 14.13, 0)); + addEntities(entities.pine5(154.25, 32.38, 0)); + + addEntities(entities.tree5(41.69, 85.00, 0)); + addEntities(entities.tree5(24.91, 79.29, 1)); + addEntities(entities.tree5(24.06, 68.17, 2)); + addEntities(entities.tree5(10.00, 62.00, 0)); + addEntities(entities.tree5(8.94, 47.38, 1)); + addEntities(entities.tree5(33.06, 56.04, 2)); + addEntities(entities.tree5(32.94, 35.21, 0)); + addEntities(entities.tree5(39.88, 15.58, 1)); + addEntities(entities.tree5(42.38, 29.96, 2)); + addEntities(entities.tree5(69.53, 30.46, 0)); + addEntities(entities.tree5(85.94, 21.08, 1)); + addEntities(entities.tree5(84.38, 23.33, 2)); + addEntities(entities.tree5(103.91, 22.46, 0)); + addEntities(entities.tree5(110.50, 33.71, 1)); + addEntities(entities.tree5(103.63, 46.04, 2)); + addEntities(entities.tree5(62.81, 46.25, 0)); + + addEntities(entities.tree4(136.78, 8.79, 0)); + addEntities(entities.tree4(155.53, 26.58, 1)); + addEntities(entities.tree4(127.63, 38.21, 2)); + addEntities(entities.tree4(117.03, 8.42, 0)); + addEntities(entities.tree4(109.47, 25.83, 1)); + addEntities(entities.tree4(100.47, 20.96, 2)); + addEntities(entities.tree4(108.94, 1.83, 0)); + addEntities(entities.tree4(76.25, 15.42, 1)); + addEntities(entities.tree4(77.81, 5.88, 2)); + addEntities(entities.tree4(42.78, 15.25, 0)); + addEntities(entities.tree4(27.84, 28.21, 1)); + addEntities(entities.tree4(28.25, 43.75, 2)); + addEntities(entities.tree4(10.66, 46.75, 0)); + addEntities(entities.tree4(29.34, 66.25, 1)); + addEntities(entities.tree4(25.59, 84.79, 2)); + addEntities(entities.pine4(4.81, 59.46, 0)); + addEntities(entities.pine4(12.88, 85.58, 0)); + addEntities(entities.pine4(10.81, 99.63, 0)); + addEntities(entities.pine4(19.47, 127.13, 0)); + addEntities(entities.pine4(3.19, 130.63, 0)); + addEntities(entities.pine4(11.91, 148.50, 0)); + addEntities(entities.pine4(28.06, 155.58, 0)); + addEntities(entities.pine4(39.81, 147.38, 0)); + addEntities(entities.pine4(40.75, 132.83, 0)); + addEntities(entities.pine4(25.63, 113.83, 0)); + addEntities(entities.pine4(36.47, 102.96, 0)); + addEntities(entities.pine4(5.75, 109.96, 0)); + addEntities(entities.pine(5.66, 156.04, 0)); + addEntities(entities.pine(8.25, 158.83, 0)); + addEntities(entities.pine(37.38, 158.75, 0)); + addEntities(entities.pine(0.75, 139.38, 0)); + addEntities(entities.pine5(2.00, 137.63, 0)); + addEntities(entities.pine4(10.16, 156.88, 0)); + addEntities(entities.pine4(35.28, 157.71, 0)); + addEntities(entities.pine5(34.09, 148.13, 0)); + addEntities(entities.pine3(22.28, 143.50, 0)); + addEntities(entities.pine3(2.09, 141.83, 0)); + addEntities(entities.pine3(42.22, 121.67, 0)); + addEntities(entities.pine3(29.94, 121.00, 0)); + addEntities(entities.pine3(16.47, 120.25, 0)); + addEntities(entities.pine3(38.50, 105.08, 0)); + addEntities(entities.pine3(15.38, 103.58, 0)); + addEntities(entities.pine3(41.34, 94.21, 0)); + addEntities(entities.pine3(27.84, 87.21, 0)); + addEntities(entities.pine3(25.63, 58.58, 0)); + addEntities(entities.pine3(6.66, 15.96, 0)); + addEntities(entities.pine3(1.47, 39.46, 0)); + addEntities(entities.pine3(28.63, 6.08, 0)); + add(entities.rock(15.78, 34.00)); + add(entities.rock(32.63, 25.42)); + add(entities.rock(8.91, 7.29)); + add(entities.rock(51.63, 13.17)); + add(entities.rock(86.00, 22.29)); + add(entities.rock(104.91, 7.92)); + add(entities.rock(108.53, 24.21)); + add(entities.rock(98.63, 38.04)); + add(entities.rock(117.22, 24.63)); + + add(createSignWithText(70.5, 70.5, 'Pony Town', ' Pony Town\n[under construction]', entities.sign)); + + addEntities(createToyStash(47.00, 55.00)); + + addEntities(entities.pine3(72.78, 64.13, 0)); + + const addCat = (x: number, y: number) => { + const entity = add(entities.cat(x, y)); + let delay = 5; + let boopDelay = 5; + let hideDelay = 5; + let hidden = false; + + entity.boopY = -0.1; + entity.boop = () => { + if (!hidden && boopDelay < 0) { + setTimeout(() => sayToAll(entity, '😠', '😠', MessageType.Thinking, {}), 500); + boopDelay = random(5, 10, true); + } + }; + + entity.serverUpdate = delta => { + delay -= delta; + boopDelay -= delta; + hideDelay -= delta; + + if (hideDelay < 0 && delay < 0) { + if (hidden) { + hidden = false; + setEntityAnimation(entity, CatAnimation.Enter); + hideDelay = random(30, 60, true); + delay = random(2, 4, true); + } else { + hidden = true; + setEntityAnimation(entity, CatAnimation.Exit); + hideDelay = random(15, 30, true); + } + } else if (!hidden && delay < 0) { + const rand = Math.random(); + + if (rand < 0.1) { + sayToAll(entity, 'meow', 'meow', MessageType.System, {}); + delay = random(2, 4, true); + } else if (rand < 0.5) { + setEntityAnimation(entity, CatAnimation.Wag); + delay = random(2, 4, true); + } else { + setEntityAnimation(entity, CatAnimation.Blink); + delay = random(2, 4, true); + } + } + }; + }; + + add(createSign(70, 61.5, 'Letter Sign', give(entities.letter.type, `Here's your letter!`), entities.sign)); + + add(entities.mistletoe(43.00, 48.00)); + add(entities.mistletoe(78.00, 85.70)); + add(entities.fence3(46.50, 53.00)); + add(entities.fence3(65.00, 71.00)); + add(entities.fence3(64.00, 76.00)); + add(entities.fence3(55.00, 76.50)); + add(entities.fence2(92.00, 96.00)); + add(entities.fence2(85.00, 97.00)); + add(entities.fence1(48.00, 75.00)); + + // pine trees + add(entities.pine1(48.50, 65.50)); + add(entities.pine2(51.00, 65.00)); + + addEntities(entities.pine3(52.00, 63.00, 0)); + addEntities(entities.pine4(53.00, 67.00, 0)); + addEntities(entities.pine5(43.00, 62.00, 0)); + addEntities(entities.pine(48.50, 63.00, 0)); + addEntities(entities.pine(46.00, 67.00, 0)); + addEntities(entities.pine(42.00, 69.00, 0)); + addEntities(entities.pine3(113.00, 104.00, 0)); + add(createCookieTable2(74.00, 66.00)); + + addEntities(entities.pine(93.00, 53.00, 0)); + addEntities(entities.pine(97.00, 57.00, 0)); + addEntities(entities.pine(94.00, 62.00, 0)); + addEntities(entities.pine(64.00, 91.00, 0)); + addEntities(entities.pine(59.00, 95.00, 0)); + addEntities(entities.pine(63.00, 98.00, 0)); + + // small trees + add(entities.trees1[0](51.50, 69.00)); + add(entities.trees1[1](75.30, 72.20)); + add(entities.trees1[2](93.00, 67.00)); + add(entities.trees1[0](87.60, 47.00)); + add(entities.trees2[0](71.70, 78.70)); + add(entities.trees2[1](94.80, 94.60)); + add(entities.trees2[2](87.00, 52.70)); + add(entities.trees3[2](87.50, 45.50)); + add(entities.trees3[0](88.30, 82.50)); + add(entities.trees3[2](95.50, 94.00)); + add(entities.trees3[0](84.50, 97.50)); + add(entities.trees3[1](74.00, 83.80)); + add(entities.trees3[2](46.00, 77.50)); + + addEntities(entities.tree4(61.70, 70.50, 0)); + addEntities(entities.tree4(47.50, 74.50, 1)); + addEntities(entities.tree4(69.00, 53.50, 2)); + addEntities(entities.tree4(88.50, 57.70, 0)); + addEntities(entities.tree4(83.50, 64.50, 1)); + addEntities(entities.tree4(76.00, 82.00, 2)); + addEntities(entities.tree4(90.00, 82.70, 1)); + addEntities(entities.tree4(95.00, 80.00, 2)); + addEntities(entities.tree4(97.00, 88.00, 0)); + addEntities(entities.tree4(83.50, 96.50, 0)); + addEntities(entities.tree4(84.00, 91.00, 1)); + addEntities(entities.tree5(86.00, 46.50, 0)); + addEntities(entities.tree5(84.00, 55.00, 1)); + addEntities(entities.tree5(63.00, 71.60, 1)); + addEntities(entities.tree5(45.00, 76.00, 0)); + addEntities(entities.tree5(82.00, 80.50, 0)); + addEntities(entities.tree5(89.00, 81.50, 2)); + addEntities(entities.tree5(98.00, 85.50, 2)); + addEntities(entities.tree5(90.00, 95.00, 0)); + addEntities(entities.tree5(78.00, 90.50, 0)); + + // trees - top + addTree(45.00, 45.00, 1); + addTree(42.00, 47.00, 2 + 4, false, isHalloween); + addTree(48.00, 50.00, 0 + 8, true, true); + addTree(82.00, 53.00, 1); + + addEntities(entities.tree4(71.50, 47.00, 0)); + addEntities(entities.tree5(70.00, 46.00, 1)); + add(entities.trees2[2](68.50, 48.00)); + + // trees - bottom right + addTree(83.00, 87.00, 1); + addTree(85.00, 82.00, 0 + 8, isHalloween); + addTree(87.00, 90.00, 1 + 4); + addTree(92.00, 85.00, 2, true, true); + addTree(79.00, 85.00, 3 + 4); + addTree(81.00, 92.00, 0 + 8, isHalloween, isHalloween); + addTree(90.00, 79.00, 1 + 4); + addTree(94.00, 76.00, 2); + addTree(96.00, 87.00, 3 + 4); + addTree(93.00, 92.00, 0 + 8, isHalloween); + addTree(97.00, 82.00, 1 + 4, false, isHalloween); + + // tree stumps - right + add(entities.treeStump2(91.50, 68.50)); + add(entities.treeStump1(92.50, 66.50)); + add(entities.treeStump2(88.50, 64.50)); + + // trees - 80x80 + addTree(102.71, 82.41, 0); + addTree(102.09, 89.41, 1 + 4); + addTree(108.71, 82.37, 2 + 8); + addTree(111.37, 86.58, 0); + addTree(116.65, 95.12, 1 + 8); + addTree(118.43, 98.41, 2); + addTree(104.59, 106.54, 0 + 4); + addTree(101.12, 110.79, 1 + 8); + addTree(107.28, 114.20, 2, isHalloween); + addTree(119.62, 114.91, 0); + addTree(96.53, 94.95, 1 + 4); + addTree(94.75, 102.37, 2 + 4); + addTree(81.81, 112.12, 0 + 8, isHalloween); + addTree(88.03, 115.83, 1); + addTree(111.84, 118.62, 2 + 4); + addTree(118.78, 47.50, 2 + 8); + + // top right + addTree(135.00, 66.90, 1); + addTree(156.38, 75.17, 0); + addTree(149.38, 78.33, 1 + 4, isHalloween); + addTree(151.13, 81.33, 2); + addTree(156.88, 86.58, 0 + 4); + addTree(152.31, 88.83, 1 + 8, isHalloween, isHalloween); + addTree(155.50, 90.17, 2); + addTree(140.88, 83.92, 0); + addTree(147.56, 95.00, 1 + 8); + + addEntities(entities.pine(141.13, 45.67, 0)); + addEntities(entities.pine(138.31, 52.33, 0)); + addEntities(entities.pine(148.25, 49.00, 0)); + addEntities(entities.pine(155.06, 46.17, 0)); + addEntities(entities.pine(157.38, 49.17, 0)); + addEntities(entities.pine(150.56, 60.42, 0)); + addEntities(entities.tree5(147.88, 80.42, 2)); + addEntities(entities.tree5(154.31, 93.33, 0)); + addEntities(entities.pine5(155.72, 52.13, 0)); + addEntities(entities.pine5(139.66, 54.50, 0)); + addEntities(entities.pine4(149.72, 51.04, 0)); + addEntities(entities.pine4(142.09, 57.75, 0)); + addEntities(entities.pine4(153.34, 48.21, 0)); + addEntities(entities.pine3(147.63, 53.96, 0)); + addEntities(entities.pine3(143.47, 44.25, 0)); + addEntities(entities.pine3(136.06, 53.88, 0)); + add(entities.treeStump1(136.91, 45.67)); + add(entities.treeStump2(132.69, 51.63)); + add(entities.treeStump1(131.41, 48.08)); + add(entities.lanternOn(136.09, 67.29)); + add(entities.lanternOn(132.38, 68.38)); + add(entities.lanternOn(138.53, 67.75)); + add(entities.rock(151.16, 63.21)); + add(entities.rock(148.66, 79.38)); + add(entities.treeStump1(154.72, 54.83)); + add(entities.treeStump2(152.22, 63.29)); + add(entities.treeStump1(149.84, 67.58)); + add(entities.treeStump2(152.81, 70.54)); + add(entities.treeStump1(154.06, 64.63)); + add(entities.treeStump2(156.50, 67.38)); + add(entities.treeStump1(156.56, 61.29)); + add(entities.treeStump2(155.44, 60.38)); + add(entities.treeStump1(157.41, 56.67)); + + // forest + addTree(122.69, 106.50, 0); + addTree(131.69, 105.58, 1); + addTree(130.94, 122.33, 2 + 4, isHalloween); + addTree(132.69, 118.08, 0 + 8); + addTree(124.38, 138.25, 1 + 8); + addTree(134.94, 137.42, 2, isHalloween, isHalloween); + addTree(131.94, 134.25, 0); + addTree(138.25, 130.92, 1 + 8); + addTree(121.69, 121.00, 2 + 4); + addTree(111.94, 126.83, 0 + 4, isHalloween); + addTree(115.06, 125.25, 1 + 8); + addTree(114.44, 129.58, 2); + addTree(120.69, 136.25, 0); + addTree(125.00, 98.92, 1 + 8); + addTree(128.56, 93.92, 2); + addTree(140.88, 111.33, 0); + addTree(137.94, 114.83, 1); + addTree(147.19, 113.58, 2, isHalloween); + addTree(143.94, 118.17, 0 + 4); + addTree(149.94, 122.92, 1 + 8); + addTree(148.13, 125.67, 2, isHalloween, isHalloween); + addTree(144.19, 136.08, 0); + addTree(152.13, 130.33, 1 + 4); + addTree(155.88, 107.25, 2 + 8); + addTree(132.31, 148.75, 0, isHalloween); + addEntities(entities.tree5(125.81, 109.00, 0)); + addEntities(entities.tree5(119.31, 122.75, 1)); + addEntities(entities.tree5(141.88, 114.58, 2)); + addEntities(entities.tree5(139.13, 138.50, 0)); + addEntities(entities.tree5(150.25, 115.92, 1)); + addEntities(entities.tree5(118.69, 138.50, 2)); + addEntities(entities.tree5(133.56, 121.67, 0)); + addEntities(entities.tree4(130.50, 97.17, 0)); + addEntities(entities.tree4(116.00, 116.08, 1)); + addEntities(entities.tree4(133.44, 108.00, 2)); + addEntities(entities.tree4(110.50, 130.33, 0)); + addEntities(entities.tree4(145.31, 120.25, 1)); + addEntities(entities.tree4(149.40, 129.33, 2)); + addEntities(entities.tree4(141.81, 85.67, 0)); + addEntities(entities.tree4(158.19, 108.67, 1)); + add(entities.tree3(123.50, 122.50)); + add(entities.tree3(139.63, 116.25)); + add(entities.tree3(112.75, 130.83)); + add(entities.tree3(156.63, 92.42)); + add(entities.tree3(137.00, 138.08)); + add(entities.rock(133.81, 137.54)); + add(entities.rock(122.97, 139.54)); + add(entities.rock(130.22, 121.79)); + add(entities.rock(126.25, 109.54)); + add(entities.rock(151.00, 129.83)); + add(entities.treeStump1(141.53, 117.29)); + add(entities.treeStump2(139.31, 132.38)); + add(entities.treeStump1(117.78, 125.63)); + add(entities.treeStump2(125.66, 111.50)); + add(entities.treeStump1(123.06, 113.79)); + add(entities.treeStump2(124.13, 117.71)); + add(entities.treeStump1(129.25, 99.88)); + add(entities.lanternOn(52.97, 134.75)); + add(entities.lanternOn(49.19, 138.25)); + add(entities.lanternOn(49.25, 142.08)); + add(entities.lanternOn(54.97, 142.96)); + add(entities.lanternOn(45.53, 146.04)); + + // graveyard + addStoneWall(142, 98, 2, false, true); // left (1) + addStoneWall(142, 104, 2, false, false, true); // left (2) + addStoneWall(142, 98, 5); // top + addStoneWall(152, 98, 5, false, true, true); // right + addStoneWall(142, 108, 5); // bottom + + add(entities.lanternOnWall(148.00, 98.20)); + add(entities.lanternOnWall(144.00, 108.20)); + add(entities.lanternOnWall(152.00, 108.20)); + addCat(149, 108.19); + + // forest path + add(entities.boxLanterns(52.50, 133.96)).interact = giveLantern; + add(entities.boxLanterns(124.53, 112.67)).interact = giveLantern; + add(entities.boxLanterns(142.59, 118.54)).interact = giveLantern; + + addWoodenFence(132, 108, 6, false); + addWoodenFence(125, 121, 4, false); + addWoodenFence(131, 130, 3, false); + addWoodenFence(125, 132, 4, false); + addWoodenFence(118, 100, 6); + addWoodenFence(117, 105, 3); + + add(entities.lanternOn(123.59, 112.67)); + add(entities.lanternOn(124.03, 116.42)); + add(entities.lanternOn(141.50, 118.25)); + add(entities.lanternOn(138.25, 116.63)); + add(entities.lanternOn(145.63, 121.33)); + add(entities.lanternOn(135.78, 122.00)); + add(entities.lanternOn(137.78, 124.17)); + add(entities.lanternOn(138.53, 120.54)); + add(entities.lanternOn(143.44, 119.04)); + add(entities.lanternOn(94.41, 132.92)); + add(entities.lanternOn(95.53, 136.17)); + add(entities.lanternOn(91.31, 133.71)); + add(entities.lanternOn(90.56, 136.58)); + add(entities.lanternOn(93.34, 138.17)); + + // bottom left + addEntities(entities.pine(45.00, 126.33, 0)); + addEntities(entities.pine(52.69, 130.42, 0)); + addEntities(entities.pine(49.81, 133.58, 0)); + addEntities(entities.pine(46.75, 140.92, 0)); + addEntities(entities.pine(57.56, 143.92, 0)); + addEntities(entities.pine(52.38, 150.42, 0)); + addEntities(entities.pine(41.50, 144.83, 0)); + addEntities(entities.pine(44.06, 151.17, 0)); + addEntities(entities.pine(95.56, 122.83, 0)); + addEntities(entities.pine(105.81, 120.92, 0)); + addEntities(entities.pine(103.19, 131.00, 0)); + addEntities(entities.pine(100.81, 129.67, 0)); + addEntities(entities.pine(97.88, 133.33, 0)); + addEntities(entities.pine(108.13, 137.92, 0)); + addEntities(entities.pine(105.13, 139.50, 0)); + addEntities(entities.pine5(45.31, 143.75, 0)); + addEntities(entities.pine5(53.94, 153.50, 0)); + addEntities(entities.pine5(41.56, 130.83, 0)); + addEntities(entities.pine5(58.63, 132.58, 0)); + addEntities(entities.pine5(60.06, 147.17, 0)); + addEntities(entities.pine5(43.69, 133.25, 0)); + addEntities(entities.pine4(55.63, 133.92, 0)); + addEntities(entities.pine4(50.31, 152.83, 0)); + addEntities(entities.pine4(62.31, 143.92, 0)); + addEntities(entities.pine4(98.50, 139.08, 0)); + addEntities(entities.pine4(107.38, 123.75, 0)); + addEntities(entities.pine4(89.63, 125.33, 0)); + addEntities(entities.pine4(64.19, 149.00, 0)); + addEntities(entities.pine3(57.00, 127.75, 0)); + addEntities(entities.pine3(62.31, 151.17, 0)); + addEntities(entities.pine3(51.00, 155.58, 0)); + addEntities(entities.pine3(110.25, 140.25, 0)); + addEntities(entities.pine3(90.06, 130.33, 0)); + addEntities(entities.pine3(105.75, 124.75, 0)); + + // bottom center + addStoneWall(93, 141.5, 3); + addStoneWall(101, 141.5, 3); + addStoneWall(90, 146, 3); + addStoneWall(103, 146.5, 3); + + // bottom right + addWoodenFence(118, 140, 4); + addWoodenFence(121, 145, 4); + addWoodenFence(136, 140, 6); + addWoodenFence(134, 144, 6); + addWoodenFence(143, 144.5, 2); + + // pumpkin field + addEntities(entities.pine5(100.38, 120.58, 0)); + addEntities(entities.pine5(93.31, 128.58, 0)); + addEntities(entities.pine5(100.00, 137.75, 0)); + + addWoodenFence(76, 120, 12); // top + addWoodenFence(76, 120, 5, false, true); // left 1 + addWoodenFence(76, 129, 7, false, false); // left 2 + addWoodenFence(88, 120, 10, false, true); // right 1 + addWoodenFence(88, 130, 1, true, true, true); // right 2 + addWoodenFence(89, 130, 7, false, false, true); // right 3 + addWoodenFence(76, 136, 6); // bottom 1 + addWoodenFence(82, 136, 1, false, true, true); // bottom 2 + addWoodenFence(82, 137, 7); // bottom 3 + + addWoodenFence(82, 147, 6); // top + addWoodenFence(75, 147, 4); // top + addWoodenFence(88, 147, 10, false, true); // right + addWoodenFence(75, 157, 13, true, false, true); // bottom + addWoodenFence(75, 147, 10, false, true, true); // left + + if (true) { + add(entities.pumpkin(77.44, 124.67)); + add(entities.pumpkin(78.31, 125.75)); + add(entities.pumpkin(80.63, 124.00)); + add(entities.pumpkin(84.25, 123.83)); + add(entities.pumpkin(82.81, 126.67)); + add(entities.pumpkin(80.75, 129.17)); + add(entities.pumpkin(82.19, 128.42)); + add(entities.pumpkin(82.38, 129.42)); + add(entities.pumpkin(81.56, 129.67)); + add(entities.pumpkin(76.94, 131.25)); + add(entities.pumpkin(78.25, 134.67)); + add(entities.pumpkin(80.56, 134.00)); + add(entities.pumpkin(84.88, 124.17)); + add(entities.pumpkin(83.81, 124.83)); + add(entities.pumpkin(84.56, 132.50)); + add(entities.pumpkin(84.19, 133.83)); + add(entities.pumpkin(84.75, 135.83)); + add(entities.pumpkin(87.13, 132.50)); + add(entities.pumpkin(86.56, 134.00)); + add(entities.pumpkin(87.70, 134.50)); + add(entities.pumpkin(88.31, 135.67)); + add(entities.pumpkin(82.25, 122.17)); + } + + add(entities.treeStump1(91.25, 123.67)); + add(entities.treeStump1(94.94, 126.33)); + add(entities.treeStump1(92.56, 131.42)); + + // rock circle 2 + add(entities.rock(111.63, 152.33)); + add(entities.rock(107.63, 147.92)); + add(entities.rock(105.31, 152.33)); + add(entities.rock(108.50, 156.33)); + add(entities.rock(115.44, 155.67)); + add(entities.rock(115.38, 149.67)); + add(entities.rock(113.69, 147.33)); + add(entities.rock(105.63, 150.50)); + add(entities.rock(112.88, 157.25)); + add(entities.rock(117.38, 152.83)); + addEntities(entities.pine3(106.38, 148.58, 0)); + addEntities(entities.pine4(115.94, 148.25, 0)); + addEntities(entities.pine4(114.13, 157.92, 0)); + addEntities(entities.pine5(104.25, 150.50, 0)); + + addEntities(entities.tree5(85.28, 116.79, 1)); + addEntities(entities.tree5(91.03, 118.33, 2)); + addEntities(entities.tree5(104.46, 115.70, 0)); + addEntities(entities.tree5(112.96, 107.33, 1)); + addEntities(entities.tree5(100.84, 92.20, 2)); + addEntities(entities.tree5(107.62, 84.29, 0)); + addEntities(entities.tree5(97.43, 64.04, 1)); + addEntities(entities.tree5(116.28, 44.75, 2)); + addEntities(entities.tree5(118.68, 70.62, 0)); + addEntities(entities.tree4(89.81, 103.54, 0)); + addEntities(entities.tree4(110.78, 92.29, 1)); + addEntities(entities.tree4(117.09, 69.66, 2)); + + add(entities.trees3[0](76.37, 118.08)); + + addEntities(entities.pine(92.28, 104.58, 0)); + addEntities(entities.pine(116.15, 105.75, 0)); + addEntities(entities.pine(112.12, 103.45, 0)); + addEntities(entities.pine(118.93, 88.95, 0)); + addEntities(entities.pine(111.34, 77.33, 0)); + addEntities(entities.pine(114.62, 75.66, 0)); + addEntities(entities.pine(98.43, 117.79, 0)); + addEntities(entities.pine(42.59, 115.91, 0)); + addEntities(entities.pine(47.09, 117.70, 0)); + addEntities(entities.pine5(44.59, 104.50, 0)); + addEntities(entities.pine4(95.34, 106.58, 0)); + addEntities(entities.pine3(47.78, 104.75, 0)); + addEntities(entities.pine3(58.78, 110.04, 0)); + + add(entities.pine2(57.50, 109.00)); + add(entities.fence3(86.43, 100.58)); + add(entities.fence3(93.96, 100.50)); + + add(entities.treeStump1(105.31, 89.12)); + add(entities.treeStump2(108.25, 88.16)); + add(entities.treeStump1(113.37, 91.54)); + add(entities.treeStump2(114.12, 95.45)); + add(entities.treeStump1(109.53, 100.58)); + add(entities.treeStump2(107.46, 100.62)); + add(entities.treeStump1(99.34, 101.04)); + add(entities.treeStump2(101.12, 95.12)); + add(entities.treeStump1(102.43, 91.79)); + add(entities.treeStump2(80.18, 113.66)); + add(entities.treeStump1(81.81, 114.95)); + add(entities.treeStump2(77.62, 118.58)); + add(entities.treeStump1(46.93, 106.70)); + add(entities.treeStump2(51.81, 117.41)); + add(entities.treeStump1(53.50, 115.58)); + add(entities.treeStump2(117.03, 48.66)); + + add(entities.rock(104.81, 88.83)); + add(entities.rock(114.53, 95.25)); + add(entities.rock(103.96, 106.25)); + add(entities.rock(84.50, 116.21)); + add(entities.rock(54.25, 116.58)); + add(entities.rock(118.06, 49.20)); + add(entities.rock(104.56, 47.65)); + + add(entities.boxLanterns(113.45, 95.16)).interact = giveLantern; + add(entities.box(105.90, 88.81)); + add(entities.lanternOn(113.31, 95.48)); + add(entities.lanternOn(112.64, 94.42)); + add(entities.lanternOn(105.98, 89.17)); + add(entities.lanternOn(106.81, 100.67)); + add(entities.lanternOn(102.94, 92.25)); + + add(entities.fence1(80.03, 97.00)); + add(entities.fence2(74.81, 100.79)); + + // rocks + add(entities.rock(46.00, 50.00)); + add(entities.rock(48.50, 81.50)); + add(entities.rock(45.50, 84.50)); + add(entities.rock(44.50, 88.50)); + add(entities.rock(46.50, 93.50)); + add(entities.rock(50.50, 94.50)); + add(entities.rock(55.50, 93.50)); + add(entities.rock(56.50, 91.50)); + add(entities.rock(56.50, 86.50)); + add(entities.rock(55.50, 83.50)); + add(entities.rock(52.50, 81.50)); + add(entities.rock(50.50, 87.50)); + add(entities.rock(70.50, 62.30)); + add(entities.rock(74.50, 67.50)); + add(entities.rock(86.50, 52.50)); + add(entities.rock(91.50, 55.50)); + add(entities.rock(88.50, 58.50)); + add(entities.rock(52.00, 68.50)); + add(entities.rock(52.50, 69.50)); + add(entities.rock(61.50, 71.50)); + add(entities.rock(83.50, 91.50)); + add(entities.rock(90.50, 83.50)); + + // pumpkins + add(entities.pumpkin(74.50, 88.50)); + add(entities.pumpkin(74.00, 90.00)); + add(entities.pumpkin(68.00, 87.00)); + add(entities.pumpkin(69.00, 93.00)); + add(entities.pumpkin(68.00, 95.00)); + add(entities.pumpkin(69.70, 61.80)); + + addCat(67.06, 71); + + // lights + + const addJacko = createAddLight(world, map, entities.jacko); + const addTorch = createAddLight(world, map, entities.torch); + + // jack-o-lanterns + // top left + addJacko(45, 48); + + add(entities.lanternOn(66.75, 62)); + add(entities.lanternOn(67.7, 62.8)); + add(entities.lanternOn(69.5, 62.2)); + add(entities.boxLanterns(67.6, 62.6)).interact = giveLantern; + add(entities.boxLanterns(66.5, 63.3)).interact = giveLantern; + + add(entities.treeStump1(70, 62.5)); + + // top left + addTorch(50.00, 54.00); + addTorch(46.50, 55.00); + // top + addTorch(76.00, 53.00); + addTorch(63.00, 50.00); + addTorch(65.90, 64.00); + addTorch(68.00, 47.00); + addTorch(71.00, 50.00); + addTorch(70.00, 57.00); + addTorch(68.30, 62.00); + addTorch(70.70, 61.50); + // top right + addTorch(92.00, 59.00); + addTorch(88.00, 53.00); + addTorch(94.00, 45.00); + addTorch(89.00, 45.00); + addTorch(92.00, 49.00); + // left + addTorch(44.00, 65.00); + addTorch(49.00, 69.00); + addTorch(55.00, 65.00); + // center + addTorch(70.00, 69.00); + // right + addTorch(84.00, 65.00); + addTorch(80.00, 68.00); + addTorch(86.00, 71.00); + addTorch(98.00, 66.50); + // bot left + addTorch(47.00, 80.50); + addTorch(43.00, 87.00); + addTorch(49.00, 94.00); + addTorch(57.00, 91.00); + addTorch(56.00, 82.00); + addTorch(49.00, 87.00); + addTorch(60.00, 88.00); + // bot + addTorch(72.00, 79.00); + addTorch(68.00, 81.00); + addTorch(69.00, 88.80); + // bot right + addTorch(87.00, 94.00); + addTorch(101.50, 94.00); + addTorch(94.50, 93.80); + addTorch(100.00, 100.00); + addTorch(99.00, 95.00); + // 80x80 + addTorch(47.81, 106.50); + addTorch(52.00, 110.85); + addTorch(56.75, 116.96); + addTorch(52.03, 116.88); + addTorch(59.88, 100.50); + addTorch(63.81, 101.67); + addTorch(60.53, 104.63); + addTorch(62.00, 102.88); + addTorch(63.47, 106.33); + addTorch(66.97, 104.00); + addTorch(77.22, 116.17); + addTorch(73.13, 100.13); + addTorch(76.78, 100.42); + addTorch(85.53, 105.50); + addTorch(89.84, 106.33); + addTorch(86.78, 109.25); + addTorch(116.34, 110.83); + addTorch(112.00, 107.60); + addTorch(109.94, 111.46); + addTorch(113.78, 113.83); + addTorch(112.97, 110.83); + addTorch(96.09, 114.88); + addTorch(100.09, 50.75); + addTorch(112.59, 45.96); + addTorch(116.03, 47.92); + addTorch(116.94, 59.08); + addTorch(109.06, 64.29); + addTorch(111.94, 69.96); + addTorch(104.94, 71.21); + addTorch(102.91, 49.42); + addTorch(115.00, 79.79); + addTorch(116.63, 77.46); + addTorch(113.19, 78.17); + addTorch(64.36, 103.92); + // 15x15 + addTorch(117.09, 140.33); + addTorch(120.13, 144.79); + addTorch(123.34, 140.13); + addTorch(126.31, 144.79); + addTorch(134.00, 143.75); + addTorch(133.88, 139.00); + addTorch(131.66, 135.79); + addTorch(126.03, 137.58); + addTorch(125.94, 133.08); + addTorch(130.06, 129.50); + addTorch(125.81, 128.67); + addTorch(125.88, 123.29); + addTorch(130.09, 123.08); + addTorch(126.19, 118.75); + addTorch(127.22, 114.00); + addTorch(132.54, 111.75); + addTorch(127.13, 109.50); + addTorch(130.72, 106.33); + addTorch(121.47, 106.10); + addTorch(123.69, 100.71); + addTorch(126.69, 101.96); + addTorch(119.28, 100.58); + addTorch(117.89, 104.67); + addTorch(138.34, 139.08); + addTorch(139.69, 144.17); + addTorch(142.78, 139.75); + addTorch(145.16, 143.96); + addTorch(148.34, 138.88); + addTorch(148.53, 143.79); + addTorch(142.50, 144.42); + addTorch(138.34, 147.29); + addTorch(140.72, 147.71); + addTorch(143.81, 148.00); + addTorch(142.91, 151.58); + addTorch(139.47, 151.08); + addTorch(136.34, 150.79); + addTorch(136.41, 144.88); + addTorch(140.72, 155.04); + addTorch(152.69, 152.17); + addTorch(155.78, 152.42); + addTorch(153.84, 155.54); + addTorch(150.88, 131.13); + addTorch(153.38, 130.83); + addTorch(113.78, 146.13); + addTorch(110.94, 141.38); + addTorch(107.06, 141.83); + addTorch(107.13, 145.71); + addTorch(100.63, 141.79); + addTorch(101.97, 146.08); + addTorch(96.44, 145.92); + addTorch(98.41, 141.71); + addTorch(92.63, 141.79); + addTorch(90.28, 145.83); + addTorch(88.22, 140.79); + addTorch(88.03, 138.17); + addTorch(83.50, 138.33); + addTorch(79.59, 136.88); + addTorch(75.69, 136.75); + addTorch(75.19, 132.13); + addTorch(75.22, 121.33); + addTorch(78.63, 111.50); + addTorch(81.69, 105.92); + addTorch(82.78, 100.71); + addTorch(69.84, 115.58); + addTorch(68.94, 119.21); + addTorch(69.03, 122.75); + addTorch(69.00, 126.42); + addTorch(69.25, 131.79); + addTorch(69.59, 136.83); + addTorch(86.09, 145.33); + addTorch(82.09, 145.00); + addTorch(78.16, 144.79); + addTorch(73.78, 144.96); + addTorch(74.41, 149.21); + addTorch(66.16, 142.13); + addTorch(69.09, 140.71); + addTorch(69.59, 144.63); + addTorch(71.13, 147.83); + addTorch(58.22, 149.33); + addTorch(61.09, 150.92); + addTorch(63.88, 140.50); + addTorch(59.91, 138.63); + addTorch(56.72, 136.50); + addTorch(124.56, 46.29); + addTorch(127.19, 48.63); + addTorch(123.91, 50.33); + addTorch(126.97, 52.75); + addTorch(131.50, 46.63); + addTorch(130.56, 50.42); + addTorch(134.09, 49.17); + addTorch(132.59, 53.79); + addTorch(126.63, 56.58); + addTorch(129.38, 54.63); + addTorch(151.78, 62.96); + addTorch(155.75, 63.33); + addTorch(149.00, 66.21); + addTorch(148.84, 70.75); + addTorch(153.31, 71.71); + addTorch(157.34, 68.79); + addTorch(157.75, 65.54); + addTorch(151.16, 69.21); + addTorch(75.31, 124.79); + addTorch(75.47, 128.88); + // cookie stands lights + addTorch(33.19 + 0.25, 26.29 + 9.5); + addTorch(65.78, 114.08); + addTorch(89.78, 132.17); + addTorch(124.78, 147.25); + addTorch(132.97, 150.29); + addTorch(144.13, 98.58); + addTorch(120.34, 48.38); + addTorch(144.62, 125.20); + + // top left hill + addTorch(22.40, 45.12); + addTorch(18.19, 40.75); + addTorch(21.13, 35.96); + addTorch(20.91, 30.63); + addTorch(14.22, 30.54); + addTorch(27.78, 30.83); + addTorch(30.16, 34.46); + addTorch(18.13, 24.42); + addTorch(16.31, 17.83); + addTorch(17.69, 13.46); + addTorch(20.88, 10.00); + addTorch(14.22, 10.92); + addTorch(21.13, 16.75); + addTorch(14.09, 22.88); + addTorch(28.03, 12.96); + addTorch(31.19, 8.17); + addTorch(36.38, 10.83); + addTorch(44.56, 8.33); + addTorch(51.28, 11.88); + addTorch(60.13, 9.33); + addTorch(62.91, 11.88); + addTorch(71.00, 8.25); + addTorch(76.28, 12.54); + addTorch(82.66, 9.58); + // center + addTorch(53.09, 71.08); + addTorch(60.78, 70.88); + addTorch(67.19, 76.38); + addTorch(60.53, 76.63); + addTorch(52.38, 76.79); + addTorch(48.13, 75.29); + addTorch(58.97, 55.75); + addTorch(53.75, 51.00); + addTorch(58.81, 42.42); + addTorch(53.97, 40.00); + addTorch(59.59, 37.00); + addTorch(53.34, 30.46); + addTorch(58.09, 30.29); + addTorch(69.84, 38.92); + addTorch(68.09, 41.92); + addTorch(79.78, 39.79); + addTorch(78.19, 43.83); + addTorch(92.19, 39.00); + addTorch(99.25, 39.21); + addTorch(99.19, 45.21); + addTorch(103.19, 36.58); + addTorch(104.63, 41.71); + addTorch(100.00, 58.58); + addTorch(92.22, 30.46); + addTorch(96.06, 28.88); + addTorch(90.88, 24.08); + addTorch(95.34, 22.50); + addTorch(90.81, 17.21); + addTorch(95.88, 15.83); + addTorch(96.28, 10.33); + addTorch(91.25, 8.83); + addTorch(94.34, 2.38); + addTorch(97.16, 6.71); + addTorch(88.78, 4.54); + addTorch(50.69, 20.42); + addTorch(59.38, 20.29); + addTorch(50.28, 29.21); + addTorch(48.72, 32.92); + addTorch(42.66, 36.50); + addTorch(38.19, 31.17); + addTorch(37.53, 37.88); + // top right + addTorch(115.50, 36.17); + addTorch(113.94, 40.13); + addTorch(109.72, 35.71); + addTorch(123.44, 37.00); + addTorch(131.59, 39.75); + addTorch(138.38, 38.13); + addTorch(137.66, 43.00); + addTorch(144.16, 36.17); + addTorch(148.09, 36.71); + addTorch(146.03, 41.00); + addTorch(150.97, 41.00); + addTorch(155.88, 37.67); + addTorch(152.56, 34.83); + addTorch(127.00, 17.88); + addTorch(127.88, 25.21); + addTorch(131.88, 15.21); + addTorch(135.78, 21.92); + addTorch(132.41, 28.33); + addTorch(122.81, 32.38); + addTorch(127.66, 33.33); + addTorch(141.53, 13.67); + addTorch(145.75, 13.75); + addTorch(141.59, 17.25); + addTorch(145.69, 17.71); + addTorch(135.72, 9.29); + addTorch(131.59, 7.63); + addTorch(129.69, 11.71); + // harbor + addTorch(22.50, 75.92); + addTorch(20.34, 70.58); + addTorch(28.66, 70.29); + addTorch(36.72, 70.88); + addTorch(32.41, 77.00); + addTorch(15.03, 67.63); + addTorch(20.03, 63.00); + addTorch(15.59, 60.63); + addTorch(16.88, 53.79); + addTorch(21.75, 54.21); + addTorch(15.47, 76.13); + // south-west forest + addTorch(36.84, 84.25); + addTorch(30.28, 89.29); + addTorch(29.84, 96.92); + addTorch(23.34, 98.54); + addTorch(21.59, 105.33); + addTorch(15.28, 107.83); + addTorch(14.81, 115.33); + addTorch(9.41, 120.79); + addTorch(15.59, 125.63); + addTorch(14.16, 135.13); + addTorch(14.97, 94.67); + addTorch(17.16, 90.21); + addTorch(22.25, 92.71); + + add(createSign(77.06, 60.16, 'Rose sign', give(entities.rose.type, `Here's your rose!`), entities.sign)); + + if (world.featureFlags.test) { + add(createSign(14.5, 70, 'Public Island', (_, client) => goToMap(world, client, 'public-island'), entities.signDebug)); + add(createSignWithText(60.7, 60.2, 'Pickable items', `Click on the item to carry it around`, entities.signDebug)); + add(entities.flower3Pickable(60, 60)).interact = (_, { pony }) => holdItem(pony, entities.flowerPick.type); + add(entities.apple(61, 61)).interact = (_, { pony }) => holdItem(pony, entities.apple.type); + add(entities.appleGreen2(61.3, 61.4)).interact = (_, { pony }) => holdItem(pony, entities.appleGreen2.type); + add(entities.orange(60.3, 61.2)).interact = (_, { pony }) => holdItem(pony, entities.orange.type); + add(entities.grapesPurple[0](60.67, 61.63)).interact = (_, { pony }) => holdItem(pony, entities.grapesPurple[0].type); + } + + if (BETA) { + const objects = [ + entities.fence1, entities.box, entities.boxLanterns, entities.gift3, entities.pumpkin, entities.sign, + entities.rope, + ].map(e => e.type); + + add(createSign(62.7, 58.2, 'Jack-o-Lanterns', give(entities.jackoLanternOn.type, 'Have a lantern'), entities.signDebug)); + add(createSign(62.7, 60.2, 'Pickable objects', (_, client) => holdItem(client.pony, sample(objects)!), entities.signDebug)); + add(createSign(77.0, 69.0, 'Palette', (_, client) => goToMap(world, client, 'palette'), entities.signDebug)); + } + + // new added on Halloween + addEntities(entities.tree4(155.18, 100.04, 1)); + addEntities(entities.tree4(144.97, 96.25, 0)); + addEntities(entities.tree(153.60, 99.54, 2)); + addEntities(entities.pine(157.50, 96.79, 0)); + add(entities.treeStump1(158.03, 110.12)); + add(entities.treeStump1(158.75, 112.91)); + add(entities.treeStump2(157.00, 111.50)); + + // center lakes + add(entities.waterRock1(77.66, 61.67)); + add(entities.waterRock7(76.56, 65.58)); + add(entities.waterRock8(76.97, 65.79)); + add(entities.waterRock10(83.47, 67.63)); + + // sea + add(entities.waterRock1(3.78, 61.96)); + add(entities.waterRock1(12.53, 75.88)); + add(entities.waterRock1(9.38, 80.50)); + add(entities.waterRock1(5.34, 95.42)); + add(entities.waterRock3(4.91, 95.71)); + add(entities.waterRock3(8.44, 85.79)); + add(entities.waterRock3(8.50, 64.00)); + add(entities.waterRock2(3.19, 61.42)); + add(entities.waterRock2(8.97, 86.00)); + add(entities.waterRock2(3.31, 100.88)); + add(entities.waterRock4(4.75, 95.21)); + add(entities.waterRock4(12.44, 77.58)); + add(entities.waterRock4(8.44, 63.54)); + add(entities.waterRock5(3.38, 61.96)); + add(entities.waterRock5(7.22, 90.79)); + add(entities.waterRock6(3.66, 100.25)); + add(entities.waterRock6(12.22, 78.04)); + add(entities.waterRock7(7.97, 63.88)); + add(entities.waterRock7(8.25, 86.38)); + add(entities.waterRock8(9.03, 80.83)); + add(entities.waterRock8(1.31, 101.71)); + add(entities.waterRock9(11.34, 79.17)); + add(entities.waterRock10(9.97, 66.79)); + add(entities.waterRock10(4.25, 94.38)); + add(entities.waterRock11(9.47, 66.88)); + add(entities.waterRock11(12.28, 70.71)); + add(entities.waterRock11(7.00, 91.25)); + + // river + add(entities.waterRock1(114.06, 1.96)); + add(entities.waterRock1(112.13, 17.92)); + add(entities.waterRock1(112.84, 28.38)); + add(entities.waterRock1(111.66, 35.75)); + add(entities.waterRock2(109.15, 16.50)); + add(entities.waterRock2(114.66, 32.50)); + add(entities.waterRock3(112.91, 20.33)); + add(entities.waterRock3(110.38, 9.75)); + add(entities.waterRock4(109.59, 16.75)); + add(entities.waterRock4(113.41, 7.96)); + add(entities.waterRock4(111.63, 35.13)); + add(entities.waterRock5(114.34, 33.04)); + add(entities.waterRock5(110.63, 9.38)); + add(entities.waterRock6(109.63, 16.58)); + add(entities.waterRock6(113.28, 28.58)); + add(entities.waterRock7(114.50, 1.54)); + add(entities.waterRock7(111.97, 35.38)); + add(entities.waterRock8(113.28, 28.17)); + add(entities.waterRock8(111.44, 22.79)); + add(entities.waterRock8(110.72, 9.63)); + add(entities.waterRock9(107.94, 12.75)); + add(entities.waterRock9(113.97, 6.58)); + add(entities.waterRock10(108.38, 12.63)); + add(entities.waterRock10(114.44, 33.46)); + add(entities.waterRock11(112.31, 18.25)); + add(entities.waterRock11(111.34, 12.25)); + add(entities.waterRock11(110.59, 39.75)); + + // river+lake + add(entities.waterRock1(107.75, 45.83)); + add(entities.waterRock1(105.63, 50.92)); + add(entities.waterRock1(118.56, 58.83)); + add(entities.waterRock1(127.59, 59.13)); + add(entities.waterRock1(131.50, 67.58)); + add(entities.waterRock1(124.25, 68.54)); + add(entities.waterRock2(117.44, 53.67)); + add(entities.waterRock2(128.41, 59.17)); + add(entities.waterRock2(105.06, 50.30)); + add(entities.waterRock2(111.56, 43.54)); + add(entities.waterRock3(112.69, 56.08)); + add(entities.waterRock3(105.53, 50.29)); + add(entities.waterRock3(127.88, 59.46)); + add(entities.waterRock3(134.09, 57.88)); + add(entities.waterRock4(111.38, 43.21)); + add(entities.waterRock4(112.97, 55.83)); + add(entities.waterRock4(124.66, 68.88)); + add(entities.waterRock5(110.47, 52.58)); + add(entities.waterRock5(118.16, 58.54)); + add(entities.waterRock5(132.63, 70.75)); + add(entities.waterRock6(108.03, 46.17)); + add(entities.waterRock6(116.94, 54.04)); + add(entities.waterRock6(129.66, 55.92)); + add(entities.waterRock7(109.91, 52.46)); + add(entities.waterRock7(120.31, 53.54)); + add(entities.waterRock7(133.09, 71.08)); + add(entities.waterRock8(132.59, 71.13)); + add(entities.waterRock8(110.22, 52.04)); + add(entities.waterRock8(111.63, 43.00)); + add(entities.waterRock8(110.81, 39.88)); + add(entities.waterRock9(109.81, 48.83)); + add(entities.waterRock10(120.41, 54.08)); + add(entities.waterRock10(131.13, 67.96)); + add(entities.waterRock11(121.75, 64.25)); + add(entities.waterRock10(121.31, 63.60)); + add(entities.waterRock1(121.13, 75.83)); + add(entities.waterRock1(125.47, 78.67)); + add(entities.waterRock1(140.31, 78.96)); + add(entities.waterRock1(146.44, 71.42)); + add(entities.waterRock1(143.47, 50.71)); + add(entities.waterRock1(142.00, 63.75)); + add(entities.waterRock2(139.69, 67.21)); + add(entities.waterRock2(147.75, 61.83)); + add(entities.waterRock2(125.94, 78.92)); + add(entities.waterRock2(140.81, 78.46)); + add(entities.waterRock3(140.06, 67.67)); + add(entities.waterRock3(145.31, 74.92)); + add(entities.waterRock3(137.53, 58.75)); + add(entities.waterRock3(143.84, 50.88)); + add(entities.waterRock3(121.63, 75.42)); + add(entities.waterRock3(133.47, 82.50)); + add(entities.waterRock4(125.94, 78.46)); + add(entities.waterRock4(148.03, 62.17)); + add(entities.waterRock4(143.91, 50.42)); + add(entities.waterRock5(137.81, 59.13)); + add(entities.waterRock5(142.47, 64.04)); + add(entities.waterRock5(146.13, 71.96)); + add(entities.waterRock5(129.16, 80.54)); + add(entities.waterRock5(146.44, 52.88)); + add(entities.waterRock6(140.38, 78.25)); + add(entities.waterRock6(133.06, 82.21)); + add(entities.waterRock6(139.31, 62.96)); + add(entities.waterRock6(138.19, 69.79)); + add(entities.waterRock6(147.38, 65.83)); + add(entities.waterRock7(129.63, 80.79)); + add(entities.waterRock7(145.56, 74.54)); + add(entities.waterRock7(146.88, 52.67)); + add(entities.waterRock8(140.09, 67.21)); + add(entities.waterRock10(133.41, 82.04)); + add(entities.waterRock11(147.66, 62.33)); + add(entities.waterRock8(145.97, 71.42)); + add(entities.waterRock8(121.56, 75.92)); + add(entities.waterRock10(122.38, 73.54)); + add(entities.waterRock8(129.53, 81.46)); + add(entities.waterRock11(136.40, 80.92)); + add(entities.waterRock8(136.88, 80.54)); + add(entities.waterRock11(144.94, 58.71)); + add(entities.waterRock8(133.75, 57.75)); + add(entities.waterRock6(126.63, 80.29)); + add(entities.waterRock1(128.59, 74.63)); + add(entities.waterRock3(129.47, 74.33)); + add(entities.waterRock6(129.16, 74.83)); + add(entities.waterRock8(128.75, 75.00)); + add(entities.waterRock7(140.59, 73.54)); + add(entities.waterRock10(140.94, 73.67)); + add(entities.waterRock8(140.59, 74.08)); + + // pine forest lakes + add(entities.waterRock1(34.81, 118.79)); + add(entities.waterRock1(31.38, 115.79)); + add(entities.waterRock1(51.63, 111.79)); + add(entities.waterRock2(62.38, 111.75)); + add(entities.waterRock2(49.63, 112.83)); + add(entities.waterRock2(31.72, 115.25)); + add(entities.waterRock2(36.69, 109.75)); + add(entities.waterRock3(34.56, 119.04)); + add(entities.waterRock3(32.63, 122.50)); + add(entities.waterRock3(52.03, 112.08)); + add(entities.waterRock4(62.59, 111.38)); + add(entities.waterRock4(51.59, 112.17)); + add(entities.waterRock4(31.91, 115.54)); + add(entities.waterRock5(34.28, 118.46)); + add(entities.waterRock5(33.44, 112.67)); + add(entities.waterRock6(30.66, 118.25)); + add(entities.waterRock6(55.28, 114.13)); + add(entities.waterRock7(33.75, 112.21)); + add(entities.waterRock7(29.94, 116.67)); + add(entities.waterRock7(49.78, 113.29)); + add(entities.waterRock8(49.97, 112.88)); + add(entities.waterRock8(32.72, 122.96)); + add(entities.waterRock9(53.41, 111.63)); + add(entities.waterRock10(64.34, 110.96)); + add(entities.waterRock10(36.91, 110.13)); + add(entities.waterRock10(30.44, 116.50)); + add(entities.waterRock8(30.31, 116.79)); + add(entities.waterRock10(32.94, 122.67)); + + // bottom lake + add(entities.waterRock1(95.34, 148.83)); + add(entities.waterRock2(98.56, 150.54)); + add(entities.waterRock3(95.44, 151.63)); + add(entities.waterRock4(95.75, 151.58)); + add(entities.waterRock5(92.44, 150.67)); + add(entities.waterRock6(92.72, 150.92)); + add(entities.waterRock8(92.75, 150.50)); + add(entities.waterRock9(98.81, 150.96)); + add(entities.waterRock10(98.41, 150.92)); + add(entities.waterRock4(95.03, 149.00)); + + // forest puddle + add(entities.waterRock5(140.44, 122.71)); + add(entities.waterRock4(140.13, 122.67)); + + add(entities.bench1(18.69, 88.50)); + add(entities.bench1(27.94, 104.21)); + add(entities.bench1(22.56, 70.50)); + add(entities.bench1(25.28, 70.46)); + add(entities.bench1(44.16, 31.46)); + add(entities.bench1(46.84, 31.50)); + add(entities.bench1(30.03, 29.46)); + add(entities.bench1(87.00, 3.33)); + add(entities.bench1(101.91, 8.25)); + add(entities.bench1(142.13, 11.54)); + add(entities.bench1(144.84, 11.58)); + add(entities.benchSeat(142.00, 15.88)); + add(entities.benchSeat(144.78, 15.83)); + add(entities.benchBack(142.00, 16.7917)); + add(entities.benchBack(144.78, 16.71)); + add(entities.benchSeat(45.78, 36.42)); + add(entities.benchBack(45.78, 37.33)); + add(entities.bench1(93.59, 146.33)); + add(entities.bench1(144.13, 138.75)); + add(entities.bench1(146.66, 138.83)); + add(entities.bench1(133.22, 150.71)); + add(entities.benchSeat(133.13, 153.71)); + add(entities.benchBack(133.13, 154.63)); + add(entities.bench1(117.28, 62.13)); + add(entities.benchSeat(117.25, 65.13)); + add(entities.benchBack(117.25, 66.04)); + add(entities.bench1(123.81, 53.08)); + add(entities.bench1(127.09, 53.04)); + add(entities.lanternOn(117.09, 64.08)); + add(entities.lanternOn(20.34, 89.04)); + add(entities.benchSeatH(147.14, 13.02)); + add(entities.benchBackH2(147.66, 15.70)); + add(entities.benchSeatH(27.44, 117.21)); + add(entities.benchBackH(26.97, 119.92)); + add(entities.benchSeatH(136.63, 151.29)); + add(entities.benchSeatH(129.34, 151.25)); + add(entities.benchBackH(128.85, 153.96)); + add(entities.benchBackH2(137.16, 153.96)); + + updateMainMapSeason(world, map, world.season, world.holiday); + + addEntities(createBunny([ + point(72.34, 56.79), + point(70.69, 56.08), + point(69.56, 55.67), + point(68.03, 57.67), + point(70.28, 57.96), + point(71.28, 58.79), + point(70.00, 59.54), + point(68.72, 58.67), + point(67.22, 59.96), + point(65.38, 61.17), + point(65.31, 62.50), + point(64.25, 64.25), + point(65.69, 66.00), + point(68.47, 69.50), + point(69.59, 69.79), + point(69.56, 68.21), + point(71.06, 68.79), + point(72.94, 64.67), + point(74.31, 63.13), + point(74.41, 60.79), + point(75.50, 60.13), + point(76.03, 57.75), + point(78.19, 54.42), + point(77.47, 52.75), + point(78.38, 50.46), + point(79.03, 51.67), + point(80.50, 50.96), + point(81.69, 50.42), + point(82.75, 51.54), + point(83.75, 51.96), + point(83.75, 52.96), + point(86.13, 54.25), + point(87.28, 54.04), + point(87.59, 55.13), + point(86.31, 55.50), + point(84.22, 56.38), + point(82.06, 56.25), + point(81.47, 54.71), + point(80.84, 54.71), + point(81.41, 56.13), + point(82.19, 59.58), + point(84.03, 62.25), + point(86.44, 63.92), + point(88.00, 63.21), + point(87.16, 64.29), + point(85.78, 65.13), + point(82.50, 65.42), + point(81.47, 66.38), + point(80.25, 66.58), + point(79.47, 68.04), + point(77.84, 68.04), + point(75.91, 67.92), + point(75.16, 66.58), + point(74.72, 64.96), + point(73.16, 63.54), + point(71.97, 62.25), + point(72.56, 60.79), + point(71.16, 59.83), + point(72.19, 58.17), + ])); + + addEntities(createBunny([ + point(111.84, 63.92), + point(114.81, 64.79), + point(111.50, 67.54), + point(115.94, 69.58), + point(112.09, 75.42), + point(105.78, 79.75), + point(111.13, 80.96), + point(117.09, 80.29), + point(120.88, 87.38), + point(124.22, 91.50), + point(128.81, 88.71), + point(125.81, 85.21), + point(131.44, 91.17), + point(129.19, 95.38), + point(132.38, 96.21), + point(137.06, 96.50), + point(142.28, 95.75), + point(140.19, 89.46), + point(145.56, 87.33), + point(150.13, 89.83), + point(151.75, 84.92), + point(148.28, 82.29), + point(150.13, 80.04), + point(153.78, 79.17), + point(150.44, 70.71), + point(153.19, 67.96), + point(152.38, 65.08), + point(154.38, 62.63), + point(152.44, 58.17), + point(148.56, 57.00), + point(149.56, 53.58), + point(152.41, 51.21), + point(151.03, 47.79), + point(148.66, 45.25), + point(143.31, 46.46), + point(140.75, 49.58), + point(141.78, 54.04), + point(140.94, 56.08), + point(139.19, 57.33), + point(137.41, 55.13), + point(134.91, 55.88), + point(131.03, 52.08), + point(128.63, 48.67), + point(124.88, 49.25), + point(124.56, 44.46), + point(122.47, 45.83), + point(121.50, 42.83), + point(117.72, 45.63), + point(115.34, 46.13), + point(114.97, 43.25), + point(115.94, 40.38), + point(114.19, 37.83), + point(108.16, 37.75), + point(108.66, 40.25), + point(108.53, 41.63), + point(107.59, 42.67), + point(101.91, 44.00), + point(102.09, 46.67), + point(100.34, 46.08), + point(100.56, 48.25), + point(102.28, 51.83), + point(105.06, 52.92), + point(104.75, 55.75), + point(108.25, 55.67), + point(106.88, 57.92), + point(106.97, 62.38), + point(111.09, 60.42), + point(112.84, 62.63), + ])); + + addEntities(createBunny([ + point(85.00, 88.54), + point(81.91, 89.75), + point(77.06, 88.46), + point(75.75, 90.58), + point(78.06, 92.46), + point(76.88, 92.96), + point(78.59, 94.13), + point(76.72, 95.29), + point(79.94, 95.71), + point(82.34, 93.63), + point(85.81, 95.04), + point(88.88, 93.58), + point(92.66, 94.04), + point(95.06, 92.21), + point(98.00, 92.96), + point(98.25, 94.92), + point(98.13, 100.46), + point(96.94, 102.71), + point(96.09, 104.46), + point(98.00, 106.46), + point(100.41, 103.96), + point(102.25, 107.50), + point(103.97, 109.42), + point(105.66, 108.04), + point(107.19, 106.13), + point(107.69, 102.42), + point(108.78, 101.96), + point(109.88, 104.42), + point(108.53, 106.38), + point(111.22, 109.50), + point(114.34, 109.58), + point(114.75, 112.63), + point(114.28, 116.50), + point(117.38, 118.50), + point(117.88, 123.04), + point(119.38, 124.38), + point(119.25, 127.21), + point(117.72, 129.13), + point(114.41, 133.63), + point(115.28, 134.75), + point(115.03, 137.63), + point(117.81, 145.83), + point(122.03, 148.08), + point(120.56, 151.42), + point(119.28, 154.29), + point(121.28, 154.71), + point(123.44, 152.13), + point(123.34, 150.17), + point(126.38, 150.25), + point(125.91, 154.08), + point(124.91, 155.33), + point(131.81, 157.13), + point(139.19, 156.83), + point(144.88, 153.63), + point(147.00, 150.38), + point(145.63, 149.13), + point(150.34, 148.50), + point(150.78, 151.04), + point(149.28, 154.17), + point(145.00, 151.04), + point(150.50, 145.29), + point(150.56, 138.17), + point(153.09, 136.33), + point(156.03, 136.29), + point(154.78, 133.42), + point(151.63, 134.08), + point(148.56, 133.21), + point(146.16, 129.38), + point(146.34, 124.08), + point(148.03, 119.42), + point(145.09, 113.92), + point(142.81, 110.83), + point(140.09, 109.42), + point(135.94, 105.75), + point(138.91, 98.96), + point(141.97, 94.92), + point(143.66, 92.50), + point(141.00, 91.29), + point(144.03, 89.58), + point(141.38, 87.79), + point(138.75, 90.54), + point(134.84, 89.50), + point(131.41, 88.00), + point(131.38, 90.63), + point(128.03, 88.46), + point(125.25, 88.08), + point(125.50, 84.13), + point(122.72, 84.42), + point(119.22, 87.17), + point(113.50, 83.25), + point(110.56, 80.33), + point(105.22, 79.96), + point(102.34, 77.04), + point(100.69, 78.54), + point(98.09, 78.04), + point(99.97, 75.92), + point(95.53, 76.25), + point(94.31, 78.00), + point(91.00, 75.92), + point(86.34, 77.46), + point(87.31, 80.75), + point(86.19, 83.83), + point(89.47, 84.04), + point(86.28, 86.33), + ])); + + addEntities(createBunny([ + point(60.94, 119.21), + point(60.44, 116.92), + point(57.34, 117.92), + point(58.25, 114.08), + point(57.41, 111.25), + point(55.63, 109.79), + point(53.94, 104.42), + point(51.97, 105.04), + point(53.31, 101.04), + point(51.34, 96.96), + point(49.16, 96.54), + point(43.22, 97.79), + point(43.47, 99.42), + point(40.50, 99.17), + point(41.00, 96.54), + point(38.16, 98.25), + point(33.91, 98.88), + point(30.03, 101.04), + point(29.25, 102.17), + point(26.34, 102.71), + point(24.63, 102.96), + point(23.81, 106.83), + point(21.50, 108.38), + point(22.63, 110.17), + point(20.72, 111.79), + point(23.06, 113.33), + point(24.44, 117.58), + point(25.19, 120.79), + point(28.19, 121.92), + point(26.88, 123.79), + point(32.19, 126.29), + point(31.97, 128.25), + point(34.28, 130.54), + point(35.91, 130.00), + point(38.59, 129.88), + point(37.25, 125.92), + point(40.75, 123.96), + point(42.25, 128.00), + point(45.91, 129.33), + point(46.59, 132.08), + point(48.53, 129.29), + point(52.03, 126.50), + point(47.31, 122.00), + point(50.09, 118.42), + point(52.94, 119.54), + point(55.59, 119.38), + ])); + + addEntities(createBunny([ + point(65.28, 127.13), + point(62.63, 129.54), + point(67.25, 130.13), + point(64.53, 130.38), + point(62.81, 133.17), + point(67.31, 133.25), + point(69.09, 134.88), + point(64.97, 136.25), + point(67.03, 139.29), + point(64.94, 141.13), + point(64.66, 143.71), + point(66.75, 144.08), + point(67.56, 141.71), + point(69.50, 142.04), + point(68.16, 144.79), + point(66.25, 147.50), + point(64.25, 146.58), + point(62.41, 148.42), + point(59.22, 150.04), + point(59.88, 151.33), + point(58.94, 153.00), + point(57.66, 151.92), + point(55.47, 149.42), + point(55.91, 147.79), + point(53.06, 147.25), + point(50.38, 144.21), + point(47.00, 145.54), + point(44.06, 145.33), + point(42.84, 143.79), + point(43.78, 142.50), + point(42.38, 140.29), + point(44.84, 138.46), + point(45.84, 133.29), + point(48.72, 132.13), + point(50.16, 128.92), + point(52.63, 129.42), + point(54.78, 127.42), + point(56.31, 129.00), + point(56.22, 130.42), + point(59.56, 130.38), + ])); + + addEntities(createBunny([ + point(62.75, 128.29), + point(62.50, 130.54), + point(66.16, 128.46), + point(67.94, 129.42), + point(67.47, 133.38), + point(63.84, 132.25), + point(65.50, 130.67), + point(67.78, 136.50), + point(66.28, 139.96), + point(64.69, 141.96), + point(65.88, 146.08), + point(68.63, 143.75), + point(65.88, 143.08), + point(68.34, 141.58), + point(68.00, 140.38), + point(67.41, 144.13), + point(67.22, 139.92), + point(67.41, 134.38), + ])); + + addEntities(createBunny([ + point(66.56, 130.17), + point(67.41, 128.33), + point(66.25, 127.21), + point(64.41, 128.00), + point(62.97, 128.46), + point(63.53, 130.92), + point(62.16, 131.96), + point(64.66, 133.42), + point(67.34, 134.13), + point(68.25, 134.83), + point(64.47, 137.50), + point(65.09, 139.58), + point(64.59, 142.83), + point(66.56, 144.63), + point(67.75, 141.71), + point(69.63, 142.79), + point(68.34, 144.83), + point(63.63, 146.50), + point(62.03, 148.67), + point(58.63, 149.83), + point(58.78, 152.63), + point(60.06, 150.38), + point(62.19, 146.71), + point(65.66, 146.75), + point(69.34, 144.58), + point(71.59, 143.54), + point(72.19, 141.54), + point(68.09, 141.21), + point(66.75, 138.88), + point(67.09, 135.96), + point(63.63, 133.46), + ])); + + addEntities(createBunny([ + point(77.28, 149.21), + point(77.44, 150.83), + point(79.34, 151.38), + point(77.00, 153.88), + point(82.84, 153.00), + point(83.47, 155.21), + point(86.59, 154.17), + point(85.94, 151.04), + point(84.69, 149.54), + point(85.44, 147.92), + point(81.41, 149.38), + ])); + + addEntities(createBunny([ + point(77.59, 149.08), + point(78.03, 152.38), + point(76.63, 152.13), + point(76.81, 154.54), + point(79.34, 153.75), + point(81.22, 154.71), + point(83.19, 154.54), + point(84.56, 152.42), + point(81.09, 151.25), + point(81.94, 149.46), + point(80.09, 149.46), + point(84.00, 153.17), + point(85.91, 149.29), + point(86.34, 153.42), + ])); + + addEntities(createBunny([ + point(86.09, 148.13), + point(84.44, 149.13), + point(86.25, 152.13), + point(86.78, 150.29), + point(84.63, 151.46), + point(86.00, 153.79), + point(84.03, 154.88), + point(81.81, 152.25), + point(82.19, 150.88), + point(80.19, 149.42), + point(78.09, 149.42), + point(77.72, 152.25), + point(80.28, 152.54), + point(77.63, 154.33), + point(79.22, 155.21), + point(82.78, 153.58), + point(83.75, 150.38), + point(82.97, 149.04), + ])); + + addEntities(createBunny([ + point(86.09, 148.13), + point(84.44, 149.13), + point(86.25, 152.13), + point(86.78, 150.29), + point(84.63, 151.46), + point(86.00, 153.79), + point(84.03, 154.88), + point(81.81, 152.25), + point(82.19, 150.88), + point(80.19, 149.42), + point(78.09, 149.42), + point(77.72, 152.25), + point(80.28, 152.54), + point(77.63, 154.33), + point(79.22, 155.21), + point(82.78, 153.58), + point(83.75, 150.38), + point(82.97, 149.04), + point(9.09, 102.54), + point(6.84, 104.00), + point(8.75, 105.00), + point(6.94, 106.58), + point(8.59, 110.33), + point(7.56, 112.79), + point(6.00, 114.46), + point(4.63, 113.46), + point(2.34, 113.75), + point(1.19, 116.25), + point(2.50, 117.54), + point(5.00, 115.92), + point(6.16, 119.08), + point(7.44, 123.25), + point(6.53, 124.42), + point(7.13, 128.13), + point(6.00, 131.71), + point(7.56, 136.38), + point(10.88, 139.00), + point(10.91, 140.54), + point(12.69, 141.08), + point(14.06, 141.92), + point(16.09, 141.42), + point(14.59, 140.08), + point(14.78, 137.63), + point(12.34, 137.79), + point(10.38, 138.25), + point(7.97, 136.75), + point(6.69, 131.33), + point(8.56, 127.42), + point(7.25, 123.58), + point(7.00, 119.13), + point(4.34, 117.67), + point(3.53, 114.75), + point(4.75, 114.58), + point(6.34, 115.63), + point(7.19, 113.21), + point(7.81, 108.46), + point(9.22, 106.58), + point(11.47, 104.83), + point(11.63, 102.63), + point(10.34, 103.17), + ])); + + addEntities(createBunny([ + point(21.72, 63.50), + point(23.09, 65.46), + point(24.53, 63.54), + point(25.59, 65.13), + point(25.28, 66.71), + point(27.69, 67.92), + point(30.22, 67.58), + point(32.94, 67.79), + point(36.38, 68.92), + point(37.16, 67.83), + point(35.88, 65.79), + point(38.34, 65.25), + point(39.50, 67.38), + point(40.19, 64.96), + point(39.34, 62.88), + point(37.28, 64.04), + point(34.44, 62.67), + point(32.44, 61.17), + point(32.53, 59.63), + point(29.41, 58.88), + point(29.22, 61.79), + point(26.50, 61.83), + point(24.53, 61.58), + point(22.88, 61.50), + point(23.00, 63.38), + point(25.78, 62.88), + ])); + + addEntities(createBunny([ + point(21.38, 28.04), + point(19.81, 26.08), + point(21.22, 23.75), + point(23.19, 25.29), + point(25.09, 22.92), + point(25.81, 25.79), + point(26.56, 22.83), + point(28.31, 19.92), + point(29.41, 16.79), + point(31.00, 15.25), + point(32.41, 16.83), + point(32.72, 18.38), + point(34.63, 16.67), + point(33.53, 15.50), + point(38.81, 13.92), + point(41.16, 14.88), + point(43.09, 13.96), + point(46.19, 12.46), + point(45.22, 7.96), + point(46.31, 5.96), + point(45.34, 5.08), + point(47.00, 4.50), + point(47.41, 5.58), + point(55.22, 4.75), + point(56.09, 7.83), + point(57.91, 8.00), + point(60.94, 7.58), + point(62.13, 6.50), + point(63.09, 7.38), + point(64.34, 6.42), + point(65.09, 6.96), + point(67.06, 11.17), + point(65.78, 12.29), + point(67.66, 13.33), + point(70.09, 12.71), + point(71.13, 14.67), + point(70.22, 16.04), + point(73.09, 16.75), + point(74.78, 14.00), + point(74.47, 17.83), + point(73.19, 21.04), + point(74.38, 21.50), + point(77.09, 20.13), + point(77.72, 19.17), + point(79.22, 20.88), + point(80.19, 22.88), + point(81.84, 22.71), + point(82.47, 21.67), + point(82.50, 20.08), + point(81.47, 19.25), + point(81.00, 17.58), + point(82.47, 16.08), + point(80.63, 15.42), + point(78.59, 15.88), + point(78.50, 14.38), + point(79.78, 13.00), + point(81.81, 12.54), + point(83.19, 12.29), + point(84.22, 10.38), + point(87.00, 10.83), + point(87.91, 10.08), + point(89.41, 11.38), + point(90.38, 8.83), + point(91.91, 6.96), + point(97.44, 8.38), + point(99.56, 9.46), + point(98.50, 10.83), + point(97.28, 10.46), + point(97.72, 7.42), + point(95.28, 7.83), + point(95.06, 11.75), + point(94.84, 16.21), + point(96.50, 17.17), + point(98.66, 17.71), + point(99.00, 19.54), + point(97.06, 20.92), + point(95.50, 20.63), + point(90.22, 21.67), + point(89.28, 24.21), + point(89.22, 26.50), + point(88.66, 26.50), + point(88.38, 28.63), + point(85.84, 28.46), + point(85.31, 30.67), + point(83.59, 31.71), + point(81.63, 31.25), + point(79.53, 31.88), + point(73.91, 31.63), + point(71.19, 31.08), + point(69.97, 32.08), + point(66.94, 31.63), + point(65.47, 34.58), + point(62.72, 36.08), + point(60.00, 34.92), + point(57.09, 32.92), + point(52.00, 32.50), + point(52.59, 36.88), + point(50.75, 39.54), + point(48.63, 39.33), + point(48.72, 41.38), + point(47.03, 40.46), + point(43.28, 41.63), + point(40.88, 44.96), + point(35.88, 44.00), + point(32.72, 46.67), + point(32.13, 50.00), + point(33.47, 52.88), + point(32.91, 54.71), + point(30.38, 55.67), + point(28.06, 54.71), + point(25.66, 53.38), + point(24.03, 54.21), + point(22.78, 53.50), + point(21.19, 56.54), + point(18.25, 56.83), + point(15.22, 57.00), + point(13.69, 55.83), + point(14.88, 55.08), + point(16.47, 55.33), + point(18.31, 53.42), + point(19.25, 51.54), + point(19.38, 49.29), + point(21.84, 44.83), + point(22.34, 43.04), + point(24.00, 43.04), + point(25.31, 44.71), + point(25.03, 45.83), + point(26.38, 46.88), + point(27.28, 45.67), + point(26.94, 43.83), + point(26.88, 41.58), + point(28.31, 39.96), + point(27.81, 37.63), + point(25.72, 35.71), + point(24.16, 35.54), + point(22.50, 30.58), + point(23.81, 29.88), + point(24.13, 27.54), + ])); + + addEntities(createBunny([ + point(133.75, 6.38), + point(131.81, 5.29), + point(128.88, 5.42), + point(128.69, 7.00), + point(131.72, 8.96), + point(133.38, 8.13), + point(133.09, 10.63), + point(129.16, 9.33), + point(128.84, 8.25), + point(125.84, 9.96), + point(124.91, 8.96), + point(122.41, 10.83), + point(119.44, 10.46), + point(117.00, 10.25), + point(115.16, 10.67), + point(114.88, 11.58), + point(113.97, 13.88), + point(114.66, 15.33), + point(114.34, 16.33), + point(115.09, 17.75), + point(115.41, 19.83), + point(115.47, 24.79), + point(116.84, 25.92), + point(117.34, 28.17), + point(120.56, 29.50), + point(122.09, 30.50), + point(121.28, 32.08), + point(123.25, 29.92), + point(126.31, 29.46), + point(128.69, 33.00), + point(131.22, 32.67), + point(132.50, 31.25), + point(134.47, 32.13), + point(135.31, 29.88), + point(136.41, 29.75), + point(136.25, 28.25), + point(134.59, 27.75), + point(135.84, 25.38), + point(136.22, 23.29), + point(137.22, 23.25), + point(137.78, 20.25), + point(138.69, 19.29), + point(137.34, 17.00), + point(137.31, 14.21), + point(134.41, 13.25), + point(131.91, 11.83), + point(132.34, 9.88), + ])); + + if (true) { + const toLake = { icon: SignIcon.Lake, name: 'Lake' }; + const toHarbor = { icon: SignIcon.Boat, name: 'Harbor' }; + const toSpawn = { icon: SignIcon.Spawn, name: 'Spawn' }; + const toTownCenter = { icon: SignIcon.TownCenter, name: 'Town Center' }; + const toPineForest = { icon: SignIcon.PineForest, name: 'Pine Forest' }; + const toPartyIsland = { icon: SignIcon.Boat, name: 'Party Island' }; + const toGiftPile = { icon: SignIcon.GiftPile, name: 'Gift Pile' }; + const toMountains = { icon: SignIcon.Mountains, name: 'Mountains' }; + const toForest = { icon: SignIcon.Forest, name: 'Forest' }; + const toPumpkinFarm = { icon: SignIcon.Pumpkins, name: 'Pumpkin Farm' }; + const toFlowerField = { icon: SignIcon.Fields, name: 'Flower Field' }; + const toBarrelStorage = { icon: SignIcon.Barrels, name: 'Barrel Storage' }; + const toMines = { icon: SignIcon.Mines, name: 'Mines' }; + const toBridge = { icon: SignIcon.Bridge, name: 'Bridge' }; + const toCarrots = { icon: SignIcon.Carrots, name: 'Carrot farm' }; + + addEntities(createDirectionSign(77, 72, { + w: [toSpawn, toGiftPile, toHarbor, toPineForest, undefined], + e: [toLake, toCarrots, toMines, toBarrelStorage], + s: [toForest, toPumpkinFarm], + })); + + addEntities(createDirectionSign(54.33, 70.58, { + r: 1, + n: [toSpawn, toMines], + w: [toPineForest, toHarbor, toMountains], + e: [toTownCenter, toLake], + })); + + addEntities(createDirectionSign(36.00, 75.98, { + w: [toHarbor, toMountains], + e: [toSpawn, toTownCenter, toMines, toLake], + s: [toPineForest], + })); + + addEntities(createDirectionSign(19.34, 71.00, { + n: [toMountains], + w: [undefined, toPartyIsland], + e: [toSpawn, toTownCenter, toPineForest], + })); + + addEntities(createDirectionSign(24.86, 9.98, { + r: 1, + e: [toBridge, toMines, toLake], + s: [toHarbor, toPineForest], + })); + + addEntities(createDirectionSign(58.66, 54.88, { + w: [toGiftPile], + })); + + addEntities(createDirectionSign(54.38, 39.29, { + r: 1, + n: [toSpawn], + e: [toMines, toBridge, toLake], + s: [toTownCenter, toHarbor], + })); + + addEntities(createDirectionSign(99.00, 40.15, { + n: [toMountains, toBarrelStorage], + w: [toSpawn, toMines, toHarbor], + e: [toBridge, toCarrots], + s: [toLake, toTownCenter, toForest], + })); + + addEntities(createDirectionSign(122.75, 37.00, { + r: 1, + w: [toTownCenter, toSpawn, toMines], + n: [toCarrots], + })); + + addEntities(createDirectionSign(103.75, 70.10, { + r: 1, + n: [toBridge, toMountains, toCarrots], + w: [toTownCenter, toHarbor], + e: [toLake, toForest], + })); + + addEntities(createDirectionSign(128.16, 102.13, { + w: [toSpawn, toTownCenter, toHarbor], + e: [toLake], + s: [toFlowerField], + })); + + addEntities(createDirectionSign(129.53, 140.75, { + w: [toPumpkinFarm, toPineForest, toHarbor], + e: [toFlowerField], + n: [toForest, toLake, toTownCenter], + })); + + addEntities(createDirectionSign(70.98, 135.85, { + r: 1, + n: [toSpawn, toTownCenter, toHarbor, toMines], + w: [undefined, toPineForest], + e: [toForest, undefined, toFlowerField], + })); + + addEntities(createDirectionSign(54.91, 7.92, { + w: [toHarbor, toPineForest], + e: [toBridge, toMines, toLake], + })); + + addEntities(createDirectionSign(90.17, 5.35, { + w: [toHarbor, toPineForest], + s: [toBridge, toMines, toLake], + })); + + addEntities(createDirectionSign(78.41, 96.46, { + w: [toTownCenter, toHarbor], + e: [toForest, toLake], + s: [toPumpkinFarm, toPineForest], + })); + + addEntities(createDirectionSign(95.84, 25.33, { + e: [toBarrelStorage], + })); + + addEntities(createDirectionSign(77.15, 39.20, { + n: [toMines], + w: [undefined, toSpawn], + e: [toBridge, toCarrots, toLake], + })); + + addEntities(createDirectionSign(106.80, 95.46, { + r: 1, + w: [toTownCenter, toPumpkinFarm, toHarbor], + n: [toLake, toMines, toCarrots], + e: [undefined, toFlowerField], + })); + + addEntities(createDirectionSign(17.67, 138.90, { + n: [toHarbor, toMountains, toTownCenter], + })); + } + + const apples = [entities.apple, entities.apple2, entities.apple, entities.apple2, entities.appleGreen, entities.appleGreen2]; + const otherFruits = [entities.orange, entities.orange2, entities.pear, entities.banana]; + + const ctrls = map.controllers; + + ctrls.push(new ctrl.UpdateController(map)); + ctrls.push(new ctrl.TorchController(world, map)); + ctrls.push(new ctrl.CloudController(world, map, 5)); + ctrls.push(new ctrl.CollectableController(world, map, apples, 8, pickEntity, checkNotCollecting)); + ctrls.push(new ctrl.CollectableController(world, map, otherFruits, 3, pickEntity, checkNotCollecting)); + + ctrls.push(new ctrl.CollectableController( + world, map, [entities.gift1, entities.gift2], 50, pickGift, undefined, undefined, undefined, + () => world.holiday === Holiday.Christmas)); + + ctrls.push(new ctrl.CollectableController( + world, map, [entities.candy], 60, pickCandy, checkLantern, undefined, undefined, + () => world.holiday === Holiday.Halloween)); + + ctrls.push(new ctrl.CollectableController( + world, map, entities.eggs, 200, pickEgg, checkBasket, 5, undefined, + () => world.holiday === Holiday.Easter)); + + ctrls.push(new ctrl.CollectableController( + world, map, [entities.fourLeafClover], 2, pickClover, checkNotCollecting, 1, positionClover, + () => world.season === Season.Spring || world.season === Season.Summer)); + + ctrls.push(new ctrl.PlantController(world, map, { + area: rect(116.2, 14.2, 7.8, 9.6), + count: 100, + stages: [ + [entities.carrot4], + [entities.carrot3], + [entities.carrot2, entities.carrot2b], + [entities.carrot1, entities.carrot1b], + ], + growOnlyOn: TileType.Dirt, + onPick: (_, client) => holdItem(client.pony, entities.carrotHeld.type), + isActive: () => world.season !== Season.Winter, + })); + + if (!DEVELOPMENT) { + ctrls.push(new ctrl.FlyingCritterController( + world, map, entities.bat, 2, 20, () => isNightTime(world.time))); + ctrls.push(new ctrl.FlyingCritterController( + world, map, entities.firefly, 1, 40, () => world.season !== Season.Winter && isNightTime(world.time))); + ctrls.push(new ctrl.FlyingCritterController( + world, map, entities.butterfly, 1.5, 40, () => world.season !== Season.Winter && isDayTime(world.time))); + } + + if (BETA) { + ctrls.push(new ctrl.WallController(world, map, entities.woodenWalls)); + } + + return map; } diff --git a/src/ts/server/maps/paletteMap.ts b/src/ts/server/maps/paletteMap.ts index 4e056a3..cd9303c 100644 --- a/src/ts/server/maps/paletteMap.ts +++ b/src/ts/server/maps/paletteMap.ts @@ -9,36 +9,36 @@ import { ServerEntity } from '../serverInterfaces'; import { setEntityName } from '../entityUtils'; export function createPaletteMap(world: World) { - const map = createServerMap('palette', MapType.None, 10, 10, TileType.Grass); + const map = createServerMap('palette', MapType.None, 10, 10, TileType.Grass); - map.spawnArea = rect(map.width / 2, map.height / 2, 0, 0); + map.spawnArea = rect(map.width / 2, map.height / 2, 0, 0); - function add(entity: ServerEntity) { - world.addEntity(entity, map); - } + function add(entity: ServerEntity) { + world.addEntity(entity, map); + } - add(createSign(map.width / 2, map.height / 2, 'Go back', (_, client) => goToMap(world, client, '', 'center'))); + add(createSign(map.width / 2, map.height / 2, 'Go back', (_, client) => goToMap(world, client, '', 'center'))); - const pad = 5; - let x = pad; - let y = pad; + const pad = 5; + let x = pad; + let y = pad; - for (const name of allEntities) { - const entityOrEntities = (entities as any)[name](x, y); - const ents = Array.isArray(entityOrEntities) ? entityOrEntities : [entityOrEntities]; + for (const name of allEntities) { + const entityOrEntities = (entities as any)[name](x, y); + const ents = Array.isArray(entityOrEntities) ? entityOrEntities : [entityOrEntities]; - for (const entity of ents) { - add(entity); - setEntityName(entity, name); - } + for (const entity of ents) { + add(entity); + setEntityName(entity, name); + } - x += 3; + x += 3; - if (x > (map.width - pad)) { - x = pad; - y += 3; - } - } + if (x > (map.width - pad)) { + x = pad; + y += 3; + } + } - return map; + return map; } diff --git a/src/ts/server/move.ts b/src/ts/server/move.ts index 7f9bef3..9e95d4b 100644 --- a/src/ts/server/move.ts +++ b/src/ts/server/move.ts @@ -22,198 +22,198 @@ const maxLagLimit = maxLagLimitSeconds * SECOND; export type Move = ReturnType; export const createMove = - (teleportCounter: CounterService) => - (client: IClient, now: number, a: number, b: number, c: number, d: number, e: number, settings: GameServerSettings) => { - if (client.loading || client.fixingPosition || client.isSwitchingMap) - return; + (teleportCounter: CounterService) => + (client: IClient, now: number, a: number, b: number, c: number, d: number, e: number, settings: GameServerSettings) => { + if (client.loading || client.fixingPosition || client.isSwitchingMap) + return; - const connectionDuration = (now - client.connectedTime) >>> 0; - const pony = client.pony; - const { x, y, dir, flags, time, camera } = decodeMovement(a, b, c, d, e); - const v = dirToVector(dir); - const speed = flagsToSpeed(flags); + const connectionDuration = (now - client.connectedTime) >>> 0; + const pony = client.pony; + const { x, y, dir, flags, time, camera } = decodeMovement(a, b, c, d, e); + const v = dirToVector(dir); + const speed = flagsToSpeed(flags); - if (checkOutsideMap(client, x, y)) - return; + if (checkOutsideMap(client, x, y)) + return; - setupCamera(client.camera, camera.x, camera.y, camera.w, camera.h, client.map); + setupCamera(client.camera, camera.x, camera.y, camera.w, camera.h, client.map); - if (checkLagging(client, time, connectionDuration, settings)) - return; + if (checkLagging(client, time, connectionDuration, settings)) + return; - if (checkTeleporting(client, x, y, time, settings, teleportCounter)) - return; + if (checkTeleporting(client, x, y, time, settings, teleportCounter)) + return; - if (!isStaticCollision(pony, client.map, true)) { - client.safeX = pony.x; - client.safeY = pony.y; - } + if (!isStaticCollision(pony, client.map, true)) { + client.safeX = pony.x; + client.safeY = pony.y; + } - pony.x = x; - pony.y = y; + pony.x = x; + pony.y = y; - if (isStaticCollision(pony, client.map)) { - pony.x = client.safeX; - pony.y = client.safeY; + if (isStaticCollision(pony, client.map)) { + pony.x = client.safeX; + pony.y = client.safeY; - if (!isStaticCollision(pony, client.map)) { - if (settings.logFixingPosition) { - client.reporter.systemLog(`Fixed colliding (${x} ${y}) -> (${pony.x} ${pony.y})`); - } + if (!isStaticCollision(pony, client.map)) { + if (settings.logFixingPosition) { + client.reporter.systemLog(`Fixed colliding (${x} ${y}) -> (${pony.x} ${pony.y})`); + } - DEVELOPMENT && !TESTS && logger.warn(`Fixing position due to collision`); - fixPosition(pony, client.map, client.safeX, client.safeY, false); - } else { - pony.x = x; - pony.y = y; - } - } + DEVELOPMENT && !TESTS && logger.warn(`Fixing position due to collision`); + fixPosition(pony, client.map, client.safeX, client.safeY, false); + } else { + pony.x = x; + pony.y = y; + } + } - pony.vx = v.x * speed; - pony.vy = v.y * speed; + pony.vx = v.x * speed; + pony.vy = v.y * speed; - let ponyState = pony.state || 0; + let ponyState = pony.state || 0; - const facingRight = hasFlag(ponyState, EntityState.FacingRight); - const right = isMovingRight(pony.vx, facingRight); + const facingRight = hasFlag(ponyState, EntityState.FacingRight); + const right = isMovingRight(pony.vx, facingRight); - if (facingRight !== right) { - ponyState = setFlag(ponyState, EntityState.FacingRight, right); - ponyState = setFlag(ponyState, EntityState.HeadTurned, false); - } + if (facingRight !== right) { + ponyState = setFlag(ponyState, EntityState.FacingRight, right); + ponyState = setFlag(ponyState, EntityState.HeadTurned, false); + } - if ((pony.vx || pony.vy) && (isSittingState(ponyState) || isLyingState(ponyState))) { - ponyState = setPonyState(ponyState, EntityState.PonyStanding); - } + if ((pony.vx || pony.vy) && (isSittingState(ponyState) || isLyingState(ponyState))) { + ponyState = setPonyState(ponyState, EntityState.PonyStanding); + } - pony.state = ponyState; + pony.state = ponyState; - updateEntity(pony, false); + updateEntity(pony, false); - if (pony.exprCancellable) { - setEntityExpression(pony, undefined); - } + if (pony.exprCancellable) { + setEntityExpression(pony, undefined); + } - pony.timestamp = now / 1000; + pony.timestamp = now / 1000; - client.lastX = pony.x; - client.lastY = pony.y; - client.lastTime = time; - client.lastVX = pony.vx; - client.lastVY = pony.vy; - }; + client.lastX = pony.x; + client.lastY = pony.y; + client.lastTime = time; + client.lastVX = pony.vx; + client.lastVY = pony.vy; + }; function checkOutsideMap(client: IClient, x: number, y: number): boolean { - if (isOutsideMap(x, y, client.map)) { - const message = `map: [${client.map.id || 'main'}] coords: [${x.toFixed(2)}, ${y.toFixed(2)}]`; + if (isOutsideMap(x, y, client.map)) { + const message = `map: [${client.map.id || 'main'}] coords: [${x.toFixed(2)}, ${y.toFixed(2)}]`; - if (!client.shadowed) { - client.reporter.warn(`Outside map`, message); - } + if (!client.shadowed) { + client.reporter.warn(`Outside map`, message); + } - kickClient(client, `outside ${message}`); - return true; - } + kickClient(client, `outside ${message}`); + return true; + } - return false; + return false; } function checkLagging(client: IClient, time: number, connectionTime: number, settings: GameServerSettings): boolean { - const dt = time - connectionTime; - const lagging = (dt > maxLagLimit) || (dt < -maxLagLimit); + const dt = time - connectionTime; + const lagging = (dt > maxLagLimit) || (dt < -maxLagLimit); - if (lagging) { - if (settings.logLagging) { - // logger.warn(`Time delta > ${maxLagLimitSeconds}s (${dt}) account: ${client.account.name} [${client.accountId}]`); - client.reporter.systemLog(`Time delta > ${maxLagLimitSeconds}s (${dt})`); - client.logDisconnect = true; - } + if (lagging) { + if (settings.logLagging) { + // logger.warn(`Time delta > ${maxLagLimitSeconds}s (${dt}) account: ${client.account.name} [${client.accountId}]`); + client.reporter.systemLog(`Time delta > ${maxLagLimitSeconds}s (${dt})`); + client.logDisconnect = true; + } - if (settings.kickLagging) { - client.reporter.systemLog(`Lagging (dt: ${dt} time: ${time} connectionTime: ${connectionTime})`); - kickClient(client, 'lagging'); - return true; - } - } + if (settings.kickLagging) { + client.reporter.systemLog(`Lagging (dt: ${dt} time: ${time} connectionTime: ${connectionTime})`); + kickClient(client, 'lagging'); + return true; + } + } - return false; + return false; } function checkTeleporting( - client: IClient, x: number, y: number, time: number, settings: GameServerSettings, counter: CounterService + client: IClient, x: number, y: number, time: number, settings: GameServerSettings, counter: CounterService ): boolean { - if (!client.lastTime) - return false; + if (!client.lastTime) + return false; - const pony = client.pony; - const borderX = 0.5; - const borderY = 0.5; - const delta = ((time - client.lastTime) / 1000) * 1; + const pony = client.pony; + const borderX = 0.5; + const borderY = 0.5; + const delta = ((time - client.lastTime) / 1000) * 1; - const afterX = roundPositionX(client.lastX + client.lastVX * delta); - const afterY = roundPositionY(client.lastY + client.lastVY * delta); + const afterX = roundPositionX(client.lastX + client.lastVX * delta); + const afterY = roundPositionY(client.lastY + client.lastVY * delta); - const afterMinX = client.lastVX === 0 ? afterX - Math.abs(client.lastVY) : afterX; - const afterMaxX = client.lastVX === 0 ? afterX + Math.abs(client.lastVY) : afterX; - const afterMinY = client.lastVY === 0 ? afterY - Math.abs(client.lastVX) : afterY; - const afterMaxY = client.lastVY === 0 ? afterY + Math.abs(client.lastVX) : afterY; + const afterMinX = client.lastVX === 0 ? afterX - Math.abs(client.lastVY) : afterX; + const afterMaxX = client.lastVX === 0 ? afterX + Math.abs(client.lastVY) : afterX; + const afterMinY = client.lastVY === 0 ? afterY - Math.abs(client.lastVX) : afterY; + const afterMaxY = client.lastVY === 0 ? afterY + Math.abs(client.lastVX) : afterY; - const minX = Math.floor((Math.min(client.lastX, afterMinX) - borderX) * tileWidth) / tileWidth; - const maxX = Math.ceil((Math.max(client.lastX, afterMaxX) + borderX) * tileWidth) / tileWidth; - const minY = Math.floor((Math.min(client.lastY, afterMinY) - borderY) * tileHeight) / tileHeight; - const maxY = Math.ceil((Math.max(client.lastY, afterMaxY) + borderY) * tileHeight) / tileHeight; + const minX = Math.floor((Math.min(client.lastX, afterMinX) - borderX) * tileWidth) / tileWidth; + const maxX = Math.ceil((Math.max(client.lastX, afterMaxX) + borderX) * tileWidth) / tileWidth; + const minY = Math.floor((Math.min(client.lastY, afterMinY) - borderY) * tileHeight) / tileHeight; + const maxY = Math.ceil((Math.max(client.lastY, afterMaxY) + borderY) * tileHeight) / tileHeight; - const outX = x < minX || x > maxX; - const outY = y < minY || y > maxY; + const outX = x < minX || x > maxX; + const outY = y < minY || y > maxY; - if (outX || outY) { - if (settings.logTeleporting) { - const colX = outX ? chalk.red : chalk.reset; - const colY = outY ? chalk.red : chalk.reset; + if (outX || outY) { + if (settings.logTeleporting) { + const colX = outX ? chalk.red : chalk.reset; + const colY = outY ? chalk.red : chalk.reset; - logger.log( - `[${chalk.gray(moment().format('MMM DD HH:mm:ss'))}] [${chalk.yellow('teleport')}] ` + - `[${chalk.gray(client.accountId)}] (${client.account.name})\n` + - `\tdx: ${client.lastX.toFixed(5)} -> ${colX(x.toFixed(5))} [${minX.toFixed(5)}-${maxX.toFixed(5)}]\n` + - `\tdy: ${client.lastY.toFixed(5)} -> ${colY(y.toFixed(5))} [${minY.toFixed(5)}-${maxY.toFixed(5)}]\n` + - `\tdt: ${delta.toFixed(5)}`); - } + logger.log( + `[${chalk.gray(moment().format('MMM DD HH:mm:ss'))}] [${chalk.yellow('teleport')}] ` + + `[${chalk.gray(client.accountId)}] (${client.account.name})\n` + + `\tdx: ${client.lastX.toFixed(5)} -> ${colX(x.toFixed(5))} [${minX.toFixed(5)}-${maxX.toFixed(5)}]\n` + + `\tdy: ${client.lastY.toFixed(5)} -> ${colY(y.toFixed(5))} [${minY.toFixed(5)}-${maxY.toFixed(5)}]\n` + + `\tdt: ${delta.toFixed(5)}`); + } - if (settings.reportTeleporting) { - const { count } = counter.add(client.accountId); + if (settings.reportTeleporting) { + const { count } = counter.add(client.accountId); - if (count > teleportReportLimit) { - counter.remove(client.accountId); - client.reporter.warn(`Teleporting (x${teleportReportLimit})`); - } - } + if (count > teleportReportLimit) { + counter.remove(client.accountId); + client.reporter.warn(`Teleporting (x${teleportReportLimit})`); + } + } - if (settings.kickTeleporting) { - kickClient(client, 'teleporting'); - return true; - } + if (settings.kickTeleporting) { + kickClient(client, 'teleporting'); + return true; + } - if (settings.fixTeleporting) { - pony.vx = 0; - pony.vy = 0; - client.reporter.systemLog(`Fixed teleporting (${x} ${y}) -> (${pony.x} ${pony.y})`); - fixPosition(client.pony, client.map, pony.x, pony.y, false); - return true; - } - } + if (settings.fixTeleporting) { + pony.vx = 0; + pony.vy = 0; + client.reporter.systemLog(`Fixed teleporting (${x} ${y}) -> (${pony.x} ${pony.y})`); + fixPosition(client.pony, client.map, pony.x, pony.y, false); + return true; + } + } - const dx = Math.abs(x - pony.x); - const dy = Math.abs(y - pony.y); + const dx = Math.abs(x - pony.x); + const dy = Math.abs(y - pony.y); - if (dx > 8 || dy > 8) { - if (settings.fixTeleporting) { - pony.vx = 0; - pony.vy = 0; - client.reporter.systemLog(`Fixed teleporting (too far) (${x} ${y}) -> (${pony.x} ${pony.y})`); - fixPosition(client.pony, client.map, pony.x, pony.y, false); - return true; - } - } + if (dx > 8 || dy > 8) { + if (settings.fixTeleporting) { + pony.vx = 0; + pony.vy = 0; + client.reporter.systemLog(`Fixed teleporting (too far) (${x} ${y}) -> (${pony.x} ${pony.y})`); + fixPosition(client.pony, client.map, pony.x, pony.y, false); + return true; + } + } - return false; + return false; } diff --git a/src/ts/server/oauth.ts b/src/ts/server/oauth.ts index abadcb9..4fc759b 100644 --- a/src/ts/server/oauth.ts +++ b/src/ts/server/oauth.ts @@ -17,76 +17,76 @@ import { IAccount } from './db'; export type OAuthProfileName = string | { familyName: string; givenName: string; }; export interface OAuthProfile { - id?: string; - name?: OAuthProfileName; - username?: string; - displayName?: string; - emails?: { value: string; }[]; - provider: string; - gender?: string; - profileUrl?: string; - _raw: string; - _json: any; + id?: string; + name?: OAuthProfileName; + username?: string; + displayName?: string; + emails?: { value: string; }[]; + provider: string; + gender?: string; + profileUrl?: string; + _raw: string; + _json: any; } export interface Strategy { - new( - options: any, - callback: ( - req: Request, - accessToken: string, - refreshToken: string, - profile: OAuthProfile, - callback: (err: Error | null, user: IAccount | null) => void) => void): any; + new( + options: any, + callback: ( + req: Request, + accessToken: string, + refreshToken: string, + profile: OAuthProfile, + callback: (err: Error | null, user: IAccount | null) => void) => void): any; } export interface OAuthProviderInfo { - id: string; - name: string; - color: string; - strategy: Strategy; - auth?: any; - connectOnly?: boolean; - additionalOptions?: any; + id: string; + name: string; + color: string; + strategy: Strategy; + auth?: any; + connectOnly?: boolean; + additionalOptions?: any; } const providerList: OAuthProviderInfo[] = [ - { - id: 'google', - name: 'Google', - color: '#DC4A3D', - strategy: GoogleStrategy, - }, - { - id: 'twitter', - name: 'Twitter', - color: '#55ACEE', - strategy: TwitterStrategy, - }, - { - id: 'facebook', - name: 'Facebook', - color: '#3765A3', - strategy: FacebookStrategy, - }, - { - id: 'github', - name: 'GitHub', - color: '#800080', - strategy: GithubStrategy, - }, - { - id: 'vkontakte', - name: 'VKontakte', - color: '#4C75A3', - strategy: VKontakteStrategy, - }, - { - id: 'patreon', - name: 'Patreon', - color: colorToCSS(PATREON_COLOR), - strategy: PatreonStrategy, - }, + { + id: 'google', + name: 'Google', + color: '#DC4A3D', + strategy: GoogleStrategy, + }, + { + id: 'twitter', + name: 'Twitter', + color: '#55ACEE', + strategy: TwitterStrategy, + }, + { + id: 'facebook', + name: 'Facebook', + color: '#3765A3', + strategy: FacebookStrategy, + }, + { + id: 'github', + name: 'GitHub', + color: '#800080', + strategy: GithubStrategy, + }, + { + id: 'vkontakte', + name: 'VKontakte', + color: '#4C75A3', + strategy: VKontakteStrategy, + }, + { + id: 'patreon', + name: 'Patreon', + color: colorToCSS(PATREON_COLOR), + strategy: PatreonStrategy, + }, ]; providerList.forEach(p => p.auth = config.oauth[p.id]); @@ -95,56 +95,56 @@ providerList.filter(p => p.auth && p.auth.connectOnly).forEach(p => p.connectOnl export const providers = providerList.filter(p => !!p.auth); export function getProfileUrl(profile: OAuthProfile): string | undefined { - if (profile.provider === 'twitter') { - return `https://twitter.com/${profile.username}`; - } else if (profile.provider === 'tumblr') { - return `http://${profile.username}.tumblr.com/`; - } else if (profile.provider === 'facebook') { - return `http://www.facebook.com/${profile.id}`; - } else if (profile._json.attributes && profile._json.attributes.url) { // patreon - return profile._json.attributes.url; - } else { - return profile.profileUrl || profile._json.url; - } + if (profile.provider === 'twitter') { + return `https://twitter.com/${profile.username}`; + } else if (profile.provider === 'tumblr') { + return `http://${profile.username}.tumblr.com/`; + } else if (profile.provider === 'facebook') { + return `http://www.facebook.com/${profile.id}`; + } else if (profile._json.attributes && profile._json.attributes.url) { // patreon + return profile._json.attributes.url; + } else { + return profile.profileUrl || profile._json.url; + } } export function getProfileEmails(profile: OAuthProfile): string[] { - if (profile.emails && profile.emails.length) { - return profile.emails.map(e => e.value); - } else if (profile._json && profile._json.attributes && profile._json.attributes.email) { // patreon - return [profile._json.attributes.email]; - } else { - return []; - } + if (profile.emails && profile.emails.length) { + return profile.emails.map(e => e.value); + } else if (profile._json && profile._json.attributes && profile._json.attributes.email) { // patreon + return [profile._json.attributes.email]; + } else { + return []; + } } export function getProfileUsername(profile: OAuthProfile): string | undefined { - return profile.username || profile.displayName || getProfileNameInternal(profile.name); + return profile.username || profile.displayName || getProfileNameInternal(profile.name); } export function getProfileName(profile: OAuthProfile): string | undefined { - return profile.displayName || profile.username || getProfileNameInternal(profile.name); + return profile.displayName || profile.username || getProfileNameInternal(profile.name); } function getProfileNameInternal(name: OAuthProfileName | undefined): string | undefined { - if (!name || isString(name)) { - return name; - } else { - return `${name.givenName} ${name.familyName}`.trim(); - } + if (!name || isString(name)) { + return name; + } else { + return `${name.givenName} ${name.familyName}`.trim(); + } } export function getProfile(provider: string, profile: OAuthProfile): Profile { - const emails = getProfileEmails(profile).map(e => e.toLowerCase()); + const emails = getProfileEmails(profile).map(e => e.toLowerCase()); - return { - id: profile.id || profile.username || '', - provider: profile.provider || provider, - username: getProfileUsername(profile) || emails[0], - name: getProfileName(profile) || emails[0], - emails, - url: getProfileUrl(profile), - createdAt: profile._json && profile._json.created_at && new Date(profile._json.created_at), - suspended: profile._json && profile._json.suspended, - }; + return { + id: profile.id || profile.username || '', + provider: profile.provider || provider, + username: getProfileUsername(profile) || emails[0], + name: getProfileName(profile) || emails[0], + emails, + url: getProfileUrl(profile), + createdAt: profile._json && profile._json.created_at && new Date(profile._json.created_at), + suspended: profile._json && profile._json.suspended, + }; } diff --git a/src/ts/server/originUtils.ts b/src/ts/server/originUtils.ts index d566315..a5caa5a 100644 --- a/src/ts/server/originUtils.ts +++ b/src/ts/server/originUtils.ts @@ -7,39 +7,39 @@ import { OriginInfoBase } from '../common/adminInterfaces'; const get_ip = require('ipware')().get_ip; export function getIP(req: { headers: any; }) { - return req.headers['cf-connecting-ip'] || (get_ip(req) ? get_ip(req).clientIp : null); + return req.headers['cf-connecting-ip'] || (get_ip(req) ? get_ip(req).clientIp : null); } export function getOriginFromHTTP(req: { headers: any; }): IOriginInfo { - const ip = getIP(req) || '0.0.0.0'; - const ipcountry = (ip === '127.0.0.1' || ip === '::ffff:127.0.0.1' || ip === '::1') ? 'LOCAL' : ''; - const country = ipcountry || req.headers['cf-ipcountry'] || '??'; - return { ip, country, last: new Date() }; + const ip = getIP(req) || '0.0.0.0'; + const ipcountry = (ip === '127.0.0.1' || ip === '::ffff:127.0.0.1' || ip === '::1') ? 'LOCAL' : ''; + const country = ipcountry || req.headers['cf-ipcountry'] || '??'; + return { ip, country, last: new Date() }; } export function getOrigin(req: Request): IOriginInfo { - const origin = getOriginFromHTTP(req); + const origin = getOriginFromHTTP(req); - if (origin.country === '??' && config.proxy) { - logger.warn('Invalid IP', JSON.stringify(req.ips)); - //create(null, null, null).danger('Invalid IP', JSON.stringify(req.ips)); - } + if (origin.country === '??' && config.proxy) { + logger.warn('Invalid IP', JSON.stringify(req.ips)); + //create(null, null, null).danger('Invalid IP', JSON.stringify(req.ips)); + } - return origin; + return origin; } export async function addOrigin(account: IAccount, origin: IOriginInfo) { - try { - const _id = account._id; - const existingOrigin = account.origins && account.origins - .find(o => o.ip === origin.ip) as (OriginInfoBase & { _id: any }) | undefined; + try { + const _id = account._id; + const existingOrigin = account.origins && account.origins + .find(o => o.ip === origin.ip) as (OriginInfoBase & { _id: any }) | undefined; - if (existingOrigin) { - await Account.updateOne({ _id, 'origins._id': existingOrigin._id }, { $set: { 'origins.$.last': new Date() } }).exec(); - } else { - await Account.updateOne({ _id }, { $push: { origins: origin } }).exec(); - } - } catch (e) { - logger.error('Failed to add origin', e); - } + if (existingOrigin) { + await Account.updateOne({ _id, 'origins._id': existingOrigin._id }, { $set: { 'origins.$.last': new Date() } }).exec(); + } else { + await Account.updateOne({ _id }, { $push: { origins: origin } }).exec(); + } + } catch (e) { + logger.error('Failed to add origin', e); + } } diff --git a/src/ts/server/paths.ts b/src/ts/server/paths.ts index 3655112..b716aaa 100644 --- a/src/ts/server/paths.ts +++ b/src/ts/server/paths.ts @@ -4,5 +4,5 @@ export const root = path.join(__dirname, '..', '..', '..'); export const store = path.join(root, 'store'); export function pathTo(...parts: string[]) { - return path.join(root, ...parts); + return path.join(root, ...parts); } diff --git a/src/ts/server/patreon.ts b/src/ts/server/patreon.ts index cdbec4a..e09e5e8 100644 --- a/src/ts/server/patreon.ts +++ b/src/ts/server/patreon.ts @@ -14,9 +14,9 @@ export const declinedTimeLimit = declinedDayLimit * DAY; export const supporterLogLimit = 10; export const SUPPORTER_REWARD_IDS: Dict = { - [rewardLevel1]: PatreonFlags.Supporter1, - [rewardLevel2]: PatreonFlags.Supporter2, - [rewardLevel3]: PatreonFlags.Supporter3, + [rewardLevel1]: PatreonFlags.Supporter1, + [rewardLevel2]: PatreonFlags.Supporter2, + [rewardLevel3]: PatreonFlags.Supporter3, }; export type RemoveOldSupporters = ReturnType; @@ -26,212 +26,212 @@ export type AddTotalPledged = ReturnType; let lastPatreonData: PatreonData | undefined = undefined; export function getLastPatreonData() { - return lastPatreonData; + return lastPatreonData; } /* istanbul ignore next */ export function createPatreonClient(accessToken: string): (path: string) => Promise { - const timeoutLimit = 10 * SECOND; - const client = patreon(accessToken); - client.setStore({ sync() { } }); + const timeoutLimit = 10 * SECOND; + const client = patreon(accessToken); + client.setStore({ sync() { } }); - return (path: string) => Promise.race([ - delay(timeoutLimit).then(() => { throw new Error('Patreon request timed out'); }), - client(path), - ]); + return (path: string) => Promise.race([ + delay(timeoutLimit).then(() => { throw new Error('Patreon request timed out'); }), + client(path), + ]); } export async function fetchPatreonData(client: (path: string) => Promise, log: LogMessage): Promise { - const campaignData = await client('/current_user/campaigns'); - const rewards = campaignData.rawJson.included - .filter(x => x.type === 'reward') - .map(x => ({ - id: x.id, - title: x.attributes.title || '', - description: x.attributes.description || '', - })); + const campaignData = await client('/current_user/campaigns'); + const rewards = campaignData.rawJson.included + .filter(x => x.type === 'reward') + .map(x => ({ + id: x.id, + title: x.attributes.title || '', + description: x.attributes.description || '', + })); - const campaignId = campaignData.rawJson.data[0].id; - const pledges: PatreonPledge[] = []; - const queryParams = '&include=patron.null,reward.null&fields%5Bpledge%5D=total_historical_amount_cents,declined_since'; - const query = 'page%5Bcount%5D=100&sort=created'; - let url = `/campaigns/${campaignId}/pledges?${query}`; - let pages = 0; + const campaignId = campaignData.rawJson.data[0].id; + const pledges: PatreonPledge[] = []; + const queryParams = '&include=patron.null,reward.null&fields%5Bpledge%5D=total_historical_amount_cents,declined_since'; + const query = 'page%5Bcount%5D=100&sort=created'; + let url = `/campaigns/${campaignId}/pledges?${query}`; + let pages = 0; - do { - const pledgeData = await client(`${url}${queryParams}`); - const pledgeItems = pledgeData.rawJson.data - .filter(x => x.relationships.patron.data && x.relationships.reward.data) - .map(x => ({ - user: x.relationships.patron.data.id, - reward: x.relationships.reward.data.id, - total: x.attributes.total_historical_amount_cents || 0, - declinedSince: x.attributes.declined_since || undefined, - })); + do { + const pledgeData = await client(`${url}${queryParams}`); + const pledgeItems = pledgeData.rawJson.data + .filter(x => x.relationships.patron.data && x.relationships.reward.data) + .map(x => ({ + user: x.relationships.patron.data.id, + reward: x.relationships.reward.data.id, + total: x.attributes.total_historical_amount_cents || 0, + declinedSince: x.attributes.declined_since || undefined, + })); - pledges.push(...pledgeItems); - url = (pledgeData.rawJson.links.next || '').replace('https://www.patreon.com/api/oauth2/api', ''); - pages++; + pledges.push(...pledgeItems); + url = (pledgeData.rawJson.links.next || '').replace('https://www.patreon.com/api/oauth2/api', ''); + pages++; - if (pages > 100) { - throw new Error('Exceeded 100 pages of patreon data'); - } - } while (url); + if (pages > 100) { + throw new Error('Exceeded 100 pages of patreon data'); + } + } while (url); - log(`fetched patreon data (pages: ${pages}, pledges: ${pledges.length}, rewards: ${rewards.length})`); + log(`fetched patreon data (pages: ${pages}, pledges: ${pledges.length}, rewards: ${rewards.length})`); - return lastPatreonData = { pledges, rewards }; + return lastPatreonData = { pledges, rewards }; } export const createUpdatePatreonInfo = - ( - queryAuths: QueryAuths, queryAccounts: QueryAccounts, removeOldSupporters: RemoveOldSupporters, - updateSupporters: UpdateSupporters, updateTotalPledged: AddTotalPledged - ) => - async ({ pledges }: PatreonData, now: Date) => { - const ids = pledges.map(p => p.user); - const query = { - provider: 'patreon', - openId: { $in: ids }, - account: { $exists: true }, - banned: { $ne: true }, - disabled: { $ne: true }, - }; + ( + queryAuths: QueryAuths, queryAccounts: QueryAccounts, removeOldSupporters: RemoveOldSupporters, + updateSupporters: UpdateSupporters, updateTotalPledged: AddTotalPledged + ) => + async ({ pledges }: PatreonData, now: Date) => { + const ids = pledges.map(p => p.user); + const query = { + provider: 'patreon', + openId: { $in: ids }, + account: { $exists: true }, + banned: { $ne: true }, + disabled: { $ne: true }, + }; - const patreonAuths = await queryAuths(query, '_id account openId pledged'); - const accountsWithPatreon = await queryAccounts({ patreon: { $exists: true, $ne: 0 } }, '_id patreon supporterDeclinedSince'); - // removes support from accounts without any non-banned patreon auth - await removeOldSupporters(patreonAuths, accountsWithPatreon); - await updateSupporters(patreonAuths, accountsWithPatreon, pledges, now); - await updateTotalPledged(patreonAuths, pledges); - }; + const patreonAuths = await queryAuths(query, '_id account openId pledged'); + const accountsWithPatreon = await queryAccounts({ patreon: { $exists: true, $ne: 0 } }, '_id patreon supporterDeclinedSince'); + // removes support from accounts without any non-banned patreon auth + await removeOldSupporters(patreonAuths, accountsWithPatreon); + await updateSupporters(patreonAuths, accountsWithPatreon, pledges, now); + await updateTotalPledged(patreonAuths, pledges); + }; export const createRemoveOldSupporters = - (updateAccounts: UpdateAccounts, log: LogAccountMessage) => - async (auths: IAuth[], accounts: IAccount[]) => { - const clear = accounts - .filter(account => auths.every(auth => !auth.account || !account._id.equals(auth.account))) - .map(account => account._id); + (updateAccounts: UpdateAccounts, log: LogAccountMessage) => + async (auths: IAuth[], accounts: IAccount[]) => { + const clear = accounts + .filter(account => auths.every(auth => !auth.account || !account._id.equals(auth.account))) + .map(account => account._id); - clear.forEach(id => log(`${id}`, `removed supporter`)); + clear.forEach(id => log(`${id}`, `removed supporter`)); - await updateAccounts({ _id: { $in: clear } }, { - $unset: { patreon: 1, supporterDeclinedSince: 1 }, - $push: { - supporterLog: { - $each: [{ date: new Date(), message: 'removed supporter' }], - $slice: -supporterLogLimit, - }, - }, - }); + await updateAccounts({ _id: { $in: clear } }, { + $unset: { patreon: 1, supporterDeclinedSince: 1 }, + $push: { + supporterLog: { + $each: [{ date: new Date(), message: 'removed supporter' }], + $slice: -supporterLogLimit, + }, + }, + }); - await updateAccounts( - { supporterDeclinedSince: { $exists: true, $lt: fromNow(-2 * MONTH) } }, - { $unset: { supporterDeclinedSince: 1 } }); - }; + await updateAccounts( + { supporterDeclinedSince: { $exists: true, $lt: fromNow(-2 * MONTH) } }, + { $unset: { supporterDeclinedSince: 1 } }); + }; export const createUpdateSupporters = - (updateAccount: UpdateAccount, log: LogAccountMessage) => - async (auths: IAuth[], accountsWithPatreon: IAccount[], pledges: PatreonPledge[], now: Date) => { - const start = Date.now(); - const pledgesMap = new Map(); - const accountsWithPatreonMap = new Map(); + (updateAccount: UpdateAccount, log: LogAccountMessage) => + async (auths: IAuth[], accountsWithPatreon: IAccount[], pledges: PatreonPledge[], now: Date) => { + const start = Date.now(); + const pledgesMap = new Map(); + const accountsWithPatreonMap = new Map(); - for (const pledge of pledges) { - pledgesMap.set(pledge.user, pledge); - } + for (const pledge of pledges) { + pledgesMap.set(pledge.user, pledge); + } - for (const account of accountsWithPatreon) { - accountsWithPatreonMap.set(account._id.toString(), account); - } + for (const account of accountsWithPatreon) { + accountsWithPatreonMap.set(account._id.toString(), account); + } - const setup = auths - .filter(auth => auth.account) - .map(auth => { - const accountId = auth.account!.toString(); - const pledge = auth.openId && pledgesMap.get(auth.openId); - const pledgeFlags = pledge && SUPPORTER_REWARD_IDS[pledge.reward] || PatreonFlags.None; - const declinedSince = (pledge && pledge.declinedSince) ? new Date(pledge.declinedSince) : undefined; - const account = accountsWithPatreonMap.get(accountId!); - const declined = isDeclined(declinedSince, now); - const patreon = declined ? PatreonFlags.None : pledgeFlags; - const current = account && account.patreon || 0; - const declinedChanged = !!account && !datesEqual(account.supporterDeclinedSince, declinedSince); - const hadPatreon = !!account; + const setup = auths + .filter(auth => auth.account) + .map(auth => { + const accountId = auth.account!.toString(); + const pledge = auth.openId && pledgesMap.get(auth.openId); + const pledgeFlags = pledge && SUPPORTER_REWARD_IDS[pledge.reward] || PatreonFlags.None; + const declinedSince = (pledge && pledge.declinedSince) ? new Date(pledge.declinedSince) : undefined; + const account = accountsWithPatreonMap.get(accountId!); + const declined = isDeclined(declinedSince, now); + const patreon = declined ? PatreonFlags.None : pledgeFlags; + const current = account && account.patreon || 0; + const declinedChanged = !!account && !datesEqual(account.supporterDeclinedSince, declinedSince); + const hadPatreon = !!account; - return { - account: accountId, patreon, declinedSince, declinedChanged, declined, current, hadPatreon - }; - }); + return { + account: accountId, patreon, declinedSince, declinedChanged, declined, current, hadPatreon + }; + }); - const grouped = toPairs(groupBy(setup, x => x.account)) - .map(([account, items]) => { - const current = max(items.map(i => i.current))!; - const patreon = max(items.map(i => i.patreon))!; + const grouped = toPairs(groupBy(setup, x => x.account)) + .map(([account, items]) => { + const current = max(items.map(i => i.current))!; + const patreon = max(items.map(i => i.patreon))!; - return { - account, - changed: items.some(i => !i.hadPatreon) || current !== patreon, - declinedChanged: items.some(i => i.declinedChanged), - patreon, - declinedSince: items.map(i => i.declinedSince).find(x => !!x), - declined: items.some(i => i.declined), - hadPatreon: items.some(i => i.hadPatreon), - }; - }) - .filter(({ changed, declinedChanged }) => changed || declinedChanged); + return { + account, + changed: items.some(i => !i.hadPatreon) || current !== patreon, + declinedChanged: items.some(i => i.declinedChanged), + patreon, + declinedSince: items.map(i => i.declinedSince).find(x => !!x), + declined: items.some(i => i.declined), + hadPatreon: items.some(i => i.hadPatreon), + }; + }) + .filter(({ changed, declinedChanged }) => changed || declinedChanged); - grouped - .filter(g => g.changed) - .map(g => ({ account: g.account, message: supporterMessage(g.patreon, g.declined, g.hadPatreon) })) - .filter(({ message }) => !!message) - .forEach(({ account, message }) => log(`${account}`, message!)); + grouped + .filter(g => g.changed) + .map(g => ({ account: g.account, message: supporterMessage(g.patreon, g.declined, g.hadPatreon) })) + .filter(({ message }) => !!message) + .forEach(({ account, message }) => log(`${account}`, message!)); - logPatreon(`update supporters (${Date.now() - start}ms) ` + - `[auths: ${auths.length}, grouped: ${grouped.length}, pledges: ${pledges.length}, ` + - `accountsWithPatreon: ${accountsWithPatreon.length}]`); + logPatreon(`update supporters (${Date.now() - start}ms) ` + + `[auths: ${auths.length}, grouped: ${grouped.length}, pledges: ${pledges.length}, ` + + `accountsWithPatreon: ${accountsWithPatreon.length}]`); - await Bluebird.map(grouped, ({ account, patreon, declinedSince, changed, declined, hadPatreon }) => { - const message = changed ? supporterMessage(patreon, declined, hadPatreon) : undefined; + await Bluebird.map(grouped, ({ account, patreon, declinedSince, changed, declined, hadPatreon }) => { + const message = changed ? supporterMessage(patreon, declined, hadPatreon) : undefined; - return updateAccount(account, { - supporterDeclinedSince: declinedSince, - ...(changed ? { patreon } : {}), - ...(message ? { - $push: { - supporterLog: { - $each: [{ date: new Date(), message }], - $slice: -supporterLogLimit, - }, - } - } : {}), - }); - }, { concurrency: 4 }); - }; + return updateAccount(account, { + supporterDeclinedSince: declinedSince, + ...(changed ? { patreon } : {}), + ...(message ? { + $push: { + supporterLog: { + $each: [{ date: new Date(), message }], + $slice: -supporterLogLimit, + }, + } + } : {}), + }); + }, { concurrency: 4 }); + }; function isDeclined(declinedSince: Date | undefined, now: Date): boolean { - return !!declinedSince && ( - now.getDate() > declinedDayLimit || - (now.getTime() - declinedSince.getTime()) > declinedTimeLimit); + return !!declinedSince && ( + now.getDate() > declinedDayLimit || + (now.getTime() - declinedSince.getTime()) > declinedTimeLimit); } function supporterMessage(patreon: PatreonFlags, declined: boolean, hadPatreon: boolean) { - return patreon ? - `added supporter (${patreon})` : - (hadPatreon ? `removed supporter${declined ? ' (declined)' : ''}` : undefined); + return patreon ? + `added supporter (${patreon})` : + (hadPatreon ? `removed supporter${declined ? ' (declined)' : ''}` : undefined); } function datesEqual(a: Date | undefined, b: Date | undefined) { - return (!a && !b) || (a && b && a.getTime() === b.getTime()); + return (!a && !b) || (a && b && a.getTime() === b.getTime()); } export const createAddTotalPledged = - (updateAuth: UpdateAuth) => - async (auths: IAuth[], pledges: PatreonPledge[]) => { - const setup = auths - .map(auth => ({ auth, pledge: pledges.find(p => p.user === auth.openId) })) - .filter(({ auth, pledge }) => pledge && pledge.total !== auth.pledged); + (updateAuth: UpdateAuth) => + async (auths: IAuth[], pledges: PatreonPledge[]) => { + const setup = auths + .map(auth => ({ auth, pledge: pledges.find(p => p.user === auth.openId) })) + .filter(({ auth, pledge }) => pledge && pledge.total !== auth.pledged); - await Bluebird.map(setup, ({ auth, pledge }) => - updateAuth(auth._id, { pledged: pledge!.total }), { concurrency: 4 }); - }; + await Bluebird.map(setup, ({ auth, pledge }) => + updateAuth(auth._id, { pledged: pledge!.total }), { concurrency: 4 }); + }; diff --git a/src/ts/server/playerUtils.ts b/src/ts/server/playerUtils.ts index c2d4c46..73134e9 100644 --- a/src/ts/server/playerUtils.ts +++ b/src/ts/server/playerUtils.ts @@ -6,12 +6,12 @@ import * as entities from '../common/entities'; import { isShadowed, isMuted, supporterLevel } from '../common/adminUtils'; import { handlePromiseDefault } from './serverUtils'; import { - removeItem, hasFlag, distance, toInt, includes, array, flatten, containsPointWitBorder, distanceXY, setFlag, invalidEnum + removeItem, hasFlag, distance, toInt, includes, array, flatten, containsPointWitBorder, distanceXY, setFlag, invalidEnum } from '../common/utils'; import { CharacterState, ServerConfig, AccountState, CharacterStateFlags, GameServerSettings } from '../common/adminInterfaces'; import { - EntityState, Expression, PonyOptions, Action, ExpressionExtra, Eye, Muzzle, CLOSED_MUZZLES, - isExpressionAction, EntityPlayerState, UpdateFlags, InteractAction + EntityState, Expression, PonyOptions, Action, ExpressionExtra, Eye, Muzzle, CLOSED_MUZZLES, + isExpressionAction, EntityPlayerState, UpdateFlags, InteractAction } from '../common/interfaces'; import { encodeExpression, EMPTY_EXPRESSION, decodeExpression } from '../common/encoders/expressionEncoder'; import { EXPRESSION_TIMEOUT, DAY, FLY_DELAY, SECOND, PONY_TYPE } from '../common/constants'; @@ -25,13 +25,13 @@ import { isMod } from '../common/accountUtils'; import { getOriginFromHTTP } from './originUtils'; import { createPony, getAndFixCharacterState, updateCharacterState } from './characterUtils'; import { - updateEntityOptions, canBoopEntity, findPlayersThetCanBeSitOn, updateEntityState, updateEntityExpression, - sendAction, pushUpdateEntityToClient, fixPosition, isHoldingGrapes + updateEntityOptions, canBoopEntity, findPlayersThetCanBeSitOn, updateEntityState, updateEntityExpression, + sendAction, pushUpdateEntityToClient, fixPosition, isHoldingGrapes } from './entityUtils'; import { replaceEmojis } from '../client/emoji'; import { expression, parseExpression } from '../common/expressionUtils'; import { - canBoop2, isPonySitting, isPonyStanding, getBoopRect, canStand, isPonyFlying, setPonyState, canSit, canLie + canBoop2, isPonySitting, isPonyStanding, getBoopRect, canStand, isPonyFlying, setPonyState, canSit, canLie } from '../common/entityUtils'; import { withBorder } from '../common/rect'; import { isOnlineFriend } from './services/friends'; @@ -39,441 +39,441 @@ import { grapePurple, grapeGreen, tools } from '../common/entities'; import { saySystem } from './chat'; export function isMutedOrShadowed(client: IClient) { - return client.shadowed || isMuted(client.account); + return client.shadowed || isMuted(client.account); } export function isIgnored(ignoring: IClient, target: IClient): boolean { - return target.ignores.has(ignoring.accountId); + return target.ignores.has(ignoring.accountId); } export function kickClient(client: IClient, reason = 'kicked') { - client.leaveReason = reason; - client.disconnect(true, true); + client.leaveReason = reason; + client.disconnect(true, true); } export function getCounter(client: IClient, key: keyof AccountState) { - return toInt(client.account.state && client.account.state[key]); + return toInt(client.account.state && client.account.state[key]); } export function createClientAndPony( - client: IClient, friends: string[], hides: string[], server: ServerConfig, world: World, states: CounterService + client: IClient, friends: string[], hides: string[], server: ServerConfig, world: World, states: CounterService ) { - const { account, character } = client.tokenData as TokenData; - const origin = client.originalRequest && getOriginFromHTTP(client.originalRequest); - const reporter = create(server, account._id, character._id, origin); - const state = getAndFixCharacterState(server, character, world, states); + const { account, character } = client.tokenData as TokenData; + const origin = client.originalRequest && getOriginFromHTTP(client.originalRequest); + const reporter = create(server, account._id, character._id, origin); + const state = getAndFixCharacterState(server, character, world, states); - client.characterState = state; - const pony = createPony(account, character, state); - pony.client = createClient(client, account, friends, hides, character, pony, world.getMainMap(), reporter, origin); - centerCameraOn(client.camera, pony); + client.characterState = state; + const pony = createPony(account, character, state); + pony.client = createClient(client, account, friends, hides, character, pony, world.getMainMap(), reporter, origin); + centerCameraOn(client.camera, pony); } export function updateClientCharacter(client: IClient, character: ICharacter) { - client.character = character; - client.characterId = client.character._id.toString(); - client.characterName = replaceEmojis(client.character.name); + client.character = character; + client.characterId = client.character._id.toString(); + client.characterName = replaceEmojis(client.character.name); } export function createClient( - client: IClient, account: IAccount, friends: string[], hides: string[], character: ICharacter, pony: ServerEntity, - defaultMap: ServerMap, reporter: Reporter, origin: IOriginInfo | undefined + client: IClient, account: IAccount, friends: string[], hides: string[], character: ICharacter, pony: ServerEntity, + defaultMap: ServerMap, reporter: Reporter, origin: IOriginInfo | undefined ): IClient { - updateClientCharacter(client, character); + updateClientCharacter(client, character); - client.ip = origin && origin.ip || ''; - client.country = origin && origin.country || '??'; - client.userAgent = client.originalRequest && client.originalRequest.headers['user-agent']; + client.ip = origin && origin.ip || ''; + client.country = origin && origin.country || '??'; + client.userAgent = client.originalRequest && client.originalRequest.headers['user-agent']; - client.accountId = account._id.toString(); - client.accountName = account.name; - client.ignores = new Set(account.ignores); - client.hides = new Set(); - client.permaHides = new Set(hides); - client.friends = new Set(friends); - client.friendsCRC = undefined; - client.accountSettings = { ...account.settings }; - client.supporterLevel = supporterLevel(account); - client.isMod = isMod(account); + client.accountId = account._id.toString(); + client.accountName = account.name; + client.ignores = new Set(account.ignores); + client.hides = new Set(); + client.permaHides = new Set(hides); + client.friends = new Set(friends); + client.friendsCRC = undefined; + client.accountSettings = { ...account.settings }; + client.supporterLevel = supporterLevel(account); + client.isMod = isMod(account); - client.reporter = reporter; - client.account = account; - client.character = character; - client.pony = pony; - client.map = defaultMap; - client.isSwitchingMap = false; + client.reporter = reporter; + client.account = account; + client.character = character; + client.pony = pony; + client.map = defaultMap; + client.isSwitchingMap = false; - client.notifications = []; - client.regions = []; + client.notifications = []; + client.regions = []; - client.shadowed = isShadowed(account); - client.country = origin && origin.country || '??'; + client.shadowed = isShadowed(account); + client.country = origin && origin.country || '??'; - client.camera = createCamera(); - client.camera.w = 800; - client.camera.h = 600; + client.camera = createCamera(); + client.camera.w = 800; + client.camera.h = 600; - client.safeX = pony.x; - client.safeY = pony.y; + client.safeX = pony.x; + client.safeY = pony.y; - client.lastPacket = Date.now(); - client.lastAction = 0; - client.lastBoopAction = 0; - client.lastExpressionAction = 0; - client.lastSays = []; - client.lastX = pony.x; - client.lastY = pony.y; - client.lastTime = 0; - client.lastVX = 0; - client.lastVY = 0; - client.lastMapSwitch = 0; + client.lastPacket = Date.now(); + client.lastAction = 0; + client.lastBoopAction = 0; + client.lastExpressionAction = 0; + client.lastSays = []; + client.lastX = pony.x; + client.lastY = pony.y; + client.lastTime = 0; + client.lastVX = 0; + client.lastVY = 0; + client.lastMapSwitch = 0; - client.lastSitX = 0; - client.lastSitY = 0; - client.lastSitTime = 0; - client.sitCount = 0; + client.lastSitX = 0; + client.lastSitY = 0; + client.lastSitTime = 0; + client.sitCount = 0; - client.lastSwap = 0; - client.lastMapLoadOrSave = 0; + client.lastSwap = 0; + client.lastMapLoadOrSave = 0; - client.lastCameraX = 0; - client.lastCameraY = 0; - client.lastCameraW = 0; - client.lastCameraH = 0; + client.lastCameraX = 0; + client.lastCameraY = 0; + client.lastCameraW = 0; + client.lastCameraH = 0; - client.updateQueue = createBinaryWriter(128); - client.regionUpdates = []; - client.saysQueue = []; - client.unsubscribes = []; - client.subscribes = []; + client.updateQueue = createBinaryWriter(128); + client.regionUpdates = []; + client.saysQueue = []; + client.unsubscribes = []; + client.subscribes = []; - client.positions = []; + client.positions = []; - return client; + return client; } export function resetClientUpdates(client: IClient) { - resetWriter(client.updateQueue); - client.regionUpdates.length = 0; - client.saysQueue.length = 0; - client.unsubscribes.length = 0; - client.subscribes.length = 0; + resetWriter(client.updateQueue); + client.regionUpdates.length = 0; + client.saysQueue.length = 0; + client.unsubscribes.length = 0; + client.subscribes.length = 0; } export function createCharacterState(entity: ServerEntity, map: ServerMap): CharacterState { - const options = entity.options as PonyOptions; - const flags: CharacterStateFlags = - (hasFlag(entity.state, EntityState.FacingRight) ? CharacterStateFlags.Right : 0) | - (options.extra ? CharacterStateFlags.Extra : 0); - const state: CharacterState = { x: entity.x, y: entity.y }; + const options = entity.options as PonyOptions; + const flags: CharacterStateFlags = + (hasFlag(entity.state, EntityState.FacingRight) ? CharacterStateFlags.Right : 0) | + (options.extra ? CharacterStateFlags.Extra : 0); + const state: CharacterState = { x: entity.x, y: entity.y }; - if (flags) { - state.flags = flags; - } + if (flags) { + state.flags = flags; + } - if (map.id) { - state.map = map.id; - } + if (map.id) { + state.map = map.id; + } - if (options.hold) { - state.hold = entities.getEntityTypeName(options.hold); - } + if (options.hold) { + state.hold = entities.getEntityTypeName(options.hold); + } - if (options.toy) { - state.toy = options.toy; - } + if (options.toy) { + state.toy = options.toy; + } - return state; + return state; } export async function createAndUpdateCharacterState(client: IClient, server: ServerConfig) { - const state = createCharacterState(client.pony, client.map); - await updateCharacterState(client.characterId, server.id, state); + const state = createCharacterState(client.pony, client.map); + await updateCharacterState(client.characterId, server.id, state); } // utils export function addIgnore(target: IClient, accountId: string) { - target.account.ignores = target.account.ignores || []; - target.account.ignores.push(accountId); - target.ignores.add(accountId); + target.account.ignores = target.account.ignores || []; + target.account.ignores.push(accountId); + target.ignores.add(accountId); } export function removeIgnore(target: IClient, accountId: string) { - if (target.account.ignores) { - removeItem(target.account.ignores, accountId); - } + if (target.account.ignores) { + removeItem(target.account.ignores, accountId); + } - target.ignores.delete(accountId); + target.ignores.delete(accountId); } export const createIgnorePlayer = - (updateAccount: UpdateAccount, handlePromise = handlePromiseDefault) => - (client: IClient, target: IClient, ignored: boolean) => { - if (target.accountId === client.accountId) - return; + (updateAccount: UpdateAccount, handlePromise = handlePromiseDefault) => + (client: IClient, target: IClient, ignored: boolean) => { + if (target.accountId === client.accountId) + return; - const id = client.accountId; - const is = isIgnored(client, target); + const id = client.accountId; + const is = isIgnored(client, target); - if (ignored === is) - return; + if (ignored === is) + return; - if (ignored) { - addIgnore(target, id); - } else { - removeIgnore(target, id); - } + if (ignored) { + addIgnore(target, id); + } else { + removeIgnore(target, id); + } - handlePromise(updateAccount(target.accountId, { [ignored ? '$push' : '$pull']: { ignores: id } }) - .then(() => updateEntityPlayerState(client, target.pony)) - .then(() => { - const { accountId, account, character } = target; - const message = `${ignored ? 'ignored' : 'unignored'} ${character.name} (${account.name}) [${accountId}]`; - client.reporter.systemLog(message); - }), client.reporter.error); - }; + handlePromise(updateAccount(target.accountId, { [ignored ? '$push' : '$pull']: { ignores: id } }) + .then(() => updateEntityPlayerState(client, target.pony)) + .then(() => { + const { accountId, account, character } = target; + const message = `${ignored ? 'ignored' : 'unignored'} ${character.name} (${account.name}) [${accountId}]`; + client.reporter.systemLog(message); + }), client.reporter.error); + }; export function findClientByEntityId(self: IClient, entityId: number): IClient | undefined { - const selected = self.selected; + const selected = self.selected; - if (selected && selected.id === entityId && selected.client) { - return selected.client; - } + if (selected && selected.id === entityId && selected.client) { + return selected.client; + } - if (self.party) { // TODO: remove ? - const client = self.party.clients.find(c => c.pony.id === entityId); + if (self.party) { // TODO: remove ? + const client = self.party.clients.find(c => c.pony.id === entityId); - if (client) { - //this.logger.log('client from party'); - return client; - } + if (client) { + //this.logger.log('client from party'); + return client; + } - const pending = self.party.pending.find(c => c.client.pony.id === entityId); + const pending = self.party.pending.find(c => c.client.pony.id === entityId); - if (pending) { - //this.logger.log('pending from party'); - return pending.client; - } - } + if (pending) { + //this.logger.log('pending from party'); + return pending.client; + } + } - const notification = self.notifications.find(c => c.entityId === entityId); + const notification = self.notifications.find(c => c.entityId === entityId); - if (notification) { - //this.logger.log('sender from notification'); - return notification.sender; - } + if (notification) { + //this.logger.log('sender from notification'); + return notification.sender; + } - return undefined; + return undefined; } export function cancelEntityExpression(entity: ServerEntity) { - if (entity.exprCancellable) { - setEntityExpression(entity, undefined); - } + if (entity.exprCancellable) { + setEntityExpression(entity, undefined); + } } export function setEntityExpression( - entity: ServerEntity, expression: Expression | undefined, timeout = EXPRESSION_TIMEOUT, cancellable = false + entity: ServerEntity, expression: Expression | undefined, timeout = EXPRESSION_TIMEOUT, cancellable = false ) { - expression = expression || entity.exprPermanent; - const expr = encodeExpression(expression); + expression = expression || entity.exprPermanent; + const expr = encodeExpression(expression); - (entity.options as PonyOptions).expr = expr; + (entity.options as PonyOptions).expr = expr; - if (expression && timeout) { - entity.exprTimeout = Date.now() + timeout; - } else { - entity.exprTimeout = undefined; - } + if (expression && timeout) { + entity.exprTimeout = Date.now() + timeout; + } else { + entity.exprTimeout = undefined; + } - const sleeping = expression !== undefined && hasFlag(expression.extra, ExpressionExtra.Zzz); - entity.exprCancellable = cancellable || sleeping; + const sleeping = expression !== undefined && hasFlag(expression.extra, ExpressionExtra.Zzz); + entity.exprCancellable = cancellable || sleeping; - updateEntityExpression(entity); + updateEntityExpression(entity); } export function playerBlush(pony: ServerEntity, args = '') { - const expr = parseOrCurrentExpression(pony, args) || expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral); - expr.extra |= ExpressionExtra.Blush; - setEntityExpression(pony, expr, DAY, !!pony.exprCancellable); + const expr = parseOrCurrentExpression(pony, args) || expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral); + expr.extra |= ExpressionExtra.Blush; + setEntityExpression(pony, expr, DAY, !!pony.exprCancellable); } export function parseOrCurrentExpression(pony: ServerEntity, message: string) { - return parseExpression(message) - || decodeExpression((!pony.options || pony.options.expr == null) ? EMPTY_EXPRESSION : pony.options.expr); + return parseExpression(message) + || decodeExpression((!pony.options || pony.options.expr == null) ? EMPTY_EXPRESSION : pony.options.expr); } export function playerSleep(pony: ServerEntity, args = '') { - if (pony.vx === 0 && pony.vy === 0) { - const base = parseOrCurrentExpression(pony, args) || expression(Eye.Closed, Eye.Closed, Muzzle.Neutral); - const muzzle = CLOSED_MUZZLES.indexOf(base.muzzle) !== -1 ? base.muzzle : Muzzle.Neutral; - const expr = { ...base, muzzle, left: Eye.Closed, right: Eye.Closed, extra: ExpressionExtra.Zzz }; - setEntityExpression(pony, expr, 0, true); - } + if (pony.vx === 0 && pony.vy === 0) { + const base = parseOrCurrentExpression(pony, args) || expression(Eye.Closed, Eye.Closed, Muzzle.Neutral); + const muzzle = CLOSED_MUZZLES.indexOf(base.muzzle) !== -1 ? base.muzzle : Muzzle.Neutral; + const expr = { ...base, muzzle, left: Eye.Closed, right: Eye.Closed, extra: ExpressionExtra.Zzz }; + setEntityExpression(pony, expr, 0, true); + } } export function playerLove(pony: ServerEntity, args = '') { - const expr = parseOrCurrentExpression(pony, args) || expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile); - expr.extra |= ExpressionExtra.Hearts; - setEntityExpression(pony, expr, DAY, !!pony.exprCancellable); + const expr = parseOrCurrentExpression(pony, args) || expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile); + expr.extra |= ExpressionExtra.Hearts; + setEntityExpression(pony, expr, DAY, !!pony.exprCancellable); } export function playerCry(pony: ServerEntity, args = '') { - const expr = parseExpression(args) || expression(Eye.Sad, Eye.Sad, Muzzle.Frown); - expr.extra = expr.extra | ExpressionExtra.Cry; - setEntityExpression(pony, expr, 0); + const expr = parseExpression(args) || expression(Eye.Sad, Eye.Sad, Muzzle.Frown); + expr.extra = expr.extra | ExpressionExtra.Cry; + setEntityExpression(pony, expr, 0); } const fruitTypes = entities.fruits.map(f => f.type); export function interactWith(client: IClient, target: ServerEntity | undefined) { - if (target) { - const pony = client.pony; + if (target) { + const pony = client.pony; - if (target.interact && (!target.interactRange || distance(pony, target) < target.interactRange)) { - target.interact(target, client); - } else if (target.triggerBounds && target.trigger) { - if (containsPointWitBorder(target.x, target.y, target.triggerBounds, pony.x, pony.y, 3)) { - target.trigger(target, client); - } else { - DEVELOPMENT && console.warn(`outside trigger bounds ` + - `(bounds: ${target.x} ${target.y} ${JSON.stringify(target.triggerBounds)} point: ${pony.x} ${pony.y})`); - } - } else if (target.interactAction) { - switch (target.interactAction) { - case InteractAction.Toolbox: { - switchTool(client, false); - break; - } - case InteractAction.GiveLantern: { - if (client.pony.options!.hold === entities.lanternOn.type) { - unholdItem(pony); - } else { - holdItem(pony, entities.lanternOn.type); - } - break; - } - case InteractAction.GiveFruits: { - const index = fruitTypes.indexOf(client.pony.options!.hold || 0) + 1; - holdItem(client.pony, fruitTypes[index % fruitTypes.length]); - break; - } - case InteractAction.GiveCookie1: { - const hold = client.pony.options!.hold; - let cookie = hold; + if (target.interact && (!target.interactRange || distance(pony, target) < target.interactRange)) { + target.interact(target, client); + } else if (target.triggerBounds && target.trigger) { + if (containsPointWitBorder(target.x, target.y, target.triggerBounds, pony.x, pony.y, 3)) { + target.trigger(target, client); + } else { + DEVELOPMENT && console.warn(`outside trigger bounds ` + + `(bounds: ${target.x} ${target.y} ${JSON.stringify(target.triggerBounds)} point: ${pony.x} ${pony.y})`); + } + } else if (target.interactAction) { + switch (target.interactAction) { + case InteractAction.Toolbox: { + switchTool(client, false); + break; + } + case InteractAction.GiveLantern: { + if (client.pony.options!.hold === entities.lanternOn.type) { + unholdItem(pony); + } else { + holdItem(pony, entities.lanternOn.type); + } + break; + } + case InteractAction.GiveFruits: { + const index = fruitTypes.indexOf(client.pony.options!.hold || 0) + 1; + holdItem(client.pony, fruitTypes[index % fruitTypes.length]); + break; + } + case InteractAction.GiveCookie1: { + const hold = client.pony.options!.hold; + let cookie = hold; - while (hold === cookie) { - cookie = sample(entities.candies1Types)!; - } + while (hold === cookie) { + cookie = sample(entities.candies1Types)!; + } - holdItem(client.pony, cookie!); - break; - } - case InteractAction.GiveCookie2: { - const hold = client.pony.options!.hold; - let cookie = hold; + holdItem(client.pony, cookie!); + break; + } + case InteractAction.GiveCookie2: { + const hold = client.pony.options!.hold; + let cookie = hold; - while (hold === cookie) { - cookie = sample(entities.candies2Types)!; - } + while (hold === cookie) { + cookie = sample(entities.candies2Types)!; + } - holdItem(client.pony, cookie!); - break; - } - default: - invalidEnum(target.interactAction); - } - } - } + holdItem(client.pony, cookie!); + break; + } + default: + invalidEnum(target.interactAction); + } + } + } } export function useHeldItem(client: IClient) { - const hold = client.pony.options!.hold || 0; + const hold = client.pony.options!.hold || 0; - if (isGift(hold)) { - openGift(client); - } + if (isGift(hold)) { + openGift(client); + } } export function canPerformAction(client: IClient) { - return client.lastAction < Date.now(); + return client.lastAction < Date.now(); } export function updateEntityPlayerState(client: IClient, entity: ServerEntity) { - const playerState = getPlayerState(client, entity); - pushUpdateEntityToClient(client, { entity, flags: UpdateFlags.PlayerState, playerState }); + const playerState = getPlayerState(client, entity); + pushUpdateEntityToClient(client, { entity, flags: UpdateFlags.PlayerState, playerState }); } // actions export function turnHead(client: IClient) { - if (canPerformAction(client)) { - updateEntityState(client.pony, client.pony.state ^ EntityState.HeadTurned); - } + if (canPerformAction(client)) { + updateEntityState(client.pony, client.pony.state ^ EntityState.HeadTurned); + } } const purpleGrapeTypes = entities.grapesPurple.map(x => x.type); const greenGrapeTypes = entities.grapesGreen.map(x => x.type); export function boop(client: IClient, now: number) { - if (canPerformAction(client) && canBoop2(client.pony) && client.lastBoopAction < now) { - cancelEntityExpression(client.pony); - sendAction(client.pony, Action.Boop); + if (canPerformAction(client) && canBoop2(client.pony) && client.lastBoopAction < now) { + cancelEntityExpression(client.pony); + sendAction(client.pony, Action.Boop); - if (!client.shadowed && (isPonySitting(client.pony) || isPonyStanding(client.pony))) { - const boopRect = getBoopRect(client.pony); - const boopBounds = withBorder(boopRect, 1); - const entities = findEntitiesInBounds(client.map, boopBounds); - const entity = entities.find(e => canBoopEntity(e, boopRect)); + if (!client.shadowed && (isPonySitting(client.pony) || isPonyStanding(client.pony))) { + const boopRect = getBoopRect(client.pony); + const boopBounds = withBorder(boopRect, 1); + const entities = findEntitiesInBounds(client.map, boopBounds); + const entity = entities.find(e => canBoopEntity(e, boopRect)); - if (entity) { - if (entity.boop) { - entity.boop(client); - } else if (entity.type === PONY_TYPE) { - const clientHold = client.pony.options!.hold || 0; + if (entity) { + if (entity.boop) { + entity.boop(client); + } else if (entity.type === PONY_TYPE) { + const clientHold = client.pony.options!.hold || 0; - if (isHoldingGrapes(entity) && clientHold !== grapeGreen.type && clientHold !== grapePurple.type) { - let index = purpleGrapeTypes.indexOf(entity.options!.hold || 0); + if (isHoldingGrapes(entity) && clientHold !== grapeGreen.type && clientHold !== grapePurple.type) { + let index = purpleGrapeTypes.indexOf(entity.options!.hold || 0); - if (index !== -1) { - holdItem(client.pony, grapePurple.type); + if (index !== -1) { + holdItem(client.pony, grapePurple.type); - if (index === (purpleGrapeTypes.length - 1)) { - unholdItem(entity); - } else { - holdItem(entity, purpleGrapeTypes[index + 1]); - } - } else { - let index = greenGrapeTypes.indexOf(entity.options!.hold || 0); + if (index === (purpleGrapeTypes.length - 1)) { + unholdItem(entity); + } else { + holdItem(entity, purpleGrapeTypes[index + 1]); + } + } else { + let index = greenGrapeTypes.indexOf(entity.options!.hold || 0); - if (index !== -1) { - holdItem(client.pony, grapeGreen.type); + if (index !== -1) { + holdItem(client.pony, grapeGreen.type); - if (index === (greenGrapeTypes.length - 1)) { - unholdItem(entity); - } else { - holdItem(entity, greenGrapeTypes[index + 1]); - } - } - } - } - } - } - } + if (index === (greenGrapeTypes.length - 1)) { + unholdItem(entity); + } else { + holdItem(entity, greenGrapeTypes[index + 1]); + } + } + } + } + } + } + } - client.lastBoopAction = now + 500; - } + client.lastBoopAction = now + 500; + } } export function stand(client: IClient) { - if (canPerformAction(client) && canStand(client.pony, client.map)) { - if (!isPonyFlying(client.pony)) { - cancelEntityExpression(client.pony); - } + if (canPerformAction(client) && canStand(client.pony, client.map)) { + if (!isPonyFlying(client.pony)) { + cancelEntityExpression(client.pony); + } - updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyStanding)); - } + updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyStanding)); + } } const SIT_MAX_TIME = 2 * SECOND; @@ -481,86 +481,86 @@ const SIT_MAX_DIST = 1; const SIT_MAX_COUNT = 5; function checkSuspiciousSitting(client: IClient) { - const now = Date.now(); - const { x, y } = client.pony; - const dist = distanceXY(x, y, client.lastSitX, client.lastSitY); + const now = Date.now(); + const { x, y } = client.pony; + const dist = distanceXY(x, y, client.lastSitX, client.lastSitY); - if ((now - client.lastSitTime) < SIT_MAX_TIME && dist < SIT_MAX_DIST && findPlayersThetCanBeSitOn(client.map, client.pony)) { - client.sitCount++; + if ((now - client.lastSitTime) < SIT_MAX_TIME && dist < SIT_MAX_DIST && findPlayersThetCanBeSitOn(client.map, client.pony)) { + client.sitCount++; - if (client.sitCount > SIT_MAX_COUNT) { - client.reporter.warn(`Suspicious sitting`); - client.sitCount = 0; - } - } else { - client.sitCount = 1; - } + if (client.sitCount > SIT_MAX_COUNT) { + client.reporter.warn(`Suspicious sitting`); + client.sitCount = 0; + } + } else { + client.sitCount = 1; + } - client.lastSitX = x; - client.lastSitY = y; - client.lastSitTime = now; + client.lastSitX = x; + client.lastSitY = y; + client.lastSitTime = now; } export function sit(client: IClient, settings: GameServerSettings) { - if (canPerformAction(client) && canSit(client.pony, client.map)) { - updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonySitting)); + if (canPerformAction(client) && canSit(client.pony, client.map)) { + updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonySitting)); - if (settings.reportSitting) { - checkSuspiciousSitting(client); - } - } + if (settings.reportSitting) { + checkSuspiciousSitting(client); + } + } } export function lie(client: IClient) { - if (canPerformAction(client) && canLie(client.pony, client.map)) { - updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyLying)); - } + if (canPerformAction(client) && canLie(client.pony, client.map)) { + updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyLying)); + } } export function fly(client: IClient) { - if (canPerformAction(client) && client.pony.canFly && !isPonyFlying(client.pony)) { - cancelEntityExpression(client.pony); - updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyFlying)); - client.pony.inTheAirDelay = FLY_DELAY; - } + if (canPerformAction(client) && client.pony.canFly && !isPonyFlying(client.pony)) { + cancelEntityExpression(client.pony); + updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyFlying)); + client.pony.inTheAirDelay = FLY_DELAY; + } } export function expressionAction(client: IClient, action: Action) { - if (canPerformAction(client) && isExpressionAction(action) && client.lastExpressionAction < Date.now()) { - cancelEntityExpression(client.pony); - sendAction(client.pony, action); - client.lastExpressionAction = Date.now() + 500; - } + if (canPerformAction(client) && isExpressionAction(action) && client.lastExpressionAction < Date.now()) { + cancelEntityExpression(client.pony); + sendAction(client.pony, action); + client.lastExpressionAction = Date.now() + 500; + } } // hold export function holdItem(entity: ServerEntity, hold: number) { - if (entity.options && entity.options.hold !== hold) { - updateEntityOptions(entity, { hold }); - } + if (entity.options && entity.options.hold !== hold) { + updateEntityOptions(entity, { hold }); + } } export function unholdItem(entity: ServerEntity) { - if (entity.options && entity.options.hold) { - updateEntityOptions(entity, { hold: 0 }); - delete entity.options.hold; - } + if (entity.options && entity.options.hold) { + updateEntityOptions(entity, { hold: 0 }); + delete entity.options.hold; + } } // toy export function holdToy(entity: ServerEntity, toy: number) { - if (entity.options && entity.options.toy !== toy) { - updateEntityOptions(entity, { toy }); - } + if (entity.options && entity.options.toy !== toy) { + updateEntityOptions(entity, { toy }); + } } export function unholdToy(entity: ServerEntity) { - if (entity.options && entity.options.toy) { - updateEntityOptions(entity, { toy: 0 }); - delete entity.options.toy; - } + if (entity.options && entity.options.toy) { + updateEntityOptions(entity, { toy: 0 }); + delete entity.options.toy; + } } // gifts and toys @@ -568,48 +568,48 @@ export function unholdToy(entity: ServerEntity) { const giftTypes = [entities.gift2.type]; const toys = [ - // hat - { type: 0, multiplier: 20 }, - { type: 0, multiplier: 10 }, - { type: 0, multiplier: 5 }, - { type: 0, multiplier: 1 }, // pink - // snowpony - { type: 0, multiplier: 20 }, - { type: 0, multiplier: 10 }, // clothes - { type: 0, multiplier: 1 }, // evil - // gift - { type: 0, multiplier: 20 }, - { type: 0, multiplier: 10 }, - { type: 0, multiplier: 10 }, - { type: 0, multiplier: 5 }, - { type: 0, multiplier: 1 }, - // hanging thing - { type: 0, multiplier: 20 }, // bell - { type: 0, multiplier: 10 }, // mistletoe - { type: 0, multiplier: 5 }, // cookie - { type: 0, multiplier: 1 }, // spider - // teddy - { type: 0, multiplier: 20 }, // brown - { type: 0, multiplier: 10 }, // brown angel - { type: 0, multiplier: 20 }, // black - { type: 0, multiplier: 10 }, // black angel - { type: 0, multiplier: 5 }, // brown clothes - { type: 0, multiplier: 5 }, // black clothes - { type: 0, multiplier: 1 }, // white santa - // xmas tree - { type: 0, multiplier: 10 }, - { type: 0, multiplier: 5 }, - // deer - { type: 0, multiplier: 5 }, - { type: 0, multiplier: 1 }, // with clothes - // candy horns - { type: 0, multiplier: 10 }, // one - { type: 0, multiplier: 2 }, // two - { type: 0, multiplier: 1 }, // two (alt) - // star - { type: 0, multiplier: 5 }, - // halo - { type: 0, multiplier: 5 }, + // hat + { type: 0, multiplier: 20 }, + { type: 0, multiplier: 10 }, + { type: 0, multiplier: 5 }, + { type: 0, multiplier: 1 }, // pink + // snowpony + { type: 0, multiplier: 20 }, + { type: 0, multiplier: 10 }, // clothes + { type: 0, multiplier: 1 }, // evil + // gift + { type: 0, multiplier: 20 }, + { type: 0, multiplier: 10 }, + { type: 0, multiplier: 10 }, + { type: 0, multiplier: 5 }, + { type: 0, multiplier: 1 }, + // hanging thing + { type: 0, multiplier: 20 }, // bell + { type: 0, multiplier: 10 }, // mistletoe + { type: 0, multiplier: 5 }, // cookie + { type: 0, multiplier: 1 }, // spider + // teddy + { type: 0, multiplier: 20 }, // brown + { type: 0, multiplier: 10 }, // brown angel + { type: 0, multiplier: 20 }, // black + { type: 0, multiplier: 10 }, // black angel + { type: 0, multiplier: 5 }, // brown clothes + { type: 0, multiplier: 5 }, // black clothes + { type: 0, multiplier: 1 }, // white santa + // xmas tree + { type: 0, multiplier: 10 }, + { type: 0, multiplier: 5 }, + // deer + { type: 0, multiplier: 5 }, + { type: 0, multiplier: 1 }, // with clothes + // candy horns + { type: 0, multiplier: 10 }, // one + { type: 0, multiplier: 2 }, // two + { type: 0, multiplier: 1 }, // two (alt) + // star + { type: 0, multiplier: 5 }, + // halo + { type: 0, multiplier: 5 }, ]; toys.forEach((toy, i) => toy.type = i + 1); @@ -617,195 +617,195 @@ toys.forEach((toy, i) => toy.type = i + 1); const toyTypes = flatten(toys.map(x => array(x.multiplier, x.type))); function hasToyUnlocked(type: number, collectedToys: number) { - const index = toys.findIndex(t => t.type === type); - return hasFlag(collectedToys, 1 << index); + const index = toys.findIndex(t => t.type === type); + return hasFlag(collectedToys, 1 << index); } function unlockToy(type: number, collectedToys: number) { - const index = toys.findIndex(t => t.type === type); - return collectedToys | (1 << index); + const index = toys.findIndex(t => t.type === type); + return collectedToys | (1 << index); } export function getCollectedToysCount(client: IClient) { - const stateToys = toInt((client.account.state || {}).toys); - const total = toys.length; - let collected = 0; + const stateToys = toInt((client.account.state || {}).toys); + const total = toys.length; + let collected = 0; - for (let i = 0, bit = 1; i < total; i++ , bit <<= 1) { - if (stateToys & bit) { - collected++; - } - } + for (let i = 0, bit = 1; i < total; i++ , bit <<= 1) { + if (stateToys & bit) { + collected++; + } + } - return { collected, total }; + return { collected, total }; } export function getNextToyOrExtra(client: IClient) { - const collectedToys = toInt((client.account.state || {}).toys); - const options = client.pony.options || {}; - const extra = !!options.extra; - const toy = toInt(options.toy); + const collectedToys = toInt((client.account.state || {}).toys); + const options = client.pony.options || {}; + const extra = !!options.extra; + const toy = toInt(options.toy); - if (extra) { - return { extra: false, toy: 0 }; - } else { - for (let i = toys.findIndex(t => t.type === toy) + 1; i < toys.length; i++) { - const type = toys[i].type; + if (extra) { + return { extra: false, toy: 0 }; + } else { + for (let i = toys.findIndex(t => t.type === toy) + 1; i < toys.length; i++) { + const type = toys[i].type; - if (hasToyUnlocked(type, collectedToys)) { - return { extra: false, toy: type }; - } - } + if (hasToyUnlocked(type, collectedToys)) { + return { extra: false, toy: type }; + } + } - return { extra: true, toy: 0 }; - } + return { extra: true, toy: 0 }; + } } export function openGift(client: IClient) { - const options = client.pony.options || {}; + const options = client.pony.options || {}; - if (isGift(options.hold)) { - let toyType = 0; + if (isGift(options.hold)) { + let toyType = 0; - do { - toyType = sample(toyTypes)!; - } while (toyType === options.toy); + do { + toyType = sample(toyTypes)!; + } while (toyType === options.toy); - sendAction(client.pony, Action.HoldPoof); - unholdItem(client.pony); - setTimeout(() => holdToy(client.pony, toyType), 200); + sendAction(client.pony, Action.HoldPoof); + unholdItem(client.pony); + setTimeout(() => holdToy(client.pony, toyType), 200); - const state = client.account.state || {}; + const state = client.account.state || {}; - if (!hasToyUnlocked(toyType, toInt(state.toys))) { - updateAccountState(client.account, state => { - state.toys = unlockToy(toyType, toInt(state.toys)); - }); - } - } + if (!hasToyUnlocked(toyType, toInt(state.toys))) { + updateAccountState(client.account, state => { + state.toys = unlockToy(toyType, toInt(state.toys)); + }); + } + } } export function isGift(type: number | undefined) { - return type !== undefined && includes(giftTypes, type); + return type !== undefined && includes(giftTypes, type); } export function isHiddenBy(a: IClient, b: IClient) { - return a.hides.has(b.accountId) || b.hides.has(a.accountId) || - a.permaHides.has(b.accountId) || b.permaHides.has(a.accountId); + return a.hides.has(b.accountId) || b.hides.has(a.accountId) || + a.permaHides.has(b.accountId) || b.permaHides.has(a.accountId); } export function getPlayerState(client: IClient, entity: ServerEntity): EntityPlayerState { - let state = EntityPlayerState.None; + let state = EntityPlayerState.None; - if (entity.client !== undefined) { - if (isIgnored(client, entity.client)) { - state |= EntityPlayerState.Ignored; - } + if (entity.client !== undefined) { + if (isIgnored(client, entity.client)) { + state |= EntityPlayerState.Ignored; + } - if (isHiddenBy(client, entity.client)) { - state |= EntityPlayerState.Hidden; - } + if (isHiddenBy(client, entity.client)) { + state |= EntityPlayerState.Hidden; + } - if (isOnlineFriend(client, entity.client)) { - state |= EntityPlayerState.Friend; - } - } + if (isOnlineFriend(client, entity.client)) { + state |= EntityPlayerState.Friend; + } + } - return state; + return state; } export async function reloadFriends(client: IClient) { - const friends = await findFriendIds(client.accountId); - client.friends = new Set(friends); - client.friendsCRC = undefined; - client.actionParam(0, Action.FriendsCRC, undefined); + const friends = await findFriendIds(client.accountId); + client.friends = new Set(friends); + client.friendsCRC = undefined; + client.actionParam(0, Action.FriendsCRC, undefined); } export function execAction(client: IClient, action: Action, settings: GameServerSettings) { - switch (action) { - case Action.Boop: - boop(client, Date.now()); - break; - case Action.TurnHead: - turnHead(client); - break; - case Action.Stand: - stand(client); - break; - case Action.Sit: - sit(client, settings); - break; - case Action.Lie: - lie(client); - break; - case Action.Fly: - fly(client); - break; - case Action.Drop: - unholdItem(client.pony); - break; - case Action.Sleep: - playerSleep(client.pony); - break; - case Action.Blush: - playerBlush(client.pony); - break; - case Action.Cry: - playerCry(client.pony); - break; - case Action.Love: - playerLove(client.pony); - break; - case Action.DropToy: - unholdToy(client.pony); - updateEntityOptions(client.pony, { extra: false }); - break; - case Action.Magic: - if (client.pony.canMagic) { - const has = hasFlag(client.pony.state, EntityState.Magic); - updateEntityState(client.pony, setFlag(client.pony.state, EntityState.Magic, !has)); - } - break; - case Action.SwitchTool: - switchTool(client, false); - break; - case Action.SwitchToolRev: - switchTool(client, true); - break; - case Action.SwitchToPlaceTool: - holdItem(client.pony, entities.hammer.type); - break; - case Action.SwitchToTileTool: - holdItem(client.pony, entities.shovel.type); - break; - default: - if (isExpressionAction(action)) { - expressionAction(client, action); - } else { - throw new Error(`Invalid action (${action})`); - } - break; - } + switch (action) { + case Action.Boop: + boop(client, Date.now()); + break; + case Action.TurnHead: + turnHead(client); + break; + case Action.Stand: + stand(client); + break; + case Action.Sit: + sit(client, settings); + break; + case Action.Lie: + lie(client); + break; + case Action.Fly: + fly(client); + break; + case Action.Drop: + unholdItem(client.pony); + break; + case Action.Sleep: + playerSleep(client.pony); + break; + case Action.Blush: + playerBlush(client.pony); + break; + case Action.Cry: + playerCry(client.pony); + break; + case Action.Love: + playerLove(client.pony); + break; + case Action.DropToy: + unholdToy(client.pony); + updateEntityOptions(client.pony, { extra: false }); + break; + case Action.Magic: + if (client.pony.canMagic) { + const has = hasFlag(client.pony.state, EntityState.Magic); + updateEntityState(client.pony, setFlag(client.pony.state, EntityState.Magic, !has)); + } + break; + case Action.SwitchTool: + switchTool(client, false); + break; + case Action.SwitchToolRev: + switchTool(client, true); + break; + case Action.SwitchToPlaceTool: + holdItem(client.pony, entities.hammer.type); + break; + case Action.SwitchToTileTool: + holdItem(client.pony, entities.shovel.type); + break; + default: + if (isExpressionAction(action)) { + expressionAction(client, action); + } else { + throw new Error(`Invalid action (${action})`); + } + break; + } } export function switchTool(client: IClient, reverse: boolean) { - const hold = client.pony.options!.hold || 0; - const index = tools.findIndex(t => t.type === hold); - const unholdIndex = reverse ? 0 : tools.length - 1; + const hold = client.pony.options!.hold || 0; + const index = tools.findIndex(t => t.type === hold); + const unholdIndex = reverse ? 0 : tools.length - 1; - if (index === unholdIndex) { - unholdItem(client.pony); - } else { - const newIndex = reverse ? (index === -1 ? tools.length - 1 : index - 1) : ((index + 1) % tools.length); - const tool = tools[newIndex]; - holdItem(client.pony, tool.type); - saySystem(client, tool.text); - } + if (index === unholdIndex) { + unholdItem(client.pony); + } else { + const newIndex = reverse ? (index === -1 ? tools.length - 1 : index - 1) : ((index + 1) % tools.length); + const tool = tools[newIndex]; + holdItem(client.pony, tool.type); + saySystem(client, tool.text); + } } export function teleportTo(client: IClient, x: number, y: number) { - fixPosition(client.pony, client.map, x, y, true); - client.safeX = client.pony.x; - client.safeY = client.pony.y; - client.lastTime = 0; + fixPosition(client.pony, client.map, x, y, true); + client.safeX = client.pony.x; + client.safeY = client.pony.y; + client.lastTime = 0; } diff --git a/src/ts/server/polling.ts b/src/ts/server/polling.ts index cd18208..c7176d5 100644 --- a/src/ts/server/polling.ts +++ b/src/ts/server/polling.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as Bluebird from 'bluebird'; import { - InternalGameServerState, BannedMuted, Settings, ServerConfig, InternalLoginServerState, SupporterFlags + InternalGameServerState, BannedMuted, Settings, ServerConfig, InternalLoginServerState, SupporterFlags } from '../common/adminInterfaces'; import { fromNow, delay } from '../common/utils'; import { logger, logPatreon, system, logPerformance } from './logger'; @@ -9,8 +9,8 @@ import { Auth, updateAccounts, updateAccount, queryAuths, queryAccounts, updateA import { getDiskSpace, getCertificateExpirationDate, getMemoryUsage } from './serverUtils'; import { HOUR, MINUTE, DAY, SECOND, YEAR } from '../common/constants'; import { - fetchPatreonData, createPatreonClient, createUpdatePatreonInfo, createRemoveOldSupporters, - createUpdateSupporters, createAddTotalPledged + fetchPatreonData, createPatreonClient, createUpdatePatreonInfo, createRemoveOldSupporters, + createUpdateSupporters, createAddTotalPledged } from './patreon'; import { create } from './reporter'; import { servers, serverStatus, loginServers, RemovedDocument } from './internal'; @@ -23,222 +23,222 @@ import { config } from './config'; let updatingPatreonPromise: Promise | undefined; async function updatePatreonDataInternal(server: ServerConfig, accessToken: string) { - try { - const removeOldSupporters = createRemoveOldSupporters(updateAccounts, system); - const updateSupporters = createUpdateSupporters(updateAccount, system); - const addTotalPledged = createAddTotalPledged(updateAuth); - const updatePatreonInfo = createUpdatePatreonInfo( - queryAuths, queryAccounts, removeOldSupporters, updateSupporters, addTotalPledged); + try { + const removeOldSupporters = createRemoveOldSupporters(updateAccounts, system); + const updateSupporters = createUpdateSupporters(updateAccount, system); + const addTotalPledged = createAddTotalPledged(updateAuth); + const updatePatreonInfo = createUpdatePatreonInfo( + queryAuths, queryAccounts, removeOldSupporters, updateSupporters, addTotalPledged); - const client = createPatreonClient(accessToken); - const data = await fetchPatreonData(client, logPatreon); - await updatePatreonInfo(data, new Date()); + const client = createPatreonClient(accessToken); + const data = await fetchPatreonData(client, logPatreon); + await updatePatreonInfo(data, new Date()); - serverStatus.lastPatreonUpdate = (new Date()).toISOString(); - } catch (e) { - const message = e.error ? (e.error.message || e.error.statusText || `${e}`) : e.message; - const stack = (e.error ? e.error.stack : e.stack) || ''; - create(server).danger('Patreon update failed', `${message}\n${stack}`.trim()); - logger.error(e); - } finally { - updatingPatreonPromise = undefined; - } + serverStatus.lastPatreonUpdate = (new Date()).toISOString(); + } catch (e) { + const message = e.error ? (e.error.message || e.error.statusText || `${e}`) : e.message; + const stack = (e.error ? e.error.stack : e.stack) || ''; + create(server).danger('Patreon update failed', `${message}\n${stack}`.trim()); + logger.error(e); + } finally { + updatingPatreonPromise = undefined; + } } export async function updatePatreonData(server: ServerConfig, { patreonToken }: Settings) { - if (patreonToken && config.supporterLink) { - return updatingPatreonPromise = updatingPatreonPromise || updatePatreonDataInternal(server, patreonToken); - } + if (patreonToken && config.supporterLink) { + return updatingPatreonPromise = updatingPatreonPromise || updatePatreonDataInternal(server, patreonToken); + } } async function clearOldIgnores() { - const start = Date.now(); - await updateAccounts({ - ignores: { $exists: true, $not: { $size: 0 } }, - lastVisit: { $lt: fromNow(-YEAR) }, - }, { ignores: [] }); - logPerformance(`[async] clearOldIgnores (${Date.now() - start}ms)`); + const start = Date.now(); + await updateAccounts({ + ignores: { $exists: true, $not: { $size: 0 } }, + lastVisit: { $lt: fromNow(-YEAR) }, + }, { ignores: [] }); + logPerformance(`[async] clearOldIgnores (${Date.now() - start}ms)`); } async function cleanupBanField(field: keyof BannedMuted) { - const start = Date.now(); - await updateAccounts({ [field]: { $exists: true, $gt: 0, $lt: Date.now() } }, { $unset: { [field]: 1 } }); - logPerformance(`[async] cleanupBanField (${field}) (${Date.now() - start}ms)`); + const start = Date.now(); + await updateAccounts({ [field]: { $exists: true, $gt: 0, $lt: Date.now() } }, { $unset: { [field]: 1 } }); + logPerformance(`[async] cleanupBanField (${field}) (${Date.now() - start}ms)`); } async function cleanupBans() { - const start = Date.now(); - await cleanupBanField('ban'); - await cleanupBanField('shadow'); - await cleanupBanField('mute'); - logPerformance(`[async] cleanupBans (${Date.now() - start}ms)`); + const start = Date.now(); + await cleanupBanField('ban'); + await cleanupBanField('shadow'); + await cleanupBanField('mute'); + logPerformance(`[async] cleanupBans (${Date.now() - start}ms)`); } async function cleanupMerges() { - const start = Date.now(); - const date = fromNow(-30 * DAY); - await updateAccounts({ merges: { $exists: true, $not: { $size: 0 } } }, { $pull: { merges: { date: { $lt: date } } } }); - await updateAccounts({ merges: { $exists: true, $size: 0 } }, { $unset: { merges: 1 } }); - logPerformance(`[async] cleanupMerges (${Date.now() - start}ms)`); + const start = Date.now(); + const date = fromNow(-30 * DAY); + await updateAccounts({ merges: { $exists: true, $not: { $size: 0 } } }, { $pull: { merges: { date: { $lt: date } } } }); + await updateAccounts({ merges: { $exists: true, $size: 0 } }, { $unset: { merges: 1 } }); + logPerformance(`[async] cleanupMerges (${Date.now() - start}ms)`); } async function cleanupAccountAlerts() { - const start = Date.now(); - await updateAccounts( - { alert: { $exists: true }, 'alert.expires': { $lt: new Date() } } as any, - { $unset: { alert: 1 } }); - logPerformance(`[async] cleanupAccountAlerts (${Date.now() - start}ms)`); + const start = Date.now(); + await updateAccounts( + { alert: { $exists: true }, 'alert.expires': { $lt: new Date() } } as any, + { $unset: { alert: 1 } }); + logPerformance(`[async] cleanupAccountAlerts (${Date.now() - start}ms)`); } export async function updatePastSupporters() { - const start = Date.now(); - const auths = await Auth.find({ - pledged: { $exists: true, $gt: 0 }, - disabled: { $ne: true }, - banned: { $ne: true } - }, 'account').exec(); + const start = Date.now(); + const auths = await Auth.find({ + pledged: { $exists: true, $gt: 0 }, + disabled: { $ne: true }, + banned: { $ne: true } + }, 'account').exec(); - const accounts = await Account.find({ - supporter: { $exists: true, $bitsAllSet: SupporterFlags.PastSupporter } - }, '_id').exec(); + const accounts = await Account.find({ + supporter: { $exists: true, $bitsAllSet: SupporterFlags.PastSupporter } + }, '_id').exec(); - const shouldBeFlagged = new Set(); - const areFlagged = new Set(); + const shouldBeFlagged = new Set(); + const areFlagged = new Set(); - for (const auth of auths) { - if (auth.account) { - shouldBeFlagged.add(auth.account.toString()); - } - } + for (const auth of auths) { + if (auth.account) { + shouldBeFlagged.add(auth.account.toString()); + } + } - for (const account of accounts) { - areFlagged.add(account._id.toString()); - } + for (const account of accounts) { + areFlagged.add(account._id.toString()); + } - for (const auth of auths) { - if (auth.account) { - if (!areFlagged.has(auth.account.toString())) { - await Account.updateOne({ _id: auth.account }, { $bit: { supporter: { or: SupporterFlags.PastSupporter } } }).exec(); - } - } - } + for (const auth of auths) { + if (auth.account) { + if (!areFlagged.has(auth.account.toString())) { + await Account.updateOne({ _id: auth.account }, { $bit: { supporter: { or: SupporterFlags.PastSupporter } } }).exec(); + } + } + } - for (const account of accounts) { - if (!shouldBeFlagged.has(account._id.toString())) { - await Account.updateOne({ _id: account._id }, { $bit: { supporter: { and: ~SupporterFlags.PastSupporter } } }).exec(); - } - } + for (const account of accounts) { + if (!shouldBeFlagged.has(account._id.toString())) { + await Account.updateOne({ _id: account._id }, { $bit: { supporter: { and: ~SupporterFlags.PastSupporter } } }).exec(); + } + } - logPerformance(`[async] cleanupAccountAlerts (${Date.now() - start}ms)`); + logPerformance(`[async] cleanupAccountAlerts (${Date.now() - start}ms)`); } const cleanupStrayAuths = (removedDocument: RemovedDocument) => - async () => { - const start = Date.now(); - const date = fromNow(-1 * DAY); - const query = { account: { $exists: false }, updatedAt: { $lt: date }, createdAt: { $lt: date } }; - const items = await queryAuths(query, '_id'); - await Auth.deleteMany(query).exec(); - await Bluebird.map(items, item => removedDocument('auths', item._id.toString()), { concurrency: 4 }); - logPerformance(`[async] cleanupAccountAlerts (${Date.now() - start}ms)`); - }; + async () => { + const start = Date.now(); + const date = fromNow(-1 * DAY); + const query = { account: { $exists: false }, updatedAt: { $lt: date }, createdAt: { $lt: date } }; + const items = await queryAuths(query, '_id'); + await Auth.deleteMany(query).exec(); + await Bluebird.map(items, item => removedDocument('auths', item._id.toString()), { concurrency: 4 }); + logPerformance(`[async] cleanupAccountAlerts (${Date.now() - start}ms)`); + }; async function updateServerState(server: InternalGameServerState | InternalLoginServerState) { - try { - const state = await server.api.state(); - Object.assign(server.state, state); - } catch { - server.state.dead = true; - } + try { + const state = await server.api.state(); + Object.assign(server.state, state); + } catch { + server.state.dead = true; + } } let lastVisitedTodayCheck = (new Date()).getDate(); async function countUsersVisitedToday() { - const start = Date.now(); - const day = (new Date()).getDate(); + const start = Date.now(); + const day = (new Date()).getDate(); - if (lastVisitedTodayCheck !== day) { - lastVisitedTodayCheck = day; - const statsFile = paths.pathTo('settings', `user-counts.log`); - const count = await Account.countDocuments({ lastVisit: { $gt: fromNow(-1 * DAY) } }).exec(); - const json = JSON.stringify({ count, date: (new Date()).toISOString() }); - await fs.appendFileAsync(statsFile, `${json}\n`, 'utf8'); - logPerformance(`[async] countUsersVisitedToday (${Date.now() - start}ms)`); - } + if (lastVisitedTodayCheck !== day) { + lastVisitedTodayCheck = day; + const statsFile = paths.pathTo('settings', `user-counts.log`); + const count = await Account.countDocuments({ lastVisit: { $gt: fromNow(-1 * DAY) } }).exec(); + const json = JSON.stringify({ count, date: (new Date()).toISOString() }); + await fs.appendFileAsync(statsFile, `${json}\n`, 'utf8'); + logPerformance(`[async] countUsersVisitedToday (${Date.now() - start}ms)`); + } } async function mergePotentialDuplicates(service: AdminService) { - if (loginServers[0].state.autoMergeDuplicates) { - await service.mergePotentialDuplicates(); - } + if (loginServers[0].state.autoMergeDuplicates) { + await service.mergePotentialDuplicates(); + } } export async function poll(action: () => any, delayTime: number) { - try { - await delay(delayTime); - await action(); - } catch (e) { - console.error(e); - } finally { - poll(action, delayTime); - } + try { + await delay(delayTime); + await action(); + } catch (e) { + console.error(e); + } finally { + poll(action, delayTime); + } } export async function pollImmediate(action: () => any, delayTime: number) { - try { - await action(); - } catch (e) { - console.error(e); - } finally { - await delay(delayTime); - poll(action, delayTime); - } + try { + await action(); + } catch (e) { + console.error(e); + } finally { + await delay(delayTime); + poll(action, delayTime); + } } export function pollServers() { - return poll(() => Promise.all([...loginServers, ...servers].map(updateServerState)), 1 * SECOND); + return poll(() => Promise.all([...loginServers, ...servers].map(updateServerState)), 1 * SECOND); } export function pollPatreon(server: ServerConfig, settings: Settings) { - return poll(() => updatePatreonData(server, settings), 10 * MINUTE); + return poll(() => updatePatreonData(server, settings), 10 * MINUTE); } export const pollDiskSpace = () => pollImmediate(() => - getDiskSpace().then(value => serverStatus.diskSpace = value), HOUR); + getDiskSpace().then(value => serverStatus.diskSpace = value), HOUR); export const pollMemoryUsage = () => pollImmediate(() => - getMemoryUsage().then(value => serverStatus.memoryUsage = value), 10 * MINUTE); + getMemoryUsage().then(value => serverStatus.memoryUsage = value), 10 * MINUTE); export const pollCertificateExpirationDate = () => pollImmediate(() => - getCertificateExpirationDate().then(value => serverStatus.certificateExpiration = value), HOUR); + getCertificateExpirationDate().then(value => serverStatus.certificateExpiration = value), HOUR); export const startBansCleanup = () => poll(cleanupBans, DAY + 10 * MINUTE); export const startMergesCleanup = () => poll(cleanupMerges, DAY + 15 * MINUTE); export const startStrayAuthsCleanup = (removedDocument: RemovedDocument) => - poll(cleanupStrayAuths(removedDocument), DAY + 35 * MINUTE); + poll(cleanupStrayAuths(removedDocument), DAY + 35 * MINUTE); export const startClearOldIgnores = () => poll(clearOldIgnores, DAY + 20 * MINUTE); export const startCollectingUsersVisitedCount = () => poll(countUsersVisitedToday, 10 * MINUTE); export const startSupporterInvitesCleanup = () => poll(() => updateSupporterInvites(SupporterInvite), HOUR); export const startPotentialDuplicatesCleanup = (service: AdminService) => - poll(() => mergePotentialDuplicates(service), 10 * MINUTE); + poll(() => mergePotentialDuplicates(service), 10 * MINUTE); export const startAccountAlertsCleanup = () => poll(cleanupAccountAlerts, DAY + 25 * MINUTE); export const startUpdatePastSupporters = () => poll(updatePastSupporters, DAY + 30 * MINUTE); export function startClearTo10Origns(adminService: AdminService) { - return poll(async () => { - if (adminService.loaded) { - const start = Date.now(); - await clearOrigins(adminService, 10, true, { old: false, singles: true, trim: true }); - logPerformance(`[async] startClearTo10Origns (${Date.now() - start}ms)`); - } - }, DAY + 35 * MINUTE); + return poll(async () => { + if (adminService.loaded) { + const start = Date.now(); + await clearOrigins(adminService, 10, true, { old: false, singles: true, trim: true }); + logPerformance(`[async] startClearTo10Origns (${Date.now() - start}ms)`); + } + }, DAY + 35 * MINUTE); } export function startClearVeryOldOrigns(adminService: AdminService) { - return poll(async () => { - if (adminService.loaded) { - const start = Date.now(); - await clearOrigins(adminService, 1, true, { old: true, singles: false, trim: false }); - logPerformance(`[async] startClearVeryOldOrigns (${Date.now() - start}ms)`); - } - }, DAY + 50 * MINUTE); + return poll(async () => { + if (adminService.loaded) { + const start = Date.now(); + await clearOrigins(adminService, 1, true, { old: true, singles: false, trim: false }); + logPerformance(`[async] startClearVeryOldOrigns (${Date.now() - start}ms)`); + } + }, DAY + 50 * MINUTE); } diff --git a/src/ts/server/pool.ts b/src/ts/server/pool.ts index 225bc14..06a80c5 100644 --- a/src/ts/server/pool.ts +++ b/src/ts/server/pool.ts @@ -1,30 +1,30 @@ export interface Pool { - create(): T; - dispose(value: T): boolean; + create(): T; + dispose(value: T): boolean; } export function createPool(count: number, createNew: () => T, reset: (value: T) => void): Pool { - const pool: T[] = []; + const pool: T[] = []; - const create = () => { - const existing = pool.pop(); + const create = () => { + const existing = pool.pop(); - if (existing) { - reset(existing); - return existing; - } else { - return createNew(); - } - }; + if (existing) { + reset(existing); + return existing; + } else { + return createNew(); + } + }; - const dispose = (value: T) => { - if (pool.length < count) { - pool.push(value); - return true; - } else { - return false; - } - }; + const dispose = (value: T) => { + if (pool.length < count) { + pool.push(value); + return true; + } else { + return false; + } + }; - return { create, dispose }; + return { create, dispose }; } diff --git a/src/ts/server/regionUtils.ts b/src/ts/server/regionUtils.ts index 70c5bc6..d42560d 100644 --- a/src/ts/server/regionUtils.ts +++ b/src/ts/server/regionUtils.ts @@ -2,7 +2,7 @@ import { createBinaryWriter, getWriterBuffer, BinaryWriter } from 'ag-sockets'; import { removeItem, pointInRect, clamp, includes } from '../common/utils'; import { ServerEntity, IClient, ServerRegion, ServerMap } from './serverInterfaces'; import { - tickTilesRestoration, resetRegionUpdates, pushRemoveEntityToRegion, removeEntityFromRegion, addEntityToRegion + tickTilesRestoration, resetRegionUpdates, pushRemoveEntityToRegion, removeEntityFromRegion, addEntityToRegion } from './serverRegion'; import { updateEntity, isEntityShadowed, isOverflowError, pushAddEntityToClient } from './entityUtils'; import { writeRegion, writeUpdate } from '../common/encoders/updateEncoder'; @@ -18,284 +18,284 @@ let updatesBuffer = new ArrayBuffer(4096); let updatesBufferOffset = 0; export function resetEncodeUpdate() { - updatesBufferOffset = 0; + updatesBufferOffset = 0; } function resizeUpdatesBuffer(e: Error) { - if (isOverflowError(e)) { - updatesBuffer = new ArrayBuffer(updatesBuffer.byteLength * 2); - updatesBufferOffset = 0; - DEVELOPMENT && logger.debug(`resize buffer to ${updatesBuffer.byteLength} (${e.message})`); - } else { - throw e; - } + if (isOverflowError(e)) { + updatesBuffer = new ArrayBuffer(updatesBuffer.byteLength * 2); + updatesBufferOffset = 0; + DEVELOPMENT && logger.debug(`resize buffer to ${updatesBuffer.byteLength} (${e.message})`); + } else { + throw e; + } } function createUpdatesWriter() { - const buffer = new Uint8Array(updatesBuffer, updatesBufferOffset, updatesBuffer.byteLength - updatesBufferOffset); - return createBinaryWriter(buffer); + const buffer = new Uint8Array(updatesBuffer, updatesBufferOffset, updatesBuffer.byteLength - updatesBufferOffset); + return createBinaryWriter(buffer); } function commitUpdatesWriter(writer: BinaryWriter) { - const result = getWriterBuffer(writer); - updatesBufferOffset += result.byteLength; - return result; + const result = getWriterBuffer(writer); + updatesBufferOffset += result.byteLength; + return result; } function encodeUpdate(region: ServerRegion): Uint8Array { - timingStart('encodeUpdate()'); + timingStart('encodeUpdate()'); - let result: Uint8Array; + let result: Uint8Array; - while (true) { - try { - const writer = createUpdatesWriter(); - writeUpdate(writer, region); - result = commitUpdatesWriter(writer); - break; - } catch (e) { - resizeUpdatesBuffer(e); - } - } + while (true) { + try { + const writer = createUpdatesWriter(); + writeUpdate(writer, region); + result = commitUpdatesWriter(writer); + break; + } catch (e) { + resizeUpdatesBuffer(e); + } + } - timingEnd(); + timingEnd(); - return result; + return result; } function encodeRegion(region: ServerRegion, client: IClient): Uint8Array { - timingStart('encodeRegion()'); + timingStart('encodeRegion()'); - let result: Uint8Array; + let result: Uint8Array; - while (true) { - try { - const writer = createUpdatesWriter(); - writeRegion(writer, region, client); - result = commitUpdatesWriter(writer); - break; - } catch (e) { - resizeUpdatesBuffer(e); - } - } + while (true) { + try { + const writer = createUpdatesWriter(); + writeRegion(writer, region, client); + result = commitUpdatesWriter(writer); + break; + } catch (e) { + resizeUpdatesBuffer(e); + } + } - timingEnd(); + timingEnd(); - return result; + return result; } export function subscribeToRegionsInRange(client: IClient) { - timingStart('subscribeToRegionsInRange()'); + timingStart('subscribeToRegionsInRange()'); - const { map, camera } = client; - const maxX = clamp(Math.floor(toWorldX(camera.x + camera.w) / REGION_SIZE) + 1, 0, map.regionsX - 1); - const maxY = clamp(Math.floor(toWorldY(camera.y + camera.h) / REGION_SIZE) + 1, 0, map.regionsY - 1); - const minX = clamp(Math.floor(toWorldX(camera.x) / REGION_SIZE) - 1, 0, maxX); - const minY = clamp(Math.floor(toWorldY(camera.y) / REGION_SIZE) - 1, 0, maxY); + const { map, camera } = client; + const maxX = clamp(Math.floor(toWorldX(camera.x + camera.w) / REGION_SIZE) + 1, 0, map.regionsX - 1); + const maxY = clamp(Math.floor(toWorldY(camera.y + camera.h) / REGION_SIZE) + 1, 0, map.regionsY - 1); + const minX = clamp(Math.floor(toWorldX(camera.x) / REGION_SIZE) - 1, 0, maxX); + const minY = clamp(Math.floor(toWorldY(camera.y) / REGION_SIZE) - 1, 0, maxY); - for (let y = minY; y <= maxY; y++) { - for (let x = minX; x <= maxX; x++) { - const region = getRegion(map, x, y); + for (let y = minY; y <= maxY; y++) { + for (let x = minX; x <= maxX; x++) { + const region = getRegion(map, x, y); - if (isRectVisible(camera, region.subscribeBounds)) { - if (!isSubscribedToRegion(client, region)) { - timingStart('subscribeToRegion()'); - region.clients.push(client); - client.regions.push(region); - client.subscribes.push(encodeRegion(region, client)); - timingEnd(); - } - } - } - } + if (isRectVisible(camera, region.subscribeBounds)) { + if (!isSubscribedToRegion(client, region)) { + timingStart('subscribeToRegion()'); + region.clients.push(client); + client.regions.push(region); + client.subscribes.push(encodeRegion(region, client)); + timingEnd(); + } + } + } + } - timingEnd(); + timingEnd(); } export function unsubscribeFromOutOfRangeRegions(client: IClient) { - timingStart('unsubscribeFromOutOfRangeRegions()'); + timingStart('unsubscribeFromOutOfRangeRegions()'); - const regions = client.regions; + const regions = client.regions; - for (let i = regions.length - 1; i >= 0; i--) { - const region = regions[i]; + for (let i = regions.length - 1; i >= 0; i--) { + const region = regions[i]; - if (!isRectVisible(client.camera, region.unsubscribeBounds)) { - if (includes(region.entities, client.pony)) { - DEVELOPMENT && logger.warn(`Trying to unsubscribe client from region they are in`); - } else { - removeItem(region.clients, client); - regions.splice(i, 1); - client.unsubscribes.push(region.x, region.y); - } - } - } + if (!isRectVisible(client.camera, region.unsubscribeBounds)) { + if (includes(region.entities, client.pony)) { + DEVELOPMENT && logger.warn(`Trying to unsubscribe client from region they are in`); + } else { + removeItem(region.clients, client); + regions.splice(i, 1); + client.unsubscribes.push(region.x, region.y); + } + } + } - timingEnd(); + timingEnd(); } export function unsubscribeFromAllRegions(client: IClient, silent: boolean) { - for (const region of client.regions) { - removeItem(region.clients, client); + for (const region of client.regions) { + removeItem(region.clients, client); - if (!silent) { - client.unsubscribes.push(region.x, region.y); - } - } + if (!silent) { + client.unsubscribes.push(region.x, region.y); + } + } - client.regions = []; + client.regions = []; } export function getExpectedRegion({ x, y, flags, region }: ServerEntity, map: ServerMap) { - if (region !== undefined && (flags & EntityFlags.Movable) !== 0 && pointInRect(x, y, region.boundsWithBorder)) { - return region; - } else { - const rx = clamp(Math.floor(x / REGION_SIZE), 0, map.regionsX - 1) | 0; - const ry = clamp(Math.floor(y / REGION_SIZE), 0, map.regionsY - 1) | 0; - return map.regions[(rx + ((ry * map.regionsX) | 0)) | 0]; - } + if (region !== undefined && (flags & EntityFlags.Movable) !== 0 && pointInRect(x, y, region.boundsWithBorder)) { + return region; + } else { + const rx = clamp(Math.floor(x / REGION_SIZE), 0, map.regionsX - 1) | 0; + const ry = clamp(Math.floor(y / REGION_SIZE), 0, map.regionsY - 1) | 0; + return map.regions[(rx + ((ry * map.regionsX) | 0)) | 0]; + } } export function updateRegion(entity: ServerEntity, map: ServerMap) { - const expectedRegion = getExpectedRegion(entity, map); + const expectedRegion = getExpectedRegion(entity, map); - if (expectedRegion !== entity.region) { - transferToRegion(entity, expectedRegion, map); - } + if (expectedRegion !== entity.region) { + transferToRegion(entity, expectedRegion, map); + } } const moves: { entity: ServerEntity, region: ServerRegion; map: ServerMap; }[] = []; export function updateRegions(maps: ServerMap[]) { - timingStart('updateRegions()'); + timingStart('updateRegions()'); - moves.length = 0; + moves.length = 0; - // TODO: only update changed entities - timingStart('getExpectedRegion'); - for (const map of maps) { - for (const region of map.regions) { - for (const entity of region.movables) { - const expectedRegion = getExpectedRegion(entity, map); + // TODO: only update changed entities + timingStart('getExpectedRegion'); + for (const map of maps) { + for (const region of map.regions) { + for (const entity of region.movables) { + const expectedRegion = getExpectedRegion(entity, map); - if (expectedRegion !== entity.region) { - moves.push({ entity, region: expectedRegion, map }); - } - } - } - } - timingEnd(); + if (expectedRegion !== entity.region) { + moves.push({ entity, region: expectedRegion, map }); + } + } + } + } + timingEnd(); - timingStart('transferToRegion'); - for (const { entity, region, map } of moves) { - transferToRegion(entity, region, map); - } - timingEnd(); + timingStart('transferToRegion'); + for (const { entity, region, map } of moves) { + transferToRegion(entity, region, map); + } + timingEnd(); - moves.length = 0; + moves.length = 0; - timingEnd(); + timingEnd(); } export function commitRegionUpdates(regions: ServerRegion[]) { - timingStart('commitRegionUpdates()'); + timingStart('commitRegionUpdates()'); - for (const region of regions) { - if (region.entityUpdates.length || region.entityRemoves.length || region.tileUpdates.length) { - if (region.clients.length) { - const data = encodeUpdate(region); + for (const region of regions) { + if (region.entityUpdates.length || region.entityRemoves.length || region.tileUpdates.length) { + if (region.clients.length) { + const data = encodeUpdate(region); - for (const client of region.clients) { - client.regionUpdates.push(data); - } - } + for (const client of region.clients) { + client.regionUpdates.push(data); + } + } - resetRegionUpdates(region); - } - } + resetRegionUpdates(region); + } + } - timingEnd(); + timingEnd(); } export function transferToRegion(entity: ServerEntity, region: ServerRegion, map: ServerMap) { - const oldRegion = entity.region; + const oldRegion = entity.region; - if (oldRegion) { - removeEntityFromRegion(oldRegion, entity, map); - updateEntity(entity, true); - } + if (oldRegion) { + removeEntityFromRegion(oldRegion, entity, map); + updateEntity(entity, true); + } - entity.region = region; - addEntityToRegion(region, entity, map); + entity.region = region; + addEntityToRegion(region, entity, map); - if (!isEntityShadowed(entity)) { - for (const client of region.clients) { - if (!oldRegion || !isSubscribedToRegion(client, oldRegion)) { - pushAddEntityToClient(client, entity); - } - } - } + if (!isEntityShadowed(entity)) { + for (const client of region.clients) { + if (!oldRegion || !isSubscribedToRegion(client, oldRegion)) { + pushAddEntityToClient(client, entity); + } + } + } } export function addToRegion(entity: ServerEntity, region: ServerRegion, map: ServerMap) { - entity.region = region; - addEntityToRegion(region, entity, map); + entity.region = region; + addEntityToRegion(region, entity, map); - if (isEntityShadowed(entity)) { - pushAddEntityToClient(entity.client, entity); - } else { - for (const client of region.clients) { - pushAddEntityToClient(client, entity); - } - } + if (isEntityShadowed(entity)) { + pushAddEntityToClient(entity.client, entity); + } else { + for (const client of region.clients) { + pushAddEntityToClient(client, entity); + } + } } export function removeFromRegion(entity: ServerEntity, region: ServerRegion, map: ServerMap) { - const removed = removeEntityFromRegion(region, entity, map); - pushRemoveEntityToRegion(region, entity); - return removed; + const removed = removeEntityFromRegion(region, entity, map); + pushRemoveEntityToRegion(region, entity); + return removed; } export function isSubscribedToRegion(client: IClient, region: ServerRegion) { - return includes(client.regions, region); + return includes(client.regions, region); } export function sparseRegionUpdate(map: ServerMap, region: ServerRegion, options: { restoreTerrain: boolean; }) { - if (options.restoreTerrain) { - tickTilesRestoration(map, region); - } + if (options.restoreTerrain) { + tickTilesRestoration(map, region); + } } // timing helpers function writingTiming() { - timingStart('write'); + timingStart('write'); } function sendingTiming() { - timingEnd(); - timingStart('send'); + timingEnd(); + timingStart('send'); } function doneTiming() { - timingEnd(); + timingEnd(); } function noop() { } export function setupTiming(client: any) { - if (client.__internalHooks) { - client.__internalHooks.writing = writingTiming; - client.__internalHooks.sending = sendingTiming; - client.__internalHooks.done = doneTiming; - } + if (client.__internalHooks) { + client.__internalHooks.writing = writingTiming; + client.__internalHooks.sending = sendingTiming; + client.__internalHooks.done = doneTiming; + } } export function clearTiming(client: any) { - if (client.__internalHooks) { - client.__internalHooks.writing = noop; - client.__internalHooks.sending = noop; - client.__internalHooks.done = noop; - } + if (client.__internalHooks) { + client.__internalHooks.writing = noop; + client.__internalHooks.sending = noop; + client.__internalHooks.done = noop; + } } diff --git a/src/ts/server/reporter.ts b/src/ts/server/reporter.ts index 84f555e..f3efbc0 100644 --- a/src/ts/server/reporter.ts +++ b/src/ts/server/reporter.ts @@ -10,94 +10,94 @@ const maxDescLength = 300; /* istanbul ignore next */ const createLogEvent = - (config: ServerConfig) => - ( - account: ID | undefined, pony: ID | undefined, originInfo: IOriginInfo | undefined, type: string, - message: string, desc?: string - ) => { - const server = config.id; + (config: ServerConfig) => + ( + account: ID | undefined, pony: ID | undefined, originInfo: IOriginInfo | undefined, type: string, + message: string, desc?: string + ) => { + const server = config.id; - if (desc) { - desc = truncate(desc, { length: maxDescLength }); - } + if (desc) { + desc = truncate(desc, { length: maxDescLength }); + } - const origin = originInfo && { ip: originInfo.ip, country: originInfo.country }; + const origin = originInfo && { ip: originInfo.ip, country: originInfo.country }; - Event.findOne({ server, account, pony, type, message, origin }).exec() - .then(event => { - if (event) { - if (!event.desc || (event.desc.length < maxDescLength && desc && event.desc.indexOf(desc) === -1)) { - event.desc = `${event.desc || ''}\n${desc || ''}`.trim(); - } + Event.findOne({ server, account, pony, type, message, origin }).exec() + .then(event => { + if (event) { + if (!event.desc || (event.desc.length < maxDescLength && desc && event.desc.indexOf(desc) === -1)) { + event.desc = `${event.desc || ''}\n${desc || ''}`.trim(); + } - return Event.updateOne({ _id: event._id }, { desc: event.desc, count: event.count + 1 }).exec(); - } else { - return Event.create({ server, account, pony, type, message, origin, desc }); - } - }) - .catch(logger.error); + return Event.updateOne({ _id: event._id }, { desc: event.desc, count: event.count + 1 }).exec(); + } else { + return Event.create({ server, account, pony, type, message, origin, desc }); + } + }) + .catch(logger.error); - return null; - }; + return null; + }; const ignoreWarnings = ['Suspicious message', 'Spam']; /* istanbul ignore next */ export function create(server: ServerConfig, account?: ID, pony?: ID, originInfo?: IOriginInfo): Reporter { - const logEvent = createLogEvent(server); - const accountId = `${account}`; + const logEvent = createLogEvent(server); + const accountId = `${account}`; - function log(type: string, message: string, desc?: string) { - logEvent(account, pony, originInfo, type, message, desc); + function log(type: string, message: string, desc?: string) { + logEvent(account, pony, originInfo, type, message, desc); - if (DEVELOPMENT) { - logger.debug('[event]', `[${type}]`, message); - } - } + if (DEVELOPMENT) { + logger.debug('[event]', `[${type}]`, message); + } + } - return { - info(message: string, desc?: string) { - log('info', message, desc); - }, - warn(message: string, desc?: string) { - log('warning', message, desc); + return { + info(message: string, desc?: string) { + log('info', message, desc); + }, + warn(message: string, desc?: string) { + log('warning', message, desc); - if (ignoreWarnings.indexOf(message) === -1) { - system(accountId, message); - } - }, - warnLog(message: string) { - logger.warn(message); - }, - danger(message: string, desc?: string) { - log('danger', message, desc); - logger.error(message, desc || ''); - }, - error(error: Error, desc?: string) { - log('danger', error.message, desc); - logger.error(error, desc || ''); - }, - system(message: string, desc?: string, logEvent = true) { - if (logEvent) { - log('info', message, desc); - } + if (ignoreWarnings.indexOf(message) === -1) { + system(accountId, message); + } + }, + warnLog(message: string) { + logger.warn(message); + }, + danger(message: string, desc?: string) { + log('danger', message, desc); + logger.error(message, desc || ''); + }, + error(error: Error, desc?: string) { + log('danger', error.message, desc); + logger.error(error, desc || ''); + }, + system(message: string, desc?: string, logEvent = true) { + if (logEvent) { + log('info', message, desc); + } - system(accountId, message); - }, - systemLog(message: string) { - system(accountId, message); - DEVELOPMENT && logger.log(message); - }, - setPony(newPony: any) { - pony = newPony; - }, - }; + system(accountId, message); + }, + systemLog(message: string) { + system(accountId, message); + DEVELOPMENT && logger.log(message); + }, + setPony(newPony: any) { + pony = newPony; + }, + }; } /* istanbul ignore next */ export function createFromRequest(server: ServerConfig, req: Request, pony?: any) { - const user = req && req.user as IAccount | undefined; - const account = user ? user.id : undefined; - const origin = req ? getOrigin(req) : undefined; - return create(server, account, pony, origin); + const user = req && req.user as IAccount | undefined; + const account = user ? user.id : undefined; + const origin = req ? getOrigin(req) : undefined; + return create(server, account, pony, origin); } diff --git a/src/ts/server/reporting.ts b/src/ts/server/reporting.ts index 24256bf..6aa194c 100644 --- a/src/ts/server/reporting.ts +++ b/src/ts/server/reporting.ts @@ -14,87 +14,87 @@ export const SWEAR_TIMEOUT = 10 * HOUR; export const FORBIDDEN_TIMEOUT = 1 * HOUR; export const createReportSuspicious = - (counter: Counter): OnSuspiciousMessage => - (client, message, suspicious) => { - const { accountId, account, reporter, shadowed } = client; - const limit = 5; - const { count, items } = counter.add(accountId, message); + (counter: Counter): OnSuspiciousMessage => + (client, message, suspicious) => { + const { accountId, account, reporter, shadowed } = client; + const limit = 5; + const { count, items } = counter.add(accountId, message); - if (count > limit || suspicious === Suspicious.Very) { - const msg = items.join('\n'); - counter.remove(accountId); + if (count > limit || suspicious === Suspicious.Very) { + const msg = items.join('\n'); + counter.remove(accountId); - if (!(isMuted(account) || shadowed)) { - reporter.warn('Suspicious messages', msg); - } - } - }; + if (!(isMuted(account) || shadowed)) { + reporter.warn('Suspicious messages', msg); + } + } + }; export const createReportSwears = - ( - counter: Counter, reportSwearing: ReportAccount, timeoutAccount: TimeoutAccount, handlePromise = handlePromiseDefault, - ): OnMessageSettings => - (client, message, settings) => { - const { accountId, account, reporter, shadowed } = client; - const limit = 5; // isNew ? 3 : 6; - const { count, items } = counter.add(accountId, message); + ( + counter: Counter, reportSwearing: ReportAccount, timeoutAccount: TimeoutAccount, handlePromise = handlePromiseDefault, + ): OnMessageSettings => + (client, message, settings) => { + const { accountId, account, reporter, shadowed } = client; + const limit = 5; // isNew ? 3 : 6; + const { count, items } = counter.add(accountId, message); - if (count > limit) { - const msg = items.join('\n'); - const timeout = settings.autoBanSwearing && !(isMuted(account) || shadowed); - const duration = SWEAR_TIMEOUT * (settings.doubleTimeouts ? 2 : 1); - counter.remove(accountId); + if (count > limit) { + const msg = items.join('\n'); + const timeout = settings.autoBanSwearing && !(isMuted(account) || shadowed); + const duration = SWEAR_TIMEOUT * (settings.doubleTimeouts ? 2 : 1); + counter.remove(accountId); - handlePromise(Promise.resolve() - .then(() => reportSwearing(accountId)) - .then(() => timeout ? timeoutAccount(accountId, fromNow(duration), 'Timed out for swearing') : undefined) - .then(() => { - if (timeout) { - reporter.system('Timed out for swearing', msg, !!settings.reportSwears); - } else if (!(isMuted(account) || shadowed)) { - reporter.warn('Swearing', msg); - } - }), reporter.error); - } - }; + handlePromise(Promise.resolve() + .then(() => reportSwearing(accountId)) + .then(() => timeout ? timeoutAccount(accountId, fromNow(duration), 'Timed out for swearing') : undefined) + .then(() => { + if (timeout) { + reporter.system('Timed out for swearing', msg, !!settings.reportSwears); + } else if (!(isMuted(account) || shadowed)) { + reporter.warn('Swearing', msg); + } + }), reporter.error); + } + }; export const createReportForbidden = - ( - counter: Counter, timeoutAccount: TimeoutAccount, handlePromise = handlePromiseDefault - ): OnMessageSettings => - (client, message, settings) => { - const { accountId, account, reporter, shadowed } = client; - const newAccount = isNew(account); - const limit = newAccount ? 5 : 10; - const { count, items } = counter.add(accountId, message); - const mutedOrShadowed = isMuted(account) || shadowed; + ( + counter: Counter, timeoutAccount: TimeoutAccount, handlePromise = handlePromiseDefault + ): OnMessageSettings => + (client, message, settings) => { + const { accountId, account, reporter, shadowed } = client; + const newAccount = isNew(account); + const limit = newAccount ? 5 : 10; + const { count, items } = counter.add(accountId, message); + const mutedOrShadowed = isMuted(account) || shadowed; - if (!mutedOrShadowed) { - if (count >= limit) { - const msg = items.join('\n'); - const duration = FORBIDDEN_TIMEOUT * (settings.doubleTimeouts ? 2 : 1); - counter.remove(accountId); + if (!mutedOrShadowed) { + if (count >= limit) { + const msg = items.join('\n'); + const duration = FORBIDDEN_TIMEOUT * (settings.doubleTimeouts ? 2 : 1); + counter.remove(accountId); - if (newAccount || settings.autoBanSwearing) { - handlePromise(timeoutAccount(accountId, fromNow(duration)) - .then(() => reporter.system('Timed out for forbidden messages', msg)), reporter.error); - } else { - reporter.warn('Forbidden messages', msg); - } - } - } - }; + if (newAccount || settings.autoBanSwearing) { + handlePromise(timeoutAccount(accountId, fromNow(duration)) + .then(() => reporter.system('Timed out for forbidden messages', msg)), reporter.error); + } else { + reporter.warn('Forbidden messages', msg); + } + } + } + }; export const reportInviteLimit = - ( - reportInviteLimitAccount: (account: string) => Promise, message: string, handlePromise = handlePromiseDefault - ): ReportInviteLimit => - ({ accountId, reporter }) => - handlePromise(reportInviteLimitAccount(accountId) - .then(count => { - reporter.systemLog(message); + ( + reportInviteLimitAccount: (account: string) => Promise, message: string, handlePromise = handlePromiseDefault + ): ReportInviteLimit => + ({ accountId, reporter }) => + handlePromise(reportInviteLimitAccount(accountId) + .then(count => { + reporter.systemLog(message); - if (count % 10 === 0) { - reporter.warn(`${message} (${count})`); - } - }), reporter.error); + if (count % 10 === 0) { + reporter.warn(`${message} (${count})`); + } + }), reporter.error); diff --git a/src/ts/server/requestUtils.ts b/src/ts/server/requestUtils.ts index 84600de..874cb55 100644 --- a/src/ts/server/requestUtils.ts +++ b/src/ts/server/requestUtils.ts @@ -18,200 +18,200 @@ import { IAccount } from './db'; const ROLLBAR_IP = '35.184.69.251'; export const notFound: RequestHandler = (_, res) => { - res.setHeader('Cache-Control', 'public, max-age=0'); - res.sendStatus(404); + res.setHeader('Cache-Control', 'public, max-age=0'); + res.sendStatus(404); }; export const validAccount = (server: ServerConfig): RequestHandler => (req, res, next) => { - const account = req.user as IAccount | undefined; - const accountId = req.body.accountId as string; - const accountName = req.body.accountName as string; + const account = req.user as IAccount | undefined; + const accountId = req.body.accountId as string; + const accountName = req.body.accountName as string; - if (!account || account.id !== accountId) { - if (!/#$/.test(accountId)) { - createFromRequest(server, req).warn(ACCOUNT_ERROR, `${accountName} [${accountId}] (${req.path})`); - } - //logger.warn(ACCOUNT_ERROR, `${accountName} [${accountId}] (${req.path})`); - res.status(403).json({ error: ACCOUNT_ERROR }); - } else { - next(null); - } + if (!account || account.id !== accountId) { + if (!/#$/.test(accountId)) { + createFromRequest(server, req).warn(ACCOUNT_ERROR, `${accountName} [${accountId}] (${req.path})`); + } + //logger.warn(ACCOUNT_ERROR, `${accountName} [${accountId}] (${req.path})`); + res.status(403).json({ error: ACCOUNT_ERROR }); + } else { + next(null); + } }; export const blockMaps = (debug: boolean, local: boolean): RequestHandler => (req, res, next) => { - if (!debug && !local && /\.map$/.test(req.path) && getIP(req) !== ROLLBAR_IP) { - res.sendStatus(404); - } else { - next(null); - } + if (!debug && !local && /\.map$/.test(req.path) && getIP(req) !== ROLLBAR_IP) { + res.sendStatus(404); + } else { + next(null); + } }; export const hash: RequestHandler = (req, res, next) => { - const apiVersion = req.get('api-version'); + const apiVersion = req.get('api-version'); - if (apiVersion !== HASH) { - res.status(400).json({ error: VERSION_ERROR }); - } else { - next(null); - } + if (apiVersion !== HASH) { + res.status(400).json({ error: VERSION_ERROR }); + } else { + next(null); + } }; export const offline = (settings: Settings): RequestHandler => (_req, res, next) => { - if (settings.isPageOffline) { - res.status(503).send(OFFLINE_ERROR); - } else { - next(null); - } + if (settings.isPageOffline) { + res.status(503).send(OFFLINE_ERROR); + } else { + next(null); + } }; export const internal = (config: AppConfig, server: ServerConfig): RequestHandler => (req, res, next) => { - if (req.get('api-token') === config.token) { - next(null); - } else { - createFromRequest(server, req).warn('Unauthorized internal api call', req.originalUrl); - res.sendStatus(403); - } + if (req.get('api-token') === config.token) { + next(null); + } else { + createFromRequest(server, req).warn('Unauthorized internal api call', req.originalUrl); + res.sendStatus(403); + } }; export const auth: RequestHandler = (req, res, next) => { - if (req.isAuthenticated()) { - next(null); - } else { - //createFromRequest(req).warn('Unauthorized access', req.originalUrl); - res.setHeader('X-Robots-Tag', 'noindex'); - res.sendStatus(403); - } + if (req.isAuthenticated()) { + next(null); + } else { + //createFromRequest(req).warn('Unauthorized access', req.originalUrl); + res.setHeader('X-Robots-Tag', 'noindex'); + res.sendStatus(403); + } }; export const admin = (server: ServerConfig): RequestHandler => (req, res, next) => { - if (req.isAuthenticated() && req.user && isAdmin(req.user)) { - next(null); - } else { - if (!/Googlebot/.test(req.get('User-Agent')!)) { - createFromRequest(server, req).warn(`Unauthorized access (admin)`, req.originalUrl); - } + if (req.isAuthenticated() && req.user && isAdmin(req.user)) { + next(null); + } else { + if (!/Googlebot/.test(req.get('User-Agent')!)) { + createFromRequest(server, req).warn(`Unauthorized access (admin)`, req.originalUrl); + } - res.setHeader('X-Robots-Tag', 'noindex, nofollow'); - res.sendStatus(403); - } + res.setHeader('X-Robots-Tag', 'noindex, nofollow'); + res.sendStatus(403); + } }; const store = new ExpressBrute.MemoryStore(); export function limit(freeRetries: number, lifetime: number) { - const options: any = { - freeRetries, - lifetime, - failCallback(req: Request, res: Response, _next: NextFunction, nextValidRequestDate: any) { - logger.warn(`rate limit ${req.url} ${req.ip}`); - res.status(429).send(`Too many requests, please try again ${moment(nextValidRequestDate).fromNow()}`); - } - }; + const options: any = { + freeRetries, + lifetime, + failCallback(req: Request, res: Response, _next: NextFunction, nextValidRequestDate: any) { + logger.warn(`rate limit ${req.url} ${req.ip}`); + res.status(429).send(`Too many requests, please try again ${moment(nextValidRequestDate).fromNow()}`); + } + }; - return (new (ExpressBrute as any)(store, options)).prevent; + return (new (ExpressBrute as any)(store, options)).prevent; } function reportError(e: Error, server: ServerConfig, req: Request) { - createFromRequest(server, req).danger(`Req error: ${e.message}`, `${req.method.toUpperCase()} ${req.originalUrl}`); - logger.error(e); + createFromRequest(server, req).danger(`Req error: ${e.message}`, `${req.method.toUpperCase()} ${req.originalUrl}`); + logger.error(e); } export function handleError(server: ServerConfig, req: Request, res: Response) { - return (e: Error) => { - if (isUserError(e)) { - reportUserError(e, server, req); - res.status(422).json({ error: e.message, userError: true }); - } else { - reportError(e, server, req); - res.status(500).json({ error: 'Error occurred' }); - } - }; + return (e: Error) => { + if (isUserError(e)) { + reportUserError(e, server, req); + res.status(422).json({ error: e.message, userError: true }); + } else { + reportError(e, server, req); + res.status(500).json({ error: 'Error occurred' }); + } + }; } let logRequest: (req: Request, result: any, url?: string) => void = noop; export function initLogRequest(func: typeof logRequest) { - logRequest = func; + logRequest = func; } export function handleJSON(server: ServerConfig, req: Request, res: Response, result: any): any { - Promise.resolve(result) - .then(result => { - res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0'); - res.json(result); - return result; - }) - .then(result => logRequest(req, result)) - .catch(handleError(server, req, res)); + Promise.resolve(result) + .then(result => { + res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0'); + res.json(result); + return result; + }) + .then(result => logRequest(req, result)) + .catch(handleError(server, req, res)); } export function wrap(server: ServerConfig, handle: (req: Request) => any): RequestHandler { - return (req, res) => handleJSON(server, req, res, handle(req)); + return (req, res) => handleJSON(server, req, res, handle(req)); } export function wrapApi(server: ServerConfig, api: any) { - return wrap(server, ({ body: { method, args = [] } }) => { - if (api[method]) { - return api[method](...args); - } else { - return Promise.reject(new Error(`Invalid method (${method})`)); - } - }); + return wrap(server, ({ body: { method, args = [] } }) => { + if (api[method]) { + return api[method](...args); + } else { + return Promise.reject(new Error(`Invalid method (${method})`)); + } + }); } interface StaticFile { - buffer: Buffer; - mimeType: string; + buffer: Buffer; + mimeType: string; } function readFiles(files: Map, dir: string, url: string) { - const mimeTypes: any = { - '.js': 'application/javascript; charset=utf-8', - '.css': 'text/css; charset=utf-8', - '.png': 'image/png', - '.jpg': 'image/jpeg', - }; + const mimeTypes: any = { + '.js': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.png': 'image/png', + '.jpg': 'image/jpeg', + }; - for (const file of fs.readdirSync(dir)) { - const filePath = path.join(dir, file); - const stat = fs.statSync(filePath); + for (const file of fs.readdirSync(dir)) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); - if (stat.isDirectory()) { - readFiles(files, filePath, `${url}/${file}`); - } else { - const ext = path.extname(file); - const mimeType = mimeTypes[ext]; + if (stat.isDirectory()) { + readFiles(files, filePath, `${url}/${file}`); + } else { + const ext = path.extname(file); + const mimeType = mimeTypes[ext]; - if (mimeType) { - const buffer = fs.readFileSync(filePath); - files.set(`${url}/${file}`, { mimeType, buffer }); - } - } - } + if (mimeType) { + const buffer = fs.readFileSync(filePath); + files.set(`${url}/${file}`, { mimeType, buffer }); + } + } + } } export function inMemoryStaticFiles(assetsPath: string, assetsUrl: string, maxAge: number): RequestHandler { - const staticFiles = new Map(); - const cacheControl = `public, max-age=${Math.floor(maxAge / 1000)}`; - readFiles(staticFiles, assetsPath, assetsUrl); + const staticFiles = new Map(); + const cacheControl = `public, max-age=${Math.floor(maxAge / 1000)}`; + readFiles(staticFiles, assetsPath, assetsUrl); - return (req, res, next) => { - if (req.method !== 'GET' && req.method !== 'HEAD') { - return next(); - } + return (req, res, next) => { + if (req.method !== 'GET' && req.method !== 'HEAD') { + return next(); + } - const staticFile = staticFiles.get(req.path); + const staticFile = staticFiles.get(req.path); - if (!staticFile) { - return next(); - } + if (!staticFile) { + return next(); + } - try { - res.setHeader('Content-Type', staticFile.mimeType); - res.setHeader('Cache-Control', cacheControl); - res.status(200).end(staticFile.buffer); - } catch (e) { - next(e); - } - }; + try { + res.setHeader('Content-Type', staticFile.mimeType); + res.setHeader('Cache-Control', cacheControl); + res.status(200).end(staticFile.buffer); + } catch (e) { + next(e); + } + }; } diff --git a/src/ts/server/routes/api-account.ts b/src/ts/server/routes/api-account.ts index a3bfb3a..8459316 100644 --- a/src/ts/server/routes/api-account.ts +++ b/src/ts/server/routes/api-account.ts @@ -1,36 +1,36 @@ import { Router } from 'express'; import { offline as createOffline, validAccount as createValidAccount, hash, wrap, limit } from '../requestUtils'; import { - createUpdateAccount, createRemoveSite, createUpdateSettings, createGetAccountCharacters, removeHide, getHides, getFriends + createUpdateAccount, createRemoveSite, createUpdateSettings, createGetAccountCharacters, removeHide, getHides, getFriends } from '../api/account'; import { findAccountSafe, findAuth, findAllCharacters, countAllVisibleAuths, IAccount } from '../db'; import { system } from '../logger'; import { Settings, ServerConfig } from '../../common/adminInterfaces'; export default function (server: ServerConfig, settings: Settings) { - const validAccount = createValidAccount(server); - const offline = createOffline(settings); - const app = Router(); + const validAccount = createValidAccount(server); + const offline = createOffline(settings); + const app = Router(); - const getAccountCharacters = createGetAccountCharacters(findAllCharacters); - const updateAccount = createUpdateAccount(findAccountSafe, system); - const updateSettings = createUpdateSettings(findAccountSafe); - const removeSite = createRemoveSite(findAuth, countAllVisibleAuths, system); + const getAccountCharacters = createGetAccountCharacters(findAllCharacters); + const updateAccount = createUpdateAccount(findAccountSafe, system); + const updateSettings = createUpdateSettings(findAccountSafe); + const removeSite = createRemoveSite(findAuth, countAllVisibleAuths, system); - app.post('/account-characters', offline, hash, validAccount, limit(60, 60), wrap(server, req => - getAccountCharacters(req.user as IAccount))); - app.post('/account-update', offline, hash, validAccount, limit(60, 60), wrap(server, req => - updateAccount(req.user as IAccount, req.body.account))); - app.post('/account-settings', offline, hash, validAccount, limit(60, 60), wrap(server, req => - updateSettings(req.user as IAccount, req.body.settings))); - app.post('/remove-site', offline, hash, validAccount, limit(60, 60), wrap(server, req => - removeSite(req.user as IAccount, req.body.siteId))); - app.post('/remove-hide', offline, hash, validAccount, limit(60, 60), wrap(server, req => - removeHide(req.user as IAccount, req.body.hideId))); - app.post('/get-hides', offline, hash, validAccount, limit(60, 60), wrap(server, req => - getHides(req.user as IAccount, req.body.page || 0))); - app.post('/get-friends', offline, hash, validAccount, limit(120, 60), wrap(server, req => - getFriends(req.user as IAccount))); + app.post('/account-characters', offline, hash, validAccount, limit(60, 60), wrap(server, req => + getAccountCharacters(req.user as IAccount))); + app.post('/account-update', offline, hash, validAccount, limit(60, 60), wrap(server, req => + updateAccount(req.user as IAccount, req.body.account))); + app.post('/account-settings', offline, hash, validAccount, limit(60, 60), wrap(server, req => + updateSettings(req.user as IAccount, req.body.settings))); + app.post('/remove-site', offline, hash, validAccount, limit(60, 60), wrap(server, req => + removeSite(req.user as IAccount, req.body.siteId))); + app.post('/remove-hide', offline, hash, validAccount, limit(60, 60), wrap(server, req => + removeHide(req.user as IAccount, req.body.hideId))); + app.post('/get-hides', offline, hash, validAccount, limit(60, 60), wrap(server, req => + getHides(req.user as IAccount, req.body.page || 0))); + app.post('/get-friends', offline, hash, validAccount, limit(120, 60), wrap(server, req => + getFriends(req.user as IAccount))); - return app; + return app; } diff --git a/src/ts/server/routes/api-game.ts b/src/ts/server/routes/api-game.ts index 009b6d0..201bb2d 100644 --- a/src/ts/server/routes/api-game.ts +++ b/src/ts/server/routes/api-game.ts @@ -8,28 +8,28 @@ import { Settings, ServerConfig } from '../../common/adminInterfaces'; import { getOrigin, addOrigin } from '../originUtils'; export default function (server: ServerConfig, settings: Settings, config: Config) { - const offline = createOffline(settings); - const validAccount = createValidAccount(server); - const join = createJoin(); - const app = Router(); + const offline = createOffline(settings); + const validAccount = createValidAccount(server); + const join = createJoin(); + const app = Router(); - let inQueue = 0; + let inQueue = 0; - const joinGame = createJoinGame(findServer, config, findCharacter, join, addOrigin, hasActiveSupporterInvites); + const joinGame = createJoinGame(findServer, config, findCharacter, join, addOrigin, hasActiveSupporterInvites); - app.post('/game/join', offline, limit(60, 5 * 60), hash, validAccount, wrap(server, async req => { - if (inQueue > 100) { - return {}; - } else { - try { - inQueue++; - const { ponyId, serverId, version, url, alert } = req.body; - return await joinGame(req.user as IAccount, ponyId, serverId, version, url, alert, getOrigin(req)); - } finally { - inQueue--; - } - } - })); + app.post('/game/join', offline, limit(60, 5 * 60), hash, validAccount, wrap(server, async req => { + if (inQueue > 100) { + return {}; + } else { + try { + inQueue++; + const { ponyId, serverId, version, url, alert } = req.body; + return await joinGame(req.user as IAccount, ponyId, serverId, version, url, alert, getOrigin(req)); + } finally { + inQueue--; + } + } + })); - return app; + return app; } diff --git a/src/ts/server/routes/api-pony.ts b/src/ts/server/routes/api-pony.ts index 6bed886..6b5446f 100644 --- a/src/ts/server/routes/api-pony.ts +++ b/src/ts/server/routes/api-pony.ts @@ -12,26 +12,26 @@ import { RemovedDocument } from '../internal'; import { logRemovedCharacter } from '../characterUtils'; export default function (server: ServerConfig, settings: Settings, removedDocument: RemovedDocument) { - const offline = createOffline(settings); - const validAccount = createValidAccount(server); - const app = Router(); + const offline = createOffline(settings); + const validAccount = createValidAccount(server); + const app = Router(); - const isSuspiciousName = createIsSuspiciousName(settings); - const isSuspiciousPony = createIsSuspiciousPony(settings); + const isSuspiciousName = createIsSuspiciousName(settings); + const isSuspiciousPony = createIsSuspiciousPony(settings); - const savePonyHandler = createSavePony( - findCharacter, findAuth, characterCount, updateCharacterCount, createCharacter, system, - isSuspiciousName, isSuspiciousPony); + const savePonyHandler = createSavePony( + findCharacter, findAuth, characterCount, updateCharacterCount, createCharacter, system, + isSuspiciousName, isSuspiciousPony); - const removePonyHandler = createRemovePony( - kickFromAllServersByCharacter, removeCharacter, updateCharacterCount, - id => removedDocument('ponies', id), logRemovedCharacter); + const removePonyHandler = createRemovePony( + kickFromAllServersByCharacter, removeCharacter, updateCharacterCount, + id => removedDocument('ponies', id), logRemovedCharacter); - app.post('/pony/save', offline, hash, validAccount, wrap(server, req => - savePonyHandler(req.user as IAccount, req.body.pony, createFromRequest(server, req)))); + app.post('/pony/save', offline, hash, validAccount, wrap(server, req => + savePonyHandler(req.user as IAccount, req.body.pony, createFromRequest(server, req)))); - app.post('/pony/remove', offline, hash, validAccount, wrap(server, req => - removePonyHandler(req.body.id, (req.user as IAccount).id))); + app.post('/pony/remove', offline, hash, validAccount, wrap(server, req => + removePonyHandler(req.body.id, (req.user as IAccount).id))); - return app; + return app; } diff --git a/src/ts/server/routes/api-tools.ts b/src/ts/server/routes/api-tools.ts index 74d7423..04707f8 100644 --- a/src/ts/server/routes/api-tools.ts +++ b/src/ts/server/routes/api-tools.ts @@ -15,81 +15,81 @@ import { flatten } from '../../common/utils'; import { serializeMap } from '../serverMap'; export default function (server: ServerConfig, settings: Settings, world: World | undefined) { - const offline = createOffline(settings); - const app = Router(); + const offline = createOffline(settings); + const app = Router(); - app.use(auth); + app.use(auth); - app.get('/ponies', offline, (req, res) => { - handleJSON(server, req, res, createGetAccountCharacters(findAllCharacters)(req.user as IAccount)); - }); + app.get('/ponies', offline, (req, res) => { + handleJSON(server, req, res, createGetAccountCharacters(findAllCharacters)(req.user as IAccount)); + }); - app.get('/animation/:id', offline, (req, res) => { - const filePath = path.join(paths.store, req.params.id); + app.get('/animation/:id', offline, (req, res) => { + const filePath = path.join(paths.store, req.params.id); - res.sendFile(filePath); - }); + res.sendFile(filePath); + }); - app.post('/animation', offline, (req, res) => { - const name = randomString(10); - const filePath = path.join(paths.store, name); + app.post('/animation', offline, (req, res) => { + const name = randomString(10); + const filePath = path.join(paths.store, name); - fs.writeFileAsync(filePath, req.body.animation, 'utf8') - .then(() => res.send({ name })); - }); + fs.writeFileAsync(filePath, req.body.animation, 'utf8') + .then(() => res.send({ name })); + }); - app.post('/animation-gif', offline, (req, res) => { - const image: string = req.body.image; - const width: number = req.body.width || 80; - const height: number = req.body.height || 80; - const fps: number = req.body.fps || 24; - const remove: number = req.body.remove || 0; + app.post('/animation-gif', offline, (req, res) => { + const image: string = req.body.image; + const width: number = req.body.width || 80; + const height: number = req.body.height || 80; + const fps: number = req.body.fps || 24; + const remove: number = req.body.remove || 0; - const name = randomString(10); - const filePath = path.join(paths.store, name + '.png'); - const header = 'data:image/gif;base64,'; - const buffer = Buffer.from(image.substr(header.length), 'base64'); - const magick = /^win/.test(process.platform) ? 'magick' : 'convert'; - const command = `${magick} -dispose 3 -delay ${100 / fps} -loop 0 "${filePath}" -crop ${width}x${height} ` - + `+repage${repeat(' +delete', remove)} "${filePath.replace(/png$/, 'gif')}"`; + const name = randomString(10); + const filePath = path.join(paths.store, name + '.png'); + const header = 'data:image/gif;base64,'; + const buffer = Buffer.from(image.substr(header.length), 'base64'); + const magick = /^win/.test(process.platform) ? 'magick' : 'convert'; + const command = `${magick} -dispose 3 -delay ${100 / fps} -loop 0 "${filePath}" -crop ${width}x${height} ` + + `+repage${repeat(' +delete', remove)} "${filePath.replace(/png$/, 'gif')}"`; - fs.writeFileAsync(filePath, buffer) - .then(() => execAsync(command)) - .then(() => res.send({ name })); - }); + fs.writeFileAsync(filePath, buffer) + .then(() => execAsync(command)) + .then(() => res.send({ name })); + }); - app.get('/maps', offline, (_, res) => { - if (world) { - res.json(world.maps.map(m => m.id)); - } else { - res.sendStatus(400); - } - }); + app.get('/maps', offline, (_, res) => { + if (world) { + res.json(world.maps.map(m => m.id)); + } else { + res.sendStatus(400); + } + }); - app.get('/map', offline, (req, res) => { - if (world) { - const id = req.query.map || ''; - const map = world.maps.find(m => m.id === id); + app.get('/map', offline, (req, res) => { + if (world) { + const id = req.query.map || ''; + const map = world.maps.find(m => m.id === id); - if (map) { - const mapInfo: ToolsMapInfo = { - ...serializeMap(map), - defaultTile: map.defaultTile, - type: map.type, - info: { - season: world.season, - entities: flatten(map.regions.map(r => r.entities)) - .map(({ type, x, y, order, id }) => ({ type, x, y, order, id })), - }, - }; + if (map) { + const mapInfo: ToolsMapInfo = { + ...serializeMap(map), + defaultTile: map.defaultTile, + type: map.type, + info: { + season: world.season, + entities: flatten(map.regions.map(r => r.entities)) + .map(({ type, x, y, order, id }) => ({ type, x, y, order, id })), + }, + }; - res.json(mapInfo); - return; - } - } + res.json(mapInfo); + return; + } + } - res.sendStatus(400); - }); + res.sendStatus(400); + }); - return app; + return app; } diff --git a/src/ts/server/routes/api.ts b/src/ts/server/routes/api.ts index e6192ed..6e73d86 100644 --- a/src/ts/server/routes/api.ts +++ b/src/ts/server/routes/api.ts @@ -8,13 +8,13 @@ import apiPony from './api-pony'; import apiGame from './api-game'; export default function (server: ServerConfig, settings: Settings, config: Config, removedDocument: RemovedDocument) { - const app = Router(); + const app = Router(); - app.use(auth); + app.use(auth); - app.use(apiAccount(server, settings)); - app.use(apiPony(server, settings, removedDocument)); - app.use(apiGame(server, settings, config)); + app.use(apiAccount(server, settings)); + app.use(apiPony(server, settings, removedDocument)); + app.use(apiGame(server, settings, config)); - return app; + return app; } diff --git a/src/ts/server/routes/api1.ts b/src/ts/server/routes/api1.ts index 017b198..6a67448 100644 --- a/src/ts/server/routes/api1.ts +++ b/src/ts/server/routes/api1.ts @@ -11,50 +11,50 @@ const MAX_CONCURRENT_REQUESTS = 100; let requests = 0; export default function (server: ServerConfig, settings: Settings) { - const app = Router(); + const app = Router(); - const getAccountData = createGetAccountData(findAllCharacters, findAllVisibleAuths); + const getAccountData = createGetAccountData(findAllCharacters, findAllVisibleAuths); - async function handleAccountRequest(account: IAccount, userAgent?: string, browserId?: string) { - if (requests < MAX_CONCURRENT_REQUESTS) { - requests++; + async function handleAccountRequest(account: IAccount, userAgent?: string, browserId?: string) { + if (requests < MAX_CONCURRENT_REQUESTS) { + requests++; - try { - const lastUserAgent = userAgent || account.lastUserAgent; - const lastBrowserId = browserId || account.lastBrowserId; + try { + const lastUserAgent = userAgent || account.lastUserAgent; + const lastBrowserId = browserId || account.lastBrowserId; - if ((lastUserAgent && account.lastUserAgent !== lastUserAgent) || - (lastBrowserId && account.lastBrowserId !== lastBrowserId)) { - account.lastUserAgent = lastUserAgent; - account.lastBrowserId = lastBrowserId; - Account.updateOne({ _id: account._id }, { lastUserAgent, lastBrowserId }, noop); - } + if ((lastUserAgent && account.lastUserAgent !== lastUserAgent) || + (lastBrowserId && account.lastBrowserId !== lastBrowserId)) { + account.lastUserAgent = lastUserAgent; + account.lastBrowserId = lastBrowserId; + Account.updateOne({ _id: account._id }, { lastUserAgent, lastBrowserId }, noop); + } - return await getAccountData(account); - } finally { - requests--; - } - } else { - return { limit: true }; - } - } + return await getAccountData(account); + } finally { + requests--; + } + } else { + return { limit: true }; + } + } - app.post('/account', offline(settings), hash, (req, res) => { - req.session!.touch(); + app.post('/account', offline(settings), hash, (req, res) => { + req.session!.touch(); - let account = req.user as IAccount | undefined; - const browserId = req.get('Api-Bid'); - const userAgent = req.get('User-Agent') || ''; - const requestedWith = req.get('X-Requested-With'); - const isWebViewUserAgent = /Chrome\/\d+\.0\.0\.0 Mobile|; wv\)/.test(userAgent); - const isWebView = requestedWith || isWebViewUserAgent; + let account = req.user as IAccount | undefined; + const browserId = req.get('Api-Bid'); + const userAgent = req.get('User-Agent') || ''; + const requestedWith = req.get('X-Requested-With'); + const isWebViewUserAgent = /Chrome\/\d+\.0\.0\.0 Mobile|; wv\)/.test(userAgent); + const isWebView = requestedWith || isWebViewUserAgent; - if (!account || (settings.blockWebView && isWebView && includes(blockApps, requestedWith))) { - handleJSON(server, req, res, null); - } else { - handleJSON(server, req, res, handleAccountRequest(account, userAgent, browserId)); - } - }); + if (!account || (settings.blockWebView && isWebView && includes(blockApps, requestedWith))) { + handleJSON(server, req, res, null); + } else { + handleJSON(server, req, res, handleAccountRequest(account, userAgent, browserId)); + } + }); - return app; + return app; } diff --git a/src/ts/server/routes/api2.ts b/src/ts/server/routes/api2.ts index 9a22ca7..b1d0485 100644 --- a/src/ts/server/routes/api2.ts +++ b/src/ts/server/routes/api2.ts @@ -9,63 +9,63 @@ import { StatsTracker } from '../stats'; import { MIN_ADULT_AGE } from '../../common/constants'; function isServerSafe(server: InternalGameServerState) { - return server.state.alert !== '18+'; + return server.state.alert !== '18+'; } function toServerState(server: InternalGameServerState): ServerInfo { - const { name, path, desc, flag, alert, online, settings, require, host } = server.state; + const { name, path, desc, flag, alert, online, settings, require, host } = server.state; - return { - id: server.id, - name, - path, - desc, - host, - flag, - alert, - dead: false, - online, - offline: isServerOffline(server), - filter: !!settings.filterSwears, - require, - }; + return { + id: server.id, + name, + path, + desc, + host, + flag, + alert, + dead: false, + online, + offline: isServerOffline(server), + filter: !!settings.filterSwears, + require, + }; } function toServerStateShort(server: InternalGameServerState): ServerInfoShort { - return { - id: server.id, - online: server.state.online, - offline: isServerOffline(server), - }; + return { + id: server.id, + online: server.state.online, + offline: isServerOffline(server), + }; } function getGameStatus( - servers: InternalGameServerState[], live: ServerLiveSettings, short: boolean, age: number + servers: InternalGameServerState[], live: ServerLiveSettings, short: boolean, age: number ): GameStatus { - const adult = age >= MIN_ADULT_AGE; + const adult = age >= MIN_ADULT_AGE; - return { - version, - update: live.updating ? true : undefined, - servers: servers - .filter(s => isServerSafe(s) || adult) - .map(short ? toServerStateShort : toServerState), - }; + return { + version, + update: live.updating ? true : undefined, + servers: servers + .filter(s => isServerSafe(s) || adult) + .map(short ? toServerStateShort : toServerState), + }; } export default function (settings: Settings, live: ServerLiveSettings, statsTracker: StatsTracker) { - const app = Router(); + const app = Router(); - app.get('/game/status', offline(settings), (req, res) => { - const status = getGameStatus(servers, live, req.query.short === 'true', req.query.d | 0); - res.json(status); - statsTracker.logRequest(req, status); - }); + app.get('/game/status', offline(settings), (req, res) => { + const status = getGameStatus(servers, live, req.query.short === 'true', req.query.d | 0); + res.json(status); + statsTracker.logRequest(req, status); + }); - app.post('/csp', offline(settings), (_, res) => { - //logger.warn('CSP report', getIPFromRequest(req), req.body['csp-report']); - res.sendStatus(200); - }); + app.post('/csp', offline(settings), (_, res) => { + //logger.warn('CSP report', getIPFromRequest(req), req.body['csp-report']); + res.sendStatus(200); + }); - return app; + return app; } diff --git a/src/ts/server/routes/auth.ts b/src/ts/server/routes/auth.ts index bd18565..9e51a8b 100644 --- a/src/ts/server/routes/auth.ts +++ b/src/ts/server/routes/auth.ts @@ -22,8 +22,8 @@ import { getOriginFromHTTP, getOrigin, addOrigin } from '../originUtils'; import { Profile } from '../../common/interfaces'; interface MergeRequest { - accountId: string; - time: number; + accountId: string; + time: number; } const FRESH_ACCOUNT_TIME = 1 * MINUTE; @@ -31,287 +31,287 @@ const mergeRequests: MergeRequest[] = []; /* tslint:disable */ const ignoreErrors = [ - 'Service unavailable', // replacement for twitter HTTP error - 'Internal error', - 'User denied your request', - 'Code was already redeemed.', - 'Code is invalid or expired.', - 'This authorization code has expired.', - 'This authorization code has been used.', - 'Failed to fetch user profile', - 'Failed to obtain access token', - 'Failed to find request token in session', - 'User authorization failed: user is deactivated.', - 'User authorization failed: user revoke access for this token.', - 'Backend Error', - 'TokenError', - 'Bad Request', - 'Rate limit exceeded', - `Sorry, this feature isn't available right now: An error occurred while processing this request. Please try again later.`, - 'Przepraszamy, ta funkcja nie jest obecnie dostępna: Podczas przetwarzania żądania wystąpił błąd. Spróbuj ponownie później.', - 'К сожалению, эта функция сейчас не доступна: Во время обработки запроса возникла ошибка. Пожалуйста, попробуйте еще раз позже', - 'К сожалению, эта функция сейчас не доступна: Во время обработки запроса возникла ошибка. Пожалуйста, попробуйте еще раз позже.', - 'Desculpe, esse recurso não está disponível no momento: Ocorreu um erro ao processar essa solicitação. Tente novamente mais tarde.', - 'Esta aplicación no está disponible: La aplicación que intentas usar ya no está disponible o tiene el acceso restringido.', - 'Xin lỗi, tính năng này không khả dụng ngay bây giờ: Đã xảy ra lỗi khi xử lý yêu cầu này. Vui lòng thử lại sau.', - 'Lo sentimos, esta función no está disponible ahora: Ocurrió un error mientras se procesaba la solicitud. Vuelve a intentarlo más tarde.', - `The access token is invalid since the user hasn't engaged the app in longer than 90 days.`, - `Application Unavailable: The application you're trying to use is either no longer available or access is restricted.`, - 'An unexpected error has occurred. Please retry your request later.', - 'Code was invalid or expired. ', - 'Internal server error: could not check access_token now, check later.', - 'failed to fetch user profile', - 'Failed to obtain request token', - 'User canceled the Dialog flow', - 'Internal Error', - 'Bad Authentication data.', - 'Diese Function ist vorübergehend nicht verfügbar', - 'Diese Funktion ist vorübergehend nicht verfügbar', - 'User authorization failed: no access_token passed.', - 'Ungültiges Anfrage-Token.', - 'Invalid Credentials', - 'Invalid code.', - 'Internal server error: Database problems, try later', - 'An invalid Platform session was found.: An invalid Platform session was found.', - `Cannot read property 'id' of undefined`, // patreon error - 'User Rate Limit Exceeded. Rate of requests for user exceed configured project quota. You may consider re-evaluating expected per-user traffic to the API and adjust project quota limits accordingly. You may monitor aggregate quota usage and adjust limits in the API Console: https://console.developers.google.com/apis/api/plus.googleapis.com/quotas?project=200390553857', + 'Service unavailable', // replacement for twitter HTTP error + 'Internal error', + 'User denied your request', + 'Code was already redeemed.', + 'Code is invalid or expired.', + 'This authorization code has expired.', + 'This authorization code has been used.', + 'Failed to fetch user profile', + 'Failed to obtain access token', + 'Failed to find request token in session', + 'User authorization failed: user is deactivated.', + 'User authorization failed: user revoke access for this token.', + 'Backend Error', + 'TokenError', + 'Bad Request', + 'Rate limit exceeded', + `Sorry, this feature isn't available right now: An error occurred while processing this request. Please try again later.`, + 'Przepraszamy, ta funkcja nie jest obecnie dostępna: Podczas przetwarzania żądania wystąpił błąd. Spróbuj ponownie później.', + 'К сожалению, эта функция сейчас не доступна: Во время обработки запроса возникла ошибка. Пожалуйста, попробуйте еще раз позже', + 'К сожалению, эта функция сейчас не доступна: Во время обработки запроса возникла ошибка. Пожалуйста, попробуйте еще раз позже.', + 'Desculpe, esse recurso não está disponível no momento: Ocorreu um erro ao processar essa solicitação. Tente novamente mais tarde.', + 'Esta aplicación no está disponible: La aplicación que intentas usar ya no está disponible o tiene el acceso restringido.', + 'Xin lỗi, tính năng này không khả dụng ngay bây giờ: Đã xảy ra lỗi khi xử lý yêu cầu này. Vui lòng thử lại sau.', + 'Lo sentimos, esta función no está disponible ahora: Ocurrió un error mientras se procesaba la solicitud. Vuelve a intentarlo más tarde.', + `The access token is invalid since the user hasn't engaged the app in longer than 90 days.`, + `Application Unavailable: The application you're trying to use is either no longer available or access is restricted.`, + 'An unexpected error has occurred. Please retry your request later.', + 'Code was invalid or expired. ', + 'Internal server error: could not check access_token now, check later.', + 'failed to fetch user profile', + 'Failed to obtain request token', + 'User canceled the Dialog flow', + 'Internal Error', + 'Bad Authentication data.', + 'Diese Function ist vorübergehend nicht verfügbar', + 'Diese Funktion ist vorübergehend nicht verfügbar', + 'User authorization failed: no access_token passed.', + 'Ungültiges Anfrage-Token.', + 'Invalid Credentials', + 'Invalid code.', + 'Internal server error: Database problems, try later', + 'An invalid Platform session was found.: An invalid Platform session was found.', + `Cannot read property 'id' of undefined`, // patreon error + 'User Rate Limit Exceeded. Rate of requests for user exceed configured project quota. You may consider re-evaluating expected per-user traffic to the API and adjust project quota limits accordingly. You may monitor aggregate quota usage and adjust limits in the API Console: https://console.developers.google.com/apis/api/plus.googleapis.com/quotas?project=200390553857', ]; function kickCurrentUser(req: Request) { - const user = req.user as IAccount | undefined; + const user = req.user as IAccount | undefined; - if (user) { - kickFromAllServers(user.id) - .catch(e => logger.error(e)); - } + if (user) { + kickFromAllServers(user.id) + .catch(e => logger.error(e)); + } } function logIn(req: Request, account: IAccount) { - return new Promise((resolve, reject) => { - kickCurrentUser(req); - req.logIn(account, e => e ? reject(e) : resolve()); - }); + return new Promise((resolve, reject) => { + kickCurrentUser(req); + req.logIn(account, e => e ? reject(e) : resolve()); + }); } function isMerge(accountId: string) { - const minTime = fromNow(-10 * MINUTE).getTime(); - remove(mergeRequests, r => r.time < minTime); - return mergeRequests.some(r => r.accountId === accountId); + const minTime = fromNow(-10 * MINUTE).getTime(); + remove(mergeRequests, r => r.time < minTime); + return mergeRequests.some(r => r.accountId === accountId); } function getIP(req: Request) { - return req.ip || req.ips[0]; + return req.ip || req.ips[0]; } function reportError(server: ServerConfig, message: string, e: Error, req: Request) { - createFromRequest(server, req).danger(message, e.toString()); - logger.error(message, e); + createFromRequest(server, req).danger(message, e.toString()); + logger.error(message, e); } function fixTwitterErrorMessage(message: string) { - return /^/.test(message) ? 'Service unavailable' : message; + return /^/.test(message) ? 'Service unavailable' : message; } async function checkBanField( - server: ServerConfig, account: IAccount, field: keyof BannedMuted, message: string, origin: IOrigin + server: ServerConfig, account: IAccount, field: keyof BannedMuted, message: string, origin: IOrigin ) { - if (isActive(origin[field]) && !isActive(account[field])) { - create(server, account._id, undefined, origin).warn(message); - account[field] = origin[field]; - await account.save(); - } + if (isActive(origin[field]) && !isActive(account[field])) { + create(server, account._id, undefined, origin).warn(message); + account[field] = origin[field]; + await account.save(); + } } async function loginUser(server: ServerConfig, req: Request, res: Response, account: IAccount) { - const origin = await Origin.findOne({ ip: getIP(req) }).exec(); + const origin = await Origin.findOne({ ip: getIP(req) }).exec(); - await addOrigin(account, getOrigin(req)); + await addOrigin(account, getOrigin(req)); - if (origin) { - await checkBanField(server, account, 'mute', 'Muted account by origin', origin); - await checkBanField(server, account, 'shadow', 'Shadowed account by origin', origin); - await checkBanField(server, account, 'ban', 'Banned account by origin', origin); - } + if (origin) { + await checkBanField(server, account, 'mute', 'Muted account by origin', origin); + await checkBanField(server, account, 'shadow', 'Shadowed account by origin', origin); + await checkBanField(server, account, 'ban', 'Banned account by origin', origin); + } - if (isBanned(account)) { - // const message = isTemporarilyBanned(account) ? `Account locked()` : 'Account locked'; - throw new UserError('Account locked', undefined, getAccountAlertMessage(account)); - } + if (isBanned(account)) { + // const message = isTemporarilyBanned(account) ? `Account locked()` : 'Account locked'; + throw new UserError('Account locked', undefined, getAccountAlertMessage(account)); + } - await logIn(req, account); - await accountChanged(account._id.toString()); + await logIn(req, account); + await accountChanged(account._id.toString()); - const isFresh = account.createdAt && account.createdAt.getTime() > fromNow(-FRESH_ACCOUNT_TIME).getTime(); - res.redirect(isFresh ? '/account' : '/'); + const isFresh = account.createdAt && account.createdAt.getTime() > fromNow(-FRESH_ACCOUNT_TIME).getTime(); + res.redirect(isFresh ? '/account' : '/'); } async function mergeUser(req: Request, res: Response, account: IAccount, removedDocument: RemovedDocument) { - const user = req.user as IAccount; - const userId = user._id.toString(); - const accountId = account._id.toString(); + const user = req.user as IAccount; + const userId = user._id.toString(); + const accountId = account._id.toString(); - remove(mergeRequests, r => r.accountId === userId); + remove(mergeRequests, r => r.accountId === userId); - if (userId !== accountId) { - await mergeAccounts(userId, accountId, 'by user', removedDocument, false); - } + if (userId !== accountId) { + await mergeAccounts(userId, accountId, 'by user', removedDocument, false); + } - res.redirect('/account?merged=true'); + res.redirect('/account?merged=true'); } function handleErrorAndRedirect( - server: ServerConfig, url: string, message: string, e: Error, req: Request, res: Response + server: ServerConfig, url: string, message: string, e: Error, req: Request, res: Response ) { - if (isUserError(e)) { - reportUserError(e, server, req); - url += `?error=${encodeURIComponent(e.message)}`; + if (isUserError(e)) { + reportUserError(e, server, req); + url += `?error=${encodeURIComponent(e.message)}`; - if (e.userInfo) { - url += `&alert=${encodeURIComponent(e.userInfo)}`; - } - } else { - reportError(server, `Auth error: ${message}`, e, req); - url += `?error=${encodeURIComponent(message)}`; - } + if (e.userInfo) { + url += `&alert=${encodeURIComponent(e.userInfo)}`; + } + } else { + reportError(server, `Auth error: ${message}`, e, req); + url += `?error=${encodeURIComponent(message)}`; + } - res.redirect(url); + res.redirect(url); } async function handleAuth( - server: ServerConfig, live: ServerLiveSettings, removedDocument: RemovedDocument, - req: Request, res: Response, error: Error | null, account: IAccount | null, + server: ServerConfig, live: ServerLiveSettings, removedDocument: RemovedDocument, + req: Request, res: Response, error: Error | null, account: IAccount | null, ) { - const user = req.user as IAccount | undefined; - const merge = isMerge(user && user.id); + const user = req.user as IAccount | undefined; + const merge = isMerge(user && user.id); - try { - if (error) { - if (isUserError(error)) { - throw error; - } + try { + if (error) { + if (isUserError(error)) { + throw error; + } - const message = fixTwitterErrorMessage(error.message); - const ignore = includes(ignoreErrors, message); - throw new UserError(message, ignore ? undefined : { error, desc: `url: ${req.path}` }); - } + const message = fixTwitterErrorMessage(error.message); + const ignore = includes(ignoreErrors, message); + throw new UserError(message, ignore ? undefined : { error, desc: `url: ${req.path}` }); + } - if (!account) { - throw new UserError('No account'); - } + if (!account) { + throw new UserError('No account'); + } - if (merge && !hasFlag(account.flags, AccountFlags.BlockMerging)) { - if (live.shutdown) { - throw new Error(`Cannot merge while server is shutdown`); - } + if (merge && !hasFlag(account.flags, AccountFlags.BlockMerging)) { + if (live.shutdown) { + throw new Error(`Cannot merge while server is shutdown`); + } - await mergeUser(req, res, account, removedDocument); - } else { - await loginUser(server, req, res, account); - } - } catch (e) { - const message = merge ? 'Account merge error' : 'Authentication error'; - handleErrorAndRedirect(server, merge ? '/account' : '/', message, e, req, res); - } + await mergeUser(req, res, account, removedDocument); + } else { + await loginUser(server, req, res, account); + } + } catch (e) { + const message = merge ? 'Account merge error' : 'Authentication error'; + handleErrorAndRedirect(server, merge ? '/account' : '/', message, e, req, res); + } } function createHandler( - server: ServerConfig, live: ServerLiveSettings, id: string, options: AuthenticateOptions, - removedDocument: RemovedDocument + server: ServerConfig, live: ServerLiveSettings, id: string, options: AuthenticateOptions, + removedDocument: RemovedDocument ): RequestHandler { - return (req, res, next) => { - const handler = authenticate(id, options, (error: Error | null, account: IAccount | null) => - handleAuth(server, live, removedDocument, req, res, error, account)); + return (req, res, next) => { + const handler = authenticate(id, options, (error: Error | null, account: IAccount | null) => + handleAuth(server, live, removedDocument, req, res, error, account)); - return handler(req, res, next); - }; + return handler(req, res, next); + }; } export function authRoutes( - host: string, server: ServerConfig, settings: Settings, live: ServerLiveSettings, mockLogin: boolean, - removedDocument: RemovedDocument + host: string, server: ServerConfig, settings: Settings, live: ServerLiveSettings, mockLogin: boolean, + removedDocument: RemovedDocument ) { - const failureRedirect = `/?error=${encodeURIComponent('Authentication failed')}`; - const app = Router(); - const checkers: SuspiciousCheckers = { - isSuspiciousName: createIsSuspiciousName(settings), - isSuspiciousAuth: createIsSuspiciousAuth(settings), - }; + const failureRedirect = `/?error=${encodeURIComponent('Authentication failed')}`; + const app = Router(); + const checkers: SuspiciousCheckers = { + isSuspiciousName: createIsSuspiciousName(settings), + isSuspiciousAuth: createIsSuspiciousAuth(settings), + }; - providers.filter(p => !!p.auth).forEach(({ id, strategy, auth, connectOnly, additionalOptions = {} }) => { - const callbackURL = `${host}auth/${id}/callback`; - const scope = id === 'patreon' ? ['users'] : ['email']; - const options = { - ...additionalOptions, - ...auth, - callbackURL, - includeEmail: true, - profileFields: ['id', 'displayName', 'name', 'emails'], - passReqToCallback: true, - }; + providers.filter(p => !!p.auth).forEach(({ id, strategy, auth, connectOnly, additionalOptions = {} }) => { + const callbackURL = `${host}auth/${id}/callback`; + const scope = id === 'patreon' ? ['users'] : ['email']; + const options = { + ...additionalOptions, + ...auth, + callbackURL, + includeEmail: true, + profileFields: ['id', 'displayName', 'name', 'emails'], + passReqToCallback: true, + }; - async function signInOrSignUp(req: Request, profile: Profile) { - const user = req.user as IAccount | undefined; - const userId = user && user._id.toString(); - const mergeAccount = (userId && isMerge(userId)) ? userId : undefined; - const createAccountOptions = createOptions(req, !!connectOnly, server, settings, checkers); - const auth = await findOrCreateAuth(profile, mergeAccount, createAccountOptions); - const account = await findOrCreateAccount(auth, profile, createAccountOptions); - const { ip, userAgent } = createAccountOptions; - system(account._id, `signed-in with "${auth.name}" [${auth._id}] [${ip}] [${userAgent}]`); - return account; - } + async function signInOrSignUp(req: Request, profile: Profile) { + const user = req.user as IAccount | undefined; + const userId = user && user._id.toString(); + const mergeAccount = (userId && isMerge(userId)) ? userId : undefined; + const createAccountOptions = createOptions(req, !!connectOnly, server, settings, checkers); + const auth = await findOrCreateAuth(profile, mergeAccount, createAccountOptions); + const account = await findOrCreateAccount(auth, profile, createAccountOptions); + const { ip, userAgent } = createAccountOptions; + system(account._id, `signed-in with "${auth.name}" [${auth._id}] [${ip}] [${userAgent}]`); + return account; + } - use(id, new strategy(options, (req, _accessToken, _refreshToken, oauthProfile, callback) => { - const profile = getProfile(id, oauthProfile); + use(id, new strategy(options, (req, _accessToken, _refreshToken, oauthProfile, callback) => { + const profile = getProfile(id, oauthProfile); - signInOrSignUp(req, profile) - .then(account => { - callback(null, account); - }) - .catch((error: Error) => { - logServer(`failed to sign-in ${JSON.stringify(profile)}`); - callback(error, null); - }); - })); + signInOrSignUp(req, profile) + .then(account => { + callback(null, account); + }) + .catch((error: Error) => { + logServer(`failed to sign-in ${JSON.stringify(profile)}`); + callback(error, null); + }); + })); - app.get(`/${id}`, limit(120, 3600), createHandler(server, live, id, { scope, failureRedirect }, removedDocument)); - app.get(`/${id}/callback`, limit(120, 3600), createHandler(server, live, id, { failureRedirect }, removedDocument)); - app.get(`/${id}/merge`, limit(120, 3600), authRequest, (req, res) => { - const accountId = (req.user as IAccount)._id.toString(); - mergeRequests.push({ accountId, time: Date.now() }); - res.redirect(`/auth/${id}`); - }); - }); + app.get(`/${id}`, limit(120, 3600), createHandler(server, live, id, { scope, failureRedirect }, removedDocument)); + app.get(`/${id}/callback`, limit(120, 3600), createHandler(server, live, id, { failureRedirect }, removedDocument)); + app.get(`/${id}/merge`, limit(120, 3600), authRequest, (req, res) => { + const accountId = (req.user as IAccount)._id.toString(); + mergeRequests.push({ accountId, time: Date.now() }); + res.redirect(`/auth/${id}`); + }); + }); - app.post('/sign-out', wrap(server, req => { - kickCurrentUser(req); - req.logout(); - return { success: true }; - })); + app.post('/sign-out', wrap(server, req => { + kickCurrentUser(req); + req.logout(); + return { success: true }; + })); - if (mockLogin) { - use(new LocalStrategy((login, _pass, done) => Account.findById(login, done))); - app.get('/local', authenticate('local', { successRedirect: '/', failureRedirect: '/failed-login' })); - } + if (mockLogin) { + use(new LocalStrategy((login, _pass, done) => Account.findById(login, done))); + app.get('/local', authenticate('local', { successRedirect: '/', failureRedirect: '/failed-login' })); + } - return app; + return app; } function createOptions( - req: Request, connectOnly: boolean, server: ServerConfig, settings: Settings, checkers: SuspiciousCheckers + req: Request, connectOnly: boolean, server: ServerConfig, settings: Settings, checkers: SuspiciousCheckers ): CreateAccountOptions { - const acl = req.cookies && req.cookies.acl; - const origin = getOriginFromHTTP(req); + const acl = req.cookies && req.cookies.acl; + const origin = getOriginFromHTTP(req); - return { - ip: getIP(req), - userAgent: req.get('User-Agent'), - browserId: req.get('Api-Bid'), - connectOnly: !!connectOnly, - creationLocked: acl && acl > (new Date()).toISOString(), - canCreateAccounts: !!settings.canCreateAccounts, - reportPotentialDuplicates: !!settings.reportPotentialDuplicates, - warn: (accountId, message, desc) => create(server, accountId, undefined, origin).warn(message, desc), - ...checkers, - }; + return { + ip: getIP(req), + userAgent: req.get('User-Agent'), + browserId: req.get('Api-Bid'), + connectOnly: !!connectOnly, + creationLocked: acl && acl > (new Date()).toISOString(), + canCreateAccounts: !!settings.canCreateAccounts, + reportPotentialDuplicates: !!settings.reportPotentialDuplicates, + warn: (accountId, message, desc) => create(server, accountId, undefined, origin).warn(message, desc), + ...checkers, + }; } diff --git a/src/ts/server/routes/index.ts b/src/ts/server/routes/index.ts index 13c0dec..dd3719d 100644 --- a/src/ts/server/routes/index.ts +++ b/src/ts/server/routes/index.ts @@ -12,156 +12,156 @@ import { pathTo } from '../paths'; import { writeBinary } from '../../common/binaryUtils'; interface RevFile { - name: string; - path: string; - url: string; + name: string; + path: string; + url: string; } interface PageOptions { - isPublic?: boolean; - production: boolean; - base: string; - assets?: string; - style: string; - script: string; - scriptES: string; - token?: string; - noindex?: boolean; - socketOptions?: ClientOptions; - webpack?: boolean; - local?: boolean; + isPublic?: boolean; + production: boolean; + base: string; + assets?: string; + style: string; + script: string; + scriptES: string; + token?: string; + noindex?: boolean; + socketOptions?: ClientOptions; + webpack?: boolean; + local?: boolean; } function getFiles(urlBase: string, dir: string, sub: string): RevFile[] { - try { - return fs.readdirSync(path.join(dir, sub)) - .filter(file => /\.(js|css|png)$/.test(file)) - .map(file => ({ - name: file.replace(/-[a-f0-9]{10}\.(js|css|png)$/, '.$1'), - path: path.join(dir, sub, file), - url: `${urlBase}/${sub}/${file}`, - })); - } catch { - return []; - } + try { + return fs.readdirSync(path.join(dir, sub)) + .filter(file => /\.(js|css|png)$/.test(file)) + .map(file => ({ + name: file.replace(/-[a-f0-9]{10}\.(js|css|png)$/, '.$1'), + path: path.join(dir, sub, file), + url: `${urlBase}/${sub}/${file}`, + })); + } catch { + return []; + } } export function createIndex(assetsPath: string, adminAssetsPath: string) { - function toOAuthProvider({ id, name, color, auth, connectOnly }: OAuthProviderInfo): OAuthProvider { - return { id, name, color, disabled: auth ? undefined : true, connectOnly }; - } + function toOAuthProvider({ id, name, color, auth, connectOnly }: OAuthProviderInfo): OAuthProvider { + return { id, name, color, disabled: auth ? undefined : true, connectOnly }; + } - const revServer = new Map(); + const revServer = new Map(); - [ - ...getFiles('assets', assetsPath, 'styles'), - ...getFiles('assets', assetsPath, 'scripts'), - ...getFiles('assets', assetsPath, 'images'), - ...getFiles('assets-admin', adminAssetsPath, 'styles'), - ...getFiles('assets-admin', adminAssetsPath, 'scripts'), - ].forEach(file => revServer.set(file.name, file)); + [ + ...getFiles('assets', assetsPath, 'styles'), + ...getFiles('assets', assetsPath, 'scripts'), + ...getFiles('assets', assetsPath, 'images'), + ...getFiles('assets-admin', adminAssetsPath, 'styles'), + ...getFiles('assets-admin', adminAssetsPath, 'scripts'), + ].forEach(file => revServer.set(file.name, file)); - function revUrlGetter(dir: string) { - return (name: string) => { - const file = revServer.get(name); - return file && file.url || `assets/${dir}/${name}`; - }; - } + function revUrlGetter(dir: string) { + return (name: string) => { + const file = revServer.get(name); + return file && file.url || `assets/${dir}/${name}`; + }; + } - function getRevPath(name: string) { - return (revServer.get(name) && revServer.get(name)!.path) || path.join(assetsPath, name); - } + function getRevPath(name: string) { + return (revServer.get(name) && revServer.get(name)!.path) || path.join(assetsPath, name); + } - const getRevScriptURL = revUrlGetter('scripts'); - const getRevStyleURL = revUrlGetter('styles'); - const getRevImageURL = revUrlGetter('images'); + const getRevScriptURL = revUrlGetter('scripts'); + const getRevStyleURL = revUrlGetter('styles'); + const getRevImageURL = revUrlGetter('images'); - const template = compileFile(pathTo('views', 'index.pug')); - const inlineStyle = fs.readFileSync(getRevPath('style-inline.css'), 'utf8'); - const loadingImage = fs.readFileSync(getRevPath('logo-gray.png')); - const oauthProviders = providers.map(toOAuthProvider); + const template = compileFile(pathTo('views', 'index.pug')); + const inlineStyle = fs.readFileSync(getRevPath('style-inline.css'), 'utf8'); + const loadingImage = fs.readFileSync(getRevPath('logo-gray.png')); + const oauthProviders = providers.map(toOAuthProvider); - function encodeSocketOptions(options: ClientOptions | undefined): string { - if (options) { - const data = writeBinary(writer => writeObject(writer, options)); - const buffer = Buffer.from(data); - return buffer.toString('base64'); - } else { - return ''; - } - } + function encodeSocketOptions(options: ClientOptions | undefined): string { + if (options) { + const data = writeBinary(writer => writeObject(writer, options)); + const buffer = Buffer.from(data); + return buffer.toString('base64'); + } else { + return ''; + } + } - function renderPage( - { isPublic, style, script, scriptES, production, noindex, base, socketOptions, token, local }: PageOptions - ) { - return template({ - doctype: 'html', - host: config.host, - title: config.title, - twitterLink: config.twitterLink, - supporterLink: config.supporterLink, - email: config.contactEmail, - logo: `${config.host}${getRevImageURL('logo-120.png')}`, - loadingImage: `data:image/png;base64,${loadingImage.toString('base64')}`, - version, - description, - base, - token, - sw: config.sw ? 'true' : undefined, - noindex: noindex || config.noindex, - production, - local: local ? 'true' : undefined, - socketOptions: encodeSocketOptions(socketOptions), - inlineStyle, - style, - script, - scriptES, - oauthProviders, - facebookAppId: config.facebookAppId, - isPublic: isPublic ? 'true' : undefined, - }); - } + function renderPage( + { isPublic, style, script, scriptES, production, noindex, base, socketOptions, token, local }: PageOptions + ) { + return template({ + doctype: 'html', + host: config.host, + title: config.title, + twitterLink: config.twitterLink, + supporterLink: config.supporterLink, + email: config.contactEmail, + logo: `${config.host}${getRevImageURL('logo-120.png')}`, + loadingImage: `data:image/png;base64,${loadingImage.toString('base64')}`, + version, + description, + base, + token, + sw: config.sw ? 'true' : undefined, + noindex: noindex || config.noindex, + production, + local: local ? 'true' : undefined, + socketOptions: encodeSocketOptions(socketOptions), + inlineStyle, + style, + script, + scriptES, + oauthProviders, + facebookAppId: config.facebookAppId, + isPublic: isPublic ? 'true' : undefined, + }); + } - function admin( - production: boolean, base: string, assetsBase: string, scriptName: string, socket: Server - ): RequestHandler { - const socketOptions = socket.options(); - const style = `${assetsBase}/${getRevStyleURL('style-admin.css')}`; - const script = `${assetsBase}/${getRevScriptURL(scriptName)}`; - const scriptES = script; + function admin( + production: boolean, base: string, assetsBase: string, scriptName: string, socket: Server + ): RequestHandler { + const socketOptions = socket.options(); + const style = `${assetsBase}/${getRevStyleURL('style-admin.css')}`; + const script = `${assetsBase}/${getRevScriptURL(scriptName)}`; + const scriptES = script; - return (req, res) => { - try { - const token = socket.token({ account: req.user } as TokenData); - res.send(renderPage({ production, base, style, script, scriptES, noindex: true, socketOptions, token })); - } catch (e) { - logger.error(e); - res.sendStatus(500); - } - }; - } + return (req, res) => { + try { + const token = socket.token({ account: req.user } as TokenData); + res.send(renderPage({ production, base, style, script, scriptES, noindex: true, socketOptions, token })); + } catch (e) { + logger.error(e); + res.sendStatus(500); + } + }; + } - function user( - production: boolean, base: string, styleName: string, scriptName: string, scriptESName: string, - socketOptions: ClientOptions | undefined, noindex: boolean, local: boolean, isPublic: boolean, - ) { - const style = `/${getRevStyleURL(styleName)}`; - const script = `/${getRevScriptURL(scriptName)}`; - const scriptES = `/${getRevScriptURL(scriptESName)}`; - const sprites1 = DEVELOPMENT ? `/assets/images/pony.png` : `/${getRevImageURL('pony.png')}`; - const sprites2 = DEVELOPMENT ? `/assets/images/pony2.png` : `/${getRevImageURL('pony2.png')}`; + function user( + production: boolean, base: string, styleName: string, scriptName: string, scriptESName: string, + socketOptions: ClientOptions | undefined, noindex: boolean, local: boolean, isPublic: boolean, + ) { + const style = `/${getRevStyleURL(styleName)}`; + const script = `/${getRevScriptURL(scriptName)}`; + const scriptES = `/${getRevScriptURL(scriptESName)}`; + const sprites1 = DEVELOPMENT ? `/assets/images/pony.png` : `/${getRevImageURL('pony.png')}`; + const sprites2 = DEVELOPMENT ? `/assets/images/pony2.png` : `/${getRevImageURL('pony2.png')}`; - const page = renderPage({ isPublic, production, base, style, script, scriptES, socketOptions, noindex, local }); + const page = renderPage({ isPublic, production, base, style, script, scriptES, socketOptions, noindex, local }); - const preload = [ - `<${script}>; rel=preload; as=script`, - `<${style}>; rel=preload; as=style`, - `<${sprites1}>; rel=preload; as=fetch; crossorigin`, - `<${sprites2}>; rel=preload; as=fetch; crossorigin`, - ]; + const preload = [ + `<${script}>; rel=preload; as=script`, + `<${style}>; rel=preload; as=style`, + `<${sprites1}>; rel=preload; as=fetch; crossorigin`, + `<${sprites2}>; rel=preload; as=fetch; crossorigin`, + ]; - return { page, preload }; - } + return { page, preload }; + } - return { admin, user, getRevScript: getRevScriptURL, getRevStyle: getRevStyleURL }; + return { admin, user, getRevScript: getRevScriptURL, getRevStyle: getRevStyleURL }; } diff --git a/src/ts/server/server.ts b/src/ts/server/server.ts index a303a3b..3e851eb 100644 --- a/src/ts/server/server.ts +++ b/src/ts/server/server.ts @@ -32,17 +32,17 @@ import { settings, reloadSettings } from './settings'; import { SocketErrorHandler } from './utils/socketErrorHandler'; import { tokenService } from './serverUtils'; import { - admin as isAdmin, auth, blockMaps, wrapApi, internal, initLogRequest, notFound, inMemoryStaticFiles + admin as isAdmin, auth, blockMaps, wrapApi, internal, initLogRequest, notFound, inMemoryStaticFiles } from './requestUtils'; import { StatsTracker } from './stats'; import { start } from './start'; import { createServerActionsFactory } from './serverActionsManager'; import { init, createRemovedDocument } from './internal'; import { - pollServers, pollDiskSpace, pollCertificateExpirationDate, pollPatreon, startBansCleanup, pollMemoryUsage, - startMergesCleanup, startStrayAuthsCleanup, startClearOldIgnores, startCollectingUsersVisitedCount, - startSupporterInvitesCleanup, startPotentialDuplicatesCleanup, startAccountAlertsCleanup, startUpdatePastSupporters, - startClearTo10Origns, startClearVeryOldOrigns + pollServers, pollDiskSpace, pollCertificateExpirationDate, pollPatreon, startBansCleanup, pollMemoryUsage, + startMergesCleanup, startStrayAuthsCleanup, startClearOldIgnores, startCollectingUsersVisitedCount, + startSupporterInvitesCleanup, startPotentialDuplicatesCleanup, startAccountAlertsCleanup, startUpdatePastSupporters, + startClearTo10Origns, startClearVeryOldOrigns } from './polling'; import { pathTo } from './paths'; import { liveSettings } from './liveSettings'; @@ -61,18 +61,18 @@ import { createEndPoints } from './api/admin'; import { World } from './world'; function getServiceWorker() { - try { - return fs.readFileSync(pathTo('build', 'sw.min.js')); - } catch { - return ''; - } + try { + return fs.readFileSync(pathTo('build', 'sw.min.js')); + } catch { + return ''; + } } mongoose.connect(config.db, { - reconnectTries: Number.MAX_VALUE, - useNewUrlParser: true, - useCreateIndex: true, - useFindAndModify: false, + reconnectTries: Number.MAX_VALUE, + useNewUrlParser: true, + useCreateIndex: true, + useFindAndModify: false, }); const MongoStore = connectMongo(expressSession); @@ -85,12 +85,12 @@ const limit = !production || args.tools ? '100mb' : '100kb'; Bluebird.config({ warnings: false, longStackTraces: !production }); const rollbar = config.rollbar && Rollbar.init({ - accessToken: config.rollbar.serverToken, - environment: config.rollbar.environment, - handleUncaughtExceptions: true, - handleUnhandledRejections: true, - captureUncaught: true, - checkIgnore: rollbarCheckIgnore, + accessToken: config.rollbar.serverToken, + environment: config.rollbar.environment, + handleUncaughtExceptions: true, + handleUnhandledRejections: true, + captureUncaught: true, + checkIgnore: rollbarCheckIgnore, } as any); let assetsPath = pathTo('build', 'assets'); @@ -99,17 +99,17 @@ let adminAssetsPath = pathTo('build', 'assets-admin'); ensureDirSync(pathTo('build-copy')); if (production && args.login) { - const newAssetsPath = pathTo('build-copy', 'assets'); - removeSync(newAssetsPath); - copySync(assetsPath, newAssetsPath); - assetsPath = newAssetsPath; + const newAssetsPath = pathTo('build-copy', 'assets'); + removeSync(newAssetsPath); + copySync(assetsPath, newAssetsPath); + assetsPath = newAssetsPath; } if (production && args.admin) { - const newAssetsPath = pathTo('build-copy', 'assets-admin'); - removeSync(newAssetsPath); - copySync(adminAssetsPath, newAssetsPath); - adminAssetsPath = newAssetsPath; + const newAssetsPath = pathTo('build-copy', 'assets-admin'); + removeSync(newAssetsPath); + copySync(adminAssetsPath, newAssetsPath); + adminAssetsPath = newAssetsPath; } app.set('port', port); @@ -120,17 +120,17 @@ app.set('x-powered-by', false); app.set('etag', false); if (config.proxy) { - app.set('trust proxy', config.proxy); + app.set('trust proxy', config.proxy); } if (production) { - app.use(require('hsts')({ maxAge })); - app.use(require('frameguard')({ action: 'sameorigin' })); - // app.use(require('shrink-ray-current')()); + app.use(require('hsts')({ maxAge })); + app.use(require('frameguard')({ action: 'sameorigin' })); + // app.use(require('shrink-ray-current')()); } if (args.login || args.admin) { - app.use(serveFavicon(pathTo('favicons', 'favicon.ico'))); + app.use(serveFavicon(pathTo('favicons', 'favicon.ico'))); } app.use(morgan('dev', { skip: (_, res) => res.statusCode < 500 || res.statusCode === 503 })); @@ -138,23 +138,23 @@ app.use(morgan('dev', { skip: (_, res) => res.statusCode < 500 || res.statusCode const serviceWorker = getServiceWorker(); if (serviceWorker) { - app.get('/sw.js', (_, res) => { - res.setHeader('Content-Type', 'application/javascript'); - res.setHeader('Cache-Control', 'public, max-age=0'); - res.send(serviceWorker); - }); + app.get('/sw.js', (_, res) => { + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Cache-Control', 'public, max-age=0'); + res.send(serviceWorker); + }); } else { - app.get('/sw.js', notFound); + app.get('/sw.js', notFound); } if (args.login || args.admin) { - if (production) { - app.use(inMemoryStaticFiles(assetsPath, '/assets', maxAge)); - } + if (production) { + app.use(inMemoryStaticFiles(assetsPath, '/assets', maxAge)); + } - app.use('/assets', blockMaps(DEVELOPMENT, !!args.local), express.static(assetsPath, { maxAge, etag })); - app.use(express.static(pathTo('public'), { maxAge, etag })); - app.use(express.static(pathTo('favicons'), { maxAge, etag })); + app.use('/assets', blockMaps(DEVELOPMENT, !!args.local), express.static(assetsPath, { maxAge, etag })); + app.use(express.static(pathTo('public'), { maxAge, etag })); + app.use(express.static(pathTo('favicons'), { maxAge, etag })); } app.use(bodyParser.json({ type: ['json', 'application/csp-report'], limit })); @@ -162,43 +162,43 @@ app.use(bodyParser.urlencoded({ extended: true, limit })); app.use(require('cookie-parser')()); if (args.login || args.admin) { - passport.serializeUser((account, done) => done(null, account._id.toString())); - passport.deserializeUser((id, done) => - Account.findById(id, (err, a) => done(err, a && !isBanned(a) ? a : false))); + passport.serializeUser((account, done) => done(null, account._id.toString())); + passport.deserializeUser((id, done) => + Account.findById(id, (err, a) => done(err, a && !isBanned(a) ? a : false))); } const ignore = [ - 'RangeNotSatisfiableError', - 'PreconditionFailedError', + 'RangeNotSatisfiableError', + 'PreconditionFailedError', ]; app.use((err: any, req: any, res: express.Response, next: any) => { - const ignored = err instanceof Error && includes(ignore, err.name); - return next(ignored ? null : err, req, res); + const ignored = err instanceof Error && includes(ignore, err.name); + return next(ignored ? null : err, req, res); }); if (rollbar) { - app.use(rollbar.errorHandler()); + app.use(rollbar.errorHandler()); } if (!production) { - app.use('/assets-admin', express.static(pathTo('assets'))); - app.use('/assets-admin', express.static(pathTo('src'))); - app.use('/assets', express.static(pathTo('assets'))); - app.use('/assets', express.static(pathTo('src'))); - app.use(require('errorhandler')()); + app.use('/assets-admin', express.static(pathTo('assets'))); + app.use('/assets-admin', express.static(pathTo('src'))); + app.use('/assets', express.static(pathTo('assets'))); + app.use('/assets', express.static(pathTo('src'))); + app.use(require('errorhandler')()); } const httpServer = http.createServer(app); const errorHandler = new SocketErrorHandler(rollbar, server); const createSession = () => expressSession({ - secret: config.secret, - resave: false, - saveUninitialized: false, - cookie: { - maxAge: WEEK * 2, - }, - store: new MongoStore({ mongooseConnection: mongoose.connection }), + secret: config.secret, + resave: false, + saveUninitialized: false, + cookie: { + maxAge: WEEK * 2, + }, + store: new MongoStore({ mongooseConnection: mongoose.connection }), }); const statsPath = pathTo('logs', `stats-${server.id}.csv`); @@ -206,18 +206,18 @@ const stats = new StatsTracker(statsPath); const sessionMiddlewares = once(() => [createSession(), passport.initialize(), passport.session()] as express.RequestHandler[]); const adminMiddlewares = once(() => [...sessionMiddlewares(), isAdmin(server)]); const socketOptionsBase: ServerOptions = { - ws: { Server: WebSocketServer }, - hash: STAMP, + ws: { Server: WebSocketServer }, + hash: STAMP, }; initLogRequest(stats.logRequest); initLogSwearingAndSpamming(stats.logSwearing, stats.logSpamming); const host = createServerHost(httpServer, { - path: args.standaloneadmin && !args.game ? '/admin/ws-admin' : server.path, - ws: { Server: WebSocketServer }, - perMessageDeflate: false, - errorHandler, + path: args.standaloneadmin && !args.game ? '/admin/ws-admin' : server.path, + ws: { Server: WebSocketServer }, + perMessageDeflate: false, + errorHandler, }); let theWorld: World | undefined = undefined; @@ -225,47 +225,47 @@ let sent = 0, received = 0; let sentPackets = 0, receivedPackets = 0; if (args.game) { - const getSettings = () => settings.servers[server.id] || {}; + const getSettings = () => settings.servers[server.id] || {}; - const { world, createServerActions, hiding } = createServerActionsFactory( - server, settings, getSettings, { - stats: () => { - const result = { sent, received, sentPackets, receivedPackets }; - sent = 0; - received = 0; - sentPackets = 0; - receivedPackets = 0; - return result; - } - }); + const { world, createServerActions, hiding } = createServerActionsFactory( + server, settings, getSettings, { + stats: () => { + const result = { sent, received, sentPackets, receivedPackets }; + sent = 0; + received = 0; + sentPackets = 0; + receivedPackets = 0; + return result; + } + }); - const options = { - ...socketOptionsBase, - verifyClient: () => !getSettings().isServerOffline && !liveSettings.shutdown, - forceBinary: true, - onSend: (packet: Packet) => { - sent += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0); - sentPackets++; - stats.logSendStats(packet); - }, - onRecv: (packet: Packet) => { - received += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0); - receivedPackets++; - stats.logRecvStats(packet); - }, - }; + const options = { + ...socketOptionsBase, + verifyClient: () => !getSettings().isServerOffline && !liveSettings.shutdown, + forceBinary: true, + onSend: (packet: Packet) => { + sent += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0); + sentPackets++; + stats.logSendStats(packet); + }, + onRecv: (packet: Packet) => { + received += packet.binary ? packet.binary.byteLength : (packet.json ? packet.json.length : 0); + receivedPackets++; + stats.logRecvStats(packet); + }, + }; - const gameSocket = host.socket(ServerActions, ClientActions, createServerActions as any, options); - const tokens = tokenService(gameSocket); + const gameSocket = host.socket(ServerActions, ClientActions, createServerActions as any, options); + const tokens = tokenService(gameSocket); - start(world, server); - init(world, tokens); + start(world, server); + init(world, tokens); - theWorld = world; + theWorld = world; - const apiInternal = createInternalApi( - world, server, reloadSettings, getSettings, tokens, hiding, stats, liveSettings); - app.use('/api-internal', internal(config, server), wrapApi(server, apiInternal)); + const apiInternal = createInternalApi( + world, server, reloadSettings, getSettings, tokens, hiding, stats, liveSettings); + app.use('/api-internal', internal(config, server), wrapApi(server, apiInternal)); } const endPoints = args.admin ? createEndPoints() : undefined; @@ -274,147 +274,147 @@ const removedDocument = createRemovedDocument(endPoints, adminService); const index = createIndex(assetsPath, adminAssetsPath); if (args.admin) { - if (args.standaloneadmin) { - app.use('/admin/assets-admin', ...adminMiddlewares(), express.static(adminAssetsPath, { maxAge, etag })); - app.get('/admin/assets-admin/*', (_, res) => res.sendStatus(404)); + if (args.standaloneadmin) { + app.use('/admin/assets-admin', ...adminMiddlewares(), express.static(adminAssetsPath, { maxAge, etag })); + app.get('/admin/assets-admin/*', (_, res) => res.sendStatus(404)); - const adminApi = new InternalAdminApi(adminService!, endPoints!); - app.use('/api-internal-admin', internal(config, server), wrapApi(server, adminApi)); - } + const adminApi = new InternalAdminApi(adminService!, endPoints!); + app.use('/api-internal-admin', internal(config, server), wrapApi(server, adminApi)); + } - const createClient = (client: ClientAdminActions & ClientExtensions) => - new AdminServerActions(client, server, settings, adminService!, endPoints!, removedDocument); + const createClient = (client: ClientAdminActions & ClientExtensions) => + new AdminServerActions(client, server, settings, adminService!, endPoints!, removedDocument); - const base = '/admin'; - const assetsBase = args.standaloneadmin ? '/admin' : ''; - const adminSocket = host.socket(AdminServerActions, ClientAdminActions, createClient, socketOptionsBase); - const sendAdminPage = index.admin(production, `${base}/`, assetsBase, 'bootstrap-admin.js', adminSocket); + const base = '/admin'; + const assetsBase = args.standaloneadmin ? '/admin' : ''; + const adminSocket = host.socket(AdminServerActions, ClientAdminActions, createClient, socketOptionsBase); + const sendAdminPage = index.admin(production, `${base}/`, assetsBase, 'bootstrap-admin.js', adminSocket); - app.get(`${base}`, ...adminMiddlewares(), sendAdminPage); - app.get(`${base}/*`, ...adminMiddlewares(), sendAdminPage); + app.get(`${base}`, ...adminMiddlewares(), sendAdminPage); + app.get(`${base}/*`, ...adminMiddlewares(), sendAdminPage); } if (args.tools) { - const toolsPage = index.user( - production, '/tools/', 'style-tools.css', 'bootstrap-tools.js', 'bootstrap-tools.js', undefined, true, !!args.local, false); + const toolsPage = index.user( + production, '/tools/', 'style-tools.css', 'bootstrap-tools.js', 'bootstrap-tools.js', undefined, true, !!args.local, false); - app.get('/tools', ...sessionMiddlewares(), auth, (_, res) => res.send(toolsPage.page)); - app.get('/tools/*', ...sessionMiddlewares(), auth, (_, res) => res.send(toolsPage.page)); + app.get('/tools', ...sessionMiddlewares(), auth, (_, res) => res.send(toolsPage.page)); + app.get('/tools/*', ...sessionMiddlewares(), auth, (_, res) => res.send(toolsPage.page)); - app.use('/api-tools', ...sessionMiddlewares(), apiTools(server, settings, theWorld)); - app.get('/api-tools/*', (_, res) => res.sendStatus(404)); + app.use('/api-tools', ...sessionMiddlewares(), apiTools(server, settings, theWorld)); + app.get('/api-tools/*', (_, res) => res.sendStatus(404)); } if (args.login) { - const socketOptions = createClientOptions(ServerActions, ClientActions, socketOptionsBase); - const userPage = index.user( - production, '/', 'style.css', 'bootstrap.js', 'bootstrap-es.js', socketOptions, false, !!args.local, !production); - const offlinePage = fs.readFileSync(pathTo('public', 'offline.html'), 'utf8'); + const socketOptions = createClientOptions(ServerActions, ClientActions, socketOptionsBase); + const userPage = index.user( + production, '/', 'style.css', 'bootstrap.js', 'bootstrap-es.js', socketOptions, false, !!args.local, !production); + const offlinePage = fs.readFileSync(pathTo('public', 'offline.html'), 'utf8'); - const script = `${config.host}${index.getRevScript('bootstrap.js')}`; - const scriptES = `${config.host}${index.getRevScript('bootstrap-es.js')}`; - const analytics = config.analytics ? 'https://www.google-analytics.com' : ''; - // const workbox = 'https://storage.googleapis.com/workbox-cdn'; - // const rollbarScripts = - // rollbar ? 'https://d37gvrvc0wt4s1.cloudfront.net https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/' : ''; - const csp = `object-src 'none';` - + `frame-src 'self';` - + `frame-ancestors 'self';` - + `worker-src ${config.host}sw.js;` - + `script-src 'unsafe-eval' ${script} ${scriptES} ${analytics};` - // + `report-uri /api2/csp` - ; + const script = `${config.host}${index.getRevScript('bootstrap.js')}`; + const scriptES = `${config.host}${index.getRevScript('bootstrap-es.js')}`; + const analytics = config.analytics ? 'https://www.google-analytics.com' : ''; + // const workbox = 'https://storage.googleapis.com/workbox-cdn'; + // const rollbarScripts = + // rollbar ? 'https://d37gvrvc0wt4s1.cloudfront.net https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/' : ''; + const csp = `object-src 'none';` + + `frame-src 'self';` + + `frame-ancestors 'self';` + + `worker-src ${config.host}sw.js;` + + `script-src 'unsafe-eval' ${script} ${scriptES} ${analytics};` + // + `report-uri /api2/csp` + ; - const linkPreloads: string[] = [ - ...userPage.preload, - ]; + const linkPreloads: string[] = [ + ...userPage.preload, + ]; - app.use('/assets-admin', ...adminMiddlewares(), express.static(adminAssetsPath, { maxAge, etag })); - app.use('/auth', ...sessionMiddlewares(), authRoutes( - config.host, server, settings, liveSettings, args.local || DEVELOPMENT, removedDocument)); - app.use('/api', ...sessionMiddlewares(), api( - server, settings, { version, host: config.host, debug: DEVELOPMENT, local: !!args.local }, removedDocument)); - app.use('/api1', ...sessionMiddlewares(), api1(server, settings)); - app.use('/api2', api2(settings, liveSettings, stats)); + app.use('/assets-admin', ...adminMiddlewares(), express.static(adminAssetsPath, { maxAge, etag })); + app.use('/auth', ...sessionMiddlewares(), authRoutes( + config.host, server, settings, liveSettings, args.local || DEVELOPMENT, removedDocument)); + app.use('/api', ...sessionMiddlewares(), api( + server, settings, { version, host: config.host, debug: DEVELOPMENT, local: !!args.local }, removedDocument)); + app.use('/api1', ...sessionMiddlewares(), api1(server, settings)); + app.use('/api2', api2(settings, liveSettings, stats)); - const loginApi = createInternalLoginApi(settings, liveSettings, stats, reloadSettings, removedDocument); - app.use('/api-internal-login', internal(config, server), wrapApi(server, loginApi)); + const loginApi = createInternalLoginApi(settings, liveSettings, stats, reloadSettings, removedDocument); + app.use('/api-internal-login', internal(config, server), wrapApi(server, loginApi)); - app.get('/assets-admin/*', notFound); - app.get('/assets/*', notFound); - app.get('/auth/*', notFound); - app.get('/api/*', notFound); - app.get('/api1/*', notFound); - app.get('/api2/*', notFound); + app.get('/assets-admin/*', notFound); + app.get('/assets/*', notFound); + app.get('/auth/*', notFound); + app.get('/api/*', notFound); + app.get('/api1/*', notFound); + app.get('/api2/*', notFound); - app.get('/*', (req, res) => { - if (settings.isPageOffline) { - res.send(offlinePage); - } else { - if (production && !args.local) { - res.setHeader('Content-Security-Policy', csp); - res.setHeader('Link', linkPreloads); - } + app.get('/*', (req, res) => { + if (settings.isPageOffline) { + res.send(offlinePage); + } else { + if (production && !args.local) { + res.setHeader('Content-Security-Policy', csp); + res.setHeader('Link', linkPreloads); + } - res.setHeader('Referrer-Policy', 'no-referrer'); - // res.setHeader('X-Frame-Options', 'DENY'); - res.setHeader('X-Content-Type-Options', 'nosniff'); - res.setHeader('X-XSS-Protection', '1; mode=block'); - res.send(userPage.page); + res.setHeader('Referrer-Policy', 'no-referrer'); + // res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + res.send(userPage.page); - stats.logRequest(req, userPage.page, '/'); - } - }); + stats.logRequest(req, userPage.page, '/'); + } + }); } app.use((err: any, req: any, res: express.Response, next: any) => { - if (err instanceof URIError) { - res.redirect(config.host); - } else { - return next(err, req, res); - } + if (err instanceof URIError) { + res.redirect(config.host); + } else { + return next(err, req, res); + } }); reloadSettings().then(() => { - if (args.login || args.game) { - stats.startStatTracking(); - } + if (args.login || args.game) { + stats.startStatTracking(); + } - if (args.login || args.admin) { - pollServers(); - } + if (args.login || args.admin) { + pollServers(); + } - if (args.admin && !args.nocleanup) { - startStrayAuthsCleanup(removedDocument); - startClearOldIgnores(); - startMergesCleanup(); - startBansCleanup(); - startCollectingUsersVisitedCount(); - startSupporterInvitesCleanup(); - startPotentialDuplicatesCleanup(adminService!); - startAccountAlertsCleanup(); - startUpdatePastSupporters(); - startClearTo10Origns(adminService!); - startClearVeryOldOrigns(adminService!); - pollPatreon(server, settings); - } + if (args.admin && !args.nocleanup) { + startStrayAuthsCleanup(removedDocument); + startClearOldIgnores(); + startMergesCleanup(); + startBansCleanup(); + startCollectingUsersVisitedCount(); + startSupporterInvitesCleanup(); + startPotentialDuplicatesCleanup(adminService!); + startAccountAlertsCleanup(); + startUpdatePastSupporters(); + startClearTo10Origns(adminService!); + startClearVeryOldOrigns(adminService!); + pollPatreon(server, settings); + } - if (args.admin) { - pollDiskSpace(); - pollMemoryUsage(); - pollCertificateExpirationDate(); - } + if (args.admin) { + pollDiskSpace(); + pollMemoryUsage(); + pollCertificateExpirationDate(); + } - httpServer.listen(port, () => { - const options = compact([ - app.get('env'), - args.login && 'login', - args.admin && 'admin', - args.standaloneadmin && '(standaloneadmin)', - args.tools && 'tools', - args.game && `game:${server.id}`, - ]); + httpServer.listen(port, () => { + const options = compact([ + app.get('env'), + args.login && 'login', + args.admin && 'admin', + args.standaloneadmin && '(standaloneadmin)', + args.tools && 'tools', + args.game && `game:${server.id}`, + ]); - logger.info(`Listening on port ${port} (${options.join(', ')})`); - }); + logger.info(`Listening on port ${port} (${options.join(', ')})`); + }); }); diff --git a/src/ts/server/serverActions.ts b/src/ts/server/serverActions.ts index 682caef..e4cc6e6 100644 --- a/src/ts/server/serverActions.ts +++ b/src/ts/server/serverActions.ts @@ -1,8 +1,8 @@ import { Socket, Method, SocketServer, Bin, getMethods } from 'ag-sockets'; import { - PlayerAction, ModAction, ChatType, PonyData, IServerActions, TileType, Action, EditorAction, Entity, - EntityOrPonyOptions, LeaveReason, SupporterInvite, InfoFlags, AccountSettings, FriendStatusFlags, - SelectFlags, UpdateFlags, isValidModTile, isValidTile, MapFlags, EntityState, houseTiles + PlayerAction, ModAction, ChatType, PonyData, IServerActions, TileType, Action, EditorAction, Entity, + EntityOrPonyOptions, LeaveReason, SupporterInvite, InfoFlags, AccountSettings, FriendStatusFlags, + SelectFlags, UpdateFlags, isValidModTile, isValidTile, MapFlags, EntityState, houseTiles } from '../common/interfaces'; import { CharacterState, ServerConfig } from '../common/adminInterfaces'; import { PARTY_LIMIT, OFFLINE_PONY, TILE_CHANGE_RANGE, MIN_HIDE_TIME, MAX_HIDE_TIME, PONY_TYPE } from '../common/constants'; @@ -16,8 +16,8 @@ import { PartyService } from './services/party'; import { World, findClientByAccountId, findAllOnlineFriends } from './world'; import { HidingService } from './services/hiding'; import { - createCharacterState, interactWith, useHeldItem, setEntityExpression, getPlayerState, execAction, - updateEntityPlayerState + createCharacterState, interactWith, useHeldItem, setEntityExpression, getPlayerState, execAction, + updateEntityPlayerState } from './playerUtils'; import { allEntities } from './api/account'; import { CounterService } from './services/counter'; @@ -39,724 +39,724 @@ import { createAnEntity } from '../common/entities'; import { mockPaletteManager } from '../common/ponyInfo'; interface AddedEntity { - name: string; - entities: Entity[]; + name: string; + entities: Entity[]; } const modActionNames = ['None', 'Report', 'Mute', 'Shadow', 'Kick', 'Ban']; const playerActionNames = [ - 'None', - 'Ignore', - 'Unignore', - 'InviteToParty', - 'RemoveFromParty', - 'PromotePartyLeader', - 'HidePlayer', - 'InviteToSupporterServers', - 'AddFriend', - 'RemoveFriend', + 'None', + 'Ignore', + 'Unignore', + 'InviteToParty', + 'RemoveFromParty', + 'PromotePartyLeader', + 'HidePlayer', + 'InviteToSupporterServers', + 'AddFriend', + 'RemoveFriend', ]; const editorAdded = new Map(); const debugRate = DEVELOPMENT ? '1000/s' : ''; @Socket({ - id: 'game', - debug: false, - connectionTokens: true, - pingInterval: 3000, - connectionTimeout: 10000, - reconnectTimeout: 500, - transferLimit: 4000, - perMessageDeflate: false, - keepOriginalRequest: true, + id: 'game', + debug: false, + connectionTokens: true, + pingInterval: 3000, + connectionTimeout: 10000, + reconnectTimeout: 500, + transferLimit: 4000, + perMessageDeflate: false, + keepOriginalRequest: true, }) export class ServerActions implements IServerActions, SocketServer { - constructor( - private readonly client: IClient, - private readonly world: World, - private readonly notificationService: NotificationService, - private readonly partyService: PartyService, - private readonly supporterInvites: SupporterInvitesService, - private readonly getSettings: GetSettings, - private readonly server: ServerConfig, - private readonly chatSay: Say, - private readonly moveFunc: Move, - private readonly hiding: HidingService, - private readonly states: CounterService, - private readonly accountService: AccountService, - private readonly ignorePlayer: IgnorePlayer, - private readonly findClientByEntityId: FindClientByEntityId, - private readonly friends: FriendsService, - ) { - } - private get account() { - return this.client.account; - } - private get pony() { - return this.client.pony; - } - private get map() { - return this.client.map; - } - connected() { - this.client.connectedTime = Date.now(); - this.client.lastPacket = Date.now(); - this.client.loading = true; - this.client.reporter.systemLog(`joined [${this.server.id}] as "${this.client.characterName}" [${this.client.ip}]`); - - if (DEVELOPMENT && /slow/.test(this.client.characterName)) { - setTimeout(() => this.world.joinClientToQueue(this.client), 5000); - } else { - this.world.joinClientToQueue(this.client); - } - } - async disconnected() { - const state = createCharacterState(this.pony, this.client.map); - const duration = Date.now() - this.client.connectedTime; - const leaveReason = this.client.leaveReason || 'disconnected'; - - if (this.client.logDisconnect) { - logger.warn(`disconnected (${leaveReason}) account: ${this.client.account.name} [${this.client.accountId}]`); - } - - this.client.offline = true; - this.client.offlineAt = new Date(); - this.client.reporter.systemLog(`left [${this.server.id}] (${leaveReason}) (${formatDuration(duration)})`); - this.world.leaveClient(this.client); - this.partyService.clientDisconnected(this.client); - this.friends.clientDisconnected(this.client); - this.states.add(this.client.characterId, state); - - await Promise.all([ - this.accountService.updateAccount(this.client.accountId, { lastVisit: new Date(), state: this.account.state }), - this.accountService.updateCharacterState(this.client.characterId, state), - ]); - } - @Method({ rateLimit: '2/s', binary: [Bin.U32, Bin.Str, Bin.U8] }) - say(entityId: number, text: string, chatType: ChatType) { - validateNumber(entityId, 'entityId'); - validateString(text, 'text'); - validateNumber(chatType, 'chatType'); - this.updateLastAction(); - - if (this.client.isSwitchingMap) - return; - - const target = entityId ? this.world.getEntityById(entityId) : undefined; - this.chatSay(this.client, text, chatType, target && target.client, this.getSettings()); - } - @Method({ rateLimit: '3/s', binary: [Bin.U32, Bin.U8] }) - select(entityId: number, flags: SelectFlags) { - validateNumber(entityId, 'entityId'); - this.updateLastAction(); - - if (this.client.isSwitchingMap) - return; - - const entity = entityId === 0 ? undefined : (this.world.getEntityById(entityId) || this.getEntityFromClients(entityId)); - const mod = this.client.isMod; - this.client.selected = entity; - - if (entity && entity.client && entity !== this.client.pony) { - if (flags) { - const baseOptions: Partial = mod ? { modInfo: getModInfo(entity.client) } : {}; - const options = { ...baseOptions, ...entity.extraOptions }; - - if (hasFlag(flags, SelectFlags.FetchInfo)) { - const playerState = getPlayerState(this.client, entity); - const flags = UpdateFlags.Options | UpdateFlags.Name | UpdateFlags.Info | UpdateFlags.PlayerState; - pushUpdateEntityToClient(this.client, { entity, flags, options, playerState }); - } else if (hasFlag(flags, SelectFlags.FetchEx) || mod) { - pushUpdateEntityToClient(this.client, { entity, flags: UpdateFlags.Options, options }); - } - } - } else if (entityId) { - this.client.updateSelection(entityId, 0); - } - } - @Method({ rateLimit: '2/s', serverRateLimit: '4/s', binary: [Bin.U32] }) - interact(entityId: number) { - validateNumber(entityId, 'entityId'); - this.updateLastAction(); - - if (this.client.isSwitchingMap) - return; - - interactWith(this.client, this.world.getEntityById(entityId)); - } - @Method({ rateLimit: '2/s', binary: [] }) - use() { - this.updateLastAction(); - - if (this.client.isSwitchingMap) - return; - - useHeldItem(this.client); - } - @Method({ rateLimit: '3/s', serverRateLimit: '8/s', binary: [Bin.U8] }) - action(action: Action) { - validateNumber(action, 'action'); - this.updateLastAction(); - - switch (action) { - case Action.KeepAlive: - break; - case Action.UnhideAllHiddenPlayers: - this.hiding.requestUnhideAll(this.client); - break; - default: - if (this.client.isSwitchingMap) - return; - - execAction(this.client, action, this.getSettings()); - break; - } - } - @Method({ rateLimit: '2/s', serverRateLimit: '4/s', binary: [Bin.U8, Bin.Obj] }) - actionParam(action: Action, param: any) { - validateNumber(action, 'action'); - - switch (action) { - case Action.CancelSupporterInvite: - validateString(param, 'param'); - // TODO: ... - break; - case Action.SwapCharacter: { - validateString(param, 'param'); - this.updateLastAction(); - - if (param !== this.client.characterId) { - swapCharacter(this.client, this.world, { account: this.client.account._id, _id: param }) - .catch(e => this.client.reporter.error(e)); - } - - break; - } - case Action.RemoveFriend: { - validateString(param, 'param'); - this.updateLastAction(); - const target = findClientByAccountId(this.world, param); - - if (target) { - this.friends.remove(this.client, target); - } else { - this.friends.removeByAccountId(this.client, param); - } - - break; - } - case Action.FriendsCRC: { - validateNumber(param, 'param'); - const crc = param >>> 0; - - if (this.client.friendsCRC === undefined) { - this.client.friendsCRC = computeFriendsCRC(Array.from(this.client.friends.values())); - } - - if (this.client.friendsCRC !== crc) { - findFriends(this.client.accountId, true) - .then(friends => { - this.client.updateFriends(friends.map(f => { - const client = findClientByAccountId(this.world, f.accountId); - - return { - accountId: f.accountId, - accountName: f.accountName, - entityId: client && client.pony.id, - status: client ? FriendStatusFlags.Online : FriendStatusFlags.None, - name: f.name, - nameBad: f.nameBad, - info: f.pony, - }; - }), true); - }) - .catch(e => logger.error(e)); - } - - break; - } - case Action.RemoveEntity: { - validateNumber(param, 'param'); - this.updateLastAction(); - const entity = this.world.getEntityById(param | 0); - - if ( - entity && hasFlag(entity.state, EntityState.Editable) && this.pony.options!.hold === entities.broom.type && - this.map.regions.some(r => includes(r.entities, entity)) - ) { - if (this.isHouseLocked()) { - saySystem(this.client, `House is locked`); - } else { - this.world.removeEntity(entity, this.map); - } - } - - break; - } - case Action.PlaceEntity: { - if ( - !param || typeof param !== 'object' || typeof param.x !== 'number' || typeof param.y !== 'number' || - typeof param.type !== 'number' - ) { - return; - } - - this.updateLastAction(); - - const { x, y, type } = param as { x: number; y: number; type: number; }; - - if ( - isOutsideMap(x, y, this.map) || - !entities.placeableEntities.some(x => x.type === type) || - !hasFlag(this.map.flags, MapFlags.EditableEntities) - ) { - return saySystem(this.client, `Cannot place object`); - } - - if (this.isHouseLocked()) { - return saySystem(this.client, `House is locked`); - } - - let totalEditableEntities = 0; - - for (const region of this.map.regions) { - for (const entity of region.entities) { - if (hasFlag(entity.state, EntityState.Editable)) { - totalEditableEntities++; - } - } - } - - if (totalEditableEntities >= this.map.editableEntityLimit) { - return saySystem(this.client, `Object limit reached`); - } - - const entity = createAnEntity(type, 0, x, y, {}, mockPaletteManager, this.world); - entity.state |= EntityState.Editable; - this.world.addEntity(entity, this.map); - break; - } - default: - throw new Error(`Invalid Action (${action})`); - } - } - @Method({ rateLimit: '10/s', serverRateLimit: '20/s', binary: [Bin.U8, Bin.Obj] }) - actionParam2(action: Action, param: any) { - validateNumber(action, 'action'); - - switch (action) { - case Action.Info: - validateNumber(param, 'param'); - this.client.incognito = hasFlag(param, InfoFlags.Incognito); - this.client.supportsWasm = hasFlag(param, InfoFlags.SupportsWASM); - this.client.supportsLetAndConst = hasFlag(param, InfoFlags.SupportsLetAndConst); - break; - case Action.RequestEntityInfo: { - validateNumber(param, 'param'); - - const entity = this.world.getEntityById(param | 0); - - if (entity && entity.client) { - this.client.entityInfo(entity.id, entity.name || '', entity.crc || 0, !!entity.nameBad); - } - - break; - } - default: - throw new Error(`Invalid Action (${action})`); - } - } - @Method({ promise: true, rateLimit: '1/s', binary: [] }) - async getInvites(): Promise { - return [ - { id: 'a', info: OFFLINE_PONY, name: 'Offline Pony', active: true }, - { id: 'b', info: OFFLINE_PONY, name: 'Fuzzy', active: true }, - { id: 'c', info: OFFLINE_PONY, name: 'Molly', active: false }, - ]; - } - @Method({ rateLimit: '2/s', serverRateLimit: '4/s', binary: [Bin.U32] }) - expression(expression: number) { - validateNumber(expression, 'expression'); - this.updateLastAction(); - - const expr = decodeExpression(expression); - const cancellable = !!expr && isCancellableExpression(expr); - - if (cancellable) { - setEntityExpression(this.pony, expr, 0, true); - } else { - this.pony.exprPermanent = expr; - setEntityExpression(this.pony, undefined, 0); - } - } - @Method({ rateLimit: '3/s', binary: [Bin.U32, Bin.U8, Bin.Obj] }) - playerAction(entityId: number, action: PlayerAction, param: unknown) { - validateNumber(entityId, 'entityId'); - validateNumber(action, 'action'); - this.updateLastAction(); - - const target = this.getClientByEntityId(entityId); - - if (!target) { - this.client.reporter.warnLog(`No client for: ${playerActionNames[action]} [${action}], id: ${entityId}`); - return; - } - - switch (action) { - case PlayerAction.Ignore: - case PlayerAction.Unignore: - this.ignorePlayer(this.client, target, action === PlayerAction.Ignore); - break; - case PlayerAction.InviteToParty: - this.partyService.invite(this.client, target); - break; - case PlayerAction.RemoveFromParty: - this.partyService.remove(this.client, target); - break; - case PlayerAction.PromotePartyLeader: - this.partyService.promoteLeader(this.client, target); - break; - case PlayerAction.HidePlayer: - const hideFor = toInt(param); - if (hideFor === 0) { - this.hiding.requestHide(this.client, target, 0); - } else { - this.hiding.requestHide(this.client, target, clamp(hideFor, MIN_HIDE_TIME, MAX_HIDE_TIME)); - } - break; - case PlayerAction.InviteToSupporterServers: - this.supporterInvites.requestInvite(this.client, target); - break; - case PlayerAction.AddFriend: - this.friends.add(this.client, target); - break; - case PlayerAction.RemoveFriend: - this.friends.remove(this.client, target); - break; - default: - throw new Error(`Invalid player action (${playerActionNames[action]}) [${action}]`); - } - } - @Method({ rateLimit: '1/s', binary: [] }) - leaveParty() { - this.updateLastAction(); - - if (this.client.isSwitchingMap) - return; - - if (this.client.party) { - this.partyService.remove(this.client.party.leader, this.client); - } - } - @Method({ promise: true, rateLimit: '10/s', binary: [Bin.U32, Bin.U8, Bin.I32] }) - async otherAction(entityId: number, action: ModAction, param: number) { - validateNumber(entityId, 'entityId'); - validateNumber(action, 'action'); - validateNumber(param, 'param'); - this.updateLastAction(); - - const target = this.getClientForModAction(entityId, modActionNames[action]); - - switch (action) { - case ModAction.Report: - await this.logModAction(target, 'Reported'); - break; - case ModAction.Mute: - await this.setBan(target, 'mute', param); - break; - case ModAction.Shadow: - await this.setBan(target, 'shadow', param); - break; - case ModAction.Ban: - await this.setBan(target, 'ban', -1); - break; - case ModAction.Kick: - await this.world.kick(target, 'mod kick'); - break; - default: - throw new Error(`Invalid mod action (${action})`); - } - } - private setBan(target: IClient, field: 'mute' | 'shadow' | 'ban', value: number) { - const timeout = value > 0 ? Date.now() + value : value; - this.logModAction(target, banMessage(field, timeout)); - return this.accountService.update(target.accountId, { [field]: timeout }); - } - @Method({ promise: true, binary: [Bin.U32, Bin.Str] }) - async setNote(entityId: number, text: string) { - validateNumber(entityId, 'entityId'); - validateString(text, 'text', true); - this.updateLastAction(); - - const client = this.getClientForModAction(entityId, 'setNote'); - await this.accountService.update(client.accountId, { note: text }); - } - @Method({ rateLimit: '5/s', binary: [Bin.Obj] }) - async saveSettings(settings: AccountSettings) { - this.updateLastAction(); - - const wasHidden = !!this.client.accountSettings.hidden; - await this.accountService.updateSettings(this.account, settings); - this.client.accountSettings = { ...this.account.settings }; - this.client.reporter.systemLog(`Saved settings`); - const isHidden = !!this.client.accountSettings.hidden; - - if (wasHidden !== isHidden) { - for (const friend of findAllOnlineFriends(this.world, this.client)) { - if (isHidden) { - friend.updateFriends([{ accountId: this.client.accountId, status: FriendStatusFlags.None }], false); - } else { - friend.updateFriends([toFriendOnline(this.client)], false); - } - - updateEntityPlayerState(friend, this.client.pony); - } - } - } - @Method({ binary: [Bin.U16] }) - acceptNotification(id: number) { - validateNumber(id, 'id'); - this.updateLastAction(); - this.notificationService.acceptNotification(this.client, id); - } - @Method({ binary: [Bin.U16] }) - rejectNotification(id: number) { - validateNumber(id, 'id'); - this.updateLastAction(); - this.notificationService.rejectNotification(this.client, id); - } - @Method({ binary: [[Bin.U32]] }) - getPonies(ids: number[]) { - const party = this.client.party; - - const createPonyData = ({ pony }: IClient): PonyData => { - return [ - pony.id, - pony.options, - pony.encodedName, - pony.encryptedInfoSafe, - getPlayerState(this.client, pony), - !!pony.nameBad, - ]; - }; - - if (party && ids && ids.length && ids.length <= PARTY_LIMIT) { - const ponies = party.clients - .filter(c => includes(ids, c.pony.id)) - .map(createPonyData); - this.client.updatePonies(ponies); - } - } - @Method({ binary: [] }) - loaded() { - this.client.loading = false; - } - @Method({ binary: [] }) - fixedPosition() { - this.client.fixingPosition = false; - } - @Method({ binary: [Bin.U32, Bin.U32, Bin.U16, Bin.U16] }) - updateCamera(x: number, y: number, width: number, height: number) { - validateNumber(x, 'x'); - validateNumber(y, 'y'); - validateNumber(width, 'width'); - validateNumber(height, 'height'); - this.updateLastAction(); - - setupCamera(this.client.camera, x, y, width, height, this.client.map); - } - @Method({ binary: [Bin.U32, Bin.U32, Bin.U32, Bin.U32, Bin.U16] }) - move(a: number, b: number, c: number, d: number, e: number) { - validateNumber(a, 'a'); - validateNumber(b, 'b'); - validateNumber(c, 'c'); - validateNumber(d, 'd'); - validateNumber(e, 'e'); - this.updateLastAction(); - - this.moveFunc(this.client, Date.now(), a, b, c, d, e, this.getSettings()); - } - private isHouseLocked() { - return this.map.editingLocked && this.client.party && this.client.party.leader !== this.client; - } - @Method({ rateLimit: debugRate || '3/s', serverRateLimit: debugRate || '7/s', binary: [Bin.U16, Bin.U16, Bin.U8] }) - changeTile(x: number, y: number, type: TileType) { - validateNumber(x, 'x'); - validateNumber(y, 'y'); - validateNumber(type, 'type'); - this.updateLastAction(); - - if (this.client.isSwitchingMap) - return; - - const wallTile = type === TileType.WallH || type === TileType.WallV; - - if (hasFlag(this.map.flags, MapFlags.EditableWalls) && wallTile) { - if (this.isHouseLocked()) { - saySystem(this.client, `House is locked`); - } else { - this.world.toggleWall(this.map, x, y, type); - } - } else if (BETA && this.client.isMod && wallTile) { - this.world.toggleWall(this.map, x, y, type); - } else if (hasFlag(this.map.flags, MapFlags.EditableTiles) && this.pony.options!.hold === entities.shovel.type) { - if (!houseTiles.some(t => t.type === type)) - return; - - if (this.isHouseLocked()) - return saySystem(this.client, `House is locked`); - - this.world.setTile(this.map, x, y, type); - } else if (BETA && this.client.isMod && isValidModTile(type)) { - this.world.setTile(this.map, x, y, type); - } else if (isValidTile(type)) { - if ((BETA || distanceXY(x, y, this.pony.x, this.pony.y) < TILE_CHANGE_RANGE)) { - const tile = getTile(this.map, x, y); - - if (tile === TileType.Dirt || tile === TileType.Grass) { - if (this.client.shadowed) { - pushUpdateTileToClient(this.client, x, y, type); - } else { - this.world.setTile(this.map, x, y, type); - } - } - } - } - } - @Method({ rateLimit: '1/s', binary: [] }) - leave() { - this.client.leaveReason = 'leave'; - this.client.left(LeaveReason.None); - } - @Method({ binary: [Bin.Obj] }) - editorAction(action: EditorAction) { - if (this.server.flags.objects && this.client.isMod) { - const added = editorAdded.get(this.client.accountId) || []; - editorAdded.set(this.client.accountId, added); - - switch (action.type) { - case 'place': - if (includes(allEntities, action.entity)) { - const name = action.entity; - const entity = (entities as any)[name](action.x, action.y); - const toAdd = Array.isArray(entity) ? entity : [entity]; - toAdd.forEach(e => this.world.addEntity(e, this.map)); - added.push({ name, entities: toAdd }); - } else { - saySystem(this.client, 'Invalid entity'); - } - break; - case 'move': - for (const { id, x, y } of action.entities) { - const entity = this.world.getEntityById(id); - - if (entity && entity.type !== PONY_TYPE) { - entity.x = x; - entity.y = y; - updateEntity(entity, false); - updateRegion(entity, this.client.map); - getExpectedRegion(entity, this.client.map).colliderDirty = true; - } - } - break; - case 'undo': - const remove = added.pop(); - remove && remove.entities.forEach(e => this.world.removeEntityFromSomeMap(e)); - break; - case 'clear': - added.forEach(x => x.entities.forEach(e => this.world.removeEntityFromSomeMap(e))); - added.length = 0; - break; - case 'list': - const existingEntities = added - .filter(({ entities }) => entities.some(e => !!this.world.getEntityById(e.id))) - .map(({ name, entities: [{ x, y }] }) => ({ name, x, y })); - this.client.entityList(existingEntities); - break; - case 'remove': - for (const id of action.entities) { - const entity = this.world.getEntityById(id); - - if (entity && entity.type !== PONY_TYPE) { - this.world.removeEntityFromSomeMap(entity); - } - } - break; - case 'tile': { - const { x, y, tile, size } = action; - - if (isValidModTile(tile)) { - for (let iy = 0; iy < size; iy++) { - for (let ix = 0; ix < size; ix++) { - this.world.setTile(this.map, x + ix, y + iy, tile); - } - } - } - - break; - } - case 'party': { - const entities = findEntities(this.map, e => !!e.client && /^debug/.test(e.name || '')); - - for (const e of entities.slice(0, PARTY_LIMIT - 1)) { - this.partyService.invite(this.client, e.client!); - } - - break; - } - default: - throw new Error(`Invalid editor action (${action})`); - } - } - } - private logModAction(client: IClient, title: string) { - client.reporter.system(`${title} by ${this.account.name}`); - } - private getClientForModAction(entityId: number, action: string) { - if (!this.client.isMod) { - this.client.disconnect(true, true); - throw new Error(`Action not allowed (${action})`); - } - - const client = this.world.getClientByEntityId(entityId); - - if (!client) { - throw new Error(`Client does not exist (${action})`); - } - - if (client.accountId === this.client.accountId) { - throw new Error(`Cannot perform action on self (${action})`); - } - - return client; - } - private getEntityFromClients(entityId: number) { - const client = this.getClientByEntityId(entityId); - return client && client.pony; - } - private getClientByEntityId(entityId: number) { - return this.world.getClientByEntityId(entityId) || this.findClientByEntityId(this.client, entityId); - } - private updateLastAction() { - this.client.lastPacket = Date.now(); - } + constructor( + private readonly client: IClient, + private readonly world: World, + private readonly notificationService: NotificationService, + private readonly partyService: PartyService, + private readonly supporterInvites: SupporterInvitesService, + private readonly getSettings: GetSettings, + private readonly server: ServerConfig, + private readonly chatSay: Say, + private readonly moveFunc: Move, + private readonly hiding: HidingService, + private readonly states: CounterService, + private readonly accountService: AccountService, + private readonly ignorePlayer: IgnorePlayer, + private readonly findClientByEntityId: FindClientByEntityId, + private readonly friends: FriendsService, + ) { + } + private get account() { + return this.client.account; + } + private get pony() { + return this.client.pony; + } + private get map() { + return this.client.map; + } + connected() { + this.client.connectedTime = Date.now(); + this.client.lastPacket = Date.now(); + this.client.loading = true; + this.client.reporter.systemLog(`joined [${this.server.id}] as "${this.client.characterName}" [${this.client.ip}]`); + + if (DEVELOPMENT && /slow/.test(this.client.characterName)) { + setTimeout(() => this.world.joinClientToQueue(this.client), 5000); + } else { + this.world.joinClientToQueue(this.client); + } + } + async disconnected() { + const state = createCharacterState(this.pony, this.client.map); + const duration = Date.now() - this.client.connectedTime; + const leaveReason = this.client.leaveReason || 'disconnected'; + + if (this.client.logDisconnect) { + logger.warn(`disconnected (${leaveReason}) account: ${this.client.account.name} [${this.client.accountId}]`); + } + + this.client.offline = true; + this.client.offlineAt = new Date(); + this.client.reporter.systemLog(`left [${this.server.id}] (${leaveReason}) (${formatDuration(duration)})`); + this.world.leaveClient(this.client); + this.partyService.clientDisconnected(this.client); + this.friends.clientDisconnected(this.client); + this.states.add(this.client.characterId, state); + + await Promise.all([ + this.accountService.updateAccount(this.client.accountId, { lastVisit: new Date(), state: this.account.state }), + this.accountService.updateCharacterState(this.client.characterId, state), + ]); + } + @Method({ rateLimit: '2/s', binary: [Bin.U32, Bin.Str, Bin.U8] }) + say(entityId: number, text: string, chatType: ChatType) { + validateNumber(entityId, 'entityId'); + validateString(text, 'text'); + validateNumber(chatType, 'chatType'); + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + const target = entityId ? this.world.getEntityById(entityId) : undefined; + this.chatSay(this.client, text, chatType, target && target.client, this.getSettings()); + } + @Method({ rateLimit: '3/s', binary: [Bin.U32, Bin.U8] }) + select(entityId: number, flags: SelectFlags) { + validateNumber(entityId, 'entityId'); + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + const entity = entityId === 0 ? undefined : (this.world.getEntityById(entityId) || this.getEntityFromClients(entityId)); + const mod = this.client.isMod; + this.client.selected = entity; + + if (entity && entity.client && entity !== this.client.pony) { + if (flags) { + const baseOptions: Partial = mod ? { modInfo: getModInfo(entity.client) } : {}; + const options = { ...baseOptions, ...entity.extraOptions }; + + if (hasFlag(flags, SelectFlags.FetchInfo)) { + const playerState = getPlayerState(this.client, entity); + const flags = UpdateFlags.Options | UpdateFlags.Name | UpdateFlags.Info | UpdateFlags.PlayerState; + pushUpdateEntityToClient(this.client, { entity, flags, options, playerState }); + } else if (hasFlag(flags, SelectFlags.FetchEx) || mod) { + pushUpdateEntityToClient(this.client, { entity, flags: UpdateFlags.Options, options }); + } + } + } else if (entityId) { + this.client.updateSelection(entityId, 0); + } + } + @Method({ rateLimit: '2/s', serverRateLimit: '4/s', binary: [Bin.U32] }) + interact(entityId: number) { + validateNumber(entityId, 'entityId'); + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + interactWith(this.client, this.world.getEntityById(entityId)); + } + @Method({ rateLimit: '2/s', binary: [] }) + use() { + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + useHeldItem(this.client); + } + @Method({ rateLimit: '3/s', serverRateLimit: '8/s', binary: [Bin.U8] }) + action(action: Action) { + validateNumber(action, 'action'); + this.updateLastAction(); + + switch (action) { + case Action.KeepAlive: + break; + case Action.UnhideAllHiddenPlayers: + this.hiding.requestUnhideAll(this.client); + break; + default: + if (this.client.isSwitchingMap) + return; + + execAction(this.client, action, this.getSettings()); + break; + } + } + @Method({ rateLimit: '2/s', serverRateLimit: '4/s', binary: [Bin.U8, Bin.Obj] }) + actionParam(action: Action, param: any) { + validateNumber(action, 'action'); + + switch (action) { + case Action.CancelSupporterInvite: + validateString(param, 'param'); + // TODO: ... + break; + case Action.SwapCharacter: { + validateString(param, 'param'); + this.updateLastAction(); + + if (param !== this.client.characterId) { + swapCharacter(this.client, this.world, { account: this.client.account._id, _id: param }) + .catch(e => this.client.reporter.error(e)); + } + + break; + } + case Action.RemoveFriend: { + validateString(param, 'param'); + this.updateLastAction(); + const target = findClientByAccountId(this.world, param); + + if (target) { + this.friends.remove(this.client, target); + } else { + this.friends.removeByAccountId(this.client, param); + } + + break; + } + case Action.FriendsCRC: { + validateNumber(param, 'param'); + const crc = param >>> 0; + + if (this.client.friendsCRC === undefined) { + this.client.friendsCRC = computeFriendsCRC(Array.from(this.client.friends.values())); + } + + if (this.client.friendsCRC !== crc) { + findFriends(this.client.accountId, true) + .then(friends => { + this.client.updateFriends(friends.map(f => { + const client = findClientByAccountId(this.world, f.accountId); + + return { + accountId: f.accountId, + accountName: f.accountName, + entityId: client && client.pony.id, + status: client ? FriendStatusFlags.Online : FriendStatusFlags.None, + name: f.name, + nameBad: f.nameBad, + info: f.pony, + }; + }), true); + }) + .catch(e => logger.error(e)); + } + + break; + } + case Action.RemoveEntity: { + validateNumber(param, 'param'); + this.updateLastAction(); + const entity = this.world.getEntityById(param | 0); + + if ( + entity && hasFlag(entity.state, EntityState.Editable) && this.pony.options!.hold === entities.broom.type && + this.map.regions.some(r => includes(r.entities, entity)) + ) { + if (this.isHouseLocked()) { + saySystem(this.client, `House is locked`); + } else { + this.world.removeEntity(entity, this.map); + } + } + + break; + } + case Action.PlaceEntity: { + if ( + !param || typeof param !== 'object' || typeof param.x !== 'number' || typeof param.y !== 'number' || + typeof param.type !== 'number' + ) { + return; + } + + this.updateLastAction(); + + const { x, y, type } = param as { x: number; y: number; type: number; }; + + if ( + isOutsideMap(x, y, this.map) || + !entities.placeableEntities.some(x => x.type === type) || + !hasFlag(this.map.flags, MapFlags.EditableEntities) + ) { + return saySystem(this.client, `Cannot place object`); + } + + if (this.isHouseLocked()) { + return saySystem(this.client, `House is locked`); + } + + let totalEditableEntities = 0; + + for (const region of this.map.regions) { + for (const entity of region.entities) { + if (hasFlag(entity.state, EntityState.Editable)) { + totalEditableEntities++; + } + } + } + + if (totalEditableEntities >= this.map.editableEntityLimit) { + return saySystem(this.client, `Object limit reached`); + } + + const entity = createAnEntity(type, 0, x, y, {}, mockPaletteManager, this.world); + entity.state |= EntityState.Editable; + this.world.addEntity(entity, this.map); + break; + } + default: + throw new Error(`Invalid Action (${action})`); + } + } + @Method({ rateLimit: '10/s', serverRateLimit: '20/s', binary: [Bin.U8, Bin.Obj] }) + actionParam2(action: Action, param: any) { + validateNumber(action, 'action'); + + switch (action) { + case Action.Info: + validateNumber(param, 'param'); + this.client.incognito = hasFlag(param, InfoFlags.Incognito); + this.client.supportsWasm = hasFlag(param, InfoFlags.SupportsWASM); + this.client.supportsLetAndConst = hasFlag(param, InfoFlags.SupportsLetAndConst); + break; + case Action.RequestEntityInfo: { + validateNumber(param, 'param'); + + const entity = this.world.getEntityById(param | 0); + + if (entity && entity.client) { + this.client.entityInfo(entity.id, entity.name || '', entity.crc || 0, !!entity.nameBad); + } + + break; + } + default: + throw new Error(`Invalid Action (${action})`); + } + } + @Method({ promise: true, rateLimit: '1/s', binary: [] }) + async getInvites(): Promise { + return [ + { id: 'a', info: OFFLINE_PONY, name: 'Offline Pony', active: true }, + { id: 'b', info: OFFLINE_PONY, name: 'Fuzzy', active: true }, + { id: 'c', info: OFFLINE_PONY, name: 'Molly', active: false }, + ]; + } + @Method({ rateLimit: '2/s', serverRateLimit: '4/s', binary: [Bin.U32] }) + expression(expression: number) { + validateNumber(expression, 'expression'); + this.updateLastAction(); + + const expr = decodeExpression(expression); + const cancellable = !!expr && isCancellableExpression(expr); + + if (cancellable) { + setEntityExpression(this.pony, expr, 0, true); + } else { + this.pony.exprPermanent = expr; + setEntityExpression(this.pony, undefined, 0); + } + } + @Method({ rateLimit: '3/s', binary: [Bin.U32, Bin.U8, Bin.Obj] }) + playerAction(entityId: number, action: PlayerAction, param: unknown) { + validateNumber(entityId, 'entityId'); + validateNumber(action, 'action'); + this.updateLastAction(); + + const target = this.getClientByEntityId(entityId); + + if (!target) { + this.client.reporter.warnLog(`No client for: ${playerActionNames[action]} [${action}], id: ${entityId}`); + return; + } + + switch (action) { + case PlayerAction.Ignore: + case PlayerAction.Unignore: + this.ignorePlayer(this.client, target, action === PlayerAction.Ignore); + break; + case PlayerAction.InviteToParty: + this.partyService.invite(this.client, target); + break; + case PlayerAction.RemoveFromParty: + this.partyService.remove(this.client, target); + break; + case PlayerAction.PromotePartyLeader: + this.partyService.promoteLeader(this.client, target); + break; + case PlayerAction.HidePlayer: + const hideFor = toInt(param); + if (hideFor === 0) { + this.hiding.requestHide(this.client, target, 0); + } else { + this.hiding.requestHide(this.client, target, clamp(hideFor, MIN_HIDE_TIME, MAX_HIDE_TIME)); + } + break; + case PlayerAction.InviteToSupporterServers: + this.supporterInvites.requestInvite(this.client, target); + break; + case PlayerAction.AddFriend: + this.friends.add(this.client, target); + break; + case PlayerAction.RemoveFriend: + this.friends.remove(this.client, target); + break; + default: + throw new Error(`Invalid player action (${playerActionNames[action]}) [${action}]`); + } + } + @Method({ rateLimit: '1/s', binary: [] }) + leaveParty() { + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + if (this.client.party) { + this.partyService.remove(this.client.party.leader, this.client); + } + } + @Method({ promise: true, rateLimit: '10/s', binary: [Bin.U32, Bin.U8, Bin.I32] }) + async otherAction(entityId: number, action: ModAction, param: number) { + validateNumber(entityId, 'entityId'); + validateNumber(action, 'action'); + validateNumber(param, 'param'); + this.updateLastAction(); + + const target = this.getClientForModAction(entityId, modActionNames[action]); + + switch (action) { + case ModAction.Report: + await this.logModAction(target, 'Reported'); + break; + case ModAction.Mute: + await this.setBan(target, 'mute', param); + break; + case ModAction.Shadow: + await this.setBan(target, 'shadow', param); + break; + case ModAction.Ban: + await this.setBan(target, 'ban', -1); + break; + case ModAction.Kick: + await this.world.kick(target, 'mod kick'); + break; + default: + throw new Error(`Invalid mod action (${action})`); + } + } + private setBan(target: IClient, field: 'mute' | 'shadow' | 'ban', value: number) { + const timeout = value > 0 ? Date.now() + value : value; + this.logModAction(target, banMessage(field, timeout)); + return this.accountService.update(target.accountId, { [field]: timeout }); + } + @Method({ promise: true, binary: [Bin.U32, Bin.Str] }) + async setNote(entityId: number, text: string) { + validateNumber(entityId, 'entityId'); + validateString(text, 'text', true); + this.updateLastAction(); + + const client = this.getClientForModAction(entityId, 'setNote'); + await this.accountService.update(client.accountId, { note: text }); + } + @Method({ rateLimit: '5/s', binary: [Bin.Obj] }) + async saveSettings(settings: AccountSettings) { + this.updateLastAction(); + + const wasHidden = !!this.client.accountSettings.hidden; + await this.accountService.updateSettings(this.account, settings); + this.client.accountSettings = { ...this.account.settings }; + this.client.reporter.systemLog(`Saved settings`); + const isHidden = !!this.client.accountSettings.hidden; + + if (wasHidden !== isHidden) { + for (const friend of findAllOnlineFriends(this.world, this.client)) { + if (isHidden) { + friend.updateFriends([{ accountId: this.client.accountId, status: FriendStatusFlags.None }], false); + } else { + friend.updateFriends([toFriendOnline(this.client)], false); + } + + updateEntityPlayerState(friend, this.client.pony); + } + } + } + @Method({ binary: [Bin.U16] }) + acceptNotification(id: number) { + validateNumber(id, 'id'); + this.updateLastAction(); + this.notificationService.acceptNotification(this.client, id); + } + @Method({ binary: [Bin.U16] }) + rejectNotification(id: number) { + validateNumber(id, 'id'); + this.updateLastAction(); + this.notificationService.rejectNotification(this.client, id); + } + @Method({ binary: [[Bin.U32]] }) + getPonies(ids: number[]) { + const party = this.client.party; + + const createPonyData = ({ pony }: IClient): PonyData => { + return [ + pony.id, + pony.options, + pony.encodedName, + pony.encryptedInfoSafe, + getPlayerState(this.client, pony), + !!pony.nameBad, + ]; + }; + + if (party && ids && ids.length && ids.length <= PARTY_LIMIT) { + const ponies = party.clients + .filter(c => includes(ids, c.pony.id)) + .map(createPonyData); + this.client.updatePonies(ponies); + } + } + @Method({ binary: [] }) + loaded() { + this.client.loading = false; + } + @Method({ binary: [] }) + fixedPosition() { + this.client.fixingPosition = false; + } + @Method({ binary: [Bin.U32, Bin.U32, Bin.U16, Bin.U16] }) + updateCamera(x: number, y: number, width: number, height: number) { + validateNumber(x, 'x'); + validateNumber(y, 'y'); + validateNumber(width, 'width'); + validateNumber(height, 'height'); + this.updateLastAction(); + + setupCamera(this.client.camera, x, y, width, height, this.client.map); + } + @Method({ binary: [Bin.U32, Bin.U32, Bin.U32, Bin.U32, Bin.U16] }) + move(a: number, b: number, c: number, d: number, e: number) { + validateNumber(a, 'a'); + validateNumber(b, 'b'); + validateNumber(c, 'c'); + validateNumber(d, 'd'); + validateNumber(e, 'e'); + this.updateLastAction(); + + this.moveFunc(this.client, Date.now(), a, b, c, d, e, this.getSettings()); + } + private isHouseLocked() { + return this.map.editingLocked && this.client.party && this.client.party.leader !== this.client; + } + @Method({ rateLimit: debugRate || '3/s', serverRateLimit: debugRate || '7/s', binary: [Bin.U16, Bin.U16, Bin.U8] }) + changeTile(x: number, y: number, type: TileType) { + validateNumber(x, 'x'); + validateNumber(y, 'y'); + validateNumber(type, 'type'); + this.updateLastAction(); + + if (this.client.isSwitchingMap) + return; + + const wallTile = type === TileType.WallH || type === TileType.WallV; + + if (hasFlag(this.map.flags, MapFlags.EditableWalls) && wallTile) { + if (this.isHouseLocked()) { + saySystem(this.client, `House is locked`); + } else { + this.world.toggleWall(this.map, x, y, type); + } + } else if (BETA && this.client.isMod && wallTile) { + this.world.toggleWall(this.map, x, y, type); + } else if (hasFlag(this.map.flags, MapFlags.EditableTiles) && this.pony.options!.hold === entities.shovel.type) { + if (!houseTiles.some(t => t.type === type)) + return; + + if (this.isHouseLocked()) + return saySystem(this.client, `House is locked`); + + this.world.setTile(this.map, x, y, type); + } else if (BETA && this.client.isMod && isValidModTile(type)) { + this.world.setTile(this.map, x, y, type); + } else if (isValidTile(type)) { + if ((BETA || distanceXY(x, y, this.pony.x, this.pony.y) < TILE_CHANGE_RANGE)) { + const tile = getTile(this.map, x, y); + + if (tile === TileType.Dirt || tile === TileType.Grass) { + if (this.client.shadowed) { + pushUpdateTileToClient(this.client, x, y, type); + } else { + this.world.setTile(this.map, x, y, type); + } + } + } + } + } + @Method({ rateLimit: '1/s', binary: [] }) + leave() { + this.client.leaveReason = 'leave'; + this.client.left(LeaveReason.None); + } + @Method({ binary: [Bin.Obj] }) + editorAction(action: EditorAction) { + if (this.server.flags.objects && this.client.isMod) { + const added = editorAdded.get(this.client.accountId) || []; + editorAdded.set(this.client.accountId, added); + + switch (action.type) { + case 'place': + if (includes(allEntities, action.entity)) { + const name = action.entity; + const entity = (entities as any)[name](action.x, action.y); + const toAdd = Array.isArray(entity) ? entity : [entity]; + toAdd.forEach(e => this.world.addEntity(e, this.map)); + added.push({ name, entities: toAdd }); + } else { + saySystem(this.client, 'Invalid entity'); + } + break; + case 'move': + for (const { id, x, y } of action.entities) { + const entity = this.world.getEntityById(id); + + if (entity && entity.type !== PONY_TYPE) { + entity.x = x; + entity.y = y; + updateEntity(entity, false); + updateRegion(entity, this.client.map); + getExpectedRegion(entity, this.client.map).colliderDirty = true; + } + } + break; + case 'undo': + const remove = added.pop(); + remove && remove.entities.forEach(e => this.world.removeEntityFromSomeMap(e)); + break; + case 'clear': + added.forEach(x => x.entities.forEach(e => this.world.removeEntityFromSomeMap(e))); + added.length = 0; + break; + case 'list': + const existingEntities = added + .filter(({ entities }) => entities.some(e => !!this.world.getEntityById(e.id))) + .map(({ name, entities: [{ x, y }] }) => ({ name, x, y })); + this.client.entityList(existingEntities); + break; + case 'remove': + for (const id of action.entities) { + const entity = this.world.getEntityById(id); + + if (entity && entity.type !== PONY_TYPE) { + this.world.removeEntityFromSomeMap(entity); + } + } + break; + case 'tile': { + const { x, y, tile, size } = action; + + if (isValidModTile(tile)) { + for (let iy = 0; iy < size; iy++) { + for (let ix = 0; ix < size; ix++) { + this.world.setTile(this.map, x + ix, y + iy, tile); + } + } + } + + break; + } + case 'party': { + const entities = findEntities(this.map, e => !!e.client && /^debug/.test(e.name || '')); + + for (const e of entities.slice(0, PARTY_LIMIT - 1)) { + this.partyService.invite(this.client, e.client!); + } + + break; + } + default: + throw new Error(`Invalid editor action (${action})`); + } + } + } + private logModAction(client: IClient, title: string) { + client.reporter.system(`${title} by ${this.account.name}`); + } + private getClientForModAction(entityId: number, action: string) { + if (!this.client.isMod) { + this.client.disconnect(true, true); + throw new Error(`Action not allowed (${action})`); + } + + const client = this.world.getClientByEntityId(entityId); + + if (!client) { + throw new Error(`Client does not exist (${action})`); + } + + if (client.accountId === this.client.accountId) { + throw new Error(`Cannot perform action on self (${action})`); + } + + return client; + } + private getEntityFromClients(entityId: number) { + const client = this.getClientByEntityId(entityId); + return client && client.pony; + } + private getClientByEntityId(entityId: number) { + return this.world.getClientByEntityId(entityId) || this.findClientByEntityId(this.client, entityId); + } + private updateLastAction() { + this.client.lastPacket = Date.now(); + } } function validateNumber(value: number, fieldName: string) { - if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) { - throw new Error(`Not a number (${fieldName})`); - } + if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) { + throw new Error(`Not a number (${fieldName})`); + } } function validateString(value: string, fieldName: string, allowNull = false) { - if (typeof value !== 'string' && !(allowNull && value === null)) { - throw new Error(`Not a string (${fieldName})`); - } + if (typeof value !== 'string' && !(allowNull && value === null)) { + throw new Error(`Not a string (${fieldName})`); + } } if (DEVELOPMENT) { - /* istanbul ignore next */ - getMethods(ServerActions) - .forEach(m => m.options.binary || console.error(`Missing binary encoding for ServerActions.${m.name}()`)); + /* istanbul ignore next */ + getMethods(ServerActions) + .forEach(m => m.options.binary || console.error(`Missing binary encoding for ServerActions.${m.name}()`)); } diff --git a/src/ts/server/serverActionsManager.ts b/src/ts/server/serverActionsManager.ts index 2235cbe..a3df907 100644 --- a/src/ts/server/serverActionsManager.ts +++ b/src/ts/server/serverActionsManager.ts @@ -5,8 +5,8 @@ import { HOUR, SECOND, SEASON, HOLIDAY, UNHIDE_TIMEOUT, MINUTE } from '../common import { CharacterState, ServerConfig, Settings } from '../common/adminInterfaces'; import { ClientActions } from '../client/clientActions'; import { - updateAccountSafe, timeoutAccount, reportInviteLimitAccount, reportSwearingAccount, reportSpammingAccount, - reportFriendLimitAccount + updateAccountSafe, timeoutAccount, reportInviteLimitAccount, reportSwearingAccount, reportSpammingAccount, + reportFriendLimitAccount } from './api/admin-accounts'; import { ServerActions } from './serverActions'; import { IClient, AccountService, GetSettings, SocketStats, TokenData } from './serverInterfaces'; @@ -31,88 +31,88 @@ import { updateCharacterState } from './characterUtils'; import { FriendsService } from './services/friends'; async function refreshSettings(account: IAccount) { - const a = await Account.findOne({ _id: account._id }, 'settings').exec(); + const a = await Account.findOne({ _id: account._id }, 'settings').exec(); - if (a) { - account.settings = a.settings; - } + if (a) { + account.settings = a.settings; + } } export function createServerActionsFactory( - server: ServerConfig, settings: Settings, getSettings: GetSettings, socketStats: SocketStats + server: ServerConfig, settings: Settings, getSettings: GetSettings, socketStats: SocketStats ) { - const reportInviteLimitFunc = reportInviteLimit(reportInviteLimitAccount, `Party invite limit`); - const reportFriendLimitFunc = reportInviteLimit(reportFriendLimitAccount, `Friend request limit`); - const notifications = new NotificationService(); - const party = new PartyService(notifications, reportInviteLimitFunc); - const supporterInvites = new SupporterInvitesService(SupporterInvite, notifications, log); - const friends = new FriendsService(notifications, reportFriendLimitFunc); - let world: World; - const hiding = new HidingService(UNHIDE_TIMEOUT, notifications, accountId => findClientByAccountId(world, accountId), log); - world = new World(server, party, friends, hiding, notifications, getSettings, liveSettings, socketStats); - const spamCounter = new CounterService(2 * HOUR); - const rapidCounter = new CounterService(1 * MINUTE); - const swearsCounter = new CounterService(2 * HOUR); - const forbiddenCounter = new CounterService(4 * HOUR); - const suspiciousCounter = new CounterService(4 * HOUR); - const teleportCounter = new CounterService(1 * HOUR); - const statesCounter = new CounterService(10 * SECOND); - const logChatMessage: LogChat = (client, text, type, ignored, target) => chat(server, client, text, type, ignored, target); + const reportInviteLimitFunc = reportInviteLimit(reportInviteLimitAccount, `Party invite limit`); + const reportFriendLimitFunc = reportInviteLimit(reportFriendLimitAccount, `Friend request limit`); + const notifications = new NotificationService(); + const party = new PartyService(notifications, reportInviteLimitFunc); + const supporterInvites = new SupporterInvitesService(SupporterInvite, notifications, log); + const friends = new FriendsService(notifications, reportFriendLimitFunc); + let world: World; + const hiding = new HidingService(UNHIDE_TIMEOUT, notifications, accountId => findClientByAccountId(world, accountId), log); + world = new World(server, party, friends, hiding, notifications, getSettings, liveSettings, socketStats); + const spamCounter = new CounterService(2 * HOUR); + const rapidCounter = new CounterService(1 * MINUTE); + const swearsCounter = new CounterService(2 * HOUR); + const forbiddenCounter = new CounterService(4 * HOUR); + const suspiciousCounter = new CounterService(4 * HOUR); + const teleportCounter = new CounterService(1 * HOUR); + const statesCounter = new CounterService(10 * SECOND); + const logChatMessage: LogChat = (client, text, type, ignored, target) => chat(server, client, text, type, ignored, target); - world.season = SEASON; - world.holiday = HOLIDAY; + world.season = SEASON; + world.holiday = HOLIDAY; - try { - const hidingData = fs.readFileSync(hidingDataPath(server.id), 'utf8'); + try { + const hidingData = fs.readFileSync(hidingDataPath(server.id), 'utf8'); - if (hidingData) { - hiding.deserialize(hidingData); - } - } catch { } + if (hidingData) { + hiding.deserialize(hidingData); + } + } catch { } - pollHidingDataSave(hiding, server.id); + pollHidingDataSave(hiding, server.id); - hiding.changes.subscribe(({ by, who }) => world.notifyHidden(by, who)); - hiding.unhidesAll.subscribe(by => world.kickByAccount(by)); + hiding.changes.subscribe(({ by, who }) => world.notifyHidden(by, who)); + hiding.unhidesAll.subscribe(by => world.kickByAccount(by)); - hiding.start(); - spamCounter.start(); - swearsCounter.start(); - forbiddenCounter.start(); - suspiciousCounter.start(); + hiding.start(); + spamCounter.start(); + swearsCounter.start(); + forbiddenCounter.start(); + suspiciousCounter.start(); - const commands = createCommands(world); - const spamCommands = getSpamCommandNames(commands); - const runCommand = createRunCommand({ world, notifications, random, liveSettings, party }, commands); - const updateSettings = createUpdateSettings(findAccountSafe); - const accountService: AccountService = { - update: updateAccountSafe, - updateSettings: (account, settings) => updateSettings(account, settings).then(noop), - refreshSettings, - updateAccount, - updateCharacterState: (characterId, state) => updateCharacterState(characterId, server.id, state), - }; - const reportSwears = createReportSwears(swearsCounter, reportSwearingAccount, timeoutAccount); - const reportForbidden = createReportForbidden(forbiddenCounter, timeoutAccount); - const reportSuspicious = createReportSuspicious(suspiciousCounter); - const checkSpam = createSpamChecker(spamCounter, rapidCounter, reportSpammingAccount, timeoutAccount); - const isSuspiciousMessage = createIsSuspiciousMessage(settings); - const say = createSay( - world, runCommand, logChatMessage, checkSpam, reportSwears, reportForbidden, reportSuspicious, spamCommands, - Math.random, isSuspiciousMessage); - const move = createMove(teleportCounter); - const ignorePlayer = createIgnorePlayer(updateAccount); + const commands = createCommands(world); + const spamCommands = getSpamCommandNames(commands); + const runCommand = createRunCommand({ world, notifications, random, liveSettings, party }, commands); + const updateSettings = createUpdateSettings(findAccountSafe); + const accountService: AccountService = { + update: updateAccountSafe, + updateSettings: (account, settings) => updateSettings(account, settings).then(noop), + refreshSettings, + updateAccount, + updateCharacterState: (characterId, state) => updateCharacterState(characterId, server.id, state), + }; + const reportSwears = createReportSwears(swearsCounter, reportSwearingAccount, timeoutAccount); + const reportForbidden = createReportForbidden(forbiddenCounter, timeoutAccount); + const reportSuspicious = createReportSuspicious(suspiciousCounter); + const checkSpam = createSpamChecker(spamCounter, rapidCounter, reportSpammingAccount, timeoutAccount); + const isSuspiciousMessage = createIsSuspiciousMessage(settings); + const say = createSay( + world, runCommand, logChatMessage, checkSpam, reportSwears, reportForbidden, reportSuspicious, spamCommands, + Math.random, isSuspiciousMessage); + const move = createMove(teleportCounter); + const ignorePlayer = createIgnorePlayer(updateAccount); - async function createServerActions(client: ClientActions & SocketClient & ClientExtensions & IClient) { - const { account } = client.tokenData as TokenData; - const [friendIds, hideIds] = await Promise.all([findFriendIds(account._id), findHideIds(account._id)]); - createClientAndPony(client, friendIds, hideIds, server, world, statesCounter); + async function createServerActions(client: ClientActions & SocketClient & ClientExtensions & IClient) { + const { account } = client.tokenData as TokenData; + const [friendIds, hideIds] = await Promise.all([findFriendIds(account._id), findHideIds(account._id)]); + createClientAndPony(client, friendIds, hideIds, server, world, statesCounter); - return new ServerActions( - client, world, notifications, party, supporterInvites, getSettings, server, say, move, hiding, statesCounter, - accountService, ignorePlayer, findClientByEntityId, friends - ); - } + return new ServerActions( + client, world, notifications, party, supporterInvites, getSettings, server, say, move, hiding, statesCounter, + accountService, ignorePlayer, findClientByEntityId, friends + ); + } - return { world, hiding, createServerActions }; + return { world, hiding, createServerActions }; } diff --git a/src/ts/server/serverInterfaces.ts b/src/ts/server/serverInterfaces.ts index cadce85..2d9e1e2 100644 --- a/src/ts/server/serverInterfaces.ts +++ b/src/ts/server/serverInterfaces.ts @@ -1,295 +1,295 @@ import { ClientExtensions, BinaryWriter } from 'ag-sockets'; import { ClientActions } from '../client/clientActions'; import { - Entity, ServerFlags, AccountSettings, NotificationFlags, Expression, Camera, SayData, Region, TileUpdate, - Rect, IMap, MapType, TileType, MapState, UpdateFlags, Action, EntityOrPonyOptions, EntityPlayerState, MapFlags + Entity, ServerFlags, AccountSettings, NotificationFlags, Expression, Camera, SayData, Region, TileUpdate, + Rect, IMap, MapType, TileType, MapState, UpdateFlags, Action, EntityOrPonyOptions, EntityPlayerState, MapFlags } from '../common/interfaces'; import { IAccount, ICharacter, UpdateAccount } from './db'; import { AccountUpdate, CharacterState, GameServerSettings, Suspicious } from '../common/adminInterfaces'; export interface EntityUpdate { - entity: Entity; - flags: UpdateFlags; + entity: Entity; + flags: UpdateFlags; - // NOTE: need to use different position than current entity.x/y - // otherwise we get jump at the start of movement due to - // updated position after the frame - x: number; - y: number; - vx: number; - vy: number; + // NOTE: need to use different position than current entity.x/y + // otherwise we get jump at the start of movement due to + // updated position after the frame + x: number; + y: number; + vx: number; + vy: number; - action: Action; - playerState: EntityPlayerState; - options: EntityOrPonyOptions | undefined; + action: Action; + playerState: EntityPlayerState; + options: EntityOrPonyOptions | undefined; } export type EntityUpdateBase = Partial & { entity: ServerEntity; flags: UpdateFlags; }; export interface Reporter { - info(message: string, desc?: string): void; - warn(message: string, desc?: string): void; - warnLog(message: string): void; - danger(message: string, desc?: string): void; - error(error: Error, desc?: string): void; - system(message: string, desc?: string, logEvent?: boolean): void; - systemLog(message: string): void; - setPony(pony: any): void; + info(message: string, desc?: string): void; + warn(message: string, desc?: string): void; + warnLog(message: string): void; + danger(message: string, desc?: string): void; + error(error: Error, desc?: string): void; + system(message: string, desc?: string, logEvent?: boolean): void; + systemLog(message: string): void; + setPony(pony: any): void; } export interface ServerNotification { - id: number; - name: string; - message: string; - note?: string; - flags?: NotificationFlags; - entityId?: number; - accept?(): void; - reject?(): void; - sender?: IClient; + id: number; + name: string; + message: string; + note?: string; + flags?: NotificationFlags; + entityId?: number; + accept?(): void; + reject?(): void; + sender?: IClient; } export interface ServerParty { - id: string; - leader: IClient; - leaderTimeout?: any; - clients: IClient[]; - pending: { client: IClient; notificationId: number; }[]; - cleanup?: number; + id: string; + leader: IClient; + leaderTimeout?: any; + clients: IClient[]; + pending: { client: IClient; notificationId: number; }[]; + cleanup?: number; } export interface TokenData { - accountId: string; - account: IAccount; - character: ICharacter; + accountId: string; + account: IAccount; + character: ICharacter; } export interface TokenService { - clearTokensForAccount(accountId: string): void; - clearTokensAll(): void; - createToken(token: TokenData): string; + clearTokensForAccount(accountId: string): void; + clearTokensAll(): void; + createToken(token: TokenData): string; } export interface LastSay { - message: string; - count: number; - age: number; + message: string; + count: number; + age: number; } export interface QueuedSay { - id: number; - type: number; - message: string; + id: number; + type: number; + message: string; } export interface ServerRegion extends Region { - tiles: Uint8Array; - entities: ServerEntity[]; - movables: ServerEntity[]; - colliders: ServerEntity[]; - // entityAdds: any[]; - reusedUpdates: number; - entityUpdates: EntityUpdate[]; - entityRemoves: number[]; - tileUpdates: TileUpdate[]; - clients: IClient[]; // subscribed clients - bounds: Readonly; - boundsWithBorder: Readonly; - subscribeBounds: Readonly; // screen space - unsubscribeBounds: Readonly; // screen space - tilesSnapshot: Uint8Array | undefined; - tilesTimeouts: Uint8Array | undefined; - encodedTiles: Uint8Array | undefined; + tiles: Uint8Array; + entities: ServerEntity[]; + movables: ServerEntity[]; + colliders: ServerEntity[]; + // entityAdds: any[]; + reusedUpdates: number; + entityUpdates: EntityUpdate[]; + entityRemoves: number[]; + tileUpdates: TileUpdate[]; + clients: IClient[]; // subscribed clients + bounds: Readonly; + boundsWithBorder: Readonly; + subscribeBounds: Readonly; // screen space + unsubscribeBounds: Readonly; // screen space + tilesSnapshot: Uint8Array | undefined; + tilesTimeouts: Uint8Array | undefined; + encodedTiles: Uint8Array | undefined; } export const enum MapUsage { - Public, - Party, + Public, + Party, } export interface ServerMap extends IMap { - id: string; - usage: MapUsage; - flags: MapFlags; - readonly type: MapType; - readonly width: number; - readonly height: number; - readonly regionsX: number; - readonly regionsY: number; - instance: string | undefined; - defaultTile: TileType; - state: MapState; - regions: ServerRegion[]; - spawnArea: Rect; - spawns: Map; - lockedTiles: Set; - lastUsed: number; - controllers: Controller[]; - dontUpdateTilesAndColliders: boolean; - tilesLocked: boolean; - editableEntityLimit: number; - editableArea?: Rect; - editingLocked: boolean; + id: string; + usage: MapUsage; + flags: MapFlags; + readonly type: MapType; + readonly width: number; + readonly height: number; + readonly regionsX: number; + readonly regionsY: number; + instance: string | undefined; + defaultTile: TileType; + state: MapState; + regions: ServerRegion[]; + spawnArea: Rect; + spawns: Map; + lockedTiles: Set; + lastUsed: number; + controllers: Controller[]; + dontUpdateTilesAndColliders: boolean; + tilesLocked: boolean; + editableEntityLimit: number; + editableArea?: Rect; + editingLocked: boolean; } export interface IClient extends ClientActions, ClientExtensions { - // origin info - ip: string; - country: string; - userAgent?: string; + // origin info + ip: string; + country: string; + userAgent?: string; - // browser info - incognito?: boolean; - supportsWasm?: boolean; - supportsLetAndConst?: boolean; + // browser info + incognito?: boolean; + supportsWasm?: boolean; + supportsLetAndConst?: boolean; - // quick access account fields - accountId: string; - accountName: string; - accountSettings: AccountSettings; - account: IAccount; - friends: Set; - friendsCRC: number | undefined; + // quick access account fields + accountId: string; + accountName: string; + accountSettings: AccountSettings; + account: IAccount; + friends: Set; + friendsCRC: number | undefined; - // quick access character fields - characterId: string; - characterName: string; - character: ICharacter; + // quick access character fields + characterId: string; + characterName: string; + character: ICharacter; - isMod: boolean; - shadowed: boolean; - supporterLevel: number; - pony: ServerEntity; - regions: ServerRegion[]; - map: ServerMap; - isSwitchingMap: boolean; - camera: Camera; - characterState: CharacterState; - offline?: boolean; - offlineAt?: Date; - selected?: ServerEntity; - reporter: Reporter; - notifications: ServerNotification[]; - party?: ServerParty; - ignores: Set; - hides: Set; - permaHides: Set; - lastSwap: number; - lastMapLoadOrSave: number; + isMod: boolean; + shadowed: boolean; + supporterLevel: number; + pony: ServerEntity; + regions: ServerRegion[]; + map: ServerMap; + isSwitchingMap: boolean; + camera: Camera; + characterState: CharacterState; + offline?: boolean; + offlineAt?: Date; + selected?: ServerEntity; + reporter: Reporter; + notifications: ServerNotification[]; + party?: ServerParty; + ignores: Set; + hides: Set; + permaHides: Set; + lastSwap: number; + lastMapLoadOrSave: number; - // last state - safeX: number; - safeY: number; - lastPacket: number; - lastAction: number; - lastBoopAction: number; - lastExpressionAction: number; - lastSays: LastSay[]; - lastX: number; - lastY: number; - lastTime: number; - lastVX: number; - lastVY: number; + // last state + safeX: number; + safeY: number; + lastPacket: number; + lastAction: number; + lastBoopAction: number; + lastExpressionAction: number; + lastSays: LastSay[]; + lastX: number; + lastY: number; + lastTime: number; + lastVX: number; + lastVY: number; - // sitting reporting - lastSitX: number; - lastSitY: number; - lastSitTime: number; - sitCount: number; + // sitting reporting + lastSitX: number; + lastSitY: number; + lastSitTime: number; + sitCount: number; - // subscription checking - lastCameraX: number; - lastCameraY: number; - lastCameraW: number; - lastCameraH: number; + // subscription checking + lastCameraX: number; + lastCameraY: number; + lastCameraW: number; + lastCameraH: number; - // flags - lastMapSwitch: number; - // queuedMapSwitch?: { map: ServerMap; x: number; y: number; }; - logDisconnect?: boolean; - loading?: boolean; - fixingPosition?: boolean; - connectedTime: number; - leaveReason?: string; + // flags + lastMapSwitch: number; + // queuedMapSwitch?: { map: ServerMap; x: number; y: number; }; + logDisconnect?: boolean; + loading?: boolean; + fixingPosition?: boolean; + connectedTime: number; + leaveReason?: string; - // pending data - updateQueue: BinaryWriter; - regionUpdates: Uint8Array[]; - saysQueue: SayData[]; - unsubscribes: number[]; - subscribes: Uint8Array[]; + // pending data + updateQueue: BinaryWriter; + regionUpdates: Uint8Array[]; + saysQueue: SayData[]; + unsubscribes: number[]; + subscribes: Uint8Array[]; - // error reporting - rateLimitMessage?: string; - rateLimitCount?: number; + // error reporting + rateLimitMessage?: string; + rateLimitCount?: number; - // debug - positions: { frame: number; x: number; y: number; moved: boolean; }[]; + // debug + positions: { frame: number; x: number; y: number; moved: boolean; }[]; } export type Interact = (target: ServerEntity, client: IClient) => void; export interface ServerEntity extends Entity { - // flags - serverFlags?: ServerFlags; - canFly?: boolean; - canMagic?: boolean; + // flags + serverFlags?: ServerFlags; + canFly?: boolean; + canMagic?: boolean; - // state - region?: ServerRegion; - client?: IClient; + // state + region?: ServerRegion; + client?: IClient; - // interaction - interact?: Interact; + // interaction + interact?: Interact; - // trigger - trigger?: Interact; + // trigger + trigger?: Interact; - // boop - boop?(client: IClient): void; - boopX?: number; - boopY?: number; + // boop + boop?(client: IClient): void; + boopX?: number; + boopY?: number; - // expression - exprTimeout?: number; - exprCancellable?: boolean; - exprPermanent?: Expression; + // expression + exprTimeout?: number; + exprCancellable?: boolean; + exprPermanent?: Expression; - // other - lightDelay?: number; + // other + lightDelay?: number; - // cached info & name - nameBad?: boolean; - encodedName?: Uint8Array; - info?: string; - infoSafe?: string; - encryptedInfoSafe?: Uint8Array; + // cached info & name + nameBad?: boolean; + encodedName?: Uint8Array; + info?: string; + infoSafe?: string; + encryptedInfoSafe?: Uint8Array; - // update - serverUpdate?: (delte: number, now: number) => void; + // update + serverUpdate?: (delte: number, now: number) => void; } export interface ServerEntityWithClient extends ServerEntity { - client: IClient; + client: IClient; } export interface Controller { - initialize(now: number): void; - update(delta: number, now: number): void; - sparseUpdate?(): void; - toggleWall?(x: number, y: number, type: TileType): void; + initialize(now: number): void; + update(delta: number, now: number): void; + sparseUpdate?(): void; + toggleWall?(x: number, y: number, type: TileType): void; } export interface AccountService { - update(accountId: string, update: AccountUpdate): Promise; - updateSettings(account: IAccount, settings: AccountSettings): Promise; - refreshSettings(account: IAccount): Promise; - updateAccount: UpdateAccount; - updateCharacterState(characterId: string, state: CharacterState): Promise; + update(accountId: string, update: AccountUpdate): Promise; + updateSettings(account: IAccount, settings: AccountSettings): Promise; + refreshSettings(account: IAccount): Promise; + updateAccount: UpdateAccount; + updateCharacterState(characterId: string, state: CharacterState): Promise; } export interface SocketStats { - stats(): { sent: number; sentPackets: number; received: number; receivedPackets: number; }; + stats(): { sent: number; sentPackets: number; received: number; receivedPackets: number; }; } export type OnMessage = (client: IClient, message: string) => void; diff --git a/src/ts/server/serverMap.ts b/src/ts/server/serverMap.ts index 560c84b..d2a7fc5 100644 --- a/src/ts/server/serverMap.ts +++ b/src/ts/server/serverMap.ts @@ -1,7 +1,7 @@ import { writeFileAsync, readFileAsync, writeFileSync } from 'fs'; import { fromByteArray } from 'base64-js'; import { - TileType, MapInfo, MapState, defaultMapState, Rect, MapType, ServerFlags, EntityFlags, MapFlags, EntityState + TileType, MapInfo, MapState, defaultMapState, Rect, MapType, ServerFlags, EntityFlags, MapFlags, EntityState } from '../common/interfaces'; import { getRegionGlobal, getTile, getRegion } from '../common/worldMap'; import { distanceSquaredXY, containsPoint, hasFlag } from '../common/utils'; @@ -21,509 +21,509 @@ import { setEntityName } from './entityUtils'; import { WallController } from './controllers/wallController'; export interface EntityData { - type: string; - x: number; - y: number; - options?: any; - name?: string; + type: string; + x: number; + y: number; + options?: any; + name?: string; } export interface MapData { - width: number; - height: number; - tiles?: string; - entities?: EntityData[]; - walls?: string; + width: number; + height: number; + tiles?: string; + entities?: EntityData[]; + walls?: string; } export interface MapLoadOptions { - offsetX?: number; - offsetY?: number; - loadOnlyTiles?: boolean; - loadEntities?: boolean; - loadEntitiesAsEditable?: boolean; - loadWalls?: boolean; + offsetX?: number; + offsetY?: number; + loadOnlyTiles?: boolean; + loadEntities?: boolean; + loadEntitiesAsEditable?: boolean; + loadWalls?: boolean; } export interface MapSaveOptions { - saveTiles?: boolean; - saveEntities?: boolean; - saveOnlyEditableEntities?: boolean; - saveWalls?: boolean; + saveTiles?: boolean; + saveEntities?: boolean; + saveOnlyEditableEntities?: boolean; + saveWalls?: boolean; } export function createServerMap( - id: string, type: MapType, regionsX: number, regionsY: number, defaultTile = TileType.None, usage = MapUsage.Public, - initRegions = true + id: string, type: MapType, regionsX: number, regionsY: number, defaultTile = TileType.None, usage = MapUsage.Public, + initRegions = true ): ServerMap { - const width = regionsX * REGION_SIZE; - const height = regionsY * REGION_SIZE; - const regions: ServerRegion[] = []; - const state: MapState = { ...defaultMapState }; - const spawnArea = rect(0, 0, 1, 1); - const lockedTiles = new Set(); + const width = regionsX * REGION_SIZE; + const height = regionsY * REGION_SIZE; + const regions: ServerRegion[] = []; + const state: MapState = { ...defaultMapState }; + const spawnArea = rect(0, 0, 1, 1); + const lockedTiles = new Set(); - if (regionsX <= 0 || regionsY <= 0 || width > POSITION_MAX || height > POSITION_MAX) { - throw new Error('Invalid map parameters'); - } + if (regionsX <= 0 || regionsY <= 0 || width > POSITION_MAX || height > POSITION_MAX) { + throw new Error('Invalid map parameters'); + } - if (initRegions) { - for (let ry = 0; ry < regionsY; ry++) { - for (let rx = 0; rx < regionsX; rx++) { - regions.push(createServerRegion(rx, ry, defaultTile)); - } - } - } + if (initRegions) { + for (let ry = 0; ry < regionsY; ry++) { + for (let rx = 0; rx < regionsX; rx++) { + regions.push(createServerRegion(rx, ry, defaultTile)); + } + } + } - return { - id, usage, type, flags: MapFlags.None, width, height, state, regions, regionsX, regionsY, defaultTile, spawnArea, - lockedTiles, spawns: new Map(), instance: undefined, lastUsed: Date.now(), controllers: [], - dontUpdateTilesAndColliders: false, tilesLocked: false, editableEntityLimit: 0, editingLocked: false, - }; + return { + id, usage, type, flags: MapFlags.None, width, height, state, regions, regionsX, regionsY, defaultTile, spawnArea, + lockedTiles, spawns: new Map(), instance: undefined, lastUsed: Date.now(), controllers: [], + dontUpdateTilesAndColliders: false, tilesLocked: false, editableEntityLimit: 0, editingLocked: false, + }; } export function serverMapInstanceFromTemplate(map: ServerMap): ServerMap { - const { - id, usage, type, flags, width, height, state, regionsX, regionsY, defaultTile, spawnArea, lockedTiles, - editableEntityLimit - } = map; + const { + id, usage, type, flags, width, height, state, regionsX, regionsY, defaultTile, spawnArea, lockedTiles, + editableEntityLimit + } = map; - return { - id, usage, type, flags, width, height, state: { ...state }, - regions: map.regions.map(cloneServerRegion), - regionsX, regionsY, defaultTile, spawnArea, - lockedTiles, spawns: new Map(), instance: undefined, lastUsed: Date.now(), controllers: [], - dontUpdateTilesAndColliders: true, tilesLocked: map.tilesLocked, - editableEntityLimit, editingLocked: false, - }; + return { + id, usage, type, flags, width, height, state: { ...state }, + regions: map.regions.map(cloneServerRegion), + regionsX, regionsY, defaultTile, spawnArea, + lockedTiles, spawns: new Map(), instance: undefined, lastUsed: Date.now(), controllers: [], + dontUpdateTilesAndColliders: true, tilesLocked: map.tilesLocked, + editableEntityLimit, editingLocked: false, + }; } export function copyMapTiles(target: ServerMap, source: ServerMap) { - for (let i = 0; i < target.regions.length; i++) { - const srcRegion = source.regions[i]; - const tgtRegion = target.regions[i]; - tgtRegion.tiles.set(srcRegion.tiles); - tgtRegion.tileIndices.set(srcRegion.tileIndices); - tgtRegion.encodedTiles = srcRegion.encodedTiles; - tgtRegion.colliderDirty = true; - } + for (let i = 0; i < target.regions.length; i++) { + const srcRegion = source.regions[i]; + const tgtRegion = target.regions[i]; + tgtRegion.tiles.set(srcRegion.tiles); + tgtRegion.tileIndices.set(srcRegion.tileIndices); + tgtRegion.encodedTiles = srcRegion.encodedTiles; + tgtRegion.colliderDirty = true; + } } export function getMapInfo(map: ServerMap): MapInfo { - return { - type: map.type, - flags: map.flags, - regionsX: map.regionsX, - regionsY: map.regionsY, - defaultTile: map.defaultTile, - editableArea: map.editableArea, - }; + return { + type: map.type, + flags: map.flags, + regionsX: map.regionsX, + regionsY: map.regionsY, + defaultTile: map.defaultTile, + editableArea: map.editableArea, + }; } export function getSizeOfMap(map: ServerMap) { - const memory = map.regions.reduce((sum, r) => sum + getSizeOfRegion(r), 0); - const entities = map.regions.reduce((sum, r) => sum + r.entities.length, 0); - return { memory, entities }; + const memory = map.regions.reduce((sum, r) => sum + getSizeOfRegion(r), 0); + const entities = map.regions.reduce((sum, r) => sum + r.entities.length, 0); + return { memory, entities }; } export function setTile(map: ServerMap, x: number, y: number, type: TileType) { - const region = getRegionGlobal(map, x, y); + 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; - setRegionTile(map, region, regionX, regionY, type); - } + if (region) { + const regionX = Math.floor(x) - region.x * REGION_SIZE; + const regionY = Math.floor(y) - region.y * REGION_SIZE; + setRegionTile(map, region, regionX, regionY, type); + } } export function snapshotTiles(map: ServerMap) { - for (const region of map.regions) { - snapshotRegionTiles(region); - } + for (const region of map.regions) { + snapshotRegionTiles(region); + } } export function lockTile(map: ServerMap, x: number, y: number) { - const index = ((x | 0) + (y | 0) * map.width) | 0; - map.lockedTiles.add(index); + const index = ((x | 0) + (y | 0) * map.width) | 0; + map.lockedTiles.add(index); } export function lockTiles(map: ServerMap, x: number, y: number, w: number, h: number) { - for (let iy = 0; iy < h; iy++) { - for (let ix = 0; ix < w; ix++) { - lockTile(map, x + ix, y + iy); - } - } + for (let iy = 0; iy < h; iy++) { + for (let ix = 0; ix < w; ix++) { + lockTile(map, x + ix, y + iy); + } + } } export function isTileLocked(map: ServerMap, x: number, y: number) { - const index = ((x | 0) + (y | 0) * map.width) | 0; - return map.lockedTiles.has(index); + const index = ((x | 0) + (y | 0) * map.width) | 0; + return map.lockedTiles.has(index); } export function serializeTiles(map: ServerMap) { - const tilesData: number[] = []; - const data: number[] = []; - const { width, height } = map; + const tilesData: number[] = []; + const data: number[] = []; + const { width, height } = map; - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - tilesData.push(getTile(map, x, y)); - } - } + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + tilesData.push(getTile(map, x, y)); + } + } - for (let i = 0; i < tilesData.length; i++) { - const tile = tilesData[i]; - let count = 1; + for (let i = 0; i < tilesData.length; i++) { + const tile = tilesData[i]; + let count = 1; - while (tilesData.length > (i + 1) && tilesData[i + 1] === tile && count < 255) { - count++; - i++; - } + while (tilesData.length > (i + 1) && tilesData[i + 1] === tile && count < 255) { + count++; + i++; + } - data.push(count, tile); - } + data.push(count, tile); + } - return new Uint8Array(data); + return new Uint8Array(data); } export function serializeMap(map: ServerMap): MapData { - const { width, height } = map; - const tiles = fromByteArray(serializeTiles(map)); - return { width, height, tiles }; + const { width, height } = map; + const tiles = fromByteArray(serializeTiles(map)); + return { width, height, tiles }; } export function deserializeMap(map: ServerMap, { tiles, width }: MapData, { offsetX = 0, offsetY = 0 }: MapLoadOptions = {}) { - const decodedTiles = deserializeTiles(tiles!); + const decodedTiles = deserializeTiles(tiles!); - for (let i = 0; i < decodedTiles.length; i++) { - const x = i % width; - const y = Math.floor(i / width); - setTile(map, x + offsetX, y + offsetY, decodedTiles[i]); - } + for (let i = 0; i < decodedTiles.length; i++) { + const x = i % width; + const y = Math.floor(i / width); + setTile(map, x + offsetX, y + offsetY, decodedTiles[i]); + } } export function saveMap(map: ServerMap, saveOptions: MapSaveOptions): MapData { - const data: MapData = { width: map.width, height: map.height }; + const data: MapData = { width: map.width, height: map.height }; - if (saveOptions.saveTiles) { - data.tiles = serializeMap(map).tiles; - } + if (saveOptions.saveTiles) { + data.tiles = serializeMap(map).tiles; + } - if (saveOptions.saveEntities) { - data.entities = []; + if (saveOptions.saveEntities) { + data.entities = []; - for (const region of map.regions) { - for (const entity of region.entities) { - if (!hasFlag(entity.serverFlags, ServerFlags.DoNotSave) && !hasFlag(entity.flags, EntityFlags.Debug)) { - if (saveOptions.saveOnlyEditableEntities && !hasFlag(entity.state, EntityState.Editable)) - continue; + for (const region of map.regions) { + for (const entity of region.entities) { + if (!hasFlag(entity.serverFlags, ServerFlags.DoNotSave) && !hasFlag(entity.flags, EntityFlags.Debug)) { + if (saveOptions.saveOnlyEditableEntities && !hasFlag(entity.state, EntityState.Editable)) + continue; - const options = entity.options && Object.keys(entity.options).length > 0 ? entity.options : undefined; - const name = entity.name; - data.entities.push({ type: getEntityTypeName(entity.type), x: entity.x, y: entity.y, options, name }); - } - } - } - } + const options = entity.options && Object.keys(entity.options).length > 0 ? entity.options : undefined; + const name = entity.name; + data.entities.push({ type: getEntityTypeName(entity.type), x: entity.x, y: entity.y, options, name }); + } + } + } + } - if (saveOptions.saveWalls) { - const controller = map.controllers.find(c => c.toggleWall) as WallController | undefined; + if (saveOptions.saveWalls) { + const controller = map.controllers.find(c => c.toggleWall) as WallController | undefined; - if (controller) { - data.walls = controller.serialize(); - } - } + if (controller) { + data.walls = controller.serialize(); + } + } - return data; + return data; } export async function saveMapToFile(map: ServerMap, fileName: string, options: MapSaveOptions) { - const data = saveMap(map, options); - const json = JSON.stringify(data, null, 2); - await writeFileAsync(fileName, json, 'utf8'); + const data = saveMap(map, options); + const json = JSON.stringify(data, null, 2); + await writeFileAsync(fileName, json, 'utf8'); } export async function saveMapToFileBinary(map: ServerMap, fileName: string) { - const tiles = serializeTiles(map); - const buffer = new Uint8Array(4 + 4 + tiles.byteLength); - const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); - view.setInt32(0, map.width, true); - view.setInt32(4, map.height, true); - buffer.set(tiles, 8); - await writeFileAsync(fileName, Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength)); + const tiles = serializeTiles(map); + const buffer = new Uint8Array(4 + 4 + tiles.byteLength); + const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); + view.setInt32(0, map.width, true); + view.setInt32(4, map.height, true); + buffer.set(tiles, 8); + await writeFileAsync(fileName, Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength)); } export async function saveMapToFileBinaryAlt(map: ServerMap, fileName: string) { - const buffer = Buffer.alloc(4 + 4 + map.width * map.height); - buffer.writeUInt32LE(map.width, 0); - buffer.writeUInt32LE(map.height, 4); + const buffer = Buffer.alloc(4 + 4 + map.width * map.height); + buffer.writeUInt32LE(map.width, 0); + buffer.writeUInt32LE(map.height, 4); - for (let y = 0, i = 8; y < map.height; y++) { - for (let x = 0; x < map.width; x++ , i++) { - buffer.writeUInt8(getTile(map, x, y), i); - } - } + for (let y = 0, i = 8; y < map.height; y++) { + for (let x = 0; x < map.width; x++ , i++) { + buffer.writeUInt8(getTile(map, x, y), i); + } + } - await writeFileAsync(fileName, buffer); + await writeFileAsync(fileName, buffer); } export async function saveEntitiesToFile(map: ServerMap, fileName: string) { - const lines: string[] = []; + const lines: string[] = []; - for (const region of map.regions) { - for (const entity of region.entities) { - lines.push(`${getEntityTypeName(entity.type)} ${entity.x} ${entity.y}`); - } - } + for (const region of map.regions) { + for (const entity of region.entities) { + lines.push(`${getEntityTypeName(entity.type)} ${entity.x} ${entity.y}`); + } + } - await writeFileAsync(fileName, lines.join('\n'), 'utf8'); + await writeFileAsync(fileName, lines.join('\n'), 'utf8'); } export function loadMap(world: World, map: ServerMap, data: MapData, loadOptions: MapLoadOptions) { - if (data.tiles) { - deserializeMap(map, data, loadOptions); - } + if (data.tiles) { + deserializeMap(map, data, loadOptions); + } - if (loadOptions.loadOnlyTiles) - return; + if (loadOptions.loadOnlyTiles) + return; - if (loadOptions.loadEntitiesAsEditable) { - const entitiesToRemove: ServerEntity[] = []; + if (loadOptions.loadEntitiesAsEditable) { + const entitiesToRemove: ServerEntity[] = []; - for (const region of map.regions) { - for (const entity of region.entities) { - if (hasFlag(entity.state, EntityState.Editable)) { - entitiesToRemove.push(entity); - } - } - } + for (const region of map.regions) { + for (const entity of region.entities) { + if (hasFlag(entity.state, EntityState.Editable)) { + entitiesToRemove.push(entity); + } + } + } - for (const entity of entitiesToRemove) { - world.removeEntity(entity, map); - } - } + for (const entity of entitiesToRemove) { + world.removeEntity(entity, map); + } + } - if (loadOptions.loadEntities && data.entities) { - for (const { x, y, type, name, options } of data.entities) { - const typeNumber = getEntityType(type); - const entity = createAnEntity(typeNumber, 0, x, y, options, mockPaletteManager, world); + if (loadOptions.loadEntities && data.entities) { + for (const { x, y, type, name, options } of data.entities) { + const typeNumber = getEntityType(type); + const entity = createAnEntity(typeNumber, 0, x, y, options, mockPaletteManager, world); - if (name) { - setEntityName(entity, name); - } + if (name) { + setEntityName(entity, name); + } - if (loadOptions.loadEntitiesAsEditable) { - entity.state |= EntityState.Editable; - } + if (loadOptions.loadEntitiesAsEditable) { + entity.state |= EntityState.Editable; + } - world.addEntity(entity, map); - } - } + world.addEntity(entity, map); + } + } - if (loadOptions.loadWalls && data.walls) { - const controller = map.controllers.find(c => c.toggleWall) as WallController | undefined; + if (loadOptions.loadWalls && data.walls) { + const controller = map.controllers.find(c => c.toggleWall) as WallController | undefined; - if (controller) { - controller.deserialize(data.width, data.height, data.walls); - } - } + if (controller) { + controller.deserialize(data.width, data.height, data.walls); + } + } } export async function loadMapFromFile(world: World, map: ServerMap, fileName: string, options: MapLoadOptions) { - const json = await readFileAsync(fileName, 'utf8'); - const data = JSON.parse(json); - loadMap(world, map, data, options); + const json = await readFileAsync(fileName, 'utf8'); + const data = JSON.parse(json); + loadMap(world, map, data, options); } export function saveRegionCollider(region: ServerRegion) { - const canvas = createCanvas(REGION_WIDTH, REGION_HEIGHT); - const context = canvas.getContext('2d')!; - context.fillStyle = 'white'; - context.fillRect(0, 0, canvas.width, canvas.height); + const canvas = createCanvas(REGION_WIDTH, REGION_HEIGHT); + const context = canvas.getContext('2d')!; + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); - context.fillStyle = '#eee'; + context.fillStyle = '#eee'; - for (let y = 0, i = 0; y < REGION_SIZE; y++ , i++) { - for (let x = 0; x < REGION_SIZE; x++ , i++) { - if ((i % 2) === 0) { - context.fillRect(x * tileWidth, y * tileHeight, tileWidth, tileHeight); - } - } - } + for (let y = 0, i = 0; y < REGION_SIZE; y++ , i++) { + for (let x = 0; x < REGION_SIZE; x++ , i++) { + if ((i % 2) === 0) { + context.fillRect(x * tileWidth, y * tileHeight, tileWidth, tileHeight); + } + } + } - context.globalAlpha = 0.8; - context.fillStyle = 'red'; + context.globalAlpha = 0.8; + context.fillStyle = 'red'; - for (let y = 0; y < REGION_HEIGHT; y++) { - for (let x = 0; x < REGION_WIDTH; x++) { - if (region.collider[x + y * REGION_WIDTH]) { - context.fillRect(x, y, 1, 1); - } - } - } + for (let y = 0; y < REGION_HEIGHT; y++) { + for (let x = 0; x < REGION_WIDTH; x++) { + if (region.collider[x + y * REGION_WIDTH]) { + context.fillRect(x, y, 1, 1); + } + } + } - writeFileSync(pathTo('store', 'collider.png'), canvas.toBuffer()); + writeFileSync(pathTo('store', 'collider.png'), canvas.toBuffer()); } function distanceSquaredToRegion(x: number, y: number, region: ServerRegion) { - const left = region.x * REGION_SIZE; - const top = region.y * REGION_SIZE; - const right = left + REGION_SIZE; - const bottom = top + REGION_SIZE; - const dx = x < left ? (left - x) : (x > right ? (x - right) : 0); - const dy = y < left ? (top - y) : (y > bottom ? (y - bottom) : 0); - return dx * dx + dy * dy; + const left = region.x * REGION_SIZE; + const top = region.y * REGION_SIZE; + const right = left + REGION_SIZE; + const bottom = top + REGION_SIZE; + const dx = x < left ? (left - x) : (x > right ? (x - right) : 0); + const dy = y < left ? (top - y) : (y > bottom ? (y - bottom) : 0); + return dx * dx + dy * dy; } export function findClosestEntity( - map: ServerMap, originX: number, originY: number, predicate: (entity: ServerEntity) => boolean + map: ServerMap, originX: number, originY: number, predicate: (entity: ServerEntity) => boolean ): ServerEntity | undefined { - let minX = Math.floor(originX / REGION_SIZE); - let minY = Math.floor(originY / REGION_SIZE); - let maxX = minX; - let maxY = minY; + let minX = Math.floor(originX / REGION_SIZE); + let minY = Math.floor(originY / REGION_SIZE); + let maxX = minX; + let maxY = minY; - let closest: ServerEntity | undefined = undefined; - let closestDist = Number.MAX_VALUE; + let closest: ServerEntity | undefined = undefined; + let closestDist = Number.MAX_VALUE; - while (minX >= 0 || minY >= 0 || maxX < map.regionsX || maxY < map.regionsY) { - let regionsChecked = 0; + while (minX >= 0 || minY >= 0 || maxX < map.regionsX || maxY < map.regionsY) { + let regionsChecked = 0; - for (let y = minY; y <= maxY; y++) { - for (let x = minX; x <= maxX; x = (x === maxX || y === minY || y === maxY) ? x + 1 : maxX) { - if (x >= 0 && y >= 0 && x < map.regionsX && y < map.regionsY) { - const region = getRegion(map, x, y); + for (let y = minY; y <= maxY; y++) { + for (let x = minX; x <= maxX; x = (x === maxX || y === minY || y === maxY) ? x + 1 : maxX) { + if (x >= 0 && y >= 0 && x < map.regionsX && y < map.regionsY) { + const region = getRegion(map, x, y); - if (distanceSquaredToRegion(originX, originY, region) < closestDist) { - regionsChecked++; + if (distanceSquaredToRegion(originX, originY, region) < closestDist) { + regionsChecked++; - for (const entity of region.entities) { - if (predicate(entity)) { - const dist = distanceSquaredXY(originX, originY, entity.x, entity.y); + for (const entity of region.entities) { + if (predicate(entity)) { + const dist = distanceSquaredXY(originX, originY, entity.x, entity.y); - if (dist < closestDist) { - closest = entity; - closestDist = dist; - } - } - } - } - } - } - } + if (dist < closestDist) { + closest = entity; + closestDist = dist; + } + } + } + } + } + } + } - if (!regionsChecked) { - break; - } + if (!regionsChecked) { + break; + } - minX -= 1; - minY -= 1; - maxX += 1; - maxY += 1; - } + minX -= 1; + minY -= 1; + maxX += 1; + maxY += 1; + } - return closest; + return closest; } export function findEntities(map: ServerMap, predicate: (entity: ServerEntity) => boolean): ServerEntity[] { - const entities: ServerEntity[] = []; + const entities: ServerEntity[] = []; - for (const region of map.regions) { - for (const entity of region.entities) { - if (predicate(entity)) { - entities.push(entity); - } - } - } + for (const region of map.regions) { + for (const entity of region.entities) { + if (predicate(entity)) { + entities.push(entity); + } + } + } - return entities; + return entities; } // TODO: maybe only regions in bounds, instead of adding 1 region border ? function forEachRegionInBounds(map: ServerMap, bounds: Rect, callback: (region: ServerRegion) => void) { - const minX = Math.max(0, Math.floor(bounds.x / REGION_SIZE) - 1) | 0; - const minY = Math.max(0, Math.floor(bounds.y / REGION_SIZE) - 1) | 0; - const maxX = Math.min(Math.floor((bounds.x + bounds.w) / REGION_SIZE) + 1, map.regionsX - 1) | 0; - const maxY = Math.min(Math.floor((bounds.y + bounds.h) / REGION_SIZE) + 1, map.regionsY - 1) | 0; + const minX = Math.max(0, Math.floor(bounds.x / REGION_SIZE) - 1) | 0; + const minY = Math.max(0, Math.floor(bounds.y / REGION_SIZE) - 1) | 0; + const maxX = Math.min(Math.floor((bounds.x + bounds.w) / REGION_SIZE) + 1, map.regionsX - 1) | 0; + const maxY = Math.min(Math.floor((bounds.y + bounds.h) / REGION_SIZE) + 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); - callback(region); - } - } + for (let ry = minY; ry <= maxY; ry++) { + for (let rx = minX; rx <= maxX; rx++) { + const region = getRegion(map, rx, ry); + callback(region); + } + } } export function findEntitiesInBounds(map: ServerMap, bounds: Rect) { - const result: ServerEntity[] = []; + const result: ServerEntity[] = []; - forEachRegionInBounds(map, bounds, region => { - for (const entity of region.entities) { - if (containsPoint(0, 0, bounds, entity.x, entity.y)) { - result.push(entity); - } - } - }); + forEachRegionInBounds(map, bounds, region => { + for (const entity of region.entities) { + if (containsPoint(0, 0, bounds, entity.x, entity.y)) { + result.push(entity); + } + } + }); - return result; + return result; } export function updateMapState(map: ServerMap, update: Partial) { - Object.assign(map.state, update); + Object.assign(map.state, update); - for (const region of map.regions) { - for (const client of region.clients) { - client.mapUpdate(map.state); - } - } + for (const region of map.regions) { + for (const client of region.clients) { + client.mapUpdate(map.state); + } + } } export function hasAnyClients(map: ServerMap) { - for (const region of map.regions) { - if (region.clients.length > 0) { - return true; - } - } + for (const region of map.regions) { + if (region.clients.length > 0) { + return true; + } + } - return false; + return false; } export function createMinimap(world: World, map: ServerMap) { - const { width, height } = map; - const buffer = new Uint32Array(width * height); + const { width, height } = map; + const buffer = new Uint32Array(width * height); - for (let y = 0; y < map.height; y++) { - for (let x = 0; x < map.width; x++) { - const tile = getTile(map, x, y); - buffer[x + y * width] = getTileColor(tile, world.season); - } - } + for (let y = 0; y < map.height; y++) { + for (let x = 0; x < map.width; x++) { + const tile = getTile(map, x, y); + buffer[x + y * width] = getTileColor(tile, world.season); + } + } - // map.entities = info.entities - // .map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager)); + // map.entities = info.entities + // .map(({ type, id, x, y }) => createAnEntity(type, id, x, y, {}, mockPaletteManager)); - // for (let i = 1; i <= 2; i++) { - // for (const e of map.entities) { - // if (e.minimap && e.minimap.order === i) { - // const { color, rect } = e.minimap; - // mapContext.fillStyle = colorToCSS(color); - // mapContext.fillRect(Math.round(e.x + rect.x), Math.round(e.y + rect.y), rect.w, rect.h); - // } - // } - // } + // for (let i = 1; i <= 2; i++) { + // for (const e of map.entities) { + // if (e.minimap && e.minimap.order === i) { + // const { color, rect } = e.minimap; + // mapContext.fillStyle = colorToCSS(color); + // mapContext.fillRect(Math.round(e.x + rect.x), Math.round(e.y + rect.y), rect.w, rect.h); + // } + // } + // } - // canvas.width = mapCanvas.width * scale; - // canvas.height = mapCanvas.height * scale; - // const context = canvas.getContext('2d')!; - // context.save(); + // canvas.width = mapCanvas.width * scale; + // canvas.height = mapCanvas.height * scale; + // const context = canvas.getContext('2d')!; + // context.save(); - // if (scale >= 1) { - // disableImageSmoothing(context); - // } + // if (scale >= 1) { + // disableImageSmoothing(context); + // } - // context.scale(scale, scale); - // context.drawImage(mapCanvas, 0, 0); - // context.restore(); + // context.scale(scale, scale); + // context.drawImage(mapCanvas, 0, 0); + // context.restore(); - return new Uint8Array(buffer.buffer); + return new Uint8Array(buffer.buffer); } diff --git a/src/ts/server/serverRegion.ts b/src/ts/server/serverRegion.ts index 19e24c5..c60239c 100644 --- a/src/ts/server/serverRegion.ts +++ b/src/ts/server/serverRegion.ts @@ -4,7 +4,7 @@ import { TileType, EntityFlags, UpdateFlags, canWalk } from '../common/interface import { compressTiles } from '../common/compress'; import { rect, withBorder, withPadding } from '../common/rect'; import { - REGION_BORDER, tileHeight, tileWidth, REGION_SIZE, TILES_RESTORE_MIN_SEC, TILES_RESTORE_MAX_SEC + REGION_BORDER, tileHeight, tileWidth, REGION_SIZE, TILES_RESTORE_MIN_SEC, TILES_RESTORE_MAX_SEC } from '../common/constants'; import { rectToScreen } from '../common/positionUtils'; import { removeItem, hasFlag } from '../common/utils'; @@ -17,226 +17,226 @@ const subscribeBoundsBottomPad = 3; const randoms = new Uint8Array(REGION_SIZE * REGION_SIZE); export function createServerRegion(x: number, y: number, defaultTile = TileType.Dirt): ServerRegion { - const bounds = rect(x * REGION_SIZE, y * REGION_SIZE, REGION_SIZE, REGION_SIZE); - const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); - const tileIndices = new Int16Array(REGION_SIZE * REGION_SIZE); - const collider = new Uint8Array(REGION_SIZE * REGION_SIZE * tileWidth * tileHeight); + const bounds = rect(x * REGION_SIZE, y * REGION_SIZE, REGION_SIZE, REGION_SIZE); + const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); + const tileIndices = new Int16Array(REGION_SIZE * REGION_SIZE); + const collider = new Uint8Array(REGION_SIZE * REGION_SIZE * tileWidth * tileHeight); - tileIndices.fill(-1); + tileIndices.fill(-1); - if (defaultTile !== 0) { - for (let i = 0; i < tiles.length; i++) { - tiles[i] = defaultTile; - } - } + if (defaultTile !== 0) { + for (let i = 0; i < tiles.length; i++) { + tiles[i] = defaultTile; + } + } - return { - x, y, - entityUpdates: [], - entityRemoves: [], - tileUpdates: [], - clients: [], - entities: [], - movables: [], - colliders: [], - collider, - colliderDirty: true, - randoms, - tiles, - tileIndices, - tilesDirty: true, - tilesSnapshot: undefined, - tilesTimeouts: undefined, - encodedTiles: undefined, - reusedUpdates: 0, - bounds, - boundsWithBorder: withBorder(bounds, REGION_BORDER), - subscribeBounds: rectToScreen(withPadding( - bounds, REGION_SIZE, REGION_SIZE, REGION_SIZE + subscribeBoundsBottomPad, REGION_SIZE)), - unsubscribeBounds: rectToScreen(withPadding( - bounds, REGION_SIZE + 1, REGION_SIZE + 1, REGION_SIZE + subscribeBoundsBottomPad + 1, REGION_SIZE + 1)), - }; + return { + x, y, + entityUpdates: [], + entityRemoves: [], + tileUpdates: [], + clients: [], + entities: [], + movables: [], + colliders: [], + collider, + colliderDirty: true, + randoms, + tiles, + tileIndices, + tilesDirty: true, + tilesSnapshot: undefined, + tilesTimeouts: undefined, + encodedTiles: undefined, + reusedUpdates: 0, + bounds, + boundsWithBorder: withBorder(bounds, REGION_BORDER), + subscribeBounds: rectToScreen(withPadding( + bounds, REGION_SIZE, REGION_SIZE, REGION_SIZE + subscribeBoundsBottomPad, REGION_SIZE)), + unsubscribeBounds: rectToScreen(withPadding( + bounds, REGION_SIZE + 1, REGION_SIZE + 1, REGION_SIZE + subscribeBoundsBottomPad + 1, REGION_SIZE + 1)), + }; } export function cloneServerRegion(region: ServerRegion): ServerRegion { - const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); - tiles.set(region.tiles); + const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); + tiles.set(region.tiles); - return { - x: region.x, - y: region.y, - entityUpdates: [], - entityRemoves: [], - tileUpdates: [], - clients: [], - entities: [], - movables: [], - colliders: [], - collider: region.collider, - colliderDirty: false, - randoms, - tiles, - tileIndices: region.tileIndices, - tilesDirty: false, - tilesSnapshot: undefined, - tilesTimeouts: undefined, - encodedTiles: region.encodedTiles, - reusedUpdates: 0, - bounds: region.bounds, - boundsWithBorder: region.boundsWithBorder, - subscribeBounds: region.subscribeBounds, - unsubscribeBounds: region.unsubscribeBounds, - }; + return { + x: region.x, + y: region.y, + entityUpdates: [], + entityRemoves: [], + tileUpdates: [], + clients: [], + entities: [], + movables: [], + colliders: [], + collider: region.collider, + colliderDirty: false, + randoms, + tiles, + tileIndices: region.tileIndices, + tilesDirty: false, + tilesSnapshot: undefined, + tilesTimeouts: undefined, + encodedTiles: region.encodedTiles, + reusedUpdates: 0, + bounds: region.bounds, + boundsWithBorder: region.boundsWithBorder, + subscribeBounds: region.subscribeBounds, + unsubscribeBounds: region.unsubscribeBounds, + }; } export function getSizeOfRegion(region: ServerRegion) { - let size = region.tiles.byteLength; - size += region.tileIndices.byteLength; - size += region.tilesSnapshot ? region.tilesSnapshot.byteLength : 0; - size += region.tilesTimeouts ? region.tilesTimeouts.byteLength : 0; - size += region.encodedTiles ? region.encodedTiles.byteLength : 0; - size += region.collider ? region.collider.byteLength : 0; - return size; + let size = region.tiles.byteLength; + size += region.tileIndices.byteLength; + size += region.tilesSnapshot ? region.tilesSnapshot.byteLength : 0; + size += region.tilesTimeouts ? region.tilesTimeouts.byteLength : 0; + size += region.encodedTiles ? region.encodedTiles.byteLength : 0; + size += region.collider ? region.collider.byteLength : 0; + return size; } export function addEntityToRegion(region: ServerRegion, entity: ServerEntity, map: ServerMap) { - region.entities.push(entity); + region.entities.push(entity); - if (canCollideWith(entity)) { - region.colliders.push(entity); - invalidateRegionsCollider(region, map); - } + if (canCollideWith(entity)) { + region.colliders.push(entity); + invalidateRegionsCollider(region, map); + } - if (hasFlag(entity.flags, EntityFlags.Movable)) { - region.movables.push(entity); - } + if (hasFlag(entity.flags, EntityFlags.Movable)) { + region.movables.push(entity); + } } export function removeEntityFromRegion(region: ServerRegion, entity: ServerEntity, map: ServerMap) { - const removed = removeItem(region.entities, entity); + const removed = removeItem(region.entities, entity); - if (canCollideWith(entity)) { - removeItem(region.colliders, entity); - invalidateRegionsCollider(region, map); - } + if (canCollideWith(entity)) { + removeItem(region.colliders, entity); + invalidateRegionsCollider(region, map); + } - removeItem(region.movables, entity); - return removed; + removeItem(region.movables, entity); + return removed; } export function pushUpdateEntityToRegion(region: ServerRegion, update: EntityUpdateBase) { - const index = findUpdate(region, update.entity); + const index = findUpdate(region, update.entity); - if (index === -1) { - region.entityUpdates.push({ x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined, ...update }); - } else { - region.reusedUpdates++; - const existing = region.entityUpdates[index]; - existing.flags |= update.flags; + if (index === -1) { + region.entityUpdates.push({ x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined, ...update }); + } else { + region.reusedUpdates++; + const existing = region.entityUpdates[index]; + existing.flags |= update.flags; - if (hasFlag(update.flags, UpdateFlags.Position)) { - const { x = 0, y = 0, vx = 0, vy = 0 } = update; - existing.x = x; - existing.y = y; - existing.vx = vx; - existing.vy = vy; - } + if (hasFlag(update.flags, UpdateFlags.Position)) { + const { x = 0, y = 0, vx = 0, vy = 0 } = update; + existing.x = x; + existing.y = y; + existing.vx = vx; + existing.vy = vy; + } - if (hasFlag(update.flags, UpdateFlags.Options)) { - existing.options = { ...existing.options, ...update.options }; - } + if (hasFlag(update.flags, UpdateFlags.Options)) { + existing.options = { ...existing.options, ...update.options }; + } - if (hasFlag(update.flags, UpdateFlags.PlayerState)) { - existing.playerState = update.playerState!; - } + if (hasFlag(update.flags, UpdateFlags.PlayerState)) { + existing.playerState = update.playerState!; + } - if (hasFlag(update.flags, UpdateFlags.Action)) { - existing.action = update.action!; - } - } + if (hasFlag(update.flags, UpdateFlags.Action)) { + existing.action = update.action!; + } + } } export function pushRemoveEntityToRegion(region: ServerRegion, entity: ServerEntity) { - region.entityRemoves.push(entity.id); + region.entityRemoves.push(entity.id); } export function setRegionTile(map: ServerMap, region: ServerRegion, x: number, y: number, type: TileType, skipRestore = false) { - const old = getRegionTile(region, x, y); + const old = getRegionTile(region, x, y); - if (type === old) - return; + if (type === old) + return; - const index = x | (y << 3); - region.tiles[index] = type; - region.tileUpdates.push({ x, y, type: type }); - region.encodedTiles = undefined; + const index = x | (y << 3); + region.tiles[index] = type; + region.tileUpdates.push({ x, y, type: type }); + region.encodedTiles = undefined; - if (region.tilesTimeouts && !skipRestore) { - region.tilesTimeouts[index] = random(TILES_RESTORE_MIN_SEC, TILES_RESTORE_MAX_SEC); - } + if (region.tilesTimeouts && !skipRestore) { + region.tilesTimeouts[index] = random(TILES_RESTORE_MIN_SEC, TILES_RESTORE_MAX_SEC); + } - if (canWalk(old) !== canWalk(type)) { - setTilesDirty(map, region.x * REGION_SIZE + x - 1, region.y * REGION_SIZE + y - 1, 3, 3); - setColliderDirty(map, region, x, y); - } + if (canWalk(old) !== canWalk(type)) { + setTilesDirty(map, region.x * REGION_SIZE + x - 1, region.y * REGION_SIZE + y - 1, 3, 3); + setColliderDirty(map, region, x, y); + } } export function resetRegionUpdates(region: ServerRegion) { - region.entityUpdates.length = 0; - region.entityRemoves.length = 0; - region.tileUpdates.length = 0; - region.reusedUpdates = 0; + region.entityUpdates.length = 0; + region.entityRemoves.length = 0; + region.tileUpdates.length = 0; + region.reusedUpdates = 0; } export function snapshotRegionTiles(region: ServerRegion) { - region.tilesSnapshot = region.tiles.slice(); - region.tilesTimeouts = new Uint8Array(region.tiles.length); + region.tilesSnapshot = region.tiles.slice(); + region.tilesTimeouts = new Uint8Array(region.tiles.length); } export function getRegionTiles(region: ServerRegion) { - if (region.encodedTiles === undefined) { - region.encodedTiles = compressTiles(region.tiles); - } + if (region.encodedTiles === undefined) { + region.encodedTiles = compressTiles(region.tiles); + } - return region.encodedTiles; + return region.encodedTiles; } export function resetTiles(map: ServerMap, region: ServerRegion) { - if (region.tilesSnapshot && region.tilesTimeouts) { - for (let i = 0; i < region.tilesTimeouts.length; i++) { - region.tilesTimeouts[i] = 0; + if (region.tilesSnapshot && region.tilesTimeouts) { + for (let i = 0; i < region.tilesTimeouts.length; i++) { + region.tilesTimeouts[i] = 0; - if (region.tiles[i] !== region.tilesSnapshot[i]) { - const x = i % REGION_SIZE; - const y = Math.floor(i / REGION_SIZE); - setRegionTile(map, region, x, y, region.tilesSnapshot[i], true); - } - } - } + if (region.tiles[i] !== region.tilesSnapshot[i]) { + const x = i % REGION_SIZE; + const y = Math.floor(i / REGION_SIZE); + setRegionTile(map, region, x, y, region.tilesSnapshot[i], true); + } + } + } } export function tickTilesRestoration(map: ServerMap, region: ServerRegion) { - if (region.tilesSnapshot && region.tilesTimeouts) { - for (let i = 0; i < region.tilesTimeouts.length; i++) { - if (region.tilesTimeouts[i] > 0) { - region.tilesTimeouts[i]--; + if (region.tilesSnapshot && region.tilesTimeouts) { + for (let i = 0; i < region.tilesTimeouts.length; i++) { + if (region.tilesTimeouts[i] > 0) { + region.tilesTimeouts[i]--; - if (region.tilesTimeouts[i] === 0 && region.tiles[i] !== region.tilesSnapshot[i]) { - const x = i % REGION_SIZE; - const y = Math.floor(i / REGION_SIZE); - setRegionTile(map, region, x, y, region.tilesSnapshot[i], true); - } - } - } - } + if (region.tilesTimeouts[i] === 0 && region.tiles[i] !== region.tilesSnapshot[i]) { + const x = i % REGION_SIZE; + const y = Math.floor(i / REGION_SIZE); + setRegionTile(map, region, x, y, region.tilesSnapshot[i], true); + } + } + } + } } function findUpdate({ entityUpdates }: ServerRegion, entity: ServerEntity) { - for (let i = 0; i < entityUpdates.length; i++) { - if (entityUpdates[i].entity === entity) { - return i; - } - } + for (let i = 0; i < entityUpdates.length; i++) { + if (entityUpdates[i].entity === entity) { + return i; + } + } - return -1; + return -1; } diff --git a/src/ts/server/serverUtils.ts b/src/ts/server/serverUtils.ts index d4dba09..716db98 100644 --- a/src/ts/server/serverUtils.ts +++ b/src/ts/server/serverUtils.ts @@ -11,37 +11,37 @@ import { hasFlag, cloneDeep, formatISODate } from '../common/utils'; import * as paths from './paths'; export function tokenService(socket: Server): TokenService { - return { - clearTokensForAccount(accountId: string) { - socket.clearTokens((_, data: TokenData) => data.accountId === accountId); - }, - clearTokensAll() { - socket.clearTokens(() => true); - }, - createToken(token: TokenData) { - return socket.token(token); - } - }; + return { + clearTokensForAccount(accountId: string) { + socket.clearTokens((_, data: TokenData) => data.accountId === accountId); + }, + clearTokensAll() { + socket.clearTokens(() => true); + }, + createToken(token: TokenData) { + return socket.token(token); + } + }; } export function isServerOffline(server: InternalGameServerState) { - return server.state.dead || !!server.state.settings.isServerOffline || !!server.state.shutdown; + return server.state.dead || !!server.state.settings.isServerOffline || !!server.state.shutdown; } export function toAccountData(account: IAccount): AccountData { - const { _id, name, birthdate, birthyear, characterCount, roles, settings, flags } = account; + const { _id, name, birthdate, birthyear, characterCount, roles, settings, flags } = account; - return { - id: _id.toString(), - name, characterCount, - birthdate: birthdate && formatISODate(birthdate) || '', - birthyear, - settings: cloneDeep(settings || {}), - supporter: supporterLevel(account) || undefined, - roles: (roles && roles.length) ? [...roles] : undefined, - flags: (hasFlag(flags, AccountFlags.DuplicatesNotification) ? AccountDataFlags.Duplicates : 0) | - (isPastSupporter(account) ? AccountDataFlags.PastSupporter : 0), - }; + return { + id: _id.toString(), + name, characterCount, + birthdate: birthdate && formatISODate(birthdate) || '', + birthyear, + settings: cloneDeep(settings || {}), + supporter: supporterLevel(account) || undefined, + roles: (roles && roles.length) ? [...roles] : undefined, + flags: (hasFlag(flags, AccountFlags.DuplicatesNotification) ? AccountDataFlags.Duplicates : 0) | + (isPastSupporter(account) ? AccountDataFlags.PastSupporter : 0), + }; } export const toPonyObjectFields = '_id name info desc site tag lastUsed flags'; @@ -49,97 +49,97 @@ export const toPonyObjectFields = '_id name info desc site tag lastUsed flags'; export function toPonyObject(character: ICharacter): PonyObject; export function toPonyObject(character: ICharacter | undefined): PonyObject | null; export function toPonyObject(character: ICharacter | undefined): PonyObject | null { - return character ? { - id: character._id.toString(), - name: character.name, - desc: character.desc || '', - info: character.info || '', - site: character.site ? character.site.toString() : undefined, - tag: character.tag || undefined, - lastUsed: character.lastUsed && character.lastUsed.toISOString(), - hideSupport: hasFlag(character.flags, CharacterFlags.HideSupport) ? true : undefined, - respawnAtSpawn: hasFlag(character.flags, CharacterFlags.RespawnAtSpawn) ? true : undefined, - } : null; + return character ? { + id: character._id.toString(), + name: character.name, + desc: character.desc || '', + info: character.info || '', + site: character.site ? character.site.toString() : undefined, + tag: character.tag || undefined, + lastUsed: character.lastUsed && character.lastUsed.toISOString(), + hideSupport: hasFlag(character.flags, CharacterFlags.HideSupport) ? true : undefined, + respawnAtSpawn: hasFlag(character.flags, CharacterFlags.RespawnAtSpawn) ? true : undefined, + } : null; } export function toPonyObjectAdmin(character: ICharacter): PonyObject; export function toPonyObjectAdmin(character: ICharacter | undefined): PonyObject | null; export function toPonyObjectAdmin(character: ICharacter | undefined): PonyObject | null { - return character ? { ...toPonyObject(character)!, creator: character.creator } : null; + return character ? { ...toPonyObject(character)!, creator: character.creator } : null; } export const toSocialSiteFields = '_id name provider url'; export function toSocialSite({ _id, name, provider, url }: IAuth): SocialSite { - return { id: _id.toString(), name, provider, url }; + return { id: _id.toString(), name, provider, url }; } /* istanbul ignore next */ export function execAsync(command: string, options?: ExecOptions) { - return new Promise<{ stdout: string; stderr: string; }>((resolve, reject) => { - exec(command, options || {}, (error, stdout, stderr) => { - if (error) { - reject(error); - } else { - resolve({ stdout, stderr }); - } - }); - }); + return new Promise<{ stdout: string; stderr: string; }>((resolve, reject) => { + exec(command, options || {}, (error, stdout, stderr) => { + if (error) { + reject(error); + } else { + resolve({ stdout, stderr }); + } + }); + }); } /* istanbul ignore next */ export async function logErrorToFile(message: string, data: any) { - const fileName = `error-${Date.now()}.json`; - const filePath = paths.pathTo('store', fileName); - await fs.writeFileAsync(filePath, JSON.stringify({ message, data }, null, 2), 'utf8'); - return fileName; + const fileName = `error-${Date.now()}.json`; + const filePath = paths.pathTo('store', fileName); + await fs.writeFileAsync(filePath, JSON.stringify({ message, data }, null, 2), 'utf8'); + return fileName; } /* istanbul ignore next */ export async function getDiskSpace() { - // NOTE: add your own code here - return ''; + // NOTE: add your own code here + return ''; } /* istanbul ignore next */ export async function getCertificateExpirationDate() { - // NOTE: add your own code here - return ''; + // NOTE: add your own code here + return ''; } /* istanbul ignore next */ export async function getMemoryUsage() { - // NOTE: add your own code here - return `0%`; + // NOTE: add your own code here + return `0%`; } /* istanbul ignore next */ export function handlePromiseDefault(promise: Promise, errorHandler: any = noop) { - Promise.resolve(promise).catch(errorHandler); + Promise.resolve(promise).catch(errorHandler); } export function cached(func: T, cacheTimeout = 1000): T & { clear(...args: any[]): void; } { - const cacheMap = new Map(); + const cacheMap = new Map(); - const cachedFunc: any = (...args: any[]) => { - const cacheKey = JSON.stringify(args); - const cache = cacheMap.get(cacheKey); + const cachedFunc: any = (...args: any[]) => { + const cacheKey = JSON.stringify(args); + const cache = cacheMap.get(cacheKey); - if (cache) { - clearTimeout(cache.timeout); - cache.timeout = setTimeout(() => cacheMap.delete(cacheKey), cacheTimeout); - return cache.result; - } else { - const result = func(...args); - const timeout = setTimeout(() => cacheMap.delete(cacheKey), cacheTimeout); - cacheMap.set(cacheKey, { result, timeout }); - return result; - } - }; + if (cache) { + clearTimeout(cache.timeout); + cache.timeout = setTimeout(() => cacheMap.delete(cacheKey), cacheTimeout); + return cache.result; + } else { + const result = func(...args); + const timeout = setTimeout(() => cacheMap.delete(cacheKey), cacheTimeout); + cacheMap.set(cacheKey, { result, timeout }); + return result; + } + }; - cachedFunc.clear = (...args: any[]) => { - cacheMap.delete(JSON.stringify(args)); - }; + cachedFunc.clear = (...args: any[]) => { + cacheMap.delete(JSON.stringify(args)); + }; - return cachedFunc; + return cachedFunc; } diff --git a/src/ts/server/services/actionLimiter.ts b/src/ts/server/services/actionLimiter.ts index 970432d..07c8dc0 100644 --- a/src/ts/server/services/actionLimiter.ts +++ b/src/ts/server/services/actionLimiter.ts @@ -3,42 +3,42 @@ import { IClient } from '../serverInterfaces'; import { isMutedOrShadowed, isIgnored } from '../playerUtils'; export const enum LimiterResult { - Yes = 0, - SameAccount = 1, - MutedOrShadowed = 2, - Ignored = 3, - LimitReached = 4, - TargetOffline = 5, + Yes = 0, + SameAccount = 1, + MutedOrShadowed = 2, + Ignored = 3, + LimitReached = 4, + TargetOffline = 5, } export class ActionLimiter { - private counters: CounterService; - constructor(clearTimeout: number, private countLimit: number) { - this.counters = new CounterService(clearTimeout); - this.counters.start(); - } - canExecute(requester: IClient, target: IClient): LimiterResult { - if (requester === target || requester.accountId === target.accountId) - return LimiterResult.SameAccount; + private counters: CounterService; + constructor(clearTimeout: number, private countLimit: number) { + this.counters = new CounterService(clearTimeout); + this.counters.start(); + } + canExecute(requester: IClient, target: IClient): LimiterResult { + if (requester === target || requester.accountId === target.accountId) + return LimiterResult.SameAccount; - if (target.offline) - return LimiterResult.TargetOffline; + if (target.offline) + return LimiterResult.TargetOffline; - if (isMutedOrShadowed(requester)) - return LimiterResult.MutedOrShadowed; + if (isMutedOrShadowed(requester)) + return LimiterResult.MutedOrShadowed; - if (isIgnored(requester, target) || isIgnored(target, requester)) - return LimiterResult.Ignored; + if (isIgnored(requester, target) || isIgnored(target, requester)) + return LimiterResult.Ignored; - if (this.counters.get(requester.accountId).count >= this.countLimit) - return LimiterResult.LimitReached; + if (this.counters.get(requester.accountId).count >= this.countLimit) + return LimiterResult.LimitReached; - return LimiterResult.Yes; - } - count(requester: IClient) { - return this.counters.add(requester.accountId).count; - } - dispose() { - this.counters.stop(); - } + return LimiterResult.Yes; + } + count(requester: IClient) { + return this.counters.add(requester.accountId).count; + } + dispose() { + this.counters.stop(); + } } diff --git a/src/ts/server/services/adminService.ts b/src/ts/server/services/adminService.ts index 90751f3..ae075d7 100644 --- a/src/ts/server/services/adminService.ts +++ b/src/ts/server/services/adminService.ts @@ -5,11 +5,11 @@ import * as db from '../db'; import { LiveList } from './liveList'; import { removeItem, includes, toInt, fromNow } from '../../common/utils'; import { - Account, Auth, Origin, OriginRef, OriginInfo, Character, ListListener, OriginInfoBase, Event, eventFields, PonyIdDateName + Account, Auth, Origin, OriginRef, OriginInfo, Character, ListListener, OriginInfoBase, Event, eventFields, PonyIdDateName } from '../../common/adminInterfaces'; import { - addToMap, removeFromMap, emailName, getIdsFromNote, compareAccounts, compareOriginRefs, compareByName, - compareAuths, createIdStore, getPotentialDuplicates, createPotentialDuplicatesFilter + addToMap, removeFromMap, emailName, getIdsFromNote, compareAccounts, compareOriginRefs, compareByName, + compareAuths, createIdStore, getPotentialDuplicates, createPotentialDuplicatesFilter } from '../../common/adminUtils'; import { logger, logPerformance } from '../logger'; import { ObservableList } from './observableList'; @@ -17,526 +17,526 @@ import { HOUR } from '../../common/constants'; import { getLoginServer } from '../internal'; function addAuthToAccount(account: Account, auth: Auth, log: string) { - const existingAuth = account.auths!.find(a => a._id === auth._id); + const existingAuth = account.auths!.find(a => a._id === auth._id); - if (existingAuth) { // TODO: remove - console.log('duplicate auth', auth._id, 'to', account._id, log); - } else { - account.authsList!.pushOrdered(auth, compareAuths); - } + if (existingAuth) { // TODO: remove + console.log('duplicate auth', auth._id, 'to', account._id, log); + } else { + account.authsList!.pushOrdered(auth, compareAuths); + } } function pushUnique(list: T[], item: T) { - if (list.indexOf(item) === -1) { - list.push(item); - } + if (list.indexOf(item) === -1) { + list.push(item); + } } function removeAuthFromAccount(account: Account, auth: Auth) { - return account.authsList!.remove(auth); + return account.authsList!.remove(auth); } function addPonyToAccount(account: Account, pony: Character) { - if (account.poniesList) { - account.poniesList.pushOrdered(pony, compareByName); - } + if (account.poniesList) { + account.poniesList.pushOrdered(pony, compareByName); + } } function removePonyFromAccount(account: Account, pony: Character) { - if (account.poniesList) { - return account.poniesList.remove(pony); - } else { - return false; - } + if (account.poniesList) { + return account.poniesList.remove(pony); + } else { + return false; + } } function getTotalPledged(auths: Auth[] | undefined) { - return Math.floor((auths || []).reduce((sum, a) => sum + toInt(a.pledged), 0) / 100); + return Math.floor((auths || []).reduce((sum, a) => sum + toInt(a.pledged), 0) / 100); } export class AdminService { - readonly accounts: LiveList; - readonly origins: LiveList; - readonly auths: LiveList; - readonly ponies: LiveList; - readonly events: LiveList; - readonly accountDeleted = new Subject(); - private emailMap = new Map(); - private noteRefMap = new Map(); - private browserIdMap = new Map(); - private unassignedAuths: Auth[] = []; - private unassignedPonies: Character[] = []; - constructor() { - const accountId = createIdStore(); + readonly accounts: LiveList; + readonly origins: LiveList; + readonly auths: LiveList; + readonly ponies: LiveList; + readonly events: LiveList; + readonly accountDeleted = new Subject(); + private emailMap = new Map(); + private noteRefMap = new Map(); + private browserIdMap = new Map(); + private unassignedAuths: Auth[] = []; + private unassignedPonies: Character[] = []; + constructor() { + const accountId = createIdStore(); - this.accounts = new LiveList(db.Account, { - fields: [ - '_id', 'updatedAt', 'createdAt', 'lastVisit', 'name', 'birthdate', 'origins', 'ignores', 'emails', 'note', - 'counters', 'mute', 'shadow', 'ban', 'flags', 'roles', 'characterCount', 'patreon', 'supporter', - 'supporterDeclinedSince', 'lastBrowserId', 'noteUpdated', 'alert', 'birthyear' - ], - clean: ({ - _id, createdAt, updatedAt, lastVisit, name, birthdate, origins, ignoresCount, emails, note, counters, mute, - shadow, ban, flags, roles, characterCount, patreon, supporter, supporterDeclinedSince, auths, noteUpdated, - alert, birthyear, - }) => - ({ - _id, createdAt, updatedAt, lastVisit, name, birthdate, origins, ignoresCount: toInt(ignoresCount), - emails, note, counters, mute, shadow, ban, flags: toInt(flags), roles, birthyear, - characterCount: toInt(characterCount), patreon: toInt(patreon), supporter: toInt(supporter), - supporterDeclinedSince, totalPledged: getTotalPledged(auths), noteUpdated, alert, - }), - fix: account => { - account._id = accountId(account._id); - account.nameLower = account.name.toLowerCase(); - account.ignoresCount = account.ignores ? account.ignores.length : 0; - account.ignores = undefined; - account.origins = (account.origins || []).map(o => ({ ip: o.ip, country: o.country, last: o.last })); - }, - onAdd: account => { - account.auths = []; - // account.ponies = []; - account.originsRefs = []; - account.authsList = new ObservableList(account.auths!, a => a._id); + this.accounts = new LiveList(db.Account, { + fields: [ + '_id', 'updatedAt', 'createdAt', 'lastVisit', 'name', 'birthdate', 'origins', 'ignores', 'emails', 'note', + 'counters', 'mute', 'shadow', 'ban', 'flags', 'roles', 'characterCount', 'patreon', 'supporter', + 'supporterDeclinedSince', 'lastBrowserId', 'noteUpdated', 'alert', 'birthyear' + ], + clean: ({ + _id, createdAt, updatedAt, lastVisit, name, birthdate, origins, ignoresCount, emails, note, counters, mute, + shadow, ban, flags, roles, characterCount, patreon, supporter, supporterDeclinedSince, auths, noteUpdated, + alert, birthyear, + }) => + ({ + _id, createdAt, updatedAt, lastVisit, name, birthdate, origins, ignoresCount: toInt(ignoresCount), + emails, note, counters, mute, shadow, ban, flags: toInt(flags), roles, birthyear, + characterCount: toInt(characterCount), patreon: toInt(patreon), supporter: toInt(supporter), + supporterDeclinedSince, totalPledged: getTotalPledged(auths), noteUpdated, alert, + }), + fix: account => { + account._id = accountId(account._id); + account.nameLower = account.name.toLowerCase(); + account.ignoresCount = account.ignores ? account.ignores.length : 0; + account.ignores = undefined; + account.origins = (account.origins || []).map(o => ({ ip: o.ip, country: o.country, last: o.last })); + }, + onAdd: account => { + account.auths = []; + // account.ponies = []; + account.originsRefs = []; + account.authsList = new ObservableList(account.auths!, a => a._id); - if (account.lastBrowserId) { - this.addBrowserIdToMap(account.lastBrowserId, account); - } + if (account.lastBrowserId) { + this.addBrowserIdToMap(account.lastBrowserId, account); + } - if (account.emails) { - for (const e of account.emails) { - this.addEmailToMap(e, account); - } - } + if (account.emails) { + for (const e of account.emails) { + this.addEmailToMap(e, account); + } + } - this.addNoteRefsToMap(account.note, account); - this.updateOriginRefs(account); - this.accountsForPotentialDuplicatesCheck.push(account); - }, - onUpdate: (oldAccount, newAccount) => { - if (oldAccount.emails) { - for (const e of oldAccount.emails) { - if (!includes(newAccount.emails, e)) { - this.removeEmailFromMap(e, oldAccount); - } - } - } + this.addNoteRefsToMap(account.note, account); + this.updateOriginRefs(account); + this.accountsForPotentialDuplicatesCheck.push(account); + }, + onUpdate: (oldAccount, newAccount) => { + if (oldAccount.emails) { + for (const e of oldAccount.emails) { + if (!includes(newAccount.emails, e)) { + this.removeEmailFromMap(e, oldAccount); + } + } + } - if (newAccount.emails) { - for (const e of newAccount.emails) { - if (!includes(oldAccount.emails, e)) { - this.addEmailToMap(e, oldAccount); - } - } - } + if (newAccount.emails) { + for (const e of newAccount.emails) { + if (!includes(oldAccount.emails, e)) { + this.addEmailToMap(e, oldAccount); + } + } + } - if (oldAccount.note !== newAccount.note) { - this.removeNoteRefsFromMap(oldAccount.note, oldAccount); - this.addNoteRefsToMap(newAccount.note, oldAccount); - } + if (oldAccount.note !== newAccount.note) { + this.removeNoteRefsFromMap(oldAccount.note, oldAccount); + this.addNoteRefsToMap(newAccount.note, oldAccount); + } - if (oldAccount.lastBrowserId !== newAccount.lastBrowserId) { - oldAccount.lastBrowserId && this.removeBrowserIdFromMap(oldAccount.lastBrowserId, oldAccount); - newAccount.lastBrowserId && this.addBrowserIdToMap(newAccount.lastBrowserId, oldAccount); - } + if (oldAccount.lastBrowserId !== newAccount.lastBrowserId) { + oldAccount.lastBrowserId && this.removeBrowserIdFromMap(oldAccount.lastBrowserId, oldAccount); + newAccount.lastBrowserId && this.addBrowserIdToMap(newAccount.lastBrowserId, oldAccount); + } - Object.assign(oldAccount, newAccount); + Object.assign(oldAccount, newAccount); - if (newAccount.birthyear === undefined) { - oldAccount.birthyear = undefined; - } + if (newAccount.birthyear === undefined) { + oldAccount.birthyear = undefined; + } - if (newAccount.alert === undefined) { - oldAccount.alert = undefined; - } + if (newAccount.alert === undefined) { + oldAccount.alert = undefined; + } - if (newAccount.patreon === undefined) { - oldAccount.patreon = undefined; - } + if (newAccount.patreon === undefined) { + oldAccount.patreon = undefined; + } - if (newAccount.supporter === undefined) { - oldAccount.supporter = undefined; - } + if (newAccount.supporter === undefined) { + oldAccount.supporter = undefined; + } - this.updateOriginRefs(oldAccount); - this.accountsForPotentialDuplicatesCheck.push(oldAccount); - }, - onAddedOrUpdated: () => { - this.assignItems(this.unassignedAuths, (account, auth) => addAuthToAccount(account, auth, 'onAddedOrUpdated')); - this.assignItems(this.unassignedPonies, addPonyToAccount); - }, - onDelete: account => { - account.origins = []; - this.updateOriginRefs(account); + this.updateOriginRefs(oldAccount); + this.accountsForPotentialDuplicatesCheck.push(oldAccount); + }, + onAddedOrUpdated: () => { + this.assignItems(this.unassignedAuths, (account, auth) => addAuthToAccount(account, auth, 'onAddedOrUpdated')); + this.assignItems(this.unassignedPonies, addPonyToAccount); + }, + onDelete: account => { + account.origins = []; + this.updateOriginRefs(account); - if (account.emails) { - for (const email of account.emails) { - this.removeEmailFromMap(email, account); - } - } + if (account.emails) { + for (const email of account.emails) { + this.removeEmailFromMap(email, account); + } + } - account.lastBrowserId && this.removeBrowserIdFromMap(account.lastBrowserId, account); - this.removeNoteRefsFromMap(account.note, account); - this.accountDeleted.next(account); - }, - onFinished: () => { - sort(this.accounts.items, compareAccounts); - this.auths.start(); - }, - }); + account.lastBrowserId && this.removeBrowserIdFromMap(account.lastBrowserId, account); + this.removeNoteRefsFromMap(account.note, account); + this.accountDeleted.next(account); + }, + onFinished: () => { + sort(this.accounts.items, compareAccounts); + this.auths.start(); + }, + }); - this.origins = new LiveList(db.Origin, { - fields: ['_id', 'updatedAt', 'ip', 'country', 'mute', 'shadow', 'ban'], - clean: ({ _id, updatedAt, ip, country, mute, shadow, ban, accounts }) => - ({ _id, updatedAt, ip, country, mute, shadow, ban, accountsCount: accounts ? accounts.length : 0 }), - onAdd: origin => { - origin.accounts = []; - }, - onSubscribeToMissing: ip => ({ ip, country: '??' }) as any, - }, origin => origin.ip); + this.origins = new LiveList(db.Origin, { + fields: ['_id', 'updatedAt', 'ip', 'country', 'mute', 'shadow', 'ban'], + clean: ({ _id, updatedAt, ip, country, mute, shadow, ban, accounts }) => + ({ _id, updatedAt, ip, country, mute, shadow, ban, accountsCount: accounts ? accounts.length : 0 }), + onAdd: origin => { + origin.accounts = []; + }, + onSubscribeToMissing: ip => ({ ip, country: '??' }) as any, + }, origin => origin.ip); - this.auths = new LiveList(db.Auth, { - fields: ['_id', 'updatedAt', 'account', 'provider', 'name', 'url', 'disabled', 'banned', 'pledged', 'lastUsed'], - clean: ({ _id, updatedAt, account, provider, name, url, disabled, banned, pledged, lastUsed }) => - ({ _id, updatedAt, account, provider, name, url, disabled, banned, pledged, lastUsed }), - fix: auth => { - if (auth.account) { - auth.account = accountId(auth.account.toString()); - } - }, - onAdd: auth => { - this.assignAccount(auth, this.unassignedAuths, account => addAuthToAccount(account, auth, 'onAdd')); - }, - onUpdate: this.createUpdater({ - remove: (account, auth) => removeAuthFromAccount(account, auth) || removeItem(this.unassignedAuths, auth), - add: (account, auth) => - account ? addAuthToAccount(account, auth, 'onUpdate') : pushUnique(this.unassignedAuths, auth), - }), - onDelete: auth => { - removeItem(this.unassignedAuths, auth); - this.accounts.for(auth.account, account => removeAuthFromAccount(account, auth)); - }, - onFinished: () => { - logger.info('Admin service loaded'); - }, - }); + this.auths = new LiveList(db.Auth, { + fields: ['_id', 'updatedAt', 'account', 'provider', 'name', 'url', 'disabled', 'banned', 'pledged', 'lastUsed'], + clean: ({ _id, updatedAt, account, provider, name, url, disabled, banned, pledged, lastUsed }) => + ({ _id, updatedAt, account, provider, name, url, disabled, banned, pledged, lastUsed }), + fix: auth => { + if (auth.account) { + auth.account = accountId(auth.account.toString()); + } + }, + onAdd: auth => { + this.assignAccount(auth, this.unassignedAuths, account => addAuthToAccount(account, auth, 'onAdd')); + }, + onUpdate: this.createUpdater({ + remove: (account, auth) => removeAuthFromAccount(account, auth) || removeItem(this.unassignedAuths, auth), + add: (account, auth) => + account ? addAuthToAccount(account, auth, 'onUpdate') : pushUnique(this.unassignedAuths, auth), + }), + onDelete: auth => { + removeItem(this.unassignedAuths, auth); + this.accounts.for(auth.account, account => removeAuthFromAccount(account, auth)); + }, + onFinished: () => { + logger.info('Admin service loaded'); + }, + }); - this.ponies = new LiveList(db.Character, { - fields: ['_id', 'createdAt', 'updatedAt', 'lastUsed', 'account', 'name', 'flags'], - noStore: true, - clean: ({ _id, createdAt, updatedAt, account, name, flags, lastUsed }) => - ({ _id, createdAt, updatedAt, account, name, flags, lastUsed }), - fix: pony => { - if (pony.account) { - pony.account = accountId(pony.account.toString()); - } - }, - ignore: pony => { - const account = this.accounts.get(pony.account); - return account === undefined || account.ponies === undefined; - }, - onAdd: pony => { - this.assignAccount(pony, this.unassignedPonies, account => addPonyToAccount(account, pony)); - }, - onUpdate: this.createUpdater({ - remove: (account, pony) => removePonyFromAccount(account, pony) || removeItem(this.unassignedPonies, pony), - add: (account, pony) => account ? addPonyToAccount(account, pony) : pushUnique(this.unassignedPonies, pony), - }), - onDelete: pony => { - removeItem(this.unassignedPonies, pony); - this.accounts.for(pony.account, account => removePonyFromAccount(account, pony)); - }, - // afterAssign: (from, to) => Promise.all([updateCharacterCount(from), updateCharacterCount(to)]), - }); + this.ponies = new LiveList(db.Character, { + fields: ['_id', 'createdAt', 'updatedAt', 'lastUsed', 'account', 'name', 'flags'], + noStore: true, + clean: ({ _id, createdAt, updatedAt, account, name, flags, lastUsed }) => + ({ _id, createdAt, updatedAt, account, name, flags, lastUsed }), + fix: pony => { + if (pony.account) { + pony.account = accountId(pony.account.toString()); + } + }, + ignore: pony => { + const account = this.accounts.get(pony.account); + return account === undefined || account.ponies === undefined; + }, + onAdd: pony => { + this.assignAccount(pony, this.unassignedPonies, account => addPonyToAccount(account, pony)); + }, + onUpdate: this.createUpdater({ + remove: (account, pony) => removePonyFromAccount(account, pony) || removeItem(this.unassignedPonies, pony), + add: (account, pony) => account ? addPonyToAccount(account, pony) : pushUnique(this.unassignedPonies, pony), + }), + onDelete: pony => { + removeItem(this.unassignedPonies, pony); + this.accounts.for(pony.account, account => removePonyFromAccount(account, pony)); + }, + // afterAssign: (from, to) => Promise.all([updateCharacterCount(from), updateCharacterCount(to)]), + }); - this.events = new LiveList(db.Event, { - fields: eventFields, - clean: ({ _id, createdAt, updatedAt, message, desc, account, pony, origin }) => - ({ _id, createdAt, updatedAt, message, desc, account, pony, origin }), - }); + this.events = new LiveList(db.Event, { + fields: eventFields, + clean: ({ _id, createdAt, updatedAt, message, desc, account, pony, origin }) => + ({ _id, createdAt, updatedAt, message, desc, account, pony, origin }), + }); - setTimeout(() => this.events.start(), 100); - setTimeout(() => this.ponies.start(), 200); - setTimeout(() => this.origins.start(), 300); - setTimeout(() => this.accounts.start(), 400); - } - get loaded() { - return this.accounts.loaded && this.origins.loaded && this.auths.loaded; - } - removedItem(type: 'events' | 'ponies' | 'accounts' | 'auths' | 'origins', id: string) { - if (type === 'accounts') { - this.accounts.removed(id); - } else if (type === 'origins') { - this.origins.removed(id); - } else if (type === 'auths') { - this.auths.removed(id); - } else if (type === 'ponies') { - this.ponies.removed(id); - } else { - console.warn(`Unhandled removedItem for type: ${type}`); - } - } - getAccountsByNoteRef(accountId: string) { - return this.noteRefMap.get(accountId) || []; - } - getAccountsByEmailName(emailName: string) { - return this.emailMap.get(emailName) || []; - } - getAccountsByBrowserId(browserId: string) { - return this.browserIdMap.get(browserId); - } - removeOriginsFromAccount(accountId: string, ips?: string[]) { - const account = this.accounts.get(accountId); + setTimeout(() => this.events.start(), 100); + setTimeout(() => this.ponies.start(), 200); + setTimeout(() => this.origins.start(), 300); + setTimeout(() => this.accounts.start(), 400); + } + get loaded() { + return this.accounts.loaded && this.origins.loaded && this.auths.loaded; + } + removedItem(type: 'events' | 'ponies' | 'accounts' | 'auths' | 'origins', id: string) { + if (type === 'accounts') { + this.accounts.removed(id); + } else if (type === 'origins') { + this.origins.removed(id); + } else if (type === 'auths') { + this.auths.removed(id); + } else if (type === 'ponies') { + this.ponies.removed(id); + } else { + console.warn(`Unhandled removedItem for type: ${type}`); + } + } + getAccountsByNoteRef(accountId: string) { + return this.noteRefMap.get(accountId) || []; + } + getAccountsByEmailName(emailName: string) { + return this.emailMap.get(emailName) || []; + } + getAccountsByBrowserId(browserId: string) { + return this.browserIdMap.get(browserId); + } + removeOriginsFromAccount(accountId: string, ips?: string[]) { + const account = this.accounts.get(accountId); - if (account) { - if (ips) { - if (remove(account.origins, o => includes(ips, o.ip)).length) { - this.updateOriginRefs(account); - } - } else if (account.origins.length) { - account.origins = []; - this.updateOriginRefs(account); - } - } - } - subscribeToAccountAuths(accountId: string, listener: ListListener) { - const account = this.accounts.get(accountId); + if (account) { + if (ips) { + if (remove(account.origins, o => includes(ips, o.ip)).length) { + this.updateOriginRefs(account); + } + } else if (account.origins.length) { + account.origins = []; + this.updateOriginRefs(account); + } + } + } + subscribeToAccountAuths(accountId: string, listener: ListListener) { + const account = this.accounts.get(accountId); - if (account) { - return account.authsList!.subscribe(listener); - } else { - return undefined; - } - } - subscribeToAccountOrigins(accountId: string, listener: ListListener) { - const account = this.accounts.get(accountId); + if (account) { + return account.authsList!.subscribe(listener); + } else { + return undefined; + } + } + subscribeToAccountOrigins(accountId: string, listener: ListListener) { + const account = this.accounts.get(accountId); - if (account) { - if (!account.originsList) { - account.originsList = new ObservableList( - account.originsRefs!, ({ origin, last }) => ({ ip: origin.ip, country: origin.country, last })); - } + if (account) { + if (!account.originsList) { + account.originsList = new ObservableList( + account.originsRefs!, ({ origin, last }) => ({ ip: origin.ip, country: origin.country, last })); + } - return account.originsList.subscribe(listener); - } + return account.originsList.subscribe(listener); + } - return undefined; - } - subscribeToAccountPonies(accountId: string, listener: ListListener) { - const account = this.accounts.get(accountId); + return undefined; + } + subscribeToAccountPonies(accountId: string, listener: ListListener) { + const account = this.accounts.get(accountId); - if (account) { - if (!account.ponies) { - account.ponies = this.ponies.items.filter(p => p.account === account._id); - this.ponies.fetch({ account: account._id }); - } + if (account) { + if (!account.ponies) { + account.ponies = this.ponies.items.filter(p => p.account === account._id); + this.ponies.fetch({ account: account._id }); + } - if (!account.poniesList) { - account.poniesList = new ObservableList( - account.ponies!, p => ({ id: p._id, name: p.name, date: p.lastUsed ? p.lastUsed.getTime() : 0 })); - } + if (!account.poniesList) { + account.poniesList = new ObservableList( + account.ponies!, p => ({ id: p._id, name: p.name, date: p.lastUsed ? p.lastUsed.getTime() : 0 })); + } - return account.poniesList.subscribe(listener); - } + return account.poniesList.subscribe(listener); + } - return undefined; - } - cleanupOriginsList(accountId: string) { - const account = this.accounts.get(accountId); + return undefined; + } + cleanupOriginsList(accountId: string) { + const account = this.accounts.get(accountId); - if (account && account.originsList && !account.originsList.hasSubscribers()) { - account.originsList = undefined; - } - } - cleanupPoniesList(accountId: string) { - const account = this.accounts.get(accountId); + if (account && account.originsList && !account.originsList.hasSubscribers()) { + account.originsList = undefined; + } + } + cleanupPoniesList(accountId: string) { + const account = this.accounts.get(accountId); - if (account && account.ponies && account.poniesList && !account.poniesList.hasSubscribers()) { - const ponies = account.ponies; - account.ponies = undefined; - account.poniesList = undefined; + if (account && account.ponies && account.poniesList && !account.poniesList.hasSubscribers()) { + const ponies = account.ponies; + account.ponies = undefined; + account.poniesList = undefined; - for (const pony of ponies) { - this.cleanupPony(pony._id); - } - } - } - cleanupPony(ponyId: string) { - const pony = this.ponies.get(ponyId); + for (const pony of ponies) { + this.cleanupPony(pony._id); + } + } + } + cleanupPony(ponyId: string) { + const pony = this.ponies.get(ponyId); - if (pony && !this.ponies.hasSubscriptions(ponyId)) { - const account = this.accounts.get(pony.account); + if (pony && !this.ponies.hasSubscriptions(ponyId)) { + const account = this.accounts.get(pony.account); - if (!account || !account.ponies) { - this.ponies.discard(ponyId); - } - } - } - private duplicateFilter = createPotentialDuplicatesFilter(id => this.browserIdMap.get(id)); - private accountsForPotentialDuplicatesCheck: Account[] = []; - async mergePotentialDuplicates() { - const start = Date.now(); - const duplicateFilter = this.duplicateFilter; + if (!account || !account.ponies) { + this.ponies.discard(ponyId); + } + } + } + private duplicateFilter = createPotentialDuplicatesFilter(id => this.browserIdMap.get(id)); + private accountsForPotentialDuplicatesCheck: Account[] = []; + async mergePotentialDuplicates() { + const start = Date.now(); + const duplicateFilter = this.duplicateFilter; - while (this.accountsForPotentialDuplicatesCheck.length) { - const popedAccount = this.accountsForPotentialDuplicatesCheck.pop()!; - const account = this.getAccount(popedAccount._id); + while (this.accountsForPotentialDuplicatesCheck.length) { + const popedAccount = this.accountsForPotentialDuplicatesCheck.pop()!; + const account = this.getAccount(popedAccount._id); - if (account && duplicateFilter(account)) { - const threshold = fromNow(-1 * HOUR).getTime(); - const duplicates = getPotentialDuplicates(account, id => this.getAccountsByBrowserId(id)) - .filter(a => a.createdAt && a.createdAt.getTime() < threshold); + if (account && duplicateFilter(account)) { + const threshold = fromNow(-1 * HOUR).getTime(); + const duplicates = getPotentialDuplicates(account, id => this.getAccountsByBrowserId(id)) + .filter(a => a.createdAt && a.createdAt.getTime() < threshold); - if (duplicates.length) { - const server = getLoginServer('login'); - const duplicate = duplicates[0]; - const accountIsOlder = account.lastVisit && duplicate.lastVisit - && account.lastVisit.getTime() < duplicate.lastVisit.getTime(); - const accountId = accountIsOlder ? duplicate._id : account._id; - const withId = accountIsOlder ? account._id : duplicate._id; - logPerformance(`mergePotentialDuplicates (${Date.now() - start}ms) [yes]`); - await server.api.mergeAccounts(accountId, withId, `by server`, false, true); - return accountId; - } - } - } + if (duplicates.length) { + const server = getLoginServer('login'); + const duplicate = duplicates[0]; + const accountIsOlder = account.lastVisit && duplicate.lastVisit + && account.lastVisit.getTime() < duplicate.lastVisit.getTime(); + const accountId = accountIsOlder ? duplicate._id : account._id; + const withId = accountIsOlder ? account._id : duplicate._id; + logPerformance(`mergePotentialDuplicates (${Date.now() - start}ms) [yes]`); + await server.api.mergeAccounts(accountId, withId, `by server`, false, true); + return accountId; + } + } + } - this.accountsForPotentialDuplicatesCheck = []; + this.accountsForPotentialDuplicatesCheck = []; - logPerformance(`mergePotentialDuplicates (${Date.now() - start}ms) [no]`); - return undefined; - } - // helpers - private addEmailToMap(email: string, account: Account) { - addToMap(this.emailMap, emailName(email), account); - } - private removeEmailFromMap(email: string, account: Account) { - removeFromMap(this.emailMap, emailName(email), account); - } - private addNoteRefsToMap(note: string, account: Account) { - for (let id of getIdsFromNote(note)) { - if (id !== account._id) { - addToMap(this.noteRefMap, id, account); - } - } - } - private removeNoteRefsFromMap(note: string, account: Account) { - for (let id of getIdsFromNote(note)) { - if (id !== account._id) { - removeFromMap(this.noteRefMap, id, account); - } - } - } - private addBrowserIdToMap(browserId: string, account: Account) { - addToMap(this.browserIdMap, browserId, account); - } - private removeBrowserIdFromMap(browserId: string, account: Account) { - removeFromMap(this.browserIdMap, browserId, account); - } - private getAccount(id: string | undefined) { - return id ? this.accounts.get(id) : undefined; - } - private getOrCreateOrigin({ ip, country }: OriginInfo): Origin { - return this.origins.get(ip) - || this.origins.add({ _id: '', ip, country, accounts: [], updatedAt: new Date(0), createdAt: new Date(0) }); - } - private assignAccount(item: T, unassigned: T[], action: (account: Account) => void) { - const account = this.getAccount(item.account); + logPerformance(`mergePotentialDuplicates (${Date.now() - start}ms) [no]`); + return undefined; + } + // helpers + private addEmailToMap(email: string, account: Account) { + addToMap(this.emailMap, emailName(email), account); + } + private removeEmailFromMap(email: string, account: Account) { + removeFromMap(this.emailMap, emailName(email), account); + } + private addNoteRefsToMap(note: string, account: Account) { + for (let id of getIdsFromNote(note)) { + if (id !== account._id) { + addToMap(this.noteRefMap, id, account); + } + } + } + private removeNoteRefsFromMap(note: string, account: Account) { + for (let id of getIdsFromNote(note)) { + if (id !== account._id) { + removeFromMap(this.noteRefMap, id, account); + } + } + } + private addBrowserIdToMap(browserId: string, account: Account) { + addToMap(this.browserIdMap, browserId, account); + } + private removeBrowserIdFromMap(browserId: string, account: Account) { + removeFromMap(this.browserIdMap, browserId, account); + } + private getAccount(id: string | undefined) { + return id ? this.accounts.get(id) : undefined; + } + private getOrCreateOrigin({ ip, country }: OriginInfo): Origin { + return this.origins.get(ip) + || this.origins.add({ _id: '', ip, country, accounts: [], updatedAt: new Date(0), createdAt: new Date(0) }); + } + private assignAccount(item: T, unassigned: T[], action: (account: Account) => void) { + const account = this.getAccount(item.account); - if (account) { - action(account); - } else { - pushUnique(unassigned, item); - } - } - private updateOriginRefs(a: Account) { - if (a.originsRefs) { - for (const o of a.originsRefs) { - removeById(o.origin.accounts!, a._id); - } - } + if (account) { + action(account); + } else { + pushUnique(unassigned, item); + } + } + private updateOriginRefs(a: Account) { + if (a.originsRefs) { + for (const o of a.originsRefs) { + removeById(o.origin.accounts!, a._id); + } + } - const oldOriginRefs = a.originsRefs; - a.originsRefs = a.origins.map(o => { origin: this.getOrCreateOrigin(o), last: o.last }); + const oldOriginRefs = a.originsRefs; + a.originsRefs = a.origins.map(o => { origin: this.getOrCreateOrigin(o), last: o.last }); - sort(a.originsRefs, compareOriginRefs); + sort(a.originsRefs, compareOriginRefs); - for (const o of a.originsRefs) { - if (o.origin.accounts && !includes(o.origin.accounts, a)) { - o.origin.accounts.push(a); - } - } + for (const o of a.originsRefs) { + if (o.origin.accounts && !includes(o.origin.accounts, a)) { + o.origin.accounts.push(a); + } + } - if (oldOriginRefs) { - for (const o of oldOriginRefs) { - if (!o.origin._id && o.origin.accounts!.length === 0) { - this.origins.removed(o.origin.ip); - } else { - this.origins.trigger(o.origin.ip, o.origin); - } - } - } + if (oldOriginRefs) { + for (const o of oldOriginRefs) { + if (!o.origin._id && o.origin.accounts!.length === 0) { + this.origins.removed(o.origin.ip); + } else { + this.origins.trigger(o.origin.ip, o.origin); + } + } + } - if (a.originsList) { - a.originsList.replace(a.originsRefs); - } - } - private assignItems(unassigned: T[], push: (account: Account, item: T) => void) { - remove(unassigned, item => { - const account = item.account && this.getAccount(item.account); + if (a.originsList) { + a.originsList.replace(a.originsRefs); + } + } + private assignItems(unassigned: T[], push: (account: Account, item: T) => void) { + remove(unassigned, item => { + const account = item.account && this.getAccount(item.account); - if (account) { - push(account, item); - return true; - } else { - return false; - } - }); - } - private createUpdater( - { add, remove }: { - remove: (account: Account, item: T) => void; - add: (account: Account | undefined, item: T) => void; - } - ) { - return (oldItem: T, newItem: T) => { - const oldAccountId = oldItem.account; - const newAccountId = newItem.account; + if (account) { + push(account, item); + return true; + } else { + return false; + } + }); + } + private createUpdater( + { add, remove }: { + remove: (account: Account, item: T) => void; + add: (account: Account | undefined, item: T) => void; + } + ) { + return (oldItem: T, newItem: T) => { + const oldAccountId = oldItem.account; + const newAccountId = newItem.account; - Object.assign(oldItem, newItem); + Object.assign(oldItem, newItem); - if (oldAccountId !== newAccountId) { - const oldAccount = this.getAccount(oldAccountId); - const newAccount = this.getAccount(newAccountId); + if (oldAccountId !== newAccountId) { + const oldAccount = this.getAccount(oldAccountId); + const newAccount = this.getAccount(newAccountId); - if (oldAccount) { - remove(oldAccount, oldItem); - } + if (oldAccount) { + remove(oldAccount, oldItem); + } - add(newAccount, oldItem); - } - }; - } + add(newAccount, oldItem); + } + }; + } } function findIndexById(items: T[], id: U): number { - for (let i = 0; i < items.length; i++) { - if (items[i]._id === id) { - return i; - } - } + for (let i = 0; i < items.length; i++) { + if (items[i]._id === id) { + return i; + } + } - return -1; + return -1; } function removeById(items: T[], id: U): T | undefined { - const index = findIndexById(items, id); + const index = findIndexById(items, id); - if (index !== -1) { - const item = items[index]; - items.splice(index, 1); - return item; - } else { - return undefined; - } + if (index !== -1) { + const item = items[index]; + items.splice(index, 1); + return item; + } else { + return undefined; + } } diff --git a/src/ts/server/services/counter.ts b/src/ts/server/services/counter.ts index 480174e..4ddd8d1 100644 --- a/src/ts/server/services/counter.ts +++ b/src/ts/server/services/counter.ts @@ -1,51 +1,51 @@ interface Counter { - date: number; - count: number; - items: T[]; + date: number; + count: number; + items: T[]; } const zeroCounter: Counter = { date: 0, count: 0, items: [] }; export class CounterService { - private counters = new Map>(); - private interval: any; - constructor(private clearTimeout: number) { - } - get(id: string): Counter { - return this.counters.get(id) || zeroCounter; - } - add(id: string, item?: T, count = 1) { - const counter = this.counters.get(id) || { date: 0, count: 0, items: [] }; - counter.date = Date.now(); - counter.count += count; + private counters = new Map>(); + private interval: any; + constructor(private clearTimeout: number) { + } + get(id: string): Counter { + return this.counters.get(id) || zeroCounter; + } + add(id: string, item?: T, count = 1) { + const counter = this.counters.get(id) || { date: 0, count: 0, items: [] }; + counter.date = Date.now(); + counter.count += count; - if (item) { - counter.items.push(item); - } + if (item) { + counter.items.push(item); + } - this.counters.set(id, counter); - return counter; - } - remove(id: string) { - this.counters.delete(id); - } - cleanup() { - const threshold = Date.now() - this.clearTimeout; - const remove: string[] = []; + this.counters.set(id, counter); + return counter; + } + remove(id: string) { + this.counters.delete(id); + } + cleanup() { + const threshold = Date.now() - this.clearTimeout; + const remove: string[] = []; - this.counters.forEach((value, key) => { - if (value.date < threshold) { - remove.push(key); - } - }); + this.counters.forEach((value, key) => { + if (value.date < threshold) { + remove.push(key); + } + }); - remove.forEach(id => this.remove(id)); - } - start() { - this.interval = this.interval || setInterval(() => this.cleanup(), this.clearTimeout / 10); - } - stop() { - clearInterval(this.interval); - this.interval = undefined; - } + remove.forEach(id => this.remove(id)); + } + start() { + this.interval = this.interval || setInterval(() => this.cleanup(), this.clearTimeout / 10); + } + stop() { + clearInterval(this.interval); + this.interval = undefined; + } } diff --git a/src/ts/server/services/friends.ts b/src/ts/server/services/friends.ts index e5c59a8..d542ce2 100644 --- a/src/ts/server/services/friends.ts +++ b/src/ts/server/services/friends.ts @@ -16,192 +16,192 @@ export const REJECTED_LIMIT = 5; export const REJECTED_TIMEOUT = 2 * HOUR; export function isFriend(client: IClient, friend: IClient) { - return client.friends.has(friend.accountId); + return client.friends.has(friend.accountId); } export function isOnlineFriend(client: IClient, friend: IClient) { - return client.friends.has(friend.accountId) && !friend.accountSettings.hidden; + return client.friends.has(friend.accountId) && !friend.accountSettings.hidden; } export function toFriendOnline(client: IClient): FriendStatusData { - return { - accountId: client.accountId, - accountName: client.accountName, - status: FriendStatusFlags.Online, - entityId: client.pony.id, - crc: client.pony.crc, - name: client.pony.name, - nameBad: client.pony.nameBad, - info: client.pony.infoSafe, - }; + return { + accountId: client.accountId, + accountName: client.accountName, + status: FriendStatusFlags.Online, + entityId: client.pony.id, + crc: client.pony.crc, + name: client.pony.name, + nameBad: client.pony.nameBad, + info: client.pony.infoSafe, + }; } export function toFriendOffline(client: IClient): FriendStatusData { - return { - accountId: client.accountId, - accountName: client.accountName, - status: FriendStatusFlags.None, - entityId: 0, - }; + return { + accountId: client.accountId, + accountName: client.accountName, + status: FriendStatusFlags.None, + entityId: 0, + }; } export function toFriendRemove(client: IClient): FriendStatusData { - return { - accountId: client.accountId, - status: FriendStatusFlags.Remove, - }; + return { + accountId: client.accountId, + status: FriendStatusFlags.Remove, + }; } export function toFriend(client: IClient): FriendStatusData { - if (client.isConnected) { - return toFriendOnline(client); - } else { - return toFriendOffline(client); - } + if (client.isConnected) { + return toFriendOnline(client); + } else { + return toFriendOffline(client); + } } export class FriendsService { - private limiter = new ActionLimiter(REJECTED_TIMEOUT, REJECTED_LIMIT); - private pending = new Map>(); - constructor( - private notificationService: NotificationService, - private reportInviteLimit: (client: IClient) => void - ) { - } - dispose() { - this.limiter.dispose(); - } - clientDisconnected(client: IClient) { - for (const key of Array.from(this.pending.keys())) { - const pending = this.pending.get(key)!; - pending.delete(client.accountId); + private limiter = new ActionLimiter(REJECTED_TIMEOUT, REJECTED_LIMIT); + private pending = new Map>(); + constructor( + private notificationService: NotificationService, + private reportInviteLimit: (client: IClient) => void + ) { + } + dispose() { + this.limiter.dispose(); + } + clientDisconnected(client: IClient) { + for (const key of Array.from(this.pending.keys())) { + const pending = this.pending.get(key)!; + pending.delete(client.accountId); - if (!pending.size) { - this.pending.delete(key); - } - } - } - remove(client: IClient, friend: IClient) { - removeFriend(client.accountId, friend.accountId).catch(e => logger.error(e)); - client.friends.delete(friend.accountId); - client.friendsCRC = undefined; - friend.friends.delete(client.accountId); - friend.friendsCRC = undefined; - client.reporter.systemLog(`Removed friend [${friend.accountId}]`); - client.updateFriends([{ accountId: friend.accountId, status: FriendStatusFlags.Remove }], false); - friend.updateFriends([{ accountId: client.accountId, status: FriendStatusFlags.Remove }], false); - updateEntityPlayerState(client, friend.pony); - updateEntityPlayerState(friend, client.pony); - } - removeByAccountId(client: IClient, friendAccountId: string) { - removeFriend(client.accountId, friendAccountId).catch(e => logger.error(e)); - client.friends.delete(friendAccountId); - client.friendsCRC = undefined; - client.reporter.systemLog(`Removed friend [${friendAccountId}]`); - client.updateFriends([{ accountId: friendAccountId, status: FriendStatusFlags.Remove }], false); - } - add(client: IClient, target: IClient) { - const can = this.limiter.canExecute(client, target); + if (!pending.size) { + this.pending.delete(key); + } + } + } + remove(client: IClient, friend: IClient) { + removeFriend(client.accountId, friend.accountId).catch(e => logger.error(e)); + client.friends.delete(friend.accountId); + client.friendsCRC = undefined; + friend.friends.delete(client.accountId); + friend.friendsCRC = undefined; + client.reporter.systemLog(`Removed friend [${friend.accountId}]`); + client.updateFriends([{ accountId: friend.accountId, status: FriendStatusFlags.Remove }], false); + friend.updateFriends([{ accountId: client.accountId, status: FriendStatusFlags.Remove }], false); + updateEntityPlayerState(client, friend.pony); + updateEntityPlayerState(friend, client.pony); + } + removeByAccountId(client: IClient, friendAccountId: string) { + removeFriend(client.accountId, friendAccountId).catch(e => logger.error(e)); + client.friends.delete(friendAccountId); + client.friendsCRC = undefined; + client.reporter.systemLog(`Removed friend [${friendAccountId}]`); + client.updateFriends([{ accountId: friendAccountId, status: FriendStatusFlags.Remove }], false); + } + add(client: IClient, target: IClient) { + const can = this.limiter.canExecute(client, target); - if (can === LimiterResult.LimitReached) { - return saySystem(client, 'Reached request rejection limit'); - } else if (can !== LimiterResult.Yes) { - return saySystem(client, 'Cannot send request'); - } + if (can === LimiterResult.LimitReached) { + return saySystem(client, 'Reached request rejection limit'); + } else if (can !== LimiterResult.Yes) { + return saySystem(client, 'Cannot send request'); + } - const pending = this.pending.get(client.accountId) || new Set(); + const pending = this.pending.get(client.accountId) || new Set(); - if (pending.has(target.accountId)) - return saySystem(client, 'Already sent request'); + if (pending.has(target.accountId)) + return saySystem(client, 'Already sent request'); - if (isFriend(client, target)) - return saySystem(client, 'Already on friends list'); + if (isFriend(client, target)) + return saySystem(client, 'Already on friends list'); - if (client.friends.size >= FRIENDS_LIMIT) - return saySystem(client, 'Your friend list is full'); + if (client.friends.size >= FRIENDS_LIMIT) + return saySystem(client, 'Your friend list is full'); - if (target.friends.size >= FRIENDS_LIMIT) - return saySystem(client, 'Target player friend list is full'); + if (target.friends.size >= FRIENDS_LIMIT) + return saySystem(client, 'Target player friend list is full'); - if (hasFlag(client.account.flags, AccountFlags.BlockFriendRequests)) - return saySystem(client, 'Cannot send request'); + if (hasFlag(client.account.flags, AccountFlags.BlockFriendRequests)) + return saySystem(client, 'Cannot send request'); - if (target.accountSettings.ignoreFriendInvites) - return saySystem(client, 'Cannot send request'); + if (target.accountSettings.ignoreFriendInvites) + return saySystem(client, 'Cannot send request'); - if (pending.size >= PENDING_LIMIT) - return saySystem(client, 'Too many pending requests'); + if (pending.size >= PENDING_LIMIT) + return saySystem(client, 'Too many pending requests'); - const notificationId = this.addInviteNotification(client, target); + const notificationId = this.addInviteNotification(client, target); - if (!notificationId) { - return saySystem(client, 'Cannot send request'); - } + if (!notificationId) { + return saySystem(client, 'Cannot send request'); + } - pending.add(target.accountId); - this.pending.set(client.accountId, pending); - client.reporter.systemLog(`Friend request [${target.accountId}]`); - } - private acceptInvitation(client: IClient, friend: IClient, notificationId: number) { - client.reporter.systemLog(`Friend request accepted by [${friend.accountId}]`); - saySystem(client, `Friend request accepted by ${getEntityName(friend.pony, client)}`); + pending.add(target.accountId); + this.pending.set(client.accountId, pending); + client.reporter.systemLog(`Friend request [${target.accountId}]`); + } + private acceptInvitation(client: IClient, friend: IClient, notificationId: number) { + client.reporter.systemLog(`Friend request accepted by [${friend.accountId}]`); + saySystem(client, `Friend request accepted by ${getEntityName(friend.pony, client)}`); - addFriend(client.accountId, friend.accountId) - .catch(e => { - if (e.message !== `Friend request already exists`) { - logger.error(e); - } - }); + addFriend(client.accountId, friend.accountId) + .catch(e => { + if (e.message !== `Friend request already exists`) { + logger.error(e); + } + }); - client.friends.add(friend.accountId); - client.friendsCRC = undefined; - friend.friends.add(client.accountId); - friend.friendsCRC = undefined; - this.removePending(client, friend); - this.notificationService.removeNotification(friend, notificationId); - client.updateFriends([toFriend(friend)], false); - friend.updateFriends([toFriend(client)], false); - updateEntityPlayerState(client, friend.pony); - updateEntityPlayerState(friend, client.pony); - } - private rejectInvitation(client: IClient, friend: IClient, notificationId: number) { - client.reporter.systemLog(`Friend request rejected by [${friend.accountId}]`); - saySystem(client, `Friend request rejected by ${getEntityName(friend.pony, client)}`); - this.removePending(client, friend); - this.notificationService.removeNotification(friend, notificationId); - this.countReject(client); - } - private removePending(client: IClient, friend: IClient) { - const pending = this.pending.get(client.accountId); + client.friends.add(friend.accountId); + client.friendsCRC = undefined; + friend.friends.add(client.accountId); + friend.friendsCRC = undefined; + this.removePending(client, friend); + this.notificationService.removeNotification(friend, notificationId); + client.updateFriends([toFriend(friend)], false); + friend.updateFriends([toFriend(client)], false); + updateEntityPlayerState(client, friend.pony); + updateEntityPlayerState(friend, client.pony); + } + private rejectInvitation(client: IClient, friend: IClient, notificationId: number) { + client.reporter.systemLog(`Friend request rejected by [${friend.accountId}]`); + saySystem(client, `Friend request rejected by ${getEntityName(friend.pony, client)}`); + this.removePending(client, friend); + this.notificationService.removeNotification(friend, notificationId); + this.countReject(client); + } + private removePending(client: IClient, friend: IClient) { + const pending = this.pending.get(client.accountId); - if (pending) { - pending.delete(friend.accountId); + if (pending) { + pending.delete(friend.accountId); - if (pending.size === 0) { - this.pending.delete(client.accountId); - } - } - } - private countReject(invitedBy: IClient) { - const count = this.limiter.count(invitedBy); + if (pending.size === 0) { + this.pending.delete(client.accountId); + } + } + } + private countReject(invitedBy: IClient) { + const count = this.limiter.count(invitedBy); - if (count >= REJECTED_LIMIT) { - this.reportInviteLimit(invitedBy); - } - } - private addInviteNotification(client: IClient, friend: IClient) { - const notificationId = this.notificationService.addNotification(friend, { - id: 0, - sender: client, - name: client.pony.name || '', - entityId: client.pony.id, - message: `
Friend request
#NAME# wants to add you to their friends`, - flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore | - (client.pony.nameBad ? NotificationFlags.NameBad : 0), - accept: () => this.acceptInvitation(client, friend, notificationId), - reject: () => this.rejectInvitation(client, friend, notificationId), - }); + if (count >= REJECTED_LIMIT) { + this.reportInviteLimit(invitedBy); + } + } + private addInviteNotification(client: IClient, friend: IClient) { + const notificationId = this.notificationService.addNotification(friend, { + id: 0, + sender: client, + name: client.pony.name || '', + entityId: client.pony.id, + message: `
Friend request
#NAME# wants to add you to their friends`, + flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore | + (client.pony.nameBad ? NotificationFlags.NameBad : 0), + accept: () => this.acceptInvitation(client, friend, notificationId), + reject: () => this.rejectInvitation(client, friend, notificationId), + }); - return notificationId; - } + return notificationId; + } } diff --git a/src/ts/server/services/hiding.ts b/src/ts/server/services/hiding.ts index 4e38517..3f8e8b5 100644 --- a/src/ts/server/services/hiding.ts +++ b/src/ts/server/services/hiding.ts @@ -14,8 +14,8 @@ import { getEntityName } from '../entityUtils'; import { saySystem } from '../chat'; interface Hide { - by: string; - who: string; + by: string; + who: string; } const hidePlayerLimit = 'Cannot hide any more players.'; @@ -25,340 +25,340 @@ const unhideAllLimit = 'Cannot unhide hidden players, try again later.'; const unhideAllLimitNote = 'You can only do this once per hour.'; function clientInfo({ accountId, account, characterName }: IClient) { - return `${characterName} (${account.name}) [${accountId}]`; + return `${characterName} (${account.name}) [${accountId}]`; } function simpleNotification(message: string, note?: string): ServerNotification { - return { id: 0, name: '', message, note, flags: NotificationFlags.Ok }; + return { id: 0, name: '', message, note, flags: NotificationFlags.Ok }; } export function hidingDataPath(serverId: string) { - return pathTo('settings', `hiding-${serverId}.json`); + return pathTo('settings', `hiding-${serverId}.json`); } export async function saveHidingData(hiding: HidingService, serverId: string) { - if (!TESTS) { - try { - const data = hiding.serialize(); - await fs.writeFileAsync(hidingDataPath(serverId), data, 'utf8'); - } catch (e) { - logger.error(e); - } - } + if (!TESTS) { + try { + const data = hiding.serialize(); + await fs.writeFileAsync(hidingDataPath(serverId), data, 'utf8'); + } catch (e) { + logger.error(e); + } + } } export function pollHidingDataSave(hiding: HidingService, serverId: string) { - setInterval(() => saveHidingData(hiding, serverId), 10 * MINUTE); + setInterval(() => saveHidingData(hiding, serverId), 10 * MINUTE); } export class HidingService { - changes = new Subject(); - unhidesAll = new Subject(); - private hides = new Map>(); - private unhides = new Map(); - private interval: any; - constructor( - private clearUnhides: number, - private notifications: NotificationService, - private findClient: (accountId: string) => IClient | undefined, - private log: (message: string) => void, - ) { - } - serialize() { - const hides: any = {}; - const unhides: any = {}; + changes = new Subject(); + unhidesAll = new Subject(); + private hides = new Map>(); + private unhides = new Map(); + private interval: any; + constructor( + private clearUnhides: number, + private notifications: NotificationService, + private findClient: (accountId: string) => IClient | undefined, + private log: (message: string) => void, + ) { + } + serialize() { + const hides: any = {}; + const unhides: any = {}; - this.hides.forEach((hidesMap, by) => { - const list: any = {}; - hidesMap.forEach((value, key) => list[key] = value); - hides[by] = list; - }); + this.hides.forEach((hidesMap, by) => { + const list: any = {}; + hidesMap.forEach((value, key) => list[key] = value); + hides[by] = list; + }); - this.unhides.forEach((value, key) => unhides[key] = value); + this.unhides.forEach((value, key) => unhides[key] = value); - return JSON.stringify({ hides, unhides }); - } - deserialize(data: string) { - try { - const { hides, unhides } = JSON.parse(data); + return JSON.stringify({ hides, unhides }); + } + deserialize(data: string) { + try { + const { hides, unhides } = JSON.parse(data); - for (const by of Object.keys(hides)) { - const hidesMap = new Map(); + for (const by of Object.keys(hides)) { + const hidesMap = new Map(); - for (const key of Object.keys(hides[by])) { - hidesMap.set(key, hides[by][key]); - } + for (const key of Object.keys(hides[by])) { + hidesMap.set(key, hides[by][key]); + } - this.hides.set(by, hidesMap); - } + this.hides.set(by, hidesMap); + } - for (const key of Object.keys(unhides)) { - this.unhides.set(key, unhides[key]); - } + for (const key of Object.keys(unhides)) { + this.unhides.set(key, unhides[key]); + } - this.cleanup(); - } catch (e) { - logger.error(e); - } - } - getStatsFor(account: string): HidingStats { - const hides = this.hides.get(account); - const hidden = hides ? Array.from(hides.keys()) : []; - const hiddenBy: string[] = []; + this.cleanup(); + } catch (e) { + logger.error(e); + } + } + getStatsFor(account: string): HidingStats { + const hides = this.hides.get(account); + const hidden = hides ? Array.from(hides.keys()) : []; + const hiddenBy: string[] = []; - this.hides.forEach((hides, by) => { - if (hides.has(account)) { - hiddenBy.push(by); - } - }); + this.hides.forEach((hides, by) => { + if (hides.has(account)) { + hiddenBy.push(by); + } + }); - return { account, hidden, hiddenBy, permaHidden: [], permaHiddenBy: [] }; - } - connected(client: IClient) { - const hides = this.hides.get(client.accountId); + return { account, hidden, hiddenBy, permaHidden: [], permaHiddenBy: [] }; + } + connected(client: IClient) { + const hides = this.hides.get(client.accountId); - if (hides) { - for (const id of Array.from(hides.keys())) { - client.hides.add(id); - } - } - } - requestHide(requester: IClient, target: IClient, timeout: number) { - const hides = this.hides.get(requester.accountId); - const count = hides && hides.size || 0; + if (hides) { + for (const id of Array.from(hides.keys())) { + client.hides.add(id); + } + } + } + requestHide(requester: IClient, target: IClient, timeout: number) { + const hides = this.hides.get(requester.accountId); + const count = hides && hides.size || 0; - if (requester.accountId === target.accountId) { - saySystem(requester, `Cannot hide yourself`); - } else if (requester.party && includes(requester.party.clients, target)) { - this.notifications.addNotification(requester, simpleNotification(cannotHidePlayerInParty)); - } else if (isFriend(requester, target)) { - this.notifications.addNotification(requester, simpleNotification(cannotHideFriends)); - } else if (count >= HIDE_LIMIT) { - this.notifications.addNotification(requester, simpleNotification(hidePlayerLimit)); - } else { - this.notifications.addNotification(requester, { - id: 0, - name: target.pony.name || '', - entityId: target.pony.id, - message: `Are you sure you want to hide #NAME# ?`, - flags: NotificationFlags.Yes | NotificationFlags.No | (target.pony.nameBad ? NotificationFlags.NameBad : 0), - accept: () => this.confirmHide(requester, target, timeout), - }); - } - } - requestUnhideAll(requester: IClient) { - const unhideTimestamp = this.unhides.get(requester.accountId) || 0; + if (requester.accountId === target.accountId) { + saySystem(requester, `Cannot hide yourself`); + } else if (requester.party && includes(requester.party.clients, target)) { + this.notifications.addNotification(requester, simpleNotification(cannotHidePlayerInParty)); + } else if (isFriend(requester, target)) { + this.notifications.addNotification(requester, simpleNotification(cannotHideFriends)); + } else if (count >= HIDE_LIMIT) { + this.notifications.addNotification(requester, simpleNotification(hidePlayerLimit)); + } else { + this.notifications.addNotification(requester, { + id: 0, + name: target.pony.name || '', + entityId: target.pony.id, + message: `Are you sure you want to hide #NAME# ?`, + flags: NotificationFlags.Yes | NotificationFlags.No | (target.pony.nameBad ? NotificationFlags.NameBad : 0), + accept: () => this.confirmHide(requester, target, timeout), + }); + } + } + requestUnhideAll(requester: IClient) { + const unhideTimestamp = this.unhides.get(requester.accountId) || 0; - if (unhideTimestamp > Date.now()) { - this.notifications.addNotification(requester, simpleNotification(unhideAllLimit, unhideAllLimitNote)); - } else { - this.notifications.addNotification(requester, { - id: 0, - name: '', - message: 'Are you sure you want to unhide all temporarily hidden players ?', - note: 'You can only do this once per hour. This action will require re-joining the game.', - flags: NotificationFlags.Yes | NotificationFlags.No, - accept: () => this.unhideAll(requester), - }); - } - } - confirmHide(requester: IClient, target: IClient, timeout: number) { - if (this.hide(requester, target, timeout)) { - let message = `${requester.characterName} (${requester.account.name}) hides ${clientInfo(target)}`; + if (unhideTimestamp > Date.now()) { + this.notifications.addNotification(requester, simpleNotification(unhideAllLimit, unhideAllLimitNote)); + } else { + this.notifications.addNotification(requester, { + id: 0, + name: '', + message: 'Are you sure you want to unhide all temporarily hidden players ?', + note: 'You can only do this once per hour. This action will require re-joining the game.', + flags: NotificationFlags.Yes | NotificationFlags.No, + accept: () => this.unhideAll(requester), + }); + } + } + confirmHide(requester: IClient, target: IClient, timeout: number) { + if (this.hide(requester, target, timeout)) { + let message = `${requester.characterName} (${requester.account.name}) hides ${clientInfo(target)}`; - if (timeout === 0) { - message += ' (permanent)'; - } + if (timeout === 0) { + message += ' (permanent)'; + } - this.log(systemMessage(requester.accountId, message)); - } - } - private isHiddenInner(who: string, from: string): boolean { - const hides = this.hides.get(who); - return hides !== undefined && hides.has(from); - } - isHidden(who: string, from: string): boolean { - return this.isHiddenInner(who, from) || this.isHiddenInner(from, who); - } - isHiddenClient(who: IClient, from: IClient) { - return this.isHidden(who.accountId, from.accountId); - } - hide(byClient: IClient, whoClient: IClient, timeout: number) { - const by = byClient.accountId; - const who = whoClient.accountId; + this.log(systemMessage(requester.accountId, message)); + } + } + private isHiddenInner(who: string, from: string): boolean { + const hides = this.hides.get(who); + return hides !== undefined && hides.has(from); + } + isHidden(who: string, from: string): boolean { + return this.isHiddenInner(who, from) || this.isHiddenInner(from, who); + } + isHiddenClient(who: IClient, from: IClient) { + return this.isHidden(who.accountId, from.accountId); + } + hide(byClient: IClient, whoClient: IClient, timeout: number) { + const by = byClient.accountId; + const who = whoClient.accountId; - if (timeout === 0) { // permanent - addHide(by, who, getEntityName(whoClient.pony, byClient) || '[none]') - .then(() => { - byClient.permaHides.add(who); - this.notify([{ by, who }]); - }) - .catch(e => logger.error(e)); - return true; - } else { + if (timeout === 0) { // permanent + addHide(by, who, getEntityName(whoClient.pony, byClient) || '[none]') + .then(() => { + byClient.permaHides.add(who); + this.notify([{ by, who }]); + }) + .catch(e => logger.error(e)); + return true; + } else { - if (by === who) - return false; + if (by === who) + return false; - if (this.isHiddenInner(by, who)) - return false; + if (this.isHiddenInner(by, who)) + return false; - const hides = this.hides.get(by) || new Map(); - hides.set(who, Date.now() + timeout); - this.hides.set(by, hides); - byClient.hides.add(who); - this.notify([{ by, who }]); - return true; - } - } - // TODO: remove ? - unhide(byClient: IClient, whoClient: IClient) { - const by = byClient.accountId; - const who = whoClient.accountId; - const hides = this.hides.get(by); + const hides = this.hides.get(by) || new Map(); + hides.set(who, Date.now() + timeout); + this.hides.set(by, hides); + byClient.hides.add(who); + this.notify([{ by, who }]); + return true; + } + } + // TODO: remove ? + unhide(byClient: IClient, whoClient: IClient) { + const by = byClient.accountId; + const who = whoClient.accountId; + const hides = this.hides.get(by); - if (hides) { - if (hides.has(who)) { - hides.delete(who); + if (hides) { + if (hides.has(who)) { + hides.delete(who); - if (hides.size === 0) { - this.hides.delete(by); - } + if (hides.size === 0) { + this.hides.delete(by); + } - byClient.hides.delete(who); + byClient.hides.delete(who); - this.notify([{ by, who }]); - } - } - } - unhideAll(byClient: IClient) { - const by = byClient.accountId; + this.notify([{ by, who }]); + } + } + } + unhideAll(byClient: IClient) { + const by = byClient.accountId; - if (this.unhides.has(by)) - return; + if (this.unhides.has(by)) + return; - const hides = this.hides.get(by); + const hides = this.hides.get(by); - if (hides) { - const notify: Hide[] = []; - hides.forEach((_, who) => notify.push({ by, who })); - this.hides.delete(by); - this.unhides.set(by, Date.now() + this.clearUnhides); - byClient.hides.clear(); - this.notify(notify); - this.unhidesAll.next(by); - } + if (hides) { + const notify: Hide[] = []; + hides.forEach((_, who) => notify.push({ by, who })); + this.hides.delete(by); + this.unhides.set(by, Date.now() + this.clearUnhides); + byClient.hides.clear(); + this.notify(notify); + this.unhidesAll.next(by); + } - this.log(systemMessage(by, 'unhide all')); - } - merged(target: string, merge: string) { - const targetHides = this.hides.get(target); - const mergeHides = this.hides.get(merge); - const notify: Hide[] = []; + this.log(systemMessage(by, 'unhide all')); + } + merged(target: string, merge: string) { + const targetHides = this.hides.get(target); + const mergeHides = this.hides.get(merge); + const notify: Hide[] = []; - if (targetHides) { - targetHides.delete(merge); - } + if (targetHides) { + targetHides.delete(merge); + } - if (mergeHides) { - const targetClient = this.findClient(target); - mergeHides.delete(target); + if (mergeHides) { + const targetClient = this.findClient(target); + mergeHides.delete(target); - if (targetHides) { - for (const id of Array.from(mergeHides.keys())) { - const who = targetHides.get(id); - targetHides.set(id, Math.max(who || 0, mergeHides.get(id)!)); - targetClient && targetClient.hides.add(id); + if (targetHides) { + for (const id of Array.from(mergeHides.keys())) { + const who = targetHides.get(id); + targetHides.set(id, Math.max(who || 0, mergeHides.get(id)!)); + targetClient && targetClient.hides.add(id); - if (!who) { - notify.push({ by: target, who: id }); - notify.push({ by: merge, who: id }); - } - } - } else { - this.hides.set(target, mergeHides); + if (!who) { + notify.push({ by: target, who: id }); + notify.push({ by: merge, who: id }); + } + } + } else { + this.hides.set(target, mergeHides); - for (const id of Array.from(mergeHides.keys())) { - targetClient && targetClient.hides.add(id); - notify.push({ by: target, who: id }); - notify.push({ by: merge, who: id }); - } - } + for (const id of Array.from(mergeHides.keys())) { + targetClient && targetClient.hides.add(id); + notify.push({ by: target, who: id }); + notify.push({ by: merge, who: id }); + } + } - this.hides.delete(merge); - } + this.hides.delete(merge); + } - const targetUnhides = this.unhides.get(target); - const mergeUnhides = this.unhides.get(merge); + const targetUnhides = this.unhides.get(target); + const mergeUnhides = this.unhides.get(merge); - if (mergeUnhides) { - this.unhides.set(target, Math.max(targetUnhides || 0, mergeUnhides)); - this.unhides.delete(merge); - } + if (mergeUnhides) { + this.unhides.set(target, Math.max(targetUnhides || 0, mergeUnhides)); + this.unhides.delete(merge); + } - this.hides.forEach((_, by) => { - const hides = this.hides.get(by)!; - const mergeHide = hides.get(merge); + this.hides.forEach((_, by) => { + const hides = this.hides.get(by)!; + const mergeHide = hides.get(merge); - if (mergeHide) { - hides.set(target, Math.max(mergeHide, hides.get(target) || 0)); - hides.delete(merge); - const client = this.findClient(by); + if (mergeHide) { + hides.set(target, Math.max(mergeHide, hides.get(target) || 0)); + hides.delete(merge); + const client = this.findClient(by); - if (client) { - client.hides.delete(merge); + if (client) { + client.hides.delete(merge); - if (target !== by) { - client && client.hides.add(target); - } - } + if (target !== by) { + client && client.hides.add(target); + } + } - notify.push({ by, who: target }); - notify.push({ by, who: merge }); - } - }); + notify.push({ by, who: target }); + notify.push({ by, who: merge }); + } + }); - this.notify(notify); - } - cleanup() { - const now = Date.now(); - const notify: Hide[] = []; + this.notify(notify); + } + cleanup() { + const now = Date.now(); + const notify: Hide[] = []; - for (const by of Array.from(this.hides.keys())) { - const hides = this.hides.get(by)!; + for (const by of Array.from(this.hides.keys())) { + const hides = this.hides.get(by)!; - for (const who of Array.from(hides.keys())) { - if (hides.get(who)! < now) { - hides.delete(who); - const client = this.findClient(by); - client && client.hides.delete(who); - notify.push({ by, who }); - } - } + for (const who of Array.from(hides.keys())) { + if (hides.get(who)! < now) { + hides.delete(who); + const client = this.findClient(by); + client && client.hides.delete(who); + notify.push({ by, who }); + } + } - if (hides.size === 0) { - this.hides.delete(by); - } - } + if (hides.size === 0) { + this.hides.delete(by); + } + } - this.notify(notify); + this.notify(notify); - for (const key of Array.from(this.unhides.keys())) { - if (this.unhides.get(key)! < now) { - this.unhides.delete(key); - } - } - } - start() { - this.interval = this.interval || setInterval(() => this.cleanup(), 10 * MINUTE); - } - stop() { - clearInterval(this.interval); - this.interval = undefined; - } - private notify(hides: Hide[]) { - for (const hide of hides) { - this.changes.next(hide); - } - } + for (const key of Array.from(this.unhides.keys())) { + if (this.unhides.get(key)! < now) { + this.unhides.delete(key); + } + } + } + start() { + this.interval = this.interval || setInterval(() => this.cleanup(), 10 * MINUTE); + } + stop() { + clearInterval(this.interval); + this.interval = undefined; + } + private notify(hides: Hide[]) { + for (const hide of hides) { + this.changes.next(hide); + } + } } diff --git a/src/ts/server/services/liveList.ts b/src/ts/server/services/liveList.ts index e14299d..bf2892c 100644 --- a/src/ts/server/services/liveList.ts +++ b/src/ts/server/services/liveList.ts @@ -9,192 +9,192 @@ const tickInterval = 1000; type Listener = (id: string, item: any) => void; export interface LiveListConfig { - fields: (keyof T)[]; - noStore?: boolean; - ignore?: (item: T) => boolean; - clean: (item: T) => Partial; // clean before sending to client - fix?: (item: T) => void; // fix after getting from DB - onSubscribeToMissing?: (id: string) => T; - // events - onAdd?: (item: T) => void; - onUpdate?: (oldItem: T, newItem: T) => void; - onDelete?: (item: T) => void; - onFinished?: () => void; - onAddedOrUpdated?: () => void; + fields: (keyof T)[]; + noStore?: boolean; + ignore?: (item: T) => boolean; + clean: (item: T) => Partial; // clean before sending to client + fix?: (item: T) => void; // fix after getting from DB + onSubscribeToMissing?: (id: string) => T; + // events + onAdd?: (item: T) => void; + onUpdate?: (oldItem: T, newItem: T) => void; + onDelete?: (item: T) => void; + onFinished?: () => void; + onAddedOrUpdated?: () => void; } function fixDocumentId(item: T) { - item._id = item._id.toString(); - return item; + item._id = item._id.toString(); + return item; } export class LiveList { - items: T[] = []; - private itemsMap = new Map(); - private listeners = new Map(); - private timestamp = new Date(0); - private finished = false; - private running = false; - private timeout: any; - private fieldsString: string; - constructor( - private model: Model, - private config: LiveListConfig, - private getId = (item: T) => item._id, - private logger = defaultLogger, - ) { - this.fieldsString = config.fields.join(' '); - } - get loaded() { - return this.finished; - } - start() { - if (this.config.noStore) { - this.timestamp = new Date(); - } + items: T[] = []; + private itemsMap = new Map(); + private listeners = new Map(); + private timestamp = new Date(0); + private finished = false; + private running = false; + private timeout: any; + private fieldsString: string; + constructor( + private model: Model, + private config: LiveListConfig, + private getId = (item: T) => item._id, + private logger = defaultLogger, + ) { + this.fieldsString = config.fields.join(' '); + } + get loaded() { + return this.finished; + } + start() { + if (this.config.noStore) { + this.timestamp = new Date(); + } - this.running = true; - this.tick(); - } - stop() { - this.running = false; - clearTimeout(this.timeout); - } - get(id: string) { - return this.itemsMap.get(id); - } - for(id: string | undefined, callback: (item: T) => void) { - const item = id ? this.get(id) : undefined; - item && callback(item); - } - add(item: T) { - const id = this.getId(item); - this.items.push(item); - this.itemsMap.set(id, item); + this.running = true; + this.tick(); + } + stop() { + this.running = false; + clearTimeout(this.timeout); + } + get(id: string) { + return this.itemsMap.get(id); + } + for(id: string | undefined, callback: (item: T) => void) { + const item = id ? this.get(id) : undefined; + item && callback(item); + } + add(item: T) { + const id = this.getId(item); + this.items.push(item); + this.itemsMap.set(id, item); - if (this.config.onAdd) { - this.config.onAdd(item); - } + if (this.config.onAdd) { + this.config.onAdd(item); + } - this.trigger(id, item); - return item; - } - // NOTE: only _id - async remove(id: string) { - await this.model.deleteOne({ _id: id }).exec(); - this.removed(id); - } - removed(id: string) { - const item = this.get(id); + this.trigger(id, item); + return item; + } + // NOTE: only _id + async remove(id: string) { + await this.model.deleteOne({ _id: id }).exec(); + this.removed(id); + } + removed(id: string) { + const item = this.get(id); - if (item) { - this.trigger(id, undefined); - this.itemsMap.delete(id); - removeItem(this.items, item); - this.config.onDelete && this.config.onDelete(item); - } - } - discard(id: string) { - const item = this.get(id); + if (item) { + this.trigger(id, undefined); + this.itemsMap.delete(id); + removeItem(this.items, item); + this.config.onDelete && this.config.onDelete(item); + } + } + discard(id: string) { + const item = this.get(id); - if (item) { - this.itemsMap.delete(id); - removeItem(this.items, item); - } - } - trigger(id: string, item: T | undefined) { - const listeners = this.listeners.get(id); + if (item) { + this.itemsMap.delete(id); + removeItem(this.items, item); + } + } + trigger(id: string, item: T | undefined) { + const listeners = this.listeners.get(id); - if (listeners) { - const cleaned = item ? this.config.clean(item) : item; - listeners.forEach(listener => listener(id, cleaned as any)); - } - } - subscribe(id: string, listener: Listener) { - const listeners = this.listeners.get(id) || []; - listeners.push(listener); - this.listeners.set(id, listeners); + if (listeners) { + const cleaned = item ? this.config.clean(item) : item; + listeners.forEach(listener => listener(id, cleaned as any)); + } + } + subscribe(id: string, listener: Listener) { + const listeners = this.listeners.get(id) || []; + listeners.push(listener); + this.listeners.set(id, listeners); - const item = this.get(id); + const item = this.get(id); - if (item) { - listener(id, this.config.clean(item)); - } else if (this.config.onSubscribeToMissing) { - this.add(this.config.onSubscribeToMissing(id)); - } + if (item) { + listener(id, this.config.clean(item)); + } else if (this.config.onSubscribeToMissing) { + this.add(this.config.onSubscribeToMissing(id)); + } - return { - unsubscribe: () => { - const listeners = this.listeners.get(id) || []; - removeItem(listeners, listener); + return { + unsubscribe: () => { + const listeners = this.listeners.get(id) || []; + removeItem(listeners, listener); - if (listeners.length === 0) { - this.listeners.delete(id); - } - } - }; - } - hasSubscriptions(id: string) { - return !!this.listeners.get(id); - } - async tick() { - if (this.running) { - clearTimeout(this.timeout); + if (listeners.length === 0) { + this.listeners.delete(id); + } + } + }; + } + hasSubscriptions(id: string) { + return !!this.listeners.get(id); + } + async tick() { + if (this.running) { + clearTimeout(this.timeout); - try { - await this.update(); - } catch (e) { - this.logger.error(e); - } finally { - this.timeout = setTimeout(() => this.tick(), tickInterval); - } - } - } - async fetch(search: any) { - await this.internalUpdate(search, true); - } - async update() { - await this.internalUpdate({ updatedAt: { $gt: this.timestamp } }, false); - } - private async internalUpdate(search: any, fetching: boolean) { - const query = this.model.find(search, this.fieldsString); - const applyUpdate = this.config.onUpdate || Object.assign; - let addedOrUpdated = false; + try { + await this.update(); + } catch (e) { + this.logger.error(e); + } finally { + this.timeout = setTimeout(() => this.tick(), tickInterval); + } + } + } + async fetch(search: any) { + await this.internalUpdate(search, true); + } + async update() { + await this.internalUpdate({ updatedAt: { $gt: this.timestamp } }, false); + } + private async internalUpdate(search: any, fetching: boolean) { + const query = this.model.find(search, this.fieldsString); + const applyUpdate = this.config.onUpdate || Object.assign; + let addedOrUpdated = false; - await iterate(query.lean(), update => { - try { - fixDocumentId(update); + await iterate(query.lean(), update => { + try { + fixDocumentId(update); - if (this.config.fix) { - this.config.fix(update); - } + if (this.config.fix) { + this.config.fix(update); + } - if (!fetching) { - this.timestamp = maxDate(this.timestamp, update.updatedAt)!; - } + if (!fetching) { + this.timestamp = maxDate(this.timestamp, update.updatedAt)!; + } - const doc = this.get(this.getId(update)); + const doc = this.get(this.getId(update)); - if (doc !== undefined) { - applyUpdate(doc, update); - this.trigger(this.getId(doc), doc); - } else if (fetching || !(this.config.ignore && this.config.ignore(update))) { - this.add(update); - } + if (doc !== undefined) { + applyUpdate(doc, update); + this.trigger(this.getId(doc), doc); + } else if (fetching || !(this.config.ignore && this.config.ignore(update))) { + this.add(update); + } - addedOrUpdated = true; - } catch (e) { - console.error(e); - } - }); + addedOrUpdated = true; + } catch (e) { + console.error(e); + } + }); - if (addedOrUpdated && this.config.onAddedOrUpdated) { - this.config.onAddedOrUpdated(); - } + if (addedOrUpdated && this.config.onAddedOrUpdated) { + this.config.onAddedOrUpdated(); + } - if (!this.finished) { - this.finished = true; - this.config.onFinished && this.config.onFinished(); - } - } + if (!this.finished) { + this.finished = true; + this.config.onFinished && this.config.onFinished(); + } + } } diff --git a/src/ts/server/services/notification.ts b/src/ts/server/services/notification.ts index 095f3af..acadc3c 100644 --- a/src/ts/server/services/notification.ts +++ b/src/ts/server/services/notification.ts @@ -4,68 +4,68 @@ import { IClient, ServerNotification } from '../serverInterfaces'; const NOTIFICATION_LIMIT = 10; function getId(notifications: ServerNotification[]) { - for (let id = 1; id <= 0xffff; id++) { - if (!findById(notifications, id)) { - return id; - } - } + for (let id = 1; id <= 0xffff; id++) { + if (!findById(notifications, id)) { + return id; + } + } - /* istanbul ignore next */ - throw new Error('Unable to get unique id for notification'); + /* istanbul ignore next */ + throw new Error('Unable to get unique id for notification'); } function hasNotification(client: IClient, notification: ServerNotification) { - return client.notifications.some(n => - n.message === notification.message && - n.flags === notification.flags && - n.note === notification.note && - n.sender === notification.sender && - n.entityId === notification.entityId); + return client.notifications.some(n => + n.message === notification.message && + n.flags === notification.flags && + n.note === notification.note && + n.sender === notification.sender && + n.entityId === notification.entityId); } export class NotificationService { - addNotification(client: IClient, notification: ServerNotification) { - if (client.notifications.length >= NOTIFICATION_LIMIT || hasNotification(client, notification)) { - return 0; - } else { - notification.id = getId(client.notifications); - client.notifications.push(notification); - const { id, entityId = 0, name, message, note = '', flags = 0 } = notification; - client.addNotification(id, entityId, name, message, note, flags); - return notification.id; - } - } - removeNotification(client: IClient, id: number) { - if (removeById(client.notifications, id)) { - client.removeNotification(id); - return true; - } else { - return false; - } - } - acceptNotification(client: IClient, id: number) { - const notification = findById(client.notifications, id); - this.removeNotification(client, id); + addNotification(client: IClient, notification: ServerNotification) { + if (client.notifications.length >= NOTIFICATION_LIMIT || hasNotification(client, notification)) { + return 0; + } else { + notification.id = getId(client.notifications); + client.notifications.push(notification); + const { id, entityId = 0, name, message, note = '', flags = 0 } = notification; + client.addNotification(id, entityId, name, message, note, flags); + return notification.id; + } + } + removeNotification(client: IClient, id: number) { + if (removeById(client.notifications, id)) { + client.removeNotification(id); + return true; + } else { + return false; + } + } + acceptNotification(client: IClient, id: number) { + const notification = findById(client.notifications, id); + this.removeNotification(client, id); - if (notification && notification.accept) { - notification.accept(); - } - } - rejectNotification(client: IClient, id: number) { - const notification = findById(client.notifications, id); - this.removeNotification(client, id); + if (notification && notification.accept) { + notification.accept(); + } + } + rejectNotification(client: IClient, id: number) { + const notification = findById(client.notifications, id); + this.removeNotification(client, id); - if (notification && notification.reject) { - notification.reject(); - } - } - rejectAll(client: IClient) { - client.notifications.slice() - .forEach(n => this.rejectNotification(client, n.id)); - } - dismissAll(client: IClient) { - while (client.notifications.length) { - this.removeNotification(client, client.notifications[0].id); - } - } + if (notification && notification.reject) { + notification.reject(); + } + } + rejectAll(client: IClient) { + client.notifications.slice() + .forEach(n => this.rejectNotification(client, n.id)); + } + dismissAll(client: IClient) { + while (client.notifications.length) { + this.removeNotification(client, client.notifications[0].id); + } + } } diff --git a/src/ts/server/services/observableList.ts b/src/ts/server/services/observableList.ts index faa26f6..d096af5 100644 --- a/src/ts/server/services/observableList.ts +++ b/src/ts/server/services/observableList.ts @@ -3,46 +3,46 @@ import { ListListener, IObservableList } from '../../common/adminInterfaces'; import { pushOrdered } from '../../common/adminUtils'; export class ObservableList implements IObservableList { - private listeners: ListListener[] = []; - constructor(private list: T[], private map: (item: T) => V) { - } - hasSubscribers() { - return this.listeners.length > 0; - } - trigger() { - if (this.listeners.length) { - const items = this.list.map(this.map); + private listeners: ListListener[] = []; + constructor(private list: T[], private map: (item: T) => V) { + } + hasSubscribers() { + return this.listeners.length > 0; + } + trigger() { + if (this.listeners.length) { + const items = this.list.map(this.map); - for (const listener of this.listeners) { - listener(items); - } - } - } - push(item: T) { - this.list.push(item); - this.trigger(); - } - pushOrdered(item: T, compare: (a: T, b: T) => number) { - pushOrdered(this.list, item, compare); - this.trigger(); - } - remove(item: T) { - const removed = removeItem(this.list, item); - this.trigger(); - return removed; - } - replace(list: T[]) { - this.list = list; - this.trigger(); - } - subscribe(listener: ListListener) { - this.listeners.push(listener); - this.trigger(); + for (const listener of this.listeners) { + listener(items); + } + } + } + push(item: T) { + this.list.push(item); + this.trigger(); + } + pushOrdered(item: T, compare: (a: T, b: T) => number) { + pushOrdered(this.list, item, compare); + this.trigger(); + } + remove(item: T) { + const removed = removeItem(this.list, item); + this.trigger(); + return removed; + } + replace(list: T[]) { + this.list = list; + this.trigger(); + } + subscribe(listener: ListListener) { + this.listeners.push(listener); + this.trigger(); - return { - unsubscribe: () => { - removeItem(this.listeners, listener); - } - }; - } + return { + unsubscribe: () => { + removeItem(this.listeners, listener); + } + }; + } } diff --git a/src/ts/server/services/party.ts b/src/ts/server/services/party.ts index 3465e88..9f8bbe9 100644 --- a/src/ts/server/services/party.ts +++ b/src/ts/server/services/party.ts @@ -16,302 +16,302 @@ export const INVITE_REJECTED_LIMIT = 5; export const INVITE_REJECTED_TIMEOUT = 1 * HOUR; function toPartyMember(client: IClient, pending: boolean, leader: boolean): [number, PartyFlags] { - const flags = (pending ? PartyFlags.Pending : 0) - | (leader ? PartyFlags.Leader : 0) - | (client.offline ? PartyFlags.Offline : 0); + const flags = (pending ? PartyFlags.Pending : 0) + | (leader ? PartyFlags.Leader : 0) + | (client.offline ? PartyFlags.Offline : 0); - return [client.pony.id, flags]; + return [client.pony.id, flags]; } function findClientInParties(parties: ServerParty[], accountId: string) { - for (const party of parties) { - for (let index = 0; index < party.clients.length; index++) { - if (party.clients[index].accountId === accountId) { - return { party, index }; - } - } - } + for (const party of parties) { + for (let index = 0; index < party.clients.length; index++) { + if (party.clients[index].accountId === accountId) { + return { party, index }; + } + } + } - return { party: undefined, index: 0 }; + return { party: undefined, index: 0 }; } export class PartyService { - parties: ServerParty[] = []; - partyChanged = new Subject(); - private id = 0; - private limiter = new ActionLimiter(INVITE_REJECTED_TIMEOUT, INVITE_REJECTED_LIMIT); - constructor( - private notificationService: NotificationService, - private reportInviteLimit: (client: IClient) => void, - ) { - } - dispose() { - this.limiter.dispose(); - } - clientConnected(client: IClient) { - const { party, index } = findClientInParties(this.parties, client.accountId); + parties: ServerParty[] = []; + partyChanged = new Subject(); + private id = 0; + private limiter = new ActionLimiter(INVITE_REJECTED_TIMEOUT, INVITE_REJECTED_LIMIT); + constructor( + private notificationService: NotificationService, + private reportInviteLimit: (client: IClient) => void, + ) { + } + dispose() { + this.limiter.dispose(); + } + clientConnected(client: IClient) { + const { party, index } = findClientInParties(this.parties, client.accountId); - if (party) { - const existing = party.clients[index]; - party.clients[index] = client; - client.party = party; + if (party) { + const existing = party.clients[index]; + party.clients[index] = client; + client.party = party; - if (party.leader === existing) { - party.leader = client; - clearTimeout(party.leaderTimeout); - } + if (party.leader === existing) { + party.leader = client; + clearTimeout(party.leaderTimeout); + } - existing.party = undefined; - existing.offlineAt = new Date(); + existing.party = undefined; + existing.offlineAt = new Date(); - this.sendPartyUpdateToAll(party); - } - } - clientDisconnected(client: IClient) { - const party = client.party; + this.sendPartyUpdateToAll(party); + } + } + clientDisconnected(client: IClient) { + const party = client.party; - if (party) { - this.sendPartyUpdateToAll(party); + if (party) { + this.sendPartyUpdateToAll(party); - party.leaderTimeout = setTimeout(() => { - const newLeader = party.clients.find(c => c !== client && !c.offline); + party.leaderTimeout = setTimeout(() => { + const newLeader = party.clients.find(c => c !== client && !c.offline); - if (newLeader) { - this.promoteLeader(client, newLeader); - } else { - this.destroyParty(party); - } - }, LEADER_TIMEOUT); - } else { - const pendingParty = this.parties.find(p => p.pending.some(x => x.client === client)); + if (newLeader) { + this.promoteLeader(client, newLeader); + } else { + this.destroyParty(party); + } + }, LEADER_TIMEOUT); + } else { + const pendingParty = this.parties.find(p => p.pending.some(x => x.client === client)); - if (pendingParty) { - remove(pendingParty.pending, x => x.client === client); - this.sendPartyUpdateToAll(pendingParty); - } - } - } - remove(leader: IClient, client: IClient) { - const party = leader.party; + if (pendingParty) { + remove(pendingParty.pending, x => x.client === client); + this.sendPartyUpdateToAll(pendingParty); + } + } + } + remove(leader: IClient, client: IClient) { + const party = leader.party; - if (!party || party.leader !== leader) - return; + if (!party || party.leader !== leader) + return; - if (includes(party.clients, client)) { - removeItem(party.clients, client); - client.party = undefined; + if (includes(party.clients, client)) { + removeItem(party.clients, client); + client.party = undefined; - if (party.leader === client && party.clients[0]) { - party.leader = party.clients[0]; - } + if (party.leader === client && party.clients[0]) { + party.leader = party.clients[0]; + } - client.updateParty(undefined); - this.sendPartyUpdateToAll(party); - this.partyChanged.next(client); - } else { - const pending = party.pending.find(p => p.client === client); + client.updateParty(undefined); + this.sendPartyUpdateToAll(party); + this.partyChanged.next(client); + } else { + const pending = party.pending.find(p => p.client === client); - if (pending) { - leader.reporter.systemLog(`Invite cancelled for [${client.accountId}]`); - removeItem(party.pending, pending); - this.notificationService.removeNotification(pending.client, pending.notificationId); - this.sendPartyUpdateToAll(party); - this.countReject(leader); - } - } + if (pending) { + leader.reporter.systemLog(`Invite cancelled for [${client.accountId}]`); + removeItem(party.pending, pending); + this.notificationService.removeNotification(pending.client, pending.notificationId); + this.sendPartyUpdateToAll(party); + this.countReject(leader); + } + } - this.cleanupParty(party); - } - invite(leader: IClient, client: IClient) { - let party = leader.party; + this.cleanupParty(party); + } + invite(leader: IClient, client: IClient) { + let party = leader.party; - const can = this.limiter.canExecute(leader, client); + const can = this.limiter.canExecute(leader, client); - if (can === LimiterResult.LimitReached) { - return saySystem(leader, 'Reached invite rejection limit'); - } else if (can !== LimiterResult.Yes) { - return saySystem(leader, 'Cannot invite'); - } + if (can === LimiterResult.LimitReached) { + return saySystem(leader, 'Reached invite rejection limit'); + } else if (can !== LimiterResult.Yes) { + return saySystem(leader, 'Cannot invite'); + } - if (client.shadowed) - return saySystem(leader, 'Cannot invite'); + if (client.shadowed) + return saySystem(leader, 'Cannot invite'); - if (hasFlag(leader.account.flags, AccountFlags.BlockPartyInvites)) - return saySystem(leader, 'Cannot invite'); + if (hasFlag(leader.account.flags, AccountFlags.BlockPartyInvites)) + return saySystem(leader, 'Cannot invite'); - if (party && party.leader !== leader) - return saySystem(leader, 'You need to be party leader'); + if (party && party.leader !== leader) + return saySystem(leader, 'You need to be party leader'); - if (party && (party.clients.length + party.pending.length) >= PARTY_LIMIT) - return saySystem(leader, 'Party is full'); + if (party && (party.clients.length + party.pending.length) >= PARTY_LIMIT) + return saySystem(leader, 'Party is full'); - if (client.party) - return saySystem(leader, 'Already in a party'); + if (client.party) + return saySystem(leader, 'Already in a party'); - if (party && party.pending.some(p => p.client === client)) - return saySystem(leader, 'Already invited'); + if (party && party.pending.some(p => p.client === client)) + return saySystem(leader, 'Already invited'); - if (client.accountSettings.ignorePartyInvites && !isFriend(client, leader)) - return saySystem(leader, 'Cannot invite'); + if (client.accountSettings.ignorePartyInvites && !isFriend(client, leader)) + return saySystem(leader, 'Cannot invite'); - if (this.parties.reduce((sum, p) => sum + p.pending.filter(x => x.client === client).length, 0) >= INVITE_LIMIT) - return saySystem(leader, 'Too many pending invites'); + if (this.parties.reduce((sum, p) => sum + p.pending.filter(x => x.client === client).length, 0) >= INVITE_LIMIT) + return saySystem(leader, 'Too many pending invites'); - const partyExisted = !!leader.party; + const partyExisted = !!leader.party; - if (!partyExisted) { - party = this.createParty(leader); - } + if (!partyExisted) { + party = this.createParty(leader); + } - /* istanbul ignore next */ - if (!party) - throw new Error(`Party not created`); + /* istanbul ignore next */ + if (!party) + throw new Error(`Party not created`); - const notificationId = this.addInviteNotification(client, leader, party); + const notificationId = this.addInviteNotification(client, leader, party); - if (!notificationId) { - if (!partyExisted) { - leader.party = undefined; - removeItem(this.parties, party); - } + if (!notificationId) { + if (!partyExisted) { + leader.party = undefined; + removeItem(this.parties, party); + } - return saySystem(leader, 'Cannot invite'); - } + return saySystem(leader, 'Cannot invite'); + } - party.pending.push({ client, notificationId }); - this.sendPartyUpdateToAll(party); - leader.reporter.systemLog(`Invite to party [${client.accountId}]`); + party.pending.push({ client, notificationId }); + this.sendPartyUpdateToAll(party); + leader.reporter.systemLog(`Invite to party [${client.accountId}]`); - if (!partyExisted) { - this.partyChanged.next(leader); - } - } - leave(client: IClient) { - if (client.party) { - this.remove(client.party.leader, client); - } - } - promoteLeader(leader: IClient, client: IClient) { - const party = leader.party; + if (!partyExisted) { + this.partyChanged.next(leader); + } + } + leave(client: IClient) { + if (client.party) { + this.remove(client.party.leader, client); + } + } + promoteLeader(leader: IClient, client: IClient) { + const party = leader.party; - if (!party) - return; + if (!party) + return; - if (leader === client) - return; + if (leader === client) + return; - if (client.offline) - return saySystem(leader, 'Player is offline'); + if (client.offline) + return saySystem(leader, 'Player is offline'); - if (party.leader !== leader) - return saySystem(leader, 'You need to be party leader'); + if (party.leader !== leader) + return saySystem(leader, 'You need to be party leader'); - if (!includes(party.clients, client)) - return saySystem(leader, 'Not in the party'); + if (!includes(party.clients, client)) + return saySystem(leader, 'Not in the party'); - party.leader = client; - this.sendPartyUpdateToAll(party); - } - cleanupParties() { - const now = Date.now(); + party.leader = client; + this.sendPartyUpdateToAll(party); + } + cleanupParties() { + const now = Date.now(); - for (let i = this.parties.length - 1; i >= 0; i--) { - const party = this.parties[i]; + for (let i = this.parties.length - 1; i >= 0; i--) { + const party = this.parties[i]; - if (party.clients.every(c => c.offline)) { - party.cleanup = party.cleanup || now; + if (party.clients.every(c => c.offline)) { + party.cleanup = party.cleanup || now; - if ((now - party.cleanup) > (10 * SECOND)) { - this.destroyParty(party); - } - } else if (party.cleanup !== undefined) { - party.cleanup = undefined; - } - } - } - private createParty(leader: IClient) { - const party: ServerParty = { - id: `party-${this.id++}`, - leader, - clients: [leader], - pending: [], - }; + if ((now - party.cleanup) > (10 * SECOND)) { + this.destroyParty(party); + } + } else if (party.cleanup !== undefined) { + party.cleanup = undefined; + } + } + } + private createParty(leader: IClient) { + const party: ServerParty = { + id: `party-${this.id++}`, + leader, + clients: [leader], + pending: [], + }; - leader.party = party; - this.parties.push(party); - return party; - } - private destroyParty(party: ServerParty) { - const clients = party.clients; - clients.forEach(c => c.party = undefined); - clients.forEach(c => c.updateParty(undefined)); - party.pending.forEach(p => this.notificationService.removeNotification(p.client, p.notificationId)); - party.clients = []; - party.pending = []; - removeItem(this.parties, party); - clients.forEach(c => this.partyChanged.next(c)); - } - private sendPartyUpdate(client: IClient, party: ServerParty) { - const clients = party.clients.map(c => toPartyMember(c, false, c === party.leader)); - const pending = party.pending.map(c => toPartyMember(c.client, true, false)); - client.updateParty([...clients, ...pending]); - } - private sendPartyUpdateToAll(party: ServerParty) { - party.clients - .filter(c => !c.offline) - .forEach(c => this.sendPartyUpdate(c, party)); - } - private cleanupParty(party: ServerParty) { - if (party.clients.length === 0 || (party.clients.length + party.pending.length) <= 1) { - this.destroyParty(party); - } - } - private acceptInvitation(party: ServerParty, client: IClient, invitedBy: IClient) { - const removed = remove(party.pending, p => p.client === client)[0]; + leader.party = party; + this.parties.push(party); + return party; + } + private destroyParty(party: ServerParty) { + const clients = party.clients; + clients.forEach(c => c.party = undefined); + clients.forEach(c => c.updateParty(undefined)); + party.pending.forEach(p => this.notificationService.removeNotification(p.client, p.notificationId)); + party.clients = []; + party.pending = []; + removeItem(this.parties, party); + clients.forEach(c => this.partyChanged.next(c)); + } + private sendPartyUpdate(client: IClient, party: ServerParty) { + const clients = party.clients.map(c => toPartyMember(c, false, c === party.leader)); + const pending = party.pending.map(c => toPartyMember(c.client, true, false)); + client.updateParty([...clients, ...pending]); + } + private sendPartyUpdateToAll(party: ServerParty) { + party.clients + .filter(c => !c.offline) + .forEach(c => this.sendPartyUpdate(c, party)); + } + private cleanupParty(party: ServerParty) { + if (party.clients.length === 0 || (party.clients.length + party.pending.length) <= 1) { + this.destroyParty(party); + } + } + private acceptInvitation(party: ServerParty, client: IClient, invitedBy: IClient) { + const removed = remove(party.pending, p => p.client === client)[0]; - if (!client.party && removed) { - party.leader.reporter.systemLog(`Invite accepted by [${client.accountId}]`); - party.clients.push(client); - client.party = party; - this.notificationService.removeNotification(client, removed.notificationId); - this.sendPartyUpdateToAll(party); + if (!client.party && removed) { + party.leader.reporter.systemLog(`Invite accepted by [${client.accountId}]`); + party.clients.push(client); + client.party = party; + this.notificationService.removeNotification(client, removed.notificationId); + this.sendPartyUpdateToAll(party); - this.parties - .filter(p => p.pending.some(x => x.client === client)) - .forEach(p => this.rejectInvitation(p, client, invitedBy)); + this.parties + .filter(p => p.pending.some(x => x.client === client)) + .forEach(p => this.rejectInvitation(p, client, invitedBy)); - this.partyChanged.next(client); - } - } - private rejectInvitation(party: ServerParty, client: IClient, invitedBy: IClient) { - const removed = remove(party.pending, p => p.client === client)[0]; + this.partyChanged.next(client); + } + } + private rejectInvitation(party: ServerParty, client: IClient, invitedBy: IClient) { + const removed = remove(party.pending, p => p.client === client)[0]; - if (removed) { - party.leader.reporter.systemLog(`Invite rejected by [${client.accountId}]`); - this.notificationService.removeNotification(client, removed.notificationId); - this.sendPartyUpdateToAll(party); - this.cleanupParty(party); - this.countReject(invitedBy); - } - } - private countReject(invitedBy: IClient) { - const count = this.limiter.count(invitedBy); + if (removed) { + party.leader.reporter.systemLog(`Invite rejected by [${client.accountId}]`); + this.notificationService.removeNotification(client, removed.notificationId); + this.sendPartyUpdateToAll(party); + this.cleanupParty(party); + this.countReject(invitedBy); + } + } + private countReject(invitedBy: IClient) { + const count = this.limiter.count(invitedBy); - if (count >= INVITE_REJECTED_LIMIT) { - this.reportInviteLimit(invitedBy); - } - } - private addInviteNotification(client: IClient, leader: IClient, party: ServerParty) { - return this.notificationService.addNotification(client, { - id: 0, - sender: leader, - name: leader.pony.name || '', - entityId: leader.pony.id, - message: `
Party invite
#NAME# invited you to a party`, - flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore | - (client.pony.nameBad ? NotificationFlags.NameBad : 0), - accept: () => this.acceptInvitation(party, client, leader), - reject: () => this.rejectInvitation(party, client, leader), - }); - } + if (count >= INVITE_REJECTED_LIMIT) { + this.reportInviteLimit(invitedBy); + } + } + private addInviteNotification(client: IClient, leader: IClient, party: ServerParty) { + return this.notificationService.addNotification(client, { + id: 0, + sender: leader, + name: leader.pony.name || '', + entityId: leader.pony.id, + message: `
Party invite
#NAME# invited you to a party`, + flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore | + (client.pony.nameBad ? NotificationFlags.NameBad : 0), + accept: () => this.acceptInvitation(party, client, leader), + reject: () => this.rejectInvitation(party, client, leader), + }); + } } diff --git a/src/ts/server/services/supporterInvites.ts b/src/ts/server/services/supporterInvites.ts index b8b8fb2..e3c24de 100644 --- a/src/ts/server/services/supporterInvites.ts +++ b/src/ts/server/services/supporterInvites.ts @@ -16,112 +16,112 @@ export const INVITE_REJECTED_TIMEOUT = 1 * HOUR; export const INVITE_REJECTED_LIMIT = 5; function formatMessage(requester: IClient, target: IClient, message: string) { - const requesterInfo = `${requester.characterName} (${requester.account.name})`; - const targetInfo = `${target.characterName} (${target.account.name}) [${target.accountId}]`; - return systemMessage(requester.accountId, `${requesterInfo} ${message} ${targetInfo}`); + const requesterInfo = `${requester.characterName} (${requester.account.name})`; + const targetInfo = `${target.characterName} (${target.account.name}) [${target.accountId}]`; + return systemMessage(requester.accountId, `${requesterInfo} ${message} ${targetInfo}`); } export class SupporterInvitesService { - private limiter = new ActionLimiter(INVITE_REJECTED_TIMEOUT, INVITE_REJECTED_LIMIT); - constructor( - private model: Model, - private notifications: NotificationService, - private log: (message: string) => void, - ) { - } - dispose() { - this.limiter.dispose(); - } - async getInvites(source: IClient): Promise { - const items = await this.model.find({ source: source.account._id }).exec(); - return items.map(({ _id, name, info, active }) => ({ id: _id.toString(), name, info, active })); - } - async isInvited(target: IClient): Promise { - const count = await this.model.countDocuments({ target: target.account._id, active: true }).exec(); - return count > 0; - } - async requestInvite(requester: IClient, target: IClient) { - const items = await this.getInvites(requester); - const limit = getSupporterInviteLimit(requester.account); + private limiter = new ActionLimiter(INVITE_REJECTED_TIMEOUT, INVITE_REJECTED_LIMIT); + constructor( + private model: Model, + private notifications: NotificationService, + private log: (message: string) => void, + ) { + } + dispose() { + this.limiter.dispose(); + } + async getInvites(source: IClient): Promise { + const items = await this.model.find({ source: source.account._id }).exec(); + return items.map(({ _id, name, info, active }) => ({ id: _id.toString(), name, info, active })); + } + async isInvited(target: IClient): Promise { + const count = await this.model.countDocuments({ target: target.account._id, active: true }).exec(); + return count > 0; + } + async requestInvite(requester: IClient, target: IClient) { + const items = await this.getInvites(requester); + const limit = getSupporterInviteLimit(requester.account); - if (items.length >= limit) - return saySystem(requester, 'Invite limit reached'); + if (items.length >= limit) + return saySystem(requester, 'Invite limit reached'); - if (this.limiter.canExecute(requester, target) !== LimiterResult.Yes) - return saySystem(requester, 'Cannot invite'); + if (this.limiter.canExecute(requester, target) !== LimiterResult.Yes) + return saySystem(requester, 'Cannot invite'); - this.log(formatMessage(requester, target, 'invited to supporter server')); + this.log(formatMessage(requester, target, 'invited to supporter server')); - this.notifications.addNotification(target, { - id: 0, - sender: requester, - name: requester.pony.name || '', - entityId: requester.pony.id, - message: `#NAME# invited you to supporter servers`, - flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore | - (requester.pony.nameBad ? NotificationFlags.NameBad : 0), - accept: () => this.acceptInvite(requester, target), - reject: () => this.rejectInvite(requester, target), - }); - } - acceptInvite(requester: IClient, target: IClient) { - this.log(formatMessage(requester, target, 'supporter invite accepted by')); - this.invite(requester, target); - } - rejectInvite(requester: IClient, target: IClient) { - this.log(formatMessage(requester, target, 'supporter invite rejected by')); - this.limiter.count(requester); - } - async invite(requester: IClient, target: IClient) { - const limit = getSupporterInviteLimit(requester.account); - const items = await this.getInvites(requester); + this.notifications.addNotification(target, { + id: 0, + sender: requester, + name: requester.pony.name || '', + entityId: requester.pony.id, + message: `#NAME# invited you to supporter servers`, + flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore | + (requester.pony.nameBad ? NotificationFlags.NameBad : 0), + accept: () => this.acceptInvite(requester, target), + reject: () => this.rejectInvite(requester, target), + }); + } + acceptInvite(requester: IClient, target: IClient) { + this.log(formatMessage(requester, target, 'supporter invite accepted by')); + this.invite(requester, target); + } + rejectInvite(requester: IClient, target: IClient) { + this.log(formatMessage(requester, target, 'supporter invite rejected by')); + this.limiter.count(requester); + } + async invite(requester: IClient, target: IClient) { + const limit = getSupporterInviteLimit(requester.account); + const items = await this.getInvites(requester); - if (items.length >= limit) { - throw new UserError('Invite limit reached'); - } + if (items.length >= limit) { + throw new UserError('Invite limit reached'); + } - await this.model.create({ - source: requester.account._id, - target: target.account._id, - name: target.characterName, - info: target.character.info, - active: true, - }); - } - uninvite(requester: IClient, inviteId: string) { - return Promise.resolve(this.model.deleteOne({ _id: inviteId, source: requester.account._id }).exec()); - } + await this.model.create({ + source: requester.account._id, + target: target.account._id, + name: target.characterName, + info: target.character.info, + active: true, + }); + } + uninvite(requester: IClient, inviteId: string) { + return Promise.resolve(this.model.deleteOne({ _id: inviteId, source: requester.account._id }).exec()); + } } type LeanInvite = ISupporterInvite & { source: IAccount }; export async function updateSupporterInvites(model: Model) { - const invites: LeanInvite[] = await model.find({}, '_id active') - .populate('source', '_id supporter patreon roles') - .lean() - .exec(); + const invites: LeanInvite[] = await model.find({}, '_id active') + .populate('source', '_id supporter patreon roles') + .lean() + .exec(); - const itemsBySource = toPairs(groupBy(invites, i => i.source._id as string)); - const itemsToUpdate = itemsBySource - .map(([_, items]) => { - const source = items[0].source; - const limit = getSupporterInviteLimit(source); + const itemsBySource = toPairs(groupBy(invites, i => i.source._id as string)); + const itemsToUpdate = itemsBySource + .map(([_, items]) => { + const source = items[0].source; + const limit = getSupporterInviteLimit(source); - return compact(items - .sort((a, b) => compareDates(a.createdAt, b.createdAt)) - .map((item, i) => { - const active = i < limit; - return item.active === active ? undefined : { id: item._id, active }; - })); - }); + return compact(items + .sort((a, b) => compareDates(a.createdAt, b.createdAt)) + .map((item, i) => { + const active = i < limit; + return item.active === active ? undefined : { id: item._id, active }; + })); + }); - const groups = toPairs(groupBy(flatten(itemsToUpdate), i => i.active)); + const groups = toPairs(groupBy(flatten(itemsToUpdate), i => i.active)); - await Promise.all(groups.map(([_, items]) => { - const active = items[0].active; - const ids = items.map(i => i.id); - return model.updateMany({ _id: { $in: ids } }, { active }).exec(); - })); + await Promise.all(groups.map(([_, items]) => { + const active = items[0].active; + const ids = items.map(i => i.id); + return model.updateMany({ _id: { $in: ids } }, { active }).exec(); + })); - await model.deleteMany({ active: false, updatedAt: { $lt: fromNow(-100 * DAY) } }).exec(); + await model.deleteMany({ active: false, updatedAt: { $lt: fromNow(-100 * DAY) } }).exec(); } diff --git a/src/ts/server/settings.ts b/src/ts/server/settings.ts index ff0aae0..89f9bf9 100644 --- a/src/ts/server/settings.ts +++ b/src/ts/server/settings.ts @@ -4,8 +4,8 @@ import { cloneDeep } from '../common/utils'; import * as paths from './paths'; const defaultSettings: Settings = { - canCreateAccounts: true, - servers: {}, + canCreateAccounts: true, + servers: {}, }; export const settings: Settings = cloneDeep(defaultSettings); @@ -14,46 +14,46 @@ const settingsPath = paths.pathTo('settings', `settings.json`); /* istanbul ignore next */ export async function loadSettings() { - try { - const json = await readFileAsync(settingsPath, 'utf8'); - return JSON.parse(json) as Settings; - } catch (e) { - if (e.code === 'ENOENT') { - try { - await mkdirAsync(paths.pathTo('settings')); - } catch (e2) { - if (e2.code !== 'EEXIST') throw e; - } - } else { - console.error('Error reading settings file: ' + e); - } + try { + const json = await readFileAsync(settingsPath, 'utf8'); + return JSON.parse(json) as Settings; + } catch (e) { + if (e.code === 'ENOENT') { + try { + await mkdirAsync(paths.pathTo('settings')); + } catch (e2) { + if (e2.code !== 'EEXIST') throw e; + } + } else { + console.error('Error reading settings file: ' + e); + } - return cloneDeep(defaultSettings); - } + return cloneDeep(defaultSettings); + } } /* istanbul ignore next */ export async function saveSettings(settings: Settings) { - const json = JSON.stringify(settings, undefined, 2); - await writeFileAsync(settingsPath, json, 'utf8'); + const json = JSON.stringify(settings, undefined, 2); + await writeFileAsync(settingsPath, json, 'utf8'); } /* istanbul ignore next */ export async function updateSettings(update: Partial) { - let settings = { ...defaultSettings }; + let settings = { ...defaultSettings }; - try { - settings = await loadSettings(); - } catch { } + try { + settings = await loadSettings(); + } catch { } - Object.assign(settings, update); - await saveSettings(settings); + Object.assign(settings, update); + await saveSettings(settings); } /* istanbul ignore next */ export async function reloadSettings() { - try { - const current = await loadSettings(); - Object.assign(settings, current); - } catch { } + try { + const current = await loadSettings(); + Object.assign(settings, current); + } catch { } } diff --git a/src/ts/server/spamChecker.ts b/src/ts/server/spamChecker.ts index 35772d2..e819143 100644 --- a/src/ts/server/spamChecker.ts +++ b/src/ts/server/spamChecker.ts @@ -23,126 +23,126 @@ export const RAPID_MESSAGE_COUNT = 35; export const RAPID_MESSAGE_TIMEOUT = 30 * SECOND; export const createSpamChecker = - ( - spamCounter: CounterService, rapidCounter: CounterService, countSpamming: ReportAccount, - timeoutAccount: TimeoutAccount, handlePromise = handlePromiseDefault, - ): OnMessageSettings => { - async function countAndTimeout( - client: IClient, timeout: boolean, message: string, items: string[], settings: GameServerSettings - ) { - const timeoutTime = fromNow(SPAM_TIMEOUT * (settings.doubleTimeouts ? 2 : 1)); + ( + spamCounter: CounterService, rapidCounter: CounterService, countSpamming: ReportAccount, + timeoutAccount: TimeoutAccount, handlePromise = handlePromiseDefault, + ): OnMessageSettings => { + async function countAndTimeout( + client: IClient, timeout: boolean, message: string, items: string[], settings: GameServerSettings + ) { + const timeoutTime = fromNow(SPAM_TIMEOUT * (settings.doubleTimeouts ? 2 : 1)); - await countSpamming(client.accountId); + await countSpamming(client.accountId); - if (!isMutedOrShadowed(client)) { - if (timeout && settings.autoBanSpamming) { - await timeoutAccount(client.accountId, timeoutTime, 'Timed out for spamming'); + if (!isMutedOrShadowed(client)) { + if (timeout && settings.autoBanSpamming) { + await timeoutAccount(client.accountId, timeoutTime, 'Timed out for spamming'); - if (settings.reportSpam) { - client.reporter.system('Timed out for spamming', items.join('\n')); - } else { - client.reporter.systemLog('Timed out for spamming'); - } - } else if (settings.reportSpam && !ignoreReporting(message)) { - client.reporter.warn('Spam', message); - } - } - } + if (settings.reportSpam) { + client.reporter.system('Timed out for spamming', items.join('\n')); + } else { + client.reporter.systemLog('Timed out for spamming'); + } + } else if (settings.reportSpam && !ignoreReporting(message)) { + client.reporter.warn('Spam', message); + } + } + } - async function countAndTimeoutForSpam(client: IClient, message: string, settings: GameServerSettings) { - const increment = message.length >= LONG_MESSAGE_LENGTH ? 2 : 1; - const { count, items } = spamCounter.add(client.accountId, message, increment); - const timeout = count >= MUTE_AFTER_LIMIT; - await countAndTimeout(client, timeout, message, items, settings); - } + async function countAndTimeoutForSpam(client: IClient, message: string, settings: GameServerSettings) { + const increment = message.length >= LONG_MESSAGE_LENGTH ? 2 : 1; + const { count, items } = spamCounter.add(client.accountId, message, increment); + const timeout = count >= MUTE_AFTER_LIMIT; + await countAndTimeout(client, timeout, message, items, settings); + } - return (client, message, settings) => { - if (client.isMod) - return; + return (client, message, settings) => { + if (client.isMod) + return; - if (message === '.') - return; + if (message === '.') + return; - const lastSays = client.lastSays; - const lastMatch = findLastSayByPartialString(lastSays, message); + const lastSays = client.lastSays; + const lastMatch = findLastSayByPartialString(lastSays, message); - if (lastMatch) { - lastMatch.count++; - lastMatch.age = 0; + if (lastMatch) { + lastMatch.count++; + lastMatch.age = 0; - const spamLimit = REPORT_AFTER_LIMIT * getLengthMultiplier(message); + const spamLimit = REPORT_AFTER_LIMIT * getLengthMultiplier(message); - if (lastMatch.count >= spamLimit) { - lastMatch.count = 0; - handlePromise(countAndTimeoutForSpam(client, message, settings), client.reporter.error); - } - } else { - if (lastSays.length < MULTIPLE_MATCH_COUNT) { - lastSays.push({ message, count: 1, age: 0 }); - } else { - lastSays.sort(byAge); - const lastSay = lastSays[lastSays.length - 1]; - lastSay.message = message; - lastSay.count = 1; - lastSay.age = 0; - } - } + if (lastMatch.count >= spamLimit) { + lastMatch.count = 0; + handlePromise(countAndTimeoutForSpam(client, message, settings), client.reporter.error); + } + } else { + if (lastSays.length < MULTIPLE_MATCH_COUNT) { + lastSays.push({ message, count: 1, age: 0 }); + } else { + lastSays.sort(byAge); + const lastSay = lastSays[lastSays.length - 1]; + lastSay.message = message; + lastSay.count = 1; + lastSay.age = 0; + } + } - for (const say of lastSays) { - say.age++; - } + for (const say of lastSays) { + say.age++; + } - const now = Date.now(); - const threshold = now - RAPID_MESSAGE_TIMEOUT; - const counter = rapidCounter.add(client.accountId, now); + const now = Date.now(); + const threshold = now - RAPID_MESSAGE_TIMEOUT; + const counter = rapidCounter.add(client.accountId, now); - while (counter.items.length && counter.items[0] < threshold) { - counter.items.shift(); - counter.count--; - } + while (counter.items.length && counter.items[0] < threshold) { + counter.items.shift(); + counter.count--; + } - if (counter.count > RAPID_MESSAGE_COUNT) { - countAndTimeout(client, true, 'rapid messages', ['rapid messages'], settings); - rapidCounter.remove(client.accountId); - } - }; - }; + if (counter.count > RAPID_MESSAGE_COUNT) { + countAndTimeout(client, true, 'rapid messages', ['rapid messages'], settings); + rapidCounter.remove(client.accountId); + } + }; + }; function findLastSayByPartialString(lastSays: LastSay[], message: string) { - for (const say of lastSays) { - if (partialString(say.message, message)) { - return say; - } - } + for (const say of lastSays) { + if (partialString(say.message, message)) { + return say; + } + } - return undefined; + return undefined; } function byAge(a: LastSay, b: LastSay) { - return b.age - a.age; + return b.age - a.age; } function ignoreReporting(message: string) { - return message.length <= 3 || /^[aаz]+$|^\/roll/i.test(message); + return message.length <= 3 || /^[aаz]+$|^\/roll/i.test(message); } function partialString(a: string, b: string): boolean { - if (a === b) { - return true; - } else { - const length = Math.floor(Math.min(a.length, b.length) * 0.75); - return length > 8 && a.substr(0, length) === b.substr(0, length); - } + if (a === b) { + return true; + } else { + const length = Math.floor(Math.min(a.length, b.length) * 0.75); + return length > 8 && a.substr(0, length) === b.substr(0, length); + } } function getLengthMultiplier(message: string) { - if (message.length >= LONG_MESSAGE_LENGTH) { - return LONG_MESSAGE_MUL; - } else if (message.length <= TINY_MESSAGE_LENGTH) { - return TINY_MESSAGE_MUL; - } else if (message.length <= SHORT_MESSAGE_LENGTH) { - return SHORT_MESSAGE_MUL; - } else { - return 1; - } + if (message.length >= LONG_MESSAGE_LENGTH) { + return LONG_MESSAGE_MUL; + } else if (message.length <= TINY_MESSAGE_LENGTH) { + return TINY_MESSAGE_MUL; + } else if (message.length <= SHORT_MESSAGE_LENGTH) { + return SHORT_MESSAGE_MUL; + } else { + return 1; + } } diff --git a/src/ts/server/start.ts b/src/ts/server/start.ts index 4bbd93f..a424108 100644 --- a/src/ts/server/start.ts +++ b/src/ts/server/start.ts @@ -19,87 +19,87 @@ import { createSign } from './controllerUtils'; import { signQuestion } from '../common/entities'; export function start(world: World, server: ServerConfig) { - const data = readFileSync(pathTo('src', 'ts', 'generated', 'pony.bin')); + const data = readFileSync(pathTo('src', 'ts', 'generated', 'pony.bin')); - normalSpriteSheet.data = { - width: 512, - height: 512, - data: new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength), - }; + normalSpriteSheet.data = { + width: 512, + height: 512, + data: new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength), + }; - initializeTileHeightmaps(); + initializeTileHeightmaps(); - world.maps.push(createMainMap(world)); - world.maps.push(createCaveMap(world)); + world.maps.push(createMainMap(world)); + world.maps.push(createCaveMap(world)); - // custom map - if (DEVELOPMENT) { // remove `if` when you're ready to publish your map - // place sign that will teleport the player to your custom map - world.addEntity(createSign( - 75, 69, 'Go to custom map', (_, client) => goToMap(world, client, 'custom'), signQuestion), world.getMainMap()); + // custom map + if (DEVELOPMENT) { // remove `if` when you're ready to publish your map + // place sign that will teleport the player to your custom map + world.addEntity(createSign( + 75, 69, 'Go to custom map', (_, client) => goToMap(world, client, 'custom'), signQuestion), world.getMainMap()); - // add map to the world, go to `/src/ts/server/maps/customMap.ts` to customize your map - world.maps.push(createCustomMap(world)); - } + // add map to the world, go to `/src/ts/server/maps/customMap.ts` to customize your map + world.maps.push(createCustomMap(world)); + } - if (world.featureFlags.test) { - const island = createIslandMap(world, false); - island.id = 'public-island'; - world.maps.push(island); + if (world.featureFlags.test) { + const island = createIslandMap(world, false); + island.id = 'public-island'; + world.maps.push(island); - const house = createHouseMap(world, false); - house.id = 'public-house'; - world.maps.push(house); - } + const house = createHouseMap(world, false); + house.id = 'public-house'; + world.maps.push(house); + } - if (BETA) { - world.maps.push(createPaletteMap(world)); - } + if (BETA) { + world.maps.push(createPaletteMap(world)); + } - if (DEVELOPMENT) { - world.controllers.push(new ctrl.TestController(world, world.getMainMap())); + if (DEVELOPMENT) { + world.controllers.push(new ctrl.TestController(world, world.getMainMap())); - // world.controllers.push(new ctrl.PerfController(world, { - // count: 2000, moving: 1000, unique: true, spread: false, saying: false, x: 20, y: 20 - // })); + // world.controllers.push(new ctrl.PerfController(world, { + // count: 2000, moving: 1000, unique: true, spread: false, saying: false, x: 20, y: 20 + // })); - // world.controllers.push(new ctrl.FakeClientsController(world, server, { - // count: 1000, - // })); + // world.controllers.push(new ctrl.FakeClientsController(world, server, { + // count: 1000, + // })); - world.setTime(12); - } + world.setTime(12); + } - let last = Date.now(); - let frames = 0; + let last = Date.now(); + let frames = 0; - world.initialize(last); + world.initialize(last); - if (!DEVELOPMENT) { - create(server).info(`Server started`); - } + if (!DEVELOPMENT) { + create(server).info(`Server started`); + } - setInterval(() => { - timingReset(); - timingStart('frame'); + setInterval(() => { + timingReset(); + timingStart('frame'); - try { - const now = Date.now(); - world.update(now - last, now); - last = now; - frames++; + try { + const now = Date.now(); + world.update(now - last, now); + last = now; + frames++; - if (frames >= SERVER_FPS) { - frames = 0; - world.sparseUpdate(now); - } - } catch (e) { - create(server).danger(e.message); - logger.error(e); - } + if (frames >= SERVER_FPS) { + frames = 0; + world.sparseUpdate(now); + } + } catch (e) { + create(server).danger(e.message); + logger.error(e); + } - timingEnd(); - }, 1000 / SERVER_FPS); + timingEnd(); + }, 1000 / SERVER_FPS); - return world; + return world; } diff --git a/src/ts/server/stats.ts b/src/ts/server/stats.ts index e541476..b9a41fa 100644 --- a/src/ts/server/stats.ts +++ b/src/ts/server/stats.ts @@ -9,28 +9,28 @@ import { HOUR } from '../common/constants'; import { ByteSize } from './utils/byteSize'; interface Stats { - count: number; - size: ByteSize; - totalCount: number; - lastHourCount: number; - lastHourTotal: string; - lastHourAverage: string; - lastHourOrder: string; - lastHourSize: ByteSize; + count: number; + size: ByteSize; + totalCount: number; + lastHourCount: number; + lastHourTotal: string; + lastHourAverage: string; + lastHourOrder: string; + lastHourSize: ByteSize; } interface SocketStats { - id: number; - name: string; - countStr: number; - countBin: number; - size: ByteSize; - lastHourCountStr: number; - lastHourCountBin: number; - lastHourTotal: string; - lastHourAverage: string; - lastHourOrder: string; - lastHourSize: ByteSize; + id: number; + name: string; + countStr: number; + countBin: number; + size: ByteSize; + lastHourCountStr: number; + lastHourCountBin: number; + lastHourTotal: string; + lastHourAverage: string; + lastHourOrder: string; + lastHourSize: ByteSize; } const MB = 1024 * 1024; @@ -38,193 +38,193 @@ const SEPARATOR = ';'; const statsHeaders = ['date', 'requests count', 'requests size', 'swearing', 'spamming']; function encodeCSV(values: any[]) { - return `${values.join(SEPARATOR)}\n`; + return `${values.join(SEPARATOR)}\n`; } function getDate() { - return (new Date()).getDate(); + return (new Date()).getDate(); } function getAverage({ bytes, mbytes }: ByteSize, count: number): string { - if (!count) { - return '0'; - } else if (mbytes >= 1) { - return `${Math.floor((mbytes / count) * MB)} b`; - } else { - return `${Math.floor(bytes / count)} b`; - } + if (!count) { + return '0'; + } else if (mbytes >= 1) { + return `${Math.floor((mbytes / count) * MB)} b`; + } else { + return `${Math.floor(bytes / count)} b`; + } } function updateHourlySocketStats(stat: SocketStats | undefined) { - if (stat) { - stat.lastHourCountBin = stat.countBin; - stat.lastHourCountStr = stat.countStr; - stat.lastHourSize = stat.size; - stat.lastHourTotal = stat.size.toHumanReadable(); - stat.lastHourAverage = getAverage(stat.size, stat.countBin + stat.countStr); - stat.lastHourOrder = stat.size.toSortableString(); - stat.countBin = 0; - stat.countStr = 0; - stat.size = new ByteSize(); - } + if (stat) { + stat.lastHourCountBin = stat.countBin; + stat.lastHourCountStr = stat.countStr; + stat.lastHourSize = stat.size; + stat.lastHourTotal = stat.size.toHumanReadable(); + stat.lastHourAverage = getAverage(stat.size, stat.countBin + stat.countStr); + stat.lastHourOrder = stat.size.toSortableString(); + stat.countBin = 0; + stat.countStr = 0; + stat.size = new ByteSize(); + } } export class StatsTracker { - private stats = new Map(); - private recvStats: (SocketStats | undefined)[] = []; - private sendStats: (SocketStats | undefined)[] = []; - private dailyDate = getDate(); - private dailyRequestCount = 0; - private dailyRequestSize = new ByteSize(); - private dailySwearing = 0; - private dailySpamming = 0; - constructor(private statsPath: string) { - } - logRequest = (req: Request, result: any, url?: string) => { - if (result && !/^\/api-internal/.test(req.baseUrl)) { - this.logStat( - url || (req.baseUrl + req.path), typeof result === 'string' ? result.length : JSON.stringify(result).length); - } - } - logSwearing = () => { - this.dailySwearing++; - } - logSpamming = () => { - this.dailySpamming++; - } - logRecvStats = (packet: Packet) => { - this.logSocketStats(this.recvStats, packet); - } - logSendStats = (packet: Packet) => { - this.logSocketStats(this.sendStats, packet); - } - private logSocketStats(stats: (SocketStats | undefined)[], { id, name, binary, json }: Packet) { - const entry = stats[id] || (stats[id] = { - id, - name, - countStr: 0, - countBin: 0, - size: new ByteSize(), - lastHourCountStr: 0, - lastHourCountBin: 0, - lastHourTotal: '0', - lastHourAverage: '0', - lastHourOrder: '0', - lastHourSize: new ByteSize(), - }); + private stats = new Map(); + private recvStats: (SocketStats | undefined)[] = []; + private sendStats: (SocketStats | undefined)[] = []; + private dailyDate = getDate(); + private dailyRequestCount = 0; + private dailyRequestSize = new ByteSize(); + private dailySwearing = 0; + private dailySpamming = 0; + constructor(private statsPath: string) { + } + logRequest = (req: Request, result: any, url?: string) => { + if (result && !/^\/api-internal/.test(req.baseUrl)) { + this.logStat( + url || (req.baseUrl + req.path), typeof result === 'string' ? result.length : JSON.stringify(result).length); + } + } + logSwearing = () => { + this.dailySwearing++; + } + logSpamming = () => { + this.dailySpamming++; + } + logRecvStats = (packet: Packet) => { + this.logSocketStats(this.recvStats, packet); + } + logSendStats = (packet: Packet) => { + this.logSocketStats(this.sendStats, packet); + } + private logSocketStats(stats: (SocketStats | undefined)[], { id, name, binary, json }: Packet) { + const entry = stats[id] || (stats[id] = { + id, + name, + countStr: 0, + countBin: 0, + size: new ByteSize(), + lastHourCountStr: 0, + lastHourCountBin: 0, + lastHourTotal: '0', + lastHourAverage: '0', + lastHourOrder: '0', + lastHourSize: new ByteSize(), + }); - if (!!binary) { - entry.countBin++; - } else { - entry.countStr++; - } + if (!!binary) { + entry.countBin++; + } else { + entry.countStr++; + } - entry.size.addBytes(binary ? (binary.length || binary.byteLength) : (json ? json.length : 0)); - } - getStats(): RequestStats[] { - const result: RequestStats[] = []; - this.stats.forEach(({ lastHourCount, lastHourTotal, lastHourAverage, lastHourOrder, totalCount }, path) => { - result.push({ - path, - totalCount, - count: lastHourCount, - average: lastHourAverage, - total: lastHourTotal, - order: lastHourOrder, - }); - }); - return result.sort((a, b) => b.order.localeCompare(a.order)); - } - private createActionsStats(type: string, stats: (SocketStats | undefined)[]) { - return compact(stats).map(s => ({ - id: s.id, - name: s.name, - type, - countBin: s.lastHourCountBin, - countStr: s.lastHourCountStr, - average: s.lastHourAverage, - total: s.lastHourTotal, - order: s.lastHourOrder, - })); - } - getSocketStats(): ServerStats { - return { - actions: [ - ...this.createActionsStats('recv', this.recvStats), - ...this.createActionsStats('send', this.sendStats), - ].sort((a, b) => b.order.localeCompare(a.order)), - }; - } - private logStat(path: string, bytes: number) { - const entry = this.stats.get(path); + entry.size.addBytes(binary ? (binary.length || binary.byteLength) : (json ? json.length : 0)); + } + getStats(): RequestStats[] { + const result: RequestStats[] = []; + this.stats.forEach(({ lastHourCount, lastHourTotal, lastHourAverage, lastHourOrder, totalCount }, path) => { + result.push({ + path, + totalCount, + count: lastHourCount, + average: lastHourAverage, + total: lastHourTotal, + order: lastHourOrder, + }); + }); + return result.sort((a, b) => b.order.localeCompare(a.order)); + } + private createActionsStats(type: string, stats: (SocketStats | undefined)[]) { + return compact(stats).map(s => ({ + id: s.id, + name: s.name, + type, + countBin: s.lastHourCountBin, + countStr: s.lastHourCountStr, + average: s.lastHourAverage, + total: s.lastHourTotal, + order: s.lastHourOrder, + })); + } + getSocketStats(): ServerStats { + return { + actions: [ + ...this.createActionsStats('recv', this.recvStats), + ...this.createActionsStats('send', this.sendStats), + ].sort((a, b) => b.order.localeCompare(a.order)), + }; + } + private logStat(path: string, bytes: number) { + const entry = this.stats.get(path); - if (entry) { - entry.count++; - entry.size.addBytes(bytes); - } else { - this.stats.set(path, { - count: 1, - size: new ByteSize(bytes), - totalCount: 0, - lastHourCount: 0, - lastHourTotal: '-', - lastHourAverage: '-', - lastHourOrder: '', - lastHourSize: new ByteSize(), - }); - } - } - private submitDailyStats(statsPath: string) { - const statsEntry = [ - moment().format('MMM DD'), // DD-MM-YY HH:mm:ss - this.dailyRequestCount.toString(), - this.dailyRequestSize.toString(), - this.dailySwearing.toString(), - this.dailySpamming.toString(), - ]; + if (entry) { + entry.count++; + entry.size.addBytes(bytes); + } else { + this.stats.set(path, { + count: 1, + size: new ByteSize(bytes), + totalCount: 0, + lastHourCount: 0, + lastHourTotal: '-', + lastHourAverage: '-', + lastHourOrder: '', + lastHourSize: new ByteSize(), + }); + } + } + private submitDailyStats(statsPath: string) { + const statsEntry = [ + moment().format('MMM DD'), // DD-MM-YY HH:mm:ss + this.dailyRequestCount.toString(), + this.dailyRequestSize.toString(), + this.dailySwearing.toString(), + this.dailySpamming.toString(), + ]; - fs.appendFileAsync(statsPath, encodeCSV(statsEntry), { encoding: 'utf8' }) - .catch(console.error) - .done(); - } - startStatTracking() { - if (!fs.existsSync(this.statsPath)) { - try { - fs.mkdirSync(path.dirname(this.statsPath), { recursive: true }); - } catch (e) { - if (e.code !== 'EEXIST') throw e; - } - fs.writeFileSync(this.statsPath, encodeCSV(statsHeaders), { encoding: 'utf8' }); - } + fs.appendFileAsync(statsPath, encodeCSV(statsEntry), { encoding: 'utf8' }) + .catch(console.error) + .done(); + } + startStatTracking() { + if (!fs.existsSync(this.statsPath)) { + try { + fs.mkdirSync(path.dirname(this.statsPath), { recursive: true }); + } catch (e) { + if (e.code !== 'EEXIST') throw e; + } + fs.writeFileSync(this.statsPath, encodeCSV(statsHeaders), { encoding: 'utf8' }); + } - setInterval(() => { - const date = getDate(); + setInterval(() => { + const date = getDate(); - if (date !== this.dailyDate) { - this.submitDailyStats(this.statsPath); - this.dailyDate = date; - this.dailyRequestCount = 0; - this.dailySwearing = 0; - this.dailySpamming = 0; - this.dailyRequestSize = new ByteSize(); - } + if (date !== this.dailyDate) { + this.submitDailyStats(this.statsPath); + this.dailyDate = date; + this.dailyRequestCount = 0; + this.dailySwearing = 0; + this.dailySpamming = 0; + this.dailyRequestSize = new ByteSize(); + } - this.stats.forEach(entry => { - entry.lastHourCount = entry.count; - entry.lastHourSize = entry.size; - entry.lastHourTotal = entry.size.toHumanReadable(); - entry.lastHourAverage = getAverage(entry.size, entry.count); - entry.lastHourOrder = entry.size.toSortableString(); - entry.totalCount += entry.count; - entry.count = 0; - entry.size = new ByteSize(); + this.stats.forEach(entry => { + entry.lastHourCount = entry.count; + entry.lastHourSize = entry.size; + entry.lastHourTotal = entry.size.toHumanReadable(); + entry.lastHourAverage = getAverage(entry.size, entry.count); + entry.lastHourOrder = entry.size.toSortableString(); + entry.totalCount += entry.count; + entry.count = 0; + entry.size = new ByteSize(); - this.dailyRequestCount += entry.lastHourCount; - this.dailyRequestSize.add(entry.lastHourSize); - }); + this.dailyRequestCount += entry.lastHourCount; + this.dailyRequestSize.add(entry.lastHourSize); + }); - this.sendStats.forEach(updateHourlySocketStats); - this.recvStats.forEach(updateHourlySocketStats); - }, 1 * HOUR); - } + this.sendStats.forEach(updateHourlySocketStats); + this.recvStats.forEach(updateHourlySocketStats); + }, 1 * HOUR); + } } diff --git a/src/ts/server/timing.ts b/src/ts/server/timing.ts index 56ffe57..8f3e609 100644 --- a/src/ts/server/timing.ts +++ b/src/ts/server/timing.ts @@ -9,57 +9,57 @@ let entriesCount = 0; let now: () => number; if (typeof window !== 'undefined') { - now = performance.now; + now = performance.now; } else { - const hrtime = process.hrtime; - const getNanoSeconds = () => { - const hr = hrtime(); - return hr[0] * 1e9 + hr[1]; - }; - const nodeLoadTime = getNanoSeconds() - process.uptime() * 1e9; - now = () => (getNanoSeconds() - nodeLoadTime) / 1e6; + const hrtime = process.hrtime; + const getNanoSeconds = () => { + const hr = hrtime(); + return hr[0] * 1e9 + hr[1]; + }; + const nodeLoadTime = getNanoSeconds() - process.uptime() * 1e9; + now = () => (getNanoSeconds() - nodeLoadTime) / 1e6; } if (ENABLED) { - for (let i = 0; i < ENTRIES_LIMIT; i++) { - entries.push({ type: 0, time: 0, name: undefined }); - } + for (let i = 0; i < ENTRIES_LIMIT; i++) { + entries.push({ type: 0, time: 0, name: undefined }); + } } export function timingStart(name: string) { - if (ENABLED) { - if (entriesCount < ENTRIES_LIMIT) { - const entry = entries[entriesCount]; - entry.type = TimingEntryType.Start; - entry.time = now(); - entry.name = name; - entriesCount++; - } else { - console.warn(`exceeded timing entry limit`); - } - } + if (ENABLED) { + if (entriesCount < ENTRIES_LIMIT) { + const entry = entries[entriesCount]; + entry.type = TimingEntryType.Start; + entry.time = now(); + entry.name = name; + entriesCount++; + } else { + console.warn(`exceeded timing entry limit`); + } + } } export function timingEnd() { - if (ENABLED) { - if (entriesCount < ENTRIES_LIMIT) { - const entry = entries[entriesCount]; - entry.type = TimingEntryType.End; - entry.time = now(); - entry.name = undefined; - entriesCount++; - } else { - console.warn(`exceeded timing entry limit`); - } - } + if (ENABLED) { + if (entriesCount < ENTRIES_LIMIT) { + const entry = entries[entriesCount]; + entry.type = TimingEntryType.End; + entry.time = now(); + entry.name = undefined; + entriesCount++; + } else { + console.warn(`exceeded timing entry limit`); + } + } } export function timingReset() { - if (ENABLED) { - entriesCount = 0; - } + if (ENABLED) { + entriesCount = 0; + } } export function timingEntries() { - return entries.slice(0, entriesCount); + return entries.slice(0, entriesCount); } diff --git a/src/ts/server/userError.ts b/src/ts/server/userError.ts index d53a314..0020825 100644 --- a/src/ts/server/userError.ts +++ b/src/ts/server/userError.ts @@ -5,48 +5,48 @@ import { IClient, Reporter } from './serverInterfaces'; import { ServerConfig } from '../common/adminInterfaces'; export interface UserErrorInfo { - error?: Error; - message?: string; - desc?: string; - data?: any; - log?: string; + error?: Error; + message?: string; + desc?: string; + data?: any; + log?: string; } export class UserError extends Error { - //static name: string; - constructor(public message: string, public info?: UserErrorInfo, public userInfo?: string) { - super(message); - Object.defineProperty(this, 'name', { value: 'UserError' }); - Error.captureStackTrace(this, UserError); - } + //static name: string; + constructor(public message: string, public info?: UserErrorInfo, public userInfo?: string) { + super(message); + Object.defineProperty(this, 'name', { value: 'UserError' }); + Error.captureStackTrace(this, UserError); + } } export function isUserError(e: Error): e is UserError { - return e.name === 'UserError'; + return e.name === 'UserError'; } function report(message: string, info: UserErrorInfo, reporter: Reporter | undefined, extra = '') { - const keys = Object.keys(info); + const keys = Object.keys(info); - if (keys.length === 1 && keys[0] === 'log') { - logger.log(info.log); - } else { - if (reporter) { - reporter.warn((info.error && info.error.message) || info.message || message || '', info.desc); - } + if (keys.length === 1 && keys[0] === 'log') { + logger.log(info.log); + } else { + if (reporter) { + reporter.warn((info.error && info.error.message) || info.message || message || '', info.desc); + } - logger.warn(info.error || info.message || message || '', info.desc || '', info.data || '', extra); - } + logger.warn(info.error || info.message || message || '', info.desc || '', info.data || '', extra); + } } export function reportUserError(e: UserError, server: ServerConfig, req: Request) { - if (e.info) { - report(e.message, e.info, createFromRequest(server, req), `${req.url} ${req.ip}`); - } + if (e.info) { + report(e.message, e.info, createFromRequest(server, req), `${req.url} ${req.ip}`); + } } export function reportUserError2(e: UserError, client: IClient | undefined) { - if (e.info) { - report(e.message, e.info, client && client.reporter); - } + if (e.info) { + report(e.message, e.info, client && client.reporter); + } } diff --git a/src/ts/server/utils/byteSize.ts b/src/ts/server/utils/byteSize.ts index cdfb502..a6284f7 100644 --- a/src/ts/server/utils/byteSize.ts +++ b/src/ts/server/utils/byteSize.ts @@ -1,31 +1,31 @@ const MB = 1024 * 1024; export class ByteSize { - constructor(public bytes = 0, public mbytes = 0) { - this.reduce(); - } - add({ bytes, mbytes }: ByteSize) { - this.addBytes(bytes, mbytes); - } - addBytes(bytes: number, mbytes = 0) { - this.mbytes += mbytes; - this.bytes += bytes; - this.reduce(); - return this; - } - toString() { - return this.mbytes ? `${this.mbytes.toString()}${this.bytes.toString().padStart(6, '0')}` : this.bytes.toString(); - } - toSortableString() { - return `${this.mbytes.toString().padStart(9, '0')}-${this.bytes.toString().padStart(6, '0')}`; - } - toHumanReadable() { - return this.mbytes >= 1 ? - `${this.mbytes} mb` : - (this.bytes >= 2048 ? `${Math.floor(this.bytes / 1024)} kb` : `${this.bytes} b`); - } - private reduce() { - this.mbytes += Math.floor(this.bytes / MB); - this.bytes = this.bytes % MB; - } + constructor(public bytes = 0, public mbytes = 0) { + this.reduce(); + } + add({ bytes, mbytes }: ByteSize) { + this.addBytes(bytes, mbytes); + } + addBytes(bytes: number, mbytes = 0) { + this.mbytes += mbytes; + this.bytes += bytes; + this.reduce(); + return this; + } + toString() { + return this.mbytes ? `${this.mbytes.toString()}${this.bytes.toString().padStart(6, '0')}` : this.bytes.toString(); + } + toSortableString() { + return `${this.mbytes.toString().padStart(9, '0')}-${this.bytes.toString().padStart(6, '0')}`; + } + toHumanReadable() { + return this.mbytes >= 1 ? + `${this.mbytes} mb` : + (this.bytes >= 2048 ? `${Math.floor(this.bytes / 1024)} kb` : `${this.bytes} b`); + } + private reduce() { + this.mbytes += Math.floor(this.bytes / MB); + this.bytes = this.bytes % MB; + } } diff --git a/src/ts/server/utils/socketErrorHandler.ts b/src/ts/server/utils/socketErrorHandler.ts index b94f878..3b65c94 100644 --- a/src/ts/server/utils/socketErrorHandler.ts +++ b/src/ts/server/utils/socketErrorHandler.ts @@ -12,145 +12,145 @@ import { ServerActions } from '../serverActions'; const reporterIgnore = /^rate limit exceeded/i; const ignoreErrors = [ - 'String message while forced binary', + 'String message while forced binary', ]; const rollbarIgnore = new RegExp('^(' + [ - 'reserved fields must be empty', - 'rate limit exceeded', - 'transfer limit exceeded', - 'some error', - 'Invalid token', - 'Action not allowed', - 'Client does not exist', - 'Cannot perform this action on admin user', - 'Account creation is temporarily disabled', - 'Not a number', - 'Not a string', - ...ignoreErrors, + 'reserved fields must be empty', + 'rate limit exceeded', + 'transfer limit exceeded', + 'some error', + 'Invalid token', + 'Action not allowed', + 'Client does not exist', + 'Cannot perform this action on admin user', + 'Account creation is temporarily disabled', + 'Not a number', + 'Not a string', + ...ignoreErrors, ].map(escapeRegExp).join('|') + ')', 'i'); let lastError = ''; let lastErrorTime = 0; function formatMessage(message: string | Uint8Array | null | undefined) { - if (message === null) { - return ''; - } else if (message === undefined) { - return ''; - } else if (typeof message === 'string') { - return message; - } else if (message instanceof Uint8Array) { - return `<${Array.from(message).toString()}>`; - } else { - return `<${JSON.stringify(message)}>`; - } + if (message === null) { + return ''; + } else if (message === undefined) { + return ''; + } else if (typeof message === 'string') { + return message; + } else if (message instanceof Uint8Array) { + return `<${Array.from(message).toString()}>`; + } else { + return `<${JSON.stringify(message)}>`; + } } function getPerson(client: IClient | undefined) { - return client && client.account ? { - id: client.accountId, - username: client.account.name, - } : {}; + return client && client.account ? { + id: client.accountId, + username: client.account.name, + } : {}; } function reportError(rollbar: Rollbar | undefined, e: Error, client: IClient | undefined, config: ServerConfig) { - if (isUserError(e)) { - reportUserError2(e, client); - return e; - } else { - if (client && client.reporter && !reporterIgnore.test(e.message)) { - client.reporter.error(e); - } else if (client && client.originalRequest) { - const origin = client.originalRequest && getOriginFromHTTP(client.originalRequest); - create(config, undefined, undefined, origin).error(e); - } else { - create(config).error(e); - } + if (isUserError(e)) { + reportUserError2(e, client); + return e; + } else { + if (client && client.reporter && !reporterIgnore.test(e.message)) { + client.reporter.error(e); + } else if (client && client.originalRequest) { + const origin = client.originalRequest && getOriginFromHTTP(client.originalRequest); + create(config, undefined, undefined, origin).error(e); + } else { + create(config).error(e); + } - if (!rollbarIgnore.test(e.message)) { - rollbar && rollbar.error(e, null as any, { person: getPerson(client) }); - } + if (!rollbarIgnore.test(e.message)) { + rollbar && rollbar.error(e, null as any, { person: getPerson(client) }); + } - return new Error('Error occurred'); - } + return new Error('Error occurred'); + } } const serverMethods = getMethods(ServerActions); function getMethodNameFromPacket(packet: string | Uint8Array) { - try { - if (typeof packet === 'string') { - const values = JSON.parse(packet); - return serverMethods[values[0]].name; - } else { - return serverMethods[packet[0]].name; - } - } catch { - return '???'; - } + try { + if (typeof packet === 'string') { + const values = JSON.parse(packet); + return serverMethods[values[0]].name; + } else { + return serverMethods[packet[0]].name; + } + } catch { + return '???'; + } } function reportRateLimit(client: IClient, e: Error, message: string) { - let reported = false; + let reported = false; - if (client.rateLimitMessage === e.message && client.rateLimitCount) { - if (++client.rateLimitCount > 5) { - reported = true; - client.reporter.warn(`${e.message} (x5)`, message); - client.rateLimitCount = 1; - client.disconnect(true, true); - } - } else { - client.rateLimitMessage = e.message; - client.rateLimitCount = 1; - } + if (client.rateLimitMessage === e.message && client.rateLimitCount) { + if (++client.rateLimitCount > 5) { + reported = true; + client.reporter.warn(`${e.message} (x5)`, message); + client.rateLimitCount = 1; + client.disconnect(true, true); + } + } else { + client.rateLimitMessage = e.message; + client.rateLimitCount = 1; + } - return reported; + return reported; } export class SocketErrorHandler implements ErrorHandler { - constructor(private rollbar: Rollbar | undefined, private config: ServerConfig) { - } - handleError(client: IClient | null, e: Error) { - if (!/no server for given id/i.test(e.message)) { - reportError(this.rollbar, e, client || undefined, this.config); - } - } - handleRejection(client: IClient, e: Error) { - if (/^rate limit exceeded/i.test(e.message)) { - reportRateLimit(client, e, 'rejection'); - return new Error('Error occurred'); - } else { - return reportError(this.rollbar, e, client, this.config); - } - } - handleRecvError(client: IClient, e: Error, socketMessage: string | Uint8Array) { - if (lastError === e.message && Date.now() < (lastErrorTime + 5000)) - return; + constructor(private rollbar: Rollbar | undefined, private config: ServerConfig) { + } + handleError(client: IClient | null, e: Error) { + if (!/no server for given id/i.test(e.message)) { + reportError(this.rollbar, e, client || undefined, this.config); + } + } + handleRejection(client: IClient, e: Error) { + if (/^rate limit exceeded/i.test(e.message)) { + reportRateLimit(client, e, 'rejection'); + return new Error('Error occurred'); + } else { + return reportError(this.rollbar, e, client, this.config); + } + } + handleRecvError(client: IClient, e: Error, socketMessage: string | Uint8Array) { + if (lastError === e.message && Date.now() < (lastErrorTime + 5000)) + return; - const message = formatMessage(socketMessage); - const method = getMethodNameFromPacket(socketMessage); - let reported = false; + const message = formatMessage(socketMessage); + const method = getMethodNameFromPacket(socketMessage); + let reported = false; - if (client.reporter) { - if (/^rate limit exceeded/i.test(e.message)) { - reported = reportRateLimit(client, e, message); - } else if (/^transfer limit exceeded/i.test(e.message)) { - reported = true; - const desc = e.message.replace(/transfer limit exceeded /i, ''); - client.reporter.warn('Transfer limit exceeded', `${desc} - (${method}) ${message}`); - } else if (!includes(ignoreErrors, e.message)) { - reported = true; - client.reporter.error(e, `(${method}) ${message}`); - } - } + if (client.reporter) { + if (/^rate limit exceeded/i.test(e.message)) { + reported = reportRateLimit(client, e, message); + } else if (/^transfer limit exceeded/i.test(e.message)) { + reported = true; + const desc = e.message.replace(/transfer limit exceeded /i, ''); + client.reporter.warn('Transfer limit exceeded', `${desc} - (${method}) ${message}`); + } else if (!includes(ignoreErrors, e.message)) { + reported = true; + client.reporter.error(e, `(${method}) ${message}`); + } + } - lastError = e.message; - lastErrorTime = Date.now(); + lastError = e.message; + lastErrorTime = Date.now(); - if (!reported && !rollbarIgnore.test(e.message || '')) { - logger.error(`recv error: ${e.stack || e}\n\n message: ${message}`); - this.rollbar && this.rollbar.error(e, null as any, { custom: { message }, person: getPerson(client) }); - } - } + if (!reported && !rollbarIgnore.test(e.message || '')) { + logger.error(`recv error: ${e.stack || e}\n\n message: ${message}`); + this.rollbar && this.rollbar.error(e, null as any, { custom: { message }, person: getPerson(client) }); + } + } } diff --git a/src/ts/server/utils/taskQueue.ts b/src/ts/server/utils/taskQueue.ts index 8bd47b4..2a46cd9 100644 --- a/src/ts/server/utils/taskQueue.ts +++ b/src/ts/server/utils/taskQueue.ts @@ -1,60 +1,60 @@ import { noop } from 'lodash'; export interface TaskQueue { - push(action: () => Promise | T): Promise; - wait(): Promise; + push(action: () => Promise | T): Promise; + wait(): Promise; } interface Task { - resolve: (value: any) => void; - reject: (error: any) => void; - action: () => any; + resolve: (value: any) => void; + reject: (error: any) => void; + action: () => any; } export function taskQueue(): TaskQueue { - const queue: Task[] = []; - let working = false; + const queue: Task[] = []; + let working = false; - function next() { - const task = queue.shift(); + function next() { + const task = queue.shift(); - if (task) { - exec(task); - } else { - working = false; - } - } + if (task) { + exec(task); + } else { + working = false; + } + } - function exec({ action, resolve, reject }: Task) { - working = true; + function exec({ action, resolve, reject }: Task) { + working = true; - Promise.resolve() - .then(action) - .then(resolve, reject) - .catch(console.error) - .finally(next); - } + Promise.resolve() + .then(action) + .then(resolve, reject) + .catch(console.error) + .finally(next); + } - function push(action: () => any): Promise { - return new Promise((resolve, reject) => { - const task: Task = { action, resolve, reject }; + function push(action: () => any): Promise { + return new Promise((resolve, reject) => { + const task: Task = { action, resolve, reject }; - if (working) { - queue.push(task); - } else { - exec(task); - } - }); - } + if (working) { + queue.push(task); + } else { + exec(task); + } + }); + } - function wait() { - return push(noop); - } + function wait() { + return push(noop); + } - return { push, wait }; + return { push, wait }; } export function makeQueued any>(action: T): T { - const queue = taskQueue(); - return ((...args: any[]) => queue.push(() => action(...args))) as T; + const queue = taskQueue(); + return ((...args: any[]) => queue.push(() => action(...args))) as T; } diff --git a/src/ts/server/world.ts b/src/ts/server/world.ts index b00cb60..0c10f56 100644 --- a/src/ts/server/world.ts +++ b/src/ts/server/world.ts @@ -1,16 +1,16 @@ import { getWriterBuffer } from 'ag-sockets'; import { remove, compact } from 'lodash'; import { - TileType, WorldState, Season, Holiday, WorldStateFlags, LeaveReason, NotificationFlags, UpdateFlags, Action, + TileType, WorldState, Season, Holiday, WorldStateFlags, LeaveReason, NotificationFlags, UpdateFlags, Action, } from '../common/interfaces'; import { removeItem, distance, clamp, randomPoint, includes, fromNow } from '../common/utils'; import { HOUR_LENGTH, DAY_LENGTH } from '../common/timeUtils'; import { - AFK_TIMEOUT, MAP_DISCARD_TIMEOUT, JOINS_PER_UPDATE, REMOVE_INTERVAL, REMOVE_TIMEOUT, - MAP_SWITCHES_PER_UPDATE, MINUTE, SERVER_FPS, HOUR + AFK_TIMEOUT, MAP_DISCARD_TIMEOUT, JOINS_PER_UPDATE, REMOVE_INTERVAL, REMOVE_TIMEOUT, + MAP_SWITCHES_PER_UPDATE, MINUTE, SERVER_FPS, HOUR } from '../common/constants'; import { - IClient, ServerEntity, Controller, GetSettings, ServerNotification, SocketStats, ServerMap, MapUsage + IClient, ServerEntity, Controller, GetSettings, ServerNotification, SocketStats, ServerMap, MapUsage } from './serverInterfaces'; import { isTileLocked, getMapInfo, setTile, hasAnyClients, createMinimap } from './serverMap'; import { getModInfo } from './accountUtils'; @@ -21,12 +21,12 @@ import { AroundEntry, ServerLiveSettings, ServerConfig } from '../common/adminIn import { fixPosition, updateEntity, pushRemoveEntityToClient, pushUpdateEntityToClient } from './entityUtils'; import { isBanned, isShadowed } from '../common/adminUtils'; import { - createAndUpdateCharacterState, setEntityExpression, reloadFriends, updateEntityPlayerState, resetClientUpdates, isMutedOrShadowed + createAndUpdateCharacterState, setEntityExpression, reloadFriends, updateEntityPlayerState, resetClientUpdates, isMutedOrShadowed } from './playerUtils'; import { - sparseRegionUpdate, addToRegion, removeFromRegion, unsubscribeFromOutOfRangeRegions, - subscribeToRegionsInRange, unsubscribeFromAllRegions, commitRegionUpdates, updateRegions, setupTiming, - clearTiming, resetEncodeUpdate + sparseRegionUpdate, addToRegion, removeFromRegion, unsubscribeFromOutOfRangeRegions, + subscribeToRegionsInRange, unsubscribeFromAllRegions, commitRegionUpdates, updateRegions, setupTiming, + clearTiming, resetEncodeUpdate } from './regionUtils'; import { roundPosition, roundPositionXMidPixel, roundPositionYMidPixel } from '../common/positionUtils'; import { logger } from './logger'; @@ -46,954 +46,954 @@ import { createHouseMap } from './maps/houseMap'; import { updateMainMapSeason } from './maps/mainMap'; interface MapSwitch { - map: ServerMap; - x: number; - y: number; - client: IClient; + map: ServerMap; + x: number; + y: number; + client: IClient; } interface ReservedID { - id: number; - time: number; + id: number; + time: number; } export class World { - season = Season.Summer; - holiday = Holiday.None; - maps: ServerMap[] = []; - controllers: Controller[] = []; - options = { - restoreTerrain: !DEVELOPMENT, - }; - clients: IClient[] = []; - clientsByAccount = new Map(); - joinQueue: IClient[] = []; - mapSwitchQueue: MapSwitch[] = []; - now = 0; - start = 0; - // mapPools = new Map>(); - private maxId = 0 >>> 0; - private offlineClients: IClient[] = []; - private baseTime = 0; - private entityById = new Map(); - private reservedIds = new Map(); - private reservedIdsByKey = new Map(); - constructor( - public readonly server: ServerConfig, - private readonly partyService: PartyService, - private readonly friendsService: FriendsService, - public readonly hidingService: HidingService, - private readonly notifications: NotificationService, - private readonly getSettings: GetSettings, - private readonly liveSettings: ServerLiveSettings, - private readonly socketStats: SocketStats, - ) { - // this.mapPools.set('island', createPool(10, () => createIslandMap(this, true), resetIslandMap)); - // this.mapPools.set('house', createPool(10, () => createHouseMap(this, true), resetHouseMap)); - - partyService.partyChanged.subscribe(client => { - if (client.isConnected && client.map.usage === MapUsage.Party) { - if ( - client.party && client.party.leader === client && client.map.instance === client.accountId && - !this.maps.some(m => m.id === client.map.id && m.instance === client.party!.id) - ) { - client.map.instance = client.party.id; - } else { - refreshMap(this, client); - } - } - }); - } - get featureFlags() { - return this.server.flags; - } - // entities - get time() { - return this.baseTime + Date.now(); - } - setTime(hour: number) { - let newBaseTime = hour * HOUR_LENGTH - (Date.now() % DAY_LENGTH); - - while (newBaseTime < 0) { - newBaseTime += DAY_LENGTH; - } - - this.baseTime = newBaseTime; - this.updateWorldState(); - } - setTile(map: ServerMap, x: number, y: number, type: TileType) { - if (!BETA && map.tilesLocked) - return; - - if (x >= 0 && y >= 0 && x < map.width && y < map.height && !isTileLocked(map, x, y) && type !== getTile(map, x, y)) { - setTile(map, x, y, type); - } - } - toggleWall(map: ServerMap, x: number, y: number, type: TileType) { - for (const controller of map.controllers) { - if (controller.toggleWall) { - controller.toggleWall(x, y, type); - } - } - } - getState(): WorldState { - return { - time: this.time, - season: this.season, - holiday: this.holiday, - flags: this.getSettings().filterSwears ? WorldStateFlags.Safe : WorldStateFlags.None, - featureFlags: this.featureFlags, - }; - } - setSeason(season: Season, holiday: Holiday) { - this.season = season; - this.holiday = holiday; - this.updateWorldState(); - updateMainMapSeason(this, this.getMainMap(), season, holiday); - } - private updateWorldState() { - const state = this.getState(); - - for (const client of this.clients) { - client.worldState(state, false); - } - } - getEntityById(id: number) { - return this.entityById.get(id); - } - getNewEntityId() { - do { - this.maxId = (this.maxId + 1) >>> 0; - } while (this.maxId === 0 || this.entityById.has(this.maxId) || this.reservedIds.has(this.maxId)); - - return this.maxId; - } - addEntity(entity: ServerEntity, map: ServerMap) { - if (DEVELOPMENT) { - if (entity.update) { - console.error('Entity update() method is only for client-side use'); - } - - if (entity.id && this.entityById.has(entity.id)) { - console.error(`Entity already added to the world ${getEntityTypeName(entity.type)} [${entity.id}]`); - } - } - - entity.id = entity.id || this.getNewEntityId(); - entity.timestamp = this.now / 1000; - this.entityById.set(entity.id, entity); - roundPosition(entity); - const region = getRegionGlobal(map, entity.x, entity.y); - addToRegion(entity, region, map); - return entity; - } - removeEntity(entity: ServerEntity, map: ServerMap) { - let removed = false; - - if (entity.region) { - removed = removeFromRegion(entity, entity.region, map); - } - - this.entityById.delete(entity.id); - return removed; - } - removeEntityFromSomeMap(entity: ServerEntity) { - const map = this.maps.find(m => m.regions.some(r => includes(r.entities, entity))); - - if (map) { - this.removeEntity(entity, map); - } else { - DEVELOPMENT && logger.error(`Missing map for entity`); - } - } - // map - getMainMap() { - return this.maps[0]; - } - switchToMap(client: IClient, map: ServerMap, x: number, y: number) { - if (client.map === map) { - DEVELOPMENT && logger.error(`Switching to the same map`); - return; - } - - if (this.mapSwitchQueue.some(x => x.client === client)) { - DEVELOPMENT && logger.error(`Already in map switch queue`); - return; - } - - this.mapSwitchQueue.push({ client, map, x, y }); - - client.isSwitchingMap = true; - client.pony.vx = 0; - client.pony.vy = 0; - updateEntity(client.pony, false); - client.mapSwitching(); - } - actualSwitchToMap(client: IClient, map: ServerMap, x: number, y: number) { - unsubscribeFromAllRegions(client, false); - - if (client.pony.region) { - removeFromRegion(client.pony, client.pony.region, client.map); - } - - x = clamp(x, 0, map.width); - y = clamp(y, 0, map.height); - - resetClientUpdates(client); - - client.mapState(getMapInfo(map), map.state); - client.map = map; - client.pony.x = x; - client.pony.y = y; - client.safeX = x; - client.safeY = y; - client.lastTime = 0; - client.lastMapSwitch = Date.now(); - client.loading = true; - client.lastCameraX = 0; - client.lastCameraY = 0; - client.lastCameraW = 0; - client.lastCameraH = 0; - client.isSwitchingMap = false; - - addToRegion(client.pony, getRegionGlobal(map, x, y), map); - fixPosition(client.pony, map, x, y, true); - - client.reporter.systemLog(`Switched map to [${client.map.id || 'main'}]`); - } - // main - initialize(now: number) { - this.start = now; - this.now = now; - const nowSeconds = now / 1000; - - for (const controller of this.controllers) { - controller.initialize(nowSeconds); - } - - for (const map of this.maps) { - for (const controller of map.controllers) { - controller.initialize(nowSeconds); - } - } - } - update(delta: number, now: number) { - const started = Date.now(); - - timingStart('world.update()'); - - resetEncodeUpdate(); - - this.now = now; - - const nowSeconds = now / 1000; - const deltaSeconds = delta / 1000; - - timingStart('update tiles'); - for (const map of this.maps) { - if (!map.dontUpdateTilesAndColliders) { - for (const region of map.regions) { - if (region.tilesDirty) { - updateTileIndices(region, map); - } - } - } - } - timingEnd(); - - timingStart('update colliders'); - for (const map of this.maps) { - if (!map.dontUpdateTilesAndColliders) { - for (const region of map.regions) { - if (region.colliderDirty) { - generateRegionCollider(region, map); - } - } - } - } - timingEnd(); - - timingStart('update positions'); - for (const map of this.maps) { - for (const region of map.regions) { - // TODO: update only moving entities, separate list of movingEntities - for (const entity of region.movables) { - // TODO: make sure timestamp is initialized if entity is moving - const delta = nowSeconds - entity.timestamp; - - if (delta > 0) { - if (entity.vx !== 0 || entity.vy !== 0) { - timingStart('updatePosition()'); - updatePosition(entity, delta, map); - timingEnd(); - } - - entity.timestamp = nowSeconds; - } - } - } - } - timingEnd(); - - timingStart('updateCamera + updateSubscriptions'); - for (const client of this.clients) { - if (updateClientCamera(client)) { - unsubscribeFromOutOfRangeRegions(client); - subscribeToRegionsInRange(client); - } - } - timingEnd(); - - timingStart('update controllers'); - for (const controller of this.controllers) { - controller.update(deltaSeconds, nowSeconds); - } - - for (const map of this.maps) { - for (const controller of map.controllers) { - controller.update(deltaSeconds, nowSeconds); - } - } - timingEnd(); - - timingStart('actualSwitchToMap'); - for (let i = 0; i < MAP_SWITCHES_PER_UPDATE && this.mapSwitchQueue.length; i++) { - const { client, map, x, y } = this.mapSwitchQueue.shift()!; - this.actualSwitchToMap(client, map, x, y); - } - timingEnd(); - - timingStart('updateRegions'); - updateRegions(this.maps); // NOTE: creates transfers - timingEnd(); - - timingStart('timeoutEntityExpression + inTheAirDelay'); - for (const { pony } of this.clients) { - // timeout expressions - if (pony.exprTimeout && pony.exprTimeout < now) { - setEntityExpression(pony, undefined); // NOTE: creates updates - } - - // count down in-the-air delay - if (pony.inTheAirDelay !== undefined && pony.inTheAirDelay > 0) { - pony.inTheAirDelay -= deltaSeconds; - } - } - timingEnd(); - - // const { totalUpdates, reusedUpdates } = this.updatesStats(); - - timingStart(`commitRegionUpdates`); // [${totalUpdates} / ${reusedUpdates}]`); - for (const map of this.maps) { - commitRegionUpdates(map.regions); - } - timingEnd(); - - let clientsWithAdds = 0; - let clientsWithUpdates = 0; - let clientsWithSays = 0; - let totalSays = 0; - - timingStart(`send updates`); - for (const client of this.clients) { - const { updateQueue, regionUpdates, saysQueue, unsubscribes, subscribes } = client; - const updateBuffer = updateQueue.offset ? getWriterBuffer(updateQueue) : null; - const total = updateQueue.offset + regionUpdates.length + saysQueue.length + unsubscribes.length + subscribes.length; - - if (total !== 0) { - if (updateQueue.offset > 0) - clientsWithAdds++; - if (regionUpdates.length > 0) - clientsWithUpdates++; - if (saysQueue.length > 0) - clientsWithSays++; - totalSays += saysQueue.length; - - setupTiming(client); - timingStart('client.update()'); - client.update(unsubscribes, subscribes, updateBuffer, regionUpdates, saysQueue); - timingEnd(); - clearTiming(client); - - resetClientUpdates(client); - } - } - timingEnd(); - - timingStart('joinQueuedClients'); - if (Date.now() < (started + (1000 / SERVER_FPS))) { - for (let i = 0; i < JOINS_PER_UPDATE && this.joinQueue.length > 0; i++) { - this.joinClientToWorld(this.joinQueue.shift()!); // NOTE: creates adds - } - } - timingEnd(); - - const { isCollidingCount, isCollidingObjectCount } = getCollisionStats(); - - timingStart(`adds [${clientsWithAdds}]\n` + - `updates [${clientsWithUpdates}]\n` + - `says [${totalSays} / ${clientsWithSays}]\n` + - `sockets [${this.socketStatsText()}]\n` + - `collisions [${isCollidingObjectCount} / ${isCollidingCount}]`); - this.cleanupOfflineClients(); - timingEnd(); - - timingEnd(); - } - sparseUpdate(now: number) { - timingStart('world.sparseUpdate()'); - - timingStart('sparse update controllers'); - for (const controller of this.controllers) { - if (controller.sparseUpdate !== undefined) { - controller.sparseUpdate(); - } - } - - for (const map of this.maps) { - for (const controller of map.controllers) { - if (controller.sparseUpdate !== undefined) { - controller.sparseUpdate(); - } - } - } - timingEnd(); - - timingStart('sparseRegionUpdate'); - for (const map of this.maps) { - for (const region of map.regions) { - sparseRegionUpdate(map, region, this.options); - } - } - timingEnd(); - - timingStart('kick afk clients'); - for (const client of this.clients) { - if ((now - client.lastPacket) > AFK_TIMEOUT) { - this.kick(client, 'afk'); - } - } - timingEnd(); - - timingStart('send queue status (join)'); - for (let i = 0; i < this.joinQueue.length; i++) { - this.joinQueue[i].queue(i + 1); - } - timingEnd(); - - timingStart('send queue status (map)'); - for (let i = 0; i < this.mapSwitchQueue.length; i++) { - this.mapSwitchQueue[i].client.queue(i + 1); - } - timingEnd(); - - timingStart('cleanup unused maps'); - const mapDiscardThreshold = now - MAP_DISCARD_TIMEOUT; - - for (const map of this.maps) { - if (map.instance && (hasAnyClients(map) || this.mapSwitchQueue.some(q => q.map === map))) { - map.lastUsed = now; - } - } - - for (let i = this.maps.length - 1; i > 0; i--) { - const map = this.maps[i]; - - if (map.instance && map.lastUsed < mapDiscardThreshold) { - this.maps.splice(i, 1); - // const pool = this.mapPools.get(map.id); - - // if (pool && pool.dispose(map)) { - // for (const region of map.regions) { - // resetRegionUpdates(region); - // } - // } else { - for (const region of map.regions) { - for (const entity of region.entities) { - this.entityById.delete(entity.id); - } - } - // } - } - } - timingEnd(); - - timingStart('cleanup reserved ids'); - const threshold = Date.now() - 5 * MINUTE; - - for (const key of Array.from(this.reservedIdsByKey.keys())) { // TODO: avoid doing this to save gc - const { id, time } = this.reservedIdsByKey.get(key)!; - - if (time < threshold) { - this.reservedIdsByKey.delete(key); - this.reservedIds.delete(id); - } - } - timingEnd(); - - timingStart('cleanup parties'); - this.partyService.cleanupParties(); - timingEnd(); - - timingEnd(); - } - private socketStatsText() { - const { sent, received, sentPackets, receivedPackets } = this.socketStats.stats(); - return `sent: ${(sent / 1024).toFixed(2)} kb (${sentPackets}), ` + - `recv: ${(received / 1024).toFixed(2)} kb (${receivedPackets})`; - } - updatesStats() { - timingStart('updatesStats()'); - - let totalUpdates = 0, reusedUpdates = 0; - - for (const map of this.maps) { - for (const region of map.regions) { - totalUpdates += region.entityUpdates.length; - reusedUpdates += region.reusedUpdates; - } - } - - timingEnd(); - - return { totalUpdates, reusedUpdates }; - } - // clients - private lastCleanup = 0; - private cleanupOfflineClients() { - const now = Date.now(); - - if ((now - this.lastCleanup) > REMOVE_INTERVAL) { - this.lastCleanup = now; - const removeFrom = now - REMOVE_TIMEOUT; - - remove(this.offlineClients, c => c.offline && !c.party && c.offlineAt && c.offlineAt.getTime() < removeFrom); - } - } - joinClientToQueue(client: IClient) { - if (this.liveSettings.shutdown) { - client.leaveReason = 'shutdown'; - client.disconnect(false, true); - return; - } - - const { tokenId } = client; - - function findClientsToKick(clients: IClient[]) { - return clients.filter(c => c.tokenId === tokenId); - } - - const clientsToKick = [ - ...findClientsToKick(this.clients), - ...findClientsToKick(this.joinQueue), - ]; - - for (const client of clientsToKick) { - const reason = client.tokenId === tokenId ? 'kicked [joining again]' : 'kicked [alone on ip]'; - this.kick(client, reason, LeaveReason.None, true); - } - - // TODO: wait for all clients to be kicked before adding to the queue - // another queue before joinQueue - - this.joinQueue.push(client); - } - joinClientToWorld(client: IClient) { - timingStart('joinClientToWorld()'); - - const key = `${client.accountId}:${client.characterId}`; - const reserved = this.reservedIdsByKey.get(key); - - if (reserved) { - client.pony.id = reserved.id; - this.reservedIdsByKey.delete(client.accountId); - this.reservedIds.delete(reserved.id); - } else { - client.pony.id = this.getNewEntityId(); - } - - client.myEntity(client.pony.id, client.characterName, client.character.info!, client.characterId, client.pony.crc || 0); - - this.clients.push(client); - this.clientsByAccount.set(client.accountId, client); - this.partyService.clientConnected(client); - this.hidingService.connected(client); - - let map = findOrCreateMapForClient(this, client.characterState.map || '', client); - - if (!map) { - map = this.getMainMap(); - const { x, y } = randomPoint(map.spawnArea); - client.pony.x = x; - client.pony.y = y; - } - - if (isStaticCollision(client.pony, map)) { - if (!fixCollision(client.pony, map)) { - const { x, y } = randomPoint(map.spawnArea); - client.pony.x = x; - client.pony.y = y; - } - } - - client.pony.x = roundPositionXMidPixel(client.pony.x); - client.pony.y = roundPositionYMidPixel(client.pony.y); - client.map = map; - - centerCameraOn(client.camera, client.pony); - - client.worldState(this.getState(), true); - client.mapState(getMapInfo(client.map), client.map.state); - - if (BETA) { - timingStart('minimap'); - client.mapTest(client.map.width, client.map.height, createMinimap(this, client.map)); - timingEnd(); - } - - updateCamera(client.camera, client.pony, map); - - this.addEntity(client.pony, client.map); - - const visibleOnlineFriends: IClient[] = []; - - for (const c of this.clients) { - if (c.selected && c.selected.client && c.selected.client.accountId === client.accountId) { - c.updateSelection(c.selected.id, client.pony.id); - } - - if (client.friends.has(c.accountId)) { - if (!c.accountSettings.hidden && !c.shadowed) { - visibleOnlineFriends.push(c); - } - - if (!client.accountSettings.hidden) { - c.updateFriends([toFriendOnline(client)], false); - } - - if (!c.friends.has(client.accountId)) { - reloadFriends(c).catch(e => logger.error(e)); - } - } else if (c.friends.has(client.accountId)) { - reloadFriends(c).catch(e => logger.error(e)); - } - } - - if (visibleOnlineFriends.length) { - client.updateFriends(visibleOnlineFriends.map(toFriendOnline), false); - } - - if (this.liveSettings.updating) { - this.notifications.addNotification(client, updateNotification()); - } - - // TEMP: duplicate pony bug - if (client.map.instance) { - for (const region of client.map.regions) { - for (const entity of region.entities) { - if (entity.client !== undefined && entity !== client.pony && entity.client.accountId === client.accountId) { - const sameClients = this.clients.filter(c => c.accountId === client.accountId).length; - - client.reporter.systemLog(`Client pony already on map ` + - `(old: ${entity.id}, new: ${client.pony.id}, sameClients: ${sameClients})`); - } - } - } - } - - timingEnd(); - } - getClientByEntityId(entityId: number) { - if (entityId === 0) { - return undefined; - } else { - const byPonyId = (c: IClient) => c.pony.id === entityId; - return this.clients.find(byPonyId) || this.offlineClients.find(byPonyId); - } - } - private removeEntityFromAnyMap(entity: ServerEntity) { - for (const map of this.maps) { - for (const region of map.regions) { - const index = region.entities.indexOf(entity); - - if (index !== - 1) { - removeEntityFromRegion(region, entity, map); - return map; - } - } - } - - return undefined; - } - leaveClient(client: IClient) { - const friends = findAllOnlineFriends(this, client); - - if (!client.accountSettings.hidden) { - for (const friend of friends) { - friend.updateFriends([toFriendOffline(client)], false); - } - } - - this.reservedIds.set(client.pony.id, client.accountId); - this.reservedIdsByKey.set(`${client.accountId}:${client.characterId}`, { id: client.pony.id, time: Date.now() }); - - if (!this.removeEntity(client.pony, client.map)) { - const map = this.removeEntityFromAnyMap(client.pony); - client.reporter.systemLog(`Removing from any map (` + - `expected: ${client.map && client.map.id} [${client.map && client.map.instance}], ` + - `actual: ${map && map.id} [${map && map.instance}])`); - } - - unsubscribeFromAllRegions(client, true); - removeItem(this.joinQueue, client); - removeItem(this.clients, client); - this.clientsByAccount.delete(client.accountId); - this.offlineClients.push(client); - - const index = this.mapSwitchQueue.findIndex(x => x.client === client); - - if (index !== -1) { - this.mapSwitchQueue.splice(index, 1); - } - } - notifyHidden(by: string, who: string) { - const byClient = findClientByAccountId(this, by); - const whoClient = findClientByAccountId(this, who); - - if (byClient && whoClient) { - updateEntityPlayerState(byClient, whoClient.pony); - updateEntityPlayerState(whoClient, byClient.pony); - } - } - resetToSpawn(client: IClient) { - Object.assign(client.pony, randomPoint(client.map.spawnArea)); - } - kick(client: IClient | undefined, leaveReason = 'kicked', reason = LeaveReason.None, force = false) { - if (client) { - removeItem(this.joinQueue, client); - this.notifications.rejectAll(client); - this.leaveClient(client); - client.leaveReason = leaveReason; - client.left(reason); - - if (force) { - client.disconnect(true); - } else { - setTimeout(() => { - if (client.isConnected) { - client.disconnect(true); - } - }, 200); - } - } - - return !!client; - } - kickAll() { - this.clients.slice().forEach(c => this.kick(c, 'kickAll')); - this.joinQueue.slice().forEach(c => c.disconnect()); - this.joinQueue = []; - } - kickByAccount(accountId: string) { - return this.kick(findClientByAccountId(this, accountId), 'kickByAccount'); - } - kickByCharacter(characterId: string) { - return this.kick(findClientByCharacterId(this, characterId), 'kickByCharacter'); - } - accountUpdated(account: IAccount) { - const accountId = account._id.toString(); - const client = findClientByAccountId(this, accountId); - - if (client) { - this.updateClientAccount(client, account); - - for (const c of this.clients) { - if (c.isMod && c.selected === client.pony) { - pushUpdateEntityToClient(c, { entity: client.pony, flags: UpdateFlags.Options, options: { modInfo: getModInfo(client) } }); - } - } - } - } - private updateClientAccount(client: IClient, newAccount: IAccount) { - const oldAccount = client.account; - client.account = newAccount; - - if (oldAccount.ban !== newAccount.ban && isBanned(newAccount)) { - sendAcl(client); - this.kick(client, 'kick (ban)'); - return; - } - - if (oldAccount.shadow !== newAccount.shadow) { - if (isShadowed(newAccount)) { - this.shadow(client); - } else if (isShadowed(oldAccount)) { - sendAcl(client); - this.kick(client, 'kick (unshadow)'); - return; - } - } - - const shouldSendAcl = oldAccount.mute !== newAccount.mute - || oldAccount.ban !== newAccount.ban - || oldAccount.shadow !== newAccount.shadow; - - if (shouldSendAcl) { - sendAcl(client); - } - } - private shadow(client: IClient) { - client.shadowed = true; - this.partyService.clientDisconnected(client); - this.friendsService.clientDisconnected(client); - this.notifications.dismissAll(client); - - if (client.pony.region) { - for (const c of client.pony.region.clients) { - if (c !== client) { - pushRemoveEntityToClient(c, client.pony); - } - } - } - } - // update notification - notifyUpdate() { - for (const client of this.clients) { - this.notifications.addNotification(client, updateNotification()); - } - } - saveClientStates() { - for (const client of this.clients) { - createAndUpdateCharacterState(client, this.server); - } - } + season = Season.Summer; + holiday = Holiday.None; + maps: ServerMap[] = []; + controllers: Controller[] = []; + options = { + restoreTerrain: !DEVELOPMENT, + }; + clients: IClient[] = []; + clientsByAccount = new Map(); + joinQueue: IClient[] = []; + mapSwitchQueue: MapSwitch[] = []; + now = 0; + start = 0; + // mapPools = new Map>(); + private maxId = 0 >>> 0; + private offlineClients: IClient[] = []; + private baseTime = 0; + private entityById = new Map(); + private reservedIds = new Map(); + private reservedIdsByKey = new Map(); + constructor( + public readonly server: ServerConfig, + private readonly partyService: PartyService, + private readonly friendsService: FriendsService, + public readonly hidingService: HidingService, + private readonly notifications: NotificationService, + private readonly getSettings: GetSettings, + private readonly liveSettings: ServerLiveSettings, + private readonly socketStats: SocketStats, + ) { + // this.mapPools.set('island', createPool(10, () => createIslandMap(this, true), resetIslandMap)); + // this.mapPools.set('house', createPool(10, () => createHouseMap(this, true), resetHouseMap)); + + partyService.partyChanged.subscribe(client => { + if (client.isConnected && client.map.usage === MapUsage.Party) { + if ( + client.party && client.party.leader === client && client.map.instance === client.accountId && + !this.maps.some(m => m.id === client.map.id && m.instance === client.party!.id) + ) { + client.map.instance = client.party.id; + } else { + refreshMap(this, client); + } + } + }); + } + get featureFlags() { + return this.server.flags; + } + // entities + get time() { + return this.baseTime + Date.now(); + } + setTime(hour: number) { + let newBaseTime = hour * HOUR_LENGTH - (Date.now() % DAY_LENGTH); + + while (newBaseTime < 0) { + newBaseTime += DAY_LENGTH; + } + + this.baseTime = newBaseTime; + this.updateWorldState(); + } + setTile(map: ServerMap, x: number, y: number, type: TileType) { + if (!BETA && map.tilesLocked) + return; + + if (x >= 0 && y >= 0 && x < map.width && y < map.height && !isTileLocked(map, x, y) && type !== getTile(map, x, y)) { + setTile(map, x, y, type); + } + } + toggleWall(map: ServerMap, x: number, y: number, type: TileType) { + for (const controller of map.controllers) { + if (controller.toggleWall) { + controller.toggleWall(x, y, type); + } + } + } + getState(): WorldState { + return { + time: this.time, + season: this.season, + holiday: this.holiday, + flags: this.getSettings().filterSwears ? WorldStateFlags.Safe : WorldStateFlags.None, + featureFlags: this.featureFlags, + }; + } + setSeason(season: Season, holiday: Holiday) { + this.season = season; + this.holiday = holiday; + this.updateWorldState(); + updateMainMapSeason(this, this.getMainMap(), season, holiday); + } + private updateWorldState() { + const state = this.getState(); + + for (const client of this.clients) { + client.worldState(state, false); + } + } + getEntityById(id: number) { + return this.entityById.get(id); + } + getNewEntityId() { + do { + this.maxId = (this.maxId + 1) >>> 0; + } while (this.maxId === 0 || this.entityById.has(this.maxId) || this.reservedIds.has(this.maxId)); + + return this.maxId; + } + addEntity(entity: ServerEntity, map: ServerMap) { + if (DEVELOPMENT) { + if (entity.update) { + console.error('Entity update() method is only for client-side use'); + } + + if (entity.id && this.entityById.has(entity.id)) { + console.error(`Entity already added to the world ${getEntityTypeName(entity.type)} [${entity.id}]`); + } + } + + entity.id = entity.id || this.getNewEntityId(); + entity.timestamp = this.now / 1000; + this.entityById.set(entity.id, entity); + roundPosition(entity); + const region = getRegionGlobal(map, entity.x, entity.y); + addToRegion(entity, region, map); + return entity; + } + removeEntity(entity: ServerEntity, map: ServerMap) { + let removed = false; + + if (entity.region) { + removed = removeFromRegion(entity, entity.region, map); + } + + this.entityById.delete(entity.id); + return removed; + } + removeEntityFromSomeMap(entity: ServerEntity) { + const map = this.maps.find(m => m.regions.some(r => includes(r.entities, entity))); + + if (map) { + this.removeEntity(entity, map); + } else { + DEVELOPMENT && logger.error(`Missing map for entity`); + } + } + // map + getMainMap() { + return this.maps[0]; + } + switchToMap(client: IClient, map: ServerMap, x: number, y: number) { + if (client.map === map) { + DEVELOPMENT && logger.error(`Switching to the same map`); + return; + } + + if (this.mapSwitchQueue.some(x => x.client === client)) { + DEVELOPMENT && logger.error(`Already in map switch queue`); + return; + } + + this.mapSwitchQueue.push({ client, map, x, y }); + + client.isSwitchingMap = true; + client.pony.vx = 0; + client.pony.vy = 0; + updateEntity(client.pony, false); + client.mapSwitching(); + } + actualSwitchToMap(client: IClient, map: ServerMap, x: number, y: number) { + unsubscribeFromAllRegions(client, false); + + if (client.pony.region) { + removeFromRegion(client.pony, client.pony.region, client.map); + } + + x = clamp(x, 0, map.width); + y = clamp(y, 0, map.height); + + resetClientUpdates(client); + + client.mapState(getMapInfo(map), map.state); + client.map = map; + client.pony.x = x; + client.pony.y = y; + client.safeX = x; + client.safeY = y; + client.lastTime = 0; + client.lastMapSwitch = Date.now(); + client.loading = true; + client.lastCameraX = 0; + client.lastCameraY = 0; + client.lastCameraW = 0; + client.lastCameraH = 0; + client.isSwitchingMap = false; + + addToRegion(client.pony, getRegionGlobal(map, x, y), map); + fixPosition(client.pony, map, x, y, true); + + client.reporter.systemLog(`Switched map to [${client.map.id || 'main'}]`); + } + // main + initialize(now: number) { + this.start = now; + this.now = now; + const nowSeconds = now / 1000; + + for (const controller of this.controllers) { + controller.initialize(nowSeconds); + } + + for (const map of this.maps) { + for (const controller of map.controllers) { + controller.initialize(nowSeconds); + } + } + } + update(delta: number, now: number) { + const started = Date.now(); + + timingStart('world.update()'); + + resetEncodeUpdate(); + + this.now = now; + + const nowSeconds = now / 1000; + const deltaSeconds = delta / 1000; + + timingStart('update tiles'); + for (const map of this.maps) { + if (!map.dontUpdateTilesAndColliders) { + for (const region of map.regions) { + if (region.tilesDirty) { + updateTileIndices(region, map); + } + } + } + } + timingEnd(); + + timingStart('update colliders'); + for (const map of this.maps) { + if (!map.dontUpdateTilesAndColliders) { + for (const region of map.regions) { + if (region.colliderDirty) { + generateRegionCollider(region, map); + } + } + } + } + timingEnd(); + + timingStart('update positions'); + for (const map of this.maps) { + for (const region of map.regions) { + // TODO: update only moving entities, separate list of movingEntities + for (const entity of region.movables) { + // TODO: make sure timestamp is initialized if entity is moving + const delta = nowSeconds - entity.timestamp; + + if (delta > 0) { + if (entity.vx !== 0 || entity.vy !== 0) { + timingStart('updatePosition()'); + updatePosition(entity, delta, map); + timingEnd(); + } + + entity.timestamp = nowSeconds; + } + } + } + } + timingEnd(); + + timingStart('updateCamera + updateSubscriptions'); + for (const client of this.clients) { + if (updateClientCamera(client)) { + unsubscribeFromOutOfRangeRegions(client); + subscribeToRegionsInRange(client); + } + } + timingEnd(); + + timingStart('update controllers'); + for (const controller of this.controllers) { + controller.update(deltaSeconds, nowSeconds); + } + + for (const map of this.maps) { + for (const controller of map.controllers) { + controller.update(deltaSeconds, nowSeconds); + } + } + timingEnd(); + + timingStart('actualSwitchToMap'); + for (let i = 0; i < MAP_SWITCHES_PER_UPDATE && this.mapSwitchQueue.length; i++) { + const { client, map, x, y } = this.mapSwitchQueue.shift()!; + this.actualSwitchToMap(client, map, x, y); + } + timingEnd(); + + timingStart('updateRegions'); + updateRegions(this.maps); // NOTE: creates transfers + timingEnd(); + + timingStart('timeoutEntityExpression + inTheAirDelay'); + for (const { pony } of this.clients) { + // timeout expressions + if (pony.exprTimeout && pony.exprTimeout < now) { + setEntityExpression(pony, undefined); // NOTE: creates updates + } + + // count down in-the-air delay + if (pony.inTheAirDelay !== undefined && pony.inTheAirDelay > 0) { + pony.inTheAirDelay -= deltaSeconds; + } + } + timingEnd(); + + // const { totalUpdates, reusedUpdates } = this.updatesStats(); + + timingStart(`commitRegionUpdates`); // [${totalUpdates} / ${reusedUpdates}]`); + for (const map of this.maps) { + commitRegionUpdates(map.regions); + } + timingEnd(); + + let clientsWithAdds = 0; + let clientsWithUpdates = 0; + let clientsWithSays = 0; + let totalSays = 0; + + timingStart(`send updates`); + for (const client of this.clients) { + const { updateQueue, regionUpdates, saysQueue, unsubscribes, subscribes } = client; + const updateBuffer = updateQueue.offset ? getWriterBuffer(updateQueue) : null; + const total = updateQueue.offset + regionUpdates.length + saysQueue.length + unsubscribes.length + subscribes.length; + + if (total !== 0) { + if (updateQueue.offset > 0) + clientsWithAdds++; + if (regionUpdates.length > 0) + clientsWithUpdates++; + if (saysQueue.length > 0) + clientsWithSays++; + totalSays += saysQueue.length; + + setupTiming(client); + timingStart('client.update()'); + client.update(unsubscribes, subscribes, updateBuffer, regionUpdates, saysQueue); + timingEnd(); + clearTiming(client); + + resetClientUpdates(client); + } + } + timingEnd(); + + timingStart('joinQueuedClients'); + if (Date.now() < (started + (1000 / SERVER_FPS))) { + for (let i = 0; i < JOINS_PER_UPDATE && this.joinQueue.length > 0; i++) { + this.joinClientToWorld(this.joinQueue.shift()!); // NOTE: creates adds + } + } + timingEnd(); + + const { isCollidingCount, isCollidingObjectCount } = getCollisionStats(); + + timingStart(`adds [${clientsWithAdds}]\n` + + `updates [${clientsWithUpdates}]\n` + + `says [${totalSays} / ${clientsWithSays}]\n` + + `sockets [${this.socketStatsText()}]\n` + + `collisions [${isCollidingObjectCount} / ${isCollidingCount}]`); + this.cleanupOfflineClients(); + timingEnd(); + + timingEnd(); + } + sparseUpdate(now: number) { + timingStart('world.sparseUpdate()'); + + timingStart('sparse update controllers'); + for (const controller of this.controllers) { + if (controller.sparseUpdate !== undefined) { + controller.sparseUpdate(); + } + } + + for (const map of this.maps) { + for (const controller of map.controllers) { + if (controller.sparseUpdate !== undefined) { + controller.sparseUpdate(); + } + } + } + timingEnd(); + + timingStart('sparseRegionUpdate'); + for (const map of this.maps) { + for (const region of map.regions) { + sparseRegionUpdate(map, region, this.options); + } + } + timingEnd(); + + timingStart('kick afk clients'); + for (const client of this.clients) { + if ((now - client.lastPacket) > AFK_TIMEOUT) { + this.kick(client, 'afk'); + } + } + timingEnd(); + + timingStart('send queue status (join)'); + for (let i = 0; i < this.joinQueue.length; i++) { + this.joinQueue[i].queue(i + 1); + } + timingEnd(); + + timingStart('send queue status (map)'); + for (let i = 0; i < this.mapSwitchQueue.length; i++) { + this.mapSwitchQueue[i].client.queue(i + 1); + } + timingEnd(); + + timingStart('cleanup unused maps'); + const mapDiscardThreshold = now - MAP_DISCARD_TIMEOUT; + + for (const map of this.maps) { + if (map.instance && (hasAnyClients(map) || this.mapSwitchQueue.some(q => q.map === map))) { + map.lastUsed = now; + } + } + + for (let i = this.maps.length - 1; i > 0; i--) { + const map = this.maps[i]; + + if (map.instance && map.lastUsed < mapDiscardThreshold) { + this.maps.splice(i, 1); + // const pool = this.mapPools.get(map.id); + + // if (pool && pool.dispose(map)) { + // for (const region of map.regions) { + // resetRegionUpdates(region); + // } + // } else { + for (const region of map.regions) { + for (const entity of region.entities) { + this.entityById.delete(entity.id); + } + } + // } + } + } + timingEnd(); + + timingStart('cleanup reserved ids'); + const threshold = Date.now() - 5 * MINUTE; + + for (const key of Array.from(this.reservedIdsByKey.keys())) { // TODO: avoid doing this to save gc + const { id, time } = this.reservedIdsByKey.get(key)!; + + if (time < threshold) { + this.reservedIdsByKey.delete(key); + this.reservedIds.delete(id); + } + } + timingEnd(); + + timingStart('cleanup parties'); + this.partyService.cleanupParties(); + timingEnd(); + + timingEnd(); + } + private socketStatsText() { + const { sent, received, sentPackets, receivedPackets } = this.socketStats.stats(); + return `sent: ${(sent / 1024).toFixed(2)} kb (${sentPackets}), ` + + `recv: ${(received / 1024).toFixed(2)} kb (${receivedPackets})`; + } + updatesStats() { + timingStart('updatesStats()'); + + let totalUpdates = 0, reusedUpdates = 0; + + for (const map of this.maps) { + for (const region of map.regions) { + totalUpdates += region.entityUpdates.length; + reusedUpdates += region.reusedUpdates; + } + } + + timingEnd(); + + return { totalUpdates, reusedUpdates }; + } + // clients + private lastCleanup = 0; + private cleanupOfflineClients() { + const now = Date.now(); + + if ((now - this.lastCleanup) > REMOVE_INTERVAL) { + this.lastCleanup = now; + const removeFrom = now - REMOVE_TIMEOUT; + + remove(this.offlineClients, c => c.offline && !c.party && c.offlineAt && c.offlineAt.getTime() < removeFrom); + } + } + joinClientToQueue(client: IClient) { + if (this.liveSettings.shutdown) { + client.leaveReason = 'shutdown'; + client.disconnect(false, true); + return; + } + + const { tokenId } = client; + + function findClientsToKick(clients: IClient[]) { + return clients.filter(c => c.tokenId === tokenId); + } + + const clientsToKick = [ + ...findClientsToKick(this.clients), + ...findClientsToKick(this.joinQueue), + ]; + + for (const client of clientsToKick) { + const reason = client.tokenId === tokenId ? 'kicked [joining again]' : 'kicked [alone on ip]'; + this.kick(client, reason, LeaveReason.None, true); + } + + // TODO: wait for all clients to be kicked before adding to the queue + // another queue before joinQueue + + this.joinQueue.push(client); + } + joinClientToWorld(client: IClient) { + timingStart('joinClientToWorld()'); + + const key = `${client.accountId}:${client.characterId}`; + const reserved = this.reservedIdsByKey.get(key); + + if (reserved) { + client.pony.id = reserved.id; + this.reservedIdsByKey.delete(client.accountId); + this.reservedIds.delete(reserved.id); + } else { + client.pony.id = this.getNewEntityId(); + } + + client.myEntity(client.pony.id, client.characterName, client.character.info!, client.characterId, client.pony.crc || 0); + + this.clients.push(client); + this.clientsByAccount.set(client.accountId, client); + this.partyService.clientConnected(client); + this.hidingService.connected(client); + + let map = findOrCreateMapForClient(this, client.characterState.map || '', client); + + if (!map) { + map = this.getMainMap(); + const { x, y } = randomPoint(map.spawnArea); + client.pony.x = x; + client.pony.y = y; + } + + if (isStaticCollision(client.pony, map)) { + if (!fixCollision(client.pony, map)) { + const { x, y } = randomPoint(map.spawnArea); + client.pony.x = x; + client.pony.y = y; + } + } + + client.pony.x = roundPositionXMidPixel(client.pony.x); + client.pony.y = roundPositionYMidPixel(client.pony.y); + client.map = map; + + centerCameraOn(client.camera, client.pony); + + client.worldState(this.getState(), true); + client.mapState(getMapInfo(client.map), client.map.state); + + if (BETA) { + timingStart('minimap'); + client.mapTest(client.map.width, client.map.height, createMinimap(this, client.map)); + timingEnd(); + } + + updateCamera(client.camera, client.pony, map); + + this.addEntity(client.pony, client.map); + + const visibleOnlineFriends: IClient[] = []; + + for (const c of this.clients) { + if (c.selected && c.selected.client && c.selected.client.accountId === client.accountId) { + c.updateSelection(c.selected.id, client.pony.id); + } + + if (client.friends.has(c.accountId)) { + if (!c.accountSettings.hidden && !c.shadowed) { + visibleOnlineFriends.push(c); + } + + if (!client.accountSettings.hidden) { + c.updateFriends([toFriendOnline(client)], false); + } + + if (!c.friends.has(client.accountId)) { + reloadFriends(c).catch(e => logger.error(e)); + } + } else if (c.friends.has(client.accountId)) { + reloadFriends(c).catch(e => logger.error(e)); + } + } + + if (visibleOnlineFriends.length) { + client.updateFriends(visibleOnlineFriends.map(toFriendOnline), false); + } + + if (this.liveSettings.updating) { + this.notifications.addNotification(client, updateNotification()); + } + + // TEMP: duplicate pony bug + if (client.map.instance) { + for (const region of client.map.regions) { + for (const entity of region.entities) { + if (entity.client !== undefined && entity !== client.pony && entity.client.accountId === client.accountId) { + const sameClients = this.clients.filter(c => c.accountId === client.accountId).length; + + client.reporter.systemLog(`Client pony already on map ` + + `(old: ${entity.id}, new: ${client.pony.id}, sameClients: ${sameClients})`); + } + } + } + } + + timingEnd(); + } + getClientByEntityId(entityId: number) { + if (entityId === 0) { + return undefined; + } else { + const byPonyId = (c: IClient) => c.pony.id === entityId; + return this.clients.find(byPonyId) || this.offlineClients.find(byPonyId); + } + } + private removeEntityFromAnyMap(entity: ServerEntity) { + for (const map of this.maps) { + for (const region of map.regions) { + const index = region.entities.indexOf(entity); + + if (index !== - 1) { + removeEntityFromRegion(region, entity, map); + return map; + } + } + } + + return undefined; + } + leaveClient(client: IClient) { + const friends = findAllOnlineFriends(this, client); + + if (!client.accountSettings.hidden) { + for (const friend of friends) { + friend.updateFriends([toFriendOffline(client)], false); + } + } + + this.reservedIds.set(client.pony.id, client.accountId); + this.reservedIdsByKey.set(`${client.accountId}:${client.characterId}`, { id: client.pony.id, time: Date.now() }); + + if (!this.removeEntity(client.pony, client.map)) { + const map = this.removeEntityFromAnyMap(client.pony); + client.reporter.systemLog(`Removing from any map (` + + `expected: ${client.map && client.map.id} [${client.map && client.map.instance}], ` + + `actual: ${map && map.id} [${map && map.instance}])`); + } + + unsubscribeFromAllRegions(client, true); + removeItem(this.joinQueue, client); + removeItem(this.clients, client); + this.clientsByAccount.delete(client.accountId); + this.offlineClients.push(client); + + const index = this.mapSwitchQueue.findIndex(x => x.client === client); + + if (index !== -1) { + this.mapSwitchQueue.splice(index, 1); + } + } + notifyHidden(by: string, who: string) { + const byClient = findClientByAccountId(this, by); + const whoClient = findClientByAccountId(this, who); + + if (byClient && whoClient) { + updateEntityPlayerState(byClient, whoClient.pony); + updateEntityPlayerState(whoClient, byClient.pony); + } + } + resetToSpawn(client: IClient) { + Object.assign(client.pony, randomPoint(client.map.spawnArea)); + } + kick(client: IClient | undefined, leaveReason = 'kicked', reason = LeaveReason.None, force = false) { + if (client) { + removeItem(this.joinQueue, client); + this.notifications.rejectAll(client); + this.leaveClient(client); + client.leaveReason = leaveReason; + client.left(reason); + + if (force) { + client.disconnect(true); + } else { + setTimeout(() => { + if (client.isConnected) { + client.disconnect(true); + } + }, 200); + } + } + + return !!client; + } + kickAll() { + this.clients.slice().forEach(c => this.kick(c, 'kickAll')); + this.joinQueue.slice().forEach(c => c.disconnect()); + this.joinQueue = []; + } + kickByAccount(accountId: string) { + return this.kick(findClientByAccountId(this, accountId), 'kickByAccount'); + } + kickByCharacter(characterId: string) { + return this.kick(findClientByCharacterId(this, characterId), 'kickByCharacter'); + } + accountUpdated(account: IAccount) { + const accountId = account._id.toString(); + const client = findClientByAccountId(this, accountId); + + if (client) { + this.updateClientAccount(client, account); + + for (const c of this.clients) { + if (c.isMod && c.selected === client.pony) { + pushUpdateEntityToClient(c, { entity: client.pony, flags: UpdateFlags.Options, options: { modInfo: getModInfo(client) } }); + } + } + } + } + private updateClientAccount(client: IClient, newAccount: IAccount) { + const oldAccount = client.account; + client.account = newAccount; + + if (oldAccount.ban !== newAccount.ban && isBanned(newAccount)) { + sendAcl(client); + this.kick(client, 'kick (ban)'); + return; + } + + if (oldAccount.shadow !== newAccount.shadow) { + if (isShadowed(newAccount)) { + this.shadow(client); + } else if (isShadowed(oldAccount)) { + sendAcl(client); + this.kick(client, 'kick (unshadow)'); + return; + } + } + + const shouldSendAcl = oldAccount.mute !== newAccount.mute + || oldAccount.ban !== newAccount.ban + || oldAccount.shadow !== newAccount.shadow; + + if (shouldSendAcl) { + sendAcl(client); + } + } + private shadow(client: IClient) { + client.shadowed = true; + this.partyService.clientDisconnected(client); + this.friendsService.clientDisconnected(client); + this.notifications.dismissAll(client); + + if (client.pony.region) { + for (const c of client.pony.region.clients) { + if (c !== client) { + pushRemoveEntityToClient(c, client.pony); + } + } + } + } + // update notification + notifyUpdate() { + for (const client of this.clients) { + this.notifications.addNotification(client, updateNotification()); + } + } + saveClientStates() { + for (const client of this.clients) { + createAndUpdateCharacterState(client, this.server); + } + } } // account creation lock function sendAcl(client: IClient) { - const acl = isMutedOrShadowed(client) ? fromNow(12 * HOUR) : new Date(0); - client.actionParam(client.pony.id, Action.ACL, acl.toISOString()); + const acl = isMutedOrShadowed(client) ? fromNow(12 * HOUR) : new Date(0); + client.actionParam(client.pony.id, Action.ACL, acl.toISOString()); } function updateNotification(): ServerNotification { - return { - id: 0, - name: '', - message: 'Server will restart shortly for updates and maintenance', - flags: NotificationFlags.Ok, - }; + return { + id: 0, + name: '', + message: 'Server will restart shortly for updates and maintenance', + flags: NotificationFlags.Ok, + }; } export function refreshMap(world: World, client: IClient) { - const map = findOrCreateMapForClient(world, client.map.id, client); + const map = findOrCreateMapForClient(world, client.map.id, client); - if (map) { - world.switchToMap(client, map, client.pony.x, client.pony.y); - } else { - logger.warn(`Missing map: ${client.map.id}`); - } + if (map) { + world.switchToMap(client, map, client.pony.x, client.pony.y); + } else { + logger.warn(`Missing map: ${client.map.id}`); + } } export function goToMap(world: World, client: IClient, id: string, spawn?: string) { - const map = findOrCreateMapForClient(world, id, client); + const map = findOrCreateMapForClient(world, id, client); - if (map) { - const area = spawn && map.spawns.get(spawn) || map.spawnArea; - const { x, y } = randomPoint(area); - world.switchToMap(client, map, x, y); - } else { - logger.warn(`Missing map: ${id}`); - } + if (map) { + const area = spawn && map.spawns.get(spawn) || map.spawnArea; + const { x, y } = randomPoint(area); + world.switchToMap(client, map, x, y); + } else { + logger.warn(`Missing map: ${id}`); + } } function findOrCreateMapInstance(world: World, id: string, instance: string) { - let map = world.maps.find(m => m.id === id && m.instance === instance); + let map = world.maps.find(m => m.id === id && m.instance === instance); - if (!map) { - // const pool = world.mapPools.get(id); + if (!map) { + // const pool = world.mapPools.get(id); - // if (!pool) { - // throw new Error(`Invalid map id: ${id}`); - // } + // if (!pool) { + // throw new Error(`Invalid map id: ${id}`); + // } - switch (id) { - case 'house': - map = createHouseMap(world, true); - break; - case 'island': - map = createIslandMap(world, true); - break; - default: - throw new Error(`Invalid map id: ${id}`); - } + switch (id) { + case 'house': + map = createHouseMap(world, true); + break; + case 'island': + map = createIslandMap(world, true); + break; + default: + throw new Error(`Invalid map id: ${id}`); + } - // map = pool.create(); - map.instance = instance; - map.lastUsed = Date.now(); - map.controllers.forEach(c => c.initialize(world.now / 1000)); - world.maps.push(map); - } + // map = pool.create(); + map.instance = instance; + map.lastUsed = Date.now(); + map.controllers.forEach(c => c.initialize(world.now / 1000)); + world.maps.push(map); + } - return map; + return map; } function findOrCreateMapForClient(world: World, id: string, client: IClient) { - const map = world.maps.find(m => !m.instance && m.id === id); + const map = world.maps.find(m => !m.instance && m.id === id); - if (map) { - return map; - } else { - if (client.party) { - return findOrCreateMapInstance(world, id, client.party.id); - } else { - return findOrCreateMapInstance(world, id, client.accountId); - } - } + if (map) { + return map; + } else { + if (client.party) { + return findOrCreateMapInstance(world, id, client.party.id); + } else { + return findOrCreateMapInstance(world, id, client.accountId); + } + } } function updateClientCamera(client: IClient) { - const camera = client.camera; + const camera = client.camera; - updateCamera(camera, client.pony, client.map); + updateCamera(camera, client.pony, client.map); - if ( - client.lastCameraX !== camera.x || client.lastCameraY !== camera.y || - client.lastCameraW !== camera.w || client.lastCameraH !== camera.h - ) { - client.lastCameraX = camera.x; - client.lastCameraY = camera.y; - client.lastCameraW = camera.w; - client.lastCameraH = camera.h; - return true; - } else { - return false; - } + if ( + client.lastCameraX !== camera.x || client.lastCameraY !== camera.y || + client.lastCameraW !== camera.w || client.lastCameraH !== camera.h + ) { + client.lastCameraX = camera.x; + client.lastCameraY = camera.y; + client.lastCameraW = camera.w; + client.lastCameraH = camera.h; + return true; + } else { + return false; + } } export function findAllOnlineFriends(world: World, client: IClient) { - return compact(Array.from(client.friends.keys()) - .map(account => findClientByAccountId(world, account))); + return compact(Array.from(client.friends.keys()) + .map(account => findClientByAccountId(world, account))); } export function findClientByAccountId(world: World, accountId: string) { - return world.clientsByAccount.get(accountId); + return world.clientsByAccount.get(accountId); } export function findClientByCharacterId(world: World, characterId: string) { - return world.clients.find(c => c.characterId === characterId); + return world.clients.find(c => c.characterId === characterId); } export function findClientsAroundAccountId(world: World, accountId: string): AroundEntry[] { - const client = findClientByAccountId(world, accountId); + const client = findClientByAccountId(world, accountId); - return client ? world.clients - .filter(c => c !== client && c.map === client.map) - .map(c => ({ - account: c.accountId, - distance: distance(client.pony, c.pony), - party: !!(c.party && c.party === client.party), - })) - .filter(x => x.distance < 5 || x.party) - .sort((a, b) => a.distance - b.distance) - .slice(0, 12) : []; + return client ? world.clients + .filter(c => c !== client && c.map === client.map) + .map(c => ({ + account: c.accountId, + distance: distance(client.pony, c.pony), + party: !!(c.party && c.party === client.party), + })) + .filter(x => x.distance < 5 || x.party) + .sort((a, b) => a.distance - b.distance) + .slice(0, 12) : []; } diff --git a/src/ts/tests/client/canvasUtils.spec.ts b/src/ts/tests/client/canvasUtils.spec.ts index 941ff26..d8a64ba 100644 --- a/src/ts/tests/client/canvasUtils.spec.ts +++ b/src/ts/tests/client/canvasUtils.spec.ts @@ -3,76 +3,76 @@ import { expect } from 'chai'; import { resizeCanvas, resizeCanvasWithRatio } from '../../client/canvasUtils'; describe('canvasUtils', () => { - describe('resizeCanvas()', () => { - it('should resize the canvas', () => { - const canvas = { width: 100, height: 200 } as any; + describe('resizeCanvas()', () => { + it('should resize the canvas', () => { + const canvas = { width: 100, height: 200 } as any; - resizeCanvas(canvas as any, 50, 300); + resizeCanvas(canvas as any, 50, 300); - expect(canvas.width).equal(50); - expect(canvas.height).equal(300); - }); + expect(canvas.width).equal(50); + expect(canvas.height).equal(300); + }); - it('should resize height of the canvas', () => { - const canvas = { width: 100, height: 200 } as any; + it('should resize height of the canvas', () => { + const canvas = { width: 100, height: 200 } as any; - resizeCanvas(canvas as any, 100, 300); + resizeCanvas(canvas as any, 100, 300); - expect(canvas.width).equal(100); - expect(canvas.height).equal(300); - }); + expect(canvas.width).equal(100); + expect(canvas.height).equal(300); + }); - it('should leave canvas the same size', () => { - const canvas = { width: 100, height: 200 } as any; + it('should leave canvas the same size', () => { + const canvas = { width: 100, height: 200 } as any; - resizeCanvas(canvas as any, 100, 200); + resizeCanvas(canvas as any, 100, 200); - expect(canvas.width).equal(100); - expect(canvas.height).equal(200); - }); - }); + expect(canvas.width).equal(100); + expect(canvas.height).equal(200); + }); + }); - describe('resizeCanvasWithRatio()', () => { - it('should resize the canvas', () => { - const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; + describe('resizeCanvasWithRatio()', () => { + it('should resize the canvas', () => { + const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; - resizeCanvasWithRatio(canvas as any, 50, 300); + resizeCanvasWithRatio(canvas as any, 50, 300); - expect(canvas.width).equal(50); - expect(canvas.height).equal(300); - expect(canvas.style.width).equal('50px'); - expect(canvas.style.height).equal('300px'); - }); + expect(canvas.width).equal(50); + expect(canvas.height).equal(300); + expect(canvas.style.width).equal('50px'); + expect(canvas.style.height).equal('300px'); + }); - it('should resize height of the canvas', () => { - const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; + it('should resize height of the canvas', () => { + const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; - resizeCanvasWithRatio(canvas as any, 100, 300); + resizeCanvasWithRatio(canvas as any, 100, 300); - expect(canvas.width).equal(100); - expect(canvas.height).equal(300); - expect(canvas.style.width).equal('100px'); - expect(canvas.style.height).equal('300px'); - }); + expect(canvas.width).equal(100); + expect(canvas.height).equal(300); + expect(canvas.style.width).equal('100px'); + expect(canvas.style.height).equal('300px'); + }); - it('should leave canvas the same size', () => { - const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; + it('should leave canvas the same size', () => { + const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; - resizeCanvasWithRatio(canvas as any, 100, 200); + resizeCanvasWithRatio(canvas as any, 100, 200); - expect(canvas.width).equal(100); - expect(canvas.height).equal(200); - expect(canvas.style.width).equal('100px'); - expect(canvas.style.height).equal('200px'); - }); + expect(canvas.width).equal(100); + expect(canvas.height).equal(200); + expect(canvas.style.width).equal('100px'); + expect(canvas.style.height).equal('200px'); + }); - it('should not update the style if passed false', () => { - const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; + it('should not update the style if passed false', () => { + const canvas = { width: 100, height: 200, style: { width: '', height: '' } } as any; - resizeCanvasWithRatio(canvas as any, 50, 300, false); + resizeCanvasWithRatio(canvas as any, 50, 300, false); - expect(canvas.style.width).equal(''); - expect(canvas.style.height).equal(''); - }); - }); + expect(canvas.style.width).equal(''); + expect(canvas.style.height).equal(''); + }); + }); }); diff --git a/src/ts/tests/client/clientActions.spec.ts b/src/ts/tests/client/clientActions.spec.ts index 6fb9225..bb2f3db 100644 --- a/src/ts/tests/client/clientActions.spec.ts +++ b/src/ts/tests/client/clientActions.spec.ts @@ -5,7 +5,7 @@ import { Subject } from 'rxjs'; import { expect } from 'chai'; import { stub, assert, SinonStub } from 'sinon'; import { - MessageType, Action, PartyFlags, NotificationFlags, LeaveReason, TileType, Weather, InfoFlags, Pony + MessageType, Action, PartyFlags, NotificationFlags, LeaveReason, TileType, Weather, InfoFlags, Pony } from '../../common/interfaces'; import * as handlers from '../../client/handlers'; import { pony } from '../../common/entities'; @@ -25,364 +25,364 @@ import { createServerRegion } from '../../server/serverRegion'; import { ServerRegion } from '../../server/serverInterfaces'; describe('ClientActions', () => { - let zone: NgZone; - let model = stubClass(Model); - let gameService = stubClass(GameService); - let server = stubClass(ServerActions); - let game: PonyTownGame; - let clientActions: ClientActions; - let onMessage: MockSubject; - let onPonyAddOrUpdate: MockSubject; - - beforeEach(() => { - zone = { run: (f: any) => f() } as any; - resetStubMethods(gameService, 'left', 'joined', 'disconnected'); - resetStubMethods(server, 'action', 'getPonies', 'fixedPosition'); - resetStubMethods(model); - onMessage = mockSubject(); - onPonyAddOrUpdate = mockSubject(); - game = mock(PonyTownGame); - game.fallbackPonies = new Map(); - game.map = createWorldMap({ type: 0, flags: 0, regionsX: 2, regionsY: 2, defaultTile: TileType.None }); - setRegion(game.map, 0, 0, createRegion(0, 0)); - game.camera = createCamera(); - game.notifications = []; - game.send = f => f(server); - game.apply = f => f(); - game.onMessage = onMessage as any; - game.onPonyAddOrUpdate = onPonyAddOrUpdate as any; - game.onPartyUpdate = new Subject(); - game.paletteManager = new PaletteManager(); - game.onActionsUpdate = new Subject(); - game.webgl = { palettes: commonPalettes } as any; - game.settings = { - account: {}, - } as any; - model.ponies = []; - clientActions = new ClientActions(gameService as any, game, model as any, zone as any); - }); - - it('can be created with defaults', () => { - clientActions = new ClientActions(gameService as any, game, model as any, zone as any); - }); - - describe('connected()', () => { - it('resets game player', () => { - game.player = {} as any; - - clientActions.connected(); - - expect(game.player).undefined; - }); - - it('resets game map', () => { - game.map = {} as any; - - clientActions.connected(); - - expect(game.map).not.undefined; - }); - - it('notifies game service', () => { - clientActions.connected(); - - assert.calledOnce(gameService.joined); - }); - - it('notifies game', () => { - const joined = stub(game, 'joined'); - - clientActions.connected(); - - assert.calledOnce(joined); - }); - - it('sends info', () => { - clientActions.connected(); - - assert.calledWith( - server.actionParam2, Action.Info, InfoFlags.SupportsWASM | InfoFlags.SupportsLetAndConst); - }); - }); - - describe('disconnected()', () => { - it('notifies game service', () => { - clientActions.disconnected(); - - assert.calledOnce(gameService.disconnected); - }); - }); - - describe('worldState()', () => { - it('sets game state', () => { - const state = {} as any; - const setWorldState = stub(game, 'setWorldState'); - - clientActions.worldState(state, true); - - assert.calledWith(setWorldState, state, true); - }); - }); - - describe('mapState()', () => { - it('initializes game map', () => { - game.map = undefined as any; - - clientActions.mapState( - { type: 0, flags: 0, regionsX: 1, regionsY: 2, defaultTile: TileType.Water }, - { weather: Weather.None }); - - expect(game.map).not.undefined; - expect(game.map.regionsX).equal(1); - expect(game.map.regionsY).equal(2); - expect(game.map.defaultTile).equal(TileType.Water); - }); - }); - - describe('myEntity()', () => { - it('sets player fields', () => { - clientActions.myEntity(123, 'name', 'info', 'charid', 456); - - expect(game.playerId).equal(123); - expect(game.playerName).equal('name'); - expect(game.playerInfo).equal('info'); - expect(game.playerCRC).equal(456); - }); + let zone: NgZone; + let model = stubClass(Model); + let gameService = stubClass(GameService); + let server = stubClass(ServerActions); + let game: PonyTownGame; + let clientActions: ClientActions; + let onMessage: MockSubject; + let onPonyAddOrUpdate: MockSubject; + + beforeEach(() => { + zone = { run: (f: any) => f() } as any; + resetStubMethods(gameService, 'left', 'joined', 'disconnected'); + resetStubMethods(server, 'action', 'getPonies', 'fixedPosition'); + resetStubMethods(model); + onMessage = mockSubject(); + onPonyAddOrUpdate = mockSubject(); + game = mock(PonyTownGame); + game.fallbackPonies = new Map(); + game.map = createWorldMap({ type: 0, flags: 0, regionsX: 2, regionsY: 2, defaultTile: TileType.None }); + setRegion(game.map, 0, 0, createRegion(0, 0)); + game.camera = createCamera(); + game.notifications = []; + game.send = f => f(server); + game.apply = f => f(); + game.onMessage = onMessage as any; + game.onPonyAddOrUpdate = onPonyAddOrUpdate as any; + game.onPartyUpdate = new Subject(); + game.paletteManager = new PaletteManager(); + game.onActionsUpdate = new Subject(); + game.webgl = { palettes: commonPalettes } as any; + game.settings = { + account: {}, + } as any; + model.ponies = []; + clientActions = new ClientActions(gameService as any, game, model as any, zone as any); + }); + + it('can be created with defaults', () => { + clientActions = new ClientActions(gameService as any, game, model as any, zone as any); + }); + + describe('connected()', () => { + it('resets game player', () => { + game.player = {} as any; + + clientActions.connected(); + + expect(game.player).undefined; + }); + + it('resets game map', () => { + game.map = {} as any; + + clientActions.connected(); + + expect(game.map).not.undefined; + }); + + it('notifies game service', () => { + clientActions.connected(); + + assert.calledOnce(gameService.joined); + }); + + it('notifies game', () => { + const joined = stub(game, 'joined'); + + clientActions.connected(); + + assert.calledOnce(joined); + }); + + it('sends info', () => { + clientActions.connected(); + + assert.calledWith( + server.actionParam2, Action.Info, InfoFlags.SupportsWASM | InfoFlags.SupportsLetAndConst); + }); + }); + + describe('disconnected()', () => { + it('notifies game service', () => { + clientActions.disconnected(); + + assert.calledOnce(gameService.disconnected); + }); + }); + + describe('worldState()', () => { + it('sets game state', () => { + const state = {} as any; + const setWorldState = stub(game, 'setWorldState'); + + clientActions.worldState(state, true); + + assert.calledWith(setWorldState, state, true); + }); + }); + + describe('mapState()', () => { + it('initializes game map', () => { + game.map = undefined as any; + + clientActions.mapState( + { type: 0, flags: 0, regionsX: 1, regionsY: 2, defaultTile: TileType.Water }, + { weather: Weather.None }); + + expect(game.map).not.undefined; + expect(game.map.regionsX).equal(1); + expect(game.map.regionsY).equal(2); + expect(game.map.defaultTile).equal(TileType.Water); + }); + }); + + describe('myEntity()', () => { + it('sets player fields', () => { + clientActions.myEntity(123, 'name', 'info', 'charid', 456); + + expect(game.playerId).equal(123); + expect(game.playerName).equal('name'); + expect(game.playerInfo).equal('info'); + expect(game.playerCRC).equal(456); + }); - it('updates self flag for party members', () => { - game.party = { - leaderId: 0, - members: [ - { id: 321, leader: false, offline: false, pending: false, pony: {} as any, self: true }, - { id: 123, leader: false, offline: false, pending: false, pony: {} as any, self: false }, - ], - }; + it('updates self flag for party members', () => { + game.party = { + leaderId: 0, + members: [ + { id: 321, leader: false, offline: false, pending: false, pony: {} as any, self: true }, + { id: 123, leader: false, offline: false, pending: false, pony: {} as any, self: false }, + ], + }; - clientActions.myEntity(123, '', '', '', 0); + clientActions.myEntity(123, '', '', '', 0); - expect(game.party.members[0].self).false; - expect(game.party.members[1].self).true; - }); - }); + expect(game.party.members[0].self).false; + expect(game.party.members[1].self).true; + }); + }); - describe('updateRegions()', () => { - let handleUpdateEntity: SinonStub; - let handleSays: SinonStub; - let region: ServerRegion; + describe('updateRegions()', () => { + let handleUpdateEntity: SinonStub; + let handleSays: SinonStub; + let region: ServerRegion; - beforeEach(() => { - region = createServerRegion(1, 2); - handleSays = stub(handlers, 'handleSays'); - handleUpdateEntity = stub(handlers, 'handleUpdateEntity'); - }); + beforeEach(() => { + region = createServerRegion(1, 2); + handleSays = stub(handlers, 'handleSays'); + handleUpdateEntity = stub(handlers, 'handleUpdateEntity'); + }); - afterEach(() => { - handleSays.restore(); - handleUpdateEntity.restore(); - }); + afterEach(() => { + handleSays.restore(); + handleUpdateEntity.restore(); + }); - it('does nothing for empty update list', () => { - const emptyUpdate = encodeUpdateSimple(region); + it('does nothing for empty update list', () => { + const emptyUpdate = encodeUpdateSimple(region); - clientActions.update([], [], null, [emptyUpdate], []); - }); + clientActions.update([], [], null, [emptyUpdate], []); + }); - it('updates map tiles', () => { - region.x = region.y = 0; - region.tileUpdates.push({ x: 1, y: 2, type: 3 }, { x: 3, y: 2, type: 1 }); - const data = encodeUpdateSimple(region); + it('updates map tiles', () => { + region.x = region.y = 0; + region.tileUpdates.push({ x: 1, y: 2, type: 3 }, { x: 3, y: 2, type: 1 }); + const data = encodeUpdateSimple(region); - clientActions.update([], [], null, [data], []); + clientActions.update([], [], null, [data], []); - expect(getTile(game.map, 1, 2)).equal(3); - expect(getTile(game.map, 3, 2)).equal(1); - }); + expect(getTile(game.map, 1, 2)).equal(3); + expect(getTile(game.map, 3, 2)).equal(1); + }); - it('calls handleSays for each entry', () => { - clientActions.update([], [], null, [], [[1, 'foo', MessageType.Chat], [2, 'var', MessageType.Party]]); + it('calls handleSays for each entry', () => { + clientActions.update([], [], null, [], [[1, 'foo', MessageType.Chat], [2, 'var', MessageType.Party]]); - assert.calledTwice(handleSays); - assert.calledWith(handleSays, game, 1, 'foo', MessageType.Chat); - assert.calledWith(handleSays, game, 2, 'var', MessageType.Party); - }); - }); + assert.calledTwice(handleSays); + assert.calledWith(handleSays, game, 1, 'foo', MessageType.Chat); + assert.calledWith(handleSays, game, 2, 'var', MessageType.Party); + }); + }); - describe('fixPosition()', () => { - it('updates player position', () => { - game.player = {} as any; + describe('fixPosition()', () => { + it('updates player position', () => { + game.player = {} as any; - clientActions.fixPosition(123, 456, true); + clientActions.fixPosition(123, 456, true); - expect(game.player!.x).equal(123); - expect(game.player!.y).equal(456); - }); + expect(game.player!.x).equal(123); + expect(game.player!.y).equal(456); + }); - it('sends fixed position message back to server', () => { - clientActions.fixPosition(123, 456, true); + it('sends fixed position message back to server', () => { + clientActions.fixPosition(123, 456, true); - assert.calledOnce(server.fixedPosition); - }); + assert.calledOnce(server.fixedPosition); + }); - it('does nothing if no player', () => { - game.player = undefined; + it('does nothing if no player', () => { + game.player = undefined; - clientActions.fixPosition(123, 456, true); - }); - }); + clientActions.fixPosition(123, 456, true); + }); + }); - describe('left()', () => { - it('resets game player', () => { - game.player = {} as any; + describe('left()', () => { + it('resets game player', () => { + game.player = {} as any; - clientActions.left(LeaveReason.None); + clientActions.left(LeaveReason.None); - expect(game.player).undefined; - }); + expect(game.player).undefined; + }); - it('resets game map', () => { - game.map = {} as any; + it('resets game map', () => { + game.map = {} as any; - clientActions.left(LeaveReason.None); + clientActions.left(LeaveReason.None); - expect(game.map).not.undefined; - }); + expect(game.map).not.undefined; + }); - it('notifies game service', () => { - clientActions.left(LeaveReason.Swearing); + it('notifies game service', () => { + clientActions.left(LeaveReason.Swearing); - assert.calledWith(gameService.left, 'clientActions.left', LeaveReason.Swearing); - }); - }); + assert.calledWith(gameService.left, 'clientActions.left', LeaveReason.Swearing); + }); + }); - describe('addNotification()', () => { - it('adds notification to game', () => { - const e = entity(456, 0, 0, pony.type, { ponyState: {} } as any); - addEntity(game.map, e); + describe('addNotification()', () => { + it('adds notification to game', () => { + const e = entity(456, 0, 0, pony.type, { ponyState: {} } as any); + addEntity(game.map, e); - clientActions.addNotification(123, 456, 'name', 'test', 'note', NotificationFlags.Ok); + clientActions.addNotification(123, 456, 'name', 'test', 'note', NotificationFlags.Ok); - expect(game.notifications).eql([ - { id: 123, message: 'test', note: 'note', flags: NotificationFlags.Ok, open: false, fresh: true, pony: e } - ]); - }); + expect(game.notifications).eql([ + { id: 123, message: 'test', note: 'note', flags: NotificationFlags.Ok, open: false, fresh: true, pony: e } + ]); + }); - it('sets pony to offline pony if no pony is provided', () => { - const pony = game.offlinePony = { offlinePony: true } as any; + it('sets pony to offline pony if no pony is provided', () => { + const pony = game.offlinePony = { offlinePony: true } as any; - clientActions.addNotification(123, 0, 'name', 'test', 'note', NotificationFlags.Ok); + clientActions.addNotification(123, 0, 'name', 'test', 'note', NotificationFlags.Ok); - expect(game.notifications).eql([ - { id: 123, message: 'test', note: 'note', flags: NotificationFlags.Ok, open: false, fresh: true, pony } - ]); - }); + expect(game.notifications).eql([ + { id: 123, message: 'test', note: 'note', flags: NotificationFlags.Ok, open: false, fresh: true, pony } + ]); + }); - it('sets pony to supporter pony if supporter pony flag is set', () => { - const pony = game.supporterPony = { supporterPony: true } as any; + it('sets pony to supporter pony if supporter pony flag is set', () => { + const pony = game.supporterPony = { supporterPony: true } as any; - clientActions.addNotification(123, 0, 'name', 'test', 'note', NotificationFlags.Supporter); + clientActions.addNotification(123, 0, 'name', 'test', 'note', NotificationFlags.Supporter); - expect(game.notifications).eql([ - { id: 123, message: 'test', note: 'note', flags: NotificationFlags.Supporter, open: false, fresh: true, pony } - ]); - }); - }); + expect(game.notifications).eql([ + { id: 123, message: 'test', note: 'note', flags: NotificationFlags.Supporter, open: false, fresh: true, pony } + ]); + }); + }); - describe('removeNotification()', () => { - it('removes notification with given id', () => { - game.notifications.push({ id: 123 } as any); + describe('removeNotification()', () => { + it('removes notification with given id', () => { + game.notifications.push({ id: 123 } as any); - clientActions.removeNotification(123); + clientActions.removeNotification(123); - expect(game.notifications).eql([]); - }); + expect(game.notifications).eql([]); + }); - it('removes notification in digest cycle', () => { - const run = stub(zone, 'run'); + it('removes notification in digest cycle', () => { + const run = stub(zone, 'run'); - clientActions.removeNotification(123); + clientActions.removeNotification(123); - assert.calledOnce(run); - }); - }); + assert.calledOnce(run); + }); + }); - describe('updateSelection()', () => { - it('selects new entity', () => { - const newPony = entity(456, 0, 0, pony.type, { ponyState: {} } as any); - game.selected = entity(123) as Pony; - addEntity(game.map, newPony); - const select = stub(game, 'select'); + describe('updateSelection()', () => { + it('selects new entity', () => { + const newPony = entity(456, 0, 0, pony.type, { ponyState: {} } as any); + game.selected = entity(123) as Pony; + addEntity(game.map, newPony); + const select = stub(game, 'select'); - clientActions.updateSelection(123, 456); + clientActions.updateSelection(123, 456); - assert.calledWith(select, newPony as any); - }); + assert.calledWith(select, newPony as any); + }); - it('does nothing if selected ID is not current ID', () => { - const select = stub(game, 'select'); + it('does nothing if selected ID is not current ID', () => { + const select = stub(game, 'select'); - clientActions.updateSelection(123, 456); + clientActions.updateSelection(123, 456); - assert.notCalled(select); - }); - }); + assert.notCalled(select); + }); + }); - describe('updateParty()', () => { - it('clears party if passed undefined', () => { - game.party = {} as any; + describe('updateParty()', () => { + it('clears party if passed undefined', () => { + game.party = {} as any; - clientActions.updateParty(undefined); + clientActions.updateParty(undefined); - expect(game.party).undefined; - }); + expect(game.party).undefined; + }); - it('clears party if passed empty list', () => { - game.party = {} as any; + it('clears party if passed empty list', () => { + game.party = {} as any; - clientActions.updateParty([]); + clientActions.updateParty([]); - expect(game.party).undefined; - }); + expect(game.party).undefined; + }); - it('updates party', () => { - clientActions.updateParty([ - [123, PartyFlags.Leader], - ]); + it('updates party', () => { + clientActions.updateParty([ + [123, PartyFlags.Leader], + ]); - expect(game.party).eql({ - leaderId: 123, - members: [ - { id: 123, leader: true, offline: false, pending: false, pony: undefined, self: false }, - ], - }); - }); + expect(game.party).eql({ + leaderId: 123, + members: [ + { id: 123, leader: true, offline: false, pending: false, pony: undefined, self: false }, + ], + }); + }); - it('gets missing pony info from server', () => { - clientActions.updateParty([ - [123, PartyFlags.Leader], - ]); + it('gets missing pony info from server', () => { + clientActions.updateParty([ + [123, PartyFlags.Leader], + ]); - assert.calledWithMatch(server.getPonies, [123]); - }); - }); + assert.calledWithMatch(server.getPonies, [123]); + }); + }); - describe('ponies()', () => { - it('does nothing for empty list', () => { - clientActions.updatePonies([]); - }); + describe('ponies()', () => { + it('does nothing for empty list', () => { + clientActions.updatePonies([]); + }); - it('updates party pony', () => { - game.party = { - leaderId: 0, - members: [ - { id: 123, pony: undefined, leader: true, pending: false, offline: false, self: false }, - ], - }; + it('updates party pony', () => { + game.party = { + leaderId: 0, + members: [ + { id: 123, pony: undefined, leader: true, pending: false, offline: false, self: false }, + ], + }; - clientActions.updatePonies([ - [123, {}, encodeString('foo')!, new Uint8Array([1, 2, 3]), 0, false], - ]); + clientActions.updatePonies([ + [123, {}, encodeString('foo')!, new Uint8Array([1, 2, 3]), 0, false], + ]); - expect(game.party.members[0].pony).not.undefined; - }); - }); + expect(game.party.members[0].pony).not.undefined; + }); + }); }); diff --git a/src/ts/tests/client/clientUtils.spec.ts b/src/ts/tests/client/clientUtils.spec.ts index bf35ec2..53ace23 100644 --- a/src/ts/tests/client/clientUtils.spec.ts +++ b/src/ts/tests/client/clientUtils.spec.ts @@ -1,232 +1,232 @@ import '../lib'; import { expect } from 'chai'; import { - cleanName, cleanMessage, filterString, isSpamMessage, toSocialSiteInfo + cleanName, cleanMessage, filterString, isSpamMessage, toSocialSiteInfo } from '../../client/clientUtils'; import { repeat, removeItem } from '../../common/utils'; import { SAY_MAX_LENGTH } from '../../common/constants'; import { oauthProviders } from '../../client/data'; const cleanNameTests: [string | undefined, string, string][] = [ - [undefined, '', 'undefined'], - ['', '', 'empty string'], - ['rainbow dash', 'rainbow dash', 'valid name'], - ['foo—bar', 'foo—bar', 'dash'], - [' pony ', 'pony', 'trimming'], - ['a pony', 'a pony', 'multiple spaces'], - ['a_po-ny(yay)[foo].,/|&#@!?aaa', 'a_po-ny(yay)[foo].,/|&#@!?aaa', 'allowed symbols'], - ['a\t\r\npony1', 'apony1', 'other whitespace symbols'], - ['a\u0000\u0008\u009f\u007fpony2', 'apony2', 'control'], - ['a\u0300\u0359\u036a\u05c2\ua94fpony3', 'apony3', 'mark, nonspacing'], - ['a\ufe00\ufe0fpony4', 'apony4', 'variation'], - ['a▇▗pony5', 'apony5', 'blocks'], - ['a⠟⠳⠀pony6', 'apony6', 'braile'], - ['aᶌᶗᶭpony7', 'apony7', 'phonetic extensions'], - ['aʰʷ〮pony8', 'apony8', 'modifiers'], - ['aⅨⅩⅪpony9', 'apony9', 'roman numerals'], - ['aᏅᏆᏇpony10', 'apony10', 'cherokee'], - ['\ud800apony11', 'apony11', 'invalid unicode'], - ['😺🦇🤡⏰', '😺🦇🤡⏰', 'emoji'], - ['aponߦy߃߄߅13', 'apony13', 'NKo'], - ['ap҉ony꙰14', 'apony14', 'Mark, Enclosing'], - ['ap󠀗ony󠀩15', 'apony15', 'Tags'], - ['apA$zony16', 'apA$zony16', 'Romaji'], - ['apony🖕17', 'apony17', 'filtered emoji'], - ['[△▽△]❥Pony™✔18', '[△▽△]❥Pony™✔18', 'triangles and symbols'], - ['ニキフォーオブ', 'ニキフォーオブ', 'allow katakana'], - ['ﷺ ﷻ﷽long', 'long', 'Weird long symbols'], - ['꧁Adam', 'Adam', 'Weird symbols'], - ['⎝Perro', 'Perro', 'weird long symbol'], - ['aaa\u1160bbb', 'aaa bbb', 'Converts hangul space to regular space'], - ['aaa\u3000bbb', 'aaa bbb', 'Converts ideographic space to regular space'], - ['aaa\u3164bbb', 'aaa bbb', 'Converts hangul filler to regular space'], - ['sắp sáng rồi', 'sắp sáng rồi', 'Vietnamese'], - ['x\u00ady', 'xy', 'Remove soft hyphen'], - ['a\u2800b', 'ab', 'Remove braille pattern blank'], + [undefined, '', 'undefined'], + ['', '', 'empty string'], + ['rainbow dash', 'rainbow dash', 'valid name'], + ['foo—bar', 'foo—bar', 'dash'], + [' pony ', 'pony', 'trimming'], + ['a pony', 'a pony', 'multiple spaces'], + ['a_po-ny(yay)[foo].,/|&#@!?aaa', 'a_po-ny(yay)[foo].,/|&#@!?aaa', 'allowed symbols'], + ['a\t\r\npony1', 'apony1', 'other whitespace symbols'], + ['a\u0000\u0008\u009f\u007fpony2', 'apony2', 'control'], + ['a\u0300\u0359\u036a\u05c2\ua94fpony3', 'apony3', 'mark, nonspacing'], + ['a\ufe00\ufe0fpony4', 'apony4', 'variation'], + ['a▇▗pony5', 'apony5', 'blocks'], + ['a⠟⠳⠀pony6', 'apony6', 'braile'], + ['aᶌᶗᶭpony7', 'apony7', 'phonetic extensions'], + ['aʰʷ〮pony8', 'apony8', 'modifiers'], + ['aⅨⅩⅪpony9', 'apony9', 'roman numerals'], + ['aᏅᏆᏇpony10', 'apony10', 'cherokee'], + ['\ud800apony11', 'apony11', 'invalid unicode'], + ['😺🦇🤡⏰', '😺🦇🤡⏰', 'emoji'], + ['aponߦy߃߄߅13', 'apony13', 'NKo'], + ['ap҉ony꙰14', 'apony14', 'Mark, Enclosing'], + ['ap󠀗ony󠀩15', 'apony15', 'Tags'], + ['apA$zony16', 'apA$zony16', 'Romaji'], + ['apony🖕17', 'apony17', 'filtered emoji'], + ['[△▽△]❥Pony™✔18', '[△▽△]❥Pony™✔18', 'triangles and symbols'], + ['ニキフォーオブ', 'ニキフォーオブ', 'allow katakana'], + ['ﷺ ﷻ﷽long', 'long', 'Weird long symbols'], + ['꧁Adam', 'Adam', 'Weird symbols'], + ['⎝Perro', 'Perro', 'weird long symbol'], + ['aaa\u1160bbb', 'aaa bbb', 'Converts hangul space to regular space'], + ['aaa\u3000bbb', 'aaa bbb', 'Converts ideographic space to regular space'], + ['aaa\u3164bbb', 'aaa bbb', 'Converts hangul filler to regular space'], + ['sắp sáng rồi', 'sắp sáng rồi', 'Vietnamese'], + ['x\u00ady', 'xy', 'Remove soft hyphen'], + ['a\u2800b', 'ab', 'Remove braille pattern blank'], ]; const cleanMessageTests: [string | undefined, string][] = [ - [undefined, ''], - ['', ''], - ['hello', 'hello'], - ['😺🦇🤡⏰', '😺🦇🤡⏰'], // emoji - ['🍪🥚', '🍪🥚'], // egg - ['a_po-ny(yay)[foo].,/|&#@!?aaa', 'a_po-ny(yay)[foo].,/|&#@!?aaa'], // allowed symbols - ['E̸̢͕̬̹̠̘̬̲̠͖͓͂̾ͧ̈́ͮͮ̈́̄͛̉ͪͤ͒͊̏̅́͘͘R̸̴̅̌͋ͯͦ̔͊̎͊͑҉̶̪͕̳̙̦̤̞̹̀R̃͛̂ͣ͊ͤ̔', 'ERR'], - ['ap󠀗ony󠀩15', 'apony15'], // Tags - ['a\u0000\u0008\u009f\u007fpony2', 'apony2'], // control - ['apA$zony16', 'apA$zony16'], // Romaji - ['spe⦅⦆。「」、・ ̄ˊcial', 'spe⦅⦆。「」、・ ̄ˊcial'], // Special - ['ニキフォーオブ', 'ニキフォーオブ'], // allow katakana - ['、。〃々〈〉「」『』〒〓〜〝〞〟〡〢〣〤〦〧〨〩〰〱〲〳〴〵', - '、。〃々〈〉「」『』〒〓〜〝〞〟〡〢〣〤〦〧〨〩〰〱〲〳〴〵'], // allow special japanese/chinese characters - ['ﷺ ﷻ﷽long', 'long'], // Weird long symbols - ['aaa\u1160bbb', 'aaa bbb'], // Converts hangul space to regular space - ['aaa\u3000bbb', 'aaa bbb'], // Converts ideographic space to regular space - ['aaa\u3164bbb', 'aaa bbb'], // Converts hangul filler to regular space - ['aaa‐‑‒–—bbb‰‱′″‴', 'aaa‐‑‒–—bbb‰‱′″‴'], // General punctuation - ['ققفقلسخهقسل', 'ققفقلسخهقسل'], // Arabic - ['₠₡₢₣₤₥', '₠₡₢₣₤₥'], // Currency symbols - ['Hi! 근데 왜-호ㅔ', 'Hi! 근데 왜-호ㅔ'], // Hangul - ['sắp sáng rồi', 'sắp sáng rồi'], // Vietnamese - ['דברים נראים כחולים', 'דברים נראים כחולים'], // Hebrew - ['℀℁ℂ℃℄℅℆ℇ℈℉', '℀℁ℂ℃℄℅℆ℇ℈℉'], // Letterlike Symbols - ['🇦🇧🇿', '🇦🇧🇿'], // regional indicators - ['誒ㄟㄝㄍ', '誒ㄟㄝㄍ'], // Bopomofo - ['⌂⌃⌄⌅⌆⌇', '⌂⌃⌄⌅⌆⌇'], // Technical - ['⅐⅑⅒⅓ⅢⅣⅤ', '⅐⅑⅒⅓ⅢⅣⅤ'], // Number Forms - ['︰︱︲︳︴︵︶', '︰︱︲︳︴︵︶'], // CJK Compatibility Forms - ['ஐஜ', 'ஐஜ'], // Tamil - ['߶߷߸߹', '߶߷߸߹'], // NKo - ['ತಥದಠ', 'ತಥದಠ'], // Kannada - ['ԳԴԵԶԷ', 'ԳԴԵԶԷ'], // Armenian - ['Τι συμβαίνει', 'Τι συμβαίνει'], // Greek - ['ႠႡႢႣႤႥ', 'ႠႡႢႣႤႥ'], // Georgian - ['╣╤╥', '╣╤╥'], // Box Drawing - ['🃉🃊🃋🃌🃍🃎🃏', '🃉🃊🃋🃌🃍🃎🃏'], // Playing Cards - ['🀀🀁🀂🀃🀄', '🀀🀁🀂🀃🀄'], // Mahjong Tiles - ['⡳⡣⡤⡥', '⡳⡣⡤⡥'], // Braille Patterns - ['ऒओऔकख', 'ऒओऔकख'], // Devanagari - ['ᐁᐂᐃᐄᐅᐆ', 'ᐁᐂᐃᐄᐅᐆ'], // Unified Canadian Aboriginal Syllabics - ['X\u200eX', 'XX'], // Remove LEFT-TO-RIGHT MARK - ['x\u00ady', 'xy'], // Remove soft hyphen + [undefined, ''], + ['', ''], + ['hello', 'hello'], + ['😺🦇🤡⏰', '😺🦇🤡⏰'], // emoji + ['🍪🥚', '🍪🥚'], // egg + ['a_po-ny(yay)[foo].,/|&#@!?aaa', 'a_po-ny(yay)[foo].,/|&#@!?aaa'], // allowed symbols + ['E̸̢͕̬̹̠̘̬̲̠͖͓͂̾ͧ̈́ͮͮ̈́̄͛̉ͪͤ͒͊̏̅́͘͘R̸̴̅̌͋ͯͦ̔͊̎͊͑҉̶̪͕̳̙̦̤̞̹̀R̃͛̂ͣ͊ͤ̔', 'ERR'], + ['ap󠀗ony󠀩15', 'apony15'], // Tags + ['a\u0000\u0008\u009f\u007fpony2', 'apony2'], // control + ['apA$zony16', 'apA$zony16'], // Romaji + ['spe⦅⦆。「」、・ ̄ˊcial', 'spe⦅⦆。「」、・ ̄ˊcial'], // Special + ['ニキフォーオブ', 'ニキフォーオブ'], // allow katakana + ['、。〃々〈〉「」『』〒〓〜〝〞〟〡〢〣〤〦〧〨〩〰〱〲〳〴〵', + '、。〃々〈〉「」『』〒〓〜〝〞〟〡〢〣〤〦〧〨〩〰〱〲〳〴〵'], // allow special japanese/chinese characters + ['ﷺ ﷻ﷽long', 'long'], // Weird long symbols + ['aaa\u1160bbb', 'aaa bbb'], // Converts hangul space to regular space + ['aaa\u3000bbb', 'aaa bbb'], // Converts ideographic space to regular space + ['aaa\u3164bbb', 'aaa bbb'], // Converts hangul filler to regular space + ['aaa‐‑‒–—bbb‰‱′″‴', 'aaa‐‑‒–—bbb‰‱′″‴'], // General punctuation + ['ققفقلسخهقسل', 'ققفقلسخهقسل'], // Arabic + ['₠₡₢₣₤₥', '₠₡₢₣₤₥'], // Currency symbols + ['Hi! 근데 왜-호ㅔ', 'Hi! 근데 왜-호ㅔ'], // Hangul + ['sắp sáng rồi', 'sắp sáng rồi'], // Vietnamese + ['דברים נראים כחולים', 'דברים נראים כחולים'], // Hebrew + ['℀℁ℂ℃℄℅℆ℇ℈℉', '℀℁ℂ℃℄℅℆ℇ℈℉'], // Letterlike Symbols + ['🇦🇧🇿', '🇦🇧🇿'], // regional indicators + ['誒ㄟㄝㄍ', '誒ㄟㄝㄍ'], // Bopomofo + ['⌂⌃⌄⌅⌆⌇', '⌂⌃⌄⌅⌆⌇'], // Technical + ['⅐⅑⅒⅓ⅢⅣⅤ', '⅐⅑⅒⅓ⅢⅣⅤ'], // Number Forms + ['︰︱︲︳︴︵︶', '︰︱︲︳︴︵︶'], // CJK Compatibility Forms + ['ஐஜ', 'ஐஜ'], // Tamil + ['߶߷߸߹', '߶߷߸߹'], // NKo + ['ತಥದಠ', 'ತಥದಠ'], // Kannada + ['ԳԴԵԶԷ', 'ԳԴԵԶԷ'], // Armenian + ['Τι συμβαίνει', 'Τι συμβαίνει'], // Greek + ['ႠႡႢႣႤႥ', 'ႠႡႢႣႤႥ'], // Georgian + ['╣╤╥', '╣╤╥'], // Box Drawing + ['🃉🃊🃋🃌🃍🃎🃏', '🃉🃊🃋🃌🃍🃎🃏'], // Playing Cards + ['🀀🀁🀂🀃🀄', '🀀🀁🀂🀃🀄'], // Mahjong Tiles + ['⡳⡣⡤⡥', '⡳⡣⡤⡥'], // Braille Patterns + ['ऒओऔकख', 'ऒओऔकख'], // Devanagari + ['ᐁᐂᐃᐄᐅᐆ', 'ᐁᐂᐃᐄᐅᐆ'], // Unified Canadian Aboriginal Syllabics + ['X\u200eX', 'XX'], // Remove LEFT-TO-RIGHT MARK + ['x\u00ady', 'xy'], // Remove soft hyphen ]; describe('clientUtils', () => { - describe('cleanName()', () => { - cleanNameTests.forEach(([value, expected, test]) => it(`cleans '${value}' to '${expected}' (${test})`, () => { - expect(cleanName(value)).equal(expected); - })); - }); + describe('cleanName()', () => { + cleanNameTests.forEach(([value, expected, test]) => it(`cleans '${value}' to '${expected}' (${test})`, () => { + expect(cleanName(value)).equal(expected); + })); + }); - describe('cleanMessage()', () => { - cleanMessageTests.forEach(([value, expected], i) => it(`cleans '${value}' to '${expected}' (${i})`, () => { - expect(cleanMessage(value)).equal(expected); - })); - }); + describe('cleanMessage()', () => { + cleanMessageTests.forEach(([value, expected], i) => it(`cleans '${value}' to '${expected}' (${i})`, () => { + expect(cleanMessage(value)).equal(expected); + })); + }); - describe('toSocialSiteInfo()', () => { - const provider = { id: 'prov', name: 'prov', color: '#123456' }; + describe('toSocialSiteInfo()', () => { + const provider = { id: 'prov', name: 'prov', color: '#123456' }; - beforeEach(() => { - oauthProviders.push(provider); - }); + beforeEach(() => { + oauthProviders.push(provider); + }); - afterEach(() => { - removeItem(oauthProviders, provider); - }); + afterEach(() => { + removeItem(oauthProviders, provider); + }); - it('returns social site info', () => { - oauthProviders.push(); + it('returns social site info', () => { + oauthProviders.push(); - expect(toSocialSiteInfo({ id: 'foo', name: 'Foo', url: 'www.foo.com', provider: 'prov' })).eql({ - id: 'foo', - name: 'Foo', - url: 'www.foo.com', - icon: 'prov', - color: '#123456', - }); - }); + expect(toSocialSiteInfo({ id: 'foo', name: 'Foo', url: 'www.foo.com', provider: 'prov' })).eql({ + id: 'foo', + name: 'Foo', + url: 'www.foo.com', + icon: 'prov', + color: '#123456', + }); + }); - it('return undefined icon and color for missing provider', () => { - oauthProviders.push(); + it('return undefined icon and color for missing provider', () => { + oauthProviders.push(); - expect(toSocialSiteInfo({ id: 'foo', name: 'Foo', url: 'www.foo.com', provider: 'non-prov' })).eql({ - id: 'foo', - name: 'Foo', - url: 'www.foo.com', - icon: undefined, - color: undefined, - }); - }); - }); + expect(toSocialSiteInfo({ id: 'foo', name: 'Foo', url: 'www.foo.com', provider: 'non-prov' })).eql({ + id: 'foo', + name: 'Foo', + url: 'www.foo.com', + icon: undefined, + color: undefined, + }); + }); + }); - describe('filterString()', () => { - it('returns empty string for empty string', () => { - expect(filterString('', () => false)).equal(''); - }); + describe('filterString()', () => { + it('returns empty string for empty string', () => { + expect(filterString('', () => false)).equal(''); + }); - it('returns empty string for undefined', () => { - expect(filterString(undefined, () => false)).equal(''); - }); + it('returns empty string for undefined', () => { + expect(filterString(undefined, () => false)).equal(''); + }); - it('return the same string for no filtered characters', () => { - expect(filterString('hello', () => true)).equal('hello'); - }); + it('return the same string for no filtered characters', () => { + expect(filterString('hello', () => true)).equal('hello'); + }); - it('removes filtered characters', () => { - expect(filterString('hello world', x => x !== 'o'.charCodeAt(0))).equal('hell wrld'); - }); + it('removes filtered characters', () => { + expect(filterString('hello world', x => x !== 'o'.charCodeAt(0))).equal('hell wrld'); + }); - it('removes all filtered characters', () => { - expect(filterString('hello world', () => false)).equal(''); - }); + it('removes all filtered characters', () => { + expect(filterString('hello world', () => false)).equal(''); + }); - it('removes 4 byte filtered characters', () => { - expect(filterString('hello😺', x => x !== '😺'.codePointAt(0))).equal('hello'); - }); + it('removes 4 byte filtered characters', () => { + expect(filterString('hello😺', x => x !== '😺'.codePointAt(0))).equal('hello'); + }); - it('removes invalid utf-16 characters', () => { - expect(filterString('hello\udb40world', () => true)).equal('helloworld'); - }); - }); + it('removes invalid utf-16 characters', () => { + expect(filterString('hello\udb40world', () => true)).equal('helloworld'); + }); + }); - describe('isSpamMessage()', () => { - it('returns false for no last messages', () => { - expect(isSpamMessage('hello', [])).false; - }); + describe('isSpamMessage()', () => { + it('returns false for no last messages', () => { + expect(isSpamMessage('hello', [])).false; + }); - it('returns false for mismatching last messages', () => { - expect(isSpamMessage('hello', ['boop'])).false; - }); + it('returns false for mismatching last messages', () => { + expect(isSpamMessage('hello', ['boop'])).false; + }); - it('returns false for command', () => { - expect(isSpamMessage('/command', ['/command'])).false; - }); + it('returns false for command', () => { + expect(isSpamMessage('/command', ['/command'])).false; + }); - it('returns true for same last message', () => { - expect(isSpamMessage('hello', ['hello'])).true; - }); + it('returns true for same last message', () => { + expect(isSpamMessage('hello', ['hello'])).true; + }); - it('returns true for doubled message', () => { - expect(isSpamMessage('hellohello', ['hello'])).true; - }); + it('returns true for doubled message', () => { + expect(isSpamMessage('hellohello', ['hello'])).true; + }); - it('returns true for trippled message', () => { - expect(isSpamMessage('hellohellohello', ['hello'])).true; - }); + it('returns true for trippled message', () => { + expect(isSpamMessage('hellohellohello', ['hello'])).true; + }); - it('returns false for really short doubled message "a"', () => { - expect(isSpamMessage('aa', ['a'])).false; - }); + it('returns false for really short doubled message "a"', () => { + expect(isSpamMessage('aa', ['a'])).false; + }); - it('returns false for really short doubled message "ha"', () => { - expect(isSpamMessage('haha', ['ha'])).false; - }); + it('returns false for really short doubled message "ha"', () => { + expect(isSpamMessage('haha', ['ha'])).false; + }); - it('returns false for really short doubled message "lol"', () => { - expect(isSpamMessage('lollol', ['lol'])).false; - }); + it('returns false for really short doubled message "lol"', () => { + expect(isSpamMessage('lollol', ['lol'])).false; + }); - it('returns true for multiplied cut to length message message', () => { - const message = repeat(100, 'hello').join('').substr(0, SAY_MAX_LENGTH); - expect(isSpamMessage(message, ['hello'])).true; - }); + it('returns true for multiplied cut to length message message', () => { + const message = repeat(100, 'hello').join('').substr(0, SAY_MAX_LENGTH); + expect(isSpamMessage(message, ['hello'])).true; + }); - it('returns true for added one character', () => { - expect(isSpamMessage('message!', ['message'])).true; - }); + it('returns true for added one character', () => { + expect(isSpamMessage('message!', ['message'])).true; + }); - it('returns true for added two characters', () => { - expect(isSpamMessage('message!!', ['message'])).true; - }); + it('returns true for added two characters', () => { + expect(isSpamMessage('message!!', ['message'])).true; + }); - it('returns false for added one character if message is too short', () => { - expect(isSpamMessage('ha!', ['ha'])).false; - }); + it('returns false for added one character if message is too short', () => { + expect(isSpamMessage('ha!', ['ha'])).false; + }); - it('returns true for added one character (in prev message)', () => { - expect(isSpamMessage('message', ['message!'])).true; - }); - }); + it('returns true for added one character (in prev message)', () => { + expect(isSpamMessage('message', ['message!'])).true; + }); + }); }); diff --git a/src/ts/tests/client/emoji.spec.ts b/src/ts/tests/client/emoji.spec.ts index 128e859..1ae6057 100644 --- a/src/ts/tests/client/emoji.spec.ts +++ b/src/ts/tests/client/emoji.spec.ts @@ -3,112 +3,112 @@ import { expect } from 'chai'; import { splitEmojis, findEmoji, replaceEmojis, emojis, hasEmojis, autocompleteMesssage } from '../../client/emoji'; describe('emotes', () => { - describe('findEmote()', () => { - let appleEmoji: any; + describe('findEmote()', () => { + let appleEmoji: any; - before(() => { - appleEmoji = emojis.find(e => e.symbol === '🍎'); - expect(appleEmoji).not.undefined; - }); + before(() => { + appleEmoji = emojis.find(e => e.symbol === '🍎'); + expect(appleEmoji).not.undefined; + }); - it('returns found emoji by symbol', () => { - expect(findEmoji('🍎')).equal(appleEmoji); - }); + it('returns found emoji by symbol', () => { + expect(findEmoji('🍎')).equal(appleEmoji); + }); - it('returns found emoji by name', () => { - expect(findEmoji('apple')).equal(appleEmoji); - }); + it('returns found emoji by name', () => { + expect(findEmoji('apple')).equal(appleEmoji); + }); - it('returns undefined if emoji does not exists', () => { - expect(findEmoji('foobar')).undefined; - }); - }); + it('returns undefined if emoji does not exists', () => { + expect(findEmoji('foobar')).undefined; + }); + }); - describe('replaceEmotes()', () => { - it('replaces emoji names with emojis', () => { - expect(replaceEmojis(':apple:')).equal('🍎'); - }); + describe('replaceEmotes()', () => { + it('replaces emoji names with emojis', () => { + expect(replaceEmojis(':apple:')).equal('🍎'); + }); - it('works with additional text around emoji', () => { - expect(replaceEmojis('text :apple: hi')).equal('text 🍎 hi'); - }); + it('works with additional text around emoji', () => { + expect(replaceEmojis('text :apple: hi')).equal('text 🍎 hi'); + }); - it('works with multiple emojis', () => { - expect(replaceEmojis(':orange: text :apple: hi')).equal('🍊 text 🍎 hi'); - }); + it('works with multiple emojis', () => { + expect(replaceEmojis(':orange: text :apple: hi')).equal('🍊 text 🍎 hi'); + }); - it('does nothing if does not contain any emoji names', () => { - expect(replaceEmojis('plain text')).equal('plain text'); - }); + it('does nothing if does not contain any emoji names', () => { + expect(replaceEmojis('plain text')).equal('plain text'); + }); - it('does nothing if emojis are already converted', () => { - expect(replaceEmojis('text 🍎 hi')).equal('text 🍎 hi'); - }); + it('does nothing if emojis are already converted', () => { + expect(replaceEmojis('text 🍎 hi')).equal('text 🍎 hi'); + }); - it('does nothing if emoji name is not found', () => { - expect(replaceEmojis('text :foo: hi')).equal('text :foo: hi'); - }); + it('does nothing if emoji name is not found', () => { + expect(replaceEmojis('text :foo: hi')).equal('text :foo: hi'); + }); - it('returns empty string for undefined', () => { - expect(replaceEmojis(undefined)).equal(''); - }); - }); + it('returns empty string for undefined', () => { + expect(replaceEmojis(undefined)).equal(''); + }); + }); - describe('splitEmotes()', () => { - it('returns array for plain text', () => { - expect(splitEmojis('foo bar')).eql(['foo bar']); - }); + describe('splitEmotes()', () => { + it('returns array for plain text', () => { + expect(splitEmojis('foo bar')).eql(['foo bar']); + }); - it('returns array for plain text', () => { - expect(splitEmojis('foo 🍎 bar')).eql(['foo ', '🍎', ' bar']); - }); - }); + it('returns array for plain text', () => { + expect(splitEmojis('foo 🍎 bar')).eql(['foo ', '🍎', ' bar']); + }); + }); - describe('hasEmotes()', () => { - it('returns true if contains emotes', () => { - expect(hasEmojis('foo 🍎 bar')).true; - }); + describe('hasEmotes()', () => { + it('returns true if contains emotes', () => { + expect(hasEmojis('foo 🍎 bar')).true; + }); - it('returns false if does not contain emotes', () => { - expect(hasEmojis('foo bar')).false; - }); - }); + it('returns false if does not contain emotes', () => { + expect(hasEmojis('foo bar')).false; + }); + }); - describe('autocompleteMesssage()', () => { - it('does nothing for empty text', () => { - expect(autocompleteMesssage('', false, {})).equal(''); - }); + describe('autocompleteMesssage()', () => { + it('does nothing for empty text', () => { + expect(autocompleteMesssage('', false, {})).equal(''); + }); - it('does nothing for regular text', () => { - const state = {}; - expect(autocompleteMesssage('hello world', false, state)).equal('hello world'); - expect(state).eql({}); - }); + it('does nothing for regular text', () => { + const state = {}; + expect(autocompleteMesssage('hello world', false, state)).equal('hello world'); + expect(state).eql({}); + }); - it('autocompletes an emote', () => { - const state = {}; - expect(autocompleteMesssage('hello :app', false, state)).equal('hello :apple:'); - expect(state).eql({ lastEmoji: ':app' }); - }); + it('autocompletes an emote', () => { + const state = {}; + expect(autocompleteMesssage('hello :app', false, state)).equal('hello :apple:'); + expect(state).eql({ lastEmoji: ':app' }); + }); - it('autocompletes considering previous autocomplete', () => { - const state = {}; - expect(autocompleteMesssage('hello :a', false, state)).equal('hello :angry:'); - expect(autocompleteMesssage('hello :angry:', false, state)).equal('hello :apple:'); - expect(state).eql({ lastEmoji: ':a' }); - }); + it('autocompletes considering previous autocomplete', () => { + const state = {}; + expect(autocompleteMesssage('hello :a', false, state)).equal('hello :angry:'); + expect(autocompleteMesssage('hello :angry:', false, state)).equal('hello :apple:'); + expect(state).eql({ lastEmoji: ':a' }); + }); - it('autocompletes with reversed order', () => { - const state = {}; - expect(autocompleteMesssage('hello :b', false, state)).equal('hello :banana:'); - expect(autocompleteMesssage('hello :bat:', false, state)).equal('hello :black_heart:'); - expect(autocompleteMesssage('hello :black_heart:', true, state)).equal('hello :bat:'); - expect(state).eql({ lastEmoji: ':b' }); - }); + it('autocompletes with reversed order', () => { + const state = {}; + expect(autocompleteMesssage('hello :b', false, state)).equal('hello :banana:'); + expect(autocompleteMesssage('hello :bat:', false, state)).equal('hello :black_heart:'); + expect(autocompleteMesssage('hello :black_heart:', true, state)).equal('hello :bat:'); + expect(state).eql({ lastEmoji: ':b' }); + }); - it('does nothing if does not match any emoji', () => { - const state = { lastEmoji: 'xyz' }; - expect(autocompleteMesssage('hello :abc', false, state)).equal('hello :abc'); - }); - }); + it('does nothing if does not match any emoji', () => { + const state = { lastEmoji: 'xyz' }; + expect(autocompleteMesssage('hello :abc', false, state)).equal('hello :abc'); + }); + }); }); diff --git a/src/ts/tests/client/gameLoop.spec.ts b/src/ts/tests/client/gameLoop.spec.ts index d0894d1..5459364 100644 --- a/src/ts/tests/client/gameLoop.spec.ts +++ b/src/ts/tests/client/gameLoop.spec.ts @@ -5,99 +5,99 @@ import { SinonFakeTimers, useFakeTimers, SinonStub, stub, assert } from 'sinon'; import { startGameLoop } from '../../client/gameLoop'; describe('gameLoop', () => { - let clock: SinonFakeTimers; - let requestAnimationFrame: SinonStub; - // let cancelAnimationFrame: SinonStub; + let clock: SinonFakeTimers; + let requestAnimationFrame: SinonStub; + // let cancelAnimationFrame: SinonStub; - beforeEach(() => { - clock = useFakeTimers(); - (global as any).requestAnimationFrame = requestAnimationFrame = stub(); - (global as any).cancelAnimationFrame = stub(); - }); + beforeEach(() => { + clock = useFakeTimers(); + (global as any).requestAnimationFrame = requestAnimationFrame = stub(); + (global as any).cancelAnimationFrame = stub(); + }); - afterEach(() => { - clock.restore(); - }); + afterEach(() => { + clock.restore(); + }); - after(() => { - delete (global as any).requestAnimationFrame; - delete (global as any).cancelAnimationFrame; - }); + after(() => { + delete (global as any).requestAnimationFrame; + delete (global as any).cancelAnimationFrame; + }); - describe('startGameLoop()', () => { - it('returns started promise', async () => { - await startGameLoop({ draw: noop, update: noop, init: noop, load: noop, fps: 0 }, noop).started; - }); + describe('startGameLoop()', () => { + it('returns started promise', async () => { + await startGameLoop({ draw: noop, update: noop, init: noop, load: noop, fps: 0 }, noop).started; + }); - it('loads game', async () => { - const load = stub().resolves(); + it('loads game', async () => { + const load = stub().resolves(); - await startGameLoop({ draw: noop, update: noop, init: noop, load, fps: 0 }, noop).started; + await startGameLoop({ draw: noop, update: noop, init: noop, load, fps: 0 }, noop).started; - assert.calledOnce(load); - }); + assert.calledOnce(load); + }); - it('initializes game after load finishes', async () => { - const load = stub(); - const init = stub(); + it('initializes game after load finishes', async () => { + const load = stub(); + const init = stub(); - await startGameLoop({ draw: noop, update: noop, init, load, fps: 0 }, noop).started; + await startGameLoop({ draw: noop, update: noop, init, load, fps: 0 }, noop).started; - assert.calledOnce(load); - assert.calledOnce(init); - assert.callOrder(load, init); - }); + assert.calledOnce(load); + assert.calledOnce(init); + assert.callOrder(load, init); + }); - // it('updates and draws the game', async () => { - // const update = stub(); - // const draw = stub(); - // clock.setSystemTime(0); + // it('updates and draws the game', async () => { + // const update = stub(); + // const draw = stub(); + // clock.setSystemTime(0); - // await startGameLoop({ draw, update, init: noop, load: noop, fps: 0 }, noop).started; + // await startGameLoop({ draw, update, init: noop, load: noop, fps: 0 }, noop).started; - // clock.setSystemTime(123); + // clock.setSystemTime(123); - // assert.calledOnce(requestAnimationFrame); - // requestAnimationFrame.args[0][0](); + // assert.calledOnce(requestAnimationFrame); + // requestAnimationFrame.args[0][0](); - // assert.calledWith(update, 0.123, 123); - // assert.calledOnce(draw); - // assert.callOrder(update, draw); - // }); + // assert.calledWith(update, 0.123, 123); + // assert.calledOnce(draw); + // assert.callOrder(update, draw); + // }); - it('updates the game using backup timer if animation frame did not fire for 0.1s', async () => { - const update = stub(); - const draw = stub(); - clock.setSystemTime(0); + it('updates the game using backup timer if animation frame did not fire for 0.1s', async () => { + const update = stub(); + const draw = stub(); + clock.setSystemTime(0); - await startGameLoop({ draw, update, init: noop, load: noop, fps: 0 }, noop).started; + await startGameLoop({ draw, update, init: noop, load: noop, fps: 0 }, noop).started; - clock.setSystemTime(100); - clock.tick(101); + clock.setSystemTime(100); + clock.tick(101); - assert.calledWith(update, 0.1); - assert.notCalled(draw); - }); + assert.calledWith(update, 0.1); + assert.notCalled(draw); + }); - it('reports error during update', async () => { - const error = new Error('test'); - const update = stub().throws(error); - const onError = stub(); + it('reports error during update', async () => { + const error = new Error('test'); + const update = stub().throws(error); + const onError = stub(); - await startGameLoop({ draw: noop, update, init: noop, load: noop, fps: 0 }, onError).started; + await startGameLoop({ draw: noop, update, init: noop, load: noop, fps: 0 }, onError).started; - assert.calledOnce(requestAnimationFrame); - requestAnimationFrame.args[0][0](123); + assert.calledOnce(requestAnimationFrame); + requestAnimationFrame.args[0][0](123); - assert.calledWith(onError, error); - }); + assert.calledWith(onError, error); + }); - it('throws if cancelled before init', async () => { - const { started, cancel } = startGameLoop({ draw: noop, update: noop, init: noop, load: noop, fps: 0 }, noop); + it('throws if cancelled before init', async () => { + const { started, cancel } = startGameLoop({ draw: noop, update: noop, init: noop, load: noop, fps: 0 }, noop); - cancel(); + cancel(); - await expect(started).rejectedWith('Cancelled'); - }); - }); + await expect(started).rejectedWith('Cancelled'); + }); + }); }); diff --git a/src/ts/tests/client/htmlUtils.spec.ts b/src/ts/tests/client/htmlUtils.spec.ts index 93d429c..8686672 100644 --- a/src/ts/tests/client/htmlUtils.spec.ts +++ b/src/ts/tests/client/htmlUtils.spec.ts @@ -2,221 +2,221 @@ import '../lib'; import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import { - textNode, element, removeAllNodes, removeFirstChild, createHtmlNodes, replaceNodes, findParentElement + textNode, element, removeAllNodes, removeFirstChild, createHtmlNodes, replaceNodes, findParentElement } from '../../client/htmlUtils'; import { removeItem } from '../../common/utils'; class MockHTMLElement { - nodeType = 'element'; - children: any[] = []; - events: any = {}; - attributes: any = {}; - className?: string; - parentElement?: MockHTMLElement; - constructor(public tagName: string) { } - get firstChild() { - return this.children[0]; - } - get lastChild() { - return this.children[this.children.length - 1]; - } - setAttribute(key: string, value: any) { - this.attributes[key] = value; - } - addEventListener(key: string, callback: any) { - this.events[key] = callback; - } - appendChild(node: any) { - this.children.push(node); - } - removeChild(node: any) { - removeItem(this.children, node); - } + nodeType = 'element'; + children: any[] = []; + events: any = {}; + attributes: any = {}; + className?: string; + parentElement?: MockHTMLElement; + constructor(public tagName: string) { } + get firstChild() { + return this.children[0]; + } + get lastChild() { + return this.children[this.children.length - 1]; + } + setAttribute(key: string, value: any) { + this.attributes[key] = value; + } + addEventListener(key: string, callback: any) { + this.events[key] = callback; + } + appendChild(node: any) { + this.children.push(node); + } + removeChild(node: any) { + removeItem(this.children, node); + } } class MockTextNode { - constructor(public nodeValue: string) { } + constructor(public nodeValue: string) { } } describe('htmlUtils', () => { - let querySelectorAll: SinonStub; + let querySelectorAll: SinonStub; - beforeEach(() => { - querySelectorAll = stub(); + beforeEach(() => { + querySelectorAll = stub(); - (global as any).document = { - createElement(tagName: string) { - return new MockHTMLElement(tagName); - }, - createTextNode(value: string) { - return new MockTextNode(value); - }, - querySelectorAll, - }; - }); + (global as any).document = { + createElement(tagName: string) { + return new MockHTMLElement(tagName); + }, + createTextNode(value: string) { + return new MockTextNode(value); + }, + querySelectorAll, + }; + }); - afterEach(() => { - delete (global as any).document; - }); + afterEach(() => { + delete (global as any).document; + }); - describe('createHtmlNodes()', () => { - it('creates single text node with given text', () => { - const nodes = createHtmlNodes('foo bar', 1); + describe('createHtmlNodes()', () => { + it('creates single text node with given text', () => { + const nodes = createHtmlNodes('foo bar', 1); - expect(nodes.length).equal(1); - expect(nodes[0]).instanceof(MockTextNode); - expect(nodes[0].nodeValue).equal('foo bar'); - }); + expect(nodes.length).equal(1); + expect(nodes[0]).instanceof(MockTextNode); + expect(nodes[0].nodeValue).equal('foo bar'); + }); - it('creates empty array for empty text', () => { - const nodes = createHtmlNodes('', 1); + it('creates empty array for empty text', () => { + const nodes = createHtmlNodes('', 1); - expect(nodes.length).equal(0); - }); - }); + expect(nodes.length).equal(0); + }); + }); - describe('textNode()', () => { - it('creates new text node', () => { - const node = textNode('foo'); + describe('textNode()', () => { + it('creates new text node', () => { + const node = textNode('foo'); - expect(node).instanceof(MockTextNode); - expect(node.nodeValue).equal('foo'); - }); - }); + expect(node).instanceof(MockTextNode); + expect(node.nodeValue).equal('foo'); + }); + }); - describe('element()', () => { - it('creates new HTML element', () => { - const div = element('div', 'foo-bar'); + describe('element()', () => { + it('creates new HTML element', () => { + const div = element('div', 'foo-bar'); - expect(div).instanceof(MockHTMLElement); - expect(div.tagName).equal('div'); - }); + expect(div).instanceof(MockHTMLElement); + expect(div.tagName).equal('div'); + }); - it('sets class name', () => { - const div = element('div', 'foo-bar'); + it('sets class name', () => { + const div = element('div', 'foo-bar'); - expect(div.className).equal('foo-bar'); - }); + expect(div.className).equal('foo-bar'); + }); - it('sets attribute values', () => { - const div = element('div', undefined, undefined, { foo: 'bar', test: 'boo' }); + it('sets attribute values', () => { + const div = element('div', undefined, undefined, { foo: 'bar', test: 'boo' }); - expect(div.attributes).eql({ foo: 'bar', test: 'boo' }); - }); + expect(div.attributes).eql({ foo: 'bar', test: 'boo' }); + }); - it('sets event listeners', () => { - const click = () => { }; - const touch = () => { }; + it('sets event listeners', () => { + const click = () => { }; + const touch = () => { }; - const div: any = element('div', undefined, undefined, undefined, { click, touch }); + const div: any = element('div', undefined, undefined, undefined, { click, touch }); - expect(div.events.click).equal(click); - expect(div.events.touch).equal(touch); - }); + expect(div.events.click).equal(click); + expect(div.events.touch).equal(touch); + }); - it('appends child nodes', () => { - const a = element('a'); - const b = element('b'); + it('appends child nodes', () => { + const a = element('a'); + const b = element('b'); - const div = element('div', undefined, [a, b]); + const div = element('div', undefined, [a, b]); - expect(div.children[0]).equal(a); - expect(div.children[1]).equal(b); - }); + expect(div.children[0]).equal(a); + expect(div.children[1]).equal(b); + }); - it('skips undefined nodes when appending child nodes', () => { - const a = element('a'); - const b = element('b'); + it('skips undefined nodes when appending child nodes', () => { + const a = element('a'); + const b = element('b'); - const div = element('div', undefined, [a, undefined, b]); + const div = element('div', undefined, [a, undefined, b]); - expect(div.children[0]).equal(a); - expect(div.children[1]).equal(b); - }); - }); + expect(div.children[0]).equal(a); + expect(div.children[1]).equal(b); + }); + }); - describe('removeAllNodes()', () => { - it('removes all child nodes from element', () => { - const div = element('div', undefined, [element('a'), element('b')]); + describe('removeAllNodes()', () => { + it('removes all child nodes from element', () => { + const div = element('div', undefined, [element('a'), element('b')]); - removeAllNodes(div as any); + removeAllNodes(div as any); - expect(div.children).eql([]); - }); + expect(div.children).eql([]); + }); - it('does nothing for no child nodes', () => { - const div = element('div', undefined, []); + it('does nothing for no child nodes', () => { + const div = element('div', undefined, []); - removeAllNodes(div as any); + removeAllNodes(div as any); - expect(div.children).eql([]); - }); - }); + expect(div.children).eql([]); + }); + }); - describe('removeFirstChild()', () => { - it('removes all child nodes from element', () => { - const a = element('a'); - const b = element('b'); - const div = element('div', undefined, [a, b]); + describe('removeFirstChild()', () => { + it('removes all child nodes from element', () => { + const a = element('a'); + const b = element('b'); + const div = element('div', undefined, [a, b]); - removeFirstChild(div as any); + removeFirstChild(div as any); - expect(div.children.length).equal(1); - expect(div.children[0]).equal(b); - }); + expect(div.children.length).equal(1); + expect(div.children[0]).equal(b); + }); - it('does nothing for no child nodes', () => { - const div = element('div', undefined, []); + it('does nothing for no child nodes', () => { + const div = element('div', undefined, []); - removeFirstChild(div as any); + removeFirstChild(div as any); - expect(div.children).eql([]); - }); - }); + expect(div.children).eql([]); + }); + }); - describe('replaceNodes()', () => { - it('adds new child nodes', () => { - const div = element('div', undefined, []); + describe('replaceNodes()', () => { + it('adds new child nodes', () => { + const div = element('div', undefined, []); - replaceNodes(div as any, 'foo bar'); + replaceNodes(div as any, 'foo bar'); - expect(div.children.length).equal(1); - expect(div.children[0]).instanceof(MockTextNode); - expect(div.children[0].nodeValue).equal('foo bar'); - }); + expect(div.children.length).equal(1); + expect(div.children[0]).instanceof(MockTextNode); + expect(div.children[0].nodeValue).equal('foo bar'); + }); - it('removes all but first node', () => { - const a = textNode('a'); - const b = textNode('b'); - const div = element('div', undefined, [a, b]); + it('removes all but first node', () => { + const a = textNode('a'); + const b = textNode('b'); + const div = element('div', undefined, [a, b]); - replaceNodes(div as any, 'foo bar'); + replaceNodes(div as any, 'foo bar'); - expect(div.children.length).equal(1); - expect(div.children[0]).equal(a); - expect(div.children[0].nodeValue).equal('foo bar'); - }); - }); + expect(div.children.length).equal(1); + expect(div.children[0]).equal(a); + expect(div.children[0].nodeValue).equal('foo bar'); + }); + }); - describe('findParentElement()', () => { - it('returns matched parent element', () => { - const div1 = element('div'); - const div2 = element('div'); - (div1 as any).parentElement = div2; - querySelectorAll.withArgs('foo > bar').returns([div2]); + describe('findParentElement()', () => { + it('returns matched parent element', () => { + const div1 = element('div'); + const div2 = element('div'); + (div1 as any).parentElement = div2; + querySelectorAll.withArgs('foo > bar').returns([div2]); - expect(findParentElement(div1, 'foo > bar')).equal(div2); - }); + expect(findParentElement(div1, 'foo > bar')).equal(div2); + }); - it('returns matched parent element', () => { - const div1 = element('div'); - const div2 = element('div'); - const div3 = element('div'); - (div1 as any).parentElement = div2; - (div2 as any).parentElement = div3; - querySelectorAll.withArgs('foo > bar').returns([div3]); + it('returns matched parent element', () => { + const div1 = element('div'); + const div2 = element('div'); + const div3 = element('div'); + (div1 as any).parentElement = div2; + (div2 as any).parentElement = div3; + querySelectorAll.withArgs('foo > bar').returns([div3]); - expect(findParentElement(div1, 'foo > bar')).equal(div3); - }); - }); + expect(findParentElement(div1, 'foo > bar')).equal(div3); + }); + }); }); diff --git a/src/ts/tests/client/paletteManager.spec.ts b/src/ts/tests/client/paletteManager.spec.ts index d634489..24765c8 100644 --- a/src/ts/tests/client/paletteManager.spec.ts +++ b/src/ts/tests/client/paletteManager.spec.ts @@ -4,107 +4,107 @@ import { Palette } from '../../common/interfaces'; import { releasePalette, PaletteManager } from '../../graphics/paletteManager'; describe('PaletteManager', () => { - describe('releasePalette()', () => { - it('reduces ref count on palette', () => { - const palette: Palette = { x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0), refs: 5 }; + describe('releasePalette()', () => { + it('reduces ref count on palette', () => { + const palette: Palette = { x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0), refs: 5 }; - releasePalette(palette); + releasePalette(palette); - expect(palette.refs).equal(4); - }); + expect(palette.refs).equal(4); + }); - it('does not reduce ref count below 0', () => { - const palette: Palette = { x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0), refs: 0 }; + it('does not reduce ref count below 0', () => { + const palette: Palette = { x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0), refs: 0 }; - releasePalette(palette); + releasePalette(palette); - expect(palette.refs).equal(0); - }); + expect(palette.refs).equal(0); + }); - it('does nothing if refs are undefined', () => { - const palette = {} as any; + it('does nothing if refs are undefined', () => { + const palette = {} as any; - releasePalette(palette); + releasePalette(palette); - expect(palette.refs).undefined; - }); + expect(palette.refs).undefined; + }); - it('does nothing if palette is undefined', () => { - releasePalette(undefined); - }); - }); + it('does nothing if palette is undefined', () => { + releasePalette(undefined); + }); + }); - describe('PaletteManager', () => { - let paletteManager: PaletteManager; + describe('PaletteManager', () => { + let paletteManager: PaletteManager; - beforeEach(() => { - paletteManager = new PaletteManager(); - }); + beforeEach(() => { + paletteManager = new PaletteManager(); + }); - after(() => { - paletteManager = undefined as any; - }); + after(() => { + paletteManager = undefined as any; + }); - describe('.add()', () => { - it('returns new palette', () => { - expect(paletteManager.add([1, 2, 3])) - .eql({ x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array([1, 2, 3]), refs: 1 }); - }); + describe('.add()', () => { + it('returns new palette', () => { + expect(paletteManager.add([1, 2, 3])) + .eql({ x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array([1, 2, 3]), refs: 1 }); + }); - it('returns existing palette for the same colors', () => { - const first = paletteManager.add([1, 2, 3]); + it('returns existing palette for the same colors', () => { + const first = paletteManager.add([1, 2, 3]); - expect(paletteManager.add([1, 2, 3])).equal(first); - }); + expect(paletteManager.add([1, 2, 3])).equal(first); + }); - it('increments ref count for existing palette', () => { - paletteManager.add([1, 2, 3]); + it('increments ref count for existing palette', () => { + paletteManager.add([1, 2, 3]); - expect(paletteManager.add([1, 2, 3]).refs).equal(2); - }); + expect(paletteManager.add([1, 2, 3]).refs).equal(2); + }); - it('returns new palette for the different colors', () => { - const first = paletteManager.add([1, 2, 3]); + it('returns new palette for the different colors', () => { + const first = paletteManager.add([1, 2, 3]); - expect(paletteManager.add([5, 6, 7])).not.equal(first); - }); + expect(paletteManager.add([5, 6, 7])).not.equal(first); + }); - it('converts all colors to unsigned integers', () => { - expect(paletteManager.add([-1, 2.5, 'bleh' as any]).colors) - .eql(new Uint32Array([4294967295, 2, 0])); - }); - }); + it('converts all colors to unsigned integers', () => { + expect(paletteManager.add([-1, 2.5, 'bleh' as any]).colors) + .eql(new Uint32Array([4294967295, 2, 0])); + }); + }); - describe('.addArray()', () => { - it('returns new palette', () => { - expect(paletteManager.addArray(new Uint32Array([1, 2, 3]))) - .eql({ x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array([1, 2, 3]), refs: 1 }); - }); + describe('.addArray()', () => { + it('returns new palette', () => { + expect(paletteManager.addArray(new Uint32Array([1, 2, 3]))) + .eql({ x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array([1, 2, 3]), refs: 1 }); + }); - it('returns existing palette for the same colors', () => { - const first = paletteManager.addArray(new Uint32Array([1, 2, 3])); + it('returns existing palette for the same colors', () => { + const first = paletteManager.addArray(new Uint32Array([1, 2, 3])); - expect(paletteManager.addArray(new Uint32Array([1, 2, 3]))).equal(first); - }); + expect(paletteManager.addArray(new Uint32Array([1, 2, 3]))).equal(first); + }); - it('returns new palette palette for the same colors if deduplication is turned off', () => { - const first = paletteManager.addArray(new Uint32Array([1, 2, 3])); - paletteManager.deduplicate = false; + it('returns new palette palette for the same colors if deduplication is turned off', () => { + const first = paletteManager.addArray(new Uint32Array([1, 2, 3])); + paletteManager.deduplicate = false; - expect(paletteManager.addArray(new Uint32Array([1, 2, 3]))).not.equal(first); - }); + expect(paletteManager.addArray(new Uint32Array([1, 2, 3]))).not.equal(first); + }); - it('increments ref count for existing palette', () => { - paletteManager.addArray(new Uint32Array([1, 2, 3])); + it('increments ref count for existing palette', () => { + paletteManager.addArray(new Uint32Array([1, 2, 3])); - expect(paletteManager.addArray(new Uint32Array([1, 2, 3])).refs).equal(2); - }); + expect(paletteManager.addArray(new Uint32Array([1, 2, 3])).refs).equal(2); + }); - it('returns new palette for the different colors', () => { - const first = paletteManager.addArray(new Uint32Array([1, 2, 3])); + it('returns new palette for the different colors', () => { + const first = paletteManager.addArray(new Uint32Array([1, 2, 3])); - expect(paletteManager.addArray(new Uint32Array([5, 6, 7]))).not.equal(first); - }); - }); - }); + expect(paletteManager.addArray(new Uint32Array([5, 6, 7]))).not.equal(first); + }); + }); + }); }); diff --git a/src/ts/tests/client/partyUtils.spec.ts b/src/ts/tests/client/partyUtils.spec.ts index 6b793b2..4354891 100644 --- a/src/ts/tests/client/partyUtils.spec.ts +++ b/src/ts/tests/client/partyUtils.spec.ts @@ -5,189 +5,189 @@ import { updateParty, isPonyInParty, isPartyLeader, isInParty } from '../../clie import { PonyTownGame } from '../../client/game'; describe('partyUtils', () => { - describe('updateParty()', () => { - it('creates new party', () => { - const info: PartyMember[] = [ - { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, - { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, - ]; + describe('updateParty()', () => { + it('creates new party', () => { + const info: PartyMember[] = [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, + { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, + ]; - const party = updateParty(undefined, info); + const party = updateParty(undefined, info); - expect(party).eql({ - leaderId: 2, - members: info, - }); - }); + expect(party).eql({ + leaderId: 2, + members: info, + }); + }); - it('adds new members', () => { - const party: PartyInfo = { - leaderId: 1, - members: [ - { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, - ], - }; - const info: PartyMember[] = [ - { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, - { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, - ]; + it('adds new members', () => { + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, + ], + }; + const info: PartyMember[] = [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, + { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, + ]; - updateParty(party, info); + updateParty(party, info); - expect(party).eql({ - leaderId: 2, - members: info, - }); - }); + expect(party).eql({ + leaderId: 2, + members: info, + }); + }); - it('removes members', () => { - const party: PartyInfo = { - leaderId: 1, - members: [ - { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, - { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, - ], - }; - const info: PartyMember[] = [ - { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: true, pending: false, offline: false }, - ]; + it('removes members', () => { + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: false, offline: false }, + { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, + ], + }; + const info: PartyMember[] = [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: true, pending: false, offline: false }, + ]; - updateParty(party, info); + updateParty(party, info); - expect(party).eql({ - leaderId: 1, - members: info, - }); - }); + expect(party).eql({ + leaderId: 1, + members: info, + }); + }); - it('updates members', () => { - const party: PartyInfo = { - leaderId: 1, - members: [ - { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: true, offline: true }, - { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, - ], - }; - const info: PartyMember[] = [ - { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: true, pending: false, offline: false }, - { id: 2, pony: { _foo: 'boo' } as any, self: true, leader: false, pending: false, offline: false }, - ]; + it('updates members', () => { + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: false, pending: true, offline: true }, + { id: 2, pony: { _foo: 'boo' } as any, self: false, leader: true, pending: false, offline: false }, + ], + }; + const info: PartyMember[] = [ + { id: 1, pony: { _foo: 'bar' } as any, self: false, leader: true, pending: false, offline: false }, + { id: 2, pony: { _foo: 'boo' } as any, self: true, leader: false, pending: false, offline: false }, + ]; - updateParty(party, info); + updateParty(party, info); - expect(party).eql({ - leaderId: 1, - members: info, - }); - }); + expect(party).eql({ + leaderId: 1, + members: info, + }); + }); - it('does nothing for undefined/empty party and info', () => { - expect(updateParty(undefined, undefined)).undefined; - expect(updateParty(undefined, [])).undefined; - }); - }); + it('does nothing for undefined/empty party and info', () => { + expect(updateParty(undefined, undefined)).undefined; + expect(updateParty(undefined, [])).undefined; + }); + }); - describe('isPonyInParty()', () => { - it('returns false for undefined party', () => { - expect(isPonyInParty(undefined, {} as any, false)).false; - }); + describe('isPonyInParty()', () => { + it('returns false for undefined party', () => { + expect(isPonyInParty(undefined, {} as any, false)).false; + }); - it('returns true if pony is in party', () => { - const pony = { _foo: 'bar' } as any; - const party: PartyInfo = { - leaderId: 1, - members: [ - { id: 1, pony: pony, self: false, leader: false, pending: false, offline: true }, - ], - }; + it('returns true if pony is in party', () => { + const pony = { _foo: 'bar' } as any; + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: pony, self: false, leader: false, pending: false, offline: true }, + ], + }; - expect(isPonyInParty(party, pony, false)).true; - }); + expect(isPonyInParty(party, pony, false)).true; + }); - it('returns false if pony is in party but pending', () => { - const pony = { _foo: 'bar' } as any; - const party: PartyInfo = { - leaderId: 1, - members: [ - { id: 1, pony: pony, self: false, leader: false, pending: true, offline: true }, - ], - }; + it('returns false if pony is in party but pending', () => { + const pony = { _foo: 'bar' } as any; + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: pony, self: false, leader: false, pending: true, offline: true }, + ], + }; - expect(isPonyInParty(party, pony, false)).false; - }); + expect(isPonyInParty(party, pony, false)).false; + }); - it('returns true if pony is in party and pending, but pending flag is true', () => { - const pony = { _foo: 'bar' } as any; - const party: PartyInfo = { - leaderId: 1, - members: [ - { id: 1, pony: pony, self: false, leader: false, pending: true, offline: true }, - ], - }; + it('returns true if pony is in party and pending, but pending flag is true', () => { + const pony = { _foo: 'bar' } as any; + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: pony, self: false, leader: false, pending: true, offline: true }, + ], + }; - expect(isPonyInParty(party, pony, true)).true; - }); + expect(isPonyInParty(party, pony, true)).true; + }); - it('returns false if pony is not in party', () => { - const pony = { _foo: 'bar' } as any; - const party: PartyInfo = { - leaderId: 1, - members: [ - { id: 1, pony: { _foo: 'boo' } as any, self: false, leader: false, pending: false, offline: true }, - ], - }; + it('returns false if pony is not in party', () => { + const pony = { _foo: 'bar' } as any; + const party: PartyInfo = { + leaderId: 1, + members: [ + { id: 1, pony: { _foo: 'boo' } as any, self: false, leader: false, pending: false, offline: true }, + ], + }; - expect(isPonyInParty(party, pony, false)).false; - }); - }); + expect(isPonyInParty(party, pony, false)).false; + }); + }); - describe('isPartyLeader()', () => { - let game: PonyTownGame; + describe('isPartyLeader()', () => { + let game: PonyTownGame; - beforeEach(() => { - game = {} as any; - }); + beforeEach(() => { + game = {} as any; + }); - it('returns true if player is party leader', () => { - game.player = { id: 123 } as any; - game.party = { leaderId: 123, members: [] }; + it('returns true if player is party leader', () => { + game.player = { id: 123 } as any; + game.party = { leaderId: 123, members: [] }; - expect(isPartyLeader(game)).true; - }); + expect(isPartyLeader(game)).true; + }); - it('returns false if player is not party leader', () => { - game.player = { id: 123 } as any; - game.party = { leaderId: 321, members: [] }; + it('returns false if player is not party leader', () => { + game.player = { id: 123 } as any; + game.party = { leaderId: 321, members: [] }; - expect(isPartyLeader(game)).false; - }); + expect(isPartyLeader(game)).false; + }); - it('returns false if player is not initialized', () => { - expect(isPartyLeader(game)).false; - }); - }); + it('returns false if player is not initialized', () => { + expect(isPartyLeader(game)).false; + }); + }); - describe('isInParty()', () => { - let game: PonyTownGame; + describe('isInParty()', () => { + let game: PonyTownGame; - beforeEach(() => { - game = {} as any; - }); + beforeEach(() => { + game = {} as any; + }); - it('returns true if player is in party', () => { - game.party = { leaderId: 0, members: [{ id: 123 } as any] }; + it('returns true if player is in party', () => { + game.party = { leaderId: 0, members: [{ id: 123 } as any] }; - expect(isInParty(game)).true; - }); + expect(isInParty(game)).true; + }); - it('returns false if player is not in a party', () => { - expect(isInParty(game)).false; - }); + it('returns false if player is not in a party', () => { + expect(isInParty(game)).false; + }); - it('returns false if party is empty', () => { - game.party = { leaderId: 0, members: [] }; + it('returns false if party is empty', () => { + game.party = { leaderId: 0, members: [] }; - expect(isInParty(game)).false; - }); - }); + expect(isInParty(game)).false; + }); + }); }); diff --git a/src/ts/tests/client/ponyDraw.spec.ts b/src/ts/tests/client/ponyDraw.spec.ts index bd42ec5..bf4d23c 100644 --- a/src/ts/tests/client/ponyDraw.spec.ts +++ b/src/ts/tests/client/ponyDraw.spec.ts @@ -18,136 +18,136 @@ import { createCanvas } from '../../server/canvasUtilsNode'; const baseFilePath = pathTo('src', 'tests', 'pony'); function createTests(): [string, PonyState, DrawPonyOptions, string][] { - const state = defaultPonyState(); - const sitting: PonyState = { ...state, animation: sit, animationFrame: 0 }; - const sittingDown: PonyState = { ...state, animation: sitDown, animationFrame: 0 }; - const wingOpen: PonyState = { ...state, animation: fly, animationFrame: 0 }; - // const lying: PonyState = { ...state, animation: lie, animationFrame: 0 }; - const trotting: PonyState = { ...state, animation: trot, animationFrame: 10 }; - const blushing: PonyState = { ...state, expression: parseExpression('o//o') }; - //const turned: PonyState = { ...defaultState, headTurned: true }; - const holding: PonyState = { ...state, holding: apple(0, 0) }; - const holdingLantern: PonyState = { ...state, holding: jackoLanternOn(0, 0) }; - const holdingLetter: PonyState = { ...state, holding: letter(0, 0) }; - const laughing: PonyState = { ...state, headAnimation: laugh }; - const blinking: PonyState = { ...state, blinkFrame: 4 }; - const blinkingAngry: PonyState = { ...state, expression: parseExpression('|B)'), blinkFrame: 5 }; - const blinkingSkip: PonyState = { ...state, expression: parseExpression('|B)'), blinkFrame: 2 }; - const faceExtra: PonyState = { - ...state, - drawFaceExtra: batch => batch.drawSprite(candy.color, WHITE, mockPaletteManager.addArray(candy.palettes![0]), 25, 40), - }; - const faceExtraHoldingLetter: PonyState = { ...faceExtra, holding: letter(0, 0) }; + const state = defaultPonyState(); + const sitting: PonyState = { ...state, animation: sit, animationFrame: 0 }; + const sittingDown: PonyState = { ...state, animation: sitDown, animationFrame: 0 }; + const wingOpen: PonyState = { ...state, animation: fly, animationFrame: 0 }; + // const lying: PonyState = { ...state, animation: lie, animationFrame: 0 }; + const trotting: PonyState = { ...state, animation: trot, animationFrame: 10 }; + const blushing: PonyState = { ...state, expression: parseExpression('o//o') }; + //const turned: PonyState = { ...defaultState, headTurned: true }; + const holding: PonyState = { ...state, holding: apple(0, 0) }; + const holdingLantern: PonyState = { ...state, holding: jackoLanternOn(0, 0) }; + const holdingLetter: PonyState = { ...state, holding: letter(0, 0) }; + const laughing: PonyState = { ...state, headAnimation: laugh }; + const blinking: PonyState = { ...state, blinkFrame: 4 }; + const blinkingAngry: PonyState = { ...state, expression: parseExpression('|B)'), blinkFrame: 5 }; + const blinkingSkip: PonyState = { ...state, expression: parseExpression('|B)'), blinkFrame: 2 }; + const faceExtra: PonyState = { + ...state, + drawFaceExtra: batch => batch.drawSprite(candy.color, WHITE, mockPaletteManager.addArray(candy.palettes![0]), 25, 40), + }; + const faceExtraHoldingLetter: PonyState = { ...faceExtra, holding: letter(0, 0) }; - const options: DrawPonyOptions = { ...defaultDrawPonyOptions(), shadow: true }; - const flipped: DrawPonyOptions = { ...options, flipped: true }; - const selected: DrawPonyOptions = { ...options, selected: true }; - const extra: DrawPonyOptions = { ...options, extra: true }; + const options: DrawPonyOptions = { ...defaultDrawPonyOptions(), shadow: true }; + const flipped: DrawPonyOptions = { ...options, flipped: true }; + const selected: DrawPonyOptions = { ...options, selected: true }; + const extra: DrawPonyOptions = { ...options, extra: true }; - const base = 'CAP////apSDaICA2QAJpDhAvAAwAIA=='; - const baseWing = 'CAP////apSDaICA2QAJpIhAvAAwAIQgIAA=='; - const whitePetal = 'CAiidXWCfnZaUERMTEzb29vl5eXj4diysrI2oIIAAhiBVgCfgAYAGoAOEBpAYQFCAJCAkwAoQgDIgB4AG4AgACQgJAQgJCAAIA=='; - const cmSocks = 'CAdZp7LapSBFjaJtZDXm5uZWVlb85VlWsACAAIE5CADhAE/AAwAOgBIQGIBhAQCEaBdddn8f8AcDgcAf8fg='; - const griffon = 'CAamno3apSD/1wDdrljp6ellX1I2QCZkJcAQKbcIBJAG8AtoDiBDUYBowLIgGEBA'; + const base = 'CAP////apSDaICA2QAJpDhAvAAwAIA=='; + const baseWing = 'CAP////apSDaICA2QAJpIhAvAAwAIQgIAA=='; + const whitePetal = 'CAiidXWCfnZaUERMTEzb29vl5eXj4diysrI2oIIAAhiBVgCfgAYAGoAOEBpAYQFCAJCAkwAoQgDIgB4AG4AgACQgJAQgJCAAIA=='; + const cmSocks = 'CAdZp7LapSBFjaJtZDXm5uZWVlb85VlWsACAAIE5CADhAE/AAwAOgBIQGIBhAQCEaBdddn8f8AcDgcAf8fg='; + const griffon = 'CAamno3apSD/1wDdrljp6ellX1I2QCZkJcAQKbcIBJAG8AtoDiBDUYBowLIgGEBA'; - return [ - ['none.png', state, options, ''], - ['base.png', state, options, base], - ['selected.png', state, selected, base], - ['offline.png', state, options, 'CAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA=='], - ['deer.png', state, options, 'CA18ZlPTh1Gfj3ofGRiqaUe9f12wnWHIw7Q0NDTcxl+ccU7S0YX///82wQIAAgTiBAAHWAJ+ICpCAwjVFZmiGquzYEgJnQAQhAJhAQiElWEQpZsA3vamBgAAAGBgAAAGBg=='], - ['all.png', state, options, 'CFTTzbqagEXaTSAgftqjmpqYj4Bi2iAA/88Ad///APWUjn0AmRgAs38AZrOzAKyyqYxWR0dzaU00ISHdVlZ9Jib/5VgekP8yzTLacNbcFDx//9Sqkg8AWrMCkAKZOJWaAB8+s4uzmxukr5BTcxl2r6g+jdkkcGcQQXBS/wBevD++0EifszfAwMBMTEyYmJgaGhro8pDWu1Tugu6nRKeVhW9nS0NPT09SUlJoWkZILyc3Nzc5OTn/pQDWmy68mFWpl3a4r55XOwhlShttWjVzZk+Jg3kzIwUsIAowJhIsJRonJiPV1dWysrKQkJBvb2//////9OH/6sP/04L/xl54bAkEgihOBBAwgAKGrAEABDiBIoAsYNHD4gIQIhInAAFBWpqAWLmDJo2AcOnj6BDENALFzBk0bAOHTx9AhpaALFzBk0BEdPH0EIAjJIgIlTCdRCAKSqEAVlkgoFi5gIjp6gqAtLmFxo2AvOnmCBDMMAxLmDJoDI6ePoJQIALgA7GdAZlzBk0bAaHTx9AhhGgMy5gyaNgNDp4+gQwjQGZcwZNGwGh08fQIYioDMuYMmjYDQ6ePoEMIMGrZu4By6dvIygGZcwZNAaHTx9BANAZlzBk0bAaHTx9Ahy9fP4EGFDiRY0eRJlS5k2dPb0KNKnUA'], - ['dragon.png', state, options, 'CAeokpLapSBgYGBSUlJqamqAcHBEREQ2oAIAIhkJEAT8ADAA3gDsAcgQMqICbgA='], - ['paws.png', state, options, 'CAa8q3/apiDapSD/1wCIXCjc0K82oCYAApkJUAQLkkADCA2gFtAbwIaRAGiAQA=='], - ['paws-socks.png', state, options, 'CAe8q3/apiDapSD/1wCIXCjc0K9GRkZWqAmAAKZChAEC5JAAwgNoBbQG8CGkQBogEIQDwgHA'], - ['glasses.png', state, options, 'CAPW0bTapSBZQSw2QAJpOgCfgAYAGIA4QGEBASAAAA=='], - ['griffon.png', state, options, griffon], - ['griffon-lantern.png', holdingLantern, options, griffon], - ['griffon-socks.png', state, options, 'CAemno3apSD/1wDdrljp6elf/8ZlX1JWkAmZChAECm3CASQBvQl1oDiiBuRgHjAsiAYQESgGhAMA'], - ['sit.png', sitting, options, base], - ['sit-wing.png', sitting, options, baseWing], - ['sit-socks.png', sitting, options, cmSocks], - ['sit-socks-cm.png', { ...sittingDown, animationFrame: 8 }, options, cmSocks], - ['sit-fetlocks.png', sitting, options, whitePetal], - ['sit-paws.png', sitting, options, 'CAmmno3apSD/1wDdrljp6ellX1JQODDb29v///82QCZiBVgCBIzOEASQBvAFtAcQIKowBowKkQDCAgEQoPEQDdZgAA=='], - ['sit-sword.png', sitting, options, 'CAf////apSDaICCVhW9nS0NPT09SUlI2QAJkKcIFcADAAgACIGEu4A=='], - ['sit-neck-accessory.png', sitting, options, 'CAeH5o7VplTH8K4yvVL/36cbGxtXV1c2wIIAIALkIAN8AT8ADAA1AEhAcgQLkIChAHCAMIhIq/DbYb5DZA=='], - ['trot.png', trotting, options, whitePetal], - ['holding.png', holding, options, whitePetal], - ['laughing.png', laughing, options, whitePetal], - ['blinking.png', blinking, options, whitePetal], - ['blinking-angry.png', blinkingAngry, options, whitePetal], - ['blinking-skip.png', blinkingSkip, options, whitePetal], - ['face-extra.png', faceExtra, options, whitePetal], - ['face-extra-holding-letter.png', faceExtraHoldingLetter, options, whitePetal], - ['cm-flip.png', state, flipped, 'CAeCQpnapSD/1wD////ugu7/pQD/4at2pAAmQoQBPwAMADEAOEBhAQEIBEI16y43AA3AA3A3A3A='], - ['freckles-flip.png', state, flipped, 'CA3/AADapSAnJyf/1wCK8f/////u7u5U3OkyzTKVhW9nS0NPT09SUlI2wAIAAAbiBAAHOAJ+ABgAYgBIQGEBAJhIrEQoPExgwq80AA=='], - //['head-turned.png', turned, options, whitePetal], - ['extra.png', state, extra, 'CAWVlZW5ubn///9SUlIvLy82QIxkI0IEcgAYAGIAsIDCA4AAFAoootFFoFA='], - ['blush.png', blushing, options, base], - ['hat-horns.png', state, options, 'CAfMoYvPWVnapSD/1wA8PDzj0M3uVVU2QAJkJkAQLkkADAA8AFhAYQGcAwTAHAA='], - ['hat-bald.png', state, options, 'CAbi38/PWVnapSD/1wBwZmbuVVU2oJoAApkJkAQLkkQFsACcABBEAYA='], - ['holding-letter.png', holdingLetter, options, 'CAPHY2MghtoA/202QAJpLgCfgAYAGIA7QGEBCIBA'], - ['mustache.png', state, options, 'CATexazapSD/1wC8UCw2QAJkJcAQKbcADAAwAEAjAQA='], - ['hair-in-front-of-wing.png', state, options, 'CAOMx+LapSD04HY2QAJpIgCfgAYAGIA6AGEBQgIA'], - ['hair-behind-neck-accessory.png', state, options, 'CAb///9xcXHaICDbQEBra2t+fn42oAIAAhkJ8IFcADAA5AEqAcgQNgBYAYA='], - // TEMP: gryphon wing pattern ['lie.png', lying, options, 'CAb///9xcXHaICDHx8eqqqq9vb02QAJkJEIFcADAAwgEnAcgQNiMS4A='], - ['no-body.png', state, { ...options, no: NoDraw.Body }, baseWing], - ['wing-open.png', wingOpen, options, griffon], - ['sitting-with-skirt-and-socks.png', sitting, options, 'DAb////CwsL/pQAjVdk9ySPugu42QAJkKsAT8ADAAnAASAAcICBCASIAqIA0ACA='], - ['cape.png', state, options, 'DASmlJTX19fHwJxbVVU2QAJkKkAT8ADAAxADhAYQEABCAQA='], - // ['no-behind.png', state, { ...options, noBehind: true }, baseWing], - // ['no-behind-leg.png', state, { ...options, noBehindLeg: true }, baseWing], - // ['no-behind-body.png', state, { ...options, noBehindBody: true }, baseWing], - // ['no-front.png', state, { ...options, noFront: true }, baseWing], - ]; + return [ + ['none.png', state, options, ''], + ['base.png', state, options, base], + ['selected.png', state, selected, base], + ['offline.png', state, options, 'CAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA=='], + ['deer.png', state, options, 'CA18ZlPTh1Gfj3ofGRiqaUe9f12wnWHIw7Q0NDTcxl+ccU7S0YX///82wQIAAgTiBAAHWAJ+ICpCAwjVFZmiGquzYEgJnQAQhAJhAQiElWEQpZsA3vamBgAAAGBgAAAGBg=='], + ['all.png', state, options, 'CFTTzbqagEXaTSAgftqjmpqYj4Bi2iAA/88Ad///APWUjn0AmRgAs38AZrOzAKyyqYxWR0dzaU00ISHdVlZ9Jib/5VgekP8yzTLacNbcFDx//9Sqkg8AWrMCkAKZOJWaAB8+s4uzmxukr5BTcxl2r6g+jdkkcGcQQXBS/wBevD++0EifszfAwMBMTEyYmJgaGhro8pDWu1Tugu6nRKeVhW9nS0NPT09SUlJoWkZILyc3Nzc5OTn/pQDWmy68mFWpl3a4r55XOwhlShttWjVzZk+Jg3kzIwUsIAowJhIsJRonJiPV1dWysrKQkJBvb2//////9OH/6sP/04L/xl54bAkEgihOBBAwgAKGrAEABDiBIoAsYNHD4gIQIhInAAFBWpqAWLmDJo2AcOnj6BDENALFzBk0bAOHTx9AhpaALFzBk0BEdPH0EIAjJIgIlTCdRCAKSqEAVlkgoFi5gIjp6gqAtLmFxo2AvOnmCBDMMAxLmDJoDI6ePoJQIALgA7GdAZlzBk0bAaHTx9AhhGgMy5gyaNgNDp4+gQwjQGZcwZNGwGh08fQIYioDMuYMmjYDQ6ePoEMIMGrZu4By6dvIygGZcwZNAaHTx9BANAZlzBk0bAaHTx9Ahy9fP4EGFDiRY0eRJlS5k2dPb0KNKnUA'], + ['dragon.png', state, options, 'CAeokpLapSBgYGBSUlJqamqAcHBEREQ2oAIAIhkJEAT8ADAA3gDsAcgQMqICbgA='], + ['paws.png', state, options, 'CAa8q3/apiDapSD/1wCIXCjc0K82oCYAApkJUAQLkkADCA2gFtAbwIaRAGiAQA=='], + ['paws-socks.png', state, options, 'CAe8q3/apiDapSD/1wCIXCjc0K9GRkZWqAmAAKZChAEC5JAAwgNoBbQG8CGkQBogEIQDwgHA'], + ['glasses.png', state, options, 'CAPW0bTapSBZQSw2QAJpOgCfgAYAGIA4QGEBASAAAA=='], + ['griffon.png', state, options, griffon], + ['griffon-lantern.png', holdingLantern, options, griffon], + ['griffon-socks.png', state, options, 'CAemno3apSD/1wDdrljp6elf/8ZlX1JWkAmZChAECm3CASQBvQl1oDiiBuRgHjAsiAYQESgGhAMA'], + ['sit.png', sitting, options, base], + ['sit-wing.png', sitting, options, baseWing], + ['sit-socks.png', sitting, options, cmSocks], + ['sit-socks-cm.png', { ...sittingDown, animationFrame: 8 }, options, cmSocks], + ['sit-fetlocks.png', sitting, options, whitePetal], + ['sit-paws.png', sitting, options, 'CAmmno3apSD/1wDdrljp6ellX1JQODDb29v///82QCZiBVgCBIzOEASQBvAFtAcQIKowBowKkQDCAgEQoPEQDdZgAA=='], + ['sit-sword.png', sitting, options, 'CAf////apSDaICCVhW9nS0NPT09SUlI2QAJkKcIFcADAAgACIGEu4A=='], + ['sit-neck-accessory.png', sitting, options, 'CAeH5o7VplTH8K4yvVL/36cbGxtXV1c2wIIAIALkIAN8AT8ADAA1AEhAcgQLkIChAHCAMIhIq/DbYb5DZA=='], + ['trot.png', trotting, options, whitePetal], + ['holding.png', holding, options, whitePetal], + ['laughing.png', laughing, options, whitePetal], + ['blinking.png', blinking, options, whitePetal], + ['blinking-angry.png', blinkingAngry, options, whitePetal], + ['blinking-skip.png', blinkingSkip, options, whitePetal], + ['face-extra.png', faceExtra, options, whitePetal], + ['face-extra-holding-letter.png', faceExtraHoldingLetter, options, whitePetal], + ['cm-flip.png', state, flipped, 'CAeCQpnapSD/1wD////ugu7/pQD/4at2pAAmQoQBPwAMADEAOEBhAQEIBEI16y43AA3AA3A3A3A='], + ['freckles-flip.png', state, flipped, 'CA3/AADapSAnJyf/1wCK8f/////u7u5U3OkyzTKVhW9nS0NPT09SUlI2wAIAAAbiBAAHOAJ+ABgAYgBIQGEBAJhIrEQoPExgwq80AA=='], + //['head-turned.png', turned, options, whitePetal], + ['extra.png', state, extra, 'CAWVlZW5ubn///9SUlIvLy82QIxkI0IEcgAYAGIAsIDCA4AAFAoootFFoFA='], + ['blush.png', blushing, options, base], + ['hat-horns.png', state, options, 'CAfMoYvPWVnapSD/1wA8PDzj0M3uVVU2QAJkJkAQLkkADAA8AFhAYQGcAwTAHAA='], + ['hat-bald.png', state, options, 'CAbi38/PWVnapSD/1wBwZmbuVVU2oJoAApkJkAQLkkQFsACcABBEAYA='], + ['holding-letter.png', holdingLetter, options, 'CAPHY2MghtoA/202QAJpLgCfgAYAGIA7QGEBCIBA'], + ['mustache.png', state, options, 'CATexazapSD/1wC8UCw2QAJkJcAQKbcADAAwAEAjAQA='], + ['hair-in-front-of-wing.png', state, options, 'CAOMx+LapSD04HY2QAJpIgCfgAYAGIA6AGEBQgIA'], + ['hair-behind-neck-accessory.png', state, options, 'CAb///9xcXHaICDbQEBra2t+fn42oAIAAhkJ8IFcADAA5AEqAcgQNgBYAYA='], + // TEMP: gryphon wing pattern ['lie.png', lying, options, 'CAb///9xcXHaICDHx8eqqqq9vb02QAJkJEIFcADAAwgEnAcgQNiMS4A='], + ['no-body.png', state, { ...options, no: NoDraw.Body }, baseWing], + ['wing-open.png', wingOpen, options, griffon], + ['sitting-with-skirt-and-socks.png', sitting, options, 'DAb////CwsL/pQAjVdk9ySPugu42QAJkKsAT8ADAAnAASAAcICBCASIAqIA0ACA='], + ['cape.png', state, options, 'DASmlJTX19fHwJxbVVU2QAJkKkAT8ADAAxADhAYQEABCAQA='], + // ['no-behind.png', state, { ...options, noBehind: true }, baseWing], + // ['no-behind-leg.png', state, { ...options, noBehindLeg: true }, baseWing], + // ['no-behind-body.png', state, { ...options, noBehindBody: true }, baseWing], + // ['no-front.png', state, { ...options, noFront: true }, baseWing], + ]; } function createOtherTests(): [string, string, Partial, PonyState, DrawPonyOptions][] { - const state = defaultPonyState(); - const options: DrawPonyOptions = { ...defaultDrawPonyOptions(), shadow: true }; + const state = defaultPonyState(); + const options: DrawPonyOptions = { ...defaultDrawPonyOptions(), shadow: true }; - return [ - ['eyes-0.png', `doesn't allow 0 value for eyes`, { eyeOpennessLeft: 0, eyeOpennessRight: 0, lockEyes: false }, state, options], - ]; + return [ + ['eyes-0.png', `doesn't allow 0 value for eyes`, { eyeOpennessLeft: 0, eyeOpennessRight: 0, lockEyes: false }, state, options], + ]; } describe('ponyUtils', () => { - before(loadSprites); - before(() => clearCompareResults('pony')); + before(loadSprites); + before(() => clearCompareResults('pony')); - describe('drawPony()', () => { - createTests().forEach(([file, state, options, data]) => it(`correct for ${file}`, () => { - const filePath = path.join(baseFilePath, file); - const expected = loadImageAsCanvas(filePath); - const info = decodePonyInfo(data, mockPaletteManager); - const actual = drawPonyCanvas(TRANSPARENT, info, state, options); - compareCanvases(expected, actual, filePath, 'pony'); - })); + describe('drawPony()', () => { + createTests().forEach(([file, state, options, data]) => it(`correct for ${file}`, () => { + const filePath = path.join(baseFilePath, file); + const expected = loadImageAsCanvas(filePath); + const info = decodePonyInfo(data, mockPaletteManager); + const actual = drawPonyCanvas(TRANSPARENT, info, state, options); + compareCanvases(expected, actual, filePath, 'pony'); + })); - createOtherTests().forEach(([file, name, data, state, options]) => it(name, () => { - const filePath = path.join(baseFilePath, file); - const expected = loadImageAsCanvas(filePath); - const ponyInfo = { ...createDefaultPony(), ...data }; - const compressed = compressPonyString(ponyInfo); - const info = decodePonyInfo(compressed, mockPaletteManager); - const actual = drawPonyCanvas(TRANSPARENT, info, state, options); - compareCanvases(expected, actual, filePath, 'pony'); - })); - }); + createOtherTests().forEach(([file, name, data, state, options]) => it(name, () => { + const filePath = path.join(baseFilePath, file); + const expected = loadImageAsCanvas(filePath); + const ponyInfo = { ...createDefaultPony(), ...data }; + const compressed = compressPonyString(ponyInfo); + const info = decodePonyInfo(compressed, mockPaletteManager); + const actual = drawPonyCanvas(TRANSPARENT, info, state, options); + compareCanvases(expected, actual, filePath, 'pony'); + })); + }); }); function drawPonyCanvas(bg: number, info: PalettePonyInfo, state: PonyState, options: DrawPonyOptions) { - state.blushColor = blushColor(info.coatPalette.colors[1]); - const canvas = drawCanvas(80, 80, paletteSpriteSheet, bg, batch => drawPony(batch, info, state, 40, 70, options)); + state.blushColor = blushColor(info.coatPalette.colors[1]); + const canvas = drawCanvas(80, 80, paletteSpriteSheet, bg, batch => drawPony(batch, info, state, 40, 70, options)); - if (options.flipped) { - const flipped = createCanvas(canvas.width, canvas.height); - const context = flipped.getContext('2d')!; - context.scale(-1, 1); - context.drawImage(canvas, -canvas.width, 0); - return flipped; - } else { - return canvas; - } + if (options.flipped) { + const flipped = createCanvas(canvas.width, canvas.height); + const context = flipped.getContext('2d')!; + context.scale(-1, 1); + context.drawImage(canvas, -canvas.width, 0); + return flipped; + } else { + return canvas; + } } diff --git a/src/ts/tests/client/ponyStates.spec.ts b/src/ts/tests/client/ponyStates.spec.ts index 35db6da..5ffe17d 100644 --- a/src/ts/tests/client/ponyStates.spec.ts +++ b/src/ts/tests/client/ponyStates.spec.ts @@ -1,121 +1,121 @@ import '../lib'; import { expect } from 'chai'; import { - trotting, flying, standing, lying, sitting, hovering, toBoopState, booping, boopingSitting, - boopingLying, boopingFlying, sittingDown, isFlyingUp, flyingUp, trottingToFlying, flyingToTrotting, - flyingDown, isFlyingUpOrDown, isFlyingDown + trotting, flying, standing, lying, sitting, hovering, toBoopState, booping, boopingSitting, + boopingLying, boopingFlying, sittingDown, isFlyingUp, flyingUp, trottingToFlying, flyingToTrotting, + flyingDown, isFlyingUpOrDown, isFlyingDown } from '../../client/ponyStates'; import { EntityState } from '../../common/interfaces'; import { flagsToState } from '../../common/pony'; describe('ponyStates', () => { - describe('flagsToState()', () => { - it('returns trotting if moving', () => { - expect(flagsToState(EntityState.PonyStanding, true, false)).equal(trotting); - }); + describe('flagsToState()', () => { + it('returns trotting if moving', () => { + expect(flagsToState(EntityState.PonyStanding, true, false)).equal(trotting); + }); - it('returns flying if moving and flying', () => { - expect(flagsToState(EntityState.PonyFlying, true, false)).equal(flying); - }); + it('returns flying if moving and flying', () => { + expect(flagsToState(EntityState.PonyFlying, true, false)).equal(flying); + }); - it('returns standing for standing state', () => { - expect(flagsToState(EntityState.PonyStanding, false, false)).equal(standing); - }); + it('returns standing for standing state', () => { + expect(flagsToState(EntityState.PonyStanding, false, false)).equal(standing); + }); - it('returns trotting for walking state', () => { - expect(flagsToState(EntityState.PonyWalking, false, false)).equal(trotting); - }); + it('returns trotting for walking state', () => { + expect(flagsToState(EntityState.PonyWalking, false, false)).equal(trotting); + }); - it('returns trotting for trotting state', () => { - expect(flagsToState(EntityState.PonyTrotting, false, false)).equal(trotting); - }); + it('returns trotting for trotting state', () => { + expect(flagsToState(EntityState.PonyTrotting, false, false)).equal(trotting); + }); - it('returns sitting for sitting state', () => { - expect(flagsToState(EntityState.PonySitting, false, false)).equal(sitting); - }); + it('returns sitting for sitting state', () => { + expect(flagsToState(EntityState.PonySitting, false, false)).equal(sitting); + }); - it('returns lying for lying state', () => { - expect(flagsToState(EntityState.PonyLying, false, false)).equal(lying); - }); + it('returns lying for lying state', () => { + expect(flagsToState(EntityState.PonyLying, false, false)).equal(lying); + }); - it('returns hovering for flying state', () => { - expect(flagsToState(EntityState.PonyFlying, false, false)).equal(hovering); - }); + it('returns hovering for flying state', () => { + expect(flagsToState(EntityState.PonyFlying, false, false)).equal(hovering); + }); - it('throws on invalid state', () => { - expect(() => flagsToState(112, false, false)).throw('Invalid pony state (112)'); - }); - }); + it('throws on invalid state', () => { + expect(() => flagsToState(112, false, false)).throw('Invalid pony state (112)'); + }); + }); - describe('isFlyingUp()', () => { - it('returns true if flying up', () => { - expect(isFlyingUp(flyingUp)).true; - }); + describe('isFlyingUp()', () => { + it('returns true if flying up', () => { + expect(isFlyingUp(flyingUp)).true; + }); - it('returns true if transitioning from trotting to flying', () => { - expect(isFlyingUp(trottingToFlying)).true; - }); + it('returns true if transitioning from trotting to flying', () => { + expect(isFlyingUp(trottingToFlying)).true; + }); - it('returns false for any other state', () => { - expect(isFlyingUp(standing)).false; - }); - }); + it('returns false for any other state', () => { + expect(isFlyingUp(standing)).false; + }); + }); - describe('isFlyingDown()', () => { - it('returns true if flying down', () => { - expect(isFlyingDown(flyingDown)).true; - }); + describe('isFlyingDown()', () => { + it('returns true if flying down', () => { + expect(isFlyingDown(flyingDown)).true; + }); - it('returns true if transitioning from flying to trotting', () => { - expect(isFlyingDown(flyingToTrotting)).true; - }); + it('returns true if transitioning from flying to trotting', () => { + expect(isFlyingDown(flyingToTrotting)).true; + }); - it('returns false for any other state', () => { - expect(isFlyingDown(standing)).false; - }); - }); + it('returns false for any other state', () => { + expect(isFlyingDown(standing)).false; + }); + }); - describe('isFlyingUpOrDown()', () => { - it('returns true if flying up', () => { - expect(isFlyingUpOrDown(flyingUp)).true; - }); + describe('isFlyingUpOrDown()', () => { + it('returns true if flying up', () => { + expect(isFlyingUpOrDown(flyingUp)).true; + }); - it('returns true if transitioning from trotting to flying', () => { - expect(isFlyingUpOrDown(trottingToFlying)).true; - }); + it('returns true if transitioning from trotting to flying', () => { + expect(isFlyingUpOrDown(trottingToFlying)).true; + }); - it('returns true if flying down', () => { - expect(isFlyingUpOrDown(flyingDown)).true; - }); + it('returns true if flying down', () => { + expect(isFlyingUpOrDown(flyingDown)).true; + }); - it('returns true if transitioning from flying to trotting', () => { - expect(isFlyingUpOrDown(flyingToTrotting)).true; - }); + it('returns true if transitioning from flying to trotting', () => { + expect(isFlyingUpOrDown(flyingToTrotting)).true; + }); - it('returns false for any other state', () => { - expect(isFlyingUpOrDown(standing)).false; - }); - }); + it('returns false for any other state', () => { + expect(isFlyingUpOrDown(standing)).false; + }); + }); - describe('toBoopState()', () => { - it('returns booping for standing state', () => { - expect(toBoopState(standing)).equal(booping); - }); + describe('toBoopState()', () => { + it('returns booping for standing state', () => { + expect(toBoopState(standing)).equal(booping); + }); - it('returns boopingSitting for sitting state', () => { - expect(toBoopState(sitting)).equal(boopingSitting); - }); + it('returns boopingSitting for sitting state', () => { + expect(toBoopState(sitting)).equal(boopingSitting); + }); - it('returns boopingLying for lying state', () => { - expect(toBoopState(lying)).equal(boopingLying); - }); + it('returns boopingLying for lying state', () => { + expect(toBoopState(lying)).equal(boopingLying); + }); - it('returns boopingFlying for hovering state', () => { - expect(toBoopState(hovering)).equal(boopingFlying); - }); + it('returns boopingFlying for hovering state', () => { + expect(toBoopState(hovering)).equal(boopingFlying); + }); - it('returns undefined for all other states', () => { - expect(toBoopState(sittingDown)).undefined; - }); - }); + it('returns undefined for all other states', () => { + expect(toBoopState(sittingDown)).undefined; + }); + }); }); diff --git a/src/ts/tests/client/spriteFont.spec.ts b/src/ts/tests/client/spriteFont.spec.ts index b9a8663..f4ac4c8 100644 --- a/src/ts/tests/client/spriteFont.spec.ts +++ b/src/ts/tests/client/spriteFont.spec.ts @@ -9,59 +9,59 @@ import { lineBreak, drawText } from '../../graphics/spriteFont'; import { commonPalettes } from '../../graphics/graphicsUtils'; const tests = [ - ['Tiny Pony Face!', 'ascii.png'], - ['New lines\nanother line', 'newlines.png'], - ['ŚŃĄjGiýŽžĹ弾Ŕŕ', 'special.png'], - ['👃🙂😵😠😐', 'tiny.png'], - ['АаБбВвГг', 'russian.png'], - ['ΑΒΓΔΕΖΗΘ', 'greek.png'], - ['ぁあぃいぅうぇえ', 'hiragana.png'], - ['ァアィイゥウェエ', 'katakana.png'], - ['漢字', 'kanji.png'], - ['ABCDEFabcdef', 'romaji.png'], - ['emoji: 💚🍎🌠🎲', 'emoji.png'], - ['◠◡◯◰▤▥▦●', 'shapes.png'], - ['abcde\ufe0efg\ufe0fhij', 'variants.png'], - ['‒⁇;⁈⁉“_”.,`', 'punctuation.png'], - ['口古句另叨 龈龋龍龟', 'chinese.png'], + ['Tiny Pony Face!', 'ascii.png'], + ['New lines\nanother line', 'newlines.png'], + ['ŚŃĄjGiýŽžĹ弾Ŕŕ', 'special.png'], + ['👃🙂😵😠😐', 'tiny.png'], + ['АаБбВвГг', 'russian.png'], + ['ΑΒΓΔΕΖΗΘ', 'greek.png'], + ['ぁあぃいぅうぇえ', 'hiragana.png'], + ['ァアィイゥウェエ', 'katakana.png'], + ['漢字', 'kanji.png'], + ['ABCDEFabcdef', 'romaji.png'], + ['emoji: 💚🍎🌠🎲', 'emoji.png'], + ['◠◡◯◰▤▥▦●', 'shapes.png'], + ['abcde\ufe0efg\ufe0fhij', 'variants.png'], + ['‒⁇;⁈⁉“_”.,`', 'punctuation.png'], + ['口古句另叨 龈龋龍龟', 'chinese.png'], ]; describe('SpriteFont', () => { - before(loadSprites); - before(() => clearCompareResults('font')); + before(loadSprites); + before(() => clearCompareResults('font')); - describe('drawText()', () => { - const width = 100; - const height = 30; + describe('drawText()', () => { + const width = 100; + const height = 30; - function test(file: string, draw: (batch: ContextSpriteBatch) => void) { - const filePath = pathTo('src', 'tests', 'font', file); - const expected = loadImageAsCanvas(filePath); - const actual = drawCanvas(width, height, paletteSpriteSheet, WHITE, draw); - compareCanvases(expected, actual, filePath, 'font'); - } + function test(file: string, draw: (batch: ContextSpriteBatch) => void) { + const filePath = pathTo('src', 'tests', 'font', file); + const expected = loadImageAsCanvas(filePath); + const actual = drawCanvas(width, height, paletteSpriteSheet, WHITE, draw); + compareCanvases(expected, actual, filePath, 'font'); + } - tests.forEach(([text, file]) => it(`correct for "${text}" (${file})`, () => { - test(file, batch => drawText(batch, text, fontPal, BLACK, 5, 5, { - palette: commonPalettes.mainFont.white, emojiPalette: commonPalettes.mainFont.emoji - })); - })); + tests.forEach(([text, file]) => it(`correct for "${text}" (${file})`, () => { + test(file, batch => drawText(batch, text, fontPal, BLACK, 5, 5, { + palette: commonPalettes.mainFont.white, emojiPalette: commonPalettes.mainFont.emoji + })); + })); - describe('lineBreak()', () => { - it('does not break short text', () => { - const text = lineBreak('hello world', fontPal, 90); - expect(text).equal('hello world'); - }); + describe('lineBreak()', () => { + it('does not break short text', () => { + const text = lineBreak('hello world', fontPal, 90); + expect(text).equal('hello world'); + }); - it('breaks into multiple lines', () => { - const text = lineBreak('this text is too long to fit', fontPal, 90); - expect(text).equal('this text is too\nlong to fit'); - }); + it('breaks into multiple lines', () => { + const text = lineBreak('this text is too long to fit', fontPal, 90); + expect(text).equal('this text is too\nlong to fit'); + }); - it('does not break single word', () => { - const text = lineBreak('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', fontPal, 90); - expect(text).equal('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); - }); - }); - }); + it('does not break single word', () => { + const text = lineBreak('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', fontPal, 90); + expect(text).equal('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); + }); + }); + }); }); diff --git a/src/ts/tests/client/webglUtils.spec.ts b/src/ts/tests/client/webglUtils.spec.ts index 6e5f61e..524e08d 100644 --- a/src/ts/tests/client/webglUtils.spec.ts +++ b/src/ts/tests/client/webglUtils.spec.ts @@ -5,45 +5,45 @@ import { getRenderTargetSize, isWebGL2, getWebGLContext } from '../../graphics/w import { WEBGL_CREATION_ERROR } from '../../common/errors'; describe('webglUtils', () => { - describe('getRenderTargetSize()', () => { - it('returns correct size for 150x200', () => { - expect(getRenderTargetSize(150, 200)).equal(256); - }); + describe('getRenderTargetSize()', () => { + it('returns correct size for 150x200', () => { + expect(getRenderTargetSize(150, 200)).equal(256); + }); - it('returns correct size for 256x512', () => { - expect(getRenderTargetSize(256, 512)).equal(512); - }); - }); + it('returns correct size for 256x512', () => { + expect(getRenderTargetSize(256, 512)).equal(512); + }); + }); - describe('getWebGLContext()', () => { - it('gets weblg2 context', () => { - const context = {} as any; + describe('getWebGLContext()', () => { + it('gets weblg2 context', () => { + const context = {} as any; - expect(getWebGLContext({ getContext: stub().withArgs('webgl2').returns(context) } as any)).equal(context); - }); + expect(getWebGLContext({ getContext: stub().withArgs('webgl2').returns(context) } as any)).equal(context); + }); - it('falls back to weblg context', () => { - const context = {} as any; + it('falls back to weblg context', () => { + const context = {} as any; - expect(getWebGLContext({ getContext: stub().withArgs('webgl').returns(context) } as any)).equal(context); - }); + expect(getWebGLContext({ getContext: stub().withArgs('webgl').returns(context) } as any)).equal(context); + }); - it('throws if context is not returned', () => { - expect(() => getWebGLContext({ getContext: stub().returns(undefined) } as any)).throw(WEBGL_CREATION_ERROR); - }); - }); + it('throws if context is not returned', () => { + expect(() => getWebGLContext({ getContext: stub().returns(undefined) } as any)).throw(WEBGL_CREATION_ERROR); + }); + }); - describe('isWebGL2()', () => { - it('returns true for webgl 2 context', () => { - expect(isWebGL2({ MAX_ELEMENT_INDEX: 1 } as any)).true; - }); + describe('isWebGL2()', () => { + it('returns true for webgl 2 context', () => { + expect(isWebGL2({ MAX_ELEMENT_INDEX: 1 } as any)).true; + }); - it('returns false for webgl 1 context', () => { - expect(isWebGL2({} as any)).false; - }); + it('returns false for webgl 1 context', () => { + expect(isWebGL2({} as any)).false; + }); - it('returns false for undefined', () => { - expect(isWebGL2(undefined)).false; - }); - }); + it('returns false for undefined', () => { + expect(isWebGL2(undefined)).false; + }); + }); }); diff --git a/src/ts/tests/client/worldMap.spec.ts b/src/ts/tests/client/worldMap.spec.ts index 1fdee5f..695fe7d 100644 --- a/src/ts/tests/client/worldMap.spec.ts +++ b/src/ts/tests/client/worldMap.spec.ts @@ -16,229 +16,229 @@ import { chatAnimationDuration } from '../../graphics/graphicsUtils'; import { handleUpdateEntity, handleSays } from '../../client/handlers'; describe('worldMap', () => { - describe('updateEntityInternal()', () => { - let game: PonyTownGame; - const def: DecodedUpdate = { - id: 0, x: 0, y: 0, vx: 0, vy: 0, state: 0, expression: 0, playerState: 0, switchRegion: false, - options: undefined, name: undefined, info: undefined, crc: undefined, type: undefined, action: undefined, - filterName: false, - }; + describe('updateEntityInternal()', () => { + let game: PonyTownGame; + const def: DecodedUpdate = { + id: 0, x: 0, y: 0, vx: 0, vy: 0, state: 0, expression: 0, playerState: 0, switchRegion: false, + options: undefined, name: undefined, info: undefined, crc: undefined, type: undefined, action: undefined, + filterName: false, + }; - beforeEach(() => { - game = {} as any; - game.map = createWorldMap({ type: 0, flags: 0, regionsX: 2, regionsY: 1, defaultTile: 0 }); - setRegion(game.map, 0, 0, createRegion(0, 0)); - setRegion(game.map, 1, 0, createRegion(1, 0)); - game.onActionsUpdate = new Subject(); - }); + beforeEach(() => { + game = {} as any; + game.map = createWorldMap({ type: 0, flags: 0, regionsX: 2, regionsY: 1, defaultTile: 0 }); + setRegion(game.map, 0, 0, createRegion(0, 0)); + setRegion(game.map, 1, 0, createRegion(1, 0)); + game.onActionsUpdate = new Subject(); + }); - it('updates x, y, vx, vy', () => { - const e = entity(123, 0, 0, 2); - addEntity(game.map, e); + it('updates x, y, vx, vy', () => { + const e = entity(123, 0, 0, 2); + addEntity(game.map, e); - handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5, state: 123, expression: 234 }); + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5, state: 123, expression: 234 }); - expect(e.x).equal(2); - expect(e.y).equal(3); - expect(e.vx).equal(4); - expect(e.vy).equal(5); - }); + expect(e.x).equal(2); + expect(e.y).equal(3); + expect(e.vx).equal(4); + expect(e.vy).equal(5); + }); - it('switches regions', () => { - const e = entity(123, 0, 0, 1) as Pony; - e.ponyState = defaultPonyState(); - addEntity(game.map, e); + it('switches regions', () => { + const e = entity(123, 0, 0, 1) as Pony; + e.ponyState = defaultPonyState(); + addEntity(game.map, e); - handleUpdateEntity(game, { ...def, id: 123, x: 12, y: 3, vx: 4, vy: 5, switchRegion: true }); + handleUpdateEntity(game, { ...def, id: 123, x: 12, y: 3, vx: 4, vy: 5, switchRegion: true }); - expect(getRegion(game.map, 1, 0)!.entities).includes(e); - }); + expect(getRegion(game.map, 1, 0)!.entities).includes(e); + }); - it('does not update x, y, vx, vy for player', () => { - const e = entity(123, 0, 0, 1) as Pony; - e.ponyState = defaultPonyState(); - addEntity(game.map, e); - game.playerId = e.id; - game.player = e as any; + it('does not update x, y, vx, vy for player', () => { + const e = entity(123, 0, 0, 1) as Pony; + e.ponyState = defaultPonyState(); + addEntity(game.map, e); + game.playerId = e.id; + game.player = e as any; - handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); - expect(e.x).equal(0); - expect(e.y).equal(0); - expect(e.vx).equal(0); - expect(e.vy).equal(0); - }); + expect(e.x).equal(0); + expect(e.y).equal(0); + expect(e.vx).equal(0); + expect(e.vy).equal(0); + }); - it('updates flags', () => { - const e = entity(123, 0, 0, 1) as Pony; - e.ponyState = defaultPonyState(); - addEntity(game.map, e); + it('updates flags', () => { + const e = entity(123, 0, 0, 1) as Pony; + e.ponyState = defaultPonyState(); + addEntity(game.map, e); - handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5, state: 123 }); + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5, state: 123 }); - expect(e.state).equal(123); - }); + expect(e.state).equal(123); + }); - it('updates expression', () => { - const e = entity(123, 0, 0, 1) as Pony; - e.expr = 0; - e.ponyState = defaultPonyState(); - addEntity(game.map, e); + it('updates expression', () => { + const e = entity(123, 0, 0, 1) as Pony; + e.expr = 0; + e.ponyState = defaultPonyState(); + addEntity(game.map, e); - handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5, expression: 234 }); + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5, expression: 234 }); - expect(e.expr).equal(234); - }); + expect(e.expr).equal(234); + }); - it('overrides right flag for player', () => { - const player = entity(123, 0, 0, 1); - game.player = player as any; - game.player!.ponyState = defaultPonyState(); - game.rightOverride = true; - addEntity(game.map, player); + it('overrides right flag for player', () => { + const player = entity(123, 0, 0, 1); + game.player = player as any; + game.player!.ponyState = defaultPonyState(); + game.rightOverride = true; + addEntity(game.map, player); - handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); - expect(player.state).equal(EntityState.FacingRight); - }); + expect(player.state).equal(EntityState.FacingRight); + }); - it('overrides headTurned flag for player', () => { - const player = entity(123, 0, 0, 1); - game.player = player as any; - game.player!.ponyState = defaultPonyState(); - game.headTurnedOverride = true; - addEntity(game.map, player); + it('overrides headTurned flag for player', () => { + const player = entity(123, 0, 0, 1); + game.player = player as any; + game.player!.ponyState = defaultPonyState(); + game.headTurnedOverride = true; + addEntity(game.map, player); - handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); - expect(player.state).equal(EntityState.HeadTurned); - }); + expect(player.state).equal(EntityState.HeadTurned); + }); - it('overrides sitting flag for player', () => { - const player = entity(123, 0, 0, 1); - game.player = player as any; - game.player!.ponyState = defaultPonyState(); - game.stateOverride = EntityState.PonySitting; - addEntity(game.map, player); + it('overrides sitting flag for player', () => { + const player = entity(123, 0, 0, 1); + game.player = player as any; + game.player!.ponyState = defaultPonyState(); + game.stateOverride = EntityState.PonySitting; + addEntity(game.map, player); - handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); + handleUpdateEntity(game, { ...def, id: 123, x: 2, y: 3, vx: 4, vy: 5 }); - expect(player.state).equal(EntityState.PonySitting); - }); - }); + expect(player.state).equal(EntityState.PonySitting); + }); + }); - describe('handleSays()', () => { - let e: Entity; - let clock: SinonFakeTimers; - let game: PonyTownGame; + describe('handleSays()', () => { + let e: Entity; + let clock: SinonFakeTimers; + let game: PonyTownGame; - beforeEach(() => { - game = mock(PonyTownGame); - e = entity(1, 1, 1, 2, { bounds: rect(0, 0, 10, 10) }); - (e as Pony).animator = createAnimator(); - (e as Pony).ponyState = defaultPonyState(); - game.map = createWorldMap({ type: 0, flags: 0, regionsX: 1, regionsY: 1, defaultTile: TileType.None }); - setRegion(game.map, 0, 0, createRegion(0, 0)); - addEntity(game.map, e); - game.camera = createCamera(); - game.settings = { account: {} } as any; - game.messageQueue = []; - game.findEntityFromChatLog = () => undefined; - (game as any).model = { friends: [] }; - clock = useFakeTimers(); - }); + beforeEach(() => { + game = mock(PonyTownGame); + e = entity(1, 1, 1, 2, { bounds: rect(0, 0, 10, 10) }); + (e as Pony).animator = createAnimator(); + (e as Pony).ponyState = defaultPonyState(); + game.map = createWorldMap({ type: 0, flags: 0, regionsX: 1, regionsY: 1, defaultTile: TileType.None }); + setRegion(game.map, 0, 0, createRegion(0, 0)); + addEntity(game.map, e); + game.camera = createCamera(); + game.settings = { account: {} } as any; + game.messageQueue = []; + game.findEntityFromChatLog = () => undefined; + (game as any).model = { friends: [] }; + clock = useFakeTimers(); + }); - afterEach(() => { - clock.restore(); - }); + afterEach(() => { + clock.restore(); + }); - it('adds says object to entity', () => { - handleSays(game, 1, 'test', MessageType.Chat); + it('adds says object to entity', () => { + handleSays(game, 1, 'test', MessageType.Chat); - expect(e.says).eql({ message: 'test', timer: 5.1875, total: 5.1875, type: MessageType.Chat, created: 0 }); - }); + expect(e.says).eql({ message: 'test', timer: 5.1875, total: 5.1875, type: MessageType.Chat, created: 0 }); + }); - // it('does nothing if entity is not on the map', () => { - // game.map.removeEntity(1); + // it('does nothing if entity is not on the map', () => { + // game.map.removeEntity(1); - // handleSays(game, 1, 'test', MessageType.Chat); + // handleSays(game, 1, 'test', MessageType.Chat); - // expect(e.says).undefined; - // }); + // expect(e.says).undefined; + // }); - it('does nothing if entity is not visible', () => { - e.x = 1000; + it('does nothing if entity is not visible', () => { + e.x = 1000; - handleSays(game, 1, 'test', MessageType.Chat); + handleSays(game, 1, 'test', MessageType.Chat); - expect(e.says).undefined; - }); + expect(e.says).undefined; + }); - // it('does nothing if entity is ignored', () => { - // e.playerState = setFlag(e.playerState, EntityPlayerState.Ignored, true); + // it('does nothing if entity is ignored', () => { + // e.playerState = setFlag(e.playerState, EntityPlayerState.Ignored, true); - // handleSays(game, 1, 'test', MessageType.Chat); + // handleSays(game, 1, 'test', MessageType.Chat); - // expect(e.says).undefined; - // }); + // expect(e.says).undefined; + // }); - it('emits chat message', () => { - e.name = 'foo'; - e.type = pony.type; - e.crc = 123; + it('emits chat message', () => { + e.name = 'foo'; + e.type = pony.type; + e.crc = 123; - handleSays(game, 1, 'test', MessageType.Chat); + handleSays(game, 1, 'test', MessageType.Chat); - expect(game.messageQueue).eql([ - { id: 1, name: 'foo', crc: 123, message: 'test', type: MessageType.Chat }, - ]); - }); + expect(game.messageQueue).eql([ + { id: 1, name: 'foo', crc: 123, message: 'test', type: MessageType.Chat }, + ]); + }); - it('emits chat message for party message even if entity is not visible', () => { - e.name = 'foo'; - e.type = pony.type; - e.x = 1000; - e.crc = 123; + it('emits chat message for party message even if entity is not visible', () => { + e.name = 'foo'; + e.type = pony.type; + e.x = 1000; + e.crc = 123; - handleSays(game, 1, 'test', MessageType.Party); + handleSays(game, 1, 'test', MessageType.Party); - expect(game.messageQueue).eql([ - { id: 1, name: 'foo', crc: 123, message: 'test', type: MessageType.Party }, - ]); - }); + expect(game.messageQueue).eql([ + { id: 1, name: 'foo', crc: 123, message: 'test', type: MessageType.Party }, + ]); + }); - it('does nothing if entity is not visible', () => { - e.type = pony.type; - e.x = 1000; + it('does nothing if entity is not visible', () => { + e.type = pony.type; + e.x = 1000; - handleSays(game, 1, 'test', MessageType.Chat); + handleSays(game, 1, 'test', MessageType.Chat); - expect(game.messageQueue).eql([]); - }); + expect(game.messageQueue).eql([]); + }); - it('does not call game.onMessage for non-pony entities', () => { - e.type = apple.type; + it('does not call game.onMessage for non-pony entities', () => { + e.type = apple.type; - handleSays(game, 1, 'test', MessageType.Chat); + handleSays(game, 1, 'test', MessageType.Chat); - expect(game.messageQueue).eql([]); - }); + expect(game.messageQueue).eql([]); + }); - it('does not call game.onMessage for "." message', () => { - e.type = pony.type; + it('does not call game.onMessage for "." message', () => { + e.type = pony.type; - handleSays(game, 1, '.', MessageType.Chat); + handleSays(game, 1, '.', MessageType.Chat); - expect(game.messageQueue).eql([]); - }); + expect(game.messageQueue).eql([]); + }); - it('dismisses previous message for "." message', () => { - e.type = pony.type; - handleSays(game, 1, 'test', MessageType.Chat); + it('dismisses previous message for "." message', () => { + e.type = pony.type; + handleSays(game, 1, 'test', MessageType.Chat); - handleSays(game, 1, '.', MessageType.Chat); + handleSays(game, 1, '.', MessageType.Chat); - expect(e.says!.timer).equal(chatAnimationDuration); - }); - }); + expect(e.says!.timer).equal(chatAnimationDuration); + }); + }); }); diff --git a/src/ts/tests/common/accountUtils.spec.ts b/src/ts/tests/common/accountUtils.spec.ts index 4295751..b632940 100644 --- a/src/ts/tests/common/accountUtils.spec.ts +++ b/src/ts/tests/common/accountUtils.spec.ts @@ -5,149 +5,149 @@ import { account } from '../mocks'; import { SupporterFlags } from '../../common/adminInterfaces'; describe('accountUtils [client]', () => { - describe('isAdmin()', () => { - it('returns true if target account has admin role', () => { - expect(isAdmin(account({ roles: ['admin'] }))).true; - }); + describe('isAdmin()', () => { + it('returns true if target account has admin role', () => { + expect(isAdmin(account({ roles: ['admin'] }))).true; + }); - it('returns true if target account has superadmin role', () => { - expect(isAdmin(account({ roles: ['superadmin'] }))).true; - }); + it('returns true if target account has superadmin role', () => { + expect(isAdmin(account({ roles: ['superadmin'] }))).true; + }); - it('returns false if target account has no roles', () => { - expect(isAdmin(account({}))).false; - }); + it('returns false if target account has no roles', () => { + expect(isAdmin(account({}))).false; + }); - it('returns false if target account has no admin or superadmin roles', () => { - expect(isAdmin(account({ roles: ['foo'] }))).false; - }); - }); + it('returns false if target account has no admin or superadmin roles', () => { + expect(isAdmin(account({ roles: ['foo'] }))).false; + }); + }); - describe('isMod()', () => { - it('returns true if target account has mod role', () => { - expect(isMod(account({ roles: ['mod'] }))).true; - }); + describe('isMod()', () => { + it('returns true if target account has mod role', () => { + expect(isMod(account({ roles: ['mod'] }))).true; + }); - it('returns true if target account has admin role', () => { - expect(isMod(account({ roles: ['admin'] }))).true; - }); + it('returns true if target account has admin role', () => { + expect(isMod(account({ roles: ['admin'] }))).true; + }); - it('returns true if target account has superadmin role', () => { - expect(isMod(account({ roles: ['superadmin'] }))).true; - }); + it('returns true if target account has superadmin role', () => { + expect(isMod(account({ roles: ['superadmin'] }))).true; + }); - it('returns false if target account has no roles', () => { - expect(isMod(account({}))).false; - }); + it('returns false if target account has no roles', () => { + expect(isMod(account({}))).false; + }); - it('returns false if target account has no admin or superadmin roles', () => { - expect(isMod(account({ roles: ['foo'] }))).false; - }); - }); + it('returns false if target account has no admin or superadmin roles', () => { + expect(isMod(account({ roles: ['foo'] }))).false; + }); + }); - describe('isDev()', () => { - it('returns true if target account has dev role', () => { - expect(isDev(account({ roles: ['dev'] }))).true; - }); + describe('isDev()', () => { + it('returns true if target account has dev role', () => { + expect(isDev(account({ roles: ['dev'] }))).true; + }); - it('returns false if target account has no roles', () => { - expect(isDev(account({}))).false; - }); + it('returns false if target account has no roles', () => { + expect(isDev(account({}))).false; + }); - it('returns false if target account has no dev role', () => { - expect(isDev(account({ roles: ['foo'] }))).false; - }); - }); + it('returns false if target account has no dev role', () => { + expect(isDev(account({ roles: ['foo'] }))).false; + }); + }); - describe('meetsRequirement()', () => { - it('returns true for undefined requirement', () => { - expect(meetsRequirement({}, undefined)).true; - }); + describe('meetsRequirement()', () => { + it('returns true for undefined requirement', () => { + expect(meetsRequirement({}, undefined)).true; + }); - it('returns true for empty requirement', () => { - expect(meetsRequirement({}, '')).true; - }); + it('returns true for empty requirement', () => { + expect(meetsRequirement({}, '')).true; + }); - it('returns true if requirement matches role', () => { - expect(meetsRequirement({ roles: ['mod'] }, 'mod')).true; - }); + it('returns true if requirement matches role', () => { + expect(meetsRequirement({ roles: ['mod'] }, 'mod')).true; + }); - it('returns true if matches supporter 1 requirement', () => { - expect(meetsRequirement({ supporter: SupporterFlags.Supporter1 }, 'sup1')).true; - }); + it('returns true if matches supporter 1 requirement', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter1 }, 'sup1')).true; + }); - it('returns true if matches supporter 2 requirement', () => { - expect(meetsRequirement({ supporter: SupporterFlags.Supporter2 }, 'sup2')).true; - }); + it('returns true if matches supporter 2 requirement', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter2 }, 'sup2')).true; + }); - it('returns true if matches supporter 3 requirement', () => { - expect(meetsRequirement({ supporter: SupporterFlags.Supporter3 }, 'sup3')).true; - }); + it('returns true if matches supporter 3 requirement', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter3 }, 'sup3')).true; + }); - it('returns false if supporter is lower level', () => { - expect(meetsRequirement({ supporter: SupporterFlags.Supporter1 }, 'sup2')).false; - }); + it('returns false if supporter is lower level', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter1 }, 'sup2')).false; + }); - it('returns true if requires supporter but is mod', () => { - expect(meetsRequirement({ roles: ['mod'] }, 'sup2')).true; - }); + it('returns true if requires supporter but is mod', () => { + expect(meetsRequirement({ roles: ['mod'] }, 'sup2')).true; + }); - it('returns true if requires supporter but is dev', () => { - expect(meetsRequirement({ roles: ['dev'] }, 'sup2')).true; - }); + it('returns true if requires supporter but is dev', () => { + expect(meetsRequirement({ roles: ['dev'] }, 'sup2')).true; + }); - it('returns false if supporter is undefined', () => { - expect(meetsRequirement({}, 'sup2')).false; - }); + it('returns false if supporter is undefined', () => { + expect(meetsRequirement({}, 'sup2')).false; + }); - it('returns false if requirement is not met', () => { - expect(meetsRequirement({}, 'mod')).false; - }); + it('returns false if requirement is not met', () => { + expect(meetsRequirement({}, 'mod')).false; + }); - it('returns false if sup2 requirement is not met', () => { - expect(meetsRequirement({ supporter: 0 }, 'sup2')).false; - }); + it('returns false if sup2 requirement is not met', () => { + expect(meetsRequirement({ supporter: 0 }, 'sup2')).false; + }); - it('returns false if inv requirement is not met', () => { - expect(meetsRequirement({}, 'inv')).false; - }); + it('returns false if inv requirement is not met', () => { + expect(meetsRequirement({}, 'inv')).false; + }); - it('returns true if inv requirement is met (supporter)', () => { - expect(meetsRequirement({ supporter: SupporterFlags.Supporter1 }, 'inv')).true; - }); + it('returns true if inv requirement is met (supporter)', () => { + expect(meetsRequirement({ supporter: SupporterFlags.Supporter1 }, 'inv')).true; + }); - it('returns true if inv requirement is met (role)', () => { - expect(meetsRequirement({ roles: ['dev'] }, 'inv')).true; - }); + it('returns true if inv requirement is met (role)', () => { + expect(meetsRequirement({ roles: ['dev'] }, 'inv')).true; + }); - it('returns true if inv requirement is met (invited)', () => { - expect(meetsRequirement({ supporterInvited: true }, 'inv')).true; - }); - }); + it('returns true if inv requirement is met (invited)', () => { + expect(meetsRequirement({ supporterInvited: true }, 'inv')).true; + }); + }); - describe('getSupporterInviteLimit()', () => { - it('returns 100 for mod', () => { - expect(getSupporterInviteLimit({ roles: ['mod'] })).equal(100); - }); + describe('getSupporterInviteLimit()', () => { + it('returns 100 for mod', () => { + expect(getSupporterInviteLimit({ roles: ['mod'] })).equal(100); + }); - it('returns 100 for dev', () => { - expect(getSupporterInviteLimit({ roles: ['dev'] })).equal(100); - }); + it('returns 100 for dev', () => { + expect(getSupporterInviteLimit({ roles: ['dev'] })).equal(100); + }); - it('returns 1 for supporter level 1', () => { - expect(getSupporterInviteLimit({ supporter: 1 })).equal(1); - }); + it('returns 1 for supporter level 1', () => { + expect(getSupporterInviteLimit({ supporter: 1 })).equal(1); + }); - it('returns 5 for supporter level 2', () => { - expect(getSupporterInviteLimit({ supporter: 2 })).equal(5); - }); + it('returns 5 for supporter level 2', () => { + expect(getSupporterInviteLimit({ supporter: 2 })).equal(5); + }); - it('returns 10 for supporter level 3', () => { - expect(getSupporterInviteLimit({ supporter: 3 })).equal(10); - }); + it('returns 10 for supporter level 3', () => { + expect(getSupporterInviteLimit({ supporter: 3 })).equal(10); + }); - it('returns 0 otherwise', () => { - expect(getSupporterInviteLimit({})).equal(0); - }); - }); + it('returns 0 otherwise', () => { + expect(getSupporterInviteLimit({})).equal(0); + }); + }); }); diff --git a/src/ts/tests/common/adminUtils.spec.ts b/src/ts/tests/common/adminUtils.spec.ts index 1fc039f..fee075c 100644 --- a/src/ts/tests/common/adminUtils.spec.ts +++ b/src/ts/tests/common/adminUtils.spec.ts @@ -6,120 +6,120 @@ import { supporterLevel, isMuted, isActive, pushOrdered, compareByName } from '. import { account } from '../mocks'; describe('adminUtils', () => { - describe('pushOrdered()', () => { - it('pushes one item to empty array', () => { - const items: ({ name: string; })[] = []; + describe('pushOrdered()', () => { + it('pushes one item to empty array', () => { + const items: ({ name: string; })[] = []; - pushOrdered(items, { name: 'foo' }, compareByName); + pushOrdered(items, { name: 'foo' }, compareByName); - expect(items).eql([{ name: 'foo' }]); - }); + expect(items).eql([{ name: 'foo' }]); + }); - it('pushes item after existing one', () => { - const items = [{ name: 'a' }]; + it('pushes item after existing one', () => { + const items = [{ name: 'a' }]; - pushOrdered(items, { name: 'b' }, compareByName); + pushOrdered(items, { name: 'b' }, compareByName); - expect(items).eql([{ name: 'a' }, { name: 'b' }]); - }); + expect(items).eql([{ name: 'a' }, { name: 'b' }]); + }); - it('pushes item before existing one', () => { - const items = [{ name: 'b' }]; + it('pushes item before existing one', () => { + const items = [{ name: 'b' }]; - pushOrdered(items, { name: 'a' }, compareByName); + pushOrdered(items, { name: 'a' }, compareByName); - expect(items).eql([{ name: 'a' }, { name: 'b' }]); - }); + expect(items).eql([{ name: 'a' }, { name: 'b' }]); + }); - it('pushes item between existing ones', () => { - const items = [{ name: 'a' }, { name: 'c' }]; + it('pushes item between existing ones', () => { + const items = [{ name: 'a' }, { name: 'c' }]; - pushOrdered(items, { name: 'b' }, compareByName); + pushOrdered(items, { name: 'b' }, compareByName); - expect(items).eql([{ name: 'a' }, { name: 'b' }, { name: 'c' }]); - }); - }); + expect(items).eql([{ name: 'a' }, { name: 'b' }, { name: 'c' }]); + }); + }); - describe('supporterLevel()', () => { - it('returns 1 if is supporter on patreon', () => { - expect(supporterLevel(account({ patreon: PatreonFlags.Supporter1 }))).equal(1); - }); + describe('supporterLevel()', () => { + it('returns 1 if is supporter on patreon', () => { + expect(supporterLevel(account({ patreon: PatreonFlags.Supporter1 }))).equal(1); + }); - it('returns 1 if has supporter flag', () => { - expect(supporterLevel(account({ supporter: SupporterFlags.Supporter1 }))).equal(1); - }); + it('returns 1 if has supporter flag', () => { + expect(supporterLevel(account({ supporter: SupporterFlags.Supporter1 }))).equal(1); + }); - it('returns max level if has supporter flag and patreon', () => { - expect(supporterLevel(account({ - patreon: PatreonFlags.Supporter3, - supporter: SupporterFlags.Supporter1 - }))).equal(3); - }); + it('returns max level if has supporter flag and patreon', () => { + expect(supporterLevel(account({ + patreon: PatreonFlags.Supporter3, + supporter: SupporterFlags.Supporter1 + }))).equal(3); + }); - it('returns 0 if patreon info is empty', () => { - expect(supporterLevel(account({}))).equal(0); - }); + it('returns 0 if patreon info is empty', () => { + expect(supporterLevel(account({}))).equal(0); + }); - it('returns 0 if has patreon info but has ignore flag set', () => { - expect(supporterLevel(account({ - patreon: PatreonFlags.Supporter1, - supporter: SupporterFlags.IgnorePatreon, - }))).equal(0); - }); - }); + it('returns 0 if has patreon info but has ignore flag set', () => { + expect(supporterLevel(account({ + patreon: PatreonFlags.Supporter1, + supporter: SupporterFlags.IgnorePatreon, + }))).equal(0); + }); + }); - describe('isMuted()', () => { - it('returns true if account has muted flag', () => { - expect(isMuted(account({ mute: -1 }))).true; - }); + describe('isMuted()', () => { + it('returns true if account has muted flag', () => { + expect(isMuted(account({ mute: -1 }))).true; + }); - it('returns true if account has timeout after current date', () => { - expect(isMuted(account({ mute: Date.now() + 10000 }))).true; - }); + it('returns true if account has timeout after current date', () => { + expect(isMuted(account({ mute: Date.now() + 10000 }))).true; + }); - it('returns false if account has timeout before current date', () => { - expect(isMuted(account({ mute: Date.now() - 10000 }))).false; - }); + it('returns false if account has timeout before current date', () => { + expect(isMuted(account({ mute: Date.now() - 10000 }))).false; + }); - it('returns false if account has not timeout, mute or shadow', () => { - expect(isMuted(account({}))).false; - }); - }); + it('returns false if account has not timeout, mute or shadow', () => { + expect(isMuted(account({}))).false; + }); + }); - describe('isActive()', () => { - let clock: SinonFakeTimers; + describe('isActive()', () => { + let clock: SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers(); - }); + beforeEach(() => { + clock = useFakeTimers(); + }); - afterEach(() => { - clock.restore(); - clock = undefined as any; - }); + afterEach(() => { + clock.restore(); + clock = undefined as any; + }); - it('returns false for undefined', () => { - expect(isActive(undefined)).false; - }); + it('returns false for undefined', () => { + expect(isActive(undefined)).false; + }); - it('returns false for 0', () => { - expect(isActive(0)).false; - }); + it('returns false for 0', () => { + expect(isActive(0)).false; + }); - it('returns true for -1', () => { - expect(isActive(-1)).true; - }); + it('returns true for -1', () => { + expect(isActive(-1)).true; + }); - it('returns true for time larger than current time', () => { - clock.setSystemTime(2000); + it('returns true for time larger than current time', () => { + clock.setSystemTime(2000); - expect(isActive(3000)).true; - }); + expect(isActive(3000)).true; + }); - it('returns false for time smaller than current time', () => { - clock.setSystemTime(3000); + it('returns false for time smaller than current time', () => { + clock.setSystemTime(3000); - expect(isActive(2000)).false; - }); - }); + expect(isActive(2000)).false; + }); + }); }); diff --git a/src/ts/tests/common/animator.spec.ts b/src/ts/tests/common/animator.spec.ts index f16edd3..681126c 100644 --- a/src/ts/tests/common/animator.spec.ts +++ b/src/ts/tests/common/animator.spec.ts @@ -1,8 +1,8 @@ import '../lib'; import { expect } from 'chai'; import { - Animator, animatorState, anyState, animatorTransition, getAnimation, getAnimationFrame, setAnimatorState, - updateAnimator, createAnimator + Animator, animatorState, anyState, animatorTransition, getAnimation, getAnimationFrame, setAnimatorState, + updateAnimator, createAnimator } from '../../common/animator'; const sit = { name: 'sit', fps: 6, loop: true, frames: { length: 12 } as any }; @@ -11,374 +11,374 @@ const stand = { name: 'stand', fps: 6, loop: true, frames: { length: 6 } as any const run = { name: 'run', fps: 6, loop: true, frames: { length: 6 } as any }; describe('Animator', () => { - let animator: Animator; - - beforeEach(() => { - animator = createAnimator(); - }); - - after(() => { - animator = undefined as any; - }); - - it('returns undefined animation by default', () => { - expect(getAnimation(animator)).undefined; - }); - - it('returns 0 frame by default', () => { - expect(getAnimationFrame(animator)).equal(0); - }); - - it('sets initial state', () => { - const sitting = animatorState('sitting', sit); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - - expect(animator.state).equal(sitting); - expect(getAnimation(animator)).equal(sit); - }); - - it('does not switch to new state immediately', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running); - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - - expect(getAnimation(animator)).equal(sit); - - setAnimatorState(animator, running); - - expect(getAnimation(animator)).equal(sit); - }); - - it('updates animation frame', () => { - const sitting = animatorState('sitting', sit); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0); - updateAnimator(animator, 1); - - expect(getAnimationFrame(animator)).equal(6); - }); - - it('loops animation frame', () => { - const state = animatorState('sitting', sit); - - setAnimatorState(animator, state); - updateAnimator(animator, 0); - updateAnimator(animator, 3); - - expect(getAnimationFrame(animator)).equal(6); - }); - - it('switches to next state immediately if exitAfter is set to 0', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running, { exitAfter: 0 }); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - updateAnimator(animator, 0.1); - - expect(getAnimation(animator)).equal(run); - }); - - it('switches to next state after half of the animation if exitAfter is set 0.5', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running, { exitAfter: 0.5 }); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0); - setAnimatorState(animator, running); - updateAnimator(animator, 0); - updateAnimator(animator, 0.1); - expect(getAnimation(animator)).equal(sit, 'after 0.1s'); - updateAnimator(animator, 0.4); - expect(getAnimation(animator)).equal(sit, 'after 0.5s'); - updateAnimator(animator, 0.6); - expect(getAnimation(animator)).equal(run, 'after 1.1s'); - }); - - it('switches to next state immediately if exitAfter is set 0.5 and time is already past 0.5', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running, { exitAfter: 0.5 }); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0); - updateAnimator(animator, 1.1); - setAnimatorState(animator, running); - updateAnimator(animator, 0); - expect(getAnimation(animator)).equal(run); - }); - - it('switches to middle of next state if enterTime is set to 0.5', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running, { exitAfter: 0, enterTime: 0.5 }); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0); - setAnimatorState(animator, running); - updateAnimator(animator, 0); - - expect(getAnimation(animator)).equal(run); - expect(getAnimationFrame(animator)).equal(3); - }); - - it('keeps time of animation after switching if keepTime is set', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running, { exitAfter: 0, keepTime: true }); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0); - updateAnimator(animator, 0.5); - - expect(getAnimationFrame(animator)).equal(3, 'before'); - - setAnimatorState(animator, running); - updateAnimator(animator, 0); - - expect(getAnimation(animator)).equal(run); - expect(getAnimationFrame(animator)).equal(3, 'after'); - }); - - it('switches to state if set to switch from any', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running, { exitAfter: 0 }); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - updateAnimator(animator, 0.1); - - expect(getAnimation(animator)).equal(run); - }); - - it('does not use switch from any state as intermediate transition', () => { - const lying = animatorState('lying', sit); - const sitting = animatorState('sitting', sit); - const standing = animatorState('standing', sit); - const flying = animatorState('flying', sit); - const hovering = animatorState('hovering', sit); - animatorTransition(lying, sitting); - animatorTransition(sitting, standing); - animatorTransition(anyState, flying); - animatorTransition(flying, hovering); - animatorTransition(standing, hovering); - - setAnimatorState(animator, lying); - updateAnimator(animator, 0); - setAnimatorState(animator, hovering); - updateAnimator(animator, 3); - expect(animator.state).equal(sitting); - updateAnimator(animator, 3); - expect(animator.state).equal(standing); - updateAnimator(animator, 3); - expect(animator.state).equal(hovering); - }); - - it('does not use switch from any state is another transition is possible', () => { - const lying = animatorState('lying', sit); - const standing = animatorState('standing', sit); - const hovering = animatorState('hovering', sit); - animatorTransition(lying, standing); - animatorTransition(anyState, hovering); - animatorTransition(standing, hovering); - - setAnimatorState(animator, lying); - updateAnimator(animator, 0); - setAnimatorState(animator, hovering); - updateAnimator(animator, 3); - expect(animator.state).equal(standing); - updateAnimator(animator, 3); - expect(animator.state).equal(hovering); - }); - - it('uses switch from any state is another transition is possible but longer than 2 jumps', () => { - const lying = animatorState('lying', sit); - const sitting = animatorState('sitting', sit); - const standing = animatorState('standing', sit); - const hovering = animatorState('hovering', sit); - animatorTransition(lying, sitting); - animatorTransition(sitting, standing); - animatorTransition(anyState, hovering); - animatorTransition(standing, hovering); - - setAnimatorState(animator, lying); - updateAnimator(animator, 0); - setAnimatorState(animator, hovering); - updateAnimator(animator, 3); - expect(animator.state).equal(hovering); - }); - - it('does not switch to next state immediately if exitNow is not set', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - updateAnimator(animator, 0.1); - - expect(getAnimation(animator)).equal(sit); - }); - - it('switches to next state when animation is finished', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - updateAnimator(animator, 3); - - expect(getAnimation(animator)).equal(run); - expect(getAnimationFrame(animator)).equal(0); - }); - - it('does nothing if switching to already targeted state', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - setAnimatorState(animator, running); - updateAnimator(animator, 3); - - expect(getAnimation(animator)).equal(run); - }); - - it('does nothing if transition is no possible', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - updateAnimator(animator, 3); - - expect(getAnimation(animator)).equal(sit); - }); - - it('switches through intermediate state', () => { - const sitting = animatorState('sitting', sit); - const other = animatorState('other', sit); - const standing = animatorState('standing', stand); - const running = animatorState('running', run); - animatorTransition(sitting, standing); - animatorTransition(other, running); - animatorTransition(standing, running); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - updateAnimator(animator, 3); - - expect(getAnimation(animator)).equal(stand); - - updateAnimator(animator, 3); - - expect(getAnimation(animator)).equal(run); - }); - - it('switches directly to target state if exitNow is set on both states', () => { - const sitting = animatorState('sitting', sit); - const standing = animatorState('standing', stand); - const running = animatorState('running', run); - animatorTransition(sitting, standing, { exitAfter: 0 }); - animatorTransition(standing, running, { exitAfter: 0 }); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - updateAnimator(animator, 0.1); - - expect(getAnimation(animator)).equal(run); - }); - - it('cancells switch to target state', () => { - const sitting = animatorState('sitting', sit); - const running = animatorState('running', run); - animatorTransition(sitting, running); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - updateAnimator(animator, 0.1); - setAnimatorState(animator, sitting); - updateAnimator(animator, 3); - - expect(getAnimation(animator)).equal(sit); - }); - - it('handles loops in states', () => { - const sitting = animatorState('sitting', sit); - const standing0 = animatorState('standing0', sit); - const standing1 = animatorState('standing1', stand); - const standing2 = animatorState('standing2', stand); - const running = animatorState('running', run); - animatorTransition(sitting, standing0, { exitAfter: 0 }); - animatorTransition(running, standing1, { exitAfter: 0 }); - animatorTransition(running, standing2, { exitAfter: 0 }); - animatorTransition(standing0, standing2, { exitAfter: 0 }); - animatorTransition(standing1, running, { exitAfter: 0 }); - animatorTransition(standing2, running, { exitAfter: 0 }); - - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - setAnimatorState(animator, running); - updateAnimator(animator, 0.1); - - expect(getAnimation(animator)).equal(run); - }); - - it('returns correct animation variant', () => { - const sitting = animatorState('sitting', sit, { alt: sit2 }); - - animator.variant = 'alt'; - setAnimatorState(animator, sitting); - updateAnimator(animator, 0.1); - - expect(getAnimation(animator)).equal(sit2); - }); - - it('does not use shorter transition route if onlyDirectTo is set', () => { - const standing = animatorState('standing', run); - const standingUp = animatorState('standing-up', run); - const sitting = animatorState('sitting', run); - const sittingUp = animatorState('sitting-up', run); - const lying = animatorState('lying', run); - const lyingToTrotting = animatorState('lying-to-trotting', run); - const trotting = animatorState('trotting', run); - animatorTransition(lying, sittingUp); - animatorTransition(sittingUp, sitting); - animatorTransition(sitting, standingUp); - animatorTransition(standingUp, standing); - animatorTransition(lying, lyingToTrotting, { onlyDirectTo: trotting }); - animatorTransition(lyingToTrotting, standing); - animatorTransition(lyingToTrotting, trotting); - - setAnimatorState(animator, lying); - updateAnimator(animator, 0); - setAnimatorState(animator, standing); - updateAnimator(animator, 1.1); - expect(animator.state).equal(sittingUp); - updateAnimator(animator, 1.1); - expect(animator.state).equal(sitting); - updateAnimator(animator, 1.1); - expect(animator.state).equal(standingUp); - updateAnimator(animator, 1.1); - expect(animator.state).equal(standing); - }); + let animator: Animator; + + beforeEach(() => { + animator = createAnimator(); + }); + + after(() => { + animator = undefined as any; + }); + + it('returns undefined animation by default', () => { + expect(getAnimation(animator)).undefined; + }); + + it('returns 0 frame by default', () => { + expect(getAnimationFrame(animator)).equal(0); + }); + + it('sets initial state', () => { + const sitting = animatorState('sitting', sit); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + + expect(animator.state).equal(sitting); + expect(getAnimation(animator)).equal(sit); + }); + + it('does not switch to new state immediately', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(sit); + + setAnimatorState(animator, running); + + expect(getAnimation(animator)).equal(sit); + }); + + it('updates animation frame', () => { + const sitting = animatorState('sitting', sit); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + updateAnimator(animator, 1); + + expect(getAnimationFrame(animator)).equal(6); + }); + + it('loops animation frame', () => { + const state = animatorState('sitting', sit); + + setAnimatorState(animator, state); + updateAnimator(animator, 0); + updateAnimator(animator, 3); + + expect(getAnimationFrame(animator)).equal(6); + }); + + it('switches to next state immediately if exitAfter is set to 0', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(run); + }); + + it('switches to next state after half of the animation if exitAfter is set 0.5', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0.5 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + setAnimatorState(animator, running); + updateAnimator(animator, 0); + updateAnimator(animator, 0.1); + expect(getAnimation(animator)).equal(sit, 'after 0.1s'); + updateAnimator(animator, 0.4); + expect(getAnimation(animator)).equal(sit, 'after 0.5s'); + updateAnimator(animator, 0.6); + expect(getAnimation(animator)).equal(run, 'after 1.1s'); + }); + + it('switches to next state immediately if exitAfter is set 0.5 and time is already past 0.5', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0.5 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + updateAnimator(animator, 1.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0); + expect(getAnimation(animator)).equal(run); + }); + + it('switches to middle of next state if enterTime is set to 0.5', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0, enterTime: 0.5 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + setAnimatorState(animator, running); + updateAnimator(animator, 0); + + expect(getAnimation(animator)).equal(run); + expect(getAnimationFrame(animator)).equal(3); + }); + + it('keeps time of animation after switching if keepTime is set', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0, keepTime: true }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0); + updateAnimator(animator, 0.5); + + expect(getAnimationFrame(animator)).equal(3, 'before'); + + setAnimatorState(animator, running); + updateAnimator(animator, 0); + + expect(getAnimation(animator)).equal(run); + expect(getAnimationFrame(animator)).equal(3, 'after'); + }); + + it('switches to state if set to switch from any', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running, { exitAfter: 0 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(run); + }); + + it('does not use switch from any state as intermediate transition', () => { + const lying = animatorState('lying', sit); + const sitting = animatorState('sitting', sit); + const standing = animatorState('standing', sit); + const flying = animatorState('flying', sit); + const hovering = animatorState('hovering', sit); + animatorTransition(lying, sitting); + animatorTransition(sitting, standing); + animatorTransition(anyState, flying); + animatorTransition(flying, hovering); + animatorTransition(standing, hovering); + + setAnimatorState(animator, lying); + updateAnimator(animator, 0); + setAnimatorState(animator, hovering); + updateAnimator(animator, 3); + expect(animator.state).equal(sitting); + updateAnimator(animator, 3); + expect(animator.state).equal(standing); + updateAnimator(animator, 3); + expect(animator.state).equal(hovering); + }); + + it('does not use switch from any state is another transition is possible', () => { + const lying = animatorState('lying', sit); + const standing = animatorState('standing', sit); + const hovering = animatorState('hovering', sit); + animatorTransition(lying, standing); + animatorTransition(anyState, hovering); + animatorTransition(standing, hovering); + + setAnimatorState(animator, lying); + updateAnimator(animator, 0); + setAnimatorState(animator, hovering); + updateAnimator(animator, 3); + expect(animator.state).equal(standing); + updateAnimator(animator, 3); + expect(animator.state).equal(hovering); + }); + + it('uses switch from any state is another transition is possible but longer than 2 jumps', () => { + const lying = animatorState('lying', sit); + const sitting = animatorState('sitting', sit); + const standing = animatorState('standing', sit); + const hovering = animatorState('hovering', sit); + animatorTransition(lying, sitting); + animatorTransition(sitting, standing); + animatorTransition(anyState, hovering); + animatorTransition(standing, hovering); + + setAnimatorState(animator, lying); + updateAnimator(animator, 0); + setAnimatorState(animator, hovering); + updateAnimator(animator, 3); + expect(animator.state).equal(hovering); + }); + + it('does not switch to next state immediately if exitNow is not set', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(sit); + }); + + it('switches to next state when animation is finished', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(run); + expect(getAnimationFrame(animator)).equal(0); + }); + + it('does nothing if switching to already targeted state', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + setAnimatorState(animator, running); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(run); + }); + + it('does nothing if transition is no possible', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(sit); + }); + + it('switches through intermediate state', () => { + const sitting = animatorState('sitting', sit); + const other = animatorState('other', sit); + const standing = animatorState('standing', stand); + const running = animatorState('running', run); + animatorTransition(sitting, standing); + animatorTransition(other, running); + animatorTransition(standing, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(stand); + + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(run); + }); + + it('switches directly to target state if exitNow is set on both states', () => { + const sitting = animatorState('sitting', sit); + const standing = animatorState('standing', stand); + const running = animatorState('running', run); + animatorTransition(sitting, standing, { exitAfter: 0 }); + animatorTransition(standing, running, { exitAfter: 0 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(run); + }); + + it('cancells switch to target state', () => { + const sitting = animatorState('sitting', sit); + const running = animatorState('running', run); + animatorTransition(sitting, running); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + setAnimatorState(animator, sitting); + updateAnimator(animator, 3); + + expect(getAnimation(animator)).equal(sit); + }); + + it('handles loops in states', () => { + const sitting = animatorState('sitting', sit); + const standing0 = animatorState('standing0', sit); + const standing1 = animatorState('standing1', stand); + const standing2 = animatorState('standing2', stand); + const running = animatorState('running', run); + animatorTransition(sitting, standing0, { exitAfter: 0 }); + animatorTransition(running, standing1, { exitAfter: 0 }); + animatorTransition(running, standing2, { exitAfter: 0 }); + animatorTransition(standing0, standing2, { exitAfter: 0 }); + animatorTransition(standing1, running, { exitAfter: 0 }); + animatorTransition(standing2, running, { exitAfter: 0 }); + + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + setAnimatorState(animator, running); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(run); + }); + + it('returns correct animation variant', () => { + const sitting = animatorState('sitting', sit, { alt: sit2 }); + + animator.variant = 'alt'; + setAnimatorState(animator, sitting); + updateAnimator(animator, 0.1); + + expect(getAnimation(animator)).equal(sit2); + }); + + it('does not use shorter transition route if onlyDirectTo is set', () => { + const standing = animatorState('standing', run); + const standingUp = animatorState('standing-up', run); + const sitting = animatorState('sitting', run); + const sittingUp = animatorState('sitting-up', run); + const lying = animatorState('lying', run); + const lyingToTrotting = animatorState('lying-to-trotting', run); + const trotting = animatorState('trotting', run); + animatorTransition(lying, sittingUp); + animatorTransition(sittingUp, sitting); + animatorTransition(sitting, standingUp); + animatorTransition(standingUp, standing); + animatorTransition(lying, lyingToTrotting, { onlyDirectTo: trotting }); + animatorTransition(lyingToTrotting, standing); + animatorTransition(lyingToTrotting, trotting); + + setAnimatorState(animator, lying); + updateAnimator(animator, 0); + setAnimatorState(animator, standing); + updateAnimator(animator, 1.1); + expect(animator.state).equal(sittingUp); + updateAnimator(animator, 1.1); + expect(animator.state).equal(sitting); + updateAnimator(animator, 1.1); + expect(animator.state).equal(standingUp); + updateAnimator(animator, 1.1); + expect(animator.state).equal(standing); + }); }); diff --git a/src/ts/tests/common/bitUtils.spec.ts b/src/ts/tests/common/bitUtils.spec.ts index d713bd9..536aa75 100644 --- a/src/ts/tests/common/bitUtils.spec.ts +++ b/src/ts/tests/common/bitUtils.spec.ts @@ -4,195 +4,195 @@ import { map, range, random } from 'lodash'; import { bitWriter, bitReader, numberToBitCount, countBits } from '../../common/bitUtils'; function toArray(buffer: Uint8Array): number[] { - return map(buffer, x => x); + return map(buffer, x => x); } describe('numberToBitCount()', () => { - it('returns 0 for 0', () => { - expect(numberToBitCount(0)).equal(0); - }); + it('returns 0 for 0', () => { + expect(numberToBitCount(0)).equal(0); + }); - it('returns 1 for 1', () => { - expect(numberToBitCount(1)).equal(1); - }); + it('returns 1 for 1', () => { + expect(numberToBitCount(1)).equal(1); + }); - it('returns 3 for 7', () => { - expect(numberToBitCount(7)).equal(3); - }); + it('returns 3 for 7', () => { + expect(numberToBitCount(7)).equal(3); + }); - it('returns 4 for 8', () => { - expect(numberToBitCount(8)).equal(4); - }); + it('returns 4 for 8', () => { + expect(numberToBitCount(8)).equal(4); + }); - it('returns 16 for 0xffff', () => { - expect(numberToBitCount(0xffff)).equal(16); - }); + it('returns 16 for 0xffff', () => { + expect(numberToBitCount(0xffff)).equal(16); + }); - it('returns 32 for 0xffffffff', () => { - expect(numberToBitCount(0xffffffff)).equal(32); - }); + it('returns 32 for 0xffffffff', () => { + expect(numberToBitCount(0xffffffff)).equal(32); + }); - it('returns 32 for -1', () => { - expect(numberToBitCount(0xffffffff)).equal(32); - }); + it('returns 32 for -1', () => { + expect(numberToBitCount(0xffffffff)).equal(32); + }); }); describe('countBits()', () => { - it('returns 0 for 0', () => { - expect(countBits(0)).equal(0); - }); + it('returns 0 for 0', () => { + expect(countBits(0)).equal(0); + }); - it('returns 1 for 1', () => { - expect(countBits(1)).equal(1); - }); + it('returns 1 for 1', () => { + expect(countBits(1)).equal(1); + }); - it('returns 16 for 0x55555555', () => { - expect(countBits(0x55555555)).equal(16); - }); + it('returns 16 for 0x55555555', () => { + expect(countBits(0x55555555)).equal(16); + }); - it('returns 16 for 0xffff', () => { - expect(countBits(0xffff)).equal(16); - }); + it('returns 16 for 0xffff', () => { + expect(countBits(0xffff)).equal(16); + }); - it('returns 32 for 0xffffffff', () => { - expect(countBits(0xffffffff)).equal(32); - }); + it('returns 32 for 0xffffffff', () => { + expect(countBits(0xffffffff)).equal(32); + }); - it('returns 32 for -1', () => { - expect(countBits(-1)).equal(32); - }); + it('returns 32 for -1', () => { + expect(countBits(-1)).equal(32); + }); }); describe('bitWriter', () => { - it('writes 1 bit', () => { - const buffer = bitWriter(write => write(1, 1)); - expect(toArray(buffer)).eql([0x80]); - }); + it('writes 1 bit', () => { + const buffer = bitWriter(write => write(1, 1)); + expect(toArray(buffer)).eql([0x80]); + }); - it('writes 8 bits', () => { - const buffer = bitWriter(write => write(123, 8)); - expect(toArray(buffer)).eql([123]); - }); + it('writes 8 bits', () => { + const buffer = bitWriter(write => write(123, 8)); + expect(toArray(buffer)).eql([123]); + }); - it('writes 32 bits', () => { - const buffer = bitWriter(write => write(0xaabbccdd, 32)); - expect(toArray(buffer)).eql([0xaa, 0xbb, 0xcc, 0xdd]); - }); + it('writes 32 bits', () => { + const buffer = bitWriter(write => write(0xaabbccdd, 32)); + expect(toArray(buffer)).eql([0xaa, 0xbb, 0xcc, 0xdd]); + }); - it('writes multiple values', () => { - const buffer = bitWriter(write => { - write(1, 1); - write(3, 2); - write(1, 1); - }); - expect(toArray(buffer)).eql([0xf0]); - }); + it('writes multiple values', () => { + const buffer = bitWriter(write => { + write(1, 1); + write(3, 2); + write(1, 1); + }); + expect(toArray(buffer)).eql([0xf0]); + }); - it('writes across bytes', () => { - const buffer = bitWriter(write => { - write(1, 4); - write(1, 8); - write(1, 4); - }); - expect(toArray(buffer)).eql([0x10, 0x11]); - }); + it('writes across bytes', () => { + const buffer = bitWriter(write => { + write(1, 4); + write(1, 8); + write(1, 4); + }); + expect(toArray(buffer)).eql([0x10, 0x11]); + }); - it('writes a lot of values', () => { - const values = range(0, 200).map(() => random(0, 255)); - const buffer = bitWriter(write => values.forEach(value => write(value, 8))); - expect(toArray(buffer)).eql(values); - }); + it('writes a lot of values', () => { + const values = range(0, 200).map(() => random(0, 255)); + const buffer = bitWriter(write => values.forEach(value => write(value, 8))); + expect(toArray(buffer)).eql(values); + }); - it('trims values that do not fit into given amount of bits', () => { - const buffer = bitWriter(write => { - write(0xff, 4); - write(0, 4); - }); - expect(toArray(buffer)).eql([0xf0]); - }); + it('trims values that do not fit into given amount of bits', () => { + const buffer = bitWriter(write => { + write(0xff, 4); + write(0, 4); + }); + expect(toArray(buffer)).eql([0xf0]); + }); - it('throws for incorrect bit count', () => { - bitWriter(write => expect(() => write(0, 33)).throw('Invalid bit count')); - }); + it('throws for incorrect bit count', () => { + bitWriter(write => expect(() => write(0, 33)).throw('Invalid bit count')); + }); - it('throws for incorrect bit count', () => { - bitWriter(write => expect(() => write(0, -1)).throw('Invalid bit count')); - }); + it('throws for incorrect bit count', () => { + bitWriter(write => expect(() => write(0, -1)).throw('Invalid bit count')); + }); }); describe('bitReader', () => { - it('reads 1 bit', () => { - const read = bitReader(new Uint8Array([0x80])); - expect(read(1)).equal(1); - }); + it('reads 1 bit', () => { + const read = bitReader(new Uint8Array([0x80])); + expect(read(1)).equal(1); + }); - it('reads 8 bits', () => { - const read = bitReader(new Uint8Array([123])); - expect(read(8)).equal(123); - }); + it('reads 8 bits', () => { + const read = bitReader(new Uint8Array([123])); + expect(read(8)).equal(123); + }); - it('reads 32 bits', () => { - const read = bitReader(new Uint8Array([0x0a, 0xbb, 0xcc, 0xdd])); - expect(read(32)).equal(0x0abbccdd); - }); + it('reads 32 bits', () => { + const read = bitReader(new Uint8Array([0x0a, 0xbb, 0xcc, 0xdd])); + expect(read(32)).equal(0x0abbccdd); + }); - it('reads always unsigned', () => { - const read = bitReader(new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd])); - expect(read(32)).equal(0xaabbccdd); - }); + it('reads always unsigned', () => { + const read = bitReader(new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd])); + expect(read(32)).equal(0xaabbccdd); + }); - it('reads multiple values', () => { - const read = bitReader(new Uint8Array([0xf0])); - expect(read(1)).equal(1); - expect(read(2)).equal(3); - expect(read(1)).equal(1); - }); + it('reads multiple values', () => { + const read = bitReader(new Uint8Array([0xf0])); + expect(read(1)).equal(1); + expect(read(2)).equal(3); + expect(read(1)).equal(1); + }); - it('reads multiple bytes', () => { - const read = bitReader(new Uint8Array([0x01, 0x01])); - expect(read(8)).equal(1); - expect(read(8)).equal(1); - }); + it('reads multiple bytes', () => { + const read = bitReader(new Uint8Array([0x01, 0x01])); + expect(read(8)).equal(1); + expect(read(8)).equal(1); + }); - it('reads across bytes', () => { - const read = bitReader(new Uint8Array([0x10, 0x11])); - expect(read(4)).equal(1); - expect(read(8)).equal(1); - expect(read(4)).equal(1); - }); + it('reads across bytes', () => { + const read = bitReader(new Uint8Array([0x10, 0x11])); + expect(read(4)).equal(1); + expect(read(8)).equal(1); + expect(read(4)).equal(1); + }); - it('reads a lot of values', () => { - const values = range(0, 200).map(() => random(0, 255)); - const read = bitReader(new Uint8Array(values)); - values.forEach(value => expect(read(8)).equal(value)); - }); + it('reads a lot of values', () => { + const values = range(0, 200).map(() => random(0, 255)); + const read = bitReader(new Uint8Array(values)); + values.forEach(value => expect(read(8)).equal(value)); + }); - it('throws for incorrect bit count', () => { - const read = bitReader(new Uint8Array(1)); - expect(() => read(33)).throw('Invalid bit count'); - }); + it('throws for incorrect bit count', () => { + const read = bitReader(new Uint8Array(1)); + expect(() => read(33)).throw('Invalid bit count'); + }); - it('throws for incorrect bit count', () => { - const read = bitReader(new Uint8Array(1)); - expect(() => read(-1)).throw('Invalid bit count'); - }); + it('throws for incorrect bit count', () => { + const read = bitReader(new Uint8Array(1)); + expect(() => read(-1)).throw('Invalid bit count'); + }); - it('throws for reading past end', () => { - const read = bitReader(new Uint8Array(1)); - read(8); - expect(() => read(1)).throw('Reading past end'); - }); + it('throws for reading past end', () => { + const read = bitReader(new Uint8Array(1)); + read(8); + expect(() => read(1)).throw('Reading past end'); + }); }); describe('bitWriter + bitReader', () => { - const tests = [ - [[1, 1, 1, 1], [7, 5, 3, 1]], - ]; + const tests = [ + [[1, 1, 1, 1], [7, 5, 3, 1]], + ]; - tests.forEach(([values, bits]) => it(`should work for ${JSON.stringify([values, bits])}`, () => { - const buffer = bitWriter(write => bits.forEach((b, i) => write(values[i], b))); - const read = bitReader(buffer); - const result = bits.map(b => read(b)); - expect(result).eql(values); - })); + tests.forEach(([values, bits]) => it(`should work for ${JSON.stringify([values, bits])}`, () => { + const buffer = bitWriter(write => bits.forEach((b, i) => write(values[i], b))); + const read = bitReader(buffer); + const result = bits.map(b => read(b)); + expect(result).eql(values); + })); }); diff --git a/src/ts/tests/common/camera.spec.ts b/src/ts/tests/common/camera.spec.ts index 6135f8b..8377e69 100644 --- a/src/ts/tests/common/camera.spec.ts +++ b/src/ts/tests/common/camera.spec.ts @@ -1,172 +1,172 @@ import '../lib'; import { expect } from 'chai'; import { - updateCamera, centerCameraOn, isWorldPointVisible, isEntityVisible, isAreaVisible, - isRectVisible, screenToWorld, worldToScreen, createCamera + updateCamera, centerCameraOn, isWorldPointVisible, isEntityVisible, isAreaVisible, + isRectVisible, screenToWorld, worldToScreen, createCamera } from '../../common/camera'; import { rect } from '../../common/rect'; import { entity } from '../mocks'; import { toScreenX, toScreenY } from '../../common/positionUtils'; describe('Camera', () => { - describe('updateCamera()', () => { - it('should not move camera if already in view', () => { - const camera = createCamera(); - camera.w = 512; - camera.h = 480; - camera.x = 1344; - camera.actualY = camera.y = 960; + describe('updateCamera()', () => { + it('should not move camera if already in view', () => { + const camera = createCamera(); + camera.w = 512; + camera.h = 480; + camera.x = 1344; + camera.actualY = camera.y = 960; - updateCamera(camera, { x: 50, y: 50 }, { width: 100, height: 100 }); + updateCamera(camera, { x: 50, y: 50 }, { width: 100, height: 100 }); - expect(camera.x).equal(1344, 'x'); - expect(camera.y).equal(960, 'y'); - }); + expect(camera.x).equal(1344, 'x'); + expect(camera.y).equal(960, 'y'); + }); - it('should move camera if player is not in view', () => { - const camera = createCamera(); - camera.w = 512; - camera.h = 480; - camera.x = 1344; - camera.actualY = camera.y = 960; + it('should move camera if player is not in view', () => { + const camera = createCamera(); + camera.w = 512; + camera.h = 480; + camera.x = 1344; + camera.actualY = camera.y = 960; - updateCamera(camera, { x: 75, y: 75 }, { width: 100, height: 100 }); + updateCamera(camera, { x: 75, y: 75 }, { width: 100, height: 100 }); - expect(camera.x).equal(2067, 'x'); - expect(camera.y).equal(1463, 'y'); - }); + expect(camera.x).equal(2067, 'x'); + expect(camera.y).equal(1463, 'y'); + }); - it('does not restrict camera position if camera is not near the edge of map', () => { - const camera = createCamera(); - camera.w = toScreenX(50); - camera.h = toScreenY(50); - camera.x = toScreenX(25); - camera.actualY = camera.y = toScreenY(25); + it('does not restrict camera position if camera is not near the edge of map', () => { + const camera = createCamera(); + camera.w = toScreenX(50); + camera.h = toScreenY(50); + camera.x = toScreenX(25); + camera.actualY = camera.y = toScreenY(25); - updateCamera(camera, { x: 50, y: 50 }, { width: 100, height: 100 }); + updateCamera(camera, { x: 50, y: 50 }, { width: 100, height: 100 }); - expect(camera.x).equal(toScreenX(25), 'x'); - expect(camera.y).equal(toScreenY(25), 'y'); - }); + expect(camera.x).equal(toScreenX(25), 'x'); + expect(camera.y).equal(toScreenY(25), 'y'); + }); - it('restricts camera position to top left edge of map', () => { - const camera = createCamera(); - camera.w = toScreenX(50); - camera.h = toScreenY(50); + it('restricts camera position to top left edge of map', () => { + const camera = createCamera(); + camera.w = toScreenX(50); + camera.h = toScreenY(50); - updateCamera(camera, { x: 0, y: 0 }, { width: 100, height: 100 }); + updateCamera(camera, { x: 0, y: 0 }, { width: 100, height: 100 }); - expect(camera.x).equal(0, 'x'); - expect(camera.y).equal(0, 'y'); - }); + expect(camera.x).equal(0, 'x'); + expect(camera.y).equal(0, 'y'); + }); - it('restricts camera position to bottom right edge of map', () => { - const camera = createCamera(); - camera.w = toScreenX(50); - camera.h = toScreenY(50); + it('restricts camera position to bottom right edge of map', () => { + const camera = createCamera(); + camera.w = toScreenX(50); + camera.h = toScreenY(50); - updateCamera(camera, { x: 100, y: 100 }, { width: 100, height: 100 }); + updateCamera(camera, { x: 100, y: 100 }, { width: 100, height: 100 }); - expect(camera.x).equal(toScreenX(50), 'x'); - expect(camera.y).equal(toScreenY(50), 'y'); - }); + expect(camera.x).equal(toScreenX(50), 'x'); + expect(camera.y).equal(toScreenY(50), 'y'); + }); - it('centers map in view if map is smaller than camera view', () => { - const camera = createCamera(); - camera.w = toScreenX(100); - camera.h = toScreenY(100); + it('centers map in view if map is smaller than camera view', () => { + const camera = createCamera(); + camera.w = toScreenX(100); + camera.h = toScreenY(100); - updateCamera(camera, { x: 25, y: 25 }, { width: 50, height: 50 }); + updateCamera(camera, { x: 25, y: 25 }, { width: 50, height: 50 }); - expect(camera.x).equal(toScreenX(-25), 'x'); - expect(camera.y).equal(toScreenY(-25), 'y'); - }); - }); + expect(camera.x).equal(toScreenX(-25), 'x'); + expect(camera.y).equal(toScreenY(-25), 'y'); + }); + }); - describe('centerCameraOn()', () => { - it('centers camera on point', () => { - const camera = createCamera(); + describe('centerCameraOn()', () => { + it('centers camera on point', () => { + const camera = createCamera(); - centerCameraOn(camera, { x: 0, y: 0 }); + centerCameraOn(camera, { x: 0, y: 0 }); - expect(camera.x).equal(-50, 'x'); - expect(camera.y).equal(-75, 'y'); - }); - }); + expect(camera.x).equal(-50, 'x'); + expect(camera.y).equal(-75, 'y'); + }); + }); - describe('isWorldPointVisible()', () => { - it('returns true if entity position is in view of camera', () => { - expect(isWorldPointVisible(createCamera(), entity(0, 1, 1))).true; - }); + describe('isWorldPointVisible()', () => { + it('returns true if entity position is in view of camera', () => { + expect(isWorldPointVisible(createCamera(), entity(0, 1, 1))).true; + }); - it('returns false if entity position is out of view of camera', () => { - expect(isWorldPointVisible(createCamera(), entity(0, 10, 10))).false; - }); - }); + it('returns false if entity position is out of view of camera', () => { + expect(isWorldPointVisible(createCamera(), entity(0, 10, 10))).false; + }); + }); - describe('isEntityVisible()', () => { - it('returns true if entity bounds are in view of camera', () => { - expect(isEntityVisible(createCamera(), entity(0, 1, 1, 1, { bounds: rect(0, 0, 0.1, 0.1) }))).true; - }); + describe('isEntityVisible()', () => { + it('returns true if entity bounds are in view of camera', () => { + expect(isEntityVisible(createCamera(), entity(0, 1, 1, 1, { bounds: rect(0, 0, 0.1, 0.1) }))).true; + }); - it('returns false if entity bounds are out of view of camera', () => { - expect(isEntityVisible(createCamera(), entity(0, 10, 10, 1, { bounds: rect(0, 0, 0.1, 0.1) }))).false; - }); - }); + it('returns false if entity bounds are out of view of camera', () => { + expect(isEntityVisible(createCamera(), entity(0, 10, 10, 1, { bounds: rect(0, 0, 0.1, 0.1) }))).false; + }); + }); - describe('isAreaVisible()', () => { - it('returns true if rectangle intersects camera view', () => { - const camera = createCamera(); + describe('isAreaVisible()', () => { + it('returns true if rectangle intersects camera view', () => { + const camera = createCamera(); - Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); + Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); - expect(isAreaVisible(camera, 50, 50, 100, 100)).true; - }); + expect(isAreaVisible(camera, 50, 50, 100, 100)).true; + }); - it('returns false if rectangle does not intersect camera view', () => { - const camera = createCamera(); + it('returns false if rectangle does not intersect camera view', () => { + const camera = createCamera(); - Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); + Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); - expect(isAreaVisible(camera, 101, 101, 100, 100)).false; - }); - }); + expect(isAreaVisible(camera, 101, 101, 100, 100)).false; + }); + }); - describe('isRectVisible()', () => { - it('returns true if rectangle intersects camera view', () => { - const camera = createCamera(); + describe('isRectVisible()', () => { + it('returns true if rectangle intersects camera view', () => { + const camera = createCamera(); - Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); + Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); - expect(isRectVisible(camera, rect(50, 50, 100, 100))).true; - }); + expect(isRectVisible(camera, rect(50, 50, 100, 100))).true; + }); - it('returns false if rectangle does not intersect camera view', () => { - const camera = createCamera(); + it('returns false if rectangle does not intersect camera view', () => { + const camera = createCamera(); - Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); + Object.assign(camera, { w: 100, h: 100, x: 0, y: 0 }); - expect(isRectVisible(camera, rect(101, 101, 100, 100))).false; - }); - }); + expect(isRectVisible(camera, rect(101, 101, 100, 100))).false; + }); + }); - describe('screenToWorld()', () => { - it('maps position from screen to world coordinates', () => { - const camera = createCamera(); - camera.x = 32; - camera.actualY = camera.y = 24; + describe('screenToWorld()', () => { + it('maps position from screen to world coordinates', () => { + const camera = createCamera(); + camera.x = 32; + camera.actualY = camera.y = 24; - expect(screenToWorld(camera, { x: 64, y: 48 })).eql({ x: 3, y: 3 }); - }); - }); + expect(screenToWorld(camera, { x: 64, y: 48 })).eql({ x: 3, y: 3 }); + }); + }); - describe('worldToScreen()', () => { - it('maps position from world to screen coordinates', () => { - const camera = createCamera(); - camera.x = 32; - camera.actualY = camera.y = 24; + describe('worldToScreen()', () => { + it('maps position from world to screen coordinates', () => { + const camera = createCamera(); + camera.x = 32; + camera.actualY = camera.y = 24; - expect(worldToScreen(camera, { x: 3, y: 3 })).eql({ x: 64, y: 48 }); - }); - }); + expect(worldToScreen(camera, { x: 3, y: 3 })).eql({ x: 64, y: 48 }); + }); + }); }); diff --git a/src/ts/tests/common/collision.spec.ts b/src/ts/tests/common/collision.spec.ts index 6290da7..59e535c 100644 --- a/src/ts/tests/common/collision.spec.ts +++ b/src/ts/tests/common/collision.spec.ts @@ -11,138 +11,138 @@ import { ponyColliders } from '../../common/mixins'; import { updateTileIndices } from '../../client/tileUtils'; export function colliders(x: number, y: number, w: number, h: number, tall = true, exact = false): Collider[] { - return [{ x, y, w, h, tall, exact }]; + return [{ x, y, w, h, tall, exact }]; } export function updateColliders(map: IMap) { - for (const region of map.regions) { - updateTileIndices(region, map); - } + for (const region of map.regions) { + updateTileIndices(region, map); + } - for (const region of map.regions) { - generateRegionCollider(region, map); - } + for (const region of map.regions) { + generateRegionCollider(region, map); + } } describe('collision', () => { - describe('updatePosition()', () => { - let map: IMap; - let ent: Entity; + describe('updatePosition()', () => { + let map: IMap; + let ent: Entity; - beforeEach(() => { - map = createServerMap('', 0, 10, 10, TileType.Dirt); - ent = entity(1, 0, 0, PONY_TYPE); - ent.colliders = colliders(0, 0, tileWidth, tileHeight); - }); + beforeEach(() => { + map = createServerMap('', 0, 10, 10, TileType.Dirt); + ent = entity(1, 0, 0, PONY_TYPE); + ent.colliders = colliders(0, 0, tileWidth, tileHeight); + }); - it('does not update position if not moving', () => { - updatePosition(ent, 1, map); + it('does not update position if not moving', () => { + updatePosition(ent, 1, map); - expect(ent.x).equal(0); - expect(ent.y).equal(0); - }); + expect(ent.x).equal(0); + expect(ent.y).equal(0); + }); - it('updates position if moving', () => { - ent.vx = 2; - ent.vy = 1; + it('updates position if moving', () => { + ent.vx = 2; + ent.vy = 1; - updatePosition(ent, 1, map); + updatePosition(ent, 1, map); - expect(ent.x).equal(2); - expect(ent.y).equal(1); - }); + expect(ent.x).equal(2); + expect(ent.y).equal(1); + }); - it('updates position if moving and not colliding', () => { - ent.vx = 2; - ent.vy = 1; - ent.flags |= EntityFlags.CanCollide; + it('updates position if moving and not colliding', () => { + ent.vx = 2; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; - updatePosition(ent, 1, map); + updatePosition(ent, 1, map); - expect(ent.x).equal(2); - expect(ent.y).equal(1); - }); + expect(ent.x).equal(2); + expect(ent.y).equal(1); + }); - it('clips move if colliding', () => { - setupCollider(map, 9, 1); - ent.x = 8; - ent.y = 1; - ent.vx = 1; - ent.flags |= EntityFlags.CanCollide; - ent.colliders = colliders(-12, -9, 24, 18); - updateColliders(map); + it('clips move if colliding', () => { + setupCollider(map, 9, 1); + ent.x = 8; + ent.y = 1; + ent.vx = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = colliders(-12, -9, 24, 18); + updateColliders(map); - updatePosition(ent, 1, map); + updatePosition(ent, 1, map); - expect(ent.x).equal(8.124969482421875); - }); + expect(ent.x).equal(8.124969482421875); + }); - it('clips move Y if colliding in Y direction', () => { - setupCollider(map, 2, 9); - setupCollider(map, 1, 9); - ent.x = 1; - ent.y = 8; - ent.vx = 1; - ent.vy = 1; - ent.flags |= EntityFlags.CanCollide; - ent.colliders = ponyColliders; - updateColliders(map); + it('clips move Y if colliding in Y direction', () => { + setupCollider(map, 2, 9); + setupCollider(map, 1, 9); + ent.x = 1; + ent.y = 8; + ent.vx = 1; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = ponyColliders; + updateColliders(map); - updatePosition(ent, 1, map); + updatePosition(ent, 1, map); - expect(ent.x).equal(2, 'x'); - expect(ent.y).equal(8.333292643229166, 'y'); - }); + expect(ent.x).equal(2, 'x'); + expect(ent.y).equal(8.333292643229166, 'y'); + }); - it('clips move X if colliding in X direction', () => { - setupCollider(map, 10, 1); - ent.x = 9; - ent.y = 1; - ent.vx = 1; - ent.vy = 1; - ent.flags |= EntityFlags.CanCollide; - ent.colliders = ponyColliders; - updateColliders(map); + it('clips move X if colliding in X direction', () => { + setupCollider(map, 10, 1); + ent.x = 9; + ent.y = 1; + ent.vx = 1; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = ponyColliders; + updateColliders(map); - updatePosition(ent, 1, map); + updatePosition(ent, 1, map); - expect(ent.x).equal(9.624969482421875, 'x'); - expect(ent.y).equal(2.0833333333333335, 'y'); - }); + expect(ent.x).equal(9.624969482421875, 'x'); + expect(ent.y).equal(2.0833333333333335, 'y'); + }); - it('updates position if moving and colliding but already in colliding position', () => { - setupCollider(map, 8, 8); - setupCollider(map, 9, 9); - setupCollider(map, 8, 9); - setupCollider(map, 9, 8); - ent.x = 8; - ent.y = 8; - ent.vx = 1; - ent.vy = 1; - ent.flags |= EntityFlags.CanCollide; - ent.colliders = colliders(-16, -12, 32, 24); - updateColliders(map); + it('updates position if moving and colliding but already in colliding position', () => { + setupCollider(map, 8, 8); + setupCollider(map, 9, 9); + setupCollider(map, 8, 9); + setupCollider(map, 9, 8); + ent.x = 8; + ent.y = 8; + ent.vx = 1; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = colliders(-16, -12, 32, 24); + updateColliders(map); - updatePosition(ent, 1, map); + updatePosition(ent, 1, map); - expect(ent.x).equal(9); - expect(ent.y).equal(9); - }); + expect(ent.x).equal(9); + expect(ent.y).equal(9); + }); - it('does not update position if moving, colliding, already in colliding position but going outside the map', () => { - setupCollider(map, 11, 11); - ent.x = 110; - ent.y = 110; - ent.vx = 1; - ent.vy = 1; - ent.flags |= EntityFlags.CanCollide; - ent.colliders = colliders(-16, -12, 32, 24); - updateColliders(map); + it('does not update position if moving, colliding, already in colliding position but going outside the map', () => { + setupCollider(map, 11, 11); + ent.x = 110; + ent.y = 110; + ent.vx = 1; + ent.vy = 1; + ent.flags |= EntityFlags.CanCollide; + ent.colliders = colliders(-16, -12, 32, 24); + updateColliders(map); - updatePosition(ent, 1, map); + updatePosition(ent, 1, map); - expect(ent.x).equal(110); - expect(ent.y).equal(110); - }); - }); + expect(ent.x).equal(110); + expect(ent.y).equal(110); + }); + }); }); diff --git a/src/ts/tests/common/color.spec.ts b/src/ts/tests/common/color.spec.ts index b1258b8..873bb1d 100644 --- a/src/ts/tests/common/color.spec.ts +++ b/src/ts/tests/common/color.spec.ts @@ -1,259 +1,259 @@ import '../lib'; import { expect } from 'chai'; import { - getR, getG, getB, getAlpha, colorToRGBA, colorToHSVA, colorToCSS, colorToHexRGB, colorToFloatArray, - colorToFloat, colorFromRGBA, colorFromHSVA, colorFromHSVAObject, parseColorFast, parseColor, - parseColorWithAlpha, makeTransparent, lerpColors, h2rgb, hsv2rgb, rgb2hsv, colorToFloatAlpha, - multiplyColor, withAlphaFloat, withAlpha + getR, getG, getB, getAlpha, colorToRGBA, colorToHSVA, colorToCSS, colorToHexRGB, colorToFloatArray, + colorToFloat, colorFromRGBA, colorFromHSVA, colorFromHSVAObject, parseColorFast, parseColor, + parseColorWithAlpha, makeTransparent, lerpColors, h2rgb, hsv2rgb, rgb2hsv, colorToFloatAlpha, + multiplyColor, withAlphaFloat, withAlpha } from '../../common/color'; describe('color', () => { - describe('getR()', () => { - it('returns red color value', () => { - expect(getR(0x12345678)).equals(0x12); - }); - }); + describe('getR()', () => { + it('returns red color value', () => { + expect(getR(0x12345678)).equals(0x12); + }); + }); - describe('getG()', () => { - it('returns green color value', () => { - expect(getG(0x12345678)).equals(0x34); - }); - }); + describe('getG()', () => { + it('returns green color value', () => { + expect(getG(0x12345678)).equals(0x34); + }); + }); - describe('getB()', () => { - it('returns blue color value', () => { - expect(getB(0x12345678)).equals(0x56); - }); - }); + describe('getB()', () => { + it('returns blue color value', () => { + expect(getB(0x12345678)).equals(0x56); + }); + }); - describe('getAlpha()', () => { - it('returns alpha color value', () => { - expect(getAlpha(0x12345678)).equals(0x78); - }); - }); + describe('getAlpha()', () => { + it('returns alpha color value', () => { + expect(getAlpha(0x12345678)).equals(0x78); + }); + }); - describe('colorToRGBA()', () => { - it('splits color into r, g, b, a values', () => { - expect(colorToRGBA(0x12345678)).eql({ - r: 0x12, - g: 0x34, - b: 0x56, - a: 0x78, - }); - }); - }); + describe('colorToRGBA()', () => { + it('splits color into r, g, b, a values', () => { + expect(colorToRGBA(0x12345678)).eql({ + r: 0x12, + g: 0x34, + b: 0x56, + a: 0x78, + }); + }); + }); - describe('colorToHSVA()', () => { - it('converts color to HSV values', () => { - expect(colorToHSVA(0xffd500ff)).eql({ - h: 50.11764705882353, - s: 1, - v: 1, - a: 1, - }); - }); - }); + describe('colorToHSVA()', () => { + it('converts color to HSV values', () => { + expect(colorToHSVA(0xffd500ff)).eql({ + h: 50.11764705882353, + s: 1, + v: 1, + a: 1, + }); + }); + }); - describe('colorToCSS()', () => { - it('converts opaque color to hex code', () => { - expect(colorToCSS(0x123456ff)).equal('#123456'); - }); + describe('colorToCSS()', () => { + it('converts opaque color to hex code', () => { + expect(colorToCSS(0x123456ff)).equal('#123456'); + }); - it('converts transparent color to rgba(...) code', () => { - expect(colorToCSS(0x123456cc)).equal('rgba(18,52,86,0.8)'); - }); - }); + it('converts transparent color to rgba(...) code', () => { + expect(colorToCSS(0x123456cc)).equal('rgba(18,52,86,0.8)'); + }); + }); - describe('colorToHexRGB()', () => { - it('returns RGB hex code', () => { - expect(colorToHexRGB(0x123456ff)).equal('123456'); - }); - }); + describe('colorToHexRGB()', () => { + it('returns RGB hex code', () => { + expect(colorToHexRGB(0x123456ff)).equal('123456'); + }); + }); - describe('colorToFloatArray()', () => { - it('returns values of r, g, b, a as array of floats', () => { - expect(Array.from(colorToFloatArray(0x123456ff))) - .eql([0.07058823853731155, 0.20392157137393951, 0.33725491166114807, 1]); - }); - }); + describe('colorToFloatArray()', () => { + it('returns values of r, g, b, a as array of floats', () => { + expect(Array.from(colorToFloatArray(0x123456ff))) + .eql([0.07058823853731155, 0.20392157137393951, 0.33725491166114807, 1]); + }); + }); - describe('colorToFloat()', () => { - it('returns float value of color', () => { - expect(colorToFloat(0x123456ff)).equal(-7.118128890449717e+37); - }); - }); + describe('colorToFloat()', () => { + it('returns float value of color', () => { + expect(colorToFloat(0x123456ff)).equal(-7.118128890449717e+37); + }); + }); - describe('colorToFloatAlpha()', () => { - it('returns float value of color', () => { - expect(colorToFloatAlpha(0x123456ff, 1)).equal(-7.118128890449717e+37); - }); + describe('colorToFloatAlpha()', () => { + it('returns float value of color', () => { + expect(colorToFloatAlpha(0x123456ff, 1)).equal(-7.118128890449717e+37); + }); - it('returns float value of color with alpha channel', () => { - expect(colorToFloatAlpha(0x123456ff, 0)).equal(7.916531978116694e-39); - }); - }); + it('returns float value of color with alpha channel', () => { + expect(colorToFloatAlpha(0x123456ff, 0)).equal(7.916531978116694e-39); + }); + }); - describe('colorFromRGBA()', () => { - it('creates color from r, g, b, a values', () => { - expect(colorFromRGBA(0x12, 0x34, 0x56, 0xff)).equal(0x123456ff); - }); - }); + describe('colorFromRGBA()', () => { + it('creates color from r, g, b, a values', () => { + expect(colorFromRGBA(0x12, 0x34, 0x56, 0xff)).equal(0x123456ff); + }); + }); - describe('colorFromHSVA()', () => { - it('creates color from h, s, v, a values', () => { - expect(colorFromHSVA(50.11764705882353, 1, 1, 1)).equal(0xffd500ff); - }); - }); + describe('colorFromHSVA()', () => { + it('creates color from h, s, v, a values', () => { + expect(colorFromHSVA(50.11764705882353, 1, 1, 1)).equal(0xffd500ff); + }); + }); - describe('colorFromHSVAObject()', () => { - it('creates color from h, s, v, a values', () => { - expect(colorFromHSVAObject({ h: 50.11764705882353, s: 1, v: 1, a: 1 })).equal(0xffd500ff); - }); - }); + describe('colorFromHSVAObject()', () => { + it('creates color from h, s, v, a values', () => { + expect(colorFromHSVAObject({ h: 50.11764705882353, s: 1, v: 1, a: 1 })).equal(0xffd500ff); + }); + }); - describe('parseColorFast()', () => { - it('returns transparent for invalid type', () => { - expect(parseColorFast(1 as any)).equal(0); - expect(parseColorFast(null as any)).equal(0); - expect(parseColorFast({} as any)).equal(0); - expect(parseColorFast(undefined as any)).equal(0); - }); + describe('parseColorFast()', () => { + it('returns transparent for invalid type', () => { + expect(parseColorFast(1 as any)).equal(0); + expect(parseColorFast(null as any)).equal(0); + expect(parseColorFast({} as any)).equal(0); + expect(parseColorFast(undefined as any)).equal(0); + }); - it('parses hex code', () => { - expect(parseColorFast('123456')).equal(0x123456ff); - }); + it('parses hex code', () => { + expect(parseColorFast('123456')).equal(0x123456ff); + }); - it('parses any other color format as opaque color', () => { - expect(parseColorFast('rgba(255, 0, 1, 0.5)')).equal(0xff0001ff); - }); - }); + it('parses any other color format as opaque color', () => { + expect(parseColorFast('rgba(255, 0, 1, 0.5)')).equal(0xff0001ff); + }); + }); - describe('parseColor()', () => { - it('returns transparent for invalid type', () => { - expect(parseColor(1 as any)).equal(0); - expect(parseColor(null as any)).equal(0); - expect(parseColor({} as any)).equal(0); - expect(parseColor(undefined as any)).equal(0); - }); + describe('parseColor()', () => { + it('returns transparent for invalid type', () => { + expect(parseColor(1 as any)).equal(0); + expect(parseColor(null as any)).equal(0); + expect(parseColor({} as any)).equal(0); + expect(parseColor(undefined as any)).equal(0); + }); - it('returns transparent for empty string, none and transparent', () => { - expect(parseColor('')).equal(0); - expect(parseColor('none')).equal(0); - expect(parseColor('transparent')).equal(0); - }); + it('returns transparent for empty string, none and transparent', () => { + expect(parseColor('')).equal(0); + expect(parseColor('none')).equal(0); + expect(parseColor('transparent')).equal(0); + }); - it('parses short form hex code', () => { - expect(parseColor('#123')).equal(0x112233ff); - }); + it('parses short form hex code', () => { + expect(parseColor('#123')).equal(0x112233ff); + }); - it('parses hex code', () => { - expect(parseColor('#123456')).equal(0x123456ff); - }); + it('parses hex code', () => { + expect(parseColor('#123456')).equal(0x123456ff); + }); - it('parses hex code with alpha', () => { - expect(parseColor('#12345678')).equal(0x12345678); - }); + it('parses hex code with alpha', () => { + expect(parseColor('#12345678')).equal(0x12345678); + }); - it('parses rgb(...) format', () => { - expect(parseColor('rgb(255, 0, 1)')).equal(0xff0001ff); - }); + it('parses rgb(...) format', () => { + expect(parseColor('rgb(255, 0, 1)')).equal(0xff0001ff); + }); - it('parses rgba(...) format', () => { - expect(parseColor('rgba(255, 0, 1, 0.5)')).equal(0xff00017f); - }); + it('parses rgba(...) format', () => { + expect(parseColor('rgba(255, 0, 1, 0.5)')).equal(0xff00017f); + }); - it('parses named color', () => { - expect(parseColor('red')).equal(0xff0000ff); - }); + it('parses named color', () => { + expect(parseColor('red')).equal(0xff0000ff); + }); - it('return black for invalid color', () => { - expect(parseColor('*$^@&')).equal(0xff); - }); - }); + it('return black for invalid color', () => { + expect(parseColor('*$^@&')).equal(0xff); + }); + }); - describe('parseColorWithAlpha()', () => { - it('parses color with given alpha', () => { - expect(parseColorWithAlpha('#12345678', 0.5)).equal(0x1234567f); - }); - }); + describe('parseColorWithAlpha()', () => { + it('parses color with given alpha', () => { + expect(parseColorWithAlpha('#12345678', 0.5)).equal(0x1234567f); + }); + }); - describe('withAlpha()', () => { - it('returns color with given alpha', () => { - expect(withAlpha(0x12345678, 127)).equal(0x1234567f); - }); - }); + describe('withAlpha()', () => { + it('returns color with given alpha', () => { + expect(withAlpha(0x12345678, 127)).equal(0x1234567f); + }); + }); - describe('withAlphaFloat()', () => { - it('returns color with given alpha', () => { - expect(withAlphaFloat(0x12345678, 0.5)).equal(0x1234567f); - }); - }); + describe('withAlphaFloat()', () => { + it('returns color with given alpha', () => { + expect(withAlphaFloat(0x12345678, 0.5)).equal(0x1234567f); + }); + }); - describe('makeTransparent()', () => { - it('adjust transparency', () => { - expect(makeTransparent(0x1234567f, 0.5)).equal(0x1234563f); - }); - }); + describe('makeTransparent()', () => { + it('adjust transparency', () => { + expect(makeTransparent(0x1234567f, 0.5)).equal(0x1234563f); + }); + }); - describe('multiplyColor()', () => { - it('multiplies r, g, b values', () => { - expect(multiplyColor(0x0a0a0aff, 0.5)).equal(0x050505ff); - }); + describe('multiplyColor()', () => { + it('multiplies r, g, b values', () => { + expect(multiplyColor(0x0a0a0aff, 0.5)).equal(0x050505ff); + }); - it('clamps r, g, b values', () => { - expect(multiplyColor(0x0a0a0aff, 100)).equal(0xffffffff); - }); - }); + it('clamps r, g, b values', () => { + expect(multiplyColor(0x0a0a0aff, 100)).equal(0xffffffff); + }); + }); - describe('lerpColors()', () => { - it('returns inbetween color', () => { - expect(lerpColors(0xff00ff00, 0x00ff00ff, 0.5)).equal(0x7f7f7f7f); - }); - }); + describe('lerpColors()', () => { + it('returns inbetween color', () => { + expect(lerpColors(0xff00ff00, 0x00ff00ff, 0.5)).equal(0x7f7f7f7f); + }); + }); - describe('rgb2hsv()', function () { - it('returns HSVA representation of RGBA values #1', function () { - expect(rgb2hsv(255, 0, 0, 0.5)).eql({ h: 0, s: 1, v: 1, a: 0.5 }); - }); + describe('rgb2hsv()', function () { + it('returns HSVA representation of RGBA values #1', function () { + expect(rgb2hsv(255, 0, 0, 0.5)).eql({ h: 0, s: 1, v: 1, a: 0.5 }); + }); - it('returns HSVA representation of RGBA values #2', function () { - expect(rgb2hsv(0, 255, 0, 0.5)).eql({ h: 120, s: 1, v: 1, a: 0.5 }); - }); + it('returns HSVA representation of RGBA values #2', function () { + expect(rgb2hsv(0, 255, 0, 0.5)).eql({ h: 120, s: 1, v: 1, a: 0.5 }); + }); - it('returns HSVA representation of RGBA values #3', function () { - expect(rgb2hsv(0, 0, 255, 0.5)).eql({ h: 240, s: 1, v: 1, a: 0.5 }); - }); + it('returns HSVA representation of RGBA values #3', function () { + expect(rgb2hsv(0, 0, 255, 0.5)).eql({ h: 240, s: 1, v: 1, a: 0.5 }); + }); - it('returns HSVA representation of RGBA values #4', function () { - expect(rgb2hsv(255, 0, 255, 0.5)).eql({ h: 300, s: 1, v: 1, a: 0.5 }); - }); + it('returns HSVA representation of RGBA values #4', function () { + expect(rgb2hsv(255, 0, 255, 0.5)).eql({ h: 300, s: 1, v: 1, a: 0.5 }); + }); - it('returns HSVA representation of RGBA values with retained hue', function () { - expect(rgb2hsv(0, 0, 0, 0.5, 100)).eql({ h: 100, s: 0, v: 0, a: 0.5 }); - }); - }); + it('returns HSVA representation of RGBA values with retained hue', function () { + expect(rgb2hsv(0, 0, 0, 0.5, 100)).eql({ h: 100, s: 0, v: 0, a: 0.5 }); + }); + }); - describe('hsv2rgb()', function () { - it('returns correct RGBA values', function () { - expect(hsv2rgb(0, 1, 1)).eql({ r: 255, g: 0, b: 0 }); - expect(hsv2rgb(60, 1, 1)).eql({ r: 255, g: 255, b: 0 }); - expect(hsv2rgb(120, 1, 1)).eql({ r: 0, g: 255, b: 0 }); - expect(hsv2rgb(240, 1, 1)).eql({ r: 0, g: 0, b: 255 }); - expect(hsv2rgb(180, 1, 1)).eql({ r: 0, g: 255, b: 255 }); - expect(hsv2rgb(300, 1, 1)).eql({ r: 255, g: 0, b: 255 }); - expect(hsv2rgb(360, 1, 1)).eql({ r: 255, g: 0, b: 0 }); - }); - }); + describe('hsv2rgb()', function () { + it('returns correct RGBA values', function () { + expect(hsv2rgb(0, 1, 1)).eql({ r: 255, g: 0, b: 0 }); + expect(hsv2rgb(60, 1, 1)).eql({ r: 255, g: 255, b: 0 }); + expect(hsv2rgb(120, 1, 1)).eql({ r: 0, g: 255, b: 0 }); + expect(hsv2rgb(240, 1, 1)).eql({ r: 0, g: 0, b: 255 }); + expect(hsv2rgb(180, 1, 1)).eql({ r: 0, g: 255, b: 255 }); + expect(hsv2rgb(300, 1, 1)).eql({ r: 255, g: 0, b: 255 }); + expect(hsv2rgb(360, 1, 1)).eql({ r: 255, g: 0, b: 0 }); + }); + }); - describe('h2rgb()', function () { - it('returns correct RGB values', function () { - expect(h2rgb(0)).eql({ r: 255, g: 0, b: 0 }); - expect(h2rgb(60)).eql({ r: 255, g: 255, b: 0 }); - expect(h2rgb(120)).eql({ r: 0, g: 255, b: 0 }); - expect(h2rgb(240)).eql({ r: 0, g: 0, b: 255 }); - expect(h2rgb(180)).eql({ r: 0, g: 255, b: 255 }); - expect(h2rgb(300)).eql({ r: 255, g: 0, b: 255 }); - expect(h2rgb(360)).eql({ r: 255, g: 0, b: 255 }); - }); - }); + describe('h2rgb()', function () { + it('returns correct RGB values', function () { + expect(h2rgb(0)).eql({ r: 255, g: 0, b: 0 }); + expect(h2rgb(60)).eql({ r: 255, g: 255, b: 0 }); + expect(h2rgb(120)).eql({ r: 0, g: 255, b: 0 }); + expect(h2rgb(240)).eql({ r: 0, g: 0, b: 255 }); + expect(h2rgb(180)).eql({ r: 0, g: 255, b: 255 }); + expect(h2rgb(300)).eql({ r: 255, g: 0, b: 255 }); + expect(h2rgb(360)).eql({ r: 255, g: 0, b: 255 }); + }); + }); }); diff --git a/src/ts/tests/common/colors.spec.ts b/src/ts/tests/common/colors.spec.ts index 0158bbe..924a4ef 100644 --- a/src/ts/tests/common/colors.spec.ts +++ b/src/ts/tests/common/colors.spec.ts @@ -1,46 +1,46 @@ import '../lib'; import { expect } from 'chai'; import { - fillToOutline, fillToOutlineColor, getMessageColor, SYSTEM_COLOR, ADMIN_COLOR, MOD_COLOR, - ANNOUNCEMENT_COLOR, PARTY_COLOR, WHITE, THINKING_COLOR, PARTY_THINKING_COLOR, SUPPORTER1_COLOR, - SUPPORTER2_COLOR, SUPPORTER3_COLOR + fillToOutline, fillToOutlineColor, getMessageColor, SYSTEM_COLOR, ADMIN_COLOR, MOD_COLOR, + ANNOUNCEMENT_COLOR, PARTY_COLOR, WHITE, THINKING_COLOR, PARTY_THINKING_COLOR, SUPPORTER1_COLOR, + SUPPORTER2_COLOR, SUPPORTER3_COLOR } from '../../common/colors'; import { MessageType } from '../../common/interfaces'; describe('colors', () => { - describe('fillToOutline()', () => { - it('returns undefined for undefined', () => { - expect(fillToOutline(undefined)).undefined; - }); + describe('fillToOutline()', () => { + it('returns undefined for undefined', () => { + expect(fillToOutline(undefined)).undefined; + }); - it('returns outline color', () => { - expect(fillToOutline('ff0000')).equals('b30000'); - }); - }); + it('returns outline color', () => { + expect(fillToOutline('ff0000')).equals('b30000'); + }); + }); - describe('fillToOutlineColor()', () => { - it('returns outline color', () => { - expect(fillToOutlineColor(0xff0000ff)).equals(0xb30000ff); - }); - }); + describe('fillToOutlineColor()', () => { + it('returns outline color', () => { + expect(fillToOutlineColor(0xff0000ff)).equals(0xb30000ff); + }); + }); - describe('getMessageColor()', () => { - it('returns correct color for each message type', () => { - expect(getMessageColor(MessageType.System)).equals(SYSTEM_COLOR); - expect(getMessageColor(MessageType.Admin)).equals(ADMIN_COLOR); - expect(getMessageColor(MessageType.Mod)).equals(MOD_COLOR); - expect(getMessageColor(MessageType.Announcement)).equals(ANNOUNCEMENT_COLOR); - expect(getMessageColor(MessageType.Party)).equals(PARTY_COLOR); - expect(getMessageColor(MessageType.Thinking)).equals(THINKING_COLOR); - expect(getMessageColor(MessageType.PartyThinking)).equals(PARTY_THINKING_COLOR); - expect(getMessageColor(MessageType.Supporter1)).equals(SUPPORTER1_COLOR); - expect(getMessageColor(MessageType.Supporter2)).equals(SUPPORTER2_COLOR); - expect(getMessageColor(MessageType.Supporter3)).equals(SUPPORTER3_COLOR); - }); + describe('getMessageColor()', () => { + it('returns correct color for each message type', () => { + expect(getMessageColor(MessageType.System)).equals(SYSTEM_COLOR); + expect(getMessageColor(MessageType.Admin)).equals(ADMIN_COLOR); + expect(getMessageColor(MessageType.Mod)).equals(MOD_COLOR); + expect(getMessageColor(MessageType.Announcement)).equals(ANNOUNCEMENT_COLOR); + expect(getMessageColor(MessageType.Party)).equals(PARTY_COLOR); + expect(getMessageColor(MessageType.Thinking)).equals(THINKING_COLOR); + expect(getMessageColor(MessageType.PartyThinking)).equals(PARTY_THINKING_COLOR); + expect(getMessageColor(MessageType.Supporter1)).equals(SUPPORTER1_COLOR); + expect(getMessageColor(MessageType.Supporter2)).equals(SUPPORTER2_COLOR); + expect(getMessageColor(MessageType.Supporter3)).equals(SUPPORTER3_COLOR); + }); - it('returns white color for other types', () => { - expect(getMessageColor(MessageType.Chat)).equals(WHITE); - expect(getMessageColor(999)).equals(WHITE); - }); - }); + it('returns white color for other types', () => { + expect(getMessageColor(MessageType.Chat)).equals(WHITE); + expect(getMessageColor(999)).equals(WHITE); + }); + }); }); diff --git a/src/ts/tests/common/compress.spec.ts b/src/ts/tests/common/compress.spec.ts index b9e1b1d..c8e8be7 100644 --- a/src/ts/tests/common/compress.spec.ts +++ b/src/ts/tests/common/compress.spec.ts @@ -5,19 +5,19 @@ import { compressTiles, decompressTiles } from '../../common/compress'; import { REGION_SIZE } from '../../common/constants'; describe('compress', () => { - describe('compressTiles() + decompressTiles()', () => { - it('test', () => { - const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); + describe('compressTiles() + decompressTiles()', () => { + it('test', () => { + const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); - for (let i = 0; i < tiles.length; i++) { - tiles[i] = random(0, 5); - } + for (let i = 0; i < tiles.length; i++) { + tiles[i] = random(0, 5); + } - // console.log(Array.from(tiles).join(', ')); - const compressed = compressTiles(new Uint8Array(tiles)); - const decompressed = decompressTiles(compressed); - //console.log(`${JSON.stringify(test)}: ${test.length} -> ${compressed.length}`); - expect(Array.from(decompressed)).eql(Array.from(tiles), `compressed: [${Array.from(compressed).join(', ')}]`); - }); - }); + // console.log(Array.from(tiles).join(', ')); + const compressed = compressTiles(new Uint8Array(tiles)); + const decompressed = decompressTiles(compressed); + //console.log(`${JSON.stringify(test)}: ${test.length} -> ${compressed.length}`); + expect(Array.from(decompressed)).eql(Array.from(tiles), `compressed: [${Array.from(compressed).join(', ')}]`); + }); + }); }); diff --git a/src/ts/tests/common/compressPony.spec.ts b/src/ts/tests/common/compressPony.spec.ts index d67c24d..e1a9be6 100644 --- a/src/ts/tests/common/compressPony.spec.ts +++ b/src/ts/tests/common/compressPony.spec.ts @@ -4,9 +4,9 @@ import { expect } from 'chai'; import { merge } from 'lodash'; import { bitWriter, bitReader } from '../../common/bitUtils'; import { - PrecompressedSet, writeSet, readSet, Precompressed, readPony, writePony, postdecompressPony, - precompressPony, compressPonyString, decompressPonyString, VERSION, precompressCM, fastPostdecompressPony, - decompressPony + PrecompressedSet, writeSet, readSet, Precompressed, readPony, writePony, postdecompressPony, + precompressPony, compressPonyString, decompressPonyString, VERSION, precompressCM, fastPostdecompressPony, + decompressPony } from '../../common/compressPony'; import { RED, ORANGE, CYAN, GREEN, BLUE, BLACK, WHITE, TRANSPARENT } from '../../common/colors'; import { PonyInfoNumber, PonyInfo } from '../../common/interfaces'; @@ -17,680 +17,680 @@ import { pathTo } from '../../server/paths'; // const poniesPath = pathTo('src', 'tests', 'ponies'); function base(black: T, white: T) { - return { - head: undefined, - nose: undefined, - ears: undefined, - horn: undefined, - wings: undefined, - frontHooves: undefined, - backHooves: undefined, - mane: undefined, - backMane: undefined, - tail: undefined, - facialHair: undefined, - headAccessory: undefined, - earAccessory: undefined, - faceAccessory: undefined, - neckAccessory: undefined, - frontLegAccessory: undefined, - backLegAccessory: undefined, - frontLegAccessoryRight: undefined, - backLegAccessoryRight: undefined, - lockBackLegAccessory: undefined, - unlockFrontLegAccessory: false, - unlockBackLegAccessory: false, - backAccessory: undefined, - waistAccessory: undefined, - chestAccessory: undefined, - sleeveAccessory: undefined, - extraAccessory: undefined, - coatFill: black, - coatOutline: black, - lockCoatOutline: undefined, - eyelashes: 0, - eyeColorLeft: black, - eyeColorRight: black, - eyeWhites: white, - eyeWhitesLeft: white, - eyeOpennessLeft: 0, - eyeOpennessRight: 0, - eyeshadow: false, - eyeshadowColor: undefined, - lockEyes: false, - lockEyeColor: false, - unlockEyeWhites: false, - fangs: 0, - muzzle: 0, - freckles: 0, - frecklesColor: undefined, - cm: undefined, - cmFlip: undefined, - customOutlines: false, - freeOutlines: false, - darkenLockedOutlines: undefined, - unlockEyelashColor: false, - eyelashColor: black, - eyelashColorLeft: black, - magicColor: white, - }; + return { + head: undefined, + nose: undefined, + ears: undefined, + horn: undefined, + wings: undefined, + frontHooves: undefined, + backHooves: undefined, + mane: undefined, + backMane: undefined, + tail: undefined, + facialHair: undefined, + headAccessory: undefined, + earAccessory: undefined, + faceAccessory: undefined, + neckAccessory: undefined, + frontLegAccessory: undefined, + backLegAccessory: undefined, + frontLegAccessoryRight: undefined, + backLegAccessoryRight: undefined, + lockBackLegAccessory: undefined, + unlockFrontLegAccessory: false, + unlockBackLegAccessory: false, + backAccessory: undefined, + waistAccessory: undefined, + chestAccessory: undefined, + sleeveAccessory: undefined, + extraAccessory: undefined, + coatFill: black, + coatOutline: black, + lockCoatOutline: undefined, + eyelashes: 0, + eyeColorLeft: black, + eyeColorRight: black, + eyeWhites: white, + eyeWhitesLeft: white, + eyeOpennessLeft: 0, + eyeOpennessRight: 0, + eyeshadow: false, + eyeshadowColor: undefined, + lockEyes: false, + lockEyeColor: false, + unlockEyeWhites: false, + fangs: 0, + muzzle: 0, + freckles: 0, + frecklesColor: undefined, + cm: undefined, + cmFlip: undefined, + customOutlines: false, + freeOutlines: false, + darkenLockedOutlines: undefined, + unlockEyelashColor: false, + eyelashColor: black, + eyelashColorLeft: black, + magicColor: white, + }; } describe('compressPony', () => { - describe('writeSet() + readSet()', () => { - const tests: [PrecompressedSet, number, boolean][] = [ - [{ type: 1, pattern: 1, colors: 1, fillLocks: 0, fills: [1], outlineLocks: 0, outlines: [] }, 1, false], - [{ type: 7, pattern: 6, colors: 3, fillLocks: 1, fills: [5, 7], outlineLocks: 0, outlines: [] }, 4, false], - [{ type: 7, pattern: 6, colors: 3, fillLocks: 1, fills: [5, 7], outlineLocks: 2, outlines: [3, 4] }, 4, true], - ]; + describe('writeSet() + readSet()', () => { + const tests: [PrecompressedSet, number, boolean][] = [ + [{ type: 1, pattern: 1, colors: 1, fillLocks: 0, fills: [1], outlineLocks: 0, outlines: [] }, 1, false], + [{ type: 7, pattern: 6, colors: 3, fillLocks: 1, fills: [5, 7], outlineLocks: 0, outlines: [] }, 4, false], + [{ type: 7, pattern: 6, colors: 3, fillLocks: 1, fills: [5, 7], outlineLocks: 2, outlines: [3, 4] }, 4, true], + ]; - tests.forEach(test => it(`works for set: ${JSON.stringify(test)}`, () => { - const [set, colorBits, customOutlines] = test; - const buffer = bitWriter(write => writeSet(write, colorBits, customOutlines, set)); - // console.log(map(buffer, x => x).map(x => x.toString(2).padStart(8, '0')).join(' ')); - const result = readSet(bitReader(buffer), colorBits, customOutlines); - expect(result).eql(set); - })); - }); + tests.forEach(test => it(`works for set: ${JSON.stringify(test)}`, () => { + const [set, colorBits, customOutlines] = test; + const buffer = bitWriter(write => writeSet(write, colorBits, customOutlines, set)); + // console.log(map(buffer, x => x).map(x => x.toString(2).padStart(8, '0')).join(' ')); + const result = readSet(bitReader(buffer), colorBits, customOutlines); + expect(result).eql(set); + })); + }); - describe('writePony() + readPony()', () => { - const set1: PrecompressedSet = { - type: 1, pattern: 1, colors: 1, fillLocks: 0, fills: [1], outlineLocks: 0, outlines: [] - }; - const set2: PrecompressedSet = { - type: 7, pattern: 6, colors: 3, fillLocks: 1, fills: [1, 2], outlineLocks: 2, outlines: [3, 4] - }; - const base: Precompressed = { - version: VERSION, - colors: [], - booleanFields: [], - numberFields: [], - colorFields: [], - setFields: [], - cm: [], - }; + describe('writePony() + readPony()', () => { + const set1: PrecompressedSet = { + type: 1, pattern: 1, colors: 1, fillLocks: 0, fills: [1], outlineLocks: 0, outlines: [] + }; + const set2: PrecompressedSet = { + type: 7, pattern: 6, colors: 3, fillLocks: 1, fills: [1, 2], outlineLocks: 2, outlines: [3, 4] + }; + const base: Precompressed = { + version: VERSION, + colors: [], + booleanFields: [], + numberFields: [], + colorFields: [], + setFields: [], + cm: [], + }; - const tests: Precompressed[] = [ - base, - merge({}, base, { colors: [RED, BLUE, GREEN] }), - merge({}, base, { colors: [RED, BLUE, GREEN], cm: [1, 2, 1, 1, 2] }), - merge({}, base, { colors: [RED, BLUE, GREEN], setFields: [set1] }), - merge({}, base, { colors: [RED, BLUE, GREEN, ORANGE, CYAN], setFields: { nose: set2 }, booleanFields: [true] }), - merge({}, base, { colors: [RED, BLUE, GREEN], cm: [1, 2, 1, 1, 2], booleanFields: [true, false, true, false] }), - merge({}, base, { booleanFields: [true, false] }), - ]; + const tests: Precompressed[] = [ + base, + merge({}, base, { colors: [RED, BLUE, GREEN] }), + merge({}, base, { colors: [RED, BLUE, GREEN], cm: [1, 2, 1, 1, 2] }), + merge({}, base, { colors: [RED, BLUE, GREEN], setFields: [set1] }), + merge({}, base, { colors: [RED, BLUE, GREEN, ORANGE, CYAN], setFields: { nose: set2 }, booleanFields: [true] }), + merge({}, base, { colors: [RED, BLUE, GREEN], cm: [1, 2, 1, 1, 2], booleanFields: [true, false, true, false] }), + merge({}, base, { booleanFields: [true, false] }), + ]; - tests.forEach(test => it(`works for pony: ${JSON.stringify(test)}`, () => { - const buffer = bitWriter(write => writePony(write, test)); - const result = readPony(bitReader(buffer)); - expect(result).eql(test); - })); - }); + tests.forEach(test => it(`works for pony: ${JSON.stringify(test)}`, () => { + const buffer = bitWriter(write => writePony(write, test)); + const result = readPony(bitReader(buffer)); + expect(result).eql(test); + })); + }); - describe('precompressCM()', () => { - it('converts colors to indexes', () => { - const cm = [RED, RED, RED]; - expect(precompressCM(cm, () => 1)).eql([1, 1, 1]); - }); + describe('precompressCM()', () => { + it('converts colors to indexes', () => { + const cm = [RED, RED, RED]; + expect(precompressCM(cm, () => 1)).eql([1, 1, 1]); + }); - it('handles holey arrays', () => { - const cm = [RED, , , RED]; - expect(precompressCM(cm, c => c === undefined ? 0 : 1)).eql([1, 0, 0, 1]); - }); + it('handles holey arrays', () => { + const cm = [RED, , , RED]; + expect(precompressCM(cm, c => c === undefined ? 0 : 1)).eql([1, 0, 0, 1]); + }); - it('empty CM', () => { - expect(precompressCM([], () => 1)).eql([]); - }); + it('empty CM', () => { + expect(precompressCM([], () => 1)).eql([]); + }); - it('trims CM', () => { - const cm = [RED, RED, RED, TRANSPARENT, TRANSPARENT]; - expect(precompressCM(cm, () => 1)).eql([1, 1, 1]); - }); + it('trims CM', () => { + const cm = [RED, RED, RED, TRANSPARENT, TRANSPARENT]; + expect(precompressCM(cm, () => 1)).eql([1, 1, 1]); + }); - it('trims CM size to 25', () => { - const cm = repeat(50, RED); - expect(precompressCM(cm, () => 1).length).equal(25); - }); - }); + it('trims CM size to 25', () => { + const cm = repeat(50, RED); + expect(precompressCM(cm, () => 1).length).equal(25); + }); + }); - describe('precompressPony() + postdecompressPony()', () => { - const BASE = base(BLACK, WHITE); + describe('precompressPony() + postdecompressPony()', () => { + const BASE = base(BLACK, WHITE); - function test(input: Partial, expected?: Partial) { - return () => { - const data = precompressPony(input as any, BLACK, x => x); - const result1 = postdecompressPony(data, x => x); - const result2 = fastPostdecompressPony(data); - expect(result1).eql({ ...BASE, ...(expected || input) }, 'postdecompressPony'); - expect(result2).eql({ ...BASE, ...(expected || input) }, 'fastPostdecompressPony'); - }; - } + function test(input: Partial, expected?: Partial) { + return () => { + const data = precompressPony(input as any, BLACK, x => x); + const result1 = postdecompressPony(data, x => x); + const result2 = fastPostdecompressPony(data); + expect(result1).eql({ ...BASE, ...(expected || input) }, 'postdecompressPony'); + expect(result2).eql({ ...BASE, ...(expected || input) }, 'fastPostdecompressPony'); + }; + } - it('empty', test({}, { - coatOutline: undefined, - eyeWhitesLeft: undefined, - eyelashColorLeft: undefined, - })); + it('empty', test({}, { + coatOutline: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); - it('colors', test({ - coatFill: RED, - coatOutline: RED, - eyeColorLeft: RED, - eyeColorRight: RED, - eyeWhites: RED, - eyeshadowColor: RED, - frecklesColor: undefined, - customOutlines: true, - freckles: 0, - eyeOpennessLeft: 0, - eyeOpennessRight: 0, - fangs: 0, - muzzle: 0, - eyelashes: 0, - eyeshadow: true, - lockEyes: false, - lockEyeColor: false, - lockCoatOutline: false, - eyeWhitesLeft: undefined, - eyelashColorLeft: undefined, - })); + it('colors', test({ + coatFill: RED, + coatOutline: RED, + eyeColorLeft: RED, + eyeColorRight: RED, + eyeWhites: RED, + eyeshadowColor: RED, + frecklesColor: undefined, + customOutlines: true, + freckles: 0, + eyeOpennessLeft: 0, + eyeOpennessRight: 0, + fangs: 0, + muzzle: 0, + eyelashes: 0, + eyeshadow: true, + lockEyes: false, + lockEyeColor: false, + lockCoatOutline: false, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); - it('booleans', test({ - customOutlines: true, - lockEyes: true, - lockEyeColor: true, - lockCoatOutline: true, - }, { - customOutlines: true, - lockEyes: true, - lockEyeColor: true, - lockCoatOutline: true, - coatOutline: undefined, - eyeColorLeft: undefined, - eyeOpennessLeft: undefined, - eyeWhitesLeft: undefined, - eyelashColorLeft: undefined, - })); + it('booleans', test({ + customOutlines: true, + lockEyes: true, + lockEyeColor: true, + lockCoatOutline: true, + }, { + customOutlines: true, + lockEyes: true, + lockEyeColor: true, + lockCoatOutline: true, + coatOutline: undefined, + eyeColorLeft: undefined, + eyeOpennessLeft: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); - it('removes lockBackLegAccessory', test({ - lockBackLegAccessory: true - }, { - coatOutline: undefined, - eyeWhitesLeft: undefined, - eyelashColorLeft: undefined, - })); + it('removes lockBackLegAccessory', test({ + lockBackLegAccessory: true + }, { + coatOutline: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); - it('set', test({ - customOutlines: true, - lockCoatOutline: true, - coatOutline: undefined, - eyeWhitesLeft: undefined, - eyelashColorLeft: undefined, - tail: { - type: 1, - pattern: 1, - fills: [RED, BLUE, GREEN, ORANGE, CYAN], - lockFills: [false, false, false, false, false, false], - outlines: [RED, RED, RED, RED, RED], - lockOutlines: [false, false, false, false, false, true], - } - })); + it('set', test({ + customOutlines: true, + lockCoatOutline: true, + coatOutline: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + tail: { + type: 1, + pattern: 1, + fills: [RED, BLUE, GREEN, ORANGE, CYAN], + lockFills: [false, false, false, false, false, false], + outlines: [RED, RED, RED, RED, RED], + lockOutlines: [false, false, false, false, false, true], + } + })); - it('missing set fields', test({ - customOutlines: true, - tail: { - type: 1, - pattern: 1 - } - }, { - customOutlines: true, - lockCoatOutline: false, - eyeWhitesLeft: undefined, - eyelashColorLeft: undefined, - tail: { - type: 1, - pattern: 1, - fills: [BLACK, BLACK, BLACK, BLACK, BLACK], - lockFills: [false, false, false, false, false, false], - outlines: [BLACK, BLACK, BLACK, BLACK, BLACK], - lockOutlines: [false, false, false, false, false, true], - } - })); + it('missing set fields', test({ + customOutlines: true, + tail: { + type: 1, + pattern: 1 + } + }, { + customOutlines: true, + lockCoatOutline: false, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + tail: { + type: 1, + pattern: 1, + fills: [BLACK, BLACK, BLACK, BLACK, BLACK], + lockFills: [false, false, false, false, false, false], + outlines: [BLACK, BLACK, BLACK, BLACK, BLACK], + lockOutlines: [false, false, false, false, false, true], + } + })); - it('missing set color', test({ - customOutlines: true, - tail: { - type: 1, - pattern: 1, - fills: [RED, BLUE, undefined, ORANGE, CYAN], - lockFills: [false, false, false, false, false, false], - outlines: [RED, undefined, RED, RED, RED], - lockOutlines: [false, false, false, false, false, true], - } - }, { - customOutlines: true, - lockCoatOutline: false, - eyeWhitesLeft: undefined, - eyelashColorLeft: undefined, - tail: { - type: 1, - pattern: 1, - fills: [RED, BLUE, BLACK, ORANGE, CYAN], - lockFills: [false, false, false, false, false, false], - outlines: [RED, BLACK, RED, RED, RED], - lockOutlines: [false, false, false, false, false, true], - } - })); + it('missing set color', test({ + customOutlines: true, + tail: { + type: 1, + pattern: 1, + fills: [RED, BLUE, undefined, ORANGE, CYAN], + lockFills: [false, false, false, false, false, false], + outlines: [RED, undefined, RED, RED, RED], + lockOutlines: [false, false, false, false, false, true], + } + }, { + customOutlines: true, + lockCoatOutline: false, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + tail: { + type: 1, + pattern: 1, + fills: [RED, BLUE, BLACK, ORANGE, CYAN], + lockFills: [false, false, false, false, false, false], + outlines: [RED, BLACK, RED, RED, RED], + lockOutlines: [false, false, false, false, false, true], + } + })); - it('cm', test({ - cm: [undefined, RED, undefined, BLUE] as any, - }, { - cm: [TRANSPARENT, RED, TRANSPARENT, BLUE], - cmFlip: false, - coatOutline: undefined, - eyeWhitesLeft: undefined, - eyelashColorLeft: undefined, - })); - }); + it('cm', test({ + cm: [undefined, RED, undefined, BLUE] as any, + }, { + cm: [TRANSPARENT, RED, TRANSPARENT, BLUE], + cmFlip: false, + coatOutline: undefined, + eyeWhitesLeft: undefined, + eyelashColorLeft: undefined, + })); + }); - describe('decompressPony()', () => { - it('works for empty string', () => { - expect(decompressPony('')).eql(base(BLACK, WHITE)); - }); + describe('decompressPony()', () => { + it('works for empty string', () => { + expect(decompressPony('')).eql(base(BLACK, WHITE)); + }); - it('decompresses a pony', () => { - const pony = decompressPony('CAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA=='); + it('decompresses a pony', () => { + const pony = decompressPony('CAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA=='); - expect(pony.coatFill).equal(0x959595ff); - expect(pony.mane!.type).equal(2); - expect(pony.mane!.pattern).equal(0); - expect(pony.mane!.fills).eql([0x2f2f2fff]); - }); + expect(pony.coatFill).equal(0x959595ff); + expect(pony.mane!.type).equal(2); + expect(pony.mane!.pattern).equal(0); + expect(pony.mane!.fills).eql([0x2f2f2fff]); + }); - it('decompresses a pony from buffer', () => { - const pony = decompressPony(toByteArray('CAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA==')); + it('decompresses a pony from buffer', () => { + const pony = decompressPony(toByteArray('CAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA==')); - expect(pony.coatFill).equal(0x959595ff); - expect(pony.mane!.type).equal(2); - expect(pony.mane!.pattern).equal(0); - expect(pony.mane!.fills).eql([0x2f2f2fff]); - }); - }); + expect(pony.coatFill).equal(0x959595ff); + expect(pony.mane!.type).equal(2); + expect(pony.mane!.pattern).equal(0); + expect(pony.mane!.fills).eql([0x2f2f2fff]); + }); + }); - describe('decompressPonyString()', () => { - it('works for empty string', () => { - expect(decompressPonyString('')).eql(base('000000', 'ffffff')); - }); + describe('decompressPonyString()', () => { + it('works for empty string', () => { + expect(decompressPonyString('')).eql(base('000000', 'ffffff')); + }); - it('works for empty string (editable: true)', () => { - decompressPonyString('', true); - }); - }); + it('works for empty string (editable: true)', () => { + decompressPonyString('', true); + }); + }); - describe('compressPony() + decompressPony()', () => { - const BASE = base('000000', 'ffffff'); + describe('compressPony() + decompressPony()', () => { + const BASE = base('000000', 'ffffff'); - function test(input: Partial, expected?: Partial) { - return () => { - const data = compressPonyString(input as any); - const result = decompressPonyString(data, false); - expect(result).eql({ ...BASE, ...(expected || input) }); - }; - } + function test(input: Partial, expected?: Partial) { + return () => { + const data = compressPonyString(input as any); + const result = decompressPonyString(data, false); + expect(result).eql({ ...BASE, ...(expected || input) }); + }; + } - it('empty', test({}, {})); + it('empty', test({}, {})); - it('coatFill', test({ coatFill: 'ff0000' }, { coatFill: 'ff0000', coatOutline: 'b30000' })); + it('coatFill', test({ coatFill: 'ff0000' }, { coatFill: 'ff0000', coatOutline: 'b30000' })); - it('eyeColorLeft', test({ - eyeColorLeft: 'ff00ff', - }, { - coatFill: '000000', - coatOutline: '000000', - eyeColorLeft: 'ff00ff', - eyeColorRight: '000000', - })); + it('eyeColorLeft', test({ + eyeColorLeft: 'ff00ff', + }, { + coatFill: '000000', + coatOutline: '000000', + eyeColorLeft: 'ff00ff', + eyeColorRight: '000000', + })); - it('eyeColorLeft + eyeColorRight', test({ - eyeColorLeft: 'ff00ff', - eyeColorRight: '00ff00', - }, { - coatFill: '000000', - coatOutline: '000000', - eyeColorLeft: 'ff00ff', - eyeColorRight: '00ff00', - })); + it('eyeColorLeft + eyeColorRight', test({ + eyeColorLeft: 'ff00ff', + eyeColorRight: '00ff00', + }, { + coatFill: '000000', + coatOutline: '000000', + eyeColorLeft: 'ff00ff', + eyeColorRight: '00ff00', + })); - it('eyeColorLeft + eyeColorRight (locked)', test({ - lockEyeColor: true, - eyeColorLeft: '00ff00', - eyeColorRight: 'ff00ff', - }, { - lockEyes: false, - lockEyeColor: true, - customOutlines: false, - coatFill: '000000', - coatOutline: '000000', - eyeColorLeft: 'ff00ff', - eyeColorRight: 'ff00ff', - })); + it('eyeColorLeft + eyeColorRight (locked)', test({ + lockEyeColor: true, + eyeColorLeft: '00ff00', + eyeColorRight: 'ff00ff', + }, { + lockEyes: false, + lockEyeColor: true, + customOutlines: false, + coatFill: '000000', + coatOutline: '000000', + eyeColorLeft: 'ff00ff', + eyeColorRight: 'ff00ff', + })); - it('eyeshadow', test({ - eyeshadow: true, - eyeshadowColor: 'ff00ff', - }, { - eyeshadow: true, - eyeshadowColor: 'ff00ff', - coatFill: '000000', - coatOutline: '000000', - eyeColorLeft: '000000', - eyeColorRight: '000000', - eyeWhites: 'ffffff', - lockEyes: false, - lockEyeColor: false, - customOutlines: false, - })); + it('eyeshadow', test({ + eyeshadow: true, + eyeshadowColor: 'ff00ff', + }, { + eyeshadow: true, + eyeshadowColor: 'ff00ff', + coatFill: '000000', + coatOutline: '000000', + eyeColorLeft: '000000', + eyeColorRight: '000000', + eyeWhites: 'ffffff', + lockEyes: false, + lockEyeColor: false, + customOutlines: false, + })); - it('cm', test({ cm: ['ff0000', '', '00ff00'] }, { cm: ['ff0000', '', '00ff00'], cmFlip: false })); + it('cm', test({ cm: ['ff0000', '', '00ff00'] }, { cm: ['ff0000', '', '00ff00'], cmFlip: false })); - it('cm (flip)', test({ - cm: ['ff0000', '', '00ff00'], - cmFlip: true, - }, { - cm: ['ff0000', '', '00ff00'], - cmFlip: true, - })); + it('cm (flip)', test({ + cm: ['ff0000', '', '00ff00'], + cmFlip: true, + }, { + cm: ['ff0000', '', '00ff00'], + cmFlip: true, + })); - it('all locked', test({ - tail: { - type: 2, - pattern: 1, - fills: [], - lockFills: [true, true, true, true, true, true], - } - }, { - tail: { - type: 2, - pattern: 1, - fills: [undefined, undefined, undefined, undefined, undefined, undefined], - lockFills: [true, true, true, true, true, true], - outlines: [undefined, undefined, undefined, undefined, undefined, undefined], - lockOutlines: [true, true, true, true, true, true], - } - })); + it('all locked', test({ + tail: { + type: 2, + pattern: 1, + fills: [], + lockFills: [true, true, true, true, true, true], + } + }, { + tail: { + type: 2, + pattern: 1, + fills: [undefined, undefined, undefined, undefined, undefined, undefined], + lockFills: [true, true, true, true, true, true], + outlines: [undefined, undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); - it('set', test({ - mane: { - type: 1, - pattern: 1, - fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0', 'ff000f'], - lockFills: [false, false, false, false, false, false], - } - }, { - mane: { - type: 1, - pattern: 1, - fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0', 'ff000f'], - lockFills: [false, false, false, false, false, false], - outlines: ['a8a8a8', 'b30000', 'b3a800', 'b30b00', 'b300a8', 'b3000a'], - lockOutlines: [true, true, true, true, true, true], - } - })); + it('set', test({ + mane: { + type: 1, + pattern: 1, + fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0', 'ff000f'], + lockFills: [false, false, false, false, false, false], + } + }, { + mane: { + type: 1, + pattern: 1, + fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0', 'ff000f'], + lockFills: [false, false, false, false, false, false], + outlines: ['a8a8a8', 'b30000', 'b3a800', 'b30b00', 'b300a8', 'b3000a'], + lockOutlines: [true, true, true, true, true, true], + } + })); - it('0 colors', test({ - ears: { - type: 0, - pattern: 0, - fills: ['000000'], - lockFills: [false], - } - }, { - ears: { - type: 0, - pattern: 0, - fills: ['000000'], - lockFills: [false, false, false, false, false, false], - outlines: ['000000', undefined, undefined, undefined, undefined, undefined], - lockOutlines: [true, true, true, true, true, true], - } - })); + it('0 colors', test({ + ears: { + type: 0, + pattern: 0, + fills: ['000000'], + lockFills: [false], + } + }, { + ears: { + type: 0, + pattern: 0, + fills: ['000000'], + lockFills: [false, false, false, false, false, false], + outlines: ['000000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); - it('extraAccessory', test({ - extraAccessory: { - type: 0, - pattern: 0, - fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0'], - lockFills: [false, false, false, false, false], - } - }, { - extraAccessory: { - type: 0, - pattern: 0, - fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0'], - lockFills: [false, false, false, false, false, false], - outlines: ['a8a8a8', 'b30000', 'b3a800', 'b30b00', 'b300a8', undefined], - lockOutlines: [true, true, true, true, true, true], - } - })); + it('extraAccessory', test({ + extraAccessory: { + type: 0, + pattern: 0, + fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0'], + lockFills: [false, false, false, false, false], + } + }, { + extraAccessory: { + type: 0, + pattern: 0, + fills: ['f0f0f0', 'ff0000', 'fff000', 'ff0f00', 'ff00f0'], + lockFills: [false, false, false, false, false, false], + outlines: ['a8a8a8', 'b30000', 'b3a800', 'b30b00', 'b300a8', undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); - it('neckAccessory', test({ - neckAccessory: { type: 0, pattern: 0 } - }, {})); + it('neckAccessory', test({ + neckAccessory: { type: 0, pattern: 0 } + }, {})); - it('locked fills', test({ - mane: { - type: 1, - pattern: 0, - fills: ['ff0000'], - lockFills: [], - }, - backMane: { - type: 15, - pattern: 1, - fills: ['ffffff', '00ff00', 'ffffff'], - lockFills: [true, false, true], - }, - }, { - mane: { - type: 1, - pattern: 0, - fills: ['ff0000'], - lockFills: [false, false, false, false, false, false], - outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], - lockOutlines: [true, true, true, true, true, true], - }, - backMane: { - type: 15, - pattern: 1, - fills: ['ff0000', '00ff00', 'ff0000'], - lockFills: [true, false, true, false, false, false], - outlines: ['b30000', '00b300', 'b30000', undefined, undefined, undefined], - lockOutlines: [true, true, true, true, true, true], - }, - })); + it('locked fills', test({ + mane: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [], + }, + backMane: { + type: 15, + pattern: 1, + fills: ['ffffff', '00ff00', 'ffffff'], + lockFills: [true, false, true], + }, + }, { + mane: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + }, + backMane: { + type: 15, + pattern: 1, + fills: ['ff0000', '00ff00', 'ff0000'], + lockFills: [true, false, true, false, false, false], + outlines: ['b30000', '00b300', 'b30000', undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + }, + })); - it('locked back hooves', test({ - frontHooves: { - type: 0, - pattern: 0, - fills: ['ff0000'], - }, - backHooves: { - type: 1, - pattern: 0, - lockFills: [true], - } - }, { - frontHooves: { - type: 0, - pattern: 0, - fills: ['ff0000'], - lockFills: [false, false, false, false, false, false], - outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], - lockOutlines: [true, true, true, true, true, true], - }, - backHooves: { - type: 1, - pattern: 0, - fills: ['ff0000'], - lockFills: [true, false, false, false, false, false], - outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], - lockOutlines: [true, true, true, true, true, true], - } - })); + it('locked back hooves', test({ + frontHooves: { + type: 0, + pattern: 0, + fills: ['ff0000'], + }, + backHooves: { + type: 1, + pattern: 0, + lockFills: [true], + } + }, { + frontHooves: { + type: 0, + pattern: 0, + fills: ['ff0000'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + }, + backHooves: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [true, false, false, false, false, false], + outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); - it('back mane', test({ - mane: { - type: 3, - pattern: 1, - fills: ['000000', 'ffffff'], - }, - backMane: { - type: 2, - pattern: 1, - fills: ['ffffff', 'ff0000'], - lockFills: [false, false, true, true, true, true], - } - }, { - mane: { - type: 3, - pattern: 1, - fills: ['000000', 'ffffff', '000000', '000000', '000000', '000000'], - lockFills: [false, false, false, false, false, false], - outlines: ['000000', 'b3b3b3', '000000', '000000', '000000', '000000'], - lockOutlines: [true, true, true, true, true, true], - }, - backMane: { - type: 2, - pattern: 1, - fills: ['ffffff', 'ff0000', 'ffffff', 'ffffff', 'ffffff', 'ffffff'], - lockFills: [false, false, true, true, true, true], - outlines: ['b3b3b3', 'b30000', 'b3b3b3', 'b3b3b3', 'b3b3b3', 'b3b3b3'], - lockOutlines: [true, true, true, true, true, true], - } - })); + it('back mane', test({ + mane: { + type: 3, + pattern: 1, + fills: ['000000', 'ffffff'], + }, + backMane: { + type: 2, + pattern: 1, + fills: ['ffffff', 'ff0000'], + lockFills: [false, false, true, true, true, true], + } + }, { + mane: { + type: 3, + pattern: 1, + fills: ['000000', 'ffffff', '000000', '000000', '000000', '000000'], + lockFills: [false, false, false, false, false, false], + outlines: ['000000', 'b3b3b3', '000000', '000000', '000000', '000000'], + lockOutlines: [true, true, true, true, true, true], + }, + backMane: { + type: 2, + pattern: 1, + fills: ['ffffff', 'ff0000', 'ffffff', 'ffffff', 'ffffff', 'ffffff'], + lockFills: [false, false, true, true, true, true], + outlines: ['b3b3b3', 'b30000', 'b3b3b3', 'b3b3b3', 'b3b3b3', 'b3b3b3'], + lockOutlines: [true, true, true, true, true, true], + } + })); - it('chest accessory (adds default sleeve if missing)', test({ - chestAccessory: { - type: 2, - pattern: 8, - fills: ['ff0000', '00ff00', '0000ff'], - } - }, { - chestAccessory: { - type: 2, - pattern: 8, - fills: ['ff0000', '00ff00', '0000ff'], - lockFills: [false, false, false, false, false, false], - outlines: ['b30000', '00b300', '0000b3', undefined, undefined, undefined], - lockOutlines: [true, true, true, true, true, true], - }, - sleeveAccessory: { - type: 0, - pattern: 0, - fills: ['ff0000', 'ff0000', 'ff0000', 'ff0000', 'ff0000', 'ff0000'], - lockFills: [true, true, true, true, true, true], - outlines: ['b30000', 'b30000', 'b30000', 'b30000', 'b30000', 'b30000'], - lockOutlines: [true, true, true, true, true, true], - } - })); + it('chest accessory (adds default sleeve if missing)', test({ + chestAccessory: { + type: 2, + pattern: 8, + fills: ['ff0000', '00ff00', '0000ff'], + } + }, { + chestAccessory: { + type: 2, + pattern: 8, + fills: ['ff0000', '00ff00', '0000ff'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', '00b300', '0000b3', undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + }, + sleeveAccessory: { + type: 0, + pattern: 0, + fills: ['ff0000', 'ff0000', 'ff0000', 'ff0000', 'ff0000', 'ff0000'], + lockFills: [true, true, true, true, true, true], + outlines: ['b30000', 'b30000', 'b30000', 'b30000', 'b30000', 'b30000'], + lockOutlines: [true, true, true, true, true, true], + } + })); - it('back leg accessory', test({ - backLegAccessory: { - type: 1, - pattern: 0, - fills: ['ff0000'], - lockFills: [false], - } - }, { - lockBackLegAccessory: false, - backLegAccessory: { - type: 1, - pattern: 0, - fills: ['ff0000'], - lockFills: [false, false, false, false, false, false], - outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], - lockOutlines: [true, true, true, true, true, true], - } - })); + it('back leg accessory', test({ + backLegAccessory: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [false], + } + }, { + lockBackLegAccessory: false, + backLegAccessory: { + type: 1, + pattern: 0, + fills: ['ff0000'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', undefined, undefined, undefined, undefined, undefined], + lockOutlines: [true, true, true, true, true, true], + } + })); - it('cm (with undefined)', test({ - cm: [ - , , , 'fde9cd', , - 'fde9cd', , , , , - , , 'fde9cd', , 'fde9cd', - , , , , , - , , 'fde9cd', , - ] as any - }, { - cm: [ - '', '', '', 'fde9cd', '', - 'fde9cd', '', '', '', '', - '', '', 'fde9cd', '', 'fde9cd', - '', '', '', '', '', - '', '', 'fde9cd' - ], - cmFlip: false, - })); + it('cm (with undefined)', test({ + cm: [ + , , , 'fde9cd', , + 'fde9cd', , , , , + , , 'fde9cd', , 'fde9cd', + , , , , , + , , 'fde9cd', , + ] as any + }, { + cm: [ + '', '', '', 'fde9cd', '', + 'fde9cd', '', '', '', '', + '', '', 'fde9cd', '', 'fde9cd', + '', '', '', '', '', + '', '', 'fde9cd' + ], + cmFlip: false, + })); - it('back leg accessory (editable)', () => { - const data = compressPonyString({ - lockBackLegAccessory: false, - backLegAccessory: { - type: 1, - pattern: 1, - fills: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff', 'ff00ff'], - lockFills: [false], - } - } as any); + it('back leg accessory (editable)', () => { + const data = compressPonyString({ + lockBackLegAccessory: false, + backLegAccessory: { + type: 1, + pattern: 1, + fills: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff', 'ff00ff'], + lockFills: [false], + } + } as any); - const result = decompressPonyString(data, true); + const result = decompressPonyString(data, true); - expect(result.lockBackLegAccessory).false; - expect(result.backLegAccessory).eql({ - type: 1, - pattern: 1, - fills: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff', 'ff00ff'], - lockFills: [false, false, false, false, false, false], - outlines: ['b30000', '00b300', '0000b3', 'b3b300', '00b3b3', 'b300b3'], - lockOutlines: [true, true, true, true, true, true], - }); - }); + expect(result.lockBackLegAccessory).false; + expect(result.backLegAccessory).eql({ + type: 1, + pattern: 1, + fills: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff', 'ff00ff'], + lockFills: [false, false, false, false, false, false], + outlines: ['b30000', '00b300', '0000b3', 'b3b300', '00b3b3', 'b300b3'], + lockOutlines: [true, true, true, true, true, true], + }); + }); - it('black colors (editable)', () => { - const data = compressPonyString({ coatFill: '000000' } as any); - const result = decompressPonyString(data, true); - expect(result.coatFill).equal('000000'); - }); + it('black colors (editable)', () => { + const data = compressPonyString({ coatFill: '000000' } as any); + const result = decompressPonyString(data, true); + expect(result.coatFill).equal('000000'); + }); - it('neckAccessory: { type: 0 }', () => { - const data = compressPonyString({ neckAccessory: { type: 0, pattern: 0 } } as any); - const result = decompressPonyString(data, true); - expect(result.neckAccessory!.type).eql(0); - }); + it('neckAccessory: { type: 0 }', () => { + const data = compressPonyString({ neckAccessory: { type: 0, pattern: 0 } } as any); + const result = decompressPonyString(data, true); + expect(result.neckAccessory!.type).eql(0); + }); - it('mane: { type: 0 }', () => { - const data = compressPonyString({ mane: { type: 0, pattern: 0 } } as any); - const result = decompressPonyString(data, true); - expect(result.mane!.type).eql(0); - }); + it('mane: { type: 0 }', () => { + const data = compressPonyString({ mane: { type: 0, pattern: 0 } } as any); + const result = decompressPonyString(data, true); + expect(result.mane!.type).eql(0); + }); - // fs.readdirSync(poniesPath).forEach(f => it(`(${f})`, () => { - // const json = JSON.parse(fs.readFileSync(path.join(poniesPath, f), 'utf8')); - // const data = compressPonyString(json); - // const result = decompressPonyString(data, true); - // expect(result).eql(json); - // })); + // fs.readdirSync(poniesPath).forEach(f => it(`(${f})`, () => { + // const json = JSON.parse(fs.readFileSync(path.join(poniesPath, f), 'utf8')); + // const data = compressPonyString(json); + // const result = decompressPonyString(data, true); + // expect(result).eql(json); + // })); - it.skip('error test', () => { - const json = JSON.parse(fs.readFileSync(pathTo('tools', 'data', 'error-1504869659641.json'), 'utf8')); - const infoJson = json.data.info; - const compressedTemp = 'CAjNzc3////apSD/1wAekP8yzTLacNbcFDw+oCoACJiRngCBNET8ADjAcAAlSUCrPH6QGAA='; - console.log(Array.from(toByteArray(compressedTemp)).map(x => x.toString(16).padStart(2, '0')).join(' ')); - console.log(Array.from(toByteArray(json.data.compressed)).map(x => x.toString(16).padStart(2, '0')).join(' ')); - const compressed = compressPonyString(infoJson); - //const result = decompressPonyString(compressed, true); - expect(compressed).eql(json.data.compressed); - }); - }); + it.skip('error test', () => { + const json = JSON.parse(fs.readFileSync(pathTo('tools', 'data', 'error-1504869659641.json'), 'utf8')); + const infoJson = json.data.info; + const compressedTemp = 'CAjNzc3////apSD/1wAekP8yzTLacNbcFDw+oCoACJiRngCBNET8ADjAcAAlSUCrPH6QGAA='; + console.log(Array.from(toByteArray(compressedTemp)).map(x => x.toString(16).padStart(2, '0')).join(' ')); + console.log(Array.from(toByteArray(json.data.compressed)).map(x => x.toString(16).padStart(2, '0')).join(' ')); + const compressed = compressPonyString(infoJson); + //const result = decompressPonyString(compressed, true); + expect(compressed).eql(json.data.compressed); + }); + }); }); diff --git a/src/ts/tests/common/encoders/updateEncoder.spec.ts b/src/ts/tests/common/encoders/updateEncoder.spec.ts index f187f4b..9f671ab 100644 --- a/src/ts/tests/common/encoders/updateEncoder.spec.ts +++ b/src/ts/tests/common/encoders/updateEncoder.spec.ts @@ -12,269 +12,269 @@ import { compressTiles } from '../../../common/compress'; import { REGION_SIZE } from '../../../common/constants'; describe('updateEncoder', () => { - describe('encodeUpdate() + decodeUpdate()', () => { - const def = { x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }; - const out = emptyUpdate(0); - const exp: DecodedRegionUpdate = { x: 0, y: 0, updates: [], removes: [], tiles: [], tileData: null }; - let region: ServerRegion; + describe('encodeUpdate() + decodeUpdate()', () => { + const def = { x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }; + const out = emptyUpdate(0); + const exp: DecodedRegionUpdate = { x: 0, y: 0, updates: [], removes: [], tiles: [], tileData: null }; + let region: ServerRegion; - function testEncodeDecode(input: ServerRegion, expected: DecodedRegionUpdate) { - const encoded = encodeUpdateSimple(input); - const decoded = decodeUpdate(encoded); - expect(decoded).eql(expected); - } + function testEncodeDecode(input: ServerRegion, expected: DecodedRegionUpdate) { + const encoded = encodeUpdateSimple(input); + const decoded = decodeUpdate(encoded); + expect(decoded).eql(expected); + } - beforeEach(() => { - region = createServerRegion(0, 0); - }); + beforeEach(() => { + region = createServerRegion(0, 0); + }); - it('encodes empty updates', () => { - testEncodeDecode(region, { ...exp }); - }); + it('encodes empty updates', () => { + testEncodeDecode(region, { ...exp }); + }); - it('encodes region x, y', () => { - region.x = 1; - region.y = 2; + it('encodes region x, y', () => { + region.x = 1; + region.y = 2; - testEncodeDecode(region, { ...exp, x: 1, y: 2 }); - }); + testEncodeDecode(region, { ...exp, x: 1, y: 2 }); + }); - it('encodes empty removes', () => { - testEncodeDecode(region, { ...exp, removes: [] }); - }); + it('encodes empty removes', () => { + testEncodeDecode(region, { ...exp, removes: [] }); + }); - it('encodes removes', () => { - region.entityRemoves.push(1, 2, 3); + it('encodes removes', () => { + region.entityRemoves.push(1, 2, 3); - testEncodeDecode(region, { ...exp, removes: [1, 2, 3] }); - }); + testEncodeDecode(region, { ...exp, removes: [1, 2, 3] }); + }); - it('encodes empty tiles', () => { - testEncodeDecode(region, { ...exp, tiles: [] }); - }); + it('encodes empty tiles', () => { + testEncodeDecode(region, { ...exp, tiles: [] }); + }); - it('encodes tiles', () => { - region.tileUpdates.push({ x: 1, y: 2, type: 3 }, { x: 7, y: 56, type: 2 }); + it('encodes tiles', () => { + region.tileUpdates.push({ x: 1, y: 2, type: 3 }, { x: 7, y: 56, type: 2 }); - testEncodeDecode(region, { ...exp, tiles: [{ x: 1, y: 2, type: 3 }, { x: 7, y: 56, type: 2 }] }); - }); + testEncodeDecode(region, { ...exp, tiles: [{ x: 1, y: 2, type: 3 }, { x: 7, y: 56, type: 2 }] }); + }); - it('encodes flags', () => { - region.entityUpdates.push({ ...def, entity: { ...entity(123), state: EntityState.PonySitting }, flags: UpdateFlags.State }); + it('encodes flags', () => { + region.entityUpdates.push({ ...def, entity: { ...entity(123), state: EntityState.PonySitting }, flags: UpdateFlags.State }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, state: EntityState.PonySitting }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, state: EntityState.PonySitting }] }); + }); - it('encodes flags with switch region', () => { - region.entityUpdates.push({ - ...def, entity: { ...entity(123), state: EntityState.PonySitting }, flags: UpdateFlags.State | UpdateFlags.SwitchRegion - }); + it('encodes flags with switch region', () => { + region.entityUpdates.push({ + ...def, entity: { ...entity(123), state: EntityState.PonySitting }, flags: UpdateFlags.State | UpdateFlags.SwitchRegion + }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, state: EntityState.PonySitting, switchRegion: true }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, state: EntityState.PonySitting, switchRegion: true }] }); + }); - it('encodes expression', () => { - region.entityUpdates.push({ ...def, entity: { ...entity(123), options: { expr: 555 } }, flags: UpdateFlags.Expression }); + it('encodes expression', () => { + region.entityUpdates.push({ ...def, entity: { ...entity(123), options: { expr: 555 } }, flags: UpdateFlags.Expression }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, expression: 555 }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, expression: 555 }] }); + }); - it('encodes position', () => { - region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11, y: 22 }); + it('encodes position', () => { + region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11, y: 22 }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: 11, y: 22, state: 0 }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: 11, y: 22, state: 0 }] }); + }); - it('encodes type', () => { - region.entityUpdates.push({ ...def, entity: entity(123, 0, 0, 111), flags: UpdateFlags.Type }); + it('encodes type', () => { + region.entityUpdates.push({ ...def, entity: entity(123, 0, 0, 111), flags: UpdateFlags.Type }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, type: 111 }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, type: 111 }] }); + }); - it('encodes options', () => { - region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Options, options: { tag: 'bar' } }); + it('encodes options', () => { + region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Options, options: { tag: 'bar' } }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, options: { tag: 'bar' } }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, options: { tag: 'bar' } }] }); + }); - it('encodes info', () => { - const e = serverEntity(123); - e.encryptedInfoSafe = new Uint8Array([1, 2, 3, 4, 5]); + it('encodes info', () => { + const e = serverEntity(123); + e.encryptedInfoSafe = new Uint8Array([1, 2, 3, 4, 5]); - region.entityUpdates.push({ ...def, entity: e, flags: UpdateFlags.Info }); + region.entityUpdates.push({ ...def, entity: e, flags: UpdateFlags.Info }); - const decoded = decodeUpdate(encodeUpdateSimple(region)); + const decoded = decodeUpdate(encodeUpdateSimple(region)); - expect(Array.from(decoded.updates[0].info!)).eql([1, 2, 3, 4, 5]); - }); + expect(Array.from(decoded.updates[0].info!)).eql([1, 2, 3, 4, 5]); + }); - it('encodes action', () => { - region.entityUpdates.push({ ...def, entity: entity(123), action: 5, flags: UpdateFlags.Action }); + it('encodes action', () => { + region.entityUpdates.push({ ...def, entity: entity(123), action: 5, flags: UpdateFlags.Action }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, action: 5 }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, action: 5 }] }); + }); - it('encodes name', () => { - const e = serverEntity(123); - e.encodedName = encodeString('foobar')!; + it('encodes name', () => { + const e = serverEntity(123); + e.encodedName = encodeString('foobar')!; - region.entityUpdates.push({ ...def, entity: e, flags: UpdateFlags.Name }); + region.entityUpdates.push({ ...def, entity: e, flags: UpdateFlags.Name }); - const decoded = decodeUpdate(encodeUpdateSimple(region)); + const decoded = decodeUpdate(encodeUpdateSimple(region)); - expect(decoded.updates[0].name).eql('foobar'); - }); + expect(decoded.updates[0].name).eql('foobar'); + }); - it('encodes bad name', () => { - const e = serverEntity(123); - e.encodedName = encodeString('foobar')!; - e.nameBad = true; + it('encodes bad name', () => { + const e = serverEntity(123); + e.encodedName = encodeString('foobar')!; + e.nameBad = true; - region.entityUpdates.push({ ...def, entity: e, flags: UpdateFlags.Name }); + region.entityUpdates.push({ ...def, entity: e, flags: UpdateFlags.Name }); - const decoded = decodeUpdate(encodeUpdateSimple(region)); + const decoded = decodeUpdate(encodeUpdateSimple(region)); - expect(decoded.updates[0].name).eql('foobar'); - expect(decoded.updates[0].filterName).true; - }); + expect(decoded.updates[0].name).eql('foobar'); + expect(decoded.updates[0].filterName).true; + }); - it('encodes position and velocity', () => { - region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11, y: 22, vx: 1, vy: 1 }); + it('encodes position and velocity', () => { + region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11, y: 22, vx: 1, vy: 1 }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: 11, y: 22, vx: 1, vy: 1, state: 0 }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: 11, y: 22, vx: 1, vy: 1, state: 0 }] }); + }); - it('encodes position and velocity (2)', () => { - region.entityUpdates.push({ - ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11.125, y: 22.5, vx: 0.125, vy: 2.5 - }); + it('encodes position and velocity (2)', () => { + region.entityUpdates.push({ + ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11.125, y: 22.5, vx: 0.125, vy: 2.5 + }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: 11.125, y: 22.5, vx: 0.125, vy: 2.5, state: 0 }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: 11.125, y: 22.5, vx: 0.125, vy: 2.5, state: 0 }] }); + }); - it('encodes position and velocity (3)', () => { - region.entityUpdates.push({ - ...def, entity: entity(123), flags: UpdateFlags.Position, x: -11.125, y: -22.5, vx: -0.125, vy: -2.5 - }); + it('encodes position and velocity (3)', () => { + region.entityUpdates.push({ + ...def, entity: entity(123), flags: UpdateFlags.Position, x: -11.125, y: -22.5, vx: -0.125, vy: -2.5 + }); - testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: -11.125, y: -22.5, vx: -0.125, vy: -2.5, state: 0 }] }); - }); + testEncodeDecode(region, { ...exp, updates: [{ ...out, id: 123, x: -11.125, y: -22.5, vx: -0.125, vy: -2.5, state: 0 }] }); + }); - it('throws on invalid velocity', () => { - region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Position, x: 0, y: 0, vx: 100, vy: 0 }); + it('throws on invalid velocity', () => { + region.entityUpdates.push({ ...def, entity: entity(123), flags: UpdateFlags.Position, x: 0, y: 0, vx: 100, vy: 0 }); - expect(() => testEncodeDecode(region, { ...exp, updates: [] })) - .throw('Exceeded max velocity (100)'); - }); + expect(() => testEncodeDecode(region, { ...exp, updates: [] })) + .throw('Exceeded max velocity (100)'); + }); - it('encodes update with all fields', () => { - region.entityUpdates.push({ - ...def, entity: { ...entity(123, 0, 0, 111), state: EntityState.PonySitting }, - flags: UpdateFlags.Position | UpdateFlags.Type | UpdateFlags.Options | UpdateFlags.Action, - x: 11, y: 22, vx: 1, vy: 1, action: 5, options: { tag: 'bar' }, - }); + it('encodes update with all fields', () => { + region.entityUpdates.push({ + ...def, entity: { ...entity(123, 0, 0, 111), state: EntityState.PonySitting }, + flags: UpdateFlags.Position | UpdateFlags.Type | UpdateFlags.Options | UpdateFlags.Action, + x: 11, y: 22, vx: 1, vy: 1, action: 5, options: { tag: 'bar' }, + }); - testEncodeDecode( - region, - { - ...exp, - updates: [{ - ...out, id: 123, type: 111, x: 11, y: 22, vx: 1, vy: 1, state: EntityState.PonySitting, - action: 5, playerState: undefined, options: { tag: 'bar' } - }] - }); - }); + testEncodeDecode( + region, + { + ...exp, + updates: [{ + ...out, id: 123, type: 111, x: 11, y: 22, vx: 1, vy: 1, state: EntityState.PonySitting, + action: 5, playerState: undefined, options: { tag: 'bar' } + }] + }); + }); - it('encodes multiple updates', () => { - region.entityUpdates.push( - { ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11, y: 22 }, - { ...def, entity: { ...entity(321), state: EntityState.PonySitting }, flags: UpdateFlags.State } - ); + it('encodes multiple updates', () => { + region.entityUpdates.push( + { ...def, entity: entity(123), flags: UpdateFlags.Position, x: 11, y: 22 }, + { ...def, entity: { ...entity(321), state: EntityState.PonySitting }, flags: UpdateFlags.State } + ); - testEncodeDecode( - region, - { - ...exp, - updates: [ - { ...out, id: 123, x: 11, y: 22, state: 0 }, - { ...out, id: 321, state: EntityState.PonySitting }, - ] - }); - }); - }); + testEncodeDecode( + region, + { + ...exp, + updates: [ + { ...out, id: 123, x: 11, y: 22, state: 0 }, + { ...out, id: 321, state: EntityState.PonySitting }, + ] + }); + }); + }); - describe('writeRegion() + decodeUpdate()', () => { - const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); - tiles.fill(TileType.Dirt); - const emptyTileData = compressTiles(tiles); + describe('writeRegion() + decodeUpdate()', () => { + const tiles = new Uint8Array(REGION_SIZE * REGION_SIZE); + tiles.fill(TileType.Dirt); + const emptyTileData = compressTiles(tiles); - function testEncodeDecode(region: ServerRegion, client: IClient, expected: DecodedRegionUpdate) { - const encoded = encodeRegionSimple(region, client); - const decoded = decodeUpdate(encoded); - expect(decoded).eql(expected); - } + function testEncodeDecode(region: ServerRegion, client: IClient, expected: DecodedRegionUpdate) { + const encoded = encodeRegionSimple(region, client); + const decoded = decodeUpdate(encoded); + expect(decoded).eql(expected); + } - it('empty region', () => { - const region = createServerRegion(1, 2); - const client = mockClient(); + it('empty region', () => { + const region = createServerRegion(1, 2); + const client = mockClient(); - testEncodeDecode(region, client, { x: 1, y: 2, removes: [], tiles: [], tileData: emptyTileData, updates: [] }); - }); + testEncodeDecode(region, client, { x: 1, y: 2, removes: [], tiles: [], tileData: emptyTileData, updates: [] }); + }); - it('encodes single entity', () => { - const region = createServerRegion(1, 2); - const entity = serverEntity(123, 10, 20, 32); - const client = mockClient(); - region.entities.push(entity); + it('encodes single entity', () => { + const region = createServerRegion(1, 2); + const entity = serverEntity(123, 10, 20, 32); + const client = mockClient(); + region.entities.push(entity); - testEncodeDecode(region, client, { - x: 1, y: 2, removes: [], tiles: [], tileData: emptyTileData, updates: [ - { - id: 123, x: 10, y: 20, vx: 0, vy: 0, type: 32, - name: undefined, switchRegion: false, crc: undefined, info: undefined, - state: 0, expression: undefined, action: undefined, options: undefined, - playerState: undefined, filterName: false, - }, - ] - }); - }); + testEncodeDecode(region, client, { + x: 1, y: 2, removes: [], tiles: [], tileData: emptyTileData, updates: [ + { + id: 123, x: 10, y: 20, vx: 0, vy: 0, type: 32, + name: undefined, switchRegion: false, crc: undefined, info: undefined, + state: 0, expression: undefined, action: undefined, options: undefined, + playerState: undefined, filterName: false, + }, + ] + }); + }); - it('encodes single entity with more fields', () => { - const region = createServerRegion(1, 2); - const entity = serverEntity(123, 10, 20, 32); - const client = mockClient(); - region.entities.push(entity); - setEntityName(entity, 'foo'); - const info = new Uint8Array([1, 2, 3]); - entity.client = mockClient(); - entity.state = 123; - entity.options = { toy: 5, expr: 123 }; - entity.encryptedInfoSafe = info; - entity.vx = 1; - entity.vy = 2; - client.hides.add(entity.client.accountId); + it('encodes single entity with more fields', () => { + const region = createServerRegion(1, 2); + const entity = serverEntity(123, 10, 20, 32); + const client = mockClient(); + region.entities.push(entity); + setEntityName(entity, 'foo'); + const info = new Uint8Array([1, 2, 3]); + entity.client = mockClient(); + entity.state = 123; + entity.options = { toy: 5, expr: 123 }; + entity.encryptedInfoSafe = info; + entity.vx = 1; + entity.vy = 2; + client.hides.add(entity.client.accountId); - testEncodeDecode(region, client, { - x: 1, y: 2, removes: [], tiles: [], tileData: emptyTileData, updates: [ - { - id: 123, x: 10, y: 20, vx: 1, vy: 2, type: 32, - name: 'foo', switchRegion: false, crc: 0, info, - state: 123, expression: undefined, action: undefined, options: { toy: 5, expr: 123 }, - playerState: 2, filterName: false, - }, - ], - }); - }); + testEncodeDecode(region, client, { + x: 1, y: 2, removes: [], tiles: [], tileData: emptyTileData, updates: [ + { + id: 123, x: 10, y: 20, vx: 1, vy: 2, type: 32, + name: 'foo', switchRegion: false, crc: 0, info, + state: 123, expression: undefined, action: undefined, options: { toy: 5, expr: 123 }, + playerState: 2, filterName: false, + }, + ], + }); + }); - it('skips shadowed entities', () => { - const region = createServerRegion(1, 2); - const entity = serverEntity(123, 10, 20, 32); - const client = mockClient(); - region.entities.push(entity); - entity.client = mockClient(); - entity.client.shadowed = true; + it('skips shadowed entities', () => { + const region = createServerRegion(1, 2); + const entity = serverEntity(123, 10, 20, 32); + const client = mockClient(); + region.entities.push(entity); + entity.client = mockClient(); + entity.client.shadowed = true; - testEncodeDecode(region, client, { x: 1, y: 2, removes: [], tiles: [], updates: [], tileData: emptyTileData }); - }); - }); + testEncodeDecode(region, client, { x: 1, y: 2, removes: [], tiles: [], updates: [], tileData: emptyTileData }); + }); + }); }); diff --git a/src/ts/tests/common/entityUtils.spec.ts b/src/ts/tests/common/entityUtils.spec.ts index f134c7c..9ac2fa6 100644 --- a/src/ts/tests/common/entityUtils.spec.ts +++ b/src/ts/tests/common/entityUtils.spec.ts @@ -4,36 +4,36 @@ import { entity } from '../mocks'; import { compareEntities } from '../../common/entityUtils'; describe('entityUtils [common]', () => { - describe('compareEntities()', () => { - it('compares by y position', () => { - const a = entity(0, 0, 1); - const b = entity(0, 0, 2); + describe('compareEntities()', () => { + it('compares by y position', () => { + const a = entity(0, 0, 1); + const b = entity(0, 0, 2); - expect(compareEntities(a, b)).lt(0); - expect(compareEntities(b, a)).gt(0); - }); + expect(compareEntities(a, b)).lt(0); + expect(compareEntities(b, a)).gt(0); + }); - it('compares by x position', () => { - const a = entity(0, 1, 1); - const b = entity(0, 2, 1); + it('compares by x position', () => { + const a = entity(0, 1, 1); + const b = entity(0, 2, 1); - expect(compareEntities(a, b)).lt(0); - expect(compareEntities(b, a)).gt(0); - }); + expect(compareEntities(a, b)).lt(0); + expect(compareEntities(b, a)).gt(0); + }); - it('compares by id', () => { - const a = entity(1, 1, 1); - const b = entity(2, 1, 1); + it('compares by id', () => { + const a = entity(1, 1, 1); + const b = entity(2, 1, 1); - expect(compareEntities(a, b)).gt(0); - expect(compareEntities(b, a)).lt(0); - }); + expect(compareEntities(a, b)).gt(0); + expect(compareEntities(b, a)).lt(0); + }); - it('returns 0 for identical entities', () => { - const a = entity(1, 1, 1); - const b = entity(1, 1, 1); + it('returns 0 for identical entities', () => { + const a = entity(1, 1, 1); + const b = entity(1, 1, 1); - expect(compareEntities(a, b)).equal(0); - }); - }); + expect(compareEntities(a, b)).equal(0); + }); + }); }); diff --git a/src/ts/tests/common/expressionUtils.spec.ts b/src/ts/tests/common/expressionUtils.spec.ts index 0b0233b..1ef9118 100644 --- a/src/ts/tests/common/expressionUtils.spec.ts +++ b/src/ts/tests/common/expressionUtils.spec.ts @@ -7,97 +7,97 @@ import { decodeExpression, encodeExpression } from '../../common/encoders/expres import { flipIris } from '../../client/ponyUtils'; function toExpression([right, left, muzzle, rightIris = 0, leftIris = 0, extra = 0]: any): Expression { - return { left, right, muzzle, rightIris, leftIris, extra }; + return { left, right, muzzle, rightIris, leftIris, extra }; } describe('expressionUtils', () => { - describe('parseExpression()', () => { - expressions.forEach(([input, expected]) => { - it(JSON.stringify(input), () => { - if (expected) { - expect(parseExpression(input)).eql(toExpression(expected)); - } else { - expect(parseExpression(input)).undefined; - } - }); - }); + describe('parseExpression()', () => { + expressions.forEach(([input, expected]) => { + it(JSON.stringify(input), () => { + if (expected) { + expect(parseExpression(input)).eql(toExpression(expected)); + } else { + expect(parseExpression(input)).undefined; + } + }); + }); - it('should return the same expression each time', () => { - const expected = { - right: Eye.ClosedHappy2, - left: Eye.ClosedHappy2, - muzzle: Muzzle.Smile, - rightIris: Iris.Forward, - leftIris: Iris.Forward, - extra: ExpressionExtra.None, - }; + it('should return the same expression each time', () => { + const expected = { + right: Eye.ClosedHappy2, + left: Eye.ClosedHappy2, + muzzle: Muzzle.Smile, + rightIris: Iris.Forward, + leftIris: Iris.Forward, + extra: ExpressionExtra.None, + }; - const expr = parseExpression('^^'); - expect(expr).eql(expected, '1st'); - expr!.extra = 999; + const expr = parseExpression('^^'); + expect(expr).eql(expected, '1st'); + expr!.extra = 999; - expect(parseExpression('^^')).eql(expected, '2nd'); - }); + expect(parseExpression('^^')).eql(expected, '2nd'); + }); - it('should return nothing for "constructor" expression', () => { - expect(parseExpression('constructor')).undefined; - }); - }); + it('should return nothing for "constructor" expression', () => { + expect(parseExpression('constructor')).undefined; + }); + }); - describe('encodeExpression() + decodeExpression()', () => { - function test(expression: Expression | undefined) { - return decodeExpression(encodeExpression(expression)); - } + describe('encodeExpression() + decodeExpression()', () => { + function test(expression: Expression | undefined) { + return decodeExpression(encodeExpression(expression)); + } - it('works for null and undefined', () => { - expect(test(null as any)).undefined; - expect(test(undefined)).undefined; - }); + it('works for null and undefined', () => { + expect(test(null as any)).undefined; + expect(test(undefined)).undefined; + }); - expressions.filter(([, x]) => !!x).forEach(([input, expected]) => { - it(JSON.stringify(input), () => { - const expression = toExpression(expected); - expect(test(expression)).eql(expression); - }); - }); - }); + expressions.filter(([, x]) => !!x).forEach(([input, expected]) => { + it(JSON.stringify(input), () => { + const expression = toExpression(expected); + expect(test(expression)).eql(expression); + }); + }); + }); - describe('flipIris()', () => { - it('returns the same iris for non flippable irises', () => { - expect(flipIris(Iris.Forward)).equal(Iris.Forward); - expect(flipIris(Iris.Up)).equal(Iris.Up); - expect(flipIris(Iris.Shocked)).equal(Iris.Shocked); - }); + describe('flipIris()', () => { + it('returns the same iris for non flippable irises', () => { + expect(flipIris(Iris.Forward)).equal(Iris.Forward); + expect(flipIris(Iris.Up)).equal(Iris.Up); + expect(flipIris(Iris.Shocked)).equal(Iris.Shocked); + }); - it('returns flipped iris', () => { - expect(flipIris(Iris.Left)).equal(Iris.Right); - expect(flipIris(Iris.Right)).equal(Iris.Left); - expect(flipIris(Iris.UpLeft)).equal(Iris.UpRight); - expect(flipIris(Iris.UpRight)).equal(Iris.UpLeft); - }); - }); + it('returns flipped iris', () => { + expect(flipIris(Iris.Left)).equal(Iris.Right); + expect(flipIris(Iris.Right)).equal(Iris.Left); + expect(flipIris(Iris.UpLeft)).equal(Iris.UpRight); + expect(flipIris(Iris.UpRight)).equal(Iris.UpLeft); + }); + }); - describe('expression()', () => { - it('creates expression with all parameters', () => { - expect(expression(Eye.Angry, Eye.ClosedHappy, Muzzle.Smile, Iris.Left, Iris.Right, ExpressionExtra.Blush)).eql({ - right: Eye.Angry, - left: Eye.ClosedHappy, - muzzle: Muzzle.Smile, - rightIris: Iris.Left, - leftIris: Iris.Right, - extra: ExpressionExtra.Blush, - }); - }); + describe('expression()', () => { + it('creates expression with all parameters', () => { + expect(expression(Eye.Angry, Eye.ClosedHappy, Muzzle.Smile, Iris.Left, Iris.Right, ExpressionExtra.Blush)).eql({ + right: Eye.Angry, + left: Eye.ClosedHappy, + muzzle: Muzzle.Smile, + rightIris: Iris.Left, + leftIris: Iris.Right, + extra: ExpressionExtra.Blush, + }); + }); - it('creates expression with defaults', () => { - expect(expression(Eye.Angry, Eye.ClosedHappy, Muzzle.Smile)).eql({ - right: Eye.Angry, - left: Eye.ClosedHappy, - muzzle: Muzzle.Smile, - rightIris: Iris.Forward, - leftIris: Iris.Forward, - extra: ExpressionExtra.None, - }); - }); - }); + it('creates expression with defaults', () => { + expect(expression(Eye.Angry, Eye.ClosedHappy, Muzzle.Smile)).eql({ + right: Eye.Angry, + left: Eye.ClosedHappy, + muzzle: Muzzle.Smile, + rightIris: Iris.Forward, + leftIris: Iris.Forward, + extra: ExpressionExtra.None, + }); + }); + }); }); diff --git a/src/ts/tests/common/mat4.spec.ts b/src/ts/tests/common/mat4.spec.ts index 140e756..85ff3cd 100644 --- a/src/ts/tests/common/mat4.spec.ts +++ b/src/ts/tests/common/mat4.spec.ts @@ -3,21 +3,21 @@ import { expect } from 'chai'; import { createMat4, ortho } from '../../common/mat4'; describe('mat4', () => { - describe('createMat4()', () => { - expect(createMat4()).eql(new Float32Array([ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, - ])); - }); + describe('createMat4()', () => { + expect(createMat4()).eql(new Float32Array([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + ])); + }); - describe('ortho()', () => { - expect(ortho(createMat4(), 100, 200, 300, 400, 10, 20)).eql(new Float32Array([ - 0.019999999552965164, 0, 0, 0, - 0, 0.019999999552965164, 0, 0, - 0, 0, -0.20000000298023224, 0, - -3, -7, -3, 1 - ])); - }); + describe('ortho()', () => { + expect(ortho(createMat4(), 100, 200, 300, 400, 10, 20)).eql(new Float32Array([ + 0.019999999552965164, 0, 0, 0, + 0, 0.019999999552965164, 0, 0, + 0, 0, -0.20000000298023224, 0, + -3, -7, -3, 1 + ])); + }); }); diff --git a/src/ts/tests/common/movementUtils.spec.ts b/src/ts/tests/common/movementUtils.spec.ts index eacf590..e93e6e9 100644 --- a/src/ts/tests/common/movementUtils.spec.ts +++ b/src/ts/tests/common/movementUtils.spec.ts @@ -2,135 +2,135 @@ import '../lib'; import { expect } from 'chai'; import { EntityState } from '../../common/interfaces'; import { - flagsToSpeed, dirToVector, vectorToDir, isMovingRight, encodeMovement, decodeMovement, shouldBeFacingRight + flagsToSpeed, dirToVector, vectorToDir, isMovingRight, encodeMovement, decodeMovement, shouldBeFacingRight } from '../../common/movementUtils'; import { PONY_SPEED_TROT, PONY_SPEED_WALK, tileWidth, tileHeight } from '../../common/constants'; import { rect } from '../../common/rect'; import { roundPositionX, roundPositionY } from '../../common/positionUtils'; describe('movementUtils', () => { - describe('flagsToSpeed()', () => { - it('returns trotting speed for trotting state', () => { - expect(flagsToSpeed(EntityState.PonyTrotting)).equal(PONY_SPEED_TROT); - }); + describe('flagsToSpeed()', () => { + it('returns trotting speed for trotting state', () => { + expect(flagsToSpeed(EntityState.PonyTrotting)).equal(PONY_SPEED_TROT); + }); - it('returns trotting speed for mixed trotting state', () => { - expect(flagsToSpeed(EntityState.PonyTrotting | EntityState.FacingRight)).equal(PONY_SPEED_TROT); - }); + it('returns trotting speed for mixed trotting state', () => { + expect(flagsToSpeed(EntityState.PonyTrotting | EntityState.FacingRight)).equal(PONY_SPEED_TROT); + }); - it('returns walk speed for trotting state', () => { - expect(flagsToSpeed(EntityState.PonyWalking)).equal(PONY_SPEED_WALK); - }); + it('returns walk speed for trotting state', () => { + expect(flagsToSpeed(EntityState.PonyWalking)).equal(PONY_SPEED_WALK); + }); - it('returns 0 for standing state', () => { - expect(flagsToSpeed(EntityState.PonyStanding)).equal(0); - }); - }); + it('returns 0 for standing state', () => { + expect(flagsToSpeed(EntityState.PonyStanding)).equal(0); + }); + }); - describe('dirToVector()', () => { - it('returns vector for given direction', () => { - expect(dirToVector(0)).eql({ x: 0, y: -1 }); - }); - }); + describe('dirToVector()', () => { + it('returns vector for given direction', () => { + expect(dirToVector(0)).eql({ x: 0, y: -1 }); + }); + }); - describe('vectorToDir()', () => { - it('returns direction for given vector', () => { - expect(vectorToDir(0, -1)).eql(0); - }); + describe('vectorToDir()', () => { + it('returns direction for given vector', () => { + expect(vectorToDir(0, -1)).eql(0); + }); - it('returns direction for given vector', () => { - expect(vectorToDir(-1, -1)).eql(14); - }); - }); + it('returns direction for given vector', () => { + expect(vectorToDir(-1, -1)).eql(14); + }); + }); - describe('encodeMovement() + decodeMovement()', () => { - const tests: [number, number, number, EntityState][] = [ - [0.015625, 0.020833333333333332, 0, EntityState.None], - [10.015625, 20.020833333333332, 1, EntityState.PonyTrotting], - [5.515625, 2.2708333333333335, 1, EntityState.PonyTrotting], - [99999.015625, 88888.02083333333, 1, EntityState.PonyTrotting], - ]; + describe('encodeMovement() + decodeMovement()', () => { + const tests: [number, number, number, EntityState][] = [ + [0.015625, 0.020833333333333332, 0, EntityState.None], + [10.015625, 20.020833333333332, 1, EntityState.PonyTrotting], + [5.515625, 2.2708333333333335, 1, EntityState.PonyTrotting], + [99999.015625, 88888.02083333333, 1, EntityState.PonyTrotting], + ]; - tests.forEach(movement => { - it(JSON.stringify(movement), () => { - const [x, y, dir, flags] = movement; - const camera = rect(roundPositionX(x) * tileWidth, roundPositionY(y) * tileHeight, 100, 100); - const [a, b, c, d, e] = encodeMovement(x, y, dir, flags, 123, camera); - expect(decodeMovement(a, b, c, d, e)).eql({ x, y, dir, flags, time: 123, camera }); - }); - }); + tests.forEach(movement => { + it(JSON.stringify(movement), () => { + const [x, y, dir, flags] = movement; + const camera = rect(roundPositionX(x) * tileWidth, roundPositionY(y) * tileHeight, 100, 100); + const [a, b, c, d, e] = encodeMovement(x, y, dir, flags, 123, camera); + expect(decodeMovement(a, b, c, d, e)).eql({ x, y, dir, flags, time: 123, camera }); + }); + }); - it('decodes camera rect', () => { - const [a, b, c, d, e] = encodeMovement(100, 100, 1, EntityState.None, 123, rect(300, 200, 800, 600)); + it('decodes camera rect', () => { + const [a, b, c, d, e] = encodeMovement(100, 100, 1, EntityState.None, 123, rect(300, 200, 800, 600)); - expect(decodeMovement(a, b, c, d, e)).eql({ - x: 100.015625, y: 100.02083333333333, dir: 1, flags: EntityState.None, time: 123, - camera: rect(300, 200, 800, 600) - }); - }); + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 100.015625, y: 100.02083333333333, dir: 1, flags: EntityState.None, time: 123, + camera: rect(300, 200, 800, 600) + }); + }); - it('clamps negative values', () => { - const [a, b, c, d, e] = encodeMovement(-10, -10, 1, EntityState.None, 123, rect(0, 0, 100, 100)); + it('clamps negative values', () => { + const [a, b, c, d, e] = encodeMovement(-10, -10, 1, EntityState.None, 123, rect(0, 0, 100, 100)); - expect(decodeMovement(a, b, c, d, e)).eql({ - x: 0.015625, y: 0.020833333333333332, dir: 1, flags: EntityState.None, time: 123, - camera: rect(0, 0, 100, 100) - }); - }); + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 0.015625, y: 0.020833333333333332, dir: 1, flags: EntityState.None, time: 123, + camera: rect(0, 0, 100, 100) + }); + }); - it('clamps values above 100000', () => { - const [a, b, c, d, e] = encodeMovement(100001, 100001, 1, EntityState.None, 123, rect(100000 * 32, 100000 * 24, 100, 100)); + it('clamps values above 100000', () => { + const [a, b, c, d, e] = encodeMovement(100001, 100001, 1, EntityState.None, 123, rect(100000 * 32, 100000 * 24, 100, 100)); - expect(decodeMovement(a, b, c, d, e)).eql({ - x: 100000.015625, y: 100000.020833333333334, dir: 1, flags: EntityState.None, time: 123, - camera: rect(100000 * 32, 100000 * 24, 100, 100) - }); - }); + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 100000.015625, y: 100000.020833333333334, dir: 1, flags: EntityState.None, time: 123, + camera: rect(100000 * 32, 100000 * 24, 100, 100) + }); + }); - it('handles invalid direction', () => { - const [a, b, c, d, e] = encodeMovement(10, 10, 999, EntityState.None, 123, rect(0, 0, 100, 100)); + it('handles invalid direction', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 999, EntityState.None, 123, rect(0, 0, 100, 100)); - expect(decodeMovement(a, b, c, d, e)).eql({ - x: 10.015625, y: 10.020833333333334, dir: 231, flags: EntityState.None, time: 123, - camera: rect(0, 0, 100, 100) - }); - }); + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 10.015625, y: 10.020833333333334, dir: 231, flags: EntityState.None, time: 123, + camera: rect(0, 0, 100, 100) + }); + }); - it('handles invalid flags', () => { - const [a, b, c, d, e] = encodeMovement(10, 10, 1, 999, 123, rect(0, 0, 100, 100)); + it('handles invalid flags', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 1, 999, 123, rect(0, 0, 100, 100)); - expect(decodeMovement(a, b, c, d, e)).eql({ - x: 10.015625, y: 10.020833333333334, dir: 1, flags: 231, time: 123, - camera: rect(0, 0, 100, 100) - }); - }); - }); + expect(decodeMovement(a, b, c, d, e)).eql({ + x: 10.015625, y: 10.020833333333334, dir: 1, flags: 231, time: 123, + camera: rect(0, 0, 100, 100) + }); + }); + }); - describe('isMovingRight()', () => { - it('returns false for negative velocity', () => { - expect(isMovingRight(-1, true)).false; - }); + describe('isMovingRight()', () => { + it('returns false for negative velocity', () => { + expect(isMovingRight(-1, true)).false; + }); - it('returns true for positive velocity', () => { - expect(isMovingRight(1, false)).true; - }); + it('returns true for positive velocity', () => { + expect(isMovingRight(1, false)).true; + }); - it('returns previous facing for 0 velocity', () => { - expect(isMovingRight(0, true)).true; - }); - }); + it('returns previous facing for 0 velocity', () => { + expect(isMovingRight(0, true)).true; + }); + }); - describe('shouldBeFacingRight()', () => { - it('returns false for negative velocity', () => { - expect(shouldBeFacingRight({ vx: -1, state: 0 } as any)).false; - }); + describe('shouldBeFacingRight()', () => { + it('returns false for negative velocity', () => { + expect(shouldBeFacingRight({ vx: -1, state: 0 } as any)).false; + }); - it('returns true for positive velocity', () => { - expect(shouldBeFacingRight({ vx: 1, state: 0 } as any)).true; - }); + it('returns true for positive velocity', () => { + expect(shouldBeFacingRight({ vx: 1, state: 0 } as any)).true; + }); - it('returns true for zero velocity if already facing right', () => { - expect(shouldBeFacingRight({ vx: 0, state: EntityState.FacingRight } as any)).true; - }); - }); + it('returns true for zero velocity if already facing right', () => { + expect(shouldBeFacingRight({ vx: 0, state: EntityState.FacingRight } as any)).true; + }); + }); }); diff --git a/src/ts/tests/common/ponyHelpers.spec.ts b/src/ts/tests/common/ponyHelpers.spec.ts index ff8439a..c770338 100644 --- a/src/ts/tests/common/ponyHelpers.spec.ts +++ b/src/ts/tests/common/ponyHelpers.spec.ts @@ -3,13 +3,13 @@ import { expect } from 'chai'; import { defaultPonyState, isStateEqual } from '../../client/ponyHelpers'; describe('interfaces', () => { - describe('isStateEqual()', () => { - it('returns true if two states are equal', () => { - expect(isStateEqual(defaultPonyState(), defaultPonyState())).true; - }); + describe('isStateEqual()', () => { + it('returns true if two states are equal', () => { + expect(isStateEqual(defaultPonyState(), defaultPonyState())).true; + }); - it('returns true if two states are not equal', () => { - expect(isStateEqual({ ...defaultPonyState(), blinkFrame: 5 }, defaultPonyState())).false; - }); - }); + it('returns true if two states are not equal', () => { + expect(isStateEqual({ ...defaultPonyState(), blinkFrame: 5 }, defaultPonyState())).false; + }); + }); }); diff --git a/src/ts/tests/common/ponyInfo.spec.ts b/src/ts/tests/common/ponyInfo.spec.ts index 61a9445..f1e2e2a 100644 --- a/src/ts/tests/common/ponyInfo.spec.ts +++ b/src/ts/tests/common/ponyInfo.spec.ts @@ -1,7 +1,7 @@ import '../lib'; import { expect } from 'chai'; import { - syncLockedPonyInfo, syncLockedSpriteSet, syncLockedPonyInfoNumber, toPaletteSet, releasePalettes + syncLockedPonyInfo, syncLockedSpriteSet, syncLockedPonyInfoNumber, toPaletteSet, releasePalettes } from '../../common/ponyInfo'; import { SpriteSet, PonyInfo, PaletteManager, ColorExtraSets, Palette } from '../../common/interfaces'; import { RED, BLUE, YELLOW, ORANGE, TRANSPARENT } from '../../common/colors'; @@ -10,350 +10,350 @@ import { repeat, times, flatten } from '../../common/utils'; type Set = SpriteSet; function ponyInfo(info: Partial): PonyInfo { - return info as PonyInfo; + return info as PonyInfo; } describe('ponyInfo', () => { - describe('syncLockedSpriteSet()', () => { - it('does nothing for undefined', () => { - syncLockedSpriteSet(undefined, false, f => f, 'ff0000', '00ff00'); - }); - - it('does nothing for missing fills', () => { - syncLockedSpriteSet({}, false, f => f, 'ff0000', '00ff00'); - }); - - it('does nothing for missing lockFills', () => { - syncLockedSpriteSet({ fills: ['00ff00'] }, false, f => f, 'ff0000', '00ff00'); - }); - - it('uses base fill if fill is locked', () => { - const set: Set = { - fills: [], - lockFills: [true], - }; - - syncLockedSpriteSet(set, false, f => f, 'ff0000', '00ff00'); - - expect(set.fills![0]).equal('ff0000'); - }); - - it('uses first fill if fill is locked', () => { - const set: Set = { - fills: ['ff0000', '0000ff', '0000ff'], - lockFills: [false, true, false], - }; - - syncLockedSpriteSet(set, false, f => f, 'ffffff', '000000'); - - expect(set.fills).eql(['ff0000', 'ff0000', '0000ff']); - }); + describe('syncLockedSpriteSet()', () => { + it('does nothing for undefined', () => { + syncLockedSpriteSet(undefined, false, f => f, 'ff0000', '00ff00'); + }); + + it('does nothing for missing fills', () => { + syncLockedSpriteSet({}, false, f => f, 'ff0000', '00ff00'); + }); + + it('does nothing for missing lockFills', () => { + syncLockedSpriteSet({ fills: ['00ff00'] }, false, f => f, 'ff0000', '00ff00'); + }); + + it('uses base fill if fill is locked', () => { + const set: Set = { + fills: [], + lockFills: [true], + }; + + syncLockedSpriteSet(set, false, f => f, 'ff0000', '00ff00'); + + expect(set.fills![0]).equal('ff0000'); + }); + + it('uses first fill if fill is locked', () => { + const set: Set = { + fills: ['ff0000', '0000ff', '0000ff'], + lockFills: [false, true, false], + }; + + syncLockedSpriteSet(set, false, f => f, 'ffffff', '000000'); + + expect(set.fills).eql(['ff0000', 'ff0000', '0000ff']); + }); - it('uses generated outline if custom outlines is false', () => { - const set: Set = { - fills: ['ff0000', '00ff00'], - outlines: ['000000', '000000'], - lockOutlines: [false, false], - }; - - syncLockedSpriteSet(set, false, f => f + 'x', 'ff0000', '00ff00'); - - expect(set.lockOutlines).eql([true, true]); - expect(set.outlines).eql(['ff0000x', '00ff00x']); - }); - - it('uses generated outline if outline is locked', () => { - const set: Set = { - fills: ['ff0000', '00ff00'], - outlines: ['000000', '000000'], - lockOutlines: [false, true], - }; - - syncLockedSpriteSet(set, true, f => f + 'x', 'ff0000', '00ff00'); - - expect(set.outlines).eql(['000000', '00ff00x']); - }); - - it('uses base outline for first fill if fill is locked too', () => { - const set: Set = { - fills: ['ff0000', '00ff00'], - lockFills: [true, false], - outlines: ['000000', '000000'], - lockOutlines: [true, false], - }; - - syncLockedSpriteSet(set, true, f => f + 'x', 'ff0000', 'ffffff'); - - expect(set.outlines).eql(['ffffff', '000000']); - }); - }); - - describe('syncLockedPonyInfo()', () => { - it('syncs coatOutline if customOutlines is false', () => { - const pony = ponyInfo({ - customOutlines: false, - coatFill: 'ff0000', - }); - - syncLockedPonyInfo(pony); - - expect(pony.coatOutline).equal('b30000'); - }); - - it('syncs coatOutline if lockCoatOutline is true', () => { - const pony = ponyInfo({ - customOutlines: true, - lockCoatOutline: true, - coatFill: 'ff0000', - }); - - syncLockedPonyInfo(pony); - - expect(pony.coatOutline).equal('b30000'); - }); - - it('does not sync coatOutline if customOutline is false and lockCoatOutline is false', () => { - const pony = ponyInfo({ - customOutlines: true, - lockCoatOutline: false, - coatFill: 'ff0000', - coatOutline: '00ff00', - }); - - syncLockedPonyInfo(pony); - - expect(pony.coatOutline).equal('00ff00'); - }); - - it('syncs eyeOpennessLeft if lockEyes is true', () => { - const pony = ponyInfo({ - lockEyes: true, - eyeOpennessLeft: 3, - eyeOpennessRight: 2, - }); - - syncLockedPonyInfo(pony); - - expect(pony.eyeOpennessLeft).equal(2); - }); - - it('does not sync eyeOpennessLeft if lockEyes is false', () => { - const pony = ponyInfo({ - lockEyes: false, - eyeOpennessLeft: 3, - eyeOpennessRight: 2, - }); - - syncLockedPonyInfo(pony); - - expect(pony.eyeOpennessLeft).equal(3); - }); - - it('syncs eyeColorLeft if lockEyeColor is true', () => { - const pony = ponyInfo({ - lockEyeColor: true, - eyeColorLeft: 'ff0000', - eyeColorRight: '00ff00', - }); - - syncLockedPonyInfo(pony); - - expect(pony.eyeColorLeft).equal('00ff00'); - }); - - it('does not sync eyeColorLeft if lockEyeColor is false', () => { - const pony = ponyInfo({ - lockEyeColor: false, - eyeColorLeft: 'ff0000', - eyeColorRight: '00ff00', - }); - - syncLockedPonyInfo(pony); - - expect(pony.eyeColorLeft).equal('ff0000'); - }); - - const fields: (keyof PonyInfo)[] = [ - 'nose', 'ears', 'horn', 'wings', 'frontHooves', 'backHooves', 'mane', 'backMane', 'tail', 'facialHair', - 'headAccessory', 'earAccessory', 'faceAccessory', 'neckAccessory', 'frontLegAccessory', 'backLegAccessory', - 'backAccessory', 'waistAccessory', 'chestAccessory', 'sleeveAccessory', - ]; - - fields.forEach(field => it(`syncs '${field}' sprite set`, () => { - const pony = ponyInfo({ - [field]: { - fills: ['ff0000'], - outlines: ['00ff00'], - lockOutlines: [true], - }, - }); - - syncLockedPonyInfo(pony); - - expect(pony[field]).eql({ - fills: ['ff0000'], - outlines: ['b30000'], - lockOutlines: [true], - }); - })); - - it('syncs extraAccessory colors to correct pony colors', () => { - const pony = ponyInfo({ - customOutlines: true, - coatFill: 'ffffff', - coatOutline: '000000', - eyeColorLeft: 'aaaaaa', - eyeColorRight: 'bbbbbb', - mane: { type: 1, fills: ['ff0000'], outlines: ['aa0000'] }, - backMane: { type: 1, fills: ['00ff00'], outlines: ['00aa00'] }, - tail: { type: 1, fills: ['0000ff'], outlines: ['0000aa'] }, - extraAccessory: { - fills: repeat(5, '111111'), - lockFills: repeat(5, true), - outlines: repeat(5, '111111'), - lockOutlines: repeat(5, true), - }, - }); - - syncLockedPonyInfo(pony); - - expect(pony.extraAccessory).eql({ - fills: ['ffffff', 'bbbbbb', 'ff0000', '00ff00', '0000ff'], - lockFills: [true, true, true, true, true], - outlines: ['000000', 'bbbbbb', 'aa0000', '00aa00', '0000aa'], - lockOutlines: [true, true, true, true, true], - }); - }); - }); - - describe('syncLockedPonyInfoNumber()', () => { - it('syncs coatOutline if customOutlines is false', () => { - const pony: any = { - customOutlines: false, - coatFill: 0xff0000ff, - }; - - syncLockedPonyInfoNumber(pony); - - expect(pony.coatOutline).equal(0xb30000ff); - }); - }); - - describe('toPaletteSet()', () => { - const getColorsForSet = (set: SpriteSet, count: number) => new Uint32Array([ - TRANSPARENT, - ...flatten(times(count, i => [ - set.fills && set.fills[i] || 0, - set.outlines && set.outlines[i] || 0 - ])), - ]); - let manager: PaletteManager; - - beforeEach(() => { - manager = { - add: x => x && Array.from(x) as any, - addArray: x => x && Array.from(x) as any, - init() { }, - }; - }); - - it('returns default object for empty set', () => { - expect(toPaletteSet({}, undefined, manager, getColorsForSet, false, true)).eql({ - type: 0, - pattern: 0, - palette: [TRANSPARENT], - extraPalette: undefined, - }); - }); - - it('returns type pattern and palette fields from set', () => { - const set: SpriteSet = { - type: 1, - pattern: 2, - fills: [], - outlines: [], - }; - - expect(toPaletteSet(set, [], manager, getColorsForSet, false, true)).eql({ - type: 1, - pattern: 2, - palette: [TRANSPARENT], - extraPalette: undefined, - }); - }); - - it('trims palette to set color count', () => { - const set: SpriteSet = { - type: 1, - pattern: 2, - fills: [RED, BLUE, 1, 2, 3, 4], - outlines: [ORANGE, YELLOW, 5, 6, 7, 8], - }; - - const sets: ColorExtraSets = [ - [], - [ - { color: {} as any }, - { color: {} as any }, - { color: {} as any, colors: 5 }, - ], - ]; - - expect(toPaletteSet(set, sets, manager, getColorsForSet, false, true)).eql({ - type: 1, - pattern: 2, - palette: [TRANSPARENT, RED, ORANGE, BLUE, YELLOW], - extraPalette: undefined, - }); - }); - - it('returns extra palette from sets', () => { - const set: SpriteSet = { - type: 0, - pattern: 0, - }; - - const sets: ColorExtraSets = [ - [ - { - color: {} as any, - palettes: [ - new Uint32Array([BLUE, YELLOW]), - ], - }, - ], - ]; - - expect(toPaletteSet(set, sets, manager, getColorsForSet, true, true)).eql({ - type: 0, - pattern: 0, - palette: [TRANSPARENT], - extraPalette: [BLUE, YELLOW], - }); - }); - }); - - describe('releasePalettes()', () => { - it('ignores other fields', () => { - releasePalettes({ foo: 'bar', bar: 12, test: { a: 4 } } as any); - }); - - it('releases palette field', () => { - const palette: Palette = { refs: 1, x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0) }; - - releasePalettes({ foo: palette } as any); - - expect(palette.refs).equal(0); - }); - - it('releases palette in object field', () => { - const palette: Palette = { refs: 1, x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0) }; - const extraPalette: Palette = { refs: 1, x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0) }; - - releasePalettes({ foo: { palette, extraPalette } } as any); - - expect(palette.refs).equal(0); - expect(extraPalette.refs).equal(0); - }); - }); + it('uses generated outline if custom outlines is false', () => { + const set: Set = { + fills: ['ff0000', '00ff00'], + outlines: ['000000', '000000'], + lockOutlines: [false, false], + }; + + syncLockedSpriteSet(set, false, f => f + 'x', 'ff0000', '00ff00'); + + expect(set.lockOutlines).eql([true, true]); + expect(set.outlines).eql(['ff0000x', '00ff00x']); + }); + + it('uses generated outline if outline is locked', () => { + const set: Set = { + fills: ['ff0000', '00ff00'], + outlines: ['000000', '000000'], + lockOutlines: [false, true], + }; + + syncLockedSpriteSet(set, true, f => f + 'x', 'ff0000', '00ff00'); + + expect(set.outlines).eql(['000000', '00ff00x']); + }); + + it('uses base outline for first fill if fill is locked too', () => { + const set: Set = { + fills: ['ff0000', '00ff00'], + lockFills: [true, false], + outlines: ['000000', '000000'], + lockOutlines: [true, false], + }; + + syncLockedSpriteSet(set, true, f => f + 'x', 'ff0000', 'ffffff'); + + expect(set.outlines).eql(['ffffff', '000000']); + }); + }); + + describe('syncLockedPonyInfo()', () => { + it('syncs coatOutline if customOutlines is false', () => { + const pony = ponyInfo({ + customOutlines: false, + coatFill: 'ff0000', + }); + + syncLockedPonyInfo(pony); + + expect(pony.coatOutline).equal('b30000'); + }); + + it('syncs coatOutline if lockCoatOutline is true', () => { + const pony = ponyInfo({ + customOutlines: true, + lockCoatOutline: true, + coatFill: 'ff0000', + }); + + syncLockedPonyInfo(pony); + + expect(pony.coatOutline).equal('b30000'); + }); + + it('does not sync coatOutline if customOutline is false and lockCoatOutline is false', () => { + const pony = ponyInfo({ + customOutlines: true, + lockCoatOutline: false, + coatFill: 'ff0000', + coatOutline: '00ff00', + }); + + syncLockedPonyInfo(pony); + + expect(pony.coatOutline).equal('00ff00'); + }); + + it('syncs eyeOpennessLeft if lockEyes is true', () => { + const pony = ponyInfo({ + lockEyes: true, + eyeOpennessLeft: 3, + eyeOpennessRight: 2, + }); + + syncLockedPonyInfo(pony); + + expect(pony.eyeOpennessLeft).equal(2); + }); + + it('does not sync eyeOpennessLeft if lockEyes is false', () => { + const pony = ponyInfo({ + lockEyes: false, + eyeOpennessLeft: 3, + eyeOpennessRight: 2, + }); + + syncLockedPonyInfo(pony); + + expect(pony.eyeOpennessLeft).equal(3); + }); + + it('syncs eyeColorLeft if lockEyeColor is true', () => { + const pony = ponyInfo({ + lockEyeColor: true, + eyeColorLeft: 'ff0000', + eyeColorRight: '00ff00', + }); + + syncLockedPonyInfo(pony); + + expect(pony.eyeColorLeft).equal('00ff00'); + }); + + it('does not sync eyeColorLeft if lockEyeColor is false', () => { + const pony = ponyInfo({ + lockEyeColor: false, + eyeColorLeft: 'ff0000', + eyeColorRight: '00ff00', + }); + + syncLockedPonyInfo(pony); + + expect(pony.eyeColorLeft).equal('ff0000'); + }); + + const fields: (keyof PonyInfo)[] = [ + 'nose', 'ears', 'horn', 'wings', 'frontHooves', 'backHooves', 'mane', 'backMane', 'tail', 'facialHair', + 'headAccessory', 'earAccessory', 'faceAccessory', 'neckAccessory', 'frontLegAccessory', 'backLegAccessory', + 'backAccessory', 'waistAccessory', 'chestAccessory', 'sleeveAccessory', + ]; + + fields.forEach(field => it(`syncs '${field}' sprite set`, () => { + const pony = ponyInfo({ + [field]: { + fills: ['ff0000'], + outlines: ['00ff00'], + lockOutlines: [true], + }, + }); + + syncLockedPonyInfo(pony); + + expect(pony[field]).eql({ + fills: ['ff0000'], + outlines: ['b30000'], + lockOutlines: [true], + }); + })); + + it('syncs extraAccessory colors to correct pony colors', () => { + const pony = ponyInfo({ + customOutlines: true, + coatFill: 'ffffff', + coatOutline: '000000', + eyeColorLeft: 'aaaaaa', + eyeColorRight: 'bbbbbb', + mane: { type: 1, fills: ['ff0000'], outlines: ['aa0000'] }, + backMane: { type: 1, fills: ['00ff00'], outlines: ['00aa00'] }, + tail: { type: 1, fills: ['0000ff'], outlines: ['0000aa'] }, + extraAccessory: { + fills: repeat(5, '111111'), + lockFills: repeat(5, true), + outlines: repeat(5, '111111'), + lockOutlines: repeat(5, true), + }, + }); + + syncLockedPonyInfo(pony); + + expect(pony.extraAccessory).eql({ + fills: ['ffffff', 'bbbbbb', 'ff0000', '00ff00', '0000ff'], + lockFills: [true, true, true, true, true], + outlines: ['000000', 'bbbbbb', 'aa0000', '00aa00', '0000aa'], + lockOutlines: [true, true, true, true, true], + }); + }); + }); + + describe('syncLockedPonyInfoNumber()', () => { + it('syncs coatOutline if customOutlines is false', () => { + const pony: any = { + customOutlines: false, + coatFill: 0xff0000ff, + }; + + syncLockedPonyInfoNumber(pony); + + expect(pony.coatOutline).equal(0xb30000ff); + }); + }); + + describe('toPaletteSet()', () => { + const getColorsForSet = (set: SpriteSet, count: number) => new Uint32Array([ + TRANSPARENT, + ...flatten(times(count, i => [ + set.fills && set.fills[i] || 0, + set.outlines && set.outlines[i] || 0 + ])), + ]); + let manager: PaletteManager; + + beforeEach(() => { + manager = { + add: x => x && Array.from(x) as any, + addArray: x => x && Array.from(x) as any, + init() { }, + }; + }); + + it('returns default object for empty set', () => { + expect(toPaletteSet({}, undefined, manager, getColorsForSet, false, true)).eql({ + type: 0, + pattern: 0, + palette: [TRANSPARENT], + extraPalette: undefined, + }); + }); + + it('returns type pattern and palette fields from set', () => { + const set: SpriteSet = { + type: 1, + pattern: 2, + fills: [], + outlines: [], + }; + + expect(toPaletteSet(set, [], manager, getColorsForSet, false, true)).eql({ + type: 1, + pattern: 2, + palette: [TRANSPARENT], + extraPalette: undefined, + }); + }); + + it('trims palette to set color count', () => { + const set: SpriteSet = { + type: 1, + pattern: 2, + fills: [RED, BLUE, 1, 2, 3, 4], + outlines: [ORANGE, YELLOW, 5, 6, 7, 8], + }; + + const sets: ColorExtraSets = [ + [], + [ + { color: {} as any }, + { color: {} as any }, + { color: {} as any, colors: 5 }, + ], + ]; + + expect(toPaletteSet(set, sets, manager, getColorsForSet, false, true)).eql({ + type: 1, + pattern: 2, + palette: [TRANSPARENT, RED, ORANGE, BLUE, YELLOW], + extraPalette: undefined, + }); + }); + + it('returns extra palette from sets', () => { + const set: SpriteSet = { + type: 0, + pattern: 0, + }; + + const sets: ColorExtraSets = [ + [ + { + color: {} as any, + palettes: [ + new Uint32Array([BLUE, YELLOW]), + ], + }, + ], + ]; + + expect(toPaletteSet(set, sets, manager, getColorsForSet, true, true)).eql({ + type: 0, + pattern: 0, + palette: [TRANSPARENT], + extraPalette: [BLUE, YELLOW], + }); + }); + }); + + describe('releasePalettes()', () => { + it('ignores other fields', () => { + releasePalettes({ foo: 'bar', bar: 12, test: { a: 4 } } as any); + }); + + it('releases palette field', () => { + const palette: Palette = { refs: 1, x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0) }; + + releasePalettes({ foo: palette } as any); + + expect(palette.refs).equal(0); + }); + + it('releases palette in object field', () => { + const palette: Palette = { refs: 1, x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0) }; + const extraPalette: Palette = { refs: 1, x: 0, y: 0, u: 0, v: 0, colors: new Uint32Array(0) }; + + releasePalettes({ foo: { palette, extraPalette } } as any); + + expect(palette.refs).equal(0); + expect(extraPalette.refs).equal(0); + }); + }); }); diff --git a/src/ts/tests/common/rect.spec.ts b/src/ts/tests/common/rect.spec.ts index 8e1cc54..293baed 100644 --- a/src/ts/tests/common/rect.spec.ts +++ b/src/ts/tests/common/rect.spec.ts @@ -3,9 +3,9 @@ import { expect } from 'chai'; import { rect, centerPoint } from '../../common/rect'; describe('rect', () => { - describe('centerPoint()', () => { - it('returns center point', () => { - expect(centerPoint(rect(10, 20, 20, 40))).eql({ x: 20, y: 40 }); - }); - }); + describe('centerPoint()', () => { + it('returns center point', () => { + expect(centerPoint(rect(10, 20, 20, 40))).eql({ x: 20, y: 40 }); + }); + }); }); diff --git a/src/ts/tests/common/timeUtils.spec.ts b/src/ts/tests/common/timeUtils.spec.ts index cdd7343..10e5b0b 100644 --- a/src/ts/tests/common/timeUtils.spec.ts +++ b/src/ts/tests/common/timeUtils.spec.ts @@ -1,7 +1,7 @@ import '../lib'; import { expect } from 'chai'; import { - formatHourMinutes, getLightColor, HOUR_LENGTH, isDay, isNight, LightData, createLightData + formatHourMinutes, getLightColor, HOUR_LENGTH, isDay, isNight, LightData, createLightData } from '../../common/timeUtils'; import { WHITE } from '../../common/colors'; import { Season } from '../../common/interfaces'; @@ -10,45 +10,45 @@ const LIGHT_DAY = WHITE; const LIGHT_NIGHT = 0x2b3374ff; describe('timeUtils', () => { - let lightData: LightData; + let lightData: LightData; - beforeEach(() => { - lightData = createLightData(Season.Spring); - }); + beforeEach(() => { + lightData = createLightData(Season.Spring); + }); - describe('getLightColor()', () => { - it('returns day light in day time', () => { - expect(getLightColor(lightData, 12 * HOUR_LENGTH)).equal(LIGHT_DAY); - }); + describe('getLightColor()', () => { + it('returns day light in day time', () => { + expect(getLightColor(lightData, 12 * HOUR_LENGTH)).equal(LIGHT_DAY); + }); - it('returns night light in night time', () => { - expect(getLightColor(lightData, 0)).equal(LIGHT_NIGHT); - }); - }); + it('returns night light in night time', () => { + expect(getLightColor(lightData, 0)).equal(LIGHT_NIGHT); + }); + }); - describe('formatHourMinutes()', () => { - it('formats hour and minute for the in-game day', () => { - expect(formatHourMinutes(948765)).equal('07:54'); - }); - }); + describe('formatHourMinutes()', () => { + it('formats hour and minute for the in-game day', () => { + expect(formatHourMinutes(948765)).equal('07:54'); + }); + }); - describe('isDay()', () => { - it('returns true if daytime', () => { - expect(isDay(12 * HOUR_LENGTH)).true; - }); + describe('isDay()', () => { + it('returns true if daytime', () => { + expect(isDay(12 * HOUR_LENGTH)).true; + }); - it('returns falst if not daytime', () => { - expect(isDay(0)).false; - }); - }); + it('returns falst if not daytime', () => { + expect(isDay(0)).false; + }); + }); - describe('isNight()', () => { - it('returns true if not daytime', () => { - expect(isNight(12 * HOUR_LENGTH)).false; - }); + describe('isNight()', () => { + it('returns true if not daytime', () => { + expect(isNight(12 * HOUR_LENGTH)).false; + }); - it('returns falst if daytime', () => { - expect(isNight(0)).true; - }); - }); + it('returns falst if daytime', () => { + expect(isNight(0)).true; + }); + }); }); diff --git a/src/ts/tests/common/utils.spec.ts b/src/ts/tests/common/utils.spec.ts index 0525a4a..de46768 100644 --- a/src/ts/tests/common/utils.spec.ts +++ b/src/ts/tests/common/utils.spec.ts @@ -2,455 +2,455 @@ import '../lib'; import { expect } from 'chai'; import { spy, assert, SinonFakeTimers, useFakeTimers } from 'sinon'; import { - arraysEqual, removeItem, removeById, dispose, fromNow, clamp, normalize, - toInt, hasFlag, setFlag, at, findById, distance, contains, maxDate, minDate, - flatten, includes, isCommand, processCommand, collidersIntersect, formatDuration, att + arraysEqual, removeItem, removeById, dispose, fromNow, clamp, normalize, + toInt, hasFlag, setFlag, at, findById, distance, contains, maxDate, minDate, + flatten, includes, isCommand, processCommand, collidersIntersect, formatDuration, att } from '../../common/utils'; import { rect } from '../../common/rect'; describe('utils', () => { - describe('fromNow()', () => { - let clock: SinonFakeTimers; + describe('fromNow()', () => { + let clock: SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers(); - }); + beforeEach(() => { + clock = useFakeTimers(); + }); - afterEach(() => { - clock.restore(); - }); - - it('returns date object', () => { - expect(fromNow(0)).instanceof(Date); - }); - - it('returns current time for 0 offset', () => { - clock.setSystemTime(123); - - expect(fromNow(0).getTime()).equal(123); - }); - - it('returns current time offset by given amount', () => { - clock.setSystemTime(123); - - expect(fromNow(100).getTime()).equal(223); - }); - }); - - describe('maxDate()', () => { - it('returns larger of two dates', () => { - expect(maxDate(new Date(123), new Date(122))!.getTime()).equal(123); - }); - - it('returns larger of two dates (2)', () => { - expect(maxDate(new Date(123), new Date(124))!.getTime()).equal(124); - }); - - it('returns non-undefined date', () => { - expect(maxDate(new Date(123), undefined)!.getTime()).equal(123); - }); - - it('returns non-undefined date (2)', () => { - expect(maxDate(undefined, new Date(123))!.getTime()).equal(123); - }); - - it('returns undefined if both dates are undefined', () => { - expect(maxDate(undefined, undefined)).undefined; - }); - }); - - describe('minDate()', () => { - it('returns smaller of two dates', () => { - expect(minDate(new Date(123), new Date(122))!.getTime()).equal(122); - }); - - it('returns smaller of two dates (2)', () => { - expect(minDate(new Date(123), new Date(124))!.getTime()).equal(123); - }); - - it('returns non-undefined date', () => { - expect(minDate(new Date(123), undefined)!.getTime()).equal(123); - }); - - it('returns non-undefined date (2)', () => { - expect(minDate(undefined, new Date(123))!.getTime()).equal(123); - }); - - it('returns undefined if both dates are undefined', () => { - expect(minDate(undefined, undefined)).undefined; - }); - }); - - describe('formatDuration()', () => { - it('returns 0s for 0 duration', () => { - expect(formatDuration(0)).equal('0s'); - }); - - it('returns duration seconds', () => { - expect(formatDuration(15000)).equal('15s'); - }); - - it('returns duration minutes and seconds', () => { - expect(formatDuration(15 * 60 * 1000 + 6 * 1000)).equal('15m 6s'); - }); - - it('returns duration hours and minutes', () => { - expect(formatDuration(15 * 3600 * 1000 + 13 * 60 * 1000 + 6 * 1000)).equal('15h 13m'); - }); - - it('returns days and hours', () => { - expect(formatDuration((10 + 24 * 2) * 3600 * 1000 + 13 * 60 * 1000 + 6 * 1000)).equal('2d 10h'); - }); - }); - - describe('clamp()', () => { - it('returns given value if within range', () => { - expect(clamp(2, 1, 3)).equal(2); - }); - - it('returns minimum for value below minimum', () => { - expect(clamp(0, 1, 3)).equal(1); - }); - - it('returns maximum for value above maximum', () => { - expect(clamp(5, 1, 3)).equal(3); - }); - - it('returns minimum for NaN', () => { - expect(clamp(NaN, 1, 3)).equal(1); - }); - }); - - describe('normalize()', () => { - it('returns given values as vector', () => { - expect(normalize(1, 0)).eql({ x: 1, y: 0 }); - }); - - it('returns normalized vector', () => { - expect(normalize(0, -5)).eql({ x: 0, y: -1 }); - }); - }); - - describe('toInt()', () => { - it('returns given integer value', () => { - expect(toInt(1)).equal(1); - expect(toInt(-5)).equal(-5); - }); - - it('converts float number to integer value', () => { - expect(toInt(1.5)).eql(1); - expect(toInt(0.15)).eql(0); - expect(toInt(123.9)).eql(123); - }); - - it('converts string to integer value', () => { - expect(toInt('1.5')).eql(1); - expect(toInt('5')).eql(5); - }); - - it('converts null or undefined to 0', () => { - expect(toInt(null)).eql(0); - expect(toInt(undefined)).eql(0); - }); - - it('converts any object or array to 0', () => { - expect(toInt([])).eql(0); - expect(toInt({})).eql(0); - }); - }); - - describe('hasFlag()', () => { - enum Foo { - Aaa = 1, - Bbb = 2, - } - - it('returns true if flag is set', () => { - expect(hasFlag(Foo.Aaa, Foo.Aaa)).true; - }); - - it('returns true if flag is also set', () => { - expect(hasFlag(Foo.Aaa | Foo.Bbb, Foo.Aaa)).true; - }); - - it('returns false if flag is not set', () => { - expect(hasFlag(Foo.Bbb, Foo.Aaa)).false; - }); - }); - - describe('setFlag()', () => { - enum Foo { - Aaa = 1, - Bbb = 2, - } - - it('sets flag', () => { - expect(setFlag(0, Foo.Aaa, true)).equal(Foo.Aaa); - }); - - it('unsets flag', () => { - expect(setFlag(Foo.Aaa, Foo.Aaa, false)).equal(0); - }); - - it('does nothing if already set', () => { - expect(setFlag(Foo.Aaa, Foo.Aaa, true)).equal(Foo.Aaa); - }); - - it('does nothing if already unset', () => { - expect(setFlag(0, Foo.Aaa, false)).equal(0); - }); - - it('sets with another flag', () => { - expect(setFlag(Foo.Bbb, Foo.Aaa, true)).equal(Foo.Aaa | Foo.Bbb); - }); - - it('unsets flag with another flag', () => { - expect(setFlag(Foo.Bbb | Foo.Aaa, Foo.Aaa, false)).equal(Foo.Bbb); - }); - }); - - describe('includes()', () => { - it('returns true if array includes given element', () => { - expect(includes(['a', 'b', 'c'], 'b')).true; - }); - - it('returns false if array does not include given element', () => { - expect(includes(['a', 'b', 'c'], 'd')).false; - }); - - it('returns false if array is undefined', () => { - expect(includes(undefined, 'b')).false; - }); - }); - - describe('flatten()', () => { - it('returns empty array for ampty array', () => { - expect(flatten([])).eql([]); - }); - - it('flattens single array', () => { - expect(flatten([[1, 2, 3]])).eql([1, 2, 3]); - }); - - it('flattens multiple arrays', () => { - expect(flatten([[1, 2, 3], [4, 5]])).eql([1, 2, 3, 4, 5]); - }); - }); - - describe('at()', () => { - it('returns item at given index', () => { - expect(at(['a', 'b', 'c'], 1)).equal('b'); - }); - - it('clamps index to array length', () => { - expect(at(['a', 'b', 'c'], 4)).equal('c'); - }); - - it('clamps index to 0', () => { - expect(at(['a', 'b', 'c'], -1)).equal('a'); - }); - - it('converts index to integer', () => { - expect(at(['a', 'b', 'c'], '1.3')).equal('b'); - }); - - it('returns undefined for empty items list', () => { - expect(at([], 0)).undefined; - }); - }); - - describe('att()', () => { - it('returns item at given index', () => { - expect(att(['a', 'b', 'c'], 1)).equal('b'); - }); - - it('clamps index to array length', () => { - expect(att(['a', 'b', 'c'], 4)).equal('c'); - }); - - it('clamps index to 0', () => { - expect(att(['a', 'b', 'c'], -1)).equal('a'); - }); - - it('converts index to integer', () => { - expect(att(['a', 'b', 'c'], '1.3')).equal('b'); - }); - - it('returns undefined for undefined items list', () => { - expect(att(undefined, 0)).undefined; - }); - - it('returns undefined for null items list', () => { - expect(att(null, 0)).undefined; - }); - - it('returns undefined for empty items list', () => { - expect(att([], 0)).undefined; - }); - }); - - describe('findById()', () => { - it('returns item with given ID', () => { - expect(findById([{ id: 'a' }, { id: 'b' }, { id: 'c' }], 'b')).eql({ id: 'b' }); - }); - - it('returns undefined if not found', () => { - expect(findById([{ id: 'a' }, { id: 'b' }, { id: 'c' }], 'd')).undefined; - }); - }); - - describe('arraysEqual()', () => { - it('returns false for identical arrays', () => { - expect(arraysEqual([1, 2], [1, 2])).true; - }); - - it('returns false for different size arrays', () => { - expect(arraysEqual([1], [1, 2])).false; - }); - - it('returns false for different values in arrays', () => { - expect(arraysEqual([1, 3], [1, 2])).false; - }); - }); - - describe('removeItem()', () => { - it('removes given item', () => { - const array = [1, 2, 3]; - expect(removeItem(array, 2)).true; - expect(array).eql([1, 3]); - }); - - it('removes only first instance of element', () => { - const array = [1, 2, 3, 2]; - expect(removeItem(array, 2)).true; - expect(array).eql([1, 3, 2]); - }); - - it('does nothing for empty array', () => { - const array: any[] = []; - expect(removeItem(array, 2)).false; - expect(array).eql([]); - }); - - it('does nothing if iten does not exist', () => { - const array = [1, 2, 3]; - expect(removeItem(array, 5)).false; - expect(array).eql([1, 2, 3]); - }); - }); - - describe('removeById()', () => { - it('removes item with given ID', () => { - const x = { id: 2 }; - const array = [{ id: 1 }, x, { id: 3 }]; - expect(removeById(array, 2)).equal(x); - expect(array).eql([{ id: 1 }, { id: 3 }]); - }); - - it('removes only first instance of element', () => { - const array = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 2 }]; - removeById(array, 2); - expect(array).eql([{ id: 1 }, { id: 3 }, { id: 2 }]); - }); - - it('does nothing for empty array', () => { - const array: any[] = []; - expect(removeById(array, 2)).undefined; - expect(array).eql([]); - }); - - it('does nothing if iten does not exist', () => { - const array = [{ id: 1 }, { id: 2 }, { id: 3 }]; - expect(removeById(array, 5)).undefined; - expect(array).eql([{ id: 1 }, { id: 2 }, { id: 3 }]); - }); - }); - - describe('dispose()', () => { - it('calls dispose method on give object', () => { - const disp = spy(); - dispose({ dispose: disp }); - assert.called(disp); - }); - - it('returns undefined', () => { - expect(dispose({ dispose() { return 5; } })).undefined; - }); - - it('does nothing for undefined', () => { - dispose(undefined); - }); - }); - - describe('distance()', () => { - it('returns distance between points', () => { - expect(distance({ x: 10, y: 0 }, { x: 20, y: 0 })).equal(10); - }); - }); - - describe('contains()', () => { - it('returns true if point is inside moved bounds', () => { - expect(contains(10, 10, { x: 0, y: 0, w: 100, h: 100 }, { x: 12, y: 12 })).true; - }); - - it('returns false if point is not inside moved bounds', () => { - expect(contains(10, 10, { x: 0, y: 0, w: 100, h: 100 }, { x: 0, y: 0 })).false; - }); - }); - - describe('collidersIntersect()', () => { - it('returns true if two colliders intersect', () => { - expect(collidersIntersect(0, 0, rect(10, 10, 20, 20), 0, 0, rect(15, 15, 20, 20))).true; - }); - - it('returns true if two colliders intersect when moved', () => { - expect(collidersIntersect(15, 15, rect(10, 10, 10, 10), 0, 0, rect(30, 30, 10, 10))).true; - expect(collidersIntersect(0, 0, rect(10, 10, 10, 10), -15, -15, rect(30, 30, 10, 10))).true; - }); - - it('returns true if two colliders do not intersect', () => { - expect(collidersIntersect(0, 0, rect(10, 10, 20, 20), 0, 0, rect(50, 50, 20, 20))).false; - }); - - it('returns true if two colliders touch edges', () => { - expect(collidersIntersect(0, 0, rect(10, 10, 10, 10), 0, 0, rect(20, 10, 20, 10))).false; - expect(collidersIntersect(0, 0, rect(10, 10, 10, 10), 0, 0, rect(10, 20, 10, 20))).false; - }); - - it('returns false if two colliders do not intersect when moved', () => { - expect(collidersIntersect(0, 0, rect(10, 10, 20, 20), 20, 20, rect(15, 15, 20, 20))).false; - }); - }); - - describe('isCommand()', () => { - it('returns false for regular text', () => { - expect(isCommand('hello')).false; - }); - - it('returns true for text starting with /', () => { - expect(isCommand('/test')).true; - }); - - it('returns true for "/"', () => { - expect(isCommand('/')).true; - }); - }); - - describe('processCommand()', () => { - it('returns command and args', () => { - expect(processCommand('/foo bar')).eql({ command: 'foo', args: 'bar' }); - }); - - it('parses command without args', () => { - expect(processCommand('/foo')).eql({ command: 'foo', args: '' }); - }); - - it('trims command name', () => { - expect(processCommand('/foo ')).eql({ command: 'foo', args: '' }); - }); - - it('trims args', () => { - expect(processCommand('/foo bar ')).eql({ command: 'foo', args: 'bar' }); - }); - }); + afterEach(() => { + clock.restore(); + }); + + it('returns date object', () => { + expect(fromNow(0)).instanceof(Date); + }); + + it('returns current time for 0 offset', () => { + clock.setSystemTime(123); + + expect(fromNow(0).getTime()).equal(123); + }); + + it('returns current time offset by given amount', () => { + clock.setSystemTime(123); + + expect(fromNow(100).getTime()).equal(223); + }); + }); + + describe('maxDate()', () => { + it('returns larger of two dates', () => { + expect(maxDate(new Date(123), new Date(122))!.getTime()).equal(123); + }); + + it('returns larger of two dates (2)', () => { + expect(maxDate(new Date(123), new Date(124))!.getTime()).equal(124); + }); + + it('returns non-undefined date', () => { + expect(maxDate(new Date(123), undefined)!.getTime()).equal(123); + }); + + it('returns non-undefined date (2)', () => { + expect(maxDate(undefined, new Date(123))!.getTime()).equal(123); + }); + + it('returns undefined if both dates are undefined', () => { + expect(maxDate(undefined, undefined)).undefined; + }); + }); + + describe('minDate()', () => { + it('returns smaller of two dates', () => { + expect(minDate(new Date(123), new Date(122))!.getTime()).equal(122); + }); + + it('returns smaller of two dates (2)', () => { + expect(minDate(new Date(123), new Date(124))!.getTime()).equal(123); + }); + + it('returns non-undefined date', () => { + expect(minDate(new Date(123), undefined)!.getTime()).equal(123); + }); + + it('returns non-undefined date (2)', () => { + expect(minDate(undefined, new Date(123))!.getTime()).equal(123); + }); + + it('returns undefined if both dates are undefined', () => { + expect(minDate(undefined, undefined)).undefined; + }); + }); + + describe('formatDuration()', () => { + it('returns 0s for 0 duration', () => { + expect(formatDuration(0)).equal('0s'); + }); + + it('returns duration seconds', () => { + expect(formatDuration(15000)).equal('15s'); + }); + + it('returns duration minutes and seconds', () => { + expect(formatDuration(15 * 60 * 1000 + 6 * 1000)).equal('15m 6s'); + }); + + it('returns duration hours and minutes', () => { + expect(formatDuration(15 * 3600 * 1000 + 13 * 60 * 1000 + 6 * 1000)).equal('15h 13m'); + }); + + it('returns days and hours', () => { + expect(formatDuration((10 + 24 * 2) * 3600 * 1000 + 13 * 60 * 1000 + 6 * 1000)).equal('2d 10h'); + }); + }); + + describe('clamp()', () => { + it('returns given value if within range', () => { + expect(clamp(2, 1, 3)).equal(2); + }); + + it('returns minimum for value below minimum', () => { + expect(clamp(0, 1, 3)).equal(1); + }); + + it('returns maximum for value above maximum', () => { + expect(clamp(5, 1, 3)).equal(3); + }); + + it('returns minimum for NaN', () => { + expect(clamp(NaN, 1, 3)).equal(1); + }); + }); + + describe('normalize()', () => { + it('returns given values as vector', () => { + expect(normalize(1, 0)).eql({ x: 1, y: 0 }); + }); + + it('returns normalized vector', () => { + expect(normalize(0, -5)).eql({ x: 0, y: -1 }); + }); + }); + + describe('toInt()', () => { + it('returns given integer value', () => { + expect(toInt(1)).equal(1); + expect(toInt(-5)).equal(-5); + }); + + it('converts float number to integer value', () => { + expect(toInt(1.5)).eql(1); + expect(toInt(0.15)).eql(0); + expect(toInt(123.9)).eql(123); + }); + + it('converts string to integer value', () => { + expect(toInt('1.5')).eql(1); + expect(toInt('5')).eql(5); + }); + + it('converts null or undefined to 0', () => { + expect(toInt(null)).eql(0); + expect(toInt(undefined)).eql(0); + }); + + it('converts any object or array to 0', () => { + expect(toInt([])).eql(0); + expect(toInt({})).eql(0); + }); + }); + + describe('hasFlag()', () => { + enum Foo { + Aaa = 1, + Bbb = 2, + } + + it('returns true if flag is set', () => { + expect(hasFlag(Foo.Aaa, Foo.Aaa)).true; + }); + + it('returns true if flag is also set', () => { + expect(hasFlag(Foo.Aaa | Foo.Bbb, Foo.Aaa)).true; + }); + + it('returns false if flag is not set', () => { + expect(hasFlag(Foo.Bbb, Foo.Aaa)).false; + }); + }); + + describe('setFlag()', () => { + enum Foo { + Aaa = 1, + Bbb = 2, + } + + it('sets flag', () => { + expect(setFlag(0, Foo.Aaa, true)).equal(Foo.Aaa); + }); + + it('unsets flag', () => { + expect(setFlag(Foo.Aaa, Foo.Aaa, false)).equal(0); + }); + + it('does nothing if already set', () => { + expect(setFlag(Foo.Aaa, Foo.Aaa, true)).equal(Foo.Aaa); + }); + + it('does nothing if already unset', () => { + expect(setFlag(0, Foo.Aaa, false)).equal(0); + }); + + it('sets with another flag', () => { + expect(setFlag(Foo.Bbb, Foo.Aaa, true)).equal(Foo.Aaa | Foo.Bbb); + }); + + it('unsets flag with another flag', () => { + expect(setFlag(Foo.Bbb | Foo.Aaa, Foo.Aaa, false)).equal(Foo.Bbb); + }); + }); + + describe('includes()', () => { + it('returns true if array includes given element', () => { + expect(includes(['a', 'b', 'c'], 'b')).true; + }); + + it('returns false if array does not include given element', () => { + expect(includes(['a', 'b', 'c'], 'd')).false; + }); + + it('returns false if array is undefined', () => { + expect(includes(undefined, 'b')).false; + }); + }); + + describe('flatten()', () => { + it('returns empty array for ampty array', () => { + expect(flatten([])).eql([]); + }); + + it('flattens single array', () => { + expect(flatten([[1, 2, 3]])).eql([1, 2, 3]); + }); + + it('flattens multiple arrays', () => { + expect(flatten([[1, 2, 3], [4, 5]])).eql([1, 2, 3, 4, 5]); + }); + }); + + describe('at()', () => { + it('returns item at given index', () => { + expect(at(['a', 'b', 'c'], 1)).equal('b'); + }); + + it('clamps index to array length', () => { + expect(at(['a', 'b', 'c'], 4)).equal('c'); + }); + + it('clamps index to 0', () => { + expect(at(['a', 'b', 'c'], -1)).equal('a'); + }); + + it('converts index to integer', () => { + expect(at(['a', 'b', 'c'], '1.3')).equal('b'); + }); + + it('returns undefined for empty items list', () => { + expect(at([], 0)).undefined; + }); + }); + + describe('att()', () => { + it('returns item at given index', () => { + expect(att(['a', 'b', 'c'], 1)).equal('b'); + }); + + it('clamps index to array length', () => { + expect(att(['a', 'b', 'c'], 4)).equal('c'); + }); + + it('clamps index to 0', () => { + expect(att(['a', 'b', 'c'], -1)).equal('a'); + }); + + it('converts index to integer', () => { + expect(att(['a', 'b', 'c'], '1.3')).equal('b'); + }); + + it('returns undefined for undefined items list', () => { + expect(att(undefined, 0)).undefined; + }); + + it('returns undefined for null items list', () => { + expect(att(null, 0)).undefined; + }); + + it('returns undefined for empty items list', () => { + expect(att([], 0)).undefined; + }); + }); + + describe('findById()', () => { + it('returns item with given ID', () => { + expect(findById([{ id: 'a' }, { id: 'b' }, { id: 'c' }], 'b')).eql({ id: 'b' }); + }); + + it('returns undefined if not found', () => { + expect(findById([{ id: 'a' }, { id: 'b' }, { id: 'c' }], 'd')).undefined; + }); + }); + + describe('arraysEqual()', () => { + it('returns false for identical arrays', () => { + expect(arraysEqual([1, 2], [1, 2])).true; + }); + + it('returns false for different size arrays', () => { + expect(arraysEqual([1], [1, 2])).false; + }); + + it('returns false for different values in arrays', () => { + expect(arraysEqual([1, 3], [1, 2])).false; + }); + }); + + describe('removeItem()', () => { + it('removes given item', () => { + const array = [1, 2, 3]; + expect(removeItem(array, 2)).true; + expect(array).eql([1, 3]); + }); + + it('removes only first instance of element', () => { + const array = [1, 2, 3, 2]; + expect(removeItem(array, 2)).true; + expect(array).eql([1, 3, 2]); + }); + + it('does nothing for empty array', () => { + const array: any[] = []; + expect(removeItem(array, 2)).false; + expect(array).eql([]); + }); + + it('does nothing if iten does not exist', () => { + const array = [1, 2, 3]; + expect(removeItem(array, 5)).false; + expect(array).eql([1, 2, 3]); + }); + }); + + describe('removeById()', () => { + it('removes item with given ID', () => { + const x = { id: 2 }; + const array = [{ id: 1 }, x, { id: 3 }]; + expect(removeById(array, 2)).equal(x); + expect(array).eql([{ id: 1 }, { id: 3 }]); + }); + + it('removes only first instance of element', () => { + const array = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 2 }]; + removeById(array, 2); + expect(array).eql([{ id: 1 }, { id: 3 }, { id: 2 }]); + }); + + it('does nothing for empty array', () => { + const array: any[] = []; + expect(removeById(array, 2)).undefined; + expect(array).eql([]); + }); + + it('does nothing if iten does not exist', () => { + const array = [{ id: 1 }, { id: 2 }, { id: 3 }]; + expect(removeById(array, 5)).undefined; + expect(array).eql([{ id: 1 }, { id: 2 }, { id: 3 }]); + }); + }); + + describe('dispose()', () => { + it('calls dispose method on give object', () => { + const disp = spy(); + dispose({ dispose: disp }); + assert.called(disp); + }); + + it('returns undefined', () => { + expect(dispose({ dispose() { return 5; } })).undefined; + }); + + it('does nothing for undefined', () => { + dispose(undefined); + }); + }); + + describe('distance()', () => { + it('returns distance between points', () => { + expect(distance({ x: 10, y: 0 }, { x: 20, y: 0 })).equal(10); + }); + }); + + describe('contains()', () => { + it('returns true if point is inside moved bounds', () => { + expect(contains(10, 10, { x: 0, y: 0, w: 100, h: 100 }, { x: 12, y: 12 })).true; + }); + + it('returns false if point is not inside moved bounds', () => { + expect(contains(10, 10, { x: 0, y: 0, w: 100, h: 100 }, { x: 0, y: 0 })).false; + }); + }); + + describe('collidersIntersect()', () => { + it('returns true if two colliders intersect', () => { + expect(collidersIntersect(0, 0, rect(10, 10, 20, 20), 0, 0, rect(15, 15, 20, 20))).true; + }); + + it('returns true if two colliders intersect when moved', () => { + expect(collidersIntersect(15, 15, rect(10, 10, 10, 10), 0, 0, rect(30, 30, 10, 10))).true; + expect(collidersIntersect(0, 0, rect(10, 10, 10, 10), -15, -15, rect(30, 30, 10, 10))).true; + }); + + it('returns true if two colliders do not intersect', () => { + expect(collidersIntersect(0, 0, rect(10, 10, 20, 20), 0, 0, rect(50, 50, 20, 20))).false; + }); + + it('returns true if two colliders touch edges', () => { + expect(collidersIntersect(0, 0, rect(10, 10, 10, 10), 0, 0, rect(20, 10, 20, 10))).false; + expect(collidersIntersect(0, 0, rect(10, 10, 10, 10), 0, 0, rect(10, 20, 10, 20))).false; + }); + + it('returns false if two colliders do not intersect when moved', () => { + expect(collidersIntersect(0, 0, rect(10, 10, 20, 20), 20, 20, rect(15, 15, 20, 20))).false; + }); + }); + + describe('isCommand()', () => { + it('returns false for regular text', () => { + expect(isCommand('hello')).false; + }); + + it('returns true for text starting with /', () => { + expect(isCommand('/test')).true; + }); + + it('returns true for "/"', () => { + expect(isCommand('/')).true; + }); + }); + + describe('processCommand()', () => { + it('returns command and args', () => { + expect(processCommand('/foo bar')).eql({ command: 'foo', args: 'bar' }); + }); + + it('parses command without args', () => { + expect(processCommand('/foo')).eql({ command: 'foo', args: '' }); + }); + + it('trims command name', () => { + expect(processCommand('/foo ')).eql({ command: 'foo', args: '' }); + }); + + it('trims args', () => { + expect(processCommand('/foo bar ')).eql({ command: 'foo', args: 'bar' }); + }); + }); }); diff --git a/src/ts/tests/generated/sprites.spec.ts b/src/ts/tests/generated/sprites.spec.ts index 84f57f5..dd5a1ea 100644 --- a/src/ts/tests/generated/sprites.spec.ts +++ b/src/ts/tests/generated/sprites.spec.ts @@ -5,48 +5,48 @@ import * as sprites from '../../generated/sprites'; /* tslint:disable */ const sets: [keyof typeof sprites, number, number, (number[] | null)[]][] = [ - // name, index, frames, expected patterns colors counts - // ['topManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], [3, 9, 9, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], [3, 9, 7, 5, 5], [3, 11, 9, 7, 5], [3, 11, 7, 5, 5], [3, 7], [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], [3, 11, 13, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5]]], - ['topManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], [3, 9, 9, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], [3, 9, 7, 5, 5], [3, 11, 9, 7, 5], [3, 11, 7, 5, 5], [3, 7], [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], [3, 11, 13, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5], [3, 11, 11, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 13, 7, 5, 5], [3, 13, 5, 11, 5], [3, 7, 5, 7, 5, 5, 7]]], - // ['frontManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], null, [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, [3, 11, 9, 7, 5], null, null, [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], null, [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5]]], - ['frontManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], null, [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, [3, 11, 9, 7, 5], null, null, [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], null, [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5], [3, 11, 11, 7, 5, 5], null, null, [3, 11, 13, 7, 5, 5], null, [3, 7, 5, 7, 5, 5, 7]]], - // ['behindManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], null, [3, 13, 13, 5, 5, 7], null, null, null, null, null, [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, null, null, null, null, [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, null, null, [3, 7, 5, 13, 5]]], - ['behindManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], null, [3, 13, 13, 5, 5, 7], null, null, null, null, null, [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, null, null, null, null, [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, null, null, [3, 7, 5, 13, 5], null, [3, 9, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], null, [3, 13, 5, 11, 5], []]], - // ['backFrontManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], null, [3, 9, 9, 7, 5, 5], null, null, null, null, [3, 11, 11, 5], null, [3, 7, 7, 5, 5], [3, 9, 9, 7, 5], null, null, [3, 7, 9, 5, 5, 5], [3, 7, 13, 5, 5, 5], [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], null, [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5]]], - ['backFrontManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], null, [3, 9, 9, 7, 5, 5], null, null, null, null, [3, 11, 11, 5], null, [3, 7, 7, 5, 5], [3, 9, 9, 7, 5], null, null, [3, 7, 9, 5, 5, 5], [3, 7, 13, 5, 5, 5], [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], null, [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5], [], [], [], [], [], []]], - // ['backBehindManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], [5, 13, 11, 9, 7, 7], null, [5, 11, 11, 7, 7, 7], [3, 7, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 9, 5, 5, 5], null, [3, 5, 11, 5], null, null, [3, 5, 5, 5], [3, 11, 7, 5, 5], null, null, [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], [3, 7, 13, 7, 5, 5], [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5]]], - ['backBehindManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], [5, 13, 11, 9, 7, 7], null, [5, 11, 11, 7, 7, 7], [3, 7, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 9, 5, 5, 5], null, [3, 5, 11, 5], null, null, [3, 5, 5, 5], [3, 11, 7, 5, 5], null, null, [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], [3, 7, 13, 7, 5, 5], [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5], [3, 7, 7, 5], [3, 11, 7, 5, 5], [3, 11, 7, 7, 5], [3, 11, 7, 7, 5, 5, 5], [3], [3, 9, 7, 5, 5]]], - ['horns', -1, 1, [null, [3, 7], [3, 5], [3, 7], [3, 5], [3, 7], [3, 9, 9], [3, 9, 13], [3, 9, 13], [3, 7], [3, 11, 5, 5], [3, 9, 5, 5], [3, 9, 5], [3, 7], [3, 9]]], - ['hornsBehind', -1, 1, [null, null, null, null, [3, 5], [3, 7], [3, 9, 9], [3, 9, 13], [3, 9, 13], [3, 7], [3, 11, 5, 5], [3, 9, 5, 5], [3, 9, 5], [3, 7], []]], - ['ears', -1, 1, [[3, 7], [3, 9], [3, 9], [3, 5], [3, 7, 7], [3, 7, 9]]], - ['earsFar', -1, 1, [[3, 7], [3, 9], [3, 9], [3, 5], [3, 7, 7], [3, 7, 9]]], - ['frontLegHooves', 1, 39, [null, [3], [3], [5], [5], [5], [3]]], - ['backLegHooves', 1, 27, [null, [3], [3], [5], [3]]], - ['wings', 1, 13, [null, [3, 5, 9], [5], [3, 5, 9], [3]]], - ['tails', 0, 3, [null, [3, 11, 13, 7, 5, 5], [3, 13, 9, 7, 5, 5], [3, 11, 9, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 13, 7, 5, 5, 5], [5, 13, 11, 7, 7], [3, 11, 13, 5, 7, 5], [3, 11, 11, 7, 5, 5, 11], [3, 11, 13, 5], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 5, 9, 5], [5, 11, 9, 7, 7], [5, 7], [3, 5, 7], [5, 11, 9, 7, 7], [7, 13, 11, 9, 9], [5, 7], [3, 11, 13, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 11, 13, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 13, 7], [3, 5, 13, 13, 11, 11], [3, 9, 13, 13, 13, 11], [3, 7, 5, 13, 5], [5, 13, 9, 9, 7, 7], [5, 13, 9, 9, 7, 7], [3, 5, 7]]], - ['noses', 0, 26, [[3], [3], [5]]], - ['facialHair', -1, 1, [null, null, null, [3], [3, 7, 9], [3, 5], null, [3, 7, 17], [3, 17, 7], [3, 13, 7, 7], [3, 13, 7, 9], [3, 13, 7, 9], [3, 13, 7, 9], [5, 13, 9, 13], [5, 13, 9, 13], [3, 17, 7, 11]]], - ['facialHairBehind', -1, 1, [null, [3], [3, 7], null, [3, 7, 9], [3, 5], [3, 7, 9], [3, 7, 17], [3, 17, 7], [3, 13, 7, 7], [3, 13, 7, 9], [3, 13, 7, 9], [3, 13, 7, 9], [5, 13, 9, 13], [5, 13, 9, 13], [3, 17, 7, 11]]], - ['headAccessoriesBehind', -1, 1, [null, [3, 5], [3, 9, 9, 9], [3, 5, 7, 9, 7], [3, 5], [3, 7], [3, 7, 13], [3, 11, 11], [3, 11, 13], [3, 11, 7, 9, 9], [3, 9, 13], [5, 11, 13], [3, 7], [3, 9, 13], [3, 9], [3, 9], [3, 7], [3, 7], [3, 7, 5, 5], [3, 7, 11]]], - ['neckAccessories', 1, 16, [null, [3, 5], [3, 5, 9, 5], [3, 5, 7, 13, 7], [3, 5], [3, 7, 7, 5, 5], [3, 5, 7], [3, 5], [3], [3], [3, 5, 9, 5], [3, 5, 9], [3, 9], [3], [3]]], - ['frontLegAccessories', 1, 39, [null, [3, 13, 13, 13, 13]]], - ['backLegAccessories', 1, 27, [null, [3, 13, 13, 13, 13]]], - ['chestAccessories', 1, 16, [null, [3, 5, 9], [3, 7, 5, 5, 7, 5, 7, 5, 7, 11, 9, 5, 11], [5, 7, 11], [5, 7]]], - ['backAccessories', 1, 16, [null, [3], [3, 7, 13, 5], [3, 7, 13, 5], [3, 9, 13, 5], [3, 3]]], - ['waistAccessories', 1, 17, [null, [9], [9], [9]]], - ['earAccessories', -1, 1, [null, [3], [3], [3], [3], [5], [3, 11], [3], [3], [3], [3, 7, 7, 7], [3, 11, 11, 11], [3]]], - ['earAccessoriesBehind', -1, 1, [null, null, null, null, null, null, null, null, [3], null, [3, 7, 7, 7], [3, 11, 11, 11], []]], - ['extraAccessories', -1, 1, [[11], [5], [5], [7], [9], [13], [5], [11], [9], [11, 11], [9], [9, 9], [7], [9], null, null, [13], [13]]], - ['extraAccessoriesBehind', -1, 1, [[11], [5], null, [7], [9], null, null, null, null, null, null, null, [7], [9], [5], [5], [13], []]], + // name, index, frames, expected patterns colors counts + // ['topManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], [3, 9, 9, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], [3, 9, 7, 5, 5], [3, 11, 9, 7, 5], [3, 11, 7, 5, 5], [3, 7], [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], [3, 11, 13, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5]]], + ['topManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], [3, 9, 9, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], [3, 9, 7, 5, 5], [3, 11, 9, 7, 5], [3, 11, 7, 5, 5], [3, 7], [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], [3, 11, 13, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5], [3, 11, 11, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 13, 7, 5, 5], [3, 13, 5, 11, 5], [3, 7, 5, 7, 5, 5, 7]]], + // ['frontManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], null, [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, [3, 11, 9, 7, 5], null, null, [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], null, [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5]]], + ['frontManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], [3, 11, 11, 7, 5, 5], [3, 13, 13, 5, 5, 7], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 11, 9, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], null, [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, [3, 11, 9, 7, 5], null, null, [3, 11, 9, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], [3, 13, 13, 7, 5, 5, 9], null, [3, 13, 11, 7, 5, 5], [3, 7, 5, 13, 5], [3, 11, 11, 7, 5, 5], null, null, [3, 11, 13, 7, 5, 5], null, [3, 7, 5, 7, 5, 5, 7]]], + // ['behindManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], null, [3, 13, 13, 5, 5, 7], null, null, null, null, null, [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, null, null, null, null, [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, null, null, [3, 7, 5, 13, 5]]], + ['behindManes', -1, 1, [null, [3, 13, 13, 7, 5, 5], null, [3, 13, 13, 5, 5, 7], null, null, null, null, null, [3, 13, 13, 7, 5], [3, 13, 13, 7, 5, 5, 13], [3, 9, 11, 5, 5], [3, 9, 9, 7, 5, 5], null, null, null, null, null, [3, 13, 13, 7, 5, 5], null, [3, 11, 11, 7, 5, 5], [3, 13, 13, 7, 5, 5], null, null, null, [3, 7, 5, 13, 5], null, [3, 9, 13, 7, 5, 5], [3, 9, 13, 7, 5, 5], null, [3, 13, 5, 11, 5], []]], + // ['backFrontManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], null, [3, 9, 9, 7, 5, 5], null, null, null, null, [3, 11, 11, 5], null, [3, 7, 7, 5, 5], [3, 9, 9, 7, 5], null, null, [3, 7, 9, 5, 5, 5], [3, 7, 13, 5, 5, 5], [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], null, [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5]]], + ['backFrontManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], null, [3, 9, 9, 7, 5, 5], null, null, null, null, [3, 11, 11, 5], null, [3, 7, 7, 5, 5], [3, 9, 9, 7, 5], null, null, [3, 7, 9, 5, 5, 5], [3, 7, 13, 5, 5, 5], [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], null, [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5], [], [], [], [], [], []]], + // ['backBehindManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], [5, 13, 11, 9, 7, 7], null, [5, 11, 11, 7, 7, 7], [3, 7, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 9, 5, 5, 5], null, [3, 5, 11, 5], null, null, [3, 5, 5, 5], [3, 11, 7, 5, 5], null, null, [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], [3, 7, 13, 7, 5, 5], [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5]]], + ['backBehindManes', -1, 1, [null, [3, 7, 7, 5, 5, 5], [5, 13, 11, 9, 7, 7], null, [5, 11, 11, 7, 7, 7], [3, 7, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 9, 5, 5, 5], null, [3, 5, 11, 5], null, null, [3, 5, 5, 5], [3, 11, 7, 5, 5], null, null, [3, 7, 7, 7, 5, 5], [5, 13, 13, 9, 7, 7], [3, 7, 5, 7, 5, 5], [3, 7, 13, 7, 5, 5], [5, 9, 13, 7], [3, 7, 13, 7, 5, 5], [3, 9, 5, 9, 5], [3, 7, 7, 5], [3, 11, 7, 5, 5], [3, 11, 7, 7, 5], [3, 11, 7, 7, 5, 5, 5], [3], [3, 9, 7, 5, 5]]], + ['horns', -1, 1, [null, [3, 7], [3, 5], [3, 7], [3, 5], [3, 7], [3, 9, 9], [3, 9, 13], [3, 9, 13], [3, 7], [3, 11, 5, 5], [3, 9, 5, 5], [3, 9, 5], [3, 7], [3, 9]]], + ['hornsBehind', -1, 1, [null, null, null, null, [3, 5], [3, 7], [3, 9, 9], [3, 9, 13], [3, 9, 13], [3, 7], [3, 11, 5, 5], [3, 9, 5, 5], [3, 9, 5], [3, 7], []]], + ['ears', -1, 1, [[3, 7], [3, 9], [3, 9], [3, 5], [3, 7, 7], [3, 7, 9]]], + ['earsFar', -1, 1, [[3, 7], [3, 9], [3, 9], [3, 5], [3, 7, 7], [3, 7, 9]]], + ['frontLegHooves', 1, 39, [null, [3], [3], [5], [5], [5], [3]]], + ['backLegHooves', 1, 27, [null, [3], [3], [5], [3]]], + ['wings', 1, 13, [null, [3, 5, 9], [5], [3, 5, 9], [3]]], + ['tails', 0, 3, [null, [3, 11, 13, 7, 5, 5], [3, 13, 9, 7, 5, 5], [3, 11, 9, 7, 5, 5], [3, 13, 11, 7, 5, 5], [3, 9, 13, 7, 5, 5], [3, 11, 13, 7, 5, 5, 5], [5, 13, 11, 7, 7], [3, 11, 13, 5, 7, 5], [3, 11, 11, 7, 5, 5, 11], [3, 11, 13, 5], [3, 11, 11, 7, 5, 5], [3, 9, 9, 7, 5], [3, 5, 9, 5], [5, 11, 9, 7, 7], [5, 7], [3, 5, 7], [5, 11, 9, 7, 7], [7, 13, 11, 9, 9], [5, 7], [3, 11, 13, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 11, 13, 7, 5, 5], [3, 9, 11, 7, 5, 5], [3, 13, 7], [3, 5, 13, 13, 11, 11], [3, 9, 13, 13, 13, 11], [3, 7, 5, 13, 5], [5, 13, 9, 9, 7, 7], [5, 13, 9, 9, 7, 7], [3, 5, 7]]], + ['noses', 0, 26, [[3], [3], [5]]], + ['facialHair', -1, 1, [null, null, null, [3], [3, 7, 9], [3, 5], null, [3, 7, 17], [3, 17, 7], [3, 13, 7, 7], [3, 13, 7, 9], [3, 13, 7, 9], [3, 13, 7, 9], [5, 13, 9, 13], [5, 13, 9, 13], [3, 17, 7, 11]]], + ['facialHairBehind', -1, 1, [null, [3], [3, 7], null, [3, 7, 9], [3, 5], [3, 7, 9], [3, 7, 17], [3, 17, 7], [3, 13, 7, 7], [3, 13, 7, 9], [3, 13, 7, 9], [3, 13, 7, 9], [5, 13, 9, 13], [5, 13, 9, 13], [3, 17, 7, 11]]], + ['headAccessoriesBehind', -1, 1, [null, [3, 5], [3, 9, 9, 9], [3, 5, 7, 9, 7], [3, 5], [3, 7], [3, 7, 13], [3, 11, 11], [3, 11, 13], [3, 11, 7, 9, 9], [3, 9, 13], [5, 11, 13], [3, 7], [3, 9, 13], [3, 9], [3, 9], [3, 7], [3, 7], [3, 7, 5, 5], [3, 7, 11]]], + ['neckAccessories', 1, 16, [null, [3, 5], [3, 5, 9, 5], [3, 5, 7, 13, 7], [3, 5], [3, 7, 7, 5, 5], [3, 5, 7], [3, 5], [3], [3], [3, 5, 9, 5], [3, 5, 9], [3, 9], [3], [3]]], + ['frontLegAccessories', 1, 39, [null, [3, 13, 13, 13, 13]]], + ['backLegAccessories', 1, 27, [null, [3, 13, 13, 13, 13]]], + ['chestAccessories', 1, 16, [null, [3, 5, 9], [3, 7, 5, 5, 7, 5, 7, 5, 7, 11, 9, 5, 11], [5, 7, 11], [5, 7]]], + ['backAccessories', 1, 16, [null, [3], [3, 7, 13, 5], [3, 7, 13, 5], [3, 9, 13, 5], [3, 3]]], + ['waistAccessories', 1, 17, [null, [9], [9], [9]]], + ['earAccessories', -1, 1, [null, [3], [3], [3], [3], [5], [3, 11], [3], [3], [3], [3, 7, 7, 7], [3, 11, 11, 11], [3]]], + ['earAccessoriesBehind', -1, 1, [null, null, null, null, null, null, null, null, [3], null, [3, 7, 7, 7], [3, 11, 11, 11], []]], + ['extraAccessories', -1, 1, [[11], [5], [5], [7], [9], [13], [5], [11], [9], [11, 11], [9], [9, 9], [7], [9], null, null, [13], [13]]], + ['extraAccessoriesBehind', -1, 1, [[11], [5], null, [7], [9], null, null, null, null, null, null, null, [7], [9], [5], [5], [13], []]], ]; describe('sprites', () => { - describe('pony', () => { - sets.map(([name, index, frames, expectedPatterns]) => it(name, () => { - const field = sprites[name] as any; - const counts = map(index !== -1 ? field[index] : field, (x: any[] | undefined) => x ? x.map(y => y.colors) : null); - expect(index === -1 ? 1 : field.length).eql(frames, 'frame count'); - expect(counts).eql(expectedPatterns, `\n\nACT: ${JSON.stringify(counts)}\nEXP: ${JSON.stringify(expectedPatterns)}\n\n`); - })); - }); + describe('pony', () => { + sets.map(([name, index, frames, expectedPatterns]) => it(name, () => { + const field = sprites[name] as any; + const counts = map(index !== -1 ? field[index] : field, (x: any[] | undefined) => x ? x.map(y => y.colors) : null); + expect(index === -1 ? 1 : field.length).eql(frames, 'frame count'); + expect(counts).eql(expectedPatterns, `\n\nACT: ${JSON.stringify(counts)}\nEXP: ${JSON.stringify(expectedPatterns)}\n\n`); + })); + }); }); diff --git a/src/ts/tests/graphics/graphicsUtils.spec.ts b/src/ts/tests/graphics/graphicsUtils.spec.ts index 9cd170d..bdf06bc 100644 --- a/src/ts/tests/graphics/graphicsUtils.spec.ts +++ b/src/ts/tests/graphics/graphicsUtils.spec.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { SpriteBatch, MessageType, PaletteSpriteBatch } from '../../common/interfaces'; import { rect } from '../../common/rect'; import { - drawBaloon, drawNamePlate, drawPixelText, drawOutline, drawBounds, createCommonPalettes, DrawNameFlags + drawBaloon, drawNamePlate, drawPixelText, drawOutline, drawBounds, createCommonPalettes, DrawNameFlags } from '../../graphics/graphicsUtils'; import { drawCanvas } from '../../graphics/contextSpriteBatch'; import { ORANGE } from '../../common/colors'; @@ -17,173 +17,173 @@ const palettes = createCommonPalettes(mockPaletteManager); const created = Date.now(); function test(file: string, draw: (batch: SpriteBatch & PaletteSpriteBatch) => void) { - return () => { - const filePath = path.join(baseFilePath, file); - const expected = loadImageAsCanvas(filePath); - const actual = drawCanvas(100, 50, paletteSpriteSheet, undefined, draw); - compareCanvases(expected, actual, filePath, 'graphics'); - }; + return () => { + const filePath = path.join(baseFilePath, file); + const expected = loadImageAsCanvas(filePath); + const actual = drawCanvas(100, 50, paletteSpriteSheet, undefined, draw); + compareCanvases(expected, actual, filePath, 'graphics'); + }; } describe('graphicsUtils', () => { - before(loadSprites); - before(() => clearCompareResults('graphics')); + before(loadSprites); + before(() => clearCompareResults('graphics')); - describe('drawBaloon()', () => { - it('draws regular text', test('hello.png', batch => { - drawBaloon(batch, { message: 'hello', created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + describe('drawBaloon()', () => { + it('draws regular text', test('hello.png', batch => { + drawBaloon(batch, { message: 'hello', created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws regular text with emoji', test('hello-apple.png', batch => { - drawBaloon(batch, { message: 'hello 🍎', created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws regular text with emoji', test('hello-apple.png', batch => { + drawBaloon(batch, { message: 'hello 🍎', created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws text with new lines', test('newline.png', batch => { - drawBaloon(batch, { message: 'hello\nworld!', created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws text with new lines', test('newline.png', batch => { + drawBaloon(batch, { message: 'hello\nworld!', created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('does not draw if outside bounds', test('outside.png', batch => { - drawBaloon(batch, { message: 'cant see me', created }, 50, 25, rect(0, 0, 10, 10), palettes); - })); + it('does not draw if outside bounds', test('outside.png', batch => { + drawBaloon(batch, { message: 'cant see me', created }, 50, 25, rect(0, 0, 10, 10), palettes); + })); - it('draws party message', test('party.png', batch => { - drawBaloon(batch, { message: 'party', type: MessageType.Party, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws party message', test('party.png', batch => { + drawBaloon(batch, { message: 'party', type: MessageType.Party, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws moderator message', test('mod.png', batch => { - drawBaloon(batch, { message: 'moderator', type: MessageType.Mod, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws moderator message', test('mod.png', batch => { + drawBaloon(batch, { message: 'moderator', type: MessageType.Mod, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws admin message', test('admin.png', batch => { - drawBaloon(batch, { message: 'admin', type: MessageType.Admin, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws admin message', test('admin.png', batch => { + drawBaloon(batch, { message: 'admin', type: MessageType.Admin, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws supporter1 message', test('sup1.png', batch => { - drawBaloon(batch, { message: 'supporter 1', type: MessageType.Supporter1, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws supporter1 message', test('sup1.png', batch => { + drawBaloon(batch, { message: 'supporter 1', type: MessageType.Supporter1, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws supporter2 message', test('sup2.png', batch => { - drawBaloon(batch, { message: 'supporter 2', type: MessageType.Supporter2, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws supporter2 message', test('sup2.png', batch => { + drawBaloon(batch, { message: 'supporter 2', type: MessageType.Supporter2, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws supporter3 message', test('sup3.png', batch => { - drawBaloon(batch, { message: 'supporter 3', type: MessageType.Supporter3, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws supporter3 message', test('sup3.png', batch => { + drawBaloon(batch, { message: 'supporter 3', type: MessageType.Supporter3, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws announcement message', test('announcement.png', batch => { - drawBaloon( - batch, { message: 'announcement', type: MessageType.Announcement, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws announcement message', test('announcement.png', batch => { + drawBaloon( + batch, { message: 'announcement', type: MessageType.Announcement, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws party announcement message', test('party-announcement.png', batch => { - drawBaloon( - batch, { message: 'announcement', type: MessageType.PartyAnnouncement, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws party announcement message', test('party-announcement.png', batch => { + drawBaloon( + batch, { message: 'announcement', type: MessageType.PartyAnnouncement, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws system message', test('system.png', batch => { - drawBaloon(batch, { message: 'system', type: MessageType.System, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws system message', test('system.png', batch => { + drawBaloon(batch, { message: 'system', type: MessageType.System, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws thinking bubble', test('thinking.png', batch => { - drawBaloon(batch, { message: 'thinking...', type: MessageType.Thinking, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws thinking bubble', test('thinking.png', batch => { + drawBaloon(batch, { message: 'thinking...', type: MessageType.Thinking, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('draws party thinking bubble', test('party-thinking.png', batch => { - drawBaloon( - batch, { message: 'party thinking...', type: MessageType.PartyThinking, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('draws party thinking bubble', test('party-thinking.png', batch => { + drawBaloon( + batch, { message: 'party thinking...', type: MessageType.PartyThinking, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('fades in text bubble (time: 10)', test('fade-in-0.png', batch => { - drawBaloon(batch, { message: 'fade', timer: 10, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('fades in text bubble (time: 10)', test('fade-in-0.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 10, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('fades in text bubble (time: 9.95)', test('fade-in-1.png', batch => { - drawBaloon(batch, { message: 'fade', timer: 9.95, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('fades in text bubble (time: 9.95)', test('fade-in-1.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 9.95, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('fades in text bubble (time: 9.7)', test('fade-in-2.png', batch => { - drawBaloon(batch, { message: 'fade', timer: 9.7, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('fades in text bubble (time: 9.7)', test('fade-in-2.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 9.7, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('fades out text bubble (time: 0)', test('fade-out-0.png', batch => { - drawBaloon(batch, { message: 'fade', timer: 0, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('fades out text bubble (time: 0)', test('fade-out-0.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 0, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('fades out text bubble (time: 0.05)', test('fade-out-1.png', batch => { - drawBaloon(batch, { message: 'fade', timer: 0.05, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('fades out text bubble (time: 0.05)', test('fade-out-1.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 0.05, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it('fades out text bubble (time: 0.3)', test('fade-out-2.png', batch => { - drawBaloon(batch, { message: 'fade', timer: 0.3, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + it('fades out text bubble (time: 0.3)', test('fade-out-2.png', batch => { + drawBaloon(batch, { message: 'fade', timer: 0.3, total: 10, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - const chatTypes: [string, MessageType][] = [ - ['chat', MessageType.Chat], - ['think', MessageType.Thinking], - ]; + const chatTypes: [string, MessageType][] = [ + ['chat', MessageType.Chat], + ['think', MessageType.Thinking], + ]; - chatTypes.forEach(([name, type]) => { - it(`breaks words on small screen (${name})`, test(`break-${name}.png`, batch => { - drawBaloon(batch, { message: 'too long to fit in one line', type, created }, 50, 25, rect(0, 0, 100, 100), palettes); - })); + chatTypes.forEach(([name, type]) => { + it(`breaks words on small screen (${name})`, test(`break-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit in one line', type, created }, 50, 25, rect(0, 0, 100, 100), palettes); + })); - it(`moves ballon right to fit on screen (${name})`, test(`move-right-${name}.png`, batch => { - drawBaloon(batch, { message: 'too long to fit', type, created }, 25, 25, rect(0, 0, 100, 100), palettes); - })); + it(`moves ballon right to fit on screen (${name})`, test(`move-right-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit', type, created }, 25, 25, rect(0, 0, 100, 100), palettes); + })); - it(`moves ballon left to fit on screen (${name})`, test(`move-left-${name}.png`, batch => { - drawBaloon(batch, { message: 'too long to fit', type, created }, 75, 25, rect(0, 0, 100, 100), palettes); - })); + it(`moves ballon left to fit on screen (${name})`, test(`move-left-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit', type, created }, 75, 25, rect(0, 0, 100, 100), palettes); + })); - it(`does not move baloon originating from outside bounds (${name})`, test(`outside-${name}.png`, batch => { - drawBaloon(batch, { message: 'too long to fit', type, created }, -25, 25, rect(0, 0, 100, 100), palettes); - })); + it(`does not move baloon originating from outside bounds (${name})`, test(`outside-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit', type, created }, -25, 25, rect(0, 0, 100, 100), palettes); + })); - it(`adjusts nipple when moving baloon (${name})`, test(`move-right-nipple-${name}.png`, batch => { - drawBaloon(batch, { message: 'too long to fit', type, created }, 1, 25, rect(0, 0, 100, 100), palettes); - })); - }); - }); + it(`adjusts nipple when moving baloon (${name})`, test(`move-right-nipple-${name}.png`, batch => { + drawBaloon(batch, { message: 'too long to fit', type, created }, 1, 25, rect(0, 0, 100, 100), palettes); + })); + }); + }); - describe('drawBounds()', () => { - it('draws bounds', test('bounds.png', batch => { - drawBounds(batch, entity(0, 0.1, 0.2), rect(5, 6, 25, 20), ORANGE); - })); + describe('drawBounds()', () => { + it('draws bounds', test('bounds.png', batch => { + drawBounds(batch, entity(0, 0.1, 0.2), rect(5, 6, 25, 20), ORANGE); + })); - it('draws nothing if rect is missing', test('bounds-none.png', batch => { - drawBounds(batch, entity(0, 0.1, 0.2), undefined, ORANGE); - })); - }); + it('draws nothing if rect is missing', test('bounds-none.png', batch => { + drawBounds(batch, entity(0, 0.1, 0.2), undefined, ORANGE); + })); + }); - describe('drawOutline()', () => { - it('draws outline', test('outline.png', batch => { - drawOutline(batch, ORANGE, 10, 15, 30, 20); - })); - }); + describe('drawOutline()', () => { + it('draws outline', test('outline.png', batch => { + drawOutline(batch, ORANGE, 10, 15, 30, 20); + })); + }); - describe('drawNamePlate()', () => { - it('draws name', test('name.png', batch => { - drawNamePlate(batch, 'name', 50, 25, DrawNameFlags.None, palettes, undefined); - })); + describe('drawNamePlate()', () => { + it('draws name', test('name.png', batch => { + drawNamePlate(batch, 'name', 50, 25, DrawNameFlags.None, palettes, undefined); + })); - it('draws name with emoji', test('name-apple.png', batch => { - drawNamePlate(batch, 'name 🍎', 50, 25, DrawNameFlags.None, palettes, undefined); - })); + it('draws name with emoji', test('name-apple.png', batch => { + drawNamePlate(batch, 'name 🍎', 50, 25, DrawNameFlags.None, palettes, undefined); + })); - it('draws party member name', test('name-party.png', batch => { - drawNamePlate(batch, 'party', 50, 25, DrawNameFlags.Party, palettes, undefined); - })); + it('draws party member name', test('name-party.png', batch => { + drawNamePlate(batch, 'party', 50, 25, DrawNameFlags.Party, palettes, undefined); + })); - const tags = ['mod', 'dev', 'sup1', 'sup2', 'sup3']; + const tags = ['mod', 'dev', 'sup1', 'sup2', 'sup3']; - tags.forEach(tag => it(`draws name with ${tag} tag`, test(`name-${tag}.png`, batch => { - drawNamePlate(batch, `A ${tag}`, 50, 25, DrawNameFlags.None, palettes, tag); - }))); - }); + tags.forEach(tag => it(`draws name with ${tag} tag`, test(`name-${tag}.png`, batch => { + drawNamePlate(batch, `A ${tag}`, 50, 25, DrawNameFlags.None, palettes, tag); + }))); + }); - describe('drawPixelText()', () => { - it('draws numbers', test('pixel-numbers.png', batch => { - drawPixelText(batch, 20, 20, 0x20B2AAff, '0 123456789X'); - })); - }); + describe('drawPixelText()', () => { + it('draws numbers', test('pixel-numbers.png', batch => { + drawPixelText(batch, 20, 20, 0x20B2AAff, '0 123456789X'); + })); + }); }); diff --git a/src/ts/tests/graphics/spriteSheetUtils.spec.ts b/src/ts/tests/graphics/spriteSheetUtils.spec.ts index cd80f86..3994852 100644 --- a/src/ts/tests/graphics/spriteSheetUtils.spec.ts +++ b/src/ts/tests/graphics/spriteSheetUtils.spec.ts @@ -6,67 +6,67 @@ import { createCanvas } from '../../client/canvasUtils'; import { SpriteSheet } from '../../common/interfaces'; function createImageData() { - return createCanvas(10, 10).getContext('2d')!.getImageData(0, 0, 10, 10); + return createCanvas(10, 10).getContext('2d')!.getImageData(0, 0, 10, 10); } describe('spriteSheetUtils', () => { - describe('createTexturesForSpriteSheets()', () => { - it('creates texture from image', () => { - const gl = {} as any; - const data = createImageData(); - const tex = {} as any; - const createTexture = stub().returns(tex); - const sheet: SpriteSheet[] = [ - { sprites: [], src: 'foo', data, texture: undefined, palette: false }, - ]; + describe('createTexturesForSpriteSheets()', () => { + it('creates texture from image', () => { + const gl = {} as any; + const data = createImageData(); + const tex = {} as any; + const createTexture = stub().returns(tex); + const sheet: SpriteSheet[] = [ + { sprites: [], src: 'foo', data, texture: undefined, palette: false }, + ]; - createTexturesForSpriteSheets(gl, sheet, createTexture); + createTexturesForSpriteSheets(gl, sheet, createTexture); - assert.calledWith(createTexture, gl, data); - expect(sheet[0].texture).equal(tex); - }); + assert.calledWith(createTexture, gl, data); + expect(sheet[0].texture).equal(tex); + }); - it('handles empty sprites', () => { - const createTexture = stub().returns({}); - const sheet: SpriteSheet[] = [ - { sprites: [undefined] as any, src: 'foo', data: createImageData(), texture: undefined, palette: false }, - ]; + it('handles empty sprites', () => { + const createTexture = stub().returns({}); + const sheet: SpriteSheet[] = [ + { sprites: [undefined] as any, src: 'foo', data: createImageData(), texture: undefined, palette: false }, + ]; - createTexturesForSpriteSheets({} as any, sheet, createTexture); - }); - }); + createTexturesForSpriteSheets({} as any, sheet, createTexture); + }); + }); - // describe('releaseTexturesForSpriteSheets()', () => { - // it('disposes textures', () => { - // const deleteTexture = stub(); - // const sheet: SpriteSheet[] = [ - // { sprites: [], src: 'foo', texture: { gl: { deleteTexture } } as any, palette: false }, - // ]; + // describe('releaseTexturesForSpriteSheets()', () => { + // it('disposes textures', () => { + // const deleteTexture = stub(); + // const sheet: SpriteSheet[] = [ + // { sprites: [], src: 'foo', texture: { gl: { deleteTexture } } as any, palette: false }, + // ]; - // disposeTexturesForSpriteSheets(sheet); + // disposeTexturesForSpriteSheets(sheet); - // assert.calledOnce(deleteTexture); - // expect(sheet[0].texture).undefined; - // }); + // assert.calledOnce(deleteTexture); + // expect(sheet[0].texture).undefined; + // }); - // it('handles empty texture', () => { - // const sheet: SpriteSheet[] = [ - // { sprites: [], src: 'foo', texture: undefined, palette: false }, - // ]; + // it('handles empty texture', () => { + // const sheet: SpriteSheet[] = [ + // { sprites: [], src: 'foo', texture: undefined, palette: false }, + // ]; - // disposeTexturesForSpriteSheets(sheet); - // }); + // disposeTexturesForSpriteSheets(sheet); + // }); - // it('handles empty sprites', () => { - // const sheet: SpriteSheet[] = [ - // { sprites: [undefined], src: 'foo', texture: undefined, palette: false }, - // ]; + // it('handles empty sprites', () => { + // const sheet: SpriteSheet[] = [ + // { sprites: [undefined], src: 'foo', texture: undefined, palette: false }, + // ]; - // disposeTexturesForSpriteSheets(sheet); - // }); + // disposeTexturesForSpriteSheets(sheet); + // }); - // it('does nothing for empty list', () => { - // disposeTexturesForSpriteSheets([]); - // }); - // }); + // it('does nothing for empty list', () => { + // disposeTexturesForSpriteSheets([]); + // }); + // }); }); diff --git a/src/ts/tests/lib.ts b/src/ts/tests/lib.ts index 0276b23..b9c1104 100644 --- a/src/ts/tests/lib.ts +++ b/src/ts/tests/lib.ts @@ -27,110 +27,110 @@ require('chai').use(require('chai-as-promised')); setPaletteManager(mockPaletteManager); export function loadImageServer(src: string) { - return loadImage(pathTo('assets', src)); + return loadImage(pathTo('assets', src)); } export const loadSprites = once(() => loadAndInitSheets(spriteSheets, loadImageServer)); export function loadImageAsCanvas(filePath: string): HTMLCanvasElement { - try { - const image = loadImageSync(filePath); - const expected = createCanvas(image.width, image.height); - expected.getContext('2d')!.drawImage(image, 0, 0); - return expected; - } catch (e) { - console.error(e); - } + try { + const image = loadImageSync(filePath); + const expected = createCanvas(image.width, image.height); + expected.getContext('2d')!.drawImage(image, 0, 0); + return expected; + } catch (e) { + console.error(e); + } - return createCanvas(0, 0); + return createCanvas(0, 0); } export function generateDiff(expectedPath: string, actualPath: string) { - spawnSync( - 'magick', ['compare', actualPath, expectedPath, actualPath.replace(/\.png$/, '-diff.png')], { encoding: 'utf8' }); + spawnSync( + 'magick', ['compare', actualPath, expectedPath, actualPath.replace(/\.png$/, '-diff.png')], { encoding: 'utf8' }); } export async function clearCompareResults(group: string) { - await del([pathTo('tools', 'temp', group, '*.png').replace(/\\/g, '/')]); + await del([pathTo('tools', 'temp', group, '*.png').replace(/\\/g, '/')]); } export function compareCanvases( - expected: HTMLCanvasElement | undefined, actual: HTMLCanvasElement | undefined, - filePath: string, group: string, diff = true + expected: HTMLCanvasElement | undefined, actual: HTMLCanvasElement | undefined, + filePath: string, group: string, diff = true ) { - try { - if (expected === actual) - return; - if (!expected) - throw new Error(`Expected canvas is null`); - if (!actual) - throw new Error(`Actual canvas is null`); - if (expected.width !== actual.width || expected.height !== actual.height) - throw new Error(`Canvas size is different than expected`); + try { + if (expected === actual) + return; + if (!expected) + throw new Error(`Expected canvas is null`); + if (!actual) + throw new Error(`Actual canvas is null`); + if (expected.width !== actual.width || expected.height !== actual.height) + throw new Error(`Canvas size is different than expected`); - const expectedData = expected.getContext('2d')!.getImageData(0, 0, expected.width, expected.height); - const actualData = actual.getContext('2d')!.getImageData(0, 0, actual.width, actual.height); - const length = expectedData.width * expectedData.height * 4; + const expectedData = expected.getContext('2d')!.getImageData(0, 0, expected.width, expected.height); + const actualData = actual.getContext('2d')!.getImageData(0, 0, actual.width, actual.height); + const length = expectedData.width * expectedData.height * 4; - for (let i = 0; i < length; i++) { - if (expectedData.data[i] !== actualData.data[i]) { - const x = Math.floor(i / 4) % actualData.width; - const y = Math.floor((i / 4) / actualData.width); - throw new Error(`Actual canvas different than expected at (${x}, ${y})`); - } - } - } catch (e) { - if (actual && diff) { - const tempRoot = pathTo('tools', 'temp', group); - const tempPath = path.join(tempRoot, filePath ? path.basename(filePath) : `${Date.now()}-failed-test.png`); - fs.writeFileSync(tempPath, actual.toBuffer()); + for (let i = 0; i < length; i++) { + if (expectedData.data[i] !== actualData.data[i]) { + const x = Math.floor(i / 4) % actualData.width; + const y = Math.floor((i / 4) / actualData.width); + throw new Error(`Actual canvas different than expected at (${x}, ${y})`); + } + } + } catch (e) { + if (actual && diff) { + const tempRoot = pathTo('tools', 'temp', group); + const tempPath = path.join(tempRoot, filePath ? path.basename(filePath) : `${Date.now()}-failed-test.png`); + fs.writeFileSync(tempPath, actual.toBuffer()); - if (filePath) { - generateDiff(filePath, tempPath); - } - } + if (filePath) { + generateDiff(filePath, tempPath); + } + } - throw e; - } + throw e; + } } const testsPath = pathTo('src', 'tests', 'filters'); export function readTestsFile(fileName: string): string[] { - const lines = fs.readFileSync(path.join(testsPath, fileName), 'utf8') - .split(/\r?\n/g) - .map(x => x.trim()) - .filter(x => !!x); + const lines = fs.readFileSync(path.join(testsPath, fileName), 'utf8') + .split(/\r?\n/g) + .map(x => x.trim()) + .filter(x => !!x); - for (let i = lines.length - 1; i > 0; i--) { - if (lines.indexOf(lines[i]) < i) { - console.error(`Duplicate line "${lines[i]}" in ${fileName}`); - } - } + for (let i = lines.length - 1; i > 0; i--) { + if (lines.indexOf(lines[i]) < i) { + console.error(`Duplicate line "${lines[i]}" in ${fileName}`); + } + } - return lines; + return lines; } export function createFunctionWithPromiseHandler(ctor: any, ...deps: any[]): any { - return (...args: any[]) => { - let result: any; - const func = ctor(...deps, (promise: Promise, handleError: any) => result = promise.catch(handleError)); - func(...args); - return result; - }; + return (...args: any[]) => { + let result: any; + const func = ctor(...deps, (promise: Promise, handleError: any) => result = promise.catch(handleError)); + func(...args); + return result; + }; } export function stubClass(ctor: new (...args: any[]) => T): SinonStubbedInstance { - return createStubInstance(ctor) as any; + return createStubInstance(ctor) as any; } export function stubFromInstance(instance: any): SinonStubbedInstance { - return mapValues(instance, () => stub()) as any; + return mapValues(instance, () => stub()) as any; } export function resetStubMethods(stub: SinonStubbedInstance, ...methods: (keyof T)[]) { - methods.forEach(method => { - (stub[method] as any).resetBehavior(); - (stub[method] as any).reset(); - }); + methods.forEach(method => { + (stub[method] as any).resetBehavior(); + (stub[method] as any).reset(); + }); } diff --git a/src/ts/tests/mocks.ts b/src/ts/tests/mocks.ts index 38a65fd..5c7348b 100644 --- a/src/ts/tests/mocks.ts +++ b/src/ts/tests/mocks.ts @@ -14,137 +14,137 @@ import { createBinaryWriter } from 'ag-sockets'; import { PONY_TYPE } from '../common/constants'; export function auth(item: Partial): IAuth { - return item as IAuth; + return item as IAuth; } export function account(item: Partial): IAccount { - return item as IAccount; + return item as IAccount; } export function character(item: Partial): ICharacter { - return item as ICharacter; + return item as ICharacter; } export function mock(ctor: new (...args: any[]) => T, fields: any = {}): T { - const object: any = {}; - const prototype = ctor.prototype; + const object: any = {}; + const prototype = ctor.prototype; - Object.getOwnPropertyNames(prototype) - .filter(key => !Object.getOwnPropertyDescriptor(prototype, key)!.get && typeof prototype[key] === 'function') - .forEach(key => object[key] = function () { }); + Object.getOwnPropertyNames(prototype) + .filter(key => !Object.getOwnPropertyDescriptor(prototype, key)!.get && typeof prototype[key] === 'function') + .forEach(key => object[key] = function () { }); - return Object.assign(object, fields); + return Object.assign(object, fields); } export function entity(id: number, x = 0, y = 0, type = 0, more: Partial = {}): Entity { - return { - id, x, y, z: 0, vx: 0, vy: 0, type, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0, - options: {}, ...more - }; + return { + id, x, y, z: 0, vx: 0, vy: 0, type, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0, + options: {}, ...more + }; } export function serverEntity(id: number, x = 0, y = 0, type = 0, more: Partial = {}): ServerEntity { - return { - id, x, y, z: 0, vx: 0, vy: 0, type, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0, - options: {}, ...more - }; + return { + id, x, y, z: 0, vx: 0, vy: 0, type, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0, + options: {}, ...more + }; } export function clientPony(): ServerEntity { - return mockClient().pony; + return mockClient().pony; } let id = 1; let ponyId = 1; export function genId() { - return (++id).toString(16).padStart(24, '0'); + return (++id).toString(16).padStart(24, '0'); } export function genObjectId() { - return Types.ObjectId(genId()); + return Types.ObjectId(genId()); } export function mockClient(fields: any = {}): IClient { - const pony = entity(++ponyId, 0, 0, PONY_TYPE); - const accountId = genId(); - const characterId = genId(); + const pony = entity(++ponyId, 0, 0, PONY_TYPE); + const accountId = genId(); + const characterId = genId(); - pony.options = {}; + pony.options = {}; - const partial: Partial = { - accountId, - characterId, - ignores: new Set(), - hides: new Set(), - permaHides: new Set(), - friends: new Set(), - accountSettings: {}, - originalRequest: { headers: {} }, - account: { id: accountId, _id: Types.ObjectId(accountId), ignores: [] }, - character: { id: characterId, _id: Types.ObjectId(characterId) }, - isMod: false, - pony, - map: createServerMap('', 0, 1, 1), - notifications: [], - regions: [], - updateQueue: createBinaryWriter(128), - regionUpdates: [], - unsubscribes: [], - subscribes: [], - saysQueue: [], - lastSays: [], - lastAction: 0, - lastBoopAction: 0, - lastExpressionAction: 0, - viewWidth: 3, - viewHeight: 3, - screenSize: { width: 20, height: 20 }, - reporter: mockReporter(), - camera: createCamera(), - reportInviteLimit() { }, - disconnect() { }, - ...fields, - }; + const partial: Partial = { + accountId, + characterId, + ignores: new Set(), + hides: new Set(), + permaHides: new Set(), + friends: new Set(), + accountSettings: {}, + originalRequest: { headers: {} }, + account: { id: accountId, _id: Types.ObjectId(accountId), ignores: [] }, + character: { id: characterId, _id: Types.ObjectId(characterId) }, + isMod: false, + pony, + map: createServerMap('', 0, 1, 1), + notifications: [], + regions: [], + updateQueue: createBinaryWriter(128), + regionUpdates: [], + unsubscribes: [], + subscribes: [], + saysQueue: [], + lastSays: [], + lastAction: 0, + lastBoopAction: 0, + lastExpressionAction: 0, + viewWidth: 3, + viewHeight: 3, + screenSize: { width: 20, height: 20 }, + reporter: mockReporter(), + camera: createCamera(), + reportInviteLimit() { }, + disconnect() { }, + ...fields, + }; - const client = mock(ClientActions, partial) as IClient; + const client = mock(ClientActions, partial) as IClient; - client.pony.client = client; - return client; + client.pony.client = client; + return client; } export function mockReporter(): Reporter { - return { - info() { }, - warn() { }, - warnLog() { }, - danger() { }, - error() { }, - system() { }, - systemLog() { }, - setPony() { }, - }; + return { + info() { }, + warn() { }, + warnLog() { }, + danger() { }, + error() { }, + system() { }, + systemLog() { }, + setPony() { }, + }; } export type MockSubject = Subject & { values: (T | undefined)[]; }; export function mockSubject(): MockSubject { - const values: (T | undefined)[] = []; + const values: (T | undefined)[] = []; - return { - values, - next(value?: T) { - values.push(value); - }, - } as any; + return { + values, + next(value?: T) { + values.push(value); + }, + } as any; } export function createStubFromInstance(instance: T): SinonStubbedInstance { - return mapValues(instance, () => stub()) as any; + return mapValues(instance, () => stub()) as any; } export function setupCollider(map: IMap, x: number, y: number) { - const entity = serverEntity(0, x, y, 0); - mixColliderRect(-16, -12, 32, 24)(entity, {}, defaultWorldState); - getRegionGlobal(map, x, y).colliders.push(entity); + const entity = serverEntity(0, x, y, 0); + mixColliderRect(-16, -12, 32, 24)(entity, {}, defaultWorldState); + getRegionGlobal(map, x, y).colliders.push(entity); } diff --git a/src/ts/tests/server/accountUtils.spec.ts b/src/ts/tests/server/accountUtils.spec.ts index 30b972e..22e30f2 100644 --- a/src/ts/tests/server/accountUtils.spec.ts +++ b/src/ts/tests/server/accountUtils.spec.ts @@ -8,82 +8,82 @@ import { account, mockClient, genObjectId } from '../mocks'; import { logger } from '../../server/logger'; describe('accountUtils [server]', () => { - describe('getModInfo()', () => { - it('returns account info', () => { - const accountId = genObjectId(); - const client = mockClient({ - accountId: accountId.toString(), - account: account({ - _id: accountId, - name: 'foo', - shadow: -1, - mute: fromNow(1.1 * DAY).getTime(), - note: 'foo', - counters: { spam: 1 }, - }), - country: 'XY', - }); + describe('getModInfo()', () => { + it('returns account info', () => { + const accountId = genObjectId(); + const client = mockClient({ + accountId: accountId.toString(), + account: account({ + _id: accountId, + name: 'foo', + shadow: -1, + mute: fromNow(1.1 * DAY).getTime(), + note: 'foo', + counters: { spam: 1 }, + }), + country: 'XY', + }); - expect(getModInfo(client)).eql({ - shadow: 'perma', - mute: 'a day', - note: 'foo', - counters: { spam: 1 }, - country: 'XY', - account: `foo [${accountId.toString().substr(-3)}]`, - }); - }); + expect(getModInfo(client)).eql({ + shadow: 'perma', + mute: 'a day', + note: 'foo', + counters: { spam: 1 }, + country: 'XY', + account: `foo [${accountId.toString().substr(-3)}]`, + }); + }); - it('returns undefined for past timeouts', () => { - const client = mockClient({ - account: account({ - _id: genObjectId(), - shadow: 1000, - mute: 2000, - }), - }); + it('returns undefined for past timeouts', () => { + const client = mockClient({ + account: account({ + _id: genObjectId(), + shadow: 1000, + mute: 2000, + }), + }); - const result = getModInfo(client); + const result = getModInfo(client); - expect(result.mute).undefined; - expect(result.shadow).undefined; - }); - }); + expect(result.mute).undefined; + expect(result.shadow).undefined; + }); + }); - describe('checkIfAdmin()', () => { - let warn: SinonStub; + describe('checkIfAdmin()', () => { + let warn: SinonStub; - beforeEach(() => { - warn = stub(logger, 'warn'); - }); + beforeEach(() => { + warn = stub(logger, 'warn'); + }); - afterEach(() => { - warn.restore(); - }); + afterEach(() => { + warn.restore(); + }); - it('does nothing if not admin', () => { - checkIfNotAdmin({} as any, ''); - }); + it('does nothing if not admin', () => { + checkIfNotAdmin({} as any, ''); + }); - it('throws if admin', () => { - expect(() => checkIfNotAdmin({ roles: ['admin'] } as any, 'test')) - .throw('Cannot perform this action on admin user'); + it('throws if admin', () => { + expect(() => checkIfNotAdmin({ roles: ['admin'] } as any, 'test')) + .throw('Cannot perform this action on admin user'); - assert.calledWith(warn, 'Cannot perform this action on admin user (test)'); - }); - }); + assert.calledWith(warn, 'Cannot perform this action on admin user (test)'); + }); + }); - describe('isNew()', () => { - it('returns true if createdAt date is not set', () => { - expect(isNew(account({}))).true; - }); + describe('isNew()', () => { + it('returns true if createdAt date is not set', () => { + expect(isNew(account({}))).true; + }); - it('returns true if created less than a day ago', () => { - expect(isNew(account({ createdAt: fromNow(-DAY + 1000) }))).true; - }); + it('returns true if created less than a day ago', () => { + expect(isNew(account({ createdAt: fromNow(-DAY + 1000) }))).true; + }); - it('returns false if created more than a day ago', () => { - expect(isNew(account({ createdAt: fromNow(-2 * DAY) }))).false; - }); - }); + it('returns false if created more than a day ago', () => { + expect(isNew(account({ createdAt: fromNow(-2 * DAY) }))).false; + }); + }); }); diff --git a/src/ts/tests/server/api/account.spec.ts b/src/ts/tests/server/api/account.spec.ts index d1b8a83..c21f15c 100644 --- a/src/ts/tests/server/api/account.spec.ts +++ b/src/ts/tests/server/api/account.spec.ts @@ -2,444 +2,444 @@ import '../../lib'; import { expect } from 'chai'; import { assert, stub, SinonStub } from 'sinon'; import { - createUpdateAccount, UpdateAccount, createRemoveSite, RemoveSite, createUpdateSettings, UpdateSettings, - createGetAccountCharacters, GetAccountCharacters, GetAccountData, createGetAccountData, modCheck + createUpdateAccount, UpdateAccount, createRemoveSite, RemoveSite, createUpdateSettings, UpdateSettings, + createGetAccountCharacters, GetAccountCharacters, GetAccountData, createGetAccountData, modCheck } from '../../../server/api/account'; import { account, genObjectId } from '../../mocks'; import * as db from '../../../server/db'; describe('api account', () => { - describe('getAccountData()', () => { - let findCharacters: SinonStub; - let findAuths: SinonStub; - let getAccountData: GetAccountData; - let findFriends: SinonStub; - - beforeEach(() => { - findCharacters = stub(); - findAuths = stub(); - findFriends = stub(db, 'findFriends').resolves([]); - getAccountData = createGetAccountData(findCharacters, findAuths); - }); - - afterEach(() => { - findFriends.restore(); - }); - - it('returns account data', async () => { - findCharacters.resolves([]); - findAuths.resolves([]); - const _id = genObjectId(); - - const result = await getAccountData(account({ _id, name: 'foo', birthdate: new Date(123), characterCount: 5 })); - - expect(result).eql({ - id: _id.toString(), - name: 'foo', - birthdate: '1970-01-01', - birthyear: undefined, - characterCount: 5, - ponies: [], - settings: {}, - sites: [], - alert: undefined, - supporter: undefined, - flags: 0, - roles: undefined, - }); - }); - - it('adds mod check if account is mod', async () => { - findCharacters.resolves([]); - findAuths.resolves([]); - - const result = await getAccountData(account({ _id: genObjectId(), roles: ['mod'] })); - - expect(result.check).eql(modCheck); - }); - }); - - describe('getAccountCharacters()', () => { - let findCharacters: SinonStub; - let getAccountCharacters: GetAccountCharacters; - - beforeEach(() => { - findCharacters = stub(); - getAccountCharacters = createGetAccountCharacters(findCharacters); - }); - - it('returns empty array', async () => { - findCharacters.resolves([]); - - const result = await getAccountCharacters(account({})); - - expect(result).eql([]); - }); - - it('returns chracters array', async () => { - const accountId = genObjectId(); - const characterId = genObjectId(); - findCharacters.withArgs(accountId).resolves([ - { - _id: characterId, - name: 'foo', - info: 'info', - lastUsed: new Date(123), - }, - ]); - - const result = await getAccountCharacters(account({ _id: accountId })); - - expect(result).eql([ - { - id: characterId.toString(), - name: 'foo', - desc: '', - info: 'info', - lastUsed: '1970-01-01T00:00:00.123Z', - site: undefined, - tag: undefined, - hideSupport: undefined, - respawnAtSpawn: undefined, - }, - ]); - }); - }); - - describe('updateAccount()', () => { - let findAccount: SinonStub; - let updateAccount: UpdateAccount; - let updateOne: SinonStub; - let log: SinonStub; - - beforeEach(() => { - findAccount = stub(); - log = stub(); - updateOne = stub(db.Account, 'updateOne').returns({ exec: stub().resolves() } as any); - updateAccount = createUpdateAccount(findAccount, log); - }); - - afterEach(() => { - updateOne.restore(); - }); - - it('resolves to account data', async () => { - const account = { - _id: genObjectId(), - name: 'name', - birthdate: new Date(123), - birthyear: 123, - roles: ['role'], - settings: { foo: 'bar' }, - characterCount: 5, - save: stub(), - } as any; - findAccount.withArgs(account._id).resolves(account); - - const result = await updateAccount(account, {} as any); - - expect(result).eql({ - id: account._id.toString(), - name: 'name', - birthdate: '1970-01-01', - birthyear: 123, - roles: ['role'], - settings: { foo: 'bar' }, - supporter: undefined, - characterCount: 5, - flags: 0, - }); - }); - - it('saves account', async () => { - const acc = account({ _id: genObjectId() }); - findAccount.resolves(acc); - - await updateAccount(acc, {} as any); - - assert.calledWith(updateOne, { _id: acc._id }, {}); - }); - - it('updates account name', async () => { - const acc = account({ _id: genObjectId() }); - findAccount.resolves(acc); - - await updateAccount(acc, { name: 'foo', birthdate: '' }); - - expect(acc.name).equal('foo'); - }); - - it('logs account rename', async () => { - const acc = account({ _id: genObjectId(), name: 'bar' }); - findAccount.resolves(acc); - - await updateAccount(acc, { name: 'foo', birthdate: '' }); - - assert.calledWith(log, acc._id, 'Renamed "bar" => "foo"'); - }); - - it('cleans account name before updating', async () => { - const acc = account({ _id: genObjectId() }); - findAccount.resolves(acc); - - await updateAccount(acc, { name: 'f\t\r\noo', birthdate: '' }); - - expect(acc.name).equal('foo'); - }); - - const invalidNameValues: any[] = [ - '', - // 'a', - 'string_that_is_exceeding_character_limit_for_account_names_aaaaaaaaaaaaaaaaaaaaaaaaa', - { foo: 'bar' }, - 123, - null, - ]; - - invalidNameValues.forEach(name => it(`does not update account name if it is invalid (${name})`, async () => { - const account = { _id: genObjectId(), name: 'name', save: stub() } as any; - findAccount.resolves(account); - - await updateAccount(account, { name, birthdate: '' }); - - expect(account.name).equal('name'); - })); - - it('updates account birthdate', async () => { - const account = { _id: genObjectId(), save: stub() } as any; - findAccount.resolves(account); - - await updateAccount(account, { name: 'foo', birthdate: '2000-02-03' }); - - expect(account.birthdate.getTime()).equal(new Date('2000-02-03').getTime()); - }); - - it('does not update birthday if it has invalid value', async () => { - const account = { _id: genObjectId(), save: stub(), birthdate: new Date(321) } as any; - findAccount.resolves(account); - - await updateAccount(account, { name: 'foo', birthdate: '0123-00-01' }); - - expect(account.birthdate.getTime()).equal(new Date(321).getTime()); - }); - - it('logs birthday change', async () => { - const account = { _id: genObjectId(), name: 'bar', save: stub(), birthdate: new Date(12345) } as any; - findAccount.resolves(account); - - await updateAccount(account, { name: 'bar', birthdate: '2000-02-03' }); - - assert.calledWith(log, account._id, 'Changed birthdate 1970-01-01 (49yo) => 2000-02-03 (19yo)'); - }); - }); - - describe('updateSettings()', () => { - let findAccount: SinonStub; - let updateOne: SinonStub; - let updateSettings: UpdateSettings; - - beforeEach(() => { - findAccount = stub(); - updateOne = stub(db.Account, 'updateOne').returns({ exec: stub().resolves() } as any); - updateSettings = createUpdateSettings(findAccount); - }); - - afterEach(() => { - updateOne.restore(); - }); - - it('returns account data', async () => { - const _id = genObjectId(); - - findAccount.resolves({ - _id, - name: 'foo', - birthdate: new Date(123), - birthyear: 123, - roles: ['mod'], - settings: { foo: 'bar' }, - characterCount: 4, - flags: 1, - supporter: 1, - save() { }, - }); - - const result = await updateSettings(account({}), {}); - - expect(result).eql({ - id: _id.toString(), - name: 'foo', - birthdate: '1970-01-01', - birthyear: 123, - roles: ['mod'], - settings: { foo: 'bar' }, - characterCount: 4, - flags: 0, - supporter: 1, - }); - }); - - it('updates account settings', async () => { - const save = stub(); - const acc = { _id: genObjectId(), save, settings: undefined as any }; - findAccount.resolves(acc); - - await updateSettings(account({ _id: acc._id }), { - filterSwearWords: true, - filterCyrillic: true, - ignorePartyInvites: true, - }); - - expect(acc.settings).eql({ - filterSwearWords: true, - filterCyrillic: true, - ignorePartyInvites: true, - }); - assert.calledWith(updateOne, { _id: acc._id }, { - settings: { - filterSwearWords: true, - filterCyrillic: true, - ignorePartyInvites: true, - } - }); - }); - - it('merges account settings', async () => { - const save = stub(); - const acc = { - _id: genObjectId(), - save, - settings: { - filterSwearWords: true, - filterCyrillic: true, - ignorePartyInvites: true, - }, - }; - findAccount.resolves(acc); - - await updateSettings(account({ _id: acc._id }), { - filterSwearWords: false, - filterCyrillic: true, - ignorePartyInvites: true, - }); - - expect(acc.settings).eql({ - filterSwearWords: false, - filterCyrillic: true, - ignorePartyInvites: true, - }); - assert.calledWith(updateOne, { _id: acc._id }, { - settings: { - filterSwearWords: false, - filterCyrillic: true, - ignorePartyInvites: true, - } - }); - }); - - it('ignores missing fields', async () => { - const acc = { _id: genObjectId(), settings: undefined as any }; - findAccount.resolves(acc); - - await updateSettings(account({ _id: acc._id }), {}); - - expect(acc.settings).eql({}); - assert.calledWith(updateOne, { _id: acc._id }, { settings: {} }); - }); - - it('ignores missing settings', async () => { - const acc = { _id: genObjectId(), settings: undefined as any }; - findAccount.resolves(acc); - - await updateSettings(account({ _id: acc._id }), undefined); - - expect(acc.settings).eql({}); - assert.calledWith(updateOne, { _id: acc._id }, { settings: {} }); - }); - }); - - describe('removeSite()', () => { - let findAuth: SinonStub; - let countAuths: SinonStub; - let log: SinonStub; - let removeSite: RemoveSite; - let updateOne: SinonStub; - - beforeEach(() => { - findAuth = stub(); - countAuths = stub(); - log = stub(); - updateOne = stub(db.Auth, 'updateOne').returns({ exec: () => stub().resolves() } as any); - removeSite = createRemoveSite(findAuth, countAuths, log); - }); - - afterEach(() => { - updateOne.restore(); - }); - - it('returns empty object', async () => { - findAuth.resolves({}); - countAuths.resolves(2); - - const result = await removeSite(account({}), 'SITE_ID'); - - expect(result).eql({}); - }); - - it('disables auth', async () => { - const authId = genObjectId(); - const accountId = genObjectId(); - const auth = { _id: authId, disabled: false }; - const acc = account({ _id: accountId }); - findAuth.withArgs('SITE_ID', accountId).resolves(auth); - countAuths.withArgs(accountId).resolves(2); - - await removeSite(acc, 'SITE_ID'); - - assert.calledWithMatch(updateOne, { _id: authId }, { disabled: true }); - }); - - it('throws if siteId is not string', async () => { - findAuth.resolves(account({})); - countAuths.resolves(2); - - await expect(removeSite(account({}), {})) - .rejectedWith('Social account not found'); - }); - - it('throws if auth does not exist', async () => { - findAuth.resolves(undefined); - countAuths.resolves(2); - - await expect(removeSite(account({}), 'SITE_ID')) - .rejectedWith('Social account not found'); - }); - - it('throws if auth is disabled', async () => { - findAuth.resolves({ disabled: true }); - countAuths.resolves(2); - - await expect(removeSite(account({}), 'SITE_ID')) - .rejectedWith('Social account not found'); - }); - - it('throws if user has only one auth', async () => { - findAuth.resolves({}); - countAuths.resolves(1); - - await expect(removeSite(account({}), 'SITE_ID')) - .rejectedWith('Cannot remove your only one social account'); - }); - - it('logs auth removal', async () => { - const accountId = genObjectId(); - const authId = genObjectId(); - findAuth.resolves({ _id: authId, name: 'foo' }); - countAuths.resolves(2); + describe('getAccountData()', () => { + let findCharacters: SinonStub; + let findAuths: SinonStub; + let getAccountData: GetAccountData; + let findFriends: SinonStub; + + beforeEach(() => { + findCharacters = stub(); + findAuths = stub(); + findFriends = stub(db, 'findFriends').resolves([]); + getAccountData = createGetAccountData(findCharacters, findAuths); + }); + + afterEach(() => { + findFriends.restore(); + }); + + it('returns account data', async () => { + findCharacters.resolves([]); + findAuths.resolves([]); + const _id = genObjectId(); + + const result = await getAccountData(account({ _id, name: 'foo', birthdate: new Date(123), characterCount: 5 })); + + expect(result).eql({ + id: _id.toString(), + name: 'foo', + birthdate: '1970-01-01', + birthyear: undefined, + characterCount: 5, + ponies: [], + settings: {}, + sites: [], + alert: undefined, + supporter: undefined, + flags: 0, + roles: undefined, + }); + }); + + it('adds mod check if account is mod', async () => { + findCharacters.resolves([]); + findAuths.resolves([]); + + const result = await getAccountData(account({ _id: genObjectId(), roles: ['mod'] })); + + expect(result.check).eql(modCheck); + }); + }); + + describe('getAccountCharacters()', () => { + let findCharacters: SinonStub; + let getAccountCharacters: GetAccountCharacters; + + beforeEach(() => { + findCharacters = stub(); + getAccountCharacters = createGetAccountCharacters(findCharacters); + }); + + it('returns empty array', async () => { + findCharacters.resolves([]); + + const result = await getAccountCharacters(account({})); + + expect(result).eql([]); + }); + + it('returns chracters array', async () => { + const accountId = genObjectId(); + const characterId = genObjectId(); + findCharacters.withArgs(accountId).resolves([ + { + _id: characterId, + name: 'foo', + info: 'info', + lastUsed: new Date(123), + }, + ]); + + const result = await getAccountCharacters(account({ _id: accountId })); + + expect(result).eql([ + { + id: characterId.toString(), + name: 'foo', + desc: '', + info: 'info', + lastUsed: '1970-01-01T00:00:00.123Z', + site: undefined, + tag: undefined, + hideSupport: undefined, + respawnAtSpawn: undefined, + }, + ]); + }); + }); + + describe('updateAccount()', () => { + let findAccount: SinonStub; + let updateAccount: UpdateAccount; + let updateOne: SinonStub; + let log: SinonStub; + + beforeEach(() => { + findAccount = stub(); + log = stub(); + updateOne = stub(db.Account, 'updateOne').returns({ exec: stub().resolves() } as any); + updateAccount = createUpdateAccount(findAccount, log); + }); + + afterEach(() => { + updateOne.restore(); + }); + + it('resolves to account data', async () => { + const account = { + _id: genObjectId(), + name: 'name', + birthdate: new Date(123), + birthyear: 123, + roles: ['role'], + settings: { foo: 'bar' }, + characterCount: 5, + save: stub(), + } as any; + findAccount.withArgs(account._id).resolves(account); + + const result = await updateAccount(account, {} as any); + + expect(result).eql({ + id: account._id.toString(), + name: 'name', + birthdate: '1970-01-01', + birthyear: 123, + roles: ['role'], + settings: { foo: 'bar' }, + supporter: undefined, + characterCount: 5, + flags: 0, + }); + }); + + it('saves account', async () => { + const acc = account({ _id: genObjectId() }); + findAccount.resolves(acc); + + await updateAccount(acc, {} as any); + + assert.calledWith(updateOne, { _id: acc._id }, {}); + }); + + it('updates account name', async () => { + const acc = account({ _id: genObjectId() }); + findAccount.resolves(acc); + + await updateAccount(acc, { name: 'foo', birthdate: '' }); + + expect(acc.name).equal('foo'); + }); + + it('logs account rename', async () => { + const acc = account({ _id: genObjectId(), name: 'bar' }); + findAccount.resolves(acc); + + await updateAccount(acc, { name: 'foo', birthdate: '' }); + + assert.calledWith(log, acc._id, 'Renamed "bar" => "foo"'); + }); + + it('cleans account name before updating', async () => { + const acc = account({ _id: genObjectId() }); + findAccount.resolves(acc); + + await updateAccount(acc, { name: 'f\t\r\noo', birthdate: '' }); + + expect(acc.name).equal('foo'); + }); + + const invalidNameValues: any[] = [ + '', + // 'a', + 'string_that_is_exceeding_character_limit_for_account_names_aaaaaaaaaaaaaaaaaaaaaaaaa', + { foo: 'bar' }, + 123, + null, + ]; + + invalidNameValues.forEach(name => it(`does not update account name if it is invalid (${name})`, async () => { + const account = { _id: genObjectId(), name: 'name', save: stub() } as any; + findAccount.resolves(account); + + await updateAccount(account, { name, birthdate: '' }); + + expect(account.name).equal('name'); + })); + + it('updates account birthdate', async () => { + const account = { _id: genObjectId(), save: stub() } as any; + findAccount.resolves(account); + + await updateAccount(account, { name: 'foo', birthdate: '2000-02-03' }); + + expect(account.birthdate.getTime()).equal(new Date('2000-02-03').getTime()); + }); + + it('does not update birthday if it has invalid value', async () => { + const account = { _id: genObjectId(), save: stub(), birthdate: new Date(321) } as any; + findAccount.resolves(account); + + await updateAccount(account, { name: 'foo', birthdate: '0123-00-01' }); + + expect(account.birthdate.getTime()).equal(new Date(321).getTime()); + }); + + it('logs birthday change', async () => { + const account = { _id: genObjectId(), name: 'bar', save: stub(), birthdate: new Date(12345) } as any; + findAccount.resolves(account); + + await updateAccount(account, { name: 'bar', birthdate: '2000-02-03' }); + + assert.calledWith(log, account._id, 'Changed birthdate 1970-01-01 (49yo) => 2000-02-03 (19yo)'); + }); + }); + + describe('updateSettings()', () => { + let findAccount: SinonStub; + let updateOne: SinonStub; + let updateSettings: UpdateSettings; + + beforeEach(() => { + findAccount = stub(); + updateOne = stub(db.Account, 'updateOne').returns({ exec: stub().resolves() } as any); + updateSettings = createUpdateSettings(findAccount); + }); + + afterEach(() => { + updateOne.restore(); + }); + + it('returns account data', async () => { + const _id = genObjectId(); + + findAccount.resolves({ + _id, + name: 'foo', + birthdate: new Date(123), + birthyear: 123, + roles: ['mod'], + settings: { foo: 'bar' }, + characterCount: 4, + flags: 1, + supporter: 1, + save() { }, + }); + + const result = await updateSettings(account({}), {}); + + expect(result).eql({ + id: _id.toString(), + name: 'foo', + birthdate: '1970-01-01', + birthyear: 123, + roles: ['mod'], + settings: { foo: 'bar' }, + characterCount: 4, + flags: 0, + supporter: 1, + }); + }); + + it('updates account settings', async () => { + const save = stub(); + const acc = { _id: genObjectId(), save, settings: undefined as any }; + findAccount.resolves(acc); + + await updateSettings(account({ _id: acc._id }), { + filterSwearWords: true, + filterCyrillic: true, + ignorePartyInvites: true, + }); + + expect(acc.settings).eql({ + filterSwearWords: true, + filterCyrillic: true, + ignorePartyInvites: true, + }); + assert.calledWith(updateOne, { _id: acc._id }, { + settings: { + filterSwearWords: true, + filterCyrillic: true, + ignorePartyInvites: true, + } + }); + }); + + it('merges account settings', async () => { + const save = stub(); + const acc = { + _id: genObjectId(), + save, + settings: { + filterSwearWords: true, + filterCyrillic: true, + ignorePartyInvites: true, + }, + }; + findAccount.resolves(acc); + + await updateSettings(account({ _id: acc._id }), { + filterSwearWords: false, + filterCyrillic: true, + ignorePartyInvites: true, + }); + + expect(acc.settings).eql({ + filterSwearWords: false, + filterCyrillic: true, + ignorePartyInvites: true, + }); + assert.calledWith(updateOne, { _id: acc._id }, { + settings: { + filterSwearWords: false, + filterCyrillic: true, + ignorePartyInvites: true, + } + }); + }); + + it('ignores missing fields', async () => { + const acc = { _id: genObjectId(), settings: undefined as any }; + findAccount.resolves(acc); + + await updateSettings(account({ _id: acc._id }), {}); + + expect(acc.settings).eql({}); + assert.calledWith(updateOne, { _id: acc._id }, { settings: {} }); + }); + + it('ignores missing settings', async () => { + const acc = { _id: genObjectId(), settings: undefined as any }; + findAccount.resolves(acc); + + await updateSettings(account({ _id: acc._id }), undefined); + + expect(acc.settings).eql({}); + assert.calledWith(updateOne, { _id: acc._id }, { settings: {} }); + }); + }); + + describe('removeSite()', () => { + let findAuth: SinonStub; + let countAuths: SinonStub; + let log: SinonStub; + let removeSite: RemoveSite; + let updateOne: SinonStub; + + beforeEach(() => { + findAuth = stub(); + countAuths = stub(); + log = stub(); + updateOne = stub(db.Auth, 'updateOne').returns({ exec: () => stub().resolves() } as any); + removeSite = createRemoveSite(findAuth, countAuths, log); + }); + + afterEach(() => { + updateOne.restore(); + }); + + it('returns empty object', async () => { + findAuth.resolves({}); + countAuths.resolves(2); + + const result = await removeSite(account({}), 'SITE_ID'); + + expect(result).eql({}); + }); + + it('disables auth', async () => { + const authId = genObjectId(); + const accountId = genObjectId(); + const auth = { _id: authId, disabled: false }; + const acc = account({ _id: accountId }); + findAuth.withArgs('SITE_ID', accountId).resolves(auth); + countAuths.withArgs(accountId).resolves(2); + + await removeSite(acc, 'SITE_ID'); + + assert.calledWithMatch(updateOne, { _id: authId }, { disabled: true }); + }); + + it('throws if siteId is not string', async () => { + findAuth.resolves(account({})); + countAuths.resolves(2); + + await expect(removeSite(account({}), {})) + .rejectedWith('Social account not found'); + }); + + it('throws if auth does not exist', async () => { + findAuth.resolves(undefined); + countAuths.resolves(2); + + await expect(removeSite(account({}), 'SITE_ID')) + .rejectedWith('Social account not found'); + }); + + it('throws if auth is disabled', async () => { + findAuth.resolves({ disabled: true }); + countAuths.resolves(2); + + await expect(removeSite(account({}), 'SITE_ID')) + .rejectedWith('Social account not found'); + }); + + it('throws if user has only one auth', async () => { + findAuth.resolves({}); + countAuths.resolves(1); + + await expect(removeSite(account({}), 'SITE_ID')) + .rejectedWith('Cannot remove your only one social account'); + }); + + it('logs auth removal', async () => { + const accountId = genObjectId(); + const authId = genObjectId(); + findAuth.resolves({ _id: authId, name: 'foo' }); + countAuths.resolves(2); - await removeSite(account({ _id: accountId }), 'SITE_ID'); - - assert.calledWith(log, accountId, `removed auth: foo [${authId}]`); - }); - }); + await removeSite(account({ _id: accountId }), 'SITE_ID'); + + assert.calledWith(log, accountId, `removed auth: foo [${authId}]`); + }); + }); }); diff --git a/src/ts/tests/server/api/game.spec.ts b/src/ts/tests/server/api/game.spec.ts index f1731a1..57046e9 100644 --- a/src/ts/tests/server/api/game.spec.ts +++ b/src/ts/tests/server/api/game.spec.ts @@ -8,169 +8,169 @@ import { delay, fromNow } from '../../../common/utils'; import { genObjectId, account } from '../../mocks'; describe('api game', () => { - describe('joinGame()', () => { - let joinGame: JoinGame; - let findCharacter: SinonStub; - let join: SinonStub; - let addOrigin: SinonStub; - let hasInvites: SinonStub; - let server: InternalGameServerState; + describe('joinGame()', () => { + let joinGame: JoinGame; + let findCharacter: SinonStub; + let join: SinonStub; + let addOrigin: SinonStub; + let hasInvites: SinonStub; + let server: InternalGameServerState; - beforeEach(() => { - findCharacter = stub(); - join = stub(); - addOrigin = stub(); - hasInvites = stub(); - server = { state: { settings: {} } } as any; - const findServer = stub(); - findServer.withArgs('serverid').returns(server); + beforeEach(() => { + findCharacter = stub(); + join = stub(); + addOrigin = stub(); + hasInvites = stub(); + server = { state: { settings: {} } } as any; + const findServer = stub(); + findServer.withArgs('serverid').returns(server); - joinGame = createJoinGame( - findServer, { version: '1', host: 'http://foo.bar/', debug: false, local: false }, findCharacter, - join, addOrigin, hasInvites); - }); + joinGame = createJoinGame( + findServer, { version: '1', host: 'http://foo.bar/', debug: false, local: false }, findCharacter, + join, addOrigin, hasInvites); + }); - it('returns join token', async () => { - const a = account({ _id: genObjectId() }); - const character = {} as any; - findCharacter.withArgs('charid').returns(character); - join.withArgs(server, a, character).returns('tokenid'); + it('returns join token', async () => { + const a = account({ _id: genObjectId() }); + const character = {} as any; + findCharacter.withArgs('charid').returns(character); + join.withArgs(server, a, character).returns('tokenid'); - await expect(joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) - .eventually.eql({ token: 'tokenid' }); - }); + await expect(joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .eventually.eql({ token: 'tokenid' }); + }); - it('resolves if meets server requirement', async () => { - server.state.require = 'sup2'; - const a = account({ _id: genObjectId(), patreon: PatreonFlags.Supporter2 }); - const character = {} as any; - findCharacter.withArgs('charid').returns(character); - join.withArgs(server, a, character).returns('tokenid'); + it('resolves if meets server requirement', async () => { + server.state.require = 'sup2'; + const a = account({ _id: genObjectId(), patreon: PatreonFlags.Supporter2 }); + const character = {} as any; + findCharacter.withArgs('charid').returns(character); + join.withArgs(server, a, character).returns('tokenid'); - await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any); - }); + await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any); + }); - it('resolves if meets server requirement (invited)', async () => { - server.state.require = 'inv'; - const a = account({ _id: genObjectId() }); - const character = {} as any; - findCharacter.withArgs('charid').returns(character); - hasInvites.resolves(true); - join.withArgs(server, a, character).returns('tokenid'); + it('resolves if meets server requirement (invited)', async () => { + server.state.require = 'inv'; + const a = account({ _id: genObjectId() }); + const character = {} as any; + findCharacter.withArgs('charid').returns(character); + hasInvites.resolves(true); + join.withArgs(server, a, character).returns('tokenid'); - await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any); - }); + await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any); + }); - it('adds origin to account', async () => { - const a = account({ _id: genObjectId() }); - const origin = {} as any; - findCharacter.withArgs('charid').returns({}); + it('adds origin to account', async () => { + const a = account({ _id: genObjectId() }); + const origin = {} as any; + findCharacter.withArgs('charid').returns({}); - await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, origin); + await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, origin); - assert.calledWith(addOrigin, a, origin); - }); + assert.calledWith(addOrigin, a, origin); + }); - it('returns alert if has account alert', async () => { - const a = account({ _id: genObjectId(), alert: { message: 'test alert', expires: fromNow(9999) } }); - const origin = {} as any; - findCharacter.withArgs('charid').returns({}); + it('returns alert if has account alert', async () => { + const a = account({ _id: genObjectId(), alert: { message: 'test alert', expires: fromNow(9999) } }); + const origin = {} as any; + findCharacter.withArgs('charid').returns({}); - const result = await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, origin); + const result = await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', false, origin); - expect(result).eql({ alert: 'test alert' }); - }); + expect(result).eql({ alert: 'test alert' }); + }); - it('does not returl alert if alredy has alert', async () => { - const a = account({ _id: genObjectId(), alert: { message: 'test alert', expires: fromNow(9999) } }); - const origin = {} as any; - const character = {} as any; - findCharacter.withArgs('charid').returns(character); - join.withArgs(server, a, character).returns('tokenid'); + it('does not returl alert if alredy has alert', async () => { + const a = account({ _id: genObjectId(), alert: { message: 'test alert', expires: fromNow(9999) } }); + const origin = {} as any; + const character = {} as any; + findCharacter.withArgs('charid').returns(character); + join.withArgs(server, a, character).returns('tokenid'); - const result = await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', true, origin); + const result = await joinGame(a, 'charid', 'serverid', '1', 'http://foo.bar/', true, origin); - expect(result).eql({ token: 'tokenid' }); - }); + expect(result).eql({ token: 'tokenid' }); + }); - it('rejects if passed version is different than server version', async () => { - await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '2', 'http://foo.bar/', false, {} as any)) - .rejectedWith(VERSION_ERROR); - }); + it('rejects if passed version is different than server version', async () => { + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '2', 'http://foo.bar/', false, {} as any)) + .rejectedWith(VERSION_ERROR); + }); - it('rejects if passed url is different than server url', async () => { - await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://im.invalid/', false, {} as any)) - .rejectedWith('Invalid data'); - }); + it('rejects if passed url is different than server url', async () => { + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://im.invalid/', false, {} as any)) + .rejectedWith('Invalid data'); + }); - it('rejects if server is not found', async () => { - await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'doesnotexist', '1', 'http://foo.bar/', false, {} as any)) - .rejectedWith('Invalid data'); - }); + it('rejects if server is not found', async () => { + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'doesnotexist', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Invalid data'); + }); - it('rejects if server is offline', async () => { - server.state.settings.isServerOffline = true; + it('rejects if server is offline', async () => { + server.state.settings.isServerOffline = true; - await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) - .rejectedWith('Server is offline'); - }); + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Server is offline'); + }); - it('rejects if server is restricted', async () => { - server.state.require = 'mod'; + it('rejects if server is restricted', async () => { + server.state.require = 'mod'; - await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) - .rejectedWith('Server is restricted'); - }); + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Server is restricted'); + }); - it('rejects if character ID is missing', async () => { - await expect(joinGame( - account({ _id: genObjectId() }), undefined as any, 'serverid', '1', 'http://foo.bar/', false, {} as any)) - .rejectedWith('Invalid data'); - }); + it('rejects if character ID is missing', async () => { + await expect(joinGame( + account({ _id: genObjectId() }), undefined as any, 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Invalid data'); + }); - it('rejects if character ID is not string', async () => { - await expect(joinGame( - account({ _id: genObjectId() }), { foo: 'bar' } as any, 'serverid', '1', 'http://foo.bar/', false, {} as any)) - .rejectedWith('Invalid data'); - }); + it('rejects if character ID is not string', async () => { + await expect(joinGame( + account({ _id: genObjectId() }), { foo: 'bar' } as any, 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Invalid data'); + }); - it('rejects if character does not exist', async () => { - await expect(joinGame( - account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) - .rejectedWith('Character does not exist'); - }); + it('rejects if character does not exist', async () => { + await expect(joinGame( + account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Character does not exist'); + }); - it('rejects if already joining', async () => { - findCharacter.withArgs('charid').returns({}); - join.returns(new Promise(() => { })); - const _id = genObjectId(); + it('rejects if already joining', async () => { + findCharacter.withArgs('charid').returns({}); + join.returns(new Promise(() => { })); + const _id = genObjectId(); - joinGame(account({ _id }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any); + joinGame(account({ _id }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any); - await delay(1); - await expect(joinGame(account({ _id }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) - .rejectedWith('Already waiting for join request'); - }); + await delay(1); + await expect(joinGame(account({ _id }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Already waiting for join request'); + }); - it('does not reject if other client is already joining', async () => { - findCharacter.withArgs('charid').returns({}); - findCharacter.withArgs('charid2').returns({}); - join.returns(new Promise(() => { })); - join.returns('tokenid'); + it('does not reject if other client is already joining', async () => { + findCharacter.withArgs('charid').returns({}); + findCharacter.withArgs('charid2').returns({}); + join.returns(new Promise(() => { })); + join.returns('tokenid'); - joinGame(account({ _id: genObjectId() }), 'charid2', 'serverid', '1', 'http://foo.bar/', false, {} as any); + joinGame(account({ _id: genObjectId() }), 'charid2', 'serverid', '1', 'http://foo.bar/', false, {} as any); - await delay(1); - await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) - .eventually.eql({ token: 'tokenid' }); - }); + await delay(1); + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .eventually.eql({ token: 'tokenid' }); + }); - it('rejects if joining is blocked', async () => { - server.state.settings.blockJoining = true; - findCharacter.withArgs('charid').returns({}); + it('rejects if joining is blocked', async () => { + server.state.settings.blockJoining = true; + findCharacter.withArgs('charid').returns({}); - await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) - .rejectedWith('Cannot join to the server'); - }); - }); + await expect(joinGame(account({ _id: genObjectId() }), 'charid', 'serverid', '1', 'http://foo.bar/', false, {} as any)) + .rejectedWith('Cannot join to the server'); + }); + }); }); diff --git a/src/ts/tests/server/api/internal-common.spec.ts b/src/ts/tests/server/api/internal-common.spec.ts index 90b634b..d17f586 100644 --- a/src/ts/tests/server/api/internal-common.spec.ts +++ b/src/ts/tests/server/api/internal-common.spec.ts @@ -3,19 +3,19 @@ import { SinonStub, stub, assert } from 'sinon'; import { createReloadSettings } from '../../../server/api/internal-common'; describe('internal-common', () => { - describe('reloadSettings', () => { - let func: ReturnType; - let reloadSettings: SinonStub; + describe('reloadSettings', () => { + let func: ReturnType; + let reloadSettings: SinonStub; - beforeEach(() => { - reloadSettings = stub(); - func = createReloadSettings(reloadSettings); - }); + beforeEach(() => { + reloadSettings = stub(); + func = createReloadSettings(reloadSettings); + }); - it('reloads settings', async () => { - await func(); + it('reloads settings', async () => { + await func(); - assert.calledOnce(reloadSettings); - }); - }); + assert.calledOnce(reloadSettings); + }); + }); }); diff --git a/src/ts/tests/server/api/internal.spec.ts b/src/ts/tests/server/api/internal.spec.ts index c1673d9..4a2d3e9 100644 --- a/src/ts/tests/server/api/internal.spec.ts +++ b/src/ts/tests/server/api/internal.spec.ts @@ -2,14 +2,14 @@ import { stubClass, resetStubMethods } from '../../lib'; import { expect } from 'chai'; import { stub, assert, SinonStub, SinonFakeTimers, useFakeTimers, SinonStubbedInstance, createStubInstance } from 'sinon'; import { - createAccountChanged, createAccountStatus, createJoin, createGetServerState, createKick, createKickAll, - createGetServerStats, createAccountMerged, createAccountAround, createAction, createShutdownServer, - createNotifyUpdate, createCancelUpdate, createHiddenStats + createAccountChanged, createAccountStatus, createJoin, createGetServerState, createKick, createKickAll, + createGetServerStats, createAccountMerged, createAccountAround, createAction, createShutdownServer, + createNotifyUpdate, createCancelUpdate, createHiddenStats } from '../../../server/api/internal'; import { World } from '../../../server/world'; import { mock, mockClient, genObjectId } from '../../mocks'; import { - PatreonFlags, ServerConfig, GameServerSettings, ServerLiveSettings, ServerStats, HidingStats + PatreonFlags, ServerConfig, GameServerSettings, ServerLiveSettings, ServerStats, HidingStats } from '../../../common/adminInterfaces'; import { ICharacter, IAccount } from '../../../server/db'; import { Types } from 'mongoose'; @@ -17,554 +17,554 @@ import { HidingService } from '../../../server/services/hiding'; import { StatsTracker } from '../../../server/stats'; describe('api internal', () => { - describe('accountChanged()', () => { - let func: ReturnType; - let world: World; - let findAccount: SinonStub; - let clearTokensForAccount: SinonStub; - - beforeEach(() => { - world = new World( - {} as any, { partyChanged: { subscribe() { } } } as any, {} as any, {} as any, {} as any, - () => ({}), {} as any, {} as any); - clearTokensForAccount = stub(); - findAccount = stub(); - func = createAccountChanged(world, { clearTokensForAccount } as any, findAccount); - }); - - it('notifies world of account update', async () => { - const account: any = { _id: genObjectId() }; - findAccount.withArgs('foobar').resolves(account); - const accountUpdated = stub(world, 'accountUpdated'); - - await func('foobar'); - - assert.calledWith(accountUpdated, account); - }); - - it('clears tokens for account if account is banned', async () => { - const account = { _id: genObjectId(), ban: -1 }; - findAccount.withArgs('foobar').resolves(account); - - await func('foobar'); - - assert.calledWith(clearTokensForAccount, 'foobar'); - }); - }); - - describe('accountMerged()', () => { - const hiding = stubClass(HidingService); - let func: ReturnType; - - beforeEach(() => { - resetStubMethods(hiding, 'merged'); - func = createAccountMerged(hiding as any); - }); - - it('notifies hiding service of merge', async () => { - await func('foo', 'bar'); - - assert.calledWith(hiding.merged, 'foo', 'bar'); - }); - }); - - describe('accountStatus()', () => { - let func: ReturnType; - let world: World; - let server: ServerConfig; - let clock: SinonFakeTimers; - - beforeEach(() => { - clock = useFakeTimers(); - world = { clientsByAccount: new Map() } as any; - server = { id: 'foo' } as any; - func = createAccountStatus(world, server); - }); - - afterEach(() => { - clock.restore(); - }); - - it('returns client account status', async () => { - const client = mockClient(); - client.characterName = 'derpy'; - client.pony.name = '?????'; - client.pony.x = 5.2; - client.pony.y = 6.1; - client.userAgent = 'useragent'; - client.connectedTime = 0; - clock.setSystemTime(12 * 1000); - world.clientsByAccount.set('bar', client); - - await expect(func('bar')).eventually.eql({ - online: true, - incognito: undefined, - character: 'derpy', - duration: '12s', - server: 'foo', - map: '-', - x: 5, - y: 6, - userAgent: 'useragent', - }); - }); - - it('returns offline status for missing client', async () => { - await expect(func('bar')).eventually.eql({ online: false }); - }); - }); - - describe('accountAround()', () => { - let func: ReturnType; - - beforeEach(() => { - func = createAccountAround({ clientsByAccount: new Map() } as any); - }); - - it('returns client arount given account', async () => { - await expect(func('bar')).eventually.eql([]); - }); - }); - - describe('hiddenStats()', () => { - let hiddenStats: ReturnType; - let hiding: SinonStubbedInstance; - - beforeEach(() => { - hiding = createStubInstance(HidingService); - hiddenStats = createHiddenStats(hiding as any); - }); - - it('returns hiding stats for given account', async () => { - const result: HidingStats = {} as any; - hiding.getStatsFor.withArgs('bar').returns(result); - - await expect(hiddenStats('bar')).eventually.equal(result); - }); - }); - - describe('join()', () => { - let func: (accountId: string, ponyId: string) => Promise; - let world: World; - let server: ServerConfig; - let settings: GameServerSettings; - let clearTokensForAccount: SinonStub; - let createToken: SinonStub; - let findAccount: SinonStub; - let findCharacter: SinonStub; - let findAuth: SinonStub; - let hasInvite: SinonStub; - let account: IAccount; - let character: ICharacter; - let clock: SinonFakeTimers; - let liveSettings: ServerLiveSettings; - - beforeEach(() => { - account = { save() { }, _id: new Types.ObjectId('5983e1f7519f95530becdf7d') } as any; - character = { save() { }, _id: new Types.ObjectId('5983e1f7519f95530becdf7a') } as any; - world = mock(World); - server = { id: 'foo' } as any; - settings = {}; - liveSettings = {} as any; - clearTokensForAccount = stub(); - createToken = stub(); - findAccount = stub().withArgs('foo').resolves(account); - findCharacter = stub().withArgs('bar', 'foo').resolves(character); - findAuth = stub(); - hasInvite = stub(); - func = createJoin( - world, () => settings, server, { clearTokensForAccount, createToken } as any, findAccount, findCharacter, - findAuth, liveSettings, hasInvite); - clock = useFakeTimers(); - }); - - afterEach(() => clock.restore()); - - it('returns new token id', () => { - createToken.returns('lalala'); - - return expect(func('foo', 'bar')).eventually.equal('lalala'); - }); - - it('creates token using fetched account and character', () => { - return func('foo', 'bar') - .then(() => { - expect(createToken.args[0][0].account).equal(account); - expect(createToken.args[0][0].character).equal(character); - }); - }); - - it('kicks all other clients with the same account', () => { - const kickByAccount = stub(world, 'kickByAccount'); - - return func('foo', 'bar') - .then(() => { - assert.calledWith(kickByAccount, 'foo'); - assert.calledWith(clearTokensForAccount, 'foo'); - }); - }); - - it('updates account default server', () => { - const save = stub(account, 'save'); - server.id = 'someidhere'; - - return func('foo', 'bar') - .then(() => { - expect(account.settings).eql({ defaultServer: 'someidhere' }); - assert.calledOnce(save); - }); - }); - - it('updates account default server (with existing settings)', () => { - const save = stub(account, 'save'); - server.id = 'someidhere'; - account.settings = { ignorePartyInvites: true }; - - return func('foo', 'bar') - .then(() => { - expect(account.settings).eql({ ignorePartyInvites: true, defaultServer: 'someidhere' }); - assert.calledOnce(save); - }); - }); - - it('updates account last visit', () => { - const save = stub(account, 'save'); - clock.setSystemTime(123); - - return func('foo', 'bar') - .then(() => { - expect(account.lastVisit.toISOString()).equal(new Date(123).toISOString()); - assert.calledOnce(save); - }); - }); - - it('updates character last used', () => { - const save = stub(character, 'save'); - clock.setSystemTime(123); - - return func('foo', 'bar') - .then(() => { - expect(character.lastUsed!.toISOString()).equal(new Date(123).toISOString()); - assert.calledOnce(save); - }); - }); - - it('rejects if server is offline', () => { - settings.isServerOffline = true; - - return expect(func('foo', 'bar')).rejectedWith('Server is offline'); - }); - - it('rejects if server is restricted from user', () => { - server.require = 'mod'; - - return expect(func('foo', 'bar')).rejectedWith('Server is restricted'); - }); - - it('resolves if user meets server restrictions', () => { - server.require = 'mod'; - account.roles = ['mod']; - - return func('foo', 'bar'); - }); - - it('resolves if user meets server restrictions (supporter)', () => { - server.require = 'sup1'; - account.patreon = PatreonFlags.Supporter2; - - return func('foo', 'bar'); - }); - - it('resolves if user meets server restrictions (invited)', () => { - server.require = 'inv'; - hasInvite.resolves(true); - - return func('foo', 'bar'); - }); - - it('sets up character social site', async () => { - const site = {}; - const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); - findAuth.withArgs(siteId, account._id).resolves(site); - character.site = siteId; - - await func('foo', 'bar'); - - expect(character.auth).equal(site); - }); - - it('does not set up character social site if its missing', async () => { - const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); - findAuth.withArgs(siteId, account._id).resolves(undefined); - character.site = siteId; - - await func('foo', 'bar'); - - expect(character.auth).undefined; - }); - - it('does not set up character social site if its disabled', async () => { - const site = { disabled: true }; - const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); - findAuth.withArgs(siteId, account._id).resolves(site); - character.site = siteId; - - await func('foo', 'bar'); - - expect(character.auth).undefined; - }); - - it('does not set up character social site if its banned', async () => { - const site = { banned: true }; - const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); - findAuth.withArgs(siteId, account._id).resolves(site); - character.site = siteId; - - await func('foo', 'bar'); - - expect(character.auth).undefined; - }); - }); - - describe('getServerState()', () => { - let func: ReturnType; - let world: World; - let server: ServerConfig; - let settings: GameServerSettings; - let liveSettings: ServerLiveSettings; - - beforeEach(() => { - world = mock(World); - server = { flags: {} } as any; - settings = {}; - liveSettings = { updating: false, shutdown: false }; - func = createGetServerState(server, () => settings, world, liveSettings); - }); - - it('returns combined server state', async () => { - Object.assign(server, { id: 'aaa', name: 'bbb', path: 'ccc', desc: 'ddd', alert: 'eee', require: 'mod' }); - world.clients = [{} as any, {} as any]; - world.joinQueue = [{} as any, {} as any, {} as any]; - world.maps = [{} as any, {} as any]; - settings.isServerOffline = true; - settings.filterSwears = true; - - const result = await func(); - - expect(result).eql({ - id: 'aaa', - name: 'bbb', - path: 'ccc', - desc: 'ddd', - alert: 'eee', - dead: false, - maps: 2, - online: 2, - onMain: 2, - queued: 3, - require: 'mod', - flags: {}, - flag: undefined, - host: undefined, - settings, - shutdown: false, - }); - }); - - it('uses defaults for missing values', async () => { - Object.assign(server, { id: 'aaa', name: 'bbb', path: 'ccc', desc: 'ddd' }); - world.clients = [{} as any, {} as any]; - world.joinQueue = []; - world.maps = []; - - const result = await func(); - - expect(result).eql({ - id: 'aaa', - name: 'bbb', - path: 'ccc', - desc: 'ddd', - alert: undefined, - dead: false, - maps: 0, - online: 2, - onMain: 2, - queued: 0, - require: undefined, - host: undefined, - flags: {}, - flag: undefined, - settings, - shutdown: false, - }); - }); - }); - - describe('getServerStats()', () => { - let stats: SinonStubbedInstance; - let func: ReturnType; - - beforeEach(() => { - stats = createStubInstance(StatsTracker); - func = createGetServerStats(stats as any); - }); - - it('returns socket stats', async () => { - const result: ServerStats = {} as any; - stats.getSocketStats.returns(result); - - await expect(func()).eventually.equal(result); - }); - }); - - describe('action()', () => { - let action: ReturnType; - - beforeEach(() => { - action = createAction({} as any); - }); - - it('throws if action is invalid', async () => { - await expect(action('foo', 'foobar')).rejectedWith('Invalid action (foo)'); - }); - }); - - describe('kick()', () => { - let func: ReturnType; - let world: World; - let clearTokensForAccount: SinonStub; - - beforeEach(() => { - world = mock(World); - clearTokensForAccount = stub(); - func = createKick(world, { clearTokensForAccount } as any); - }); - - it('kicks clients by account ID', async () => { - const kickByAccount = stub(world, 'kickByAccount'); - - await func('foo', undefined); - - assert.calledWith(kickByAccount, 'foo'); - }); - - it('clears tokens by account ID', async () => { - await func('foo', undefined); - - assert.calledWith(clearTokensForAccount, 'foo'); - }); - - it('kicks clients by character ID', async () => { - const kickByCharacter = stub(world, 'kickByCharacter'); - - await func(undefined, 'bar'); - - assert.calledWith(kickByCharacter, 'bar'); - }); - - it('does nothing if ID is not provided', async () => { - await func(undefined, undefined); - }); - }); - - describe('kickAll()', () => { - let func: ReturnType; - let world: World; - let clearTokensAll: SinonStub; - - beforeEach(() => { - world = mock(World); - clearTokensAll = stub(); - func = createKickAll(world, { clearTokensAll } as any); - }); - - it('kicks all clients', async () => { - const kickAll = stub(world, 'kickAll'); - - await func(); - - assert.calledOnce(kickAll); - }); + describe('accountChanged()', () => { + let func: ReturnType; + let world: World; + let findAccount: SinonStub; + let clearTokensForAccount: SinonStub; + + beforeEach(() => { + world = new World( + {} as any, { partyChanged: { subscribe() { } } } as any, {} as any, {} as any, {} as any, + () => ({}), {} as any, {} as any); + clearTokensForAccount = stub(); + findAccount = stub(); + func = createAccountChanged(world, { clearTokensForAccount } as any, findAccount); + }); + + it('notifies world of account update', async () => { + const account: any = { _id: genObjectId() }; + findAccount.withArgs('foobar').resolves(account); + const accountUpdated = stub(world, 'accountUpdated'); + + await func('foobar'); + + assert.calledWith(accountUpdated, account); + }); + + it('clears tokens for account if account is banned', async () => { + const account = { _id: genObjectId(), ban: -1 }; + findAccount.withArgs('foobar').resolves(account); + + await func('foobar'); + + assert.calledWith(clearTokensForAccount, 'foobar'); + }); + }); + + describe('accountMerged()', () => { + const hiding = stubClass(HidingService); + let func: ReturnType; + + beforeEach(() => { + resetStubMethods(hiding, 'merged'); + func = createAccountMerged(hiding as any); + }); + + it('notifies hiding service of merge', async () => { + await func('foo', 'bar'); + + assert.calledWith(hiding.merged, 'foo', 'bar'); + }); + }); + + describe('accountStatus()', () => { + let func: ReturnType; + let world: World; + let server: ServerConfig; + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + world = { clientsByAccount: new Map() } as any; + server = { id: 'foo' } as any; + func = createAccountStatus(world, server); + }); + + afterEach(() => { + clock.restore(); + }); + + it('returns client account status', async () => { + const client = mockClient(); + client.characterName = 'derpy'; + client.pony.name = '?????'; + client.pony.x = 5.2; + client.pony.y = 6.1; + client.userAgent = 'useragent'; + client.connectedTime = 0; + clock.setSystemTime(12 * 1000); + world.clientsByAccount.set('bar', client); + + await expect(func('bar')).eventually.eql({ + online: true, + incognito: undefined, + character: 'derpy', + duration: '12s', + server: 'foo', + map: '-', + x: 5, + y: 6, + userAgent: 'useragent', + }); + }); + + it('returns offline status for missing client', async () => { + await expect(func('bar')).eventually.eql({ online: false }); + }); + }); + + describe('accountAround()', () => { + let func: ReturnType; + + beforeEach(() => { + func = createAccountAround({ clientsByAccount: new Map() } as any); + }); + + it('returns client arount given account', async () => { + await expect(func('bar')).eventually.eql([]); + }); + }); + + describe('hiddenStats()', () => { + let hiddenStats: ReturnType; + let hiding: SinonStubbedInstance; + + beforeEach(() => { + hiding = createStubInstance(HidingService); + hiddenStats = createHiddenStats(hiding as any); + }); + + it('returns hiding stats for given account', async () => { + const result: HidingStats = {} as any; + hiding.getStatsFor.withArgs('bar').returns(result); + + await expect(hiddenStats('bar')).eventually.equal(result); + }); + }); + + describe('join()', () => { + let func: (accountId: string, ponyId: string) => Promise; + let world: World; + let server: ServerConfig; + let settings: GameServerSettings; + let clearTokensForAccount: SinonStub; + let createToken: SinonStub; + let findAccount: SinonStub; + let findCharacter: SinonStub; + let findAuth: SinonStub; + let hasInvite: SinonStub; + let account: IAccount; + let character: ICharacter; + let clock: SinonFakeTimers; + let liveSettings: ServerLiveSettings; + + beforeEach(() => { + account = { save() { }, _id: new Types.ObjectId('5983e1f7519f95530becdf7d') } as any; + character = { save() { }, _id: new Types.ObjectId('5983e1f7519f95530becdf7a') } as any; + world = mock(World); + server = { id: 'foo' } as any; + settings = {}; + liveSettings = {} as any; + clearTokensForAccount = stub(); + createToken = stub(); + findAccount = stub().withArgs('foo').resolves(account); + findCharacter = stub().withArgs('bar', 'foo').resolves(character); + findAuth = stub(); + hasInvite = stub(); + func = createJoin( + world, () => settings, server, { clearTokensForAccount, createToken } as any, findAccount, findCharacter, + findAuth, liveSettings, hasInvite); + clock = useFakeTimers(); + }); + + afterEach(() => clock.restore()); + + it('returns new token id', () => { + createToken.returns('lalala'); + + return expect(func('foo', 'bar')).eventually.equal('lalala'); + }); + + it('creates token using fetched account and character', () => { + return func('foo', 'bar') + .then(() => { + expect(createToken.args[0][0].account).equal(account); + expect(createToken.args[0][0].character).equal(character); + }); + }); + + it('kicks all other clients with the same account', () => { + const kickByAccount = stub(world, 'kickByAccount'); + + return func('foo', 'bar') + .then(() => { + assert.calledWith(kickByAccount, 'foo'); + assert.calledWith(clearTokensForAccount, 'foo'); + }); + }); + + it('updates account default server', () => { + const save = stub(account, 'save'); + server.id = 'someidhere'; + + return func('foo', 'bar') + .then(() => { + expect(account.settings).eql({ defaultServer: 'someidhere' }); + assert.calledOnce(save); + }); + }); + + it('updates account default server (with existing settings)', () => { + const save = stub(account, 'save'); + server.id = 'someidhere'; + account.settings = { ignorePartyInvites: true }; + + return func('foo', 'bar') + .then(() => { + expect(account.settings).eql({ ignorePartyInvites: true, defaultServer: 'someidhere' }); + assert.calledOnce(save); + }); + }); + + it('updates account last visit', () => { + const save = stub(account, 'save'); + clock.setSystemTime(123); + + return func('foo', 'bar') + .then(() => { + expect(account.lastVisit.toISOString()).equal(new Date(123).toISOString()); + assert.calledOnce(save); + }); + }); + + it('updates character last used', () => { + const save = stub(character, 'save'); + clock.setSystemTime(123); + + return func('foo', 'bar') + .then(() => { + expect(character.lastUsed!.toISOString()).equal(new Date(123).toISOString()); + assert.calledOnce(save); + }); + }); + + it('rejects if server is offline', () => { + settings.isServerOffline = true; + + return expect(func('foo', 'bar')).rejectedWith('Server is offline'); + }); + + it('rejects if server is restricted from user', () => { + server.require = 'mod'; + + return expect(func('foo', 'bar')).rejectedWith('Server is restricted'); + }); + + it('resolves if user meets server restrictions', () => { + server.require = 'mod'; + account.roles = ['mod']; + + return func('foo', 'bar'); + }); + + it('resolves if user meets server restrictions (supporter)', () => { + server.require = 'sup1'; + account.patreon = PatreonFlags.Supporter2; + + return func('foo', 'bar'); + }); + + it('resolves if user meets server restrictions (invited)', () => { + server.require = 'inv'; + hasInvite.resolves(true); + + return func('foo', 'bar'); + }); + + it('sets up character social site', async () => { + const site = {}; + const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); + findAuth.withArgs(siteId, account._id).resolves(site); + character.site = siteId; + + await func('foo', 'bar'); + + expect(character.auth).equal(site); + }); + + it('does not set up character social site if its missing', async () => { + const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); + findAuth.withArgs(siteId, account._id).resolves(undefined); + character.site = siteId; + + await func('foo', 'bar'); + + expect(character.auth).undefined; + }); + + it('does not set up character social site if its disabled', async () => { + const site = { disabled: true }; + const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); + findAuth.withArgs(siteId, account._id).resolves(site); + character.site = siteId; + + await func('foo', 'bar'); + + expect(character.auth).undefined; + }); + + it('does not set up character social site if its banned', async () => { + const site = { banned: true }; + const siteId = new Types.ObjectId('5983e1f7519f95530becdf70'); + findAuth.withArgs(siteId, account._id).resolves(site); + character.site = siteId; + + await func('foo', 'bar'); + + expect(character.auth).undefined; + }); + }); + + describe('getServerState()', () => { + let func: ReturnType; + let world: World; + let server: ServerConfig; + let settings: GameServerSettings; + let liveSettings: ServerLiveSettings; + + beforeEach(() => { + world = mock(World); + server = { flags: {} } as any; + settings = {}; + liveSettings = { updating: false, shutdown: false }; + func = createGetServerState(server, () => settings, world, liveSettings); + }); + + it('returns combined server state', async () => { + Object.assign(server, { id: 'aaa', name: 'bbb', path: 'ccc', desc: 'ddd', alert: 'eee', require: 'mod' }); + world.clients = [{} as any, {} as any]; + world.joinQueue = [{} as any, {} as any, {} as any]; + world.maps = [{} as any, {} as any]; + settings.isServerOffline = true; + settings.filterSwears = true; + + const result = await func(); + + expect(result).eql({ + id: 'aaa', + name: 'bbb', + path: 'ccc', + desc: 'ddd', + alert: 'eee', + dead: false, + maps: 2, + online: 2, + onMain: 2, + queued: 3, + require: 'mod', + flags: {}, + flag: undefined, + host: undefined, + settings, + shutdown: false, + }); + }); + + it('uses defaults for missing values', async () => { + Object.assign(server, { id: 'aaa', name: 'bbb', path: 'ccc', desc: 'ddd' }); + world.clients = [{} as any, {} as any]; + world.joinQueue = []; + world.maps = []; + + const result = await func(); + + expect(result).eql({ + id: 'aaa', + name: 'bbb', + path: 'ccc', + desc: 'ddd', + alert: undefined, + dead: false, + maps: 0, + online: 2, + onMain: 2, + queued: 0, + require: undefined, + host: undefined, + flags: {}, + flag: undefined, + settings, + shutdown: false, + }); + }); + }); + + describe('getServerStats()', () => { + let stats: SinonStubbedInstance; + let func: ReturnType; + + beforeEach(() => { + stats = createStubInstance(StatsTracker); + func = createGetServerStats(stats as any); + }); + + it('returns socket stats', async () => { + const result: ServerStats = {} as any; + stats.getSocketStats.returns(result); + + await expect(func()).eventually.equal(result); + }); + }); + + describe('action()', () => { + let action: ReturnType; + + beforeEach(() => { + action = createAction({} as any); + }); + + it('throws if action is invalid', async () => { + await expect(action('foo', 'foobar')).rejectedWith('Invalid action (foo)'); + }); + }); + + describe('kick()', () => { + let func: ReturnType; + let world: World; + let clearTokensForAccount: SinonStub; + + beforeEach(() => { + world = mock(World); + clearTokensForAccount = stub(); + func = createKick(world, { clearTokensForAccount } as any); + }); + + it('kicks clients by account ID', async () => { + const kickByAccount = stub(world, 'kickByAccount'); + + await func('foo', undefined); + + assert.calledWith(kickByAccount, 'foo'); + }); + + it('clears tokens by account ID', async () => { + await func('foo', undefined); + + assert.calledWith(clearTokensForAccount, 'foo'); + }); + + it('kicks clients by character ID', async () => { + const kickByCharacter = stub(world, 'kickByCharacter'); + + await func(undefined, 'bar'); + + assert.calledWith(kickByCharacter, 'bar'); + }); + + it('does nothing if ID is not provided', async () => { + await func(undefined, undefined); + }); + }); + + describe('kickAll()', () => { + let func: ReturnType; + let world: World; + let clearTokensAll: SinonStub; + + beforeEach(() => { + world = mock(World); + clearTokensAll = stub(); + func = createKickAll(world, { clearTokensAll } as any); + }); + + it('kicks all clients', async () => { + const kickAll = stub(world, 'kickAll'); + + await func(); + + assert.calledOnce(kickAll); + }); - it('clears all tokens', async () => { - await func(); + it('clears all tokens', async () => { + await func(); - assert.calledOnce(clearTokensAll); - }); - }); + assert.calledOnce(clearTokensAll); + }); + }); - describe('notifyUpdate()', () => { - let func: ReturnType; - let world = stubClass(World); - let liveSettings: ServerLiveSettings; + describe('notifyUpdate()', () => { + let func: ReturnType; + let world = stubClass(World); + let liveSettings: ServerLiveSettings; - beforeEach(() => { - resetStubMethods(world, 'notifyUpdate', 'saveClientStates'); - liveSettings = {} as any; - func = createNotifyUpdate(world as any, liveSettings); - }); + beforeEach(() => { + resetStubMethods(world, 'notifyUpdate', 'saveClientStates'); + liveSettings = {} as any; + func = createNotifyUpdate(world as any, liveSettings); + }); - it('notifies world of update', async () => { - await func(); + it('notifies world of update', async () => { + await func(); - assert.calledOnce(world.notifyUpdate); - }); + assert.calledOnce(world.notifyUpdate); + }); - it('updates character state', async () => { - await func(); + it('updates character state', async () => { + await func(); - assert.calledOnce(world.saveClientStates); - }); - }); + assert.calledOnce(world.saveClientStates); + }); + }); - describe('cancelUpdate()', () => { - let func: ReturnType; - let live: ServerLiveSettings; + describe('cancelUpdate()', () => { + let func: ReturnType; + let live: ServerLiveSettings; - beforeEach(() => { - live = {} as any; - func = createCancelUpdate(live); - }); + beforeEach(() => { + live = {} as any; + func = createCancelUpdate(live); + }); - it('sets updating to false', async () => { - live.updating = true; + it('sets updating to false', async () => { + live.updating = true; - await func(); + await func(); - expect(live.updating).false; - }); - }); + expect(live.updating).false; + }); + }); - describe('shutdownServer()', () => { - let shutdownServer: ReturnType; - let world: World; - let liveSettings: ServerLiveSettings; + describe('shutdownServer()', () => { + let shutdownServer: ReturnType; + let world: World; + let liveSettings: ServerLiveSettings; - beforeEach(() => { - world = mock(World); - (world as any).server = { id: 'foo' }; - liveSettings = {} as any; - shutdownServer = createShutdownServer(world, liveSettings); - }); + beforeEach(() => { + world = mock(World); + (world as any).server = { id: 'foo' }; + liveSettings = {} as any; + shutdownServer = createShutdownServer(world, liveSettings); + }); - it('updates shutdown option in live settings to true', async () => { - await shutdownServer(true); + it('updates shutdown option in live settings to true', async () => { + await shutdownServer(true); - expect(liveSettings.shutdown).true; - }); + expect(liveSettings.shutdown).true; + }); - it('updates shutdown option in live settings to false', async () => { - await shutdownServer(false); + it('updates shutdown option in live settings to false', async () => { + await shutdownServer(false); - expect(liveSettings.shutdown).false; - }); + expect(liveSettings.shutdown).false; + }); - it('kicks all players', async () => { - const kickAll = stub(world, 'kickAll'); + it('kicks all players', async () => { + const kickAll = stub(world, 'kickAll'); - await shutdownServer(true); + await shutdownServer(true); - assert.calledOnce(kickAll); - }); - }); + assert.calledOnce(kickAll); + }); + }); }); diff --git a/src/ts/tests/server/api/pony.spec.ts b/src/ts/tests/server/api/pony.spec.ts index e5d8e0a..30feaad 100644 --- a/src/ts/tests/server/api/pony.spec.ts +++ b/src/ts/tests/server/api/pony.spec.ts @@ -14,387 +14,387 @@ import { Reporter } from '../../../server/serverInterfaces'; const info = OFFLINE_PONY; describe('api pony', () => { - describe('savePony()', () => { - let savePony: SavePony; - let findCharacter: SinonStub; - let findAuth: SinonStub; - let characterCount: SinonStub; - let updateCharacterCount: SinonStub; - let createCharacter: SinonStub; - let log: SinonStub; - let isSuspiciousName: SinonStub; - let isSuspiciousPony: SinonStub; - let clock: SinonFakeTimers; - let reporter: SinonStubbedInstance; - - beforeEach(() => { - findCharacter = stub(); - findAuth = stub(); - characterCount = stub(); - updateCharacterCount = stub(); - createCharacter = stub(); - log = stub(); - isSuspiciousName = stub(); - isSuspiciousPony = stub(); - clock = useFakeTimers(); - reporter = { - danger: stub(), - setPony: stub(), - warn: stub(), - } as any; - - savePony = createSavePony( - findCharacter, findAuth, characterCount, updateCharacterCount, createCharacter, log, - isSuspiciousName, isSuspiciousPony); - }); - - afterEach(() => { - clock.restore(); - }); - - describe('for existing character', () => { - const characterId = genId(); - const characterObjectId = Types.ObjectId(characterId); - let character: ICharacter; - let account = { _id: 'accid' } as any; - - beforeEach(() => { - character = { - name: 'oldname', - _id: characterObjectId, - createdAt: new Date(10), - save() { return this; } - } as any; - - findCharacter.withArgs(characterId, 'accid').resolves(character); - }); - - it('returns pony object', async () => { - clock.setSystemTime(123); - - await expect(savePony(account, { id: characterId, name: 'foo', info }, reporter)).eventually.eql({ - id: characterId, - info, - lastUsed: '1970-01-01T00:00:00.123Z', - name: 'foo', - desc: '', - site: undefined, - tag: undefined, - hideSupport: undefined, - respawnAtSpawn: undefined, - }); - }); - - it('saves character', async () => { - const save = stub(character, 'save').resolves(character); - - await savePony(account, { id: characterId, name: 'foo', info }, reporter); - - assert.calledOnce(save); - }); - - it('updates character fields', async () => { - clock.setSystemTime(123); - - await savePony(account, { id: characterId, name: 'foo', tag: 'tag', info }, reporter); - - expect(character.name).equal('foo'); - expect(character.tag).equal('tag'); - expect(character.info).equal(info); - expect(character.lastUsed!.toISOString()).equal((new Date()).toISOString()); - }); - - it('does not reject if character limit is reached', async () => { - characterCount.resolves(getCharacterLimit({ supporter: 0 }) * 2); - - await savePony(account, { id: characterId, name: 'foo', info }, reporter); - }); - - it('logs name change', async () => { - await savePony(account, { id: characterId, name: 'foo', info }, reporter); - - assert.calledWith(log, account._id, 'renamed pony "oldname" => "foo"'); - }); - - it('does not log if nothing changed', async () => { - await savePony(account, { id: characterId, name: 'oldname', info }, reporter); + describe('savePony()', () => { + let savePony: SavePony; + let findCharacter: SinonStub; + let findAuth: SinonStub; + let characterCount: SinonStub; + let updateCharacterCount: SinonStub; + let createCharacter: SinonStub; + let log: SinonStub; + let isSuspiciousName: SinonStub; + let isSuspiciousPony: SinonStub; + let clock: SinonFakeTimers; + let reporter: SinonStubbedInstance; + + beforeEach(() => { + findCharacter = stub(); + findAuth = stub(); + characterCount = stub(); + updateCharacterCount = stub(); + createCharacter = stub(); + log = stub(); + isSuspiciousName = stub(); + isSuspiciousPony = stub(); + clock = useFakeTimers(); + reporter = { + danger: stub(), + setPony: stub(), + warn: stub(), + } as any; + + savePony = createSavePony( + findCharacter, findAuth, characterCount, updateCharacterCount, createCharacter, log, + isSuspiciousName, isSuspiciousPony); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('for existing character', () => { + const characterId = genId(); + const characterObjectId = Types.ObjectId(characterId); + let character: ICharacter; + let account = { _id: 'accid' } as any; + + beforeEach(() => { + character = { + name: 'oldname', + _id: characterObjectId, + createdAt: new Date(10), + save() { return this; } + } as any; + + findCharacter.withArgs(characterId, 'accid').resolves(character); + }); + + it('returns pony object', async () => { + clock.setSystemTime(123); + + await expect(savePony(account, { id: characterId, name: 'foo', info }, reporter)).eventually.eql({ + id: characterId, + info, + lastUsed: '1970-01-01T00:00:00.123Z', + name: 'foo', + desc: '', + site: undefined, + tag: undefined, + hideSupport: undefined, + respawnAtSpawn: undefined, + }); + }); + + it('saves character', async () => { + const save = stub(character, 'save').resolves(character); + + await savePony(account, { id: characterId, name: 'foo', info }, reporter); + + assert.calledOnce(save); + }); + + it('updates character fields', async () => { + clock.setSystemTime(123); + + await savePony(account, { id: characterId, name: 'foo', tag: 'tag', info }, reporter); + + expect(character.name).equal('foo'); + expect(character.tag).equal('tag'); + expect(character.info).equal(info); + expect(character.lastUsed!.toISOString()).equal((new Date()).toISOString()); + }); + + it('does not reject if character limit is reached', async () => { + characterCount.resolves(getCharacterLimit({ supporter: 0 }) * 2); + + await savePony(account, { id: characterId, name: 'foo', info }, reporter); + }); + + it('logs name change', async () => { + await savePony(account, { id: characterId, name: 'foo', info }, reporter); + + assert.calledWith(log, account._id, 'renamed pony "oldname" => "foo"'); + }); + + it('does not log if nothing changed', async () => { + await savePony(account, { id: characterId, name: 'oldname', info }, reporter); - assert.notCalled(log); - }); + assert.notCalled(log); + }); - it('reports suspicious name', async () => { - isSuspiciousName.withArgs('moderator').returns(true); + it('reports suspicious name', async () => { + isSuspiciousName.withArgs('moderator').returns(true); - await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); - assert.calledWith(reporter.setPony, characterId); - assert.calledWith(reporter.warn, 'Suspicious pony created', '"moderator" (name)'); - }); + assert.calledWith(reporter.setPony, characterId); + assert.calledWith(reporter.warn, 'Suspicious pony created', '"moderator" (name)'); + }); - it('does not report suspicious name if not changed', async () => { - character.name = 'moderator'; - isSuspiciousName.withArgs('moderator').returns(true); + it('does not report suspicious name if not changed', async () => { + character.name = 'moderator'; + isSuspiciousName.withArgs('moderator').returns(true); - await savePony(account, { id: characterId, name: 'moderator', info }, reporter); - }); + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + }); - it('reports suspicious look', async () => { - isSuspiciousPony.returns(true); + it('reports suspicious look', async () => { + isSuspiciousPony.returns(true); - await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); - assert.calledWith(reporter.setPony, characterId); - assert.calledWith(reporter.warn, 'Suspicious pony created', '"moderator" (look)'); - }); + assert.calledWith(reporter.setPony, characterId); + assert.calledWith(reporter.warn, 'Suspicious pony created', '"moderator" (look)'); + }); - it('does not report suspicious look if not changed', async () => { - character.info = info; - isSuspiciousPony.returns(true); + it('does not report suspicious look if not changed', async () => { + character.info = info; + isSuspiciousPony.returns(true); - await savePony(account, { id: characterId, name: 'moderator', info }, reporter); - }); + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + }); - it('reports suspicious name & look', async () => { - isSuspiciousName.withArgs('moderator').returns(true); - isSuspiciousPony.returns(true); + it('reports suspicious name & look', async () => { + isSuspiciousName.withArgs('moderator').returns(true); + isSuspiciousPony.returns(true); - await savePony(account, { id: characterId, name: 'moderator', info }, reporter); + await savePony(account, { id: characterId, name: 'moderator', info }, reporter); - assert.calledWith(reporter.setPony, characterId); - assert.calledWith(reporter.warn, 'Suspicious pony created', '"moderator" (name, look)'); - }); + assert.calledWith(reporter.setPony, characterId); + assert.calledWith(reporter.warn, 'Suspicious pony created', '"moderator" (name, look)'); + }); - it('rejects on decoding error', async () => { - await expect(savePony(account, { id: characterId, name: 'foo', info: 'xxyf@hs' }, reporter)) - .rejectedWith('Error saving character'); - }); + it('rejects on decoding error', async () => { + await expect(savePony(account, { id: characterId, name: 'foo', info: 'xxyf@hs' }, reporter)) + .rejectedWith('Error saving character'); + }); - it('sets bad CM flag', async () => { - const info = 'CASZlZXapSD/1wBAPTk2QAJkI0AT8ADAAxADhAYQHMMkhhMkkJhkkMA='; + it('sets bad CM flag', async () => { + const info = 'CASZlZXapSD/1wBAPTk2QAJkI0AT8ADAAxADhAYQHMMkhhMkkJhkkMA='; - await savePony(account, { id: characterId, name: 'foo', info }, reporter); + await savePony(account, { id: characterId, name: 'foo', info }, reporter); - expect(character.flags).equal(CharacterFlags.BadCM); - }); + expect(character.flags).equal(CharacterFlags.BadCM); + }); - it('sets hide support pony flag', async () => { - await savePony(account, { id: characterId, name: 'foo', info, hideSupport: true }, reporter); + it('sets hide support pony flag', async () => { + await savePony(account, { id: characterId, name: 'foo', info, hideSupport: true }, reporter); - expect(character.flags).equal(CharacterFlags.HideSupport); - }); + expect(character.flags).equal(CharacterFlags.HideSupport); + }); - it('sets respawn at spawn pony flag', async () => { - await savePony(account, { id: characterId, name: 'foo', info, respawnAtSpawn: true }, reporter); + it('sets respawn at spawn pony flag', async () => { + await savePony(account, { id: characterId, name: 'foo', info, respawnAtSpawn: true }, reporter); - expect(character.flags).equal(CharacterFlags.RespawnAtSpawn); - }); + expect(character.flags).equal(CharacterFlags.RespawnAtSpawn); + }); - it('sets auth', async () => { - const authid = {} as any; - findAuth.withArgs('authid', 'accid').resolves({ _id: authid }); + it('sets auth', async () => { + const authid = {} as any; + findAuth.withArgs('authid', 'accid').resolves({ _id: authid }); - await savePony(account, { id: characterId, name: 'foo', site: 'authid', info }, reporter); + await savePony(account, { id: characterId, name: 'foo', site: 'authid', info }, reporter); - expect(character.site).equal(authid); - }); + expect(character.site).equal(authid); + }); - it('does not set auth if not found', async () => { - await savePony(account, { id: characterId, name: 'foo', site: 'authid', info }, reporter); + it('does not set auth if not found', async () => { + await savePony(account, { id: characterId, name: 'foo', site: 'authid', info }, reporter); - expect(character.site).null; - }); - }); + expect(character.site).null; + }); + }); - describe('for new character', () => { - const characterId = genId(); - let acc = account({ _id: genObjectId() }); - let character: ICharacter; + describe('for new character', () => { + const characterId = genId(); + let acc = account({ _id: genObjectId() }); + let character: ICharacter; - beforeEach(() => { - character = { - _id: Types.ObjectId(characterId), - save() { return this; } - } as any; + beforeEach(() => { + character = { + _id: Types.ObjectId(characterId), + save() { return this; } + } as any; - createCharacter.withArgs(acc).returns(character); - }); + createCharacter.withArgs(acc).returns(character); + }); - it('returns pony object', async () => { - clock.setSystemTime(123); + it('returns pony object', async () => { + clock.setSystemTime(123); - await expect(savePony(acc, { name: 'foo', info }, reporter)).eventually.eql({ - id: characterId, - info, - lastUsed: '1970-01-01T00:00:00.123Z', - name: 'foo', - desc: '', - site: undefined, - tag: undefined, - hideSupport: undefined, - respawnAtSpawn: undefined, - }); - }); + await expect(savePony(acc, { name: 'foo', info }, reporter)).eventually.eql({ + id: characterId, + info, + lastUsed: '1970-01-01T00:00:00.123Z', + name: 'foo', + desc: '', + site: undefined, + tag: undefined, + hideSupport: undefined, + respawnAtSpawn: undefined, + }); + }); - it('saves character', async () => { - const save = stub(character, 'save').resolves(character); + it('saves character', async () => { + const save = stub(character, 'save').resolves(character); - await savePony(acc, { name: 'foo', info }, reporter); + await savePony(acc, { name: 'foo', info }, reporter); - assert.calledOnce(save); - }); + assert.calledOnce(save); + }); - it('sets character fields', async () => { - clock.setSystemTime(123); + it('sets character fields', async () => { + clock.setSystemTime(123); - await savePony(acc, { id: characterId, name: 'foo', tag: 'tag', info }, reporter); + await savePony(acc, { id: characterId, name: 'foo', tag: 'tag', info }, reporter); - expect(character.name).equal('foo'); - expect(character.tag).equal('tag'); - expect(character.info).equal(info); - expect(character.lastUsed!.toISOString()).equal((new Date()).toISOString()); - }); + expect(character.name).equal('foo'); + expect(character.tag).equal('tag'); + expect(character.info).equal(info); + expect(character.lastUsed!.toISOString()).equal((new Date()).toISOString()); + }); - it('rejects if character limit is reached', async () => { - characterCount.resolves(getCharacterLimit({ supporter: 0 })); + it('rejects if character limit is reached', async () => { + characterCount.resolves(getCharacterLimit({ supporter: 0 })); - await expect(savePony(acc, { name: 'foo', info }, reporter)) - .rejectedWith('Character limit reached'); - }); + await expect(savePony(acc, { name: 'foo', info }, reporter)) + .rejectedWith('Character limit reached'); + }); - it('logs character creation', async () => { - stub(character, 'save').resolves({ name: 'foo', createdAt: new Date() } as any); + it('logs character creation', async () => { + stub(character, 'save').resolves({ name: 'foo', createdAt: new Date() } as any); - await savePony(acc, { name: 'foo', info }, reporter); + await savePony(acc, { name: 'foo', info }, reporter); - assert.calledWith(log, acc._id, 'created pony "foo"'); - }); + assert.calledWith(log, acc._id, 'created pony "foo"'); + }); - describe('for supporters', () => { - beforeEach(() => { - acc.patreon = PatreonFlags.Supporter1; - }); + describe('for supporters', () => { + beforeEach(() => { + acc.patreon = PatreonFlags.Supporter1; + }); - it('has larger limit', async () => { - characterCount.resolves(getCharacterLimit({ supporter: 0 })); - const save = stub(character, 'save').resolves(character); + it('has larger limit', async () => { + characterCount.resolves(getCharacterLimit({ supporter: 0 })); + const save = stub(character, 'save').resolves(character); - await savePony(acc, { name: 'foo', info }, reporter); + await savePony(acc, { name: 'foo', info }, reporter); - assert.calledOnce(save); - }); + assert.calledOnce(save); + }); - it('rejects if character limit is reached', async () => { - characterCount.resolves(getCharacterLimit({ supporter: 1 })); + it('rejects if character limit is reached', async () => { + characterCount.resolves(getCharacterLimit({ supporter: 1 })); - await expect(savePony(acc, { name: 'foo', info }, reporter)).rejectedWith('Character limit reached'); - }); - }); - }); + await expect(savePony(acc, { name: 'foo', info }, reporter)).rejectedWith('Character limit reached'); + }); + }); + }); - it('rejects on missing pony', async () => { - await expect(savePony({} as any, undefined as any, reporter)).rejectedWith('Invalid data'); - }); + it('rejects on missing pony', async () => { + await expect(savePony({} as any, undefined as any, reporter)).rejectedWith('Invalid data'); + }); - it('rejects on missing pony name', async () => { - await expect(savePony({} as any, { info }, reporter)).rejectedWith('Invalid data'); - }); + it('rejects on missing pony name', async () => { + await expect(savePony({} as any, { info }, reporter)).rejectedWith('Invalid data'); + }); - it('rejects on non-string pony name', async () => { - await expect(savePony({} as any, { name: {} as any, info }, reporter)).rejectedWith('Invalid data'); - }); + it('rejects on non-string pony name', async () => { + await expect(savePony({} as any, { name: {} as any, info }, reporter)).rejectedWith('Invalid data'); + }); - it('rejects on missing pony info', async () => { - await expect(savePony({} as any, { name: 'foo' }, reporter)).rejectedWith('Invalid data'); - }); + it('rejects on missing pony info', async () => { + await expect(savePony({} as any, { name: 'foo' }, reporter)).rejectedWith('Invalid data'); + }); - it('rejects on too long pony name', async () => { - await expect(savePony({} as any, { name: randomString(PLAYER_NAME_MAX_LENGTH + 1), info }, reporter)) - .rejectedWith('Invalid name'); - }); + it('rejects on too long pony name', async () => { + await expect(savePony({} as any, { name: randomString(PLAYER_NAME_MAX_LENGTH + 1), info }, reporter)) + .rejectedWith('Invalid name'); + }); - it('rejects on database error', async () => { - findCharacter.rejects(new Error('test')); + it('rejects on database error', async () => { + findCharacter.rejects(new Error('test')); - await expect(savePony({} as any, { id: 'charid', name: 'foo', info }, reporter)).rejectedWith('Invalid data'); - }); - }); + await expect(savePony({} as any, { id: 'charid', name: 'foo', info }, reporter)).rejectedWith('Invalid data'); + }); + }); - describe('removePony()', () => { - let removePony: RemovePony; - let kickFromAllServersByCharacter: SinonStub; - let removeCharacter: SinonStub; - let updateCharacterCount: SinonStub; - let removedCharacter: SinonStub; - let logRemovedCharacter: SinonStub; + describe('removePony()', () => { + let removePony: RemovePony; + let kickFromAllServersByCharacter: SinonStub; + let removeCharacter: SinonStub; + let updateCharacterCount: SinonStub; + let removedCharacter: SinonStub; + let logRemovedCharacter: SinonStub; - beforeEach(() => { - kickFromAllServersByCharacter = stub(); - removeCharacter = stub(); - updateCharacterCount = stub(); - removedCharacter = stub(); - logRemovedCharacter = stub(); + beforeEach(() => { + kickFromAllServersByCharacter = stub(); + removeCharacter = stub(); + updateCharacterCount = stub(); + removedCharacter = stub(); + logRemovedCharacter = stub(); - removePony = createRemovePony( - kickFromAllServersByCharacter, removeCharacter, updateCharacterCount, removedCharacter, logRemovedCharacter); - }); + removePony = createRemovePony( + kickFromAllServersByCharacter, removeCharacter, updateCharacterCount, removedCharacter, logRemovedCharacter); + }); - it('kicks user from all servers', async () => { - await removePony('ponid', 'accid'); + it('kicks user from all servers', async () => { + await removePony('ponid', 'accid'); - assert.calledWith(kickFromAllServersByCharacter, 'ponid'); - }); + assert.calledWith(kickFromAllServersByCharacter, 'ponid'); + }); - it('removes character', async () => { - await removePony('ponid', 'accid'); + it('removes character', async () => { + await removePony('ponid', 'accid'); - assert.calledWith(removeCharacter, 'ponid', 'accid'); - }); + assert.calledWith(removeCharacter, 'ponid', 'accid'); + }); - it('updates character count', async () => { - await removePony('ponid', 'accid'); + it('updates character count', async () => { + await removePony('ponid', 'accid'); - assert.calledWith(updateCharacterCount, 'accid'); - }); + assert.calledWith(updateCharacterCount, 'accid'); + }); - it('logs character removed', async () => { - const character = { name: 'test', info: 'INFO' }; - removeCharacter.resolves(character); + it('logs character removed', async () => { + const character = { name: 'test', info: 'INFO' }; + removeCharacter.resolves(character); - await removePony('ponid', 'accid'); + await removePony('ponid', 'accid'); - assert.calledWith(logRemovedCharacter, character); - }); + assert.calledWith(logRemovedCharacter, character); + }); - it('notifies of character removal', async () => { - removeCharacter.resolves({ name: 'test' }); + it('notifies of character removal', async () => { + removeCharacter.resolves({ name: 'test' }); - await removePony('ponid', 'accid'); + await removePony('ponid', 'accid'); - assert.calledWith(removedCharacter, 'ponid'); - }); + assert.calledWith(removedCharacter, 'ponid'); + }); - it('does not log character removal if character is not found', async () => { - removeCharacter.resolves(undefined); + it('does not log character removal if character is not found', async () => { + removeCharacter.resolves(undefined); - await removePony('ponid', 'accid'); + await removePony('ponid', 'accid'); - assert.notCalled(logRemovedCharacter); - }); + assert.notCalled(logRemovedCharacter); + }); - it('does not notify of character removal if character is not found', async () => { - removeCharacter.resolves(undefined); + it('does not notify of character removal if character is not found', async () => { + removeCharacter.resolves(undefined); - await removePony('ponid', 'accid'); + await removePony('ponid', 'accid'); - assert.notCalled(removedCharacter); - }); + assert.notCalled(removedCharacter); + }); - it('rejects if pony ID is not a string', async () => { - await expect(removePony({} as any, 'accid')).rejectedWith('Invalid ponyId ([object Object])'); - }); + it('rejects if pony ID is not a string', async () => { + await expect(removePony({} as any, 'accid')).rejectedWith('Invalid ponyId ([object Object])'); + }); - it('rejects if pony ID is empty', async () => { - await expect(removePony('', 'accid')).rejectedWith('Invalid ponyId ()'); - }); - }); + it('rejects if pony ID is empty', async () => { + await expect(removePony('', 'accid')).rejectedWith('Invalid ponyId ()'); + }); + }); }); diff --git a/src/ts/tests/server/authUtils.spec.ts b/src/ts/tests/server/authUtils.spec.ts index e813e62..b190ade 100644 --- a/src/ts/tests/server/authUtils.spec.ts +++ b/src/ts/tests/server/authUtils.spec.ts @@ -7,80 +7,80 @@ import { auth, genId } from '../mocks'; import { Profile } from '../../common/interfaces'; function profile(options: Partial): Profile { - return options as Profile; + return options as Profile; } describe('authUtils', () => { - describe('updateAuthInfo()', () => { - it('updates url and name fields', async () => { - const updateAuth = stub(); - const a = auth({ _id: 'bar' }); + describe('updateAuthInfo()', () => { + it('updates url and name fields', async () => { + const updateAuth = stub(); + const a = auth({ _id: 'bar' }); - await updateAuthInfo(updateAuth, a, profile({ username: 'foo', url: 'bar' }), undefined); + await updateAuthInfo(updateAuth, a, profile({ username: 'foo', url: 'bar' }), undefined); - expect(a.name).eql('foo'); - expect(a.url).eql('bar'); - assert.calledWith(updateAuth, 'bar', { name: 'foo', url: 'bar' }); - }); + expect(a.name).eql('foo'); + expect(a.url).eql('bar'); + assert.calledWith(updateAuth, 'bar', { name: 'foo', url: 'bar' }); + }); - it('updates email field', async () => { - const a = auth({ emails: ['a'] }); + it('updates email field', async () => { + const a = auth({ emails: ['a'] }); - await updateAuthInfo(stub(), a, profile({ emails: ['b', 'c'] }), undefined); + await updateAuthInfo(stub(), a, profile({ emails: ['b', 'c'] }), undefined); - expect(a.emails).eql(['a', 'b', 'c']); - }); + expect(a.emails).eql(['a', 'b', 'c']); + }); - it('updates email field (from empty)', async () => { - const updateAuth = stub(); - const a = auth({ _id: 'bar' }); + it('updates email field (from empty)', async () => { + const updateAuth = stub(); + const a = auth({ _id: 'bar' }); - await updateAuthInfo(updateAuth, a, profile({ emails: ['b', 'c'] }), undefined); + await updateAuthInfo(updateAuth, a, profile({ emails: ['b', 'c'] }), undefined); - expect(a.emails).eql(['b', 'c']); - assert.calledWith(updateAuth, 'bar', { emails: ['b', 'c'] }); - }); + expect(a.emails).eql(['b', 'c']); + assert.calledWith(updateAuth, 'bar', { emails: ['b', 'c'] }); + }); - it('saves updated auth', async () => { - const updateAuth = stub(); + it('saves updated auth', async () => { + const updateAuth = stub(); - await updateAuthInfo(updateAuth, auth({ _id: 'bar' }), profile({ username: 'foo' }), undefined); + await updateAuthInfo(updateAuth, auth({ _id: 'bar' }), profile({ username: 'foo' }), undefined); - assert.calledWith(updateAuth, 'bar', { name: 'foo' }); - }); + assert.calledWith(updateAuth, 'bar', { name: 'foo' }); + }); - it('updates account if passed account ID', async () => { - const a = auth({}); - const accountId = genId(); + it('updates account if passed account ID', async () => { + const a = auth({}); + const accountId = genId(); - await updateAuthInfo(stub(), a, profile({ username: 'foo', url: 'bar' }), accountId); + await updateAuthInfo(stub(), a, profile({ username: 'foo', url: 'bar' }), accountId); - expect(a.account).eql(Types.ObjectId(accountId)); - }); + expect(a.account).eql(Types.ObjectId(accountId)); + }); - it('does not save auth if nothing changed', async () => { - const updateAuth = stub(); + it('does not save auth if nothing changed', async () => { + const updateAuth = stub(); - await updateAuthInfo(updateAuth, auth({ _id: 'bar', name: 'foo' }), profile({ username: 'foo' }), undefined); + await updateAuthInfo(updateAuth, auth({ _id: 'bar', name: 'foo' }), profile({ username: 'foo' }), undefined); - assert.notCalled(updateAuth); - }); + assert.notCalled(updateAuth); + }); - it('does nothing if email list is the same', async () => { - const updateAuth = stub(); - const a = auth({ _id: 'bar', emails: ['a', 'b'] }); + it('does nothing if email list is the same', async () => { + const updateAuth = stub(); + const a = auth({ _id: 'bar', emails: ['a', 'b'] }); - await updateAuthInfo(updateAuth, a, profile({ emails: ['b', 'a'] }), undefined); + await updateAuthInfo(updateAuth, a, profile({ emails: ['b', 'a'] }), undefined); - assert.notCalled(updateAuth); - }); + assert.notCalled(updateAuth); + }); - it('does nothing if auth is undefined', async () => { - const updateAuth = stub(); + it('does nothing if auth is undefined', async () => { + const updateAuth = stub(); - await updateAuthInfo(updateAuth, undefined, profile({}), undefined); + await updateAuthInfo(updateAuth, undefined, profile({}), undefined); - assert.notCalled(updateAuth); - }); - }); + assert.notCalled(updateAuth); + }); + }); }); diff --git a/src/ts/tests/server/characterUtils.spec.ts b/src/ts/tests/server/characterUtils.spec.ts index 338b003..807235a 100644 --- a/src/ts/tests/server/characterUtils.spec.ts +++ b/src/ts/tests/server/characterUtils.spec.ts @@ -14,352 +14,352 @@ import { createCharacterState } from '../../server/playerUtils'; import { hasFlag } from '../../common/utils'; describe('characterUtils', () => { - describe('createPony()', () => { - const defaultState: CharacterState = { x: 0, y: 0, flags: 0 }; + describe('createPony()', () => { + const defaultState: CharacterState = { x: 0, y: 0, flags: 0 }; - it('creates pony entity', () => { - const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), defaultState); + it('creates pony entity', () => { + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), defaultState); - expect(pony).not.undefined; - expect(pony.type).equal(entities.pony.type); - }); + expect(pony).not.undefined; + expect(pony.type).equal(entities.pony.type); + }); - it('sets initial position for character from state', () => { - const main: CharacterState = { ...defaultState, x: 1, y: 2 }; + it('sets initial position for character from state', () => { + const main: CharacterState = { ...defaultState, x: 1, y: 2 }; - const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); - expect(pony.x).eql(1, 'x'); - expect(pony.y).eql(2, 'y'); - }); + expect(pony.x).eql(1, 'x'); + expect(pony.y).eql(2, 'y'); + }); - it('sets facing from state', () => { - const main: CharacterState = { ...defaultState, flags: CharacterStateFlags.Right }; + it('sets facing from state', () => { + const main: CharacterState = { ...defaultState, flags: CharacterStateFlags.Right }; - const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); - expect(pony.state).equal(EntityState.FacingRight); - }); + expect(pony.state).equal(EntityState.FacingRight); + }); - it('sets extra flag from state', () => { - const main: CharacterState = { ...defaultState, flags: CharacterStateFlags.Extra }; + it('sets extra flag from state', () => { + const main: CharacterState = { ...defaultState, flags: CharacterStateFlags.Extra }; - const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); - expect(pony.options!.extra).true; - }); + expect(pony.options!.extra).true; + }); - it('sets held item from state', () => { - const main: CharacterState = { ...defaultState, hold: 'apple' }; + it('sets held item from state', () => { + const main: CharacterState = { ...defaultState, hold: 'apple' }; - const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); - expect(pony.options!.hold).equal(entities.apple.type); - }); + expect(pony.options!.hold).equal(entities.apple.type); + }); - it('ignores held item from state if type is invalid', () => { - const main: CharacterState = { ...defaultState, hold: 'does_not_exist' }; + it('ignores held item from state if type is invalid', () => { + const main: CharacterState = { ...defaultState, hold: 'does_not_exist' }; - const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); + const pony = createPony(account({ _id: '' }), character({ name: 'foo' }), main); - expect(pony.options!.hold).undefined; - }); + expect(pony.options!.hold).undefined; + }); - it('sets canCollide flag', () => { - const pony = createPony( - account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); + it('sets canCollide flag', () => { + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); - expect(hasFlag(pony.flags, EntityFlags.CanCollide)).true; - }); + expect(hasFlag(pony.flags, EntityFlags.CanCollide)).true; + }); - it('sets canFly flag to false for ponies without wings', () => { - const pony = createPony( - account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); + it('sets canFly flag to false for ponies without wings', () => { + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); - expect(pony.canFly).false; - }); + expect(pony.canFly).false; + }); - it('sets canFly flag to true for ponies with wings', () => { - const info = 'CAb///9xcXHaICDHx8eqqqq9vb02QAJkJEIFcADAAwgEnAcgQNiMS4A='; - const pony = createPony( - account({ _id: genObjectId() }), character({ name: 'foo', info }), {} as any); + it('sets canFly flag to true for ponies with wings', () => { + const info = 'CAb///9xcXHaICDHx8eqqqq9vb02QAJkJEIFcADAAwgEnAcgQNiMS4A='; + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info }), {} as any); - expect(pony.canFly).true; - }); + expect(pony.canFly).true; + }); - it('sets character name', () => { - const pony = createPony( - account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); + it('sets character name', () => { + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); - expect(pony.name).equal('foo'); - }); + expect(pony.name).equal('foo'); + }); - it('sets extra options name', () => { - const pony = createPony( - account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); + it('sets extra options name', () => { + const pony = createPony( + account({ _id: genObjectId() }), character({ name: 'foo', info: OFFLINE_PONY }), {} as any); - expect(pony.extraOptions).eql(createExtraOptions(character({ name: 'foo' }))); - }); - }); + expect(pony.extraOptions).eql(createExtraOptions(character({ name: 'foo' }))); + }); + }); - describe('updatePony()', () => { - it('sets name', () => { - const entity = serverEntity(1); + describe('updatePony()', () => { + it('sets name', () => { + const entity = serverEntity(1); - updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Foo' })); + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Foo' })); - expect(entity.name).equal('Foo'); - }); + expect(entity.name).equal('Foo'); + }); - it('sets options', () => { - const entity = serverEntity(1); + it('sets options', () => { + const entity = serverEntity(1); - updatePony(entity, account({ _id: genObjectId(), roles: ['mod'] }), character({ name: 'Foo', tag: 'mod' })); + updatePony(entity, account({ _id: genObjectId(), roles: ['mod'] }), character({ name: 'Foo', tag: 'mod' })); - expect(entity.options).eql({ tag: 'mod' }); - }); + expect(entity.options).eql({ tag: 'mod' }); + }); - it('sets extra options', () => { - const entity = serverEntity(1); + it('sets extra options', () => { + const entity = serverEntity(1); - updatePony( - entity, - account({ _id: genObjectId() }), - { name: 'Foo', auth: { provider: 'github', name: 'FooAcc', url: 'foo.com' } } as any); + updatePony( + entity, + account({ _id: genObjectId() }), + { name: 'Foo', auth: { provider: 'github', name: 'FooAcc', url: 'foo.com' } } as any); - expect(entity.extraOptions).eql({ - ex: true, - site: { - provider: 'github', - name: 'FooAcc', - url: 'foo.com', - } - }); - }); + expect(entity.extraOptions).eql({ + ex: true, + site: { + provider: 'github', + name: 'FooAcc', + url: 'foo.com', + } + }); + }); - it('sets canFly flag', () => { - const entity1 = serverEntity(1); - const entity2 = serverEntity(2); + it('sets canFly flag', () => { + const entity1 = serverEntity(1); + const entity2 = serverEntity(2); - updatePony(entity1, account({ _id: genObjectId() }), character({ name: 'Foo', info: OFFLINE_PONY })); - updatePony( - entity2, account({ _id: '' }), character({ name: 'Bar', info: 'DAT/AADapSD/1wC7Li42QAJkJEAT8ADAAxADhAYQFGAQAA==' })); + updatePony(entity1, account({ _id: genObjectId() }), character({ name: 'Foo', info: OFFLINE_PONY })); + updatePony( + entity2, account({ _id: '' }), character({ name: 'Bar', info: 'DAT/AADapSD/1wC7Li42QAJkJEAT8ADAAxADhAYQFGAQAA==' })); - expect(entity1.canFly).false; - expect(entity2.canFly).true; - }); + expect(entity1.canFly).false; + expect(entity2.canFly).true; + }); - it('sets info and encrypted info', () => { - const entity = serverEntity(1); + it('sets info and encrypted info', () => { + const entity = serverEntity(1); - updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Foo', info: OFFLINE_PONY })); + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Foo', info: OFFLINE_PONY })); - expect(entity.info).equal(OFFLINE_PONY); - expect(entity.encryptedInfoSafe).eql(encryptInfo(OFFLINE_PONY)); - }); + expect(entity.info).equal(OFFLINE_PONY); + expect(entity.encryptedInfoSafe).eql(encryptInfo(OFFLINE_PONY)); + }); - it('sets encoded name fields', () => { - const entity = serverEntity(1); + it('sets encoded name fields', () => { + const entity = serverEntity(1); - updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Fuck Foo' })); + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Fuck Foo' })); - expect(entity.encodedName).eql(encodeString('Fuck Foo')); - }); + expect(entity.encodedName).eql(encodeString('Fuck Foo')); + }); - it('sets info safe fields', () => { - const entity = serverEntity(1); + it('sets info safe fields', () => { + const entity = serverEntity(1); - updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Foo', info: OFFLINE_PONY })); + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'Foo', info: OFFLINE_PONY })); - expect(entity.infoSafe).eql(OFFLINE_PONY); - expect(entity.encryptedInfoSafe).eql(encryptInfo(OFFLINE_PONY)); - }); + expect(entity.infoSafe).eql(OFFLINE_PONY); + expect(entity.encryptedInfoSafe).eql(encryptInfo(OFFLINE_PONY)); + }); - it('sets info safe fields to info with removed CM if bad CM flag is true', () => { - const entity = serverEntity(1); - const offlinePonyWithoutCM = 'DAKVlZUvLy82QIxomgCfgAYAGIAoQGEBAA=='; + it('sets info safe fields to info with removed CM if bad CM flag is true', () => { + const entity = serverEntity(1); + const offlinePonyWithoutCM = 'DAKVlZUvLy82QIxomgCfgAYAGIAoQGEBAA=='; - updatePony( - entity, - account({ _id: genObjectId() }), - character({ name: 'Foo', info: OFFLINE_PONY, flags: CharacterFlags.BadCM })); + updatePony( + entity, + account({ _id: genObjectId() }), + character({ name: 'Foo', info: OFFLINE_PONY, flags: CharacterFlags.BadCM })); - expect(entity.infoSafe).eql(offlinePonyWithoutCM); - expect(entity.encryptedInfoSafe).eql(encryptInfo(offlinePonyWithoutCM)); - }); + expect(entity.infoSafe).eql(offlinePonyWithoutCM); + expect(entity.encryptedInfoSafe).eql(encryptInfo(offlinePonyWithoutCM)); + }); - it('sets options', () => { - const entity = serverEntity(1); + it('sets options', () => { + const entity = serverEntity(1); - updatePony(entity, account({ _id: genObjectId() }), character({ name: 'foo' })); + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'foo' })); - expect(entity.name).equal('foo'); - }); + expect(entity.name).equal('foo'); + }); - it('fills in missing info', () => { - const entity = serverEntity(1); + it('fills in missing info', () => { + const entity = serverEntity(1); - updatePony(entity, account({ _id: genObjectId() }), character({ name: 'foo' })); + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'foo' })); - expect(entity.name).equal('foo'); - }); + expect(entity.name).equal('foo'); + }); - it('includes tag', () => { - const entity = serverEntity(1); + it('includes tag', () => { + const entity = serverEntity(1); - updatePony(entity, account({ _id: genObjectId(), roles: ['mod'] }), character({ name: 'foo', tag: 'mod' })); + updatePony(entity, account({ _id: genObjectId(), roles: ['mod'] }), character({ name: 'foo', tag: 'mod' })); - expect(entity.options!.tag).equal('mod'); - }); + expect(entity.options!.tag).equal('mod'); + }); - it('prioritazes set tag', () => { - const entity = serverEntity(1); + it('prioritazes set tag', () => { + const entity = serverEntity(1); - updatePony( - entity, - account({ _id: genObjectId(), supporter: SupporterFlags.Supporter1, roles: ['mod'] }), - character({ name: 'foo', tag: 'mod' })); + updatePony( + entity, + account({ _id: genObjectId(), supporter: SupporterFlags.Supporter1, roles: ['mod'] }), + character({ name: 'foo', tag: 'mod' })); - expect(entity.options!.tag).equal('mod'); - }); - - it('creates supporter tag', () => { - const entity = serverEntity(1); - - updatePony(entity, account({ _id: genObjectId(), supporter: SupporterFlags.Supporter1 }), character({ name: 'foo' })); - - expect(entity.options!.tag).equal('sup1'); - }); + expect(entity.options!.tag).equal('mod'); + }); + + it('creates supporter tag', () => { + const entity = serverEntity(1); + + updatePony(entity, account({ _id: genObjectId(), supporter: SupporterFlags.Supporter1 }), character({ name: 'foo' })); + + expect(entity.options!.tag).equal('sup1'); + }); - it('does not create support tag if hide support flag is true', () => { - const entity = serverEntity(1); + it('does not create support tag if hide support flag is true', () => { + const entity = serverEntity(1); - updatePony( - entity, - account({ _id: genObjectId(), supporter: SupporterFlags.Supporter1 }), - character({ name: 'foo', flags: CharacterFlags.HideSupport })); + updatePony( + entity, + account({ _id: genObjectId(), supporter: SupporterFlags.Supporter1 }), + character({ name: 'foo', flags: CharacterFlags.HideSupport })); - expect(entity.options!.tag).undefined; - }); + expect(entity.options!.tag).undefined; + }); - it('does not include tag if role is missing', () => { - const entity = serverEntity(1); + it('does not include tag if role is missing', () => { + const entity = serverEntity(1); - updatePony(entity, account({ _id: genObjectId() }), character({ name: 'foo', tag: 'mod' })); + updatePony(entity, account({ _id: genObjectId() }), character({ name: 'foo', tag: 'mod' })); - expect(entity.options!.tag).undefined; - }); - }); + expect(entity.options!.tag).undefined; + }); + }); - describe('createExtraOptions()', () => { - it('sets ex flag', () => { - expect(createExtraOptions(character({}))).eql({ - ex: true, - }); - }); + describe('createExtraOptions()', () => { + it('sets ex flag', () => { + expect(createExtraOptions(character({}))).eql({ + ex: true, + }); + }); - it('sets site object from auth', () => { - expect(createExtraOptions(character({ - auth: { - provider: 'github', - name: 'foo', - url: 'foo.com', - } as any, - }))).eql({ - ex: true, - site: { - provider: 'github', - name: 'foo', - url: 'foo.com', - } - }); - }); - }); - - describe('getAndFixCharacterState()', () => { - it('returns saved state', () => { - const server = { id: 'srvr' } as any; - const character = { _id: genObjectId(), state: { srvr: { x: 100, y: 321, map: 'bar' } } } as any; - const map = { id: 'bar', spawnArea: rect(10, 20, 0, 0) }; - const world = { getMainMap: () => map, getMap: () => map } as any; - const states = new CounterService(1); + it('sets site object from auth', () => { + expect(createExtraOptions(character({ + auth: { + provider: 'github', + name: 'foo', + url: 'foo.com', + } as any, + }))).eql({ + ex: true, + site: { + provider: 'github', + name: 'foo', + url: 'foo.com', + } + }); + }); + }); + + describe('getAndFixCharacterState()', () => { + it('returns saved state', () => { + const server = { id: 'srvr' } as any; + const character = { _id: genObjectId(), state: { srvr: { x: 100, y: 321, map: 'bar' } } } as any; + const map = { id: 'bar', spawnArea: rect(10, 20, 0, 0) }; + const world = { getMainMap: () => map, getMap: () => map } as any; + const states = new CounterService(1); - const state = getAndFixCharacterState(server, character, world, states); - - expect(state).eql({ x: 100, y: 321, map: 'bar' }); - }); - - it('returns state from from counter service if available', () => { - const server = { id: 'srvr' } as any; - const character = { _id: genObjectId(), state: { srvr: { x: 100, y: 321, map: 'bar' } } } as any; - const map = { id: 'bar', spawnArea: rect(10, 20, 0, 0) }; - const world = { getMainMap: () => map, getMap: () => map } as any; - const states = new CounterService(1); - states.add(character._id.toString(), { - x: 4, y: 7, map: 'foo', flags: CharacterStateFlags.Right | CharacterStateFlags.Extra - }); - - const state = getAndFixCharacterState(server, character, world, states); - - expect(state).eql({ x: 4, y: 7, map: 'foo', flags: CharacterStateFlags.Right | CharacterStateFlags.Extra }); - }); - - it('creates default state if none is provided', () => { - const server = {} as any; - const character = { _id: genObjectId() } as any; - const map = { id: 'foo', spawnArea: rect(10, 20, 0, 0) }; - const world = { getMainMap: () => map, getMap: () => map } as any; - const states = new CounterService(1); - - const state = getAndFixCharacterState(server, character, world, states); - - expect(state).eql({ x: 10, y: 20, map: 'foo' }); - }); - - it('spawns on main map at spawn point if RespawnAtSpawn flag is set', () => { - const server = { id: 'srvr' } as any; - const character = { - _id: genObjectId(), - state: { srvr: { x: 100, y: 321, map: 'bar' } }, - flags: CharacterFlags.RespawnAtSpawn, - } as any; - const map = { id: 'foo', spawnArea: rect(10, 20, 0, 0) }; - const world = { getMainMap: () => map, getMap: () => map } as any; - const states = new CounterService(1); - - const state = getAndFixCharacterState(server, character, world, states); - - expect(state).eql({ x: 10, y: 20, map: 'foo' }); - }); - }); - - describe('createCharacterState()', () => { - const map = createServerMap('foo', 0, 1, 1); - - it('returns state of character', () => { - expect(createCharacterState(entity(0, 12, 23), map)).eql({ - x: 12, - y: 23, - map: 'foo', - }); - }); - - it('encodes right flag', () => { - const state = createCharacterState(entity(0, 12, 23, 0, { state: EntityState.FacingRight }), map); - - expect(hasFlag(state.flags, CharacterStateFlags.Right)).true; - }); - - it('encodes held object', () => { - const state = createCharacterState(entity(0, 12, 23, 0, { options: { hold: entities.apple.type } }), map); - - expect(state.hold).equal('apple'); - }); - - it('encodes extra flag', () => { - const state = createCharacterState(entity(0, 12, 23, 0, { options: { extra: true } }), map); - - expect(hasFlag(state.flags, CharacterStateFlags.Extra)).true; - }); - }); + const state = getAndFixCharacterState(server, character, world, states); + + expect(state).eql({ x: 100, y: 321, map: 'bar' }); + }); + + it('returns state from from counter service if available', () => { + const server = { id: 'srvr' } as any; + const character = { _id: genObjectId(), state: { srvr: { x: 100, y: 321, map: 'bar' } } } as any; + const map = { id: 'bar', spawnArea: rect(10, 20, 0, 0) }; + const world = { getMainMap: () => map, getMap: () => map } as any; + const states = new CounterService(1); + states.add(character._id.toString(), { + x: 4, y: 7, map: 'foo', flags: CharacterStateFlags.Right | CharacterStateFlags.Extra + }); + + const state = getAndFixCharacterState(server, character, world, states); + + expect(state).eql({ x: 4, y: 7, map: 'foo', flags: CharacterStateFlags.Right | CharacterStateFlags.Extra }); + }); + + it('creates default state if none is provided', () => { + const server = {} as any; + const character = { _id: genObjectId() } as any; + const map = { id: 'foo', spawnArea: rect(10, 20, 0, 0) }; + const world = { getMainMap: () => map, getMap: () => map } as any; + const states = new CounterService(1); + + const state = getAndFixCharacterState(server, character, world, states); + + expect(state).eql({ x: 10, y: 20, map: 'foo' }); + }); + + it('spawns on main map at spawn point if RespawnAtSpawn flag is set', () => { + const server = { id: 'srvr' } as any; + const character = { + _id: genObjectId(), + state: { srvr: { x: 100, y: 321, map: 'bar' } }, + flags: CharacterFlags.RespawnAtSpawn, + } as any; + const map = { id: 'foo', spawnArea: rect(10, 20, 0, 0) }; + const world = { getMainMap: () => map, getMap: () => map } as any; + const states = new CounterService(1); + + const state = getAndFixCharacterState(server, character, world, states); + + expect(state).eql({ x: 10, y: 20, map: 'foo' }); + }); + }); + + describe('createCharacterState()', () => { + const map = createServerMap('foo', 0, 1, 1); + + it('returns state of character', () => { + expect(createCharacterState(entity(0, 12, 23), map)).eql({ + x: 12, + y: 23, + map: 'foo', + }); + }); + + it('encodes right flag', () => { + const state = createCharacterState(entity(0, 12, 23, 0, { state: EntityState.FacingRight }), map); + + expect(hasFlag(state.flags, CharacterStateFlags.Right)).true; + }); + + it('encodes held object', () => { + const state = createCharacterState(entity(0, 12, 23, 0, { options: { hold: entities.apple.type } }), map); + + expect(state.hold).equal('apple'); + }); + + it('encodes extra flag', () => { + const state = createCharacterState(entity(0, 12, 23, 0, { options: { extra: true } }), map); + + expect(hasFlag(state.flags, CharacterStateFlags.Extra)).true; + }); + }); }); diff --git a/src/ts/tests/server/chat.spec.ts b/src/ts/tests/server/chat.spec.ts index 9fa731e..d658d9e 100644 --- a/src/ts/tests/server/chat.spec.ts +++ b/src/ts/tests/server/chat.spec.ts @@ -9,8 +9,8 @@ import { IClient, ServerRegion } from '../../server/serverInterfaces'; import { mock, mockClient, serverEntity, entity } from '../mocks'; import { parseExpression } from '../../common/expressionUtils'; import { - createSay, filterUrls, Say, sayTo, sayToEveryone, sayToOthers, - sayToClientTest as sayToClient, sayToPartyTest as sayToParty, sayWhisperTest as sayWhisper + createSay, filterUrls, Say, sayTo, sayToEveryone, sayToOthers, + sayToClientTest as sayToClient, sayToPartyTest as sayToParty, sayWhisperTest as sayWhisper } from '../../server/chat'; import { encodeExpression } from '../../common/encoders/expressionEncoder'; import { toScreenX, toScreenY } from '../../common/positionUtils'; @@ -18,937 +18,937 @@ import { createServerRegion } from '../../server/serverRegion'; import * as playerUtils from '../../server/playerUtils'; describe('chat', () => { - describe('say()', () => { - let client: IClient; - let region: ServerRegion; - let world: World; - let runCommand: SinonStub; - let log: SinonStub; - let say: Say; - let checkSpam: SinonStub; - let reportSwears: SinonStub; - let reportForbidden: SinonStub; - let reportSuspicious: SinonStub; - let isSuspiciousMessage: SinonStub; - let execAction: SinonStub; - - beforeEach(() => { - execAction = stub(playerUtils, 'execAction'); - region = createServerRegion(1, 1); - client = mockClient(); - client.pony.region = region; - region.clients.push(client); - world = mock(World); - const map = createServerMap('', 0, 1, 1); - stub(world, 'getMainMap').returns(map); - runCommand = stub(); - log = stub(); - checkSpam = stub(); - reportSwears = stub(); - reportForbidden = stub(); - reportSuspicious = stub(); - isSuspiciousMessage = stub(); - const spamCommands = ['roll']; - say = createSay( - world, runCommand, log, checkSpam, reportSwears, reportForbidden, - reportSuspicious, spamCommands, () => 0, isSuspiciousMessage); - }); - - afterEach(() => { - execAction.restore(); - }); - - it('does nothing if whispering to self', () => { - say(client, 'hey me', ChatType.Whisper, client, {}); - - assert.notCalled(log); - }); - - it('sends back error message if whispering to missing client', () => { - say(client, 'hey no one', ChatType.Whisper, undefined, {}); - - assert.calledOnce(log); - expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); - }); - - it('runs commands for whispers', () => { - runCommand.returns(true); - const target = mockClient(); - - say(client, '/gifts', ChatType.Whisper, target, {}); - - assert.calledOnce(log); - assert.calledWith(runCommand, client, 'gifts', '', ChatType.Whisper, target, {}); - }); + describe('say()', () => { + let client: IClient; + let region: ServerRegion; + let world: World; + let runCommand: SinonStub; + let log: SinonStub; + let say: Say; + let checkSpam: SinonStub; + let reportSwears: SinonStub; + let reportForbidden: SinonStub; + let reportSuspicious: SinonStub; + let isSuspiciousMessage: SinonStub; + let execAction: SinonStub; + + beforeEach(() => { + execAction = stub(playerUtils, 'execAction'); + region = createServerRegion(1, 1); + client = mockClient(); + client.pony.region = region; + region.clients.push(client); + world = mock(World); + const map = createServerMap('', 0, 1, 1); + stub(world, 'getMainMap').returns(map); + runCommand = stub(); + log = stub(); + checkSpam = stub(); + reportSwears = stub(); + reportForbidden = stub(); + reportSuspicious = stub(); + isSuspiciousMessage = stub(); + const spamCommands = ['roll']; + say = createSay( + world, runCommand, log, checkSpam, reportSwears, reportForbidden, + reportSuspicious, spamCommands, () => 0, isSuspiciousMessage); + }); + + afterEach(() => { + execAction.restore(); + }); + + it('does nothing if whispering to self', () => { + say(client, 'hey me', ChatType.Whisper, client, {}); + + assert.notCalled(log); + }); + + it('sends back error message if whispering to missing client', () => { + say(client, 'hey no one', ChatType.Whisper, undefined, {}); + + assert.calledOnce(log); + expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); + }); + + it('runs commands for whispers', () => { + runCommand.returns(true); + const target = mockClient(); + + say(client, '/gifts', ChatType.Whisper, target, {}); + + assert.calledOnce(log); + assert.calledWith(runCommand, client, 'gifts', '', ChatType.Whisper, target, {}); + }); - it('sends back error message if whispering to non-friend when having non-friend whispers disabled', () => { - client.accountSettings.ignoreNonFriendWhispers = true; + it('sends back error message if whispering to non-friend when having non-friend whispers disabled', () => { + client.accountSettings.ignoreNonFriendWhispers = true; - say(client, 'hey you', ChatType.Whisper, mockClient(), {}); - - assert.calledOnce(log); - expect(client.saysQueue).eql([ - [client.pony.id, 'You can only whisper to friends', MessageType.System], - ]); - }); + say(client, 'hey you', ChatType.Whisper, mockClient(), {}); + + assert.calledOnce(log); + expect(client.saysQueue).eql([ + [client.pony.id, 'You can only whisper to friends', MessageType.System], + ]); + }); - it('sends whisper to target', () => { - const target = mockClient(); + it('sends whisper to target', () => { + const target = mockClient(); - say(client, 'hey you', ChatType.Whisper, target, {}); + say(client, 'hey you', ChatType.Whisper, target, {}); - assert.calledOnce(log); - expect(target.saysQueue).eql([[client.pony.id, 'hey you', MessageType.Whisper]]); - }); + assert.calledOnce(log); + expect(target.saysQueue).eql([[client.pony.id, 'hey you', MessageType.Whisper]]); + }); - it('does not send whisper to target if shadowed', () => { - const target = mockClient(); - client.shadowed = true; + it('does not send whisper to target if shadowed', () => { + const target = mockClient(); + client.shadowed = true; - say(client, 'hey you', ChatType.Whisper, target, {}); + say(client, 'hey you', ChatType.Whisper, target, {}); - assert.calledOnce(log); - expect(client.saysQueue).eql([[target.pony.id, 'hey you', MessageType.WhisperTo]]); - expect(target.saysQueue).eql([]); - }); + assert.calledOnce(log); + expect(client.saysQueue).eql([[target.pony.id, 'hey you', MessageType.WhisperTo]]); + expect(target.saysQueue).eql([]); + }); - it('does not send whisper to target if muted', () => { - const target = mockClient(); - client.account.mute = Date.now() + 10000; + it('does not send whisper to target if muted', () => { + const target = mockClient(); + client.account.mute = Date.now() + 10000; - say(client, 'hey you', ChatType.Whisper, target, {}); + say(client, 'hey you', ChatType.Whisper, target, {}); - assert.calledOnce(log); - expect(client.saysQueue).eql([[target.pony.id, 'hey you', MessageType.WhisperTo]]); - expect(target.saysQueue).eql([]); - }); + assert.calledOnce(log); + expect(client.saysQueue).eql([[target.pony.id, 'hey you', MessageType.WhisperTo]]); + expect(target.saysQueue).eql([]); + }); - it('does not send whisper to target if target is shadowed', () => { - const target = mockClient(); - target.shadowed = true; + it('does not send whisper to target if target is shadowed', () => { + const target = mockClient(); + target.shadowed = true; - say(client, 'hey you', ChatType.Whisper, target, {}); + say(client, 'hey you', ChatType.Whisper, target, {}); - assert.calledOnce(log); - expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); - expect(target.saysQueue).eql([]); - }); + assert.calledOnce(log); + expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); + expect(target.saysQueue).eql([]); + }); - it('does not send whisper to target if target is hidden', () => { - const target = mockClient(); - target.hides.add(client.accountId); + it('does not send whisper to target if target is hidden', () => { + const target = mockClient(); + target.hides.add(client.accountId); - say(client, 'hey you', ChatType.Whisper, target, {}); + say(client, 'hey you', ChatType.Whisper, target, {}); - assert.calledOnce(log); - expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); - expect(target.saysQueue).eql([]); - }); + assert.calledOnce(log); + expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); + expect(target.saysQueue).eql([]); + }); - it('does not check for spam if whispering to friend', () => { - const target = mockClient(); - client.friends.add(target.accountId); + it('does not check for spam if whispering to friend', () => { + const target = mockClient(); + client.friends.add(target.accountId); - say(client, 'hey you', ChatType.Whisper, target, {}); + say(client, 'hey you', ChatType.Whisper, target, {}); - assert.notCalled(checkSpam); - }); + assert.notCalled(checkSpam); + }); - it('logs chat message', () => { - say(client, 'hey there', ChatType.Say, undefined, {}); + it('logs chat message', () => { + say(client, 'hey there', ChatType.Say, undefined, {}); - assert.calledWith(log, client, 'hey there', ChatType.Say, false); - }); + assert.calledWith(log, client, 'hey there', ChatType.Say, false); + }); - it('logs party chat message', () => { - say(client, 'hey there', ChatType.Party, undefined, {}); + it('logs party chat message', () => { + say(client, 'hey there', ChatType.Party, undefined, {}); - assert.calledWith(log, client, 'hey there', ChatType.Party, false); - }); + assert.calledWith(log, client, 'hey there', ChatType.Party, false); + }); - it('trims text', () => { - say(client, ' test ', ChatType.Say, undefined, {}); + it('trims text', () => { + say(client, ' test ', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); - it('sends say to everyone in the world', () => { - say(client, 'test', ChatType.Say, undefined, {}); + it('sends say to everyone in the world', () => { + say(client, 'test', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); - it('sends say for say command in party chat', () => { - say(client, '/s test', ChatType.Party, undefined, {}); + it('sends say for say command in party chat', () => { + say(client, '/s test', ChatType.Party, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); - it('sends say for invalid type', () => { - say(client, 'test', 100, undefined, {}); + it('sends say for invalid type', () => { + say(client, 'test', 100, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); - it('sends think message to everyone', () => { - say(client, 'test', ChatType.Think, undefined, {}); + it('sends think message to everyone', () => { + say(client, 'test', ChatType.Think, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Thinking]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Thinking]]); + }); - it('sends think message to everyone', () => { - say(client, '/t test', ChatType.Say, undefined, {}); + it('sends think message to everyone', () => { + say(client, '/t test', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Thinking]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Thinking]]); + }); - it('ignores empty say command', () => { - say(client, '/s ', ChatType.Say, undefined, {}); + it('ignores empty say command', () => { + say(client, '/s ', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([]); + }); - it(`reports suspicious message`, () => { - isSuspiciousMessage.withArgs('foo bar').returns(true); + it(`reports suspicious message`, () => { + isSuspiciousMessage.withArgs('foo bar').returns(true); - say(client, 'foo bar', ChatType.Say, undefined, {}); + say(client, 'foo bar', ChatType.Say, undefined, {}); - assert.calledWith(reportSuspicious, client, 'foo bar'); - }); + assert.calledWith(reportSuspicious, client, 'foo bar'); + }); - it(`reports suspicious party message with prefix`, () => { - isSuspiciousMessage.withArgs('foo bar').returns(true); + it(`reports suspicious party message with prefix`, () => { + isSuspiciousMessage.withArgs('foo bar').returns(true); - say(client, 'foo bar', ChatType.Party, undefined, {}); + say(client, 'foo bar', ChatType.Party, undefined, {}); - assert.calledWith(reportSuspicious, client, '/p foo bar'); - }); + assert.calledWith(reportSuspicious, client, '/p foo bar'); + }); - describe('in a party', () => { - beforeEach(() => { - client.party = { id: '', leader: client, clients: [client], pending: [] }; - }); + describe('in a party', () => { + beforeEach(() => { + client.party = { id: '', leader: client, clients: [client], pending: [] }; + }); - it('sends party message for party type', () => { - say(client, 'test', ChatType.Party, undefined, {}); + it('sends party message for party type', () => { + say(client, 'test', ChatType.Party, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + }); - it('sends party message for party command', () => { - say(client, '/p test', ChatType.Say, undefined, {}); + it('sends party message for party command', () => { + say(client, '/p test', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + }); - it('ignores empty party command', () => { - say(client, '/p ', ChatType.Say, undefined, {}); + it('ignores empty party command', () => { + say(client, '/p ', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([]); + }); - it('sends party think message to party if in party chat', () => { - say(client, '/t test', ChatType.Party, undefined, {}); + it('sends party think message to party if in party chat', () => { + say(client, '/t test', ChatType.Party, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.PartyThinking]]); - }); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.PartyThinking]]); + }); + }); - it('does not set expression in party think command', () => { - client.pony.options!.expr = 123; + it('does not set expression in party think command', () => { + client.pony.options!.expr = 123; - say(client, '/t :)', ChatType.Party, undefined, {}); + say(client, '/t :)', ChatType.Party, undefined, {}); - expect(client.pony.options!.expr).equal(123); - }); + expect(client.pony.options!.expr).equal(123); + }); - it('runs command if text is command', () => { - runCommand.returns(true); + it('runs command if text is command', () => { + runCommand.returns(true); - say(client, '/test arg', ChatType.Say, undefined, {}); + say(client, '/test arg', ChatType.Say, undefined, {}); - assert.calledWith(runCommand, client, 'test', 'arg'); - }); + assert.calledWith(runCommand, client, 'test', 'arg'); + }); - it('notifies of invalid command', () => { - runCommand.returns(false); + it('notifies of invalid command', () => { + runCommand.returns(false); - say(client, '/test arg', ChatType.Say, undefined, {}); + say(client, '/test arg', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'Invalid command', MessageType.System]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'Invalid command', MessageType.System]]); + }); - it('sets expression', () => { - say(client, 'hi :)', ChatType.Say, undefined, {}); + it('sets expression', () => { + say(client, 'hi :)', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); - }); + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); + }); - it('sets invisible expression', () => { - runCommand.returns(false); + it('sets invisible expression', () => { + runCommand.returns(false); - say(client, '/:)', ChatType.Say, undefined, {}); + say(client, '/:)', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); - expect(client.saysQueue).eql([]); - }); + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); + expect(client.saysQueue).eql([]); + }); - it('sets invisible expression (with space)', () => { - runCommand.returns(false); + it('sets invisible expression (with space)', () => { + runCommand.returns(false); - say(client, '/ :)', ChatType.Say, undefined, {}); + say(client, '/ :)', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); - expect(client.saysQueue).eql([]); - }); + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); + expect(client.saysQueue).eql([]); + }); - it('does not set expression in think command', () => { - client.pony.options!.expr = 123; + it('does not set expression in think command', () => { + client.pony.options!.expr = 123; - say(client, '/t :)', ChatType.Say, undefined, {}); + say(client, '/t :)', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(123); - }); + expect(client.pony.options!.expr).equal(123); + }); - it('calls laugh action', () => { - say(client, 'haha', ChatType.Say, undefined, {}); + it('calls laugh action', () => { + say(client, 'haha', ChatType.Say, undefined, {}); - assert.calledWith(execAction, client, Action.Laugh); - }); + assert.calledWith(execAction, client, Action.Laugh); + }); - it('checks for spam', () => { - const settings = {}; + it('checks for spam', () => { + const settings = {}; - say(client, 'test', ChatType.Say, undefined, settings); + say(client, 'test', ChatType.Say, undefined, settings); - assert.calledWith(checkSpam, client, 'test', settings); - }); + assert.calledWith(checkSpam, client, 'test', settings); + }); - it('does not check for spam in party chat', () => { - say(client, 'test', ChatType.Party, undefined, {}); + it('does not check for spam in party chat', () => { + say(client, 'test', ChatType.Party, undefined, {}); - assert.notCalled(checkSpam); - }); + assert.notCalled(checkSpam); + }); - it('does not trim repeated letters in party chat', () => { - client.party = { id: '', leader: client, clients: [client], pending: [] }; + it('does not trim repeated letters in party chat', () => { + client.party = { id: '', leader: client, clients: [client], pending: [] }; - say(client, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', ChatType.Party, undefined, {}); + say(client, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', ChatType.Party, undefined, {}); - expect(client.saysQueue).eql([ - [client.pony.id, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', MessageType.Party], - ]); - }); + expect(client.saysQueue).eql([ + [client.pony.id, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', MessageType.Party], + ]); + }); - it('trims repeated letters', () => { - say(client, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', ChatType.Say, undefined, {}); + it('trims repeated letters', () => { + say(client, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'AAAAAAAAAAAAAAAA…', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'AAAAAAAAAAAAAAAA…', MessageType.Chat]]); + }); - it('trims repeated emoji', () => { - say(client, '🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸', ChatType.Say, undefined, {}); + it('trims repeated emoji', () => { + say(client, '🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, '🌸🌸🌸🌸🌸🌸🌸🌸…', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, '🌸🌸🌸🌸🌸🌸🌸🌸…', MessageType.Chat]]); + }); - describe('supporter', () => { - it('sends supporter message', () => { - client.supporterLevel = 1; + describe('supporter', () => { + it('sends supporter message', () => { + client.supporterLevel = 1; - say(client, 'hello', ChatType.Supporter, undefined, {}); + say(client, 'hello', ChatType.Supporter, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter1]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter1]]); + }); - it('sends supporter message of correct level (2)', () => { - client.supporterLevel = 2; + it('sends supporter message of correct level (2)', () => { + client.supporterLevel = 2; - say(client, 'hello', ChatType.Supporter, undefined, {}); + say(client, 'hello', ChatType.Supporter, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter2]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter2]]); + }); - it('sends supporter message of correct level (3)', () => { - client.supporterLevel = 3; + it('sends supporter message of correct level (3)', () => { + client.supporterLevel = 3; - say(client, 'hello', ChatType.Supporter, undefined, {}); + say(client, 'hello', ChatType.Supporter, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter3]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter3]]); + }); - it('sends supporter message as regular message for non-supporters', () => { - say(client, 'hello', ChatType.Supporter, undefined, {}); + it('sends supporter message as regular message for non-supporters', () => { + say(client, 'hello', ChatType.Supporter, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Chat]]); + }); - it('sends supporter 1 message', () => { - client.supporterLevel = 3; + it('sends supporter 1 message', () => { + client.supporterLevel = 3; - say(client, 'hello', ChatType.Supporter1, undefined, {}); + say(client, 'hello', ChatType.Supporter1, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter1]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter1]]); + }); - it('sends supporter 2 message', () => { - client.supporterLevel = 3; + it('sends supporter 2 message', () => { + client.supporterLevel = 3; - say(client, 'hello', ChatType.Supporter2, undefined, {}); + say(client, 'hello', ChatType.Supporter2, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter2]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter2]]); + }); - it('sends supporter 3 message', () => { - client.supporterLevel = 3; + it('sends supporter 3 message', () => { + client.supporterLevel = 3; - say(client, 'hello', ChatType.Supporter3, undefined, {}); + say(client, 'hello', ChatType.Supporter3, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter3]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Supporter3]]); + }); - it('sends chat message if supporter level is lower than message level', () => { - client.supporterLevel = 2; + it('sends chat message if supporter level is lower than message level', () => { + client.supporterLevel = 2; - say(client, 'hello', ChatType.Supporter3, undefined, {}); + say(client, 'hello', ChatType.Supporter3, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Chat]]); + }); - it('sends chat message if non-supporter sends supporter messsage', () => { - say(client, 'hello', ChatType.Supporter, undefined, {}); + it('sends chat message if non-supporter sends supporter messsage', () => { + say(client, 'hello', ChatType.Supporter, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Chat]]); - }); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hello', MessageType.Chat]]); + }); + }); - describe('urls', () => { - it('removes url in regular chat', () => { - say(client, 'hey www.google.com', ChatType.Say, undefined, {}); + describe('urls', () => { + it('removes url in regular chat', () => { + say(client, 'hey www.google.com', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'hey [LINK]', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'hey [LINK]', MessageType.Chat]]); + }); - it('removes url in censored messages', () => { - say(client, 'fuck www.google.com', ChatType.Say, undefined, {}); + it('removes url in censored messages', () => { + say(client, 'fuck www.google.com', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'fuck [LINK]', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'fuck [LINK]', MessageType.Chat]]); + }); - it('does not remove url in party chat', () => { - client.party = { id: '', leader: client, clients: [client], pending: [] }; + it('does not remove url in party chat', () => { + client.party = { id: '', leader: client, clients: [client], pending: [] }; - say(client, 'hey www.google.com', ChatType.Party, undefined, {}); + say(client, 'hey www.google.com', ChatType.Party, undefined, {}); - expect(client.saysQueue).eql([ - [client.pony.id, 'hey www.google.com', MessageType.Party], - ]); - }); + expect(client.saysQueue).eql([ + [client.pony.id, 'hey www.google.com', MessageType.Party], + ]); + }); - it('does not report url as swearing', () => { - say(client, 'hey www.google.com', ChatType.Say, undefined, { filterSwears: true }); + it('does not report url as swearing', () => { + say(client, 'hey www.google.com', ChatType.Say, undefined, { filterSwears: true }); - assert.notCalled(reportSwears); - }); - }); + assert.notCalled(reportSwears); + }); + }); - describe('commands', () => { - it('checks for spam', () => { - runCommand.returns(true); - const settings = {}; + describe('commands', () => { + it('checks for spam', () => { + runCommand.returns(true); + const settings = {}; - say(client, '/roll', ChatType.Say, undefined, settings); + say(client, '/roll', ChatType.Say, undefined, settings); - assert.calledWith(checkSpam, client, '/roll', settings); - }); + assert.calledWith(checkSpam, client, '/roll', settings); + }); - it('does not check for spam in party chat', () => { - runCommand.returns(true); + it('does not check for spam in party chat', () => { + runCommand.returns(true); - say(client, '/test', ChatType.Party, undefined, {}); + say(client, '/test', ChatType.Party, undefined, {}); - assert.notCalled(checkSpam); - }); + assert.notCalled(checkSpam); + }); - it('does not check for spam for /:)', () => { - runCommand.returns(false); + it('does not check for spam for /:)', () => { + runCommand.returns(false); - say(client, '/:)', ChatType.Say, undefined, {}); + say(client, '/:)', ChatType.Say, undefined, {}); - assert.notCalled(checkSpam); - }); + assert.notCalled(checkSpam); + }); - it('does not check for spam for /e', () => { - runCommand.returns(true); + it('does not check for spam for /e', () => { + runCommand.returns(true); - say(client, '/e :)', ChatType.Say, undefined, {}); + say(client, '/e :)', ChatType.Say, undefined, {}); - assert.notCalled(checkSpam); - }); - }); + assert.notCalled(checkSpam); + }); + }); - describe('swear message', () => { - it('does not report normal message', () => { - say(client, 'test', ChatType.Say, undefined, {}); + describe('swear message', () => { + it('does not report normal message', () => { + say(client, 'test', ChatType.Say, undefined, {}); - assert.notCalled(reportSwears); - }); + assert.notCalled(reportSwears); + }); - it('does not report is filterSwears is false', () => { - say(client, 'fuck', ChatType.Say, undefined, { filterSwears: false }); + it('does not report is filterSwears is false', () => { + say(client, 'fuck', ChatType.Say, undefined, { filterSwears: false }); - assert.notCalled(reportSwears); - }); + assert.notCalled(reportSwears); + }); - it('reports', () => { - const settings = { filterSwears: true }; - - say(client, 'fuck', ChatType.Say, undefined, settings); - - assert.calledWith(reportSwears, client, 'fuck', settings); - }); - }); - - describe('kicking', () => { - let kick: SinonStub; - - beforeEach(() => { - kick = stub(world, 'kick'); - }); - - it('kicks player if messages contain swears and kickSwearing settings is true', () => { - say(client, 'fuck', ChatType.Say, undefined, { kickSwearing: true }); - - assert.calledWith(kick, client, 'swearing', LeaveReason.Swearing); - }); - - it('does not kick player if messages contain swears and kickSwearing setting is true but is party message', () => { - say(client, 'fuck', ChatType.Party, undefined, { kickSwearing: true }); - - assert.notCalled(kick); - }); - - it('does not reset player to spawn if kickSwearingToSpawn setting is false', () => { - const resetToSpawn = stub(world, 'resetToSpawn'); - - say(client, 'fuck', ChatType.Say, undefined, { kickSwearing: true, kickSwearingToSpawn: false }); - - assert.notCalled(resetToSpawn); - }); - - it('resets player to spawn if kickSwearingToSpawn setting is true', () => { - const resetToSpawn = stub(world, 'resetToSpawn'); - - say(client, 'fuck', ChatType.Say, undefined, { kickSwearing: true, kickSwearingToSpawn: true }); - - assert.calledWith(resetToSpawn, client); - }); - }); - }); - - describe('filterUrls()', () => { - [ - '', - 'test', - 'hello there.', - '12.3254', - '999.999.999.999', - 'www...', - 'Pare...Com Isto...', - 'Solo tengo 1.000.000.000.000', - // 'Is a smol .com', - 'battle.net', - 'paint.net', - 'fimfiction.net', - 'fanfiction.net', - 'Serio,com licença', - 'Sim,net caiu', - ].forEach(message => it(`returns the same message: "${message}"`, () => { - expect(filterUrls(message)).equal(message); - })); - - [ - ['1.1.1.1', '[LINK]'], - ['192.168.0.255', '[LINK]'], - ['hello 192.168.0.255 aaa', 'hello [LINK] aaa'], - ['hello192.168.0.255aaa', 'hello[LINK]aaa'], - ['192.168.0.255 aaa 192.168.0.255', '[LINK] aaa [LINK]'], - ['ip:147.230.64.174', 'ip:[LINK]'] - ].forEach(([message, expected]) => it(`replaces ip addresses: "${message}"`, () => { - expect(filterUrls(message)).equal(expected); - })); - - [ - ['http://google.com/', '[LINK]'], - ['HTTP://GOOGLE.COM/', '[LINK]'], - ['https://foo', '[LINK]'], - ['https//last_name_is_.net', '[LINK]'], - ['www.test.pl', '[LINK]'], - ['foo,com/bar', '[LINK]'], - ['a123,net/bar/123', '[LINK]'], - ['foo.com', '[LINK]'], - ['foo.c0m', '[LINK]'], - ['foo.net', '[LINK]'], - ['foo.net/abc/xyz', '[LINK]/abc/xyz'], - ['WWW.test.pl', '[LINK]'], - ['foo.COM', '[LINK]'], - ['hello http://google.com/', 'hello [LINK]'], - ['hello foo.com aaa', 'hello [LINK] aaa'], - ['hellohttp://google.com/', 'hello[LINK]'], - ['http://google.com/ aaa http://google.com/', '[LINK] aaa [LINK]'], - ['foo.com foo.com', '[LINK] [LINK]'], - ['goo.gl/Cjy2Qj', '[LINK]'], - ['goo,gl/Cjy2Qj', '[LINK]'], - ['bit.ly/1mHSR3x', '[LINK]'], - ['adf.ly/13ajex', '[LINK]'], - ['dhttps://www.twitch.tv/foobar', 'd[LINK]'], - ['bestgore. com', '[LINK]'], - [ - 'https:/ mlpfanart.fandom .com/wiki/Banned_From_Equestria_(Daily)', - 'https:/ [LINK]/wiki/Banned_From_Equestria_(Daily)' - ], - ].forEach(([message, expected]) => it(`replaces urls addresses: "${message}"`, () => { - expect(filterUrls(message)).equal(expected); - })); - }); - - describe('sayToClient()', () => { - let client: IClient; - let settings: GameServerSettings; - - beforeEach(() => { - client = mockClient(); - client.camera.w = toScreenX(10); - client.camera.h = toScreenY(10); - settings = {}; - }); - - it('sends message to client', () => { - sayToClient(client, serverEntity(5), 'foo', 'foo', MessageType.Chat, settings); - - expect(client.saysQueue).eql([ - [5, 'foo', MessageType.Chat], - ]); - }); - - it('sends original message if filterSwearWords is false', () => { - client.accountSettings = { filterSwearWords: false }; - - sayToClient(client, serverEntity(5), 'foo', 'bar', MessageType.Chat, settings); - - expect(client.saysQueue).eql([ - [5, 'foo', MessageType.Chat], - ]); - }); - - it('sends censored message if filterSwearWords is true', () => { - client.accountSettings = { filterSwearWords: true }; - - sayToClient(client, serverEntity(5), 'foo', 'bar', MessageType.Chat, settings); - - expect(client.saysQueue).eql([ - [5, 'bar', MessageType.Chat], - ]); - }); - - it('sends censored message if filterSwears is true', () => { - settings.filterSwears = true; - - sayToClient(client, serverEntity(5), 'foo', '***', MessageType.Chat, settings); - - expect(client.saysQueue).eql([ - [5, '***', MessageType.Chat], - ]); - }); - - it('sends original message to self', () => { - client.pony.id = 5; - - sayToClient(client, client.pony, 'foo', 'foo', MessageType.Chat, settings); - - expect(client.saysQueue).eql([ - [5, 'foo', MessageType.Chat], - ]); - }); - - it('sends original message to self if filterSwearWords is true', () => { - client.pony.id = 5; - client.account.settings = { filterSwearWords: true }; - - sayToClient(client, client.pony, 'foo', 'bar', MessageType.Chat, settings); - - expect(client.saysQueue).eql([ - [5, 'foo', MessageType.Chat], - ]); - }); - - it('sends original message to self if filterSwears is true', () => { - settings.filterSwears = true; - client.pony.id = 5; + it('reports', () => { + const settings = { filterSwears: true }; + + say(client, 'fuck', ChatType.Say, undefined, settings); + + assert.calledWith(reportSwears, client, 'fuck', settings); + }); + }); + + describe('kicking', () => { + let kick: SinonStub; + + beforeEach(() => { + kick = stub(world, 'kick'); + }); + + it('kicks player if messages contain swears and kickSwearing settings is true', () => { + say(client, 'fuck', ChatType.Say, undefined, { kickSwearing: true }); + + assert.calledWith(kick, client, 'swearing', LeaveReason.Swearing); + }); + + it('does not kick player if messages contain swears and kickSwearing setting is true but is party message', () => { + say(client, 'fuck', ChatType.Party, undefined, { kickSwearing: true }); + + assert.notCalled(kick); + }); + + it('does not reset player to spawn if kickSwearingToSpawn setting is false', () => { + const resetToSpawn = stub(world, 'resetToSpawn'); + + say(client, 'fuck', ChatType.Say, undefined, { kickSwearing: true, kickSwearingToSpawn: false }); + + assert.notCalled(resetToSpawn); + }); + + it('resets player to spawn if kickSwearingToSpawn setting is true', () => { + const resetToSpawn = stub(world, 'resetToSpawn'); + + say(client, 'fuck', ChatType.Say, undefined, { kickSwearing: true, kickSwearingToSpawn: true }); + + assert.calledWith(resetToSpawn, client); + }); + }); + }); + + describe('filterUrls()', () => { + [ + '', + 'test', + 'hello there.', + '12.3254', + '999.999.999.999', + 'www...', + 'Pare...Com Isto...', + 'Solo tengo 1.000.000.000.000', + // 'Is a smol .com', + 'battle.net', + 'paint.net', + 'fimfiction.net', + 'fanfiction.net', + 'Serio,com licença', + 'Sim,net caiu', + ].forEach(message => it(`returns the same message: "${message}"`, () => { + expect(filterUrls(message)).equal(message); + })); + + [ + ['1.1.1.1', '[LINK]'], + ['192.168.0.255', '[LINK]'], + ['hello 192.168.0.255 aaa', 'hello [LINK] aaa'], + ['hello192.168.0.255aaa', 'hello[LINK]aaa'], + ['192.168.0.255 aaa 192.168.0.255', '[LINK] aaa [LINK]'], + ['ip:147.230.64.174', 'ip:[LINK]'] + ].forEach(([message, expected]) => it(`replaces ip addresses: "${message}"`, () => { + expect(filterUrls(message)).equal(expected); + })); + + [ + ['http://google.com/', '[LINK]'], + ['HTTP://GOOGLE.COM/', '[LINK]'], + ['https://foo', '[LINK]'], + ['https//last_name_is_.net', '[LINK]'], + ['www.test.pl', '[LINK]'], + ['foo,com/bar', '[LINK]'], + ['a123,net/bar/123', '[LINK]'], + ['foo.com', '[LINK]'], + ['foo.c0m', '[LINK]'], + ['foo.net', '[LINK]'], + ['foo.net/abc/xyz', '[LINK]/abc/xyz'], + ['WWW.test.pl', '[LINK]'], + ['foo.COM', '[LINK]'], + ['hello http://google.com/', 'hello [LINK]'], + ['hello foo.com aaa', 'hello [LINK] aaa'], + ['hellohttp://google.com/', 'hello[LINK]'], + ['http://google.com/ aaa http://google.com/', '[LINK] aaa [LINK]'], + ['foo.com foo.com', '[LINK] [LINK]'], + ['goo.gl/Cjy2Qj', '[LINK]'], + ['goo,gl/Cjy2Qj', '[LINK]'], + ['bit.ly/1mHSR3x', '[LINK]'], + ['adf.ly/13ajex', '[LINK]'], + ['dhttps://www.twitch.tv/foobar', 'd[LINK]'], + ['bestgore. com', '[LINK]'], + [ + 'https:/ mlpfanart.fandom .com/wiki/Banned_From_Equestria_(Daily)', + 'https:/ [LINK]/wiki/Banned_From_Equestria_(Daily)' + ], + ].forEach(([message, expected]) => it(`replaces urls addresses: "${message}"`, () => { + expect(filterUrls(message)).equal(expected); + })); + }); + + describe('sayToClient()', () => { + let client: IClient; + let settings: GameServerSettings; + + beforeEach(() => { + client = mockClient(); + client.camera.w = toScreenX(10); + client.camera.h = toScreenY(10); + settings = {}; + }); + + it('sends message to client', () => { + sayToClient(client, serverEntity(5), 'foo', 'foo', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); + + it('sends original message if filterSwearWords is false', () => { + client.accountSettings = { filterSwearWords: false }; + + sayToClient(client, serverEntity(5), 'foo', 'bar', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); + + it('sends censored message if filterSwearWords is true', () => { + client.accountSettings = { filterSwearWords: true }; + + sayToClient(client, serverEntity(5), 'foo', 'bar', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'bar', MessageType.Chat], + ]); + }); + + it('sends censored message if filterSwears is true', () => { + settings.filterSwears = true; + + sayToClient(client, serverEntity(5), 'foo', '***', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, '***', MessageType.Chat], + ]); + }); + + it('sends original message to self', () => { + client.pony.id = 5; + + sayToClient(client, client.pony, 'foo', 'foo', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); + + it('sends original message to self if filterSwearWords is true', () => { + client.pony.id = 5; + client.account.settings = { filterSwearWords: true }; + + sayToClient(client, client.pony, 'foo', 'bar', MessageType.Chat, settings); + + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); + + it('sends original message to self if filterSwears is true', () => { + settings.filterSwears = true; + client.pony.id = 5; - sayToClient(client, client.pony, 'foo', 'bar', MessageType.Chat, settings); + sayToClient(client, client.pony, 'foo', 'bar', MessageType.Chat, settings); - expect(client.saysQueue).eql([ - [5, 'foo', MessageType.Chat], - ]); - }); + expect(client.saysQueue).eql([ + [5, 'foo', MessageType.Chat], + ]); + }); - it('sends cyrillic message to self if filterCyrillic setting is true', () => { - client.accountSettings = { filterCyrillic: true }; - client.pony.id = 5; + it('sends cyrillic message to self if filterCyrillic setting is true', () => { + client.accountSettings = { filterCyrillic: true }; + client.pony.id = 5; - sayToClient(client, client.pony, 'Здравствуй', 'Здравствуй', MessageType.Chat, settings); + sayToClient(client, client.pony, 'Здравствуй', 'Здравствуй', MessageType.Chat, settings); - expect(client.saysQueue).eql([ - [5, 'Здравствуй', MessageType.Chat], - ]); - }); + expect(client.saysQueue).eql([ + [5, 'Здравствуй', MessageType.Chat], + ]); + }); - it('ignores messages if ignored', () => { - const e = serverEntity(5); - e.client = mockClient(); - e.client.ignores.add(client.accountId); + it('ignores messages if ignored', () => { + const e = serverEntity(5); + e.client = mockClient(); + e.client.ignores.add(client.accountId); - expect(sayToClient(client, e, 'test', 'test', MessageType.Chat, settings)).false; + expect(sayToClient(client, e, 'test', 'test', MessageType.Chat, settings)).false; - expect(client.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([]); + }); - it('ignores messages if hidden', () => { - const e = serverEntity(5); - e.client = mockClient(); - client.hides.add(e.client.accountId); + it('ignores messages if hidden', () => { + const e = serverEntity(5); + e.client = mockClient(); + client.hides.add(e.client.accountId); - expect(sayToClient(client, e, 'test', 'test', MessageType.Chat, settings)).false; + expect(sayToClient(client, e, 'test', 'test', MessageType.Chat, settings)).false; - expect(client.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([]); + }); - it('ignores messages if swearing in whisper to non-friend', () => { - const e = serverEntity(5); - e.client = mockClient(); - settings.hideSwearing = true; + it('ignores messages if swearing in whisper to non-friend', () => { + const e = serverEntity(5); + e.client = mockClient(); + settings.hideSwearing = true; - expect(sayToClient(client, e, 'test', '****', MessageType.Whisper, settings)).false; + expect(sayToClient(client, e, 'test', '****', MessageType.Whisper, settings)).false; - expect(client.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([]); + }); - it('ignores messages if outside client camera', () => { - const e = serverEntity(5, 10, 10); - client.camera.x = toScreenX(20); - client.camera.y = toScreenY(20); - client.camera.w = toScreenX(10); - client.camera.h = toScreenY(10); - client.pony.x = 25; - client.pony.y = 25; + it('ignores messages if outside client camera', () => { + const e = serverEntity(5, 10, 10); + client.camera.x = toScreenX(20); + client.camera.y = toScreenY(20); + client.camera.w = toScreenX(10); + client.camera.h = toScreenY(10); + client.pony.x = 25; + client.pony.y = 25; - sayToClient(client, e, 'test', 'test', MessageType.Chat, settings); + sayToClient(client, e, 'test', 'test', MessageType.Chat, settings); - expect(client.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([]); + }); - it('ignores messages if contain swears and hideSwearing setting is true', () => { - settings.hideSwearing = true; + it('ignores messages if contain swears and hideSwearing setting is true', () => { + settings.hideSwearing = true; - sayToClient(client, serverEntity(5), 'fuck', '****', MessageType.Chat, settings); + sayToClient(client, serverEntity(5), 'fuck', '****', MessageType.Chat, settings); - expect(client.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([]); + }); - it('does not ignore messages if contain swears and hideSwearing setting is true but sending to self', () => { - settings.hideSwearing = true; + it('does not ignore messages if contain swears and hideSwearing setting is true but sending to self', () => { + settings.hideSwearing = true; - sayToClient(client, client.pony, 'fuck', '****', MessageType.Chat, settings); + sayToClient(client, client.pony, 'fuck', '****', MessageType.Chat, settings); - expect(client.saysQueue).eql([[client.pony.id, 'fuck', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'fuck', MessageType.Chat]]); + }); - it('does not ignore messages if contain swears and hideSwearing setting is true but is party message', () => { - settings.hideSwearing = true; + it('does not ignore messages if contain swears and hideSwearing setting is true but is party message', () => { + settings.hideSwearing = true; - sayToClient(client, serverEntity(6), 'fuck', '****', MessageType.Party, settings); + sayToClient(client, serverEntity(6), 'fuck', '****', MessageType.Party, settings); - expect(client.saysQueue).eql([[6, 'fuck', MessageType.Party]]); - }); + expect(client.saysQueue).eql([[6, 'fuck', MessageType.Party]]); + }); - it('sends party messages even if outside client camera', () => { - const e = serverEntity(5, 1, 1); - client.camera.x = toScreenX(20); - client.camera.y = toScreenY(20); - client.camera.w = toScreenX(10); - client.camera.h = toScreenY(10); - client.pony.x = 25; - client.pony.y = 25; + it('sends party messages even if outside client camera', () => { + const e = serverEntity(5, 1, 1); + client.camera.x = toScreenX(20); + client.camera.y = toScreenY(20); + client.camera.w = toScreenX(10); + client.camera.h = toScreenY(10); + client.pony.x = 25; + client.pony.y = 25; - sayToClient(client, e, 'test', 'test', MessageType.Party, settings); + sayToClient(client, e, 'test', 'test', MessageType.Party, settings); - expect(client.saysQueue).eql([[5, 'test', MessageType.Party]]); - }); + expect(client.saysQueue).eql([[5, 'test', MessageType.Party]]); + }); - it('sends messages if hidden but client is moderator', () => { - const e = serverEntity(5); - e.client = mockClient(); - client.isMod = true; - client.hides.add(e.client.accountId); + it('sends messages if hidden but client is moderator', () => { + const e = serverEntity(5); + e.client = mockClient(); + client.isMod = true; + client.hides.add(e.client.accountId); - sayToClient(client, e, 'test', 'test', MessageType.Chat, settings); + sayToClient(client, e, 'test', 'test', MessageType.Chat, settings); - expect(client.saysQueue).eql([[5, 'test', MessageType.Chat]]); - }); - }); + expect(client.saysQueue).eql([[5, 'test', MessageType.Chat]]); + }); + }); - describe('sayTo()', () => { - it('adds message to message queue', () => { - const client = mockClient(); + describe('sayTo()', () => { + it('adds message to message queue', () => { + const client = mockClient(); - sayTo(client, entity(123), 'test', MessageType.Chat); + sayTo(client, entity(123), 'test', MessageType.Chat); - expect(client.saysQueue).eql([[123, 'test', MessageType.Chat]]); - }); - }); + expect(client.saysQueue).eql([[123, 'test', MessageType.Chat]]); + }); + }); - describe('sayToParty()', () => { - it('sends message to all party members', () => { - const client = mockClient(); - const client2 = mockClient(); - client.party = { clients: [client, client2] } as any; + describe('sayToParty()', () => { + it('sends message to all party members', () => { + const client = mockClient(); + const client2 = mockClient(); + client.party = { clients: [client, client2] } as any; - sayToParty(client, 'test', MessageType.Party); + sayToParty(client, 'test', MessageType.Party); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); - expect(client2.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + expect(client2.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + }); - it('sends message only to client if muted or shadowed', () => { - const client = mockClient(); - const client2 = mockClient(); - client.shadowed = true; - client.party = { clients: [client, client2] } as any; + it('sends message only to client if muted or shadowed', () => { + const client = mockClient(); + const client2 = mockClient(); + client.shadowed = true; + client.party = { clients: [client, client2] } as any; - sayToParty(client, 'test', MessageType.Party); + sayToParty(client, 'test', MessageType.Party); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); - expect(client2.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + expect(client2.saysQueue).eql([]); + }); - it('sends error message to client if not in party', () => { - const client = mockClient(); + it('sends error message to client if not in party', () => { + const client = mockClient(); - sayToParty(client, 'test', MessageType.Party); + sayToParty(client, 'test', MessageType.Party); - expect(client.saysQueue).eql([[client.pony.id, `you're not in a party`, MessageType.System]]); - }); - }); + expect(client.saysQueue).eql([[client.pony.id, `you're not in a party`, MessageType.System]]); + }); + }); - describe('sayWhisper()', () => { - it('sends whisper to client and target', () => { - const client = mockClient(); - const target = mockClient(); + describe('sayWhisper()', () => { + it('sends whisper to client and target', () => { + const client = mockClient(); + const target = mockClient(); - sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, target, {}); + sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, target, {}); - expect(client.saysQueue).eql([[target.pony.id, 'hey there', MessageType.WhisperTo]]); - expect(target.saysQueue).eql([[client.pony.id, 'hey there', MessageType.Whisper]]); - }); + expect(client.saysQueue).eql([[target.pony.id, 'hey there', MessageType.WhisperTo]]); + expect(target.saysQueue).eql([[client.pony.id, 'hey there', MessageType.Whisper]]); + }); - it('sends announcement whisper to client and target', () => { - const client = mockClient(); - const target = mockClient(); + it('sends announcement whisper to client and target', () => { + const client = mockClient(); + const target = mockClient(); - sayWhisper(client, 'hey there', 'hey there', MessageType.WhisperAnnouncement, target, {}); + sayWhisper(client, 'hey there', 'hey there', MessageType.WhisperAnnouncement, target, {}); - expect(client.saysQueue).eql([[target.pony.id, 'hey there', MessageType.WhisperToAnnouncement]]); - expect(target.saysQueue).eql([[client.pony.id, 'hey there', MessageType.WhisperAnnouncement]]); - }); + expect(client.saysQueue).eql([[target.pony.id, 'hey there', MessageType.WhisperToAnnouncement]]); + expect(target.saysQueue).eql([[client.pony.id, 'hey there', MessageType.WhisperAnnouncement]]); + }); - it('sends error message to client if target is undefined', () => { - const client = mockClient(); + it('sends error message to client if target is undefined', () => { + const client = mockClient(); - sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, undefined, {}); + sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); - }); + expect(client.saysQueue).eql([[client.pony.id, `Couldn't find this player`, MessageType.System]]); + }); - it('sends error message to client if target is ignoring whispers', () => { - const client = mockClient(); - const target = mockClient(); - target.accountSettings.ignoreNonFriendWhispers = true; + it('sends error message to client if target is ignoring whispers', () => { + const client = mockClient(); + const target = mockClient(); + target.accountSettings.ignoreNonFriendWhispers = true; - sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, target, {}); + sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, target, {}); - expect(client.saysQueue).eql([[client.pony.id, `Can't whisper to this player`, MessageType.System]]); - expect(target.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([[client.pony.id, `Can't whisper to this player`, MessageType.System]]); + expect(target.saysQueue).eql([]); + }); - it('sends whisper to client and target if target is ignoring whispers but is friend of client', () => { - const client = mockClient(); - const target = mockClient(); - target.accountSettings.ignoreNonFriendWhispers = true; - client.friends.add(target.accountId); + it('sends whisper to client and target if target is ignoring whispers but is friend of client', () => { + const client = mockClient(); + const target = mockClient(); + target.accountSettings.ignoreNonFriendWhispers = true; + client.friends.add(target.accountId); - sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, target, {}); + sayWhisper(client, 'hey there', 'hey there', MessageType.Whisper, target, {}); - expect(client.saysQueue).eql([[target.pony.id, 'hey there', MessageType.WhisperTo]]); - expect(target.saysQueue).eql([[client.pony.id, 'hey there', MessageType.Whisper]]); - }); - }); + expect(client.saysQueue).eql([[target.pony.id, 'hey there', MessageType.WhisperTo]]); + expect(target.saysQueue).eql([[client.pony.id, 'hey there', MessageType.Whisper]]); + }); + }); - describe('sayToEveryone()', () => { - it('sends message to all clients', () => { - const client = mockClient(); - const client2 = mockClient(); - const entity = client.pony; - entity.region = createServerRegion(0, 0); - entity.region.clients.push(client, client2); + describe('sayToEveryone()', () => { + it('sends message to all clients', () => { + const client = mockClient(); + const client2 = mockClient(); + const entity = client.pony; + entity.region = createServerRegion(0, 0); + entity.region.clients.push(client, client2); - sayToEveryone(client, 'test', 'test2', MessageType.Chat, {}); + sayToEveryone(client, 'test', 'test2', MessageType.Chat, {}); - expect(client.saysQueue).eql([[entity.id, 'test', MessageType.Chat]]); - expect(client2.saysQueue).eql([[entity.id, 'test', MessageType.Chat]]); - }); + expect(client.saysQueue).eql([[entity.id, 'test', MessageType.Chat]]); + expect(client2.saysQueue).eql([[entity.id, 'test', MessageType.Chat]]); + }); - it('sends message only to client if muted or shadowed', () => { - const client = mockClient(); - const client2 = mockClient(); - client.shadowed = true; - const entity = client.pony; - entity.region = createServerRegion(0, 0); - entity.region.clients.push(client, client2); + it('sends message only to client if muted or shadowed', () => { + const client = mockClient(); + const client2 = mockClient(); + client.shadowed = true; + const entity = client.pony; + entity.region = createServerRegion(0, 0); + entity.region.clients.push(client, client2); - sayToEveryone(client, 'test', 'test', MessageType.Chat, {}); + sayToEveryone(client, 'test', 'test', MessageType.Chat, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); - expect(client2.saysQueue).eql([]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + expect(client2.saysQueue).eql([]); + }); - it('does nothing if message is empty', () => { - const client = mockClient(); + it('does nothing if message is empty', () => { + const client = mockClient(); - sayToEveryone(client, '', '', MessageType.Chat, {}); + sayToEveryone(client, '', '', MessageType.Chat, {}); - expect(client.saysQueue).eql([]); - }); - }); + expect(client.saysQueue).eql([]); + }); + }); - describe('sayToOthers()', () => { - it('sends message to party if party message', () => { - const client = mockClient(); - client.party = { id: '', leader: client, clients: [client], pending: [] }; + describe('sayToOthers()', () => { + it('sends message to party if party message', () => { + const client = mockClient(); + client.party = { id: '', leader: client, clients: [client], pending: [] }; - sayToOthers(client, 'test', MessageType.Party, undefined, {}); + sayToOthers(client, 'test', MessageType.Party, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Party]]); + }); - it('sends message to everyone if not party message', () => { - const client = mockClient(); - client.pony.region = createServerRegion(0, 0); - client.pony.region.clients.push(client); + it('sends message to everyone if not party message', () => { + const client = mockClient(); + client.pony.region = createServerRegion(0, 0); + client.pony.region.clients.push(client); - sayToOthers(client, 'test', MessageType.Chat, undefined, {}); + sayToOthers(client, 'test', MessageType.Chat, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); - }); - }); + expect(client.saysQueue).eql([[client.pony.id, 'test', MessageType.Chat]]); + }); + }); }); diff --git a/src/ts/tests/server/cmUtils.spec.ts b/src/ts/tests/server/cmUtils.spec.ts index c9ca783..3eee79e 100644 --- a/src/ts/tests/server/cmUtils.spec.ts +++ b/src/ts/tests/server/cmUtils.spec.ts @@ -7,444 +7,444 @@ import { createCanvas } from '../../server/canvasUtilsNode'; import { pathTo } from '../../server/paths'; const positives: { [key: string]: [string, string[]]; } = { - blackOnTransparent: ['ff0000', [ - '000000', ' ', '000000', '000000', '000000', - '000000', ' ', '000000', ' ', ' ', - '000000', '000000', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', '000000', - '000000', '000000', '000000', ' ', '000000', - ]], - shortArm: ['ff0000', [ - ' ', ' ', '000000', '000000', '000000', - '000000', ' ', '000000', ' ', ' ', - '000000', '000000', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', '000000', - '000000', '000000', '000000', ' ', '000000', - ]], - noCoat: ['', [ - '000000', ' ', '000000', '000000', '000000', - '000000', ' ', '000000', ' ', ' ', - '000000', '000000', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', '000000', - '000000', '000000', '000000', ' ', '000000', - ]], - blackOnWhite: ['ff0000', [ - '000000', 'ffffff', '000000', '000000', '000000', - '000000', 'ffffff', '000000', 'ffffff', 'ffffff', - '000000', '000000', '000000', '000000', '000000', - 'ffffff', 'ffffff', '000000', 'ffffff', '000000', - '000000', '000000', '000000', 'ffffff', '000000', - ]], - fadingEnds: ['ff0000', [ - ' ', ' ', 'ffa500', 'ffa500', ' ', - 'ffa500', ' ', 'ffa500', ' ', ' ', - 'ffa500', 'ffdb99', 'fff2db', 'ffdb99', 'ffa500', - ' ', ' ', 'ffdb99', ' ', 'ffa500', - ' ', 'ffa500', 'ffa500', - ]], - shortLong: ['000000', [ - ' ', ' ', 'ffffff', 'ffffff', 'ffffff', - 'ffffff', ' ', 'ffffff', ' ', ' ', - 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', - ' ', ' ', 'ffffff', ' ', 'ffffff', - 'ffffff', 'ffffff', 'ffffff', - ]], - longShort: ['000000', [ - 'ffffff', ' ', 'ffffff', 'ffffff', ' ', - 'ffffff', ' ', 'ffffff', ' ', ' ', - 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', - ' ', ' ', 'ffffff', ' ', 'ffffff', - ' ', 'ffffff', 'ffffff', ' ', 'ffffff', - ]], - fadingEndsOnRed: ['ff0000', [ - 'ff0000', 'ff0000', 'ffa500', 'ffa500', 'ff0000', - 'ffa500', 'ff0000', 'ffa500', 'ff0000', 'ff0000', - 'ffa500', 'ffdb99', 'fff2db', 'ffdb99', 'ffa500', - 'ff0000', 'ff0000', 'ffdb99', 'ff0000', 'ffa500', - 'ff0000', 'ffa500', 'ffa500', 'ff0000', 'ff0000', - ]], - fadedRedRect: ['ff0000', [ - '443737', 'e6cdcd', '0c0b0b', '443737', '443737', - '443737', ' ', '773e3e', ' ', 'e6cdcd', - '0c0b0b', '773e3e', '443737', '773e3e', '0c0b0b', - 'e6cdcd', ' ', '773e3e', ' ', '443737', - '443737', '443737', '0c0b0b', 'e6cdcd', '443737', - ]], - fadedRedRectOnWhite: ['ff0000', [ - '443737', 'e6cdcd', '0c0b0b', '443737', '443737', - '443737', 'ffffff', '773e3e', 'ffffff', 'e6cdcd', - '0c0b0b', '773e3e', '443737', '773e3e', '0c0b0b', - 'e6cdcd', 'ffffff', '773e3e', 'ffffff', '443737', - '443737', '443737', '0c0b0b', 'e6cdcd', '443737', - ]], - grayOnGray: ['999595', [ - '403d39', '999595', '403d39', '403d39', '403d39', - '403d39', '999595', '403d39', '999595', '999595', - '403d39', '403d39', '403d39', '403d39', '403d39', - '999595', '999595', '403d39', '999595', '403d39', - '403d39', '403d39', '403d39', '999595', '403d39', - ]], - whiteWithCornersAndEnds: ['000000', [ - 'a5a5a5', '443434', 'a07575', 'ffffff', 'a5a5a5', - 'ffffff', '3d0000', 'ffffff', '3d0000', '443434', - 'a07575', 'ffffff', 'ffffff', 'ffffff', 'a07575', - '443434', '3d0000', 'ffffff', '3d0000', 'ffffff', - 'a5a5a5', 'ffffff', 'a07575', '443434', 'a5a5a5', - ]], - aryanne: ['ebebeb', [ - '000000', '000000', '000000', ' ', '000000', - ' ', 'ffb5b5', '000000', 'ffb5b5', '000000', - '000000', '000000', '000000', '000000', '000000', - '000000', 'ffb5b5', '000000', 'ffb5b5', ' ', - '000000', ' ', '000000', '000000', '000000', - ]], - blackWithRedGlowOnPink: ['f9c6ff', [ - '6f2b2b', 'ff9d9d', '52004e', '52004e', '6f2b2b', - '52004e', 'ff6363', '000000', 'ff6363', 'ff9d9d', - '52004e', '000000', '000000', '000000', '52004e', - 'ff9d9d', 'ff6363', '000000', 'ff6363', '52004e', - '6f2b2b', '52004e', '52004e', 'ff9d9d', '6f2b2b', - ]], - rainbod: ['000000', [ - 'b62709', ' ', 'b62709', 'b62709', 'b62709', - 'ee7b11', ' ', 'ee7b11', ' ', ' ', - 'eeb711', 'eeb711', 'eeb711', 'eeb711', 'eeb711', - ' ', ' ', '178c0d', ' ', '178c0d', - '4296df', '4296df', '4296df', ' ', '4296df', - ]], - asymetric: ['aa957e', [ - '000000', '000000', '000000', ' ', ' ', - ' ', ' ', '000000', ' ', '000000', - '000000', '000000', '000000', '000000', '000000', - '000000', ' ', '000000', ' ', ' ', - ' ', ' ', '000000', '000000', '000000', - ]], - oneLongArm: ['ffffff', [ - ' ', ' ', '000000', '000000', ' ', - '000000', ' ', '000000', ' ', ' ', - '000000', '000000', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', '000000', - ' ', '000000', '000000', ' ', '000000', - ]], - noCorners: ['ffffff', [ - '000000', '000000', ' ', ' ', '000000', - ' ', ' ', '000000', ' ', '000000', - ' ', '000000', '000000', '000000', ' ', - '000000', ' ', '000000', ' ', ' ', - '000000', ' ', ' ', '000000', '000000', - ]], - noCornersReverse: ['ffffff', [ - '000000', ' ', ' ', '000000', '000000', - '000000', ' ', '000000', ' ', ' ', - ' ', '000000', '000000', '000000', ' ', - ' ', ' ', '000000', ' ', '000000', - '000000', '000000', ' ', ' ', '000000', - ]], - faded: ['6d8064', [ - '363636', '6e4b4b', '363636', '363636', '363636', - '363636', '8f4e4e', '363636', '8f4e4e', '6e4b4b', - '363636', '363636', '363636', '363636', '363636', - '6e4b4b', '8f4e4e', '363636', '8f4e4e', '363636', - '363636', '363636', '363636', '6e4b4b', '363636', - ]], - missingBit: ['5d617c', [ - ' ', '464d6e', '464d6e', ' ', ' ', - ' ', ' ', '464d6e', ' ', '464d6e', - '464d6e', '464d6e', '464d6e', '464d6e', '464d6e', - '464d6e', ' ', '464d6e', ' ', ' ', - ' ', ' ', '464d6e', '464d6e', - ]], - smallTopRight: ['', [ - ' ', '160404', '160404', ' ', '160404', - ' ', ' ', '160404', '160404', '160404', - ' ', '160404', '160404', '160404', ' ', - ' ', '160404', ' ', '160404', '160404', - ' ', ' ', ' ', ' ', ' ', - ]], - smallTopLeft: ['', [ - '160404', '160404', ' ', '160404', ' ', - ' ', '160404', '160404', '160404', ' ', - '160404', '160404', '160404', ' ', ' ', - '160404', ' ', '160404', '160404', ' ', - ' ', ' ', ' ', ' ', ' ', - ]], - smallBottomRight: ['', [ - ' ', ' ', ' ', ' ', ' ', - ' ', '160404', '160404', ' ', '160404', - ' ', ' ', '160404', '160404', '160404', - ' ', '160404', '160404', '160404', ' ', - ' ', '160404', ' ', '160404', '160404', - ]], - smallBottomLeft: ['', [ - ' ', ' ', ' ', ' ', ' ', - '160404', '160404', ' ', '160404', ' ', - ' ', '160404', '160404', '160404', ' ', - '160404', '160404', '160404', ' ', ' ', - '160404', ' ', '160404', '160404', ' ', - ]], - smallTopRightRev: ['', [ - ' ', '160404', ' ', '160404', '160404', - ' ', '160404', '160404', '160404', ' ', - ' ', ' ', '160404', '160404', '160404', - ' ', '160404', '160404', ' ', '160404', - ' ', ' ', ' ', ' ', ' ', - ]], - weirdShape: ['', [ - 'fffffe', '000000', '000000', 'fffffe', '000000', - 'fffffe', 'fff9ee', '000000', 'fff9ee', '000000', - 'fffbf4', '000000', '000000', '000000', '000000', - 'fff9ee', '000000', 'fffffe', '000000', 'fff9ee', - 'fff9ee', '000000', 'fffffe', '000000', '000000', - ]], - wonky: ['ffffff', [ - '000000', ' ', '000000', '000000', ' ', - '000000', ' ', '000000', ' ', ' ', - '000000', '000000', '000000', '000000', '000000', - ' ', '000000', ' ', ' ', '000000', - '000000', '000000', ' ', ' ', '000000', - ]], - wonky2: ['ffffff', [ - '000000', ' ', ' ', '000000', '000000', - '000000', ' ', ' ', '000000', ' ', - '000000', '000000', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', '000000', - ' ', '000000', '000000', ' ', '000000', - ]], - wonky3: ['ffffff', [ - ' ', ' ', 'ff0000', 'ff0000', 'ff0000', - 'ff0000', ' ', 'e70000', ' ', ' ', - ' ', 'ff0000', 'e70000', 'ff0000', ' ', - ' ', ' ', 'e70000', ' ', 'ff0000', - 'ff0000', 'ff0000', ' ', ' ', 'ff0000', - ]], - wonky4: ['ffffff', [ - 'ff0000', ' ', 'ff0000', 'ff0000', 'ff0000', - 'ff0000', ' ', 'e70000', ' ', ' ', - ' ', 'ff0000', 'e70000', 'ff0000', ' ', - ' ', ' ', 'e70000', ' ', 'ff0000', - 'ff0000', 'ff0000', ' ', ' ', ' ', - ]], - wonky5: ['ffffff', [ - 'ff0000', ' ', ' ', 'ff0000', ' ', - 'ff0000', ' ', 'e70000', ' ', ' ', - ' ', 'ff0000', 'e70000', 'ff0000', ' ', - ' ', ' ', 'e70000', ' ', 'ff0000', - 'ff0000', 'ff0000', ' ', ' ', 'ff0000', - ]], - wonky6: ['ffffff', [ - 'ff0000', ' ', ' ', 'ff0000', 'ff0000', - 'ff0000', ' ', 'e70000', ' ', ' ', - ' ', 'ff0000', 'e70000', 'ff0000', ' ', - ' ', ' ', 'e70000', ' ', 'ff0000', - ' ', 'ff0000', ' ', ' ', 'ff0000', - ]], - wonky7: ['000000', [ - 'ffffff', ' ', 'ffffff', 'ffffff', 'ffffff', - 'ffffff', ' ', 'ffffff', ' ', ' ', - 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', - ' ', ' ', 'ffffff', 'ffffff', 'ffffff', - 'ffffff', 'ffffff', 'ffffff', ' ', 'ffffff', - ]], - wonky8: ['000000', [ - 'ffffff', ' ', 'ffffff', 'ffffff', 'ffffff', - 'ffffff', ' ', 'ffffff', ' ', ' ', - 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', - ' ', 'ffffff', 'ffffff', ' ', 'ffffff', - 'ffffff', 'ffffff', 'ffffff', ' ', 'ffffff', - ]], - wonky9: ['ffffff', [ - '000000', '000000', '000000', 'ffffff', '000000', - 'ffffff', 'ffffff', '160b0b', 'ffffff', '000000', - '000000', '000000', '000000', '000000', 'ffffff', - '000000', 'ffffff', '000000', 'ffffff', 'ffffff', - 'ffffff', 'ffffff', '000000', '000000', '000000', - ]], - wonky10: ['ffffff', [ - '000000', ' ', '000000', '000000', '000000', - '000000', ' ', '000000', ' ', ' ', - ' ', '000000', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', '000000', - '000000', '000000', '000000', ' ', '000000', - ]], - // layered: ['f0dcff', [ - // '080808', 'd9f4c8', '080808', '6d6d6d', '080808', - // '6d6d6d', 'd9f4c8', '080808', 'd9f4c8', 'd9f4c8', - // '080808', '080808', '6d6d6d', '080808', '080808', - // 'd9f4c8', 'd9f4c8', '080808', 'd9f4c8', '6d6d6d', - // '080808', '6d6d6d', '080808', 'd9f4c8', '080808', - // ]], - // layered2: ['d1b078', [ - // '000000', 'b8ffce', '000000', '065e21', '000000', - // '065e21', 'b8ffce', '065e21', 'b8ffce', 'b8ffce', - // '000000', '065e21', '065e21', '065e21', '000000', - // 'b8ffce', 'b8ffce', '065e21', 'b8ffce', '065e21', - // '000000', '065e21', '000000', 'b8ffce', '000000', - // ]], - // redOrange: ['fed395', [ - // 'ff0000', 'ffa859', 'ff0000', 'ff0000', 'ff0000', - // 'ff0000', 'ffa859', 'ff0000', 'ffa859', 'ffa859', - // 'ff0000', 'ff0000', 'ff0000', 'ff0000', 'ff0000', - // 'ffa859', 'ffa859', 'ff0000', 'ffa859', 'ff0000', - // 'ff0000', 'ff0000', 'ff0000', 'ffa859', 'ff0000', - // ]], - grayOnGray2: ['808080', [ - '808080', ' ', '808080', '808080', '808080', - '808080', ' ', '808080', ' ', ' ', - '808080', '808080', '808080', '808080', '808080', - ' ', ' ', '808080', ' ', '808080', - '808080', '808080', '808080', ' ', '808080', - ]], - missingArm1: ['ffffff', [ - ' ', ' ', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', ' ', - '000000', '000000', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', '000000', - '000000', '000000', '000000', ' ', '000000', - ]], - missingArm2: ['ffffff', [ - '000000', ' ', '000000', '000000', '000000', - '000000', ' ', '000000', ' ', ' ', - '000000', '000000', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', ' ', - '000000', '000000', '000000', ' ', ' ', - ]], - missingArm3: ['ffffff', [ - '000000', ' ', '000000', '000000', '000000', - '000000', ' ', '000000', ' ', ' ', - '000000', '000000', '000000', '000000', '000000', - ' ', ' ', '000000', ' ', '000000', - ' ', ' ', '000000', ' ', '000000', - ]], + blackOnTransparent: ['ff0000', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + shortArm: ['ff0000', [ + ' ', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + noCoat: ['', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + blackOnWhite: ['ff0000', [ + '000000', 'ffffff', '000000', '000000', '000000', + '000000', 'ffffff', '000000', 'ffffff', 'ffffff', + '000000', '000000', '000000', '000000', '000000', + 'ffffff', 'ffffff', '000000', 'ffffff', '000000', + '000000', '000000', '000000', 'ffffff', '000000', + ]], + fadingEnds: ['ff0000', [ + ' ', ' ', 'ffa500', 'ffa500', ' ', + 'ffa500', ' ', 'ffa500', ' ', ' ', + 'ffa500', 'ffdb99', 'fff2db', 'ffdb99', 'ffa500', + ' ', ' ', 'ffdb99', ' ', 'ffa500', + ' ', 'ffa500', 'ffa500', + ]], + shortLong: ['000000', [ + ' ', ' ', 'ffffff', 'ffffff', 'ffffff', + 'ffffff', ' ', 'ffffff', ' ', ' ', + 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', + ' ', ' ', 'ffffff', ' ', 'ffffff', + 'ffffff', 'ffffff', 'ffffff', + ]], + longShort: ['000000', [ + 'ffffff', ' ', 'ffffff', 'ffffff', ' ', + 'ffffff', ' ', 'ffffff', ' ', ' ', + 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', + ' ', ' ', 'ffffff', ' ', 'ffffff', + ' ', 'ffffff', 'ffffff', ' ', 'ffffff', + ]], + fadingEndsOnRed: ['ff0000', [ + 'ff0000', 'ff0000', 'ffa500', 'ffa500', 'ff0000', + 'ffa500', 'ff0000', 'ffa500', 'ff0000', 'ff0000', + 'ffa500', 'ffdb99', 'fff2db', 'ffdb99', 'ffa500', + 'ff0000', 'ff0000', 'ffdb99', 'ff0000', 'ffa500', + 'ff0000', 'ffa500', 'ffa500', 'ff0000', 'ff0000', + ]], + fadedRedRect: ['ff0000', [ + '443737', 'e6cdcd', '0c0b0b', '443737', '443737', + '443737', ' ', '773e3e', ' ', 'e6cdcd', + '0c0b0b', '773e3e', '443737', '773e3e', '0c0b0b', + 'e6cdcd', ' ', '773e3e', ' ', '443737', + '443737', '443737', '0c0b0b', 'e6cdcd', '443737', + ]], + fadedRedRectOnWhite: ['ff0000', [ + '443737', 'e6cdcd', '0c0b0b', '443737', '443737', + '443737', 'ffffff', '773e3e', 'ffffff', 'e6cdcd', + '0c0b0b', '773e3e', '443737', '773e3e', '0c0b0b', + 'e6cdcd', 'ffffff', '773e3e', 'ffffff', '443737', + '443737', '443737', '0c0b0b', 'e6cdcd', '443737', + ]], + grayOnGray: ['999595', [ + '403d39', '999595', '403d39', '403d39', '403d39', + '403d39', '999595', '403d39', '999595', '999595', + '403d39', '403d39', '403d39', '403d39', '403d39', + '999595', '999595', '403d39', '999595', '403d39', + '403d39', '403d39', '403d39', '999595', '403d39', + ]], + whiteWithCornersAndEnds: ['000000', [ + 'a5a5a5', '443434', 'a07575', 'ffffff', 'a5a5a5', + 'ffffff', '3d0000', 'ffffff', '3d0000', '443434', + 'a07575', 'ffffff', 'ffffff', 'ffffff', 'a07575', + '443434', '3d0000', 'ffffff', '3d0000', 'ffffff', + 'a5a5a5', 'ffffff', 'a07575', '443434', 'a5a5a5', + ]], + aryanne: ['ebebeb', [ + '000000', '000000', '000000', ' ', '000000', + ' ', 'ffb5b5', '000000', 'ffb5b5', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', 'ffb5b5', '000000', 'ffb5b5', ' ', + '000000', ' ', '000000', '000000', '000000', + ]], + blackWithRedGlowOnPink: ['f9c6ff', [ + '6f2b2b', 'ff9d9d', '52004e', '52004e', '6f2b2b', + '52004e', 'ff6363', '000000', 'ff6363', 'ff9d9d', + '52004e', '000000', '000000', '000000', '52004e', + 'ff9d9d', 'ff6363', '000000', 'ff6363', '52004e', + '6f2b2b', '52004e', '52004e', 'ff9d9d', '6f2b2b', + ]], + rainbod: ['000000', [ + 'b62709', ' ', 'b62709', 'b62709', 'b62709', + 'ee7b11', ' ', 'ee7b11', ' ', ' ', + 'eeb711', 'eeb711', 'eeb711', 'eeb711', 'eeb711', + ' ', ' ', '178c0d', ' ', '178c0d', + '4296df', '4296df', '4296df', ' ', '4296df', + ]], + asymetric: ['aa957e', [ + '000000', '000000', '000000', ' ', ' ', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + ' ', ' ', '000000', '000000', '000000', + ]], + oneLongArm: ['ffffff', [ + ' ', ' ', '000000', '000000', ' ', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + ' ', '000000', '000000', ' ', '000000', + ]], + noCorners: ['ffffff', [ + '000000', '000000', ' ', ' ', '000000', + ' ', ' ', '000000', ' ', '000000', + ' ', '000000', '000000', '000000', ' ', + '000000', ' ', '000000', ' ', ' ', + '000000', ' ', ' ', '000000', '000000', + ]], + noCornersReverse: ['ffffff', [ + '000000', ' ', ' ', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + ' ', '000000', '000000', '000000', ' ', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', ' ', ' ', '000000', + ]], + faded: ['6d8064', [ + '363636', '6e4b4b', '363636', '363636', '363636', + '363636', '8f4e4e', '363636', '8f4e4e', '6e4b4b', + '363636', '363636', '363636', '363636', '363636', + '6e4b4b', '8f4e4e', '363636', '8f4e4e', '363636', + '363636', '363636', '363636', '6e4b4b', '363636', + ]], + missingBit: ['5d617c', [ + ' ', '464d6e', '464d6e', ' ', ' ', + ' ', ' ', '464d6e', ' ', '464d6e', + '464d6e', '464d6e', '464d6e', '464d6e', '464d6e', + '464d6e', ' ', '464d6e', ' ', ' ', + ' ', ' ', '464d6e', '464d6e', + ]], + smallTopRight: ['', [ + ' ', '160404', '160404', ' ', '160404', + ' ', ' ', '160404', '160404', '160404', + ' ', '160404', '160404', '160404', ' ', + ' ', '160404', ' ', '160404', '160404', + ' ', ' ', ' ', ' ', ' ', + ]], + smallTopLeft: ['', [ + '160404', '160404', ' ', '160404', ' ', + ' ', '160404', '160404', '160404', ' ', + '160404', '160404', '160404', ' ', ' ', + '160404', ' ', '160404', '160404', ' ', + ' ', ' ', ' ', ' ', ' ', + ]], + smallBottomRight: ['', [ + ' ', ' ', ' ', ' ', ' ', + ' ', '160404', '160404', ' ', '160404', + ' ', ' ', '160404', '160404', '160404', + ' ', '160404', '160404', '160404', ' ', + ' ', '160404', ' ', '160404', '160404', + ]], + smallBottomLeft: ['', [ + ' ', ' ', ' ', ' ', ' ', + '160404', '160404', ' ', '160404', ' ', + ' ', '160404', '160404', '160404', ' ', + '160404', '160404', '160404', ' ', ' ', + '160404', ' ', '160404', '160404', ' ', + ]], + smallTopRightRev: ['', [ + ' ', '160404', ' ', '160404', '160404', + ' ', '160404', '160404', '160404', ' ', + ' ', ' ', '160404', '160404', '160404', + ' ', '160404', '160404', ' ', '160404', + ' ', ' ', ' ', ' ', ' ', + ]], + weirdShape: ['', [ + 'fffffe', '000000', '000000', 'fffffe', '000000', + 'fffffe', 'fff9ee', '000000', 'fff9ee', '000000', + 'fffbf4', '000000', '000000', '000000', '000000', + 'fff9ee', '000000', 'fffffe', '000000', 'fff9ee', + 'fff9ee', '000000', 'fffffe', '000000', '000000', + ]], + wonky: ['ffffff', [ + '000000', ' ', '000000', '000000', ' ', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', '000000', ' ', ' ', '000000', + '000000', '000000', ' ', ' ', '000000', + ]], + wonky2: ['ffffff', [ + '000000', ' ', ' ', '000000', '000000', + '000000', ' ', ' ', '000000', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + ' ', '000000', '000000', ' ', '000000', + ]], + wonky3: ['ffffff', [ + ' ', ' ', 'ff0000', 'ff0000', 'ff0000', + 'ff0000', ' ', 'e70000', ' ', ' ', + ' ', 'ff0000', 'e70000', 'ff0000', ' ', + ' ', ' ', 'e70000', ' ', 'ff0000', + 'ff0000', 'ff0000', ' ', ' ', 'ff0000', + ]], + wonky4: ['ffffff', [ + 'ff0000', ' ', 'ff0000', 'ff0000', 'ff0000', + 'ff0000', ' ', 'e70000', ' ', ' ', + ' ', 'ff0000', 'e70000', 'ff0000', ' ', + ' ', ' ', 'e70000', ' ', 'ff0000', + 'ff0000', 'ff0000', ' ', ' ', ' ', + ]], + wonky5: ['ffffff', [ + 'ff0000', ' ', ' ', 'ff0000', ' ', + 'ff0000', ' ', 'e70000', ' ', ' ', + ' ', 'ff0000', 'e70000', 'ff0000', ' ', + ' ', ' ', 'e70000', ' ', 'ff0000', + 'ff0000', 'ff0000', ' ', ' ', 'ff0000', + ]], + wonky6: ['ffffff', [ + 'ff0000', ' ', ' ', 'ff0000', 'ff0000', + 'ff0000', ' ', 'e70000', ' ', ' ', + ' ', 'ff0000', 'e70000', 'ff0000', ' ', + ' ', ' ', 'e70000', ' ', 'ff0000', + ' ', 'ff0000', ' ', ' ', 'ff0000', + ]], + wonky7: ['000000', [ + 'ffffff', ' ', 'ffffff', 'ffffff', 'ffffff', + 'ffffff', ' ', 'ffffff', ' ', ' ', + 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', + ' ', ' ', 'ffffff', 'ffffff', 'ffffff', + 'ffffff', 'ffffff', 'ffffff', ' ', 'ffffff', + ]], + wonky8: ['000000', [ + 'ffffff', ' ', 'ffffff', 'ffffff', 'ffffff', + 'ffffff', ' ', 'ffffff', ' ', ' ', + 'ffffff', 'ffffff', 'ffffff', 'ffffff', 'ffffff', + ' ', 'ffffff', 'ffffff', ' ', 'ffffff', + 'ffffff', 'ffffff', 'ffffff', ' ', 'ffffff', + ]], + wonky9: ['ffffff', [ + '000000', '000000', '000000', 'ffffff', '000000', + 'ffffff', 'ffffff', '160b0b', 'ffffff', '000000', + '000000', '000000', '000000', '000000', 'ffffff', + '000000', 'ffffff', '000000', 'ffffff', 'ffffff', + 'ffffff', 'ffffff', '000000', '000000', '000000', + ]], + wonky10: ['ffffff', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + ' ', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + // layered: ['f0dcff', [ + // '080808', 'd9f4c8', '080808', '6d6d6d', '080808', + // '6d6d6d', 'd9f4c8', '080808', 'd9f4c8', 'd9f4c8', + // '080808', '080808', '6d6d6d', '080808', '080808', + // 'd9f4c8', 'd9f4c8', '080808', 'd9f4c8', '6d6d6d', + // '080808', '6d6d6d', '080808', 'd9f4c8', '080808', + // ]], + // layered2: ['d1b078', [ + // '000000', 'b8ffce', '000000', '065e21', '000000', + // '065e21', 'b8ffce', '065e21', 'b8ffce', 'b8ffce', + // '000000', '065e21', '065e21', '065e21', '000000', + // 'b8ffce', 'b8ffce', '065e21', 'b8ffce', '065e21', + // '000000', '065e21', '000000', 'b8ffce', '000000', + // ]], + // redOrange: ['fed395', [ + // 'ff0000', 'ffa859', 'ff0000', 'ff0000', 'ff0000', + // 'ff0000', 'ffa859', 'ff0000', 'ffa859', 'ffa859', + // 'ff0000', 'ff0000', 'ff0000', 'ff0000', 'ff0000', + // 'ffa859', 'ffa859', 'ff0000', 'ffa859', 'ff0000', + // 'ff0000', 'ff0000', 'ff0000', 'ffa859', 'ff0000', + // ]], + grayOnGray2: ['808080', [ + '808080', ' ', '808080', '808080', '808080', + '808080', ' ', '808080', ' ', ' ', + '808080', '808080', '808080', '808080', '808080', + ' ', ' ', '808080', ' ', '808080', + '808080', '808080', '808080', ' ', '808080', + ]], + missingArm1: ['ffffff', [ + ' ', ' ', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', ' ', '000000', + ]], + missingArm2: ['ffffff', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', ' ', ' ', + ]], + missingArm3: ['ffffff', [ + '000000', ' ', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', ' ', + '000000', '000000', '000000', '000000', '000000', + ' ', ' ', '000000', ' ', '000000', + ' ', ' ', '000000', ' ', '000000', + ]], }; const negatives: { [key: string]: [string, string[]]; } = { - empty: ['ff0000', []], - short: ['ff0000', ['ff0000', '00ff00']], - black: ['ff0000', [ - '000000', '000000', '000000', '000000', '000000', - '000000', '000000', '000000', '000000', '000000', - '000000', '000000', '000000', '000000', '000000', - '000000', '000000', '000000', '000000', '000000', - '000000', '000000', '000000', '000000', '000000', - ]], - random: ['ff0000', [ - '000000', ' ', ' ', '000000', '000000', - '000000', '000000', '000000', ' ', '000000', - '000000', ' ', ' ', '000000', '000000', - ' ', '000000', '000000', '000000', ' ', - '000000', ' ', '000000', '000000', '000000', - ]], - almostSwastika: ['ff0000', [ - '000000', '111111', '000000', '000000', '000000', - '000000', ' ', '000000', ' ', '111111', - '000000', '000000', '000000', '000000', '000000', - '111111', ' ', '000000', ' ', '000000', - '000000', '000000', '000000', '111111', '000000', - ]], - flower: ['d7d8ef', [ - ' ', 'eb7db2', ' ', 'df639f', ' ', - 'df639f', 'ad517d', ' ', 'ad517d', 'eb7db2', - ' ', ' ', 'e8b2cc', ' ', ' ', - 'eb7db2', '8f4569', ' ', '8f4569', 'ad517d', - ' ', 'ad517d', ' ', 'eb7db2', ' ', - ]], - // swirl: ['fee9f8', [ - // ' ', 'ab4842', ' ', ' ', ' ', - // ' ', 'ab4842', ' ', 'ab4842', 'ab4842', - // ' ', ' ', 'ab4842', ' ', ' ', - // 'ab4842', 'ab4842', ' ', 'ab4842', ' ', - // ' ', ' ', ' ', 'ab4842', ' ', - // ]], - floweryThing: ['a636df', [ - ' ', '00b6b9', '208be8', '00026c', ' ', - '00026c', ' ', '13824b', ' ', '00b6b9', - '3c077a', '13824b', ' ', 'bb83fe', '3c077a', - '00b6b9', ' ', 'bb83fe', ' ', '00026c', - ' ', '00026c', '208be8', '00b6b9', ' ', - ]], - hashTag: ['d5d4d4', [ - ' ', '173953', ' ', '1084dc', ' ', - '1084dc', '1084dc', '173953', '1084dc', '173953', - ' ', '173953', '0090fe', '173953', ' ', - '173953', '1084dc', '173953', '1084dc', '1084dc', - ' ', '1084dc', ' ', '173953', ' ', - ]], - flower2: ['ffab8b', [ - ' ', 'ff446a', 'ff6484', 'ff7a95', ' ', - 'ff7a95', 'ff446a', 'ff6484', 'ff446a', 'ff446a', - 'ff6484', 'ff6484', ' ', 'ff6484', 'ff6484', - 'ff446a', 'ff446a', 'ff6484', 'ff446a', 'ff7a95', - ' ', 'ff7a95', 'ff6484', 'ff446a', ' ', - ]], + empty: ['ff0000', []], + short: ['ff0000', ['ff0000', '00ff00']], + black: ['ff0000', [ + '000000', '000000', '000000', '000000', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', '000000', '000000', '000000', '000000', + '000000', '000000', '000000', '000000', '000000', + ]], + random: ['ff0000', [ + '000000', ' ', ' ', '000000', '000000', + '000000', '000000', '000000', ' ', '000000', + '000000', ' ', ' ', '000000', '000000', + ' ', '000000', '000000', '000000', ' ', + '000000', ' ', '000000', '000000', '000000', + ]], + almostSwastika: ['ff0000', [ + '000000', '111111', '000000', '000000', '000000', + '000000', ' ', '000000', ' ', '111111', + '000000', '000000', '000000', '000000', '000000', + '111111', ' ', '000000', ' ', '000000', + '000000', '000000', '000000', '111111', '000000', + ]], + flower: ['d7d8ef', [ + ' ', 'eb7db2', ' ', 'df639f', ' ', + 'df639f', 'ad517d', ' ', 'ad517d', 'eb7db2', + ' ', ' ', 'e8b2cc', ' ', ' ', + 'eb7db2', '8f4569', ' ', '8f4569', 'ad517d', + ' ', 'ad517d', ' ', 'eb7db2', ' ', + ]], + // swirl: ['fee9f8', [ + // ' ', 'ab4842', ' ', ' ', ' ', + // ' ', 'ab4842', ' ', 'ab4842', 'ab4842', + // ' ', ' ', 'ab4842', ' ', ' ', + // 'ab4842', 'ab4842', ' ', 'ab4842', ' ', + // ' ', ' ', ' ', 'ab4842', ' ', + // ]], + floweryThing: ['a636df', [ + ' ', '00b6b9', '208be8', '00026c', ' ', + '00026c', ' ', '13824b', ' ', '00b6b9', + '3c077a', '13824b', ' ', 'bb83fe', '3c077a', + '00b6b9', ' ', 'bb83fe', ' ', '00026c', + ' ', '00026c', '208be8', '00b6b9', ' ', + ]], + hashTag: ['d5d4d4', [ + ' ', '173953', ' ', '1084dc', ' ', + '1084dc', '1084dc', '173953', '1084dc', '173953', + ' ', '173953', '0090fe', '173953', ' ', + '173953', '1084dc', '173953', '1084dc', '1084dc', + ' ', '1084dc', ' ', '173953', ' ', + ]], + flower2: ['ffab8b', [ + ' ', 'ff446a', 'ff6484', 'ff7a95', ' ', + 'ff7a95', 'ff446a', 'ff6484', 'ff446a', 'ff446a', + 'ff6484', 'ff6484', ' ', 'ff6484', 'ff6484', + 'ff446a', 'ff446a', 'ff6484', 'ff446a', 'ff7a95', + ' ', 'ff7a95', 'ff6484', 'ff446a', ' ', + ]], }; function createCMCanvas(coat: string, cm: string[]) { - const canvas = createCanvas(7, 7); - const context = canvas.getContext('2d')!; - context.fillStyle = `#${coat}`; - context.fillRect(0, 0, canvas.width, canvas.height); + const canvas = createCanvas(7, 7); + const context = canvas.getContext('2d')!; + context.fillStyle = `#${coat}`; + context.fillRect(0, 0, canvas.width, canvas.height); - for (let y = 0, i = 0; y < 5; y++) { - for (let x = 0; x < 5; x++ , i++) { - if ((cm[i] || '').trim()) { - context.fillStyle = `#${cm[i]}`; - context.fillRect(x + 1, y + 1, 1, 1); - } - } - } + for (let y = 0, i = 0; y < 5; y++) { + for (let x = 0; x < 5; x++ , i++) { + if ((cm[i] || '').trim()) { + context.fillStyle = `#${cm[i]}`; + context.fillRect(x + 1, y + 1, 1, 1); + } + } + } - return canvas; + return canvas; } function check(name: string, [coat, cm]: [string, string[]], expected: boolean) { - const result = isBadCM(cm.map(x => x.trim()), coat); + const result = isBadCM(cm.map(x => x.trim()), coat); - if (!!result !== expected) { - const canvas = createCMCanvas(coat, cm); - fs.writeFileSync(pathTo('tools', 'temp', 'cms', `${name}-${expected ? 'bad' : 'good'}.png`), canvas.toBuffer()); - } + if (!!result !== expected) { + const canvas = createCMCanvas(coat, cm); + fs.writeFileSync(pathTo('tools', 'temp', 'cms', `${name}-${expected ? 'bad' : 'good'}.png`), canvas.toBuffer()); + } - expect(!!result).equal(expected, result); + expect(!!result).equal(expected, result); } function checkColor(a: string, b: string) { - return theSameColor(hexToLab(a), hexToLab(b)); + return theSameColor(hexToLab(a), hexToLab(b)); } describe('cmUtils', () => { - before(() => clearCompareResults('cms')); + before(() => clearCompareResults('cms')); - describe('isBadCM()', () => { - Object.keys(positives).forEach(key => { - it(`returns true for positive (${key})`, () => { - check(key, positives[key], true); - }); - }); + describe('isBadCM()', () => { + Object.keys(positives).forEach(key => { + it(`returns true for positive (${key})`, () => { + check(key, positives[key], true); + }); + }); - Object.keys(negatives).forEach(key => { - it(`returns false for negative (${key})`, () => { - check(key, negatives[key], false); - }); - }); - }); + Object.keys(negatives).forEach(key => { + it(`returns false for negative (${key})`, () => { + check(key, negatives[key], false); + }); + }); + }); - describe('checkColor()', () => { - it('returns false for 000000 and ffffff', () => { - expect(checkColor('000000', 'ffffff')).false; - }); + describe('checkColor()', () => { + it('returns false for 000000 and ffffff', () => { + expect(checkColor('000000', 'ffffff')).false; + }); - it('returns true for 000000 and 000000', () => { - expect(checkColor('000000', '000000')).true; - }); + it('returns true for 000000 and 000000', () => { + expect(checkColor('000000', '000000')).true; + }); - it('returns true for 000000 and 52004e', () => { - //console.log('000000', '52004e', getDeltaE00(hexToLab('000000'), hexToLab('52004e'))); - expect(checkColor('000000', '52004e')).true; - }); + it('returns true for 000000 and 52004e', () => { + //console.log('000000', '52004e', getDeltaE00(hexToLab('000000'), hexToLab('52004e'))); + expect(checkColor('000000', '52004e')).true; + }); - it('returns false for eb7db2 and d7d8ef', () => { - //console.log('eb7db2', 'd7d8ef', getDeltaE00(hexToLab('eb7db2'), hexToLab('d7d8ef'))); - expect(checkColor('eb7db2', 'd7d8ef')).false; - }); - }); + it('returns false for eb7db2 and d7d8ef', () => { + //console.log('eb7db2', 'd7d8ef', getDeltaE00(hexToLab('eb7db2'), hexToLab('d7d8ef'))); + expect(checkColor('eb7db2', 'd7d8ef')).false; + }); + }); }); diff --git a/src/ts/tests/server/commands.spec.ts b/src/ts/tests/server/commands.spec.ts index 73fd47a..47feb35 100644 --- a/src/ts/tests/server/commands.spec.ts +++ b/src/ts/tests/server/commands.spec.ts @@ -6,7 +6,7 @@ import { ChatType, MessageType, Action, ExpressionExtra, Eye, Muzzle } from '../ import { IClient } from '../../server/serverInterfaces'; import { UserError } from '../../server/userError'; import { - getChatPrefix, parseCommand, createRunCommand, CommandContext, Command, createCommands, RunCommand + getChatPrefix, parseCommand, createRunCommand, CommandContext, Command, createCommands, RunCommand } from '../../server/commands'; import { parseExpression, expression } from '../../common/expressionUtils'; import { encodeExpression } from '../../common/encoders/expressionEncoder'; @@ -14,602 +14,602 @@ import { createServerMap } from '../../server/serverMap'; import * as playerUtils from '../../server/playerUtils'; describe('commands', () => { - describe('parseCommand()', () => { - it('returns text and type for regular text', () => { - expect(parseCommand('hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Say }); - }); - - it('returns party chat type for /p command', () => { - expect(parseCommand('/p hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Party }); - }); - - it('returns supporter chat type for /ss command', () => { - expect(parseCommand('/ss hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter }); - }); - - it('returns supporter 1 chat type for /s1 command', () => { - expect(parseCommand('/s1 hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter1 }); - }); - - it('returns supporter 2 chat type for /s2 command', () => { - expect(parseCommand('/s2 hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter2 }); - }); - - it('returns supporter 3 chat type for /s3 command', () => { - expect(parseCommand('/s3 hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter3 }); - }); - - it('returns say chat type for /s command', () => { - expect(parseCommand('/s hello', ChatType.Party)).eql({ args: 'hello', type: ChatType.Say }); - }); - - it('returns think chat type for /t command', () => { - expect(parseCommand('/t hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Think }); - }); - - it('returns think chat type for /T command', () => { - expect(parseCommand('/T hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Think }); - }); - - it('returns party think chat type for /t command in party chat', () => { - expect(parseCommand('/t hello', ChatType.Party)).eql({ args: 'hello', type: ChatType.PartyThink }); - }); - - it('returns correct command', () => { - expect(parseCommand('/test hello', ChatType.Say)).eql({ command: 'test', args: 'hello', type: ChatType.Say }); - }); - - it('returns correct command with no arguments', () => { - expect(parseCommand('/test', ChatType.Say)).eql({ command: 'test', args: '', type: ChatType.Say }); - }); - - it('keeps the same chat type for commands', () => { - expect(parseCommand('/test', ChatType.Party)).eql({ command: 'test', args: '', type: ChatType.Party }); - }); - - it('trims args', () => { - expect(parseCommand('/test foo bar ', ChatType.Say)).eql({ command: 'test', args: 'foo bar', type: ChatType.Say }); - }); - - it('returns command name lowercased', () => { - expect(parseCommand('/Te&$St foo', ChatType.Say)).eql({ command: 'Te&$St', args: 'foo', type: ChatType.Say }); - }); - }); - - describe('getChatPrefix()', () => { - it('returns empty string for regular chat', () => { - expect(getChatPrefix(ChatType.Say)).equal(''); - }); - - it('returns "/p " for party chat', () => { - expect(getChatPrefix(ChatType.Party)).equal('/p '); - }); - - it('returns "/p " for party thinking', () => { - expect(getChatPrefix(ChatType.PartyThink)).equal('/p '); - }); - - it('returns "" for thinking', () => { - expect(getChatPrefix(ChatType.Think)).equal(''); - }); - - it('returns "/ss " for supporter chat', () => { - expect(getChatPrefix(ChatType.Supporter)).equal('/ss '); - }); - - it('returns "" for supporter 1 chat', () => { - expect(getChatPrefix(ChatType.Supporter1)).equal(''); - }); - - it('returns "" for supporter 2 chat', () => { - expect(getChatPrefix(ChatType.Supporter2)).equal(''); - }); - - it('returns "" for supporter 3 chat', () => { - expect(getChatPrefix(ChatType.Supporter3)).equal(''); - }); - }); - - describe('runCommand()', () => { - let client: IClient; - let context: CommandContext; - let command: Command; - let runCommand: RunCommand; - let handler: SinonStub; - - beforeEach(() => { - handler = stub(); - command = { names: ['test'], handler, help: '', role: '' }; - client = mockClient(); - context = { - liveSettings: {} as any, - world: { sayTo() { } } as any, - notifications: {} as any, - party: {} as any, - random: () => 0, - }; - runCommand = createRunCommand(context, [command]); - }); - - it('runs given command', () => { - runCommand(client, 'test', 'args', ChatType.Say, undefined, {}); - - assert.calledWith(handler, context, client, 'args', ChatType.Say); - }); - - it('returns true if run command', () => { - expect(runCommand(client, 'test', '', ChatType.Say, undefined, {})).true; - }); - - it('returns true if command does not exist', () => { - expect(runCommand(client, 'foo', '', ChatType.Say, undefined, {})).false; - }); - - it('should not run command if client is missing required role', () => { - command.role = 'admin'; - - expect(runCommand(client, 'test', '', ChatType.Say, undefined, {})).false; - }); - - it('should run command if client has required role', () => { - client.account.roles = ['admin']; - command.role = 'admin'; - - expect(runCommand(client, 'test', '', ChatType.Say, undefined, {})).true; - }); - - it('sends user error to user', () => { - handler.throws(new UserError('test error')); - - runCommand(client, 'test', '', ChatType.Say, undefined, {}); - - expect(client.saysQueue).eql([ - [client.pony.id, 'test error', MessageType.System], - ]); - }); - - it('rethrows non-user error', () => { - expect(() => { - handler.throws(new Error('test error')); - runCommand(client, 'test', '', ChatType.Say, undefined, {}); - }).throw('test error'); - }); - - it('should find correct command case-insensitive', () => { - expect(runCommand(client, 'TeSt', '', ChatType.Say, undefined, {})).true; - }); - }); - - describe('individual commands', () => { - let client: IClient; - let context: CommandContext; - let runCommand: RunCommand; - let execAction: SinonStub; - - beforeEach(() => { - execAction = stub(playerUtils, 'execAction'); - client = mockClient(); - client.map = createServerMap('', 0, 3, 3); - client.pony.region = client.map.regions[0]; - client.pony.region.clients.push(client); - client.account.roles = ['mod', 'admin']; - client.isMod = true; - context = { - liveSettings: {} as any, - world: { - featureFlags: { flying: true, swap: true, friends: true }, - getSettings: () => ({}), - action() { }, - unholdItem() { }, - sayTo() { }, - sayToOthers() { }, - sayToEveryone() { }, - setTime() { }, - resetToSpawn() { }, - kick() { }, - fixPosition() { }, - } as any, - notifications: {} as any, - party: {} as any, - random: () => 0, - }; - const commands = createCommands(context.world); - runCommand = createRunCommand(context, commands); - }); - - afterEach(() => { - execAction.restore(); - }); - - describe('/help', () => { - it('prints commands help', () => { - runCommand(client, 'help', '', ChatType.Say, undefined, {}); - - expect(client.saysQueue.length).equal(1); - expect(client.saysQueue[0][0]).equal(client.pony.id); - expect(client.saysQueue[0][2]).equal(MessageType.System); - }); - }); - - describe('/roll', () => { - it('rolls random number from 1 to 100 without args', () => { - stub(context, 'random').withArgs(1, 100).returns(12); - - runCommand(client, 'roll', '', ChatType.Say, undefined, {}); - - expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 12 of 100', MessageType.Announcement]]); - }); - - it('rolls random number from 1 to given number', () => { - stub(context, 'random').withArgs(1, 50).returns(12); - - runCommand(client, 'roll', '50', ChatType.Say, undefined, {}); - - expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 12 of 50', MessageType.Announcement]]); - }); - - it('rolls random number between given numbers', () => { - stub(context, 'random').withArgs(50, 200).returns(123); - - runCommand(client, 'roll', '50-200', ChatType.Say, undefined, {}); - - expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 123 of 50-200', MessageType.Announcement]]); - }); - - it('clamps minimum and maximum', () => { - stub(context, 'random').withArgs(1000000, 1000000).returns(1000000); - - runCommand(client, 'roll', '999999999-999999999', ChatType.Say, undefined, {}); - - expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 1000000 of 1000000-1000000', MessageType.Announcement]]); - }); - - it(`uses default behaviour if args don't match pattern`, () => { - stub(context, 'random').withArgs(1, 100).returns(50); - - runCommand(client, 'roll', 'foo bar', ChatType.Say, undefined, {}); - - expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 50 of 100', MessageType.Announcement]]); - }); - - it('rolls apple', () => { - runCommand(client, 'roll', '🍎', ChatType.Say, undefined, {}); - - expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 🍎 of 100', MessageType.Announcement]]); - }); - }); - - describe('/e', () => { - it('updates permanent expression', () => { - runCommand(client, 'e', '>:|', ChatType.Say, undefined, {}); - - expect(client.pony.exprPermanent).eql(parseExpression('>:|')); - }); + describe('parseCommand()', () => { + it('returns text and type for regular text', () => { + expect(parseCommand('hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Say }); + }); + + it('returns party chat type for /p command', () => { + expect(parseCommand('/p hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Party }); + }); + + it('returns supporter chat type for /ss command', () => { + expect(parseCommand('/ss hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter }); + }); + + it('returns supporter 1 chat type for /s1 command', () => { + expect(parseCommand('/s1 hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter1 }); + }); + + it('returns supporter 2 chat type for /s2 command', () => { + expect(parseCommand('/s2 hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter2 }); + }); + + it('returns supporter 3 chat type for /s3 command', () => { + expect(parseCommand('/s3 hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Supporter3 }); + }); + + it('returns say chat type for /s command', () => { + expect(parseCommand('/s hello', ChatType.Party)).eql({ args: 'hello', type: ChatType.Say }); + }); + + it('returns think chat type for /t command', () => { + expect(parseCommand('/t hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Think }); + }); + + it('returns think chat type for /T command', () => { + expect(parseCommand('/T hello', ChatType.Say)).eql({ args: 'hello', type: ChatType.Think }); + }); + + it('returns party think chat type for /t command in party chat', () => { + expect(parseCommand('/t hello', ChatType.Party)).eql({ args: 'hello', type: ChatType.PartyThink }); + }); + + it('returns correct command', () => { + expect(parseCommand('/test hello', ChatType.Say)).eql({ command: 'test', args: 'hello', type: ChatType.Say }); + }); + + it('returns correct command with no arguments', () => { + expect(parseCommand('/test', ChatType.Say)).eql({ command: 'test', args: '', type: ChatType.Say }); + }); + + it('keeps the same chat type for commands', () => { + expect(parseCommand('/test', ChatType.Party)).eql({ command: 'test', args: '', type: ChatType.Party }); + }); + + it('trims args', () => { + expect(parseCommand('/test foo bar ', ChatType.Say)).eql({ command: 'test', args: 'foo bar', type: ChatType.Say }); + }); + + it('returns command name lowercased', () => { + expect(parseCommand('/Te&$St foo', ChatType.Say)).eql({ command: 'Te&$St', args: 'foo', type: ChatType.Say }); + }); + }); + + describe('getChatPrefix()', () => { + it('returns empty string for regular chat', () => { + expect(getChatPrefix(ChatType.Say)).equal(''); + }); + + it('returns "/p " for party chat', () => { + expect(getChatPrefix(ChatType.Party)).equal('/p '); + }); + + it('returns "/p " for party thinking', () => { + expect(getChatPrefix(ChatType.PartyThink)).equal('/p '); + }); + + it('returns "" for thinking', () => { + expect(getChatPrefix(ChatType.Think)).equal(''); + }); + + it('returns "/ss " for supporter chat', () => { + expect(getChatPrefix(ChatType.Supporter)).equal('/ss '); + }); + + it('returns "" for supporter 1 chat', () => { + expect(getChatPrefix(ChatType.Supporter1)).equal(''); + }); + + it('returns "" for supporter 2 chat', () => { + expect(getChatPrefix(ChatType.Supporter2)).equal(''); + }); + + it('returns "" for supporter 3 chat', () => { + expect(getChatPrefix(ChatType.Supporter3)).equal(''); + }); + }); + + describe('runCommand()', () => { + let client: IClient; + let context: CommandContext; + let command: Command; + let runCommand: RunCommand; + let handler: SinonStub; + + beforeEach(() => { + handler = stub(); + command = { names: ['test'], handler, help: '', role: '' }; + client = mockClient(); + context = { + liveSettings: {} as any, + world: { sayTo() { } } as any, + notifications: {} as any, + party: {} as any, + random: () => 0, + }; + runCommand = createRunCommand(context, [command]); + }); + + it('runs given command', () => { + runCommand(client, 'test', 'args', ChatType.Say, undefined, {}); + + assert.calledWith(handler, context, client, 'args', ChatType.Say); + }); + + it('returns true if run command', () => { + expect(runCommand(client, 'test', '', ChatType.Say, undefined, {})).true; + }); + + it('returns true if command does not exist', () => { + expect(runCommand(client, 'foo', '', ChatType.Say, undefined, {})).false; + }); + + it('should not run command if client is missing required role', () => { + command.role = 'admin'; + + expect(runCommand(client, 'test', '', ChatType.Say, undefined, {})).false; + }); + + it('should run command if client has required role', () => { + client.account.roles = ['admin']; + command.role = 'admin'; + + expect(runCommand(client, 'test', '', ChatType.Say, undefined, {})).true; + }); + + it('sends user error to user', () => { + handler.throws(new UserError('test error')); + + runCommand(client, 'test', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([ + [client.pony.id, 'test error', MessageType.System], + ]); + }); + + it('rethrows non-user error', () => { + expect(() => { + handler.throws(new Error('test error')); + runCommand(client, 'test', '', ChatType.Say, undefined, {}); + }).throw('test error'); + }); + + it('should find correct command case-insensitive', () => { + expect(runCommand(client, 'TeSt', '', ChatType.Say, undefined, {})).true; + }); + }); + + describe('individual commands', () => { + let client: IClient; + let context: CommandContext; + let runCommand: RunCommand; + let execAction: SinonStub; + + beforeEach(() => { + execAction = stub(playerUtils, 'execAction'); + client = mockClient(); + client.map = createServerMap('', 0, 3, 3); + client.pony.region = client.map.regions[0]; + client.pony.region.clients.push(client); + client.account.roles = ['mod', 'admin']; + client.isMod = true; + context = { + liveSettings: {} as any, + world: { + featureFlags: { flying: true, swap: true, friends: true }, + getSettings: () => ({}), + action() { }, + unholdItem() { }, + sayTo() { }, + sayToOthers() { }, + sayToEveryone() { }, + setTime() { }, + resetToSpawn() { }, + kick() { }, + fixPosition() { }, + } as any, + notifications: {} as any, + party: {} as any, + random: () => 0, + }; + const commands = createCommands(context.world); + runCommand = createRunCommand(context, commands); + }); + + afterEach(() => { + execAction.restore(); + }); + + describe('/help', () => { + it('prints commands help', () => { + runCommand(client, 'help', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue.length).equal(1); + expect(client.saysQueue[0][0]).equal(client.pony.id); + expect(client.saysQueue[0][2]).equal(MessageType.System); + }); + }); + + describe('/roll', () => { + it('rolls random number from 1 to 100 without args', () => { + stub(context, 'random').withArgs(1, 100).returns(12); + + runCommand(client, 'roll', '', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 12 of 100', MessageType.Announcement]]); + }); + + it('rolls random number from 1 to given number', () => { + stub(context, 'random').withArgs(1, 50).returns(12); + + runCommand(client, 'roll', '50', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 12 of 50', MessageType.Announcement]]); + }); + + it('rolls random number between given numbers', () => { + stub(context, 'random').withArgs(50, 200).returns(123); + + runCommand(client, 'roll', '50-200', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 123 of 50-200', MessageType.Announcement]]); + }); + + it('clamps minimum and maximum', () => { + stub(context, 'random').withArgs(1000000, 1000000).returns(1000000); + + runCommand(client, 'roll', '999999999-999999999', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 1000000 of 1000000-1000000', MessageType.Announcement]]); + }); + + it(`uses default behaviour if args don't match pattern`, () => { + stub(context, 'random').withArgs(1, 100).returns(50); + + runCommand(client, 'roll', 'foo bar', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 50 of 100', MessageType.Announcement]]); + }); + + it('rolls apple', () => { + runCommand(client, 'roll', '🍎', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, '🎲 rolled 🍎 of 100', MessageType.Announcement]]); + }); + }); + + describe('/e', () => { + it('updates permanent expression', () => { + runCommand(client, 'e', '>:|', ChatType.Say, undefined, {}); + + expect(client.pony.exprPermanent).eql(parseExpression('>:|')); + }); - it('resets existing expression', () => { - runCommand(client, 'e', '>:|', ChatType.Say, undefined, {}); + it('resets existing expression', () => { + runCommand(client, 'e', '>:|', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression(parseExpression('>:|'))); - }); - }); + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression('>:|'))); + }); + }); - describe('/boop /)', () => { - it('invokes action', () => { - runCommand(client, 'boop', '', ChatType.Say, undefined, {}); + describe('/boop /)', () => { + it('invokes action', () => { + runCommand(client, 'boop', '', ChatType.Say, undefined, {}); - assert.calledWith(execAction, client, Action.Boop); - }); + assert.calledWith(execAction, client, Action.Boop); + }); - it('sets expression', () => { - runCommand(client, 'boop', '>:|', ChatType.Say, undefined, {}); + it('sets expression', () => { + runCommand(client, 'boop', '>:|', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression(parseExpression('>:|'))); - }); - }); + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression('>:|'))); + }); + }); - describe('/drop', () => { - it('invokes action', () => { - runCommand(client, 'drop', '', ChatType.Say, undefined, {}); + describe('/drop', () => { + it('invokes action', () => { + runCommand(client, 'drop', '', ChatType.Say, undefined, {}); - assert.calledWith(execAction, client, Action.Drop); - }); - }); + assert.calledWith(execAction, client, Action.Drop); + }); + }); - describe('/turn', () => { - it('invokes action', () => { - runCommand(client, 'turn', '', ChatType.Say, undefined, {}); + describe('/turn', () => { + it('invokes action', () => { + runCommand(client, 'turn', '', ChatType.Say, undefined, {}); - assert.calledWith(execAction, client, Action.TurnHead); - }); - }); + assert.calledWith(execAction, client, Action.TurnHead); + }); + }); - describe('/blush', () => { - it('sets expression with blush', () => { - runCommand(client, 'blush', '>:|', ChatType.Say, undefined, {}); + describe('/blush', () => { + it('sets expression with blush', () => { + runCommand(client, 'blush', '>:|', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression({ - ...parseExpression('>:|'), - extra: ExpressionExtra.Blush, - } as any)); - }); + expect(client.pony.options!.expr).equal(encodeExpression({ + ...parseExpression('>:|'), + extra: ExpressionExtra.Blush, + } as any)); + }); - it('uses current expression if available', () => { - client.pony.options = { expr: encodeExpression(parseExpression(':(')) }; + it('uses current expression if available', () => { + client.pony.options = { expr: encodeExpression(parseExpression(':(')) }; - runCommand(client, 'blush', '', ChatType.Say, undefined, {}); + runCommand(client, 'blush', '', ChatType.Say, undefined, {}); - expect(client.pony.options.expr).equal(encodeExpression({ - ...parseExpression(':('), - extra: ExpressionExtra.Blush, - } as any)); - }); + expect(client.pony.options.expr).equal(encodeExpression({ + ...parseExpression(':('), + extra: ExpressionExtra.Blush, + } as any)); + }); - it('uses default expression if not provided', () => { - runCommand(client, 'blush', '', ChatType.Say, undefined, {}); + it('uses default expression if not provided', () => { + runCommand(client, 'blush', '', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression({ - ...expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral), - extra: ExpressionExtra.Blush, - })); - }); + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral), + extra: ExpressionExtra.Blush, + })); + }); - it('passes cancellable flag', () => { - client.pony.exprCancellable = true; + it('passes cancellable flag', () => { + client.pony.exprCancellable = true; - runCommand(client, 'blush', '', ChatType.Say, undefined, {}); + runCommand(client, 'blush', '', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression({ - ...expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral), - extra: ExpressionExtra.Blush, - })); - }); - }); + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral), + extra: ExpressionExtra.Blush, + })); + }); + }); - describe('/gifts', () => { - it('announces collected gifts count', () => { - client.account.state = { gifts: 5 }; + describe('/gifts', () => { + it('announces collected gifts count', () => { + client.account.state = { gifts: 5 }; - runCommand(client, 'gifts', '', ChatType.Say, undefined, {}); + runCommand(client, 'gifts', '', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'collected 5 🎁', MessageType.Announcement]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'collected 5 🎁', MessageType.Announcement]]); + }); - it('announces 0 collected gifts if missing gifts entry', () => { - runCommand(client, 'gifts', '', ChatType.Say, undefined, {}); + it('announces 0 collected gifts if missing gifts entry', () => { + runCommand(client, 'gifts', '', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'collected 0 🎁', MessageType.Announcement]]); - }); - }); + expect(client.saysQueue).eql([[client.pony.id, 'collected 0 🎁', MessageType.Announcement]]); + }); + }); - describe('/candies', () => { - it('announces collected candies count', () => { - client.account.state = { candies: 5 }; + describe('/candies', () => { + it('announces collected candies count', () => { + client.account.state = { candies: 5 }; - runCommand(client, 'candies', '', ChatType.Say, undefined, {}); + runCommand(client, 'candies', '', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'collected 5 🍬', MessageType.Announcement]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'collected 5 🍬', MessageType.Announcement]]); + }); - it('announces 0 collected candies if missing candies entry', () => { - runCommand(client, 'candies', '', ChatType.Say, undefined, {}); + it('announces 0 collected candies if missing candies entry', () => { + runCommand(client, 'candies', '', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'collected 0 🍬', MessageType.Announcement]]); - }); - }); + expect(client.saysQueue).eql([[client.pony.id, 'collected 0 🍬', MessageType.Announcement]]); + }); + }); - describe('/clovers', () => { - it('announces collected clovers count', () => { - client.account.state = { clovers: 5 }; + describe('/clovers', () => { + it('announces collected clovers count', () => { + client.account.state = { clovers: 5 }; - runCommand(client, 'clovers', '', ChatType.Say, undefined, {}); + runCommand(client, 'clovers', '', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'collected 5 🍀', MessageType.Announcement]]); - }); + expect(client.saysQueue).eql([[client.pony.id, 'collected 5 🍀', MessageType.Announcement]]); + }); - it('announces 0 collected clovers if missing clovers entry', () => { - runCommand(client, 'clovers', '', ChatType.Say, undefined, {}); + it('announces 0 collected clovers if missing clovers entry', () => { + runCommand(client, 'clovers', '', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([[client.pony.id, 'collected 0 🍀', MessageType.Announcement]]); - }); - }); + expect(client.saysQueue).eql([[client.pony.id, 'collected 0 🍀', MessageType.Announcement]]); + }); + }); - describe('/unstuck', () => { - it('resets client to spawn', () => { - const resetToSpawn = stub(context.world, 'resetToSpawn'); + describe('/unstuck', () => { + it('resets client to spawn', () => { + const resetToSpawn = stub(context.world, 'resetToSpawn'); - runCommand(client, 'unstuck', '', ChatType.Say, undefined, {}); + runCommand(client, 'unstuck', '', ChatType.Say, undefined, {}); - assert.calledWith(resetToSpawn, client); - }); + assert.calledWith(resetToSpawn, client); + }); - it('kicks client', () => { - const kick = stub(context.world, 'kick'); + it('kicks client', () => { + const kick = stub(context.world, 'kick'); - runCommand(client, 'unstuck', '', ChatType.Say, undefined, {}); + runCommand(client, 'unstuck', '', ChatType.Say, undefined, {}); - assert.calledWith(kick, client); - }); - }); + assert.calledWith(kick, client); + }); + }); - describe('/sleep', () => { - it('sets expression with sleeping', () => { - runCommand(client, 'sleep', '>:|', ChatType.Say, undefined, {}); + describe('/sleep', () => { + it('sets expression with sleeping', () => { + runCommand(client, 'sleep', '>:|', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression({ - ...parseExpression('>:|'), - left: Eye.Closed, - right: Eye.Closed, - extra: ExpressionExtra.Zzz, - } as any)); - }); + expect(client.pony.options!.expr).equal(encodeExpression({ + ...parseExpression('>:|'), + left: Eye.Closed, + right: Eye.Closed, + extra: ExpressionExtra.Zzz, + } as any)); + }); - it('uses current expression if available', () => { - client.pony.options = { expr: encodeExpression(parseExpression(':(')) }; + it('uses current expression if available', () => { + client.pony.options = { expr: encodeExpression(parseExpression(':(')) }; - runCommand(client, 'sleep', '', ChatType.Say, undefined, {}); + runCommand(client, 'sleep', '', ChatType.Say, undefined, {}); - expect(client.pony.options.expr).equal(encodeExpression({ - ...parseExpression(':('), - left: Eye.Closed, - right: Eye.Closed, - extra: ExpressionExtra.Zzz, - } as any)); - }); + expect(client.pony.options.expr).equal(encodeExpression({ + ...parseExpression(':('), + left: Eye.Closed, + right: Eye.Closed, + extra: ExpressionExtra.Zzz, + } as any)); + }); - it('uses default expression if not provided', () => { - runCommand(client, 'sleep', '', ChatType.Say, undefined, {}); + it('uses default expression if not provided', () => { + runCommand(client, 'sleep', '', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression({ - ...expression(Eye.Closed, Eye.Closed, Muzzle.Neutral), - extra: ExpressionExtra.Zzz, - })); - }); + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Closed, Eye.Closed, Muzzle.Neutral), + extra: ExpressionExtra.Zzz, + })); + }); - it('closes mouth', () => { - runCommand(client, 'sleep', ':D', ChatType.Say, undefined, {}); + it('closes mouth', () => { + runCommand(client, 'sleep', ':D', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression({ - ...expression(Eye.Closed, Eye.Closed, Muzzle.Neutral), - extra: ExpressionExtra.Zzz, - })); - }); + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Closed, Eye.Closed, Muzzle.Neutral), + extra: ExpressionExtra.Zzz, + })); + }); - it('does nothing if moving', () => { - client.pony.vx = 1; - client.pony.options!.expr = 123; + it('does nothing if moving', () => { + client.pony.vx = 1; + client.pony.options!.expr = 123; - runCommand(client, 'sleep', '', ChatType.Say, undefined, {}); + runCommand(client, 'sleep', '', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(123); - }); - }); + expect(client.pony.options!.expr).equal(123); + }); + }); - describe('/cry', () => { - it('sets expression with tears', () => { - runCommand(client, 'cry', '>:|', ChatType.Say, undefined, {}); + describe('/cry', () => { + it('sets expression with tears', () => { + runCommand(client, 'cry', '>:|', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression({ - ...parseExpression('>:|'), - extra: ExpressionExtra.Cry, - } as any)); - }); + expect(client.pony.options!.expr).equal(encodeExpression({ + ...parseExpression('>:|'), + extra: ExpressionExtra.Cry, + } as any)); + }); - it('uses default expression if not provided', () => { - runCommand(client, 'cry', '', ChatType.Say, undefined, {}); + it('uses default expression if not provided', () => { + runCommand(client, 'cry', '', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression({ - ...expression(Eye.Sad, Eye.Sad, Muzzle.Frown), - extra: ExpressionExtra.Cry, - } as any)); - }); - }); + expect(client.pony.options!.expr).equal(encodeExpression({ + ...expression(Eye.Sad, Eye.Sad, Muzzle.Frown), + extra: ExpressionExtra.Cry, + } as any)); + }); + }); - describe('/smile', () => { - it('sets expression', () => { - runCommand(client, 'smile', '', ChatType.Say, undefined, {}); + describe('/smile', () => { + it('sets expression', () => { + runCommand(client, 'smile', '', ChatType.Say, undefined, {}); - expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); - }); - }); + expect(client.pony.options!.expr).equal(encodeExpression(parseExpression(':)'))); + }); + }); - describe('/yawn', () => { - it('invokes action', () => { - runCommand(client, 'yawn', '', ChatType.Say, undefined, {}); + describe('/yawn', () => { + it('invokes action', () => { + runCommand(client, 'yawn', '', ChatType.Say, undefined, {}); - assert.calledWith(execAction, client, Action.Yawn); - }); - }); - - describe('/m', () => { - it('send mod message', () => { - runCommand(client, 'm', 'message', ChatType.Say, undefined, {}); - - expect(client.saysQueue).eql([[client.pony.id, 'message', MessageType.Mod]]); - }); - }); - - describe('/a', () => { - it('send admin message', () => { - runCommand(client, 'a', 'message', ChatType.Say, undefined, {}); - - expect(client.saysQueue).eql([[client.pony.id, 'message', MessageType.Admin]]); - }); - }); - - describe('/time', () => { - it('sets world time', () => { - const setTime = stub(context.world, 'setTime'); - - runCommand(client, 'time', '12', ChatType.Say, undefined, {}); + assert.calledWith(execAction, client, Action.Yawn); + }); + }); + + describe('/m', () => { + it('send mod message', () => { + runCommand(client, 'm', 'message', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'message', MessageType.Mod]]); + }); + }); + + describe('/a', () => { + it('send admin message', () => { + runCommand(client, 'a', 'message', ChatType.Say, undefined, {}); + + expect(client.saysQueue).eql([[client.pony.id, 'message', MessageType.Admin]]); + }); + }); + + describe('/time', () => { + it('sets world time', () => { + const setTime = stub(context.world, 'setTime'); + + runCommand(client, 'time', '12', ChatType.Say, undefined, {}); - assert.calledWith(setTime, 12); - }); - - it('does modulo 24 on hour', () => { - const setTime = stub(context.world, 'setTime'); - - runCommand(client, 'time', '26', ChatType.Say, undefined, {}); - - assert.calledWith(setTime, 2); - }); - - it('prints error if args are invalid', () => { - runCommand(client, 'time', 'foo', ChatType.Say, undefined, {}); + assert.calledWith(setTime, 12); + }); + + it('does modulo 24 on hour', () => { + const setTime = stub(context.world, 'setTime'); + + runCommand(client, 'time', '26', ChatType.Say, undefined, {}); + + assert.calledWith(setTime, 2); + }); + + it('prints error if args are invalid', () => { + runCommand(client, 'time', 'foo', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([ - [client.pony.id, 'invalid parameter', MessageType.System], - ]); - }); - }); + expect(client.saysQueue).eql([ + [client.pony.id, 'invalid parameter', MessageType.System], + ]); + }); + }); - describe('/tp', () => { - it('fixes location of player', () => { - const fixPosition = stub(client, 'fixPosition'); + describe('/tp', () => { + it('fixes location of player', () => { + const fixPosition = stub(client, 'fixPosition'); - runCommand(client, 'tp', '10 20', ChatType.Say, undefined, {}); + runCommand(client, 'tp', '10 20', ChatType.Say, undefined, {}); - assert.calledWith(fixPosition, 10, 20, true); - expect(client.pony.x).equal(10); - expect(client.pony.y).equal(20); - }); + assert.calledWith(fixPosition, 10, 20, true); + expect(client.pony.x).equal(10); + expect(client.pony.y).equal(20); + }); - it('updates safe position', () => { - client.pony.x = 10; - client.pony.y = 20; + it('updates safe position', () => { + client.pony.x = 10; + client.pony.y = 20; - runCommand(client, 'tp', '10 20', ChatType.Say, undefined, {}); + runCommand(client, 'tp', '10 20', ChatType.Say, undefined, {}); - expect(client.safeX).equal(10); - expect(client.safeY).equal(20); - }); + expect(client.safeX).equal(10); + expect(client.safeY).equal(20); + }); - it('resets lastTime to zero', () => { - runCommand(client, 'tp', '10 20', ChatType.Say, undefined, {}); + it('resets lastTime to zero', () => { + runCommand(client, 'tp', '10 20', ChatType.Say, undefined, {}); - expect(client.lastTime).equal(0); - }); - - it('throws on invalid parameters', () => { - runCommand(client, 'tp', '10', ChatType.Say, undefined, {}); + expect(client.lastTime).equal(0); + }); + + it('throws on invalid parameters', () => { + runCommand(client, 'tp', '10', ChatType.Say, undefined, {}); - expect(client.saysQueue).eql([ - [client.pony.id, 'invalid parameters', MessageType.System], - ]); - }); - }); + expect(client.saysQueue).eql([ + [client.pony.id, 'invalid parameters', MessageType.System], + ]); + }); + }); - const placeholderCommands = [ - 's', 'say', 'p', 'party', 't', 'think', 'ss', 's1', 's2', 's3', 'sit', 'stand', 'lie', 'lay', 'fly', - 'w', 'whisper', 'r', 'reply', - ]; + const placeholderCommands = [ + 's', 'say', 'p', 'party', 't', 'think', 'ss', 's1', 's2', 's3', 'sit', 'stand', 'lie', 'lay', 'fly', + 'w', 'whisper', 'r', 'reply', + ]; - placeholderCommands.forEach(command => { - describe(`/${command}`, () => { - it('throws', () => { - expect(() => runCommand(client, command, 'foo bar', ChatType.Say, undefined, {})) - .throw('Should not be called'); - }); - }); - }); - }); + placeholderCommands.forEach(command => { + describe(`/${command}`, () => { + it('throws', () => { + expect(() => runCommand(client, command, 'foo bar', ChatType.Say, undefined, {})) + .throw('Should not be called'); + }); + }); + }); + }); }); diff --git a/src/ts/tests/server/entityUtils.spec.ts b/src/ts/tests/server/entityUtils.spec.ts index c3e6412..402ee27 100644 --- a/src/ts/tests/server/entityUtils.spec.ts +++ b/src/ts/tests/server/entityUtils.spec.ts @@ -10,132 +10,132 @@ import { IClient } from '../../server/serverInterfaces'; import { createServerMap } from '../../server/serverMap'; describe('entityUtils [server]', () => { - describe('findClosest()', () => { - it('returns undefined for empty list', () => { - expect(findClosest(0, 0, [])); - }); + describe('findClosest()', () => { + it('returns undefined for empty list', () => { + expect(findClosest(0, 0, [])); + }); - it('returns closest entity', () => { - const entities: any[] = [ - { x: 1, y: 0 }, - { x: 0, y: 0 }, - { x: 0, y: 1 }, - ]; + it('returns closest entity', () => { + const entities: any[] = [ + { x: 1, y: 0 }, + { x: 0, y: 0 }, + { x: 0, y: 1 }, + ]; - expect(findClosest(0, 0, entities)).equal(entities[1]); - }); - }); + expect(findClosest(0, 0, entities)).equal(entities[1]); + }); + }); - describe('updateEntityOptions()', () => { - it('updates entity options field', () => { - const entity = serverEntity(2, 0, 0, 0, { options: { tag: 'bar' } }); + describe('updateEntityOptions()', () => { + it('updates entity options field', () => { + const entity = serverEntity(2, 0, 0, 0, { options: { tag: 'bar' } }); - updateEntityOptions(entity, { expr: 5 }); + updateEntityOptions(entity, { expr: 5 }); - expect(entity.options).eql({ tag: 'bar', expr: 5 }); - }); + expect(entity.options).eql({ tag: 'bar', expr: 5 }); + }); - it('handles undefined options field', () => { - const entity = serverEntity(2); - entity.options = undefined; + it('handles undefined options field', () => { + const entity = serverEntity(2); + entity.options = undefined; - updateEntityOptions(entity, { expr: 5 }); + updateEntityOptions(entity, { expr: 5 }); - expect(entity.options).eql({ expr: 5 }); - }); + expect(entity.options).eql({ expr: 5 }); + }); - it('adds update to region', () => { - const entity = serverEntity(2, 0, 0, 0, { options: { tag: 'bar' } }); - entity.region = createServerRegion(0, 0); - entity.region.clients.push(mockClient(), mockClient()); + it('adds update to region', () => { + const entity = serverEntity(2, 0, 0, 0, { options: { tag: 'bar' } }); + entity.region = createServerRegion(0, 0); + entity.region.clients.push(mockClient(), mockClient()); - updateEntityOptions(entity, { expr: 5 }); + updateEntityOptions(entity, { expr: 5 }); - expect(entity.region.entityUpdates).eql([ - { entity, flags: UpdateFlags.Options, x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: { expr: 5 } }, - ]); - }); - }); + expect(entity.region.entityUpdates).eql([ + { entity, flags: UpdateFlags.Options, x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: { expr: 5 } }, + ]); + }); + }); - describe('updateEntityState()', () => { - it('sets flags on entity', () => { - const entity = serverEntity(0); + describe('updateEntityState()', () => { + it('sets flags on entity', () => { + const entity = serverEntity(0); - updateEntityState(entity, 123); + updateEntityState(entity, 123); - expect(entity.state).equal(123); - }); + expect(entity.state).equal(123); + }); - it('adds flag update to region updates', () => { - const entity = serverEntity(0); - const region = createServerRegion(0, 0); - entity.client = mockClient({}); - entity.region = region; + it('adds flag update to region updates', () => { + const entity = serverEntity(0); + const region = createServerRegion(0, 0); + entity.client = mockClient({}); + entity.region = region; - updateEntityState(entity, 123); + updateEntityState(entity, 123); - expect(region.entityUpdates).eql([ - { entity, flags: UpdateFlags.State, x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }, - ]); - }); + expect(region.entityUpdates).eql([ + { entity, flags: UpdateFlags.State, x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }, + ]); + }); - it('sends flag update to client if shadowed', () => { - const entity = serverEntity(12); - entity.region = createServerRegion(0, 0); - entity.client = mockClient({ shadowed: true }); + it('sends flag update to client if shadowed', () => { + const entity = serverEntity(12); + entity.region = createServerRegion(0, 0); + entity.client = mockClient({ shadowed: true }); - updateEntityState(entity, 123); + updateEntityState(entity, 123); - expect(getWriterBuffer(entity.client.updateQueue)).eql(new Uint8Array([2, 0, 4, 0, 0, 0, 12, 123])); - }); - }); + expect(getWriterBuffer(entity.client.updateQueue)).eql(new Uint8Array([2, 0, 4, 0, 0, 0, 12, 123])); + }); + }); - describe('fixPosition()', () => { - let client: IClient; + describe('fixPosition()', () => { + let client: IClient; - beforeEach(() => { - client = mockClient(); - client.map = createServerMap('', 0, 1, 1); - }); + beforeEach(() => { + client = mockClient(); + client.map = createServerMap('', 0, 1, 1); + }); - it('updates entity position to given position', () => { - const entity = serverEntity(1); - entity.x = 10; - entity.y = 5; + it('updates entity position to given position', () => { + const entity = serverEntity(1); + entity.x = 10; + entity.y = 5; - fixPosition(entity, client.map, 1, 2, false); + fixPosition(entity, client.map, 1, 2, false); - expect(entity.x).equal(1); - expect(entity.y).equal(2); - }); + expect(entity.x).equal(1); + expect(entity.y).equal(2); + }); - it('submits entity update to region', () => { - const region = createServerRegion(0, 0); - const entity = serverEntity(1); - entity.region = region; + it('submits entity update to region', () => { + const region = createServerRegion(0, 0); + const entity = serverEntity(1); + entity.region = region; - fixPosition(entity, client.map, 1, 2, false); + fixPosition(entity, client.map, 1, 2, false); - expect(region.entityUpdates).eql([ - { - entity, flags: UpdateFlags.Position | UpdateFlags.State, x: 1, y: 2, vx: 0, vy: 0, - action: 0, playerState: 0, options: undefined - }, - ]); - }); + expect(region.entityUpdates).eql([ + { + entity, flags: UpdateFlags.Position | UpdateFlags.State, x: 1, y: 2, vx: 0, vy: 0, + action: 0, playerState: 0, options: undefined + }, + ]); + }); - it('sends fix position message to client', () => { - const fixPositionStub = stub(client, 'fixPosition'); + it('sends fix position message to client', () => { + const fixPositionStub = stub(client, 'fixPosition'); - fixPosition(client.pony, client.map, 1, 2, false); + fixPosition(client.pony, client.map, 1, 2, false); - assert.calledWith(fixPositionStub, 1, 2, false); - }); + assert.calledWith(fixPositionStub, 1, 2, false); + }); - it('sets fixing flag on client', () => { - fixPosition(client.pony, client.map, 1, 2, false); + it('sets fixing flag on client', () => { + fixPosition(client.pony, client.map, 1, 2, false); - expect(client.fixingPosition).true; - }); - }); + expect(client.fixingPosition).true; + }); + }); }); diff --git a/src/ts/tests/server/liveEndPoint.spec.ts b/src/ts/tests/server/liveEndPoint.spec.ts index 8aeb15d..9108d8a 100644 --- a/src/ts/tests/server/liveEndPoint.spec.ts +++ b/src/ts/tests/server/liveEndPoint.spec.ts @@ -9,251 +9,251 @@ import { MINUTE } from '../../common/constants'; import { times } from '../../common/utils'; interface Thing extends Doc { - name: string; - desc: string; + name: string; + desc: string; } describe('liveEndPoint', () => { - let clock: SinonFakeTimers; - let model: Model; - let encode: SinonStub; - let beforeDelete: SinonStub; - let afterDelete: SinonStub; - let afterAssign: SinonStub; - let liveEndPoint: LiveEndPoint; + let clock: SinonFakeTimers; + let model: Model; + let encode: SinonStub; + let beforeDelete: SinonStub; + let afterDelete: SinonStub; + let afterAssign: SinonStub; + let liveEndPoint: LiveEndPoint; - function stubFind(items: any) { - return stub(model, 'find').returns({ - sort: stub().withArgs(match({ updatedAt: 1 })).returns({ - limit: stub().withArgs(ITEM_LIMIT + 1).returns({ - lean: stub().returns({ - exec: () => Promise.resolve(items) - }) - }) - }) - } as any); - } + function stubFind(items: any) { + return stub(model, 'find').returns({ + sort: stub().withArgs(match({ updatedAt: 1 })).returns({ + limit: stub().withArgs(ITEM_LIMIT + 1).returns({ + lean: stub().returns({ + exec: () => Promise.resolve(items) + }) + }) + }) + } as any); + } - beforeEach(() => { - clock = useFakeTimers(); - model = { - findByIdAndUpdate() { }, - findById() { }, - find() { }, - } as any; - encode = stub(); - beforeDelete = stub(); - afterDelete = stub(); - afterAssign = stub(); - liveEndPoint = createLiveEndPoint({ - model, fields: ['_id', 'name', 'desc'], encode, beforeDelete, afterDelete, afterAssign - }); - }); + beforeEach(() => { + clock = useFakeTimers(); + model = { + findByIdAndUpdate() { }, + findById() { }, + find() { }, + } as any; + encode = stub(); + beforeDelete = stub(); + afterDelete = stub(); + afterAssign = stub(); + liveEndPoint = createLiveEndPoint({ + model, fields: ['_id', 'name', 'desc'], encode, beforeDelete, afterDelete, afterAssign + }); + }); - afterEach(() => { - clock.restore(); - liveEndPoint.destroy(); - }); + afterEach(() => { + clock.restore(); + liveEndPoint.destroy(); + }); - it('clears removed items after 10 minutes', () => { - const item = { _id: 'foo', remove: stub() }; - (stub(model, 'findById') as any).withArgs('foo').returns({ exec: () => Promise.resolve(item) }); - clock.setSystemTime(10000); - stubFind([]); + it('clears removed items after 10 minutes', () => { + const item = { _id: 'foo', remove: stub() }; + (stub(model, 'findById') as any).withArgs('foo').returns({ exec: () => Promise.resolve(item) }); + clock.setSystemTime(10000); + stubFind([]); - return liveEndPoint.removeItem('foo') - .then(() => clock.tick(1 * MINUTE + 100)) - .then(() => liveEndPoint.getAll()) - .then(result => expect(result.deletes).eql(['foo'])); - }); + return liveEndPoint.removeItem('foo') + .then(() => clock.tick(1 * MINUTE + 100)) + .then(() => liveEndPoint.getAll()) + .then(result => expect(result.deletes).eql(['foo'])); + }); - describe('get()', () => { - it('finds item by id', () => { - const item = {}; - (stub(model, 'findById') as any).withArgs('foo').returns({ - lean: stub().returns({ - exec: stub().resolves(item) - }) - }); + describe('get()', () => { + it('finds item by id', () => { + const item = {}; + (stub(model, 'findById') as any).withArgs('foo').returns({ + lean: stub().returns({ + exec: stub().resolves(item) + }) + }); - return liveEndPoint.get('foo') - .then(result => expect(result).equal(item)); - }); - }); + return liveEndPoint.get('foo') + .then(result => expect(result).equal(item)); + }); + }); - describe('getAll()', () => { - it('returns encoded items', () => { - stubFind([{ _id: 'aaa' }, { _id: 'bbb' }]); - encode.returns('encoded'); + describe('getAll()', () => { + it('returns encoded items', () => { + stubFind([{ _id: 'aaa' }, { _id: 'bbb' }]); + encode.returns('encoded'); - return liveEndPoint.getAll() - .then(result => expect(result).eql({ - base: {}, - deletes: [], - updates: 'encoded', - more: false, - })); - }); + return liveEndPoint.getAll() + .then(result => expect(result).eql({ + base: {}, + deletes: [], + updates: 'encoded', + more: false, + })); + }); - it('passes 0 timestamp to find method by default', () => { - const find = stubFind([{ _id: 'aaa' }, { _id: 'bbb' }]); + it('passes 0 timestamp to find method by default', () => { + const find = stubFind([{ _id: 'aaa' }, { _id: 'bbb' }]); - return liveEndPoint.getAll() - .then(() => assert.calledWithMatch(find, { updatedAt: { $gt: new Date(0) } }, '_id name desc')); - }); + return liveEndPoint.getAll() + .then(() => assert.calledWithMatch(find, { updatedAt: { $gt: new Date(0) } }, '_id name desc')); + }); - it('passes given timestamp to find method', () => { - const timestamp = '2017-09-09T16:39:54.199Z'; - const find = stubFind([{ _id: 'aaa' }, { _id: 'bbb' }]); + it('passes given timestamp to find method', () => { + const timestamp = '2017-09-09T16:39:54.199Z'; + const find = stubFind([{ _id: 'aaa' }, { _id: 'bbb' }]); - return liveEndPoint.getAll(timestamp) - .then(() => assert.calledWithMatch(find, { updatedAt: { $gt: new Date(timestamp) } }, '_id name desc')); - }); + return liveEndPoint.getAll(timestamp) + .then(() => assert.calledWithMatch(find, { updatedAt: { $gt: new Date(timestamp) } }, '_id name desc')); + }); - describe('if items exceed limit', () => { - describe('if last 2 items have different updatedAt', () => { - beforeEach(() => { - const items = times(ITEM_LIMIT + 1, i => ({ _id: `foo_${i}`, updatedAt: new Date(i) })); - stubFind(items); - }); + describe('if items exceed limit', () => { + describe('if last 2 items have different updatedAt', () => { + beforeEach(() => { + const items = times(ITEM_LIMIT + 1, i => ({ _id: `foo_${i}`, updatedAt: new Date(i) })); + stubFind(items); + }); - it('removes last item', () => { - return liveEndPoint.getAll() - .then(() => expect(encode.args[0][0].length).equal(ITEM_LIMIT)); - }); + it('removes last item', () => { + return liveEndPoint.getAll() + .then(() => expect(encode.args[0][0].length).equal(ITEM_LIMIT)); + }); - it('returns more flag', () => { - return liveEndPoint.getAll() - .then(({ more }) => expect(more).true); - }); - }); + it('returns more flag', () => { + return liveEndPoint.getAll() + .then(({ more }) => expect(more).true); + }); + }); - describe('if last 2 items have the same updatedAt', () => { - let items: any[]; - let restItems: any[]; - let find: SinonStub; + describe('if last 2 items have the same updatedAt', () => { + let items: any[]; + let restItems: any[]; + let find: SinonStub; - beforeEach(() => { - items = times(ITEM_LIMIT, i => ({ _id: `foo_${i}`, updatedAt: new Date(i) })); - items.push({ _id: 'bar', updatedAt: items[items.length - 1].updatedAt }); - restItems = [{ _id: 'bar1' }, { _id: 'bar2' }]; - find = stubFind(items).onSecondCall().returns({ - lean: stub().returns({ - exec: stub().resolves(restItems) - }) - } as any); - }); + beforeEach(() => { + items = times(ITEM_LIMIT, i => ({ _id: `foo_${i}`, updatedAt: new Date(i) })); + items.push({ _id: 'bar', updatedAt: items[items.length - 1].updatedAt }); + restItems = [{ _id: 'bar1' }, { _id: 'bar2' }]; + find = stubFind(items).onSecondCall().returns({ + lean: stub().returns({ + exec: stub().resolves(restItems) + }) + } as any); + }); - it('fetches additional items if last items have the same date', () => { - return liveEndPoint.getAll() - .then(() => expect(encode.args[0][0].length).equal(ITEM_LIMIT + 1 + 2)); - }); + it('fetches additional items if last items have the same date', () => { + return liveEndPoint.getAll() + .then(() => expect(encode.args[0][0].length).equal(ITEM_LIMIT + 1 + 2)); + }); - it('filters out duplicate items', () => { - restItems.push(items[items.length - 1]); + it('filters out duplicate items', () => { + restItems.push(items[items.length - 1]); - return liveEndPoint.getAll() - .then(() => expect(encode.args[0][0].length).equal(ITEM_LIMIT + 1 + 2)); - }); + return liveEndPoint.getAll() + .then(() => expect(encode.args[0][0].length).equal(ITEM_LIMIT + 1 + 2)); + }); - it('fetches more items using timestamp of last element', () => { - const timestamp = items[items.length - 1].updatedAt; + it('fetches more items using timestamp of last element', () => { + const timestamp = items[items.length - 1].updatedAt; - return liveEndPoint.getAll() - .then(() => assert.calledWithMatch(find, { updatedAt: timestamp }, '_id name desc')); - }); + return liveEndPoint.getAll() + .then(() => assert.calledWithMatch(find, { updatedAt: timestamp }, '_id name desc')); + }); - it('returns more flag', () => { - return liveEndPoint.getAll() - .then(({ more }) => expect(more).true); - }); - }); - }); - }); + it('returns more flag', () => { + return liveEndPoint.getAll() + .then(({ more }) => expect(more).true); + }); + }); + }); + }); - describe('removeItem()', () => { - describe('if item exists', () => { - let item: { _id: string; remove: SinonStub; }; + describe('removeItem()', () => { + describe('if item exists', () => { + let item: { _id: string; remove: SinonStub; }; - beforeEach(() => { - item = { _id: 'foo', remove: stub() }; - (stub(model, 'findById') as any).withArgs('foo').returns({ exec: stub().resolves(item) }); - }); + beforeEach(() => { + item = { _id: 'foo', remove: stub() }; + (stub(model, 'findById') as any).withArgs('foo').returns({ exec: stub().resolves(item) }); + }); - it('removes item', () => { - return liveEndPoint.removeItem('foo') - .then(() => assert.calledOnce(item.remove)); - }); + it('removes item', () => { + return liveEndPoint.removeItem('foo') + .then(() => assert.calledOnce(item.remove)); + }); - it('calls beforeDelete hook', () => { - return liveEndPoint.removeItem('foo') - .then(() => assert.calledWith(beforeDelete, item)); - }); + it('calls beforeDelete hook', () => { + return liveEndPoint.removeItem('foo') + .then(() => assert.calledWith(beforeDelete, item)); + }); - it('calls afterDelete hook', () => { - return liveEndPoint.removeItem('foo') - .then(() => assert.calledWith(afterDelete, item)); - }); + it('calls afterDelete hook', () => { + return liveEndPoint.removeItem('foo') + .then(() => assert.calledWith(afterDelete, item)); + }); - it('adds item ID to removed items', () => { - clock.setSystemTime(10000); - stubFind([]); + it('adds item ID to removed items', () => { + clock.setSystemTime(10000); + stubFind([]); - return liveEndPoint.removeItem('foo') - .then(() => liveEndPoint.getAll()) - .then(result => expect(result.deletes).eql(['foo'])); - }); - }); + return liveEndPoint.removeItem('foo') + .then(() => liveEndPoint.getAll()) + .then(result => expect(result.deletes).eql(['foo'])); + }); + }); - describe('if item does not exist', () => { - let item: { remove: SinonStub }; + describe('if item does not exist', () => { + let item: { remove: SinonStub }; - beforeEach(() => { - item = { remove: stub() }; - (stub(model, 'findById') as any).withArgs('bar').returns({ exec: stub().resolves(null as any) }); - }); + beforeEach(() => { + item = { remove: stub() }; + (stub(model, 'findById') as any).withArgs('bar').returns({ exec: stub().resolves(null as any) }); + }); - it('does nothing if item does not exist', () => { - return liveEndPoint.removeItem('bar') - .then(() => assert.notCalled(item.remove)); - }); + it('does nothing if item does not exist', () => { + return liveEndPoint.removeItem('bar') + .then(() => assert.notCalled(item.remove)); + }); - it('does not call onDelete hook', () => { - return liveEndPoint.removeItem('bar') - .then(() => assert.notCalled(beforeDelete)); - }); + it('does not call onDelete hook', () => { + return liveEndPoint.removeItem('bar') + .then(() => assert.notCalled(beforeDelete)); + }); - it('does not call hook', () => { - return liveEndPoint.removeItem('bar') - .then(() => assert.notCalled(afterDelete)); - }); - }); - }); + it('does not call hook', () => { + return liveEndPoint.removeItem('bar') + .then(() => assert.notCalled(afterDelete)); + }); + }); + }); - describe('assignAccount()', () => { - const item = { account: 'origacc' }; + describe('assignAccount()', () => { + const item = { account: 'origacc' }; - beforeEach(() => { - (stub(model, 'findById') as any).withArgs('foo', 'account') - .returns({ lean: stub().returns({ exec: stub().resolves(item) }) }); - }); + beforeEach(() => { + (stub(model, 'findById') as any).withArgs('foo', 'account') + .returns({ lean: stub().returns({ exec: stub().resolves(item) }) }); + }); - it('assigns account to item', () => { - const exec = stub(); - const findByIdAndUpdate = stub(model, 'findByIdAndUpdate').returns({ exec } as any); + it('assigns account to item', () => { + const exec = stub(); + const findByIdAndUpdate = stub(model, 'findByIdAndUpdate').returns({ exec } as any); - return liveEndPoint.assignAccount('foo', 'bar') - .then(() => { - assert.calledWithMatch(findByIdAndUpdate as any, 'foo', { account: 'bar' }); - assert.calledOnce(exec); - }); - }); + return liveEndPoint.assignAccount('foo', 'bar') + .then(() => { + assert.calledWithMatch(findByIdAndUpdate as any, 'foo', { account: 'bar' }); + assert.calledOnce(exec); + }); + }); - it('calls afterAssign hook', () => { - stub(model, 'findByIdAndUpdate').returns({ exec: stub() } as any); + it('calls afterAssign hook', () => { + stub(model, 'findByIdAndUpdate').returns({ exec: stub() } as any); - return liveEndPoint.assignAccount('foo', 'bar') - .then(() => assert.calledWith(afterAssign, 'origacc', 'bar')); - }); - }); + return liveEndPoint.assignAccount('foo', 'bar') + .then(() => assert.calledWith(afterAssign, 'origacc', 'bar')); + }); + }); }); diff --git a/src/ts/tests/server/mapUtils.spec.ts b/src/ts/tests/server/mapUtils.spec.ts index 3e031ed..4c9e466 100644 --- a/src/ts/tests/server/mapUtils.spec.ts +++ b/src/ts/tests/server/mapUtils.spec.ts @@ -7,131 +7,131 @@ import { addEntityToRegion } from '../../server/serverRegion'; import { getRegion } from '../../common/worldMap'; describe('mapUtils', () => { - let map: ServerMap; + let map: ServerMap; - function addEntities(region: ServerRegion, ...entities: ServerEntity[]) { - entities.forEach(e => addEntityToRegion(region, e, map)); - } + function addEntities(region: ServerRegion, ...entities: ServerEntity[]) { + entities.forEach(e => addEntityToRegion(region, e, map)); + } - beforeEach(() => { - map = createServerMap('', 0, 10, 10); - }); + beforeEach(() => { + map = createServerMap('', 0, 10, 10); + }); - describe('findEntities()', () => { - it('returns all entities matching given predicate (1)', () => { - const entity = serverEntity(3); - addEntities(getRegion(map, 3, 4), serverEntity(1), serverEntity(2), entity); + describe('findEntities()', () => { + it('returns all entities matching given predicate (1)', () => { + const entity = serverEntity(3); + addEntities(getRegion(map, 3, 4), serverEntity(1), serverEntity(2), entity); - expect(findEntities(map, e => e.id === 3)).eql([entity]); - }); + expect(findEntities(map, e => e.id === 3)).eql([entity]); + }); - it('returns all entities matching given predicate (2)', () => { - const entity3 = serverEntity(3); - const entity2 = serverEntity(3); - addEntities(getRegion(map, 3, 4), serverEntity(1), entity2, entity3); + it('returns all entities matching given predicate (2)', () => { + const entity3 = serverEntity(3); + const entity2 = serverEntity(3); + addEntities(getRegion(map, 3, 4), serverEntity(1), entity2, entity3); - expect(findEntities(map, e => e.id > 1)).eql([entity2, entity3]); - }); + expect(findEntities(map, e => e.id > 1)).eql([entity2, entity3]); + }); - it('returns empty array if not found', () => { - expect(findEntities(map, e => e.id === 3)).eql([]); - }); - }); + it('returns empty array if not found', () => { + expect(findEntities(map, e => e.id === 3)).eql([]); + }); + }); - describe('findClosestEntity()', () => { - it('returns undefined for empty map', () => { - const map = createServerMap('', 0, 1, 1); + describe('findClosestEntity()', () => { + it('returns undefined for empty map', () => { + const map = createServerMap('', 0, 1, 1); - const result = findClosestEntity(map, 0, 0, () => true); + const result = findClosestEntity(map, 0, 0, () => true); - expect(result).undefined; - }); + expect(result).undefined; + }); - it('returns first matching entity (first)', () => { - const map = createServerMap('', 0, 1, 1); - const entity = serverEntity(1); - map.regions[0].entities.push(entity); + it('returns first matching entity (first)', () => { + const map = createServerMap('', 0, 1, 1); + const entity = serverEntity(1); + map.regions[0].entities.push(entity); - const result = findClosestEntity(map, 0, 0, () => true); + const result = findClosestEntity(map, 0, 0, () => true); - expect(result).equal(entity); - }); + expect(result).equal(entity); + }); - it('returns first matching entity (second)', () => { - const map = createServerMap('', 0, 1, 1); - const entity1 = serverEntity(1); - const entity2 = serverEntity(2); - map.regions[0].entities.push(entity1, entity2); + it('returns first matching entity (second)', () => { + const map = createServerMap('', 0, 1, 1); + const entity1 = serverEntity(1); + const entity2 = serverEntity(2); + map.regions[0].entities.push(entity1, entity2); - const result = findClosestEntity(map, 0, 0, e => e.id === 2); + const result = findClosestEntity(map, 0, 0, e => e.id === 2); - expect(result).equal(entity2); - }); + expect(result).equal(entity2); + }); - it('returns first matching entity (in 2nd region)', () => { - const map = createServerMap('', 0, 2, 2); - const entity1 = serverEntity(1); - const entity2 = serverEntity(2); - map.regions[0].entities.push(entity1); - map.regions[1].entities.push(entity2); + it('returns first matching entity (in 2nd region)', () => { + const map = createServerMap('', 0, 2, 2); + const entity1 = serverEntity(1); + const entity2 = serverEntity(2); + map.regions[0].entities.push(entity1); + map.regions[1].entities.push(entity2); - const result = findClosestEntity(map, 0, 0, e => e.id === 2); + const result = findClosestEntity(map, 0, 0, e => e.id === 2); - expect(result).equal(entity2); - }); + expect(result).equal(entity2); + }); - it('returns closest matching entity (2nd is closest)', () => { - const map = createServerMap('', 0, 1, 1); - const entity1 = serverEntity(1, 0, 0); - const entity2 = serverEntity(2, 1, 1); - map.regions[0].entities.push(entity1, entity2); + it('returns closest matching entity (2nd is closest)', () => { + const map = createServerMap('', 0, 1, 1); + const entity1 = serverEntity(1, 0, 0); + const entity2 = serverEntity(2, 1, 1); + map.regions[0].entities.push(entity1, entity2); - const result = findClosestEntity(map, 1, 1, () => true); + const result = findClosestEntity(map, 1, 1, () => true); - expect(result).equal(entity2); - }); + expect(result).equal(entity2); + }); - it('stops searching if found in first region', () => { - const map = createServerMap('', 0, 2, 2); - const entity1 = serverEntity(1, 0, 0); - const entity2 = serverEntity(2, 11, 11); - map.regions[0].entities.push(entity1); - map.regions[1].entities.push(entity2); - let checks = 0; + it('stops searching if found in first region', () => { + const map = createServerMap('', 0, 2, 2); + const entity1 = serverEntity(1, 0, 0); + const entity2 = serverEntity(2, 11, 11); + map.regions[0].entities.push(entity1); + map.regions[1].entities.push(entity2); + let checks = 0; - const result = findClosestEntity(map, 1, 1, () => (checks++ , true)); + const result = findClosestEntity(map, 1, 1, () => (checks++ , true)); - expect(checks).equal(1); - expect(result).equal(entity1); - }); + expect(checks).equal(1); + expect(result).equal(entity1); + }); - it('searches for entity in ring pattern', () => { - const map = createServerMap('', 0, 5, 5); + it('searches for entity in ring pattern', () => { + const map = createServerMap('', 0, 5, 5); - for (let y = 0; y < 5; y++) { - for (let x = 0; x < 5; x++) { - getRegion(map, x, y).entities.push(serverEntity(0, x * 8 + 1, y * 8 + 1, 1, { name: `${x},${y}` })); - } - } + for (let y = 0; y < 5; y++) { + for (let x = 0; x < 5; x++) { + getRegion(map, x, y).entities.push(serverEntity(0, x * 8 + 1, y * 8 + 1, 1, { name: `${x},${y}` })); + } + } - let checks: string[] = []; + let checks: string[] = []; - const result = findClosestEntity(map, map.width / 2, map.height / 2, e => (checks.push(e.name!), false)); + const result = findClosestEntity(map, map.width / 2, map.height / 2, e => (checks.push(e.name!), false)); - expect(checks).eql([ - '2,2', + expect(checks).eql([ + '2,2', - '1,1', '2,1', '3,1', - '1,2', /* */ '3,2', - '1,3', '2,3', '3,3', + '1,1', '2,1', '3,1', + '1,2', /* */ '3,2', + '1,3', '2,3', '3,3', - '0,0', '1,0', '2,0', '3,0', '4,0', - '0,1', /* */ '4,1', - '0,2', /* */ '4,2', - '0,3', /* */ '4,3', - '0,4', '1,4', '2,4', '3,4', '4,4', - ]); - expect(result).undefined; - }); - }); + '0,0', '1,0', '2,0', '3,0', '4,0', + '0,1', /* */ '4,1', + '0,2', /* */ '4,2', + '0,3', /* */ '4,3', + '0,4', '1,4', '2,4', '3,4', '4,4', + ]); + expect(result).undefined; + }); + }); }); diff --git a/src/ts/tests/server/move.spec.ts b/src/ts/tests/server/move.spec.ts index c72f2bb..ab1f546 100644 --- a/src/ts/tests/server/move.spec.ts +++ b/src/ts/tests/server/move.spec.ts @@ -17,305 +17,305 @@ import { PONY_SPEED_TROT } from '../../common/constants'; import * as collision from '../../common/collision'; describe('move', () => { - describe('move()', () => { - let camera: Camera; - let client: IClient; - let counter = stubClass>(CounterService); - let move: Move; - let isStaticCollision: SinonStub; + describe('move()', () => { + let camera: Camera; + let client: IClient; + let counter = stubClass>(CounterService); + let move: Move; + let isStaticCollision: SinonStub; - beforeEach(() => { - resetStubMethods(counter, 'add', 'remove'); + beforeEach(() => { + resetStubMethods(counter, 'add', 'remove'); - isStaticCollision = stub(collision, 'isStaticCollision'); - camera = createCamera(); - client = mockClient(); - client.map = createServerMap('', 0, 10, 10); - move = createMove(counter as any); - }); + isStaticCollision = stub(collision, 'isStaticCollision'); + camera = createCamera(); + client = mockClient(); + client.map = createServerMap('', 0, 10, 10); + move = createMove(counter as any); + }); - afterEach(() => { - isStaticCollision.restore(); - }); + afterEach(() => { + isStaticCollision.restore(); + }); - it('does nothing if loading flag is true', () => { - client.loading = true; + it('does nothing if loading flag is true', () => { + client.loading = true; - move(client, 0, 1, 2, 3, 4, 5, {}); + move(client, 0, 1, 2, 3, 4, 5, {}); - expect(client.pony.x).equal(0, 'x'); - expect(client.pony.y).equal(0, 'y'); - }); - - it('does nothing if fixing position', () => { - client.loading = true; - - move(client, 0, 1, 2, 3, 4, 5, {}); + expect(client.pony.x).equal(0, 'x'); + expect(client.pony.y).equal(0, 'y'); + }); + + it('does nothing if fixing position', () => { + client.loading = true; + + move(client, 0, 1, 2, 3, 4, 5, {}); - expect(client.pony.x).equal(0, 'x'); - expect(client.pony.y).equal(0, 'y'); - }); + expect(client.pony.x).equal(0, 'x'); + expect(client.pony.y).equal(0, 'y'); + }); - it('updates pony coordinates', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.None, 123, camera); + it('updates pony coordinates', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.None, 123, camera); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.x).equal(12.015625, 'x'); - expect(client.pony.y).equal(34.020833333333336, 'y'); - }); + expect(client.pony.x).equal(12.015625, 'x'); + expect(client.pony.y).equal(34.020833333333336, 'y'); + }); - it('updates last coordinates, velocity and time', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.None, 123, camera); + it('updates last coordinates, velocity and time', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.None, 123, camera); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.lastX).equal(12.015625, 'lastX'); - expect(client.lastY).equal(34.020833333333336, 'lastY'); - expect(client.lastVX).equal(0, 'lastVX'); - expect(client.lastVY).equal(0, 'lastVY'); - expect(client.lastTime).equal(123, 'lastTime'); - }); + expect(client.lastX).equal(12.015625, 'lastX'); + expect(client.lastY).equal(34.020833333333336, 'lastY'); + expect(client.lastVX).equal(0, 'lastVX'); + expect(client.lastVY).equal(0, 'lastVY'); + expect(client.lastTime).equal(123, 'lastTime'); + }); - it('updates pony coordinates (has last time)', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.None, 123, camera); - client.lastTime = 1; + it('updates pony coordinates (has last time)', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.None, 123, camera); + client.lastTime = 1; - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.x).equal(12.015625, 'x'); - expect(client.pony.y).equal(34.020833333333336, 'y'); - }); + expect(client.pony.x).equal(12.015625, 'x'); + expect(client.pony.y).equal(34.020833333333336, 'y'); + }); - it('updates pony velocity', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + it('updates pony velocity', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.vx).equal(PONY_SPEED_TROT, 'vx'); - expect(client.pony.vy).equal(-PONY_SPEED_TROT, 'vy'); - }); + expect(client.pony.vx).equal(PONY_SPEED_TROT, 'vx'); + expect(client.pony.vy).equal(-PONY_SPEED_TROT, 'vy'); + }); - it('updates safe position if not colliding', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); - isStaticCollision.returns(false); - client.pony.x = 10; - client.pony.y = 30; + it('updates safe position if not colliding', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + isStaticCollision.returns(false); + client.pony.x = 10; + client.pony.y = 30; - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.safeX).equal(10, 'safeX'); - expect(client.safeY).equal(30, 'safeY'); - assert.calledWith(isStaticCollision, client.pony, client.map, true); - }); + expect(client.safeX).equal(10, 'safeX'); + expect(client.safeY).equal(30, 'safeY'); + assert.calledWith(isStaticCollision, client.pony, client.map, true); + }); - it('does not update safe position if colliding', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); - isStaticCollision.returns(false); - isStaticCollision.withArgs(client.pony, client.map, true).returns(true); - client.pony.x = 10; - client.pony.y = 30; - client.safeX = 1; - client.safeY = 3; + it('does not update safe position if colliding', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + isStaticCollision.returns(false); + isStaticCollision.withArgs(client.pony, client.map, true).returns(true); + client.pony.x = 10; + client.pony.y = 30; + client.safeX = 1; + client.safeY = 3; - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.safeX).equal(1, 'safeX'); - expect(client.safeY).equal(3, 'safeY'); - }); + expect(client.safeX).equal(1, 'safeX'); + expect(client.safeY).equal(3, 'safeY'); + }); - it('resets pony to safe position if colliding', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); - const fixPositionStub = stub(client, 'fixPosition'); - isStaticCollision.onCall(0).returns(true); - isStaticCollision.onCall(1).returns(true); - isStaticCollision.onCall(2).returns(false); - client.safeX = 1; - client.safeY = 3; + it('resets pony to safe position if colliding', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + const fixPositionStub = stub(client, 'fixPosition'); + isStaticCollision.onCall(0).returns(true); + isStaticCollision.onCall(1).returns(true); + isStaticCollision.onCall(2).returns(false); + client.safeX = 1; + client.safeY = 3; - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.x).equal(1, 'x'); - expect(client.pony.y).equal(3, 'y'); - assert.calledWith(fixPositionStub, 1, 3, false); - }); + expect(client.pony.x).equal(1, 'x'); + expect(client.pony.y).equal(3, 'y'); + assert.calledWith(fixPositionStub, 1, 3, false); + }); - it('does not reset pony to safe position if safe position is colliding', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); - const fixPositionStub = stub(client, 'fixPosition'); - isStaticCollision.onCall(0).returns(true); - isStaticCollision.onCall(1).returns(true); - isStaticCollision.onCall(2).returns(true); - client.safeX = 1; - client.safeY = 3; + it('does not reset pony to safe position if safe position is colliding', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + const fixPositionStub = stub(client, 'fixPosition'); + isStaticCollision.onCall(0).returns(true); + isStaticCollision.onCall(1).returns(true); + isStaticCollision.onCall(2).returns(true); + client.safeX = 1; + client.safeY = 3; - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.x).equal(12.015625, 'x'); - expect(client.pony.y).equal(34.020833333333336, 'y'); - assert.notCalled(fixPositionStub); - }); + expect(client.pony.x).equal(12.015625, 'x'); + expect(client.pony.y).equal(34.020833333333336, 'y'); + assert.notCalled(fixPositionStub); + }); - it('updates pony right flag', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + it('updates pony right flag', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.state).equal(EntityState.FacingRight, 'flags'); - }); + expect(client.pony.state).equal(EntityState.FacingRight, 'flags'); + }); - it('resets head turned flag if turning', () => { - client.pony.state = EntityState.HeadTurned; - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + it('resets head turned flag if turning', () => { + client.pony.state = EntityState.HeadTurned; + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.state).equal(EntityState.FacingRight, 'flags'); - }); + expect(client.pony.state).equal(EntityState.FacingRight, 'flags'); + }); - it('resets sitting flag', () => { - client.pony.state = EntityState.PonySitting; - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); + it('resets sitting flag', () => { + client.pony.state = EntityState.PonySitting; + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 123, camera); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.state).equal(EntityState.FacingRight, 'flags'); - }); + expect(client.pony.state).equal(EntityState.FacingRight, 'flags'); + }); - it('adds entity update', () => { - const region = createServerRegion(0, 0); - client.pony.region = region; - const [a, b, c, d, e] = encodeMovement(0, 0, 0, 0, 0, rect(0, 0, 0, 0)); + it('adds entity update', () => { + const region = createServerRegion(0, 0); + client.pony.region = region; + const [a, b, c, d, e] = encodeMovement(0, 0, 0, 0, 0, rect(0, 0, 0, 0)); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(region.entityUpdates).eql([ - { - entity: client.pony, flags: UpdateFlags.Position | UpdateFlags.State, - x: 0.015625, y: 0.020833333333333332, vx: 0, vy: -0, - action: 0, playerState: 0, options: undefined - }, - ]); - }); + expect(region.entityUpdates).eql([ + { + entity: client.pony, flags: UpdateFlags.Position | UpdateFlags.State, + x: 0.015625, y: 0.020833333333333332, vx: 0, vy: -0, + action: 0, playerState: 0, options: undefined + }, + ]); + }); - it('clears cancellable expression', () => { - client.pony.exprCancellable = true; - client.pony.options!.expr = 123; + it('clears cancellable expression', () => { + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; - const [a, b, c, d, e] = encodeMovement(0, 0, 0, 0, 0, rect(0, 0, 0, 0)); - move(client, 0, a, b, c, d, e, {}); + const [a, b, c, d, e] = encodeMovement(0, 0, 0, 0, 0, rect(0, 0, 0, 0)); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); - expect(client.pony.exprCancellable).false; - }); + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + expect(client.pony.exprCancellable).false; + }); - it('reports client outside the map', () => { - const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); - const warn = stub(client.reporter, 'warn'); - Object.assign(client.map, { id: 'foo', width: 100, height: 100 }); + it('reports client outside the map', () => { + const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); + const warn = stub(client.reporter, 'warn'); + Object.assign(client.map, { id: 'foo', width: 100, height: 100 }); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - assert.calledWith(warn, 'Outside map', 'map: [foo] coords: [10000.02, 10000.02]'); - }); + assert.calledWith(warn, 'Outside map', 'map: [foo] coords: [10000.02, 10000.02]'); + }); - it('logs client outside the map', () => { - const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); - Object.assign(client.map, { id: 'foo', width: 100, height: 100 }); + it('logs client outside the map', () => { + const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); + Object.assign(client.map, { id: 'foo', width: 100, height: 100 }); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.leaveReason).equal('outside map: [foo] coords: [10000.02, 10000.02]'); - }); + expect(client.leaveReason).equal('outside map: [foo] coords: [10000.02, 10000.02]'); + }); - it('disconnects client outside the map', () => { - const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); - const disconnect = stub(client, 'disconnect'); - Object.assign(client.map, { id: 'foo', width: 100, height: 100 }); + it('disconnects client outside the map', () => { + const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); + const disconnect = stub(client, 'disconnect'); + Object.assign(client.map, { id: 'foo', width: 100, height: 100 }); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - assert.calledWith(disconnect, true, true); - }); + assert.calledWith(disconnect, true, true); + }); - it('does not update pony position when coordinates are outside the map', () => { - const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); - Object.assign(client.map, { width: 100, height: 100 }); + it('does not update pony position when coordinates are outside the map', () => { + const [a, b, c, d, e] = encodeMovement(10000, 10000, 0, EntityState.None, 123, camera); + Object.assign(client.map, { width: 100, height: 100 }); - move(client, 0, a, b, c, d, e, {}); + move(client, 0, a, b, c, d, e, {}); - expect(client.pony.x).equal(0, 'x'); - expect(client.pony.y).equal(0, 'y'); - }); + expect(client.pony.x).equal(0, 'x'); + expect(client.pony.y).equal(0, 'y'); + }); - it('logs lagging player if logLagging setting is true', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 16000, camera); - const systemLog = stub(client.reporter, 'systemLog'); - client.account.name = 'Foo'; - client.accountId = 'foo'; + it('logs lagging player if logLagging setting is true', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 16000, camera); + const systemLog = stub(client.reporter, 'systemLog'); + client.account.name = 'Foo'; + client.accountId = 'foo'; - move(client, 0, a, b, c, d, e, { logLagging: true }); + move(client, 0, a, b, c, d, e, { logLagging: true }); - assert.calledWith(systemLog, 'Time delta > 15s (16000)'); - expect(client.logDisconnect).true; - }); + assert.calledWith(systemLog, 'Time delta > 15s (16000)'); + expect(client.logDisconnect).true; + }); - it('kicks player for lagging if kickLagging setting is true', () => { - const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 16000, camera); - const disconnect = stub(client, 'disconnect'); + it('kicks player for lagging if kickLagging setting is true', () => { + const [a, b, c, d, e] = encodeMovement(12, 34, 2, EntityState.PonyTrotting, 16000, camera); + const disconnect = stub(client, 'disconnect'); - move(client, 0, a, b, c, d, e, { kickLagging: true }); + move(client, 0, a, b, c, d, e, { kickLagging: true }); - assert.calledWith(disconnect, true, true); - expect(client.leaveReason).equal('lagging'); - }); + assert.calledWith(disconnect, true, true); + expect(client.leaveReason).equal('lagging'); + }); - it('counts teleporting', () => { - const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); - counter.add.returns({ count: 1, items: [], date: 0 }); - Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); + it('counts teleporting', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); + counter.add.returns({ count: 1, items: [], date: 0 }); + Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); - move(client, 0, a, b, c, d, e, { reportTeleporting: true }); + move(client, 0, a, b, c, d, e, { reportTeleporting: true }); - assert.calledWith(counter.add, client.accountId); - }); + assert.calledWith(counter.add, client.accountId); + }); - it('reports teleporting if counter exceeded limit', () => { - const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); - counter.add.returns({ count: 20, items: [], date: 0 }); - Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); - const warn = stub(client.reporter, 'warn'); + it('reports teleporting if counter exceeded limit', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); + counter.add.returns({ count: 20, items: [], date: 0 }); + Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); + const warn = stub(client.reporter, 'warn'); - move(client, 0, a, b, c, d, e, { reportTeleporting: true }); + move(client, 0, a, b, c, d, e, { reportTeleporting: true }); - assert.calledWith(counter.add, client.accountId); - assert.calledWith(counter.remove, client.accountId); - assert.calledWith(warn, 'Teleporting (x10)'); - }); + assert.calledWith(counter.add, client.accountId); + assert.calledWith(counter.remove, client.accountId); + assert.calledWith(warn, 'Teleporting (x10)'); + }); - it('kicks player for teleporting', () => { - const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); - const disconnect = stub(client, 'disconnect'); - Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); + it('kicks player for teleporting', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); + const disconnect = stub(client, 'disconnect'); + Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); - move(client, 0, a, b, c, d, e, { kickTeleporting: true }); + move(client, 0, a, b, c, d, e, { kickTeleporting: true }); - assert.calledWith(disconnect, true, true); - expect(client.leaveReason).equal('teleporting'); - }); + assert.calledWith(disconnect, true, true); + expect(client.leaveReason).equal('teleporting'); + }); - it('fixes player position if teleporting', () => { - const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); - const systemLog = stub(client.reporter, 'systemLog'); - const fixPositionStub = stub(client, 'fixPosition'); - Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); + it('fixes player position if teleporting', () => { + const [a, b, c, d, e] = encodeMovement(10, 10, 2, EntityState.PonyTrotting, 1001, camera); + const systemLog = stub(client.reporter, 'systemLog'); + const fixPositionStub = stub(client, 'fixPosition'); + Object.assign(client, { lastX: 0, lastY: 0, lastVX: 0, lastVY: 0, lastTime: 1 }); - move(client, 0, a, b, c, d, e, { fixTeleporting: true, logFixingPosition: true }); + move(client, 0, a, b, c, d, e, { fixTeleporting: true, logFixingPosition: true }); - expect(client.pony.vx).equal(0); - expect(client.pony.vy).equal(0); - assert.calledWith(fixPositionStub, 0, 0, false); - assert.calledWith(systemLog, 'Fixed teleporting (10.015625 10.020833333333334) -> (0 0)'); - }); - }); + expect(client.pony.vx).equal(0); + expect(client.pony.vy).equal(0); + assert.calledWith(fixPositionStub, 0, 0, false); + assert.calledWith(systemLog, 'Fixed teleporting (10.015625 10.020833333333334) -> (0 0)'); + }); + }); }); diff --git a/src/ts/tests/server/originUtils.spec.ts b/src/ts/tests/server/originUtils.spec.ts index fc1a613..43bdb92 100644 --- a/src/ts/tests/server/originUtils.spec.ts +++ b/src/ts/tests/server/originUtils.spec.ts @@ -6,41 +6,41 @@ import { addOrigin } from '../../server/originUtils'; import { Account } from '../../server/db'; describe('originUtils', () => { - describe.skip('addOrigin()', () => { - let clock: SinonFakeTimers; - let update: SinonStub; + describe.skip('addOrigin()', () => { + let clock: SinonFakeTimers; + let update: SinonStub; - beforeEach(() => { - clock = useFakeTimers(); - update = stub(Account, 'update').returns({ exec: () => Promise.resolve() } as any); - }); + beforeEach(() => { + clock = useFakeTimers(); + update = stub(Account, 'update').returns({ exec: () => Promise.resolve() } as any); + }); - afterEach(() => { - clock.restore(); - update.restore(); - }); + afterEach(() => { + clock.restore(); + update.restore(); + }); - it('adds origin to account', async () => { - const acc = account({}); - const origin = { foo: 'bar' } as any; + it('adds origin to account', async () => { + const acc = account({}); + const origin = { foo: 'bar' } as any; - await addOrigin(acc, origin); + await addOrigin(acc, origin); - expect(acc.origins).contains(origin); - assert.calledWithMatch(update, { _id: acc._id }, { $push: { origins: origin } }); - }); + expect(acc.origins).contains(origin); + assert.calledWithMatch(update, { _id: acc._id }, { $push: { origins: origin } }); + }); - it('updates date of existing origin', async () => { - const acc = account({ - origins: [{ _id: 'foo', ip: '1.2.3.4', last: new Date(9999) }], - } as any); - const origin = { ip: '1.2.3.4' } as any; - clock.setSystemTime(12345); + it('updates date of existing origin', async () => { + const acc = account({ + origins: [{ _id: 'foo', ip: '1.2.3.4', last: new Date(9999) }], + } as any); + const origin = { ip: '1.2.3.4' } as any; + clock.setSystemTime(12345); - await addOrigin(acc, origin); + await addOrigin(acc, origin); - expect(acc.origins).eql([{ _id: 'foo', ip: '1.2.3.4', last: new Date(12345) }]); - assert.calledWith(update, { _id: acc._id, 'origins._id': 'foo' }); //, { 'origins.$.last': new Date(12345) }); - }); - }); + expect(acc.origins).eql([{ _id: 'foo', ip: '1.2.3.4', last: new Date(12345) }]); + assert.calledWith(update, { _id: acc._id, 'origins._id': 'foo' }); //, { 'origins.$.last': new Date(12345) }); + }); + }); }); diff --git a/src/ts/tests/server/other.spec.ts b/src/ts/tests/server/other.spec.ts index 007ca5c..61b3c0e 100644 --- a/src/ts/tests/server/other.spec.ts +++ b/src/ts/tests/server/other.spec.ts @@ -3,10 +3,10 @@ import { expect } from 'chai'; import { CHANGELOG } from '../../generated/changelog'; describe('other', () => { - it('package version is the same as latest changelog version entry', () => { - const packageJson: any = require('../../../../package.json'); - const packageVersion = packageJson.version.replace(/-alpha$/, ''); - const changelogVersion = CHANGELOG[0].version.replace(/^v/, ''); - expect(packageVersion).equal(changelogVersion, `package: ${packageVersion}, changelog: ${changelogVersion}`); - }); + it('package version is the same as latest changelog version entry', () => { + const packageJson: any = require('../../../../package.json'); + const packageVersion = packageJson.version.replace(/-alpha$/, ''); + const changelogVersion = CHANGELOG[0].version.replace(/^v/, ''); + expect(packageVersion).equal(changelogVersion, `package: ${packageVersion}, changelog: ${changelogVersion}`); + }); }); diff --git a/src/ts/tests/server/patreon.spec.ts b/src/ts/tests/server/patreon.spec.ts index 49bb182..9298a1d 100644 --- a/src/ts/tests/server/patreon.spec.ts +++ b/src/ts/tests/server/patreon.spec.ts @@ -4,8 +4,8 @@ import { Types } from 'mongoose'; import { expect } from 'chai'; import { stub, assert, SinonStub, SinonFakeTimers, useFakeTimers, match } from 'sinon'; import { - createRemoveOldSupporters, createUpdateSupporters, fetchPatreonData, createUpdatePatreonInfo, - RemoveOldSupporters, UpdateSupporters, createAddTotalPledged, AddTotalPledged + createRemoveOldSupporters, createUpdateSupporters, fetchPatreonData, createUpdatePatreonInfo, + RemoveOldSupporters, UpdateSupporters, createAddTotalPledged, AddTotalPledged } from '../../server/patreon'; import { genId, auth, account } from '../mocks'; import { PatreonFlags, PatreonData } from '../../common/adminInterfaces'; @@ -18,633 +18,633 @@ const queryAdd = '&include=patron.null,reward.null&fields%5Bpledge%5D=total_hist const query = `${queryBase}${queryAdd}`; describe('patreon', () => { - describe('fetchPatreonData()', () => { - let client: SinonStub; - - beforeEach(() => { - client = stub(); - client.withArgs('/current_user/campaigns') - .resolves({ rawJson: { data: [{ id: 'foo' }], included: [] } }); - client.withArgs(`/campaigns/foo/pledges?${query}`) - .resolves({ rawJson: { data: [], links: {} } }); - }); - - it('returns rewards', async () => { - client.withArgs('/current_user/campaigns') - .resolves({ - rawJson: { - data: [{ id: 'foo', attributes: {} }], - included: [ - { type: 'reward', id: '123', attributes: { title: 'title', description: 'desc' } }, - { type: 'other', id: '111', attributes: { title: 'ttt', description: 'ddd' } }, - ], - }, - }); - - const result = await fetchPatreonData(client as any, noop); - - expect(result.rewards).eql([ - { id: '123', title: 'title', description: 'desc' }, - ]); - }); - - it('fixes incomplete data', async () => { - client.withArgs('/current_user/campaigns') - .resolves({ - rawJson: { - data: [{ id: 'foo' }], - included: [ - { type: 'reward', id: '123', attributes: {} }, - ], - }, - }); - - const result = await fetchPatreonData(client as any, noop); - - expect(result.rewards).eql([ - { id: '123', title: '', description: '' }, - ]); - }); - - it('returns pledges', async () => { - client.withArgs(`/campaigns/foo/pledges?${query}`) - .resolves({ - rawJson: { - data: [ - { - attributes: { total_historical_amount_cents: 100 }, - relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, - }, - { - attributes: { total_historical_amount_cents: 200 }, - relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } }, - }, - ], - links: {}, - }, - }); - - const result = await fetchPatreonData(client as any, noop); - - expect(result.pledges).eql([ - { user: 'patid1', reward: 'rewid1', total: 100, declinedSince: undefined }, - { user: 'patid2', reward: 'rewid2', total: 200, declinedSince: undefined }, - ]); - }); - - it('skips pledges with missing data', async () => { - client.withArgs(`/campaigns/foo/pledges?${query}`) - .resolves({ - rawJson: { - data: [ - { - attributes: {}, - relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, - }, - { - attributes: {}, - relationships: { patron: {}, reward: { data: { id: 'rewid2' } } }, - }, - { - attributes: {}, - relationships: { patron: { data: { id: 'patid3' } }, reward: {} }, - }, - ], - links: {}, - }, - }); - - const result = await fetchPatreonData(client as any, noop); - - expect(result.pledges).eql([ - { user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined }, - ]); - }); - - it('fetches multiple pages of pledges (with correct parameters for next page)', async () => { - client.withArgs(`/campaigns/foo/pledges?${query}`) - .resolves({ - rawJson: { - data: [ - { - attributes: {}, - relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, - }, - ], - links: { - next: 'https://www.patreon.com/api/oauth2/api/link-to-next-page?page=5', - }, - }, - }); - client.withArgs('/link-to-next-page?page=5' + queryAdd) - .resolves({ - rawJson: { - data: [ - { - attributes: {}, - relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } }, - }, - ], - links: {}, - }, - }); - - const result = await fetchPatreonData(client as any, noop); - - assert.calledWith(client, '/link-to-next-page?page=5' + queryAdd); - expect(result.pledges).eql([ - { user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined }, - { user: 'patid2', reward: 'rewid2', total: 0, declinedSince: undefined }, - ]); - }); - - it('throws on too many pages', async () => { - client.withArgs(`/campaigns/foo/pledges?${query}`) - .resolves({ - rawJson: { - data: [ - { - attributes: {}, - relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, - }, - ], - links: { - next: `/campaigns/foo/pledges?${queryBase}`, - }, - }, - }); - - await expect(fetchPatreonData(client as any, noop)).rejectedWith('Exceeded 100 pages of patreon data'); - }); - }); - - describe('updatePatreonInfo()', () => { - let queryAuths: SinonStub; - let queryAccounts: SinonStub; - let removeOldSupporters: SinonStub; - let updateSupporters: SinonStub; - let addTotalPledged: SinonStub; - let updatePatreonInfo: (data: PatreonData, now: Date) => any; - - beforeEach(() => { - queryAuths = stub(); - queryAccounts = stub(); - removeOldSupporters = stub(); - updateSupporters = stub(); - addTotalPledged = stub(); - updatePatreonInfo = createUpdatePatreonInfo( - queryAuths, queryAccounts, removeOldSupporters, updateSupporters, addTotalPledged); - }); - - it('queries auths', async () => { - queryAuths.resolves([]); - queryAccounts.resolves([]); - - await updatePatreonInfo({ - pledges: [ - { reward: '123', user: 'foo', total: 0 }, - { reward: '123', user: 'bar', total: 0 }, - ], - rewards: [], - }, new Date()); - - assert.calledWith(queryAuths, match({ - provider: 'patreon', - openId: { $in: ['foo', 'bar'] }, - account: { $exists: true }, - banned: { $ne: true }, - disabled: { $ne: true }, - }), '_id account openId pledged'); - }); - - it('queries accounts', async () => { - queryAuths.resolves([]); - queryAccounts.resolves([]); - - await updatePatreonInfo({ pledges: [], rewards: [] }, new Date()); - - assert.calledWith( - queryAccounts, match({ patreon: { $exists: true, $ne: 0 } }), '_id patreon supporterDeclinedSince'); - }); - - it('removes old supporters', async () => { - const auths = [{}] as any; - const accounts = [{}] as any; - queryAuths.resolves(auths); - queryAccounts.resolves(auths); - - await updatePatreonInfo({ pledges: [], rewards: [] }, new Date()); - - assert.calledWith(removeOldSupporters, auths, accounts); - }); - - it('adds new supporters', async () => { - const auths = [{}] as any; - const accounts = [{}] as any; - const pledges = [{}] as any; - const now = new Date(); - queryAuths.resolves(auths); - queryAccounts.resolves(auths); - - await updatePatreonInfo({ pledges, rewards: [] }, now); - - assert.calledWith(updateSupporters, auths, accounts, pledges, now); - }); - - it('adds total pledged', async () => { - const auths = [{}] as any; - const pledges = [{}] as any; - queryAuths.resolves(auths); - queryAccounts.resolves(auths); - - await updatePatreonInfo({ pledges, rewards: [] }, new Date()); - - assert.calledWith(addTotalPledged, auths, pledges); - }); - }); - - describe('removeOldSupporters()', () => { - let updateAccounts: SinonStub; - let log: SinonStub; - let removeOldSupporters: RemoveOldSupporters; - let clock: SinonFakeTimers; - - const update = { - $unset: { patreon: 1, supporterDeclinedSince: 1 }, - $push: { - supporterLog: { - $each: [{ date: new Date(1234), message: 'removed supporter' }], - $slice: -10, - }, - }, - }; - - beforeEach(() => { - clock = useFakeTimers(); - clock.setSystemTime(1234); - updateAccounts = stub(); - log = stub(); - removeOldSupporters = createRemoveOldSupporters(updateAccounts, log); - }); - - afterEach(() => { - clock.restore(); - }); - - it('does nothing if list of auth and accounts are empty', async () => { - await removeOldSupporters([], []); - - assert.calledWithMatch(updateAccounts, { _id: { $in: [] } }, update); - }); - - it('unsets patreon for all accounts without corresponding auths', async () => { - const accountId = dbId(); - - await removeOldSupporters([], [account({ _id: accountId })]); - - assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update); - assert.calledWith(log, accountId.toHexString(), `removed supporter`); - }); - - it('unsets patreon for all accounts without corresponding auths (2)', async () => { - const account1Id = dbId(); - const account2Id = dbId(); - - await removeOldSupporters( - [auth({ account: Types.ObjectId(account2Id.toHexString()) })], - [account({ _id: account1Id }), account({ _id: account2Id })]); - - assert.calledWithMatch(updateAccounts, { _id: { $in: [account1Id] } }, update); - assert.calledWith(log, account1Id.toHexString(), `removed supporter`); - }); - - it('works with unassigned auths', async () => { - const accountId = dbId(); - - await removeOldSupporters( - [auth({ account: undefined })], - [account({ _id: accountId })]); - - assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update); - }); - }); - - describe('updateSupporters()', () => { - let updateAccount: SinonStub; - let log: SinonStub; - let updateSupporters: UpdateSupporters; - let clock: SinonFakeTimers; - - function push(message: string) { - return { - supporterLog: { - $each: [{ date: new Date(1234), message }], - $slice: -10, - }, - }; - } - - beforeEach(() => { - clock = useFakeTimers(); - clock.setSystemTime(1234); - updateAccount = stub(); - log = stub(); - updateSupporters = createUpdateSupporters(updateAccount, log); - }); - - afterEach(() => { - clock.restore(); - }); - - it('does nothing if list of auth and accounts are empty', async () => { - await updateSupporters([], [], [], new Date()); - - assert.notCalled(updateAccount); - }); - - it('adds patreon info to account', async () => { - const accountId = dbId(); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [], - [{ reward: rewardLevel1, user: '123', total: 0 }], - new Date()); - - assert.calledWith(updateAccount, accountId.toString(), { - patreon: PatreonFlags.Supporter1, - supporterDeclinedSince: undefined, - $push: push('added supporter (1)'), - }); - assert.calledWith(log, accountId.toString(), 'added supporter (1)'); - }); - - it('handles duplicate auths', async () => { - const accountId = dbId(); - - await updateSupporters( - [auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })], - [], - [{ reward: rewardLevel2, user: '321', total: 0 }, { reward: rewardLevel1, user: '123', total: 0 }], - new Date()); - - assert.calledWith(updateAccount, accountId.toString(), { - patreon: PatreonFlags.Supporter2, - supporterDeclinedSince: undefined, - $push: push('added supporter (2)'), - }); - assert.calledWith(log, accountId.toString(), 'added supporter (2)'); - }); - - it('handles unassigned auths', async () => { - await updateSupporters( - [auth({ account: undefined, openId: '123' })], - [], - [{ reward: rewardLevel2, user: '123', total: 0 }], - new Date()); - - assert.notCalled(updateAccount); - }); - - it('sets supporter to none if pledge is missing', async () => { - const accountId = dbId(); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ _id: accountId, patreon: PatreonFlags.Supporter1 })], - [], - new Date()); - - assert.calledWith(updateAccount, accountId.toString(), { - patreon: PatreonFlags.None, - supporterDeclinedSince: undefined, - $push: push('removed supporter'), - }); - assert.calledWith(log, accountId.toString(), 'removed supporter'); - }); - - it('updates existing info', async () => { - const accountId = dbId(); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], - [{ reward: rewardLevel2, user: '123', total: 0 }], - new Date()); - - assert.calledWith(updateAccount, accountId.toString(), { - patreon: PatreonFlags.Supporter2, - supporterDeclinedSince: undefined, - $push: push('added supporter (2)'), - }); - assert.calledWith(log, accountId.toString(), 'added supporter (2)'); - }); - - it('does not update if supporter level did not change (2 patreon accounts)', async () => { - const accountId = dbId(); - - await updateSupporters( - [auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })], - [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })], - [{ reward: rewardLevel2, user: '123', total: 0 }], - new Date()); - - assert.notCalled(updateAccount); - assert.notCalled(log); - }); - - it('does not remove support if decline is set but day of month is < 7', async () => { - const accountId = dbId(); - const now = new Date('2018-04-02T09:00:00.000Z'); - const date = fromDate(now, -1 * DAY); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], - [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], - now); - - assert.calledWith(updateAccount, accountId.toString(), { - supporterDeclinedSince: date, - }); - assert.notCalled(log); - }); - - it('removes support if decline is set and day of month is > 14', async () => { - const accountId = dbId(); - const now = new Date('2018-04-16T09:00:00.000Z'); - const date = fromDate(now, -1 * DAY); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], - [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], - now); - - assert.calledWith(updateAccount, accountId.toString(), { - patreon: PatreonFlags.None, - supporterDeclinedSince: date, - $push: push('removed supporter (declined)'), - }); - assert.calledWith(log, accountId.toString(), 'removed supporter (declined)'); - }); - - it('removes support if decline is set and day of month is < 14 and decline is > 14 days old', async () => { - const accountId = dbId(); - const now = new Date('2018-04-02T09:00:00.000Z'); - const date = fromDate(now, -16 * DAY); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], - [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], - now); - - assert.calledWith(updateAccount, accountId.toString(), { - patreon: PatreonFlags.None, - supporterDeclinedSince: date, - $push: push('removed supporter (declined)'), - }); - assert.calledWith(log, accountId.toString(), 'removed supporter (declined)'); - }); - - it('does not add log if declined but supporter is already removed', async () => { - const accountId = dbId(); - const now = new Date('2018-04-16T09:00:00.000Z'); - const date = fromDate(now, -1 * DAY); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [], - [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], - now); - - assert.calledWith(updateAccount, accountId.toString(), { - patreon: PatreonFlags.None, - supporterDeclinedSince: date, - }); - assert.notCalled(log); - }); - - it('updates declined date', async () => { - const accountId = dbId(); - const date = fromNow(-100); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], - [{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }], - new Date()); - - assert.calledWith(updateAccount, accountId.toString(), { - patreon: PatreonFlags.Supporter2, - supporterDeclinedSince: date, - $push: push('added supporter (2)'), - }); - assert.calledWith(log, accountId.toString(), 'added supporter (2)'); - }); - - it('updates declined date even if patreon is not changed', async () => { - const accountId = dbId(); - const date = fromNow(-100); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })], - [{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }], - new Date()); - - assert.calledWith(updateAccount, accountId.toString(), { - supporterDeclinedSince: date, - }); - assert.notCalled(log); - }); - - it('sets supporter to none if reward has invalid ID', async () => { - const accountId = dbId(); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], - [{ reward: 'invalid', user: '123', total: 0 }], - new Date()); - - assert.calledWith(updateAccount, accountId.toString(), { - patreon: PatreonFlags.None, - supporterDeclinedSince: undefined, - $push: push('removed supporter'), - }); - assert.calledWith(log, accountId.toString(), 'removed supporter'); - }); - - it('does nothing if patreon info is already set', async () => { - const accountId = dbId(); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], - [{ reward: rewardLevel1, user: '123', total: 0 }], - new Date()); - - assert.notCalled(updateAccount); - }); - - it('does nothing if declined date is the same', async () => { - const accountId = dbId(); - - await updateSupporters( - [auth({ account: accountId, openId: '123' })], - [account({ - _id: Types.ObjectId(accountId.toHexString()), - patreon: PatreonFlags.Supporter1, - supporterDeclinedSince: new Date(1234), - })], - [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: (new Date(1234).toISOString()) }], - new Date()); - - assert.notCalled(updateAccount); - }); - }); - - describe('addTotalPledged()', () => { - let updateAuth: SinonStub; - let addTotalPledged: AddTotalPledged; - - beforeEach(() => { - updateAuth = stub(); - addTotalPledged = createAddTotalPledged(updateAuth); - }); - - it('does nothing if lists of auth and pledges are empty', async () => { - await addTotalPledged([], []); - - assert.notCalled(updateAuth); - }); - - it('does nothing if cannot find auths for pledges', async () => { - await addTotalPledged([], [{ reward: 'some', user: '123', total: 0 }]); - - assert.notCalled(updateAuth); - }); - - it('does nothing if cannot find pledges for auth', async () => { - await addTotalPledged([auth({ openId: '123' })], []); - - assert.notCalled(updateAuth); - }); - - it('does nothing if total is already correct', async () => { - await addTotalPledged([auth({ openId: '123', pledged: 10 })], [{ reward: 'some', user: '123', total: 10 }]); - - assert.notCalled(updateAuth); - }); - - it('updates total if different', async () => { - const authId = dbId(); - - await addTotalPledged([auth({ _id: authId, openId: '123' })], [{ reward: 'some', user: '123', total: 10 }]); - - assert.calledWith(updateAuth, authId, { pledged: 10 }); - }); - }); + describe('fetchPatreonData()', () => { + let client: SinonStub; + + beforeEach(() => { + client = stub(); + client.withArgs('/current_user/campaigns') + .resolves({ rawJson: { data: [{ id: 'foo' }], included: [] } }); + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ rawJson: { data: [], links: {} } }); + }); + + it('returns rewards', async () => { + client.withArgs('/current_user/campaigns') + .resolves({ + rawJson: { + data: [{ id: 'foo', attributes: {} }], + included: [ + { type: 'reward', id: '123', attributes: { title: 'title', description: 'desc' } }, + { type: 'other', id: '111', attributes: { title: 'ttt', description: 'ddd' } }, + ], + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + expect(result.rewards).eql([ + { id: '123', title: 'title', description: 'desc' }, + ]); + }); + + it('fixes incomplete data', async () => { + client.withArgs('/current_user/campaigns') + .resolves({ + rawJson: { + data: [{ id: 'foo' }], + included: [ + { type: 'reward', id: '123', attributes: {} }, + ], + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + expect(result.rewards).eql([ + { id: '123', title: '', description: '' }, + ]); + }); + + it('returns pledges', async () => { + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ + rawJson: { + data: [ + { + attributes: { total_historical_amount_cents: 100 }, + relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, + }, + { + attributes: { total_historical_amount_cents: 200 }, + relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } }, + }, + ], + links: {}, + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + expect(result.pledges).eql([ + { user: 'patid1', reward: 'rewid1', total: 100, declinedSince: undefined }, + { user: 'patid2', reward: 'rewid2', total: 200, declinedSince: undefined }, + ]); + }); + + it('skips pledges with missing data', async () => { + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ + rawJson: { + data: [ + { + attributes: {}, + relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, + }, + { + attributes: {}, + relationships: { patron: {}, reward: { data: { id: 'rewid2' } } }, + }, + { + attributes: {}, + relationships: { patron: { data: { id: 'patid3' } }, reward: {} }, + }, + ], + links: {}, + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + expect(result.pledges).eql([ + { user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined }, + ]); + }); + + it('fetches multiple pages of pledges (with correct parameters for next page)', async () => { + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ + rawJson: { + data: [ + { + attributes: {}, + relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, + }, + ], + links: { + next: 'https://www.patreon.com/api/oauth2/api/link-to-next-page?page=5', + }, + }, + }); + client.withArgs('/link-to-next-page?page=5' + queryAdd) + .resolves({ + rawJson: { + data: [ + { + attributes: {}, + relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } }, + }, + ], + links: {}, + }, + }); + + const result = await fetchPatreonData(client as any, noop); + + assert.calledWith(client, '/link-to-next-page?page=5' + queryAdd); + expect(result.pledges).eql([ + { user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined }, + { user: 'patid2', reward: 'rewid2', total: 0, declinedSince: undefined }, + ]); + }); + + it('throws on too many pages', async () => { + client.withArgs(`/campaigns/foo/pledges?${query}`) + .resolves({ + rawJson: { + data: [ + { + attributes: {}, + relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } }, + }, + ], + links: { + next: `/campaigns/foo/pledges?${queryBase}`, + }, + }, + }); + + await expect(fetchPatreonData(client as any, noop)).rejectedWith('Exceeded 100 pages of patreon data'); + }); + }); + + describe('updatePatreonInfo()', () => { + let queryAuths: SinonStub; + let queryAccounts: SinonStub; + let removeOldSupporters: SinonStub; + let updateSupporters: SinonStub; + let addTotalPledged: SinonStub; + let updatePatreonInfo: (data: PatreonData, now: Date) => any; + + beforeEach(() => { + queryAuths = stub(); + queryAccounts = stub(); + removeOldSupporters = stub(); + updateSupporters = stub(); + addTotalPledged = stub(); + updatePatreonInfo = createUpdatePatreonInfo( + queryAuths, queryAccounts, removeOldSupporters, updateSupporters, addTotalPledged); + }); + + it('queries auths', async () => { + queryAuths.resolves([]); + queryAccounts.resolves([]); + + await updatePatreonInfo({ + pledges: [ + { reward: '123', user: 'foo', total: 0 }, + { reward: '123', user: 'bar', total: 0 }, + ], + rewards: [], + }, new Date()); + + assert.calledWith(queryAuths, match({ + provider: 'patreon', + openId: { $in: ['foo', 'bar'] }, + account: { $exists: true }, + banned: { $ne: true }, + disabled: { $ne: true }, + }), '_id account openId pledged'); + }); + + it('queries accounts', async () => { + queryAuths.resolves([]); + queryAccounts.resolves([]); + + await updatePatreonInfo({ pledges: [], rewards: [] }, new Date()); + + assert.calledWith( + queryAccounts, match({ patreon: { $exists: true, $ne: 0 } }), '_id patreon supporterDeclinedSince'); + }); + + it('removes old supporters', async () => { + const auths = [{}] as any; + const accounts = [{}] as any; + queryAuths.resolves(auths); + queryAccounts.resolves(auths); + + await updatePatreonInfo({ pledges: [], rewards: [] }, new Date()); + + assert.calledWith(removeOldSupporters, auths, accounts); + }); + + it('adds new supporters', async () => { + const auths = [{}] as any; + const accounts = [{}] as any; + const pledges = [{}] as any; + const now = new Date(); + queryAuths.resolves(auths); + queryAccounts.resolves(auths); + + await updatePatreonInfo({ pledges, rewards: [] }, now); + + assert.calledWith(updateSupporters, auths, accounts, pledges, now); + }); + + it('adds total pledged', async () => { + const auths = [{}] as any; + const pledges = [{}] as any; + queryAuths.resolves(auths); + queryAccounts.resolves(auths); + + await updatePatreonInfo({ pledges, rewards: [] }, new Date()); + + assert.calledWith(addTotalPledged, auths, pledges); + }); + }); + + describe('removeOldSupporters()', () => { + let updateAccounts: SinonStub; + let log: SinonStub; + let removeOldSupporters: RemoveOldSupporters; + let clock: SinonFakeTimers; + + const update = { + $unset: { patreon: 1, supporterDeclinedSince: 1 }, + $push: { + supporterLog: { + $each: [{ date: new Date(1234), message: 'removed supporter' }], + $slice: -10, + }, + }, + }; + + beforeEach(() => { + clock = useFakeTimers(); + clock.setSystemTime(1234); + updateAccounts = stub(); + log = stub(); + removeOldSupporters = createRemoveOldSupporters(updateAccounts, log); + }); + + afterEach(() => { + clock.restore(); + }); + + it('does nothing if list of auth and accounts are empty', async () => { + await removeOldSupporters([], []); + + assert.calledWithMatch(updateAccounts, { _id: { $in: [] } }, update); + }); + + it('unsets patreon for all accounts without corresponding auths', async () => { + const accountId = dbId(); + + await removeOldSupporters([], [account({ _id: accountId })]); + + assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update); + assert.calledWith(log, accountId.toHexString(), `removed supporter`); + }); + + it('unsets patreon for all accounts without corresponding auths (2)', async () => { + const account1Id = dbId(); + const account2Id = dbId(); + + await removeOldSupporters( + [auth({ account: Types.ObjectId(account2Id.toHexString()) })], + [account({ _id: account1Id }), account({ _id: account2Id })]); + + assert.calledWithMatch(updateAccounts, { _id: { $in: [account1Id] } }, update); + assert.calledWith(log, account1Id.toHexString(), `removed supporter`); + }); + + it('works with unassigned auths', async () => { + const accountId = dbId(); + + await removeOldSupporters( + [auth({ account: undefined })], + [account({ _id: accountId })]); + + assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update); + }); + }); + + describe('updateSupporters()', () => { + let updateAccount: SinonStub; + let log: SinonStub; + let updateSupporters: UpdateSupporters; + let clock: SinonFakeTimers; + + function push(message: string) { + return { + supporterLog: { + $each: [{ date: new Date(1234), message }], + $slice: -10, + }, + }; + } + + beforeEach(() => { + clock = useFakeTimers(); + clock.setSystemTime(1234); + updateAccount = stub(); + log = stub(); + updateSupporters = createUpdateSupporters(updateAccount, log); + }); + + afterEach(() => { + clock.restore(); + }); + + it('does nothing if list of auth and accounts are empty', async () => { + await updateSupporters([], [], [], new Date()); + + assert.notCalled(updateAccount); + }); + + it('adds patreon info to account', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [], + [{ reward: rewardLevel1, user: '123', total: 0 }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.Supporter1, + supporterDeclinedSince: undefined, + $push: push('added supporter (1)'), + }); + assert.calledWith(log, accountId.toString(), 'added supporter (1)'); + }); + + it('handles duplicate auths', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })], + [], + [{ reward: rewardLevel2, user: '321', total: 0 }, { reward: rewardLevel1, user: '123', total: 0 }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.Supporter2, + supporterDeclinedSince: undefined, + $push: push('added supporter (2)'), + }); + assert.calledWith(log, accountId.toString(), 'added supporter (2)'); + }); + + it('handles unassigned auths', async () => { + await updateSupporters( + [auth({ account: undefined, openId: '123' })], + [], + [{ reward: rewardLevel2, user: '123', total: 0 }], + new Date()); + + assert.notCalled(updateAccount); + }); + + it('sets supporter to none if pledge is missing', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: accountId, patreon: PatreonFlags.Supporter1 })], + [], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: undefined, + $push: push('removed supporter'), + }); + assert.calledWith(log, accountId.toString(), 'removed supporter'); + }); + + it('updates existing info', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel2, user: '123', total: 0 }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.Supporter2, + supporterDeclinedSince: undefined, + $push: push('added supporter (2)'), + }); + assert.calledWith(log, accountId.toString(), 'added supporter (2)'); + }); + + it('does not update if supporter level did not change (2 patreon accounts)', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })], + [{ reward: rewardLevel2, user: '123', total: 0 }], + new Date()); + + assert.notCalled(updateAccount); + assert.notCalled(log); + }); + + it('does not remove support if decline is set but day of month is < 7', async () => { + const accountId = dbId(); + const now = new Date('2018-04-02T09:00:00.000Z'); + const date = fromDate(now, -1 * DAY); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], + now); + + assert.calledWith(updateAccount, accountId.toString(), { + supporterDeclinedSince: date, + }); + assert.notCalled(log); + }); + + it('removes support if decline is set and day of month is > 14', async () => { + const accountId = dbId(); + const now = new Date('2018-04-16T09:00:00.000Z'); + const date = fromDate(now, -1 * DAY); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], + now); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: date, + $push: push('removed supporter (declined)'), + }); + assert.calledWith(log, accountId.toString(), 'removed supporter (declined)'); + }); + + it('removes support if decline is set and day of month is < 14 and decline is > 14 days old', async () => { + const accountId = dbId(); + const now = new Date('2018-04-02T09:00:00.000Z'); + const date = fromDate(now, -16 * DAY); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], + now); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: date, + $push: push('removed supporter (declined)'), + }); + assert.calledWith(log, accountId.toString(), 'removed supporter (declined)'); + }); + + it('does not add log if declined but supporter is already removed', async () => { + const accountId = dbId(); + const now = new Date('2018-04-16T09:00:00.000Z'); + const date = fromDate(now, -1 * DAY); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }], + now); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: date, + }); + assert.notCalled(log); + }); + + it('updates declined date', async () => { + const accountId = dbId(); + const date = fromNow(-100); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.Supporter2, + supporterDeclinedSince: date, + $push: push('added supporter (2)'), + }); + assert.calledWith(log, accountId.toString(), 'added supporter (2)'); + }); + + it('updates declined date even if patreon is not changed', async () => { + const accountId = dbId(); + const date = fromNow(-100); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })], + [{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + supporterDeclinedSince: date, + }); + assert.notCalled(log); + }); + + it('sets supporter to none if reward has invalid ID', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: 'invalid', user: '123', total: 0 }], + new Date()); + + assert.calledWith(updateAccount, accountId.toString(), { + patreon: PatreonFlags.None, + supporterDeclinedSince: undefined, + $push: push('removed supporter'), + }); + assert.calledWith(log, accountId.toString(), 'removed supporter'); + }); + + it('does nothing if patreon info is already set', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })], + [{ reward: rewardLevel1, user: '123', total: 0 }], + new Date()); + + assert.notCalled(updateAccount); + }); + + it('does nothing if declined date is the same', async () => { + const accountId = dbId(); + + await updateSupporters( + [auth({ account: accountId, openId: '123' })], + [account({ + _id: Types.ObjectId(accountId.toHexString()), + patreon: PatreonFlags.Supporter1, + supporterDeclinedSince: new Date(1234), + })], + [{ reward: rewardLevel1, user: '123', total: 0, declinedSince: (new Date(1234).toISOString()) }], + new Date()); + + assert.notCalled(updateAccount); + }); + }); + + describe('addTotalPledged()', () => { + let updateAuth: SinonStub; + let addTotalPledged: AddTotalPledged; + + beforeEach(() => { + updateAuth = stub(); + addTotalPledged = createAddTotalPledged(updateAuth); + }); + + it('does nothing if lists of auth and pledges are empty', async () => { + await addTotalPledged([], []); + + assert.notCalled(updateAuth); + }); + + it('does nothing if cannot find auths for pledges', async () => { + await addTotalPledged([], [{ reward: 'some', user: '123', total: 0 }]); + + assert.notCalled(updateAuth); + }); + + it('does nothing if cannot find pledges for auth', async () => { + await addTotalPledged([auth({ openId: '123' })], []); + + assert.notCalled(updateAuth); + }); + + it('does nothing if total is already correct', async () => { + await addTotalPledged([auth({ openId: '123', pledged: 10 })], [{ reward: 'some', user: '123', total: 10 }]); + + assert.notCalled(updateAuth); + }); + + it('updates total if different', async () => { + const authId = dbId(); + + await addTotalPledged([auth({ _id: authId, openId: '123' })], [{ reward: 'some', user: '123', total: 10 }]); + + assert.calledWith(updateAuth, authId, { pledged: 10 }); + }); + }); }); diff --git a/src/ts/tests/server/playerUtils.spec.ts b/src/ts/tests/server/playerUtils.spec.ts index 7836a3c..c42ffea 100644 --- a/src/ts/tests/server/playerUtils.spec.ts +++ b/src/ts/tests/server/playerUtils.spec.ts @@ -1,9 +1,9 @@ import { expect } from 'chai'; import { useFakeTimers, SinonFakeTimers, SinonStub, stub, assert } from 'sinon'; import { - createClient, createIgnorePlayer, findClientByEntityId, setEntityExpression, interactWith, - cancelEntityExpression, canPerformAction, createClientAndPony, addIgnore, turnHead, boop, stand, - sit, lie, fly, expressionAction, isIgnored, resetClientUpdates, holdItem, unholdItem, holdToy, unholdToy + createClient, createIgnorePlayer, findClientByEntityId, setEntityExpression, interactWith, + cancelEntityExpression, canPerformAction, createClientAndPony, addIgnore, turnHead, boop, stand, + sit, lie, fly, expressionAction, isIgnored, resetClientUpdates, holdItem, unholdItem, holdToy, unholdToy } from '../../server/playerUtils'; import { mockClient, clientPony, serverEntity, genObjectId, setupCollider } from '../mocks'; import { IClient } from '../../server/serverInterfaces'; @@ -24,1033 +24,1033 @@ import { sendAction } from '../../server/entityUtils'; import { updateColliders } from '../common/collision.spec'; describe('playerUtils', () => { - const def = { x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }; - - let client: IClient; - - beforeEach(() => { - client = mockClient(); - }); - - describe('isIgnored()', () => { - it('returns true if target account id is on ignored list', () => { - expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set(['foo']) } as any)).true; - }); - - it('returns false if target account id is not on ignored list', () => { - expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set(['bar']) } as any)).false; - }); - - it('returns false if account ignore list is empty', () => { - expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set() } as any)).false; - }); - - it('returns false if account does not have ignore list', () => { - expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set() } as any)).false; - }); - }); - - describe('createClientAndPony()', () => { - it('sets up client and pony', () => { - const account = { _id: genObjectId() }; - const character = { _id: genObjectId(), name: 'Foo' }; - const client = { - tokenData: { account, character }, - } as any; - const map = { spawnArea: rect(0, 0, 0, 0) }; - const world = { - getMainMap: () => map, - getMap: () => map, - isColliding: stub(), - }; - - createClientAndPony(client, [], [], { name: 'test' } as any, world as any, new CounterService(1)); - - // ... - }); - }); - - describe('createClient()', () => { - let clock: SinonFakeTimers; - - beforeEach(() => { - clock = useFakeTimers(); - clock.setSystemTime(123); - }); - - afterEach(() => { - clock.restore(); - }); - - it('initializes client fields', () => { - const originalRequest = { headers: { 'user-agent': 'test' } }; - const client = { originalRequest } as any; - const account = { _id: genObjectId(), _account: 1, name: 'Foo' } as any; - const character = { _id: genObjectId(), _character: 1, name: 'Im :apple:' } as any; - const pony = { _pony: 1, x: 10, y: 20 } as any; - const reporter = { _reporter: 1 } as any; - const origin = { ip: '', country: 'XY' }; - const map = {} as any; - - const result = createClient(client, account, [], [], character, pony, map, reporter, origin); - - expect(result).equal(client); - expect(result).eql({ - accountId: account._id.toString(), - accountName: 'Foo', - characterId: character._id.toString(), - characterName: 'Im 🍎', - ignores: new Set(), - hides: new Set(), - permaHides: new Set(), - friends: new Set(), - friendsCRC: undefined, - accountSettings: {}, - originalRequest, - supporterLevel: 0, - isMod: false, - userAgent: 'test', - reporter, - account, - character, - ip: '', - map, - isSwitchingMap: false, - pony, - notifications: [], - updateQueue: createBinaryWriter(128), - regionUpdates: [], - saysQueue: [], - unsubscribes: [], - subscribes: [], - regions: [], - camera: Object.assign(createCamera(), { w: 800, h: 600 }), - lastSays: [], - lastSwap: 0, - shadowed: false, - country: 'XY', - safeX: 10, - safeY: 20, - lastPacket: 123, - lastAction: 0, - lastBoopAction: 0, - lastExpressionAction: 0, - lastX: 10, - lastY: 20, - lastTime: 0, - lastVX: 0, - lastVY: 0, - lastCameraX: 0, - lastCameraY: 0, - lastCameraW: 0, - lastCameraH: 0, - lastMapSwitch: 0, - sitCount: 0, - lastSitX: 0, - lastSitY: 0, - lastSitTime: 0, - lastMapLoadOrSave: 0, - positions: [], - }); - }); - - it('sets shadowed field if is shadowed', () => { - const originalRequest = { headers: { 'user-agent': 'test' } }; - const client = { originalRequest } as any; - const account = { _id: genObjectId(), shadow: -1 } as any; - const character = { _id: genObjectId() } as any; - const pony = {} as any; - const reporter = {} as any; - const map = {} as any; - - const result = createClient(client, account, [], [], character, pony, map, reporter, undefined); - - expect(result.shadowed).true; - expect(result.country).equal('??'); - }); - }); - - describe('resetClientUpdates()', () => { - it('resets all queues', () => { - const client = mockClient(); - client.updateQueue.offset = 100; - client.regionUpdates.push({} as any); - client.saysQueue.push({} as any); - client.unsubscribes.push({} as any); - client.subscribes.push({} as any); - - resetClientUpdates(client); - - expect(client.updateQueue.offset).equal(0); - expect(client.regionUpdates).eql([]); - expect(client.saysQueue).eql([]); - expect(client.unsubscribes).eql([]); - expect(client.subscribes).eql([]); - }); - }); - - describe('ignorePlayer()', () => { - let ignorePlayer: (client: IClient, target: IClient, ignored: boolean) => void; - let updateAccount: SinonStub; - - beforeEach(() => { - updateAccount = stub().resolves(); - ignorePlayer = createFunctionWithPromiseHandler(createIgnorePlayer, updateAccount); - }); - - it('adds client to targets ignores list', async () => { - const client = mockClient(); - const target = mockClient(); - - await ignorePlayer(client, target, true); - - assert.calledWithMatch(updateAccount, target.accountId, { $push: { ignores: client.accountId } }); - }); - - it('removes client from targets ignores list', async () => { - const client = mockClient(); - const target = mockClient(); - addIgnore(target, client.accountId); - - await ignorePlayer(client, target, false); - - assert.calledWithMatch(updateAccount, target.accountId, { $pull: { ignores: client.accountId } }); - }); - - it('does nothing if already ignored', async () => { - const client = mockClient(); - const target = mockClient(); - addIgnore(target, client.accountId); - - await ignorePlayer(client, target, true); - - assert.notCalled(updateAccount); - }); - - it('does nothing if already unignored', async () => { - const client = mockClient(); - const target = mockClient(); - - await ignorePlayer(client, target, false); - - assert.notCalled(updateAccount); - }); - - it('does nothing if called for self', async () => { - const client = mockClient(); - - await ignorePlayer(client, client, true); - - assert.notCalled(updateAccount); - }); - - it('adds client to targets account instance ignores list', async () => { - const client = mockClient(); - const target = mockClient(); - target.account.ignores = undefined; - - await ignorePlayer(client, target, true); - - expect(target.account.ignores).eql([client.accountId]); - }); - - it('removes client from targets account instance ignores list', async () => { - const client = mockClient(); - const target = mockClient(); - addIgnore(target, client.accountId); - - await ignorePlayer(client, target, false); - - expect(target.account.ignores).eql([]); - }); - - it('sends target player state update to client', async () => { - const client = mockClient(); - const target = mockClient(); - target.pony.id = 123; - - await ignorePlayer(client, target, true); - - expect(Array.from(getWriterBuffer(client.updateQueue))) - .eql([2, 4, 0, 0, 0, 0, 123, 1]); - }); - }); + const def = { x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }; + + let client: IClient; + + beforeEach(() => { + client = mockClient(); + }); + + describe('isIgnored()', () => { + it('returns true if target account id is on ignored list', () => { + expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set(['foo']) } as any)).true; + }); + + it('returns false if target account id is not on ignored list', () => { + expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set(['bar']) } as any)).false; + }); + + it('returns false if account ignore list is empty', () => { + expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set() } as any)).false; + }); + + it('returns false if account does not have ignore list', () => { + expect(isIgnored({ accountId: 'foo' } as any, { ignores: new Set() } as any)).false; + }); + }); + + describe('createClientAndPony()', () => { + it('sets up client and pony', () => { + const account = { _id: genObjectId() }; + const character = { _id: genObjectId(), name: 'Foo' }; + const client = { + tokenData: { account, character }, + } as any; + const map = { spawnArea: rect(0, 0, 0, 0) }; + const world = { + getMainMap: () => map, + getMap: () => map, + isColliding: stub(), + }; + + createClientAndPony(client, [], [], { name: 'test' } as any, world as any, new CounterService(1)); + + // ... + }); + }); + + describe('createClient()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + clock.setSystemTime(123); + }); + + afterEach(() => { + clock.restore(); + }); + + it('initializes client fields', () => { + const originalRequest = { headers: { 'user-agent': 'test' } }; + const client = { originalRequest } as any; + const account = { _id: genObjectId(), _account: 1, name: 'Foo' } as any; + const character = { _id: genObjectId(), _character: 1, name: 'Im :apple:' } as any; + const pony = { _pony: 1, x: 10, y: 20 } as any; + const reporter = { _reporter: 1 } as any; + const origin = { ip: '', country: 'XY' }; + const map = {} as any; + + const result = createClient(client, account, [], [], character, pony, map, reporter, origin); + + expect(result).equal(client); + expect(result).eql({ + accountId: account._id.toString(), + accountName: 'Foo', + characterId: character._id.toString(), + characterName: 'Im 🍎', + ignores: new Set(), + hides: new Set(), + permaHides: new Set(), + friends: new Set(), + friendsCRC: undefined, + accountSettings: {}, + originalRequest, + supporterLevel: 0, + isMod: false, + userAgent: 'test', + reporter, + account, + character, + ip: '', + map, + isSwitchingMap: false, + pony, + notifications: [], + updateQueue: createBinaryWriter(128), + regionUpdates: [], + saysQueue: [], + unsubscribes: [], + subscribes: [], + regions: [], + camera: Object.assign(createCamera(), { w: 800, h: 600 }), + lastSays: [], + lastSwap: 0, + shadowed: false, + country: 'XY', + safeX: 10, + safeY: 20, + lastPacket: 123, + lastAction: 0, + lastBoopAction: 0, + lastExpressionAction: 0, + lastX: 10, + lastY: 20, + lastTime: 0, + lastVX: 0, + lastVY: 0, + lastCameraX: 0, + lastCameraY: 0, + lastCameraW: 0, + lastCameraH: 0, + lastMapSwitch: 0, + sitCount: 0, + lastSitX: 0, + lastSitY: 0, + lastSitTime: 0, + lastMapLoadOrSave: 0, + positions: [], + }); + }); + + it('sets shadowed field if is shadowed', () => { + const originalRequest = { headers: { 'user-agent': 'test' } }; + const client = { originalRequest } as any; + const account = { _id: genObjectId(), shadow: -1 } as any; + const character = { _id: genObjectId() } as any; + const pony = {} as any; + const reporter = {} as any; + const map = {} as any; + + const result = createClient(client, account, [], [], character, pony, map, reporter, undefined); + + expect(result.shadowed).true; + expect(result.country).equal('??'); + }); + }); + + describe('resetClientUpdates()', () => { + it('resets all queues', () => { + const client = mockClient(); + client.updateQueue.offset = 100; + client.regionUpdates.push({} as any); + client.saysQueue.push({} as any); + client.unsubscribes.push({} as any); + client.subscribes.push({} as any); + + resetClientUpdates(client); + + expect(client.updateQueue.offset).equal(0); + expect(client.regionUpdates).eql([]); + expect(client.saysQueue).eql([]); + expect(client.unsubscribes).eql([]); + expect(client.subscribes).eql([]); + }); + }); + + describe('ignorePlayer()', () => { + let ignorePlayer: (client: IClient, target: IClient, ignored: boolean) => void; + let updateAccount: SinonStub; + + beforeEach(() => { + updateAccount = stub().resolves(); + ignorePlayer = createFunctionWithPromiseHandler(createIgnorePlayer, updateAccount); + }); + + it('adds client to targets ignores list', async () => { + const client = mockClient(); + const target = mockClient(); + + await ignorePlayer(client, target, true); + + assert.calledWithMatch(updateAccount, target.accountId, { $push: { ignores: client.accountId } }); + }); + + it('removes client from targets ignores list', async () => { + const client = mockClient(); + const target = mockClient(); + addIgnore(target, client.accountId); + + await ignorePlayer(client, target, false); + + assert.calledWithMatch(updateAccount, target.accountId, { $pull: { ignores: client.accountId } }); + }); + + it('does nothing if already ignored', async () => { + const client = mockClient(); + const target = mockClient(); + addIgnore(target, client.accountId); + + await ignorePlayer(client, target, true); + + assert.notCalled(updateAccount); + }); + + it('does nothing if already unignored', async () => { + const client = mockClient(); + const target = mockClient(); + + await ignorePlayer(client, target, false); + + assert.notCalled(updateAccount); + }); + + it('does nothing if called for self', async () => { + const client = mockClient(); + + await ignorePlayer(client, client, true); + + assert.notCalled(updateAccount); + }); + + it('adds client to targets account instance ignores list', async () => { + const client = mockClient(); + const target = mockClient(); + target.account.ignores = undefined; + + await ignorePlayer(client, target, true); + + expect(target.account.ignores).eql([client.accountId]); + }); + + it('removes client from targets account instance ignores list', async () => { + const client = mockClient(); + const target = mockClient(); + addIgnore(target, client.accountId); + + await ignorePlayer(client, target, false); + + expect(target.account.ignores).eql([]); + }); + + it('sends target player state update to client', async () => { + const client = mockClient(); + const target = mockClient(); + target.pony.id = 123; + + await ignorePlayer(client, target, true); + + expect(Array.from(getWriterBuffer(client.updateQueue))) + .eql([2, 4, 0, 0, 0, 0, 123, 1]); + }); + }); - describe('findClientByEntityId()', () => { - it('returns client from selected pony', () => { - const self = mockClient(); - const client = mockClient(); - self.selected = client.pony; + describe('findClientByEntityId()', () => { + it('returns client from selected pony', () => { + const self = mockClient(); + const client = mockClient(); + self.selected = client.pony; - expect(findClientByEntityId(self, client.pony.id)).equal(client); - }); + expect(findClientByEntityId(self, client.pony.id)).equal(client); + }); - it('returns client from party clients', () => { - const self = mockClient(); - const client = mockClient(); - self.party = { - id: '', - clients: [client], - leader: client, - pending: [], - }; + it('returns client from party clients', () => { + const self = mockClient(); + const client = mockClient(); + self.party = { + id: '', + clients: [client], + leader: client, + pending: [], + }; - expect(findClientByEntityId(self, client.pony.id)).equal(client); - }); + expect(findClientByEntityId(self, client.pony.id)).equal(client); + }); - it('returns client from party pending', () => { - const self = mockClient(); - const client = mockClient(); - self.party = { - id: '', - clients: [], - leader: client, - pending: [{ client, notificationId: 0 }], - }; + it('returns client from party pending', () => { + const self = mockClient(); + const client = mockClient(); + self.party = { + id: '', + clients: [], + leader: client, + pending: [{ client, notificationId: 0 }], + }; - expect(findClientByEntityId(self, client.pony.id)).equal(client); - }); + expect(findClientByEntityId(self, client.pony.id)).equal(client); + }); - it('returns undefined if not found in party', () => { - const self = mockClient(); - const client = mockClient(); - self.party = { - id: '', - clients: [], - leader: mockClient(), - pending: [], - }; + it('returns undefined if not found in party', () => { + const self = mockClient(); + const client = mockClient(); + self.party = { + id: '', + clients: [], + leader: mockClient(), + pending: [], + }; - expect(findClientByEntityId(self, client.pony.id)).undefined; - }); + expect(findClientByEntityId(self, client.pony.id)).undefined; + }); - it('returns client from notifications', () => { - const self = mockClient(); - const client = mockClient(); - self.notifications = [ - { id: 0, name: 'name', message: '', entityId: client.pony.id, sender: client }, - ]; + it('returns client from notifications', () => { + const self = mockClient(); + const client = mockClient(); + self.notifications = [ + { id: 0, name: 'name', message: '', entityId: client.pony.id, sender: client }, + ]; - expect(findClientByEntityId(self, client.pony.id)).equal(client); - }); + expect(findClientByEntityId(self, client.pony.id)).equal(client); + }); - it('returns undefined if not found', () => { - const self = mockClient(); + it('returns undefined if not found', () => { + const self = mockClient(); - expect(findClientByEntityId(self, 1)).undefined; - }); - }); + expect(findClientByEntityId(self, 1)).undefined; + }); + }); - describe('cancelEntityExpression()', () => { - it('cancels expression', () => { - const entity = serverEntity(0); - entity.options = { expr: 1234 } as PonyOptions; - entity.exprCancellable = true; - entity.exprPermanent = decodeExpression(1111); + describe('cancelEntityExpression()', () => { + it('cancels expression', () => { + const entity = serverEntity(0); + entity.options = { expr: 1234 } as PonyOptions; + entity.exprCancellable = true; + entity.exprPermanent = decodeExpression(1111); - cancelEntityExpression(entity); + cancelEntityExpression(entity); - expect(entity.options).eql({ expr: 1111 }); - expect(entity.exprCancellable).false; - }); + expect(entity.options).eql({ expr: 1111 }); + expect(entity.exprCancellable).false; + }); - it('does nothing if entity does not have cancellable expression', () => { - const entity = serverEntity(0); - entity.options = { expr: 1234 } as PonyOptions; + it('does nothing if entity does not have cancellable expression', () => { + const entity = serverEntity(0); + entity.options = { expr: 1234 } as PonyOptions; - cancelEntityExpression(entity); + cancelEntityExpression(entity); - expect(entity.options).eql({ expr: 1234 }); - }); - }); + expect(entity.options).eql({ expr: 1234 }); + }); + }); - describe('setEntityExpression()', () => { - let clock: SinonFakeTimers; + describe('setEntityExpression()', () => { + let clock: SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers(); - }); + beforeEach(() => { + clock = useFakeTimers(); + }); - afterEach(() => { - clock.restore(); - }); + afterEach(() => { + clock.restore(); + }); - it('sets expression on pony options', () => { - const pony = clientPony(); - const expression = parseExpression(':)'); + it('sets expression on pony options', () => { + const pony = clientPony(); + const expression = parseExpression(':)'); - setEntityExpression(pony, expression); + setEntityExpression(pony, expression); - expect(pony.options!.expr).equal(encodeExpression(expression)); - }); + expect(pony.options!.expr).equal(encodeExpression(expression)); + }); - it('sets empty expression by default', () => { - const pony = clientPony(); + it('sets empty expression by default', () => { + const pony = clientPony(); - setEntityExpression(pony, undefined); + setEntityExpression(pony, undefined); - expect(pony.options!.expr).equal(encodeExpression(undefined)); - }); + expect(pony.options!.expr).equal(encodeExpression(undefined)); + }); - it('uses permanent expression if no expression provided', () => { - const pony = clientPony(); - const expression = parseExpression(':)'); - pony.exprPermanent = expression; + it('uses permanent expression if no expression provided', () => { + const pony = clientPony(); + const expression = parseExpression(':)'); + pony.exprPermanent = expression; - setEntityExpression(pony, undefined); + setEntityExpression(pony, undefined); - expect(pony.options!.expr).equal(encodeExpression(expression)); - }); + expect(pony.options!.expr).equal(encodeExpression(expression)); + }); - it('sets default expression timeout', () => { - const pony = clientPony(); - clock.setSystemTime(1234); + it('sets default expression timeout', () => { + const pony = clientPony(); + clock.setSystemTime(1234); - setEntityExpression(pony, parseExpression(':)')); + setEntityExpression(pony, parseExpression(':)')); - expect(pony.exprTimeout).equal(1234 + EXPRESSION_TIMEOUT); - }); + expect(pony.exprTimeout).equal(1234 + EXPRESSION_TIMEOUT); + }); - it('sets custom expression timeout if provided', () => { - const pony = clientPony(); - clock.setSystemTime(1234); + it('sets custom expression timeout if provided', () => { + const pony = clientPony(); + clock.setSystemTime(1234); - setEntityExpression(pony, parseExpression(':)'), 123); + setEntityExpression(pony, parseExpression(':)'), 123); - expect(pony.exprTimeout).equal(1234 + 123); - }); + expect(pony.exprTimeout).equal(1234 + 123); + }); - it('unsets expression timeout if given 0 for timeout', () => { - const pony = clientPony(); - pony.exprTimeout = 1234; + it('unsets expression timeout if given 0 for timeout', () => { + const pony = clientPony(); + pony.exprTimeout = 1234; - setEntityExpression(pony, parseExpression(':)'), 0); + setEntityExpression(pony, parseExpression(':)'), 0); - expect(pony.exprTimeout).undefined; - }); + expect(pony.exprTimeout).undefined; + }); - it('sets expression cancellable flag', () => { - const pony = clientPony(); + it('sets expression cancellable flag', () => { + const pony = clientPony(); - setEntityExpression(pony, parseExpression(':)'), 123, true); + setEntityExpression(pony, parseExpression(':)'), 123, true); - expect(pony.exprCancellable).true; - }); + expect(pony.exprCancellable).true; + }); - it('sets expression cancellable flag', () => { - const pony = clientPony(); + it('sets expression cancellable flag', () => { + const pony = clientPony(); - setEntityExpression(pony, parseExpression(':)'), 123, false); + setEntityExpression(pony, parseExpression(':)'), 123, false); - expect(pony.exprCancellable).false; - }); + expect(pony.exprCancellable).false; + }); - it('adds expression to region updates', () => { - const entity = clientPony(); - const region = createServerRegion(0, 0); - entity.region = region; + it('adds expression to region updates', () => { + const entity = clientPony(); + const region = createServerRegion(0, 0); + entity.region = region; - setEntityExpression(entity, parseExpression(':)'), 123, false); + setEntityExpression(entity, parseExpression(':)'), 123, false); - expect(region.entityUpdates).eql([ - { entity, flags: UpdateFlags.Expression, x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }, - ]); - }); + expect(region.entityUpdates).eql([ + { entity, flags: UpdateFlags.Expression, x: 0, y: 0, vx: 0, vy: 0, action: 0, playerState: 0, options: undefined }, + ]); + }); - it('sends expression update to user instead of region updates for shadowed client', () => { - const pony = clientPony(); - pony.region = createServerRegion(0, 0); - pony.client!.shadowed = true; - pony.id = 123; + it('sends expression update to user instead of region updates for shadowed client', () => { + const pony = clientPony(); + pony.region = createServerRegion(0, 0); + pony.client!.shadowed = true; + pony.id = 123; - setEntityExpression(pony, parseExpression(':)'), 123, false); + setEntityExpression(pony, parseExpression(':)'), 123, false); - expect(Array.from(getWriterBuffer(pony.client!.updateQueue))) - .eql([2, 0, 8, 0, 0, 0, 123, 0, 0, 4, 32]); - }); - }); + expect(Array.from(getWriterBuffer(pony.client!.updateQueue))) + .eql([2, 0, 8, 0, 0, 0, 123, 0, 0, 4, 32]); + }); + }); - describe('interactWith()', () => { - it('calls interact method on target', () => { - const client = mockClient(); - const interact = stub(); - const target = serverEntity(1, 0, 0, 1, { interact }); + describe('interactWith()', () => { + it('calls interact method on target', () => { + const client = mockClient(); + const interact = stub(); + const target = serverEntity(1, 0, 0, 1, { interact }); - interactWith(client, target); + interactWith(client, target); - assert.calledWith(interact, target, client); - }); + assert.calledWith(interact, target, client); + }); - it('calls interact method on target if within range', () => { - const client = mockClient(); - const interact = stub(); - const target = serverEntity(1, 10, 10, 1, { interact, interactRange: 5 }); - client.pony.x = 9; - client.pony.y = 11; + it('calls interact method on target if within range', () => { + const client = mockClient(); + const interact = stub(); + const target = serverEntity(1, 10, 10, 1, { interact, interactRange: 5 }); + client.pony.x = 9; + client.pony.y = 11; - interactWith(client, target); + interactWith(client, target); - assert.calledWith(interact, target, client); - }); + assert.calledWith(interact, target, client); + }); - it('does not call interact if out of range', () => { - const client = mockClient(); - const interact = stub(); - const target = serverEntity(1, 10, 10, 1, { interact, interactRange: 5 }); - client.pony.x = 2; - client.pony.y = 1; + it('does not call interact if out of range', () => { + const client = mockClient(); + const interact = stub(); + const target = serverEntity(1, 10, 10, 1, { interact, interactRange: 5 }); + client.pony.x = 2; + client.pony.y = 1; - interactWith(client, target); + interactWith(client, target); - assert.notCalled(interact); - }); + assert.notCalled(interact); + }); - it('does nothing for undefined entity', () => { - interactWith(mockClient(), undefined); - }); + it('does nothing for undefined entity', () => { + interactWith(mockClient(), undefined); + }); - it('does nothing for entity without interact', () => { - interactWith(mockClient(), serverEntity(1)); - }); - }); + it('does nothing for entity without interact', () => { + interactWith(mockClient(), serverEntity(1)); + }); + }); - describe('canPerformAction()', () => { - it('returns true if last action date is below current time', () => { - expect(canPerformAction(mockClient({ lastAction: 1234 }))).true; - }); + describe('canPerformAction()', () => { + it('returns true if last action date is below current time', () => { + expect(canPerformAction(mockClient({ lastAction: 1234 }))).true; + }); - it('returns false if last action date is ahead or current time', () => { - expect(canPerformAction(mockClient({ lastAction: Date.now() + 1000 }))).false; - }); - }); + it('returns false if last action date is ahead or current time', () => { + expect(canPerformAction(mockClient({ lastAction: Date.now() + 1000 }))).false; + }); + }); - describe('sendAction()', () => { - it('adds action to region', () => { - const entity = serverEntity(1); - const region = createServerRegion(0, 0); - entity.region = region; + describe('sendAction()', () => { + it('adds action to region', () => { + const entity = serverEntity(1); + const region = createServerRegion(0, 0); + entity.region = region; - sendAction(entity, Action.Boop); + sendAction(entity, Action.Boop); - expect(region.entityUpdates).eql([ - { - entity, flags: UpdateFlags.Action, x: 0, y: 0, vx: 0, vy: 0, action: Action.Boop, - playerState: 0, options: undefined, - }, - ]); - }); + expect(region.entityUpdates).eql([ + { + entity, flags: UpdateFlags.Action, x: 0, y: 0, vx: 0, vy: 0, action: Action.Boop, + playerState: 0, options: undefined, + }, + ]); + }); - it('sends only to entity client if shadowed', () => { - const client = mockClient(); - client.shadowed = true; - const entity = client.pony; - const region = createServerRegion(0, 0); - entity.region = region; - entity.region.clients.push(mockClient(), client); - client.pony.id = 123; + it('sends only to entity client if shadowed', () => { + const client = mockClient(); + client.shadowed = true; + const entity = client.pony; + const region = createServerRegion(0, 0); + entity.region = region; + entity.region.clients.push(mockClient(), client); + client.pony.id = 123; - sendAction(entity, Action.Boop); + sendAction(entity, Action.Boop); - expect(Array.from(getWriterBuffer(client.updateQueue))).eql([2, 0, 128, 0, 0, 0, 123, 1]); - expect(region.entityUpdates).eql([]); - }); - }); + expect(Array.from(getWriterBuffer(client.updateQueue))).eql([2, 0, 128, 0, 0, 0, 123, 1]); + expect(region.entityUpdates).eql([]); + }); + }); - describe('boop()', () => { - let client: IClient; + describe('boop()', () => { + let client: IClient; - beforeEach(() => { - client = mockClient(); - client.map = createServerMap('foo', 0, 1, 1); - client.pony.region = client.map.regions[0]; - client.lastAction = 0; - }); + beforeEach(() => { + client = mockClient(); + client.map = createServerMap('foo', 0, 1, 1); + client.pony.region = client.map.regions[0]; + client.lastAction = 0; + }); - it('sends boop action', () => { - boop(client, 1000); + it('sends boop action', () => { + boop(client, 1000); - expect(client.pony.region!.entityUpdates).eql([ - { - entity: client.pony, flags: UpdateFlags.Action, x: 0, y: 0, vx: 0, vy: 0, action: Action.Boop, - playerState: 0, options: undefined, - }, - ]); - }); + expect(client.pony.region!.entityUpdates).eql([ + { + entity: client.pony, flags: UpdateFlags.Action, x: 0, y: 0, vx: 0, vy: 0, action: Action.Boop, + playerState: 0, options: undefined, + }, + ]); + }); - it('cancels expression', () => { - client.pony.exprCancellable = true; - client.pony.options!.expr = 123; + it('cancels expression', () => { + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; - boop(client, 1000); + boop(client, 1000); - expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); - }); + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + }); - it('updates last boop action', () => { - client.lastBoopAction = 0; + it('updates last boop action', () => { + client.lastBoopAction = 0; - boop(client, 100); + boop(client, 100); - expect(client.lastBoopAction).equal(100 + 500); - }); + expect(client.lastBoopAction).equal(100 + 500); + }); - it('executes boop on found entity', () => { - const boop = stub(); - client.pony.x = 5; - client.pony.y = 5; - getRegion(client.map, 0, 0).entities.push(serverEntity(0, 4.2, 5, 0, { boop })); + it('executes boop on found entity', () => { + const boop = stub(); + client.pony.x = 5; + client.pony.y = 5; + getRegion(client.map, 0, 0).entities.push(serverEntity(0, 4.2, 5, 0, { boop })); - boop(client); + boop(client); - assert.calledWith(boop, client); - }); + assert.calledWith(boop, client); + }); - it('does not execute boop on found entity if shadowed', () => { - const stubBoop = stub(); - client.pony.x = 5; - client.pony.y = 5; - client.shadowed = true; - getRegion(client.map, 0, 0).entities.push(serverEntity(0, 4.2, 5, 0, { boop: stubBoop })); + it('does not execute boop on found entity if shadowed', () => { + const stubBoop = stub(); + client.pony.x = 5; + client.pony.y = 5; + client.shadowed = true; + getRegion(client.map, 0, 0).entities.push(serverEntity(0, 4.2, 5, 0, { boop: stubBoop })); - boop(client, 0); + boop(client, 0); - assert.notCalled(stubBoop); - }); + assert.notCalled(stubBoop); + }); - it('does nothing if cannot perform action', () => { - client.lastAction = 1000; + it('does nothing if cannot perform action', () => { + client.lastAction = 1000; - boop(client, 0); + boop(client, 0); - expect(client.pony.region!.entityUpdates).eql([]); - }); + expect(client.pony.region!.entityUpdates).eql([]); + }); - it('does nothing if moving', () => { - client.pony.vx = 1; + it('does nothing if moving', () => { + client.pony.vx = 1; - boop(client, 0); + boop(client, 0); - expect(client.pony.region!.entityUpdates).eql([]); - }); - }); + expect(client.pony.region!.entityUpdates).eql([]); + }); + }); - describe('turnHead()', () => { - it('updates HeadTurned flag', () => { - const client = mockClient(); - client.pony.state = 0; + describe('turnHead()', () => { + it('updates HeadTurned flag', () => { + const client = mockClient(); + client.pony.state = 0; - turnHead(client); + turnHead(client); - expect(client.pony.state).equal(EntityState.HeadTurned); - }); + expect(client.pony.state).equal(EntityState.HeadTurned); + }); - it('does not update flags if cannot perform action', () => { - const client = mockClient(); - client.lastAction = Date.now() + 1000; - client.pony.state = 0; + it('does not update flags if cannot perform action', () => { + const client = mockClient(); + client.lastAction = Date.now() + 1000; + client.pony.state = 0; - turnHead(client); + turnHead(client); - expect(client.pony.state).equal(0); - }); - }); + expect(client.pony.state).equal(0); + }); + }); - describe('stand()', () => { - beforeEach(() => { - client.pony.exprCancellable = true; - client.pony.options!.expr = 123; - }); + describe('stand()', () => { + beforeEach(() => { + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; + }); - it('updates entity flag to standing', () => { - client.pony.state = EntityState.PonySitting; + it('updates entity flag to standing', () => { + client.pony.state = EntityState.PonySitting; - stand(client); + stand(client); - expect(client.pony.state).equal(EntityState.PonyStanding); - }); + expect(client.pony.state).equal(EntityState.PonyStanding); + }); - it('does not change other entity flags', () => { - client.pony.state = EntityState.PonySitting | EntityState.FacingRight; + it('does not change other entity flags', () => { + client.pony.state = EntityState.PonySitting | EntityState.FacingRight; - stand(client); + stand(client); - expect(client.pony.state).equal(EntityState.PonyStanding | EntityState.FacingRight); - }); + expect(client.pony.state).equal(EntityState.PonyStanding | EntityState.FacingRight); + }); - it('cancels expression', () => { - client.pony.state = EntityState.PonySitting; + it('cancels expression', () => { + client.pony.state = EntityState.PonySitting; - stand(client); + stand(client); - expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); - }); + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + }); - it('does not cancel expression if transitioning from flying', () => { - client.pony.state = EntityState.PonyFlying; + it('does not cancel expression if transitioning from flying', () => { + client.pony.state = EntityState.PonyFlying; - stand(client); + stand(client); - expect(client.pony.options!.expr).equal(123); - }); + expect(client.pony.options!.expr).equal(123); + }); - it('does nothing if already standing', () => { - client.pony.state = EntityState.PonyStanding; + it('does nothing if already standing', () => { + client.pony.state = EntityState.PonyStanding; - stand(client); + stand(client); - expect(client.pony.state).equal(EntityState.PonyStanding); - expect(client.pony.options!.expr).equal(123); - }); + expect(client.pony.state).equal(EntityState.PonyStanding); + expect(client.pony.options!.expr).equal(123); + }); - it('does nothing if cannot perform action', () => { - client.lastAction = Date.now() + 1000; - client.pony.state = 0; + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = 0; - stand(client); + stand(client); - expect(client.pony.state).equal(0); - expect(client.pony.options!.expr).equal(123); - }); + expect(client.pony.state).equal(0); + expect(client.pony.options!.expr).equal(123); + }); - it('does nothing if cannot land', () => { - client.pony.state = EntityState.PonyFlying; - client.pony.x = 0.5; - client.pony.y = 0.5; - setupCollider(client.map, 0.5, 0.5); - updateColliders(client.map); + it('does nothing if cannot land', () => { + client.pony.state = EntityState.PonyFlying; + client.pony.x = 0.5; + client.pony.y = 0.5; + setupCollider(client.map, 0.5, 0.5); + updateColliders(client.map); - stand(client); + stand(client); - expect(client.pony.state).equal(EntityState.PonyFlying); - expect(client.pony.options!.expr).equal(123); - }); - }); + expect(client.pony.state).equal(EntityState.PonyFlying); + expect(client.pony.options!.expr).equal(123); + }); + }); - describe('sit()', () => { - it('updates entity flag to sitting', () => { - client.pony.state = EntityState.PonyStanding; + describe('sit()', () => { + it('updates entity flag to sitting', () => { + client.pony.state = EntityState.PonyStanding; - sit(client, {}); + sit(client, {}); - expect(client.pony.state).equal(EntityState.PonySitting); - }); + expect(client.pony.state).equal(EntityState.PonySitting); + }); - it('does not change other entity flags', () => { - client.pony.state = EntityState.PonyStanding | EntityState.FacingRight; + it('does not change other entity flags', () => { + client.pony.state = EntityState.PonyStanding | EntityState.FacingRight; - sit(client, {}); + sit(client, {}); - expect(client.pony.state).equal(EntityState.PonySitting | EntityState.FacingRight); - }); + expect(client.pony.state).equal(EntityState.PonySitting | EntityState.FacingRight); + }); - it('does nothing if already sitting', () => { - client.pony.state = EntityState.PonySitting; + it('does nothing if already sitting', () => { + client.pony.state = EntityState.PonySitting; - sit(client, {}); + sit(client, {}); - expect(client.pony.state).equal(EntityState.PonySitting); - }); + expect(client.pony.state).equal(EntityState.PonySitting); + }); - it('does nothing if cannot perform action', () => { - client.lastAction = Date.now() + 1000; - client.pony.state = 0; + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = 0; - sit(client, {}); + sit(client, {}); - expect(client.pony.state).equal(0); - }); + expect(client.pony.state).equal(0); + }); - it('does nothing if moving', () => { - client.pony.vx = 1; - client.pony.state = 0; + it('does nothing if moving', () => { + client.pony.vx = 1; + client.pony.state = 0; - sit(client, {}); + sit(client, {}); - expect(client.pony.state).equal(0); - }); - }); + expect(client.pony.state).equal(0); + }); + }); - describe('lie()', () => { - it('updates entity flag to lying', () => { - client.pony.state = EntityState.PonyStanding; + describe('lie()', () => { + it('updates entity flag to lying', () => { + client.pony.state = EntityState.PonyStanding; - lie(client); + lie(client); - expect(client.pony.state).equal(EntityState.PonyLying); - }); + expect(client.pony.state).equal(EntityState.PonyLying); + }); - it('does not change other entity flags', () => { - client.pony.state = EntityState.PonyStanding | EntityState.FacingRight; + it('does not change other entity flags', () => { + client.pony.state = EntityState.PonyStanding | EntityState.FacingRight; - lie(client); + lie(client); - expect(client.pony.state).equal(EntityState.PonyLying | EntityState.FacingRight); - }); + expect(client.pony.state).equal(EntityState.PonyLying | EntityState.FacingRight); + }); - it('does nothing if already lying', () => { - client.pony.state = EntityState.PonyLying; + it('does nothing if already lying', () => { + client.pony.state = EntityState.PonyLying; - lie(client); + lie(client); - expect(client.pony.state).equal(EntityState.PonyLying); - }); + expect(client.pony.state).equal(EntityState.PonyLying); + }); - it('does nothing if cannot perform action', () => { - client.lastAction = Date.now() + 1000; - client.pony.state = 0; + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = 0; - lie(client); + lie(client); - expect(client.pony.state).equal(0); - }); + expect(client.pony.state).equal(0); + }); - it('does nothing if moving', () => { - client.pony.vx = 1; - client.pony.state = 0; + it('does nothing if moving', () => { + client.pony.vx = 1; + client.pony.state = 0; - lie(client); + lie(client); - expect(client.pony.state).equal(0); - }); - }); + expect(client.pony.state).equal(0); + }); + }); - describe('fly()', () => { - beforeEach(() => { - client.pony.canFly = true; - client.pony.exprCancellable = true; - client.pony.options!.expr = 123; - }); + describe('fly()', () => { + beforeEach(() => { + client.pony.canFly = true; + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; + }); - it('updates entity flag to flying', () => { - client.pony.state = EntityState.PonyStanding; + it('updates entity flag to flying', () => { + client.pony.state = EntityState.PonyStanding; - fly(client); + fly(client); - expect(client.pony.state).equal(EntityState.PonyFlying | EntityState.Flying); - }); + expect(client.pony.state).equal(EntityState.PonyFlying | EntityState.Flying); + }); - it('does not change other entity flags', () => { - client.pony.state = EntityState.PonyStanding | EntityState.FacingRight; + it('does not change other entity flags', () => { + client.pony.state = EntityState.PonyStanding | EntityState.FacingRight; - fly(client); + fly(client); - expect(client.pony.state).equal(EntityState.PonyFlying | EntityState.FacingRight | EntityState.Flying); - }); + expect(client.pony.state).equal(EntityState.PonyFlying | EntityState.FacingRight | EntityState.Flying); + }); - it('cancels expression', () => { - fly(client); + it('cancels expression', () => { + fly(client); - expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); - }); + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + }); - it('does nothing if already flying', () => { - client.lastAction = Date.now() + 1000; - client.pony.state = EntityState.PonyFlying; + it('does nothing if already flying', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = EntityState.PonyFlying; - fly(client); + fly(client); - expect(client.pony.state).equal(EntityState.PonyFlying); - expect(client.pony.options!.expr).equal(123); - }); + expect(client.pony.state).equal(EntityState.PonyFlying); + expect(client.pony.options!.expr).equal(123); + }); - it('does nothing if cannot perform action', () => { - client.lastAction = Date.now() + 1000; - client.pony.state = 0; + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; + client.pony.state = 0; - fly(client); + fly(client); - expect(client.pony.state).equal(0); - expect(client.pony.options!.expr).equal(123); - }); + expect(client.pony.state).equal(0); + expect(client.pony.options!.expr).equal(123); + }); - it('does nothing if cannot fly', () => { - client.pony.canFly = false; - client.pony.state = 0; + it('does nothing if cannot fly', () => { + client.pony.canFly = false; + client.pony.state = 0; - fly(client); + fly(client); - expect(client.pony.state).equal(0); - expect(client.pony.options!.expr).equal(123); - }); - }); + expect(client.pony.state).equal(0); + expect(client.pony.options!.expr).equal(123); + }); + }); - describe('expressionAction()', () => { - beforeEach(() => { - client.pony.region = createServerRegion(1, 1); - client.pony.exprCancellable = true; - client.pony.options!.expr = 123; - }); + describe('expressionAction()', () => { + beforeEach(() => { + client.pony.region = createServerRegion(1, 1); + client.pony.exprCancellable = true; + client.pony.options!.expr = 123; + }); - it('sends given action', () => { - expressionAction(client, Action.Yawn); + it('sends given action', () => { + expressionAction(client, Action.Yawn); - expect(client.pony.region!.entityUpdates).eql([ - { ...def, entity: client.pony, flags: UpdateFlags.Expression | UpdateFlags.Action, action: Action.Yawn }, - ]); - }); + expect(client.pony.region!.entityUpdates).eql([ + { ...def, entity: client.pony, flags: UpdateFlags.Expression | UpdateFlags.Action, action: Action.Yawn }, + ]); + }); - it('cancels expression', () => { - expressionAction(client, Action.Yawn); + it('cancels expression', () => { + expressionAction(client, Action.Yawn); - expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); - }); + expect(client.pony.options!.expr).equal(EMPTY_EXPRESSION); + }); - it('does nothing if cannot perform action', () => { - client.lastAction = Date.now() + 1000; + it('does nothing if cannot perform action', () => { + client.lastAction = Date.now() + 1000; - expressionAction(client, Action.Yawn); + expressionAction(client, Action.Yawn); - expect(client.pony.region!.entityUpdates).eql([]); - }); + expect(client.pony.region!.entityUpdates).eql([]); + }); - it('does nothing if not expression action', () => { - client.pony.canFly = false; + it('does nothing if not expression action', () => { + client.pony.canFly = false; - expressionAction(client, Action.Boop); + expressionAction(client, Action.Boop); - expect(client.pony.region!.entityUpdates).eql([]); - }); + expect(client.pony.region!.entityUpdates).eql([]); + }); - it('updates last expression action', () => { - client.lastExpressionAction = 0; + it('updates last expression action', () => { + client.lastExpressionAction = 0; - expressionAction(client, Action.Yawn); + expressionAction(client, Action.Yawn); - expect(client.lastExpressionAction).greaterThan(Date.now()); - }); - }); + expect(client.lastExpressionAction).greaterThan(Date.now()); + }); + }); - describe('holdItem()', () => { - it('updates entity options', () => { - const entity = serverEntity(123); + describe('holdItem()', () => { + it('updates entity options', () => { + const entity = serverEntity(123); - holdItem(entity, 456); + holdItem(entity, 456); - expect(entity.options).eql({ hold: 456 }); - }); + expect(entity.options).eql({ hold: 456 }); + }); - it('sends entity update', () => { - const entity = serverEntity(123); - const region = createServerRegion(1, 1); - entity.region = region; + it('sends entity update', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; - holdItem(entity, 456); + holdItem(entity, 456); - expect(region.entityUpdates).eql([ - { ...def, entity, flags: UpdateFlags.Options, options: { hold: 456 } }, - ]); - }); + expect(region.entityUpdates).eql([ + { ...def, entity, flags: UpdateFlags.Options, options: { hold: 456 } }, + ]); + }); - it('does not send entity update if hold is already set', () => { - const entity = serverEntity(123); - const region = createServerRegion(1, 1); - entity.region = region; - entity.options = { hold: 456 }; + it('does not send entity update if hold is already set', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + entity.options = { hold: 456 }; - holdItem(entity, 456); + holdItem(entity, 456); - expect(region.entityUpdates).eql([]); - }); - }); + expect(region.entityUpdates).eql([]); + }); + }); - describe('unholdItem()', () => { - it('updates entity options', () => { - const entity = serverEntity(123); - entity.options = { hold: 456 }; + describe('unholdItem()', () => { + it('updates entity options', () => { + const entity = serverEntity(123); + entity.options = { hold: 456 }; - unholdItem(entity); + unholdItem(entity); - expect(entity.options).eql({}); - }); + expect(entity.options).eql({}); + }); - it('sends entity update', () => { - const entity = serverEntity(123); - const region = createServerRegion(1, 1); - entity.region = region; - entity.options = { hold: 456 }; + it('sends entity update', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + entity.options = { hold: 456 }; - unholdItem(entity); + unholdItem(entity); - expect(region.entityUpdates).eql([ - { ...def, entity, flags: UpdateFlags.Options, options: { hold: 0 } }, - ]); - }); + expect(region.entityUpdates).eql([ + { ...def, entity, flags: UpdateFlags.Options, options: { hold: 0 } }, + ]); + }); - it('does not send entity update if hold is not set', () => { - const entity = serverEntity(123); - const region = createServerRegion(1, 1); - entity.region = region; + it('does not send entity update if hold is not set', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; - unholdItem(entity); + unholdItem(entity); - expect(region.entityUpdates).eql([]); - }); - }); + expect(region.entityUpdates).eql([]); + }); + }); - describe('holdToy()', () => { - it('updates entity options', () => { - const entity = serverEntity(123); + describe('holdToy()', () => { + it('updates entity options', () => { + const entity = serverEntity(123); - holdToy(entity, 456); + holdToy(entity, 456); - expect(entity.options).eql({ toy: 456 }); - }); + expect(entity.options).eql({ toy: 456 }); + }); - it('sends entity update', () => { - const entity = serverEntity(123); - const region = createServerRegion(1, 1); - entity.region = region; + it('sends entity update', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; - holdToy(entity, 456); + holdToy(entity, 456); - expect(region.entityUpdates).eql([ - { ...def, entity, flags: UpdateFlags.Options, options: { toy: 456 } }, - ]); - }); + expect(region.entityUpdates).eql([ + { ...def, entity, flags: UpdateFlags.Options, options: { toy: 456 } }, + ]); + }); - it('does not send entity update if hold is already set', () => { - const entity = serverEntity(123); - const region = createServerRegion(1, 1); - entity.region = region; - entity.options = { toy: 456 }; + it('does not send entity update if hold is already set', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + entity.options = { toy: 456 }; - holdToy(entity, 456); + holdToy(entity, 456); - expect(region.entityUpdates).eql([]); - }); - }); + expect(region.entityUpdates).eql([]); + }); + }); - describe('unholdToy()', () => { - it('updates entity options', () => { - const entity = serverEntity(123); - entity.options = { toy: 456 }; + describe('unholdToy()', () => { + it('updates entity options', () => { + const entity = serverEntity(123); + entity.options = { toy: 456 }; - unholdToy(entity); + unholdToy(entity); - expect(entity.options).eql({}); - }); + expect(entity.options).eql({}); + }); - it('sends entity update', () => { - const entity = serverEntity(123); - const region = createServerRegion(1, 1); - entity.region = region; - entity.options = { toy: 456 }; + it('sends entity update', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; + entity.options = { toy: 456 }; - unholdToy(entity); + unholdToy(entity); - expect(region.entityUpdates).eql([ - { ...def, entity, flags: UpdateFlags.Options, options: { toy: 0 } }, - ]); - }); + expect(region.entityUpdates).eql([ + { ...def, entity, flags: UpdateFlags.Options, options: { toy: 0 } }, + ]); + }); - it('does not send entity update if toy is not set', () => { - const entity = serverEntity(123); - const region = createServerRegion(1, 1); - entity.region = region; + it('does not send entity update if toy is not set', () => { + const entity = serverEntity(123); + const region = createServerRegion(1, 1); + entity.region = region; - unholdToy(entity); + unholdToy(entity); - expect(region.entityUpdates).eql([]); - }); - }); + expect(region.entityUpdates).eql([]); + }); + }); }); diff --git a/src/ts/tests/server/regionUtils.spec.ts b/src/ts/tests/server/regionUtils.spec.ts index bfa73f9..15f4683 100644 --- a/src/ts/tests/server/regionUtils.spec.ts +++ b/src/ts/tests/server/regionUtils.spec.ts @@ -2,9 +2,9 @@ import '../lib'; import { expect } from 'chai'; import { stub, assert } from 'sinon'; import { - addToRegion, removeFromRegion, commitRegionUpdates, transferToRegion, isSubscribedToRegion, - getExpectedRegion, unsubscribeFromOutOfRangeRegions, subscribeToRegionsInRange, unsubscribeFromAllRegions, - updateRegions + addToRegion, removeFromRegion, commitRegionUpdates, transferToRegion, isSubscribedToRegion, + getExpectedRegion, unsubscribeFromOutOfRangeRegions, subscribeToRegionsInRange, unsubscribeFromAllRegions, + updateRegions } from '../../server/regionUtils'; import { IClient, ServerRegion } from '../../server/serverInterfaces'; import { addEntityToRegion, createServerRegion } from '../../server/serverRegion'; @@ -14,376 +14,376 @@ import { getRegion } from '../../common/worldMap'; import { EntityFlags } from '../../common/interfaces'; describe('regionUtils', () => { - let client: IClient; - let region: ServerRegion; + let client: IClient; + let region: ServerRegion; - beforeEach(() => { - client = mockClient(); - region = createServerRegion(2, 3); - }); - - describe('getExpectedRegion()', () => { - it('returns only available region on the map', () => { - const map = createServerMap('', 0, 1, 1); - const entity = serverEntity(1); - - expect(getExpectedRegion(entity, map)).equal(map.regions[0]); - }); - - it('returns region at location if entity region is undefined', () => { - const map = createServerMap('', 0, 10, 10); - const entity = serverEntity(1, 8 * 5, 8 * 5); - - expect(getExpectedRegion(entity, map)).equal(getRegion(map, 5, 5)); - }); - - it('returns current region if entity is inside current region', () => { - const map = createServerMap('', 0, 10, 10); - const entity = serverEntity(1, 8 * 5, 8 * 5); - entity.region = getRegion(map, 5, 5); - - expect(getExpectedRegion(entity, map)).equal(getRegion(map, 5, 5)); - }); - - it('returns edge region if entity is outside the map', () => { - const map = createServerMap('', 0, 10, 10); - const entity = serverEntity(1, 10000, 8 * 5); - - expect(getExpectedRegion(entity, map)).equal(getRegion(map, 9, 5)); - }); - - it('returns the same region if entity is outside region but inside region border', () => { - const map = createServerMap('', 0, 10, 10); - const entity = serverEntity(1, 8 * 5 + 0.1, 8 * 5, 1, { flags: EntityFlags.Movable }); - entity.region = getRegion(map, 5, 5); - - expect(getExpectedRegion(entity, map)).equal(getRegion(map, 5, 5)); - }); - }); - - describe('subscribeToRegions()', () => { - it('subscribes to regions that are in camera view', () => { - const map = createServerMap('', 0, 2, 1); - Object.assign(client.camera, { x: -10, y: 0, w: 5, h: 5 }); - client.map = map; - - subscribeToRegionsInRange(client); - - expect(client.subscribes).eql([ - new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0]), - ]); - }); - }); - - describe('unsubscribeFromRegions()', () => { - it('adds unsubscribes to client for regions that are not in camera view', () => { - const region1 = createServerRegion(1, 1); - const region2 = createServerRegion(1, 2); - client.regions = [region1, region2]; - Object.assign(client.camera, { x: -10, y: 0, w: 5, h: 5 }); + beforeEach(() => { + client = mockClient(); + region = createServerRegion(2, 3); + }); + + describe('getExpectedRegion()', () => { + it('returns only available region on the map', () => { + const map = createServerMap('', 0, 1, 1); + const entity = serverEntity(1); + + expect(getExpectedRegion(entity, map)).equal(map.regions[0]); + }); + + it('returns region at location if entity region is undefined', () => { + const map = createServerMap('', 0, 10, 10); + const entity = serverEntity(1, 8 * 5, 8 * 5); + + expect(getExpectedRegion(entity, map)).equal(getRegion(map, 5, 5)); + }); + + it('returns current region if entity is inside current region', () => { + const map = createServerMap('', 0, 10, 10); + const entity = serverEntity(1, 8 * 5, 8 * 5); + entity.region = getRegion(map, 5, 5); + + expect(getExpectedRegion(entity, map)).equal(getRegion(map, 5, 5)); + }); + + it('returns edge region if entity is outside the map', () => { + const map = createServerMap('', 0, 10, 10); + const entity = serverEntity(1, 10000, 8 * 5); + + expect(getExpectedRegion(entity, map)).equal(getRegion(map, 9, 5)); + }); + + it('returns the same region if entity is outside region but inside region border', () => { + const map = createServerMap('', 0, 10, 10); + const entity = serverEntity(1, 8 * 5 + 0.1, 8 * 5, 1, { flags: EntityFlags.Movable }); + entity.region = getRegion(map, 5, 5); + + expect(getExpectedRegion(entity, map)).equal(getRegion(map, 5, 5)); + }); + }); + + describe('subscribeToRegions()', () => { + it('subscribes to regions that are in camera view', () => { + const map = createServerMap('', 0, 2, 1); + Object.assign(client.camera, { x: -10, y: 0, w: 5, h: 5 }); + client.map = map; + + subscribeToRegionsInRange(client); + + expect(client.subscribes).eql([ + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0]), + ]); + }); + }); + + describe('unsubscribeFromRegions()', () => { + it('adds unsubscribes to client for regions that are not in camera view', () => { + const region1 = createServerRegion(1, 1); + const region2 = createServerRegion(1, 2); + client.regions = [region1, region2]; + Object.assign(client.camera, { x: -10, y: 0, w: 5, h: 5 }); - unsubscribeFromOutOfRangeRegions(client); + unsubscribeFromOutOfRangeRegions(client); - expect(client.unsubscribes).eql([1, 2]); - }); - }); + expect(client.unsubscribes).eql([1, 2]); + }); + }); - describe('updateRegions()', () => { - it('does nothing for empty map', () => { - const map = createServerMap('', 0, 1, 1); + describe('updateRegions()', () => { + it('does nothing for empty map', () => { + const map = createServerMap('', 0, 1, 1); - updateRegions([map]); - }); + updateRegions([map]); + }); - it('does nothing if no regions are changed', () => { - const map = createServerMap('', 0, 3, 3); - const entity = serverEntity(1, 5, 5); - addEntityToRegion(getRegion(map, 0, 0), entity, map); - const region = getRegion(map, 0, 0); - entity.region = region; + it('does nothing if no regions are changed', () => { + const map = createServerMap('', 0, 3, 3); + const entity = serverEntity(1, 5, 5); + addEntityToRegion(getRegion(map, 0, 0), entity, map); + const region = getRegion(map, 0, 0); + entity.region = region; - updateRegions([map]); + updateRegions([map]); - expect(entity.region).equal(region); - }); + expect(entity.region).equal(region); + }); - it('transfers entity to another region', () => { - const map = createServerMap('', 0, 3, 3); - const entity = serverEntity(1, 15, 15); - entity.flags |= EntityFlags.Movable; - addEntityToRegion(getRegion(map, 0, 0), entity, map); - entity.region = getRegion(map, 0, 0); + it('transfers entity to another region', () => { + const map = createServerMap('', 0, 3, 3); + const entity = serverEntity(1, 15, 15); + entity.flags |= EntityFlags.Movable; + addEntityToRegion(getRegion(map, 0, 0), entity, map); + entity.region = getRegion(map, 0, 0); - updateRegions([map]); + updateRegions([map]); - expect(entity.region).equal(getRegion(map, 1, 1)); - }); - }); + expect(entity.region).equal(getRegion(map, 1, 1)); + }); + }); - describe('commitRegionUpdates()', () => { - it('creates update packets', () => { - const client1 = mockClient(); - const client2 = mockClient(); - region.clients.push(client1, client2); - const entityUpdates = [] as any; - const entityRemoves = [{} as any]; - const tileUpdates = [[] as any]; - region.x = 5; - region.y = 6; - region.entityUpdates = entityUpdates; - region.entityRemoves = entityRemoves; - region.tileUpdates = tileUpdates; + describe('commitRegionUpdates()', () => { + it('creates update packets', () => { + const client1 = mockClient(); + const client2 = mockClient(); + region.clients.push(client1, client2); + const entityUpdates = [] as any; + const entityRemoves = [{} as any]; + const tileUpdates = [[] as any]; + region.x = 5; + region.y = 6; + region.entityUpdates = entityUpdates; + region.entityRemoves = entityRemoves; + region.tileUpdates = tileUpdates; - commitRegionUpdates([region]); + commitRegionUpdates([region]); - expect(client1.regionUpdates.length).equal(1); - expect(client2.regionUpdates.length).equal(1); - }); + expect(client1.regionUpdates.length).equal(1); + expect(client2.regionUpdates.length).equal(1); + }); - it('does not send any updates if all lists are empty', () => { - region.clients.push(mockClient(), mockClient()); - const updateEntities1 = stub(region.clients[0], 'update'); - const updateEntities2 = stub(region.clients[1], 'update'); + it('does not send any updates if all lists are empty', () => { + region.clients.push(mockClient(), mockClient()); + const updateEntities1 = stub(region.clients[0], 'update'); + const updateEntities2 = stub(region.clients[1], 'update'); - commitRegionUpdates([region]); + commitRegionUpdates([region]); - assert.notCalled(updateEntities1); - assert.notCalled(updateEntities2); - }); + assert.notCalled(updateEntities1); + assert.notCalled(updateEntities2); + }); - it('resets region updates', () => { - region.tileUpdates.push({ x: 1, y: 2, type: 3 }); - region.entityUpdates = [{}, {}] as any; + it('resets region updates', () => { + region.tileUpdates.push({ x: 1, y: 2, type: 3 }); + region.entityUpdates = [{}, {}] as any; - commitRegionUpdates([region]); + commitRegionUpdates([region]); - expect(region.entityUpdates).eql([]); - }); - }); + expect(region.entityUpdates).eql([]); + }); + }); - describe('transferToRegion()', () => { - const map = createServerMap('', 0, 1, 1); + describe('transferToRegion()', () => { + const map = createServerMap('', 0, 1, 1); - it('removes entity from current region', () => { - const entity = serverEntity(1); - const oldRegion = entity.region = createServerRegion(0, 0); - addEntityToRegion(entity.region, entity, map); + it('removes entity from current region', () => { + const entity = serverEntity(1); + const oldRegion = entity.region = createServerRegion(0, 0); + addEntityToRegion(entity.region, entity, map); - transferToRegion(entity, region, map); + transferToRegion(entity, region, map); - expect(oldRegion.entities).not.contain(entity); - }); + expect(oldRegion.entities).not.contain(entity); + }); - it('adds entity to new region', () => { - const entity = serverEntity(1); + it('adds entity to new region', () => { + const entity = serverEntity(1); - transferToRegion(entity, region, map); + transferToRegion(entity, region, map); - expect(region.entities).contain(entity); - }); + expect(region.entities).contain(entity); + }); - it('updates entity region', () => { - const entity = serverEntity(1); + it('updates entity region', () => { + const entity = serverEntity(1); - transferToRegion(entity, region, map); + transferToRegion(entity, region, map); - expect(entity.region).equal(region); - }); + expect(entity.region).equal(region); + }); - it('adds entity update to current region', () => { - const entity = serverEntity(1); - entity.region = createServerRegion(0, 0); + it('adds entity update to current region', () => { + const entity = serverEntity(1); + entity.region = createServerRegion(0, 0); - transferToRegion(entity, region, map); + transferToRegion(entity, region, map); - expect(region.entityUpdates).eql([ - // ... - ]); - }); + expect(region.entityUpdates).eql([ + // ... + ]); + }); - // it('sends addEntity message to clients subscribed to destination region', () => { - // const entity = serverEntity(1); - // const otherClient = mockClient(); - // const addEntity = stub(otherClient, 'addEntity'); - // region.clients.push(otherClient); - // otherClient.regions.push(region); + // it('sends addEntity message to clients subscribed to destination region', () => { + // const entity = serverEntity(1); + // const otherClient = mockClient(); + // const addEntity = stub(otherClient, 'addEntity'); + // region.clients.push(otherClient); + // otherClient.regions.push(region); - // transferToRegion(entity, region, {}, {} as any); + // transferToRegion(entity, region, {}, {} as any); - // assert.calledWith(addEntity as any, entity.id); - // }); + // assert.calledWith(addEntity as any, entity.id); + // }); - // it('does not sent addEntity message to clients subscribed to destination region if entity is shadowed', () => { - // const entity = serverEntity(1); - // entity.client = mockClient({ shadowed: true }); - // const otherClient = mockClient(); - // const addEntity = stub(otherClient, 'addEntity'); - // region.clients.push(otherClient); - // otherClient.regions.push(region); + // it('does not sent addEntity message to clients subscribed to destination region if entity is shadowed', () => { + // const entity = serverEntity(1); + // entity.client = mockClient({ shadowed: true }); + // const otherClient = mockClient(); + // const addEntity = stub(otherClient, 'addEntity'); + // region.clients.push(otherClient); + // otherClient.regions.push(region); - // transferToRegion(entity, region, {}, {} as any); + // transferToRegion(entity, region, {}, {} as any); - // assert.notCalled(addEntity); - // }); + // assert.notCalled(addEntity); + // }); - // it('does not send addEntity message to clients subscribed to destination and source regions', () => { - // const entity = serverEntity(1); - // entity.region = createServerRegion(0, 0, 0); - // const otherClient = mockClient(); - // const addEntity = stub(otherClient, 'addEntity'); - // region.clients.push(otherClient); - // entity.region.clients.push(otherClient); - // otherClient.regions.push(entity.region, region); + // it('does not send addEntity message to clients subscribed to destination and source regions', () => { + // const entity = serverEntity(1); + // entity.region = createServerRegion(0, 0, 0); + // const otherClient = mockClient(); + // const addEntity = stub(otherClient, 'addEntity'); + // region.clients.push(otherClient); + // entity.region.clients.push(otherClient); + // otherClient.regions.push(entity.region, region); - // transferToRegion(entity, region, {}, {} as any); + // transferToRegion(entity, region, {}, {} as any); - // assert.notCalled(addEntity); - // }); - }); + // assert.notCalled(addEntity); + // }); + }); - describe('addToRegion()', () => { - const map = createServerMap('', 0, 1, 1); + describe('addToRegion()', () => { + const map = createServerMap('', 0, 1, 1); - it('adds entity to region', () => { - const entity = serverEntity(1); + it('adds entity to region', () => { + const entity = serverEntity(1); - addToRegion(entity, region, map); + addToRegion(entity, region, map); - expect(region.entities).contain(entity); - }); + expect(region.entities).contain(entity); + }); - it('sets entity region', () => { - const entity = serverEntity(1); + it('sets entity region', () => { + const entity = serverEntity(1); - addToRegion(entity, region, map); + addToRegion(entity, region, map); - expect(entity.region).equal(region); - }); + expect(entity.region).equal(region); + }); - // it('sends addEntity message to all clients', () => { - // region.clients.push(mockClient(), mockClient()); - // const addEntity1 = stub(region.clients[0], 'addEntity'); - // const addEntity2 = stub(region.clients[1], 'addEntity'); + // it('sends addEntity message to all clients', () => { + // region.clients.push(mockClient(), mockClient()); + // const addEntity1 = stub(region.clients[0], 'addEntity'); + // const addEntity2 = stub(region.clients[1], 'addEntity'); - // addToRegion(serverEntity(1), region, {}, hiding); + // addToRegion(serverEntity(1), region, {}, hiding); - // assert.calledOnce(addEntity1); - // assert.calledOnce(addEntity2); - // }); + // assert.calledOnce(addEntity1); + // assert.calledOnce(addEntity2); + // }); - // it('only sents addEntity message to entity client if shadowed', () => { - // region.clients.push(mockClient()); - // const addEntity1 = stub(region.clients[0], 'addEntity'); - // const entity = serverEntity(1); - // entity.client = mockClient({ shadowed: true }); - // const addEntity2 = stub(entity.client, 'addEntity'); + // it('only sents addEntity message to entity client if shadowed', () => { + // region.clients.push(mockClient()); + // const addEntity1 = stub(region.clients[0], 'addEntity'); + // const entity = serverEntity(1); + // entity.client = mockClient({ shadowed: true }); + // const addEntity2 = stub(entity.client, 'addEntity'); - // addToRegion(entity, region, {}, hiding); + // addToRegion(entity, region, {}, hiding); - // assert.notCalled(addEntity1); - // assert.calledOnce(addEntity2); - // }); + // assert.notCalled(addEntity1); + // assert.calledOnce(addEntity2); + // }); - it('adds entity to region even if shadowed', () => { - const entity = serverEntity(1); - entity.client = mockClient({ shadowed: true }); + it('adds entity to region even if shadowed', () => { + const entity = serverEntity(1); + entity.client = mockClient({ shadowed: true }); - addToRegion(entity, region, map); + addToRegion(entity, region, map); - expect(region.entities).contain(entity); - }); + expect(region.entities).contain(entity); + }); - it('sets entity region even if shadowed', () => { - const entity = serverEntity(1); - entity.client = mockClient({ shadowed: true }); + it('sets entity region even if shadowed', () => { + const entity = serverEntity(1); + entity.client = mockClient({ shadowed: true }); - addToRegion(entity, region, map); + addToRegion(entity, region, map); - expect(entity.region).equal(region); - }); - }); + expect(entity.region).equal(region); + }); + }); - describe('removeFromRegion()', () => { - const map = createServerMap('', 0, 1, 1); + describe('removeFromRegion()', () => { + const map = createServerMap('', 0, 1, 1); - it('removes entity from region', () => { - const entity = serverEntity(1); - addEntityToRegion(region, entity, map); + it('removes entity from region', () => { + const entity = serverEntity(1); + addEntityToRegion(region, entity, map); - removeFromRegion(entity, region, map); + removeFromRegion(entity, region, map); - expect(region.entities).not.contain(entity); - }); + expect(region.entities).not.contain(entity); + }); - it('unsets entity region', () => { - const entity = serverEntity(1); - addEntityToRegion(region, entity, map); + it('unsets entity region', () => { + const entity = serverEntity(1); + addEntityToRegion(region, entity, map); - removeFromRegion(entity, region, map); + removeFromRegion(entity, region, map); - expect(entity.region).undefined; - }); + expect(entity.region).undefined; + }); - it('adds entity to removed entities list', () => { - const entity = serverEntity(123); + it('adds entity to removed entities list', () => { + const entity = serverEntity(123); - removeFromRegion(entity, region, map); + removeFromRegion(entity, region, map); - expect(region.entityRemoves).eql([123]); - }); - }); + expect(region.entityRemoves).eql([123]); + }); + }); - describe('isSubscribedToRegion()', () => { - it('returns true if subscribed to region', () => { - client.regions.push(region); - region.clients.push(client); + describe('isSubscribedToRegion()', () => { + it('returns true if subscribed to region', () => { + client.regions.push(region); + region.clients.push(client); - expect(isSubscribedToRegion(client, region)).true; - }); + expect(isSubscribedToRegion(client, region)).true; + }); - it('returns false if not subscribed to region', () => { - expect(isSubscribedToRegion(client, region)).false; - }); - }); + it('returns false if not subscribed to region', () => { + expect(isSubscribedToRegion(client, region)).false; + }); + }); - describe('unsubscribeFromAllRegions()', () => { - it('removes client from region', () => { - region.clients.push(client); - client.regions.push(region); + describe('unsubscribeFromAllRegions()', () => { + it('removes client from region', () => { + region.clients.push(client); + client.regions.push(region); - unsubscribeFromAllRegions(client, false); + unsubscribeFromAllRegions(client, false); - expect(region.clients).not.contain(client); - }); + expect(region.clients).not.contain(client); + }); - it('removes region from client', () => { - client.regions.push(region); + it('removes region from client', () => { + client.regions.push(region); - unsubscribeFromAllRegions(client, false); + unsubscribeFromAllRegions(client, false); - expect(client.regions).not.contain(region); - }); + expect(client.regions).not.contain(region); + }); - it('adds unsubscribes to client with region coordinates', () => { - client.regions.push(createServerRegion(2, 3)); + it('adds unsubscribes to client with region coordinates', () => { + client.regions.push(createServerRegion(2, 3)); - unsubscribeFromAllRegions(client, false); + unsubscribeFromAllRegions(client, false); - expect(client.unsubscribes).eql([2, 3]); - }); + expect(client.unsubscribes).eql([2, 3]); + }); - it('adds unsubscribes to client with all regions coordinates', () => { - client.regions.push(createServerRegion(2, 3), createServerRegion(5, 6)); + it('adds unsubscribes to client with all regions coordinates', () => { + client.regions.push(createServerRegion(2, 3), createServerRegion(5, 6)); - unsubscribeFromAllRegions(client, false); + unsubscribeFromAllRegions(client, false); - expect(client.unsubscribes).eql([2, 3, 5, 6]); - }); + expect(client.unsubscribes).eql([2, 3, 5, 6]); + }); - it('does not add unsibscribes to client if silent flag is set', () => { - unsubscribeFromAllRegions(client, true); + it('does not add unsibscribes to client if silent flag is set', () => { + unsubscribeFromAllRegions(client, true); - expect(client.unsubscribes).eql([]); - }); - }); + expect(client.unsubscribes).eql([]); + }); + }); }); diff --git a/src/ts/tests/server/reporting.spec.ts b/src/ts/tests/server/reporting.spec.ts index 55290d8..e28b758 100644 --- a/src/ts/tests/server/reporting.spec.ts +++ b/src/ts/tests/server/reporting.spec.ts @@ -7,244 +7,244 @@ import { mock, mockClient } from '../mocks'; import { createReportSwears, createReportForbidden, reportInviteLimit } from '../../server/reporting'; describe('reporting', () => { - describe('reportSwears()', () => { - let client: IClient; - let counter: CounterService; - let settings: GameServerSettings; - let reportSwearingAccount: SinonStub; - let timeoutAccount: SinonStub; - let reportSwears: OnMessageSettings; + describe('reportSwears()', () => { + let client: IClient; + let counter: CounterService; + let settings: GameServerSettings; + let reportSwearingAccount: SinonStub; + let timeoutAccount: SinonStub; + let reportSwears: OnMessageSettings; - beforeEach(() => { - client = mockClient(); - counter = mock>(CounterService); - settings = { filterSwears: true }; - reportSwearingAccount = stub().resolves(); - timeoutAccount = stub(); - reportSwears = createFunctionWithPromiseHandler( - createReportSwears, counter, reportSwearingAccount, timeoutAccount); - }); + beforeEach(() => { + client = mockClient(); + counter = mock>(CounterService); + settings = { filterSwears: true }; + reportSwearingAccount = stub().resolves(); + timeoutAccount = stub(); + reportSwears = createFunctionWithPromiseHandler( + createReportSwears, counter, reportSwearingAccount, timeoutAccount); + }); - it('increments counter', async () => { - const add = stub(counter, 'add').returns({ count: 0, items: [], date: 0 }); + it('increments counter', async () => { + const add = stub(counter, 'add').returns({ count: 0, items: [], date: 0 }); - await reportSwears(client, 'test', settings); + await reportSwears(client, 'test', settings); - assert.calledWith(add, client.accountId, 'test'); - }); + assert.calledWith(add, client.accountId, 'test'); + }); - describe('after excceded limit', () => { - beforeEach(() => { - stub(counter, 'add').returns({ count: 6, items: ['test'], date: 0 }); - }); + describe('after excceded limit', () => { + beforeEach(() => { + stub(counter, 'add').returns({ count: 6, items: ['test'], date: 0 }); + }); - it('reports swearing', async () => { - await reportSwears(client, 'test', settings); + it('reports swearing', async () => { + await reportSwears(client, 'test', settings); - assert.calledWith(reportSwearingAccount, client.accountId); - }); + assert.calledWith(reportSwearingAccount, client.accountId); + }); - it('timeouts account for 10 hours', async () => { - settings.autoBanSwearing = true; + it('timeouts account for 10 hours', async () => { + settings.autoBanSwearing = true; - await reportSwears(client, 'test', settings); + await reportSwears(client, 'test', settings); - assert.calledWith(timeoutAccount, client.accountId); // , fromNow(10 * HOUR) - }); + assert.calledWith(timeoutAccount, client.accountId); // , fromNow(10 * HOUR) + }); - it('doesnt timeout account if autoBanSwearing is false', async () => { - await reportSwears(client, 'test', settings); + it('doesnt timeout account if autoBanSwearing is false', async () => { + await reportSwears(client, 'test', settings); - assert.notCalled(timeoutAccount); - }); + assert.notCalled(timeoutAccount); + }); - it('reports timing out', async () => { - const system = stub(client.reporter, 'system'); - settings.autoBanSwearing = true; - settings.reportSwears = true; + it('reports timing out', async () => { + const system = stub(client.reporter, 'system'); + settings.autoBanSwearing = true; + settings.reportSwears = true; - await reportSwears(client, 'test', settings); + await reportSwears(client, 'test', settings); - assert.calledWith(system, 'Timed out for swearing', 'test', true); - }); + assert.calledWith(system, 'Timed out for swearing', 'test', true); + }); - it('does not report timing out if turned off in settings', async () => { - const system = stub(client.reporter, 'system'); - settings.autoBanSwearing = true; - settings.reportSwears = false; + it('does not report timing out if turned off in settings', async () => { + const system = stub(client.reporter, 'system'); + settings.autoBanSwearing = true; + settings.reportSwears = false; - await reportSwears(client, 'test', settings); + await reportSwears(client, 'test', settings); - assert.calledWith(system, 'Timed out for swearing', 'test', false); - }); + assert.calledWith(system, 'Timed out for swearing', 'test', false); + }); - it('reports swearing if not timed out', async () => { - const warn = stub(client.reporter, 'warn'); + it('reports swearing if not timed out', async () => { + const warn = stub(client.reporter, 'warn'); - await reportSwears(client, 'test', settings); + await reportSwears(client, 'test', settings); - assert.calledWith(warn, 'Swearing', 'test'); - }); + assert.calledWith(warn, 'Swearing', 'test'); + }); - it('does not timeout if already muted', async () => { - client.account.mute = -1; - settings.autoBanSwearing = true; + it('does not timeout if already muted', async () => { + client.account.mute = -1; + settings.autoBanSwearing = true; - await reportSwears(client, 'test', settings); + await reportSwears(client, 'test', settings); - assert.notCalled(timeoutAccount); - }); + assert.notCalled(timeoutAccount); + }); - // reports unhandled rejection - it.skip('handles error', async () => { - const err = new Error('test1'); - timeoutAccount.rejects(err); - const error = stub(client.reporter, 'error'); - settings.autoBanSwearing = true; + // reports unhandled rejection + it.skip('handles error', async () => { + const err = new Error('test1'); + timeoutAccount.rejects(err); + const error = stub(client.reporter, 'error'); + settings.autoBanSwearing = true; - await reportSwears(client, 'test', settings); + await reportSwears(client, 'test', settings); - assert.calledWith(error, err); - }); - }); - }); + assert.calledWith(error, err); + }); + }); + }); - describe('reportForbidden()', () => { - let client: IClient; - let counter: CounterService; - let settings: GameServerSettings; - let onTimeoutAccount: SinonStub; - let reportForbidden: OnMessageSettings; + describe('reportForbidden()', () => { + let client: IClient; + let counter: CounterService; + let settings: GameServerSettings; + let onTimeoutAccount: SinonStub; + let reportForbidden: OnMessageSettings; - beforeEach(() => { - client = mockClient(); - counter = mock>(CounterService); - settings = {}; - onTimeoutAccount = stub().resolves(); - reportForbidden = createFunctionWithPromiseHandler(createReportForbidden, counter, onTimeoutAccount); - }); + beforeEach(() => { + client = mockClient(); + counter = mock>(CounterService); + settings = {}; + onTimeoutAccount = stub().resolves(); + reportForbidden = createFunctionWithPromiseHandler(createReportForbidden, counter, onTimeoutAccount); + }); - it('increments counter', async () => { - const add = stub(counter, 'add').returns({ count: 0, items: [], date: 0 }); + it('increments counter', async () => { + const add = stub(counter, 'add').returns({ count: 0, items: [], date: 0 }); - await reportForbidden(client, 'test', settings); + await reportForbidden(client, 'test', settings); - assert.calledWith(add, client.accountId, 'test'); - }); + assert.calledWith(add, client.accountId, 'test'); + }); - describe('after excceded limit', () => { - beforeEach(() => { - stub(counter, 'add').returns({ count: 12, items: ['test'], date: 0 }); - }); + describe('after excceded limit', () => { + beforeEach(() => { + stub(counter, 'add').returns({ count: 12, items: ['test'], date: 0 }); + }); - it('timeouts if account is new', async () => { - client.account.createdAt = new Date(); + it('timeouts if account is new', async () => { + client.account.createdAt = new Date(); - await reportForbidden(client, 'test', settings); + await reportForbidden(client, 'test', settings); - assert.calledWith(onTimeoutAccount, client.accountId); - }); + assert.calledWith(onTimeoutAccount, client.accountId); + }); - it('timeouts if autoBanSwearing is true', async () => { - settings.autoBanSwearing = true; + it('timeouts if autoBanSwearing is true', async () => { + settings.autoBanSwearing = true; - await reportForbidden(client, 'test', settings); + await reportForbidden(client, 'test', settings); - assert.calledWith(onTimeoutAccount, client.accountId); - }); + assert.calledWith(onTimeoutAccount, client.accountId); + }); - it('does not timeout account if autoBanSwearing is false and account is old', async () => { - client.account.createdAt = new Date(0); + it('does not timeout account if autoBanSwearing is false and account is old', async () => { + client.account.createdAt = new Date(0); - await reportForbidden(client, 'test', settings); + await reportForbidden(client, 'test', settings); - assert.notCalled(onTimeoutAccount); - }); + assert.notCalled(onTimeoutAccount); + }); - it('reports timing out', async () => { - const system = stub(client.reporter, 'system'); - settings.autoBanSwearing = true; + it('reports timing out', async () => { + const system = stub(client.reporter, 'system'); + settings.autoBanSwearing = true; - await reportForbidden(client, 'test', settings); + await reportForbidden(client, 'test', settings); - assert.calledWith(system, 'Timed out for forbidden messages', 'test'); - }); + assert.calledWith(system, 'Timed out for forbidden messages', 'test'); + }); - it('reports forbidden if not timed out', async () => { - const warn = stub(client.reporter, 'warn'); - settings.autoBanSwearing = false; - client.account.createdAt = new Date(0); + it('reports forbidden if not timed out', async () => { + const warn = stub(client.reporter, 'warn'); + settings.autoBanSwearing = false; + client.account.createdAt = new Date(0); - await reportForbidden(client, 'test', settings); + await reportForbidden(client, 'test', settings); - assert.calledWith(warn, 'Forbidden messages', 'test'); - }); + assert.calledWith(warn, 'Forbidden messages', 'test'); + }); - it('does not timeout if already muted', async () => { - client.account.mute = -1; - settings.autoBanSwearing = true; + it('does not timeout if already muted', async () => { + client.account.mute = -1; + settings.autoBanSwearing = true; - await reportForbidden(client, 'test', settings); + await reportForbidden(client, 'test', settings); - assert.notCalled(onTimeoutAccount); - }); + assert.notCalled(onTimeoutAccount); + }); - it('handles error', async () => { - const err = new Error('test2'); - onTimeoutAccount.rejects(err); - const error = stub(client.reporter, 'error'); - settings.autoBanSwearing = true; + it('handles error', async () => { + const err = new Error('test2'); + onTimeoutAccount.rejects(err); + const error = stub(client.reporter, 'error'); + settings.autoBanSwearing = true; - await reportForbidden(client, 'test', settings); + await reportForbidden(client, 'test', settings); - assert.calledWith(error, err); - }); - }); - }); + assert.calledWith(error, err); + }); + }); + }); - describe('reportInviteLimit()', () => { - let client: IClient; - let reportInviteLimitAccount: SinonStub; - let report: ReportInviteLimit; + describe('reportInviteLimit()', () => { + let client: IClient; + let reportInviteLimitAccount: SinonStub; + let report: ReportInviteLimit; - beforeEach(() => { - client = mockClient(); - reportInviteLimitAccount = stub().resolves(); - report = createFunctionWithPromiseHandler(reportInviteLimit, reportInviteLimitAccount, 'Invite limit reached'); - }); + beforeEach(() => { + client = mockClient(); + reportInviteLimitAccount = stub().resolves(); + report = createFunctionWithPromiseHandler(reportInviteLimit, reportInviteLimitAccount, 'Invite limit reached'); + }); - it('reports spamming account', async () => { - await report(client); + it('reports spamming account', async () => { + await report(client); - assert.calledWith(reportInviteLimitAccount, client.accountId); - }); + assert.calledWith(reportInviteLimitAccount, client.accountId); + }); - it('reports error during invite limit reporting', async () => { - const error = new Error('test3'); - reportInviteLimitAccount.rejects(error); - const reporterError = stub(client.reporter, 'error'); + it('reports error during invite limit reporting', async () => { + const error = new Error('test3'); + reportInviteLimitAccount.rejects(error); + const reporterError = stub(client.reporter, 'error'); - await report(client); + await report(client); - assert.calledWith(reporterError, error); - }); + assert.calledWith(reporterError, error); + }); - it('logs invite limit reached', async () => { - reportInviteLimitAccount.resolves(5); - const systemLog = stub(client.reporter, 'systemLog'); + it('logs invite limit reached', async () => { + reportInviteLimitAccount.resolves(5); + const systemLog = stub(client.reporter, 'systemLog'); - await report(client); + await report(client); - assert.calledWith(systemLog, 'Invite limit reached'); - }); + assert.calledWith(systemLog, 'Invite limit reached'); + }); - it('reports warning every tenth invite limit report', async () => { - reportInviteLimitAccount.resolves(10); - const reporterWarn = stub(client.reporter, 'warn'); + it('reports warning every tenth invite limit report', async () => { + reportInviteLimitAccount.resolves(10); + const reporterWarn = stub(client.reporter, 'warn'); - await report(client); + await report(client); - assert.calledWith(reporterWarn, 'Invite limit reached (10)'); - }); - }); + assert.calledWith(reporterWarn, 'Invite limit reached (10)'); + }); + }); }); diff --git a/src/ts/tests/server/serverActions.spec.ts b/src/ts/tests/server/serverActions.spec.ts index 594aeee..c61e8f5 100644 --- a/src/ts/tests/server/serverActions.spec.ts +++ b/src/ts/tests/server/serverActions.spec.ts @@ -5,7 +5,7 @@ import { range } from 'lodash'; import { getWriterBuffer } from 'ag-sockets'; import { encodeString } from 'ag-sockets/dist/utf8'; import { - ChatType, TileType, Action, PlayerAction, ModAction, Eye, Muzzle, SelectFlags, InfoFlags + ChatType, TileType, Action, PlayerAction, ModAction, Eye, Muzzle, SelectFlags, InfoFlags } from '../../common/interfaces'; import { CharacterState, ServerConfig, GameServerSettings } from '../../common/adminInterfaces'; import { IClient, AccountService } from '../../server/serverInterfaces'; @@ -26,1016 +26,1016 @@ import { FriendsService } from '../../server/services/friends'; import * as playerUtils from '../../server/playerUtils'; describe('ServerActions', () => { - let accountService = stubFromInstance({ - update() { }, - updateAccount() { }, - updateSettings() { }, - updateCharacterState() { } - }); - let notifications = stubClass(NotificationService); - let partyService = stubClass(PartyService); - let hiding = stubClass(HidingService); - let friends = stubClass(FriendsService); - let states = stubClass>(CounterService); - let teleports = stubClass>(CounterService); - let supporterInvites = stubClass(SupporterInvitesService); - let client: IClient; - let world: World; - let serverActions: ServerActions; - let settings: GameServerSettings; - let server: ServerConfig; - let ignorePlayer: SinonSpy; - let findClientByEntityId: SinonStub; - let say: SinonSpy; - let move: SinonSpy; - let execAction: SinonStub; - - beforeEach(() => { - resetStubMethods(accountService, 'update', 'updateSettings'); - resetStubMethods(notifications, 'acceptNotification', 'rejectNotification'); - resetStubMethods(partyService, 'invite', 'remove', 'promoteLeader'); - resetStubMethods(hiding, 'requestUnhideAll', 'requestHide'); - resetStubMethods(friends, 'add', 'remove', 'removeByAccountId'); - resetStubMethods(teleports); - - execAction = stub(playerUtils, 'execAction'); - client = mockClient(); - world = new World( - { flags: { friends: true } } as any, { partyChanged: { subscribe() { } } } as any, {} as any, {} as any, - {} as any, () => ({}), {} as any, {} as any); - const map = createServerMap('', 0, 1, 1); - client.map = map; - world.maps.push(map); - settings = {}; - server = { flags: {} } as any; - ignorePlayer = spy(); - findClientByEntityId = stub(); - say = spy(); - move = spy(); - - serverActions = new ServerActions( - client, world, notifications, partyService as any, supporterInvites as any, () => settings, server, say, - move, hiding as any, states as any, accountService, ignorePlayer, - findClientByEntityId, friends as any); - }); - - afterEach(() => { - execAction.restore(); - }); - - describe('connected()', () => { - // TODO: ... - }); - - describe('disconnected()', () => { - let clock: SinonFakeTimers; - - beforeEach(() => { - clock = useFakeTimers(); - }); - - afterEach(() => { - clock.restore(); - }); - - it('sets client offline flag to true', () => { - serverActions.disconnected(); - - expect(client.offline).true; - }); - - it('leaves client from world', () => { - const leaveClient = stub(world, 'leaveClient'); - - serverActions.disconnected(); - - assert.calledWith(leaveClient, client); - }); - - it('notifies party service', () => { - serverActions.disconnected(); - - assert.calledWith(partyService.clientDisconnected, client); - }); - - it('updates last visit', () => { - clock.setSystemTime(1234); - - serverActions.disconnected(); - - assert.calledWith(accountService.updateAccount, client.accountId, { lastVisit: new Date(1234), state: undefined }); - }); - - it('updates character state', () => { - client.pony.x = 123; - client.pony.y = 321; - - serverActions.disconnected(); - - assert.calledWith( - accountService.updateCharacterState, client.characterId, createCharacterState(client.pony, client.map)); - }); - - it('adds state to counter service', () => { - client.pony.x = 123; - client.pony.y = 321; - - serverActions.disconnected(); - - assert.calledWithMatch(states.add, client.characterId, { x: 123, y: 321 }); - }); - - it('logs client leaving', () => { - const systemLog = stub(client.reporter, 'systemLog'); - server.id = 'server_id'; - clock.setSystemTime(12 * 1000); - client.connectedTime = 0; - - serverActions.disconnected(); - - assert.calledWith(systemLog, 'left [server_id] (disconnected) (12s)'); - }); - }); + let accountService = stubFromInstance({ + update() { }, + updateAccount() { }, + updateSettings() { }, + updateCharacterState() { } + }); + let notifications = stubClass(NotificationService); + let partyService = stubClass(PartyService); + let hiding = stubClass(HidingService); + let friends = stubClass(FriendsService); + let states = stubClass>(CounterService); + let teleports = stubClass>(CounterService); + let supporterInvites = stubClass(SupporterInvitesService); + let client: IClient; + let world: World; + let serverActions: ServerActions; + let settings: GameServerSettings; + let server: ServerConfig; + let ignorePlayer: SinonSpy; + let findClientByEntityId: SinonStub; + let say: SinonSpy; + let move: SinonSpy; + let execAction: SinonStub; + + beforeEach(() => { + resetStubMethods(accountService, 'update', 'updateSettings'); + resetStubMethods(notifications, 'acceptNotification', 'rejectNotification'); + resetStubMethods(partyService, 'invite', 'remove', 'promoteLeader'); + resetStubMethods(hiding, 'requestUnhideAll', 'requestHide'); + resetStubMethods(friends, 'add', 'remove', 'removeByAccountId'); + resetStubMethods(teleports); + + execAction = stub(playerUtils, 'execAction'); + client = mockClient(); + world = new World( + { flags: { friends: true } } as any, { partyChanged: { subscribe() { } } } as any, {} as any, {} as any, + {} as any, () => ({}), {} as any, {} as any); + const map = createServerMap('', 0, 1, 1); + client.map = map; + world.maps.push(map); + settings = {}; + server = { flags: {} } as any; + ignorePlayer = spy(); + findClientByEntityId = stub(); + say = spy(); + move = spy(); + + serverActions = new ServerActions( + client, world, notifications, partyService as any, supporterInvites as any, () => settings, server, say, + move, hiding as any, states as any, accountService, ignorePlayer, + findClientByEntityId, friends as any); + }); + + afterEach(() => { + execAction.restore(); + }); + + describe('connected()', () => { + // TODO: ... + }); + + describe('disconnected()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('sets client offline flag to true', () => { + serverActions.disconnected(); + + expect(client.offline).true; + }); + + it('leaves client from world', () => { + const leaveClient = stub(world, 'leaveClient'); + + serverActions.disconnected(); + + assert.calledWith(leaveClient, client); + }); + + it('notifies party service', () => { + serverActions.disconnected(); + + assert.calledWith(partyService.clientDisconnected, client); + }); + + it('updates last visit', () => { + clock.setSystemTime(1234); + + serverActions.disconnected(); + + assert.calledWith(accountService.updateAccount, client.accountId, { lastVisit: new Date(1234), state: undefined }); + }); + + it('updates character state', () => { + client.pony.x = 123; + client.pony.y = 321; + + serverActions.disconnected(); + + assert.calledWith( + accountService.updateCharacterState, client.characterId, createCharacterState(client.pony, client.map)); + }); + + it('adds state to counter service', () => { + client.pony.x = 123; + client.pony.y = 321; + + serverActions.disconnected(); + + assert.calledWithMatch(states.add, client.characterId, { x: 123, y: 321 }); + }); + + it('logs client leaving', () => { + const systemLog = stub(client.reporter, 'systemLog'); + server.id = 'server_id'; + clock.setSystemTime(12 * 1000); + client.connectedTime = 0; + + serverActions.disconnected(); + + assert.calledWith(systemLog, 'left [server_id] (disconnected) (12s)'); + }); + }); - describe('say()', () => { - it('calls chatSay', async () => { - serverActions.say(0, 'hello', ChatType.Say); + describe('say()', () => { + it('calls chatSay', async () => { + serverActions.say(0, 'hello', ChatType.Say); - assert.calledWith(say, client, 'hello', ChatType.Say, undefined, settings); - }); + assert.calledWith(say, client, 'hello', ChatType.Say, undefined, settings); + }); - it('throws if message is not a string', () => { - expect(() => serverActions.say(0, {} as any, ChatType.Say)).throw('Not a string (text)'); - expect(() => serverActions.say(0, 123 as any, ChatType.Say)).throw('Not a string (text)'); - expect(() => serverActions.say(0, null as any, ChatType.Say)).throw('Not a string (text)'); - }); + it('throws if message is not a string', () => { + expect(() => serverActions.say(0, {} as any, ChatType.Say)).throw('Not a string (text)'); + expect(() => serverActions.say(0, 123 as any, ChatType.Say)).throw('Not a string (text)'); + expect(() => serverActions.say(0, null as any, ChatType.Say)).throw('Not a string (text)'); + }); - it('throws if type is not a number', () => { - expect(() => serverActions.say(0, 'test', {} as any)).throw('Not a number (chatType)'); - expect(() => serverActions.say(0, 'test', '1' as any)).throw('Not a number (chatType)'); - expect(() => serverActions.say(0, 'test', null as any)).throw('Not a number (chatType)'); - }); - }); + it('throws if type is not a number', () => { + expect(() => serverActions.say(0, 'test', {} as any)).throw('Not a number (chatType)'); + expect(() => serverActions.say(0, 'test', '1' as any)).throw('Not a number (chatType)'); + expect(() => serverActions.say(0, 'test', null as any)).throw('Not a number (chatType)'); + }); + }); - describe('select()', () => { - it('sets selected entity', () => { - const entity = serverEntity(1); - stub(world, 'getEntityById').withArgs(123).returns(entity); + describe('select()', () => { + it('sets selected entity', () => { + const entity = serverEntity(1); + stub(world, 'getEntityById').withArgs(123).returns(entity); - serverActions.select(123, SelectFlags.FetchEx); + serverActions.select(123, SelectFlags.FetchEx); - expect(client.selected).equal(entity); - }); + expect(client.selected).equal(entity); + }); - it('sets selected entity from other sources', () => { - const entity = { id: 123 }; - findClientByEntityId.withArgs(client, 123).returns({ pony: entity }); - stub(world, 'getEntityById').returns(undefined); + it('sets selected entity from other sources', () => { + const entity = { id: 123 }; + findClientByEntityId.withArgs(client, 123).returns({ pony: entity }); + stub(world, 'getEntityById').returns(undefined); - serverActions.select(123, SelectFlags.FetchEx); + serverActions.select(123, SelectFlags.FetchEx); - expect(client.selected).equal(entity); - }); + expect(client.selected).equal(entity); + }); - it('sends extra data for selected entity', () => { - const entity = serverEntity(1, 0, 0, 0, { client: {} as any, extraOptions: { foo: 5 } }); - stub(world, 'getEntityById').withArgs(123).returns(entity); + it('sends extra data for selected entity', () => { + const entity = serverEntity(1, 0, 0, 0, { client: {} as any, extraOptions: { foo: 5 } }); + stub(world, 'getEntityById').withArgs(123).returns(entity); - serverActions.select(123, SelectFlags.FetchEx); + serverActions.select(123, SelectFlags.FetchEx); - expect(Array.from(getWriterBuffer(client.updateQueue))) - .eql([2, 0, 32, 0, 0, 0, 1, 129, 3, 102, 111, 111, 165]); - }); + expect(Array.from(getWriterBuffer(client.updateQueue))) + .eql([2, 0, 32, 0, 0, 0, 1, 129, 3, 102, 111, 111, 165]); + }); - it('does not send extra data for selected entity if fetch flag is false', () => { - const entity = serverEntity(1, 0, 0, 0, { client: {} as any, extraOptions: { foo: 5 } }); - stub(world, 'getEntityById').withArgs(123).returns(entity); + it('does not send extra data for selected entity if fetch flag is false', () => { + const entity = serverEntity(1, 0, 0, 0, { client: {} as any, extraOptions: { foo: 5 } }); + stub(world, 'getEntityById').withArgs(123).returns(entity); - serverActions.select(123, SelectFlags.None); + serverActions.select(123, SelectFlags.None); - expect(Array.from(getWriterBuffer(client.updateQueue))).eql([]); - }); + expect(Array.from(getWriterBuffer(client.updateQueue))).eql([]); + }); - it('sends extra mod data for selected entity', () => { - const entity = serverEntity(1, 0, 0, 0, { - client: { - accountId: '12345678901234567890aa', - account: { - name: 'foobar', - shadow: 0, - mute: -1, - note: 'bar' - } - } as any - }); - stub(world, 'getEntityById').withArgs(123).returns(entity); - client.isMod = true; + it('sends extra mod data for selected entity', () => { + const entity = serverEntity(1, 0, 0, 0, { + client: { + accountId: '12345678901234567890aa', + account: { + name: 'foobar', + shadow: 0, + mute: -1, + note: 'bar' + } + } as any + }); + stub(world, 'getEntityById').withArgs(123).returns(entity); + client.isMod = true; - serverActions.select(123, SelectFlags.FetchEx); + serverActions.select(123, SelectFlags.FetchEx); - expect(Array.from(getWriterBuffer(client.updateQueue))).eql([ - 2, 0, 32, 0, 0, 0, 1, 129, 7, 109, 111, 100, 73, 110, 102, 111, 134, 6, 115, 104, 97, 100, 111, - 119, 0, 4, 109, 117, 116, 101, 69, 112, 101, 114, 109, 97, 4, 110, 111, 116, 101, 67, 98, 97, - 114, 8, 99, 111, 117, 110, 116, 101, 114, 115, 128, 7, 99, 111, 117, 110, 116, 114, 121, 0, 7, - 97, 99, 99, 111, 117, 110, 116, 76, 102, 111, 111, 98, 97, 114, 32, 91, 48, 97, 97, 93 - ]); - }); - }); + expect(Array.from(getWriterBuffer(client.updateQueue))).eql([ + 2, 0, 32, 0, 0, 0, 1, 129, 7, 109, 111, 100, 73, 110, 102, 111, 134, 6, 115, 104, 97, 100, 111, + 119, 0, 4, 109, 117, 116, 101, 69, 112, 101, 114, 109, 97, 4, 110, 111, 116, 101, 67, 98, 97, + 114, 8, 99, 111, 117, 110, 116, 101, 114, 115, 128, 7, 99, 111, 117, 110, 116, 114, 121, 0, 7, + 97, 99, 99, 111, 117, 110, 116, 76, 102, 111, 111, 98, 97, 114, 32, 91, 48, 97, 97, 93 + ]); + }); + }); - describe('interact()', () => { - it('calls interaction with client and entity', () => { - const entity = serverEntity(1); - const interact = stub(); - entity.interact = interact; - stub(world, 'getEntityById').withArgs(123).returns(entity); + describe('interact()', () => { + it('calls interaction with client and entity', () => { + const entity = serverEntity(1); + const interact = stub(); + entity.interact = interact; + stub(world, 'getEntityById').withArgs(123).returns(entity); - serverActions.interact(123); + serverActions.interact(123); - assert.calledOnce(interact); - }); + assert.calledOnce(interact); + }); - it('updates last action', () => { - client.lastPacket = 0; + it('updates last action', () => { + client.lastPacket = 0; - serverActions.interact(123); + serverActions.interact(123); - expect(client.lastPacket).not.equal(0); - }); + expect(client.lastPacket).not.equal(0); + }); - it('throws on not a number', () => { - expect(() => serverActions.interact('foo' as any)).throw('Not a number (entityId)'); - }); - }); + it('throws on not a number', () => { + expect(() => serverActions.interact('foo' as any)).throw('Not a number (entityId)'); + }); + }); - describe('use()', () => { - // TODO: ... - }); + describe('use()', () => { + // TODO: ... + }); - describe('action()', () => { - it('calls unhideAll on hiding service', () => { - serverActions.action(Action.UnhideAllHiddenPlayers); + describe('action()', () => { + it('calls unhideAll on hiding service', () => { + serverActions.action(Action.UnhideAllHiddenPlayers); - assert.calledWith(hiding.requestUnhideAll, client); - }); + assert.calledWith(hiding.requestUnhideAll, client); + }); - it('does nothing for KeepAlive action', () => { - serverActions.action(Action.KeepAlive); + it('does nothing for KeepAlive action', () => { + serverActions.action(Action.KeepAlive); - assert.notCalled(execAction); - }); + assert.notCalled(execAction); + }); - it('executes player action', () => { - serverActions.action(Action.TurnHead); + it('executes player action', () => { + serverActions.action(Action.TurnHead); - assert.calledWith(execAction, client, Action.TurnHead); - }); + assert.calledWith(execAction, client, Action.TurnHead); + }); - it('updates last action', () => { - client.lastPacket = 0; + it('updates last action', () => { + client.lastPacket = 0; - serverActions.action(Action.Boop); + serverActions.action(Action.Boop); - expect(client.lastPacket).not.equal(0); - }); + expect(client.lastPacket).not.equal(0); + }); - it('throws on not a number', () => { - expect(() => serverActions.action('foo' as any)).throw('Not a number (action)'); - }); - }); + it('throws on not a number', () => { + expect(() => serverActions.action('foo' as any)).throw('Not a number (action)'); + }); + }); - describe('actionParam()', () => { - it('on RemoveFriend: calls friends.remove()', () => { - const friend = mockClient(); - world.clientsByAccount.set('some_account_id', friend); + describe('actionParam()', () => { + it('on RemoveFriend: calls friends.remove()', () => { + const friend = mockClient(); + world.clientsByAccount.set('some_account_id', friend); - serverActions.actionParam(Action.RemoveFriend, 'some_account_id'); + serverActions.actionParam(Action.RemoveFriend, 'some_account_id'); - assert.calledWith(friends.remove, client, friend); - }); + assert.calledWith(friends.remove, client, friend); + }); - it('on RemoveFriend: calls friends.removeByAccountId() if cannot find client', () => { - serverActions.actionParam(Action.RemoveFriend, 'some_account_id'); + it('on RemoveFriend: calls friends.removeByAccountId() if cannot find client', () => { + serverActions.actionParam(Action.RemoveFriend, 'some_account_id'); - assert.calledWith(friends.removeByAccountId, client, 'some_account_id'); - }); - }); + assert.calledWith(friends.removeByAccountId, client, 'some_account_id'); + }); + }); - describe('actionParam2()', () => { - it('on Info: update client flags', () => { - serverActions.actionParam2(Action.Info, InfoFlags.SupportsWASM | InfoFlags.SupportsLetAndConst); + describe('actionParam2()', () => { + it('on Info: update client flags', () => { + serverActions.actionParam2(Action.Info, InfoFlags.SupportsWASM | InfoFlags.SupportsLetAndConst); - expect(client.supportsWasm).true; - expect(client.supportsLetAndConst).true; - }); + expect(client.supportsWasm).true; + expect(client.supportsLetAndConst).true; + }); - it('throws on invalid action', () => { - expect(() => serverActions.actionParam2(99 as any, undefined)).throws('Invalid Action (99)'); - }); - }); + it('throws on invalid action', () => { + expect(() => serverActions.actionParam2(99 as any, undefined)).throws('Invalid Action (99)'); + }); + }); - describe('expression()', () => { - it('sets expression for player character', () => { - const expression = createExpression(Eye.Angry, Eye.Closed, Muzzle.Blep); + describe('expression()', () => { + it('sets expression for player character', () => { + const expression = createExpression(Eye.Angry, Eye.Closed, Muzzle.Blep); - serverActions.expression(encodeExpression(expression)); + serverActions.expression(encodeExpression(expression)); - expect(client.pony.options!.expr).equal(encodeExpression(expression)); - expect(client.pony.exprPermanent).eql(expression); - }); + expect(client.pony.options!.expr).equal(encodeExpression(expression)); + expect(client.pony.exprPermanent).eql(expression); + }); - it('updates last action', () => { - client.lastPacket = 0; + it('updates last action', () => { + client.lastPacket = 0; - serverActions.expression(0); + serverActions.expression(0); - expect(client.lastPacket).not.equal(0); - }); + expect(client.lastPacket).not.equal(0); + }); - it('throws on not a number', () => { - expect(() => serverActions.expression('foo' as any)).throw('Not a number (expression)'); - }); - }); + it('throws on not a number', () => { + expect(() => serverActions.expression('foo' as any)).throw('Not a number (expression)'); + }); + }); - describe('playerAction()', () => { - let clock: SinonFakeTimers; + describe('playerAction()', () => { + let clock: SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers(); - }); + beforeEach(() => { + clock = useFakeTimers(); + }); - afterEach(() => clock.restore()); + afterEach(() => clock.restore()); - it('ignores player', () => { - const target = mockClient(); - stub(world, 'getClientByEntityId').withArgs(123).returns(target); + it('ignores player', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); - serverActions.playerAction(123, PlayerAction.Ignore, undefined); + serverActions.playerAction(123, PlayerAction.Ignore, undefined); - assert.calledWith(ignorePlayer, client, target, true); - }); + assert.calledWith(ignorePlayer, client, target, true); + }); - it('unignores player', () => { - const target = mockClient(); - stub(world, 'getClientByEntityId').withArgs(123).returns(target); + it('unignores player', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); - serverActions.playerAction(123, PlayerAction.Unignore, undefined); + serverActions.playerAction(123, PlayerAction.Unignore, undefined); - assert.calledWith(ignorePlayer, client, target, false); - }); + assert.calledWith(ignorePlayer, client, target, false); + }); - it('invites player to party', () => { - const target = mockClient(); - stub(world, 'getClientByEntityId').withArgs(123).returns(target); + it('invites player to party', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); - serverActions.playerAction(123, PlayerAction.InviteToParty, undefined); + serverActions.playerAction(123, PlayerAction.InviteToParty, undefined); - assert.calledWith(partyService.invite, client, target); - }); + assert.calledWith(partyService.invite, client, target); + }); - it('removes player from party', () => { - const target = mockClient(); - stub(world, 'getClientByEntityId').withArgs(123).returns(target); + it('removes player from party', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); - serverActions.playerAction(123, PlayerAction.RemoveFromParty, undefined); + serverActions.playerAction(123, PlayerAction.RemoveFromParty, undefined); - assert.calledWith(partyService.remove, client, target); - }); + assert.calledWith(partyService.remove, client, target); + }); - it('promotes player to party leader', () => { - const target = mockClient(); - stub(world, 'getClientByEntityId').withArgs(123).returns(target); + it('promotes player to party leader', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); - serverActions.playerAction(123, PlayerAction.PromotePartyLeader, undefined); + serverActions.playerAction(123, PlayerAction.PromotePartyLeader, undefined); - assert.calledWith(partyService.promoteLeader, client, target); - }); + assert.calledWith(partyService.promoteLeader, client, target); + }); - it('hides player', () => { - const target = mockClient(); - stub(world, 'getClientByEntityId').withArgs(123).returns(target); + it('hides player', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); - serverActions.playerAction(123, PlayerAction.HidePlayer, 12345678); + serverActions.playerAction(123, PlayerAction.HidePlayer, 12345678); - assert.calledWith(hiding.requestHide, client, target, 12345678); - }); + assert.calledWith(hiding.requestHide, client, target, 12345678); + }); - it('invites player to supporters', () => { - const target = mockClient(); - stub(world, 'getClientByEntityId').withArgs(123).returns(target); + it('invites player to supporters', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').withArgs(123).returns(target); - serverActions.playerAction(123, PlayerAction.InviteToSupporterServers, undefined); + serverActions.playerAction(123, PlayerAction.InviteToSupporterServers, undefined); - assert.calledWith(supporterInvites.requestInvite, client, target); - }); + assert.calledWith(supporterInvites.requestInvite, client, target); + }); - it('updates last action', () => { - stub(world, 'getClientByEntityId').returns({} as any); - client.lastPacket = 0; - clock.setSystemTime(123); + it('updates last action', () => { + stub(world, 'getClientByEntityId').returns({} as any); + client.lastPacket = 0; + clock.setSystemTime(123); - serverActions.playerAction(1, PlayerAction.InviteToParty, undefined); + serverActions.playerAction(1, PlayerAction.InviteToParty, undefined); - expect(client.lastPacket).equal(123); - }); + expect(client.lastPacket).equal(123); + }); - it('logs warning if cannot find target player', () => { - const warnLog = stub(client.reporter, 'warnLog'); + it('logs warning if cannot find target player', () => { + const warnLog = stub(client.reporter, 'warnLog'); - serverActions.playerAction(1, PlayerAction.Ignore, undefined); + serverActions.playerAction(1, PlayerAction.Ignore, undefined); - assert.calledOnce(warnLog); - }); + assert.calledOnce(warnLog); + }); - it('AddFriend: calls friends.add()', () => { - const target = mockClient(); - stub(world, 'getClientByEntityId').returns(target); + it('AddFriend: calls friends.add()', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').returns(target); - serverActions.playerAction(1, PlayerAction.AddFriend, undefined); + serverActions.playerAction(1, PlayerAction.AddFriend, undefined); - assert.calledWith(friends.add, client, target); - }); + assert.calledWith(friends.add, client, target); + }); - it('RemoveFriend: calls friends.remove()', () => { - const target = mockClient(); - stub(world, 'getClientByEntityId').returns(target); + it('RemoveFriend: calls friends.remove()', () => { + const target = mockClient(); + stub(world, 'getClientByEntityId').returns(target); - serverActions.playerAction(1, PlayerAction.RemoveFriend, undefined); + serverActions.playerAction(1, PlayerAction.RemoveFriend, undefined); - assert.calledWith(friends.remove, client, target); - }); + assert.calledWith(friends.remove, client, target); + }); - it('throws on entityId not a number', () => { - expect(() => serverActions.playerAction('foo' as any, PlayerAction.Ignore, undefined)) - .throw('Not a number (entityId)'); - }); + it('throws on entityId not a number', () => { + expect(() => serverActions.playerAction('foo' as any, PlayerAction.Ignore, undefined)) + .throw('Not a number (entityId)'); + }); - it('throws on action not a number', () => { - expect(() => serverActions.playerAction(1, 'foo' as any, undefined)) - .throw('Not a number (action)'); - }); + it('throws on action not a number', () => { + expect(() => serverActions.playerAction(1, 'foo' as any, undefined)) + .throw('Not a number (action)'); + }); - it('throws on invalid action', () => { - stub(world, 'getClientByEntityId').returns({} as any); + it('throws on invalid action', () => { + stub(world, 'getClientByEntityId').returns({} as any); - expect(() => serverActions.playerAction(1, 999, undefined)) - .throw('Invalid player action (undefined) [999]'); - }); - }); + expect(() => serverActions.playerAction(1, 999, undefined)) + .throw('Invalid player action (undefined) [999]'); + }); + }); - describe('leaveParty()', () => { - it('removes client from party', () => { - const leader = {} as any; - client.party = { id: '', clients: [leader, client], leader, pending: [] }; + describe('leaveParty()', () => { + it('removes client from party', () => { + const leader = {} as any; + client.party = { id: '', clients: [leader, client], leader, pending: [] }; - serverActions.leaveParty(); + serverActions.leaveParty(); - assert.calledWith(partyService.remove, leader, client); - }); + assert.calledWith(partyService.remove, leader, client); + }); - it('does nothing if not in a party', () => { - serverActions.leaveParty(); + it('does nothing if not in a party', () => { + serverActions.leaveParty(); - assert.notCalled(partyService.remove); - }); + assert.notCalled(partyService.remove); + }); - it('updates last action', () => { - client.lastPacket = 0; + it('updates last action', () => { + client.lastPacket = 0; - serverActions.leaveParty(); + serverActions.leaveParty(); - expect(client.lastPacket).not.equal(0); - }); - }); + expect(client.lastPacket).not.equal(0); + }); + }); - describe('otherAction()', () => { - let target: IClient; - let clock: SinonFakeTimers; - let getClientByEntityId: SinonStub; + describe('otherAction()', () => { + let target: IClient; + let clock: SinonFakeTimers; + let getClientByEntityId: SinonStub; - beforeEach(() => { - clock = useFakeTimers(); - clock.setSystemTime(123456); - target = mockClient(); - getClientByEntityId = stub(world, 'getClientByEntityId').withArgs(222).returns(target); - client.account.roles = ['admin']; - client.account.name = 'Acc'; - client.character.name = 'Char'; - client.isMod = true; - }); + beforeEach(() => { + clock = useFakeTimers(); + clock.setSystemTime(123456); + target = mockClient(); + getClientByEntityId = stub(world, 'getClientByEntityId').withArgs(222).returns(target); + client.account.roles = ['admin']; + client.account.name = 'Acc'; + client.character.name = 'Char'; + client.isMod = true; + }); - afterEach(() => { - clock.restore(); - }); + afterEach(() => { + clock.restore(); + }); - it('reports target client', async () => { - const system = stub(target.reporter, 'system'); + it('reports target client', async () => { + const system = stub(target.reporter, 'system'); - await serverActions.otherAction(222, ModAction.Report, 0); + await serverActions.otherAction(222, ModAction.Report, 0); - assert.calledWith(system, 'Reported by Acc'); - }); + assert.calledWith(system, 'Reported by Acc'); + }); - it('mutes target client', async () => { - const system = stub(target.reporter, 'system'); + it('mutes target client', async () => { + const system = stub(target.reporter, 'system'); - await serverActions.otherAction(222, ModAction.Mute, -1); + await serverActions.otherAction(222, ModAction.Mute, -1); - assert.calledWith(system, 'Muted by Acc'); - assert.calledWith(accountService.update, target.accountId, { mute: -1 }); - }); + assert.calledWith(system, 'Muted by Acc'); + assert.calledWith(accountService.update, target.accountId, { mute: -1 }); + }); - it('mutes target client for given amount of time', async () => { - const system = stub(target.reporter, 'system'); + it('mutes target client for given amount of time', async () => { + const system = stub(target.reporter, 'system'); - await serverActions.otherAction(222, ModAction.Mute, 123); + await serverActions.otherAction(222, ModAction.Mute, 123); - assert.calledWith(system, 'Muted for (a few seconds) by Acc'); - assert.calledWith(accountService.update, target.accountId, { mute: Date.now() + 123 }); - }); + assert.calledWith(system, 'Muted for (a few seconds) by Acc'); + assert.calledWith(accountService.update, target.accountId, { mute: Date.now() + 123 }); + }); - it('unmutes target client', async () => { - const system = stub(target.reporter, 'system'); + it('unmutes target client', async () => { + const system = stub(target.reporter, 'system'); - await serverActions.otherAction(222, ModAction.Mute, 0); + await serverActions.otherAction(222, ModAction.Mute, 0); - assert.calledWith(system, 'Unmuted by Acc'); - assert.calledWith(accountService.update, target.accountId, { mute: 0 }); - }); + assert.calledWith(system, 'Unmuted by Acc'); + assert.calledWith(accountService.update, target.accountId, { mute: 0 }); + }); - it('shadows target client', async () => { - const system = stub(target.reporter, 'system'); + it('shadows target client', async () => { + const system = stub(target.reporter, 'system'); - await serverActions.otherAction(222, ModAction.Shadow, -1); + await serverActions.otherAction(222, ModAction.Shadow, -1); - assert.calledWith(system, 'Shadowed by Acc'); - assert.calledWith(accountService.update, target.accountId, { shadow: -1 }); - }); + assert.calledWith(system, 'Shadowed by Acc'); + assert.calledWith(accountService.update, target.accountId, { shadow: -1 }); + }); - it('shadows target client for given amount of time', async () => { - const system = stub(target.reporter, 'system'); + it('shadows target client for given amount of time', async () => { + const system = stub(target.reporter, 'system'); - await serverActions.otherAction(222, ModAction.Shadow, 123); + await serverActions.otherAction(222, ModAction.Shadow, 123); - assert.calledWith(system, 'Shadowed for (a few seconds) by Acc'); - assert.calledWith(accountService.update, target.accountId, { shadow: Date.now() + 123 }); - }); + assert.calledWith(system, 'Shadowed for (a few seconds) by Acc'); + assert.calledWith(accountService.update, target.accountId, { shadow: Date.now() + 123 }); + }); - it('unshadows target client', async () => { - const system = stub(target.reporter, 'system'); + it('unshadows target client', async () => { + const system = stub(target.reporter, 'system'); - await serverActions.otherAction(222, ModAction.Shadow, 0); + await serverActions.otherAction(222, ModAction.Shadow, 0); - assert.calledWith(system, 'Unshadowed by Acc'); - assert.calledWith(accountService.update, target.accountId, { shadow: 0 }); - }); + assert.calledWith(system, 'Unshadowed by Acc'); + assert.calledWith(accountService.update, target.accountId, { shadow: 0 }); + }); - it('updates last action', async () => { - client.lastPacket = 0; - clock.tick(1000); + it('updates last action', async () => { + client.lastPacket = 0; + clock.tick(1000); - await serverActions.otherAction(222, 1, 1); + await serverActions.otherAction(222, 1, 1); - expect(client.lastPacket).not.equal(0); - }); + expect(client.lastPacket).not.equal(0); + }); - it('rejects on missing client', async () => { - await expect(serverActions.otherAction(111, ModAction.Report, 0)).rejectedWith('Client does not exist (Report)'); - }); + it('rejects on missing client', async () => { + await expect(serverActions.otherAction(111, ModAction.Report, 0)).rejectedWith('Client does not exist (Report)'); + }); - it('rejects on non admin user', async () => { - client.account.roles = []; - client.isMod = false; + it('rejects on non admin user', async () => { + client.account.roles = []; + client.isMod = false; - await expect(serverActions.otherAction(111, ModAction.Report, 0)).rejectedWith('Action not allowed (Report)'); - }); + await expect(serverActions.otherAction(111, ModAction.Report, 0)).rejectedWith('Action not allowed (Report)'); + }); - it('disconnectes on non admin user', async () => { - client.account.roles = []; - client.isMod = false; - const disconnect = stub(client, 'disconnect'); + it('disconnectes on non admin user', async () => { + client.account.roles = []; + client.isMod = false; + const disconnect = stub(client, 'disconnect'); - try { - await serverActions.otherAction(111, ModAction.Report, 0); - } catch { } + try { + await serverActions.otherAction(111, ModAction.Report, 0); + } catch { } - assert.calledWith(disconnect, true, true); - }); + assert.calledWith(disconnect, true, true); + }); - it('rejects on action on self', async () => { - getClientByEntityId.withArgs(1).returns(client); + it('rejects on action on self', async () => { + getClientByEntityId.withArgs(1).returns(client); - await expect(serverActions.otherAction(1, ModAction.Report, 0)).rejectedWith('Cannot perform action on self (Report)'); - }); + await expect(serverActions.otherAction(1, ModAction.Report, 0)).rejectedWith('Cannot perform action on self (Report)'); + }); - it('rejects on invalid action', async () => { - await expect(serverActions.otherAction(222, 123, 0)).rejectedWith('Invalid mod action (123)'); - }); + it('rejects on invalid action', async () => { + await expect(serverActions.otherAction(222, 123, 0)).rejectedWith('Invalid mod action (123)'); + }); - it('rejects on entityId not a number', async () => { - await expect(serverActions.otherAction('foo' as any, 1, 1)).rejectedWith('Not a number (entityId)'); - }); + it('rejects on entityId not a number', async () => { + await expect(serverActions.otherAction('foo' as any, 1, 1)).rejectedWith('Not a number (entityId)'); + }); - it('rejects on action not a number', async () => { - await expect(serverActions.otherAction(1, 'foo' as any, 1)).rejectedWith('Not a number (action)'); - }); + it('rejects on action not a number', async () => { + await expect(serverActions.otherAction(1, 'foo' as any, 1)).rejectedWith('Not a number (action)'); + }); - it('rejects on param not a number', async () => { - await expect(serverActions.otherAction(1, 1, 'foo' as any)).rejectedWith('Not a number (param)'); - }); - }); + it('rejects on param not a number', async () => { + await expect(serverActions.otherAction(1, 1, 'foo' as any)).rejectedWith('Not a number (param)'); + }); + }); - describe('setNote()', () => { - it('updates last action', async () => { - const other = mockClient(); - other.accountId = 'dlfhigdh'; - client.lastPacket = 0; - client.account.roles = ['mod']; - client.isMod = true; - stub(world, 'getClientByEntityId').withArgs(1).returns(other); + describe('setNote()', () => { + it('updates last action', async () => { + const other = mockClient(); + other.accountId = 'dlfhigdh'; + client.lastPacket = 0; + client.account.roles = ['mod']; + client.isMod = true; + stub(world, 'getClientByEntityId').withArgs(1).returns(other); - await serverActions.setNote(1, 'foo'); + await serverActions.setNote(1, 'foo'); - expect(client.lastPacket).not.equal(0); - }); + expect(client.lastPacket).not.equal(0); + }); - it('updates account note', async () => { - const other = mockClient(); - other.accountId = 'gooboo'; - client.account.roles = ['mod']; - client.isMod = true; - stub(world, 'getClientByEntityId').withArgs(1).returns(other); + it('updates account note', async () => { + const other = mockClient(); + other.accountId = 'gooboo'; + client.account.roles = ['mod']; + client.isMod = true; + stub(world, 'getClientByEntityId').withArgs(1).returns(other); - await serverActions.setNote(1, 'foo'); + await serverActions.setNote(1, 'foo'); - assert.calledWithMatch(accountService.update, 'gooboo', { note: 'foo' }); - }); + assert.calledWithMatch(accountService.update, 'gooboo', { note: 'foo' }); + }); - it('throws if user is not a mod', async () => { - const other = mockClient(); - other.accountId = 'gooboo'; - stub(world, 'getClientByEntityId').withArgs(1).returns(other); + it('throws if user is not a mod', async () => { + const other = mockClient(); + other.accountId = 'gooboo'; + stub(world, 'getClientByEntityId').withArgs(1).returns(other); - await expect(serverActions.setNote(1, 'foo')).rejectedWith('Action not allowed (setNote)'); - }); + await expect(serverActions.setNote(1, 'foo')).rejectedWith('Action not allowed (setNote)'); + }); - it('throws on not a number', async () => { - await expect(serverActions.setNote('foo' as any, 'foo')).rejectedWith('Not a number (entityId)'); - }); + it('throws on not a number', async () => { + await expect(serverActions.setNote('foo' as any, 'foo')).rejectedWith('Not a number (entityId)'); + }); - it('throws on not a string', async () => { - await expect(serverActions.setNote(1, 5 as any)).rejectedWith('Not a string (text)'); - }); - }); + it('throws on not a string', async () => { + await expect(serverActions.setNote(1, 5 as any)).rejectedWith('Not a string (text)'); + }); + }); - describe('saveSettings()', () => { - it('updates last action', () => { - accountService.updateSettings.resolves(); - client.lastPacket = 0; + describe('saveSettings()', () => { + it('updates last action', () => { + accountService.updateSettings.resolves(); + client.lastPacket = 0; - serverActions.saveSettings({}); + serverActions.saveSettings({}); - expect(client.lastPacket).not.equal(0); - }); + expect(client.lastPacket).not.equal(0); + }); - it('updates account settings', () => { - accountService.updateSettings.resolves(); - const settings = {}; + it('updates account settings', () => { + accountService.updateSettings.resolves(); + const settings = {}; - serverActions.saveSettings(settings); + serverActions.saveSettings(settings); - assert.calledWith(accountService.updateSettings, client.account, settings); - }); - }); + assert.calledWith(accountService.updateSettings, client.account, settings); + }); + }); - describe('acceptNotification()', () => { - it('accepts notification', () => { - serverActions.acceptNotification(123); + describe('acceptNotification()', () => { + it('accepts notification', () => { + serverActions.acceptNotification(123); - assert.calledWith(notifications.acceptNotification, client, 123); - }); + assert.calledWith(notifications.acceptNotification, client, 123); + }); - it('updates last action', () => { - client.lastPacket = 0; + it('updates last action', () => { + client.lastPacket = 0; - serverActions.acceptNotification(123); + serverActions.acceptNotification(123); - expect(client.lastPacket).not.equal(0); - }); + expect(client.lastPacket).not.equal(0); + }); - it('throws on not a number', () => { - expect(() => serverActions.acceptNotification('foo' as any)).throw('Not a number (id)'); - }); - }); + it('throws on not a number', () => { + expect(() => serverActions.acceptNotification('foo' as any)).throw('Not a number (id)'); + }); + }); - describe('rejectNotification()', () => { - it('rejects notification', () => { - serverActions.rejectNotification(123); + describe('rejectNotification()', () => { + it('rejects notification', () => { + serverActions.rejectNotification(123); - assert.calledWith(notifications.rejectNotification, client, 123); - }); + assert.calledWith(notifications.rejectNotification, client, 123); + }); - it('updates last action', () => { - client.lastPacket = 0; + it('updates last action', () => { + client.lastPacket = 0; - serverActions.rejectNotification(123); + serverActions.rejectNotification(123); - expect(client.lastPacket).not.equal(0); - }); + expect(client.lastPacket).not.equal(0); + }); - it('throws on not a number', () => { - expect(() => serverActions.rejectNotification('foo' as any)).throw('Not a number (id)'); - }); - }); + it('throws on not a number', () => { + expect(() => serverActions.rejectNotification('foo' as any)).throw('Not a number (id)'); + }); + }); - describe('getPonies()', () => { - it('sends ponies to client', () => { - const updatePonies = stub(client, 'updatePonies'); - const name1 = encodeString('foo')!; - const name2 = encodeString('bar')!; - const name3 = encodeString('xxx')!; - const info1 = new Uint8Array([1, 2, 3]); - const info2 = new Uint8Array([4, 5, 6]); - const info3 = new Uint8Array([7, 8, 9]); - client.party = { - clients: [ - { pony: { id: 1, options: {}, encodedName: name1, encryptedInfoSafe: info1 } }, - { pony: { id: 2, options: {}, encodedName: name2, encryptedInfoSafe: info2 } }, - { pony: { id: 3, options: {}, encodedName: name3, encryptedInfoSafe: info3 } }, - ], - } as any; + describe('getPonies()', () => { + it('sends ponies to client', () => { + const updatePonies = stub(client, 'updatePonies'); + const name1 = encodeString('foo')!; + const name2 = encodeString('bar')!; + const name3 = encodeString('xxx')!; + const info1 = new Uint8Array([1, 2, 3]); + const info2 = new Uint8Array([4, 5, 6]); + const info3 = new Uint8Array([7, 8, 9]); + client.party = { + clients: [ + { pony: { id: 1, options: {}, encodedName: name1, encryptedInfoSafe: info1 } }, + { pony: { id: 2, options: {}, encodedName: name2, encryptedInfoSafe: info2 } }, + { pony: { id: 3, options: {}, encodedName: name3, encryptedInfoSafe: info3 } }, + ], + } as any; - serverActions.getPonies([1, 2]); + serverActions.getPonies([1, 2]); - assert.calledWithMatch(updatePonies, [ - [1, {}, name1, info1, 0, false], - [2, {}, name2, info2, 0, false], - ]); - }); + assert.calledWithMatch(updatePonies, [ + [1, {}, name1, info1, 0, false], + [2, {}, name2, info2, 0, false], + ]); + }); - it('does nothing if not in party', () => { - const updatePonies = stub(client, 'updatePonies'); + it('does nothing if not in party', () => { + const updatePonies = stub(client, 'updatePonies'); - serverActions.getPonies([1, 2]); + serverActions.getPonies([1, 2]); - assert.notCalled(updatePonies); - }); + assert.notCalled(updatePonies); + }); - it('does nothing if ids is null or empty', () => { - const updatePonies = stub(client, 'updatePonies'); + it('does nothing if ids is null or empty', () => { + const updatePonies = stub(client, 'updatePonies'); - serverActions.getPonies(null as any); - serverActions.getPonies([]); + serverActions.getPonies(null as any); + serverActions.getPonies([]); - assert.notCalled(updatePonies); - }); + assert.notCalled(updatePonies); + }); - it('does nothing if requesting too many ponies', () => { - const updatePonies = stub(client, 'updatePonies'); + it('does nothing if requesting too many ponies', () => { + const updatePonies = stub(client, 'updatePonies'); - serverActions.getPonies(range(20)); + serverActions.getPonies(range(20)); - assert.notCalled(updatePonies); - }); - }); + assert.notCalled(updatePonies); + }); + }); - describe('loaded()', () => { - it('sets ignoreUpdates flag to false', () => { - client.loading = true; + describe('loaded()', () => { + it('sets ignoreUpdates flag to false', () => { + client.loading = true; - serverActions.loaded(); + serverActions.loaded(); - expect(client.loading).false; - }); - }); + expect(client.loading).false; + }); + }); - describe('fixedPosition()', () => { - it('sets fixing position flag to false', () => { - client.fixingPosition = true; + describe('fixedPosition()', () => { + it('sets fixing position flag to false', () => { + client.fixingPosition = true; - serverActions.fixedPosition(); + serverActions.fixedPosition(); - expect(client.fixingPosition).false; - }); - }); + expect(client.fixingPosition).false; + }); + }); - describe('updateCamera()', () => { - let clock: SinonFakeTimers; + describe('updateCamera()', () => { + let clock: SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers(); - }); + beforeEach(() => { + clock = useFakeTimers(); + }); - afterEach(() => { - clock.restore(); - }); + afterEach(() => { + clock.restore(); + }); - it('sets up camera', () => { - serverActions.updateCamera(1, 2, 3, 4); + it('sets up camera', () => { + serverActions.updateCamera(1, 2, 3, 4); - expect(client.camera).eql({ ...createCamera(), x: 1, y: 2, w: 64, h: 64 }); - }); + expect(client.camera).eql({ ...createCamera(), x: 1, y: 2, w: 64, h: 64 }); + }); - it('updates last action', () => { - client.lastPacket = 0; - clock.setSystemTime(123); + it('updates last action', () => { + client.lastPacket = 0; + clock.setSystemTime(123); - serverActions.updateCamera(0, 0, 0, 0); + serverActions.updateCamera(0, 0, 0, 0); - expect(client.lastPacket).equal(123); - }); + expect(client.lastPacket).equal(123); + }); - it('throws on "a" not a number', () => { - expect(() => serverActions.updateCamera('foo' as any, 0, 0, 0)).throw('Not a number (x)'); - }); + it('throws on "a" not a number', () => { + expect(() => serverActions.updateCamera('foo' as any, 0, 0, 0)).throw('Not a number (x)'); + }); - it('throws on "b" not a number', () => { - expect(() => serverActions.updateCamera(0, 'foo' as any, 0, 0)).throw('Not a number (y)'); - }); + it('throws on "b" not a number', () => { + expect(() => serverActions.updateCamera(0, 'foo' as any, 0, 0)).throw('Not a number (y)'); + }); - it('throws on "c" not a number', () => { - expect(() => serverActions.updateCamera(0, 0, 'foo' as any, 0)).throw('Not a number (width)'); - }); + it('throws on "c" not a number', () => { + expect(() => serverActions.updateCamera(0, 0, 'foo' as any, 0)).throw('Not a number (width)'); + }); - it('throws on "d" not a number', () => { - expect(() => serverActions.updateCamera(0, 0, 0, 'foo' as any)).throw('Not a number (height)'); - }); - }); + it('throws on "d" not a number', () => { + expect(() => serverActions.updateCamera(0, 0, 0, 'foo' as any)).throw('Not a number (height)'); + }); + }); - describe('update()', () => { - let clock: SinonFakeTimers; + describe('update()', () => { + let clock: SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers(); - }); + beforeEach(() => { + clock = useFakeTimers(); + }); - afterEach(() => { - clock.restore(); - }); + afterEach(() => { + clock.restore(); + }); - it('calls move', () => { - serverActions.move(1, 2, 3, 4, 5); + it('calls move', () => { + serverActions.move(1, 2, 3, 4, 5); - assert.calledWith(move, client, 0, 1, 2, 3, 4, 5); - }); + assert.calledWith(move, client, 0, 1, 2, 3, 4, 5); + }); - it('updates last action', () => { - client.lastPacket = 0; - clock.setSystemTime(123); + it('updates last action', () => { + client.lastPacket = 0; + clock.setSystemTime(123); - serverActions.move(0, 0, 0, 0, 0); + serverActions.move(0, 0, 0, 0, 0); - expect(client.lastPacket).equal(123); - }); + expect(client.lastPacket).equal(123); + }); - it('throws on "a" not a number', () => { - expect(() => serverActions.move('foo' as any, 0, 0, 0, 0)).throw('Not a number (a)'); - }); + it('throws on "a" not a number', () => { + expect(() => serverActions.move('foo' as any, 0, 0, 0, 0)).throw('Not a number (a)'); + }); - it('throws on "b" not a number', () => { - expect(() => serverActions.move(0, 'foo' as any, 0, 0, 0)).throw('Not a number (b)'); - }); + it('throws on "b" not a number', () => { + expect(() => serverActions.move(0, 'foo' as any, 0, 0, 0)).throw('Not a number (b)'); + }); - it('throws on "c" not a number', () => { - expect(() => serverActions.move(0, 0, 'foo' as any, 0, 0)).throw('Not a number (c)'); - }); + it('throws on "c" not a number', () => { + expect(() => serverActions.move(0, 0, 'foo' as any, 0, 0)).throw('Not a number (c)'); + }); - it('throws on "d" not a number', () => { - expect(() => serverActions.move(0, 0, 0, 'foo' as any, 0)).throw('Not a number (d)'); - }); + it('throws on "d" not a number', () => { + expect(() => serverActions.move(0, 0, 0, 'foo' as any, 0)).throw('Not a number (d)'); + }); - it('throws on "e" not a number', () => { - expect(() => serverActions.move(0, 0, 0, 0, 'foo' as any)).throw('Not a number (e)'); - }); - }); + it('throws on "e" not a number', () => { + expect(() => serverActions.move(0, 0, 0, 0, 'foo' as any)).throw('Not a number (e)'); + }); + }); - describe('changeTile()', () => { - it('sets tile', () => { - setTile(client.map, 1, 2, TileType.Dirt); - const setTileStub = stub(world, 'setTile'); + describe('changeTile()', () => { + it('sets tile', () => { + setTile(client.map, 1, 2, TileType.Dirt); + const setTileStub = stub(world, 'setTile'); - serverActions.changeTile(1, 2, TileType.Dirt); + serverActions.changeTile(1, 2, TileType.Dirt); - assert.calledWith(setTileStub, client.map, 1, 2, TileType.Dirt); - }); + assert.calledWith(setTileStub, client.map, 1, 2, TileType.Dirt); + }); - it.skip('sets tile for mod', () => { - const setTile = stub(world, 'setTile'); - client.isMod = true; + it.skip('sets tile for mod', () => { + const setTile = stub(world, 'setTile'); + client.isMod = true; - serverActions.changeTile(1, 2, TileType.Wood); + serverActions.changeTile(1, 2, TileType.Wood); - assert.calledWith(setTile, client.map, 1, 2, TileType.Wood); - }); + assert.calledWith(setTile, client.map, 1, 2, TileType.Wood); + }); - it('does not set tile if shadowed', () => { - setTile(client.map, 1, 2, TileType.Dirt); - const setTileStub = stub(world, 'setTile'); - client.shadowed = true; + it('does not set tile if shadowed', () => { + setTile(client.map, 1, 2, TileType.Dirt); + const setTileStub = stub(world, 'setTile'); + client.shadowed = true; - serverActions.changeTile(1, 2, TileType.Dirt); + serverActions.changeTile(1, 2, TileType.Dirt); - assert.notCalled(setTileStub); - }); + assert.notCalled(setTileStub); + }); - it('sends update to client if shadowed', () => { - setTile(client.map, 1, 2, TileType.Dirt); - client.shadowed = true; + it('sends update to client if shadowed', () => { + setTile(client.map, 1, 2, TileType.Dirt); + client.shadowed = true; - serverActions.changeTile(1, 2, TileType.Dirt); + serverActions.changeTile(1, 2, TileType.Dirt); - expect(getWriterBuffer(client.updateQueue)).eql(new Uint8Array([4, 0, 1, 0, 2, 1])); - }); + expect(getWriterBuffer(client.updateQueue)).eql(new Uint8Array([4, 0, 1, 0, 2, 1])); + }); - it('does nothing if invalid tile type', () => { - setTile(client.map, 1, 2, TileType.Dirt); - const setTileStub = stub(world, 'setTile'); + it('does nothing if invalid tile type', () => { + setTile(client.map, 1, 2, TileType.Dirt); + const setTileStub = stub(world, 'setTile'); - serverActions.changeTile(1, 2, 999); + serverActions.changeTile(1, 2, 999); - assert.notCalled(setTileStub); - expect(getWriterBuffer(client.updateQueue)).eql(new Uint8Array([])); - }); + assert.notCalled(setTileStub); + expect(getWriterBuffer(client.updateQueue)).eql(new Uint8Array([])); + }); - it.skip('toggles wall', () => { - setTile(client.map, 1, 2, TileType.Dirt); - client.account.roles = ['mod']; - client.isMod = true; - const toggleWall = stub(world, 'toggleWall'); + it.skip('toggles wall', () => { + setTile(client.map, 1, 2, TileType.Dirt); + client.account.roles = ['mod']; + client.isMod = true; + const toggleWall = stub(world, 'toggleWall'); - serverActions.changeTile(1, 2, TileType.WallH); - serverActions.changeTile(3, 4, TileType.WallV); + serverActions.changeTile(1, 2, TileType.WallH); + serverActions.changeTile(3, 4, TileType.WallV); - assert.calledWith(toggleWall, client.map, 1, 2, TileType.WallH); - assert.calledWith(toggleWall, client.map, 3, 4, TileType.WallV); - }); + assert.calledWith(toggleWall, client.map, 1, 2, TileType.WallH); + assert.calledWith(toggleWall, client.map, 3, 4, TileType.WallV); + }); - it('does not toggle wall for non moderators', () => { - setTile(client.map, 1, 2, TileType.Dirt); - const toggleWall = stub(world, 'toggleWall'); + it('does not toggle wall for non moderators', () => { + setTile(client.map, 1, 2, TileType.Dirt); + const toggleWall = stub(world, 'toggleWall'); - serverActions.changeTile(1, 2, TileType.WallH); + serverActions.changeTile(1, 2, TileType.WallH); - assert.notCalled(toggleWall); - }); + assert.notCalled(toggleWall); + }); - it('updates last action', () => { - client.lastPacket = 0; + it('updates last action', () => { + client.lastPacket = 0; - serverActions.changeTile(0, 0, TileType.Dirt); + serverActions.changeTile(0, 0, TileType.Dirt); - expect(client.lastPacket).not.equal(0); - }); + expect(client.lastPacket).not.equal(0); + }); - it('throws on "x" not a number', () => { - expect(() => serverActions.changeTile('foo' as any, 0, TileType.Dirt)).throw('Not a number (x)'); - }); + it('throws on "x" not a number', () => { + expect(() => serverActions.changeTile('foo' as any, 0, TileType.Dirt)).throw('Not a number (x)'); + }); - it('throws on "y" not a number', () => { - expect(() => serverActions.changeTile(0, 'foo' as any, TileType.Dirt)).throw('Not a number (y)'); - }); + it('throws on "y" not a number', () => { + expect(() => serverActions.changeTile(0, 'foo' as any, TileType.Dirt)).throw('Not a number (y)'); + }); - it('throws on "type" not a number', () => { - expect(() => serverActions.changeTile(0, 0, 'foo' as any)).throw('Not a number (type)'); - }); - }); + it('throws on "type" not a number', () => { + expect(() => serverActions.changeTile(0, 0, 'foo' as any)).throw('Not a number (type)'); + }); + }); - describe('leave()', () => { - it('notifies client', () => { - const left = stub(client, 'left'); + describe('leave()', () => { + it('notifies client', () => { + const left = stub(client, 'left'); - serverActions.leave(); + serverActions.leave(); - assert.calledOnce(left); - }); - }); + assert.calledOnce(left); + }); + }); - describe('editorAction()', () => { - it('places', () => { - client.isMod = true; - server.flags.objects = true; + describe('editorAction()', () => { + it('places', () => { + client.isMod = true; + server.flags.objects = true; - serverActions.editorAction({ type: 'place', x: 0, y: 0, entity: 'foo' }); - }); + serverActions.editorAction({ type: 'place', x: 0, y: 0, entity: 'foo' }); + }); - it('undos', () => { - client.isMod = true; - server.flags.objects = true; + it('undos', () => { + client.isMod = true; + server.flags.objects = true; - serverActions.editorAction({ type: 'undo' }); - }); + serverActions.editorAction({ type: 'undo' }); + }); - it('clears', () => { - client.isMod = true; - server.flags.objects = true; + it('clears', () => { + client.isMod = true; + server.flags.objects = true; - serverActions.editorAction({ type: 'clear' }); - }); + serverActions.editorAction({ type: 'clear' }); + }); - it('does nothing for non-mod client', () => { - client.isMod = false; - server.flags.objects = true; + it('does nothing for non-mod client', () => { + client.isMod = false; + server.flags.objects = true; - serverActions.editorAction({ type: 'undo' }); - }); - }); + serverActions.editorAction({ type: 'undo' }); + }); + }); }); diff --git a/src/ts/tests/server/serverMap.spec.ts b/src/ts/tests/server/serverMap.spec.ts index b38a085..a7b71f0 100644 --- a/src/ts/tests/server/serverMap.spec.ts +++ b/src/ts/tests/server/serverMap.spec.ts @@ -5,49 +5,49 @@ import { getRegion, getRegionGlobal } from '../../common/worldMap'; import { ServerMap } from '../../server/serverInterfaces'; describe('serverMap', () => { - let map: ServerMap; + let map: ServerMap; - beforeEach(() => { - map = createServerMap('', 0, 10, 10); - }); + beforeEach(() => { + map = createServerMap('', 0, 10, 10); + }); - it('throws when provided zero size for any of the parameters', () => { - expect(() => createServerMap('', 0, 0, 1)).throw('Invalid map parameters'); - expect(() => createServerMap('', 0, 1, 0)).throw('Invalid map parameters'); - }); + it('throws when provided zero size for any of the parameters', () => { + expect(() => createServerMap('', 0, 0, 1)).throw('Invalid map parameters'); + expect(() => createServerMap('', 0, 1, 0)).throw('Invalid map parameters'); + }); - it('gets total width of map', () => { - expect(map.width).equal(80); - }); + it('gets total width of map', () => { + expect(map.width).equal(80); + }); - it('gets total height of map', () => { - expect(map.height).equal(80); - }); + it('gets total height of map', () => { + expect(map.height).equal(80); + }); - describe('getRegion()', () => { - it('returns region', () => { - expect(getRegion(map, 0, 0)).equal(map.regions[0]); - }); + describe('getRegion()', () => { + it('returns region', () => { + expect(getRegion(map, 0, 0)).equal(map.regions[0]); + }); - it('throws if out of range', () => { - expect(() => getRegion(map, -1, 0)).throw('Invalid region coords (-1, 0)'); - expect(() => getRegion(map, 0, -1)).throw('Invalid region coords (0, -1)'); - expect(() => getRegion(map, 99, 0)).throw('Invalid region coords (99, 0)'); - expect(() => getRegion(map, 0, 99)).throw('Invalid region coords (0, 99)'); - }); - }); + it('throws if out of range', () => { + expect(() => getRegion(map, -1, 0)).throw('Invalid region coords (-1, 0)'); + expect(() => getRegion(map, 0, -1)).throw('Invalid region coords (0, -1)'); + expect(() => getRegion(map, 99, 0)).throw('Invalid region coords (99, 0)'); + expect(() => getRegion(map, 0, 99)).throw('Invalid region coords (0, 99)'); + }); + }); - describe('getRegionGlobal()', () => { - it('returns region at given position', () => { - expect(getRegionGlobal(map, 0.5, 8.5)).equal(getRegion(map, 0, 1)); - }); + describe('getRegionGlobal()', () => { + it('returns region at given position', () => { + expect(getRegionGlobal(map, 0.5, 8.5)).equal(getRegion(map, 0, 1)); + }); - it('clamps position outside of the map (1)', () => { - expect(getRegionGlobal(map, -0.5, -10)).equal(getRegion(map, 0, 0)); - }); + it('clamps position outside of the map (1)', () => { + expect(getRegionGlobal(map, -0.5, -10)).equal(getRegion(map, 0, 0)); + }); - it('clamps position outside of the map (2)', () => { - expect(getRegionGlobal(map, 1000, 2000)).equal(getRegion(map, 9, 9)); - }); - }); + it('clamps position outside of the map (2)', () => { + expect(getRegionGlobal(map, 1000, 2000)).equal(getRegion(map, 9, 9)); + }); + }); }); diff --git a/src/ts/tests/server/serverRegion.spec.ts b/src/ts/tests/server/serverRegion.spec.ts index 580cdb8..436c850 100644 --- a/src/ts/tests/server/serverRegion.spec.ts +++ b/src/ts/tests/server/serverRegion.spec.ts @@ -1,7 +1,7 @@ import '../lib'; import { expect } from 'chai'; import { - resetRegionUpdates, setRegionTile, pushUpdateEntityToRegion, pushRemoveEntityToRegion, createServerRegion + resetRegionUpdates, setRegionTile, pushUpdateEntityToRegion, pushRemoveEntityToRegion, createServerRegion } from '../../server/serverRegion'; import { serverEntity } from '../mocks'; import { TileType, UpdateFlags } from '../../common/interfaces'; @@ -9,113 +9,113 @@ import { getRegionTile } from '../../common/region'; import { ServerRegion, EntityUpdate } from '../../server/serverInterfaces'; describe('serverRegion', () => { - let region: ServerRegion; + let region: ServerRegion; - beforeEach(() => { - region = createServerRegion(1, 2); - }); + beforeEach(() => { + region = createServerRegion(1, 2); + }); - it('has correct bounds', () => { - expect(region.bounds).eql({ x: 8, y: 16, w: 8, h: 8 }); - }); + it('has correct bounds', () => { + expect(region.bounds).eql({ x: 8, y: 16, w: 8, h: 8 }); + }); - it('has correct boundsWithBorder', () => { - expect(region.boundsWithBorder).eql({ x: 7, y: 15, w: 10, h: 10 }); - }); + it('has correct boundsWithBorder', () => { + expect(region.boundsWithBorder).eql({ x: 7, y: 15, w: 10, h: 10 }); + }); - it('sets and gets tile at given position', () => { - setRegionTile({} as any, region, 1, 2, TileType.Grass); + it('sets and gets tile at given position', () => { + setRegionTile({} as any, region, 1, 2, TileType.Grass); - expect(getRegionTile(region, 1, 2)).equal(TileType.Grass); - }); + expect(getRegionTile(region, 1, 2)).equal(TileType.Grass); + }); - describe('addUpdate()', () => { - it('adds entity update to update list', () => { - const entity = serverEntity(1, 5, 4); + describe('addUpdate()', () => { + it('adds entity update to update list', () => { + const entity = serverEntity(1, 5, 4); - pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position, x: 5, y: 4, vx: 0, vy: 0 }); + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position, x: 5, y: 4, vx: 0, vy: 0 }); - expect(region.entityUpdates).eql([ - { - entity, - flags: UpdateFlags.Position, - x: 5, - y: 4, - vx: 0, - vy: 0, - action: 0, - playerState: 0, - options: undefined, - }, - ]); - }); + expect(region.entityUpdates).eql([ + { + entity, + flags: UpdateFlags.Position, + x: 5, + y: 4, + vx: 0, + vy: 0, + action: 0, + playerState: 0, + options: undefined, + }, + ]); + }); - it('updates existing entity update', () => { - const entity = serverEntity(1, 5, 4); - pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.None }); + it('updates existing entity update', () => { + const entity = serverEntity(1, 5, 4); + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.None }); - pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position | UpdateFlags.Expression, x: 10, y: 11, vx: 5, vy: 3 }); + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position | UpdateFlags.Expression, x: 10, y: 11, vx: 5, vy: 3 }); - expect(region.entityUpdates).eql([ - { - entity, - flags: UpdateFlags.Position | UpdateFlags.Expression, - x: 10, - y: 11, - vx: 5, - vy: 3, - action: 0, - playerState: 0, - options: undefined, - }, - ]); - }); + expect(region.entityUpdates).eql([ + { + entity, + flags: UpdateFlags.Position | UpdateFlags.Expression, + x: 10, + y: 11, + vx: 5, + vy: 3, + action: 0, + playerState: 0, + options: undefined, + }, + ]); + }); - it('does not update position of existing entry if position flag is false', () => { - const entity = serverEntity(1, 5, 4); - pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position, x: 5, y: 4, vx: 0, vy: 0 }); - entity.x = 10; - entity.y = 11; - entity.vx = 5; - entity.vy = 3; + it('does not update position of existing entry if position flag is false', () => { + const entity = serverEntity(1, 5, 4); + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position, x: 5, y: 4, vx: 0, vy: 0 }); + entity.x = 10; + entity.y = 11; + entity.vx = 5; + entity.vy = 3; - pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Expression }); + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Expression }); - expect(region.entityUpdates).eql([ - { - entity, - flags: UpdateFlags.Position | UpdateFlags.Expression, - x: 5, - y: 4, - vx: 0, - vy: 0, - action: 0, - playerState: 0, - options: undefined, - }, - ]); - }); - }); + expect(region.entityUpdates).eql([ + { + entity, + flags: UpdateFlags.Position | UpdateFlags.Expression, + x: 5, + y: 4, + vx: 0, + vy: 0, + action: 0, + playerState: 0, + options: undefined, + }, + ]); + }); + }); - describe('addRemove()', () => { - it('adds entity remove to remove list', () => { - pushRemoveEntityToRegion(region, serverEntity(123)); + describe('addRemove()', () => { + it('adds entity remove to remove list', () => { + pushRemoveEntityToRegion(region, serverEntity(123)); - expect(region.entityRemoves).eql([123]); - }); - }); + expect(region.entityRemoves).eql([123]); + }); + }); - describe('resetRegionUpdates()', () => { - it('resets all update lists to empty lists', () => { - region.entityUpdates = [{}, {}] as any; - region.entityRemoves = [{}, {}] as any; - region.tileUpdates = [{}, {}] as any; + describe('resetRegionUpdates()', () => { + it('resets all update lists to empty lists', () => { + region.entityUpdates = [{}, {}] as any; + region.entityRemoves = [{}, {}] as any; + region.tileUpdates = [{}, {}] as any; - resetRegionUpdates(region); + resetRegionUpdates(region); - expect(region.entityUpdates).eql([]); - expect(region.entityRemoves).eql([]); - expect(region.tileUpdates).eql([]); - }); - }); + expect(region.entityUpdates).eql([]); + expect(region.entityRemoves).eql([]); + expect(region.tileUpdates).eql([]); + }); + }); }); diff --git a/src/ts/tests/server/serverUtils.spec.ts b/src/ts/tests/server/serverUtils.spec.ts index ee70ee8..ef3c23f 100644 --- a/src/ts/tests/server/serverUtils.spec.ts +++ b/src/ts/tests/server/serverUtils.spec.ts @@ -8,164 +8,164 @@ import { CharacterFlags } from '../../common/adminInterfaces'; import { TokenService } from '../../server/serverInterfaces'; describe('serverUtils', () => { - describe('tokenService()', () => { - let service: TokenService; - let socket: { - clearTokens: SinonStub; - token: SinonStub; - }; + describe('tokenService()', () => { + let service: TokenService; + let socket: { + clearTokens: SinonStub; + token: SinonStub; + }; - beforeEach(() => { - socket = { - clearTokens: stub(), - token: stub(), - }; + beforeEach(() => { + socket = { + clearTokens: stub(), + token: stub(), + }; - service = tokenService(socket as any); - }); + service = tokenService(socket as any); + }); - it('clears tokens for account', () => { - service.clearTokensForAccount('foo'); + it('clears tokens for account', () => { + service.clearTokensForAccount('foo'); - const filter = socket.clearTokens.args[0][0]; - expect(filter('', { accountId: 'foo' })).true; - expect(filter('', { accountId: 'bar' })).false; - assert.calledOnce(socket.clearTokens); - }); + const filter = socket.clearTokens.args[0][0]; + expect(filter('', { accountId: 'foo' })).true; + expect(filter('', { accountId: 'bar' })).false; + assert.calledOnce(socket.clearTokens); + }); - it('clears all tokens', () => { - service.clearTokensAll(); + it('clears all tokens', () => { + service.clearTokensAll(); - const filter = socket.clearTokens.args[0][0]; - expect(filter('', {})).true; - assert.calledOnce(socket.clearTokens); - }); + const filter = socket.clearTokens.args[0][0]; + expect(filter('', {})).true; + assert.calledOnce(socket.clearTokens); + }); - it('creates token', () => { - const token = { account: {}, character: {} } as any; + it('creates token', () => { + const token = { account: {}, character: {} } as any; - service.createToken(token); + service.createToken(token); - assert.calledWith(socket.token, token); - }); - }); + assert.calledWith(socket.token, token); + }); + }); - describe('toPonyObject()', () => { - it('returns pony object', () => { - const id = genId(); + describe('toPonyObject()', () => { + it('returns pony object', () => { + const id = genId(); - expect(toPonyObject(character({ - _id: Types.ObjectId(id), - name: 'foo', - desc: 'aaa', - info: 'info', - site: Types.ObjectId('000000000000000000000002'), - tag: 'tag', - lastUsed: new Date(123), - }))).eql({ - id: id, - name: 'foo', - desc: 'aaa', - info: 'info', - site: '000000000000000000000002', - tag: 'tag', - lastUsed: '1970-01-01T00:00:00.123Z', - hideSupport: undefined, - respawnAtSpawn: undefined, - }); - }); + expect(toPonyObject(character({ + _id: Types.ObjectId(id), + name: 'foo', + desc: 'aaa', + info: 'info', + site: Types.ObjectId('000000000000000000000002'), + tag: 'tag', + lastUsed: new Date(123), + }))).eql({ + id: id, + name: 'foo', + desc: 'aaa', + info: 'info', + site: '000000000000000000000002', + tag: 'tag', + lastUsed: '1970-01-01T00:00:00.123Z', + hideSupport: undefined, + respawnAtSpawn: undefined, + }); + }); - it('handles empty fields', () => { - const id = genId(); + it('handles empty fields', () => { + const id = genId(); - expect(toPonyObject(character({ - _id: Types.ObjectId(id), - name: 'foo', - }))).eql({ - id: id, - name: 'foo', - desc: '', - info: '', - site: undefined, - tag: undefined, - lastUsed: undefined, - hideSupport: undefined, - respawnAtSpawn: undefined, - }); - }); + expect(toPonyObject(character({ + _id: Types.ObjectId(id), + name: 'foo', + }))).eql({ + id: id, + name: 'foo', + desc: '', + info: '', + site: undefined, + tag: undefined, + lastUsed: undefined, + hideSupport: undefined, + respawnAtSpawn: undefined, + }); + }); - it('sets hide support field', () => { - const output = toPonyObject(character({ - _id: Types.ObjectId(genId()), - name: 'foo', - flags: CharacterFlags.HideSupport, - })); + it('sets hide support field', () => { + const output = toPonyObject(character({ + _id: Types.ObjectId(genId()), + name: 'foo', + flags: CharacterFlags.HideSupport, + })); - expect(output!.hideSupport).true; - }); + expect(output!.hideSupport).true; + }); - it('sets respawn at spawn field', () => { - const output = toPonyObject(character({ - _id: Types.ObjectId(genId()), - name: 'foo', - flags: CharacterFlags.RespawnAtSpawn, - })); + it('sets respawn at spawn field', () => { + const output = toPonyObject(character({ + _id: Types.ObjectId(genId()), + name: 'foo', + flags: CharacterFlags.RespawnAtSpawn, + })); - expect(output!.respawnAtSpawn).true; - }); + expect(output!.respawnAtSpawn).true; + }); - it('returns null for undefined character', () => { - expect(toPonyObject(undefined)).null; - }); - }); + it('returns null for undefined character', () => { + expect(toPonyObject(undefined)).null; + }); + }); - describe('toPonyObjectAdmin()', () => { - it('returns pony object', () => { - const id = genId(); + describe('toPonyObjectAdmin()', () => { + it('returns pony object', () => { + const id = genId(); - expect(toPonyObjectAdmin(character({ - _id: Types.ObjectId(id), - name: 'foo', - desc: 'aaa', - info: 'info', - site: Types.ObjectId('000000000000000000000001'), - tag: 'tag', - lastUsed: new Date(123), - creator: 'foo bar', - }))).eql({ - id: id, - name: 'foo', - desc: 'aaa', - info: 'info', - site: '000000000000000000000001', - tag: 'tag', - lastUsed: '1970-01-01T00:00:00.123Z', - hideSupport: undefined, - respawnAtSpawn: undefined, - creator: 'foo bar', - }); - }); + expect(toPonyObjectAdmin(character({ + _id: Types.ObjectId(id), + name: 'foo', + desc: 'aaa', + info: 'info', + site: Types.ObjectId('000000000000000000000001'), + tag: 'tag', + lastUsed: new Date(123), + creator: 'foo bar', + }))).eql({ + id: id, + name: 'foo', + desc: 'aaa', + info: 'info', + site: '000000000000000000000001', + tag: 'tag', + lastUsed: '1970-01-01T00:00:00.123Z', + hideSupport: undefined, + respawnAtSpawn: undefined, + creator: 'foo bar', + }); + }); - it('returns null for undefined character', () => { - expect(toPonyObjectAdmin(undefined)).null; - }); - }); + it('returns null for undefined character', () => { + expect(toPonyObjectAdmin(undefined)).null; + }); + }); - describe('toSocialSite()', () => { - it('returns site object', () => { - const id = genId(); + describe('toSocialSite()', () => { + it('returns site object', () => { + const id = genId(); - expect(toSocialSite(auth({ - _id: Types.ObjectId(id), - name: 'foo', - provider: 'github', - url: 'foo.com', - }))).eql({ - id: id, - name: 'foo', - provider: 'github', - url: 'foo.com', - }); - }); - }); + expect(toSocialSite(auth({ + _id: Types.ObjectId(id), + name: 'foo', + provider: 'github', + url: 'foo.com', + }))).eql({ + id: id, + name: 'foo', + provider: 'github', + url: 'foo.com', + }); + }); + }); }); diff --git a/src/ts/tests/server/services/hiding.spec.ts b/src/ts/tests/server/services/hiding.spec.ts index 5da9406..6f73354 100644 --- a/src/ts/tests/server/services/hiding.spec.ts +++ b/src/ts/tests/server/services/hiding.spec.ts @@ -13,596 +13,596 @@ import { times } from '../../../common/utils'; const DURATION = 24 * HOUR; function isHidden(a: IClient, b: IClient) { - return a.hides.has(b.accountId) || b.hides.has(a.accountId); + return a.hides.has(b.accountId) || b.hides.has(a.accountId); } describe('HidingService', () => { - let notifications = stubClass(NotificationService); - let service: HidingService; - let clock: SinonFakeTimers; - let log: SinonStub; - let clients: Map; - - function addClients(...items: IClient[]) { - items.forEach(c => clients.set(c.accountId, c)); - } - - beforeEach(() => { - resetStubMethods(notifications, 'addNotification'); - clock = useFakeTimers(); - clock.setSystemTime(DAY); - log = stub(); - clients = new Map(); - service = new HidingService(HOUR, notifications, id => clients.get(id), log); - service.start(); - }); - - afterEach(() => { - clock.restore(); - service.stop(); - }); - - describe('requestHide()', () => { - it('adds hide notification', () => { - const requester = mockClient(); - const target = mockClient(); - target.pony.name = 'foo'; - - service.requestHide(requester, target, DURATION); - - assert.calledWith(notifications.addNotification, requester, match({ - name: 'foo', - message: `Are you sure you want to hide #NAME# ?`, - entityId: target.pony.id, - })); - }); - - it('adds hide limit notification if reached hide limit', () => { - const requester = mockClient(); - const target = mockClient(); - times(HIDE_LIMIT, () => service.hide(requester, mockClient(), DURATION)); - - service.requestHide(requester, target, DURATION); - - assert.calledWith(notifications.addNotification, requester, match({ - message: 'Cannot hide any more players.', - })); - }); - - it('accepting notification hides target player', () => { - const requester = mockClient(); - requester.characterName = 'req_pony'; - requester.account.name = 'req'; - requester.accountId = 'REQ'; - const target = mockClient(); - target.characterName = 'tgt_pony'; - target.account.name = 'tgt'; - target.accountId = 'TGT'; - service.requestHide(requester, target, DURATION); - - const notification = notifications.addNotification.args[0][1] as ServerNotification; - notification.accept!(); - - expect(isHidden(requester, target)).true; - expect(service.isHidden('REQ', 'TGT')).true; - assert.calledWith(log, '[REQ][system]\treq_pony (req) hides tgt_pony (tgt) [TGT]'); - }); - - it('does not log message if already hidden', () => { - const requester = mockClient(); - requester.accountId = 'REQ'; - const target = mockClient(); - target.accountId = 'TGT'; - service.requestHide(requester, target, DURATION); - service.hide(requester, target, DURATION); - - const notification = notifications.addNotification.args[0][1] as ServerNotification; - notification.accept!(); - - assert.notCalled(log); - }); - - it('prevents hiding party members', () => { - const requester = mockClient(); - const target = mockClient(); - requester.party = target.party = { id: '', clients: [requester, target], leader: requester, pending: [] }; - - service.requestHide(requester, target, DURATION); - - expect(isHidden(requester, target)).false; - assert.calledWith(notifications.addNotification, requester, match({ - message: 'Cannot hide players from your party.', - })); - }); - }); - - describe('requestUnhideAll()', () => { - it('adds unhide notification', () => { - const requester = mockClient(); - - service.requestUnhideAll(requester); - - assert.calledWith(notifications.addNotification, requester, match({ - message: 'Are you sure you want to unhide all temporarily hidden players ?', - })); - }); - - it('adds unhide notification if previous limit timed out', () => { - const requester = mockClient(); - const target = mockClient(); - - service.hide(requester, target, DURATION); - service.unhideAll(requester); - service.stop(); - clock.tick(HOUR + 1); - - service.requestUnhideAll(requester); - - assert.calledWith(notifications.addNotification, requester, match({ - message: 'Are you sure you want to unhide all temporarily hidden players ?', - })); - }); - - it('adds unhide limit notification if already used unhideAll', () => { - const requester = mockClient(); - const target = mockClient(); - - service.hide(requester, target, DURATION); - service.unhideAll(requester); - - service.requestUnhideAll(requester); - - assert.calledWith(notifications.addNotification, requester, match({ - message: 'Cannot unhide hidden players, try again later.', - })); - }); + let notifications = stubClass(NotificationService); + let service: HidingService; + let clock: SinonFakeTimers; + let log: SinonStub; + let clients: Map; + + function addClients(...items: IClient[]) { + items.forEach(c => clients.set(c.accountId, c)); + } + + beforeEach(() => { + resetStubMethods(notifications, 'addNotification'); + clock = useFakeTimers(); + clock.setSystemTime(DAY); + log = stub(); + clients = new Map(); + service = new HidingService(HOUR, notifications, id => clients.get(id), log); + service.start(); + }); + + afterEach(() => { + clock.restore(); + service.stop(); + }); + + describe('requestHide()', () => { + it('adds hide notification', () => { + const requester = mockClient(); + const target = mockClient(); + target.pony.name = 'foo'; + + service.requestHide(requester, target, DURATION); + + assert.calledWith(notifications.addNotification, requester, match({ + name: 'foo', + message: `Are you sure you want to hide #NAME# ?`, + entityId: target.pony.id, + })); + }); + + it('adds hide limit notification if reached hide limit', () => { + const requester = mockClient(); + const target = mockClient(); + times(HIDE_LIMIT, () => service.hide(requester, mockClient(), DURATION)); + + service.requestHide(requester, target, DURATION); + + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Cannot hide any more players.', + })); + }); + + it('accepting notification hides target player', () => { + const requester = mockClient(); + requester.characterName = 'req_pony'; + requester.account.name = 'req'; + requester.accountId = 'REQ'; + const target = mockClient(); + target.characterName = 'tgt_pony'; + target.account.name = 'tgt'; + target.accountId = 'TGT'; + service.requestHide(requester, target, DURATION); + + const notification = notifications.addNotification.args[0][1] as ServerNotification; + notification.accept!(); + + expect(isHidden(requester, target)).true; + expect(service.isHidden('REQ', 'TGT')).true; + assert.calledWith(log, '[REQ][system]\treq_pony (req) hides tgt_pony (tgt) [TGT]'); + }); + + it('does not log message if already hidden', () => { + const requester = mockClient(); + requester.accountId = 'REQ'; + const target = mockClient(); + target.accountId = 'TGT'; + service.requestHide(requester, target, DURATION); + service.hide(requester, target, DURATION); + + const notification = notifications.addNotification.args[0][1] as ServerNotification; + notification.accept!(); + + assert.notCalled(log); + }); + + it('prevents hiding party members', () => { + const requester = mockClient(); + const target = mockClient(); + requester.party = target.party = { id: '', clients: [requester, target], leader: requester, pending: [] }; + + service.requestHide(requester, target, DURATION); + + expect(isHidden(requester, target)).false; + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Cannot hide players from your party.', + })); + }); + }); + + describe('requestUnhideAll()', () => { + it('adds unhide notification', () => { + const requester = mockClient(); + + service.requestUnhideAll(requester); + + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Are you sure you want to unhide all temporarily hidden players ?', + })); + }); + + it('adds unhide notification if previous limit timed out', () => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + service.unhideAll(requester); + service.stop(); + clock.tick(HOUR + 1); + + service.requestUnhideAll(requester); + + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Are you sure you want to unhide all temporarily hidden players ?', + })); + }); + + it('adds unhide limit notification if already used unhideAll', () => { + const requester = mockClient(); + const target = mockClient(); + + service.hide(requester, target, DURATION); + service.unhideAll(requester); + + service.requestUnhideAll(requester); + + assert.calledWith(notifications.addNotification, requester, match({ + message: 'Cannot unhide hidden players, try again later.', + })); + }); - it('accepting notification unhides all players', () => { - const requester = mockClient(); - const target = mockClient(); - service.hide(requester, target, DURATION); - service.requestUnhideAll(requester); - expect(isHidden(requester, target)).true; - - const notification = notifications.addNotification.args[0][1] as ServerNotification; - notification.accept!(); - - expect(isHidden(requester, target)).false; - expect(service.isHidden(requester.accountId, target.accountId)).false; - assert.calledWith(log, systemMessage(requester.accountId, 'unhide all')); - }); - }); - - describe('hide()', () => { - it('hides target user from source user', () => { - const requester = mockClient(); - const target = mockClient(); - addClients(requester, target); + it('accepting notification unhides all players', () => { + const requester = mockClient(); + const target = mockClient(); + service.hide(requester, target, DURATION); + service.requestUnhideAll(requester); + expect(isHidden(requester, target)).true; + + const notification = notifications.addNotification.args[0][1] as ServerNotification; + notification.accept!(); + + expect(isHidden(requester, target)).false; + expect(service.isHidden(requester.accountId, target.accountId)).false; + assert.calledWith(log, systemMessage(requester.accountId, 'unhide all')); + }); + }); + + describe('hide()', () => { + it('hides target user from source user', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); - service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); - expect(isHidden(requester, target)).true; - expect(service.isHiddenClient(requester, target)).true; - }); - - it('does nothing if already hidden', () => { - const requester = mockClient(); - const target = mockClient(); - addClients(requester, target); + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); + + it('does nothing if already hidden', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); - service.hide(requester, target, DURATION); - service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); - expect(isHidden(requester, target)).true; - expect(service.isHiddenClient(requester, target)).true; - }); + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); - it('hides source user from target user', () => { - const requester = mockClient(); - const target = mockClient(); - addClients(requester, target); + it('hides source user from target user', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); - service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); - expect(isHidden(requester, target)).true; - expect(service.isHiddenClient(target, requester)).true; - }); + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(target, requester)).true; + }); - it('does not hide user from themselves', () => { - const requester = mockClient(); - addClients(requester); + it('does not hide user from themselves', () => { + const requester = mockClient(); + addClients(requester); - service.hide(requester, requester, DURATION); + service.hide(requester, requester, DURATION); - expect(isHidden(requester, requester)).false; - expect(service.isHiddenClient(requester, requester)).false; - }); + expect(isHidden(requester, requester)).false; + expect(service.isHiddenClient(requester, requester)).false; + }); - it('does not clean hides too early', () => { - const requester = mockClient(); - const target = mockClient(); - addClients(requester, target); + it('does not clean hides too early', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); - service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); - clock.tick(25 * MINUTE); + clock.tick(25 * MINUTE); - expect(isHidden(requester, target)).true; - expect(service.isHiddenClient(requester, target)).true; - }); + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); - it('cleans up old hides', () => { - const requester = mockClient(); - const target = mockClient(); - addClients(requester, target); + it('cleans up old hides', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); - service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); - clock.tick(25 * HOUR); + clock.tick(25 * HOUR); - expect(isHidden(requester, target)).false; - expect(service.isHiddenClient(requester, target)).false; - }); + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); - it('clears hide after given duration', () => { - const requester = mockClient(); - const target = mockClient(); - addClients(requester, target); + it('clears hide after given duration', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); - service.hide(requester, target, HOUR / 2); + service.hide(requester, target, HOUR / 2); - clock.tick(HOUR); + clock.tick(HOUR); - expect(isHidden(requester, target)).false; - expect(service.isHiddenClient(requester, target)).false; - }); + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); - it('triggers change event', done => { - const requester = mockClient(); - const target = mockClient(); - addClients(requester, target); - service.changes.subscribe(hide => { - expect(hide).eql({ by: requester.accountId, who: target.accountId }); - done(); - }); + it('triggers change event', done => { + const requester = mockClient(); + const target = mockClient(); + addClients(requester, target); + service.changes.subscribe(hide => { + expect(hide).eql({ by: requester.accountId, who: target.accountId }); + done(); + }); - service.hide(requester, target, DURATION); - }); - }); + service.hide(requester, target, DURATION); + }); + }); - describe('isHiddenClient()', () => { - it('hides target user from source user', () => { - const who = mockClient(); - const from = mockClient(); - service.hide(who, from, DURATION); + describe('isHiddenClient()', () => { + it('hides target user from source user', () => { + const who = mockClient(); + const from = mockClient(); + service.hide(who, from, DURATION); - expect(isHidden(who, from)).true; - expect(service.isHiddenClient(who, from)).true; - }); - }); + expect(isHidden(who, from)).true; + expect(service.isHiddenClient(who, from)).true; + }); + }); - describe('unhide()', () => { - it('unhides user', () => { - const requester = mockClient(); - const target = mockClient(); + describe('unhide()', () => { + it('unhides user', () => { + const requester = mockClient(); + const target = mockClient(); - service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); - service.unhide(requester, target); + service.unhide(requester, target); - expect(isHidden(requester, target)).false; - expect(service.isHiddenClient(requester, target)).false; - }); + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); - it('unhides only given user', () => { - const requester = mockClient(); - const target1 = mockClient(); - const target2 = mockClient(); + it('unhides only given user', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); - service.hide(requester, target1, DURATION); - service.hide(requester, target2, DURATION); + service.hide(requester, target1, DURATION); + service.hide(requester, target2, DURATION); - service.unhide(requester, target1); + service.unhide(requester, target1); - expect(isHidden(requester, target1)).false; - expect(isHidden(requester, target2)).true; - expect(service.isHiddenClient(requester, target1)).false; - expect(service.isHiddenClient(requester, target2)).true; - }); + expect(isHidden(requester, target1)).false; + expect(isHidden(requester, target2)).true; + expect(service.isHiddenClient(requester, target1)).false; + expect(service.isHiddenClient(requester, target2)).true; + }); - it('does nothing if not hidden', () => { - const requester = mockClient(); - const target = mockClient(); + it('does nothing if not hidden', () => { + const requester = mockClient(); + const target = mockClient(); - service.unhide(requester, target); + service.unhide(requester, target); - expect(isHidden(requester, target)).false; - expect(service.isHiddenClient(requester, target)).false; - }); + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); - it('does nothing if not hidden (2)', () => { - const requester = mockClient(); - const target1 = mockClient(); - const target2 = mockClient(); + it('does nothing if not hidden (2)', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); - service.hide(requester, target1, DURATION); + service.hide(requester, target1, DURATION); - service.unhide(requester, target2); + service.unhide(requester, target2); - expect(isHidden(requester, target1)).true; - expect(isHidden(requester, target2)).false; - expect(service.isHiddenClient(requester, target1)).true; - expect(service.isHiddenClient(requester, target2)).false; - }); + expect(isHidden(requester, target1)).true; + expect(isHidden(requester, target2)).false; + expect(service.isHiddenClient(requester, target1)).true; + expect(service.isHiddenClient(requester, target2)).false; + }); - it('triggers change event', done => { - const requester = mockClient(); - const target = mockClient(); + it('triggers change event', done => { + const requester = mockClient(); + const target = mockClient(); - service.hide(requester, target, DURATION); - service.changes.subscribe(hide => { - expect(hide).eql({ by: requester.accountId, who: target.accountId }); - done(); - }); + service.hide(requester, target, DURATION); + service.changes.subscribe(hide => { + expect(hide).eql({ by: requester.accountId, who: target.accountId }); + done(); + }); - service.unhide(requester, target); - }); - }); + service.unhide(requester, target); + }); + }); - describe('unhideAll()', () => { - it('does nothing if no users are hidden', () => { - const requester = mockClient(); - const target1 = mockClient(); - const target2 = mockClient(); - - service.unhideAll(requester); - - expect(isHidden(requester, target1)).false; - expect(isHidden(requester, target2)).false; - expect(service.isHiddenClient(requester, target1)).false; - expect(service.isHiddenClient(requester, target2)).false; - }); + describe('unhideAll()', () => { + it('does nothing if no users are hidden', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + + service.unhideAll(requester); + + expect(isHidden(requester, target1)).false; + expect(isHidden(requester, target2)).false; + expect(service.isHiddenClient(requester, target1)).false; + expect(service.isHiddenClient(requester, target2)).false; + }); - it('unhides all', () => { - const requester = mockClient(); - const target1 = mockClient(); - const target2 = mockClient(); + it('unhides all', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); - service.hide(requester, target1, DURATION); - service.hide(requester, target2, DURATION); + service.hide(requester, target1, DURATION); + service.hide(requester, target2, DURATION); - service.unhideAll(requester); + service.unhideAll(requester); - expect(isHidden(requester, target1)).false; - expect(isHidden(requester, target2)).false; - expect(service.isHiddenClient(requester, target1)).false; - expect(service.isHiddenClient(requester, target2)).false; - }); + expect(isHidden(requester, target1)).false; + expect(isHidden(requester, target2)).false; + expect(service.isHiddenClient(requester, target1)).false; + expect(service.isHiddenClient(requester, target2)).false; + }); - it('does not count to limit if noone is hidden', () => { - const requester = mockClient(); - const target = mockClient(); + it('does not count to limit if noone is hidden', () => { + const requester = mockClient(); + const target = mockClient(); - service.unhideAll(requester); - service.hide(requester, target, DURATION); - service.unhideAll(requester); + service.unhideAll(requester); + service.hide(requester, target, DURATION); + service.unhideAll(requester); - expect(isHidden(requester, target)).false; - expect(service.isHiddenClient(requester, target)).false; - }); + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); - it('prevents unhide all if used too fast', () => { - const requester = mockClient(); - const target = mockClient(); + it('prevents unhide all if used too fast', () => { + const requester = mockClient(); + const target = mockClient(); - service.hide(requester, target, DURATION); - service.unhideAll(requester); - service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); + service.unhideAll(requester); + service.hide(requester, target, DURATION); - service.unhideAll(requester); + service.unhideAll(requester); - expect(isHidden(requester, target)).true; - expect(service.isHiddenClient(requester, target)).true; - }); + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); - it('does not clean up old unhides too early', () => { - const requester = mockClient(); - const target = mockClient(); + it('does not clean up old unhides too early', () => { + const requester = mockClient(); + const target = mockClient(); - service.hide(requester, target, DURATION); - service.unhideAll(requester); - service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); + service.unhideAll(requester); + service.hide(requester, target, DURATION); - clock.tick(25 * MINUTE); + clock.tick(25 * MINUTE); - service.unhideAll(requester); - expect(isHidden(requester, target)).true; - expect(service.isHiddenClient(requester, target)).true; - }); + service.unhideAll(requester); + expect(isHidden(requester, target)).true; + expect(service.isHiddenClient(requester, target)).true; + }); - it('cleans up old unhides', () => { - const requester = mockClient(); - const target = mockClient(); + it('cleans up old unhides', () => { + const requester = mockClient(); + const target = mockClient(); - service.hide(requester, target, DURATION); - service.unhideAll(requester); - service.hide(requester, target, DURATION); + service.hide(requester, target, DURATION); + service.unhideAll(requester); + service.hide(requester, target, DURATION); - clock.tick(2 * HOUR); - - service.unhideAll(requester); - expect(isHidden(requester, target)).false; - expect(service.isHiddenClient(requester, target)).false; - }); + clock.tick(2 * HOUR); + + service.unhideAll(requester); + expect(isHidden(requester, target)).false; + expect(service.isHiddenClient(requester, target)).false; + }); - it('triggers unhidesAll event', done => { - const requester = mockClient(); - const target = mockClient(); + it('triggers unhidesAll event', done => { + const requester = mockClient(); + const target = mockClient(); - service.hide(requester, target, DURATION); - service.unhidesAll.subscribe(id => { - expect(id).equal(requester.accountId); - done(); - }); + service.hide(requester, target, DURATION); + service.unhidesAll.subscribe(id => { + expect(id).equal(requester.accountId); + done(); + }); - service.unhideAll(requester); - }); - }); + service.unhideAll(requester); + }); + }); - describe('merged()', () => { - it('transfers hide list to new account ID', () => { - const requester1 = mockClient(); - const requester2 = mockClient(); - const target = mockClient(); - addClients(requester2); + describe('merged()', () => { + it('transfers hide list to new account ID', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); + addClients(requester2); - service.hide(requester1, target, DURATION); + service.hide(requester1, target, DURATION); - service.merged(requester2.accountId, requester1.accountId); + service.merged(requester2.accountId, requester1.accountId); - expect(isHidden(requester2, target)).true; - expect(service.isHiddenClient(requester2, target)).true; - }); + expect(isHidden(requester2, target)).true; + expect(service.isHiddenClient(requester2, target)).true; + }); - it('transfers hide list to new account ID (no client)', () => { - const requester1 = mockClient(); - const requester2 = mockClient(); - const target = mockClient(); + it('transfers hide list to new account ID (no client)', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); - service.hide(requester1, target, DURATION); + service.hide(requester1, target, DURATION); - service.merged(requester2.accountId, requester1.accountId); + service.merged(requester2.accountId, requester1.accountId); - expect(service.isHiddenClient(requester2, target)).true; - }); + expect(service.isHiddenClient(requester2, target)).true; + }); - it('merges hide lists', () => { - const requester1 = mockClient(); - const requester2 = mockClient(); - const target1 = mockClient(); - const target2 = mockClient(); - addClients(requester2, target1, target2); + it('merges hide lists', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + addClients(requester2, target1, target2); - service.hide(requester1, target1, DURATION); - service.hide(requester2, target2, DURATION); + service.hide(requester1, target1, DURATION); + service.hide(requester2, target2, DURATION); - service.merged(requester2.accountId, requester1.accountId); - - expect(isHidden(requester2, target1)).true; - expect(isHidden(requester2, target2)).true; - expect(service.isHiddenClient(requester2, target1)).true; - expect(service.isHiddenClient(requester2, target2)).true; - }); - - it('does not allow to be hidden by yourself after merge', () => { - const requester = mockClient(); - const target = mockClient(); - addClients(target); - - service.hide(requester, target, DURATION); - // service.hide(target, requester, DURATION); - - service.merged(target.accountId, requester.accountId); - - expect(isHidden(target, target)).false; - // expect(isHidden(requester, target)).false; // requester client does not exist at this point - // expect(isHidden(target, requester)).false; // requester client does not exist at this point - expect(service.isHiddenClient(target, target)).false; - expect(service.isHiddenClient(requester, target)).false; - expect(service.isHiddenClient(target, requester)).false; - }); - - it('updates in hidden lists', () => { - const requester = mockClient(); - const target1 = mockClient(); - const target2 = mockClient(); - addClients(requester, target2); - - service.hide(requester, target1, DURATION); - - service.merged(target2.accountId, target1.accountId); - - expect(isHidden(requester, target2)).true; - expect(service.isHiddenClient(requester, target2)).true; - }); - - it('picks newer date in hidden lists', () => { - const requester = mockClient(); - const target1 = mockClient(); - const target2 = mockClient(); - addClients(requester, target2); - - clock.setSystemTime(HOUR); - service.hide(requester, target1, DURATION); - clock.setSystemTime(5 * HOUR); - service.hide(requester, target2, DURATION); - - service.merged(target2.accountId, target1.accountId); - - clock.tick(22 * HOUR); - expect(isHidden(requester, target2)).true; - expect(service.isHiddenClient(requester, target2)).true; - }); - - it('transfers unhide countdown', () => { - const requester1 = mockClient(); - const requester2 = mockClient(); - const target = mockClient(); - addClients(requester2, target); - - service.hide(requester1, target, DURATION); - service.unhideAll(requester1); - - service.merged(requester2.accountId, requester1.accountId); - - service.hide(requester2, target, DURATION); - service.unhideAll(requester2); - expect(isHidden(requester2, target)).true; - expect(service.isHiddenClient(requester2, target)).true; - }); - - it('does not trigger change event if state of user didn\'t change', () => { - const requester1 = mockClient(); - const requester2 = mockClient(); - const target = mockClient(); - addClients(requester2, target); - - service.hide(requester1, target, DURATION); - service.hide(requester2, target, DURATION); - - service.merged(requester2.accountId, requester1.accountId); - }); - - it('triggers change events (1)', done => { - const requester1 = mockClient(); - const requester2 = mockClient(); - const target = mockClient(); - addClients(requester2, target); - - service.hide(requester1, target, DURATION); - service.changes.pipe(bufferCount(2), first()).subscribe(([hide1, hide2]) => { - expect(hide1).eql({ by: requester2.accountId, who: target.accountId }); - expect(hide2).eql({ by: requester1.accountId, who: target.accountId }); - done(); - }); - - service.merged(requester2.accountId, requester1.accountId); - }); - - it('triggers change events (2)', done => { - const requester = mockClient(); - const target1 = mockClient(); - const target2 = mockClient(); - addClients(requester, target2); - - service.hide(requester, target1, DURATION); - service.changes.pipe(bufferCount(2), first()).subscribe(([hide1, hide2]) => { - expect(hide1).eql({ by: requester.accountId, who: target2.accountId }); - expect(hide2).eql({ by: requester.accountId, who: target1.accountId }); - done(); - }); - - service.merged(target2.accountId, target1.accountId); - }); - }); + service.merged(requester2.accountId, requester1.accountId); + + expect(isHidden(requester2, target1)).true; + expect(isHidden(requester2, target2)).true; + expect(service.isHiddenClient(requester2, target1)).true; + expect(service.isHiddenClient(requester2, target2)).true; + }); + + it('does not allow to be hidden by yourself after merge', () => { + const requester = mockClient(); + const target = mockClient(); + addClients(target); + + service.hide(requester, target, DURATION); + // service.hide(target, requester, DURATION); + + service.merged(target.accountId, requester.accountId); + + expect(isHidden(target, target)).false; + // expect(isHidden(requester, target)).false; // requester client does not exist at this point + // expect(isHidden(target, requester)).false; // requester client does not exist at this point + expect(service.isHiddenClient(target, target)).false; + expect(service.isHiddenClient(requester, target)).false; + expect(service.isHiddenClient(target, requester)).false; + }); + + it('updates in hidden lists', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + addClients(requester, target2); + + service.hide(requester, target1, DURATION); + + service.merged(target2.accountId, target1.accountId); + + expect(isHidden(requester, target2)).true; + expect(service.isHiddenClient(requester, target2)).true; + }); + + it('picks newer date in hidden lists', () => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + addClients(requester, target2); + + clock.setSystemTime(HOUR); + service.hide(requester, target1, DURATION); + clock.setSystemTime(5 * HOUR); + service.hide(requester, target2, DURATION); + + service.merged(target2.accountId, target1.accountId); + + clock.tick(22 * HOUR); + expect(isHidden(requester, target2)).true; + expect(service.isHiddenClient(requester, target2)).true; + }); + + it('transfers unhide countdown', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); + addClients(requester2, target); + + service.hide(requester1, target, DURATION); + service.unhideAll(requester1); + + service.merged(requester2.accountId, requester1.accountId); + + service.hide(requester2, target, DURATION); + service.unhideAll(requester2); + expect(isHidden(requester2, target)).true; + expect(service.isHiddenClient(requester2, target)).true; + }); + + it('does not trigger change event if state of user didn\'t change', () => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); + addClients(requester2, target); + + service.hide(requester1, target, DURATION); + service.hide(requester2, target, DURATION); + + service.merged(requester2.accountId, requester1.accountId); + }); + + it('triggers change events (1)', done => { + const requester1 = mockClient(); + const requester2 = mockClient(); + const target = mockClient(); + addClients(requester2, target); + + service.hide(requester1, target, DURATION); + service.changes.pipe(bufferCount(2), first()).subscribe(([hide1, hide2]) => { + expect(hide1).eql({ by: requester2.accountId, who: target.accountId }); + expect(hide2).eql({ by: requester1.accountId, who: target.accountId }); + done(); + }); + + service.merged(requester2.accountId, requester1.accountId); + }); + + it('triggers change events (2)', done => { + const requester = mockClient(); + const target1 = mockClient(); + const target2 = mockClient(); + addClients(requester, target2); + + service.hide(requester, target1, DURATION); + service.changes.pipe(bufferCount(2), first()).subscribe(([hide1, hide2]) => { + expect(hide1).eql({ by: requester.accountId, who: target2.accountId }); + expect(hide2).eql({ by: requester.accountId, who: target1.accountId }); + done(); + }); + + service.merged(target2.accountId, target1.accountId); + }); + }); }); diff --git a/src/ts/tests/server/services/notification.spec.ts b/src/ts/tests/server/services/notification.spec.ts index f4629cc..52ae530 100644 --- a/src/ts/tests/server/services/notification.spec.ts +++ b/src/ts/tests/server/services/notification.spec.ts @@ -7,201 +7,201 @@ import { NotificationService } from '../../../server/services/notification'; import { times } from '../../../common/utils'; describe('NotificationService', () => { - let notificationService: NotificationService; - let client: IClient; + let notificationService: NotificationService; + let client: IClient; - function createClient(): IClient { - return { - notifications: [], - addNotification() { }, - removeNotification() { }, - } as any; - } + function createClient(): IClient { + return { + notifications: [], + addNotification() { }, + removeNotification() { }, + } as any; + } - beforeEach(() => { - notificationService = new NotificationService(); - client = createClient(); - }); + beforeEach(() => { + notificationService = new NotificationService(); + client = createClient(); + }); - after(() => { - notificationService = undefined as any; - client = undefined as any; - }); + after(() => { + notificationService = undefined as any; + client = undefined as any; + }); - describe('addNotification()', () => { - it('adds notification to client', () => { - const notification = { id: 0, name: 'name', message: 'test' }; + describe('addNotification()', () => { + it('adds notification to client', () => { + const notification = { id: 0, name: 'name', message: 'test' }; - notificationService.addNotification(client, notification); + notificationService.addNotification(client, notification); - expect(client.notifications).contain(notification); - }); + expect(client.notifications).contain(notification); + }); - it('returns new notification ID', () => { - const notification = { id: 0, name: 'name', message: 'test' }; + it('returns new notification ID', () => { + const notification = { id: 0, name: 'name', message: 'test' }; - expect(notificationService.addNotification(client, notification)).equal(1); - }); + expect(notificationService.addNotification(client, notification)).equal(1); + }); - it('assigns ID to notification', () => { - const notification = { id: 0, name: 'name', message: 'test1' }; - client.notifications.push({ id: 1, name: 'name', message: 'test2' }); + it('assigns ID to notification', () => { + const notification = { id: 0, name: 'name', message: 'test1' }; + client.notifications.push({ id: 1, name: 'name', message: 'test2' }); - notificationService.addNotification(client, notification); + notificationService.addNotification(client, notification); - expect(notification.id).not.equal(0); - }); + expect(notification.id).not.equal(0); + }); - it('sends addNotification', () => { - const notification = { id: 0, name: 'name', message: 'test', note: 'note', flags: 123 }; - const addNotification = stub(client, 'addNotification'); + it('sends addNotification', () => { + const notification = { id: 0, name: 'name', message: 'test', note: 'note', flags: 123 }; + const addNotification = stub(client, 'addNotification'); - notificationService.addNotification(client, notification); + notificationService.addNotification(client, notification); - assert.calledWith(addNotification, 1, 0, 'name', 'test', 'note', 123); - }); + assert.calledWith(addNotification, 1, 0, 'name', 'test', 'note', 123); + }); - it('does not add notification to client if limit is reached', () => { - times(10, () => notificationService.addNotification(client, { id: 0, name: 'name', message: uniqueId('test') })); - const addNotification = stub(client, 'addNotification'); + it('does not add notification to client if limit is reached', () => { + times(10, () => notificationService.addNotification(client, { id: 0, name: 'name', message: uniqueId('test') })); + const addNotification = stub(client, 'addNotification'); - expect(notificationService.addNotification(client, { id: 0, name: 'name', message: uniqueId('test') })).equal(0); + expect(notificationService.addNotification(client, { id: 0, name: 'name', message: uniqueId('test') })).equal(0); - assert.notCalled(addNotification); - }); + assert.notCalled(addNotification); + }); - it('does not add notification to client if identical notification already exists', () => { - const addNotification = stub(client, 'addNotification'); + it('does not add notification to client if identical notification already exists', () => { + const addNotification = stub(client, 'addNotification'); - expect(notificationService.addNotification(client, { id: 0, name: 'name', message: 'foo' })).not.equal(0); - expect(notificationService.addNotification(client, { id: 0, name: 'name', message: 'foo' })).equal(0); + expect(notificationService.addNotification(client, { id: 0, name: 'name', message: 'foo' })).not.equal(0); + expect(notificationService.addNotification(client, { id: 0, name: 'name', message: 'foo' })).equal(0); - expect(client.notifications.length).equal(1); - assert.calledOnce(addNotification); - }); - }); + expect(client.notifications.length).equal(1); + assert.calledOnce(addNotification); + }); + }); - describe('removeNotification()', () => { - it('removes notification from client', () => { - const notification = { id: 1, name: 'name', message: 'test' }; - client.notifications.push(notification); + describe('removeNotification()', () => { + it('removes notification from client', () => { + const notification = { id: 1, name: 'name', message: 'test' }; + client.notifications.push(notification); - notificationService.removeNotification(client, 1); + notificationService.removeNotification(client, 1); - expect(client.notifications).not.contain(notification); - }); + expect(client.notifications).not.contain(notification); + }); - it('does nothing if notification does not exist', () => { - const removeNotification = stub(client, 'removeNotification'); + it('does nothing if notification does not exist', () => { + const removeNotification = stub(client, 'removeNotification'); - notificationService.removeNotification(client, 1); + notificationService.removeNotification(client, 1); - assert.notCalled(removeNotification); - }); + assert.notCalled(removeNotification); + }); - it('sends removeNotification', () => { - client.notifications.push({ id: 1, name: 'name', message: 'test' }); - const removeNotification = stub(client, 'removeNotification'); + it('sends removeNotification', () => { + client.notifications.push({ id: 1, name: 'name', message: 'test' }); + const removeNotification = stub(client, 'removeNotification'); - notificationService.removeNotification(client, 1); + notificationService.removeNotification(client, 1); - assert.calledWith(removeNotification, 1); - }); + assert.calledWith(removeNotification, 1); + }); - it('returns true if notification is removed', () => { - client.notifications.push({ id: 1, name: 'name', message: 'test' }); + it('returns true if notification is removed', () => { + client.notifications.push({ id: 1, name: 'name', message: 'test' }); - expect(notificationService.removeNotification(client, 1)).true; - }); + expect(notificationService.removeNotification(client, 1)).true; + }); - it('returns false if notification does not exist', () => { - expect(notificationService.removeNotification(client, 1)).false; - }); - }); + it('returns false if notification does not exist', () => { + expect(notificationService.removeNotification(client, 1)).false; + }); + }); - describe('acceptNotification()', () => { - it('calls accept callback', () => { - const accept = spy(); - notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', accept }); + describe('acceptNotification()', () => { + it('calls accept callback', () => { + const accept = spy(); + notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', accept }); - notificationService.acceptNotification(client, 1); + notificationService.acceptNotification(client, 1); - assert.calledOnce(accept); - }); + assert.calledOnce(accept); + }); - it('removes notification', () => { - notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', accept() { } }); - const removeNotification = stub(notificationService, 'removeNotification'); + it('removes notification', () => { + notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', accept() { } }); + const removeNotification = stub(notificationService, 'removeNotification'); - notificationService.acceptNotification(client, 1); + notificationService.acceptNotification(client, 1); - assert.calledWith(removeNotification, client, 1); - }); + assert.calledWith(removeNotification, client, 1); + }); - it('does nothing if notification does not exist', () => { - const removeNotification = stub(client, 'removeNotification'); + it('does nothing if notification does not exist', () => { + const removeNotification = stub(client, 'removeNotification'); - notificationService.acceptNotification(client, 1); + notificationService.acceptNotification(client, 1); - assert.notCalled(removeNotification); - }); + assert.notCalled(removeNotification); + }); - it('works if notification does not have accept callback', () => { - const notification = { id: 0, name: 'name', message: 'test' }; - notificationService.addNotification(client, notification); + it('works if notification does not have accept callback', () => { + const notification = { id: 0, name: 'name', message: 'test' }; + notificationService.addNotification(client, notification); - notificationService.acceptNotification(client, 1); + notificationService.acceptNotification(client, 1); - expect(client.notifications).not.include(notification); - }); - }); + expect(client.notifications).not.include(notification); + }); + }); - describe('rejectNotification()', () => { - it('calls reject callback', () => { - const reject = spy(); - notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', reject }); + describe('rejectNotification()', () => { + it('calls reject callback', () => { + const reject = spy(); + notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', reject }); - notificationService.rejectNotification(client, 1); + notificationService.rejectNotification(client, 1); - assert.calledOnce(reject); - }); + assert.calledOnce(reject); + }); - it('removes notification', () => { - notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', accept() { } }); - const removeNotification = stub(notificationService, 'removeNotification'); + it('removes notification', () => { + notificationService.addNotification(client, { id: 0, name: 'name', message: 'test', accept() { } }); + const removeNotification = stub(notificationService, 'removeNotification'); - notificationService.rejectNotification(client, 1); + notificationService.rejectNotification(client, 1); - assert.calledWith(removeNotification, client, 1); - }); + assert.calledWith(removeNotification, client, 1); + }); - it('does nothing if notification does not exist', () => { - const removeNotification = stub(client, 'removeNotification'); + it('does nothing if notification does not exist', () => { + const removeNotification = stub(client, 'removeNotification'); - notificationService.rejectNotification(client, 1); + notificationService.rejectNotification(client, 1); - assert.notCalled(removeNotification); - }); + assert.notCalled(removeNotification); + }); - it('works if notification does not have reject callback', () => { - const notification = { id: 0, name: 'name', message: 'test' }; - notificationService.addNotification(client, notification); + it('works if notification does not have reject callback', () => { + const notification = { id: 0, name: 'name', message: 'test' }; + notificationService.addNotification(client, notification); - notificationService.rejectNotification(client, 1); + notificationService.rejectNotification(client, 1); - expect(client.notifications).not.include(notification); - }); - }); + expect(client.notifications).not.include(notification); + }); + }); - describe('rejectAll()', () => { - it('rejects all notifications', () => { - client.notifications = [{ id: 1 }, { id: 2 }] as any; - const rejectNotification = stub(notificationService, 'rejectNotification'); + describe('rejectAll()', () => { + it('rejects all notifications', () => { + client.notifications = [{ id: 1 }, { id: 2 }] as any; + const rejectNotification = stub(notificationService, 'rejectNotification'); - notificationService.rejectAll(client); + notificationService.rejectAll(client); - assert.calledWith(rejectNotification, client, 1); - assert.calledWith(rejectNotification, client, 2); - }); - }); + assert.calledWith(rejectNotification, client, 1); + assert.calledWith(rejectNotification, client, 2); + }); + }); }); diff --git a/src/ts/tests/server/services/party.spec.ts b/src/ts/tests/server/services/party.spec.ts index 12cd457..e2c267d 100644 --- a/src/ts/tests/server/services/party.spec.ts +++ b/src/ts/tests/server/services/party.spec.ts @@ -8,866 +8,866 @@ import { PARTY_LIMIT, HOUR } from '../../../common/constants'; import { NotificationService } from '../../../server/services/notification'; import { IClient, ServerParty } from '../../../server/serverInterfaces'; import { - PartyService, LEADER_TIMEOUT, INVITE_LIMIT, INVITE_REJECTED_LIMIT, INVITE_REJECTED_TIMEOUT + PartyService, LEADER_TIMEOUT, INVITE_LIMIT, INVITE_REJECTED_LIMIT, INVITE_REJECTED_TIMEOUT } from '../../../server/services/party'; import { mockClient } from '../../mocks'; import { addIgnore } from '../../../server/playerUtils'; import { times } from '../../../common/utils'; describe('PartyService', () => { - let notificationService: NotificationService; - let partyService: PartyService; - let leader: IClient; - let client: IClient; - let addNotification: SinonStub; - let leaderUpdateParty: SinonStub; - let clientUpdateParty: SinonStub; - let reportInviteLimit: SinonStub; - let clock: SinonFakeTimers; - - function createClient(id: number, characterId?: string, accountId?: string): IClient { - return mockClient({ - accountId, - characterId, - pony: { id }, - character: { id: characterId }, - account: { id: accountId }, - }); - } - - function createParty(leader: IClient, clients: IClient[] = [], pending: IClient[] = []): ServerParty { - const party: ServerParty = { - id: 'some_id', - leader, - clients: [leader, ...clients], - pending: pending.map(client => ({ client, notificationId: 5 })), - }; - party.clients.forEach(c => c.party = party); - partyService.parties.push(party); - return party; - } - - beforeEach(() => { - clock = useFakeTimers(Date.now()); - leader = createClient(1, 'foo', 'foofoo'); - client = createClient(2, 'bar', 'barbar'); - leaderUpdateParty = stub(leader, 'updateParty'); - clientUpdateParty = stub(client, 'updateParty'); - reportInviteLimit = stub(); - notificationService = new NotificationService(); - addNotification = stub(notificationService, 'addNotification').returns(1); - partyService = new PartyService(notificationService, reportInviteLimit); - }); - - afterEach(() => { - clock.restore(); - partyService.dispose(); - }); - - describe('clientConnected()', () => { - it('does nothing if there is no party for client', () => { - partyService.clientConnected(client); - - assert.notCalled(clientUpdateParty); - }); - - describe('rejoining', () => { - beforeEach(() => { - partyService.invite(leader, client); - addNotification.firstCall.args[1].accept(); - }); - - it('replaces matching client', () => { - const newClient = createClient(5, 'bar', 'barbar'); - - partyService.clientConnected(newClient); - - expect(leader.party!.clients[1]).equal(newClient); - }); - - it('replaces matching client for the same account', () => { - const newClient = createClient(5, 'abc', 'barbar'); - - partyService.clientConnected(newClient); - - expect(leader.party!.clients[1]).equal(newClient); - }); - - it('updates leader if leader reconnected', () => { - const newLeader = createClient(5, 'foo', 'foofoo'); - - partyService.clientConnected(newLeader); - - expect(client.party!.leader).equal(newLeader); - }); - - it('sends party update', () => { - const newClient = createClient(5, 'bar', 'barbar'); - const updateParty = stub(newClient, 'updateParty'); - leaderUpdateParty.reset(); - - partyService.clientConnected(newClient); - - assert.calledOnce(updateParty); - assert.calledOnce(leaderUpdateParty); - }); - - it('sets party for new client', () => { - const newClient = createClient(5, 'bar', 'barbar'); - - partyService.clientConnected(newClient); + let notificationService: NotificationService; + let partyService: PartyService; + let leader: IClient; + let client: IClient; + let addNotification: SinonStub; + let leaderUpdateParty: SinonStub; + let clientUpdateParty: SinonStub; + let reportInviteLimit: SinonStub; + let clock: SinonFakeTimers; + + function createClient(id: number, characterId?: string, accountId?: string): IClient { + return mockClient({ + accountId, + characterId, + pony: { id }, + character: { id: characterId }, + account: { id: accountId }, + }); + } + + function createParty(leader: IClient, clients: IClient[] = [], pending: IClient[] = []): ServerParty { + const party: ServerParty = { + id: 'some_id', + leader, + clients: [leader, ...clients], + pending: pending.map(client => ({ client, notificationId: 5 })), + }; + party.clients.forEach(c => c.party = party); + partyService.parties.push(party); + return party; + } + + beforeEach(() => { + clock = useFakeTimers(Date.now()); + leader = createClient(1, 'foo', 'foofoo'); + client = createClient(2, 'bar', 'barbar'); + leaderUpdateParty = stub(leader, 'updateParty'); + clientUpdateParty = stub(client, 'updateParty'); + reportInviteLimit = stub(); + notificationService = new NotificationService(); + addNotification = stub(notificationService, 'addNotification').returns(1); + partyService = new PartyService(notificationService, reportInviteLimit); + }); + + afterEach(() => { + clock.restore(); + partyService.dispose(); + }); + + describe('clientConnected()', () => { + it('does nothing if there is no party for client', () => { + partyService.clientConnected(client); + + assert.notCalled(clientUpdateParty); + }); + + describe('rejoining', () => { + beforeEach(() => { + partyService.invite(leader, client); + addNotification.firstCall.args[1].accept(); + }); + + it('replaces matching client', () => { + const newClient = createClient(5, 'bar', 'barbar'); + + partyService.clientConnected(newClient); + + expect(leader.party!.clients[1]).equal(newClient); + }); + + it('replaces matching client for the same account', () => { + const newClient = createClient(5, 'abc', 'barbar'); + + partyService.clientConnected(newClient); + + expect(leader.party!.clients[1]).equal(newClient); + }); + + it('updates leader if leader reconnected', () => { + const newLeader = createClient(5, 'foo', 'foofoo'); + + partyService.clientConnected(newLeader); + + expect(client.party!.leader).equal(newLeader); + }); + + it('sends party update', () => { + const newClient = createClient(5, 'bar', 'barbar'); + const updateParty = stub(newClient, 'updateParty'); + leaderUpdateParty.reset(); + + partyService.clientConnected(newClient); + + assert.calledOnce(updateParty); + assert.calledOnce(leaderUpdateParty); + }); + + it('sets party for new client', () => { + const newClient = createClient(5, 'bar', 'barbar'); + + partyService.clientConnected(newClient); - expect(newClient.party).equal(leader.party); - }); + expect(newClient.party).equal(leader.party); + }); - it('unsets party for old client', () => { - const newClient = createClient(5, 'bar', 'barbar'); + it('unsets party for old client', () => { + const newClient = createClient(5, 'bar', 'barbar'); - partyService.clientConnected(newClient); + partyService.clientConnected(newClient); - expect(client.party).undefined; - }); + expect(client.party).undefined; + }); - it('sets offlineAt to current time', () => { - const newClient = createClient(5, 'bar', 'barbar'); + it('sets offlineAt to current time', () => { + const newClient = createClient(5, 'bar', 'barbar'); - clock.setSystemTime(100); - partyService.clientConnected(newClient); + clock.setSystemTime(100); + partyService.clientConnected(newClient); - expect(client.offlineAt!.getTime()).equal(new Date().getTime()); - }); - }); + expect(client.offlineAt!.getTime()).equal(new Date().getTime()); + }); + }); - it('cancels new leader promotion', () => { - const party = createParty(leader, [client, createClient(9, 'x', 'xx')]); - const promoteLeader = stub(partyService, 'promoteLeader'); - const newLeader = createClient(10, 'foo', 'foofoo'); + it('cancels new leader promotion', () => { + const party = createParty(leader, [client, createClient(9, 'x', 'xx')]); + const promoteLeader = stub(partyService, 'promoteLeader'); + const newLeader = createClient(10, 'foo', 'foofoo'); - leader.offline = true; - partyService.clientDisconnected(leader); - partyService.clientConnected(newLeader); - clock.tick(LEADER_TIMEOUT + 100); + leader.offline = true; + partyService.clientDisconnected(leader); + partyService.clientConnected(newLeader); + clock.tick(LEADER_TIMEOUT + 100); - expect(party.leader).equal(newLeader); - assert.notCalled(promoteLeader); - }); - }); + expect(party.leader).equal(newLeader); + assert.notCalled(promoteLeader); + }); + }); - describe('clientDisconnected()', () => { - it('sends party update', () => { - createParty(leader, [client]); + describe('clientDisconnected()', () => { + it('sends party update', () => { + createParty(leader, [client]); - client.offline = true; - partyService.clientDisconnected(client); + client.offline = true; + partyService.clientDisconnected(client); - assert.calledWith(leaderUpdateParty, [ - [1, PartyFlags.Leader], - [2, PartyFlags.Offline], - ]); - }); + assert.calledWith(leaderUpdateParty, [ + [1, PartyFlags.Leader], + [2, PartyFlags.Offline], + ]); + }); - it('does not send party update to disconnected client', () => { - createParty(leader, [client]); + it('does not send party update to disconnected client', () => { + createParty(leader, [client]); - client.offline = true; - partyService.clientDisconnected(client); + client.offline = true; + partyService.clientDisconnected(client); - assert.notCalled(clientUpdateParty); - }); + assert.notCalled(clientUpdateParty); + }); - it('does nothing if not member of a party', () => { - createParty(leader, [createClient(9)]); + it('does nothing if not member of a party', () => { + createParty(leader, [createClient(9)]); - client.offline = true; - partyService.clientDisconnected(client); + client.offline = true; + partyService.clientDisconnected(client); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(leaderUpdateParty); + }); - it('promotes new leader after timeout', () => { - createParty(leader, [client, createClient(9)]); - const promoteLeader = stub(partyService, 'promoteLeader'); + it('promotes new leader after timeout', () => { + createParty(leader, [client, createClient(9)]); + const promoteLeader = stub(partyService, 'promoteLeader'); - leader.offline = true; - partyService.clientDisconnected(leader); - clock.tick(LEADER_TIMEOUT + 100); + leader.offline = true; + partyService.clientDisconnected(leader); + clock.tick(LEADER_TIMEOUT + 100); - assert.calledWith(promoteLeader, leader, client); - }); + assert.calledWith(promoteLeader, leader, client); + }); - it('does not promote offline player as the new leader', () => { - const anotherClient = createClient(9); - createParty(leader, [client, anotherClient]); - const promoteLeader = stub(partyService, 'promoteLeader'); + it('does not promote offline player as the new leader', () => { + const anotherClient = createClient(9); + createParty(leader, [client, anotherClient]); + const promoteLeader = stub(partyService, 'promoteLeader'); - leader.offline = true; - client.offline = true; - partyService.clientDisconnected(leader); - clock.tick(LEADER_TIMEOUT + 100); + leader.offline = true; + client.offline = true; + partyService.clientDisconnected(leader); + clock.tick(LEADER_TIMEOUT + 100); - assert.calledWith(promoteLeader, leader, anotherClient); - }); + assert.calledWith(promoteLeader, leader, anotherClient); + }); - it('removes client if pending', () => { - const party = createParty(leader, [createClient(9)], [client]); + it('removes client if pending', () => { + const party = createParty(leader, [createClient(9)], [client]); - client.offline = true; - partyService.clientDisconnected(client); + client.offline = true; + partyService.clientDisconnected(client); - expect(party.pending).empty; - }); + expect(party.pending).empty; + }); - it('sends party update (pending)', () => { - createParty(leader, [createClient(9)], [client]); + it('sends party update (pending)', () => { + createParty(leader, [createClient(9)], [client]); - client.offline = true; - partyService.clientDisconnected(client); + client.offline = true; + partyService.clientDisconnected(client); - assert.calledOnce(leaderUpdateParty); - }); + assert.calledOnce(leaderUpdateParty); + }); - it('disbands party if cant find new leader after timeout', () => { - const party = createParty(leader, [], [client, createClient(9)]); + it('disbands party if cant find new leader after timeout', () => { + const party = createParty(leader, [], [client, createClient(9)]); - leader.offline = true; - partyService.clientDisconnected(leader); - clock.tick(LEADER_TIMEOUT + 100); + leader.offline = true; + partyService.clientDisconnected(leader); + clock.tick(LEADER_TIMEOUT + 100); - expect(partyService.parties).not.include(party); - }); - }); + expect(partyService.parties).not.include(party); + }); + }); - describe('invite()', () => { - it('creates new party on the leader if none exists', () => { - partyService.invite(leader, client); + describe('invite()', () => { + it('creates new party on the leader if none exists', () => { + partyService.invite(leader, client); - expect(leader.party).not.empty; - expect(leader.party!.leader).equal(leader); - expect(leader.party!.clients).eql([leader]); - expect(leader.party!.pending).eql([{ client, notificationId: 1 }]); - expect(partyService.parties).contain(leader.party!); - }); + expect(leader.party).not.empty; + expect(leader.party!.leader).equal(leader); + expect(leader.party!.clients).eql([leader]); + expect(leader.party!.pending).eql([{ client, notificationId: 1 }]); + expect(partyService.parties).contain(leader.party!); + }); - it('adds client to pending members', () => { - partyService.invite(leader, client); + it('adds client to pending members', () => { + partyService.invite(leader, client); - expect(leader.party!.pending[0].client).equal(client); - }); + expect(leader.party!.pending[0].client).equal(client); + }); - it('logs party invitation', () => { - const systemLog = stub(leader.reporter, 'systemLog'); + it('logs party invitation', () => { + const systemLog = stub(leader.reporter, 'systemLog'); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.calledWith(systemLog, 'Invite to party [barbar]'); - }); + assert.calledWith(systemLog, 'Invite to party [barbar]'); + }); - it('sends invite notice the the client', () => { - leader.pony.name = 'foo'; + it('sends invite notice the the client', () => { + leader.pony.name = 'foo'; - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.calledWith(addNotification, client, match({ - name: 'foo', - entityId: leader.pony.id, - message: '
Party invite
#NAME# invited you to a party', - flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore, - })); - }); + assert.calledWith(addNotification, client, match({ + name: 'foo', + entityId: leader.pony.id, + message: '
Party invite
#NAME# invited you to a party', + flags: NotificationFlags.Accept | NotificationFlags.Reject | NotificationFlags.Ignore, + })); + }); - it('sends invite notice the the client (existing party)', () => { - leader.party = createParty(leader, [createClient(3)]); + it('sends invite notice the the client (existing party)', () => { + leader.party = createParty(leader, [createClient(3)]); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.calledOnce(addNotification); - }); + assert.calledOnce(addNotification); + }); - it('sends party update to the leader', () => { - partyService.invite(leader, client); + it('sends party update to the leader', () => { + partyService.invite(leader, client); - assert.calledWithMatch(leaderUpdateParty, [ - [1, PartyFlags.Leader], - [2, PartyFlags.Pending], - ]); - }); + assert.calledWithMatch(leaderUpdateParty, [ + [1, PartyFlags.Leader], + [2, PartyFlags.Pending], + ]); + }); - it('does nothing if already is in party and not a leader', () => { - const someone = createClient(3); - someone.party = createParty(leader, [someone]); + it('does nothing if already is in party and not a leader', () => { + const someone = createClient(3); + someone.party = createParty(leader, [someone]); - partyService.invite(someone, client); + partyService.invite(someone, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if client is already in a party', () => { - client.party = createParty(client); + it('does nothing if client is already in a party', () => { + client.party = createParty(client); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if client is already pending', () => { - leader.party = createParty(leader, [], [client]); + it('does nothing if client is already pending', () => { + leader.party = createParty(leader, [], [client]); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if party is already at member limit', () => { - leader.party = createParty(leader, range(0, PARTY_LIMIT - 1).map(i => createClient(i + 10))); + it('does nothing if party is already at member limit', () => { + leader.party = createParty(leader, range(0, PARTY_LIMIT - 1).map(i => createClient(i + 10))); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if party is already at member limit (pending)', () => { - leader.party = createParty(leader, [], range(0, PARTY_LIMIT - 1).map(i => createClient(i + 10))); + it('does nothing if party is already at member limit (pending)', () => { + leader.party = createParty(leader, [], range(0, PARTY_LIMIT - 1).map(i => createClient(i + 10))); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if leader is ignored', () => { - leader.party = createParty(leader, [createClient(9)], []); - addIgnore(leader, client.accountId); + it('does nothing if leader is ignored', () => { + leader.party = createParty(leader, [createClient(9)], []); + addIgnore(leader, client.accountId); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if leader is timedout', () => { - leader.account.mute = Date.now() + HOUR; + it('does nothing if leader is timedout', () => { + leader.account.mute = Date.now() + HOUR; - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if leader is muted', () => { - leader.account.mute = -1; + it('does nothing if leader is muted', () => { + leader.account.mute = -1; - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if leader is shadowed', () => { - leader.shadowed = true; + it('does nothing if leader is shadowed', () => { + leader.shadowed = true; - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if client is offline', () => { - leader.party = createParty(leader, [createClient(9)], []); - client.offline = true; + it('does nothing if client is offline', () => { + leader.party = createParty(leader, [createClient(9)], []); + client.offline = true; - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if inviting self (client)', () => { - partyService.invite(leader, leader); + it('does nothing if inviting self (client)', () => { + partyService.invite(leader, leader); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if inviting self (account)', () => { - client.accountId = leader.accountId; + it('does nothing if inviting self (account)', () => { + client.accountId = leader.accountId; - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - function setupRejectLimit() { - range(0, INVITE_REJECTED_LIMIT).forEach(i => { - const c = createClient(10 + i); - partyService.invite(leader, c); - addNotification.firstCall.args[1].reject(); - addNotification.reset(); - addNotification.returns(1); - }); - } + function setupRejectLimit() { + range(0, INVITE_REJECTED_LIMIT).forEach(i => { + const c = createClient(10 + i); + partyService.invite(leader, c); + addNotification.firstCall.args[1].reject(); + addNotification.reset(); + addNotification.returns(1); + }); + } - it('does nothing if reached invite limit', () => { - setupRejectLimit(); - addNotification.reset(); - addNotification.returns(1); - leaderUpdateParty.reset(); + it('does nothing if reached invite limit', () => { + setupRejectLimit(); + addNotification.reset(); + addNotification.returns(1); + leaderUpdateParty.reset(); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('reports if reached invite limit', () => { - setupRejectLimit(); + it('reports if reached invite limit', () => { + setupRejectLimit(); - assert.calledWith(reportInviteLimit, leader); - }); + assert.calledWith(reportInviteLimit, leader); + }); - it('resets invite limit periodically', () => { - setupRejectLimit(); - addNotification.reset(); - addNotification.returns(1); - leaderUpdateParty.reset(); - clock.tick(INVITE_REJECTED_TIMEOUT * 2); + it('resets invite limit periodically', () => { + setupRejectLimit(); + addNotification.reset(); + addNotification.returns(1); + leaderUpdateParty.reset(); + clock.tick(INVITE_REJECTED_TIMEOUT * 2); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.calledOnce(addNotification); - assert.calledOnce(leaderUpdateParty); - }); + assert.calledOnce(addNotification); + assert.calledOnce(leaderUpdateParty); + }); - it('does nothing if leader has party invites blocked', () => { - leader.party = createParty(leader, [createClient(9)], []); - leader.account.flags = AccountFlags.BlockPartyInvites; + it('does nothing if leader has party invites blocked', () => { + leader.party = createParty(leader, [createClient(9)], []); + leader.account.flags = AccountFlags.BlockPartyInvites; - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if user ignores all party requests', () => { - leader.party = createParty(leader, [createClient(9)], []); - client.accountSettings = { ignorePartyInvites: true }; + it('does nothing if user ignores all party requests', () => { + leader.party = createParty(leader, [createClient(9)], []); + client.accountSettings = { ignorePartyInvites: true }; - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(addNotification); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if user already reached party request limit', () => { - range(0, INVITE_LIMIT).forEach(i => partyService.invite(createClient(10 + i), client)); - addNotification.reset(); + it('does nothing if user already reached party request limit', () => { + range(0, INVITE_LIMIT).forEach(i => partyService.invite(createClient(10 + i), client)); + addNotification.reset(); - partyService.invite(leader, client); + partyService.invite(leader, client); - assert.notCalled(addNotification); - }); + assert.notCalled(addNotification); + }); - it('does nothing if add notification returns 0', () => { - addNotification.returns(0); + it('does nothing if add notification returns 0', () => { + addNotification.returns(0); - partyService.invite(leader, client); + partyService.invite(leader, client); - expect(leader.party).undefined; - assert.notCalled(leaderUpdateParty); - assert.notCalled(clientUpdateParty); - }); + expect(leader.party).undefined; + assert.notCalled(leaderUpdateParty); + assert.notCalled(clientUpdateParty); + }); - it('does nothing for existing party if add notification returns 0', () => { - const party = createParty(leader, [createClient(9)], []); - leader.party = party; - addNotification.returns(0); + it('does nothing for existing party if add notification returns 0', () => { + const party = createParty(leader, [createClient(9)], []); + leader.party = party; + addNotification.returns(0); - partyService.invite(leader, client); + partyService.invite(leader, client); - expect(leader.party).equal(party); - assert.notCalled(clientUpdateParty); - }); + expect(leader.party).equal(party); + assert.notCalled(clientUpdateParty); + }); - describe('notification.accept()', () => { - function accept() { - addNotification.firstCall.args[1].accept(); - } + describe('notification.accept()', () => { + function accept() { + addNotification.firstCall.args[1].accept(); + } - it('removes client from pending', () => { - partyService.invite(leader, client); + it('removes client from pending', () => { + partyService.invite(leader, client); - accept(); + accept(); - expect(leader.party!.pending).empty; - }); + expect(leader.party!.pending).empty; + }); - it('removes notification', () => { - const removeNotification = stub(notificationService, 'removeNotification'); - partyService.invite(leader, client); + it('removes notification', () => { + const removeNotification = stub(notificationService, 'removeNotification'); + partyService.invite(leader, client); - accept(); + accept(); - assert.calledWith(removeNotification, client, 1); - }); + assert.calledWith(removeNotification, client, 1); + }); - it('adds client to clients', () => { - partyService.invite(leader, client); + it('adds client to clients', () => { + partyService.invite(leader, client); - accept(); + accept(); - expect(leader.party!.clients).contain(client); - }); + expect(leader.party!.clients).contain(client); + }); - it('sets party for client', () => { - partyService.invite(leader, client); + it('sets party for client', () => { + partyService.invite(leader, client); - accept(); + accept(); - expect(client.party).equal(leader.party); - }); + expect(client.party).equal(leader.party); + }); - it('sends party update', () => { - partyService.invite(leader, client); - leaderUpdateParty.reset(); - clientUpdateParty.reset(); + it('sends party update', () => { + partyService.invite(leader, client); + leaderUpdateParty.reset(); + clientUpdateParty.reset(); - accept(); + accept(); - assert.calledOnce(leaderUpdateParty); - assert.calledOnce(clientUpdateParty); - }); + assert.calledOnce(leaderUpdateParty); + assert.calledOnce(clientUpdateParty); + }); - it('logs accept', () => { - const systemLog = stub(leader.reporter, 'systemLog'); - partyService.invite(leader, client); + it('logs accept', () => { + const systemLog = stub(leader.reporter, 'systemLog'); + partyService.invite(leader, client); - accept(); + accept(); - assert.calledWith(systemLog, 'Invite accepted by [barbar]'); - }); + assert.calledWith(systemLog, 'Invite accepted by [barbar]'); + }); - it('rejects all other party invites', () => { - const leader2 = createClient(8); - const leader3 = createClient(9); - partyService.invite(leader, client); - partyService.invite(leader2, client); - partyService.invite(leader3, client); + it('rejects all other party invites', () => { + const leader2 = createClient(8); + const leader3 = createClient(9); + partyService.invite(leader, client); + partyService.invite(leader2, client); + partyService.invite(leader3, client); - accept(); + accept(); - expect(leader2.party).undefined; - expect(leader3.party).undefined; - }); + expect(leader2.party).undefined; + expect(leader3.party).undefined; + }); - it('does nothing if not in pending', () => { - createParty(leader, [createClient(9)]); - partyService.invite(leader, client); - partyService.remove(leader, client); - leaderUpdateParty.reset(); + it('does nothing if not in pending', () => { + createParty(leader, [createClient(9)]); + partyService.invite(leader, client); + partyService.remove(leader, client); + leaderUpdateParty.reset(); - accept(); + accept(); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if party does not exist', () => { - partyService.invite(leader, client); - partyService.remove(leader, client); - leaderUpdateParty.reset(); + it('does nothing if party does not exist', () => { + partyService.invite(leader, client); + partyService.remove(leader, client); + leaderUpdateParty.reset(); - accept(); + accept(); - assert.notCalled(leaderUpdateParty); - }); - }); + assert.notCalled(leaderUpdateParty); + }); + }); - describe('notification.reject()', () => { - function reject() { - addNotification.firstCall.args[1].reject(); - } + describe('notification.reject()', () => { + function reject() { + addNotification.firstCall.args[1].reject(); + } - it('removes client from pending', () => { - createParty(leader, [createClient(9)]); - partyService.invite(leader, client); + it('removes client from pending', () => { + createParty(leader, [createClient(9)]); + partyService.invite(leader, client); - reject(); + reject(); - expect(leader.party!.pending).empty; - }); + expect(leader.party!.pending).empty; + }); - it('removes notification', () => { - const removeNotification = stub(notificationService, 'removeNotification'); - partyService.invite(leader, client); + it('removes notification', () => { + const removeNotification = stub(notificationService, 'removeNotification'); + partyService.invite(leader, client); - reject(); + reject(); - assert.calledWith(removeNotification, client, 1); - }); + assert.calledWith(removeNotification, client, 1); + }); - it('sends party update', () => { - createParty(leader, [createClient(9)]); - partyService.invite(leader, client); - leaderUpdateParty.reset(); + it('sends party update', () => { + createParty(leader, [createClient(9)]); + partyService.invite(leader, client); + leaderUpdateParty.reset(); - reject(); + reject(); - assert.calledOnce(leaderUpdateParty); - }); + assert.calledOnce(leaderUpdateParty); + }); - it('logs rejection', () => { - const systemLog = stub(leader.reporter, 'systemLog'); - partyService.invite(leader, client); + it('logs rejection', () => { + const systemLog = stub(leader.reporter, 'systemLog'); + partyService.invite(leader, client); - reject(); + reject(); - assert.calledWith(systemLog, 'Invite rejected by [barbar]'); - }); + assert.calledWith(systemLog, 'Invite rejected by [barbar]'); + }); - it('disbands party if less than 2 users', () => { - partyService.invite(leader, client); - leaderUpdateParty.reset(); + it('disbands party if less than 2 users', () => { + partyService.invite(leader, client); + leaderUpdateParty.reset(); - reject(); + reject(); - expect(leader.party).undefined; - }); + expect(leader.party).undefined; + }); - it('does nothing if client is not pending anymore', () => { - createParty(leader, [createClient(9)]); - partyService.invite(leader, client); - partyService.remove(leader, client); - leaderUpdateParty.reset(); + it('does nothing if client is not pending anymore', () => { + createParty(leader, [createClient(9)]); + partyService.invite(leader, client); + partyService.remove(leader, client); + leaderUpdateParty.reset(); - reject(); + reject(); - assert.notCalled(leaderUpdateParty); - }); - }); - }); + assert.notCalled(leaderUpdateParty); + }); + }); + }); - describe('remove()', () => { - it('removes client from the party', () => { - const party = createParty(leader, [client, createClient(3)]); - client.party = party; + describe('remove()', () => { + it('removes client from the party', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; - partyService.remove(leader, client); + partyService.remove(leader, client); - expect(party.clients).not.include(client); - }); + expect(party.clients).not.include(client); + }); - it('removes party from the client', () => { - const party = createParty(leader, [client, createClient(3)]); - client.party = party; + it('removes party from the client', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; - partyService.remove(leader, client); + partyService.remove(leader, client); - expect(client.party).undefined; - }); + expect(client.party).undefined; + }); - it('removes pending client from the party', () => { - const party = createParty(leader, [createClient(3)], [client]); - client.party = party; + it('removes pending client from the party', () => { + const party = createParty(leader, [createClient(3)], [client]); + client.party = party; - partyService.remove(leader, client); + partyService.remove(leader, client); - expect(party.pending).empty; - }); + expect(party.pending).empty; + }); - it('logs cancel if removed pending client', () => { - const systemLog = stub(leader.reporter, 'systemLog'); - const party = createParty(leader, [createClient(3)], [client]); - client.party = party; + it('logs cancel if removed pending client', () => { + const systemLog = stub(leader.reporter, 'systemLog'); + const party = createParty(leader, [createClient(3)], [client]); + client.party = party; - partyService.remove(leader, client); + partyService.remove(leader, client); - assert.calledWith(systemLog, 'Invite cancelled for [barbar]'); - }); + assert.calledWith(systemLog, 'Invite cancelled for [barbar]'); + }); - it('counts invite limit for cancels', () => { - const party = createParty(leader, [createClient(3)], []); + it('counts invite limit for cancels', () => { + const party = createParty(leader, [createClient(3)], []); - times(INVITE_REJECTED_LIMIT, () => { - client.party = party; - party.pending.push({ client, notificationId: 0 }); - partyService.remove(leader, client); - }); + times(INVITE_REJECTED_LIMIT, () => { + client.party = party; + party.pending.push({ client, notificationId: 0 }); + partyService.remove(leader, client); + }); - assert.calledWith(reportInviteLimit, leader); - }); + assert.calledWith(reportInviteLimit, leader); + }); - it('removes pending client notification', () => { - const removeNotification = stub(notificationService, 'removeNotification'); - const party = createParty(leader, [createClient(3)], [client]); - client.party = party; + it('removes pending client notification', () => { + const removeNotification = stub(notificationService, 'removeNotification'); + const party = createParty(leader, [createClient(3)], [client]); + client.party = party; - partyService.remove(leader, client); + partyService.remove(leader, client); - assert.calledWith(removeNotification, client, 5); - }); + assert.calledWith(removeNotification, client, 5); + }); - it('sends party update to all clients', () => { - const party = createParty(leader, [client, createClient(3)]); - client.party = party; + it('sends party update to all clients', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; - partyService.remove(leader, client); + partyService.remove(leader, client); - assert.calledOnce(leaderUpdateParty); - }); + assert.calledOnce(leaderUpdateParty); + }); - it('sends party update to all clients (pending)', () => { - const party = createParty(leader, [createClient(3)], [client]); - client.party = party; + it('sends party update to all clients (pending)', () => { + const party = createParty(leader, [createClient(3)], [client]); + client.party = party; - partyService.remove(leader, client); + partyService.remove(leader, client); - assert.calledOnce(leaderUpdateParty); - }); + assert.calledOnce(leaderUpdateParty); + }); - it('sends undefined party update to the client', () => { - const party = createParty(leader, [client, createClient(3)]); - client.party = party; + it('sends undefined party update to the client', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; - partyService.remove(leader, client); + partyService.remove(leader, client); - assert.calledWith(clientUpdateParty, undefined); - }); + assert.calledWith(clientUpdateParty, undefined); + }); - it('does nothing if given leader is not the leader of the party', () => { - const party = createParty(leader, [client, createClient(3)]); - client.party = party; + it('does nothing if given leader is not the leader of the party', () => { + const party = createParty(leader, [client, createClient(3)]); + client.party = party; - partyService.remove(createClient(4), client); + partyService.remove(createClient(4), client); - expect(party.clients).include(client); - assert.notCalled(leaderUpdateParty); - }); + expect(party.clients).include(client); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if given client is not in the party', () => { - const party = createParty(leader, [createClient(3), createClient(4)]); + it('does nothing if given client is not in the party', () => { + const party = createParty(leader, [createClient(3), createClient(4)]); - partyService.remove(leader, client); + partyService.remove(leader, client); - expect(party.clients).not.include(client); - assert.notCalled(leaderUpdateParty); - }); + expect(party.clients).not.include(client); + assert.notCalled(leaderUpdateParty); + }); - it('selects new party leader', () => { - const party = createParty(leader, [client, createClient(3)]); + it('selects new party leader', () => { + const party = createParty(leader, [client, createClient(3)]); - partyService.remove(leader, leader); + partyService.remove(leader, leader); - expect(party.leader).equal(client); - }); + expect(party.leader).equal(client); + }); - it('disbands party if less than 2 members are left', () => { - createParty(leader, [client]); + it('disbands party if less than 2 members are left', () => { + createParty(leader, [client]); - partyService.remove(leader, client); + partyService.remove(leader, client); - expect(leader.party).undefined; - expect(client.party).undefined; - }); + expect(leader.party).undefined; + expect(client.party).undefined; + }); - it('disbands party if cannot find new leader', () => { - const party = createParty(leader, [], [createClient(9), createClient(10)]); + it('disbands party if cannot find new leader', () => { + const party = createParty(leader, [], [createClient(9), createClient(10)]); - partyService.remove(leader, leader); + partyService.remove(leader, leader); - expect(partyService.parties).not.include(party); - }); + expect(partyService.parties).not.include(party); + }); - it('disbands party and clear all its fields', () => { - const party = createParty(leader, [client], []); + it('disbands party and clear all its fields', () => { + const party = createParty(leader, [client], []); - partyService.remove(leader, leader); + partyService.remove(leader, leader); - expect(party.clients).empty; - expect(party.pending).empty; - }); - }); + expect(party.clients).empty; + expect(party.pending).empty; + }); + }); - describe('leave()', () => { - it('calls removeFromParty', () => { - createParty(leader, [client]); - const remove = stub(partyService, 'remove'); + describe('leave()', () => { + it('calls removeFromParty', () => { + createParty(leader, [client]); + const remove = stub(partyService, 'remove'); - partyService.leave(client); + partyService.leave(client); - assert.calledWith(remove, leader, client); - }); + assert.calledWith(remove, leader, client); + }); - it('does not call removeFromParty if not in party', () => { - const remove = stub(partyService, 'remove'); + it('does not call removeFromParty if not in party', () => { + const remove = stub(partyService, 'remove'); - partyService.leave(client); + partyService.leave(client); - assert.notCalled(remove); - }); - }); + assert.notCalled(remove); + }); + }); - describe('promoteLeader()', () => { - it('sets client as leader', () => { - const party = createParty(leader, [client]); + describe('promoteLeader()', () => { + it('sets client as leader', () => { + const party = createParty(leader, [client]); - partyService.promoteLeader(leader, client); + partyService.promoteLeader(leader, client); - expect(party.leader).equal(client); - }); + expect(party.leader).equal(client); + }); - it('sends party update', () => { - createParty(leader, [client]); + it('sends party update', () => { + createParty(leader, [client]); - partyService.promoteLeader(leader, client); + partyService.promoteLeader(leader, client); - assert.calledWithMatch(leaderUpdateParty, [ - [1, PartyFlags.None], - [2, PartyFlags.Leader], - ]); - }); + assert.calledWithMatch(leaderUpdateParty, [ + [1, PartyFlags.None], + [2, PartyFlags.Leader], + ]); + }); - it('does nothing if client is already the leader', () => { - createParty(leader, [client]); + it('does nothing if client is already the leader', () => { + createParty(leader, [client]); - partyService.promoteLeader(leader, leader); + partyService.promoteLeader(leader, leader); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if client is offline', () => { - createParty(leader, [client]); - client.offline = true; + it('does nothing if client is offline', () => { + createParty(leader, [client]); + client.offline = true; - partyService.promoteLeader(leader, client); + partyService.promoteLeader(leader, client); - assert.notCalled(leaderUpdateParty); - }); + assert.notCalled(leaderUpdateParty); + }); - it('does nothing if leader is not in party', () => { - createParty(createClient(9), [client]); + it('does nothing if leader is not in party', () => { + createParty(createClient(9), [client]); - partyService.promoteLeader(leader, client); + partyService.promoteLeader(leader, client); - assert.notCalled(clientUpdateParty); - }); + assert.notCalled(clientUpdateParty); + }); - it('does nothing if leader is not a leader', () => { - createParty(createClient(9), [leader, client]); + it('does nothing if leader is not a leader', () => { + createParty(createClient(9), [leader, client]); - partyService.promoteLeader(leader, client); + partyService.promoteLeader(leader, client); - assert.notCalled(clientUpdateParty); - }); + assert.notCalled(clientUpdateParty); + }); - it('does nothing if client is not in party', () => { - createParty(leader, [createClient(9)]); + it('does nothing if client is not in party', () => { + createParty(leader, [createClient(9)]); - partyService.promoteLeader(leader, client); + partyService.promoteLeader(leader, client); - assert.notCalled(leaderUpdateParty); - }); - }); + assert.notCalled(leaderUpdateParty); + }); + }); }); diff --git a/src/ts/tests/server/services/supporterInvites.spec.ts b/src/ts/tests/server/services/supporterInvites.spec.ts index bf2f4d0..1ae32e4 100644 --- a/src/ts/tests/server/services/supporterInvites.spec.ts +++ b/src/ts/tests/server/services/supporterInvites.spec.ts @@ -13,369 +13,369 @@ import { addIgnore } from '../../../server/playerUtils'; import { DAY } from '../../../common/constants'; function exec(value: any): any { - return { exec: stub().resolves(value) }; + return { exec: stub().resolves(value) }; } describe('SupporterInvitesService', () => { - let model: SinonStubbedInstance> & { countDocuments: SinonStub; }; - let notifications = stubClass(NotificationService); - let log: SinonStub; - let service: SupporterInvitesService; - - beforeEach(() => { - resetStubMethods(notifications, 'addNotification'); - model = { - find: stub(), - countDocuments: stub(), - create: stub(), - deleteOne: stub(), - deleteMany: stub(), - updateMany: stub(), - } as any; - log = stub(); - service = new SupporterInvitesService(model as any, notifications, log); - }); - - afterEach(() => { - service.dispose(); - }); - - describe('getInvites()', () => { - it('returns all invites from given client', async () => { - const client = mockClient(); - model.find.withArgs({ source: client.account._id }).returns({ - exec: stub().resolves([ - { _id: 'foo', name: 'Foo', info: 'info', active: true, anotherField: 'xyz' }, - ]) - } as any); - - const result = await service.getInvites(client); - - expect(result).eql([ - { id: 'foo', name: 'Foo', info: 'info', active: true }, - ]); - }); - }); - - describe('isInvited()', () => { - it('returns true if has any active invites', async () => { - const client = mockClient(); - model.countDocuments.withArgs({ target: client.account._id, active: true }).returns(exec(1)); - - const result = await service.isInvited(client); - - expect(result).true; - }); - - it('returns false if doesn not have any active invites', async () => { - const client = mockClient(); - model.countDocuments.withArgs({ target: client.account._id, active: true }).returns(exec(0)); - - const result = await service.isInvited(client); - - expect(result).false; - }); - }); - - describe('requestInvite()', () => { - let requester: IClient; - let target: IClient; - - beforeEach(() => { - requester = mockClient(); - requester.account.supporter = SupporterFlags.Supporter1; - target = mockClient(); - model.find.returns(exec([])); - }); - - it('adds notification', async () => { - requester.pony.name = 'Foo'; - - await service.requestInvite(requester, target); - - assert.calledWith(notifications.addNotification, target, match.any); - const notification = notifications.addNotification.args[0][1]; - expect(notification.sender).equal(requester); - expect(notification.entityId).equal(requester.pony.id); - expect(notification.message).equal(`#NAME# invited you to supporter servers`); - }); - - it('fails if inviting self', async () => { - await service.requestInvite(requester, requester); - - assert.notCalled(notifications.addNotification); - expect(requester.saysQueue).eql([ - [requester.pony.id, 'Cannot invite', MessageType.System], - ]); - }); - - it('fails if requester is shadowed', async () => { - requester.shadowed = true; + let model: SinonStubbedInstance> & { countDocuments: SinonStub; }; + let notifications = stubClass(NotificationService); + let log: SinonStub; + let service: SupporterInvitesService; + + beforeEach(() => { + resetStubMethods(notifications, 'addNotification'); + model = { + find: stub(), + countDocuments: stub(), + create: stub(), + deleteOne: stub(), + deleteMany: stub(), + updateMany: stub(), + } as any; + log = stub(); + service = new SupporterInvitesService(model as any, notifications, log); + }); + + afterEach(() => { + service.dispose(); + }); + + describe('getInvites()', () => { + it('returns all invites from given client', async () => { + const client = mockClient(); + model.find.withArgs({ source: client.account._id }).returns({ + exec: stub().resolves([ + { _id: 'foo', name: 'Foo', info: 'info', active: true, anotherField: 'xyz' }, + ]) + } as any); + + const result = await service.getInvites(client); + + expect(result).eql([ + { id: 'foo', name: 'Foo', info: 'info', active: true }, + ]); + }); + }); + + describe('isInvited()', () => { + it('returns true if has any active invites', async () => { + const client = mockClient(); + model.countDocuments.withArgs({ target: client.account._id, active: true }).returns(exec(1)); + + const result = await service.isInvited(client); + + expect(result).true; + }); + + it('returns false if doesn not have any active invites', async () => { + const client = mockClient(); + model.countDocuments.withArgs({ target: client.account._id, active: true }).returns(exec(0)); + + const result = await service.isInvited(client); + + expect(result).false; + }); + }); + + describe('requestInvite()', () => { + let requester: IClient; + let target: IClient; + + beforeEach(() => { + requester = mockClient(); + requester.account.supporter = SupporterFlags.Supporter1; + target = mockClient(); + model.find.returns(exec([])); + }); + + it('adds notification', async () => { + requester.pony.name = 'Foo'; + + await service.requestInvite(requester, target); + + assert.calledWith(notifications.addNotification, target, match.any); + const notification = notifications.addNotification.args[0][1]; + expect(notification.sender).equal(requester); + expect(notification.entityId).equal(requester.pony.id); + expect(notification.message).equal(`#NAME# invited you to supporter servers`); + }); + + it('fails if inviting self', async () => { + await service.requestInvite(requester, requester); + + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); + + it('fails if requester is shadowed', async () => { + requester.shadowed = true; - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - assert.notCalled(notifications.addNotification); - expect(requester.saysQueue).eql([ - [requester.pony.id, 'Cannot invite', MessageType.System], - ]); - }); + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); - it('fails if requester is muted', async () => { - requester.account.mute = Date.now() + 10000; + it('fails if requester is muted', async () => { + requester.account.mute = Date.now() + 10000; - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - assert.notCalled(notifications.addNotification); - expect(requester.saysQueue).eql([ - [requester.pony.id, 'Cannot invite', MessageType.System], - ]); - }); + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); - it('fails if target is offline', async () => { - target.offline = true; + it('fails if target is offline', async () => { + target.offline = true; - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - assert.notCalled(notifications.addNotification); - expect(requester.saysQueue).eql([ - [requester.pony.id, 'Cannot invite', MessageType.System], - ]); - }); + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); - it('fails if target ignores requester', async () => { - addIgnore(requester, target.accountId); + it('fails if target ignores requester', async () => { + addIgnore(requester, target.accountId); - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - assert.notCalled(notifications.addNotification); - expect(requester.saysQueue).eql([ - [requester.pony.id, 'Cannot invite', MessageType.System], - ]); - }); + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); - it('fails if requester ignores target', async () => { - addIgnore(target, requester.accountId); + it('fails if requester ignores target', async () => { + addIgnore(target, requester.accountId); - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - assert.notCalled(notifications.addNotification); - expect(requester.saysQueue).eql([ - [requester.pony.id, 'Cannot invite', MessageType.System], - ]); - }); + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); - it('fails if already reached invite limit', async () => { - model.find.returns(exec([{ _id: 'foo' }])); + it('fails if already reached invite limit', async () => { + model.find.returns(exec([{ _id: 'foo' }])); - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - assert.notCalled(notifications.addNotification); - expect(requester.saysQueue).eql([ - [requester.pony.id, 'Invite limit reached', MessageType.System], - ]); - }); + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Invite limit reached', MessageType.System], + ]); + }); - it('fails if exceeded reject limit', async () => { - for (let i = 0; i < 5; i++) { - await service.requestInvite(requester, target); - notifications.addNotification.args[i][1].reject!(); - } + it('fails if exceeded reject limit', async () => { + for (let i = 0; i < 5; i++) { + await service.requestInvite(requester, target); + notifications.addNotification.args[i][1].reject!(); + } - notifications.addNotification.reset(); + notifications.addNotification.reset(); - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - assert.notCalled(notifications.addNotification); - expect(requester.saysQueue).eql([ - [requester.pony.id, 'Cannot invite', MessageType.System], - ]); - }); + assert.notCalled(notifications.addNotification); + expect(requester.saysQueue).eql([ + [requester.pony.id, 'Cannot invite', MessageType.System], + ]); + }); - it('logs invite', async () => { - requester.character.name = 'Foo'; + it('logs invite', async () => { + requester.character.name = 'Foo'; - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - assert.calledOnce(log); - }); + assert.calledOnce(log); + }); - it('accepts invite when accept callback is invoked', async () => { - const acceptInvite = stub(service, 'acceptInvite'); + it('accepts invite when accept callback is invoked', async () => { + const acceptInvite = stub(service, 'acceptInvite'); - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - const { accept } = notifications.addNotification.args[0][1]; - accept!(); + const { accept } = notifications.addNotification.args[0][1]; + accept!(); - assert.calledWith(acceptInvite, requester, target); - }); + assert.calledWith(acceptInvite, requester, target); + }); - it('rejects invite when reject callback is invoked', async () => { - const rejectInvite = stub(service, 'rejectInvite'); + it('rejects invite when reject callback is invoked', async () => { + const rejectInvite = stub(service, 'rejectInvite'); - await service.requestInvite(requester, target); + await service.requestInvite(requester, target); - const { reject } = notifications.addNotification.args[0][1]; - reject!(); + const { reject } = notifications.addNotification.args[0][1]; + reject!(); - assert.calledWith(rejectInvite, requester, target); - }); - }); + assert.calledWith(rejectInvite, requester, target); + }); + }); - describe('acceptInvite()', () => { - it('invites user', () => { - const requester = mockClient(); - const target = mockClient(); - const invite = stub(service, 'invite'); + describe('acceptInvite()', () => { + it('invites user', () => { + const requester = mockClient(); + const target = mockClient(); + const invite = stub(service, 'invite'); - service.acceptInvite(requester, target); + service.acceptInvite(requester, target); - assert.calledOnce(invite); - }); + assert.calledOnce(invite); + }); - it('logs accepted invite', () => { - const requester = mockClient(); - const target = mockClient(); - stub(service, 'invite'); + it('logs accepted invite', () => { + const requester = mockClient(); + const target = mockClient(); + stub(service, 'invite'); - service.acceptInvite(requester, target); + service.acceptInvite(requester, target); - assert.calledOnce(log); - }); - }); + assert.calledOnce(log); + }); + }); - describe('rejectInvite()', () => { - it('logs rejected invite', () => { - const requester = mockClient(); - const target = mockClient(); + describe('rejectInvite()', () => { + it('logs rejected invite', () => { + const requester = mockClient(); + const target = mockClient(); - service.rejectInvite(requester, target); + service.rejectInvite(requester, target); - assert.calledOnce(log); - }); - }); + assert.calledOnce(log); + }); + }); - describe('invite()', () => { - it('creates new invite', async () => { - const requester = mockClient(); - requester.account.supporter = SupporterFlags.Supporter1; - const target = mockClient(); - model.find.returns(exec([])); - - await service.invite(requester, target); + describe('invite()', () => { + it('creates new invite', async () => { + const requester = mockClient(); + requester.account.supporter = SupporterFlags.Supporter1; + const target = mockClient(); + model.find.returns(exec([])); + + await service.invite(requester, target); - assert.calledWithMatch(model.create, { - source: requester.account._id, - target: target.account._id, - name: target.character.name, - info: target.character.info, - active: true, - }); - }); + assert.calledWithMatch(model.create, { + source: requester.account._id, + target: target.account._id, + name: target.character.name, + info: target.character.info, + active: true, + }); + }); - it('throws if reached invite limit', async () => { - const requester = mockClient(); - requester.account.supporter = SupporterFlags.Supporter1; - const target = mockClient(); - model.find.returns(exec([{}])); + it('throws if reached invite limit', async () => { + const requester = mockClient(); + requester.account.supporter = SupporterFlags.Supporter1; + const target = mockClient(); + model.find.returns(exec([{}])); - await expect(service.invite(requester, target)).rejectedWith(); + await expect(service.invite(requester, target)).rejectedWith(); - assert.notCalled(model.create); - }); - }); + assert.notCalled(model.create); + }); + }); - describe('uninvite()', () => { - it('removes invite', async () => { - const requester = mockClient(); - model.deleteOne.returns({ exec: stub() } as any); + describe('uninvite()', () => { + it('removes invite', async () => { + const requester = mockClient(); + model.deleteOne.returns({ exec: stub() } as any); - await service.uninvite(requester, 'foobar'); + await service.uninvite(requester, 'foobar'); - assert.calledWithMatch(model.deleteOne, { _id: 'foobar', source: requester.account._id }); - }); - }); + assert.calledWithMatch(model.deleteOne, { _id: 'foobar', source: requester.account._id }); + }); + }); - describe('updateSupporterInvites()', () => { - let clock: SinonFakeTimers; - - beforeEach(() => { - clock = useFakeTimers(); - }); - - afterEach(() => { - clock.restore(); - }); - - it('activates inactive items', async () => { - const data = [ - { _id: 'aaa', active: false, source: { supporter: SupporterFlags.Supporter1 } }, - { _id: 'bbb', active: true, source: { supporter: SupporterFlags.Supporter1 } }, - ]; - model.find.withArgs({}, '_id active') - .returns({ - populate: stub().withArgs('source', '_id supporter patreon roles') - .returns({ lean: stub().returns(exec(data)) }) - } as any); - model.deleteMany.returns({ exec: stub() } as any); - model.updateMany.returns({ exec: stub() } as any); - - await updateSupporterInvites(model as any); - - assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['aaa'] } }, { active: true }); - }); - - it('deactivates active items', async () => { - const data = [ - { _id: 'aaa', active: true, source: { supporter: SupporterFlags.Supporter1 } }, - { _id: 'bbb', active: true, source: {} }, - ]; - model.find.returns({ populate: stub().returns({ lean: stub().returns(exec(data)) }) } as any); - model.deleteMany.returns({ exec: stub() } as any); - model.updateMany.returns({ exec: stub() } as any); - - await updateSupporterInvites(model as any); - - assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['bbb'] } }, { active: false }); - }); - - it('activates and deactivates items', async () => { - const data = [ - { _id: 'aaa', active: false, source: { supporter: SupporterFlags.Supporter1 } }, - { _id: 'bbb', active: true, source: {} }, - ]; - model.find.returns({ populate: stub().returns({ lean: stub().returns(exec(data)) }) } as any); - model.deleteMany.returns({ exec: stub() } as any); - model.updateMany.returns({ exec: stub() } as any); - - await updateSupporterInvites(model as any); - - assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['aaa'] } }, { active: true }); - assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['bbb'] } }, { active: false }); - }); - - it('does nothing if all items have correct active flag', async () => { - const data = [ - { _id: 'aaa', active: true, source: { supporter: SupporterFlags.Supporter1 } }, - { _id: 'bbb', active: false, source: {} }, - ]; - model.find.returns({ populate: stub().returns({ lean: stub().returns(exec(data)) }) } as any); - model.deleteMany.returns({ exec: stub() } as any); - model.updateMany.returns({ exec: stub() } as any); - - await updateSupporterInvites(model as any); - - assert.notCalled(model.updateMany); - }); - - it('removes old inactive entries', async () => { - model.find.returns({ populate: stub().returns({ lean: stub().returns({ exec: stub() }) }) } as any); - model.deleteMany.returns({ exec: stub() } as any); - clock.setSystemTime(123 * DAY); - - await updateSupporterInvites(model as any); - - assert.calledWithMatch(model.deleteMany, { active: false, updatedAt: { $lt: new Date(23 * DAY) } }); - }); - }); + describe('updateSupporterInvites()', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('activates inactive items', async () => { + const data = [ + { _id: 'aaa', active: false, source: { supporter: SupporterFlags.Supporter1 } }, + { _id: 'bbb', active: true, source: { supporter: SupporterFlags.Supporter1 } }, + ]; + model.find.withArgs({}, '_id active') + .returns({ + populate: stub().withArgs('source', '_id supporter patreon roles') + .returns({ lean: stub().returns(exec(data)) }) + } as any); + model.deleteMany.returns({ exec: stub() } as any); + model.updateMany.returns({ exec: stub() } as any); + + await updateSupporterInvites(model as any); + + assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['aaa'] } }, { active: true }); + }); + + it('deactivates active items', async () => { + const data = [ + { _id: 'aaa', active: true, source: { supporter: SupporterFlags.Supporter1 } }, + { _id: 'bbb', active: true, source: {} }, + ]; + model.find.returns({ populate: stub().returns({ lean: stub().returns(exec(data)) }) } as any); + model.deleteMany.returns({ exec: stub() } as any); + model.updateMany.returns({ exec: stub() } as any); + + await updateSupporterInvites(model as any); + + assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['bbb'] } }, { active: false }); + }); + + it('activates and deactivates items', async () => { + const data = [ + { _id: 'aaa', active: false, source: { supporter: SupporterFlags.Supporter1 } }, + { _id: 'bbb', active: true, source: {} }, + ]; + model.find.returns({ populate: stub().returns({ lean: stub().returns(exec(data)) }) } as any); + model.deleteMany.returns({ exec: stub() } as any); + model.updateMany.returns({ exec: stub() } as any); + + await updateSupporterInvites(model as any); + + assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['aaa'] } }, { active: true }); + assert.calledWithMatch(model.updateMany as any, { _id: { $in: ['bbb'] } }, { active: false }); + }); + + it('does nothing if all items have correct active flag', async () => { + const data = [ + { _id: 'aaa', active: true, source: { supporter: SupporterFlags.Supporter1 } }, + { _id: 'bbb', active: false, source: {} }, + ]; + model.find.returns({ populate: stub().returns({ lean: stub().returns(exec(data)) }) } as any); + model.deleteMany.returns({ exec: stub() } as any); + model.updateMany.returns({ exec: stub() } as any); + + await updateSupporterInvites(model as any); + + assert.notCalled(model.updateMany); + }); + + it('removes old inactive entries', async () => { + model.find.returns({ populate: stub().returns({ lean: stub().returns({ exec: stub() }) }) } as any); + model.deleteMany.returns({ exec: stub() } as any); + clock.setSystemTime(123 * DAY); + + await updateSupporterInvites(model as any); + + assert.calledWithMatch(model.deleteMany, { active: false, updatedAt: { $lt: new Date(23 * DAY) } }); + }); + }); }); diff --git a/src/ts/tests/server/spamChecker.spec.ts b/src/ts/tests/server/spamChecker.spec.ts index 90d6689..d2de782 100644 --- a/src/ts/tests/server/spamChecker.spec.ts +++ b/src/ts/tests/server/spamChecker.spec.ts @@ -8,265 +8,265 @@ import { fromNow, times as utilsTimes } from '../../common/utils'; import { randomString } from '../../common/stringUtils'; import { DAY, SAY_MAX_LENGTH } from '../../common/constants'; import { - REPORT_AFTER_LIMIT, MUTE_AFTER_LIMIT, SHORT_MESSAGE_MUL, LONG_MESSAGE_MUL, TINY_MESSAGE_MUL, - createSpamChecker, RAPID_MESSAGE_COUNT + REPORT_AFTER_LIMIT, MUTE_AFTER_LIMIT, SHORT_MESSAGE_MUL, LONG_MESSAGE_MUL, TINY_MESSAGE_MUL, + createSpamChecker, RAPID_MESSAGE_COUNT } from '../../server/spamChecker'; import { mockClient } from '../mocks'; import { SPAM_TIMEOUT } from '../../server/reporting'; function times(count: number, action: (i: number) => any) { - return Promise.all(utilsTimes(count, action)); + return Promise.all(utilsTimes(count, action)); } describe('SpamChecker', () => { - describe('check()', () => { - let client: IClient; - let settings: GameServerSettings; - let spamCounter: CounterService; - let rapidCounter: CounterService; - let countSpamming: SinonStub; - let timeoutAccount: SinonStub; - let spamChecker: OnMessageSettings; + describe('check()', () => { + let client: IClient; + let settings: GameServerSettings; + let spamCounter: CounterService; + let rapidCounter: CounterService; + let countSpamming: SinonStub; + let timeoutAccount: SinonStub; + let spamChecker: OnMessageSettings; - beforeEach(() => { - client = mockClient(); - client.account.createdAt = fromNow(-2 * DAY); - settings = { - reportSpam: true, - autoBanSpamming: true, - }; - spamCounter = new CounterService(1000); - rapidCounter = new CounterService(1000); - countSpamming = stub().resolves(); - timeoutAccount = stub().resolves(); - spamChecker = createFunctionWithPromiseHandler( - createSpamChecker, spamCounter, rapidCounter, countSpamming, timeoutAccount); - }); + beforeEach(() => { + client = mockClient(); + client.account.createdAt = fromNow(-2 * DAY); + settings = { + reportSpam: true, + autoBanSpamming: true, + }; + spamCounter = new CounterService(1000); + rapidCounter = new CounterService(1000); + countSpamming = stub().resolves(); + timeoutAccount = stub().resolves(); + spamChecker = createFunctionWithPromiseHandler( + createSpamChecker, spamCounter, rapidCounter, countSpamming, timeoutAccount); + }); - it('does not count spam for mods', async () => { - client.isMod = true; + it('does not count spam for mods', async () => { + client.isMod = true; - await times(10, () => spamChecker(client, 'long_spam_text', settings)); + await times(10, () => spamChecker(client, 'long_spam_text', settings)); - assert.notCalled(countSpamming); - }); + assert.notCalled(countSpamming); + }); - it('counts spam if reporting is turned off', async () => { - settings.reportSpam = false; + it('counts spam if reporting is turned off', async () => { + settings.reportSpam = false; - await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); - assert.calledOnce(countSpamming); - }); + assert.calledOnce(countSpamming); + }); - it('does not report if reporting is turned off', async () => { - settings.reportSpam = false; - const warn = stub(client.reporter, 'warn'); + it('does not report if reporting is turned off', async () => { + settings.reportSpam = false; + const warn = stub(client.reporter, 'warn'); - await times(10, () => spamChecker(client, 'long_spam_text', settings)); + await times(10, () => spamChecker(client, 'long_spam_text', settings)); - assert.notCalled(warn); - }); + assert.notCalled(warn); + }); - it('reports spam', async () => { - const warn = stub(client.reporter, 'warn'); + it('reports spam', async () => { + const warn = stub(client.reporter, 'warn'); - await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); - assert.calledWith(warn, 'Spam', 'long_spam_text'); - }); + assert.calledWith(warn, 'Spam', 'long_spam_text'); + }); - it(`counts spam after ${REPORT_AFTER_LIMIT} messages`, async () => { - await times(REPORT_AFTER_LIMIT - 1, () => spamChecker(client, 'long_spam_text', settings)); + it(`counts spam after ${REPORT_AFTER_LIMIT} messages`, async () => { + await times(REPORT_AFTER_LIMIT - 1, () => spamChecker(client, 'long_spam_text', settings)); - assert.notCalled(countSpamming); + assert.notCalled(countSpamming); - await times(1, () => spamChecker(client, 'long_spam_text', settings)); + await times(1, () => spamChecker(client, 'long_spam_text', settings)); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it(`counts spam after ${REPORT_AFTER_LIMIT} commands`, async () => { - await times(REPORT_AFTER_LIMIT - 1, () => spamChecker(client, '/random 1000', settings)); + it(`counts spam after ${REPORT_AFTER_LIMIT} commands`, async () => { + await times(REPORT_AFTER_LIMIT - 1, () => spamChecker(client, '/random 1000', settings)); - assert.notCalled(countSpamming); + assert.notCalled(countSpamming); - await times(1, () => spamChecker(client, '/random 1000', settings)); + await times(1, () => spamChecker(client, '/random 1000', settings)); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it(`counts spam after ${REPORT_AFTER_LIMIT * SHORT_MESSAGE_MUL} messages for short messages`, async () => { - await times(REPORT_AFTER_LIMIT * SHORT_MESSAGE_MUL - 1, () => spamChecker(client, 'short', settings)); + it(`counts spam after ${REPORT_AFTER_LIMIT * SHORT_MESSAGE_MUL} messages for short messages`, async () => { + await times(REPORT_AFTER_LIMIT * SHORT_MESSAGE_MUL - 1, () => spamChecker(client, 'short', settings)); - assert.notCalled(countSpamming); + assert.notCalled(countSpamming); - await times(1, () => spamChecker(client, 'short', settings)); + await times(1, () => spamChecker(client, 'short', settings)); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it(`counts spam after ${REPORT_AFTER_LIMIT * TINY_MESSAGE_MUL} messages for tiny messages`, async () => { - await times(REPORT_AFTER_LIMIT * TINY_MESSAGE_MUL - 1, () => spamChecker(client, 'abc', settings)); + it(`counts spam after ${REPORT_AFTER_LIMIT * TINY_MESSAGE_MUL} messages for tiny messages`, async () => { + await times(REPORT_AFTER_LIMIT * TINY_MESSAGE_MUL - 1, () => spamChecker(client, 'abc', settings)); - assert.notCalled(countSpamming); + assert.notCalled(countSpamming); - await times(1, () => spamChecker(client, 'abc', settings)); + await times(1, () => spamChecker(client, 'abc', settings)); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it(`counts spam after ${REPORT_AFTER_LIMIT} messages mixed with other messages`, async () => { - await times(REPORT_AFTER_LIMIT - 1, async () => { - await spamChecker(client, '1long_spam_text1', settings); - await spamChecker(client, '2long_spam_text2', settings); - }); + it(`counts spam after ${REPORT_AFTER_LIMIT} messages mixed with other messages`, async () => { + await times(REPORT_AFTER_LIMIT - 1, async () => { + await spamChecker(client, '1long_spam_text1', settings); + await spamChecker(client, '2long_spam_text2', settings); + }); - assert.notCalled(countSpamming); + assert.notCalled(countSpamming); - await times(1, () => spamChecker(client, '1long_spam_text1', settings)); + await times(1, () => spamChecker(client, '1long_spam_text1', settings)); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it(`counts spam after ${REPORT_AFTER_LIMIT} message mixed with ${REPORT_AFTER_LIMIT} other messages`, async () => { - await times(REPORT_AFTER_LIMIT, async () => { - await spamChecker(client, '1long_spam_text1', settings); - await spamChecker(client, '2long_spam_text2', settings); - await spamChecker(client, '3long_spam_text3', settings); - await spamChecker(client, '4long_spam_text4', settings); - await spamChecker(client, '5long_spam_text5', settings); - }); + it(`counts spam after ${REPORT_AFTER_LIMIT} message mixed with ${REPORT_AFTER_LIMIT} other messages`, async () => { + await times(REPORT_AFTER_LIMIT, async () => { + await spamChecker(client, '1long_spam_text1', settings); + await spamChecker(client, '2long_spam_text2', settings); + await spamChecker(client, '3long_spam_text3', settings); + await spamChecker(client, '4long_spam_text4', settings); + await spamChecker(client, '5long_spam_text5', settings); + }); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it(`forgets message after ${REPORT_AFTER_LIMIT} other messages`, async () => { - await spamChecker(client, 'long_spam_text1', settings); + it(`forgets message after ${REPORT_AFTER_LIMIT} other messages`, async () => { + await spamChecker(client, 'long_spam_text1', settings); - for (let i = 0; i < REPORT_AFTER_LIMIT; i++) { - await spamChecker(client, i + '2long_spam_text2', settings); - await spamChecker(client, i + '3long_spam_text3', settings); - await spamChecker(client, i + '4long_spam_text4', settings); - await spamChecker(client, i + '5long_spam_text5', settings); - await spamChecker(client, i + '6long_spam_text6', settings); - await spamChecker(client, i + '7long_spam_text7', settings); - } + for (let i = 0; i < REPORT_AFTER_LIMIT; i++) { + await spamChecker(client, i + '2long_spam_text2', settings); + await spamChecker(client, i + '3long_spam_text3', settings); + await spamChecker(client, i + '4long_spam_text4', settings); + await spamChecker(client, i + '5long_spam_text5', settings); + await spamChecker(client, i + '6long_spam_text6', settings); + await spamChecker(client, i + '7long_spam_text7', settings); + } - for (let i = 2; i < REPORT_AFTER_LIMIT; i++) { - await spamChecker(client, 'long_spam_text1', settings); - } + for (let i = 2; i < REPORT_AFTER_LIMIT; i++) { + await spamChecker(client, 'long_spam_text1', settings); + } - assert.notCalled(countSpamming); - }); + assert.notCalled(countSpamming); + }); - it(`counts spam with timeout if autoBanSpamming option is on and counter is ${MUTE_AFTER_LIMIT}`, async () => { - settings.autoBanSpamming = true; - stub(spamCounter, 'add').returns({ count: MUTE_AFTER_LIMIT, items: ['long_spam_text'], date: 0 }); + it(`counts spam with timeout if autoBanSpamming option is on and counter is ${MUTE_AFTER_LIMIT}`, async () => { + settings.autoBanSpamming = true; + stub(spamCounter, 'add').returns({ count: MUTE_AFTER_LIMIT, items: ['long_spam_text'], date: 0 }); - await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it('adds entry to spam counter', async () => { - const add = stub(spamCounter, 'add').returns({ count: 0, items: [], date: 0 }); + it('adds entry to spam counter', async () => { + const add = stub(spamCounter, 'add').returns({ count: 0, items: [], date: 0 }); - await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); - assert.calledWith(add, client.accountId, 'long_spam_text', 1); - }); + assert.calledWith(add, client.accountId, 'long_spam_text', 1); + }); - it('adds entry to spam counter with increment of 2 for max length message', async () => { - const add = stub(spamCounter, 'add').returns({ count: 0, items: [], date: 0 }); - const message = randomString(SAY_MAX_LENGTH); + it('adds entry to spam counter with increment of 2 for max length message', async () => { + const add = stub(spamCounter, 'add').returns({ count: 0, items: [], date: 0 }); + const message = randomString(SAY_MAX_LENGTH); - await times(REPORT_AFTER_LIMIT, () => spamChecker(client, message, {})); + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, message, {})); - assert.calledWith(add, client.accountId, message, 2); - }); + assert.calledWith(add, client.accountId, message, 2); + }); - it('adds entry to spam counter with increment of 2 for long messages', async () => { - const add = stub(spamCounter, 'add').returns({ count: 0, items: [], date: 0 }); - const message = 'AAAAALGIRNGLRINGLISAHGLEISRHGLISRHGLISRHGLISRHGISRXY'; + it('adds entry to spam counter with increment of 2 for long messages', async () => { + const add = stub(spamCounter, 'add').returns({ count: 0, items: [], date: 0 }); + const message = 'AAAAALGIRNGLRINGLISAHGLEISRHGLISRHGLISRHGLISRHGISRXY'; - await times(REPORT_AFTER_LIMIT, () => spamChecker(client, message, {})); + await times(REPORT_AFTER_LIMIT, () => spamChecker(client, message, {})); - assert.calledWith(add, client.accountId, message, 2); - }); + assert.calledWith(add, client.accountId, message, 2); + }); - const longLimit = Math.ceil(REPORT_AFTER_LIMIT * LONG_MESSAGE_MUL); + const longLimit = Math.ceil(REPORT_AFTER_LIMIT * LONG_MESSAGE_MUL); - it(`counts spam after ${longLimit} LONG messages`, async () => { - const longestMessage = 'kgfdjhskgdfhgkdlufhgkdfghdfgudhrkughrdkughkdruhgkdurhgkdurhgkudh'; + it(`counts spam after ${longLimit} LONG messages`, async () => { + const longestMessage = 'kgfdjhskgdfhgkdlufhgkdfghdfgudhrkughrdkughkdruhgkdurhgkdurhgkudh'; - await times(longLimit - 1, () => spamChecker(client, longestMessage, {})); + await times(longLimit - 1, () => spamChecker(client, longestMessage, {})); - assert.notCalled(countSpamming); + assert.notCalled(countSpamming); - await times(1, () => spamChecker(client, longestMessage, {})); + await times(1, () => spamChecker(client, longestMessage, {})); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it(`matches spam`, async () => { - await times(REPORT_AFTER_LIMIT - 1, () => spamChecker(client, `some message`, {})); + it(`matches spam`, async () => { + await times(REPORT_AFTER_LIMIT - 1, () => spamChecker(client, `some message`, {})); - assert.notCalled(countSpamming); + assert.notCalled(countSpamming); - await times(1, () => spamChecker(client, `some message`, {})); + await times(1, () => spamChecker(client, `some message`, {})); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it(`matches partial spam `, async () => { - await times(REPORT_AFTER_LIMIT - 1, i => spamChecker(client, `common message part ${i} aaa`, {})); + it(`matches partial spam `, async () => { + await times(REPORT_AFTER_LIMIT - 1, i => spamChecker(client, `common message part ${i} aaa`, {})); - assert.notCalled(countSpamming); + assert.notCalled(countSpamming); - await times(1, () => spamChecker(client, `common message part x aaa`, {})); + await times(1, () => spamChecker(client, `common message part x aaa`, {})); - assert.calledWith(countSpamming, client.accountId); - }); + assert.calledWith(countSpamming, client.accountId); + }); - it('timeouts for spamming', async () => { - await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + it('timeouts for spamming', async () => { + await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); - assert.calledWith(timeoutAccount, client.accountId); - }); + assert.calledWith(timeoutAccount, client.accountId); + }); - it('uses double timeout length is doubleTimeouts setting is set', async () => { - settings.doubleTimeouts = true; + it('uses double timeout length is doubleTimeouts setting is set', async () => { + settings.doubleTimeouts = true; - await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); - assert.calledWith(timeoutAccount, client.accountId); - expect(timeoutAccount.args[0][1].getTime()).greaterThan(fromNow(SPAM_TIMEOUT * 1.9).getTime()); - }); + assert.calledWith(timeoutAccount, client.accountId); + expect(timeoutAccount.args[0][1].getTime()).greaterThan(fromNow(SPAM_TIMEOUT * 1.9).getTime()); + }); - it('reports timing out', async () => { - const system = stub(client.reporter, 'system'); + it('reports timing out', async () => { + const system = stub(client.reporter, 'system'); - await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); - assert.calledWith(system, 'Timed out for spamming'); - }); + assert.calledWith(system, 'Timed out for spamming'); + }); - it('logs timing out if reporting is turned off', async () => { - settings.reportSpam = false; - const system = stub(client.reporter, 'system'); - const systemLog = stub(client.reporter, 'systemLog'); + it('logs timing out if reporting is turned off', async () => { + settings.reportSpam = false; + const system = stub(client.reporter, 'system'); + const systemLog = stub(client.reporter, 'systemLog'); - await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); + await times(REPORT_AFTER_LIMIT * MUTE_AFTER_LIMIT, () => spamChecker(client, 'long_spam_text', settings)); - assert.calledWith(systemLog, 'Timed out for spamming'); - assert.notCalled(system); - }); + assert.calledWith(systemLog, 'Timed out for spamming'); + assert.notCalled(system); + }); - it('counts a lot of rapid messages as spam', async () => { - await times(RAPID_MESSAGE_COUNT + 1, i => spamChecker(client, i + '-' + i, {})); + it('counts a lot of rapid messages as spam', async () => { + await times(RAPID_MESSAGE_COUNT + 1, i => spamChecker(client, i + '-' + i, {})); - assert.calledWith(countSpamming, client.accountId); - }); - }); + assert.calledWith(countSpamming, client.accountId); + }); + }); }); diff --git a/src/ts/tests/server/utils/byteSize.spec.ts b/src/ts/tests/server/utils/byteSize.spec.ts index 36ba76d..3567ea4 100644 --- a/src/ts/tests/server/utils/byteSize.spec.ts +++ b/src/ts/tests/server/utils/byteSize.spec.ts @@ -3,100 +3,100 @@ import { expect } from 'chai'; import { ByteSize } from '../../../server/utils/byteSize'; describe('ByteSize ', () => { - it('starts with zeroed fields', () => { - const size = new ByteSize(); + it('starts with zeroed fields', () => { + const size = new ByteSize(); - expect(size.bytes).equal(0); - expect(size.mbytes).equal(0); - }); + expect(size.bytes).equal(0); + expect(size.mbytes).equal(0); + }); - it('can be created with inital values', () => { - const size = new ByteSize(5, 6); + it('can be created with inital values', () => { + const size = new ByteSize(5, 6); - expect(size.bytes).equal(5); - expect(size.mbytes).equal(6); - }); + expect(size.bytes).equal(5); + expect(size.mbytes).equal(6); + }); - it('reduces overflowing value of bytes when created', () => { - const size = new ByteSize(6 * 1024 * 1024 + 5, 4); + it('reduces overflowing value of bytes when created', () => { + const size = new ByteSize(6 * 1024 * 1024 + 5, 4); - expect(size.bytes).equal(5); - expect(size.mbytes).equal(10); - }); + expect(size.bytes).equal(5); + expect(size.mbytes).equal(10); + }); - it('adds two values together', () => { - const size = new ByteSize(6, 4); - const x = new ByteSize(6, 4); + it('adds two values together', () => { + const size = new ByteSize(6, 4); + const x = new ByteSize(6, 4); - size.add(x); + size.add(x); - expect(size.bytes).equal(12); - expect(size.mbytes).equal(8); - }); + expect(size.bytes).equal(12); + expect(size.mbytes).equal(8); + }); - it('adds value directly', () => { - const size = new ByteSize(6, 4); + it('adds value directly', () => { + const size = new ByteSize(6, 4); - size.addBytes(6, 4); + size.addBytes(6, 4); - expect(size.bytes).equal(12); - expect(size.mbytes).equal(8); - }); + expect(size.bytes).equal(12); + expect(size.mbytes).equal(8); + }); - it('adds value directly with just bytes', () => { - const size = new ByteSize(6, 4); + it('adds value directly with just bytes', () => { + const size = new ByteSize(6, 4); - size.addBytes(6); + size.addBytes(6); - expect(size.bytes).equal(12); - expect(size.mbytes).equal(4); - }); + expect(size.bytes).equal(12); + expect(size.mbytes).equal(4); + }); - describe('.toString()', () => { - it('get string value for only bytes', () => { - const size = new ByteSize(6); + describe('.toString()', () => { + it('get string value for only bytes', () => { + const size = new ByteSize(6); - expect(size.toString()).equal('6'); - }); + expect(size.toString()).equal('6'); + }); - it('get string value for bytes and megabytes', () => { - const size = new ByteSize(6, 5); + it('get string value for bytes and megabytes', () => { + const size = new ByteSize(6, 5); - expect(size.toString()).equal('5000006'); - }); - }); + expect(size.toString()).equal('5000006'); + }); + }); - describe('.toSortableString()', () => { - it('get padded string value for only bytes', () => { - const size = new ByteSize(6); + describe('.toSortableString()', () => { + it('get padded string value for only bytes', () => { + const size = new ByteSize(6); - expect(size.toSortableString()).equal('000000000-000006'); - }); + expect(size.toSortableString()).equal('000000000-000006'); + }); - it('get padded string value for bytes and megabytes', () => { - const size = new ByteSize(6, 5); + it('get padded string value for bytes and megabytes', () => { + const size = new ByteSize(6, 5); - expect(size.toSortableString()).equal('000000005-000006'); - }); - }); + expect(size.toSortableString()).equal('000000005-000006'); + }); + }); - describe('.toHumanReadable()', () => { - it('get value in b for values below 2 kb', () => { - const size = new ByteSize(6); + describe('.toHumanReadable()', () => { + it('get value in b for values below 2 kb', () => { + const size = new ByteSize(6); - expect(size.toHumanReadable()).equal('6 b'); - }); + expect(size.toHumanReadable()).equal('6 b'); + }); - it('get value in kb for values above 2 kb', () => { - const size = new ByteSize(6 * 1024); + it('get value in kb for values above 2 kb', () => { + const size = new ByteSize(6 * 1024); - expect(size.toHumanReadable()).equal('6 kb'); - }); + expect(size.toHumanReadable()).equal('6 kb'); + }); - it('get value in mb for values above 1 mb', () => { - const size = new ByteSize(5, 2); + it('get value in mb for values above 1 mb', () => { + const size = new ByteSize(5, 2); - expect(size.toHumanReadable()).equal('2 mb'); - }); - }); + expect(size.toHumanReadable()).equal('2 mb'); + }); + }); }); diff --git a/src/ts/tests/server/utils/taskQueue.spec.ts b/src/ts/tests/server/utils/taskQueue.spec.ts index 295911d..a1e485b 100644 --- a/src/ts/tests/server/utils/taskQueue.spec.ts +++ b/src/ts/tests/server/utils/taskQueue.spec.ts @@ -4,116 +4,116 @@ import { stub, assert, SinonFakeTimers, useFakeTimers } from 'sinon'; import { taskQueue, TaskQueue, makeQueued } from '../../../server/utils/taskQueue'; describe('taskQueue', () => { - let queue: TaskQueue; - let clock: SinonFakeTimers; + let queue: TaskQueue; + let clock: SinonFakeTimers; - beforeEach(() => { - queue = taskQueue(); - clock = useFakeTimers(); - }); + beforeEach(() => { + queue = taskQueue(); + clock = useFakeTimers(); + }); - afterEach(() => { - clock.restore(); - clock = undefined as any; - queue = undefined as any; - }); + afterEach(() => { + clock.restore(); + clock = undefined as any; + queue = undefined as any; + }); - it('waits on empty queue', async () => { - await queue.wait(); - }); + it('waits on empty queue', async () => { + await queue.wait(); + }); - it('executes given action', async () => { - const action = stub(); + it('executes given action', async () => { + const action = stub(); - queue.push(action); - await queue.wait(); + queue.push(action); + await queue.wait(); - assert.calledOnce(action); - }); + assert.calledOnce(action); + }); - it('executes two actions', async () => { - const action1 = stub(); - const action2 = stub(); + it('executes two actions', async () => { + const action1 = stub(); + const action2 = stub(); - queue.push(action1); - queue.push(action2); - await queue.wait(); + queue.push(action1); + queue.push(action2); + await queue.wait(); - assert.calledOnce(action1); - assert.calledOnce(action2); - }); + assert.calledOnce(action1); + assert.calledOnce(action2); + }); - it('executes actions in order', async () => { - const order = [0]; - const action1 = stub().callsFake(() => order.push(1)); - const action2 = stub().callsFake(() => order.push(2)); + it('executes actions in order', async () => { + const order = [0]; + const action1 = stub().callsFake(() => order.push(1)); + const action2 = stub().callsFake(() => order.push(2)); - queue.push(action1); - queue.push(action2); - await queue.wait(); + queue.push(action1); + queue.push(action2); + await queue.wait(); - expect(order).eql([0, 1, 2]); - }); + expect(order).eql([0, 1, 2]); + }); - it('executes two actions with wait', async () => { - const action1 = stub(); - const action2 = stub(); + it('executes two actions with wait', async () => { + const action1 = stub(); + const action2 = stub(); - queue.push(action1); - await queue.wait(); - queue.push(action2); - await queue.wait(); + queue.push(action1); + await queue.wait(); + queue.push(action2); + await queue.wait(); - assert.calledOnce(action1); - assert.calledOnce(action2); - }); + assert.calledOnce(action1); + assert.calledOnce(action2); + }); - it('returns promise that resolves after passed action', async () => { - const action = stub(); + it('returns promise that resolves after passed action', async () => { + const action = stub(); - const promise = queue.push(action); - assert.notCalled(action); - await promise.then(() => assert.calledOnce(action)); - }); + const promise = queue.push(action); + assert.notCalled(action); + await promise.then(() => assert.calledOnce(action)); + }); - it('returns promise that resolves to value returned from action', async () => { - const action = stub().returns('foo'); + it('returns promise that resolves to value returned from action', async () => { + const action = stub().returns('foo'); - await expect(queue.push(action)).eventually.equal('foo'); - }); + await expect(queue.push(action)).eventually.equal('foo'); + }); - it('returns promise that resolves to value resolved from action', async () => { - const action = stub().resolves('foo'); + it('returns promise that resolves to value resolved from action', async () => { + const action = stub().resolves('foo'); - await expect(queue.push(action)).eventually.equal('foo'); - }); + await expect(queue.push(action)).eventually.equal('foo'); + }); - it('returns promise that rejects to error thrown from action', async () => { - const action = stub().throws(new Error('test')); + it('returns promise that rejects to error thrown from action', async () => { + const action = stub().throws(new Error('test')); - await expect(queue.push(action)).rejectedWith('test'); - }); + await expect(queue.push(action)).rejectedWith('test'); + }); - it('returns promise that rejects to error rejected from action', async () => { - const action = stub().rejects(new Error('test')); + it('returns promise that rejects to error rejected from action', async () => { + const action = stub().rejects(new Error('test')); - await expect(queue.push(action)).rejectedWith('test'); - }); + await expect(queue.push(action)).rejectedWith('test'); + }); - it('returns promise that rejects to error rejected from action', async () => { - const action = stub().rejects(new Error('test')); + it('returns promise that rejects to error rejected from action', async () => { + const action = stub().rejects(new Error('test')); - await expect(queue.push(action)).rejectedWith('test'); - }); + await expect(queue.push(action)).rejectedWith('test'); + }); - describe('makeQueued()', () => { - it('creates queued function', async () => { - const action = stub(); - const queued = makeQueued(action); + describe('makeQueued()', () => { + it('creates queued function', async () => { + const action = stub(); + const queued = makeQueued(action); - await queued(); + await queued(); - assert.calledOnce(action); - }); - }); + assert.calledOnce(action); + }); + }); }); diff --git a/src/ts/tests/server/world.spec.ts b/src/ts/tests/server/world.spec.ts index d26294c..f6fe336 100644 --- a/src/ts/tests/server/world.spec.ts +++ b/src/ts/tests/server/world.spec.ts @@ -15,333 +15,333 @@ import { FriendsService } from '../../server/services/friends'; import { HidingService } from '../../server/services/hiding'; describe('World', () => { - const partyService = stubClass(PartyService); - const friendsService = stubClass(FriendsService); - const hidingService = stubClass(HidingService); - partyService.partyChanged = new Subject(); - const notifications = stubClass(NotificationService); - let liveSettings: ServerLiveSettings; - let getSettings: SinonStub; - let world: World; - let map: ServerMap; - let client: IClient; - - beforeEach(() => { - resetStubMethods(partyService, 'leave'); - resetStubMethods(notifications, 'rejectAll'); - liveSettings = {} as any; - getSettings = stub().returns({}); - client = mockClient(); - world = new World( - { flags: {} } as any, partyService as any, friendsService as any, hidingService as any, - notifications, getSettings, liveSettings, { stats() { return {}; } } as any); - world.maps.push(map = createServerMap('', 0, 1, 1)); - client.map = world.getMainMap(); - }); - - describe('joinClientToWorld()', () => { - let client: IClient; - - beforeEach(() => { - client = mockClient(); - client.accountId = 'foobar'; - client.map = world.getMainMap(); - client.characterState = {} as any; - }); - - it('sends world state', () => { - const worldState = stub(client, 'worldState'); - const state: WorldState = {} as any; - stub(world, 'getState').returns(state); - - world.joinClientToWorld(client); - - assert.calledWith(worldState, state, true); - }); - - it('sends map info', () => { - const mapState = stub(client, 'mapState'); - const state: MapState = {} as any; - map.state = state; - - world.joinClientToWorld(client); - - assert.calledWith(mapState, { type: 0, editableArea: undefined, flags: 0, defaultTile: 0, regionsX: 1, regionsY: 1 }, state); - }); - - it('adds client to clients list', () => { - world.joinClientToWorld(client); - - expect(world.clients).contain(client); - }); - - it('initializes client pony ID', () => { - stub(world, 'getNewEntityId').returns(1234); - - world.joinClientToWorld(client); - - expect(client.pony.id).equal(1234); - }); - - it('sends client pony ID', () => { - stub(world, 'getNewEntityId').returns(1234); - const myEntity = stub(client, 'myEntity'); - client.characterName = 'charname'; - client.character.info = client.pony.info = 'INFO'; - client.pony.crc = 456; - - world.joinClientToWorld(client); - - assert.calledWith(myEntity, 1234, 'charname', 'INFO', client.characterId, 456); - }); - - it('adds client pony to the world', () => { - const addEntity = stub(world, 'addEntity'); - - world.joinClientToWorld(client); - - assert.calledWith(addEntity, client.pony, client.map); - }); - - it('updates selection for other clients', () => { - const otherClient = mockClient(); - otherClient.selected = { id: 123, client: { accountId: 'foobar' } } as any; - const otherClient2 = mockClient(); - const updateSelection = stub(otherClient, 'updateSelection'); - const updateSelection2 = stub(otherClient2, 'updateSelection'); - stub(world, 'getNewEntityId').returns(321); - world.clients.push(otherClient); - world.clients.push(otherClient2); - - world.joinClientToWorld(client); - - assert.calledWith(updateSelection, 123, 321); - assert.notCalled(updateSelection2); - }); + const partyService = stubClass(PartyService); + const friendsService = stubClass(FriendsService); + const hidingService = stubClass(HidingService); + partyService.partyChanged = new Subject(); + const notifications = stubClass(NotificationService); + let liveSettings: ServerLiveSettings; + let getSettings: SinonStub; + let world: World; + let map: ServerMap; + let client: IClient; + + beforeEach(() => { + resetStubMethods(partyService, 'leave'); + resetStubMethods(notifications, 'rejectAll'); + liveSettings = {} as any; + getSettings = stub().returns({}); + client = mockClient(); + world = new World( + { flags: {} } as any, partyService as any, friendsService as any, hidingService as any, + notifications, getSettings, liveSettings, { stats() { return {}; } } as any); + world.maps.push(map = createServerMap('', 0, 1, 1)); + client.map = world.getMainMap(); + }); + + describe('joinClientToWorld()', () => { + let client: IClient; + + beforeEach(() => { + client = mockClient(); + client.accountId = 'foobar'; + client.map = world.getMainMap(); + client.characterState = {} as any; + }); + + it('sends world state', () => { + const worldState = stub(client, 'worldState'); + const state: WorldState = {} as any; + stub(world, 'getState').returns(state); + + world.joinClientToWorld(client); + + assert.calledWith(worldState, state, true); + }); + + it('sends map info', () => { + const mapState = stub(client, 'mapState'); + const state: MapState = {} as any; + map.state = state; + + world.joinClientToWorld(client); + + assert.calledWith(mapState, { type: 0, editableArea: undefined, flags: 0, defaultTile: 0, regionsX: 1, regionsY: 1 }, state); + }); + + it('adds client to clients list', () => { + world.joinClientToWorld(client); + + expect(world.clients).contain(client); + }); + + it('initializes client pony ID', () => { + stub(world, 'getNewEntityId').returns(1234); + + world.joinClientToWorld(client); + + expect(client.pony.id).equal(1234); + }); + + it('sends client pony ID', () => { + stub(world, 'getNewEntityId').returns(1234); + const myEntity = stub(client, 'myEntity'); + client.characterName = 'charname'; + client.character.info = client.pony.info = 'INFO'; + client.pony.crc = 456; + + world.joinClientToWorld(client); + + assert.calledWith(myEntity, 1234, 'charname', 'INFO', client.characterId, 456); + }); + + it('adds client pony to the world', () => { + const addEntity = stub(world, 'addEntity'); + + world.joinClientToWorld(client); + + assert.calledWith(addEntity, client.pony, client.map); + }); + + it('updates selection for other clients', () => { + const otherClient = mockClient(); + otherClient.selected = { id: 123, client: { accountId: 'foobar' } } as any; + const otherClient2 = mockClient(); + const updateSelection = stub(otherClient, 'updateSelection'); + const updateSelection2 = stub(otherClient2, 'updateSelection'); + stub(world, 'getNewEntityId').returns(321); + world.clients.push(otherClient); + world.clients.push(otherClient2); + + world.joinClientToWorld(client); + + assert.calledWith(updateSelection, 123, 321); + assert.notCalled(updateSelection2); + }); - it('adds update notification to client if updating', () => { - liveSettings.updating = true; + it('adds update notification to client if updating', () => { + liveSettings.updating = true; - world.joinClientToWorld(client); + world.joinClientToWorld(client); - assert.calledWith(notifications.addNotification, client, { - id: 0, name: '', message: 'Server will restart shortly for updates and maintenance', flags: NotificationFlags.Ok - }); - }); - }); + assert.calledWith(notifications.addNotification, client, { + id: 0, name: '', message: 'Server will restart shortly for updates and maintenance', flags: NotificationFlags.Ok + }); + }); + }); - describe('initialize()', () => { - it('initializes all controllers', () => { - const initialize = stub(); - world.controllers.push({ initialize, update() { } }); + describe('initialize()', () => { + it('initializes all controllers', () => { + const initialize = stub(); + world.controllers.push({ initialize, update() { } }); - world.initialize(123); + world.initialize(123); - assert.calledWith(initialize, 0.123); - }); - }); + assert.calledWith(initialize, 0.123); + }); + }); - describe('update()', () => { - it('updates entities positions', () => { - const e = serverEntity(1, 1, 1, 0); - e.flags |= EntityFlags.Movable; - e.vx = 1; - e.vy = 1; - e.timestamp = 1; - addEntityToRegion(map.regions[0], e, map); + describe('update()', () => { + it('updates entities positions', () => { + const e = serverEntity(1, 1, 1, 0); + e.flags |= EntityFlags.Movable; + e.vx = 1; + e.vy = 1; + e.timestamp = 1; + addEntityToRegion(map.regions[0], e, map); - world.update(1000, 2000); + world.update(1000, 2000); - expect(e.x).equal(2); - expect(e.y).equal(2); - }); + expect(e.x).equal(2); + expect(e.y).equal(2); + }); - it('does not update entity position if timestamp is in the future', () => { - const e = serverEntity(1, 1, 1, 0); - e.vx = 1; - e.vy = 1; - addEntityToRegion(map.regions[0], e, map); + it('does not update entity position if timestamp is in the future', () => { + const e = serverEntity(1, 1, 1, 0); + e.vx = 1; + e.vy = 1; + addEntityToRegion(map.regions[0], e, map); - world.update(1000, 2000); + world.update(1000, 2000); - expect(e.x).equal(1); - expect(e.y).equal(1); - }); + expect(e.x).equal(1); + expect(e.y).equal(1); + }); - // it('updates regions', () => { - // world.update(123, 123); + // it('updates regions', () => { + // world.update(123, 123); - // ... - // }); + // ... + // }); - it('updates all controllers', () => { - const update = stub(); - world.controllers.push({ update, initialize() { } }); + it('updates all controllers', () => { + const update = stub(); + world.controllers.push({ update, initialize() { } }); - world.update(123, 123000); + world.update(123, 123000); - assert.calledWith(update, 0.123); - }); + assert.calledWith(update, 0.123); + }); - // timeouts expressions + // timeouts expressions - it('commits region updates', () => { - const region = createServerRegion(0, 0); - const entity = serverEntity(1); - const client = mockClient(); - pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position }); - region.clients.push(client); - client.regions.push(region); - map.regions = [region]; - const update = stub(client, 'update'); + it('commits region updates', () => { + const region = createServerRegion(0, 0); + const entity = serverEntity(1); + const client = mockClient(); + pushUpdateEntityToRegion(region, { entity, flags: UpdateFlags.Position }); + region.clients.push(client); + client.regions.push(region); + map.regions = [region]; + const update = stub(client, 'update'); - world.update(123, 123000); + world.update(123, 123000); - expect(update); - }); + expect(update); + }); - it('joins queued clients', () => { - const client = mockClient(); - world.joinClientToQueue(client); - const joinClientToWorld = stub(world, 'joinClientToWorld'); + it('joins queued clients', () => { + const client = mockClient(); + world.joinClientToQueue(client); + const joinClientToWorld = stub(world, 'joinClientToWorld'); - world.update(123, 123000); + world.update(123, 123000); - assert.calledWith(joinClientToWorld, client); - }); - }); + assert.calledWith(joinClientToWorld, client); + }); + }); - describe('kick()', () => { - let clock: SinonFakeTimers; + describe('kick()', () => { + let clock: SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers(); - }); + beforeEach(() => { + clock = useFakeTimers(); + }); - afterEach(() => { - clock.tick(99999); - clock.restore(); - }); + afterEach(() => { + clock.tick(99999); + clock.restore(); + }); - it('does nothing for undefined', () => { - expect(world.kick(undefined)).false; - }); + it('does nothing for undefined', () => { + expect(world.kick(undefined)).false; + }); - it('returns true if kicked', () => { - const client = mockClient(); + it('returns true if kicked', () => { + const client = mockClient(); - expect(world.kick(client)).true; - }); + expect(world.kick(client)).true; + }); - it('rejects all notifications', () => { - const client = mockClient(); + it('rejects all notifications', () => { + const client = mockClient(); - world.kick(client); + world.kick(client); - assert.calledWith(notifications.rejectAll, client); - }); + assert.calledWith(notifications.rejectAll, client); + }); - it('notifies client of leaving', () => { - const client = mockClient(); - const left = stub(client, 'left'); + it('notifies client of leaving', () => { + const client = mockClient(); + const left = stub(client, 'left'); - world.kick(client); + world.kick(client); - assert.calledOnce(left); - }); + assert.calledOnce(left); + }); - it('sets client leave reason', () => { - const client = mockClient(); + it('sets client leave reason', () => { + const client = mockClient(); - world.kick(client); + world.kick(client); - expect(client.leaveReason).equal('kicked'); - }); + expect(client.leaveReason).equal('kicked'); + }); - it('disconnects client after timeout', () => { - const client = mockClient({ isConnected: true }); - const disconnect = stub(client, 'disconnect'); + it('disconnects client after timeout', () => { + const client = mockClient({ isConnected: true }); + const disconnect = stub(client, 'disconnect'); - world.kick(client); + world.kick(client); - clock.tick(201); - assert.calledOnce(disconnect); - }); + clock.tick(201); + assert.calledOnce(disconnect); + }); - it('skips disconnecting if client already disconnected', () => { - const client = mockClient({ isConnected: false }); - const disconnect = stub(client, 'disconnect'); + it('skips disconnecting if client already disconnected', () => { + const client = mockClient({ isConnected: false }); + const disconnect = stub(client, 'disconnect'); - world.kick(client); + world.kick(client); - clock.tick(201); - assert.notCalled(disconnect); - }); - }); + clock.tick(201); + assert.notCalled(disconnect); + }); + }); - describe('kickAll()', () => { - it('does nothing for no clients', () => { - world.kickAll(); - }); + describe('kickAll()', () => { + it('does nothing for no clients', () => { + world.kickAll(); + }); - it('kicks all clients', () => { - const a = mockClient(); - const b = mockClient(); - world.clients.push(a); - world.clients.push(b); - const kick = stub(world, 'kick'); + it('kicks all clients', () => { + const a = mockClient(); + const b = mockClient(); + world.clients.push(a); + world.clients.push(b); + const kick = stub(world, 'kick'); - world.kickAll(); + world.kickAll(); - assert.calledWith(kick, a); - assert.calledWith(kick, b); - }); + assert.calledWith(kick, a); + assert.calledWith(kick, b); + }); - it('works while removing clients', () => { - const a = mockClient(); - const b = mockClient(); - world.clients.push(a); - world.clients.push(b); - const aLeft = stub(a, 'left'); - const bLeft = stub(b, 'left'); + it('works while removing clients', () => { + const a = mockClient(); + const b = mockClient(); + world.clients.push(a); + world.clients.push(b); + const aLeft = stub(a, 'left'); + const bLeft = stub(b, 'left'); - world.kickAll(); + world.kickAll(); - assert.calledOnce(aLeft); - assert.calledOnce(bLeft); - }); - }); + assert.calledOnce(aLeft); + assert.calledOnce(bLeft); + }); + }); - describe('kickByAccount()', () => { - it('does nothing if not found', () => { - world.kickByAccount('foo'); - }); + describe('kickByAccount()', () => { + it('does nothing if not found', () => { + world.kickByAccount('foo'); + }); - it('kick client by account', () => { - const client = mockClient(); - world.clients.push(client); - world.clientsByAccount.set(client.accountId, client); - const kick = stub(world, 'kick'); + it('kick client by account', () => { + const client = mockClient(); + world.clients.push(client); + world.clientsByAccount.set(client.accountId, client); + const kick = stub(world, 'kick'); - world.kickByAccount(client.accountId); + world.kickByAccount(client.accountId); - assert.calledWith(kick, client); - }); - }); + assert.calledWith(kick, client); + }); + }); - describe('kickByCharacter()', () => { - it('does nothing if not found', () => { - world.kickByCharacter('foo'); - }); + describe('kickByCharacter()', () => { + it('does nothing if not found', () => { + world.kickByCharacter('foo'); + }); - it('kick client by account', () => { - const client = mockClient(); - world.clients.push(client); - const kick = stub(world, 'kick'); + it('kick client by account', () => { + const client = mockClient(); + world.clients.push(client); + const kick = stub(world, 'kick'); - world.kickByCharacter(client.characterId); + world.kickByCharacter(client.characterId); - assert.calledWith(kick, client); - }); - }); + assert.calledWith(kick, client); + }); + }); }); diff --git a/src/ts/tests/services/errorReporter.spec.ts b/src/ts/tests/services/errorReporter.spec.ts index 78f390d..754e5ec 100644 --- a/src/ts/tests/services/errorReporter.spec.ts +++ b/src/ts/tests/services/errorReporter.spec.ts @@ -5,60 +5,60 @@ import { ErrorReporter } from '../../components/services/errorReporter'; import { times } from '../../common/utils'; describe('ErrorReporter', () => { - describe('createClientErrorHandler()', () => { - let socketOptions: ClientOptions; - let errorReporter: ErrorReporter; - let clientErrorHandler: ClientErrorHandler; - let reportError: SinonStub; + describe('createClientErrorHandler()', () => { + let socketOptions: ClientOptions; + let errorReporter: ErrorReporter; + let clientErrorHandler: ClientErrorHandler; + let reportError: SinonStub; - beforeEach(() => { - socketOptions = {} as any; - errorReporter = new ErrorReporter(); - clientErrorHandler = errorReporter.createClientErrorHandler(socketOptions); - reportError = stub(errorReporter, 'reportError'); - }); + beforeEach(() => { + socketOptions = {} as any; + errorReporter = new ErrorReporter(); + clientErrorHandler = errorReporter.createClientErrorHandler(socketOptions); + reportError = stub(errorReporter, 'reportError'); + }); - it('reports error without data', () => { - const error = new Error('foo'); + it('reports error without data', () => { + const error = new Error('foo'); - clientErrorHandler.handleRecvError(error, undefined as any); + clientErrorHandler.handleRecvError(error, undefined as any); - assert.calledWithMatch(reportError, error, { data: undefined, method: undefined }); - }); + assert.calledWithMatch(reportError, error, { data: undefined, method: undefined }); + }); - it('sends stringified data and method from socket options', () => { - const error = new Error('foo'); - socketOptions.client = ['bar']; + it('sends stringified data and method from socket options', () => { + const error = new Error('foo'); + socketOptions.client = ['bar']; - clientErrorHandler.handleRecvError(error, new Uint8Array([0, 1, 2])); + clientErrorHandler.handleRecvError(error, new Uint8Array([0, 1, 2])); - assert.calledWithMatch(reportError, error, { data: '<0,1,2>', method: 'bar' }); - }); + assert.calledWithMatch(reportError, error, { data: '<0,1,2>', method: 'bar' }); + }); - it('handles 0 length buffer', () => { - const error = new Error('foo'); - socketOptions.client = ['bar']; + it('handles 0 length buffer', () => { + const error = new Error('foo'); + socketOptions.client = ['bar']; - clientErrorHandler.handleRecvError(error, new Uint8Array(0)); + clientErrorHandler.handleRecvError(error, new Uint8Array(0)); - assert.calledWithMatch(reportError, error, { data: '<>', method: undefined }); - }); + assert.calledWithMatch(reportError, error, { data: '<>', method: undefined }); + }); - it('trims buffer value after 200 elements', () => { - const error = new Error('foo'); - socketOptions.client = [['bar', {}]]; + it('trims buffer value after 200 elements', () => { + const error = new Error('foo'); + socketOptions.client = [['bar', {}]]; - clientErrorHandler.handleRecvError(error, new Uint8Array(300)); + clientErrorHandler.handleRecvError(error, new Uint8Array(300)); - assert.calledWithMatch(reportError, error, { data: `<${times(200, () => '0').join(',')}...>`, method: 'bar' }); - }); + assert.calledWithMatch(reportError, error, { data: `<${times(200, () => '0').join(',')}...>`, method: 'bar' }); + }); - it('does nothing if error has no message', () => { - const error = new Error(''); + it('does nothing if error has no message', () => { + const error = new Error(''); - clientErrorHandler.handleRecvError(error, undefined as any); + clientErrorHandler.handleRecvError(error, undefined as any); - assert.notCalled(reportError); - }); - }); + assert.notCalled(reportError); + }); + }); }); diff --git a/src/ts/tests/services/gameService.spec.ts b/src/ts/tests/services/gameService.spec.ts index b2132fd..b738264 100644 --- a/src/ts/tests/services/gameService.spec.ts +++ b/src/ts/tests/services/gameService.spec.ts @@ -8,62 +8,62 @@ import { ErrorReporter } from '../../components/services/errorReporter'; import { StorageService } from '../../components/services/storageService'; describe('GameService', () => { - let model = stubClass(Model); - let game = stubClass(PonyTownGame); - let errorHandler = stubClass(ErrorHandler); - let errorReporter = stubClass(ErrorReporter); - let storage = stubClass(StorageService); - let window = stubFromInstance({ addEventListener() { } }); - let gameService: GameService; - // let connectSocket: SinonStub; - // let startLoop: SinonStub; + let model = stubClass(Model); + let game = stubClass(PonyTownGame); + let errorHandler = stubClass(ErrorHandler); + let errorReporter = stubClass(ErrorReporter); + let storage = stubClass(StorageService); + let window = stubFromInstance({ addEventListener() { } }); + let gameService: GameService; + // let connectSocket: SinonStub; + // let startLoop: SinonStub; - beforeEach(() => { - resetStubMethods(model); - resetStubMethods(game, 'leave'); - resetStubMethods(errorHandler); - resetStubMethods(errorReporter); - resetStubMethods(window); - resetStubMethods(storage); - const zone = { - run: (f: () => void) => f(), - runOutsideAngular: (f: () => void) => f(), - }; - gameService = new GameService( - model as any, game as any, zone as any, errorHandler as any, errorReporter as any, storage as any); + beforeEach(() => { + resetStubMethods(model); + resetStubMethods(game, 'leave'); + resetStubMethods(errorHandler); + resetStubMethods(errorReporter); + resetStubMethods(window); + resetStubMethods(storage); + const zone = { + run: (f: () => void) => f(), + runOutsideAngular: (f: () => void) => f(), + }; + gameService = new GameService( + model as any, game as any, zone as any, errorHandler as any, errorReporter as any, storage as any); - (global as any).WebSocket = {}; - }); + (global as any).WebSocket = {}; + }); - afterEach(() => { - delete (global as any).WebSocket; - }); + afterEach(() => { + delete (global as any).WebSocket; + }); - // describe('join()', () => { - // it('joins to the game', async () => { - // model.join.resolves({ token: 'token' }); - // startLoop.returns({}); - // gameService.server = { id: 'serverid' } as any; + // describe('join()', () => { + // it('joins to the game', async () => { + // model.join.resolves({ token: 'token' }); + // startLoop.returns({}); + // gameService.server = { id: 'serverid' } as any; - // await gameService.join('ponyid'); + // await gameService.join('ponyid'); - // assert.calledWith(model.join, 'serverid', 'ponyid'); - // }); - // }); + // assert.calledWith(model.join, 'serverid', 'ponyid'); + // }); + // }); - describe('leave()', () => { - // it('notifies game of leaving', () => { - // gameService.leave('test'); + describe('leave()', () => { + // it('notifies game of leaving', () => { + // gameService.leave('test'); - // assert.calledOnce(game.leave); - // }); + // assert.calledOnce(game.leave); + // }); - it('calls left()', () => { - const left = stub(gameService, 'left'); + it('calls left()', () => { + const left = stub(gameService, 'left'); - gameService.leave('test'); + gameService.leave('test'); - assert.calledOnce(left); - }); - }); + assert.calledOnce(left); + }); + }); }); diff --git a/src/ts/tests/services/liveCollection.spec.ts b/src/ts/tests/services/liveCollection.spec.ts index 09d7ddf..3385ddd 100644 --- a/src/ts/tests/services/liveCollection.spec.ts +++ b/src/ts/tests/services/liveCollection.spec.ts @@ -7,456 +7,456 @@ import { ClientAdminActions } from '../../client/clientAdminActions'; import { IAdminServerActions, LiveResponse } from '../../common/adminInterfaces'; interface Thing { - _id: string; - name: string; - updatedAt: Date; - deleted?: boolean; + _id: string; + name: string; + updatedAt: Date; + deleted?: boolean; } describe('LiveCollection', () => { - let socket: SocketService; - let collection: LiveCollection; - let clock: SinonFakeTimers; - let options: Options; - let logError: SinonStub; - - beforeEach(() => { - socket = { - server: { - getAll() { }, - removeItem() { }, - assignAccount() { }, - }, - } as any; - - options = { - decode: x => ({ _id: x[0], name: x[1], updatedAt: new Date(x[2]) }), - }; - - logError = stub(); - collection = new LiveCollection('events', 2000, i => i._id, options, socket, '2010-01-01T10:00:00.000Z', logError); - clock = useFakeTimers(); - }); - - afterEach(() => { - clock.restore(); - }); - - after(() => { - socket = undefined as any; - collection = undefined as any; - clock = undefined as any; - options = undefined as any; - logError = undefined as any; - }); - - describe('.push()', () => { - it('adds item to collection', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - - collection.push(item); - - expect(collection.items).contain(item); - expect(collection.get('foo')).equal(item); - }); - }); - - describe('.remove()', () => { - it('sends remove request to server', () => { - const removeItem = stub(socket.server, 'removeItem').returns(Promise.resolve()); - - return collection.remove('foo') - .then(() => assert.calledWith(removeItem, 'events', 'foo')); - }); - - it('removes item from list', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - stub(socket.server, 'removeItem').returns(Promise.resolve()); - options.deleteItems = true; - collection.push(item); - - return collection.remove('foo') - .then(() => { - expect(collection.items).not.contain(item); - expect(collection.get('foo')).undefined; - }); - }); - }); - - describe('.removeItem()', () => { - it('does not send remove request to server', () => { - const removeItem = stub(socket.server, 'removeItem').returns(Promise.resolve()); - - collection.removeItem('foo'); - - assert.notCalled(removeItem); - }); - - it('removes item from list', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - options.deleteItems = true; - collection.push(item); - - collection.removeItem('foo'); - - expect(collection.items).not.contain(item); - expect(collection.get('foo')).undefined; - }); - - it('does not remove item is deleteItems flag is set to false', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - options.deleteItems = false; - collection.push(item); - - collection.removeItem('foo', true); - - expect(collection.items).contain(item); - expect(item.deleted).true; - }); - - it('does nothing deleteItems flag is set to false and deleted flag is false', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - options.deleteItems = false; - collection.push(item); - - collection.removeItem('foo'); - - expect(collection.items).contain(item); - expect(item.deleted).not.true; - }); - }); - - describe('.assignAccount()', () => { - it('send assign account request to server', () => { - const assignAccount = stub(socket.server, 'assignAccount').returns(Promise.resolve()); - - return collection.assignAccount('foo', 'bar') - .then(() => assert.calledWith(assignAccount, 'events', 'foo', 'bar')); - }); - }); - - describe('.live()', () => { - it('returns promise if running', () => { - return collection.live(); - }); - - it('schedules next update', () => { - return collection.live() - .then(() => { - const live = stub(collection, 'live'); - clock.tick(2100); - assert.calledOnce(live); - }); - }); - - it('schedules next update fast if more flag is true', () => { - stub(socket.server, 'getAll').resolves({} as any); - stub(collection, 'read').returns(true); - socket.isConnected = true; - - return collection.live() - .then(() => { - const live = stub(collection, 'live'); - clock.tick(200); - assert.calledOnce(live); - }); - }); - - it('polls server for updates if connected', () => { - const data = {}; - stub(socket.server, 'getAll').withArgs('events', '2010-01-01T10:00:00.000Z').resolves(data as any); - const read = stub(collection, 'read'); - socket.isConnected = true; - - return collection.live() - .then(() => assert.calledWith(read, data as any)); - }); - - it('does not poll the server if not connected', () => { - const getAll = stub(socket.server, 'getAll'); - stub(collection, 'read'); - socket.isConnected = false; - - return collection.live() - .then(() => assert.notCalled(getAll)); - }); - - it('does not poll the server if stopped', async () => { - const getAll = stub(socket.server, 'getAll'); - stub(collection, 'read'); - socket.isConnected = true; - - collection.stop(); - - await collection.live(); - assert.notCalled(getAll); - }); - - it('does not reject for server error', async () => { - stub(socket.server, 'getAll').rejects(new Error('test')); - socket.isConnected = true; - - await collection.live(); - }); - }); - - describe('.read()', () => { - const base: LiveResponse = { - updates: [], - deletes: [], - base: {}, - more: false, - }; - - it('returns more flag', () => { - expect(collection.read({ ...base, more: false })).false; - expect(collection.read({ ...base, more: true })).true; - }); - - it('updates timestamp', async () => { - const getAll = stub(socket.server, 'getAll').resolves({} as any); - socket.isConnected = true; - - collection.read({ - ...base, - updates: [ - ['bar', 'Bar', '2010-01-03T10:00:00.000Z'], - ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], - ], - }); - - stub(collection, 'read'); - await collection.live(); - assert.calledWith(getAll, 'events', '2010-01-03T10:00:00.000Z'); - }); - - it('does not update timestamp if not live fetch', async () => { - const getAll = stub(socket.server, 'getAll').resolves({} as any); - socket.isConnected = true; - - collection.read({ - ...base, - updates: [ - ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], - ], - }, false); - - stub(collection, 'read'); - await collection.live(); - assert.calledWith(getAll, 'events', '2010-01-01T10:00:00.000Z'); - }); - - it('adds new items', () => { - collection.read({ - ...base, - updates: [ - ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], - ], - }); - - expect(collection.items[0]).eql({ - _id: 'foo', - name: 'Foo', - updatedAt: new Date('2010-01-02T10:00:00.000Z'), - }); - }); - - it('does not add new items if ignored', () => { - options.ignore = () => true; - - collection.read({ - ...base, - updates: [ - ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], - ], - }); - - expect(collection.items).empty; - }); - - it('adds items anyway if ignored but not live fetch', () => { - options.ignore = () => true; - - collection.read({ - ...base, - updates: [ - ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], - ], - }, false); - - expect(collection.items[0]).eql({ - _id: 'foo', - name: 'Foo', - updatedAt: new Date('2010-01-02T10:00:00.000Z'), - }); - }); - - it('updates existing items', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - collection.push(item); - - collection.read({ - ...base, - updates: [ - ['foo', 'Bar', '2010-01-03T10:00:00.000Z'], - ], - }); - - expect(item).eql({ - _id: 'foo', - name: 'Bar', - updatedAt: new Date('2010-01-03T10:00:00.000Z'), - }); - }); - - it('removes deleted items', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - options.deleteItems = true; - collection.push(item); - - collection.read({ - ...base, - deletes: ['foo'], - }); - - expect(collection.items).not.contain(item); - }); - - it('sets deleted flag on items if deleteItems is false', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - options.deleteItems = false; - collection.push(item); - - collection.read({ - ...base, - deletes: ['foo'], - }); - - expect(collection.items).contain(item); - expect(item.deleted).true; - }); - - it('calls beforeUpdate hook with items', () => { - const beforeUpdate = stub(); - options.beforeUpdate = beforeUpdate; - - collection.read({ - ...base, - updates: [ - ['foo', 'Foo', '2010-01-03T10:00:00.000Z'], - ], - }); - - assert.calledWithMatch(beforeUpdate, [ - { _id: 'foo', name: 'Foo', updatedAt: new Date('2010-01-03T10:00:00.000Z') }, - ]); - }); - - it('calls onUpdated hook with added and all items', () => { - const onUpdated = stub(); - options.onUpdated = onUpdated; - collection.push({ _id: 'foo', name: 'Foo', updatedAt: new Date() }); - - collection.read({ - ...base, - updates: [ - ['bar', 'Bar', '2010-01-04T10:00:00.000Z'], - ['foo', 'Foo2', '2010-01-03T10:00:00.000Z'], - ], - }); - - expect(onUpdated.args[0][0]).eql([ - { _id: 'bar', name: 'Bar', updatedAt: new Date('2010-01-04T10:00:00.000Z') }, - ], 'added'); - - expect(onUpdated.args[0][1]).eql([ - { _id: 'bar', name: 'Bar', updatedAt: new Date('2010-01-04T10:00:00.000Z') }, - { _id: 'foo', name: 'Foo2', updatedAt: new Date('2010-01-03T10:00:00.000Z') }, - ], 'all'); - }); - - it('does not call onUpdated hook for no items', () => { - const onUpdated = stub(); - options.onUpdated = onUpdated; - - collection.read({ - ...base, - updates: [], - }); - - assert.notCalled(onUpdated); - }); - - it('calls onDelete hook for deleted items', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - const onDelete = stub(); - options.onDelete = onDelete; - collection.push(item); - - collection.read({ - ...base, - deletes: ['foo'], - }); - - assert.calledWith(onDelete, item); - }); - - it('calls onFinished hook when more flag is false', () => { - const onFinished = stub(); - options.onFinished = onFinished; - - collection.read({ ...base, more: false }); - - assert.calledOnce(onFinished); - }); - - it('calls onFinished hook only once', () => { - const onFinished = stub(); - options.onFinished = onFinished; - - collection.read({ ...base, more: false }); - collection.read({ ...base, more: false }); - - assert.calledOnce(onFinished); - }); - - it('does not call onFinished hook for non live fetch calls', () => { - const onFinished = stub(); - options.onFinished = onFinished; - - collection.read({ ...base, more: false }, false); - - assert.notCalled(onFinished); - }); - - it('calls onUpdate hook for updated items', () => { - const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; - const onUpdate = stub(); - options.onUpdate = onUpdate; - collection.push(item); - - collection.read({ - ...base, - updates: [ - ['foo', 'Foo2', '2010-01-03T10:00:00.000Z'], - ], - }); - - assert.calledWith(onUpdate, item); - }); - - it('does not call onUpdate hook for added items', () => { - const onUpdate = stub(); - options.onUpdate = onUpdate; - - collection.read({ - ...base, - updates: [ - ['foo', 'Foo2', '2010-01-03T10:00:00.000Z'], - ], - }); - - assert.notCalled(onUpdate); - }); - }); + let socket: SocketService; + let collection: LiveCollection; + let clock: SinonFakeTimers; + let options: Options; + let logError: SinonStub; + + beforeEach(() => { + socket = { + server: { + getAll() { }, + removeItem() { }, + assignAccount() { }, + }, + } as any; + + options = { + decode: x => ({ _id: x[0], name: x[1], updatedAt: new Date(x[2]) }), + }; + + logError = stub(); + collection = new LiveCollection('events', 2000, i => i._id, options, socket, '2010-01-01T10:00:00.000Z', logError); + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + after(() => { + socket = undefined as any; + collection = undefined as any; + clock = undefined as any; + options = undefined as any; + logError = undefined as any; + }); + + describe('.push()', () => { + it('adds item to collection', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + + collection.push(item); + + expect(collection.items).contain(item); + expect(collection.get('foo')).equal(item); + }); + }); + + describe('.remove()', () => { + it('sends remove request to server', () => { + const removeItem = stub(socket.server, 'removeItem').returns(Promise.resolve()); + + return collection.remove('foo') + .then(() => assert.calledWith(removeItem, 'events', 'foo')); + }); + + it('removes item from list', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + stub(socket.server, 'removeItem').returns(Promise.resolve()); + options.deleteItems = true; + collection.push(item); + + return collection.remove('foo') + .then(() => { + expect(collection.items).not.contain(item); + expect(collection.get('foo')).undefined; + }); + }); + }); + + describe('.removeItem()', () => { + it('does not send remove request to server', () => { + const removeItem = stub(socket.server, 'removeItem').returns(Promise.resolve()); + + collection.removeItem('foo'); + + assert.notCalled(removeItem); + }); + + it('removes item from list', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = true; + collection.push(item); + + collection.removeItem('foo'); + + expect(collection.items).not.contain(item); + expect(collection.get('foo')).undefined; + }); + + it('does not remove item is deleteItems flag is set to false', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = false; + collection.push(item); + + collection.removeItem('foo', true); + + expect(collection.items).contain(item); + expect(item.deleted).true; + }); + + it('does nothing deleteItems flag is set to false and deleted flag is false', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = false; + collection.push(item); + + collection.removeItem('foo'); + + expect(collection.items).contain(item); + expect(item.deleted).not.true; + }); + }); + + describe('.assignAccount()', () => { + it('send assign account request to server', () => { + const assignAccount = stub(socket.server, 'assignAccount').returns(Promise.resolve()); + + return collection.assignAccount('foo', 'bar') + .then(() => assert.calledWith(assignAccount, 'events', 'foo', 'bar')); + }); + }); + + describe('.live()', () => { + it('returns promise if running', () => { + return collection.live(); + }); + + it('schedules next update', () => { + return collection.live() + .then(() => { + const live = stub(collection, 'live'); + clock.tick(2100); + assert.calledOnce(live); + }); + }); + + it('schedules next update fast if more flag is true', () => { + stub(socket.server, 'getAll').resolves({} as any); + stub(collection, 'read').returns(true); + socket.isConnected = true; + + return collection.live() + .then(() => { + const live = stub(collection, 'live'); + clock.tick(200); + assert.calledOnce(live); + }); + }); + + it('polls server for updates if connected', () => { + const data = {}; + stub(socket.server, 'getAll').withArgs('events', '2010-01-01T10:00:00.000Z').resolves(data as any); + const read = stub(collection, 'read'); + socket.isConnected = true; + + return collection.live() + .then(() => assert.calledWith(read, data as any)); + }); + + it('does not poll the server if not connected', () => { + const getAll = stub(socket.server, 'getAll'); + stub(collection, 'read'); + socket.isConnected = false; + + return collection.live() + .then(() => assert.notCalled(getAll)); + }); + + it('does not poll the server if stopped', async () => { + const getAll = stub(socket.server, 'getAll'); + stub(collection, 'read'); + socket.isConnected = true; + + collection.stop(); + + await collection.live(); + assert.notCalled(getAll); + }); + + it('does not reject for server error', async () => { + stub(socket.server, 'getAll').rejects(new Error('test')); + socket.isConnected = true; + + await collection.live(); + }); + }); + + describe('.read()', () => { + const base: LiveResponse = { + updates: [], + deletes: [], + base: {}, + more: false, + }; + + it('returns more flag', () => { + expect(collection.read({ ...base, more: false })).false; + expect(collection.read({ ...base, more: true })).true; + }); + + it('updates timestamp', async () => { + const getAll = stub(socket.server, 'getAll').resolves({} as any); + socket.isConnected = true; + + collection.read({ + ...base, + updates: [ + ['bar', 'Bar', '2010-01-03T10:00:00.000Z'], + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }); + + stub(collection, 'read'); + await collection.live(); + assert.calledWith(getAll, 'events', '2010-01-03T10:00:00.000Z'); + }); + + it('does not update timestamp if not live fetch', async () => { + const getAll = stub(socket.server, 'getAll').resolves({} as any); + socket.isConnected = true; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }, false); + + stub(collection, 'read'); + await collection.live(); + assert.calledWith(getAll, 'events', '2010-01-01T10:00:00.000Z'); + }); + + it('adds new items', () => { + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }); + + expect(collection.items[0]).eql({ + _id: 'foo', + name: 'Foo', + updatedAt: new Date('2010-01-02T10:00:00.000Z'), + }); + }); + + it('does not add new items if ignored', () => { + options.ignore = () => true; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }); + + expect(collection.items).empty; + }); + + it('adds items anyway if ignored but not live fetch', () => { + options.ignore = () => true; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-02T10:00:00.000Z'], + ], + }, false); + + expect(collection.items[0]).eql({ + _id: 'foo', + name: 'Foo', + updatedAt: new Date('2010-01-02T10:00:00.000Z'), + }); + }); + + it('updates existing items', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + collection.push(item); + + collection.read({ + ...base, + updates: [ + ['foo', 'Bar', '2010-01-03T10:00:00.000Z'], + ], + }); + + expect(item).eql({ + _id: 'foo', + name: 'Bar', + updatedAt: new Date('2010-01-03T10:00:00.000Z'), + }); + }); + + it('removes deleted items', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = true; + collection.push(item); + + collection.read({ + ...base, + deletes: ['foo'], + }); + + expect(collection.items).not.contain(item); + }); + + it('sets deleted flag on items if deleteItems is false', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + options.deleteItems = false; + collection.push(item); + + collection.read({ + ...base, + deletes: ['foo'], + }); + + expect(collection.items).contain(item); + expect(item.deleted).true; + }); + + it('calls beforeUpdate hook with items', () => { + const beforeUpdate = stub(); + options.beforeUpdate = beforeUpdate; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo', '2010-01-03T10:00:00.000Z'], + ], + }); + + assert.calledWithMatch(beforeUpdate, [ + { _id: 'foo', name: 'Foo', updatedAt: new Date('2010-01-03T10:00:00.000Z') }, + ]); + }); + + it('calls onUpdated hook with added and all items', () => { + const onUpdated = stub(); + options.onUpdated = onUpdated; + collection.push({ _id: 'foo', name: 'Foo', updatedAt: new Date() }); + + collection.read({ + ...base, + updates: [ + ['bar', 'Bar', '2010-01-04T10:00:00.000Z'], + ['foo', 'Foo2', '2010-01-03T10:00:00.000Z'], + ], + }); + + expect(onUpdated.args[0][0]).eql([ + { _id: 'bar', name: 'Bar', updatedAt: new Date('2010-01-04T10:00:00.000Z') }, + ], 'added'); + + expect(onUpdated.args[0][1]).eql([ + { _id: 'bar', name: 'Bar', updatedAt: new Date('2010-01-04T10:00:00.000Z') }, + { _id: 'foo', name: 'Foo2', updatedAt: new Date('2010-01-03T10:00:00.000Z') }, + ], 'all'); + }); + + it('does not call onUpdated hook for no items', () => { + const onUpdated = stub(); + options.onUpdated = onUpdated; + + collection.read({ + ...base, + updates: [], + }); + + assert.notCalled(onUpdated); + }); + + it('calls onDelete hook for deleted items', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + const onDelete = stub(); + options.onDelete = onDelete; + collection.push(item); + + collection.read({ + ...base, + deletes: ['foo'], + }); + + assert.calledWith(onDelete, item); + }); + + it('calls onFinished hook when more flag is false', () => { + const onFinished = stub(); + options.onFinished = onFinished; + + collection.read({ ...base, more: false }); + + assert.calledOnce(onFinished); + }); + + it('calls onFinished hook only once', () => { + const onFinished = stub(); + options.onFinished = onFinished; + + collection.read({ ...base, more: false }); + collection.read({ ...base, more: false }); + + assert.calledOnce(onFinished); + }); + + it('does not call onFinished hook for non live fetch calls', () => { + const onFinished = stub(); + options.onFinished = onFinished; + + collection.read({ ...base, more: false }, false); + + assert.notCalled(onFinished); + }); + + it('calls onUpdate hook for updated items', () => { + const item: Thing = { _id: 'foo', name: 'Foo', updatedAt: new Date() }; + const onUpdate = stub(); + options.onUpdate = onUpdate; + collection.push(item); + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo2', '2010-01-03T10:00:00.000Z'], + ], + }); + + assert.calledWith(onUpdate, item); + }); + + it('does not call onUpdate hook for added items', () => { + const onUpdate = stub(); + options.onUpdate = onUpdate; + + collection.read({ + ...base, + updates: [ + ['foo', 'Foo2', '2010-01-03T10:00:00.000Z'], + ], + }); + + assert.notCalled(onUpdate); + }); + }); }); diff --git a/src/ts/tests/services/liveList.spec.ts b/src/ts/tests/services/liveList.spec.ts index 5f432d1..6bef5c7 100644 --- a/src/ts/tests/services/liveList.spec.ts +++ b/src/ts/tests/services/liveList.spec.ts @@ -4,460 +4,460 @@ import { stub, SinonStub, assert, SinonFakeTimers, useFakeTimers } from 'sinon'; import { LiveList, LiveListConfig } from '../../server/services/liveList'; describe('LiveList', () => { - let model: { - find: SinonStub, - findById: SinonStub; - update: SinonStub; - deleteOne: SinonStub; - }; - let liveList: LiveList; - let config: LiveListConfig; - let logger: any; + let model: { + find: SinonStub, + findById: SinonStub; + update: SinonStub; + deleteOne: SinonStub; + }; + let liveList: LiveList; + let config: LiveListConfig; + let logger: any; - beforeEach(() => { - model = { - find: stub(), - findById: stub(), - update: stub(), - deleteOne: stub(), - }; - config = { fields: ['foo', 'bar'], clean: item => item }; - logger = {}; - liveList = new LiveList(model as any, config, undefined, logger); - }); + beforeEach(() => { + model = { + find: stub(), + findById: stub(), + update: stub(), + deleteOne: stub(), + }; + config = { fields: ['foo', 'bar'], clean: item => item }; + logger = {}; + liveList = new LiveList(model as any, config, undefined, logger); + }); - it('returns loaded state', () => { - (liveList as any).finished = true; - expect(liveList.loaded).true; - (liveList as any).finished = false; - expect(liveList.loaded).false; - }); + it('returns loaded state', () => { + (liveList as any).finished = true; + expect(liveList.loaded).true; + (liveList as any).finished = false; + expect(liveList.loaded).false; + }); - describe('for()', () => { - it('calls callback for item', () => { - const callback = stub(); - const item = { _id: 'foo' }; - liveList.add(item); + describe('for()', () => { + it('calls callback for item', () => { + const callback = stub(); + const item = { _id: 'foo' }; + liveList.add(item); - liveList.for('foo', callback); + liveList.for('foo', callback); - assert.calledWith(callback, item); - }); + assert.calledWith(callback, item); + }); - it('does nothing if item is not found', () => { - const callback = stub(); + it('does nothing if item is not found', () => { + const callback = stub(); - liveList.for('foo', callback); + liveList.for('foo', callback); - assert.notCalled(callback); - }); + assert.notCalled(callback); + }); - it('does nothing for undefined id', () => { - const callback = stub(); + it('does nothing for undefined id', () => { + const callback = stub(); - liveList.for(undefined, callback); + liveList.for(undefined, callback); - assert.notCalled(callback); - }); - }); + assert.notCalled(callback); + }); + }); - describe('add()', () => { - it('adds item to items', () => { - const item = { _id: 'foo' }; + describe('add()', () => { + it('adds item to items', () => { + const item = { _id: 'foo' }; - liveList.add(item); + liveList.add(item); - expect(liveList.items).contain(item); - }); + expect(liveList.items).contain(item); + }); - it('adds item to item map', () => { - const item = { _id: 'foo' }; + it('adds item to item map', () => { + const item = { _id: 'foo' }; - liveList.add(item); + liveList.add(item); - expect(liveList.get('foo')).equal(item); - }); + expect(liveList.get('foo')).equal(item); + }); - it('triggers event listeners', () => { - const item = { _id: 'foo' }; - const listener = stub(); - liveList.subscribe('foo', listener); + it('triggers event listeners', () => { + const item = { _id: 'foo' }; + const listener = stub(); + liveList.subscribe('foo', listener); - liveList.add(item); + liveList.add(item); - assert.calledWith(listener, 'foo', item); - }); + assert.calledWith(listener, 'foo', item); + }); - it('returns item', () => { - const item = { _id: 'foo' }; + it('returns item', () => { + const item = { _id: 'foo' }; - expect(liveList.add(item)).equal(item); - }); - }); + expect(liveList.add(item)).equal(item); + }); + }); - describe('remove()', () => { - it('removes item from database', async () => { - model.deleteOne.returns({ exec: stub().resolves() }); + describe('remove()', () => { + it('removes item from database', async () => { + model.deleteOne.returns({ exec: stub().resolves() }); - await liveList.remove('foo'); + await liveList.remove('foo'); - assert.calledWithMatch(model.deleteOne, { _id: 'foo' }); - }); + assert.calledWithMatch(model.deleteOne, { _id: 'foo' }); + }); - it('notifies removal of item', async () => { - model.deleteOne.returns({ exec: stub().resolves() }); - const removed = stub(liveList, 'removed'); + it('notifies removal of item', async () => { + model.deleteOne.returns({ exec: stub().resolves() }); + const removed = stub(liveList, 'removed'); - await liveList.remove('foo'); + await liveList.remove('foo'); - assert.calledWith(removed, 'foo'); - }); - }); + assert.calledWith(removed, 'foo'); + }); + }); - describe('removed()', () => { - it('removed item from items', () => { - const item = { _id: 'foo' }; - liveList.add(item); + describe('removed()', () => { + it('removed item from items', () => { + const item = { _id: 'foo' }; + liveList.add(item); - liveList.removed('foo'); + liveList.removed('foo'); - expect(liveList.items).not.contain(item); - }); + expect(liveList.items).not.contain(item); + }); - it('removed item from items map', () => { - const item = { _id: 'foo' }; - liveList.add(item); + it('removed item from items map', () => { + const item = { _id: 'foo' }; + liveList.add(item); - liveList.removed('foo'); + liveList.removed('foo'); - expect(liveList.get('foo')).undefined; - }); + expect(liveList.get('foo')).undefined; + }); - it('triggers event listeners', () => { - const item = { _id: 'foo' }; - liveList.add(item); - const trigger = stub(liveList, 'trigger'); + it('triggers event listeners', () => { + const item = { _id: 'foo' }; + liveList.add(item); + const trigger = stub(liveList, 'trigger'); - liveList.removed('foo'); + liveList.removed('foo'); - assert.calledWith(trigger, 'foo', undefined); - }); + assert.calledWith(trigger, 'foo', undefined); + }); - it('calls onDelete event handler', () => { - const item = { _id: 'foo' }; - liveList.add(item); - const onDelete = stub(); - config.onDelete = onDelete; + it('calls onDelete event handler', () => { + const item = { _id: 'foo' }; + liveList.add(item); + const onDelete = stub(); + config.onDelete = onDelete; - liveList.removed('foo'); + liveList.removed('foo'); - assert.calledWith(onDelete, item); - }); + assert.calledWith(onDelete, item); + }); - it('does nothing if item is not found', () => { - const trigger = stub(liveList, 'trigger'); + it('does nothing if item is not found', () => { + const trigger = stub(liveList, 'trigger'); - liveList.removed('foo'); + liveList.removed('foo'); - assert.notCalled(trigger); - }); - }); + assert.notCalled(trigger); + }); + }); - describe('trigger() / subscribe()', () => { - it('calls all listeners', () => { - const item = { _id: 'foo' }; - const listener1 = stub(); - const listener2 = stub(); - liveList.subscribe('foo', listener1); - liveList.subscribe('foo', listener2); + describe('trigger() / subscribe()', () => { + it('calls all listeners', () => { + const item = { _id: 'foo' }; + const listener1 = stub(); + const listener2 = stub(); + liveList.subscribe('foo', listener1); + liveList.subscribe('foo', listener2); - liveList.trigger('foo', item); + liveList.trigger('foo', item); - assert.calledWith(listener1, 'foo', item); - assert.calledWith(listener2, 'foo', item); - }); + assert.calledWith(listener1, 'foo', item); + assert.calledWith(listener2, 'foo', item); + }); - it('does nothing for no listeners', () => { - const item = { _id: 'foo' }; + it('does nothing for no listeners', () => { + const item = { _id: 'foo' }; - liveList.trigger('foo', item); - }); + liveList.trigger('foo', item); + }); - it('lets unsubscribe listeners', () => { - const item = { _id: 'foo' }; - const listener1 = stub(); - const listener2 = stub(); - const subscription = liveList.subscribe('foo', listener1); - liveList.subscribe('foo', listener2); + it('lets unsubscribe listeners', () => { + const item = { _id: 'foo' }; + const listener1 = stub(); + const listener2 = stub(); + const subscription = liveList.subscribe('foo', listener1); + liveList.subscribe('foo', listener2); - subscription.unsubscribe(); - liveList.trigger('foo', item); + subscription.unsubscribe(); + liveList.trigger('foo', item); - assert.notCalled(listener1); - assert.calledWith(listener2, 'foo', item); - }); + assert.notCalled(listener1); + assert.calledWith(listener2, 'foo', item); + }); - it('does nothing if unsubscribed twice', () => { - const item = { _id: 'foo' }; - const listener = stub(); + it('does nothing if unsubscribed twice', () => { + const item = { _id: 'foo' }; + const listener = stub(); - const subscription = liveList.subscribe('foo', listener); - subscription.unsubscribe(); - subscription.unsubscribe(); - liveList.trigger('foo', item); + const subscription = liveList.subscribe('foo', listener); + subscription.unsubscribe(); + subscription.unsubscribe(); + liveList.trigger('foo', item); - assert.notCalled(listener); - }); + assert.notCalled(listener); + }); - it('cleans document before sending', () => { - const item = { _id: 'foo' }; - const listener = stub(); - liveList.subscribe('foo', listener); - config.clean = item => ({ ...item, bar: 5 }); + it('cleans document before sending', () => { + const item = { _id: 'foo' }; + const listener = stub(); + liveList.subscribe('foo', listener); + config.clean = item => ({ ...item, bar: 5 }); - liveList.trigger('foo', item); + liveList.trigger('foo', item); - assert.calledWithMatch(listener, 'foo', { _id: 'foo', bar: 5 }); - }); + assert.calledWithMatch(listener, 'foo', { _id: 'foo', bar: 5 }); + }); - it('skips cleanig if document is undefined', () => { - const listener = stub(); - liveList.subscribe('foo', listener); - config.clean = item => item.error; + it('skips cleanig if document is undefined', () => { + const listener = stub(); + liveList.subscribe('foo', listener); + config.clean = item => item.error; - liveList.trigger('foo', undefined); + liveList.trigger('foo', undefined); - assert.calledWithMatch(listener, 'foo', undefined); - }); + assert.calledWithMatch(listener, 'foo', undefined); + }); - it('calls listener on subscribe if found document', () => { - const item = { _id: 'foo' }; - liveList.add(item); - const listener = stub(); + it('calls listener on subscribe if found document', () => { + const item = { _id: 'foo' }; + liveList.add(item); + const listener = stub(); - liveList.subscribe('foo', listener); + liveList.subscribe('foo', listener); - assert.calledWith(listener, 'foo', item); - }); + assert.calledWith(listener, 'foo', item); + }); - it('cleans document when sending it on subscribe', () => { - const item = { _id: 'foo' }; - liveList.add(item); - const listener = stub(); - config.clean = item => ({ ...item, bar: 5 }); + it('cleans document when sending it on subscribe', () => { + const item = { _id: 'foo' }; + liveList.add(item); + const listener = stub(); + config.clean = item => ({ ...item, bar: 5 }); - liveList.subscribe('foo', listener); + liveList.subscribe('foo', listener); - assert.calledWithMatch(listener, 'foo', { _id: 'foo', bar: 5 }); - }); - }); + assert.calledWithMatch(listener, 'foo', { _id: 'foo', bar: 5 }); + }); + }); - describe('start()', () => { - it('starts first tick', () => { - const tick = stub(liveList, 'tick'); + describe('start()', () => { + it('starts first tick', () => { + const tick = stub(liveList, 'tick'); - liveList.start(); + liveList.start(); - assert.calledOnce(tick); - }); - }); + assert.calledOnce(tick); + }); + }); - describe('tick()', () => { - let clock: SinonFakeTimers; + describe('tick()', () => { + let clock: SinonFakeTimers; - beforeEach(() => { - clock = useFakeTimers(); - }); + beforeEach(() => { + clock = useFakeTimers(); + }); - afterEach(() => { - clock.restore(); - }); + afterEach(() => { + clock.restore(); + }); - it('does nothing if not running', async () => { - const update = stub(liveList, 'update').resolves(); + it('does nothing if not running', async () => { + const update = stub(liveList, 'update').resolves(); - await liveList.tick(); + await liveList.tick(); - assert.notCalled(update); - }); + assert.notCalled(update); + }); - it('calls update', async () => { - const update = stub(liveList, 'update').resolves(); - (liveList as any).running = true; + it('calls update', async () => { + const update = stub(liveList, 'update').resolves(); + (liveList as any).running = true; - await liveList.tick(); + await liveList.tick(); - assert.calledOnce(update); - }); + assert.calledOnce(update); + }); - it('schedules next tick with new timestamp', async () => { - const update = stub(liveList, 'update').resolves(); - (liveList as any).running = true; + it('schedules next tick with new timestamp', async () => { + const update = stub(liveList, 'update').resolves(); + (liveList as any).running = true; - await liveList.tick(); - clock.tick(2000); + await liveList.tick(); + clock.tick(2000); - assert.calledTwice(update); - }); + assert.calledTwice(update); + }); - it('stop() prevents further ticks', async () => { - const update = stub(liveList, 'update').resolves(); - (liveList as any).running = true; + it('stop() prevents further ticks', async () => { + const update = stub(liveList, 'update').resolves(); + (liveList as any).running = true; - await liveList.tick(); + await liveList.tick(); - liveList.stop(); + liveList.stop(); - clock.tick(2000); + clock.tick(2000); - assert.calledOnce(update); - }); - }); + assert.calledOnce(update); + }); + }); - describe('update()', () => { - function createQuery(items: T[]) { - return { - cursor() { - return { - on(event: string, callback: any) { - if (event === 'data') { - items.forEach(callback); - } else if (event === 'end') { - callback(); - } - return this; - } - }; - } - }; - } + describe('update()', () => { + function createQuery(items: T[]) { + return { + cursor() { + return { + on(event: string, callback: any) { + if (event === 'data') { + items.forEach(callback); + } else if (event === 'end') { + callback(); + } + return this; + } + }; + } + }; + } - function setupFind(items: T[]) { - model.find.returns({ lean: stub().returns(createQuery(items)) }); - } + function setupFind(items: T[]) { + model.find.returns({ lean: stub().returns(createQuery(items)) }); + } - it('queried database with current timestamp', async () => { - const timestamp = new Date(12345); - (liveList as any).timestamp = timestamp; - setupFind([]); + it('queried database with current timestamp', async () => { + const timestamp = new Date(12345); + (liveList as any).timestamp = timestamp; + setupFind([]); - await liveList.update(); + await liveList.update(); - assert.calledWithMatch(model.find, { updatedAt: { $gt: timestamp } }, 'foo bar'); - }); + assert.calledWithMatch(model.find, { updatedAt: { $gt: timestamp } }, 'foo bar'); + }); - it('adds new item', async () => { - const item = { _id: 'foo' }; - setupFind([item]); + it('adds new item', async () => { + const item = { _id: 'foo' }; + setupFind([item]); - await liveList.update(); + await liveList.update(); - expect(liveList.items).contain(item); - }); + expect(liveList.items).contain(item); + }); - it('calls onAddedOrUpdated event handler', async () => { - const onAddedOrUpdated = stub(); - config.onAddedOrUpdated = onAddedOrUpdated; - setupFind([{ _id: 'foo' }]); + it('calls onAddedOrUpdated event handler', async () => { + const onAddedOrUpdated = stub(); + config.onAddedOrUpdated = onAddedOrUpdated; + setupFind([{ _id: 'foo' }]); - await liveList.update(); + await liveList.update(); - assert.calledOnce(onAddedOrUpdated); - }); + assert.calledOnce(onAddedOrUpdated); + }); - it('does not call onAddedOrUpdated event handler if did not fetch any items', async () => { - const onAddedOrUpdated = stub(); - config.onAddedOrUpdated = onAddedOrUpdated; - setupFind([]); + it('does not call onAddedOrUpdated event handler if did not fetch any items', async () => { + const onAddedOrUpdated = stub(); + config.onAddedOrUpdated = onAddedOrUpdated; + setupFind([]); - await liveList.update(); + await liveList.update(); - assert.notCalled(onAddedOrUpdated); - }); + assert.notCalled(onAddedOrUpdated); + }); - it('calls onAdd event when adding', () => { - const item = { _id: 'foo' }; - const onAdd = stub(); - config.onAdd = onAdd; - setupFind([item]); + it('calls onAdd event when adding', () => { + const item = { _id: 'foo' }; + const onAdd = stub(); + config.onAdd = onAdd; + setupFind([item]); - liveList.update(); + liveList.update(); - assert.calledWith(onAdd, item); - }); + assert.calledWith(onAdd, item); + }); - it('updates existing item', () => { - const item = { _id: 'foo', value: 1 }; - liveList.add(item); - setupFind([{ _id: 'foo', value: 2 }]); + it('updates existing item', () => { + const item = { _id: 'foo', value: 1 }; + liveList.add(item); + setupFind([{ _id: 'foo', value: 2 }]); - liveList.update(); + liveList.update(); - expect(item.value).equal(2); - }); + expect(item.value).equal(2); + }); - it('fixes documents before updating', () => { - const item = { _id: 'foo', value: 1 }; - config.fix = item => item.field = 'bar'; - liveList.add(item); - setupFind([{ _id: 'foo', value: 2 }]); + it('fixes documents before updating', () => { + const item = { _id: 'foo', value: 1 }; + config.fix = item => item.field = 'bar'; + liveList.add(item); + setupFind([{ _id: 'foo', value: 2 }]); - liveList.update(); + liveList.update(); - expect(item).eql({ _id: 'foo', value: 2, field: 'bar' }); - }); + expect(item).eql({ _id: 'foo', value: 2, field: 'bar' }); + }); - it('triggers listeners updates existing item', () => { - const trigger = stub(liveList, 'trigger'); - const item = { _id: 'foo', value: 1 }; - liveList.add(item); - setupFind([{ _id: 'foo', value: 2 }]); + it('triggers listeners updates existing item', () => { + const trigger = stub(liveList, 'trigger'); + const item = { _id: 'foo', value: 1 }; + liveList.add(item); + setupFind([{ _id: 'foo', value: 2 }]); - liveList.update(); + liveList.update(); - assert.calledWith(trigger, 'foo', item); - }); + assert.calledWith(trigger, 'foo', item); + }); - it('calls onUpdate event when updating', () => { - const item = { _id: 'foo', value: 1 }; - const update = { _id: 'foo', value: 2 }; - const onUpdate = stub(); - config.onUpdate = onUpdate; - liveList.add(item); - setupFind([update]); + it('calls onUpdate event when updating', () => { + const item = { _id: 'foo', value: 1 }; + const update = { _id: 'foo', value: 2 }; + const onUpdate = stub(); + config.onUpdate = onUpdate; + liveList.add(item); + setupFind([update]); - liveList.update(); + liveList.update(); - assert.calledWith(onUpdate, item, update); - }); + assert.calledWith(onUpdate, item, update); + }); - it('sets loaded to true ', async () => { - setupFind([]); + it('sets loaded to true ', async () => { + setupFind([]); - await liveList.update(); + await liveList.update(); - expect(liveList.loaded).true; - }); + expect(liveList.loaded).true; + }); - it('calls onFinished event handler', async () => { - const onFinished = stub(); - config.onFinished = onFinished; - setupFind([]); + it('calls onFinished event handler', async () => { + const onFinished = stub(); + config.onFinished = onFinished; + setupFind([]); - await liveList.update(); + await liveList.update(); - assert.calledOnce(onFinished); - }); + assert.calledOnce(onFinished); + }); - it('calls onFinished event handler only on first finish', async () => { - const onFinished = stub(); - config.onFinished = onFinished; - setupFind([]); + it('calls onFinished event handler only on first finish', async () => { + const onFinished = stub(); + config.onFinished = onFinished; + setupFind([]); - await liveList.update(); - await liveList.update(); + await liveList.update(); + await liveList.update(); - assert.calledOnce(onFinished); - }); - }); + assert.calledOnce(onFinished); + }); + }); }); diff --git a/src/ts/tests/services/model.spec.ts b/src/ts/tests/services/model.spec.ts index 8cd78d1..d5281fe 100644 --- a/src/ts/tests/services/model.spec.ts +++ b/src/ts/tests/services/model.spec.ts @@ -10,224 +10,224 @@ import { NAME_ERROR } from '../../common/errors'; import { StorageService } from '../../components/services/storageService'; describe.skip('Model', () => { - let http: any = stubClass(HttpClient); - let router = stubClass(Router); - let storage = stubClass(StorageService); - let errorReporter = stubClass(ErrorReporter); - let model: Model; + let http: any = stubClass(HttpClient); + let router = stubClass(Router); + let storage = stubClass(StorageService); + let errorReporter = stubClass(ErrorReporter); + let model: Model; - beforeEach(async () => { - resetStubMethods(http, 'post', 'get'); - resetStubMethods(router); - resetStubMethods(storage); - resetStubMethods(errorReporter); - http.post.withArgs('/api1/account').returns(from([{ - id: 'accid', - name: 'accname', - settings: {}, - friends: [], - }])); - model = new Model(http as any, router as any, storage as any, errorReporter); - await model.accountPromise; - (global as any).location = { href: 'local-href' }; - }); + beforeEach(async () => { + resetStubMethods(http, 'post', 'get'); + resetStubMethods(router); + resetStubMethods(storage); + resetStubMethods(errorReporter); + http.post.withArgs('/api1/account').returns(from([{ + id: 'accid', + name: 'accname', + settings: {}, + friends: [], + }])); + model = new Model(http as any, router as any, storage as any, errorReporter); + await model.accountPromise; + (global as any).location = { href: 'local-href' }; + }); - describe('supporter', () => { - it('returns 0 if not supporter', () => { - expect(model.supporter).equal(0); - }); + describe('supporter', () => { + it('returns 0 if not supporter', () => { + expect(model.supporter).equal(0); + }); - it('returns supporter level', () => { - model.account!.supporter = 2; + it('returns supporter level', () => { + model.account!.supporter = 2; - expect(model.supporter).equal(2); - }); - }); + expect(model.supporter).equal(2); + }); + }); - describe('signOut()', () => { - it('makes sign out request', async () => { - http.post.withArgs('/auth/sign-out').returns(from([{}])); + describe('signOut()', () => { + it('makes sign out request', async () => { + http.post.withArgs('/auth/sign-out').returns(from([{}])); - await model.signOut(); - await model.accountPromise; + await model.signOut(); + await model.accountPromise; - assert.calledWithMatch(http.post, '/auth/sign-out', {}, match.any); - }); + assert.calledWithMatch(http.post, '/auth/sign-out', {}, match.any); + }); - it('re-fetches account details', async () => { - http.post.reset(); - http.post.withArgs('/auth/sign-out').returns(from([{}])); - http.post.withArgs('/api1/account').returns(from([null])); + it('re-fetches account details', async () => { + http.post.reset(); + http.post.withArgs('/auth/sign-out').returns(from([{}])); + http.post.withArgs('/api1/account').returns(from([null])); - await model.signOut(); - await model.accountPromise; + await model.signOut(); + await model.accountPromise; - assert.calledWithMatch(http.post, '/api1/account', match.any, match.any); - }); - }); + assert.calledWithMatch(http.post, '/api1/account', match.any, match.any); + }); + }); - describe('updateAccount()', () => { - it('makes update request', async () => { - http.post.withArgs('/api/account-update').returns(from([{ name: 'bar' }])); + describe('updateAccount()', () => { + it('makes update request', async () => { + http.post.withArgs('/api/account-update').returns(from([{ name: 'bar' }])); - await model.updateAccount({ name: 'bar', birthdate: '' }); + await model.updateAccount({ name: 'bar', birthdate: '' }); - assert.calledWithMatch(http.post, '/api/account-update', { account: { name: 'bar' } }, match.any); - }); + assert.calledWithMatch(http.post, '/api/account-update', { account: { name: 'bar' } }, match.any); + }); - it('updates account with returned result', async () => { - http.post.withArgs('/api/account-update').returns(from([{ name: 'bar' }])); + it('updates account with returned result', async () => { + http.post.withArgs('/api/account-update').returns(from([{ name: 'bar' }])); - await model.updateAccount({ name: 'bar', birthdate: '' }); + await model.updateAccount({ name: 'bar', birthdate: '' }); - expect(model.account!.name).equal('bar'); - }); - }); + expect(model.account!.name).equal('bar'); + }); + }); - describe('saveSettings()', () => { - it('makes save settings request', async () => { - http.post.withArgs('/api/account-settings') - .returns(from([{ settings: { ignorePartyInvites: true } }])); + describe('saveSettings()', () => { + it('makes save settings request', async () => { + http.post.withArgs('/api/account-settings') + .returns(from([{ settings: { ignorePartyInvites: true } }])); - await model.saveSettings({ ignorePartyInvites: true }); + await model.saveSettings({ ignorePartyInvites: true }); - assert.calledWithMatch(http.post, '/api/account-settings', { settings: { ignorePartyInvites: true } }, match.any); - }); + assert.calledWithMatch(http.post, '/api/account-settings', { settings: { ignorePartyInvites: true } }, match.any); + }); - it('updates settings with returned result', async () => { - http.post.withArgs('/api/account-settings') - .returns(from([{ settings: { ignorePartyInvites: true } }])); + it('updates settings with returned result', async () => { + http.post.withArgs('/api/account-settings') + .returns(from([{ settings: { ignorePartyInvites: true } }])); - await model.saveSettings({ ignorePartyInvites: true }); + await model.saveSettings({ ignorePartyInvites: true }); - expect(model.account!.settings.ignorePartyInvites).true; - }); - }); + expect(model.account!.settings.ignorePartyInvites).true; + }); + }); - describe('removeSite()', () => { - it('makes remove request', async () => { - http.post.withArgs('/api/remove-site').returns(from([{}])); + describe('removeSite()', () => { + it('makes remove request', async () => { + http.post.withArgs('/api/remove-site').returns(from([{}])); - await model.removeSite('siteid'); + await model.removeSite('siteid'); - assert.calledWithMatch(http.post, '/api/remove-site', { siteId: 'siteid' }, match.any); - }); + assert.calledWithMatch(http.post, '/api/remove-site', { siteId: 'siteid' }, match.any); + }); - it('removes site from the list', async () => { - http.post.withArgs('/api/remove-site').returns(from([{}])); - const site = { id: 'siteid' } as any; - model.account!.sites = [site]; + it('removes site from the list', async () => { + http.post.withArgs('/api/remove-site').returns(from([{}])); + const site = { id: 'siteid' } as any; + model.account!.sites = [site]; - await model.removeSite('siteid'); + await model.removeSite('siteid'); - expect(model.account!.sites).not.contain(site); - }); - }); + expect(model.account!.sites).not.contain(site); + }); + }); - describe('savePony()', () => { - it('makes save request', async () => { - http.post.withArgs('/api/pony/save').returns(from([{}])); + describe('savePony()', () => { + it('makes save request', async () => { + http.post.withArgs('/api/pony/save').returns(from([{}])); - await model.savePony({ - id: 'ponyid', - name: 'ponyname', - info: 'ponyinfo', - site: 'siteid', - tag: 'tagname', - hideSupport: true, - }); + await model.savePony({ + id: 'ponyid', + name: 'ponyname', + info: 'ponyinfo', + site: 'siteid', + tag: 'tagname', + hideSupport: true, + }); - assert.calledWithMatch(http.post, '/api/pony/save', { - pony: { - id: 'ponyid', - name: 'ponyname', - info: 'ponyinfo', - site: 'siteid', - tag: 'tagname', - hideSupport: true, - }, - }, match.any); - }); + assert.calledWithMatch(http.post, '/api/pony/save', { + pony: { + id: 'ponyid', + name: 'ponyname', + info: 'ponyinfo', + site: 'siteid', + tag: 'tagname', + hideSupport: true, + }, + }, match.any); + }); - it('rejects if name is invalid', async () => { - http.post.withArgs('/api/pony/save').returns(from([{}])); + it('rejects if name is invalid', async () => { + http.post.withArgs('/api/pony/save').returns(from([{}])); - await expect(model.savePony({ id: '', name: '', info: 'ponyinfo' })).rejectedWith(NAME_ERROR); - }); + await expect(model.savePony({ id: '', name: '', info: 'ponyinfo' })).rejectedWith(NAME_ERROR); + }); - it('rejects if already saving', async () => { - http.post.withArgs('/api/pony/save').returns(from([{}])); + it('rejects if already saving', async () => { + http.post.withArgs('/api/pony/save').returns(from([{}])); - await Promise.all([ - model.savePony({ id: '', name: 'ponyname', info: 'ponyinfo' }), - expect(model.savePony({ id: '', name: '', info: 'ponyinfo' })).rejectedWith('Saving in progress'), - ]); - }); - }); + await Promise.all([ + model.savePony({ id: '', name: 'ponyname', info: 'ponyinfo' }), + expect(model.savePony({ id: '', name: '', info: 'ponyinfo' })).rejectedWith('Saving in progress'), + ]); + }); + }); - describe('removePony()', () => { - it('makes remove request', async () => { - const ponyObject = { id: 'ponyid' } as any; - http.post.withArgs('/api/pony/remove').returns(from([{}])); + describe('removePony()', () => { + it('makes remove request', async () => { + const ponyObject = { id: 'ponyid' } as any; + http.post.withArgs('/api/pony/remove').returns(from([{}])); - await model.removePony(ponyObject); + await model.removePony(ponyObject); - assert.calledWithMatch(http.post, '/api/pony/remove', { id: 'ponyid' }, match.any); - }); - }); + assert.calledWithMatch(http.post, '/api/pony/remove', { id: 'ponyid' }, match.any); + }); + }); - describe('status()', () => { - it('fetches full server status', async () => { - const status = {}; - http.get.withArgs('/api2/game/status').returns(from([status])); + describe('status()', () => { + it('fetches full server status', async () => { + const status = {}; + http.get.withArgs('/api2/game/status').returns(from([status])); - const result = await model.status(false); + const result = await model.status(false); - assert.calledWithMatch(http.get, '/api2/game/status'); - expect(result).eql(status); - }); - }); + assert.calledWithMatch(http.get, '/api2/game/status'); + expect(result).eql(status); + }); + }); - describe('join()', () => { - it('fetches join result', async () => { - const state = {}; - http.post.withArgs('/api/game/join').returns(from([state])); + describe('join()', () => { + it('fetches join result', async () => { + const state = {}; + http.post.withArgs('/api/game/join').returns(from([state])); - const result = await model.join('serverid', 'ponyid'); + const result = await model.join('serverid', 'ponyid'); - assert.calledWithMatch(http.post, '/api/game/join', { - accountId: 'accid', - accountName: 'accname', - version: undefined, - serverId: 'serverid', - ponyId: 'ponyid', - url: 'local-href', - }, match.any); - expect(result).equal(state); - }); + assert.calledWithMatch(http.post, '/api/game/join', { + accountId: 'accid', + accountName: 'accname', + version: undefined, + serverId: 'serverid', + ponyId: 'ponyid', + url: 'local-href', + }, match.any); + expect(result).equal(state); + }); - it('rejects if called when already pending', async () => { - http.post.withArgs('/api/game/join').returns(from([{}])); + it('rejects if called when already pending', async () => { + http.post.withArgs('/api/game/join').returns(from([{}])); - const promise1 = model.join('serverid', 'ponyid'); - const promise2 = model.join('serverid', 'ponyid'); + const promise1 = model.join('serverid', 'ponyid'); + const promise2 = model.join('serverid', 'ponyid'); - await Promise.all([ - promise1, - expect(promise2).rejectedWith('Joining in progress'), - ]); - }); + await Promise.all([ + promise1, + expect(promise2).rejectedWith('Joining in progress'), + ]); + }); - it('rejects if server is invalid', async () => { - http.post.withArgs('/api/game/join').returns(from([{}])); + it('rejects if server is invalid', async () => { + http.post.withArgs('/api/game/join').returns(from([{}])); - await expect(model.join('', 'ponyid')).rejectedWith('Invalid server ID'); - }); + await expect(model.join('', 'ponyid')).rejectedWith('Invalid server ID'); + }); - it('rejects if pony is invalid', async () => { - http.post.withArgs('/api/game/join').returns(from([{}])); + it('rejects if pony is invalid', async () => { + http.post.withArgs('/api/game/join').returns(from([{}])); - await expect(model.join('serverid', '')).rejectedWith('Invalid pony ID'); - }); - }); + await expect(model.join('serverid', '')).rejectedWith('Invalid pony ID'); + }); + }); }); diff --git a/src/ts/tests/services/settingsService.spec.ts b/src/ts/tests/services/settingsService.spec.ts index 39da976..b57b443 100644 --- a/src/ts/tests/services/settingsService.spec.ts +++ b/src/ts/tests/services/settingsService.spec.ts @@ -6,100 +6,100 @@ import { StorageService } from '../../components/services/storageService'; import { Model } from '../../components/services/model'; describe('SettingsService', () => { - let model = stubClass(Model); - let storage = stubClass(StorageService); - let service: SettingsService; + let model = stubClass(Model); + let storage = stubClass(StorageService); + let service: SettingsService; - beforeEach(() => { - resetStubMethods(model, 'saveSettings'); - service = new SettingsService(storage as any, model as any); - }); + beforeEach(() => { + resetStubMethods(model, 'saveSettings'); + service = new SettingsService(storage as any, model as any); + }); - it('loads browser settings from storage', () => { - const settings = { foo: 'bar' }; - storage.getJSON.returns(settings); + it('loads browser settings from storage', () => { + const settings = { foo: 'bar' }; + storage.getJSON.returns(settings); - service = new SettingsService(storage as any, model as any); + service = new SettingsService(storage as any, model as any); - expect(service.browser).equal(settings); - }); + expect(service.browser).equal(settings); + }); - it('returns account settings', () => { - const settings = { foo: 'bar' }; - model.account = { settings } as any; + it('returns account settings', () => { + const settings = { foo: 'bar' }; + model.account = { settings } as any; - expect(service.account).equal(settings); - }); + expect(service.account).equal(settings); + }); - it('returns empty settings if account is empty', () => { - model.account = undefined as any; + it('returns empty settings if account is empty', () => { + model.account = undefined as any; - expect(service.account).eql({}); - }); + expect(service.account).eql({}); + }); - it('update account settings field', async () => { - const settings = { foo: 'bar' }; - model.account = {} as any; + it('update account settings field', async () => { + const settings = { foo: 'bar' }; + model.account = {} as any; - await service.saveAccountSettings(settings as any); + await service.saveAccountSettings(settings as any); - expect((model.account as any).settings).equal(settings); - }); + expect((model.account as any).settings).equal(settings); + }); - it('does not update account settings field if account is empty', async () => { - const settings = { foo: 'bar' }; - model.account = undefined as any; + it('does not update account settings field if account is empty', async () => { + const settings = { foo: 'bar' }; + model.account = undefined as any; - await service.saveAccountSettings(settings as any); + await service.saveAccountSettings(settings as any); - expect(model.account).undefined; - }); + expect(model.account).undefined; + }); - it('saves settings to model', async () => { - const settings = { foo: 'bar' }; + it('saves settings to model', async () => { + const settings = { foo: 'bar' }; - await service.saveAccountSettings(settings as any); + await service.saveAccountSettings(settings as any); - assert.calledWith(model.saveSettings, settings as any); - }); + assert.calledWith(model.saveSettings, settings as any); + }); - it('calls saving callback', async () => { - const settings = { foo: 'bar' }; - const save = stub().returns(true); - service.saving(save); + it('calls saving callback', async () => { + const settings = { foo: 'bar' }; + const save = stub().returns(true); + service.saving(save); - await service.saveAccountSettings(settings as any); + await service.saveAccountSettings(settings as any); - assert.calledOnce(save); - assert.notCalled(model.saveSettings); - }); + assert.calledOnce(save); + assert.notCalled(model.saveSettings); + }); - it('saves settings to model if saving callback returns false', async () => { - const settings = { foo: 'bar' }; - const save = stub().returns(false); - service.saving(save); + it('saves settings to model if saving callback returns false', async () => { + const settings = { foo: 'bar' }; + const save = stub().returns(false); + service.saving(save); - await service.saveAccountSettings(settings as any); + await service.saveAccountSettings(settings as any); - assert.calledOnce(save); - assert.calledWith(model.saveSettings, settings as any); - }); + assert.calledOnce(save); + assert.calledWith(model.saveSettings, settings as any); + }); - it('saves current browser settings', () => { - const settings = { foo: 'bar' }; - service.browser = settings as any; + it('saves current browser settings', () => { + const settings = { foo: 'bar' }; + service.browser = settings as any; - service.saveBrowserSettings(); + service.saveBrowserSettings(); - assert.calledWith(storage.setJSON, 'browser-settings', settings); - }); + assert.calledWith(storage.setJSON, 'browser-settings', settings); + }); - it('saves given browser settings', () => { - const settings = { foo: 'bar' }; + it('saves given browser settings', () => { + const settings = { foo: 'bar' }; - service.saveBrowserSettings(settings as any); + service.saveBrowserSettings(settings as any); - expect(service.browser).equal(settings); - assert.calledWith(storage.setJSON, 'browser-settings', settings); - }); + expect(service.browser).equal(settings); + assert.calledWith(storage.setJSON, 'browser-settings', settings); + }); }); diff --git a/src/ts/tools/canvas-utils.ts b/src/ts/tools/canvas-utils.ts index 428bd50..220fa82 100644 --- a/src/ts/tools/canvas-utils.ts +++ b/src/ts/tools/canvas-utils.ts @@ -3,281 +3,281 @@ import { createCanvas, Image } from 'canvas'; import { ExtCanvas, Point } from './types'; export function loadImage(filePath: string) { - const image = new Image(); - image.src = fs.readFileSync(filePath); - (image as any).currentSrc = filePath; - return image; + const image = new Image(); + image.src = fs.readFileSync(filePath); + (image as any).currentSrc = filePath; + return image; } export function createExtCanvas(width: number, height: number, info: string): ExtCanvas { - const canvas: ExtCanvas = createCanvas(width, height) as any; - canvas.info = info; - return canvas; + const canvas: ExtCanvas = createCanvas(width, height) as any; + canvas.info = info; + return canvas; } export function imageToCanvas(image: HTMLImageElement): ExtCanvas { - const canvas = createExtCanvas(image.width, image.height, `imageToCanvas(${image.currentSrc})`); - canvas.getContext('2d')!.drawImage(image, 0, 0); - return canvas; + const canvas = createExtCanvas(image.width, image.height, `imageToCanvas(${image.currentSrc})`); + canvas.getContext('2d')!.drawImage(image, 0, 0); + return canvas; } export function cropCanvas(canvas: ExtCanvas | undefined, x: number, y: number, w: number, h: number): ExtCanvas { - if (Math.round(x) !== x || Math.round(y) !== y || Math.round(w) !== w || Math.round(h) !== h) { - throw new Error(`Invalid cropping dimentions (${x} ${y} ${w} ${h})`); - } + if (Math.round(x) !== x || Math.round(y) !== y || Math.round(w) !== w || Math.round(h) !== h) { + throw new Error(`Invalid cropping dimentions (${x} ${y} ${w} ${h})`); + } - const result = createExtCanvas(w, h, `${canvas ? canvas.info : 'from null'} (cropped ${x} ${y} ${w} ${h})`); + const result = createExtCanvas(w, h, `${canvas ? canvas.info : 'from null'} (cropped ${x} ${y} ${w} ${h})`); - if (canvas) { - w = Math.min(w, canvas.width - x); - h = Math.min(h, canvas.height - y); - result.getContext('2d')!.drawImage(canvas, x, y, w, h, 0, 0, w, h); - } + if (canvas) { + w = Math.min(w, canvas.width - x); + h = Math.min(h, canvas.height - y); + result.getContext('2d')!.drawImage(canvas, x, y, w, h, 0, 0, w, h); + } - return result; + return result; } export function mirrorCanvas(canvas: ExtCanvas, offsetX = 0) { - const mirrored = createExtCanvas(canvas.width, canvas.height, `${canvas.info} (mirrored)`); - const context = mirrored.getContext('2d')!; - context.translate(canvas.width, 0); - context.scale(-1, 1); - context.translate(offsetX, 0); - context.drawImage(canvas, 0, 0); - return mirrored; + const mirrored = createExtCanvas(canvas.width, canvas.height, `${canvas.info} (mirrored)`); + const context = mirrored.getContext('2d')!; + context.translate(canvas.width, 0); + context.scale(-1, 1); + context.translate(offsetX, 0); + context.drawImage(canvas, 0, 0); + return mirrored; } export function padCanvas(canvas: ExtCanvas, left: number, top: number, right = 0, bottom = 0, bg?: string) { - if (left === 0 && top === 0 && right === 0 && bottom === 0) - return canvas; + if (left === 0 && top === 0 && right === 0 && bottom === 0) + return canvas; - const result = createExtCanvas( - canvas.width + left + right, canvas.height + top + bottom, `${canvas.info} (pad ${left} ${top} ${right} ${bottom})`); - const context = result.getContext('2d')!; + const result = createExtCanvas( + canvas.width + left + right, canvas.height + top + bottom, `${canvas.info} (pad ${left} ${top} ${right} ${bottom})`); + const context = result.getContext('2d')!; - if (bg) { - context.fillStyle = bg; - context.fillRect(0, 0, result.width, result.height); - } + if (bg) { + context.fillStyle = bg; + context.fillRect(0, 0, result.width, result.height); + } - context.drawImage(canvas, left, top); - return result; + context.drawImage(canvas, left, top); + return result; } export function clipCanvas(canvas: ExtCanvas, x: number, y: number, w: number, h: number) { - return padCanvas(cropCanvas(canvas, x, y, w, h), x, y, canvas.width - (w + x), canvas.height - (h + y)); + return padCanvas(cropCanvas(canvas, x, y, w, h), x, y, canvas.width - (w + x), canvas.height - (h + y)); } export function mergeCanvases(...canvases: (ExtCanvas | undefined)[]): ExtCanvas { - const existing = canvases.filter(c => !!c) as ExtCanvas[]; - const { width, height } = existing[0]; - const result = createExtCanvas(width, height, existing.map(c => c.info).join(' + ')); - const context = result.getContext('2d')!; - existing.forEach(c => context.drawImage(c, 0, 0)); - return result; + const existing = canvases.filter(c => !!c) as ExtCanvas[]; + const { width, height } = existing[0]; + const result = createExtCanvas(width, height, existing.map(c => c.info).join(' + ')); + const context = result.getContext('2d')!; + existing.forEach(c => context.drawImage(c, 0, 0)); + return result; } export function reverseMaskCanvas(canvas: ExtCanvas): ExtCanvas; export function reverseMaskCanvas(canvas: ExtCanvas | undefined): ExtCanvas | undefined; export function reverseMaskCanvas(canvas: ExtCanvas | undefined) { - if (!canvas) - return undefined; + if (!canvas) + return undefined; - const result = createExtCanvas(canvas.width, canvas.height, `${canvas.info} reversed mask`); - const context = result.getContext('2d')!; - context.fillStyle = 'white'; - context.fillRect(0, 0, canvas.width, canvas.height); - context.globalCompositeOperation = 'destination-out'; - context.drawImage(canvas, 0, 0); - return result; + const result = createExtCanvas(canvas.width, canvas.height, `${canvas.info} reversed mask`); + const context = result.getContext('2d')!; + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + context.globalCompositeOperation = 'destination-out'; + context.drawImage(canvas, 0, 0); + return result; } export function maskCanvas(canvas: ExtCanvas, mask: ExtCanvas): ExtCanvas; export function maskCanvas(canvas: ExtCanvas | undefined, mask: ExtCanvas | undefined): ExtCanvas | undefined; export function maskCanvas(canvas: ExtCanvas | undefined, mask: ExtCanvas | undefined) { - if (!canvas || !mask) - return undefined; + if (!canvas || !mask) + return undefined; - const result = createExtCanvas(canvas.width, canvas.height, `${canvas.info} masked by ${mask.info}`); - const context = result.getContext('2d')!; - context.drawImage(canvas, 0, 0); - context.globalCompositeOperation = 'destination-in'; - context.drawImage(mask, 0, 0); - return result; + const result = createExtCanvas(canvas.width, canvas.height, `${canvas.info} masked by ${mask.info}`); + const context = result.getContext('2d')!; + context.drawImage(canvas, 0, 0); + context.globalCompositeOperation = 'destination-in'; + context.drawImage(mask, 0, 0); + return result; } export function colorCanvas(canvas: ExtCanvas, color: string): ExtCanvas; export function colorCanvas(canvas: ExtCanvas | undefined, color: string): ExtCanvas | undefined; export function colorCanvas(canvas: ExtCanvas | undefined, color: string): ExtCanvas | undefined { - const copy = copyCanvas(canvas); + const copy = copyCanvas(canvas); - if (copy) { - const context = copy.getContext('2d')!; - context.globalCompositeOperation = 'source-in'; - context.fillStyle = color; - context.fillRect(0, 0, copy.width, copy.height); - } + if (copy) { + const context = copy.getContext('2d')!; + context.globalCompositeOperation = 'source-in'; + context.fillStyle = color; + context.fillRect(0, 0, copy.width, copy.height); + } - return copy; + return copy; } export function copyCanvas(canvas: ExtCanvas): ExtCanvas; export function copyCanvas(canvas: ExtCanvas | undefined): ExtCanvas | undefined; export function copyCanvas(canvas: ExtCanvas | undefined): ExtCanvas | undefined { - if (!canvas) - return undefined; + if (!canvas) + return undefined; - const newCanvas = createExtCanvas(canvas.width, canvas.height, `${canvas.info} (copy)`); - newCanvas.getContext('2d')!.drawImage(canvas, 0, 0); - return newCanvas; + const newCanvas = createExtCanvas(canvas.width, canvas.height, `${canvas.info} (copy)`); + newCanvas.getContext('2d')!.drawImage(canvas, 0, 0); + return newCanvas; } export function recolorCanvas(canvas: ExtCanvas, color: string) { - const result = createExtCanvas(canvas.width, canvas.height, `recolorCanvas(${canvas.info}, ${color})`); - const context = result.getContext('2d')!; - context.fillStyle = color; - context.fillRect(0, 0, result.width, result.height); - context.globalCompositeOperation = 'destination-in'; - context.drawImage(canvas, 0, 0); - return result; + const result = createExtCanvas(canvas.width, canvas.height, `recolorCanvas(${canvas.info}, ${color})`); + const context = result.getContext('2d')!; + context.fillStyle = color; + context.fillRect(0, 0, result.width, result.height); + context.globalCompositeOperation = 'destination-in'; + context.drawImage(canvas, 0, 0); + return result; } export function createColorCanvas(width: number, height: number, color: string) { - const canvas = createExtCanvas(width, height, `createColorCanvas(${color})`); - const context = canvas.getContext('2d')!; - context.fillStyle = color; - context.fillRect(0, 0, canvas.width, canvas.height); - return canvas; + const canvas = createExtCanvas(width, height, `createColorCanvas(${color})`); + const context = canvas.getContext('2d')!; + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + return canvas; } export function isCanvasEmpty(canvas: ExtCanvas | undefined): boolean { - if (canvas && canvas.width > 0 && canvas.height > 0) { - const context = canvas.getContext('2d')!; - const data = context.getImageData(0, 0, canvas.width, canvas.height); - const size = data.width * data.height * 4; + if (canvas && canvas.width > 0 && canvas.height > 0) { + const context = canvas.getContext('2d')!; + const data = context.getImageData(0, 0, canvas.width, canvas.height); + const size = data.width * data.height * 4; - for (let i = 0; i < size; i++) { - if (data.data[i] !== 0) { - return false; - } - } - } + for (let i = 0; i < size; i++) { + if (data.data[i] !== 0) { + return false; + } + } + } - return true; + return true; } export function saveCanvas(filePath: string, canvas: HTMLCanvasElement) { - fs.writeFileSync(filePath, canvas.toBuffer()); + fs.writeFileSync(filePath, canvas.toBuffer()); } function getColorAt(d: Uint8ClampedArray, i: number) { - return ((d[i] << 24) | (d[i + 1] << 16) | (d[i + 2] << 8) | d[i + 3]) >>> 0; + return ((d[i] << 24) | (d[i + 1] << 16) | (d[i + 2] << 8) | d[i + 3]) >>> 0; } export function forEachPixel(canvas: ExtCanvas, action: (color: number, x: number, y: number) => void) { - const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); - for (let y = 0, i = 0; y < data.height; y++) { - for (let x = 0; x < data.width; x++ , i += 4) { - action(getColorAt(data.data, i), x, y); - } - } + for (let y = 0, i = 0; y < data.height; y++) { + for (let x = 0; x < data.width; x++ , i += 4) { + action(getColorAt(data.data, i), x, y); + } + } } export function forEachPixelOf2Canvases( - canvas1: ExtCanvas, canvas2: ExtCanvas, action: (color1: number, color2: number, x: number, y: number) => void + canvas1: ExtCanvas, canvas2: ExtCanvas, action: (color1: number, color2: number, x: number, y: number) => void ) { - if (canvas1.width !== canvas2.width || canvas1.height !== canvas2.height) { - throw new Error('Canvas not the same size'); - } + if (canvas1.width !== canvas2.width || canvas1.height !== canvas2.height) { + throw new Error('Canvas not the same size'); + } - const data1 = canvas1.getContext('2d')!.getImageData(0, 0, canvas1.width, canvas1.height); - const data2 = canvas2.getContext('2d')!.getImageData(0, 0, canvas2.width, canvas2.height); + const data1 = canvas1.getContext('2d')!.getImageData(0, 0, canvas1.width, canvas1.height); + const data2 = canvas2.getContext('2d')!.getImageData(0, 0, canvas2.width, canvas2.height); - for (let y = 0, i = 0; y < data1.height; y++) { - for (let x = 0; x < data1.width; x++ , i += 4) { - action(getColorAt(data1.data, i), getColorAt(data2.data, i), x, y); - } - } + for (let y = 0, i = 0; y < data1.height; y++) { + for (let x = 0; x < data1.width; x++ , i += 4) { + action(getColorAt(data1.data, i), getColorAt(data2.data, i), x, y); + } + } } type MapColor = (color: number, x: number, y: number) => number; export function mapEachPixel(canvas: ExtCanvas, action: MapColor) { - const context = canvas.getContext('2d')!; - const data = context.getImageData(0, 0, canvas.width, canvas.height); - const d = data.data; + const context = canvas.getContext('2d')!; + const data = context.getImageData(0, 0, canvas.width, canvas.height); + const d = data.data; - for (let y = 0, i = 0; y < data.height; y++) { - for (let x = 0; x < data.width; x++ , i += 4) { - const c = ((d[i] << 24) | (d[i + 1] << 16) | (d[i + 2] << 8) | d[i + 3]) >>> 0; - const out = action(c, x, y); - d[i] = (out >>> 24) & 0xff; - d[i + 1] = (out >>> 16) & 0xff; - d[i + 2] = (out >>> 8) & 0xff; - d[i + 3] = out & 0xff; - } - } + for (let y = 0, i = 0; y < data.height; y++) { + for (let x = 0; x < data.width; x++ , i += 4) { + const c = ((d[i] << 24) | (d[i + 1] << 16) | (d[i + 2] << 8) | d[i + 3]) >>> 0; + const out = action(c, x, y); + d[i] = (out >>> 24) & 0xff; + d[i + 1] = (out >>> 16) & 0xff; + d[i + 2] = (out >>> 8) & 0xff; + d[i + 3] = out & 0xff; + } + } - context.putImageData(data, 0, 0); + context.putImageData(data, 0, 0); } export function mapColors(canvas: ExtCanvas, map: MapColor): ExtCanvas; export function mapColors(canvas: ExtCanvas | undefined, map: MapColor): ExtCanvas | undefined; export function mapColors(canvas: ExtCanvas | undefined, map: MapColor): ExtCanvas | undefined { - const result = copyCanvas(canvas); + const result = copyCanvas(canvas); - if (result) { - mapEachPixel(result, map); - } + if (result) { + mapEachPixel(result, map); + } - return result; + return result; } function compareTemplate(canvas: ImageData, template: ImageData, ox: number, oy: number) { - for (let y = 0; y < template.height; y++) { - for (let x = 0; x < template.width; x++) { - for (let i = 0; i < 4; i++) { - if (canvas.data[i + (x + ox) * 4 + (y + oy) * canvas.width * 4] !== template.data[i + x * 4 + y * template.width * 4]) { - return false; - } - } - } - } + for (let y = 0; y < template.height; y++) { + for (let x = 0; x < template.width; x++) { + for (let i = 0; i < 4; i++) { + if (canvas.data[i + (x + ox) * 4 + (y + oy) * canvas.width * 4] !== template.data[i + x * 4 + y * template.width * 4]) { + return false; + } + } + } + } - return true; + return true; } export function findTemplate(canvas: HTMLCanvasElement, template: HTMLCanvasElement) { - const canvasData = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); - const templateData = template.getContext('2d')!.getImageData(0, 0, template.width, template.height); - const maxX = canvas.width - template.width; - const maxY = canvas.height - template.height; + const canvasData = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + const templateData = template.getContext('2d')!.getImageData(0, 0, template.width, template.height); + const maxX = canvas.width - template.width; + const maxY = canvas.height - template.height; - for (let y = 0; y <= maxY; y++) { - for (let x = 0; x <= maxX; x++) { - if (compareTemplate(canvasData, templateData, x, y)) { - return { x, y }; - } - } - } + for (let y = 0; y <= maxY; y++) { + for (let x = 0; x <= maxX; x++) { + if (compareTemplate(canvasData, templateData, x, y)) { + return { x, y }; + } + } + } - return null; + return null; } export function offsetCanvas(canvas: ExtCanvas | undefined, { x, y }: Point) { - return canvas && padCanvas(canvas, x, y); + return canvas && padCanvas(canvas, x, y); } export type CanvasGetter = (Canvas: ExtCanvas, col: number, row: number) => ExtCanvas; export type ByIndexGetter = (Canvas: ExtCanvas, index: number) => ExtCanvas; export function cropAndPadByColRow( - x: number, y: number, w: number, h: number, dx: number, dy: number, padLeft = 0, padTop = 0 + x: number, y: number, w: number, h: number, dx: number, dy: number, padLeft = 0, padTop = 0 ): CanvasGetter { - return (canvas, col, row) => padCanvas(cropCanvas(canvas, x + dx * col, y + dy * row, w, h), padLeft, padTop); + return (canvas, col, row) => padCanvas(cropCanvas(canvas, x + dx * col, y + dy * row, w, h), padLeft, padTop); } export function cropByIndex(get: CanvasGetter, perLine: number): ByIndexGetter { - return (canvas, i) => get(canvas, i % perLine, Math.floor(i / perLine)); + return (canvas, i) => get(canvas, i % perLine, Math.floor(i / perLine)); } diff --git a/src/ts/tools/common.ts b/src/ts/tools/common.ts index a3063df..6e8560a 100644 --- a/src/ts/tools/common.ts +++ b/src/ts/tools/common.ts @@ -26,25 +26,25 @@ export const TEETH_SHADE_COLOR = 0x77d9d9ff; // TEMP: remove after adding palettes for effects const holdPoofColors = [ - 0xffff47ff, 0xeaed58ff, 0xd8dc00ff, 0xff6741ff, 0xff0000ff, 0xff7af9ff, 0xff00ccff, 0xb67affff, - 0x9876ffff, 0x76a6ffff, 0x0097ffff, 0x00ff9bff, 0x76ed5cff, 0x28dc00ff, 0x76ed5cff, + 0xffff47ff, 0xeaed58ff, 0xd8dc00ff, 0xff6741ff, 0xff0000ff, 0xff7af9ff, 0xff00ccff, 0xb67affff, + 0x9876ffff, 0x76a6ffff, 0x0097ffff, 0x00ff9bff, 0x76ed5cff, 0x28dc00ff, 0x76ed5cff, ]; export const defaultPalette = [ - TRANSPARENT, WHITE, BLACK, MOUTH_COLOR, TONGUE_COLOR, LIGHT_SHADE_COLOR, TEAR_COLOR, DARK_GRAY, - ...holdPoofColors, + TRANSPARENT, WHITE, BLACK, MOUTH_COLOR, TONGUE_COLOR, LIGHT_SHADE_COLOR, TEAR_COLOR, DARK_GRAY, + ...holdPoofColors, ]; export function cartesian(a: A[], b: B[]): [A, B][]; export function cartesian(a: A[], b: B[], c: C[]): [A, B, C][]; export function cartesian(...args: any[]) { - return reduce(args, (a, b) => flatten(map(a, x => map(b as any, y => x.concat([y])))), [[]]); + return reduce(args, (a, b) => flatten(map(a, x => map(b as any, y => x.concat([y])))), [[]]); } export function mkdir(dirpath: string) { - try { - fs.mkdirSync(dirpath); - } catch { } + try { + fs.mkdirSync(dirpath); + } catch { } } const isDirectory = (dir: string) => fs.lstatSync(dir).isDirectory(); @@ -52,92 +52,92 @@ const isDirectory = (dir: string) => fs.lstatSync(dir).isDirectory(); export const getDirectories = (dir: string) => fs.readdirSync(dir).map(name => path.join(dir, name)).filter(isDirectory); function findLayerByPath([name, ...child]: string[], layer: Layer | Psd | undefined): Layer | undefined { - return name ? findLayerByPath(child, layer && findByName(layer.children, name)) : layer; + return name ? findLayerByPath(child, layer && findByName(layer.children, name)) : layer; } export function findLayer(path: string, layer: Layer | Psd | undefined): Layer | undefined { - return findLayerByPath(path.split('/'), layer); + return findLayerByPath(path.split('/'), layer); } export function findLayerSafe(name: string, parent: Layer | Psd): Layer { - const layer = findLayer(name, parent); + const layer = findLayer(name, parent); - if (!layer) { - throw new Error(`Missing layer "${name}" in "${parent.info}"`); - } + if (!layer) { + throw new Error(`Missing layer "${name}" in "${parent.info}"`); + } - return layer; + return layer; } export function findByName(items: T[], name: string): T | undefined { - return items.find(i => i.name === name); + return items.find(i => i.name === name); } export function findByIndex(items: T[], index: number) { - return items.find(i => i.index === index); + return items.find(i => i.index === index); } export const nameMatches = (regex: RegExp) => (l: { name: string; }) => regex.test(l.name); export function compareNames(a: { name: string }, b: { name: string }) { - return a.name.localeCompare(b.name); + return a.name.localeCompare(b.name); } export const time = (function () { - const start = Date.now(); - let last = start; + const start = Date.now(); + let last = start; - return function (text: string) { - console.log(text, (Date.now() - last), 'ms'); - last = Date.now(); - return true; - }; + return function (text: string) { + console.log(text, (Date.now() - last), 'ms'); + last = Date.now(); + return true; + }; })(); export function spawnAsync(command: string, args?: string[]) { - return new Promise((resolve, reject) => { - spawn(command, args) - .on('error', (err: Error) => reject(err)) - .on('exit', (code: number) => code === 0 ? resolve() : reject(new Error(`Non-zero return code for ${command} (${code})`))); - }); + return new Promise((resolve, reject) => { + spawn(command, args) + .on('error', (err: Error) => reject(err)) + .on('exit', (code: number) => code === 0 ? resolve() : reject(new Error(`Non-zero return code for ${command} (${code})`))); + }); } // canvas export function getCanvas(layer: Layer | undefined): ExtCanvas | undefined { - if (!layer) - return undefined; + if (!layer) + return undefined; - const canvas = layer.canvas; + const canvas = layer.canvas; - if (canvas) { - canvas.info = layer.info; - } + if (canvas) { + canvas.info = layer.info; + } - return canvas; + return canvas; } export function getCanvasSafe(layer: Layer): ExtCanvas { - const canvas = getCanvas(layer); + const canvas = getCanvas(layer); - if (!canvas) { - throw new Error(`Cannot find canvas in layer "${layer.info}"`); - } + if (!canvas) { + throw new Error(`Cannot find canvas in layer "${layer.info}"`); + } - return canvas; + return canvas; } export function getLayerCanvas(name: string, parent: Layer | Psd | undefined) { - return getCanvas(findLayer(name, parent)); + return getCanvas(findLayer(name, parent)); } export function getLayerCanvasSafe(name: string, parent: Layer | Psd): ExtCanvas { - return getCanvasSafe(findLayerSafe(name, parent)); + return getCanvasSafe(findLayerSafe(name, parent)); } export function parseWithNumber(name: string) { - const match = /(\d+)/.exec(name); - return parseInt(match ? match[1] : '0', 10); + const match = /(\d+)/.exec(name); + return parseInt(match ? match[1] : '0', 10); } export const matcher = (regex: RegExp) => (text: string) => regex.test(text); @@ -146,7 +146,7 @@ const isArrayEmpty = (a: T[] | null) => !a || a.length === 0; const nullForEmpty = (a: T[] | null) => isArrayEmpty(a) ? null : a; export function trimRight(items: ((T | null)[] | null)[]): ((T | null)[] | null)[] { - return dropRightWhile(items.map(nullForEmpty) as any, isArrayEmpty as any) as any; + return dropRightWhile(items.map(nullForEmpty) as any, isArrayEmpty as any) as any; } // sprites @@ -154,60 +154,60 @@ export function trimRight(items: ((T | null)[] | null)[]): ((T | null)[] | nu const redCanvas = createColorCanvas(1000, 1000, 'red'); export function addImage(images: HTMLCanvasElement[], canvas: HTMLCanvasElement) { - if (canvas) { - // TODO: remove duplicated - images.push(canvas); - return images.length - 1; - } else { - return 0; - } + if (canvas) { + // TODO: remove duplicated + images.push(canvas); + return images.length - 1; + } else { + return 0; + } } export function createSprite(index: number, image: HTMLCanvasElement, { w, h, x, y }: Rect) { - return { index, image, w, h, x: 0, y: 0, ox: x, oy: y }; + return { index, image, w, h, x: 0, y: 0, ox: x, oy: y }; } const maxSpriteWidth = 500; const maxSpriteHeight = 500; export function addSprite( - sprites: Sprite[], canvas?: ExtCanvas, pattern?: ExtCanvas, palette?: number[], out: ColorsOutput = {} + sprites: Sprite[], canvas?: ExtCanvas, pattern?: ExtCanvas, palette?: number[], out: ColorsOutput = {} ): number { - if (canvas) { - const rect = getSpriteRect(canvas, 0, 0, canvas.width, canvas.height); + if (canvas) { + const rect = getSpriteRect(canvas, 0, 0, canvas.width, canvas.height); - if (rect.w && rect.h) { - if (rect.w > maxSpriteWidth || rect.h > maxSpriteHeight) { - throw new Error(`Sprite too large (${rect.w}, ${rect.h}) from [${canvas.info}]`); - } + if (rect.w && rect.h) { + if (rect.w > maxSpriteWidth || rect.h > maxSpriteHeight) { + throw new Error(`Sprite too large (${rect.w}, ${rect.h}) from [${canvas.info}]`); + } - const image = imageToPalette(rect, canvas, pattern || redCanvas, palette, out); - sprites.push(createSprite(sprites.length, image, rect)); - return sprites.length - 1; - } - } + const image = imageToPalette(rect, canvas, pattern || redCanvas, palette, out); + sprites.push(createSprite(sprites.length, image, rect)); + return sprites.length - 1; + } + } - return 0; + return 0; } export function addSpriteWithColors( - sprites: Sprite[], colorImage?: ExtCanvas, patternImage?: ExtCanvas, forceWhite?: boolean + sprites: Sprite[], colorImage?: ExtCanvas, patternImage?: ExtCanvas, forceWhite?: boolean ): ColorExtra { - const out: ColorsOutput = { forceWhite }; - const color = addSprite(sprites, colorImage, patternImage, undefined, out); - return { color, colors: out.colors! }; + const out: ColorsOutput = { forceWhite }; + const color = addSprite(sprites, colorImage, patternImage, undefined, out); + return { color, colors: out.colors! }; } export function getColorsCount(colorImage?: ExtCanvas, patternImage?: ExtCanvas, forceWhite?: boolean): number { - const out: ColorsOutput = { forceWhite }; - addSprite([], colorImage, patternImage, undefined, out); - return out.colors!; + const out: ColorsOutput = { forceWhite }; + addSprite([], colorImage, patternImage, undefined, out); + return out.colors!; } export function createPixelSprites({ objects, objects2, images, sprites }: Result) { - const pixel = createColorCanvas(3, 3, 'white'); - objects['pixelRect'] = addImage(images, pixel); - objects2['pixelRect2'] = addSprite(sprites, pixel, undefined, defaultPalette); + const pixel = createColorCanvas(3, 3, 'white'); + objects['pixelRect'] = addImage(images, pixel); + objects2['pixelRect2'] = addSprite(sprites, pixel, undefined, defaultPalette); } // layers @@ -215,20 +215,20 @@ export function createPixelSprites({ objects, objects2, images, sprites }: Resul export const compareLayers = (a: Layer, b: Layer) => parseWithNumber(a.name) - parseWithNumber(b.name); export function getPatternLayers(layer: Layer) { - return layer.children.filter(nameMatches(/^pattern/)).sort(compareLayers); + return layer.children.filter(nameMatches(/^pattern/)).sort(compareLayers); } export function getPatternCanvases(layer: Layer): ExtCanvas[] { - const canvases = getPatternLayers(layer).map(getCanvas); - return dropRightWhile(canvases, isCanvasEmpty) as ExtCanvas[]; + const canvases = getPatternLayers(layer).map(getCanvas); + return dropRightWhile(canvases, isCanvasEmpty) as ExtCanvas[]; } export function clipPattern(color: ExtCanvas, pattern: ExtCanvas | undefined): ExtCanvas | undefined { - if (pattern) { - const ctx = pattern.getContext('2d')!; - ctx.globalCompositeOperation = 'destination-in'; - ctx.drawImage(color, 0, 0); - } + if (pattern) { + const ctx = pattern.getContext('2d')!; + ctx.globalCompositeOperation = 'destination-in'; + ctx.drawImage(color, 0, 0); + } - return pattern; + return pattern; } diff --git a/src/ts/tools/convert-tiles.ts b/src/ts/tools/convert-tiles.ts index d1d0114..8a69816 100644 --- a/src/ts/tools/convert-tiles.ts +++ b/src/ts/tools/convert-tiles.ts @@ -6,36 +6,36 @@ const tileHeight = 24; const cols = 10; const tiles = [ - 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, + 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, ]; interface Rev { - number: number; - index: number; - dstIndex: number; + number: number; + index: number; + dstIndex: number; } const revtiles: Rev[] = tiles - .map((number, index) => ({ number, index, dstIndex: 0 })) - .filter(x => x.number != null) as Rev[]; + .map((number, index) => ({ number, index, dstIndex: 0 })) + .filter(x => x.number != null) as Rev[]; revtiles.sort((a, b) => a.number - b.number); revtiles.forEach((t, i) => t.dstIndex = i); export function tilesToSprites(canvas: ExtCanvas, spaceH: number, spaceV: number) { - return revtiles - .sort((a, b) => a.dstIndex - b.dstIndex) - .map(t => { - const srcIndex = t.index; - const srcCol = srcIndex % cols; - const srcRow = Math.floor(srcIndex / cols); - const srcX = spaceH + srcCol * (tileWidth + spaceH); - const srcY = spaceV + srcRow * (tileHeight + spaceV); - return cropCanvas(canvas, srcX, srcY, tileWidth, tileHeight); - }); + return revtiles + .sort((a, b) => a.dstIndex - b.dstIndex) + .map(t => { + const srcIndex = t.index; + const srcCol = srcIndex % cols; + const srcRow = Math.floor(srcIndex / cols); + const srcX = spaceH + srcCol * (tileWidth + spaceH); + const srcY = spaceV + srcRow * (tileHeight + spaceV); + return cropCanvas(canvas, srcX, srcY, tileWidth, tileHeight); + }); } diff --git a/src/ts/tools/create-font.ts b/src/ts/tools/create-font.ts index 641576c..696f9cb 100644 --- a/src/ts/tools/create-font.ts +++ b/src/ts/tools/create-font.ts @@ -3,383 +3,383 @@ import { compact } from 'lodash'; import { ExtCanvas } from './types'; export const CHINESE = [ - '一丁七万丈三上下不与丐丑专且丕世丘丙业丛东丝丞両丢两严並丧个丫中', - '丰串临丸丹为主丼丽举乃久么义之乌乍乎乏乐乒乓乔乖乗乘乙九乞也习乡', - '书买乱乳乾亀了予争事二于亏云互五井亘亙亚些亜亞亟亡亢交亥亦产亨亩', - '享京亭亮亲亵人亿什仁仅仆仇今介仍从仏仑仓仔仕他仗付仙代令以仪们仮', - '仰仲件价任份仿企伊伍伎伏伐休众优伙会伝伞伟传伢伤伦伪伯估伴伶伸伺', - '似伽佃但位低住佐佑体何余佚佛作佟你佣佩佬佯佳併佼使侃侄來侈例侍侏', - '侑供依侠価侣侥侦侧侨侬侮侯侵侶便係促俄俊俏俐俗俘保俞俟俠信俣俨俩', - '俭修俯俱俳俵俸俺俾倉個倍倏倒倔倖倘候倚借倡倣値倦倩倪倫倭倶倹债值', - '倾假偉偌偎偏偕做停健偲側偵偶偷偽偿傀傅傍傑傘備傣储傩催傭傲傳債傷', - '傻傾僅働像僑僕僚僞僧僭僵價僻儀億儉儒償儡優儲儿兀允元兄充兆先光克', - '免兎児兑兒兔党兜兢入全八公六兮兰共关兴兵其具典兹养兼兽冀内円冈冉', - '冊册再冒冕冗写军农冠冢冤冥冨冬冯冰冲决冴况冶冷冻净凄准凉凋凌凍减', - '凑凛凜凝几凡凤処凧凪凭凯凰凱凳凶凸凹出击函凿刀刁刃分切刈刊刑划列', - '刘则刚创初删判別刨利别刮到制刷券刹刺刻刽剂剃則削剌前剑剔剖剛剣剤', - '剥剧剩剪副剰割創剿劇劈劉劍力劝办功加务劣动助努劫励劲劳労効劾势勁', - '勃勅勇勉勋勒動勘務勝募勢勤勧勲勳勺勾勿匀匁匂包匆匈匕化北匙匠匡匣', - '匪匮匹区医匾匿十千升午卉半华协卑卒卓協单卖南単博卜占卡卢卤卦卧卫', - '卯印危即却卵卷卸卿厂厄厅历厉压厌厕厘厚原厢厥厦厨厩厮厳去县参又叉', - '及友双反収发叔取受变叙叛叟叠叡叢口古句另叨叩只叫召叭叮可台叱史右', - '叶号司叹叼叽吁吃各吆合吉吊同名后吏吐向吓吕吗君吝吞吟吠否吧吨吩含', - '听吭吮启吱吴吵吸吹吻吼吾呀呂呃呆呈呉告呐呕呗员呛呜呟呢周呪呱味呵', - '呸呻呼命咀咄咆咋和咎咏咐咒咔咕咖咙咚咦咧咨咪咬咯咱咲咳咸咽哀品哄', - '哆哇哈哉响哎哑哒哗哝哟員哥哦哧哨哩哪哭哮哲哺哼哽唄唆唇唉唐唔唠唤', - '唧唬售唯唱唷唾啃啄商啊問啓啡啤啥啦啧啪啬啸啼喀喂喃善喇喉喊喋喔喘', - '喙喚喜喝喧喩喪喫喬單喰喳営喷喻喽嗅嗒嗓嗔嗖嗜嗟嗡嗣嗤嗦嗨嗯嗳嗽嘀', - '嘆嘈嘉嘎嘗嘘嘛嘟嘩嘱嘲嘴嘶嘻嘿噂噌噎噗噛噜噢器噩噪噬噴噶嚇嚎嚏嚓', - '嚣嚴嚷嚼囊囚四回因团団园困囱囲図围固国图圃圆圈國圏園圓團土圣圧在', - '圭地圳场圾址坂均坊坍坎坏坐坑块坚坛坝坞坟坠坡坤坦坪坯坷垂垃垄型垒', - '垢垣垦垫垮埃埋城埔埜域埠埴執培基埼堀堂堅堆堑堕堡堤堪堯堰報場堵堺', - '塀塁塊塌塑塔塗塘塙塚塞塩填塾境墅墓増墙墜增墟墨墩墳墾壁壇壊壌壑壕', - '壘壞壤士壬壮壯声壱売壳壶壷壽处备変复夏夕外多夜够夢大天太夫夭央失', - '头夷夸夹夺奂奄奇奈奉奋奎奏契奔奕奖套奚奠奢奥奧奨奪奬奮女奴奶奸她', - '好如妃妄妆妇妈妊妒妓妖妙妞妥妨妬妮妹妻妾姆姉姊始姐姑姓委姗姚姜姥', - '姦姨姪姫姬姻姿威娃娅娇娓娘娜娟娠娥娩娯娱娴娶娼婆婉婚婢婦婪婴婵婶', - '婷婿媒媚媛媲媳嫁嫂嫉嫌嫔嫖嫡嫣嫦嫩嬉嬢孃子孔孕字存孙孚孜孝孟季孤', - '学孩孫孰孵孽宁它宅宇守安宋完宏宕宗官宙定宛宜宝实実宠审客宣室宥宦', - '宪宫宮宰害宴宵家容宽宾宿寂寄寅密寇富寐寒寓寛寝寞察寡寢寥實寧寨審', - '寮寰寵寸对寺寻导対寿封専射将將專尉尊尋導小少尔尖尘尚尝尤尧尬尭就', - '尴尸尹尺尻尼尽尾尿局屁层居屈屉届屋屎屏屑展属屠屡層履屯山屹屿岁岂', - '岐岑岔岗岚岛岡岩岬岭岳岸峙峠峡峦峨峭峯峰島峻峽崇崎崔崖崚崛崩崭崽', - '嵋嵌嵐嵩嵯嶋嶺巅巌巍巖川州巡巢巣工左巧巨巩巫差己已巳巴巷巻巽巾币', - '市布帅帆师希帐帕帖帘帚帛帜帝帥带帧師席帮帯帰帳帶帷常帽幅幌幔幕幡', - '幢幣干平年并幸幹幻幼幽幾广庁広庄庆庇床序庐库应底店庙庚府庞废度座', - '庫庭庵庶康庸廃廉廊廓廖廟廣廳延廷建廻廿开弁异弃弄弊式弐弓弔引弗弘', - '弛弟张弥弦弧弩弯弱張強弹强弼弾彈彌归当录彗彙彝形彦彩彪彫彬彭彰影', - '彷役彻彼往征径待很徊律後徐徒従得徘徙從徠御復循微徳徴德徹徽心必忆', - '忌忍忏忒忖志忘忙応忠忡忧快忱念忽忿怀态怂怅怎怒怔怕怖怜思怠怡急怦', - '性怨怪怯总恃恆恋恍恐恒恕恙恚恢恣恤恥恨恩恪恬恭息恰恳恵恶恺恻恼恿', - '悄悉悌悍悔悖悚悟悠患悦您悩悪悬悯悲悴悸悻悼情惇惊惋惑惕惘惚惜惟惠', - '惡惣惦惧惨惩惫惬惭惮惯惰想惴惶惹惺愁愈愉意愕愚愛感愣愤愧愫愼愿慄', - '慈態慌慎慑慕慢慣慧慨慮慰慶慷憂憋憎憐憔憚憤憧憨憩憬憲憶憾懂懇懈應', - '懊懐懒懦懲懵懷懸懿戈戊戌戍戎戏成我戒或战戚戟戦截戮戯戰戲戳戴户戸', - '戻房所扁扇扈扉手才扎扑扒打扔払托扛扣执扩扫扬扭扮扯扰扱扳扶批扼找', - '承技抄抉把抑抒抓投抖抗折抚抛抜択抡抢护报抨披抬抱抵抹押抽抿拂担拆', - '拇拈拉拌拍拎拐拒拓拔拖拗拘拙拚招拜拝拟拠拡拢拣拥拦拧拨择括拭拯拱', - '拳拴拶拷拼拽拾拿持挂指按挑挖挙挚挛挝挟挠挡挣挤挥挨挪挫振挺挽挿捂', - '捅捆捉捍捎捏捐捕捗捜捞损捡换捣捧捨据捲捶捷捺捻掀掃授掉掌掏掐排掖', - '掘掛掠採探掣接控推掩措掬掲掳掴掷掺掻揃揉揍描提插揖揚換握揣揩揪揭', - '揮援揺揽搀搁搂搅損搏搐搓搔搖搜搞搬搭携搾摂摄摆摇摊摑摒摔摘摧摩摯', - '摸摹摺撂撃撇撑撒撕撚撞撤撩撫撬播撮撰撲撵撸撼擁擂擅擊操擎擒擞擢擦', - '擬攀攒攘攝攥攫支收攸改攻放政故效敌敍敏救敕敖敗教敛敝敞敢散敦敬数', - '敲整敵敷文斉斋斌斎斐斑斓斗料斜斟斡斤斥斧斩斬断斯新方於施旁旅旋族', - '旗无既日旦旧旨早旬旭旱时旷旺昂昆昇昊昌明昏易昔星映春昧昨昭是昴昼', - '显時晃晄晋晌晏晒晓晕晖晚晝晟晤晦晨晩普景晰晴晶智晾暁暂暄暇暉暑暖', - '暗暢暦暧暨暫暮暴曇曉曖曙曜曝曦曰曲曳更書曹曼曽曾替最月有朋服朔朕', - '朗望朝期朦木未末本札术朱朴朵机朽杀杂权杆杉李杏材村杖杜杞束杠条来', - '杨杭杯杰東杵杷松板极构枇枉析枕林枚果枝枠枢枣枪枫枭枯架枷柄柊柏某', - '柑染柔柘柚柜查柬柯柱柳柴柵査柾柿栃栄栅标栈栋栏树栓栖栗栞校栩株样', - '核根格栽桁桂桃桅框案桌桐桑桓桔桜桟桢档桥桦桧桨桩桶梁梅梓梗梛條梢', - '梦梧梨梭梯械梳梵梶检棄棉棋棍棒棕棘棚棟棠森棱棲棵棺椀椅椋植椎椒椛', - '検椭椰椿楊楓楔楕楚楞楠楢業楯極楷楼楽概榄榆榈榊榎榛榜榨榮榴榻槇構', - '槌槍槐様槙槛槻槽樂樊樋標樟模樣権横樫樱樵樹樺樽橄橋橘橙機橡橱檀檎', - '檐檜檢檬櫂櫓櫛櫻欄欠次欢欣欤欧欲欺欽款歇歉歌歎歓止正此步武歧歩歪', - '歯歳歴歹死歼殃殆殉殊残殖殡殴段殷殺殻殿毁毅毋母毎每毒毓比毕毗毘毙', - '毛毡毫毬毯氏民氓气氖気氛氟氢氣氦氧氨氮氯水氷永氾汀汁求汇汉汎汐汕', - '汗汚汛汝汞江池污汤汪汰汲汶汹決汽汾沁沂沃沈沉沌沐沓沖沙沛沟没沢沥', - '沦沧沪沫沮河沸油治沼沽沾沿況泄泉泊泌法泛泞泡波泣泥注泪泰泳泵泻泼', - '泽洁洋洒洗洛洞津洪洲洵洸活洼洽派流浄浅浆浇浊测济浏浑浒浓浙浚浜浦', - '浩浪浬浮浴海浸涂涅消涉涌涎涕涙涛涟涡涣涤润涧涨涩涯液涵涸涼淀淄淆', - '淋淌淑淖淘淡淤淨淫淮深淳淵混淹添清渇済渉渊渋渍渎渐渓渔渗渚減渝渠', - '渡渣渤渥渦温測渭港渲渴游渺湃湄湊湍湖湘湛湧湯湾湿満溃溅溉源準溜溝', - '溢溥溪溯溶溺滅滇滉滋滑滔滕滚滝滞满滤滥滨滩滯滴漁漂漆漉漏漓演漕漠', - '漢漣漩漪漫漬漱漳漸漾潇潔潘潜潟潤潦潭潮潰澁澄澈澎澜澡澪澱澳激濁濃', - '濒濕濠濡濫濯瀑瀕瀚瀛瀧瀬灌灘火灭灯灰灵灶灸灼災灾灿炉炊炎炒炕炖炙', - '炫炬炭炮炯炳炸点為炼炽烁烂烈烏烘烙烛烟烤烦烧烫烬热烹烽焉焕焘焙焚', - '無焦焰然焼煉煌煎煙煜煞煤照煩煮煽熄熊熏熔熙熟熬熱熹燃燈燎燒燕燥燦', - '燭燿爆爪爬爭爱爲爵父爷爸爹爽爾片版牌牒牙牛牟牡牢牧物牲牵特牺牽犀', - '犁犊犠犬犯状犹狂狄狈狐狒狗狙狞狠狡狩独狭狮狱狸狹狼猎猓猖猛猜猝猟', - '猩猪猫献猴猶猾猿獄獅獗獣獲獸玄率玉王玑玖玛玩玫环现玲玺玻珀珂珈珊', - '珍珑珠珥班現球琅理琉琏琐琛琢琥琦琪琳琴琵琶琼瑕瑙瑚瑛瑜瑞瑟瑠瑰瑳', - '瑶瑾璃璇璋璐璞璧環璽瓜瓢瓣瓦瓮瓶瓷甄甘甚甜生産甥用甩甫甭田由甲申', - '电男甸町画畅界畏畑畔留畜畝畠畢略番異畳畴畸畿疆疊疋疎疏疑疗疙疚疡', - '疣疤疫疮疯疲疵疹疼疾病症痉痊痒痔痕痘痛痢痩痪痰痴痹瘍瘟瘢瘤瘦瘩瘪', - '瘫瘸瘾療癌癒癖癫発登白百皂的皆皇皈皋皐皑皓皖皮皱皿盃盅盆盈益盎盏', - '盐监盒盔盖盗盘盛盜盟盡監盤目盯盲直相盼盾省眈眉看県眞真眠眨眩眯眶', - '眷眸眺眼着睁睐睑睛睡督睦睨睫睬睹睽睾睿瞄瞅瞎瞒瞟瞥瞧瞩瞪瞬瞭瞰瞳', - '瞻瞿矗矛矜矢矣知矩矫短矮矯石矶矿码砂砌砍研砕砖砥砦砧砰砲破砸砾础', - '硅硕硝硫硬确硯碁碉碌碍碎碑碓碗碘碟碧碩碰碱碳確碾磁磅磊磋磐磕磨磯', - '磷磺礁礎示礼社祀祁祇祈祉祐祕祖祝神祟祠祢祥票祭祯祷祸祺祿禀禁禄禅', - '禍禎福禧禪禮禰禱禹禺离禽禾秀私秃秉秋种科秒秘租秤秦秧秩积称秸移秽', - '稀程稍税稔稚稜稟稠稣種稲稳稷稻稼稽稿穀穂穆積穏穗穣穫穰穴究穷穹空', - '穿突窃窄窍窑窒窓窖窗窘窜窝窟窥窦窪窮窯窺窿立竖站竜竞竟章竣童竪竭', - '端競竹竺竿笃笆笈笋笑笔笙笛笠符笨第笹笺笼筆筈等筋筏筐筑筒答策筛筝', - '筵筷筹签简箇箋箍箔箕算管箫箭箱箸節範篇築篠篡篤篮篱篷簇簌簡簧簸簾', - '簿籍籠米类籽籾粉粋粒粗粘粛粟粤粥粧粪粮粱粹精糊糕糖糙糜糟糧糸系糾', - '紀約紅紊紋納紐純紗紘紙級紛素紡索紧紫紬累細紳紹紺終絃組絆経結絞絡', - '絢給絮統絵絶絹継続綜維綱網綴綸綺綻綾綿緊緋総緑緒線締編緩緯練緻縁', - '縄縛縞縣縦縫縮縱績繁繊繍織繕繡繫繭繰纂纏纖纠红纤约级纪纬纯纱纲纳', - '纵纶纷纸纹纺纽线练组绅细织终绊绍绎经绑绒结绕绘给绚绛络绝绞统绢绣', - '绥继绩绪绫续绮绯绰绳维绵绷绸综绽绿缀缄缅缆缉缎缓缔缕编缘缚缝缠缤', - '缩缪缭缮缰缴缶缸缺罐网罕罗罚罡罢罩罪置罰署罵罷羁羅羊羌美羔羚羞羡', - '群羨義羲羹羽翁翅翌翎習翔翘翟翠翩翰翻翼耀老考者耆而耍耐耕耗耳耶耸', - '耻耽耿聂聆聊聋职联聖聘聚聞聡聪聴職聽聿肃肆肇肉肋肌肖肘肚肛肝肠股', - '肢肤肥肩肪肮肯育肴肺肾肿胀胁胃胆背胎胖胚胜胞胡胤胥胧胰胱胳胴胶胸', - '胺能脂脅脆脇脈脉脊脏脐脑脓脖脚脩脯脱脳脸脹脾腊腋腎腐腑腓腔腕腥腫', - '腮腰腸腹腺腻腾腿膀膊膏膚膛膜膝膨膳膺臀臂臆臓臟臣臥臨自臭至致臻臼', - '舅舆與興舌舍舎舒舔舗舜舞舟航般舰舱舵舶舷船艇艘艦良艰色艳艶艺艾节', - '芋芒芙芜芝芥芦芬芭芯花芳芸芹芽苇苍苏苑苔苗苛苞苟若苦苯英苹苺茂范', - '茄茅茉茎茜茧茨茫茬茵茶茸茹荀荃荆草荐荒荔荘荡荣荧荫药荷荻荼莉莊莎', - '莞莫莱莲获莹莺莽菁菅菇菊菌菓菖菜菩菫華菱菲萃萄萊萌萍萎萝萠萤营萦', - '萧萨萩萬萱落葆葉著葛葡董葦葫葬葱葵葺蒂蒋蒐蒔蒙蒜蒲蒸蒼蓄蓉蓋蓑蓓', - '蓝蓦蓬蓮蔑蔓蔗蔚蔡蔣蔦蔬蔭蔵蔷蔼蔽蕃蕉蕊蕎蕗蕨蕪蕴蕾薄薇薗薙薛薦', - '薩薪薫薬薯藁藉藍藏藐藕藝藤藥藩藻蘇蘑蘭蘸虎虏虐虑虔虚虜虞虫虹虻虽', - '虾蚀蚁蚂蚊蚌蚕蛀蛇蛊蛋蛍蛙蛛蛟蛤蛮蛹蛾蜀蜂蜈蜒蜕蜗蜘蜜蜡蜷蜿蝇蝉', - '蝎蝗蝙蝠蝦蝴蝶螂螃螅融螺蟀蟆蟋蟒蟹蟾蠕蠟蠢血衅衆行衍術衔街衙衛衝', - '衞衡衣补表衫衬衰衷衿袁袄袅袈袋袍袒袖袜被袭袱袴裁裂装裏裔裕裘裙補', - '裝裟裡裤裳裴裸裹製裾褂複褐褒褚褥褪褶襄襖襟襲西要覆覇見規視覗覚覧', - '親観覽见观规觅视览觉觑角解触言訂訃計訊討訓託記訟訣訪設許訳訴診註', - '証詐詔評詞詠詢詣試詩詫詮詰話該詳詹誇誉誌認誓誕誘語誠誤説読誰課誼', - '調諄談請諏諒論諜諦諧諭諮諸諺諾謀謁謂謄謎謙講謝謠謡謹識譜警譬議譲', - '護讃讐讓计订认讥讨让讪训议讯记讲讳讶讷许讹论讼讽设访诀证诃评诅识', - '诈诉诊词诏译试诗诘诙诚诛话诞诠诡询诣该详诧诩诫诬语误诰诱诲说诵请', - '诸诺读诽课谀谁调谅谈谊谋谍谎谏谐谑谒谓谕谙谚谛谜谟谢谣谤谦谨谩谬', - '谭谱谴谷豁豆豊豚象豢豪豫豹貌貝貞負財貢貧貨販貪貫責貯貰貴買貸費貼', - '貿賀賃賄資賊賑賓賛賜賞賠賢賣賦質賭購贈贝贞负贡财责贤败账货质贩贪', - '贫贬购贮贯贰贱贴贵贷贸费贺贻贼贾贿赁赂赃资赋赌赎赏赐赓赔赖赘赚赛', - '赞赠赡赢赣赤赦赫走赳赴赵赶起趁超越趋趟趣足趴趾跃跄跋跌跑跚跛距跟', - '跡跤跨跪路跳践跷跺踉踊踌踏踝踞踢踩踪踱踵蹂蹄蹈蹊蹋蹑蹙蹟蹦蹬蹭蹲', - '蹴蹿躁躇躍躏身躬躯躲躺車軌軍軒軟転軸軽較載輔輝輩輪輯輸輿轄轉轍轟', - '车轧轨轩转轮软轰轲轴轻轼载轿较辄辅辆辈辉辐辑输辕辖辗辘辙辛辜辞辟', - '辣辨辩辫辰辱農边辺辻込辽达辿迁迂迄迅过迈迎运近返还这进远违连迟迢', - '迥迦迪迫迭述迷迸迹追退送适逃逆选逊逍透逐递逓途逗這通逛逝逞速造逢', - '連逮週進逵逸逻逼逾遁遂遅遇遊運遍過遏遐道達違遗遙遜遠遡遣遥適遭遮', - '遵遷選遺遼遽避邀邃還邑邓邢那邦邪邮邯邱邵邸邹邻郁郊郎郑郝郡部郭郵', - '郷都鄂鄙鄭酉酋酌配酎酒酔酝酢酣酥酪酬酮酱酵酶酷酸酿醇醉醋醍醐醒醜', - '醤醬醸釀采釈釉释里重野量金釘釜針釣釧鈍鈴鉄鉛鉢鉱鉴銀銃銅銑銘銚銭', - '鋒鋭鋳鋸鋼錆錐錘錠錦錫錬錯録鍋鍛鍬鍵鎌鎖鎧鎭鎮鏡鐘鑄鑑鑫针钉钊钓', - '钗钙钝钞钟钠钡钢钥钦钧钩钮钱钳钵钻钾铀铁铃铅铎铐铜铝铢铭铮铲银铸', - '铺链销锁锄锅锈锋锌锐错锚锡锢锣锤锥锦键锯锵锻镀镇镑镖镜镯镰镶長长', - '門閃閉開閏閑間関閣閤閥閲闇闘门闪闭问闯闲间闵闷闸闹闺闻闽阀阁阅阉', - '阎阐阑阔阙阜队阪阮阱防阳阴阵阶阻阿陀附际陆陇陈陋陌降限陕陛陡院陣', - '除陥陨险陪陰陳陵陶陷陸険陽隅隆隈隊隋階随隐隔隘隙際障隠隣隧險隶隷', - '隻隼难雀雁雄雅集雇雌雍雏雑雕雛雜離難雨雪雫雯雰雲雳零雷電雾需霄霆', - '震霉霊霍霎霓霖霜霞霧露霸霹青靖静靜非靠靡面革靳靴靶鞄鞅鞋鞍鞘鞠鞭', - '韓韦韧韩韬音韵韶韻響頁頂頃項順須頌預頑頒頓頗領頬頭頰頻頼題額顎顔', - '顕願顛類顧顯页顶顷项顺须顼顽顾顿颁颂预颅领颇颈颉颊颌颍颐频颓颖颗', - '题颚颜额颠颤風颯风飘飛飜飞食飢飯飲飼飽飾餅養餌餐餓館饗饥饬饭饮饰', - '饱饲饵饶饷饺饼饿馅馆馈馋馒首香馨馬馳馴駄駅駆駈駐駒駕駿騎騒験騰騷', - '驍驗驚马驭驮驯驰驱驳驴驶驹驻驼驾驿骂骄骆骇验骏骑骗骚骡骤骥骨骷骸', - '骼髄髅髓高髦髪髭髮鬓鬱鬼魁魂魄魅魏魔魚魯鮎鮮鯉鯛鯨鰯鱈鱒鱗鱼鲁鲍', - '鲜鲤鲨鲸鳃鳄鳌鳍鳖鳞鳥鳩鳳鳴鳶鴨鴻鵜鵬鶏鶴鷄鷗鷲鷹鷺鸟鸠鸡鸣鸥鸦', - '鸭鸯鸳鸽鸾鸿鹃鹅鹉鹊鹏鹤鹦鹰鹳鹿麒麓麗麟麦麹麺麻麽麿黄黎黏黑黒黔', - '默黙黛黝黯鼎鼓鼠鼻鼾齊齐齢齿龄龈龋龍龙龚龟 ', + '一丁七万丈三上下不与丐丑专且丕世丘丙业丛东丝丞両丢两严並丧个丫中', + '丰串临丸丹为主丼丽举乃久么义之乌乍乎乏乐乒乓乔乖乗乘乙九乞也习乡', + '书买乱乳乾亀了予争事二于亏云互五井亘亙亚些亜亞亟亡亢交亥亦产亨亩', + '享京亭亮亲亵人亿什仁仅仆仇今介仍从仏仑仓仔仕他仗付仙代令以仪们仮', + '仰仲件价任份仿企伊伍伎伏伐休众优伙会伝伞伟传伢伤伦伪伯估伴伶伸伺', + '似伽佃但位低住佐佑体何余佚佛作佟你佣佩佬佯佳併佼使侃侄來侈例侍侏', + '侑供依侠価侣侥侦侧侨侬侮侯侵侶便係促俄俊俏俐俗俘保俞俟俠信俣俨俩', + '俭修俯俱俳俵俸俺俾倉個倍倏倒倔倖倘候倚借倡倣値倦倩倪倫倭倶倹债值', + '倾假偉偌偎偏偕做停健偲側偵偶偷偽偿傀傅傍傑傘備傣储傩催傭傲傳債傷', + '傻傾僅働像僑僕僚僞僧僭僵價僻儀億儉儒償儡優儲儿兀允元兄充兆先光克', + '免兎児兑兒兔党兜兢入全八公六兮兰共关兴兵其具典兹养兼兽冀内円冈冉', + '冊册再冒冕冗写军农冠冢冤冥冨冬冯冰冲决冴况冶冷冻净凄准凉凋凌凍减', + '凑凛凜凝几凡凤処凧凪凭凯凰凱凳凶凸凹出击函凿刀刁刃分切刈刊刑划列', + '刘则刚创初删判別刨利别刮到制刷券刹刺刻刽剂剃則削剌前剑剔剖剛剣剤', + '剥剧剩剪副剰割創剿劇劈劉劍力劝办功加务劣动助努劫励劲劳労効劾势勁', + '勃勅勇勉勋勒動勘務勝募勢勤勧勲勳勺勾勿匀匁匂包匆匈匕化北匙匠匡匣', + '匪匮匹区医匾匿十千升午卉半华协卑卒卓協单卖南単博卜占卡卢卤卦卧卫', + '卯印危即却卵卷卸卿厂厄厅历厉压厌厕厘厚原厢厥厦厨厩厮厳去县参又叉', + '及友双反収发叔取受变叙叛叟叠叡叢口古句另叨叩只叫召叭叮可台叱史右', + '叶号司叹叼叽吁吃各吆合吉吊同名后吏吐向吓吕吗君吝吞吟吠否吧吨吩含', + '听吭吮启吱吴吵吸吹吻吼吾呀呂呃呆呈呉告呐呕呗员呛呜呟呢周呪呱味呵', + '呸呻呼命咀咄咆咋和咎咏咐咒咔咕咖咙咚咦咧咨咪咬咯咱咲咳咸咽哀品哄', + '哆哇哈哉响哎哑哒哗哝哟員哥哦哧哨哩哪哭哮哲哺哼哽唄唆唇唉唐唔唠唤', + '唧唬售唯唱唷唾啃啄商啊問啓啡啤啥啦啧啪啬啸啼喀喂喃善喇喉喊喋喔喘', + '喙喚喜喝喧喩喪喫喬單喰喳営喷喻喽嗅嗒嗓嗔嗖嗜嗟嗡嗣嗤嗦嗨嗯嗳嗽嘀', + '嘆嘈嘉嘎嘗嘘嘛嘟嘩嘱嘲嘴嘶嘻嘿噂噌噎噗噛噜噢器噩噪噬噴噶嚇嚎嚏嚓', + '嚣嚴嚷嚼囊囚四回因团団园困囱囲図围固国图圃圆圈國圏園圓團土圣圧在', + '圭地圳场圾址坂均坊坍坎坏坐坑块坚坛坝坞坟坠坡坤坦坪坯坷垂垃垄型垒', + '垢垣垦垫垮埃埋城埔埜域埠埴執培基埼堀堂堅堆堑堕堡堤堪堯堰報場堵堺', + '塀塁塊塌塑塔塗塘塙塚塞塩填塾境墅墓増墙墜增墟墨墩墳墾壁壇壊壌壑壕', + '壘壞壤士壬壮壯声壱売壳壶壷壽处备変复夏夕外多夜够夢大天太夫夭央失', + '头夷夸夹夺奂奄奇奈奉奋奎奏契奔奕奖套奚奠奢奥奧奨奪奬奮女奴奶奸她', + '好如妃妄妆妇妈妊妒妓妖妙妞妥妨妬妮妹妻妾姆姉姊始姐姑姓委姗姚姜姥', + '姦姨姪姫姬姻姿威娃娅娇娓娘娜娟娠娥娩娯娱娴娶娼婆婉婚婢婦婪婴婵婶', + '婷婿媒媚媛媲媳嫁嫂嫉嫌嫔嫖嫡嫣嫦嫩嬉嬢孃子孔孕字存孙孚孜孝孟季孤', + '学孩孫孰孵孽宁它宅宇守安宋完宏宕宗官宙定宛宜宝实実宠审客宣室宥宦', + '宪宫宮宰害宴宵家容宽宾宿寂寄寅密寇富寐寒寓寛寝寞察寡寢寥實寧寨審', + '寮寰寵寸对寺寻导対寿封専射将將專尉尊尋導小少尔尖尘尚尝尤尧尬尭就', + '尴尸尹尺尻尼尽尾尿局屁层居屈屉届屋屎屏屑展属屠屡層履屯山屹屿岁岂', + '岐岑岔岗岚岛岡岩岬岭岳岸峙峠峡峦峨峭峯峰島峻峽崇崎崔崖崚崛崩崭崽', + '嵋嵌嵐嵩嵯嶋嶺巅巌巍巖川州巡巢巣工左巧巨巩巫差己已巳巴巷巻巽巾币', + '市布帅帆师希帐帕帖帘帚帛帜帝帥带帧師席帮帯帰帳帶帷常帽幅幌幔幕幡', + '幢幣干平年并幸幹幻幼幽幾广庁広庄庆庇床序庐库应底店庙庚府庞废度座', + '庫庭庵庶康庸廃廉廊廓廖廟廣廳延廷建廻廿开弁异弃弄弊式弐弓弔引弗弘', + '弛弟张弥弦弧弩弯弱張強弹强弼弾彈彌归当录彗彙彝形彦彩彪彫彬彭彰影', + '彷役彻彼往征径待很徊律後徐徒従得徘徙從徠御復循微徳徴德徹徽心必忆', + '忌忍忏忒忖志忘忙応忠忡忧快忱念忽忿怀态怂怅怎怒怔怕怖怜思怠怡急怦', + '性怨怪怯总恃恆恋恍恐恒恕恙恚恢恣恤恥恨恩恪恬恭息恰恳恵恶恺恻恼恿', + '悄悉悌悍悔悖悚悟悠患悦您悩悪悬悯悲悴悸悻悼情惇惊惋惑惕惘惚惜惟惠', + '惡惣惦惧惨惩惫惬惭惮惯惰想惴惶惹惺愁愈愉意愕愚愛感愣愤愧愫愼愿慄', + '慈態慌慎慑慕慢慣慧慨慮慰慶慷憂憋憎憐憔憚憤憧憨憩憬憲憶憾懂懇懈應', + '懊懐懒懦懲懵懷懸懿戈戊戌戍戎戏成我戒或战戚戟戦截戮戯戰戲戳戴户戸', + '戻房所扁扇扈扉手才扎扑扒打扔払托扛扣执扩扫扬扭扮扯扰扱扳扶批扼找', + '承技抄抉把抑抒抓投抖抗折抚抛抜択抡抢护报抨披抬抱抵抹押抽抿拂担拆', + '拇拈拉拌拍拎拐拒拓拔拖拗拘拙拚招拜拝拟拠拡拢拣拥拦拧拨择括拭拯拱', + '拳拴拶拷拼拽拾拿持挂指按挑挖挙挚挛挝挟挠挡挣挤挥挨挪挫振挺挽挿捂', + '捅捆捉捍捎捏捐捕捗捜捞损捡换捣捧捨据捲捶捷捺捻掀掃授掉掌掏掐排掖', + '掘掛掠採探掣接控推掩措掬掲掳掴掷掺掻揃揉揍描提插揖揚換握揣揩揪揭', + '揮援揺揽搀搁搂搅損搏搐搓搔搖搜搞搬搭携搾摂摄摆摇摊摑摒摔摘摧摩摯', + '摸摹摺撂撃撇撑撒撕撚撞撤撩撫撬播撮撰撲撵撸撼擁擂擅擊操擎擒擞擢擦', + '擬攀攒攘攝攥攫支收攸改攻放政故效敌敍敏救敕敖敗教敛敝敞敢散敦敬数', + '敲整敵敷文斉斋斌斎斐斑斓斗料斜斟斡斤斥斧斩斬断斯新方於施旁旅旋族', + '旗无既日旦旧旨早旬旭旱时旷旺昂昆昇昊昌明昏易昔星映春昧昨昭是昴昼', + '显時晃晄晋晌晏晒晓晕晖晚晝晟晤晦晨晩普景晰晴晶智晾暁暂暄暇暉暑暖', + '暗暢暦暧暨暫暮暴曇曉曖曙曜曝曦曰曲曳更書曹曼曽曾替最月有朋服朔朕', + '朗望朝期朦木未末本札术朱朴朵机朽杀杂权杆杉李杏材村杖杜杞束杠条来', + '杨杭杯杰東杵杷松板极构枇枉析枕林枚果枝枠枢枣枪枫枭枯架枷柄柊柏某', + '柑染柔柘柚柜查柬柯柱柳柴柵査柾柿栃栄栅标栈栋栏树栓栖栗栞校栩株样', + '核根格栽桁桂桃桅框案桌桐桑桓桔桜桟桢档桥桦桧桨桩桶梁梅梓梗梛條梢', + '梦梧梨梭梯械梳梵梶检棄棉棋棍棒棕棘棚棟棠森棱棲棵棺椀椅椋植椎椒椛', + '検椭椰椿楊楓楔楕楚楞楠楢業楯極楷楼楽概榄榆榈榊榎榛榜榨榮榴榻槇構', + '槌槍槐様槙槛槻槽樂樊樋標樟模樣権横樫樱樵樹樺樽橄橋橘橙機橡橱檀檎', + '檐檜檢檬櫂櫓櫛櫻欄欠次欢欣欤欧欲欺欽款歇歉歌歎歓止正此步武歧歩歪', + '歯歳歴歹死歼殃殆殉殊残殖殡殴段殷殺殻殿毁毅毋母毎每毒毓比毕毗毘毙', + '毛毡毫毬毯氏民氓气氖気氛氟氢氣氦氧氨氮氯水氷永氾汀汁求汇汉汎汐汕', + '汗汚汛汝汞江池污汤汪汰汲汶汹決汽汾沁沂沃沈沉沌沐沓沖沙沛沟没沢沥', + '沦沧沪沫沮河沸油治沼沽沾沿況泄泉泊泌法泛泞泡波泣泥注泪泰泳泵泻泼', + '泽洁洋洒洗洛洞津洪洲洵洸活洼洽派流浄浅浆浇浊测济浏浑浒浓浙浚浜浦', + '浩浪浬浮浴海浸涂涅消涉涌涎涕涙涛涟涡涣涤润涧涨涩涯液涵涸涼淀淄淆', + '淋淌淑淖淘淡淤淨淫淮深淳淵混淹添清渇済渉渊渋渍渎渐渓渔渗渚減渝渠', + '渡渣渤渥渦温測渭港渲渴游渺湃湄湊湍湖湘湛湧湯湾湿満溃溅溉源準溜溝', + '溢溥溪溯溶溺滅滇滉滋滑滔滕滚滝滞满滤滥滨滩滯滴漁漂漆漉漏漓演漕漠', + '漢漣漩漪漫漬漱漳漸漾潇潔潘潜潟潤潦潭潮潰澁澄澈澎澜澡澪澱澳激濁濃', + '濒濕濠濡濫濯瀑瀕瀚瀛瀧瀬灌灘火灭灯灰灵灶灸灼災灾灿炉炊炎炒炕炖炙', + '炫炬炭炮炯炳炸点為炼炽烁烂烈烏烘烙烛烟烤烦烧烫烬热烹烽焉焕焘焙焚', + '無焦焰然焼煉煌煎煙煜煞煤照煩煮煽熄熊熏熔熙熟熬熱熹燃燈燎燒燕燥燦', + '燭燿爆爪爬爭爱爲爵父爷爸爹爽爾片版牌牒牙牛牟牡牢牧物牲牵特牺牽犀', + '犁犊犠犬犯状犹狂狄狈狐狒狗狙狞狠狡狩独狭狮狱狸狹狼猎猓猖猛猜猝猟', + '猩猪猫献猴猶猾猿獄獅獗獣獲獸玄率玉王玑玖玛玩玫环现玲玺玻珀珂珈珊', + '珍珑珠珥班現球琅理琉琏琐琛琢琥琦琪琳琴琵琶琼瑕瑙瑚瑛瑜瑞瑟瑠瑰瑳', + '瑶瑾璃璇璋璐璞璧環璽瓜瓢瓣瓦瓮瓶瓷甄甘甚甜生産甥用甩甫甭田由甲申', + '电男甸町画畅界畏畑畔留畜畝畠畢略番異畳畴畸畿疆疊疋疎疏疑疗疙疚疡', + '疣疤疫疮疯疲疵疹疼疾病症痉痊痒痔痕痘痛痢痩痪痰痴痹瘍瘟瘢瘤瘦瘩瘪', + '瘫瘸瘾療癌癒癖癫発登白百皂的皆皇皈皋皐皑皓皖皮皱皿盃盅盆盈益盎盏', + '盐监盒盔盖盗盘盛盜盟盡監盤目盯盲直相盼盾省眈眉看県眞真眠眨眩眯眶', + '眷眸眺眼着睁睐睑睛睡督睦睨睫睬睹睽睾睿瞄瞅瞎瞒瞟瞥瞧瞩瞪瞬瞭瞰瞳', + '瞻瞿矗矛矜矢矣知矩矫短矮矯石矶矿码砂砌砍研砕砖砥砦砧砰砲破砸砾础', + '硅硕硝硫硬确硯碁碉碌碍碎碑碓碗碘碟碧碩碰碱碳確碾磁磅磊磋磐磕磨磯', + '磷磺礁礎示礼社祀祁祇祈祉祐祕祖祝神祟祠祢祥票祭祯祷祸祺祿禀禁禄禅', + '禍禎福禧禪禮禰禱禹禺离禽禾秀私秃秉秋种科秒秘租秤秦秧秩积称秸移秽', + '稀程稍税稔稚稜稟稠稣種稲稳稷稻稼稽稿穀穂穆積穏穗穣穫穰穴究穷穹空', + '穿突窃窄窍窑窒窓窖窗窘窜窝窟窥窦窪窮窯窺窿立竖站竜竞竟章竣童竪竭', + '端競竹竺竿笃笆笈笋笑笔笙笛笠符笨第笹笺笼筆筈等筋筏筐筑筒答策筛筝', + '筵筷筹签简箇箋箍箔箕算管箫箭箱箸節範篇築篠篡篤篮篱篷簇簌簡簧簸簾', + '簿籍籠米类籽籾粉粋粒粗粘粛粟粤粥粧粪粮粱粹精糊糕糖糙糜糟糧糸系糾', + '紀約紅紊紋納紐純紗紘紙級紛素紡索紧紫紬累細紳紹紺終絃組絆経結絞絡', + '絢給絮統絵絶絹継続綜維綱網綴綸綺綻綾綿緊緋総緑緒線締編緩緯練緻縁', + '縄縛縞縣縦縫縮縱績繁繊繍織繕繡繫繭繰纂纏纖纠红纤约级纪纬纯纱纲纳', + '纵纶纷纸纹纺纽线练组绅细织终绊绍绎经绑绒结绕绘给绚绛络绝绞统绢绣', + '绥继绩绪绫续绮绯绰绳维绵绷绸综绽绿缀缄缅缆缉缎缓缔缕编缘缚缝缠缤', + '缩缪缭缮缰缴缶缸缺罐网罕罗罚罡罢罩罪置罰署罵罷羁羅羊羌美羔羚羞羡', + '群羨義羲羹羽翁翅翌翎習翔翘翟翠翩翰翻翼耀老考者耆而耍耐耕耗耳耶耸', + '耻耽耿聂聆聊聋职联聖聘聚聞聡聪聴職聽聿肃肆肇肉肋肌肖肘肚肛肝肠股', + '肢肤肥肩肪肮肯育肴肺肾肿胀胁胃胆背胎胖胚胜胞胡胤胥胧胰胱胳胴胶胸', + '胺能脂脅脆脇脈脉脊脏脐脑脓脖脚脩脯脱脳脸脹脾腊腋腎腐腑腓腔腕腥腫', + '腮腰腸腹腺腻腾腿膀膊膏膚膛膜膝膨膳膺臀臂臆臓臟臣臥臨自臭至致臻臼', + '舅舆與興舌舍舎舒舔舗舜舞舟航般舰舱舵舶舷船艇艘艦良艰色艳艶艺艾节', + '芋芒芙芜芝芥芦芬芭芯花芳芸芹芽苇苍苏苑苔苗苛苞苟若苦苯英苹苺茂范', + '茄茅茉茎茜茧茨茫茬茵茶茸茹荀荃荆草荐荒荔荘荡荣荧荫药荷荻荼莉莊莎', + '莞莫莱莲获莹莺莽菁菅菇菊菌菓菖菜菩菫華菱菲萃萄萊萌萍萎萝萠萤营萦', + '萧萨萩萬萱落葆葉著葛葡董葦葫葬葱葵葺蒂蒋蒐蒔蒙蒜蒲蒸蒼蓄蓉蓋蓑蓓', + '蓝蓦蓬蓮蔑蔓蔗蔚蔡蔣蔦蔬蔭蔵蔷蔼蔽蕃蕉蕊蕎蕗蕨蕪蕴蕾薄薇薗薙薛薦', + '薩薪薫薬薯藁藉藍藏藐藕藝藤藥藩藻蘇蘑蘭蘸虎虏虐虑虔虚虜虞虫虹虻虽', + '虾蚀蚁蚂蚊蚌蚕蛀蛇蛊蛋蛍蛙蛛蛟蛤蛮蛹蛾蜀蜂蜈蜒蜕蜗蜘蜜蜡蜷蜿蝇蝉', + '蝎蝗蝙蝠蝦蝴蝶螂螃螅融螺蟀蟆蟋蟒蟹蟾蠕蠟蠢血衅衆行衍術衔街衙衛衝', + '衞衡衣补表衫衬衰衷衿袁袄袅袈袋袍袒袖袜被袭袱袴裁裂装裏裔裕裘裙補', + '裝裟裡裤裳裴裸裹製裾褂複褐褒褚褥褪褶襄襖襟襲西要覆覇見規視覗覚覧', + '親観覽见观规觅视览觉觑角解触言訂訃計訊討訓託記訟訣訪設許訳訴診註', + '証詐詔評詞詠詢詣試詩詫詮詰話該詳詹誇誉誌認誓誕誘語誠誤説読誰課誼', + '調諄談請諏諒論諜諦諧諭諮諸諺諾謀謁謂謄謎謙講謝謠謡謹識譜警譬議譲', + '護讃讐讓计订认讥讨让讪训议讯记讲讳讶讷许讹论讼讽设访诀证诃评诅识', + '诈诉诊词诏译试诗诘诙诚诛话诞诠诡询诣该详诧诩诫诬语误诰诱诲说诵请', + '诸诺读诽课谀谁调谅谈谊谋谍谎谏谐谑谒谓谕谙谚谛谜谟谢谣谤谦谨谩谬', + '谭谱谴谷豁豆豊豚象豢豪豫豹貌貝貞負財貢貧貨販貪貫責貯貰貴買貸費貼', + '貿賀賃賄資賊賑賓賛賜賞賠賢賣賦質賭購贈贝贞负贡财责贤败账货质贩贪', + '贫贬购贮贯贰贱贴贵贷贸费贺贻贼贾贿赁赂赃资赋赌赎赏赐赓赔赖赘赚赛', + '赞赠赡赢赣赤赦赫走赳赴赵赶起趁超越趋趟趣足趴趾跃跄跋跌跑跚跛距跟', + '跡跤跨跪路跳践跷跺踉踊踌踏踝踞踢踩踪踱踵蹂蹄蹈蹊蹋蹑蹙蹟蹦蹬蹭蹲', + '蹴蹿躁躇躍躏身躬躯躲躺車軌軍軒軟転軸軽較載輔輝輩輪輯輸輿轄轉轍轟', + '车轧轨轩转轮软轰轲轴轻轼载轿较辄辅辆辈辉辐辑输辕辖辗辘辙辛辜辞辟', + '辣辨辩辫辰辱農边辺辻込辽达辿迁迂迄迅过迈迎运近返还这进远违连迟迢', + '迥迦迪迫迭述迷迸迹追退送适逃逆选逊逍透逐递逓途逗這通逛逝逞速造逢', + '連逮週進逵逸逻逼逾遁遂遅遇遊運遍過遏遐道達違遗遙遜遠遡遣遥適遭遮', + '遵遷選遺遼遽避邀邃還邑邓邢那邦邪邮邯邱邵邸邹邻郁郊郎郑郝郡部郭郵', + '郷都鄂鄙鄭酉酋酌配酎酒酔酝酢酣酥酪酬酮酱酵酶酷酸酿醇醉醋醍醐醒醜', + '醤醬醸釀采釈釉释里重野量金釘釜針釣釧鈍鈴鉄鉛鉢鉱鉴銀銃銅銑銘銚銭', + '鋒鋭鋳鋸鋼錆錐錘錠錦錫錬錯録鍋鍛鍬鍵鎌鎖鎧鎭鎮鏡鐘鑄鑑鑫针钉钊钓', + '钗钙钝钞钟钠钡钢钥钦钧钩钮钱钳钵钻钾铀铁铃铅铎铐铜铝铢铭铮铲银铸', + '铺链销锁锄锅锈锋锌锐错锚锡锢锣锤锥锦键锯锵锻镀镇镑镖镜镯镰镶長长', + '門閃閉開閏閑間関閣閤閥閲闇闘门闪闭问闯闲间闵闷闸闹闺闻闽阀阁阅阉', + '阎阐阑阔阙阜队阪阮阱防阳阴阵阶阻阿陀附际陆陇陈陋陌降限陕陛陡院陣', + '除陥陨险陪陰陳陵陶陷陸険陽隅隆隈隊隋階随隐隔隘隙際障隠隣隧險隶隷', + '隻隼难雀雁雄雅集雇雌雍雏雑雕雛雜離難雨雪雫雯雰雲雳零雷電雾需霄霆', + '震霉霊霍霎霓霖霜霞霧露霸霹青靖静靜非靠靡面革靳靴靶鞄鞅鞋鞍鞘鞠鞭', + '韓韦韧韩韬音韵韶韻響頁頂頃項順須頌預頑頒頓頗領頬頭頰頻頼題額顎顔', + '顕願顛類顧顯页顶顷项顺须顼顽顾顿颁颂预颅领颇颈颉颊颌颍颐频颓颖颗', + '题颚颜额颠颤風颯风飘飛飜飞食飢飯飲飼飽飾餅養餌餐餓館饗饥饬饭饮饰', + '饱饲饵饶饷饺饼饿馅馆馈馋馒首香馨馬馳馴駄駅駆駈駐駒駕駿騎騒験騰騷', + '驍驗驚马驭驮驯驰驱驳驴驶驹驻驼驾驿骂骄骆骇验骏骑骗骚骡骤骥骨骷骸', + '骼髄髅髓高髦髪髭髮鬓鬱鬼魁魂魄魅魏魔魚魯鮎鮮鯉鯛鯨鰯鱈鱒鱗鱼鲁鲍', + '鲜鲤鲨鲸鳃鳄鳌鳍鳖鳞鳥鳩鳳鳴鳶鴨鴻鵜鵬鶏鶴鷄鷗鷲鷹鷺鸟鸠鸡鸣鸥鸦', + '鸭鸯鸳鸽鸾鸿鹃鹅鹉鹊鹏鹤鹦鹰鹳鹿麒麓麗麟麦麹麺麻麽麿黄黎黏黑黒黔', + '默黙黛黝黯鼎鼓鼠鼻鼾齊齐齢齿龄龈龋龍龙龚龟 ', ].map(lengthChecker(32)).join(''); export const BASE_CHARS = [ - ' ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼', - ` !"#$%&'()*+,-./0123456789:;<=>?`, - '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_', - '`abcdefghijklmnopqrstuvwxyz{|}~⌂', + ' ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼', + ` !"#$%&'()*+,-./0123456789:;<=>?`, + '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_', + '`abcdefghijklmnopqrstuvwxyz{|}~⌂', ].map(lengthChecker(32)).join(''); export const CHARS = [ - ' ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼', - ` !"#$%&'()*+,-./0123456789:;<=>?`, - '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_', - '`abcdefghijklmnopqrstuvwxyz{|}~⌂', - 'ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø×ƒ', - 'áíóúñѪº¿®¬½¼¡«»АаБбВвГгДдЕеЁёЖж', - 'ЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦц', - 'ЧчШшЩщЪъЫыЬьЭэЮюЯяĄąĆćĘęŃńŚśŹźŻż', - 'ŁłÓÔŪū€·´°⚲⚥♡ß™ÁÃãÕõČčĎďĚěÍÚŇňŘř', - 'ŠšŤťŮůÝýŽžĹ弾ŔŕĞğİıŞşÀÈÌÒÙЀЁЂЃЄ', - 'ЅІЇЈЉЊЋЌЍЎЏѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤ', - 'ѥѰѱѲѳҊҋҌҍҎҏҐґ ĂăÂÎȘșȚțŊŋŐőŰűĀāĈĉ', - '■□▢▣▤▥▦▧▨▩▪▫▭▮▯▰▱△▴▵▶▷▸▹▻▽▾▿◀◁◂◃', - '◅◆◇◈◉◊◌◍◎●◐◑◒◓◔◕◖◗◚◛◜◝◞◟◠◡◢◣◤◥◦◧', - '◨◩◪◫◬◭◮◯◰◱◲◳◴◵◶◷◸◹◺◻◼◽◾◿¸¾ÊËÏÐÛÞ', - 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩĊċĐđĒēĚě', - 'αβγδεζηθικλμνξοπρστυφχψωĖėĜĝĠġĢģ', - 'ĤĥĦħĨĩĪīĬĭĮįIJijĴĵĶķĸĻļĿŀŅņʼnŌōŎŏŒœ', - 'ŖŗŜŝŢţŦŧŨũŬŭŲųŴŵŶŷŸſ¢¤¥¦¨©¯±¹²³µ', - 'ð÷þĔĕ★☆✰✦✧卐卍❥ღஐϟ【】《》✿❀№♢♤♧✓✔✕✖✗✘', - 'ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただ', - 'ちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむ', - 'めもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ⦅⦆。「」、・ ̄ˊς', - 'ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダ', - 'チヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミム', - 'メモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ ', - '、。〃々〈〉「」『』〒〓〜〝〞〟〡〢〣〤〦〧〨〩〰〱〲〳〴〵 ', - '‐‑‒–—―‖‗‘’‚‛“”„‟‧⁇⁈⁉ ', - '⚧☿♁⚨⚩⚦⚢⚣⚤♔♕♚♛❣☀… ', - '👃🙂😵😠😐😑😆😟🙃 ', - ' ', - ' ', + ' ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼', + ` !"#$%&'()*+,-./0123456789:;<=>?`, + '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_', + '`abcdefghijklmnopqrstuvwxyz{|}~⌂', + 'ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø×ƒ', + 'áíóúñѪº¿®¬½¼¡«»АаБбВвГгДдЕеЁёЖж', + 'ЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦц', + 'ЧчШшЩщЪъЫыЬьЭэЮюЯяĄąĆćĘęŃńŚśŹźŻż', + 'ŁłÓÔŪū€·´°⚲⚥♡ß™ÁÃãÕõČčĎďĚěÍÚŇňŘř', + 'ŠšŤťŮůÝýŽžĹ弾ŔŕĞğİıŞşÀÈÌÒÙЀЁЂЃЄ', + 'ЅІЇЈЉЊЋЌЍЎЏѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤ', + 'ѥѰѱѲѳҊҋҌҍҎҏҐґ ĂăÂÎȘșȚțŊŋŐőŰűĀāĈĉ', + '■□▢▣▤▥▦▧▨▩▪▫▭▮▯▰▱△▴▵▶▷▸▹▻▽▾▿◀◁◂◃', + '◅◆◇◈◉◊◌◍◎●◐◑◒◓◔◕◖◗◚◛◜◝◞◟◠◡◢◣◤◥◦◧', + '◨◩◪◫◬◭◮◯◰◱◲◳◴◵◶◷◸◹◺◻◼◽◾◿¸¾ÊËÏÐÛÞ', + 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩĊċĐđĒēĚě', + 'αβγδεζηθικλμνξοπρστυφχψωĖėĜĝĠġĢģ', + 'ĤĥĦħĨĩĪīĬĭĮįIJijĴĵĶķĸĻļĿŀŅņʼnŌōŎŏŒœ', + 'ŖŗŜŝŢţŦŧŨũŬŭŲųŴŵŶŷŸſ¢¤¥¦¨©¯±¹²³µ', + 'ð÷þĔĕ★☆✰✦✧卐卍❥ღஐϟ【】《》✿❀№♢♤♧✓✔✕✖✗✘', + 'ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただ', + 'ちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむ', + 'めもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ⦅⦆。「」、・ ̄ˊς', + 'ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダ', + 'チヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミム', + 'メモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ ', + '、。〃々〈〉「」『』〒〓〜〝〞〟〡〢〣〤〦〧〨〩〰〱〲〳〴〵 ', + '‐‑‒–—―‖‗‘’‚‛“”„‟‧⁇⁈⁉ ', + '⚧☿♁⚨⚩⚦⚢⚣⚤♔♕♚♛❣☀… ', + '👃🙂😵😠😐😑😆😟🙃 ', + ' ', + ' ', ].map(lengthChecker(32)).join(''); export const ROMAJI = [ - '\uff00!"#$%&'()*+,-./0123456789:;<=>?', - '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_', - '`abcdefghijklmnopqrstuvwxyz{|}~ ', + '\uff00!"#$%&'()*+,-./0123456789:;<=>?', + '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_', + '`abcdefghijklmnopqrstuvwxyz{|}~ ', ].map(lengthChecker(32)).join(''); export const EMOJI = [ - '😍😈👿🤔 ', - '❤💙💚💛💜🖤💔💖💗💕 ', - '🦋🕷🦇👻🐾🐈🐱😺😸😹😻😼😽🙀😿😾', - '🌸💮🌲🎄💐🌿🍎🍏🍊🏀🎃🍕🍬🍭🍡🌈', - '💎🥌🎁⛄❄🕯🎅🌠⭐🌟🎲✨⚡🔥🎵🎶', - '♈♉♊♋♌♍♎♏♐♑♒♓⛎👑☘🍀', - '🍪🥚🍐🥭🥕🍇⛏🍌 ', + '😍😈👿🤔 ', + '❤💙💚💛💜🖤💔💖💗💕 ', + '🦋🕷🦇👻🐾🐈🐱😺😸😹😻😼😽🙀😿😾', + '🌸💮🌲🎄💐🌿🍎🍏🍊🏀🎃🍕🍬🍭🍡🌈', + '💎🥌🎁⛄❄🕯🎅🌠⭐🌟🎲✨⚡🔥🎵🎶', + '♈♉♊♋♌♍♎♏♐♑♒♓⛎👑☘🍀', + '🍪🥚🍐🥭🥕🍇⛏🍌 ', ].map(lengthChecker(16)).join(''); function isSurrogate(code: number) { - return code >= 0xd800 && code <= 0xdbff; + return code >= 0xd800 && code <= 0xdbff; } function fromSurrogate(high: number, low: number) { - return ((high & 0x3ff) << 10) + (low & 0x3ff) + 0x10000; + return ((high & 0x3ff) << 10) + (low & 0x3ff) + 0x10000; } export function charsToCodes(text: string) { - const chars: number[] = []; + const chars: number[] = []; - for (let i = 0; i < text.length; i++) { - let code = text.charCodeAt(i); + for (let i = 0; i < text.length; i++) { + let code = text.charCodeAt(i); - if (isSurrogate(code) && (i + 1) < text.length) { - code = fromSurrogate(code, text.charCodeAt(i + 1)); - i++; - } + if (isSurrogate(code) && (i + 1) < text.length) { + code = fromSurrogate(code, text.charCodeAt(i + 1)); + i++; + } - chars.push(code); - } + chars.push(code); + } - return chars; + return chars; } export interface FontSprite { - code: number; - sprite: number; + code: number; + sprite: number; } export function createFont( - canvas: ExtCanvas, w: number, h: number, addImage: (canvas: ExtCanvas) => number, - options: { noChinese?: boolean; mono?: number; onlyBase?: boolean; } = {}, + canvas: ExtCanvas, w: number, h: number, addImage: (canvas: ExtCanvas) => number, + options: { noChinese?: boolean; mono?: number; onlyBase?: boolean; } = {}, ): FontSprite[] { - const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); - const cols = canvas.width / w; - const baseCodes = charsToCodes(BASE_CHARS); - const charsCodes = charsToCodes(CHARS); - const chineseCodes = options.noChinese ? [] : charsToCodes(CHINESE); - const romajiCodes = charsToCodes(ROMAJI); - const codes = options.onlyBase ? [...baseCodes] : [...charsCodes, ...chineseCodes, ...romajiCodes]; - const baseCodesLength = charsCodes.length + chineseCodes.length; - const added = new Set(); - const arrows = new Set('↑↓'.split('').map(x => x.charCodeAt(0))); + const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + const cols = canvas.width / w; + const baseCodes = charsToCodes(BASE_CHARS); + const charsCodes = charsToCodes(CHARS); + const chineseCodes = options.noChinese ? [] : charsToCodes(CHINESE); + const romajiCodes = charsToCodes(ROMAJI); + const codes = options.onlyBase ? [...baseCodes] : [...charsCodes, ...chineseCodes, ...romajiCodes]; + const baseCodesLength = charsCodes.length + chineseCodes.length; + const added = new Set(); + const arrows = new Set('↑↓'.split('').map(x => x.charCodeAt(0))); - return compact(codes - .map((code, i) => { - if (i >= baseCodesLength) { - i = 32 + (i - baseCodesLength); - } + return compact(codes + .map((code, i) => { + if (i >= baseCodesLength) { + i = 32 + (i - baseCodesLength); + } - const { x, y } = getXY(cols, i); - let { left, width } = getCharWidth(data, x, y, w, h); - const actualCode = i ? code : 0; + const { x, y } = getXY(cols, i); + let { left, width } = getCharWidth(data, x, y, w, h); + const actualCode = i ? code : 0; - if (arrows.has(code)) { - left -= 1; - width += 2; - } + if (arrows.has(code)) { + left -= 1; + width += 2; + } - if (width !== 0 && options.mono !== undefined) { - left = 0; - width = options.mono; - } + if (width !== 0 && options.mono !== undefined) { + left = 0; + width = options.mono; + } - if (!width) { - return undefined; - } else if (added.has(actualCode)) { - // console.log('skipping character', actualCode); - return undefined; - } else { - const sprite = addImage(cropCanvas(canvas, x * w + left, y * h, width - left, h)); - added.add(actualCode); - return { code: actualCode, sprite }; - } - })) - .sort(compareFontSprite); + if (!width) { + return undefined; + } else if (added.has(actualCode)) { + // console.log('skipping character', actualCode); + return undefined; + } else { + const sprite = addImage(cropCanvas(canvas, x * w + left, y * h, width - left, h)); + added.add(actualCode); + return { code: actualCode, sprite }; + } + })) + .sort(compareFontSprite); } export function createEmojis( - canvas: ExtCanvas, w: number, h: number, addImage: (canvas: ExtCanvas) => number + canvas: ExtCanvas, w: number, h: number, addImage: (canvas: ExtCanvas) => number ): FontSprite[] { - const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); - const cols = canvas.width / w; + const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + const cols = canvas.width / w; - return compact(charsToCodes(EMOJI) - .map((code, i) => { - const { x, y } = getXY(cols, i); - const { left, width } = getCharWidth(data, x, y, w, h); + return compact(charsToCodes(EMOJI) + .map((code, i) => { + const { x, y } = getXY(cols, i); + const { left, width } = getCharWidth(data, x, y, w, h); - if (!code || code === 32) { - return undefined; - } + if (!code || code === 32) { + return undefined; + } - if (!width) { - console.log('emoji not found in PSD', code, width); - return undefined; - } + if (!width) { + console.log('emoji not found in PSD', code, width); + return undefined; + } - const sprite = addImage(cropCanvas(canvas, x * w + left, y * h, width - left, h)); - return { code, sprite }; - })) - .sort(compareFontSprite); + const sprite = addImage(cropCanvas(canvas, x * w + left, y * h, width - left, h)); + return { code, sprite }; + })) + .sort(compareFontSprite); } export function fontSpritesToStringAndSprites(fontSprites: FontSprite[]): { chars: string; sprites: number[]; } { - const chars = fontSprites.map(s => String.fromCodePoint(s.code)).join(''); - const sprites = fontSprites.map(s => s.sprite); - return { chars, sprites }; + const chars = fontSprites.map(s => String.fromCodePoint(s.code)).join(''); + const sprites = fontSprites.map(s => s.sprite); + return { chars, sprites }; } function compareFontSprite(a: FontSprite, b: FontSprite) { - return a.code - b.code; + return a.code - b.code; } function isColEmpty(data: ImageData, x: number, y: number, h: number) { - for (let yy = 0; yy < h; yy++) { - if (data.data[((y + yy) * data.width + x) * 4 + 3]) - return false; - } + for (let yy = 0; yy < h; yy++) { + if (data.data[((y + yy) * data.width + x) * 4 + 3]) + return false; + } - return true; + return true; } function getXY(cols: number, i: number) { - return { x: i % cols, y: Math.floor(i / cols) }; + return { x: i % cols, y: Math.floor(i / cols) }; } function getCharWidth(data: ImageData, x: number, y: number, w: number, h: number) { - let left = 0; - let width = w; + let left = 0; + let width = w; - for (; left < w; left++) { - if (!isColEmpty(data, x * w + left, y * h, h)) { - break; - } - } + for (; left < w; left++) { + if (!isColEmpty(data, x * w + left, y * h, h)) { + break; + } + } - for (; width > 0; width--) { - if (!isColEmpty(data, x * w + width - 1, y * h, h)) { - break; - } - } + for (; width > 0; width--) { + if (!isColEmpty(data, x * w + width - 1, y * h, h)) { + break; + } + } - return { left, width }; + return { left, width }; } function lengthChecker(expected: number) { - return function (line: string) { - const length = charsToCodes(line).length; + return function (line: string) { + const length = charsToCodes(line).length; - if (length !== expected) - throw new Error(`Invalid line length (${length}/${expected}) in "${line}"`); + if (length !== expected) + throw new Error(`Invalid line length (${length}/${expected}) in "${line}"`); - return line; - }; + return line; + }; } diff --git a/src/ts/tools/create-sprites.ts b/src/ts/tools/create-sprites.ts index eb31ae8..0b51cf3 100644 --- a/src/ts/tools/create-sprites.ts +++ b/src/ts/tools/create-sprites.ts @@ -7,26 +7,26 @@ require('source-map-support').install(); import * as fs from 'fs'; import * as path from 'path'; import { - max, range, flatten, mapValues, dropRightWhile, isEqual, uniq, times, compact, includes, toPairs, + max, range, flatten, mapValues, dropRightWhile, isEqual, uniq, times, compact, includes, toPairs, } from 'lodash'; import { - ColorShadow, Nose, Eye, Tree, Animation, ColorExtra, Button, Emote, Layer, Sprite, ExtCanvas, Psd, - Result, TileSprites + ColorShadow, Nose, Eye, Tree, Animation, ColorExtra, Button, Emote, Layer, Sprite, ExtCanvas, Psd, + Result, TileSprites } from './types'; import { - findLayer, mkdir, cartesian, findLayerSafe, getCanvas, getLayerCanvas, getLayerCanvasSafe, - getCanvasSafe, matcher, getDirectories, trimRight, addSprite, createSprite, - TRANSPARENT, BLACK, defaultPalette, WHITE, OUTLINE_COLOR, SHADE_COLOR, TONGUE_COLOR, MOUTH_COLOR, - TEETH_COLOR, TEETH_SHADE_COLOR, LIGHT_SHADE_COLOR, compareLayers, addImage, - addSpriteWithColors, createPixelSprites, nameMatches, getPatternCanvases, clipPattern + findLayer, mkdir, cartesian, findLayerSafe, getCanvas, getLayerCanvas, getLayerCanvasSafe, + getCanvasSafe, matcher, getDirectories, trimRight, addSprite, createSprite, + TRANSPARENT, BLACK, defaultPalette, WHITE, OUTLINE_COLOR, SHADE_COLOR, TONGUE_COLOR, MOUTH_COLOR, + TEETH_COLOR, TEETH_SHADE_COLOR, LIGHT_SHADE_COLOR, compareLayers, addImage, + addSpriteWithColors, createPixelSprites, nameMatches, getPatternCanvases, clipPattern } from './common'; import { createSpriteSheet, imageToSprite, saveSpriteSheet, saveCanvasAsRaw, saveSpriteSheetAsBinary } from './sprite-sheet'; import { tilesToSprites } from './convert-tiles'; import { createFont, FontSprite, createEmojis } from './create-font'; import { - padCanvas, cropCanvas, mergeCanvases, colorCanvas, maskCanvas, saveCanvas, imageToCanvas, - isCanvasEmpty, forEachPixel, mapColors, createExtCanvas, forEachPixelOf2Canvases, - cropAndPadByColRow, cropByIndex, ByIndexGetter, reverseMaskCanvas, loadImage, mirrorCanvas, + padCanvas, cropCanvas, mergeCanvases, colorCanvas, maskCanvas, saveCanvas, imageToCanvas, + isCanvasEmpty, forEachPixel, mapColors, createExtCanvas, forEachPixelOf2Canvases, + cropAndPadByColRow, cropByIndex, ByIndexGetter, reverseMaskCanvas, loadImage, mirrorCanvas, } from './canvas-utils'; import { openPsd, openPsdFiles } from './psd-utils'; import { sheets, Sheet } from '../common/sheets'; @@ -52,646 +52,646 @@ const getFrames = (layers: Layer[]) => layers.filter(nameMatches(/^frame/)).sort const ponyPsd = (name: string) => openPsd(path.join(ponyPath, name)); function openPng(fileName: string) { - return imageToCanvas(loadImage(fileName)); + return imageToCanvas(loadImage(fileName)); } function createPaletteFromList(canvases: ExtCanvas[]) { - return canvases.reduce((pal, can) => createPalette(can, pal), []); + return canvases.reduce((pal, can) => createPalette(can, pal), []); } function createPaletteFromLayers(layers: Layer[]) { - return createPaletteFromList(layers.map(getCanvasSafe)); + return createPaletteFromList(layers.map(getCanvasSafe)); } function addCMSprite(sprites: Sprite[], flip: boolean, ox = 43, oy = 49) { - const canvas = createExtCanvas(5, 5, 'cm'); - const context = canvas.getContext('2d')!; - const imageData = context.getImageData(0, 0, 5, 5); + const canvas = createExtCanvas(5, 5, 'cm'); + const context = canvas.getContext('2d')!; + const imageData = context.getImageData(0, 0, 5, 5); - for (let i = 0, y = 0; y < 5; y++) { - for (let x = 0; x < 5; x++) { - imageData.data[i++] = flip ? (y * 5 + (4 - x)) : (y * 5 + x); - imageData.data[i++] = 255; - imageData.data[i++] = 0; - imageData.data[i++] = 255; - } - } + for (let i = 0, y = 0; y < 5; y++) { + for (let x = 0; x < 5; x++) { + imageData.data[i++] = flip ? (y * 5 + (4 - x)) : (y * 5 + x); + imageData.data[i++] = 255; + imageData.data[i++] = 0; + imageData.data[i++] = 255; + } + } - context.putImageData(imageData, 0, 0); - sprites.push(createSprite(sprites.length, padCanvas(canvas, ox, oy), { x: ox, y: oy, w: 5, h: 5 })); - return sprites.length - 1; + context.putImageData(imageData, 0, 0); + sprites.push(createSprite(sprites.length, padCanvas(canvas, ox, oy), { x: ox, y: oy, w: 5, h: 5 })); + return sprites.length - 1; } function createPalette(canvas: ExtCanvas, palette: number[] = []) { - forEachPixel(canvas, c => { - if (!includes(palette, c)) { - palette.push(c); - } - }); + forEachPixel(canvas, c => { + if (!includes(palette, c)) { + palette.push(c); + } + }); - if (palette.length > MAX_PALETTE_SIZE) { - throw new Error(`Exceeded max palette size ${palette.length}/${MAX_PALETTE_SIZE} (${canvas.info})`); - } + if (palette.length > MAX_PALETTE_SIZE) { + throw new Error(`Exceeded max palette size ${palette.length}/${MAX_PALETTE_SIZE} (${canvas.info})`); + } - return palette.sort((a, b) => a - b); + return palette.sort((a, b) => a - b); } function splitButton(canvas: ExtCanvas, border: number) { - return { - topLeft: cropCanvas(canvas, 0, 0, border, border), - top: cropCanvas(canvas, border, 0, canvas.width - border * 2, border), - topRight: cropCanvas(canvas, canvas.width - border, 0, border, border), - left: cropCanvas(canvas, 0, border, border, canvas.height - border * 2), - bg: cropCanvas(canvas, border, border, canvas.width - border * 2, canvas.height - border * 2), - right: cropCanvas(canvas, canvas.width - border, border, border, canvas.height - border * 2), - bottomLeft: cropCanvas(canvas, 0, canvas.height - border, border, border), - bottom: cropCanvas(canvas, border, canvas.height - border, canvas.width - border * 2, border), - bottomRight: cropCanvas(canvas, canvas.width - border, canvas.height - border, border, border), - }; + return { + topLeft: cropCanvas(canvas, 0, 0, border, border), + top: cropCanvas(canvas, border, 0, canvas.width - border * 2, border), + topRight: cropCanvas(canvas, canvas.width - border, 0, border, border), + left: cropCanvas(canvas, 0, border, border, canvas.height - border * 2), + bg: cropCanvas(canvas, border, border, canvas.width - border * 2, canvas.height - border * 2), + right: cropCanvas(canvas, canvas.width - border, border, border, canvas.height - border * 2), + bottomLeft: cropCanvas(canvas, 0, canvas.height - border, border, border), + bottom: cropCanvas(canvas, border, canvas.height - border, canvas.width - border * 2, border), + bottomRight: cropCanvas(canvas, canvas.width - border, canvas.height - border, border, border), + }; } // main methods function getEyesFromPsd({ objects2, sprites }: Result, eyesPsd: Psd, irisesPsd: Psd) { - const eyeCount = 24; - const left = 20; - const top = 20; - const rightEyeWidth = 12; - const perLine = 10; - const h = 30, dx = 30, dy = 30; - const irisesCount = 8; + const eyeCount = 24; + const left = 20; + const top = 20; + const rightEyeWidth = 12; + const perLine = 10; + const h = 30, dx = 30, dy = 30; + const irisesCount = 8; - const irises = colorCanvas(getLayerCanvasSafe('irises', irisesPsd), 'white'); - const whites = getLayerCanvasSafe('whites', eyesPsd); - const lineart = getLayerCanvasSafe('lineart', eyesPsd); - const eyeshadow = getLayerCanvasSafe('eyeshadow', eyesPsd); - const bases = [mergeCanvases(whites, lineart), ...findLayerSafe('eyelashes', eyesPsd).children - .sort(compareLayers) - .map(getCanvas) - .map(c => mergeCanvases(whites, lineart, c))]; - const shadow = colorCanvas(eyeshadow, 'white'); - const shine = mapColors(eyeshadow, c => c === 0xffffffff ? c : 0); + const irises = colorCanvas(getLayerCanvasSafe('irises', irisesPsd), 'white'); + const whites = getLayerCanvasSafe('whites', eyesPsd); + const lineart = getLayerCanvasSafe('lineart', eyesPsd); + const eyeshadow = getLayerCanvasSafe('eyeshadow', eyesPsd); + const bases = [mergeCanvases(whites, lineart), ...findLayerSafe('eyelashes', eyesPsd).children + .sort(compareLayers) + .map(getCanvas) + .map(c => mergeCanvases(whites, lineart, c))]; + const shadow = colorCanvas(eyeshadow, 'white'); + const shine = mapColors(eyeshadow, c => c === 0xffffffff ? c : 0); - const getRightEye = cropAndPadByColRow(left, top, rightEyeWidth, h, dx, dy, left, top); - const getLeftEye = cropAndPadByColRow(left + rightEyeWidth, top, 30 - rightEyeWidth, h, dx, dy, left + rightEyeWidth, top); - const getRight = cropByIndex(getRightEye, perLine); - const getLeft = cropByIndex(getLeftEye, perLine); + const getRightEye = cropAndPadByColRow(left, top, rightEyeWidth, h, dx, dy, left, top); + const getLeftEye = cropAndPadByColRow(left + rightEyeWidth, top, 30 - rightEyeWidth, h, dx, dy, left + rightEyeWidth, top); + const getRight = cropByIndex(getRightEye, perLine); + const getLeft = cropByIndex(getLeftEye, perLine); - // const mirrored = (get: ByIndexGetter): ByIndexGetter => (canvas, index) => mirrorCanvas(get(canvas, index), -15); + // const mirrored = (get: ByIndexGetter): ByIndexGetter => (canvas, index) => mirrorCanvas(get(canvas, index), -15); - const palette = [0, WHITE, BLACK]; // + const palette = [0, WHITE, BLACK]; // - const getEye = (get: ByIndexGetter) => (i: number) => bases.map(base => { - const s = addSprite(sprites, get(shadow, i), undefined, palette); + const getEye = (get: ByIndexGetter) => (i: number) => bases.map(base => { + const s = addSprite(sprites, get(shadow, i), undefined, palette); - return { - base: addSprite(sprites, get(base, i), undefined, palette), - irises: range(0, irisesCount) - .map(j => maskCanvas(get(irises, j), get(whites, i))) - .map(canvas => addSprite(sprites, canvas, undefined, palette)), - shadow: s ? s : addSprite(sprites, get(shadow, 0), undefined, palette), - shine: s ? addSprite(sprites, get(shine, i), undefined, palette) : addSprite(sprites, get(shine, 0), undefined, palette), - }; - }); + return { + base: addSprite(sprites, get(base, i), undefined, palette), + irises: range(0, irisesCount) + .map(j => maskCanvas(get(irises, j), get(whites, i))) + .map(canvas => addSprite(sprites, canvas, undefined, palette)), + shadow: s ? s : addSprite(sprites, get(shadow, 0), undefined, palette), + shine: s ? addSprite(sprites, get(shine, i), undefined, palette) : addSprite(sprites, get(shine, 0), undefined, palette), + }; + }); - objects2['eyeRight: PonyEyes'] = [null, ...range(0, eyeCount).map(getEye(getRight))]; - objects2['eyeLeft: PonyEyes'] = [null, ...range(0, eyeCount).map(getEye(getLeft))]; - // objects2['eyeRight2: PonyEyes'] = [null, ...range(0, eyeCount).map(getEye(mirrored(getLeft)))]; + objects2['eyeRight: PonyEyes'] = [null, ...range(0, eyeCount).map(getEye(getRight))]; + objects2['eyeLeft: PonyEyes'] = [null, ...range(0, eyeCount).map(getEye(getLeft))]; + // objects2['eyeRight2: PonyEyes'] = [null, ...range(0, eyeCount).map(getEye(mirrored(getLeft)))]; } function getBlushFromPsd({ sprites, objects2 }: Result, psd: Psd) { - objects2.blush = addSprite(sprites, getLayerCanvasSafe('color', psd)); + objects2.blush = addSprite(sprites, getLayerCanvasSafe('color', psd)); } function getPonyShadowsAndSelection({ sprites, objects2 }: Result, psd: Psd) { - const count = 5; - const shadow = colorCanvas(getLayerCanvasSafe('shadow', psd), 'white'); - const selection = getLayerCanvasSafe('selection', psd); - const crop = cropAndPadByColRow(0, 0, psd.width, 10, 0, 10); + const count = 5; + const shadow = colorCanvas(getLayerCanvasSafe('shadow', psd), 'white'); + const selection = getLayerCanvasSafe('selection', psd); + const crop = cropAndPadByColRow(0, 0, psd.width, 10, 0, 10); - objects2.ponyShadows = times(count, i => crop(shadow, 0, i)).map(c => addSprite(sprites, c)); - objects2.ponySelections = times(count, i => crop(selection, 0, i)).map(c => addSprite(sprites, c)); + objects2.ponyShadows = times(count, i => crop(shadow, 0, i)).map(c => addSprite(sprites, c)); + objects2.ponySelections = times(count, i => crop(selection, 0, i)).map(c => addSprite(sprites, c)); } function splitMuzzleMouth(canvas: ExtCanvas) { - const muzzleCanvas = mapColors(canvas, c => (c === WHITE || c === OUTLINE_COLOR || c === SHADE_COLOR) ? c : 0); - const mouthCanvas = mapColors(canvas, c => { - if (c === MOUTH_COLOR || c === TONGUE_COLOR) { - return c; - } else if (c === TEETH_COLOR) { - return WHITE; - } else if (c === TEETH_SHADE_COLOR) { - return LIGHT_SHADE_COLOR; - } else { - return 0; - } - }); + const muzzleCanvas = mapColors(canvas, c => (c === WHITE || c === OUTLINE_COLOR || c === SHADE_COLOR) ? c : 0); + const mouthCanvas = mapColors(canvas, c => { + if (c === MOUTH_COLOR || c === TONGUE_COLOR) { + return c; + } else if (c === TEETH_COLOR) { + return WHITE; + } else if (c === TEETH_SHADE_COLOR) { + return LIGHT_SHADE_COLOR; + } else { + return 0; + } + }); - return { mouthCanvas, muzzleCanvas }; + return { mouthCanvas, muzzleCanvas }; } function getMuzzlesFromPsd({ sprites, objects2 }: Result, psd: Psd) { - const columns = 5; - const typeCount = 2; - const expressionsPerLine = 5; - const expressionList = [ - 0, 1, 24, 4, 9, - 5, 2, 3, 8, 10, - 11, 12, 13, 14, 15, - 17, 18, 19, 20, 21, - 22, 23, 30, 32, 6, - 33, - ]; + const columns = 5; + const typeCount = 2; + const expressionsPerLine = 5; + const expressionList = [ + 0, 1, 24, 4, 9, + 5, 2, 3, 8, 10, + 11, 12, 13, 14, 15, + 17, 18, 19, 20, 21, + 22, 23, 30, 32, 6, + 33, + ]; - const { mouthCanvas, muzzleCanvas } = splitMuzzleMouth(getLayerCanvasSafe('muzzle', psd)); - const fangsCanvas = colorCanvas(getLayerCanvasSafe('fangs', psd), 'white'); - const noseCanvas = colorCanvas(getLayerCanvasSafe('nose', psd), 'white'); - const noseMuzzleCanvas = muzzleCanvas; // mergeCanvases(muzzleCanvas, noseCanvas); - const nosePatternCanvas = mergeCanvases(colorCanvas(muzzleCanvas, 'red'), colorCanvas(noseCanvas, '#00ff00')); - const getImage = cropAndPadByColRow(20, 20, 30, 30, 30, 30, 20, 20); - const muzzleIndices = [...range(0, typeCount), -1]; + const { mouthCanvas, muzzleCanvas } = splitMuzzleMouth(getLayerCanvasSafe('muzzle', psd)); + const fangsCanvas = colorCanvas(getLayerCanvasSafe('fangs', psd), 'white'); + const noseCanvas = colorCanvas(getLayerCanvasSafe('nose', psd), 'white'); + const noseMuzzleCanvas = muzzleCanvas; // mergeCanvases(muzzleCanvas, noseCanvas); + const nosePatternCanvas = mergeCanvases(colorCanvas(muzzleCanvas, 'red'), colorCanvas(noseCanvas, '#00ff00')); + const getImage = cropAndPadByColRow(20, 20, 30, 30, 30, 30, 20, 20); + const muzzleIndices = [...range(0, typeCount), -1]; - saveCanvas(path.join(outputPath, 'noseMuzzleCanvas.png'), noseMuzzleCanvas); - saveCanvas(path.join(outputPath, 'nosePatternCanvas.png'), nosePatternCanvas); + saveCanvas(path.join(outputPath, 'noseMuzzleCanvas.png'), noseMuzzleCanvas); + saveCanvas(path.join(outputPath, 'nosePatternCanvas.png'), nosePatternCanvas); - // [expression][type][pattern] - objects2['noses: PonyNose[][][]'] = expressionList.map(expression => muzzleIndices.map(type => { - const x = expression % expressionsPerLine + (type > 0 ? type : 0) * columns; - const y = Math.floor(expression / expressionsPerLine); - const fangs = addSprite(sprites, getImage(fangsCanvas, x, y)) || 0; - const mouth = addSprite(sprites, getImage(mouthCanvas, x, y), undefined, defaultPalette); + // [expression][type][pattern] + objects2['noses: PonyNose[][][]'] = expressionList.map(expression => muzzleIndices.map(type => { + const x = expression % expressionsPerLine + (type > 0 ? type : 0) * columns; + const y = Math.floor(expression / expressionsPerLine); + const fangs = addSprite(sprites, getImage(fangsCanvas, x, y)) || 0; + const mouth = addSprite(sprites, getImage(mouthCanvas, x, y), undefined, defaultPalette); - if (type < 0) { - const color = getImage(noseMuzzleCanvas, x, y); - const pattern = getImage(nosePatternCanvas, x, y); - return [{ ...addSpriteWithColors(sprites, color, pattern), mouth, fangs }]; - } else { - return [{ color: addSprite(sprites, getImage(muzzleCanvas, x, y)), colors: 3, mouth, fangs }]; - } - })); + if (type < 0) { + const color = getImage(noseMuzzleCanvas, x, y); + const pattern = getImage(nosePatternCanvas, x, y); + return [{ ...addSpriteWithColors(sprites, color, pattern), mouth, fangs }]; + } else { + return [{ color: addSprite(sprites, getImage(muzzleCanvas, x, y)), colors: 3, mouth, fangs }]; + } + })); } interface SetImportInfo { - name: string; - layerName: string; - mask?: string; - reverse?: boolean; - maskFile?: string; - mirror?: boolean; - mirrorOffsetX?: number; + name: string; + layerName: string; + mask?: string; + reverse?: boolean; + maskFile?: string; + mirror?: boolean; + mirrorOffsetX?: number; } function getSetNameLayer(sheet: Sheet): SetImportInfo[] { - const layersWithSets = sheet.layers.filter(l => l.set); + const layersWithSets = sheet.layers.filter(l => l.set); - return [ - ...layersWithSets.map(l => ({ - name: l.set!, - layerName: l.name - })), - ...layersWithSets.filter(l => !!l.importMirrored).map(l => ({ - name: l.importMirrored!.fieldName!, - layerName: l.name, - mirror: true, - mirrorOffsetX: l.importMirrored!.offsetX, - })), - ]; + return [ + ...layersWithSets.map(l => ({ + name: l.set!, + layerName: l.name + })), + ...layersWithSets.filter(l => !!l.importMirrored).map(l => ({ + name: l.importMirrored!.fieldName!, + layerName: l.name, + mirror: true, + mirrorOffsetX: l.importMirrored!.offsetX, + })), + ]; } function importSprites({ sprites, objects2 }: Result, sheet: Sheet) { - const psd = ponyPsd(`${sheet.file}.psd`); - const { width, height, offset, offsetY = 0, padLeft = 0, padTop = 20, wrap = 0, importOffsets } = sheet; - const setsWithEmpties = sheet.setsWithEmpties; - let frameCount = Math.floor(psd.width / offset); - let typeCount = Math.floor(psd.height / height); - let getImage = cropAndPadByColRow(padLeft, 0, width, height, offset, height, 10 + padLeft, padTop - offsetY); - let hasExtra = false; + const psd = ponyPsd(`${sheet.file}.psd`); + const { width, height, offset, offsetY = 0, padLeft = 0, padTop = 20, wrap = 0, importOffsets } = sheet; + const setsWithEmpties = sheet.setsWithEmpties; + let frameCount = Math.floor(psd.width / offset); + let typeCount = Math.floor(psd.height / height); + let getImage = cropAndPadByColRow(padLeft, 0, width, height, offset, height, 10 + padLeft, padTop - offsetY); + let hasExtra = false; - if (wrap) { - typeCount = typeCount * frameCount; - frameCount = 1; - const oldGet = getImage; - getImage = (canvas, _, type) => oldGet(canvas, type % wrap, Math.floor(type / wrap)); - } else if (sheet.single) { - typeCount = typeCount * frameCount; - frameCount = 1; - const oldGet = getImage; - getImage = (canvas, _, type) => oldGet(canvas, type, 0); - } + if (wrap) { + typeCount = typeCount * frameCount; + frameCount = 1; + const oldGet = getImage; + getImage = (canvas, _, type) => oldGet(canvas, type % wrap, Math.floor(type / wrap)); + } else if (sheet.single) { + typeCount = typeCount * frameCount; + frameCount = 1; + const oldGet = getImage; + getImage = (canvas, _, type) => oldGet(canvas, type, 0); + } - const sets: SetImportInfo[] = [ - ...getSetNameLayer(sheet), - ...(sheet.masks || []), - ]; + const sets: SetImportInfo[] = [ + ...getSetNameLayer(sheet), + ...(sheet.masks || []), + ]; - const maskFiles: any = {}; + const maskFiles: any = {}; - if (sheet.masks) { - compact(sheet.masks.map(m => m.maskFile)) - .forEach(file => maskFiles[file] = openPsd(path.join(ponyPath, file + '.psd'))); - } + if (sheet.masks) { + compact(sheet.masks.map(m => m.maskFile)) + .forEach(file => maskFiles[file] = openPsd(path.join(ponyPath, file + '.psd'))); + } - const animations = sets.map(({ layerName, name, mask, reverse, maskFile, mirror, mirrorOffsetX }) => { - const layer = findLayerSafe(layerName, psd); - let color = getLayerCanvasSafe('color', layer); - const extraCanvas = sheet.extra ? getLayerCanvas('extra', layer) : undefined; - const patterns = getPatternCanvases(layer); + const animations = sets.map(({ layerName, name, mask, reverse, maskFile, mirror, mirrorOffsetX }) => { + const layer = findLayerSafe(layerName, psd); + let color = getLayerCanvasSafe('color', layer); + const extraCanvas = sheet.extra ? getLayerCanvas('extra', layer) : undefined; + const patterns = getPatternCanvases(layer); - if (mask) { - const maskColor = getLayerCanvasSafe(mask, maskFiles[maskFile || ''] || psd); - const maskRepeated = createExtCanvas(psd.width, psd.height, `${maskColor.info} repeated`); - const maskContext = maskRepeated.getContext('2d')!; + if (mask) { + const maskColor = getLayerCanvasSafe(mask, maskFiles[maskFile || ''] || psd); + const maskRepeated = createExtCanvas(psd.width, psd.height, `${maskColor.info} repeated`); + const maskContext = maskRepeated.getContext('2d')!; - for (let i = 0; i < typeCount; i++) { - maskContext.drawImage(maskColor, 0, i * (height + offsetY)); - } + for (let i = 0; i < typeCount; i++) { + maskContext.drawImage(maskColor, 0, i * (height + offsetY)); + } - color = maskCanvas(color, reverse ? reverseMaskCanvas(maskRepeated) : maskRepeated); - } + color = maskCanvas(color, reverse ? reverseMaskCanvas(maskRepeated) : maskRepeated); + } - // [frame][type][pattern] - const frames = trimRight(range(0, frameCount).map(frame => { - return trimRight(range(0, typeCount).map(type => { - const { x, y } = importOffsets && importOffsets[frame] || { x: 0, y: 0 }; - const getAndPadBase = (canvas: ExtCanvas) => padCanvas(getImage(canvas, frame, type), -x, -y); - const getAndPad = mirror ? (canvas: ExtCanvas) => mirrorCanvas(getAndPadBase(canvas), mirrorOffsetX) : getAndPadBase; - const accessoryFrame = getAndPad(color); - const extraFrame = extraCanvas && getAndPad(extraCanvas); + // [frame][type][pattern] + const frames = trimRight(range(0, frameCount).map(frame => { + return trimRight(range(0, typeCount).map(type => { + const { x, y } = importOffsets && importOffsets[frame] || { x: 0, y: 0 }; + const getAndPadBase = (canvas: ExtCanvas) => padCanvas(getImage(canvas, frame, type), -x, -y); + const getAndPad = mirror ? (canvas: ExtCanvas) => mirrorCanvas(getAndPadBase(canvas), mirrorOffsetX) : getAndPadBase; + const accessoryFrame = getAndPad(color); + const extraFrame = extraCanvas && getAndPad(extraCanvas); - if (isCanvasEmpty(accessoryFrame)) { - return null; - } else { - let extraProps: any = {}; + if (isCanvasEmpty(accessoryFrame)) { + return null; + } else { + let extraProps: any = {}; - if (extraFrame) { - const palette = createPalette(extraFrame); - const extra = addSprite(sprites, extraFrame, undefined, palette); - extraProps = { extra, palette }; - hasExtra = true; - } + if (extraFrame) { + const palette = createPalette(extraFrame); + const extra = addSprite(sprites, extraFrame, undefined, palette); + extraProps = { extra, palette }; + hasExtra = true; + } - const patternCanvases = patterns - .map(getAndPad) - .map(pattern => clipPattern(accessoryFrame, pattern)); + const patternCanvases = patterns + .map(getAndPad) + .map(pattern => clipPattern(accessoryFrame, pattern)); - return dropRightWhile(patternCanvases, isCanvasEmpty) - .map(patternFrame => { - ...addSpriteWithColors(sprites, accessoryFrame, patternFrame), - ...extraProps - }); - } - })); - })); + return dropRightWhile(patternCanvases, isCanvasEmpty) + .map(patternFrame => { + ...addSpriteWithColors(sprites, accessoryFrame, patternFrame), + ...extraProps + }); + } + })); + })); - return { name, frames }; - }); + return { name, frames }; + }); - // fill-in missing types - if (animations.length > 1) { - animations.forEach(({ frames }) => { - frames.forEach((types, i) => { - const typeCount = max(animations.map(({ frames }) => frames[i] ? frames[i]!.length : 0))!; + // fill-in missing types + if (animations.length > 1) { + animations.forEach(({ frames }) => { + frames.forEach((types, i) => { + const typeCount = max(animations.map(({ frames }) => frames[i] ? frames[i]!.length : 0))!; - while (types && types.length < typeCount) { - types.push([]); - } - }); - }); - } + while (types && types.length < typeCount) { + types.push([]); + } + }); + }); + } - // fix pattern color counts - if (sheet.single) { - const groups = sheet.groups || [animations.map(a => a.name)]; - const colorCounts: number[][][] = groups.map(() => []); // [group][type][pattern] + // fix pattern color counts + if (sheet.single) { + const groups = sheet.groups || [animations.map(a => a.name)]; + const colorCounts: number[][][] = groups.map(() => []); // [group][type][pattern] - animations.forEach(({ name, frames }) => { - const gi = groups.findIndex(g => includes(g, name)); - const groupColorCounts = colorCounts[gi]; + animations.forEach(({ name, frames }) => { + const gi = groups.findIndex(g => includes(g, name)); + const groupColorCounts = colorCounts[gi]; - frames.forEach(types => { - (types || []).forEach((patterns, ti) => { - groupColorCounts[ti] = groupColorCounts[ti] || []; + frames.forEach(types => { + (types || []).forEach((patterns, ti) => { + groupColorCounts[ti] = groupColorCounts[ti] || []; - (patterns || []).forEach((pattern, pi) => { - if (pattern) { - groupColorCounts[ti][pi] = Math.max(pattern.colors, groupColorCounts[ti][pi] || 0); - } - }); - }); - }); - }); + (patterns || []).forEach((pattern, pi) => { + if (pattern) { + groupColorCounts[ti][pi] = Math.max(pattern.colors, groupColorCounts[ti][pi] || 0); + } + }); + }); + }); + }); - animations.forEach(({ name, frames }) => { - const gi = groups.findIndex(g => includes(g, name)); - const groupColorCounts = colorCounts[gi]; + animations.forEach(({ name, frames }) => { + const gi = groups.findIndex(g => includes(g, name)); + const groupColorCounts = colorCounts[gi]; - frames.forEach(types => { - (types || []).forEach((patterns, ti) => { - (patterns || []).forEach((pattern, pi) => { - if (pattern) { - pattern.colors = groupColorCounts[ti] && groupColorCounts[ti][pi] || 0; - } - }); - }); - }); - }); - } + frames.forEach(types => { + (types || []).forEach((patterns, ti) => { + (patterns || []).forEach((pattern, pi) => { + if (pattern) { + pattern.colors = groupColorCounts[ti] && groupColorCounts[ti][pi] || 0; + } + }); + }); + }); + }); + } - animations.map(({ name, frames }) => { - if (sheet.empties && setsWithEmpties && includes(setsWithEmpties, name)) { - for (let i = 0; i < frames.length; i++) { - frames[i] = frames[i] && frames[i]!.filter((_, j) => !includes(sheet.empties, j)); - } - } + animations.map(({ name, frames }) => { + if (sheet.empties && setsWithEmpties && includes(setsWithEmpties, name)) { + for (let i = 0; i < frames.length; i++) { + frames[i] = frames[i] && frames[i]!.filter((_, j) => !includes(sheet.empties, j)); + } + } - if (sheet.single) { - objects2[`${name}: StaticSprites${hasExtra ? 'Extra' : ''}`] = frames[0]; - } else { - objects2[`${name}: AnimatedSprites`] = frames; - } - }); + if (sheet.single) { + objects2[`${name}: StaticSprites${hasExtra ? 'Extra' : ''}`] = frames[0]; + } else { + objects2[`${name}: AnimatedSprites`] = frames; + } + }); } function getTreesFromPsd({ sprites, objects2 }: Result, psd: Psd, name: string, palettes?: number[][]) { - const groups = psd.children.filter(c => c.children && c.children.length).sort(compareLayers); - const width = psd.width / groups.length; - const spr = (name: string, index: number, palette: number[], parent: Layer) => { - const canvas = getLayerCanvasSafe(name, parent); - const cropped = cropCanvas(canvas, width * index, 0, width, canvas.height); - return addSprite(sprites, cropped, undefined, palette); - }; + const groups = psd.children.filter(c => c.children && c.children.length).sort(compareLayers); + const width = psd.width / groups.length; + const spr = (name: string, index: number, palette: number[], parent: Layer) => { + const canvas = getLayerCanvasSafe(name, parent); + const cropped = cropCanvas(canvas, width * index, 0, width, canvas.height); + return addSprite(sprites, cropped, undefined, palette); + }; - const children = flatten(groups.map(c => c.children)); - const trunkPalettes = palettes || [createPaletteFromLayers(children.filter(l => /^(stump|trunk)$/.test(l.name)))]; - const crownPalettes = palettes || [createPaletteFromLayers(children.filter(l => /crown/.test(l.name)))]; + const children = flatten(groups.map(c => c.children)); + const trunkPalettes = palettes || [createPaletteFromLayers(children.filter(l => /^(stump|trunk)$/.test(l.name)))]; + const crownPalettes = palettes || [createPaletteFromLayers(children.filter(l => /crown/.test(l.name)))]; - const hasStumpWinter = children.some(l => /stump winter/.test(l.name)); - const stumpWinterPalettes = hasStumpWinter ? [createPaletteFromLayers(children.filter(l => /stump winter/.test(l.name)))] : []; + const hasStumpWinter = children.some(l => /stump winter/.test(l.name)); + const stumpWinterPalettes = hasStumpWinter ? [createPaletteFromLayers(children.filter(l => /stump winter/.test(l.name)))] : []; - groups.forEach((group, index) => { - objects2[`${name}Stump${index}`] = { - color: spr('stump', index, trunkPalettes[0], group), - //shadow: spr('stump shadow', index, shadowPalette, group), - palettes: trunkPalettes, - }; + groups.forEach((group, index) => { + objects2[`${name}Stump${index}`] = { + color: spr('stump', index, trunkPalettes[0], group), + //shadow: spr('stump shadow', index, shadowPalette, group), + palettes: trunkPalettes, + }; - if (hasStumpWinter) { - objects2[`${name}StumpWinter${index}`] = { - color: spr('stump winter', index, stumpWinterPalettes[0], group), - //shadow: spr('stump shadow', index, shadowPalette, group), - palettes: stumpWinterPalettes, - }; - } + if (hasStumpWinter) { + objects2[`${name}StumpWinter${index}`] = { + color: spr('stump winter', index, stumpWinterPalettes[0], group), + //shadow: spr('stump shadow', index, shadowPalette, group), + palettes: stumpWinterPalettes, + }; + } - if (findLayer('trunk', group)) { - objects2[`${name}Trunk${index}`] = { - color: spr('trunk', index, trunkPalettes[0], group), - palettes: trunkPalettes, - }; - } + if (findLayer('trunk', group)) { + objects2[`${name}Trunk${index}`] = { + color: spr('trunk', index, trunkPalettes[0], group), + palettes: trunkPalettes, + }; + } - const crown = spr('crown', index, crownPalettes[0], group); + const crown = spr('crown', index, crownPalettes[0], group); - for (let i = 0; i < groups.length; i++) { - objects2[`${name}Crown${index}_${i}`] = { - color: crown, - shadow: spr(`shadow ${index + 1}`, i, shadowPalette, groups[i]), - palettes: crownPalettes, - }; - } - }); + for (let i = 0; i < groups.length; i++) { + objects2[`${name}Crown${index}_${i}`] = { + color: crown, + shadow: spr(`shadow ${index + 1}`, i, shadowPalette, groups[i]), + palettes: crownPalettes, + }; + } + }); } function getTreesOrObjectFromPsd(result: Result, psd: Psd, name: string, palettes: number[][]) { - if (findLayer('color', psd)) { - const color = getLayerCanvas('color', psd); - const shadow = getLayerCanvas('shadow', psd); - addColorShadow(result, name, color, shadow, palettes); - } else { - getTreesFromPsd(result, psd, name, palettes); - } + if (findLayer('color', psd)) { + const color = getLayerCanvas('color', psd); + const shadow = getLayerCanvas('shadow', psd); + addColorShadow(result, name, color, shadow, palettes); + } else { + getTreesFromPsd(result, psd, name, palettes); + } } function getTreeStagesFromPsds(result: Result, psds: Psd[], name: string) { - const groups = range(1, 3).map(i => `tree ${i}`); - const layers = ['crown', 'trunk', 'stump']; - const palettes = createPalettes(psds, ['color', ...cartesian(groups, layers).map(([a, b]) => `${a}/${b}`)]); + const groups = range(1, 3).map(i => `tree ${i}`); + const layers = ['crown', 'trunk', 'stump']; + const palettes = createPalettes(psds, ['color', ...cartesian(groups, layers).map(([a, b]) => `${a}/${b}`)]); - psds - .filter(psd => !isPalettePsd(psd)) - .forEach(psd => getTreesOrObjectFromPsd(result, psd, `${name}_${psd.name}`, palettes)); + psds + .filter(psd => !isPalettePsd(psd)) + .forEach(psd => getTreesOrObjectFromPsd(result, psd, `${name}_${psd.name}`, palettes)); } function getTreesFromPsds(result: Result, directory: string) { - getDirectories(directory) - .filter(dir => !/^_/.test(path.basename(dir))) - .forEach(dir => getTreeStagesFromPsds(result, openPsdFiles(dir, /\d+\.psd$/), path.basename(dir))); + getDirectories(directory) + .filter(dir => !/^_/.test(path.basename(dir))) + .forEach(dir => getTreeStagesFromPsds(result, openPsdFiles(dir, /\d+\.psd$/), path.basename(dir))); } function addColorShadow( - { sprites, objects2 }: Result, name: string, color: ExtCanvas | undefined, - shadow: ExtCanvas | undefined, palettes: number[][] | undefined + { sprites, objects2 }: Result, name: string, color: ExtCanvas | undefined, + shadow: ExtCanvas | undefined, palettes: number[][] | undefined ) { - objects2[name] = { - color: addSprite(sprites, color, undefined, palettes && palettes[0]), - shadow: addSprite(sprites, shadow, undefined, shadowPalette), - palettes, - }; + objects2[name] = { + color: addSprite(sprites, color, undefined, palettes && palettes[0]), + shadow: addSprite(sprites, shadow, undefined, shadowPalette), + palettes, + }; } function getObjectFromPsd(result: Result, psd: Psd, name: string) { - const color = getLayerCanvas('color', psd); - const shadow = getLayerCanvas('shadow', psd); - const palettes = color ? [createPalette(color)] : undefined; - addColorShadow(result, name, color, shadow, palettes); + const color = getLayerCanvas('color', psd); + const shadow = getLayerCanvas('shadow', psd); + const palettes = color ? [createPalette(color)] : undefined; + addColorShadow(result, name, color, shadow, palettes); } function createOtherPalette(basePalette: number[], base: ExtCanvas, color: ExtCanvas, palette: number[]): number[] { - forEachPixelOf2Canvases(base, color, (b, c) => { - if (b !== c) { - const index = basePalette.indexOf(b); + forEachPixelOf2Canvases(base, color, (b, c) => { + if (b !== c) { + const index = basePalette.indexOf(b); - if (index === -1) { - throw new Error(`cannot find color in palette`); - } + if (index === -1) { + throw new Error(`cannot find color in palette`); + } - palette[index] = c; - } - }); + palette[index] = c; + } + }); - return palette; + return palette; } function otherPaletteFromPsd(basePalette: number[], basePsd: Psd, palettePsd: Psd, palette: number[], layers: string[]) { - return layers.reduce((palette, layer) => { - const base = getLayerCanvas(layer, basePsd); - const pal = getLayerCanvas(layer, palettePsd); - return base && pal ? createOtherPalette(basePalette, base, pal, palette) : palette; - }, palette); + return layers.reduce((palette, layer) => { + const base = getLayerCanvas(layer, basePsd); + const pal = getLayerCanvas(layer, palettePsd); + return base && pal ? createOtherPalette(basePalette, base, pal, palette) : palette; + }, palette); } function isPalettePsd(psd: Psd) { - return /^palette_/.test(psd.name); + return /^palette_/.test(psd.name); } function getLayerCanvases(names: string[], psd: Psd) { - return compact(names.map(name => getLayerCanvas(name, psd))); + return compact(names.map(name => getLayerCanvas(name, psd))); } function createPalettes(psds: Psd[], layers: string[]) { - const main = psds.filter(psd => !isPalettePsd(psd)); - const canvases = flatten(main.map(psd => getLayerCanvases(layers, psd))); - const palette = createPaletteFromList(canvases); - const otherPalettes = psds - .filter(isPalettePsd) - .map(psd => { - const [, index, name] = psd.name.split('_'); - return { index: +index, name, psd }; - }); - const paletteCount = max(otherPalettes.map(p => p.index)) || 0; - const other = range(1, paletteCount + 1) - .map(i => otherPalettes.filter(p => p.index === i)) - .filter(x => x.length) - .map(x => x.map(({ name, psd }) => ({ base: main.find(x => x.name === name)!, psd }))) - .map(x => x.reduce((pal, { base, psd }) => otherPaletteFromPsd(palette, base, psd, pal, layers), palette.slice())); + const main = psds.filter(psd => !isPalettePsd(psd)); + const canvases = flatten(main.map(psd => getLayerCanvases(layers, psd))); + const palette = createPaletteFromList(canvases); + const otherPalettes = psds + .filter(isPalettePsd) + .map(psd => { + const [, index, name] = psd.name.split('_'); + return { index: +index, name, psd }; + }); + const paletteCount = max(otherPalettes.map(p => p.index)) || 0; + const other = range(1, paletteCount + 1) + .map(i => otherPalettes.filter(p => p.index === i)) + .filter(x => x.length) + .map(x => x.map(({ name, psd }) => ({ base: main.find(x => x.name === name)!, psd }))) + .map(x => x.reduce((pal, { base, psd }) => otherPaletteFromPsd(palette, base, psd, pal, layers), palette.slice())); - return [palette, ...other]; + return [palette, ...other]; } function getObjectGroupFromPsd(result: Result, psds: Psd[], dir: string) { - const palettes = createPalettes(psds, ['color']); + const palettes = createPalettes(psds, ['color']); - psds - .filter(psd => !isPalettePsd(psd)) - .forEach(psd => { - const color = getLayerCanvas('color', psd); - const shadow = getLayerCanvas('shadow', psd); - addColorShadow(result, `${dir}_${psd.name}`, color, shadow, palettes); - }); + psds + .filter(psd => !isPalettePsd(psd)) + .forEach(psd => { + const color = getLayerCanvas('color', psd); + const shadow = getLayerCanvas('shadow', psd); + addColorShadow(result, `${dir}_${psd.name}`, color, shadow, palettes); + }); } function getObjectsFromPsds(result: Result, directory: string) { - openPsdFiles(directory, /psd$/) - .forEach(psd => getObjectFromPsd(result, psd, psd.name)); + openPsdFiles(directory, /psd$/) + .forEach(psd => getObjectFromPsd(result, psd, psd.name)); - getDirectories(directory) - .filter(dir => !/^_/.test(path.basename(dir))) - .forEach(dir => { - const files = openPsdFiles(dir, /psd$/) - .filter(psd => !/^_/.test(path.basename(psd.name))); + getDirectories(directory) + .filter(dir => !/^_/.test(path.basename(dir))) + .forEach(dir => { + const files = openPsdFiles(dir, /psd$/) + .filter(psd => !/^_/.test(path.basename(psd.name))); - getObjectGroupFromPsd(result, files, path.basename(dir)); - }); + getObjectGroupFromPsd(result, files, path.basename(dir)); + }); } function createOtherSprites({ objects, images }: Result, directory: string) { - getPngs(directory).forEach(f => { - const canvas = openPng(path.join(directory, f)); - const name = path.basename(f, '.png'); - objects[name] = addImage(images, canvas); - }); + getPngs(directory).forEach(f => { + const canvas = openPng(path.join(directory, f)); + const name = path.basename(f, '.png'); + objects[name] = addImage(images, canvas); + }); } function createOtherSpritesPalette({ objects2, sprites }: Result, directory: string) { - getPngs(directory).forEach(f => { - const canvas = openPng(path.join(directory, f)); - const name = path.basename(f, '.png'); - const palette = createPalette(canvas); - const color = addSprite(sprites, canvas, undefined, palette); - objects2[name + '_2'] = { color, palette }; - }); + getPngs(directory).forEach(f => { + const canvas = openPng(path.join(directory, f)); + const name = path.basename(f, '.png'); + const palette = createPalette(canvas); + const color = addSprite(sprites, canvas, undefined, palette); + objects2[name + '_2'] = { color, palette }; + }); } function createIcons({ objects2, sprites }: Result, directory: string) { - getPngs(directory).forEach(f => { - const canvas = openPng(path.join(directory, f)); - const name = path.basename(f, '.png'); - objects2[name] = addSprite(sprites, canvas, undefined, defaultPalette); - }); + getPngs(directory).forEach(f => { + const canvas = openPng(path.join(directory, f)); + const name = path.basename(f, '.png'); + objects2[name] = addSprite(sprites, canvas, undefined, defaultPalette); + }); } function createOtherSpritesAnimations({ objects, images }: Result, directory: string) { - openPsdFiles(directory).forEach(psd => { - const canvases = getFrames(psd.children).map(getCanvas); - const frames = canvases.map(c => c ? addImage(images, c) : 0); - objects[psd.name] = { frames }; - }); + openPsdFiles(directory).forEach(psd => { + const canvases = getFrames(psd.children).map(getCanvas); + const frames = canvases.map(c => c ? addImage(images, c) : 0); + objects[psd.name] = { frames }; + }); } function createButtons({ objects, objects2, images, sprites }: Result, directory: string) { - getPngs(directory).forEach(f => { - const [, name, borderText] = /^(.+)-(\d+)$/.exec(path.basename(f, '.png')) as string[]; - const border = +borderText; - const canvas = openPng(path.join(directory, f)); - const canvases = splitButton(canvas, border); - objects[name] = { border, ...mapValues(canvases, c => addImage(images, c)) }; + getPngs(directory).forEach(f => { + const [, name, borderText] = /^(.+)-(\d+)$/.exec(path.basename(f, '.png')) as string[]; + const border = +borderText; + const canvas = openPng(path.join(directory, f)); + const canvases = splitButton(canvas, border); + objects[name] = { border, ...mapValues(canvases, c => addImage(images, c)) }; - const palette = createPalette(canvas); - objects2[name + '_2'] = { border, palette, ...mapValues(canvases, c => addSprite(sprites, c, undefined, palette)) }; - }); + const palette = createPalette(canvas); + objects2[name + '_2'] = { border, palette, ...mapValues(canvases, c => addSprite(sprites, c, undefined, palette)) }; + }); } const lightsPad = 4; function createLights({ objects, images }: Result, directory: string) { - return getPngs(directory).map(f => { - const canvas = openPng(path.join(directory, f)); - const name = path.basename(f, '.png'); - return objects[name] = addImage(images, padCanvas(canvas, lightsPad, lightsPad, lightsPad, lightsPad, 'black')); - }); + return getPngs(directory).map(f => { + const canvas = openPng(path.join(directory, f)); + const name = path.basename(f, '.png'); + return objects[name] = addImage(images, padCanvas(canvas, lightsPad, lightsPad, lightsPad, lightsPad, 'black')); + }); } function createAnimations({ objects2, sprites }: Result, directory: string) { - getPngs(directory).forEach(f => { - const [, name, w, h] = /^(.+)-(\d+)x(\d+)\.png$/.exec(f)!; - const canvas = imageToCanvas(loadImage(path.join(directory, f))); - const spriteWidth = canvas.width / +w; - const spriteHeight = canvas.height / +h; - const palette = createPalette(canvas); - const frames = cartesian(range(+w), range(+h)) - .map(([x, y]) => cropCanvas(canvas, x * spriteWidth, y * spriteHeight, spriteWidth, spriteHeight)) - .map(bitmap => addSprite(sprites, bitmap, undefined, palette)); + getPngs(directory).forEach(f => { + const [, name, w, h] = /^(.+)-(\d+)x(\d+)\.png$/.exec(f)!; + const canvas = imageToCanvas(loadImage(path.join(directory, f))); + const spriteWidth = canvas.width / +w; + const spriteHeight = canvas.height / +h; + const palette = createPalette(canvas); + const frames = cartesian(range(+w), range(+h)) + .map(([x, y]) => cropCanvas(canvas, x * spriteWidth, y * spriteHeight, spriteWidth, spriteHeight)) + .map(bitmap => addSprite(sprites, bitmap, undefined, palette)); - objects2[name] = { frames, palette } as Animation; - }); + objects2[name] = { frames, palette } as Animation; + }); - openPsdFiles(directory).forEach(psd => { - const canvases = getFrames(psd.children).map(getCanvas); - const palette = createPaletteFromList(compact(canvases)); - const frames = canvases.map(c => addSprite(sprites, c, undefined, palette)); - const shadowLayer = findLayer('shadow', psd); - const shadow = shadowLayer ? addSprite(sprites, getCanvas(shadowLayer), undefined, shadowPalette) : undefined; + openPsdFiles(directory).forEach(psd => { + const canvases = getFrames(psd.children).map(getCanvas); + const palette = createPaletteFromList(compact(canvases)); + const frames = canvases.map(c => addSprite(sprites, c, undefined, palette)); + const shadowLayer = findLayer('shadow', psd); + const shadow = shadowLayer ? addSprite(sprites, getCanvas(shadowLayer), undefined, shadowPalette) : undefined; - objects2[psd.name] = { frames, palette, shadow } as Animation; - }); + objects2[psd.name] = { frames, palette, shadow } as Animation; + }); - getDirectories(directory) - .filter(dir => !/^_/.test(path.basename(dir))) - .forEach(dir => { - const psds = openPsdFiles(dir); - const dirName = path.basename(dir); - const canvases = flatten(psds.map(psd => compact(psd.children.filter(x => /^frame/i.test(x.name)).map(x => getCanvas(x))))); - const palette = createPaletteFromList(canvases); + getDirectories(directory) + .filter(dir => !/^_/.test(path.basename(dir))) + .forEach(dir => { + const psds = openPsdFiles(dir); + const dirName = path.basename(dir); + const canvases = flatten(psds.map(psd => compact(psd.children.filter(x => /^frame/i.test(x.name)).map(x => getCanvas(x))))); + const palette = createPaletteFromList(canvases); - for (const psd of psds) { - const canvases = getFrames(psd.children).map(getCanvas); - const frames = canvases.map(c => addSprite(sprites, c, undefined, palette)); - const shadowLayer = findLayer('shadow', psd); - const shadow = shadowLayer ? addSprite(sprites, getCanvas(shadowLayer), undefined, shadowPalette) : undefined; + for (const psd of psds) { + const canvases = getFrames(psd.children).map(getCanvas); + const frames = canvases.map(c => addSprite(sprites, c, undefined, palette)); + const shadowLayer = findLayer('shadow', psd); + const shadow = shadowLayer ? addSprite(sprites, getCanvas(shadowLayer), undefined, shadowPalette) : undefined; - objects2[`${dirName}_${psd.name}`] = { frames, palette, shadow } as Animation; - } - }); + objects2[`${dirName}_${psd.name}`] = { frames, palette, shadow } as Animation; + } + }); } function createEmoteAnimations({ objects2, sprites }: Result, directory: string) { - openPsdFiles(directory).forEach(psd => { - const frames = getFrames(psd.children) - .map(getCanvasSafe) - .map(c => addSprite(sprites, c, undefined, defaultPalette)); + openPsdFiles(directory).forEach(psd => { + const frames = getFrames(psd.children) + .map(getCanvasSafe) + .map(c => addSprite(sprites, c, undefined, defaultPalette)); - objects2[psd.name] = { frames, palette: [] } as Animation; - }); + objects2[psd.name] = { frames, palette: [] } as Animation; + }); } // .ts file generation @@ -699,140 +699,140 @@ function createEmoteAnimations({ objects2, sprites }: Result, directory: string) const palettes = [defaultPalette]; function createObject(name: string, value: any) { - if (!/^[a-z_][a-z0-9_]*(: [a-z]+(\[\])*)?$/i.test(name)) { - throw new Error(`Invalid sprite name (${name})`); - } + if (!/^[a-z_][a-z0-9_]*(: [a-z]+(\[\])*)?$/i.test(name)) { + throw new Error(`Invalid sprite name (${name})`); + } - return `export const ${name} = ${obj(value, name, true)};\n`; + return `export const ${name} = ${obj(value, name, true)};\n`; } function encodeArray(items: any[]) { - return `[${items.join(', ')}]`; + return `[${items.join(', ')}]`; } function addPalette(palette: number[]) { - const index = palettes.findIndex(p => isEqual(p, palette)); - const paletteIndex = index === -1 ? (palettes.push(palette) - 1) : index; - return paletteIndex; + const index = palettes.findIndex(p => isEqual(p, palette)); + const paletteIndex = index === -1 ? (palettes.push(palette) - 1) : index; + return paletteIndex; } function addPalettes(palettes: number[][]) { - return `[${palettes.map(addPalette).join(', ')}]`; + return `[${palettes.map(addPalette).join(', ')}]`; } function obj(value: any, name: string, indent = false): string { - if (value == null) { - return 'undefined'; - } else if (typeof value === 'string') { - return value; - } else if (typeof value === 'number') { - return `sprites[${value.toString()}]`; - } else if (Array.isArray(value)) { - if (/: StaticSprites(Extra)?$/.test(name)) { - // [type][pattern] - const types = value as (({ color: number; colors: number; extra?: number; palette?: number[]; } | null)[] | null)[]; - const key = name.replace(/: StaticSprites(Extra)?/, ''); - const lines: string[] = []; - // const typeCount = types.length; - // const patternCounts = types.map(type => type ? type.length : 0); - // const patternCountMax = max(patternCounts)!; - // const emptyLine = times(patternCountMax, () => 0); - // const colorsCounts = types.map(type => { - // type = type || []; - // // console.log(type); - // return [...type.map(p => p!.colors), ...emptyLine.slice(0, patternCountMax - type.length)]; - // }); + if (value == null) { + return 'undefined'; + } else if (typeof value === 'string') { + return value; + } else if (typeof value === 'number') { + return `sprites[${value.toString()}]`; + } else if (Array.isArray(value)) { + if (/: StaticSprites(Extra)?$/.test(name)) { + // [type][pattern] + const types = value as (({ color: number; colors: number; extra?: number; palette?: number[]; } | null)[] | null)[]; + const key = name.replace(/: StaticSprites(Extra)?/, ''); + const lines: string[] = []; + // const typeCount = types.length; + // const patternCounts = types.map(type => type ? type.length : 0); + // const patternCountMax = max(patternCounts)!; + // const emptyLine = times(patternCountMax, () => 0); + // const colorsCounts = types.map(type => { + // type = type || []; + // // console.log(type); + // return [...type.map(p => p!.colors), ...emptyLine.slice(0, patternCountMax - type.length)]; + // }); - // lines.push(`/* NEW */ export const ${key}SpritesArray = mapSprites2([\n${ - // types.map(patterns => ` ${ - // padArray(patterns, patternCountMax, { color: 0, colors: 0 }).map(x => x!.color).join(', ') - // },`).join('\n') - // }\n]);`); + // lines.push(`/* NEW */ export const ${key}SpritesArray = mapSprites2([\n${ + // types.map(patterns => ` ${ + // padArray(patterns, patternCountMax, { color: 0, colors: 0 }).map(x => x!.color).join(', ') + // },`).join('\n') + // }\n]);`); - // lines.push(`/* NEW */ export const ${key}TypeCount = ${typeCount};`); - // lines.push(`/* NEW */ export const ${key}PatternCounts = [${patternCounts.join(', ')}];`); - // lines.push(`/* NEW */ export const ${key}PatternCountsMax = ${patternCountMax};`); - // lines.push(`/* NEW */ export const ${key}PatternColorCounts = [\n` + - // `${colorsCounts.map(counts => ` ${counts.join(', ')},`).join('\n')}` + - // `\n];`); + // lines.push(`/* NEW */ export const ${key}TypeCount = ${typeCount};`); + // lines.push(`/* NEW */ export const ${key}PatternCounts = [${patternCounts.join(', ')}];`); + // lines.push(`/* NEW */ export const ${key}PatternCountsMax = ${patternCountMax};`); + // lines.push(`/* NEW */ export const ${key}PatternColorCounts = [\n` + + // `${colorsCounts.map(counts => ` ${counts.join(', ')},`).join('\n')}` + + // `\n];`); - if (/Extra$/.test(name)) { - lines.push(`export const ${key}Extra: StaticSprites = [\n${ - types.map(patterns => patterns ? - `\t[${patterns.map(x => x!.extra ? - `createColorPalette(${x!.extra}, [${addPalette(x!.palette!)}])` : - 'emptyColorPalette()').join(', ')}],` : - '\tundefined,' - ).join('\n')}\n];`); - } + if (/Extra$/.test(name)) { + lines.push(`export const ${key}Extra: StaticSprites = [\n${ + types.map(patterns => patterns ? + `\t[${patterns.map(x => x!.extra ? + `createColorPalette(${x!.extra}, [${addPalette(x!.palette!)}])` : + 'emptyColorPalette()').join(', ')}],` : + '\tundefined,' + ).join('\n')}\n];`); + } - const items = `\n${value.map((x, i) => '\t' + obj(x, `${name}[${i}]`)).join(',\n')}\n`; - return `[${indent ? items : items.replace(/\t/g, '').replace(/\n/g, ' ').trim()}];` + - '\n' + lines.join('\n') + '\n'; - } else { - const items = `\n${value.map((x, i) => '\t' + obj(x, `${name}[${i}]`)).join(',\n')}\n`; - return `[${indent ? items : items.replace(/\t/g, '').replace(/\n/g, ' ').trim()}]`; - } - } else { - return createObj(value, name); - } + const items = `\n${value.map((x, i) => '\t' + obj(x, `${name}[${i}]`)).join(',\n')}\n`; + return `[${indent ? items : items.replace(/\t/g, '').replace(/\n/g, ' ').trim()}];` + + '\n' + lines.join('\n') + '\n'; + } else { + const items = `\n${value.map((x, i) => '\t' + obj(x, `${name}[${i}]`)).join(',\n')}\n`; + return `[${indent ? items : items.replace(/\t/g, '').replace(/\n/g, ' ').trim()}]`; + } + } else { + return createObj(value, name); + } } function encodeColor(value: number) { - const alpha = value & 0xff; + const alpha = value & 0xff; - if (alpha === 0) { - return '0'; - } else if (alpha !== 0xff) { - return value.toString(16).padStart(8, '0'); - } else { - return (value >>> 8).toString(16).padStart(6, '0'); - } + if (alpha === 0) { + return '0'; + } else if (alpha !== 0xff) { + return value.toString(16).padStart(8, '0'); + } else { + return (value >>> 8).toString(16).padStart(6, '0'); + } } function createObj( - s: ColorShadow & Nose & Eye & Tree & Animation & ColorExtra & TileSprites & Button & Emote, name: string + s: ColorShadow & Nose & Eye & Tree & Animation & ColorExtra & TileSprites & Button & Emote, name: string ) { - if (s.frames && s.palette && s.shadow) { - return `createAnimationShadow(${encodeArray(s.frames)}, ${s.shadow}, ${addPalette(s.palette)})`; - } else if (s.frames && s.palette) { - return `createAnimationPalette(${encodeArray(s.frames)}, ${addPalette(s.palette)})`; - } else if (s.frames) { - return `createAnimation(${encodeArray(s.frames)})`; - } else if (s.fangs != null) { - return `createNose(${s.color}, ${s.colors}, ${s.mouth}, ${s.fangs})`; - } else if (s.color && s.colors && s.extra && s.palette) { - return `createColorExtraPal(${s.color}, ${s.colors}, ${s.extra}, [${addPalette(s.palette)}])`; - } else if (s.color && s.colors) { - return `colorPal${s.colors}(${s.color})`; - } else if (s.base && s.irises != null) { - return `createEye(${s.base}, ${encodeArray(s.irises)}, ${s.shadow}, ${s.shine})`; - } else if (s.color && s.shadow && s.palettes) { - return `createColorShadowPalette(${s.color}, ${s.shadow}, ${addPalettes(s.palettes)})`; - } else if (s.color && s.palettes) { - return `createColorPalette(${s.color}, ${addPalettes(s.palettes)})`; - } else if (s.sprites && s.palettes) { - return `createSpritesPalette(${encodeArray(s.sprites)}, ${addPalettes(s.palettes)})`; - } else if (s.color && s.palette) { - return `/* no palettes */ createColorPalette(${s.color}, [${addPalette(s.palette)}])`; - } else if (s.color && s.shadow) { - return `createColorShadow(${s.color}, ${s.shadow})`; - } else if (s.color) { - return `createColor(${s.color})`; - } else if (s.shadow) { - return `createShadow(${s.shadow})`; - } else if (s.topLeft) { - return `createButton(${s.border}, ${s.topLeft}, ${s.top}, ${s.topRight}, ${s.left}, ${s.bg},` - + ` ${s.right}, ${s.bottomLeft}, ${s.bottom}, ${s.bottomRight})`; - } else if (s.name && s.sprite) { - return `createEmote('${s.name}', ${s.sprite})`; - } else { - throw new Error(`Failed '${name}' createSprite(${JSON.stringify(s)})`); - } + if (s.frames && s.palette && s.shadow) { + return `createAnimationShadow(${encodeArray(s.frames)}, ${s.shadow}, ${addPalette(s.palette)})`; + } else if (s.frames && s.palette) { + return `createAnimationPalette(${encodeArray(s.frames)}, ${addPalette(s.palette)})`; + } else if (s.frames) { + return `createAnimation(${encodeArray(s.frames)})`; + } else if (s.fangs != null) { + return `createNose(${s.color}, ${s.colors}, ${s.mouth}, ${s.fangs})`; + } else if (s.color && s.colors && s.extra && s.palette) { + return `createColorExtraPal(${s.color}, ${s.colors}, ${s.extra}, [${addPalette(s.palette)}])`; + } else if (s.color && s.colors) { + return `colorPal${s.colors}(${s.color})`; + } else if (s.base && s.irises != null) { + return `createEye(${s.base}, ${encodeArray(s.irises)}, ${s.shadow}, ${s.shine})`; + } else if (s.color && s.shadow && s.palettes) { + return `createColorShadowPalette(${s.color}, ${s.shadow}, ${addPalettes(s.palettes)})`; + } else if (s.color && s.palettes) { + return `createColorPalette(${s.color}, ${addPalettes(s.palettes)})`; + } else if (s.sprites && s.palettes) { + return `createSpritesPalette(${encodeArray(s.sprites)}, ${addPalettes(s.palettes)})`; + } else if (s.color && s.palette) { + return `/* no palettes */ createColorPalette(${s.color}, [${addPalette(s.palette)}])`; + } else if (s.color && s.shadow) { + return `createColorShadow(${s.color}, ${s.shadow})`; + } else if (s.color) { + return `createColor(${s.color})`; + } else if (s.shadow) { + return `createShadow(${s.shadow})`; + } else if (s.topLeft) { + return `createButton(${s.border}, ${s.topLeft}, ${s.top}, ${s.topRight}, ${s.left}, ${s.bg},` + + ` ${s.right}, ${s.bottomLeft}, ${s.bottom}, ${s.bottomRight})`; + } else if (s.name && s.sprite) { + return `createEmote('${s.name}', ${s.sprite})`; + } else { + throw new Error(`Failed '${name}' createSprite(${JSON.stringify(s)})`); + } } function spriteType({ shade, layer }: Sprite) { - return shade ? (layer ? layer - 1 : (layer || 0)) : (3 + (layer || 0)); + return shade ? (layer ? layer - 1 : (layer || 0)) : (3 + (layer || 0)); } let minOX = 0, minOY = 0; @@ -840,451 +840,451 @@ let maxOX = 0, maxOY = 0; let maxW = 0, maxH = 0; function toHex(value: number): string { - const result = value.toString(16); - return result.length === 1 ? `0${result}` : result; + const result = value.toString(16); + return result.length === 1 ? `0${result}` : result; } function encodeSprite(s: Sprite) { - if (s.x > 0xfff || s.y > 0xfff || s.ox > 0xff || s.oy > 0xff || s.w > 0x1ff || s.h > 0x1ff || spriteType(s) > 0x3f) { - throw new Error(`Invalid sprite (${s})`); - } + if (s.x > 0xfff || s.y > 0xfff || s.ox > 0xff || s.oy > 0xff || s.w > 0x1ff || s.h > 0x1ff || spriteType(s) > 0x3f) { + throw new Error(`Invalid sprite (${s})`); + } - const buffer = bitWriter(write => { - write(s.x, 12); - write(s.y, 12); - write(s.w, 9); - write(s.h, 9); - write(s.ox, 8); - write(s.oy, 8); - write(spriteType(s), 6); - }); + const buffer = bitWriter(write => { + write(s.x, 12); + write(s.y, 12); + write(s.w, 9); + write(s.h, 9); + write(s.ox, 8); + write(s.oy, 8); + write(spriteType(s), 6); + }); - if (buffer.length !== 8) { - throw new Error(`Invalid encoded sprite length (${buffer.length} !== 8)`); - } + if (buffer.length !== 8) { + throw new Error(`Invalid encoded sprite length (${buffer.length} !== 8)`); + } - let result = ''; + let result = ''; - for (let i = 0; i < buffer.length; i++) { - result += toHex(buffer[i]); - } + for (let i = 0; i < buffer.length; i++) { + result += toHex(buffer[i]); + } - return result; + return result; } function toSpritesArray(sprites: (Sprite | null)[]): string { - const index = sprites.findIndex((s, i) => !!i && (!s || !s.w || !s.h)); + const index = sprites.findIndex((s, i) => !!i && (!s || !s.w || !s.h)); - if (index !== -1) { - console.error(`Invalid sprite at ${index}`, sprites[index]); - throw new Error(`Invalid sprite at ${index}`); - } + if (index !== -1) { + console.error(`Invalid sprite at ${index}`, sprites[index]); + throw new Error(`Invalid sprite at ${index}`); + } - return compact(sprites) - .filter(s => s.w && s.h) - .map(s => { - maxW = Math.max(maxW, s.w); - maxH = Math.max(maxH, s.h); - minOX = Math.min(minOX, s.ox); - maxOX = Math.max(maxOX, s.ox); - minOY = Math.min(minOY, s.oy); - maxOY = Math.max(maxOY, s.oy); - return encodeSprite(s); // `\t${s.x}, ${s.y}, ${s.w}, ${s.h}, ${s.ox}, ${s.oy}, ${spriteType(s)},\n`; - }) - .join('') - .trim(); + return compact(sprites) + .filter(s => s.w && s.h) + .map(s => { + maxW = Math.max(maxW, s.w); + maxH = Math.max(maxH, s.h); + minOX = Math.min(minOX, s.ox); + maxOX = Math.max(maxOX, s.ox); + minOY = Math.min(minOY, s.oy); + maxOY = Math.max(maxOY, s.oy); + return encodeSprite(s); // `\t${s.x}, ${s.y}, ${s.w}, ${s.h}, ${s.ox}, ${s.oy}, ${spriteType(s)},\n`; + }) + .join('') + .trim(); } interface FontCharGroup { - firstCode: number; - lastCode: number; - sprites: FontSprite[]; + firstCode: number; + lastCode: number; + sprites: FontSprite[]; } function groupFont(fontSprites: FontSprite[], maxWaste: number) { - const groups: FontCharGroup[] = []; - let group: FontCharGroup | undefined = undefined; + const groups: FontCharGroup[] = []; + let group: FontCharGroup | undefined = undefined; - for (const codeSprite of fontSprites) { - if (!group || (codeSprite.code - group.lastCode) > maxWaste) { - group = { firstCode: codeSprite.code, lastCode: codeSprite.code, sprites: [] }; - groups.push(group); - } + for (const codeSprite of fontSprites) { + if (!group || (codeSprite.code - group.lastCode) > maxWaste) { + group = { firstCode: codeSprite.code, lastCode: codeSprite.code, sprites: [] }; + groups.push(group); + } - while ((codeSprite.code - group.lastCode) > 1) { - group.sprites.push({ sprite: 0, code: 0 }); - group.lastCode++; - } + while ((codeSprite.code - group.lastCode) > 1) { + group.sprites.push({ sprite: 0, code: 0 }); + group.lastCode++; + } - group.sprites.push(codeSprite); - group.lastCode = codeSprite.code; - } + group.sprites.push(codeSprite); + group.lastCode = codeSprite.code; + } - return groups; + return groups; } function createFontCode(name: string, fontSprites: FontSprite[], sprites: string) { - const groups = groupFont(fontSprites, 5); + const groups = groupFont(fontSprites, 5); - return `export const ${name} = createFont(${sprites}, [\n` + - `${groups.map(g => ` [${g.firstCode}, [${g.sprites.map(s => s.sprite).join(', ')}]],`).join('\n')}\n]);`; + return `export const ${name} = createFont(${sprites}, [\n` + + `${groups.map(g => ` [${g.firstCode}, [${g.sprites.map(s => s.sprite).join(', ')}]],`).join('\n')}\n]);`; } interface SpriteTSConfig { - spriteFileName: string; - paletteFileName: string; - paletteAlphaFileName: string; - sprites: (Sprite | null)[]; - paletteSprites: (Sprite | null)[]; - result: Result; - palettes: number[][]; - fonts: { [key: string]: FontSprite[]; }; - fontsPal: { [key: string]: FontSprite[]; }; - namedPalettes: { [key: string]: number; }; + spriteFileName: string; + paletteFileName: string; + paletteAlphaFileName: string; + sprites: (Sprite | null)[]; + paletteSprites: (Sprite | null)[]; + result: Result; + palettes: number[][]; + fonts: { [key: string]: FontSprite[]; }; + fontsPal: { [key: string]: FontSprite[]; }; + namedPalettes: { [key: string]: number; }; } function createSpritesTS(dest: string, config: SpriteTSConfig) { - const { objects, objects2 } = config.result; + const { objects, objects2 } = config.result; - let ts = fs.readFileSync(path.join(rootPath, 'src', 'ts', 'tools', 'sprites-template.ts'), 'utf8'); - ts = ts.replace(/export \{.+?\r\n/, ''); - ts = ts.replace('/*SPRITE_SHEET*/', `images/${config.spriteFileName}`); - ts = ts.replace('/*SPRITE_SHEET_PALETTE*/', `images/${config.paletteFileName}`); - ts = ts.replace('/*SPRITE_SHEET_PALETTE_ALPHA*/', `images/${config.paletteAlphaFileName}`); - ts = ts.replace('/*SPRITES*/', toSpritesArray(config.sprites)); - ts = ts.replace('/*SPRITES_PALETTE*/', toSpritesArray(config.paletteSprites)); - ts = ts.replace('/*FONTS*/', [ - Object.keys(config.fonts).map(key => createFontCode(key, config.fonts[key], 'sprites')).join('\n\n'), - Object.keys(config.fontsPal).map(key => createFontCode(key, config.fontsPal[key], 'sprites2')).join('\n\n'), - ].join('\n\n')); + let ts = fs.readFileSync(path.join(rootPath, 'src', 'ts', 'tools', 'sprites-template.ts'), 'utf8'); + ts = ts.replace(/export \{.+?\r\n/, ''); + ts = ts.replace('/*SPRITE_SHEET*/', `images/${config.spriteFileName}`); + ts = ts.replace('/*SPRITE_SHEET_PALETTE*/', `images/${config.paletteFileName}`); + ts = ts.replace('/*SPRITE_SHEET_PALETTE_ALPHA*/', `images/${config.paletteAlphaFileName}`); + ts = ts.replace('/*SPRITES*/', toSpritesArray(config.sprites)); + ts = ts.replace('/*SPRITES_PALETTE*/', toSpritesArray(config.paletteSprites)); + ts = ts.replace('/*FONTS*/', [ + Object.keys(config.fonts).map(key => createFontCode(key, config.fonts[key], 'sprites')).join('\n\n'), + Object.keys(config.fontsPal).map(key => createFontCode(key, config.fontsPal[key], 'sprites2')).join('\n\n'), + ].join('\n\n')); - ts += Object.keys(objects).map(key => createObject(key, objects[key])).join(''); - ts += Object.keys(objects2).map(key => createObject(key, objects2[key])).join('').replace(/sprites/g, 'sprites2'); + ts += Object.keys(objects).map(key => createObject(key, objects[key])).join(''); + ts += Object.keys(objects2).map(key => createObject(key, objects2[key])).join('').replace(/sprites/g, 'sprites2'); - const colors = uniq(flatten(config.palettes)).sort((a, b) => a - b); - const palettesCode = config.palettes - .map(p => p.map(c => colors.indexOf(c)).join(', ')) - .map(p => `\t[${p}]`) - .join(',\n') - .trim(); + const colors = uniq(flatten(config.palettes)).sort((a, b) => a - b); + const palettesCode = config.palettes + .map(p => p.map(c => colors.indexOf(c)).join(', ')) + .map(p => `\t[${p}]`) + .join(',\n') + .trim(); - ts = ts.replace('/*COLORS*/', colors.map(encodeColor).join(' ')); - ts = ts.replace('/*PALETTES*/', palettesCode); - ts = ts.replace('/*NAMED_PALETTES*/', toPairs(config.namedPalettes) - .map(([key, value]) => `export const ${key} = palettes[${value}];`).join('\n')); - ts = ts.replace('/*NAMED_SPRITES*/', `export const emptySprite = sprites[0];\nexport const emptySprite2 = sprites2[0];`); + ts = ts.replace('/*COLORS*/', colors.map(encodeColor).join(' ')); + ts = ts.replace('/*PALETTES*/', palettesCode); + ts = ts.replace('/*NAMED_PALETTES*/', toPairs(config.namedPalettes) + .map(([key, value]) => `export const ${key} = palettes[${value}];`).join('\n')); + ts = ts.replace('/*NAMED_SPRITES*/', `export const emptySprite = sprites[0];\nexport const emptySprite2 = sprites2[0];`); - ts += ` + ts += ` export const head0 = [ - undefined, - [[${head0Indices.join(', ')}].map(i => head[1]![0]![i])], + undefined, + [[${head0Indices.join(', ')}].map(i => head[1]![0]![i])], ]; export const head1 = [ - undefined, - [[${head1Indices.join(', ')}].map(i => head[1]![0]![i])], + undefined, + [[${head1Indices.join(', ')}].map(i => head[1]![0]![i])], ]; - `; + `; - fs.writeFileSync(dest, ts.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'), 'utf8'); + fs.writeFileSync(dest, ts.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'), 'utf8'); } function fixPixelRect(sprites: (Sprite | null)[], objects: any, srcName: string, dstName: string) { - const r = sprites[objects[srcName]]!; - sprites.push({ x: r.x + 1, y: r.y + 1, w: 1, h: 1, ox: 0, oy: 0, layer: r.layer, image: null as any }); - objects[dstName] = sprites.length - 1; + const r = sprites[objects[srcName]]!; + sprites.push({ x: r.x + 1, y: r.y + 1, w: 1, h: 1, ox: 0, oy: 0, layer: r.layer, image: null as any }); + objects[dstName] = sprites.length - 1; } export function getFramesFromPSD({ sprites }: Result, psd: Psd, padY = 5): any[] { - return findLayerSafe('frames', psd).children - .slice() - .sort(compareLayers) - .map(getCanvasSafe) - .map(canvas => { - const cropped = padCanvas(canvas, 0, padY); - const pattern = clipPattern(cropped, colorCanvas(cropped, 'red')); - return addSpriteWithColors(sprites, cropped, pattern); - }); + return findLayerSafe('frames', psd).children + .slice() + .sort(compareLayers) + .map(getCanvasSafe) + .map(canvas => { + const cropped = padCanvas(canvas, 0, padY); + const pattern = clipPattern(cropped, colorCanvas(cropped, 'red')); + return addSpriteWithColors(sprites, cropped, pattern); + }); } // main function createResult(): Result { - return { - objects: {}, - objects2: {}, - images: [createExtCanvas(1, 1, 'empty')], - sprites: [{ image: createExtCanvas(1, 1, 'empty'), x: 0, y: 0, w: 0, h: 0, ox: 0, oy: 0 }], - }; + return { + objects: {}, + objects2: {}, + images: [createExtCanvas(1, 1, 'empty')], + sprites: [{ image: createExtCanvas(1, 1, 'empty'), x: 0, y: 0, w: 0, h: 0, ox: 0, oy: 0 }], + }; } function createPonySprites(result: Result) { - getEyesFromPsd(result, ponyPsd('eyes.psd'), ponyPsd('irises.psd')); - getMuzzlesFromPsd(result, ponyPsd('muzzles.psd')); - getBlushFromPsd(result, ponyPsd('blush.psd')); - getPonyShadowsAndSelection(result, ponyPsd('shadows.psd')); + getEyesFromPsd(result, ponyPsd('eyes.psd'), ponyPsd('irises.psd')); + getMuzzlesFromPsd(result, ponyPsd('muzzles.psd')); + getBlushFromPsd(result, ponyPsd('blush.psd')); + getPonyShadowsAndSelection(result, ponyPsd('shadows.psd')); - result.objects2.cms = addCMSprite(result.sprites, false); - result.objects2.cmsFlip = addCMSprite(result.sprites, true); + result.objects2.cms = addCMSprite(result.sprites, false); + result.objects2.cmsFlip = addCMSprite(result.sprites, true); - sheets - .filter(s => 'name' in s && !!s.file && !s.skipImport) - .forEach(s => importSprites(result, s as Sheet)); + sheets + .filter(s => 'name' in s && !!s.file && !s.skipImport) + .forEach(s => importSprites(result, s as Sheet)); - // const bug = getFramesFromPSD(result, ponyPsd('fly-bug.psd')); - // result.objects2['wings: AnimatedSprites'].slice(3) - // .forEach((frame: any, i: number) => frame[1] = [pegasus[i]]); + // const bug = getFramesFromPSD(result, ponyPsd('fly-bug.psd')); + // result.objects2['wings: AnimatedSprites'].slice(3) + // .forEach((frame: any, i: number) => frame[1] = [pegasus[i]]); - getTreesFromPsds(result, path.join(sourcePath, 'trees')); - getObjectsFromPsds(result, path.join(sourcePath, 'objects')); + getTreesFromPsds(result, path.join(sourcePath, 'trees')); + getObjectsFromPsds(result, path.join(sourcePath, 'objects')); - createButtons(result, path.join(sourcePath, 'buttons')); - createAnimations(result, path.join(sourcePath, 'animations')); - createOtherSprites(result, path.join(sourcePath, 'sprites')); - createOtherSpritesPalette(result, path.join(sourcePath, 'sprites-palette')); - createIcons(result, path.join(sourcePath, 'icons')); - createOtherSpritesAnimations(result, path.join(sourcePath, 'sprites-animations')); - createEmoteAnimations(result, path.join(sourcePath, 'emotes')); - createWalls(result, path.join(sourcePath, 'walls')); + createButtons(result, path.join(sourcePath, 'buttons')); + createAnimations(result, path.join(sourcePath, 'animations')); + createOtherSprites(result, path.join(sourcePath, 'sprites')); + createOtherSpritesPalette(result, path.join(sourcePath, 'sprites-palette')); + createIcons(result, path.join(sourcePath, 'icons')); + createOtherSpritesAnimations(result, path.join(sourcePath, 'sprites-animations')); + createEmoteAnimations(result, path.join(sourcePath, 'emotes')); + createWalls(result, path.join(sourcePath, 'walls')); - createPixelSprites(result); + createPixelSprites(result); } interface WallConfig { - thickness: number; - fullHeight: number; - halfHeight: number; - fullHeightVertical: number; - halfHeightVertical: number; + thickness: number; + fullHeight: number; + halfHeight: number; + fullHeightVertical: number; + halfHeightVertical: number; } function createWalls(result: Result, rootPath: string) { - createWall(result, 'wall_wood', openPsd(path.join(rootPath, 'wood.psd')), { - thickness: 8, fullHeight: 85, halfHeight: 22, fullHeightVertical: 97, halfHeightVertical: 34, - }); + createWall(result, 'wall_wood', openPsd(path.join(rootPath, 'wood.psd')), { + thickness: 8, fullHeight: 85, halfHeight: 22, fullHeightVertical: 97, halfHeightVertical: 34, + }); - createWall(result, 'wall_stone', openPsd(path.join(rootPath, 'stone.psd')), { - thickness: 8, fullHeight: 85, halfHeight: 22, fullHeightVertical: 97, halfHeightVertical: 34, - }); + createWall(result, 'wall_stone', openPsd(path.join(rootPath, 'stone.psd')), { + thickness: 8, fullHeight: 85, halfHeight: 22, fullHeightVertical: 97, halfHeightVertical: 34, + }); } function createWall(result: Result, name: string, psd: Psd, config: WallConfig) { - const full = getCanvasSafe(findLayerSafe('full', psd)); - const half = getCanvasSafe(findLayerSafe('half', psd)); - const palette = createPaletteFromList([full, half]); - const h0wall = 16; - const v0wall = 17; - const lcut = 18; - const rcut = 19; - const thickness = config.thickness; - const map: { x: number; w: number; vertical: boolean; }[] = []; - let offset = 0; + const full = getCanvasSafe(findLayerSafe('full', psd)); + const half = getCanvasSafe(findLayerSafe('half', psd)); + const palette = createPaletteFromList([full, half]); + const h0wall = 16; + const v0wall = 17; + const lcut = 18; + const rcut = 19; + const thickness = config.thickness; + const map: { x: number; w: number; vertical: boolean; }[] = []; + let offset = 0; - function push(index: number, w: number, vertical = false) { - if (map[index]) { - throw new Error('Already taken'); - } + function push(index: number, w: number, vertical = false) { + if (map[index]) { + throw new Error('Already taken'); + } - map[index] = { x: offset, w, vertical }; - offset += w; - } + map[index] = { x: offset, w, vertical }; + offset += w; + } - function gap() { - offset++; - } + function gap() { + offset++; + } - function createSprites(canvas: ExtCanvas, y: number, height: number, verticalHeight: number) { - return map - .map(({ x, w, vertical }) => cropCanvas(canvas, x, y, w, vertical ? verticalHeight : height)) - .map(part => ({ palette, color: addSprite(result.sprites, part, undefined, palette) })); - } + function createSprites(canvas: ExtCanvas, y: number, height: number, verticalHeight: number) { + return map + .map(({ x, w, vertical }) => cropCanvas(canvas, x, y, w, vertical ? verticalHeight : height)) + .map(part => ({ palette, color: addSprite(result.sprites, part, undefined, palette) })); + } - // 0b top right bottom left - push(0b0100, thickness); - push(h0wall, 32 - thickness); - push(0b0101, thickness); - push(0b0001, thickness); - gap(); - push(0b0000, thickness); - gap(); - push(0b0010, thickness); - gap(); - push(0b1000, thickness); - gap(); - push(0b1100, thickness); - push(0b1101, thickness); - push(0b1001, thickness); - gap(); - push(0b0110, thickness); - push(0b0111, thickness); - push(0b0011, thickness); - gap(); - push(0b1010, thickness); - gap(); - push(0b1110, thickness); - push(0b1111, thickness); - push(0b1011, thickness); - gap(); - push(v0wall, thickness, true); - gap(); - push(lcut, 32 - thickness); - push(rcut, 32 - thickness); + // 0b top right bottom left + push(0b0100, thickness); + push(h0wall, 32 - thickness); + push(0b0101, thickness); + push(0b0001, thickness); + gap(); + push(0b0000, thickness); + gap(); + push(0b0010, thickness); + gap(); + push(0b1000, thickness); + gap(); + push(0b1100, thickness); + push(0b1101, thickness); + push(0b1001, thickness); + gap(); + push(0b0110, thickness); + push(0b0111, thickness); + push(0b0011, thickness); + gap(); + push(0b1010, thickness); + gap(); + push(0b1110, thickness); + push(0b1111, thickness); + push(0b1011, thickness); + gap(); + push(v0wall, thickness, true); + gap(); + push(lcut, 32 - thickness); + push(rcut, 32 - thickness); - result.objects2[`${name}_full`] = createSprites(full, 0, config.fullHeight, config.fullHeightVertical); + result.objects2[`${name}_full`] = createSprites(full, 0, config.fullHeight, config.fullHeightVertical); - map.pop(); - map.pop(); + map.pop(); + map.pop(); - result.objects2[`${name}_half`] = createSprites( - half, config.fullHeight - config.halfHeight, config.halfHeight, config.halfHeightVertical); + result.objects2[`${name}_half`] = createSprites( + half, config.fullHeight - config.halfHeight, config.halfHeight, config.halfHeightVertical); } function createTileSprites({ sprites, objects2 }: Result) { - const basePath = path.join(sourcePath, 'tiles'); - const types = [ - { name: 'grassTiles', file: 'dirt-grass.png', space: 1, alts: ['dirt-grass-autumn.png'] }, - { name: 'snowTiles', file: 'dirt-snow.png', space: 1, alts: [] }, - { name: 'woodTiles', file: 'wood-tiles.png', space: 1, alts: [] }, - { name: 'stoneTiles', file: 'stone-tiles.png', space: 1, alts: [] }, - { name: 'stone2Tiles', file: 'stone2-tiles.png', space: 1, alts: [] }, - { - name: 'waterTiles1', file: 'dirt-water-1.png', space: 1, - alts: ['dirt-water-1-autumn.png', 'dirt-water-1-winter.png', 'dirt-water-1-cave.png'] - }, - { name: 'waterTiles2', file: 'dirt-water-2.png', space: 1, alts: [] }, - { name: 'waterTiles3', file: 'dirt-water-3.png', space: 1, alts: [] }, - { name: 'waterTiles4', file: 'dirt-water-4.png', space: 1, alts: [] }, - { name: 'iceTiles', file: 'dirt-ice.png', space: 0, alts: ['dirt-ice-autumn.png', 'dirt-ice-winter.png'] }, - { name: 'grassTilesNew', file: 'grass.png', space: 1, alts: [] }, - { name: 'snowOnIceTiles', file: 'ice-snow.png', space: 1, alts: [] }, - { name: 'caveTiles', file: 'dirt-stone-cave.png', space: 1, alts: [] }, - ]; + const basePath = path.join(sourcePath, 'tiles'); + const types = [ + { name: 'grassTiles', file: 'dirt-grass.png', space: 1, alts: ['dirt-grass-autumn.png'] }, + { name: 'snowTiles', file: 'dirt-snow.png', space: 1, alts: [] }, + { name: 'woodTiles', file: 'wood-tiles.png', space: 1, alts: [] }, + { name: 'stoneTiles', file: 'stone-tiles.png', space: 1, alts: [] }, + { name: 'stone2Tiles', file: 'stone2-tiles.png', space: 1, alts: [] }, + { + name: 'waterTiles1', file: 'dirt-water-1.png', space: 1, + alts: ['dirt-water-1-autumn.png', 'dirt-water-1-winter.png', 'dirt-water-1-cave.png'] + }, + { name: 'waterTiles2', file: 'dirt-water-2.png', space: 1, alts: [] }, + { name: 'waterTiles3', file: 'dirt-water-3.png', space: 1, alts: [] }, + { name: 'waterTiles4', file: 'dirt-water-4.png', space: 1, alts: [] }, + { name: 'iceTiles', file: 'dirt-ice.png', space: 0, alts: ['dirt-ice-autumn.png', 'dirt-ice-winter.png'] }, + { name: 'grassTilesNew', file: 'grass.png', space: 1, alts: [] }, + { name: 'snowOnIceTiles', file: 'ice-snow.png', space: 1, alts: [] }, + { name: 'caveTiles', file: 'dirt-stone-cave.png', space: 1, alts: [] }, + ]; - types.forEach(({ name, file, alts, space }) => { - const tileSprites = tilesToSprites(openPng(path.join(basePath, file)), space, space); - const palette = createPaletteFromList(tileSprites); - const palettes = [palette, ...alts.map(altFile => { - const altSprites = tilesToSprites(openPng(path.join(basePath, altFile)), space, space); - const altPalette = palette.slice(); - return altSprites.reduce((pal, s, i) => createOtherPalette(palette, tileSprites[i], s, pal), altPalette); - })]; + types.forEach(({ name, file, alts, space }) => { + const tileSprites = tilesToSprites(openPng(path.join(basePath, file)), space, space); + const palette = createPaletteFromList(tileSprites); + const palettes = [palette, ...alts.map(altFile => { + const altSprites = tilesToSprites(openPng(path.join(basePath, altFile)), space, space); + const altPalette = palette.slice(); + return altSprites.reduce((pal, s, i) => createOtherPalette(palette, tileSprites[i], s, pal), altPalette); + })]; - objects2[name] = { - palettes, - sprites: tileSprites.map(s => addSprite(sprites, s, undefined, palette)), - }; - }); + objects2[name] = { + palettes, + sprites: tileSprites.map(s => addSprite(sprites, s, undefined, palette)), + }; + }); - const otherTiles: string[] = []; + const otherTiles: string[] = []; - otherTiles.forEach(tile => { - const image = openPng(path.join(basePath, `${tile}.png`)); - const palette = createPaletteFromList([image]); + otherTiles.forEach(tile => { + const image = openPng(path.join(basePath, `${tile}.png`)); + const palette = createPaletteFromList([image]); - objects2[`${tile}Tiles`] = { - palettes: [palette], - sprites: [addSprite(sprites, image, undefined, palette)], - }; - }); + objects2[`${tile}Tiles`] = { + palettes: [palette], + sprites: [addSprite(sprites, image, undefined, palette)], + }; + }); - const cliffs = openPng(path.join(sourcePath, 'tiles', 'cliffs-grass.png')); - const cliffsAutumn = openPng(path.join(sourcePath, 'tiles', 'cliffs-grass-autumn.png')); - const cliffsWinter = openPng(path.join(sourcePath, 'tiles', 'cliffs-grass-winter.png')); - const cliffsPalette = createPaletteFromList([cliffs]); - const cliffsPaletteAutumn = createOtherPalette(cliffsPalette, cliffs, cliffsAutumn, [...cliffsPalette]); - const cliffsPaletteWinter = createOtherPalette(cliffsPalette, cliffs, cliffsWinter, [...cliffsPalette]); + const cliffs = openPng(path.join(sourcePath, 'tiles', 'cliffs-grass.png')); + const cliffsAutumn = openPng(path.join(sourcePath, 'tiles', 'cliffs-grass-autumn.png')); + const cliffsWinter = openPng(path.join(sourcePath, 'tiles', 'cliffs-grass-winter.png')); + const cliffsPalette = createPaletteFromList([cliffs]); + const cliffsPaletteAutumn = createOtherPalette(cliffsPalette, cliffs, cliffsAutumn, [...cliffsPalette]); + const cliffsPaletteWinter = createOtherPalette(cliffsPalette, cliffs, cliffsWinter, [...cliffsPalette]); - const cave = openPng(path.join(sourcePath, 'tiles', 'cave-walls.png')); - const cavePalette = createPaletteFromList([cave]); + const cave = openPng(path.join(sourcePath, 'tiles', 'cave-walls.png')); + const cavePalette = createPaletteFromList([cave]); - createCliffs('cliffs_grass', cliffs, [cliffsPalette, cliffsPaletteAutumn, cliffsPaletteWinter]); - createCliffs('cave_walls', cave, [cavePalette]); + createCliffs('cliffs_grass', cliffs, [cliffsPalette, cliffsPaletteAutumn, cliffsPaletteWinter]); + createCliffs('cave_walls', cave, [cavePalette]); - function createCliffs(baseName: string, canvas: ExtCanvas, palettes: number[][]) { - function addCliffSprite(name: string, x: number, y: number, w = 1, h = 1) { - const color = addSprite(sprites, cropCanvas(canvas, 32 * x, 24 * y, 32 * w, 24 * h), undefined, palettes[0]); - objects2[`${baseName}_${name}`] = { color, palettes }; - } + function createCliffs(baseName: string, canvas: ExtCanvas, palettes: number[][]) { + function addCliffSprite(name: string, x: number, y: number, w = 1, h = 1) { + const color = addSprite(sprites, cropCanvas(canvas, 32 * x, 24 * y, 32 * w, 24 * h), undefined, palettes[0]); + objects2[`${baseName}_${name}`] = { color, palettes }; + } - addCliffSprite('decal_1', 3, 1); - addCliffSprite('decal_2', 3, 2); - addCliffSprite('decal_3', 4, 2); - addCliffSprite('decal_l', 4, 0); - addCliffSprite('decal_r', 4, 1); + addCliffSprite('decal_1', 3, 1); + addCliffSprite('decal_2', 3, 2); + addCliffSprite('decal_3', 4, 2); + addCliffSprite('decal_l', 4, 0); + addCliffSprite('decal_r', 4, 1); - addCliffSprite('top_nw', 1, 0); - addCliffSprite('top_n', 3, 0); - addCliffSprite('top_ne', 5, 0); - addCliffSprite('top_w', 1, 1); - addCliffSprite('top_e', 5, 1); - addCliffSprite('top_sw', 1, 2); - addCliffSprite('top_se', 5, 2); - addCliffSprite('top_s1', 2, 3); - addCliffSprite('top_s2', 3, 3); - addCliffSprite('top_s3', 4, 3); - addCliffSprite('mid_sw1', 1, 4); - addCliffSprite('mid_sw2', 1, 3); - addCliffSprite('mid_s1', 2, 4); - addCliffSprite('mid_s2', 3, 4); - addCliffSprite('mid_s3', 4, 4); - addCliffSprite('mid_se1', 5, 4); - addCliffSprite('mid_se2', 5, 3); - addCliffSprite('bot_sw', 1, 5); - addCliffSprite('bot_s1', 2, 5); - addCliffSprite('bot_s2', 3, 5); - addCliffSprite('bot_s3', 4, 5); - addCliffSprite('bot_se', 5, 5); + addCliffSprite('top_nw', 1, 0); + addCliffSprite('top_n', 3, 0); + addCliffSprite('top_ne', 5, 0); + addCliffSprite('top_w', 1, 1); + addCliffSprite('top_e', 5, 1); + addCliffSprite('top_sw', 1, 2); + addCliffSprite('top_se', 5, 2); + addCliffSprite('top_s1', 2, 3); + addCliffSprite('top_s2', 3, 3); + addCliffSprite('top_s3', 4, 3); + addCliffSprite('mid_sw1', 1, 4); + addCliffSprite('mid_sw2', 1, 3); + addCliffSprite('mid_s1', 2, 4); + addCliffSprite('mid_s2', 3, 4); + addCliffSprite('mid_s3', 4, 4); + addCliffSprite('mid_se1', 5, 4); + addCliffSprite('mid_se2', 5, 3); + addCliffSprite('bot_sw', 1, 5); + addCliffSprite('bot_s1', 2, 5); + addCliffSprite('bot_s2', 3, 5); + addCliffSprite('bot_s3', 4, 5); + addCliffSprite('bot_se', 5, 5); - addCliffSprite('top_sb', 2, 0); - addCliffSprite('mid_sb', 2, 1); - addCliffSprite('bot_sb', 2, 2); + addCliffSprite('top_sb', 2, 0); + addCliffSprite('mid_sb', 2, 1); + addCliffSprite('bot_sb', 2, 2); - addCliffSprite('top_trim_left', 0, 1); - addCliffSprite('mid_trim_left', 0, 2); - addCliffSprite('bot_trim_left', 0, 4); - addCliffSprite('top_trim_right', 6, 1); - addCliffSprite('mid_trim_right', 6, 2); - addCliffSprite('bot_trim_right', 6, 4); + addCliffSprite('top_trim_left', 0, 1); + addCliffSprite('mid_trim_left', 0, 2); + addCliffSprite('bot_trim_left', 0, 4); + addCliffSprite('top_trim_right', 6, 1); + addCliffSprite('mid_trim_right', 6, 2); + addCliffSprite('bot_trim_right', 6, 4); - // combined sections - addCliffSprite('sw', 1, 2, 1, 4); - addCliffSprite('s1', 2, 3, 1, 3); - addCliffSprite('s2', 3, 3, 1, 3); - addCliffSprite('s3', 4, 3, 1, 3); - addCliffSprite('sb', 2, 0, 1, 3); - addCliffSprite('se', 5, 2, 1, 4); - } + // combined sections + addCliffSprite('sw', 1, 2, 1, 4); + addCliffSprite('s1', 2, 3, 1, 3); + addCliffSprite('s2', 3, 3, 1, 3); + addCliffSprite('s3', 4, 3, 1, 3); + addCliffSprite('sb', 2, 0, 1, 3); + addCliffSprite('se', 5, 2, 1, 4); + } } function fontCanvas(name: string) { - const filePath = path.join(sourcePath, 'fonts', name); - const file = openPsd(filePath); - return getLayerCanvasSafe('color', file); + const filePath = path.join(sourcePath, 'fonts', name); + const file = openPsd(filePath); + return getLayerCanvasSafe('color', file); } function createStripedCanvas(canvas: ExtCanvas, colors: number[]) { - const stripes = createExtCanvas(canvas.width, canvas.height, `${canvas.info} (stripes)`); - const context = stripes.getContext('2d')!; + const stripes = createExtCanvas(canvas.width, canvas.height, `${canvas.info} (stripes)`); + const context = stripes.getContext('2d')!; - for (let y = 0; y < canvas.height; y++) { - context.fillStyle = colorToCSS(colors[y % colors.length]); - context.fillRect(0, y, canvas.width, 1); - } + for (let y = 0; y < canvas.height; y++) { + context.fillStyle = colorToCSS(colors[y % colors.length]); + context.fillRect(0, y, canvas.width, 1); + } - context.globalCompositeOperation = 'destination-in'; - context.drawImage(canvas, 0, 0); - return stripes; + context.globalCompositeOperation = 'destination-in'; + context.drawImage(canvas, 0, 0); + return stripes; } function bandedPalette(colors: number[], bands: number[]) { - return flatten(bands.map((t, i) => times(t, () => colors[i] >>> 0))); + return flatten(bands.map((t, i) => times(t, () => colors[i] >>> 0))); } function bandedTextPalette(colors: number[]) { - return bandedPalette(colors, [3, 2, 3, 2]); + return bandedPalette(colors, [3, 2, 3, 2]); } function bandedTinyPalette(colors: number[]) { - return bandedPalette(colors, [2, 2, 2, 3]); + return bandedPalette(colors, [2, 2, 2, 3]); } const SUPPORTER1 = 0xf86754ff; @@ -1292,108 +1292,108 @@ const SUPPORTER2_BANDS = [0xffdfc1ff, 0xffcd99ff, 0xff9f3bff, 0xd97e09ff]; const SUPPORTER3_BANDS = [0xffffffff, 0xfffda4ff, 0xffea3bff, 0xfdbb0bff]; export function createSprites(log: boolean) { - mkdir(destPath); - mkdir(generatedPath); + mkdir(destPath); + mkdir(generatedPath); - const result = createResult(); + const result = createResult(); - createPonySprites(result); - createTileSprites(result); + createPonySprites(result); + createTileSprites(result); - const mainFont = fontCanvas('main.psd'); - const mainEmoji = fontCanvas('emoji.psd'); - const tinyFont = fontCanvas('tiny.psd'); - const monoFont = fontCanvas('mono.psd'); - const emojiPalette = createPalette(mainEmoji); + const mainFont = fontCanvas('main.psd'); + const mainEmoji = fontCanvas('emoji.psd'); + const tinyFont = fontCanvas('tiny.psd'); + const monoFont = fontCanvas('mono.psd'); + const emojiPalette = createPalette(mainEmoji); - const mainFontPalette = [TRANSPARENT, ...times(10, i => 0xff + i * 256)]; - const stripedMainFont = createStripedCanvas(mainFont, mainFontPalette.slice(1)); - const smallFontPalette = [TRANSPARENT, ...times(9, i => 0xff + i * 256)]; - const stripedSmallFont = createStripedCanvas(tinyFont, smallFontPalette.slice(1)); - const stripedMonoFont = createStripedCanvas(monoFont, smallFontPalette.slice(1)); + const mainFontPalette = [TRANSPARENT, ...times(10, i => 0xff + i * 256)]; + const stripedMainFont = createStripedCanvas(mainFont, mainFontPalette.slice(1)); + const smallFontPalette = [TRANSPARENT, ...times(9, i => 0xff + i * 256)]; + const stripedSmallFont = createStripedCanvas(tinyFont, smallFontPalette.slice(1)); + const stripedMonoFont = createStripedCanvas(monoFont, smallFontPalette.slice(1)); - const fontSprites = createFont(mainFont, 10, 10, - canvas => addImage(result.images, canvas), { noChinese: true }); - const emojiSprites = createEmojis(mainEmoji, 10, 10, - canvas => addImage(result.images, canvas)); - const smallFontSprites = createFont(tinyFont, 8, 9, - canvas => addImage(result.images, canvas), { noChinese: true }); - const monoFontSprites = createFont(monoFont, 8, 9, - canvas => addImage(result.images, canvas), { noChinese: true, mono: 4, onlyBase: true }); + const fontSprites = createFont(mainFont, 10, 10, + canvas => addImage(result.images, canvas), { noChinese: true }); + const emojiSprites = createEmojis(mainEmoji, 10, 10, + canvas => addImage(result.images, canvas)); + const smallFontSprites = createFont(tinyFont, 8, 9, + canvas => addImage(result.images, canvas), { noChinese: true }); + const monoFontSprites = createFont(monoFont, 8, 9, + canvas => addImage(result.images, canvas), { noChinese: true, mono: 4, onlyBase: true }); - const fontSpritesPal = createFont(stripedMainFont, 10, 10, - canvas => addSprite(result.sprites, canvas, undefined, mainFontPalette)); - const emojiSpritesPal = createEmojis(mainEmoji, 10, 10, - canvas => addSprite(result.sprites, canvas, undefined, emojiPalette)); - const smallFontSpritesPal = createFont(stripedSmallFont, 8, 9, - canvas => addSprite(result.sprites, canvas, undefined, smallFontPalette), { noChinese: true }); - const monoFontSpritesPal = createFont(stripedMonoFont, 8, 9, - canvas => addSprite(result.sprites, canvas, undefined, smallFontPalette), { noChinese: true, mono: 4, onlyBase: true }); + const fontSpritesPal = createFont(stripedMainFont, 10, 10, + canvas => addSprite(result.sprites, canvas, undefined, mainFontPalette)); + const emojiSpritesPal = createEmojis(mainEmoji, 10, 10, + canvas => addSprite(result.sprites, canvas, undefined, emojiPalette)); + const smallFontSpritesPal = createFont(stripedSmallFont, 8, 9, + canvas => addSprite(result.sprites, canvas, undefined, smallFontPalette), { noChinese: true }); + const monoFontSpritesPal = createFont(stripedMonoFont, 8, 9, + canvas => addSprite(result.sprites, canvas, undefined, smallFontPalette), { noChinese: true, mono: 4, onlyBase: true }); - const lights = createLights(result, path.join(sourcePath, 'lights')); + const lights = createLights(result, path.join(sourcePath, 'lights')); - const ponySheet = createSpriteSheet('ponySheet', result.images.map(imageToSprite), log, 1024); - const ponySheet2 = createSpriteSheet('ponySheet2', result.sprites, log, 1024, 'black', true); + const ponySheet = createSpriteSheet('ponySheet', result.images.map(imageToSprite), log, 1024); + const ponySheet2 = createSpriteSheet('ponySheet2', result.sprites, log, 1024, 'black', true); - fixPixelRect(ponySheet.sprites, result.objects, 'pixelRect', 'pixel'); - fixPixelRect(ponySheet2.sprites, result.objects2, 'pixelRect2', 'pixel2'); + fixPixelRect(ponySheet.sprites, result.objects, 'pixelRect', 'pixel'); + fixPixelRect(ponySheet2.sprites, result.objects2, 'pixelRect2', 'pixel2'); - lights.map(i => ponySheet.sprites[i]).forEach(s => { - if (s) { - s.x += lightsPad; - s.y += lightsPad; - s.w -= lightsPad * 2; - s.h -= lightsPad * 2; - } - }); + lights.map(i => ponySheet.sprites[i]).forEach(s => { + if (s) { + s.x += lightsPad; + s.y += lightsPad; + s.w -= lightsPad * 2; + s.h -= lightsPad * 2; + } + }); - saveSpriteSheetAsBinary(path.join(generatedPath, 'pony.bin'), ponySheet.image); + saveSpriteSheetAsBinary(path.join(generatedPath, 'pony.bin'), ponySheet.image); - const spritesConfig: SpriteTSConfig = { - spriteFileName: saveSpriteSheet(path.join(destPath, 'pony.png'), ponySheet.image), - paletteFileName: saveSpriteSheet(path.join(destPath, 'pony2.png'), ponySheet2.image), - paletteAlphaFileName: saveSpriteSheet(path.join(destPath, 'pony2a.png'), ponySheet2.alpha), - sprites: ponySheet.sprites, - paletteSprites: ponySheet2.sprites, - result, - palettes, - fonts: { - font: fontSprites, - emoji: emojiSprites, - fontSmall: smallFontSprites, - fontMono: monoFontSprites, - }, - fontsPal: { - fontPal: fontSpritesPal, - emojiPal: emojiSpritesPal, - fontSmallPal: smallFontSpritesPal, - fontMonoPal: monoFontSpritesPal, - }, - namedPalettes: { - defaultPalette: 0, - emojiPalette: addPalette(emojiPalette), - // main - fontPalette: addPalette([TRANSPARENT, ...times(10, () => WHITE)]), - fontSupporter1Palette: addPalette([TRANSPARENT, ...times(10, () => SUPPORTER1)]), - fontSupporter2Palette: addPalette([TRANSPARENT, ...bandedTextPalette(SUPPORTER2_BANDS)]), - fontSupporter3Palette: addPalette([TRANSPARENT, ...bandedTextPalette(SUPPORTER3_BANDS)]), - // small - fontSmallPalette: addPalette([TRANSPARENT, ...times(9, () => WHITE)]), - fontSmallSupporter1Palette: addPalette([TRANSPARENT, ...times(9, () => SUPPORTER1)]), - fontSmallSupporter2Palette: addPalette([TRANSPARENT, ...bandedTinyPalette(SUPPORTER2_BANDS)]), - fontSmallSupporter3Palette: addPalette([TRANSPARENT, ...bandedTinyPalette(SUPPORTER3_BANDS)]), - }, - }; + const spritesConfig: SpriteTSConfig = { + spriteFileName: saveSpriteSheet(path.join(destPath, 'pony.png'), ponySheet.image), + paletteFileName: saveSpriteSheet(path.join(destPath, 'pony2.png'), ponySheet2.image), + paletteAlphaFileName: saveSpriteSheet(path.join(destPath, 'pony2a.png'), ponySheet2.alpha), + sprites: ponySheet.sprites, + paletteSprites: ponySheet2.sprites, + result, + palettes, + fonts: { + font: fontSprites, + emoji: emojiSprites, + fontSmall: smallFontSprites, + fontMono: monoFontSprites, + }, + fontsPal: { + fontPal: fontSpritesPal, + emojiPal: emojiSpritesPal, + fontSmallPal: smallFontSpritesPal, + fontMonoPal: monoFontSpritesPal, + }, + namedPalettes: { + defaultPalette: 0, + emojiPalette: addPalette(emojiPalette), + // main + fontPalette: addPalette([TRANSPARENT, ...times(10, () => WHITE)]), + fontSupporter1Palette: addPalette([TRANSPARENT, ...times(10, () => SUPPORTER1)]), + fontSupporter2Palette: addPalette([TRANSPARENT, ...bandedTextPalette(SUPPORTER2_BANDS)]), + fontSupporter3Palette: addPalette([TRANSPARENT, ...bandedTextPalette(SUPPORTER3_BANDS)]), + // small + fontSmallPalette: addPalette([TRANSPARENT, ...times(9, () => WHITE)]), + fontSmallSupporter1Palette: addPalette([TRANSPARENT, ...times(9, () => SUPPORTER1)]), + fontSmallSupporter2Palette: addPalette([TRANSPARENT, ...bandedTinyPalette(SUPPORTER2_BANDS)]), + fontSmallSupporter3Palette: addPalette([TRANSPARENT, ...bandedTinyPalette(SUPPORTER3_BANDS)]), + }, + }; - createSpritesTS(path.join(generatedPath, 'sprites.ts'), spritesConfig); + createSpritesTS(path.join(generatedPath, 'sprites.ts'), spritesConfig); - // Other exports - saveCanvasAsRaw(path.join(outputPath, 'pony2.raw'), ponySheet2.image); + // Other exports + saveCanvasAsRaw(path.join(outputPath, 'pony2.raw'), ponySheet2.image); } if (require.main === module) { - const start = Date.now(); - createSprites(true); - const time = ((Date.now() - start) / 1000).toFixed(2); - console.log(`[sprites] done: ${time}s, stats: { w: ${0}-${maxW} h: ${0}-${maxH} ox: ${minOX}-${maxOX} oy: ${minOY}-${maxOY} }`); + const start = Date.now(); + createSprites(true); + const time = ((Date.now() - start) / 1000).toFixed(2); + console.log(`[sprites] done: ${time}s, stats: { w: ${0}-${maxW} h: ${0}-${maxH} ox: ${minOX}-${maxOX} oy: ${minOY}-${maxOY} }`); } diff --git a/src/ts/tools/name-tester.ts b/src/ts/tools/name-tester.ts index fdc5cba..a280d0a 100644 --- a/src/ts/tools/name-tester.ts +++ b/src/ts/tools/name-tester.ts @@ -11,38 +11,38 @@ const rootPath = path.join(__dirname, '..', '..', '..', 'tools'); const items: Item = JSON.parse(fs.readFileSync(path.join(rootPath, 'names.json'), 'utf8')); function isNonPrintableCharacter(code: number): boolean { - return code >= 0xfe00 && code <= 0xfe0f; + return code >= 0xfe00 && code <= 0xfe0f; } function ignore(code: number): boolean { - return (code >= 0x0180 && code <= 0x024F) // Latin Extended-B - || (code >= 0x0600 && code <= 0x06FF) // Arabic - || (code >= 0x2719 && code <= 0x2721) // crosses - || (code >= 0x0300 && code <= 0x036F) // Combining Diacritical Marks - || (code >= 0x2200 && code <= 0x22FF) // Mathematical Operators - || (code >= 0x0E00 && code <= 0x0E7F) // Thai - || (code >= 0x0250 && code <= 0x02AF) // IPA Extensions - || (code >= 0x2460 && code <= 0x24FF) // Enclosed Alphanumerics - || (code >= 0x2300 && code <= 0x23FF) // Miscellaneous Technical - ; + return (code >= 0x0180 && code <= 0x024F) // Latin Extended-B + || (code >= 0x0600 && code <= 0x06FF) // Arabic + || (code >= 0x2719 && code <= 0x2721) // crosses + || (code >= 0x0300 && code <= 0x036F) // Combining Diacritical Marks + || (code >= 0x2200 && code <= 0x22FF) // Mathematical Operators + || (code >= 0x0E00 && code <= 0x0E7F) // Thai + || (code >= 0x0250 && code <= 0x02AF) // IPA Extensions + || (code >= 0x2460 && code <= 0x24FF) // Enclosed Alphanumerics + || (code >= 0x2300 && code <= 0x23FF) // Miscellaneous Technical + ; } charsToCodes(CHARS + ROMAJI + EMOJI).forEach(code => existing.add(code)); items.forEach(({ name }) => { - const codes = charsToCodes(name); - const missingChars = codes.reduce((count, code) => { - if (existing.has(code) || isNonPrintableCharacter(code)) - return count; + const codes = charsToCodes(name); + const missingChars = codes.reduce((count, code) => { + if (existing.has(code) || isNonPrintableCharacter(code)) + return count; - const current = missing.get(code) || 0; - missing.set(code, current + 1); - return count + 1; - }, 0); + const current = missing.get(code) || 0; + missing.set(code, current + 1); + return count + 1; + }, 0); - if (missingChars) { - missingNames.push(name); - } + if (missingChars) { + missingNames.push(name); + } }); const missingChars: [number, string, number][] = []; @@ -51,4 +51,4 @@ missingChars.sort(([a], [b]) => b - a); fs.writeFileSync(path.join(rootPath, 'output', 'missing-names.txt'), missingNames.join('\n'), 'utf8'); fs.writeFileSync(path.join(rootPath, 'output', 'missing-chars.txt'), missingChars - .map(([a, b, c]) => `${a}: "${b}" (${c}) [U+${c.toString(16)}]${ignore(c) ? ' (ignore)' : ''}`).join('\n'), 'utf8'); + .map(([a, b, c]) => `${a}: "${b}" (${c}) [U+${c.toString(16)}]${ignore(c) ? ' (ignore)' : ''}`).join('\n'), 'utf8'); diff --git a/src/ts/tools/palette-utils.ts b/src/ts/tools/palette-utils.ts index 50acbb2..a33dd6f 100644 --- a/src/ts/tools/palette-utils.ts +++ b/src/ts/tools/palette-utils.ts @@ -2,14 +2,14 @@ import { Rect, ExtCanvas } from './types'; import { cropCanvas, createExtCanvas } from './canvas-utils'; const validPatternColors = [ - 255, // red - 255 << 8, // green - 255 | (255 << 8), // yellow - 255 << 16, // blue - 255 | (255 << 16), // purple - (255 << 8) | (255 << 16), // teal - 255 | (255 << 8) | (255 << 16), // white - 0, // black + 255, // red + 255 << 8, // green + 255 | (255 << 8), // yellow + 255 << 16, // blue + 255 | (255 << 16), // purple + (255 << 8) | (255 << 16), // teal + 255 | (255 << 8) | (255 << 16), // white + 0, // black ]; const getAlpha = (alpha: number) => alpha < 15 ? false : (alpha > 250 ? true : null); @@ -19,110 +19,110 @@ const rgbToColor = (r: number, g: number, b: number) => round255(r) | (round255( const findValidPatternColor = (r: number, g: number, b: number) => validPatternColors.indexOf(rgbToColor(r, g, b)); function getShade(shade: number) { - // outlines - // 135 - dark outline - // 159 - outline + // outlines + // 135 - dark outline + // 159 - outline - // fills - // 174 - dark shades - // 204 - shade - // 217 - dark fill - // 255 - fill + // fills + // 174 - dark shades + // 204 - shade + // 217 - dark fill + // 255 - fill - if (shade > 130 && shade < 140) { // dark outline - return 135; - } else if (shade > 150 && shade < 165) { // outline - return 159; - } else if (shade > 165 && shade < 180) { // dark shade - return 174; - } else if (shade > 190 && shade < 210) { // shade - return 204; - } else if (shade > 210 && shade < 220) { // dark fill - return 217; - } else if (shade > 245) { // fill - return 255; - } else { - return -1; - } + if (shade > 130 && shade < 140) { // dark outline + return 135; + } else if (shade > 150 && shade < 165) { // outline + return 159; + } else if (shade > 165 && shade < 180) { // dark shade + return 174; + } else if (shade > 190 && shade < 210) { // shade + return 204; + } else if (shade > 210 && shade < 220) { // dark fill + return 217; + } else if (shade > 245) { // fill + return 255; + } else { + return -1; + } } function getShadeForShading(shade: number) { - if (shade === 135) { // dark outline - return 204; - } else if (shade === 159) { // outline - return 255; - } else { - return shade; - } + if (shade === 135) { // dark outline + return 204; + } else if (shade === 159) { // outline + return 255; + } else { + return shade; + } } function pixel(data: Uint8ClampedArray, index: number) { - return data.slice(index, index + 4).join(', '); + return data.slice(index, index + 4).join(', '); } export interface ColorsOutput { - colors?: number; - forceWhite?: boolean; + colors?: number; + forceWhite?: boolean; } export function imageToPalette( - rect: Rect, image: ExtCanvas, pattern: ExtCanvas, palette: number[] | undefined, config: ColorsOutput + rect: Rect, image: ExtCanvas, pattern: ExtCanvas, palette: number[] | undefined, config: ColorsOutput ) { - const pat = cropCanvas(pattern, 0, 0, image.width, image.height); - const ctx = pat.getContext('2d')!; - ctx.globalCompositeOperation = 'destination-in'; - ctx.drawImage(image, 0, 0); + const pat = cropCanvas(pattern, 0, 0, image.width, image.height); + const ctx = pat.getContext('2d')!; + ctx.globalCompositeOperation = 'destination-in'; + ctx.drawImage(image, 0, 0); - const imageData = image.getContext('2d')!.getImageData(rect.x, rect.y, rect.w, rect.h); - const pattData = ctx.getImageData(rect.x, rect.y, rect.w, rect.h); - const length = imageData.width * imageData.height * 4; - const data = imageData.data; - const pdata = pattData.data; + const imageData = image.getContext('2d')!.getImageData(rect.x, rect.y, rect.w, rect.h); + const pattData = ctx.getImageData(rect.x, rect.y, rect.w, rect.h); + const length = imageData.width * imageData.height * 4; + const data = imageData.data; + const pdata = pattData.data; - let maxIndex = 0; + let maxIndex = 0; - for (let i = 0; i < length; i += 4) { - const x = ((i / 4) % rect.w) + rect.x; - const y = Math.floor((i / 4) / rect.h) + rect.y; + for (let i = 0; i < length; i += 4) { + const x = ((i / 4) % rect.w) + rect.x; + const y = Math.floor((i / 4) / rect.h) + rect.y; - if (palette) { - const color = ((data[i] << 24) | (data[i + 1] << 16) | (data[i + 2] << 8) | data[i + 3]) >>> 0; - const index = palette.indexOf(color); + if (palette) { + const color = ((data[i] << 24) | (data[i + 1] << 16) | (data[i + 2] << 8) | data[i + 3]) >>> 0; + const index = palette.indexOf(color); - if (index === -1) { - throw new Error(`Invalid color (palette) [${pixel(data, i)}] [0x${color.toString(16)}] (${x} ${y}) (${image.info})`); - } + if (index === -1) { + throw new Error(`Invalid color (palette) [${pixel(data, i)}] [0x${color.toString(16)}] (${x} ${y}) (${image.info})`); + } - data[i] = index; - data[i + 1] = 255; - } else { - const alpha = getAlpha(data[i + 3]); - const shade = getShade(data[i]); - const index = findValidPatternColor(pdata[i], pdata[i + 1], pdata[i + 2]); + data[i] = index; + data[i + 1] = 255; + } else { + const alpha = getAlpha(data[i + 3]); + const shade = getShade(data[i]); + const index = findValidPatternColor(pdata[i], pdata[i + 1], pdata[i + 2]); - if (index === -1) - throw new Error(`Invalid color (pattern) [${pixel(pdata, i)}] (${x} ${y}) (${pat.info})`); - if (alpha == null) - throw new Error(`Invalid color [${pixel(data, i)}] (${x} ${y}) (${image.info})`); + if (index === -1) + throw new Error(`Invalid color (pattern) [${pixel(pdata, i)}] (${x} ${y}) (${pat.info})`); + if (alpha == null) + throw new Error(`Invalid color [${pixel(data, i)}] (${x} ${y}) (${image.info})`); - const actualIndex = (index * 2) + (isOutline(shade) ? 2 : 1); - const shadeForShading = getShadeForShading(shade); + const actualIndex = (index * 2) + (isOutline(shade) ? 2 : 1); + const shadeForShading = getShadeForShading(shade); - data[i] = alpha ? actualIndex : 0; - data[i + 1] = (config.forceWhite && shade !== -1) ? 255 : (alpha ? shadeForShading : 255); + data[i] = alpha ? actualIndex : 0; + data[i + 1] = (config.forceWhite && shade !== -1) ? 255 : (alpha ? shadeForShading : 255); - if (alpha) { - maxIndex = Math.max(maxIndex, index); - } - } + if (alpha) { + maxIndex = Math.max(maxIndex, index); + } + } - data[i + 2] = 0; - data[i + 3] = 255; - } + data[i + 2] = 0; + data[i + 3] = 255; + } - config.colors = palette ? palette.length : ((maxIndex + 1) * 2 + 1); + config.colors = palette ? palette.length : ((maxIndex + 1) * 2 + 1); - const result = createExtCanvas(image.width, image.height, `${image.info} (image to palette)`); - result.getContext('2d')!.putImageData(imageData, rect.x, rect.y); - return result; + const result = createExtCanvas(image.width, image.height, `${image.info} (image to palette)`); + result.getContext('2d')!.putImageData(imageData, rect.x, rect.y); + return result; } diff --git a/src/ts/tools/psd-utils.ts b/src/ts/tools/psd-utils.ts index 286a808..d57704f 100644 --- a/src/ts/tools/psd-utils.ts +++ b/src/ts/tools/psd-utils.ts @@ -8,51 +8,51 @@ import { matcher, parseWithNumber } from './common'; initializeCanvas((width, height) => createExtCanvas(width, height, 'loaded from psd')); export function openPsd(filePath: string) { - try { - const buffer = fs.readFileSync(filePath); - const name = path.basename(filePath, '.psd'); - const dir = path.basename(path.dirname(filePath)); - const psd = readPsd(buffer, { - skipCompositeImageData: true, - skipThumbnail: true, - throwForMissingFeatures: true, - logMissingFeatures: true, - }); - return toPsd(psd, name, dir); - } catch (e) { - console.error(`Failed to load: ${filePath}: ${e.message}`); - throw e; - } + try { + const buffer = fs.readFileSync(filePath); + const name = path.basename(filePath, '.psd'); + const dir = path.basename(path.dirname(filePath)); + const psd = readPsd(buffer, { + skipCompositeImageData: true, + skipThumbnail: true, + throwForMissingFeatures: true, + logMissingFeatures: true, + }); + return toPsd(psd, name, dir); + } catch (e) { + console.error(`Failed to load: ${filePath}: ${e.message}`); + throw e; + } } function toPsd({ width, height, children }: PsdFile, name: string, dir: string): Psd { - const info = `${dir}/${name}`; + const info = `${dir}/${name}`; - return { - dir, name, width, height, info, children: (children || []).map(c => toLayer(c, width, height, info)), - }; + return { + dir, name, width, height, info, children: (children || []).map(c => toLayer(c, width, height, info)), + }; } function toLayer({ name, canvas, left, top, children }: PsdLayer, width: number, height: number, parentInfo: string): Layer { - const info = `${parentInfo}/${name}`; + const info = `${parentInfo}/${name}`; - return { - name: name || '', - info, - canvas: fixCanvas(canvas, width, height, left || 0, top || 0, info), - children: (children || []).map(c => toLayer(c, width, height, info)), - }; + return { + name: name || '', + info, + canvas: fixCanvas(canvas, width, height, left || 0, top || 0, info), + children: (children || []).map(c => toLayer(c, width, height, info)), + }; } function fixCanvas( - canvas: HTMLCanvasElement | undefined, width: number, height: number, left: number, top: number, info: string + canvas: HTMLCanvasElement | undefined, width: number, height: number, left: number, top: number, info: string ) { - if (!canvas) - return undefined; + if (!canvas) + return undefined; - const result = createExtCanvas(width, height, info); - result.getContext('2d')!.drawImage(canvas, left, top); - return result; + const result = createExtCanvas(width, height, info); + result.getContext('2d')!.drawImage(canvas, left, top); + return result; } const isPsd = matcher(/\.psd$/); @@ -60,9 +60,9 @@ const isPsd = matcher(/\.psd$/); export const getPsds = (directory: string) => fs.readdirSync(directory).filter(isPsd); export function openPsdFiles(directory: string, match?: RegExp) { - return getPsds(directory) - .filter(f => match ? match.test(f) : true) - .sort((a, b) => parseWithNumber(a) - parseWithNumber(b)) - .map(f => path.join(directory, f)) - .map(openPsd); + return getPsds(directory) + .filter(f => match ? match.test(f) : true) + .sort((a, b) => parseWithNumber(a) - parseWithNumber(b)) + .map(f => path.join(directory, f)) + .map(openPsd); } diff --git a/src/ts/tools/sprite-sheet.ts b/src/ts/tools/sprite-sheet.ts index f420c48..e539ae7 100644 --- a/src/ts/tools/sprite-sheet.ts +++ b/src/ts/tools/sprite-sheet.ts @@ -7,486 +7,486 @@ import { removeItem } from '../common/utils'; import { createExtCanvas, saveCanvas } from './canvas-utils'; interface ExtSprite extends Sprite { - index?: number; - layers?: number; - duplicateOf?: ExtSprite; - overlays?: ExtSprite[]; - overlayedOn?: ExtSprite; - data?: ImageData; + index?: number; + layers?: number; + duplicateOf?: ExtSprite; + overlays?: ExtSprite[]; + overlayedOn?: ExtSprite; + data?: ImageData; } function isIdenticalSprite(a: ExtSprite | undefined, b: ExtSprite | undefined): boolean { - return !!(a && b && a.data && b.data && isIdenticalData(a.data, b.data)); + return !!(a && b && a.data && b.data && isIdenticalData(a.data, b.data)); } function isIdenticalData(a: ImageData, b: ImageData) { - if (a.width !== b.width || a.height !== b.height) - return false; + if (a.width !== b.width || a.height !== b.height) + return false; - const length = (a.width * a.height * 4) | 0; - const adat = a.data; - const bdat = b.data; + const length = (a.width * a.height * 4) | 0; + const adat = a.data; + const bdat = b.data; - for (let i = 0; i < length; i = (i + 1) | 0) { - if (adat[i] !== bdat[i]) { - return false; - } - } + for (let i = 0; i < length; i = (i + 1) | 0) { + if (adat[i] !== bdat[i]) { + return false; + } + } - return true; + return true; } function isIdenticalChannel(a: Sprite | undefined, b: Sprite | undefined, channel: number) { - if (!a || !b || a.w !== b.w || a.h !== b.h) - return false; + if (!a || !b || a.w !== b.w || a.h !== b.h) + return false; - const adata = a.image.getContext('2d')!.getImageData(a.ox, a.oy, a.w, a.h); - const bdata = b.image.getContext('2d')!.getImageData(b.ox, b.oy, b.w, b.h); - const length = (adata.width * adata.height * 4) | 0; - const adat = adata.data; - const bdat = bdata.data; + const adata = a.image.getContext('2d')!.getImageData(a.ox, a.oy, a.w, a.h); + const bdata = b.image.getContext('2d')!.getImageData(b.ox, b.oy, b.w, b.h); + const length = (adata.width * adata.height * 4) | 0; + const adat = adata.data; + const bdat = bdata.data; - for (let i = channel | 0; i < length; i = (i + 4) | 0) { - if (adat[i] !== bdat[i]) { - return false; - } - } + for (let i = channel | 0; i < length; i = (i + 4) | 0) { + if (adat[i] !== bdat[i]) { + return false; + } + } - return true; + return true; } function trimImageData(data: ImageData): Rect { - const width = data.width | 0; - const imageData = data.data; + const width = data.width | 0; + const imageData = data.data; - let top = 0; - let left = 0; - let right = data.width | 0; - let bottom = data.height | 0; + let top = 0; + let left = 0; + let right = data.width | 0; + let bottom = data.height | 0; - function isEmpty(x: number, y: number) { - return imageData[((getIndex(x, y, width) << 2) + 3) | 0] === 0; - } + function isEmpty(x: number, y: number) { + return imageData[((getIndex(x, y, width) << 2) + 3) | 0] === 0; + } - function isRowEmpty(y: number) { - for (let x = left | 0; x < right; x = (x + 1) | 0) { - if (!isEmpty(x | 0, y | 0)) { - return false; - } - } + function isRowEmpty(y: number) { + for (let x = left | 0; x < right; x = (x + 1) | 0) { + if (!isEmpty(x | 0, y | 0)) { + return false; + } + } - return true; - } + return true; + } - function isColEmpty(x: number) { - for (let y = top | 0; y < bottom; y = (y + 1) | 0) { - if (!isEmpty(x | 0, y | 0)) { - return false; - } - } + function isColEmpty(x: number) { + for (let y = top | 0; y < bottom; y = (y + 1) | 0) { + if (!isEmpty(x | 0, y | 0)) { + return false; + } + } - return true; - } + return true; + } - while (bottom > top && isRowEmpty(bottom - 1)) - bottom--; - while (right > left && isColEmpty(right - 1)) - right--; - while (top < bottom && isRowEmpty(top)) - top++; - while (left < right && isColEmpty(left)) - left++; + while (bottom > top && isRowEmpty(bottom - 1)) + bottom--; + while (right > left && isColEmpty(right - 1)) + right--; + while (top < bottom && isRowEmpty(top)) + top++; + while (left < right && isColEmpty(left)) + left++; - return { y: top, x: left, w: right - left, h: bottom - top }; + return { y: top, x: left, w: right - left, h: bottom - top }; } export function getSpriteRect(canvas: HTMLCanvasElement, x: number, y: number, w: number, h: number): Rect { - const data = canvas.getContext('2d')!.getImageData(x, y, w, h); - const rect = trimImageData(data); - return { x: x + rect.x, y: y + rect.y, w: rect.w, h: rect.h }; + const data = canvas.getContext('2d')!.getImageData(x, y, w, h); + const rect = trimImageData(data); + return { x: x + rect.x, y: y + rect.y, w: rect.w, h: rect.h }; } export function imageToSprite(image: HTMLCanvasElement, index: number): ExtSprite { - const { w, h, x, y } = getSpriteRect(image, 0, 0, image.width, image.height); - return { image, index, w, h, x: 0, y: 0, ox: x, oy: y }; + const { w, h, x, y } = getSpriteRect(image, 0, 0, image.width, image.height); + return { image, index, w, h, x: 0, y: 0, ox: x, oy: y }; } function getIndex(x: number, y: number, outputWidth: number) { - return ((x | 0) + (((y | 0) * outputWidth) | 0)) | 0; + return ((x | 0) + (((y | 0) * outputWidth) | 0)) | 0; } function isEmpty(x: number, y: number, w: number, h: number, outputWidth: number, taken: Uint8Array) { - outputWidth = outputWidth | 0; + outputWidth = outputWidth | 0; - if (((x + w) | 0) > outputWidth || ((y + h) | 0) > outputWidth) { - return false; - } + if (((x + w) | 0) > outputWidth || ((y + h) | 0) > outputWidth) { + return false; + } - for (let iy = 0; iy < h; iy++) { - for (let ix = 0; ix < w; ix++) { - if (taken[getIndex((ix + x) | 0, (iy + y) | 0, outputWidth)] !== 0) { - return false; - } - } - } + for (let iy = 0; iy < h; iy++) { + for (let ix = 0; ix < w; ix++) { + if (taken[getIndex((ix + x) | 0, (iy + y) | 0, outputWidth)] !== 0) { + return false; + } + } + } - return true; + return true; } interface Line { - start: number; - length: number; + start: number; + length: number; } interface Taken { - lines: Line[][]; - data: Uint8Array; // TODO: remove completely, just use lines + lines: Line[][]; + data: Uint8Array; // TODO: remove completely, just use lines } function getFirstFree(outputWidth: number, width: number, height: number, { data, lines }: Taken) { - const maxY = (outputWidth - height) | 0; + const maxY = (outputWidth - height) | 0; - for (let y = 0; y < maxY; y = (y + 1) | 0) { - const spans = lines[y]; + for (let y = 0; y < maxY; y = (y + 1) | 0) { + const spans = lines[y]; - for (let i = 0; i < spans.length; i++) { - const span = spans[i]; - const start = span.start | 0; - const end = (start + span.length - width) | 0; + for (let i = 0; i < spans.length; i++) { + const span = spans[i]; + const start = span.start | 0; + const end = (start + span.length - width) | 0; - for (let x = start; x < end; x = (x + 1) | 0) { - if (isEmpty(x, y, width, height, outputWidth, data)) { - return { x, y, layer: 0 }; - } - } - } - } + for (let x = start; x < end; x = (x + 1) | 0) { + if (isEmpty(x, y, width, height, outputWidth, data)) { + return { x, y, layer: 0 }; + } + } + } + } - throw new Error(`Cannot find free space for (${width}, ${height}) [getFirstFree]`); + throw new Error(`Cannot find free space for (${width}, ${height}) [getFirstFree]`); } function getFirstFreePacked(outputWidth: number, width: number, height: number, takens: Taken[]) { - const maxY = outputWidth - height; + const maxY = outputWidth - height; - for (let layer = 0; layer < takens.length; layer = (layer + 1) | 0) { - const { data, lines } = takens[layer]; + for (let layer = 0; layer < takens.length; layer = (layer + 1) | 0) { + const { data, lines } = takens[layer]; - for (let y = 0; y < maxY; y = (y + 1) | 0) { - const spans = lines[y]; + for (let y = 0; y < maxY; y = (y + 1) | 0) { + const spans = lines[y]; - for (let i = 0; i < spans.length; i++) { - const span = spans[i]; - const start = span.start | 0; - const end = (start + span.length - width) | 0; + for (let i = 0; i < spans.length; i++) { + const span = spans[i]; + const start = span.start | 0; + const end = (start + span.length - width) | 0; - for (let x = start | 0; x < end; x = (x + 1) | 0) { - if (isEmpty(x, y, width, height, outputWidth, data)) { - return { x, y, layer }; - } - } - } - } - } + for (let x = start | 0; x < end; x = (x + 1) | 0) { + if (isEmpty(x, y, width, height, outputWidth, data)) { + return { x, y, layer }; + } + } + } + } + } - throw new Error(`Cannot find free space for (${width}, ${height}) [getFirstFreePacked]`); + throw new Error(`Cannot find free space for (${width}, ${height}) [getFirstFreePacked]`); } function positionSprite(sprite: ExtSprite, outputWidth: number, taken: Taken[], pack: boolean) { - const layers = sprite.layers || 1; - const { x, y, layer } = (pack && layers === 1) ? - getFirstFreePacked(outputWidth, sprite.w, sprite.h, taken) : - getFirstFree(outputWidth, sprite.w, sprite.h, taken[0]); + const layers = sprite.layers || 1; + const { x, y, layer } = (pack && layers === 1) ? + getFirstFreePacked(outputWidth, sprite.w, sprite.h, taken) : + getFirstFree(outputWidth, sprite.w, sprite.h, taken[0]); - sprite.x = x; - sprite.y = y; - sprite.layer = layer; + sprite.x = x; + sprite.y = y; + sprite.layer = layer; - const w = sprite.w; - const right = x + w; + const w = sprite.w; + const right = x + w; - for (let il = 0; il < layers; il++) { - const { data, lines } = taken[il + layer]; + for (let il = 0; il < layers; il++) { + const { data, lines } = taken[il + layer]; - for (let iy = 0; iy < sprite.h; iy++) { - const yy = y + iy; - const spans = lines[yy]; + for (let iy = 0; iy < sprite.h; iy++) { + const yy = y + iy; + const spans = lines[yy]; - for (let ix = 0; ix < sprite.w; ix++) { - const xx = x + ix; - data[getIndex(xx, yy, outputWidth)] = 1; - } + for (let ix = 0; ix < sprite.w; ix++) { + const xx = x + ix; + data[getIndex(xx, yy, outputWidth)] = 1; + } - for (let i = 0; i < spans.length; i++) { - const span = spans[i]; - const start = span.start; - const length = span.length; - const end = start + length; + for (let i = 0; i < spans.length; i++) { + const span = spans[i]; + const start = span.start; + const length = span.length; + const end = start + length; - if (start >= right) // right of span - break; + if (start >= right) // right of span + break; - if (end <= right) // left of span - continue; + if (end <= right) // left of span + continue; - if (start === x) { - if (length === w) { // entire span - spans.splice(i, 1); - } else { // at the start of span - span.start += w; - span.length -= w; - } - } else { - if (end === right) { // at the end of span - span.length -= w; - } else { // in the middle of span - span.length = x - start; - spans.splice(i + 1, 0, { start: right, length: end - right }); - } - } + if (start === x) { + if (length === w) { // entire span + spans.splice(i, 1); + } else { // at the start of span + span.start += w; + span.length -= w; + } + } else { + if (end === right) { // at the end of span + span.length -= w; + } else { // in the middle of span + span.length = x - start; + spans.splice(i + 1, 0, { start: right, length: end - right }); + } + } - // let start = span.start; - // let lineMoved = false; + // let start = span.start; + // let lineMoved = false; - // for (let ix = 0; ix < sprite.w; ix++) { - // const xx = x + ix; - // data[getIndex(xx, yy, outputWidth)] = 1; + // for (let ix = 0; ix < sprite.w; ix++) { + // const xx = x + ix; + // data[getIndex(xx, yy, outputWidth)] = 1; - // if (xx === start) { - // start++; - // lineMoved = true; - // } - // } + // if (xx === start) { + // start++; + // lineMoved = true; + // } + // } - // if (lineMoved) { - // for (let x = start; x < outputWidth && data[getIndex(x, yy, outputWidth)] !== 0; x++) { - // start++; - // } + // if (lineMoved) { + // for (let x = start; x < outputWidth && data[getIndex(x, yy, outputWidth)] !== 0; x++) { + // start++; + // } - // span.start = start; - // span.length = outputWidth - start; - // } - } - } - } + // span.start = start; + // span.length = outputWidth - start; + // } + } + } + } } function hasShading(s: Sprite) { - const data = s.image.getContext('2d')!.getImageData(s.ox, s.oy, s.w, s.h); + const data = s.image.getContext('2d')!.getImageData(s.ox, s.oy, s.w, s.h); - for (let y = 0; y < data.height; y++) { - for (let x = 0; x < data.width; x++) { - if (data.data[(x + y * data.width) * 4 + 1] !== 0xff) { - return true; - } - } - } + for (let y = 0; y < data.height; y++) { + for (let x = 0; x < data.width; x++) { + if (data.data[(x + y * data.width) * 4 + 1] !== 0xff) { + return true; + } + } + } - return false; + return false; } function getSpriteImageData(s: ExtSprite): ImageData | undefined { - if (!s.w || !s.h) - return undefined; + if (!s.w || !s.h) + return undefined; - const context = s.image.getContext('2d')!; - const { width, height, data } = context.getImageData(s.ox, s.oy, s.w, s.h); - return { width, height, data }; + const context = s.image.getContext('2d')!; + const { width, height, data } = context.getImageData(s.ox, s.oy, s.w, s.h); + return { width, height, data }; } export function createSpriteSheet(name: string, images: ExtSprite[], log: boolean, size: number, bg?: string, pack = false) { - const maxLayers = 4; - const sprites = images.slice(); + const maxLayers = 4; + const sprites = images.slice(); - let outputWidth = size; - let maxY = 0; - let areaTaken = 0; - let deduplicated = 0; - let layered = 0; - let tooBig = sprites.find(s => s.w >= outputWidth); + let outputWidth = size; + let maxY = 0; + let areaTaken = 0; + let deduplicated = 0; + let layered = 0; + let tooBig = sprites.find(s => s.w >= outputWidth); - while (tooBig) { - throw new Error(`Sprite too large (${tooBig.w}x${tooBig.h}) in ${name} (${size}x${size})`); - } + while (tooBig) { + throw new Error(`Sprite too large (${tooBig.w}x${tooBig.h}) in ${name} (${size}x${size})`); + } - sprites - .forEach(s => s.data = getSpriteImageData(s)); + sprites + .forEach(s => s.data = getSpriteImageData(s)); - sprites - .forEach((s, i) => { - if (s.w && s.h) { - for (let j = 0; j < i; j++) { - if (!s.duplicateOf && isIdenticalSprite(sprites[j], s)) { - s.duplicateOf = sprites[j]; - deduplicated++; - //console.log('duplicate', sprite.name, '==', sprites[j].name); - break; - } - } - } - }); + sprites + .forEach((s, i) => { + if (s.w && s.h) { + for (let j = 0; j < i; j++) { + if (!s.duplicateOf && isIdenticalSprite(sprites[j], s)) { + s.duplicateOf = sprites[j]; + deduplicated++; + //console.log('duplicate', sprite.name, '==', sprites[j].name); + break; + } + } + } + }); - sprites - .filter(s => !s.duplicateOf && s.w && s.h) - .forEach(s => { - s.shade = !pack || hasShading(s); - s.layers = s.shade ? 2 : 1; - }); + sprites + .filter(s => !s.duplicateOf && s.w && s.h) + .forEach(s => { + s.shade = !pack || hasShading(s); + s.layers = s.shade ? 2 : 1; + }); - if (pack) { - sprites - .filter(s => !s.duplicateOf && s.shade) - .reduce((pool, sprite) => { - const match = pool.find(d => isIdenticalChannel(sprite, d, 1)); + if (pack) { + sprites + .filter(s => !s.duplicateOf && s.shade) + .reduce((pool, sprite) => { + const match = pool.find(d => isIdenticalChannel(sprite, d, 1)); - if (match) { - match.overlays = match.overlays || []; - match.overlays.push(sprite); - sprite.layer = match.layers!; - sprite.overlayedOn = match; - match.layers = match.layers! + 1; + if (match) { + match.overlays = match.overlays || []; + match.overlays.push(sprite); + sprite.layer = match.layers!; + sprite.overlayedOn = match; + match.layers = match.layers! + 1; - if (match.layers >= maxLayers) { - removeItem(pool, match); - } + if (match.layers >= maxLayers) { + removeItem(pool, match); + } - layered++; - } else { - pool.push(sprite); - } + layered++; + } else { + pool.push(sprite); + } - return pool; - }, [] as ExtSprite[]); - } + return pool; + }, [] as ExtSprite[]); + } - sprites.sort((a, b) => ((b.layers || 1) - (a.layers || 1)) || ((b.h * 1024 + b.w) - (a.h * 1024 + a.w))); + sprites.sort((a, b) => ((b.layers || 1) - (a.layers || 1)) || ((b.h * 1024 + b.w) - (a.h * 1024 + a.w))); - maxY = 0; - areaTaken = 0; + maxY = 0; + areaTaken = 0; - const taken: Taken[] = times(maxLayers, () => ({ - lines: times(outputWidth, () => [{ start: 0, length: outputWidth }]), - data: new Uint8Array(outputWidth * outputWidth), - })); + const taken: Taken[] = times(maxLayers, () => ({ + lines: times(outputWidth, () => [{ start: 0, length: outputWidth }]), + data: new Uint8Array(outputWidth * outputWidth), + })); - sprites - .filter(s => !s.duplicateOf && !s.overlayedOn) - .forEach(s => { - try { - positionSprite(s, outputWidth, taken, pack); - maxY = Math.max(maxY, s.y + s.h); - areaTaken += s.w * s.h; - } catch (e) { - console.error(e); - } - }); + sprites + .filter(s => !s.duplicateOf && !s.overlayedOn) + .forEach(s => { + try { + positionSprite(s, outputWidth, taken, pack); + maxY = Math.max(maxY, s.y + s.h); + areaTaken += s.w * s.h; + } catch (e) { + console.error(e); + } + }); - if (maxY > outputWidth) { - throw new Error(`Exceeded sprite sheet size for ${name} (${size}x${size})`); - } + if (maxY > outputWidth) { + throw new Error(`Exceeded sprite sheet size for ${name} (${size}x${size})`); + } - if (log) { - const efficiency = (areaTaken * 100 / outputWidth / maxY).toFixed(); + if (log) { + const efficiency = (areaTaken * 100 / outputWidth / maxY).toFixed(); - console.log( - `[sprites] [${name}] Packed ${sprites.length} sprites into ${outputWidth} x ${maxY} sheet, ` - + `${efficiency}% efficiency, ${deduplicated} deduplicated, ${layered} layered`); - } + console.log( + `[sprites] [${name}] Packed ${sprites.length} sprites into ${outputWidth} x ${maxY} sheet, ` + + `${efficiency}% efficiency, ${deduplicated} deduplicated, ${layered} layered`); + } - sprites - .filter(s => s.overlayedOn) - .forEach(s => { - s.x = s.overlayedOn!.x; - s.y = s.overlayedOn!.y; - }); + sprites + .filter(s => s.overlayedOn) + .forEach(s => { + s.x = s.overlayedOn!.x; + s.y = s.overlayedOn!.y; + }); - sprites - .filter(s => s.duplicateOf) - .forEach(s => { - s.x = s.duplicateOf!.x; - s.y = s.duplicateOf!.y; - s.layer = s.duplicateOf!.layer; - s.shade = s.duplicateOf!.shade; - }); + sprites + .filter(s => s.duplicateOf) + .forEach(s => { + s.x = s.duplicateOf!.x; + s.y = s.duplicateOf!.y; + s.layer = s.duplicateOf!.layer; + s.shade = s.duplicateOf!.shade; + }); - const image = createExtCanvas(outputWidth, outputWidth, 'sprite sheet image'); - const alpha = createExtCanvas(outputWidth, outputWidth, 'sprite sheet alpha'); - const context = image.getContext('2d')!; - const alphaContext = alpha.getContext('2d')!; + const image = createExtCanvas(outputWidth, outputWidth, 'sprite sheet image'); + const alpha = createExtCanvas(outputWidth, outputWidth, 'sprite sheet alpha'); + const context = image.getContext('2d')!; + const alphaContext = alpha.getContext('2d')!; - if (bg) { - context.fillStyle = bg; - context.fillRect(0, 0, outputWidth, outputWidth); - alphaContext.fillStyle = bg; - alphaContext.fillRect(0, 0, outputWidth, outputWidth); - } + if (bg) { + context.fillStyle = bg; + context.fillRect(0, 0, outputWidth, outputWidth); + alphaContext.fillStyle = bg; + alphaContext.fillRect(0, 0, outputWidth, outputWidth); + } - if (pack) { - const data = context.getImageData(0, 0, image.width, image.height); - const alphaData = alphaContext.getImageData(0, 0, image.width, image.height); + if (pack) { + const data = context.getImageData(0, 0, image.width, image.height); + const alphaData = alphaContext.getImageData(0, 0, image.width, image.height); - sprites - .filter(s => !s.duplicateOf && s.w && s.h) - .forEach(s => { - if (s.layer === 3) { - drawChannel(s.image, alphaData, 0, 0, s.ox, s.oy, s.x, s.y, s.w, s.h); - } else { - drawChannel(s.image, data, 0, s.layer || 0, s.ox, s.oy, s.x, s.y, s.w, s.h); - } + sprites + .filter(s => !s.duplicateOf && s.w && s.h) + .forEach(s => { + if (s.layer === 3) { + drawChannel(s.image, alphaData, 0, 0, s.ox, s.oy, s.x, s.y, s.w, s.h); + } else { + drawChannel(s.image, data, 0, s.layer || 0, s.ox, s.oy, s.x, s.y, s.w, s.h); + } - if (s.shade) { - drawChannel(s.image, data, 1, 1, s.ox, s.oy, s.x, s.y, s.w, s.h); - } - }); + if (s.shade) { + drawChannel(s.image, data, 1, 1, s.ox, s.oy, s.x, s.y, s.w, s.h); + } + }); - context.putImageData(data, 0, 0); - alphaContext.putImageData(alphaData, 0, 0); - } else { - sprites - .filter(s => !s.duplicateOf && s.w && s.h) - .forEach(s => context.drawImage(s.image, s.ox, s.oy, s.w, s.h, s.x, s.y, s.w, s.h)); - } + context.putImageData(data, 0, 0); + alphaContext.putImageData(alphaData, 0, 0); + } else { + sprites + .filter(s => !s.duplicateOf && s.w && s.h) + .forEach(s => context.drawImage(s.image, s.ox, s.oy, s.w, s.h, s.x, s.y, s.w, s.h)); + } - return { - sprites: images.map((_, index) => findByIndex(sprites, index) || null), - image, - alpha, - }; + return { + sprites: images.map((_, index) => findByIndex(sprites, index) || null), + image, + alpha, + }; } function drawChannel( - src: HTMLCanvasElement, dst: ImageData, srcChannel: number, dstChannel: number, - sx: number, sy: number, dx: number, dy: number, w: number, h: number + src: HTMLCanvasElement, dst: ImageData, srcChannel: number, dstChannel: number, + sx: number, sy: number, dx: number, dy: number, w: number, h: number ) { - const srcData = src.getContext('2d')!.getImageData(sx, sy, w, h); + const srcData = src.getContext('2d')!.getImageData(sx, sy, w, h); - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - dst.data[((x + dx) + (y + dy) * dst.width) * 4 + dstChannel] = - srcData.data[(x + y * srcData.width) * 4 + srcChannel]; - } - } + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + dst.data[((x + dx) + (y + dy) * dst.width) * 4 + dstChannel] = + srcData.data[(x + y * srcData.width) * 4 + srcChannel]; + } + } } export function saveSpriteSheet(filePath: string, canvas: HTMLCanvasElement) { - saveCanvas(filePath, canvas); - return path.basename(filePath); + saveCanvas(filePath, canvas); + return path.basename(filePath); } export function saveSpriteSheetAsBinary(filePath: string, canvas: HTMLCanvasElement) { - const context = canvas.getContext('2d')!; - const data = context.getImageData(0, 0, canvas.width, canvas.height); - fs.writeFileSync(filePath, Buffer.from(data.data.buffer)); + const context = canvas.getContext('2d')!; + const data = context.getImageData(0, 0, canvas.width, canvas.height); + fs.writeFileSync(filePath, Buffer.from(data.data.buffer)); } export function saveCanvasAsRaw(filePath: string, canvas: HTMLCanvasElement) { - const buffer = Buffer.alloc(4 + 4 + 4 + 4 * canvas.width * canvas.height); - buffer.writeUInt8('R'.charCodeAt(0), 0); - buffer.writeUInt8('A'.charCodeAt(0), 1); - buffer.writeUInt8('W'.charCodeAt(0), 2); - buffer.writeUInt8(' '.charCodeAt(0), 3); - buffer.writeUInt32LE(canvas.width, 4); - buffer.writeUInt32LE(canvas.height, 8); - const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); - buffer.set(data.data, 12); - fs.writeFileSync(filePath, buffer); + const buffer = Buffer.alloc(4 + 4 + 4 + 4 * canvas.width * canvas.height); + buffer.writeUInt8('R'.charCodeAt(0), 0); + buffer.writeUInt8('A'.charCodeAt(0), 1); + buffer.writeUInt8('W'.charCodeAt(0), 2); + buffer.writeUInt8(' '.charCodeAt(0), 3); + buffer.writeUInt32LE(canvas.width, 4); + buffer.writeUInt32LE(canvas.height, 8); + const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height); + buffer.set(data.data, 12); + fs.writeFileSync(filePath, buffer); } diff --git a/src/ts/tools/sprites-template.ts b/src/ts/tools/sprites-template.ts index d6721fb..98f81c2 100644 --- a/src/ts/tools/sprites-template.ts +++ b/src/ts/tools/sprites-template.ts @@ -2,7 +2,7 @@ /* tslint:disable */ import { - Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, Shadow, ColorShadow, TileSprites, SpriteSheet + Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, Shadow, ColorShadow, TileSprites, SpriteSheet } from '../common/interfaces'; import { parseSpriteColor } from '../common/utils'; import { bitReaderCustom } from '../common/bitUtils'; @@ -21,7 +21,7 @@ const sprites2 = createSprites('/*SPRITES_PALETTE*/'); /*FONTS*/ const palettes: Uint32Array[] = createPalettes('/*COLORS*/', [ - /*PALETTES*/ + /*PALETTES*/ ]); /*NAMED_SPRITES*/ @@ -29,106 +29,106 @@ const palettes: Uint32Array[] = createPalettes('/*COLORS*/', [ /*NAMED_PALETTES*/ export const spriteSheets: SpriteSheet[] = [ - { - src: '/*SPRITE_SHEET*/', - data: undefined, texture: undefined, sprites: sprites, palette: false - }, - { - src: '/*SPRITE_SHEET_PALETTE*/', srcA: '/*SPRITE_SHEET_PALETTE_ALPHA*/', - data: undefined, texture: undefined, sprites: sprites2, palette: true - }, + { + src: '/*SPRITE_SHEET*/', + data: undefined, texture: undefined, sprites: sprites, palette: false + }, + { + src: '/*SPRITE_SHEET_PALETTE*/', srcA: '/*SPRITE_SHEET_PALETTE_ALPHA*/', + data: undefined, texture: undefined, sprites: sprites2, palette: true + }, ]; export const normalSpriteSheet = spriteSheets[0]; export const paletteSpriteSheet = spriteSheets[1]; export function createSprites(data: string): Sprite[] { - const sprites: Sprite[] = [ - { x: 0, y: 0, w: 0, h: 0, ox: 0, oy: 0, type: 0 }, - ]; + const sprites: Sprite[] = [ + { x: 0, y: 0, w: 0, h: 0, ox: 0, oy: 0, type: 0 }, + ]; - let offset = 0; - const read = bitReaderCustom(() => { - const value = parseInt(data.substr(offset, 2), 16); - offset += 2; - return value; - }); + let offset = 0; + const read = bitReaderCustom(() => { + const value = parseInt(data.substr(offset, 2), 16); + offset += 2; + return value; + }); - while (offset < data.length) { - sprites.push({ - x: read(12), - y: read(12), - w: read(9), - h: read(9), - ox: read(8), - oy: read(8), - type: read(6), - }); - } + while (offset < data.length) { + sprites.push({ + x: read(12), + y: read(12), + w: read(9), + h: read(9), + ox: read(8), + oy: read(8), + type: read(6), + }); + } - return sprites; + return sprites; } export function createFont(sprites: Sprite[], groups: [number, number[]][]) { - const chars: { code: number; sprite: Sprite; }[] = []; + const chars: { code: number; sprite: Sprite; }[] = []; - for (const [start, codes] of groups) { - for (let i = 0; i < codes.length; i++) { - if (codes[i]) { - chars.push({ code: start + i, sprite: sprites[codes[i]] }); - } - } - } + for (const [start, codes] of groups) { + for (let i = 0; i < codes.length; i++) { + if (codes[i]) { + chars.push({ code: start + i, sprite: sprites[codes[i]] }); + } + } + } - return chars; + return chars; } export function createButton( - border: number, topLeft: number, top: number, topRight: number, left: number, bg: number, right: number, - bottomLeft: number, bottom: number, bottomRight: number + border: number, topLeft: number, top: number, topRight: number, left: number, bg: number, right: number, + bottomLeft: number, bottom: number, bottomRight: number ): SpriteBorder { - return { - border, - topLeft: sprites[topLeft], - top: sprites[top], - topRight: sprites[topRight], - left: sprites[left], - bg: sprites[bg], - right: sprites[right], - bottomLeft: sprites[bottomLeft], - bottom: sprites[bottom], - bottomRight: sprites[bottomRight] - }; + return { + border, + topLeft: sprites[topLeft], + top: sprites[top], + topRight: sprites[topRight], + left: sprites[left], + bg: sprites[bg], + right: sprites[right], + bottomLeft: sprites[bottomLeft], + bottom: sprites[bottom], + bottomRight: sprites[bottomRight] + }; } export function mapSprites(frames: number[]) { - return frames.map(i => sprites[i]); + return frames.map(i => sprites[i]); } export function mapSprites2(frames: number[]) { - return frames.map(i => sprites2[i]); + return frames.map(i => sprites2[i]); } export function createPalettes(colorsString: string, palettes: number[][]): Uint32Array[] { - const colors = colorsString.split(/ /g).map(parseSpriteColor); + const colors = colorsString.split(/ /g).map(parseSpriteColor); - return palettes.map(palette => { - const result = new Uint32Array(palette.length); + return palettes.map(palette => { + const result = new Uint32Array(palette.length); - for (let i = 0; i < palette.length; i++) { - result[i] = colors[palette[i]] >>> 0; - } + for (let i = 0; i < palette.length; i++) { + result[i] = colors[palette[i]] >>> 0; + } - return result; - }); + return result; + }); } export function createColorPal(color: number, colors: number): ColorExtra { - return { color: sprites2[color], colors }; + return { color: sprites2[color], colors }; } export function colorPal(colors: number) { - return (color: number) => createColorPal(color, colors); + return (color: number) => createColorPal(color, colors); } const colorPal3 = colorPal(3); @@ -140,56 +140,56 @@ const colorPal13 = colorPal(13); const colorPal17 = colorPal(17); export function getPalette(index: number) { - return palettes[index]; + return palettes[index]; } const emptyPalette = new Uint32Array(0); export function emptyColorPalette(): ColorExtra { - return { color: sprites2[0], palettes: [emptyPalette] }; + return { color: sprites2[0], palettes: [emptyPalette] }; } export function createSpritesPalette(sprites: number[], paletteIndexes: number[]): TileSprites { - return { sprites: sprites.map(i => sprites2[i]), palettes: paletteIndexes.map(getPalette) }; + return { sprites: sprites.map(i => sprites2[i]), palettes: paletteIndexes.map(getPalette) }; } export function createColorPalette(color: number, paletteIndexes: number[]): ColorExtra { - return { color: sprites2[color], palettes: paletteIndexes.map(getPalette) }; + return { color: sprites2[color], palettes: paletteIndexes.map(getPalette) }; } export function createColorExtraPal(color: number, colors: number, extra: number, paletteIndexes: number[]): ColorExtra { - return { color: sprites2[color], colors, extra: sprites2[extra], palettes: paletteIndexes.map(getPalette) }; + return { color: sprites2[color], colors, extra: sprites2[extra], palettes: paletteIndexes.map(getPalette) }; } export function createShadow(shadow: number): Shadow { - return { shadow: sprites2[shadow] }; + return { shadow: sprites2[shadow] }; } export function createColorShadowPalette(color: number, shadow: number, paletteIndexes: number[]): ColorShadow { - return { color: sprites2[color], shadow: sprites2[shadow], palettes: paletteIndexes.map(getPalette) }; + return { color: sprites2[color], shadow: sprites2[shadow], palettes: paletteIndexes.map(getPalette) }; } export function createNose(color: number, colors: number, mouth: number, fangs: number) { - return { color: sprites2[color], colors, mouth: sprites2[mouth], fangs: sprites2[fangs] }; + return { color: sprites2[color], colors, mouth: sprites2[mouth], fangs: sprites2[fangs] }; } export function createEye(base: number, irises: number[], shadow?: number, shine?: number): PonyEye { - return { base: sprites2[base], irises: mapSprites2(irises), shadow: sprites2[shadow || 0], shine: sprites2[shine || 0] }; + return { base: sprites2[base], irises: mapSprites2(irises), shadow: sprites2[shadow || 0], shine: sprites2[shine || 0] }; } export function createAnimation(frames: number[]) { - return { frames: mapSprites(frames) }; + return { frames: mapSprites(frames) }; } export function createAnimationPalette(frames: number[], palette: number) { - return { frames: mapSprites2(frames), palette: getPalette(palette) }; + return { frames: mapSprites2(frames), palette: getPalette(palette) }; } export function createAnimationShadow(frames: number[], shadow: number, palette: number) { - return { frames: mapSprites2(frames), shadow: sprites2[shadow], palette: getPalette(palette) }; + return { frames: mapSprites2(frames), shadow: sprites2[shadow], palette: getPalette(palette) }; } export { - Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, - colorPal3, colorPal5, colorPal7, colorPal9, colorPal11, colorPal13, colorPal17 + Sprite, SpriteBorder, PonyNose, PonyEye, ColorExtra, ColorExtraSets, + colorPal3, colorPal5, colorPal7, colorPal9, colorPal11, colorPal13, colorPal17 }; diff --git a/src/ts/tools/types.ts b/src/ts/tools/types.ts index 48dad23..3480ba5 100644 --- a/src/ts/tools/types.ts +++ b/src/ts/tools/types.ts @@ -1,112 +1,112 @@ export interface Psd { - dir: string; - name: string; - info: string; - width: number; - height: number; - children: Layer[]; + dir: string; + name: string; + info: string; + width: number; + height: number; + children: Layer[]; } export interface Layer { - name: string; - info: string; - children: Layer[]; - canvas?: ExtCanvas; + name: string; + info: string; + children: Layer[]; + canvas?: ExtCanvas; } export interface ExtCanvas extends HTMLCanvasElement { - info: string; + info: string; } export interface Sprite extends Rect { - ox: number; - oy: number; - image: HTMLCanvasElement; - shade?: boolean; - layer?: number; + ox: number; + oy: number; + image: HTMLCanvasElement; + shade?: boolean; + layer?: number; } export interface Result { - objects: any; - objects2: any; - images: HTMLCanvasElement[]; - sprites: Sprite[]; - // palettes: number[][]; + objects: any; + objects2: any; + images: HTMLCanvasElement[]; + sprites: Sprite[]; + // palettes: number[][]; } export interface ColorShadow { - color: number; - shadow: number; + color: number; + shadow: number; } export interface Nose { - mouth: number; - muzzle: number; - fangs: number; + mouth: number; + muzzle: number; + fangs: number; } export interface Eye { - base: number; - irises: number[]; - shadow: number; - shine: number; + base: number; + irises: number[]; + shadow: number; + shine: number; } export interface Point { - x: number; - y: number; + x: number; + y: number; } export interface Rect { - x: number; - y: number; - w: number; - h: number; + x: number; + y: number; + w: number; + h: number; } export interface Tree { - stump: number; - trunk: number; - crown: number; - stumpShadow: number; - shadow: number; - palette: number[]; + stump: number; + trunk: number; + crown: number; + stumpShadow: number; + shadow: number; + palette: number[]; } export interface Animation { - frames: number[]; - palette: number[]; - shadow?: number; + frames: number[]; + palette: number[]; + shadow?: number; } export interface ColorExtra { - color: number; - colors: number; - extra?: number; - palette?: number[]; - palettes?: number[][]; + color: number; + colors: number; + extra?: number; + palette?: number[]; + palettes?: number[][]; } export interface TileSprites { - sprites: number[]; - palettes: number[][]; + sprites: number[]; + palettes: number[][]; } export interface Button { - border: number; - topLeft: number; - top: number; - topRight: number; - left: number; - bg: number; - right: number; - bottomLeft: number; - bottom: number; - bottomRight: number; - palette?: number[]; + border: number; + topLeft: number; + top: number; + topRight: number; + left: number; + bg: number; + right: number; + bottomLeft: number; + bottom: number; + bottomRight: number; + palette?: number[]; } export interface Emote { - name: string; - sprite: number; + name: string; + sprite: number; } diff --git a/src/typings/my/canvas.d.ts b/src/typings/my/canvas.d.ts index c7d7cc1..76b6d0c 100644 --- a/src/typings/my/canvas.d.ts +++ b/src/typings/my/canvas.d.ts @@ -1,22 +1,22 @@ interface HTMLCanvasElement { - toBuffer(callback: (err: any, buffer: Buffer) => void): void; - toBuffer(): Buffer; + toBuffer(callback: (err: any, buffer: Buffer) => void): void; + toBuffer(): Buffer; } interface NodeCanvasImage extends HTMLImageElement { - src: any; + src: any; } interface NodeCanvas extends HTMLCanvasElement { - new(width?: number, height?: number): NodeCanvas; + new(width?: number, height?: number): NodeCanvas; } interface NodeStaticImage { - new(width?: number, height?: number): NodeCanvasImage; + new(width?: number, height?: number): NodeCanvasImage; } -declare module "canvas" { - export function createCanvas(width: number, height: number): HTMLCanvasElement; - export type Canvas = HTMLCanvasElement; - export const Image: NodeStaticImage; +declare module 'canvas' { + export function createCanvas(width: number, height: number): HTMLCanvasElement; + export type Canvas = HTMLCanvasElement; + export const Image: NodeStaticImage; } diff --git a/src/typings/my/fix.d.ts b/src/typings/my/fix.d.ts index 34d6c5f..6eeebe6 100644 --- a/src/typings/my/fix.d.ts +++ b/src/typings/my/fix.d.ts @@ -1,21 +1,21 @@ interface HTMLCanvasElement { - getContext(contextId: "webgl2", contextAttributes?: WebGLContextAttributes): WebGLRenderingContext | null; + getContext(contextId: 'webgl2', contextAttributes?: WebGLContextAttributes): WebGLRenderingContext | null; } interface WebGLRenderingContext { - readonly MAX_ELEMENT_INDEX: number; + readonly MAX_ELEMENT_INDEX: number; } interface Window { - readonly Notification: any; + readonly Notification: any; } interface NodeModule { - hot?: { accept(): void; }; + hot?: { accept(): void; }; } interface Promise { - finally(handler: () => any): Promise; + finally(handler: () => any): Promise; } declare const requestIdleCallback: (callback: () => void) => number; @@ -46,21 +46,21 @@ declare module 'passport-steam' { export const Strategy: any; } declare module 'passport-patreon' { export const Strategy: any; } declare module 'timsort' { - export function sort(array: T[], compare: (a: T, b: T) => number): void; + export function sort(array: T[], compare: (a: T, b: T) => number): void; } declare module 'color-convert' { - export const hex: { - lab(color: string): [number, number, number]; - }; + export const hex: { + lab(color: string): [number, number, number]; + }; } declare module 'delta-e' { - export interface LAB { - L: number; - A: number; - B: number; - } + export interface LAB { + L: number; + A: number; + B: number; + } - export function getDeltaE00(a: LAB, b: LAB): number; + export function getDeltaE00(a: LAB, b: LAB): number; } diff --git a/src/typings/my/node-async.d.ts b/src/typings/my/node-async.d.ts index e7c686a..17cb2e2 100644 --- a/src/typings/my/node-async.d.ts +++ b/src/typings/my/node-async.d.ts @@ -1,22 +1,22 @@ -declare module "fs" { - import * as Promise from "bluebird"; +declare module 'fs' { + import * as Promise from "bluebird"; - export function writeFileAsync(filename: string, data: any): Promise; - export function writeFileAsync(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; } | string): Promise; - export function writeFileAsync(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; } | string): Promise; - export function appendFileAsync(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; } | string): Promise; - export function appendFileAsync(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; } | string): Promise; - export function appendFileAsync(filename: string, data: any): Promise; - export function readFileAsync(filename: string, encoding: string): Promise; - export function readFileAsync(filename: string, options: { encoding: string; flag?: string; }): Promise; - export function readFileAsync(filename: string, options: { flag?: string; }): Promise; - export function readFileAsync(filename: string): Promise; - export function readdirAsync(path: string): Promise; - export function unlinkAsync(path: string): Promise; - export function mkdirAsync(path: string): Promise; - export function mkdirAsync(path: string, mode: number): Promise; - export function mkdirAsync(path: string, mode: string): Promise; - export function renameAsync(oldPath: string, newPath: string): Promise; - export function statAsync(path: string): Promise; - export function rmdirAsync(path: string): Promise; + export function writeFileAsync(filename: string, data: any): Promise; + export function writeFileAsync(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; } | string): Promise; + export function writeFileAsync(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; } | string): Promise; + export function appendFileAsync(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; } | string): Promise; + export function appendFileAsync(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; } | string): Promise; + export function appendFileAsync(filename: string, data: any): Promise; + export function readFileAsync(filename: string, encoding: string): Promise; + export function readFileAsync(filename: string, options: { encoding: string; flag?: string; }): Promise; + export function readFileAsync(filename: string, options: { flag?: string; }): Promise; + export function readFileAsync(filename: string): Promise; + export function readdirAsync(path: string): Promise; + export function unlinkAsync(path: string): Promise; + export function mkdirAsync(path: string): Promise; + export function mkdirAsync(path: string, mode: number): Promise; + export function mkdirAsync(path: string, mode: string): Promise; + export function renameAsync(oldPath: string, newPath: string): Promise; + export function statAsync(path: string): Promise; + export function rmdirAsync(path: string): Promise; } diff --git a/src/typings/my/patreon.d.ts b/src/typings/my/patreon.d.ts index d503e24..24cfb03 100644 --- a/src/typings/my/patreon.d.ts +++ b/src/typings/my/patreon.d.ts @@ -1,62 +1,66 @@ declare module 'patreon' { - export interface PatronDataRelation { - data: { - id: string; - type: string; - }; - links: { - related: string; - }; - } - - export interface PatronDataEntry { - attributes: { - title?: string; - description?: string; - amount_cents?: number; - created_at?: string; - declined_since?: string | null; - patron_pays_fees?: boolean; - pledge_cap_cents?: number; - total_historical_amount_cents?: number; - }; - id: string; - relationships: { - patron: PatronDataRelation; - reward: PatronDataRelation; - }; - type: string; - } + export interface PatronDataRelation { + data: { + id: string; + type: string; + }; + links: { + related: string; + }; + } - export interface PatronData { - rawJson: { - data: PatronDataEntry[]; - included: PatronDataEntry[]; - links: { - next?: string; - }; - }; - } + export interface PatronDataEntry { + attributes: { + title?: string; + description?: string; + amount_cents?: number; + created_at?: string; + declined_since?: string | null; + patron_pays_fees?: boolean; + pledge_cap_cents?: number; + total_historical_amount_cents?: number; + }; + id: string; + relationships: { + patron: PatronDataRelation; + reward: PatronDataRelation; + }; + type: string; + } - export interface PatreonClient { - (pathname: string): Promise; - getStore(): any; - setStore(store: { sync: () => void }): void; - } + export interface PatronData { + rawJson: { + data: PatronDataEntry[]; + included: PatronDataEntry[]; + links: { + next?: string; + }; + }; + } - export interface PatreonTokens { - access_token: string; - refresh_token: string; - expires_in: string; - scope: string; - token_type: string; - } + export interface PatreonClient { + (pathname: string): Promise; - export interface PatreonOAuthClient { - getTokens(redirectCode: string, redirectUri: string): Promise; - refreshToken(refreshToken: string): Promise; - } + getStore(): any; - export function patreon(accessToken: string): PatreonClient; - export function oauth(clientId: string, clientSecret: string): PatreonOAuthClient; + setStore(store: { sync: () => void }): void; + } + + export interface PatreonTokens { + access_token: string; + refresh_token: string; + expires_in: string; + scope: string; + token_type: string; + } + + export interface PatreonOAuthClient { + getTokens(redirectCode: string, redirectUri: string): Promise; + + refreshToken(refreshToken: string): Promise; + } + + export function patreon(accessToken: string): PatreonClient; + + export function oauth(clientId: string, clientSecret: string): PatreonOAuthClient; } diff --git a/tslint.json b/tslint.json index 62ae03b..a226754 100644 --- a/tslint.json +++ b/tslint.json @@ -1,81 +1,82 @@ { - "rules": { - "ban": [ - true, - "name", - "length", - "focus" - ], - "class-name": true, - "forin": true, - "indent": [ - true, - "tabs" - ], - "label-position": true, - "no-arg": true, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], - "adjacent-overload-signatures": true, - "no-internal-module": true, - "no-namespace": true, - "no-construct": true, - "no-consecutive-blank-lines": [ - true, - 2 - ], - "no-debugger": true, - "no-duplicate-variable": true, - "no-eval": true, - "no-trailing-whitespace": true, - "linebreak-style": [ - true, - "CRLF" - ], - "max-line-length": [ - true, - 130 - ], - "one-line": [ - true, - "check-open-brace", - "check-catch", - "check-whitespace" - ], - "radix": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "variable-name": false, - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-module", - "check-separator", - "check-type" - ], - "eofline": true, - "quotemark": [ - true, - "single", - "avoid-escape" - ], - "array-type": [ - true, - "array" - ] - } -} \ No newline at end of file + "rules": { + "ban": [ + true, + "name", + "length", + "focus" + ], + "class-name": true, + "forin": true, + "indent": [ + true, + "spaces", + 2 + ], + "label-position": true, + "no-arg": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "adjacent-overload-signatures": true, + "no-internal-module": true, + "no-namespace": true, + "no-construct": true, + "no-consecutive-blank-lines": [ + true, + 2 + ], + "no-debugger": true, + "no-duplicate-variable": true, + "no-eval": true, + "no-trailing-whitespace": true, + "linebreak-style": [ + true, + "LF" + ], + "max-line-length": [ + true, + 140 + ], + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-whitespace" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-module", + "check-separator", + "check-type" + ], + "eofline": true, + "quotemark": [ + true, + "single", + "avoid-escape" + ], + "array-type": [ + true, + "array" + ] + } +}