Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[client] Add Minimap Component to Display Worldmap Overview #1718

Merged
merged 34 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9dadf29
feat: minimap dev start
r0man1337 Sep 24, 2024
c783c34
feat: minimap draw structures
r0man1337 Sep 24, 2024
a012a45
refactor
r0man1337 Sep 24, 2024
0350a58
camera frustrum
r0man1337 Sep 24, 2024
e9f58ed
feat: minimap click support
r0man1337 Sep 24, 2024
33f6ad5
feat: camera dragging
r0man1337 Sep 24, 2024
0dfc995
extract draw structures to function
r0man1337 Sep 26, 2024
7608301
draw biomes
r0man1337 Sep 26, 2024
556edad
draw armies
r0man1337 Sep 26, 2024
9f3801b
cache colors
r0man1337 Sep 26, 2024
1e33df2
throttle map update function
r0man1337 Sep 26, 2024
45494dc
minimap scaled coords optimization
r0man1337 Sep 26, 2024
0ea6200
minimap config
r0man1337 Sep 26, 2024
9cc9e74
move map range when click on border
r0man1337 Sep 26, 2024
cec73e4
minimap border width
r0man1337 Sep 26, 2024
fbb8d88
minimap code refactor
r0man1337 Sep 26, 2024
133aab5
center minimap on worldmap open
r0man1337 Sep 26, 2024
0982d1f
minimap size
r0man1337 Sep 26, 2024
f178350
hide minimap on scene switch
r0man1337 Sep 26, 2024
25ca968
minimap styling and propagation
r0man1337 Sep 26, 2024
d528691
fix: prettier errors
r0man1337 Sep 26, 2024
7c26d0d
little refactor
r0man1337 Sep 30, 2024
36286f1
feat: minimap zoom in and out
r0man1337 Sep 30, 2024
b67081d
feat: minimap coords improvements
r0man1337 Sep 30, 2024
6cbdeba
feat: map optimizations
r0man1337 Sep 30, 2024
db6eab5
fix: minimap bg
r0man1337 Sep 30, 2024
3666c6c
colors update
r0man1337 Sep 30, 2024
e3d6f1c
minimap legend
r0man1337 Sep 30, 2024
712a688
minimap colors, legend, z index
r0man1337 Sep 30, 2024
1cc61fb
fix: move canvas to react code
r0man1337 Oct 4, 2024
76f8c34
fix: lint
r0man1337 Oct 4, 2024
6b6ac4a
merge
ponderingdemocritus Oct 7, 2024
8d40181
merge and fix position
ponderingdemocritus Oct 7, 2024
f459088
prettier
ponderingdemocritus Oct 7, 2024
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: 7 additions & 1 deletion client/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand Down Expand Up @@ -81,6 +81,12 @@
</head>
<body>
<div id="root"></div>
<canvas
id="minimap"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While adding the canvas element directly in the HTML is fine, consider creating it dynamically in JavaScript for more flexibility, especially if you plan to make the minimap size adjustable in the future.

width="200"
height="112"
style="position: absolute; top: 10px; right: 10px; border: 1px solid rgba(0, 0, 0, 0.3);background-color: rgba(0, 0, 0, 0.3); border-radius: 5px; z-index: 1000; display: none;"
></canvas>
<script type="module" src="/src/main.tsx"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have this in the Canvas and not in the react?

