More improvements
This commit is contained in:
parent
3848e460cd
commit
e3c44f775b
11 changed files with 908 additions and 233 deletions
|
|
@ -1,8 +1,14 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { CommandGenerator, PrimaryActionCommand, SecondaryActionCommand } from 'shared';
|
||||
import { CommandGenerator, PrimaryActionCommand, holdDurationToCharge } from 'shared';
|
||||
import { Game } from '../game';
|
||||
import { ChargeIndicator } from '../charge-indicator';
|
||||
import { Pointer } from '../helper/pointer';
|
||||
|
||||
export class MouseListener extends CommandGenerator {
|
||||
// Timestamp (ms) of the primary press, or null when not held. On release the
|
||||
// held duration is mapped to the charge scalar; a quick tap reads as ~0.
|
||||
private primaryDownAt: number | null = null;
|
||||
|
||||
constructor(
|
||||
private target: HTMLElement,
|
||||
private readonly game: Game,
|
||||
|
|
@ -10,22 +16,43 @@ export class MouseListener extends CommandGenerator {
|
|||
super();
|
||||
|
||||
target.addEventListener('mousedown', this.mouseDownListener);
|
||||
target.addEventListener('mouseup', this.mouseUpListener);
|
||||
target.addEventListener('mousemove', this.mouseMoveListener);
|
||||
target.addEventListener('contextmenu', this.contextMenuListener);
|
||||
}
|
||||
|
||||
private mouseDownListener = (event: MouseEvent) => {
|
||||
const position = this.positionFromEvent(event);
|
||||
// Only the screen position is stored; it is reprojected to world space each
|
||||
// frame so the gaze stays correct even while the camera pans under a still
|
||||
// cursor.
|
||||
private mouseMoveListener = (event: MouseEvent) => {
|
||||
Pointer.setDisplayPosition(event.clientX, event.clientY);
|
||||
};
|
||||
|
||||
private mouseDownListener = (event: MouseEvent) => {
|
||||
if (event.button === 0) {
|
||||
this.sendCommandToSubscribers(new PrimaryActionCommand(position));
|
||||
this.primaryDownAt = performance.now();
|
||||
// The ring follows the cursor and only fades in once this press has
|
||||
// clearly become a hold.
|
||||
ChargeIndicator.begin(event.clientX, event.clientY, true);
|
||||
}
|
||||
};
|
||||
|
||||
private mouseUpListener = (event: MouseEvent) => {
|
||||
if (event.button !== 0 || this.primaryDownAt === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChargeIndicator.end();
|
||||
const charge = holdDurationToCharge((performance.now() - this.primaryDownAt) / 1000);
|
||||
this.primaryDownAt = null;
|
||||
this.sendCommandToSubscribers(
|
||||
new PrimaryActionCommand(this.positionFromEvent(event), charge),
|
||||
);
|
||||
};
|
||||
|
||||
// Suppress the browser context menu on the canvas; right-click has no action.
|
||||
private contextMenuListener = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const position = this.positionFromEvent(event);
|
||||
this.sendCommandToSubscribers(new SecondaryActionCommand(position));
|
||||
};
|
||||
|
||||
private positionFromEvent(event: MouseEvent): vec2 {
|
||||
|
|
@ -35,7 +62,10 @@ export class MouseListener extends CommandGenerator {
|
|||
}
|
||||
|
||||
public destroy() {
|
||||
ChargeIndicator.end();
|
||||
this.target.removeEventListener('mousedown', this.mouseDownListener);
|
||||
this.target.removeEventListener('mouseup', this.mouseUpListener);
|
||||
this.target.removeEventListener('mousemove', this.mouseMoveListener);
|
||||
this.target.removeEventListener('contextmenu', this.contextMenuListener);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { CommandGenerator, MoveActionCommand, last, PrimaryActionCommand } from 'shared';
|
||||
import {
|
||||
CommandGenerator,
|
||||
MoveActionCommand,
|
||||
last,
|
||||
PrimaryActionCommand,
|
||||
holdDurationToCharge,
|
||||
settings,
|
||||
} from 'shared';
|
||||
import { Game } from '../game';
|
||||
import { ChargeIndicator } from '../charge-indicator';
|
||||
|
||||
export class TouchListener extends CommandGenerator {
|
||||
private static readonly deadZone = 8;
|
||||
|
|
@ -10,6 +18,12 @@ export class TouchListener extends CommandGenerator {
|
|||
private joystickButton: HTMLElement;
|
||||
private isJoystickActive = false;
|
||||
private touchStartPosition!: vec2;
|
||||
private primaryDownAt: number | null = null;
|
||||
|
||||
private fireButton: HTMLElement;
|
||||
private fireStrengthRing: HTMLElement;
|
||||
|
||||
private fireDownAt: number | null = null;
|
||||
|
||||
constructor(
|
||||
private target: HTMLElement,
|
||||
|
|
@ -23,6 +37,17 @@ export class TouchListener extends CommandGenerator {
|
|||
this.joystickButton = document.createElement('div');
|
||||
this.joystick.appendChild(this.joystickButton);
|
||||
|
||||
this.fireButton = document.createElement('div');
|
||||
this.fireButton.className = 'touch-button fire';
|
||||
this.fireStrengthRing = document.createElement('div');
|
||||
this.fireStrengthRing.className = 'strength-ring';
|
||||
this.fireButton.appendChild(this.fireStrengthRing);
|
||||
|
||||
this.fireButton.addEventListener('touchstart', this.fireButtonDownListener);
|
||||
this.fireButton.addEventListener('touchend', this.fireButtonUpListener);
|
||||
|
||||
this.overlay.appendChild(this.fireButton);
|
||||
|
||||
target.addEventListener('touchstart', this.touchStartListener);
|
||||
target.addEventListener('touchmove', this.touchMoveListener);
|
||||
target.addEventListener('touchend', this.touchEndListener);
|
||||
|
|
@ -43,6 +68,8 @@ export class TouchListener extends CommandGenerator {
|
|||
event.touches[0].clientX,
|
||||
event.touches[0].clientY,
|
||||
);
|
||||
this.primaryDownAt = performance.now();
|
||||
ChargeIndicator.begin(this.touchStartPosition.x, this.touchStartPosition.y);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -60,6 +87,8 @@ export class TouchListener extends CommandGenerator {
|
|||
|
||||
if (!this.isJoystickActive && deltaLength > TouchListener.deadZone) {
|
||||
this.isJoystickActive = true;
|
||||
this.primaryDownAt = null;
|
||||
ChargeIndicator.end();
|
||||
this.overlay.appendChild(this.joystick);
|
||||
this.joystickButton.style.transform = `translateX(-50%) translateY(-50%)`;
|
||||
this.joystick.style.transform = `translateX(${this.touchStartPosition.x}px) translateY(${this.touchStartPosition.y}px) translateX(-50%) translateY(-50%)`;
|
||||
|
|
@ -71,7 +100,8 @@ export class TouchListener extends CommandGenerator {
|
|||
|
||||
vec2.set(delta, delta.x, -delta.y);
|
||||
if (deltaLength > TouchListener.deadZone) {
|
||||
this.sendCommandToSubscribers(new MoveActionCommand(vec2.normalize(delta, delta)));
|
||||
const direction = vec2.normalize(delta, delta);
|
||||
this.sendCommandToSubscribers(new MoveActionCommand(direction));
|
||||
} else {
|
||||
this.sendCommandToSubscribers(new MoveActionCommand(vec2.create()));
|
||||
}
|
||||
|
|
@ -81,12 +111,18 @@ export class TouchListener extends CommandGenerator {
|
|||
event.preventDefault();
|
||||
|
||||
if (!this.isJoystickActive) {
|
||||
ChargeIndicator.end();
|
||||
const charge =
|
||||
this.primaryDownAt === null
|
||||
? 0
|
||||
: holdDurationToCharge((performance.now() - this.primaryDownAt) / 1000);
|
||||
this.primaryDownAt = null;
|
||||
const center = vec2.fromValues(
|
||||
event.changedTouches[0].clientX,
|
||||
event.changedTouches[0].clientY,
|
||||
);
|
||||
this.sendCommandToSubscribers(
|
||||
new PrimaryActionCommand(this.game.displayToWorldCoordinates(center)),
|
||||
new PrimaryActionCommand(this.game.displayToWorldCoordinates(center), charge),
|
||||
);
|
||||
} else if (event.touches.length === 0) {
|
||||
this.isJoystickActive = false;
|
||||
|
|
@ -95,9 +131,62 @@ export class TouchListener extends CommandGenerator {
|
|||
}
|
||||
};
|
||||
|
||||
private swallowTouch = (event: TouchEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
private fireButtonDownListener = (event: TouchEvent) => {
|
||||
this.swallowTouch(event);
|
||||
this.fireDownAt = performance.now();
|
||||
const rect = this.fireButton.getBoundingClientRect();
|
||||
ChargeIndicator.begin(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
||||
};
|
||||
|
||||
private fireButtonUpListener = (event: TouchEvent) => {
|
||||
this.swallowTouch(event);
|
||||
ChargeIndicator.end();
|
||||
if (this.fireDownAt === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const charge = holdDurationToCharge((performance.now() - this.fireDownAt) / 1000);
|
||||
this.fireDownAt = null;
|
||||
|
||||
const character = this.game.gameObjects.player;
|
||||
if (!character) {
|
||||
return;
|
||||
}
|
||||
const aim = vec2.scaleAndAdd(
|
||||
vec2.create(),
|
||||
character.bodyCenter,
|
||||
character.facingDirection,
|
||||
settings.touchAimRange,
|
||||
);
|
||||
this.sendCommandToSubscribers(new PrimaryActionCommand(aim, charge));
|
||||
};
|
||||
|
||||
public update(_deltaTimeInSeconds: number) {
|
||||
if (!this.fireButton.parentElement) {
|
||||
this.overlay.appendChild(this.fireButton);
|
||||
}
|
||||
|
||||
const character = this.game.gameObjects.player;
|
||||
if (character) {
|
||||
this.fireStrengthRing.style.background = `conic-gradient(rgba(255, 255, 255, 0.75) ${character.strengthFraction * 360
|
||||
}deg, transparent 0deg)`;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
ChargeIndicator.end();
|
||||
this.target.removeEventListener('touchstart', this.touchStartListener);
|
||||
this.target.removeEventListener('touchmove', this.touchMoveListener);
|
||||
this.target.removeEventListener('touchend', this.touchEndListener);
|
||||
|
||||
this.fireButton.removeEventListener('touchstart', this.fireButtonDownListener);
|
||||
this.fireButton.removeEventListener('touchend', this.fireButtonUpListener);
|
||||
|
||||
this.fireButton.parentElement?.removeChild(this.fireButton);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
import { settings } from 'shared';
|
||||
import { Pointer } from './helper/pointer';
|
||||
|
||||
// Lightweight, self-managing combat-feedback overlay for the local player:
|
||||
// hitmarkers, a personal killfeed, multi-kill callouts and a points popup. It
|
||||
// owns a single fixed, click-through root under <body> and auto-expires every
|
||||
// element it creates, so callers (CharacterView remote-call handlers) just fire
|
||||
// and forget — no wiring through the game loop.
|
||||
export abstract class FeedbackHud {
|
||||
private static root?: HTMLElement;
|
||||
private static killfeed?: HTMLElement;
|
||||
|
|
@ -24,8 +19,6 @@ export abstract class FeedbackHud {
|
|||
return { root: this.root, killfeed: this.killfeed };
|
||||
}
|
||||
|
||||
// Where transient marks/popups appear: the cursor on desktop, screen centre on
|
||||
// touch (where the local character fires along its facing).
|
||||
private static focusPoint(): { x: number; y: number } {
|
||||
const cursor = Pointer.getDisplayPosition();
|
||||
if (cursor) {
|
||||
|
|
@ -40,7 +33,6 @@ export abstract class FeedbackHud {
|
|||
setTimeout(() => element.parentElement?.removeChild(element), lifetimeMs);
|
||||
}
|
||||
|
||||
// A non-lethal hit landed: a quick crosshair tick at the focus point.
|
||||
public static hitMarker() {
|
||||
const { x, y } = this.focusPoint();
|
||||
const marker = document.createElement('div');
|
||||
|
|
@ -50,7 +42,6 @@ export abstract class FeedbackHud {
|
|||
this.addTransient(marker, 250);
|
||||
}
|
||||
|
||||
// A kill was confirmed: killfeed line, points popup and (for streaks) a callout.
|
||||
public static killConfirmed(victimName?: string, streak = 1) {
|
||||
const { killfeed } = this.ensureRoot();
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { StepCommand } from '../commands/types/step';
|
|||
import { Game } from '../game';
|
||||
import { Camera } from './types/camera';
|
||||
import { CharacterView } from './types/character-view';
|
||||
import { PlanetView } from './types/planet-view';
|
||||
|
||||
export class GameObjectContainer extends CommandReceiver {
|
||||
protected objects: Map<Id, GameObject> = new Map();
|
||||
|
|
@ -57,6 +58,16 @@ export class GameObjectContainer extends CommandReceiver {
|
|||
super();
|
||||
}
|
||||
|
||||
public get planets(): Array<PlanetView> {
|
||||
const planets: Array<PlanetView> = [];
|
||||
this.objects.forEach((o) => {
|
||||
if (o instanceof PlanetView) {
|
||||
planets.push(o);
|
||||
}
|
||||
});
|
||||
return planets;
|
||||
}
|
||||
|
||||
protected defaultCommandExecutor(c: Command) {
|
||||
this.objects.forEach((o) => o.handleCommand(c));
|
||||
this.camera.handleCommand(c);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ export class Scoreboard {
|
|||
this.declaFill.style.width = fraction(declaCount) + '%';
|
||||
this.redFill.style.width = fraction(redCount) + '%';
|
||||
|
||||
const isMatchPoint = (count: number) =>
|
||||
count / limit >= settings.matchPointScoreRatio;
|
||||
this.declaFill.classList.toggle('match-point', isMatchPoint(declaCount));
|
||||
this.redFill.classList.toggle('match-point', isMatchPoint(redCount));
|
||||
|
||||
this.declaScore.innerText = String(Math.round(declaCount));
|
||||
this.redScore.innerText = String(Math.round(redCount));
|
||||
|
||||
|
|
|
|||
219
frontend/src/scripts/shapes/character-shape.ts
Normal file
219
frontend/src/scripts/shapes/character-shape.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import { mat2d, vec2 } from 'gl-matrix';
|
||||
import { Drawable, DrawableDescriptor } from 'sdf-2d';
|
||||
import { Circle } from 'shared';
|
||||
|
||||
// A single character silhouette shared by every team (teams differ only by
|
||||
// colour): a circular head sitting on two thin line-legs that run down to the
|
||||
// two feet.
|
||||
export class CharacterShape extends Drawable {
|
||||
public static descriptor: DrawableDescriptor = {
|
||||
sdf: {
|
||||
shader: `
|
||||
uniform vec2 characterHeadCenters[CHARACTER_COUNT];
|
||||
uniform vec2 characterLeftFeet[CHARACTER_COUNT];
|
||||
uniform vec2 characterRightFeet[CHARACTER_COUNT];
|
||||
uniform vec2 characterGazeTargets[CHARACTER_COUNT];
|
||||
uniform float characterHeadRadii[CHARACTER_COUNT];
|
||||
uniform float characterFootRadii[CHARACTER_COUNT];
|
||||
uniform int characterColors[CHARACTER_COUNT];
|
||||
uniform float characterFlash[CHARACTER_COUNT];
|
||||
|
||||
float circleDistance(vec2 circleCenter, float radius, vec2 target) {
|
||||
return distance(target, circleCenter) - radius;
|
||||
}
|
||||
|
||||
// Distance to the segment a->b; subtract a radius to get a capsule (a
|
||||
// rounded thick line).
|
||||
float segmentDistance(vec2 a, vec2 b, vec2 target) {
|
||||
vec2 pa = target - a;
|
||||
vec2 ba = b - a;
|
||||
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||
return length(pa - ba * h);
|
||||
}
|
||||
|
||||
float characterMinDistance(vec2 target, out vec4 color) {
|
||||
float minDistance = 1000.0;
|
||||
|
||||
for (int i = 0; i < CHARACTER_COUNT; i++) {
|
||||
vec2 head = characterHeadCenters[i];
|
||||
vec2 leftFoot = characterLeftFeet[i];
|
||||
vec2 rightFoot = characterRightFeet[i];
|
||||
float headRadius = characterHeadRadii[i];
|
||||
float footRadius = characterFootRadii[i];
|
||||
|
||||
// The server rotates the whole posture toward travel, so the head
|
||||
// leads: forward is the facing direction, perp its 90deg rotation, so
|
||||
// the face sits on the leading side and reads at any rotation.
|
||||
vec2 footAverage = (leftFoot + rightFoot) * 0.5;
|
||||
vec2 toHead = head - footAverage;
|
||||
float toHeadLength = length(toHead);
|
||||
vec2 forward = toHeadLength > 0.001 ? toHead / toHeadLength : vec2(0.0, 1.0);
|
||||
vec2 perp = vec2(-forward.y, forward.x);
|
||||
|
||||
// A circular head, drawn a bit smaller than the head->feet gap so the
|
||||
// legs below it read as two distinct LINES rather than being swallowed
|
||||
// by the head.
|
||||
float renderRadius = headRadius * 0.75;
|
||||
float headDistance = circleDistance(head, renderRadius, target);
|
||||
|
||||
// Legs as thin line-segments (capsules) from the head down to each
|
||||
// foot, hard-min'd onto the head so the head stays a crisp circle.
|
||||
float legThickness = footRadius * 0.35;
|
||||
float leftLeg = segmentDistance(head, leftFoot, target) - legThickness;
|
||||
float rightLeg = segmentDistance(head, rightFoot, target) - legThickness;
|
||||
|
||||
// Rounded feet at the ends of the lines, kept large enough to stay
|
||||
// visible even when the leg is short and the head nearly reaches them.
|
||||
float footRender = footRadius * 0.7;
|
||||
float leftFootDistance = circleDistance(leftFoot, footRender, target);
|
||||
float rightFootDistance = circleDistance(rightFoot, footRender, target);
|
||||
|
||||
float body = min(
|
||||
headDistance,
|
||||
min(min(leftLeg, rightLeg), min(leftFootDistance, rightFootDistance))
|
||||
);
|
||||
|
||||
// Real eyes painted on the leading face — a bright white sclera with a
|
||||
// dark pupil — rather than carved holes, so the character reads as
|
||||
// alive instead of hollow-socketed.
|
||||
// Big white sclera, small pupil: the eye must read as mostly white so
|
||||
// it looks like an eye, not a dark socket. (An 8-bit albedo caps the
|
||||
// sclera at pure white, so its area — not its colour — is what makes
|
||||
// the eye read brighter against the body.)
|
||||
vec2 eyeBase = head + forward * (renderRadius * 0.26);
|
||||
vec2 leftEyeCenter = eyeBase + perp * (renderRadius * 0.38);
|
||||
vec2 rightEyeCenter = eyeBase - perp * (renderRadius * 0.38);
|
||||
float scleraRadius = renderRadius * 0.26;
|
||||
float pupilRadius = renderRadius * 0.11;
|
||||
|
||||
// The pupil slides toward the gaze target (the cursor for the local
|
||||
// player, otherwise the travel direction), kept well inside the rim so
|
||||
// a generous ring of white always frames it. The normalize is guarded
|
||||
// so a cursor resting exactly on an eye can't produce NaNs.
|
||||
vec2 gazeTarget = characterGazeTargets[i];
|
||||
float pupilReach = (scleraRadius - pupilRadius) * 0.6;
|
||||
vec2 toLeftGaze = gazeTarget - leftEyeCenter;
|
||||
vec2 toRightGaze = gazeTarget - rightEyeCenter;
|
||||
vec2 leftGaze = length(toLeftGaze) > 0.001 ? normalize(toLeftGaze) : forward;
|
||||
vec2 rightGaze = length(toRightGaze) > 0.001 ? normalize(toRightGaze) : forward;
|
||||
|
||||
float sclera = min(
|
||||
circleDistance(leftEyeCenter, scleraRadius, target),
|
||||
circleDistance(rightEyeCenter, scleraRadius, target)
|
||||
);
|
||||
float pupil = min(
|
||||
circleDistance(leftEyeCenter + leftGaze * pupilReach, pupilRadius, target),
|
||||
circleDistance(rightEyeCenter + rightGaze * pupilReach, pupilRadius, target)
|
||||
);
|
||||
|
||||
// Soften each eye edge by at least one screen texel so the colour
|
||||
// boundary antialiases instead of staircasing when the character is
|
||||
// small on screen. The fixed world-space term sets a floor on the
|
||||
// softness when zoomed in; fwidth widens the band to a texel as the
|
||||
// head shrinks. Computed here in uniform control flow — NOT inside the
|
||||
// body branch below — so the screen-space derivatives stay defined.
|
||||
// fwidth needs WebGL2 derivatives (core in GLSL ES 3.00), so the
|
||||
// WebGL1 fallback keeps the plain world-space band.
|
||||
float eyeAaBase = renderRadius * 0.025;
|
||||
#ifdef WEBGL2_IS_AVAILABLE
|
||||
float scleraAa = max(eyeAaBase, fwidth(sclera));
|
||||
float pupilAa = max(eyeAaBase, fwidth(pupil));
|
||||
#else
|
||||
float scleraAa = eyeAaBase;
|
||||
float pupilAa = eyeAaBase;
|
||||
#endif
|
||||
|
||||
if (body < minDistance) {
|
||||
minDistance = body;
|
||||
// Brief white punch when taking a hit.
|
||||
color = mix(
|
||||
readFromPalette(characterColors[i]),
|
||||
vec4(1.0),
|
||||
clamp(characterFlash[i], 0.0, 1.0)
|
||||
);
|
||||
|
||||
// Paint the eyes over the body colour. The sclera albedo is HDR
|
||||
// (>> 1): the shading pass multiplies it by the (often dim, reddish)
|
||||
// scene light before clamping to the screen, so an over-bright white
|
||||
// reads as a clean white eye everywhere instead of dimming into a
|
||||
// dark socket. Needs the float colour buffer in sdf-2d; on 8-bit
|
||||
// fallback it simply clamps back to plain white. The smoothstep
|
||||
// widths (scleraAa / pupilAa, computed above) antialias the colour
|
||||
// boundary in a zoom-aware way.
|
||||
color = mix(color, vec4(10.0, 10.0, 10.0, 1.0), 1.0 - smoothstep(-scleraAa, scleraAa, sclera));
|
||||
color = mix(color, vec4(0.04, 0.04, 0.07, 1.0), 1.0 - smoothstep(-pupilAa, pupilAa, pupil));
|
||||
}
|
||||
}
|
||||
|
||||
return minDistance;
|
||||
}
|
||||
`,
|
||||
distanceFunctionName: 'characterMinDistance',
|
||||
},
|
||||
propertyUniformMapping: {
|
||||
footRadius: 'characterFootRadii',
|
||||
headRadius: 'characterHeadRadii',
|
||||
rightFootCenter: 'characterRightFeet',
|
||||
leftFootCenter: 'characterLeftFeet',
|
||||
headCenter: 'characterHeadCenters',
|
||||
gazeTarget: 'characterGazeTargets',
|
||||
color: 'characterColors',
|
||||
flash: 'characterFlash',
|
||||
},
|
||||
uniformCountMacroName: 'CHARACTER_COUNT',
|
||||
shaderCombinationSteps: [0, 1, 2, 8],
|
||||
empty: new CharacterShape(0),
|
||||
};
|
||||
|
||||
protected head!: Circle;
|
||||
protected leftFoot!: Circle;
|
||||
protected rightFoot!: Circle;
|
||||
|
||||
// 0..1 transient white flash on taking a hit. Set per frame.
|
||||
public hitFlash = 0;
|
||||
// World point the eyes look at; the pupils slide toward it. Set per frame
|
||||
// (the cursor for the local player, the travel direction for everyone else).
|
||||
public gazeTarget = vec2.create();
|
||||
|
||||
public constructor(private readonly color: number) {
|
||||
super();
|
||||
|
||||
const circle = new Circle(vec2.create(), 200);
|
||||
this.setCircles([circle, circle, circle]);
|
||||
}
|
||||
|
||||
public setCircles([head, leftFoot, rightFoot]: [Circle, Circle, Circle]) {
|
||||
this.head = head;
|
||||
this.leftFoot = leftFoot;
|
||||
this.rightFoot = rightFoot;
|
||||
}
|
||||
|
||||
public minDistance(target: vec2): number {
|
||||
return Math.min(
|
||||
this.head.distance(target),
|
||||
this.leftFoot.distance(target),
|
||||
this.rightFoot.distance(target),
|
||||
);
|
||||
}
|
||||
|
||||
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
|
||||
return {
|
||||
headCenter: vec2.transformMat2d(vec2.create(), this.head.center, transform2d),
|
||||
leftFootCenter: vec2.transformMat2d(
|
||||
vec2.create(),
|
||||
this.leftFoot.center,
|
||||
transform2d,
|
||||
),
|
||||
rightFootCenter: vec2.transformMat2d(
|
||||
vec2.create(),
|
||||
this.rightFoot.center,
|
||||
transform2d,
|
||||
),
|
||||
gazeTarget: vec2.transformMat2d(vec2.create(), this.gazeTarget, transform2d),
|
||||
headRadius: this.head.radius * transform1d,
|
||||
footRadius: this.leftFoot.radius * transform1d,
|
||||
color: this.color,
|
||||
flash: this.hitFlash,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -122,8 +122,6 @@ export class PlanetShape extends PolygonFactory(settings.planetEdgeCount, 0) {
|
|||
};
|
||||
|
||||
public randomOffset = 0;
|
||||
// Radians the surface noise is rotated by; advanced over time by PlanetView so
|
||||
// the planet's textured surface slowly spins.
|
||||
public rotation = 0;
|
||||
|
||||
constructor(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue