Archive commit

This commit is contained in:
Erik McClure
2019-08-28 21:15:02 -07:00
commit 735845746e
1435 changed files with 140724 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
h1(focusTitle) About
.row.text-large
.col-lg-7
p.lead.
A game of ponies building a town
h2 Contact
p For all inquiries please send an email to #[b {{contactEmail}}]
//- div
hr
h2.text-success Custom servers
p.
You can #[a.text-success(href="...") download the files] and start your own custom {{title}} server.
p.
The package contains server files and instruction on steps required to start your own server.
It also allows you to customize and modify the game to your liking.
p.
Feel free to use this to host a server for your friends, fandom, language group or just a general
server for everyone.
a.d-block.mx-auto.my-4.btn.btn-lg.btn-outline-success.px-3(href="..." style="max-width: 400px;")
| Download game files
p.
All necessary instructions required to build and run the server are included in README.md file
included in the package.
p.
Running the server requires basic knowledge of configuring and hosting web applications,
it's not intended for beginner users.
hr
div(*ngIf="patreonLink")
h2 Support {{title}}
p.
If you'd like to help keep {{title}} running, and support future development of the project,
please consider #[a([href]="patreonLink" target="_blank" rel="noopener noreferrer") supporting us on Patreon].
support-button.d-block.mx-auto.my-4(style="max-width: 400px;")
p.
Patreon supporter rewards include the following:
ul
li(*ngFor="let r of rewards")
| {{r}}
p.
Note that in order to receive any patreon rewards you need to connect your Patreon to your {{title}} account.
h2 Technology
p.
The entire game is written in #[a(href="http://www.typescriptlang.org/") TypeScript],
a typed superset of JavaScript that compiles to plain JavaScript.
Server side code is running on #[a(href="https://nodejs.org/en/") Node.js]
server with WebSockets for communication.
User interface is built using #[a(href="https://angular.io/") Angular]
framework and the game itself is using WebGL for rendering graphics.
div(*ngIf="credits.length")
h2 The Team
.mb-4
.row.team-members
.col-md-6(*ngFor="let c of credits")
.team-member
.team-avatar.float-left(
[style.background-image]="c.background"
[style.background-position]="c.position"
style="background-size: 328px 246px")
b.text-truncate.sno {{c.name}}
.text-muted {{c.title}}
site-links([links]="c.links")
//- small.text-muted
em avatars by #[a.text-muted(href="link") Name]
h2 Contributors
div(*ngFor="let g of contributors")
h3 {{g.group}}
p(*ngFor="let c of g.contributors")
strong {{c.name}}
span(*ngIf="c.links")
| -
site-links([links]="c.links")
.col-lg-5
h2 Changelog
div(*ngFor="let v of changelog")
h4 {{v.version}}
ul
li(*ngFor="let c of v.changes" [innerHTML]="c")
.text-center.text-muted
.tinyface
+33
View File
@@ -0,0 +1,33 @@
.team-members {
margin-top: 10px;
}
.team-member {
b {
white-space: nowrap;
}
.text-muted {
font-size: 14px;
}
}
.team-avatar {
background: white;
position: relative;
margin: 0 15px 20px 0;
width: 90px;
height: 90px;
border-radius: 50%;
border: solid 4px white;
transform: scale(0.7);
margin-right: 5px;
margin-bottom: 10px;
margin-top: -10px;
> img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
+31
View File
@@ -0,0 +1,31 @@
import { Component } from '@angular/core';
import { emojis } from '../../../client/emoji';
import { getUrl } from '../../../client/rev';
import { CREDITS, CONTRIBUTORS, Credit } from '../../../client/credits';
import { CHANGELOG } from '../../../generated/changelog';
import { SUPPORTER_REWARDS_LIST } from '../../../common/constants';
import { supporterLink, contactEmail } from '../../../client/data';
function toCredit(credit: Credit) {
return {
...credit,
background: `url(${getUrl('images/avatars.jpg')})`,
position: `${(credit.avatarIndex % 4) * -82}px ${Math.floor(credit.avatarIndex / 4) * -82}px`,
};
}
@Component({
selector: 'about',
templateUrl: 'about.pug',
styleUrls: ['about.scss'],
})
export class About {
readonly title = document.title;
readonly emotes = emojis;
readonly credits = CREDITS.map(toCredit);
readonly contributors = CONTRIBUTORS;
readonly changelog = CHANGELOG;
readonly rewards = SUPPORTER_REWARDS_LIST;
readonly patreonLink = supporterLink;
readonly contactEmail = contactEmail;
}
+134
View File
@@ -0,0 +1,134 @@
h1(focusTitle) Account settings
.row.text-large
.col-md-6
form(name="form" (submit)="submit()" style="max-width: 400px; margin-bottom: 100px")
.form-group
h3 Account details
.form-group
label(for="account-id") Account ID
input#account-id.form-control([value]="account?.id" readonly)
.form-group
label(for="account-name") Account Name
input#account-name.form-control(
type="text" name="name" [(ngModel)]="data.name" required [maxlength]="nameMaxLength")
small.form-text.text-warning This name will be visible to other players
.form-group
label(for="birthdate")
| Date of birth
fa-icon.ml-1.text-danger(*ngIf="showAccountAlert" [icon]="alertIcon" title="Missing field value")
date-picker([(date)]="data.birthdate")
.form-group(*ngIf="accountError")
.alert.alert-danger
| {{accountError}}
.form-group(*ngIf="accountSaved")
.alert.alert-success
| Changes saved
.form-group.text-right
button.btn.btn-default(type="submit" [disabled]="!canSubmit" style="min-width: 100px;")
| Save
div(style="margin-bottom: 100px;")
.form-group
h3 Connected social accounts
.form-group
p.text-muted.
If you remove social account, you won't be able to use it to sign-in to the game anymore.
.form-group
ul.list-unstyled
li.item-connected-account.d-flex.justify-content-between(*ngFor="let s of sites")
a([href]="s.url" target="_blank" rel="noopener noreferrer")
fa-icon.mr-1([icon]="icon(s.icon)" [fixedWidth]="true")
b #[span.sr-only {{s.icon}}] {{s.name}}
button.btn.btn-sm.btn-danger(*ngIf="sites.length > 1" (click)="removeSite(s)" [disabled]="removingSite")
| remove
.form-group
p.text-danger.
Please do NOT connect any social sites that #[b do not belong to you!]
Connecting social sites belonging to other people may result in unexpected account merge
and loss of one or both accounts.
p.text-danger.
Please contact our support email if you accidentally merge your account with someone else.
.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(dropdownToggle)
| connect another social site to this account
.dropdown-menu(*dropdownMenu)
button.dropdown-item(*ngFor="let p of providers" (click)="connectSite(p)")
fa-icon.mr-1([icon]="icon(p.id)" [fixedWidth]="true")
| #[span.sr-only {{p.name}}] {{p.name}}
.form-group(*ngIf="mergeError")
.alert.alert-danger
| {{mergeError}}
.form-group(*ngIf="authError")
.alert.alert-danger
| {{authError}}
.form-group(*ngIf="mergedAccount")
.alert.alert-success
| Account connected successfully
.form-group(*ngIf="removedAccount")
.alert.alert-success
| Account removed successfully
.col-md-6
div(style="margin-bottom: 100px;")
.clearfix(*ngIf="showSupporter")
.form-group
h3
fa-icon.mr-2([icon]="starIcon" [ngClass]="supporterClass")
| Thank you for your support
supporter-pony.float-right(#pony (mouseenter)="pony.excite()" (mouseleave)="pony.reset()")
.form-group
p Your reward tier: #[b {{supporterTitle}}]
p Your rewards include:
ul
li(*ngFor="let r of supporterRewards")
| {{r}}
p(*ngIf="supporter > 0")
| Use <code>/ss hello</code> command to use supporter chat color.
p(*ngIf="supporter > 1")
| Use <code>/s1 hello</code> and <code>/s2 hello</code> commands to use lower tier supporter chat colors.
.clearfix(*ngIf="showSupporterInfo")
.form-group
h3 Supporter info
supporter-pony.float-right(#pony (mouseenter)="pony.excite()" (mouseleave)="pony.reset()")
.form-group.text-muted
p.
You've successfully connected your Patreon account to Pony Town.
If you support Pony Town on Patreon your supporter info will show up here.
p.
It should take no longer than 10 minutes to register your Patreon pledges.
div(style="margin-bottom: 100px;")
.form-group
.float-right
span.text-muted Page: {{page + 1}}
button.btn.btn-sm.btn-outline-secondary.ml-2((click)="page = page - 1; pageChanged()" [disabled]="!hides || page === 0")
| Previous page
button.btn.btn-sm.btn-outline-secondary.ml-1((click)="page = page + 1; pageChanged()" [disabled]="!hides || hides.length < hidesPerPage")
| Next page
h3 Hidden players
div(*ngIf="!hides")
.text-muted.text-center.p-4 loading...
ul.list-unstyled(*ngIf="hides")
.text-muted.text-center.p-4(*ngIf="!hides.length") no hidden players
li.item-connected-account.d-flex.align-items-center(*ngFor="let h of hides")
b.flex-grow-1 {{h.name}}
span.text-muted.mr-2 hidden {{h.date}}
button.btn.btn-sm.btn-danger((click)="unhidePlayer(h)")
| unhide player
@@ -0,0 +1,17 @@
.item-connected-account {
padding: 5px;
min-height: 40px;
> a {
position: relative;
top: 6px;
> i {
margin-right: 5px;
}
}
&:hover {
background: rgba(255, 255, 255, 0.05);
}
}
+130
View File
@@ -0,0 +1,130 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ACCOUNT_NAME_MAX_LENGTH, ACCOUNT_NAME_MIN_LENGTH, HIDES_PER_PAGE } from '../../../common/constants';
import { UpdateAccountData, SocialSiteInfo, OAuthProvider, HiddenPlayer } from '../../../common/interfaces';
import {
toSocialSiteInfo, cleanName, supporterTitle, supporterClass, isSupporterOrPastSupporter, supporterRewards
} from '../../../client/clientUtils';
import { oauthProviders } from '../../../client/data';
import { Model } from '../../services/model';
import { getProviderIcon } from '../../shared/sign-in-box/sign-in-box';
import { faStar, faExclamationCircle, faSync } from '../../../client/icons';
@Component({
selector: 'account',
templateUrl: 'account.pug',
styleUrls: ['account.scss'],
})
export class Account implements OnInit, OnDestroy {
readonly refreshIcon = faSync;
readonly starIcon = faStar;
readonly alertIcon = faExclamationCircle;
readonly providers = oauthProviders.filter(p => !p.disabled);
readonly nameMinLength = ACCOUNT_NAME_MIN_LENGTH;
readonly nameMaxLength = ACCOUNT_NAME_MAX_LENGTH;
readonly hidesPerPage = HIDES_PER_PAGE;
data: UpdateAccountData = {
name: '',
birthdate: '',
};
sites?: SocialSiteInfo[];
password?: string;
removingSite?: boolean;
mergeError?: string;
removedAccount?: boolean;
accountError?: string;
accountSaved = false;
hides: HiddenPlayer[] | undefined = undefined;
page = 0;
constructor(private model: Model) {
}
ngOnInit() {
const account = this.account!;
this.sites = account.sites && account.sites.map(toSocialSiteInfo);
this.data = {
name: account.name,
birthdate: account.birthdate,
};
this.pageChanged();
}
ngOnDestroy() {
this.model.mergedAccount = false;
}
pageChanged() {
this.model.getHides(this.page)
.then(result => this.hides = result);
}
get authError() {
return this.model.authError;
}
get mergedAccount() {
return this.model.mergedAccount;
}
get account() {
return this.model.account;
}
get supporter() {
return this.model.supporter;
}
get showSupporter() {
return isSupporterOrPastSupporter(this.account);
}
get canSubmit() {
return this.account && this.data.name && !!cleanName(this.data.name).length;
}
get supporterTitle() {
return supporterTitle(this.account);
}
get supporterClass() {
return supporterClass(this.account);
}
get supporterRewards() {
return supporterRewards(this.account);
}
get showSupporterInfo() {
const account = this.account;
return !!(!this.supporter && account && account.sites && account.sites.some(s => s.provider === 'patreon'));
}
get showAccountAlert() {
return this.model.missingBirthdate;
}
icon(id: string) {
return getProviderIcon(id);
}
submit() {
if (this.canSubmit) {
this.resetAllMessages();
this.data.name = cleanName(this.data.name).substr(0, ACCOUNT_NAME_MAX_LENGTH);
this.model.updateAccount(this.data)
.catch((e: Error) => this.accountError = e.message)
.then(() => this.accountSaved = true);
}
}
removeSite(site: SocialSiteInfo) {
if (confirm('Are you sure you want to remove this social account ?')) {
this.removingSite = true;
this.resetAllMessages();
this.model.removeSite(site.id)
.then(() => this.sites = this.account!.sites!.map(toSocialSiteInfo))
.then(() => this.removedAccount = true)
.catch((e: Error) => this.mergeError = e.message)
.then(() => this.removingSite = false);
}
}
connectSite(provider: OAuthProvider) {
this.model.connectSite(provider);
}
private resetAllMessages() {
this.accountSaved = false;
this.mergeError = undefined;
this.accountError = undefined;
this.removedAccount = false;
this.model.authError = undefined;
this.model.mergedAccount = false;
}
unhidePlayer(player: HiddenPlayer) {
this.model.unhidePlayer(player.id)
.then(() => this.pageChanged())
.catch((e: Error) => console.error(e));
}
}
+66
View File
@@ -0,0 +1,66 @@
import { NgModule, ErrorHandler } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { BrowserModule } from '@angular/platform-browser';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { PopoverModule } from 'ngx-bootstrap/popover';
import { ButtonsModule } from 'ngx-bootstrap/buttons';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { SharedModule } from '../shared/shared.module';
import { App } from './app';
import { Home } from './home/home';
import { Help } from './help/help';
import { About } from './about/about';
import { Account } from './account/account';
import { Character } from './character/character';
import { EditorBox } from './editor-box/editor-box';
import { AuthGuard } from '../services/authGuard';
import { RollbarService, rollbarFactory, RollbarErrorHandler } from '../services/rollbarErrorHandler';
import { ErrorReporter } from '../services/errorReporter';
import { RollbarErrorReporter } from '../services/rollbarErrorReporter';
export const routes: Routes = [
{ path: '', component: Home },
{ path: 'help', component: Help },
{ path: 'about', component: About },
{ path: 'account', component: Account, canActivate: [AuthGuard] },
{ path: 'character', component: Character, canActivate: [AuthGuard] },
{ path: '**', redirectTo: '/', pathMatch: 'full' },
];
@NgModule({
imports: [
BrowserModule,
RouterModule,
FormsModule,
HttpClientModule,
PopoverModule.forRoot(),
ButtonsModule.forRoot(),
TooltipModule.forRoot(),
// TypeaheadModule.forRoot(),
SharedModule,
RouterModule.forRoot(routes),
FontAwesomeModule,
],
declarations: [
App,
Home,
Help,
About,
Account,
Character,
EditorBox,
],
providers: [
{ provide: RollbarService, useFactory: rollbarFactory },
{ provide: ErrorHandler, useClass: RollbarErrorHandler },
{ provide: ErrorReporter, useClass: RollbarErrorReporter },
],
bootstrap: [App],
})
export class AppModule {
}
+61
View File
@@ -0,0 +1,61 @@
#app-game.app-game([hidden]="!playing")
canvas#canvas.unselectable.pixelart
span#stats.d-none.d-sm-inline
pony-box#pony-box(*ngIf="selected" [pony]="selected" (sendMessage)="messageToPony(chatBox, $event)")
editor-box#editor(*hasFeature="'editor'; also playing && isMod")
party-list#party-list
settings-box#settings-box
swap-box#swap-box
notification-list#notifications([notifications]="notifications" [notificationsLength]="notifications.length")
chat-log#chat-log(*ngIf="playing" (toggleType)="chatBox.setChatType($event)" (nameClick)="chatLogNameClick(chatBox, $event)")
action-bar#action-bar(*ngIf="showActionBar" [blurred]="chatBox.isOpen" [editable]="editingActions")
friends-box#friends-box((sendMessage)="messageToFriend(chatBox, $event)")
chat-box#chat-box(#chatBox [disabled]="editingActions")
#game-announcer(#announcer style="display: none")
div(#announcerText aria-live="assertive")
#touch-origin
#touch-position
#range-indicator
div
dropdown-outlet
.container([hidden]="playing")
menu-bar(
*ngIf="!playing" [logo]="true" [account]="account" [loading]="loading" (signIn)="signIn($event)" (signOut)="signOut()")
menu-item(route="/" name="Home" [icon]="homeIcon")
menu-item(route="/help" name="Help" [icon]="helpIcon")
menu-item(route="/about" name="About" [icon]="aboutIcon")
menu-item(route="/character" name="Characters" [icon]="charactersIcon" *ngIf="account")
main
router-outlet
footer.text-muted.text-nowrap(*ngIf="!playing")
.app-version
| version #[b {{version}}]
.text-right
.d-flex.flex-wrap
//- .text-nowrap.mr-2
| &copy; 2016-{{date | date:'yyyy'}}
| #[a.text-muted.mr-1([href]="twitterLink" target="_blank") {{copyright}}] |
.text-nowrap
a.text-muted.mr-2(*ngIf="twitterLink" [href]="twitterLink" target="_blank" title="Twitter")
fa-icon([icon]="twitterIcon" [fixedWidth]="true")
a.text-muted.mr-2(*ngIf="patreonLink" [href]="patreonLink" target="_blank" title="Patreon")
fa-icon([icon]="patreonIcon" [fixedWidth]="true")
a.text-muted.mr-2(*ngIf="contactEmail" href="mailto:{{contactEmail}}" target="_blank" title="Email")
fa-icon([icon]="emailIcon" [fixedWidth]="true")
div
a.text-muted.privacy-policy(href="/privacypolicy.html" target="_blank" rel="noopener") privacy policy
span.mx-2 |
a.text-muted(href="/termsofservice.html" target="_blank" rel="noopener") terms of service
draggable-outlet
ng-template(#reloadModal)
.modal-body
iframe.modal-frame#reload-frame(src="/reload.html" frameBorder="0")
.modal-footer
button.btn.btn-outline-secondary((click)="cancelReloadModal()")
| Cancel
+204
View File
@@ -0,0 +1,204 @@
@import '../../../styles/partials/variables';
:host {
display: block;
position: relative;
}
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
}
main {
padding-top: 50px;
padding-bottom: 100px;
flex-grow: 1;
@include media-breakpoint-down(sm) {
padding-top: 20px;
padding-bottom: 50px;
}
}
footer {
display: flex;
justify-content: space-between;
padding-bottom: 20px;
@include media-breakpoint-down(xs) {
white-space: normal !important;
display: block;
font-size: 16px;
.app-version {
padding-bottom: 10px;
}
.text-right {
text-align: left !important;
}
a {
display: inline-block;
padding: 10px 10px;
&.privacy-policy {
padding-left: 0;
}
}
}
}
.app-game {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
width: 100vw;
height: 100vh;
overflow: hidden;
}
#canvas {
left: 0;
top: 0;
position: fixed;
touch-action: none;
cursor: default;
user-select: none;
}
#pony-box {
position: fixed;
top: 10px;
left: 10px;
}
$chat-pad: 5px;
#chat-box {
position: fixed;
left: $chat-pad;
bottom: $chat-pad;
right: $chat-pad;
height: 0;
}
#chat-log {
position: fixed;
left: $chat-pad;
bottom: $chat-pad + 42px;
right: $chat-pad;
height: 0;
}
#action-bar {
position: fixed;
left: $chat-pad + 45px;
bottom: 0;
}
#party-list {
position: fixed;
top: 105px;
left: 10px;
z-index: 2;
}
#editor {
position: fixed;
left: 5px;
top: 5px;
}
#settings-box {
position: fixed;
top: 5px;
right: 5px;
}
#friends-box {
position: fixed;
top: 5px;
right: 45px;
}
#swap-box {
position: fixed;
top: 5px;
right: 86px;
}
#notifications {
position: fixed;
top: 45px;
right: 5px;
}
#stats {
position: fixed;
right: 5px;
bottom: 0px;
color: #222;
font-size: 12px;
font-family: monospace;
pointer-events: none;
user-select: none;
}
#touch-origin {
background: rgba(0, 0, 0, 0.5);
position: fixed;
left: 0;
top: 0;
height: 100px;
width: 100px;
display: none;
border: 4px solid white;
border-radius: 50%;
will-change: transform;
}
#touch-position {
background: white;
position: fixed;
left: 0;
top: 0;
height: 50px;
width: 50px;
display: none;
border-radius: 50%;
will-change: transform;
}
#range-indicator {
pointer-events: none;
position: fixed;
border: solid 5px white;
border-radius: 50%;
display: none;
}
#game-announcer {
position: fixed;
left: 0;
top: 0;
right: 0;
display: flex;
justify-content: center;
text-align: center;
> div {
font-size: 16px;
padding: 10px 15px;
background: #222;
color: $body-color;
max-width: 400px;
border-radius: 10px;
margin-top: 100px;
}
}
+210
View File
@@ -0,0 +1,210 @@
import { Component, OnInit, OnDestroy, HostListener, ViewChild, ElementRef, TemplateRef } from '@angular/core';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { TooltipConfig } from 'ngx-bootstrap/tooltip';
import { PopoverConfig } from 'ngx-bootstrap/popover';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { Subscription } from 'rxjs';
import { GameService } from '../services/gameService';
import { Model, Friend } from '../services/model';
import { version, host, contactEmail, supporterLink, twitterLink, copyrightName } from '../../client/data';
import { PonyTownGame } from '../../client/game';
import { faTwitter, faPatreon, faEnvelope, faCog, faHome, faGamepad, faInfoCircle, faHorseHead } from '../../client/icons';
import { InstallService } from '../services/installService';
import { OAuthProvider, Entity, FakeEntity, Pony } from '../../common/interfaces';
import { registerServiceWorker, isBrowserOutdated, checkIframeKey } from '../../client/clientUtils';
import { ErrorReporter } from '../services/errorReporter';
import { SECOND, PONY_TYPE } from '../../common/constants';
import { ChatBox } from '../shared/chat-box/chat-box';
import { ChatLogMessage } from '../shared/chat-log/chat-log';
import { isPony } from '../../common/pony';
import { findEntityById } from '../../common/worldMap';
import { isSelected } from '../../client/gameUtils';
export function tooltipConfig() {
return Object.assign(new TooltipConfig(), { container: 'body' });
}
export function popoverConfig() {
return Object.assign(new PopoverConfig(), { container: 'body' });
}
@Component({
selector: 'pony-town-app',
templateUrl: 'app.pug',
styleUrls: ['app.scss'],
providers: [
{ provide: TooltipConfig, useFactory: tooltipConfig },
{ provide: PopoverConfig, useFactory: popoverConfig },
]
})
export class App implements OnInit, OnDestroy {
@ViewChild('announcer', { static: true }) announcer!: ElementRef;
@ViewChild('announcerText', { static: true }) announcerText!: ElementRef;
@ViewChild('reloadModal', { static: true }) reloadModal!: TemplateRef<any>;
@ViewChild('signInModal', { static: true }) signInModal!: TemplateRef<any>;
readonly version = version;
readonly date = new Date();
readonly emailIcon = faEnvelope;
readonly twitterIcon = faTwitter;
readonly patreonIcon = faPatreon;
readonly cogIcon = faCog;
readonly homeIcon = faHome;
readonly helpIcon = faGamepad;
readonly aboutIcon = faInfoCircle;
readonly charactersIcon = faHorseHead;
readonly contactEmail = contactEmail;
readonly patreonLink = supporterLink;
readonly twitterLink = twitterLink;
readonly copyright = copyrightName;
private url = location.pathname;
private reloadModalRef?: BsModalRef;
private reloadInterval?: any;
private subscriptions: Subscription[] = [];
constructor(
private modalService: BsModalService,
private gameService: GameService,
private model: Model,
private game: PonyTownGame,
private router: Router,
private activatedRoute: ActivatedRoute,
private installService: InstallService,
private errorReporter: ErrorReporter,
) {
}
get canInstall() {
return this.installService.canInstall;
}
get loading() {
return this.model.loading;
}
get account() {
return this.model.account;
}
get isMod() {
return this.model.isMod;
}
get notifications() {
return this.game.notifications;
}
get selected() {
return this.gameService.selected;
}
get playing() {
return this.gameService.playing;
}
get showActionBar() {
return this.playing;
}
get editingActions() {
return this.game.editingActions;
}
ngOnInit() {
if (typeof ga !== 'undefined') {
this.subscriptions.push(this.router.events.subscribe(event => {
if (event instanceof NavigationEnd && this.url !== event.url) {
ga('set', 'page', this.url = event.url);
ga('send', 'pageview');
}
}));
}
if (isBrowserOutdated) {
this.errorReporter.disable();
}
if (!DEVELOPMENT) {
registerServiceWorker(`${host}sw.js`, () => {
this.model.updating = true;
setTimeout(() => {
this.model.updatingTakesLongTime = true;
}, 20 * SECOND);
});
}
if (DEVELOPMENT) {
this.subscriptions.push(this.game.announcements.subscribe(message => {
(this.announcer.nativeElement as HTMLElement).style.display = 'flex';
const announcerText = this.announcerText.nativeElement as HTMLElement;
announcerText.textContent = '';
setTimeout(() => announcerText.textContent = message, 100);
}));
}
this.activatedRoute.queryParams.subscribe(({ error, merged, alert }) => {
this.model.authError = error;
this.model.accountAlert = alert;
this.model.mergedAccount = !!merged;
});
this.subscriptions.push(this.model.protectionErrors.subscribe(() => {
this.openReloadModal();
}));
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
@HostListener('window:focus')
focus() {
this.model.verifyAccount();
}
signIn(provider: OAuthProvider) {
this.model.signIn(provider);
}
signOut() {
this.model.signOut();
}
openReloadModal() {
if (!this.reloadModalRef) {
this.reloadModalRef = this.modalService.show(
this.reloadModal, { class: 'modal-lg', ignoreBackdropClick: true, keyboard: false });
this.reloadInterval = setInterval(() => {
if (checkIframeKey('reload-frame', 'gep84r9jshge4g')) {
this.cancelReloadModal();
}
}, 500);
}
}
cancelReloadModal() {
if (this.reloadModalRef) {
this.reloadModalRef.hide();
this.reloadModalRef = undefined;
}
clearInterval(this.reloadInterval);
}
chatLogNameClick(chatBox: ChatBox, message: ChatLogMessage) {
if (!message.entityId) {
return;
}
let entity = findEntityById(this.game.map, message.entityId);
if (entity && (!isPony(entity) || entity === this.game.player)) {
return;
}
if (!entity) {
entity = { fake: true, type: PONY_TYPE, id: message.entityId, name: message.name } as FakeEntity as any;
}
if (isSelected(this.game, message.entityId)) {
this.game.whisperTo = entity;
chatBox.setChatType('whisper');
} else {
this.game.select(entity as Pony);
}
}
messageToFriend(chatBox: ChatBox, friend: Friend) {
if (friend.entityId) {
const entity: any = { id: friend.entityId, name: friend.actualName || 'unknown' };
this.messageToPony(chatBox, entity);
}
}
messageToPony(chatBox: ChatBox, pony: Entity) {
setTimeout(() => {
this.game.whisperTo = pony;
chatBox.setChatType('whisper');
});
}
}
@@ -0,0 +1,389 @@
ng-template(#descriptionTooltip)
.text-left
p Description and tags help you organize and search for your ponies.
p Use # tags to be able to quickly find specific ponies like #oc #winter or #unicorn
p
| Use @ tags to sort your ponies. @top to put them at the start of the list, @end at the end,
| and @1 @2 @3 ... to order them manually.
page-loader(*ngIf="!playing")
.row(*ngIf="!playing && !loading")
.col-lg-6.text-center
h1.sr-only(focusTitle) Characters
h2.sr-only Character selection
.mx-auto(style="max-width: 400px;")
support-button.mb-3
install-button.mb-3
character-select(
[(error)]="error" [newButton]="true" [removeButton]="true" (change)="updateMuzzles()"
(preview)="previewPony = $event")
.character-preview-box.mx-auto.my-3((fixToTop)="fixed = $event" [fixToTopOffset]="-40")
character-preview(
#characterPreview [pony]="previewInfo" [state]="state" [noBackground]="true" [noShadow]="fixed"
[name]="previewName" [tag]="previewTag" [extra]="previewExtra" style="z-index: 100")
.form-group.text-center
button.btn.btn-lg.btn-default([disabled]="!canDuplicate" (click)="duplicate()" aria-label="Duplicate character")
| Duplicate
button.btn.btn-lg.btn-default.ml-2([disabled]="!canRevert" (click)="revert()" aria-label="Revert changes")
| Revert
button.btn.btn-lg.btn-default.ml-2([disabled]="!canSave" (click)="save()" aria-label="Save character")
| Save
play-box(label="Save & Play" [(error)]="error")
play-notice
.col-lg-6.character-tabs([setOutlineHidden]="!customOutlines" [class.character-tab-warning]="updateWarning")
h2.sr-only Character customization
tabset(type="pills" *ngIf="loaded" saveActiveTab="character-active-tab" label="Character customization")
tab(title="body")
.character-tab(*tabContent)
div(*ngIf="account?.supporter")
.row.form-group
label.col-sm-4.col-form-label.text-patreon
| Supporter options
.col-sm-8.text-muted.pt-2
custom-checkbox([(checked)]="pony.hideSupport") hide supporter tag
hr.my-2
.row.form-group
label.col-sm-4.col-form-label Advanced options
.col-sm-8.text-muted.pt-1
custom-checkbox([(checked)]="pony.respawnAtSpawn")
| always respawn at spawn point
custom-checkbox([(checked)]="info.customOutlines")
| allow custom outlines
custom-checkbox([(checked)]="info.freeOutlines" (checkedChange)="freeOutlinesChanged($event)")
| don't darken outlines
custom-checkbox(
[(checked)]="info.darkenLockedOutlines" [disabled]="!info.freeOutlines"
(checkedChange)="darkenLockedOutlinesChanged($event)")
| darken locked outlines
.row.form-group(*ngIf="debug")
label.col-sm-4.col-form-label Debug options
.col-sm-8
button.btn.btn-sm.btn-default((click)="addBlush()") Add blush
.row.form-group
label.col-sm-4.col-form-label Export/Import options
.col-sm-8
button.btn.btn-sm.btn-default((click)="exportPony()") Export pony
button.btn.btn-sm.btn-default.ml-1((click)="exportPonies()") Export all ponies
button.btn.btn-sm.btn-default.ml-1((click)="importFile.click()") Import ponies
input.d-none(#importFile type="file" (change)="importPonies(importFile.files[0])")
.row.form-group
label#social-site-label.col-sm-4.col-form-label Show social site
.col-sm-8
.dropdown.btn-group(dropdown)
button.btn.btn-default.dropdown-toggle(
dropdownToggle aria-labelledby="social-site-label" aria-describedby="social-site-help")
fa-icon.mr-1(
*ngIf="site?.icon" [icon]="icon(site.icon)" [fixedWidth]="true" [style.color]="site.color"
size="lg" [attr.aria-label]="site.icon")
strong {{site?.name}}
.dropdown-menu(*dropdownMenu)
a.dropdown-item(*ngFor="let s of sites" (click)="site = s")
fa-icon.mr-1(
*ngIf="s.icon" [icon]="icon(s.icon)" [fixedWidth]="true" [style.color]="s.color"
size="lg" [attr.aria-label]="s.icon")
strong {{s.name}}
.dropdown.btn-group.ml-1(dropdown *ngIf="tags.length > 1")
button.btn.btn-default.dropdown-toggle(dropdownToggle aria-label="Show tag")
span.badge(*ngIf="tag.id" [ngClass]="'badge-' + tag.className") {{tag.id}}
strong(*ngIf="!tag.id") {{tag.name}}
.dropdown-menu(*dropdownMenu)
a.dropdown-item(*ngFor="let t of tags" (click)="tag = t")
span.badge(*ngIf="t.id" [ngClass]="'badge-' + t.className") {{t.id}}
strong(*ngIf="!t.id") {{t.name}}
#social-site-help.text-muted.mt-1
| Shows social site next to your name to prove to others that it's your account.
.row.form-group
label.col-sm-4.col-form-label(for="description")
| Description
fa-icon.text-muted.ml-2([icon]="infoIcon" [tooltip]="descriptionTooltip" containerClass="tooltip-desc")
.col-sm-8
input.form-control#description(
[(ngModel)]="pony.desc" placeholder="example: with a hat #oc #winter" [maxlength]="maxDescLength")
.row.form-group
.col-sm-4.col-lock-box
label(for="frame-input") Animation
check-box.lock-box([icon]="playIcon" [(checked)]="playAnimation" label="Play animation")
.col-sm-8
.input-group
input.form-control#frame-input(
type="number" [(ngModel)]="state.animationFrame" [disabled]="playAnimation" min="0"
title="Animation frame" aria-label="Animation frame")
.input-group-append.dropdown(*ngIf="canSaveFiles" dropdown)
button.btn.btn-default.dropdown-toggle(dropdownToggle *ngIf="canExport" aria-label="Export / Import")
fa-icon([icon]="codeIcon")
.dropdown-menu.dropdown-menu-right(*dropdownMenu)
a.dropdown-item((click)="export()") export all
a.dropdown-item((click)="import()") import data
a.dropdown-item((click)="testSize()") print compressed
a.dropdown-item((click)="testJSON()") print raw
.input-group-append.dropdown(dropdown)
button.btn.btn-default(
*ngIf="canSaveFiles" (click)="export(activeAnimation)" title="Save as PNG" aria-label="Save as PNG")
fa-icon([icon]="saveIcon")
button.btn.btn-default.dropdown-toggle(dropdownToggle style="min-width: 80px")
strong {{animations[activeAnimation]().name}}
.dropdown-menu.dropdown-menu-right(*dropdownMenu)
a.dropdown-item(*ngFor="let a of animations; let i = index" (click)="setActiveAnimation(i)")
| {{a().name}}
hr
fill-outline(
label="Body color" [(fill)]="info.coatFill" [(outline)]="info.coatOutline"
[(outlineLocked)]="info.lockCoatOutline" [outlineHidden]="!customOutlines" (change)="changed()")
hr
set-selection(
label="Horn" [base]="coatFill" [set]="info.horn" [sets]="horns" [compact]="true" (change)="changed()"
[darken]="darken")
hr
.row.form-group
label.col-sm-4.col-form-label Magic color
.col-sm-8
color-picker([(color)]="info.magicColor" label="Magic color")
hr
set-selection(
label="Wings" [base]="coatFill" [set]="info.wings" [sets]="wings" [compact]="true" (change)="changed()"
[darken]="darken")
hr
set-selection(
label="Ears" [base]="coatFill" [set]="info.ears" [sets]="ears" [compact]="true" (change)="changed()"
[darken]="darken")
hr
set-selection(
label="Front hooves" [base]="coatFill" [set]="info.frontHooves" [sets]="frontHooves" [compact]="true"
[darken]="darken" (change)="changed()")
hr
set-selection(
label="Back hooves" [base]="hoovesFill" [set]="info.backHooves" [sets]="backHooves" [compact]="true"
[darken]="darken" (change)="changed()")
hr
butt-mark-editor([info]="info" [state]="buttMarkState")
tab(title="mane")
.character-tab(*tabContent)
set-selection(
label="Mane" [base]="maneFill" [set]="info.mane" [sets]="manes" [nonLockable]="true"
[darken]="darken" (change)="changed()")
hr
set-selection(
label="Back mane" [base]="maneFill" [set]="info.backMane" [sets]="backManes"
[darken]="darken" (change)="changed()")
tab(title="tail")
.character-tab(*tabContent)
set-selection(
label="Tail" [base]="maneFill" [set]="info.tail" [sets]="tails" [darken]="darken" (change)="changed()")
.form-group.text-center
button.btn.btn-sm.btn-default((click)="copyCoatColorToTail()" [disabled]="info.tail.lockFills[0]")
| Use body color
tab(title="face")
.character-tab(*tabContent)
.row.form-group
.col-sm-4.col-lock-box
label#eye-color Eye color
check-box.lock-box(
[(checked)]="info.lockEyeColor" [icon]="lockIcon" (checkedChange)="eyeColorLockChanged($event)"
label="Lock color for both eyes" title="Lock color for both eyes")
.col-sm-8
color-picker([(color)]="info.eyeColorRight" (colorChange)="changed()" labelledBy="eye-color")
.row.form-group(*ngIf="!info.lockEyeColor")
label#eye-color-left.col-sm-4.col-form-label Eye color (left)
.col-sm-8
color-picker([(color)]="info.eyeColorLeft" labelledBy="eye-color-left")
.row.form-group
.col-sm-4.col-lock-box
label#eye-whites-label Eye whites
check-box.lock-box(
[(checked)]="lockEyeWhites" [icon]="lockIcon" (checkedChange)="eyeWhiteLockChanged($event)"
label="Lock left eye white for both eyes" title="Lock left eye white for both eyes")
.col-sm-8
color-picker([(color)]="info.eyeWhites" (colorChange)="changed()" labelledBy="eye-whites-label")
.row.form-group(*ngIf="info.unlockEyeWhites")
label#eye-whites-label-left.col-sm-4.col-form-label Eye whites (left)
.col-sm-8
color-picker([(color)]="info.eyeWhitesLeft" labelledBy="eye-whites-label-left")
.row.form-group
.col-sm-4.col-lock-box
label#eyes {{info.lockEyes ? 'Eyes' : 'Right eye'}}
check-box.lock-box(
[(checked)]="info.lockEyes" [icon]="lockIcon" (checkedChange)="eyeOpennessChanged($event)"
label="Lock both eyes" title="Lock both eyes")
.col-sm-8
sprite-selection(
[(selected)]="info.eyeOpennessRight" [sprites]="eyesRight" [fill]="info.eyeColorRight"
[circle]="info.coatFill" [reverseExtra]="true" [skip]="1" [limit]="9" [darken]="darken"
(selectedChange)="eyeOpennessChanged(info.lockEyes)" aria-labelledby="eyes")
.row.form-group(*ngIf="!info.lockEyes")
.col-sm-4
label#left-eye Left eye
.col-sm-8
sprite-selection(
[(selected)]="info.eyeOpennessLeft" [sprites]="eyesLeft" [fill]="info.eyeColorLeft" [darken]="darken"
[circle]="info.coatFill" [reverseExtra]="true" [skip]="1" [limit]="5" aria-labelledby="left-eye")
.row.form-group
.col-sm-4.col-lock-box
label Eyeshadow
check-box.lock-box(
[(checked)]="info.eyeshadow" (checkedChange)="characterPreview.blink()"
label="Use eye shadow" title="Use eye shadow")
.col-sm-8
color-picker([(color)]="info.eyeshadowColor" [isDisabled]="!info.eyeshadow" label="Eyeshadow color")
.row.form-group
label#eyelashes.col-sm-4.col-form-label Eyelashes
.col-sm-8
sprite-selection(
[(selected)]="info.eyelashes" [sprites]="eyelashes" [fill]="info.eyeColorRight" [darken]="darken"
[circle]="info.coatFill" [reverseExtra]="true" aria-labelledby="eyelashes")
.row.form-group
.col-sm-4.col-lock-box
label#eyelashes-label Eyelash color
check-box.lock-box(
[(checked)]="lockEyelashColor" [icon]="lockIcon" (checkedChange)="eyelashLockChanged($event)"
label="Lock eyelash color for both eyes" title="Lock eyelash color for both eyes")
.col-sm-8
color-picker([(color)]="info.eyelashColor" (colorChange)="changed()" labelledBy="eyelashes-label")
.row.form-group(*ngIf="!lockEyelashColor")
label#eyelashes-label-left.col-sm-4.col-form-label Eyelash color (left)
.col-sm-8
color-picker([(color)]="info.eyelashColorLeft" labelledBy="eyelashes-label-left")
hr
set-selection(
label="Muzzle" base="info.coatFill" [set]="info.nose" [sets]="noses" [darken]="darken"
[compact]="true" (change)="changed(); updateMuzzles()")
hr
set-selection(
label="Markings" [base]="coatFill" [set]="info.head" [sets]="heads" [darken]="darken"
[onlyPatterns]="true" (change)="changed()")
hr
.row.form-group
label#fangs.col-sm-4.col-form-label Fangs
.col-sm-8
sprite-selection(
[(selected)]="info.fangs" [sprites]="fangs" [fill]="info.nose.fills" [darken]="darken"
[outline]="info.nose.outlines" [circle]="info.coatFill" aria-labelledby="fangs")
.row.form-group
label#expression.col-sm-4.col-form-label(for="#expression-selection") Expression
.col-sm-8
sprite-selection#expression-selection(
[(selected)]="info.muzzle" [sprites]="muzzles" [fill]="info.nose.fills" [darken]="darken"
[outline]="info.nose.outlines" [circle]="info.coatFill" [limit]="9" aria-labelledby="expression")
hr
set-selection(
label="Facial hair" [base]="maneFill" [set]="info.facialHair" [sets]="facialHair" [darken]="darken"
(change)="changed()")
tab(title="other")
.character-tab(*tabContent)
tabset.accessories-tabset(saveActiveTab="character-active-accessory-tab" label="Character accessories")
tab(title="head")
.accessory-tab(*tabContent)
set-selection(
label="Head accessories" [set]="info.headAccessory" [sets]="headAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
hr
set-selection(
label="Ear accessories" [set]="info.earAccessory" [sets]="earAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
hr
set-selection(
label="Face accessories" [set]="info.faceAccessory" [sets]="faceAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
tab(title="neck")
.accessory-tab(*tabContent)
set-selection(
label="Neck accessories" [set]="info.neckAccessory" [sets]="neckAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
tab(title="legs")
.accessory-tab(*tabContent)
.row.form-group
label.col-sm-4.col-form-label Use the same accessory
.col-sm-8.text-muted.pt-1
.mb-2
custom-checkbox([(checked)]="info.lockBackLegAccessory")
| for front and back legs
.mb-2
custom-checkbox([(checked)]="lockFrontLegAccessory")
| for both front legs
.mb-2
custom-checkbox([(checked)]="lockBackLegAccessory" [disabled]="info.lockBackLegAccessory")
| for both back legs
set-selection(
[label]="info.lockFrontLegAccessories ? 'Front leg accessory' : 'Front leg accessory (left)'"
[set]="info.frontLegAccessory" [sets]="frontLegAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
hr
div(*ngIf="info.unlockFrontLegAccessory")
set-selection(
label="Front leg accessory (right)"
[set]="info.frontLegAccessoryRight" [sets]="frontLegAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
hr
//- .form-group.d-flex.justify-content-center
custom-checkbox([(checked)]="info.lockBackLegAccessory") use the same accessory for back legs
div(*ngIf="!info.lockBackLegAccessory")
set-selection(
[label]="info.lockBackLegAccessories ? 'Back leg accessory' : 'Back leg accessory (left)'"
[set]="info.backLegAccessory" [sets]="backLegAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
hr
div(*ngIf="!info.lockBackLegAccessory && info.unlockBackLegAccessory")
set-selection(
label="Back leg accessory (right)"
[set]="info.backLegAccessoryRight" [sets]="backLegAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
tab(title="chest")
.accessory-tab(*tabContent)
set-selection(
label="Chest accessories" [set]="info.chestAccessory" [sets]="chestAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
set-selection(
*ngIf="hasSleeves(info.chestAccessory.type)" label="Sleeves" [set]="info.sleeveAccessory" [darken]="darken"
[sets]="sleeveAccessories" (change)="changed()")
tab(title="back")
.accessory-tab(*tabContent)
set-selection(
label="Back accessories" [set]="info.backAccessory" [sets]="backAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
tab(title="waist")
.accessory-tab(*tabContent)
set-selection(
label="Waist accessories" [set]="info.waistAccessory" [sets]="waistAccessories" [darken]="darken"
[nonLockable]="true" (change)="changed()")
tab(title="other")
.accessory-tab(*tabContent)
.form-group.d-flex.justify-content-center.pt-2
custom-checkbox([(checked)]="previewExtra") show preview
em.text-muted.ml-2
| (you have to enable these accessories in-game)
set-selection(
label="Extra accessories" [set]="info.extraAccessory" [sets]="extraAccessories"
[darken]="darken" (change)="changed()")
@@ -0,0 +1,62 @@
@import '../../../../styles/partials/variables';
.character-tabs {
min-height: 500px;
margin-bottom: 80px;
}
.character-tab {
background: $dark-bg;
margin-top: 10px;
padding: 10px 15px;
min-height: 900px;
border-radius: $border-radius-sm;
.character-tab-warning & {
box-shadow: 0 0 0 2px $warning;
}
}
.accessory-tab {
margin-top: 10px;
}
.character-preview-box {
position: relative;
width: 400px;
height: 400px;
background: $grass-color;
border-radius: $border-radius-lg;
@include media-breakpoint-down(sm) {
width: 280px;
height: 280px;
}
&.fixed-to-top {
character-preview {
position: fixed;
top: -45px;
z-index: 100;
pointer-events: none;
}
}
}
.col-lock-box {
display: flex;
align-items: flex-start;
justify-content: space-between;
> .lock-box {
@include media-breakpoint-down(xs) {
margin-right: 0;
margin-bottom: 5px;
}
}
> label {
margin: 0;
padding-top: 6px;
}
}
@@ -0,0 +1,501 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { clamp } from 'lodash';
import { PLAYER_NAME_MAX_LENGTH, PLAYER_DESC_MAX_LENGTH } from '../../../common/constants';
import {
PonyInfo, PonyObject, PonyState, SocialSiteInfo, ColorExtraSet, ColorExtra, CharacterTag, PonyEye, Eye, Muzzle,
Iris, ExpressionExtra
} from '../../../common/interfaces';
import { findById, toInt, cloneDeep, delay } from '../../../common/utils';
import {
SLEEVED_ACCESSORIES, frontHooves, mergedBackManes, mergedManes, mergedFacialHair, mergedEarAccessories,
mergedChestAccessories, mergedFaceAccessories, mergedBackAccessories, mergedExtraAccessories, mergedHeadAccessories
} from '../../../client/ponyUtils';
import { defaultPonyState, defaultDrawPonyOptions } from '../../../client/ponyHelpers';
import { toPalette, getBaseFill, syncLockedPonyInfo } from '../../../common/ponyInfo';
import * as sprites from '../../../generated/sprites';
import { boop, trot, stand, sitDownUp, lieDownUp, fly, flyBug } from '../../../client/ponyAnimations';
import { drawCanvas } from '../../../graphics/contextSpriteBatch';
import { Model, getPonyTag } from '../../services/model';
import { loadAndInitSpriteSheets, addTitles, createEyeSprite, addLabels } from '../../../client/spriteUtils';
import { GameService } from '../../services/gameService';
import { TRANSPARENT, BLACK, blushColor } from '../../../common/colors';
import { precompressPony, compressPonyString, decompressPony, decompressPonyString } from '../../../common/compressPony';
import { saveCanvas } from '../../../client/canvasUtils';
import { drawPony } from '../../../client/ponyDraw';
import { getProviderIcon } from '../../shared/sign-in-box/sign-in-box';
import { faPlay, faLock, faSave, faCode, faInfoCircle } from '../../../client/icons';
import { isFileSaverSupported, createExpression, readFileAsText } from '../../../client/clientUtils';
import { emptyTag, getAvailableTags } from '../../../common/tags';
import { ButtMarkEditorState } from '../../shared/butt-mark-editor/butt-mark-editor';
import { parseColorWithAlpha } from '../../../common/color';
const frontHoofTitles = ['', 'Fetlocks', 'Paws', 'Claws', ''];
const backHoofTitles = ['', 'Fetlocks', 'Paws', '', ''];
const horns = addLabels(sprites.horns, [
'None', 'Unicorn horn', 'Short unicorn horn', 'Curved unicorn horn', 'Tiny deer antlers',
'Short deer antlers', 'Medium deer antlers', 'Large deer antlers', 'Raindeer antlers', 'Goat horns',
'Ram horns', 'Buffalo horns', 'Moose horns', 'Bug antenna', 'Long unicorn horn',
]);
const wings = addLabels(sprites.wings[0]!, [
'None', 'Pegasus wings', 'Bat wings', 'Gryphon wings', 'Bug wings'
]);
const ears = addLabels(sprites.ears, [
'Regular ears', 'Fluffy ears', 'Long feathered ears', 'Bug ears', 'Short feathered ears', 'Deer ears',
]);
const noses = addTitles(sprites.noses[0], ['Pony muzzle', 'Gryphon beak', 'Deer nose']);
const flyAnimations = [{ ...stand, name: 'fly' }, fly, fly, fly, { ...flyBug, name: 'fly' }];
function eyeSprite(e: PonyEye | undefined) {
return createEyeSprite(e, 0, sprites.defaultPalette);
}
@Component({
selector: 'character',
templateUrl: 'character.pug',
styleUrls: ['character.scss'],
})
export class Character implements OnInit, OnDestroy {
readonly debug = DEVELOPMENT || BETA;
readonly playIcon = faPlay;
readonly lockIcon = faLock;
readonly saveIcon = faSave;
readonly codeIcon = faCode;
readonly infoIcon = faInfoCircle;
readonly maxNameLength = PLAYER_NAME_MAX_LENGTH;
readonly maxDescLength = PLAYER_DESC_MAX_LENGTH;
readonly horns = horns;
readonly manes = mergedManes;
readonly backManes = mergedBackManes;
readonly tails = sprites.tails[0];
readonly wings = wings;
readonly ears = ears;
readonly facialHair = mergedFacialHair;
readonly headAccessories = mergedHeadAccessories;
readonly earAccessories = mergedEarAccessories;
readonly faceAccessories = mergedFaceAccessories;
readonly neckAccessories = sprites.neckAccessories[1];
readonly frontLegAccessories = sprites.frontLegAccessories[1];
readonly backLegAccessories = sprites.backLegAccessories[1];
readonly backAccessories = mergedBackAccessories;
readonly chestAccessories = mergedChestAccessories;
readonly sleeveAccessories = sprites.frontLegSleeves[1];
readonly waistAccessories = sprites.waistAccessories[1];
readonly extraAccessories = mergedExtraAccessories;
readonly frontHooves = addTitles(frontHooves[1], frontHoofTitles);
readonly backHooves = addTitles(sprites.backLegHooves[1], backHoofTitles);
readonly animations = [
() => stand,
() => trot,
() => boop,
() => sitDownUp,
() => lieDownUp,
() => flyAnimations[this.previewInfo!.wings!.type || 0],
];
readonly eyelashes: ColorExtraSet = sprites.eyeLeft[1]!.map(eyeSprite);
readonly eyesLeft: ColorExtraSet = sprites.eyeLeft.map(e => e && e[0]).map(eyeSprite);
readonly eyesRight: ColorExtraSet = sprites.eyeRight.map(e => e && e[0]).map(eyeSprite);
readonly noses = noses;
readonly heads = sprites.head1[1];
readonly buttMarkState: ButtMarkEditorState = {
brushType: 'brush',
brush: 'orange',
};
muzzles: ColorExtraSet;
fangs: ColorExtraSet;
tags: CharacterTag[] = [
emptyTag,
];
state = defaultPonyState();
saved: PonyInfo[] = [];
activeAnimation = 0;
loaded = false;
playAnimation = true;
deleting = false;
fixed = false;
previewExtra = false;
previewPony: PonyObject | undefined = undefined;
sites: SocialSiteInfo[] = [];
error?: string;
canSaveFiles = isFileSaverSupported();
private savingLocked = false;
private interval?: any;
private syncTimeout?: any;
private animationTime = 0;
constructor(private gameService: GameService, private model: Model) {
this.createMuzzles();
this.updateMuzzles();
}
private getMuzzleType() {
return clamp(toInt(this.info && this.info.nose && this.info.nose.type), 0, sprites.noses[0].length);
}
createMuzzles() {
const type = this.getMuzzleType();
const happy = sprites.noses[0][type][0];
this.muzzles = sprites.noses
.slice()
.map(n => n[type][0])
.map(({ color, colors, mouth }) => ({ color, colors, extra: mouth, palettes: [sprites.defaultPalette] } as ColorExtra));
this.fangs = [undefined, { color: happy.color, colors: 3, extra: happy.fangs, palettes: [sprites.defaultPalette] }];
}
updateMuzzles() {
const type = this.getMuzzleType();
const happy = sprites.noses[0][type][0];
this.muzzles!.forEach((m, i) => {
if (m) {
const { color, colors, mouth } = sprites.noses[i][type][0];
m.color = color;
m.colors = colors;
m.extra = mouth;
m.timestamp = Date.now();
}
});
const fangs = this.fangs![1]!;
fangs.color = happy.color;
fangs.extra = happy.fangs;
fangs.timestamp = Date.now();
}
get account() {
return this.model.account;
}
get loading() {
return this.model.loading || this.model.updating;
}
get updateWarning() {
return this.gameService.updateWarning;
}
get playing() {
return this.gameService.playing;
}
get previewInfo() {
return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo;
}
get previewName() {
return this.previewPony ? this.previewPony.name : this.pony.name;
}
get previewTag() {
return getPonyTag(this.previewPony || this.pony, this.account);
}
get customOutlines() {
return this.info.customOutlines;
}
get ponies() {
return this.model.ponies;
}
get pony() {
return this.model.pony;
}
set pony(value: PonyObject) {
this.model.selectPony(value);
}
get info() {
return this.pony.ponyInfo!;
}
get maneFill() {
return getBaseFill(this.info.mane);
}
get coatFill() {
return this.info.coatFill;
}
get hoovesFill() {
return getBaseFill(this.info.frontHooves);
}
get canExport() {
return DEVELOPMENT;
}
get site() {
return findById(this.sites, this.pony.site) || this.sites[0];
}
set site(value: SocialSiteInfo) {
this.pony.site = value.id;
}
get tag() {
return findById(this.tags, this.pony.tag) || this.tags[0];
}
set tag(value: CharacterTag) {
this.pony.tag = value.id;
}
get lockEyeWhites() {
return !this.info.unlockEyeWhites;
}
set lockEyeWhites(value) {
this.info.unlockEyeWhites = !value;
}
get darken() {
return !this.info.freeOutlines;
}
get lockFrontLegAccessory() {
return !this.info.unlockFrontLegAccessory;
}
set lockFrontLegAccessory(value) {
this.info.unlockFrontLegAccessory = !value;
}
get lockBackLegAccessory() {
return !this.info.unlockBackLegAccessory;
}
set lockBackLegAccessory(value) {
this.info.unlockBackLegAccessory = !value;
}
get lockEyelashColor() {
return !this.info.unlockEyelashColor;
}
set lockEyelashColor(value) {
this.info.unlockEyelashColor = !value;
}
icon(id: string) {
return getProviderIcon(id);
}
hasSleeves(type: number) {
return SLEEVED_ACCESSORIES.indexOf(type) !== -1;
}
ngOnInit() {
if (this.model.account) {
this.tags.push(...getAvailableTags(this.model.account));
}
this.sites = this.model.sites.filter(s => !!s.name);
this.updateMuzzles();
let last = Date.now();
return loadAndInitSpriteSheets().then(() => {
this.loaded = true;
this.interval = setInterval(() => {
const now = Date.now();
this.update((now - last) / 1000);
last = now;
}, 1000 / 24);
});
}
ngOnDestroy() {
clearInterval(this.interval);
}
changed() {
if (!this.syncTimeout) {
this.syncTimeout = requestAnimationFrame(() => {
this.syncTimeout = undefined;
syncLockedPonyInfo(this.info);
});
}
if (DEVELOPMENT || BETA) {
this.state.blushColor = blushColor(parseColorWithAlpha(this.coatFill || '', 1));
}
}
update(delta: number) {
this.animationTime += delta;
const animation = this.animations[this.activeAnimation]();
this.state.animation = animation;
if (this.playAnimation) {
this.state.animationFrame = Math.floor(this.animationTime * animation.fps) % animation.frames.length;
}
}
copyCoatColorToTail() {
if (this.info.tail && this.info.tail.fills) {
this.info.tail.fills[0] = this.info.coatFill;
this.changed();
}
}
eyeColorLockChanged(locked: boolean) {
if (locked) {
this.info.eyeColorLeft = this.info.eyeColorRight;
}
}
eyeWhiteLockChanged(locked: boolean) {
if (locked) {
this.info.eyeWhitesLeft = this.info.eyeWhites;
}
}
eyeOpennessChanged(locked: boolean) {
if (locked) {
this.info.eyeOpennessLeft = this.info.eyeOpennessRight;
}
}
eyelashLockChanged(locked: boolean) {
if (locked) {
this.info.eyelashColorLeft = this.info.eyelashColor;
}
}
select(pony: PonyObject | undefined) {
if (pony) {
this.deleting = false;
this.pony = pony;
}
}
setActiveAnimation(index: number) {
this.activeAnimation = index;
this.animationTime = 0;
}
freeOutlinesChanged(_free: boolean) {
this.changed();
}
darkenLockedOutlinesChanged(_darken: boolean) {
this.changed();
}
get canSave() {
return !this.model.pending && !!this.pony && !!this.pony.name && !this.savingLocked;
}
save() {
if (this.canSave) {
this.error = undefined;
this.deleting = false;
this.savingLocked = true;
this.model.savePony(this.pony)
.catch((e: Error) => this.error = e.message)
.then(() => delay(2000))
.then(() => this.savingLocked = false);
}
}
get canRevert() {
return !!findById(this.ponies, this.pony.id);
}
revert() {
if (this.canRevert) {
this.select(findById(this.ponies, this.pony.id));
}
}
get canDuplicate() {
return this.ponies.length < this.model.characterLimit;
}
duplicate() {
if (this.canDuplicate) {
this.deleting = false;
this.pony = cloneDeep(this.pony);
this.pony.name = '';
this.pony.id = '';
}
}
export(index?: number) {
const frameWidth = 80;
const frameHeight = 90;
const animations = index === undefined ? this.animations.map(a => a()) : [this.animations[index]()];
const frames = animations.reduce((sum, a) => sum + a.frames.length, 0);
const info = toPalette(this.info);
const options = defaultDrawPonyOptions();
const canvas = drawCanvas(frameWidth * frames, frameHeight, sprites.paletteSpriteSheet, TRANSPARENT, batch => {
let i = 0;
animations.forEach(a => {
for (let f = 0; f < a.frames.length; f++ , i++) {
const state: PonyState = {
...defaultPonyState(),
animation: a,
animationFrame: f,
blinkFrame: 1,
};
drawPony(batch, info, state, i * frameWidth + frameWidth / 2, frameHeight - 10, options);
}
});
});
const name = animations.length === 1 ? animations[0].name : 'all';
saveCanvas(canvas, `${this.pony.name}-${name}.png`);
}
import() {
if (DEVELOPMENT) {
const data = prompt('enter data');
if (data) {
this.importPony(data);
}
}
}
private importPony(data: string) {
if (DEVELOPMENT) {
this.pony.ponyInfo = decompressPonyString(data, true);
const t = decompressPonyString(data, false);
console.log(JSON.stringify(t, undefined, 2));
}
}
addBlush() {
if (DEVELOPMENT || BETA) {
this.state = {
...this.state,
expression: createExpression(Eye.Neutral, Eye.Neutral, Muzzle.Smile, Iris.Forward, Iris.Forward, ExpressionExtra.Blush),
};
this.changed();
}
}
testSize() {
function stringifyValues(values: any[]): string {
return values.map(x => JSON.stringify(x)).join(typeof values[0] === 'object' ? ',\n\t' : ', ');
}
if (DEVELOPMENT) {
const compressed = compressPonyString(this.info);
const regularSize = JSON.stringify(this.info).length;
const ponyInfoNumber = decompressPony(compressed);
const precomp = precompressPony(ponyInfoNumber, BLACK, x => x) as any;
const details = Object.keys(precomp)
.filter(key => key !== 'version')
.map(key => ({ key, values: precomp[key] || [] as any[] }))
.map(({ key, values }) => `${key}: [\n\t${stringifyValues(values)}\n]`)
.join(',\n');
const serialized = compressPonyString(this.info);
console.log(serialized);
console.log(details);
console.log(`${serialized.length} / ${regularSize}`);
}
}
testJSON() {
if (DEVELOPMENT) {
console.log(JSON.stringify(this.info, undefined, 2));
}
}
exportPony() {
const data = ponyToExport(this.pony) + '\r\n';
saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), `${this.pony.name}.txt`);
}
exportPonies() {
const data = this.ponies.map(ponyToExport).join('\r\n') + '\r\n';
saveAs(new Blob([data], { type: 'text/plain;charset=utf-8' }), 'ponies.txt');
}
async importPonies(file: File | undefined) {
if (file) {
const text = await readFileAsText(file);
const lines = text.split(/\r?\n/g);
let imported = 0;
for (const line of lines) {
try {
const [name, info, desc = ''] = line.split(/\t/g);
if (name && info) {
const pony: PonyObject = {
name,
id: '',
info,
desc,
ponyInfo: decompressPonyString(info, true),
};
await this.model.savePony(pony, true);
imported++;
}
} catch (e) {
DEVELOPMENT && console.error(e);
}
}
alert(`Imported ${imported} ponies`);
}
}
}
function ponyToExport(pony: PonyObject) {
return `${pony.name}\t${pony.info}\t${pony.desc || ''}`.trim();
}
@@ -0,0 +1,138 @@
.d-flex
button.game-button.px-2((click)="toggleEditor()")
fa-icon([icon]="editIcon")
.d-flex.align-items-start.pl-2.pt-1(*ngIf="hasEditor")
.dropdown-menu.p-0(*ngIf="hasSelectedEntities" style="display: block; width: 500px; position: absolute; top: 36px; left: 0;")
.px-2
//- name
div
.row.form-group.mb-2
label.col-12.col-form-label
.float-right
button.btn.btn-xs.btn-dark.ml-1((click)="showEntitiesInfo()" title="Show info")
| info
button.btn.btn-xs.btn-danger.ml-1((click)="deleteEntities()" title="Delete entities")
fa-icon([icon]="deleteIcon")
| {{entityName}}
//- position
div(*ngIf="singleEntity")
.row.form-group.mb-2
label.col-4.col-form-label
| position
label.col-1.col-form-label
| x
.col-3
input.form-control([(ngModel)]="entityX" type="number")
label.col-1.col-form-label
| y
.col-3
input.form-control([(ngModel)]="entityY" type="number")
//- light
div(*ngIf="isLightEntity")
.row.form-group.mb-2
label.col-5.col-form-label
| light color
.col-7
color-picker([(color)]="entityLightColor")
.row.form-group.mb-2
label.col-5.col-form-label
| light scale ({{entityLightScale | number:'0.1-1'}})
.col-7.py-2
slider-bar([(value)]="entityLightScale" [min]="0" [max]="3")
//- light sprite
div(*ngIf="isLightSpriteEntity")
.row.form-group.mb-2
label.col-5.col-form-label
| light sprite color
.col-7
color-picker([(color)]="entityLightSpriteColor")
.row.form-group.mb-2
label.col-4.col-form-label
| light sprite
label.col-1.col-form-label
| x
.col-3
input.form-control([(ngModel)]="entityLightSpriteX" type="number")
label.col-1.col-form-label
| y
.col-3
input.form-control([(ngModel)]="entityLightSpriteY" type="number")
.input-group.input-group-sm
input.form-control([(ngModel)]="editorEntity" placeholder="entity")
.input-group-append(dropdown)
button.btn.btn-default.dropdown-toggle(dropdownToggle)
.dropdown-menu(*dropdownMenu style="max-height: 90vh; column-width: 100px; font-size: 10px;")
a.dropdown-item(*ngFor="let e of editorEntities" (click)="editorEntity = e")
| {{e}}
.dropdown.ml-1(dropdown)
button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle)
fa-icon([icon]="cogIcon")
.dropdown-menu(*dropdownMenu)
a.dropdown-item((click)="clearLocalStorage()")
| Clear local storage
a.dropdown-item((click)="editorClear()" title="Remove all placed entities")
| Clear all entities
a.dropdown-item((click)="listEntities()" title="List all placed entities in console")
| List entities
div(*ngIf="dev")
h6.dropdown-header Show (numpad 4)
a.dropdown-item(*ngFor="let field of showFields" (click)="toggleShow(field); $event.stopPropagation()")
fa-icon.mr-1([icon]="isShow(field) ? checkIcon : emptyIcon" [fixedWidth]="true")
| {{field}}
button.btn.btn-sm.btn-default.ml-1(
[btnHighlight]="selectingEntities" (click)="toggleSelecting()" title="Select entities")
fa-icon([icon]="selectIcon")
.dropdown.ml-1(dropdown)
button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle)
| tile: {{tiles[editorTile + 1]}}
.dropdown-menu(*dropdownMenu)
a.dropdown-item(*ngFor="let t of tiles; let i = index" (click)="editorTile = i - 1")
| {{t}}
.dropdown.ml-1(dropdown)
button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle)
| brush: {{editor.brushSize}}
.dropdown-menu(*dropdownMenu)
a.dropdown-item((click)="editor.brushSize = 1") 1
a.dropdown-item((click)="editor.brushSize = 2") 2
a.dropdown-item((click)="editor.brushSize = 3") 3
a.dropdown-item((click)="editor.brushSize = 4") 4
a.dropdown-item((click)="editor.brushSize = 5") 5
a.dropdown-item((click)="editor.brushSize = 6") 6
.dropdown.ml-1(dropdown [autoClose]="false")
button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle)
| global
.dropdown-menu(*dropdownMenu style="width: 400px")
button(style="display: block; width: 0; height: 0;")
.px-2
.row.form-group
label.col-5.col-form-label.d-flex.justify-content-between
| light color
custom-checkbox([(checked)]="editor.customLight")
.col-7
color-picker([(color)]="editor.lightColor" [isDisabled]="!editor.customLight")
.row.form-group.mb-0
label.col-5.col-form-label shadow opacity ({{shadowOpacity}})
.col-7.py-2
slider-bar([(value)]="shadowOpacity" [min]="0" [max]="255")
.dropdown.ml-1(dropdown)
button.btn.btn-sm.btn-default.dropdown-toggle(dropdownToggle)
| engine
.dropdown-menu(*dropdownMenu)
a.dropdown-item(*ngFor="let e of engines" (click)="setEngine(e)" [class.active]="isActiveEngine(e)")
| {{e.name}}
.btn.btn-sm.btn-default.disabled.ml-1(*ngIf="hasElevation")
| elevation: {{editorElevation}}
.btn.btn-sm.btn-default.disabled.ml-1(*ngIf="hasElevation")
| special: {{editorSpecial}}
@@ -0,0 +1,205 @@
import { Component, NgZone } from '@angular/core';
import { uniq } from 'lodash';
import { faEdit, faCog, faDrawPolygon, faTrash, emptyIcon, faCheck } from '../../../client/icons';
import { PonyTownGame, engines } from '../../../client/game';
import { StorageService } from '../../services/storageService';
import { Model, getEntityNameFromType, getEntityNames } from '../../services/model';
import { getAlpha, withAlpha, colorToHexRGB, parseColor } from '../../../common/color';
import { BLACK } from '../../../common/colors';
import { Entity, Engine, EngineInfo, DebugFlags, tileTypeNames } from '../../../common/interfaces';
@Component({
selector: 'editor-box',
templateUrl: 'editor-box.pug',
})
export class EditorBox {
readonly dev = DEVELOPMENT;
readonly cogIcon = faCog;
readonly editIcon = faEdit;
readonly selectIcon = faDrawPolygon;
readonly deleteIcon = faTrash;
readonly checkIcon = faCheck;
readonly emptyIcon = emptyIcon;
readonly tiles = ['---', ...tileTypeNames];
readonly engines = engines;
readonly editorEntities: string[];
readonly showFields: (keyof DebugFlags)[] = ['id', 'bounds', 'collider', 'cover', 'interact', 'trigger'];
private showEditor = false;
constructor(
public model: Model,
private game: PonyTownGame,
private storage: StorageService,
private zone: NgZone,
) {
this.game.editor.type = this.storage.getItem('editor-entity') || 'rock';
this.showEditor = this.storage.getBoolean('show-editor');
this.editorEntities = getEntityNames().slice().sort();
}
get editor() {
return this.game.editor;
}
get hasElevation() {
return this.game.engine === Engine.LayeredTiles;
}
get editorElevation() {
return this.game.editor.elevation;
}
get editorSpecial() {
return this.game.editor.special;
}
get editorEntity() {
return this.game.editor.type;
}
set editorEntity(value: string) {
this.game.editor.type = value;
this.storage.setItem('editor-entity', value);
}
get editorTile() {
return this.game.editor.tile;
}
set editorTile(value: number) {
this.game.editor.tile = value;
}
get hasEditor() {
return this.model.isMod && this.showEditor;
}
get oneEntity() {
return this.editor.selectedEntities[0];
}
get singleEntity() {
return this.editor.selectedEntities.length === 1;
}
get hasSelectedEntities() {
return this.editor.selectedEntities.length > 0;
}
get isLightEntity() {
return this.editor.selectedEntities.some(e => !!e.drawLight);
}
get isLightSpriteEntity() {
return this.editor.selectedEntities.some(e => !!e.drawLightSprite);
}
get selectingEntities() {
return this.game.editor.selectingEntities;
}
set selectingEntities(value) {
this.game.editor.selectingEntities = value;
}
get shadowOpacity() {
return getAlpha(this.game.shadowColor);
}
set shadowOpacity(value) {
this.game.shadowColor = withAlpha(this.game.shadowColor, value);
}
private getEntityName(type: number) {
return getEntityNameFromType(type);
}
private getEntityValue<T>(map: (entity: Entity) => T) {
const entity = this.editor.selectedEntities[0] as any;
return map(entity);
}
get entityName() {
const entities = this.editor.selectedEntities;
const types = entities.map(e => e.type);
const names = uniq(types).map(type => this.getEntityName(type)).join(', ');
return types.length === 1 ? `${names} [${entities[0].id}]` : names;
}
get entityLightColor() {
return colorToHexRGB(this.getEntityValue(e => e && e.lightColor || BLACK));
}
set entityLightColor(value) {
this.editor.selectedEntities.forEach(e => e.lightColor = parseColor(value));
}
get entityLightSpriteColor() {
const entity = this.editor.selectedEntities[0];
return colorToHexRGB(entity && entity.lightSpriteColor || BLACK);
}
set entityLightSpriteColor(value) {
this.editor.selectedEntities.forEach(e => e.lightSpriteColor = parseColor(value));
}
get entityLightSpriteX() {
const entity = this.editor.selectedEntities[0];
return entity && entity.lightSpriteX || 0;
}
set entityLightSpriteX(value) {
console.log('set x', value, this.editor.selectedEntities);
this.editor.selectedEntities.forEach(e => e.lightSpriteX = value);
}
get entityLightSpriteY() {
const entity = this.editor.selectedEntities[0];
return entity && entity.lightSpriteY || 0;
}
set entityLightSpriteY(value) {
this.editor.selectedEntities.forEach(e => e.lightSpriteY = value);
}
get entityLightScale() {
return this.editor.selectedEntities.length ? this.editor.selectedEntities[0].lightScaleAdjust : 1;
}
set entityLightScale(value) {
this.editor.selectedEntities.forEach(e => e.lightScaleAdjust = value);
}
get entityX() {
return this.oneEntity.x;
}
set entityX(value) {
this.oneEntity.x = value;
const { id, x, y } = this.oneEntity;
this.game.send(server => server.editorAction({
type: 'move',
entities: [{ id, x, y }],
}));
}
get entityY() {
return this.oneEntity.y;
}
set entityY(value) {
this.oneEntity.y = value;
const { id, x, y } = this.oneEntity;
this.game.send(server => server.editorAction({
type: 'move',
entities: [{ id, x, y }],
}));
}
editorClear() {
this.game.send(server => server.editorAction({ type: 'clear' }));
}
clearLocalStorage() {
this.storage.clear();
}
setEngine(engine: EngineInfo) {
this.game.engine = engine.engine;
}
isActiveEngine(engine: EngineInfo) {
return this.game.engine === engine.engine;
}
toggleEditor() {
this.zone.run(() => {
this.showEditor = !this.showEditor;
this.storage.setBoolean('show-editor', this.showEditor);
});
}
toggleSelecting() {
this.selectingEntities = !this.selectingEntities;
if (!this.selectingEntities) {
this.editor.selectedEntities.length = 0;
}
}
listEntities() {
this.game.send(server => server.editorAction({ type: 'list' }));
}
deleteEntities() {
const entities = this.editor.selectedEntities.map(e => e.id);
this.game.send(server => server.editorAction({ type: 'remove', entities }));
this.editor.selectedEntities.length = 0;
}
showEntitiesInfo() {
console.log(this.editor.selectedEntities);
}
toggleShow(field: keyof DebugFlags) {
(this.game.debug as any)[field] = !this.isShow(field);
this.game.saveDebug();
}
isShow(field: keyof DebugFlags) {
return !!this.game.debug[field];
}
}
+288
View File
@@ -0,0 +1,288 @@
h1(focusTitle) Help
.row.text-large
.col-md-7
section
h2 Contact
p For all inquiries please send an email to #[b {{contactEmail}}]
section
h2 Controls
ul.spaced-list
li
b movement
.pl-2.mb-1.
Use
#[kbd-key(title="up arrow key"): fa-icon([icon]="upIcon")]
#[kbd-key(title="left arrow key"): fa-icon([icon]="leftIcon")]
#[kbd-key(title="down arrow key"): fa-icon([icon]="downIcon")]
#[kbd-key(title="right arrow key"): fa-icon([icon]="rightIcon")]
,
#[kbd-key(title="W key") W]
#[kbd-key(title="A key") A]
#[kbd-key(title="S key") S]
#[kbd-key(title="D key") D]
,
#[kbd-key.gamepad.gamepad-stick(title="Gamepad left stick") L#[.t]#[.l]#[.r]#[.b]]
or #[b touch and drag] to move.
#[br]
Hold #[kbd-key(title="shift key") shift] to walk slowly.
li
b chat
.pl-2.mb-1.
Use #[kbd-key(title="enter key") enter]
or #[kbd-key.gamepad.gamepad-blue(title="Gamepad X button") X]
to open chat box and send a message.
#[br]
Use #[kbd-key(title="escape key") esc]
or #[kbd-key.gamepad.gamepad-blue(title="Gamepad X button") X]
to cancel the message.
li.
hold #[kbd-key(title="shift key") shift] to be able to click on ground and items behind other players
li.
#[b boop] -
#[kbd-key(title="B key") B] ,
#[kbd-key.gamepad.gamepad-red(title="Gamepad B button") B] ,
#[b two finger tap]
or #[code /boop] command
li.
#[b interact] or #[b use item] - #[kbd-key(title="E key") E] or
#[kbd-key.gamepad.gamepad-green(title="Gamepad A button") A]
li.
#[b land] or #[b sit down] or #[b lie down] -
#[kbd-key(title="X key") X] ,
#[kbd-key.gamepad.gamepad-dpad(title="Gamepad dpad down button") #[.dpad-down]]
or #[code /stand] and #[code /sit] and #[code /lie] commands
li.
#[b sit up] or #[b stand up] or #[b fly up] -
#[kbd-key(title="C key") C] ,
#[kbd-key.gamepad.gamepad-dpad(title="Gamepad dpad up button") #[.dpad-up]]
or #[code /sit] and #[code /stand] and #[code /fly] commands
li.
#[b turn head] -
#[kbd-key(title="H key") H]
or #[kbd-key.gamepad.gamepad-stick(title="Gamepad right stick") R#[.t]#[.l]#[.r]#[.b]]
or #[code /turn] command
li.
#[b zoom in / out] -
#[kbd-key(title="P key") P] and #[kbd-key(title="O key") O]
or #[kbd-key.gamepad.gamepad-yellow(title="Gamepad Y button") Y]
li.
#[b hide all text] -
#[kbd-key(title="F2 key") F2]
li.
#[b hide public chat] -
#[kbd-key(title="F3 key") F3]
li.
#[b see through objects] -
#[kbd-key(title="F4 key") F4]
li
| #[b fullscreen] -
span(*ngIf="!mac")
| #[kbd-key(title="F11 key") F11]
span(*ngIf="mac")
| #[kbd-key(title="Control key") ctrl] + #[kbd-key(title="command key") ⌘] + #[kbd-key(title="F key") F]
section
h2 Expressions
p To change your pony face expression in game:
ul.spaced-list
li type any emote in the chat <code>:)</code>
li end your message with an emote <code>Hey there :D</code>
li change expression <b>without saying</b> anything, by writing the emote as a chat command <code>/:D</code>
li make the expression permanent using <code>/e :(</code> command and use <code>/e</code> to get your default expression back
li use both horizontal and vertical emotes: <code>&gt;:(</code> <code>D:</code> <code>O_o</code> <code>^c^</code> <code>6.9</code>
li use emotes with blushies like <code>O//O</code> or <code>O//v//O</code>
section
h2 Chat commands
p Following chat commands are available in the game chat:
ul.spaced-list
li <code>/help</code>, <code>/h</code> or <code>/?</code> - list all available commands
li
| <code>/roll</code>, <code>/rand</code> or <code>/random</code> - roll a random number between 1 and 100
ul.spaced-list
li use <code>/roll 10</code> to roll a number between 1 and 10
li use <code>/roll 5-10</code> to roll a number between 5 and 10
li <code>/say</code> or <code>/s</code> - say in public chat (can be used to switch from party chat)
li <code>/party</code> or <code>/p</code> - say in party chat
li <code>/think</code> or <code>/t</code> - say in thinking bubble
li <code>/whisper &lt;name&gt;</code> or <code>/w &lt;name&gt;</code> - whisper to player (you can also click on player name in chatlog)
li <code>/reply</code> or <code>/r</code> - reply to last incoming whisper
li <code>/e</code> - set a permanent expression like this <code>/e :)</code>
li <code>/unstuck</code> - resets position to spawn point and leaves the game
li <code>/leave</code> - leaves the game
li <code>/turn</code> - turn head around
li <code>/magic</code> - toggle magic light
li <code>/drop</code> - drop held item
li <code>/droptoy</code> - drop held toy
li <code>/open</code> - open gift
li <code>/toys</code> - show number of collected toys
li <code>/boop</code> or <code>/)</code> - do a boop. Can also be used with expression: <code>/) XD</code>
li <code>/lie</code> - lie down or sit up
li <code>/sit</code> - sit down or stand up
li <code>/stand</code> - stand up or land
li <code>/fly</code> - fly up or land
li <code>/sleep</code> or <code>/zzz</code> - fall asleep. Can also be used with some expressions: <code>/zzz :)</code>
li <code>/love</code> or <code>/&lt;3</code> - show floating hearts. Can also be used with expressions: <code>/love :)</code>
li <code>/cry</code> - start crying. Can also be used with expressions: <code>/cry :D</code>
li <code>/blush</code> - start blushing. Can also be used with expressions: <code>/blush :D</code>
li.
Following expression commands are available:
<code>/smile</code> <code>/happy</code>
<code>/frown</code>
<code>/angry</code>
<code>/sad</code>
<code>/thinking</code>
<code>/yawn</code>
<code>/laugh</code> <code>/haha</code> <code>/хаха</code> <code>/jaja</code> <code>/lol</code>
<code>/sneeze</code> <code>/achoo</code>
li
strong House saving
ul
li <code>/savehouse</code> - saves current house setup (single save state for entire account)
li <code>/loadhouse</code> - loads saved house setup (need to be alone in the house or a party leader)
li <code>/resethouse</code> - resets house setup to original state (need to be alone in the house or a party leader)
li <code>/lockhouse</code> - prevents other people from changing the house (need to be alone in the house or a party leader)
li <code>/unlockhouse</code> - enables editing by other people (need to be alone in the house or a party leader)
li <code>/removetoolbox</code> - removes toolbox from the house (need to be alone in the house or a party leader)
li <code>/restoretoolbox</code> - restores toolbox to the house (need to be alone in the house or a party leader)
section
h4 Supporter-only commands
ul.spaced-list
li <code>/ss</code> - supporter colored text
li <code>/s1</code> - tier 1 supporter colored text (for use by higher tier supporters)
li <code>/s2</code> - tier 2 supporter colored text (for use by higher tier supporters)
li <code>/s3</code> - tier 3 supporter colored text (for use by higher tier supporters)
section
h2 Chat emojis
p.
You can use emojis in chat by typing their name surrounded by colons #[samp :apple:]
or by using unicode characters assigned to them. Available emotes:
.emotes
ul
li(*ngFor="let e of emotes")
span.emote-sample.mr-2
emote-box([emote]="e")
| :{{e}}:
.col-md-5
section
h2 Common issues
section
h4 Keys stuck when using iPad keyboard
p.text-fading.
Due to a bug on iOS system some keys will get stuck occasionally,
when the key gets stuck just press it again to unstuck it.
p.text-fading.
We recommend you use arrow keys for movement as those keys do not get stuck.
section
h4 Other players can't see my messages
p.text-fading.
This happens when you get temporarily muted for breaking the rules.
This typically takes from a few hours to a few days depending on severity and frequency of the violation.
p.text-fading.
Avoid breaking the rules to prevent this from happening in the future.
section
h4 Page not loading or stuck updating
p.text-fading.
Try reloading and clearing your browser data.
If that doesn't help you should check if any of the extensions are causing the issue,
you can check that by using incognito mode of your browser.
section
h4 Mobile keyboard is covering the chat box when the game is installed on mobile device
p.text-fading.
Try re-installing the game.
section
h4 Screen black or blinking after joining the game
p.text-fading.
This is usually caused by outdated graphics drivers or browser.
Check if your browser is #[a(href="https://updatemybrowser.org/") up to date] and check
if there are new updates for your graphics card or device.
p.text-fading.
If your browser is already up to date, try installing different browser.
We recommend using #[a(href="https://www.google.com/chrome/") Chrome]
or #[a(href="https://www.mozilla.org/en-US/firefox/new/") Firefox].
p.text-fading.
Some older devices don't support necessary functions and will never work with the game.
section
h4 Graphical glitches, disappearing objects
p.text-fading.
This can be caused by outdated drivers or not enough graphics memory,
see previous points for possible solutions.
section
h4 Cannot connect to the game
p.text-fading.
This can be caused by outdated browser or network issues.
Try updating your browser or installing a different one, see previous points for more info.
p.text-fading.
Connection issue might also be a result of our DDOS protection service,
reloading the page and trying again should solve the issue in that case.
section
h4 Game slow or stuttering
p.text-fading.
This can be caused by outdated browser or some browser extensions.
p.text-fading.
Adblock extension on #[b Chrome] browser is known to cause stuttering in the game,
make sure to disable it to avoid any issues. Other similar extensions may also cause
slowdowns and stuttering in the game.
p.text-fading.
Firefox is known to have issues with stuttering, we recommend
using #[b Chrome] or #[b Edge] browsers for best performance.
section
h4 Why can't I create multiple accounts
p.text-fading.
This restriction is in-place to protect you and the game itself from other players in the game.
p.text-fading.
Allowing multiple accounts for single person invalidates ignore and hide systems that
allow you to ignore and hide other players that are harassing or bothering you in any way.
It also invalidates mute and ban system that is in-place to protect you from players breaking
the game rules to bother or harass others.
p.text-fading.
Allowing use of multiple accounts at the same time would dramatically increase load on the
server and potentially make the game unplayable and laggy for everyone else. Increased amount
of player characters in the game would also reduce rendering performance on slower computers,
especially on mobile devices to the point when a lot of players wouldn't be able to play the
game anymore.
p.text-fading.
Use of multiple accounts also invalidates any limits (like character limit) that we put on the accounts.
The limits are in-place to prevent large amount of players that we have at the moment from
exhausting our limited server resources.
section
h4 Why can't I modify the game with hacks or scripts
p.text-fading.
This restriction is in-place to protect you and the game itself from other players in the game.
p.text-fading.
Almost all third-party scripts used in-game were used to harass other players. Even when the
creator of the script did not intend it to be used in that way, the limited knowledge of the
game and security issues can lead to unintentionally creating a script that's harmful to other
players in the game.
p.text-fading.
Even in the case where the script doesn't seem to impact other players, it often causes performance
issues for the server or other players. That forces us to work on putting more limits on player
interactions and takes development time away from adding new features to the game.
p.text-fading.
Adding more limits on player interactions also makes the game less responsive and more limiting
for everyone, not only the players using scripts. This is especially bad for players on bad
internet connections where network issues can be misinterpreted as use of scripts or spam.
.text-center.text-muted.mt-3
.tinyface
+21
View File
@@ -0,0 +1,21 @@
.emote-sample {
display: inline-block;
width: 24px;
text-align: center;
margin-bottom: 3px;
&:hover {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
}
.emotes {
column-width: 180px;
}
.spaced-list {
> li {
padding-bottom: 2px;
}
}
+19
View File
@@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import { emojis } from '../../../client/emoji';
import { faArrowLeft, faArrowRight, faArrowUp, faArrowDown } from '../../../client/icons';
import { contactEmail } from '../../../client/data';
@Component({
selector: 'help',
templateUrl: 'help.pug',
styleUrls: ['help.scss'],
})
export class Help {
readonly leftIcon = faArrowLeft;
readonly rightIcon = faArrowRight;
readonly upIcon = faArrowUp;
readonly downIcon = faArrowDown;
readonly emotes = emojis.map(e => e.names[0]);
readonly mac = /Macintosh/.test(navigator.userAgent);
readonly contactEmail = contactEmail;
}
+32
View File
@@ -0,0 +1,32 @@
div(*ngIf="!playing")
.text-center.heading
img.pixelart.home-logo(revSrc="images/logo-large.png" alt="Pony Town")
.mx-auto.home-content
page-loader
div(*ngIf="!loading")
h1.sr-only(focusTitle) Home
.form-group(*ngIf="authError")
.alert.alert-danger(role="alert")
| {{authError}}
.alert.alert-warning(role="alert" *ngIf="accountAlert")
| {{accountAlert}}
.form-group(*ngIf="!account")
sign-in-box((signIn)="signIn($event)")
div(*ngIf="!!account")
.form-group
support-button
.form-group(*ngIf="canInstall")
install-button
.form-group
character-select([(error)]="error" [editButton]="true" (preview)="previewPony = $event")
.form-group
character-preview([pony]="previewInfo" [state]="state" [passive]="true" [name]="previewName" [tag]="previewTag")
.form-group.text-center
play-box([(error)]="error")
play-notice
+26
View File
@@ -0,0 +1,26 @@
@import '../../../../styles/partials/variables';
.home-logo {
width: 574px;
@include media-breakpoint-down(sm) {
width: 287px;
}
}
.home-content {
max-width: 400px;
margin-top: 50px;
@include media-breakpoint-down(md) {
margin-top: 15px;
}
}
.heading {
margin-bottom: 50px;
@include media-breakpoint-down(sm) {
margin-bottom: 20px;
}
}
+56
View File
@@ -0,0 +1,56 @@
import { Component } from '@angular/core';
import { Model, getPonyTag } from '../../services/model';
import { defaultPonyState } from '../../../client/ponyHelpers';
import { GameService } from '../../services/gameService';
import { InstallService } from '../../services/installService';
import { OAuthProvider, PonyObject } from '../../../common/interfaces';
@Component({
selector: 'home',
templateUrl: 'home.pug',
styleUrls: ['home.scss'],
})
export class Home {
state = defaultPonyState();
previewPony: PonyObject | undefined = undefined;
error?: string;
constructor(
private gameService: GameService,
private model: Model,
private installService: InstallService,
) {
}
get authError() {
return this.model.authError;
}
get accountAlert() {
return this.model.accountAlert;
}
get canInstall() {
return this.installService.canInstall;
}
get playing() {
return this.gameService.playing;
}
get loading() {
return this.model.loading || this.model.updating;
}
get account() {
return this.model.account;
}
get pony() {
return this.model.pony;
}
get previewInfo() {
return this.previewPony ? this.previewPony.ponyInfo : this.pony.ponyInfo;
}
get previewName() {
return this.previewPony ? this.previewPony.name : this.pony.name;
}
get previewTag() {
return getPonyTag(this.previewPony || this.pony, this.account);
}
signIn(provider: OAuthProvider) {
this.model.signIn(provider);
}
}