</body>
</html>
2 changes: 1 addition & 1 deletion client/src/three/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ export default class GameRenderer {
this.hudScene = new HUDScene(this.sceneManager, this.controls);

this.renderModels();

// Init animation
this.animate();
}
Expand Down Expand Up @@ -321,6 +320,7 @@ export default class GameRenderer {
this.renderer.render(this.hudScene.getScene(), this.hudScene.getCamera());
this.labelRenderer.render(this.hudScene.getScene(), this.hudScene.getCamera());

// Update the minimap
requestAnimationFrame(() => {
this.animate();
});
Expand Down
6 changes: 5 additions & 1 deletion client/src/three/SceneManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { HexagonScene } from "./scenes/HexagonScene";
export class SceneManager {
private currentScene: SceneName | undefined = undefined;
private scenes = new Map<SceneName, HexagonScene>();
constructor(private transitionManager: TransitionManager) {}
constructor(private transitionManager: TransitionManager) { }

getCurrentScene() {
return this.currentScene;
Expand All @@ -26,6 +26,10 @@ export class SceneManager {
switchScene(sceneName: SceneName) {
const scene = this.scenes.get(sceneName);
if (scene) {
const previousScene = this.scenes.get(this.currentScene!);
if (previousScene) {
previousScene.onSwitchOff();
}
this.transitionManager.fadeOut(() => {
this._updateCurrentScene(sceneName);
if (scene.setup) {
Expand Down
4 changes: 4 additions & 0 deletions client/src/three/components/ArmyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,10 @@ export class ArmyManager {
this.cachedChunks.clear();
}

public getArmies() {
return Array.from(this.armies.values());
}

update(deltaTime: number) {
let needsBoundingUpdate = false;
const movementSpeed = 1.25; // Constant movement speed
Expand Down
278 changes: 278 additions & 0 deletions client/src/three/components/Minimap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { FELT_CENTER } from "@/ui/config";
import { getHexForWorldPosition } from "@/ui/utils/utils";
import { throttle } from "lodash";
import * as THREE from "three";
import WorldmapScene from "../scenes/Worldmap";
import { ArmyManager } from "./ArmyManager";
import { Biome, BIOME_COLORS } from "./Biome";
import { StructureManager } from "./StructureManager";

const MINIMAP_CONFIG = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving this configuration object to a separate file for better maintainability and easier adjustments.

MAP_COLS_WIDTH: 200,
MAP_ROWS_HEIGHT: 100,
COLORS: {
ARMY: "#0000FF",
STRUCTURE: "#FF0000",
CAMERA: "#FFFFFF",
},
SIZES: {
STRUCTURE: 3,
ARMY: 3,
CAMERA: {
TOP_SIDE_WIDTH_FACTOR: 105,
BOTTOM_SIDE_WIDTH_FACTOR: 170,
HEIGHT_FACTOR: 13,
},
},
BORDER_WIDTH_PERCENT: 0.10,
};

class Minimap {
private worldmapScene: WorldmapScene;
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D;
private camera: THREE.PerspectiveCamera;
private exploredTiles: Map<number, Set<number>>;
private structureManager: StructureManager;
private armyManager: ArmyManager;
private biome: Biome;
private displayRange: any = {
minCol: 150,
maxCol: 350,
minRow: 100,
maxRow: 200,
};
private scaleX: number;
private scaleY: number;
private isDragging: boolean = false;
private biomeCache: Map<string, string>;
private scaledCoords: Map<string, { scaledCol: number, scaledRow: number }>;
private BORDER_WIDTH_PERCENT = MINIMAP_CONFIG.BORDER_WIDTH_PERCENT;

constructor(
worldmapScene: WorldmapScene,
exploredTiles: Map<number, Set<number>>,
camera: THREE.PerspectiveCamera,
structureManager: StructureManager,
armyManager: ArmyManager,
biome: Biome,
) {
this.worldmapScene = worldmapScene;
this.canvas = document.getElementById("minimap") as HTMLCanvasElement;
this.context = this.canvas.getContext("2d")!;
this.structureManager = structureManager;
this.exploredTiles = exploredTiles;
this.armyManager = armyManager;
this.biome = biome;
this.camera = camera;
this.scaleX = this.canvas.width / (this.displayRange.maxCol - this.displayRange.minCol);
this.scaleY = this.canvas.height / (this.displayRange.maxRow - this.displayRange.minRow);
this.biomeCache = new Map();
this.scaledCoords = new Map();
this.computeScaledCoords();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The throttle value of 1000/30 (about 33ms) is a good choice for 30 FPS updates. However, you might want to make this configurable or tie it to the game's overall frame rate if that varies.

this.draw = throttle(this.draw, 1000 / 30);

this.canvas.addEventListener("click", this.handleClick);
this.canvas.addEventListener("mousedown", this.handleMouseDown);
this.canvas.addEventListener("mousemove", this.handleMouseMove);
this.canvas.addEventListener("mouseup", this.handleMouseUp);
}

private computeScaledCoords() {
this.scaledCoords.clear();
for (let col = this.displayRange.minCol; col <= this.displayRange.maxCol; col++) {
for (let row = this.displayRange.minRow; row <= this.displayRange.maxRow; row++) {
const scaledCol = (col - this.displayRange.minCol) * this.scaleX;
const scaledRow = (row - this.displayRange.minRow) * this.scaleY;
this.scaledCoords.set(`${col},${row}`, { scaledCol, scaledRow });
}
}
}

private getMousePosition(event: MouseEvent) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const col = Math.floor(x / this.scaleX) + this.displayRange.minCol;
const row = Math.floor(y / this.scaleY) + this.displayRange.minRow;
return { col, row, x, y };
}

draw() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawExploredTiles();
this.drawStructures();
this.drawArmies();
this.drawCamera();
}

private drawExploredTiles() {
this.exploredTiles.forEach((rows, col) => {
rows.forEach((row) => {
const cacheKey = `${col},${row}`;
let biomeColor;

if (this.biomeCache.has(cacheKey)) {
biomeColor = this.biomeCache.get(cacheKey)!;
} else {
const biome = this.biome.getBiome(col + FELT_CENTER, row + FELT_CENTER);
biomeColor = BIOME_COLORS[biome].getStyle();
this.biomeCache.set(cacheKey, biomeColor);
}
if (this.scaledCoords.has(cacheKey)) {
const { scaledCol, scaledRow } = this.scaledCoords.get(cacheKey)!;
this.context.fillStyle = biomeColor;
this.context.fillRect(scaledCol, scaledRow, this.scaleX, this.scaleY);
}
});
});
}

private drawStructures() {
const allStructures = this.structureManager.structures.getStructures();

for (const [structureType, structures] of allStructures) {
structures.forEach((structure) => {
const { col, row } = structure.hexCoords;
const cacheKey = `${col},${row}`;
if (this.scaledCoords.has(cacheKey)) {
const { scaledCol, scaledRow } = this.scaledCoords.get(cacheKey)!;
this.context.fillStyle = MINIMAP_CONFIG.COLORS.STRUCTURE;
this.context.fillRect(scaledCol, scaledRow, MINIMAP_CONFIG.SIZES.STRUCTURE, MINIMAP_CONFIG.SIZES.STRUCTURE);
}
});
}
}

private drawArmies() {
const allArmies = this.armyManager.getArmies();

allArmies.forEach((army) => {
const { x: col, y: row } = army.hexCoords.getNormalized();
const cacheKey = `${col},${row}`;
if (this.scaledCoords.has(cacheKey)) {
const { scaledCol, scaledRow } = this.scaledCoords.get(cacheKey)!;
this.context.fillStyle = MINIMAP_CONFIG.COLORS.ARMY;
this.context.fillRect(scaledCol, scaledRow, MINIMAP_CONFIG.SIZES.ARMY, MINIMAP_CONFIG.SIZES.ARMY);
}
});
}

drawCamera() {
const cameraPosition = this.camera.position;
const { col, row } = getHexForWorldPosition(cameraPosition);
const cacheKey = `${col},${row}`;
if (this.scaledCoords.has(cacheKey)) {
const { scaledCol, scaledRow } = this.scaledCoords.get(cacheKey)!;

this.context.strokeStyle = MINIMAP_CONFIG.COLORS.CAMERA;
this.context.beginPath();
const topSideWidth = (window.innerWidth / MINIMAP_CONFIG.SIZES.CAMERA.TOP_SIDE_WIDTH_FACTOR) * this.scaleX;
const bottomSideWidth = (window.innerWidth / MINIMAP_CONFIG.SIZES.CAMERA.BOTTOM_SIDE_WIDTH_FACTOR) * this.scaleX;
const height = MINIMAP_CONFIG.SIZES.CAMERA.HEIGHT_FACTOR * this.scaleY;
this.context.moveTo(scaledCol - topSideWidth / 2, scaledRow - height);
this.context.lineTo(scaledCol + topSideWidth / 2, scaledRow - height);
this.context.lineTo(scaledCol + bottomSideWidth / 2, scaledRow);
this.context.lineTo(scaledCol - bottomSideWidth / 2, scaledRow);
this.context.lineTo(scaledCol - topSideWidth / 2, scaledRow - height);
this.context.closePath();
this.context.lineWidth = 1;
this.context.stroke();
}
}

hideMinimap() {
this.canvas.style.display = "none";
}

showMinimap() {
this.canvas.style.display = "block";
}

moveMinimapCenterToUrlLocation() {
const url = new URL(window.location.href);
const col = parseInt(url.searchParams.get("col") || "0");
const row = parseInt(url.searchParams.get("row") || "0");
Comment on lines +272 to +273
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding error handling here in case the URL parameters are not present or not valid numbers.

Suggested change
const col = parseInt(url.searchParams.get("col") || "0");
const row = parseInt(url.searchParams.get("row") || "0");
let col, row;
try {
col = parseInt(url.searchParams.get("col") || "0");
row = parseInt(url.searchParams.get("row") || "0");
if (isNaN(col) || isNaN(row)) {
throw new Error("Invalid URL parameters");
}
} catch (error) {
console.error("Error parsing URL parameters:", error);
col = 0;
row = 0;
}

this.displayRange.minCol = col - MINIMAP_CONFIG.MAP_COLS_WIDTH / 2;
this.displayRange.maxCol = col + MINIMAP_CONFIG.MAP_COLS_WIDTH / 2;
this.displayRange.minRow = row - MINIMAP_CONFIG.MAP_ROWS_HEIGHT / 2;
this.displayRange.maxRow = row + MINIMAP_CONFIG.MAP_ROWS_HEIGHT / 2;
this.computeScaledCoords();
}

update() {
this.draw();
}

private handleMouseDown = (event: MouseEvent) => {
this.isDragging = true;
this.moveCamera(event);
}

private handleMouseMove = (event: MouseEvent) => {
if (this.isDragging) {
this.moveCamera(event);
}
}

private handleMouseUp = () => {
this.isDragging = false;
}

private moveCamera(event: MouseEvent) {
const { col, row } = this.getMousePosition(event);
this.worldmapScene.moveCameraToColRow(col, row, 0);
}

private moveMapRange(direction: string) {
const colShift = (this.displayRange.maxCol - this.displayRange.minCol) / 4;
const rowShift = (this.displayRange.maxRow - this.displayRange.minRow) / 4;

switch (direction) {
case 'left':
this.displayRange.minCol -= colShift;
this.displayRange.maxCol -= colShift;
break;
case 'right':
this.displayRange.minCol += colShift;
this.displayRange.maxCol += colShift;
break;
case 'top':
this.displayRange.minRow -= rowShift;
this.displayRange.maxRow -= rowShift;
break;
case 'bottom':
this.displayRange.minRow += rowShift;
this.displayRange.maxRow += rowShift;
break;
default:
return;
}

this.scaleX = this.canvas.width / (this.displayRange.maxCol - this.displayRange.minCol);
this.scaleY = this.canvas.height / (this.displayRange.maxRow - this.displayRange.minRow);
this.computeScaledCoords();
}

handleClick = (event: MouseEvent) => {
event.stopPropagation();
const { col, row, x, y } = this.getMousePosition(event);

const borderWidthX = this.canvas.width * this.BORDER_WIDTH_PERCENT;
const borderWidthY = this.canvas.height * this.BORDER_WIDTH_PERCENT;

if (x < borderWidthX) {
this.moveMapRange('left');
} else if (x > this.canvas.width - borderWidthX) {
this.moveMapRange('right');
} else if (y < borderWidthY) {
this.moveMapRange('top');
} else if (y > this.canvas.height - borderWidthY) {
this.moveMapRange('bottom');
}
this.worldmapScene.moveCameraToColRow(col, row, 0);
}
}

export default Minimap;
3 changes: 2 additions & 1 deletion client/src/three/scenes/HexagonScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ export abstract class HexagonScene {
mesh.position.set(x, -0.05, z);
mesh.receiveShadow = true;
// disable raycast
mesh.raycast = () => {};
mesh.raycast = () => { };

this.scene.add(mesh);
}
Expand Down Expand Up @@ -428,4 +428,5 @@ export abstract class HexagonScene {
protected abstract onHexagonRightClick(hexCoords: HexPosition): void;
public abstract setup(): void;
public abstract moveCameraToURLLocation(): void;
public abstract onSwitchOff(): void;
}
8 changes: 5 additions & 3 deletions client/src/three/scenes/Hexception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export default class HexceptionScene extends HexagonScene {
this.modelLoadPromises.push(loadPromise);
}

Promise.all(this.modelLoadPromises).then(() => {});
Promise.all(this.modelLoadPromises).then(() => { });
}

setup() {
Expand Down Expand Up @@ -218,6 +218,8 @@ export default class HexceptionScene extends HexagonScene {
this.moveCameraToURLLocation();
}

onSwitchOff() { }

protected onHexagonClick(hexCoords: HexPosition | null): void {
if (hexCoords === null) return;
const normalizedCoords = { col: hexCoords.col, row: hexCoords.row };
Expand Down Expand Up @@ -269,8 +271,8 @@ export default class HexceptionScene extends HexagonScene {
this.buildingPreview?.resetBuildingColor();
}
}
protected onHexagonRightClick(): void {}
protected onHexagonDoubleClick(): void {}
protected onHexagonRightClick(): void { }
protected onHexagonDoubleClick(): void { }

public moveCameraToURLLocation() {
this.moveCameraToColRow(10, 10, 0);
Expand Down
Loading
Loading