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
+80
View File
@@ -0,0 +1,80 @@
import {
BASE_CHARACTER_LIMIT, ADDITIONAL_CHARACTERS_SUPPORTER1, ADDITIONAL_CHARACTERS_SUPPORTER2,
ADDITIONAL_CHARACTERS_SUPPORTER3, ADDITIONAL_CHARACTERS_PAST_SUPPORTER
} from './constants';
import { AccountDataFlags } from './interfaces';
import { hasFlag } from './utils';
export interface AccountRoles {
roles?: string[] | undefined;
}
export interface AccountSupporter extends AccountRoles {
supporter?: number | undefined;
supporterInvited?: boolean;
flags?: AccountDataFlags;
}
export function hasRole(account: AccountRoles | undefined, role: string): boolean {
return !!(account && account.roles && account.roles.indexOf(role) !== -1);
}
export function isAdmin(account: AccountRoles): boolean {
return hasRole(account, 'admin') || hasRole(account, 'superadmin');
}
export function isMod(account: AccountRoles): boolean {
return hasRole(account, 'mod') || isAdmin(account);
}
export function isDev(account: AccountRoles): boolean {
return hasRole(account, 'dev');
}
export function meetsRequirement(account: AccountSupporter, require: string | undefined): boolean {
return !require || hasRole(account, require) || meetsSupporterRequirement(account, require);
}
function meetsSupporterRequirement(account: AccountSupporter, require: string): boolean {
const level = account.supporter || 0;
const modOrDev = isMod(account) || isDev(account);
if (require === 'inv') {
return modOrDev || level >= 1 || !!account.supporterInvited;
} else if (require === 'sup1') {
return modOrDev || level >= 1;
} else if (require === 'sup2') {
return modOrDev || level >= 2;
} else if (require === 'sup3') {
return modOrDev || level >= 3;
} else {
return false;
}
}
export function getCharacterLimit(account: AccountSupporter) {
switch (account.supporter || 0) {
case 1: return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_SUPPORTER1;
case 2: return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_SUPPORTER2;
case 3: return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_SUPPORTER3;
default:
if (hasFlag(account.flags, AccountDataFlags.PastSupporter)) {
return BASE_CHARACTER_LIMIT + ADDITIONAL_CHARACTERS_PAST_SUPPORTER;
} else {
return BASE_CHARACTER_LIMIT;
}
}
}
export function getSupporterInviteLimit(account: AccountSupporter) {
if (isMod(account) || isDev(account)) {
return 100;
} else {
switch (account.supporter) {
case 1: return 1;
case 2: return 5;
case 3: return 10;
default: return 0;
}
}
}
+923
View File
@@ -0,0 +1,923 @@
import {
PonyInfo, AccountSettings, AccountData, PonyObject, AccountCounters, ServerFeatureFlags, Dict, Subscription
} from './interfaces';
export const ITEM_LIMIT = 1000;
export const ROLES = ['superadmin', 'admin', 'mod', 'dev'];
export const SERVER_LABELS: { [key: string]: string; } = {
'dev': 'badge-test',
'test': 'badge-test',
'main': 'badge-none',
'main-ru': 'badge-none',
'safe': 'badge-success',
'safe-ru': 'badge-success',
'safe-pr': 'badge-success',
'safe-sp': 'badge-success',
};
export const enum Suspicious {
No,
Yes,
Very,
}
export const enum CharacterFlags {
None = 0,
BadCM = 1,
HideSupport = 4,
RespawnAtSpawn = 8,
ForbiddenName = 16,
}
// NOTE: also update createLoginServerStatus() (internal-login.ts)
export interface GeneralSettings {
isPageOffline?: boolean;
canCreateAccounts?: boolean;
blockWebView?: boolean;
reportPotentialDuplicates?: boolean;
autoMergeDuplicates?: boolean;
suspiciousNames?: string;
suspiciousPonies?: string;
suspiciousMessages?: string;
suspiciousSafeMessages?: string;
suspiciousSafeWholeMessages?: string;
suspiciousSafeInstantMessages?: string;
suspiciousSafeInstantWholeMessages?: string;
suspiciousAuths?: string;
patreonToken?: string;
}
// NOTE: also update createLoginServerStatus()
export const LOGIN_SERVER_SETTINGS: { id: keyof GeneralSettings; label: string; }[] = [
{ id: 'canCreateAccounts', label: 'Can create accounts' },
{ id: 'blockWebView', label: 'Block web view' },
{ id: 'reportPotentialDuplicates', label: 'Report potential duplicates' },
{ id: 'autoMergeDuplicates', label: 'Auto-merge duplicates' },
];
export interface ServerLiveSettings {
updating: boolean;
shutdown: boolean;
}
export interface GameServerSettings {
isServerOffline?: boolean;
filterSwears?: boolean;
autoBanSwearing?: boolean;
autoBanSpamming?: boolean;
doubleTimeouts?: boolean;
reportSpam?: boolean;
reportSwears?: boolean;
reportTeleporting?: boolean;
logLagging?: boolean;
logTeleporting?: boolean;
logFixingPosition?: boolean;
hideSwearing?: boolean;
kickSwearing?: boolean;
kickSwearingToSpawn?: boolean;
blockJoining?: boolean;
kickTeleporting?: boolean;
fixTeleporting?: boolean;
kickLagging?: boolean;
reportSitting?: boolean;
}
export interface Settings extends GeneralSettings {
servers: Dict<GameServerSettings>;
}
export const SERVER_SETTINGS: { id: keyof GameServerSettings; label: string; }[] = [
{ id: 'filterSwears', label: 'Swear filter' },
{ id: 'autoBanSwearing', label: 'Auto-Timeout for swearing' },
{ id: 'autoBanSpamming', label: 'Auto-Timeout for spam' },
{ id: 'doubleTimeouts', label: 'Double timeouts duration' },
{ id: 'reportSpam', label: 'Report spam' },
{ id: 'reportSwears', label: 'Report swearing' },
{ id: 'reportTeleporting', label: 'Report teleporting' },
{ id: 'logLagging', label: 'Log lagging' },
{ id: 'logTeleporting', label: 'Log teleporting' },
{ id: 'logFixingPosition', label: 'Log fixing position' },
{ id: 'hideSwearing', label: 'Hide swearing' },
{ id: 'kickSwearing', label: 'Kick for swearing' },
{ id: 'kickSwearingToSpawn', label: 'Reset swearing to spawn' },
{ id: 'blockJoining', label: 'Block joining' },
{ id: 'kickTeleporting', label: 'Kick teleporting players' },
{ id: 'fixTeleporting', label: 'Fix teleporting players' },
{ id: 'kickLagging', label: 'Kick lagging players' },
{ id: 'reportSitting', label: 'Report sitting' },
];
export interface InternalCommonApi {
reloadSettings(): Promise<void>;
}
export interface InternalApi extends InternalCommonApi {
state(): Promise<GameServerState>;
stats(): Promise<ServerStats>;
statsTable(stats: Stats): Promise<StatsTable>;
action(action: string, accountId: string): Promise<void>;
join(accountId: string, ponyId: string): Promise<string>;
kick(accountId: string | undefined, characterId: string | undefined): Promise<boolean>;
kickAll(): Promise<void>;
accountChanged(accountId: string): Promise<void>;
accountMerged(accountId: string, mergeId: string): Promise<void>;
accountStatus(accountId: string): Promise<AccountStatus>;
accountAround(accountId: string): Promise<AroundEntry[]>;
accountHidden(accountId: string): Promise<HidingStats>;
notifyUpdate(): Promise<void>;
cancelUpdate(): Promise<void>;
shutdownServer(value: boolean): Promise<void>;
getTimings(): Promise<any[]>;
teleportTo(adminAccountId: string, targetAccountId: string): Promise<void>;
}
export interface InternalLoginApi extends InternalCommonApi {
state(): Promise<LoginServerStatus>;
loginServerStats(): Promise<RequestStats[]>;
updateLiveSettings(update: Partial<ServerLiveSettings>): Promise<void>;
mergeAccounts(id: string, withId: string, reason: string, allowAdmin: boolean, creatingDuplicates: boolean): Promise<void>;
}
export interface ServerConfig {
id: string;
port: number;
path: string;
local: string;
name: string;
desc: string;
flag: string;
host?: string;
alert?: string;
require?: string;
flags: ServerFeatureFlags;
hidden?: boolean;
}
export interface GameServerState {
id: string;
path: string;
name: string;
desc: string;
flag: string;
host?: string;
alert?: string;
require?: string;
world?: {
mapSize: number;
regionSize: number;
};
flags: ServerFeatureFlags;
dead: boolean;
maps: number;
online: number;
onMain: number;
queued: number;
shutdown: boolean;
settings: GameServerSettings;
}
export interface InternalServerState {
id: string;
api: InternalCommonApi;
}
export interface InternalLoginServerState extends InternalServerState {
api: InternalLoginApi;
state: LoginServerStatus;
}
export interface InternalGameServerState extends InternalServerState {
api: InternalApi;
state: GameServerState;
}
export interface ServerStatus {
diskSpace: string;
memoryUsage: string;
certificateExpiration: string;
lastPatreonUpdate: string;
}
export interface LoginServerStatus extends GeneralSettings {
updating: boolean;
dead: boolean;
}
export interface AdminState {
status: ServerStatus;
loginServers: LoginServerStatus[];
gameServers: GameServerState[];
}
export interface MemoryStatus {
total: number;
used: number;
free: number;
}
export interface OriginInfoBase {
ip: string;
country: string;
last?: Date;
}
export interface MergeItemData {
id: string;
name: string;
}
export interface MergeHideData {
id: string;
name: string;
date: string;
}
export interface MergeAccountData {
name: string;
note: string;
flags: AccountFlags;
emails: string[];
ignores: string[];
counters: AccountCounters;
auths: MergeItemData[];
characters: MergeItemData[];
state: AccountState;
birthdate?: Date;
settings?: AccountSettings;
friends?: string[];
hides?: MergeHideData[];
}
export interface MergeData {
account: MergeAccountData;
merge: MergeAccountData;
}
export interface MergeInfo {
_id?: string;
id: string;
name: string;
//code: number;
date: Date;
reason?: string;
data?: MergeData;
split?: boolean;
}
export interface LogEntry {
message: string;
date: Date;
}
export interface AccountDetails {
merges: MergeInfo[];
supporterLog: LogEntry[];
banLog: LogEntry[];
invitesReceived: SupporterInvite[];
invitesSent: SupporterInvite[];
state: AccountState;
}
export interface AuthDetails {
id: string;
lastUsed: string | undefined;
}
export interface BannedMuted {
mute?: number;
shadow?: number;
ban?: number;
}
export interface Document extends Timestamps {
_id: string;
deleted?: boolean;
}
// bases
export interface TimestampsBase {
createdAt?: Date;
updatedAt: Date;
}
export interface ChatMessageBase {
createdAt: Date;
message: string;
}
export const accountCounters = [
{ name: 'spam', label: 'spam' },
{ name: 'swears', label: 'swearing' },
{ name: 'timeouts', label: 'timeouts' },
{ name: 'inviteLimit', label: 'party limits' },
{ name: 'friendLimit', label: 'friend limits' },
];
export const enum AccountFlags {
None = 0,
BlockPartyInvites = 1,
CreatingDuplicates = 2,
DuplicatesNotification = 4,
BlockMerging = 16,
BlockFriendRequests = 256,
}
export const accountFlags = [
{ value: AccountFlags.BlockPartyInvites, name: 'BlockPartyInvites', label: 'block party invites' },
{ value: AccountFlags.CreatingDuplicates, name: 'CreatingDuplicates', label: 'creating duplicates' },
{ value: AccountFlags.DuplicatesNotification, name: 'DuplicatesNotification', label: 'duplicates notification' },
{ value: AccountFlags.BlockMerging, name: 'BlockMerging', label: 'block merging' },
{ value: AccountFlags.BlockFriendRequests, name: 'BlockFriendRequests', label: 'block friend requests' },
];
export const enum PatreonFlags {
None = 0,
Supporter1 = 1,
Supporter2 = 2,
Supporter3 = 3,
}
export const enum SupporterFlags {
None = 0,
Supporter1 = 1,
Supporter2 = 2,
Supporter3 = 3,
SupporterMask = 0x0003,
IgnorePatreon = 0x0080,
PastSupporter = 0x0100,
ForcePastSupporter = 0x0200,
IgnorePastSupporter = 0x0400,
}
export const supporterFlags = [
{ value: SupporterFlags.IgnorePatreon, label: 'ignore data from patreon' },
];
// NOTE: also update mergeStates (merge.ts)
export interface AccountState {
gifts?: number;
candies?: number;
clovers?: number;
toys?: number;
eggs?: number;
}
export interface AccountAlert {
expires: Date;
message: string;
}
export interface AccountBase<ID> extends TimestampsBase, BannedMuted {
name: string;
birthdate?: Date;
birthyear?: number;
emails?: string[];
roles: string[];
origins: OriginInfoBase[];
settings?: AccountSettings;
note: string;
noteUpdated?: Date;
lastVisit: Date;
lastUserAgent?: string;
lastBrowserId?: string;
lastOnline?: Date;
lastCharacter?: ID;
ignores?: string[];
flags: AccountFlags;
counters?: AccountCounters;
characterCount: number;
patreon?: PatreonFlags;
supporter?: SupporterFlags;
supporterLog?: LogEntry[];
supporterTotal?: number;
supporterDeclinedSince?: Date;
merges?: MergeInfo[];
banLog?: LogEntry[];
state?: AccountState;
alert?: AccountAlert;
savedMap?: string;
}
export interface AuthBase<ID> extends TimestampsBase {
account?: ID;
openId?: string;
provider: string;
name: string;
url: string;
emails?: string[];
disabled?: boolean;
banned?: boolean;
pledged?: number;
lastUsed?: Date;
}
export interface OriginBase extends TimestampsBase, OriginInfoBase, BannedMuted {
}
export const enum CharacterStateFlags {
None = 0,
Right = 1,
Extra = 2,
}
export interface CharacterState {
x: number;
y: number;
map?: string;
toy?: number;
flags?: CharacterStateFlags;
hold?: string;
}
export interface CharacterBase<ID> extends TimestampsBase {
account: ID;
site?: ID;
tag?: string;
name: string;
desc?: string;
info?: string;
flags: CharacterFlags;
lastUsed?: Date;
creator?: string;
state?: { [key: string]: CharacterState | undefined; };
}
export interface EventBase<ID> extends TimestampsBase {
account?: ID;
pony?: ID;
type: string;
server: string;
message: string;
desc: string;
origin?: OriginInfoBase;
count: number;
}
export interface SupporterInviteBase<ID> extends TimestampsBase {
source: ID;
target: ID;
name: string;
info: string;
active: boolean;
}
export interface FriendRequestBase<ID> {
source: ID;
target: ID;
}
export interface HideRequestBase<ID> {
source: ID;
target: ID;
name: string;
date: Date;
}
export const eventFields: (keyof Event)[] = [
'_id', 'updatedAt', 'createdAt', 'type', 'server', 'message', 'desc', 'count', 'origin', 'account', 'pony'
];
// models
export interface OriginInfo extends OriginInfoBase {
}
export interface Timestamps extends TimestampsBase {
}
export interface AccountStatus {
online: boolean;
server?: string;
map?: string;
incognito?: boolean;
character?: string;
x?: number;
y?: number;
userAgent?: string;
duration?: string;
}
export interface DuplicatesInfo {
count: number;
name: boolean;
emails: boolean;
browserId: boolean;
generatedAt: number;
perma: boolean;
}
export type ListListener<T> = (items: T[]) => void;
export interface IObservableList<T, V> {
hasSubscribers(): boolean;
trigger(): void;
push(item: T): void;
pushOrdered(item: T, compare: (a: T, b: T) => number): void;
remove(item: T): boolean;
replace(list: T[]): void;
subscribe(listener: ListListener<V>): Subscription;
}
export interface PonyIdDateName {
id: string;
date: number;
name: string;
}
export interface Account extends AccountBase<string>, Document {
nameLower?: string;
auths?: Auth[];
originsRefs?: OriginRef[];
ignoredByLimit?: number;
ignoresLimit?: number;
ignoresCount?: number;
duplicatesLimit?: number;
totalPledged?: number;
ponies?: Character[];
invitesReceived?: SupporterInvite[];
invitesSent?: SupporterInvite[];
authsList?: IObservableList<Auth, string>;
poniesList?: IObservableList<Character, PonyIdDateName>;
originsList?: IObservableList<OriginRef, OriginInfoBase>;
}
export interface Auth extends AuthBase<string>, Document {
}
export interface Character extends CharacterBase<string>, Document {
ponyInfo?: PonyInfo;
deleted?: boolean;
}
export interface Origin extends OriginBase, Document {
accounts?: Account[];
accountsCount?: number;
}
export interface OriginRef {
origin: Origin;
last: Date;
}
export interface Event extends EventBase<string>, Document {
deleted?: boolean;
descHTML?: any;
}
export interface ChatEvent {
event: Event;
account: Account | undefined;
}
export interface SupporterInvite extends SupporterInviteBase<string>, Document {
}
export interface FriendRequest extends FriendRequestBase<string>, Document {
}
// other
export interface UpdateOrigin extends OriginInfo, BannedMuted {
}
export interface AccountUpdate extends BannedMuted {
age?: number;
name?: string;
note?: string;
flags?: number;
supporter?: number;
}
export interface BaseValues {
updatedAt?: string;
createdAt?: string;
lastVisit?: string;
}
export interface LiveResponse {
updates: any[][];
deletes: string[];
base: BaseValues;
more: boolean;
}
export interface RequestStats {
path: string;
count: number;
average: string;
total: string;
order: string;
totalCount: number;
}
export interface UserCountStats {
count: number;
date: string;
}
export interface LoginStats {
requests: RequestStats[];
userCounts: UserCountStats[];
}
export interface ItemCounts {
accounts: number;
characters: number;
auths: number;
origins: number;
}
export interface FindPonyQuery {
search?: string;
orderBy?: string;
}
export interface AuthUpdate {
disabled?: boolean;
banned?: boolean;
pledged?: number;
}
export interface AccountPonies {
account: string;
count: number;
ponies: any[][];
}
export interface AccountPoniesResponse {
base: BaseValues;
accounts: AccountPonies[];
}
export interface PoniesResponse {
base: BaseValues;
ponies: any[][];
}
export interface PonyCreator {
_id: string;
name: string;
creator: string;
}
export interface AccountOrigins {
accountId: string;
ips: string[];
}
export interface ServerStats {
actions: {
id: number;
name: string;
type: string;
countBin: number;
countStr: number;
average: string;
total: string;
}[];
}
export interface DuplicateInfoEntry {
account: string;
userAgent: string;
ponies: string[];
}
export interface AroundEntry {
account: string;
distance: number;
party: boolean;
}
export const enum Stats {
Country,
Support,
Maps,
}
export type StatsTable = string[][];
export interface OriginStats {
uniqueOrigins: number;
duplicateOrigins: number;
singleOrigins: number;
totalOrigins: number;
totalOriginsIP4: number;
totalOriginsIP6: number;
distribution: number[];
}
export interface OtherStats {
totalIgnores: number;
authsWithEmptyAccount: number;
authsWithMissingAccount: number;
}
export interface Around {
account: Account;
distance: number;
party: boolean;
}
export interface DuplicateBase {
indenticalEmail: boolean;
emails: number;
origins: number;
ponies?: string[];
userAgent?: string;
browserId?: boolean;
birthdate: boolean;
perma: boolean;
name: number;
note: number;
lastVisit: Date;
}
export interface DuplicateResult extends DuplicateBase {
account: string;
}
export interface Duplicate extends DuplicateBase {
account: Account;
}
export interface FindAccountQuery {
search: string;
showOnly: string;
not: boolean;
page: number;
itemsPerPage: number;
force?: boolean;
}
export interface FindAccountResult {
accounts: string[];
page: number;
totalItems: number;
}
export interface AdminCacheEntry<T> {
query: string;
result: T;
timestamp: Date;
}
export interface AdminCache {
findAccounts?: AdminCacheEntry<Account[]>;
}
export interface ClearOrignsOptions {
old?: boolean;
singles?: boolean;
trim?: boolean;
veryOld?: boolean;
country?: string;
}
export interface PatreonReward {
id: string;
title: string;
description: string;
}
export interface PatreonPledge {
user: string;
reward: string;
total: number;
declinedSince?: string;
account?: string;
}
export interface PatreonData {
rewards: PatreonReward[];
pledges: PatreonPledge[];
}
export interface HidingStats {
account: string;
hidden: string[];
hiddenBy: string[];
permaHidden: string[];
permaHiddenBy: string[];
}
export const enum TimingEntryType {
Start,
End,
}
export interface TimingEntry {
type: TimingEntryType;
time: number;
name?: string;
}
export type ModelTypes =
'accounts' | 'auths' | 'origins' | 'ponies' | 'accountAuths' | 'accountOrigins' | 'accountPonies';
export interface IAdminServerActions {
// subscribing
subscribe(model: ModelTypes, id: string): void;
unsubscribe(model: ModelTypes, id: string): void;
// other
getSignedAccount(): Promise<AccountData>;
getCounts(): Promise<ItemCounts>;
getState(): Promise<AdminState>;
updateSettings(update: Partial<Settings>): Promise<void>;
updateGameServerSettings(serverId: string, update: Partial<GameServerSettings>): Promise<void>;
fetchServerStats(serverId: string): Promise<ServerStats>;
fetchServerStatsTable(serverId: string, stats: Stats): Promise<StatsTable>;
notifyUpdate(serverId: string): Promise<void>;
shutdownServers(serverId: string): Promise<void>;
resetUpdating(serverId: string): Promise<void>;
report(accountId: string): Promise<void>;
action(action: string, accountId: string): Promise<void>;
kick(accountId: string): Promise<void>;
kickAll(serverId: string): Promise<void>;
getChat(search: string, date: string, caseInsensitive: boolean): Promise<string>;
getChatForAccounts(accountIds: string[], date: string): Promise<string>;
getRequestStats(): Promise<LoginStats>;
updatePatreon(): Promise<void>;
resetSupporter(accountId: string): Promise<void>;
getLastPatreonData(): Promise<PatreonData | undefined>;
updatePastSupporters(): Promise<void>;
// live
get(endPoint: 'events', id: string): Promise<any>;
getAll(endPoint: 'events', timestamp?: string): Promise<LiveResponse>;
assignAccount(endPoint: 'events', id: string, account: string): Promise<void>;
removeItem(endPoint: 'events', id: string): Promise<void>;
// events
removeEvent(id: string): Promise<void>;
// origins
updateOrigin(origin: UpdateOrigin): Promise<void>;
getOriginStats(): Promise<OriginStats>;
getOtherStats(): Promise<OtherStats>;
clearOrigins(count: number, andHigher: boolean, options: ClearOrignsOptions): Promise<void>;
clearOriginsForAccounts(accounts: string[], options: ClearOrignsOptions): Promise<void>;
// characters
getPony(id: string): Promise<Character | undefined>;
getPonyInfo(id: string): Promise<PonyObject | null>;
getPoniesCreators(accountId: string): Promise<PonyCreator[]>;
getPoniesForAccount(accountId: string): Promise<Character[]>;
getDetailsForAccount(accountId: string): Promise<AccountDetails>;
findPonies(query: FindPonyQuery, page: number, skipTotalCount: boolean): Promise<{ items: string[]; totalCount: number; }>;
createPony(account: string, name: string, info: string): Promise<void>;
assignPony(ponyId: string, accountId: string): Promise<void>;
removePony(id: string): Promise<void>;
removePoniesAboveLimit(accountId: string): Promise<void>;
removeAllPonies(accountId: string): Promise<void>;
// auths
getAuth(id: string): Promise<Auth | undefined>;
getAuthsForAccount(accountId: string): Promise<Auth[]>;
fetchAuthDetails(auths: string[]): Promise<AuthDetails[]>;
updateAuth(id: string, update: AuthUpdate): Promise<void>;
assignAuth(authId: string, accountId: string): Promise<void>;
removeAuth(id: string): Promise<void>;
// accounts
getAccount(id: string): Promise<Account | undefined>;
findAccounts(query: FindAccountQuery): Promise<FindAccountResult>;
createAccount(name: string): Promise<string>;
getAccountsByEmails(emails: string[]): Promise<Dict<string[]>>;
getAccountsByOrigin(ip: string): Promise<string[]>;
setName(accountId: string, name: string): Promise<void>;
setAge(accountId: string, age: number): Promise<void>;
setRole(accountId: string, role: string, set: boolean): Promise<void>;
updateAccount(accountId: string, update: AccountUpdate, message?: string): Promise<void>;
timeoutAccount(accountId: string, timeout: number): Promise<void>;
updateAccountCounter(accountId: string, name: keyof AccountCounters, value: number): Promise<void>;
removeAllOrigins(accountId: string): Promise<void>;
removeOriginsForAccount(accountId: string, ips: string[]): Promise<void>;
removeOriginsForAccounts(origins: AccountOrigins[]): Promise<void>;
addOriginToAccount(accountId: string, origin: OriginInfo): Promise<void>;
mergeAccounts(accountId: string, withId: string): Promise<void>;
unmergeAccounts(accountId: string, mergeId: string | undefined, split: MergeAccountData, keep: MergeAccountData): Promise<void>;
addEmail(accountId: string, email: string): Promise<void>;
removeEmail(accountId: string, email: string): Promise<void>;
removeIgnore(accountId: string, ignore: string): Promise<void>;
addIgnores(accountId: string, ignores: string[]): Promise<void>;
removeFriend(accountId: string, friendId: string): Promise<void>;
addFriend(accountId: string, friendId: string): Promise<void>;
setAccountState(accountId: string, state: AccountState): Promise<void>;
getAccountStatus(accountId: string): Promise<AccountStatus[]>;
getAccountAround(accountId: string): Promise<AroundEntry[]>;
getAccountHidden(accountId: string): Promise<HidingStats>;
getAccountFriends(accountId: string): Promise<string[]>;
removeAccount(accountId: string): Promise<void>;
setAlert(accountId: string, message: string, expiresIn: number): Promise<void>;
getIgnoresAndIgnoredBy(accountId: string): Promise<{ ignores: string[]; ignoredBy: string[]; }>;
getAllDuplicatesQuickInfo(accountId: string): Promise<DuplicatesInfo>;
getAllDuplicates(accountId: string): Promise<DuplicateResult[]>;
getDuplicateEntries(force: boolean): Promise<string[]>;
clearSessions(accountId: string): Promise<void>;
// other
getTimings(serverId: string): Promise<any[]>;
teleportTo(accountId: string): Promise<void>;
}
+570
View File
@@ -0,0 +1,570 @@
import * as moment from 'moment';
import { escape, escapeRegExp, startsWith, range, uniq, compact } from 'lodash';
import { fromNow, toInt, hasFlag, compareDates, removeItem, includes } from './utils';
import { DAY } from './constants';
import {
Account, OriginInfo, Document, OriginRef, SupporterFlags, BannedMuted, AccountBase, LogEntry,
DuplicateResult, DuplicateBase, Duplicate, Auth, MergeInfo
} from './adminInterfaces';
import { hasRole } from './accountUtils';
import { filterBadWordsPartial } from './swears';
import { faPlusCircle, faClock, faMinusCircle, faCaretSquareUp, faCaretSquareDown } from '../client/icons';
import { element, textNode } from '../client/htmlUtils';
interface UpdatedAt {
updatedAt: Date;
}
export const compareUpdatedAt = (a: UpdatedAt, b: UpdatedAt) => compareDates(a.updatedAt, b.updatedAt);
export const compareOrigins = (a: OriginInfo, b: OriginInfo) => a.ip.localeCompare(b.ip);
export const compareOriginRefs = (a: OriginRef, b: OriginRef) =>
compareDates(b.last, a.last) || compareOrigins(a.origin, b.origin);
export const compareByName = <T extends { name: string; }>(a: T, b: T) => (a.name || '').localeCompare(b.name || '');
export const getId = (item: Document) => item._id;
export const tagBad = (s: string) => `<span class='bad'>${s}</span>`;
export function compareAccounts(a: Account, b: Account) {
return compareDates(a.createdAt, b.createdAt);
}
export function compareAuths(a: Auth, b: Auth) {
const aDeleted = a.disabled || a.banned || false;
const bDeleted = b.disabled || b.banned || false;
if (aDeleted && !bDeleted) {
return 1;
} else if (!aDeleted && bDeleted) {
return -1;
} else {
return compareByName(a, b);
}
}
export function highlightWords(text?: string) {
text = text || '';
text = filterBadWordsPartial(text, tagBad);
return text;
}
export function getAge(birthdate: Date) {
return moment().diff(birthdate, 'years');
}
// chat & events
function enc(text?: string): string {
return escape(text || '');
}
function encWithHighlight(text?: string): string {
return highlightWords(enc(text || ''));
}
export function formatEventDesc(text: string): string {
return encWithHighlight(text).replace(/\[([a-z0-f]{24})\]/g, `<a tabindex onclick="goToAccount('$1')">[$1]</a>`);
}
function getMessageTag(message: string) {
if (/^\/p /.test(message)) {
return 'party';
} else if (/^\/w /.test(message)) {
return 'whisper';
} else if (/^\/s[s123] /.test(message)) {
return 'supporter';
} else if (/^\//.test(message)) {
return 'command';
} else {
return 'none';
}
}
export function replaceSwears(element: HTMLElement) {
const text = element.textContent;
if (text) {
const replaced = encWithHighlight(text);
if (text !== replaced) {
element.innerHTML = replaced;
}
}
}
function formatChatLine(l: string): HTMLElement {
// 00:00:01 [system] Timed out for swearing
// 00:00:01 [patreon] fetched patreon data
// 00:00:01 [dev][Autumn Leafs] hello world
// 00:00:01 [dev][Autumn Leafs][muted] hello world
// 00:00:01 [dev][Autumn Leafs][ignored] hello world
// 00:00:01 [dev-pl][Autumn Leafs][ignored] hello world
// 00:00:01 [57a3dc6f2f0019a161cdebf6][dev][Autumn Leafs][ignored] hello world
// 00:00:01 [1][dev][Autumn Leafs][ignored] hello world
// 00:00:01 [1:merged][dev][Autumn Leafs][ignored] hello world
// 00:00:01 [merged][dev][Autumn Leafs][ignored] hello world
// 00:00:01 [merged][dev][main][Autumn Leafs][ignored] hello world
/* tslint:disable:max-line-length */
const regex = /^([0-9:]+) (\[(?:merged|\d+|\d+:merged|[a-z0-9]{24})\])?\[([a-z0-9_-]+)\](?:\[([a-z0-9_-]+)\])?((?:\[.*?\])?)(?:\[(muted|ignored|ignorepub)\])?\t(.*)$/;
const m = regex.exec(l);
if (m) {
const [, time, accountId, server, map, name, mutedIgnored, message] = m;
const messageTag = server === 'system' ? 'system' : getMessageTag(message);
const modTag = mutedIgnored ? ' message-muted' : '';
return element('div', 'chatlog-line', [
element('span', 'time', [], { 'data-text': time }),
accountId ? element('span', 'account-id', [textNode(accountId)]) : undefined,
element('span', `server server-${server.replace(/-.+$/g, '')}`, [textNode(`[${server}]`)]),
map ? element('span', `map map-${map}`, [textNode(`[${map}]`)]) : undefined,
element('span', mutedIgnored ? `name ${mutedIgnored}` : `name`, [textNode(name)]),
textNode(' '),
element('span', `message message-${messageTag}${modTag}`, [textNode(message)]),
textNode(' '),
element('a', 'chat-translate', [], undefined, { click: translateChat }),
]);
} else {
return element('div', '', [textNode(highlightWords(l))]);
}
}
function translateChat(this: HTMLElement) {
const lines: string[] = [];
let parent = this.parentElement;
for (let i = 0; parent && i < 10; i++) {
lines.push(parent.querySelector('.message')!.textContent!);
parent = parent.nextElementSibling as HTMLElement;
}
window.open(`https://translate.google.com/#auto/en/${encodeURIComponent(lines.join('\n'))}`);
}
if (typeof window !== 'undefined') {
(window as any).goToAccount = (accountId: string) => {
window.dispatchEvent(new CustomEvent('go-to-account', { detail: accountId }));
};
}
export function formatChat(chat: string): HTMLElement[] {
return (chat || '<no messages>')
.trim()
.split(/\r?\n/g)
.reverse()
.map(formatChatLine);
}
export interface ChatDate {
value: string;
label: string;
}
export function createChatDate(date: moment.Moment): ChatDate {
return {
value: date.toISOString(),
label: date.format('MMMM Do YYYY'),
};
}
export function createDateRange(startDate: string | Date, days: number): ChatDate[] {
return range(days, 0)
.map(d => moment(startDate).subtract(d, 'days'))
.map(createChatDate);
}
// filtering
export function filterAccounts(items: Account[], search: string, showOnly: string, not: boolean) {
if (search) {
items = items.filter(createFilter(search));
}
const filter = createFilter2(showOnly);
if (filter) {
if (not) {
items = items.filter(i => !filter(i));
} else {
items = items.filter(filter);
}
}
return items;
}
export function createFilter(search: string): (account: Account) => boolean {
const regex = new RegExp(escapeRegExp(search), 'i');
function test(value: string): boolean {
return !!value && regex.test(value);
}
function testAuth(auth: Auth) {
return test(auth.name) || auth.provider === search || auth.url === search;
}
function testMerge(merge: MergeInfo) {
return merge.id === search;
}
function filter(account: Account): boolean {
if (account._id === search)
return true;
if (test(account.name))
return true;
if (test(account.note))
return true;
if (account.roles && account.roles.some(test))
return true;
if (account.emails && account.emails.some(test))
return true;
if (account.auths && account.auths.some(testAuth))
return true;
if (account.merges && account.merges.some(testMerge))
return true;
return false;
}
function prefixWith(prefix: string, action: (phrase: string) => (account: Account) => boolean) {
return startsWith(search, prefix) ? action(search.substr(prefix.length)) : undefined;
}
function prefixWithRegex(prefix: string, action: (regex: RegExp) => (account: Account) => boolean) {
return prefixWith(prefix, phrase => action(new RegExp(escapeRegExp(phrase), 'i')));
}
function prefixWithNumber(prefix: string, action: (count: number) => (account: Account) => boolean) {
return prefixWith(prefix, phrase => action(+phrase));
}
const exactMatch = (phrase: string) => (account: Account) => account.nameLower === phrase;
const isOld = (max: number) => (account: Account) => !account.lastVisit || account.lastVisit.getTime() < max;
return prefixWithRegex('name:', regex => account => regex.test(account.name))
|| prefixWithRegex('note:', regex => account => regex.test(account.note))
|| prefixWithRegex('email:', regex => account => !!account.emails && account.emails.some(e => regex.test(e)))
|| prefixWith('role:', role => account => hasRole(account, role))
|| prefixWith('exact:', phrase => exactMatch(phrase.toLowerCase()))
|| prefixWith('disabled!', () => account => !!account.auths && account.auths.some(a => !!a.disabled))
|| prefixWith('locked!', () => account => !!account.auths && account.auths.some(a => !!a.banned))
|| prefixWithNumber('ignores:', count => account => (account.ignoresCount || 0) >= count)
|| prefixWithNumber('ponies:', count => account => account.characterCount >= count)
|| prefixWithNumber('auths:', count => account => !!account.auths && account.auths.length >= count)
|| prefixWithNumber('old:', days => isOld(fromNow(-days * DAY).getTime()))
|| prefixWithNumber('spam:', count => account => !!account.counters && account.counters.spam! >= count)
|| prefixWithNumber('swearing:', count => account => !!account.counters && account.counters.swears! >= count)
|| prefixWithNumber('timeouts:', count => account => !!account.counters && account.counters.timeouts! >= count)
|| prefixWithNumber('limits:', count => account => !!account.counters && account.counters.inviteLimit! >= count)
|| filter;
}
function hasAnyBan(account: Account) {
return isBanned(account) || isMuted(account) || isShadowed(account);
}
export function createPotentialDuplicatesFilter(getAccountsByBrowserId: (id: string) => Account[] | undefined): (account: Account) => boolean {
return i => {
const name = i.nameLower;
if (name === 'anonymous' || !i.lastBrowserId)
return false;
const accounts = getAccountsByBrowserId(i.lastBrowserId);
if (accounts !== undefined && accounts.length > 1) {
for (const a of accounts) {
if (a !== i && a.nameLower === name) {
return true;
}
}
}
return false;
};
}
export function createFilter2(showOnly: string): ((account: Account) => boolean) | undefined {
const now = Date.now();
if (showOnly === 'banned') {
return hasAnyBan;
} else if (showOnly === 'timed out') {
return i => !!((i.mute && i.mute > now) || (i.shadow && i.shadow > now) || (i.ban && i.ban > now));
} else if (showOnly === 'with flags') {
return i => !!i.flags;
} else if (showOnly === 'notes') {
return i => !!i.note;
} else if (showOnly === 'supporters') {
return i => !!(i.patreon || i.supporter || i.supporterDeclinedSince);
} else {
return undefined;
}
}
export function getPotentialDuplicates(account: Account, getAccountsByBrowserId: (id: string) => Account[] | undefined) {
const accounts = account.lastBrowserId ? getAccountsByBrowserId(account.lastBrowserId) : undefined;
const name = account.nameLower;
if (accounts !== undefined && accounts.length > 1 && name !== 'anonymous') {
return accounts.filter(a => a !== account && a.nameLower === name);
} else {
return [];
}
}
// duplicates
export function compareDuplicates(a: DuplicateBase, b: DuplicateBase): number {
if (a.note !== b.note)
return b.note - a.note;
if (a.emails !== b.emails)
return b.emails - a.emails;
if (a.name !== b.name)
return b.name - a.name;
if (a.browserId !== b.browserId)
return a.browserId ? -1 : 1;
if (a.origins !== b.origins)
return b.origins - a.origins;
if (a.ponies !== b.ponies)
return (b.ponies ? b.ponies.length : 0) - (a.ponies ? a.ponies.length : 0);
return b.lastVisit.getTime() - a.lastVisit.getTime();
}
export function emailName(email: string): string {
return email.substr(0, email.indexOf('@')).toLowerCase();
}
export function createEmailMatcher(emails: string[]): ((email: string) => boolean) | undefined {
if (!emails || !emails.length) {
return undefined;
} else {
const match = emails.map(emailName).map(escapeRegExp).join('|');
const regex = new RegExp(`^(?:${match})@`, 'i');
return email => regex.test(email);
}
}
export function createDuplicate(account: Account, base: Account): Duplicate {
const indenticalEmail = account.emails && base.emails && account.emails.some(e => base.emails!.indexOf(e) !== -1);
const isMatch = createEmailMatcher(base.emails || []);
const duplicateEmails = isMatch && account.emails
&& account.emails.reduce((sum, e) => sum + (isMatch(e) ? 1 : 0), 0);
const duplicateOrigins = base.originsRefs && account.originsRefs
&& account.originsRefs.reduce((sum, o) => sum + (base.originsRefs!.some(r => o.origin.ip === r.origin.ip) ? 1 : 0), 0);
const name = account.nameLower !== 'anonymous' && account.nameLower === base.nameLower;
const note = (account.note && account.note.indexOf(base._id) !== -1)
|| (base.note && base.note.indexOf(account._id) !== -1);
const browserId = !!account.lastBrowserId && account.lastBrowserId === base.lastBrowserId;
const birthdate = !!(base.birthdate && account.birthdate && base.birthdate.getTime() === account.birthdate.getTime());
return {
account,
name: name ? 1 : 0,
note: note ? 1 : 0,
indenticalEmail: !!indenticalEmail,
emails: toInt(duplicateEmails),
origins: toInt(duplicateOrigins),
lastVisit: account.lastVisit || new Date(0),
browserId,
birthdate,
perma: isPermaBanned(account) || isPermaShadowed(account),
};
}
export function createDuplicateResult(account: Account, base: Account): DuplicateResult {
return { ...createDuplicate(account, base), account: account._id };
}
export function pushOrdered<T>(items: T[], item: T, compare: (a: T, b: T) => number) {
for (let i = 0; i < items.length; i++) {
if (compare(items[i], item) >= 0) {
items.splice(i, 0, item);
return;
}
}
items.push(item);
}
export function duplicatesCollector(duplicates: string[]) {
const set = new Set();
return (item: string) => {
if (set.has(item)) {
duplicates.push(item);
} else {
set.add(item);
}
};
}
export function patreonSupporterLevel(account: AccountBase<any>) {
return account.patreon! & 0xf;
}
export function supporterLevel(account: AccountBase<any>) {
const flags = account.supporter!;
const ignore = hasFlag(flags, SupporterFlags.IgnorePatreon);
const patreonSupporter = patreonSupporterLevel(account);
const flagsSupporter = flags & 0xf;
return Math.max(ignore ? 0 : patreonSupporter, flagsSupporter);
}
export function isPastSupporter(account: AccountBase<any>) {
const flags = account.supporter!;
return (hasFlag(flags, SupporterFlags.PastSupporter) || hasFlag(flags, SupporterFlags.ForcePastSupporter)) &&
!hasFlag(flags, SupporterFlags.IgnorePastSupporter);
}
const fieldToAction: { [key: string]: string | undefined; } = {
mute: 'Muted',
shadow: 'Shadowed',
ban: 'Banned',
};
export function banMessage(field: string, value: number) {
const action = fieldToAction[field] || 'Did';
if (value === 0) {
return `Un${action.toLowerCase()}`;
} else if (value === -1) {
return action;
} else {
return `${action} for (${moment.duration(value - Date.now()).humanize()})`;
}
}
export function isActive(value: number | undefined): boolean {
return !!value && (value === -1 || value > Date.now());
}
export function isPerma(value: number | undefined): boolean {
return value === -1;
}
export function isTemporarilyActive(value: number | undefined): boolean {
return !!value && value > Date.now();
}
export function isMuted(account: BannedMuted): boolean {
return isActive(account.mute);
}
export function isShadowed(account: BannedMuted): boolean {
return isActive(account.shadow);
}
export function isBanned(account: BannedMuted): boolean {
return isActive(account.ban);
}
export function isPermaShadowed(account: BannedMuted): boolean {
return isPerma(account.shadow);
}
export function isPermaBanned(account: BannedMuted): boolean {
return isPerma(account.ban);
}
export function isTemporarilyBanned(account: BannedMuted): boolean {
return isTemporarilyActive(account.ban);
}
export interface SupporterChange {
message: string;
date: Date;
icon: any;
class: string;
}
export function createSupporterChanges(entries: LogEntry[]): SupporterChange[] {
const changes = entries.map(l => ({
message: l.message,
level: +((/\d+/.exec(l.message) || ['0'])[0]),
added: /added/i.test(l.message),
date: new Date(l.date),
icon: /added/i.test(l.message) ? faPlusCircle : (/decline/i.test(l.message) ? faClock : faMinusCircle),
class: /added/i.test(l.message) ? 'text-success' : (/decline/i.test(l.message) ? 'text-warning' : 'text-danger'),
}));
for (let i = 1; i < changes.length; i++) {
const prev = changes[i - 1];
const current = changes[i];
if (current.date.getMonth() !== prev.date.getMonth()) {
current.class += ' border-left border-success pl-2';
}
if (current.added && prev.added) {
if (current.level > prev.level) {
current.icon = faCaretSquareUp;
current.class = 'text-info';
} else if (current.level < prev.level) {
current.icon = faCaretSquareDown;
current.class = 'text-info';
}
}
}
return changes;
}
export function getIdsFromNote(note: string | undefined) {
return note ? uniq(note.match(/[0-9a-f]{24}/g)) : [];
}
export function addToMap<T>(map: Map<string, T[]>, key: string, item: T) {
const items = map.get(key);
if (items) {
items.push(item);
} else {
map.set(key, [item]);
}
}
export function removeFromMap<T>(map: Map<string, T[]>, key: string, item: T) {
const items = map.get(key);
if (items) {
removeItem(items, item);
if (items.length === 0) {
map.delete(key);
}
}
}
export function parsePonies(ponies: string, filterIds?: string[]) {
return compact(ponies
.split(/\n\r?/g)
.map(x => /\[system\] removed pony \[([a-f0-9]{24})\] "(.+)" (\S+)/.exec(x)))
.map(([_, id, name, info]) => ({ id, name, info }))
.filter(({ id }) => !filterIds || includes(filterIds, id));
}
export function createIdStore() {
const idsMap = new Map<string, string>();
return (id: string) => {
const result = idsMap.get(id);
if (result) {
return result;
} else {
idsMap.set(id, id);
return id;
}
};
}
export function getTranslationUrl(text: string) {
return `https://translate.google.com/#view=home&op=translate&sl=auto&tl=en&text=${encodeURIComponent(text)}`;
// return `https://translate.google.com/#auto/en/${encodeURIComponent(text)}`;
}
+135
View File
@@ -0,0 +1,135 @@
import { sample } from 'lodash';
import { Sprite, Palette, PaletteSpriteBatch } from './interfaces';
import { drawSpriteCropped } from '../graphics/graphicsUtils';
import { includes } from './utils';
import { WHITE } from './colors';
const enum AnimationPhase {
Starting,
Playing,
Ending,
}
export interface SpriteAnimation {
loop: boolean;
start: number;
middle: number;
end: number;
fps: number;
palette: Uint32Array;
frames: Sprite[];
flipFrames?: Sprite[];
}
export interface AnimationPlayer {
nextAnimation: SpriteAnimation | undefined;
currentAnimation: SpriteAnimation | undefined;
time: number;
frame: number;
phase: AnimationPhase;
dirty: boolean;
palette: Palette;
}
export function createAnimationPlayer(palette: Palette): AnimationPlayer {
return {
nextAnimation: undefined,
currentAnimation: undefined,
time: 0,
frame: 0,
phase: AnimationPhase.Starting,
dirty: true,
palette,
};
}
export function isAnimationPlaying(player: AnimationPlayer) {
return player.currentAnimation !== undefined;
}
export function playOneOfAnimations(player: AnimationPlayer, animations: SpriteAnimation[]) {
if (player.phase === AnimationPhase.Ending || !includes(animations, player.currentAnimation)) {
playAnimation(player, sample(animations));
}
}
export function playAnimation(player: AnimationPlayer, animation: SpriteAnimation | undefined) {
if (player.currentAnimation !== animation) {
if (player.currentAnimation) {
if (player.nextAnimation !== animation || player.phase !== AnimationPhase.Ending) {
player.nextAnimation = animation;
player.time = (player.frame + 1) / player.currentAnimation.fps;
player.phase = AnimationPhase.Ending;
}
} else {
player.currentAnimation = animation;
player.time = 0;
player.phase = AnimationPhase.Starting;
}
player.dirty = true;
} else if (player.phase === AnimationPhase.Ending) {
player.nextAnimation = animation;
player.dirty = true;
}
}
export function updateAnimation(player: AnimationPlayer, delta: number) {
if (player.currentAnimation !== undefined) {
player.time += delta;
const { start, middle, end, fps, loop } = player.currentAnimation;
let extraFrame = Math.floor(player.time * fps);
if (player.phase === AnimationPhase.Starting && extraFrame > start) {
player.phase = loop ? AnimationPhase.Playing : AnimationPhase.Ending;
player.dirty = true;
}
if (player.phase === AnimationPhase.Playing) {
extraFrame = start + ((extraFrame - start) % middle);
}
if (player.phase === AnimationPhase.Ending && extraFrame > (start + middle + end)) {
player.currentAnimation = undefined;
player.dirty = true;
if (player.nextAnimation !== undefined) {
const nextAnimation = player.nextAnimation;
player.nextAnimation = undefined;
playAnimation(player, nextAnimation);
}
}
if (player.frame !== extraFrame) {
player.frame = extraFrame;
player.dirty = true;
}
}
}
export function drawAnimation(
batch: PaletteSpriteBatch, player: AnimationPlayer, x: number, y: number, color = WHITE, flip = false, maxY = 0
) {
const animation = player.currentAnimation;
if (animation !== undefined) {
const frames = (flip && animation.flipFrames) ? animation.flipFrames : animation.frames;
if (player.frame < frames.length) {
const frame = frames[player.frame];
if (DEVELOPMENT && !frame) {
throw new Error('Undefined frame in sprite animation');
}
if (!frame) // TEMP
return;
if (maxY === 0) {
batch.drawSprite(frame, color, player.palette, x, y);
} else {
drawSpriteCropped(batch, frame, color, player.palette, x, y, maxY);
}
}
}
}
+183
View File
@@ -0,0 +1,183 @@
export interface Animation {
fps: number;
loop: boolean;
frames: any[];
}
export interface AnimatorTransition<T extends Animation> {
state: AnimatorState<T>;
exitAfter?: number;
enterTime?: number;
keepTime?: boolean;
onlyDirectTo?: AnimatorState<T>;
}
export interface AnimatorState<T extends Animation = Animation> {
name: string;
animation: T;
variants: { [key: string]: T; };
from: AnimatorTransition<T>[];
}
export function animatorState<T extends Animation>(
name: string, animation: T, variants: { [key: string]: T; } = {}
): AnimatorState<T> {
return { name, animation, variants, from: [] };
}
export function animatorTransition<T extends Animation>(
from: AnimatorState<T>, to: AnimatorState<T>, options: Partial<AnimatorTransition<T>> = {}
) {
to.from.push({ state: from, ...options });
}
export const anyState = animatorState<any>('any', { fps: 1, loop: false, frames: [] });
export interface Animator<T extends Animation> {
state: AnimatorState<T> | undefined;
target: AnimatorState<T> | undefined;
next: AnimatorTransition<T> | undefined;
time: number;
variant: string;
}
export function createAnimator<T extends Animation>(): Animator<T> {
return {
time: 0,
variant: '',
state: undefined,
target: undefined,
next: undefined,
};
}
export function getAnimation<T extends Animation>(animator: Animator<T>) {
return animator.state && getAnimationForState(animator.state, animator.variant);
}
export function getAnimationFrame<T extends Animation>(animator: Animator<T>) {
const animation = getAnimation(animator);
return animation ? Math.floor(animator.time * animation.fps) % animation.frames.length : 0;
}
export function resetAnimatorState<T extends Animation>(animator: Animator<T>) {
animator.state = undefined;
animator.target = undefined;
animator.next = undefined;
}
export function setAnimatorState<T extends Animation>(animator: Animator<T>, state: AnimatorState<T>) {
if (animator.target !== state) {
if (animator.state !== state) {
if (animator.state === undefined) {
animator.state = state;
} else {
animator.target = state;
}
} else {
animator.target = undefined;
}
animator.next = undefined;
}
}
export function updateAnimator<T extends Animation>(animator: Animator<T>, delta: number) {
const time = animator.time;
animator.time += delta;
if (animator.target !== undefined && animator.state !== undefined && animator.state !== animator.target) {
const animation = getAnimationForState(animator.state, animator.variant);
const animationLength = animation.frames.length / animation.fps;
const frameBefore = Math.floor(time / animationLength);
const frameAfter = Math.floor(animator.time / animationLength);
const frameTimeAfter = (animator.time % animationLength) / animationLength;
let animationEnded = frameBefore !== frameAfter;
let switched = false;
do {
switched = false;
const transition = animator.next = animator.next || findTransition(animator.state, animator.target);
if (transition !== undefined) {
const exitAfter = transition.exitAfter === undefined ? 1 : transition.exitAfter;
if (frameTimeAfter >= exitAfter || animationEnded) {
if (!transition.keepTime) {
animator.time = transition.enterTime || 0;
} else {
animator.time = animator.time % animationLength;
}
setCurrentState(animator, transition.state);
switched = true;
animationEnded = false;
}
}
} while (switched && animator.target);
}
}
function setCurrentState<T extends Animation>(animator: Animator<T>, state: AnimatorState<T>) {
animator.next = undefined;
animator.state = state;
if (state === animator.target) {
animator.target = undefined;
}
}
function getAnimationForState<T extends Animation>(state: AnimatorState<T>, variant: string) {
return state.variants[variant] || state.animation;
}
function findTransition<T extends Animation>(
current: AnimatorState<T>, target: AnimatorState<T>
): AnimatorTransition<T> | undefined {
return findTransMinMax(current, target, 0, 1)
|| findTrans(anyState, target, target, 0, 0, [])
|| findTransMinMax(current, target, 2, 10);
}
function findTransMinMax<T extends Animation>(
current: AnimatorState<T>, target: AnimatorState<T>, min: number, max: number
): AnimatorTransition<T> | undefined {
for (let i = min; i <= max; i++) {
const trans = findTrans(current, target, target, 0, i, [current]);
if (trans !== undefined) {
return trans;
}
}
return undefined;
}
function findTrans<T extends Animation>(
current: AnimatorState<T>, target: AnimatorState<T>, finalTarget: AnimatorState<T>,
depth: number, maxDepth: number, done: AnimatorState<T>[]
): AnimatorTransition<T> | undefined {
if (done.indexOf(target) === -1) {
done.push(target);
for (const from of target.from) {
if (from.state === current && (from.onlyDirectTo === undefined || from.onlyDirectTo === finalTarget)) {
return { ...from, state: target };
}
}
if (depth < maxDepth) {
for (const from of target.from) {
const trans = findTrans(current, from.state, finalTarget, depth + 1, maxDepth, done);
if (trans !== undefined) {
return trans;
}
}
}
}
return undefined;
}
+20
View File
@@ -0,0 +1,20 @@
import { BinaryWriter, getWriterBuffer, createBinaryWriter, resizeWriter } from 'ag-sockets/dist/browser';
export function writeBinary(write: (writer: BinaryWriter) => void): Uint8Array {
const writer = createBinaryWriter();
do {
try {
write(writer);
break;
} catch (e) {
if (e instanceof RangeError || /DataView/.test(e.message)) {
resizeWriter(writer);
} else {
throw e;
}
}
} while (true);
return getWriterBuffer(writer);
}
+114
View File
@@ -0,0 +1,114 @@
export type WriteBits = (value: number, bits: number) => void;
export type ReadBits = (bits: number) => number;
export function numberToBitCount(value: number) {
value = value >>> 0;
for (let mask = 0xffffffff >>> 0, bits = 0; mask; mask = (mask << 1) >>> 0, bits++) {
if ((value & mask) === 0) {
return bits;
}
}
return 32;
}
export function countBits(value: number) {
value = value >>> 0;
let bits = 0;
while (value) {
bits += value & 1;
value = value >>> 1;
}
return bits;
}
export function bitWriter(writes: (writer: WriteBits) => void): Uint8Array {
let buffer = new Uint8Array(16);
let length = 0;
let byte = 0;
let byteBits = 0;
function writeByte(value: number) {
if (buffer.length <= length) {
const newBuffer = new Uint8Array(buffer.length * 2);
newBuffer.set(buffer);
buffer = newBuffer;
}
buffer[length] = value;
length++;
}
writes((value, bits) => {
if (bits < 0 || bits > 32) {
throw new Error('Invalid bit count');
}
while (bits) {
const revByteBits = 8 - byteBits;
const writeBits = revByteBits < bits ? revByteBits : bits;
const write = (value >> (bits - writeBits)) & (0xff >> (8 - writeBits));
byte |= write << (revByteBits - writeBits);
byteBits += writeBits;
bits -= writeBits;
if (byteBits === 8) {
writeByte(byte);
byte = 0;
byteBits = 0;
}
}
});
if (byteBits) {
writeByte(byte);
byteBits = 0;
byte = 0;
}
return buffer.subarray(0, length);
}
export function bitReader(buffer: Uint8Array): ReadBits {
let offset = 0;
return bitReaderCustom(() => {
if (buffer.length <= offset) {
throw new Error('Reading past end');
}
return buffer[offset++];
});
}
export function bitReaderCustom(readByte: () => number): ReadBits {
let byte = 0;
let byteBits = 0;
return bits => {
if (bits < 0 || bits > 32) {
throw new Error('Invalid bit count');
}
let result = 0;
while (bits) {
if (!byteBits) {
byte = readByte();
byteBits = 8;
}
const readBits = byteBits < bits ? byteBits : bits;
const read = (byte >> (byteBits - readBits)) & (0xff >> (8 - readBits));
result = (result << readBits) | read;
bits -= readBits;
byteBits -= readBits;
}
return result >>> 0;
};
}
+132
View File
@@ -0,0 +1,132 @@
import { Entity, Rect, Point, Size, Camera } from './interfaces';
import { CAMERA_WIDTH_MAX, CAMERA_WIDTH_MIN, CAMERA_HEIGHT_MAX, CAMERA_HEIGHT_MIN } from './constants';
import { clamp, intersect, pointInXYWH, pointInRect, lerp } from './utils';
import { toScreenX, toScreenY, toWorldX, toWorldY } from './positionUtils';
import { getChatBallonXY } from '../graphics/graphicsUtils';
const cameraPadding = 0.3;
export const characterHeight = 25;
export function createCamera(): Camera {
return {
x: 0,
y: 0,
w: 100,
h: 100,
offset: 0,
shift: 0,
shiftTarget: 0,
shiftRatio: 0,
actualY: 0,
};
}
export function setupCamera(camera: Camera, x: number, y: number, width: number, height: number, map: Size) {
camera.w = clamp(width, CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX);
camera.h = clamp(height, CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX);
camera.x = clamp(x, 0, toScreenX(map.width) - camera.w);
camera.y = clamp(y, 0, toScreenY(map.height) - camera.h);
}
export function updateCamera(camera: Camera, player: Point, map: Size) {
const cameraWith = camera.w;
const cameraHeight = camera.h;
const cameraHeightShifted = Math.ceil(camera.h - camera.offset);
const playerX = toScreenX(player.x);
const playerY = toScreenY(player.y);
const mapWidth = toScreenX(map.width);
const mapHeight = toScreenY(map.height);
const minX = Math.min(0, (mapWidth - cameraWith) / 2);
const minY = Math.min(0, (mapHeight - cameraHeight) / 2);
const minYShifted = Math.min(0, (mapHeight - cameraHeightShifted) / 2);
const maxX = Math.max(mapWidth - cameraWith, minX);
const maxY = Math.max(mapHeight - cameraHeight, minY);
const maxYShifted = Math.max(mapHeight - cameraHeightShifted, minY);
const hSpace = Math.floor(cameraWith * cameraPadding);
const vSpace = Math.floor(cameraHeight * cameraPadding);
const vSpaceShifted = Math.floor(cameraHeightShifted * cameraPadding);
const hPad = (cameraWith - hSpace) / 2;
const vPad = (cameraHeight - vSpace) / 2;
const vPadShifted = (cameraHeightShifted - vSpaceShifted) / 2;
const minCamX = clamp(playerX - (hSpace + hPad), minX, maxX);
const maxCamX = clamp(playerX - hPad, minX, maxX);
const minCamY = clamp(playerY - (vSpace + vPad) - characterHeight, minY, maxY);
const maxCamY = clamp(playerY - vPad - characterHeight, minY, maxY);
const minCamYShifted = clamp(playerY - (vSpaceShifted + vPadShifted) - characterHeight, minYShifted, maxYShifted);
const maxCamYShifted = clamp(playerY - vPadShifted - characterHeight, minYShifted, maxYShifted);
camera.x = Math.floor(clamp(camera.x, minCamX, maxCamX));
camera.y = Math.floor(clamp(camera.y, minCamY, maxCamY));
camera.shiftTarget = Math.floor(clamp(camera.shiftTarget, minCamYShifted, maxCamYShifted));
camera.actualY = calculateCameraY(camera);
}
export function centerCameraOn(camera: Camera, point: Point) {
camera.x = Math.floor(toScreenX(point.x) - camera.w / 2);
camera.y = Math.floor((toScreenY(point.y) - camera.h / 2) - characterHeight);
camera.shiftTarget = Math.floor((toScreenY(point.y) - Math.ceil(camera.h - camera.offset) / 2) - characterHeight);
}
export function calculateCameraY(camera: Camera) {
return Math.round(lerp(camera.y, camera.shiftTarget - camera.offset, camera.shiftRatio));
}
export function isWorldPointVisible(camera: Camera, point: Point): boolean {
return pointInRect(toScreenX(point.x), toScreenY(point.y), camera);
}
export function isWorldPointWithPaddingVisible(camera: Camera, point: Point, padding: number): boolean {
return pointInXYWH(
toScreenX(point.x), toScreenY(point.y),
camera.x - padding, camera.actualY - padding, camera.w + 2 * padding, camera.h + 2 * padding);
}
export function isAreaVisible(camera: Camera, x: number, y: number, w: number, h: number): boolean {
return intersect(camera.x, camera.actualY, camera.w, camera.h, x, y, w, h);
}
export function isRectVisible(camera: Camera, rect: Rect): boolean {
return intersect(camera.x, camera.actualY, camera.w, camera.h, rect.x, rect.y, rect.w, rect.h);
}
export function isBoundsVisible(camera: Camera, bounds: Rect | undefined, x: number, y: number): boolean {
return bounds !== undefined &&
isAreaVisible(camera, toScreenX(x) + bounds.x, toScreenY(y) + bounds.y, bounds.w, bounds.h);
}
export function isEntityVisible(camera: Camera, entity: Entity): boolean {
return isBoundsVisible(camera, entity.bounds, entity.x, entity.y);
}
function isChatBaloonAboveScreenTop(camera: Camera, entity: Entity) {
return getChatBallonXY(entity, camera).y <= -5;
}
export function isChatVisible(camera: Camera, entity: Entity): boolean {
return isBoundsVisible(camera, entity.bounds, entity.x, entity.y)
&& !isChatBaloonAboveScreenTop(camera, entity);
}
export function screenToWorld(camera: Camera, point: Point): Point {
return {
x: toWorldX(point.x + camera.x),
y: toWorldY(point.y + camera.actualY),
};
}
export function worldToScreen(camera: Camera, point: Point): Point {
return {
x: Math.floor(toScreenX(point.x) - camera.x),
y: Math.floor(toScreenY(point.y) - camera.actualY),
};
}
// export function mapDepth(camera: Camera, y: number): number {
// return (toScreenY(y) - camera.actualY) - camera.maxDepth;
// }
+306
View File
@@ -0,0 +1,306 @@
import { Entity, IMap, Region, EntityFlags } from './interfaces';
import { clamp } from './utils';
import { toWorldX, toWorldY } from './positionUtils';
import { getRegionGlobal, isInWaterAt } from './worldMap';
import { tileWidth, tileHeight, PONY_TYPE, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from './constants';
import { isInTheAir, isFlying } from './entityUtils';
let isCollidingCount = 0;
let isCollidingObjectCount = 0;
export function getCollisionStats() {
const stats = { isCollidingCount, isCollidingObjectCount };
isCollidingCount = 0;
isCollidingObjectCount = 0;
return stats;
}
export function isOutsideMap<T>(x: number, y: number, map: IMap<T>): boolean {
return x < 0 || y < 0 || x >= map.width || y >= map.height;
}
export function canCollideWith(entity: Entity): boolean {
return (entity.flags & EntityFlags.CanCollideWith) !== 0;
}
export function isStaticCollision<T>(entity: Entity, map: IMap<T>, forceOnGround = false) {
if (DEVELOPMENT && entity.type !== PONY_TYPE) {
console.error(`isStaticCollision: non-pony entity`);
}
const flying = !forceOnGround && isInTheAir(entity);
return isPonyColliding(entity.x, entity.y, map as any, flying);
}
export function fixCollision<T>(entity: Entity, map: IMap<T>) {
if (DEVELOPMENT && entity.type !== PONY_TYPE) {
console.error(`fixCollision: non-pony entity`);
}
const flying = isInTheAir(entity);
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
const tx = entity.x + x;
const ty = entity.y + y;
if (!isPonyColliding(tx, ty, map as any, flying)) {
entity.x += x;
entity.y += y;
return true;
}
}
}
return false;
}
function isPonyColliding<T extends Region | undefined>(x: number, y: number, map: IMap<T>, flying: boolean): boolean {
if (isOutsideMap(x, y, map)) {
return true;
}
const region = getRegionGlobal(map, x, y);
if (region === undefined) {
return true;
}
const rx = clamp(Math.floor((x - region.x * REGION_SIZE) * tileWidth), 0, REGION_WIDTH);
const ry = clamp(Math.floor((y - region.y * REGION_SIZE) * tileHeight), 0, REGION_HEIGHT);
const pixel = region.collider[rx + ry * REGION_WIDTH];
const mask = flying ? 2 : 1;
return (pixel & mask) !== 0;
}
function isColliding(x: number, y: number, mask: number, map: IMap<Region | undefined>) {
if (x < 0 || x >= (map.width * tileWidth) || y < 0 || y >= (map.height * tileHeight)) {
return true;
} else {
const regionX = (x / REGION_WIDTH) | 0;
const regionY = (y / REGION_HEIGHT) | 0;
const region = map.regions[regionX + regionY * map.regionsX];
if (region === undefined) {
return true;
} else {
const insideX = (x % REGION_WIDTH) | 0;
const insideY = (y % REGION_HEIGHT) | 0;
return (region.collider[insideX + insideY * REGION_WIDTH] & mask) !== 0;
}
}
}
export function updatePosition(entity: Entity, delta: number, map: IMap<Region | undefined>) {
const ex = entity.x;
const ey = entity.y;
const speed = (!isFlying(entity) && isInWaterAt(map, ex, ey)) ? 0.5 : 1.0;
const destX = ex + entity.vx * speed * delta;
const destY = ey + entity.vy * speed * delta;
if ((entity.flags & EntityFlags.CanCollide) === 0) {
entity.x = destX;
entity.y = destY;
return;
}
if (DEVELOPMENT && entity.type !== PONY_TYPE) {
console.error(`updatePosition: non-pony entity`);
}
const flying = isInTheAir(entity);
const mask = flying ? 2 : 1;
const srcX = ex * tileWidth;
const srcY = ey * tileHeight;
let dstX = destX * tileWidth;
let dstY = destY * tileHeight;
const x0 = Math.floor(srcX) | 0;
const y0 = Math.floor(srcY) | 0;
const x1 = Math.floor(dstX) | 0;
const y1 = Math.floor(dstY) | 0;
const minX = Math.min(x0, x1) | 0;
const maxX = Math.max(x0, x1) | 0;
const minY = Math.min(y0, y1) | 0;
const maxY = Math.max(y0, y1) | 0;
let x = x0 | 0;
let y = y0 | 0;
let actualX = x | 0;
let actualY = y | 0;
if (isColliding(actualX, actualY, mask, map)) {
if (!isOutsideMap(destX, destY, map)) {
entity.x = destX;
entity.y = destY;
}
return;
}
const a = (dstY - srcY) / (dstX - srcX);
const b = srcY - a * srcX;
const useGt = srcY < dstY;
let stepXT = 0 | 0, stepYT = 0 | 0;
let stepXF = 0 | 0, stepYF = 0 | 0;
let ox = 0, oy = 0;
const shiftRight = srcX <= dstX;
const shiftLeft = srcX >= dstX;
const shiftUp = srcY >= dstY;
const shiftDown = srcY <= dstY;
const horizontalOrVertical = srcX === dstX || srcY === dstY;
if (srcX < dstX) {
if (srcY < dstY) {
ox = 1;
oy = 1;
stepYT = 1 | 0;
stepXF = 1 | 0;
} else {
ox = 1;
stepYT = -1 | 0;
stepXF = 1 | 0;
}
} else if (srcX > dstX) {
if (srcY < dstY) {
oy = 1;
stepYT = 1 | 0;
stepXF = -1 | 0;
} else {
stepYT = -1 | 0;
stepXF = -1 | 0;
}
} else {
if (srcY < dstY) {
stepYF = stepYT = 1 | 0;
} else {
stepYF = stepYT = -1 | 0;
}
}
let steps = 1000;
for (; steps; steps--) {
const fx = a * (x + ox) + b;
const fy = y + oy;
let tx = 0 | 0;
let ty = 0 | 0;
if (useGt ? (fx > fy) : (fx < fy)) {
tx = (tx + stepXT) | 0;
ty = (ty + stepYT) | 0;
} else {
tx = (tx + stepXF) | 0;
ty = (ty + stepYF) | 0;
}
x = (x + tx) | 0;
y = (y + ty) | 0;
if (x < minX || x > maxX || y < minY || y > maxY) {
break;
}
let actualNX = (actualX + tx) | 0;
let actualNY = (actualY + ty) | 0;
let collides = isColliding(actualNX, actualNY, mask, map);
let canMove = false;
if (collides) {
if (tx !== 0) {
let canShiftUp = false;
let canShiftDown = false;
if (
shiftUp && (canShiftUp = !isColliding(actualX, actualY - 1, mask, map)) &&
!isColliding(actualNX, actualY - 1, mask, map)
) {
actualNX = actualX;
actualNY -= 1;
dstY -= 1;
collides = false;
} else if (
shiftDown && (canShiftDown = !isColliding(actualX, actualY + 1, mask, map)) &&
!isColliding(actualNX, actualY + 1, mask, map)
) {
actualNX = actualX;
actualNY += 1;
dstY += 1;
collides = false;
} else if (shiftUp && canShiftUp && !isColliding(actualNX, actualY - 2, mask, map)) {
actualNX = actualX;
actualNY -= 1;
dstY -= 1;
collides = false;
} else if (shiftDown && canShiftDown && !isColliding(actualNX, actualY + 2, mask, map)) {
actualNX = actualX;
actualNY += 1;
dstY += 1;
collides = false;
}
canMove = canShiftUp || canShiftDown;
} else {
let canShiftLeft = false;
let canShiftRight = false;
if (
shiftLeft && (canShiftLeft = !isColliding(actualX - 1, actualY, mask, map)) &&
!isColliding(actualX - 1, actualNY, mask, map)
) {
actualNX -= 1;
actualNY = actualY;
dstX -= 1;
collides = false;
} else if (
shiftRight && (canShiftRight = !isColliding(actualX + 1, actualY, mask, map)) &&
!isColliding(actualX + 1, actualNY, mask, map)
) {
actualNX += 1;
actualNY = actualY;
dstX += 1;
collides = false;
} else if (shiftLeft && canShiftLeft && !isColliding(actualX - 2, actualNY, mask, map)) {
actualNX -= 1;
actualNY = actualY;
dstX -= 1;
collides = false;
} else if (shiftRight && canShiftRight && !isColliding(actualX + 2, actualNY, mask, map)) {
actualNX += 1;
actualNY = actualY;
dstX += 1;
collides = false;
}
canMove = canShiftLeft || canShiftRight;
}
}
if (!collides) {
actualX = actualNX;
actualY = actualNY;
} else if (!canMove || horizontalOrVertical) {
break;
}
}
const epsilon = 1 / 1024;
const left = Math.min(x0, actualX);
const right = Math.max(x0 + 1, actualX + 1) - epsilon;
const top = Math.min(y0, actualY);
const bottom = Math.max(y0 + 1, actualY + 1) - epsilon;
entity.x = toWorldX(clamp(dstX, left, right));
entity.y = toWorldY(clamp(dstY, top, bottom));
if (DEVELOPMENT && steps <= 0) {
console.error('Overflow collision steps');
}
}
+495
View File
@@ -0,0 +1,495 @@
import { isString } from 'lodash';
import { clamp } from './utils';
export const colorNames: { [key: string]: string | undefined } = {
aliceblue: 'f0f8ff',
antiquewhite: 'faebd7',
aqua: '00ffff',
aquamarine: '7fffd4',
azure: 'f0ffff',
beige: 'f5f5dc',
bisque: 'ffe4c4',
black: '000000',
blanchedalmond: 'ffebcd',
blue: '0000ff',
blueviolet: '8a2be2',
brown: 'a52a2a',
burlywood: 'deb887',
cadetblue: '5f9ea0',
chartreuse: '7fff00',
chocolate: 'd2691e',
coral: 'ff7f50',
cornflowerblue: '6495ed',
cornsilk: 'fff8dc',
crimson: 'dc143c',
cyan: '00ffff',
darkblue: '00008b',
darkcyan: '008b8b',
darkgoldenrod: 'b8860b',
darkgray: 'a9a9a9',
darkgreen: '006400',
darkkhaki: 'bdb76b',
darkmagenta: '8b008b',
darkolivegreen: '556b2f',
darkorange: 'ff8c00',
darkorchid: '9932cc',
darkred: '8b0000',
darksalmon: 'e9967a',
darkseagreen: '8fbc8f',
darkslateblue: '483d8b',
darkslategray: '2f4f4f',
darkturquoise: '00ced1',
darkviolet: '9400d3',
deeppink: 'ff1493',
deepskyblue: '00bfff',
dimgray: '696969',
dodgerblue: '1e90ff',
feldspar: 'd19275',
firebrick: 'b22222',
floralwhite: 'fffaf0',
forestgreen: '228b22',
fuchsia: 'ff00ff',
gainsboro: 'dcdcdc',
ghostwhite: 'f8f8ff',
gold: 'ffd700',
goldenrod: 'daa520',
gray: '808080',
green: '008000',
greenyellow: 'adff2f',
honeydew: 'f0fff0',
hotpink: 'ff69b4',
indianred: 'cd5c5c',
indigo: '4b0082',
ivory: 'fffff0',
khaki: 'f0e68c',
lavender: 'e6e6fa',
lavenderblush: 'fff0f5',
lawngreen: '7cfc00',
lemonchiffon: 'fffacd',
lightblue: 'add8e6',
lightcoral: 'f08080',
lightcyan: 'e0ffff',
lightgoldenrodyellow: 'fafad2',
lightgrey: 'd3d3d3',
lightgreen: '90ee90',
lightpink: 'ffb6c1',
lightsalmon: 'ffa07a',
lightseagreen: '20b2aa',
lightskyblue: '87cefa',
lightslateblue: '8470ff',
lightslategray: '778899',
lightsteelblue: 'b0c4de',
lightyellow: 'ffffe0',
lime: '00ff00',
limegreen: '32cd32',
linen: 'faf0e6',
magenta: 'ff00ff',
maroon: '800000',
mediumaquamarine: '66cdaa',
mediumblue: '0000cd',
mediumorchid: 'ba55d3',
mediumpurple: '9370d8',
mediumseagreen: '3cb371',
mediumslateblue: '7b68ee',
mediumspringgreen: '00fa9a',
mediumturquoise: '48d1cc',
mediumvioletred: 'c71585',
midnightblue: '191970',
mintcream: 'f5fffa',
mistyrose: 'ffe4e1',
moccasin: 'ffe4b5',
navajowhite: 'ffdead',
navy: '000080',
oldlace: 'fdf5e6',
olive: '808000',
olivedrab: '6b8e23',
orange: 'ffa500',
orangered: 'ff4500',
orchid: 'da70d6',
palegoldenrod: 'eee8aa',
palegreen: '98fb98',
paleturquoise: 'afeeee',
palevioletred: 'd87093',
papayawhip: 'ffefd5',
peachpuff: 'ffdab9',
peru: 'cd853f',
pink: 'ffc0cb',
plum: 'dda0dd',
powderblue: 'b0e0e6',
purple: '800080',
red: 'ff0000',
rosybrown: 'bc8f8f',
royalblue: '4169e1',
saddlebrown: '8b4513',
salmon: 'fa8072',
sandybrown: 'f4a460',
seagreen: '2e8b57',
seashell: 'fff5ee',
sienna: 'a0522d',
silver: 'c0c0c0',
skyblue: '87ceeb',
slateblue: '6a5acd',
slategray: '708090',
snow: 'fffafa',
springgreen: '00ff7f',
steelblue: '4682b4',
tan: 'd2b48c',
teal: '008080',
thistle: 'd8bfd8',
tomato: 'ff6347',
turquoise: '40e0d0',
violet: 'ee82ee',
violetred: 'd02090',
wheat: 'f5deb3',
white: 'ffffff',
whitesmoke: 'f5f5f5',
yellow: 'ffff00',
yellowgreen: '9acd32'
};
const TRANSPARENT = 0x00000000 >>> 0;
const BLACK = 0x000000ff >>> 0;
export interface HSVA {
h: number;
s: number;
v: number;
a: number;
}
export interface RGB {
r: number;
g: number;
b: number;
}
export interface RGBA extends RGB {
a: number;
}
export function getR(color: number) {
return (color >> 24) & 0xff;
}
export function getG(color: number) {
return (color >> 16) & 0xff;
}
export function getB(color: number) {
return (color >> 8) & 0xff;
}
export function getAlpha(color: number) {
return color & 0xff;
}
export function withAlpha(color: number, alpha: number) {
return (color & 0xffffff00) | (alpha & 0xff);
}
export function withAlphaFloat(color: number, alpha: number) {
return (color & 0xffffff00) | ((alpha * 255) & 0xff);
}
// to
export function colorToRGBA(color: number): RGBA {
return {
r: getR(color),
g: getG(color),
b: getB(color),
a: getAlpha(color),
};
}
export function colorToHSVA(color: number, h?: number): HSVA {
return rgb2hsv(getR(color), getG(color), getB(color), getAlpha(color) / 255, h);
}
export function colorToCSS(color: number): string {
const alpha = getAlpha(color);
if (alpha === 0xff) {
return `#${colorToHexRGB(color)}`;
} else {
return `rgba(${getR(color)},${getG(color)},${getB(color)},${alpha / 255})`;
}
}
function toHex(value: number, length: number): string {
return value.toString(16).padStart(length, '0');
}
export function colorToHexRGB(color: number) {
return toHex(color >>> 8, 6);
}
export function colorToFloatArray(color: number): Float32Array {
const result = new Float32Array(4);
colorToExistingFloatArray(result, color);
return result;
}
export function colorToExistingFloatArray(array: Float32Array, color: number) {
array[0] = getR(color) / 255;
array[1] = getG(color) / 255;
array[2] = getB(color) / 255;
array[3] = getAlpha(color) / 255;
}
const int8 = new Int8Array(4);
const int32 = new Int32Array(int8.buffer, 0, 1);
const float32 = new Float32Array(int8.buffer, 0, 1);
export function colorToFloat(color: number): number {
const int = (getAlpha(color) << 24) | (getB(color) << 16) | (getG(color) << 8) | getR(color);
int32[0] = int & 0xfeffffff;
return float32[0];
}
export function colorToFloatAlpha(color: number, alpha: number /* 0-1 */): number {
const int = (((getAlpha(color) * alpha) & 0xff) << 24) | (getB(color) << 16) | (getG(color) << 8) | getR(color);
int32[0] = int & 0xfeffffff;
return float32[0];
}
// from
export function colorFromRGBA(r: number, g: number, b: number, a: number /* 0-255 */) {
return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0;
}
export function colorFromHSVA(h: number, s: number, v: number, a: number /* 0-1 */) {
const { r, g, b } = hsv2rgb(h, s, v);
return colorFromRGBA(r, g, b, a * 255);
}
export function colorFromHSVAObject({ h, s, v, a }: HSVA) {
return colorFromHSVA(h, s, v, a);
}
// parse
export function parseColorFast(str: string): number {
if (!isString(str))
return TRANSPARENT;
const int = parseInt(str, 16);
if (str.length !== 6 || isNaN(int) || int < 0) {
return parseColorWithAlpha(str, 1);
} else {
return (((int << 8) | 0xff) >>> 0);
}
}
export function parseColor(str: string): number {
if (!isString(str))
return TRANSPARENT;
str = str.trim().toLowerCase();
if (str === '' || str === 'none' || str === 'transparent')
return TRANSPARENT;
str = colorNames[str] || str;
const m = /(\d+)[ ,]+(\d+)[ ,]+(\d+)(?:[ ,]+(\d*\.?\d+))?/.exec(str);
if (m) {
return colorFromRGBA(
parseInt(m[1], 10),
parseInt(m[2], 10),
parseInt(m[3], 10),
m[4] ? parseFloat(m[4]) * 255 : 255);
}
const n = /[0-9a-f]+/i.exec(str);
if (n) {
const s = n[0];
if (s.length === 3) {
return colorFromRGBA(
parseInt(s.charAt(0), 16) * 0x11,
parseInt(s.charAt(1), 16) * 0x11,
parseInt(s.charAt(2), 16) * 0x11, 255);
} else {
return colorFromRGBA(
parseInt(s.substr(0, 2), 16),
parseInt(s.substr(2, 2), 16),
parseInt(s.substr(4, 2), 16),
s.length >= 8 ? parseInt(s.substr(6, 2), 16) : 255);
}
}
return BLACK;
}
export function parseColorWithAlpha(str: string, alpha: number /* 0-1 */): number {
return ((parseColor(str) & 0xffffff00) | ((alpha * 255) & 0xff)) >>> 0;
}
// utils
export function toGrayscale(color: number) {
const c = Math.round(clamp(getR(color) * 0.2126 + getG(color) * 0.7152 + getB(color) * 0.0722, 0, 255)) | 0;
const a = getAlpha(color);
return colorFromRGBA(c, c, c, a);
}
export function makeTransparent(color: number, factor: number /* 0-1 */): number {
return ((color & 0xffffff00) | ((getAlpha(color) * factor) & 0xff)) >>> 0;
}
export function multiplyColor(color: number, factor: number /* 0-1 */): number {
return colorFromRGBA(
clamp(getR(color) * factor, 0, 255),
clamp(getG(color) * factor, 0, 255),
clamp(getB(color) * factor, 0, 255),
getAlpha(color)
);
}
export function lerpColors(a: number, b: number, factor: number): number {
const f = factor;
const t = 1 - factor;
return colorFromRGBA(
getR(a) * t + getR(b) * f,
getG(a) * t + getG(b) * f,
getB(a) * t + getB(b) * f,
getAlpha(a) * t + getAlpha(b) * f
);
}
/// r, g, b = <0, 255>, a = <0, 1>
export function rgb2hsv(r: number, g: number, b: number, a: number /* 0-1 */, h = 0): HSVA {
r = r / 255;
g = g / 255;
b = b / 255;
h = h / 360;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const v = max;
const d = max - min;
const s = max === 0 ? 0 : d / max;
if (max !== min) {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return { h: h * 360, s, v, a };
}
/// h = <0, 360>; s, v = <0, 1>
export function hsv2rgb(h: number, s: number, v: number): RGB {
h = Math.max(0, Math.min(360, h === 360 ? 0 : h));
s = Math.max(0, Math.min(1, s));
v = Math.max(0, Math.min(1, v));
let r = v;
let g = v;
let b = v;
if (s !== 0) {
h /= 60;
const i = Math.floor(h);
const f = h - i;
const p = v * (1 - s);
const q = v * (1 - s * f);
const t = v * (1 - s * (1 - f));
switch (i) {
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
default:
r = v;
g = p;
b = q;
}
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
};
}
export function h2rgb(h: number): RGB {
h /= 60;
let r = 0, g = 0, b = 0;
const i = Math.floor(h);
const f = h - i;
const q = (1 - f);
const t = (1 - (1 - f));
switch (i) {
case 0:
r = 1;
g = t;
break;
case 1:
r = q;
g = 1;
break;
case 2:
g = 1;
b = t;
break;
case 3:
g = q;
b = 1;
break;
case 4:
r = t;
b = 1;
break;
default:
r = 1;
b = q;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
+173
View File
@@ -0,0 +1,173 @@
import { MessageType, Season, TileType } from './interfaces';
import { colorFromHSVA, colorToHexRGB, parseColorFast, colorToHSVA, withAlphaFloat } from './color';
import { invalidEnum, invalidEnumReturn } from './utils';
import { darkenForOutline } from './ponyInfo';
// basic
export const TRANSPARENT = 0;
export const WHITE = 0xffffffff;
export const BLACK = 0x000000ff;
export const ORANGE = 0xffa500ff;
export const BLUE = 0x0000ffff;
export const GREEN = 0x00ff00ff;
export const YELLOW = 0xffff00ff;
export const MAGENTA = 0xff00ffff;
export const CYAN = 0x00ffffff;
export const GRAY = 0x444444ff;
export const RED = 0xff0000ff;
export const HOTPINK = 0xff69b4ff;
export const PURPLE = 0x800080ff;
// messages
export const BG_COLOR = 0x333333ff;
export const ADMIN_COLOR = 0xff69b4ff;
export const MOD_COLOR = 0xb689ffff;
export const SYSTEM_COLOR = 0xbbbbbbff;
export const MESSAGE_COLOR = 0x333333ff;
export const ANNOUNCEMENT_COLOR = 0xf0e68Cff;
export const PARTY_COLOR = 0x71daffff;
export const THINKING_COLOR = 0xafafafff;
export const PARTY_THINKING_COLOR = 0x5da9c4ff;
export const OUTLINE_COLOR = withAlphaFloat(BLACK, 0.4);
export const PATREON_COLOR = 0xf86754ff;
export const WHISPER_COLOR = 0xffa1dfff;
export const FRIENDS_COLOR = 0x71ff7fff;
export const SUPPORTER1_COLOR = PATREON_COLOR;
export const SUPPORTER2_COLOR = 0xffa32bff;
export const SUPPORTER3_COLOR = 0xffcf00ff;
export const SUPPORTER2_BANDS = [0xffdfc1ff, 0xffcd99ff, 0xff9f3bff, 0xd97e09ff];
export const SUPPORTER3_BANDS = [0xffffffff, 0xfffda4ff, 0xffea3bff, 0xfdbb0bff];
// game
export const SHADOW_COLOR = withAlphaFloat(BLACK, 0.3);
export const CLOUD_SHADOW_COLOR = withAlphaFloat(BLACK, 0.2);
export const SHINES_COLOR = withAlphaFloat(WHITE, 0.4);
export const FAR_COLOR = colorFromHSVA(0, 0, 0.8, 1);
export const GRASS_COLOR = 0x90ee90ff;
export const HEARTS_COLOR = 0xf15f9dff;
export const CAVE_LIGHT = 0x090c21ff; // 0x253f76ff;
export const CAVE_SHADOW = 0x00000055;
export let ACTION_EXPRESSION_BG = '#e7aa4e';
export const ACTION_EXPRESSION_EYE_COLOR = '#b17a00';
export const ACTION_ACTION_BG = '#dc9d82';
export const ACTION_ACTION_COAT_COLOR = '#d9835e';
export const ACTION_COMMAND_BG = '#5fb7b3';
export const ACTION_ITEM_BG = '#cecf59';
export const ENTITY_ITEM_BG = '#dc76bc';
export const MAGIC_ALPHA = 150;
export function updateActionColor(color: string) {
if (DEVELOPMENT) {
ACTION_EXPRESSION_BG = color;
}
}
// utils
export function getMessageColor(type: MessageType): number {
switch (type) {
case MessageType.Chat: return WHITE;
case MessageType.System: return SYSTEM_COLOR;
case MessageType.Admin: return ADMIN_COLOR;
case MessageType.Mod: return MOD_COLOR;
case MessageType.Party: return PARTY_COLOR;
case MessageType.Thinking: return THINKING_COLOR;
case MessageType.PartyThinking: return PARTY_THINKING_COLOR;
case MessageType.Supporter1: return SUPPORTER1_COLOR;
case MessageType.Supporter2: return SUPPORTER2_COLOR;
case MessageType.Supporter3: return SUPPORTER3_COLOR;
case MessageType.Whisper:
case MessageType.WhisperTo:
return WHISPER_COLOR;
case MessageType.Announcement:
case MessageType.PartyAnnouncement:
case MessageType.WhisperAnnouncement:
case MessageType.WhisperToAnnouncement:
return ANNOUNCEMENT_COLOR;
case MessageType.Dismiss: return TRANSPARENT;
default:
return invalidEnumReturn(type, WHITE);
}
}
export function fillToOutline(color: string | undefined): string | undefined {
return color ? colorToHexRGB(fillToOutlineColor(parseColorFast(color))) : undefined;
}
export function fillToOutlineWithDarken(color: string | undefined): string | undefined {
return color ? colorToHexRGB(darkenForOutline(fillToOutlineColor(parseColorFast(color)))) : undefined;
}
export function fillToOutlineColor(color: number): number {
const { h, s, v, a } = colorToHSVA(color);
return colorFromHSVA(h, Math.min(s * 1.3, 1), v * 0.7, a);
}
const LIGHT_BLUSH = 0xff89aeff;
const DARK_BLUSH = 0xc90040ff;
export function blushColor(coat: number): number {
const { h, s, v } = colorToHSVA(coat);
if (
(h < 15 && s > 0.2 && s < 0.7 && v > 0.85) ||
(h > 15 && h < 50 && s > 0.2 && v > 0.85) ||
(h > 280 && s > 0.2 && s < 0.7 && v > 0.85)
) {
return DARK_BLUSH;
} else {
return LIGHT_BLUSH;
}
}
export function getTileColor(tile: TileType, season: Season) {
switch (tile) {
case TileType.Dirt:
case TileType.ElevatedDirt:
if (season === Season.Autumn) {
return 0xedd29eff;
} else if (season === Season.Winter) {
return 0xd9c2a1ff;
} else {
return 0xf5d99bff;
}
case TileType.Water:
case TileType.WalkableWater:
case TileType.Boat:
return 0x6dbdecff;
case TileType.Grass:
if (season === Season.Autumn) {
return 0xddcf71ff;
} else if (season === Season.Winter) {
return 0xe1ebf8ff;
} else {
return 0x7cc991ff;
}
case TileType.Ice:
case TileType.WalkableIce:
return 0xc1dcecff;
case TileType.SnowOnIce:
return 0xe4eefbff;
case TileType.Wood:
return 0xd7ac7eff;
case TileType.Stone:
return 0x9da6abff;
case TileType.Stone2:
return 0xa0a691ff;
case TileType.None:
case TileType.WallH:
case TileType.WallV:
return BLACK;
default:
invalidEnum(tile);
return BLACK;
}
}
+157
View File
@@ -0,0 +1,157 @@
import { toByteArray } from 'base64-js';
import { bitWriter, bitReader } from './bitUtils';
import { REGION_SIZE } from './constants';
function getBitsForNumber(value: number) {
let bits = 0;
let max = value - 1;
while (max > 0) {
bits++;
max >>= 1;
}
return bits;
}
export function compressTiles(tiles: Uint8Array): Uint8Array {
const types: number[] = [];
for (let i = 0; i < tiles.length; i++) {
const tile = tiles[i];
if (types.indexOf(tile) === -1) {
types.push(tile);
}
}
const bitsPerTile = getBitsForNumber(types.length);
const bitsPerRun = 4;
return bitWriter(write => {
write(types.length, 8);
for (const type of types) {
write(type, 8);
}
if (types.length > 1) {
for (let i = 0; i < tiles.length; i++) {
const value = tiles[i];
let count = 1;
if (i === (tiles.length - 1)) {
write(count | 0b1000, bitsPerRun);
write(types.indexOf(value), bitsPerTile);
} else {
i++;
if (value === tiles[i]) {
while (i < tiles.length && count < 0b111 && tiles[i] === value) {
i++;
count++;
}
i--;
write(count, bitsPerRun);
write(types.indexOf(value), bitsPerTile);
} else {
let last = tiles[i];
let last2 = last;
let pushLast = true;
const values = [value];
count++;
for (i++; i < tiles.length; i++) {
last2 = tiles[i];
if (last2 === last) {
i -= 2;
count--;
pushLast = false;
break;
} else if (count === 0b111) {
i -= 1;
break;
} else {
values.push(last);
count++;
last = last2;
}
}
write(count | 0b1000, bitsPerRun);
for (const v of values) {
write(types.indexOf(v), bitsPerTile);
}
if (pushLast) {
write(types.indexOf(last), bitsPerTile);
}
}
}
}
}
});
}
export function decompressTiles(data: Uint8Array): Uint8Array {
const size = REGION_SIZE * REGION_SIZE;
const result = new Uint8Array(size);
const read = bitReader(data);
const typesCount = read(8);
const types: number[] = [];
for (let i = 0; i < typesCount; i++) {
types.push(read(8));
}
if (types.length === 1) {
result.fill(types[0]);
} else {
const bitsPerTile = getBitsForNumber(typesCount);
const bitsPerRun = 4;
for (let i = 0; i < size;) {
const value = read(bitsPerRun);
if ((value & 0b1000) === 0) {
const count = value;
const entry = read(bitsPerTile);
for (let j = 0; j < count; j++) {
result[i] = types[entry];
i++;
}
} else {
const count = value & 0b0111;
for (let j = 0; j < count; j++) {
result[i] = types[read(bitsPerTile)];
i++;
}
}
}
}
return result;
}
export function deserializeTiles(tiles: string) {
const decodedTiles = toByteArray(tiles);
const result: number[] = [];
for (let i = 0; i < decodedTiles.length; i += 2) {
let count = decodedTiles[i];
const tile = decodedTiles[i + 1];
while (count > 0) {
result.push(tile);
count--;
}
}
return result;
}
+624
View File
@@ -0,0 +1,624 @@
import { findLastIndex, isString, isBoolean, isNumber, merge } from 'lodash';
import { fromByteArray, toByteArray } from 'base64-js';
import { PonyInfoNumber, SpriteSet, PonyInfo, PonyInfoBase, PaletteManager, PalettePonyInfo, ColorExtraSet } from './interfaces';
import { syncLockedPonyInfoNumber, syncLockedPonyInfo, createBasePony, toPaletteNumber } from './ponyInfo';
import { bitWriter, bitReader, ReadBits, WriteBits, countBits, numberToBitCount } from './bitUtils';
import { BLACK, WHITE, TRANSPARENT } from './colors';
import { at, toInt, pushUniq, array, clamp, includes, att } from './utils';
import { getColorCount } from '../client/spriteUtils';
import * as sprites from '../generated/sprites';
import { parseColorFast, colorToHexRGB } from './color';
import {
SLEEVED_ACCESSORIES, frontHooves, mergedFacialHair, mergedBackAccessories, mergedManes,
mergedBackManes, mergedExtraAccessories, mergedHeadAccessories
} from '../client/ponyUtils';
import { CM_SIZE } from './constants';
export const VERSION = 3;
interface FieldDefinition<T> {
name: keyof PonyInfo;
default?: T;
omit?: (info: PonyInfoBase<any, SpriteSet<any>>) => boolean;
dontSave?: boolean;
}
interface SetDefinition extends FieldDefinition<PrecompressedSet> {
preserveOnZero?: boolean;
sets: ColorExtraSet[];
minColors?: number;
// defaultLockFills?: boolean[];
// defaultLockOutlines?: boolean[];
}
export interface PrecompressedSet {
type: number;
pattern: number;
colors: number;
fillLocks: number;
fills: number[];
outlineLocks: number;
outlines: number[];
}
export interface Precompressed {
version: number;
colors: number[];
setFields: (PrecompressedSet | undefined)[];
colorFields: number[];
numberFields: number[];
booleanFields: boolean[];
cm: number[];
}
const identity = <T>(x: T) => x;
const not = <T>(x: T) => !x;
function emptyOrUnlocked<T>(set: SpriteSet<T> | undefined): boolean {
return !set || !set.type || !set.lockFills || set.lockFills.every(x => !x);
}
function emptyOrZeroLocked<T>(set: SpriteSet<T> | undefined, customOutlines: boolean): boolean {
return !set || (
set.type === 0 && set.pattern === 0 && set.lockFills !== undefined && set.lockFills[0] === true &&
(!customOutlines || (set.lockOutlines !== undefined && set.lockOutlines[0] === true)));
}
function empty<T>(set: SpriteSet<T> | undefined): boolean {
return !set || !set.type;
}
function omitMane(info: PonyInfoNumber) {
return empty(info.mane) && emptyOrUnlocked(info.backMane)
&& emptyOrUnlocked(info.tail) && emptyOrUnlocked(info.facialHair);
}
function omitHead(info: PonyInfoNumber): boolean {
return emptyOrZeroLocked(info.head, !!info.customOutlines);
}
function omitSleeves(info: PonyInfoNumber) {
return !info.chestAccessory || !includes(SLEEVED_ACCESSORIES, toInt(info.chestAccessory.type));
}
function omitFrontHooves(info: PonyInfoNumber) {
return empty(info.frontHooves) && emptyOrUnlocked(info.backHooves);
}
function readTimes(read: ReadBits, count: number, bitsPerItem: number): number[] {
const result: number[] = [];
for (let i = 0; i < count; i++) {
result[i] = read(bitsPerItem);
}
return result;
}
// NOTE: do not reorder or remove
const setFields: SetDefinition[] = [
{ name: 'extraAccessory', sets: mergedExtraAccessories!, preserveOnZero: true },
{ name: 'nose', sets: sprites.noses[0]!, preserveOnZero: true },
{ name: 'ears', sets: sprites.ears!, preserveOnZero: true },
{ name: 'mane', sets: mergedManes!, preserveOnZero: true, minColors: 1, omit: omitMane },
{ name: 'backMane', sets: mergedBackManes! },
{ name: 'tail', sets: sprites.tails[0]! },
{ name: 'horn', sets: sprites.horns! },
{ name: 'wings', sets: sprites.wings[0]! },
{ name: 'frontHooves', sets: frontHooves[1]!, preserveOnZero: true, minColors: 1, omit: omitFrontHooves },
{ name: 'backHooves', sets: sprites.backLegHooves[1]! },
{ name: 'facialHair', sets: mergedFacialHair! },
{ name: 'headAccessory', sets: mergedHeadAccessories },
{ name: 'earAccessory', sets: sprites.earAccessories! },
{ name: 'faceAccessory', sets: sprites.faceAccessories! },
{ name: 'neckAccessory', sets: sprites.neckAccessories[1]! },
{ name: 'frontLegAccessory', sets: sprites.frontLegAccessories[1]! },
{ name: 'backLegAccessory', sets: sprites.backLegAccessories[1]!, omit: info => !!info.lockBackLegAccessory },
{ name: 'backAccessory', sets: mergedBackAccessories! },
{ name: 'waistAccessory', sets: sprites.waistAccessories[1]! },
{ name: 'chestAccessory', sets: sprites.chestAccessories[1]! },
{ name: 'sleeveAccessory', sets: sprites.frontLegSleeves[1]!, preserveOnZero: true, omit: omitSleeves },
{ name: 'head', sets: sprites.head0[1]!, preserveOnZero: true, omit: omitHead },
{
name: 'frontLegAccessoryRight',
sets: sprites.frontLegAccessories[1]!,
omit: info => !info.unlockFrontLegAccessory,
},
{
name: 'backLegAccessoryRight',
sets: sprites.backLegAccessories[1]!,
omit: info => !info.unlockBackLegAccessory || !!info.lockBackLegAccessory,
},
];
const booleanFields: FieldDefinition<boolean>[] = [
{ name: 'customOutlines' },
{ name: 'lockEyes' },
{ name: 'lockEyeColor' },
{ name: 'lockCoatOutline', omit: info => !info.customOutlines },
{
name: 'lockBackLegAccessory', omit: info =>
empty(info.frontLegAccessory) && empty(info.backLegAccessory) &&
empty(info.frontLegAccessoryRight) && empty(info.backLegAccessoryRight)
},
{ name: 'eyeshadow' },
{ name: 'cmFlip', omit: info => info.cm === undefined || info.cm.every(not) },
{ name: 'unlockEyeWhites' },
{ name: 'freeOutlines' },
{ name: 'unlockFrontLegAccessory' },
{ name: 'unlockBackLegAccessory', omit: info => !!info.lockBackLegAccessory },
{ name: 'unlockEyelashColor' },
{ name: 'darkenLockedOutlines', omit: info => !info.freeOutlines },
];
const numberFields: FieldDefinition<number>[] = [
{ name: 'eyelashes' },
{ name: 'eyeOpennessRight' },
{ name: 'eyeOpennessLeft', omit: info => !!info.lockEyes },
{ name: 'fangs' },
{ name: 'muzzle' },
{ name: 'freckles', dontSave: true }, // TODO: remove
];
const colorFields: FieldDefinition<number>[] = [
{ name: 'coatFill' },
{ name: 'coatOutline', omit: info => !info.customOutlines || !!info.lockCoatOutline },
{ name: 'eyeColorRight' },
{ name: 'eyeColorLeft', omit: info => !!info.lockEyeColor },
{ name: 'eyeWhites', default: WHITE },
{ name: 'eyeshadowColor', omit: info => !info.eyeshadow },
{ name: 'frecklesColor', omit: info => !info.freckles, dontSave: true }, // TODO: remove
{ name: 'eyeWhitesLeft', default: WHITE, omit: info => !info.unlockEyeWhites },
{ name: 'eyelashColor' },
{ name: 'eyelashColorLeft', omit: info => !info.unlockEyelashColor },
{ name: 'magicColor', default: WHITE },
];
const omittableFields: FieldDefinition<any>[] = [
...setFields,
...booleanFields,
...numberFields,
...colorFields,
].filter(f => !!f.omit);
const VERSION_BITS = 6; // max 63
const COLORS_LENGTH_BITS = 10; // max 1024
const BOOLEAN_FIELDS_LENGTH_BITS = 4; // max 15
const NUMBER_FIELDS_LENGTH_BITS = 4; // max 15
const COLOR_FIELDS_LENGTH_BITS = 4; // max 15
const SET_FIELDS_LENGTH_BITS = 5; // max 31
const CM_LENGTH_BITS = 5; // max 31
const NUMBERS_BITS = 6; // max 63
/* istanbul ignore next */
if (DEVELOPMENT) {
(function () {
function verifyFields(obj: any, lengthBits: number, defs: FieldDefinition<any>[], verify: (field: any) => boolean) {
const missing = Object.keys(obj)
.filter(key => verify(obj[key]))
.filter(key => defs.every(d => d.name !== key));
const unnecessary = defs
.filter(({ name }) => !verify(obj[name]));
if (missing.length || unnecessary.length) {
throw new Error(`Incorrect fields (${missing} / ${unnecessary})`);
}
if (lengthBits < countBits(defs.length)) {
throw new Error(`Incorrect field length bits (${lengthBits}/${countBits(defs.length)})`);
}
}
const defaultPony = createBasePony();
verifyFields(defaultPony, SET_FIELDS_LENGTH_BITS, setFields, f => f.type !== undefined);
verifyFields(defaultPony, COLOR_FIELDS_LENGTH_BITS, colorFields, isString);
verifyFields(defaultPony, NUMBER_FIELDS_LENGTH_BITS, numberFields, isNumber);
verifyFields(defaultPony, BOOLEAN_FIELDS_LENGTH_BITS, booleanFields, isBoolean);
if (setFields.some(f => !f.sets)) {
throw new Error(`Undefined set in set field (${setFields.find(f => !f.sets)!.name})`);
}
})();
}
function trimRight<T>(items: T[]) {
const index = findLastIndex(items, x => !!x);
return (index !== (items.length - 1)) ? items.slice(0, index + 1) : items;
}
export function precompressCM<T>(cm: (T | undefined)[] | undefined, addColor: (color: T | undefined) => number): number[] {
const result: number[] = [];
if (cm) {
let length = CM_SIZE * CM_SIZE;
while (length > 0 && !cm[length - 1]) {
length--;
}
for (let i = 0; i < length; i++) {
result.push(addColor(cm[i]));
}
}
return result;
}
// lock sets
export function compressLockSet(set: boolean[] | undefined, count: number): number {
const locks = set && set.slice ? set.slice(0, count) : [];
return locks.reduce((result, l, i) => result | (l ? (1 << i) : 0), 0);
}
export function decompressLockSet(set: number, count: number, defaultValues: boolean[]): boolean[] {
const result: boolean[] = [];
for (let i = 0; i < MAX_COLORS; i++) {
result[i] = i < count ? !!(set & (1 << i)) : defaultValues[i];
}
return result;
}
// colors
export function precompressColorSet<T>(
set: (T | undefined)[] | undefined, count: number, locks: number, defaultColor: T, addColor: (color: T) => number
): number[] {
const result: number[] = [];
if (set) {
for (let i = 0; i < count; i++) {
if ((locks & (1 << i)) === 0) {
const color = set[i];
result.push(!color || color === defaultColor ? 0 : addColor(color));
}
}
}
return result;
}
export function postdecompressColorSet<T>(
colors: number[], count: number, locks: number, colorList: number[], parseColor: (color: number) => T
): T[] {
const result: T[] = [];
for (let i = 0, j = 0; i < count; i++) {
const locked = (locks & (1 << i)) !== 0;
result.push(parseColor((locked ? 0 : colorList[colors[j++] - 1]) || BLACK));
}
return result;
}
// set
const MAX_COLORS = 6;
const ALL_UNLOCKED = array(MAX_COLORS, false);
const ALL_LOCKED = array(MAX_COLORS, true);
export function precompressSet<T>(
set: SpriteSet<T> | undefined, def: SetDefinition, customOutlines: boolean, defaultColor: T, addColor: (color: T) => number
): PrecompressedSet | undefined {
if (!set)
return undefined;
const type = clamp(toInt(set.type), 0, def.sets.length - 1);
if (type === 0 && !def.preserveOnZero)
return undefined;
const patterns = at(def.sets, type);
const pattern = clamp(toInt(set.pattern), 0, patterns ? patterns.length - 1 : 0);
const sprite = att(patterns, pattern);
const colors = Math.max(getColorCount(sprite), def.minColors || 0);
/* istanbul ignore next */
if (type === 0 && pattern === 0 && colors === 0)
return undefined;
const fillLocks = compressLockSet(set.lockFills, colors);
const fills = precompressColorSet(set.fills, colors, fillLocks, defaultColor, addColor);
const outlineLocks = customOutlines ? compressLockSet(set.lockOutlines, colors) : 0;
const outlines = customOutlines ? precompressColorSet(set.outlines, colors, outlineLocks, defaultColor, addColor) : [];
return { type, pattern, colors, fillLocks, fills, outlineLocks, outlines };
}
export function postdecompressSet<T>(
set: PrecompressedSet, _def: SetDefinition, customOutlines: boolean, colorList: number[], parseColor: (color: number) => T
): SpriteSet<T> | undefined {
return {
type: set.type,
pattern: set.pattern,
lockFills: decompressLockSet(set.fillLocks, set.colors, /*def.defaultLockFills ||*/ ALL_UNLOCKED),
fills: postdecompressColorSet(set.fills, set.colors, set.fillLocks, colorList, parseColor),
lockOutlines: customOutlines ?
decompressLockSet(set.outlineLocks, set.colors, /*def.defaultLockOutlines ||*/ ALL_LOCKED) :
ALL_LOCKED,
outlines: customOutlines ? postdecompressColorSet(set.outlines, set.colors, set.outlineLocks, colorList, parseColor) : [],
};
}
// helpers
function precompressFields<TDef extends FieldDefinition<TResult>, TValue, TResult>(
data: any, defs: TDef[], defaultValue: TResult, encode: (value: TValue | undefined, def: TDef) => TResult
): TResult[] {
return trimRight(defs.map(def => {
if (def.dontSave || (def.omit && def.omit(data))) {
return defaultValue;
} else {
return encode(data[def.name], def);
}
}));
}
function postdecompressFields<TDef extends FieldDefinition<TValue>, TValue, TResult>(
result: any, defs: TDef[], values: (TValue | undefined)[], defaultValue: TValue, decode: (value: TValue, def: TDef) => TResult
) {
for (let i = 0; i < defs.length; i++) {
const def = defs[i];
const value = i >= values.length ? undefined : values[i];
result[def.name] = decode(value === undefined ? defaultValue : value, def);
}
}
// pony
type Info<T> = PonyInfoBase<T, SpriteSet<T>>;
export function precompressPony<T>(info: Info<T>, defaultColor: T, parseColor: (color: T) => number): Precompressed {
const colors: number[] = [];
const customOutlines = !!info.customOutlines;
const addColor = (color: T | undefined) => {
const c = color === undefined ? 0 : parseColor(color);
return c === 0 ? 0 : pushUniq(colors, c);
};
return {
version: VERSION,
colors,
booleanFields: precompressFields(info, booleanFields, false as boolean, x => !!x),
numberFields: precompressFields(info, numberFields, 0, toInt),
colorFields: precompressFields(info, colorFields, 0,
(x: T | undefined, def) => (x === undefined || parseColor(x) === (def.default || BLACK)) ? 0 : addColor(x)),
setFields: precompressFields(info, setFields, undefined,
(x: SpriteSet<T> | undefined, def: SetDefinition) => precompressSet(x, def, customOutlines, defaultColor, addColor)),
cm: precompressCM(info.cm, addColor),
};
}
const frecklesToPattern = [0, 1, 1, 2, 2, 2, 1];
const frecklesToColor: number[][] = [[], [1], [1, 2], [2], [1], [1, 2], [2]];
function fixVersion<T>(result: Info<T>, data: Precompressed, parseColor: (color: number) => T) {
if (data.version < 3) {
result.head = {
type: 0,
pattern: frecklesToPattern[result.freckles || 0] || 0,
fills: [result.coatFill],
outlines: [result.coatOutline],
lockFills: [true, true, true, true, true, true],
lockOutlines: [true, true, true, true, true, true],
};
frecklesToColor[result.freckles || 0].forEach(index => {
result.head!.fills![index] = result.frecklesColor || parseColor(BLACK);
result.head!.lockFills![index] = false;
});
}
}
export function createPostDecompressPony() {
return new Function('postdecompressSet', 'setFields', 'ommitableFields', 'fixVersion', [
'function identity(x) { return x; }',
'function getColor(colors, i) { return (i >= 0 && i < colors.length) ? colors[i] : 0; }',
'function getCM(cm, colors) {',
' var result = [];',
' for(var i = 0; i < cm.length; i++) { result.push(getColor(colors, cm[i] - 1) || 0); }',
' return result;',
'}',
...omittableFields.map((def, i) => `var omit_${def.name} = ommitableFields[${i}].omit;`),
'return function (data) {',
' var dataColors = data.colors;',
' var bools = data.booleanFields;',
' var numbers = data.numberFields;',
' var colors = data.colorFields;',
' var sets = data.setFields;',
' var result = {};',
...booleanFields.map((def, i) => ` result.${def.name} = bools.length > ${i} ? bools[${i}] : false;`),
...numberFields.map((def, i) => ` result.${def.name} = numbers.length > ${i} ? numbers[${i}] : 0;`),
...colorFields.map((def, i) => ` result.${def.name} = colors.length > ${i} ? ` +
`getColor(dataColors, colors[${i}] - 1) || ${def.default || BLACK} : ${def.default || BLACK};`),
' var customOutlines = !!result.customOutlines;',
...setFields.map((def, i) => ` result.${def.name} = sets.length > ${i} && sets[${i}] !== undefined ? ` +
`postdecompressSet(sets[${i}], setFields[${i}], customOutlines, data.colors, identity) : undefined;`),
` result.cm = data.cm.length ? getCM(data.cm, dataColors) : undefined;`,
...omittableFields.map(def => ` if (omit_${def.name}(result)) result.${def.name} = undefined;`),
' fixVersion(result, data, identity);',
' return result;',
'};',
].join('\n'));
}
export const fastPostdecompressPony = createPostDecompressPony()(
postdecompressSet, setFields, omittableFields, fixVersion);
export function postdecompressPony<T>(data: Precompressed, parseColor: (color: number) => T): Info<T> {
// NOTE: when updating also update createPostDecompressPony()
const result: Info<T> = {} as any;
postdecompressFields(result, booleanFields, data.booleanFields, false as boolean, identity);
postdecompressFields(result, numberFields, data.numberFields, 0 as number, identity);
postdecompressFields(result, colorFields, data.colorFields, 0 as number,
(x, def) => parseColor(data.colors[x - 1] || def.default || BLACK));
const customOutlines = !!result.customOutlines;
postdecompressFields(result, setFields, data.setFields, undefined,
(x, def) => x === undefined ? undefined : postdecompressSet(x, def, customOutlines, data.colors, parseColor));
result.cm = data.cm.length ? data.cm.map(x => parseColor(data.colors[x - 1] || TRANSPARENT)) : undefined;
omittableFields.forEach(def => {
if (def.omit && def.omit(result)) {
result[def.name] = undefined;
}
});
fixVersion(result, data, parseColor);
return result;
}
// set
const TYPE_BITS = 5; // max 31
const PATTERN_BITS = 4; // max 15
const COLORS_BITS = 3; // max 7
export function writeSet(write: WriteBits, colorBits: number, customOutlines: boolean, set: PrecompressedSet | undefined) {
write(set ? 1 : 0, 1);
if (set) {
write(set.type, TYPE_BITS);
write(set.pattern, PATTERN_BITS);
write(set.colors - 1, COLORS_BITS);
write(set.fillLocks, set.colors);
set.fills.forEach(c => write(c, colorBits));
if (customOutlines) {
write(set.outlineLocks, set.colors);
set.outlines.forEach(c => write(c, colorBits));
}
}
}
export function readSet(read: ReadBits, colorBits: number, customOutlines: boolean): PrecompressedSet | undefined {
const has = read(1);
if (has) {
const type = read(TYPE_BITS);
const pattern = read(PATTERN_BITS);
const colors = read(COLORS_BITS) + 1;
const fillLocks = read(colors);
const fills = readTimes(read, colors - countBits(fillLocks), colorBits);
const outlineLocks = customOutlines ? read(colors) : 0;
const outlines = customOutlines ? readTimes(read, colors - countBits(outlineLocks), colorBits) : [];
return { type, pattern, colors, fillLocks, fills, outlineLocks, outlines };
} else {
return undefined;
}
}
// helpers
function writeFields<T>(write: WriteBits, lengthBits: number, fields: T[], writeField: (value: T) => void) {
write(fields.length, lengthBits);
fields.forEach(writeField);
}
function readFields<T>(read: ReadBits, lengthBits: number, readField: (read: ReadBits) => T): T[] {
const length = read(lengthBits);
const result: T[] = [];
for (let i = 0; i < length; i++) {
result.push(readField(read));
}
return result;
}
// pony
export function writePony(write: WriteBits, data: Precompressed) {
const colorBits = Math.max(numberToBitCount(data.colors.length), 1);
const customOutlines = !!data.booleanFields[0];
write(data.version, VERSION_BITS);
writeFields(write, COLORS_LENGTH_BITS, data.colors, x => write(x >> 8, 24));
writeFields(write, BOOLEAN_FIELDS_LENGTH_BITS, data.booleanFields, x => write(x ? 1 : 0, 1));
writeFields(write, NUMBER_FIELDS_LENGTH_BITS, data.numberFields, x => write(x, NUMBERS_BITS));
writeFields(write, COLOR_FIELDS_LENGTH_BITS, data.colorFields, x => write(x, colorBits));
writeFields(write, SET_FIELDS_LENGTH_BITS, data.setFields, x => writeSet(write, colorBits, customOutlines, x));
writeFields(write, CM_LENGTH_BITS, data.cm, x => write(x, colorBits));
}
const readColorValue = (read: ReadBits) => ((read(24) << 8) | 0xff) >>> 0;
const readBoolean = (read: ReadBits) => !!read(1);
const readBits = (bits: number) => (read: ReadBits) => read(bits);
const readNumber = readBits(NUMBERS_BITS);
export function readPony(read: ReadBits): Precompressed {
const version = read(VERSION_BITS);
const colors = readFields(read, COLORS_LENGTH_BITS, readColorValue);
const colorBits = Math.max(numberToBitCount(colors.length), 1);
const readColor = readBits(colorBits);
const booleanFields = readFields(read, BOOLEAN_FIELDS_LENGTH_BITS, readBoolean);
const customOutlines = !!booleanFields[0];
const numberFields = readFields(read, NUMBER_FIELDS_LENGTH_BITS, readNumber);
const colorFields = readFields(read, COLOR_FIELDS_LENGTH_BITS, readColor);
const setFields = readFields(read, SET_FIELDS_LENGTH_BITS, read => readSet(read, colorBits, customOutlines));
const cm = readFields(read, CM_LENGTH_BITS, readColor);
return { version, colors, booleanFields, numberFields, colorFields, setFields, cm };
}
function writePonyToString(data: Precompressed): string {
return fromByteArray(bitWriter(write => writePony(write, data)));
}
function readPonyFromBuffer(info: Uint8Array): Precompressed {
return readPony(bitReader(info));
}
function readPonyFromString(info: string): Precompressed {
return info ? readPonyFromBuffer(toByteArray(info)) : {
version: VERSION,
colors: [],
booleanFields: [],
numberFields: [],
colorFields: [],
setFields: [],
cm: [],
};
}
// compress
export function compressPony(info: PonyInfoNumber): string {
return writePonyToString(precompressPony(info, BLACK, identity));
}
export function decompressPony(info: string | Uint8Array): PonyInfoNumber {
const data = typeof info === 'string' ? readPonyFromString(info) : readPonyFromBuffer(info);
const pony = fastPostdecompressPony(data); // postdecompressPony(data, identity);
return syncLockedPonyInfoNumber(pony);
}
// compress (string)
function parseColorFastSafe(color: string): number {
return color ? parseColorFast(color) : TRANSPARENT;
}
function colorToString(color: number): string {
return color ? colorToHexRGB(color) : '';
}
export function compressPonyString(info: PonyInfo): string {
return writePonyToString(precompressPony(info, '000000', parseColorFastSafe));
}
export function decompressPonyString(info: string, editable = false): PonyInfo {
const data = readPonyFromString(info);
const pony = postdecompressPony(data, colorToString);
const result = editable ? merge(createBasePony(), pony) : pony;
return syncLockedPonyInfo(result);
}
// decode
export function decodePonyInfo(info: string | Uint8Array, paletteManager: PaletteManager): PalettePonyInfo {
return toPaletteNumber(decompressPony(info), paletteManager);
}
+190
View File
@@ -0,0 +1,190 @@
import { Season, Holiday } from './interfaces';
export const SEASON: Season = Season.Summer;
export const HOLIDAY: Holiday = Holiday.None;
export const SECOND = 1000;
export const MINUTE = SECOND * 60;
export const HOUR = MINUTE * 60;
export const DAY = HOUR * 24;
export const WEEK = DAY * 7;
export const MONTH = DAY * 30;
export const YEAR = DAY * 365;
export const BATCH_SIZE_MAX = 10000;
export const MAX_VELOCITY = 16; // do not change
export const PONY_TYPE = 1;
export const PONY_SPEED_TROT = 4; // tiles per sec
export const PONY_SPEED_WALK = 2; // tiles per sec
export const SAYS_TIME_MIN = 5; // sec
export const SAYS_TIME_MAX = 8; // sec
export const TILE_CHANGE_RANGE = 5;
export const EXPRESSION_TIMEOUT = 7000; // ms
export const FLY_DELAY = 0.4; // sec
export const SERVER_FPS = 10;
export const AFK_TIMEOUT = 15 * MINUTE;
export const REMOVE_TIMEOUT = 15 * MINUTE;
export const REMOVE_INTERVAL = 1 * MINUTE;
export const MAP_DISCARD_TIMEOUT = 15 * MINUTE;
export const MAP_SWITCH_DELAY = 1 * SECOND;
export const MAP_SWITCHES_PER_UPDATE = 1;
export const JOINS_PER_UPDATE = 1;
export const DEFAULT_CHATLOG_OPACITY = 35;
export const MAX_CHATLOG_RANGE = 11;
export const MIN_CHATLOG_RANGE = 2;
export function isChatlogRangeUnlimited(range: number | undefined) {
return !range || range < MIN_CHATLOG_RANGE || range >= MAX_CHATLOG_RANGE;
}
export const WATER_FPS = 6;
export const WATER_HEIGHT = [0, -1, -2, -1];
export const CM_SIZE = 5;
export const MIN_SCALE = 1;
export const MAX_SCALE = 4;
export const SAY_MAX_LENGTH = 64;
export const PLAYER_NAME_MAX_LENGTH = 20;
export const PLAYER_DESC_MAX_LENGTH = 40;
export const ACCOUNT_NAME_MIN_LENGTH = 1;
export const ACCOUNT_NAME_MAX_LENGTH = 32;
export const MAX_FILTER_WORDS_LENGTH = 1000;
export const PARTY_LIMIT = 30;
export const FRIENDS_LIMIT = 100;
export const HIDE_LIMIT = 1000;
export const UNHIDE_TIMEOUT = HOUR;
export const MIN_HIDE_TIME = HOUR;
export const MAX_HIDE_TIME = 10 * DAY;
export const SWAP_TIMEOUT = 1000;
export const MAP_LOAD_SAVE_TIMEOUT = 5000;
export const HIDES_PER_PAGE = 20;
export const LATEST_CHARACTER_LIMIT = 10;
export const BASE_CHARACTER_LIMIT = 1000;
export const ADDITIONAL_CHARACTERS_SUPPORTER1 = 200;
export const ADDITIONAL_CHARACTERS_SUPPORTER2 = 300;
export const ADDITIONAL_CHARACTERS_SUPPORTER3 = 450;
export const ADDITIONAL_CHARACTERS_PAST_SUPPORTER = 100;
export const ACTIONS_LIMIT = 50;
export const COMMAND_ACTION_TIME_DELAY = 1000;
export const ENTITY_TYPE_LIMIT = 0xffff;
export const HOUSE_ENTITY_LIMIT = 150;
export const CAMERA_WIDTH_MIN = 64;
export const CAMERA_WIDTH_MAX = 0xbff; // 3071
export const CAMERA_HEIGHT_MIN = 64;
export const CAMERA_HEIGHT_MAX = 0x7ff; // 2047
export const blinkFps = 24;
export const tileWidth = 32;
export const tileHeight = 24;
export const tileElevation = 20; // 24;
export const REGION_SIZE = 8; // in tiles
export const REGION_WIDTH = REGION_SIZE * tileWidth;
export const REGION_HEIGHT = REGION_SIZE * tileHeight;
export const REGION_BORDER = 1; // in tiles
export const TILES_RESTORE_MIN_SEC = 1; // max: TILES_RESTORE_MAX_SEC - 1
export const TILES_RESTORE_MAX_SEC = 10; // max: 255
export const PONY_INFO_KEY = 0x76;
export const MIN_ADULT_AGE = 18;
export const REQUEST_DATE_OF_BIRTH = true;
export const TIMEOUTS = [
{ value: MINUTE * 5, label: '5 minutes' },
{ value: MINUTE * 10, label: '10 minutes' },
{ value: MINUTE * 30, label: '30 minutes' },
{ value: HOUR * 1, label: '1 hour' },
{ value: HOUR * 5, label: '5 hours' },
{ value: HOUR * 10, label: '10 hours' },
{ value: HOUR * 24, label: '24 hours' },
{ value: DAY * 2, label: '2 days' },
{ value: DAY * 5, label: '5 days' },
];
export const MONTH_NAMES_EN = [
'January',
'February ',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
export const OFFLINE_PONY = 'DAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA==';
export const SUPPORTER_PONY = 'CAfz9PUFLUnapSD/1wD5aFT////+hHM2QIJkJ8AQLkkADAA6jXrsBT1Iw+wBMJOqoW1C2oW1AAI=';
// patreon reward tier IDs
export const rewardLevel1 = '2255086';
export const rewardLevel2 = '2411886';
export const rewardLevel3 = '2411888';
const SUPPORTER_REWARDS_COMMON = [
`In-game supporter tag`,
`Supporter chat color`,
];
const SUPPORTER_REWARDS_MORE = [
`Access to patreon posts`,
`Early access to new and experimental features`,
];
export const SUPPORTER_REWARDS = [
[],
[
...SUPPORTER_REWARDS_COMMON,
`${ADDITIONAL_CHARACTERS_SUPPORTER1} additional slots for saving ponies`,
],
[
...SUPPORTER_REWARDS_COMMON,
...SUPPORTER_REWARDS_MORE,
`${ADDITIONAL_CHARACTERS_SUPPORTER2} additional slots for saving ponies`,
],
[
...SUPPORTER_REWARDS_COMMON,
...SUPPORTER_REWARDS_MORE,
`${ADDITIONAL_CHARACTERS_SUPPORTER3} additional slots for saving ponies`,
],
];
export const SUPPORTER_REWARDS_LIST = [
...SUPPORTER_REWARDS_COMMON,
...SUPPORTER_REWARDS_MORE,
`Additional slots for saving ponies`,
];
export const PAST_SUPPORTER_REWARDS = [
`${ADDITIONAL_CHARACTERS_PAST_SUPPORTER} additional slots for saving ponies`,
];
export const GENERAL_RULES = [
`Be kind to others`,
`Don't spam`,
`Don't use multiple accounts`,
`Don't modify the game with hacks or scripts`,
`Don't encourage behaviour violating the rules`,
`Violation of the rules may result in temporary or permanent ban`,
];
+251
View File
@@ -0,0 +1,251 @@
export const countryCodeToName: { [key: string]: string | undefined; } = {
AD: `Andorra`,
AE: `United Arab Emirates (the)`,
AF: `Afghanistan`,
AG: `Antigua and Barbuda`,
AI: `Anguilla`,
AL: `Albania`,
AM: `Armenia`,
AO: `Angola`,
AQ: `Antarctica`,
AR: `Argentina`,
AS: `American Samoa`,
AT: `Austria`,
AU: `Australia`,
AW: `Aruba`,
AX: `Åland Islands`,
AZ: `Azerbaijan`,
BA: `Bosnia and Herzegovina`,
BB: `Barbados`,
BD: `Bangladesh`,
BE: `Belgium`,
BF: `Burkina Faso`,
BG: `Bulgaria`,
BH: `Bahrain`,
BI: `Burundi`,
BJ: `Benin`,
BL: `Saint Barthélemy`,
BM: `Bermuda`,
BN: `Brunei Darussalam`,
BO: `Bolivia (Plurinational State of)`,
BQ: `Bonaire, Sint Eustatius and Saba`,
BR: `Brazil`,
BS: `Bahamas (the)`,
BT: `Bhutan`,
BV: `Bouvet Island`,
BW: `Botswana`,
BY: `Belarus`,
BZ: `Belize`,
CA: `Canada`,
CC: `Cocos (Keeling) Islands (the)`,
CD: `Congo (the Democratic Republic of the)`,
CF: `Central African Republic (the)`,
CG: `Congo (the)`,
CH: `Switzerland`,
CI: `Côte d'Ivoire`,
CK: `Cook Islands (the)`,
CL: `Chile`,
CM: `Cameroon`,
CN: `China`,
CO: `Colombia`,
CR: `Costa Rica`,
CU: `Cuba`,
CV: `Cabo Verde`,
CW: `Curaçao`,
CX: `Christmas Island`,
CY: `Cyprus`,
CZ: `Czechia`,
DE: `Germany`,
DJ: `Djibouti`,
DK: `Denmark`,
DM: `Dominica`,
DO: `Dominican Republic (the)`,
DZ: `Algeria`,
EC: `Ecuador`,
EE: `Estonia`,
EG: `Egypt`,
EH: `Western Sahara*`,
ER: `Eritrea`,
ES: `Spain`,
ET: `Ethiopia`,
FI: `Finland`,
FJ: `Fiji`,
FK: `Falkland Islands (the) [Malvinas]`,
FM: `Micronesia (Federated States of)`,
FO: `Faroe Islands (the)`,
FR: `France`,
GA: `Gabon`,
GB: `United Kingdom of Great Britain and Northern Ireland (the)`,
GD: `Grenada`,
GE: `Georgia`,
GF: `French Guiana`,
GG: `Guernsey`,
GH: `Ghana`,
GI: `Gibraltar`,
GL: `Greenland`,
GM: `Gambia (the)`,
GN: `Guinea`,
GP: `Guadeloupe`,
GQ: `Equatorial Guinea`,
GR: `Greece`,
GS: `South Georgia and the South Sandwich Islands`,
GT: `Guatemala`,
GU: `Guam`,
GW: `Guinea-Bissau`,
GY: `Guyana`,
HK: `Hong Kong`,
HM: `Heard Island and McDonald Islands`,
HN: `Honduras`,
HR: `Croatia`,
HT: `Haiti`,
HU: `Hungary`,
ID: `Indonesia`,
IE: `Ireland`,
IL: `Israel`,
IM: `Isle of Man`,
IN: `India`,
IO: `British Indian Ocean Territory (the)`,
IQ: `Iraq`,
IR: `Iran (Islamic Republic of)`,
IS: `Iceland`,
IT: `Italy`,
JE: `Jersey`,
JM: `Jamaica`,
JO: `Jordan`,
JP: `Japan`,
KE: `Kenya`,
KG: `Kyrgyzstan`,
KH: `Cambodia`,
KI: `Kiribati`,
KM: `Comoros (the)`,
KN: `Saint Kitts and Nevis`,
KP: `Korea (the Democratic People's Republic of)`,
KR: `Korea (the Republic of)`,
KW: `Kuwait`,
KY: `Cayman Islands (the)`,
KZ: `Kazakhstan`,
LA: `Lao People's Democratic Republic (the)`,
LB: `Lebanon`,
LC: `Saint Lucia`,
LI: `Liechtenstein`,
LK: `Sri Lanka`,
LR: `Liberia`,
LS: `Lesotho`,
LT: `Lithuania`,
LU: `Luxembourg`,
LV: `Latvia`,
LY: `Libya`,
MA: `Morocco`,
MC: `Monaco`,
MD: `Moldova (the Republic of)`,
ME: `Montenegro`,
MF: `Saint Martin (French part)`,
MG: `Madagascar`,
MH: `Marshall Islands (the)`,
MK: `Macedonia (the former Yugoslav Republic of)`,
ML: `Mali`,
MM: `Myanmar`,
MN: `Mongolia`,
MO: `Macao`,
MP: `Northern Mariana Islands (the)`,
MQ: `Martinique`,
MR: `Mauritania`,
MS: `Montserrat`,
MT: `Malta`,
MU: `Mauritius`,
MV: `Maldives`,
MW: `Malawi`,
MX: `Mexico`,
MY: `Malaysia`,
MZ: `Mozambique`,
NA: `Namibia`,
NC: `New Caledonia`,
NE: `Niger (the)`,
NF: `Norfolk Island`,
NG: `Nigeria`,
NI: `Nicaragua`,
NL: `Netherlands (the)`,
NO: `Norway`,
NP: `Nepal`,
NR: `Nauru`,
NU: `Niue`,
NZ: `New Zealand`,
OM: `Oman`,
PA: `Panama`,
PE: `Peru`,
PF: `French Polynesia`,
PG: `Papua New Guinea`,
PH: `Philippines (the)`,
PK: `Pakistan`,
PL: `Poland`,
PM: `Saint Pierre and Miquelon`,
PN: `Pitcairn`,
PR: `Puerto Rico`,
PS: `Palestine, State of`,
PT: `Portugal`,
PW: `Palau`,
PY: `Paraguay`,
QA: `Qatar`,
RE: `Réunion`,
RO: `Romania`,
RS: `Serbia`,
RU: `Russian Federation (the)`,
RW: `Rwanda`,
SA: `Saudi Arabia`,
SB: `Solomon Islands`,
SC: `Seychelles`,
SD: `Sudan (the)`,
SE: `Sweden`,
SG: `Singapore`,
SH: `Saint Helena, Ascension and Tristan da Cunha`,
SI: `Slovenia`,
SJ: `Svalbard and Jan Mayen`,
SK: `Slovakia`,
SL: `Sierra Leone`,
SM: `San Marino`,
SN: `Senegal`,
SO: `Somalia`,
SR: `Suriname`,
SS: `South Sudan`,
ST: `Sao Tome and Principe`,
SV: `El Salvador`,
SX: `Sint Maarten (Dutch part)`,
SY: `Syrian Arab Republic`,
SZ: `Swaziland`,
TC: `Turks and Caicos Islands (the)`,
TD: `Chad`,
TF: `French Southern Territories (the)`,
TG: `Togo`,
TH: `Thailand`,
TJ: `Tajikistan`,
TK: `Tokelau`,
TL: `Timor-Leste`,
TM: `Turkmenistan`,
TN: `Tunisia`,
TO: `Tonga`,
TR: `Turkey`,
TT: `Trinidad and Tobago`,
TV: `Tuvalu`,
TW: `Taiwan (Province of China)`,
TZ: `Tanzania, United Republic of`,
UA: `Ukraine`,
UG: `Uganda`,
UM: `United States Minor Outlying Islands (the)`,
US: `United States of America (the)`,
UY: `Uruguay`,
UZ: `Uzbekistan`,
VA: `Holy See (the)`,
VC: `Saint Vincent and the Grenadines`,
VE: `Venezuela (Bolivarian Republic of)`,
VG: `Virgin Islands (British)`,
VI: `Virgin Islands (U.S.)`,
VN: `Viet Nam`,
VU: `Vanuatu`,
WF: `Wallis and Futuna`,
WS: `Samoa`,
YE: `Yemen`,
YT: `Mayotte`,
ZA: `South Africa`,
ZM: `Zambia`,
ZW: `Zimbabwe`,
};
+49
View File
@@ -0,0 +1,49 @@
import { MessageType } from './interfaces';
export const sampleMessages: { name: string; message: string; id?: number; type?: MessageType; }[] = [];
if (DEVELOPMENT) {
sampleMessages.push(
{ name: 'Soubi', message: 'Me lo hubieras dicho al menos.', type: MessageType.Party },
{ name: 'Doggy', message: 'Mira un menor' },
{ name: 'carry *br*', message: 'menos frama vai...nunca te falei isso' },
{ name: 'Doggy', message: 'A uste le gustan menores' },
{ name: 'Doggy', message: 'Pero no soy menor de edad' },
{ name: 'springtrap girl(:3)', message: 'pelo menos desincalho' },
{ name: 'Foxy (Menina)', message: 'que vida em a metade do povo sabe menos nois ;-;' },
{ name: 'Mangle (br)', message: 'mais pelo menos meus pais ta aqui em casa', type: MessageType.Party },
{ name: 'Experiment-z30 (ESP)', message: 'Con una menos en la clase' },
{ name: '🌙 Ň✞ĦŦΜΔŘ€ ŞΔŇŞ 🌙', message: 'aceita q doi menos' },
{ name: '⭐✨柊|Lihan|柊✨⭐', message: 'quanta frescura no rabo mano, aceita que doi menos' },
{ name: '🌙 Ň✞ĦŦΜΔŘ€ ŞΔŇŞ 🌙', message: 'aceita q doi menos' },
{ name: '⭐✨柊|Lihan|柊✨⭐', message: 'quanta frescura no rabo mano, aceita que doi menos' },
{ name: 'Ilenos Pijama', message: 'Muito menos participar' },
{ name: 'luz(br)', message: 'pelo menos fan nao vai comer todos os lanches' },
{ name: '=tord=', message: 'quien saque menos' },
{ name: 'ladybug', message: 'mais ou menos .-.' },
{ name: 'springtrap girl(:3)', message: 'pelo menos desincalho' },
{ name: 'Foxy (Menina)', message: 'que vida em a metade do povo sabe menos nois ;-;' },
{ name: 'Experiment-z30 (ESP)', message: 'Con una menos en la clase' },
{ name: 'Ilenos Pijama', message: 'Muito menos participar' },
{ name: 'Mangle (br)', message: 'mais pelo menos meus pais ta aqui em casa', type: MessageType.Party },
{ name: 'luz(br)', message: 'pelo menos fan nao vai comer todos os lanches' },
{
message: '/help - show help\n/roll [[min-]max] - randomize a number\n/s - say\n/p - party chat\n/t - thinking baloon',
name: '', type: MessageType.System,
},
{ name: 'Molley', message: 'Some admin message here', type: MessageType.Admin },
{ name: 'Dolleyert', message: 'Some moderator message here', type: MessageType.Mod },
{ name: '', message: 'The server will restart soon', type: MessageType.Announcement },
{ name: 'Molley', message: '🎲 rolled 5 of 100', type: MessageType.Announcement },
{ name: 'Molley', message: 'Some thinki👻n👻g 🍎 mes<b>aaa</b>sage', type: MessageType.Thinking },
{ name: 'Molley', message: 'Some party thinking message', type: MessageType.PartyThinking },
{ name: 'Molley', message: 'Some supporter 🙂 message 1', type: MessageType.Supporter1 },
{ name: 'Molley', message: 'Some supporter 🙂 message 2', type: MessageType.Supporter2, id: 1 },
{ name: 'Molley', message: 'Some supporter 🙂 message 3', type: MessageType.Supporter3, id: 2 },
{ name: 'Molley', message: 'Some whisper message', type: MessageType.Whisper, id: 2 },
{ name: 'WWWWWWWWWWWWWWWWWWWW', message: 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW' },
{ name: 'WWWWWWWWWWWWWWWWWWWW', message: 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW' },
{ name: 'tord ⚧☿♁⚨⚩⚦⚢⚣⚤', message: 'quien saque menos ⚧☿♁⚨⚩⚦⚢⚣⚤' },
{ name: 'more symbols', message: '♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎♈♉♊♋♌♍♎♏♐♑♒♓⛎' },
);
}
@@ -0,0 +1,34 @@
import { Expression, ExpressionExtra } from '../interfaces';
import { hasFlag } from '../utils';
export const EMPTY_EXPRESSION = 0x1fffffff;
export function encodeExpression(expression: Expression | undefined): number {
if (!expression)
return EMPTY_EXPRESSION;
const { extra, rightIris, leftIris, right, left, muzzle } = expression;
// bits: 5 | 4 | 4 | 5 | 5 | 5 = 28/32
return ((extra << 23) | (rightIris << 19) | (leftIris << 15) | (right << 10) | (left << 5) | muzzle) >>> 0;
}
export function decodeExpression(value: number): Expression | undefined {
value = value >>> 0;
if (value === EMPTY_EXPRESSION)
return undefined;
const muzzle = value & 0x1f;
const left = (value >> 5) & 0x1f;
const right = (value >> 10) & 0x1f;
const leftIris = (value >> 15) & 0xf;
const rightIris = (value >> 19) & 0xf;
const extra = (value >> 23) & 0x1f;
return { muzzle, left, right, leftIris, rightIris, extra };
}
export function isCancellableExpression(expression: Expression) {
return hasFlag(expression.extra, ExpressionExtra.Zzz);
}
+152
View File
@@ -0,0 +1,152 @@
import {
BinaryWriter, BinaryReader, writeInt16, readInt16, createBinaryReader, readUint16, readLength,
readUint32, readUint8, readObject, readUint8Array
} from 'ag-sockets/dist/browser';
import { decodeString } from 'ag-sockets/dist/utf8';
import { DecodedUpdate, DecodedRegionUpdate, TileUpdate, UpdateFlags } from '../interfaces';
import { tileWidth, tileHeight, MAX_VELOCITY } from '../constants';
export function writeVelocity(writer: BinaryWriter, value: number) {
if (value >= MAX_VELOCITY || value <= -MAX_VELOCITY) {
throw new Error(`Exceeded max velocity (${value})`);
}
writeInt16(writer, (value * 0x8000) / MAX_VELOCITY);
}
export function readVelocity(reader: BinaryReader) {
return (readInt16(reader) * MAX_VELOCITY) / 0x8000;
}
export function writeCoordX(writer: BinaryWriter, value: number) {
writeInt16(writer, (value * tileWidth) | 0);
}
export function writeCoordY(writer: BinaryWriter, value: number) {
writeInt16(writer, (value * tileHeight) | 0);
}
export function readCoordX(reader: BinaryReader) {
return readInt16(reader) / tileWidth;
}
export function readCoordY(reader: BinaryReader) {
return readInt16(reader) / tileHeight;
}
export function emptyUpdate(id: number): DecodedUpdate {
return {
id,
x: undefined,
y: undefined,
vx: 0,
vy: 0,
state: undefined,
expression: undefined,
type: undefined,
options: undefined,
crc: undefined,
name: undefined,
filterName: false,
info: undefined,
action: undefined,
switchRegion: false,
playerState: undefined,
};
}
export function decodeUpdate(data: Uint8Array): DecodedRegionUpdate {
const reader = createBinaryReader(data);
const x = readUint16(reader);
const y = readUint16(reader);
const updates: DecodedUpdate[] = [];
let update: DecodedUpdate | undefined;
while (update = readOneUpdate(reader)) {
updates.push(update);
}
const removesLength = readLength(reader);
const removes: number[] = [];
for (let i = 0; i < removesLength; i++) {
removes.push(readUint32(reader));
}
const tilesLength = readLength(reader);
const tiles: TileUpdate[] = [];
for (let i = 0; i < tilesLength; i++) {
tiles.push({
x: readUint8(reader),
y: readUint8(reader),
type: readUint8(reader),
});
}
const tileData = readUint8Array(reader);
return { x, y, updates, removes, tiles, tileData };
}
export function readOneUpdate(reader: BinaryReader): DecodedUpdate | undefined {
if (reader.offset >= reader.view.byteLength)
return undefined;
const flags = readUint16(reader);
if (flags === 0) {
return undefined;
}
const id = readUint32(reader);
const update = emptyUpdate(id);
update.switchRegion = (flags & UpdateFlags.SwitchRegion) !== 0;
if ((flags & UpdateFlags.Position) !== 0) {
update.x = readCoordX(reader);
update.y = readCoordY(reader);
}
if ((flags & UpdateFlags.Velocity) !== 0) {
update.vx = readVelocity(reader);
update.vy = readVelocity(reader);
}
if ((flags & UpdateFlags.State) !== 0) {
update.state = readUint8(reader);
}
if ((flags & UpdateFlags.Expression) !== 0) {
update.expression = readUint32(reader);
}
if ((flags & UpdateFlags.Type) !== 0) {
update.type = readUint16(reader);
}
if ((flags & UpdateFlags.Options) !== 0) {
update.options = readObject(reader);
}
if ((flags & UpdateFlags.Info) !== 0) {
update.crc = readUint16(reader);
update.info = readUint8Array(reader)!;
}
if ((flags & UpdateFlags.Action) !== 0) {
update.action = readUint8(reader);
}
if ((flags & UpdateFlags.Name) !== 0) {
update.name = decodeString(readUint8Array(reader)) || undefined;
update.filterName = (flags & UpdateFlags.NameBad) !== 0;
}
if ((flags & UpdateFlags.PlayerState) !== 0) {
update.playerState = readUint8(reader);
}
return update;
}
+167
View File
@@ -0,0 +1,167 @@
import {
BinaryWriter, writeUint32, writeUint16, writeUint8, writeObject, writeUint8Array, writeLength
} from 'ag-sockets/dist/browser';
import { UpdateFlags, EntityPlayerState, Action } from '../interfaces';
import { writeBinary } from '../binaryUtils';
import { ServerEntity, IClient, ServerRegion } from '../../server/serverInterfaces';
import { isEntityShadowed } from '../../server/entityUtils';
import { getRegionTiles } from '../../server/serverRegion';
import { writeCoordX, writeVelocity, writeCoordY } from './updateDecoder';
import { getPlayerState } from '../../server/playerUtils';
import { logger } from '../../server/logger';
function getOptionsOrUndefined(entity: ServerEntity) {
return (entity.options !== undefined && Object.keys(entity.options).length > 0) ? entity.options : undefined;
}
export function writeOneUpdate(
writer: BinaryWriter, entity: ServerEntity, flags: UpdateFlags, x: number, y: number, vx: number, vy: number,
options: any, action: Action, playerState: EntityPlayerState
) {
if (DEVELOPMENT && flags === 0) {
logger.error(`Writing empty update`);
}
if ((flags & UpdateFlags.Position) !== 0) {
flags |= UpdateFlags.State;
if (vx || vy) {
flags |= UpdateFlags.Velocity;
}
}
if ((flags & UpdateFlags.Name) !== 0 && entity.nameBad === true) {
flags |= UpdateFlags.NameBad;
}
writeUint16(writer, flags);
writeUint32(writer, entity.id);
if ((flags & UpdateFlags.Position) !== 0) {
writeCoordX(writer, x);
writeCoordY(writer, y);
}
if ((flags & UpdateFlags.Velocity) !== 0) {
writeVelocity(writer, vx);
writeVelocity(writer, vy);
}
if ((flags & UpdateFlags.State) !== 0) {
writeUint8(writer, entity.state);
}
if ((flags & UpdateFlags.Expression) !== 0) {
writeUint32(writer, entity.options!.expr!);
}
if ((flags & UpdateFlags.Type) !== 0) {
writeUint16(writer, entity.type);
}
if ((flags & UpdateFlags.Options) !== 0) {
writeObject(writer, options);
}
if ((flags & UpdateFlags.Info) !== 0) {
writeUint16(writer, entity.crc!);
writeUint8Array(writer, entity.encryptedInfoSafe!);
}
if ((flags & UpdateFlags.Action) !== 0) {
writeUint8(writer, action!);
}
if ((flags & UpdateFlags.Name) !== 0) {
writeUint8Array(writer, entity.encodedName!);
}
if ((flags & UpdateFlags.PlayerState) !== 0) {
writeUint8(writer, playerState!);
}
}
export function writeOneEntity(writer: BinaryWriter, entity: ServerEntity, client: IClient) {
const { x, y, vx, vy } = entity;
// TODO: const expression = !!entity.options && !!entity.options.expr; // instead of in options
const options = getOptionsOrUndefined(entity);
const playerState = getPlayerState(client, entity);
let flags = UpdateFlags.Position | UpdateFlags.State | UpdateFlags.Type;
if (entity.encryptedInfoSafe !== undefined) {
flags |= UpdateFlags.Info;
}
if (entity.encodedName !== undefined) {
flags |= UpdateFlags.Name;
}
if (playerState !== 0) {
flags |= UpdateFlags.PlayerState;
}
if (options !== undefined) {
flags |= UpdateFlags.Options;
}
writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, Action.None, playerState);
}
export function writeUpdate(writer: BinaryWriter, region: ServerRegion) {
const { x, y, entityUpdates, entityRemoves, tileUpdates } = region;
writeUint16(writer, x);
writeUint16(writer, y);
for (const { entity, flags, x, y, vx, vy, options, action, playerState } of entityUpdates) {
writeOneUpdate(writer, entity, flags, x, y, vx, vy, options, action, playerState);
}
writeUint16(writer, 0); // end marker
writeLength(writer, entityRemoves.length);
for (const remove of entityRemoves) {
writeUint32(writer, remove);
}
writeLength(writer, tileUpdates.length);
for (const { x, y, type: tile } of tileUpdates) {
writeUint8(writer, x);
writeUint8(writer, y);
writeUint8(writer, tile);
}
writeUint8Array(writer, null); // tile data
}
export function writeRegion(writer: BinaryWriter, region: ServerRegion, client: IClient) {
const { x, y, entities } = region;
writeUint16(writer, x);
writeUint16(writer, y);
for (const entity of entities) {
if (!isEntityShadowed(entity) || entity === client.pony) {
writeOneEntity(writer, entity, client);
}
}
writeUint16(writer, 0); // end marker
writeLength(writer, 0); // removes
writeLength(writer, 0); // tile updates
writeUint8Array(writer, getRegionTiles(region)); // tile data
}
// For testing
export function encodeUpdateSimple(region: ServerRegion) {
return writeBinary(writer => writeUpdate(writer, region));
}
// For testing
export function encodeRegionSimple(region: ServerRegion, client: IClient) {
return writeBinary(writer => writeRegion(writer, region, client));
}
File diff suppressed because it is too large Load Diff
+225
View File
@@ -0,0 +1,225 @@
import { sort } from 'timsort';
import {
Entity, EntityState, BodyAnimation, Point, EntityFlags, IMap, Pony, Says, EntityPlayerState, WorldMap
} from './interfaces';
import { hasFlag, distance, pushUniq, setFlag } from './utils';
import { stand, sit, lie, fly, flyBug, swim } from '../client/ponyAnimations';
import { releasePony, isPony } from './pony';
import { toScreenX, toScreenY } from './positionUtils';
import { releasePalette } from '../graphics/paletteManager';
import { rect } from './rect';
import { addOrRemoveFromEntityList } from './worldMap';
import { PONY_TYPE } from './constants';
import { isStaticCollision } from './collision';
export function releaseEntity(entity: Entity) {
if (isPony(entity)) {
releasePony(entity);
}
if (entity.palettes !== undefined) {
for (const palette of entity.palettes) {
releasePalette(palette);
}
}
}
export function addChatBubble(map: WorldMap, entity: Entity, says: Says) {
entity.says = says;
pushUniq(map.entitiesWithChat, entity);
}
export function updateEntityVelocity(map: WorldMap, entity: Entity, vx: number, vy: number) {
const wasMoving = isMoving(entity);
entity.vx = vx;
entity.vy = vy;
const isMovingNow = isMoving(entity);
addOrRemoveFromEntityList(map.entitiesMoving, entity, wasMoving, isMovingNow);
}
export function compareEntities(a: Entity, b: Entity) {
return (toScreenY(a.y) - toScreenY(b.y))
|| (a.order - b.order)
|| (b.id - a.id)
|| (toScreenX(a.x) - toScreenX(b.x))
|| (toScreenY(a.z) - toScreenY(b.z)
);
}
export function sortEntities(entities: Entity[]) {
sort(entities, compareEntities);
}
export function closestEntity(point: Point, entities: Entity[]): Entity | undefined {
return entities.reduce((best, entity) => distance(point, entity) < distance(point, best) ? entity : best, entities[0]);
}
export function getBoopRect(entity: Entity) {
const right = hasFlag(entity.state, EntityState.FacingRight);
const sitting = isPonySitting(entity);
return rect(entity.x + (right ? 0.6 : -0.9) * (sitting ? 0.6 : 1), entity.y - 0.2, 0.3, 0.4);
}
export function isMoving(entity: Entity) {
return entity.vx !== 0 || entity.vy !== 0;
}
export function isDrawable(entity: Entity) {
return entity.type === PONY_TYPE || entity.draw !== undefined;
}
export function canLand<T>(entity: Entity, map: IMap<T>) {
return !isStaticCollision(entity, map, true);
}
export function canStand<T>(entity: Entity, map: IMap<T>) {
return !isPonyStanding(entity) && isPonyLandedOrCanLand(entity, map);
}
export function canSit<T>(entity: Entity, map: IMap<T>) {
return !isPonySitting(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity);
}
export function canLie<T>(entity: Entity, map: IMap<T>) {
return !isPonyLying(entity) && isPonyLandedOrCanLand(entity, map) && !isMoving(entity);
}
export function entityInRange(entity: Entity, player: Entity) {
return (!entity.interactRange || distance(player, entity) < entity.interactRange);
}
export function getInteractBounds(pony: Pony) {
const boundsWidth = 1;
const boundsHeight = 1;
const boundsOffset = 0.5 + (isPonySitting(pony) ? -0.3 : (isPonyLying(pony) ? -0.2 : 0));
return rect(
toScreenX(isFacingRight(pony) ? (pony.x + boundsOffset) : (pony.x - boundsOffset - boundsWidth)),
toScreenY(pony.y - boundsHeight / 2),
toScreenX(boundsWidth),
toScreenY(boundsHeight));
}
export const SIT_ON_BOUNDS_WIDTH = 1.2;
export const SIT_ON_BOUNDS_HEIGHT = 0.5;
export const SIT_ON_BOUNDS_OFFSET = 0.4;
export function getSitOnBounds(pony: Pony) {
const width = SIT_ON_BOUNDS_WIDTH;
const height = SIT_ON_BOUNDS_HEIGHT;
const offset = isFacingRight(pony) ? -SIT_ON_BOUNDS_OFFSET : (SIT_ON_BOUNDS_OFFSET - SIT_ON_BOUNDS_WIDTH);
return rect(toScreenX(pony.x + offset), toScreenY(pony.y - SIT_ON_BOUNDS_HEIGHT / 2), toScreenX(width), toScreenY(height));
}
// pony state
export function isIdleAnimation(animation: BodyAnimation) {
return animation === stand || animation === sit || animation === lie || animation === fly ||
animation === flyBug || animation === swim;
}
export function isIdle(pony: Pony) {
return !isMoving(pony) && isIdleAnimation(pony.ponyState.animation);
}
export function canBoop(pony: Pony) {
return isIdle(pony);
}
export function canBoop2(entity: Entity) {
return !isMoving(entity) && (isPonyStanding(entity) || isPonySitting(entity) || isPonyLying(entity) || isPonyFlying(entity));
}
// entity player state
export function isHidden(entity: Entity) {
return (entity.playerState & EntityPlayerState.Hidden) !== 0;
}
export function isIgnored(entity: Entity) {
return (entity.playerState & EntityPlayerState.Ignored) !== 0;
}
export function isFriend(entity: Entity) {
return (entity.playerState & EntityPlayerState.Friend) !== 0;
}
export function isInTheAir(entity: Entity) {
return isFlying(entity) && (entity.inTheAirDelay === undefined || entity.inTheAirDelay <= 0);
}
// entity state
export function isFlying(entity: Entity) {
return (entity.state & EntityState.Flying) !== 0;
}
export function isFacingRight(entity: Entity) {
return (entity.state & EntityState.FacingRight) !== 0;
}
export function hasHeadTurned(entity: Entity) {
return (entity.state & EntityState.HeadTurned) !== 0;
}
export function isHeadFacingRight(entity: Entity) {
const headTurned = hasHeadTurned(entity);
const facingRight = isFacingRight(entity);
return facingRight ? !headTurned : headTurned;
}
export function getPonyState(state: EntityState): EntityState {
return state & EntityState.PonyStateMask;
}
export function setPonyState(state: EntityState, set: EntityState) {
state = (state & ~EntityState.PonyStateMask) | set;
state = setFlag(state, EntityState.Flying, set === EntityState.PonyFlying);
return state;
}
export function isSittingState(state: EntityState) {
return getPonyState(state) === EntityState.PonySitting;
}
export function isLyingState(state: EntityState) {
return getPonyState(state) === EntityState.PonyLying;
}
export function isPonyWalking(entity: Entity) {
return getPonyState(entity.state) === EntityState.PonyWalking;
}
export function isPonyTrotting(entity: Entity) {
return getPonyState(entity.state) === EntityState.PonyTrotting;
}
export function isPonySitting(entity: Entity) {
return getPonyState(entity.state) === EntityState.PonySitting;
}
export function isPonyStanding(entity: Entity) {
return getPonyState(entity.state) === EntityState.PonyStanding;
}
export function isPonyLying(entity: Entity) {
return getPonyState(entity.state) === EntityState.PonyLying;
}
export function isPonyFlying(entity: Entity) {
return getPonyState(entity.state) === EntityState.PonyFlying;
}
export function isPonyLandedOrCanLand<T>(entity: Entity, map: IMap<T>) {
return !isPonyFlying(entity) || canLand(entity, map);
}
// entity flags
export function isDecal(entity: Entity) {
return (entity.flags & EntityFlags.Decal) !== 0;
}
export function isCritter(entity: Entity) {
return (entity.flags & EntityFlags.Critter) !== 0;
}
+12
View File
@@ -0,0 +1,12 @@
export const WEBGL_CREATION_ERROR = 'Failed to create WebGL context';
export const ACCESS_ERROR = 'Access denied';
export const ACCOUNT_ERROR = 'Invalid account';
export const NOT_FOUND_ERROR = 'Not found';
export const OFFLINE_ERROR = 'Server is offline';
export const PROTECTION_ERROR = 'DDOS protection error, reload the page to continue';
export const VERSION_ERROR = 'Invalid version';
export const BROWSER_NOT_SUPPORTED_ERROR = 'Your browser is not supported';
export const NAME_ERROR = 'Invalid name';
export const CHARACTER_SAVING_ERROR = 'Error saving character';
export const CHARACTER_LIMIT_ERROR = 'Character limit reached';
export const NOT_AUTHENTICATED_ERROR = 'Not authenticated';
+352
View File
@@ -0,0 +1,352 @@
import { escapeRegExp } from 'lodash';
import { Muzzle, Eye, Expression, Iris, ExpressionExtra, Dict } from './interfaces';
import { createPlainMap } from './utils';
const double = (items: string[]) => items.map(x => x + x);
const prefix = (items: string[], fix: string) => items.map(x => fix + x);
const suffix = (items: string[], fix: string) => items.map(x => x + fix);
export const THREE_LETTER_WORDS = [
'ace', 'act', 'ama', 'amp', 'amo', 'amu', 'amy', 'ana', 'ane', 'and', 'ant', 'any', 'ape', 'app', 'apo',
'apt', 'ava', 'ave', 'avo', 'awe', 'awn', 'awp', 'axe',
'boa', 'bob', 'bod', 'bog', 'bon', 'boo', 'bop', 'bot', 'boy', 'bub', 'bud', 'bug', 'bup', 'but', 'bun', 'buy',
'dad', 'doe', 'dog', 'dot', 'doy', 'dna', 'dub', 'dud', 'due', 'dun', 'dug', 'duo', 'dup', 'dva', 'dvd',
'eco', 'ecu', 'eme', 'emu', 'emo', 'eon', 'end', 'eng', 'eva', 'eve', 'exe', 'exp',
'gnu', 'goa', 'god', 'gog', 'gon', 'goo', 'got', 'gud', 'gut', 'gun', 'guv', 'guy',
'nnn', 'nog', 'non', 'noo', 'nop', 'not', 'nun', 'nut', 'nub',
'oca', 'omo', 'one', 'ooo', 'oot', 'ope', 'opt', 'oud', 'out', 'ova', 'owe', 'own', 'oxo', 'oxe', 'omg',
'pay', 'pnp', 'pod', 'pon', 'poo', 'pop', 'pot', 'pov', 'ppp', 'pub', 'pud', 'pug', 'pup', 'pun', 'put', 'pvp',
'qqq', 'que', 'qua',
'tnt', 'ton', 'top', 'tod', 'toe', 'tog', 'too', 'toy', 'tub', 'tug', 'tun', 'twa', 'two',
'uuu', 'una', 'und', 'uno', 'ump', 'upo', 'uva',
'voe', 'voy', 'vpn', 'vug', 'vvv',
'yay', 'yob', 'yod', 'yon', 'you', 'yup',
];
export const TWO_LETTER_WORDS = [
'ox', 'ex', 'by', 'my', 'up', 'of', 'if', 'me', 'ow', 'am', 'we', 'uh', 'um', 'be', 'em', 'bi', 'oh',
'go', 'eh', 'ah', 'ye', 'ya', 'he', 'hi', 'ho', 'ha', 'yo', 'us', 'on', 'id', 'an', 'do', 'no',
'as', 'at', 'it', 'is', 'or', 'so', 'to', 'pc',
];
const threeLetterWords = new RegExp(`^(${THREE_LETTER_WORDS.join('|')})$`);
const twoLetterWords = new RegExp(`^(${TWO_LETTER_WORDS.join('|')})$`);
// vertical :)
const smilesRight = [')', ']', '}', '>'];
const smilesLeft = ['(', '[', '{', '<', 'C', 'c'];
const flatBoth = ['|', 'i', 'l'];
const concernedBoth = ['/', '\\', 's', 'S', '?'];
const muzzlesBoth = [
[Muzzle.Scrunch, 't', 'T', 'I'],
[Muzzle.Blep, 'P', 'p', 'd'],
[Muzzle.FlatBlep, 'b'],
[Muzzle.Flat, ...flatBoth],
[Muzzle.Concerned, ...concernedBoth],
[Muzzle.ConcernedOpen, '0', 'v'],
[Muzzle.ConcernedOpen2, 'O'],
[Muzzle.Oh, 'o'],
[Muzzle.Kiss, '*', 'x', 'X'],
[Muzzle.NeutralPant, 'L'],
[Muzzle.SmilePant, 'Q'],
[Muzzle.FrownOpen, 'V'],
[Muzzle.NeutralOpen2, 'u', 'n'],
[Muzzle.NeutralOpen3, 'U'],
[Muzzle.NeutralTeeth, ...double(flatBoth)],
[Muzzle.ConcernedTeeth, ...double(concernedBoth)],
];
export const muzzlesRight = createMap<Muzzle>([
...muzzlesBoth,
[Muzzle.Smile, '3', ...smilesRight],
[Muzzle.Frown, ...smilesLeft],
[Muzzle.SmileOpen, 'D'],
[Muzzle.SmileOpen2, 'DD'],
[Muzzle.SmileOpen3, 'DDD'],
[Muzzle.SmileTeeth, ...double(smilesRight)],
[Muzzle.FrownTeeth, ...double(smilesLeft)],
]);
export const muzzlesLeft = createMap<Muzzle>([
...muzzlesBoth,
[Muzzle.Smile, ...smilesLeft],
[Muzzle.Frown, ...smilesRight],
[Muzzle.ConcernedOpen2, 'D'],
[Muzzle.ConcernedOpen3, 'DD', 'DDD'],
[Muzzle.SmileTeeth, ...double(smilesLeft)],
[Muzzle.FrownTeeth, ...double(smilesRight)],
]);
const neutralEyes = [';', ':', '=', '%', '8'];
const verticalEyesBoth = [
[Eye.Neutral, ...neutralEyes],
[Eye.X, 'X', 'x'],
[Eye.Neutral3, 'B'],
[Eye.Lines, '|'],
];
export const verticalEyesRight = createMap<Eye>([
...verticalEyesBoth,
[Eye.Angry, ...prefix(neutralEyes, '>')],
[Eye.Angry2, '>B'],
[Eye.Sad, ...prefix(neutralEyes, '<')],
[Eye.Sad2, '<B'],
[Eye.Frown, ...prefix(neutralEyes, '|')],
[Eye.Frown2, '|B'],
]);
export const verticalEyesLeft = createMap<Eye>([
...verticalEyesBoth,
[Eye.Angry, ...suffix(neutralEyes, '<')],
[Eye.Sad, ...suffix(neutralEyes, '>')],
[Eye.Frown, ...suffix(neutralEyes, '|')],
]);
// horizontal -_-
export const horizontalMuzzles = createMap<Muzzle>([
[Muzzle.Smile, 'c', 'C', 'v', 'V', 'u', 'U', 'w', 'W', '👃'],
[Muzzle.SmilePant, 'Q', 'P'],
[Muzzle.Frown, 'n', 'm', '^'],
[Muzzle.Neutral, '-', '//'],
[Muzzle.NeutralPant, 'q', 'p'],
[Muzzle.Flat, '_'],
[Muzzle.Kiss, '.', ',', '*', 'x', 'X', '3'],
[Muzzle.Concerned, '~'],
[Muzzle.ConcernedOpen, 'o'],
[Muzzle.ConcernedOpen2, 'A', 'O', '0'],
]);
const horizontalEyes = [
[Eye.Neutral, `'`, '.', '0', '°', 'o', 'O', 'e', 'g', '9', '6', 'd', 'b'],
[Eye.Neutral4, '='],
[Eye.Closed, '-', 'v', 'V', 'u', 'U', 'y', 'Y'],
[Eye.ClosedHappy, 'n'],
[Eye.ClosedHappy2, '^'],
[Eye.Sad, 'q', 'Q', 'p', 'P', ';', ':', ','],
[Eye.Peaceful, 't', 'T'],
[Eye.Frown, 'ô', 'Ô', 'õ', 'Õ', 'ō', 'Ō', 'ŏ', 'Ŏ'],
[Eye.Frown2, 'a'],
];
export const horizontalEyesLeft = createMap<Eye>([
...horizontalEyes,
[Eye.Neutral2, '>'],
[Eye.X, '<'],
[Eye.Sad, 'ò', 'Ò'],
[Eye.Angry, 'ó', 'Ó'],
]);
export const horizontalEyesRight = createMap<Eye>([
...horizontalEyes,
[Eye.Neutral2, '<'],
[Eye.X, '>'],
[Eye.Sad, 'ó', 'Ó'],
[Eye.Angry, 'ò', 'Ò'],
]);
const horizontalIrises = createMap<Iris>([
[Iris.Up, '9'],
[Iris.UpLeft, 'e'],
[Iris.UpRight, 'g'],
[Iris.Right, '<', 'd'],
[Iris.Left, '>', 'b'],
]);
const muzzleToEye: Eye[] = [];
muzzleToEye[Muzzle.Frown] = Eye.Sad;
muzzleToEye[Muzzle.FrownOpen] = Eye.Sad;
muzzleToEye[Muzzle.ConcernedOpen2] = Eye.Sad;
muzzleToEye[Muzzle.ConcernedOpen3] = Eye.Sad;
const neutralToSmile: Muzzle[] = [];
neutralToSmile[Muzzle.ConcernedOpen] = Muzzle.SmileOpen2;
neutralToSmile[Muzzle.ConcernedOpen2] = Muzzle.SmileOpen3;
function any(obj: object) {
return `(${Object.keys(obj).map(escapeRegExp).join('|')})`;
}
const bigEyes = /[O0ÒÓÔÕŌŎQ]/;
const cryingEye = /[;pqPQTyY]/;
const tears = "(['`,]?)";
const tearsRegex = /['`,]/;
const verticalRightRegex = new RegExp(`^${any(verticalEyesRight)}${tears}-?${any(muzzlesRight)}$`);
const verticalLeftRegex = new RegExp(`^${any(muzzlesLeft)}-?${tears}${any(verticalEyesLeft)}$`);
const horizontalRegex = new RegExp(`^${any(horizontalEyesRight)}(//)?${any(horizontalMuzzles)}(//)?${any(horizontalEyesLeft)}$`);
function matchVertical(
text: string, regex: RegExp, flip: boolean, muzzleMap: Dict<Muzzle>, eyesMap: Dict<Eye>
): Expression | undefined {
if (/^([|]{2,}|BS|8x|x8|x-?x|\d+)$/i.test(text))
return undefined;
const match = regex.exec(text);
if (!match)
return undefined;
const eyesStr = flip ? match[3] : match[1];
const muzzleStr = flip ? match[1] : match[3];
const muzzle = muzzleMap[muzzleStr];
const veye = eyesMap[eyesStr];
const eye = veye === Eye.Neutral && !/[OV]/.test(muzzleStr) ? (muzzleToEye[muzzle] || veye) : veye;
const blink = /;/.test(eyesStr);
const tear = blink && muzzleToEye[muzzle] === Eye.Sad;
const left = tear ? (/[<>]/.test(eyesStr) ? eye : Eye.Sad2) : (blink && flip ? Eye.Closed : eye);
const right = tear ? (/[<>]/.test(eyesStr) ? eye : Eye.Sad2) : (blink && !flip ? Eye.Closed : eye);
const shocked = /8/.test(eyesStr);
const rightIris = shocked ? Iris.Shocked : Iris.Forward;
const leftIris = shocked ? Iris.Shocked : (/%/.test(eyesStr) ? Iris.Up : Iris.Forward);
const extra = (tearsRegex.test(match[2]) || tear) ? ExpressionExtra.Tears : ExpressionExtra.None;
return { right, left, muzzle, rightIris, leftIris, extra };
}
function matchHorizontal(text: string): Expression | undefined {
if (/\.\.|--|vv|uu|qq|pp|nn|^\d+$/i.test(text)) {
return undefined;
}
if (/[a-zA-Z][a-z][a-z]|[A-Z]{3}/.test(text)) {
const clear = text.replace(/[^a-z]/ig, '').toLowerCase();
if (clear.length === 3 && threeLetterWords.test(clear)) {
return undefined;
}
}
if (/[a-z][a-z][.,*-]/i.test(text)) {
const clear = text.replace(/[^a-z]/ig, '').toLowerCase();
if (clear.length === 2 && twoLetterWords.test(clear)) {
return undefined;
}
}
const match = horizontalRegex.exec(text);
if (!match) {
return undefined;
}
const [, rightStr, rightBlush, muzzleStr, leftBlush, leftStr] = match;
if ((rightBlush || leftBlush) && rightBlush !== leftBlush) {
return undefined;
}
const leftEye = horizontalEyesLeft[leftStr];
const rightEye = horizontalEyesRight[rightStr];
const muzzle = horizontalMuzzles[muzzleStr];
const same = rightStr === leftStr;
const lookingToSide = same && /[<>]/.test(rightStr);
const shocked = bigEyes.test(leftStr) && bigEyes.test(rightStr) && rightStr !== '0' && leftStr !== '0';
const lookingDown = (same && rightStr === '6') || (rightStr === 'b' && leftStr === 'd');
const unamused = !lookingDown && same && rightStr === '-' && /[.,_]/.test(muzzleStr);
const left = (lookingToSide || (leftStr === 'o' && bigEyes.test(rightStr))) ? Eye.Neutral2 : leftEye;
const right = (lookingToSide || (rightStr === 'o' && bigEyes.test(leftStr))) ? Eye.Neutral2 : rightEye;
const blush = /[/][/]/.test(muzzleStr) || (rightBlush && rightBlush === leftBlush);
const cry = cryingEye.test(leftStr) || cryingEye.test(rightStr);
return {
left: unamused ? Eye.Frown2 : left,
right: unamused ? Eye.Frown2 : right,
muzzle: same && (leftEye === Eye.ClosedHappy || leftEye === Eye.ClosedHappy2) ? (neutralToSmile[muzzle] || muzzle) : muzzle,
rightIris: lookingDown ? Iris.Down : (shocked ? Iris.Shocked : (horizontalIrises[rightStr] || Iris.Forward)),
leftIris: lookingDown ? Iris.Down : (shocked ? Iris.Shocked : (horizontalIrises[leftStr] || Iris.Forward)),
extra: (blush ? ExpressionExtra.Blush : ExpressionExtra.None) | (cry ? ExpressionExtra.Cry : ExpressionExtra.None),
};
}
export function expression(
right: Eye, left: Eye, muzzle: Muzzle, rightIris = Iris.Forward, leftIris = Iris.Forward, extra = ExpressionExtra.None
): Expression {
return { right, left, muzzle, rightIris, leftIris, extra };
}
const constants = createPlainMap<() => Expression | undefined>({
'^^': () => expression(Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.Smile),
'))': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile),
'((': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Frown),
'>>': () => expression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Left, Iris.Left),
'<<': () => expression(Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Right),
'🙂': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile),
'😵': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Up, Iris.Forward),
'😐': () => expression(Eye.Neutral, Eye.Neutral, Muzzle.Flat),
'😑': () => expression(Eye.Lines, Eye.Lines, Muzzle.Flat),
'😆': () => expression(Eye.X, Eye.X, Muzzle.SmileOpen),
'😟': () => expression(Eye.Sad, Eye.Sad, Muzzle.Neutral),
'😠': () => expression(Eye.Angry, Eye.Angry, Muzzle.Smile),
'🤔': () => expression(Eye.Neutral, Eye.Frown2, Muzzle.Kiss),
'😈': () => expression(Eye.Angry, Eye.Angry, Muzzle.Smile, Iris.Up, Iris.Forward),
'👿': () => expression(Eye.Angry, Eye.Angry, Muzzle.SmileTeeth),
});
function matchOther(text: string): Expression | undefined {
if (/^A{5,}\.*$/.test(text)) {
return expression(Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3, Iris.Shocked, Iris.Shocked);
} else if (/^a{5,}\.*$/.test(text)) {
return expression(Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3);
} else if (/^z{3,}\.*$/i.test(text)) {
return expression(Eye.Closed, Eye.Closed, Muzzle.Neutral);
} else {
return constants[text] && constants[text]();
}
}
export function matchExpression(text: string): Expression | undefined {
if (/тот/ui.test(text)) {
return undefined;
}
text = replaceRussian(text)
.replace(/D{4,}/, 'DDD')
.replace(/\\/g, '/')
.replace(/\/{3,}/g, '//');
return matchVertical(text, verticalRightRegex, false, muzzlesRight, verticalEyesRight)
|| matchVertical(text, verticalLeftRegex, true, muzzlesLeft, verticalEyesLeft)
|| matchHorizontal(text)
|| matchOther(text);
}
export function parseExpression(text: string): Expression | undefined {
const emoteMatch = /(?:^| )(\S+)\s*$/.exec(text);
const emote = emoteMatch && emoteMatch[1].trim();
return emote ? matchExpression(emote) : undefined;
}
function createMap<T>(values: any[][]): Dict<T> {
return values.reduce((obj: Dict<T>, [exp, ...values]) => (values.forEach(v => obj[v] = exp), obj), Object.create(null));
}
const charMap = createPlainMap<string>({
'З': '3', 'з': '3', 'Э': '3', 'э': '3',
'А': 'A', 'а': 'a', 'Д': 'A', 'д': 'A',
'В': 'B', 'в': 'B',
'Г': 'L',
'М': 'M', 'м': 'M',
'О': 'O', 'о': 'o',
'П': 'n', 'п': 'n',
'Р': 'P', 'р': 'p',
'С': 'C', 'с': 'c',
'Т': 'T', 'т': 'T',
'Х': 'X', 'х': 'x',
'Ш': 'W', 'ш': 'w',
'Ь': 'b', 'ь': 'b',
'е': 'e',
'у': 'y', 'У': 'Y',
});
const charRegex = new RegExp(`[${Object.keys(charMap).join('')}]`, 'g');
function mapChar(x: string) {
return charMap[x];
}
function replaceRussian(text: string): string {
return text.replace(charRegex, mapChar);
}
+295
View File
@@ -0,0 +1,295 @@
import { Eye, Muzzle, Iris, ExpressionExtra } from './interfaces';
import { THREE_LETTER_WORDS, TWO_LETTER_WORDS } from './expressionUtils';
type Result = undefined
| [Eye, Eye, Muzzle]
| [Eye, Eye, Muzzle, Iris, Iris]
| [Eye, Eye, Muzzle, Iris, Iris, ExpressionExtra];
export const expressions: [string, Result][] = [
// invalid
['', undefined],
['a', undefined],
['123', undefined],
[':::', undefined],
['XDK', undefined],
['fooXD', undefined],
[':) hey', undefined],
['тот', undefined],
// in text
[' :) ', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['hi :)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
// horizontal (right)
[':-)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
[':)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['=)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
[':]', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
[':>', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
[':}', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
[':3', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['rus :з', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['rus :э', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
[':(', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
[':[', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
[':C', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
[':c', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
['rus :С', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
['rus :с', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
[':<', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
[':{', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
[':I', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
[':t', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
[':T', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
['rus :Т', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
['rus :т', [Eye.Neutral, Eye.Neutral, Muzzle.Scrunch]],
[':P', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
[':p', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
[':d', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
[':b', [Eye.Neutral, Eye.Neutral, Muzzle.FlatBlep]],
['rus :Р', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
['rus :р', [Eye.Neutral, Eye.Neutral, Muzzle.Blep]],
[':D', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen]],
[':DD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen2]],
[':DDD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen3]],
[':DDDDD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen3]],
[':O', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen2]],
[':0', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen]],
[':o', [Eye.Neutral, Eye.Neutral, Muzzle.Oh]],
[':|', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
[':l', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
[':i', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
[':v', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen]],
[':V', [Eye.Neutral, Eye.Neutral, Muzzle.FrownOpen]],
[':u', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen2]],
[':n', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen2]],
[':U', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen3]],
[':*', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
[':x', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
[':X', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
[':/', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
[':\\', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
[':S', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
[':s', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
[':?', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
['>:(', [Eye.Angry, Eye.Angry, Muzzle.Frown]],
['>:<', [Eye.Angry, Eye.Angry, Muzzle.Frown]],
['<:>', [Eye.Sad, Eye.Sad, Muzzle.Smile]],
['XD', [Eye.X, Eye.X, Muzzle.SmileOpen]],
['xD', [Eye.X, Eye.X, Muzzle.SmileOpen]],
[':L', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralPant]],
[':Q', [Eye.Neutral, Eye.Neutral, Muzzle.SmilePant]],
['B)', [Eye.Neutral3, Eye.Neutral3, Muzzle.Smile]],
['8)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Shocked, Iris.Shocked]],
['>8)', [Eye.Angry, Eye.Angry, Muzzle.Smile, Iris.Shocked, Iris.Shocked]],
['<:)', [Eye.Sad, Eye.Sad, Muzzle.Smile]],
['>B)', [Eye.Angry2, Eye.Angry2, Muzzle.Smile]],
['<B)', [Eye.Sad2, Eye.Sad2, Muzzle.Smile]],
['|:)', [Eye.Frown, Eye.Frown, Muzzle.Smile]],
['|B)', [Eye.Frown2, Eye.Frown2, Muzzle.Smile]],
['|)', [Eye.Lines, Eye.Lines, Muzzle.Smile]],
[':))', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]],
[':]]', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]],
[':||', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralTeeth]],
[':((', [Eye.Neutral, Eye.Neutral, Muzzle.FrownTeeth]],
[':[[', [Eye.Neutral, Eye.Neutral, Muzzle.FrownTeeth]],
['://', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedTeeth]],
[':SS', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedTeeth]],
[';)', [Eye.Closed, Eye.Neutral, Muzzle.Smile]],
[';(', [Eye.Sad2, Eye.Sad2, Muzzle.Frown, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
['>;(', [Eye.Angry, Eye.Angry, Muzzle.Frown, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
['%)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Up]],
[`c':`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
[`:')`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
[`:'(`, [Eye.Sad, Eye.Sad, Muzzle.Frown, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
[`=,)`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
['=`)', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Tears]],
// TODO: :@ :y :'9
// horizontal (left)
['(-:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['|:', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
['(:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['[:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['c:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['C:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['rus с:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['rus С:', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['):', [Eye.Sad, Eye.Sad, Muzzle.Frown]],
['D:', [Eye.Sad, Eye.Sad, Muzzle.ConcernedOpen2]],
['DD:', [Eye.Sad, Eye.Sad, Muzzle.ConcernedOpen3]],
['DDDDD:', [Eye.Sad, Eye.Sad, Muzzle.ConcernedOpen3]],
['D:<', [Eye.Angry, Eye.Angry, Muzzle.ConcernedOpen2]],
['D8<', [Eye.Angry, Eye.Angry, Muzzle.ConcernedOpen2, Iris.Shocked, Iris.Shocked]],
['):<', [Eye.Angry, Eye.Angry, Muzzle.Frown]],
['v:', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen]],
['/:', [Eye.Neutral, Eye.Neutral, Muzzle.Concerned]],
['(:>', [Eye.Sad, Eye.Sad, Muzzle.Smile]],
['(:|', [Eye.Frown, Eye.Frown, Muzzle.Smile]],
['(|', [Eye.Lines, Eye.Lines, Muzzle.Smile]],
['((:', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]],
['[[:', [Eye.Neutral, Eye.Neutral, Muzzle.SmileTeeth]],
['||:', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralTeeth]],
[')):', [Eye.Neutral, Eye.Neutral, Muzzle.FrownTeeth]],
['(;', [Eye.Neutral, Eye.Closed, Muzzle.Smile]],
// horizontal (invalid)
['||', undefined],
['|||', undefined],
['>||', undefined],
['>xD', undefined],
['(X<', undefined],
['x-x', undefined],
// vertical
['-_-', [Eye.Frown2, Eye.Frown2, Muzzle.Flat]],
['-.-', [Eye.Frown2, Eye.Frown2, Muzzle.Kiss]],
['-,-', [Eye.Frown2, Eye.Frown2, Muzzle.Kiss]],
['^_^', [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.Flat]],
['-_^', [Eye.Closed, Eye.ClosedHappy2, Muzzle.Flat]],
['o_O', [Eye.Neutral2, Eye.Neutral, Muzzle.Flat]],
['0_o', [Eye.Neutral, Eye.Neutral2, Muzzle.Flat]],
['o_o', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
['o,o', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
['ono', [Eye.Neutral, Eye.Neutral, Muzzle.Frown]],
['O_O', [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
['OoO', [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen, Iris.Shocked, Iris.Shocked]],
['0_0', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
['°_°', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
['0.0', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss]],
['._.', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
[',_,', [Eye.Sad, Eye.Sad, Muzzle.Flat]],
['v_V', [Eye.Closed, Eye.Closed, Muzzle.Flat]],
['u_U', [Eye.Closed, Eye.Closed, Muzzle.Flat]],
['n_n', [Eye.ClosedHappy, Eye.ClosedHappy, Muzzle.Flat]],
['>_<', [Eye.X, Eye.X, Muzzle.Flat, Iris.Left, Iris.Right]],
['>c<', [Eye.X, Eye.X, Muzzle.Smile, Iris.Left, Iris.Right]],
[`'c'`, [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['-C-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
['rus -с-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
['-v-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
['-V-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
['-U-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
['-u-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
['-w-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
['-W-', [Eye.Closed, Eye.Closed, Muzzle.Smile]],
[`-👃-`, [Eye.Closed, Eye.Closed, Muzzle.Smile]],
[`'_'`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
[`-*-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
[`-x-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
[`-X-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
[`>x<`, [Eye.X, Eye.X, Muzzle.Kiss, Iris.Left, Iris.Right]],
[`-o-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen]],
[`-O-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen2]],
[`-0-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen2]],
[`^o^`, [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.SmileOpen2]],
[`^O^`, [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.SmileOpen3]],
[`-n-`, [Eye.Closed, Eye.Closed, Muzzle.Frown]],
[`-m-`, [Eye.Closed, Eye.Closed, Muzzle.Frown]],
[`-^-`, [Eye.Closed, Eye.Closed, Muzzle.Frown]],
[`-~-`, [Eye.Closed, Eye.Closed, Muzzle.Concerned]],
[`-3-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
[`rus -з-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
[`rus -э-`, [Eye.Closed, Eye.Closed, Muzzle.Kiss]],
[`-q-`, [Eye.Closed, Eye.Closed, Muzzle.NeutralPant]],
[`-p-`, [Eye.Closed, Eye.Closed, Muzzle.NeutralPant]],
[`-P-`, [Eye.Closed, Eye.Closed, Muzzle.SmilePant]],
[`-Q-`, [Eye.Closed, Eye.Closed, Muzzle.SmilePant]],
[`-A-`, [Eye.Closed, Eye.Closed, Muzzle.ConcernedOpen2]],
[`q-q`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`p-p`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`p-q`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`rus р-р`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`;-;`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`:-:`, [Eye.Sad, Eye.Sad, Muzzle.Neutral]],
[`P-P`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`rus Р-Р`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`t-t`, [Eye.Peaceful, Eye.Peaceful, Muzzle.Neutral]],
[`Т_Т`, [Eye.Peaceful, Eye.Peaceful, Muzzle.Flat, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`rus Т_т`, [Eye.Peaceful, Eye.Peaceful, Muzzle.Flat, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`Q-Q`, [Eye.Sad, Eye.Sad, Muzzle.Neutral, Iris.Shocked, Iris.Shocked, ExpressionExtra.Cry]],
[`y-y`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`Y-Y`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`у-у`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`У-У`, [Eye.Closed, Eye.Closed, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Cry]],
[`ò_ó`, [Eye.Angry, Eye.Angry, Muzzle.Flat]],
[`ó_ò`, [Eye.Sad, Eye.Sad, Muzzle.Flat]],
[`ô_ô`, [Eye.Frown, Eye.Frown, Muzzle.Flat]],
[`õ_õ`, [Eye.Frown, Eye.Frown, Muzzle.Flat]],
[`ō_ō`, [Eye.Frown, Eye.Frown, Muzzle.Flat]],
[`ŏ_ŏ`, [Eye.Frown, Eye.Frown, Muzzle.Flat]],
[`Ò_Ó`, [Eye.Angry, Eye.Angry, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
[`Ô_Ô`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
[`Õ_Õ`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
[`Ō_Ō`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
[`Ŏ_Ŏ`, [Eye.Frown, Eye.Frown, Muzzle.Flat, Iris.Shocked, Iris.Shocked]],
[`=_=`, [Eye.Neutral4, Eye.Neutral4, Muzzle.Flat]],
[`a_a`, [Eye.Frown2, Eye.Frown2, Muzzle.Flat]],
[`e_e`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.UpLeft, Iris.UpLeft]],
[`е_е`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.UpLeft, Iris.UpLeft]],
[`g_g`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.UpRight, Iris.UpRight]],
[`9_9`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Up, Iris.Up]],
[`6_9`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Forward, Iris.Up]],
['>_>', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Left, Iris.Left]],
['<_<', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Right]],
['<_>', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Left]],
['d_d', [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Right, Iris.Right]],
['b_b', [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Left, Iris.Left]],
['twO', [Eye.Peaceful, Eye.Neutral, Muzzle.Smile]],
['o//o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
['o/////o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
['o\\\\o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
['o\\\\\\o', [Eye.Neutral, Eye.Neutral, Muzzle.Neutral, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
['>//<', [Eye.X, Eye.X, Muzzle.Neutral, Iris.Left, Iris.Right, ExpressionExtra.Blush]],
['-//v//-', [Eye.Closed, Eye.Closed, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
['-///v///-', [Eye.Closed, Eye.Closed, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush]],
[';//v//;', [Eye.Sad, Eye.Sad, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush | ExpressionExtra.Cry]],
[`6_6`, [Eye.Neutral, Eye.Neutral, Muzzle.Flat, Iris.Down, Iris.Down]],
['6.6', [Eye.Neutral, Eye.Neutral, Muzzle.Kiss, Iris.Down, Iris.Down]],
['bcd', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Down, Iris.Down]],
// TODO: o-o' o-o' ~_~ @_@ o=o oyo *_* (amazed) -_-/ -_-\ D_D
// vertical (short)
['^^', [Eye.ClosedHappy2, Eye.ClosedHappy2, Muzzle.Smile]],
['))', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['((', [Eye.Neutral, Eye.Neutral, Muzzle.Frown]],
['<<', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Right, Iris.Right]],
['>>', [Eye.Neutral2, Eye.Neutral2, Muzzle.Flat, Iris.Left, Iris.Left]],
// vertical (invalid)
...[
'---', '...', '000', 'QQQ', 'One', 'Up.', 'UP.',
...THREE_LETTER_WORDS,
...THREE_LETTER_WORDS.map(x => x.toUpperCase()),
...TWO_LETTER_WORDS.map(x => x + '.'),
...TWO_LETTER_WORDS.map(x => x + ','),
...TWO_LETTER_WORDS.map(x => x + '-'),
...TWO_LETTER_WORDS.map(x => x + '*'),
].map(x => [x, undefined] as [string, any]),
['BS', undefined],
['x8', undefined],
['8x', undefined],
['xx', undefined],
['-//c-', undefined],
['-c//-', undefined],
['030', undefined],
['80', undefined],
// other
[`aaaaa`, [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3]],
[`AAAAAA`, [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3, Iris.Shocked, Iris.Shocked]],
[`aaaaa...`, [Eye.Neutral, Eye.Neutral, Muzzle.ConcernedOpen3]],
[`zzz`, [Eye.Closed, Eye.Closed, Muzzle.Neutral]],
[`ZZZZZ`, [Eye.Closed, Eye.Closed, Muzzle.Neutral]],
[`zzz...`, [Eye.Closed, Eye.Closed, Muzzle.Neutral]],
// emoji
['🙂', [Eye.Neutral, Eye.Neutral, Muzzle.Smile]],
['😵', [Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Up, Iris.Forward]],
['😐', [Eye.Neutral, Eye.Neutral, Muzzle.Flat]],
['😑', [Eye.Lines, Eye.Lines, Muzzle.Flat]],
['😆', [Eye.X, Eye.X, Muzzle.SmileOpen]],
['😟', [Eye.Sad, Eye.Sad, Muzzle.Neutral]],
['😠', [Eye.Angry, Eye.Angry, Muzzle.Smile]],
['🤔', [Eye.Neutral, Eye.Frown2, Muzzle.Kiss]],
['😈', [Eye.Angry, Eye.Angry, Muzzle.Smile, Iris.Up, Iris.Forward]],
['👿', [Eye.Angry, Eye.Angry, Muzzle.SmileTeeth]],
// unsafe faces
[':L', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralPant]],
[':Q', [Eye.Neutral, Eye.Neutral, Muzzle.SmilePant]],
// safe replacements
[':u', [Eye.Neutral, Eye.Neutral, Muzzle.NeutralOpen2]],
[':DD', [Eye.Neutral, Eye.Neutral, Muzzle.SmileOpen2]],
];
+164
View File
@@ -0,0 +1,164 @@
import { fromPairs } from 'lodash';
import { matchRomaji, replaceRomaji } from '../client/clientUtils';
import { flatten } from './utils';
const MAX_REPEATS = 16; // needs to be even for emoji
export const ipRegexText = '(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})';
export const ipExceptionRegex = /\d\.\d\.\d\.\d/ui;
export const urlExceptionRegex = /^(battle|paint|f(im|an)fiction)\.net$/ui;
export const urlRegexTexts = [
'https?:?//\\S+',
'\\bwww\\.[^. ]\\S+',
'\\S+[^. ]\\. *(c[o0]m|net)\\b',
'\\S+[^. ] *\\.(c[o0]m|net)\\b',
'(^| )[a-z][a-z0-9]{2,}[.,][a-z]{2,3}(/[a-z0-9_?=+-]+)+\\b',
];
export function trimRepeatedLetters(test: string): string {
if (test.length > MAX_REPEATS && (/^.?(.)\1+$/u.test(test) || /^.?(..)\1+$/u.test(test))) {
return test.substr(0, MAX_REPEATS) + '…';
} else {
return test;
}
}
function createCharacterMap(data: string[][]): { [key: string]: string; } {
const mappings = data.map(([to, from]) => from.split(/ /g).map(x => [x, to]));
return fromPairs(flatten(mappings));
}
const characters = createCharacterMap([
[`'`, 'Ъ ъ Ь ь'],
['a', 'á ă ắ ặ ằ ẳ ẵ ǎ â ấ ậ ầ ẩ ẫ ä ǟ ȧ ǡ ạ ȁ à ả ȃ ā ą ᶏ ẚ å ǻ ḁ ⱥ ã ɐ ₐ А а @ α'],
['A', 'Á Ă Ắ Ặ Ằ Ẳ Ẵ Ǎ Â Ấ Ậ Ầ Ẩ Ẫ Ä Ǟ Ȧ Ǡ Ạ Ȁ À Ả Ȃ Ā Ą Å Ǻ Ḁ Ⱥ Ã Ɐ ᴀ'],
['aa', 'ꜳ'],
['AA', 'Ꜳ'],
['ae', 'æ ǽ ǣ ᴂ'],
['AE', 'Æ Ǽ Ǣ ᴁ'],
['ao', 'ꜵ'],
['AO', 'Ꜵ'],
['au', 'ꜷ'],
['AU', 'Ꜷ'],
['av', 'ꜹ ꜻ'],
['AV', 'Ꜹ Ꜻ'],
['ay', 'ꜽ'],
['AY', 'Ꜽ'],
['b', 'ḃ ḅ ɓ ḇ ᵬ ᶀ ƀ ƃ б'],
['B', 'Ḃ Ḅ Ɓ Ḇ Ƀ Ƃ ʙ ᴃ Б'],
['c', 'ć č ç ḉ ĉ ɕ ċ ƈ ȼ ↄ ꜿ'],
['C', 'Ć Č Ç Ḉ Ĉ Ċ Ƈ Ȼ Ꜿ ᴄ'],
['ch', 'ч'],
['CH', 'Ч'],
['d', 'ď ḑ ḓ ȡ ḋ ḍ ɗ ᶑ ḏ ᵭ ᶁ đ ɖ ƌ ꝺ д'],
['D', 'Ď Ḑ Ḓ Ḋ Ḍ Ɗ Ḏ Dz Dž Đ Ƌ Ꝺ ᴅ Д'],
['dz', 'dz dž'],
['DZ', 'DZ DŽ'],
['e', 'é ĕ ě ȩ ḝ ê ế ệ ề ể ễ ḙ ë ė ẹ ȅ è ẻ ȇ ē ḗ ḕ ⱸ ę ᶒ ɇ ẽ ḛ ɛ ᶓ ɘ ǝ ₑ е э ε'],
['E', 'É Ĕ Ě Ȩ Ḝ Ê Ế Ệ Ề Ể Ễ Ḙ Ë Ė Ẹ Ȅ È Ẻ Ȇ Ē Ḗ Ḕ Ę Ɇ Ẽ Ḛ Ɛ Ǝ ᴇ ⱻ Е Э'],
['et', 'ꝫ'],
['ET', 'Ꝫ'],
['f', 'ḟ ƒ ᵮ ᶂ ꝼ ф'],
['F', 'Ḟ Ƒ Ꝼ ꜰ Ф'],
['ff', 'ff'],
['ffi', 'ffi'],
['ffl', 'ffl'],
['fi', 'fi'],
['fl', 'fl'],
['g', 'ǵ ğ ǧ ģ ĝ ġ ɠ ḡ ᶃ ǥ ᵹ ɡ ᵷ г'],
['G', 'Ǵ Ğ Ǧ Ģ Ĝ Ġ Ɠ Ḡ Ǥ Ᵹ ɢ ʛ Г'],
['h', 'ḫ ȟ ḩ ĥ ⱨ ḧ ḣ ḥ ɦ ẖ ħ ɥ ʮ ʯ х'],
['H', 'Ḫ Ȟ Ḩ Ĥ Ⱨ Ḧ Ḣ Ḥ Ħ ʜ Х'],
['hv', 'ƕ'],
['i', 'ı í ĭ ǐ î ï ḯ ị ȉ ì ỉ ȋ ī į ᶖ ɨ ĩ ḭ ᴉ ᵢ й ы и ι'],
['I', 'Í Ĭ Ǐ Î Ï Ḯ İ Ị Ȉ Ì Ỉ Ȋ Ī Į Ɨ Ĩ Ḭ ɪ Й Ы И'],
['ij', 'ij'],
['IJ', 'IJ'],
['is', 'ꝭ'],
['IS', 'Ꝭ'],
['j', 'ȷ ɟ ʄ ǰ ĵ ʝ ɉ ⱼ'],
['J', 'Ĵ Ɉ ᴊ'],
['k', 'ḱ ǩ ķ ⱪ ꝃ ḳ ƙ ḵ ᶄ ꝁ ꝅ ʞ к'],
['K', 'Ḱ Ǩ Ķ Ⱪ Ꝃ Ḳ Ƙ Ḵ Ꝁ Ꝅ ᴋ К'],
['l', 'ĺ ƚ ɬ ľ ļ ḽ ȴ ḷ ḹ ⱡ ꝉ ḻ ŀ ɫ ᶅ ɭ ł ꞁ л'],
['L', 'Ĺ Ƚ Ľ Ļ Ḽ Ḷ Ḹ Ⱡ Ꝉ Ḻ Ŀ Ɫ Lj Ł Ꞁ ʟ ᴌ Л'],
['lj', 'lj'],
['LJ', 'LJ'],
['m', 'ḿ ṁ ṃ ɱ ᵯ ᶆ ɯ ɰ м'],
['M', 'Ḿ Ṁ Ṃ Ɱ Ɯ ᴍ М'],
['n', 'ń ň ņ ṋ ȵ ṅ ṇ ǹ ɲ ṉ ƞ ᵰ ᶇ ɳ ñ н η'],
['N', 'Ń Ň Ņ Ṋ Ṅ Ṇ Ǹ Ɲ Ṉ Ƞ Nj Ñ ɴ ᴎ Н'],
['nj', 'nj'],
['NJ', 'NJ'],
['o', 'ɵ ó ŏ ǒ ô ố ộ ồ ổ ỗ ö ȫ ȯ ȱ ọ ő ȍ ò ỏ ơ ớ ợ ờ ở ỡ ȏ ꝋ ꝍ ⱺ ō ṓ ṑ ǫ ǭ ø ǿ õ ṍ ṏ ȭ ɔ ᶗ ᴑ ᴓ ₒ о'],
['O', 'Ó Ŏ Ǒ Ô Ố Ộ Ồ Ổ Ỗ Ö Ȫ Ȯ Ȱ Ọ Ő Ȍ Ò Ỏ Ơ Ớ Ợ Ờ Ở Ỡ Ȏ Ꝋ Ꝍ Ō Ṓ Ṑ Ɵ Ǫ Ǭ Ø Ǿ Õ Ṍ Ṏ Ȭ Ɔ ᴏ ᴐ О'],
['oe', 'ᴔ œ'],
['OE', 'Œ ɶ'],
['oi', 'ƣ'],
['OI', 'Ƣ'],
['oo', 'ꝏ'],
['OO', 'Ꝏ'],
['ou', 'ȣ'],
['OU', 'Ȣ ᴕ'],
['p', 'ṕ ṗ ꝓ ƥ ᵱ ᶈ ꝕ ᵽ ꝑ п'],
['P', 'Ṕ Ṗ Ꝓ Ƥ Ꝕ Ᵽ Ꝑ ᴘ П'],
['q', 'ꝙ ʠ ɋ ꝗ'],
['Q', 'Ꝙ Ꝗ'],
['r', 'ꞃ ŕ ř ŗ ṙ ṛ ṝ ȑ ɾ ᵳ ȓ ṟ ɼ ᵲ ᶉ ɍ ɽ ɿ ɹ ɻ ɺ ⱹ ᵣ р'],
['R', 'Ꞃ Ŕ Ř Ŗ Ṙ Ṛ Ṝ Ȑ Ȓ Ṟ Ɍ Ɽ ʁ ʀ ᴙ ᴚ Р ®'],
['s', 'ꞅ ſ ẜ ẛ ẝ ś ṥ š ṧ ş ŝ ș ṡ ṣ ṩ ʂ ᵴ ᶊ ȿ с'],
['S', 'Ꞅ Ś Ṥ Š Ṧ Ş Ŝ Ș Ṡ Ṣ Ṩ ꜱ С $'],
['sch', 'щ'],
['SCH', 'Щ'],
['sh', 'ш'],
['SH', 'Ш'],
['ss', 'ß'],
['st', 'st'],
['t', 'ꞇ ť ţ ṱ ț ȶ ẗ ⱦ ṫ ṭ ƭ ṯ ᵵ ƫ ʈ ŧ ʇ т'],
['T', 'Ꞇ Ť Ţ Ṱ Ț Ⱦ Ṫ Ṭ Ƭ Ṯ Ʈ Ŧ ᴛ Т'],
['th', 'ᵺ'],
['ts', 'ц'],
['TS', 'Ц'],
['tz', 'ꜩ'],
['TZ', 'Ꜩ'],
['u', 'ᴝ ú ŭ ǔ û ṷ ü ǘ ǚ ǜ ǖ ṳ ụ ű ȕ ù ủ ư ứ ự ừ ử ữ ȗ ū ṻ ų ᶙ ů ũ ṹ ṵ ᵤ у'],
['U', 'Ú Ŭ Ǔ Û Ṷ Ü Ǘ Ǚ Ǜ Ǖ Ṳ Ụ Ű Ȕ Ù Ủ Ư Ứ Ự Ừ Ử Ữ Ȗ Ū Ṻ Ų Ů Ũ Ṹ Ṵ ᴜ У'],
['ue', 'ᵫ'],
['um', 'ꝸ'],
['v', 'ʌ ⱴ ꝟ ṿ ʋ ᶌ ⱱ ṽ ᵥ в'],
['V', 'Ʌ Ꝟ Ṿ Ʋ Ṽ ᴠ В'],
['vy', 'ꝡ'],
['VY', 'Ꝡ'],
['w', 'ʍ ẃ ŵ ẅ ẇ ẉ ẁ ⱳ ẘ'],
['W', 'Ẃ Ŵ Ẅ Ẇ Ẉ Ẁ Ⱳ ᴡ'],
['x', 'ẍ ẋ ᶍ ₓ'],
['X', 'Ẍ Ẋ'],
['y', 'ʎ ý ŷ ÿ ẏ ỵ ỳ ƴ ỷ ỿ ȳ ẙ ɏ ỹ'],
['Y', 'Ý Ŷ Ÿ Ẏ Ỵ Ỳ Ƴ Ỷ Ỿ Ȳ Ɏ Ỹ ʏ'],
['ya', 'я'],
['Ya', 'Я'],
['yo', 'ё'],
['YO', 'Ё'],
['yu', 'ю'],
['YU', 'Ю'],
['z', 'ź ž ẑ ʑ ⱬ ż ẓ ȥ ẕ ᵶ ᶎ ʐ ƶ ɀ з'],
['Z', 'Ź Ž Ẑ Ⱬ Ż Ẓ Ȥ Ẕ Ƶ ᴢ З'],
['zh', 'ж'],
['ZH', 'Ж'],
]);
const nonAscii = /[^A-Za-z0-9]/g;
export function latinize(text: string): string {
return text
.replace(matchRomaji, replaceRomaji)
.replace(nonAscii, x => characters[x] || x);
}
export function latinize2(name: string): string {
return latinize(name
.replace(/[ǫ]/ui, 'q')
.replace(/[с]/ui, 'c')
.replace(/[н]|\|-\|/ui, 'h')
.replace(/[лпий]/ui, 'n'));
}
File diff suppressed because it is too large Load Diff
+122
View File
@@ -0,0 +1,122 @@
import { Matrix2D } from './interfaces';
export function createMat2D(): Matrix2D {
const out = new Float32Array(6);
out[0] = 1;
out[3] = 1;
return out;
}
export function identityMat2D(out: Matrix2D) {
out[0] = 1;
out[1] = 0;
out[2] = 0;
out[3] = 1;
out[4] = 0;
out[5] = 0;
return out;
}
export function copyMat2D(out: Matrix2D, a: Matrix2D) {
out.set(a);
return out;
}
export function setMat2D(out: Matrix2D, a: number, b: number, c: number, d: number, tx: number, ty: number) {
out[0] = a;
out[1] = b;
out[2] = c;
out[3] = d;
out[4] = tx;
out[5] = ty;
return out;
}
export function mulMat2D(out: Matrix2D, a: Matrix2D, b: Matrix2D) {
const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5];
const b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5];
out[0] = a0 * b0 + a2 * b1;
out[1] = a1 * b0 + a3 * b1;
out[2] = a0 * b2 + a2 * b3;
out[3] = a1 * b2 + a3 * b3;
out[4] = a0 * b4 + a2 * b5 + a4;
out[5] = a1 * b4 + a3 * b5 + a5;
return out;
}
export function translateMat2D(out: Matrix2D, a: Matrix2D, x: number, y: number) {
const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5];
out[0] = a0;
out[1] = a1;
out[2] = a2;
out[3] = a3;
out[4] = a0 * x + a2 * y + a4;
out[5] = a1 * x + a3 * y + a5;
return out;
}
export function rotateMat2D(out: Matrix2D, a: Matrix2D, rad: number) {
const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5];
const s = Math.sin(rad);
const c = Math.cos(rad);
out[0] = a0 * c + a2 * s;
out[1] = a1 * c + a3 * s;
out[2] = a0 * -s + a2 * c;
out[3] = a1 * -s + a3 * c;
out[4] = a4;
out[5] = a5;
return out;
}
export function scaleMat2D(out: Matrix2D, a: Matrix2D, x: number, y: number) {
const a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5];
out[0] = a0 * x;
out[1] = a1 * x;
out[2] = a2 * y;
out[3] = a3 * y;
out[4] = a4;
out[5] = a5;
return out;
}
const temp = createMat2D();
export function skewX(out: Matrix2D, a: Matrix2D, angle: number): Matrix2D {
setMat2D(temp, 1, 0, Math.tan(angle), 1, 0, 0);
mulMat2D(out, a, temp);
return out;
}
export function skewY(out: Matrix2D, a: Matrix2D, angle: number): Matrix2D {
setMat2D(temp, 1, Math.tan(angle), 0, 1, 0, 0);
mulMat2D(out, a, temp);
return out;
}
const tempMatrix = createMat2D();
export function skewTransform(base: Matrix2D | undefined, skew: number, ox: number, oy: number, x: number, y: number): Matrix2D {
identityMat2D(tempMatrix);
if (skew) {
translateMat2D(tempMatrix, tempMatrix, ox + x, oy + y);
skewY(tempMatrix, tempMatrix, skew);
translateMat2D(tempMatrix, tempMatrix, -ox, -oy);
} else {
translateMat2D(tempMatrix, tempMatrix, x, y);
}
if (base !== undefined) {
mulMat2D(tempMatrix, base, tempMatrix);
}
return tempMatrix;
}
export function isIdentity(m: Matrix2D) {
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
}
export function isTranslation(m: Matrix2D) {
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1;
}
+33
View File
@@ -0,0 +1,33 @@
import { Matrix4 } from './interfaces';
export function createMat4(): Matrix4 {
const out = new Float32Array(16);
out[0] = 1;
out[5] = 1;
out[10] = 1;
out[15] = 1;
return out;
}
export function ortho(out: Matrix4, left: number, right: number, bottom: number, top: number, near: number, far: number) {
const lr = 1 / (left - right);
const bt = 1 / (bottom - top);
const nf = 1 / (near - far);
out[0] = -2 * lr;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = -2 * bt;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = 2 * nf;
out[11] = 0;
out[12] = (left + right) * lr;
out[13] = (top + bottom) * bt;
out[14] = (far + near) * nf;
out[15] = 1;
return out;
}
+850
View File
@@ -0,0 +1,850 @@
import { clamp } from 'lodash';
import * as sprites from '../generated/sprites';
import {
EntityPart, Sprite, Rect, SpriteBatch, PaletteManager, Palette, PaletteRenderable, PaletteSpriteBatch,
DrawOptions, getAnimationFromEntityState, EntityState, SignEntityOptions, EntityFlags, Collider, MixinEntity,
Season,
} from './interfaces';
import { at, att, hasFlag, invalidEnum } from './utils';
import { WHITE, BLACK, RED } from './colors';
import { toScreenX, toWorldX, toWorldY, toScreenYWithZ } from './positionUtils';
import { rect, addRects, addRect } from './rect';
import { SECOND } from './constants';
import { mockPaletteManager } from './ponyInfo';
import { releasePalette } from '../graphics/paletteManager';
interface Renderable {
color?: Sprite;
shadow?: Sprite;
}
export interface AnimatedRenderable {
frames: Sprite[];
shadow?: Sprite;
palette: Uint32Array;
}
export interface AnimatedRenderable1 {
frames: (Sprite | undefined)[];
}
const predefinedSteps = [
[],
[1],
[3, 1],
[3, 2, 1],
[4, 2, 1, 1],
[5, 3, 2, 1, 1],
[9, 5, 3, 2, 1, 1],
[14, 9, 5, 3, 2, 1, 1],
];
let paletteManager: PaletteManager | undefined;
export function createPalette(palette: Uint32Array | undefined): Palette | undefined {
return palette && paletteManager && paletteManager.addArray(palette);
}
export function setPaletteManager(manager: PaletteManager | undefined) {
paletteManager = manager;
}
export function fakePaletteManager<T>(action: () => T): T {
const tempPaletteManager = paletteManager;
paletteManager = mockPaletteManager;
const result = action();
paletteManager = tempPaletteManager;
return result;
}
function getBounds(sprite: Sprite | undefined, ox: number, oy: number): Rect {
return sprite ? rect(sprite.ox + ox, sprite.oy + oy, sprite.w, sprite.h) : rect(0, 0, 0, 0);
}
export function getRenderableBounds({ color, shadow }: Renderable, dx: number, dy: number): Rect {
if (color && shadow) {
return addRects(getBounds(color, -dx, -dy), getBounds(shadow, -dx, -dy));
} else if (color) {
return getBounds(color, -dx, -dy);
} else if (shadow) {
return getBounds(shadow, -dx, -dy);
} else {
return rect(0, 0, 0, 0);
}
}
function getBoundsForFrames(frames: (Sprite | undefined)[], dx: number, dy: number) {
return frames.reduce((bounds, f) => f ? addRects(bounds, getBounds(f, dx, dy)) : bounds, rect(0, 0, 0, 0));
}
export function pickable(pickableX: number, pickableY: number): EntityPart {
return { pickableX, pickableY };
}
export function mixPickable(pickableX: number, pickableY: number): MixinEntity {
return base => {
base.pickableX = pickableX;
base.pickableY = pickableY;
};
}
export function mixTrigger(tileX: number, tileY: number, tileW: number, tileH: number, tall: boolean): MixinEntity {
const x = toWorldX(tileX);
const y = toWorldY(tileY);
const w = toWorldX(tileW);
const h = toWorldY(tileH);
const bounds = rect(x, y, w, h);
return base => {
base.triggerBounds = bounds;
base.triggerTall = tall;
base.triggerOn = false;
};
}
export function collider(x: number, y: number, w: number, h: number, tall = true, exact = false): Collider {
return { x, y, w, h, tall, exact };
}
export const ponyColliders = roundedColliderList(-12, -4, 25, 7, 2);
export const ponyCollidersBounds = getColliderBounds(ponyColliders);
function getColliderBounds(colliders: Collider[]) {
const bounds = rect(0, 0, 0, 0);
for (const collider of colliders) {
addRect(bounds, collider);
}
return bounds;
}
function roundedColliderList(x: number, y: number, w: number, h: number, stepsCount: number, tall = true) {
const list: Collider[] = [];
const steps = predefinedSteps[stepsCount];
if (DEVELOPMENT && !steps) {
console.error('Invalid step count', steps);
}
for (let i = 0; i < steps.length; i++) {
list.push(collider(x + steps[i], y + i, w - steps[i] * 2, 1, tall));
}
list.push(collider(x, y + steps.length, w, h - steps.length * 2, tall));
for (let i = 0; i < steps.length; i++) {
const ii = steps.length - (i + 1);
list.push(collider(x + steps[ii], y + h - steps.length + i, w - steps[ii] * 2, 1, tall));
}
return list;
}
export function mixColliderRect(x: number, y: number, w: number, h: number, tall = true, exact = false): MixinEntity {
return mixColliders(collider(x, y, w, h, tall, exact));
}
export function mixColliderRounded(x: number, y: number, w: number, h: number, stepsCount: number, tall = true): MixinEntity {
return mixColliders(...roundedColliderList(x, y, w, h, stepsCount, tall));
}
export function mixColliders(...list: Collider[]): MixinEntity {
const bounds = getColliderBounds(list);
return base => {
base.flags |= EntityFlags.CanCollideWith;
base.colliders = list;
base.collidersBounds = bounds;
};
}
export function taperColliderSE(x: number, y: number, w: number, h: number, tall?: boolean) {
const colliders: Collider[] = [];
for (let iy = 0, ix = w - 2; iy < h; iy++ , ix -= ((iy % 3) ? 1 : 2)) {
colliders.push(collider(x + ix, y + iy, w - ix, 1, tall));
}
return colliders;
}
export function taperColliderSW(x: number, y: number, w: number, h: number, tall?: boolean) {
const colliders: Collider[] = [];
for (let iy = 0, ix = w - 2; iy < h; iy++ , ix -= ((iy % 3) ? 1 : 2)) {
colliders.push(collider(x, y + iy, w - ix, 1, tall));
}
return colliders;
}
export function taperColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) {
const colliders: Collider[] = [];
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
colliders.push(collider(x, y + iy, w - ix, 1, tall));
}
return colliders;
}
export function taperColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) {
const colliders: Collider[] = [];
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
colliders.push(collider(x + ix, y + iy, w - ix, 1, tall));
}
return colliders;
}
export function skewColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) {
const colliders: Collider[] = [];
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
colliders.push(collider(x - ix, y + iy, w, 1, tall));
}
return colliders;
}
export function skewColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) {
const colliders: Collider[] = [];
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
colliders.push(collider(x + ix, y + iy, w, 1, tall));
}
return colliders;
}
export function triangleColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) {
const colliders: Collider[] = [];
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
colliders.push(collider(x - ix, y + iy, w + ix, 1, tall));
}
return colliders;
}
export function triangleColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) {
const colliders: Collider[] = [];
for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) {
colliders.push(collider(x, y + iy, w + ix, 1, tall));
}
return colliders;
}
export function mixInteract(x: number, y: number, w: number, h: number, interactRange?: number): MixinEntity {
const interactBounds = rect(x, y, w, h);
return base => {
base.flags |= EntityFlags.Interactive;
base.interactBounds = interactBounds;
base.interactRange = interactRange;
};
}
export function mixInteractAt(interactRange?: number): MixinEntity {
return base => {
base.flags |= EntityFlags.Interactive;
base.interactRange = interactRange;
};
}
export function mixMinimap(color: number, rect: Rect, order = 1): MixinEntity {
const minimap = { color, rect, order };
return base => base.minimap = minimap;
}
export interface AnimatedMixinOptions {
color?: number;
repeat?: boolean;
animations?: number[][];
lightSprite?: AnimatedRenderable1;
useGameTime?: boolean;
flipped?: boolean;
}
export function mixAnimation(
anim: AnimatedRenderable, fps: number, dx: number, dy: number,
{ color = WHITE, repeat = true, animations, lightSprite, useGameTime, flipped = false }: AnimatedMixinOptions = {}
): MixinEntity {
const bounds = getBoundsForFrames(anim.frames, -dx, -dy);
const lightSpriteBounds = lightSprite ? getBoundsForFrames(lightSprite.frames, -dx, -dy) : rect(0, 0, 0, 0);
if (SERVER && !TESTS) {
return base => base.bounds = bounds;
}
return base => {
const defaultPalette = anim.shadow && createPalette(sprites.defaultPalette);
const palette = createPalette(anim.palette);
let time = repeat ? Math.random() * 5 : 0;
let animation = 0;
let lastFrame = 0;
const getFrame = (options: DrawOptions) => {
let frameNumber = Math.floor(time * fps);
if (useGameTime) {
frameNumber = Math.floor((options.gameTime / 1000) * fps);
}
if (animations) {
if (repeat) {
frameNumber = frameNumber % animations[animation].length;
}
return at(animations[animation], frameNumber) || 0;
} else {
return repeat ? (frameNumber % anim.frames.length) : Math.min(frameNumber, anim.frames.length - 1);
}
};
base.bounds = bounds;
base.palettes = [];
defaultPalette && base.palettes.push(defaultPalette);
palette && base.palettes.push(palette);
base.update = function (delta: number) {
time += delta;
const anim = getAnimationFromEntityState(this.state);
if (animations && anim !== animation) {
animation = anim;
time = 0;
}
const frameNumber = Math.floor(time * fps);
if (lastFrame !== frameNumber) {
lastFrame = frameNumber;
return true;
} else {
return false;
}
};
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
const frame = getFrame(options);
const frameSprite = anim.frames[frame];
const x = toScreenX(this.x);
const y = toScreenYWithZ(this.y, this.z);
batch.save();
batch.translate(x, y);
if (hasFlag(this.state, EntityState.FacingRight) || flipped) {
batch.scale(-1, 1);
}
batch.translate(-dx, -dy);
anim.shadow && batch.drawSprite(anim.shadow, options.shadowColor, defaultPalette, 0, 0);
frameSprite && batch.drawSprite(frameSprite, color, palette, 0, 0);
batch.restore();
};
if (lightSprite) {
base.lightSpriteColor = WHITE;
base.lightSpriteBounds = lightSpriteBounds;
base.drawLightSprite = function (batch, options) {
const frame = getFrame(options);
const frameSprite = lightSprite.frames[frame];
const x = toScreenX(this.x);
const y = toScreenYWithZ(this.y, this.z);
batch.save();
batch.translate(x, y);
if (hasFlag(this.state, EntityState.FacingRight) || flipped) {
batch.scale(-1, 1);
}
batch.translate(-dx, -dy);
batch.drawSprite(frameSprite, this.lightSpriteColor!, 0, 0);
batch.restore();
};
}
};
}
export function mixDrawWindow(
sprite: PaletteRenderable, dx: number, dy: number, paletteIndex: number,
padLeft: number, padTop: number, padRight: number, padBottom: number,
): MixinEntity {
const bounds = getRenderableBounds(sprite, dx, dy);
return base => {
base.bounds = bounds;
if (!SERVER || TESTS) {
const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette);
const palette = createPalette(att(sprite.palettes, paletteIndex));
base.palettes = [];
defaultPalette && base.palettes.push(defaultPalette);
palette && base.palettes.push(palette);
base.draw = function (batch, options) {
const baseX = toScreenX(this.x + (this.ox || 0));
const baseY = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0));
const x = baseX - dx;
const y = baseY - dy;
if (sprite.shadow !== undefined) {
batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y);
}
if (sprite.color !== undefined) {
batch.drawRect(options.lightColor,
x + padLeft, y + padTop, sprite.color.w - (padLeft + padRight), sprite.color.h - (padTop + padBottom));
batch.drawSprite(sprite.color, WHITE, palette, x, y);
}
};
}
};
}
export function mixDraw(sprite: PaletteRenderable, dx: number, dy: number, paletteIndex = 0): MixinEntity {
const bounds = getRenderableBounds(sprite, dx, dy);
return base => {
base.bounds = bounds;
if (!SERVER || TESTS) {
const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette);
const palette = createPalette(att(sprite.palettes, paletteIndex));
base.palettes = [];
defaultPalette && base.palettes.push(defaultPalette);
palette && base.palettes.push(palette);
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
const x = toScreenX(this.x + (this.ox || 0)) - dx;
const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy;
const opacity = 1 - 0.6 * (this.coverLifting || 0);
if (sprite.shadow !== undefined) {
batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y);
}
batch.globalAlpha = opacity;
if (sprite.color !== undefined) {
batch.drawSprite(sprite.color, WHITE, palette, x, y);
}
batch.globalAlpha = 1;
};
}
};
}
export interface MixDraw {
sprite: PaletteRenderable;
dx: number;
dy: number;
palette: number;
}
export interface MixDrawSeasonal {
summer: MixDraw;
autumn?: Partial<MixDraw>;
winter?: Partial<MixDraw>;
spring?: Partial<MixDraw>;
}
function addBounds(bounds: Rect, setup: MixDraw) {
addRect(bounds, getRenderableBounds(setup.sprite, setup.dx, setup.dy));
}
export function mixDrawSeasonal(setup: MixDrawSeasonal): MixinEntity {
const bounds = rect(0, 0, 0, 0);
const summer = setup.summer;
const autumn = { ...summer, ...setup.autumn };
const winter = { ...summer, ...setup.winter };
const spring = { ...summer, ...setup.spring };
addBounds(bounds, summer);
addBounds(bounds, autumn);
addBounds(bounds, winter);
addBounds(bounds, spring);
return (base, _, worldState) => {
base.bounds = bounds;
if (!SERVER || TESTS) {
let season = Season.Summer;
let { sprite, dx, dy, palette: paletteIndex } = setup.summer;
let defaultPalette: Palette | undefined = undefined;
let palette: Palette | undefined = undefined;
const setupSeason = (newSeason: Season) => {
season = newSeason;
let set: MixDraw;
switch (season) {
case Season.Summer:
set = summer;
break;
case Season.Autumn:
set = autumn;
break;
case Season.Winter:
set = winter;
break;
case Season.Spring:
set = spring;
break;
default:
invalidEnum(season);
return;
}
sprite = set.sprite;
dx = set.dx;
dy = set.dy;
paletteIndex = set.palette;
if (base.palettes) {
for (const palette of base.palettes) {
releasePalette(palette);
}
}
defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette);
palette = createPalette(att(sprite.palettes, paletteIndex));
base.palettes = [];
defaultPalette && base.palettes.push(defaultPalette);
palette && base.palettes.push(palette);
};
setupSeason(worldState.season);
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
const x = toScreenX(this.x + (this.ox || 0)) - dx;
const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy;
const opacity = 1 - 0.6 * (this.coverLifting || 0);
if (sprite.shadow !== undefined) {
batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y);
}
batch.globalAlpha = opacity;
if (sprite.color !== undefined) {
batch.drawSprite(sprite.color, WHITE, palette, x, y);
}
batch.globalAlpha = 1;
if (season !== options.season) {
setupSeason(options.season);
}
};
}
};
}
function splitSprite(sprite: Sprite, x: number, w: number, h: number) {
const result: Sprite[] = [];
for (let y = 0; y < sprite.h; y += h) {
result.push({ x: sprite.x + x, y: sprite.y + y, w, h, ox: sprite.ox, oy: sprite.oy, type: sprite.type });
}
return result;
}
const poles = [
{ sprite: sprites.direction_pole_3, dy: -39 },
{ sprite: sprites.direction_pole_4, dy: -50 },
{ sprite: sprites.direction_pole_5, dy: -61 },
];
const shadowLeft = sprites.direction_shadow_left.shadow;
const shadowRight = sprites.direction_shadow_right.shadow;
const leftSprites = splitSprite(sprites.direction_left_right.color, 0, 17, 10);
const rightSprites = splitSprite(sprites.direction_left_right.color, 17, 17, 10);
const dirUpDown = [
{
shadowUp: sprites.direction_shadow_up_left.shadow,
shadowDown: sprites.direction_shadow_down_right.shadow,
spriteUp: sprites.direction_up_left.color,
spriteDown: sprites.direction_down_right.color,
shadowUpDX: -6, shadowUpDY: -9,
shadowDownDX: -1, shadowDownDY: 3,
upDX: -6, upDY: -7,
downDX: -1, downDY: 5,
},
{
shadowUp: sprites.direction_shadow_up_right.shadow,
shadowDown: sprites.direction_shadow_down_left.shadow,
spriteUp: sprites.direction_up_right.color,
spriteDown: sprites.direction_down_left.color,
shadowUpDX: 1, shadowUpDY: -9,
shadowDownDX: -4, shadowDownDY: 3,
upDX: 1, upDY: -7,
downDX: -5, downDY: 4,
},
];
export function mixDrawDirectionSign(): MixinEntity {
const poleDX = -4;
const leftDX = -20;
const rightDX = 3;
const plateDY = 2;
const leftRightStep = 11;
const upDownStep = 11;
return (base, options = {}) => {
const { sign: { r = 0, w = [], e = [], s = [], n = [] } = {} } = options as SignEntityOptions;
const max = clamp(Math.max(w.length, e.length, s.length, n.length), 3, 5);
const boundsH = 7 + max * 11;
base.bounds = rect(-20, -boundsH, 40, boundsH);
base.options = options;
if (SERVER && !TESTS)
return;
const {
shadowUp, shadowDown, spriteUp, spriteDown, upDX, upDY, downDX, downDY,
shadowUpDX, shadowUpDY, shadowDownDX, shadowDownDY,
} = dirUpDown[r];
const leftShadow = !!w.length;
const rightShadow = !!e.length;
const upShadow = !!n.length;
const downShadow = !!s.length;
const pole = poles[max - 3];
const defaultPalette = pole.sprite.shadow && createPalette(sprites.defaultPalette);
const palette = createPalette(att(pole.sprite.palettes, 0));
base.palettes = [];
defaultPalette && base.palettes.push(defaultPalette);
palette && base.palettes.push(palette);
base.draw = function (batch, options) {
const x = toScreenX(this.x);
const y = toScreenYWithZ(this.y, this.z);
batch.drawSprite(pole.sprite.shadow, options.shadowColor, defaultPalette, x + poleDX, y + pole.dy);
leftShadow && batch.drawSprite(shadowLeft, options.shadowColor, defaultPalette, x - 18, y - 1);
rightShadow && batch.drawSprite(shadowRight, options.shadowColor, defaultPalette, x + 4, y - 1);
upShadow && batch.drawSprite(shadowUp, options.shadowColor, defaultPalette, x + shadowUpDX, y + shadowUpDY);
downShadow && batch.drawSprite(shadowDown, options.shadowColor, defaultPalette, x + shadowDownDX, y + shadowDownDY);
for (let i = n.length - 1; i >= 0; i--) {
if (n[i] !== -1) {
batch.drawSprite(spriteUp, WHITE, palette, x + upDX, y + pole.dy + upDY + i * upDownStep);
}
}
batch.drawSprite(pole.sprite.color, WHITE, palette, x + poleDX, y + pole.dy);
for (let i = 0; i < w.length; i++) {
if (w[i] !== -1) {
const sprite = leftSprites[w[i]];
sprite && batch.drawSprite(sprite, WHITE, palette, x + leftDX, y + pole.dy + plateDY + i * leftRightStep);
}
}
for (let i = 0; i < e.length; i++) {
if (e[i] !== -1) {
const sprite = rightSprites[e[i]];
sprite && batch.drawSprite(rightSprites[e[i]], WHITE, palette, x + rightDX, y + pole.dy + plateDY + i * leftRightStep);
}
}
for (let i = s.length - 1; i >= 0; i--) {
if (s[i] !== -1) {
batch.drawSprite(spriteDown, WHITE, palette, x + downDX, y + pole.dy + downDY + i * upDownStep);
}
}
};
};
}
export function mixLight(color: number, dx: number, dy: number, w: number, h: number): MixinEntity {
return base => {
if (!SERVER || TESTS) {
base.lightOn = true;
base.lightColor = color;
base.lightScale = 1;
base.lightTarget = 1;
base.lightScaleAdjust = 1;
base.lightBounds = rect(-(dx + w / 2), -(dy + h / 2), w, h);
base.drawLight = function (batch: SpriteBatch) {
if (!this.lightOn)
return;
const x = toScreenX(this.x);
const y = toScreenYWithZ(this.y, this.z);
const s = this.lightScale! * this.lightScaleAdjust!;
const width = w * s;
const height = h * s;
const color = this.lightColor!;
batch.drawImage(color, -1, -1, 2, 2, x - (dx + width / 2), y - (dy + height / 2), width, height);
};
}
};
}
export function mixLightSprite(sprite: Sprite, color: number, dx: number, dy: number): MixinEntity {
return base => {
if (!SERVER || TESTS) {
base.lightSpriteOn = true;
base.lightSpriteX = dx;
base.lightSpriteY = dy;
base.lightSpriteColor = color;
base.lightSpriteBounds = getBounds(sprite, -dx, -dy);
base.drawLightSprite = function (batch: SpriteBatch) {
if (!this.lightSpriteOn)
return;
const x = toScreenX(this.x) - this.lightSpriteX!;
const y = toScreenYWithZ(this.y, this.z) - this.lightSpriteY!;
batch.drawSprite(sprite, this.lightSpriteColor || BLACK, x, y);
};
}
};
}
export function mixDrawRain(): MixinEntity {
const sprite = sprites.rainfall.color; // 110x477
const bounds = rect(toScreenX(-4), -sprite.h, toScreenX(8), sprite.h);
return base => {
base.bounds = bounds;
if (SERVER && !TESTS)
return;
let time = 0;
const palette = createPalette(sprites.defaultPalette);
base.palettes = [palette];
// update(delta: number) {
// time += delta * 1000;
// if (time > 200) {
// time -= 200;
// }
// },
base.draw = function (batch: PaletteSpriteBatch) {
const x = toScreenX(this.x) + bounds.x;
const y = toScreenYWithZ(this.y, this.z) - sprite.h + Math.floor(time);
batch.drawImage(sprite.type, RED, palette, sprite.x, sprite.y, sprite.w, sprite.h, x, y, sprite.w, sprite.h);
};
};
}
export function mixDrawShadow(sprite: PaletteRenderable, dx: number, dy: number, shadowColor?: number): MixinEntity {
const bounds = getRenderableBounds(sprite, dx, dy);
return base => {
base.bounds = bounds;
if (!SERVER || TESTS) {
const defaultPalette = createPalette(sprites.defaultPalette);
base.palettes = [defaultPalette];
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
const x = toScreenX(this.x + (this.ox || 0)) - dx;
const y = toScreenYWithZ(this.y + (this.oy || 0), this.z) - dy;
const color = shadowColor === undefined ? options.shadowColor : shadowColor;
sprite.shadow && batch.drawSprite(sprite.shadow, color, defaultPalette, x, y);
};
}
};
}
export function mixBobbing(bobsFps: number, bobs: number[]): MixinEntity {
return base => {
base.flags |= EntityFlags.Bobbing;
base.bobsFps = bobsFps;
base.bobs = bobs;
};
}
let fullWalls = true;
export function toggleWalls() {
fullWalls = !fullWalls;
}
export function mixDrawWall(
full: PaletteRenderable, half: PaletteRenderable, dx: number, dy: number, dy2: number
): MixinEntity {
const fullBounds = getRenderableBounds(full, dx, dy);
// const halfBounds = getRenderableBounds(half, dx, dy2);
return base => {
base.bounds = fullBounds; // fullWalls ? fullBounds : halfBounds
if (SERVER && !TESTS)
return;
const fullPalette = createPalette(att(full.palettes, 0));
const halfPalette = createPalette(att(half.palettes, 0));
base.palettes = [];
fullPalette && base.palettes.push(fullPalette);
halfPalette && base.palettes.push(halfPalette);
base.draw = function (batch: PaletteSpriteBatch) {
const sprite = fullWalls ? full : half;
const palette = fullWalls ? fullPalette : halfPalette;
const x = toScreenX(this.x) - dx;
const y = toScreenYWithZ(this.y, this.z) - (fullWalls ? dy : dy2);
sprite.color && batch.drawSprite(sprite.color, WHITE, palette, x, y);
};
};
}
export function mixDrawSpider(
sprite: PaletteRenderable, dx: number, dy: number
): MixinEntity {
const heightOffset = 30;
const spriteColor = sprite.color;
const baseBounds = getRenderableBounds(sprite, dx, dy);
if (!spriteColor)
throw new Error('Missing sprite');
return base => {
const { height, time } = base.options as { height: number; time: number; };
const bounds = { ...baseBounds };
bounds.y -= (height + heightOffset);
bounds.h += height;
base.bounds = bounds;
if (SERVER && !TESTS)
return;
const palette = createPalette(sprite.palettes && sprite.palettes[0]);
base.palettes = [palette];
base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) {
const t = options.gameTime / SECOND - time;
const h = clamp(Math.sin(t / 4) * 4, 0, 1) * height;
if (h < height) {
const lineLength = height - h - 4;
const x = toScreenX(this.x) - dx;
const y = toScreenYWithZ(this.y, this.z) - dy - heightOffset - h;
batch.drawRect(0x181818ff, x + 2, y - lineLength, 1, lineLength + 1);
batch.drawSprite(spriteColor, WHITE, palette, x, y);
}
};
};
}
+123
View File
@@ -0,0 +1,123 @@
import { EntityState, Point, Rect, Entity } from './interfaces';
import { PONY_SPEED_TROT, PONY_SPEED_WALK, tileWidth, tileHeight } from './constants';
import { clamp, hasFlag } from './utils';
import { toWorldX, toWorldY } from './positionUtils';
import { rect } from './rect';
const DIRS = [
[0, -1], // 0
[0.5, -1],
[1, -1],
[1, -0.5],
[1, 0], // 4
[1, 0.5],
[1, 1],
[0.5, 1],
[0, 1], // 8
[-0.5, 1],
[-1, 1],
[-1, 0.5],
[-1, 0], // 12
[-1, -0.5],
[-1, -1],
[-0.5, -1],
];
const SECA = 0xcd3003ca;
const SECB = 0x5b903a62;
const SECC = 0x1c267e56;
const SECD = 0x1921ba6f;
const SECE = 0x0000bc0e;
const PI2 = Math.PI * 2;
const DIRS_ANGLE = DIRS.length / PI2;
export function flagsToSpeed(flags: EntityState): number {
const state = flags & EntityState.PonyStateMask;
if (state === EntityState.PonyTrotting) {
return PONY_SPEED_TROT;
} else if (state === EntityState.PonyWalking) {
return PONY_SPEED_WALK;
} else {
return 0;
}
}
export function dirToVector(dir: number): Point {
const [x, y] = DIRS[(dir | 0) % DIRS.length];
return { x, y };
}
export function vectorToDir(x: number, y: number): number {
const angle = Math.atan2(x, -y);
return Math.round((angle < 0 ? angle + PI2 : angle) * DIRS_ANGLE) % DIRS.length;
}
export interface Movement {
x: number;
y: number;
dir: number;
flags: EntityState;
time: number;
camera: Rect;
}
export const POSITION_MIN = 0;
export const POSITION_MAX = 100000;
export function encodeMovement(
x: number, y: number, dir: number, flags: EntityState, time: number, camera: Rect
): [number, number, number, number, number] {
const pixelX = Math.floor(clamp(x, POSITION_MIN, POSITION_MAX) * tileWidth);
const pixelY = Math.floor(clamp(y, POSITION_MIN, POSITION_MAX) * tileHeight);
const camX = ((pixelX - camera.x) & 0xfff) >>> 0;
const camY = ((pixelY - camera.y) & 0xfff) >>> 0;
const camW = (camera.w & 0xfff) >>> 0;
const camH = (camera.h & 0xfff) >>> 0;
const a = pixelX | ((dir & 0xff) << 24);
const b = pixelY | ((flags & 0xff) << 24);
const c = time;
const d = (camX << 20) | (camY << 8) | (camW >>> 4);
const e = ((camW & 0xf) << 12) | camH;
return [
(a ^ SECA) >>> 0,
(b ^ SECB) >>> 0,
(c ^ SECC) >>> 0,
(d ^ SECD) >>> 0,
(e ^ SECE) >>> 0,
];
}
export function decodeMovement(a: number, b: number, c: number, d: number, e: number): Movement {
a = (a >>> 0) ^ SECA;
b = (b >>> 0) ^ SECB;
c = (c >>> 0) ^ SECC;
d = (d >>> 0) ^ SECD;
e = (e >>> 0) ^ SECE;
const pixelX = a & 0xffffff;
const pixelY = b & 0xffffff;
const x = toWorldX(pixelX + 0.5);
const y = toWorldY(pixelY + 0.5);
const dir = (a >>> 24) & 0xff;
const flags = (b >>> 24) & 0xff;
const time = c;
const camX = pixelX - ((d >>> 20) & 0xfff);
const camY = pixelY - ((d >>> 8) & 0xfff);
const camW = ((d & 0xff) << 4) | ((e >>> 12) & 0xf);
const camH = e & 0xfff;
return { x, y, dir, flags, time, camera: rect(camX, camY, camW, camH) };
}
export function isMovingRight(vx: number, right: boolean): boolean {
return vx < 0 ? false : (vx > 0 ? true : right);
}
export function shouldBeFacingRight(entity: Entity): boolean {
return isMovingRight(entity.vx, hasFlag(entity.state, EntityState.FacingRight));
}
+138
View File
@@ -0,0 +1,138 @@
interface Point {
x: number;
y: number;
}
type Pt = [number, number];
const createPoint = ([x, y]: Pt): Point => ({ x, y });
const createPoints = (pts: Pt[]) => pts.map(createPoint);
export const cmOffsets: Point[] = [];
export const headOffsets: Point[] = [];
export const tailOffsets: Point[] = [];
export const wingOffsets: Point[] = [];
export const frontLegOffsets: Point[] = [];
export const backLegOffsets: Point[] = [];
export const neckAccessoryOffsets: Point[] = [];
export const backAccessoryOffsets: Point[] = [];
export const waistAccessoryOffsets: Point[] = [];
export const chestAccessoryOffsets: Point[] = [];
function offsets(
_index: number, cm: Pt, head: Pt, tail: Pt, wing: Pt, frontLeg: Pt, backLeg: Pt,
neckAccessory: Pt, backAccessory: Pt, waistAccessory: Pt, chestAccessory: Pt
) {
cmOffsets.push(createPoint(cm));
headOffsets.push(createPoint(head));
tailOffsets.push(createPoint(tail));
wingOffsets.push(createPoint(wing));
frontLegOffsets.push(createPoint(frontLeg));
backLegOffsets.push(createPoint(backLeg));
neckAccessoryOffsets.push(createPoint(neckAccessory));
backAccessoryOffsets.push(createPoint(backAccessory));
waistAccessoryOffsets.push(createPoint(waistAccessory));
chestAccessoryOffsets.push(createPoint(chestAccessory));
}
// stand: cm head tail wing frontLeg backLeg neck back waist chest
offsets(0, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 0], [0, 0]);
// sit: cm head tail wing frontLeg backLeg neck back waist chest
offsets(1, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 0], [0, 0]);
offsets(2, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 1], [1, 1]);
offsets(3, [1, 1], [0, 1], [1, 1], [2, 1], [2, 0], [2, 0], /***/[2, 1], [1, 1], [0, 1], [2, 2]);
offsets(4, [4, 2], [2, 3], [4, 3], [5, 2], [5, 2], [5, 2], /***/[4, 3], [4, 3], [3, 3], [5, 4]);
offsets(5, [7, 6], [4, 6], [7, 7], [7, 4], [7, 4], [7, 6], /***/[6, 5], [6, 6], [5, 5], [7, 6]);
offsets(6, [8, 9], [7, 8], [9, 14], [8, 7], [9, 5], [8, 8], /***/[9, 8], [8, 7], [8, 7], [9, 8]);
offsets(7, [8, 12], [8, 8], [9, 15], [9, 9], [9, 5], [8, 11], /***/[9, 7], [8, 8], [8, 7], [9, 7]);
offsets(8, [8, 12], [9, 7], [9, 15], [9, 9], [9, 5], [8, 11], /***/[9, 7], [8, 8], [8, 7], [9, 7]);
offsets(9, [8, 11], [9, 6], [9, 14], [9, 8], [9, 5], [8, 11], /***/[9, 6], [8, 8], [8, 7], [9, 6]);
// lie: cm head tail wing frontLeg backLeg neck back waist chest
offsets(10, [8, 11], [8, 6], [9, 14], [9, 9], [9, 6], [8, 11], /***/[9, 6], [8, 8], [8, 7], [9, 6]);
offsets(11, [8, 11], [7, 7], [9, 14], [8, 9], [8, 6], [8, 11], /***/[8, 7], [8, 8], [7, 7], [8, 6]);
offsets(12, [8, 11], [6, 9], [9, 14], [7, 10], [7, 7], [8, 11], /***/[7, 9], [8, 8], [6, 8], [7, 7]);
offsets(13, [8, 11], [6, 11], [9, 14], [7, 10], [6, 9], [8, 11], /***/[6, 11], [8, 8], [5, 10], [6, 9]);
offsets(14, [8, 11], [7, 12], [9, 14], [7, 11], [6, 9], [8, 11], /***/[7, 12], [8, 8], [6, 10], [7, 10]);
offsets(15, [8, 11], [7, 11], [9, 14], [7, 11], [6, 9], [8, 11], /***/[7, 11], [8, 8], [6, 10], [7, 9]);
offsets(16, [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], /***/[0, 0], [0, 0], [0, 0], [0, 0]);
export const EAR_ACCESSORY_OFFSETS = createPoints([
[0, 0], // 0
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0], // 5
]);
export const EXTRA_ACCESSORY_OFFSETS = createPoints([
[0, 9], // 0
[0, 0],
[0, 0],
[0, 1],
[0, 2],
[0, 2], // 5
[0, 1],
[0, 1],
[0, 2],
[0, 3],
[0, 2], // 10
[0, 2],
[0, 3],
[0, 1],
[0, 1],
[0, 1], // 15
[0, 9],
[0, 3],
[0, 3],
[0, 3],
[0, 3], // 20
[0, 3],
[0, 2],
[0, 3],
[0, 3],
[0, 3], // 25
[0, 2],
[0, 3],
[-1, 3],
[0, 3],
[0, 3], // 30
[0, 3],
]);
export const HEAD_ACCESSORY_OFFSETS = createPoints([
[0, 0], // 0
[0, -5],
[0, -5],
[0, -4],
[0, -4],
[0, -4], // 5
[0, -4],
[1, -4],
[0, -4],
[0, -3],
[0, -4], // 10
[0, -4],
[0, -3],
[1, -5],
[0, -4],
[0, -4], // 15
[0, 0],
[0, -4],
[0, -4],
[0, -4],
[0, -4], // 20
[0, -4],
[0, -5],
[0, -5],
[0, -4],
[1, -3], // 25
[0, -4],
[0, -4],
[0, -4],
[0, -4],
[0, -4], // 30
[0, -3],
]);
+678
View File
@@ -0,0 +1,678 @@
import { PONY_WIDTH, PONY_HEIGHT, BLINK_FRAMES, canFly } from '../client/ponyUtils';
import { stand, sneeze, defaultHeadAnimation, defaultBodyFrame, defaultHeadFrame } from '../client/ponyAnimations';
import {
PaletteSpriteBatch, Pony, BodyAnimation, EntityState, SpriteBatch, ExpressionExtra, HeadAnimation, Palette,
PaletteManager, DrawOptions, Rect, EntityFlags, IMap, Entity, DoAction, Muzzle, Expression, isEyeSleeping,
Iris, EntityPlayerState,
} from './interfaces';
import { hasFlag, setFlag } from './utils';
import { blinkFps, PONY_TYPE } from './constants';
import { releasePalettes } from './ponyInfo';
import { createAnEntity, boopSplashRight, boopSplashLeft } from './entities';
import {
createAnimationPlayer, isAnimationPlaying, drawAnimation, playAnimation, updateAnimation, playOneOfAnimations
} from './animationPlayer';
import { blushColor, WHITE, MAGIC_ALPHA, HEARTS_COLOR } from './colors';
import { encodeExpression, decodeExpression } from './encoders/expressionEncoder';
import { toScreenX, toWorldX, toWorldY, toScreenYWithZ } from './positionUtils';
import { getPonyAnimationFrame, getHeadY, drawPony, getPonyHeadPosition, createHeadTransform } from '../client/ponyDraw';
import {
isPonySitting, isPonyFlying, isPonyLying, isPonyStanding, isPonyLandedOrCanLand, isIdle, isIdleAnimation,
isFacingRight, releaseEntity
} from './entityUtils';
import {
getAnimation, getAnimationFrame, setAnimatorState, updateAnimator, createAnimator, AnimatorState,
resetAnimatorState
} from './animator';
import {
trotting, flying, hovering, toBoopState, isFlyingUpOrDown, isFlyingDown, isSittingDown, isSittingUp, swinging,
standing, sitting, lying, swimming, isSwimmingState, swimmingToFlying,
} from '../client/ponyStates';
import { decodePonyInfo } from './compressPony';
import { defaultPonyState, defaultDrawPonyOptions, isStateEqual } from '../client/ponyHelpers';
import {
sneezeAnimation, holdPoofAnimation, heartsAnimation, tearsAnimation, cryAnimation, zzzAnimations, magicAnimation
} from '../client/spriteAnimations';
import { rect } from './rect';
import { addOrRemoveFromEntityList } from './worldMap';
import { hasDrawLight, hasLightSprite } from '../client/draw';
import { ponyColliders, ponyCollidersBounds } from './mixins';
import { PonyTownGame } from '../client/game';
import { playEffect } from '../client/handlers';
import * as sprites from '../generated/sprites';
import { withAlpha } from './color';
const flyY = 15;
const lightExtentX = 100;
const lightExtentY = 70;
const emptyBounds = rect(0, 0, 0, 0);
const bounds = rect(-PONY_WIDTH / 2, -PONY_HEIGHT, PONY_WIDTH, PONY_HEIGHT + 5);
const boundsFly = rect(bounds.x, bounds.y - flyY, bounds.w, bounds.h + flyY);
const lightBounds = makeLightBounds(bounds);
const lightBoundsFly = makeLightBounds(boundsFly);
const interactBounds = rect(-20, -50, 40, 50);
const interactBoundsFly = rect(interactBounds.x, interactBounds.y - flyY, interactBounds.w, interactBounds.h);
const defaultExpr = encodeExpression(undefined);
export function createPony(
id: number, state: EntityState, info: string | Uint8Array | undefined, defaultPalette: Palette,
paletteManager: PaletteManager
): Pony {
const pony: Pony = {
id,
state,
playerState: EntityPlayerState.None,
type: PONY_TYPE,
flags: EntityFlags.Movable | EntityFlags.CanCollide | EntityFlags.Interactive,
expr: defaultExpr,
ponyState: defaultPonyState(),
x: 0,
y: 0,
z: 0,
vx: 0,
vy: 0,
info,
order: 0,
timestamp: 0,
colliders: ponyColliders,
collidersBounds: ponyCollidersBounds,
selected: false,
extra: false,
toy: 0,
swimming: false,
ex: false, // extended data indicator, sent in extended option
inTheAirDelay: 0,
name: undefined,
tag: undefined,
site: undefined,
modInfo: undefined,
hold: 0,
palettePonyInfo: undefined,
headAnimation: undefined,
batch: undefined,
discardBatch: false,
headTime: Math.random() * 5,
blinkTime: 0,
nextBlink: Math.random() * 5,
currentExpression: defaultExpr,
drawingOptions: { ...defaultDrawPonyOptions(), shadow: true, bounce: BETA },
zzzEffect: createAnimationPlayer(defaultPalette),
cryEffect: createAnimationPlayer(defaultPalette),
sneezeEffect: createAnimationPlayer(defaultPalette),
holdPoofEffect: createAnimationPlayer(defaultPalette),
heartsEffect: createAnimationPlayer(defaultPalette),
magicEffect: createAnimationPlayer(paletteManager.addArray(sprites.magic2.palette)),
animator: createAnimator<BodyAnimation>(),
lastX: 0,
lastY: 0,
lastRight: false,
lastState: defaultPonyState(),
initialized: false,
doAction: DoAction.None,
bounds: bounds,
interactBounds: interactBounds,
chatBounds: interactBounds,
lightBounds: emptyBounds,
lightSpriteBounds: emptyBounds,
paletteManager,
lastBoopSplash: 0,
magicColor: 0,
};
pony.ponyState.drawFaceExtra = batch => drawFaceExtra(batch, pony);
return pony;
}
export function isPony(entity: Entity): entity is Pony {
return entity.type === PONY_TYPE;
}
export function isPonyOnTheGround(pony: Pony) {
return !isPonyFlying(pony) && !isFlyingUpOrDown(pony.animator.state);
}
export function getPaletteInfo(pony: Pony) {
return ensurePonyInfoDecoded(pony);
}
export function releasePony(pony: Pony) {
if (pony.ponyState.holding) {
releaseEntity(pony.ponyState.holding);
}
releasePalettePonyInfo(pony);
}
export function canPonyFly(pony: Pony) {
return !!pony.palettePonyInfo && canFly(pony.palettePonyInfo);
}
export function canPonyLie<T>(pony: Pony, map: IMap<T>) {
return !isPonyLying(pony) && (isIdle(pony) || isSittingDown(pony.animator.state) || isFlyingDown(pony.animator.state)) &&
isPonyLandedOrCanLand(pony, map);
}
export function canPonySit<T>(pony: Pony, map: IMap<T>) {
return !isPonySitting(pony) && (isIdle(pony) || isFlyingDown(pony.animator.state)) &&
isPonyLandedOrCanLand(pony, map);
}
export function canPonyStand<T>(pony: Pony, map: IMap<T>) {
return !isPonyStanding(pony) && (isIdleAnimation(pony.ponyState.animation) || isSittingUp(pony.animator.state)) &&
isPonyLandedOrCanLand(pony, map);
}
export function canPonyFlyUp(pony: Pony) {
return !isPonyFlying(pony) && canPonyFly(pony) && !isFlyingUpOrDown(pony.animator.state);
}
export function getPonyChatHeight(pony: Pony) {
const baseHeight = 2;
const state = pony.ponyState;
if (pony.animator.state === trotting) {
return baseHeight;
} else if (pony.animator.state === flying || pony.animator.state === hovering) {
return baseHeight - 16;
} else {
const frame = getPonyAnimationFrame(state.animation, state.animationFrame, defaultBodyFrame);
const animation = state.headAnimation || defaultHeadAnimation;
const headFrame = getPonyAnimationFrame(animation, state.headAnimationFrame, defaultHeadFrame);
return baseHeight + getHeadY(frame, headFrame);
}
}
export function updatePonyInfo(pony: Pony, info: string | Uint8Array, apply: () => void) {
pony.info = info;
if (pony.palettePonyInfo !== undefined) {
releasePalettePonyInfo(pony);
ensurePonyInfoDecoded(pony);
pony.discardBatch = true;
if (isPonyFlying(pony) && !canPonyFly(pony)) {
DEVELOPMENT && console.warn('Force land');
pony.state = setFlag(pony.state, EntityState.PonyFlying, false);
resetAnimatorState(pony.animator);
}
apply();
}
}
export function ensurePonyInfoDecoded(pony: Pony) {
if (pony.info !== undefined && pony.palettePonyInfo === undefined) {
pony.palettePonyInfo = decodePonyInfo(pony.info, pony.paletteManager);
const wingType = pony.palettePonyInfo.wings && pony.palettePonyInfo.wings.type || 0;
pony.animator.variant = wingType === 4 ? 'bug' : '';
pony.ponyState.blushColor = blushColor(pony.palettePonyInfo.coatPalette.colors[1]);
pony.magicColor = withAlpha(pony.palettePonyInfo.magicColorValue, MAGIC_ALPHA);
}
return pony.palettePonyInfo!;
}
export function invalidatePalettesForPony(pony: Pony) {
pony.discardBatch = true;
}
export function doBoopPonyAction(game: PonyTownGame, pony: Pony) {
doPonyAction(pony, DoAction.Boop);
if (pony.swimming && pony.lastBoopSplash < performance.now()) {
if (isFacingRight(pony)) {
playEffect(game, pony, boopSplashRight.type);
} else {
playEffect(game, pony, boopSplashLeft.type);
}
pony.lastBoopSplash = performance.now() + 800;
}
}
export function doPonyAction(pony: Pony, action: DoAction) {
pony.doAction = action;
}
export function setPonyExpression(pony: Pony, expr: number) {
pony.expr = expr;
}
export function hasExtendedInfo(pony: Pony) {
return pony.ex;
}
export function hasHeadAnimation(pony: Pony) {
return pony.headAnimation !== undefined;
}
export function setHeadAnimation(pony: Pony, headAnimation: HeadAnimation | undefined) {
if (pony.headAnimation !== headAnimation) {
pony.headTime = 0;
pony.headAnimation = headAnimation;
}
}
export function drawPonyEntity(batch: PaletteSpriteBatch, pony: Pony, drawOptions: DrawOptions) {
if (pony.discardBatch && pony.batch !== undefined) {
batch.releaseBatch(pony.batch);
pony.batch = undefined;
pony.discardBatch = false;
}
if (pony.batch !== undefined) {
batch.drawBatch(pony.batch);
} else if (pony.palettePonyInfo !== undefined) {
let swimming = false;
if (isSwimmingState(pony.animator.state)) {
if (pony.animator.state === swimmingToFlying) {
swimming = pony.animator.time < 0.4;
} else {
swimming = true;
}
}
const createBatch = pony.vx === 0 && pony.vy === 0 && !swimming;
const right = isFacingRight(pony);
if (createBatch) {
batch.startBatch();
}
batch.save();
transformBatch(batch, pony);
const options = pony.drawingOptions;
options.flipped = right;
options.selected = pony.selected === true;
options.extra = pony.extra;
options.toy = pony.toy;
options.swimming = swimming;
options.shadow = !pony.swimming;
options.gameTime = drawOptions.gameTime + pony.id * 0.1;
options.shadowColor = drawOptions.shadowColor;
const ponyState = pony.ponyState;
drawPony(batch, pony.palettePonyInfo, ponyState, 0, 0, options);
if (
isAnimationPlaying(pony.zzzEffect) || isAnimationPlaying(pony.sneezeEffect) ||
isAnimationPlaying(pony.holdPoofEffect) || isAnimationPlaying(pony.heartsEffect) ||
isAnimationPlaying(pony.magicEffect)
) {
const { x, y } = getPonyHeadPosition(pony.ponyState, 0, 0);
const right = isFacingRight(pony);
const flip = right ? !ponyState.headTurned : ponyState.headTurned;
batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState));
drawAnimation(batch, pony.zzzEffect, 0, 0, WHITE, flip);
drawAnimation(batch, pony.sneezeEffect, 0, 0, WHITE, flip);
drawAnimation(batch, pony.holdPoofEffect, 0, 0, WHITE, flip);
drawAnimation(batch, pony.heartsEffect, 0, 0, HEARTS_COLOR, flip);
if (pony.magicEffect.currentAnimation !== undefined) {
drawAnimation(batch, pony.magicEffect, 0, 0, pony.magicColor, flip);
const sprite = sprites.magic3.frames[pony.magicEffect.frame];
sprite && batch.drawSprite(sprite, WHITE, pony.heartsEffect.palette, 0, 0);
}
}
batch.restore();
if (createBatch) {
pony.batch = batch.finishBatch();
pony.lastX = toScreenX(pony.x);
pony.lastY = toScreenYWithZ(pony.y, pony.z);
pony.lastRight = right;
pony.zzzEffect.dirty = false;
pony.cryEffect.dirty = false;
pony.sneezeEffect.dirty = false;
pony.holdPoofEffect.dirty = false;
pony.heartsEffect.dirty = false;
pony.magicEffect.dirty = false;
Object.assign(pony.lastState, ponyState);
}
}
}
const magickLightSizes = [
0, 1.02, // fade-in
0.97, 0.94, 0.91, 0.94, 0.97, 1.00, // loop
0.97, 0.94, 0.91, // fade-out
];
export function drawPonyEntityLight(batch: SpriteBatch, pony: Pony, options: DrawOptions) {
const ponyState = pony.ponyState;
const holding = ponyState.holding;
const drawHolding = holding !== undefined && holding.drawLight !== undefined;
const drawMagic = pony.magicEffect.currentAnimation !== undefined;
const draw = drawHolding || drawMagic;
if (draw) {
batch.save();
transformBatch(batch, pony);
const { x, y } = getPonyHeadPosition(ponyState, 0, 0);
batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState));
if (drawHolding) {
holding!.x = toWorldX(holding!.pickableX!);
holding!.y = toWorldY(holding!.pickableY!);
holding!.drawLight!(batch, options);
}
if (drawMagic) {
const size = 200 * (magickLightSizes[pony.magicEffect.frame] || 0);
batch.drawImage(WHITE, -1, -1, 2, 2, 30 - size / 2, 27 - size / 2, size, size);
}
batch.restore();
}
}
export function drawPonyEntityLightSprite(batch: SpriteBatch, pony: Pony, options: DrawOptions) {
const ponyState = pony.ponyState;
const holding = ponyState.holding;
const drawHolding = holding !== undefined && holding.drawLightSprite !== undefined;
// const drawMagic = pony.magicEffect.currentAnimation !== undefined;
const draw = drawHolding; // || drawMagic;
if (draw) {
batch.save();
transformBatch(batch, pony);
const { x, y } = getPonyHeadPosition(ponyState, 0, 0);
batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState));
if (drawHolding) {
holding!.x = toWorldX(holding!.pickableX!);
holding!.y = toWorldY(holding!.pickableY!);
holding!.drawLightSprite!(batch, options);
}
// if (drawMagic) {
// const frame = pony.magicEffect.frame;
// const sprite = sprites.magic2_light.frames[frame];
// batch.drawSprite(sprite, WHITE, 0, 0);
// }
batch.restore();
}
}
export function flagsToState(state: EntityState, moving: boolean, isSwimming: boolean): AnimatorState<BodyAnimation> {
const ponyState = state & EntityState.PonyStateMask;
if (isSwimming) {
return swimming;
} else if (moving) {
if (ponyState === EntityState.PonyFlying) {
return flying;
} else {
return trotting;
}
} else {
switch (ponyState) {
case EntityState.PonyStanding: return standing;
case EntityState.PonyWalking: return trotting;
case EntityState.PonyTrotting: return trotting;
case EntityState.PonySitting: return sitting;
case EntityState.PonyLying: return lying;
case EntityState.PonyFlying: return hovering;
default:
throw new Error(`Invalid pony state (${ponyState})`);
}
}
}
export function updatePonyEntity(pony: Pony, delta: number, gameTime: number, safe: boolean) {
// update state
const state = pony.ponyState;
const walking = pony.vx !== 0 || pony.vy !== 0;
const animationState = flagsToState(pony.state, walking, pony.swimming);
if (pony.inTheAirDelay > 0) {
pony.inTheAirDelay -= delta;
}
if (pony.doAction !== DoAction.None) {
switch (pony.doAction) {
case DoAction.Boop:
setAnimatorState(pony.animator, toBoopState(animationState) || animationState);
break;
case DoAction.Swing:
setAnimatorState(pony.animator, swinging);
break;
case DoAction.HoldPoof:
playAnimation(pony.holdPoofEffect, holdPoofAnimation);
break;
default:
if (DEVELOPMENT) {
console.error(`Invalid DoAction: ${pony.doAction}`);
}
}
pony.doAction = DoAction.None;
} else {
setAnimatorState(pony.animator, animationState);
}
// head
pony.headTime += delta;
if (pony.headAnimation !== undefined) {
const frame = Math.floor(pony.headTime * pony.headAnimation.fps);
if (frame >= pony.headAnimation.frames.length && !pony.headAnimation.loop) {
pony.headAnimation = undefined;
state.headAnimationFrame = 0;
} else {
state.headAnimationFrame = frame % pony.headAnimation.frames.length;
}
}
if (state.headAnimation !== pony.headAnimation) {
state.headAnimation = pony.headAnimation;
if (pony.headAnimation === sneeze) {
playAnimation(pony.sneezeEffect, sneezeAnimation);
}
}
// effects / expressions
if (pony.currentExpression !== pony.expr) {
updatePonyExpression(pony, pony.expr, safe);
}
if ((pony.state & EntityState.Magic) !== 0) {
playAnimation(pony.magicEffect, magicAnimation);
} else {
playAnimation(pony.magicEffect, undefined);
}
updateAnimation(pony.zzzEffect, delta);
updateAnimation(pony.cryEffect, delta);
updateAnimation(pony.sneezeEffect, delta);
updateAnimation(pony.holdPoofEffect, delta);
updateAnimation(pony.heartsEffect, delta);
updateAnimation(pony.magicEffect, delta);
// holding
const holdingUpdated =
state.holding !== undefined &&
state.holding.update !== undefined &&
state.holding.update(delta, gameTime);
// blink
pony.blinkTime += delta;
if ((pony.blinkTime - pony.nextBlink) > 1) {
pony.nextBlink = pony.blinkTime + Math.random() * 2 + 3;
}
// update animator
updateAnimator(pony.animator, delta);
// update state
const blinkFrame = Math.floor((pony.blinkTime - pony.nextBlink) * blinkFps);
state.blinkFrame = blinkFrame < BLINK_FRAMES.length ? BLINK_FRAMES[blinkFrame] : 1;
state.headTurned = (pony.state & EntityState.HeadTurned) !== 0;
state.animation = getAnimation(pony.animator) || stand;
state.animationFrame = getAnimationFrame(pony.animator);
// randomize animator time at startup
if (!pony.initialized) {
pony.initialized = true;
updateAnimator(pony.animator, Math.random() * 2);
}
// discard batch if outdated
if (pony.batch !== undefined) {
const options = pony.drawingOptions;
const right = isFacingRight(pony);
if (
holdingUpdated ||
toScreenX(pony.x) !== pony.lastX || toScreenYWithZ(pony.y, pony.z) !== pony.lastY ||
pony.lastRight !== right ||
pony.zzzEffect.dirty || pony.cryEffect.dirty || pony.sneezeEffect.dirty || pony.holdPoofEffect.dirty ||
pony.heartsEffect.dirty || pony.magicEffect.dirty ||
options.flipped !== right || options.selected !== pony.selected || options.extra !== pony.extra ||
options.toy !== pony.toy ||
!isStateEqual(pony.lastState, state)
) {
pony.discardBatch = true;
}
}
// update bounds
const flying = isPonyFlying(pony);
const flyingUpOrDown = isFlyingUpOrDown(pony.animator.state);
const flyingOrFlyingUpOrDown = flying || flyingUpOrDown;
pony.bounds = flyingOrFlyingUpOrDown ? boundsFly : bounds;
pony.interactBounds = flying ? interactBoundsFly : interactBounds;
pony.lightBounds = flyingOrFlyingUpOrDown ? lightBoundsFly : lightBounds;
pony.lightSpriteBounds = flyingOrFlyingUpOrDown ? lightBoundsFly : lightBounds;
}
export function updatePonyHold(pony: Pony, game: PonyTownGame) {
const ponyState = pony.ponyState;
const hadLight = hasDrawLight(pony);
const hadLightSprite = hasLightSprite(pony);
if (pony.hold !== 0) {
if (ponyState.holding === undefined) {
ponyState.holding = createAnEntity(pony.hold, 0, 0, 0, {}, pony.paletteManager, game);
} else if (ponyState.holding.type !== pony.hold) {
releaseEntity(ponyState.holding);
ponyState.holding = createAnEntity(pony.hold, 0, 0, 0, {}, pony.paletteManager, game);
}
} else if (ponyState.holding !== undefined) {
releaseEntity(ponyState.holding);
ponyState.holding = undefined;
}
const hasLight = hasDrawLight(pony);
const hasLightSprite1 = hasLightSprite(pony);
addOrRemoveFromEntityList(game.map.entitiesLight, pony, hadLight, hasLight);
addOrRemoveFromEntityList(game.map.entitiesLightSprite, pony, hadLightSprite, hasLightSprite1);
}
function filterExpression(expression: Expression) {
const extra = expression.extra;
const blush = hasFlag(extra, ExpressionExtra.Blush);
if (
blush ||
hasFlag(extra, ExpressionExtra.Hearts) ||
hasFlag(extra, ExpressionExtra.Cry) ||
isEyeSleeping(expression.left) ||
isEyeSleeping(expression.right)
) {
if (expression.muzzle === Muzzle.SmilePant || expression.muzzle === Muzzle.NeutralPant) {
expression.muzzle = Muzzle.Neutral;
}
}
if (
blush ||
expression.muzzle === Muzzle.SmilePant ||
expression.muzzle === Muzzle.NeutralPant
) {
if (expression.leftIris === Iris.Up || expression.rightIris === Iris.Up) {
expression.leftIris = Iris.Forward;
expression.rightIris = Iris.Forward;
}
if (expression.muzzle === Muzzle.SmilePant) {
expression.muzzle = Muzzle.SmileOpen;
} else if (expression.muzzle === Muzzle.NeutralPant) {
expression.muzzle = Muzzle.NeutralOpen2;
}
}
if (blush) {
if (expression.muzzle === Muzzle.SmileOpen2) {
expression.muzzle = Muzzle.SmileOpen;
} else if (expression.muzzle === Muzzle.FrownOpen) {
expression.muzzle = Muzzle.ConcernedOpen;
} else if (expression.muzzle === Muzzle.NeutralOpen2) {
expression.muzzle = Muzzle.Oh;
}
}
}
function updatePonyExpression(pony: Pony, expr: number, safe: boolean) {
const expression = decodeExpression(expr);
pony.currentExpression = pony.expr;
pony.ponyState.expression = expression;
if (expression && safe) {
filterExpression(expression);
}
const extra = (expression && expression.extra) || 0;
if (hasFlag(extra, ExpressionExtra.Cry)) {
playAnimation(pony.cryEffect, cryAnimation);
} else if (hasFlag(extra, ExpressionExtra.Tears)) {
playAnimation(pony.cryEffect, tearsAnimation);
} else {
playAnimation(pony.cryEffect, undefined);
}
if (hasFlag(extra, ExpressionExtra.Zzz)) {
playOneOfAnimations(pony.zzzEffect, zzzAnimations);
} else {
playAnimation(pony.zzzEffect, undefined);
}
if (hasFlag(extra, ExpressionExtra.Hearts)) {
playAnimation(pony.heartsEffect, heartsAnimation);
} else {
playAnimation(pony.heartsEffect, undefined);
}
}
function transformBatch(batch: SpriteBatch | PaletteSpriteBatch, entity: Entity) {
batch.translate(toScreenX(entity.x), toScreenYWithZ(entity.y, entity.z));
batch.scale(isFacingRight(entity) ? -1 : 1, 1);
}
function releasePalettePonyInfo(pony: Pony) {
if (pony.palettePonyInfo !== undefined) {
releasePalettes(pony.palettePonyInfo);
pony.palettePonyInfo = undefined;
}
}
function makeLightBounds({ x, y, w, h }: Rect) {
return rect(x - lightExtentX, y - lightExtentY, w + lightExtentX * 2, h + lightExtentY * 2);
}
function drawFaceExtra(batch: PaletteSpriteBatch, pony: Pony) {
if (isAnimationPlaying(pony.cryEffect)) {
const flip = isFacingRight(pony) ? !pony.ponyState.headTurned : pony.ponyState.headTurned;
const maxY = isPonyLying(pony) ? 62 : (isPonySitting(pony) ? 65 : 0);
drawAnimation(batch, pony.cryEffect, 0, 0, WHITE, flip, maxY);
}
}
+587
View File
@@ -0,0 +1,587 @@
import * as sprites from '../generated/sprites';
import { releasePalette, createPalette } from '../graphics/paletteManager';
import {
PonyInfo, SpriteSet, PalettePonyInfo, PaletteSpriteSet, PaletteManager, Palette, ColorExtraSets, PonyInfoBase,
PonyInfoNumber, ColorExtra
} from './interfaces';
import { toInt, array, includes, att } from './utils';
import { CM_SIZE } from './constants';
import { parseColorFast, getR, getAlpha, colorFromRGBA, getG, getB, colorToHexRGB } from './color';
import { BLACK, fillToOutline, fillToOutlineColor, WHITE, TRANSPARENT, fillToOutlineWithDarken } from './colors';
import {
mergedManes, mergedBackManes, mergedFacialHair, mergedEarAccessories, mergedChestAccessories,
SLEEVED_ACCESSORIES, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories
} from '../client/ponyUtils';
const MAX_COLORS = 6;
const FILLS = ['1e90ff', '32cd32', 'da70d6', 'dc143c', '7fffd4'];
const frontHooves = sprites.frontLegHooves[1] as ColorExtraSets;
const backHooves = sprites.backLegHooves[1] as ColorExtraSets;
const frontLegAccessories = sprites.frontLegAccessories[1] as ColorExtraSets;
const backLegAccessories = sprites.backLegAccessories[1] as ColorExtraSets;
const frontLegSleeves = sprites.frontLegSleeves[1] as ColorExtraSets;
type Arr<T> = (T | undefined)[] | undefined;
type PonyInfoGeneric<T> = PonyInfoBase<T, SpriteSet<T>>;
export const mockPaletteManager: PaletteManager = {
add(colors: number[]): Palette {
return this.addArray(new Uint32Array(colors));
},
addArray(colors: Uint32Array): Palette {
return createPalette(colors);
},
init() {
}
};
export function spriteSet(type: number, lockFirstFill = true, fill = 'ffd700', otherFills = FILLS): SpriteSet<string> {
if (otherFills.length !== (MAX_COLORS - 1))
throw new Error('Invalid fills count');
const fills = [fill, ...otherFills];
const outlines = fills.map(fillToOutline);
return {
type,
pattern: 0,
fills,
outlines,
lockFills: [lockFirstFill, ...array(MAX_COLORS - 1, false)],
lockOutlines: array(MAX_COLORS, true),
};
}
export function createDefaultPony(): PonyInfo {
const pony = createBasePony();
pony.mane!.type = 2;
pony.backMane!.type = 1;
pony.tail!.type = 1;
return pony;
}
export function createBasePony(): PonyInfo {
return syncLockedPonyInfo({
head: spriteSet(0, true, 'ff0000', ['800000', '32cd32', 'da70d6', 'dc143c', '7fffd4']),
nose: spriteSet(0, true, 'ff0000', ['800000', '32cd32', 'da70d6', 'dc143c', '7fffd4']),
ears: spriteSet(0, true, 'ff0000'),
horn: spriteSet(0, true, 'ff0000'),
wings: spriteSet(0, true, 'ff0000'),
frontHooves: spriteSet(0, false, 'ffa500', ['ffff00', '32cd32', 'da70d6', 'dc143c', '7fffd4']),
backHooves: spriteSet(0, true, 'ffa500'),
mane: spriteSet(0, false),
backMane: spriteSet(0),
tail: spriteSet(0),
facialHair: spriteSet(0),
headAccessory: spriteSet(0, false, 'ee82ee'),
earAccessory: spriteSet(0, false, '808080'),
faceAccessory: spriteSet(0, false, '000000'),
neckAccessory: spriteSet(0, false, 'ee82ee'),
frontLegAccessory: spriteSet(0, false, 'ee82ee'),
backLegAccessory: spriteSet(0, false, 'ee82ee'),
frontLegAccessoryRight: spriteSet(0, false, 'ee82ee'),
backLegAccessoryRight: spriteSet(0, false, 'ee82ee'),
lockBackLegAccessory: true,
unlockFrontLegAccessory: false,
unlockBackLegAccessory: false,
backAccessory: spriteSet(0, false, 'ee82ee'),
waistAccessory: spriteSet(0, false, '95856f', ['674b43', '4f4f4f', '525252', 'c37850', '8a3d34']),
chestAccessory: spriteSet(0, false, 'ee82ee'),
sleeveAccessory: spriteSet(0, true, 'ee82ee'),
extraAccessory: {
...spriteSet(0, true, 'ff0000', ['daa520', 'ffd700', 'ffd700', 'ffd700', 'ffd700']),
lockFills: array(5, true),
},
coatFill: 'ff0000',
coatOutline: '8b0000',
lockCoatOutline: true,
eyelashes: 0,
eyeColorLeft: 'daa520',
eyeColorRight: 'daa520',
eyeWhitesLeft: 'ffffff',
eyeWhites: 'ffffff',
eyeOpennessLeft: 1,
eyeOpennessRight: 1,
eyeshadow: false,
eyeshadowColor: '000000',
lockEyes: true,
lockEyeColor: true,
unlockEyeWhites: false,
unlockEyelashColor: false,
eyelashColor: '000000',
eyelashColorLeft: '000000',
fangs: 0,
muzzle: 0,
freckles: 0,
frecklesColor: '8b0000',
magicColor: 'ffffff',
cm: [],
cmFlip: false,
customOutlines: false,
freeOutlines: false,
darkenLockedOutlines: false,
});
}
// sync
type FillToOutline<T> = (fill: T | undefined) => T | undefined;
export function getBaseFill<T>(set?: SpriteSet<T>): T | undefined {
return set && set.fills && set.fills[0];
}
export function getBaseOutline<T>(set?: SpriteSet<T>): T | undefined {
return set && set.outlines && set.outlines[0];
}
export function syncLockedSpriteSet<T>(
set: SpriteSet<T> | undefined, customOutlines: boolean, fillToOutline: FillToOutline<T>, baseFill?: T,
baseOutline?: T
) {
if (set === undefined)
return;
const fills = set.fills;
if (!fills)
return;
const lockFills = set.lockFills;
if (lockFills) {
for (let i = 0; i < lockFills.length; i++) {
if (lockFills[i]) {
fills[i] = i === 0 ? baseFill : fills[0];
}
}
}
const outlines = set.outlines;
const lockOutlines = set.lockOutlines;
if (outlines && lockOutlines) {
for (let i = 0; i < lockOutlines.length; i++) {
if (!customOutlines) {
lockOutlines[i] = true;
}
if (lockOutlines[i]) {
if (i === 0 && baseOutline && lockFills && lockFills[i]) {
outlines[i] = baseOutline;
} else {
outlines[i] = fillToOutline(fills[i]);
}
}
}
}
}
function syncLockedSpritesSet2<T>(
set: SpriteSet<T> | undefined, fillToOutline: FillToOutline<T>, baseFills: (T | undefined)[],
baseOutlines: (T | undefined)[]
) {
if (set && set.fills && set.lockFills) {
set.lockFills.forEach((locked, i) => {
if (locked) {
set.fills![i] = baseFills[i];
}
});
}
if (set && set.fills && set.outlines && set.lockOutlines) {
set.lockOutlines.forEach((locked, i) => {
if (locked) {
if (baseOutlines[i] && set.lockFills && set.lockFills[i]) {
set.outlines![i] = baseOutlines[i];
} else {
set.outlines![i] = fillToOutline(set.fills![i]);
}
}
});
}
}
function getFillOf2<T>(set: SpriteSet<T> | undefined, defaultColor: T): T | undefined {
return set && set.type && set.fills && set.fills[0] || defaultColor;
}
function getOutlineOf2<T>(set: SpriteSet<T> | undefined, defaultColor: T): T | undefined {
return set && set.type && set.outlines && set.outlines[0] || defaultColor;
}
function syncLockedBasePonyInfo<T>(
info: PonyInfoGeneric<T>, fillToOutline: FillToOutline<T>, defaultColor: T
): PonyInfoGeneric<T> {
const customOutlines = !!info.customOutlines;
if (!customOutlines || info.lockCoatOutline) {
info.coatOutline = fillToOutline(info.coatFill);
}
if (info.lockEyes) {
info.eyeOpennessLeft = info.eyeOpennessRight;
}
if (info.lockEyeColor) {
info.eyeColorLeft = info.eyeColorRight;
}
if (!info.unlockEyeWhites) {
info.eyeWhitesLeft = info.eyeWhites;
}
if (!info.unlockEyelashColor) {
info.eyelashColorLeft = info.eyelashColor;
}
syncLockedSpriteSet<T>(info.head, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
syncLockedSpriteSet<T>(info.nose, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
syncLockedSpriteSet<T>(info.ears, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
syncLockedSpriteSet<T>(info.horn, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
syncLockedSpriteSet<T>(info.wings, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
syncLockedSpriteSet<T>(info.frontHooves, customOutlines, fillToOutline, info.coatFill, info.coatOutline);
syncLockedSpriteSet<T>(
info.backHooves, customOutlines, fillToOutline, getBaseFill(info.frontHooves), getBaseOutline(info.frontHooves));
syncLockedSpriteSet<T>(info.mane, customOutlines, fillToOutline);
const baseManeFill = getBaseFill(info.mane);
const baseManeOutline = getBaseOutline(info.mane);
syncLockedSpriteSet<T>(info.backMane, customOutlines, fillToOutline, baseManeFill, baseManeOutline);
syncLockedSpriteSet<T>(info.tail, customOutlines, fillToOutline, baseManeFill, baseManeOutline);
syncLockedSpriteSet<T>(info.facialHair, customOutlines, fillToOutline, baseManeFill, baseManeOutline);
syncLockedSpriteSet<T>(info.headAccessory, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.earAccessory, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.faceAccessory, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.neckAccessory, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.frontLegAccessory, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.backLegAccessory, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.frontLegAccessoryRight, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.backLegAccessoryRight, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.backAccessory, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.waistAccessory, customOutlines, fillToOutline);
syncLockedSpriteSet<T>(info.chestAccessory, customOutlines, fillToOutline);
if (info.chestAccessory && !info.sleeveAccessory && includes(SLEEVED_ACCESSORIES, info.chestAccessory.type)) {
info.sleeveAccessory = {
type: 0,
pattern: 0,
fills: [],
outlines: [],
lockFills: array(MAX_COLORS, true),
lockOutlines: array(MAX_COLORS, true),
};
}
syncLockedSpriteSet<T>(
info.sleeveAccessory, customOutlines, fillToOutline, getBaseFill(info.chestAccessory), getBaseOutline(info.chestAccessory));
syncLockedSpritesSet2<T>(info.extraAccessory, fillToOutline, [
info.coatFill,
info.eyeColorRight,
getFillOf2(info.mane, defaultColor),
getFillOf2(info.backMane, defaultColor),
getFillOf2(info.tail, defaultColor),
], [
info.coatOutline,
info.eyeColorRight,
getOutlineOf2(info.mane, defaultColor),
getOutlineOf2(info.backMane, defaultColor),
getOutlineOf2(info.tail, defaultColor),
]);
return info;
}
export function syncLockedPonyInfo(info: PonyInfo): PonyInfo {
const darkenLocked = !!info.freeOutlines && !!info.darkenLockedOutlines;
const fillToOutlineFunc = darkenLocked ? fillToOutlineWithDarken : fillToOutline;
return syncLockedBasePonyInfo<string>(info, fillToOutlineFunc, '000000');
}
function fillToOutlineSafe(color: number | undefined) {
return fillToOutlineColor((color === undefined || color === 0) ? BLACK : color);
}
function fillToOutlineSafeWithDarken(color: number | undefined) {
return darkenForOutline(fillToOutlineColor((color === undefined || color === 0) ? BLACK : color));
}
export function syncLockedPonyInfoNumber(info: PonyInfoNumber): PonyInfoNumber {
const darkenLocked = !!info.freeOutlines && !!info.darkenLockedOutlines;
const fillToOutlineFunc = darkenLocked ? fillToOutlineSafeWithDarken : fillToOutlineSafe;
return syncLockedBasePonyInfo<number>(info, fillToOutlineFunc, BLACK);
}
// PalettePonyInfo
function parseFast(color: string | undefined): number {
return color ? parseColorFast(color) : BLACK;
}
function parseCMColor(color: string): number {
return color ? parseColorFast(color) : TRANSPARENT;
}
export function toColorList(colors: (string | undefined)[]): Uint32Array {
const result = new Uint32Array(colors.length + 1);
for (let i = 0; i < colors.length; i++) {
result[i + 1] = parseFast(colors[i]);
}
return result;
}
export function darkenForOutline(color: number) {
const mult = (159 / 255);
const r = (mult * getR(color)) | 0;
const g = (mult * getG(color)) | 0;
const b = (mult * getB(color)) | 0;
const a = getAlpha(color);
return colorFromRGBA(r, g, b, a);
}
function getColorsGeneric(
fillColors: Arr<string>, outlineColors: Arr<string>, defaultColor: string, length: number, darken: boolean
): string[] {
const fills = fillColors || [];
const outlines = outlineColors || [];
const colors = array(length * 2, defaultColor);
for (let i = 0; i < length; i++) {
colors[i * 2] = fills[i] || defaultColor;
if (darken) {
colors[i * 2 + 1] = outlines[i] ? colorToHexRGB(darkenForOutline(parseColorFast(outlines[i]!))) : defaultColor;
} else {
colors[i * 2 + 1] = outlines[i] || defaultColor;
}
}
return colors;
}
export function getColorsFromSet({ fills, outlines }: SpriteSet<string>, defaultColor: string, darken: boolean): string[] {
const length = Math.max(fills ? fills.length : 0, outlines ? outlines.length : 0);
return getColorsGeneric(fills, outlines, defaultColor, length, darken);
}
export function toColorListNumber(colors: (number | undefined)[]): Uint32Array {
const result = new Uint32Array(colors.length + 1);
for (let i = 0; i < colors.length; i++) {
result[i + 1] = colors[i] || BLACK;
}
return result;
}
export type GetColorsForSet<T> = (set: SpriteSet<T>, count: number, darken: boolean) => Uint32Array;
export const getColorsForSet: GetColorsForSet<string> = (set, count, darken) => {
const t = getColorsGeneric(set.fills, set.outlines, '000000', count, darken);
return toColorList(t);
};
const emptyArray: number[] = [];
export const getColorsForSetNumber: GetColorsForSet<number> = (set, length, darken) => {
const fills = set.fills || emptyArray;
const outlines = set.outlines || emptyArray;
const result = new Uint32Array(length * 2 + 1);
for (let i = 0; i < length; i++) {
result[((i << 1) + 1) | 0] = i < fills.length ? (fills[i] || BLACK) : BLACK;
if (darken) {
result[((i << 1) + 2) | 0] = i < outlines.length ? darkenForOutline(outlines[i] || BLACK) : BLACK;
} else {
result[((i << 1) + 2) | 0] = i < outlines.length ? (outlines[i] || BLACK) : BLACK;
}
}
return result;
};
function getExtraPalette(pattern: ColorExtra | undefined, manager: PaletteManager): Palette | undefined {
const extraPalette = pattern && pattern.palettes && pattern.palettes[0];
return extraPalette && manager.addArray(new Uint32Array(extraPalette));
}
export function toPaletteSet<T>(
set: SpriteSet<T>, sets: ColorExtraSets, manager: PaletteManager, getColorsForSet: GetColorsForSet<T>,
hasExtra: boolean, darken: boolean
): PaletteSpriteSet | undefined {
const pattern = att(att(sets, set.type), set.pattern);
const colorCount = pattern !== undefined && pattern.colors !== undefined ? ((pattern.colors - 1) >> 1) : 0;
const colors = getColorsForSet(set, colorCount, darken);
return {
type: toInt(set.type),
pattern: toInt(set.pattern),
palette: manager.addArray(colors),
extraPalette: hasExtra ? getExtraPalette(pattern, manager) : undefined,
};
}
function createCMPalette<T>(
cm: T[] | undefined, manager: PaletteManager, parseColor: (color: T) => number
): Palette | undefined {
const size = CM_SIZE * CM_SIZE;
if (cm === undefined || cm.length === 0 || cm.length > size)
return undefined;
const result = new Uint32Array(size);
for (let i = 0; i < cm.length; i++) {
result[i] = parseColor(cm[i]);
}
return manager.addArray(result);
}
export type ToSet<T> = (set: SpriteSet<T> | undefined, sets: ColorExtraSets, extra?: boolean) => PaletteSpriteSet | undefined;
const defaultPalette = new Uint32Array(sprites.defaultPalette);
export const createToPaletteSet =
<T>(manager: PaletteManager, getColorsForSet: GetColorsForSet<T>, extra: boolean, darken: boolean): ToSet<T> =>
(set, sets) => set === undefined ? undefined : toPaletteSet(set, sets, manager, getColorsForSet, extra, darken);
export function toPaletteGeneric<T>(
info: PonyInfoGeneric<T>, manager: PaletteManager, toColorList: (color: (T | undefined)[]) => Uint32Array,
getColorsForSet: GetColorsForSet<T>, blackColor: T, whiteColor: T, parseCMColor: (color: T) => number
): PalettePonyInfo {
const darken = !info.freeOutlines;
const toSet = createToPaletteSet(manager, getColorsForSet, false, darken);
const toSetExtra = createToPaletteSet(manager, getColorsForSet, true, darken);
const defaultSet = { type: 0, pattern: 0, fills: [info.coatFill], outlines: [info.coatOutline] };
// const defaultSet = { type: 0, pattern: 1, fills: [info.coatFill, whiteColor], outlines: [info.coatOutline, blackColor] };
return {
body: toSet(defaultSet, sprites.body[1]),
head: toSet(info.head || defaultSet, sprites.head0[1]),
nose: toSet(info.nose, sprites.noses[0]),
ears: toSet(info.ears || defaultSet, sprites.ears),
horn: toSet(info.horn, sprites.horns),
wings: toSet(info.wings, sprites.wings[0]),
frontLegs: toSet(defaultSet, sprites.frontLegs[1]),
backLegs: toSet(defaultSet, sprites.backLegs[1]),
frontHooves: toSet(info.frontHooves, frontHooves),
backHooves: toSet(info.backHooves, backHooves),
mane: toSet(info.mane, mergedManes),
backMane: toSet(info.backMane, mergedBackManes),
tail: toSet(info.tail, sprites.tails[0]),
facialHair: toSet(info.facialHair, mergedFacialHair),
headAccessory: toSet(info.headAccessory, mergedHeadAccessories),
earAccessory: toSet(info.earAccessory, mergedEarAccessories),
faceAccessory: toSetExtra(info.faceAccessory, sprites.faceAccessories),
// faceAccessoryExtraPalette: getExtraPartPalette(info.faceAccessory, sprites.faceAccessoriesExtra, manager),
neckAccessory: toSet(info.neckAccessory, sprites.neckAccessories[1]),
frontLegAccessory: toSet(
info.frontLegAccessory, frontLegAccessories),
backLegAccessory: toSet(
info.lockBackLegAccessory ? info.frontLegAccessory : info.backLegAccessory, backLegAccessories),
frontLegAccessoryRight: toSet(
info.unlockFrontLegAccessory ? info.frontLegAccessoryRight : info.frontLegAccessory, frontLegAccessories),
backLegAccessoryRight: toSet(
info.lockBackLegAccessory ?
(info.unlockFrontLegAccessory ? info.frontLegAccessoryRight : info.frontLegAccessory) :
(info.unlockBackLegAccessory ? info.backLegAccessoryRight : info.backLegAccessory), backLegAccessories),
lockBackLegAccessory: info.lockBackLegAccessory,
unlockFrontLegAccessory: info.unlockFrontLegAccessory,
unlockBackLegAccessory: info.unlockBackLegAccessory,
backAccessory: toSet(info.backAccessory, mergedBackAccessories),
waistAccessory: toSet(info.waistAccessory, sprites.waistAccessories[1]),
chestAccessory: toSet(info.chestAccessory, mergedChestAccessories),
sleeveAccessory: toSet(info.sleeveAccessory, frontLegSleeves),
extraAccessory: toSet(info.extraAccessory, mergedExtraAccessories),
coatPalette: manager.addArray(toColorList([info.coatFill, info.coatOutline])),
coatFill: undefined,
coatOutline: undefined,
lockCoatOutline: !!info.lockCoatOutline,
eyelashes: toInt(info.eyelashes),
eyePaletteLeft: manager.addArray(toColorList([
info.eyeWhitesLeft || whiteColor,
info.eyelashColor || blackColor
])),
eyePalette: manager.addArray(toColorList([
info.eyeWhites || whiteColor,
(info.unlockEyelashColor ? info.eyelashColorLeft : info.eyelashColor) || blackColor
])),
eyeColorLeft: manager.addArray(toColorList([info.eyeColorLeft])),
eyeColorRight: manager.addArray(toColorList([info.eyeColorRight])),
eyeWhitesLeft: undefined,
eyeWhites: undefined,
eyeOpennessLeft: toInt(info.eyeOpennessLeft),
eyeOpennessRight: toInt(info.eyeOpennessRight),
eyeshadow: info.eyeshadow,
eyeshadowColor: manager.addArray(toColorList([info.eyeshadowColor])),
lockEyes: !!info.lockEyes,
lockEyeColor: !!info.lockEyeColor,
unlockEyeWhites: !!info.unlockEyeWhites,
unlockEyelashColor: !!info.unlockEyelashColor,
eyelashColor: undefined,
eyelashColorLeft: undefined,
fangs: toInt(info.fangs),
muzzle: toInt(info.muzzle),
freckles: 0, // remove
frecklesColor: undefined, // TODO: remove
magicColor: undefined,
magicColorValue: typeof info.magicColor === 'string' ? parseColorFast(info.magicColor) : toInt(info.magicColor),
cm: undefined,
cmFlip: !!info.cmFlip,
cmPalette: createCMPalette<T>(info.cm, manager, parseCMColor),
customOutlines: !!info.customOutlines,
freeOutlines: !!info.freeOutlines,
darkenLockedOutlines: !!info.darkenLockedOutlines,
defaultPalette: manager.addArray(defaultPalette),
waterPalette: manager.addArray(sprites.pony_wake_1.palette),
};
}
export function toPalette(info: PonyInfo, manager = mockPaletteManager): PalettePonyInfo {
return toPaletteGeneric(info, manager, toColorList, getColorsForSet, '000000', 'ffffff', parseCMColor);
}
export function toPaletteNumber(info: PonyInfoNumber, manager = mockPaletteManager): PalettePonyInfo {
return toPaletteGeneric<number>(info, manager, toColorListNumber, getColorsForSetNumber, BLACK, WHITE, x => x);
}
export function releasePalettes(info: PalettePonyInfo): void {
for (const key of Object.keys(info)) {
const value = (info as any)[key]; // undefined | number | string | PaletteSpriteSet | Palette;
if (value && typeof value === 'object') {
if ('refs' in value) {
const palette = value as Palette;
releasePalette(palette);
} else if ('palette' in value) {
const set = value as PaletteSpriteSet;
releasePalette(set.palette);
releasePalette(set.extraPalette);
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
import { tileWidth, tileHeight, tileElevation } from './constants';
import { Point, Rect } from './interfaces';
export function toScreenX(x: number) {
return Math.floor(x * tileWidth) | 0;
}
export function toScreenY(y: number) {
return Math.floor(y * tileHeight) | 0;
}
export function toScreenYWithZ(y: number, z: number) {
return Math.floor(y * tileHeight - z * tileElevation) | 0;
}
export function toWorldX(x: number) {
return x / tileWidth;
}
export function toWorldY(y: number) {
return y / tileHeight;
}
export function toWorldZ(z: number) {
return z / tileElevation;
}
export function pointToScreen({ x, y }: Point): Point {
return {
x: toScreenX(x),
y: toScreenY(y),
};
}
export function pointToWorld({ x, y }: Point): Point {
return {
x: toWorldX(x),
y: toWorldY(y),
};
}
export function rectToScreen({ x, y, w, h }: Rect): Rect {
return {
x: toScreenX(x),
y: toScreenY(y),
w: toScreenX(w),
h: toScreenY(h),
};
}
export function roundPositionX(x: number) {
return Math.floor(x * tileWidth) / tileWidth;
}
export function roundPositionY(y: number) {
return Math.floor(y * tileHeight) / tileHeight;
}
export function roundPositionXMidPixel(x: number) {
return (Math.floor(x * tileWidth) + 0.5) / tileWidth;
}
export function roundPositionYMidPixel(y: number) {
return (Math.floor(y * tileHeight) + 0.5) / tileHeight;
}
export function roundPosition(point: Point) {
point.x = roundPositionX(point.x);
point.y = roundPositionY(point.y);
}
+50
View File
@@ -0,0 +1,50 @@
import { Rect, Point } from './interfaces';
import { intersect } from './utils';
export function rect(x: number, y: number, w: number, h: number): Rect {
return { x, y, w, h };
}
export function centerPoint(rect: Rect): Point {
return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 };
}
export function copyRect(dst: Rect, src: Rect) {
dst.x = src.x;
dst.y = src.y;
dst.w = src.w;
dst.h = src.h;
}
export function withBorder({ x, y, w, h }: Rect, border: number) {
return rect(x - border, y - border, w + border * 2, h + border * 2);
}
export function withPadding({ x, y, w, h }: Rect, top: number, right: number, bottom: number, left: number) {
return rect(x - top, y - left, w + left + right, h + top + bottom);
}
export function rectsIntersect(a: Rect, b: Rect): boolean {
return intersect(a.x, a.y, a.w, a.h, b.x, b.y, b.w, b.h);
}
export function addRect(a: Rect, b: Rect) {
const x = Math.min(a.x, b.x);
const y = Math.min(a.y, b.y);
a.w = Math.max(a.x + a.w, b.x + b.w) - x;
a.h = Math.max(a.y + a.h, b.y + b.h) - y;
a.x = x;
a.y = y;
}
export function addRects(a: Rect, b: Rect): Rect {
const x = Math.min(a.x, b.x);
const y = Math.min(a.y, b.y);
return {
x, y,
w: Math.max(a.x + a.w, b.x + b.w) - x,
h: Math.max(a.y + a.h, b.y + b.h) - y,
};
}
+202
View File
@@ -0,0 +1,202 @@
import { TileType, Region, IMap } from './interfaces';
import { clamp } from './utils';
import { tileWidth, tileHeight, REGION_SIZE, REGION_WIDTH, REGION_HEIGHT } from './constants';
import { getRegion } from './worldMap';
import { toScreenX, toScreenY } from './positionUtils';
import { ponyColliders, ponyCollidersBounds } from './mixins';
import { decompressTiles } from './compress';
const { min, max, floor } = Math;
export function createRegion(x: number, y: number, tileData?: Uint8Array): Region {
const size = REGION_SIZE;
const tiles = tileData ? decompressTiles(tileData) : new Uint8Array(size * size);
const tileIndices = new Int16Array(size * size);
const randoms = new Uint8Array(size * size);
// const elevation = new Uint8Array(size * size);
const collider = new Uint8Array(size * size * tileWidth * tileHeight);
if (!tileData) {
tiles.fill(TileType.Dirt);
}
tileIndices.fill(-1);
for (let i = 0; i < randoms.length; i++) {
randoms[i] = (Math.random() * 256) | 0;
}
return {
x, y, tiles, tileIndices,
randoms,
// elevation,
entities: [],
colliders: [],
collider,
colliderDirty: true,
tilesDirty: true,
};
}
export function getRegionTile(region: Region, x: number, y: number): TileType {
return region.tiles[x | (y << 3)];
}
export function setRegionTile(region: Region, x: number, y: number, type: TileType) {
region.tiles[x | (y << 3)] = type;
}
export function getRegionTileIndex(region: Region, x: number, y: number) {
return region.tileIndices[x | (y << 3)];
}
export function setRegionTileDirty(region: Region, x: number, y: number) {
region.tileIndices[x | (y << 3)] = -1;
region.tilesDirty = true;
}
export function getRegionElevation(_region: Region, _x: number, _y: number) {
return 0; // region.elevation[x | (y << 3)];
}
export function setRegionElevation(_region: Region, _x: number, _y: number, _value: number) {
// region.elevation[x | (y << 3)] = value;
}
export function worldToRegionX<T>(x: number, map: IMap<T>) {
return clamp(floor(x / REGION_SIZE), 0, map.regionsX - 1);
}
export function worldToRegionY<T>(y: number, map: IMap<T>) {
return clamp(floor(y / REGION_SIZE), 0, map.regionsY - 1);
}
export function invalidateRegionsCollider<T extends Region | undefined>(region: Region, map: IMap<T>) {
const minY = max(0, region.y - 1);
const maxY = min(map.regionsY - 1, region.y + 1);
const minX = max(0, region.x - 1);
const maxX = min(map.regionsX - 1, region.x + 1);
for (let ry = minY; ry <= maxY; ry++) {
for (let rx = minX; rx <= maxX; rx++) {
const r = getRegion(map, rx, ry);
if (r) {
r.colliderDirty = true;
}
}
}
}
export function generateRegionCollider<T extends Region | undefined>(region: Region, map: IMap<T>) {
const regionCollider = region.collider;
const tileTypes = region.tiles;
region.colliderDirty = false;
regionCollider.fill(0);
for (let ty = 0, i = 0; ty < REGION_SIZE; ty++) {
for (let tx = 0; tx < REGION_SIZE; tx++ , i++) {
const type = tileTypes[i];
if (type === TileType.None) {
const x0 = (tx * tileWidth) | 0;
const y0 = (ty * tileHeight) | 0;
const x1 = (x0 + tileWidth) | 0;
const y1 = (y0 + tileHeight) | 0;
for (let y = y0; y < y1; y++) {
for (let x = x0; x < x1; x++) {
regionCollider[(x + ((y * REGION_WIDTH) | 0)) | 0] = 3;
}
}
}
}
}
const minY = max(0, region.y - 1);
const maxY = min(map.regionsY - 1, region.y + 1);
const minX = max(0, region.x - 1);
const maxX = min(map.regionsX - 1, region.x + 1);
const pBounds = ponyCollidersBounds;
const pbX0 = pBounds.x | 0;
const pbY0 = pBounds.y | 0;
const pbX1 = (pbX0 + pBounds.w) | 0;
const pbY1 = (pbY0 + pBounds.h) | 0;
const baseX = region.x * REGION_SIZE;
const baseY = region.y * REGION_SIZE;
for (let ry = minY; ry <= maxY; ry++) {
for (let rx = minX; rx <= maxX; rx++) {
const r = getRegion(map, rx, ry);
if (r === undefined)
continue;
for (const entity of r.colliders) {
const entityX = toScreenX(entity.x - baseX) | 0;
const entityY = toScreenY(entity.y - baseY) | 0;
const cBounds = entity.collidersBounds!;
const ecbX = entityX + cBounds.x;
const ecbY = entityY + cBounds.y;
if (
(ecbX + pbX0) > REGION_WIDTH || (ecbY + pbY0) > REGION_HEIGHT ||
(ecbX + cBounds.w + pbX1) < 0 || (ecbY + cBounds.h + pbY1) < 0
) {
continue;
}
for (const c of entity.colliders!) {
const value = (c.tall ? 3 : 1) | 0;
const baseX0 = (entityX + c.x) | 0;
const baseY0 = (entityY + c.y) | 0;
const baseX1 = (baseX0 + c.w) | 0;
const baseY1 = (baseY0 + c.h) | 0;
if (c.exact) {
const x0 = (baseX0 < 0 ? 0 : baseX0) | 0;
const y0 = (baseY0 < 0 ? 0 : baseY0) | 0;
const x1 = (baseX1 > REGION_WIDTH ? REGION_WIDTH : baseX1) | 0;
const y1 = (baseY1 > REGION_HEIGHT ? REGION_HEIGHT : baseY1) | 0;
if (x1 > x0 && y1 > y0) {
for (let y = y0 | 0; y < y1; y = (y + 1) | 0) {
const oy = (y * REGION_WIDTH) | 0;
for (let x = x0 | 0; x < x1; x = (x + 1) | 0) {
regionCollider[(x + oy) | 0] |= value;
}
}
}
} else {
for (const pc of ponyColliders) {
const tx0 = (baseX0 + pc.x) | 0;
const ty0 = (baseY0 + pc.y) | 0;
const tx1 = (baseX1 + ((pc.x + pc.w) | 0)) | 0;
const ty1 = (baseY1 + ((pc.y + pc.h) | 0)) | 0;
const x0 = (tx0 < 0 ? 0 : tx0) | 0;
const y0 = (ty0 < 0 ? 0 : ty0) | 0;
const x1 = (tx1 > REGION_WIDTH ? REGION_WIDTH : tx1) | 0;
const y1 = (ty1 > REGION_HEIGHT ? REGION_HEIGHT : ty1) | 0;
if (x1 > x0 && y1 > y0) {
for (let y = y0 | 0; y < y1; y = (y + 1) | 0) {
const oy = (y * REGION_WIDTH) | 0;
for (let x = x0 | 0; x < x1; x = (x + 1) | 0) {
regionCollider[(x + oy) | 0] |= value;
}
}
}
}
}
}
}
}
}
}
+102
View File
@@ -0,0 +1,102 @@
import { escapeRegExp } from 'lodash';
import { LogArgument } from 'rollbar';
import { CHARACTER_LIMIT_ERROR } from './errors';
const IGNORE = new RegExp([
// adware / extensions
'plantsandplay', 'anyplacetrivial', 'surfbuyermac', 'hotshoppymac', 'GM_getValue', '__gCrWeb.autofill',
'.com/affs', 'advpartners', 'tlscdn', 'yaaknaa', 'mecash', 'digitaloptout',
'Script error', 'NS_ERROR_', 'davebestdeals', 'mflcdn', `'feedConf' of null`, 'n46gd0nenr1az.ru',
'googst2.ru', 'downloader12.ru', 'adsafeprotected', 'gobobr.info', 'elt.parentNode',
`getElementsByTagName('video')`, 'chrome-extension', 'bestpriceninja', `'tgt' of null`,
'jh8hrfnvs.ru', 'OperaIce', 'blueblockgames', 'adguard.com', 'kaspersky', 'igamesecrets.com',
'Unexpected identifier', 'UnknownError', 'diableNightMode', 'Unexpected end of script',
'Internal Server Error', 'hilitor', 'kejnoj7.ru', 'v207.info', 'inj_js',
'object is not a function', `'Float32Array' is undefined`, 'vertamedia', '.ru/', 'v24s.net',
'window.document.location is null', 'mediaonspot', 'ydpi.pw', 'moz-extension', 'trafficanalytics',
'amazonaws', 'adtelligent', 'searchsens.info', 'solid-waste.top', 'cdn.immereeako.info',
'technologiecoloniale.com', 'cloudcnfare.com', 'MyAppGet', 'rugged-r.top', `Can't find variable: webkit`,
`rgvqcsxqge.com`, 'all_small_polls', `Cannot read property 'document' of undefined`, `extAbbr is not defined`,
'__gCrWeb', 'DOMBnbPlug',
// GPU errors
`Failed to execute 'shaderSource'`,
'compiling shader',
'Failed to create WebGL context',
'CONTEXT_LOST_WEBGL',
'Framebuffer unsupported',
'Framebuffer failed for unspecified reason',
'Недостаточно ресурсов памяти для завершения операции.',
'Failed to initialize graphics device (Shader error)',
'Failed to initialize graphics device (Failed to create WebGL context)',
'Failed to initialize graphics device (Failed to create texture)',
'Shader error',
// GPU halt
'GPU device instance has been suspended',
'Die GPU-Geräteinstanz wurde angehalten',
'GPU zostało zawieszone',
`GPU приостановлен`,
'GPU se ha suspendido',
'GPU aygıt örneği askıya alınmış',
'GPU-enhetsinstansen har försatts',
// other
'androidInterface is not defined',
'/images/',
'out of memory',
'object is not a function',
'Array buffer allocation failed',
'Server is offline',
'Failed to register a ServiceWorker',
'Permission denied to access property',
'Not enough storage is available',
'Failed to initialize graphics device',
'Not enough memory resources',
'Ikke nok minneressurser tilgjengelig', // out of memory
'suficientes recursos de memoria',
'Onvoldoende geheugenbronnen',
`Cannot read property 'version' of undefined`,
'Maximum call stack size exceeded', // howler error on chrome mobile
// user errors
CHARACTER_LIMIT_ERROR,
'Too many requests',
'Saving in progress',
'Too many requests, please try again in',
'Already waiting for join request',
// server
'Range Not Satisfiable', 'Precondition Failed',
].map(escapeRegExp).join('|'), 'i');
export interface Person {
id: string;
username: string;
custom?: any;
}
function getLabel(arg: LogArgument | undefined) {
if (typeof arg === 'string') {
return arg;
} else if (arg && 'message' in arg) {
return arg.message + (arg.stack || '');
} else {
return arg ? arg.toString() : '';
}
}
export function isIgnoredMessage(message: string) {
return IGNORE.test(message);
}
export function isIgnoredError(error: Error) {
return isIgnoredMessage(error.message || `${error}` || '') || isIgnoredMessage(error.stack || '');
}
export function rollbarCheckIgnore(_isUncaught: boolean, args: LogArgument[], _payload: object): boolean {
return (Array.isArray(args) ? args : [args])
.map(getLabel)
.some(isIgnoredMessage);
}
+140
View File
@@ -0,0 +1,140 @@
import { escapeRegExp, compact, isMatchWith } from 'lodash';
import { PonyInfoNumber, PonyInfo } from './interfaces';
import { urlRegexTexts, ipRegexText } from './filterUtils';
import { AuthBase, GeneralSettings, Suspicious, GameServerSettings } from './adminInterfaces';
import { parseColorFast } from './color';
// suspicious
export const urlRegex = new RegExp(urlRegexTexts.join('|'), 'ui');
export const ipRegex = new RegExp(ipRegexText, 'ui');
function createRegExpFromList(list: string | undefined, wholeWords = false): RegExp | undefined {
const lines = list && compact(list.split(/\r?\n/).map(x => x.trim()));
if (lines && lines.length) {
const combined = lines.map(escapeRegExp).join('|');
if (wholeWords) {
return new RegExp(`\\b(${combined})\\b`, 'ui');
} else {
return new RegExp(combined, 'ui');
}
} else {
return undefined;
}
}
export const createCachedTest = (wholeWords = false) => {
let cachedList: string | undefined = undefined;
let cachedRegex: RegExp | undefined = undefined;
return (list: string | undefined, value: string) => {
if (cachedList !== list) {
cachedList = list;
cachedRegex = createRegExpFromList(list, wholeWords);
}
return cachedRegex ? cachedRegex.test(value) : false;
};
};
export const createIsSuspiciousMessage = (general: GeneralSettings) => {
const test = createCachedTest();
const testSafe = createCachedTest();
const testWhole = createCachedTest(true);
const testSafeInstant = createCachedTest();
const testWholeInstant = createCachedTest(true);
return (text: string, { filterSwears }: GameServerSettings): Suspicious => {
if (test(general.suspiciousMessages, text))
return Suspicious.Very;
if (filterSwears) {
if (testSafeInstant(general.suspiciousSafeInstantMessages, text) ||
testWholeInstant(general.suspiciousSafeInstantWholeMessages, text)) {
return Suspicious.Very;
}
if (testSafe(general.suspiciousSafeMessages, text) ||
testWhole(general.suspiciousSafeWholeMessages, text)) {
return Suspicious.Yes;
}
}
return Suspicious.No;
};
};
export const createIsSuspiciousName =
(settings: GeneralSettings) => {
const test = createCachedTest();
return (name: string) => test(settings.suspiciousNames, name);
};
export const createIsSuspiciousAuth =
(settings: GeneralSettings) => {
const test = createCachedTest();
return ({ name, emails = [] }: AuthBase<any>) =>
test(settings.suspiciousAuths, name) ||
emails.some(email => test(settings.suspiciousAuths, email));
};
// pony
function tryParseJSON(value: string): any {
try {
return JSON.parse(value);
} catch {
return undefined;
}
}
function createMatchesFromList(list: string | undefined): Partial<PonyInfo>[] {
return compact((list || '').split(/\n/g).map(x => x.trim()).map(tryParseJSON));
}
export const createIsSuspiciousPony =
(settings: GeneralSettings) =>
(info: PonyInfoNumber) => {
const matches = createMatchesFromList(settings.suspiciousPonies);
return matches.some(match => matchPony(info, match));
};
function matchPony(info: PonyInfoNumber, match: Partial<PonyInfo>) {
return isMatchWith(info, match, comparePonyInfoFields);
}
function comparePonyInfoFields(a: any, b: any): boolean {
if (typeof a === 'number' && typeof b === 'string') {
return a === parseColorFast(b);
} else {
return undefined as any;
}
}
// forbidden messages
export function isForbiddenMessage(_message: string): boolean {
// NOTE: uncomment, to filter offensive messages
// if (/niggers$/.test(_message) || /faggots?/.test(_message)) return true;
// NOTE: add more filters here
return false;
}
// forbidden name
export function isForbiddenName(_value: string): boolean {
// NOTE: uncomment, to filter offensive names
// if (/niggers$/.test(_value) || /faggots?/.test(_value) || /hitler/.test(_value)) return true;
// NOTE: uncomment, to filter links in names
// if (ipRegex.test(_value) && !ipExceptionRegex.test(_value)) return true;
// if (urlRegex.test(_value) && !urlExceptionRegex.test(_value)) return true;
// NOTE: add more filters here
return false;
}
+882
View File
@@ -0,0 +1,882 @@
import { range, times } from 'lodash';
import { PonyInfo, Point, PonyState, DrawPonyOptions, PonyInfoNumber, SpriteSet, PalettePonyInfo, NoDraw } from './interfaces';
import * as offsets from './offsets';
import { defaultPonyState } from '../client/ponyHelpers';
import { WHITE, BLACK, ORANGE, BLUE, CYAN, RED } from './colors';
import { createBodyFrame } from '../client/ponyAnimations';
import { setFlag, repeat } from './utils';
type OnFrame = (pony: PonyInfoNumber, state: PonyState, options: DrawPonyOptions, x: number, y: number, pattern: number) => void;
export interface SheetLayer {
name: string;
set?: string;
setOverride?: string;
options?: Partial<DrawPonyOptions>;
patterns?: number;
drawBlack?: boolean;
shiftY?: number;
head?: boolean;
noFace?: boolean;
body?: boolean;
frontLeg?: boolean;
backLeg?: boolean;
frontFarLeg?: boolean;
backFarLeg?: boolean;
extra?: keyof PalettePonyInfo; // extra field name
fieldName?: keyof PonyInfo;
setup?: (pony: PonyInfoNumber, state: PonyState) => void;
frame?: OnFrame;
frameSet?: (set: SpriteSet<number>, x: number, y: number, pattern: number) => void;
importMirrored?: { fieldName: string; offsetX: number };
}
export interface Spacer {
spacer: true;
}
export interface Sheet {
name: string;
file?: string;
skipImport?: boolean;
alert?: string;
spacer?: boolean;
rows?: number;
width: number;
height: number;
offset: number;
offsetY?: number;
padLeft?: number;
padTop?: number;
offsets?: Point[];
importOffsets?: Point[];
fieldName?: keyof PonyInfo;
groups?: string[][]; // groups for filling-in missing palette colors (used for manes)
setsWithEmpties?: string[]; // skipping slots (used for manes)
empties?: number[];
frame?: OnFrame;
layers: SheetLayer[];
masks?: {
name: string,
layerName: string,
mask: string,
reverse?: boolean,
maskFile?: string;
}[];
state?: PonyState;
extra?: boolean;
single?: boolean; // single frame (no animation)
duplicateFirstFrame?: number;
wrap?: number;
paletteOffsetY?: number;
}
interface BodyFrame {
body: number;
front: number;
back: number;
wing: number;
tail: number;
}
export const DEFAULT_COLOR = 0xdec078ff;
export const SPECIAL_COLOR = ORANGE;
const headFrames: BodyFrame[] = [
{ body: 1, front: 1, back: 1, wing: 0, tail: 0 },
];
const bodyFrames: BodyFrame[] = [
{ body: 0, front: 1, back: 1, wing: 0, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 0, tail: 0 },
{ body: 2, front: 29, back: 1, wing: 0, tail: 0 },
{ body: 3, front: 30, back: 21, wing: 0, tail: 0 },
{ body: 4, front: 31, back: 22, wing: 0, tail: 0 },
{ body: 5, front: 32, back: 23, wing: 0, tail: 1 },
{ body: 6, front: 33, back: 24, wing: 1, tail: 2 },
{ body: 7, front: 34, back: 25, wing: 2, tail: 2 },
{ body: 8, front: 34, back: 25, wing: 2, tail: 2 },
{ body: 9, front: 34, back: 26, wing: 2, tail: 2 },
{ body: 10, front: 35, back: 26, wing: 2, tail: 2 },
{ body: 11, front: 36, back: 26, wing: 1, tail: 2 },
{ body: 12, front: 37, back: 26, wing: 1, tail: 2 },
{ body: 13, front: 38, back: 26, wing: 0, tail: 2 },
{ body: 14, front: 38, back: 26, wing: 0, tail: 2 },
{ body: 15, front: 38, back: 26, wing: 0, tail: 2 },
];
const waistFrames: BodyFrame[] = [
...bodyFrames,
{ body: 1, front: 1, back: 1, wing: 3, tail: 0 },
];
const wingFrames: BodyFrame[] = [
{ body: 1, front: 1, back: 1, wing: 0, tail: 0 },
{ body: 6, front: 33, back: 24, wing: 1, tail: 0 },
{ body: 9, front: 34, back: 26, wing: 2, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 3, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 4, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 5, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 6, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 7, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 8, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 9, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 10, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 11, tail: 0 },
{ body: 1, front: 1, back: 1, wing: 12, tail: 0 },
];
const exampleCM = [
BLUE, BLUE, BLUE, BLUE, BLUE,
BLUE, CYAN, CYAN, CYAN, BLUE,
BLUE, CYAN, CYAN, CYAN, BLUE,
BLUE, CYAN, CYAN, CYAN, BLUE,
BLUE, BLUE, BLUE, BLUE, BLUE,
];
const frontLegsCount = 39;
const backLegsCount = 27;
const frontLegsSheet = {
width: 55,
height: 60,
offset: 50,
state: state(frontLegsCount, range(0, frontLegsCount)),
};
const backLegsSheet = {
width: 55,
height: 60,
offset: 55,
state: state(backLegsCount, undefined, undefined, range(0, backLegsCount)),
};
const bodySheet = {
width: 60,
height: 60,
offset: 50,
state: stateFromFrames(bodyFrames),
};
const chestSheet = {
width: 60,
height: 60,
offset: 60,
state: stateFromFrames(bodyFrames),
};
const waistSheet = {
...chestSheet,
state: stateFromFrames(waistFrames),
};
const singleFrameSheet = {
...bodySheet,
state: stateFromFrames(headFrames),
};
const headSheet = {
width: 60,
height: 75,
offset: 60,
offsetY: 20,
state: stateFromFrames(headFrames),
};
const bodyLayer: SheetLayer = {
name: '<body>', body: true, head: true, frontLeg: true, backLeg: true, frontFarLeg: true, backFarLeg: true,
};
const muzzleLayer: SheetLayer = { name: '<muzzle>', setup: pony => pony.nose = defaultSet() };
const frontLegLayer: SheetLayer = { name: '<front leg>', frontLeg: true, setup: pony => pony.coatFill = SPECIAL_COLOR };
const backLegLayer: SheetLayer = { name: '<back leg>', backLeg: true, setup: pony => pony.coatFill = SPECIAL_COLOR };
export const sheets: (Sheet | Spacer)[] = [
// front legs
{
...frontLegsSheet,
name: 'front legs',
file: 'front-legs',
frame: (_pony, state, _options, _x, y) => {
if (y > 0) {
state.animation.frames.forEach(f => f.frontLeg = 0);
}
},
layers: [
{ ...bodyLayer, frontLeg: false },
{
name: 'front', set: 'frontLegs', frontLeg: true,
frame: (pony, _state, _options, _x, _y, pattern) => {
pony.coatFill = pattern === 0 ? RED : WHITE;
pony.coatOutline = pattern === 0 ? RED : WHITE;
},
},
],
},
{
...frontLegsSheet,
name: 'front legs - hooves',
file: 'front-legs-hooves',
fieldName: 'frontHooves',
layers: [
{ ...bodyLayer, frontLeg: false },
frontLegLayer,
{
name: 'front', set: 'frontLegHooves', frontLeg: true, options: { useAllHooves: true },
setup: pony => pony.coatFill = BLACK
},
],
},
{
...frontLegsSheet,
name: 'front legs - socks',
file: 'front-legs-accessories',
fieldName: 'frontLegAccessory',
layers: [
{ ...bodyLayer, frontLeg: false },
frontLegLayer,
{ name: 'front', set: 'frontLegAccessories', frontLeg: true, setup: pony => pony.coatFill = BLACK },
],
},
{
...frontLegsSheet,
name: 'front legs - sleeves',
file: 'front-legs-sleeves',
fieldName: 'sleeveAccessory',
layers: [
{ ...bodyLayer, frontLeg: false },
frontLegLayer,
{
name: 'front', set: 'frontLegSleeves', frontLeg: true, options: { no: NoDraw.FarSleeves }, setup: pony => {
pony.chestAccessory = ignoreSet(2);
pony.coatFill = BLACK;
}
},
],
},
// back legs
{
...backLegsSheet,
alert: 'Does not export mask layer',
name: 'back legs',
file: 'back-legs',
masks: [
{
name: 'backLegs2',
layerName: 'front',
mask: 'mask',
},
],
frame: (_pony, state, _options, _x, y) => {
if (y > 0) {
state.animation.frames.forEach(f => f.backLeg = 0);
}
},
layers: [
// TODO: mask layer
{ ...bodyLayer, backLeg: false },
{
name: 'front', set: 'backLegs', backLeg: true,
frame: (pony, _state, _options, _x, _y, pattern) => {
pony.coatFill = pattern === 0 ? RED : WHITE;
pony.coatOutline = pattern === 0 ? RED : WHITE;
},
},
],
},
{
...backLegsSheet,
name: 'back legs - hooves',
file: 'back-legs-hooves',
fieldName: 'backHooves',
masks: [
{
name: 'backLegHooves2',
layerName: 'front',
mask: 'mask',
maskFile: 'back-legs',
},
],
layers: [
{ ...bodyLayer, backLeg: false },
backLegLayer,
{ name: 'front', set: 'backLegHooves', backLeg: true, setup: pony => pony.coatFill = BLACK },
],
},
{
...backLegsSheet,
name: 'back legs - socks',
file: 'back-legs-accessories',
fieldName: 'backLegAccessory',
masks: [
{
name: 'backLegAccessories2',
layerName: 'front',
mask: 'mask',
maskFile: 'back-legs',
},
],
layers: [
{ ...bodyLayer, backLeg: false },
backLegLayer,
{ name: 'front', set: 'backLegAccessories', backLeg: true, setup: pony => pony.coatFill = BLACK },
],
},
{
...backLegsSheet,
name: 'back legs - sleeves',
file: 'back-legs-sleeves',
fieldName: 'backAccessory',
rows: 2,
masks: [
{
name: 'backLegSleeves2',
layerName: 'front',
mask: 'mask',
maskFile: 'back-legs',
},
],
layers: [
{ ...bodyLayer, backLeg: false },
backLegLayer,
{
name: 'front', set: 'backLegSleeves', setOverride: 'backAccessories', patterns: 2,
options: { no: NoDraw.BackAccessory | NoDraw.FarSleeves },
setup: pony => pony.coatFill = BLACK,
frameSet: (set, _x, y, pattern) => {
set.type = y === 0 ? 5 : -1;
set.pattern = pattern;
},
},
],
},
{
...bodySheet,
name: 'body',
file: 'body',
// fieldName: 'body', // TODO: uncomment when body set is added
frame: (_pony, _state, options, _x, y) => {
// fix for missing body set
if (y > 0) {
options.no = setFlag(options.no, NoDraw.BodyOnly, true);
}
},
layers: [
{ name: '<far legs>', frontFarLeg: true, backFarLeg: true },
{
name: 'body', body: true, set: 'body',
frame: (pony, _state, _options, _x, _y, pattern) => {
pony.coatFill = pattern === 0 ? RED : WHITE;
pony.coatOutline = pattern === 0 ? RED : WHITE;
},
},
{ name: '<front leg>', frontLeg: true },
{ name: '<back leg>', backLeg: true },
{ name: '<head>', head: true },
],
},
{
name: 'body - wings',
file: 'body-wings',
fieldName: 'wings',
width: 80,
height: 70,
offset: 70,
offsetY: 10,
state: stateFromFrames(wingFrames),
layers: [
bodyLayer,
{ name: 'front', set: 'wings', options: { no: NoDraw.Behind } },
],
importOffsets: [1, 6, 9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1].map(i => offsets.wingOffsets[i]),
},
{
name: 'body - tails',
file: 'tails',
fieldName: 'tail',
width: 80,
height: 70,
offset: 70,
padLeft: 20,
state: stateFromFrames([1, 5, 9].map(i => bodyFrames[i])),
layers: [
{ name: 'behind-body', set: 'tails' },
bodyLayer,
],
importOffsets: [1, 5, 9].map(i => offsets.tailOffsets[i]),
},
{
...bodySheet,
name: 'body - neck accessory',
file: 'neck-accessory',
fieldName: 'neckAccessory',
layers: [
{ ...bodyLayer, head: false },
{ name: 'front', set: 'neckAccessories' },
{ name: '<head>', head: true },
],
importOffsets: offsets.neckAccessoryOffsets,
},
{
...bodySheet,
name: 'body - chest accessory',
file: 'body-chest-accessory',
fieldName: 'chestAccessory',
layers: [
{ name: 'behind', set: 'chestAccessoriesBehind', options: { no: NoDraw.Front } },
bodyLayer,
{ name: 'front', set: 'chestAccessories', options: { no: NoDraw.Behind } },
frontLegLayer,
],
importOffsets: offsets.chestAccessoryOffsets,
},
{
...chestSheet,
name: 'body - back accessory',
file: 'body-back-accessory',
fieldName: 'backAccessory',
// masks: [
// { name: 'backAccessories1', layerName: 'front', mask: 'mask' },
// { name: 'backAccessories2', layerName: 'front', mask: 'mask', reverse: true },
// ],
layers: [
bodyLayer,
{ name: 'front', set: 'backAccessories', options: { no: NoDraw.Sleeves } },
],
importOffsets: offsets.backAccessoryOffsets,
},
{
...waistSheet,
name: 'body - waist accessory',
file: 'body-waist-accessory',
fieldName: 'waistAccessory',
layers: [
bodyLayer,
{ name: 'front', set: 'waistAccessories' },
{
name: '<wing>', options: { no: NoDraw.Behind }, frame: (pony, _state, _options, x) => {
pony.wings = x === 16 ? specialSet(1) : ignoreSet();
},
},
],
importOffsets: offsets.waistAccessoryOffsets,
},
// head
{
...headSheet,
name: 'head',
file: 'head',
fieldName: 'head',
state: stateFromFrames(times(2, i => ({ body: 1, front: 1, back: 1, wing: 0, head: i, tail: 0 }))),
layers: [
{ ...bodyLayer, options: { no: NoDraw.Head | NoDraw.Eyes | NoDraw.CloseEar | NoDraw.Nose } },
{ name: 'front', set: 'head', head: true, drawBlack: false, options: { no: NoDraw.Ears | NoDraw.Nose | NoDraw.Eyes } },
{ name: '<face>', head: true, options: { no: NoDraw.Head | NoDraw.FarEar } },
],
},
{
...singleFrameSheet,
name: 'head - ears',
file: 'ears',
fieldName: 'ears',
single: true,
wrap: 8,
paletteOffsetY: 30,
layers: [
{
name: 'behind', set: 'earsFar', head: true, noFace: true, drawBlack: false,
options: { no: NoDraw.CloseEar | NoDraw.FarEarShade }
},
{ ...bodyLayer, options: { no: NoDraw.Ears } },
{
name: 'front', set: 'ears', head: true, noFace: true, drawBlack: false,
options: { no: NoDraw.FarEar },
// importMirrored: { fieldName: 'ears2', offsetX: 0 },
},
],
// TODO: add <hair> layer(s)
},
{
...headSheet,
name: 'head - horns',
file: 'horns',
fieldName: 'horn',
single: true,
wrap: 8,
layers: [
{ name: '<far ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.CloseEar } },
{ name: 'behind', set: 'hornsBehind', options: { no: NoDraw.Front } },
{ ...bodyLayer, options: { no: NoDraw.Ears } },
{
...bodyLayer, name: '<body with mane>', options: { no: NoDraw.Ears | NoDraw.FrontMane },
setup: pony => {
pony.mane = specialSet(1);
pony.backMane = specialSet(1);
}
},
{ name: 'front', set: 'horns', options: { no: NoDraw.Behind } },
{ name: '<ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } },
{
name: '<front mane>', head: true, noFace: true, drawBlack: false,
options: { no: NoDraw.Ears | NoDraw.Behind | NoDraw.TopMane },
setup: pony => pony.mane = specialSet(1),
},
],
},
{
...headSheet,
name: 'head - manes',
file: 'manes',
fieldName: 'mane',
groups: [
['frontManes', 'topManes', 'behindManes'],
['backFrontManes', 'backBehindManes'],
],
setsWithEmpties: ['backFrontManes', 'backBehindManes'],
empties: [3, 10, 13],
single: true,
wrap: 8,
layers: [
{ name: 'behind', set: 'behindManes', options: { no: NoDraw.FrontMane | NoDraw.TopMane } },
{ name: 'back-behind', set: 'backBehindManes', fieldName: 'backMane', options: { no: NoDraw.FrontMane } },
{ ...bodyLayer, options: { no: NoDraw.CloseEar } },
{ name: 'back', set: 'backFrontManes', fieldName: 'backMane', options: { no: NoDraw.Behind } },
{ name: 'top', set: 'topManes', options: { no: NoDraw.FrontMane | NoDraw.Behind } },
{ name: '<horn>', setup: pony => pony.horn = specialSet(1) },
{ name: '<ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } },
{ name: 'front', set: 'frontManes', options: { no: NoDraw.TopMane | NoDraw.Behind } },
],
},
{
...singleFrameSheet,
name: 'head - facial hair',
file: 'facial-hair',
fieldName: 'facialHair',
single: true,
wrap: 8,
paletteOffsetY: 35,
layers: [
{ ...bodyLayer, options: { no: NoDraw.Nose } },
{ name: 'front', set: 'facialHairBehind' },
muzzleLayer,
{ name: 'front-2', set: 'facialHair' },
],
},
{
...singleFrameSheet,
name: 'head - ear accessory',
file: 'ear-accessory',
fieldName: 'earAccessory',
single: true,
wrap: 8,
layers: [
{ name: 'behind', set: 'earAccessoriesBehind', options: { no: NoDraw.Front } },
{ ...bodyLayer },
{ name: 'front', set: 'earAccessories', options: { no: NoDraw.Behind } },
],
},
{
...headSheet,
name: 'head - head accessory',
file: 'head-accessory',
fieldName: 'headAccessory',
single: true,
wrap: 8,
layers: [
{ name: '<far ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.CloseEar } },
{ name: 'behind', set: 'headAccessoriesBehind' },
{ ...bodyLayer, options: { no: NoDraw.FarEar } },
{
...bodyLayer, name: '<body with mane>', shiftY: 5, options: { no: NoDraw.FarEar }, setup: pony => {
pony.mane = specialSet(1);
pony.backMane = specialSet(1);
}
},
{ name: 'front', set: 'headAccessories' },
],
},
{
...headSheet,
name: 'head - face accessory',
file: 'face-accessory',
fieldName: 'faceAccessory',
single: true,
extra: true,
wrap: 8,
layers: [
{ ...bodyLayer, options: { no: NoDraw.CloseEar } },
{ name: 'front', set: 'faceAccessories', extra: 'faceAccessory', options: { no: NoDraw.FaceAccessory2 } },
{ name: '<ear>', head: true, noFace: true, drawBlack: false, options: { no: NoDraw.FarEar } },
{ name: 'front-2', set: 'faceAccessories2', options: { no: NoDraw.FaceAccessory1 } },
muzzleLayer,
{ name: '<horn>', setup: pony => pony.horn = specialSet(1) },
],
},
{
...headSheet,
name: 'head - extra accessory',
file: 'extra-accessory',
fieldName: 'extraAccessory',
single: true,
wrap: 8,
paletteOffsetY: 45,
layers: [
{
name: 'behind', set: 'extraAccessoriesBehind', options: { extra: true, no: NoDraw.Front },
setup: pony => pony.mane = ignoreSet(1)
},
{
...bodyLayer, setup: pony => {
pony.mane = specialSet(1);
pony.backMane = specialSet(1);
}
},
{
name: 'front', set: 'extraAccessories', options: { extra: true, no: NoDraw.Behind },
setup: pony => pony.mane = ignoreSet(1)
},
],
},
{
spacer: true,
},
// offsets
{
...bodySheet,
name: 'offset - front legs',
offsets: offsets.frontLegOffsets,
state: stateFromFrames(bodyFrames.map(f => ({ ...f, front: 1 }))),
layers: [bodyLayer],
},
{
...bodySheet,
name: 'offset - back legs',
offsets: offsets.backLegOffsets,
state: stateFromFrames(bodyFrames.map(f => ({ ...f, back: 1 }))),
layers: [bodyLayer],
},
{
...bodySheet,
name: 'offset - wings',
fieldName: 'wings',
offsets: offsets.wingOffsets,
layers: [
bodyLayer,
{ name: 'front', set: 'wings', options: { no: NoDraw.Behind } },
],
duplicateFirstFrame: bodyFrames.length,
},
{
width: 80,
height: 70,
offset: 70,
state: stateFromFrames(bodyFrames),
name: 'offset - tails',
fieldName: 'tail',
offsets: offsets.tailOffsets,
layers: [
{ name: 'behindBody', set: 'tails' },
bodyLayer,
],
duplicateFirstFrame: bodyFrames.length,
},
{
...bodySheet,
name: 'offset - head',
offsets: offsets.headOffsets,
layers: [bodyLayer],
},
{
...bodySheet,
name: 'offset - cm',
offsets: offsets.cmOffsets,
layers: [
{ ...bodyLayer, setup: pony => pony.cm = exampleCM },
],
},
{
...bodySheet,
name: 'offset - neck accessory',
fieldName: 'neckAccessory',
offsets: offsets.neckAccessoryOffsets,
layers: [
{ ...bodyLayer, head: false },
{ name: 'front', set: 'neckAccessories' },
{ name: '<head>', head: true },
],
importOffsets: offsets.neckAccessoryOffsets,
},
{
...bodySheet,
name: 'offset - chest accessory',
fieldName: 'chestAccessory',
offsets: offsets.chestAccessoryOffsets,
layers: [
{ name: 'behind', set: 'chestAccessoriesBehind', options: { no: NoDraw.Front } },
bodyLayer,
{ name: 'front', set: 'chestAccessories', options: { no: NoDraw.Behind } },
],
},
{
...waistSheet,
name: 'offset - waist accessory',
fieldName: 'waistAccessory',
offsets: offsets.waistAccessoryOffsets,
layers: [
bodyLayer,
{ name: 'front', set: 'waistAccessories' },
{
name: '<wing>', options: { no: NoDraw.Behind }, frame: (pony, _state, _options, x) => {
pony.wings = x === 16 ? specialSet(1) : ignoreSet();
},
},
],
},
{
...chestSheet,
name: 'offset - back accessory',
fieldName: 'backAccessory',
offsets: offsets.backAccessoryOffsets,
layers: [
{ name: '<tail>', setup: pony => pony.tail = specialSet(2) },
bodyLayer,
{ name: 'front', set: 'backAccessories' },
],
},
{
width: 55,
height: 40,
offset: 55,
offsetY: 10,
state: stateFromFrames(repeat(offsets.HEAD_ACCESSORY_OFFSETS.length, bodyFrames[1])),
name: 'offset - hats',
rows: 19,
offsets: offsets.HEAD_ACCESSORY_OFFSETS,
layers: [
{
...bodyLayer,
frame: (pony, _state, _options, x, y) => {
pony.mane = specialSet(x);
// pony.backMane = specialSet(x === 0 ? 0 : 1);
pony.headAccessory = whiteSet(y + 1);
},
},
],
},
{
width: 55,
height: 40,
offset: 55,
offsetY: 10,
state: stateFromFrames(repeat(offsets.EAR_ACCESSORY_OFFSETS.length, bodyFrames[1])),
name: 'offset - earrings',
rows: 13,
offsets: offsets.EAR_ACCESSORY_OFFSETS,
layers: [
{
...bodyLayer,
frame: (pony, _state, _options, x, y) => {
pony.ears = defaultSet(x);
pony.earAccessory = whiteSet(y + 1);
},
},
],
},
{
width: 55,
height: 40,
offset: 55,
offsetY: 10,
state: stateFromFrames(repeat(offsets.EXTRA_ACCESSORY_OFFSETS.length, bodyFrames[1])),
name: 'offset - extra',
rows: 17,
offsets: offsets.EXTRA_ACCESSORY_OFFSETS,
layers: [
{
...bodyLayer,
frame: (pony, _state, options, x, y) => {
pony.mane = specialSet(x);
pony.extraAccessory = createSet(y + 1, WHITE, 7);
options.extra = true;
},
},
],
},
];
function stateFromFrames(frames: BodyFrame[]) {
const front = frames.map(f => f.front);
const back = frames.map(f => f.back);
const body = frames.map(f => f.body);
const wing = frames.map(f => f.wing);
const tail = frames.map(f => f.tail);
return state(frames.length, front, front, back, back, undefined, body, wing, tail);
}
function state(
frames: number, frontLegs?: number[], frontFarLegs?: number[], backLegs?: number[], backFarLegs?: number[],
head?: number[], body?: number[], wing?: (number | undefined)[], tail?: number[]
): PonyState {
const state = defaultPonyState();
state.blushColor = 0;
state.animation = {} as any;
const ones = times(frames, () => 1);
const zeros = times(frames, () => 0);
state.animation = {
name: '',
loop: false,
fps: 24,
frames: times(frames, i => ({
...createBodyFrame([]),
head: (head || ones)[i],
body: (body || ones)[i],
wing: (wing && wing[i]) || 0,
tail: (tail || zeros)[i],
frontLeg: (frontLegs || ones)[i],
frontFarLeg: (frontFarLegs || ones)[i],
backLeg: (backLegs || ones)[i],
backFarLeg: (backFarLegs || ones)[i],
})),
};
return state;
}
export function ignoreSet(type = 0): SpriteSet<number> {
return createSet(type, BLACK);
}
function defaultSet(type = 0): SpriteSet<number> {
return createSet(type, DEFAULT_COLOR);
}
function specialSet(type = 0): SpriteSet<number> {
return createSet(type, SPECIAL_COLOR);
}
function whiteSet(type = 0): SpriteSet<number> {
return createSet(type, WHITE);
}
function createSet(type: number, color: number, count = 2): SpriteSet<number> {
return {
type,
fills: times(count, () => color),
lockFills: times(count, () => false),
outlines: times(count, () => color),
lockOutlines: times(count, () => true),
};
}
+90
View File
@@ -0,0 +1,90 @@
const lowercaseCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789_';
const uppercaseCharacters = lowercaseCharacters + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const CARRIAGERETURN = '\r'.charCodeAt(0);
export function randomString(length: number, useUpperCase = false): string {
const characters = useUpperCase ? uppercaseCharacters : lowercaseCharacters;
let result = '';
for (let i = 0; i < length; i++) {
result += characters[(Math.random() * characters.length) | 0];
}
return result;
}
export function isSurrogate(code: number): boolean {
return code >= 0xd800 && code <= 0xdbff;
}
export function isLowSurrogate(code: number): boolean {
return (code & 0xfc00) === 0xdc00;
}
export function fromSurrogate(high: number, low: number): number {
return (((high & 0x3ff) << 10) + (low & 0x3ff) + 0x10000) | 0;
}
export function charsToCodes(text: string) {
const chars: number[] = [];
for (let i = 0; i < text.length; i++) {
let code = text.charCodeAt(i);
if (isSurrogate(code) && (i + 1) < text.length) {
const extra = text.charCodeAt(i + 1);
if (isLowSurrogate(extra)) {
code = fromSurrogate(code, extra);
i++;
}
}
chars.push(code);
}
return chars;
}
export function stringToCodes(buffer: Uint32Array, text: string): number {
const textLength = text.length | 0;
let length = 0 | 0;
for (let i = 0; i < textLength; i = (i + 1) | 0) {
let code = text.charCodeAt(i) | 0;
if (isSurrogate(code) && ((i + 1) | 0) < textLength) {
const extra = text.charCodeAt(i + 1) | 0;
if (isLowSurrogate(extra)) {
code = fromSurrogate(code, extra) | 0;
i = (i + 1) | 0;
}
}
if (isVisibleChar(code)) {
buffer[length] = code;
length = (length + 1) | 0;
}
}
return length;
}
export let codesBuffer = new Uint32Array(32);
export function stringToCodesTemp(text: string) {
while (text.length > codesBuffer.length) {
codesBuffer = new Uint32Array(codesBuffer.length * 2);
}
return stringToCodes(codesBuffer, text);
}
export function matcher(regex: RegExp) {
return (text: string): boolean => !!text && regex.test(text);
}
export function isVisibleChar(code: number) {
return code !== CARRIAGERETURN && !(code >= 0xfe00 && code <= 0xfe0f);
}
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
import { CharacterTag, FontPalettes } from './interfaces';
import { hasRole, AccountRoles } from './accountUtils';
import { MOD_COLOR, ADMIN_COLOR, PATREON_COLOR, ANNOUNCEMENT_COLOR, WHITE } from './colors';
const placeholder = { id: '', tagClass: '', label: '' };
const tags: { [key: string]: CharacterTag; } = {
'mod': { ...placeholder, name: 'moderator', className: 'mod', color: MOD_COLOR },
'dev': { ...placeholder, name: 'developer', className: 'dev', color: ADMIN_COLOR },
'dev:art': { ...placeholder, name: 'dev artist', className: 'dev', color: ADMIN_COLOR },
'dev:music': { ...placeholder, name: 'dev musician', className: 'dev', color: ADMIN_COLOR },
'sup1': { ...placeholder, name: 'supporter', className: 'sup1', color: PATREON_COLOR },
'sup2': { ...placeholder, name: 'supporter', className: 'sup2', color: WHITE },
'sup3': { ...placeholder, name: 'supporter', className: 'sup3', color: WHITE },
'hidden': { ...placeholder, name: 'hidden', className: 'hidden', color: ANNOUNCEMENT_COLOR },
};
Object.keys(tags).forEach(id => {
const tag = tags[id];
tag.id = id;
tag.label = `<${tag.name.toUpperCase()}>`;
tag.tagClass = `tag-${tag.className}`;
});
export const emptyTag: CharacterTag = { id: '', name: 'no tag', label: '', className: '', tagClass: '', color: 0 };
export function getAllTags() {
return Object.keys(tags).map(key => tags[key]);
}
export function getTag(id: string | undefined): CharacterTag | undefined {
return id ? tags[id] : undefined;
}
export function getTagPalette(tag: CharacterTag, palettes: FontPalettes) {
switch (tag.id) {
case 'sup2': return palettes.supporter2;
case 'sup3': return palettes.supporter3;
default: return palettes.white;
}
}
export function canUseTag(account: AccountRoles, tag: string) {
if (tag === 'mod') {
return hasRole(account, 'mod');
} else if (tag === 'dev' || /^dev:/.test(tag)) {
return hasRole(account, 'dev');
} else {
return false;
}
}
export function getAvailableTags(account: AccountRoles): CharacterTag[] {
return getAllTags().filter(tag => canUseTag(account, tag.id));
}
+127
View File
@@ -0,0 +1,127 @@
import { lerpColors, withAlphaFloat } from './color';
import { WHITE, SHADOW_COLOR, BLACK } from './colors';
import { MINUTE } from './constants';
import { Season } from './interfaces';
const DAY_START = 4.75; // 04:45
const DAY_END = 20.25; // 20:15
const SUN_EASE = 1.5; // 01:30
const SUN_HALF = SUN_EASE / 2;
const SUN_GAP = SUN_EASE / 4;
export const HOUR_LENGTH = 2 * MINUTE; // 48 min -> 24 hours
export const DAY_LENGTH = HOUR_LENGTH * 24;
const getTimeOfDay = (time: number) => time % DAY_LENGTH;
const getHourOfDay = (timeOfDay: number) => timeOfDay * 24 / DAY_LENGTH;
export function getHour(time: number) {
const timeOfDay = getTimeOfDay(time);
const hourOfDay = getHourOfDay(timeOfDay);
return hourOfDay;
}
export function formatHourMinutes(time: number): string {
const timeOfDay = getTimeOfDay(time);
const minutesInDay = 60 * 24;
const totalMinutes = Math.floor(timeOfDay * minutesInDay / DAY_LENGTH);
const minutes = totalMinutes % 60;
const hours = Math.floor(totalMinutes / 60);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
const isHour = (test: (hour: number) => boolean) => (time: number) => {
return test(getHour(time));
};
export const isDay = isHour(hour => hour > DAY_START && hour <= DAY_END);
export const isNight = (time: number) => !isDay(time);
export const isFullDay = isHour(hour => hour > (DAY_START + SUN_HALF) && hour <= (DAY_END - SUN_HALF));
export const isFullNight = isHour(hour => hour < (DAY_START - SUN_HALF) || hour >= (DAY_END + SUN_HALF));
export const isSunRaising = isHour(hour => hour > (DAY_START - SUN_HALF) && hour <= (DAY_START + SUN_HALF));
export const isSunSetting = isHour(hour => hour > (DAY_END - SUN_HALF) && hour <= (DAY_END + SUN_HALF));
export const isDayTime = isHour(hour => hour > DAY_START && hour < (DAY_END - SUN_HALF));
export const isNightTime = isHour(hour => hour < (DAY_START - SUN_HALF) || hour > DAY_END);
// light color
export interface LightData {
lightColors: number[];
shadowColors: number[];
lightStops: number[];
}
export function createLightData(season: Season): LightData {
const lightDay = WHITE;
const lightNight = season === Season.Winter ? 0x253f76ff : 0x2b3374ff;
const sunrise1 = 0x853d7dff;
const sunrise2 = 0xc96161ff;
const sunrise3 = 0xeeb7a0ff;
const sunset1 = sunrise3;
const sunset2 = sunrise2;
const sunset3 = sunrise1;
const shadowAlphaMultiplier = season === Season.Winter ? 0.7 : 1;
const shadowDay = withAlphaFloat(BLACK, 0.3 * shadowAlphaMultiplier);
const shadowNight = withAlphaFloat(BLACK, 0.2 * shadowAlphaMultiplier);
const shadowSunset = withAlphaFloat(BLACK, 0.25 * shadowAlphaMultiplier);
const shadowSunrise = withAlphaFloat(BLACK, 0.25 * shadowAlphaMultiplier);
const lightPoints = [
// night
{ time: 0, light: lightNight, shadow: shadowNight },
// transition to day
{ time: DAY_START - SUN_HALF, light: lightNight, shadow: shadowNight },
{ time: DAY_START - SUN_HALF + SUN_GAP, light: sunrise1, shadow: shadowSunrise },
{ time: DAY_START - SUN_HALF + SUN_GAP * 2, light: sunrise2, shadow: shadowSunrise },
{ time: DAY_START - SUN_HALF + SUN_GAP * 3, light: sunrise3, shadow: shadowSunrise },
{ time: DAY_START + SUN_HALF, light: lightDay, shadow: shadowDay },
// transition to night
{ time: DAY_END - SUN_HALF, light: lightDay, shadow: shadowDay },
{ time: DAY_END - SUN_HALF + SUN_GAP, light: sunset1, shadow: shadowSunset },
{ time: DAY_END - SUN_HALF + SUN_GAP * 2, light: sunset2, shadow: shadowSunset },
{ time: DAY_END - SUN_HALF + SUN_GAP * 3, light: sunset3, shadow: shadowSunset },
{ time: DAY_END + SUN_HALF, light: lightNight, shadow: shadowNight },
// night
{ time: 24, light: lightNight, shadow: shadowNight },
];
const lightColors = lightPoints.map(l => l.light);
const shadowColors = lightPoints.map(l => l.shadow);
const lightStops = lightPoints.map(l => l.time);
return { lightColors, shadowColors, lightStops };
}
export function getLightColor(data: LightData, time: number): number {
return getColorForTime(time, data.lightStops, data.lightColors, WHITE);
}
export function getShadowColor(data: LightData, time: number): number {
return getColorForTime(time, data.lightStops, data.shadowColors, SHADOW_COLOR);
}
function getColorForTime(time: number, stops: number[], colors: number[], defaultColor: number) {
const timeOfDay = getTimeOfDay(time);
const hourOfDay = getHourOfDay(timeOfDay);
for (let i = 1; i < stops.length; i++) {
if (stops[i] >= hourOfDay) {
const from = stops[i - 1];
const to = stops[i];
const fromLight = colors[i - 1];
const toLight = colors[i];
return lerpColors(fromLight, toLight, (hourOfDay - from) / (to - from));
}
}
return defaultColor;
}
+541
View File
@@ -0,0 +1,541 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Point, Rect, Entity, Dict } from './interfaces';
import { tileWidth, tileHeight, SECOND, MINUTE, HOUR, DAY } from './constants';
import { ACCESS_ERROR, NOT_FOUND_ERROR, OFFLINE_ERROR, PROTECTION_ERROR } from './errors';
// enum
export function invalidEnum(value: never) {
if (DEVELOPMENT) {
throw new Error(`Invalid enum value: ${value}`);
}
}
export function invalidEnumReturn<T>(value: never, ret: T): T {
if (DEVELOPMENT && !TESTS) {
throw new Error(`Invalid enum value: ${value}`);
}
return ret;
}
// date
export function fromDate(date: Date, duration: number): Date {
date.setTime(date.getTime() + duration);
return date;
}
export function fromNow(duration: number): Date {
return fromDate(new Date(), duration);
}
export function compareDates(a?: Date, b?: Date) {
return a ? (b ? a.getTime() - b.getTime() : 1) : (b ? -1 : 0);
}
export function maxDate(a?: Date, b?: Date) {
return (compareDates(a, b) > 0 ? a : b) || a || b;
}
export function minDate(a?: Date, b?: Date) {
return (compareDates(a, b) < 0 ? a : b) || a || b;
}
export function formatDuration(duration: number) {
const s = Math.floor(duration / SECOND) % 60;
const m = Math.floor(duration / MINUTE) % 60;
const h = Math.floor(duration / HOUR) % 24;
const d = Math.floor(duration / DAY);
if (d > 0) {
return h ? `${d}d ${h}h` : `${d}d`;
} else if (h > 0) {
return m ? `${h}h ${m}m` : `${h}h`;
} else if (m > 0) {
return s ? `${m}m ${s}s` : `${m}m`;
} else {
return `${s}s`;
}
}
export function formatISODate(date: Date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
}
export function parseISODate(value: string) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
let day = 0;
let month = 0;
let year = 0;
if (match) {
year = parseInt(match[1], 10);
month = parseInt(match[2], 10);
day = parseInt(match[3], 10);
}
return { day, month, year };
}
export function createValidBirthDate(day: number, month: number, year: number) {
const date = new Date(0);
const currentYear = (new Date()).getFullYear();
date.setFullYear(year, month - 1, day);
if (
date.getFullYear() === year && date.getMonth() === (month - 1) && date.getDate() === day &&
year >= (currentYear - 120) && year < currentYear
) {
return date;
} else {
return undefined;
}
}
// color
export function parseSpriteColor(str: string): number {
return str === '0' ? 0 : (str.length === 6 ? (((parseInt(str, 16) << 8) | 0xff) >>> 0) : (parseInt(str, 16) >>> 0));
}
// numbers
export function clamp(value: number, min: number, max: number): number {
return value > min ? (value < max ? value : max) : min;
}
export function lerp(a: number, b: number, t: number) {
return a + t * (b - a);
}
export function normalize(x: number, y: number): Point {
const d = Math.sqrt(x * x + y * y);
return { x: x / d, y: y / d };
}
export function computeCRC(colors: Uint32Array): number {
let crc = 0;
for (let i = 0; i < colors.length; i++) {
crc ^= colors[i];
for (let j = 0; j < 8; j++) {
crc = (crc & 1) ? ((crc >>> 1) ^ 0x82f63b78) : (crc >>> 1);
}
}
return crc >>> 0;
}
export function computeFriendsCRC(friends: string[]) {
if (!friends.length) {
return 0;
}
friends.sort();
const data = new Uint32Array(friends.length * 3);
for (let i = 0; i < friends.length; i++) {
const id = friends[i];
data[i * 3] = parseInt(id.substr(0, 8), 16);
data[i * 3 + 1] = parseInt(id.substr(8, 8), 16);
data[i * 3 + 2] = parseInt(id.substr(16, 8), 16);
}
return computeCRC(data);
}
export function lerpColor(a: number[] | Float32Array, b: number[] | Float32Array, t: number) {
a[0] = t * b[0] + (1 - t) * a[0];
a[1] = t * b[1] + (1 - t) * a[1];
a[2] = t * b[2] + (1 - t) * a[2];
a[3] = t * b[3] + (1 - t) * a[3];
}
// common
export function toInt(value: any): number {
return value | 0;
}
export function dispose<T extends { dispose(): void; }>(obj: T | undefined): undefined {
obj && obj.dispose();
return undefined;
}
export function cloneDeep<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
// enums
export function hasFlag(value: number | undefined, flag: number): boolean {
return (value! & flag) === flag;
}
export function setFlag(value: number | undefined, flag: number, on: boolean): number {
return (value! & ~flag) | (on ? flag : 0);
}
export function flagsToString(value: number, flags: { value: number; name: string; }[], none = 'None') {
return flags
.filter(flag => hasFlag(value, flag.value))
.map(flag => flag.name).join(' | ') || none;
}
// collections
export function includes<T>(array: T[] | undefined, item: T): boolean {
return array !== undefined && array.indexOf(item) !== -1;
}
export function array<T>(size: number, defaultValue: T) {
const result: T[] = [];
for (let i = 0; i < size; i++) {
result.push(defaultValue);
}
return result;
}
export function repeat<T>(count: number, ...values: T[]): T[] {
const result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(...values);
}
return result;
}
export function times<T>(count: number, action: (index: number) => T) {
const result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(action(i));
}
return result;
}
export function last<T>(array: T[]): T | undefined {
return array.length > 0 ? array[array.length - 1] : undefined;
}
export function flatten<T>(arrays: T[][]): T[] {
return ([] as T[]).concat(...arrays);
}
export function at<T>(items: T[], index: any): T | undefined {
return items[clamp(index | 0, 0, items.length - 1)];
}
export function att<T>(items: T[] | null | undefined, index: any): T | undefined {
return items ? items[clamp(index | 0, 0, items.length - 1)] : undefined;
}
export function findById<U, T extends { id: U }>(items: T[], id: U): T | undefined {
for (let i = 0; i < items.length; i++) {
if (items[i].id === id) {
return items[i];
}
}
return undefined;
}
export function findIndexById<U, T extends { id: U }>(items: T[], id: U): number {
for (let i = 0; i < items.length; i++) {
if (items[i].id === id) {
return i;
}
}
return -1;
}
export function removeItem<T>(items: T[], item: T): boolean {
const index = items.indexOf(item);
if (index !== -1) {
items.splice(index, 1);
return true;
} else {
return false;
}
}
export function removeItemFast<T>(items: T[], item: T): boolean {
const index = items.indexOf(item);
if (index !== -1) {
items[index] = items[items.length - 1];
items.pop();
return true;
} else {
return false;
}
}
export function removeById<U, T extends { id: U }>(items: T[], id: U): T | undefined {
const index = findIndexById(items, id);
if (index !== -1) {
const item = items[index];
items.splice(index, 1);
return item;
} else {
return undefined;
}
}
export function arraysEqual<T>(a: T[], b: T[]): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
export function pushUniq<T>(array: T[], item: T) {
const index = array.indexOf(item);
if (index === -1) {
array.push(item);
return array.length;
} else {
return index + 1;
}
}
export function createPlainMap<T>(values: Dict<T>): Dict<T> {
return Object.keys(values).reduce((obj: Dict<T>, key: string) => (obj[key] = values[key], obj), Object.create(null));
}
// rects / points
export function point(x: number, y: number): Point {
return { x, y };
}
export function contains(x: number, y: number, bounds: Rect, point: Point): boolean {
const bx = bounds.x / tileWidth + x;
const by = bounds.y / tileHeight + y;
const bw = bounds.w / tileWidth;
const bh = bounds.h / tileHeight;
return point.x > bx && point.x < bx + bw && point.y > by && point.y < by + bh;
}
export function containsPoint(dx: number, dy: number, rect: Rect, px: number, py: number): boolean {
return pointInXYWH(px, py, rect.x + dx, rect.y + dy, rect.w, rect.h);
}
export function containsPointWitBorder(dx: number, dy: number, rect: Rect, px: number, py: number, border: number): boolean {
return pointInXYWH(px, py, rect.x + dx - border, rect.y + dy - border, rect.w + border * 2, rect.h + border * 2);
}
export function pointInRect(x: number, y: number, rect: Rect) {
return x > rect.x && x < rect.x + rect.w && y > rect.y && y < rect.y + rect.h;
}
export function pointInXYWH(px: number, py: number, rx: number, ry: number, rw: number, rh: number) {
return px > rx && px < rx + rw && py > ry && py < ry + rh;
}
export function randomPoint({ x, y, w, h }: Rect): Point {
return {
x: x + w * Math.random(),
y: y + h * Math.random(),
};
}
export function lengthOfXY(dx: number, dy: number): number {
return Math.sqrt(dx * dx + dy * dy);
}
export function distanceXY(ax: number, ay: number, bx: number, by: number): number {
return lengthOfXY(ax - bx, ay - by);
}
export function distanceSquaredXY(ax: number, ay: number, bx: number, by: number): number {
const dx = ax - bx;
const dy = ay - by;
return dx * dx + dy * dy;
}
export function distance(a: Point, b: Point): number {
return distanceXY(a.x, a.y, b.x, b.y);
}
export function entitiesIntersect(a: Entity, b: Entity): boolean {
const aBounds = a.bounds;
const bBounds = b.bounds;
if (!aBounds || !bBounds) {
return false;
}
const ax = a.x * tileWidth + aBounds.x;
const ay = a.y * tileHeight + aBounds.y;
const bx = b.x * tileWidth + bBounds.x;
const by = b.y * tileHeight + bBounds.y;
return intersect(ax, ay, aBounds.w, aBounds.h, bx, by, bBounds.w, bBounds.h);
}
export function collidersIntersect(ax: number, ay: number, a: Rect, bx: number, by: number, b: Rect): boolean {
const axmin = Math.floor((ax + a.x) * tileWidth) | 0;
const axmax = Math.ceil((ax + a.x + a.w) * tileWidth) | 0;
const aymin = Math.floor((ay + a.y) * tileHeight) | 0;
const aymax = Math.ceil((ay + a.y + a.h) * tileHeight) | 0;
const bxmin = Math.floor((bx + b.x) * tileWidth) | 0;
const bxmax = Math.ceil((bx + b.x + b.w) * tileWidth) | 0;
const bymin = Math.floor((by + b.y) * tileHeight) | 0;
const bymax = Math.ceil((by + b.y + b.h) * tileHeight) | 0;
return axmin < bxmax && axmax > bxmin && aymin < bymax && aymax > bymin;
}
export function boundsIntersect(
ax: number, ay: number, a: Rect | undefined, bx: number, by: number, b: Rect | undefined
): boolean {
return !!(a && b && intersect(
ax * tileWidth + a.x, ay * tileHeight + a.y, a.w, a.h,
bx * tileWidth + b.x, by * tileHeight + b.y, b.w, b.h));
}
export function intersect(
ax: number, ay: number, aw: number, ah: number, bx: number, by: number, bw: number, bh: number
): boolean {
return ax <= (bx + bw) && (ax + aw) >= bx && ay <= (by + bh) && (ay + ah) >= by;
}
// requests
export type RequestError = Error & { status?: number; text?: string; };
export function createError(status: number, data: string | { error: string; }): Error {
if (status > 500 && status < 600) {
return new Error(PROTECTION_ERROR);
// } else if (status === 400) {
// return new Error('Bad Request');
} else if (status === 403) {
return new Error(ACCESS_ERROR);
} else if (status === 404) {
return new Error(NOT_FOUND_ERROR);
} else if (typeof data === 'string') {
return new Error(data || OFFLINE_ERROR);
} else {
return new Error((data && data.error) || OFFLINE_ERROR);
}
}
export function delay(timeout: number) {
return new Promise<void>(resolve => setTimeout(resolve, timeout));
}
export function observableToPromise<T>(observable: Observable<T>) {
return observable.toPromise()
.catch(({ status, error }: HttpErrorResponse) => {
const text = error && error.text;
try {
error = JSON.parse(error);
} catch { }
const e: RequestError = createError(status || 0, error);
e.status = status;
e.text = text;
throw e;
});
}
// other
function setTransformDefault(element: HTMLElement | undefined, transform: string) {
if (element) {
element.style.transform = transform;
}
}
function setTransformSafari(element: HTMLElement | undefined, transform: string) {
if (element) {
(element.style as any).webkitTransform = transform;
}
}
export const setTransform = (typeof document !== 'undefined' && 'transform' in document.body.style) ?
setTransformDefault : setTransformSafari;
export class ObjectCache<T> {
private cache: T[] = [];
constructor(private limit: number, private ctor: () => T) {
}
get(): T {
return this.cache.pop() || this.ctor();
}
put(item: T) {
if (this.cache.length < this.limit) {
this.cache.push(item);
}
}
}
export function bitmask(data: Uint8Array, key: number) {
if (key) {
for (let i = 0; i < data.length; i++) {
data[i] = data[i] ^ key;
}
}
return data;
}
export function isCommand(text: string) {
return /^\//.test(text);
}
export function processCommand(text: string) {
text = text.substr(1);
const space = text.indexOf(' ');
const command = (space === -1 ? text : text.substr(0, space)).trim() as string | undefined;
const args = space === -1 ? '' : text.substr(space + 1).trim();
return { command, args };
}
// events
export type AnyEvent = MouseEvent | PointerEvent | TouchEvent;
export function isTouch(e: AnyEvent): e is TouchEvent {
return /^touch/i.test(e.type);
}
export function getButton(e: AnyEvent): number {
return ('button' in e) ? (e.button || 0) : 0;
}
export function getX(e: AnyEvent): number {
return ('touches' in e && e.touches.length > 0) ? e.touches[0].pageX : (e as any).pageX;
}
export function getY(e: AnyEvent): number {
return ('touches' in e && e.touches.length > 0) ? e.touches[0].pageY : (e as any).pageY;
}
export function isKeyEventInvalid(e: KeyboardEvent) {
return e.target && /^(input|textarea|select)$/i.test((<any>e.target).tagName);
}
+741
View File
@@ -0,0 +1,741 @@
import {
Entity, Point, TileType, Rect, MapInfo, Camera, Region, IMap, MapState, defaultMapState, Pony,
MapType, EntityFlags, WorldMap, Weather, EntityState, canWalk, MapFlags,
} from './interfaces';
import { contains, removeItem, boundsIntersect, array, pushUniq, containsPoint, removeItemFast } from './utils';
import { isBoundsVisible, } from './camera';
import {
getRegionTile, setRegionTile, setRegionTileDirty, getRegionElevation, setRegionElevation,
getRegionTileIndex, worldToRegionX, worldToRegionY, generateRegionCollider, invalidateRegionsCollider
} from './region';
import { weatherRain, splash } from './entities';
import { releaseEntity, isMoving, isHidden, isDrawable, isPonyFlying } from './entityUtils';
import { updatePonyEntity, invalidatePalettesForPony, ensurePonyInfoDecoded, isPony, isPonyOnTheGround } from './pony';
import { getTileHeight, updateTileIndices, isInWater } from '../client/tileUtils';
import { toScreenX, toScreenY, toScreenYWithZ, rectToScreen, toWorldZ } from './positionUtils';
import { hasDrawLight, hasLightSprite } from '../client/draw';
import { PonyTownGame } from '../client/game';
import { WATER_FPS, PONY_TYPE, REGION_SIZE } from './constants';
import { updatePosition, canCollideWith } from './collision';
import { PaletteManager } from '../graphics/paletteManager';
import { timeEnd, timeStart } from '../client/timing';
import { playEffect } from '../client/handlers';
import { isFlyingDown } from '../client/ponyStates';
const defaultMapInfo: MapInfo = {
type: MapType.None,
flags: MapFlags.None,
regionsX: 0,
regionsY: 0,
defaultTile: TileType.None,
};
export function createWorldMap(info = defaultMapInfo, state: MapState = { ...defaultMapState }): WorldMap {
const { type, flags, regionsX, regionsY, defaultTile, editableArea } = info;
const map: WorldMap = {
type,
flags,
tileTime: 0,
entities: [],
entitiesDrawable: [],
entitiesWithNames: [],
entitiesWithChat: [],
entitiesMoving: [],
entitiesTriggers: [],
entitiesLight: [],
entitiesLightSprite: [],
entitiesById: new Map<number, Entity>(),
poniesToDecode: [],
regionsX,
regionsY,
regions: array(regionsX * regionsY, undefined),
defaultTile,
width: regionsX * REGION_SIZE,
height: regionsY * REGION_SIZE,
minRegionX: 0,
minRegionY: 0,
maxRegionX: 0,
maxRegionY: 0,
state,
editableArea,
};
updateMinMaxRegion(map);
return map;
}
function pickAny(entity: Entity, point: Point): boolean {
const bounds = entity.interactBounds || entity.bounds;
return !!bounds && contains(entity.x, entity.y, bounds, point);
}
export function getAnyBounds(entity: Entity) {
return [
entity.interactBounds,
entity.bounds,
entity.lightBounds,
entity.lightSpriteBounds,
entity.collidersBounds,
entity.triggerBounds && rectToScreen(entity.triggerBounds),
].filter(x => x && x.w > 0 && x.h > 0)[0];
}
function pickAnyEvenLights(entity: Entity, point: Point): boolean {
const bounds = getAnyBounds(entity);
return !!bounds && contains(entity.x, entity.y, bounds, point);
}
function pick(entity: Entity, point: Point, pickHidden: boolean, pickEditable: boolean): boolean {
const editableOrInteractive = pickEditable ?
(entity.type !== PONY_TYPE && ((entity.state & EntityState.Editable) !== 0)) :
((entity.flags & EntityFlags.Interactive) !== 0);
return editableOrInteractive && (!isHidden(entity) || pickHidden) && pickAny(entity, point);
}
function pickEntity(
entity: Entity, point: Point, ignorePonies: boolean, pickHidden: boolean, pickEditable: boolean
): boolean {
return (!ignorePonies || entity.type !== PONY_TYPE) && pick(entity, point, pickHidden, pickEditable);
}
function pickByBounds(entity: Entity, rect: Rect, pickHidden: boolean): boolean {
if ((entity.flags & EntityFlags.Interactive) === 0 || (isHidden(entity) && !pickHidden)) {
return false;
} else {
const bounds = entity.interactBounds || entity.bounds;
return !!bounds && boundsIntersect(entity.x, entity.y, bounds, 0, 0, rect);
}
}
function pickEntityByBounds(entity: Entity, rect: Rect, ignorePonies: boolean, pickHidden: boolean): boolean {
return (!ignorePonies || entity.type !== PONY_TYPE) && pickByBounds(entity, rect, pickHidden);
}
export function pickAnyEntities(map: WorldMap, point: Point) {
return map.entities.filter(e => pickAnyEvenLights(e, point)).reverse();
}
export function pickEntities(map: WorldMap, point: Point, ignorePonies: boolean, pickHidden: boolean, pickEditable = false) {
return map.entities.filter(e => pickEntity(e, point, ignorePonies, pickHidden, pickEditable)).reverse();
}
export function pickEntitiesByRect(map: WorldMap, rect: Rect, ignorePonies: boolean, pickHidden: boolean) {
return map.entities.filter(e => pickEntityByBounds(e, rect, ignorePonies, pickHidden)).reverse();
}
export function removeRegions(map: WorldMap, coords: number[]) {
if (coords.length === 0)
return;
const entitiesToRemove = new Set<Entity>();
for (let i = 0; i < coords.length; i += 2) {
const x = coords[i];
const y = coords[i + 1];
const index = x + y * map.regionsX;
const region = map.regions[index];
if (region) {
for (const entity of region.entities) {
entitiesToRemove.add(entity);
releaseEntity(entity);
map.entitiesById.delete(entity.id);
}
}
map.regions[index] = undefined;
setTilesDirty(map, x * REGION_SIZE - 1, y * REGION_SIZE - 1, REGION_SIZE + 2, REGION_SIZE + 2);
}
removeEntitiesFromEntities(map, entitiesToRemove);
updateMinMaxRegion(map);
}
export function setRegion(map: WorldMap, x: number, y: number, region: Region) {
if (x >= 0 && y >= 0 && x < map.regionsX && y < map.regionsY) {
const index = x + y * map.regionsX;
const oldRegion = map.regions[index];
if (oldRegion) {
DEVELOPMENT && !TESTS && console.error(`Region already set (${x}, ${y})`);
for (const e of oldRegion.entities.slice()) {
releaseAndRemoveEntityFromMap(map, e);
}
}
map.regions[index] = undefined;
setTilesDirty(map, x * REGION_SIZE - 1, y * REGION_SIZE - 1, REGION_SIZE + 2, REGION_SIZE + 2);
map.regions[index] = region;
updateMinMaxRegion(map);
} else {
DEVELOPMENT && !TESTS && console.error(`Invalid region coords (${x}, ${y})`);
}
}
export function findEntityById(map: WorldMap, id: number) {
return map.entitiesById.get(id);
}
export function addEntity(map: WorldMap, entity: Entity) {
const region = getRegionGlobal(map, entity.x, entity.y);
if (!region) {
throw new Error(`Missing region at ${entity.x} ${entity.y}`);
} else {
addEntityToMapRegion(map, region, entity);
}
}
export function removeEntity(map: WorldMap, entity: Entity) {
removeEntityFromMapRegion(map, entity);
releaseAndRemoveEntityFromMap(map, entity);
}
function releaseAndRemoveEntityFromMap(map: WorldMap, entity: Entity) {
releaseEntity(entity);
removeEntityFromEntities(map, entity);
}
export function removeEntityDirectly(map: WorldMap, entity: Entity) {
forEachRegion(map, region => {
const removed = removeEntityFromRegion(region, entity, map);
if (removed) {
releaseEntity(entity);
removeEntityFromEntities(map, entity);
return false;
} else {
return true;
}
});
}
export function setTile(map: WorldMap, worldX: number, worldY: number, type: TileType) {
const region = getRegionGlobal(map, worldX, worldY);
if (!region)
return;
const x = Math.floor(worldX - region.x * REGION_SIZE);
const y = Math.floor(worldY - region.y * REGION_SIZE);
const old = getRegionTile(region, x, y);
setRegionTile(region, x, y, type);
setTilesDirty(map, worldX - 1, worldY - 1, 3, 3);
if (canWalk(old) !== canWalk(type)) {
setColliderDirty(map, region, x, y);
}
}
export function setColliderDirty(map: IMap<Region | undefined>, region: Region, x: number, y: number) {
region.colliderDirty = true;
if (x === 0) {
const r = getRegionUnsafe(map, region.x - 1, region.y);
r && (r.colliderDirty = true);
} else if (x === (REGION_SIZE - 1)) {
const r = getRegionUnsafe(map, region.x + 1, region.y);
r && (r.colliderDirty = true);
}
if (y === 0) {
const r = getRegionUnsafe(map, region.x, region.y - 1);
r && (r.colliderDirty = true);
} else if (y === (REGION_SIZE - 1)) {
const r = getRegionUnsafe(map, region.x, region.y + 1);
r && (r.colliderDirty = true);
}
}
export function setTileAtRegion(map: WorldMap, regionX: number, regionY: number, x: number, y: number, type: TileType) {
setTile(map, regionX * REGION_SIZE + x, regionY * REGION_SIZE + y, type);
}
export function setTilesDirty(map: IMap<Region | undefined>, ox: number, oy: number, w: number, h: number) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
doRelativeToRegion(map, x + ox, y + oy, (region, x, y) => setRegionTileDirty(region, x, y));
}
}
}
function getTileIndex(map: IMap<Region | undefined>, x: number, y: number) {
const region = getRegionGlobal(map, x, y);
return region ? getRegionTileIndex(region, x - region.x * REGION_SIZE, y - region.y * REGION_SIZE) : 0;
}
export function getElevation(map: WorldMap, x: number, y: number) {
const region = getRegionGlobal(map, x, y);
return region ? getRegionElevation(region, x - region.x * REGION_SIZE, y - region.y * REGION_SIZE) : 0;
}
export function setElevation(map: WorldMap, x: number, y: number, value: number) {
doRelativeToRegion(map, x, y, (region, x, y) => setRegionElevation(region, x, y, value));
}
export function forEachRegion(map: WorldMap, callback: (region: Region) => boolean | void) {
for (let y = map.minRegionY; y <= map.maxRegionY; y++) {
for (let x = map.minRegionX; x <= map.maxRegionX; x++) {
const region = getRegion(map, x, y);
if (region && callback(region) === false) {
return;
}
}
}
}
function updateMinMaxRegion(map: WorldMap) {
map.minRegionX = map.regionsX;
map.minRegionY = map.regionsY;
map.maxRegionX = 0;
map.maxRegionY = 0;
for (let y = 0; y < map.regionsY; y++) {
for (let x = 0; x < map.regionsX; x++) {
if (getRegion(map, x, y)) {
map.minRegionX = Math.min(x, map.minRegionX);
map.minRegionY = Math.min(y, map.minRegionY);
map.maxRegionX = Math.max(x, map.maxRegionX);
map.maxRegionY = Math.max(y, map.maxRegionY);
}
}
}
map.maxRegionX = Math.min(map.maxRegionX, map.regionsX - 1);
map.maxRegionY = Math.min(map.maxRegionY, map.regionsY - 1);
}
function doRelativeToRegion(
map: IMap<Region | undefined>, x: number, y: number, action: (region: Region, x: number, y: number) => void
) {
const region = getRegionGlobal(map, x, y);
if (region) {
const regionX = Math.floor(x - region.x * REGION_SIZE);
const regionY = Math.floor(y - region.y * REGION_SIZE);
action(region, regionX, regionY);
}
}
function addEntityToRegion(region: Region, entity: Entity, map: WorldMap) {
region.entities.push(entity);
if (canCollideWith(entity)) {
region.colliders.push(entity);
invalidateRegionsCollider(region, map);
}
}
function removeEntityFromRegion(region: Region, entity: Entity, map: WorldMap) {
const removed = removeItemFast(region.entities, entity);
if (removed && canCollideWith(entity)) {
removeItemFast(region.colliders, entity);
invalidateRegionsCollider(region, map);
}
return removed;
}
export function addEntityToMapRegion(map: WorldMap, region: Region, entity: Entity) {
if (entity.id !== 0) {
const existing = map.entitiesById.get(entity.id);
if (existing) {
DEVELOPMENT && !TESTS && console.error(`Adding duplicate entity ${entity.id} (` +
`${worldToRegionX(existing.x, map)}, ${worldToRegionY(existing.y, map)} => ` +
`${worldToRegionX(entity.x, map)}, ${worldToRegionY(entity.y, map)})`);
removeEntity(map, existing);
}
map.entitiesById.set(entity.id, entity);
}
if (isPony(entity) && entity.palettePonyInfo === undefined) {
map.poniesToDecode.push(entity);
}
addEntityToRegion(region, entity, map);
map.entities.push(entity);
if (isDrawable(entity)) {
map.entitiesDrawable.push(entity);
}
if (isMoving(entity)) {
map.entitiesMoving.push(entity);
}
if (hasDrawLight(entity)) {
pushUniq(map.entitiesLight, entity);
}
if (hasLightSprite(entity)) {
pushUniq(map.entitiesLightSprite, entity);
}
if (entity.triggerBounds !== undefined) {
pushUniq(map.entitiesTriggers, entity);
}
}
function removeEntityFromEntities(map: WorldMap, entity: Entity) {
map.entitiesById.delete(entity.id);
removeItemFast(map.entities, entity);
removeItem(map.entitiesWithChat, entity);
removeItem(map.entitiesWithNames, entity);
if (isDrawable(entity)) {
removeItem(map.entitiesDrawable, entity);
}
if (isMoving(entity)) {
removeItemFast(map.entitiesMoving, entity);
}
if (isPony(entity)) {
removeItemFast(map.poniesToDecode, entity);
}
if (hasDrawLight(entity)) {
removeItemFast(map.entitiesLight, entity);
}
if (hasLightSprite(entity)) {
removeItemFast(map.entitiesLightSprite, entity);
}
if (entity.triggerBounds !== undefined) {
removeItemFast(map.entitiesTriggers, entity);
}
}
function removeEntitiesFromEntities(map: WorldMap, set: Set<Entity>) {
if (set.size > 0) {
const filter = (entity: Entity) => !set.has(entity);
map.entities = map.entities.filter(filter);
map.entitiesDrawable = map.entitiesDrawable.filter(filter);
map.entitiesWithChat = map.entitiesWithChat.filter(filter);
map.entitiesWithNames = map.entitiesWithNames.filter(filter);
map.entitiesMoving = map.entitiesMoving.filter(filter);
map.poniesToDecode = map.poniesToDecode.filter(filter);
map.entitiesLight = map.entitiesLight.filter(filter);
map.entitiesLightSprite = map.entitiesLightSprite.filter(filter);
map.entitiesTriggers = map.entitiesTriggers.filter(filter);
}
}
function removeEntityFromMapRegion(map: WorldMap, entity: Entity) {
forEachRegion(map, region => !removeEntityFromRegion(region, entity, map));
}
export function getTile<T>(map: IMap<T>, x: number, y: number): TileType {
const region = getRegionGlobal(map, x, y) as any as Region;
if (region) {
const regionX = Math.floor(x - region.x * REGION_SIZE);
const regionY = Math.floor(y - region.y * REGION_SIZE);
return getRegionTile(region, regionX, regionY);
} else {
return TileType.None;
}
}
export function getRegionGlobal<T>(map: IMap<T>, x: number, y: number): T {
const rx = worldToRegionX(x, map);
const ry = worldToRegionY(y, map);
return getRegion(map, rx, ry);
}
export function getRegion<T>(map: IMap<T>, x: number, y: number): T {
if (x < 0 || y < 0 || x >= map.regionsX || y >= map.regionsY) {
throw new Error(`Invalid region coords (${x}, ${y})`);
} else {
return map.regions[((x | 0) + (y | 0) * map.regionsX) | 0];
}
}
export function getRegionUnsafe<T>(map: IMap<T>, x: number, y: number): T | undefined {
if (x < 0 || y < 0 || x >= map.regionsX || y >= map.regionsY) {
return undefined;
} else {
return map.regions[((x | 0) + (y | 0) * map.regionsX) | 0];
}
}
export function addOrRemoveFromEntityList(list: Entity[], entity: Entity, had: boolean, has: boolean) {
if (had !== has) {
if (has) {
pushUniq(list, entity);
} else {
removeItemFast(list, entity);
}
}
}
export function updateEntitiesWithNames(map: WorldMap, hover: Point, player: Entity) {
for (let i = map.entitiesWithNames.length - 1; i >= 0; i--) {
const entity = map.entitiesWithNames[i];
if (!pickAny(entity, hover)) {
map.entitiesWithNames.splice(i, 1);
}
}
const regionX = worldToRegionX(hover.x, map);
const regionY = worldToRegionY(hover.y, map);
const minX = Math.max(0, regionX - 1) | 0;
const minY = Math.max(0, regionY - 1) | 0;
const maxX = Math.min(regionX + 1, map.regionsX - 1) | 0;
const maxY = Math.min(regionY + 1, map.regionsY - 1) | 0;
for (let ry = minY; ry <= maxY; ry++) {
for (let rx = minX; rx <= maxX; rx++) {
const region = getRegion(map, rx, ry);
if (region !== undefined) {
for (const e of region.entities) {
if (e.name !== undefined && e !== player && pickAny(e, hover)) {
pushUniq(map.entitiesWithNames, e);
}
}
}
}
}
}
export function updateEntitiesCoverLifted(map: WorldMap, player: Entity, hideObjects: boolean, delta: number) {
const playerX = toScreenX(player.x);
const playerY = toScreenYWithZ(player.y, player.z);
for (const e of map.entitiesDrawable) {
if (e.coverBounds !== undefined) {
e.coverLifted = hideObjects || containsPoint(toScreenX(e.x), toScreenY(e.y), e.coverBounds, playerX, playerY);
const lifting = e.coverLifting || 0;
if (e.coverLifted && lifting < 1) {
e.coverLifting = Math.min(lifting + delta * 2, 1);
} else if (!e.coverLifted && lifting > 0) {
e.coverLifting = Math.max(lifting - delta * 2, 0);
}
}
}
}
export function updateEntitiesTriggers(map: WorldMap, player: Pony, game: PonyTownGame) {
for (const e of map.entitiesTriggers) {
const on = (e.triggerTall || isPonyOnTheGround(player)) &&
containsPoint(e.x, e.y, e.triggerBounds!, player.x, player.y);
if (e.triggerOn !== on) {
if (on) {
game.send(server => server.interact(e.id));
}
e.triggerOn = on;
}
}
}
export function updateMap(map: WorldMap, delta: number) {
map.tileTime += delta * WATER_FPS;
forEachRegion(map, region => {
if (region.tilesDirty) {
updateTileIndices(region, map);
}
if (region.colliderDirty) {
generateRegionCollider(region, map);
}
});
}
export function getMapHeightAt(map: WorldMap, x: number, y: number, gameTime: number) {
return getTileHeight(getTile(map, x, y), getTileIndex(map, x, y), x, y, gameTime, map.type);
}
export function isInWaterAt(map: IMap<Region | undefined>, x: number, y: number) {
return getTile(map, x, y) === TileType.Water && isInWater(getTileIndex(map, x, y), x, y);
}
export function updateEntities(game: PonyTownGame, gameTime: number, delta: number, safe: boolean) {
TIMING && timeStart('updateEntities');
const map = game.map;
for (const entity of map.entitiesMoving) {
updatePosition(entity, delta, map);
}
for (const entity of map.entities) {
const flags = entity.flags;
if ((flags & EntityFlags.Bobbing) !== 0) {
const bobs = entity.bobs!;
const frame = (((gameTime / 1000) * entity.bobsFps!) | 0) % bobs.length;
entity.z = toWorldZ(bobs[frame]);
} else if ((flags & EntityFlags.StaticY) === 0) {
entity.z = getMapHeightAt(map, entity.x, entity.y, gameTime);
}
if (entity.type === PONY_TYPE) {
const pony = entity as Pony;
updatePonyEntity(pony, delta, gameTime, safe);
const wasSwimming = pony.swimming;
pony.swimming = !isPonyFlying(pony) && isInWaterAt(map, pony.x, pony.y);
if (wasSwimming !== pony.swimming) {
if (isFlyingDown(pony.animator.state)) {
setTimeout(() => playEffect(game, pony, splash.type), 400);
} else {
playEffect(game, pony, splash.type);
}
}
} else if (entity.update !== undefined) {
entity.update(delta, gameTime);
}
if ((flags & EntityFlags.OnOff) !== 0) {
const on = (entity.state & EntityState.On) !== 0;
if (entity.lightOn !== undefined) {
entity.lightOn = on;
}
if (entity.lightSpriteOn !== undefined) {
entity.lightSpriteOn = on;
}
}
if ((flags & EntityFlags.Light) !== 0) {
if (entity.lightOn) {
const move = delta * 0.2;
if (Math.abs(entity.lightScale! - entity.lightTarget!) < move) {
entity.lightScale = entity.lightTarget;
entity.lightTarget = 1 - Math.random() * 0.15;
} else {
entity.lightScale! += entity.lightScale! < entity.lightTarget! ? move : -move;
}
}
}
}
for (let i = map.entitiesWithChat.length - 1; i >= 0; i--) {
const entity = map.entitiesWithChat[i];
const says = entity.says!;
if (says.timer) {
says.timer -= delta;
if (says.timer < 0) {
says.timer = 0;
entity.says = undefined;
map.entitiesWithChat.splice(i, 1);
}
}
}
TIMING && timeEnd();
}
export function invalidatePalettes(entities: Entity[]) {
for (const entity of entities) {
if (isPony(entity)) {
invalidatePalettesForPony(entity);
}
}
}
export function ensureAllVisiblePoniesAreDecoded(map: WorldMap, camera: Camera, paletteManager: PaletteManager) {
const poniesToDecode = map.poniesToDecode;
if (!poniesToDecode.length)
return;
const decode = new Set<number>();
for (let i = 0; i < poniesToDecode.length; i++) {
const pony = poniesToDecode[i];
if (isBoundsVisible(camera, pony.bounds, pony.x, pony.y)) {
decode.add(i);
}
}
if (!decode.size)
return;
if (decode.size > 100) {
paletteManager.deduplicate = false;
}
map.poniesToDecode = poniesToDecode.filter((pony, i) => {
if (pony.palettePonyInfo !== undefined) {
return false;
} else if (decode.has(i)) {
ensurePonyInfoDecoded(pony);
return false;
} else {
return true;
}
});
if (decode.size > 100) {
paletteManager.deduplicate = true;
}
}
export function switchEntityRegion(map: WorldMap, entity: Entity, x: number, y: number) {
removeEntityFromMapRegion(map, entity);
const region = getRegionGlobal(map, x, y);
if (region) {
addEntityToRegion(region, entity, map);
} else {
releaseAndRemoveEntityFromMap(map, entity);
}
}
export function updateMapState(map: WorldMap, prevState: MapState, newState: MapState) {
if (prevState.weather !== newState.weather) {
switch (newState.weather) {
case Weather.None:
removeWeatherEffects(map);
break;
case Weather.Rain:
addRainEffects(map);
break;
}
}
}
function removeWeatherEffects(map: WorldMap) {
const effects = map.entities.filter(e => e.id === 0 && e.type === weatherRain.type);
for (const entity of effects) {
removeEntityDirectly(map, entity);
}
}
function addRainEffects(map: WorldMap) {
forEachRegion(map, region => {
if (region.x === 3 && region.y === 4) { // TEMP: testing
const entity = weatherRain((region.x + 0.5) * REGION_SIZE, (region.y + 0.5) * REGION_SIZE);
addEntity(map, entity);
}
});
}