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,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
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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
|
||||
| © 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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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>>:(</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 <name></code> or <code>/w <name></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>/<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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user