mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-24 13:55:04 +02:00
853 lines
25 KiB
TypeScript
853 lines
25 KiB
TypeScript
import { sample } from 'lodash';
|
|
import { createBinaryWriter, resetWriter } from 'ag-sockets';
|
|
import { IClient, ServerEntity, Reporter, TokenData, ServerMap } from './serverInterfaces';
|
|
import { IAccount, ICharacter, UpdateAccount, IOriginInfo, findFriendIds } from './db';
|
|
import * as entities from '../common/entities';
|
|
import { isShadowed, isMuted, supporterLevel } from '../common/adminUtils';
|
|
import { handlePromiseDefault } from './serverUtils';
|
|
import {
|
|
removeItem, hasFlag, distance, toInt, includes, array, flatten, containsPointWitBorder, distanceXY, setFlag, invalidEnum
|
|
} from '../common/utils';
|
|
import { CharacterState, ServerConfig, AccountState, CharacterStateFlags, GameServerSettings } from '../common/adminInterfaces';
|
|
import {
|
|
EntityState, Expression, PonyOptions, Action, ExpressionExtra, Eye, Muzzle, getMuzzleOpenness,
|
|
isExpressionAction, EntityPlayerState, UpdateFlags, InteractAction, Rect
|
|
} from '../common/interfaces';
|
|
import { encodeExpression, EMPTY_EXPRESSION, decodeExpression } from '../common/encoders/expressionEncoder';
|
|
import { EXPRESSION_TIMEOUT, DAY, FLY_DELAY, SECOND, PONY_TYPE } from '../common/constants';
|
|
import { World } from './world';
|
|
import { centerCameraOn, createCamera } from '../common/camera';
|
|
import { findEntitiesInBounds } from './serverMap';
|
|
import { CounterService } from './services/counter';
|
|
import { create } from './reporter';
|
|
import { updateAccountState } from './accountUtils';
|
|
import { isMod } from '../common/accountUtils';
|
|
import { getOriginFromHTTP } from './originUtils';
|
|
import { createPony, getAndFixCharacterState, updateCharacterState } from './characterUtils';
|
|
import {
|
|
updateEntityOptions, canBoopEntity, findPlayersThetCanBeSitOn, updateEntityState, updateEntityExpression,
|
|
sendAction, pushUpdateEntityToClient, fixPosition, isHoldingGrapes
|
|
} from './entityUtils';
|
|
import { replaceEmojis } from '../client/emoji';
|
|
import { expression, parseExpression } from '../common/expressionUtils';
|
|
import {
|
|
canBoopOrKiss, isPonySitting, isPonyStanding, getBoopRect, canStand, isPonyFlying, setPonyState, canSit,
|
|
canLie, getkissRect, getSneezeRect
|
|
} from '../common/entityUtils';
|
|
import { withBorder } from '../common/rect';
|
|
import { isOnlineFriend } from './services/friends';
|
|
import { grapePurple, grapeGreen, tools } from '../common/entities';
|
|
import { saySystem } from './chat';
|
|
|
|
export function isMutedOrShadowed(client: IClient) {
|
|
return client.shadowed || isMuted(client.account);
|
|
}
|
|
|
|
export function isIgnored(ignoring: IClient, target: IClient): boolean {
|
|
return target.ignores.has(ignoring.accountId);
|
|
}
|
|
|
|
export function kickClient(client: IClient, reason = 'kicked') {
|
|
client.leaveReason = reason;
|
|
client.disconnect(true, true);
|
|
}
|
|
|
|
export function getCounter(client: IClient, key: keyof AccountState) {
|
|
return toInt(client.account.state && client.account.state[key]);
|
|
}
|
|
|
|
export function createClientAndPony(
|
|
client: IClient, friends: string[], hides: string[], server: ServerConfig, world: World, states: CounterService<CharacterState>
|
|
) {
|
|
const { account, character } = client.tokenData as TokenData;
|
|
const origin = client.originalRequest && getOriginFromHTTP(client.originalRequest);
|
|
const reporter = create(server, account._id, character._id, origin);
|
|
const state = getAndFixCharacterState(server, character, world, states);
|
|
|
|
client.characterState = state;
|
|
const pony = createPony(account, character, state);
|
|
pony.client = createClient(client, account, friends, hides, character, pony, world.getMainMap(), reporter, origin);
|
|
centerCameraOn(client.camera, pony);
|
|
}
|
|
|
|
export function updateClientCharacter(client: IClient, character: ICharacter) {
|
|
client.character = character;
|
|
client.characterId = client.character._id.toString();
|
|
client.characterName = replaceEmojis(client.character.name);
|
|
}
|
|
|
|
function isMobileUserAgent(userAgent?: string) {
|
|
if (!userAgent) {
|
|
return false;
|
|
}
|
|
return userAgent.includes('Android') ||
|
|
userAgent.includes('iPhone') ||
|
|
userAgent.includes('iPad') ||
|
|
userAgent.includes('iPod') ||
|
|
userAgent.includes('Windows Phone');
|
|
}
|
|
|
|
export function createClient(
|
|
client: IClient, account: IAccount, friends: string[], hides: string[], character: ICharacter, pony: ServerEntity,
|
|
defaultMap: ServerMap, reporter: Reporter, origin: IOriginInfo | undefined
|
|
): IClient {
|
|
updateClientCharacter(client, character);
|
|
|
|
client.ip = origin && origin.ip || '';
|
|
client.country = origin && origin.country || '??';
|
|
client.userAgent = client.originalRequest && client.originalRequest.headers['user-agent'];
|
|
|
|
client.isMobile = isMobileUserAgent(client.userAgent);
|
|
client.accountId = account._id.toString();
|
|
client.accountName = account.name;
|
|
client.ignores = new Set(account.ignores);
|
|
client.hides = new Set();
|
|
client.permaHides = new Set(hides);
|
|
client.friends = new Set(friends);
|
|
client.friendsCRC = undefined;
|
|
client.accountSettings = { ...account.settings };
|
|
client.supporterLevel = supporterLevel(account);
|
|
client.isMod = isMod(account);
|
|
|
|
client.reporter = reporter;
|
|
client.account = account;
|
|
client.character = character;
|
|
client.pony = pony;
|
|
client.map = defaultMap;
|
|
client.isSwitchingMap = false;
|
|
|
|
client.notifications = [];
|
|
client.regions = [];
|
|
|
|
client.shadowed = isShadowed(account);
|
|
client.country = origin && origin.country || '??';
|
|
|
|
client.camera = createCamera();
|
|
client.camera.w = 800;
|
|
client.camera.h = 600;
|
|
|
|
client.safeX = pony.x;
|
|
client.safeY = pony.y;
|
|
|
|
client.lastPacket = Date.now();
|
|
client.lastBoopOrKissAction = 0;
|
|
client.lastExpressionAction = 0;
|
|
client.lastSays = [];
|
|
client.lastX = pony.x;
|
|
client.lastY = pony.y;
|
|
client.lastTime = 0;
|
|
client.lastVX = 0;
|
|
client.lastVY = 0;
|
|
client.lastMapSwitch = 0;
|
|
|
|
client.lastSitX = 0;
|
|
client.lastSitY = 0;
|
|
client.lastSitTime = 0;
|
|
client.sitCount = 0;
|
|
|
|
client.lastSwap = 0;
|
|
client.lastMapLoadOrSave = 0;
|
|
|
|
client.lastCameraX = 0;
|
|
client.lastCameraY = 0;
|
|
client.lastCameraW = 0;
|
|
client.lastCameraH = 0;
|
|
|
|
client.updateQueue = createBinaryWriter(128);
|
|
client.regionUpdates = [];
|
|
client.saysQueue = [];
|
|
client.unsubscribes = [];
|
|
client.subscribes = [];
|
|
|
|
client.positions = [];
|
|
|
|
return client;
|
|
}
|
|
|
|
export function resetClientUpdates(client: IClient) {
|
|
resetWriter(client.updateQueue);
|
|
client.regionUpdates.length = 0;
|
|
client.saysQueue.length = 0;
|
|
client.unsubscribes.length = 0;
|
|
client.subscribes.length = 0;
|
|
}
|
|
|
|
export function createCharacterState(entity: ServerEntity, map: ServerMap): CharacterState {
|
|
const options = entity.options as PonyOptions;
|
|
const flags: CharacterStateFlags =
|
|
(hasFlag(entity.state, EntityState.FacingRight) ? CharacterStateFlags.Right : 0) |
|
|
(options.extra ? CharacterStateFlags.Extra : 0);
|
|
const state: CharacterState = { x: entity.x, y: entity.y };
|
|
|
|
if (flags) {
|
|
state.flags = flags;
|
|
}
|
|
|
|
if (map.id) {
|
|
state.map = map.id;
|
|
}
|
|
|
|
if (options.hold) {
|
|
state.hold = entities.getEntityTypeName(options.hold);
|
|
}
|
|
|
|
if (options.toy) {
|
|
state.toy = options.toy;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
export async function createAndUpdateCharacterState(client: IClient, server: ServerConfig) {
|
|
const state = createCharacterState(client.pony, client.map);
|
|
await updateCharacterState(client.characterId, server.id, state);
|
|
}
|
|
|
|
// utils
|
|
|
|
export function addIgnore(target: IClient, accountId: string) {
|
|
target.account.ignores = target.account.ignores || [];
|
|
target.account.ignores.push(accountId);
|
|
target.ignores.add(accountId);
|
|
}
|
|
|
|
export function removeIgnore(target: IClient, accountId: string) {
|
|
if (target.account.ignores) {
|
|
removeItem(target.account.ignores, accountId);
|
|
}
|
|
|
|
target.ignores.delete(accountId);
|
|
}
|
|
|
|
export const createIgnorePlayer =
|
|
(updateAccount: UpdateAccount, handlePromise = handlePromiseDefault) =>
|
|
(client: IClient, target: IClient, ignored: boolean) => {
|
|
if (target.accountId === client.accountId)
|
|
return;
|
|
|
|
const id = client.accountId;
|
|
const is = isIgnored(client, target);
|
|
|
|
if (ignored === is)
|
|
return;
|
|
|
|
if (ignored) {
|
|
addIgnore(target, id);
|
|
} else {
|
|
removeIgnore(target, id);
|
|
}
|
|
|
|
handlePromise(updateAccount(target.accountId, { [ignored ? '$push' : '$pull']: { ignores: id } })
|
|
.then(() => updateEntityPlayerState(client, target.pony))
|
|
.then(() => {
|
|
const { accountId, account, character } = target;
|
|
const message = `${ignored ? 'ignored' : 'unignored'} ${character.name} (${account.name}) [${accountId}]`;
|
|
client.reporter.systemLog(message);
|
|
}), client.reporter.error);
|
|
};
|
|
|
|
export function findClientByEntityId(self: IClient, entityId: number): IClient | undefined {
|
|
const selected = self.selected;
|
|
|
|
if (selected && selected.id === entityId && selected.client) {
|
|
return selected.client;
|
|
}
|
|
|
|
if (self.party) { // TODO: remove ?
|
|
const client = self.party.clients.find(c => c.pony.id === entityId);
|
|
|
|
if (client) {
|
|
//this.logger.log('client from party');
|
|
return client;
|
|
}
|
|
|
|
const pending = self.party.pending.find(c => c.client.pony.id === entityId);
|
|
|
|
if (pending) {
|
|
//this.logger.log('pending from party');
|
|
return pending.client;
|
|
}
|
|
}
|
|
|
|
const notification = self.notifications.find(c => c.entityId === entityId);
|
|
|
|
if (notification) {
|
|
//this.logger.log('sender from notification');
|
|
return notification.sender;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function cancelEntityExpression(entity: ServerEntity) {
|
|
if (entity.exprCancellable) {
|
|
setEntityExpression(entity, undefined);
|
|
}
|
|
}
|
|
|
|
export function setEntityExpression(
|
|
entity: ServerEntity, expression: Expression | undefined, timeout = EXPRESSION_TIMEOUT, cancellable = false
|
|
) {
|
|
expression = expression || entity.exprPermanent;
|
|
const expr = encodeExpression(expression);
|
|
|
|
(entity.options as PonyOptions).expr = expr;
|
|
|
|
if (expression && timeout) {
|
|
entity.exprTimeout = Date.now() + timeout;
|
|
} else {
|
|
entity.exprTimeout = undefined;
|
|
}
|
|
|
|
const sleeping = expression !== undefined && hasFlag(expression.extra, ExpressionExtra.Zzz);
|
|
entity.exprCancellable = cancellable || sleeping;
|
|
|
|
updateEntityExpression(entity);
|
|
}
|
|
|
|
export function playerBlush(pony: ServerEntity, args = '') {
|
|
const expr = parseOrCurrentExpression(pony, args) || expression(Eye.Neutral, Eye.Neutral, Muzzle.Neutral);
|
|
expr.extra |= ExpressionExtra.Blush;
|
|
setEntityExpression(pony, expr, DAY, !!pony.exprCancellable);
|
|
}
|
|
|
|
export function parseOrCurrentExpression(pony: ServerEntity, message: string) {
|
|
return parseExpression(message)
|
|
|| decodeExpression((!pony.options || pony.options.expr == null) ? EMPTY_EXPRESSION : pony.options.expr);
|
|
}
|
|
|
|
export function playerSleep(pony: ServerEntity, args = '') {
|
|
if (pony.vx === 0 && pony.vy === 0) {
|
|
const base = parseOrCurrentExpression(pony, args) || expression(Eye.Closed, Eye.Closed, Muzzle.Neutral);
|
|
const muzzle = getMuzzleOpenness(base.muzzle) === 0 ? base.muzzle : Muzzle.Neutral;
|
|
const expr = { ...base, muzzle, left: Eye.Closed, right: Eye.Closed, extra: ExpressionExtra.Zzz };
|
|
setEntityExpression(pony, expr, 0, true);
|
|
}
|
|
}
|
|
|
|
export function playerLove(pony: ServerEntity, args = '') {
|
|
const expr = parseOrCurrentExpression(pony, args) || expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile);
|
|
expr.extra |= ExpressionExtra.Hearts;
|
|
setEntityExpression(pony, expr, DAY, !!pony.exprCancellable);
|
|
}
|
|
|
|
export function playerCry(pony: ServerEntity, args = '') {
|
|
const expr = parseExpression(args) || expression(Eye.Sad, Eye.Sad, Muzzle.Frown);
|
|
expr.extra = expr.extra | ExpressionExtra.Cry;
|
|
setEntityExpression(pony, expr, 0);
|
|
}
|
|
|
|
const fruitTypes = entities.fruits.map(f => f.type);
|
|
|
|
export function interactWith(client: IClient, target: ServerEntity | undefined) {
|
|
if (target) {
|
|
const pony = client.pony;
|
|
|
|
if (target.interact && (!target.interactRange || distance(pony, target) < target.interactRange)) {
|
|
target.interact(target, client);
|
|
} else if (target.triggerBounds && target.trigger) {
|
|
if (containsPointWitBorder(target.x, target.y, target.triggerBounds, pony.x, pony.y, 3)) {
|
|
target.trigger(target, client);
|
|
} else {
|
|
DEVELOPMENT && console.warn(`outside trigger bounds ` +
|
|
`(bounds: ${target.x} ${target.y} ${JSON.stringify(target.triggerBounds)} point: ${pony.x} ${pony.y})`);
|
|
}
|
|
} else if (target.interactAction) {
|
|
switch (target.interactAction) {
|
|
case InteractAction.Toolbox: {
|
|
switchTool(client, false);
|
|
break;
|
|
}
|
|
case InteractAction.GiveLantern: {
|
|
if (client.pony.options!.hold === entities.lanternOn.type) {
|
|
unholdItem(pony);
|
|
} else {
|
|
holdItem(pony, entities.lanternOn.type);
|
|
}
|
|
break;
|
|
}
|
|
case InteractAction.GiveFruits: {
|
|
const index = fruitTypes.indexOf(client.pony.options!.hold || 0) + 1;
|
|
holdItem(client.pony, fruitTypes[index % fruitTypes.length]);
|
|
break;
|
|
}
|
|
case InteractAction.GiveCookie1: {
|
|
const hold = client.pony.options!.hold;
|
|
let cookie = hold;
|
|
|
|
while (hold === cookie) {
|
|
cookie = sample(entities.candies1Types)!;
|
|
}
|
|
|
|
holdItem(client.pony, cookie!);
|
|
break;
|
|
}
|
|
case InteractAction.GiveCookie2: {
|
|
const hold = client.pony.options!.hold;
|
|
let cookie = hold;
|
|
|
|
while (hold === cookie) {
|
|
cookie = sample(entities.candies2Types)!;
|
|
}
|
|
|
|
holdItem(client.pony, cookie!);
|
|
break;
|
|
}
|
|
default:
|
|
invalidEnum(target.interactAction);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function useHeldItem(client: IClient) {
|
|
const hold = client.pony.options!.hold || 0;
|
|
|
|
if (isGift(hold)) {
|
|
openGift(client);
|
|
}
|
|
}
|
|
|
|
export function canPerformAction(client: IClient, now?: number) {
|
|
if (!now) {
|
|
now = Date.now();
|
|
}
|
|
return client.lastExpressionAction < now && client.lastBoopOrKissAction < now;
|
|
}
|
|
|
|
export function updateEntityPlayerState(client: IClient, entity: ServerEntity) {
|
|
const playerState = getPlayerState(client, entity);
|
|
pushUpdateEntityToClient(client, { entity, flags: UpdateFlags.PlayerState, playerState });
|
|
}
|
|
|
|
// actions
|
|
|
|
export function turnHead(client: IClient) {
|
|
updateEntityState(client.pony, client.pony.state ^ EntityState.HeadTurned);
|
|
}
|
|
|
|
const purpleGrapeTypes = entities.grapesPurple.map(x => x.type);
|
|
const greenGrapeTypes = entities.grapesGreen.map(x => x.type);
|
|
|
|
function boopEntity(client: IClient, rect: Rect, isOnlyBooping: boolean) {
|
|
if (!client.shadowed && (isPonySitting(client.pony) || isPonyStanding(client.pony))) {
|
|
const boopBounds = withBorder(rect, 1);
|
|
const entities = findEntitiesInBounds(client.map, boopBounds);
|
|
const entity = entities.find(e => canBoopEntity(e, rect));
|
|
|
|
if (entity) {
|
|
if (entity.boop) {
|
|
entity.boop(client);
|
|
} else if (!isOnlyBooping && entity.type === PONY_TYPE) {
|
|
const clientHold = client.pony.options!.hold || 0;
|
|
|
|
if (isHoldingGrapes(entity) && clientHold !== grapeGreen.type && clientHold !== grapePurple.type) {
|
|
let index = purpleGrapeTypes.indexOf(entity.options!.hold || 0);
|
|
|
|
if (index !== -1) {
|
|
holdItem(client.pony, grapePurple.type);
|
|
|
|
if (index === (purpleGrapeTypes.length - 1)) {
|
|
unholdItem(entity);
|
|
} else {
|
|
holdItem(entity, purpleGrapeTypes[index + 1]);
|
|
}
|
|
} else {
|
|
let index = greenGrapeTypes.indexOf(entity.options!.hold || 0);
|
|
|
|
if (index !== -1) {
|
|
holdItem(client.pony, grapeGreen.type);
|
|
|
|
if (index === (greenGrapeTypes.length - 1)) {
|
|
unholdItem(entity);
|
|
} else {
|
|
holdItem(entity, greenGrapeTypes[index + 1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function boop(client: IClient, now: number) {
|
|
if (canPerformAction(client, now) && canBoopOrKiss(client.pony)) {
|
|
cancelEntityExpression(client.pony);
|
|
sendAction(client.pony, Action.Boop);
|
|
boopEntity(client, getBoopRect(client.pony), false);
|
|
client.lastBoopOrKissAction = now + 850;
|
|
}
|
|
}
|
|
|
|
export function kiss(client: IClient, now: number) {
|
|
if (canPerformAction(client, now) && canBoopOrKiss(client.pony)) {
|
|
cancelEntityExpression(client.pony);
|
|
sendAction(client.pony, Action.Kiss);
|
|
boopEntity(client, getkissRect(client.pony), false);
|
|
client.lastBoopOrKissAction = now + 3400;
|
|
}
|
|
}
|
|
|
|
export function sneeze(client: IClient) {
|
|
const now = Date.now();
|
|
if (canPerformAction(client, now)) {
|
|
cancelEntityExpression(client.pony);
|
|
sendAction(client.pony, Action.Sneeze);
|
|
boopEntity(client, getSneezeRect(client.pony), true);
|
|
client.lastExpressionAction = now + 750;
|
|
}
|
|
}
|
|
|
|
export function stand(client: IClient) {
|
|
if (canPerformAction(client) && canStand(client.pony, client.map)) {
|
|
if (!isPonyFlying(client.pony)) {
|
|
cancelEntityExpression(client.pony);
|
|
}
|
|
|
|
updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyStanding));
|
|
}
|
|
}
|
|
|
|
const SIT_MAX_TIME = 2 * SECOND;
|
|
const SIT_MAX_DIST = 1;
|
|
const SIT_MAX_COUNT = 5;
|
|
|
|
function checkSuspiciousSitting(client: IClient) {
|
|
const now = Date.now();
|
|
const { x, y } = client.pony;
|
|
const dist = distanceXY(x, y, client.lastSitX, client.lastSitY);
|
|
|
|
if ((now - client.lastSitTime) < SIT_MAX_TIME && dist < SIT_MAX_DIST && findPlayersThetCanBeSitOn(client.map, client.pony)) {
|
|
client.sitCount++;
|
|
|
|
if (client.sitCount > SIT_MAX_COUNT) {
|
|
client.reporter.warn(`Suspicious sitting`);
|
|
client.sitCount = 0;
|
|
}
|
|
} else {
|
|
client.sitCount = 1;
|
|
}
|
|
|
|
client.lastSitX = x;
|
|
client.lastSitY = y;
|
|
client.lastSitTime = now;
|
|
}
|
|
|
|
export function sit(client: IClient, settings: GameServerSettings) {
|
|
if (canPerformAction(client) && canSit(client.pony, client.map)) {
|
|
updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonySitting));
|
|
|
|
if (settings.reportSitting) {
|
|
checkSuspiciousSitting(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function lie(client: IClient) {
|
|
if (canPerformAction(client) && canLie(client.pony, client.map)) {
|
|
updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyLying));
|
|
}
|
|
}
|
|
|
|
export function fly(client: IClient) {
|
|
if (canPerformAction(client) && client.pony.canFly && !isPonyFlying(client.pony)) {
|
|
cancelEntityExpression(client.pony);
|
|
updateEntityState(client.pony, setPonyState(client.pony.state, EntityState.PonyFlying));
|
|
client.pony.inTheAirDelay = FLY_DELAY;
|
|
}
|
|
}
|
|
|
|
export function expressionAction(client: IClient, action: Action) {
|
|
const now = Date.now();
|
|
if (canPerformAction(client, now) && isExpressionAction(action)) {
|
|
cancelEntityExpression(client.pony);
|
|
sendAction(client.pony, action);
|
|
client.lastExpressionAction = now + 750;
|
|
}
|
|
}
|
|
|
|
// hold
|
|
|
|
export function holdItem(entity: ServerEntity, hold: number) {
|
|
if (entity.options && entity.options.hold !== hold) {
|
|
updateEntityOptions(entity, { hold });
|
|
}
|
|
}
|
|
|
|
export function unholdItem(entity: ServerEntity) {
|
|
if (entity.options && entity.options.hold) {
|
|
updateEntityOptions(entity, { hold: 0 });
|
|
delete entity.options.hold;
|
|
}
|
|
}
|
|
|
|
// toy
|
|
|
|
export function holdToy(entity: ServerEntity, toy: number) {
|
|
if (entity.options && entity.options.toy !== toy) {
|
|
updateEntityOptions(entity, { toy });
|
|
}
|
|
}
|
|
|
|
export function unholdToy(entity: ServerEntity) {
|
|
if (entity.options && entity.options.toy) {
|
|
updateEntityOptions(entity, { toy: 0 });
|
|
delete entity.options.toy;
|
|
}
|
|
}
|
|
|
|
// gifts and toys
|
|
|
|
const giftTypes = [entities.gift2.type];
|
|
|
|
const toys = [
|
|
// hat
|
|
{ type: 0, multiplier: 20 },
|
|
{ type: 0, multiplier: 10 },
|
|
{ type: 0, multiplier: 5 },
|
|
{ type: 0, multiplier: 1 }, // pink
|
|
// snowpony
|
|
{ type: 0, multiplier: 20 },
|
|
{ type: 0, multiplier: 10 }, // clothes
|
|
{ type: 0, multiplier: 1 }, // evil
|
|
// gift
|
|
{ type: 0, multiplier: 20 },
|
|
{ type: 0, multiplier: 10 },
|
|
{ type: 0, multiplier: 10 },
|
|
{ type: 0, multiplier: 5 },
|
|
{ type: 0, multiplier: 1 },
|
|
// hanging thing
|
|
{ type: 0, multiplier: 20 }, // bell
|
|
{ type: 0, multiplier: 10 }, // mistletoe
|
|
{ type: 0, multiplier: 5 }, // cookie
|
|
{ type: 0, multiplier: 1 }, // spider
|
|
// teddy
|
|
{ type: 0, multiplier: 20 }, // brown
|
|
{ type: 0, multiplier: 10 }, // brown angel
|
|
{ type: 0, multiplier: 20 }, // black
|
|
{ type: 0, multiplier: 10 }, // black angel
|
|
{ type: 0, multiplier: 5 }, // brown clothes
|
|
{ type: 0, multiplier: 5 }, // black clothes
|
|
{ type: 0, multiplier: 1 }, // white santa
|
|
// xmas tree
|
|
{ type: 0, multiplier: 10 },
|
|
{ type: 0, multiplier: 5 },
|
|
// deer
|
|
{ type: 0, multiplier: 5 },
|
|
{ type: 0, multiplier: 1 }, // with clothes
|
|
// candy horns
|
|
{ type: 0, multiplier: 10 }, // one
|
|
{ type: 0, multiplier: 2 }, // two
|
|
{ type: 0, multiplier: 1 }, // two (alt)
|
|
// star
|
|
{ type: 0, multiplier: 5 },
|
|
// halo
|
|
{ type: 0, multiplier: 5 },
|
|
];
|
|
|
|
toys.forEach((toy, i) => toy.type = i + 1);
|
|
|
|
const toyTypes = flatten(toys.map(x => array(x.multiplier, x.type)));
|
|
|
|
function hasToyUnlocked(type: number, collectedToys: number) {
|
|
const index = toys.findIndex(t => t.type === type);
|
|
return hasFlag(collectedToys, 1 << index);
|
|
}
|
|
|
|
function unlockToy(type: number, collectedToys: number) {
|
|
const index = toys.findIndex(t => t.type === type);
|
|
return collectedToys | (1 << index);
|
|
}
|
|
|
|
export function getCollectedToysCount(client: IClient) {
|
|
const stateToys = toInt((client.account.state || {}).toys);
|
|
const total = toys.length;
|
|
let collected = 0;
|
|
|
|
for (let i = 0, bit = 1; i < total; i++ , bit <<= 1) {
|
|
if (stateToys & bit) {
|
|
collected++;
|
|
}
|
|
}
|
|
|
|
return { collected, total };
|
|
}
|
|
|
|
export function getNextToyOrExtra(client: IClient) {
|
|
const collectedToys = toInt((client.account.state || {}).toys);
|
|
const options = client.pony.options || {};
|
|
const extra = !!options.extra;
|
|
const toy = toInt(options.toy);
|
|
|
|
if (extra) {
|
|
return { extra: false, toy: 0 };
|
|
} else {
|
|
for (let i = toys.findIndex(t => t.type === toy) + 1; i < toys.length; i++) {
|
|
const type = toys[i].type;
|
|
|
|
if (hasToyUnlocked(type, collectedToys)) {
|
|
return { extra: false, toy: type };
|
|
}
|
|
}
|
|
|
|
return { extra: true, toy: 0 };
|
|
}
|
|
}
|
|
|
|
export function openGift(client: IClient) {
|
|
const options = client.pony.options || {};
|
|
|
|
if (isGift(options.hold)) {
|
|
let toyType = 0;
|
|
|
|
do {
|
|
toyType = sample(toyTypes)!;
|
|
} while (toyType === options.toy);
|
|
|
|
sendAction(client.pony, Action.HoldPoof);
|
|
unholdItem(client.pony);
|
|
setTimeout(() => holdToy(client.pony, toyType), 200);
|
|
|
|
const state = client.account.state || {};
|
|
|
|
if (!hasToyUnlocked(toyType, toInt(state.toys))) {
|
|
updateAccountState(client.account, state => {
|
|
state.toys = unlockToy(toyType, toInt(state.toys));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export function isGift(type: number | undefined) {
|
|
return type !== undefined && includes(giftTypes, type);
|
|
}
|
|
|
|
export function isHiddenBy(a: IClient, b: IClient) {
|
|
return a.hides.has(b.accountId) || b.hides.has(a.accountId) ||
|
|
a.permaHides.has(b.accountId) || b.permaHides.has(a.accountId);
|
|
}
|
|
|
|
export function getPlayerState(client: IClient, entity: ServerEntity): EntityPlayerState {
|
|
let state = EntityPlayerState.None;
|
|
|
|
if (entity.client !== undefined) {
|
|
if (isIgnored(client, entity.client)) {
|
|
state |= EntityPlayerState.Ignored;
|
|
}
|
|
|
|
if (isHiddenBy(client, entity.client)) {
|
|
state |= EntityPlayerState.Hidden;
|
|
}
|
|
|
|
if (isOnlineFriend(client, entity.client)) {
|
|
state |= EntityPlayerState.Friend;
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
export async function reloadFriends(client: IClient) {
|
|
const friends = await findFriendIds(client.accountId);
|
|
client.friends = new Set(friends);
|
|
client.friendsCRC = undefined;
|
|
client.actionParam(0, Action.FriendsCRC, undefined);
|
|
}
|
|
|
|
export function execAction(client: IClient, action: Action, settings: GameServerSettings) {
|
|
switch (action) {
|
|
case Action.Boop:
|
|
boop(client, Date.now());
|
|
break;
|
|
case Action.TurnHead:
|
|
turnHead(client);
|
|
break;
|
|
case Action.Sneeze:
|
|
sneeze(client);
|
|
break;
|
|
case Action.Stand:
|
|
stand(client);
|
|
break;
|
|
case Action.Sit:
|
|
sit(client, settings);
|
|
break;
|
|
case Action.Lie:
|
|
lie(client);
|
|
break;
|
|
case Action.Fly:
|
|
fly(client);
|
|
break;
|
|
case Action.Drop:
|
|
unholdItem(client.pony);
|
|
break;
|
|
case Action.Sleep:
|
|
playerSleep(client.pony);
|
|
break;
|
|
case Action.Blush:
|
|
playerBlush(client.pony);
|
|
break;
|
|
case Action.Cry:
|
|
playerCry(client.pony);
|
|
break;
|
|
case Action.Love:
|
|
playerLove(client.pony);
|
|
break;
|
|
case Action.DropToy:
|
|
unholdToy(client.pony);
|
|
updateEntityOptions(client.pony, { extra: false });
|
|
break;
|
|
case Action.Magic:
|
|
if (client.pony.canMagic) {
|
|
const has = hasFlag(client.pony.state, EntityState.Magic);
|
|
updateEntityState(client.pony, setFlag(client.pony.state, EntityState.Magic, !has));
|
|
}
|
|
break;
|
|
case Action.Kiss:
|
|
kiss(client, Date.now());
|
|
break;
|
|
case Action.SwitchTool:
|
|
switchTool(client, false);
|
|
break;
|
|
case Action.SwitchToolRev:
|
|
switchTool(client, true);
|
|
break;
|
|
case Action.SwitchToPlaceTool:
|
|
holdItem(client.pony, entities.hammer.type);
|
|
break;
|
|
case Action.SwitchToTileTool:
|
|
holdItem(client.pony, entities.shovel.type);
|
|
break;
|
|
default:
|
|
if (isExpressionAction(action)) {
|
|
expressionAction(client, action);
|
|
} else {
|
|
throw new Error(`Invalid action (${action})`);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
export function switchTool(client: IClient, reverse: boolean) {
|
|
const hold = client.pony.options!.hold || 0;
|
|
const index = tools.findIndex(t => t.type === hold);
|
|
const unholdIndex = reverse ? 0 : tools.length - 1;
|
|
|
|
if (index === unholdIndex) {
|
|
unholdItem(client.pony);
|
|
} else {
|
|
const newIndex = reverse ? (index === -1 ? tools.length - 1 : index - 1) : ((index + 1) % tools.length);
|
|
const tool = tools[newIndex];
|
|
holdItem(client.pony, tool.type);
|
|
const text = (client.isMobile && tool.textMobile) ? tool.textMobile : tool.text;
|
|
saySystem(client, text);
|
|
}
|
|
}
|
|
|
|
export function teleportTo(client: IClient, x: number, y: number) {
|
|
fixPosition(client.pony, client.map, x, y, true);
|
|
client.safeX = client.pony.x;
|
|
client.safeY = client.pony.y;
|
|
client.lastTime = 0;
|
|
}
|