mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 14:09:09 +00:00
2c4d2334dd
## Description: Makes so that when clicking on the attack warning message in chat, the camera focuses on the "average position" of the attack, instead of just the player. The average position is calculated by taking the average position of all attacking border cells. It makes the calculation for every AttackUpdate, which adds some calculations every tick, but it shouldn't affect performance that much, as it's just a sum of coordinates. If you have a better way of getting the averagePosition information (calculating it only when necessary instead of every tick), it would be great. closes #703 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: leo21_ --------- Co-authored-by: Scott Anderson <scottanderson@users.noreply.github.com> Co-authored-by: evanpelle <evanpelle@gmail.com>
261 lines
8.0 KiB
TypeScript
261 lines
8.0 KiB
TypeScript
import { EventBus } from "../../core/EventBus";
|
|
import { Cell } from "../../core/game/Game";
|
|
import { GameView } from "../../core/game/GameView";
|
|
import { CenterCameraEvent, DragEvent, ZoomEvent } from "../InputHandler";
|
|
import {
|
|
GoToPlayerEvent,
|
|
GoToPositionEvent,
|
|
GoToUnitEvent,
|
|
} from "./layers/Leaderboard";
|
|
|
|
export const GOTO_INTERVAL_MS = 16;
|
|
export const CAMERA_MAX_SPEED = 15;
|
|
export const CAMERA_SMOOTHING = 0.03;
|
|
|
|
export class TransformHandler {
|
|
public scale: number = 1.8;
|
|
private offsetX: number = -350;
|
|
private offsetY: number = -200;
|
|
private lastGoToCallTime: number | null = null;
|
|
|
|
private target: Cell | null;
|
|
private intervalID: NodeJS.Timeout | null = null;
|
|
private changed = false;
|
|
|
|
constructor(
|
|
private game: GameView,
|
|
private eventBus: EventBus,
|
|
private canvas: HTMLCanvasElement,
|
|
) {
|
|
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e));
|
|
this.eventBus.on(DragEvent, (e) => this.onMove(e));
|
|
this.eventBus.on(GoToPlayerEvent, (e) => this.onGoToPlayer(e));
|
|
this.eventBus.on(GoToPositionEvent, (e) => this.onGoToPosition(e));
|
|
this.eventBus.on(GoToUnitEvent, (e) => this.onGoToUnit(e));
|
|
this.eventBus.on(CenterCameraEvent, () => this.centerCamera());
|
|
}
|
|
|
|
boundingRect(): DOMRect {
|
|
return this.canvas.getBoundingClientRect();
|
|
}
|
|
|
|
width(): number {
|
|
return this.boundingRect().width;
|
|
}
|
|
hasChanged(): boolean {
|
|
return this.changed;
|
|
}
|
|
|
|
handleTransform(context: CanvasRenderingContext2D) {
|
|
// Disable image smoothing for pixelated effect
|
|
context.imageSmoothingEnabled = false;
|
|
|
|
// Apply zoom and pan
|
|
context.setTransform(
|
|
this.scale,
|
|
0,
|
|
0,
|
|
this.scale,
|
|
this.game.width() / 2 - this.offsetX * this.scale,
|
|
this.game.height() / 2 - this.offsetY * this.scale,
|
|
);
|
|
this.changed = false;
|
|
}
|
|
|
|
worldToScreenCoordinates(cell: Cell): { x: number; y: number } {
|
|
// Step 1: Convert from Cell coordinates to game coordinates
|
|
// (reverse of Math.floor operation - we'll use the exact values)
|
|
const gameX = cell.x;
|
|
const gameY = cell.y;
|
|
|
|
// Step 2: Reverse the game center offset calculation
|
|
// Original: gameX = centerX + this.game.width() / 2
|
|
// Therefore: centerX = gameX - this.game.width() / 2
|
|
const centerX = gameX - this.game.width() / 2;
|
|
const centerY = gameY - this.game.height() / 2;
|
|
|
|
// Step 3: Reverse the world point calculation
|
|
// Original: centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX
|
|
// Therefore: canvasX = (centerX - this.offsetX) * this.scale + this.game.width() / 2
|
|
const canvasX =
|
|
(centerX - this.offsetX) * this.scale + this.game.width() / 2;
|
|
const canvasY =
|
|
(centerY - this.offsetY) * this.scale + this.game.height() / 2;
|
|
|
|
// Step 4: Convert canvas coordinates back to screen coordinates
|
|
const canvasRect = this.boundingRect();
|
|
const screenX = canvasX + canvasRect.left;
|
|
const screenY = canvasY + canvasRect.top;
|
|
return { x: screenX, y: screenY };
|
|
}
|
|
|
|
screenToWorldCoordinates(screenX: number, screenY: number): Cell {
|
|
const canvasRect = this.boundingRect();
|
|
const canvasX = screenX - canvasRect.left;
|
|
const canvasY = screenY - canvasRect.top;
|
|
|
|
// Calculate the world point we want to zoom towards
|
|
const centerX =
|
|
(canvasX - this.game.width() / 2) / this.scale + this.offsetX;
|
|
const centerY =
|
|
(canvasY - this.game.height() / 2) / this.scale + this.offsetY;
|
|
|
|
const gameX = centerX + this.game.width() / 2;
|
|
const gameY = centerY + this.game.height() / 2;
|
|
|
|
return new Cell(Math.floor(gameX), Math.floor(gameY));
|
|
}
|
|
|
|
screenBoundingRect(): [Cell, Cell] {
|
|
const canvasRect = this.boundingRect();
|
|
const canvasWidth = canvasRect.width;
|
|
const canvasHeight = canvasRect.height;
|
|
|
|
const LeftX = -this.game.width() / 2 / this.scale + this.offsetX;
|
|
const TopY = -this.game.height() / 2 / this.scale + this.offsetY;
|
|
|
|
const gameLeftX = LeftX + this.game.width() / 2;
|
|
const gameTopY = TopY + this.game.height() / 2;
|
|
|
|
const rightX =
|
|
(canvasWidth - this.game.width() / 2) / this.scale + this.offsetX;
|
|
const bottomY =
|
|
(canvasHeight - this.game.height() / 2) / this.scale + this.offsetY;
|
|
|
|
const gameRightX = rightX + this.game.width() / 2;
|
|
const gameBottomY = bottomY + this.game.height() / 2;
|
|
|
|
return [
|
|
new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)),
|
|
new Cell(Math.floor(gameRightX), Math.floor(gameBottomY)),
|
|
];
|
|
}
|
|
|
|
isOnScreen(cell: Cell): boolean {
|
|
const [topLeft, bottomRight] = this.screenBoundingRect();
|
|
return (
|
|
cell.x > topLeft.x &&
|
|
cell.x < bottomRight.x &&
|
|
cell.y > topLeft.y &&
|
|
cell.y < bottomRight.y
|
|
);
|
|
}
|
|
|
|
screenCenter(): { screenX: number; screenY: number } {
|
|
const [upperLeft, bottomRight] = this.screenBoundingRect();
|
|
return {
|
|
screenX: upperLeft.x + Math.floor((bottomRight.x - upperLeft.x) / 2),
|
|
screenY: upperLeft.y + Math.floor((bottomRight.y - upperLeft.y) / 2),
|
|
};
|
|
}
|
|
|
|
onGoToPlayer(event: GoToPlayerEvent) {
|
|
this.game.setFocusedPlayer(event.player);
|
|
this.clearTarget();
|
|
this.target = new Cell(
|
|
event.player.nameLocation().x,
|
|
event.player.nameLocation().y,
|
|
);
|
|
this.intervalID = setInterval(() => this.goTo(), GOTO_INTERVAL_MS);
|
|
}
|
|
|
|
onGoToPosition(event: GoToPositionEvent) {
|
|
this.clearTarget();
|
|
this.target = new Cell(event.x, event.y);
|
|
this.intervalID = setInterval(() => this.goTo(), GOTO_INTERVAL_MS);
|
|
}
|
|
|
|
onGoToUnit(event: GoToUnitEvent) {
|
|
this.clearTarget();
|
|
this.target = new Cell(
|
|
this.game.x(event.unit.lastTile()),
|
|
this.game.y(event.unit.lastTile()),
|
|
);
|
|
this.intervalID = setInterval(() => this.goTo(), GOTO_INTERVAL_MS);
|
|
}
|
|
|
|
centerCamera() {
|
|
this.clearTarget();
|
|
const player = this.game.myPlayer();
|
|
if (!player || !player.nameLocation()) return;
|
|
this.target = new Cell(player.nameLocation().x, player.nameLocation().y);
|
|
this.intervalID = setInterval(() => this.goTo(), GOTO_INTERVAL_MS);
|
|
}
|
|
|
|
private goTo() {
|
|
const { screenX, screenY } = this.screenCenter();
|
|
|
|
if (this.target === null) throw new Error("null target");
|
|
|
|
if (
|
|
Math.abs(this.target.x - screenX) + Math.abs(this.target.y - screenY) <
|
|
2
|
|
) {
|
|
this.clearTarget();
|
|
return;
|
|
}
|
|
|
|
let dt: number;
|
|
const now = window.performance.now();
|
|
if (this.lastGoToCallTime === null) {
|
|
dt = GOTO_INTERVAL_MS;
|
|
} else {
|
|
dt = now - this.lastGoToCallTime;
|
|
}
|
|
this.lastGoToCallTime = now;
|
|
|
|
const r = 1 - Math.pow(CAMERA_SMOOTHING, dt / 1000);
|
|
|
|
this.offsetX += Math.max(
|
|
Math.min((this.target.x - screenX) * r, CAMERA_MAX_SPEED),
|
|
-CAMERA_MAX_SPEED,
|
|
);
|
|
this.offsetY += Math.max(
|
|
Math.min((this.target.y - screenY) * r, CAMERA_MAX_SPEED),
|
|
-CAMERA_MAX_SPEED,
|
|
);
|
|
|
|
this.changed = true;
|
|
}
|
|
|
|
onZoom(event: ZoomEvent) {
|
|
this.clearTarget();
|
|
const oldScale = this.scale;
|
|
const zoomFactor = 1 + event.delta / 600;
|
|
this.scale /= zoomFactor;
|
|
|
|
// Clamp the scale to prevent extreme zooming
|
|
this.scale = Math.max(0.2, Math.min(20, this.scale));
|
|
|
|
const canvasRect = this.boundingRect();
|
|
const canvasX = event.x - canvasRect.left;
|
|
const canvasY = event.y - canvasRect.top;
|
|
|
|
// Calculate the world point we want to zoom towards
|
|
const zoomPointX =
|
|
(canvasX - this.game.width() / 2) / oldScale + this.offsetX;
|
|
const zoomPointY =
|
|
(canvasY - this.game.height() / 2) / oldScale + this.offsetY;
|
|
|
|
// Adjust the offset
|
|
this.offsetX = zoomPointX - (canvasX - this.game.width() / 2) / this.scale;
|
|
this.offsetY = zoomPointY - (canvasY - this.game.height() / 2) / this.scale;
|
|
this.changed = true;
|
|
}
|
|
|
|
onMove(event: DragEvent) {
|
|
this.clearTarget();
|
|
this.offsetX -= event.deltaX / this.scale;
|
|
this.offsetY -= event.deltaY / this.scale;
|
|
this.changed = true;
|
|
}
|
|
|
|
private clearTarget() {
|
|
if (this.intervalID !== null) {
|
|
clearInterval(this.intervalID);
|
|
this.intervalID = null;
|
|
}
|
|
this.target = null;
|
|
}
|
|
}
|