Even more improvements
This commit is contained in:
parent
e3c44f775b
commit
ec579650d7
5 changed files with 724 additions and 109 deletions
|
|
@ -311,6 +311,7 @@ export class NPC extends PlayerBase {
|
|||
const leadTime = distance / projectileSpeed;
|
||||
const aim = vec2.scaleAndAdd(vec2.create(), target.center, velocity, leadTime);
|
||||
|
||||
const spread =
|
||||
(npcTuning.spreadBase + distance * npcTuning.spreadPerDistance) *
|
||||
(npcTuning.spreadAggressionFalloff - this.aggression);
|
||||
aim.x += Random.getRandomInRange(-spread, spread);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
html {
|
||||
font-size: 0.85rem;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
|
@ -30,17 +31,21 @@ img {
|
|||
}
|
||||
|
||||
h1 {
|
||||
|
||||
&,
|
||||
* {
|
||||
font-family: 'Comfortaa', sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
font-size: 6rem;
|
||||
text-align: center;
|
||||
padding-bottom: $medium-padding;
|
||||
|
||||
@media (max-width: $height-breakpoint) {
|
||||
font-size: 4.5rem;
|
||||
}
|
||||
|
||||
@media (max-height: $height-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -48,6 +53,7 @@ h1 {
|
|||
|
||||
.red {
|
||||
color: $accent;
|
||||
|
||||
&::selection {
|
||||
color: $accent;
|
||||
background-color: white;
|
||||
|
|
@ -75,8 +81,10 @@ body {
|
|||
top: 0;
|
||||
|
||||
@include background;
|
||||
|
||||
#spinner {
|
||||
@include square(20vmax);
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
@include square(20vmax);
|
||||
}
|
||||
|
|
@ -120,8 +128,10 @@ body {
|
|||
|
||||
&.decla {
|
||||
color: $bright-decla;
|
||||
div:first-child {
|
||||
|
||||
.health {
|
||||
background-color: $bright-decla;
|
||||
|
||||
&:before {
|
||||
background-color: $bright-decla;
|
||||
opacity: 0.3;
|
||||
|
|
@ -131,8 +141,10 @@ body {
|
|||
|
||||
&.red {
|
||||
color: $bright-red;
|
||||
div:first-child {
|
||||
|
||||
.health {
|
||||
background-color: $bright-red;
|
||||
|
||||
&:before {
|
||||
background-color: $bright-red;
|
||||
opacity: 0.4;
|
||||
|
|
@ -140,7 +152,7 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
div:first-child {
|
||||
.health {
|
||||
position: relative;
|
||||
height: 5px;
|
||||
border-radius: 1000px;
|
||||
|
|
@ -155,9 +167,33 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
div:not(:first-child) {
|
||||
letter-spacing: 2px;
|
||||
.charge {
|
||||
height: 3px;
|
||||
margin-top: 2px;
|
||||
border-radius: 1000px;
|
||||
background-color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.9);
|
||||
|
||||
.stat {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
fill: currentColor;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,9 +208,13 @@ body {
|
|||
|
||||
.falling-point {
|
||||
font-size: 1.2em;
|
||||
font-weight: 700;
|
||||
animation: falling-point 2s ease-out forwards;
|
||||
|
||||
&.decla {
|
||||
color: $bright-decla;
|
||||
}
|
||||
|
||||
&.red {
|
||||
color: $bright-red;
|
||||
}
|
||||
|
|
@ -184,6 +224,7 @@ body {
|
|||
transition: transform 150ms;
|
||||
|
||||
@include square($large-icon);
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
@include square($small-icon);
|
||||
}
|
||||
|
|
@ -219,6 +260,56 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.touch-button {
|
||||
display: none;
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
cursor: pointer;
|
||||
|
||||
$size: $large-icon * 1.6;
|
||||
@include square($size);
|
||||
top: auto;
|
||||
left: auto;
|
||||
bottom: $medium-padding;
|
||||
border-radius: 1000px;
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
box-shadow: inset 0 0 8px 3px rgba(0, 0, 0, 0.33);
|
||||
border: $border-width solid rgba(255, 255, 255, 0.4);
|
||||
|
||||
font-size: 1.6rem;
|
||||
line-height: $size - 2 * $border-width;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 6px rgba(0, 0, 0, 0.8);
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.fire {
|
||||
right: $medium-padding;
|
||||
|
||||
&::after {
|
||||
content: '\25C9';
|
||||
}
|
||||
}
|
||||
|
||||
.strength-ring {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -3px;
|
||||
width: calc(100% + 6px);
|
||||
height: calc(100% + 6px);
|
||||
border-radius: 1000px;
|
||||
pointer-events: none;
|
||||
-webkit-mask: radial-gradient(farthest-side,
|
||||
transparent calc(100% - 5px),
|
||||
#000 calc(100% - 5px));
|
||||
mask: radial-gradient(farthest-side,
|
||||
transparent calc(100% - 5px),
|
||||
#000 calc(100% - 5px));
|
||||
}
|
||||
}
|
||||
|
||||
.announcement {
|
||||
top: 25%;
|
||||
transform: translateX(calc(-50% + 50vw)) translateY(-50%);
|
||||
|
|
@ -242,20 +333,65 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.tutorial-hint {
|
||||
top: auto;
|
||||
bottom: calc(#{$medium-padding} + #{$large-icon} * 1.6 + #{$medium-padding});
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
|
||||
font-size: 1.6rem;
|
||||
@include background;
|
||||
z-index: 1000;
|
||||
padding: $small-padding $medium-padding;
|
||||
border-radius: 1000px;
|
||||
opacity: 0.9;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.planet-progress {
|
||||
top: $small-padding;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 50%;
|
||||
display: flex;
|
||||
$height: 8px;
|
||||
$height: 20px;
|
||||
height: $height;
|
||||
|
||||
justify-content: space-between;
|
||||
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
|
||||
|
||||
border-radius: 4px;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.4);
|
||||
border-radius: 1000px;
|
||||
|
||||
.fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: $height;
|
||||
transition: width $animation-time;
|
||||
}
|
||||
|
||||
.fill.decla {
|
||||
right: 50%;
|
||||
background: $bright-decla;
|
||||
color: $bright-decla;
|
||||
border-radius: 1000px 0 0 1000px;
|
||||
}
|
||||
|
||||
.fill.red {
|
||||
left: 50%;
|
||||
background: $bright-red;
|
||||
color: $bright-red;
|
||||
border-radius: 0 1000px 1000px 0;
|
||||
}
|
||||
|
||||
.fill.match-point {
|
||||
z-index: 1;
|
||||
animation: match-point-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
|
@ -263,38 +399,87 @@ body {
|
|||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
background-color: #888;
|
||||
height: 24px;
|
||||
width: 4px;
|
||||
background-color: #fff;
|
||||
height: $height + 8px;
|
||||
width: 3px;
|
||||
border-radius: 1000px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.score {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 3;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
text-shadow:
|
||||
0 0 3px rgba(0, 0, 0, 0.95),
|
||||
0 0 6px rgba(0, 0, 0, 0.8);
|
||||
opacity: 0.8;
|
||||
transition:
|
||||
opacity $animation-time,
|
||||
text-shadow $animation-time,
|
||||
font-size $animation-time;
|
||||
}
|
||||
|
||||
.score.decla {
|
||||
right: calc(50% + #{$small-padding});
|
||||
}
|
||||
|
||||
.score.red {
|
||||
left: calc(50% + #{$small-padding});
|
||||
}
|
||||
|
||||
.score.leading {
|
||||
opacity: 1;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.score.decla.leading {
|
||||
text-shadow:
|
||||
0 0 3px rgba(0, 0, 0, 0.95),
|
||||
0 0 8px $bright-decla;
|
||||
}
|
||||
|
||||
.score.red.leading {
|
||||
text-shadow:
|
||||
0 0 3px rgba(0, 0, 0, 0.95),
|
||||
0 0 8px $bright-red;
|
||||
}
|
||||
|
||||
.you-marker {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
margin-top: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.9);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(50%);
|
||||
|
||||
@include square(24px);
|
||||
|
||||
background-image: url('../static/flag.svg');
|
||||
background-size: contain;
|
||||
bottom: 100%;
|
||||
transform: translateX(-50%);
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 5px solid currentColor;
|
||||
}
|
||||
|
||||
div {
|
||||
height: $height;
|
||||
box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2);
|
||||
&.decla {
|
||||
right: calc(50% + #{$small-padding});
|
||||
color: $bright-decla;
|
||||
}
|
||||
|
||||
div:nth-child(1) {
|
||||
background: $bright-decla;
|
||||
border-radius: 100px 0 0 100px;
|
||||
&.red {
|
||||
left: calc(50% + #{$small-padding});
|
||||
color: $bright-red;
|
||||
}
|
||||
|
||||
div:nth-child(2) {
|
||||
background: $bright-red;
|
||||
border-radius: 0 100px 100px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -308,6 +493,7 @@ body {
|
|||
background-color: transparent;
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $accent;
|
||||
border-radius: $border-radius;
|
||||
|
|
@ -321,6 +507,10 @@ body {
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
box-sizing: content-box;
|
||||
|
||||
user-select: none;
|
||||
|
|
@ -328,6 +518,7 @@ body {
|
|||
|
||||
padding: $medium-padding;
|
||||
@include square($large-icon);
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
@include square($small-icon);
|
||||
padding: $small-padding;
|
||||
|
|
@ -338,3 +529,217 @@ body {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-hud {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 2000;
|
||||
overflow: hidden;
|
||||
|
||||
.hitmarker {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
@include square(26px);
|
||||
opacity: 0.9;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
background-color: white;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
animation: hitmarker-pop 250ms ease-out forwards;
|
||||
}
|
||||
|
||||
.kill-popup {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
font-weight: 700;
|
||||
font-size: 1.4rem;
|
||||
white-space: nowrap;
|
||||
color: $bright-neutral;
|
||||
text-shadow: 0 0 6px rgba(0, 0, 0, 0.9);
|
||||
animation: kill-popup-rise 1200ms ease-out forwards;
|
||||
|
||||
.heal {
|
||||
color: #6fcf6f;
|
||||
}
|
||||
}
|
||||
|
||||
.streak-callout {
|
||||
position: absolute;
|
||||
top: 33%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-weight: 700;
|
||||
font-size: 2.6rem;
|
||||
color: $accent;
|
||||
text-shadow: 0 0 12px rgba(0, 0, 0, 0.9);
|
||||
white-space: nowrap;
|
||||
animation: streak-pop 1400ms ease-out forwards;
|
||||
}
|
||||
|
||||
.killfeed {
|
||||
position: absolute;
|
||||
top: calc(#{$small-padding} + 36px);
|
||||
right: $medium-padding;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
|
||||
.kill-entry {
|
||||
font-size: 1rem;
|
||||
padding: 4px 10px;
|
||||
border-radius: 1000px;
|
||||
@include background;
|
||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.9);
|
||||
animation: kill-entry-fade 4500ms ease-out forwards;
|
||||
|
||||
b {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charge-ring {
|
||||
position: fixed;
|
||||
@include square(56px);
|
||||
margin: -28px 0 0 -28px;
|
||||
border-radius: 1000px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 2000;
|
||||
opacity: 0;
|
||||
transition: opacity 100ms;
|
||||
-webkit-mask: radial-gradient(farthest-side,
|
||||
transparent calc(100% - 6px),
|
||||
#000 calc(100% - 6px));
|
||||
mask: radial-gradient(farthest-side,
|
||||
transparent calc(100% - 6px),
|
||||
#000 calc(100% - 6px));
|
||||
|
||||
&.full {
|
||||
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.8));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes match-point-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 4px 0 currentColor;
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 14px 3px currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes falling-point {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, calc(-50% - 90px));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hitmarker-pop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(1.6);
|
||||
}
|
||||
|
||||
25% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes kill-popup-rise {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -30%) scale(0.8);
|
||||
}
|
||||
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -60%) scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -160%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes streak-pop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) scale(0.6);
|
||||
}
|
||||
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes kill-entry-fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
10% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { CircleLight, Renderer } from 'sdf-2d';
|
||||
|
||||
import {
|
||||
Circle,
|
||||
|
|
@ -6,6 +7,9 @@ import {
|
|||
CharacterBase,
|
||||
CharacterTeam,
|
||||
settings,
|
||||
clamp,
|
||||
clamp01,
|
||||
mix,
|
||||
CommandExecutors,
|
||||
UpdatePropertyCommand,
|
||||
} from 'shared';
|
||||
|
|
@ -13,15 +17,36 @@ import { BeforeDestroyCommand } from '../../commands/types/before-destroy';
|
|||
import { RenderCommand } from '../../commands/types/render';
|
||||
import { StepCommand } from '../../commands/types/step';
|
||||
import { CircleExtrapolator } from '../../helper/extrapolators/circle-extrapolator';
|
||||
import { BlobShape } from '../../shapes/blob-shape';
|
||||
import { LinearExtrapolator } from '../../helper/extrapolators/linear-extrapolator';
|
||||
import { Pointer } from '../../helper/pointer';
|
||||
import { CharacterShape } from '../../shapes/character-shape';
|
||||
import { SoundHandler, Sounds } from '../../sound-handler';
|
||||
import { VibrationHandler } from '../../vibration-handler';
|
||||
import { FeedbackHud } from '../../feedback-hud';
|
||||
|
||||
const muzzleFlashDecaySeconds = 0.12;
|
||||
const hitFlashDecaySeconds = 0.15;
|
||||
|
||||
const killIcon =
|
||||
'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">' +
|
||||
'<path d="M6.2,2.44L18.1,14.34L20.22,12.22L21.63,13.63L19.16,16.1L22.34,19.28C22.73,19.67 22.73,20.3 22.34,20.69L21.63,21.4C21.24,21.79 20.61,21.79 20.22,21.4L17,18.23L14.56,20.7L13.15,19.29L15.27,17.17L3.37,5.27V2.44H6.2M15.89,10L20.63,5.26V2.44H17.8L13.06,7.18L15.89,10M10.94,15L8.11,12.13L5.9,14.34L3.78,12.22L2.37,13.63L4.84,16.1L1.66,19.28C1.27,19.67 1.27,20.3 1.66,20.69L2.37,21.4C2.76,21.79 3.39,21.79 3.78,21.4L7,18.23L9.42,20.7L10.83,19.29L8.71,17.17L10.94,15Z"/></svg>';
|
||||
const deathIcon =
|
||||
'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">' +
|
||||
'<path d="M12,2A9,9 0 0,0 3,11C3,14.03 4.53,16.82 7,18.47V22H9V19H11V22H13V19H15V18.46C17.47,16.81 19,14.03 19,11A9,9 0 0,0 12,2M8,11A2,2 0 0,1 10,13A2,2 0 0,1 8,15A2,2 0 0,1 6,13A2,2 0 0,1 8,11M16,11A2,2 0 0,1 18,13A2,2 0 0,1 16,15A2,2 0 0,1 14,13A2,2 0 0,1 16,11Z"/></svg>';
|
||||
|
||||
export class CharacterView extends CharacterBase {
|
||||
private shape: BlobShape;
|
||||
private shape: CharacterShape;
|
||||
private muzzleFlash: CircleLight;
|
||||
private muzzleFlashIntensity = 0;
|
||||
private hitFlashIntensity = 0;
|
||||
private strength = settings.playerMaxStrength;
|
||||
private strengthExtrapolator = new LinearExtrapolator(settings.playerMaxStrength);
|
||||
private nameElement: HTMLElement = document.createElement('div');
|
||||
private statsElement: HTMLElement = document.createElement('div');
|
||||
private killCountElement: HTMLElement = document.createElement('span');
|
||||
private deathCountElement: HTMLElement = document.createElement('span');
|
||||
private healthElement: HTMLElement = document.createElement('div');
|
||||
private chargeElement: HTMLElement = document.createElement('div');
|
||||
|
||||
public isMainCharacter = false;
|
||||
|
||||
|
|
@ -48,7 +73,12 @@ export class CharacterView extends CharacterBase {
|
|||
rightFoot?: Circle,
|
||||
) {
|
||||
super(id, name, killCount, deathCount, team, health, head, leftFoot, rightFoot);
|
||||
this.shape = new BlobShape(settings.colorIndices[team]);
|
||||
this.shape = new CharacterShape(settings.colorIndices[team]);
|
||||
this.muzzleFlash = new CircleLight(
|
||||
vec2.clone(this.head!.center),
|
||||
settings.paletteDim[settings.colorIndices[team]],
|
||||
0,
|
||||
);
|
||||
|
||||
this.leftFootExtrapolator = new CircleExtrapolator(this.leftFoot!);
|
||||
this.rightFootExtrapolator = new CircleExtrapolator(this.rightFoot!);
|
||||
|
|
@ -56,7 +86,24 @@ export class CharacterView extends CharacterBase {
|
|||
|
||||
this.nameElement.className = 'player-tag ' + this.team;
|
||||
this.nameElement.innerText = this.name;
|
||||
this.healthElement.className = 'health';
|
||||
this.chargeElement.className = 'charge';
|
||||
|
||||
this.statsElement.className = 'stats';
|
||||
this.killCountElement.className = 'value';
|
||||
this.deathCountElement.className = 'value';
|
||||
const killStat = document.createElement('span');
|
||||
killStat.className = 'stat kills';
|
||||
killStat.innerHTML = killIcon;
|
||||
killStat.appendChild(this.killCountElement);
|
||||
const deathStat = document.createElement('span');
|
||||
deathStat.className = 'stat deaths';
|
||||
deathStat.innerHTML = deathIcon;
|
||||
deathStat.appendChild(this.deathCountElement);
|
||||
this.statsElement.append(killStat, deathStat);
|
||||
|
||||
this.nameElement.appendChild(this.healthElement);
|
||||
this.nameElement.appendChild(this.chargeElement);
|
||||
this.nameElement.appendChild(this.statsElement);
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +111,29 @@ export class CharacterView extends CharacterBase {
|
|||
return this.head!.center;
|
||||
}
|
||||
|
||||
public get bodyCenter(): vec2 {
|
||||
const center = vec2.add(vec2.create(), this.head!.center, this.leftFoot!.center);
|
||||
vec2.add(center, center, this.rightFoot!.center);
|
||||
return vec2.scale(center, center, 1 / 3);
|
||||
}
|
||||
|
||||
public get facingDirection(): vec2 {
|
||||
const footAverage = vec2.add(
|
||||
vec2.create(),
|
||||
this.leftFoot!.center,
|
||||
this.rightFoot!.center,
|
||||
);
|
||||
vec2.scale(footAverage, footAverage, 0.5);
|
||||
const forward = vec2.subtract(footAverage, this.head!.center, footAverage);
|
||||
return vec2.length(forward) > 0
|
||||
? vec2.normalize(forward, forward)
|
||||
: vec2.fromValues(0, 1);
|
||||
}
|
||||
|
||||
public get strengthFraction(): number {
|
||||
return clamp01(this.strength / settings.playerMaxStrength);
|
||||
}
|
||||
|
||||
private updateProperty({
|
||||
propertyKey,
|
||||
propertyValue,
|
||||
|
|
@ -78,18 +148,25 @@ export class CharacterView extends CharacterBase {
|
|||
if (propertyKey === 'rightFoot') {
|
||||
this.rightFootExtrapolator.addFrame(propertyValue, rateOfChange);
|
||||
}
|
||||
if (propertyKey === 'strength') {
|
||||
this.strengthExtrapolator.addFrame(propertyValue, rateOfChange);
|
||||
}
|
||||
}
|
||||
|
||||
public setHealth(health: number) {
|
||||
const previousHealth = this.health;
|
||||
const damage = this.health - health;
|
||||
super.setHealth(health);
|
||||
|
||||
if (damage > 0) {
|
||||
SoundHandler.play(
|
||||
Sounds.hit,
|
||||
(0.4 * 2 * (previousHealth - health)) / settings.playerMaxStrength,
|
||||
Math.min(1, (0.8 * damage) / settings.playerMaxStrength),
|
||||
);
|
||||
this.hitFlashIntensity = Math.min(1, 0.4 + damage / settings.playerMaxStrength);
|
||||
|
||||
if (this.isMainCharacter) {
|
||||
VibrationHandler.vibrate(Math.min(200, (previousHealth - this.health) * 4));
|
||||
VibrationHandler.vibrate(Math.min(200, damage * 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,14 +176,58 @@ export class CharacterView extends CharacterBase {
|
|||
}
|
||||
}
|
||||
|
||||
public onHitConfirmed() {
|
||||
if (!this.isMainCharacter) {
|
||||
return;
|
||||
}
|
||||
SoundHandler.play(Sounds.click, 0.4, 1.7);
|
||||
FeedbackHud.hitMarker();
|
||||
}
|
||||
|
||||
public onKillConfirmed(victimName?: string, streak = 1) {
|
||||
if (!this.isMainCharacter) {
|
||||
return;
|
||||
}
|
||||
SoundHandler.play(Sounds.click, 1, 0.7);
|
||||
VibrationHandler.vibrate(60);
|
||||
FeedbackHud.killConfirmed(victimName, streak);
|
||||
}
|
||||
|
||||
private step({ deltaTimeInSeconds }: StepCommand): void {
|
||||
this.head! = this.headExtrapolator.getValue(deltaTimeInSeconds);
|
||||
this.leftFoot! = this.leftFootExtrapolator.getValue(deltaTimeInSeconds);
|
||||
this.rightFoot! = this.rightFootExtrapolator.getValue(deltaTimeInSeconds);
|
||||
|
||||
this.strength = clamp(
|
||||
this.strengthExtrapolator.getValue(deltaTimeInSeconds),
|
||||
0,
|
||||
settings.playerMaxStrength,
|
||||
);
|
||||
|
||||
if (this.muzzleFlashIntensity > 0) {
|
||||
this.muzzleFlashIntensity = Math.max(
|
||||
0,
|
||||
this.muzzleFlashIntensity - deltaTimeInSeconds / muzzleFlashDecaySeconds,
|
||||
);
|
||||
this.muzzleFlash.center = this.head!.center;
|
||||
this.muzzleFlash.intensity = this.muzzleFlashIntensity;
|
||||
}
|
||||
|
||||
if (this.hitFlashIntensity > 0) {
|
||||
this.hitFlashIntensity = Math.max(
|
||||
0,
|
||||
this.hitFlashIntensity - deltaTimeInSeconds / hitFlashDecaySeconds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public onShoot(strength: number) {
|
||||
SoundHandler.play(Sounds.shoot, (0.6 * strength) / settings.playerMaxStrength);
|
||||
const q = clamp01(
|
||||
(strength - settings.chargeShotStrengthMin) /
|
||||
(settings.chargeShotStrengthMax - settings.chargeShotStrengthMin),
|
||||
);
|
||||
SoundHandler.play(Sounds.shoot, mix(0.55, 1, q), mix(1.15, 0.8, q));
|
||||
this.muzzleFlashIntensity = mix(0.35, 1, q);
|
||||
}
|
||||
|
||||
private beforeDestroy(): void {
|
||||
|
|
@ -127,15 +248,34 @@ export class CharacterView extends CharacterBase {
|
|||
|
||||
this.healthElement.style.width =
|
||||
(50 * this.health) / settings.playerMaxHealth + 'px';
|
||||
this.statsElement.innerText = this.getStatsText();
|
||||
this.chargeElement.style.width =
|
||||
(50 * this.strength) / settings.playerMaxStrength + 'px';
|
||||
this.killCountElement.innerText = String(this.killCount);
|
||||
this.deathCountElement.innerText = String(this.deathCount);
|
||||
}
|
||||
|
||||
this.shape.hitFlash = this.hitFlashIntensity;
|
||||
this.shape.gazeTarget = this.calculateGazeTarget(renderer);
|
||||
this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]);
|
||||
renderer.addDrawable(this.shape);
|
||||
|
||||
if (this.muzzleFlashIntensity > 0) {
|
||||
renderer.addDrawable(this.muzzleFlash);
|
||||
}
|
||||
}
|
||||
|
||||
private getStatsText(): string {
|
||||
return `${this.killCount}⚔/${this.deathCount}☠`;
|
||||
private calculateGazeTarget(renderer: Renderer): vec2 {
|
||||
const cursor = Pointer.getDisplayPosition();
|
||||
if (this.isMainCharacter && cursor) {
|
||||
return renderer.displayToWorldCoordinates(cursor);
|
||||
}
|
||||
|
||||
return vec2.scaleAndAdd(
|
||||
vec2.create(),
|
||||
this.head!.center,
|
||||
this.facingDirection,
|
||||
this.head!.radius * 8,
|
||||
);
|
||||
}
|
||||
|
||||
private calculateTextPosition(): vec2 {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,49 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Id, Random, PlanetBase, UpdatePropertyCommand, CommandExecutors } from 'shared';
|
||||
import { vec2, vec3 } from 'gl-matrix';
|
||||
import { CircleLight } from 'sdf-2d';
|
||||
import {
|
||||
Id,
|
||||
Random,
|
||||
PlanetBase,
|
||||
UpdatePropertyCommand,
|
||||
CommandExecutors,
|
||||
CharacterTeam,
|
||||
settings,
|
||||
} from 'shared';
|
||||
import { BeforeDestroyCommand } from '../../commands/types/before-destroy';
|
||||
import { RenderCommand } from '../../commands/types/render';
|
||||
import { StepCommand } from '../../commands/types/step';
|
||||
import { PlanetShape } from '../../shapes/planet-shape';
|
||||
|
||||
type FallingPoint = {
|
||||
velocity: vec2;
|
||||
position: vec2;
|
||||
element: HTMLElement;
|
||||
addedToOverlay: boolean;
|
||||
timeToLive: number;
|
||||
};
|
||||
const fallingPointLifetimeMs = 2000;
|
||||
|
||||
// Global budget for simultaneously-lit capture flares, so a clustered wave of
|
||||
// captures can never white out the SDF exposure — excess flips still pulse the
|
||||
// ring and toast, they just skip the extra light. The acquire/release pair keeps
|
||||
// the count in one place instead of being hand-maintained at every call site.
|
||||
abstract class FlareBudget {
|
||||
private static active = 0;
|
||||
|
||||
public static tryAcquire(): boolean {
|
||||
if (FlareBudget.active >= settings.maxConcurrentFlipFlares) {
|
||||
return false;
|
||||
}
|
||||
FlareBudget.active++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static release(): void {
|
||||
FlareBudget.active = Math.max(0, FlareBudget.active - 1);
|
||||
}
|
||||
}
|
||||
|
||||
export class PlanetView extends PlanetBase {
|
||||
private shape: PlanetShape;
|
||||
private ownershipProgress: HTMLElement;
|
||||
private readonly rotationSpeed: number;
|
||||
|
||||
private flareLight?: CircleLight;
|
||||
private flareIntensity = 0;
|
||||
private holdsFlareSlot = false;
|
||||
|
||||
protected commandExecutors: CommandExecutors = {
|
||||
[RenderCommand.type]: this.draw.bind(this),
|
||||
|
|
@ -27,45 +55,69 @@ export class PlanetView extends PlanetBase {
|
|||
constructor(id: Id, vertices: Array<vec2>, ownership: number) {
|
||||
super(id, vertices);
|
||||
this.shape = new PlanetShape(vertices, ownership);
|
||||
(this.shape as any).randomOffset = Random.getRandom();
|
||||
this.shape.randomOffset = Random.getRandom();
|
||||
this.rotationSpeed =
|
||||
(0.05 + Random.getRandom() * 0.07) * (Random.getRandom() < 0.5 ? -1 : 1);
|
||||
|
||||
this.ownershipProgress = document.createElement('div');
|
||||
this.ownershipProgress.className = 'ownership';
|
||||
}
|
||||
|
||||
private step({ deltaTimeInSeconds }: StepCommand): void {
|
||||
this.shape.randomOffset += deltaTimeInSeconds / 4;
|
||||
this.shape.rotation += deltaTimeInSeconds * this.rotationSpeed;
|
||||
this.shape.colorMixQ = this.ownership;
|
||||
|
||||
this.generatedPointElements.forEach((p) => {
|
||||
vec2.add(
|
||||
p.velocity,
|
||||
p.velocity,
|
||||
vec2.scale(vec2.create(), vec2.fromValues(0, 50), deltaTimeInSeconds),
|
||||
if (this.flareIntensity > 0) {
|
||||
this.flareIntensity = Math.max(
|
||||
0,
|
||||
this.flareIntensity - deltaTimeInSeconds / settings.lampFlareDecaySeconds,
|
||||
);
|
||||
|
||||
vec2.add(
|
||||
p.position,
|
||||
p.position,
|
||||
vec2.scale(vec2.create(), p.velocity, deltaTimeInSeconds),
|
||||
);
|
||||
|
||||
p.timeToLive -= deltaTimeInSeconds;
|
||||
});
|
||||
if (this.flareLight) {
|
||||
this.flareLight.intensity =
|
||||
settings.lampFlareIntensity * this.flareIntensity * this.flareIntensity;
|
||||
}
|
||||
|
||||
private generatedPointElements: Array<FallingPoint> = [];
|
||||
if (this.flareIntensity === 0) {
|
||||
this.releaseFlareSlot();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private releaseFlareSlot(): void {
|
||||
if (this.holdsFlareSlot) {
|
||||
this.holdsFlareSlot = false;
|
||||
FlareBudget.release();
|
||||
}
|
||||
}
|
||||
|
||||
private lastGeneratedPoint?: number;
|
||||
public generatedPoints(value: number) {
|
||||
this.lastGeneratedPoint = value;
|
||||
}
|
||||
|
||||
public onFlipped(team: CharacterTeam): void {
|
||||
const color = settings.palette[settings.colorIndices[team]];
|
||||
|
||||
if (!this.flareLight) {
|
||||
this.flareLight = new CircleLight(vec2.clone(this.center), vec3.clone(color), 0);
|
||||
} else {
|
||||
this.flareLight.color = vec3.clone(color);
|
||||
}
|
||||
|
||||
if (!this.holdsFlareSlot) {
|
||||
if (!FlareBudget.tryAcquire()) {
|
||||
return;
|
||||
}
|
||||
this.holdsFlareSlot = true;
|
||||
}
|
||||
|
||||
this.flareIntensity = 1;
|
||||
}
|
||||
|
||||
private beforeDestroy(): void {
|
||||
this.ownershipProgress.parentElement?.removeChild(this.ownershipProgress);
|
||||
this.generatedPointElements.forEach((p) =>
|
||||
p.element.parentElement?.removeChild(p.element),
|
||||
);
|
||||
this.releaseFlareSlot();
|
||||
}
|
||||
|
||||
private updateProperty({ propertyValue }: UpdatePropertyCommand): void {
|
||||
|
|
@ -80,26 +132,6 @@ export class PlanetView extends PlanetBase {
|
|||
|
||||
const screenPosition = renderer.worldToDisplayCoordinates(this.center);
|
||||
|
||||
this.generatedPointElements.forEach((p) => {
|
||||
if (!p.addedToOverlay) {
|
||||
overlay.appendChild(p.element);
|
||||
}
|
||||
|
||||
p.element.style.transform = `translateX(${
|
||||
screenPosition.x + p.position.x
|
||||
}px) translateY(${screenPosition.y + p.position.y}px)`;
|
||||
|
||||
if (p.timeToLive <= 0) {
|
||||
p.element.parentElement?.removeChild(p.element);
|
||||
} else {
|
||||
p.element.style.opacity = Math.min(1, p.timeToLive).toString();
|
||||
}
|
||||
});
|
||||
|
||||
this.generatedPointElements = this.generatedPointElements.filter(
|
||||
(p) => p.timeToLive > 0,
|
||||
);
|
||||
|
||||
this.ownershipProgress.style.transform = `translateX(${screenPosition.x}px) translateY(${screenPosition.y}px) translateX(-50%) translateY(-50%)`;
|
||||
this.ownershipProgress.style.background = this.getGradient();
|
||||
|
||||
|
|
@ -107,19 +139,23 @@ export class PlanetView extends PlanetBase {
|
|||
const element = document.createElement('div');
|
||||
element.className = 'falling-point ' + (this.ownership < 0.5 ? 'decla' : 'red');
|
||||
element.innerText = '+' + this.lastGeneratedPoint;
|
||||
this.generatedPointElements.push({
|
||||
element,
|
||||
addedToOverlay: false,
|
||||
timeToLive: Random.getRandomInRange(2, 3),
|
||||
position: vec2.create(),
|
||||
velocity: vec2.fromValues(Random.getRandomInRange(-30, 30), 0),
|
||||
});
|
||||
element.style.left = `${screenPosition.x}px`;
|
||||
element.style.top = `${screenPosition.y}px`;
|
||||
overlay.appendChild(element);
|
||||
setTimeout(
|
||||
() => element.parentElement?.removeChild(element),
|
||||
fallingPointLifetimeMs,
|
||||
);
|
||||
|
||||
this.lastGeneratedPoint = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
renderer.addDrawable(this.shape);
|
||||
|
||||
if (this.flareIntensity > 0 && this.flareLight) {
|
||||
renderer.addDrawable(this.flareLight);
|
||||
}
|
||||
}
|
||||
|
||||
private getGradient(): string {
|
||||
|
|
|
|||
|
|
@ -6,39 +6,62 @@ const neutralColor = rgb255(82, 165, 64);
|
|||
const redColor = rgb255(209, 86, 82);
|
||||
const q = 2.5;
|
||||
const declaColorDim = rgb255(64 * q, 105 * q, 165 * q);
|
||||
const redColorDim = rgb255(209 * q, 8 * q, 82 * q);
|
||||
const redColorDim = rgb255(209 * q, 86 * q, 82 * q);
|
||||
const declaPlanetColor = declaColorDim;
|
||||
const redPlanetColor = redColorDim;
|
||||
|
||||
export const settings = {
|
||||
lightCutoffDistance: 600,
|
||||
lightOverlapReduction: 0.85,
|
||||
radiusSteps: 500,
|
||||
spawnDespawnTime: 0.7,
|
||||
worldRadius: 10000,
|
||||
worldRadius: 4000,
|
||||
objectsOnCircleLength: 0.002,
|
||||
updateMessageInterval: 1 / 25,
|
||||
planetEdgeCount: 7,
|
||||
playerKillPoint: 10,
|
||||
takeControlTimeInSeconds: 4,
|
||||
loseControlTimeInSeconds: 24,
|
||||
playerKillPoint: 25,
|
||||
takeControlTimeInSeconds: 2.5,
|
||||
loseControlTimeInSeconds: 16,
|
||||
planetPointGenerationInterval: 1.5,
|
||||
planetPointGenerationValue: 1,
|
||||
planetSizePointMultiplierMax: 4,
|
||||
captureFlipPointReward: 12,
|
||||
maxGravityDistance: 800,
|
||||
minGravityDistance: 1,
|
||||
maxGravityQ: 5000,
|
||||
planetControlThreshold: 0.2,
|
||||
playerMaxHealth: 100,
|
||||
maxGravityStrength: 50000,
|
||||
planetMinReferenceRadius: 150,
|
||||
planetMaxReferenceRadius: 1200,
|
||||
maxAcceleration: 120000,
|
||||
playerMaxStrength: 80,
|
||||
endGameDeltaScaling: 2,
|
||||
playerDiedTimeout: 5,
|
||||
playerStrengthRegenerationPerSeconds: 80,
|
||||
playerOutOfCombatDelaySeconds: 5,
|
||||
playerHealthRegenerationPerSeconds: 8,
|
||||
playerKillHealthReward: 25,
|
||||
spawnInvulnerabilityExtraSeconds: 0.4,
|
||||
spawnSafetyDistance: 2000,
|
||||
touchAimRange: 1000,
|
||||
postureHeadStiffness: 12,
|
||||
postureFeetStiffness: 10,
|
||||
planetDetachmentSeconds: 0.5,
|
||||
planetDetachmentForceThreshold: 100,
|
||||
climbDotThreshold: 0.8,
|
||||
climbGravityScale: 0.35,
|
||||
projectileMaxStrength: 40,
|
||||
projectileSpeed: 2500,
|
||||
projectileMaxBounceCount: 2,
|
||||
projectileTimeout: 3,
|
||||
projectileFadeSpeed: 20,
|
||||
projectileCreationInterval: 0.1,
|
||||
chargeShotFullHoldSeconds: 0.7,
|
||||
chargeShotStrengthMin: 40,
|
||||
chargeShotStrengthMax: 100,
|
||||
chargeShotRadiusMin: 20,
|
||||
chargeShotRadiusMax: 32,
|
||||
chargeShotSpeedMin: 2500,
|
||||
chargeShotSpeedMax: 3400,
|
||||
playerColorIndexOffset: 3,
|
||||
backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)],
|
||||
declaColor,
|
||||
|
|
@ -104,4 +127,14 @@ export const settings = {
|
|||
paletteDim: [declaColorDim, neutralColor, redColorDim],
|
||||
targetPhysicsDeltaTimeInSeconds: 1 / 200,
|
||||
inViewAreaSize: 1920 * 1080 * 4,
|
||||
scoreboardHalfWidthPercent: 50,
|
||||
scoreboardMinFillPercent: 1.5,
|
||||
matchPointScoreRatio: 0.9,
|
||||
lampLerpSeconds: 0.5,
|
||||
lampMinLightness: 0.4,
|
||||
lampMaxLightness: 1,
|
||||
lampFlareIntensity: 1.2,
|
||||
lampFlareDecaySeconds: 0.6,
|
||||
maxConcurrentFlipFlares: 3,
|
||||
announcementVisibleSeconds: 2,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue