Archive commit

This commit is contained in:
Erik McClure
2019-08-28 21:15:02 -07:00
commit 735845746e
1435 changed files with 140724 additions and 0 deletions
+676
View File
@@ -0,0 +1,676 @@
import { compact } from 'lodash';
import {
Expression, ExpressionButtonAction, CommandButtonAction, ActionButtonAction, ItemButtonAction,
ColorShadow, Eye, Muzzle, Iris, ButtonActionSlot, ExpressionExtra, ButtonAction, ChatType, BodyAnimation,
Action, isPartyChat, EntityButtonAction, defaultDrawOptions
} from '../common/interfaces';
import * as sprites from '../generated/sprites';
import { createExpression } from './clientUtils';
import { encodeExpression, decodeExpression } from '../common/encoders/expressionEncoder';
import { PonyTownGame } from './game';
import { boopAction, upAction, downAction, turnHeadAction } from './playerActions';
import { ACTIONS_LIMIT, COMMAND_ACTION_TIME_DELAY } from '../common/constants';
import { cloneDeep, hasFlag } from '../common/utils';
import { boop, defaultHeadFrame, stand, sneeze, yawn, lie, sit, fly, laugh } from './ponyAnimations';
import { createDefaultPony, syncLockedPonyInfo, toPalette, mockPaletteManager } from '../common/ponyInfo';
import {
ACTION_EXPRESSION_EYE_COLOR, ACTION_EXPRESSION_BG, ACTION_ACTION_COAT_COLOR, WHITE, HEARTS_COLOR,
ACTION_COMMAND_BG, BLACK, ACTION_ACTION_BG, ACTION_ITEM_BG, blushColor, ENTITY_ITEM_BG, TRANSPARENT
} from '../common/colors';
import { resizeCanvasWithRatio, getPixelRatio, disableImageSmoothing } from './canvasUtils';
import { drawCanvas, ContextSpriteBatch } from '../graphics/contextSpriteBatch';
import { defaultPonyState, defaultDrawPonyOptions } from './ponyHelpers';
import { drawHead, drawPony } from './ponyDraw';
import { parseColor, toGrayscale, colorToHexRGB } from '../common/color';
import { rect, addRects, centerPoint } from '../common/rect';
import { drawTextAligned, HAlign, VAlign } from '../graphics/spriteFont';
import { fontPal } from '../client/fonts';
import { isPonyLying, isPonySitting, isPonyStanding, isPonyFlying } from '../common/entityUtils';
import { canPonyFly } from '../common/pony';
import { apple2, createAnEntity } from '../common/entities';
import { fakePaletteManager } from '../common/mixins';
import { spriteSheetsLoaded } from './spriteUtils';
import { getEntityTypesFromName } from '../components/services/model';
import { toWorldY, toWorldX } from '../common/positionUtils';
const CANVAS_SIZE = 29;
const ICON_SIZE = 16;
const headX = -26;
const headY = -30;
const headlessBoopFrame = { ...cloneDeep(boop.frames[7]), head: 0 };
const headlessBoop: BodyAnimation = { name: '', loop: false, fps: 1, frames: [headlessBoopFrame] };
function createPony(coatColor: string, wings = false, horn = false) {
const info = createDefaultPony();
info.coatFill = coatColor;
info.mane!.type = 0;
info.backMane!.type = 0;
info.tail!.type = 0;
info.eyeColorRight = ACTION_EXPRESSION_EYE_COLOR;
if (wings) {
info.wings!.type = 1;
}
if (horn) {
info.horn!.type = 1;
}
syncLockedPonyInfo(info);
return toPalette(info, mockPaletteManager);
}
function createState() {
const state = defaultPonyState();
state.blushColor = blushColor(parseColor(ACTION_ACTION_COAT_COLOR));
return state;
}
function colorToGrayscale(value: string) {
return colorToHexRGB(toGrayscale(parseColor(value)));
}
const ACTION_ACTION_BG_DISABLED = toGrayscale(parseColor(ACTION_ACTION_BG));
const expressionPony = createPony(ACTION_EXPRESSION_BG);
const actionPony = createPony(ACTION_ACTION_COAT_COLOR);
const actionPonyWithHorn = createPony(ACTION_ACTION_COAT_COLOR, false, true);
const actionPonyWithWings = createPony(ACTION_ACTION_COAT_COLOR, true);
const actionPonyDisabled = createPony(colorToGrayscale(ACTION_ACTION_COAT_COLOR));
const actionPonyWithWingsDisabled = createPony(colorToGrayscale(ACTION_ACTION_COAT_COLOR), true);
const defaultPalette = mockPaletteManager.addArray(sprites.defaultPalette);
export const actionExpressionDefaultPalette = mockPaletteManager.add(Array.from(sprites.defaultPalette));
expressionPony.defaultPalette = actionExpressionDefaultPalette;
expressionPony.defaultPalette.colors[4] = 0xe16200ff; // tongue color
export function expressionButtonAction(expression: Expression | undefined): ExpressionButtonAction {
return { type: 'expression', expression, title: expression ? '' : 'Reset expression' };
}
export function commandButtonAction(command: string, icon: string): CommandButtonAction {
return { type: 'command', command, title: command, icon };
}
export function actionButtonAction(action: string, title: string, sendAction = Action.None): ActionButtonAction {
return { type: 'action', action, title, sendAction };
}
export function itemButtonAction(icon: ColorShadow, count?: number): ItemButtonAction {
return { type: 'item', icon, count };
}
export function entityButtonAction(entity: string): EntityButtonAction {
return { type: 'entity', entity, title: entity };
}
const actionActions = [
actionButtonAction('boop', 'Boop'),
actionButtonAction('down', 'Sit down / Land'),
actionButtonAction('up', 'Stand up / Fly up'),
actionButtonAction('turn-head', 'Turn head'),
actionButtonAction('sneeze', 'Sneeze', Action.Sneeze),
actionButtonAction('sleep', 'Sleep', Action.Sleep),
actionButtonAction('yawn', 'Yawn', Action.Yawn),
actionButtonAction('love', 'Love', Action.Love),
actionButtonAction('laugh', 'Laugh', Action.Laugh),
actionButtonAction('blush', 'Blush', Action.Blush),
actionButtonAction('drop', 'Drop item', Action.Drop),
actionButtonAction('drop-toy', 'Drop toy', Action.DropToy),
actionButtonAction('magic', 'Magic', Action.Magic),
actionButtonAction('switch-tool', 'Switch tool', Action.SwitchTool),
actionButtonAction('switch-entity', 'Switch item to place'),
actionButtonAction('switch-entity-rev', 'Switch item to place (reverse)'),
actionButtonAction('switch-tile', 'Switch tile to place'),
];
const commandActions = [
commandButtonAction('/roll', '🎲'),
commandButtonAction('/gifts', '🎁'),
commandButtonAction('/candies', '🍬'),
commandButtonAction('/clovers', '🍀'),
commandButtonAction('/toys', '🎅'),
commandButtonAction('/eggs', '🥚'),
];
const additionalActionsActions = [
expressionButtonAction(undefined),
];
function getActionAction(action: string) {
return actionActions.find(a => a.action === action);
}
function getCommandAction(command: string) {
return commandActions.find(a => a.command === command);
}
export function createButtionActionActions() {
return [...actionActions, ...additionalActionsActions];
}
export function createButtonCommandActions() {
return [...commandActions];
}
export function createDefaultButtonActions(): ButtonActionSlot[] {
return DEVELOPMENT ? [
{ action: getActionAction('boop') },
{ action: getActionAction('down') },
{ action: getActionAction('up') },
{ action: getActionAction('turn-head') },
{ action: expressionButtonAction(createExpression(Eye.Closed, Eye.Closed, Muzzle.Smile)) },
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral3, Muzzle.Smile)) },
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Scrunch, Iris.Forward, Iris.Up)) },
{ action: expressionButtonAction(createExpression(Eye.Angry, Eye.Angry, Muzzle.Scrunch)) },
{ action: expressionButtonAction(createExpression(Eye.Neutral3, Eye.Neutral3, Muzzle.Flat, Iris.Left, Iris.Left)) },
{ action: expressionButtonAction(createExpression(Eye.X, Eye.X, Muzzle.Flat)) },
{ action: expressionButtonAction(createExpression(Eye.Neutral4, Eye.Neutral4, Muzzle.Flat)) },
{ action: undefined },
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Shocked, Iris.Shocked)) },
{ action: expressionButtonAction(createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left)) },
{ action: getCommandAction('/roll') },
{ action: itemButtonAction(sprites.flower_2, 14) },
{ action: itemButtonAction(sprites.apple_1, 5) },
{ action: itemButtonAction(sprites.pumpkin_default) },
{ action: itemButtonAction(sprites.tree_1, 2) },
{
action: expressionButtonAction(
createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Shocked, Iris.Shocked, ExpressionExtra.Tears))
},
{
action: expressionButtonAction(
createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, ExpressionExtra.Cry))
},
{
action: expressionButtonAction(
createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, ExpressionExtra.Hearts))
},
{
action: expressionButtonAction(
createExpression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left, ExpressionExtra.Zzz))
},
{
action: expressionButtonAction(
createExpression(
Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left,
ExpressionExtra.Zzz | ExpressionExtra.Cry | ExpressionExtra.Hearts | ExpressionExtra.Blush))
},
] : [
{ action: getActionAction('boop') },
{ action: getActionAction('down') },
{ action: getActionAction('up') },
{ action: getActionAction('turn-head') },
{ action: getActionAction('magic') },
{ action: expressionButtonAction(undefined) },
{ action: expressionButtonAction(createExpression(Eye.Closed, Eye.Closed, Muzzle.Smile)) },
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral3, Muzzle.Smile)) },
{ action: expressionButtonAction(createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Scrunch, Iris.Forward, Iris.Up)) },
{ action: expressionButtonAction(createExpression(Eye.Angry, Eye.Angry, Muzzle.Scrunch)) },
];
}
export function serializeActions(slots: ButtonActionSlot[]): string {
const serialized = slots.slice(0, ACTIONS_LIMIT).map(serializeAction);
while (serialized.length && !serialized[serialized.length - 1]) {
serialized.pop();
}
return JSON.stringify(serialized);
}
export function deserializeActions(data: string): ButtonActionSlot[] {
try {
const json = JSON.parse(data);
return json.slice(0, ACTIONS_LIMIT).map(deserializeAction);
} catch (e) {
DEVELOPMENT && console.error(e);
return [];
}
}
function serializeAction({ action }: ButtonActionSlot): any {
if (action) {
switch (action.type) {
case 'action':
return { act: action.action };
case 'command':
return { cmd: action.command };
case 'expression':
return { exp: encodeExpression(action.expression) };
case 'entity':
return { ent: action.entity };
default:
DEVELOPMENT && console.warn(`Missing serialization for ${JSON.stringify(action)}`);
return null;
}
} else {
return null;
}
}
function deserializeAction(data: any): ButtonActionSlot {
if (data) {
if ('act' in data || 'action' in data) {
return { action: getActionAction(data.act || data.action) };
} else if ('cmd' in data || 'command' in data) {
return { action: getCommandAction(data.cmd || data.command) };
} else if ('exp' in data || 'expression' in data) {
const expression = decodeExpression(data.exp || data.expression | 0);
return { action: expressionButtonAction(expression) };
} else if ('ent' in data || 'entity' in data) {
return { action: entityButtonAction(data.ent || data.entity) };
} else {
DEVELOPMENT && console.warn(`Missing deserialization for ${JSON.stringify(data)}`);
}
}
return { action: undefined };
}
const lastCommandCalls: { [key: string]: number; } = {};
export function useAction(game: PonyTownGame, action: ButtonAction | undefined) {
if (action) {
switch (action.type) {
case 'expression':
game.send(server => server.expression(encodeExpression(action.expression)));
break;
case 'action':
if (action.sendAction) {
game.send(server => server.action(action.sendAction));
} else {
switch (action.action) {
case 'boop':
boopAction(game);
break;
case 'up':
upAction(game);
break;
case 'down':
downAction(game);
break;
case 'turn-head':
turnHeadAction(game);
break;
case 'switch-entity':
game.send(server => server.action(Action.SwitchToPlaceTool));
game.changePlaceEntity(false);
break;
case 'switch-entity-rev':
game.send(server => server.action(Action.SwitchToPlaceTool));
game.changePlaceEntity(true);
break;
case 'switch-tile':
game.send(server => server.action(Action.SwitchToTileTool));
game.changePlaceTile(false);
break;
default:
console.log('Action not supported: ', action.action);
}
}
break;
case 'command':
const now = performance.now();
const lastCall = lastCommandCalls[action.command] | 0;
if ((now - lastCall) > COMMAND_ACTION_TIME_DELAY) {
lastCommandCalls[action.command] = now;
const chatType = isPartyChat(game.lastChatMessageType) ? ChatType.Party : ChatType.Say;
game.send(server => server.say(0, action.command, chatType));
}
break;
case 'entity':
if (BETA) {
game.editor.type = action.entity;
}
break;
default:
console.log('Action type not supported: ', action.type);
}
}
}
function shouldRedrawAction(action: ButtonAction | undefined, state: any, game: PonyTownGame) {
if (action !== state.action) {
return true;
} else if (action) {
switch (action.type) {
case 'action': {
switch (action.action) {
case 'up':
return state.draw !== getUpDrawFunc(game);
case 'down':
return state.draw !== getDownDrawFunc(game);
// case 'turn-head':
// return state.right !== (game.player && isHeadFacingRight(game.player));
default:
return false;
}
}
default:
return false;
}
} else {
return false;
}
}
const canvasCache = new Map<string, HTMLCanvasElement>();
const palette = mockPaletteManager.addArray(sprites.fontPalette);
const emojiPalette = mockPaletteManager.addArray(sprites.emojiPalette);
function drawCanvasCached(key: string, action: (batch: ContextSpriteBatch) => void) {
const canvas = canvasCache.get(key) || drawCanvas(ICON_SIZE, ICON_SIZE, sprites.paletteSpriteSheet, undefined, action);
canvasCache.set(key, canvas);
return canvas;
}
export function drawAction(canvas: HTMLCanvasElement, action: ButtonAction | undefined, state: any, game: PonyTownGame) {
if (resizeCanvasWithRatio(canvas, CANVAS_SIZE, CANVAS_SIZE)) {
state.action = 0;
}
if (!spriteSheetsLoaded || !shouldRedrawAction(action, state, game))
return;
const context = canvas.getContext('2d');
if (!context)
return;
state.action = action;
context.save();
context.clearRect(0, 0, canvas.width, canvas.height);
disableImageSmoothing(context);
const scale = 2 * getPixelRatio();
const bufferSize = ICON_SIZE;
if (action) {
switch (action.type) {
case 'expression': {
const buffer = drawCanvas(bufferSize, bufferSize, sprites.paletteSpriteSheet, undefined, batch => {
const state = { ...createState(), expression: action.expression };
const options = { ...defaultDrawPonyOptions(), noEars: true };
drawHead(batch, expressionPony, headX, headY, undefined, defaultHeadFrame, state, options, false, 0);
if (action.expression) {
const extra = action.expression.extra;
if (hasFlag(extra, ExpressionExtra.Zzz)) {
batch.drawSprite(sprites.emote_sleep1.frames[13], WHITE, defaultPalette, headX + 15, headY + 3);
}
if (hasFlag(extra, ExpressionExtra.Hearts)) {
batch.drawSprite(sprites.emote_hearts.frames[41], HEARTS_COLOR, defaultPalette, headX + 8, headY + 22);
}
if (hasFlag(extra, ExpressionExtra.Cry)) {
batch.drawSprite(sprites.emote_cry2.frames[4], WHITE, defaultPalette, headX, headY);
} else if (hasFlag(extra, ExpressionExtra.Tears)) {
batch.drawSprite(sprites.emote_tears.frames[0], WHITE, defaultPalette, headX, headY);
}
} else {
const color = parseColor(ACTION_EXPRESSION_BG);
batch.drawRect(color, 0, 3, 15, 5);
batch.drawRect(color, 0, 8, 3, 1);
batch.drawRect(color, 8, 8, 4, 1);
}
});
context.fillStyle = ACTION_EXPRESSION_BG;
context.fillRect(0, 0, canvas.width, canvas.height);
context.scale(scale, scale);
context.drawImage(buffer, 0, 0);
break;
}
case 'command': {
const buffer = drawCanvasCached(`command:${action.icon}`, batch => {
const bounds = rect(0, 0, 15, 15);
const options = { palette, emojiPalette };
drawTextAligned(batch, action.icon, fontPal, BLACK, bounds, HAlign.Center, VAlign.Middle, options);
});
context.fillStyle = ACTION_COMMAND_BG;
context.fillRect(0, 0, canvas.width, canvas.height);
context.scale(scale, scale);
context.drawImage(buffer, -0.5, 0.5);
break;
}
case 'action': {
let buffer: HTMLCanvasElement;
if (action.action === 'up' || action.action === 'down') {
state.draw = action.action === 'up' ? getUpDrawFunc(game) : getDownDrawFunc(game);
buffer = drawCanvasCached(`action:${action.action}:${state.draw}`, batch => {
state.draw && getDrawFuncByName(state.draw)(batch);
});
} else {
buffer = drawCanvasCached(`action:${action.action}`, batch => {
switch (action.action) {
case 'boop': {
const state = { ...createState(), animation: headlessBoop, animationFrame: 0 };
drawPony(batch, actionPony, state, 25, 32, defaultDrawPonyOptions());
break;
}
case 'turn-head': {
// state.right = game.player && isHeadFacingRight(game.player);
const ponyState = { ...createState(), animation: stand };
drawPony(batch, actionPony, ponyState, 15, 40, defaultDrawPonyOptions());
// if (!state.right) {
// context.translate(context.canvas.width, 0);
// context.scale(-1, 1);
// }
break;
}
case 'sneeze': {
const state = { ...createState(), headAnimation: sneeze, headAnimationFrame: 3 };
drawPony(batch, actionPony, state, 17, 40, defaultDrawPonyOptions());
break;
}
case 'sleep': {
const state = { ...createState(), expression: createExpression(Eye.Closed, Eye.Closed, Muzzle.Neutral) };
drawPony(batch, actionPony, state, 18, 40, defaultDrawPonyOptions());
batch.drawSprite(sprites.emote_sleep1.frames[13], WHITE, defaultPalette, headX + 15, headY + 3);
break;
}
case 'drop': {
const state = { ...createState(), holding: fakePaletteManager(() => apple2(0, 0)) };
drawPony(batch, actionPony, state, 20, 40, defaultDrawPonyOptions());
batch.drawSprite(sprites.arrow_down, BLACK, defaultPalette, 1, 3);
break;
}
case 'drop-toy': {
const state = { ...createState() };
const options = { ...defaultDrawPonyOptions(), toy: 17 };
drawPony(batch, actionPony, state, 18, 52, options);
batch.drawSprite(sprites.arrow_down, BLACK, defaultPalette, 1, 3);
break;
}
case 'yawn': {
const state = { ...createState(), headAnimation: yawn, headAnimationFrame: 3 };
drawPony(batch, actionPony, state, 17, 40, defaultDrawPonyOptions());
break;
}
case 'laugh': {
const state = { ...createState(), headAnimation: laugh, headAnimationFrame: 3 };
drawPony(batch, actionPony, state, 17, 38, defaultDrawPonyOptions());
break;
}
case 'blush': {
const state = {
...createState(), expression: createExpression(
Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush)
};
drawPony(batch, actionPony, state, 17, 40, defaultDrawPonyOptions());
break;
}
case 'love': {
batch.drawSprite(sprites.emote_hearts.frames[10], 0xbc414fff, defaultPalette, headX + 2, headY + 18);
break;
}
case 'magic': {
const state = { ...createState(), headAnimation: laugh, headAnimationFrame: 3 };
drawPony(batch, actionPonyWithHorn, state, 17, 48, defaultDrawPonyOptions());
batch.drawSprite(sprites.magic_icon, WHITE, defaultPalette, 4, 2);
break;
}
case 'switch-tool': {
const palette = mockPaletteManager.addArray(sprites.tools_icon.palettes![0]);
batch.drawSprite(sprites.tools_icon.color, WHITE, palette, 0, 2);
break;
}
case 'switch-entity': {
const palette = mockPaletteManager.addArray(sprites.hammer.palettes![0]);
batch.drawSprite(sprites.hammer.color, WHITE, palette, 2, 2);
break;
}
case 'switch-entity-rev': {
const palette = mockPaletteManager.addArray(sprites.hammer.palettes![0]);
batch.drawSprite(sprites.hammer.color, WHITE, palette, 2, 3);
batch.drawSprite(sprites.arrow_left, BLACK, defaultPalette, 1, 1);
break;
}
case 'switch-tile': {
const palette = mockPaletteManager.addArray(sprites.hammer.palettes![0]);
batch.drawSprite(sprites.hammer.color, WHITE, palette, 2, 2);
break;
}
default:
throw new Error(`Invalid action: ${action.action}`);
}
});
}
context.fillStyle = ACTION_ACTION_BG;
context.fillRect(0, 0, canvas.width, canvas.height);
context.scale(scale, scale);
context.drawImage(buffer, 0, 0);
break;
}
case 'item': {
const buffer = drawCanvas(bufferSize, bufferSize, sprites.paletteSpriteSheet, undefined, batch => {
const palette = mockPaletteManager.addArray(action.icon.palettes![0]);
const sprite = action.icon.color;
batch.drawSprite(sprite, WHITE, palette,
Math.round((15 - sprite.w) / 2), Math.round((15 - sprite.h) / 2) + 1);
});
context.fillStyle = ACTION_ITEM_BG;
context.fillRect(0, 0, canvas.width, canvas.height);
context.scale(scale, scale);
context.drawImage(buffer, -0.5, -0.5);
break;
}
case 'entity': {
if (BETA) {
const types = getEntityTypesFromName(action.entity) || [];
const size = bufferSize * scale;
const entities = types.map(type => createAnEntity(type, 0, 0, 0, {}, mockPaletteManager, game));
// createAnEntity(type, 0, toWorldX(size / 2 - 2), toWorldY(size * 0.75), {}, mockPaletteManager));
const bounds = compact(entities.map(e => e.bounds)).reduce(addRects, rect(0, 0, 0, 0));
const center = centerPoint(bounds);
const buffer = drawCanvas(size, size, sprites.paletteSpriteSheet, undefined, batch => {
for (const entity of entities) {
if (entity.draw) {
entity.x += toWorldX(size / 2 - 2 - center.x);
entity.y += toWorldY(size / 2 - center.y);
entity.draw(batch, { ...defaultDrawOptions, shadowColor: TRANSPARENT });
}
}
});
context.fillStyle = ENTITY_ITEM_BG;
context.fillRect(0, 0, canvas.width, canvas.height);
context.drawImage(buffer, 0, 0);
}
break;
}
}
}
context.restore();
}
function drawLie(batch: ContextSpriteBatch) {
const state = { ...createState(), animation: lie };
drawPony(batch, actionPony, state, -6, 15, defaultDrawPonyOptions());
}
function drawLieDisabled(batch: ContextSpriteBatch) {
const state = { ...createState(), animation: lie };
batch.drawRect(ACTION_ACTION_BG_DISABLED, 0, 0, 50, 50);
drawPony(batch, actionPonyDisabled, state, -6, 15, defaultDrawPonyOptions());
}
function drawSit(batch: ContextSpriteBatch) {
const state = { ...createState(), animation: sit };
drawPony(batch, actionPony, state, -6, 15, defaultDrawPonyOptions());
}
function drawStand(batch: ContextSpriteBatch) {
const state = { ...createState(), animation: stand };
drawPony(batch, actionPony, state, -1, 15, defaultDrawPonyOptions());
}
function drawFly(batch: ContextSpriteBatch) {
const state = { ...createState(), animation: fly };
drawPony(batch, actionPonyWithWings, state, 0, 30, defaultDrawPonyOptions());
}
function drawFlyDisabled(batch: ContextSpriteBatch) {
const state = { ...createState(), animation: fly };
batch.drawRect(ACTION_ACTION_BG_DISABLED, 0, 0, 50, 50);
drawPony(batch, actionPonyWithWingsDisabled, state, 0, 30, defaultDrawPonyOptions());
}
function getDrawFuncByName(name: string) {
switch (name) {
case 'lie': return drawLie;
case 'sit': return drawSit;
case 'stand': return drawStand;
case 'fly': return drawFly;
case 'flyDisabled': return drawFlyDisabled;
case 'lieDisabled': return drawLieDisabled;
default:
throw new Error(`Invalid name: ${name}`);
}
}
function getUpDrawFunc(game: PonyTownGame) {
const player = game.player;
if (player) {
if (isPonyLying(player)) {
return 'sit';
} else if (isPonySitting(player)) {
return 'stand';
} else if (isPonyStanding(player) && canPonyFly(player)) {
return 'fly';
}
}
return 'flyDisabled';
}
function getDownDrawFunc(game: PonyTownGame) {
const player = game.player;
if (player) {
if (isPonySitting(player)) {
return 'lie';
} else if (isPonyStanding(player)) {
return 'sit';
} else if (isPonyFlying(player)) {
return 'stand';
}
}
return 'lieDisabled';
}
+101
View File
@@ -0,0 +1,101 @@
import { saveAs } from 'file-saver';
/* istanbul ignore next */
export let createCanvas = (width: number, height: number): HTMLCanvasElement => {
const canvas = document.createElement('canvas');
canvas.width = width | 0;
canvas.height = height | 0;
return canvas;
};
/* istanbul ignore next */
export let loadImage = (src: string): Promise<HTMLImageElement | ImageBitmap> => {
return new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.addEventListener('load', () => resolve(img));
img.addEventListener('error', () => reject(new Error(`Error loading image (${src})`)));
img.src = src;
});
};
/* istanbul ignore next */
function canUseImageBitmap() {
return typeof fetch === 'function' &&
typeof createImageBitmap === 'function' &&
!/yabrowser/i.test(navigator.userAgent); // disabled due to yandex browser bug
}
/* istanbul ignore next */
if (canUseImageBitmap()) {
loadImage = src => fetch(src)
.then(response => response.blob())
.then(createImageBitmap);
}
export function setup(methods: {
createCanvas(width: number, height: number): HTMLCanvasElement;
loadImage(src: string): Promise<HTMLImageElement>;
}) {
createCanvas = methods.createCanvas;
loadImage = methods.loadImage;
}
/* istanbul ignore next */
export const getPixelRatio = SERVER ? () => 1 : () => window.devicePixelRatio;
export function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number) {
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
}
export function resizeCanvasWithRatio(canvas: HTMLCanvasElement, width: number, height: number, updateStyle = true) {
const ratio = getPixelRatio();
const w = Math.round(width * ratio);
const h = Math.round(height * ratio);
let resized = false;
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
resized = true;
}
if (updateStyle && (canvas.style.width !== width + 'px' || canvas.style.height !== height + 'px')) {
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
resized = true;
}
return resized;
}
/* istanbul ignore next */
export function canvasToSource(canvas: HTMLCanvasElement) {
return new Promise<string>((resolve, reject) => {
canvas.toBlob(blob => {
if (blob) {
resolve(URL.createObjectURL(blob));
} else {
reject(new Error('Failed to convert canvas'));
}
});
});
}
/* istanbul ignore next */
export function saveCanvas(canvas: HTMLCanvasElement, name: string) {
canvas.toBlob(blob => blob && saveAs(blob, name));
}
/* istanbul ignore next */
export function disableImageSmoothing(context: CanvasRenderingContext2D) {
if ('imageSmoothingEnabled' in context) {
context.imageSmoothingEnabled = false;
} else {
(context as any).webkitImageSmoothingEnabled = false;
(context as any).mozImageSmoothingEnabled = false;
(context as any).msImageSmoothingEnabled = false;
}
}
+304
View File
@@ -0,0 +1,304 @@
import { NgZone } from '@angular/core';
import { Method, SocketClient, Bin, getMethods } from 'ag-sockets/dist/browser';
import {
MapInfo, WorldState, PartyMember, PartyFlags, Action, NotificationFlags, Pony, LeaveReason,
SayData, MapState, defaultMapState, Apply, InfoFlags, PonyData, FriendStatusData, WorldMap
} from '../common/interfaces';
import { hasFlag, findById } from '../common/utils';
import { isPony } from '../common/pony';
import { setTileAtRegion, findEntityById, createWorldMap, removeRegions, updateMapState } from '../common/worldMap';
import { GameService } from '../components/services/gameService';
import { PonyTownGame } from './game';
import { supportsLetAndConst, isInIncognitoMode } from './clientUtils';
import { savePlayerPosition, setAclCookie } from './sec';
import { updateParty } from './partyUtils';
import { addNotification, removeNotification, markGameAsLoaded, resetGameFields, isSelected } from './gameUtils';
import { Model } from '../components/services/model';
import { decodeUpdate } from '../common/encoders/updateDecoder';
import {
updatePonyInfoWithPoof, subscribeRegion, handleUpdates, handleUpdateEntity, handleRemoveEntity, handleSays,
handleEntityInfo, handleUpdatePonies, filterEntityName, handleUpdateFriends
} from './handlers';
import { nameToHTML } from './emoji';
const BinEntityId = Bin.U32;
const BinEntityPlayerState = Bin.U8;
const BinNotificationId = Bin.U16;
const BinSayDatas = [BinEntityId, Bin.Str, Bin.U8];
function findPonyById(map: WorldMap, id: number) {
const entity = findEntityById(map, id);
return entity && isPony(entity) ? entity : undefined;
}
export class ClientActions implements SocketClient {
constructor(private gameService: GameService, private game: PonyTownGame, private model: Model, private zone: NgZone) {
}
private apply: Apply = func => this.zone.run(func);
connected() {
resetGameFields(this.game);
this.game.map = createWorldMap();
this.game.player = undefined;
this.game.joined();
this.apply(() => this.gameService.joined());
const supportsWasm = typeof WebAssembly !== 'undefined';
const info = 0 |
(isInIncognitoMode ? InfoFlags.Incognito : 0) |
(supportsWasm ? InfoFlags.SupportsWASM : 0) |
(supportsLetAndConst() ? InfoFlags.SupportsLetAndConst : 0);
this.game.send(server => server.actionParam2(Action.Info, info));
}
disconnected() {
resetGameFields(this.game);
this.apply(() => this.gameService.disconnected());
}
invalidVersion() {
DEVELOPMENT && !TESTS && console.error('Invalid version');
}
@Method({ binary: [Bin.U32] })
queue(place: number) {
this.game.placeInQueue = place;
}
@Method({ binary: [Bin.Obj, Bin.Bool] })
worldState(state: WorldState, initial: boolean) {
this.game.placeInQueue = 0;
this.game.setWorldState(state, initial);
}
@Method({ binary: [Bin.Obj, Bin.Obj] })
mapState(info: MapInfo, state: MapState) {
this.game.map = createWorldMap(info, state);
this.game.player = undefined;
this.game.setupMap();
updateMapState(this.game.map, defaultMapState, this.game.map.state);
}
@Method({ binary: [Bin.Obj] })
mapUpdate(state: MapState) {
const prevState = this.game.map.state;
this.game.map.state = state;
updateMapState(this.game.map, prevState, this.game.map.state);
}
@Method({ binary: [] })
mapSwitching() {
this.game.loaded = false;
this.game.placeInQueue = 0;
if (this.game.player) {
this.game.player.vx = 0;
this.game.player.vy = 0;
}
}
@Method({ binary: [Bin.I32, Bin.I32, Bin.U8Array] })
mapTest(width: number, height: number, buffer: Uint8Array) {
const data = new Uint32Array(width * height);
(new Uint8Array(data.buffer)).set(buffer);
this.game.minimap = { width, height, data };
}
@Method({ binary: [BinEntityId, Bin.Str, Bin.Str, Bin.Str, Bin.U16] })
myEntity(id: number, name: string, info: string, characterId: string, crc: number) {
this.game.playerId = id;
this.game.playerName = name;
this.game.playerInfo = info;
this.game.playerCRC = crc;
const pony = findById(this.model.ponies, characterId);
if (pony) {
this.model.selectPony(pony);
}
if (this.game.party) {
this.game.party.members.forEach(m => m.self = m.id === id);
this.game.onPartyUpdate.next();
}
const entity = findEntityById(this.game.map, id) as Pony | undefined;
if (entity) {
entity.name = name;
updatePonyInfoWithPoof(this.game, entity, info, crc);
}
this.game.onActionsUpdate.next();
}
@Method({ binary: [[Bin.U8], [Bin.U8Array], Bin.U8Array, [Bin.U8Array], BinSayDatas] })
update(unsubscribes: number[], subscribes: Uint8Array[], updates: Uint8Array | null, regions: Uint8Array[], says: SayData[]) {
removeRegions(this.game.map, unsubscribes);
for (const subscribe of subscribes) {
subscribeRegion(this.game, subscribe);
}
if (subscribes.length) {
markGameAsLoaded(this.game);
}
if (updates) {
handleUpdates(this.game, updates);
}
for (const region of regions) {
const { x, y, updates, removes, tiles } = decodeUpdate(region);
for (const update of updates) {
handleUpdateEntity(this.game, update);
}
for (const id of removes) {
handleRemoveEntity(this.game, id);
}
for (const tile of tiles) {
setTileAtRegion(this.game.map, x, y, tile.x, tile.y, tile.type);
}
}
for (const [id, message, type] of says) {
handleSays(this.game, id, message, type);
}
}
@Method({ binary: [Bin.F32, Bin.F32, Bin.Bool] })
fixPosition(x: number, y: number, safe: boolean) {
if (DEVELOPMENT && !TESTS && !safe) {
console.error(`fix position (${x.toFixed(2)}, ${y.toFixed(2)})`);
}
const player = this.game.player;
if (player) {
player.x = x;
player.y = y;
savePlayerPosition();
}
this.game.send(server => server.fixedPosition());
}
@Method({ binary: [BinEntityId, Bin.U8, Bin.Obj] })
actionParam(id: number, action: Action, param: any) {
switch (action) {
case Action.ACL:
if (id === this.game.playerId && param) {
setAclCookie(param);
}
break;
case Action.FriendsCRC:
this.game.nextFriendsCRC = 0;
break;
default:
DEVELOPMENT && !TESTS && console.error(`actionParam: Invalid action: ${action}`);
}
}
@Method({ binary: [Bin.U8] })
left(reason: LeaveReason) {
this.game.player = undefined;
this.game.map = createWorldMap();
this.apply(() => this.gameService.left('clientActions.left', reason));
}
@Method({ binary: [BinNotificationId, BinEntityId, Bin.Str, Bin.Str, Bin.Str, Bin.U8] })
addNotification(id: number, entityId: number, name: string, message: string, note: string, flags: NotificationFlags) {
const defaultCharacter = hasFlag(flags, NotificationFlags.Supporter) ? this.game.supporterPony : this.game.offlinePony;
const pony = (entityId && findPonyById(this.game.map, entityId)) || defaultCharacter;
const filteredName = filterEntityName(this.game, name, hasFlag(flags, NotificationFlags.NameBad));
message = message.replace(/#NAME#/g, nameToHTML(filteredName || ''));
this.apply(() => addNotification(this.game, { id, message, note, pony, flags, open: false, fresh: true }));
}
@Method({ binary: [BinNotificationId] })
removeNotification(id: number) {
this.apply(() => removeNotification(this.game, id));
}
@Method({ binary: [BinEntityId, BinEntityId] })
updateSelection(currentId: number, newId: number) {
if (isSelected(this.game, currentId)) {
this.game.select(newId ? findPonyById(this.game.map, newId) : undefined);
}
}
@Method({ binary: [[BinEntityId, Bin.U8]] })
updateParty(party: [number, PartyFlags][] | undefined) {
const members = party && party.map<PartyMember>(([id, flags]) => ({
id,
pony: findPonyById(this.game.map, id) || this.game.fallbackPonies.get(id),
self: id === this.game.playerId,
leader: hasFlag(flags, PartyFlags.Leader),
pending: hasFlag(flags, PartyFlags.Pending),
offline: hasFlag(flags, PartyFlags.Offline),
}));
if (members) {
const missing = members.filter(p => !p.pony).map(p => p.id);
if (missing.length) {
this.game.send(server => server.getPonies(missing));
}
}
this.apply(() => {
this.game.party = updateParty(this.game.party, members);
this.game.onPartyUpdate.next();
});
}
@Method({ binary: [[BinEntityId, Bin.Obj, Bin.U8Array, Bin.U8Array, BinEntityPlayerState, Bin.Bool]] })
updatePonies(ponies: PonyData[]) {
handleUpdatePonies(this.game, ponies);
}
@Method({ binary: [Bin.Obj, Bin.Bool] })
updateFriends(friends: FriendStatusData[], removeMissing: boolean) {
handleUpdateFriends(this.game, friends, removeMissing);
}
@Method({ binary: [BinEntityId, Bin.Str, Bin.U32, Bin.Bool] })
entityInfo(id: number, name: string, crc: number, nameBad: boolean) {
handleEntityInfo(this.game, id, name, crc, nameBad);
}
@Method({ binary: [Bin.Obj] })
entityList(value: { name: string; x: number; y: number; }[]) {
if (DEVELOPMENT || BETA) {
const list = value.map(({ name, x, y }) => `${name}(${x.toFixed(2)}, ${y.toFixed(2)})`).join('\n');
console.log(`ENTITIES:\n${list}`);
}
}
@Method({ binary: [Bin.Obj] })
testPositions(data: { frame: number; x: number | undefined; y: number | undefined; moved: boolean; }[]) {
if (DEVELOPMENT) {
const round = (x: number) => Math.round(x * 100);
const same = (ax = 0, ay = 0, bx = 0, by = 0) => round(ax) === round(bx) && round(ay) === round(by);
const fmt = (x: number | undefined) => (x === undefined ? '-' : x.toFixed(2)).padStart(5);
for (let i = 1; i < data.length; i++) {
if (data[i - 1].frame !== (data[i].frame - 1)) {
data.splice(i, 0, { frame: data[i - 1].frame + 1, x: undefined, y: undefined, moved: false });
}
}
const clientIndex = this.game.positions.findIndex(p => p.moved);
const serverIndex = data.findIndex(p => p.moved);
const offset = serverIndex - clientIndex;
const dat = data.map((p, i) => {
const pt = this.game.positions[i - offset] || { x: undefined, y: undefined };
return { frame: p.frame, ax: p.x, ay: p.y, bx: pt.x, by: pt.y, serverMoved: p.moved, clientMoved: pt.moved };
});
const log = dat.map(({ frame, ax, ay, bx, by, serverMoved, clientMoved }, i) =>
`${frame.toString().padStart(7)} | ` +
`${fmt(ax)}, ${fmt(ay)} ${serverMoved ? 'M' : ' '} | ` +
`${fmt(bx)}, ${fmt(by)} ${clientMoved ? 'M' : ' '} | ` +
`${same(ax, ay, bx, by) ? '= ' : ' '} ` +
`${i > 0 && dat[i - 1].frame !== (frame - 1) ? 'I ' : ' '}`)
.join('\n');
console.log(
` frame | server | client | \n` +
`-----------------------------------------------\n` +
`${log}`);
}
}
}
/* istanbul ignore next */
if (DEVELOPMENT) {
getMethods(ClientActions)
.filter(m => !m.options.binary)
.forEach(m => console.error(`Missing binary encoding for ClientActions.${m.name}()`));
}
+34
View File
@@ -0,0 +1,34 @@
import { Method } from 'ag-sockets/dist/browser';
import { AdminModel } from '../components/services/adminModel';
import { ModelTypes } from '../common/adminInterfaces';
import { ModelSubscriber } from '../components/services/modelSubscriber';
export interface ClientUpdate {
type: ModelTypes;
id: string;
update: any;
}
export class ClientAdminActions {
constructor(private model: AdminModel) {
}
connected() {
this.model.initialize(true);
this.model.connectedToSocket();
}
disconnected() {
this.model.updateTitle();
}
@Method()
updates(updates: ClientUpdate[]) {
for (const { type, id, update } of updates) {
const model = this.model[type] as ModelSubscriber<any>;
if (model) {
model.update(id, update);
} else {
console.error(`Invalid model type "${type}"`);
}
}
}
}
+502
View File
@@ -0,0 +1,502 @@
import { clamp } from 'lodash';
import {
SocialSite, SocialSiteInfo, Eye, Muzzle, Iris, ExpressionExtra, Expression, ServerInfo, ServerFeatureFlags,
AccountData, AccountDataFlags
} from '../common/interfaces';
import {
PLAYER_NAME_MAX_LENGTH, SAY_MAX_LENGTH, SAYS_TIME_MIN, SAYS_TIME_MAX, isChatlogRangeUnlimited, SUPPORTER_REWARDS,
PAST_SUPPORTER_REWARDS
} from '../common/constants';
import { matcher, isSurrogate, fromSurrogate, isLowSurrogate } from '../common/stringUtils';
import { oauthProviders } from './data';
import { Subject } from '../../../node_modules/rxjs';
import { PonyTownGame } from './game';
import { toScreenX, toScreenY } from '../common/positionUtils';
import { hasFlag } from '../common/utils';
export const matchCyrillic = /[\u0400-\u04FF]/g;
export const containsCyrillic = matcher(matchCyrillic);
const otherValid = [
'♂♀⚲⚥⚧☿♁⚨⚩⚦⚢⚣⚤', // gender symbols
'™®♥♦♣♠❥♡♢♤♧ღஐ·´°•◦✿❀◆◇◈◉◊。¥€«»,:■□—', // other
'〈〉「」『』【】《》♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼№●○◌★☆✰✦✧▪▫・', // other 2
'\u1160\u3000\u3164', // spaces (replaced later)
].join('').split('').reduce((set, c) => (set.add(c.charCodeAt(0)), set), new Set<number>());
export function isValid(c: number): boolean {
return (c >= 0x0020 && c <= 0x007e) // latin
|| (c >= 0x00a0 && c <= 0x00ff) // latin 1 supplement
|| (c >= 0x0100 && c <= 0x017F) // Latin Extended-A
|| (c >= 0x0180 && c <= 0x024F) // Latin Extended-B
|| (c >= 0x1e00 && c <= 0x1eff) // Latin Extended Additional
|| (c >= 0x0370 && c <= 0x03FF) // Greek and Coptic
|| (c >= 0x0400 && c <= 0x0481) || (c >= 0x048A && c <= 0x04FF) // cyrillic
|| (c >= 0x3041 && c <= 0x3096) // hiragana
|| (c >= 0x30A0 && c <= 0x30FF) // hatakana
|| (c >= 0x3400 && c <= 0x4DB5) || (c >= 0x4E00 && c <= 0x9FCB) || (c >= 0xF900 && c <= 0xFA6A) // kanji
|| (c >= 0x2F00 && c <= 0x2FDF) // Kangxi Radicals
|| (c >= 0x3000 && c <= 0x302D) // CJK Symbols and Punctuation
|| (c >= 0x1D00 && c <= 0x1D7F) // Phonetic Extensions
|| (c >= 0x0250 && c <= 0x02AF) // IPA Extensions
|| (c >= 0xA720 && c <= 0xA7FF) // Latin Extended-D
|| (c >= 0x0E00 && c <= 0x0E7F) // Thai
|| (c >= 0xff01 && c <= 0xff5e) // Romaji (replaced later)
|| (c >= 0x2200 && c <= 0x22FF) // Mathematical Operators
|| (c >= 0x25A0 && c <= 0x25FF) // Geometric Shapes
|| (c >= 0x2600 && c <= 0x26ff) || (c >= 0x2700 && c <= 0x27bf) || (c >= 0x2b00 && c <= 0x2bef) // emoji
|| (c >= 0x1f600 && c <= 0x1f64f) || (c >= 0x1f680 && c <= 0x1f6f6) || (c >= 0x1f300 && c <= 0x1f5ff) // emoji
|| (c >= 0x231a && c <= 0x231b) || (c >= 0x23e9 && c <= 0x23fa) // emoji
|| (c >= 0x1f900 && c <= 0x1f9ff) // Supplemental Symbols and Pictographs
|| otherValid.has(c) // other symbols
;
}
export function isValid2(c: number): boolean {
return (c >= 0x2b0 && c <= 0x2ff) // Spacing Modifier Letters
|| (c >= 0x531 && c <= 0x556) || (c >= 0x559 && c <= 0x55f) || (c >= 0x561 && c <= 0x587)
|| (c >= 0x589 && c <= 0x58a) || (c >= 0x58c && c <= 0x58f) // Armenian
|| (c >= 0x591 && c <= 0x5c7) || (c >= 0x5d0 && c <= 0x5ea) || (c >= 0x5f0 && c <= 0x5f4) // Hebrew
|| (c >= 0x600 && c <= 0x6ff) // Arabic
|| (c >= 0x7c0 && c <= 0x7fa) // NKo
|| (c >= 0x900 && c <= 0x97f) // Devanagari
|| (c === 0xb90) || (c === 0xb9c) // Tamil
|| (c >= 0xc85 && c <= 0xc8c) || (c >= 0xc8e && c <= 0xc90) || (c >= 0xc91 && c <= 0xca8)
|| (c >= 0xcaa && c <= 0xcb3) || (c >= 0xcb5 && c <= 0xcb9) || (c >= 0xce6 && c <= 0xcef) // Kannada
|| (c >= 0x10a0 && c <= 0x10c5) || (c === 0x10c7) || (c === 0x10cd) || (c >= 0x10d0 && c <= 0x10ff) // Georgian
|| (c >= 0x1100 && c <= 0x11ff) || (c >= 0x3130 && c <= 0x318f) || (c >= 0xac00 && c <= 0xd7af) // Hangul
|| (c >= 0x1400 && c <= 0x167f) // Unified Canadian Aboriginal Syllabics
|| (c >= 0x2010 && c <= 0x2027) || (c >= 0x2030 && c <= 0x205e) // General Punctuation
|| (c >= 0x20a0 && c <= 0x20bf) // Currency Symbols
|| (c >= 0x2100 && c <= 0x214f) // Letterlike Symbols
|| (c >= 0x2150 && c <= 0x218b) // Number Forms
|| (c >= 0x2300 && c <= 0x239a) || (c >= 0x23b4 && c <= 0x23fa) // Miscellaneous Technical
|| (c >= 0x2500 && c <= 0x257f) // Box Drawing
|| (c >= 0x2800 && c <= 0x28ff) // Braille Patterns
|| (c >= 0x3000 && c <= 0x303f) // CJK Symbols and Punctuation
|| (c >= 0x3105 && c <= 0x312d) // Bopomofo
|| (c >= 0xfe30 && c <= 0xfe4f) // CJK Compatibility Forms
|| (c >= 0xff01 && c <= 0xffef) // Halfwidth and Fullwidth Forms
// || (c >= 0x1f170 && c < 0x1f189) // Enclosed Alphanumeric Supplement [a-z]
|| (c >= 0x1f000 && c <= 0x1f02b) // Mahjong Tiles
|| (c >= 0x1f0a0 && c <= 0x1f0ae) || (c >= 0x1f0b1 && c <= 0x1f0bf) || (c >= 0x1f0c1 && c <= 0x1f0cf)
|| (c >= 0x1f0d1 && c <= 0x1f0df) || (c >= 0x1f0e0 && c <= 0x1f0f5) // Playing Cards
|| (c >= 0x1f1e6 && c <= 0x1f1ff) // Enclosed Alphanumeric Supplement (regional indicators)
;
}
function isInvalid(c: number): boolean {
return c === 0x1f595 // middle finger emoji
|| c === 0x00ad // soft hyphen
;
}
function isValidForName(c: number): boolean {
return isValid(c) && !isInvalid(c);
}
function isValidForMessage(c: number): boolean {
return (isValid(c) || isValid2(c)) && !isInvalid(c);
}
export const matchRomaji = /[\uff01-\uff5e]/g;
const matchOtherWhitespace = /[\u1160\u2800\u3000\u3164\uffa0]+/g;
export function replaceRomaji(match: string): string {
return String.fromCharCode(match.charCodeAt(0) - 0xfee0);
}
export function cleanName(name: string | undefined): string {
return filterString(name, isValidForName)
.replace(matchOtherWhitespace, ' ') // whitespace characters
.replace(/\s+/g, ' ')
.replace(matchRomaji, replaceRomaji)
.trim();
}
export function cleanMessage(text: string | undefined): string {
return filterString(text, isValidForMessage)
.replace(matchOtherWhitespace, ' ') // whitespace characters
.replace(/[\r\n]/g, '')
.replace(matchRomaji, replaceRomaji)
.trim()
.substr(0, SAY_MAX_LENGTH);
}
export function filterString(value: string | undefined, filter: (code: number) => boolean): string {
value = value || '';
for (let i = 0; i < value.length; i++) {
let code = value.charCodeAt(i);
let size = 1;
let invalidSurrogate = false;
if (isSurrogate(code) && (i + 1) < value.length) {
const extra = value.charCodeAt(i + 1);
if (isLowSurrogate(extra)) {
code = fromSurrogate(code, extra);
i++;
size++;
} else {
invalidSurrogate = true;
}
}
if (invalidSurrogate || !filter(code)) {
i -= size;
value = value.substr(0, i + 1) + value.substr(i + size + 1);
}
}
return value;
}
export function validatePonyName(name: string | undefined): boolean {
return !!name && !!name.length && name.length <= PLAYER_NAME_MAX_LENGTH && !/^[.,_-]+$/.test(name);
}
export function toSocialSiteInfo({ id, name, url, provider }: SocialSite): SocialSiteInfo {
const oauth = oauthProviders.find(p => p.id === provider);
return {
id,
name,
url,
icon: oauth && oauth.id,
color: oauth && oauth.color,
};
}
function isMultipleMatch(message: string, last: string): boolean {
const minMessageLength = 4;
if (message.length >= minMessageLength && last.length >= minMessageLength) {
let current = last;
while (current.length < message.length) {
current += last;
}
return message === current.substr(0, SAY_MAX_LENGTH);
} else {
return false;
}
}
function checkTrailing(message: string, last: string) {
return message.indexOf(last) === 0 && (message.length - last.length) < 3;
}
function isTrailingMatch(message: string, last: string) {
const minMessageLength = 5;
if (message.length > last.length && last.length > minMessageLength) {
return checkTrailing(message, last);
} else if (message.length < last.length && message.length > minMessageLength) {
return checkTrailing(last, message);
} else {
return false;
}
}
export function isSpamMessage(message: string, lastMessages: string[]): boolean {
if (!/^\//.test(message) && lastMessages.length) {
return lastMessages.some(last => message === last || isMultipleMatch(message, last) || isTrailingMatch(message, last));
} else {
return false;
}
}
export function getSaysTime(message: string): number {
return SAYS_TIME_MIN + clamp(message.length / SAY_MAX_LENGTH, 0, 1) * (SAYS_TIME_MAX - SAYS_TIME_MIN);
}
export function createExpression(
right: Eye, left: Eye, muzzle: Muzzle, rightIris = Iris.Forward, leftIris = Iris.Forward, extra = ExpressionExtra.None
): Expression {
return { right, left, muzzle, rightIris, leftIris, extra };
}
export const isAndroidBrowser = (() => {
const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent;
// Android browser
// Mozilla/5.0 (Linux; U; Android 4.4.2; es-ar; LG-D375AR Build/KOT49I)
// AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36
if (/Android /.test(ua) && /AppleWebKit/.test(ua) && (!/chrome/i.test(ua) || /Chrome\/30\./.test(ua))) {
return true;
}
return false;
})();
/* istanbul ignore next */
export const isBrowserOutdated = (() => {
const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent;
// Safari <= 8
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)
// AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25
const safari = /Version\/(\d+)\.[0-9.]+ Safari/.exec(ua);
if (safari && parseInt(safari[1], 10) <= 8) {
return true;
}
// Android browser
// Mozilla/5.0 (Linux; U; Android 4.4.2; es-ar; LG-D375AR Build/KOT49I)
// AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.1599.103 Mobile Safari/537.36
if (isAndroidBrowser) {
return true;
}
if (!supportsLetAndConst()) {
return true;
}
return false;
})();
export function getLocale() {
return (navigator.languages ? navigator.languages[0] : navigator.language) || 'en-US';
}
/* istanbul ignore next */
export function isLanguage(lang: string) {
const languages = navigator.languages || [navigator.language];
return languages.some(l => l === lang);
}
/* istanbul ignore next */
export function sortServersForRussian(a: ServerInfo, b: ServerInfo) {
if (a.flag === 'ru' && a.flag !== b.flag) {
return -1;
}
if (b.flag === 'ru' && a.flag !== b.flag) {
return 1;
}
return a.id.localeCompare(b.id);
}
export function readFileAsText(file: File) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e: any) => resolve(e.target && e.target.result || '');
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsText(file);
});
}
/* istanbul ignore next */
export function isFileSaverSupported() {
try {
return !!new Blob;
} catch {
return false;
}
}
export let isInIncognitoMode = false;
export function setIsIncognitoMode(value: boolean) {
isInIncognitoMode = value;
}
/* istanbul ignore next */
function checkIncognitoMode(wnd: any) {
if (!wnd || !wnd.chrome)
return;
const fs = wnd.RequestFileSystem || wnd.webkitRequestFileSystem;
if (!fs)
return;
fs(wnd.TEMPORARY, 100, () => { }, () => isInIncognitoMode = true);
}
let focused = true;
/* istanbul ignore next */
export function isFocused() {
return focused;
}
/* istanbul ignore next */
if (typeof window !== 'undefined') {
checkIncognitoMode(window);
window.addEventListener('focus', () => focused = true);
window.addEventListener('blur', () => focused = false);
}
/* istanbul ignore next */
export function isStandalone() {
return !!window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone === true; // safari
}
/* istanbul ignore next */
export function supportsLetAndConst() {
try {
return (new Function('let x = true; return x;'))();
} catch {
return false;
}
}
/* istanbul ignore next */
export function registerServiceWorker(url: string, onUpdate: () => void) {
try {
if ('serviceWorker' in navigator && typeof navigator.serviceWorker.register === 'function') {
let hadWorker = false;
navigator.serviceWorker.register(url)
.then(worker => {
hadWorker = !!worker.active;
worker.addEventListener('updatefound', () => {
if (hadWorker) {
onUpdate();
}
});
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (hadWorker) {
location.reload();
}
});
}
} catch (e) {
console.error(e);
}
}
/* istanbul ignore next */
export function unregisterServiceWorker() {
if ('serviceWorker' in navigator && typeof navigator.serviceWorker.getRegistrations === 'function') {
return navigator.serviceWorker.getRegistrations()
.then(registrations => {
for (const registration of registrations) {
registration.unregister();
}
});
} else {
return Promise.resolve();
}
}
/* istanbul ignore next */
export function attachDebugMethod(name: string, method: any) {
if (typeof window !== 'undefined') {
(window as any)[name] = method;
}
}
/* istanbul ignore next */
export function updateRangeIndicator(range: number | undefined, { player, scale, camera }: PonyTownGame) {
const e = document.getElementById('range-indicator')!;
if (player && !isChatlogRangeUnlimited(range)) {
const x = (toScreenX(player.x) - camera.x) * scale;
const y = (toScreenY(player.y) - camera.actualY) * scale;
const w = toScreenX(range!) * scale * 2;
const h = toScreenY(range!) * scale * 2;
e.style.width = `${w}px`;
e.style.height = `${h}px`;
e.style.left = `${-w / 2}px`;
e.style.top = `${-h / 2}px`;
e.style.transform = `translate3d(${x}px, ${y}px, 0)`;
e.style.display = 'block';
} else {
e.style.display = 'none';
}
}
/* istanbul ignore next */
export function checkIframeKey(iframeId: string, expectedKey: string) {
try {
const iframe = document.getElementById(iframeId) as HTMLIFrameElement;
const doc = iframe && iframe.contentWindow && iframe.contentWindow.document;
const key = doc && doc.body && doc.body.getAttribute('data-key');
return key === expectedKey;
} catch (e) {
if (DEVELOPMENT) {
console.error(e);
}
return false;
}
}
let flags: ServerFeatureFlags = {};
export const featureFlagsChanged = new Subject<ServerFeatureFlags>();
export function initFeatureFlags(newFlags: ServerFeatureFlags) {
flags = newFlags;
featureFlagsChanged.next(newFlags);
}
export function hasFeatureFlag(flag: keyof ServerFeatureFlags) {
return !!flags[flag];
}
export function hardReload() {
unregisterServiceWorker()
.then(() => location.reload(true));
}
const LOGGING = false;
let logger = (_: string) => { };
export function initLogger(newLogger: (message: string) => void) {
if (LOGGING) {
logger = newLogger;
}
}
export function log(message: string) {
if (LOGGING) {
logger(message);
}
}
export function isSupporterOrPastSupporter(account: AccountData | undefined) {
return !!account && (!!account.supporter || hasFlag(account.flags, AccountDataFlags.PastSupporter));
}
export function supporterTitle(account: AccountData | undefined) {
if (account && account.supporter) {
return `Supporter Tier ${account.supporter}`;
} else if (account && hasFlag(account.flags, AccountDataFlags.PastSupporter)) {
return 'Past supporter';
} else {
return '';
}
}
export function supporterClass(account: AccountData | undefined) {
if (account && account.supporter) {
return `supporter-${account.supporter}`;
} else if (account && hasFlag(account.flags, AccountDataFlags.PastSupporter)) {
return 'supporter-past';
} else {
return 'd-none';
}
}
export function supporterRewards(account: AccountData | undefined) {
if (account && account.supporter) {
return SUPPORTER_REWARDS[account.supporter];
} else if (account && hasFlag(account.flags, AccountDataFlags.PastSupporter)) {
return PAST_SUPPORTER_REWARDS;
} else {
return SUPPORTER_REWARDS[0];
}
}
+75
View File
@@ -0,0 +1,75 @@
export interface Credit {
name: string;
title: string;
avatarIndex: number;
links: string[];
}
export interface Contributor {
name: string;
links?: string[];
}
export interface Contributors {
group: string;
contributors: Contributor[];
}
export const CREDITS: Credit[] = [
// example:
// {
// name: 'Your name',
// title: 'Your role on the team',
// avatarIndex: 0, // place of the avatar in /assets/images/avatars.jpg
// links: ['https://twitter.com/your_twitter_handle'],
// },
];
export const CONTRIBUTORS: Contributors[] = [
{
group: 'Artists & Animators',
contributors: [
{ name: 'Shino', links: ['https://www.deviantart.com/shinodage'] },
{ name: 'ChiraChan', links: ['https://www.deviantart.com/chiramii-chan', 'https://chirachan-art.tumblr.com/'] },
{ name: 'Goodly', links: ['https://www.deviantart.com/goodlyay'] },
{ name: 'TioRafaJP', links: ['https://www.deviantart.com/tiorafajp', 'https://www.youtube.com/user/RafaelJP2'] },
{ name: 'ShareMyShipment', links: ['https://www.deviantart.com/sharemyshipment'] },
{ name: 'Velenor', links: ['https://www.deviantart.com/velenor'] },
{ name: 'OtakuAP', links: ['https://www.deviantart.com/otakuap'] },
],
},
{
group: 'Programmers & Artists',
contributors: [
{ name: 'CyberPon3', links: ['https://www.deviantart.com/cyberpon3'] },
],
},
{
group: 'Artists',
contributors: [
{ name: 'Disastral' },
{ name: 'Meno', links: ['https://www.deviantart.com/menojar'] },
{ name: 'Paulpeoples', links: ['https://www.deviantart.com/paulpeopless'] },
{ name: 'Velvet-Frost', links: ['https://www.deviantart.com/velvet-frost'] },
{ name: 'Jet7Wave', links: ['https://www.deviantart.com/jetwave'] },
{ name: 'Lalieri', links: ['https://lalieri.tumblr.com/'] },
{ name: 'Ruef-bae', links: ['https://www.deviantart.com/ruef-bae'] },
{ name: 'Alchemist3rd' },
{ name: 'Firecracker' },
{ name: 'ZippySqrl', links: ['https://www.deviantart.com/zippysqrl'] },
{ name: 'Karnel333' },
{ name: 'Wellfugzee' },
{ name: 'ScribblesHeart', links: ['https://www.deviantart.com/scribblesdesu'] },
{ name: 'dsp2003', links: ['https://dsp2003.tumblr.com/', 'http://www.deviantart.com/dsp2003'] },
{ name: 'MysticBlare', links: ['https://twitter.com/MysticBlare'] },
{ name: 'Towmacow Waffles', links: ['https://www.deviantart.com/towmacowwaffles'] },
{ name: 'OrchidPony', links: ['https://www.deviantart.com/orchidpony'] },
{ name: 'Cherry Cerise', links: ['https://www.deviantart.com/cherryceriseart'] },
{ name: 'Radio' },
{ name: 'Ultimate Fluff' },
{ name: 'SC', links: ['https://0somecunt0.tumblr.com/tagged/sfw'] },
{ name: 'SailorDolpin', links: ['https://vk.com/id324582699'] },
{ name: 'Deeraw', links: ['https://www.deviantart.com/deerdraw', 'https://twitter.com/TheOnlyDeeraw'] },
],
},
];
+73
View File
@@ -0,0 +1,73 @@
import { toByteArray } from 'base64-js';
import { ClientOptions, createBinaryReader, readObject } from 'ag-sockets/dist/browser';
import { OAuthProvider } from '../common/interfaces';
/* istanbul ignore next */
function attr(name: string): string | undefined {
return typeof document !== 'undefined' ? (document.body.getAttribute(name) || undefined) : undefined;
}
/* istanbul ignore next */
function data(id: string): string | undefined {
const element = typeof document !== 'undefined' ? document.getElementById(id) : undefined;
return element ? element.innerHTML : undefined;
}
function json<T>(id: string, def: string): T {
return JSON.parse(data(id) || def);
}
export let isMobile = false;
export const sw = attr('data-sw') === 'true';
export const host = attr('data-host')!;
export const local = attr('data-local') === 'true';
export const token = attr('data-token');
export const version = attr('data-version');
export const isPublic = attr('data-public') === 'true';
export const supporterLink = attr('data-supporter-link');
export const twitterLink = attr('data-twitter-link');
export const contactEmail = attr('data-email');
export const copyrightName = attr('data-copyright');
/* istanbul ignore next */
export const oauthProviders = json<OAuthProvider[]>('oauth-providers', '[]')
.map(a => <OAuthProvider>{ ...a, url: `/auth/${a.id}` });
/* istanbul ignore next */
export const signUpProviders = oauthProviders.filter(i => !i.connectOnly);
/* istanbul ignore next */
export const signInProviders = oauthProviders.filter(i => i.connectOnly);
/* istanbul ignore next */
export function socketOptions(): ClientOptions {
const options = data('socket-options');
if (options) {
const buffer = toByteArray(options);
const reader = createBinaryReader(buffer);
return readObject(reader);
} else {
throw new Error('Missing socket options');
}
}
/* istanbul ignore next */
function setMobile() {
isMobile = true;
window.removeEventListener('touchstart', setMobile);
document.body.classList.add('is-mobile');
}
/* istanbul ignore next */
if (typeof window !== 'undefined') {
if (!/windows/i.test(navigator.userAgent)) {
window.addEventListener('touchstart', setMobile);
}
if (/Trident/.test(navigator.userAgent)) {
document.body.classList.add('is-msie');
}
if (/YaBrowser/.test(navigator.userAgent)) {
document.body.classList.add('is-yandex');
}
}
+297
View File
@@ -0,0 +1,297 @@
import {
Entity, DrawOptions, Camera, PaletteSpriteBatch, SpriteBatch, TileSets, Engine, Pony, WorldMap, EntityState
} from '../common/interfaces';
import { isBoundsVisible } from '../common/camera';
import { drawBounds, drawPixelText, drawBoundsOutline, drawOutlineRect, drawWorldBounds } from '../graphics/graphicsUtils';
import { ORANGE, BLUE, PURPLE, BLACK, RED, WHITE, CYAN, HOTPINK, GRAY } from '../common/colors';
import { toScreenX, toScreenY, toWorldX, toWorldY } from '../common/positionUtils';
import { tileWidth, tileHeight, PONY_TYPE, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from '../common/constants';
import { forEachRegion, getAnyBounds, getRegion, isInWaterAt } from '../common/worldMap';
import { drawPonyEntity, drawPonyEntityLight, drawPonyEntityLightSprite } from '../common/pony';
import { getInteractBounds, sortEntities, isHidden, getSitOnBounds } from '../common/entityUtils';
import { drawTiles, drawTilesNew, drawTilesDebugInfo } from './tileUtils';
import { withAlphaFloat } from '../common/color';
import { timeStart, timeEnd } from './timing';
const SELECTED_ENTITY_BOUNDS = withAlphaFloat(ORANGE, 0.5);
function drawEntities(batch: PaletteSpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) {
const drawHidden = options.drawHidden;
let entitiesDrawn = 0;
for (const entity of entities) {
if ((!isHidden(entity) || drawHidden) && isBoundsVisible(camera, entity.bounds, entity.x, entity.y)) {
if (entity.type === PONY_TYPE) {
drawPonyEntity(batch, entity as Pony, options);
entitiesDrawn++;
} else if (entity.draw !== undefined) {
entity.draw(batch, options);
entitiesDrawn++;
}
} else {
if (entity.type === PONY_TYPE) {
const pony = entity as Pony;
if (pony.batch !== undefined) {
batch.releaseBatch(pony.batch);
pony.batch = undefined;
}
}
}
}
return entitiesDrawn;
}
export function drawEntityLights(batch: SpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) {
const drawHidden = options.drawHidden;
for (const entity of entities) {
if (DEVELOPMENT && (entity.type !== PONY_TYPE && !entity.drawLight)) {
console.error('Cannot draw entity light', entity);
}
if ((!isHidden(entity) || drawHidden) && isBoundsVisible(camera, entity.lightBounds, entity.x, entity.y)) {
if (entity.type === PONY_TYPE) {
drawPonyEntityLight(batch, entity as Pony, options);
} else {
entity.drawLight!(batch, options);
}
}
}
}
export function drawEntityLightSprites(batch: SpriteBatch, entities: Entity[], camera: Camera, options: DrawOptions) {
const drawHidden = options.drawHidden;
for (const entity of entities) {
if (DEVELOPMENT && (entity.type !== PONY_TYPE && !entity.drawLightSprite)) {
console.error('Cannot draw entity light sprite', entity);
}
if ((!isHidden(entity) || drawHidden) && isBoundsVisible(camera, entity.lightSpriteBounds, entity.x, entity.y)) {
if (entity.type === PONY_TYPE) {
drawPonyEntityLightSprite(batch, entity as Pony, options);
} else {
entity.drawLightSprite!(batch, options);
}
}
}
}
export function hasDrawLight(entity: Entity) {
if (entity.type === PONY_TYPE) {
const pony = entity as Pony;
return (pony.ponyState.holding !== undefined && pony.ponyState.holding.drawLight !== undefined) ||
((pony.state & EntityState.Magic) !== 0);
} else {
return entity.drawLight !== undefined;
}
}
export function hasLightSprite(entity: Entity) {
if (entity.type === PONY_TYPE) {
const pony = entity as Pony;
return (pony.ponyState.holding !== undefined && pony.ponyState.holding.drawLightSprite !== undefined);
} else {
return entity.drawLightSprite !== undefined;
}
}
export function drawMap(
batch: PaletteSpriteBatch, map: WorldMap, camera: Camera, player: Pony, options: DrawOptions,
tileSets: TileSets, selectedEntities: Entity[],
) {
TIMING && timeStart('forEachRegion');
if (BETA && options.engine === Engine.Whiteness) {
batch.drawRect(WHITE, 0, 0, toScreenX(map.width), toScreenY(map.height));
} else if (BETA && options.engine === Engine.LayeredTiles) {
forEachRegion(map, region => drawTilesNew(batch, region, camera, map, tileSets, options));
} else {
forEachRegion(map, region => drawTiles(batch, region, camera, map, tileSets, options));
}
TIMING && timeEnd();
TIMING && timeStart('sortEntities');
sortEntities(map.entitiesDrawable);
TIMING && timeEnd();
TIMING && timeStart('drawEntities');
const entitiesDrawn = drawEntities(batch, map.entitiesDrawable, camera, options);
TIMING && timeEnd();
if (BETA || TOOLS) {
forEachRegion(map, region => drawTilesDebugInfo(batch, region, camera, options));
}
if (BETA && options.debug.showHelpers) {
drawDebugHelpers(batch, map.entities, options);
}
if (BETA) {
for (const entity of selectedEntities) {
const bounds = getAnyBounds(entity);
drawBoundsOutline(batch, entity, bounds, SELECTED_ENTITY_BOUNDS, 2);
}
}
if (BETA && options.debug.showHelpers) {
drawOutlineRect(batch, PURPLE, getInteractBounds(player));
drawOutlineRect(batch, 0xff000066, getSitOnBounds(player));
}
if (BETA && options.showColliderMap) {
drawDebugCollider(batch, map, camera);
batch.drawRect(PURPLE, toScreenX(player.x) - 1, toScreenY(player.y), 3, 1);
batch.drawRect(PURPLE, toScreenX(player.x), toScreenY(player.y) - 1, 1, 3);
}
if (BETA && options.showHeightmap) {
drawDebugInWater(batch, map, camera);
}
return entitiesDrawn;
}
// debug
function drawDebugHelpers(batch: PaletteSpriteBatch, entities: Entity[], options: DrawOptions) {
const textColor = 0x000000b2;
const show = options.debug;
for (const e of entities) {
batch.globalAlpha = 0.3;
show.bounds && drawBounds(batch, e, e.bounds, ORANGE);
show.cover && drawBounds(batch, e, e.coverBounds, BLUE);
show.interact && drawBounds(batch, e, e.interactBounds, PURPLE);
show.trigger && drawWorldBounds(batch, e, e.triggerBounds, CYAN);
if (show.collider) {
batch.globalAlpha = 0.5;
const x = Math.floor(e.x * tileWidth);
const y = Math.floor(e.y * tileHeight);
if (e.colliders !== undefined) {
for (const collider of e.colliders) {
const x1 = x + collider.x;
const x2 = x + collider.x + collider.w;
const y1 = y + collider.y;
const y2 = y + collider.y + collider.h;
batch.drawRect(collider.tall ? RED : HOTPINK, x1, y1, x2 - x1, y2 - y1);
}
}
}
batch.globalAlpha = 1;
batch.drawRect(BLACK, toScreenX(e.x), toScreenY(e.y), 1, 1); // anchor
if (show.id) {
drawPixelText(batch, toScreenX(e.x) + 2, toScreenY(e.y) + 2, textColor, e.id.toFixed());
}
}
}
function drawDebugInWater(batch: PaletteSpriteBatch, map: WorldMap, camera: Camera) {
const color = withAlphaFloat(ORANGE, 0.4);
forEachRegion(map, region => {
const sx = toScreenX(region.x * REGION_SIZE);
const sy = toScreenY(region.y * REGION_SIZE);
const w = REGION_WIDTH;
const h = REGION_HEIGHT;
const cameraLeft = camera.x;
const cameraRight = camera.x + camera.w;
const cameraTop = camera.actualY;
const cameraBottom = camera.actualY + camera.h;
if (sx > cameraRight || sy > cameraBottom || (sx + w) < cameraLeft || (sy + h) < cameraTop)
return;
for (let y = 0; y < h; y++) {
if ((sy + y + 1) < cameraTop || (sy + y) > cameraBottom)
continue;
for (let x = 0; x < w; x++) {
if ((sx + x + 1) < cameraLeft || (sx + x) > cameraRight)
continue;
const tx = x;
while (isInWaterAt(map, toWorldX(sx + x + 0.5), toWorldY(sy + y + 0.5)) && x < w) {
x++;
}
if (x > tx) {
batch.drawRect(color, sx + tx, sy + y, x - tx, 1);
}
}
}
});
}
function drawDebugCollider(batch: PaletteSpriteBatch, map: WorldMap, camera: Camera) {
const color = withAlphaFloat(PURPLE, 0.4);
forEachRegion(map, ({ x, y, collider }) => {
const sx = toScreenX(x * REGION_SIZE);
const sy = toScreenY(y * REGION_SIZE);
const w = REGION_WIDTH;
const h = REGION_HEIGHT;
const cameraLeft = camera.x;
const cameraRight = camera.x + camera.w;
const cameraTop = camera.actualY;
const cameraBottom = camera.actualY + camera.h;
if (sx > cameraRight || sy > cameraBottom || (sx + w) < cameraLeft || (sy + h) < cameraTop)
return;
for (let y = 0; y < h; y++) {
if ((sy + y + 1) < cameraTop || (sy + y) > cameraBottom)
continue;
for (let x = 0; x < w; x++) {
if ((sx + x + 1) < cameraLeft || (sx + x) > cameraRight)
continue;
const tx = x;
while (collider[x + y * w] !== 0 && x < w) {
x++;
}
if (x > tx) {
batch.drawRect(color, sx + tx, sy + y, x - tx, 1);
}
}
}
});
}
export function drawDebugRegions(batch: SpriteBatch, map: WorldMap, player: Pony, { w, h }: Camera) {
const rw = 10;
const rh = 8;
const width = rw * map.regionsX;
const height = rh * map.regionsY;
const x = w - width - 10;
const y = h - height - 30;
for (let i = 0; i < map.regionsY; i++) {
for (let j = 0; j < map.regionsX; j++) {
if (getRegion(map, j, i)) {
const inside = j === Math.floor(player.x / REGION_SIZE) && i === Math.floor(player.y / REGION_SIZE);
batch.drawRect(inside ? ORANGE : RED, x + rw * j, y + rh * i, rw, rh);
}
}
}
for (let i = 0; i <= map.regionsY; i++) {
batch.drawRect(GRAY, x, y + rh * i, width + 1, 1);
}
for (let i = 0; i <= map.regionsX; i++) {
batch.drawRect(GRAY, x + rw * i, y, 1, height);
}
}
+194
View File
@@ -0,0 +1,194 @@
import { escape } from 'lodash';
import { Sprite } from '../common/interfaces';
import { canvasToSource } from './canvasUtils';
import { drawCanvas } from '../graphics/contextSpriteBatch';
import { WHITE } from '../common/colors';
import { normalSpriteSheet } from '../generated/sprites';
import { includes } from '../common/utils';
export interface Emoji {
names: string[];
symbol: string;
}
export const emojis: Emoji[] = [
// faces
['🙂', 'face', 'tiny', 'tinyface', 'slight_smile'],
['😵', 'derp', 'dizzy_face'],
['😠', 'angry'],
['😐', 'neutral', 'neutral_face'],
['😑', 'expressionless'],
['😆', 'laughing'],
['😍', 'heart_eyes'],
['😟', 'worried'],
['🤔', 'thinking'],
['🙃', 'upside_down'],
['😈', 'evil', 'smiling_imp'],
['👿', 'imp', 'angry_evil'],
['👃', 'nose', 'c'],
// cat faces
['🐱', 'cat'],
['😺', 'smiley_cat'],
['😸', 'smile_cat'],
['😹', 'joy_cat'],
['😻', 'heart_eyes_cat'],
['😼', 'smirk_cat'],
['😽', 'kissing_cat'],
['🙀', 'scream_cat'],
['😿', 'cryingcat', 'crying_cat_face'],
['😾', 'pouting_cat'],
// hearts
['❤', 'heart'],
['💙', 'blue_heart', 'meno'],
['💚', 'green_heart', 'chira'],
['💛', 'yellow_heart'],
['💜', 'purple_heart'],
['🖤', 'black_heart', 'shino'],
['💔', 'broken_heart'],
['💖', 'sparkling_heart'],
['💗', 'heartpulse'],
['💕', 'two_hearts'],
// food / objects
['🥌', 'rock', 'stone'],
['🍕', 'pizza'],
['🍎', 'apple'],
['🍏', 'gapple', 'green_apple'],
['🍊', 'orange', 'tangerine'],
['🍐', 'pear'],
['🥭', 'mango'],
['🥕', 'carrot'],
['🍇', 'grapes'],
['🍌', 'banana'],
['⛏', 'pick'],
['🥚', 'egg'],
['💮', 'flower', 'white_flower'],
['🌸', 'cherry_blossom'],
['🍬', 'candy'],
['🍡', 'candy_cane'],
['🍭', 'lollipop'],
['⭐', 'star'],
['🌟', 'star2'],
['🌠', 'shooting_star'],
['⚡', 'zap'],
['❄', 'snow', 'snowflake'],
['⛄', 'snowpony', 'snowman'],
['🏀', 'pumpkin'],
['🎃', 'jacko', 'jack_o_lantern'],
['🌲', 'evergreen_tree', 'pinetree'],
['🎄', 'christmas_tree'],
['🕯', 'candle'],
['🎅', 'santa_hat', 'santa_claus'],
['💐', 'holly'],
['🌿', 'mistletoe'],
['🎲', 'die', 'dice', 'game_die'],
['✨', 'sparkles'],
['🎁', 'gift', 'present'],
['🔥', 'fire'],
['🎵', 'musical_note'],
['🎶', 'notes'],
['🌈', 'rainbow'],
['🐾', 'feet', 'paw', 'paws'],
['👑', 'crown'],
['💎', 'gem'],
['☘', 'shamrock', 'clover'],
['🍀', 'four_leaf_clover'],
['🍪', 'cookie'],
// animals
['🦋', 'butterfly'],
['🦇', 'bat'],
['🕷', 'spider'],
['👻', 'ghost'],
['🐈', 'cat2'],
// other
['™', 'tm'],
['♂', 'male'],
['♀', 'female'],
['⚧', 'trans', 'transgender'],
].map(createEmoji);
export const emojiMap = new Map<string, string>();
export const emojiNames = emojis.slice().sort().map(e => `:${e.names[0]}:`);
emojis.forEach(e => e.names.forEach(name => emojiMap.set(`:${name}:`, e.symbol)));
export function findEmoji(name: string): Emoji | undefined {
return emojis.find(e => name === e.symbol || includes(e.names, name));
}
export function replaceEmojis(text: string | undefined): string {
return (text || '').replace(/:[a-z0-9_]+:/ig, match => emojiMap.get(match) || match);
}
function createEmoji([symbol, ...names]: string[]): Emoji {
return { symbol, names: [...names, ...names.filter(n => /_/.test(n)).map(n => n.replace(/_/g, ''))] };
}
const emojiImages = new Map<Sprite, string>();
const emojiImagePromises = new Map<Sprite, Promise<string>>();
export function getEmojiImageAsync(sprite: Sprite, callback: (str: string) => void) {
const src = emojiImages.get(sprite);
if (src) {
callback(src);
return;
}
const promise = emojiImagePromises.get(sprite);
if (promise) {
promise.then(callback);
return;
}
const width = sprite.w + sprite.ox;
// const height = sprite.h + sprite.oy;
const canvas = drawCanvas(width, 10, normalSpriteSheet, undefined, batch => batch.drawSprite(sprite, WHITE, 0, 0));
const newPromise = canvasToSource(canvas);
emojiImagePromises.set(sprite, newPromise);
newPromise
.then(src => {
emojiImages.set(sprite, src);
emojiImagePromises.delete(sprite);
return src;
})
.then(callback);
}
const emojisRegex = new RegExp(`(${[
...emojis.map(e => e.symbol),
'♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎',
].join('|')})`, 'g');
export function splitEmojis(text: string) {
return text.split(emojisRegex);
}
export function hasEmojis(text: string) {
return emojisRegex.test(text);
}
export function nameToHTML(name: string) {
return escape(name);
}
export interface AutocompleteState {
lastEmoji?: string;
}
const names = emojiNames.slice().sort();
export function autocompleteMesssage(message: string, shift: boolean, state: AutocompleteState): string {
return message.replace(/:[a-z0-9_]+:?$/, match => {
state.lastEmoji = state.lastEmoji || match;
const matches = names.filter(e => e.indexOf(state.lastEmoji!) === 0);
const index = matches.indexOf(match);
const offset = index === -1 ? 0 : (index + matches.length + (shift ? -1 : 1)) % matches.length;
return matches[offset] || match;
});
}
+39
View File
@@ -0,0 +1,39 @@
import { SpriteFont, createSpriteFont } from '../graphics/spriteFont';
import * as sprites from '../generated/sprites';
export let font: SpriteFont;
export let fontPal: SpriteFont;
export let fontSmall: SpriteFont;
export let fontSmallPal: SpriteFont;
export let fontMono: SpriteFont;
export let fontMonoPal: SpriteFont;
export function createFonts() {
font = createSpriteFont(sprites.font, sprites.emoji, 3);
font.lineSpacing = 3;
font.letterShiftY = -2;
fontPal = createSpriteFont(sprites.fontPal, sprites.emojiPal, 3);
fontPal.lineSpacing = 3;
fontPal.letterShiftY = -2;
fontSmall = createSpriteFont(sprites.fontSmall, [], 2);
fontSmall.lineSpacing = 4;
fontSmall.letterShiftY = -2;
fontSmall.letterHeightReal += 2;
fontSmallPal = createSpriteFont(sprites.fontSmallPal, [], 2);
fontSmallPal.lineSpacing = 4;
fontSmallPal.letterShiftY = -2;
fontSmallPal.letterHeightReal += 2;
fontMono = createSpriteFont(sprites.fontMono, [], 4);
fontMono.lineSpacing = 4;
fontMono.letterShiftY = -2;
fontMono.letterHeightReal += 2;
fontMonoPal = createSpriteFont(sprites.fontMonoPal, [], 4);
fontMonoPal.lineSpacing = 4;
fontMonoPal.letterShiftY = -2;
fontMonoPal.letterHeightReal += 2;
}
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
export interface Game {
fps: number;
load(): any;
init(): void;
update(delta: number, now: number, last: number): void;
draw(): void;
}
export interface GameLoop {
started: Promise<void>;
cancel(): void;
}
let gameLoop: GameLoop | undefined = undefined;
export function startGameLoop(game: Game, onError = (e: Error) => console.error(e)): GameLoop {
let handle: any;
let backup: any;
let cancelled = false;
let last = Math.round(performance.now());
let lastFps = last;
let frames = 0;
let fps = 0;
function step(now: number, draw: boolean) {
if (draw) {
handle = requestAnimationFrame(onFrame);
}
frames++;
if ((now - lastFps) > 1000) {
fps = frames * 1000 / (now - lastFps);
frames = 0;
lastFps = now;
}
try {
game.fps = fps;
game.update((now - last) / 1000, now, last);
if (draw) {
game.draw();
}
} catch (e) {
onError(e);
}
last = now;
}
function onTimer() {
step(Math.round(performance.now()), false);
backup = setTimeout(onTimer, 1000 / 10);
}
function onFrame() {
clearTimeout(backup);
step(Math.round(performance.now()), true);
backup = setTimeout(onTimer, 1000 / 10);
}
function cancel() {
cancelAnimationFrame(handle);
clearTimeout(backup);
cancelled = true;
}
if (gameLoop) {
gameLoop.cancel();
}
const started = Promise.resolve()
.then(() => game.load())
.then(() => {
if (cancelled) {
throw new Error('Cancelled (loop)');
} else {
game.init();
handle = requestAnimationFrame(onFrame);
backup = setTimeout(onTimer, 1000 / 10);
}
});
gameLoop = { started, cancel };
return gameLoop;
}
+49
View File
@@ -0,0 +1,49 @@
import { Notification } from '../common/interfaces';
import { PonyTownGame } from './game';
import { removeById } from '../common/utils';
export function addNotification({ notifications }: PonyTownGame, notification: Notification) {
const open = notifications.length === 0;
notifications.push(notification);
setTimeout(() => {
notification.open = open;
notification.fresh = false;
}, 500);
}
export function removeNotification({ notifications }: PonyTownGame, id: number) {
const notification = removeById(notifications, id);
if (notification && notification.open && notifications.length) {
notifications[0].open = true;
}
}
export function resetGameFields(game: PonyTownGame) {
game.loaded = false;
game.placeInQueue = 0;
game.playerId = undefined;
game.playerName = undefined;
game.playerInfo = undefined;
game.playerCRC = undefined;
game.party = undefined;
game.whisperTo = undefined;
game.messageQueue = [];
game.lastWhisperFrom = undefined;
game.onPartyUpdate.next();
game.fallbackPonies.clear();
}
export function markGameAsLoaded(game: PonyTownGame) {
if (!game.loaded) {
game.loaded = true;
game.fullyLoaded = false;
setTimeout(() => game.fullyLoaded = true, 300);
}
}
export function isSelected(game: PonyTownGame, id: number) {
return game.selected && game.selected.id === id;
}
+802
View File
@@ -0,0 +1,802 @@
import { compact, escapeRegExp, repeat } from 'lodash';
import { createBinaryReader, readUint8, readUint32, readUint16 } from 'ag-sockets/dist/browser';
import { decodeString } from 'ag-sockets/dist/utf8';
import {
Entity, Region, Pony, EntityState, PonyOptions, MessageType, isNonIgnorableMessage, EntityPlayerState,
EntityOrPonyOptions, isPublicMessage, FakeEntity, Action, WorldMap, DoAction, UpdateType, DecodedUpdate,
PonyData, WorldStateFlags, FriendStatusData, FriendStatusFlags, isWhisper, isWhisperTo,
} from '../common/interfaces';
import { bitmask, setFlag, findById, distance, hasFlag, distanceXY, invalidEnum, removeItem } from '../common/utils';
import { isChatVisible } from '../common/camera';
import { createRegion, worldToRegionX, worldToRegionY } from '../common/region';
import { createAnEntity, poof, poof2 } from '../common/entities';
import { getPonyState, setPonyState, isPonyFlying, addChatBubble, isHidden } from '../common/entityUtils';
import {
isPony, createPony, setPonyExpression, updatePonyInfo, updatePonyHold, doPonyAction, hasHeadAnimation,
setHeadAnimation,
doBoopPonyAction
} from '../common/pony';
import { PonyTownGame } from './game';
import { setupPlayer, savePlayerPosition } from './sec';
import { PONY_INFO_KEY, FLY_DELAY, isChatlogRangeUnlimited, SECOND, PONY_TYPE } from '../common/constants';
import { getSaysTime, containsCyrillic } from './clientUtils';
import { dismissSays } from '../graphics/graphicsUtils';
import { decodeUpdate, readOneUpdate } from '../common/encoders/updateDecoder';
import { updateEntityVelocity } from '../common/entityUtils';
import { decodePonyInfo } from '../common/compressPony';
import { mockPaletteManager } from '../common/ponyInfo';
import { yawn, laugh, sneeze } from './ponyAnimations';
import {
findEntityById, getRegionGlobal, setTile, removeEntity, addEntity, removeEntityDirectly, setRegion,
addEntityToMapRegion, switchEntityRegion, getRegionUnsafe, addOrRemoveFromEntityList,
} from '../common/worldMap';
import { isSelected } from './gameUtils';
import { compareFriends } from '../components/services/model';
import { canCollideWith } from '../common/collision';
import { hasDrawLight, hasLightSprite } from './draw';
function log(message: string) {
if (DEVELOPMENT && !TESTS) {
console.error(message);
}
}
function handleAddEntity(game: PonyTownGame, region: Region, update: DecodedUpdate, initial: boolean) {
const {
id, type = 0, x = 0, y = 0, vx = 0, vy = 0, state = 0, playerState = 0, options = {},
name, filterName, info, crc = 0, action // , expression
} = update;
const filteredName = filterEntityName(game, name, filterName);
const entity = createEntityOrPony(game, type, id, x, y, options, crc, filteredName, info, state);
entity.id = id;
entity.x = x;
entity.y = y;
entity.vx = vx;
entity.vy = vy;
entity.playerState = playerState || EntityPlayerState.None;
addEntityToMapRegion(game.map, region, entity);
if (isPony(entity)) {
if (id === game.playerId) {
game.apply(() => setupPlayer(game, entity));
}
if (isSelected(game, id)) {
game.select(entity);
}
if (game.whisperTo && game.whisperTo.id === id) {
game.whisperTo = entity;
}
if (!initial) {
game.onPonyAddOrUpdate.next(entity);
}
}
if (action !== undefined) {
handleAction(game, id, action);
}
}
export function handleUpdateEntity(game: PonyTownGame, update: DecodedUpdate) {
const {
id, x, y, vx, vy, state, playerState, expression, options, switchRegion,
name, filterName, info, crc = 0, action
} = update;
const filteredName = filterEntityName(game, name, filterName);
const entity = findEntityByIdInGame(game, id);
if (entity) {
const isPlayer = id === game.playerId;
if (x !== undefined && y !== undefined) {
if (switchRegion) {
if (DEVELOPMENT && isPlayer && !getRegionGlobal(game.map, x, y)) {
console.error(`Switching player to unsubscribed region`);
}
switchEntityRegion(game.map, entity, x, y);
}
if (!isPlayer) {
// if (DEVELOPMENT && isPony(entity)) {
// const dx = entity.x - x;
// const dy = entity.y - y;
// console.log(`adjust x: [${num(dx)}] (${ms(dx)}) y: [${num(dy)}] (${ms(dy)})`);
// }
entity.x = x;
entity.y = y;
updateEntityVelocity(game.map, entity, vx, vy);
if (canCollideWith(entity)) {
const rx = worldToRegionX(entity.x, game.map);
const ry = worldToRegionY(entity.y, game.map);
for (let y = -1; y <= 1; y++) {
for (let x = -1; x <= 1; x++) {
const region = getRegionUnsafe(game.map, rx + x, ry + y);
if (region) {
region.colliderDirty = true;
}
}
}
}
} else if (distanceXY(entity.x, entity.y, x, y) > 8) {
log(`Fixing player position (${entity.x}, ${entity.y}) => (${x}, ${y})`);
entity.x = x;
entity.y = y;
savePlayerPosition();
}
}
if (state !== undefined) {
updateEntityStateInternal(game, entity, state);
}
if (playerState !== undefined) {
updateEntityPlayerStateInternal(game, entity, playerState);
}
if (expression !== undefined && isPony(entity)) {
setPonyExpression(entity, expression);
}
if (options != null) {
updateEntityOptionsInternal(entity, options, game);
}
if (filteredName !== undefined && !isPlayer) {
entity.name = filteredName;
}
if (info !== undefined && !isPlayer) {
const ponyInfo = bitmask(info, PONY_INFO_KEY);
if (entity.fake) {
(entity as Pony).palettePonyInfo = decodePonyInfo(ponyInfo, mockPaletteManager);
} else {
updatePonyInfoWithPoof(game, entity, ponyInfo, crc);
}
}
if (action !== undefined) {
handleAction(game, id, action);
}
applyIfSelected(game, id);
} else {
log(`handleUpdateEntity: missing entity: ${id}`);
}
}
export function handleUpdatePonies(game: PonyTownGame, ponies: PonyData[]) {
for (const [id, options = {}, name, info, playerState, nameBad] of ponies) {
const decodedName = name && decodeString(name) || undefined;
const filteredName = filterEntityName(game, decodedName, nameBad);
const decodedInfo = info ? bitmask(info, PONY_INFO_KEY) : '';
const pony = createPonyEntity(game, id, options, filteredName, decodedInfo, EntityState.None);
pony.playerState = playerState;
game.fallbackPonies.set(pony.id, pony);
}
const missing = game.party && game.party.members.filter(p => !p.pony);
if (missing && missing.length) {
game.apply(() => missing.forEach(p => p.pony = game.fallbackPonies.get(p.id)));
}
}
function createPonyEntity(
game: PonyTownGame, id: number, options: PonyOptions, name: string | undefined, info: string | Uint8Array,
state: EntityState
) {
if (!game.webgl) {
throw new Error('WebGL not initialized');
}
const pony = createPony(id, state, info, game.webgl.palettes.defaultPalette, game.paletteManager);
if (name) {
pony.name = name;
}
updateEntityOptionsInternal(pony, options, game);
// bypass name/info filtering for player pony
if (id === game.playerId) {
if (game.playerName) {
pony.name = game.playerName;
}
if (game.playerInfo) {
pony.crc = game.playerCRC;
updatePonyInfo(pony, game.playerInfo, game.applyChanges);
}
}
return pony;
}
function updateEntityStateInternal(game: PonyTownGame, entity: Entity, state: EntityState) {
if (entity === game.player) {
const right = game.rightOverride;
const headTurned = game.headTurnedOverride;
const stateOverride = game.stateOverride;
if (right !== undefined) {
state = setFlag(state, EntityState.FacingRight, right);
game.rightOverride = undefined;
}
if (headTurned !== undefined) {
state = setFlag(state, EntityState.HeadTurned, headTurned);
game.headTurnedOverride = undefined;
}
if (stateOverride !== undefined) {
if (stateOverride !== getPonyState(state)) {
state = setPonyState(state, stateOverride);
}
game.stateOverride = undefined;
}
game.onActionsUpdate.next();
}
const wasPonyFlying = isPonyFlying(entity);
const hadLight = hasDrawLight(entity);
const hadLightSprite = hasLightSprite(entity);
entity.state = state;
if (!wasPonyFlying && isPonyFlying(entity) && isPony(entity)) {
entity.inTheAirDelay = FLY_DELAY;
}
const hasLight = hasDrawLight(entity);
const hasLightSprite1 = hasLightSprite(entity);
addOrRemoveFromEntityList(game.map.entitiesLight, entity, hadLight, hasLight);
addOrRemoveFromEntityList(game.map.entitiesLightSprite, entity, hadLightSprite, hasLightSprite1);
}
function updateEntityPlayerStateInternal(game: PonyTownGame, entity: Entity, playerState: EntityPlayerState) {
if (!entity.fake && !isHidden(entity) && hasFlag(playerState, EntityPlayerState.Hidden)) {
playEffect(game, entity, poof.type);
if (isSelected(game, entity.id)) {
game.select(undefined);
}
}
entity.playerState = playerState;
}
function findEntityByIdInGame(game: PonyTownGame, id: number) {
let entity = findEntityById(game.map, id);
if (!entity && isSelected(game, id)) {
entity = game.selected;
}
return entity;
}
function applyIfSelected(game: PonyTownGame, id: number) {
if (isSelected(game, id)) {
game.applyChanges();
}
}
export function handleUpdates(game: PonyTownGame, updates: Uint8Array) {
const reader = createBinaryReader(updates);
while (reader.offset < reader.view.byteLength) {
const type = readUint8(reader) as UpdateType;
switch (type) {
case UpdateType.None:
log(`handleUpdates (none)`);
break;
case UpdateType.AddEntity: {
const update = readOneUpdate(reader)!;
const { x = 0, y = 0 } = update;
const region = getRegionGlobal(game.map, x, y);
if (region) {
handleAddEntity(game, region, update, false);
} else {
log(`handleUpdates (add): missing region at ${x} ${y}`);
}
break;
}
case UpdateType.UpdateEntity: {
const update = readOneUpdate(reader)!;
handleUpdateEntity(game, update);
break;
}
case UpdateType.RemoveEntity: {
const id = readUint32(reader);
handleRemoveEntity(game, id);
break;
}
case UpdateType.UpdateTile: {
const x = readUint16(reader);
const y = readUint16(reader);
const type = readUint8(reader);
setTile(game.map, x, y, type);
break;
}
default:
invalidEnum(type);
}
}
}
export function updatePonyInfoWithPoof(game: PonyTownGame, entity: Entity, info: string | Uint8Array, crc: number) {
const update = (pony: Pony) => {
pony.crc = crc;
updatePonyInfo(pony, info, game.applyChanges);
game.onPonyAddOrUpdate.next(pony);
};
if (entity && isPony(entity)) {
if (isHidden(entity)) {
update(entity);
} else {
playEffect(game, entity, poof2.type);
setTimeout(() => update(entity), 100);
}
}
}
export function handleRemoveEntity(game: PonyTownGame, id: number) {
const entity = findEntityById(game.map, id);
if (entity) {
removeEntity(game.map, entity);
} else {
log(`handleRemoveEntity: Missing entity: ${id}`);
}
if (id === game.playerId) {
log(`handleRemoveEntity: Removing player`);
}
if (entity && entity.type === PONY_TYPE) {
playEffect(game, entity, poof.type);
}
if (isSelected(game, id)) {
setTimeout(() => {
if (isSelected(game, id)) {
game.select(undefined);
}
}, 15 * SECOND);
}
}
function findPonyById(map: WorldMap, id: number) {
const entity = findEntityById(map, id);
return entity && isPony(entity) ? entity : undefined;
}
export function handleAction(game: PonyTownGame, id: number, action: Action) {
const pony = findPonyById(game.map, id);
if (pony) {
switch (action) {
case Action.Boop:
doBoopPonyAction(game, pony);
break;
case Action.HoldPoof:
doPonyAction(pony, DoAction.HoldPoof);
break;
case Action.Yawn:
if (!hasHeadAnimation(pony)) {
setHeadAnimation(pony, yawn);
}
break;
case Action.Laugh:
if (!hasHeadAnimation(pony)) {
setHeadAnimation(pony, laugh);
}
break;
case Action.Sneeze:
if (!hasHeadAnimation(pony)) {
setHeadAnimation(pony, sneeze);
}
break;
default:
log(`handleAction: Invalid action: ${action}`);
}
} else {
log(`handleAction: Missing entity: ${id}`);
}
}
export function playEffect(game: PonyTownGame, target: Entity, type: number) {
if (isHidden(target))
return;
try {
const entity = createAnEntity(type, 0, target.x, target.y, {}, game.paletteManager, game);
addEntity(game.map, entity);
setTimeout(() => removeEntityDirectly(game.map, entity), 1000);
} catch (e) {
DEVELOPMENT && console.error(e);
}
}
export function findEntityOrMockByAnyMeans(game: PonyTownGame, id: number) {
if (!id) {
return undefined;
}
let entity: Entity | FakeEntity | undefined = findEntityById(game.map, id);
if (!entity && game.party) {
const member = findById(game.party.members, id);
entity = member && member.pony;
}
if (!entity) {
const friend = game.model.friends && game.model.friends.find(f => f.entityId === id);
if (friend) {
entity = { fake: true, type: PONY_TYPE, id: friend.entityId, name: friend.actualName, crc: friend.crc };
}
}
if (!entity) {
entity = game.findEntityFromChatLog(id);
}
return entity;
}
export function findBestEntityByName(game: PonyTownGame, name: string): Entity | FakeEntity | undefined {
const regex = new RegExp(`^${escapeRegExp(name)}$`, 'i');
if (game.model.friends) {
for (const friend of game.model.friends) {
if (friend.online && friend.entityId && regex.test(friend.actualName)) {
return { fake: true, type: PONY_TYPE, id: friend.entityId, name: friend.actualName, crc: friend.crc };
}
}
}
let result: Entity | FakeEntity | undefined = undefined;
if (game.player) {
for (const entity of game.map.entities) {
if (entity.type === PONY_TYPE && entity.id !== game.playerId && !isHidden(entity) && entity.name && regex.test(entity.name)) {
if (!result || (distance(game.player, entity) < distance(game.player, result))) {
result = entity;
}
}
}
}
if (!result) {
result = game.findEntityFromChatLogByName(name);
}
return result;
}
export function findMatchingEntityNames(game: PonyTownGame, match: string): string[] {
const result: string[] = [];
const ids = new Set<number>();
const regex = new RegExp(`^${escapeRegExp(match)}`, 'i');
if (game.model.friends) {
for (const friend of game.model.friends) {
if (friend.online && friend.entityId && friend.actualName && regex.test(friend.actualName)) {
ids.add(friend.entityId);
result.push(friend.actualName);
}
}
}
for (const entity of game.map.entities) {
if (
entity.type === PONY_TYPE &&
entity.id !== game.playerId &&
entity.name &&
!isHidden(entity) &&
regex.test(entity.name) &&
!ids.has(entity.id)
) {
result.push(entity.name);
}
}
return result;
}
let cachedFilter: string | undefined = undefined;
let cachedRegex: RegExp | undefined = undefined;
export function containsFilteredWords(message: string, filter: string | undefined) {
if (cachedFilter !== filter) {
if (filter) {
const words = compact(filter.replace(/[,]/g, ' ').split(/[\r\n\t ]+/g).map(x => x.trim()));
cachedRegex = new RegExp(`(^| )(${words.map(escapeRegExp).join('|')})($| )`, 'i');
} else {
cachedRegex = undefined;
}
cachedFilter = filter;
}
return cachedRegex && cachedRegex.test(message);
}
export function handleSays(game: PonyTownGame, id: number, message: string, type: MessageType) {
const entity = findEntityOrMockByAnyMeans(game, id);
if (entity) {
handleSay(game, entity, message, type);
} else {
DEVELOPMENT && console.warn('incomplete say');
game.incompleteSays.push({ id, message, type, time: Date.now() });
game.send(server => server.actionParam2(Action.RequestEntityInfo, id));
}
}
function isFriendEntityId(game: PonyTownGame, id: number) {
if (game.model.friends) {
for (const friend of game.model.friends) {
if (friend.entityId === id) {
return true;
}
}
}
return false;
}
function shouldShowChatMessage(game: PonyTownGame, entity: Entity | FakeEntity, message: string, type: MessageType): boolean {
if (entity === game.player)
return true;
if (isWhisperTo(type))
return true;
if (isWhisper(type) && isFriendEntityId(game, entity.id))
return true;
if (isPublicMessage(type) && !entity.fake && !isChatVisible(game.camera, entity))
return false;
if (isNonIgnorableMessage(type))
return true;
if (game.settings.account.filterCyrillic && containsCyrillic(message))
return false;
if (game.settings.account.ignorePublicChat && isPublicMessage(type))
return false;
if (isWhisper(type) && game.settings.account.ignoreNonFriendWhispers)
return false;
if (containsFilteredWords(message, game.settings.account.filterWords))
return false;
return true;
}
function isChatInRange(entity: Entity, player: Entity | undefined, range: number | undefined) {
return player === undefined || isChatlogRangeUnlimited(range) || distance(entity, player) < range!;
}
function shouldShowChatMessageInChatlog(game: PonyTownGame, entity: Entity | FakeEntity, type: MessageType) {
if (entity.type !== PONY_TYPE)
return false;
if (entity.fake)
return true;
if (!isPublicMessage(type))
return true;
if (!isChatInRange(entity, game.player, game.settings.account.chatlogRange))
return false;
return true;
}
export function handleSay(game: PonyTownGame, entity: Entity | FakeEntity, message: string, type: MessageType) {
if (!shouldShowChatMessage(game, entity, message, type))
return;
if (type === MessageType.Dismiss || message === '.') {
if (!entity.fake && entity.says) {
dismissSays(entity.says);
}
} else {
const bubbleEntity = isWhisperTo(type) ? game.player : entity;
if (bubbleEntity && !bubbleEntity.fake && game.map.entitiesById.has(bubbleEntity.id)) {
const total = getSaysTime(message);
addChatBubble(game.map, bubbleEntity, { message, type, total, timer: total, created: Date.now() });
}
if (isWhisper(type)) {
const friend = game.model.friends && game.model.friends.find(f => f.entityId === entity.id);
game.lastWhisperFrom = { entityId: entity.id, accountId: friend && friend.accountId };
}
if (shouldShowChatMessageInChatlog(game, entity, type)) {
const { id, name = '', crc } = entity;
game.messageQueue.push({ id, crc, name, message, type });
}
}
}
export function handleEntityInfo(game: PonyTownGame, id: number, name: string, crc: number, nameBad: boolean) {
name = filterEntityName(game, name, nameBad)!;
for (let i = 0; i < game.incompleteSays.length;) {
const say = game.incompleteSays[i];
if (say.id === id) {
game.incompleteSays.splice(i, 1);
const entity: FakeEntity = { fake: true, type: PONY_TYPE, id, name, crc };
handleSay(game, entity, say.message, say.type);
} else {
i++;
}
}
}
export function subscribeRegion(game: PonyTownGame, data: Uint8Array) {
const { x, y, updates, tileData } = decodeUpdate(data);
const region = createRegion(x, y, tileData!);
const initial = !game.loaded;
setRegion(game.map, x, y, region);
for (const update of updates) {
handleAddEntity(game, region, update, initial);
}
}
export function filterEntityName({ settings, worldFlags }: PonyTownGame, name: string | undefined, nameBad: boolean) {
if (name && nameBad && (settings.account.filterSwearWords || hasFlag(worldFlags, WorldStateFlags.Safe))) {
return repeat('*', name.length);
} else if (name && containsFilteredWords(name, settings.account.filterWords)) {
return repeat('?', name.length);
} else {
return name;
}
}
function createEntityOrPony(
game: PonyTownGame, type: number, id: number, x: number, y: number, options: EntityOrPonyOptions,
crc: number, name: string | undefined, info: Uint8Array | undefined, state: EntityState
): Entity {
if (type === PONY_TYPE) {
const entity = createPonyEntity(game, id, options, name, info ? bitmask(info, PONY_INFO_KEY) : '', state);
const member = game.party && game.party.members.find(p => p.id === id);
entity.crc = crc;
if (member) {
game.apply(() => member.pony = entity);
}
if (isSelected(game, id)) {
game.select(entity);
}
return entity;
} else {
const entity = createAnEntity(type, id, x, y, options, game.paletteManager, game);
entity.state = state;
if (name) {
entity.name = name;
}
return entity;
}
}
function updateEntityOptionsInternal(entity: Entity, options: Partial<EntityOrPonyOptions>, game: PonyTownGame) {
Object.assign(entity, options);
if (isPony(entity) && 'hold' in options) {
updatePonyHold(entity, game);
}
}
export function handleUpdateFriends(game: PonyTownGame, friends: FriendStatusData[], removeMissing: boolean) {
if (!game.model.friends)
return;
for (const { accountId, accountName, status, entityId, name, info, crc, nameBad = false } of friends) {
let friend = game.model.friends.find(f => f.accountId === accountId);
if (hasFlag(status, FriendStatusFlags.Remove)) {
if (friend) {
removeItem(game.model.friends, friend);
}
} else {
if (!friend) {
friend = {
accountId,
accountName: '',
online: false,
name: undefined,
nameBad: false,
pony: undefined,
entityId: 0,
crc: 0,
ponyInfo: undefined,
actualName: '',
};
game.model.friends.push(friend);
}
friend.online = hasFlag(status, FriendStatusFlags.Online);
if (accountName !== undefined) {
friend.accountName = accountName;
}
if (entityId !== undefined) {
if (game.lastWhisperFrom && game.lastWhisperFrom.accountId === friend.accountId) {
game.lastWhisperFrom.entityId = entityId;
}
game.onEntityIdUpdate.next({ old: friend.entityId, new: entityId });
friend.entityId = entityId;
}
if (name !== undefined) {
friend.name = name;
friend.nameBad = nameBad;
friend.actualName = filterEntityName(game, name, nameBad) || '';
}
if (crc !== undefined) {
friend.crc = crc;
}
if (info !== undefined) {
friend.pony = info;
friend.ponyInfo = decodePonyInfo(info, mockPaletteManager);
}
if (friend.entityId && game.whisperTo && game.whisperTo.id === friend.entityId) {
game.whisperTo.name = friend.actualName;
game.whisperTo.crc = friend.crc;
}
}
}
if (removeMissing) {
for (let i = game.model.friends.length - 1; i >= 0; i--) {
if (!friends.find(f => f.accountId === game.model.friends![i].accountId)) {
game.model.friends.splice(i, 1);
}
}
DEVELOPMENT && console.log('Refreshing friend list');
}
game.model.friends.sort(compareFriends);
game.apply(() => { });
}
+174
View File
@@ -0,0 +1,174 @@
import { hasEmojis, splitEmojis, findEmoji, getEmojiImageAsync } from './emoji';
import { Dict } from '../common/interfaces';
import { font } from './fonts';
import { getCharacterSprite } from '../graphics/spriteFont';
export function createHtmlNodes(value: string | undefined, scale: number): Node[] {
return value ? splitEmojis(value).map(x => {
const sprite = hasEmojis(x) && font && getCharacterSprite(x, font);
if (sprite) {
const emote = findEmoji(x);
const img = document.createElement('img');
img.className = 'pixelart';
img.style.display = 'inline-block';
img.style.visibility = 'hidden';
img.style.width = `${(sprite.w + sprite.ox) * scale}px`;
img.style.height = `${10 * scale}px`;
if (emote) {
img.setAttribute('aria-label', emote.names[0]);
}
getEmojiImageAsync(sprite, src => {
img.alt = x;
img.src = src;
img.style.visibility = 'visible';
});
return img;
} else {
return document.createTextNode(x);
}
}) : [];
}
export function textNode(text: string) {
return document.createTextNode(text);
}
export function element(
tag: string, className?: string, nodes?: (Node | undefined)[], attrs?: Dict<any>, events?: Dict<() => any>
) {
const element = document.createElement(tag);
if (className) {
element.className = className;
}
if (nodes !== undefined) {
appendAllNodes(element, nodes);
}
if (attrs !== undefined) {
Object.keys(attrs).forEach(key => element.setAttribute(key, attrs[key]));
}
if (events !== undefined) {
Object.keys(events).forEach(key => element.addEventListener(key, events[key]));
}
return element;
}
export function appendAllNodes(element: Element, nodes: (Node | undefined)[]) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node !== undefined) {
element.appendChild(node);
}
}
}
export function removeAllNodes(element: Element) {
let child: Node | null;
while (child = element.lastChild) {
element.removeChild(child);
}
}
export function removeFirstChild(element: HTMLElement) {
let child: Node | null;
if (child = element.firstChild) {
element.removeChild(child);
}
}
export function removeElement(element: HTMLElement) {
element.parentElement && element.parentElement.removeChild(element);
}
export function replaceNodes(element: HTMLElement, text: string) {
while (element.lastChild && element.lastChild !== element.firstChild) {
element.removeChild(element.lastChild);
}
let firstChild = element.firstChild;
if (!firstChild) {
element.appendChild(firstChild = textNode(''));
}
if (hasEmojis(text)) {
firstChild.nodeValue = '';
appendAllNodes(element, createHtmlNodes(text, 2));
} else {
firstChild.nodeValue = text;
}
}
export function findParentElement(element: HTMLElement, selector: string) {
const elements = Array.from(document.querySelectorAll(selector));
let current = element.parentElement;
while (current && elements.indexOf(current) === -1) {
current = current.parentElement;
}
return current;
}
export function findFocusableElements(root: HTMLElement) {
const elements = root.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
return Array.from(elements) as HTMLElement[];
}
export function focusFirstElement(root: HTMLElement) {
const elements = findFocusableElements(root);
if (elements.length) {
elements[0].focus();
return elements[0];
}
return undefined;
}
export function focusElement(root: HTMLElement, selector: string) {
const target = root.querySelector(selector) as HTMLElement | null;
if (target) {
target.focus();
}
}
export function focusElementAfterTimeout(root: HTMLElement, selector: string) {
setTimeout(() => focusElement(root, selector), 10);
}
export function isParentOf(parent: Element, child: Element) {
for (let current = child.parentElement; current; current = current.parentElement) {
if (current === parent) {
return true;
}
}
return false;
}
export function showTextInNewTab(text: string) {
const wnd = window.open()!;
const pre = wnd.document.createElement('pre');
pre.innerText = text;
wnd.document.body.appendChild(pre);
}
export function addStyle(style: string) {
const styleElement = document.createElement('style');
styleElement.appendChild(document.createTextNode(style));
document.head.appendChild(styleElement);
return styleElement;
}
+206
View File
@@ -0,0 +1,206 @@
import {
faCrown,
faPlug,
faGamepad,
faMobile,
faTablet,
faTv,
} from '../generated/fa-icons';
export {
faHashtag,
faCog,
faCogs,
faMinus,
faPlus,
faCheck,
faFlag,
faStickyNote,
faCertificate,
faGlobe,
faGamepad,
faDesktop,
faQuestionCircle,
faInfo,
faSync,
faUserSecret,
faTrash,
faLock,
faApple,
faEdit,
faImage,
faLaughBeam,
faLanguage,
faCircle,
faEyeSlash,
faEnvelope,
faFont,
faCompressArrowsAlt,
faIdBadge,
faFilter,
faEraser,
faBell,
faClock,
faComment,
faComments,
faCommentSlash,
faHdd,
faMicrochip,
faUser,
faUsers,
faUserFriends,
faSpinner,
faBan,
faMicrophoneSlash,
faFileAlt,
faTimes,
faSearch,
faClipboard,
faChevronUp,
faChevronDown,
faChevronLeft,
faChevronRight,
faStar,
faAngleDoubleUp,
faAngleDoubleDown,
faAngleDoubleLeft,
faAngleDoubleRight,
faPlay,
faRedo,
faSave,
faArrowLeft,
faArrowRight,
faArrowUp,
faArrowDown,
faEyeDropper,
faPaintBrush,
faEllipsisV,
faExclamationCircle,
faUserPlus,
faUserMinus,
faUserTimes,
faSignOutAlt,
faStepForward,
faVolumeOff,
faVolumeDown,
faVolumeUp,
faHome,
faStop,
faRetweet,
faFile,
faCopy,
faShare,
faCode,
faTerminal,
faClone,
faPause,
faCrosshairs,
faFileImage,
faHeart,
faPlusCircle,
faMinusCircle,
faInfoCircle,
faCaretUp,
faCaretSquareUp,
faCaretSquareDown,
faCheckCircle,
faWrench,
faDrawPolygon,
faUserCog,
faSlidersH,
faExchangeAlt,
faDatabase,
faHorseHead,
faMapMarkerAlt,
faChartPie,
faCalendar,
} from '../generated/fa-icons';
import {
faPatreon,
faDeviantart,
faTwitter,
faTumblr,
faFacebook,
faGithub,
faVk,
faGoogle,
faChrome,
faInternetExplorer,
faEdge,
faAndroid,
faFirefox,
faSafari,
faOpera,
faWindows,
faApple,
faLinux,
faAmilia,
faYandexInternational,
} from '../generated/fa-icons';
export {
faPatreon,
faDeviantart,
faTwitter,
faTumblr,
faGithub,
} from '../generated/fa-icons';
export const partyLeaderIcon = faCrown;
export const offlineIcon = faPlug;
export const emptyIcon = {
prefix: 'fas',
iconName: 'empty-icon',
icon: [512, 512, [], 'ffff', ''],
};
export const oauthIcons: { [key: string]: any; } = {
patreon: faPatreon,
deviantart: faDeviantart,
twitter: faTwitter,
tumblr: faTumblr,
facebook: faFacebook,
github: faGithub,
vkontakte: faVk,
google: faGoogle,
};
export const uaIcons: { [key: string]: any; } = {
// browser
'Chrome': faChrome,
'Chromium': faChrome,
'IE': faInternetExplorer,
'Edge': faEdge,
'Android Browser': faAndroid,
'Firefox': faFirefox,
'Safari': faSafari,
'Mobile Safari': faSafari,
'Opera': faOpera,
'Opera Mini': faOpera,
'Amigo': faAmilia,
'YaBrowser': faYandexInternational,
// os
'Windows': faWindows,
'Windows Phone': faWindows,
'Android': faAndroid,
'iOS': faApple,
'Mac OS': faApple,
'Arch': faLinux,
'CentOS': faLinux,
'Fedora': faLinux,
'FreeBSD': faLinux,
'OpenBSD': faLinux,
'Debian': faLinux,
'Ubuntu': faLinux,
'Linux': faLinux,
'Chromium OS': faChrome,
'Firefox OS': faFirefox,
'Playstation': faGamepad,
'Nintendo': faGamepad,
// device
'console': faGamepad,
'mobile': faMobile,
'tablet': faTablet,
'smarttv': faTv,
};
+160
View File
@@ -0,0 +1,160 @@
import { GAMEPAD_MAPPINGS, GamepadAxes, GamepadButtons, GamepadMapping } from '../../generated/gamepad-mappings';
import { Key, InputController } from './input';
import { InputManager } from './inputManager';
import { isFocused } from '../clientUtils';
interface GamepadInstance {
gamepad: Gamepad;
mapping: GamepadMapping;
}
const JOYSTICK_THRESHHOLD = 0.2;
function createGamepad(gamepad: Gamepad): GamepadInstance {
const mapping = detectMapping(gamepad.id, navigator.userAgent);
return { gamepad, mapping };
}
function isCompatible(mapping: any, id: string, browser: string) {
for (let i = 0; i < mapping.supported.length; i++) {
const supported = mapping.supported[i];
if (id.indexOf(supported.id) !== -1 && browser.indexOf(supported.os) !== -1 && browser.indexOf(browser) !== -1) {
return true;
}
}
return false;
}
function detectMapping(id: string, browser: string) {
for (let i = 0; i < GAMEPAD_MAPPINGS.length; i++) {
if (isCompatible(GAMEPAD_MAPPINGS[i], id, browser)) {
return GAMEPAD_MAPPINGS[i];
}
}
return GAMEPAD_MAPPINGS[0];
}
function axis({ mapping, gamepad }: GamepadInstance, name: GamepadAxes) {
const axe = mapping.axes[name] as any;
return axe ? gamepad.axes[axe.index] : 0;
}
function button({ mapping, gamepad }: GamepadInstance, name: GamepadButtons) {
const button = mapping.buttons[name] as any;
if (!button) {
return false;
}
if (button.index !== undefined) {
return gamepad.buttons[button.index] && gamepad.buttons[button.index].pressed;
}
if (button.axis !== undefined) {
if (button.direction < 0) {
return gamepad.axes[button.axis] < -0.75;
} else {
return gamepad.axes[button.axis] > 0.75;
}
}
return false;
}
export class GamePadController implements InputController {
private initialized = false;
private gamepadIndex = -1;
private zeroed1 = false;
private zeroed2 = false;
constructor(private manager: InputManager) {
}
initialize() {
if (!this.initialized) {
this.initialized = true;
window.addEventListener('gamepadconnected', this.gamepadconnected);
window.addEventListener('gamepaddisconnected', this.gamepaddisconnected);
this.scanGamepads();
}
}
release() {
this.initialized = false;
window.removeEventListener('gamepadconnected', this.gamepadconnected);
window.removeEventListener('gamepaddisconnected', this.gamepaddisconnected);
}
update() {
if (this.manager.disabledGamepad || !isFocused() || this.gamepadIndex === -1)
return;
const gamepads = navigator.getGamepads();
const gamepad = gamepads[this.gamepadIndex];
if (!gamepad) {
this.scanGamepads();
return;
}
const pad = createGamepad(gamepad);
this.zeroed1 = readAxis(
this.manager, Key.GAMEPAD_AXIS1_X, Key.GAMEPAD_AXIS1_Y,
axis(pad, GamepadAxes.LeftStickX), axis(pad, GamepadAxes.LeftStickY), this.zeroed1);
this.zeroed2 = readAxis(
this.manager, Key.GAMEPAD_AXIS2_X, Key.GAMEPAD_AXIS2_Y,
axis(pad, GamepadAxes.RightStickX), axis(pad, GamepadAxes.RightStickY), this.zeroed2);
this.manager.setValue(Key.GAMEPAD_BUTTON_X, button(pad, GamepadButtons.X) ? 1 : 0);
this.manager.setValue(Key.GAMEPAD_BUTTON_Y, button(pad, GamepadButtons.Y) ? 1 : 0);
this.manager.setValue(Key.GAMEPAD_BUTTON_A, button(pad, GamepadButtons.A) ? 1 : 0);
this.manager.setValue(Key.GAMEPAD_BUTTON_B, button(pad, GamepadButtons.B) ? 1 : 0);
this.manager.setValue(Key.GAMEPAD_BUTTON_DOWN, button(pad, GamepadButtons.DpadDown) ? 1 : 0);
this.manager.setValue(Key.GAMEPAD_BUTTON_LEFT, button(pad, GamepadButtons.DpadLeft) ? 1 : 0);
this.manager.setValue(Key.GAMEPAD_BUTTON_RIGHT, button(pad, GamepadButtons.DpadRight) ? 1 : 0);
this.manager.setValue(Key.GAMEPAD_BUTTON_UP, button(pad, GamepadButtons.DpadUp) ? 1 : 0);
}
clear() {
}
private scanGamepads() {
const gamepads = navigator.getGamepads();
// Using regular loop because of issues with iterating over gamepads
for (let i = 0; i < gamepads.length; i++) {
const gamepad = gamepads[i];
if (gamepad) {
this.gamepadIndex = gamepad.index;
return;
}
}
this.gamepadIndex = -1;
}
private gamepadconnected = (e: Event) => {
this.gamepadIndex = (e as GamepadEvent).gamepad.index;
}
private gamepaddisconnected = (e: Event) => {
if (this.gamepadIndex === (e as GamepadEvent).gamepad.index) {
this.scanGamepads();
}
}
}
function readAxis(manager: InputManager, keyX: Key, keyY: Key, axisX: number, axisY: number, zeroed: boolean): boolean {
const dist = Math.sqrt(axisX * axisX + axisY * axisY);
if (dist > JOYSTICK_THRESHHOLD) {
const scaledDist = Math.min((dist - JOYSTICK_THRESHHOLD) / (1 - JOYSTICK_THRESHHOLD), 1);
const theta = Math.atan2(axisY, axisX);
manager.setValue(keyX, Math.cos(theta) * scaledDist);
manager.setValue(keyY, Math.sin(theta) * scaledDist);
return false;
} else if (!zeroed) {
manager.setValue(keyX, 0);
manager.setValue(keyY, 0);
return true;
}
return zeroed;
}
+144
View File
@@ -0,0 +1,144 @@
export const enum Key {
// Keyboard
BACKSPACE = 8,
TAB = 9,
ENTER = 13,
SHIFT = 16,
CTRL = 17,
ALT = 18,
PAUSE = 19,
CAPS_LOCK = 20,
ESCAPE = 27,
SPACE = 32,
PAGE_UP = 33,
PAGE_DOWN = 34,
END = 35,
HOME = 36,
LEFT = 37,
UP = 38,
RIGHT = 39,
DOWN = 40,
INSERT = 45,
DELETE = 46,
KEY_0 = 48,
KEY_1 = 49,
KEY_2 = 50,
KEY_3 = 51,
KEY_4 = 52,
KEY_5 = 53,
KEY_6 = 54,
KEY_7 = 55,
KEY_8 = 56,
KEY_9 = 57,
KEY_A = 65,
KEY_B = 66,
KEY_C = 67,
KEY_D = 68,
KEY_E = 69,
KEY_F = 70,
KEY_G = 71,
KEY_H = 72,
KEY_I = 73,
KEY_J = 74,
KEY_K = 75,
KEY_L = 76,
KEY_M = 77,
KEY_N = 78,
KEY_O = 79,
KEY_P = 80,
KEY_Q = 81,
KEY_R = 82,
KEY_S = 83,
KEY_T = 84,
KEY_U = 85,
KEY_V = 86,
KEY_W = 87,
KEY_X = 88,
KEY_Y = 89,
KEY_Z = 90,
LEFT_META = 91,
RIGHT_META = 92,
SELECT = 93,
NUMPAD_0 = 96,
NUMPAD_1 = 97,
NUMPAD_2 = 98,
NUMPAD_3 = 99,
NUMPAD_4 = 100,
NUMPAD_5 = 101,
NUMPAD_6 = 102,
NUMPAD_7 = 103,
NUMPAD_8 = 104,
NUMPAD_9 = 105,
MULTIPLY = 106,
ADD = 107,
SUBTRACT = 109,
DECIMAL = 110,
DIVIDE = 111,
F1 = 112,
F2 = 113,
F3 = 114,
F4 = 115,
F5 = 116,
F6 = 117,
F7 = 118,
F8 = 119,
F9 = 120,
F10 = 121,
F11 = 122,
F12 = 123,
NUM_LOCK = 144,
SCROLL_LOCK = 145,
SEMICOLON = 186,
EQUALS = 187,
COMMA = 188,
DASH = 189,
PERIOD = 190,
FORWARD_SLASH = 191,
GRAVE_ACCENT = 192,
OPEN_BRACKET = 219,
BACK_SLASH = 220,
CLOSE_BRACKET = 221,
SINGLE_QUOTE = 222,
// Mouse
MOUSE_X = 300,
MOUSE_Y,
MOUSE_BUTTON1,
MOUSE_BUTTON2,
MOUSE_BUTTON3,
MOUSE_WHEEL_X,
MOUSE_WHEEL_Y,
// Gamepad
GAMEPAD_AXIS1_X,
GAMEPAD_AXIS1_Y,
GAMEPAD_AXIS2_X,
GAMEPAD_AXIS2_Y,
GAMEPAD_BUTTON_A,
GAMEPAD_BUTTON_B,
GAMEPAD_BUTTON_X,
GAMEPAD_BUTTON_Y,
GAMEPAD_BUTTON_L1,
GAMEPAD_BUTTON_R1,
GAMEPAD_BUTTON_L2,
GAMEPAD_BUTTON_R2,
GAMEPAD_BUTTON_START,
GAMEPAD_BUTTON_SELECT,
GAMEPAD_BUTTON_ANALOG1,
GAMEPAD_BUTTON_ANALOG2,
GAMEPAD_BUTTON_UP,
GAMEPAD_BUTTON_DOWN,
GAMEPAD_BUTTON_LEFT,
GAMEPAD_BUTTON_RIGHT,
// Touch
TOUCH,
TOUCH_CLICK,
TOUCH_SECOND_CLICK,
// Other
MAX_VALUE,
}
export interface InputController {
initialize(element: HTMLElement): void;
release(): void;
update(): void;
clear(): void;
}
+170
View File
@@ -0,0 +1,170 @@
import { clamp } from 'lodash';
import { KeyboardController } from './keyboard';
import { MouseController } from './mouse';
import { TouchController } from './touch';
import { GamePadController } from './gamepad';
import { InputController, Key } from './input';
import { array, times } from '../../common/utils';
type Handler = (input: Key, value: number) => boolean | void;
const KEYS = Key.MAX_VALUE;
export class InputManager {
disabledGamepad = false;
disabledKeyboard = false;
disableArrows = false;
usingTouch = false;
private state: number[];
private prevState: number[];
private actions: Handler[][];
private controllers: InputController[] = [];
constructor() {
this.state = array(KEYS, 0);
this.prevState = array(KEYS, 0);
this.actions = times(KEYS, () => []);
}
get axisX() {
const axisX = this.getRange(Key.GAMEPAD_AXIS1_X);
const left = this.disableArrows ? this.getState(Key.KEY_A) : this.getState(Key.LEFT, Key.KEY_A);
const right = this.disableArrows ? this.getState(Key.KEY_D) : this.getState(Key.RIGHT, Key.KEY_D);
const x = axisX + (left ? -1 : (right ? 1 : 0));
return clamp(x, -1, 1);
}
get axisY() {
const axisY = this.getRange(Key.GAMEPAD_AXIS1_Y);
const up = this.disableArrows ? this.getState(Key.KEY_W) : this.getState(Key.UP, Key.KEY_W);
const down = this.disableArrows ? this.getState(Key.KEY_S) : this.getState(Key.DOWN, Key.KEY_S);
const y = axisY + (up ? -1 : (down ? 1 : 0));
return clamp(y, -1, 1);
}
get isMovementFromButtons() {
const up = this.getState(Key.UP, Key.KEY_W);
const down = this.getState(Key.DOWN, Key.KEY_S);
const left = this.getState(Key.LEFT, Key.KEY_A);
const right = this.getState(Key.RIGHT, Key.KEY_D);
return up || down || left || right;
}
get axis2X() {
return clamp(this.getRange(Key.GAMEPAD_AXIS2_X), -1, 1);
}
get axis2Y() {
return clamp(this.getRange(Key.GAMEPAD_AXIS2_Y), -1, 1);
}
get pointerX() {
return this.getRange(Key.MOUSE_X);
}
get pointerY() {
return this.getRange(Key.MOUSE_Y);
}
get wheelX() {
return this.getRange(Key.MOUSE_WHEEL_X);
}
get wheelY() {
return this.getRange(Key.MOUSE_WHEEL_Y);
}
initialize(element: HTMLElement) {
this.controllers = [
new KeyboardController(this),
new MouseController(this),
new TouchController(this),
new GamePadController(this),
];
this.controllers.forEach(c => c.initialize(element));
this.clear();
}
release() {
this.controllers.forEach(c => c.release());
this.controllers = [];
this.clear();
}
update() {
for (const controller of this.controllers) {
controller.update();
}
}
end() {
for (let i = 0; i < KEYS; i++) {
this.prevState[i] = this.state[i];
}
this.setValue(Key.TOUCH_CLICK, 0);
this.setValue(Key.TOUCH_SECOND_CLICK, 0);
this.setValue(Key.MOUSE_WHEEL_X, 0);
this.setValue(Key.MOUSE_WHEEL_Y, 0);
}
clear() {
for (let i = 0; i < KEYS; i++) {
this.state[i] = 0;
this.prevState[i] = 0;
}
for (const controller of this.controllers) {
controller.clear();
}
}
onPressed(inputs: Key[] | Key, handler: () => void) {
this.onAction(inputs, (_, v) => {
if (v === 1) {
handler();
}
});
}
onReleased(inputs: Key[] | Key, handler: () => void) {
this.onAction(inputs, (_, v) => {
if (v === 0) {
handler();
}
});
}
isPressed(key: Key) {
return this.state[key] !== 0;
}
wasPressed(key: Key): boolean {
return this.state[key] === 1 && this.prevState[key] === 0;
}
private onAction(inputs: Key[] | Key, handler: Handler) {
const inputsArray = Array.isArray(inputs) ? inputs : [inputs];
for (const i of inputsArray) {
this.actions[i].push(handler);
}
}
private getState(...inputs: Key[]): boolean {
for (const i of inputs) {
if (this.state[i] !== 0) {
return true;
}
}
return false;
}
private getRange(input: Key): number {
return this.state[input];
}
setValue(input: Key, value: number): boolean {
if (input < 0 || input >= KEYS) {
console.warn(`Input out of range: ${input}`);
} else if (this.state[input] !== value) {
this.state[input] = value;
if (this.actions[input] && this.actions[input].length) {
for (const action of this.actions[input]) {
action(input, value);
}
return true;
}
}
return false;
}
addValue(input: Key, value: number) {
if (input < 0 || input >= KEYS) {
console.warn(`Input out of range: ${input}`);
} else {
this.state[input] += value;
}
}
}
+96
View File
@@ -0,0 +1,96 @@
import { InputController, Key } from './input';
import { InputManager } from './inputManager';
import { removeItem, includes } from '../../common/utils';
const firefox = !SERVER && /firefox/i.test(navigator.userAgent);
function isKeyEventInvalid(e: KeyboardEvent) {
return e.target && /^(input|textarea|select)$/i.test((e.target as HTMLElement).tagName);
}
function allowKey(key: number) {
return key === Key.ESCAPE || key === Key.F5 || key === Key.F12 || key === Key.F11 || key === Key.TAB;
}
function fixKeyCode(key: number) {
if (firefox) {
if (key === 173) return Key.DASH;
if (key === 61) return Key.EQUALS;
}
return key;
}
const iosKeyToKeyCode: { [key: string]: number | undefined; } = {
UIKeyInputEscape: Key.ESCAPE,
UIKeyInputUpArrow: Key.UP,
UIKeyInputLeftArrow: Key.LEFT,
UIKeyInputRightArrow: Key.RIGHT,
UIKeyInputDownArrow: Key.DOWN,
};
const iosHandledKeyCodes = [Key.ESCAPE, Key.UP, Key.LEFT, Key.RIGHT, Key.DOWN];
export class KeyboardController implements InputController {
private initialized = false;
private stack: number[] = [];
constructor(private manager: InputManager) {
}
initialize() {
if (!this.initialized) {
this.initialized = true;
window.addEventListener('keydown', this.keydown);
window.addEventListener('keyup', this.keyup);
window.addEventListener('blur', this.blur);
}
}
release() {
this.initialized = false;
window.removeEventListener('keydown', this.keydown);
window.removeEventListener('keyup', this.keyup);
window.removeEventListener('blur', this.blur);
this.clear();
}
update() {
}
clear() {
this.stack.length = 0;
}
private keydown = (e: KeyboardEvent) => {
if (!this.manager.disabledKeyboard && !isKeyEventInvalid(e)) {
const code = fixKeyCode(e.keyCode);
this.manager.setValue(code, 1);
if (!allowKey(code)) {
e.preventDefault();
e.stopPropagation();
}
if (!includes(this.stack, code) && !includes(iosHandledKeyCodes, code)) {
this.stack.push(code);
}
}
}
private keyup = (e: KeyboardEvent) => {
let code = fixKeyCode(e.keyCode);
// fix keyCode on iOS bluetooth keyboard
if (code === 0) {
code = iosKeyToKeyCode[e.key] || 0;
if (code === 0) {
code = this.stack.pop() || 0;
}
}
if (this.manager.setValue(code, 0)) {
e.preventDefault();
e.stopPropagation();
}
removeItem(this.stack, code);
}
private blur = () => {
this.manager.clear();
}
}
+87
View File
@@ -0,0 +1,87 @@
import { Key, InputController } from './input';
import { InputManager } from './inputManager';
import { clamp } from '../../common/utils';
const MOUSE_BUTTONS = [Key.MOUSE_BUTTON1, Key.MOUSE_BUTTON3, Key.MOUSE_BUTTON2];
export class MouseController implements InputController {
private initialized = false;
private element?: HTMLElement;
constructor(private manager: InputManager) {
}
initialize(element: HTMLElement) {
if (!this.initialized) {
this.initialized = true;
this.element = element;
element.addEventListener('mousemove', this.mousemove);
element.addEventListener('mousedown', this.mousedown);
element.addEventListener('mouseup', this.mouseup);
element.addEventListener('mousewheel', this.mousewheel);
element.addEventListener('contextmenu', this.contextmenu);
element.addEventListener('click', this.click);
window.addEventListener('blur', this.blur);
}
}
release() {
this.initialized = false;
if (this.element) {
this.element.removeEventListener('mousemove', this.mousemove);
this.element.removeEventListener('mousedown', this.mousedown);
this.element.removeEventListener('mouseup', this.mouseup);
this.element.removeEventListener('mousewheel', this.mousewheel);
this.element.removeEventListener('contextmenu', this.contextmenu);
this.element.removeEventListener('click', this.click);
this.element = undefined;
}
window.removeEventListener('blur', this.blur);
}
update() {
}
clear() {
}
private mousemove = (e: MouseEvent) => {
if (this.element) {
const rect = this.element.getBoundingClientRect();
this.manager.setValue(Key.MOUSE_X, Math.floor(e.clientX - rect.left));
this.manager.setValue(Key.MOUSE_Y, Math.floor(e.clientY - rect.top));
}
}
private mousedown = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
this.manager.usingTouch = false;
const button = MOUSE_BUTTONS[e.button];
if (button) {
this.manager.setValue(button, 1);
}
}
private mouseup = (e: MouseEvent) => {
const button = MOUSE_BUTTONS[e.button];
if (button) {
this.manager.setValue(button, 0);
}
}
private mousewheel: any = (e: MouseWheelEvent) => {
this.manager.addValue(Key.MOUSE_WHEEL_X, clamp(e.deltaX, -1, 1));
this.manager.addValue(Key.MOUSE_WHEEL_Y, clamp(e.deltaY, -1, 1));
}
private contextmenu = (e: Event) => {
e.preventDefault();
e.stopPropagation();
}
private click = (e: Event) => {
e.preventDefault();
e.stopPropagation();
}
private blur = () => {
for (const button of MOUSE_BUTTONS) {
this.manager.setValue(button, 0);
}
}
}
+203
View File
@@ -0,0 +1,203 @@
import { Key, InputController } from './input';
import { Point } from '../../common/interfaces';
import { setTransform } from '../../common/utils';
import { InputManager } from './inputManager';
function getTouch(e: TouchEvent, id: number) {
if (id !== -1) {
for (let i = 0; i < e.changedTouches.length; ++i) {
const touch = e.changedTouches.item(i);
if (touch && touch.identifier === id) {
return touch;
}
}
}
return undefined;
}
const TOUCH_DEADZONE = 15;
const TOUCH_MAX = 100;
export class TouchController implements InputController {
private initialized = false;
private touchId = -1;
private touch2Id = -1;
private touchStart: Point = { x: 0, y: 0 };
private touchCurrent: Point = { x: 0, y: 0 };
private touchIsDrag = false;
private tapInvalidated = false;
private origin?: HTMLElement;
private position?: HTMLElement;
private originShown = false;
private originTransform?: string;
private positionShown = false;
private positionTransform?: string;
private element?: HTMLElement;
constructor(private manager: InputManager) {
}
initialize(element: HTMLElement) {
if (!this.initialized) {
this.initialized = true;
this.element = element;
this.origin = document.getElementById('touch-origin')!;
this.position = document.getElementById('touch-position')!;
element.addEventListener('touchstart', this.touchstart);
element.addEventListener('touchmove', this.touchmove);
element.addEventListener('touchend', this.touchend);
window.addEventListener('touchend', this.blur);
window.addEventListener('blur', this.blur);
}
}
release() {
this.initialized = false;
if (this.element) {
this.element.removeEventListener('touchstart', this.touchstart);
this.element.removeEventListener('touchmove', this.touchmove);
this.element.removeEventListener('touchend', this.touchend);
this.element = undefined;
}
window.removeEventListener('touchend', this.blur);
window.removeEventListener('blur', this.blur);
}
update() {
const showOrigin = this.touchIsDrag && this.touchId !== -1;
const showPosition = this.touchId !== -1;
if (this.origin && this.position) {
if (this.originShown !== showOrigin) {
this.originShown = showOrigin;
this.origin.style.display = showOrigin ? 'block' : 'none';
}
if (this.positionShown !== showPosition) {
this.positionShown = showPosition;
this.position.style.display = showPosition ? 'block' : 'none';
}
if (showOrigin) {
const transform = `translate3d(${this.touchStart.x - 50}px, ${this.touchStart.y - 50}px, 0px)`;
if (this.originTransform !== transform) {
this.originTransform = transform;
setTransform(this.origin, transform);
}
}
if (showPosition) {
const transform = `translate3d(${this.touchCurrent.x - 25}px, ${this.touchCurrent.y - 25}px, 0px)`;
if (this.positionTransform !== transform) {
this.positionTransform = transform;
setTransform(this.position, transform);
}
}
}
}
clear() {
}
private reset() {
this.touch2Id = -1;
this.resetTouch();
}
private resetTouch() {
this.touchId = -1;
this.touchStart = this.touchCurrent = { x: 0, y: 0 };
this.touchIsDrag = false;
this.manager.setValue(Key.TOUCH, 0);
this.updateInput();
}
private updateInput() {
const dy = this.touchStart.y - this.touchCurrent.y;
const dx = this.touchStart.x - this.touchCurrent.x;
const theta = Math.atan2(dy, dx);
const dist = Math.sqrt(dy * dy + dx * dx);
if (dist > TOUCH_DEADZONE) {
const scaledDist = Math.min((dist - TOUCH_DEADZONE) / (TOUCH_MAX - TOUCH_DEADZONE), 1);
this.touchIsDrag = true;
this.manager.setValue(Key.GAMEPAD_AXIS1_X, -Math.cos(theta) * scaledDist);
this.manager.setValue(Key.GAMEPAD_AXIS1_Y, -Math.sin(theta) * scaledDist);
} else {
this.manager.setValue(Key.GAMEPAD_AXIS1_X, 0);
this.manager.setValue(Key.GAMEPAD_AXIS1_Y, 0);
}
}
private touchstart = (e: any) => {
e.cancellable && e.preventDefault();
e.stopPropagation();
this.manager.usingTouch = true;
if (this.touchId === -1) {
const touch = e.changedTouches.item(0);
if (touch) {
this.tapInvalidated = false;
this.touchId = touch.identifier;
this.touchStart = this.touchCurrent = this.getTouchXY(touch);
this.manager.setValue(Key.MOUSE_X, this.touchStart.x);
this.manager.setValue(Key.MOUSE_Y, this.touchStart.y);
this.manager.setValue(Key.TOUCH, 1);
}
} else if (this.touch2Id === -1) {
const touch = e.changedTouches.item(0);
if (touch) {
this.tapInvalidated = true;
this.touch2Id = touch.identifier;
}
}
}
private touchmove = (e: any) => {
e.preventDefault();
e.stopPropagation();
const touch = getTouch(e, this.touchId);
if (touch) {
this.touchCurrent = this.getTouchXY(touch);
this.manager.setValue(Key.MOUSE_X, this.touchCurrent.x);
this.manager.setValue(Key.MOUSE_Y, this.touchCurrent.y);
this.updateInput();
}
}
private touchend = (e: any) => {
e.preventDefault();
e.stopPropagation();
const touch = getTouch(e, this.touchId);
if (touch) {
if (!this.touchIsDrag && !this.tapInvalidated) {
this.manager.setValue(Key.MOUSE_X, this.touchStart.x);
this.manager.setValue(Key.MOUSE_Y, this.touchStart.y);
this.manager.setValue(Key.TOUCH_CLICK, 1);
}
this.resetTouch();
}
const touch2 = getTouch(e, this.touch2Id);
if (touch2) {
this.manager.setValue(Key.TOUCH_SECOND_CLICK, 1);
this.touch2Id = -1;
}
}
private blur = () => {
this.reset();
}
private getTouchXY(touch: Touch) {
const { left, top } = this.element!.getBoundingClientRect();
return {
x: touch.clientX - left,
y: touch.clientY - top,
};
}
}
+44
View File
@@ -0,0 +1,44 @@
import { remove } from 'lodash';
import { PartyMember, PartyInfo, Pony } from '../common/interfaces';
import { PonyTownGame } from './game';
export function updateParty(current: PartyInfo | undefined, info: PartyMember[] | undefined): PartyInfo | undefined {
if (!info || !info.length) {
return undefined;
} else {
const party = current || {
leaderId: 0,
members: [],
};
remove(party.members, p => !info.some(m => p.id === m.id));
info.forEach(m => {
const existing = party.members.find(x => m.id === x.id);
if (existing) {
Object.assign(existing, m);
} else {
party.members.push(m);
}
if (m.leader) {
party.leaderId = m.id;
}
});
return party;
}
}
export function isPonyInParty(party: PartyInfo | undefined, pony: Pony, pending: boolean) {
return !!party && party.members.some(m => m.pony === pony && (pending || !m.pending));
}
export function isPartyLeader(game: PonyTownGame): boolean {
return game.party !== undefined && game.player !== undefined && game.player.id === game.party.leaderId;
}
export function isInParty(game: PonyTownGame): boolean {
return game.party !== undefined && game.party.members.length > 0;
}
+229
View File
@@ -0,0 +1,229 @@
import { isCommand, processCommand, hasFlag, includes, point } from '../common/utils';
import { canPonyLie, canPonyFlyUp, canPonyStand, canPonySit, doBoopPonyAction } from '../common/pony';
import { PonyTownGame } from './game';
import {
setPonyState, canBoop, isPonyLying, isPonyFlying, isPonyStanding, isPonySitting, getInteractBounds,
isFacingRight, closestEntity, entityInRange
} from '../common/entityUtils';
import { EntityState, Action, Pony, ChatType, EntityFlags, Point, TileType } from '../common/interfaces';
import { FLY_DELAY } from '../common/constants';
import { randomString } from '../common/stringUtils';
import { pickEntitiesByRect, pickAnyEntities } from '../common/worldMap';
import { centerPoint } from '../common/rect';
import { pointToWorld, roundPositionX, roundPositionY } from '../common/positionUtils';
import { hammer, shovel } from '../common/entities';
export function handleActionCommand(message: string, game: PonyTownGame): boolean {
if (isCommand(message)) {
const { command = '' } = processCommand(message);
const player = game.player;
if (DEVELOPMENT) {
if (command === 'spammessages') {
let i = 0;
setInterval(() => game.send(server => server.say(0, randomString(5) + ` #${i++}`, ChatType.Say)), 100);
return true;
}
}
switch (command.toLowerCase()) {
case 'testerrorreporting':
throw new Error('test error');
case 'disablepixelratio':
game.togglePixelRatio();
return true;
case 'lie':
case 'lay':
if (player) {
if (isPonyLying(player)) {
sitAction(player, game);
} else {
lieAction(player, game);
}
}
return true;
case 'sit':
if (player) {
if (isPonyFlying(player)) {
standAction(player, game);
} else {
sitAction(player, game);
}
}
return true;
case 'stand':
if (player) {
standAction(player, game);
}
return true;
case 'fly':
if (player) {
if (isPonyFlying(player)) {
standAction(player, game);
} else {
flyAction(player, game);
}
}
return true;
}
}
return false;
}
export function upAction(game: PonyTownGame) {
const player = game.player;
if (player) {
if (isPonyLying(player)) {
sitAction(player, game);
} else if (isPonySitting(player)) {
standAction(player, game);
} else if (isPonyStanding(player)) {
flyAction(player, game);
}
}
}
export function downAction(game: PonyTownGame) {
const player = game.player;
if (player) {
if (isPonySitting(player)) {
lieAction(player, game);
} else if (isPonyStanding(player)) {
sitAction(player, game);
} else if (isPonyFlying(player)) {
standAction(player, game);
}
}
}
export function sitAction(player: Pony, game: PonyTownGame) {
if (canPonySit(player, game.map) && game.send(server => server.action(Action.Sit))) {
player.state = setPonyState(player.state, EntityState.PonySitting);
game.stateOverride = EntityState.PonySitting;
game.onActionsUpdate.next();
}
}
export function standAction(player: Pony, game: PonyTownGame) {
if (canPonyStand(player, game.map) && game.send(server => server.action(Action.Stand))) {
player.state = setPonyState(player.state, EntityState.PonyStanding);
game.stateOverride = EntityState.PonyStanding;
game.onActionsUpdate.next();
}
}
export function lieAction(player: Pony, game: PonyTownGame) {
if (canPonyLie(player, game.map) && game.send(server => server.action(Action.Lie))) {
player.state = setPonyState(player.state, EntityState.PonyLying);
game.stateOverride = EntityState.PonyLying;
game.onActionsUpdate.next();
}
}
export function flyAction(player: Pony, game: PonyTownGame) {
if (canPonyFlyUp(player) && game.send(server => server.action(Action.Fly))) {
player.state = setPonyState(player.state, EntityState.PonyFlying);
player.inTheAirDelay = FLY_DELAY;
game.stateOverride = EntityState.PonyFlying;
game.onActionsUpdate.next();
}
}
export function boopAction(game: PonyTownGame) {
if (game.player && canBoop(game.player) && game.send(server => server.action(Action.Boop))) {
doBoopPonyAction(game, game.player);
}
}
export function turnHeadAction(game: PonyTownGame) {
if (game.player && game.send(server => server.action(Action.TurnHead))) {
game.player.state = game.player.state ^ EntityState.HeadTurned;
game.headTurnedOverride = hasFlag(game.player.state, EntityState.HeadTurned);
game.onActionsUpdate.next();
}
}
export function interact(game: PonyTownGame, shift: boolean) {
const player = game.player;
if (player) {
const bounds = getInteractBounds(player);
const entities = pickEntitiesByRect(game.map, bounds, true, false);
const center = centerPoint(bounds);
center.x += (bounds.w / 4) * (isFacingRight(player) ? -1 : 1);
const entity = closestEntity(pointToWorld(center), entities);
if (entity && entityInRange(entity, player)) {
game.send(server => server.interact(entity.id));
} else if (player.hold === hammer.type) {
game.changePlaceEntity(shift);
} else if (player.hold === shovel.type) {
game.changePlaceTile(shift);
} else if (player.ponyState.holding && hasFlag(player.ponyState.holding.flags, EntityFlags.Usable)) {
game.send(server => server.use());
}
}
}
export function toggleWall(game: PonyTownGame, hover: Point) {
const x = hover.x | 0;
const y = hover.y | 0;
const dx = hover.x - x;
const dy = hover.y - y;
if (dx > dy) {
if ((dx + dy) < 1) {
game.send(server => server.changeTile(x, y, TileType.WallH));
} else {
game.send(server => server.changeTile(x + 1, y, TileType.WallV));
}
} else {
if ((dx + dy) < 1) {
game.send(server => server.changeTile(x, y, TileType.WallV));
} else {
game.send(server => server.changeTile(x, y + 1, TileType.WallH));
}
}
}
export function editorSelectEntities(game: PonyTownGame, hover: Point, shift: boolean) {
game.apply(() => {
const entities = pickAnyEntities(game.map, hover);
if (shift) {
const entity = entities.filter(e => !includes(game.editor.selectedEntities, e))[0];
entity && game.editor.selectedEntities.push(entity);
} else {
const index = entities.findIndex(e => includes(game.editor.selectedEntities, e));
const entity = entities[(index + 1) % entities.length];
game.editor.selectedEntities = entity ? [entity] : [];
}
});
}
export function editorDragEntities(game: PonyTownGame, hover: Point, buttonPressed: boolean) {
if (buttonPressed) {
const dx = hover.x - game.editor.draggingStart.x;
const dy = hover.y - game.editor.draggingStart.y;
game.editor.selectedEntities.forEach(e => {
e.x = roundPositionX(e.draggingStart!.x + dx);
e.y = roundPositionY(e.draggingStart!.y + dy);
});
} else {
game.apply(() => game.editor.draggingEntities = false);
game.send(server => server.editorAction({
type: 'move',
entities: game.editor.selectedEntities.map(({ id, x, y }) => ({ id, x, y })),
}));
}
}
export function editorMoveEntities(game: PonyTownGame, hover: Point) {
game.editor.draggingEntities = true;
game.editor.draggingStart = hover;
game.editor.selectedEntities.forEach(e => e.draggingStart = point(e.x, e.y));
}
+33
View File
@@ -0,0 +1,33 @@
/// <reference path="../../typings/my.d.ts" />
// Safari <= 8.4, Android
try {
if (!('performance' in window && 'now' in performance)) {
(window as any).performance = Date;
}
} catch { }
try {
if (!('getGamepads' in navigator)) {
(window.navigator as any).getGamepads = () => [];
}
} catch { }
try {
if (!('requestAnimationFrame' in window)) {
(window as any).requestAnimationFrame = (callback: any) => setTimeout(() => callback(performance.now()), 1000 / 60) as any;
}
} catch { }
try {
if (!('cancelAnimationFrame' in window)) {
(window as any).cancelAnimationFrame = clearTimeout;
}
} catch { }
// IE <= 10
try {
if (!('devicePixelRatio' in window)) {
(window as any).devicePixelRatio = 1;
}
} catch { }
+547
View File
@@ -0,0 +1,547 @@
import { BodyAnimation, BodyAnimationFrame, HeadAnimation, HeadAnimationFrame, BodyShadow } from '../common/interfaces';
import { repeat, flatten } from '../common/utils';
// body animations
export function createBodyFrame([
body = 0, head = 0, wing = 0, tail = 0,
frontLeg = 0, frontFarLeg = 0, backLeg = 0, backFarLeg = 0,
bodyX = 0, bodyY = 0, headX = 0, headY = 0,
frontLegX = 0, frontLegY = 0, frontFarLegX = 0, frontFarLegY = 0,
backLegX = 0, backLegY = 0, backFarLegX = 0, backFarLegY = 0
]: number[]): Readonly<BodyAnimationFrame> {
return {
body, head, wing, tail,
frontLeg, frontFarLeg, backLeg, backFarLeg,
bodyX, bodyY, headX, headY,
frontLegX, frontLegY, frontFarLegX, frontFarLegY,
backLegX, backLegY, backFarLegX, backFarLegY
};
}
export function createBodyAnimation(
name: string, fps: number, loop: boolean, frames: number[][], shadowOffsets?: number[][]
): Readonly<BodyAnimation> {
if (shadowOffsets && shadowOffsets.length !== frames.length) {
throw new Error(`Incorrect frame count for shadowOffsets for ${name}`);
}
const shadow = shadowOffsets && shadowOffsets.map<BodyShadow>(([frame, offset]) => ({ frame, offset }));
return { name, loop, fps, frames: frames.map(createBodyFrame), shadow };
}
export const stand = createBodyAnimation('stand', 24, true, [
[1, 1, 0, 0, 1, 1, 1, 1],
]);
export const swim = createBodyAnimation('swim', 4, true, [
[1, 1, 0, 0, 8, 10, 6, 5, 0, 14],
[1, 1, 0, 0, 8, 10, 6, 5, 0, 13],
[1, 1, 0, 0, 8, 10, 6, 5, 0, 12],
[1, 1, 0, 0, 8, 10, 6, 5, 0, 13]
]);
export const trotToSwim = createBodyAnimation('trot-to-swim', 24, false, [
[1, 1, 0, 0, 8, 10, 6, 5, 0, 2],
[1, 1, 0, 0, 8, 10, 6, 5, 0, 8],
[1, 1, 0, 0, 8, 10, 6, 5, 0, 10],
[1, 1, 0, 0, 8, 10, 6, 5, 0, 16]
]);
export const swimToTrot = createBodyAnimation('swim-to-trot', 24, false, [
[1, 1, 0, 0, 8, 10, 6, 5, 0, 12],
[1, 1, 0, 0, 12, 3, 4, 23, 0, 8],
[1, 1, 0, 0, 14, 26, 3, 24, 0, 4],
[1, 1, 0, 0, 18, 27, 2, 5]
]);
export const flyToSwim = createBodyAnimation('fly-to-swim', 16, false, [
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -12],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -8],
[1, 1, 6, 0, 8, 10, 6, 5, 0, -2],
[1, 1, 7, 0, 8, 10, 6, 5, 0, 4],
[1, 1, 11, 0, 8, 10, 6, 5, 0, 10],
[1, 1, 1, 0, 8, 10, 6, 5, 0, 14]
]);
export const flyToSwimBug = createBodyAnimation('fly-to-swim-bug', 16, false, [
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -12],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -8],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -2],
[1, 1, 3, 0, 8, 10, 6, 5, 0, 4],
[1, 1, 4, 0, 8, 10, 6, 5, 0, 10],
[1, 1, 1, 0, 8, 10, 6, 5, 0, 14]
]);
export const swimToFly = createBodyAnimation('swim-to-fly', 16, false, [
[1, 1, 11, 0, 8, 10, 6, 5, 0, 13],
[1, 1, 12, 0, 8, 10, 6, 5, 0, 14, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1],
[2, 1, 3, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[2, 1, 4, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[2, 1, 5, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[1, 1, 6, 0, 8, 10, 6, 1, 0, 13],
[1, 1, 7, 0, 6, 7, 5, 5, 0, -7, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 8, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 9, 0, 8, 10, 6, 5, 0, -17],
[1, 1, 10, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 11, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 12, 0, 8, 10, 6, 5, 0, -17]
]);
export const swimToFlyBug = createBodyAnimation('swim-to-fly-bug', 16, false, [
[1, 1, 3, 0, 8, 10, 6, 5, 0, 13],
[1, 1, 4, 0, 8, 10, 6, 5, 0, 14, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1],
[2, 1, 5, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[2, 1, 4, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[2, 1, 5, 0, 8, 10, 6, 5, 0, 15, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[1, 1, 4, 0, 8, 10, 6, 1, 0, 13],
[1, 1, 5, 0, 6, 7, 5, 5, 0, -7, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 3, 0, 8, 10, 6, 5, 0, -17],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17]
]);
//const trotSkew = [-1, 0, 1, 0, -1, -2, -3, -2, -1, 0, 1, 0, -1, -2, -3, -2].map(x => (x + 2) * 0.25);
export const trot = createBodyAnimation('trot', 24, true, [
[1, 1, 0, 0, 2, 10, 2, 10, 0, 1, 0, -1],
[1, 1, 0, 0, 3, 11, 3, 11],
[1, 1, 0, 0, 4, 12, 4, 12, 0, -1],
[1, 1, 0, 0, 5, 13, 5, 13, 0, -2],
[1, 1, 0, 0, 6, 14, 6, 14, 0, -2],
[1, 1, 0, 0, 7, 15, 7, 15, 0, -2],
[1, 1, 0, 0, 8, 16, 8, 16, 0, -1],
[1, 1, 0, 0, 9, 17, 9, 17],
[1, 1, 0, 0, 10, 2, 10, 2, 0, 1, 0, -1],
[1, 1, 0, 0, 11, 3, 11, 3],
[1, 1, 0, 0, 12, 4, 12, 4, 0, -1],
[1, 1, 0, 0, 13, 5, 13, 5, 0, -2],
[1, 1, 0, 0, 14, 6, 14, 6, 0, -2],
[1, 1, 0, 0, 15, 7, 15, 7, 0, -2],
[1, 1, 0, 0, 16, 8, 16, 8, 0, -1],
[1, 1, 0, 0, 17, 9, 17, 9],
]);
export const boop = createBodyAnimation('boop', 24, false, [
[1, 1, 0, 0, 1, 1, 1, 1],
[1, 1, 0, 0, 18, 1, 1, 1],
[1, 1, 0, 0, 19, 1, 1, 1],
[1, 1, 0, 0, 20, 1, 1, 1],
[1, 1, 0, 0, 21, 1, 1, 1],
[1, 1, 0, 0, 22, 28, 18, 18, -1],
[1, 1, 0, 0, 23, 26, 19, 19, -2, -1],
...repeat(5, [1, 1, 0, 0, 23, 27, 20, 20, -3, -1]),
[1, 1, 0, 0, 23, 26, 19, 19, -2, -1],
[1, 1, 0, 0, 22, 1, 1, 1],
[1, 1, 0, 0, 24, 1, 1, 1],
[1, 1, 0, 0, 25, 1, 1, 1],
[1, 1, 0, 0, 18, 1, 1, 1],
[1, 1, 0, 0, 1, 1, 1, 1],
]);
export const boopSit = createBodyAnimation('boop-sit', 24, false, [
[9, 1, 2, 2, 34, 34, 26, 26],
[9, 1, 2, 2, 13, 34, 26, 26, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -2],
[9, 1, 2, 2, 19, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
[9, 1, 2, 2, 20, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
[9, 1, 2, 2, 21, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
[9, 1, 2, 2, 22, 34, 26, 26, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -2],
[9, 1, 2, 2, 23, 34, 26, 26, 0, -1, 0, 0, -1, -1, 0, 1, 0, 1, 0, -1],
...repeat(5, [9, 1, 2, 2, 23, 34, 26, 26, -1, -2, 0, 0, -2, -2, 1, 2, 1, 2, 1]),
[9, 1, 2, 2, 23, 34, 26, 26, 0, -1, 0, 0, -1, -1, 0, 1, 0, 1, 0, -1],
[9, 1, 2, 2, 22, 34, 26, 26, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, -2],
[9, 1, 2, 2, 24, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
[9, 1, 2, 2, 25, 34, 26, 26, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -2],
[9, 1, 2, 2, 12, 34, 26, 26, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -2],
[9, 1, 2, 2, 34, 34, 26, 26],
], repeat(18, [0, 6]));
export const boopLie = createBodyAnimation('boop-lie', 24, false, [
[15, 1, 0, 2, 38, 38, 26, 26],
...repeat(2, [15, 1, 0, 2, 24, 38, 26, 26, 0, 0, 0, 0, 0, 1]),
[15, 1, 0, 2, 21, 38, 26, 26, 0, 0, 0, 0, 0, 1],
[15, 1, 0, 2, 22, 38, 26, 26, 0, 0, 0, 0, 0, 1],
[15, 1, 0, 2, 23, 38, 26, 26, 0, 0, -1, -1, 0, 1],
[12, 1, 0, 2, 23, 37, 26, 26, -1, 0, 0, 0, -1, 1, 0, 0, 1, 0, 1],
...repeat(4, [12, 1, 0, 2, 23, 37, 26, 26, -1, 0, 0, 0, -2, 1, 0, 0, 1, 0, 1]),
[15, 1, 0, 2, 23, 38, 26, 26, 0, 0, 0, 0, 0, 1],
[15, 1, 0, 2, 22, 38, 26, 26, 0, 0, 0, 0, 0, 1],
[15, 1, 0, 2, 21, 38, 26, 26, 0, 0, 0, 0, 0, 1],
], repeat(14, [3, 3]));
export const boopSwim = createBodyAnimation('boop-swim', 24, false, [
[1, 1, 0, 0, 1, 10, 6, 5, 0, 13],
[1, 1, 0, 0, 18, 10, 6, 5, 0, 13],
[1, 1, 0, 0, 19, 10, 6, 5, 0, 13],
[1, 1, 0, 0, 20, 10, 6, 5, 0, 13],
[1, 1, 0, 0, 21, 10, 6, 5, 0, 12],
[1, 1, 0, 0, 22, 9, 6, 5, -1, 12],
[1, 1, 0, 0, 23, 8, 6, 5, -2, 11],
...repeat(5, [1, 1, 0, 0, 23, 8, 6, 5, -3, 11]),
[1, 1, 0, 0, 23, 9, 6, 5, -2, 11],
[1, 1, 0, 0, 22, 10, 6, 5, 0, 13],
[1, 1, 0, 0, 24, 10, 6, 5, 0, 13],
[1, 1, 0, 0, 25, 10, 6, 5, 0, 14],
[1, 1, 0, 0, 18, 10, 6, 5, 0, 14],
[1, 1, 0, 0, 1, 10, 6, 5, 0, 14]
]);
export const sit = createBodyAnimation('sit', 24, true, [
[9, 1, 2, 2, 34, 34, 26, 26],
], [[0, 6]]);
const sitShadow = [0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 6].map(offset => [0, offset]);
export const sitDown = createBodyAnimation('sit-down', 24, false, [
[1, 1, 0, 0, 1, 1, 1, 1],
...repeat(2, [2, 1, 0, 0, 29, 29, 1, 1]),
...repeat(2, [3, 1, 0, 0, 30, 30, 21, 21]),
[4, 1, 0, 0, 31, 31, 22, 22],
[5, 1, 0, 1, 32, 32, 23, 23],
[6, 1, 1, 2, 33, 33, 24, 24],
[7, 1, 2, 2, 34, 34, 25, 25],
[8, 1, 2, 2, 34, 34, 25, 25],
[9, 1, 2, 2, 34, 34, 26, 26],
], sitShadow);
export const standUp = createBodyAnimation('stand-up', 24, false, [
[9, 1, 2, 2, 34, 34, 26, 26],
[8, 1, 2, 2, 34, 34, 25, 25],
[7, 1, 2, 2, 34, 34, 25, 25],
[6, 1, 1, 2, 33, 33, 24, 24],
[5, 1, 0, 1, 32, 32, 23, 23],
[4, 1, 0, 0, 31, 31, 22, 22],
...repeat(2, [3, 1, 0, 0, 30, 30, 21, 21]),
[1, 1, 0, 0, 1, 1, 1, 1],
], sitShadow.slice(2).reverse());
export const sitToTrot = createBodyAnimation('sit-to-trot', 24, false, [
[7, 1, 2, 2, 34, 35, 24, 25, 0, -1, 0, 0, 0, 2, 1, 1, 0, -1],
[6, 1, 1, 2, 27, 36, 23, 24, 0, -2, 0, 0, 0, -2, 0, 0, 0, 1],
[5, 1, 0, 1, 5, 13, 5, 23, 0, -2],
[4, 1, 0, 0, 6, 14, 6, 5, 0, -2],
[3, 1, 0, 0, 7, 15, 7, 15, 0, -2],
[2, 1, 0, 0, 8, 16, 8, 16, 0, -1],
], [[0, 6], [0, 5], [0, 4], [0, 3], [0, 1], [0, 0]]);
export const lie = createBodyAnimation('lie', 24, true, [
[15, 1, 0, 2, 38, 38, 26, 26],
], [[3, 3]]);
const lieShadow = [[0, 6], [0, 6], [1, 5], [2, 4], [3, 3], [3, 3], [3, 3]];
export const lieDown = createBodyAnimation('lie-down', 24, false, [
[9, 1, 2, 2, 34, 34, 26, 26],
[10, 1, 2, 2, 35, 34, 26, 26, 0, 0, 0, 0, 0, 0, 0, -1],
[11, 1, 1, 2, 36, 36, 26, 26, 0, 0, 0, 0, 0, 0, 1],
[12, 1, 1, 2, 37, 37, 26, 26, 0, 0, 0, 0, 0, 0, 1],
[13, 1, 0, 2, 38, 38, 26, 26, 0, 0, 0, 0, 0, 0, 1],
...repeat(2, [14, 1, 0, 2, 38, 38, 26, 26]),
], lieShadow);
export const sitUp = createBodyAnimation('sit-up', 24, false, [
...repeat(2, [14, 1, 0, 2, 38, 38, 26, 26]),
[13, 1, 0, 2, 38, 38, 26, 26],
[12, 1, 1, 2, 37, 37, 26, 26],
[11, 1, 1, 2, 36, 36, 26, 26],
[10, 1, 2, 2, 35, 34, 26, 26, 0, 0, 0, 0, 0, 0, 0, -1],
[9, 1, 2, 2, 34, 34, 26, 26],
], lieShadow.slice().reverse());
export const lieToTrot = createBodyAnimation('lie-to-trot', 24, false, [
[1, 1, 0, 1, 36, 37, 24, 25, 4, 8, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 1, 0, 0, 30, 12, 23, 24, 2, 5, 0, 0, 0, -3],
[1, 1, 0, 0, 5, 13, 5, 23, 1, 1],
[1, 1, 0, 0, 6, 14, 6, 21, 0, 0, 0, -1],
[1, 1, 0, 0, 7, 15, 7, 15, 0, -1, 0, -1],
[1, 1, 0, 0, 8, 16, 8, 16, 0, -2],
], [[2, 1], [1, 0], ...repeat(4, [0, 0])]);
export const fly = createBodyAnimation('fly', 16, true, [
[1, 1, 3, 0, 8, 10, 6, 5, 0, -16],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 6, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 7, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 8, 0, 8, 10, 6, 5, 0, -17],
[1, 1, 9, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 10, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 11, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 12, 0, 8, 10, 6, 5, 0, -17],
]);
export const boopFly = createBodyAnimation('boop-fly', 16, false, [
[1, 1, 3, 0, 8, 10, 6, 5, 0, -16],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 5, 0, 20, 10, 6, 5, 0, -14],
[1, 1, 6, 0, 21, 10, 6, 5, 0, -14],
[1, 1, 7, 0, 22, 10, 6, 5, -1, -15],
[1, 1, 8, 0, 23, 10, 5, 5, -1, -17, -1, 0, -1, -1, 2],
[1, 1, 9, 0, 23, 10, 4, 5, -1, -18, -1, 0, -1, -1, 2],
[1, 1, 10, 0, 23, 10, 4, 4, -1, -18, -1, 0, -1, -1, 2],
[1, 1, 11, 0, 23, 10, 4, 3, -1, -18, -1, 0, -1, -1, 2],
[1, 1, 12, 0, 22, 10, 4, 3, 0, -17, -1, 0, 0, 0, 2],
[1, 1, 3, 0, 21, 10, 5, 4, 0, -16, 0, 0, 0, 0, 2],
[1, 1, 4, 0, 14, 10, 6, 5, 0, -15, 0, 0, 0, 0, 2]
]);
export const boopFlyBug = createBodyAnimation('boop-fly-bug', 20, false, [
[1, 1, 3, 0, 8, 10, 6, 5, 0, -16],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 5, 0, 20, 10, 6, 5, 0, -14],
[1, 1, 4, 0, 21, 10, 6, 5, 0, -14],
[1, 1, 3, 0, 22, 10, 6, 5, -1, -15],
[1, 1, 4, 0, 23, 10, 5, 5, -1, -17, -1, 0, -1, -1, 2],
[1, 1, 5, 0, 23, 10, 4, 5, -1, -18, -1, 0, -1, -1, 2],
[1, 1, 4, 0, 23, 10, 4, 4, -1, -18, -1, 0, -1, -1, 2],
[1, 1, 3, 0, 23, 10, 4, 3, -1, -18, -1, 0, -1, -1, 2],
[1, 1, 4, 0, 22, 10, 4, 3, 0, -17, -1, 0, 0, 0, 2],
[1, 1, 5, 0, 21, 10, 5, 4, 0, -16, 0, 0, 0, 0, 2],
[1, 1, 4, 0, 14, 10, 6, 5, 0, -15, 0, 0, 0, 0, 2]
]);
export const flyUp = createBodyAnimation('fly-up', 16, false, [
[1, 1, 11, 0, 1, 1, 1, 1],
[1, 1, 12, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1],
[2, 1, 3, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[2, 1, 4, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[2, 1, 5, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[1, 1, 6, 0, 1, 1, 1, 1],
[1, 1, 7, 0, 6, 7, 5, 5, 0, -10, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 8, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 9, 0, 8, 10, 6, 5, 0, -17],
[1, 1, 10, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 11, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 12, 0, 8, 10, 6, 5, 0, -17],
]);
export const trotToFly = createBodyAnimation('trot-to-fly', 20, false, [
[1, 1, 11, 0, 6, 14, 6, 14, 0, -2],
[1, 1, 12, 0, 7, 15, 7, 15, 0, -2],
[1, 1, 3, 0, 8, 16, 8, 16, 0, -1],
[1, 1, 4, 0, 9, 17, 9, 17, 0, 1, 0, 1, 0, 0, 0, -1, 0, -1],
[1, 1, 5, 0, 10, 2, 10, 2, 0, 3, 0, 1, 0, 0, 0, -2, 0, -2],
[1, 1, 6, 0, 11, 3, 11, 3],
[1, 1, 7, 0, 11, 4, 11, 4, 0, -10],
[1, 1, 8, 0, 10, 5, 9, 5, 0, -15],
[1, 1, 9, 0, 9, 10, 6, 6, 0, -17],
[1, 1, 10, 0, 8, 10, 6, 7, 0, -18],
[1, 1, 11, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 12, 0, 8, 10, 6, 5, 0, -17]
]);
export const trotToFlyBug = createBodyAnimation('trot-to-fly-bug', 20, false, [
[1, 1, 3, 0, 6, 14, 6, 14, 0, -2],
[1, 1, 4, 0, 7, 15, 7, 15, 0, -2],
[1, 1, 5, 0, 8, 16, 8, 16, 0, -1],
[1, 1, 3, 0, 9, 17, 9, 17, 0, 1, 0, 1, 0, 0, 0, -1, 0, -1],
[1, 1, 4, 0, 10, 2, 10, 2, 0, 3, 0, 1, 0, 0, 0, -2, 0, -2],
[1, 1, 5, 0, 11, 3, 11, 3],
[1, 1, 3, 0, 11, 4, 11, 4, 0, -10],
[1, 1, 4, 0, 10, 5, 9, 5, 0, -15],
[1, 1, 5, 0, 8, 10, 6, 6, 0, -17],
[1, 1, 3, 0, 8, 10, 6, 7, 0, -18],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -17]
]);
export const flyToTrot = createBodyAnimation('fly-to-trot', 20, false, [
[1, 1, 3, 0, 8, 10, 6, 10, 0, -16],
[1, 1, 4, 0, 8, 11, 5, 11, 0, -15],
[1, 1, 5, 0, 8, 12, 4, 12, 0, -12],
[1, 1, 6, 0, 7, 13, 5, 13, 0, -8],
[1, 1, 6, 0, 7, 14, 6, 14, 0, -6],
[1, 1, 6, 0, 7, 15, 7, 15, 0, -4],
[1, 1, 7, 0, 8, 16, 8, 16, 0, -1],
[1, 1, 11, 0, 9, 17, 9, 17],
[1, 1, 0, 0, 10, 2, 10, 2, 0, 3, 0, -1, 0, 0, 0, -2, 0, -2]
// [1, 1, 4, 0, 8, 10, 6, 10, 0, -16],
// [1, 1, 5, 0, 8, 10, 6, 10, 0, -18],
// [1, 1, 7, 0, 8, 11, 5, 11, 0, -20],
// [1, 1, 8, 0, 10, 12, 4, 12, 0, -21],
// [1, 1, 9, 0, 11, 13, 4, 13, 0, -20],
// [1, 1, 10, 0, 12, 14, 5, 14, 0, -18],
// [1, 1, 11, 0, 13, 15, 7, 15, 0, -14, 0, -1],
// [1, 1, 4, 0, 14, 16, 8, 16, 0, 0, 0, -1],
// [1, 1, 5, 0, 2, 17, 9, 17, 0, 2, 0, 0, 0, -1, 0, -2, 0, -2, 0, -2],
// [1, 1, 6, 0, 3, 2, 10, 2, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 2]
]);
export const flyToTrotBug = createBodyAnimation('fly-to-trot-bug', 20, false, [
[1, 1, 3, 0, 8, 10, 6, 10, 0, -16],
[1, 1, 4, 0, 8, 11, 5, 11, 0, -15],
[1, 1, 5, 0, 8, 12, 4, 12, 0, -12],
[1, 1, 4, 0, 7, 13, 5, 13, 0, -8],
[1, 1, 3, 0, 7, 14, 6, 14, 0, -6],
[1, 1, 4, 0, 7, 15, 7, 15, 0, -4],
[1, 1, 5, 0, 8, 16, 8, 16, 0, -1],
[1, 1, 4, 0, 9, 17, 9, 17],
[1, 1, 3, 0, 10, 2, 10, 2, 0, 3, 0, -1, 0, 0, 0, -2, 0, -2]
// [1, 1, 4, 0, 8, 10, 6, 10, 0, -16],
// [1, 1, 5, 0, 8, 10, 6, 10, 0, -18],
// [1, 1, 4, 0, 8, 11, 5, 11, 0, -20],
// [1, 1, 3, 0, 10, 12, 4, 12, 0, -21],
// [1, 1, 4, 0, 11, 13, 4, 13, 0, -20],
// [1, 1, 5, 0, 12, 14, 5, 14, 0, -18],
// [1, 1, 4, 0, 13, 15, 7, 15, 0, -14, 0, -1],
// [1, 1, 3, 0, 14, 16, 8, 16, 0, 0, 0, -1],
// [1, 1, 4, 0, 2, 17, 9, 17, 0, 2, 0, 0, 0, -1, 0, -2, 0, -2, 0, -2],
// [1, 1, 5, 0, 3, 2, 10, 2, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 2]
]);
export const flyDown = createBodyAnimation('fly-down', 16, false, [
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -12],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -10],
[1, 1, 6, 0, 8, 10, 6, 5, 0, -8],
[1, 1, 7, 0, 8, 10, 6, 5, 0, -6],
[1, 1, 11, 0, 8, 10, 6, 5, 0, -4],
[1, 1, 1, 0, 8, 10, 6, 5, 0, -2]
]);
export const flyBug = createBodyAnimation('fly-bug', 24, true, [
[1, 1, 3, 0, 8, 10, 6, 5, 0, -16],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -16],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 3, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -16],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17],
[1, 1, 3, 0, 8, 10, 6, 5, 0, -17],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 3, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -17],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17]
]);
export const flyUpBug = createBodyAnimation('fly-up-bug', 16, false, [
[1, 1, 3, 0, 1, 1, 1, 1],
[1, 1, 4, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1],
[2, 1, 5, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[2, 1, 4, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[2, 1, 5, 0, 29, 29, 21, 21, 0, 2, 0, 2, 0, -2, 0, -2, 2, -2, 2, -2],
[1, 1, 4, 0, 1, 1, 1, 1],
[1, 1, 5, 0, 6, 7, 5, 5, 0, -10, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -15],
[1, 1, 3, 0, 8, 10, 6, 5, 0, -17],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -18],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -17]
]);
export const flyDownBug = createBodyAnimation('fly-down-bug', 16, false, [
[1, 1, 3, 0, 8, 10, 6, 5, 0, -14],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -12],
[1, 1, 5, 0, 8, 10, 6, 5, 0, -10],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -8],
[1, 1, 3, 0, 8, 10, 6, 5, 0, -6],
[1, 1, 4, 0, 8, 10, 6, 5, 0, -4],
[1, 1, 1, 0, 8, 10, 6, 5, 0, -2]
]);
export const swing = createBodyAnimation('swing', 12, false, [
...repeat(1, [1, 1, 0, 0, 1, 1, 1, 1]),
...repeat(3, [2, 1, 0, 0, 12, 17, 11, 11, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]),
]);
export const flyAnims = [undefined, fly, fly, fly, flyBug];
export const flyUpAnims = [undefined, flyUp, flyUp, flyUp, flyUpBug];
export const flyDownAnims = [undefined, flyDown, flyDown, flyDown, flyDownBug];
export const animations = [
stand, trot, boop, boopSit, boopLie, boopSwim, boopFly, boopFlyBug, sit, sitDown, standUp, lie, lieDown, sitUp,
fly, flyBug, flyUp, flyUpBug, flyDown, flyDownBug, sitToTrot, lieToTrot, flyToTrot, flyToTrotBug,
swim, trotToSwim, swimToTrot, flyToSwim, swimToFly,
];
export const sitDownUp = mergeAnimations('sit', 24, false, [...repeat(12, stand), sitDown, ...repeat(12, sit), standUp]);
export const lieDownUp = mergeAnimations('lie', 24, false, [...repeat(12, sit), lieDown, ...repeat(12, lie), sitUp]);
export function mergeAnimations(name: string, fps: number, loop: boolean, animations: BodyAnimation[]): BodyAnimation {
return {
name,
fps,
loop,
frames: flatten(animations.map(a => a.frames)),
shadow: flatten(animations.map(a => a.shadow || a.frames.map(() => ({ frame: 0, offset: 0 })))),
};
}
// head animations
export function createHeadFrame([headX = 0, headY = 0, left = 0, right = 0, mouth = 0]: number[]): HeadAnimationFrame {
return { headX, headY, left, right, mouth };
}
export function createHeadAnimation(name: string, fps: number, loop: boolean, frames: number[][]): HeadAnimation {
return { name, fps, loop, frames: frames.map(createHeadFrame) };
}
export const smile = createHeadAnimation('smile', 24, true, [
[0, 0, 1, 1, 0],
]);
export const nom = createHeadAnimation('nom', 12, true, [
[0, 0, 1, 1, 0],
[0, 0, 1, 1, 25],
]);
export const laugh = createHeadAnimation('laugh', 8, false, [
...repeat(4, [0, 0, 14, 14, 5], [0, 1, 14, 14, 5]),
]);
export const yawn = createHeadAnimation('yawn', 12, false, [
[0, 0, 3, 3, 8],
...repeat(18, [1, -1, 12, 12, 16]),
...repeat(8, [0, 0, 12, 12, 12]),
[0, 0, 18, 18, 2],
]);
export const surprise = createHeadAnimation('surprise', 8, false, [
[0, 1, 6, 6, 1],
...repeat(10, [0, 0, 1, 1, 12]),
]);
export const excite = createHeadAnimation('excite', 8, false, [
[0, 1, 6, 6, 0],
...repeat(10, [0, 0, 1, 1, 5]),
]);
export const surpriseSad = createHeadAnimation('surpriseSad', 8, false, [
[0, 1, 15, 15, 8],
...repeat(8, [0, 0, 15, 15, 8]),
]);
export const sneeze = createHeadAnimation('sneeze', 12, false, [
[0, 0, 18, 18, 8],
...repeat(2, [1, -1, 18, 18, 16]),
...repeat(8, [-1, 1, 23, 23, 13]),
...repeat(4, [0, 0, 18, 18, 7]),
]);
export const headAnimations = [
smile, nom, laugh, yawn, surprise, surpriseSad, sneeze, excite,
];
// default animations
export const defaultBodyAnimation = createBodyAnimation('default', 24, true, [[1, 1, 0, 0, 1, 1, 1, 1]]);
export const defaultHeadAnimation = createHeadAnimation('default', 24, true, [[0, 0, -1, -1, -1]]);
export const defaultBodyFrame = defaultBodyAnimation.frames[0];
export const defaultHeadFrame = defaultHeadAnimation.frames[0];
+790
View File
@@ -0,0 +1,790 @@
import {
PonyEye, PonyState, PalettePonyInfo, PaletteSpriteSet, Palette, HeadAnimationFrame,
Eye, Iris, ColorExtraSets, ExpressionExtra, BodyAnimationFrame, DrawPonyOptions, Muzzle, BodyShadow, DrawOptions,
NoDraw, PaletteSpriteBatch, defaultDrawOptions, PonyStateFlags, PaletteManager, isEyeSleeping, Matrix2D,
} from '../common/interfaces';
import { WHITE, SHINES_COLOR, FAR_COLOR, TRANSPARENT, fillToOutlineColor } from '../common/colors';
import { toInt, hasFlag, repeat, flatten, point } from '../common/utils';
import * as sprites from '../generated/sprites';
import * as offsets from '../common/offsets';
import { defaultHeadAnimation, defaultBodyFrame, defaultHeadFrame } from './ponyAnimations';
import { toWorldX, toWorldY } from '../common/positionUtils';
import {
frontHooves, PONY_WIDTH, PONY_HEIGHT, wings, chestBehind, tails, chest, neckAccessories, waistAccessories,
SLEEVED_ACCESSORIES, blinkFrames, flipIris, claws, Sets, backAccessories, SLEEVED_BACK_ACCESSORIES,
CHEST_ACCESSORIES_IN_FRONT, flipFaceAccessoryType, flipFaceAccessoryPattern, backLegSleeves,
NO_MANE_HEAD_ACCESSORIES, backHoovesInFront, frontHoovesInFront
} from './ponyUtils';
import { HEAD_ACCESSORY_OFFSETS, EAR_ACCESSORY_OFFSETS, EXTRA_ACCESSORY_OFFSETS } from '../common/offsets';
import { createMat2D, identityMat2D, translateMat2D, copyMat2D, rotateMat2D, scaleMat2D } from '../common/mat2d';
import { darkenForOutline } from '../common/ponyInfo';
type Batch = PaletteSpriteBatch;
type Info = Readonly<PalettePonyInfo>;
type State = Readonly<PonyState>;
type Options = Readonly<DrawPonyOptions>;
const holdingDrawOptions: DrawOptions = {
...defaultDrawOptions,
shadowColor: TRANSPARENT,
};
function checker(parts: number[]) {
const set = new Set(parts);
return (part: number) => set.has(part);
}
function partChecker(parts: number[]) {
const check = checker(parts);
return (set: PaletteSpriteSet | undefined) => set !== undefined && !!set.type && check(set.type);
}
const SHADOW_OX = 20;
const SHADOW_OY = 64;
const FAR_OX = -3;
const FAR_OY = -1;
const FAR_WING_OX = -4;
const FAR_WING_OY = 0;
const hasNoMane = checker(NO_MANE_HEAD_ACCESSORIES);
const behindBackAccessory = partChecker([1]);
const sleevedAccessory = partChecker(SLEEVED_ACCESSORIES);
const sleevedBackAccessory = partChecker(SLEEVED_BACK_ACCESSORIES);
const chestAccessoryInFront = partChecker(CHEST_ACCESSORIES_IN_FRONT);
const pointZero = point(0, 0);
const headFlipOffsetX = 35.5;
const headFlipOffsetY = 42;
const headTransform = createMat2D();
function clamp(value: number, min: number, max: number): number {
return value > min ? (value < max ? value : max) : min;
}
function at<T>(items: T[], index: any): T | undefined {
// if (DEVELOPMENT) {
// if (items.length === 0) {
// console.warn(`Empty array at: ${getStackLocation(2)} / ${getStackLocation(3)}`);
// } else if (index < 0 || index >= items.length) {
// console.warn(`Index out of range ${index} of 0..${items.length} at: ${getStackLocation(2)} / ${getStackLocation(3)}`);
// }
// }
return items[clamp(index | 0, 0, items.length - 1)];
}
function att<T>(items: T[] | undefined, index: any): T | undefined {
return items && items[clamp(index | 0, 0, items.length - 1)];
}
function atDef<T>(items: T[] | undefined, index: number, def: T): T {
return (items && items.length > 0 && index >= 0 && index < items.length) ? items[index | 0] : def;
}
export function getPonyAnimationFrame<T>({ frames }: { frames: T[] }, frame: number, defaultFrame: T): T {
return frames.length > 0 ? frames[Math.max(0, frame) % frames.length] : defaultFrame;
}
function getHeadXY(x: number, y: number, turned: boolean, frame: BodyAnimationFrame, headFrame: HeadAnimationFrame) {
const headOffset = at(offsets.headOffsets, frame.body)!;
const headX = x + frame.headX + (headFrame.headX * (turned ? -1 : 1)) + headOffset.x;
const headY = y + frame.headY + headFrame.headY + headOffset.y;
return { headX, headY };
}
export function getPonyHeadPosition(state: State, ponyX: number, ponyY: number) {
const frame = getPonyAnimationFrame(state.animation, state.animationFrame, defaultBodyFrame);
const headFrame = getPonyAnimationFrame(state.headAnimation || defaultHeadAnimation, state.headAnimationFrame, defaultHeadFrame);
const baseX = ponyX - PONY_WIDTH / 2;
const baseY = ponyY - PONY_HEIGHT;
const x = baseX + frame.bodyX;
const y = baseY + frame.bodyY;
const { headX, headY } = getHeadXY(x, y, state.headTurned, frame, headFrame);
return { x: headX, y: headY };
}
export function createHeadTransform(
originalTransform: Matrix2D | undefined, headX: number, headY: number, { headTilt, headTurned }: State
) {
if (originalTransform !== undefined) {
copyMat2D(headTransform, originalTransform);
} else {
identityMat2D(headTransform);
}
translateMat2D(headTransform, headTransform, headX + headFlipOffsetX, headY + headFlipOffsetY);
if (headTilt) {
rotateMat2D(headTransform, headTransform, headTilt * 0.1);
}
scaleMat2D(headTransform, headTransform, headTurned ? -1 : 1, 1);
translateMat2D(headTransform, headTransform, -headFlipOffsetX, -headFlipOffsetY);
return headTransform;
}
export function getHeadY(frame: BodyAnimationFrame, headFrame: HeadAnimationFrame): number {
const headOffset = offsets.headOffsets[frame.body];
return frame.bodyY + frame.headY + headFrame.headY + headOffset.y;
}
const defaultShadow: BodyShadow = { frame: 0, offset: 0 };
const hairOffsets = [
0, 0, 0, 0,
-1, -1, 0, 0,
0, 0, 0, -1,
-1, 0, 0, 0,
0, 0, 0, 0,
...repeat(100, 0),
];
function draw(options: Options, flag: NoDraw) {
if (TOOLS) {
return !hasFlag(options.no, flag);
} else {
return true;
}
}
const headOffsetsX = [0, 1, 1, 1, 1, 1, 0];
const headOffsetsY = [0, 0, 1, 1, 0, 0, 0];
let toys: (PaletteSpriteSet | undefined)[] = [];
export function initializeToys(paletteManager: PaletteManager) {
function set(type: number, pattern: number, colors: number[]): PaletteSpriteSet {
const palette = [
TRANSPARENT,
...flatten(colors.map(color => [color, darkenForOutline(fillToOutlineColor(color))]))
];
return { type, pattern, palette: paletteManager.add(palette) };
}
toys = [
undefined,
// hat
set(2, 0, [0xffffffff, 0xff2525ff]),
set(2, 0, [0xffffffff, 0x22ac22ff]),
set(2, 0, [0xffffffff, 0x2e58f4ff]),
set(2, 0, [0xffffffff, 0xff71ffff]),
// snowpony
set(3, 0, [0xffffffff, 0x000000ff, 0xff9100ff, 0xff0000ff]),
set(4, 0, [0xffffffff, 0x000000ff, 0xff9100ff, 0xff0000ff]),
set(4, 0, [0x404040ff, 0xff0000ff, 0x000000ff, 0xff0000ff]),
// gift
set(6, 0, [0xcf1717ff, 0xecd132ff]),
set(6, 0, [0xdfc588ff, 0xe7559bff]),
set(6, 0, [0x7fc484ff, 0x4a79daff]),
set(6, 0, [0xd56a69ff, 0x62ab64ff]),
set(6, 0, [0xe586dfff, 0x9553c1ff]),
// hanging thing
set(5, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0xb9c0d8ff, 0xffd94fff, 0xeca242ff]), // bell
set(7, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0xb9c0d8ff, 0xc0ccc4ff]), // mistletoe
set(17, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0x000000ff, 0xe7b86fff, 0x3f1d0fff]), // cookie
set(10, 0, [0x91622fff, 0xc02455ff, 0x429a51ff, 0x000000ff]), // spider
// teddy
set(8, 0, [0xa86230ff, 0xdfbe8bff, TRANSPARENT, TRANSPARENT]), // brown
set(8, 0, [0xa86230ff, 0xdfbe8bff, 0xffa500ff, 0xffffffff]), // brown angel
set(8, 0, [0x474444ff, 0x96623eff, TRANSPARENT, TRANSPARENT]), // black
set(8, 0, [0x474444ff, 0x96623eff, 0xffa500ff, 0xffffffff]), // black angel
set(9, 1, [0xa86230ff, 0xdfbe8bff, 0xff0000ff, TRANSPARENT, 0xf5f5f5ff]), // brown clothes
set(9, 1, [0x474444ff, 0x96623eff, 0xff0000ff, TRANSPARENT, 0xf5f5f5ff]), // black clothes
set(9, 0, [0xdce5edff, 0xffffffff, 0xff0000ff, 0xdfbe8bff, 0x645137ff]), // white santa
// xmas tree
set(11, 0, [0x1a9b2fff, 0x56c7ffff, 0xde4d68ff, 0xf1d224ff]),
set(11, 1, [0x1a9b2fff, 0xf1d224ff, 0x1a9b2fff, 0xde4d68ff]),
// deer
set(12, 0, [0x7b4b24ff, 0xcf0e0eff, 0xbfaa8cff]),
set(16, 0, [0x7b4b24ff, 0xcf0e0eff, 0xbfaa8cff, 0xffffffff, 0x56c7ffff, 0xf1d224ff]),
// candy horns
set(13, 0, [0xffffffff, 0xff1b1bff, TRANSPARENT, TRANSPARENT]), // one
set(13, 0, [0xffffffff, 0xff1b1bff, 0xffffffff, 0xff1b1bff]), // two
set(13, 0, [0x58df6aff, 0xffffffff, 0x3387e9ff, 0xffffffff]), // two (alt)
// star
set(14, 0, [0xffd94fff, 0xeca242ff]),
// halo
set(15, 0, [0xffd94fff, 0xeca242ff]),
];
if (DEVELOPMENT && toys.length > 33) {
console.error('too many toys', toys.length);
}
}
const zeroPoint = point(0, 0);
const wakes = [
{ ox: 21, oy: 60, behind: sprites.pony_wake_4, front: sprites.pony_wake_3 },
{ ox: 24, oy: 60, behind: sprites.pony_wake_6, front: sprites.pony_wake_5 },
{ ox: 18, oy: 51, behind: sprites.pony_wake_2, front: sprites.pony_wake_1 },
];
const wakeIndices = [0, 2, 1, 0, 2, 2, 2, 0, 2, 2, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1];
function getWakeIndex(info: Info) {
const tail = info.tail && info.tail.type || 0;
return wakeIndices[tail];
}
export function drawPony(batch: Batch, info: Info, state: State, ponyX: number, ponyY: number, options: Options) {
const frame = getPonyAnimationFrame(state.animation, state.animationFrame, defaultBodyFrame);
const headFrame = getPonyAnimationFrame(state.headAnimation || defaultHeadAnimation, state.headAnimationFrame, defaultHeadFrame);
const baseX = ponyX - PONY_WIDTH / 2;
const baseY = ponyY - PONY_HEIGHT;
const x = baseX + frame.bodyX;
const y = baseY + frame.bodyY;
const body = frame.body;
const { headX, headY } = getHeadXY(x, y, state.headTurned, frame, headFrame);
const frontLegOffset = at(offsets.frontLegOffsets, body)!;
const backLegOffset = at(offsets.backLegOffsets, body)!;
const wingOffset = at(offsets.wingOffsets, body)!;
const chestOffset = at(offsets.chestAccessoryOffsets, body)!;
const chestX = x + chestOffset.x;
const chestY = y + chestOffset.y;
const shadow = atDef(state.animation.shadow, state.animationFrame, defaultShadow);
const backOffset = at(offsets.backAccessoryOffsets, body)!;
const wing = at(wings, frame.wing);
const flipped = options.flipped;
const headOffset = clamp(state.headTurn, 0, headOffsetsX.length - 1);
const headOffsetX = headOffsetsX[headOffset];
const headOffsetY = headOffsetsY[headOffset];
const headTotalY = headY + headOffsetY;
const headTransform = createHeadTransform(undefined, headX + headOffsetX, headTotalY, state);
const headCropY = 42 - ((headTotalY + headFlipOffsetY) - baseY);
const cropW = 80;
const cropH = 65;
const shadowX = baseX + shadow.offset + SHADOW_OX;
const shadowY = baseY + SHADOW_OY;
const wake = wakes[getWakeIndex(info)];
const wakeX = baseX + wake.ox;
const wakeY = baseY + wake.oy;
const wakeFrame = Math.floor(options.gameTime * 7 / 1000) % wake.behind.frames.length;
const swimming = options.swimming;
if (swimming) {
batch.drawSprite(wake.behind.frames[wakeFrame], WHITE, info.waterPalette, wakeX, wakeY);
batch.crop(-40, -70, cropW, cropH);
}
// selection
if (options.selected) {
const sprite = at(sprites.ponySelections, shadow.frame);
sprite && batch.drawSprite(sprite, WHITE, info.defaultPalette, shadowX, shadowY);
}
// shadow
if (options.shadow) {
const sprite = at(sprites.ponyShadows, shadow.frame);
sprite && batch.drawSprite(sprite, options.shadowColor, info.defaultPalette, shadowX, shadowY);
}
// head accessory
const bounce = BETA && options.bounce;
const maneOffsetY = bounce ? hairOffsets[state.animationFrame] : 0;
const maneBehindOffsetY = bounce ? hairOffsets[state.animationFrame] : 0;
const hatOffsetY = maneBehindOffsetY;
let hatOffset = at(HEAD_ACCESSORY_OFFSETS, info.mane ? info.mane.type : 0)!;
const noMane = !info.mane || hasNoMane(info.mane.type);
if (info.headAccessory !== undefined && info.headAccessory.type === 20) {
hatOffset = zeroPoint;
}
if (draw(options, NoDraw.Behind)) {
// far wing
drawSet(batch, wing, info.wings, x + FAR_WING_OX + wingOffset.x, y + FAR_WING_OY + wingOffset.y, FAR_COLOR);
batch.save();
batch.multiplyTransform(headTransform);
if (swimming) {
// batch.drawRect(0xffff0066, 0, headCropY, cropW, cropH);
batch.crop(0, headCropY, cropW, cropH);
}
if (noMane) {
drawSet(batch, sprites.headAccessoriesBehind, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE);
}
if (draw(options, NoDraw.FarEar)) {
drawSet(batch, sprites.earAccessoriesBehind, info.earAccessory, 0, 0, WHITE);
}
}
if (draw(options, NoDraw.Body) && draw(options, NoDraw.FarEar)) {
drawSet(batch, sprites.earsFar, info.ears, 0, 0, draw(options, NoDraw.FarEarShade) ? FAR_COLOR : WHITE);
}
if (draw(options, NoDraw.Behind)) {
drawSet(batch, sprites.hornsBehind, info.horn, 0, 0, WHITE);
if (!noMane) {
drawSet(batch, sprites.headAccessoriesBehind, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE);
}
drawSet(batch, sprites.backBehindManes, info.backMane, 0, maneBehindOffsetY, WHITE);
if (!state.headTurned) {
drawSet(batch, sprites.behindManes, info.mane, 0, maneBehindOffsetY, WHITE);
}
batch.restore();
// chest accessory behind
drawSet(batch, chestBehind[body], info.chestAccessory, chestX, chestY, WHITE);
}
// legs
const behindX = x + FAR_OX;
const behindY = y + FAR_OY;
const hasTailAccessory = behindBackAccessory(info.backAccessory);
const hasSleeves = sleevedAccessory(info.chestAccessory);
const hasBackSleeves = sleevedBackAccessory(info.backAccessory);
const frontBehindX = behindX + frontLegOffset.x + frame.frontFarLegX;
const frontBehindY = behindY + frontLegOffset.y + frame.frontFarLegY;
const backBehindX = behindX + backLegOffset.x + frame.backFarLegX;
const backBehindY = behindY + backLegOffset.y + frame.backFarLegY;
// far leg back
if (draw(options, NoDraw.BackFarLeg)) {
drawLeg(batch, backBehindX, backBehindY, frame.backFarLeg, sprites.backLegs,
sprites.backLegHooves, sprites.backLegAccessories, info.backLegs,
flipped ? info.backLegAccessory : info.backLegAccessoryRight, info.backHooves, backHoovesInFront, FAR_COLOR,
undefined, false, 0, 0);
}
// far leg front
if (draw(options, NoDraw.FrontFarLeg)) {
drawLeg(batch, frontBehindX, frontBehindY, frame.frontFarLeg, sprites.frontLegs,
frontHooves, sprites.frontLegAccessories, info.frontLegs,
flipped ? info.frontLegAccessory : info.frontLegAccessoryRight, info.frontHooves, frontHoovesInFront, FAR_COLOR,
undefined, false, 0, 0);
}
// far leg back sleeve
if (draw(options, NoDraw.FarSleeves) && hasBackSleeves) {
drawSet(batch, at(sprites.backLegSleeves, frame.backFarLeg), info.backAccessory, backBehindX, backBehindY, FAR_COLOR);
}
// far leg front sleeve
if (draw(options, NoDraw.FarSleeves) && hasSleeves) {
drawSet(batch, at(sprites.frontLegSleeves, frame.frontFarLeg), info.sleeveAccessory, frontBehindX, frontBehindY, FAR_COLOR);
}
// tail
const tailOffset = at(offsets.tailOffsets, body)!;
const tailX = x + tailOffset.x;
const tailY = y + tailOffset.y;
const failFrame = hasFlag(state.flags, PonyStateFlags.CurlTail) ? 1 : frame.tail;
drawSet(batch, at(tails, failFrame), info.tail, tailX, tailY, WHITE);
// tail accessory
if (draw(options, NoDraw.BackAccessory) && hasTailAccessory) {
drawSet(batch, backAccessories[body], info.backAccessory, x + backOffset.x, y + backOffset.y, WHITE);
}
// body
if (draw(options, NoDraw.Body) && draw(options, NoDraw.BodyOnly)) {
drawSet(batch, sprites.body[body], info.body, x, y, WHITE);
}
// neck accessory
const frontNeckAccessory = true; // hasPart(info.neckAccessory, FRONT_NECK_ACCESSORIES);
// if (!frontNeckAccessory) {
// drawSpriteSet(context, neckAccessories[frame.body], info.neckAccessory, x, y);
// }
const frontX = x + frontLegOffset.x + frame.frontLegX;
const frontY = y + frontLegOffset.y + frame.frontLegY;
const backX = x + backLegOffset.x + frame.backLegX;
const backY = y + backLegOffset.y + frame.backLegY;
const cmOffset = at(offsets.cmOffsets, body)!;
const hooves = (TOOLS && options.useAllHooves) ? sprites.frontLegHooves : frontHooves;
// close legs back
if (draw(options, NoDraw.BackLeg)) {
drawLeg(
batch, backX, backY, frame.backLeg, sprites.backLegs, sprites.backLegHooves, sprites.backLegAccessories,
info.backLegs, flipped ? info.backLegAccessoryRight : info.backLegAccessory, info.backHooves, backHoovesInFront, WHITE,
info.cmPalette, flipped && !!info.cmFlip, x + cmOffset.x, y + cmOffset.y);
}
// back accessory
if (draw(options, NoDraw.BackAccessory) && !hasTailAccessory) {
drawSet(batch, backAccessories[body], info.backAccessory, x + backOffset.x, y + backOffset.y, WHITE);
}
// close leg back sleeves
if (draw(options, NoDraw.CloseSleeves) && hasBackSleeves) {
drawSet(batch, at(backLegSleeves, frame.backLeg), info.backAccessory, backX, backY, WHITE);
}
const isChestAccessoryInFront = chestAccessoryInFront(info.chestAccessory);
// chest accessory
if (!isChestAccessoryInFront && draw(options, NoDraw.Front)) {
drawSet(batch, chest[body], info.chestAccessory, chestX, chestY, WHITE);
}
// close legs front
if (draw(options, NoDraw.FrontLeg)) {
drawLeg(
batch, frontX, frontY, frame.frontLeg, sprites.frontLegs, hooves, sprites.frontLegAccessories,
info.frontLegs, flipped ? info.frontLegAccessoryRight : info.frontLegAccessory, info.frontHooves, frontHoovesInFront, WHITE,
undefined, false, 0, 0);
}
// close legs front sleeves
if (draw(options, NoDraw.CloseSleeves) && hasSleeves) {
drawSet(batch, at(sprites.frontLegSleeves, frame.frontLeg), info.sleeveAccessory, frontX, frontY, WHITE);
}
// chest accessory
if (isChestAccessoryInFront && draw(options, NoDraw.Front)) {
drawSet(batch, chest[body], info.chestAccessory, chestX, chestY, WHITE);
}
// close legs back (2)
if (draw(options, NoDraw.BackLeg)) {
drawLeg(
batch, backX, backY, frame.backLeg, sprites.backLegs2, sprites.backLegHooves2, sprites.backLegAccessories2,
info.backLegs, flipped ? info.backLegAccessoryRight : info.backLegAccessory, info.backHooves, backHoovesInFront, WHITE,
undefined, false, 0, 0);
}
// close legs back sleeves (2)
if (draw(options, NoDraw.CloseSleeves) && hasBackSleeves) {
drawSet(batch, at(sprites.backLegSleeves2, frame.backLeg), info.backAccessory, backX, backY, WHITE);
}
// neck accessory
if (frontNeckAccessory) {
const neckOffset = at(offsets.neckAccessoryOffsets, body)!;
drawSet(batch, neckAccessories[body], info.neckAccessory, x + neckOffset.x, y + neckOffset.y, WHITE);
}
// waist accessory
const waistFrame = frame.wing > 2 ? 16 : body;
const waistOffset = at(offsets.waistAccessoryOffsets, waistFrame)!;
drawSet(batch, waistAccessories[waistFrame], info.waistAccessory, x + waistOffset.x, y + waistOffset.y, WHITE);
// wings
drawSet(batch, wing, info.wings, x + wingOffset.x, y + wingOffset.y, WHITE);
// head
const headTurned = state.headTurned;
const headFlip = headTurned ? !flipped : flipped;
const headSprite = headFlip ? sprites.head0[frame.head] : sprites.head1[frame.head];
if (swimming) {
batch.clearCrop();
}
batch.save();
batch.multiplyTransform(headTransform);
if (swimming) {
batch.crop(0, headCropY, cropW, cropH);
}
if (headTurned) {
drawSet(batch, sprites.behindManes, info.mane, 0, maneBehindOffsetY, WHITE);
}
drawHead(batch, info, 0, 0, headSprite, headFrame, state, options, headFlip, maneOffsetY);
drawSet(batch, sprites.headAccessories, info.headAccessory, hatOffset.x, hatOffset.y + hatOffsetY, WHITE);
batch.restore();
if (swimming) {
batch.drawSprite(wake.front.frames[wakeFrame], WHITE, info.waterPalette, wakeX, wakeY);
}
}
export function drawHead(
batch: Batch, info: Info, x: number, y: number, headSprites: ColorExtraSets, headFrame: HeadAnimationFrame,
{ blinkFrame, expression, holding, blushColor, drawFaceExtra }: State,
options: Options, flip: boolean, maneOffsetY: number,
) {
const extraOffset = at(EXTRA_ACCESSORY_OFFSETS, info.mane && info.mane.type) || pointZero;
const extraX = x + extraOffset.x;
const extraY = y + extraOffset.y;
const toy = att(toys, options.toy);
if (toy !== undefined) {
drawSet(batch, sprites.extraAccessoriesBehind, toy, extraX, extraY, WHITE);
} else if (options.extra && draw(options, NoDraw.Behind)) {
drawSet(batch, sprites.extraAccessoriesBehind, info.extraAccessory, extraX, extraY, WHITE);
}
if (draw(options, NoDraw.Body)) {
if (draw(options, NoDraw.Head)) {
drawSet(batch, headSprites, info.head, x, y, WHITE);
}
let eyeLeftBase = -1;
let eyeRightBase = -1;
let irisLeft = Iris.Forward;
let irisRight = Iris.Forward;
if (expression !== undefined) {
if (hasFlag(expression.extra, ExpressionExtra.Blush)) {
batch.drawSprite(sprites.blush, blushColor, info.defaultPalette, x, y);
}
eyeLeftBase = expression.left;
eyeRightBase = expression.right;
irisLeft = expression.leftIris;
irisRight = expression.rightIris;
// make sure eyes are closed if sleeping
if (hasFlag(expression.extra, ExpressionExtra.Zzz)) {
if (!isEyeSleeping(eyeLeftBase)) {
eyeLeftBase = Eye.Closed;
}
if (!isEyeSleeping(eyeRightBase)) {
eyeRightBase = Eye.Closed;
}
}
}
const eyeRight = getEyeFrame(info.eyeOpennessRight || 1, eyeRightBase, headFrame.right, blinkFrame);
const eyeLeft = getEyeFrame(info.eyeOpennessLeft || 1, eyeLeftBase, headFrame.left, blinkFrame);
const eyeFrameLeft = flip ? eyeRight : eyeLeft;
const eyeFrameRight = flip ? eyeLeft : eyeRight;
const eyeColorLeft = flip ? info.eyeColorRight : info.eyeColorLeft;
const eyeColorRight = flip ? info.eyeColorLeft : info.eyeColorRight;
const eyePaletteLeft = flip ? info.eyePalette : info.eyePaletteLeft;
const eyePaletteRight = flip ? info.eyePaletteLeft : info.eyePalette;
const eyeIrisLeft = flip ? flipIris(irisRight) : irisLeft;
const eyeIrisRight = flip ? flipIris(irisLeft) : irisRight;
const eyeLeftSprites = sprites.eyeLeft;
const eyeRightSprites = sprites.eyeRight;
if (draw(options, NoDraw.Eyes)) {
drawEye(
batch, att(at(eyeLeftSprites, eyeFrameLeft), info.eyelashes),
eyeIrisLeft, info, eyeColorLeft, eyePaletteLeft, x, y);
drawEye(
batch, att(at(eyeRightSprites, eyeFrameRight), info.eyelashes),
eyeIrisRight, info, eyeColorRight, eyePaletteRight, x, y);
}
}
if (draw(options, NoDraw.Front)) {
drawSet(batch, sprites.facialHairBehind, info.facialHair, x, y, WHITE);
}
if (drawFaceExtra !== undefined) {
drawFaceExtra(batch);
}
const faceAccessory = info.faceAccessory;
let faceAccessoryType = 0;
let faceAccessoryPattern = 0;
if (faceAccessory !== undefined) {
faceAccessoryType = flip ? flipFaceAccessoryType(faceAccessory.type) : faceAccessory.type;
faceAccessoryPattern = flip ? flipFaceAccessoryPattern(faceAccessoryType, faceAccessory.pattern) : faceAccessory.pattern;
if (draw(options, NoDraw.FaceAccessory1)) {
drawTypePattern(
batch, sprites.faceAccessories, faceAccessoryType, faceAccessoryPattern,
faceAccessory.palette, faceAccessory.extraPalette, x, y, WHITE);
// if (info.faceAccessoryExtraPalette) {
// drawTypePattern(
// batch, sprites.faceAccessoriesExtra, faceAccessoryType, faceAccessoryPattern,
// info.faceAccessoryExtraPalette, x, y, WHITE);
// }
}
}
if (draw(options, NoDraw.Body) && draw(options, NoDraw.Nose)) {
const muzzle = holding ?
Muzzle.Smile :
headFrame.mouth === -1 ?
(expression ? expression.muzzle : info.muzzle) :
headFrame.mouth;
const noses = at(sprites.noses, muzzle);
const nose = att(noses, info.nose && info.nose.type)![0];
nose.mouth && batch.drawSprite(nose.mouth, WHITE, info.defaultPalette, x, y);
if (holding !== undefined && holding.draw !== undefined) {
holding.x = toWorldX(x + toInt(holding.pickableX));
holding.y = toWorldY(y + toInt(holding.pickableY));
holding.draw(batch, holdingDrawOptions);
}
drawSet(batch, noses, info.nose, x, y, WHITE);
if (info.fangs && nose.fangs) {
batch.drawSprite(nose.fangs, WHITE, info.defaultPalette, x, y);
}
}
if (draw(options, NoDraw.Front2)) {
drawSet(batch, sprites.facialHair, info.facialHair, x, y, WHITE);
}
const skipTopAndFrontMane = info.headAccessory !== undefined && info.headAccessory.type === 20;
if (draw(options, NoDraw.FrontMane)) {
drawSet(batch, sprites.backFrontManes, info.backMane, x, y + maneOffsetY, WHITE);
}
if (draw(options, NoDraw.TopMane) && !skipTopAndFrontMane) {
drawSet(batch, sprites.topManes, info.mane, x, y, WHITE);
}
if (toy !== undefined) {
drawSet(batch, sprites.extraAccessories, toy, extraX, extraY, WHITE);
} else if (options.extra && draw(options, NoDraw.Front)) {
drawSet(batch, sprites.extraAccessories, info.extraAccessory, extraX, extraY, WHITE);
}
if (draw(options, NoDraw.Front)) {
drawSet(batch, sprites.horns, info.horn, x, y, WHITE);
}
if (draw(options, NoDraw.Body) && draw(options, NoDraw.CloseEar) && !options.noEars) {
drawSet(batch, sprites.ears, info.ears, x, y, WHITE);
}
if (faceAccessory !== undefined && draw(options, NoDraw.FaceAccessory2)) {
drawTypePattern(
batch, sprites.faceAccessories2, faceAccessoryType, faceAccessoryPattern,
faceAccessory.palette, faceAccessory.extraPalette, x, y, WHITE);
// if (info.faceAccessoryExtraPalette) {
// drawTypePattern(
// batch, sprites.faceAccessories2Extra, faceAccessoryType, faceAccessoryPattern,
// info.faceAccessoryExtraPalette, x, y, WHITE);
// }
}
const earAccessoryOffset = at(EAR_ACCESSORY_OFFSETS, info.ears && info.ears.type)!;
const frontEarAccessory = false; // info.earAccessory !== undefined && info.earAccessory.type === 13;
if (!frontEarAccessory && draw(options, NoDraw.Front) && draw(options, NoDraw.CloseEar)) {
drawSet(batch, sprites.earAccessories, info.earAccessory, x + earAccessoryOffset.x, y + earAccessoryOffset.y, WHITE);
}
if (draw(options, NoDraw.FrontMane) && !skipTopAndFrontMane) {
drawSet(batch, sprites.frontManes, info.mane, x, y + maneOffsetY, WHITE);
}
if (frontEarAccessory && draw(options, NoDraw.Front) && draw(options, NoDraw.CloseEar)) {
drawSet(batch, sprites.earAccessories, info.earAccessory, x + earAccessoryOffset.x, y + earAccessoryOffset.y, WHITE);
}
}
function drawLeg(
batch: Batch, x: number, y: number, frame: number, leg: Sets, hoof: Sets, sock: Sets,
legSet: PaletteSpriteSet | undefined, sockSet: PaletteSpriteSet | undefined, hoofSet: PaletteSpriteSet | undefined,
hoovesInFront: boolean[], color: number, cmPalette: Palette | undefined, cmFlip: boolean, cmX: number, cmY: number
) {
const hoofInFront = hoofSet !== undefined && !!hoovesInFront[hoofSet.type];
drawSet(batch, at(leg, frame), legSet, x, y, color);
if (!hoofInFront) {
drawSet(batch, at(hoof, frame), hoofSet, x, y, color);
}
// CM
if (cmPalette !== undefined) {
const cm = cmFlip ? sprites.cmsFlip : sprites.cms;
batch.drawSprite(cm, WHITE, cmPalette, cmX, cmY);
}
drawSet(batch, at(sock, frame), sockSet, x, y, color);
if (hoofInFront) {
const hasClaws = hoofSet && hoofSet.type === 3 && hoof === frontHooves;
const hasSocks = !!(sockSet && sockSet.type > 0);
const hoofSprites = (hasClaws && hasSocks) ? claws : hoof;
drawSet(batch, at(hoofSprites, frame), hoofSet, x, y, color);
}
}
function getEyeFrame(base: Eye, expression: Eye, anim: Eye, blinkFrame: number) {
if (anim !== -1)
return anim;
const frame = expression === -1 ? base : expression;
const blink = blinkFrames[frame];
if (blinkFrame > 1 && blink) {
const frameOffset = 6 - blinkFrame;
if (frameOffset < blink.length) {
return blink[blink.length - frameOffset - 1];
}
}
return frame;
}
function drawEye(
batch: Batch, eye: PonyEye | undefined, iris: Iris, info: Info, palette: Palette | undefined, eyePalette: Palette,
x: number, y: number
) {
if (eye !== undefined) {
if (info.eyeshadow === true) {
eye.shadow && batch.drawSprite(eye.shadow, WHITE, info.eyeshadowColor, x, y);
eye.shine && batch.drawSprite(eye.shine, SHINES_COLOR, info.defaultPalette, x, y);
}
eye.base && batch.drawSprite(eye.base, WHITE, eyePalette, x, y);
const sprite = at(eye.irises, iris);
sprite && batch.drawSprite(sprite, WHITE, palette, x, y);
}
}
function drawSet(
batch: Batch, sprites: ColorExtraSets, set: PaletteSpriteSet | undefined, x: number, y: number, tint: number
) {
if (set !== undefined) {
const patterns = att(sprites, set.type);
if (patterns !== undefined) {
const patternSprite = at(patterns, set.pattern);
if (patternSprite !== undefined) {
batch.drawSprite(patternSprite.color, tint, set.palette, x, y);
}
}
}
}
function drawTypePattern(
batch: Batch, sprites: ColorExtraSets, type: number, pattern: number, palette: Palette, extraPalette: Palette | undefined,
x: number, y: number, tint: number
) {
const patterns = att(sprites, type);
if (patterns !== undefined) {
const patternSprite = at(patterns, pattern);
if (patternSprite !== undefined) {
batch.drawSprite(patternSprite.color, tint, palette, x, y);
if (patternSprite.extra !== undefined && extraPalette !== undefined) {
batch.drawSprite(patternSprite.extra, WHITE, extraPalette, x, y);
}
}
}
}
+56
View File
@@ -0,0 +1,56 @@
import { DrawPonyOptions, NoDraw, PonyState, PonyStateFlags } from '../common/interfaces';
import { SHADOW_COLOR, blushColor } from '../common/colors';
import { stand } from './ponyAnimations';
const defaultBlushColor = blushColor(0);
export function defaultPonyState(): PonyState {
return {
animation: stand,
animationFrame: 0,
headAnimation: undefined,
headAnimationFrame: 0,
headTurned: false,
headTilt: 0,
headTurn: 0,
blinkFrame: 0,
blushColor: defaultBlushColor,
holding: undefined,
expression: undefined,
drawFaceExtra: undefined,
flags: PonyStateFlags.None,
};
}
export function isStateEqual(a: PonyState, b: PonyState) {
return a.animation === b.animation &&
a.animationFrame === b.animationFrame &&
a.headAnimation === b.headAnimation &&
a.headAnimationFrame === b.headAnimationFrame &&
a.headTurned === b.headTurned &&
a.headTilt === b.headTilt &&
a.headTurn === b.headTurn &&
a.blinkFrame === b.blinkFrame &&
a.blushColor === b.blushColor &&
a.holding === b.holding &&
a.expression === b.expression &&
a.drawFaceExtra === b.drawFaceExtra &&
a.flags === b.flags;
}
export function defaultDrawPonyOptions(): DrawPonyOptions {
return {
flipped: false,
selected: false,
shadow: false,
extra: false,
toy: 0,
swimming: false,
bounce: false,
shadowColor: SHADOW_COLOR,
noEars: false,
no: NoDraw.None,
useAllHooves: false,
gameTime: 0,
};
}
+155
View File
@@ -0,0 +1,155 @@
import { animatorState as state, animatorTransition as transition, anyState, AnimatorState } from '../common/animator';
import {
stand, sit, sitDown, standUp, lie, lieDown, sitUp, flyBug, fly, flyUp, flyDown, flyUpBug, flyDownBug,
trot, boop, boopSit, swim, sitToTrot, lieToTrot, boopLie, trotToFly, trotToFlyBug, boopFly, boopFlyBug,
flyToTrot, flyToTrotBug, swing, swimToTrot, trotToSwim, swimToFly, flyToSwim, boopSwim, swimToFlyBug, flyToSwimBug
} from './ponyAnimations';
import { BodyAnimation } from '../common/interfaces';
function n(value: string) {
return (DEVELOPMENT || SERVER) ? value : '';
}
export const standing = state(n('standing'), stand);
export const trotting = state(n('trotting'), trot);
export const swimming = state(n('swimming'), swim);
export const swimmingToTrotting = state(n('swimming-to-trotting'), swimToTrot);
export const trottingToSwimming = state(n('trotting-to-swimming'), trotToSwim);
export const swimmingToFlying = state(n('swimming-to-flying'), swimToFly, { bug: swimToFlyBug });
export const flyingToSwimming = state(n('flying-to-swimming'), flyToSwim, { bug: flyToSwimBug });
export const booping = state(n('booping'), boop);
export const boopingSitting = state(n('booping-sitting'), boopSit);
export const boopingLying = state(n('booping-lying'), boopLie);
export const boopingFlying = state(n('booping-flying'), boopFly, { bug: boopFlyBug });
export const boopingSwimming = state(n('booping-swimming'), boopSwim);
export const sitting = state(n('sitting'), sit);
export const sittingDown = state(n('sitting-down'), sitDown);
export const standingUp = state(n('standing-up'), standUp);
export const sittingToTrotting = state(n('sitting-to-trotting'), sitToTrot);
export const lying = state(n('lying'), lie);
export const lyingDown = state(n('lying-down'), lieDown);
export const sittingUp = state(n('sitting-up'), sitUp);
export const lyingToTrotting = state(n('lying-to-trotting'), lieToTrot);
export const hovering = state(n('hovering'), fly, { bug: flyBug });
export const flying = state(n('flying'), fly, { bug: flyBug });
export const flyingUp = state(n('flying-up'), flyUp, { bug: flyUpBug });
export const flyingDown = state(n('flying-down'), flyDown, { bug: flyDownBug });
export const trottingToFlying = state(n('trotting-to-flying'), trotToFly, { bug: trotToFlyBug });
export const flyingToTrotting = state(n('flying-to-trotting'), flyToTrot, { bug: flyToTrotBug });
export const swinging = state(n('swinging'), swing);
export const ponyStates = [
anyState, standing, trotting, swimming, swimmingToTrotting, trottingToSwimming,
booping, boopingSitting, boopingLying, boopingFlying,
sitting, sittingDown, standingUp, sittingToTrotting,
lying, lyingDown, sittingUp, lyingToTrotting,
hovering, flying, flyingUp, flyingDown, trottingToFlying, flyingToTrotting,
swinging, swimmingToFlying, flyingToSwimming, boopingSwimming,
];
transition(hovering, flyingDown, { exitAfter: 0 });
transition(flyingDown, standing);
transition(standing, sittingDown, { exitAfter: 0 });
transition(sittingDown, sitting);
transition(sitting, lyingDown, { exitAfter: 0 });
transition(lyingDown, lying);
transition(lying, sittingUp, { exitAfter: 0 });
transition(sittingUp, sitting);
transition(sitting, standingUp, { exitAfter: 0 });
transition(standingUp, standing);
transition(standing, flyingUp, { exitAfter: 0 });
transition(flyingUp, hovering);
// transition(flyingUp, trottingToFlying, { exitAfter: 0, keepTime: true });
transition(sitting, sittingToTrotting, { exitAfter: 0, onlyDirectTo: trotting });
transition(sittingToTrotting, trotting, { enterTime: 6.1 / 16 });
transition(sittingToTrotting, standing);
transition(lying, lyingToTrotting, { exitAfter: 0, onlyDirectTo: trotting });
transition(lyingToTrotting, trotting, { enterTime: 6.1 / 16 });
transition(lyingToTrotting, standing, { exitAfter: 5 / 6 });
transition(trotting, trottingToFlying, { exitAfter: 4 / 16 });
transition(trottingToFlying, flying);
transition(flying, flyingToTrotting, { exitAfter: 0 });
transition(flyingToTrotting, trotting, { enterTime: 6 / 16 });
transition(swimming, swimmingToTrotting, { exitAfter: 0 });
transition(swimming, swimmingToFlying, { exitAfter: 0 });
transition(swimmingToTrotting, trotting);
transition(swimmingToTrotting, standing);
transition(trotting, trottingToSwimming, { exitAfter: 0 });
transition(standing, trottingToSwimming, { exitAfter: 0 });
transition(trottingToSwimming, swimming);
transition(swimmingToFlying, hovering);
transition(swimmingToFlying, flying);
transition(flying, flyingToSwimming, { exitAfter: 0 });
transition(hovering, flyingToSwimming, { exitAfter: 0 });
transition(flyingToSwimming, swimming);
transition(trotting, standing, { exitAfter: 0 });
transition(flying, hovering, { exitAfter: 0, keepTime: true });
transition(boopingSwimming, swimming);
transition(swimming, boopingSwimming, { exitAfter: 0 });
transition(booping, standing);
transition(standing, booping, { exitAfter: 0 });
transition(boopingSitting, sitting);
transition(sitting, boopingSitting, { exitAfter: 0 });
transition(boopingLying, lying);
transition(lying, boopingLying, { exitAfter: 0 });
transition(boopingFlying, hovering, { enterTime: 1.1 / 10 });
transition(hovering, boopingFlying, { exitAfter: 0 });
// transition(anyState, trottingToSwimming, { exitAfter: 0 });
transition(anyState, trotting, { exitAfter: 0, keepTime: true });
transition(anyState, flying, { exitAfter: 0, keepTime: true });
transition(standing, swinging, { exitAfter: 0 });
transition(swinging, standing);
export function isFlyingUp(state: AnimatorState<BodyAnimation> | undefined) {
return state === flyingUp || state === trottingToFlying || state === swimmingToFlying;
}
export function isFlyingDown(state: AnimatorState<BodyAnimation> | undefined) {
return state === flyingDown || state === flyingToTrotting || state === flyingToSwimming;
}
export function isSwimmingState(state: AnimatorState<BodyAnimation> | undefined) {
return state === swimming || state === trottingToSwimming || state === swimmingToTrotting ||
state === flyingToSwimming || state === swimmingToFlying || state === boopingSwimming;
}
export function isFlyingUpOrDown(state: AnimatorState<BodyAnimation> | undefined) {
return isFlyingUp(state) || isFlyingDown(state);
}
export function isSittingDown(state: AnimatorState<BodyAnimation> | undefined) {
return state === sittingDown;
}
export function isSittingUp(state: AnimatorState<BodyAnimation> | undefined) {
return state === sittingUp;
}
export function toBoopState(state: AnimatorState<BodyAnimation>) {
switch (state) {
case standing: return booping;
case sitting: return boopingSitting;
case lying: return boopingLying;
case hovering: return boopingFlying;
case swimming: return boopingSwimming;
default: return undefined;
}
}
+176
View File
@@ -0,0 +1,176 @@
/// <reference path="../../typings/my.d.ts" />
import { range, dropRight, compact, max, zip } from 'lodash';
import {
Eye, Iris, Muzzle, ExpressionExtra, Sprite, ColorExtraSets, PonyInfoBase, SpriteSetBase, ColorExtra, ColorExtraSet
} from '../common/interfaces';
import * as sprites from '../generated/sprites';
import { HEAD_ACCESSORY_OFFSETS, EXTRA_ACCESSORY_OFFSETS, EAR_ACCESSORY_OFFSETS } from '../common/offsets';
export const PONY_WIDTH = 80;
export const PONY_HEIGHT = 70;
export const BLINK_FRAMES = [2, 6, 6, 4, 2];
export const SLEEVED_ACCESSORIES = [2, 3, 4];
export const SLEEVED_BACK_ACCESSORIES = [5, 6];
export const CHEST_ACCESSORIES_IN_FRONT = [1];
export const NO_MANE_HEAD_ACCESSORIES = [0, 16];
// export const FRONT_NECK_ACCESSORIES = [2, 10];
export type Sprites = (Sprite | undefined)[];
export type Sets = ColorExtraSets[]; // [frame][type][pattern]
export const headCenter = [
undefined,
[[0].map(i => sprites.head[2]![0]![i])],
];
export const claws: Sets = sprites.frontLegHooves
.map(f => f && [undefined, undefined, undefined, f[4], undefined, undefined]);
export const frontHooves: Sets = sprites.frontLegHooves
.map(f => f && [...f.slice(0, 4), ...f.slice(5)]);
export const frontHoovesInFront = [false, false, true, true, false, false];
export const backHoovesInFront = [false, false, true, false, false];
const bodyFrames = sprites.body.length;
export const wings = createCompleteSets(sprites.wings, 3 + 10);
export const tails = createCompleteSets(sprites.tails, 3);
export const chest = createCompleteSets(sprites.chestAccessories, bodyFrames);
export const chestBehind = createCompleteSets(sprites.chestAccessoriesBehind, bodyFrames);
export const backAccessories = createCompleteSets(sprites.backAccessories, bodyFrames);
sprites.neckAccessories.forEach(f => f && f.pop()); // TEMP: remove headphones
export const neckAccessories = createCompleteSets(sprites.neckAccessories, bodyFrames);
export const waistAccessories = createCompleteSets(sprites.waistAccessories, bodyFrames + 1);
function frameType(sets: Sets, frame: number, type: number) {
const set = sets[frame];
return set && set[type];
}
function createCompleteSets(sets: Sets, frameCount: number): Sets {
const typeCount = sets.reduce((max, s) => Math.max(max, s ? s.length : 0), 0);
const typeRange = range(0, typeCount);
const result: Sets = [];
for (let frame = 0; frame < frameCount; frame++) {
result.push(typeRange.map(type => frameType(sets, frame, type) || frameType(result, frame - 1, type)));
}
return result;
}
export function canFly(info: PonyInfoBase<any, SpriteSetBase>) {
const type = info.wings && info.wings.type || 0;
return type > 0;
}
export function canMagic(info: PonyInfoBase<any, SpriteSetBase>) {
const type = info.horn && info.horn.type || 0;
return type === 1 || type === 2 || type === 3 || type === 14;
}
export function flipIris(iris: Iris): Iris {
if (iris === Iris.Left || iris === Iris.UpLeft) {
return iris + 1;
} else if (iris === Iris.Right || iris === Iris.UpRight) {
return iris - 1;
} else {
return iris;
}
}
export function flipFaceAccessoryType(type: number) {
if (type === 6) return 7;
if (type === 7) return 6;
if (type === 9) return 10;
if (type === 10) return 9;
return type;
}
export function flipFaceAccessoryPattern(type: number, pattern: number) {
if (type === 2) { // dark glasses
if (pattern === 1) return 2;
if (pattern === 2) return 1;
} else if (type === 11) { // large dark glasses
if (pattern === 1) return 2;
if (pattern === 2) return 1;
}
return pattern;
}
export const defaultExpression = {
left: Eye.Neutral,
leftIris: Iris.Forward,
right: Eye.Neutral,
rightIris: Iris.Forward,
muzzle: Muzzle.Neutral,
extra: ExpressionExtra.None,
};
export const blinkFrames: Eye[][] = [];
function setupBlinkFrames(frames: Eye[]) {
dropRight(frames, 1).forEach((f, i) => blinkFrames[f] = blinkFrames[f] || frames.slice(i + 1));
}
setupBlinkFrames([Eye.Neutral, Eye.Neutral2, Eye.Neutral3, Eye.Neutral4, Eye.Neutral5, Eye.Closed]);
setupBlinkFrames([Eye.Frown, Eye.Frown2, Eye.Frown3, Eye.Frown4, Eye.Closed]);
setupBlinkFrames([Eye.Sad, Eye.Sad2, Eye.Sad3, Eye.Sad4, Eye.Neutral5, Eye.Closed]);
setupBlinkFrames([Eye.Angry, Eye.Angry2, Eye.Neutral4, Eye.Neutral5, Eye.Closed]);
// sets
function mergeColorExtras(sprites: (ColorExtra | undefined)[]): ColorExtra | undefined {
const filtered = compact(sprites);
return {
...filtered[0],
colors: max(filtered.map(x => x.colors || 0)),
colorMany: filtered.length > 1 ? filtered.map(x => x.color) : undefined,
};
}
function mergeSprites(sets: ColorExtraSet[]): ColorExtraSet {
return zip(...sets).map(mergeColorExtras);
}
function mergeSpriteSets(...sets: ColorExtraSets[]): ColorExtraSets {
return zip(...sets).map(mergeSprites);
}
export const backLegSleeves: Sets = sprites.backLegSleeves
.map(sets => sets && [undefined, undefined, undefined, undefined, undefined, ...sets]);
// TEMP: remove summer hat
sprites.headAccessoriesBehind.pop();
sprites.headAccessories.pop();
export const mergedManes = mergeSpriteSets(sprites.behindManes, sprites.topManes, sprites.frontManes)!;
export const mergedBackManes = mergeSpriteSets(sprites.backBehindManes, sprites.backFrontManes)!;
export const mergedFacialHair = mergeSpriteSets(sprites.facialHairBehind, sprites.facialHair)!;
export const mergedEarAccessories = mergeSpriteSets(sprites.earAccessoriesBehind, sprites.earAccessories)!;
export const mergedHeadAccessories = mergeSpriteSets(sprites.headAccessoriesBehind, sprites.headAccessories)!;
export const mergedFaceAccessories = mergeSpriteSets(sprites.faceAccessories, sprites.faceAccessories2)!;
export const mergedChestAccessories = mergeSpriteSets(sprites.chestAccessoriesBehind[1], sprites.chestAccessories[1])!;
export const mergedBackAccessories = mergeSpriteSets(
backAccessories[1], [undefined, undefined, undefined, undefined, undefined, ...sprites.backLegSleeves[1]!])!;
export const mergedExtraAccessories = mergeSpriteSets(sprites.extraAccessoriesBehind, sprites.extraAccessories)!
.slice(0, DEVELOPMENT ? 100 : 2);
if (DEVELOPMENT) {
assertSizes('HEAD_ACCESSORY_OFFSETS', HEAD_ACCESSORY_OFFSETS, mergedManes);
assertSizes('EXTRA_ACCESSORY_OFFSETS', EXTRA_ACCESSORY_OFFSETS, mergedManes);
assertSizes('EAR_ACCESSORY_OFFSETS', EAR_ACCESSORY_OFFSETS, sprites.ears);
assertSizes('frontHoovesInFront', frontHoovesInFront, frontHooves[1]!);
assertSizes('backHoovesInFront', backHoovesInFront, sprites.backLegHooves[1]!);
}
function assertSizes(name: string, a: any[], b: any[]) {
if (a.length !== b.length) {
throw new Error(`Invalid ${name} length (${a.length} !== ${b.length})`);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { REV } from '../generated/rev';
/* istanbul ignore next */
export function getUrl(name: string): string {
if (DEVELOPMENT)
return `/assets/${name}`;
if (!REV[name])
throw new Error(`Cannot find file url (${name})`);
return `/assets/${name.replace(/(\.\S+)$/, `-${REV[name]}$1`)}`;
}
+44
View File
@@ -0,0 +1,44 @@
import { PonyTownGame } from './game';
import { Pony, EntityFlags } from '../common/interfaces';
import { setFlag, fromNow } from '../common/utils';
import { fixCollision, isStaticCollision } from '../common/collision';
import { WEEK } from '../common/constants';
let currentPlayer: Pony | undefined;
let setX = 0;
let setY = 0;
export function setupPlayer(game: PonyTownGame, player: Pony) {
const pony = player;
pony.flags = setFlag(pony.flags, EntityFlags.Interactive, false);
if (isStaticCollision(player, game.map, false)) {
fixCollision(player, game.map);
}
game.setPlayer(pony);
currentPlayer = player;
savePlayerPosition();
}
export function savePlayerPosition() {
if (currentPlayer) {
setX = currentPlayer.x;
setY = currentPlayer.y;
}
}
export function restorePlayerPosition() {
if (currentPlayer) {
if (currentPlayer.x !== setX || currentPlayer.y !== setY) {
currentPlayer.x = setX;
currentPlayer.y = setY;
DEVELOPMENT && console.warn('Restoring player position');
}
}
}
// Account creation lock
export const setAclCookie = (acl: string) => {
document.cookie = `acl=${acl}; expires=${fromNow(WEEK).toUTCString()}; path=/`;
};
+31
View File
@@ -0,0 +1,31 @@
import * as sprites from '../generated/sprites';
import { SpriteAnimation } from '../common/animationPlayer';
import { AnimatedRenderable } from '../common/mixins';
import { Sprite } from '../common/interfaces';
export const zzzAnimation1 = createSpriteAnimation(
sprites.emote_sleep1, 8, 8, 4, 7, true, sprites.emote_sleep1_flip.frames);
export const zzzAnimation2 = createSpriteAnimation(
sprites.emote_sleep2, 12, 13, 13, 12, true, sprites.emote_sleep2_flip.frames);
export const zzzAnimations = [zzzAnimation1, zzzAnimation2];
export const cryAnimation = createSpriteAnimation(sprites.emote_cry2, 12, 0, 13, 0);
export const tearsAnimation = createSpriteAnimation(sprites.emote_tears, 12, 0, 1, 0);
export const heartsAnimation = createSpriteAnimation(sprites.emote_hearts, 12, 18, 18, 9, true);
sprites.emote_sneeze.frames.unshift(sprites.emptySprite, sprites.emptySprite);
export const sneezeAnimation = createSpriteAnimation(sprites.emote_sneeze, 8, 0, 4, 0, false);
sprites.hold_poof.frames.push(sprites.emptySprite);
export const holdPoofAnimation = createSpriteAnimation(sprites.hold_poof, 12, 0, 4, 0, false);
export const magicAnimation = createSpriteAnimation(sprites.magic2, 8, 2, 6, 0, true);
function createSpriteAnimation(
{ frames, palette }: AnimatedRenderable, fps: number, start: number, middle: number, end: number, loop = true,
flipFrames?: Sprite[],
): SpriteAnimation {
return { start, middle, end, fps, palette, frames, loop, flipFrames };
}
+81
View File
@@ -0,0 +1,81 @@
import { once, noop } from 'lodash';
import { ColorExtra, ColorExtraSets, PonyEye, SpriteSheet, Sprite } from '../common/interfaces';
import { spriteSheets } from '../generated/sprites';
import { loadImage, createCanvas } from '../client/canvasUtils';
import { getUrl } from './rev';
import { createFonts } from './fonts';
export function createSprite(x: number, y: number, w: number, h: number, ox: number, oy: number, type: number): Sprite {
return { x, y, w, h, ox, oy, type };
}
export function addTitles(sprites: ColorExtraSets, titles: string[]): ColorExtraSets {
return sprites && sprites.map((ns, i) =>
ns && ns.map(s => s && { color: s.color, colors: s.colors, title: titles[i], label: titles[i] }));
}
export function addLabels(sprites: ColorExtraSets, labels: string[]) {
sprites && sprites.forEach((s, i) => s && s[0] ? s[0]!.label = labels[i] : undefined);
return sprites;
}
export function createEyeSprite(eye: PonyEye | undefined, iris: number, defaultPalette: Uint32Array): ColorExtra | undefined {
return eye && { color: eye.irises[iris]!, colors: 2, extra: eye.base, palettes: [defaultPalette] };
}
export function getColorCount(sprite: ColorExtra | undefined): number {
return sprite && sprite.colors ? Math.floor((sprite.colors - 1) / 2) : 0;
}
export function createSpriteUtils() {
createFonts();
}
type LoadImage = (src: string) => Promise<HTMLImageElement | ImageBitmap>;
function getImageData(img: HTMLImageElement | ImageBitmap) {
const canvas = createCanvas(img.width, img.height);
const context = canvas.getContext('2d')!;
context.drawImage(img, 0, 0);
return context.getImageData(0, 0, img.width, img.height);
}
function loadSpriteSheet(sheet: SpriteSheet, loadImage: LoadImage) {
return Promise.all([
loadImage(sheet.src!),
sheet.srcA ? loadImage(sheet.srcA) : Promise.resolve(undefined)
])
.then(([img, imgA]) => {
sheet.data = getImageData(img);
if (imgA) {
const alpha = getImageData(imgA);
const alphaData = alpha.data;
const sheedData = sheet.data.data;
for (let i = 0; i < sheedData.length; i += 4) {
sheedData[i + 3] = alphaData[i];
}
}
});
}
export function loadSpriteSheets(sheets: SpriteSheet[], loadImage: LoadImage) {
return Promise.all(sheets.map(s => loadSpriteSheet(s, loadImage))).then(noop);
}
export let spriteSheetsLoaded = false;
export function loadAndInitSheets(sheets: SpriteSheet[], loadImage: LoadImage) {
return loadSpriteSheets(sheets, loadImage)
.then(createSpriteUtils)
.then(() => true)
.catch(e => (console.error(e), false))
.then(loaded => spriteSheetsLoaded = loaded);
}
export function loadImageFromUrl(url: string) {
return loadImage(getUrl(url));
}
export const loadAndInitSpriteSheets = once(() => loadAndInitSheets(spriteSheets, loadImageFromUrl));
+602
View File
@@ -0,0 +1,602 @@
import {
PaletteManager, Season, TileSets, Region, TileType, Camera, PaletteSpriteBatch, DrawOptions, WorldMap, IMap,
Sprite, MapType
} from '../common/interfaces';
import * as sprites from '../generated/sprites';
import { getRegionTile, getRegionElevation } from '../common/region';
import { getRegionGlobal } from '../common/worldMap';
import { tileWidth, tileHeight, tileElevation, WATER_FPS, REGION_SIZE, WATER_HEIGHT } from '../common/constants';
import { clamp, toInt, at, invalidEnumReturn } from '../common/utils';
import { isAreaVisible } from '../common/camera';
import { WHITE } from '../common/colors';
import { releasePalette } from '../graphics/paletteManager';
import { drawPixelText } from '../graphics/graphicsUtils';
import { toScreenX, toScreenY, toWorldZ } from '../common/positionUtils';
const TILE_COUNTS = [[0, 4], [2, 3], [4, 3], [6, 3], [8, 3], [13, 3], [14, 3], [47, 4]];
export const TILE_COUNT_MAP: number[] = [];
export const TILE_MAP_MAP: number[] = [];
TILE_COUNTS.forEach(([tile, count]) => {
while (TILE_COUNT_MAP.length < (tile + 1)) {
TILE_COUNT_MAP.push(1);
}
TILE_COUNT_MAP[tile] = count;
});
let tileIndex = 0;
for (let i = 0; i <= 47; i++) {
TILE_MAP_MAP.push(tileIndex);
tileIndex += TILE_COUNT_MAP[i];
}
// 1 | 2 | 4
// ----+----+----
// 8 | | 16
// ----+----+----
// 32 | 64 | 128
export const TILE_MAP = [
46, 46, 22, 22, 46, 46, 22, 22, 21, 21, // 0-9
17, 11, 21, 21, 17, 11, 19, 19, 18, 18, // 10-19
19, 19, 12, 12, 14, 14, 24, 28, 14, 14, // 20-29
30, 6, 46, 46, 22, 22, 46, 46, 22, 22, // 30-39
21, 21, 17, 11, 21, 21, 17, 11, 19, 19, // 40-49
18, 18, 19, 19, 12, 12, 14, 14, 24, 28, // 50-59
14, 14, 30, 6, 20, 20, 13, 13, 20, 20, // 60-69
13, 13, 16, 16, 23, 32, 16, 16, 23, 32, // 70-79
15, 15, 25, 25, 15, 15, 34, 34, 26, 26, // 80-89
45, 41, 26, 26, 42, 36, 20, 20, 13, 13, // 90-99
20, 20, 13, 13, 10, 10, 31, 4, 10, 10, // 100-109
31, 4, 15, 15, 25, 25, 15, 15, 34, 34, // 110-119
27, 27, 43, 37, 27, 27, 35, 5, 46, 46, // 120-129
22, 22, 46, 46, 22, 22, 21, 21, 17, 11, // 130-139
21, 21, 17, 11, 19, 19, 18, 18, 19, 19, // 140-149
12, 12, 14, 14, 24, 28, 14, 14, 30, 6, // 150-159
46, 46, 22, 22, 46, 46, 22, 22, 21, 21, // 160-169
17, 11, 21, 21, 17, 11, 19, 19, 18, 18, // 170-179
19, 19, 12, 12, 14, 14, 24, 28, 14, 14, // 180-189
30, 6, 20, 20, 13, 13, 20, 20, 13, 13, // 190-199
16, 16, 23, 32, 16, 16, 23, 32, 9, 9, // 200-209
33, 33, 9, 9, 8, 8, 29, 29, 44, 39, // 210-219
29, 29, 38, 7, 20, 20, 13, 13, 20, 20, // 220-229
13, 13, 10, 10, 31, 4, 10, 10, 31, 4, // 230-239
9, 9, 33, 33, 9, 9, 8, 8, 2, 2, // 240-249
40, 3, 2, 2, 1, 0 // 250-255
];
const enum TileTypeNumber {
None = 0,
Grass = 1,
Water = 2,
Wood = 3,
GrassNew = 4,
Water2 = 5,
Water3 = 6,
Water4 = 7,
Ice = 8,
SnowOnIce = 9,
Stone = 10,
Stone2 = 11,
Boat = 12,
}
const waterFrames: number[] = [
TileTypeNumber.Water, TileTypeNumber.Water2, TileTypeNumber.Water3, TileTypeNumber.Water4
];
export function updateTileSets(
paletteManager: PaletteManager, tileSets: TileSets | undefined, season: Season, mapType: MapType
) {
if (tileSets) {
tileSets.forEach(t => releasePalette(t.palette));
}
return createTileSets(paletteManager, season, mapType);
}
export function createTileSets(paletteManager: PaletteManager, season: Season, mapType: MapType): TileSets {
const isWinter = season === Season.Winter;
const isAutumn = season === Season.Autumn;
const isCave = mapType === MapType.Cave;
const grassTiles = isCave ? sprites.caveTiles : (isWinter ? sprites.snowTiles : sprites.grassTiles);
const grassPalette = grassTiles.palettes[isCave ? 0 : (isAutumn ? 1 : 0)];
const icePaletteIndex = isWinter ? 2 : (isAutumn ? 1 : 0);
const waterPaletteIndex = isCave ? 3 : (isWinter ? 2 : (isAutumn ? 1 : 0));
const waterPalette = sprites.waterTiles1.palettes[waterPaletteIndex];
// indexes equal to TileTypeNumber values
return [
{ // 0
sprites: [sprites.tile_none.color],
palette: paletteManager.addArray(sprites.tile_none.palettes![0]),
},
{ // 1
sprites: grassTiles.sprites,
palette: paletteManager.addArray(grassPalette),
},
{ // 2
sprites: sprites.waterTiles1.sprites,
palette: paletteManager.addArray(waterPalette),
},
{ // 3
sprites: sprites.woodTiles.sprites,
palette: paletteManager.addArray(sprites.woodTiles.palettes[0]),
},
{ // 4
sprites: sprites.grassTilesNew.sprites,
palette: paletteManager.addArray(sprites.grassTilesNew.palettes[0]),
},
// water frames
{ // 5
sprites: sprites.waterTiles2.sprites,
palette: paletteManager.addArray(waterPalette),
},
{ // 6
sprites: sprites.waterTiles3.sprites,
palette: paletteManager.addArray(waterPalette),
},
{ // 7
sprites: sprites.waterTiles4.sprites,
palette: paletteManager.addArray(waterPalette),
},
// ice
{ // 8
sprites: sprites.iceTiles.sprites,
palette: paletteManager.addArray(sprites.iceTiles.palettes[icePaletteIndex]),
},
// snow on ice
{ // 9
sprites: sprites.snowOnIceTiles.sprites,
palette: paletteManager.addArray(sprites.snowOnIceTiles.palettes[0]),
},
// stone
{ // 10
sprites: sprites.stoneTiles.sprites,
palette: paletteManager.addArray(sprites.stoneTiles.palettes[0]),
},
// stone 2
{ // 11
sprites: sprites.stone2Tiles.sprites,
palette: paletteManager.addArray(sprites.stone2Tiles.palettes[0]),
},
];
}
export function drawTiles(
batch: PaletteSpriteBatch, region: Region, camera: Camera, map: WorldMap, tileSets: TileSets, options: DrawOptions
) {
const { tileIndices } = region;
const { tileTime } = map;
const regionX = region.x * REGION_SIZE;
const regionY = region.y * REGION_SIZE;
if (isAreaVisible(camera, regionX * tileWidth, regionY * tileHeight, REGION_SIZE * tileWidth, REGION_SIZE * tileHeight)) {
const minX = clamp(Math.floor(camera.x / tileWidth - regionX), 0, REGION_SIZE);
const minY = clamp(Math.floor(camera.actualY / tileHeight - regionY), 0, REGION_SIZE);
const maxX = clamp(Math.ceil((camera.x + camera.w) / tileWidth - regionX), 0, REGION_SIZE);
const maxY = clamp(Math.ceil((camera.actualY + camera.h) / tileHeight - regionY), 0, REGION_SIZE);
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
const tileIndex = tileIndices[x | (y << 3)];
if (tileIndex === -1) {
options.error(`Uninitialized tile index at (${x}, ${y}) ` +
`region: (${region.x}, ${region.y}, ${region.tilesDirty}, ${region.lastTileUpdate}) ` +
`now: ${performance.now()}`);
region.tilesDirty = true;
continue;
}
const tileTypeNumber = tileIndex >>> 8;
const isWater = tileTypeNumber === TileTypeNumber.Water || tileTypeNumber === TileTypeNumber.Boat;
const tileSpriteIndex = tileIndex & 0xff;
const tileSetIndex = isWater ? at(waterFrames, toInt(tileTime) % waterFrames.length)! : tileTypeNumber;
const tileSet = tileSets[tileSetIndex];
if (!tileSet) {
options.error(`Missing tileset: position: (${x}, ${y}) tile: (${getRegionTile(region, x, y)}) ` +
`info: (${tileIndex}, ${tileSetIndex}, ${tileTime}, ${tileSpriteIndex}, ${JSON.stringify(waterFrames)})`);
tileIndices[x | (y << 3)] = -1;
region.tilesDirty = true;
continue;
}
const rx = (x + regionX) * tileWidth;
const ry = (y + regionY) * tileHeight;
if (DEVELOPMENT && !tileSet.sprites[tileSpriteIndex]) {
console.error('Missing sprite', tileSetIndex, tileSpriteIndex);
}
batch.drawSprite(tileSet.sprites[tileSpriteIndex], WHITE, tileSet.palette, rx, ry);
}
}
}
}
export function drawTilesDebugInfo(batch: PaletteSpriteBatch, region: Region, camera: Camera, options: DrawOptions) {
const { tileIndices } = region;
const regionX = region.x * REGION_SIZE;
const regionY = region.y * REGION_SIZE;
if (isAreaVisible(camera, regionX * tileWidth, regionY * tileHeight, REGION_SIZE * tileWidth, REGION_SIZE * tileHeight)) {
const minX = clamp(Math.floor(camera.x / tileWidth - regionX), 0, REGION_SIZE);
const minY = clamp(Math.floor(camera.actualY / tileHeight - regionY), 0, REGION_SIZE);
const maxX = clamp(Math.ceil((camera.x + camera.w) / tileWidth - regionX), 0, REGION_SIZE);
const maxY = clamp(Math.ceil((camera.actualY + camera.h) / tileHeight - regionY), 0, REGION_SIZE);
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
const tileIndex = tileIndices[x | (y << 3)];
if (tileIndex === -1) {
continue;
}
const tileTypeNumber = tileIndex >>> 8;
const tileSpriteIndex = tileIndex & 0xff;
const rx = (x + regionX) * tileWidth;
const ry = (y + regionY) * tileHeight;
if (options.tileIndices) {
drawPixelText(batch, rx + 2, ry + 2, 0x000000ff, `${tileTypeNumber}:${tileSpriteIndex}`);
drawPixelText(batch, rx + 2, ry + 2 + 7, 0x555555ff, `${region.tiles[x + REGION_SIZE * y]}`);
}
if (options.tileGrid) {
batch.drawRect(y !== 0 ? 0x00000011 : 0x00000022, rx, ry, tileWidth, 1);
batch.drawRect(x !== 0 ? 0x00000011 : 0x00000022, rx, ry + 1, 1, tileHeight - 1);
}
}
}
}
}
export function drawTilesNew(
batch: PaletteSpriteBatch, region: Region, camera: Camera, map: WorldMap, tileSets: TileSets, options: DrawOptions
) {
const regionX = region.x * REGION_SIZE;
const regionY = region.y * REGION_SIZE;
const TILE_COLOR = 0x666666ff;
const TILE_FRONT_COLOR = 0x5e5e5eff;
const OUTLINE_2_COLOR = 0xffffff22;
const OUTLINE_COLOR = 0x00000022;
if (isAreaVisible(camera, regionX * tileWidth, regionY * tileHeight, REGION_SIZE * tileWidth, REGION_SIZE * tileHeight)) {
const minX = clamp(Math.floor(camera.x / tileWidth - regionX), 0, REGION_SIZE);
const minY = clamp(Math.floor(camera.y / tileHeight - regionY), 0, REGION_SIZE);
const maxX = clamp(Math.ceil((camera.x + camera.w) / tileWidth - regionX), 0, REGION_SIZE);
const maxY = clamp(Math.ceil((camera.y + camera.h) / tileHeight - regionY), 0, REGION_SIZE);
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
const elevation = getRegionElevation(region, x, y);
const cliffTop = getRegionElevation(region, x, y - 1) < elevation;
const cliffLeft = getRegionElevation(region, x - 1, y) < elevation;
const cliffRight = getRegionElevation(region, x + 1, y) < elevation;
const cliffBottom = getRegionElevation(region, x, y + 1) < elevation;
const elevDiff = Math.max(0, elevation - getRegionElevation(region, x, y + 1));
const tx = (x + regionX) * tileWidth;
const ty = (y + regionY) * tileHeight - elevation * tileElevation;
batch.drawRect(TILE_COLOR, tx, ty, tileWidth, tileHeight);
if (elevation) {
batch.drawRect(TILE_FRONT_COLOR, tx, ty + tileHeight, tileWidth, elevDiff * tileElevation);
if (cliffLeft) {
batch.drawRect(OUTLINE_COLOR, tx, ty + tileHeight, 1, elevation * tileElevation);
}
if (cliffRight) {
batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, ty + tileHeight, 1, elevation * tileElevation);
}
if (cliffBottom) {
batch.drawRect(OUTLINE_2_COLOR, tx, ty + tileHeight - 1, tileWidth, 1);
batch.drawRect(OUTLINE_2_COLOR, tx, (ty + tileHeight + elevDiff * tileElevation) - 1, tileWidth, 1);
}
}
if (cliffTop) {
batch.drawRect(OUTLINE_2_COLOR, tx, ty, tileWidth, 1);
}
if (cliffLeft) {
batch.drawRect(OUTLINE_2_COLOR, tx, ty, 1, tileHeight);
}
if (cliffRight) {
batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, ty, 1, tileHeight);
}
if (cliffBottom) {
batch.drawRect(OUTLINE_COLOR, tx, ty + tileHeight - 1, tileWidth, 1);
}
if (options.gridLines) {
batch.drawRect(OUTLINE_COLOR, tx + tileWidth - 1, ty, 1, tileHeight);
batch.drawRect(OUTLINE_COLOR, tx, ty + tileHeight - 1, tileWidth - 1, 1);
// drawPixelText(batch, tx + 1, ty + 1, OUTLINE_COLOR, elevation.toString(10));
}
const rx = x + regionX;
const ry = y + regionY;
const tileIndex = region.tileIndices[x | (y << 3)];
// const tileTypeNumber = tileIndex >> 8;
const tileOffset = tileIndex & 0xff;
const grass = tileSets[3];
const baseX = (region.x * REGION_SIZE) | 0;
const baseY = (region.y * REGION_SIZE) | 0;
if (getTileNormal(region.tiles, baseX, baseY, x, y, map, TileType.None) === TileType.Grass) {
batch.drawSprite(grass.sprites[tileOffset], WHITE, grass.palette, rx * tileWidth, ry * tileHeight);
}
}
}
}
}
export function updateTileIndices(region: Region, map: IMap<Region | undefined>) {
for (let y = 0, i = 0; y < REGION_SIZE; y++) {
for (let x = 0; x < REGION_SIZE; x++ , i++) {
if (region.tileIndices[i] === -1) {
region.tileIndices[i] = getTileIndex(region, i, x, y, map);
}
}
}
region.tilesDirty = false;
region.lastTileUpdate = performance.now();
}
function tileTypeNumber(type: TileType) {
switch (type) {
case TileType.Water:
case TileType.WalkableWater:
return TileTypeNumber.Water;
case TileType.Wood:
return TileTypeNumber.Wood;
case TileType.Ice:
case TileType.WalkableIce:
return TileTypeNumber.Ice;
case TileType.SnowOnIce:
return TileTypeNumber.SnowOnIce;
case TileType.Stone:
return TileTypeNumber.Stone;
case TileType.Stone2:
return TileTypeNumber.Stone2;
case TileType.Boat:
return TileTypeNumber.Boat;
case TileType.Grass:
case TileType.Dirt:
case TileType.ElevatedDirt:
return TileTypeNumber.Grass;
case TileType.None:
case TileType.WallH:
case TileType.WallV:
return TileTypeNumber.None;
default:
return invalidEnumReturn(type, TileTypeNumber.None);
}
}
function normalizeTile(type: TileType, base: TileType) {
switch (type) {
case TileType.SnowOnIce:
return base === TileType.SnowOnIce ? type : TileType.Ice;
case TileType.WalkableIce:
return TileType.Ice;
case TileType.WalkableWater:
case TileType.Boat:
return TileType.Water;
case TileType.ElevatedDirt:
return TileType.Dirt;
default:
return type;
}
}
function normalizeTileBase(type: TileType) {
switch (type) {
case TileType.WalkableIce:
return TileType.Ice;
case TileType.WalkableWater:
case TileType.Boat:
return TileType.Water;
case TileType.ElevatedDirt:
return TileType.Dirt;
default:
return type;
}
}
function getTileNormal(
tiles: Uint8Array, baseX: number, baseY: number, x: number, y: number, map: IMap<Region | undefined>, base: TileType
) {
if (x >= 0 && y >= 0 && x < REGION_SIZE && y < REGION_SIZE) {
return normalizeTile(tiles[x | (y << 3)], base);
} else {
const mapX = clamp(x + baseX, 0, map.width - 1);
const mapY = clamp(y + baseY, 0, map.height - 1);
const region = getRegionGlobal(map, mapX, mapY);
if (region !== undefined) {
const regionX = mapX - region.x * REGION_SIZE;
const regionY = mapY - region.y * REGION_SIZE;
return normalizeTile(region.tiles[regionX | (regionY << 3)], base);
} else {
return TileType.None;
}
}
}
function getTileIndex(region: Region, index: number, x: number, y: number, map: IMap<Region | undefined>): number {
const tiles = region.tiles;
const type = tiles[x | (y << 3)] as TileType;
const tileType = tileTypeNumber(type);
let baseTileIndex = 0;
if (type === TileType.Dirt || type === TileType.ElevatedDirt) {
baseTileIndex = 47;
} else if (type !== TileType.None) {
let topLeft = 0, top = 0, topRight = 0, left = 0, right = 0, bottomLeft = 0, bottom = 0, bottomRight = 0;
if (x > 1 && y > 1 && x < (REGION_SIZE - 1) && y < (REGION_SIZE - 1)) {
topLeft = normalizeTile(tiles[(x - 1) | (y - 1) << 3], type);
top = normalizeTile(tiles[(x) | (y - 1) << 3], type);
topRight = normalizeTile(tiles[(x + 1) | (y - 1) << 3], type);
left = normalizeTile(tiles[(x - 1) | (y) << 3], type);
right = normalizeTile(tiles[(x + 1) | (y) << 3], type);
bottomLeft = normalizeTile(tiles[(x - 1) | (y + 1) << 3], type);
bottom = normalizeTile(tiles[(x) | (y + 1) << 3], type);
bottomRight = normalizeTile(tiles[(x + 1) | (y + 1) << 3], type);
} else {
const baseX = (region.x * REGION_SIZE) | 0;
const baseY = (region.y * REGION_SIZE) | 0;
topLeft = getTileNormal(tiles, baseX, baseY, x - 1, y - 1, map, type);
top = getTileNormal(tiles, baseX, baseY, x, y - 1, map, type);
topRight = getTileNormal(tiles, baseX, baseY, x + 1, y - 1, map, type);
left = getTileNormal(tiles, baseX, baseY, x - 1, y, map, type);
right = getTileNormal(tiles, baseX, baseY, x + 1, y, map, type);
bottomLeft = getTileNormal(tiles, baseX, baseY, x - 1, y + 1, map, type);
bottom = getTileNormal(tiles, baseX, baseY, x, y + 1, map, type);
bottomRight = getTileNormal(tiles, baseX, baseY, x + 1, y + 1, map, type);
}
const normalized = normalizeTileBase(type);
const index = 0
| ((topLeft === normalized) ? 1 : 0)
| ((top === normalized) ? 2 : 0)
| ((topRight === normalized) ? 4 : 0)
| ((left === normalized) ? 8 : 0)
| ((right === normalized) ? 16 : 0)
| ((bottomLeft === normalized) ? 32 : 0)
| ((bottom === normalized) ? 64 : 0)
| ((bottomRight === normalized) ? 128 : 0);
baseTileIndex = TILE_MAP[index];
}
const tileCount = type !== TileType.None ? TILE_COUNT_MAP[baseTileIndex] : 1;
const tileIndex = TILE_MAP_MAP[baseTileIndex] + (tileCount > 1 ? region.randoms[index] % tileCount : 0);
return (tileType << 8) | tileIndex;
}
const tileIndices = [
47, 47, 0, 0, 13, 19, 21, 20, 15, 16,
47, 47, 0, 0, 13, 13, 45, 22, 18, 17,
9, 2, 2, 2, 10, 14, 14, 14, 35, 36,
8, 5, null, 7, 4, 27, 26, 29, 37, 38,
8, null, 46, null, 4, 28, 24, 30, 39, 40,
8, 3, null, 1, 4, 23, 31, 32, 41, 42,
12, 6, 6, 6, 11, 25, 33, 34, 43, 44,
];
let tileHeightMaps = new Map<number, number[]>();
let tileHeightMapsInitialized = false;
function valueToHeight(value: number, bottom: number, top: number) {
return bottom + ((value / 255) * (top - bottom));
}
export function initializeTileHeightmaps() {
if (tileHeightMapsInitialized)
return;
function createTileHeightMaps(sprite: Sprite, tileType: TileTypeNumber, bottom: number, top: number) {
const sheetData = sprites.normalSpriteSheet.data!;
const tiles: number[][] = [];
for (let ty = 0; ty < 7; ty++) {
for (let tx = 0; tx < 10; tx++) {
const tile: number[] = [];
const baseX = tx * tileWidth + sprite.x;
const baseY = ty * tileHeight + sprite.y;
for (let y = 0, i = 0; y < tileHeight; y++) {
for (let x = 0; x < tileWidth; x++ , i++) {
const sx = baseX + x;
const sy = baseY + y;
const value = sheetData.data[(sx + sy * sheetData.width) * 4];
tile.push(valueToHeight(value, bottom, top));
}
}
tiles.push(tile);
}
}
tileHeightMapsInitialized = true;
const counts = new Uint8Array(100);
for (let i = 0; i < tileIndices.length; i++) {
const index = tileIndices[i];
if (index !== null) {
const key = (tileType << 8) | (TILE_MAP_MAP[index] + counts[index]);
tileHeightMaps.set(key, tiles[i]);
counts[index]++;
}
}
}
createTileHeightMaps(sprites.dirt_water_heightmap, TileTypeNumber.Water, -0.25, 0);
createTileHeightMaps(sprites.dirt_ice_heightmap, TileTypeNumber.Ice, -0.2, 0);
createTileHeightMaps(sprites.dirt_stone_cave_height_map, TileTypeNumber.Grass, 0.2, 0);
}
const waterHeight = WATER_HEIGHT.map(toWorldZ);
export function isInWater(tileIndex: number, x: number, y: number) {
const tileType = (tileIndex & 0xff00) >> 8;
if (tileType === TileTypeNumber.Water) {
const heightMaps = tileHeightMaps.get(tileIndex);
if (heightMaps !== undefined) {
const tx = clamp(toScreenX(x - Math.floor(x)), 0, tileWidth - 1) | 0;
const ty = clamp(toScreenY(y - Math.floor(y)), 0, tileHeight - 1) | 0;
return heightMaps[tx + ty * tileWidth] === -0.25;
}
}
return false;
}
export function getTileHeight(
tileType: TileType, tileIndex: number, x: number, y: number, gameTime: number, mapType: MapType
) {
const typeNumber = (tileIndex & 0xff00) >> 8;
if (
typeNumber === TileTypeNumber.Ice ||
typeNumber === TileTypeNumber.Water ||
(mapType === MapType.Cave && tileType === TileType.Grass)
) {
if (tileType !== TileType.WalkableWater && tileType !== TileType.WalkableIce) {
const heightMaps = tileHeightMaps.get(tileIndex);
if (heightMaps !== undefined) {
const tx = clamp(toScreenX(x - Math.floor(x)), 0, tileWidth - 1) | 0;
const ty = clamp(toScreenY(y - Math.floor(y)), 0, tileHeight - 1) | 0;
return heightMaps[tx + ty * tileWidth];
}
}
} else if (typeNumber === TileTypeNumber.SnowOnIce) {
return -0.2;
} else if (tileType === TileType.ElevatedDirt) {
return 0.5;
} else if (typeNumber === TileTypeNumber.Boat) {
const frame = ((gameTime / 1000) * WATER_FPS) | 0;
return waterHeight[frame % waterHeight.length];
}
return 0;
}
+107
View File
@@ -0,0 +1,107 @@
interface TimingEntry {
time: number;
name?: string;
}
interface TimingResult {
name: string;
count: number;
selfTime: number;
totalTime: number;
selfPercent: number;
totalPercent: number;
}
const ENABLED = false;
const ENTRIES_LIMIT = 8000;
const entries: TimingEntry[] = [];
let entriesCount = 0;
if (TIMING && ENABLED) {
for (let i = 0; i < ENTRIES_LIMIT; i++) {
entries.push({ time: 0, name: undefined });
}
}
export function timeStart(name: string) {
if (TIMING && ENABLED) {
if (entriesCount < ENTRIES_LIMIT) {
const entry = entries[entriesCount];
entry.time = performance.now();
entry.name = name;
entriesCount++;
} else {
console.warn(`exceeded timing entry limit`);
}
}
}
export function timeEnd() {
if (TIMING && ENABLED) {
if (entriesCount < ENTRIES_LIMIT) {
const entry = entries[entriesCount];
entry.time = performance.now();
entry.name = undefined;
entriesCount++;
} else {
console.warn(`exceeded timing entry limit`);
}
}
}
export function timeReset() {
if (TIMING && ENABLED) {
entriesCount = 0;
}
}
export function timingCollate(): TimingResult[] {
if (TIMING && ENABLED && entriesCount > 0) {
interface Entry extends TimingEntry {
excludedTime: number;
}
const listings: TimingResult[] = [];
const startStack: Entry[] = [];
for (let i = 0; i < entriesCount; i++) {
const entry = entries[i];
if (entry.name !== undefined) {
startStack.push({ ...entry, excludedTime: 0 });
} else {
const start = startStack.pop()!;
const name = start.name!;
const time = entry.time - start.time;
let listing = listings.find(l => l.name === name);
if (!listing) {
listing = { name, selfTime: 0, totalTime: 0, selfPercent: 0, totalPercent: 0, count: 0 };
listings.push(listing);
}
listing.count++;
listing.selfTime += (time - start.excludedTime);
listing.totalTime += time;
if (startStack.length) {
startStack[startStack.length - 1].excludedTime += time;
}
}
}
const firstTime = entries[0].time;
const lastTime = entries[entriesCount - 1].time;
const totalTime = lastTime - firstTime;
for (const listing of listings) {
listing.selfPercent = 100 * listing.selfTime / totalTime;
listing.totalPercent = 100 * listing.totalTime / totalTime;
}
return listings.sort((a, b) => b.selfTime - a.selfTime);
}
return [];
}
+136
View File
@@ -0,0 +1,136 @@
import { spriteShader, paletteLayersShader, lightShader } from '../generated/shaders';
import { FrameBuffer, createFrameBuffer, disposeFrameBuffer } from '../graphics/webgl/frameBuffer';
import { SpriteSheet, CommonPalettes, PaletteManager, Camera } from '../common/interfaces';
import { Shader, createShader, disposeShader } from '../graphics/webgl/shader';
import { PaletteSpriteBatch, PALETTE_BATCH_BYTES_PER_VERTEX } from '../graphics/paletteSpriteBatch';
import { getWebGLContext, getRenderTargetSize, unbindAllTexturesAndBuffers } from '../graphics/webgl/webglUtils';
import { createCommonPalettes } from '../graphics/graphicsUtils';
import { createTexturesForSpriteSheets, disposeTexturesForSpriteSheets } from '../graphics/spriteSheetUtils';
import * as sprites from '../generated/sprites';
import { SpriteBatch } from '../graphics/spriteBatch';
import { BATCH_SIZE_MAX } from '../common/constants';
export interface WebGL {
gl: WebGLRenderingContext;
frameBuffer: FrameBuffer | undefined;
frameBufferSheet: SpriteSheet;
spriteShader: Shader;
lightShader: Shader;
paletteShader: Shader;
spriteBatch: SpriteBatch;
paletteBatch: PaletteSpriteBatch;
palettes: CommonPalettes;
failedFBO: boolean;
renderer: string;
}
const spriteShaderSource = spriteShader;
const paletteShaderSource = paletteLayersShader;
const lightShaderSource = lightShader;
function createIndices(capacity: number) {
const numIndices = (capacity * 6) | 0;
const indices = new Uint16Array(numIndices);
for (let i = 0, j = 0; i < numIndices; j = (j + 4) | 0) {
indices[i++] = (j + 0) | 0;
indices[i++] = (j + 1) | 0;
indices[i++] = (j + 2) | 0;
indices[i++] = (j + 0) | 0;
indices[i++] = (j + 2) | 0;
indices[i++] = (j + 3) | 0;
}
return indices;
}
export function initWebGL(canvas: HTMLCanvasElement, paletteManager: PaletteManager, camera: Camera): WebGL {
const gl = getWebGLContext(canvas);
return initWebGLResources(gl, paletteManager, camera);
}
export function initWebGLResources(gl: WebGLRenderingContext, paletteManager: PaletteManager, camera: Camera): WebGL {
let renderer = '';
let failedFBO = false;
let frameBuffer: FrameBuffer | undefined;
let frameBufferSheet: SpriteSheet = { texture: undefined, sprites: [], palette: false };
try {
const size = getRenderTargetSize(camera.w, camera.h);
frameBuffer = createFrameBuffer(gl, size, size);
frameBufferSheet.texture = frameBuffer.texture;
} catch (e) {
DEVELOPMENT && console.warn(e);
failedFBO = true;
}
createTexturesForSpriteSheets(gl, sprites.spriteSheets);
const palettes = createCommonPalettes(paletteManager);
const paletteShader = createShader(gl, paletteShaderSource);
const spriteShader = createShader(gl, spriteShaderSource);
const lightShader = createShader(gl, lightShaderSource);
const VERTICES_PER_SPRITE = 4;
const buffer = new ArrayBuffer(BATCH_SIZE_MAX * VERTICES_PER_SPRITE * PALETTE_BATCH_BYTES_PER_VERTEX);
const vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
throw new Error(`Failed to allocate vertex buffer`);
}
const indexBuffer = gl.createBuffer();
if (!indexBuffer) {
throw new Error(`Failed to allocate index buffer`);
}
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, createIndices(BATCH_SIZE_MAX), gl.STATIC_DRAW);
const vertexBuffer2 = gl.createBuffer();
if (!vertexBuffer2) {
throw new Error(`Failed to allocate vertex buffer (2)`);
}
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer2);
gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW);
const spriteBatch = new SpriteBatch(gl, BATCH_SIZE_MAX, buffer, vertexBuffer2, indexBuffer);
const paletteBatch = new PaletteSpriteBatch(gl, BATCH_SIZE_MAX, buffer, vertexBuffer, indexBuffer);
spriteBatch.rectSprite = sprites.pixel;
paletteBatch.rectSprite = sprites.pixel2;
paletteBatch.defaultPalette = palettes.defaultPalette;
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
paletteManager.init(gl);
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (debugInfo) {
renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
}
return {
gl, paletteShader, spriteShader, lightShader, spriteBatch, paletteBatch,
frameBuffer, frameBufferSheet, palettes, failedFBO, renderer,
};
}
export function disposeWebGL(webgl: WebGL) {
const { gl } = webgl;
unbindAllTexturesAndBuffers(gl);
disposeTexturesForSpriteSheets(gl, sprites.spriteSheets);
disposeFrameBuffer(gl, webgl.frameBuffer);
disposeShader(gl, webgl.lightShader);
disposeShader(gl, webgl.spriteShader);
disposeShader(gl, webgl.paletteShader);
webgl.spriteBatch.dispose();
webgl.paletteBatch.dispose();
}