mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-25 22:25:53 +02:00
Archive commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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)}`;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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`,
|
||||
];
|
||||
@@ -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`,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]],
|
||||
];
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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],
|
||||
]);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user