Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Web/Map/Scripts/livemap/Attack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export class Attacks extends THREE.Points {
public update(): void {
}

/**
* Releases the geometry and material of the attack particles.
*/
public dispose(): void {
this.geometry.dispose();
this.material.dispose();
}

public addAttack(attacker: GameObject, target: GameObject): void {
if (this.freeAttackIndexes.peek() === null) {
return;
Expand Down
104 changes: 75 additions & 29 deletions src/Web/Map/Scripts/livemap/Attackable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ export class Attackable<TData extends ObjectData> extends THREE.Mesh implements
public material: THREE.Material;
public readonly nameLabel: NameLabel;

private moveTween: TWEEN.Tween;
/*
* All tweens of the current movement - the tween of the first step and the
* step tweens which are chained to it. They are all kept, because stopping
* the first tween doesn't stop the chained ones anymore as soon as it finished.
*/
private moveTweens: TWEEN.Tween[] = [];
private rotateTween: TWEEN.Tween | null = null;
private scaleTween: TWEEN.Tween | null = null;
private fadeTween: TWEEN.Tween | null = null;

constructor(data: TData, geometry: THREE.Geometry, material: THREE.Material) {
super(geometry, material);
this.data = data;
this.moveTween = null;
this.nameLabel = new NameLabel();
this.nameLabel.position.z = NAME_LABEL_Z_POSITION;
this.add(this.nameLabel);
Expand All @@ -31,14 +38,26 @@ export class Attackable<TData extends ObjectData> extends THREE.Mesh implements
this.nameLabel.hide();
}

/**
* Stops all running animations and releases the resources of this object.
* The geometry is not disposed, because it's shared between all objects of the same type.
*/
public dispose(): void {
this.stopTweens();
this.remove(this.nameLabel);
this.nameLabel.dispose();
this.material.dispose();
}

public gotKilled(): void {
// we fade the color out
const fadeOutDurationMs = 1000;
const startingOpacity = 1;
const fadedOutOpacity = 0.1;


this.fadeTween?.stop();
const state = { opacity: startingOpacity };
const tween = new TWEEN.Tween(state)
this.fadeTween = new TWEEN.Tween(state)
.to({ opacity: fadedOutOpacity }, fadeOutDurationMs)
.onUpdate(() => this.material.opacity = state.opacity)
.easing(TWEEN.Easing.Circular.Out)
Expand All @@ -49,8 +68,12 @@ export class Attackable<TData extends ObjectData> extends THREE.Mesh implements
const scaleUpDurationMs = 500;
this.data = newData;
this.material.opacity = 1.0;

this.fadeTween?.stop();
this.fadeTween = null;
this.scaleTween?.stop();
const state = { scale: 0 };
const tween = new TWEEN.Tween(state)
this.scaleTween = new TWEEN.Tween(state)
.to({ scale: 1 }, scaleUpDurationMs)
.onUpdate(() => this.scale.setScalar(state.scale))
.easing(TWEEN.Easing.Back.Out)
Expand All @@ -61,53 +84,76 @@ export class Attackable<TData extends ObjectData> extends THREE.Mesh implements

public moveTo(newX: number, newY: number, moveType: any, walkDelay: number, steps: Step[]): void {
const state = { x: this.data.x, y: this.data.y };
this.data = this.data = Object.assign({}, this.data, { x: newX, y: newY });
this.data = Object.assign({}, this.data, { x: newX, y: newY });

if (this.moveTween !== null) {
this.moveTween.stop();
}

this.moveTween = new TWEEN.Tween(state)
.onUpdate(() => this.setObjectPositionOnMap(state.x, state.y));
this.stopMoveTweens();

if (moveType === "Instant" || moveType === 1) {
const isWalking = moveType !== "Instant" && moveType !== 1
&& steps !== undefined && steps !== null && steps.length > 0;
if (!isWalking) {
const moveDurationMs = 300;
this.moveTween = this.moveTween.easing(TWEEN.Easing.Elastic.Out)
.to({ x: newX, y: newY }, moveDurationMs);
} else {
for (const i in steps) {
if (steps.hasOwnProperty(i)) {
const step = steps[i];
const stepTween = new TWEEN.Tween(state)
.to({ x: step.x, y: step.y }, walkDelay)
.onStart(() => this.rotateTo(step.direction))
.onUpdate(() => this.setObjectPositionOnMap(state.x, state.y));
this.moveTween.chain(stepTween);
}
}
const moveTween = new TWEEN.Tween(state)
.to({ x: newX, y: newY }, moveDurationMs)
.onUpdate(() => this.setObjectPositionOnMap(state.x, state.y))
.easing(TWEEN.Easing.Elastic.Out);
this.moveTweens.push(moveTween);
moveTween.start();
return;
}

// Each step tween is chained to its predecessor, so that the steps are walked one after another.
let previousTween: TWEEN.Tween | null = null;
for (const step of steps) {
const stepTween = new TWEEN.Tween(state)
.to({ x: step.x, y: step.y }, walkDelay)
.onStart(() => this.rotateTo(step.direction))
.onUpdate(() => this.setObjectPositionOnMap(state.x, state.y));
previousTween?.chain(stepTween);
previousTween = stepTween;
this.moveTweens.push(stepTween);
}

this.moveTween.start();
this.moveTweens[0].start();
}

public rotateTo(rotation: Direction): void {
if (this.data !== undefined) {
this.data = Object.assign({}, this.data, rotation);
this.data = Object.assign({}, this.data, { direction: rotation });
}

const degreesOfOneTurn = 360;
const numberOfDirectionValues = 8;
const targetAngle = THREE.Math.degToRad((rotation * degreesOfOneTurn) / numberOfDirectionValues);
const rotateDurationMs = 200;

this.rotateTween?.stop();
const state = { z: this.rotation.z };
new TWEEN.Tween(state)
this.rotateTween = new TWEEN.Tween(state)
.to({ z: targetAngle }, rotateDurationMs)
.onUpdate(() => this.rotation.z = state.z)
.easing(TWEEN.Easing.Quadratic.Out)
.start();
}

private stopTweens(): void {
this.stopMoveTweens();

this.rotateTween?.stop();
this.rotateTween = null;
this.scaleTween?.stop();
this.scaleTween = null;
this.fadeTween?.stop();
this.fadeTween = null;
}

private stopMoveTweens(): void {
for (const moveTween of this.moveTweens) {
moveTween.stop();
}

this.moveTweens = [];
}

private setRotation(value: Direction): void {
const degreesOfOneTurn = 360;
const numberOfDirectionValues = 8;
Expand Down
33 changes: 33 additions & 0 deletions src/Web/Map/Scripts/livemap/Debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Debug logging for the live map.
*
* The per-object messages are disabled by default: the browser console keeps
* live references to everything which is logged to it, so a message for every
* added, updated and removed object would pin each object which ever appeared
* on the map for the lifetime of the console buffer.
*
* To enable them, set 'liveMapDebugLogging' to true on the window object,
* e.g. by entering 'liveMapDebugLogging = true' in the browser console.
*/

interface DebugWindow extends Window {
liveMapDebugLogging?: boolean;
}

/**
* Gets a value indicating whether the verbose debug logging is enabled.
*/
export function isDebugLoggingEnabled(): boolean {
return (window as DebugWindow).liveMapDebugLogging === true;
}

/**
* Writes a debug message to the console, if the debug logging is enabled.
* @param message - The message.
* @param parameters - The additional parameters which are logged with the message.
*/
export function logDebug(message: string, ...parameters: any[]): void {
if (isDebugLoggingEnabled()) {
console.debug(message, ...parameters);
}
}
6 changes: 6 additions & 0 deletions src/Web/Map/Scripts/livemap/GameObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ export interface GameObject extends THREE.Object3D {
moveTo(newX: number, newY: number, moveType: any, walkDelay: number, steps: Step[]): void;
rotateTo(rotation: Direction): void;
gotKilled(): void;

/**
* Stops all running animations and releases the resources (materials, textures)
* which are exclusively used by this object.
*/
dispose(): void;
}
27 changes: 22 additions & 5 deletions src/Web/Map/Scripts/livemap/MapApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class MapApp {
private lastHighlightedId: number | null = null;
private isDisposing: boolean = false;
private isDisposed: boolean = false;
private animationFrameId: number | null = null;
private resizeEventListener: () => void;
private readonly onPickObjectHandler: (data: ObjectData) => void;

Expand Down Expand Up @@ -62,7 +63,6 @@ export class MapApp {
}
},
(data) => this.onObjectHovered(data));
this.container.appendChild(this.renderer.domElement);

this.animate(); //starts the rendering loop
}
Expand Down Expand Up @@ -102,15 +102,31 @@ export class MapApp {

this.isDisposing = true;
window.removeEventListener("resize", this.resizeEventListener);
const webGlRenderer = this.renderer as THREE.WebGLRenderer;
if (webGlRenderer != null) {
webGlRenderer.dispose();
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}

this.picker.dispose();

this.scene.remove(this.world);
this.world.dispose();
this.world = null;

const canvas = this.renderer.domElement;
const webGlRenderer = this.renderer as THREE.WebGLRenderer;
if (webGlRenderer != null) {
webGlRenderer.dispose();

// Releases the WebGL context. Browsers only allow a limited number of them,
// so we don't want to keep it until it's garbage collected.
webGlRenderer.forceContextLoss();
}

if (canvas.parentNode !== null) {
canvas.parentNode.removeChild(canvas);
}

this.renderer = null;
this.isDisposing = false;
this.isDisposed = true;
Expand All @@ -122,7 +138,8 @@ export class MapApp {
return;
}

requestAnimationFrame(() => this.animate(time)); // request the next frame to be rendered
// request the next frame to be rendered, with the timestamp of that frame
this.animationFrameId = requestAnimationFrame((frameTime) => this.animate(frameTime));
this.stats?.update(); // updates the stats (fps and frametimes)
TWEEN.update(time); // updates all existing Tweens
this.world.update(); // update world and it's objects
Expand Down
Loading
Loading