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,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();
|
||||
}
|
||||
Reference in New Issue
Block a user