Even more improvements

This commit is contained in:
Andras Schmelczer 2026-06-11 21:43:49 +01:00
parent e3c44f775b
commit ec579650d7
5 changed files with 724 additions and 109 deletions

View file

@ -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);

View file

@ -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);
}
@ -107,7 +115,7 @@ body {
overflow: hidden;
& > * {
&>* {
display: inline-block;
position: absolute;
top: 0;
@ -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;
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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,
};