diff --git a/README.md b/README.md index 23cc7e5..b021fdd 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ Add `config.json` file in root directory with following content. You can use `co ```javascript { "title": "Pony Town", + "discordLink": "https://discordapp.com/invite/", // optional "twitterLink": "https://twitter.com/", // optional "contactEmail": "", "port": 8090, diff --git a/config-template.json b/config-template.json index ade1ae3..82092ca 100644 --- a/config-template.json +++ b/config-template.json @@ -1,5 +1,6 @@ { "title": "Your Town", + "discordLink": "", "twitterLink": "", "contactEmail": "your_contact_email@example.com", "port": 8090, diff --git a/src/styles/partials/_variables.scss b/src/styles/partials/_variables.scss index 4c78280..1f5c3af 100644 --- a/src/styles/partials/_variables.scss +++ b/src/styles/partials/_variables.scss @@ -9,6 +9,7 @@ $warning: #df871c; $danger: #d9534f; $dark: #222; $patreon: #f96955; +$discord: #7289DA; $none: #777; $dev: #bd3e91; @@ -43,7 +44,8 @@ $theme-colors: ( warning: $warning, danger: $danger, dark: $dark, - patreon: $patreon + patreon: $patreon, + discord: $discord ); $gold: #ffcf00; diff --git a/src/ts/client/data.ts b/src/ts/client/data.ts index bbc750e..294c3b3 100644 --- a/src/ts/client/data.ts +++ b/src/ts/client/data.ts @@ -25,6 +25,7 @@ export const token = attr('data-token'); export const version = attr('data-version'); export const isPublic = attr('data-public') === 'true'; export const supporterLink = attr('data-supporter-link'); +export const discordLink = attr('data-discord-link'); export const twitterLink = attr('data-twitter-link'); export const contactEmail = attr('data-email'); export const copyrightName = attr('data-copyright'); diff --git a/src/ts/client/game.ts b/src/ts/client/game.ts index 8a6f59a..48cf32c 100644 --- a/src/ts/client/game.ts +++ b/src/ts/client/game.ts @@ -11,7 +11,7 @@ import { clamp, lengthOfXY, setFlag, hasFlag, boundsIntersect, point, toInt, lerpColor, distanceXY } from '../common/utils'; import { - OFFLINE_PONY, MAX_SCALE, SUPPORTER_PONY, HOUR, MIN_SCALE, CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX, + OFFLINE_PONY, MAX_SCALE, SUPPORTER_PONY, DISCORD_PONY, HOUR, MIN_SCALE, CAMERA_WIDTH_MIN, CAMERA_WIDTH_MAX, CAMERA_HEIGHT_MIN, CAMERA_HEIGHT_MAX, SECOND, TILE_CHANGE_RANGE, MINUTE, PONY_TYPE, REGION_SIZE, tileWidth, tileHeight } from '../common/constants'; @@ -206,6 +206,7 @@ export class PonyTownGame implements Game { tileSets?: TileSets; offlinePony = createPony(0, 0, OFFLINE_PONY, mockPaletteManager.addArray(defaultPalette), mockPaletteManager); supporterPony = createPony(0, 0, SUPPORTER_PONY, mockPaletteManager.addArray(defaultPalette), mockPaletteManager); + discordPony = createPony(0, 0, DISCORD_PONY, mockPaletteManager.addArray(defaultPalette), mockPaletteManager); scale: number; failedFBO = false; rightOverride?: boolean; @@ -718,6 +719,7 @@ export class PonyTownGame implements Game { this.offlinePony = createPony(0, 0, OFFLINE_PONY, palettes.defaultPalette, this.paletteManager); this.supporterPony = createPony(0, 0, SUPPORTER_PONY, palettes.defaultPalette, this.paletteManager); + this.discordPony = createPony(0, 0, DISCORD_PONY, palettes.defaultPalette, this.paletteManager); initializeToys(this.paletteManager); } catch (e) { this.errorReporter.captureEvent({ name: 'failed game.initWebGL', error: e.message, stack: e.stack }); diff --git a/src/ts/common/constants.ts b/src/ts/common/constants.ts index 725762f..b9927c1 100644 --- a/src/ts/common/constants.ts +++ b/src/ts/common/constants.ts @@ -136,6 +136,7 @@ export const MONTH_NAMES_EN = [ export const OFFLINE_PONY = 'DAKVlZUvLy82QIxomgCfgAYAGIAoQGEBwAEERFEUEA=='; export const SUPPORTER_PONY = 'CAfz9PUFLUnapSD/1wD5aFT////+hHM2QIJkJ8AQLkkADAA6jXrsBT1Iw+wBMJOqoW1C2oW1AAI='; +export const DISCORD_PONY = 'EAlyidplAf/U7f/r9//09vf///9//9QsLzOXp+E2QQJiBLQBPwAMADqNANFZmyFFGhKgorM8ICAEIBAgJGZyzAzMzMzAwMzMzAAAwA=='; // patreon reward tier IDs export const rewardLevel1 = '2255086'; diff --git a/src/ts/components/app/about/about.ts b/src/ts/components/app/about/about.ts index e5470f8..f59c994 100644 --- a/src/ts/components/app/about/about.ts +++ b/src/ts/components/app/about/about.ts @@ -4,7 +4,7 @@ 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'; +import { supporterLink, discordLink, contactEmail } from '../../../client/data'; function toCredit(credit: Credit) { return { @@ -27,5 +27,6 @@ export class About { readonly changelog = CHANGELOG; readonly rewards = SUPPORTER_REWARDS_LIST; readonly patreonLink = supporterLink; + readonly discordLink = discordLink; readonly contactEmail = contactEmail; } diff --git a/src/ts/components/app/app.ts b/src/ts/components/app/app.ts index 8071de1..1b7297e 100644 --- a/src/ts/components/app/app.ts +++ b/src/ts/components/app/app.ts @@ -6,9 +6,9 @@ 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 { version, host, contactEmail, supporterLink, twitterLink, discordLink, copyrightName } from '../../client/data'; import { PonyTownGame } from '../../client/game'; -import { faTwitter, faPatreon, faEnvelope, faCog, faHome, faGamepad, faInfoCircle, faHorseHead } from '../../client/icons'; +import { faTwitter, faPatreon, faEnvelope, faCog, faHome, faGamepad, faInfoCircle, faHorseHead, faDiscord } from '../../client/icons'; import { InstallService } from '../services/installService'; import { OAuthProvider, Entity, FakeEntity, Pony } from '../../common/interfaces'; import { registerServiceWorker, isBrowserOutdated, checkIframeKey } from '../../client/clientUtils'; @@ -47,6 +47,7 @@ export class App implements OnInit, OnDestroy { readonly emailIcon = faEnvelope; readonly twitterIcon = faTwitter; readonly patreonIcon = faPatreon; + readonly discordIcon = faDiscord; readonly cogIcon = faCog; readonly homeIcon = faHome; readonly helpIcon = faGamepad; @@ -54,6 +55,7 @@ export class App implements OnInit, OnDestroy { readonly charactersIcon = faHorseHead; readonly contactEmail = contactEmail; readonly patreonLink = supporterLink; + readonly discordLink = discordLink; readonly twitterLink = twitterLink; readonly copyright = copyrightName; private url = location.pathname; diff --git a/src/ts/components/app/character/character.pug b/src/ts/components/app/character/character.pug index 14427c6..3e6d2cd 100644 --- a/src/ts/components/app/character/character.pug +++ b/src/ts/components/app/character/character.pug @@ -14,6 +14,7 @@ page-loader(*ngIf="!playing") h2.sr-only Character selection .mx-auto(style="max-width: 400px;") support-button.mb-3 + discord-button.mb-3 install-button.mb-3 character-select( [(error)]="error" [newButton]="true" [removeButton]="true" (change)="updateMuzzles()" diff --git a/src/ts/components/app/home/home.pug b/src/ts/components/app/home/home.pug index d4e2f83..e757cf0 100644 --- a/src/ts/components/app/home/home.pug +++ b/src/ts/components/app/home/home.pug @@ -20,6 +20,8 @@ div(*ngIf="!playing") div(*ngIf="!!account") .form-group support-button + .form-group + discord-button .form-group(*ngIf="canInstall") install-button .form-group diff --git a/src/ts/components/shared/discord-button/discord-button.pug b/src/ts/components/shared/discord-button/discord-button.pug new file mode 100644 index 0000000..1695dfc --- /dev/null +++ b/src/ts/components/shared/discord-button/discord-button.pug @@ -0,0 +1,11 @@ +a.btn.btn-lg.btn-outline-discord.btn-block.mb-2( + *ngIf="discordLink" (mouseenter)="pony.excite()" (mouseout)="pony.reset()" [href]="discordLink" target="_blank" + rel="noopener noreferrer") + | Join us on Discord! + svg.discord-logo(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300") + circle(fill="#2C2F33" cx="150" cy="150" r="150") + //path(xmlns="http://www.w3.org/2000/svg" d="M40 12s-4.586-3.59-10-4l-.488.977C34.406 10.176 36.652 11.89 39 14c-4.047-2.066-8.04-4-15-4-6.96 0-10.953 1.934-15 4 2.348-2.11 5.02-4.016 9.488-5.023L18 8c-5.68.535-10 4-10 4s-5.121 7.426-6 22c5.16 5.953 13 6 13 6l1.64-2.184C13.856 36.848 10.716 35.121 8 32c3.238 2.45 8.125 5 16 5s12.762-2.55 16-5c-2.715 3.121-5.855 4.848-8.64 5.816L33 40s7.84-.047 13-6c-.879-14.574-6-22-6-22zM17.5 30c-1.934 0-3.5-1.79-3.5-4s1.566-4 3.5-4 3.5 1.79 3.5 4-1.566 4-3.5 4zm13 0c-1.934 0-3.5-1.79-3.5-4s1.566-4 3.5-4 3.5 1.79 3.5 4-1.566 4-3.5 4z" fill="#8C9EFF" scale="3") + path(xmlns="http://www.w3.org/2000/svg" transform="translate(28,40)" fill="#7289DA" class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z") + path(xmlns="http://www.w3.org/2000/svg" transform="translate(28,40)" fill="#7289DA" class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z") + .discord-pony + discord-pony(#pony [scale]="2") diff --git a/src/ts/components/shared/discord-button/discord-button.scss b/src/ts/components/shared/discord-button/discord-button.scss new file mode 100644 index 0000000..1fa2c31 --- /dev/null +++ b/src/ts/components/shared/discord-button/discord-button.scss @@ -0,0 +1,45 @@ +@import '../../../../styles/partials/variables'; + +:host { + display: block; +} + +.btn { + position: relative; + + @media (max-width: 320px) { + white-space: normal; + } +} + +.discord-logo { + position: absolute; + left: 8px; + top: 5px; + width: 30px; + height: 30px; + + @media (max-width: 290px) { + display: none; + } +} + +.discord-pony { + position: absolute; + right: 0; + bottom: 0; + height: 70px; + width: 100px; + overflow: hidden; + border-radius: $border-radius-lg - 2px; + + > discord-pony { + position: absolute; + bottom: -95px; + left: -20px; + } + + @media (max-width: 360px) { + display: none; + } +} diff --git a/src/ts/components/shared/discord-button/discord-button.ts b/src/ts/components/shared/discord-button/discord-button.ts new file mode 100644 index 0000000..0562199 --- /dev/null +++ b/src/ts/components/shared/discord-button/discord-button.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { discordLink } from '../../../client/data'; + +@Component({ + selector: 'discord-button', + templateUrl: 'discord-button.pug', + styleUrls: ['discord-button.scss'], +}) +export class DiscordButton { + readonly discordLink = discordLink; + constructor() { + } +} diff --git a/src/ts/components/shared/discord-pony/discord-pony.pug b/src/ts/components/shared/discord-pony/discord-pony.pug new file mode 100644 index 0000000..e5bc86f --- /dev/null +++ b/src/ts/components/shared/discord-pony/discord-pony.pug @@ -0,0 +1,3 @@ +character-preview( + #characterPreview [pony]="pony" [state]="state" [noBackground]="true" [noShadow]="true" + [noOutline]="true" [scale]="scale" style="width: 180px; height: 180px; position: relative; top: -20px;") diff --git a/src/ts/components/shared/discord-pony/discord-pony.ts b/src/ts/components/shared/discord-pony/discord-pony.ts new file mode 100644 index 0000000..4191180 --- /dev/null +++ b/src/ts/components/shared/discord-pony/discord-pony.ts @@ -0,0 +1,88 @@ +import { Component, OnInit, OnDestroy, Input, ViewChild } from '@angular/core'; +import { defaultExpression } from '../../../client/ponyUtils'; +import { defaultPonyState } from '../../../client/ponyHelpers'; +import { DISCORD_PONY } from '../../../common/constants'; +import { Expression, Muzzle, HeadAnimation, Iris } from '../../../common/interfaces'; +import { excite } from '../../../client/ponyAnimations'; +import { FrameService, FrameLoop } from '../../services/frameService'; +import { CharacterPreview } from '../character-preview/character-preview'; +import { decompressPonyString } from '../../../common/compressPony'; + +const BLEP: Expression = { + ...defaultExpression, + muzzle: Muzzle.Blep, +}; + +const EXCITED: Expression = { + ...defaultExpression, + muzzle: Muzzle.SmileOpen, +}; + +const DERP: Expression = { + ...defaultExpression, + muzzle: Muzzle.SmileOpen, + leftIris: Iris.Up, +}; + +@Component({ + selector: 'discord-pony', + templateUrl: 'discord-pony.pug', +}) +export class DiscordPony implements OnInit, OnDestroy { + @ViewChild('characterPreview', { static: true }) characterPreview!: CharacterPreview; + @Input() scale = 3; + pony = decompressPonyString(DISCORD_PONY); + state = defaultPonyState(); + private expression?: Expression; + private headAnimation?: HeadAnimation; + private headTime = 0; + private loop: FrameLoop; + constructor(frameService: FrameService) { + this.loop = frameService.create(delta => this.tick(delta)); + } + ngOnInit() { + this.loop.init(); + } + ngOnDestroy() { + this.loop.destroy(); + } + excite() { + this.headTime = 0; + this.headAnimation = excite; + this.expression = Math.random() < 0.2 ? DERP : EXCITED; + } + reset() { + this.expression = undefined; + } + private tick(delta: number) { + this.headTime += delta; + + if (this.headAnimation) { + const frame = Math.floor(this.headTime * this.headAnimation.fps); + + if (frame >= this.headAnimation.frames.length && !this.headAnimation.loop) { + this.headAnimation = undefined; + this.state.headAnimation = undefined; + this.state.headAnimationFrame = 0; + this.characterPreview.blink(); + } else { + this.state.headAnimation = this.headAnimation; + this.state.headAnimationFrame = frame % this.headAnimation.frames.length; + } + } else { + this.state.headAnimation = undefined; + + if (this.expression) { + if (Math.random() < 0.01) { + this.expression = undefined; + } + } else { + if (Math.random() < 0.005) { + this.expression = BLEP; + } + } + } + + this.state.expression = this.expression; + } +} diff --git a/src/ts/components/shared/play-notice/play-notice.ts b/src/ts/components/shared/play-notice/play-notice.ts index 77776cb..f858d52 100644 --- a/src/ts/components/shared/play-notice/play-notice.ts +++ b/src/ts/components/shared/play-notice/play-notice.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { supporterLink } from '../../../client/data'; +import { supporterLink, discordLink } from '../../../client/data'; import { GENERAL_RULES } from '../../../common/constants'; @Component({ @@ -8,5 +8,6 @@ import { GENERAL_RULES } from '../../../common/constants'; }) export class PlayNotice { readonly patreonLink = supporterLink; + readonly discordLink = discordLink; readonly rules = GENERAL_RULES; } diff --git a/src/ts/components/shared/shared.module.ts b/src/ts/components/shared/shared.module.ts index 9bc1180..ed810eb 100644 --- a/src/ts/components/shared/shared.module.ts +++ b/src/ts/components/shared/shared.module.ts @@ -25,6 +25,8 @@ import { SpriteBox } from './sprite-box/sprite-box'; import { SpriteSelection } from './sprite-selection/sprite-selection'; import { SupportButton } from './support-button/support-button'; import { SupporterPony } from './supporter-pony/supporter-pony'; +import { DiscordButton } from './discord-button/discord-button'; +import { DiscordPony } from './discord-pony/discord-pony'; import { SetSelection, SetOutlineHidden } from './set-selection/set-selection'; import { CheckBox } from './check-box/check-box'; import { PortraitBox } from './portrait-box/portrait-box'; @@ -91,6 +93,8 @@ const declarations = [ SpriteSelection, SupportButton, SupporterPony, + DiscordButton, + DiscordPony, SetSelection, SetOutlineHidden, CheckBox, diff --git a/src/ts/server/config.ts b/src/ts/server/config.ts index 0feaca0..eb2ea28 100644 --- a/src/ts/server/config.ts +++ b/src/ts/server/config.ts @@ -5,6 +5,7 @@ export interface AppConfig { title: string; twitterLink?: string; supporterLink?: string; + discordLink?: string; contactEmail?: string; port: number; adminPort?: number; diff --git a/src/ts/server/routes/index.ts b/src/ts/server/routes/index.ts index 13c0dec..a81dfaf 100644 --- a/src/ts/server/routes/index.ts +++ b/src/ts/server/routes/index.ts @@ -100,6 +100,7 @@ export function createIndex(assetsPath: string, adminAssetsPath: string) { title: config.title, twitterLink: config.twitterLink, supporterLink: config.supporterLink, + discordLink: config.discordLink, email: config.contactEmail, logo: `${config.host}${getRevImageURL('logo-120.png')}`, loadingImage: `data:image/png;base64,${loadingImage.toString('base64')}`, diff --git a/views/index.pug b/views/index.pug index de47719..724b289 100644 --- a/views/index.pug +++ b/views/index.pug @@ -50,6 +50,7 @@ html(lang="en") data-key="kmncxb908z" data-version=version data-supporter-link=supporterLink + data-discord-link=discordLink data-twitter-link=twitterLink data-email=email data-copyright=title